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-tells — 381 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
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ne, and the arming echo is not the landing method (objectstack-ai#18713)

Fixes objectstack-ai#18461

`Clause-②: no`

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

Reserved rows are untouched and verified by content: `:10`–`:12` and
`:431` (objectstack-ai#18469 PR-A's pending patch),
`:209` (PR objectstack-ai#18666's one-line change, awaiting the maintainer — it rides
at `:208` after the payment, byte
identical), and the nine rows PR objectstack-ai#18689 adopted an hour before this
branch (`:28`, `:85`, `:249`, `:277`–`:278`,
`:291`, `:347`, `:357`, `:393`, `:410` — none appears in any hunk of
this diff).

## Row ② — rewritten in place at `:50`

Ruling reference: the maintainer's letter **A** on objectstack-ai#18421, comment
5716260764 (2026-09-17T14:41Z, 「同意」).
Its operative conclusion is the one this row carries:
**`auto_merge.merge_method` is not a reading of what will
land** — the landing shape is the merge queue's to decide.

**before** (120 B, the falsified form):

```text
- 挂上的 auto-merge 存的方法恒为 `merge`,不论请求了什么;REST `auto_merge.merge_method` 读回 `merge`。
```

**after** (117 B):

```text
- auto-merge 回读 `merge_method` 不恒定:同 `SQUASH` 载荷 `merge`/`squash` 皆现,⛔ 非落地方法判据。
```

**Why 「不恒定」 and not either constant.** 「恒为 `merge`」 is falsified by this
card's four PRs; 「恒为
`squash`」 is falsified by the same table's first row and by the ruling's
own measurement. Same endpoint
(`PUT .../pulls/{n}/ccr/auto_merge`), same `{"merge_method":"SQUASH"}`
payload:

| PR | echoed `merge_method` | landed as |
|---|---|---|
| objectstack-ai#18395 | `merge` | single-parent squash |
| objectstack-ai#18392 | `squash` | single-parent squash |
| objectstack-ai#18416 | `squash` | single-parent squash |
| objectstack-ai#18445 | `squash` | single-parent squash |

The objectstack-ai#18421 ruling measured the same endpoint answering `merge` (both
arming channels, MCP and CCR), and the
dispatching seat's own arms today echoed `squash` on PR objectstack-ai#18689 / objectstack-ai#18690
/ objectstack-ai#18700 and `merge` on earlier ones.
Four landings, two echoes, one landing shape ⇒ the echo varies and
decides nothing. A row asserting either
constant would teach a seat to investigate a read-back that is simply
not a signal.

**Neighbours left as they are.** `:49` is about the bare `PATCH` draft
bit — unrelated. `:51`
(「仓库 `allow_merge_commit:false` 时同样读回 `merge`」) does not restate 「恒」: it
is one conditioned
measurement, and 「同样」 now attaches to `merge`, which the rewritten row
names as one of the two observed
values. `:53`–`:55` already carry the landing half (the branch rule
decides; three carriers agreeing proves
nothing) and are untouched — the row above them no longer contradicts
them.

## Row ① — added at `:272`

**after** (118 B):

```text
- 多标签页还静默截断:`totalCount` 231 而 `returned` 30,单标签同车道 24/24 ⇒ 求交读成空车道。
```

**Placement — a declared deviation from the dispatch.** The dispatch
named the `:247`–`:248` neighbourhood
(the MCP `list_issues` rows). The OR half of this reading is already in
the file, in the 读数陷阱 cluster at
`:269`–`:274`: `:269` 「`labels` 数组是并集」, `:270` 「⛔ 永不读作交集」, `:272`
「失效全静默」, `:273`
「正确读法 = 整车道单标签一次读全加本地对 labels 求交」. Writing the new row at `:247` would
have put a second
home for the labels-OR fact six rows away from the first — the same
duplication the dispatch forbids for row ②
(one row for one fact, never two). So the row is placed where the fact
it compounds already lives: directly
between the silent-failure row `:272` and the prescription `:273`, which
it is the reason for. The fact
delivered is unchanged; only the address is.

**What the row adds over `:269`–`:274`.** Those rows carry the OR
semantics and the prescription. They do not
carry the **truncation**, and the two compound: the returned page is a
fraction of the set with nothing in the
response saying which fraction, so a local intersection taken over *that
page* can return zero and read as
「本车道无活」. The measurement is inside the row:
`labels=["domain:cli","pm:queue"]` answered `totalCount=231`
with `returned=30`, while the single-label query on the same lane in the
same minute answered 24 / 24 —
complete, and intersectable. That is what makes `:273` load-bearing
rather than a style preference.

## The payment — one dedup, `:202`+`:203` become one row

| payment | before | after | fact lost |
|---|---|---|---|
| `:202`+`:203` | 「换道:探针绿走 REST 列表端点 `GET
/repos/{o}/{r}/issues?state=open&labels=a,b&per_page=N`。」 + 「它走 core 桶且
`labels` 是真 AND;⛔ 完整性自证靠 `&page=N` 加总数核对。」 | 「换道:探针绿走 REST 列表端点列卡,走 core
桶且 `labels` 真 AND;拼写与自证见 `rest-channel.md`。」 (118 B) | the inline
endpoint spelling and the `&page=N`-plus-total spelling — both held
verbatim in `rest-channel.md` 〈读侧〉, which `:137` already names as their
single home (「逐操作通道归属、写侧配方与队列路由三读法见 `rest-channel.md`,⛔ 不在本表复述」). The
two facts the row keeps inline are the ones a seat needs before it
switches channel; `labels` being a true AND on REST is also stated
in-file at `:274`, and the core bucket at `:115` / `:128`. |

Net: 3 lines changed, 466 in, 466 out.

## Gate readings

`node scripts/pm/check-governed-merges.mjs --test
.claude/skills/pm-dispatch/references/platform-readings.md`
exits **3 = GOVERNED** (`.claude/**` ×1) — recorded as the verdict it
is. This PR stays draft; no seat flips it
ready, enqueues it or arms auto-merge. The references tier applies: the
skills seat's `## Contract review`
record goes on this thread, and the seat lands it.

- `pnpm check:pm-skill-ratchet` :: exit 0 — 「platform-readings.md is 466
lines (ceiling 466; headroom 0)」,
「widest table row is 0 bytes (pin 0; headroom 0)」; no line over 120 B
(`awk 'length > 120'` returns nothing).
- `pnpm check:skill-frame-sync` :: exit 0 — 「the one declared copy of
the decision frame is internally
  coherent … 4 axes … 74 markdown files scanned for undeclared copies」.
- `pnpm lint` (repo-wide, `eslint . --no-inline-config`) :: exit 0.
- `grep -naP` for control characters over the file: no hits. No
tag-shaped fragment in either new row.

### Derived gate list, with exit codes

Derived from the worktree with `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack`
(no hand-fed paths; change set 1 path vs merge base `72dd95fa5`), every
command run, each exit code captured by
redirect-then-`$?`:

```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
pnpm check:pm-settings-deny-roster                                  :: exit 0   (outside the derivation; run
                                                                                 because its roster lives under
                                                                                 .claude, which this path is in)
```

`check:doc-formula-expressions` first exited **3 — PREREQUISITE NOT
MET** (`@objectstack/formula` and
`@objectstack/lint` not built, 「Nothing was measured」). It was re-run to
a real verdict after
`pnpm exec turbo run build --filter=@objectstack/formula
--filter=@objectstack/lint` through
`scripts/pm/os-verify-lock.sh` (VERDICT command-exit 0, held 1s, waited
0s); only the exit 0 is recorded above.

Reconciliation: `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --ran ran.txt` →
「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)」. Outside that union: the 53 artifact-roster
families, 11 wide-population families,
14 changeset-pending families and the CI-only families
(`--verify-required-set`, `check-half-states
--provenance`) — CI's, not this branch's, and named here so their
absence is not read as a clearance.

## Out of scope, reported not written

The card's third item — `scripts/pm/check-expected-skips.mjs` cannot run
in a seat container
(`ERR_MODULE_NOT_FOUND: Cannot find package 'yaml'`) — is not a
references line and is not written here. In this
worktree, after `pnpm install`, `yaml@2.9.0` is present; the crash is a
property of the container the seat runs
it from, not of the script.

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

---
_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
… through its own definition (objectstack-ai#18750)

Fixes objectstack-ai#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 objectstack-ai#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 objectstack-ai#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."** objectstack-ai#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 objectstack-ai#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 objectstack-ai#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 objectstack-ai#18560's instrument
— a row with a counterfactual fixture — and not this one. That is why
the relation above is `Part of` and objectstack-ai#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 objectstack-ai#18702._

## Pins — PR objectstack-ai#18700's shape, one fixture per factory, both arms

A new self-test battery, `objectstack-ai#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, objectstack-ai#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>
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/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants