Repository navigation
Commit 6b22246
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
0 commit comments