Skip to content

fix(pm): check-widening-tells' parameter decline survives a hunk's leading context - #18804

Merged
os-justin merged 1 commit into
mainfrom
claude/issue-18721-refinement-ctx-parameter-false-t1
Sep 17, 2026
Merged

os-justin merged 1 commit into
mainfrom
claude/issue-18721-refinement-ctx-parameter-false-t1

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #18721

Clause-②: no

The defect

scripts/pm/check-widening-tells.mjs read + ctx: z.RefinementCtx, — the second parameter of an exported object-level refinement, this repo's own prescribed #16489 signature — as "a new key on a Zod object schema", although #17618's parameter decline for exactly that line already existed and was pinned. Every PR that adds an object-level refusal therefore earned a free T1 and a C5 / exit 4 on --pair, and the cheapest remedy that clears it — re-declaring Clause-②: yes — is the wrong one: such a diff tightens the accept set, it does not widen it. The instrument read the tightening direction as the widening one, which is the inverse of what clause 2 exists to catch.

The two before-readings, re-derived rather than inherited

Both taken on this worktree's base, origin/main 7572329069 (the card measured 94b3f37be; the defect is unchanged on the newer tip).

FALSE POSITIVE — the card's own probe, rebuilt from git:

git diff 72dd95fa5a..09e16a5745 -- packages/spec/src/ui/dashboard.zod.ts > d.patch   # 202 lines, ONE hunk
node scripts/pm/check-widening-tells.mjs --declaration no --diff d.patch
-> exit 4
   T1 packages/spec/src/ui/dashboard.zod.ts:628 - a new key on a Zod object schema
   + ctx: z.RefinementCtx,

TRUE-POSITIVE CONTROL — same matcher, same run, same real file path, built as a real git diff in the worktree and then restored with git checkout HEAD -- packages/spec/src/ui/dashboard.zod.ts:

+  brandNewAuthorableKey: z.string().optional(),     # added to DashboardWidgetSchema
node scripts/pm/check-widening-tells.mjs --declaration no --diff control.patch
-> exit 4
   T1 packages/spec/src/ui/dashboard.zod.ts:701 - a new key on a Zod object schema

The matcher fires on a real new key. So the first reading is a FALSE POSITIVE, not a dead instrument. The filing card's own first control read 0 and was its own mis-build (a synthetic path off the declared surface is judged by nothing); this one sits on the path the probe was taken from.

The WHY, measured — and the line that proves it

enclosingDelimiter walks from the first line of the line's own hunk and abandoned the walk, answering null, the first time a closer arrived with an empty stack. null is what both callers read as "keep the tell firing".

A real hunk opens on CONTEXT lines. This one's are the tail of the previous declaration. Instrumented over the real patch, the new-side reading is:

lines index of ctx: 110  newFile index: 110  new-file line: 628  hunk: 0
inParameterList    -> false
enclosingDelimiter -> null
hunk start newFile index: 0
  newFile[0] kind=context text="  });"        #  THE PROVING LINE: two closers, no opener above them
  newFile[1] kind=context text="}"
  newFile[2] kind=context text=""
function head newFile index: 108 "export function checkDashboardWidgetMetricMeasureArity("
enclosingDelimiter from a window starting at the head -> {"opener":"(","head":"export function checkDashboardWidgetMetricMeasureArity"}

The ) on patch line 6 — the hunk's FIRST line, a context line — underflows a stack that has seen no opener, and the reading was over 108 lines before the hunk reached the export function ...( head it went on to show. Neither the 202-line hunk length, nor the object-literal type on the first parameter (widget: { id?: unknown; ... },, whose braces close on their own line), nor the distance to the head is the cause: the single branch is the underflow return null. The three-line synthetic the existing pin drives carries no context line at all, so that pin stayed green through every real diff it was written to protect.

The repair — route A's shape, at that branch

An underflow now DROPS the closer and the walk continues:

if (ch === ')' || ch === ']' || ch === '}') {
  // #18721 - UNDERFLOW: this closes an opener the hunk never showed. Drop it
  // and keep walking. ...
  if (stack.length === 0) continue;
  stack.pop();
}

The argument is a stack one, and it is why this does not loosen the no criterion: everything a hunk opens is strictly INSIDE everything it did not show, so the shown stack is a SUFFIX of the real one and its top — whenever it has one — IS the innermost open delimiter, whatever sits below. An empty shown stack still answers null, so the reading stays positive-evidence-only: the answer is always an opener this hunk showed, never one inferred from a closer.

Not route B. A z.RefinementCtx type-name exception is walked past by one differently-named parameter type, and it would leave the same branch broken for every other parameter shape.

SCHEMA_PROPERTY_FORMS is untouched and still 10 rows. #18560 / PR #18700 and #18702 / PR #18750 are the false-NEGATIVE direction on this same matcher; their rows, fixtures and batteries are untouched, and their headers' words are the ones this round's header section uses.

The pins — one battery, both directions

New battery #18721 - a hunk's LEADING CONTEXT is not a reason to abandon the parameter reading, 14 cases, registered in the roster at 14:

#17618's existing pin in the #18560 battery (#17618's parameter decline is untouched by the wider vocabulary) and its own battery's underflow pin are byte-unchanged and green.

Self-test

node scripts/pm/check-widening-tells.mjs --self-test
-> exit 0 :: 473 cases pass   (459 before this round, + the 14 new)

Every case that fires today keeps firing: the whole suite was run, no pre-existing case changed its verdict, and the header section records the direction in both halves.

Ablation, from the committed fix

