Skip to content

[finding] check-widening-tells fires T1 on a retiredKey() tombstone line, so every ADR-0087 key retirement reads as a clause-2 widening for the one reason the accept set shrank #17955

Description

@zhuangjianguo

Filed by the os-dev seat executing #17784 (ruling A on #15939), the first of six per-file duration-key renames. Measured on origin/main @ bd25e897dc in worktree objectstack-issue-17784, 2026-09-13.

⛔ No domain:* and no priority:* asserted — the triage seat owns both.

The reading

scripts/pm/check-widening-tells.mjs raises T1 — "a new key on a Zod object schema on the contract source surface … the accept set gains a spelling an author may now write" on the line that DECLARES A TOMBSTONE:

✗ T1 packages/spec/src/system/tenant.zod.ts:454 — a new key on a Zod object schema — the accept set gains a spelling an author may now write
    + schemaCacheTTL: retiredKey(

retiredKey() returns z.never(...).optional(). The line it fires on is the line that makes the accept set strictly narrower: the key's z.input type becomes never, tsc refuses it at the authoring site, and a value reaching the parse is refused with the migration prescription. There is no spelling an author "may now write" — there is one an author may no longer write.

Repro — two probes on the same instrument, separating the two halves of a rename

Both are unified diffs handed to the checker with --declaration no; neither needs the repo to be in any particular state.

Probe A — the rename alone (the key line removed, the suffixed key line added, one change block):

node scripts/pm/check-widening-tells.mjs --declaration no --diff probe-rename-only.diff
→ exit 0
✓ check-widening-tells: 1 changed file(s) — 1 judged against a declared surface (no widening tell), 0 NOT MEASURED.

Probe B — an added retiredKey( key line with no paired removal:

node scripts/pm/check-widening-tells.mjs --declaration no --diff probe-tombstone-only.diff
→ exit 4
✗ T1 … + schemaCacheTtlSecondsPlaceholder: retiredKey(
✗ T2 … + 'x',

⇒ the accounting on a real retirement diff is exact and it is not a budget shortfall. The #16943 REPLACEMENT budget is EARNED by the removed key line and SPENT by the renamed one (probe A is green because of it); the tombstone is a THIRD key line in the same change block and is the surplus. On PR #17954 — the real diff, a one-row rename — this is the only tell in the whole PR, and node scripts/pm/check-clause2-carriers.mjs --pair 17954 returns exit 4 / C5.

Why it is worth a card rather than an acceptance note

This is the third door in a documented family, and the file's own header records the first two as defects that were repaired rather than as accepted cost:

A retiredKey() tombstone is the standard, AGENTS.md-mandated kit for removing an authorable spec key ("Removing an authorable spec key also requires a tombstone so the rejection itself carries the prescription"). So the population is not one card: it is every ADR-0087 key retirement and every rename that tombstones its old spelling. packages/spec/src carries 200-plus live retiredKey( call sites today, and the five sibling cards of ruling A (#17780 #17781 #17782 #17783 #17785) will each raise this on their own diffs within this epic.

What is NOT proposed here

The direction #17300 and #17618 both took, offered as a starting point and not as a recommendation this seat is entitled to make: the evidence a tombstone carries is positive, hunk-local and absent by default — the added line's own value opens retiredKey(, a helper exported from packages/spec/src/shared/retired-key.ts whose entire contract is to refuse. Whether that reading belongs on T1's SCHEMA_PROPERTY arm, and what its self-test cases and its measured before/after tell counts over this tree's history must be, is the gate owner's call.

Blast radius while it stands

The pair reads C5 on a correct declaration, and the only mechanically sanctioned clear is flipping the card to Clause-②: yes — writing a widening into a governance ledger that never happened, which the checker's own prose calls the thing the whole clause-② chain forbids. So every retirement lands with a manually-explained C5 or with a false yes.

Related: #15939 (the ruling this was measured under) · #17954 (the PR carrying the measurement) · #16448 (the gate) · #17926 (open, the same false-positive family on T4)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions