Skip to content

check-widening-tells fires T2 on the retirement ledger's own generated rows, so every retirement's clause-② reading is adverse for the one reason the accept set SHRANK #17300

Description

@os-bill

Filed by the domain:spec execution seat, session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-10T04:40Z, out of PR #17298 (card #17063, the maintainer's 「撤」 retirement of the list-view type: 'page' mount). ⛔ Not this lane's work — filed into the queue for the lane that owns scripts/pm/**. No domain:* label: ⛔ that label is the triage seat's to produce.

The reading

check-widening-tells's T2 tell is "a new member of a closed set (z.enum / union / an as const array) — the accept set gains a value". A retirement adds rows to the retirement ledger, and those rows are members of closed sets. So T2 fires on them.

Measured on PR #17298's finished diff, from a byte-verified origin/main mirror of the scripts:

# line added row
1 packages/spec/src/migrations/registry.ts:5443 'view-page-mount-removed',
2 packages/spec/src/migrations/registry.ts:12757 'ui/ListView:pageName',
3 packages/spec/src/migrations/registry.ts:12791 'ui/ObjectListView:pageName',
check-widening-tells --declaration no   → EXIT 4, 3 tells
check-widening-tells --declaration yes  → EXIT 0
check-clause2-carriers --pair 17298     → EXIT 4 (C5)

Two of the three rows are inside registry.ts's os-generated markers (⚠️ the third is not — see the correction at the end). They are not hand-written; gen:migration-registry emits them from the new files under src/migrations/entries/. And each one exists because the accept set shrank — they are the machine-readable record of two authorable things being removed (an enum value and a key). T2 cannot see that the closed set it is watching is the ledger of removals.

Why this is not a one-off

Every retirement adds RETIRED_*_BY_MAJOR rows. That is the retirement kit working as designed — the tombstone ledger is what carries the prescription to an upgrading consumer. So this reading is structural: the clause-② mechanical axis will read adverse on the one change class whose semantic direction is most unambiguously narrowing.

What it costs, and why the cost is not visible yet

On PR #17298 the seat ruled the declaration stays Clause-②: no and recorded the disagreement (ruling: #17063 comment 5613244487). That is survivable there for a reason that will not generalise: #17298 is GOVERNED (skills/** ×2), so it ends in a maintainer hand merge and is never enqueued — and --pair is an enqueue-legibility gate.

⚠️ On the next retirement that is not governed, the same situation has no exit. The seat's three landing pre-checks require --pair exit 0. The options collapse to:

  • declare no (semantically right) ⇒ --pair exit 4 ⇒ ⛔ cannot land; or
  • declare yes (semantically backwards) ⇒ the clause-② axis reports widening for a pure narrowing, and that becomes the precedent for every retirement thereafter.

⛔ Neither is acceptable as a standing answer, which is why this is a card and not a note.

Shape of a fix — offered as input, ⛔ not as a ruling

The narrow version: teach the tell-scanner that a + line inside registry.ts's os-generated retirement-ledger regions is a narrowing artefact, not a T2 tell. The regions are already machine-delimited (<os-generated semantic:NN> and the retired-key/retired-def blocks), so the discriminator exists in the file rather than needing to be invented.

⚠️ Two things for whoever takes it to weigh, ⛔ neither of which this seat is deciding:

  • A blanket exclusion of a generated region is a hole, not just a fix — anything a generator emits into that region stops being judged. The narrower predicate ("inside the retirement-ledger markers and the row is a retired-key / retired-def / conversion id") keeps the hole the size of the actual problem.
  • Whatever is built must be able to fail. A tell-scanner change that can only ever suppress is untestable in the direction that matters: it needs a case proving a genuine T2 inside those files still fires, and a case proving a ledger row does not.

Acceptance

  • A retirement diff whose only closed-set additions are retirement-ledger rows reads --declaration no exit 0, and --pair exit 0 against a Clause-②: no declaration.
  • A genuine widening in the same file family still reads exit 4 — proved by a case in the checker's own --self-test, ⛔ not by argument.
  • The --self-test case count moves, and the new cases are pinned.

Dedup — complete enumerations, stated as complete

enumeration population hits
label:finding open 165 0
label:domain:devx open 114 0
label:domain:skills open 18 0

Matched on widening.tell, T2, retirement ledger, retired_*_by_major, clause.2, generated region over titles. ⛔ No free-text search_issues was used and no zero here rests on one — that call returns a silent total_count: 0 on this board.

Refs: PR #17298 · card #17063 · seat ruling 5613244487 · scripts/pm/check-widening-tells.mjs · scripts/pm/check-clause2-carriers.mjs

⛔ Correction (2026-09-10T04:55Z) — one of the three rows is NOT generated, and it changes the fix

An at-tier contract review of PR #17298 caught this, and I re-measured it myself on that head before correcting:

line 5443:  'view-page-mount-removed',
   nearest os-generated marker above → 5055: // </os-generated semantic:17>   ← a CLOSING marker
line 12757: 'ui/ListView:pageName',
   nearest os-generated marker above → 10428: // <os-generated retired-key:18>  ← an OPENING marker

registry.ts:5443 is outside every generated region. It is the hand-maintained step18.conversionIds array — a chain step's list of the conversions that run at that hop. Only rows 2 and 3 (ui/ListView:pageName, ui/ObjectListView:pageName) sit inside <os-generated retired-key:18>.

Everything the card concludes still holds — the row still exists because the accept set shrank, nothing authorable joins any accept set, and the clause-② axis still reads adverse on a pure narrowing. ⚠️ But the fix shape above was wrong, and would have shipped a half-fix: a predicate keyed only on "inside a generated retirement-ledger region" covers 2 of the 3 tells, and --pair stays exit 4 on every retirement that also registers a conversion. Which is most of them — a D2 retirement registers a conversion by definition.

Corrected target for whoever takes this. The discriminator has to cover both shapes:

  1. a row added inside the retirement-ledger generated regions (<os-generated retired-key:NN> / retired-def), and
  2. a conversion id added to a chain step's conversionIds array.

⭐ And shape 2 is the one that needs care, because it is hand-maintained: ⛔ it cannot be excluded by position alone. A defensible predicate is "the added string resolves to a registered conversion id" — which is checkable against the conversion registry rather than against where the line sits — but that is a judgement for the owning lane, ⛔ not a decision this card makes.

The acceptance criteria stand and matter more now: a genuine T2 in the same file family must still fire, proved by a --self-test case, ⛔ not by argument.

⭐ Recorded rather than silently edited above, because the original framing is what a reader would otherwise have built on: I asserted "all three are generated rows" from the dev's report and my own reading of the tell list, and ⛔ neither of us checked which marker each line actually sat under. The nearest marker above a line can be a closing one — which is exactly how a line outside every region looks like a line inside one.


Generated by Claude Code

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions