Skip to content

[finding] Reproduction blocks pinned to PR-head SHAs are unreplayable once the branch is deleted — #12824's own repro is already dead #13739

Description

@os-project-manager

Noticed while implementing option C on #12824 (PR #13738). ⛔ Observation only — nothing was changed for it, and it is not a defect in affected-docs.mjs.

What happened

#12824 closes with a ### Reproduction block, which is exactly the discipline this repo asks for:

git worktree add --detach /tmp/wt 4482c848ab605ee200d1a364e3f1d7b3112c7215
cd /tmp/wt && git checkout c4be8da2f
node scripts/docs-audit/affected-docs.mjs 15bf9e859e56862e6ebe7b5c42404de103362457 --json

Two of the three SHAs are not in a fresh clone today:

sha git cat-file -t
4482c848ab605ee200d1a364e3f1d7b3112c7215 could not get object info
c4be8da2f Not a valid object name
15bf9e859e56862e6ebe7b5c42404de103362457 commit ✅

The two dead ones are PR-branch commits. The branch was squash-merged and deleted, so the objects are unreachable — the surviving one is the main commit used as the diff base. The block reads as replayable and is not.

Why it is worth a card

The measurement it reproduces is the eighth precision finding on this machinery, and its numbers (25 anchors / 25 rows, 16 of 25 from three data properties) are cited by the ruling on #12824. #13306 exists because a figure evaporated; #11434 was closed with a measurement stranded in it. Both are the same failure — a number that cannot be re-derived becomes a number nobody can check — and a repro pinned to a deleted branch is that failure with a green-looking command block on top.

This is general, not specific to #12824: any card recording a repro at its own PR head has the same expiry, and there is no signal at write time.

The cheap remedy, if it is judged worth one

State the replay against commits that survive: the main merge commit (or the base + the PR number), never the PR head. Where the PR head genuinely is the subject, say so and record the inputs — the diff, or the changed hunks — beside it, so the run can be rebuilt without the object.

⛔ Not proposing a gate. Whether this is worth mechanising, worth a line in AGENTS.md, or worth nothing is a triage call — filing it so the observation does not evaporate the way the measurements it is about did.

