Skip to content

feat(dispatch-gates): a checkable whole-tree-population channel, so a repo-wide gate is placed by declaration instead of landing near undetermined - #14774

Merged
os-project-manager merged 4 commits into
mainfrom
claude/issue-14189-whole-tree-population-channel
Sep 3, 2026
Merged

feat(dispatch-gates): a checkable whole-tree-population channel, so a repo-wide gate is placed by declaration instead of landing near undetermined#14774
os-project-manager merged 4 commits into
mainfrom
claude/issue-14189-whole-tree-population-channel

Conversation

@claude

@claude claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes #14189

A checkable, in-source channel for a gate to declare "my population is the WHOLE tree; every card implicates me; do not narrow me" — the exact inverse of dispatch-gates: no-path-population — consumed by the derivation so a declaring gate is placed by its declaration instead of landing near undetermined by accident, and rendered as an always-runs row rather than as leads.

The marker

// dispatch-gates: whole-tree-population -- REASON        (JS/TS gates)
#  dispatch-gates: whole-tree-population -- REASON        (shell gates)

Same comment-form tolerance as its sibling, parsed by a sibling regex, reason REQUIRED. ⛔ Not the ROOT_DIR_WATCH_HINTS literal idiom, and no whole-tree literal anywhere: a ** / . / bare-root declaration is the fabricated-lead move scripts/workspace-enumerator.mjs prices at +41725 (gate, file) pairs.

Unlike no-path-population, this declaration does not contradict a gate that names paths — a whole-tree gate still spells its own baseline artifacts, and those literals stay in entry.hints where the unreachable sweep still grades them. The declaration supersedes them for PLACEMENT only. What it does contradict is no-path-population, and that pair is refused.

Which gates were declared, and why the census is five and not six

Each candidate was judged by reading its walk on this branch, never by the census that nominated it.

gate the walk its own source performs declared
scripts/check-nul-bytes.mjs git ls-files PLUS git ls-files --others --exclude-standard yes
scripts/check-comment-mask-corpus.mjs collectSources(root = REPO_ROOT) — a repo-root walk over every authored JS/TS file yes
scripts/check-refd-timer-probe.mjs readTree(root = REPO_ROOT), the same tracked-plus-untracked sweep yes
scripts/check-closing-keyword-parity.mjs sweep(root) over git ls-files -z, node_modules and dist aside yes
scripts/check-watch-hint-literal.mjs audit(walk(REPO_ROOT)) — every authored JS/TS file in the tree yes
scripts/check-self-test-workflow-commands.mjs walkScripts(join(ROOT, 'scripts')) plus .github/workflows — a bounded subtree no

The last row is the reason the six nominated by #14325's census are five here. Its population is narrowable, so the ordinary subtree declaration is its remedy, not this channel — declaring it would put a row on every card for a gate that reads only scripts/**, which is the fabricated lead this channel exists to avoid.

The liveness predicate, and its limits

A declaration is checkable: repoRootWalkSpelling reads the gate's own source with comments and --self-test bodies masked (the same normalization payloadEnvDependence applies) and looks for a published set of repo-root walk spellings:

  • A — a git ls-files enumeration of the tracked corpus;
  • B — a walk CALLED on the repo-root binding as its whole argument;
  • C — a walk whose DEFAULT PARAMETER is the repo-root binding.

wholeTreePopulationRefusal then refuses two contradictions: a declaration with no walk behind it, and a gate carrying both markers.

Stated limits, because they change how the row should be read:

  • It is necessary, not sufficient. A gate that seeds a root walk and then filters to a subtree passes it. Nothing textual settles that; the declaration's REASON is where the human answer lives, judged by a reader — the same contract unreachableLines states for its own listing.
  • Its own failure direction is the loud one: a real whole-tree gate spelled some new way is REFUSED, which is a missing declaration somebody fixes, never a wrong row on every card. The recognised list is therefore PUBLISHED (REPO_ROOT_WALK_SPELLINGS), printed in the refusal text, and the remedy for a new spelling is to extend it with a self-test case beside it.
  • Limb B was tightened after measurement. Written to allow a trailing argument it selected resolve(REPO_ROOT, maskerPath) — a path BUILD, not a walk — and would have vouched for practically any gate holding a REPO_ROOT constant, i.e. a liveness check that cannot fail. Requiring the root to be the whole argument list restores the discrimination, and a pin holds it.

Measured over the six candidates, limbs A/B/C select exactly the five declared and none of the sixth.

What the derivation does with it

placeFamily is the ONE seam: it returns always-runs for a declaring family and otherwise delegates to classifyEntry unchanged.

  • never matched (a lead on every card is a lead on no card), never silent (a false clearance), never undetermined (its population is not unknown, it has been READ);
  • rendered under its own Always runs (declared population) heading, identical for every card, each row carrying the gate's reason and the liveness spelling that vouches for it — and a REFUSED declaration prints as refused rather than vanishing;
  • its command IS in the runnable union --commands prints, and --ran reconciles against it, because a gate every card implicates is a gate the card owes;
  • --json carries it under its own alwaysRunsPopulation key, out of matched;
  • --residue no longer lists a declaring gate as Silent, and residueLines accounts for the fourth bucket as its own partition term — a placement wired into the derivation and not into that sum would shrink the residue silently, which is what that function's throw exists to catch.

Sample, on a card that touches none of them (packages/rest/src/server.ts):

Always runs (declared population) — 5 famil(ies) DECLARE that their population is the WHOLE TREE, so every card implicates them. ...
  - pnpm check:nul-bytes   [cut-rc.yml, lint.yml, release.yml]   declared whole-tree population — the scan set is `git ls-files` PLUS ...
      ↳ liveness: its own source carries a `git ls-files` enumeration of the tracked corpus
  ...
  ⇒ Placed by DECLARATION, not by your paths: ... They are NOT leads — nothing enters or leaves the matched column because of them ...

and the residue partition moves from 21 matched / 40 undetermined / 141 silent to 21 matched / 40 undetermined / 136 silent / 5 always-runs over the same 202 discovered families.

Byte-identity for non-declaring gates

--commands replayed on the merge base (7a17f3bf1) and on this branch, over four probe paths. Every diff is a pure addition of the declaring gates' commands: no line removed, none reordered, none changed.

probe path base branch diff removals
packages/rest/src/server.ts 21 26 0
docs/adr/0112-error-code-vocabulary-and-ledger.md 5 10 0
scripts/pm/dispatch-gates.mjs 16 20 0
packages/spec/src/index.ts 39 44 0

The scripts/pm/dispatch-gates.mjs probe adds four, not five, and that is the channel behaving: pnpm check:watch-hint-literal was already in that card's union by path, so it moved from the matched block to the always-runs block and the command set is unchanged for it.

Tests

40 new self-test cases in dispatch-gates.mjs — marker parsing in both comment forms, the two markers never reading as each other, each published liveness spelling against the shape it was written for, prose and self-test-body masking, the bounded-subtree negative, both refusals, placement (declared ⇒ always-runs on four unrelated probe paths, and NOT matched even for a card editing the gate's own script, where the identity key would otherwise hit), the byte-identity of placeFamily against classifyEntry for non-declaring entries, the union and reconciliation terms, the rendering, and the residue partition throw. Plus a live half: the live tree carries at least one declaration, every one carries a non-empty reason, every one is backed by a root walk and contradicts no other marker, and no live declaring family lands in matched/silent/undetermined for any card.

node scripts/pm/dispatch-gates.mjs --self-test1283 cases pass at a7dead9d9.

Discriminating mutation, on the committed tree: deleting the one declaration line from placeFamily (mutation confirmed on disk — occurrences before=1 after=0, on-disk blob bc8c0e7b vs HEAD blob 5fbb9802, git diff --stat 1 deletion) turns 7 of 1283 cases red, and every one of them is a placement pin — unit, live-tree and end-to-end. Restored with git checkout HEAD -- ABSOLUTE_PATH under a trap; restoration proven by blob equality (5fbb9802 == 5fbb9802) and an empty git diff HEAD, not by an exit code.

Gate family derived in this worktree with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (22 families, the script deriving its own change set from the merge base rather than from a hand-written path list) and every one run: all green, except node scripts/check-test-completeness.mjs, which exits 3 = PREREQUISITE NOT MET (it grades a saved turbo run test log CI tees; there is no local log) — recorded as NOT MEASURED, not as a red, per its own verdict text.

origin/main was merged in before pushing; the union was re-derived on the merged head and is identical, and the whole union was re-run there. Every reading quoted above is from 7d9b7bd54, the merged head, except the 1283-case self-test and the mutation, which were measured at a7dead9d9 and re-run at 7d9b7bd54.

Not in this change

  • scripts/pm/bare-root-worklist.mjs untouched — its --self-test is green as-is and demanded no row for the new channel.
  • ⛔ No per-gate --self-test pin was added inside the five declaring gates. The pin that matters lives in dispatch-gates.mjs's own live half, which grades every declarer at once against the real tree; five copies of it would be five things to remember and the sixth gate to declare would carry none.
  • ⛔ No other gate logic changed.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV


Generated by Claude Code

@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 3, 2026
@github-actions github-actions Bot added the size/l label Sep 3, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 3, 2026 01:15
@baozhoutao
baozhoutao enabled auto-merge September 3, 2026 01:16
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33706452234 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   integration  test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
      ↳ 失败原因: @objectstack/cli:test: AssertionError: expected 'SIGKILL' to be null
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 38 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 3, 2026
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33712974515 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   integration  test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
      ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 49 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33712928455 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   integration  test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
      ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1800
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 2 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 50 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@claude

claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Standing down on this eviction — not re-arming (domain:devx execution seat, session session_01LAwHpn4uVuf4N1geBcD5i3, took over the seat 04:19Z).


Generated by Claude Code

This was referenced Sep 3, 2026
@os-project-manager
os-project-manager added this pull request to the merge queue Sep 3, 2026
Merged via the queue into main with commit 8ac3ed5 Sep 3, 2026
36 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-14189-whole-tree-population-channel branch September 3, 2026 09:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants