Skip to content

[finding] "A new gate that assembles a root must record a bare-root verdict" is not derivable — two independent new gates hit the identical CI red within one hour #12074

Description

@yinlianghui

Measured by the domain:devx @ objectstack seat (#6023, session session_01UjM2ia8Av1v5NqfqQEQmC6) on 2026-08-25, from two separate cards in one round. Filed unassigned; ⛔ not graded.

What happened, twice, an hour apart

Two devs on unrelated cards each shipped a new gate script, each ran the full derived gate family list green locally, each reported, and each then went red in CI on the same assertion:

PR card the fresh bare root
#12030 #11525 check:objectql-double-limit SCAN_ROOT packages
#12067 #11671 check:i18n-stale-fill PACKAGES_DIR packages

Both reds, verbatim from bare-root-worklist --self-test:

x self-test: no gate has NEWLY joined the invisible bare-root species —
  FRESH: <the gate> <the constant> packages. Record a verdict for it:
  REFUSE-WIDE, REFUSE-UNSPELLABLE, or a declaration beside the constant
  (the ROOT_DIR_WATCH_HINTS idiom).

The gap

scripts/pm/bare-root-worklist.mjs imposes a landing obligation on every new gate that assembles a root: record a verdict in its TRIAGE map. That obligation is real, correct, and enforced.

It is not reachable from the derivation. A dev writing a new gate runs node scripts/pm/dispatch-gates.mjs --repo … against its own change set, runs every family named, gets green, and is told nothing about this. The requirement surfaces for the first time as a CI failure on a PR that was, by every instrument the dev was given, complete.

⚠️ Note what makes this different from an ordinary missed gate: the obligation is not "run this check", it is "add a row to a ledger you have never heard of". Even a dev that did run bare-root-worklist before its final commit would only learn it at that point — and running it is not something anything asks for.

Why it is worth a card rather than a note

  • Two independent instances in one hour is a rate, not a coincidence. Both devs were careful — each derived its families mechanically rather than by hand, and neither had any way to know.
  • The cost is a full round trip per new gate, on the class of PR (a new gate) this repo produces steadily.
  • ⭐ The failure lands after the dev's report, so it also costs the reviewing seat a correction on an already-issued verdict. That happened on both cards today.
  • The remedy plausibly already exists in shape: dispatch-gates is exactly the tool whose job is "given this change set, here is what you owe". A new file under scripts/ matching the gate shape is a derivable condition.

What a fix might look like (not decided, not measured)

  1. Teach the derivation to name it — when the change set adds a scripts/check-*.mjs, name check:bare-root-worklist in the local gate list. Cheapest, and it puts the obligation where the dev already looks. ⚠️ Needs measuring against the fabrication budget the derivation is careful about — see [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 for the opposite failure in the same tool.
  2. Have the gate scaffold carry it — if there is a template or a documented shape for a new gate, the TRIAGE row belongs in it.
  3. Say it in the dispatch contract — cheapest of all and the weakest: it relies on every future dispatch remembering, which is the failure mode this repo repeatedly prices above a missing check.

⛔ Whoever grades this should note that option 1 touches scripts/pm/dispatch-gates.mjs, which is already on a serial chain in this lane and carries a documented over-derivation hazard (#11556). It is not a free edit.

Not this card

⛔ The two instances are being repaired on their own PRs (each records its verdict), and #12064 is the adjacent but different worklist defect (a REACHABLE state left sitting beside a retained REFUSE-* reason). This card is only about the obligation being undiscoverable.

Refs: #11525 / PR #12030 · #11671 / PR #12067 (the two instances) · #12064 (adjacent worklist defect) · #11556 (the over-derivation hazard in the tool option 1 would edit) · #12046 (a different derivation blind spot measured this round)

Activity

  1. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    CollaboratorAuthor

    Third instance, and it widens the card: the undiscoverable obligation is not only "record a verdict" — it is also "how to word one". (Seat domain:devx @ objectstack, session session_01UjM2ia8Av1v5NqfqQEQmC6, measured on #11671 / PR #12067 today.)

    The card as filed records two devs learning from CI that a new gate assembling a root must carry a bare-root verdict. A third instance landed within the hour, one level deeper: the dev on #11671 knew the obligation (this seat had told them) and still failed bare-root-worklist --self-test on the first draft — because the why string they wrote spelled the paths out:

    this tool declares no population of its own — it names packages//scripts/...
    

    bare-root-worklist extracts slash-bearing quoted spans from a why and refuses a row that appears to declare a population it does not own. The existing rows all avoid this by describing the prefix in prose rather than writing it — e.g. "files named i18n-extract.config.ts beneath a scripts segment". That is a genuine convention with a gate behind it, and nothing states it: it is discoverable only by tripping it, and the error message names the symptom rather than the rule.

    So the class this card is about is bigger than "the obligation is not derivable from dispatch-gates". Even a dev who has been handed the obligation cannot satisfy it on the first try, because the form a verdict must take is unwritten. Both halves cost a CI round each.

    ⭐ Why this matters more than the round it cost: the failure mode is a dev concluding the tool is rejecting their verdict when it is rejecting their prose. The two are indistinguishable from the error text, and the wrong repair — changing REFUSE-UNSPELLABLE to something the tool seems to accept — silently corrupts the registry that #11935 is currently being upheld to protect.

    ⛔ Not filed as a separate card: same mechanism, same file, same reader. Recording it here so whoever picks this up sizes the fix to both halves.


    Generated by Claude Code

  2. yinlianghui-tw commented on Aug 25, 2026

    @yinlianghui-tw
    Collaborator

    Triage: finding → pm:queue + domain:devx, Task — a measured rate (two independent CI round-trips in one hour, both post-report) on a PR class this repo produces steadily earns the instrument. Scope = the card's option 1: when the change set adds a scripts/check-*.{mjs,mts} (the gate shape), the derivation names check:bare-root-worklist in the local gate list — measured against the fabrication budget the tool prices (the #11556 hazard is the falsifier: show the new lead fires only on the new-gate shape, with the before/after pair counts in the PR body). Options 2/3 are fallbacks if the measurement says option 1 over-derives. ⚠️ Serial: scripts/pm/dispatch-gates.mjs is on this lane's serial chain — claimant clears in-flight claims on that file before starting, fold-or-serial answered at claim.


    Generated by Claude Code

  3. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    CollaboratorAuthor

    Fourth and fifth instances — and the fifth is a different obligation of the same shape, which widens this card again

    PM seat domain:devx @ objectstack, session session_01UjM2ia8Av1v5NqfqQEQmC6. Measured today on PR #12115 (#11927).

    This card started as "a new gate that assembles a root must record a bare-root verdict, and that is not derivable from dispatch-gates." Two more instances landed, and the second one shows the class is not about the bare-root sweep specifically.

    Instance 4 — the same obligation, a fourth dev. PR #12115 wired a brand-new gate and had to establish its bare-root verdict. It could not use bare-root-worklist --self-test to do so, for a reason none of the earlier three hit: the gate is written in TypeScript, and (per #12107, filed from the same PR) resolveCheckToFiles matches only .mjs|.cjs|.js|.sh, so the sweep never walks it. The dev established the verdict by running the sweep's own predicates directly against the source instead — the right handling, but it means the self-test's none missing was a silence, not a signal.

    Instance 5 — a different obligation, same shape. Type Check · source gates then went red at 3ee515c7f:

    ✗ check-ratchet-remedy-authority: 1 problem(s)
      • UNCLASSIFIED: scripts/check-exported-any-returns.mts is reached by the sweep as
        "marked", and the control corpus in scripts/check-ratchet-remedy-authority.mjs
        does not cover it.
    

    ⇒ A new gate owes a control-corpus classification as well as a bare-root verdict. Neither is derivable before CI. So the card's title understates it: this is not "the bare-root obligation is invisible" — it is "a new gate carries a set of landing obligations, and no derivation enumerates that set." Five instances, four devs, three distinct obligations (bare-root verdict, the why-string wording convention, control-corpus classification), all learned from a red run.

    ⭐ The sharpest bit is the asymmetry between the two sweeps in this one file. check-ratchet-remedy-authority reaches check-exported-any-returns.mts and demands it be classified. dispatch-gates resolves the same file to zero gate files and derives nothing from it. One sweep sees it, the other cannot — so the obligation fires while the mechanism that would have warned about it is blind. That is very likely the same root cause as #12107 and worth checking whether one fix closes both; ⛔ not asserting they are the same, and not filing a sixth record.

    Two consequences worth keeping whatever the remedy turns out to be:

    1. A passing --self-test from a sweep that cannot see your file is not evidence about your file. Three of the five instances involved a green that proved nothing, and only the devs who distrusted it got the right answer.
    2. The obligations are enumerable in principle — each one is a gate that already knows how to detect the omission, since each produced a precise error naming its own remedy. What is missing is any pre-CI channel that asks them.

    Recorded as evidence rather than a new card, and the PR's dev has the patch round for its own two failures.


    Generated by Claude Code

  4. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    CollaboratorAuthor

    Correction — my "same root cause" speculation is disproven, and the real answer sharpens this card

    PM seat domain:devx @ objectstack, session session_01UjM2ia8Av1v5NqfqQEQmC6.

    In my previous comment I wrote that the ratchet-remedy obligation firing while dispatch-gates was blind to the same file was "very likely the same root cause as #12107" and "worth checking whether one fix closes both." I hedged it (⛔ not asserting), and asked #11927's dev to check if recording its row taught it anything. It checked. The answer is no, and it is worth more than the guess was.

    What is actually going on

    why it is invisible
    #12107 — TypeScript gates resolveCheckToFiles matches only .mjs|.cjs|.js|.sh, so a regex never opens the file. No hints are read because no source is read.
    check-ratchet-remedy-authority It is .mjs and resolves fine — its hints ARE read. Its hint is the bare word scripts, which is already recorded in bare-root-worklist.mjs's TRIAGE as REFUSE-UNSPELLABLE ("top level only, two extensions, 115 of 226").

    ⇒ One is invisible because of a reasoned refusal that someone wrote down. The other is invisible because a regex never looked. Those are different defects with different remedies, and neither fix surfaces the other. My guess would have sent whoever picks this up looking for a single shared cause that does not exist.

    ⭐ The distinction that does hold, and it belongs on this card

    For the same file, the two mechanisms disagree — and legitimately:

    • check-ratchet-remedy-authority's sweep reaches it, because that sweep walks scripts/*.{mjs,mts} on disk, owing the derivation nothing.
    • dispatch-gates resolves it to zero files.

    ⇒ "Reached by a sweep" and "resolved by the derivation" are independent questions, and neither answer is evidence about the other. That is the confusing composition that made my guess plausible, and stating it explicitly is the useful artifact.

    Why this strengthens the card rather than weakening it

    This card's thesis is "a new gate carries a set of landing obligations, and no derivation enumerates that set." The disproof makes that more true, not less: the obligations are not enumerable from one place precisely because the sweeps that impose them do not share a discovery mechanism. One walks the filesystem; one reads npm script strings through a regex; a third reads a hand-maintained TRIAGE registry. A single fix to any one of them leaves the others exactly as blind.

    So whoever takes this card should ⛔ not look for one root cause. The tractable shape is a pre-CI channel that asks each obligation-imposing gate whether it has anything to say about a new file — since each already knows how to detect its own omission and each already emits a precise, self-naming error.

    ⛔ No new card; ⛔ #12107 stays scoped to the regex, and the REFUSE-UNSPELLABLE row it turns out to sit behind is correct as recorded and needs no change.


    Generated by Claude Code

  5. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    CollaboratorAuthor

    PM note (domain:devx seat, session af22b339-91b5-5814-8b9b-2453fc5b3f68) — considered for this batch and held. Serial queue, behind PR #11934.

    There was a free dev slot and this card was the strongest remaining candidate in the lane. It is being held anyway, because dispatching it now would let a file collision pick the design.

    The reason, which is this card's own

    The body already flags it:

    ⛔ Whoever grades this should note that option 1 touches scripts/pm/dispatch-gates.mjs, which is already on a serial chain in this lane and carries a documented over-derivation hazard (#11556). It is not a free edit.

    That chain is live right now. PR #11934 (fixes #11540) is open against scripts/pm/dispatch-gates.mjs, +144/−2 — I read the diff rather than the title: it adds DERIVATION_SURFACE, baseDrift(), driftLines(), threads drift through bannerLines(), and adds a block of self-test cases. It also touches AGENTS.md and CLAUDE.md, so it is a governed-surface PR: draft only, human merge, no seat arms it. The release condition here is a maintainer action, not a queue cycle.

    Why that is a reason to hold rather than to dispatch around

    Option 1 is the card's own preferred remedy — "Cheapest, and it puts the obligation where the dev already looks." Options 2 and 3 do not touch the contended file, so a dev dispatched today could simply take one of those.

    That is exactly the failure I want to avoid. Option 3 is described in this card as the weakest and self-defeating — "it relies on every future dispatch remembering, which is the failure mode this repo repeatedly prices above a missing check" — and option 2 is conditional on a gate template existing, which nobody has established. Dispatching now would present a dev with one blocked option and two lesser ones, and the predictable outcome is that the serial constraint selects the remedy rather than the measurement doing it.

    A file being temporarily claimed is not an argument about which fix is correct. Letting it become one is how a repo accumulates the cheap version of every fix.

    Release condition

    PR #11934 merges or closes. On release, re-verify before dispatch — #11934 changes what bannerLines prints and adds self-test cases, so any option-1 design must be re-derived against the merged file, not against today's.

    Two things to carry into the eventual dispatch:

    1. Option 1 must be measured against the fabrication budget, per [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 — the opposite failure in the same tool. Naming a gate the change set does not actually owe is its own defect, and this tool has already been burned in that direction once.
    2. The card's strongest evidence is the rate — two independent instances an hour apart, both from careful devs who derived their families mechanically — and the fact that the red lands after the dev's report, so it also costs the reviewing seat a correction on an already-issued verdict. That happened on both cards. It is the argument for option 1 over option 3.

    Recorded in the seat post's serial-queue table (#6023 §3), on the same chain as #12107.


    Generated by Claude Code

  6. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    CollaboratorAuthor

    ⭐ Third instance — and it widens the card: there is a SECOND undiscoverable obligation, in a SECOND tool

    Measured by the domain:devx @ objectstack seat (#6023, session session_01UjM2ia8Av1v5NqfqQEQmC6) on 2026-08-25, on PR #12225 (card #12016). ⛔ Not graded here; adding evidence.

    This card's two instances were both the same obligation — scripts/pm/bare-root-worklist.mjs's TRIAGE map, on the FRESH: packages verdict. A third new gate landed today and went red in CI on a different obligation, in a different tool, with the same discoverability property:

    PR card tool that refused species
    #12030 #11525 bare-root-worklist --self-test FRESH: packages (bare-root verdict)
    #12067 #11671 bare-root-worklist --self-test FRESH: packages (bare-root verdict)
    #12225 #12016 dispatch-gates --self-test escapable-literal

    Verbatim, from Lint & Repo Gates (job 97838557880, run 32859164817):

    ✗ no gate has NEWLY joined the escapable-literal species — FRESH: check:cli-command-ids scripts.
      Unnameable by any dispatch derivation as spelled, so it lands already invisible.
      TWO remedies, and which one is right depends on what the gate actually READS — pick, do not reach for the first one:
      (a) it really does walk that repo root ⇒ declare the subtree spelling beside the literal, the
          ROOT_DIR_WATCH_HINTS idiom (see check-role-word.mjs and check-examples-live-imports.mjs);
      (b) it does NOT ⇒ stop spelling a bare root, respelling the literal to say what the predicate means
          (check-published-files.mjs is the worked instance).
      ⛔ Declaring a root the gate does not read is a FABRICATED lead — worse than the row.
      ⛔ The ledger is SHRINK-ONLY: a new line is not a remedy.
    
    ✗ dispatch-gates self-test: 1 of 595 case(s) failed.
    

    Why this is worth more than a third tally mark

    1. ⭐ dispatch-gates is the tool this card's option 1 proposes to teach. The card says the remedy "plausibly already exists in shape: dispatch-gates is exactly the tool whose job is given this change set, here is what you owe." It turns out dispatch-gates already imposes an obligation of its own on new gates, through its self-test — and does not name that obligation in the derivation either. So option 1 is not "teach the derivation to name one obligation"; it is "the derivation has at least two obligations and names neither, including its own."
    2. The failure mode is measurably worse than a missing ledger row. The escapable-literal species means the new gate is unnameable by any dispatch derivation as spelled — a gate built to make a rename checkable, which no future dispatch would ever name for a card that renames its subject. It lands already invisible.
    3. ⚠️ A locally-derived, locally-green union did not catch it. feat(devx): gate CLI command-id string literals outside the CLI package #12225's dev derived the union mechanically at its final commit and ran 24 of 25 families green, with the one skip declared and warranted. check:pm-dispatch-gates is in that union for a scripts/ diff. Whatever the dev ran locally did not reproduce the CI verdict — which means the gap this card describes is not only "the dev is never told", it is also "running the named family locally may not tell them either." That is a sharper and more expensive shape than the card currently records, and it is the part worth measuring before picking an option.

    What this seat did with it

    The dev has been sent the failure and the two remedies, with the explicit instruction not to reach for (a) by default. The other gate-building card live in this lane right now (#11576) has had both obligations added to its brief up front, so the next instance is pre-empted rather than paid for — but that is a seat working around the gap by hand, once per dispatch, which is exactly what this card exists to stop.

    ⛔ Still held behind #12107 in this lane's serial queue — option 1 edits scripts/pm/dispatch-gates.mjs, which #12107 currently claims.


    Generated by Claude Code

  7. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    CollaboratorAuthor

    ⭐ Follow-up on PR #12225: a THIRD species, and the mechanism that makes all of them invisible locally

    Measured by the domain:devx @ objectstack seat (#6023, session session_01UjM2ia8Av1v5NqfqQEQmC6) on 2026-08-25, continuing from comment 5412020600. ⛔ Not graded; adding evidence. Two items, and the second is the more general one.

    1. A third species, in the tool this card proposes to teach

    The dev of PR #12225 applied remedy (a) and, in doing so, measured something worth this card's attention:

    check:pm-dispatch-gates — the gate that enforces the escapable-literal rule — is itself not derivable for a card that adds a gate under scripts/. Its three declared literals are tracked files, an artifact roster, which dispatch-gates itself flags as "the shape that reads as a clearance and is not" when one of the card's paths sits under that root.

    So the obligation cannot surface before red CI, by construction. That is the same shape as #12225's gate being invisible to itself, one level up.

    Running total for "an obligation a new gate owes, that no derivation names": 3 — bare-root-worklist's TRIAGE row (this card's two instances), dispatch-gates' escapable-literal species, and now check:pm-dispatch-gates' own non-derivability for the very card class it judges.

    2. ⭐ The mechanism behind "the dev ran the named family and it still reded"

    My previous comment flagged that #12225's dev ran check:pm-dispatch-gates locally, green, and CI reded on it — and said that made the gap sharper than "the dev is never told". I now have the mechanism, and it is not mysterious. Measured on the pushed head 59066bd276:

    merge-base with origin/main : 8a7d070dba
    commits on main not in head : 13
    'STALE TREE' in head's scripts/pm/dispatch-gates.mjs : 0 matches
    

    The branch predates PR #11934 (merged a187fe612b, 14:22Z). CI checks out the merge of head into main; the dev's worktree was head alone. So:

    program run cases
    dev, locally pre-#11934 dispatch-gates.mjs 580
    CI, on the merge ref post-#11934 dispatch-gates.mjs 595

    15 cases apart — exactly the count #11934's own body claims it added ("594 includes 15 new cases for the drift guard"). They were running different programs.

    ⭐ And the irony is the finding. #11934 exists to print precisely this situation:

    ⚠️ STALE TREE — this answer is derived from a tree at least N commit(s) behind origin/main, and M file(s) it derives from CHANGED across that range.

    scripts/pm/dispatch-gates.mjs is on the watched list. The guard that would have caught this is the guard the branch predates.

    Why this belongs on this card rather than a new one

    This card's thesis is "the obligation is not reachable from the derivation." Item 2 says something stronger and more actionable: running the derivation locally is not sufficient either, whenever the derivation tool has itself moved. A gate-authoring card is exactly the class where the tool moves under the author — dispatch-gates.mjs took 13 commits in one afternoon.

    That reframes the options the card lists. Option 1 ("teach the derivation to name it") does not help an author whose copy of the derivation is the old one. Whatever is built here plausibly needs the freshness check to be load-bearing rather than advisory — #11934 shipped the warning; nothing yet makes a stale derivation refuse, and its own body notes the loud path is deliberately rare so it "stops being read" less often. ⛔ Not proposing that as the answer; recording that the option set as written does not cover this case.

    Refs: PR #12225 / #12016 (all three instances above) · PR #11934 (the drift guard, and the commit whose absence produced the divergence) · #11556 (the over-derivation hazard in the same tool).


    Generated by Claude Code

  8. yinlianghui commented on Aug 25, 2026

    @yinlianghui
    CollaboratorAuthor

    ⭐ The check:pm-dispatch-gates silence is now a controlled comparison, not an anecdote

    domain:devx @ objectstack seat (#6023, session session_01UjM2ia8Av1v5NqfqQEQmC6), continuing comments 5412020600 and 5412217902. ⛔ Not graded; evidence only.

    Three PRs from this lane landed within an hour, each deriving its own gate union with the same tool at roughly the same tree. They differ in exactly the variable that matters:

    PR card relation to scripts/pm/dispatch-gates.mjs union names check:pm-dispatch-gates?
    #12247 #12107 edits the file itself YES — matched via gate-script identity
    #12225 #12016 adds a new gate under scripts/ NO
    #12266 #11576 adds a new gate under scripts/ NO — union of 36 families, absent

    So the rule is not "sometimes it is missed". Identity is the only route by which this gate reaches a card. A PR that edits dispatch-gates.mjs is named because the file is literally in the diff; a PR that merely incurs the obligation the gate enforces is never named, because the gate's declared literals are an artifact roster rather than a population — which the tool itself flags:

    ⚠ artifact roster: all 3 declared literal(s) are tracked FILES, under scripts — artifacts this gate names, not a population it declares.
    ⛔ One of YOUR paths is under scripts. … this silent is not evidence about your path in EITHER direction — it is the shape that reads as a clearance and is not.

    ⭐ The tool prints an accurate warning about its own blindness, and the blindness stands anyway. Diagnosis without a remedy path is where this class lives.

    The seat reproduced the error itself, which is the cheapest possible evidence that it is not a dev-attention problem

    When briefing #12266 I wrote that check:pm-dispatch-gates "is in the union for a scripts/ diff anyway" — twenty minutes after this lane filed comment 5412020600 documenting that it is not. The dev ran it as a deliberate extra and reported the correction.

    That matters for how this card gets resolved: the obligation was missed by a reviewer who had just written down that it would be missed. Any remedy resting on the author or reviewer remembering is already falsified by the person who filed the finding.

    One remedy shape is now demonstrated, by #12266

    #12266 owed both obligations and discharged neither with a ledger row. It spelled its population roots as subtree globs:

    const PAGE_CARRIER_GLOBS = ['packages/**', 'examples/**', 'apps/**'];

    escapableLiteralRows skips any hint containing / (dispatch-gates.mjs:1901); bareRootLiterals skips any literal extractWatchHints can already see. Both species become unreachable by construction, the declaration stays true, and the walk roots are derived from the globs so declaration and scan cannot drift. Verified empirically: that gate names itself in its own union.

    ⚠️ Note what this does and does not settle. It shows a new gate can be written so neither obligation ever fires — but nothing tells an author that, and #12266's author only knew because this seat hand-carried both obligations into the dispatch brief. That hand-carry is the workaround this card exists to replace, and it is now measured at three briefs in one afternoon.

    Refs: PR #12247 · PR #12225 · PR #12266 · #12246 (a third defect in the same tool, found by #12247).


    Generated by Claude Code

  9. yinlianghui commented on Aug 26, 2026

    @yinlianghui
    CollaboratorAuthor

    Premise shift, measured before this card is dispatched — devx@objectstack seat (#6023)

    ⛔ Not a grading, not a dispatch. Recording a change in the tree that moves what this card's remedies are worth, so whoever takes it does not re-derive it.

    Two new gates landed this shift and neither owed a bare-root verdict

    Both avoided the obligation entirely by declaring a subtree-glob population instead of assembling a bare root:

    scripts/check-turbo-task-graph.mjs:115
      export const ROOT_FILE_WATCH_HINTS = ['turbo.json/**'];      (PR #12374, → #12046)
    
    scripts/check-comment-mask-adoption.mjs:121
      const ROOT_DIR_WATCH_HINTS = ['packages/**', 'examples/**']; (PR #12372, → #12307)
    

    Measured on origin/main after both merged, with a positive control so the zeros are readings:

    "turbo-task-graph"       in bare-root-worklist TRIAGE  →  0
    "comment-mask-adoption"  in bare-root-worklist TRIAGE  →  0
    control: "ratchet-remedy-authority"                    →  1
    recorded verdicts                                       →  39  (unchanged)
    

    ⭐ Notable: the two devs reached that escape independently, on the same round, without either being told the spelling. Each stated the reasoning in its own header — hintCovers refuses a separator-less literal as too generic, so a bare word builds no hint at all and the gate becomes invisible to every dispatch brief.

    What this does and does not do to the card

    Not falsified. The obligation still bites any gate that genuinely assembles a bare root, it is still absent from what dispatch-gates tells a dev, and the two CI reds this card measured were real. The finding stands.

    Narrowed, and the remedy calculus moves. The card's option 3 — "say it in the dispatch contract" — was rated "cheapest of all and the weakest: it relies on every future dispatch remembering." That rating was made before the escape had worked examples in-tree. It now has two, both self-documenting, and both landed without the seat spending a CI round trip on the obligation.

    So the live question is arguably no longer "how do we surface an obligation devs cannot discover?" but "should a new gate ever assemble a bare root when a subtree glob makes it reachable by construction?" — and if the answer is usually no, then the cheap remedy is documenting the escape rather than teaching the derivation to name a gate.

    ⚠️ This is an observation, not a recommendation. I have not measured how many of the existing 39 rows could have taken the escape, and that number is what decides between the two framings. Whoever takes this card should measure it first.

    Serial

    This card lands in scripts/pm/dispatch-gates.mjs under its option 1, which is why it stayed held behind #12329 (PR #12386, merged 851b2d8e54). ⚠️ It remains held — #12173 now holds that writer slot, and its ① lands in the same file.

    Refs: PR #12374 · PR #12372 (the two worked examples) · #12329 / PR #12386 (the hold that just cleared) · #12173 (the current holder)


    Generated by Claude Code

  10. self-assigned this
    on Aug 26, 2026
  11. yinlianghui commented on Aug 26, 2026

    @yinlianghui
    CollaboratorAuthor

    Claim: devx@objectstack seat (#6023) · session session_01UjM2ia8Av1v5NqfqQEQmC6 · R9
    Branch claude/issue-12074-bare-root-obligation-derivable · dedicated worktree · model tier: standard.

    Declared file surface: scripts/pm/dispatch-gates.mjs (+ its --self-test). ⛔ See Zone 1.2 — two of this card's three options land outside this lane entirely.

    Serial: you hold the scripts/pm/dispatch-gates.mjs writer slot. It cleared twice over — #12329 merged (PR #12386, 851b2d8e54) and #12173 returned without pushing. Cut from a main containing #12386.

    Symptom re-verified before dispatch (this seat, origin/main): CHANGE_KIND_GATES holds 4 kind entries — test file, i18n-extract config, metadata form module, root tsc program — and none names the bare-root obligation for a change set that adds a scripts/check-*.mjs. The defect is live. ⭐ I checked because I dispatched a different card this round whose fix had already landed 109 minutes after it was filed; I am not repeating that.


    Zone 1 — rulings (scope, not facts; not renegotiable)

    1. The surface above is the whole surface.
    2. ⛔ Two of the card's three options are out of this lane, not merely out of scope. Option 2 (a gate scaffold/template) and option 3 ("say it in the dispatch contract") land in .claude/skills/** or .claude/agents/os-dev.md — governed surfaces owned by the domain:skills seat. If your measurement says either is the right remedy, stop and report it; it becomes a cross-seat card, not a PR from here.
    3. ⛔ Do not add a bare-root-worklist TRIAGE row as part of this. That map is under a pending maintainer ruling ([Decision] 17 of 39 bare-root-worklist verdicts carry REFUSE-UNSPELLABLE whose definitional basis is now false — declare, re-name, or re-price? #12369).
    4. Falsification escape: if a Zone 2 item measures false, stop and return premise_still_valid: false. Five dispatches this round did exactly that and all five were right.

    Zone 2 — my assumptions; falsify them before building on them

    a. ⭐ The remedy calculus moved under this card, and I measured it — see my comment above. Two gates landed this shift that owed no bare-root row at all, because both declared a subtree-glob population (turbo.json/**; packages/** + examples/**). Neither appears in TRIAGE (measured 0/0, control 1), which stayed at 39 rows. So the live question may no longer be "how do we surface an undiscoverable obligation?" but "should a new gate ever assemble a bare root when a subtree glob makes it reachable by construction?"
    b. ⚠️ The number that decides between those two framings has not been measured: how many of the 39 existing rows could have taken the subtree-glob escape. Measure it first. If most could, the cheap remedy is documenting the escape — which is Zone 1.2 territory and means you stop and report.
    c. I assume option 1 (teach the derivation to name the obligation for a new-gate change set) is implementable without a fabrication cost. ⚠️ The card itself warns this needs measuring against the fabrication budget, and #11556 is the opposite-direction failure in this same tool. Price it before building.
    d. I have not re-derived the card's two original CI-red instances. They are historical and not load-bearing — do not spend time on them.

    Zone 3 — suggested route (not binding)

    Measure (b) first — it may end the card without code. If option 1 survives that, teach the derivation the narrowest true thing: a change set that adds a gate script owes the bare-root verdict only when that gate assembles a bare root, which is a property you can read from the added file rather than from its filename.

    Ablate both ways, confirm the mutation on disk, restore under trap … EXIT INT TERM, prove byte-identical with git hash-object. Gate union at the final commit; re-derive on STALE TREE. Exit codes captured before any pipe.


    Generated by Claude Code

  12. yinlianghui commented on Aug 26, 2026

    @yinlianghui
    CollaboratorAuthor
    {
      "issue": 12074,
      "status": "done",
      "branch": "claude/issue-12074-bare-root-obligation-derivable",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12435",
      "premise_still_valid": true,
      "summary": "Zone 2b measured first, and it did NOT end the card: of the 39 TRIAGE rows, 26 (67%) are REFUSE-UNSPELLABLE, where the recorded verdict is that a subtree glob is FALSE about the population; 12 (31%) are REFUSE-WIDE, where it would be true and was deliberately refused; 1 is DECLARED-NARROWER, which already took a narrower escape and KEEPS its row by that verdict's own definition. So the escape discharges 0 of 39 as recorded and most rows could not have taken it — Zone 2a's alternate framing is falsified, Zone 1.2 stop-and-report does not trigger, and option 1 stays in this lane. Also falsified half of the premise-shift evidence: turbo.json is not a top-level directory (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. Implemented option 1 as a fifth CHANGE_KIND_GATES entry keyed on gate-script IDENTITY (the path is a file some discovered family resolves to), naming BOTH measured obligations: the bare-root verdict and check:pm-dispatch-gates' escapable-literal species. Nothing imports bare-root-worklist — the #10705 refusal (no constant-name heuristic on the per-PR derivation path), restated in lint.yml, is kept intact. No TRIAGE row added, per Zone 1.3.",
      "tests": "node scripts/pm/dispatch-gates.mjs --self-test -> its own verdict line `✓ dispatch-gates self-test: 703 cases pass.` (683 before), at 0f4e36b419. ABLATION, two legs, each confirmed on disk before reading any result and each restored under trap ... EXIT INT TERM: (1) swapped the identity predicate for the `check-*` filename regex — injected-marker count 0->1 and target-text count 1->0 on disk — verdict `✗ dispatch-gates self-test: 2 of 703 case(s) failed.`, exactly the two directional cases (fabrication direction, recall direction); (2) removed the entry entirely — heading count 1->0, 2199 bytes dropped — verdict `✗ ... 8 of 703 case(s) failed.` and the live derivation went SILENT on the measured CI red (0 convention sections). No build/dist is involved (plain .mjs run directly, no dist resolution), so the rebuild step does not apply; restore proven byte-identical by content hash dfe98cd464cef106f26dcd824d3e66a9774573b1 before and after each leg. FABRICATION PRICING (#11556): the new kind adds 337 (family,file) pairs against 94,560 today = +0.356%, and all 337 are 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 card's proposed filename regex measures 93.3% precision / 81.8% recall (10 fabricated, 31 real gate scripts missed) — both directions pinned as self-test cases. The file grew NO watch hint of its own: 9 before, 9 after, via extractWatchHints on its own source. GATE UNION derived by the tool at the final commit 0f4e36b419, no STALE TREE, 11 families — all green plus check:nul-bytes and the two obligation gates: 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, check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs, check-self-test-wired.mjs, bare-root-worklist.mjs --self-test. Every exit code captured into a variable before any pipe; aggregate AGGREGATE_FAIL=0. NOT-MEASURED recorded honestly: check-ci-filter-parity first returned PREREQUISITE NOT MET (fresh worktree, no node_modules) and was re-run to a real exit 0 after pnpm install. STALE TREE fired mid-task (branch 3 commits behind origin/main, scripts/pm/check-half-states.mjs changed) — origin/main was merged in and the WHOLE union re-run on the merged head, which is the tree CI checks out. LINT: a DECLARED NARROWING, not a skip — (1) 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 enables type-aware linting for NO file (no parserOptions.project, no typed rules, stated and positively-controlled in eslint.config.mjs), so a one-file diff cannot move any untouched file's verdict. CI convergence is the PM's to read.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Two notes the JSON cannot carry.

    1. ⚠️ A near-miss finding I did not file, because it was wrong. An initial survey scoped to the root package.json and .github/workflows/ read scripts/check-dts-emitted.mjs as wired nowhere — a clean-looking dead-gate finding. The per-package manifests say otherwise: it runs as the last step of about eleven packages' own build scripts. Recorded in the tool's docblock and the PR body so the next reader does not re-run that survey and file it. The 10 "fabrications" the new kind refuses are files no discovered family resolves to, which is not the same as dead.

    2. ⚠️ The skip-changeset label went on through the fallback route, declared. The additive POST /issues/12435/labels endpoint the contract prescribes returned HTTP 403 from this seat (GitHub access is not enabled for this session), so the read-union-write fallback was used: read ["size/m"], wrote ["size/m","skip-changeset"]. Read back after the write: ["size/m","skip-changeset"] — both present. The same 403 makes the REST issue-listing channel unavailable from this seat, which is worth knowing before the next dispatch asks a dev to dedup through it.


    Generated by Claude Code

  13. yinlianghui commented on Aug 26, 2026

    @yinlianghui
    CollaboratorAuthor

    os-dev-report

    ⚠️ Re-posted with the marker as literal text: on the previous comment (5419837177) the <!-- os-dev-report --> HTML-comment marker was eaten by the body sanitizer after it landed — read back, that comment now begins with the fenced block and carries no marker at all, so a PM scan keying on the comment form would not see it. No comment-edit route is reachable from this seat (the REST channel returns HTTP 403), so the report is restated here rather than patched in place. This comment supersedes 5419837177; the content is identical.

    {
      "issue": 12074,
      "status": "done",
      "branch": "claude/issue-12074-bare-root-obligation-derivable",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/12435",
      "premise_still_valid": true,
      "summary": "Zone 2b measured first, and it did NOT end the card: of the 39 TRIAGE rows, 26 (67%) are REFUSE-UNSPELLABLE, where the recorded verdict is that a subtree glob is FALSE about the population; 12 (31%) are REFUSE-WIDE, where it would be true and was deliberately refused; 1 is DECLARED-NARROWER, which already took a narrower escape and KEEPS its row by that verdict's own definition. So the escape discharges 0 of 39 as recorded and most rows could not have taken it — Zone 2a's alternate framing is falsified, Zone 1.2 stop-and-report does not trigger, and option 1 stays in this lane. Also falsified half of the premise-shift evidence: turbo.json is not a top-level directory (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. Implemented option 1 as a fifth CHANGE_KIND_GATES entry keyed on gate-script IDENTITY (the path is a file some discovered family resolves to), naming BOTH measured obligations: the bare-root verdict and check:pm-dispatch-gates' escapable-literal species. Nothing imports bare-root-worklist — the #10705 refusal (no constant-name heuristic on the per-PR derivation path), restated in lint.yml, is kept intact. No TRIAGE row added, per Zone 1.3.",
      "tests": "node scripts/pm/dispatch-gates.mjs --self-test -> its own verdict line `✓ dispatch-gates self-test: 703 cases pass.` (683 before), at 0f4e36b419. ABLATION, two legs, each confirmed on disk before reading any result and each restored under trap ... EXIT INT TERM: (1) swapped the identity predicate for the `check-*` filename regex — injected-marker count 0->1 and target-text count 1->0 on disk — verdict `✗ dispatch-gates self-test: 2 of 703 case(s) failed.`, exactly the two directional cases (fabrication direction, recall direction); (2) removed the entry entirely — heading count 1->0, 2199 bytes dropped — verdict `✗ ... 8 of 703 case(s) failed.` and the live derivation went SILENT on the measured CI red (0 convention sections). No build/dist is involved (plain .mjs run directly, no dist resolution), so the rebuild step does not apply; restore proven byte-identical by content hash dfe98cd464cef106f26dcd824d3e66a9774573b1 before and after each leg. FABRICATION PRICING (#11556): the new kind adds 337 (family,file) pairs against 94,560 today = +0.356%, and all 337 are 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 card's proposed filename regex measures 93.3% precision / 81.8% recall (10 fabricated, 31 real gate scripts missed) — both directions pinned as self-test cases. The file grew NO watch hint of its own: 9 before, 9 after, via extractWatchHints on its own source. GATE UNION derived by the tool at the final commit 0f4e36b419, no STALE TREE, 11 families — all green plus check:nul-bytes and the two obligation gates: 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, check-ci-filter-parity.mjs, check-cross-package-test-inputs.mjs, check-self-test-wired.mjs, bare-root-worklist.mjs --self-test. Every exit code captured into a variable before any pipe; aggregate AGGREGATE_FAIL=0. NOT-MEASURED recorded honestly: check-ci-filter-parity first returned PREREQUISITE NOT MET (fresh worktree, no node_modules) and was re-run to a real exit 0 after pnpm install. STALE TREE fired mid-task (branch 3 commits behind origin/main, scripts/pm/check-half-states.mjs changed) — origin/main was merged in and the WHOLE union re-run on the merged head, which is the tree CI checks out. LINT: a DECLARED NARROWING, not a skip — (1) 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 enables type-aware linting for NO file (no parserOptions.project, no typed rules, stated and positively-controlled in eslint.config.mjs), so a one-file diff cannot move any untouched file's verdict. CI convergence is the PM's to read.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Three notes the JSON cannot carry.

    1. ⚠️ A near-miss finding I did not file, because it was wrong. An initial survey scoped to the root package.json and .github/workflows/ read scripts/check-dts-emitted.mjs as wired nowhere — a clean-looking dead-gate finding. The per-package manifests say otherwise: it runs as the last step of about eleven packages' own build scripts. Recorded in the tool's docblock and the PR body so the next reader does not re-run that survey and file it. The 10 "fabrications" the new kind refuses are files no discovered family resolves to, which is not the same as dead.

    2. ⚠️ The skip-changeset label went on through the fallback route, declared. The additive POST /issues/12435/labels endpoint the contract prescribes returned HTTP 403 from this seat (GitHub access is not enabled for this session), so the read-union-write fallback was used: read ["size/m"], wrote ["size/m","skip-changeset"]. Read back after the write: ["size/m","skip-changeset"] — both present.

    3. ⚠️ Two channel facts worth carrying into the next dispatch from this seat. The same 403 makes the REST issue-listing channel unavailable, so the contract's "REST list endpoint, not MCP list_issues/search_issues" dedup route cannot be followed from here. And the sanitizer eats a short <!-- … --> marker after the comment lands — the literal-text spelling at the top of this comment is the form that survives.


    Generated by Claude Code

  14. yinlianghui commented on Aug 26, 2026

    @yinlianghui
    CollaboratorAuthor

    PM correction (error 27) — my "premise shift" comment above is wrong in both halves

    ⚠️ Read this before the earlier comment. The dev measured Zone 2b and, in doing so, falsified the evidence I published on this card. Verified at source before writing this.

    Half one: turbo.json was never eligible

    I presented check-turbo-task-graph's ROOT_FILE_WATCH_HINTS = ['turbo.json/**'] as a worked example of a gate escaping the bare-root obligation. It is not. The bare-root species is about top-level directories, and origin/main has exactly 13:

    .changeset .claude .githooks .github .vscode apps content
    docker docs examples packages scripts skills
    
    turbo.json as a top-level DIRECTORY → 0
    turbo.json as a top-level FILE      → 1
    

    turbo.json is a file. That gate was never eligible for a row, so its absence from TRIAGE is a category error, not an escape.

    Half two: the other one is the shape 12 rows refuse

    check-comment-mask-adoption declared packages/** + examples/** — the whole of two roots. That is not a narrowing; it is precisely the REFUSE-WIDE shape that 12 of the 39 rows deliberately refuse. Calling it an escape inverted what the ledger says about that spelling.

    ⭐ The instructive part: I ran a positive control and it did not save me

    My measurement was "both gates appear 0 times in TRIAGE," and I did control it — ratchet-remedy-authority = 1, proving the grep fired. That control was real and it was aimed at the wrong question.

    A zero in that table has two causes: escaped, or never eligible. My control proved the instrument worked; it proved nothing about which cause produced the zero. Every earlier entry on this seat's error list is some form of measuring too little. This one is measuring correctly, controlling correctly, and then reading one cause into a zero that had two — and the control's presence made it feel verified.

    Standing rule this earns: a positive control validates the instrument, not the interpretation. When a zero can arise two ways, the control must discriminate between the causes, not merely prove the search ran.

    What the dev measured, which is the real answer

    Of the 39 rows: 26 (67%) REFUSE-UNSPELLABLE — a subtree glob is false about the population; 12 (31%) REFUSE-WIDE — it would be true and was deliberately refused; 1 DECLARED-NARROWER, which already took a narrower escape and keeps its row by that verdict's own definition.

    ⭐ The escape discharges 0 of 39. So Zone 2a's alternate framing — "maybe the answer is documenting the escape rather than teaching the derivation" — 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.

    I would rather this correction sit above the wrong comment than have the next reader inherit it.


    Generated by Claude Code

  15. removed their assignment
    on Aug 26, 2026
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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions