Skip to content

feat(spec)!: the four system/tracing.zod.ts duration keys carry their unit in the key name (#17785) - #18016

Merged
zhuangjianguo merged 7 commits into
mainfrom
claude/issue-17785-tracing-duration-units
Sep 13, 2026
Merged

zhuangjianguo merged 7 commits into
mainfrom
claude/issue-17785-tracing-duration-units

Conversation

@claude

@claude claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Fixes #17785

Clause-②: yes — each rename puts a spelling on OpenTelemetryCompatibility and
TracingConfig that no author could write before. minor, feat(spec)!, with an
adr-0087: registered disposition (epic settlement on #15939, correcting Ruling A's
own patch).

Executes director-seat ruling A on #15939 (2026-09-11, maintainer 「同意」, decision
batch #115), the per-file remediation of the #14478 rule — the seventh and last of
that ruling's seven cards.

The four rows

Re-located by symbol path on origin/main @ 98bd7986fe; line numbers came from PR
#17635's enumeration and are re-derived here, not trusted.

symbol path before after default
OpenTelemetryCompatibility.exporter timeout timeoutMs 10000
OpenTelemetryCompatibility.exporter.batch exportTimeout exportTimeoutMs 30000
OpenTelemetryCompatibility.exporter.batch scheduledDelay scheduledDelayMs 5000
TracingConfig.performance exportInterval exportIntervalMs 5000

Each declared exactly once in key position, lit control z.number 20 on the file, dark
control 0. All four carried no .describe() at all — not "a describe that names no
unit", but none — so the published reference row was a bare integer with no unit anywhere
on the page. Values, defaults and int().positive() bounds are unchanged.

The suffix is Ms, settled from tree counts

Key position across packages/spec/src: 281 *Ms declarations over 42 distinct
names — timeoutMs 65, intervalMs 14, durationMs 73 — against 0 key-position
timeoutSeconds and 77 *Seconds of any name. exportTimeoutMs / scheduledDelayMs /
exportIntervalMs have 0 competing spellings of any form. The Delay-plus-Ms pairing is
already attested (maxDelayMs, initialDelayMs, retryDelayMs, delayMs,
debounceDelayMs). This file's own landed precedent is Span.duration to durationMs
(#15679) — the opposite of the sibling metrics card, whose rows were seconds; every
JSDoc here was read individually and every one says milliseconds.

The two *TimeoutMs keys one nesting level apart — accepted, and why. The near-duplicate
pre-exists the rename: the batch sub-object is the OpenTelemetry batch span processor's own
four knobs (max batch size, max queue size, scheduled delay, export timeout) sitting beside the
exporter's own request deadline. Renaming either to something more distinctive would depart from
the vocabulary this shape mirrors, and the nesting already disambiguates every read point —
exporter.timeoutMs versus exporter.batch.exportTimeoutMs, never interchangeable because they
live in different objects. Rejected alternative: requestTimeoutMs for the outer one, which
would have invented a spelling OTel does not use to solve a problem the nesting already solves.

The ratchet moves nothing — and that is the correct outcome, not an omission

Verified from the generator source, not assumed. packages/spec/scripts/build-schemas.ts:852
reads schema.properties one level deep and records ${defKey}:${name}, so only
top-level keys per def ever enter authorable-surface/ or authorable-defaults/.
Corroborated on the committed artifacts: system/OpenTelemetryCompatibility:exporter is a
single row (the whole nested object), there are 0 dotted keys anywhere in
authorable-surface/system.json and 0 in authorable-defaults/system.json against 247
total keys there, and the lit control system/Span:duration [RETIRED] is present.
All four of my rows are nested. check:authorable-surface is green without any
regeneration
, and git diff --name-only against the base matches 0 files under
authorable-surface or authorable-defaults. This is the #17954 reading, not #17986's.

The pin guard this card was written to trip

packages/spec/src/system/tracing.test.ts:545
it('leaves the OTel exporter timeout alone — its describe names no unit, so it is outside the population'). It succeeds by failing: its key, its "names no unit" clause and its
"outside the population" clause all go false here. It is neither deleted, skipped, weakened
nor fixed green — it is replaced and relocated out of a describe headed
Span.duration carries its unit (#15679), which is no longer its subject, into a new
#17785 block carrying the three-part shape #15679 itself set on this file: a refusal pin
per row asserting the issue code (not.toBe('unrecognized_keys')) and the FROM to TO
prescription, an acceptance pin at each retired key's magnitude with the same default, a
bounds pin, and a describe pin proving the unit now reaches the published channel.

Stale-prose sweep (file:line)

Swept packages/spec/src, packages/spec/scripts and content/docs for sentences this
card falsifies (outside the population · names no unit · no unit at all ·
left alone · leaves ... alone · untouched here · stays bare), then narrowed to
hits whose SUBJECT is one of my four keys. Lit control unit 2308 on that corpus, dark
control 0.

file:line disposition
packages/spec/src/system/tracing.test.ts:545 this card's — replaced and relocated, above
packages/spec/CHANGELOG.md:2866 left alone, reported. "ServiceLevelObjective.errorBudget's burn-rate window and the OpenTelemetry exporter timeout name no unit anywhere in their prose, so both are outside the gate's population entirely." Two limbs go false — mine here, the window one at #17783. packages/*/CHANGELOG.md is RELEASE-OWNED (AGENTS.md): never edited in a code PR; a factual error in a released entry is amended in a dedicated docs-only PR. It is also jointly owned by a sibling card, so it is not this PR's sentence to rewrite. Raised in the report.
packages/spec/src/migrations/entries/semantic/18.system-tracing-span-duration-unit-in-key.ts left alone, deliberately. Its "it is the only offender on its file" is scoped to ruling B's describe-channel population, which never contained these four. Per the epic settlement, a predecessor's semantic entry is never amended; this round's entry instead opens by saying how it relates to it.

Consumers

Zero in-repo readers outside packages/spec. OpenTelemetryCompatibility,
TracingConfig, exportTimeout, scheduledDelay and exportInterval each occur 0
times across the whole tree at 98bd7986fe excluding packages/spec and
content/docs/references, against a lit control of 18920 Schema occurrences on
exactly that corpus — both counts from one git grep -o over that ref carrying both
pathspec exclusions — and a dark control of 0. The named consumer packages were each run anyway rather than reasoned
about — including the package being edited, the #17986 lesson.

Pinned objectui, .objectui-sha = 53ded82bf7a494f54e344e19099dbf00854b8694, 6409
tracked files: all 37 exports of tracing.zod.ts and each of the four key names occur
0 times. The 404 Span and 40 SpanSchema raw hits are objectui's own HTML text-span
component (TextSpanSchema) — a different name, read rather than counted. Lit controls
objectstack 10171 and @objectstack/spec 3479 on the same corpus; dark control 0.

Rollback is therefore consumer-free: reverting this PR restores the four bare spellings
and drops four tombstones no caller in this repo, and none in the pinned sibling, reads.

The kit

  • retiredKey() tombstone per row — none of these shapes is .strict(), so
    unrecognized_keys was never the alternative: a bare deletion is an ADR-0049 silent
    strip landing a default on an exporter deadline and a background export period
  • ADR-0087 D3 semantic entry system-tracing-otel-exporter-durations-unit-in-key plus
    four RETIRED_KEYS_BY_MAJOR[18] rows (the epic settlement: both, every time)
  • registry.ts regenerated with gen:migration-registry, never hand-merged
  • content/docs/references/system/tracing.mdx regenerated with gen:docs
  • minor changeset with the BREAKING banner, the FROM to TO mapping for all four and
    the adr-0087: registered disposition

Acceptance notes

Authored by the os-dev seat for the #15939 epic PM, session session_015c5G6TmpMKgnusmTpD7Ntt (https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt).


Generated by Claude Code

…ir unit in the key name

Ruling A on #15939 (2026-09-11, maintainer 「同意」, decision batch #115),
per-file remediation of the #14478 rule — the seventh and last card.

`exporter.timeout`, `exporter.batch.exportTimeout`,
`exporter.batch.scheduledDelay` and `performance.exportInterval` each named
milliseconds in a source JSDoc and carried no `.describe()` at all, so the
published reference row was a bare integer with no unit anywhere on it.

Each is renamed with the `Ms` suffix, tombstoned with `retiredKey()`, and
registered as an ADR-0087 D3 semantic entry plus a `RETIRED_KEYS_BY_MAJOR[18]`
row. All four are nested, so the authorable-surface / authorable-defaults
ratchets move nothing.

Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 4 changed file(s) yielded no anchor (packages/spec/src/migrations/entries/retired-keys/18.system__OpenTelemetryCompatibility__exporter.batch.exportTimeout.ts, packages/spec/src/migrations/entries/retired-keys/18.system__OpenTelemetryCompatibility__exporter.batch.scheduledDelay.ts, packages/spec/src/migrations/entries/retired-keys/18.system__OpenTelemetryCompatibility__exporter.timeout.ts, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 4 changed file(s) yielded no anchor (packages/spec/src/migrations/entries/retired-keys/18.system__OpenTelemetryCompatibility__exporter.batch.exportTimeout.ts, packages/spec/src/migrations/entries/retired-keys/18.system__OpenTelemetryCompatibility__exporter.batch.scheduledDelay.ts, packages/spec/src/migrations/entries/retired-keys/18.system__OpenTelemetryCompatibility__exporter.timeout.ts, …) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ff1e7716c5680938fdaa8d100ad3a2156c5d87e0packageMentionDocs.

Which tree this was computed on

This run read content/docs from a9d1149341a5cd3ba342e2ac44a9434d0c7b7834 — the merge of head 1de92aba380312d8f83d0b16b384249fbb6776ca into base ff1e7716c5680938fdaa8d100ad3a2156c5d87e0, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a9d1149341a5cd3ba342e2ac44a9434d0c7b7834 && git checkout a9d1149341a5cd3ba342e2ac44a9434d0c7b7834
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ff1e7716c5680938fdaa8d100ad3a2156c5d87e0 1de92aba380312d8f83d0b16b384249fbb6776ca && git checkout -B drift-repro ff1e7716c5680938fdaa8d100ad3a2156c5d87e0 && git merge --no-ff 1de92aba380312d8f83d0b16b384249fbb6776ca

node scripts/docs-audit/affected-docs.mjs --json ff1e7716c5680938fdaa8d100ad3a2156c5d87e0

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

…to its ref

A count in a migration entry ships to consumers through the upgrade guide and
is permanent published prose, so a number a future reader cannot reproduce is
the same defect class this epic exists to remove. Every family count now names
`98bd7986fe` and its corpus, and the objectui readings name the pinned sha.

Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator

Lint & Repo Gates is a real red on this head, and its cause is a PM instruction — fix in flight

Epic PM for #15939 · 2026-09-13T13:50Z. ⚠️ Unlike the earlier reds in this epic, this one is not a superseded head: c3d53951a9 is still the live head, and the failure is genuine.

What failed, job 103730395921, step pnpm check:cli-command-ids:

✗ check-cli-command-ids: command-id literal(s) that resolve to no command path:

  packages/spec/src/migrations/entries/semantic/18.system-tracing-otel-exporter-durations-unit-in-key.ts:69
    literal: "objectstack 10171 and"  ->  no such command under packages/cli/src/commands/
    + 'objectstack 10171 and @objectstack/spec 3479 on that same corpus and sha.',
  packages/spec/src/migrations/registry.ts:10734
    literal: "objectstack 10171 and"  ->  no such command under packages/cli/src/commands/

Root cause is mine, not the diff's reasoning. After a sibling PR baked an unreproducible count into published migration prose, I required every count in migration prose to carry the ref it was measured at. This round did exactly that — and the phrasing placed the bare token objectstack immediately before a number, which check:cli-command-ids reads as a quoted CLI command invocation. The instruction and the gate collide; the dev could not have known.

Fix in flight, per the gate's own remedy line ("reword it so it is not a quoted command phrase"): reword so the token is not followed by a bare word inside the string, chosen against check-cli-command-ids.mjs's delimiter rule rather than guessed, then regenerate registry.ts so the mirror at :10734 moves with the entry. ⛔ The counts and the ref stay — the provenance rule is not being dodged, only the phrasing changes.

⚠️ Do not read the rest of that job as passing. It stops at the first non-zero exit by design, and its own epilogue says so: unmeasured-gate-tail: measured=no never_ran=NOT_MEASURED. The gates behind check:cli-command-ids never ran, so a green on the next push is the first evidence about them.

An at-tier contract review is running against c3d53951a9. The incoming push is scoped to exactly "reworded count prose + regenerated mirror" so its verdict can be reconciled against the new head honestly rather than re-run from scratch.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Contract review

Head-sha: c3d53951a940fdb10a2aa2d3559bc9031bacbc41

Isolated at-tier review for the domain:spec seat. Own worktree at the head sha (/tmp/review-18016, merge-base with origin/main = 98bd7986fe, origin/main = 1e20f816ea at read time). Every reading below is mine unless it is listed under "taken on trust". Dark control token for this review: vxkqzt (0 everywhere it was run).

① Derived judgments

  • OpenTelemetryCompatibility.exporter.timeout → timeoutMs (default 10000) — PASS. At origin/main:packages/spec/src/system/tracing.zod.ts:541 the key was z.number().int().positive().optional().default(10000) under a JSDoc "Timeout in milliseconds" and carried NO .describe(); the rename keeps value, default and int().positive() bounds; the new describe names milliseconds; the tombstone refuses with invalid_type at path exporter.timeout and the FROM → TO prescription (executed, not read).
  • exporter.batch.exportTimeout → exportTimeoutMs (30000) — PASS, same shape; JSDoc "Export timeout in milliseconds", no describe on main (:565); refuses with invalid_type at exporter.batch.exportTimeout.
  • exporter.batch.scheduledDelay → scheduledDelayMs (5000) — PASS, same shape; JSDoc "Scheduled delay in milliseconds", no describe on main (:570); refuses at exporter.batch.scheduledDelay. The Delay-plus-Ms pairing is attested on the ref (see counts).
  • TracingConfig.performance.exportInterval → exportIntervalMs (5000) — PASS, same shape; JSDoc "Background export interval in milliseconds", no describe on main (:723); refuses at performance.exportInterval.
  • Unit claims true against JSDoc and bounds — PASS: all four JSDocs say milliseconds; each key declared exactly once in key position on main (lit z.number 20 on the file, dark 0); 0 of the 4 carried a .describe() (lit: 64 .describe( on the file). The author's correction to the card ("no describe at all", not "a describe without the unit") is measured and right.
  • Ms is the family spelling here — PASS: the file's only suffixed key on main is durationMs (:220, from [#14478 stack 4/6] system/: the 15 remaining duration keys carry their unit in the key name — ADR-0087 conversions with readers; metrics.zod.ts size needs an honest name, not the mechanical one #15679; 0 *Seconds keys on the file), and in key position across packages/spec/src at 98bd7986fe there are 281 *Ms declarations over 42 names (durationMs 73, timeoutMs 65, intervalMs 14) against 0 timeoutSeconds. The two *TimeoutMs one level apart mirror the OTel batch-processor vocabulary; accepted.
  • Each tombstone refuses (ADR-0049) — PASS: 0 .strict() and 0 strictObject on the file, so without a tombstone the alternative is a silent strip, not unrecognized_keys — proven by ablation leg 1 (below): with the exporter.timeout tombstone removed, safeParse SUCCEEDS. Strictness matters only for which alternative the tombstone displaces; here it is the only audible route, and retiredKey() on a non-strict shape is the route AGENTS.md prescribes. Type level: a scratch tsc proves all four old spellings fail to compile (z.input types them never) and all four new ones compile, with a control leg that goes red when one @ts-expect-error is removed.
  • Adding a .describe() where none existed — PASS, inside ruling A's shape, not an unflagged widening: batch Add comprehensive test coverage for ObjectStack spec protocols - 100% coverage achieved #65's adopted remedy is "move the unit into the describe, where the existing rule then puts it in the key name"; the landed siblings do exactly this (Span.duration → durationMs carries .describe('Duration in milliseconds') on main; feat(spec)!: the three kernel/plugin-lifecycle-advanced.zod.ts duration keys carry their unit in the key name #17986's three renamed keys each gained a unit-naming describe). A describe adds no key or value to the accept set; its published effect is the regenerated reference page, gated by check:docs (green), and it is disclosed in the changeset and the entry.
  • ADR-0087 kit — PASS: one D3 semantic entry file + four retired-key entry files; registry.ts is generator output (check:migration-registry green in my check:generated run; and regenerating on the merged tree reproduces the text merge byte-for-byte, below). D3-not-D2 is right for these defs: packages/spec/src/stack.zod.ts names no tracing/telemetry collection (0 hits), MetadataTypeSchema has no tracing type (0 hits), no other def in packages/spec/src embeds either schema (0 references outside tracing.* and migrations/), 0 in-repo readers outside packages/spec at the ref, and every sibling id of this epic is likewise absent from conversions/registry.ts (D3 only). Note for the record: step-18 entries are not yet projected into spec-changes.json or docs/protocol-upgrade-guide.md (the guide renders majors up to PROTOCOL_MAJOR = 17), so this entry publishes when 18 is cut — same as every sibling.
  • Ratchet moves nothing — PASS, and it is a correct no-movement, not a forgotten regeneration: build-schemas.ts:848-856 iterates schema.properties one level and records ${defKey}:${name}; both system shards carry 0 dotted keys (1307 keys in authorable-surface/system.json, 247 in authorable-defaults/system.json); system/OpenTelemetryCompatibility:exporter and system/TracingConfig:performance are single rows; lit system/Span:duration [RETIRED] present. All four keys sit under exporter, exporter.batch or performance. check:authorable-surface ran and passed at head in my check:generated run with 0 shard files in the diff — the gate that would red on a stale shard was exercised, which is what distinguishes this from a forgotten regeneration. One pre-existing blind spot worth knowing: RETIRED_KEYS_BY_MAJOR dotted rows never enter currentKeys, so checks (b)/(b2) cannot verify them; I verified the four spellings by hand against the schema paths.
  • Pin guard tracing.test.ts:545 — PASS: replaced and relocated, not deleted/skipped/weakened. The old it('leaves the OTel exporter timeout alone …') is gone from describe('Span.duration carries its unit (#15679)'), whose header would otherwise be falsified; a new #17785 describe carries 3+1 refusal pins asserting the issue code is not unrecognized_keys plus the prescription, an acceptance pin at the old magnitudes with the same 10000/30000/5000/5000 defaults, a bounds pin, and a describe pin. 45/45 green at head.
  • Counts carrying a ref — PARTIAL. Re-measured at 98bd7986fe over packages/spec/src *.ts: 281 / 42 / 73 / 65 / 14 / 0 timeoutSeconds / 77 *Seconds / maxDelayMs 9, initialDelayMs 9, maxRetryDelayMs 5, debounceDelayMs 2, delayMs 2, retryDelayMs 1 / 0 competing exportTimeout*, scheduledDelay*, exportInterval* spellings — all reproduce exactly. In-repo readers: 0 for all five subjects on the stated corpus — reproduces. objectui at 53ded82bf7a494f54e344e19099dbf00854b8694: 6409 tracked files, 0 for the four keys and both defs, 0 for all 37 exports except Span / SpanSchema (16 / 2 word-boundary; 404 / 40 substring — objectui's own HTML span component, no @objectstack/spec tracing import), lit objectstack 10171 and @objectstack/spec 3479 — all reproduce. ✗ One does not: the entry says "0 times across the whole tree at that ref excluding packages/spec and the generated reference pages, against a lit control of 20875 Schema occurrences on that same corpus and ref". On that corpus the substring count is 18920; 20875 is the count with content/docs/references still included (git grep -o Schema 98bd7986fe -- . ':!packages/spec'). The subject zero cannot have been taken on that wider corpus (the def names occur in content/docs/references/system/tracing.mdx), so the zero and its control were measured on different corpora and the sentence says otherwise. Same number in the PR body. Exactly the class the ref requirement exists to stop.
  • Cross-file staleness sweep (whole tree at head, no package excluded; single-line and multi-line exporter{…timeout} / batch{…} / performance{…} shapes; hand-written content/docs, skills/, .claude/skills/, examples/, apps/, docs/; lit timeoutMs 61 on that corpus, dark 0) — the only falsified sentence is the one the author found, packages/spec/CHANGELOG.md:2866. Every other occurrence of the four names is this PR's own changeset, entries, tombstones, tests (the :552 comment quotes the replaced pin deliberately), or the regenerated tracing.mdx. The predecessor entry's "only offender on its file" is scoped to ruling B's describe-channel population and stays true in that scope. Nothing missed that I could find.
  • Clause ② — node scripts/pm/check-clause2-carriers.mjs --pair 18016EXIT=0 (13:31:48Z), reading Clause-②: yes from correction comment 5653031207 superseding claim 5652963724; it warns "ATTRIBUTION NOT VERIFIED" because the claim carries no Session: line. Independently: yes is right — four keys an author could not write before are now accepted on two published defs. And because all four are nested, check:authorable-surface is blind to this widening, so the declaration is the ONLY carrier here; it must stay yes.
  • Ablation (my own run, under an EXIT INT TERM trap, restored via git checkout HEAD -- …, byte identity proven by blob hash and empty porcelain): leg 1 (remove the exporter.timeout tombstone; blob 81c8fb8b → d34d7cbc) → vitest exit 1, exactly 1 failed / 44 passed, AssertionError: expected true to be false at the exporter.timeout refusal pin — i.e. safeParse succeeded, the ADR-0049 silent strip; leg 2 (drop the exportIntervalMs describe; blob → b673b0cd) → exit 1, exactly the describe pin red (expected undefined to be 'Background span-export…'). Both restored to 81c8fb8b. The counts are the right counts: the removed tombstone is exercised by exactly one pin (the it.each row), and the describe is read by exactly one; each leg proves what it claims.
  • CI at head — FAIL: the required context Lint & Repo Gates (job 103730395921) concluded failure at 13:47:50Z on pnpm check:cli-command-ids: the semantic entry's reason at 18.system-tracing-otel-exporter-durations-unit-in-key.ts:69 (and its generated copy in registry.ts) now has a string literal that BEGINS 'objectstack 10171 and @objectstack/spec 3479 …', which the gate reads as a quoted objectstack … command phrase resolving to no command. Reproduced locally: EXIT=1 at head; EXIT=0 at the previous commit 96e730f0d2, where the same numbers sat mid-string (… lit controls objectstack 10171 and ') — the final "anchor every count to its ref" commit re-wrapped the concatenation and tripped it. The job stops at the first red, so the 80 check:* gates behind that step in lint.yml (144 steps) are NOT MEASURED. The other five required contexts are success. Note check:cli-command-ids is absent from the 109-command set dispatch-gates.mjs --commands derives, so the author's "109 derived / 109 run / 0 NOT-MEASURED" reconciliation could not have seen this gate — a derivation gap for the PM, separate from this PR.

acceptRetiredDefaultResidue() — the primary question

1. What it does that retiredKey() does not, and what it prevents. retiredKey(guidance) is z.never({ error }).optional().describe('[REMOVED] …'): absence parses, any value refuses with the prescription, z.input is never. acceptRetiredDefaultResidue(schema, residue) (retired-key.ts:182-211) wraps a ZodObject in a z.preprocess stage that, for each residue key, deletes the key from the input when and only when input[key] === residue[key] (identity against a literal captured at retirement time — the test pins that 0/''/null are not residue for a false default), then hands the body to the inner shape whose tombstone still refuses every other value byte-for-byte; it also re-attaches a read-through shape. The strip is deliberately silent. What it prevents is a specific death: the parse that BUILT an artifact under the old toolchain emitted the default (allowRestore: false in 75 permission entries of one real HotCRM artifact, per the docblock and permission.zod.ts:78-88), so under a plain tombstone every previously built artifact, stored row or old-server response carrying that emitted default refuses with invalid_type expected never on the next upgrade — "sentences every previously built artifact … to death". The class test (retired-key.test.ts) and the founding pin (plugin-artifact-forward-conversion.test.ts, the hotcrm-17.1-built-permissions.artifact.json fixture parsed by _parseAndRegisterArtifact) both describe that population and no other.

2. The precise precondition, verified. From the helper's own founding case and both call sites, three things must hold: (a) the key carried a .default() before retirement, so a released toolchain EMITTED it; (b) the schema's parsed output — defaults materialised — is serialised by that toolchain into something a newer toolchain later RE-PARSES through the same shape; (c) the emitted default is behaviourally identical to absence, so stripping it is inert. The two call sites are the only two in the tree (permission.zod.ts:415 ObjectPermissionSchema, whose output is built into plugin artifacts and stored in sys_permission_set.object_permissions; permission.zod.ts:455 EffectiveObjectPermissionSchema, the /me/permissions wire response a 17.x server still emits with the defaults materialised and a newer client parses). What they share is exactly (b): parsed output crossing a serialisation boundary — built artifact, stored row, wire — and re-entering a parse. The author's reading ("needed only where an authorable surface's parsed output is STORED and re-parsed") is that condition, and I verify it holds nowhere for these two defs: not a stack collection (stack.zod.ts 0 hits), not a metadata type (metadata-plugin.zod.ts 0 hits, so never a sys_metadata row), not embedded in any other def (packages/spec/src 0 references outside the tracing files and migrations/), 0 readers in every other package at 98bd7986fe, 0 in the pinned objectui — so no toolchain has ever materialised timeout: 10000 into anything that comes back through the parse. It is the same reachability test that makes the entry D3 rather than D2 (the conversion chain walks a normalised stack and applyConversionsToStoredItem walks stored rows; a def reachable from neither has no seam), and the predecessor entry on this file recorded the same disposition. Executed: the old key at exactly its old default (exporter: { type: 'console', timeout: 10000 }) is refused at head — and that is correct here, because the only population that can present it is a hand-authored source, precisely what a tombstone exists to refuse loudly. Adding the residue stage would instead silently accept timeout: 10000 from an author's JSON/YAML with no signal (the #17425 complaint) and protect nothing. So: plain retiredKey() is RIGHT for these four, and the defaulted-key case is safe here concretely because there is no re-parsed population, not because the default happens to be the same number.

3. The five merged siblings. The evidence does not lead to them being wrong. Bounded check on the same precondition: SchemaLevelIsolationStrategySchema.schemaCacheTTL (#17954), RuntimeConfigSchema.resourceLimits.timeout (#17983), PluginHealthCheckSchema.interval/timeout and HotReloadConfigSchema.debounceDelay (#17986), HttpDestinationConfig.* and LoggingConfig.buffer.flushInterval (#17999): none is referenced from stack.zod.ts or the metadata-type registry, none is embedded in another def outside its own file (excluding tests/migrations), and all four ids are D3-only in the registries; the one def with a real consumer, HotReloadConfig, is read by packages/core/src/hot-reload.ts from in-memory plugin config, not from a store. #15679's Span.duration is runtime-emitted and never stored per its own entry; its other 14 rows I did not re-derive (shallow history) and say so. Where a future retirement DOES meet (a)+(b)+(c) — a defaulted key on a stack collection member, a metadata type, or a wire response — the helper is required and plain retiredKey() would be the defect; that is the rule to write down epic-wide (open question C on the author's report is the right vehicle), not a reason to re-open these six.

② Semver level

minor is right. major is refused in the launch window by scripts/check-changeset-no-major.mjs (no pm/ segment; EXIT=0 on this range at 13:35:12Z, level axis "not applicable" locally by construction); breaking-ness rides the **BREAKING** banner plus the adr-0087: registered system-tracing-otel-exporter-durations-unit-in-key disposition, and scripts/check-adr-0087-registration.mjs --base 98bd7986fe --head HEAD → EXIT=0. Clause-②: yes requires at least minor (AGENTS.md step 3). Landed siblings: #15678 (CHANGELOG.md:1804) and #15679 (:2764) both sit under ## 17.4.0 › ### Minor Changes; the four epic siblings' changesets (#17954, #17983, #17986, #17999) are all "@objectstack/spec": minor with a registered disposition. Changeset text vs diff: the four-row table, defaults and bounds match the schema; I executed the ## Migration block — the "after" bodies parse (otlp_grpc is a real OtelExporterType member; defaults materialise as 10000/30000/5000/5000) and the "before" bodies refuse at all four paths with invalid_type. No consumer package needs a changeset: 0 readers outside packages/spec at the ref (contrast #17986, which added @objectstack/core because core reads HotReloadConfig).

③ Boundary flags

Readings I re-measured

All UTC, 2026-09-13, in /tmp/review-18016 at c3d53951a9 unless a ref is named.

  • 13:30:37Z git worktree add; git merge-base HEAD origin/main = 98bd7986fe; origin/main = 1e20f816ea; git rev-list --count 98bd7986fe..origin/main = 11.
  • 13:31:32Z git show origin/main:packages/spec/src/system/tracing.zod.ts | grep -nE '^\s+(timeout|exportTimeout|scheduledDelay|exportInterval):':541 :565 :570 :723, once each; describe on those lines 0; lit .describe( 64; .strict() 0; strictObject 0; z.number 20; dark 0.
  • 13:31:47Z shard greps: 0 dotted / 1307 keys (surface), 0 / 247 (defaults); system/Span:duration [RETIRED] present. 13:31:34Z sed -n 800,900p packages/spec/scripts/build-schemas.ts (one-level schema.properties walk at :848-856); :1000-1060 for checks (b)/(b2).
  • 13:31:48Z node scripts/pm/check-clause2-carriers.mjs --pair 18016 → EXIT=0 (verbatim above).
  • 13:32:59Z git grep -hoE '^\s*[A-Za-z_]+Ms\s*:' 98bd7986fe -- 'packages/spec/src/**/*.ts' | sort | uniq -c → 281 / 42 names and the per-name counts above; timeoutSeconds 0; *Seconds 77; six competing spellings 0; dark 0.
  • 13:33:03Z / 13:44:07Z git grep -o <subject> 98bd7986fe -- . ':!packages/spec' ':!content/docs/references' → 0 ×5; substring Schema 18920 on that corpus, 20875 with ':!packages/spec' alone, 747 word-boundary.
  • 13:35:12Z node scripts/check-adr-0087-registration.mjs --base 98bd7986fe --head HEAD EXIT=0; node scripts/check-changeset-no-major.mjs --base 98bd7986fe --head HEAD EXIT=0.
  • 13:37:50Z npx vitest run src/system/tracing.test.ts src/shared/retired-key.test.ts src/shared/retired-key-migrate-sentence.test.ts → 3 files, 67 passed.
  • 13:39:19Z pnpm --filter @objectstack/spec build EXIT=0; pnpm --filter @objectstack/spec check:generated → 15/15 current (incl. check:migration-registry, check:authorable-surface, check:docs).
  • 13:41:07Z objectui checkout at 53ded82bf7… (remote verified): git ls-files | wc -l 6409; per-subject git grep -o … | wc -l as listed; lit 10171 / 3479; dark 0.
  • 13:41:19Z pnpm --filter @objectstack/spec check:duration-unit-keys EXIT=0 (211 keys, 0 offenders); check:objectui-pin-citations EXIT=0 (22 citations match .objectui-sha).
  • 13:41:34Z ablation legs 1 and 2 (script in scratchpad; blobs d34d7cbc / b673b0cd; restored 81c8fb8b; porcelain empty).
  • 13:46:12Z merge probe: git clone --bare --shared + merge-tree --write-tree → clean; probe commit fetched to a ref I own, git restore --source=…, gen:migration-registry, cmp → identical; ref deleted, tree restored, porcelain empty.
  • 13:46:32Z tsx run of the changeset's before/after bodies (results in ②) and of the old key at its old default (refused).
  • 13:48:11Z tsc --noEmit -p packages/observability/tsconfig.json → 11 errors (TS2554 ×10, TS2552 ×1), 0 tracing mentions.
  • 13:50:34Z scratch tsc never-proof EXIT=0; control leg (one @ts-expect-error removed) → TS2322, EXIT=2.
  • 13:52:46Z pnpm check:cli-command-ids at head EXIT=1 (two hits, …:69 and registry.ts:10611); 13:52:58Z same at 96e730f0d2 EXIT=0; 13:53:11Z dispatch-gates.mjs --commands = 109 lines, cli-command-ids absent; 13:54:35Z 144 run: steps / 80 check:* gates after lint.yml:2838.
  • Check runs read at 13:31Z and 13:52Z: Lint & Repo Gates job 103730395921 → failure (completed 13:47:50Z); TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL)success.

Taken on trust (not re-run): the per-consumer typecheck/test exit codes for core, verify, cli, rest, dogfood, downstream-contract, http-conformance and refd-timer-testkit; the os-verify-lock VERDICT lines; the dispatch-gates --ran 109/109 reconciliation; check:type-check-coverage exit 0; the "2 derivation inputs" attribution (I verified the two files moved, not the derivation); #15679's original diff (shallow history — judged from the landed shape on origin/main).

Implemented-by: branch claude/issue-17785-tracing-duration-units
Reviewed-by: session_015c5G6TmpMKgnusmTpD7Ntt

VERDICT: FAIL — (1) the required Lint & Repo Gates context is red on this diff: check:cli-command-ids reads the string literal beginning 'objectstack 10171 and @objectstack/spec 3479 …' in packages/spec/src/migrations/entries/semantic/18.system-tracing-otel-exporter-durations-unit-in-key.ts:69 as a quoted command phrase; reword that clause so no string literal starts with objectstack followed by non-command text (e.g. "the bare word objectstack 10171 times and the package name @objectstack/spec 3479 times"), regenerate registry.ts with gen:migration-registry, and re-run the gate to EXIT=0; (2) in the same entry, the lit control "20875 Schema occurrences on that same corpus" is not reproducible on the corpus the sentence names — it is 18920 there (20875 includes the reference pages) — so either write 18920 or restate the corpus, and mirror the correction in the PR body. Everything else — the four renames, the tombstones, the D3 kit, the untouched ratchet, the relocated pin, the ablation, minor, Clause-②: yes, and plain retiredKey() on defaulted keys — passes as written.


Generated by Claude Code

…k:cli-command-ids

`check-cli-command-ids` matches a bin name that is the FIRST thing inside a
quoted run and is followed by one or two bare lowercase words. The count prose
broke its string concatenation so that a line began `'objectstack 10171 and`,
which that pair reads as a quoted CLI invocation resolving to no command path.

Reworded, not dropped: both counts and the measurement sha stay, and the two
control tokens are still named. The new spelling is out on either half of the
rule alone — the token is neither first in its quoted run nor followed by a
bare lowercase word — so a later reflow of the concatenation cannot reintroduce
the match. The mirror in `migrations/registry.ts` is regenerated, not edited.

Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt
Co-authored-by: Claude <noreply@anthropic.com>
…ased package version

Two corrections from the in-seat contract review of PR #18016.

The ADR-0087 entry's lit control said 20875 Schema occurrences on a corpus it
described as the tree excluding `packages/spec` AND the generated reference
pages. 20875 is the count with `content/docs/references` still in; the corpus
the sentence names reads 18920. Both numbers now come from one `git grep -o`
over `98bd7986fe` carrying both pathspec exclusions, and the sentence names
that corpus exactly.

The four tombstone prescriptions said `@objectstack/spec 18`. That conflates
two axes: the prose names the released PACKAGE version, which is 17 (this
ships in 17.5.0), while `RETIRED_KEYS_BY_MAJOR[18]` names the PROTOCOL major.
A reader told "18" would look in a v18 that does not carry it. The tree spells
it 17 in 265 places against 14, and this file's own `Span.duration` precedent
says 17.

`registry.ts` and the reference page are regenerated, not edited.

Claude-Session: https://claude.ai/code/session_015c5G6TmpMKgnusmTpD7Ntt
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator

Contract review — delta

Head-sha: efbf3005086c21958957f8da3cc5ecd600fedc6a
Baseline: c3d53951a940fdb10a2aa2d3559bc9031bacbc41 (FAIL, comment 5653714585)

Isolated at-tier delta review for the domain:spec seat. Own worktree at the head sha (/tmp/review-18016b, created 14:17:17Z; merge-base with origin/main = 98bd7986fe, origin/main = a0dd872c1b at read time; the clone is shallow and both shas were fetched by hash). Scope is the baseline-to-head delta only; everything the baseline passed is taken as passed unless the delta disturbs it. Dark control tokens for this review: pfjqwrv and zzrmvk (0 everywhere they were run).

Delta reviewed

git diff --stat c3d5395..efbf300 → exactly the four named files, +20 / −16, in two commits: 37f29a4a01 (reword the lit-control counts out of check:cli-command-ids) and efbf300508 (correct the lit control, name the released package version). --name-status lists nothing else; both commits carry the model-free trailer pair. Nothing else moved.

  • packages/spec/src/system/tracing.zod.ts — four hunks, each one token (1817) inside a retiredKey() message. Byte proof: the baseline blob with the four tokens rewritten cmps identical to the head blob, so the delta is provably nothing but the version token — no key, bound, default, describe or behaviour moved. retiredKey( count 5 at both heads.
  • content/docs/references/system/tracing.mdx — two rendered [REMOVED] rows. Same byte proof: baseline with the two tokens rewritten cmps identical to head. A faithful regeneration, and check:docs is green in my check:generated run (below).
  • 18.system-tracing-otel-exporter-durations-unit-in-key.ts and its generated mirror in registry.ts — the two reworded clauses; the registry block, indentation stripped, is line-for-line identical to the entry body (77/77) and check:migration-registry is green.
  • The delta touches neither shared/retired-key.ts nor security/permission.zod.ts (0 hits in the file list), so the baseline's acceptRetiredDefaultResidue() conclusion — plain retiredKey() is right for these four defaulted keys because no re-parsed population exists — is untouched.

The three fixes

1. check:cli-command-ids — DISCHARGED. Read against the checker's own rule, not the assumption: literalsOn() (scripts/check-cli-command-ids.mjs:815-826) skips comment-prefixed lines and matches a quote character immediately followed by a bin name — packages/cli declares bin keys objectstack and os, oclif.bin = os — then 1–2 space-separated lowercase/digit words; the population is tracked src/ and scripts/ sources minus tests and CLI packages, so the entry file and registry.ts are both in it. In the reworded text objectstack is preceded by token and @objectstack/spec by specifier @, neither adjacent to a quote, so the entry contributes no candidate at all: --list prints 552 lines and 0 name the entry or the registry mirror. Gate run at head, 14:22:08Z: pnpm check:cli-command-idsEXIT=0 (self-test 57/57; 487 literals across 140 files resolve; 74 ids derived) — not exit 3. Lit control, 14:23:23Z: the same gate against the baseline blobs of the two files (restored to 6b6fb747f0 / d13b6af358) → EXIT=1 with the identical two "objectstack 10171 and" hits; restored to 4f65e36916 / 9b916ca4d7, porcelain empty. Provenance survived the reword: the text still carries 10171, 3479, 6409 tracked files and .objectui-sha = 53ded82bf7a494f54e344e19099dbf00854b8694 in the same sentence (joined registry.ts: 10171 2, 3479 2 — the second of each is a sibling entry).

2. The count vs its corpus — DISCHARGED. Re-measured at 98bd7986fe, 14:17:54Z, on exactly the corpus the sentence now names (git grep -o Schema 98bd7986fe -- . ':!packages/spec' ':!content/docs/references'): 18920. With only ':!packages/spec': 20875. The five subjects on the named corpus: 0 × 5; dark 0. The sentence's added clause — "both counts from one git grep -o over 98bd798 with those two pathspec exclusions" — is literally what reproduces it. Joined registry.ts: 18920 1, 20875 0. ⚠️ One rider from the baseline is not discharged: the PR body still reads "with packages/spec and the generated reference pages excluded, against a lit control of 20875 Schema occurrences on the same corpus" — a count that disagrees with its stated corpus, in the text a squash landing inherits. It is prose outside the diff, the published migration text is now correct, and the fix is a body edit with no commit — so I judge it a pre-landing note, not a FAIL item: edit the body to 18920 (or restate the corpus) before this lands. Escalated to the epic PM.

3. Tombstone version wording — DISCHARGED, and the settlement is right. Measured with literals joined (perl -0pe collapsing the ' + ' breaks) — the single-line grep at the baseline returned 1 for 17, which is the false reading this epic keeps hitting. tracing.zod.ts: baseline @objectstack/spec 18 ×4 / 17 ×1 (the Span.duration tombstone); head 18 ×0 / 17 ×5. tracing.mdx: in @objectstack/spec 17 ×3, 18 ×0 (the two batch tombstones do not render — the one-level nesting the baseline recorded, unchanged).

My own judgement of 17-vs-18, with the case for 18 considered: strict semver would put a breaking removal in a major, and the registry does record these four under step 18. But the two numbers answer different questions and the sources say which one the prose owes. retiredKey()'s own docblock (retired-key.ts:91-92) says the guidance names "the version that removed it — this string IS the migration doc for anyone who hits it", and its worked example is 17.0.0, a package version. The refusal first ships in @objectstack/spec 17.5.0 (package at 17.4.0; 37 pending minor / 0 major changesets for spec; check-changeset-no-major refuses major in the launch window), so every reader who hits this string is on 17.x, and 18 names a version that does not exist yet and cannot exist without a major cut — PROTOCOL_VERSION = 17.0.0 is pinned in lockstep with the package major by protocol-version.test.ts. MigrationStep.major is documented as "the protocol major this step migrates into (N; migrates N−1 sources to N)": step 18 is the 17→18 hop of migrate meta, the axis RETIRED_KEYS_BY_MAJOR[18] names, not the release that removed the key. Conflating them sends a 17.5.0 reader to a version that does not contain the change. Tree-wide, joined, packages/spec/src non-test excluding registry.ts: 238 tombstones say in @objectstack/spec 17, 14 say 18 (7 files: plugin-lifecycle-advanced.zod.ts 5, export.zod.ts 2, cache.zod.ts 2, disaster-recovery.zod.ts 2, execution.zod.ts 1, connector.zod.ts 1, view.zod.ts 1); the same-file Span.duration precedent says 17. So 17 is right and the settlement holds. Two notes, neither blocking: bare 17 versus 17.5.0 — the house form is split 152 bare / 83 dotted and the exact minor is a release-time fact this PR cannot assert, so the bare major is the safe spelling and matches the file's own precedent; and the 14 18-worded tombstones refuse today in 17.x and are wrong by this same reading — an epic-wide sweep for #15939, not this PR's.

Readings I re-measured

All UTC 2026-09-13, in /tmp/review-18016b at efbf300508 unless a ref is named.

  • 14:17:17Z git worktree add; merge-base HEAD origin/main = 98bd7986fe; origin/main = a0dd872c1b.
  • 14:17:48Z git diff --stat / --name-status c3d5395..efbf300 → 4 files, +20/−16; git log → 2 commits.
  • 14:17:54Z git grep -o Schema 98bd7986fe -- . ':!packages/spec' ':!content/docs/references' | wc -l → 18920; with ':!packages/spec' alone → 20875; subjects 0 ×5; dark pfjqwrv 0.
  • 14:18:08Z joined-literal counts on tracing.zod.ts at both heads (above); tracing.mdx 3/0; joined registry.ts 18920 1, 20875 0.
  • 14:18:13Z packages/spec/package.json 17.4.0; PROTOCOL_VERSION 17.0.0; pending spec changesets 37 minor / 0 major; CHANGELOG.md top ## 17.4.0.
  • 14:20:04Z tree-wide joined in @objectstack/spec 17 238 / 18 14; dotted 17.0 81, 17.4 1, 17.5 2; dark zzrmvk 0. 14:22:30Z bare 17 152 vs dotted 17.x 83.
  • 14:20:25Z byte proofs: zod cmp identical; mdx cmp identical; registry block vs entry body diff empty (77/77); delta hits on retired-key/permission.zod 0.
  • 14:21:18Z sed -n 780,870p scripts/check-cli-command-ids.mjs (population and delimiter rule, quoted above).
  • 14:22:08Z pnpm check:cli-command-ids → EXIT=0; 14:22:19Z --list 552 lines, 0 naming the entry.
  • 14:22:25Z npx vitest run src/system/tracing.test.ts src/shared/retired-key.test.ts src/shared/retired-key-migrate-sentence.test.ts → 3 files, 67 passed (same as baseline).
  • 14:23:23Z lit control on the baseline blobs → EXIT=1, two "objectstack 10171 and" hits; restored; git status --porcelain empty.
  • 14:22:23Z→14:25:44Z pnpm --filter @objectstack/spec build → EXIT=0; 14:27:01Z check:generatedEXIT=0, 15/15 up to date (incl. check:migration-registry, check:authorable-surface, check:docs); porcelain empty.
  • 14:26:05Z CI at head: Lint & Repo Gates job 103736666783 in flight, not readin_progress, head_sha = efbf300…, 31/178 steps done, 0 failed steps so far; from the 14:22Z check-run read: Type Check · source gates, Type Check · debt ledger, Build Docs, Dogfood Regression Gate (1/3) success; Test Core, Build Core, Temporal Conformance (live PG + MySQL) in progress. The local gate run above is the measurement that decides fix 1; the 80-odd gates behind that step remain unmeasured until the job completes, and the PM should read the job's conclusion before arming anything.

Taken on trust (not re-run): everything the baseline passed — the four renames, the Ms suffix, the ratchet-moves-nothing finding, the pin guard replacement, the ablation legs, minor, Clause-②, and the residue conclusion — shown untouched by the file list and the byte proofs rather than re-derived; the objectui counts 10171 / 3479 / 6409 at 53ded82bf7… (reproduced by the baseline at 13:41Z; here I verified only that they survive in the text); the --list derivation's 74 ids.

Escalated: (a) the PR body's 20875 (pre-landing body edit); (b) the 14 18-worded tombstones across 7 files for the epic's settlement sweep; (c) the CI job outcome, unread at post time.

Implemented-by: branch claude/issue-17785-tracing-duration-units
Reviewed-by: session_015c5G6TmpMKgnusmTpD7Ntt

VERDICT: PASS — the diff at efbf3005086c21958957f8da3cc5ecd600fedc6a discharges all three items of the baseline FAIL: check:cli-command-ids is EXIT=0 at head with a lit control that reds on the baseline text; the lit control reads 18920 on exactly the corpus it names; the four tombstones say 17, which is the released package version and the right number; the four-file delta is provably nothing beyond those three edits and their generated projections, and check:generated is 15/15 current. One non-diff rider stays open: edit the PR body's 20875 to 18920 before landing (a body edit, not a push).


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Contract review — delta

This comment supersedes 5653880451 for machine-readability only — the Implemented-by: line there carried a leading word before the branch, which check-clause2-carriers reads as a half-written authorship pair; verdict and findings are unchanged and nothing below is re-derived. The one live reading that moved is the CI job's state, updated at the end.

Head-sha: efbf3005086c21958957f8da3cc5ecd600fedc6a
Baseline: c3d53951a940fdb10a2aa2d3559bc9031bacbc41 (FAIL, comment 5653714585)

Isolated at-tier delta review for the domain:spec seat. Own worktree at the head sha (/tmp/review-18016b, created 14:17:17Z; merge-base with origin/main = 98bd7986fe, origin/main = a0dd872c1b at read time; the clone is shallow and both shas were fetched by hash). Scope is the baseline-to-head delta only; everything the baseline passed is taken as passed unless the delta disturbs it. Dark control tokens: pfjqwrv and zzrmvk (0 everywhere they were run).

Delta reviewed

git diff --stat c3d5395..efbf300 → exactly the four named files, +20 / −16, in two commits: 37f29a4a01 (reword the lit-control counts out of check:cli-command-ids) and efbf300508 (correct the lit control, name the released package version). --name-status lists nothing else; both commits carry the model-free trailer pair. Nothing else moved.

  • packages/spec/src/system/tracing.zod.ts — four hunks, each one token (1817) inside a retiredKey() message. Byte proof: the baseline blob with the four tokens rewritten cmps identical to the head blob, so the delta is provably nothing but the version token — no key, bound, default, describe or behaviour moved. retiredKey( count 5 at both heads.
  • content/docs/references/system/tracing.mdx — two rendered [REMOVED] rows. Same byte proof: baseline with the two tokens rewritten cmps identical to head. A faithful regeneration; check:docs is green in my check:generated run (below).
  • 18.system-tracing-otel-exporter-durations-unit-in-key.ts and its generated mirror in registry.ts — the two reworded clauses; the registry block, indentation stripped, is line-for-line identical to the entry body (77/77) and check:migration-registry is green.
  • The delta touches neither shared/retired-key.ts nor security/permission.zod.ts (0 hits in the file list), so the baseline's acceptRetiredDefaultResidue() conclusion — plain retiredKey() is right for these four defaulted keys because no re-parsed population exists — is untouched.

The three fixes

1. check:cli-command-ids — DISCHARGED. Read against the checker's own rule, not the assumption: literalsOn() (scripts/check-cli-command-ids.mjs:815-826) skips comment-prefixed lines and matches a quote character immediately followed by a bin name — packages/cli declares bin keys objectstack and os, oclif.bin = os — then 1–2 space-separated lowercase/digit words; the population is tracked src/ and scripts/ sources minus tests and CLI packages, so the entry file and registry.ts are both in it. In the reworded text objectstack is preceded by token and @objectstack/spec by specifier @, neither adjacent to a quote, so the entry contributes no candidate at all: --list prints 552 lines and 0 name the entry or the registry mirror. Gate run at head, 14:22:08Z: pnpm check:cli-command-idsEXIT=0 (self-test 57/57; 487 literals across 140 files resolve; 74 ids derived) — not exit 3. Reverse control, 14:23:23Z: the same gate against the baseline blobs of the two files (restored to 6b6fb747f0 / d13b6af358) → EXIT=1 with the identical two "objectstack 10171 and" hits; restored to 4f65e36916 / 9b916ca4d7, tree byte-identical, porcelain empty. Provenance survived the reword: the text still carries 10171, 3479, 6409 tracked files and .objectui-sha = 53ded82bf7a494f54e344e19099dbf00854b8694 in the same sentence (joined registry.ts: 10171 2, 3479 2 — the second of each is a sibling entry).

2. The count vs its corpus — DISCHARGED. Re-measured at 98bd7986fe, 14:17:54Z, on exactly the corpus the sentence now names (git grep -o Schema 98bd7986fe -- . ':!packages/spec' ':!content/docs/references'): 18920. With only ':!packages/spec': 20875. The five subjects on the named corpus: 0 × 5; dark 0. The sentence's added clause — "both counts from one git grep -o over 98bd798 with those two pathspec exclusions" — is literally what reproduces it. Joined registry.ts: 18920 1, 20875 0. ⚠️ One rider from the baseline was not discharged at my 14:17Z read: the PR body still read "with packages/spec and the generated reference pages excluded, against a lit control of 20875 Schema occurrences on the same corpus" — a count that disagrees with its stated corpus, in the text a squash landing inherits. It is prose outside the diff, the published migration text is now correct, and the fix is a body edit with no commit — so I judge it a pre-landing note, not a FAIL item: edit the body to 18920 (or restate the corpus) before this lands. A body edit was reported in flight at post time and is not re-read here; it moves no head sha.

3. Tombstone version wording — DISCHARGED, and the settlement is right. Measured with literals joined (perl -0pe collapsing the ' + ' breaks) — the single-line grep at the baseline returned 1 for 17, which is the false reading this epic keeps hitting. tracing.zod.ts: baseline @objectstack/spec 18 ×4 / 17 ×1 (the Span.duration tombstone); head 18 ×0 / 17 ×5. tracing.mdx: in @objectstack/spec 17 ×3, 18 ×0 (the two batch tombstones do not render — the one-level nesting the baseline recorded, unchanged).

My own judgement of 17-vs-18, with the case for 18 considered: strict semver would put a breaking removal in a major, and the registry does record these four under step 18. But the two numbers answer different questions and the sources say which one the prose owes. retiredKey()'s own docblock (retired-key.ts:91-92) says the guidance names "the version that removed it — this string IS the migration doc for anyone who hits it", and its worked example is 17.0.0, a package version. The refusal first ships in @objectstack/spec 17.5.0 (package at 17.4.0; 37 pending minor / 0 major changesets for spec; check-changeset-no-major refuses major in the launch window), so every reader who hits this string is on 17.x, and 18 names a version that does not exist yet and cannot exist without a major cut — PROTOCOL_VERSION = 17.0.0 is pinned in lockstep with the package major by protocol-version.test.ts. MigrationStep.major is documented as "the protocol major this step migrates into (N; migrates N−1 sources to N)": step 18 is the 17→18 hop of migrate meta, the axis RETIRED_KEYS_BY_MAJOR[18] names, not the release that removed the key. Conflating them sends a 17.5.0 reader to a version that does not contain the change. Tree-wide, joined, packages/spec/src non-test excluding registry.ts: 238 tombstones say in @objectstack/spec 17, 14 say 18 (7 files: plugin-lifecycle-advanced.zod.ts 5, export.zod.ts 2, cache.zod.ts 2, disaster-recovery.zod.ts 2, execution.zod.ts 1, connector.zod.ts 1, view.zod.ts 1); the same-file Span.duration precedent says 17. So 17 is right and the settlement holds. Two notes, neither blocking: bare 17 versus 17.5.0 — the house form is split 152 bare / 83 dotted and the exact minor is a release-time fact this PR cannot assert, so the bare major is the safe spelling and matches the file's own precedent; and the 14 18-worded tombstones refuse today in 17.x and are wrong by this same reading — an epic-wide sweep for #15939, not this PR's.

Readings I re-measured

All UTC 2026-09-13, in /tmp/review-18016b at efbf300508 unless a ref is named.

  • 14:17:17Z git worktree add; merge-base HEAD origin/main = 98bd7986fe; origin/main = a0dd872c1b.
  • 14:17:48Z git diff --stat / --name-status c3d5395..efbf300 → 4 files, +20/−16; git log → 2 commits.
  • 14:17:54Z git grep -o Schema 98bd7986fe -- . ':!packages/spec' ':!content/docs/references' | wc -l → 18920; with ':!packages/spec' alone → 20875; subjects 0 ×5; dark pfjqwrv 0.
  • 14:18:08Z joined-literal counts on tracing.zod.ts at both heads (above); tracing.mdx 3/0; joined registry.ts 18920 1, 20875 0.
  • 14:18:13Z packages/spec/package.json 17.4.0; PROTOCOL_VERSION 17.0.0; pending spec changesets 37 minor / 0 major; CHANGELOG.md top ## 17.4.0.
  • 14:20:04Z tree-wide joined in @objectstack/spec 17 238 / 18 14; dotted 17.0 81, 17.4 1, 17.5 2; dark zzrmvk 0. 14:22:30Z bare 17 152 vs dotted 17.x 83.
  • 14:20:25Z byte proofs: zod cmp identical; mdx cmp identical; registry block vs entry body diff empty (77/77); delta hits on retired-key/permission.zod 0.
  • 14:21:18Z sed -n 780,870p scripts/check-cli-command-ids.mjs (population and delimiter rule, quoted above).
  • 14:22:08Z pnpm check:cli-command-ids → EXIT=0; 14:22:19Z --list 552 lines, 0 naming the entry.
  • 14:22:25Z npx vitest run src/system/tracing.test.ts src/shared/retired-key.test.ts src/shared/retired-key-migrate-sentence.test.ts → 3 files, 67 passed (same as baseline).
  • 14:23:23Z reverse control on the baseline blobs → EXIT=1, two "objectstack 10171 and" hits; restored; git status --porcelain empty.
  • 14:22:23Z→14:25:44Z pnpm --filter @objectstack/spec build → EXIT=0; 14:27:01Z check:generatedEXIT=0, 15/15 up to date (incl. check:migration-registry, check:authorable-surface, check:docs); porcelain empty.
  • CI at head, re-read 14:35:25Z: Lint & Repo Gates job 103736666783 still in flight, not readin_progress, head_sha = efbf300…, 140/178 steps completed, 0 failed steps so far (the check:cli-command-ids step is among the completed ones with no failure). The other five required contexts are success on this head: TypeScript Type Check (14:30:49Z), Test Core (14:30:55Z), Dogfood Regression Gate (14:25:32Z), Build Core (14:23:26Z), Temporal Conformance (live PG + MySQL) (14:23:16Z); every other check on the head is success or skipped, none failure. The local gate run above is the measurement that decides fix 1; the gates behind that step in the job remain unmeasured until it completes, and the PM should read the job's conclusion before arming anything.

Taken on trust (not re-run): everything the baseline passed — the four renames, the Ms suffix, the ratchet-moves-nothing finding, the pin guard replacement, the ablation legs, minor, Clause-②, and the residue conclusion — shown untouched by the file list and the byte proofs rather than re-derived; the objectui counts 10171 / 3479 / 6409 at 53ded82bf7… (reproduced by the baseline at 13:41Z; here I verified only that they survive in the text); the --list derivation's 74 ids.

Escalated: (a) the PR body's 20875 (pre-landing body edit, reported in flight); (b) the 14 18-worded tombstones across 7 files for the epic's settlement sweep; (c) the Lint & Repo Gates conclusion, unread at post time.

Implemented-by: claude/issue-17785-tracing-duration-units
Reviewed-by: session_015c5G6TmpMKgnusmTpD7Ntt

VERDICT: PASS — the diff at efbf3005086c21958957f8da3cc5ecd600fedc6a discharges all three items of the baseline FAIL: check:cli-command-ids is EXIT=0 at head with a reverse control that reds on the baseline text; the lit control reads 18920 on exactly the corpus it names; the four tombstones say 17, which is the released package version and the right number; the four-file delta is provably nothing beyond those three edits and their generated projections, and check:generated is 15/15 current. One non-diff rider stays open: edit the PR body's 20875 to 18920 before landing (a body edit, not a push).


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Contract review — delta

Head-sha: efbf3005086c21958957f8da3cc5ecd600fedc6a
Baseline: c3d53951a940fdb10a2aa2d3559bc9031bacbc41 (FAIL, comment 5653714585)

This comment supersedes 5653922476 for machine-readability only — the head sha there sat inside a code span together with its label, which contractReviewHeadMatch (pure-hex spans only) cannot read; verdict and findings are unchanged and nothing below is re-derived. Two live readings moved and are folded in at the end: the Lint & Repo Gates job has concluded, and the PR-body rider is discharged.

Isolated at-tier delta review for the domain:spec seat. Own worktree at the head sha (/tmp/review-18016b, created 14:17:17Z; merge-base with origin/main = 98bd7986fe, origin/main = a0dd872c1b at read time; the clone is shallow and both shas were fetched by hash). Scope is the baseline-to-head delta only; everything the baseline passed is taken as passed unless the delta disturbs it. Dark control tokens: pfjqwrv and zzrmvk (0 everywhere they were run).

Delta reviewed

git diff --stat c3d5395..efbf300 → exactly the four named files, +20 / −16, in two commits: 37f29a4a01 (reword the lit-control counts out of check:cli-command-ids) and efbf300508 (correct the lit control, name the released package version). --name-status lists nothing else; both commits carry the model-free trailer pair. Nothing else moved.

  • packages/spec/src/system/tracing.zod.ts — four hunks, each one token (1817) inside a retiredKey() message. Byte proof: the baseline blob with the four tokens rewritten cmps identical to the head blob, so the delta is provably nothing but the version token — no key, bound, default, describe or behaviour moved. retiredKey( count 5 at both heads.
  • content/docs/references/system/tracing.mdx — two rendered [REMOVED] rows. Same byte proof: baseline with the two tokens rewritten cmps identical to head. A faithful regeneration; check:docs is green in my check:generated run (below).
  • 18.system-tracing-otel-exporter-durations-unit-in-key.ts and its generated mirror in registry.ts — the two reworded clauses; the registry block, indentation stripped, is line-for-line identical to the entry body (77/77) and check:migration-registry is green.
  • The delta touches neither shared/retired-key.ts nor security/permission.zod.ts (0 hits in the file list), so the baseline's acceptRetiredDefaultResidue() conclusion — plain retiredKey() is right for these four defaulted keys because no re-parsed population exists — is untouched.

The three fixes

1. check:cli-command-ids — DISCHARGED. Read against the checker's own rule, not the assumption: literalsOn() (scripts/check-cli-command-ids.mjs:815-826) skips comment-prefixed lines and matches a quote character immediately followed by a bin name — packages/cli declares bin keys objectstack and os, oclif.bin = os — then 1–2 space-separated lowercase/digit words; the population is tracked src/ and scripts/ sources minus tests and CLI packages, so the entry file and registry.ts are both in it. In the reworded text objectstack is preceded by token and @objectstack/spec by specifier @, neither adjacent to a quote, so the entry contributes no candidate at all: --list prints 552 lines and 0 name the entry or the registry mirror. Gate run at head, 14:22:08Z: pnpm check:cli-command-idsEXIT=0 (self-test 57/57; 487 literals across 140 files resolve; 74 ids derived) — not exit 3. Reverse control, 14:23:23Z: the same gate against the baseline blobs of the two files (restored to 6b6fb747f0 / d13b6af358) → EXIT=1 with the identical two "objectstack 10171 and" hits; restored to 4f65e36916 / 9b916ca4d7, tree byte-identical, porcelain empty. Provenance survived the reword: the text still carries 10171, 3479, 6409 tracked files and .objectui-sha = 53ded82bf7a494f54e344e19099dbf00854b8694 in the same sentence (joined registry.ts: 10171 2, 3479 2 — the second of each is a sibling entry).

2. The count vs its corpus — DISCHARGED. Re-measured at 98bd7986fe, 14:17:54Z, on exactly the corpus the sentence now names (git grep -o Schema 98bd7986fe -- . ':!packages/spec' ':!content/docs/references'): 18920. With only ':!packages/spec': 20875. The five subjects on the named corpus: 0 × 5; dark 0. The sentence's added clause — "both counts from one git grep -o over 98bd798 with those two pathspec exclusions" — is literally what reproduces it. Joined registry.ts: 18920 1, 20875 0. The baseline's rider — mirror the correction in the PR body — was open at my 14:17Z read (the body still said 20875 on the narrower corpus; judged a pre-landing note rather than a FAIL item, since it is prose outside the diff and the published migration text was already correct). Discharged: at 14:39:38Z the body reads "excluding packages/spec and content/docs/references, against a lit control of 18920 Schema occurrences on exactly that corpus — both counts from one git grep -o over that ref carrying both pathspec exclusions"; 20875 is gone; the head sha is unmoved (a body edit, updated_at 14:36:39Z).

3. Tombstone version wording — DISCHARGED, and the settlement is right. Measured with literals joined (perl -0pe collapsing the ' + ' breaks) — the single-line grep at the baseline returned 1 for 17, which is the false reading this epic keeps hitting. tracing.zod.ts: baseline @objectstack/spec 18 ×4 / 17 ×1 (the Span.duration tombstone); head 18 ×0 / 17 ×5. tracing.mdx: in @objectstack/spec 17 ×3, 18 ×0 (the two batch tombstones do not render — the one-level nesting the baseline recorded, unchanged).

My own judgement of 17-vs-18, with the case for 18 considered: strict semver would put a breaking removal in a major, and the registry does record these four under step 18. But the two numbers answer different questions and the sources say which one the prose owes. retiredKey()'s own docblock (retired-key.ts:91-92) says the guidance names "the version that removed it — this string IS the migration doc for anyone who hits it", and its worked example is 17.0.0, a package version. The refusal first ships in @objectstack/spec 17.5.0 (package at 17.4.0; 37 pending minor / 0 major changesets for spec; check-changeset-no-major refuses major in the launch window), so every reader who hits this string is on 17.x, and 18 names a version that does not exist yet and cannot exist without a major cut — PROTOCOL_VERSION = 17.0.0 is pinned in lockstep with the package major by protocol-version.test.ts. MigrationStep.major is documented as "the protocol major this step migrates into (N; migrates N−1 sources to N)": step 18 is the 17→18 hop of migrate meta, the axis RETIRED_KEYS_BY_MAJOR[18] names, not the release that removed the key. Conflating them sends a 17.5.0 reader to a version that does not contain the change. Tree-wide, joined, packages/spec/src non-test excluding registry.ts: 238 tombstones say in @objectstack/spec 17, 14 say 18 (7 files: plugin-lifecycle-advanced.zod.ts 5, export.zod.ts 2, cache.zod.ts 2, disaster-recovery.zod.ts 2, execution.zod.ts 1, connector.zod.ts 1, view.zod.ts 1); the same-file Span.duration precedent says 17. So 17 is right and the settlement holds. Two notes, neither blocking: bare 17 versus 17.5.0 — the house form is split 152 bare / 83 dotted and the exact minor is a release-time fact this PR cannot assert, so the bare major is the safe spelling and matches the file's own precedent; and the 14 18-worded tombstones refuse today in 17.x and are wrong by this same reading — an epic-wide sweep for #15939, not this PR's.

Readings I re-measured

All UTC 2026-09-13, in /tmp/review-18016b at efbf300508 unless a ref is named.

  • 14:17:17Z git worktree add; merge-base HEAD origin/main = 98bd7986fe; origin/main = a0dd872c1b.
  • 14:17:48Z git diff --stat / --name-status c3d5395..efbf300 → 4 files, +20/−16; git log → 2 commits.
  • 14:17:54Z git grep -o Schema 98bd7986fe -- . ':!packages/spec' ':!content/docs/references' | wc -l → 18920; with ':!packages/spec' alone → 20875; subjects 0 ×5; dark pfjqwrv 0.
  • 14:18:08Z joined-literal counts on tracing.zod.ts at both heads (above); tracing.mdx 3/0; joined registry.ts 18920 1, 20875 0.
  • 14:18:13Z packages/spec/package.json 17.4.0; PROTOCOL_VERSION 17.0.0; pending spec changesets 37 minor / 0 major; CHANGELOG.md top ## 17.4.0.
  • 14:20:04Z tree-wide joined in @objectstack/spec 17 238 / 18 14; dotted 17.0 81, 17.4 1, 17.5 2; dark zzrmvk 0. 14:22:30Z bare 17 152 vs dotted 17.x 83.
  • 14:20:25Z byte proofs: zod cmp identical; mdx cmp identical; registry block vs entry body diff empty (77/77); delta hits on retired-key/permission.zod 0.
  • 14:21:18Z sed -n 780,870p scripts/check-cli-command-ids.mjs (population and delimiter rule, quoted above).
  • 14:22:08Z pnpm check:cli-command-ids → EXIT=0; 14:22:19Z --list 552 lines, 0 naming the entry.
  • 14:22:25Z npx vitest run src/system/tracing.test.ts src/shared/retired-key.test.ts src/shared/retired-key-migrate-sentence.test.ts → 3 files, 67 passed (same as baseline).
  • 14:23:23Z reverse control on the baseline blobs → EXIT=1, two "objectstack 10171 and" hits; restored; git status --porcelain empty.
  • 14:22:23Z→14:25:44Z pnpm --filter @objectstack/spec build → EXIT=0; 14:27:01Z check:generatedEXIT=0, 15/15 up to date (incl. check:migration-registry, check:authorable-surface, check:docs); porcelain empty.
  • 14:39:38Z PR body re-read: 20875 absent, 18920 present on the named corpus; head sha unmoved.
  • CI at head, re-read 14:39:38Z: Lint & Repo Gates job 103736666783 completed, conclusion success at 14:37:17Z, 179/179 steps, 0 failed steps, head_sha = efbf300… — the gates behind the check:cli-command-ids step, unmeasured at the baseline, have now run green. All six required contexts are success on this head: Lint & Repo Gates (14:37:17Z), TypeScript Type Check (14:30:49Z), Test Core (14:30:55Z), Dogfood Regression Gate (14:25:32Z), Build Core (14:23:26Z), Temporal Conformance (live PG + MySQL) (14:23:16Z); no check on the head is failure.

Taken on trust (not re-run): everything the baseline passed — the four renames, the Ms suffix, the ratchet-moves-nothing finding, the pin guard replacement, the ablation legs, minor, Clause-②, and the residue conclusion — shown untouched by the file list and the byte proofs rather than re-derived; the objectui counts 10171 / 3479 / 6409 at 53ded82bf7… (reproduced by the baseline at 13:41Z; here I verified only that they survive in the text); the --list derivation's 74 ids.

Escalated: the 14 18-worded tombstones across 7 files, for the epic's settlement sweep. Retired: the PR-body rider (discharged 14:39Z) and the unread CI conclusion (now success).

Implemented-by: claude/issue-17785-tracing-duration-units
Reviewed-by: session_015c5G6TmpMKgnusmTpD7Ntt

VERDICT: PASS — the diff at efbf3005086c21958957f8da3cc5ecd600fedc6a discharges all three items of the baseline FAIL: check:cli-command-ids is EXIT=0 at head with a reverse control that reds on the baseline text, and the required Lint & Repo Gates context is now success on this head; the lit control reads 18920 on exactly the corpus it names, and the PR body now says the same; the four tombstones say 17, which is the released package version and the right number; the four-file delta is provably nothing beyond those three edits and their generated projections, and check:generated is 15/15 current.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Carriers cleared — provenance

needs:contract-review stripped from both carriers at 2026-09-13T14:43Z: this PR and card #17785, each read back after the write.

review of record #18016 (comment) (delta)
baseline c3d53951a9 — FAIL, record 5653714585; all three items discharged
head judged efbf3005086c21958957f8da3cc5ecd600fedc6a — unchanged at strip time
verdict PASS, adopted verbatim
served tier verified from the review transcript's harness stamps: claude-fable-5-1 × 116, no other model value, dark control 0
--pair 18016, post-strip exit 0
checks 35 runs, 31 success + 4 skipped, 0 failure; Lint & Repo Gates success, 179/179 steps

That last row is the one this PR was actually waiting on: check:cli-command-ids lives inside Lint & Repo Gates, and this is the first head of this PR on which that job reached a conclusion at all — the two before it were one failure and one cancelled, so the ~80 gates behind it had never been measured.

The three fixes, each verified independently by the review

check:cli-command-ids EXIT=0 at head, with a reverse control: the same gate against the baseline blobs exits 1 with the identical two "objectstack 10171 and" hits; tree restored byte-identical, porcelain empty
the 20875 count re-measured 18920 on the corpus the sentence names; 20875 reproduces only with content/docs/references included. Body rider also discharged
tombstone version 1817 proven at the byte level: the baseline blob with its four version tokens rewritten cmps identical to the head blob, for both tracing.zod.ts and the regenerated tracing.mdx — so the source delta is provably nothing but the version token and the mdx is purely its projection

The 17 settlement was also independently judged correct, on better evidence than the seat's: retiredKey()'s own docblock names "the version that removed it" with a package-version example; the refusal ships in 17.5.0; PROTOCOL_VERSION is lockstepped to the package major; and MigrationStep.major is documented as the hop a step migrates into — a different axis.

⚠️ One superseded record here is my error, and one re-post was unnecessary

5653880451 carried Implemented-by: branch claude/…, which is unreadable (「值紧跟冒号,前置词即不可读」) — that re-post was owed.

The second was not. I then asked for another re-post on the theory that `Head-sha: <sha>` hides the sha from H51_SHA_SPAN. That is true in general, and it was the real defect on sibling PR #18007 — but this record already carried `efbf300508` and the full sha as bare-hex spans elsewhere in its body, and contractReviewHeadMatch scans the whole body and accepts any pure-hex span prefixing the head. Measured: 14 spans, 3 of them matching. ⇒ 5653922476 was already machine-readable and I propagated a sibling's diagnosis here without checking it applied. The reviewer said so at the time and was right.

Root cause of both filed as #18042 — the contract-review record is the only machine-read artefact in this skill with no literal template to copy.

⛔ 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

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 13, 2026 14:44
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 13, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Sep 13, 2026
@zhuangjianguo
zhuangjianguo marked this pull request as draft September 13, 2026 15:13
…acing-duration-units

# Conflicts:
#	packages/spec/src/migrations/registry.ts
`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>

Copy link
Copy Markdown
Collaborator

Contract review — delta

Head-sha: 1de92aba380312d8f83d0b16b384249fbb6776ca
Baseline: efbf3005086c21958957f8da3cc5ecd600fedc6a (PASS, comment 5653949483; the full review is 5653714585)

Isolated at-tier delta review for the domain:spec seat. Own worktree at the head sha (/tmp/review-18016c, created 15:26:18Z; the clone is shallow and every sha below was fetched by hash). origin/main had moved to 60b99552a2 by read time, so every "main" reading is pinned to the merge's own second parent ff1e7716c5 — the sha the 15:13Z baseline was taken on — unless a different ref is named. Scope is the baseline-to-head delta only; everything the two earlier reviews passed stands unless this delta disturbs it. Dark control token, invented at read time: Zorblax1789313264 (0 everywhere it was run).

Delta reviewed

git log --first-parent efbf300508..1de92aba38 → exactly two commits: ee396323ee (merge; parents efbf300508 and ff1e7716c5) and 1de92aba38 (regeneration; touches packages/spec/src/migrations/registry.ts only, +128/−0). git diff --name-only efbf300508..1de92aba38 is 113 files, and that set is identical to the 113 files of 98bd7986fe..ff1e7716c5 (what main brought) plus registry.ts. For 112 of the 113 the blob at head is main's blob; the only file whose head blob belongs to neither side is registry.ts, the regenerated one. Exactly one path changed on both sides (98bd7986fe..efbf30050898bd7986fe..ff1e7716c5): registry.ts. The PR's own delta against its base is the same 10 files before and after the merge, and for 9 of them the patch text is byte-identical (per-file git diff minus index lines, md5); the tenth is registry.ts, whose context moved. So the three earlier fixes survive by construction — measured anyway, below. The regen commit carries the model-free trailer pair; the merge commit carries git's default merge message and no trailers, the same shape os-regen-merge.sh step 3 itself produces, so I record it and do not count it.

Primary — sibling rows survived?

Yes — by four independent instruments, and I did not accept the round's decomposition.

1. The occurrence table, grep -o | wc -l over packages/spec/src/migrations at head, 15:27:44Z — reproduces the 15:13Z baseline on ff1e7716c5 cell for cell: summary.maxAge 2 · errorBudget.burnRateWindows.window 2 · MetricExportConfig:interval 6 · MetricsConfig:collectionInterval 6 · retention.period 2 · system-metrics-jsdoc-durations-unit-in-key 14 · lit logging-durations-unit-in-key 10 · lit kernel-runtime-config-timeout-unit-in-key 4 · dark 0. Each 2 is one export const entry = '…'; in the entry file plus its generated mirror in registry.ts (per-file split read, not inferred). The 14 is 7 in registry.ts + 7 across entries (the semantic entry, the sibling system-metrics-window-… entry, and the five retired-key entries that name it); the 10 is 5 + 5; the 4 is 2 + 2 — the registry side of every id equals its entries side, which is what a generated mirror should read.

The 6s do not decompose as "quoted-exact 2 + prefix-of-suffixed 2" — that sums to 4. Read by context with file:line: system/MetricExportConfig:interval is 2 quoted-exact (…interval'; at entry :19, …interval', at registry.ts:13816), 2 as the prefix of system/MetricExportConfig:intervalSeconds (entry :15, registry :13812), and 2 more inside the comment line reading "system/MetricExportConfig:interval [RETIRED] beside a new …" (entry :14, registry :13811) — followed by a space, so neither a quote nor a suffix. Same 2 + 2 + 2 for MetricsConfig:collectionInterval (:18/:13832, :14/:13828, :13/:13827). The round's conclusion — one quoted-exact registration in the entry and one in the registry — holds; the note under-counted the comment mentions and should not be repeated as written.

2. The whole-set difference over entries/ (a true set: 579 .ts files under semantic/, retired-keys/, retired-defs/, plus one README.md), git ls-tree -r --name-only at four trees, 15:27:58Z: merge-base 98bd7986fe 561 (210/173/178) → pre-merge head efbf300508 566 (+5/−0, 211/177/178); 98bd7986feff1e7716c5 574 (+13/−0, 214/182/178); head 1de92aba38 579 (215/186/178). Head vs pre-merge: 0 lost / 13 gained, and the 13 are exactly main's — #17783's five retired-key entries and its semantic entry, #17999's four logging entries and its semantic entry, and two further semantic entries (dataset-measure-selecting-aggregate-field-type-refused, screen-field-lookup-reference-required). Head vs main: 0 lost / 5 gained — exactly this PR's five. And sort -u of (pre-merge ∪ main) diffs empty against head: head is precisely the union. 561 + 5 + 13 = 579, matched.

3. The generator's own check at all four trees, not one. packages/spec/scripts/build-migration-registry.ts --self-test --check is the exact script and flag pair check:migration-registry names; run under node --experimental-strip-types because my worktree has no node_modules (its imports are node:fs, node:path, node:url only). Head, 15:32:26Z: EXIT=0, 215 semantic, 186 retired-key, 178 retired-def. The other three trees extracted with git archive, 15:33:52Z: ff1e7716c5 EXIT=0 214/182/178; efbf300508 EXIT=0 211/177/178; 98bd7986fe EXIT=0 210/173/178. The generator's printed line and the file-set count agree at every endpoint, so the round's "predicted before, matched after" is confirmed by both methods at both ends rather than at one.

4. A line-set proof on registry.ts itself (sorted added/removed lines of git diff, 15:32:18Z): main's contribution 98bd7986fe..ff1e7716c5 is +452/−9, and the same 452 added and 9 removed lines reappear identically in efbf300508..1de92aba38; the PR's +128/−0 of 98bd7986fe..efbf300508 reappears identically in ff1e7716c5..1de92aba38, and the regen commit's own +128 equals that set exactly. The merge commit's 137 deletions against the pre-merge head are the PR's 128 rows (re-added by the regen) plus main's 9 (an edit inside an existing region). Nothing from either side is absent from the head file.

The conflict resolution

Facts. The merge commit's registry.ts blob is c7a7d1be…, byte-identical to ff1e7716c5's — the "placeholder side" was main's whole file, 0 conflict markers — and the regen commit then added 128 lines and removed none. Split at the os-generated marker comments (13 markers in all four trees): the hand-written prose outside the markers is 1913 lines at both 98bd7986fe and efbf300508 (the PR touched no prose; diff empty) and 1928 lines at both ff1e7716c5 and head (the merge kept main's prose whole; diff empty; main's own 20-line rationale edit for list-view-sort-string-clause-to-array is carried). The generated regions at head differ from main's by exactly 128 lines.

(a) Is the resolution sound, and is a green check:migration-registry sufficient? Sound, yes — for this PR, and the reason deserves stating precisely. --check renders the regions from entries/ and compares them to the file; its own failure text is "its generated regions do not match entries/". So a green gate proves only that registry.ts equals f(entries/) — internal consistency with whatever entries/ now contains — and says nothing about the prose outside the markers. It becomes proof of no lost row only together with two facts it cannot see: (i) entries/ at head is the union of both sides (instrument 2 — a file-per-entry set that a three-way merge cannot silently thin when neither side deleted anything), and (ii) the discarded side had no prose edit in registry.ts (the 1913 = 1913 reading). Both hold here, so "take main's side, commit the merge first, regenerate, commit the discharge" is the AGENTS.md §11 sequence and its outcome is exact. The hazard the round's reasoning left implicit: had this PR hand-edited a rationale outside the markers, taking main's side would have dropped it and every gate, check:migration-registry included, would have stayed green. A runner resolving this file by "take a side, then regenerate" owes the prose reading too; I took it.

(b) The script's "NON-generated, resolve by hand" line. scripts/pm/os-regen-merge.sh builds its regen list from .gitattributes alone (an awk over merge=os-regen lines; 0 references to regen-artifacts.mjs) and partitions conflicted paths into that list and "everything else". registry.ts appears in no non-comment line of .gitattributes (git check-attr mergeunspecified, against os-regen for authorable-surface/system.json and spec-changes.json) because NOT_DRIVER_MANAGED in regen-artifacts.mjs deliberately keeps it out: it is MIXED — three generated append tables between markers plus hand-written rationale prose. So the script labels it "NON-generated" and prescribes "resolve by hand (semantic merge, both intents stack) … Do not resolve generated files textually". For the regions that advice is wrong: regen-artifacts.mjs says in its own words that "a conflict INSIDE a region is resolved by gen:migration-registry and nothing else — check:migration-registry fails if it was resolved any other way", and the script's self-test case 6 exists precisely so a MIXED conflict is never called "in NON-generated files" — but that case covers only driver-routed mixed: rows (content/docs/permissions/system-context.mdx), not the NOT_DRIVER_MANAGED rows that carry a gen: (migrations/registry.ts, skills/README.md, content/docs/ai/skills-reference.mdx, the scaffold-emission-policy file). Yes — a genuine misclassification and a script defect worth filing, at low severity: the net exists (a hand semantic merge inside a region reds check:migration-registry, and this round did the right thing despite the text), so the cost is a misleading instruction and a wasted lap, not a silent loss. Fix shape: a third bucket — "MIXED but not driver-managed: hand-resolve the prose outside the markers, take either side inside them, then run that row's gen script" — sourced from the NOT_DRIVER_MANAGED rows with a gen: field, with a self-test twin of case 6. Not this PR's to fix. The epic PM filed it as #18047 while this review was in flight (the script partitions by merge=os-regen routing rather than by generatedness — the unhandled third class left by #14671's fix in #14733); my reading of the script agrees with that card: the partition has exactly two buckets, keyed on membership in the .gitattributes merge=os-regen list, and registry.ts can only ever land in the non-regen one.

The three earlier fixes survived the merge

Measured at head with concatenated literals joined first (perl -0777 collapsing quote-plus-quote breaks, then grep -o | wc -l), 15:28:07Z:

  • Tombstone version: tracing.zod.ts spec 17 5, spec 18 0; tracing.mdx 3 / 0 (the two batch tombstones do not render — the one-level nesting the baseline recorded, unchanged). Differently drawn control for the zero: the five raw sites print at :224 :552 :603 :611 :780 under grep -n -B1 -A1, each spanning a ' + ' break before @objectstack/spec 17.
  • Count correction: 18920 1 in the semantic entry (:61) and 1 in its registry mirror; 20875 0 in every delta file.
  • check:cli-command-ids reword: 0 string literals in the entry whose first characters are objectstack followed by a space and a word; lit token objectstack 1 and specifier @objectstack 1 in both the entry and its registry mirror; the provenance counts 10171, 3479, 6409 and 53ded82bf7 still present. Quote-adjacent objectstack … lines in registry.ts: 33 at head = 33 at ff1e7716c5 (all backtick-quoted real commands: validate 21, migrate meta 12, build 1) vs 32 at the pre-merge head — the +1 is main's. I could not run the gate locally (EXIT=3 without node_modules: PREREQUISITE NOT MET, neither pass nor finding); the Lint & Repo Gates context on this head decides it (status below).

The ratchet moves nothing

packages/spec/authorable-surface.base.json blob 4c329b0d… at ff1e7716c5, at head, and at the current origin/main 60b99552a2 — identical; baseRev 53ef05744f… on both sides. git diff --name-only over authorable-surface/ + authorable-defaults/: head vs ff1e7716c5 0 files, head vs 60b99552a2 0 files — the all-four-nested reading survives the merge. Lit controls drawn differently: the same command over packages/spec/ lists 8 files; authorable-surface/system.json at head holds 1310 keys, 0 dotted, system/Span:duration 2 rows, system/OpenTelemetryCompatibility:exporter exactly 1 row; the four old and four new spellings 0 across both trees. The 3 authorable files that differ between the pre-merge head and head are main's (system.json surface and defaults, automation.json) and equal main's blobs. My first control here mis-drew itself (a filename grep that read 0) — replaced, not reported.

Readings I re-measured

All UTC 2026-09-13, in /tmp/review-18016c at 1de92aba38 unless a ref is named.

  • 15:26:03Z fetch by hash; 15:26:18Z worktree; origin/main = 60b99552a2 at read time; merge-base 98bd7986fe; merge parents read from ee396323ee.
  • 15:27:44Z occurrence table (all nine cells) and decomposition by trailing context; 15:29:49Z file:line contexts and per-file splits.
  • 15:27:58Z four-tree ls-tree sets, comm both ways, union identity.
  • 15:28:07Z joined-literal version tokens, 18920 / 20875, raw contexts.
  • 15:28:16Z and 15:30:26Z ratchet blobs, baseRev, 0-file diffs, redrawn lit controls.
  • 15:30:14Z first line-set pass — its added-line filter was mis-spelled and read 0, discarded; 15:32:18Z the corrected pass quoted above.
  • 15:32:24Z prose/regions split (1913 / 1913 / 1928 / 1928; 13 markers in each tree; regions Δ 128).
  • 15:32:26Z generator --self-test --check at head EXIT=0; 15:33:52Z at ff1e7716c5, efbf300508, 98bd7986fe, EXIT=0 each.
  • 15:32:37Z 113-file set identity; blob membership (112 main, 1 neither = registry.ts).
  • 15:32:42Z reword survival; check-cli-command-ids EXIT=3 locally; contractReviewHeadMatch imported from scripts/pm/check-half-states.mjs and run on this body before posting.
  • 15:33:58Z check-attr; .gitattributes non-comment hits for migrations/registry 0; the script's regen-artifacts.mjs references 0.
  • CI on this head, read 15:41Z (my own poll; the adopting seat read the same picture at 15:39Z as 35 checks, 30 success, 4 skipped, 0 failure, 1 running): 39 check runs, 0 failure; five of the six required contexts successTypeScript Type Check 15:38:51Z, Test Core 15:37:28Z, Dogfood Regression Gate 15:33:03Z, Build Core 15:31:04Z, Temporal Conformance (live PG + MySQL) 15:31:36Z; the sixth, Lint & Repo Gates, in flight at 15:41Z, not read — a declared limit, not a pass. It is the context that runs check:cli-command-ids. On the previous head efbf300508 it concluded success at 14:37:17Z (179/179 steps), and this delta changes that gate's population only by main's own registry.ts rows — a green precedent on this diff's own text, not a claim about this head.

Taken on trust (not re-run): everything the two earlier reviews passed — the four renames, the Ms suffix, the pin guard, the ablation legs, the residue conclusion, semver, Clause-② — shown untouched by the nine byte-identical patches rather than re-derived; the round's local pre-commit deferral state, which is not observable from the remote.

Escalated: the os-regen-merge.sh misclassification in (b) — already filed as #18047, and my reading agrees with it; the round's "2 + 2" decomposition sentence, which should read 2 + 2 + 2.

Implemented-by: claude/issue-17785-tracing-duration-units
Reviewed-by: session_015c5G6TmpMKgnusmTpD7Ntt

VERDICT: PASS — the delta at 1de92aba380312d8f83d0b16b384249fbb6776ca is a merge that kept main's registry.ts whole plus a regeneration that adds back exactly the PR's 128 lines; #17783's five registrations and its semantic entry are present once in entries/ and once in the registry at head; entries/ at head is precisely the union of both sides (0 lost either way; 561 + 5 + 13 = 579 by file set and by the generator at all four trees); the prose outside the markers is main's byte-for-byte and the PR had none to lose; the three earlier fixes read 5 / 0, 18920 present / 20875 absent, and reword intact with literals joined; and the ratchet is byte-identical to main on both baselines.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator

Carriers cleared — provenance

needs:contract-review stripped from both carriers at 2026-09-13T15:51Z: this PR and card #17785, each read back after the write.

review of record #18016 (comment)
head judged 1de92aba380312d8f83d0b16b384249fbb6776ca — unchanged at strip time
verdict PASS, adopted verbatim
served tier verified from the transcript's harness stamps: claude-fable-5-1 × 126, no other model value, dark control 0
--pair 18016, post-strip exit 0
checks 35 runs, 31 success + 4 skipped, 0 failure; Lint & Repo Gates success

Full acceptance, including the four instruments that proved the sibling's rows survived and one number I relayed wrongly, is on the card: #17785 comment 5654318032.

⚠️ The declared limit is now discharged. The reviewer stated Lint & Repo Gates as in_progress at its 15:41Z poll and explicitly not read, citing the previous head's green as precedent on the text rather than as a claim about this head. It completed success and was read here at 15:49Z. ⇒ the gap the reviewer honestly left open was closed by the seat, not assumed away — which is the point of declaring a limit instead of waiting silently for it.

⚠️ One earlier carrier omission of mine, for the record. At 15:13Z I re-hung needs:contract-review on card #17785 but not on this PR, leaving the carrier the enqueue gate reads bare while a fresh review was owed — the fail-open direction. The round's dev found it, identified it as seat state it should not write back, and reported it instead of correcting it. Re-hung at 15:23Z; --pair returned to exit 0 immediately.

⛔ 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

@zhuangjianguo
zhuangjianguo marked this pull request as ready for review September 13, 2026 15:50
@zhuangjianguo
zhuangjianguo added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit 6d64785 Sep 13, 2026
44 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-17785-tracing-duration-units branch September 13, 2026 16:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: 4 duration key(s) in system/tracing.zod.ts name their unit only in JSDoc — #15939 Ruling A remediation (4 of the 21-row delta)

2 participants