tooling(pm): measure the scheduled-only routing question and pin its zero - #15236
Merged
Conversation
…zero (#14899) The card reported that dispatch-gates derives the live half-state board sweep into the per-PR gate family for any diff carrying a changeset, on the reading that its only caller is a `schedule`-triggered workflow, and asked for a general "scheduled-only, not a PR gate" class before anything was built. Measured first, from the workflow text at 50d6c92: of 30 workflows, 15 declare `schedule:` and 8 of those contribute a discovered check family; of 251 discovered families, 21 reach a scheduled workflow and 10 are reached ONLY through scheduled workflows — and ZERO are scheduled-only, because every patrol here declares a `pull_request:` trigger with a `paths:` filter naming its own script, so changes to a patrol are exercised before they merge. The card's own specimen already carried that trigger on the day it was filed, and three of the ten are `validate-deps.yml`'s, including `check:override-consistency`. The zero does not depend on how wide PR-time is drawn: narrowing it to `pull_request` alone leaves the same zero. So the class is not shipped — an empty classification is a capability with nothing in it. What ships is the reading that makes the deferral honest: `declaredTriggerEvents` re-takes the measurement from the workflow text on every `--self-test`, so the day a family really is scheduled-only the pin reds on the PR that creates it, and its case names the two exits. No derivation consumes the reader, and `--commands` is byte-identical for every path: the cost the card measured is separately gone, because #15083 classified that invocation value-bearing off its `$PROVENANCE` argv. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
…14899) `main` moved five commits under the branch and added one check family, so the header's table is re-measured rather than left describing the pre-merge tree: 252 discovered families, not 251. Every other row is unchanged, and the answer is unchanged — ZERO scheduled-only families. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
This was referenced Sep 4, 2026
os-steve
marked this pull request as ready for review
September 4, 2026 07:45
os-steve
enabled auto-merge
September 4, 2026 07:45
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #14899
The card reported that
dispatch-gatesderives the live half-state board sweep into the per-PR gate family for any diff carrying a changeset, on the reading that the sweep's only caller is aschedule-triggered workflow that no PR runs. It deliberately left the repair undecided and asked for a measurement first. This PR is that measurement, and the answer it selects.1. Measured, from the workflow text at
fa8c1963Every workflow's
on:block classified by its declared events, then every discovered family classified by the events of the workflows that invoke it.The ten candidates, by command and workflow, with the trigger that resolves each and the answer to "is this one a dev SHOULD run on a PR", read from the family's own source:
on:eventsscripts/check-engine-split-ratio.mjs --days 90engine-split-metric.ymlpull_requestpaths: the script + the workflowscripts/pm/check-half-states.mjs --format=markdown --provenance="$PROVENANCE"half-state-patrol.ymlpull_requestpaths: the sweeper,scripts/invoked-as.mjs, the workflow--commandsas VALUE-BEARINGscripts/check-prerelease-pin-watch.mjs --self-testprerelease-pin-watch.ymlpull_requestpaths: the script, the workflow,pnpm-workspace.yamlscripts/check-prerelease-pin-watch.mjs --verboseprerelease-pin-watch.ymlscripts/check-release-section-coverage.mjs --strictrelease-coverage-patrol.ymlpull_requestpaths: the script + the workflowscripts/render-release-coverage-anchor.mjs --self-testrelease-coverage-patrol.ymlscripts/check-required-contexts.mjs --verify-required-setrequired-set-patrol.ymlpull_requestpaths: the script + the workflowcheck:override-consistencyvalidate-deps.ymlpull_requestpaths incl.**/package.json,pnpm-workspace.yaml, the scriptscripts/check-osv-exemptions.mjs --self-testvalidate-deps.ymlscripts/check-osv-exemptions.mjsvalidate-deps.ymlAll ten dissolve on one deliberate repo posture, stated in the patrols' own comments: every patrol here declares a
pull_request:trigger with apaths:filter naming its own script, so "Changes to the patrol itself get exercised before they merge" (half-state-patrol.yml, and the same words inrequired-set-patrol.yml,release-coverage-patrol.yml,merged-branch-reaper.yml). The card's own specimen already carried that trigger on the day the card was filed - read atafacebb6(2026-08-31), the newest commit to that file at or before 2026-09-03T08:42Z:So the premise "it is not triggered by a PR" was already false when the card was written. What was true, and is the reason the card was worth filing, is the cost: the bare
node scripts/pm/check-half-states.mjsline really was in--commandsand really did sit for 3m09s.The reading does not depend on where the PR-time line is drawn. Narrowing
PR_TIME_TRIGGER_EVENTSfrom {pull_request, pull_request_target, merge_group, push} topull_requestalone leaves the same zero, because all ten are reached through apull_requesttrigger specifically.The honest complement, so the zero is not the union quietly hiding a member: exactly four families are reached by no PR-time event at all, and none of them is scheduled - all four come from
cut-rc.yml, the human release lane, which isworkflow_dispatch-only. Three are already withheld as VALUE-BEARING (--base "$SNAPSHOT_SHA"); the fourth,node scripts/check-changeset-no-major.mjs --self-test, is a runnable command in--commandsfor a changeset diff, and is one a dev should run anyway - measured at 0.85s, offline, no API, and it proves the parser for the very file the diff adds ("116 assertions ... frontmatter dialects measured against @changesets/parse").2. The shape taken: (c), plus the pin that makes it honest
(a) is refused as an empty classification. It has zero members on this tree, so shipping a "scheduled-only, not a PR gate" class - a row field, a labelled heading, a
--jsonfield, a--ranbucket and a third--commandssubtraction - would be a capability with nothing in it. That is the same refusalextractTriggerPathsalready records forpaths-ignore:two sections up in the same file: "Speculative capability is what this repo's own review standard rejects ... when one does and its families matter, model it then." The precedence question the ruling asks about - what a family that is both value-bearing and scheduled-only would report - is moot for the same reason: no class is shipped, and the one family that would have been the specimen is already withheld by the value-bearing class, which is the ordercommandsForstates (CI-measured first, then value-bearing; a scheduled-only class would have been the third and last subtraction).(c) alone would leave the deferral unwatched, so it is not shipped alone. A deferral is only honest while its population stays empty, and nothing was measuring that. So what ships is the reading:
declaredTriggerEvents(workflowText)- the third of the file's three narrowon:walkers, besideextractTriggerPathsanddeclaresPullRequestTriggerso the trio cannot drift. Dependency-free indentation walk, like its two siblings.PR_TIME_TRIGGER_EVENTS- the reviewable definition of "in front of a pull request", in one place.--self-testblock that re-takes the whole measurement from the workflow text on every run, so the day a family really is scheduled-only, the pin reds on the PR that creates it. Its case names the two exits: give the workflow thepull_requestpaths trigger every patrol here already carries, or ship the class the header defers. Editing the pin's expectation is not one of them - the zero is a reading, not a roster.No derivation consumes the reader.
entry.scheduledOnlydoes not exist,commandsForis untouched, and no row gains a field. The measured cost the card reported is separately gone: PR #15114 classified that invocation VALUE-BEARING off its$PROVENANCEargv, so the sweep left--commandswith no scheduled-only rule existing.Four-axis reasoning
check:override-consistency- a loud tightening in the wrong direction. The pin instead makes the silent direction loud: a scheduled-only family arriving tomorrow currently produces no signal anywhere, and afterwards produces a red on the PR that introduces it, with the remedy named in the case text rather than left to be re-derived.2026-08-04: 「我们是一个创业项目,应该先专注于核心能力」. An unpulled declaration surface is handled implementation-first; a class with zero members is exactly that. Nothing staged, nothing dual-spelled, no grace window: the class is simply not built until something is in it.3. Rendering before/after: unchanged, and proven
The ruling asks for the rendering before/after on a changeset path. It is byte-identical, which is the intended result - this PR moves no output. Measured by running the tool from a worktree at
origin/main(9c1bcda3) and from this branch's head, on the card's own probe path:The two half-states spellings in that rendering, unchanged on both sides:
The offline half
lint.ymlruns on every PR stays a runnable command; the live sweep stays out of--commandson the value-bearing class. Both are pinned below.4. Self-test cases (18 added; battery now 1371)
Extractor fixtures, shaped from the real
on:blocks in this tree:on:mapping's events are read in declaration orderpush:underpull_request:)jobs:is not readtypes,paths,branches) never enter the list'on',"on",true)on:block yields an empty list, not a fabricated eventAgainst the real workflows, read from the tree rather than pasted (a quoted copy of a workflow is a second revision of it waiting to rot):
pull_requesttrigger beside its schedulepull_requestto everything (stale.yml) - the control that keeps case 10 from being satisfied by a broken readerLive, non-vacuity first:
pull_requestalone leaves the same zeroThe control the ruling names - a family with a PR-time trigger keeps its class - plus the half this card must not move:
check:pm-half-statesreacheslint.yml, carries neither withholding class, and still renders a runnable commandThe pin is proven to be able to red
Reverse verification, from the committed state, with a restore trap and both legs proven on disk. The mutation removed the
pull_request:trigger block fromhalf-state-patrol.yml, making its family genuinely scheduled-only:The ablation ran at
45a9153b, before themainmerge, which is why its output says 251 families rather than 252. Five of the eighteen turn red and thirteen stay green, so the pin discriminates rather than reacting to any edit; the complement case correctly re-counts 5 no-PR-time families instead of 4. No build leg exists for this ablation and none is claimed: the tool reads.github/workflows/*.ymland the gate sources from the working tree at runtime, so there is nodist/between the mutation and the reading.5. Gates - verification run at head
ff0c7a56Derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(no paths) after the final commit. Each exit code captured before any pipe; each row quotes the gate's own verdict line.node scripts/check-ci-filter-parity.mjsOK: all 143 declared cross-package glob(s) (99 unique) are covered ...node scripts/check-closing-keyword-parity.mjscheck-closing-keyword-parity: OK (3 parsers agree on all 9 keywords ...)node scripts/check-closing-keyword-parity.mjs --self-testcheck-closing-keyword-parity --self-test: 24 assertions, 5 mutations ... driven to red.node scripts/check-comment-mask-corpus.mjscomment-mask corpus sweep: 5874 files, 0 disagree, 0 unparseable, 64.6snode scripts/check-self-test-wired.mjscheck-self-test-wired: every one of the 168 script(s) CI runs that ship a --self-test has that self-test run by CI.node scripts/check-self-test-wired.mjs --self-test4 live ledger row(s) verified ... every battery at or above its pinned floor.node scripts/check-whole-set-label-write.mjscheck-whole-set-label-write: 0 violations - 260 file(s) over 3 root(s) ...node scripts/check-whole-set-label-write.mjs --self-testall cases pass (24 fixture trees + 5 refusals + 1 allowlist hatch)pnpm check:agent-test-spellingcheck-agent-test-spelling: 0 violations - 442 file(s) ... 9 separator(s) JUDGEDpnpm check:bash32-floor26 tracked shell file(s) ... name no bash 4+ constructpnpm check:cli-command-ids329 command-id literal(s) across 116 file(s) ... all resolve to a real command pathpnpm check:cross-package-test-inputsOK: 26 package(s) read outside themselves, all declaredpnpm check:declared-population-live201 of 252 famil(ies) declare a path population, and every one of them reaches this tree's 8276 tracked file(s).pnpm check:entry-guard209 scripts/ file(s) - every entry guard goes through invoked-as.mjs; 155 export bindings, 155 of them inert on importpnpm check:nul-bytesOK (scanned 8269 text file(s) ... no raw ASCII control bytes).pnpm check:parse-guardcheck:parse-guard: 208 scripts/ file(s) - every TypeScript parse goes through ts-parse.mjs.pnpm check:pm-dispatch-gatesdispatch-gates self-test: 1371 cases pass.pnpm check:pnpm-filter-targets142/181 --filter occurrence(s) across 33 file(s) resolve against 79 workspace package(s)pnpm check:refd-timer-probe5869 source file(s) swept; the process-global timer probe is read in ... and nowhere else.pnpm check:watch-hint-literal49 declaration(s) across 4 rostered name(s) ... no unrostered spelling of the idiom in the tree.Beyond the derived list, both explicitly required by the dispatch:
node scripts/pm/bare-root-worklist.mjs --self-testOK self-test: 54 live row(s), 46 unreachable as spelled, 46 recorded verdict(s) - none stale, none missing, none contradicted.pnpm lint(eslint . --no-inline-config, whole repo)The long self-test also ran through the shared verify lock:
os-verify-lock: VERDICT command-exit 0 - held the lock 394s (6m34s) - waited 47s.check-test-completenessis a value-bearing family and is NOT MEASURED here, in its own words.skip-changeset:scripts/pm/**publishes nothing.Draft, per the dispatch:
scripts/pm/**with no.mdis not a governed surface, and the seat reviews at the contract tier before flipping ready.Generated by Claude Code