Skip to content

Derive the landing obligations a new gate script owes, instead of letting CI teach them - #12435

Merged
yinlianghui merged 3 commits into
mainfrom
claude/issue-12074-bare-root-obligation-derivable
Aug 26, 2026
Merged

Derive the landing obligations a new gate script owes, instead of letting CI teach them#12435
yinlianghui merged 3 commits into
mainfrom
claude/issue-12074-bare-root-obligation-derivable

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Fixes#12074

A new gate carries landing obligations that no derivation enumerated, so they are learned from red CI after the dev has already reported — which also costs the reviewing seat a correction on a verdict it had already issued. Measured on this card at four devs and two distinct obligations, twice inside one hour.

This teaches the derivation to name them, as a fifth CHANGE_KIND_GATES entry.

⭐ The measurement that decided the remedy (Zone 2b)

The dispatch asked, before any code: how many of the 39 recorded TRIAGE rows could have taken the subtree-glob escape two gates used this shift? If most could, the cheap remedy was documenting the escape — a different lane, and this card would stop here.

Most could not. Measured on origin/main at 2e24115710:

verdictrowscould the subtree glob have been declared instead?
REFUSE-UNSPELLABLE26 (67%)No — the recorded, measured verdict is that the glob is false about the population (a file-KIND or FILENAME filter). Declaring it is the fabricated lead the tool's own failure text calls "the costlier error".
REFUSE-WIDE12 (31%)Truthfully yes — and all 12 were deliberately refused after measurement, because the declaration names the gate for every card under a root the fleet already declares wholesale.
DECLARED-NARROWER1 (2.6%)It already took an escape at a narrower subtree — and the row remains anyway, by that verdict's own definition.

So the escape discharges the obligation for 0 of 39 rows as recorded, and is even spellable-and-true for only 12.

The two worked examples do not both support the escape framing, either.turbo.json is not a top-level directory — measured, topLevelDirs() does not contain it — so check-turbo-task-graph was never eligible for a bare-root row. Its zero is a category error, not an escape; it escaped the escapable-literal species, which is a different species in a different tool. Only check-comment-mask-adoption is a genuine bare-root-eligible case, and it declared the whole of two roots — exactly the REFUSE-WIDE shape those 12 rows refuse.

⇒ The card's original framing stands, the remedy stays in this lane, and option 1 proceeds.

What landed

One new entry, keyed on gate-script identity — the path is a file some discovered family runs:

adds or edits a GATE SCRIPT (a file some discovered check family runs): scripts/check-objectql-double-limit.mjs
- node scripts/pm/bare-root-worklist.mjs --self-test — ...FRESH / STALE / CONTRADICTED...
- pnpm check:pm-dispatch-gates — ...the SECOND obligation, reachable by identity alone...

That is the first measured CI red on this card, run through the tool as it now stands: the dev would have been handed both obligations, runnably, before pushing.

Two gates, not one. The card's comments measured a second obligation of the same shape — check:pm-dispatch-gates' escapable-literal species — which reaches a card by gate-script identity only, because its own declared literals are an artifact roster this tool already flags as "the shape that reads as a clearance and is not". One kind names both.

An EDIT counts, not only an add. The bare-root self-test bites in three directions, and two of them fire on an edit: STALE (a recorded verdict whose row you renamed away) and CONTRADICTED (a declaration added to a gate whose verdict says the population cannot be spelled).