Reverted the branch on disk (continue back to return null), proved the mutation landed by blob hash and by anchor counts, ran the suite, restored under a trap and verified the restore by hash. There is no build step and no dist/ for a scripts/pm/*.mjs file, so the on-disk proof is the hash plus the anchor counts.

HEAD blob      : 025f8e51ec3e2858d23b45bc739018ab9e2abd13
anchor counts  : removed-text 1 -> 0 ; injected-text 0 -> 1
mutated blob   : 9459620c63e6590b7e2c81a0c3a2a9a0cbb7020c
VERDICT --self-test under the ablation: exit 1   -> 6 of 473 cases failed
VERDICT probe under the ablation: exit 4         -> T1 back at dashboard.zod.ts:628
VERDICT true-positive control under the ablation: exit 4
restored blob  : 025f8e51ec3e2858d23b45bc739018ab9e2abd13  (== HEAD blob)
git diff HEAD  : 0 line(s)

All 6 failures are in the new #18721 battery and nothing pre-existing reds:

  THE FINDING - PR #18720's real hunk ... reads NO tell
  ...and the whole verdict is CLEAN ...
  CONTROL - a real key added AFTER the parameter list closes still tells, underflowing context and all
  an opener the hunk shows AFTER an underflow is the answer ...
  the OLD side moves too - a REMOVED parameter behind leading context ... buys no budget
  ...and the row that fires is the genuine new key the phantom budget used to pay for

An earlier ablation attempt was a NO-OP (perl with a double-escaped pattern, anchor counts 1 -> 1, blob unchanged): the script's own guard refused it and exited non-zero rather than reporting a reading. The run above is the one that landed.

Gates

Derived from this worktree with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no hand-fed path list; change set scripts/pm/check-widening-tells.mjs, 1 path vs merge base 757232906) — 29 families, all run, all exit 0. Reconciled with --ran: 29 derived, 29 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero — every row carries its exit code).

node scripts/check-ci-filter-parity.mjs                          :: exit 0
node scripts/check-closing-keyword-parity.mjs                    :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test        :: exit 0
node scripts/check-comment-mask-corpus.mjs                       :: exit 0
node scripts/check-declaration-mirrors.mjs                       :: exit 0
node scripts/check-declaration-mirrors.mjs --self-test           :: exit 0
node scripts/check-scripts-symbol-anchors.mjs                    :: exit 0
node scripts/check-scripts-symbol-anchors.mjs --self-test        :: exit 0
node scripts/check-self-test-wired.mjs                           :: exit 0
node scripts/check-self-test-wired.mjs --self-test               :: exit 0
node scripts/check-self-test-workflow-commands.mjs               :: exit 0
node scripts/check-self-test-workflow-commands.mjs --self-test   :: exit 0
node scripts/check-whole-set-label-write.mjs                     :: exit 0
node scripts/check-whole-set-label-write.mjs --self-test         :: exit 0
node scripts/pm/bare-root-worklist.mjs --self-test               :: exit 0
pnpm check:agent-test-spelling                                   :: exit 0
pnpm check:bash32-floor                                          :: exit 0
pnpm check:cli-command-ids                                       :: exit 0
pnpm check:cross-package-test-inputs                             :: exit 0
pnpm check:driver-memory-census                                  :: exit 0
pnpm check:entry-guard                                           :: exit 0
pnpm check:nul-bytes                                             :: exit 0
pnpm check:parse-guard                                           :: exit 0
pnpm check:pm-dispatch-gates                                     :: exit 0   (detached, 742.1s, 1809 cases)
pnpm check:pm-widening-tells                                     :: exit 0
pnpm check:pnpm-filter-targets                                   :: exit 0
pnpm check:ratchet-remedy-authority                              :: exit 0
pnpm check:refd-timer-probe                                      :: exit 0
pnpm check:watch-hint-literal                                    :: exit 0
pnpm check:pm-clause2-carriers   (consumes this file's verdict)  :: exit 0   (838 cases)
pnpm lint   (repo-wide, eslint . --no-inline-config)             :: exit 0

check-scripts-symbol-anchors was the one red in the first sweep — the header section cited path:628, and a line number is not an anchor form. Rewritten to the symbol anchor packages/spec/src/ui/dashboard.zod.ts#checkDashboardWidgetMetricMeasureArity; green on re-run, and the line numbers that carry evidence stayed, in prose.

pnpm lint is the repo-wide run at this PR's final commit, not a narrowing.

No changeset: scripts/pm/** publishes nothing from any released package — skip-changeset.


Generated by Claude Code

…ading context

`enclosingDelimiter` abandoned its walk — answering `null`, which every caller
reads as "keep the tell firing" — the first time a closer arrived with an empty
stack. A real hunk opens on CONTEXT lines, and on this repo's spec files that
context is the tail of the previous declaration (`  });`), so #17618's parameter
decline was over before the hunk reached the `export function …(` head it went
on to show. Every PR adding an object-level refinement earned a false T1 on
`ctx: z.RefinementCtx,` and a C5 / exit 4 on `--pair`, whose cheapest repair —
re-declaring `yes` — is the wrong one.

An underflow now DROPS the closer and the walk continues. The openers a hunk
shows are strictly inside the ones it did not, so the shown stack is a suffix of
the real one and its top IS the innermost open delimiter; an empty shown stack
still answers `null`, so the reading stays positive-evidence-only.

Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Co-authored-by: Claude <noreply@anthropic.com>
@os-justin os-justin added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 17, 2026 — with Claude
@os-justin
os-justin marked this pull request as ready for review September 17, 2026 22:13
@os-justin
os-justin added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit a7bafc2 Sep 17, 2026
37 checks passed
@os-justin
os-justin deleted the claude/issue-18721-refinement-ctx-parameter-false-t1 branch September 17, 2026 22:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants