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:
- a row added inside the retirement-ledger generated regions (
<os-generated retired-key:NN> / retired-def), and
- 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
Filed by the
domain:specexecution seat,session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-10T04:40Z, out of PR #17298 (card #17063, the maintainer's 「撤」 retirement of the list-viewtype: 'page'mount). ⛔ Not this lane's work — filed into the queue for the lane that ownsscripts/pm/**. Nodomain:*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 / anas constarray) — 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/mainmirror of the scripts:packages/spec/src/migrations/registry.ts:5443'view-page-mount-removed',packages/spec/src/migrations/registry.ts:12757'ui/ListView:pageName',packages/spec/src/migrations/registry.ts:12791'ui/ObjectListView:pageName',⭐ Two of the three rows are inside⚠️ the third is not — see the correction at the end). They are not hand-written;
registry.ts'sos-generatedmarkers (gen:migration-registryemits them from the new files undersrc/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_MAJORrows. 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-②: noand recorded the disagreement (ruling: #17063 comment5613244487). 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--pairis an enqueue-legibility gate.--pairexit 0. The options collapse to:no(semantically right) ⇒--pairexit 4 ⇒ ⛔ cannot land; oryes(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 insideregistry.ts'sos-generatedretirement-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.Acceptance
--declaration noexit 0, and--pairexit 0 against aClause-②: nodeclaration.--self-test, ⛔ not by argument.--self-testcase count moves, and the new cases are pinned.Dedup — complete enumerations, stated as complete
label:findingopenlabel:domain:devxopenlabel:domain:skillsopenMatched on
widening.tell,T2,retirement ledger,retired_*_by_major,clause.2,generated regionover titles. ⛔ No free-textsearch_issueswas used and no zero here rests on one — that call returns a silenttotal_count: 0on 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:
⇒
registry.ts:5443is outside every generated region. It is the hand-maintainedstep18.conversionIdsarray — 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
--pairstays 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:
<os-generated retired-key:NN>/ retired-def), andconversionIdsarray.⭐ 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-testcase, ⛔ 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