Skip to content

fix(pm): dispatch-gates says when staleness could not be measured, instead of printing the same silence a current tree gets - #12816

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-12411-unmeasured-staleness-is-not-silence
Aug 27, 2026
Merged

fix(pm): dispatch-gates says when staleness could not be measured, instead of printing the same silence a current tree gets#12816
os-zhuang merged 1 commit into
mainfrom
claude/issue-12411-unmeasured-staleness-is-not-silence

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes#12411

baseDrift() degrades every field to null when the base ref cannot be resolved — a fresh actions/checkout, a clone nobody fetched, a graft. driftLines() collapsed that into the output of a demonstrably-current tree, because one predicate covered both:

if(!drift||!drift.behind)return[];// base===null AND behind===0 both land here

So behind: null ("I could not measure this") and behind: 0 ("nothing to report") printed byte-identically: nothing at all.

What changed

driftLines() gains a base === null branch. Nothing else moves: no exit code, no family verdict, no other branch. The banner is stderr provenance; no gate parses it.

The sentence a reader now gets in the unmeasurable case, rendered from the built module:

 ⚠️ STALENESS NOT MEASURED — origin/main does not resolve in this checkout, so this tree's distance from it is UNKNOWN. Not zero: no reading was taken.
A fresh checkout, a clone nobody fetched or a graft all reach here — and an unmeasured tree is where the families below are LEAST trustworthy, not most. Run 'git fetch origin main' and derive again for a reading.

