Skip to content

fix(core): plugin startup elapsed time is durationMs, the unit-bearing name its spec contract declares - #16057

Merged
zhuangjianguo merged 5 commits into
mainfrom
claude/issue-15820-plugin-startup-duration-name
Sep 6, 2026
Merged

fix(core): plugin startup elapsed time is durationMs, the unit-bearing name its spec contract declares#16057
zhuangjianguo merged 5 commits into
mainfrom
claude/issue-15820-plugin-startup-duration-name

Conversation

@claude

@claude claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #15820

PluginStartupResult.startTime has always been assigned Date.now() - startTime, an elapsed duration, on both the success and the failure path. The name asserts the opposite of the value: a reader who correctly takes startTime for an instant and writes Date.now() - result.startTime gets an age near the epoch rather than a wait. An ambiguous name makes someone stop and check; this one lets them proceed confidently wrong.

The root cause is a declared-versus-enforced divergence, not drift. packages/spec/src/kernel/startup-orchestrator.zod.ts declares the same measure on the same result — the outcome of starting one plugin — as durationMs: z.number().min(0).describe('Time taken to start the plugin in milliseconds') (:173 on origin/main f377394ae) and tombstones the bare spelling at :176 with retiredKey(...): "PluginStartupResult.duration was renamed to durationMs in @objectstack/spec 17 — the unit of a duration-shaped number lives in the key name, not only in the describe prose. Rename the key to durationMs". So the contract surface was already correct and packages/core had drifted away from it. The same computation already has an honest name twelve lines above the defect in the same file: PluginLoadResult.loadTime (plugin-loader.ts:85, assigned at :182). This PR aligns core to the contract that exists; it invents no name and does not touch packages/spec.

Contract-review patch round — head 4458bcd49, discharging verdict 5555409410 on 4d20aa70. The first head of this PR spelled the new member duration, reasoning from the origin/main of 2026-09-05 (c463d03e), where the spec still declared duration. That premise is stale: #15626 (e9fcd6bbd) landed the rename, the tombstone and the gate packages/spec/scripts/check-duration-unit-keys.ts (packages/spec/package.json check:duration-unit-keys) on main. Two maintainer rulings make the rule govern every runtime-emitted duration (5518649320, 5548763981), and a per-plugin startup elapsed time on a public result type is one. The patch commit renames the member to durationMs, follows at both emit sites and in the two pin tests, and corrects the provenance sentence — in the interface JSDoc that ships inside the published dist/index.d.ts, in kernel.ts's {@link}, in the changeset and in this body — which had cited the now-retired PluginStartupResultSchema.duration. Nothing else in the round changed: the startTime deprecation route, getPluginStartupDurations() with the getPluginMetrics() alias, and the minor changeset are as reviewed. No ADR-0087 treatment is owed on the core side: the spec's own tombstone entry, packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginStartupResult__duration.ts, records that core's PluginStartupResult is a different type and not a reader of the schema; core simply does not adopt the retired spelling.

One provenance claim in the card did not survive re-measurement, and the wording here is deliberate because of it. The card attributed the core-to-spec mapping to packages/spec/api-surface/contracts.json:218 and to the PluginStartupResult rows in the package CHANGELOGs. Measured: contracts.json:218 is packages/spec's own export listing, and the CHANGELOG row describes a consolidation inside packages/spec — its own text says "Neither side had any consumer outside spec". packages/core neither imports nor references startup-orchestrator, and the two PluginStartupResult declarations share a name and no shape. So there is no declared mapping row, and this PR does not claim one. The reason to take the contract's name is undamaged and is stated on its own terms: packages/spec is this repo's one contract surface, it names this measure durationMs, and core produced the same measure under a name that says the opposite. That is enough, and it does not depend on a mapping row.

What changed — three sites, all additive

Site Before After
plugin-loader.ts interface member startTime?: number durationMs?: number added; startTime kept, still populated, now @deprecated with a doc comment stating plainly that it holds elapsed milliseconds and not an instant
kernel.ts private field pluginStartTimes pluginStartupDurations (private; renamed outright)
kernel.ts public getter getPluginMetrics() getPluginStartupDurations() added; getPluginMetrics() becomes a @deprecated delegating alias returning the same map

