Skip to content

Commit 55f845a

Browse files
feat(pm): dispatch-gates derives the ui/scripts/__tests__ suite for diffs under ui source, ui apps, skills/objectui and ui/scripts (#26383)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DF9skmntVkpgbtnjpLwPvPW)_ ## What `scripts/pm/dispatch-gates.mjs` now derives `pnpm --dir ui exec vitest run scripts/` (the whole `ui/scripts/__tests__` suite) for any diff touching one of the card's four roots: `ui/packages/**/src`, `ui/apps/**`, `skills/objectui/**` or `ui/scripts/**`. Before this, those 180 files (178 that run, 2 skipped) ran only in CI's 16 `Test UI` shards and no derived family named them. ## How (the tool's own convention, no parallel mechanism) - **A change-kind row.** A new row in `CHANGE_KIND_ROWS` (`scripts/pm/dispatch-gates.data.mjs`) with predicate `ui-scripts-suite-input`, bound in `CHANGE_KIND_PREDICATES`. The roots are data (`UI_SCRIPTS_SUITE_ROOTS`). They are matched in CI's own `paths:` language through the existing `triggerListCovers`, so a directory surface counts the way it counts for a workflow trigger. - **Bound to the CI job it stands in for.** Every other gate in that table resolves by name against a discovered `check:*` family. This suite has no such family: the only step that runs it is `Test UI`'s vitest line, which carries a shard and the affected-test selector's filter list, and `.github/**` is out of scope here. So the gate declares `ciJob: 'Test UI'` and `ciRunner: 'pnpm --dir ui exec vitest run'`. - `ciJobRunsRunner` resolves the gate against the same `workflowEntries` the discovery reads. If the job is renamed or the runner is respelled, the gate renders STALE, which is the same rot report a renamed family gets. - `bindChangeKindRows` refuses a half-declared binding at load. - The census guard in the self-test reds when the live workflows stop running it. - **It reaches every output.** Through `kindGroups` the gate is in `--commands`, `--json`, the human convention block, the `--residue` kind list and `--ran` (`commandsFor` → `runReconciliation`). Nothing moves in the family partition: the residue still places 326 of 326. - **The `why` names its prerequisite.** The suite needs the seven `@objectstack` packages `Test UI` builds first. Unbuilt, 16 files go red on a missing dist import (measured below). So the line prints the job's own turbo build command. ## Measured first All measured on a 4-vCPU cloud container at base `6390fe52`. | Reading | Result | |---|---| | Suite, packages unbuilt | 16 files failed / 162 passed, 231 s (missing `@objectstack/spec` dist and similar) | | Seven packages built (job's turbo command) | 274 s cold | | Suite, built — run 1 | **178 passed, 2 skipped, 284 s** | | Suite, built — run 2 | **178 passed, 2 skipped, 286 s** | Vitest reports ~270 s of the run as module import. Reach of the 180 files, from a static scan of each file plus the helpers it imports relatively. This is a source scan, not a runtime trace. | Root | Files that read it (file + helpers) | File alone | |---|---|---| | `ui/packages/**/src` | 165 | 128 | | `ui/apps/**` | 141 | 66 | | `skills/objectui/**` | 16 | 8 | | `ui/scripts/**` | 170 | 140 | **Decision: the whole suite, not an affected subset.** On this container it is about five minutes, not the card's "about a minute". It still replaces a ~25-minute CI round trip. A subset map would have to model what each file reads off the tree: 165 of the 180 read package source and 141 the apps. That map is exactly the second list that goes stale, and it would save minutes, not a round trip. The deletion criterion and the stated blind spot sit with the row's docblock. ## Self-test pins (fast tier, in-process) - A diff under each of the four roots derives the suite, both in the rendered convention block (anchored on the delimiter) and in the runnable union. - `packages/core/src/x.ts`, `ui/packages/core/package.json`, `skills/objectstack/SKILL.md` and `scripts/pm/dispatch-gates.mjs` derive neither. - A run record that carries every derived command except the suite is not ok, and names the suite unrun. The control with it is ok. - An unresolved binding renders STALE, naming the job and the runner. - `ciJobRunsRunner` resolves on a fixture job, fails on a renamed job and on a respelled runner, and resolves against the live workflows. - A half-declared binding refuses at load. - One existing pin moved. The `inherited-population` marker line census reads `dispatch-gates.mjs:738` instead of `:736`, because the data import and its re-export each gained one name. It carries a comment like its neighbours. `node scripts/pm/dispatch-gates.mjs --self-test --fast`, detached: **1935 cases pass**. The full battery result is in the gate run below. ## End to end on PR #26274 head 1ef5c48 The head is reachable. Merge base `2409c9c8`, 57 changed files. - `--commands` over those 57 files: 66 commands. Against the base tool (same tree, `6390fe52`) the only difference is the added line `pnpm --dir ui exec vitest run scripts/`. - I ran that command in a worktree at `1ef5c482`, after the seven-package build: **4 files failed, 174 passed**. It reproduces all three reds the card measured: - `check-skill-examples.test.ts`: `json: 69 marked, floor 70` (the MARKED_FLOOR). - `turbo-build-inputs.test.ts`: `@object-ui/console`'s build reads `packages/sdui-parser/src/retired-visibility-dialects.ts`, which turbo's build inputs do not declare. - `check-doc-expression-carriage.test.ts`: 10 failures on the carriage fixtures. - The fourth, `check-hand-rolled-comment-mask.test.ts › exits 0 as a process`, was a 15 s timeout while the dispatch-gates self-test ran on the same 4 cores. It passed in both clean runs on main. I read it as container load, not that head's. ## No narrowing `--commands` from this branch vs main's tool on the same tree, sorted and diffed: | Paths | main | this branch | Difference | |---|---|---|---| | `packages/core/src/index.ts` | 49 | 49 | none | | `scripts/pm/dispatch-gates.mjs` | 30 | 30 | none | | `ui/scripts/check-skill-examples.mjs` | 12 | 13 | + the suite | | `skills/objectui/SKILL.md` | 34 | 35 | + the suite | | `ui/apps/console/package.json` + a spec file | 85 | 86 | + the suite | | #26274's 57 files | 65 | 66 | + the suite | No line was removed in any set. ## Local gates for this diff `node scripts/pm/dispatch-gates.mjs --commands` (no paths, from git, merge base `6390fe52`) derives 30 families for this diff. All 30 ran, each recorded as `CMD :: exit N`, and **all 30 exited 0**. `pnpm check:pm-dispatch-gates` is the fast tier, 1935 cases in 377 s. `node scripts/pm/dispatch-gates.mjs --ran ran.list` reports: `30 derived famil(ies) accounted for — 30 run, 0 NOT-MEASURED (a DERIVED zero)`, exit 0. The slow tier (`--self-test` without `--fast`) runs on `main`'s push and schedule only. Its local reading is reported on the card. ## Out of scope, noted for the seat - `dispatch-gates.mjs`'s `pnpm` recognizer reads `pnpm [--filter X] [run] check:*`, never `pnpm --dir ui run check:*`. So the 38 distinct `check:*` scripts that `Lint UI` / `Build & E2E UI` invoke that way in `ci.yml` are discovered as no family at all. - Those jobs' `if:` is `ui_tree != 'false' && ui != 'false'`, which `jobIfReading` refuses as an intersection. So they appear in neither the job-filtered tail nor the always-runs tail. - That is a reproducible under-derivation for every ui card, and it is not fixed here. It needs its own card (recognizer + `&&`-of-filter-outputs reading). ## Governance - Changed paths: `scripts/pm/dispatch-gates.mjs`, `scripts/pm/dispatch-gates.data.mjs`, `scripts/pm/dispatch-gates.self-test.mjs`. - No `.github/**`, `AGENTS.md`, `.claude/**` or `skills/**` file is touched, and no governed text quotes the change-kind list. - `check-governed-merges --test` on these paths: 0 hits. - The data module's header says "A new gate family under a kind is one row here, still only on the maintainer's words". This row comes from card #26318, which was dispatched under the maintainer's standing devx grant. Flagged so the seat can judge whether that suffices. - Changeset: none. This is a private root script, so the PR carries `skip-changeset`. Clause-②: no Metadata-protocol: none Fixes #26318 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01AYg1gtz1tWt66GpPnTo1QF --- _Generated by [Claude Code](https://claude.ai/code/session_01AYg1gtz1tWt66GpPnTo1QF)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent bb60b86 commit 55f845a

3 files changed

Lines changed: 217 additions & 6 deletions

File tree

‎scripts/pm/dispatch-gates.data.mjs‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,9 +900,52 @@ export const ESCAPABLE_LITERAL_LEDGER_ROWS = Object.freeze([
900900
* already has. ⛔ Nor does either gate happening to go quiet: three of the
901901
* five measured instances involved a green that proved nothing, because a
902902
* sweep that cannot see your file is not evidence about your file.
903+
* - ui-scripts-suite entry: when a workflow runs the `ui/scripts/__tests__`
904+
* suite as a step of its own, under an invocation the discovery reads, the
905+
* row can name that family instead of standing in for a CI job — and when
906+
* the `Test UI` job stops running the suite at all, the binding goes STALE
907+
* by itself. ⛔ A path predicate that narrows the four roots to the files
908+
* each test "really" reads does NOT qualify: that map is a second thing
909+
* that goes stale, and the run is cheap enough not to need one (measured
910+
* below).
903911
*
904912
*
905913
* Delete an entry the day its criterion is met, not before.
914+
*
915+
* ## Why the ui-scripts-suite entry stands in for a CI JOB (#26318)
916+
*
917+
* The 180 test files under `ui/scripts/__tests__/` (178 that run, 2 skipped)
918+
* belong to no package and run
919+
* only inside the 16 `Test UI` shards, as the "always-on" files the affected-test
920+
* selector adds to every run because no package owns them. They pin facts a
921+
* change under ui package source, the Console app, the objectui skill bundle or
922+
* `ui/scripts` itself moves — turbo build inputs, the skill-examples floor, the
923+
* SchemaRenderer channel keys — and no derived family named them: measured on
924+
* PR #26274's head 1ef5c482, three reds there were that PR's own and had passed
925+
* every derived local gate.
926+
*
927+
* Every other gate in this table is a family the workflows run, resolved by
928+
* name. This one cannot be: the only step that runs the suite is the `Test UI`
929+
* job's vitest line, which carries a shard and the selector's filter list, and
930+
* no workflow spells a narrower invocation. So its gate declares the job and the
931+
* runner prefix it narrows (`ciJob`, `ciRunner`), and the engine's
932+
* `ciJobRunsRunner` resolves it against the live workflows the same way a family
933+
* is resolved: a job renamed or a runner respelled renders it STALE, and the
934+
* census guard in the self-test reds on the PR that does it.
935+
*
936+
* It names the WHOLE suite, never an affected subset: a subset map is a second
937+
* list that rots. Measured at 6390fe52 on a 4-vCPU container, after the seven
938+
* @objectstack packages the job builds: 178 files passing in 284 s and 286 s
939+
* (two runs; the card's "about a minute" was read elsewhere), nearly all of it
940+
* module import — so a subset would save minutes, not a CI round trip, and the
941+
* reach it would have to model is the tree itself: by a static scan of each
942+
* file and the helpers it imports, 165 of the 180 read ui package source, 141
943+
* the ui apps, 16 the objectui skill bundle and 170 ui/scripts. What it does not see is
944+
* stated rather than hidden: a ui path outside the four roots (a ui package's
945+
* tests outside `src`, its `package.json`, a root config) is not named here,
946+
* though CI's `Test UI` job still schedules the suite for it — the full battery
947+
* on `main` remains the backstop for that, as it is for the selector's own
948+
* misses.
906949
*/
907950
export const CHANGE_KIND_ROWS = [
908951
{
@@ -1013,8 +1056,37 @@ export const CHANGE_KIND_ROWS = [
10131056
},
10141057
],
10151058
},
1059+
{
1060+
kind: 'edits a file the ui/scripts/__tests__ suite reads (ui package source, the ui apps, the objectui skill bundle, or ui/scripts itself)',
1061+
matches: 'ui-scripts-suite-input',
1062+
gates: [
1063+
{
1064+
name: 'pnpm --dir ui exec vitest run scripts/',
1065+
ciJob: 'Test UI',
1066+
ciRunner: 'pnpm --dir ui exec vitest run',
1067+
why: "the 178 test files under ui/scripts/__tests__ belong to no package and run only in CI's 16 Test UI shards, where the affected-test selector adds them to every run, so no package suite and no check family covers them: they pin turbo build inputs, the skill-examples floor, the SchemaRenderer channel keys and the other facts a change under ui package source, the ui apps, skills/objectui or ui/scripts moves. The whole suite, never a subset: measured at about five minutes on a 4-vCPU container (284 s and 286 s, 178 files passing, import-bound), against the CI round trip of about 25 minutes it saves. It needs the seven @objectstack packages Test UI builds first, or about twenty files red on an import of a missing dist — build them exactly as the job does: pnpm exec turbo run build --filter=@objectstack/spec... --filter=@objectstack/client... --filter=@objectstack/core... --filter=@objectstack/formula... --filter=@objectstack/lint... --filter=@objectstack/sdui-parser... --filter=@objectstack/types... . Positional filter, no bare `--`, from the repo root: ui/scripts/vitest-invocation-guard.mjs refuses every other spelling",
1068+
},
1069+
],
1070+
},
10161071
];
10171072

1073+
/**
1074+
* The roots whose change the `ui/scripts/__tests__` suite can see (#26318), in CI's own `paths:` language
1075+
* — the engine matches them with `triggerListCovers`, the reader of every workflow trigger. They are the
1076+
* card's four, as written there: ui package SOURCE (any depth below `ui/packages`, so a nested package
1077+
* counts), the ui apps, the objectui skill bundle the suite reads through `ui/scripts/skill-bundle-root.mjs`,
1078+
* and the suite's own tree. The bare `src` directory form is listed beside its subtree because a dispatch
1079+
* surface is often given as that directory, and the trigger language reads a directory only through a
1080+
* pattern's literal prefix.
1081+
*/
1082+
export const UI_SCRIPTS_SUITE_ROOTS = Object.freeze([
1083+
'ui/packages/**/src',
1084+
'ui/packages/**/src/**',
1085+
'ui/apps/**',
1086+
'skills/objectui/**',
1087+
'ui/scripts/**',
1088+
]);
1089+
10181090
/**
10191091
* The globs that MANDATE a model tier for any card whose file surface touches
10201092
* them, as DATA. This is the one list in this file besides CHANGE_KIND_GATES,

‎scripts/pm/dispatch-gates.mjs‎

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,7 @@ import {
473473
RETIRED_TIER_WORDS,
474474
ESCAPABLE_LITERAL_LEDGER_ROWS,
475475
CHANGE_KIND_ROWS,
476+
UI_SCRIPTS_SUITE_ROOTS,
476477
MANDATORY_TIER_GLOB_ROWS,
477478
SUSPECT_TIER_GLOB_ROWS,
478479
GENERATED_MODULE_SOURCES,
@@ -491,6 +492,7 @@ export {
491492
TIER_DEFAULT,
492493
RETIRED_TIER_WORDS,
493494
GENERATED_MODULE_SOURCES,
495+
UI_SCRIPTS_SUITE_ROOTS,
494496
};
495497

496498
// Re-exported so this tool's self-test drives the SAME predicates the gate
@@ -11043,6 +11045,45 @@ export function reachesMetadataFormModule(path, modulePaths) {
1104311045
return modulePaths.some((m) => m === path || m.startsWith(`${path}/`));
1104411046
}
1104511047

11048+
/**
11049+
* Does this input path sit under a root the `ui/scripts/__tests__` suite reads (#26318)?
11050+
*
11051+
* The roots are `UI_SCRIPTS_SUITE_ROOTS` in the data module, and they are matched in CI's own `paths:`
11052+
* language through `triggerListCovers`, so a directory surface counts exactly as it counts for a workflow
11053+
* trigger and this file grows no second glob dialect. A leading `./` is tolerated for the reason
11054+
* `isGateScriptPath` tolerates one: a seat pastes paths as its shell printed them.
11055+
*/
11056+
export function readByUiScriptsSuite(path, roots = UI_SCRIPTS_SUITE_ROOTS) {
11057+
return triggerListCovers(roots, path.replace(/^\.\//, '')) !== null;
11058+
}
11059+
11060+
/**
11061+
* Does a live workflow still run the CI job a change-kind gate is the local SUBSET of (#26318)?
11062+
*
11063+
* Every other gate in the change-kind roster is named by an invocation the workflows run, and it resolves
11064+
* against the discovered families. The `ui/scripts/__tests__` suite cannot: CI runs it only inside the
11065+
* `Test UI` job's vitest step, which passes a shard and the affected-test selector's filter list, so that
11066+
* step is no family a dev can paste and the workflows spell no narrower invocation. Such a gate declares
11067+
* `ciJob` (the job's display-name prefix) and `ciRunner` (the command prefix that job's step spells), and
11068+
* its own `name` is that prefix narrowed to the subset a card owes. It resolves to that `name` while some
11069+
* workflow job of that name still runs a `run:` line beginning with the prefix, and to null once none
11070+
* does — the STALE report a renamed family gets, so the row cannot outlive the job it stands in for.
11071+
*
11072+
* ⛔ Not a second discovery. It adds nothing to the families, the residue partition or any path verdict;
11073+
* it answers one question for one table row, from the same `workflowEntries` the discovery read.
11074+
*/
11075+
export function ciJobRunsRunner({ ciJob, ciRunner }, workflowEntries) {
11076+
for (const { text } of workflowEntries) {
11077+
for (const job of extractJobBlocks(text)) {
11078+
if (!job.name.startsWith(ciJob)) continue;
11079+
for (const command of runCommandTexts(job.text)) {
11080+
if (joinLineContinuations(command).split('\n').some((line) => line.trim().startsWith(`${ciRunner} `))) return true;
11081+
}
11082+
}
11083+
}
11084+
return false;
11085+
}
11086+
1104611087
/**
1104711088
* The change-kind roster is data: `CHANGE_KIND_ROWS` in `dispatch-gates.data.mjs` carries each kind, the
1104811089
* NAME of the predicate that recognises it, and the gates it names — with the docblock that governs the
@@ -11058,6 +11099,7 @@ const CHANGE_KIND_PREDICATES = Object.freeze({
1105811099
'error-code-literal': stampsAnErrorCodeLiteral,
1105911100
'http-status-emit': emitsAnHttpStatus,
1106011101
'durability-seam': guardsADurabilityCriticalCall,
11102+
'ui-scripts-suite-input': (path) => readByUiScriptsSuite(path),
1106111103
});
1106211104

1106311105
export function bindChangeKindRows(rows, predicates = CHANGE_KIND_PREDICATES) {
@@ -11069,6 +11111,17 @@ export function bindChangeKindRows(rows, predicates = CHANGE_KIND_PREDICATES) {
1106911111
`known: ${Object.keys(predicates).join(', ')}`,
1107011112
);
1107111113
}
11114+
// A CI-job-subset gate (`ciJobRunsRunner`) carries both halves of its binding, and its name narrows
11115+
// the runner it stands in for — refused at load, so a half-declared row cannot resolve by accident.
11116+
for (const gate of row.gates) {
11117+
const bound = gate.ciJob !== undefined || gate.ciRunner !== undefined;
11118+
if (bound && !(typeof gate.ciJob === 'string' && typeof gate.ciRunner === 'string' && gate.name.startsWith(`${gate.ciRunner} `))) {
11119+
throw new Error(
11120+
`dispatch-gates: change-kind row '${row.kind}' gate '${gate.name}' declares a CI-job binding that is not whole — ` +
11121+
'a gate standing in for a CI job carries both `ciJob` and `ciRunner`, and its name begins with that runner',
11122+
);
11123+
}
11124+
}
1107211125
return { ...row, matches };
1107311126
});
1107411127
}
@@ -11103,7 +11156,14 @@ export function changeKindGates(paths, resolveInvocation, kinds = CHANGE_KIND_GA
1110311156
groups.push({
1110411157
kind,
1110511158
hits,
11106-
gates: gates.map(({ name, why }) => ({ name, why, command: resolveInvocation(name) })),
11159+
// The gate rides into the resolver beside its name: a CI-job-subset gate resolves through its
11160+
// binding, never through the family map (`ciJobRunsRunner`).
11161+
gates: gates.map((gate) => ({
11162+
name: gate.name,
11163+
why: gate.why,
11164+
command: resolveInvocation(gate.name, gate),
11165+
...(gate.ciJob ? { ciJob: gate.ciJob, ciRunner: gate.ciRunner } : {}),
11166+
})),
1110711167
});
1110811168
}
1110911169
return groups;
@@ -11113,11 +11173,13 @@ export function changeKindLines(paths, resolveInvocation, kinds = CHANGE_KIND_GA
1111311173
const lines = [];
1111411174
for (const { kind, hits, gates } of changeKindGates(paths, resolveInvocation, kinds)) {
1111511175
lines.push(` ${kind}: ${hits.join(', ')}`);
11116-
for (const { name, why, command } of gates) {
11176+
for (const { name, why, command, ciJob, ciRunner } of gates) {
1111711177
lines.push(
1111811178
command
1111911179
? ` - ${command} — ${why}`
11120-
: ` - ⚠ ${name}: STALE — no workflow runs a gate under this name. It was renamed or retired; fix CHANGE_KIND_GATES in this script.`,
11180+
: ciJob
11181+
? ` - ⚠ ${name}: STALE — no workflow job named '${ciJob}…' runs '${ciRunner}' any more. The job was renamed or retired; fix CHANGE_KIND_GATES in this script.`
11182+
: ` - ⚠ ${name}: STALE — no workflow runs a gate under this name. It was renamed or retired; fix CHANGE_KIND_GATES in this script.`,
1112111183
);
1112211184
}
1112311185
}
@@ -14545,7 +14607,8 @@ function derive(paths, { showResidue = false, mode = 'human', runRecord = [], si
1454514607
// `--commands` list and the `--json` document are readings of these same
1454614608
// rows: the card this section answers is about two renderings of one
1454714609
// derivation drifting apart, so a second traversal here would reintroduce it.
14548-
const resolveInvocation = (name) => {
14610+
const resolveInvocation = (name, gate = null) => {
14611+
if (gate?.ciJob) return ciJobRunsRunner(gate, workflowEntries) ? name : null;
1454914612
const entry = byCheck.get(name);
1455014613
return entry ? runnableInvocation(entry) : null;
1455114614
};

0 commit comments

Comments
 (0)