Skip to content

fix(pm): check-widening-tells resolves a FILE-LOCAL declaring factory through its own definition - #18750

Merged
os-justin merged 4 commits into
mainfrom
claude/issue-18702-file-local-declaring-factories
Sep 17, 2026
Merged

os-justin merged 4 commits into
mainfrom
claude/issue-18702-file-local-declaring-factories

Conversation

@os-justin

@os-justin os-justin commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

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

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, [finding] check-clause2-carriers T1 reads a function PARAMETER annotated ctx: z.RefinementCtx as a new authorable key — so every PR that adds an object-level refusal raises a widening tell for the refusal itself #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: 24b8494cf3c4aa2058436de293ad6976f96e134a
--- anchor occurrences AFTER:  0  (expect 0)
--- injected occurrences:      1  (expect 1)
--- mutated blob hash: 7d3aa266ed95e1382b3423b5ebe999b4701c56d0 (HEAD was 24b8494cf3c4aa2058436de293ad6976f96e134a)
ABLATED self-test exit: 1
✗ check-widening-tells self-test: 15 of 456 case(s) failed.
--- restored blob hash: 24b8494cf3c4aa2058436de293ad6976f96e134a
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

…ition

T1 read a property's value with a NAMED list of shared, exported helpers
(`SCHEMA_PROPERTY_FORMS`, #18560) and stayed blind to a factory declared
inside the one file that uses it: `memberTellKind` answered `null`, the row
neither fired nor spent the #16943 budget, and nothing in the output said a
thing.

The repair is structural, never name-shaped: when a key line's value opens
with an identifier call the shared list has no row for, the factory's own
definition is resolved out of the judged file's HEAD BLOB (`index <old>..<new>`
on the local path, `sha` on an API row; `git cat-file blob`, falling back to
the working tree only after `git hash-object` proves it IS that blob) and
classified by what its body returns at the body's own top level.

Two boundaries, both a STATED silence the reader now prints with the
file:line, the factory and the reason: an imported factory (imports are not
chased) and a body this reader cannot classify.

Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Co-authored-by: Claude <noreply@anthropic.com>
… heuristic

Four `*Arm(` factories in ONE objectui file land in opposite registers:
`chatbotRequestBodyArm(` returns `z.record(…)` and `chatbotOnClearArm(`
returns `handlerKeyRefusal(…)`. A name-shaped reading is wrong about one of
them whichever way it guesses, and nothing in the name says which.

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
…bulary row

Measured on the tip: `placeholderFree` is declared in
packages/spec/src/data/driver/common.zod.ts and IMPORTED at all 23 of its key
positions across six driver files. It was never file-local at any of them, so
no reading of one file could reach it and the structural resolver is not the
instrument for it. It takes a `SCHEMA_PROPERTY_FORMS` row, with the filing
probe line as its counterfactual fixture.

The row stops there: the four sibling factories measured beside it return
prose or a `$ZodErrorMap`, never a schema, so rows for them would mint 38
false T1 positives. Which instrument a factory belongs to is a fact about
where it is DECLARED; whether it belongs to either is a fact about what it
RETURNS.

The #18702 battery's counterfactual and imported-boundary fixtures re-anchor
onto factories the list has no row for, because a counterfactual that passes
through the fast path pins nothing about the resolver.

Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl and removed size/l labels Sep 17, 2026
@os-justin
os-justin marked this pull request as ready for review September 17, 2026 19:12
@os-justin
os-justin added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit e2050ce Sep 17, 2026
40 checks passed
@os-justin
os-justin deleted the claude/issue-18702-file-local-declaring-factories branch September 17, 2026 19:36
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…status endpoint as an empty-set default (objectstack-ai#18775)

Fixes objectstack-ai#18733

`Clause-②: no`

One file: `.claude/skills/pm-dispatch/references/platform-readings.md`,
held at **466 / 466**
(ceiling 466, headroom 0; widest line in the file still 120 B at `:464`,
untouched). No ceiling
raised, net line change **0**: one row added, one row retired in the
same file.
`skip-changeset` — `.claude/**` is shipped by no package's `files[]`, so
nothing published moves.

## The trap

`:28` is the row that sends a reader to `GET /commits/{sha}/status`. On
a commit that carries **no
legacy commit statuses at all**, that endpoint answers `state:
"pending"` with `total_count: 0` —
the API's default for an empty set, not a verdict about anything. In a
repo whose gates are all
check-runs the field is `pending` forever and means nothing, while
reading exactly like "gates still
running": a seat that waits on it waits forever, and a seat that reads
`pending` as not-yet-red can
read a red PR as merely unfinished. The gate reading is
`/commits/{sha}/check-runs`.

## Measurements

**Three `objectstack-ai/cloud` heads — the card's table, verbatim** (all
fully green):

