Skip to content

feat(spec,core)!: the startup contract describes what the kernel produces — retire the orchestrator, declare PluginStartupResult once (#16059) - #18303

Merged
os-warren merged 5 commits into
mainfrom
claude/issue-16059-startup-orchestrator-shipped-shape
Sep 15, 2026
Merged

os-warren merged 5 commits into
mainfrom
claude/issue-16059-startup-orchestrator-shipped-shape

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes #16059

Executes the maintainer ruling (director seat, decision batch #60, 2026-09-06, maintainer reply 「同意」): the spec keeps a startup-result contract and it describes what the kernel actually produces. The never-landed orchestrator is retired under ADR-0049 enforce-or-remove; the one shape that is real is re-declared against what @objectstack/core ships and core now imports it instead of declaring a twin.

What is retired

IStartupOrchestrator (orchestrateStartup / rollback / checkHealth / startWithTimeout) plus the three schemas it tied together — 3 emitted defs, 8 exported names across ./kernel and ./contracts (11 api-surface rows).

retired why
IStartupOrchestrator nothing in any repository implements it or calls it
StartupOptionsSchema / StartupOptions / StartupOptionsParsed only ever the options argument of orchestrateStartup; the kernel reads PluginMetadata.startupTimeout and KernelConfig.rollbackOnFailure
HealthStatusSchema / HealthStatus the return vocabulary of checkHealth and the value of PluginStartupResult.health — no probe system has ever existed behind either
StartupOrchestrationResultSchema / StartupOrchestrationResult the aggregate orchestrateStartup returned; no implementation, so no aggregate was ever built

Route 3 of the spec-property-retirement playbook: no authored document carried any of the three defs, so there is no seam for a D2 conversion and no author to hand a tombstone to. RETIRED_DEFS_BY_MAJOR[18] plus the D3 semantic entry startup-orchestrator-retired are the declaration, and the three json-schema.manifest/kernel.json keys with their 16 authorable-surface/kernel.json lines are deleted deliberately in this change. The build printed all three proofs:

ℹ️  3 schema(s) left the published set since 827cacbf4b79, each declared (#4725):
     - json-schema/kernel/HealthStatus.json — RETIRED_DEFS_BY_MAJOR, major 18.
     - json-schema/kernel/StartupOptions.json — RETIRED_DEFS_BY_MAJOR, major 18.
     - json-schema/kernel/StartupOrchestrationResult.json — RETIRED_DEFS_BY_MAJOR, major 18.

ℹ️  3 baseline deletion(s) since 827cacbf4b79 carry their own proof (#4650):
     - kernel/HealthStatus:* (5 line(s)) — def no longer emitted by this build; …
     - kernel/StartupOptions:* (6 line(s)) — …
     - kernel/StartupOrchestrationResult:* (5 line(s)) — …

What survives, re-declared

PluginStartupResultSchema / PluginStartupResult on both entries, rewritten to the shape ObjectKernel.startPluginWithTimeout() has always returned: pluginName, success, optional durationMs, timedOut, and error as the serializable projection. packages/core/src/plugin-loader.ts no longer declares an interface — it re-exports the spec type, so the two cannot drift again.

Three keys of the surviving def take the tombstone route rather than a bare deletion, registered in RETIRED_KEYS_BY_MAJOR[18], because this def keeps emitting and its type is now imported by @objectstack/core — the tsc channel makes the prescription receivable even though nothing parses the schema:

  • plugin (a nested plugin object the kernel never built) — write pluginName
  • health (a HealthStatus nothing ever filled) — delete it
  • startTime — see below

The one member the ruling left open, and what decided it

The ruling did not settle whether core's deprecated startTime is mirrored or tombstoned, and neither does the playbook: startTime was never on the spec surface, so "retirement" was not its frame. A repo gate decided it. Mirroring it reds check:duration-unit-keys (maintainer ruling B on #14478, no baseline, no allowlist):

[unit-in-prose-not-in-name] packages/spec/src/kernel/startup-orchestrator.zod.ts:136 `startTime`
  — describe names ms but the key name carries no unit.

Neither of that rule's two schema-declared exemptions fits: it is not an EpochMs instant (it holds an elapsed duration, which is exactly the confusion the rule separates) and it mirrors no external standard. The remaining options were to rename it to startTimeMs — minting a spelling nothing has ever produced, for a member core's own TSDoc calls "slated for removal" — or to end the ADR-0087 L1 window here. This PR ends it: the spec tombstones startTime, and ObjectKernel.startPluginWithTimeout() stops setting it.

⚠️ This is the only observable behaviour change in the diff and the only judgement call in it. Everything else removes surfaces nothing read. If the maintainer wants the alias to live one release longer, the cheap reversal is a startTimeMs rename in both packages; say so and it is a one-commit follow-up.

Evidence

Consumers, re-measured on this card (the ruling's assertion was written 2026-09-06; the prerequisite was to re-verify it):

  • objectstack at 827cacbf4b: zero files outside packages/spec/src name StartupOptionsSchema, HealthStatusSchema or StartupOrchestrationResultSchema. IStartupOrchestrator and orchestrateStartup hit 3 and 5 files — every one a released CHANGELOG.md or a generated artifact (api-surface/contracts.json, export-origins/contracts.json). Same-corpus controls on the same query lit up: defineStack 347 files, ManifestSchema 62, PluginStartupResult 15.
  • objectui at the pinned .objectui-sha 53ded82bf7: all six symbols zero, controls defineStack 27 files and ManifestSchema 6.
  • cloud is NOT MEASURED — not attached to this session, so no reading exists for it.

Reverse verification (this is a cross-package type change, so the point is proving core reads the rebuilt .d.ts rather than a cache). From the committed state, health: { healthy: true, checkedAt: 1 } was inserted into the success-path literal in packages/core/src/kernel.ts, proven on disk by occurrence count and by git hash-object differing from the HEAD blob:

--- marker occurrences BEFORE: 0 / AFTER MUTATION: 1 ---
MUTATED LEG EXIT=1
src/kernel.ts(795,17): error TS2322: Type '{ healthy: true; checkedAt: number; }' is not assignable to type 'undefined'.
--- restored blob 55b34588… == HEAD blob 55b34588… ---
RESTORED LEG EXIT=0

The tombstone's never crossed the package boundary and the restore reproduced the HEAD blob byte for byte (git diff HEAD empty).

Same-major bookkeeping. The timeout / timestamp / totalDuration rename registrations that #15678 and #15676 landed in this same unreleased major 18 are KEPT, not absorbed. The playbook's absorption clause is motivated by the conversion table's fixture-disjointness contract, and those three entries record "No D2 conversion" themselves, so that mechanism does not exist here. The tree's own answer agrees: 14 RETIRED_KEYS_BY_MAJOR[18] entries already name keys whose def is retired in the same major 18 (the #15513 incident-response / training / change-management family). Gate (b2) states this steady state explicitly — "an entry naming a key the build no longer emits at all is NOT an error".

Acceptance notes

  • The generated page keeps the frontmatter title: Startup Orchestrator / description: Startup Orchestrator protocol schemas. Those two strings are derived from the module FILENAME by build-docs.ts; the page BODY is rewritten and now describes the result contract. Renaming startup-orchestrator.zod.ts would move the docs route, the export-origins paths and the pin file's module slot, and the ruling names the file to edit rather than to move — so it is noted here, not done.
  • content/docs/references/index.mdx moves 1525 → 1522 schemas and the Kernel row 162 → 159 — the whole-def-removal reading the playbook's ratchet-visibility table predicts, and the counter-evidence to a zero-delta run.
  • packages/spec/src/type-alias-convention.pin.test.ts drops Iso467 / Iso469 with their schemas (785 → 783 pins); Iso468 stays, still isomorphic — every new member is .optional() with no default and no transform.

Generated by Claude Code

@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Sep 15, 2026
@github-actions

github-actions Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/core, @objectstack/spec, touching 32 documentable anchor(s). ⚠️ 14 changed file(s) yielded no anchor (packages/spec/api-surface/contracts.json, packages/spec/api-surface/kernel.json, packages/spec/authorable-defaults/kernel.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/automation/flows.mdx (via durationMs (symbol, a field of interface PluginStartupResult, an authorable key of kernel/PluginStartupResult), durationMs (literal, a string literal on a changed line), timeoutMs (literal, a string literal on a changed line))
  • content/docs/automation/hook-bodies.mdx (via timeoutMs (literal, a string literal on a changed line))
  • content/docs/automation/jobs.mdx (via timeoutMs (literal, a string literal on a changed line))
  • content/docs/automation/webhooks.mdx (via timeoutMs (literal, a string literal on a changed line))
  • content/docs/deployment/environment-variables.mdx (via timeoutMs (literal, a string literal on a changed line))
  • content/docs/getting-started/quick-reference.mdx (via PluginStartupResult (symbol, a top-level interface; a top-level type))
  • content/docs/kernel/cluster.mdx (via RETIRED_DEFS_BY_MAJOR (symbol, a top-level const object))
  • content/docs/kernel/contracts/cache-service.mdx (via memoryUsage (literal, a string literal on a changed line))
  • content/docs/plugins/index.mdx (via healthCheck (literal, a string literal on a changed line))
  • content/docs/protocol/kernel/lifecycle.mdx (via pluginName (symbol, a field of interface PluginStartupResult, an authorable key of kernel/PluginStartupResult), startTime (symbol, a field of interface PluginStartupResult, an authorable key of kernel/PluginStartupResult), healthCheck (literal, a string literal on a changed line), memoryUsage (literal, a string literal on a changed line), pluginName (literal, a string literal on a changed line), rollbackOnFailure (literal, a string literal on a changed line), timeoutMs (literal, a string literal on a changed line))
  • content/docs/protocol/kernel/plugin-spec.mdx (via healthCheck (literal, a string literal on a changed line))
  • content/docs/protocol/knowledge.mdx (via healthCheck (literal, a string literal on a changed line))

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

  • content/docs/releases/v16.mdx (via timeoutMs (literal, a string literal on a changed line))
  • content/docs/releases/v17/17-0.mdx (via healthCheck (literal, a string literal on a changed line), timeoutMs (literal, a string literal on a changed line))
  • content/docs/releases/v17/17-3.mdx (via durationMs (symbol, a field of interface PluginStartupResult, an authorable key of kernel/PluginStartupResult), durationMs (literal, a string literal on a changed line))
  • content/docs/releases/v17/17-4.mdx (via timeoutMs (literal, a string literal on a changed line))

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
  • 14 changed file(s) yielded no anchor (packages/spec/api-surface/contracts.json, packages/spec/api-surface/kernel.json, packages/spec/authorable-defaults/kernel.json, …) — pages documenting those are invisible to this run
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 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 — 141 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 53dd5aaaafbe877a1cc72aef26511bd15c927ecfpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 28e9a06b7dc4ddabf6b5f7a0b8297649c7933214 — the merge of head e345ffa9e179ac123cc983a68714df45db430641 into base 53dd5aaaafbe877a1cc72aef26511bd15c927ecf, 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 28e9a06b7dc4ddabf6b5f7a0b8297649c7933214 && git checkout 28e9a06b7dc4ddabf6b5f7a0b8297649c7933214
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 53dd5aaaafbe877a1cc72aef26511bd15c927ecf e345ffa9e179ac123cc983a68714df45db430641 && git checkout -B drift-repro 53dd5aaaafbe877a1cc72aef26511bd15c927ecf && git merge --no-ff e345ffa9e179ac123cc983a68714df45db430641

node scripts/docs-audit/affected-docs.mjs --json 53dd5aaaafbe877a1cc72aef26511bd15c927ecf

⚠️ 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 53dd5aaaafbe877a1cc72aef26511bd15c927ecf → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e345ffa9e179ac123cc983a68714df45db430641

Reviewed first-hand in a detached worktree at the head (merge base 827cacbf4b, 30 files, +984 / −855; branch tip on the remote is this sha; git status clean). The implementer's report was an input, not evidence; every load-bearing claim below is re-derived.

① Derived judgments

Retired defs (3), judged correct. kernel/HealthStatus, kernel/StartupOptions, kernel/StartupOrchestrationResult leave json-schema.manifest/kernel.json (−3 keys), authorable-surface/kernel.json (−16 lines: 5 + 6 + 5), authorable-defaults/kernel.json (−4), declaration-map/kernel.json (−6), and arrive in RETIRED_DEFS_BY_MAJOR[18] with one entry file each. Route 3 of the playbook (no authored document could carry any of the three; none is reachable from a metadata-type root) is the right route, and the two-half proof the playbook demands is present in the artifacts themselves: the manifest keys are deleted deliberately in this diff and the defs are declared in the table, which is exactly what the build's "3 schema(s) left the published set … each declared (#4725)" and "3 baseline deletion(s) … carry their own proof (#4650)" lines read. Consumer census re-derived at 827cacbf4b with word-boundary git grep, outside packages/spec/src: StartupOptionsSchema / HealthStatusSchema / StartupOrchestrationResultSchema 4 files each (the reference page plus three generated JSON artifacts), IStartupOrchestrator 6 files and orchestrateStartup 3 files (the reference page, released CHANGELOG.md files, api-surface/contracts.json, export-origins/contracts.json) — zero code. Same-corpus controls lit: defineStack 426 files, ManifestSchema 69, PluginStartupResult 17. objectui at the pinned .objectui-sha 53ded82bf7: all eight retired names 0 files; controls defineStack 27, ManifestSchema 6. objectstack-ai/cloud: NOT MEASURED (not attached to this session). The ruling's "nothing implements or consumes them" holds on both measured corpora.

Exported names (8) and api-surface rows (11), judged correct and complete. StartupOptionsSchema, StartupOptions, StartupOptionsParsed, HealthStatusSchema, HealthStatus, StartupOrchestrationResultSchema, StartupOrchestrationResult, IStartupOrchestrator. api-surface/kernel.json −7, api-surface/contracts.json −4 (HealthStatus, IStartupOrchestrator, StartupOptions, StartupOptionsParsed); export-origins mirrors both. Both barrels are export *, so no barrel edit was owed. PluginStartupResult survives on ./contracts (type re-export, as at the base) and PluginStartupResultSchema / PluginStartupResult on ./kernel; the new startup-orchestrator-retirement.test.ts pins absence over every public entry via the export-origins artifact and pins survival, with anti-vacuity floors. type-alias-convention.pin.test.ts drops Iso467 / Iso469 (785 to 783) and keeps Iso468; every new member is .optional() with no default or transform, so the isomorphism still holds.

Re-declared PluginStartupResultSchema, judged right against the tree, not the report. pluginName: string (required), success: boolean (required), durationMs: number.min(0).optional(), error as the serialisable projection (name, message, stack?, code?, optional), timedOut: boolean.optional(); tombstones startTime, duration, plugin, health; type is z.input. durationMs optional is correct: ObjectKernel.startPluginWithTimeout() returns { success: true, pluginName } with no duration for a plugin that declares no start() (kernel.ts:770 at the base and at the head), so the old spec's required durationMs described a value the kernel did not always produce, and the ruling says the spec describes what the kernel ships. The required-to-optional movement IS declared, in the changeset's member table. timedOut matches the kernel's timeout path. The error projection is what the ruling names; the kernel still hands the live Error through and every read site in kernel.ts (lines 461–478) guards with instanceof Error, and the changeset declares the one narrowing this implies for core consumers.

Core re-exports the spec type, verified by reverse verification at THIS head. packages/core/src/plugin-loader.ts now reads export type { PluginStartupResult } from '@objectstack/spec/kernel' and declares no interface. Against a fresh @objectstack/spec build in my worktree: clean tsc --noEmit in packages/core exit 0; inserting health: { healthy: true, checkedAt: 1 } into the success literal of kernel.ts exits 2 with src/kernel.ts(795,17): error TS2322: Type '{ healthy: true; checkedAt: number; }' is not assignable to type 'undefined'; inserting startTime: duration exits 2 with TS2322: Type 'number' is not assignable to type 'undefined'; restore proven by blob identity back to 57d2340cae4ad3f9d12f10932f979ad4941d9980, the head's blob for that path. The tombstones cross the package boundary. (On the implementer's own leg, see ③.)

Retired-keys entries (3), judged correct. kernel/PluginStartupResult:health, :plugin, :startTime are in RETIRED_KEYS_BY_MAJOR[18], each with its own entry file, and each has a matching [RETIRED] line in authorable-surface/kernel.json, so gate (b) is satisfied and (b2) is not tripped (none is live). No conversions/ entry is written and none is owed: the result is emitted by the kernel, never authored, so it is the playbook's "response-surface key with no source to rewrite" case, registered as the D3 semantic entry startup-orchestrator-retired, whose acceptanceCriteria names all three keys. No liveness-ledger row exists for any of these defs, so none was owed.

Docs. content/docs/references/kernel/startup-orchestrator.mdx body is regenerated from the module docblock and now describes exactly what the ruling asked for: what startPluginWithTimeout() returns on each of its three paths, when durationMs is absent, and when timedOut is set (timeout only, absent rather than false on a throw). The frontmatter title: Startup Orchestrator / description: Startup Orchestrator protocol schemas is derived from the FILENAME at packages/spec/scripts/build-docs.ts:450 (zodFile.split('-')…), confirmed; no edit to the module can move it. I read the ruling as satisfied: it names the file to rewrite and the content the page must describe, and both are done. The frontmatter residue is a generator or a file-rename question for a later card, correctly noted and not smuggled in. quick-reference.mdx row and references/index.mdx counts (1525 to 1522, Kernel 162 to 159) are the whole-def-removal reading the playbook predicts. Fences all hold: no path under .claude/, skills/, content/docs/releases/, docs/adr/, and packages/spec/src/shared/retired-key.ts untouched; the playbook is byte-identical between base and head. Not a governed-surface PR.

② Semver level

minor on @objectstack/spec and @objectstack/core is the correct level in this tree. scripts/check-changeset-no-major.mjs refuses a major during the launch window and its header states the convention and its GA end condition; ADR-0087's 2026-09-13 amendment says the same in the ADR's own words ("Pre-GA, a metadata-facing retirement or break ships minor, carrying the BREAKING banner and its ADR-0087 disposition entry"). Run against the merge base: check-changeset-no-major exit 0 ("introduces no major bump"), check-adr-0087-registration exit 0 ("1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … registered startup-orchestrator-retired (new here)"). The changeset carries the feat(…)!: title, the BREAKING banner, the disposition marker, a removed-from-to table for the retirement (every one of the 8 names, with the honest "no replacement" rows for healthCheck, HealthStatus, parallel, context), and a before/after member table for the re-declared shape including the required-to-optional durationMs, the removed startTime, and the error narrowing for core consumers. It states both halves the ruling requires.

③ Boundary flags

Q1 — is the check:duration-unit-keys conflict real? Yes, for any honest mirror; measured, not read from the report. At the head the gate is clean (208 duration-shaped numeric key(s) … zero offenders, no baseline). Mirroring core's deprecated member as startTime: z.number().min(0).optional().describe('… the same elapsed milliseconds') in my worktree turns it red: exit 1, [unit-in-prose-not-in-name] packages/spec/src/kernel/startup-orchestrator.zod.ts:157 startTime — describe names ms but the key name carries no unit. Neither schema-declared exemption fits: exemption (i) requires the value to BE the shared EpochMs instant schema, and this value is an elapsed duration (declaring it an instant would assert the very confusion the member already caused); exemption (ii) requires .meta({ externalVocabulary }), and no external standard fixes this name. The rule has no baseline and no allowlist. One nuance the record should carry: a mirror whose .describe() is SILENT about the unit passes the gate (exit 0 in my run, both with and without a JSDoc naming milliseconds — startTime is not a duration-shaped name, so the divergence branch does not reach it). That is the #14519 shape the gate's own header calls "listed, not judged", a documented blind spot rather than a sanctioned spelling, and it would publish a bare number under an instant-shaped name on the reference page. So "the tree decided it" is accurate for every mirror that tells the truth about the unit; the two gate-legal alternatives were startTimeMs (a spelling nothing has produced) or the tombstone shipped.

Q2 — measurable readers of startTime: zero in production, in both measured corpora, with lit same-subject controls. objectstack at 827cacbf4b, property-access form .startTime over the whole tree (CHANGELOGs excluded): the only reads on a PluginStartupResult are packages/core/src/kernel.test.ts:640 and :655 (ok.startTime, failed.startTime), the pins this PR rewrites to not.toHaveProperty('startTime'). Every other hit is a different subject: packages/services/service-automation/src/engine.ts (11 lines, run.startTime, a flow run's instant), suspended-run-store.ts (2, same subject), security/sandbox-runtime.ts (startTime: Date), the KernelContext.startTime retirement rows in context.mdx and the migrations registry. kernel.ts itself only writes the member; it never reads it. Same-subject control: .durationMs reads on the same result light at kernel.test.ts:637–654, and getPluginStartupDurations() at kernel.ts:628. objectui at 53ded82bf7: PluginStartupResult 0 files, PluginStartupResultSchema 0; startTime 6 files, all other subjects (TimeFieldMetadata example, PerformanceEntry.startTime in three perf hooks and a script, chatbot-reasoning local state); controls defineStack 27, ManifestSchema 6, durationMs 16. objectstack-ai/cloud: NOT MEASURED — not attached to this session; this is not a clean reading.

Q3 — does the ruling's last sentence bite? My reading: the diff sits inside the grant's letter and brushes its spirit at exactly one point; the evidence weighs toward inside, and I do not decide it. The sentence is conditioned: "If any out-of-repo consumer of the retired interface is found … a deprecation window becomes a maintainer question." Two facts bear on it. First, the trigger did not fire: startTime was never part of the retired interface (it was never on the spec surface; it is core's own alias), and the census above found no consumer of it anywhere measured, with cloud unmeasured. Second, the "window" being ended is narrower than the phrase suggests: the deprecated-and-populated alias shipped in ONE released version, @objectstack/core@17.4.0 (commit 66dc6ab, #16057, 2026-09-06; 17.4.0 is on npm), whose TSDoc promised "nothing has to change on THIS release … slated for removal", and this PR lands in the next minor. ADR-0087's L1 window is a one-protocol-major fleet-compatibility window for D2 conversions of AUTHORED metadata; this member is a runtime result no document authors, the ratified pre-GA exemption allows a break to ship one-step without a window, and the same 17.4.0 core CHANGELOG carries a precedent of a runtime interface losing an optional member under adr-0087: not-required (runtime-interface-only …). Against that: the ruling's own member list for the re-declared shape (pluginName, the error projection, timedOut) is silent on startTime, the ruling nowhere authorises changing what the kernel EMITS, and the implementer itself names this the diff's only observable behaviour change. Reading: inside the grant as written, with one uninstructed behaviour change whose blast radius is measured at zero readers; the maintainer's confirmation of option A or B is a question of preference about the window, not a contract defect, and the verdict below does not rest on it.

Deviation 3 — the three same-major rename registrations kept: verified, and the keep is right. RETIRED_KEYS_BY_MAJOR[18] holds 161 entries at the head; 17 name a key whose def is in RETIRED_DEFS_BY_MAJOR[18], of which 14 pre-exist this PR (the system/Incident*, Training*, ChangeRequest, ChangeImpact, RollbackPlan family) — the count the implementer states, confirmed. Gate (b2) text at packages/spec/scripts/build-schemas.ts:1033–1039 says verbatim that "an entry naming a key the build no longer emits at all is NOT an error: that is the expected steady state". The playbook's same-major absorption clause is motivated by the conversion table's fixture-disjointness contract; the three entries (kernel/StartupOptions:timeout, kernel/HealthStatus:timestamp, kernel/StartupOrchestrationResult:totalDuration, all at the base from e6965dd7ee) each record "No D2 conversion" or "semantic entry rather than a D2 conversion", and this PR touches nothing under packages/spec/src/conversions/. There is no conversion to stack, so there is nothing to absorb; the entries stay as the dated record of the narrower step.

Reverse verification — the implementer's leg was NOT taken at this head; the claim holds at this head on my own run. The report names HEAD blob 55b34588… and mutated blob 4755ba72…. 55b34588 is kernel.ts at the base and at the first three PR commits (66801ae148, a77b7955bc, 9faaefadd2); the head's blob is 57d2340cae… (changed in ac0fe6f145). I reconstructed 4755ba728820c7bf19c5fa237dacd46c3ca2b7c9 exactly by inserting the marker after line 794 of 55b34588, so that leg ran before the startTime commit, and the report's "all readings at e345ffa unless stated" is wrong for it. The tree at the head is clean and the restore was to the blob that was HEAD at the time, so the leg was sound for the tree it ran on; it simply was not this tree. The at-head result is now the one recorded in ① (two mutations red, restore by blob identity to 57d2340c). A reporting defect, not a contract defect.

Tombstone dating contradicts ADR-0087's 2026-09-13 amendment; same spelling as main, three strings, ungated. The three new retiredKey() prescriptions read "was removed in @objectstack/spec 18 (ADR-0049)". ADR-0087 (amended 2026-09-13, #18003) says: "A tombstone names the npm release it ships in, ⛔ never the protocol major … a retirement shipping minor lands in 17.x.y, so prose dating it to @objectstack/spec 18 is wrong on the day it is written (#18021)." This changeset ships minor, so the token should name the 17.x release. The base already carries 13 such lines (6 of them retiredKey prescriptions in plugin-lifecycle-advanced.zod.ts and cache.zod.ts, the same major-18 wave), no gate reads the token, the remedy inside the string ("Delete the key and read durationMs") is intact, and the ledger keys the retirement by protocol major 18 correctly. #18003 / #18021 are not readable from this session (404), so I cannot say whether that sweep already owns these lines. Recorded so the landing seat can fix the three strings in one commit or hand them to the sweep; it does not move the accept-set or the exported surface.

Stale comment in the diff. packages/spec/src/type-alias-convention.pin.test.ts:2225–2226 says "the deprecated startTime alias is mirrored, not defaulted" — written before ac0fe6f145; at the head the alias is tombstoned. A prose residue in a test comment, not contract text.

Not findings. My census counts differ slightly from the report's (e.g. IStartupOrchestrator 6 files outside packages/spec/src, not 5; the three schema names 4 files each, not 0) because I did not exclude generated artifacts and the reference page; the classification is identical: zero code consumers. The changeset's "two keys … take the tombstone route" counts the two that were on the old spec surface; the registry carries three, with startTime described in its own paragraph. pnpm --filter @objectstack/core typecheck in my worktree fails only at check:test-typecheck on three test files this PR does not touch (an unbuilt @objectstack/metadata-core in my closure); the bare tsc --noEmit leg that this review needed is exit 0, and CI's TypeScript Type Check on this head is success. CI at this head: 35 check runs, 33 success, 2 skipped (Console Pin Gate, Packed-tarball smoke (opt-in)), 0 failing — context only; landing is the seat's.

Implemented-by: claude/issue-16059-startup-orchestrator-shipped-shape
Reviewed-by: session_01KB5PFtxuy1x3dcR5gxudx6

VERDICT: PASS


Generated by Claude Code

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/xl tests tooling

Projects

None yet

2 participants