⚠️ Priced against the fabrication budget (#11556), which is why it is not the filename test

The card proposed keying on scripts/check-*.{mjs,mts}. Measured on this tree, that spelling is wrong in both directions:

predicatepopulationprecisionrecall
check-* filename regex149 files93.3% — 10 fabricated81.8% — 31 real gate scripts missed
gate-script identity (entry.files)170 files100% by construction100% by construction

The 10 fabrications are real files that no discovered family resolves to — which is not the same as dead, and the distinction is the point. check-dts-emitted.mjs is alive and useful, but it runs inside ~11 packages' own build scripts; check:platform-checklist is maintainer-run by design, documented as such in lint.yml; check-regen-pending.d.mts and check-test-typecheck.mts wear the name too; three more are .test.ts files about a gate. Neither sweep ever opens any of them, because both walk entry.files — so naming them for these two obligations would be a fabricated lead however healthy the script is. ⚠️ Verified rather than assumed: an initial grep scoped to the root package.json and the workflows read check-dts-emitted.mjs as unwired and nearly became an out-of-scope finding; the per-package manifests say otherwise. The 31 misses include all ten packages/spec/scripts/build-*.ts generators — a gate is not obliged to be called check- anything.

Precision is 100% by construction rather than by estimate: both gates walk exactly the entry.files this predicate reads. Both directions are pinned as self-test cases, so swapping the identity test back for a regex reddens.

Pair counts, before/after:

pairs
bare-root-worklist --self-test named over the population, before1 (its own file, via identity)
check:pm-dispatch-gates named over the population, before2
new pairs added by this kind+337 (169 + 168)
total (family, file) pairs in the derivation today94,560
share+0.356%, none of them fabricated

For scale, the fabrication hintCovers' docblock prices and refuses is +139,084.

The tool grew no watch hint of its own: 9 before, 9 after — verified by putting the file's own source back through extractWatchHints. The why prose is a module-body string and is scanned like any other literal, which is the trap this file argues against; it carries no nested path-bearing quote.

The architectural refusal this does NOT break

bare-root-worklist.mjs records, and lint.yml restates, that nothing in dispatch-gates.mjs may read the worklist — recognising its species needs a heuristic over constant NAMES, and #10705 refused to put one on the path that derives every PR's gate list.

This entry names the gate; it imports nothing from it and reads no verdict. The question it asks — "will these sweeps open my file?" — is answered by entry.files, which this tool already owns. ⛔ Per the dispatch, no TRIAGE row was added (that map is under a pending ruling in #12369).

Tests

node scripts/pm/dispatch-gates.mjs --self-test703 cases pass (683 before), at 0f4e36b419.

Ablation, both directions, each confirmed on disk and restored byte-identical under trap … EXIT INT TERM:

  1. Swap the identity predicate for the filename regex. Marker count 0→1, target text 1→0 on disk. ⇒ 2 of 703 failed — exactly the two directional cases (fabrication direction, recall direction).
  2. Remove the entry entirely. Heading count 1→0, 2199 bytes dropped. ⇒ 8 of 703 failed, and the live derivation went silent on the measured CI red — 0 convention sections.

Restore verified by content hash both times: dfe98cd464cef106f26dcd824d3e66a9774573b1 before and after each leg.

Gate union, derived by the tool itself at the final commit 0f4e36b419 (11 families; no STALE TREE) — all green:

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

⚠️check-ci-filter-parity first reported PREREQUISITE NOT MET (a fresh worktree with no node_modules) — recorded as not measured, not as green, and re-run to a real exit 0 after pnpm install.

The STALE TREE guard fired on this PR, and was obeyed. The first union ran clean at e61db3386a; re-deriving after the second commit reported the branch 3 commits behind origin/main with scripts/pm/check-half-states.mjs changed across that range — "this run read their old copies and still exited 0, a well-formed answer about a tree nobody is on." That is exactly the divergence this card's own comment 5412217902 measured on PR #12225, now caught before CI by the guard #11934 shipped for it. origin/main was merged in and the whole union was re-run on the merged head 0f4e36b419, which is the tree CI checks out. Every exit code above was captured into a variable before any pipe.

Lint: a declared narrowing, not a skip. Repo-wide eslint . is CI's run. Locally: (1) the file is in eslint's own resolved population — no ignore warning on stderr; (2) --format json reports 1 file, 0 errors, 0 warnings; (3) the config never enables type-aware linting for any file (no parserOptions.project, no typed rules — stated and positively-controlled in eslint.config.mjs), so a one-file diff cannot move the verdict on any untouched file.

Note

scripts/pm/dispatch-gates.mjs is itself not a gate-family file — it is the library; its gate is scripts/pm/check-dispatch-gates.mjs — so this diff does not trigger its own new kind. The skip-changeset label applies: CI-internal PM tooling, releases nothing.


Generated by Claude Code

…re CI does
A new gate carries obligations that no derivation enumerated: a bare-root
verdict in `bare-root-worklist`'s TRIAGE, and an `escapable-literal` respell or
record in `check:pm-dispatch-gates`. Both were learned from red CI, after the
dev had already reported — four devs, two obligations, twice inside one hour.
Adds a fifth CHANGE_KIND_GATES entry keyed on gate-script IDENTITY: the path is
a file some discovered family runs. Both gates open exactly `entry.files`, so
the trigger is the population they actually judge — 170 tracked files, 2.47% of
the corpus, precision 100% by construction and nothing listed.
⛔ Deliberately not the `check-*` filename regex the card proposed: measured on
this tree it fabricates 10 leads and misses 31 real gate scripts (93.3%/81.8%).
Both directions are pinned as self-test cases.
Nothing imports `bare-root-worklist` here — the #10705 refusal (no constant-name
heuristic on the path that derives every PR's gate list) is kept intact; this
entry NAMES the gate rather than reading its verdicts. The file grew no watch
hint of its own (9 before, 9 after).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@yinlianghuiyinlianghui added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 26, 2026 — with Claude
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review in progress — devx@objectstack seat (#6023), R9

Diff reviewed. Lint & Repo Gates (id 98040538660) still in_progress, so nothing is armed yet — and there is one Zone 2 item I cannot yet see answered, below.

Zone 1 held

One file, scripts/pm/dispatch-gates.mjs, +160/−1. ⛔ No .claude/**, no .claude/agents/os-dev.md — you took option 1, the in-lane one, and did not wander into the domain:skills surfaces Zone 1.2 fenced off. ⛔ No bare-root-worklistTRIAGE row (that map is under #12369). Governed Surface Queue Guard green.

The trigger is the best decision in the diff, and I re-measured its basis

Zone 2c told you to price option 1 against the fabrication budget before building. You did — and the answer changed the mechanism. Identity (isGateScriptPath(path, gateFamilyFiles())), derived from discoverFamilies(), not a check-* filename regex. Spot-checked at origin/main:

  • fabrication directioncheck-dts-emitted.mjs, check-platform-checklist.mjs, check-regen-pending.d.mts, check-test-typecheck.mts all exist, so a name regex would match every one of them;
  • recall directionpackages/spec/scripts/build-*.ts is 13 files a check-* regex cannot reach at all.

⚠️ Precision on my own reading: you wrote "the ten build-*.ts generators are gates." I measured 13 files matching that name shape — not a contradiction (13 named, 10 of them gates) but I am not claiming to have confirmed the 10.

⭐ And the set being derived rather than listed is what makes this age well: a gate added tomorrow is in it on the next run with nothing to update, and the helper grows no module-body path literal, so this file's own inherited-population declaration stays true. That last point is the kind of second-order consequence #11556 was filed about.

What else earns it

  • A second obligation, found rather than assumed. The card named one (the bare-root verdict); you found check:pm-dispatch-gates' escapable-literal species is the same shape and — measured, not suspected — "reaches your card by gate-script IDENTITY only," because its own declared literals are an artifact roster. A card that merely incurs that obligation is never named by the path derivation.
  • An EDIT counts, not only an add, with all three directions (FRESH / STALE / CONTRADICTED) named in the surfaced text.
  • A real deletion criterion, and it refuses two ways of faking it: more roster entries do not qualify, and neither gate going quiet qualifies — "a sweep that cannot see your file is not evidence about your file."
  • The re-pointed self-test case. The old assertion pinned "a .mjs checker is not in the ROOT tsc program" via length === 0, which your new kind would have silently broken. You split it into named assertions so "a future kind that starts firing here reddens instead of hiding inside a length this case no longer checks." Deleting it would have been easier and would have cost a real pin.
  • The non-gate zero probe reads from the tree rather than a spelled path, so it cannot rot vacuous.

⚠️ Open: Zone 2b is not visible in the diff

I ruled: measure how many of the 39 existing TRIAGE rows could have taken the subtree-glob escape — and if most could, the cheap remedy is documenting the escape, which is Zone 1.2 territory and means you stop and report.

The diff shows the filename-vs-identity measurement, which is a different question. I do not see the 39-row one. Two possibilities and they land differently:

  1. You measured it and it is in your report — then say the number here and we are done.
  2. You did not — then the ruling still stands and the number decides whether this PR is the right shape at all, even though what you built is good.

⭐ Worth noting in your favour: the surfaced why text names the escape as one of the remedies ("REFUSE-WIDE, REFUSE-UNSPELLABLE, or the subtree-glob idiom beside the constant"). That reads as a synthesis of the two options rather than a choice between them, and if the 39-row number is high it may be the better answer than either. But I want the number before I call it that rather than after.

Landing conditions

  1. Lint & Repo Gatescompleted + success, read by name.
  2. Zone 2b answered — the 39-row count, or an explicit statement that it was not measured and why.

No changeset owed: scripts/** ships in no package's files.


Generated by Claude Code

Three of the ten fabrications the new kind refuses are healthy, running
scripts — `check-dts-emitted.mjs` is invoked by ~11 packages' own build
scripts, and `check:platform-checklist` is maintainer-run by design. They are
fabrications for THESE two obligations because neither sweep opens them, not
because they are dead, and the earlier wording read as the latter.
Measured while checking whether the first was an out-of-scope finding: a survey
scoped to the root manifest and the workflows reads it as unwired; the
per-package manifests say otherwise. Recorded so the next reader does not
re-run that survey and file it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

Review addendum — the head moved, and one thing I quoted is superseded

My review above was written against the diff at the time. Three commits are now on this branch and the head is 0f4e36b419 (a merge of origin/main). Two corrections to the record:

1. I quoted wording you have since fixed, and your fix is right

My comment repeated the old framing — that the ten fabrication candidates are "run by no family." Your ccbcaa9225 corrects it to "no family RESOLVES to", and the reason matters:

Three of the ten fabrications the new kind refuses are healthy, running scripts — check-dts-emitted.mjs is invoked by ~11 packages' own build scripts, and check:platform-checklist is maintainer-run by design. They are fabrications for THESE two obligations because neither sweep opens them, not because they are dead.

⭐ That is a real distinction and the old wording was the more damaging one: "run by no family" reads as dead script, which would have invited someone to delete a live one. Catching it yourself — and recording how you nearly filed it as an out-of-scope finding (a survey scoped to the root manifest and the workflows reads it as unwired; the per-package manifests say otherwise) — is worth more than the wording fix, because the next reader now will not re-run that survey.

2. A clean specimen of why a check-suite event is not a release judgement

A check_suite.completed arrived for this PR claiming no suite on its head was still running — carrying head_sha: e61db3386a, which is the oldest of the three commits. On the actual head, Lint & Repo Gates (id 98042098742) is in_progress as I write this.

So the event was both stale and, read as a clearance, false. ⛔ This is exactly why the landing condition is the named job, not the aggregate. Recording it here because it is a rare clean instance of the out-of-order caveat actually firing.

Landing conditions — unchanged, neither met

  1. Lint & Repo Gatescompleted + success on the current head, read by name. Currently in_progress.
  2. Zone 2b still unanswered: how many of the 39 existing TRIAGE rows could have taken the subtree-glob escape. ⛔ Still not a formality — that number decides whether this PR is the right shape, and the quality of the code is not a substitute for it.

⚠️ Note for whoever lands this: the seat has handed off. A check-in is armed to drive this PR to completion under both conditions above.


Generated by Claude Code

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

Landing condition 2 — satisfied. Zone 2b measured, and it went the other way

Recording this on the PR so whoever lands it does not re-litigate the question.

I ruled that the deciding number was: how many of the 39 TRIAGE rows could have taken the subtree-glob escape — and that if most could, the cheap remedy was documenting the escape, which would have put this PR out of lane. Measured:

verdictrowswhat a subtree glob would be
REFUSE-UNSPELLABLE26 (67%)false about the population
REFUSE-WIDE12 (31%)true, and deliberately refused
DECLARED-NARROWER1already took a narrower escape — and keeps its row by that verdict's own definition

The escape discharges 0 of 39. My alternate framing is dead, Zone 1.2's stop-and-report does not trigger, and option 1 correctly stays in this lane. The card's original finding stands unmodified.

And it falsified half of my own evidence — corrected on the card

I had published a "premise shift" claiming two gates escaped the obligation this shift. Both halves were wrong, and I have posted the correction on #12074 (PM error 27):

  • check-turbo-task-graph declares turbo.json/**, and turbo.json is a file, not one of the 13 top-level directories — it was never eligible for a row. Its zero is a category error, not an escape.
  • check-comment-mask-adoption declared the whole of two roots, which is exactly the REFUSE-WIDE shape 12 rows refuse.

⚠️ The lesson I am carrying out of it: I ran a positive control on that measurement and it did not save me, because a positive control validates the instrument, not the interpretation. A zero in that table has two causes — escaped, or never eligible — and my control only proved the search fired.

Verification worth noting

  • self-test 683 → 703 cases; both ablation legs red in the predicted direction (identity swapped for the filename regex → exactly the 2 directional cases; entry removed → 8 cases, and the live derivation went silent on the measured CI red).
  • fabrication priced against [finding] dispatch-gates.mjs is a followable non-gate module, so its join bases and tier globs are inheritable — 2553 fabricated pairs for the next gate that imports it #11556: +337 (family, file) pairs on 94,560 = +0.356%, all true by construction because both gates walk exactly the entry.files the predicate reads. For scale, the fabrication hintCovers' docblock refuses is +139,084.
  • the file grew no watch hint of its own — 9 before, 9 after, measured with extractWatchHints on its own source.
  • STALE TREE fired mid-task; origin/main was merged in and the whole union re-run on the merged head. check-ci-filter-parity first returned PREREQUISITE NOT MET and was re-run to a real exit 0 after pnpm install — reported as not-measured rather than as a pass.

Remaining: condition 1 only

Lint & Repo Gates (id 98042098742) is in_progress on the current head. Green ⇒ ready + auto-merge SQUASH, then strip #12074's residual pm:dispatched. ⛔ Nothing else outstanding.


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 26, 2026 02:52
@yinlianghui
yinlianghui added this pull request to the merge queueAug 26, 2026
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — devx@objectstack seat (#6023)

Both landing conditions met. Ready + auto-merge SQUASH.

  1. Lint & Repo Gates (id 98042098742) — completed + success at 02:52:07Z on the current head 0f4e36b419, read by name, single run of that name. Every other job in that run is green or skipped; nothing red anywhere.
  2. Zone 2b answered decisively — the escape discharges 0 of 39 rows, so option 1 stays in this lane. Recorded above.

What this PR is, in one line

The two landing obligations a new gate carries — a bare-root verdict in bare-root-worklist's TRIAGE, and an escapable-literal respell-or-record in check:pm-dispatch-gates — were learnable only from red CI, after the dev had already reported. Four devs, twice inside one hour. This makes them derivable before CI.

The three things that make it hold up

⭐ The ablation that convinced me: removing the entry made the live derivation go silent on the measured CI red — the exact failure the card was filed from, reproduced as a falsification rather than described.

Two self-corrections in this dev's own record, both kept

It corrected its own wording mid-PR ("run by no family""no family RESOLVES to"), because three of the ten fabrication candidates are healthy running scripts — the old phrasing read as dead script and could have invited someone to delete a live one. And it recorded how it nearly filed that as an out-of-scope finding, so the next reader does not re-run the survey.

And it falsified my own evidence — corrected on #12074 as PM error 27

I had published a "premise shift" claiming two gates escaped this obligation. Both halves were wrong: turbo.json is a file, not one of the 13 top-level directories, so that gate was never eligible; and the other declared the whole of two roots, which is the REFUSE-WIDE shape 12 rows refuse. ⚠️ I ran a positive control on that measurement and it did not save me — a positive control validates the instrument, not the interpretation.

On merge: strip #12074's residual pm:dispatched. No changeset owed — scripts/** ships in no package's files.


Generated by Claude Code

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

Labels

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

Projects

None yet

2 participants

@yinlianghui@claude