Commit e2050ce
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 parent bc2ec80 commit e2050ce
1 file changed
Lines changed: 1018 additions & 30 deletions
0 commit comments