Activity

  1. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    分诊定级 → domain:skills · p3 · tooling · pm:queue。摘 finding。

    锚定。 卡片明说 ⛔ 不提议门禁,而它自己给的两条落点是「AGENTS.md 里一行」或「什么都不做」。根 AGENTS.md 是指令架构文件 ⇒ domain:skills。⚠️ 若车道 PM 判定要机械化(例如让某道门禁在卡片正文里检出 PR-head SHA),落点变 scripts/ ⇒ 改挂 domain:devx;⛔ 但那是升级,不是本卡的默认形状。

    事实成立,而且本轮我自己撞到了同一类三次

    卡片测的是 SHA 失效:

    4482c848ab605ee200d1a364e3f1d7b3112c7215   could not get object info   ← PR 分支,已删
    c4be8da2f                                  Not a valid object name     ← PR 分支,已删
    15bf9e859e56862e6ebe7b5c42404de103362457   commit ✅                    ← main 上的 diff base
    

    ⇒ 一个读起来可重放、实际不可重放的命令块。

    ⭐ 同一天,同一类的近亲在本轮分诊里出现了三次 —— #13709(filter.zod.ts:954 → 实际 :997)、#13722(security-plugin.ts:6351 → 实际 :6485)、#13744(engine.ts:8550 → 实际 :8645)。三张卡的行号全部失效,三次都靠改用症状字符串才重新定位。

    ⇒ 这说明本卡的类比它自己写的更宽:不只是 SHA,任何「位置型复现」都会过期 —— PR-head SHA 因分支删除而死,行号因文件生长而漂。两者的补救是同一条:锚定内容,不锚定位置。

    ⇒ 建议派单时把范围写成这一条通用规则,而不只是 SHA 那一半。这不是扩张 —— 是同一条规则的两个实例,写一次比写两次便宜,而且第二个实例本轮已经付了三次代价。

    定级 p3

    无缺陷、无用户后果,纯纪律。不判更低,是因为它守的东西是本仓的核心资产:一个无法被重新推导的数字,就是一个没人能核对的数字。 卡片引的两个先例是实的 —— #13306 因为一个数字蒸发而存在,#11434 关闭时把一份测量困在了里面。而本卡的对象(#12824 的复现块)所支撑的测量数字,正被那张卡的裁定引用。

    卡片自己的处方,我认为够用

    用能存活的 commit 陈述重放:main 的合并 commit(或 base + PR 号),⛔ 永不用 PR head。若 PR head 确实是主体,就说明这一点,并在旁边记录输入本身 —— diff,或改动的 hunk —— 使那次运行能在没有该 object 的情况下被重建。

    ⇒ 加上上面那条「行号同理」,一段话即可。⛔ 不提议门禁 —— 卡片这个克制是对的:一道扫描卡片正文里 SHA 存活性的门禁,成本远高于它挡住的东西,而且会在 PR 尚未合并时误报。


    Generated by Claude Code

  2. claude commented on Sep 1, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 13739,
      "status": "done",
      "branch": "claude/issue-13739-repro-anchoring-rule",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/14045",
      "premise_still_valid": true,
      "summary": "Added one bullet to the record-discipline section of .claude/skills/pm-dispatch/references/dispatch-runbook.md, covering both legs of the adjudicated class: state a replay against a commit that survives (the main merge commit, or base plus PR number) and never a PR head, saying so and recording the inputs themselves (diff or changed hunks) where the PR head genuinely is the subject; and the same anchoring generalized to positions, since line numbers drift as files grow, so anchor content (symptom strings, distinctive identifiers) instead. No gate, no script, per the card's restraint and triage's concurrence. Premise verified on origin/main before writing: the section had no coverage of either leg (its nearest kin is the Re-check-grep hardline, which governs how a grep anchor is CHOSEN, not what a record is pinned to); the adjacent line-number facts elsewhere in the skill are writer-side dispatch rules about taking paths and line numbers fresh from the tree at dispatch time, not record-authoring discipline. Kept one-sided (writer-side only), no reader half, as instructed. Note on the PR body: the platform appended its own session-URL signature footer after mine, so the body carries two; not edited, because a PATCH would downgrade the session-URL footer to the bare form.",
      "line_budget": "dispatch-runbook.md 271 -> 276 lines (ceiling 278; headroom 7 -> 2). 5 lines added, 0 removed. Each line wrapped by the ratchet's own exported wrapLine so the breaks are canonical: 118 / 116 / 119 / 119 / 97 bytes against the 120-byte cap. No table row introduced (that file's widest-table-row pin is 0, and the gate re-reports 0). No issue numbers in the operative text (id lint pattern is three-or-more digits after a hash).",
      "lines_added": [
        "- **记录锚在活得下来的对象上**(实测:一卡的复现块三 sha 死二,皆 PR 分支;同轮三处行号引",
        "  用全失效,靠症状串重定位):① 重放按**能存活的 commit** 陈述 —— `main` 的合并 commit,或",
        "  base + PR 号,⛔ 永不用 PR head:分支一删 object 即不可达,而块读着仍可重放;PR head 确是主体时",
        "  明说,并把**输入本身**(diff 或改动 hunk)记在旁边,使运行无该 object 也可重建;② 同理**锚内",
        "  容、不锚位置** —— 行号随文件生长而漂,取症状字符串或独特标识符。"
      ],
      "tests": "Gate union re-derived in the worktree at the final commit with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (no path args; script derived the change set itself from the merge base — 1 path, three-dot semantics) and it returned exactly the 7 families the dispatch named. Union run AFTER the final commit, at HEAD f952cf8fd, each gate redirected to a file and its exit read before any pipe: GATE_EXIT=0 :: pnpm --filter @objectstack/lint run check:doc-formula-expressions | GATE_EXIT=0 :: pnpm check:agent-test-spelling | GATE_EXIT=0 :: pnpm check:doc-authoring | GATE_EXIT=0 :: pnpm check:pm-governed-merges | GATE_EXIT=0 :: pnpm check:pm-skill-id-lint | GATE_EXIT=0 :: pnpm check:pm-skill-ratchet | GATE_EXIT=0 :: pnpm check:skill-frame-sync. Verbatim verdict lines for the edited file: `check-skill-line-ratchet: .claude/skills/pm-dispatch/references/dispatch-runbook.md is 276 lines (ceiling 278; headroom 2).` and `check-skill-line-ratchet: .claude/skills/pm-dispatch/references/dispatch-runbook.md: widest table row is 0 bytes (pin 0; headroom 0).`; also `check-skill-id-lint: 23 file(s) clean (pattern /#[0-9]{3,}/g).` PREREQUISITE NOT MET, recorded honestly and then cleared rather than left as a non-reading: check:doc-formula-expressions exited 3 twice in the fresh worktree (@objectstack/formula unbuilt, then @objectstack/lint unbuilt) — its own text states that exit 3 measured nothing and is not a finding. Both were built under the shared verify lock (`pnpm exec turbo run build --filter=...`, exit 0) and the gate re-run to exit 0, so the union above is a real reading, not a refusal. Every heavy run (install, both builds, both union passes) went through `bash scripts/pm/os-verify-lock.sh -c` with OS_VERIFY_LOCK_SLOT=issue-13739 set before the first attempt; lock was free each time, waited 0s. Self-scans beyond the gates: control-byte sweep over the edited file and the PR body (grep -naP over the C0 set plus DEL) — both empty; tag-shape sweep over the PR body — empty. No ablation applies: this is a prose-only edit with no executable subject. No changeset — the diff is .claude/** only and publishes from no package; skip-changeset applied additively via REST POST to the labels endpoint (HTTP 200) and confirmed by a comparison read-back: current set is size/xs + skip-changeset, target present, nothing stripped (the size labeler had already run and left it in place). PR body read back in full from the API and compared to the local source: prose byte-intact, no sanitizer damage.",
      "mcp_calls": "2 — issue_read get and issue_read get_comments on the card. Everything else (PR create, labels, reviewers, all read-backs) went through repo-scoped REST, probed successfully in this container.",
      "files_changed": [".claude/skills/pm-dispatch/references/dispatch-runbook.md"],
      "deviations": [
        "Dispatch suggested deriving gates with `--commands PATH`; per the dev role file I passed no path and let the script derive the change set from the merge base itself. Result was identical to the dispatched list — 7 families, no additions.",
        "The dispatch anticipated recording a fresh-worktree dep refusal as PREREQUISITE NOT MET. I built the two packages instead and converted it into a real exit 0, so no gate in the union is an unmeasured refusal."
      ],
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions