Skip to content

fix(pm): read-back judge collapses the read path's own-repo reference expansion before calling a body mutated - #24804

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-24747-readback-judge-reference-expansion
Oct 20, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-24747-readback-judge-reference-expansion

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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

维护者速读(草稿)

  • 改了什么:写入工具的回读判定器在判「正文被改写」之前多问一步。云容器经代理读回的正文会把裸 #N 间歇展开成 objectstack-ai/objectstack#N;判定器从发出的字节出发,把这种展开折回,再用原有规则判一次。对得上的读数归入新类别 reference-expanded-on-read,算写入成功,退出码 0,并在 stderr 用一行报出展开个数。作者自己手写的全拼引用不会被误折。
  • 为什么改:原判定器逐字节比对,这种展开会被判成「没存上」,退出码 4。随之给出的处置是去读已存正文再发,而那份读数正是被展开过的,照做就会把展开真的写进卡片。这是一个假红,还会把人引向一个真坑。
  • 风险与代价(含回滚):只改一个脚本,不加门禁、不加标签,页脚规则一字未动;折回之后仍有任何一个字节不同,照旧判「没存上」。已知的剩余缺口:判定器不知道正文属于哪个仓库,只要求前缀是舰队名册里的同一个仓库。如果读回把引用整体换成名册里的另一个兄弟仓库,也会被放行。这种情形从未实测出现过,彻底收口要等兄弟卡改的调用方传入仓库名。回滚即还原本 PR。
  • 席位意见:
  • 你要做的:无需操作;席位复核 ACCEPT 后翻 ready 入队。

Generated by Claude Code

… expansion before calling a body mutated

A proxied REST read in a cloud seat container intermittently spells a bare
`#N` as `owner/repo#N`. The stored bytes are unchanged, but classifyReadBack
compared them byte-exact and answered `mutated` (exit 4 / relay `not-stored`),
whose remedy sends a seat to re-send the expanded body.

Before `mutated`, the judge now walks sent and stored in step and, only where
the sent body opens a bare reference, allows one of the fleet roster's slugs
in front of that `#` (one slug per body). If the collapsed body is one of the
declared classes, the verdict is the new landed class
`reference-expanded-on-read` with `afterCollapse`, `referenceExpanded` and
`referencePrefix`; otherwise it stays `mutated`, with the offset taken after
the collapse. post-stamped prints one stderr line naming the count on both
output shapes, `--json` carries the fields, and the NOT STORED text warns
against re-sending an expanded read. classifyReadBack's signature is
unchanged, so fleet-write/dispatch.mjs's imported judge and its content-based
comment lookup take the collapse with no edit there.

Claude-Session: https://claude.ai/code/session_01HEdP5tPhqQfzwRmKDxwn85
Co-authored-by: Claude <noreply@anthropic.com>
… as named red cases

Claude-Session: https://claude.ai/code/session_01HEdP5tPhqQfzwRmKDxwn85
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 20, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)· PR #24804 · domain:skills seat 1 · os-justin · session_01HEdP5tPhqQfzwRmKDxwn85 · 2026-10-20T07:17Z

席位核对

  • 形态: draft、base main、首行 Fixes #24747、行首 Clause-②: no、assignee os-justin、skip-changeset 随创建同笔(REST 回读 labels = size/m, skip-changeset);1 文件 +343/−6,两个提交(第二个把一条自测断言改为 null-safe,因首轮突变 M1 以崩溃而非具名红例报红);正文页脚会话 URL 形;闭合关键词只有首行一处。
  • 前提实测(席位,origin/main a43405d59 与分支 tip f0ecb34f6): 新类进入 sentBodyLanded(它读「非 mutated 即落库」,新类由构造落库);classifyReadBack({ sent, stored }) 签名未动,第 4459 行的签名钉未改;新导入 GOVERNED_REPOS(check-governed-merges.mjs,该文件不反向导入 post-stamped,无环);消息表、--json、NOT STORED 文案均带新读数;pnpm check:pm-post-stamped 724 例 / 25 battery 全绿(基线 679/24),pnpm check:pm-fleet-write-dispatch 222 例全绿。
  • 门禁 check-runs(head f0ecb34f6): 2026-10-20T07:17Z 43 条(39 名),30 success、9 skipped、0 failed、0 in_progress;Lint & Repo Gates 与 TypeScript Type Check 均 success 且钉 f0ecb34f6;check-expected-skips --pr 24804:OK — 11 skipped check-run(s), every one in the roster (exit 0);check-governed-merges:非受管面。dev 本地 34 条派生门禁全 0,--ran 34/34,钉 head(派发令清单的第 35 条为空行,非缺门禁)。
  • 接受的偏差: 假设 4(既有类 + 计数字段)未采 —— 卡文要求「a declared benign class of its own」,且 relay 行只打印类名,采新类名更诚实;假设 1 采「不改签名、前缀自 stored 读取并限名册」而非传仓名,理由如上(调用方不传仓名);基点 46bb5e3e5 → a43405d59 未合并(filing-gate.md 一行,无交集,merge-tree 干净);三次 push(探针 + 两提交)。
  • 复核记录: 非受管面,本记录即席位 TIER_DEFAULT 复核记录(本席逐段读了算法与钉子;施工 TIER_DEFAULT)。
  • 范围外去向: dispatch.mjs 侧三项(relay 行不打印计数;notStoredText 处方无「先折回」提醒;传 payload.repo 使前缀精确)—— 由本席在 fleet-write: a relay stroke of up to 20 content-creating writes runs at the 3 s gap — write-pace rule ⑤ (batch gap 30 s) never applies on the relay path, so one stroke is a burst #24755 落地后作为后续卡或折进其 PR,记座位贴;评论 6279597228 的原长 4,673 与折算 4,711 不合 —— 已在 PR docs(pm-skill): record the intermittent REST-read reference expansion in platform-readings #24744 记过,单位未决,不立卡。
  • 落地条款: 非受管;全绿后本席 pr_ready + automerge_enable。

ACCEPT


Generated by Claude Code

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