feat(spec)!: the fourteen kernel/ duration keys carry their unit in the key name (#15678, stack 3/6) - #15906
Conversation
…15676) Ruling B on #14478 exempts two structural classes from the duration-unit rule, and is explicit that both are declared ON THE SCHEMA, never in a gate ledger. This commit lands the declaration channels themselves: - `EpochMs` (`packages/spec/src/shared/epoch.zod.ts`) — the shared epoch-milliseconds instant. A key whose value IS this schema is an instant, not a duration, and `check:duration-unit-keys` recognises that structurally. - `.meta({ externalVocabulary: '<the standard>' })` — the marker a key carries when it mirrors a name fixed outside this repo. It rides `z.toJSONSchema` verbatim, the same channel `xRef` / `xExpression` already use. Neither exemption is a pass on lying: a marked key still fails `name-unit-contradicts-prose`, and an `EpochMs` key whose describe names a unit other than milliseconds fails the new `instant-unit-contradicts-schema`. Both classes stay visible in the census — `--list` marks them and the verdict line counts them. The gate also now reads `description` out of `.meta()`. Without it, moving a describe into `.meta({ description })` would take a key out of the population silently — an exemption by blindness. Measured: one numeric key declares its description that way today (`data/Field.precision`), naming no time unit, so the reading adds no offender. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…ternal-vocabulary keys (#15676) The two exemption classes ruling B declares, applied to the keys the gate lists. Instants (exemption i) — all six now typed `EpochMs`; the four whose name was bare are renamed to the `*At` instant convention, tombstoned with `retiredKey()` and registered in `RETIRED_KEYS_BY_MAJOR[18]` plus one D3 semantic entry: api/WebSocketEvent.timestamp -> occurredAt api/SimplePresenceState.lastSeen -> lastSeenAt kernel/KernelContext.startTime -> startedAt (+ TenantRuntimeContext) kernel/HealthStatus.timestamp -> checkedAt kernel/ServiceMetadata.registeredAt (already `*At`, schema only) kernel/ScopeInfo.createdAt (already `*At`, schema only) `*At` and not `*Ms`, measured rather than chosen: on this package's own authorable surface all 51 distinct `*Ms` keys are durations and all 51 distinct `*At` keys are instants, so spelling an instant `*Ms` would move it into the family the rule exists to separate it from. Semantic entries rather than D2 conversions because all four are runtime-emitted — wire payloads, a host-constructed kernel context, an emitted health report — so no conversion seam ever sees one. That is the disposition `kernel/KernelContext:previewMode` already carries on one of these defs, and what ruling B prescribes for a runtime-emitted key. External-standard mirrors (exemption ii) — eleven keys marked, not thirteen. Two of the thirteen the card attributed do not survive verification against their own schema and are left for their directory cards; the PR body records the evidence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…page (#15676) The published half of exemption (ii). A marked key keeps its bare name BECAUSE an external standard fixes it, and that argument only reaches the reference-page reader if the page names the standard — so the description cell now carries "(unit per <the standard>)". Without it the exemption would leave exactly the reader `check:duration-unit-keys` was filed for where the gate found them. Also: `EpochMs` gains its type alias (the docs import-surface ratchet demands one for every documented schema) and its ADR-0122 isomorphism pin. Regenerated: json-schema.manifest/, authorable-surface/, api-surface/, export-origins/, declaration-map/, content/docs/references/**. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…he tombstones caught (#15676) - `src/contracts/startup-orchestrator.test.ts` built a `HealthStatus` with the old `timestamp` key. The `retiredKey()` tombstone refused it at compile time (`Type 'number' is not assignable to type 'undefined'`, 7 errors) — the audible-removal property the tombstone exists for, working on the first consumer it met. - `type-alias-convention.pin.test.ts`: the pin count assertion follows the new `Iso868`. - Regenerated: export-origins/, declaration-map/, api-surface/ and the `objectstack-api` skill reference index (one generated line, naming the new `shared/epoch.zod.ts` module). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…ellings (#15677) The schema half of stack card 2/6. Gate reads 48 -> 36 with src/api/ at 0. Readers, registry entries and regenerated artifacts follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…enames (#15677) 12 retired-key entries, one D2 conversion (api-endpoint-cache-ttl-to-cache-ttl-seconds — apis: is a stack collection) and five semantic entries for the eleven runtime-emitted / construction-argument keys. Readers moved in runtime, metadata, rest-adjacent tests, dogfood fixtures and the showcase example; liveness/api.json carries the renamed row plus the dead tombstone row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…acts (#15677) Per-key refusal tests assert the prescription (code + rename text), not a bare throw. Two readers the key-name grep missed and tsc/the tombstones caught: the ApiEndpoint alias table (cacheTTL/ttl/cache retargeted onto cacheTtlSeconds — an alias must point at a key the schema accepts) and the showcase endpoint fixture in metadata-type-api-registration.test.ts. Regenerated: authorable surface + defaults, reference docs, liveness state-counts. skills/objectstack-api/SKILL.md carries the rename (governed; net 0 lines, file and package both). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
@objectstack/spec minor with the BREAKING banner naming every renamed key, the six adr-0087 ids registered, the retryAfter wire note, and the disposition split (one D2 conversion, five semantic entries). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…ied on incidentally (#15677) An ADR-0087 conversion fixture copies a routable metadata kind verbatim, so an `apis:` fixture carries `method:` beside `path:` — because that is what an ApiEndpoint IS. Ruling A named conversions/registry.ts as the guard's target but enforced it with requireMethodSignal, a content proxy that held only while no conversion fixture carried a verb. This card's apis: conversion is the first that does, and the live pin red exactly as designed. The fixture is correct and stays. The exclusion moves to CONVERSION_REPLAY_FILE_RE, which states the structural fact instead of testing a symptom, and three cases pin the new guard as load-bearing rather than incidental in its turn. NOT restricting kind (b) to packages/spec/src/api/**: that is the invariant the live pin asserts, and enforcing it in the walk would make that pin true by construction — a check that cannot fail. Measured tail-neutral: the scan census is byte-identical to the base (17 route sources, 12 call sites, 5 contract declarations, 78 tails, 61 reachable). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…e issue ids (#15677) Lap 1 regenerated content/docs/references/** but left the HAND-WRITTEN pages teaching the old spellings. Three of them carried `os:check` blocks authoring `cacheTtl`, so check:skill-examples was RED and lap 1 never ran it — it sits in check:generated's "not run here" list and I did not run it separately. cacheTtl -> cacheTtlSeconds: 14 occurrences on 13 lines, all the ApiEndpoint key. retryAfter -> retryAfterSeconds: 14 occurrences, the ADR-0112 envelope field only. Deliberately NOT swept, each verified rather than assumed: - the HTTP `Retry-After` response header (6 locals over 4 sites) — RFC 9110, a separate surface, and the thing the tombstone prose exists to protect; - `retry_after` as a RetryStrategy ENUM VALUE (errors.zod.ts z.enum); - `details.retry_after` on the wire, and the pre-existing `details.retryAfterSeconds` the runtime really emits (endpoint-policy.ts). Also strips `(#14478 ruling B)` from the twelve tombstone prescriptions THIS card wrote: check:doc-authoring forbids an internal issue id in customer-facing spec text (maintainer ruling 2026-08-12), and the campaign's own earlier tombstones already comply. The version and the FROM -> TO mapping stay — those are the durable references AGENTS.md requires. Measured: the gate read 4 findings on the base and 16 on my head; it now reads the base's 4 again, so this PR adds none. Those 4 are card 1/6's (PR #15814) and are not mine to touch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…mbstone prescriptions (#15676) `check:doc-authoring` rule 3 forbids an internal issue id in customer-facing spec text (maintainer ruling 2026-08-12). The four `retiredKey()` prescriptions this stack card added each opened with a `(#14478 ruling B)` parenthetical — a citation-shaped token that resolves to nothing for the author who meets it the moment their key is refused, and which the generated reference pages publish verbatim. The parenthetical goes; the sentence keeps everything actionable it carried — the FROM to TO rename, that the value is unchanged, and the `SimplePresenceState.lastSeen` neighbour caveat — matching the shape the campaign's already-compliant tombstones use (`hook.timeout`, `job.timeout`, `DriverOptions.timeout`). The internal anchor is untouched in the adjacent `//` and `/** */` comments, which are not customer-facing and were never findings. `content/docs/references/**` regenerated with `pnpm --filter @objectstack/spec gen:docs` — no generated artifact was hand-edited. check:doc-authoring: 4 findings before, exit 0 after. check:duration-unit-keys: unmoved — 48 offender(s) among 215 duration-shaped numeric key(s), (6 declared `EpochMs` instant(s), 11 declared `externalVocabulary` mirror(s)). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…external-vocabulary-exemptions' into claude/issue-15677-api-duration-unit-in-key-name
The merge with card 1/6's advanced tip (9605808) touched api/websocket.zod.ts on both sides. The schema source auto-merged; the generated content/docs/references/api/websocket.mdx is routed to merge=os-regen, so the driver deferred it and the merge kept OUR side — silently dropping card 1/6's half. Regenerating from the merged tree is what repairs it, and it carries both sides: their two stripped prescriptions land (issue-id occurrences 2 -> 0) while my four renamed keys stay (6 -> 6). Not hand-edited and not resolved by taking a side: the bytes come from `pnpm --filter @objectstack/spec check:generated --fix` on the merged tree, and the staged diff was read before committing (`git diff` reads clean over this trap; only the staged diff shows it). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
… spellings (#15678) The schema half of stack card 3/6. Gate reads 36 -> 22 with src/kernel/ at 0. Readers, registrations and regenerated artifacts follow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…#15678) 14 retired-key entries and five semantic entries. No D2 conversion on this card: none of the twelve defs is a stack collection member or a stored sys_metadata row (stack.zod.ts declares no eventBus / startup / plugin-security root), so the conversion chain has no seam that would see one. Readers moved in core's health monitor, the kernel and contracts test suites, and the hand-written lifecycle protocol page. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…15678) Per-key refusal tests assert the prescription (issue code + rename text), not a bare throw, plus acceptance pins at the same magnitudes and defaults. Two deliberate NEGATIVE controls: RuntimeConfig.resourceLimits.timeout names no unit so it stays bare, and EventSourcingConfig.snapshotRetention is a count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
@objectstack/spec minor + @objectstack/core patch, BREAKING banner naming every renamed key, adr-0087: registered with the five semantic ids. Documents the two unit collisions the rename removes (responseTime hours vs ms; uptime ms vs the seconds-valued GET /health) and the three keys deliberately left bare. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
📓 Docs Drift CheckThis PR changes 2 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 135 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 93d8885ae8115a3537a9ca7d28b1bb44a769339c && git checkout 93d8885ae8115a3537a9ca7d28b1bb44a769339c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 43cc4659ac5de8f0c4c5b8d2bb4a44354c4c0fe2 7abb2ff4b945e4b4685ff632956a0a75833e9393 && git checkout -B drift-repro 43cc4659ac5de8f0c4c5b8d2bb4a44354c4c0fe2 && git merge --no-ff 7abb2ff4b945e4b4685ff632956a0a75833e9393
node scripts/docs-audit/affected-docs.mjs --json 43cc4659ac5de8f0c4c5b8d2bb4a44354c4c0fe2
|
Seat verification — independently reproduced, clean on every axisSeparate
It moved nothing it does not own
Every non- The gate family — the check that was skipped on #15814 and cost four hidden findings
The two inherited reds — confirmed, with one correction to the PR's framing
They are already fixed on card 1/6's branch (head
|
…s left bare (#15678) The changeset stated the key "names no unit anywhere in its prose". That is false and it would have published into the CHANGELOG: the JSDoc directly above the key (plugin-security-advanced.zod.ts:291-293) says "Execution timeout in milliseconds". Its `.describe('Maximum execution time')` names none. The accurate reason is stronger: `check:duration-unit-keys` reads `.describe()` and `.meta({ description })` and does NOT read JSDoc, so this key's unit sits in a channel the gate cannot see. The gate therefore lists it among the duration-shaped keys but deliberately does not judge it — neither an offender nor an exemption. That channel gap is tracked in #15939. The same false clause appeared in three more places on this branch and is corrected in all of them: the ADR-0087 semantic migration entry (its `reason` publishes into the generated upgrade guide), its generated mirror in registry.ts (via gen:migration-registry, never hand-edited), and the comment above the pin test. Prose only. No key renamed, no schema, `.describe()`, JSDoc, tombstone or pin assertion touched; the key stays bare and stays out of the rename. `check:duration-unit-keys` still reads 22 offender(s) among 215 duration-shaped numeric key(s) in 805 source file(s), `kernel/` at 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
Corrected one false claim in the changeset — prose onlyPushed The defect
Measurably false. /**
* Execution timeout in milliseconds
*/
timeout: z.number().int().min(0).optional()
.describe('Maximum execution time'),The JSDoc names milliseconds. The The corrected sentence
Two things in the old sentence were wrong, not one. The second is the gate's own vocabulary: the key is in the population — The same clause was in three more places on this branch
Not touched, and reported rather than edited: Readings — all at
|
Seat ruling on the two flagged deviations, and the
|
quick-reference.mdx:209 |
llms.txt |
|
|---|---|---|
stack base c09fd1023 |
## Shared Protocol (5 of **9** schemas) |
208 schemas; shared | 14 | … Epoch Instant (EpochMs) |
this branch adad660a1 |
## Shared Protocol (5 of **8** schemas) |
— |
This branch was cut from card 1/6's head before that count lap, so its ancestry does not carry the fix. It flows forward as each card merges the advanced base — card 2/6 is doing exactly that right now. ⛔ Nothing to repair here, and ⛔ nothing to file: a defect already fixed on a branch that is now merged does not need a second record.
Lint & Repo Gates on this PR, because heavy CI never runs on a stacked head (lint.yml and ci.yml both declare pull_request: branches: [main]). It would red at integration — which is the same reason it must be, and is, fixed before then.
The residual imprecision left in place — agreed
Two sites still say "outside the gate's population" and were deliberately not touched because they carry the imprecise phrase without the false claim (18.kernel__SandboxConfig__process.timeout.ts, whose neighbouring clause "describe names no unit at all" is true, and #15939's own body). ⛔ Agreed — do not widen. Recording the distinction on #15939 is the right home.
Generated by Claude Code
|
Contract review (clause ②) PASS at head Landing stays with the Implemented-by: Generated by Claude Code |
The trunk landed cards 1/6 and 2/6 as a SQUASH commit, so their content has no shared ancestry with this branch and the merge replays it. One real text conflict, packages/spec/src/migrations/registry.ts, resolved as the UNION of both sides' RETIRED_KEYS_BY_MAJOR[18] rows: 76 base + 17 from the trunk + 14 from this card = 107, nothing dropped. The two os-regen driver-managed artifacts both sides moved (packages/spec/authorable-surface/kernel.json, content/docs/references/kernel/startup-orchestrator.mdx) take the trunk's side here; the regeneration follows as its own commit on the merged tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
Discharges the deferral the merge commit recorded. `check:generated` proved exactly one artifact set stale (content/docs/references/**) after the spec build re-derived authorable-surface/kernel.json, so only that one was regenerated. Both files re-derive this card's [RETIRED] rows on top of the trunk's side; the trunk's own renames (checkedAt, startedAt) survive in both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
74ddc0d
into
claude/issue-14478-duration-unit-in-key-name
Part of #14478
Stack card 3/6 — implements #15678,
kernel/(no closing keyword: this card is one of six and the stack lands as a unit).Stacked on card 2/6. Base =
claude/issue-15677-api-duration-unit-in-key-name, cut atb4633903b282cbb1e343c1a6e6ec99bde5dc4d59.Maintainer ruling B on #14478 (2026-09-02, decision batch #43): a duration-shaped
z.number()carries its unit in the key NAME, never only in its.describe()prose, and no existing offender is grandfathered.The number
check:duration-unit-keyson this head (8f2b8f389), exit code captured before any pipe:b4633903b)✗ check:duration-unit-keys — 36 offender(s) among 215 duration-shaped numeric key(s) in 786 source file(s) (6 declaredEpochMsinstant(s), 11 declaredexternalVocabularymirror(s)):8f2b8f389)✗ check:duration-unit-keys — 22 offender(s) among 215 duration-shaped numeric key(s) in 805 source file(s) (6 declaredEpochMsinstant(s), 11 declaredexternalVocabularymirror(s)):36 → 22, and
src/kernel/**reads 0. The gate still exits 1, as it must until card 6/6 closes the stack — the 22 that remain aresystem/(15, card 4/6) anddata/ui/ai/integration(7, card 5/6). The source-file count moves 786 → 805 because this card adds 19 ADR-0087 registry entry files underpackages/spec/src/; the 215 duration-shaped key population is unchanged, which is what a pure rename should do.The 14 keys
EventPersistence.retentionretentionDaysEventSourcingConfig.retentionretentionDaysUpgradePlan.estimatedDurationestimatedDurationSecondsPluginHealthReport.metrics.uptimeuptimeMsPluginHealthReport.metrics.responseTimeresponseTimeMsSandboxConfig.process.timeouttimeoutMsKernelSecurityPolicy.authentication.tokenExpirationtokenExpirationSecondsKernelSecurityPolicy.auditLog.retentionretentionDaysPluginSecurityManifest.vulnerabilityDisclosure.responseTimeresponseTimeHoursPackageDependencyResolutionResult.resolvedInresolvedInMsMultiVersionSupport.rollout.durationdurationMsStartupOptions.timeouttimeoutMsPluginStartupResult.durationdurationMsStartupOrchestrationResult.totalDurationtotalDurationMsEvery value is unchanged and every default moves with its key. Each old spelling is a
retiredKey()tombstone, so authoring it failstsc(input typenever) and fails the parse with the rename prescription rather than a bare unrecognized-key error.Population re-derived from the gate on this branch's own head before the first edit, per ruling (1) — the gate's predicate is the population's definition. The card's table listed the right file+key pairs; its line numbers were stale from
e68ae2b58(startup-orchestrator.zod.tshad shifted 37 → 39, 149 → 161, 196 → 208 under cards 1/6 and 2/6).Two unit collisions this removes
responseTimemeant two different units on two kernel shapes: HOURS on the security manifest's disclosure block, MILLISECONDS on the health report.responseTime: 24was a day on one and a fortieth of a second on the other. They land onresponseTimeHoursandresponseTimeMs; a pin asserts the manifest prescription does not nameresponseTimeMs, so one find-and-replace cannot rewrite both.uptimewas already costing prose:content/docs/protocol/kernel/lifecycle.mdxcarried a standing paragraph whose only job was distinguishing the milliseconds-valuedmetrics.uptimefrom the seconds-valueduptimeofGET /health.metrics.uptimeMsmakes the paragraph a statement about the fix rather than a warning; the HTTP body'suptimeis a separate surface and is untouched.Dispositions — five semantic entries, no D2 conversion
Decided per key from the schema's producer, not from the card's
classcolumn, and the answer here is uniform: none of the fourteen gets a D2 conversion. A D2 conversion runs over a stack document, andstack.zod.tsdeclares noeventBus,startup,upgradeor plugin-security root — none of these twelve defs is a stack collection member (mapCollectiontargets:actions agents analyticsCubes apis apps books connectors dashboards datasets datasources flows hooks jobs mappings objects permissions sharingRules skills themes tools translations) and none is a registered metadata kind stored as asys_metadatarow. So the chain has no seam that would see one, and each key carries a semantic entry instead — the dispositionkernel/HealthStatus:timestampalready holds on one of these very files from card 1/6.All fourteen are registered by exact key in
RETIRED_KEYS_BY_MAJORunder major 18, including the seven nested ones the authorable-surface walk does not reach (theRestApiPluginConfig:performance.defaultCacheTtlprecedent from card 2/6). Gate (b) named exactly the seven top-level keys and now passes.Keys deliberately left bare — with a pin each
EventSourcingConfig.snapshotRetentionis a COUNT of snapshots andMultiVersionSupport.rollout.percentageis a proportion; neither is a duration.RuntimeConfig.resourceLimits.timeoutnames no unit anywhere in its prose, so it is outside the gate's population — a pin asserts it still parses bare, so a later sweep cannot read the four security renames as "every timeout on that file".Readers were found by TYPE, not by name, and each hit was read rather than rewritten. Two near-misses that a name grep would have renamed:
HealthStatus.details = { uptime: 1000 }is a free-formz.recordentry, andpackages/core/src/plugin-loader.tsdeclares its OWN localPluginStartupResultinterface — a different type carryingstartTime, no duration key at all, not a reader of this schema. Both untouched.Verification
Local, because heavy CI does not report on a stacked PR — verified on this head:
lint.ymlandci.ymlboth declarepull_request: branches: [main], and this PR's base is card 2/6's branch, so none of the 7 required contexts runs.pnpm --filter @objectstack/spec test— 473 files / 12786 tests passedpnpm --filter @objectstack/spec typecheck— clean (tsc --noEmit, scripts project, and the test layer at its unchanged 54 files / 261 errors / 145 pinned signatures)pnpm --filter @objectstack/core typecheck+ full core suite — 49 files / 1190 tests passedcheck:generated— all 15 generated artifacts up to datecheck:doc-authoring0 findings across 14971 customer-facing strings,check:skill-examples257 prose examples type-checked,check:doc-formula-expressions,check:doc-security-posture), and ~70 root-level gates derived from the diff byscripts/pm/dispatch-gates.mjs— green.Prerequisite non-readings were treated as non-readings, never as passes:
check:skill-examples,check:doc-formula-expressionsandcheck:doc-security-posturefirst exited 3 / needed builds, and were re-run green after building@objectstack/formula,@objectstack/lint,@objectstack/client-reactand@objectstack/client.check:i18nandcheck:published-readme-exportsremain PREREQUISITE-NOT-MET (exit 3) — reported as not measured, not as passes.Reverse verification
Prediction written before running: reverting the
StartupOptions.timeoutMsrename and dropping its tombstone should move the gate 22 → 23 with exactly one newkernel/row, and red the twoStartupOptionspins while the other two startup refusal pins stay green.Observed: gate 23 offender(s),
kernel/rows 0 → 1, the single new rowsrc/kernel/startup-orchestrator.zod.ts:39timeout``;startup-orchestrator.test.ts5 failed / 11 passed — the two predicted pins plus the three pre-existing `StartupOptionsSchema` tests this PR moved onto `timeoutMs`, which the prediction under-counted. Direction and localisation held exactly; the magnitude did not, and is reported as measured.The mutation was proved on disk before measuring — injected marker
grep -c= 1, removed marker = 0, blob3d952c6ca...→370a6101b...— and the restore was proved bygit rev-parse HEAD:PATHmatchinggit hash-object PATHat3d952c6ca...withgit diff HEADandgit status --porcelainboth empty, never by an exit code. The script carried an absolute-pathtrap ... EXIT INT TERM.packages/specwas rebuilt afterwards, because restoring the source madedistolder thansrcand five gates correctly refused to read a stale build.Two reds that are NOT this card's
check:llms-txtandcheck:quick-reference-countsare red on this head and were already red on the base. One cause:3f9544447(card 1/6) addedpackages/spec/src/shared/epoch.zod.tsandcontent/docs/references/shared/epoch.mdxwithout moving the two hand-kept count claims. This branch changes 0 files underpackages/spec/src/sharedand 0 undercontent/docs/references/shared, and adds no schema file at all. Filed as #15904 rather than repaired here — that is card 1/6's diff.Scope
No
api/, nosystem/, nodata/ui/ai/integration, no gate-population change, nocontent/docs/releases/. Noskills/**file names any of these 14 keys (everyretentionhit underskills/isdata.lifecycle.retention, a different key that is an object rather than a duration number), so nothing was dropped to avoid governance — there was nothing to keep.Clause-②: yes—needs:contract-reviewapplied to this PR and to #15678.🤖 Generated with Claude Code
https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
Generated by Claude Code