Nothing is removed, so no consumer has to change on this release. That is deliberate and is the ADR-0087 route for a public type member rather than deleting the key: L1 in the ADR's ladder — the old shape keeps working while the fleet moves — instead of an L3 break. Read result.durationMs where you read result.startTime, and kernel.getPluginStartupDurations() where you called kernel.getPluginMetrics(); the values are identical, so the move can be made at leisure.

packages/core/ADVANCED_FEATURES.md and packages/core/examples/kernel-features-example.ts are updated so the in-package docs stop teaching the old name. skills/objectstack-platform/SKILL.md documents getPluginMetrics() and is deliberately not touched: the method still exists and still behaves identically, so that page is not wrong, and skills/** is a governed surface a code PR should not ride on.

Clause-② — measured at 4458bcd49, both limbs answered separately

Measured with the dist/index.d.ts ablation on @objectstack/core at 4458bcd49: build at head, git restore --source=e1d4f9e3f (the merge base) on the two changed sources, rebuild, read the built declaration file, restore with git checkout HEAD -- on the two paths and prove it, rebuild. Mutation on disk before the base build: both source blobs equal to the merge-base blobs (81a869b5f / 24c3c31c5), durationMs count 0 in both files. Restore: both blobs equal to the HEAD blobs (d541278af / a368dca10), git diff HEAD empty, and the rebuilt head dist/index.d.ts byte-identical to the pre-ablation one (cmp). Both builds proven by the dist/index.d.ts mtime moving (178871074817887111371788711148).

Limb 1 — does any exported symbol or signature move? YES. Base-to-head, the published dist/index.d.ts differs by 38 lines: PluginStartupResult gains durationMs?: number (head 1, base 0), ObjectKernel gains getPluginStartupDurations() returning a Map of plugin name to number (head 2, base 0), and the emitted private member changes name from private pluginStartTimes; to private pluginStartupDurations; (a private member's existence is visible in the .d.ts; its signature is not). Read by exported-name set as well: base 260 names, head 260 names, added none, removed none (firing control: PluginStartupResult and ObjectKernel both in the set) — the widening is member-level and additive, no top-level export moves. duration?: number appears 0 times in either build; .d.cts agrees with .d.ts on every count.

Limb 2 — is any request newly accepted or rejected? NO. No schema, parser or validation path is touched and packages/spec is untouched. Every input the kernel accepted before it accepts now, and every rejection is unchanged. The only runtime difference is that two extra result members are populated with a value that was already computed.

Limb 1 fires on its own, so this is Clause-②: yesneeds:contract-review on this PR and on the card, and the PR stays draft until that review clears. The verdict recorded for 4d20aa70 does not cover this head; a re-review is owed on 4458bcd49.

The #14478 convergence question, re-answered on today's main

The first head answered duration because, at base e1d4f9e3f, check:duration-unit-keys did not exist in the tree. On origin/main f377394ae it does (packages/spec/package.json:290, landed e9fcd6bbd), and the schema spells the key durationMs with duration retired. The card's own "or a unit-bearing name, if the convention landing in #14478 settles on one" has settled. Two readings of that gate, both stated because they answer different questions:

  • Its population is numeric Zod chains under every workspace member's src/** (its header; and --list measured on this tree: 0 rows from packages/core/, 0 from plugin-loader.ts, against 8 rows from startup-orchestrator.zod.ts). A TypeScript interface in packages/core is invisible to it, so its green is not evidence that this rename is right; the evidence is the diff and the built declaration above.
  • Run anyway on the tree CI's merge ref will see — this head merged with f377394ae in a scratch worktree, never pushed, 0 conflicts, my three core files byte-equal to the branch head and the spec schema byte-equal to main: ✓ check:duration-unit-keys — 215 duration-shaped numeric key(s) across 2326 source file(s) all carry their unit in the key name (or in a sibling unit, or under a declared exemption: 6 declared EpochMs instant(s), 11 declared externalVocabulary mirror(s)); zero offenders, no baseline. On this branch's own tree it exits 1 with 71 offenders — 70 in packages/spec, 1 in packages/drivers/driver-turso, 0 in packages/core — the merge-base-era spellings of files this PR does not touch (0 packages/spec files in the diff), all already renamed on main. Not a finding about this diff.

The card's zero — re-run with a firing control, and one PM assumption came back FALSE

The card measured no readers of getPluginMetrics() or pluginStartTimes outside packages/core/src/kernel.ts, reverse-checked against PluginStartupResult. Re-run whole-repo at base:

  • pluginStartTimeszero outside kernel.ts holds: 3 hits, all in kernel.ts.
  • getPluginMetricsthe zero does not hold. Seven hits outside kernel.ts: packages/core/ADVANCED_FEATURES.md (2), packages/core/examples/kernel-features-example.ts (1), packages/core/src/kernel.test.ts (2), and skills/objectstack-platform/SKILL.md (2). The card's sweep was scoped to packages apps examples, which does not reach skills/.
  • Reverse control fires: PluginStartupResult returns 40-plus hits across the repo, so the search reaches.
  • Sibling repo objectui at its current checkout: zero hits for all three symbols, against a firing control (ObjectKernel / @objectstack/core hits across changesets, docs and packages/app-shell).

No promotion to p1. Every one of those readers treats the value as a duration already (${duration}ms, "startup ms", "per-plugin startup durations", toBeGreaterThan(0)); none does instant arithmetic on it, and no out-of-repo consumer was measurable. Worth recording separately: PluginStartupResult is exported but no public method of @objectstack/core returns one — startPluginWithTimeout is private — so an external consumer would have to construct the shape itself to be misled by it.

Tests

Two pin tests assert the meaning of the number, not merely that one is present. toBeGreaterThan(0) cannot separate the two readings, because an epoch-millisecond instant passes it too; a ceiling can, since any instant today is around 1.7e12, orders of magnitude above any plugin's start(). The pins survive the rename unchanged apart from reading .durationMs; their mutation test at the first head (emit the start instant where the duration belongs) failed both with expected 1788646580220 to be less than 1000000000, the epoch-instant reading the pin exists to catch, and the review re-ran it independently.

Package suite and gates at 4458bcd49 (the final commit), worktree at the branch head:

  • pnpm --filter @objectstack/core testTest Files 50 passed (50), Tests 1206 passed (1206).
  • pnpm --filter @objectstack/core typecheck — exit 0; check:test-typecheck: OK — @objectstack/core's test layer compiles under packages/core/tsconfig.test.json; 4 file(s) / 4 error(s) / 4 pinned signature(s) held.
  • pnpm lint — the full repo-wide run, exit 0, 0 problems. Not narrowed.
  • pnpm check:nul-bytescheck-nul-bytes: OK (scanned 7739 text file(s) ... no raw ASCII control bytes); a control-byte self-scan of the four edited files: no hits.
  • node scripts/check-adr-0087-registration.mjs --base e1d4f9e3f --head 4458bcd49 (origin/main's copy of the script; the branch's differs) — this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).
  • node scripts/check-changeset-no-major.mjs --base e1d4f9e3f --head 4458bcd49 (origin/main's copy) — This diff introduces no major bump; its level axis prints NOT MEASURED locally (no pull_request payload to read a declaration from) — CI has the payload.
  • node scripts/check-empty-changeset.mjs --base e1d4f9e3fNo empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).

And on the scratch merge tree described above, so that the gates read the scripts and the packages/spec CI's merge ref will use:

  • pnpm --filter @objectstack/core testTest Files 50 passed (50), Tests 1217 passed (1217) (main added eleven core tests since the merge base); typecheckcheck:test-typecheck: OK.
  • The 54 families node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack derives for the six pinned diff paths (git diff --name-only e1d4f9e3f 4458bcd49; origin/main's copy of the script at f377394ae): 52 exit 0; 2 exit 3 = PREREQUISITE NOT MET, not measured — check:dual-build-cjs-loads (76 packages unbuilt in the scratch tree; its own text: "This is NOT a pass: nothing was measured") and check:type-check-debt (--re-measure reports a stale tier itemisation on @objectstack/spec-monorepo, a maintainer's act to re-tally; its coverage half printed OK — 75/79 workspace packages type-checked). check:dts-closure swept the 4 built packages — core, metadata-core, spec, types are the only dist/ directories in that tree — 41/41 declared declaration file(s) present. Six further families the derivation marks NOT RUNNABLE LOCALLY ($RUNNER_TEMP, matrix shards) are CI's.

Every exit code above was captured before any pipe (redirect-then-read), and each verdict is the gate's own printed line.

Changeset level

minor, and the act decides it rather than the fix( commit type: this is a purely additive widening of a published package's public surface (a new member on an exported interface, a new method on ObjectKernel), which the Check Changeset step's WHICH LEVEL prose puts at least at minor. Nothing is removed, so there is no breaking declaration and no ADR-0087 ledger entry — the changeset states that disposition in writing anyway, and names the removal of the deprecated spellings as the change that will carry one.

Out-of-scope finding, filed not fixed

Re-deriving the anchors surfaced a wider question that is not fixed here: packages/spec's whole kernel startup-orchestrator contract — IStartupOrchestrator, PluginStartupResultSchema, StartupOptionsSchema, StartupOrchestrationResultSchema — is declared, exported and documented at content/docs/references/kernel/startup-orchestrator.mdx, and is implemented, parsed and consumed by nothing in the repo (zero implementers and zero schema consumers outside packages/spec, against a firing control). Filed as #16059 with the measurements, unassigned; it has since been ruled (card comment 5560225831) to land after this card so the two do not edit the same declaration concurrently. It is a packages/spec question and folding it in here would have hidden it inside a rename.

Authored by Claude Code in session session_01ARYe3yQTQCUFm5qPYNgKaJ (https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ); the contract-review patch round ran in the same session.


Generated by Claude Code

claude Bot and others added 3 commits September 5, 2026 22:08
…ec contract already uses

`PluginStartupResult.startTime` has always carried `Date.now() - startTime`, an
elapsed duration, so the name asserts the opposite of the value: a reader who
correctly takes it for an instant and writes `Date.now() - result.startTime`
gets an age near the epoch. `packages/spec/src/kernel/startup-orchestrator.zod.ts`
already declares the correct name for the same measure (`duration`, "Time taken
to start the plugin in milliseconds"), and `PluginLoadResult.loadTime` twelve
lines above the defect already spells the identical computation truthfully -- so
this is a declared-vs-enforced divergence between `packages/core` and the spec
contract it implements, not a naming preference.

Three sites, all additive (nothing is removed, so no consumer changes):
  - `PluginStartupResult` gains `duration?: number`; `startTime` stays,
    populated with the same value, marked `@deprecated` with a doc comment that
    states plainly what it holds (ADR-0087 L1 -- the old shape keeps working).
  - the private `pluginStartTimes` map is renamed `pluginStartupDurations`
    (private; measured zero readers outside `kernel.ts`).
  - `getPluginStartupDurations()` is added and `getPluginMetrics()` becomes a
    deprecated delegating alias.

Pin tests assert the value is a bounded elapsed duration rather than an
epoch-millisecond instant, on the success and the failure path -- the assertion
`toBeGreaterThan(0)` could never make.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
`PluginMetadata` requires `init`, so the object-literal `as` casts tripped
TS2352 under `tsconfig.test.json`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…ctstack/core

Additive widening of a published package's public surface (a new exported
member on `PluginStartupResult`, a new method on `ObjectKernel`) takes at
least `minor` per the `Check Changeset` step's WHICH LEVEL prose; the act
wins over the `fix(` commit type.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 5, 2026
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/core, touching 8 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/core/ADVANCED_FEATURES.md, packages/core/examples/kernel-features-example.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/knowledge-rag.mdx (via ObjectKernel (symbol, a top-level class))
  • content/docs/automation/flows.mdx (via durationMs (symbol, a field of interface PluginStartupResult, an authorable key of kernel/PluginStartupResult))
  • content/docs/kernel/architecture.mdx (via ObjectKernel (symbol, a top-level class))
  • content/docs/kernel/events.mdx (via ObjectKernel (symbol, a top-level class))
  • content/docs/kernel/index.mdx (via ObjectKernel (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx (via ObjectKernel (symbol, a top-level class))
  • content/docs/plugins/anatomy.mdx (via ObjectKernel (symbol, a top-level class))
  • content/docs/plugins/packages.mdx (via ObjectKernel (symbol, a top-level class))
  • content/docs/protocol/kernel/index.mdx (via ObjectKernel (symbol, a top-level class))
  • content/docs/protocol/kernel/lifecycle.mdx (via ObjectKernel (symbol, a top-level class))

2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via ObjectKernel (symbol, a top-level class))
  • content/docs/releases/v17.mdx (via durationMs (symbol, a field of interface PluginStartupResult, an authorable key of kernel/PluginStartupResult))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/core/ADVANCED_FEATURES.md, packages/core/examples/kernel-features-example.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 24 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f377394ae2c2510636cdb12a34a49b5a442457d6packageMentionDocs.

Which tree this was computed on

This run read content/docs from 8f61445a7a078f26df3d8ce2cf9b39c6421358c1 — the merge of head 4458bcd49fbb6c27d2e8ec8e8f64fc4c8471b9ee into base f377394ae2c2510636cdb12a34a49b5a442457d6, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8f61445a7a078f26df3d8ce2cf9b39c6421358c1 && git checkout 8f61445a7a078f26df3d8ce2cf9b39c6421358c1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f377394ae2c2510636cdb12a34a49b5a442457d6 4458bcd49fbb6c27d2e8ec8e8f64fc4c8471b9ee && git checkout -B drift-repro f377394ae2c2510636cdb12a34a49b5a442457d6 && git merge --no-ff 4458bcd49fbb6c27d2e8ec8e8f64fc4c8471b9ee

node scripts/docs-audit/affected-docs.mjs --json f377394ae2c2510636cdb12a34a49b5a442457d6

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs f377394ae2c2510636cdb12a34a49b5a442457d6 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…esult implements'

Measured: `packages/core` neither imports nor references
`packages/spec/src/kernel/startup-orchestrator.zod.ts`, and nothing in the repo
implements `IStartupOrchestrator`. The two `PluginStartupResult` declarations
describe the same domain result and share no shape, so 'implements' overstated a
relationship that does not exist in code. The reason to take the contract's name
is unchanged: it is the name the spec surface declares for this measure.

Filed separately as the wider question this made visible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ

Copy link
Copy Markdown
Contributor

Contract review (clause ②) — NOT PASSED — one rename owed — PR #16057 at head 4d20aa70

Director seat, summon #15, session_01TezFG8ZMrNH6n5VTNpPpdH (os-zhuang), 2026-09-05T23:07Z, on the maintainer's instruction 「你应该按批次执行完所有的契约复审」. Tier fuse: get_session this session reads session_context.model = last_served_model = CONTRACT_REVIEW_TIER. Readings from the PR diff, origin/main c463d03e, the stack branch claude/issue-14478-duration-unit-in-key-name at 43cc4659 (stack 2/6 landed 22:40Z), and PR #15906 (stack 3/6). Dev report 5555263077 (22:38Z) read afterwards as cross-check.

Implemented-by: session_01ARYe3yQTQCUFm5qPYNgKaJ os-dev round (branch claude/issue-15820-plugin-startup-duration-name)
Reviewed-by: session_01TezFG8ZMrNH6n5VTNpPpdH

① Derived judgments

# claim reading verdict
1 PluginStartupResult.startTime holds Date.now() - startTime on both paths Diff context: const duration = Date.now() - startTime; … startTime: duration on the success and the failure return. Triage (5550872117) reproduced line for line. correct
2 The spec contract for the same measure declares duration origin/main packages/spec/src/kernel/startup-orchestrator.zod.ts:149 reads duration: z.number().min(0).describe('Time taken to start the plugin in milliseconds'). True on main today, false on the tree this repo has already ruled into being: PR #15906 (stack 3/6, tier-PASSed, queued to land into the stack branch) renames it to durationMs and tombstones duration (retired-keys/18.kernel__PluginStartupResult__duration.ts; startup-orchestrator.mdx: "duration never [REMOVED] … renamed to durationMs in @objectstack/spec 17"). The same PR renames StartupOptions.timeout → timeoutMs and totalDuration → totalDurationMs. stale premise
3 "durationMs cannot be the answer from a convention that is not yet in the repo" The gate is in the repo: packages/spec/scripts/check-duration-unit-keys.ts (759 lines) is on the stack branch since 2/6 landed at 22:40Z, seven minutes after this head. The convention is not a preference to wait on — it is two maintainer rulings: ruling B (5518649320: a duration-shaped number key carries its unit in its name, no baseline) and the population ruling B (5548763981: the rule governs every authored duration and every runtime-emitted duration). A per-plugin startup elapsed time on a public result type is a runtime-emitted duration. That the gate cannot see a TS interface in packages/core (it scans zod schemas; #15682 widens it to workspace zod schemas, not to interfaces) does not exempt the key from the rule — the maintainer's standing rule is 「开发应该对齐协议」, and the protocol this key aligns to spells it durationMs. incorrect
4 Additive widening: duration?: number added, startTime kept @deprecated; getPluginStartupDurations() added, getPluginMetrics() deprecated alias; private map renamed Diff read; dist/index.d.ts ablation as declared (37 lines, both legs proven to reach dist). Limb 1 fires, limb 2 does not. Clause-②: yes correctly declared and hung on PR and card. ADR-0087 L1 (old spellings kept, populated) is the right route for a public type member. correct, modulo the key name
5 Pins assert meaning (ceiling INSTANT_FLOOR_MS = 1e9), mutation proven Two tests; mutation to the instant fails both with the epoch-magnitude assertion. Sound. correct
6 Changeset @objectstack/core minor, no ADR-0087 entry (nothing retired) Correct level for an additive public widening; the group is fixed so the level is moot in effect. No entry owed while nothing is removed. correct
7 skills/objectstack-platform/SKILL.md deliberately untouched (governed; still true) Accepted — a code PR must not ride on skills/**. The page will teach a deprecated alias until the removal card; that is the removal card's docs edit. accepted

The one change owed

Spell the new member durationMs?: number — the key PluginStartupResult in @objectstack/spec will carry once #15906 lands, and the spelling both rulings require for a runtime-emitted duration. Consequential edits, same round: the three doc comments and the changeset that cite PluginStartupResultSchema.duration cite durationMs; the pin tests read .durationMs; startTime: duration stays as the deprecated alias exactly as written. The method name getPluginStartupDurations() is not a key and may stay; the local const duration is not surface. Landing order is unconstrained — packages/core does not import the spec schema, so this can land before or after the stack.

Not asked: no change to the deprecation route, tests, or changeset level; no packages/spec edit here (#15906 owns that key).

Boundary flags

Evidence and gate

Checks on 4d20aa70: 33 success / 3 skipped / 0 red. check-governed-merges --test not run — no landing this round. needs:contract-review stays on this PR and on #15820. Patch round routed to the dispatching seat session_01ARYe3yQTQCUFm5qPYNgKaJ: push the rename on this branch (⛔ no rebase), post the new head here; this seat re-reads the delta and clears both carriers in one stroke.


Generated by Claude Code

…it-bearing name its spec contract declares

Discharges contract review 5555409410 on PR #16057 (head 4d20aa7): the new
published member was spelled `duration`, a key `packages/spec` has since
retired. Measured on origin/main f377394:
`packages/spec/src/kernel/startup-orchestrator.zod.ts:173` declares
`durationMs`; `:176` tombstones `duration` with `retiredKey()` ("Rename the
key to `durationMs`"); the gate
`packages/spec/scripts/check-duration-unit-keys.ts` is in the tree (landed
e9fcd6b). Two maintainer rulings make the rule govern every runtime-emitted
duration, and a per-plugin startup elapsed time on a public result type is one.

- `PluginStartupResult.durationMs?: number` replaces the never-released
  `duration?: number`; both emit sites in `kernel.ts` follow; `startTime`
  keeps its deprecated-alias treatment exactly as before.
- The provenance sentence in the interface JSDoc (which ships in the published
  `dist/index.d.ts`), in `kernel.ts`'s `{@link}` and in the changeset cited
  the retired `PluginStartupResultSchema.duration`; all three now cite
  `durationMs` and say the bare spelling is retired.
- The two pin tests read `.durationMs`; their ceiling assertion is unchanged.

No ADR-0087 treatment on the core side: the spec tombstone entry
(`packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginStartupResult__duration.ts`)
records that core's interface is a different type and not a reader of the
schema; core simply does not adopt the retired spelling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
@claude

claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

Contract-review patch round — verdict 5555409410 (bound to head 4d20aa70) discharged at head 4458bcd49. That verdict is now void by construction — a clear binds to a head — and a re-review is owed on the new head. needs:contract-review stays on this PR and on #15820; the PR stays draft.

What changed, one commit (4458bcd49):

  • PluginStartupResult.durationMs?: number replaces duration?: number; both emit sites in kernel.ts follow (durationMs: duration); the two pin tests read .durationMs, ceiling assertion unchanged. startTime keeps its @deprecated alias treatment, getPluginStartupDurations() / getPluginMetrics() are as reviewed, the changeset stays minor.
  • The false provenance sentence — the interface JSDoc that ships in the published dist/index.d.ts, kernel.ts's {@link}, the changeset and the PR body all cited the retired PluginStartupResultSchema.duration — now cites durationMs and says the bare spelling is retired. Built at this head, dist/index.d.ts and .d.cts carry durationMs?: number once, duration?: number zero times, the retired citation zero times.
  • No ADR-0087 treatment on the core side: the spec's tombstone entry packages/spec/src/migrations/entries/retired-keys/18.kernel__PluginStartupResult__duration.ts itself records that core's interface is a different type and not a reader of the schema; core simply does not adopt the retired spelling.

Re-derived on origin/main f377394ae: startup-orchestrator.zod.ts:173 declares durationMs, :176 is duration: retiredKey(...), and the gate is present (packages/spec/package.json check:duration-unit-keys, landed e9fcd6bbd).

The gate, run on a scratch merge of this head with f377394ae (the tree CI's merge ref sees; never pushed):

✓ check:duration-unit-keys — 215 duration-shaped numeric key(s) across 2326 source file(s) all carry their unit in the key name (or in a sibling `unit`, or under a declared exemption: 6 declared `EpochMs` instant(s), 11 declared `externalVocabulary` mirror(s)); zero offenders, no baseline.

Stated with it: that gate's population is numeric Zod chains — --list on this tree shows 0 rows from packages/core/ against 8 from startup-orchestrator.zod.ts — so a TS interface is invisible to it, and its green is not the evidence for the rename; the diff and the built declaration are. On the branch's own tree it exits 1 with 71 offenders (70 in packages/spec, 1 in driver-turso, 0 in packages/core): the merge-base-era spellings of files this PR does not touch, all already renamed on main.

Clause-② re-measured at 4458bcd49 (dist ablation, both legs proven on disk; exported-name set 260 → 260, nothing added or removed): limb 1 fires (durationMs?: number, getPluginStartupDurations()), limb 2 does not — Clause-②: yes, unchanged. The card's claim comment now carries the declaration in the fixed spelling; node scripts/pm/check-clause2-carriers.mjs --pair 16057 exits 0: "the clause-② declaration is readable in the fixed spelling and both carriers agree".

Local readings: core test 50 files / 1206 tests, typecheck OK, full pnpm lint exit 0; on the merge tree core 50 / 1217, typecheck OK, and 52 of the 54 dispatch-gates families exit 0 (2 exit 3 = not measured: check:dual-build-cjs-loads, 76 packages unbuilt there; check:type-check-debt's re-measure, a maintainer's act). Full readings are in the PR body.


Generated by Claude Code

@claude

claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

Contract review (clause ②) — PASS, bound to head 4458bcd49fbb6c27d2e8ec8e8f64fc4c8471b9ee

domain:engine dispatching seat, session session_01ARYe3yQTQCUFm5qPYNgKaJ, 2026-09-06T22:5xZ. This is the replacement verdict. The human verdict 5555409410 (NOT PASSED — one rename owed) was bound to 4d20aa70 and is void by construction once the head moved; it is discharged, not overruled.

Reviewed at CONTRACT_REVIEW_TIER by a dedicated reviewer seat; every citation re-verified against the tree by this seat. Tier disclosure as recorded on #16308 (5562566510): the reviewer's tier is asserted, never measuredget_session describes the session, not the agent calling it — and the substitute control is that the citations are checkable, which is what was done.

1. The rename owed by 5555409410 is discharged, and the measurement is member-level

plugin-loader.ts:105 is durationMs?: number; — verified. The name set is 260 → 260, and ⭐ that number is worthless on its own; the member-level ablation is the reading. Three legs on disk (HEAD → merge-base sources swapped by blob → HEAD restored, leg 3's index.d.ts byte-identical to leg 1), yielding a 38-line diff in exactly two exported declarations:

  • interface PluginStartupResultdurationMs?: number ADDED; startTime?: number unchanged in type and optionality, now carrying @deprecated.
  • class ObjectKernelgetPluginStartupDurations(): Map<string, number> ADDED; getPluginMetrics() signature unchanged, now @deprecated; the private field renamed (private pluginStartTimes;private pluginStartupDurations;).

limb 1 fires, additively. Clause-②: yes is correct.

2. Limb 2 does not fire, and both old spellings are still live

startTime?: number 1→1 and getPluginMetrics(): Map<string, number> 1→1 in the built .d.ts, and both remain populated and callable: kernel.ts:712-715 and :725-728 set startTime: duration on the success and the failure path; kernel.ts:557-559 returns this.getPluginStartupDurations() — a fresh Map copy, exactly as before. No spec file is touched; no zod schema, parser or accept path is in the diff. Nothing that compiles today stops compiling.

Consumer sweep with a firing control: the sibling objectui has 0 files for getPluginMetrics / pluginStartTimes / getPluginStartupDurations / PluginStartupResult, against controls of 13 files for @objectstack/core and 1 for ObjectKernel. That zero is a reading.

3. ⚠️ Two corrections to this seat's own dispatch, both from the reviewer

My Zone 2.4 named a file that does not exist. I wrote scripts/pm/check-duration-unit-keys.mjs. The gate is packages/spec/scripts/check-duration-unit-keys.ts, and it exists only on origin/main — it is absent from this PR's tree entirely, so CI sees it on the merge ref only. My conclusion (that the gate is structurally blind to a plain TS interface) survives, but it was re-earned properly rather than inherited: run from main's copy, --list gives 288 census rows with 0 from packages/core/ and 0 from plugin-loader.ts, against 8 from startup-orchestrator.zod.ts; and a --root ablation on a scratch tree gives a copy of plugin-loader.ts (carrying loadTime?, durationMs?, startTime?) 0 rows while a zod positive control gives 2 rows, 1 offender. ⇒ green there is not evidence, and now that is measured.

My Zone 2.2 invoked ADR-0087 L1 too loosely. docs/adr/0087…:31 defines L1 as "a versioned conversion layer in the spec: old shapes accepted and centrally converted at load (D2)" — a mechanism for spec shapes. This PR has no spec shape and no conversion; it borrows L1's outcome ("no consumer action at all"), not its mechanism. The limb-2 conclusion stands on the measurement, not on the label.

4. Findings — all notes, none blocking

The one worth reading. The JSDoc that ships inside dist/index.d.ts (plugin-loader.ts:97-101) cites PluginStartupResultSchema.durationMs "where the bare duration spelling is retired". This seat checked it on both trees: on the PR's own tree startup-orchestrator.zod.ts:149 still reads duration: z.number()… with 0 hits for durationMs, while on origin/main :173 reads durationMs: and :176 carries the retiredKey tombstone. So the shipped sentence is false on the PR's tree and true on the merge target — and git merge-tree confirms this PR merges clean into main, so it is true at release. Note only; the PR body already discloses it.

Also noted: the private field rename is visible as .d.ts text but is not nameable by any consumer; durationMs? is correctly optional because the !plugin.start early return at kernel.ts:687-689 emits neither member; and skills/objectstack-platform/SKILL.md:991,1202 still teach the alias — still true, governed surface, correctly untouched.

Changeset: "@objectstack/core": minor is right. pr-automation.yml:667-671 puts a purely additive widening at at least minor and says a fix( commit may raise but never lower it. Not breaking ⇒ no ADR-0087 marker owed.

5. Landing

① this record, ② --pair 16057 exit 0, ③ 36/36 complete, 0 failing. Card #15820 carries no sibling PR, so there is no batching constraint. Released.


Generated by Claude Code

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 6, 2026 22:22
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 6, 2026
Merged via the queue into main with commit 66dc6ab Sep 6, 2026
41 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-15820-plugin-startup-duration-name branch September 6, 2026 23:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

core: PluginStartupResult.startTime carries an elapsed duration, and the spec contract for the same result declares it as duration

3 participants