| PR | head | check-runs | combined status |
|:--|:--|:--|:--|
| `objectstack-ai/cloud#2319` | `03c170ec` | 2/2 `completed/success`
(2026-09-16T16:13:44Z) | `state: "pending"`, `total_count: 0` |
| `objectstack-ai/cloud#2320` | `b4b1c5c4` | 2/2 `completed/success`
(2026-09-17T15:47:03Z) | `state: "pending"`, `total_count: 0` |
| `objectstack-ai/cloud#2321` | `23e928b7` | 2/2 `completed/success`
(2026-09-17T17:14:33Z) | `state: "pending"`, `total_count: 0` |

**This repo, fetched through the REST proxy at
2026-09-17T19:57Z–19:59Z** — measured here, ⛔ not
retyped from the card. Raw `state` / `total_count` on both sides:

| commit | `/commits/{sha}/status` | its `statuses[]` |
`/commits/{sha}/check-runs` |
|:--|:--|:--|:--|
| `d852dae1` (PR objectstack-ai#18750 head) | `state: "success"`, `total_count: 1` |
`Vercel` / `success` | `total_count: 40`, 29 `completed/success` + 11
`completed/skipped` |
| `702b4241` (PR objectstack-ai#18756 head) | `state: "success"`, `total_count: 1` |
`Vercel` / `success` | `total_count: 39`, 28 `completed/success` + 11
`completed/skipped` |
| `2f38ee0d` (PR objectstack-ai#18758 head) | `state: "success"`, `total_count: 1` |
`Vercel` / `success` | `total_count: 32`, 24 `completed/success` + 8
`completed/skipped` |
| `95e17452` (`main` tip at branch point) | `state: "success"`,
`total_count: 1` | `Vercel` / `success` | `total_count: 59` |
| `e2050cef` (`main`) | `state: "success"`, `total_count: 1` | `Vercel`
/ `success` | `total_count: 71` |
| `bc2ec806` (`main`) | `state: "success"`, `total_count: 1` | `Vercel`
/ `success` | `total_count: 64` |

**The reading that scopes the row**: `objectstack-ai/objectstack` posts
a legacy `Vercel` status on
every commit, so the empty-set default never arises here. One further
control makes that exact,
taken on this branch's own first commit `55c8e844` at 19:59:48Z, seconds
after the push and before
any workflow had started:

| commit | `/commits/{sha}/status` | its `statuses[]` |
`/commits/{sha}/check-runs` |
|:--|:--|:--|:--|
| `55c8e844` (this branch, pre-CI) | `state: "pending"`, `total_count:
1` | `Vercel` / `pending` | `total_count: 0` |

This repo answers `pending` too — for the opposite reason. There,
`total_count: 1` and a real
`Vercel` status genuinely in flight; in the card's three cloud heads,
`total_count: 0` and no status
at all. **`total_count` is the discriminant, `state` is not**, which is
why the row names both
fields rather than the word `pending` alone.

⇒ the row is scoped 「零 legacy status 的仓」, ⛔ not 「所有仓」.

## The row added — `:29`, 118 B

```text
- 零 legacy status 的仓恒答空集默认值 `pending`+`total_count: 0`,⛔ 非门禁读数,门禁读 check-runs。
```

It sits directly under `:28` 「`unstable` 可源自 check-runs 看不见的 commit
STATUS(如 `Vercel`)⇒
③ 另读 `/commits/{sha}/status`」, which names this repo's legacy status and
points at the endpoint.
The new row inherits that subject — the idiom the block already uses at
`:30`, `:63`, `:65`, `:67` —
and states the condition under which the endpoint stops answering the
question.

Against the card's proposed line, at the file's ≤ 120 B cap: 「那是空 legacy
集合的默认值」 is kept as
「空集默认值」 (「legacy」 already stands in the row's first clause), 「⛔ 不是门禁读数」
as
「⛔ 非门禁读数」, and 「门禁只认 `/commits/{sha}/check-runs` 的 `completed/success`」
as
「门禁读 check-runs」. What changed on purpose is the scope: the card wrote
「在只有 check-runs 的
commit 上」, and the measurement above makes it a property of the
**repo**, which is where the legacy
status is configured.

## The payment — `:343` retired, 85 B

**before** (`:343`, pre-edit numbering):

```text
- 署名页脚的写侧变异按通道与输入双重定域,⛔ 不是一条定律。
```

Why the tree no longer needs it as a separate line: it is a **preamble
that states the block's own
conclusion**, which the same block states again at its end, more
precisely. It carried two clauses,
and both are held:

| the retired clause | where it is held (post-edit numbering) |
|:--|:--|
| 「按通道与输入双重定域」 | `:355` 「⇒ 形态随动作与送出体尾部变,改侧还随通道变」 — three loci named
where the retired row named two, and `:342` 「⇒ 失效既依拼写又依载体:评论里验过页脚对 PR
正文什么都没证明」 states the same localisation at the point of use |
| 「⛔ 不是一条定律」 | `:355` 「⛔ 不由任一条推其余,写后必回读」, and `:351` 「⛔
无受控对照(同通道只差该块两送)⇒ 是拟合不是定论,⛔ 不外推到别的动作」 |

No fact leaves the corpus: every reading the block holds (`:340`–`:343`,
`:344`–`:354`) is untouched,
and the row that generalises them — the one a reader reaches **after**
the evidence rather than
before it — keeps the generalisation with the read-back prescription
attached. The retired row is not
one of the nine PR objectstack-ai#18689 adopted, not one of the three `配额` rows objectstack-ai#18744
names (`:137`, `:145`,
`:146`), and not one of the four PR objectstack-ai#18741 landed today (`:47`–`:49`,
`:163`).

The ratchet's standing one-file exception 「唯一例外:`platform-readings.md`
增量抬上限到落地行数,免
决策卡,记 `ruledRaises` 引常设裁决」 (`scripts/pm/check-skill-line-ratchet.mjs`)
was **not** taken:
a payment with zero fact loss was available, so the ceiling stays at 466
and no ruling is spent.

## ② re-read — CARRIED, no row, and a premise correction to the card

The card's ② says: "The existing entry says `/search/*` is refused by
the egress proxy. What it does
not say is the **shape** of the refusal." On `main` that sentence is
false. Three consecutive rows
say exactly the shape (pre-edit numbering, the seat's `:209`–`:211`):

```text
- 会话代理只服务 repo-scoped 路径,`/search/*` 的 403 体解析成净零。
- 代理回 403 加体 sessions are bound to their configured repositories,而那是合法 JSON。
- 读 `total_count` 得 None、打印成 total: None,与真空集只差一个字符,而请求根本没跑。
```

Mapped against the card's proposed addition, clause by clause:
「它的拒绝是成功形状的」 is
`:209`'s 「403 体解析成净零」; 「回 JSON」 is `:210`'s 「而那是合法 JSON」; 「`total_count`
键缺失 ⇒
`.get()` 读成 `None`、打印出来像 0」 is `:211`, which adds the measurement the
card does not
have — 「与真空集只差一个字符,而请求根本没跑」.

The only residual is the card's **prescription** 「断言键在,⛔ 不断言值」, and that
is precisely what
the open PR objectstack-ai#18666 (for objectstack-ai#18454, governed, awaiting the maintainer) lands
on that same line. Its hunk
on this file is one line, `@@ -209,7 +209,7 @@`:

```text
-- 读 `total_count` 得 None、打印成 total: None,与真空集只差一个字符,而请求根本没跑。
+- 读 `total_count` 得 None、打印成 total: None,与空集只差一字符;缺键即拒绝,判别式是状态码。
```

「缺键即拒绝」 is the assert-the-key prescription in the file's own voice, and
「判别式是状态码」 is
one notch sharper than the card asked for: it names the discriminant
rather than the symptom. So the
residual is zero once objectstack-ai#18666 lands, and the line a residual row would
have to touch is objectstack-ai#18666's own
hunk — a reserved line. ⇒ **no row for ②**, and the `search_issues`
block `:192`–`:201` is untouched.

The card's own attribution note observes that the `domain:spec @
objectui` seat recorded the same
shape independently and argues that is the reason to put it in the
shared table. It is in the shared
table; what the card measured is that a reader did not find it there.

## Reserved rows — verified against the open PRs' hunks, by content

- `:10`–`:12` (objectstack-ai#18469 PR-A) — byte-identical before and after; the
diff's two hunks are at `:26`–`:32`
  and `:341`–`:347`, so these lines are in neither.
- `:208`–`:212` (PR objectstack-ai#18666) — the only open PR touching this file,
confirmed by reading all 26 open
PRs' file lists at 2026-09-17T19:50Z; its hunk's changed line is the
`total_count` row. **The band
moves by the insertion**: on `main` the changed line is `:211` and the
hunk's full context window
is `:208`–`:214`; on this head they are `:212` and `:209`–`:215`. Every
byte in the band is
  unchanged, so objectstack-ai#18666 still applies.
- `:431` (objectstack-ai#18469 PR-A) — **this number was already stale before this
diff**. The row the card names,
「harness 按内容拒写:同会话派发 PR 上 PASS 拒为 `[Self-Approval]`」, reads `:432` on
`main`, and
read `:432` at `7636cd9b81^` too, so PR objectstack-ai#18741's body carried the
off-by-one rather than the row
having moved. `:431` is a different row (「分支删除被拒有第二形态:代理回 403」). After
this diff the
`[Self-Approval]` row is still at `:432`: the insertion at `:29` and the
retirement at `:344`
  cancel across it.

## Ratchet before / after

`node scripts/pm/check-skill-line-ratchet.mjs`, both pins, exit 0 on
both sides:

```text
main  447e2e8  ✓ .claude/skills/pm-dispatch/references/platform-readings.md: widest table row is 0 bytes (pin 0; headroom 0).
main  447e2e8  ✓ .claude/skills/pm-dispatch/references/platform-readings.md is 466 lines (ceiling 466; headroom 0).
head  d6bc477  ✓ .claude/skills/pm-dispatch/references/platform-readings.md: widest table row is 0 bytes (pin 0; headroom 0).
head  d6bc477  ✓ .claude/skills/pm-dispatch/references/platform-readings.md is 466 lines (ceiling 466; headroom 0).
```

Widest line in the file, measured per row in bytes: **120 B at `:464` on
both sides**, unchanged; no
row exceeds 120 B on either side; the added row is 118 B.

```text
 .claude/skills/pm-dispatch/references/platform-readings.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
```

## Derived gates

`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` from the worktree,
no hand-fed path list — 18 families, all re-run at the final head
`d6bc4775e5` with `$?` captured
before any pipe:

```text
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/pm/check-governed-queue-guard.mjs --self-test :: exit 0
node scripts/pm/check-harness-current.mjs --self-test :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:pm-governed-merges :: exit 0
pnpm check:pm-half-states :: exit 0
pnpm check:pm-skill-id-lint :: exit 0
pnpm check:pm-skill-ratchet :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:required-contexts :: exit 0
pnpm check:skill-frame-sync :: exit 0
pnpm check:watch-hint-literal :: exit 0
```

Reconciled: `dispatch-gates.mjs --ran` reads 「18 derived famil(ies)
accounted for — 18 run, 0
NOT-MEASURED (a DERIVED zero — all 18 recorded an exit code and none of
them is 3)」.

On the first pass `check:doc-formula-expressions` exited **3 —
PREREQUISITE NOT MET**, its own text
「Nothing was measured: this gate exited before running a single check」;
its two declared
prerequisites were built under `scripts/pm/os-verify-lock.sh` (`VERDICT
command-exit 0`, held 158 s,
waited 0 s) and it then exited 0, which is the reading recorded above.

Also run, outside the derived 18: `pnpm check:pm-settings-deny-roster`
exit 0 — the derivation marks
it 「roster under .claude, which one of your paths is in」, where silence
is evidence in neither
direction. Repo-wide `pnpm lint` (`eslint . --no-inline-config`): **exit
0** at `d6bc4775e5`.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ading context (objectstack-ai#18804)

Fixes objectstack-ai#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 `objectstack-ai#16489` signature — as "a new key on a Zod object
schema", although objectstack-ai#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 72dd95f..09e16a5 -- 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:

```js
if (ch === ')' || ch === ']' || ch === '}') {
  // objectstack-ai#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. objectstack-ai#18560 / PR
objectstack-ai#18700 and objectstack-ai#18702 / PR objectstack-ai#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 `objectstack-ai#18721 - a hunk's LEADING CONTEXT is not a reason to
abandon the parameter reading`, 14 cases, registered in the roster at
14:

- THE FINDING: PR objectstack-ai#18720's own hunk, reduced only as far as the failing
branch requires (the leading context that closes the previous
declaration, the function head, the object-literal-typed first
parameter, the `ctx` line) at the line the card reported — T1 silent,
and the whole verdict CLEAN.
- the line number is asserted from the fixture itself, so the fixture is
the probe and not merely a shape like it.
- the object-literal type on the first parameter is pinned as NOT the
confusing element.
- TRUE-POSITIVE CONTROL on the same file: `+ brandNewAuthorableKey:
z.string().optional(),` FIRES, at
`packages/spec/src/ui/dashboard.zod.ts:701`.
- a new key behind the SAME underflowing context still tells (no opener
shown, so no positive evidence).
- a real key added AFTER the parameter list closes still tells,
underflowing context and all, and the row reported is the shape member.
- the branch itself: an opener shown after an underflow is the answer;
an underflow with no opener after it is still `null`; the drop does not
leak past the parameter list's own close; no reading crosses a hunk
boundary.
- the OLD side: objectstack-ai#17618 reads the same decline on the removed side, so a
removed parameter behind leading context now buys no objectstack-ai#16943 budget — and
a genuine key added in the same block, which that phantom budget used to
pay for, FIRES. One repair, one false positive closed and one false
negative with it.

objectstack-ai#17618's existing pin in the `objectstack-ai#18560` battery (`objectstack-ai#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      : 025f8e5
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  : 025f8e5  (== HEAD blob)
git diff HEAD  : 0 line(s)
```

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

```
  THE FINDING - PR objectstack-ai#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](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants