Summary
check-clause2-carriers' T1 widening tell reads any added source line shaped identifier: z.Something as "a new key on a Zod object schema — the accept set gains a spelling an author may now write". It does not require that line to be inside a Zod object shape, so a TypeScript function parameter annotated with a zod type matches it.
Measured on PR #17616, whose diff narrows an accept set and adds no key at all:
$ node scripts/pm/check-clause2-carriers.mjs --pair 17616
T1 packages/spec/src/ui/dashboard.zod.ts:434 — a new key on a Zod object schema —
the accept set gains a spelling an author may now write
+ ctx: z.RefinementCtx,
Line 434 is the second parameter of an exported refinement check:
export function checkDashboardWidgetStageOrder(
widget: { type?: unknown; options?: { stageOrder?: unknown } | null },
ctx: z.RefinementCtx, // <- T1 reports this as an authorable key
): void {
z.RefinementCtx is a TYPE, not a schema; nothing constructs a shape here and no accept set gains a spelling. The exact opposite is true of the diff the row appears on — it adds a superRefine that refuses metadata which parses today.
Why it is worth a card rather than a shrug
The row is report-only, so nothing is mis-labelled by it. The cost is on the reading, and it is the same cost #16943 recorded for T2:
the C5 row it raises cannot be cleared except by declaring Clause-② yes on a change that does not widen
Here the pair does declare yes (correctly, for the narrowing), so C5 fires for the other reason and the T1 line is the only evidence it prints. A reviewer reading "the accept set gains a spelling an author may now write" against a narrowing PR has to open the file to find out it is a function parameter. A tell that is wrong in the direction opposite to the change is the shape that gets a diagnostic ignored, and then ignored on the run where it was right.
⭐ The false positive is also systematic, not incidental: (value, ctx: z.RefinementCtx) is the repo's own prescribed signature for an exported object-level refinement (the #16489 convention — checkListViewPageMount, checkListViewCalendarVisualization, checkPageSourceCompleteness, checkGlobalFilterDateDefaultValue). So every PR that adds one of these checks — which is to say every PR that adds a cross-field REFUSAL — will raise a T1 widening tell for the parameter of the function doing the refusing.
Discriminator
Both probes on the same run, so this is not an artifact of how it is written:
| line added by PR #17616 |
T1 |
ctx: z.RefinementCtx, (a function parameter) |
reported as a new authorable key |
stageOrder: z.array(...).optional() — the real shape member, UNCHANGED by the diff |
not reported (correctly — it is not an added line) |
And the control that shows T1 is not simply reporting every added z. line: the same diff adds const WIDGET_TYPE_DEFAULT = 'metric'; and several .superRefine(...) / .describe(...) lines, none of which are reported.
Suggested repair, and the trap in it
The obvious narrowing — require the match to sit inside a z.object({ … }) / strictObject(…, { … }) shape body — is a depth-aware read over a comment-and-string mask, which is exactly the reader scripts/check-widget-option-census.mjs had to build for the same question (its header measures four cheaper readers that each fabricate or truncate). ⚠️ The cheap version fails GREEN: a reader whose scope ends at the first }) truncates, reports fewer tells, and a widening tell that goes missing is worse than one that fires twice.
A cheaper and possibly sufficient discriminator: exclude a match whose line sits inside a parameter list — between an unclosed ( opened by a function / arrow declaration and its ). That is a smaller claim than "is it a schema shape" and it covers the whole systematic class above.
Where
scripts/pm/check-clause2-carriers.mjs — the T1 tell.
- Reproduce:
node scripts/pm/check-clause2-carriers.mjs --pair 17616 (exit 4; the C5 row prints both tells).
Related
Generated by Claude Code
Summary
check-clause2-carriers' T1 widening tell reads any added source line shapedidentifier: z.Somethingas "a new key on a Zod object schema — the accept set gains a spelling an author may now write". It does not require that line to be inside a Zod object shape, so a TypeScript function parameter annotated with a zod type matches it.Measured on PR #17616, whose diff narrows an accept set and adds no key at all:
Line 434 is the second parameter of an exported refinement check:
z.RefinementCtxis a TYPE, not a schema; nothing constructs a shape here and no accept set gains a spelling. The exact opposite is true of the diff the row appears on — it adds asuperRefinethat refuses metadata which parses today.Why it is worth a card rather than a shrug
The row is report-only, so nothing is mis-labelled by it. The cost is on the reading, and it is the same cost #16943 recorded for T2:
Here the pair does declare
yes(correctly, for the narrowing), so C5 fires for the other reason and the T1 line is the only evidence it prints. A reviewer reading "the accept set gains a spelling an author may now write" against a narrowing PR has to open the file to find out it is a function parameter. A tell that is wrong in the direction opposite to the change is the shape that gets a diagnostic ignored, and then ignored on the run where it was right.⭐ The false positive is also systematic, not incidental:
(value, ctx: z.RefinementCtx)is the repo's own prescribed signature for an exported object-level refinement (the#16489convention —checkListViewPageMount,checkListViewCalendarVisualization,checkPageSourceCompleteness,checkGlobalFilterDateDefaultValue). So every PR that adds one of these checks — which is to say every PR that adds a cross-field REFUSAL — will raise a T1 widening tell for the parameter of the function doing the refusing.Discriminator
Both probes on the same run, so this is not an artifact of how it is written:
ctx: z.RefinementCtx,(a function parameter)stageOrder: z.array(...).optional()— the real shape member, UNCHANGED by the diffAnd the control that shows T1 is not simply reporting every added
z.line: the same diff addsconst WIDGET_TYPE_DEFAULT = 'metric';and several.superRefine(...)/.describe(...)lines, none of which are reported.Suggested repair, and the trap in it
The obvious narrowing — require the match to sit inside a⚠️ The cheap version fails GREEN: a reader whose scope ends at the first
z.object({ … })/strictObject(…, { … })shape body — is a depth-aware read over a comment-and-string mask, which is exactly the readerscripts/check-widget-option-census.mjshad to build for the same question (its header measures four cheaper readers that each fabricate or truncate).})truncates, reports fewer tells, and a widening tell that goes missing is worse than one that fires twice.A cheaper and possibly sufficient discriminator: exclude a match whose line sits inside a parameter list — between an unclosed
(opened by afunction/ arrow declaration and its). That is a smaller claim than "is it a schema shape" and it covers the whole systematic class above.Where
scripts/pm/check-clause2-carriers.mjs— the T1 tell.node scripts/pm/check-clause2-carriers.mjs --pair 17616(exit 4; the C5 row prints both tells).Related
check-clause2-carriersreads a key-INITIAL describing line as a declaration — its own self-test asserts the general property "only DESCRIBES ⇒ MISSING", and the fixture pinning it covers only the other half #17098, [finding]check-clause2-carriersC4 is silent on every 2026-09-09 contract-review verdict — itsVERDICT:discriminator and session-onlyReviewed-by:grammar match neither the boldVerdict:heading dialect nor the isolated-subagent authorship the seats now write #17346 — othercheck-clause2-carriersreading defects, open.options.stageOrderis declared for every chart type, documented for a chart type that does not exist, and silently dropped in every non-enlocale #17344 finding 1 (PR fix(spec)!: refusedashboard.widgets[].options.stageOrderon every widget type that does not read it #17616). ⛔ Not fixed there: that PR's face ispackages/spec/src/ui/dashboard.zod.ts, and a PM-tooling change does not belong in a PR carrying a published-surface narrowing.Generated by Claude Code