feat(spec)!: declare the duration rule's two structural exemptions on the schema — a shared EpochMs instant and a .meta({ externalVocabulary }) marker - #15814
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
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 129 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 3fc03730b132a399c9f07613ccb833bc8413ce50 && git checkout 3fc03730b132a399c9f07613ccb833bc8413ce50
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 140e0b266578c1325d96d95294513232520c72ae 5b1b5308e4c19e872fc8dc33733f21454c0f18cb && git checkout -B drift-repro 140e0b266578c1325d96d95294513232520c72ae && git merge --no-ff 5b1b5308e4c19e872fc8dc33733f21454c0f18cb
node scripts/docs-audit/affected-docs.mjs --json 140e0b266578c1325d96d95294513232520c72ae
|
Seat check on the freshly-opened PR — base correct, Prime Directive intact, and this PR is GOVERNED for a reason worth readingEpic PM for the #14478 subtree, ✅ Base is right — the one instruction I put first
✅ Prime Directive intact
⛔ GOVERNED — and the exception did NOT lift it, which is the interesting part
The predicate's own reason, quoted:
⭐ Normally a purely-regenerated skills artifact is lifted out of governance. Here it is not, because this PR also changes the generator ( ⇒ Disposition: accept governance. ⛔ Do NOT split. The predicate suggests landing the generator change and the artifact regeneration as separate PRs; here that buys nothing, because this stack is human-merged regardless — #15626, the base, is itself governed on ⛔ Therefore: no seat flips this ready, enqueues it, arms auto-merge, or approves it. A human merges the stack. ⭐ A judgement the rest of the stack can reuseCards 2/6–5/6 (#15677–#15680) are pure conversions. If they regenerate Still open on this PRCI is early (9 runs: 6 green, 3 in progress, 0 red). The reading that decides whether this card did its job is Generated by Claude Code |
Seat reading on the dev report — one structural finding, one disposition, one number held back1. ⭐ Heavy CI does not run on this PR, and cannot while its base is a feature branchMeasured
The control is there because a zero-hit is not a reading until a neighbouring term that certainly exists returns non-zero. It does, so the zero is real. Cause, read from on:
pull_request:
branches:
- mainThis PR's base is What follows for the stack, recorded once so it is not rediscovered per card:
⛔ This is not an argument to re-base onto 2. Disposition on the open question — A, accept the governance⛔ Do not relocate The governance bit costs this stack nothing new: the base PR #15626 is itself governed ( 3. ⛔ Card 4/6's title is NOT corrected yet — the delta does not reconcileThe report asks for #15679 to move 13 → 15. The seat will not move it on this arithmetic: Two attributions were demoted, one in
There is also a bucket that is named nowhere: the marker table marks The gate prints its offenders, so this is settled by enumeration, not by arithmetic — a per-directory offender list on Standing state of this PRDraft, and it stays one. ⛔ The seat will not flip it ready, enqueue it, enable auto-merge, or approve it — it is a governed-surface PR, and an agent seat never approves or merges one. Merge is a human action, into Generated by Claude Code |
Enumeration landed — the dev's numbers are confirmed, and the +2 was two different thingsIndependent re-run, ⛔ not a re-reading of the dev's output: The baseline line reproduces character for character. All 48 and all 65 are the single rule
−17 = 6 instants + 11 markers, and it decomposes by directory exactly: api 4 markers + 2 instants, kernel 4 instants, system 5 markers, data 1, shared 1. The dev's split is exactly reproducible and complete. The
|
…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
⛔ This PR carries 4
|
…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
Follow-up lap: internal issue id removed from the four tombstone prescriptions
What changed — the The Files: 3 sources plus their 3 regenerated reference pages.
ReadingsHeavy CI does not report on this PR (base is a feature branch), so these are local, on
Others, all at
Generated by Claude Code |
…keys onto the epoch rename (#15676) Adding `src/shared/epoch.zod.ts` and its generated reference page moved two counts that no generator re-derives, and left four JSDoc `@example` blocks teaching keys this branch turned into tombstones. The counts (both re-derived by gates, both hand-kept by design): - `packages/spec/llms.txt` schema-inventory heading 207 -> 208 and the `shared` row 13 -> 14. Per that gate's header the number is not the claim, the prose beside it is: the row's Key Schemas cell now names the epoch instant, so the sentence and the count say the same thing. - `content/docs/getting-started/quick-reference.mdx` "Shared Protocol" (5 of 8) -> (5 of 9). Only M moves: the table is a curated subset by that page's own stated rule, so N stays 5 and no row is added. The examples (no gate reads a JSDoc `@example`, so each was classified in context by reading the schema it documents): - `WebSocketEventSchema`'s two examples: `timestamp` -> `occurredAt`. - `HealthStatusSchema`'s example: `"timestamp"` -> `"checkedAt"`. - `PluginStartupResultSchema`'s example: the nested `health.timestamp` -> `"checkedAt"` — `health` carries `HealthStatusSchema`, so it is the same renamed key. Its sibling `"duration"` is untouched. No key name, describe, meta, schema shape, tombstone, disposition, retired-key row, conversion or changeset changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
Follow-up lap on this branch — the two hand-kept counts, and four
|
| Site | Was | Now | Why |
|---|---|---|---|
api/websocket.zod.ts:450 |
timestamp: Date.now() |
occurredAt: Date.now() |
Inside WebSocketEventSchema's own docblock (@example "Subscribe to channel"). That schema's timestamp is now a retiredKey tombstone; the live key is occurredAt. |
api/websocket.zod.ts:460 |
timestamp: Date.now() |
occurredAt: Date.now() |
Same docblock, @example "Data change notification". Same schema, same rename. |
kernel/startup-orchestrator.zod.ts:84 |
"timestamp": 1706659200000, |
"checkedAt": 1706659200000, |
HealthStatusSchema's @example. HealthStatus.timestamp is tombstoned; the live key is checkedAt (declared at :102, tombstone at :105). |
kernel/startup-orchestrator.zod.ts:140 |
"timestamp": 1706659200000 |
"checkedAt": 1706659200000 |
Verified before changing, since it sits in a different docblock: it is PluginStartupResultSchema's @example, nested under "health", and that field is declared health: HealthStatusSchema.optional() (:176). So it IS the same renamed key. Its sibling "duration" in that example is a live key on this branch and was left untouched. |
Each surrounding example still parses as a whole: WebSocketEventSchema takes type / channel / payload / occurredAt; HealthStatusSchema takes healthy / checkedAt / details; PluginStartupResultSchema takes plugin / success / duration / health.
A re-scan of this branch's own changed sources for the other renamed spellings (timestamp, lastSeen, startTime, ttl, timeout, duration in comment or docblock position) found no further @example teaching a retired key — the remaining hits are tombstone prescriptions and rename notes, which are correct as written.
The rest of the sweep, on 5b1b5308e
check:duration-unit-keys EXIT=1 (unchanged, expected — this is the campaign's remaining backlog)
✗ check:duration-unit-keys — 48 offender(s) among 215 duration-shaped numeric key(s) in 769 source file(s) (6 declared `EpochMs` instant(s), 11 declared `externalVocabulary` mirror(s)):
check:doc-authoring EXIT=0 (394 files clean; 14871 customer-facing strings across 768 spec sources clean; sibling baseline holds)
check:generated EXIT=0 ("All 15 generated artifacts are up to date" — nothing went stale, nothing hand-edited)
check:nul-bytes EXIT=0 (7632 text files, no raw control bytes)
check:doc-anchors EXIT=0 (307 fragment links across 411 sources resolve — the edited heading's slug moves with its text, and nothing links to it)
check:docs-single-h1 EXIT=0
check:published-files EXIT=0
check:error-status-conformance EXIT=0
check:docs-locale-catch-all EXIT=0
pnpm --filter @objectstack/spec typecheck EXIT=0
pnpm --filter @objectstack/spec test EXIT=0 — Test Files 473 passed (473), Tests 12731 passed (12731)
check:doc-anchors, check:docs-single-h1, check:published-files, check:error-status-conformance and check:docs-locale-catch-all were added to the asked-for set by grepping for the scripts that actually read content/docs/getting-started/quick-reference.mdx and packages/spec/llms.txt; heavy CI does not run on this stacked PR (lint.yml and ci.yml both declare pull_request: branches: [main]), so local measurement is the only evidence here.
Filed while verifying, not fixed here: #15919 — api-surface/ records a name declared as both a const and a type under (type) only, so the value half of 132 such exports is invisible to check:api-surface. EpochMs is one of the 132; nothing about it is specific to this branch.
Generated by Claude Code
Part of #14478
Implements #15676 — card 1 of 6 of the #14478 stack (this card is not closed by merging; the stack lands together).
⭐ Stacked, not parallel. Base is
claude/issue-14478-duration-unit-in-key-name(PR #15626) at140e0b266, ⛔ notmain—check:duration-unit-keysexists only on that branch, so a PR cut frommaincould not be evaluated against the gate this card teaches.The gate's own reading — before and after
Both taken with
pnpm --filter @objectstack/spec check:duration-unit-keys, verbatim, never recounted by another rule:48, not the 46 the card projected. The difference is exactly the two attributions the card told the dev to verify and correct — see "Two attributions that did not survive" below. The denominator is unchanged at 215 deliberately: an exempted key stays IN the census and the verdict line counts it, so the exemption is visible rather than a silent subtraction. The remaining 48 are cards 2/6–5/6's populations; the gate stays red until they land, which is what "the set lands together" means.
1. Epoch instants — the shared
EpochMsschemapackages/spec/src/shared/epoch.zod.tsexportsEpochMs, az.number().int()describing milliseconds since the Unix epoch. The gate recognises a key whose value IS that schema as an instant. ⛔ No key name appears in the gate, in a ledger, or in a baseline.*Atand not*Ms, measured rather than chosen. On this package's own authorable surface all 51 distinct keys ending inMsare durations (timeoutMs,backoffMs,latencyMs,uptimeMs,windowMs) and all 51 distinct keys ending inAtare instants (createdAt,expiresAt,lastUsedAt,registeredAt). Renaming an instant to*Mswould move it INTO the duration family — the opposite of what ruling B separates.api/WebSocketEventtimestampoccurredAtRETIRED_KEYS_BY_MAJOR[18]api/SimplePresenceStatelastSeenlastSeenAtRETIRED_KEYS_BY_MAJOR[18]kernel/KernelContextstartTimestartedAtRETIRED_KEYS_BY_MAJOR[18]kernel/TenantRuntimeContextstartTimestartedAtkernel/HealthStatustimestampcheckedAtRETIRED_KEYS_BY_MAJOR[18]kernel/ServiceMetadataregisteredAtregisteredAt*At— schema only, no retirementkernel/ScopeInfocreatedAtcreatedAt*At— schema only, no retirementSemantic entry, not a D2 conversion, for all four renames — one entry,
epoch-instant-keys-renamed. All four keys are runtime-emitted: two wire payloads, a host-constructed kernel context, an emitted health report. None is a stack collection member and none is stored as asys_metadatarow, so aMetadataConversionwould be a transform with no seam that ever runs. That is the dispositionkernel/KernelContext:previewModealready carries on one of these very defs, and it is what the ruling prescribes verbatim: an ADR-0087 conversion where the key is authorable, a semantic entry where it is runtime-emitted.api/PresenceState.lastSeen(api/realtime-shared.zod.ts) is a different key of a different type — an ISO-8601 datetime string. Untouched, and the tombstone prose says so, because renaming it along with its neighbour is the obvious mistake.One tightening, stated rather than slipped in.
WebSocketEvent.timestampandSimplePresenceState.lastSeenwere barez.number();EpochMsisz.number().int(), so a fractional epoch that used to parse at those two sites is now refused. The other four already declared.int(). Adopting a shared schema means one shape, and loosening four sites to keep two loose would have been the wrong direction.2. External-standard mirrors —
.meta({ externalVocabulary })A key whose name is fixed outside this repo carries
.meta({ externalVocabulary: 'THE_STANDARD' }). It merges with the existing.describe()rather than replacing it (measured against zod 4.4.3), ridesz.toJSONSchemaverbatim — the channelxRef/xExpression/xEnumDeprecatedalready use — and the reference page publishes it:Publishing is not decoration.
check:duration-unit-keysexists because a baremaxAgepublishes a naked number to a reader who cannot see the source; exempting the key without naming its standard would leave exactly that reader where the gate found them. 23 such notes now render across 9 reference pages.Eleven keys marked, each verified against its own schema rather than inherited from the card:
api/CacheControl.maxAgemax-age(RFC 9111 §5.2.2.1)directivesenum spells the same names on the wireapi/CacheControl.staleWhileRevalidatestale-while-revalidate(RFC 5861 §3)api/CacheControl.staleIfErrorstale-if-error(RFC 5861 §4)shared/CorsConfig.maxAgeAccess-Control-Max-Age(WHATWG Fetch)api/PresignedUrlResponse.data.expiresInexpiresInstorage-routes.tsholds it, the adapter signature takes it,s3-storage-adapter.tspassesgetSignedUrl(client, cmd, { expiresIn })system/PresignedUrlConfig.expiresInexpiresIn.max(604800)beside it is that standard's own 7-day ceilingsystem/EmailAndPasswordConfig.resetPasswordTokenExpiresInsystem/EmailVerificationConfig.expiresInsystem/AuthConfig.session.expiresInsession.expiresIndata/PostgresConfig.statementTimeoutstatement_timeoutsystem/FailoverConfig.dns.ttlttlby every provider API it forwards toThe card warned that a key which turns out NOT to mirror a standard is a rename for its directory card, ⛔ not a marker, and that getting this wrong exempts a key permanently and silently. Two of the thirteen fail on their own evidence, so they are left as offenders:
api/DeviceRequestResponse.interval(card said RFC 8628). The schema does not mirror RFC 8628's vocabulary as a set:codeis notdevice_code,verificationUrlis notverification_uri, andexpiresAtis notexpires_in— different name AND different type (ISO string vs seconds). The schema already renames RFC fields to house style wherever it prefers to, so "the standard fixes this name" is falsified by its own sibling. ⇒ a rename for card 2/6 ([#14478 stack 2/6]api/: the 12 remaining duration keys carry their unit in the key name — ADR-0087 conversions with readers,ApiError.retryAfterwith its own BREAKING note #15677).system/AccessControlConfig.maxAge(card said "CORS on bucket rules"). Its siblings (allowedOrigins,allowedMethods,allowedHeaders,exposeHeaders) are the bucket-CORS-rule vocabulary — and every bucket-CORS standard spells that field WITH its unit: S3MaxAgeSeconds, GCSmaxAgeSeconds, AzureMaxAgeInSeconds. Marking it would exempt a deviation from the standard cited, not a mirror of it. ⇒ a rename tomaxAgeSecondsfor card 4/6 ([#14478 stack 4/6]system/: the 15 remaining duration keys carry their unit in the key name — ADR-0087 conversions with readers;metrics.zod.tssizeneeds an honest name, not the mechanical one #15679).What this does to the stack's arithmetic. Measured per directory on this head:
api/12,kernel/14,system/15,data/+ui/+ai/+integration/7 = 48. Cards 2/6, 3/6 and 5/6 are unchanged at 12 / 14 / 7 — exactly their titles. Only card 4/6 (system/) moves: 13 → 15. Note that card 2/6's own stated 12 independently corroborates the first demotion: hadintervalbeen marked,api/would be 11, one below the number that card carries.3. The gate learns both classes, and neither becomes a get-out-of-jail card
name-unit-contradicts-prose— a marker waives the RENAME, never a contradiction.maxAgeMsdescribing seconds is the 1000x bug whatever standard its name mirrors.instant-unit-contradicts-schema: anEpochMskey whose describe names a unit other than milliseconds is refused. The schema says milliseconds; prose that says seconds means one of the two is wrong.descriptionout of.meta()as well as.describe(). Without it, moving a describe into.meta({ description })would take a key out of the population silently — an exemption by blindness, which is what the ruling refuses. Measured: exactly one numeric key declares its description that way today (data/Field.precision, naming no time unit), so this adds no offender. It stops the next one.EpochMsby identifier, because the gate is a syntactic scan with no module resolution. A self-test case readssrc/shared/epoch.zod.tsand asserts it really exports that symbol — the same couplingROOT_DIR_WATCH_HINTSalready has, so a rename on one side is RED rather than a silently empty exemption.Self-test: 37 cases, all pass (17 new). Reverse-verified — see below.
Verification
Full commands, exit codes captured before any pipe, on head
c1949b9ac:pnpm --filter @objectstack/spec testpnpm --filter @objectstack/spec typechecktsc --noEmit+ scripts + test layer)check:generated·check:authorable-surface·check:docs·check:api-surface·check:export-origins·check:declaration-mapcheck:spec-parsed-alias·check:nul-bytes·check:migration-registry·check:spec-changes·check:upgrade-guide·check:liveness·check:dual-source-exports·check:entry-nameability·check:exported-any·check:declared-population-live·check:watch-hint-literal·check:self-test-wired·check:cross-package-test-inputs·check:test-source-alias· the five skill gatescheck-adr-0087-registrationregistered epoch-instant-keys-renamed (new here)check-changeset-no-major·check-empty-changesetcheck:duration-unit-keysReverse verification (ablation), both halves, each from a committed state so the restore had a real restore point:
externalVocabularyNote(prop)term from the description cell turns 3 of the 5 new docs cases red and leaves 2 green — the 2 that assert the note's ABSENCE. Predicted before running; the asymmetry is the point.INSTANT_ROOTand deleting the marker branch turns 6 self-test cases red (including theEpochMs-is-exported pin) and moves the tree reading48 → 59offenders among215 → 209sites. Worth recording: breaking the instant root does not turn the six instants into offenders, it makes them invisible — which is exactly why the export-liveness pin exists, and it is the case that went red.Both mutations were confirmed on disk by blob hash (
git hash-objectvs theHEADblob) before any measurement, and both restores were proved bygit diff HEADempty plus a hash match, not by an exit code. No build step was needed for either: both files are loaded from source by vitest and bytsx, never throughdist/.Declared narrowing. Downstream typecheck was scoped to
@objectstack/specrather than the ~70 consumers. Justification: every consumer of the four renamed keys was enumerated withgit grepoverpackages apps examplesand each hit read individually — the only matches outsidepackages/specare unrelated local symbols (core/kernel.ts's ownPluginStartupResult.startTime,core/health-monitor.ts's ownPluginHealthReport.timestamp, both local interfaces). And the tombstones make any missed consumer a compile error, not a silent strip: the first one they met wassrc/contracts/startup-orchestrator.test.ts, whichtscrefused with 7 errors before any test ran. CI runs the full farm regardless.⛔ Governed — read this before enqueuing
node scripts/pm/check-governed-merges.mjs --test FINAL_FILE_LISTexits 3:skills/objectstack-api/references/_index.mdis on the register. It is one generated line —gen:skill-refsindexing the newshared/epoch.zod.tsmodule — andcheck:generatedrefuses to be green without it. The #11705 generated-surface exception does not lift it, because this PR also edits generator scripts, so the recompute would run this PR's own generator.This PR is a draft and stays one. ⛔ Not flipped ready, not enqueued, no auto-merge, not approved — human merge, the same regime PR #15626 is already under.
Files
48 changed under this branch's own diff: the new
shared/epoch.zod.ts+ its export, the gate and its self-test, the docs generator + its 5 new tests, 8 schema files, 3 test files, 6 migration-registry entry files + the regenerated registry, one changeset, and the regenerated artifacts (json-schema.manifest/,authorable-surface/,api-surface/,export-origins/,declaration-map/,content/docs/references/**, the skill reference index).⛔ Not touched, deliberately: PR #15626's diff, its draft flag, its labels and its body; the directory populations of cards 2/6–5/6; the gate's declared population (card 6/6);
content/docs/releases/.Generated by Claude Code