Skip to content

fix(pm): teach check-widening-tells the declaring helpers, as a named list with a counterfactual pin - #18700

Merged
os-justin merged 3 commits into
mainfrom
claude/issue-18560-widening-tells-declaring-helpers
Sep 17, 2026
Merged

os-justin merged 3 commits into
mainfrom
claude/issue-18560-widening-tells-declaring-helpers

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #18560

Clause-②: no

scripts/pm/check-widening-tells.mjs is the TELL half of the clause-② gate (the DECLARATION half, check-clause2-carriers.mjs, is a different file and PR #18681's subject). Its T1 reader decides a line declares a schema member by what the property's VALUE opens with, and that vocabulary was five alternatives inside one 130-character regex literal. A form missing from it is not a line judged leniently — it is a line that is not a key line at all: memberTellKind answers null, the row neither fires nor spends the #16943 replacement budget nor earns it on the removed side, and nothing in the output says so. The silence is indistinguishable from a correct no.

The counterfactual, before and after

Re-derived here rather than inherited from the card's reading (objectui#9647 comment 5707064702, an at-tier reviewer's measurement). Run as PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-widening-tells.mjs --declaration no --diff …, the BEFORE leg against the file as it stands at 6dfa3ea77 (this branch's merge base), the AFTER leg at 7f1a81419:

diff declaration BEFORE (6dfa3ea) AFTER (7f1a814)
objectui#9647, unmodified no exit 0, 1 file judged, 0 tells exit 0, 1 file judged, 0 tells
objectui#9647 plus one key added through stripImportedDefaults( no exit 0, 0 tells exit 4, T1 at packages/types/src/zod/data-display.zod.ts:584
the same widened diff yes exit 0 exit 0 (a yes is never blocked)

Row 2 is the red this PR turns. Row 1 does NOT change verdict, and that is the correction the re-derivation forced.

What the re-derivation corrects

The two helpers the card names are objectui's REFUSAL family (packages/types/src/zod/tombstone.zod.ts), read off the source:

  • retirementTombstone(guidance) returns z.never({ error: guidance }).optional().describe(guidance) — the same primitive as this repo's retiredKey();
  • handlerKeyRefusal(key, disposition, label) returns a z.custom predicate typed never that returns false, and its own docblock records that "The predicate refuses EVERYTHING, a live function included".

A key declared through either is a key an author may NO LONGER write. Making those two fire a tell would re-mint, on 290 objectui key lines, the exact false positive #17955 removed on 255 objectstack ones — and a false tell does not cost a word in a comment, it costs the false yes this file's own header refuses to ask an author for.

The form that DOES carry a widening, and that no seat had named, is stripImportedDefaults() (packages/types/src/zod/imported-defaults.ts), whose docblock states its contract as "the same TypeScript type, the same keys, the same checks, the same registry metadata and the same accept set". It returns a LIVE schema and is spelled at 45 key positions on the judged objectui surface.

The named list

SCHEMA_PROPERTY_FORMS — exported, frozen, and the constant SCHEMA_PROPERTY is BUILT from it. Two fields carry two questions: pattern (what makes the line a KEY LINE) and writable (whether the key it declares is one an author may write). Counts are key-POSITION counts, each with the tree it was taken against.

form writable measured
z. yes 7,784 at objectstack 6dfa3ea77 · 1,482 at objectui 15f01223d
lazySchema( yes 0 at objectstack 6dfa3ea77 (live at DECLARATION positions)
strictObject( yes 47 at objectstack 6dfa3ea77
*Schema yes 995 at objectstack 6dfa3ea77 · 56 at objectui 15f01223d
stripImportedDefaults( yes 45 at objectui 15f01223d (added)
retiredKey( no 255 at objectstack 6dfa3ea77
retirementTombstone( no 187 at objectui 15f01223d (added)
handlerKeyRefusal( no 90 at objectui 15f01223d (added)
aliasKeyRefusal( no 13 at objectui 15f01223d (added)

The writable: false arm is #17955's decline generalised from one helper name to the family, on the SAME positive, line-local evidence: the value must BE the call and nothing after it. declaresRetiredKeyTombstone is renamed declaresUnwritableKey accordingly; it is not imported by any other file.

The no criterion is not loosened anywhere, and the direction is provable rather than argued. An unrecognised line reports NOTHING, so no row that fires today can stop firing when the list grows. A self-test case keeps the literal this replaced as the reference and asserts it: every legacy verdict is byte-identical, and the only cells that move are the four added forms moving from "not a key line" to "a key line" — one direction, zero losses.

The pins

New battery, registered on the roster and pinned: '#18560 — the declaring vocabulary is a NAMED list, every form pinned by a counterfactual fixture': 30 (39 cases run against a floor of 30). Its unit is the FORM, not the assertion:

  • a frozen fixture roster is asserted EQUAL to the form set, in both directions and naming the offenders — a form added to the list without a fixture reds, a form silently dropped from the list reds. The fixtures are deliberately NOT generated from the list, which would make every future form pass by construction;
  • every form is asserted RECOGNISED (memberTellKind answers T1) — writable or not, because an unrecognised line is invisible to both sides of the budget, which is the defect itself;
  • every form's fixture is then driven through tellsInFile and asserted against its OWN register: a writable form must FIRE with its file:line, an unwritable one must be recognised and DECLINE;
  • every unwritable form carries the chained-arm control that FIRES, so the decline is bound to the evidence the line carries and never to the helper's name;
  • the objectui#9647 shape is carried as a case in all three readings, plus the stripImportedDefaults( widening with its refusal, its file:line, the yes control and the default-board control.

check:entry-guard is the reason the vocabulary's structural guard is a const initializer's map step rather than a top-level for: this file exports bindings the sibling gate imports, so a load-time throw would run inside the importer.

The census — report-only, no re-grade, no state write

Has the silence already been relied on? Zero confirmed Clause-②: no landings through these forms, over 11 of 18 rows read.

The ablation

From the committed fix, HEAD blob 1b741ee6c22db6523475b698d392a8ecd41686d6, under a trap '…' EXIT INT TERM restoring git checkout HEAD -- scripts/pm/check-widening-tells.mjs at an absolute path. No build or dist/ is involved — the gate runs from source, so there is no rebuild leg to prove.

  • mutation: the retirementTombstone( and handlerKeyRefusal( rows deleted from SCHEMA_PROPERTY_FORMS;
  • mutation proved on disk, not from an exit code: anchor counts 1 → 0 for each form, file 257,365 → 256,697 bytes, blob 9fb65edf6f68b1501a0aec29d0ddb4c95bcf44a2 which is not the HEAD blob;
  • predicted direction: RED. Observed: RED — --self-test exits 1, "6 of 377 case(s) failed" plus the verdict-handshake report. The failures name the mechanism: the orphaned-fixture case prints both dropped forms, both RECOGNITION cases fail, both objectui#9647 cases fail, and the one-direction case fails;
  • restore verified by hash, not by a return code: git hash-object back to 1b741ee6c22db6523475b698d392a8ecd41686d6, git diff HEAD empty, git status --porcelain clean, and the suite back to 381 cases pass.

Self-test

pnpm check:pm-widening-tells381 cases pass (342 before this PR; +39). Exit 0. The two gates that IMPORT this module were run too: check:pm-clause2-carriers exit 0, check:pm-prior-rulings exit 0.

Derived gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack from the worktree, no hand-fed path list — 29 commands, each run with $? captured BEFORE any pipe. Reconciled with --ran: "29 derived famil(ies) accounted for — 29 run, 0 NOT-MEASURED (a DERIVED zero — all 29 recorded an exit code and none of them is 3)".

exit 0  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  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

Repo-wide pnpm lint (eslint . --no-inline-config) at 7f1a81419: exit 0, no findings. Control-byte scan over the edited file: no hits.

skip-changeset: scripts/pm/** is not published by any package's files[], and this diff touches nothing else.

One boundary this does NOT close, recorded rather than left to be found

A FILE-LOCAL declaring factory. Both trees mint them — placeholderFree( (23 key lines), strictIdent( (12), emptyProps( (9) at objectstack 6dfa3ea77; chatbotRequestBodyArm( (2), retiredDeclarativeKanbanKey( (1) at objectui 15f01223d — and a list of shared, exported helpers cannot name a factory private to one file. A name-shaped heuristic is refused in the header, with the overturn condition written down. Filed as its own finding in the report on the card, not fixed here.


Generated by Claude Code

… list with a counterfactual pin

`SCHEMA_PROPERTY` was five alternatives inside one regex literal, and a form
missing from it is not a line judged leniently — it is a line that is not a key
line at all, invisible to both sides of the #16943 budget, in silence.

The vocabulary is now `SCHEMA_PROPERTY_FORMS`: a named, enumerable list the
regex is built from, with two registers (`pattern` = recognition, `writable` =
whether the key it declares is one an author may write). objectui's declaring
helpers join it; the refusal family declines on #17955's own line-local
evidence, and `stripImportedDefaults(` — the live one, which no seat had named —
now fires.

Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Co-authored-by: Claude <noreply@anthropic.com>
`check:entry-guard` reads a top-level `for` that throws as load-time dispatch,
and this file exports bindings the sibling gate imports. The same assertion is
now the const initializer's own map step — a declaration, which is what the rest
of this module's vocabulary already is.

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 16:02
@os-justin
os-justin added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit a7e9a66 Sep 17, 2026
37 checks passed
@os-justin
os-justin deleted the claude/issue-18560-widening-tells-declaring-helpers branch September 17, 2026 16:25
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