Skip to content

Commit 6b22246

Browse files
fix(pm): read-back judge collapses the read path's own-repo reference expansion before calling a body mutated (#24804)
Fixes #24747 Clause-②: no One file, `scripts/pm/post-stamped.mjs`. The read-back judge (`classifyReadBack`) now undoes the read path's own-repository reference expansion before it calls a stored body `mutated`. This is not a governed surface (`scripts/pm/**`). There is no changeset: nothing under `scripts/pm/` ships in any package's `files[]`, and `skip-changeset` is sent with this create. Opened as a draft; making it ready and queueing it are the owning seat's steps. ## The defect In a cloud seat container, the proxy rewrites REST reads. A body read there intermittently comes back with a bare `#N` spelled `objectstack-ai/objectstack#N`. Card #24729 recorded this, and PR #24744 added it to `platform-readings.md` (配额段). Contract review 6283172819 (flag ③-2) escalated it. The stored bytes are unchanged, but the judge compares bytes exactly. So a body that was stored whole read back as `mutated`, which meant exit 4 on `post-stamped` and `not-stored` on the fleet-write relay row. That verdict's remedy then sends the seat to read the expanded body and send it back, which stores the expansion for real. ## What changed - `referenceExpansion(sent, stored)` (new, exported, pure) walks both bodies in step, starting from the SENT side. It allows the stored body to carry an `owner/repo` prefix before a `#` only where the sent body opens a bare reference. A bare reference is a `#` followed by a digit, at the start of the body or after a character that is neither an ASCII letter or digit nor `/`. The first prefix it meets fixes the prefix for the whole body. Every other character is compared literally, and the walk stops at the first other difference. It never does a replace over the stored bytes: a body may carry an author's own full `objectstack-ai/objectstack#N`, and a global replace would collapse that too, then disagree with a sent body that still spells it in full. - `OWN_REFERENCE_PREFIXES` (new, exported) is the fleet roster's slugs, imported from `GOVERNED_REPOS` rather than written out again. - `classifyReadBack({ sent, stored } = {})` keeps its signature, so the existing self-test pin on it is unchanged. The existing arms run first. When none matches, the judge undoes the expansion and asks the same arms again: - A match gives the new class `reference-expanded-on-read`, carrying `afterCollapse` (the class the collapsed body reads as), `referenceExpanded` (the count) and `referencePrefix`. - No match gives `mutated`. Its offset is taken after the collapse, in sent bytes, so it points at the real difference instead of the first expanded reference. The count is kept beside it. - `READ_BACK_CLASSES` gains the class. `sentBodyLanded` is untouched: it reads `class` other than `mutated`, so the new class gives `body_landed` true and `body_mutated` false. - `readBackVerdict` prints one clean line for the new class: the count, the prefix, the collapsed class and the length identity (bytes read − 26 × count = bytes collapsed). The identity is printed as a reading, never tested. When a `mutated` verdict carried a collapse, a fifth line says those references are not the difference. - `referenceExpansionNote` adds one stderr line naming the count. `main` prints it on both output shapes, text and `--json`, whatever the verdict. - `--json` `read_back` gains `after_collapse`, `reference_expanded` and `reference_prefix`. - When the read carried an expansion, `notStoredText` adds that a body read on this path must not be sent back as it reads. - In the header, the exit-register list gains an EXPANDED row and the file gains a new reference-expansion section. What is not widened: no footer arm changed, there is no tolerance band and no length test, and no new gate or label. A body that still differs after the collapse is `mutated`. ## Dispatch hypotheses, as measured - **Signature (hypothesis 1).** Kept, with no `repo` parameter. The relay's judge call in `fleet-write/dispatch.mjs` (`judgeStoredBody`) passes `{ sent, stored }` only, and that relay path is the one the expansion lives on. A judge that needed the repository would have left that path uncovered until `dispatch.mjs` changes. So the prefix is read off the stored body, limited to the roster, and must be one per body. The residual risk: a body whose every bare reference came back naming a sibling roster repository would pass. Nothing has been measured producing that, because the proxy spells the repository it read from. The exact cure is on the caller's side: `readBackStroke` already holds `payload.repo` and could hand it over. - **Matcher (hypothesis 2).** Built from the sent bytes, as the hypothesis described. An author's full spelling is matched literally and never collapsed. - **Class (hypothesis 4).** A class of its own, as the card's ruling text says, with the existing class kept as `afterCollapse`. A separate word matters because the relay row (`readBackLine`) prints the class. Reusing `identical` would describe bytes that were not read. With its own word, the reading shows on the relay row with no edit to `dispatch.mjs`. The combinations are pinned: footer appended (with and without the newline strip), re-anchored, blank collapsed (with the strip), and trailing newlines stripped. ## The locator half of the card The card says 「The same collapse applies to the locator's bytes-based lookup」. Measured on this branch: fleet-write's content-based comment lookup in `readBackStroke` picks a comment by `judgeStoredBody(...).verdict === 'landed'`, which is this file's `classifyReadBack`. So that lookup gets the collapse without any edit to `dispatch.mjs`. A probe of `judgeStoredBody` on an expanded body answered `landed reference-expanded-on-read` for both `issue_patch` and `comment`, and still `not-stored` for the one-byte control. `post-stamped`'s own relay lookup, `pickRelayComment`, is pinned the same way here. `dispatch.mjs` and `execute.mjs` belong to the sibling card #24755 and are untouched. As dispatched, the locator half of the card (any `dispatch.mjs`-side change) waits until #24755 lands and is taken up then; the items are listed under Acceptance notes. ## Verification (local, at `f0ecb34f62`) - `node scripts/pm/post-stamped.mjs --self-test`: exit 0, "724 cases pass across 25 batteries" (base `46bb5e3e54`: 679 across 24). The new battery has 45 cases and its floor is pinned at 45. - `pnpm check:pm-fleet-write-dispatch`: exit 0, "222 cases pass across 20 batteries". - Reverse mutations, each run once through `node scripts/ablation-replace.mjs` in WRAP mode. For every one the tool reported the mutation landed (anchor 1 → 0, blob changed), and afterwards that the restore held (blob equal to HEAD `dbdd62185e89`, `git diff HEAD` empty). Each self-test went red: - M1, no collapse (`const expansion = null;`): exit 1, 20 of 724 red, including the filed shape, the director-post reading, all four combinations and the relay lookup. - M2, global replace over the stored bytes: exit 1, 1 red, the author's full reference beside an expanded bare one. - M3, any `owner/repo` instead of the roster: exit 1, 2 red, including the prefix outside the roster. - M4, prefix not held once per body: exit 1, 1 red, two roster slugs in one body. - M5, bare-reference predicate removed: exit 1, 2 red, the prefix after a letter and after a `/`. - M6, offset taken before the collapse: exit 1, 2 red, the offset at the real difference and the digit after the prefix. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 34 commands from merge base `46bb5e3e5`. All 34 exited 0. `--ran` reconciled: "34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN". They are: - every `--self-test` and scan of `check-ci-filter-parity`, `check-closing-keyword-parity`, `check-comment-mask-corpus`, `check-declaration-mirrors`, `check-position-name-fold-loaders`, `check-scripts-symbol-anchors`, `check-self-test-wired`, `check-self-test-workflow-commands`, `check-whole-set-label-write` and `bare-root-worklist`; - the `check:` gates `agent-test-spelling`, `bash32-floor`, `cli-command-ids`, `cross-package-test-inputs`, `driver-memory-census`, `entry-guard`, `gitlink-declared`, `nul-bytes`, `parse-guard`, `pm-dispatch-gates` (fast tier, 1913 cases), `pm-post-stamped`, `pm-write-pace`, `pnpm-filter-targets`, `pnpm-flag-spellings`, `ratchet-remedy-authority`, `refd-timer-probe` and `watch-hint-literal`. - Run outside the derived set: `pnpm exec eslint --no-inline-config scripts/pm/post-stamped.mjs` exited 0. The repo-wide lint, the artifact-roster families and the type-check lanes are CI's and were not run here. - Upstream: `origin/main` is at `a43405d596`, one commit ahead of the base. That commit touches one line of `.claude/skills/pm-dispatch/references/filing-gate.md`, which nothing here reads, and `git merge-tree` is clean, so the branch was not merged. ## Acceptance notes - Comment 6279597228's figures do not add up under the collapse: 5,751 − 40 × 26 = 4,711, against a recorded raw size of 4,673. PR #24744 had already flagged this. On that reading the judge answers `mutated`, unless the raw size was counted in another unit; the raw body is needed to decide. Noted, not filed. Carrier: none. - Left in `dispatch.mjs`, the sibling card's file surface: - The relay row names the class but not the count, because `judgeStoredBody`'s landed result drops `referenceExpanded`. - That file's `notStoredText` remedy (「Go READ what is stored and send a body that can land」) carries no warning about undoing an expansion. - Passing `payload.repo` to the judge would make the prefix exact. Noted, not filed. Carrier: the `domain:skills` seat, to sequence after #24755 lands. ## 维护者速读(草稿) - 改了什么:写入工具的回读判定器在判「正文被改写」之前多问一步。云容器经代理读回的正文会把裸 `#N` 间歇展开成 `objectstack-ai/objectstack#N`;判定器从发出的字节出发,把这种展开折回,再用原有规则判一次。对得上的读数归入新类别 `reference-expanded-on-read`,算写入成功,退出码 0,并在 stderr 用一行报出展开个数。作者自己手写的全拼引用不会被误折。 - 为什么改:原判定器逐字节比对,这种展开会被判成「没存上」,退出码 4。随之给出的处置是去读已存正文再发,而那份读数正是被展开过的,照做就会把展开真的写进卡片。这是一个假红,还会把人引向一个真坑。 - 风险与代价(含回滚):只改一个脚本,不加门禁、不加标签,页脚规则一字未动;折回之后仍有任何一个字节不同,照旧判「没存上」。已知的剩余缺口:判定器不知道正文属于哪个仓库,只要求前缀是舰队名册里的同一个仓库。如果读回把引用整体换成名册里的另一个兄弟仓库,也会被放行。这种情形从未实测出现过,彻底收口要等兄弟卡改的调用方传入仓库名。回滚即还原本 PR。 - 席位意见: - 你要做的:无需操作;席位复核 ACCEPT 后翻 ready 入队。 --- _Generated by [Claude Code](https://claude.ai/code/session_01HEdP5tPhqQfzwRmKDxwn85)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 25e16ab commit 6b22246

1 file changed

Lines changed: 343 additions & 6 deletions

File tree

0 commit comments

Comments
 (0)