feat(spec)!: the fifteen system/ duration keys carry their unit in the key name [#14478 stack 4/6] - #15938
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
… spellings (#15679) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…s for the system/ renames (#15679) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…renames (#15679) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…rt the prescription explains the non-mechanical name (#15679) 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
📓 Docs Drift CheckThis PR changes 5 package(s): 14 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 — 136 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 e7d832b7312f46f3332466e1576f94976a7d78f2 && git checkout e7d832b7312f46f3332466e1576f94976a7d78f2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 74ddc0d6d090903deafdfa4df2abbe50a6f4301d 9509c6d67b54c85fe545ebf495d2595f60c169c4 && git checkout -B drift-repro 74ddc0d6d090903deafdfa4df2abbe50a6f4301d && git merge --no-ff 9509c6d67b54c85fe545ebf495d2595f60c169c4
node scripts/docs-audit/affected-docs.mjs --json 74ddc0d6d090903deafdfa4df2abbe50a6f4301d
|
|
Contract review (clause ②) PASS at head Implemented-by: Generated by Claude Code |
…tem/ card Card 3/6 landed on the trunk as a squash, which destroyed ancestry and rewound the merge base to 140e0b2 — before cards 1/6, 2/6 and 3/6. The four textual conflicts are all one thing: the prose correction card 3/6 made after this branch forked, about RuntimeConfig.resourceLimits.timeout. The gate reads .describe() and .meta({ description }) but not the JSDoc above a key, so that key is listed among the duration-shaped keys without being judged — outside the verdict, not outside the population. The JSDoc-channel gap is filed separately. Resolution: - .changeset/kernel-duration-keys-unit-in-key-name.md, packages/spec/src/migrations/entries/semantic/18.kernel-plugin-security-durations-unit-in-key.ts, packages/spec/src/kernel/plugin-security-advanced.test.ts — trunk's side taken whole. This branch never edited them: each blob is byte-identical to the fork point 8f2b8f3, so there was nothing of ours to preserve. - packages/spec/src/migrations/registry.ts — trunk's side inside the single conflict hunk (the same prose correction); everything outside it kept. All 25 'system/' rows from the trunk and all 40 from this branch survive. The two content/docs/references/system/*.mdx artifacts are merge=os-regen paths and are deferred to a regeneration commit on the merged tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…p here The merge deferred two merge=os-regen artifacts: content/docs/references/system/object-storage.mdx and content/docs/references/system/disaster-recovery.mdx. Both were regenerated from the merged tree, in the prescribed order and out of MERGE state: pnpm --filter "@objectstack/spec..." build pnpm --filter @objectstack/spec gen:schema && gen:docs pnpm --filter @objectstack/spec check:generated -> all 15 artifacts current The regeneration is provably not a no-op measurement: the trunk's bytes were restored into the working tree first (worktree only, never the index), and gen:docs wrote this branch's bytes back over them, byte-identical to what the merge commit already carried. So the driver kept the correct side for both paths and there is nothing to correct — the deferral is discharged with an empty tree change rather than an unexplained silence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
…orking branch Card 5/6 was stacked on this card, so its PR base was this branch and its squash landed directly on origin's tip while the trunk merge was in progress here. Its own branch is deleted, so this branch now carries cards 4/6 and 5/6. Measured before merging, both readings agree and neither reports a conflict: git merge-tree --write-tree bcf0412 HEAD -> exit 0 git -c merge.os-regen.driver=false merge-tree --write-tree bcf0412 HEAD -> exit 0 both write tree 7b40011 Nothing to resolve by hand. src/migrations/registry.ts auto-merged and every retired-key row from all three inputs survives: 138 rows on the trunk 74ddc0d, 153 on this branch after the trunk merge, 161 on bcf0412; the result holds 161, equal to their union, with no row missing and none invented. The forty 'system/ rows of this card are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
Part of #14478
Stack card 4/6 — issue #15679,
system/: the fifteen remaining duration keys carry their unit in the key name.Stacked on card 3/6. Base branch is
claude/issue-15678-kernel-duration-unit-in-key-nameat8f2b8f389f44862bfa7ca899a3cc4a580d5f0afb, notmain. Review the diff against that branch.Clause-②: yes— this changes contract accept/reject behaviour on the published spec surface.What the gate says
Re-derived on this branch's own head before any edit, and again on the tip. The gate's verbatim verdict lines:
Before (base
8f2b8f389):After (tip
45a35896c):src/system/**goes 15 → 0 and the whole tree falls 22 → 7. The seven that remain are card 5/6's entire population and nosrc/system/row is among them:ai/conversation.zod.tsduration,data/driver-nosql.zod.tstimeout,data/driver/memory.zod.tsautoSaveInterval,data/driver/turso.zod.tstimeout,integration/connector.zod.tsmonitoringWindowandinterval,ui/dashboard.zod.tsrefreshInterval. Exit 1 is by design until card 6/6 declares the population empty.The source-file count rises 805 → 828 because this PR adds exactly 23 files under
packages/spec/src/migrations/entries/(15 retired-key entries + 8 semantic entries). The 215 duration-shaped-key denominator is unchanged, as a pure rename must leave it.The population was 15, not the 14 the card's table lists
The card's table subtracted card 1/6's intended exemptions and so omitted
AccessControlConfig.maxAge. Card 1/6's dev demoted that key rather than marking it, and the gate on this base agrees: it reports 15src/system/rows,object-storage.zod.ts:261 maxAgeamong them.FROM → TO
CacheTier.ttlttlSecondsCacheAvalanchePrevention.circuitBreaker.resetTimeoutresetTimeoutSecondsCollaborationSessionConfig.idleTimeoutidleTimeoutMsCollaborationSessionConfig.snapshot.intervalintervalMsFailoverConfig.healthCheckIntervalhealthCheckIntervalSecondsMetricAggregationConfig.window.sizedurationSecondsServiceLevelIndicator.window.sizedurationSecondsServiceLevelObjective.period.durationdurationSecondsAccessControlConfig.maxAgemaxAgeSecondsStorageConnection.timeouttimeoutMsRegistryUpstream.syncIntervalsyncIntervalSecondsRegistryUpstream.timeouttimeoutMsRegistryConfig.cache.ttlttlSecondsSpan.durationdurationMsQueueConfig.rateLimit.durationdurationMsEvery value, default and bound is unchanged. Every old spelling is a
retiredKey()tombstone, so it failstscat the authoring site and fails the parse with the rename prescription rather than a bare unrecognized-key error. No prescription carries an internal issue id — those live only in//and/** */comments, percheck:doc-authoring.The two
sizekeys get an honest name, not the mechanical oneThe gate prints
sizeSecondsforMetricAggregationConfig.window.sizeandServiceLevelIndicator.window.size. This PR renames both todurationSecondsinstead, and says so in the prescription so the next author does not read the departure as a slip.Two reasons, in order of weight:
sizeis byte/row-count vocabulary everywhere else in this spec.CacheTier.maxSizeis megabytes,RegistryConfig.cache.maxSizeis bytes, andMetricExportConfig.batch.sizeon the very same file is a record count.sizeSecondswould have kept the misleading half of the name and bolted a unit onto it — a reader still has to decide whether the number is a quantity of data measured per second or a length of time.window, so the card's suggestedwindowSecondswould readwindow.windowSeconds.durationSecondsnames what the number is, and the file supplied its own precedent one schema down:ServiceLevelObjective.period.durationalready called a period length a duration. After the rename all three read alike. A pin asserts the prescription contains the sentence explaining the choice.The
maxAgeasymmetry is preserved, deliberatelyAccessControlConfig.maxAge(bucket CORS) is renamed. Its twinshared/CorsConfig.maxAge(HTTP CORS) keeps its bare name under itsexternalVocabularymarker, and this PR does not touch it.The demotion card 1/6 made is right on the schema's own evidence: every bucket-CORS standard the value is forwarded to already spells the unit — S3
MaxAgeSeconds, GCSmaxAgeSeconds, AzureMaxAgeInSeconds— so marking that key would have exempted a deviation from the cited standard rather than a mirror of it. The Fetch response header the twin mirrors,Access-Control-Max-Age, genuinely carries no unit token.object-storage.test.tsassertsCorsConfig.maxAgesurvives, and is the only guard.Dispositions — eight semantic entries, no D2 conversion
Decided per key from the schema's actual producer rather than defaulted, and this card's answer is uniform.
stack.zod.tsdeclares nocache,collaboration,disasterRecovery,metrics,objectStorage,registry,tracingorworkerroot — I read its collection list rather than assuming — and none of the twelve declaring defs is a registered metadata kind stored as asys_metadatarow, so the conversion chain has no seam that would see one. They are host configuration, call arguments and runtime-emitted measurements.All fifteen are registered by exact key in
RETIRED_KEYS_BY_MAJOR[18], nested spellings included (system/CacheAvalanchePrevention:circuitBreaker.resetTimeout,system/RegistryConfig:cache.ttl, and so on). The registry diff is 394 insertions, 0 deletions — no other card's tombstone was disturbed.Readers moved in the same PR
Grepped by TYPE across every workspace package, then each hit read rather than rewritten. No package outside
@objectstack/specimports any of the twelve declaring types, so the readers are: the eightsrc/system/*.test.tssuites, the regeneratedcontent/docs/references/system/*.mdx, and theauthorable-surface/+authorable-defaults/ratchets. No liveness ledger, form, i18n bundle orskills/**page names any of these keys — checked, not assumed.Keys deliberately left alone, each with a pin
FailoverConfig.dns.ttl(declaredexternalVocabularymirror, RFC 1035 §4.1.3) ·CacheAvalanchePrevention.lockout.lockTimeoutMs(already correct, and milliseconds where itsresetTimeoutSecondssibling is seconds) ·MetricExportConfig.batch.sizeandQueueConfig.rateLimit.max(counts, not durations) · the error-budget burn-ratewindowand the OpenTelemetry exportertimeout(their describes name no unit, so both are outside the gate's population) ·FileMetadata.size(bytes) ·MetricDefinition.summary.maxAgeandMetricExportConfig.interval(different keys that a name-based sweep would have caught). Pin tests assert each, so a later sweep cannot read this card as "every duration-shaped number on these files".Verification
Everything below is a measurement; nothing is recalled.
pnpm --filter @objectstack/spec testpnpm --filter @objectstack/spec typechecktsc --noEmit,check:scripts-typecheck,check:test-typecheck— 54 files / 261 errors / 145 pinned signatures held, unchanged)pnpm --filter @objectstack/spec check:generatedpnpm check:doc-authoringpnpm check:adr-0087-registrationcheck:skill-examples@objectstack/client-reactbuilt first)check:yaml-examples·check:variant-docs·check:empty-statecheck:exported-any·check:dual-source-exports·check:entry-nameabilitycheck:error-code-provenance·check:error-code-casing·check:nul-bytescheck:doc-anchors·check:docs-single-h1·check:docs-image-tag·check:docs-redirects·check:doc-frontmattercheck:test-source-alias·check:cross-package-test-inputs·check:type-check-coveragecheck:declared-population-live·check:stack-collection-maps·check:spec-parsed-aliascheck:empty-changeset·check:changeset-no-major·check:published-filescheck:duration-unit-keysEvery exit code was captured before any pipe (
cmd > log 2>&1; EXIT=$?), and every verdict quoted is the gate's own printed line. The full suite andcheck:generatedwere re-run on the final commit45a35896c;git rev-parse --short HEADat that run is the sha quoted above.Reverse verification — two legs, predictions written before running
Both legs mutate
packages/spec/src/system/cache.zod.ts, prove the mutation landed by counting the removed and injected markers on disk (never by an exit code), and restore viagit checkout HEAD -- pathverified by both a blob-hash match against the HEAD blob and an emptygit diff HEAD. The script carries atrap ... EXIT INT TERMrestore with absolute paths.No dist rebuild leg is needed here, and the reason is checked rather than assumed: the gate walks
packages/spec/src/as source text (SRC_ROOT = join(pkgRoot, 'src')), andcache.test.tsimports./cache.zodrelatively. Neither subject resolves through the package'sexportsintodist/.Leg A — revert the rename entirely (key back to⚠️ My prediction under-counted: I forgot the two pre-existing
ttl, tombstone removed).Predicted: gate 7 → 8 with a new row naming
src/system/cache.zod.tsttl;cache.test.tsred on 2CacheTiercases.Observed: gate 8 offender(s), the new row
src/system/cache.zod.ts:70 ttl— as predicted. Tests: 4 failures, not 2.CacheTierSchemafixtures this PR had already moved ontottlSeconds, which the revert also breaks. Direction and the two named cases were right, the magnitude was wrong, and it is reported as measured rather than as predicted.Leg B — keep the rename, delete only the tombstone.
Predicted: gate stays at 7 (it reads the key name, not the tombstone);
cache.test.tsred on exactly 1 case —REFUSES the retired CacheTier.ttl— becausettlis then silently stripped instead of refused.Observed: gate 7 offender(s) with no
cache.zod.tsrow, and exactly 1 failure, that case. Prediction met exactly.Leg B is the load-bearing one: it shows the refusal pins catch a silent strip that the headline gate is blind to by construction, which is the failure mode the tombstones exist for.
Two inherited reds — not from this PR
check:llms-txtandcheck:quick-reference-countsare red on this branch with three findings:Confirmed inherited by measurement, not by assertion: I ran both gates in a throwaway detached worktree at the base commit
8f2b8f389and diffed the output against this head's, with repo paths normalised. Both are byte-identical. They trace to card 1/6 and are already fixed on its branch; they clear when the stack integrates in order. Repairing them here would be another card's diff.Narrowing declared
Heavy CI does not run on a stacked PR (
lint.ymlandci.ymlboth declarepull_request: branches: [main]), so the gate family above was run by hand.scripts/pm/dispatch-gates.mjsderives its change set three-dot againstorigin/main, which for a card stacked four deep is the whole stack's 268 paths and therefore the whole stack's ~180 families — the other cards' obligations, not this one's. I narrowed to the families that read this card's own diff surface, which is exactly seven directories:content/docs/references/system/,packages/spec/authorable-surface/,packages/spec/authorable-defaults/,packages/spec/src/migrations/and its twoentries/subdirectories,packages/spec/src/system/, plus one.changeset/file. The narrowing is a measurement of the diff, not a judgement about which gates matter.Not touched
api/,kernel/,data/ui/ai/integration(card 5/6), the gate's declared population (card 6/6), any other PR's diff, andcontent/docs/releases/.🤖 Generated with Claude Code
Generated by Claude Code