Repository navigation
dispatch-gates: baseDrift can resolve the base ref and STILL fail to read the distance — that second unmeasured state is printed as nothing #12815
Description
Activity
huangyiirene commented
on Aug 27, 2026 CollaboratorMore actionsGraded:
Task·tooling·domain:devx·pm:queue— dispatchable now, and its dependency closed 23 minutes before this gradingTriage seat, session
session_01Aujz2zykf5LXt3T98gRsGe, round anchoredsince=2026-08-27T22:24:00Z.Routing, read from the code rather than the title
git grep -ln "baseDrift" -> scripts/pm/dispatch-gates.mjs (sole hit) git grep -ln "zzz_no_such_symbol_xyz" -> (empty) [negative control]The fix lands in
scripts/pm/dispatch-gates.mjs. Siblings in that same file — #12411 and #12410 — are bothtooling+domain:devx, so this follows the established anchor rather than inventing one. ⛔ Notescripts/**is not governed surface (docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md), so this is an ordinary PR, not draft-only.The dependency this card was filed under is already discharged
#12411 closed 2026-08-27T23:16:27Z via merged PR #12816 — inside this round's window. That matters because this card's stated fix shape is "route both states through one predicate rather than adding a second branch beside the first", which requires the first branch to exist. It now does, verified on
origin/main(tip632e862d1):export function driftLines(drift) { if (!drift) return []; if (drift.base === null) { // <- state A, spoken as of #12816 return [ ` ⚠️ STALENESS NOT MEASURED — ...` , ... ]; } if (!drift.behind) return []; // <- state B, still silent
and the producing side is unchanged at
baseDrift:const counted = read(['rev-list', '--count', `HEAD..${DEFAULT_BASE_REF}`]); const behind = /^\d+$/.test(counted ?? '') ? Number(counted) : null; // state B
⇒ the card's premise is true on current
main, not merely true when filed.behind === nullfrom a resolvable base still returns[]atif (!drift.behind), byte-identical to a level tree. So this ispm:queue, notpm:blocked— there is nothing left to wait for.Why it clears the manual floor
- Not gate weakening — the opposite.
driftLinesoutput is report text (lines.push(...driftLines(drift))); it does not feed an exit code. The ask adds a warning where there is currently silence, so it can only make the gate louder. - Not a feature addition, not ADR-class, not protocol, not destructive, not a security/permission boundary, no new runtime dependency.
- Clause ②: does not fire. Path leg no (
packages/spec/src/**untouched). Content leg no — it changes neither what dispatch-gates accepts or rejects, nor any published surface;driftLines/baseDriftare exports of an internal repo script.
Type follows the sibling, deliberately
Task, matching #12411 — the same collapse class, one layer down. I consideredBug: the instrument does report a false clean state. But #12411 is the governing precedent for this exact defect in this exact function, and splitting the type across two halves of one collapse would make the pair harder to read than either choice is worth.One note for whoever takes it
The card asks that the new direction be pinned from a real repo, not an object literal. That bar is already met on the other side and is worth copying rather than re-deriving:
--self-testbuilds an unresolvable-ref repo and asserts against it (driftLines(unresolvableRepo)), alongside the literal-based assertions at thedriftLinescases. The unborn-HEAD fixture in the card body is the state-B analogue of that existing fixture.置信缺口
I did not run
--self-teston this tree, so I am asserting the fixture exists from its source text, not that it passes today. And I have not measured how often state B is actually reached in CI — the card argues rarity is the reason to speak, not a reason to shrug, and I graded it on that argument rather than on a frequency I do not have.
Generated by Claude Code
- Not gate weakening — the opposite.
认领 —
domain:devx@ objectstack 执行席(R19)席位贴 #6023 · session
session_01PfaSTikked61BkcsB5Rn69· 分支claude/issue-12815-drift-state-b· 模型opus。Serial constraints cleared:
scripts/pm/dispatch-gates.mjs上的同族卡 —— #12410(R18,已关:其处方在 #12476/e980f6448已先行落地,且落在另一个文件)· #12411(R17,已合 #12816/632e862d1)。#12749 / #12500 / #12797 仍在队列中排在本卡之后,本轮不派。⇒ 该文件本轮独占。派发前重测(⛔ 不引用卡上数字,现推于
632e862d1)本席这一班在 #12410 上刚栽过一次「卡的处方在填卡与派发之间已经落地」,所以本卡的 re-check 是先跑的,不是事后补的:
scripts/pm/dispatch-gates.mjs:4938-4939 const counted = read(['rev-list', '--count', `HEAD..${DEFAULT_BASE_REF}`]); const behind = /^\d+$/.test(counted ?? '') ? Number(counted) : null; ← state B 的产生侧,未变 driftLines(): if (!drift) return []; if (drift.base === null) { …STALENESS NOT MEASURED… } ← state A,#12816 已落 if (!drift.behind) return []; ← state B,仍然静默阳性对照
STALENESS NOT MEASURED= 3(证明 #12816 确已在这棵树上);阴性对照(一个不存在的符号)= 0,证明这把 grep 会读 0。⇒ 卡的前提在今天的main上成立,不只是填卡当天成立。triage(5446419013)同向,本席独立复现,非转述。闸门(现推于
origin/main的分离 worktree,面 =scripts/pm/dispatch-gates.mjs+ 一个.changeset/*.md)pnpm check:agent-test-spelling · check:bash32-floor · check:changeset-gate-self-tests check:cli-command-ids · check:cross-package-test-inputs · check:entry-guard check:objectui-changeset · check:parse-guard · check:pm-dispatch-gates check:pm-half-states · check:pnpm-filter-targets node scripts/check-adr-0087-registration.mjs · check-changeset-no-major.mjs check-ci-filter-parity.mjs · check-cross-package-test-inputs.mjs check-empty-changeset.mjs · check-self-test-wired.mjs scripts/pm/check-half-states.mjs · scripts/pm/release-rehearsal-clone.mjs --self-test⚠️ check:pm-dispatch-gates与check-self-test-wired.mjs这次是路径推导自己点名的(⇢ gate source 'scripts/pm/dispatch-gates.mjs'),不是本席按惯例手工补的 —— 但仍然照跑。交给 dev 的边界
- ⛔ 本卡不是决策卡,不踩人工地板:
driftLines的输出是报告文本(lines.push(...driftLines(drift))),不喂 exit code;把静默换成一句警告只会让门禁更响,这是「加宽」的反面,不是削弱。 - 卡自己给的 fix shape(「两个 state 走同一个谓词,而不是在第一个分支旁边再写一个」)是 Zone 3 建议,实测优先;若实测表明两句话的差异大到共用谓词反而更难读,允许推翻并说明。
- 卡要求新方向从真实 repo 钉(unborn-HEAD fixture,四条命令可建),⛔ 不只用对象字面量 —— 理由卡上写了:字面量证明不了这个状态可达。
--self-test里已有的 unresolvable-ref fixture 是它的对侧样板。 - ⭐ 本轮最该继承的一条:dispatch-gates renders "staleness could not be measured" identically to "this tree is current" — both are silence #12411 删掉的那条断言是绿在缺陷上的(
'no measurable base ref prints nothing rather than guessing')。state B 现在也可能有同形的钉子 —— 先找,找到就替换,⛔ 不要在它旁边再加一条。
Generated by Claude Code
- ⛔ 本卡不是决策卡,不踩人工地板:
os-dev-report
{ "issue": 12815, "status": "done", "branch": "claude/issue-12815-drift-state-b", "pr": 12826, "pr_url": "https://github.com/objectstack-ai/objectstack/pull/12826", "premise_still_valid": true, "files_changed": [ "scripts/pm/dispatch-gates.mjs" ], "gates_run": [ { "name": "pnpm check:nul-bytes", "result": "pass — OK (7135 text files scanned, no raw ASCII control bytes)" }, { "name": "pnpm check:agent-test-spelling", "result": "pass" }, { "name": "pnpm check:bash32-floor", "result": "pass" }, { "name": "pnpm check:cli-command-ids", "result": "pass" }, { "name": "pnpm check:cross-package-test-inputs", "result": "pass" }, { "name": "pnpm check:entry-guard", "result": "pass — 170 scripts/ files; self-test 54 cases pass" }, { "name": "pnpm check:parse-guard", "result": "pass" }, { "name": "pnpm check:pnpm-filter-targets", "result": "pass" }, { "name": "pnpm check:pm-dispatch-gates", "result": "pass — dispatch-gates self-test: 788 cases pass (baseline 775)" }, { "name": "node scripts/check-ci-filter-parity.mjs", "result": "pass" }, { "name": "node scripts/check-cross-package-test-inputs.mjs", "result": "pass" }, { "name": "node scripts/check-self-test-wired.mjs", "result": "pass — 140 self-test-carrying scripts, all run by CI" }, { "name": "pnpm check:changeset-gate-self-tests", "result": "pass (not named by this diff's paths; run anyway)" }, { "name": "pnpm check:objectui-changeset", "result": "pass (not named by this diff's paths; run anyway)" }, { "name": "pnpm check:pm-half-states", "result": "pass — 1515 cases (this is check-half-states.mjs --self-test; the brief's bare live-sweep spelling needs a transport this container lacks)" }, { "name": "node scripts/check-adr-0087-registration.mjs", "result": "pass (not named by this diff's paths; run anyway)" }, { "name": "node scripts/check-changeset-no-major.mjs", "result": "pass (not named by this diff's paths; run anyway)" }, { "name": "node scripts/check-empty-changeset.mjs", "result": "pass — 0 declaring changesets added, diffed from c4ecf0c49" }, { "name": "node scripts/pm/release-rehearsal-clone.mjs --self-test", "result": "pass (not named by this diff's paths; run anyway)" } ], "gate_union_head": "0c1d4f6f6", "mcp_calls": "0 — repo-scoped REST probe returned 200, so PR create, label write and every read-back went through container curl; issue body and comments came from the public-repo page payload (zero quota).", "zone2_falsified": [ "PM assumed state B is near-absent in CI: HOLDS for the CI-default checkout shape. Measured a --depth=1 shallow clone before and after the upstream moved: `git rev-list --count HEAD..origin/main` answered 0 then 1, status 0 both times. A shallow clone is NOT a silent instance.", "But the card's rarity framing is LOOSER than it argued, in the direction that strengthens the card: state B is reachable from an ordinary fully fetched clone by one ordinary command. `git checkout --orphan a-branch` leaves HEAD unborn while origin/main still resolves, and the count then fails with status 128. Pinned in --self-test as a real-repo case, and stated in the PR body.", "PM's 'one predicate reads better than two branches' assumption: HOLDS, and for a measurable reason rather than taste — the two remedies do not overlap (a fetch buys a base ref and buys nothing for a HEAD with no commit), so the shared frame is what needed one home and the differing fields are exactly what the predicate returns." ], "judgment_calls": [ "CHANGESET: none; took the `skip-changeset` label instead of the brief's '需要 .changeset/*.md (patch)'. Decided from the repo before the PM's correction arrived, on three readings: (a) .claude/skills/pm-dispatch/references/lanes/devx.md:24 — 'changeset 按 publish 面判:根 scripts/ / docs / test-only ⇒ skip-changeset'; (b) PR #12816, the immediate predecessor on this exact file, landed with no changeset and carries the label; (c) scripts/pm/ belongs to no workspace package, so a patch changeset could only name a package this diff does not touch. The PM's mid-task correction independently confirmed all three. Label applied by additive REST POST (HTTP 200) and read back after the size-labeler had already run: labels are ['size/m','skip-changeset'].", "ZONE 1 (starred): searched for a pre-existing assertion green on state B and found NONE, so the new pins are additions, not a replacement. Two readings: the literal `behind: null` occurs in exactly one pre-existing self-test object and is paired with `base: null` (state A); and mechanically, all 775 pre-existing cases still pass with the fix applied (775 -> 788), where a nail requiring the defect would have turned red. Reported as a negative result rather than forcing a replacement.", "Two of my OWN new assertions were that species when first written ('does NOT cry stale', 'the two doors are told apart') — both are satisfied by an empty list. Each now carries a `length === 2` conjunct, measured under ablation to go red; the reason is written in the source beside them.", "IN-SCOPE WIDENING, named in the PR body: the reading test is `Number.isFinite(drift.behind)`, not `behind === null`. Absent / NaN / non-numeric are not readings either, and the only direction this moves a case is silence -> speech (`behind: 0` is a reading and stays silent). Same defect class, same file, same gate family, no new verification surface; pinned by its own case.", "Branch history carries two `wip(...)` commits from the mandated mid-work checkpoints. Not rewritten: tidying them needs a force-push, which AGENTS.md §3 forbids outright. The final commit carries the full message and the PR title is what the squash lands.", "The platform appended a SECOND session-URL footer to the PR body on create. Left alone deliberately — an edit to remove it would normalise the footer down to bare and rewrite a body that read back otherwise byte-identical." ], "residual_findings": [], "tests": "Ablations, both with the mutation confirmed ON DISK (anchor counts both directions + git hash-object) and the restore proven BY STATE (git diff HEAD empty AND blob == HEAD blob), each under a trap with absolute paths. No rebuild leg is owed: plain .mjs, no dist between the mutation and the run. (1) RENDERER ablated — `if (!Number.isFinite(drift.behind))` guarded to `false &&`, pins untouched: anchor 1->0, injected 0->1, blob a3f2cc12->6b7c857e, result 10 of 787 failed, every one a new arrival pin, zero pre-existing cases moved; the two producer pins stayed green, correctly, since baseDrift was not ablated. (2) FIXTURES ablated — the unborn fixture's fetch guarded out and `--orphan` turned into plain `-b`, blob ->4f9692d9, result 4 of 787 failed, exactly the fixture-dependent pins, so the reachability controls are not decoration. Restores verified: blob back to a3f2cc121628a9bfdf6b5ec110ccb5049e315bcd, git diff HEAD empty, both times. Green union re-run AFTER the final commit at 0c1d4f6f6, under scripts/pm/os-verify-lock.sh: all 19 gates exit 0, dispatch-gates self-test 788 cases pass (baseline 775). Premise reproduced first on 632e862d1 from a real repo: rev-parse --short origin/main -> 4df19bc status 0, rev-list --count HEAD..origin/main -> status 128, baseDrift -> {base:'4df19bc',behind:null}, driftLines -> 0 lines.", "summary": "driftLines() now speaks for BOTH ways baseDrift() reaches 'no reading was taken', through one predicate (unmeasuredDrift) rather than a second hand-written branch. State B — the base ref resolves and the distance cannot be counted — used to fall through `if (!drift.behind) return []` and render byte-identically to a level tree; it now names which step failed and hands over the remedy for THAT step, since a fetch fixes the other door and does nothing for a HEAD with no commit. Report text only, so the tool gets louder and no exit code moves. Pinned from real repos on both sides (an unborn checkout with a fetched base ref; the same state reached in a fully fetched clone by `git checkout --orphan`) plus the banner a reader actually sees, and every new pin was shown to fail under ablation before being trusted." }Generated by Claude Code
Generated by Claude Code
Filed unassigned while implementing #12411 (out-of-scope finding). Not claimed, not graded. #12411's adopted scope is the
base === nullbranch ONLY and that scope is correct; this is the sibling state one layer down, deliberately left alone there.What was measured
baseDrift()has two ways to reach "no reading was taken", and #12411's fix addresses one:In state B the base ref resolves fine and the distance is what could not be read.
driftLinesreachesif (!drift.behind) return []withbehind === nulland prints nothing — byte-identical to a tree measured level, which is exactly the collapse #12411 is about.Repro, measured on this branch (with #12411's fix applied)
A checkout whose base ref is fetched but whose own HEAD is unborn:
So after #12411 lands,
STALENESS NOT MEASUREDcovers the unresolvable-ref case and this one still reads as a clean bill of health.Why it is worth a card rather than a shrug
Rarity is the argument for saying so, not against: an unborn HEAD, a
gitthat fails mid-run, a futureread()that returns something non-numeric all land here, and the reader gets the tool's most reassuring output — silence — in the state where the derived family list is least trustworthy. The same reasoning #12411's triage adopted applies unchanged.Shape of the fix
Route both states through one predicate rather than adding a second branch beside the first: the rendered sentence differs only in which step failed, and a second hand-written branch is a second thing to keep in sync. Pin the new direction in
--self-testfrom a real repo (the unborn-HEAD fixture above builds in four commands), not from an object literal alone — the literal cannot show that the state is reachable.Sibling cards held on this same file at filing time: #12410, #12749, #12500, #12797.
Generated by Claude Code