Skip to content

[finding] check-widening-tells T2 reads a z.enum( token INSIDE a string literal as a closed-set opener — every mandated retirement prescription that names the API to call instead reads as a widening #19221

Description

@os-bill

Path: none | instrument (scripts/pm/check-widening-tells.mjs T2 — an opener token INSIDE a string literal) | Blocked-by: #19384
Blocked-by: #19384
Restart-when: PR #19438 reads MERGED by content on origin/main ⇒ re-run the card's three legs on that tree; leg A silent ⇒ close as fixed by #19384, else pm:queue

Filed by the domain:spec seat 2 execution seat (座位贴 #18549, session_01JbZnqu8bt6YqfJsr9vaFb3), from the ② note of the at-tier contract review of PR #19215 (record 5746455074). ⛔ Filed unassigned, ⛔ no priority:*, ⛔ no domain:*, ⛔ no type — routing and grading are triage's. ⛔ Not a claim. ⛔ Not a ruling.

The defect

scripts/pm/check-widening-tells.mjs's T2 matcher reads the token z.enum( inside a string literal as a closed-set opener. A prescription string that teaches the author which API to call instead therefore reads as "the accept set gains a value", and --declaration no exits 4 on a diff that widens nothing.

Measured — three legs, one instrument, two controls

Synthetic diff, one file, three added lines, run through the gate itself:

✗ T2 packages/spec/src/probe.ts:3
    + + 'is a declaration with no enforcement; call `z.enum()` directly instead.',
✗ T2 packages/spec/src/probe.ts:7
    + return z.enum(values, {
leg added line want got
A — prose fragment whose TEXT mentions the opener in backticks + 'is a declaration with no enforcement; call `z.enum()` directly instead.', silent T2 fires ⇐ the defect
B — dark control: the same continuation-fragment shape with no opener token in its text + 'in the enum — the enum accepts the value, so the prescription can never fire.', silent silent
C — firing control: a real new opener return z.enum(values, { T2 T2

⇒ the trigger is the token inside the prose, not the fragment shape. Leg B is the discriminator: it rules out "the concatenation-fragment guard is broken", because that guard is demonstrably still working on the identical shape.

Where it was hit for real

PR #19215 (Fixes #17109), packages/spec/src/shared/retired-key.ts:329:

throw new Error(
  'enumWithRetiredValues: no retired member declared — a retirement wrapper that can never fire '
    + 'is a declaration with no enforcement; call `z.enum()` directly instead.',
);

node scripts/pm/check-widening-tells.mjs --declaration no --diff <pr19215.diff>exit 4, 2 tells, one of them this line.

⚠️ The sibling tell on the same run — :349, return z.enum(values, { — is NOT part of this card: it is a real opener, and #18640 settled that shape by maintainer ruling (batch #155 item 3, letter A, 「同意」 2026-09-18T05:13Z): the subset test stays and the remedy is Clause-②: yes plus the spec lane's at-tier review, ⛔ no licence card. That is exactly the route PR #19215 took. ⛔ Do not read this card as re-litigating that ruling.

⭐ Why this one bites systematically rather than once

This repo requires the text it is firing on. AGENTS.md Post-Task Checklist #3 and the spec-property-retirement kit make a removal's prescription mandatory, and a good prescription names the API the author should call instead. So the matcher is biased precisely against the diffs this repo mandates — retirements, tombstones and construction-time refusals — and it is biased in the direction that costs a landing, not the one that lets a widening through.

Remedy shape (the gate's own prescription, quoted)

if the tell is FALSE, repair it here in the matcher (scripts/pm/check-widening-tells.mjs, with a --self-test case pinning the shape), or file that repair as its own card when it is out of this PR's scope.

The three legs above are ready-made --self-test battery cases; leg B is the one that keeps a repair from being a blanket "ignore lines containing quotes".

⚠️ NOT measured

  • Whether the same prose-mention blindness exists on T1. Only T2 was probed.
  • How often it has actually cost a landing. No sweep of past runs was taken. ⛔ Do not read "it bit once here" as a frequency.
  • check-clause2-carriers.mjs's twin matcher, if it has one. Not probed.

Dedupe

Searched open and closed over check-widening-tells: 12 results, listed rather than summarised, none covering the prose-mention shape.

  • #19099 open — T1 on a member bounded inside a previously-z.unknown() bag.
  • #19156 open — misses a key re-typed INTO a universal acceptor.
  • #19165 open — check-changeset-no-major's remediation prose, a different script.
  • #18640 closed not_planned — a REAL opener in an appended options object; the maintainer ruling quoted above.
  • #16822 closed completed (LANDED, PR fix(pm): stop check-widening-tells T2 keying on a continuation operator, and price a false positive honestly #16944, added isConcatenationFragment) — T2 on the FIRST fragment of a multi-line string ARGUMENT. Closest sibling, and leg B is the evidence its guard still works: the fragment shape alone no longer fires. This card is about the token in the text.
  • #18234, #18629, #18488, #18702, #18560, #17955, #17300 closed — T1-on-z.union(, the z.unknown() reverse blind spot, matchingCloser, file-local declaring factories, the repo's own declaring helpers, retiredKey() tombstone lines, and the retirement ledger's generated rows respectively. None is a token read out of a string literal.

⛔ The zero on this shape is a reading, not a void one: the same query returns 12 siblings, so the instrument fires.

Dedupe words

check-widening-tells · T2 · BARE_STRING_ELEMENT · opener inside a string literal · prescription prose · z.enum() in backticks · false C5


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

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions