feat(spec)!: the five system/metrics.zod.ts durations carry their unit in the key name (#17783) - #18007
Conversation
WIP — five renames on system/metrics.zod.ts plus their ADR-0087 entries. Generated artifacts, tests and changeset still to come. Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt Co-authored-by: Claude <noreply@anthropic.com>
WIP — tests, generated artifacts and reference docs regenerated. Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt Co-authored-by: Claude <noreply@anthropic.com>
The five durations on system/metrics.zod.ts whose unit lived in a source JSDoc only now carry it in the key name, and each published describe states it: summary.maxAge -> maxAgeSeconds, errorBudget.burnRateWindows[].window -> durationSeconds, MetricExportConfig.interval -> intervalSeconds, collectionInterval -> collectionIntervalSeconds and retention.period -> durationSeconds. Every value and default is unchanged. Each old spelling is a retiredKey() tombstone carrying the FROM -> TO prescription; none of the five enclosing shapes is strict, so a bare deletion would have stripped in silence. One ADR-0087 D3 semantic entry plus five RETIRED_KEYS_BY_MAJOR[18] rows. Two of the five are top level, so the authorable-surface ratchet moves for those two only. Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check11 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. 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 44981ce8e657febfb33f99493b672d3d0cf4726c && git checkout 44981ce8e657febfb33f99493b672d3d0cf4726c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a0dd872c1b01d5afc0ef6eb389d7c874b4a51cfa 84e62ed7129e7cd6e1b6a307530cdf4f9d3602a3 && git checkout -B drift-repro a0dd872c1b01d5afc0ef6eb389d7c874b4a51cfa && git merge --no-ff 84e62ed7129e7cd6e1b6a307530cdf4f9d3602a3
node scripts/docs-audit/affected-docs.mjs --json a0dd872c1b01d5afc0ef6eb389d7c874b4a51cfa |
…trics-duration-unit-keys
The red
|
|
Hold released — PR #17999 landed, this PR's
|
| subject | count |
|---|---|
system/HttpDestinationConfig:batch.flushInterval |
4 |
system/HttpDestinationConfig:retry.initialDelay |
2 |
system/HttpDestinationConfig:timeout |
2 |
system/LoggingConfig:buffer.flushInterval |
4 |
logging-durations-unit-in-key |
10 |
lit control — #17781's kernel-runtime-config-timeout-unit-in-key |
4 |
| dark control | 0 |
packages/spec/authorable-surface/system.json now also carries system/HttpDestinationConfig:timeoutMs.
Owed before this PR may be enqueued: bash scripts/pm/os-regen-merge.sh (⛔ never a hand-merge of either shard) → gen:schema → check:generated clean → a set-difference over the whole registry reported as "N lost, M gained", plus exact-name re-assertion that both cards' rows survive, with a lit control and a dark control at 0 → --pair 18007 re-run.
check:generated green.
Sibling status, for the record: #18016 (#17785, tracing) is not in this constraint — all four of its rows are nested, so its diff touches neither shard. Verified from its file list, not assumed.
Generated by Claude Code
…cs migration prose A count without a ref is a claim a future reader cannot check — the defect class this epic exists to remove. The seconds-suffix family counts in the ADR-0087 semantic entry and the changeset now name the sha they were taken at, and the two bare *S keys the corpus contains are named so the zero is falsifiable. Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt Co-authored-by: Claude <noreply@anthropic.com>
…trics-duration-unit-keys
Second lander on packages/spec/authorable-surface/system.json and authorable-defaults/system.json after #17999 (#17782, logging). Both shards were merged by the os-regen driver, so step 2 took main's side and this commit regenerates them from the merged source — the only way both cards' rows are proven present rather than assumed. Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt Co-authored-by: Claude <noreply@anthropic.com>
Contract review
Reviewed from an isolated worktree at the head sha ( ① Derived judgmentsThe five renames (accepted-set changes, from the diff of
⭐ Suffix decisions — the split is principled, and the file's own #15679 convention is the reason, not the exception:
The three same-named decoys: at head The ratchet (2 move, 3 do not): ADR-0087 kit: one D3 entry The pin guard replaced: Counts carrying a ref in migration prose (spot-checked three): "Seconds 40 at fc28c1d … 45 at 9b62f54" — reproduces exactly; "1195 defineStack … at fc28c1d" — Primary A — the #15679 acceptance sentence vs the "never amend a predecessor's entry" precedentWhat the sentence is, read from the entry text. It lives in Why the #17983 precedent does not squarely govern. #17983's rationale was that #15678's sentence is "a scoped, past-tense statement about what #15678 did, it stays true". Two things differ here: (1) #15678's "One key deliberately left alone … it is outside this rename" sits in How the projections actually render it today — a measured correction to both the precedent and this PR. Neither entry renders anywhere: Does the author's entry neutralise it? Only for a reader who reads both. The supersession notice lives in the SUCCESSOR's Verdict on A: option A as shipped satisfies the "record stays" half and misses the "pointer at the site" half. The fix is one appended clause in Primary B — dot-joined path across an array element
② Semver level
③ Boundary flags
Readings I re-measuredAll in
Taken on trust (not re-measured): the pinned objectui grep (no local objectui checkout; Implemented-by: branch claude/issue-17783-metrics-duration-unit-keys VERDICT: FAIL — two things must change, both small. (1) Generated by Claude Code |
…s the burn-rate window Contract review on PR #18007 ruled that the "Both keep their names" sentence in `system-metrics-window-durations-unit-in-key`'s acceptanceCriteria must gain a pointer. That field's contract is "how the consumer proves the hand-migration correct" — it renders as "Done when:" in the upgrade guide and "verify:" in `migrate meta` — so a normative sentence saying the burn-rate window keeps its name does not merely go stale once the key is tombstoned, it instructs a reviewer that a correct sweep was an error. Not a rewrite: every existing word is left in place and the clause is appended, per Prime Directive #13. Nor is it amending published history — the protocol-18 step is unreleased and still assembling (PROTOCOL_VERSION is 17.0.0, the guide ends at 16 to 17, and both entry ids render nowhere today), so this finishes the step rather than editing a shipped record. Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt Co-authored-by: Claude <noreply@anthropic.com>
Contract review — delta
Reviewed from an isolated worktree at the head sha ( Delta reviewed
FAIL itemsItem 1 — pointer clause on #15679's entry: DISCHARGED.
Item 2 — ablation table in the PR body: DISCHARGED.
Optional count fix: done, and reproduced three ways. Retired-key rows 175 → 182 and semantic ids 210 → 213 from Anything the delta disturbsNothing found. Readings I re-measuredAll in
Taken on trust (not re-measured): the full Implemented-by: branch claude/issue-17783-metrics-duration-unit-keys VERDICT: PASS Generated by Claude Code |
Contract review — deltaThis record supersedes comment 5653835652 for machine-readability only — its
Reviewed from an isolated worktree at the head sha ( Delta reviewed
FAIL itemsItem 1 — pointer clause on #15679's entry: DISCHARGED.
Item 2 — ablation table in the PR body: DISCHARGED.
Optional count fix: done, and reproduced three ways. Retired-key rows 175 → 182 and semantic ids 210 → 213 from Anything the delta disturbsNothing found. Readings I re-measuredAll in
Taken on trust (not re-measured): the full Implemented-by: claude/issue-17783-metrics-duration-unit-keys VERDICT: PASS Generated by Claude Code |
Contract review — deltaThis record supersedes comment 5653920672 for machine-readability only — that record's Head-sha: Reviewed from an isolated worktree at the head sha ( Delta reviewed
FAIL itemsItem 1 — pointer clause on #15679's entry: DISCHARGED.
Item 2 — ablation table in the PR body: DISCHARGED.
Optional count fix: done, and reproduced three ways. Retired-key rows 175 → 182 and semantic ids 210 → 213 from Anything the delta disturbsNothing found. Readings I re-measuredAll in
Taken on trust (not re-measured): the full Implemented-by: claude/issue-17783-metrics-duration-unit-keys VERDICT: PASS Generated by Claude Code |
Carriers cleared — provenance
Full acceptance is on the card: #17783 comment
⛔ Neither re-post changed the verdict, the findings, or a single ①②③ line. The reviewer verified its third draft by importing the parser directly —
⛔ No approval is given or implied. The clause-② review clears the queue gate; it is not a review approval, and this seat neither approves nor merges. Generated by Claude Code |
`registry.ts` is generated from `src/migrations/entries/` and is deliberately NOT_DRIVER_MANAGED, so it conflicts textually on every pair of parallel registrations — #18007 (#17783, metrics) landed 9 rows and 4 semantic entries while this branch carried 5 of its own. The entries directory itself merged clean at 579 rows with zero conflict markers, which is what that directory exists for, so the conflict is resolved by regeneration, never by hand. Set difference over the entry set, by exact path: 0 lost / 13 gained against this branch's pre-merge head, 0 lost / 5 gained against origin/main. The generator's own line reads 215 semantic, 186 retired-key, 178 retired-def. Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt Co-authored-by: Claude <noreply@anthropic.com>
Fixes #17783
Clause-②: yesExecutes director-seat ruling A on #15939 (2026-09-11, maintainer 「同意」, decision batch #115) for
packages/spec/src/system/metrics.zod.ts— the sixth of the seven per-file remediation cards. PR #17635 (the gate) lands last, into a tree these cards have cleaned.The five rows, re-located by symbol path
Line numbers from #17635's enumeration rot; each row was re-located by symbol and its JSDoc read.
MetricDefinition.summary.maxAgemaxAgeSecondsServiceLevelObjective.errorBudget.burnRateWindows[].windowdurationSecondsMetricExportConfig.intervalintervalSecondsMetricsConfig.collectionIntervalcollectionIntervalSecondsMetricsConfig.retention.perioddurationSecondsEvery value is seconds as before; every default (600, 60, 15, 604800) is unchanged. Four of the five carried no
.describe()at all, so the reference page published a bare integer.Decoys — measured, and none of them moved
Measured at
fc28c1d38, occurrences viagrep -o | wc -l(grep -ccounts lines):window:346MetricAggregationConfig.windowand:442ServiceLevelIndicator.window, bothz.object({period:509ServiceLevelObjective.period, az.object({maxAge·interval·collectionIntervalLit control on that file
z.number= 38; dark control (fabricated token) = 0. All three decoys are objects that already hold adurationSecondsof their own from #15679, and a pin in this PR asserts none of them moved.Suffix:
Seconds, and why three of the five are not the mechanical nameCounted in key position across
packages/spec/src/**/*.zod.tsatfc28c1d38:Seconds40 ·Sec1 ·S0. The singleSecismaxExecutionTimeSec; the two bare*S:hits on the corpus aremaxCommitTimeMS(a millisecond spelling) andenableRLS(a boolean), so as a seconds suffixSreads 0. At this PR's headSecondsis 45 — the five added here.This file already has a stated, reasoned naming convention from #15679, recorded in its two tombstone entries, and counting alone cannot see it:
burnRateWindows[].window→durationSeconds, notwindowSeconds. It is the fourth window length on this file; [#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 renamed the other three todurationSecondsprecisely "so the three measurements now read alike", and rejectedwindowSecondsbecause the parent key was alreadywindow. The same stutter applies here against the enclosingburnRateWindowsarray. Independently: on this treewindowSecondsis not an authorable key at all — its only key-position occurrence issystem/stack-server.zod.ts:88, an entry inServerRateLimitConfigSchema'saliasesmap that maps the spelling away towindowMs.retention.period→durationSeconds, notperiodSeconds.periodis calendar vocabulary elsewhere in this spec (ServiceLevelObjective.period.typeselects rolling or calendar;PluginRegistryEntry.pricing.billingPeriodis monthly or yearly), soperiodSecondskeeps the ambiguous half of the name — the objection [#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 raised againstsizeSeconds.periodSecondsis attested 0 times on this tree;durationSeconds4.collectionInterval→collectionIntervalSeconds, keeping the qualifier, becauseMetricExportConfig.intervalSecondsis a different cadence one def over that this same PR creates. The qualifier-plus-IntervalSecondscompound is attested:syncIntervalSeconds,refreshIntervalSeconds,healthCheckIntervalSeconds.The two mechanical spellings are attested rather than invented:
maxAgeSeconds—AccessControlConfig.maxAgeSecondsonsystem/object-storage.zod.tsis the landed rename of amaxAgeunder this same rule, tombstone and all. It also keeps theagestem that the sibling keyageBucketscounts buckets of;durationSecondswould have orphaned that pair.intervalSeconds— 4 key-position occurrences, every one a seconds-valued cadence.The pin this card was written to trip
metrics.test.tscarriedit('leaves the two non-duration keys on this file alone'), holding the burn-ratewindowbare because it is "outside the gate population entirely". This card is the sweep that guard was written to catch, so it succeeds by failing. It bundled two subjects; the exporter batchsizehalf is a COUNT of records, still true, and survives untouched under a header narrowed to name only it. Thewindowhalf is replaced by a refusal pin, an acceptance pin at the same 3600, and a describe pin, in a block whose own header stays true. ⛔ Nothing deleted, skipped or loosened.- outside the gate population entirely→+ inside the gate's census and outside its verdict), and its own text says the pin "must be re-read, not trusted, when the rename lands". That hunk has no subject after this PR; expect a conflict there and drop that half of its repair.Kit
retiredKey()tombstone per key (⛔ none of the five enclosing shapes is.strict()— measured: 0.strict()on this file — so a bare deletion would silently strip). Every tombstone const is declared above the schema that reads it:gen:schemaandcheck:authorable-surfacerun withOS_EAGER_SCHEMAS=1, which makeslazySchemaevaluate at module load, and a const below its reader is a TDZ read (the trap PR feat(spec)!: RuntimeConfigresourceLimits.timeoutcarries its unit in the key name (#17781) #17983 hit).system-metrics-jsdoc-durations-unit-in-key+ fiveRETIRED_KEYS_BY_MAJOR[18]rows. It opens by stating how it relates to [#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'ssystem-metrics-window-durations-unit-in-keyrather than rewriting it.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's entry gains a pointer clause (contract review,bcfdd35609). ItsacceptanceCriteriaended "Both keep their names." — and that field's contract is how the consumer proves the hand-migration correct, rendering as "Done when:" in the guide and "verify:" inmigrate meta, so a normative sentence saying the burn-rate window keeps its name instructs a future reviewer that this correct sweep was an error. Every existing word is left in place and a clause is appended naming the successor (Prime Directive [WIP] Add Chinese version of the documentation #13 pointer, not a rewrite). This is not amending published history: the protocol-18 step is unreleased and still assembling —PROTOCOL_VERSIONis17.0.0,docs/protocol-upgrade-guide.mdends at "Protocol 16 → 17", and both entry ids render 0 times in the guide, re-measured on this head.stack.zod.tsdeclares no metrics collection and none of these defs is a storedsys_metadatarow — the reading [#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 already recorded for this file.minorchangeset with the**BREAKING**banner, FROM → TO for all five, andadr-0087: registered.Verification
pnpm --filter @objectstack/spec buildos-verify-lock.sh,VERDICT command-exit 0(latest on head84e62ed712)pnpm --filter @objectstack/spec testpnpm --filter @objectstack/spec typecheckcheck:generated84e62ed712after the pointer commitdispatch-gatesderived familiesbcfdd35609; the only change since is the #15679 pointer clause and itsregistry.tsmirrorcore·verify·cli·resttypecheckobservability·downstream-contract·http-conformancetest.objectui-shawindow2710 ·timeout832 ·period160 ·interval156 ·metrics301; dark 0 ⇒ no pin bump owedConsumers. Outside
packages/spec, every occurrence of every distinctive key on these shapes (burnRateWindows,errorBudget,downsampling,collectionInterval,cardinalityLimits,maxLabelCombinations,ageBuckets) is in the generatedcontent/docs/references/system/metrics.mdx, which this PR regenerates — lit controldefineStack1195 on the same corpus, dark 0. Zero in-repo code consumers, confirming the dispatch's own measurement.Ablation (both hardest pins,
EXIT INT TERMtrap, byte-identity proven):window: retiredKey(SLO_BURN_RATE_WINDOW_RETIRED),8768749…≠a747965…a747965…,git diff HEADempty,git status --porcelainemptyperiod: retiredKey(RETENTION_PERIOD_RETIRED),8a84628…≠a747965…The two legs fail differently, and prove different things — corrected here after the contract review caught the body claiming they proved the same one:
AssertionError: expected undefined to be definedatmetrics.test.ts:622.result.successwas stillfalse, but no issue landed aterrorBudget.burnRateWindows.0.window: the siblingdurationSecondson that array element is required, so with the tombstone gone the parse is refused anyway for a missing required key. What leg 1 proves is the lost prescription — the author gets a bare "required" refusal instead of the FROM → TO rename message the tombstone carries. ⛔ It is not a silent-strip demonstration.AssertionError: expected true to be falseatmetrics.test.ts:659—result.successwastrue.retention.durationSecondsis.optional().default(604800), so with the tombstone gone the unknownperiodkey is accepted and stripped and the parse succeeds. This is the ADR-0049 silent strip, live.Both legs are valid evidence that their pin can fail, which is what an ablation is for. Leg 1 also answers Zone 2.4's first question by test rather than assumption:
retiredKey()on an array-element object refuses exactly as it does on a plain nested object, at patherrorBudget.burnRateWindows.0.window.Second-lander merge.
bash scripts/pm/os-regen-merge.sh(⛔ never a hand-merge) after #17999 landed. Step 2 took main's side of both shards; the regeneration commit rebuilt them from the merged source. Set-difference over the whole registry across the merge: retired-key rows 175 → 182, 0 lost, 7 gained; semantic ids 210 → 213, 0 lost, 3 gained. Those totals aregen:migration-registry's own printed line, not a hand-rolled census — an earlier revision of this body carried 177 → 184 and 209 → 212 from a regex overregistry.ts, which miscounts; the review could not reproduce them and was right. The deltas were identical under both methods, and are corroborated a third way by file count:entries/retired-keysholds 177 files onmain@8261ff7171+ this card's 5 = 182, andentries/semantic212 + 1 = 213 — so nothing was dropped. #17782's four logging rows and itslogging-durations-unit-in-keyid re-assert at 4 / 2 / 2 / 4 / 10 — matching the pre-merge baseline exactly — with lit controlkernel-runtime-config-timeout-unit-in-key4 and dark control 0.Array path notation —
system/ServiceLevelObjective:errorBudget.burnRateWindows.windowuses plain dots with no bracket token. Settled by the contract review, which closed the evidence gap I had declared: the twochange-management.zod.tsprecedents do cross an array element with plain dots (reachable atbf1054a4c0despite the shallow clone), and there is a live precedent I had missed —kernel/Manifest:contributes.kinds.globs, wherekinds: z.array(strictObject({containsglobs: retiredKey(.Acceptance notes
check-widening-tellsT1 onretiredKey()lines — the known inverted false positive [finding] check-widening-tells fires T1 on a retiredKey() tombstone line, so every ADR-0087 key retirement reads as a clause-2 widening for the one reason the accept set shrank #17955. Not reshaped, not weakened. In this run the whole derived family exited 0.resolutionis a sixth JSDoc-only duration on this file, and no gate will ever say so.MetricsConfig.retention.downsampling[].resolution— JSDoc "Resolution in seconds", describe "Downsampled resolution" — is outsidecheck:duration-unit-keyson the name axis (resolutionis not inDURATION_SHAPED_TOKENS), so unlike finding: check:duration-unit-keys reads .describe() but not JSDoc — a duration key documenting its unit only in JSDoc never enters the population, and one card already recorded a wrong reason because of it #15939's prose axis, PR feat(spec): refuse a duration key whose JSDoc names a unit its describe does not #17635's widening does not reach it. Not folded in here: triage certified this batch as exactly 21 rows with "⛔ 无第八张". Filed as spec: MetricsConfig.retention.downsampling[].resolution names its unit only in JSDoc — and is outside check:duration-unit-keys on the NAME axis, so #17635 will not reach it #18030.⛔ Draft. Not ready, not enqueued, no auto-merge. The in-seat clause-② contract review is owed first and landing is the PM's step.
Generated by Claude Code