Repository navigation
fix(pm): teach check-widening-tells the declaring helpers, as a named list with a counterfactual pin - #18700
Merged
os-justin merged 3 commits intoSep 17, 2026
Conversation
Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu Co-authored-by: Claude <noreply@anthropic.com>
… 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>
This was referenced Sep 17, 2026
os-justin
marked this pull request as ready for review
September 17, 2026 16:02
This was referenced Sep 17, 2026
os-justin
deleted the
claude/issue-18560-widening-tells-declaring-helpers
branch
September 17, 2026 16:25
This was referenced Sep 17, 2026
This was referenced Sep 17, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #18560
Clause-②: no
scripts/pm/check-widening-tells.mjsis 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:memberTellKindanswersnull, 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 correctno.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 at6dfa3ea77(this branch's merge base), the AFTER leg at7f1a81419:nostripImportedDefaults(nopackages/types/src/zod/data-display.zod.ts:584yesyesis 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)returnsz.never({ error: guidance }).optional().describe(guidance)— the same primitive as this repo'sretiredKey();handlerKeyRefusal(key, disposition, label)returns az.custompredicate typedneverthat returnsfalse, 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
yesthis 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 constantSCHEMA_PROPERTYis BUILT from it. Two fields carry two questions:pattern(what makes the line a KEY LINE) andwritable(whether the key it declares is one an author may write). Counts are key-POSITION counts, each with the tree it was taken against.z.6dfa3ea77· 1,482 at objectui15f01223dlazySchema(6dfa3ea77(live at DECLARATION positions)strictObject(6dfa3ea77*Schema6dfa3ea77· 56 at objectui15f01223dstripImportedDefaults(15f01223d(added)retiredKey(6dfa3ea77retirementTombstone(15f01223d(added)handlerKeyRefusal(15f01223d(added)aliasKeyRefusal(15f01223d(added)The
writable: falsearm 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.declaresRetiredKeyTombstoneis renameddeclaresUnwritableKeyaccordingly; it is not imported by any other file.The
nocriterion 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:memberTellKindanswers T1) — writable or not, because an unrecognised line is invisible to both sides of the budget, which is the defect itself;tellsInFileand asserted against its OWN register: awritableform must FIRE with its file:line, an unwritable one must be recognised and DECLINE;stripImportedDefaults(widening with its refusal, its file:line, theyescontrol and the default-board control.check:entry-guardis the reason the vocabulary's structural guard is a const initializer'smapstep rather than a top-levelfor: 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-②: nolandings through these forms, over 11 of 18 rows read.git rev-parse --is-shallow-repository= false, 10,282 commits, initial commit 2026-01-13) up to the checkout tip15f01223d(2026-09-16). PR objectui#9647 itself is OUTSIDE it:git merge-base --is-ancestor 604476d HEADexits 1, with the initial commit as the control leg at exit 0 on the same non-shallow checkout. The judgeable part of that window starts 2026-09-10, when fix(pm): split check-widening-tells' file count and let its CLI be told the board #17278 first let this CLI be told which board it judges.packages/types/src/zod/**; 18 land inside the judgeable window.Clause-②: yesin the PR body (objectui#8884, ObjectQL.cascadeDeleteRelations fails OPEN: a failed dependents probe skips therestrictguard entirely, so a delete that should be refused succeeds silently #8895, fix(engine-core): a system-context insert resolves the install's organization, or is refused — the runtime producer of the autonumber fork (#8844) #8930, gate: nothing reconciles the documented HTTP status of an error code against the status the runtime actually emits — measured on MISSING_REQUIRED_FIELD (400 documented, 422 served) #8967, docs: drop stale renderer-status notes now that type:'form' is shipped #9051, QA run · integration-system (6/14 items consulted) · e4e5c6e3 · 2026-08-17 · 2 PASS / 4 PARTIAL / 0 FAIL / 8 NOT-RUN #9338, governance: human merge IS the review record for governed surfaces — retire the per-PR ADR approval check, add a report-only post-merge audit #9495,record_viewslists anip_addresscolumn that no read-path writer ever stamps — a declared-but-unwritten column on a shipped compliance view #9539, docs(plugin-audit): the published README documents the record-view audit surface that shipped #9541, fix(driver-sql): bound the metadata-lock wait on boot schema-sync's MySQL widening ALTER too, keeping boot's swallow (#9542) #9565); 1 (objectui#9443) carries a "Clause-② carriers" section attachingneeds:contract-reviewwith noyes/notoken.optionsfield description overstates the refusal: says "credential material is refused", true only forauth.password#9254, probeInstallOrganizations answers a failed sys_organization read with a memoised empty list, so system-insert organization resolution fails open #9261, publishPackageDrafts still discards the runtime authoring gate's per-draft advisories — Studio's "publish whole app" reports none of them #9343, Four more published READMEs document symbols that do not exist — driver-sql, mcp (×2), objectql, and spec's own front page #9544, Restore plugin-audit's README "See Also" link toruntime-services/audit-service, dropped in PR #9531 because the page was wrong #9589, A negative-lookbehind literal cannot be carried in an issue/PR body — the!is deleted from(?+ less-than +!+[, and AGENTS.md's sanitizer clause names only less-than plus a letter #9621 carry no declaration in the PR body; the remaining carrier is each card's claim comment, which was not read. This is NOT a zero — it is seven rows unread and named.Clause-②line in a changeset, which is why the census reads PR bodies rather than the tree.The ablation
From the committed fix,
HEADblob1b741ee6c22db6523475b698d392a8ecd41686d6, under atrap '…' EXIT INT TERMrestoringgit checkout HEAD -- scripts/pm/check-widening-tells.mjsat an absolute path. No build ordist/is involved — the gate runs from source, so there is no rebuild leg to prove.retirementTombstone(andhandlerKeyRefusal(rows deleted fromSCHEMA_PROPERTY_FORMS;9fb65edf6f68b1501a0aec29d0ddb4c95bcf44a2which is not the HEAD blob;--self-testexits 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;git hash-objectback to1b741ee6c22db6523475b698d392a8ecd41686d6,git diff HEADempty,git status --porcelainclean, 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-carriersexit 0,check:pm-prior-rulingsexit 0.Derived gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackfrom 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)".Repo-wide
pnpm lint(eslint . --no-inline-config) at7f1a81419: exit 0, no findings. Control-byte scan over the edited file: no hits.skip-changeset:scripts/pm/**is not published by any package'sfiles[], 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 objectstack6dfa3ea77;chatbotRequestBodyArm((2),retiredDeclarativeKanbanKey((1) at objectui15f01223d— 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