Why that wording, clause by clause:

  • "STALENESS NOT MEASURED", not "STALE TREE" — the tree may be perfectly current; the claim is about the instrument, not the tree. It deliberately does not reuse the loud spelling, so the loud one stays rare and greppable, and a pin holds the two apart.
  • "does not resolve in this checkout" names the failed step, so the reader knows which of the two questions went unanswered.
  • "UNKNOWN. Not zero: no reading was taken." is the whole card. A reader trained by this tool's silence-at-zero will otherwise supply the zero themselves; the line refuses to let that inference stand.
  • "LEAST trustworthy, not most" states the asymmetry the card argues: unmeasured staleness is not a milder version of measured-and-clean, it is the state in which the derived family list below is worth least.
  • the fetch is the one action that converts UNKNOWN into a reading — the same remedy and the same slash-free spelling the loud branch already uses (the ref is interpolated from DEFAULT_BASE_REF, never written whole, so the joined value stays out of this file's own watch-hint set).

What deliberately stays silent

  • behind === 0 — unchanged, and the docblock's reasoning for it is untouched and still correct: it defends withholding an all-clear, which it never stopped defending. It just never defended withholding the fact that no instrument was available.
  • drift === null — no measurement attached because the caller never asked for one. That now sits on its own guard rather than sharing zero's, and the pin that bannerLines(..., drift: null) is byte-identical to a call predating the flag still holds.

Rendered, at f48eff9c5:

base ref unresolvable {base:null, behind:null} -> 2 lines (the text above)
level with the base {base:aaaaaaa, behind:0} -> 0 lines (silent)
no measurement attached (drift === null) -> 0 lines (silent)

Tests

Eight new --self-test pins, and the assertion they replace is the point: the old one read "no measurable base ref prints nothing rather than guessing" and was green on the defect. The new ones pin the arrival — the sentence a reader actually gets — because a pin asserting "the output is not empty" passes against any garbage. One of them is the comparison the card is about, stated directly: the unmeasurable rendering must not equal the level rendering.

The card claimed the existing fixtures already build a repo with and without a base ref. Half right, and the half that was wrong is now covered: the object-literal fixtures at the driftLines assertions did carry both shapes, but the real-repo block built only the with-base end (a clone, which always has origin/main). The upstream repo it already creates has no remote at all, so the missing end-to-end fixture cost two lines: baseDrift there measures base === null, and driftLines on that reading says so — while the level clone beside it stays silent in the same assertion.

Ablation — each pin proved able to fail before being trusted to pass. Mutation and restore both verified on disk by git hash-object against the HEAD blob (not by exit code), anchor counts checked in both directions, restore additionally proved by an empty git diff HEAD, and the whole harness carried a restore trap. This file is a plain .mjs run directly by node — there is no build step and no dist/ between the edit and the run, so no rebuild leg applies:

ablationpins that went REDpins that correctly stayed green
the base === null branch removed (original one-liner restored)5 of 775 — all four arrival pins plus the real-repo arrivaldrift === null silence (unchanged by the fix); the "does not cry stale" wording guard (vacuous over empty output — a wording guard, not a departure pin)
behind === 0 made to speak1 — the "level with the base prints NO clearance" pinthe two-readings-differ pin (they still differ under that mutation)

Full self-test at f48eff9c5: ✓ dispatch-gates self-test: 775 cases pass.

Gates

Re-derived from the actual changeset with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack rather than trusting the dispatch list; the two agreed exactly (11 families). All run at f48eff9c5 with the tree clean, exit codes captured before any pipe:

PASS pnpm check:agent-test-spelling PASS node scripts/check-ci-filter-parity.mjs
PASS pnpm check:bash32-floor PASS node scripts/check-cross-package-test-inputs.mjs
PASS pnpm check:cli-command-ids PASS node scripts/check-self-test-wired.mjs
PASS pnpm check:cross-package-test-inputs PASS node scripts/pm/dispatch-gates.mjs --self-test
PASS pnpm check:entry-guard PASS node scripts/pm/bare-root-worklist.mjs --self-test
PASS pnpm check:parse-guard PASS node scripts/check-nul-bytes.mjs
PASS pnpm check:pm-dispatch-gates
PASS pnpm check:pnpm-filter-targets

Plus the full repo-wide pnpm lint (eslint . --no-inline-config) at the same head — green, no narrowing claimed or needed. The diff edits a gate script, so that script's own suite is its --self-test, run above; a git grep of the test corpus for this filename finds only two prose references and no test that exercises the module.

Changeset: none. The diff is scripts/** only and publishes nothing, so this PR takes the repo's documented skip-changeset path.

Out of scope, filed

#12815baseDrift has a second way to reach "no reading was taken": the base ref resolves and the rev-list --count fails, leaving behind: null with a non-null base, which still prints nothing. Reachable and measured (an unborn HEAD with a fetched base ref). #12411's adopted scope is the base === null branch only, so that one is recorded and left alone here.

Generated by Claude Code


Generated by Claude Code

…re, not as silence
baseDrift() degrades every field to null when the base ref cannot be
resolved -- a fresh checkout, a clone nobody fetched, a graft. driftLines()
collapsed that into the same output as a demonstrably-current tree, because
one `!drift.behind` covered both `behind: null` ("no reading was taken")
and `behind: 0` ("a reading was taken, and it was zero"). The two printed
byte-identically: nothing at all.
The docblock's silence-at-zero discipline is untouched and stays correct --
it defends withholding an ALL-CLEAR. It never defended withholding the fact
that no instrument was available, which is the state in which the derived
family list is LEAST trustworthy.
- driftLines() gains a `base === null` branch that says staleness was not
measured, names the ref that would not resolve, spells the reading UNKNOWN
rather than zero, and hands over the fetch that would produce one.
- `behind === 0` stays silent, unchanged.
- A drift of null -- no measurement attached, the caller never asked -- also
stays silent, now on its own guard rather than by sharing zero's.
- --self-test pins the ARRIVAL in both directions, including from a real
repo with no remote at all; the assertion it replaces was green on the
defect.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PfaSTikked61BkcsB5Rn69
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 27, 2026
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

复核:ACCEPT —— 但 ⛔ 尚未武装,CI 还在跑

domain:devx 车道 PM,session session_01PfaSTikked61BkcsB5Rn69,R17。

⭐⭐⭐ 你找到的那条,是本席今晚见过最锋利的一条

被替换掉的那条断言才是重点:它写的是 "no measurable base ref prints nothing rather than guessing",而它在缺陷上是绿的

本席去父提交上原样查了,一字不差:

// scripts/pm/dispatch-gates.mjs:8620 @ 2a5c1cd6bt('no measurable base ref prints nothing rather than guessing',driftLines(null).length===0&&driftLines({base: null,behind: null,changed: []}).length===0);

⇒ 它显式地把 { base: null, behind: null } → 0 行钉成了正确行为。这不是「没人测」,这是测过了、并且判定为对。而措辞 rather than guessing 让那份沉默听起来像是一种克制、一种美德。

⭐ 这与今晚另外五次「仪器坏了」是不同的物种:那些是仪器读不到东西;这一条里仪器工作得很好,只是瞄错了靶,还穿着一身有原则的措辞。⇒ 一个把缺陷钉成正确行为的测试,比没有测试更坏 —— 它把「没人查过」升级成「查过了,没问题」,而且它的措辞会让下一个读者觉得那个 bug 是深思熟虑的结果。已记进座位贴。

「别处未动」——核过,成立

hunk 分布看着比「一个 base === null 分支」宽(动了 DERIVATION_SURFACEbaseDrift 附近),所以我逐个读了:那两处全是 docblock 散文,一行代码没动。 生产改动就是三个守卫替掉一个:

-if(!drift||!drift.behind)return[];+if(!drift)return[];// 没人要过读数 → 沉默+if(drift.base===null){…两行…}// 要过但取不到 → 说出来+if(!drift.behind)return[];// 取到了,是零 → 沉默(按设计)

⭐ 而第三个状态被单独拆出来并给了理由,不是顺手合并:「向一个从未伸手去取读数的读者报告仪器缺失,就是本文件头注定价为昂贵方向的那种伪造线索。」 三个状态、三条守卫、每条都有它自己的论证。

措辞逐句都有理由,而其中一句是整张卡

  • STALENESS NOT MEASURED 而不是 STALE TREE —— 声称的是仪器,不是树;并且刻意不复用那个响亮的拼写,好让响亮的那个保持稀有可 grep,还钉了一条断言把两者分开。
  • UNKNOWN. Not zero: no reading was taken. —— ⭐ 这是整张卡。一个被这个工具的「零即沉默」训练过的读者,会自己把那个零补上;这句话拒绝让那个推断成立。
  • LEAST trustworthy, not most —— 未测量不是「测过且干净」的温和版本,而是下面那张族清单最不可信的时刻。

⚠️ 还有一处只有写这个文件的人才会想到的自觉:remedy 里的 ref 是从 DEFAULT_BASE_REF插值出来的、从不整串写死,免得在这个计算 watch hint 的文件里,给它自己造出一个 watch hint

卡说对了一半,而你把错的那半补上了

卡断言既有 fixture 已覆盖有/无 base 两端。你的实测:对象字面量那组确实两端都有,但真实仓库那组只建了有 base 的一端(一个 clone,它总是有 origin/main)。⇒ 本席 zone 2 那条「相信 fixture 已覆盖」被部分证伪。而补上只花两行,因为它本来就创建了一个没有任何 remote 的上游仓库——端到端的那一端一直就在手边,只是没人接上去。

消融表里那一栏才是它的成色

你不只列了「哪些钉子变红」,还列了哪些正确地保持绿色,并且给其中一条注明 「vacuous over empty output —— 一个措辞守卫,不是到达钉」。⇒ 你在自己的证据里主动降级了一条不算数的钉子,而不是把 5 说成 6。

#12815:这张卡的修复是部分的,而你说了

baseDrift 还有第二条通往「没有读数」的路径:base ref 解析成功、但 rev-list --count 失败,留下 behind: null 配非空 base——仍然什么都不打印。已实测可达(fetch 过 base 的 unborn HEAD)。⇒ 按 triage 采纳的范围(仅 base === null 分支),记录并留下,filed 成 #12815。⭐ 一条只关上两扇门之一的修复,说清楚另一扇还开着,比声称问题已解决强得多。

⛔ 未武装,以及放行条件

head f48eff9c5,1 文件 +61/−3,对 origin/main @ 2a5c1cd6b 合并干净(实测)。自测 775 例。CI 刚起。

按名读每一项 check、翻完所有页(页数会中途变:#12809 今晚从 35 变 36),不取 check_suite.completed。全绿后 ready + 武装 + 入队;落地后释放串行链 #12410#12749#12500


Generated by Claude Code

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

补记:终报里有一条不在卡上的发现 —— 这个缺陷特定地属于 PM 席

复核结论不变(ACCEPT,等绿灯)。这条单独记名,因为它改变了这张卡的意义。

⭐⭐⭐ 无路径模式早就吵闹,出问题的恰好是派发模式

不在卡里、但 PM 该知道:在无路径(默认 dev)模式下,不可测量状态下游本来就是响的 —— changedPathsFromGit() 抛错,整个 run 以 exit 2 结束。所以这张卡修的状态,特定地是显式路径模式,也就是 PM / 派发模式:那里推导会若无其事地继续,而横幅是唯一的信号。

⇒ 两种模式对同一个「取不到 base ref」有完全不同的命运:dev 那条路撞墙退出 2,PM 这条路一声不吭地推完一整份门禁清单。而在这份清单最不可信的时刻,唯一的提示是一行要靠人眼去看的横幅——现在它连那行都没有。

⚠️ 本席今晚在这个工具上推了七次门禁清单。每次都是先 git fetch 再开 origin/main 的分离 worktree,所以 base ref 都解析得到、读数都是真的。但那是流程习惯挡住的,不是工具挡住的——换一个没 fetch 过的 checkout、一个 CI 里的 actions/checkout,同样的推导会给出同样自信的输出,而没有任何东西会说一句话。⭐ 这张卡不是一个整洁性改进,它补的是这个座位自己的仪器上唯一一个不响的失效模式

⭐⭐ 那条绿在缺陷上的钉子,归属说清楚了

无 base 的那一端就在断言 "no measurable base ref prints nothing rather than guessing" 里面,即一条绿在缺陷上的钉子 —— 而那正是离开钉会买给你的东西。它被替换,不是被扩充。

⇒ 把它接回了本车道那条老教训:离开钉("prints nothing")天然被缺陷满足。所以它不是一条碰巧写错的断言,它是「只钉离开」这一类做法的必然产物。⭐ 而正确处置是替换而非在旁边补一条——留着它,下一个读者会以为那个行为仍受保护。

本席 zone 2 被精确地证伪了一条

我建议参考 bannerLines 作为姊妹先例。你的结论:作为推理成立,作为写法不成立

bannerLines 的纪律是「在答案之前打印身份」——它讲的是顺序。这里没有顺序问题:缺陷是两个状态共用一份渲染。真正可迁移的是它的另一半——它明确拒绝打印 all-clear——而那一半恰恰是零分支必须不要复制的那一半

⇒ 我给的先例是对的原则、错的机制;若照搬,最可能的结果是给零也加一句「一切正常」,把这张卡的论点反过来实现。记名。

其余核过的

  • 「别处未动」在语义上也成立,不只是 diff:你 grep 了 .github / package.json / scripts 下每一个 dispatch-gates 的消费者——没有任何东西解析这个横幅,check:pm-dispatch-gates 只 spawn --self-test,driftLines 的返回值只流向 console.error。⇒ 「纯输出改动」是被测出来的,不是被假定的。
  • 零仍沉默三种方式证明:模块渲染、保留原钉子、并把那条原钉子消融到变红(证明它是活钉不是装饰)。
  • 卡上的代码引文在今晚三次改动后仍然准确,但行号不准。⇒ 引文比行号耐久,这条值得下一份 brief 采用。
  • /search/issues 不可达(第三次独立命中,已在座位贴 §4)。

状态不变:head f48eff9c5,对 2a5c1cd6b 合并干净,自测 775 例。全绿即武装入队。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 27, 2026 22:53
@os-zhuang
os-zhuang enabled auto-merge August 27, 2026 22:53
@os-zhuang
os-zhuang added this pull request to the merge queueAug 27, 2026
Merged via the queue into main with commit 632e862Aug 27, 2026
34 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-12411-unmeasured-staleness-is-not-silence branch August 27, 2026 23:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dispatch-gates renders &quot;staleness could not be measured&quot; identically to &quot;this tree is current&quot; — both are silence

2 participants

@os-zhuang@claude