Skip to content

Commit e2050ce

Browse files
os-justinclaude
andauthored
fix(pm): check-widening-tells resolves a FILE-LOCAL declaring factory through its own definition (#18750)
Fixes #18702 — both halves. Round 1 (`1b1c7a6c5c`) is the file-local resolver described below; round 2 (`d852dae1fb`, on the seat's ruling 1=C, 2=A, 3=A on the card) adds the one `placeholderFree(` row to `SCHEMA_PROPERTY_FORMS` with the card's own probe line as its counterfactual fixture, so the filing probe now exits 4 with one T1 at `memory.zod.ts:112`. The section "What this leaves open, and why it is a different instrument" is round 1's reading; on the relation it is superseded by the round-2 report on the card (comment 5719726758), and this opening line is the seat's edit. Clause-②: no `skip-changeset`: nothing published moves. The whole diff is one file under `scripts/pm/**`, which no package's `files[]` ships. ## The defect T1 decides that a property line declares a schema member by reading the property's VALUE against a named vocabulary of shared, exported declaring helpers — `SCHEMA_PROPERTY_FORMS`, landed by PR #18700. A list of shared helpers cannot name a factory declared inside the one file that uses it, so such a line is not a line judged leniently: it 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 a word. That silence is indistinguishable from a correct `Clause-②: no`, which is the one failure shape this whole chain is written against. ## Before-readings, taken on the tip (objectstack `30bac2880`) | probe | command | reading | | --- | --- | --- | | the card's probe: one added line `snapshotPath: placeholderFree(z.string(), 'persistence.snapshotPath').optional(),` on `packages/spec/src/data/driver/memory.zod.ts` | `node scripts/pm/check-widening-tells.mjs --declaration no --diff FILE` | **exit 0**, 1 file JUDGED, **zero tells** — reproduced exactly as filed | | the lit control: the identical line spelled `snapshotPath: z.string().optional(),` | same | **exit 4**, one T1 at `memory.zod.ts:112` | | the file-local probe: one added line `snapshotObject: strictIdent('Object whose snapshot is taken (snake_case)'),` on `packages/spec/src/ai/solution-blueprint.zod.ts`, whose `strictIdent` is declared in that same file | same | **exit 0**, 1 file JUDGED, **zero tells** | The third row is the one this change turns: after it, the same diff exits **4** with one T1 at `solution-blueprint.zod.ts:343`. ## The reading rule When a key line's value opens with an identifier CALL that `SCHEMA_PROPERTY_FORMS` has no row for, the factory's own definition is resolved out of the judged file and classified by what its body RETURNS: - returns a `z.` schema, a name ending in `Schema`, a form the shared vocabulary already declares writable (`strictObject(`, `lazySchema(`), or the factory's OWN ARGUMENT handed back (`placeholderFree` returns the schema it was given) ⇒ a **WRITABLE** key, and the line FIRES T1 with its file:line; - returns `z.never(` or a `z.custom` whose predicate refuses everything — the same primitives `UNWRITABLE_FORMS` already names — ⇒ **DECLINES**, the same reading `declaresUnwritableKey` gives `retiredKey(`, bound to the same line-local evidence (a live arm chained onto the call fires); - anything else ⇒ **unresolved**, reported by name. **How the file is read: the HEAD BLOB, through git, never "the file of that name in whatever tree this process stands in."** #17300 measured that second reading wrong for this whole family — a seat's worktree is not the diff's head — and a blob id is content, so a reading taken through one can be missing but can never be about the wrong commit. Both input paths carry the id: `git diff` writes it into its `index OLD..NEW` line, and a `/pulls/N/files` row carries it as `sha`. It is read with `git cat-file blob`; only when that fails is the working tree consulted at all, and then only after `git hash-object` proves the file on disk IS that blob, byte for byte. **The shared list stays the fast path and is consulted first.** `keyValueFactoryName` answers `null` for any line `SCHEMA_PROPERTY` already reads, so a form with a row is never re-judged here and the two registers cannot disagree about one line. **The `no` criterion does not loosen.** The resolver only ever ADDS a recognition, and it is consulted on the ADDED side ONLY: a removed local-factory key line buys nothing, so no line that fires today can stop firing because a removal newly pays for it. The price of that asymmetry is stated rather than left to be found — a block that REPLACES one local-factory key with another fires on the added one, a false positive, which is the cost #16448 accepted and the loud direction. It is pinned as a case, and the overturn condition (the first LANDED diff whose only tell is such a replacement) is written into the header. ## The two boundaries — both a STATED silence - **An IMPORTED factory stays unrecognised; imports are not chased.** Only the file the diff CARRIES is pinned to the judged head by the diff itself. The file an import points at is not in the diff, nothing pins it, and reading it out of the local tree is #17300's mistake wearing a longer path. - **A body this reader cannot classify stays unrecognised** — no `return` it can read at the body's own top level, or one that is a template string, an arrow, a number. A `return` belonging to a NESTED callback is not read as the factory's own answer either: brace depth decides, because reading a nested refusal as the factory's verdict is exactly the quiet direction. Neither is the old invisible silence. Every unresolved key line is now reported with its file:line, the factory's name and the reason, under its own heading, and it moves no exit code: ``` ⚠️ 1 key line(s) name a declaring factory this reader could not resolve — a STATED silence: no tell fired on them and nothing cleared them, so this verdict is evidence about neither. packages/spec/src/data/driver/memory.zod.ts:112 — `placeholderFree(` — no definition in this file — an IMPORTED factory is outside this reading ``` ## Census — report-only, no re-grade of any landed PR, no state write The eight factories the card names, at objectstack `30bac2880`, counted at KEY POSITIONS on `packages/spec/src/**`: | factory | key lines | file-local | what its body returns | register | | --- | ---: | ---: | --- | --- | | `placeholderFree(` | 23 | **0** | `return schema.superRefine(…)` — the schema it was handed | WRITABLE | | `strictIdent(` | 12 | 12 | `z.string().regex(SNAKE_CASE).describe(…)` | WRITABLE | | `ruleArrayFilterError(` | 11 | **0** | `return (issue) => {…}` — a `$ZodErrorMap` | not a schema | | `INLINE_CREDENTIAL_REFUSED(` | 10 | **0** | a template string | not a schema | | `objectBlockHistory(` | 9 | 9 | a template string | not a schema | | `emptyProps(` | 9 | 9 | `strictObject(…)`, itself a declared form | WRITABLE | | `strictIdentOrNull(` | 8 | 8 | `z.string().regex(SNAKE_CASE).nullable()…` | WRITABLE | | `belongsInConfig(` | 8 | 8 | a template string | not a schema | Two readings follow, and both change what the repair is rather than decorating it: 1. **FOUR of the eight mint prose or an error map, not a schema** — 38 key lines that declare no author-writable key at all. A name-shaped heuristic would have fired on every one of them. 2. **The sibling board settles the heuristic question outright.** At objectui `15f01223d`, file-local to `packages/types/src/zod/complex.zod.ts`: `chatbotRequestBodyArm(` (2 key lines) returns `z.record(…)` ⇒ WRITABLE, `chatbotEnableMarkdownArm(` (2) and `chatbotEnableFileUploadArm(` (2) return `z.boolean()` ⇒ WRITABLE, while `chatbotOnClearArm(` (2) returns `handlerKeyRefusal(…)` ⇒ REFUSING and `retiredDeclarativeKanbanKey(` (1) returns `retirementTombstone(…)` ⇒ REFUSING. Four factories with the same `*Arm(` shape, in ONE file, in OPPOSITE registers — a name-shaped reading is wrong about one of them whichever way it guesses, and nothing a name carries says which. That is the measurement that retires the heuristic rather than declining it on principle. ## What this leaves open, and why it is a different instrument `placeholderFree` is declared in `packages/spec/src/data/driver/common.zod.ts` and **IMPORTED at all 23 of its key positions**, `memory.zod.ts:9` included. So the card's probe LINE is BOUNDARY ONE, not the class the card's title names: it exits 0 still — but no longer in silence, because the line is now reported by name with its file:line and the reason, which is why this PR does not leave the probe where it found it. The class the card's TITLE names is closed over the **29 file-local key lines that carry a schema** (`strictIdent(` 12, `strictIdentOrNull(` 8, `emptyProps(` 9), and the other 17 file-local key lines are now a NAMED silence instead of an unseen one. What remains is a shared, exported helper absent from `SCHEMA_PROPERTY_FORMS`, which is #18560's instrument — a row with a counterfactual fixture — and not this one. That is why the relation above is `Part of` and #18702 remains open; adding such a row was outside this card's dispatch, and the reasoning belongs to whoever grades it rather than to this diff. _Round 2 (seat's note): the seat ruled the row in (option C), the dev landed it in `d852dae1fb`, and the relation is now `Fixes` — see the opening line and the round-2 report on #18702._ ## Pins — PR #18700's shape, one fixture per factory, both arms A new self-test battery, `#18702 — a declaring factory PRIVATE to one file, resolved through its own DEFINITION`, 53 cases: - a frozen fixture roster asserted EQUAL to the eight factories the card names, so a factory added with no fixture reds and one silently dropped reds; - each fixture carries that factory's REAL definition from the tip (signature verbatim, return expression verbatim at its opener, prose truncated) and its quoted `returns` is asserted to be text the definition actually contains, so a fixture cannot drift into describing a definition it does not hold; - RESOLUTION asserted for every fixture against its own arm, then the VERDICT: a writable factory FIRES with its own file:line, an unclassifiable one fires nothing AND is reported by name; - the REFUSING arm read off a `z.never(…)` definition (`refusedInlineCredentialKey`) rather than off a name, with the chained-arm control that fires; - THE COUNTERFACTUAL: the card's probe LINE on the file where its factory is DECLARED — fires after, and silent with the resolver blind, which is the before-reading taken by disabling the resolver rather than by editing the fixture; - BOUNDARY ONE: the card's literal probe on `memory.zod.ts` — no tell, one named unresolved row, and the reader PRINTS it; - BOUNDARY TWO: two definitions of one name are AMBIGUOUS; a body with no readable `return` is unresolved; a `return` inside a nested callback is NOT read as the factory's own; - the head-blob readings: the API row's `sha`, the local `index` line, an all-zero id refused, a patch naming neither answering null (which is why every other fixture in this file never touches an object store), and a path climbing out of the tree never read; - the direction: the fast path is consulted first, a line that fires without the resolver still fires with it, the ADDED-side-only asymmetry, and the price of that asymmetry; - the controls: `Clause-②: yes` never blocked, the objectui board not judging an objectstack path, #17618's parameter decline untouched, a comment still not a key line, a file off the contract source surface reading no blob at all; - the two objectui `*Arm(` factories in opposite registers. ## Ablation One line reverted — the single call that consults the resolver, `const localForms = localDeclaringForms(file, lines, onContractSource, readSource, unresolved);` becoming `const localForms = null;` — from the committed state at `1b1c7a6c5c`, with the mutation proved on disk by blob hash before the run and the restore proved by blob hash and an empty `git diff HEAD` after it. ``` HEAD blob for scripts/pm/check-widening-tells.mjs: 24b8494 --- anchor occurrences AFTER: 0 (expect 0) --- injected occurrences: 1 (expect 1) --- mutated blob hash: 7d3aa266ed95e1382b3423b5ebe999b4701c56d0 (HEAD was 24b8494) ABLATED self-test exit: 1 ✗ check-widening-tells self-test: 15 of 456 case(s) failed. --- restored blob hash: 24b8494 RESTORED: byte-identical to HEAD, git diff HEAD empty RESTORED self-test exit: 0 — 456 cases pass ``` All 15 red cases are in the new battery, and every one of them is a case about the READING: the four writable factories firing, the four unclassifiable ones being reported by name, the chained-arm control, the counterfactual with its file:line and its refusal verdict, boundary one's stated silence and its printed line, and the price case. The other 38 cases in the battery are resolution-level and hold either way, and **the 403 cases standing before this round are green under the ablation, every one of them** — which is the direction claim, measured rather than argued. ## Self-test `pnpm check:pm-widening-tells` — **403 cases / 24 batteries at `30bac2880`, 456 cases / 25 batteries here.** `pnpm check:pm-clause2-carriers`, which consumes this file's verdict, is **786 cases, exit 0**. ## Derived gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` from the worktree (no hand-fed path list; the change set is the tool's own, one path, three-dot against the merge base `30bac2880`) derived **29 commands**. All 29 were run, each exit code captured by redirect-then-`$?`, and **all 29 exited 0**. Reconciliation with `--ran` recording `command :: exit N`: ``` ✓ dispatch-gates --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). ``` The 29: `check-ci-filter-parity`, `check-closing-keyword-parity` (scan + self-test), `check-comment-mask-corpus`, `check-declaration-mirrors` (scan + self-test), `check-scripts-symbol-anchors` (scan + self-test), `check-self-test-wired` (scan + self-test), `check-self-test-workflow-commands` (scan + self-test), `check-whole-set-label-write` (scan + self-test), `bare-root-worklist --self-test`, and `pnpm check:` `agent-test-spelling`, `bash32-floor`, `cli-command-ids`, `cross-package-test-inputs`, `driver-memory-census`, `entry-guard`, `nul-bytes`, `parse-guard`, `pm-dispatch-gates`, `pm-widening-tells`, `pnpm-filter-targets`, `ratchet-remedy-authority`, `refd-timer-probe`, `watch-hint-literal`. Repo-wide **`pnpm lint` exit 0** (`eslint . --no-inline-config`, the whole tree, run after the final commit). The control-byte self-scan over the edited file reports nothing, and `pnpm check:nul-bytes` exits 0. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent bc2ec80 commit e2050ce

1 file changed

Lines changed: 1018 additions & 30 deletions

File tree

0 commit comments

Comments
 (0)