Skip to content

Commit 93b4acb

Browse files
fix(scripts): dispatch-gates says a path-scheduled full-battery job does not run on the PR or in the merge queue (#24272)
Fixes #24073 Clause-②: no ## What `dispatch-gates` now says, on the row a reader reads, that a path-scheduled CI job whose `if:` carries ci.yml's full-battery clause does not run on this PR or in the merge queue, and that its first run on the change is `main`'s push battery after the merge. Scope is ruling 6243472660 (on #24171), letter B, term 2: the card's "Done when" items 1 and 2 only. Item 3 (a local command for the dogfood files) would be a new derived row and stays frozen. It is not in this PR. ## Why (measured at `a422f04f`) The job `if:` reader already stripped the full-battery clause (`&& github.event_name != 'pull_request' && github.event_name != 'merge_group'`) as the expression's last conjunct, so the job's filter population could still be read. The fact the clause carries was then dropped. `jobFilterPopulation` returned `{ paths, outputs, dropped }`, the job-filtered row was `{ workflow, job, outputs, hits, dropped, steps }`, and neither renderer had a field to read. `dispatch-gates.data.mjs` is not on this path, so it is unchanged. `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack packages/runtime/src/route-ledger.ts`, the path PR #24009 changed: ```text before (a422f04), stderr: + 5 CI job(s) are scheduled BY YOUR PATHS and run 34 step(s) no family above names — NOT above. ... ⊘ NOT MEASURED — [ci.yml · Dogfood Regression Gate (${{ matrix.shard }}/3)] 5 step(s): Get pnpm store directory · Install dependencies · Build the dogfood package's dependency closure · Snapshot the working tree before the dogfood tests · Boot example apps and exercise real user flows ⊘ NOT MEASURED — [ci.yml · Dogfood Verify CLI] 3 step(s): Get pnpm store directory · Install dependencies · Verify example apps via the `objectstack verify` CLI after (004ba7b), stderr: ⊘ NOT MEASURED — [ci.yml · Dogfood Regression Gate (${{ matrix.shard }}/3)] ⛔ does NOT run on this PR or in the merge queue: its `if:` excludes the pull_request and merge_group events, so its first run on this change is `main`'s push battery, AFTER the merge — 5 step(s): Get pnpm store directory · Install dependencies · Build the dogfood package's dependency closure · Snapshot the working tree before the dogfood tests · Boot example apps and exercise real user flows ⊘ NOT MEASURED — [ci.yml · Dogfood Verify CLI] ⛔ does NOT run on this PR or in the merge queue: its `if:` excludes the pull_request and merge_group events, so its first run on this change is `main`'s push battery, AFTER the merge — 3 step(s): Get pnpm store directory · Install dependencies · Verify example apps via the `objectstack verify` CLI ``` The Temporal Conformance row carries the same clause, because its `if:` does. The Test Core and Build Core rows are unchanged, because theirs do not. stdout (the 49 commands) is byte-identical before and after (diffed). ## Change - `jobIfReading(ifExpression)` returns `{ refs, fullBatteryOnly }`. `jobFilterOutputRefs` is that reading with the second half dropped: same refusals, same refs. - `jobFilterPopulation`, `jobFilterPopulations` and `jobFilteredSteps` carry `fullBatteryOnly` onto each job row. `--json` gets it too, under `jobFilteredSteps.jobs`. - `fullBatteryOnlyClause(row)` renders the one sentence from the flag, never from a job name. `jobFilteredNotMeasuredLine(row)` is the `--commands` / `--json` stderr row, pulled out of the inline template so the pin reads the line a dispatch is built from. The clause goes right after the job name, ahead of the step list. The plain rendering appends it to the row's own line. - The plain rendering's header said "YOUR path puts this job on the PR". It now says the path schedules the job, on this PR unless its row says it does NOT. No new gate, derived row, ratchet, `--tier` mandate or constant. ## Pin The cases are in `scripts/pm/dispatch-gates.self-test.mjs`, fast tier, in the existing sections: - In the full-battery `if:` section, `jobIfReading` returns `fullBatteryOnly: true` for the stripped clause and `false` for the live spelling without it. - In the path-scheduled-jobs section, LIVE on ci.yml for `packages/runtime/src/route-ledger.ts`, four checks run for each of Dogfood Regression Gate and Dogfood Verify CLI: - the row carries `fullBatteryOnly: true`; - the `--commands` line says "does NOT run on this PR or in the merge queue", ahead of the step list; - the plain row line says it too; - in place, the same row with the field stripped renders the statement in neither line. - Negative control: Build Core, scheduled by the same path and run on the PR, carries `false` and no statement. ## Verification at `004ba7b4` (`git rev-parse --short HEAD`) - `pnpm check:pm-dispatch-gates` (fast tier), exit 0, 396.9 s: `✓ dispatch-gates self-test (fast tier): 1913 cases pass; 6 slow section(s) deferred.` - Both tiers, `node scripts/pm/dispatch-gates.mjs --self-test`: exit 0, `✓ dispatch-gates self-test: 2173 cases pass.` (fast 1913 case(s) in 406.7 s, slow 260 case(s) in 1201.5 s). The slow tier is the one CI runs only on `main`'s push and hourly runs. - One-time ablation of the producer, using `node scripts/ablation-replace.mjs` in wrap mode: - The anchor `const fullBatteryOnly = eventFree !== expr;` was replaced by `const fullBatteryOnly = false;`. Anchor count went x1 to x0, and the blob went `b2836327f780` to `cf8939035532`. - Fast tier result: `✗ dispatch-gates self-test (fast tier): 7 of 1913 case(s) failed.` These are exactly the 7 predicted: the `jobIfReading` true case, plus three per dogfood job (the flag, the `--commands` line, the plain line). The in-place ablation cases and the Build Core control stayed green. - Restore proven: blob == HEAD (`b2836327f780`) and `git diff HEAD` is empty. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` with no paths derives 32 commands; the change set is the two files vs merge base `a422f04fe`. All 32 ran with exit 0, each code captured before any pipe. `--ran` printed `✓ dispatch-gates --ran: 32 derived famil(ies) accounted for — 32 run, 0 NOT-MEASURED (a DERIVED zero — all 32 recorded an exit code and none of them is 3).` The same three-path derivation at `a422f04f` gives the identical set of 32. ## Acceptance notes - Console Pin Gate's `if:` carries the same clause, so its row (for `.objectui-sha`) gets the statement from the same flag. No text is written per job. - A check family derived through a full-battery job's filter, such as the console-pin families for `.objectui-sha`, keeps its matched-row provenance "CI job filter for 'Console Pin Gate' in ci.yml" with no WHEN. That family is on stdout as a local command the dev runs, so the dev never has to infer that the PR covers it. Not touched here, which keeps this PR inside the term 2 scope. No carrier. - `dispatch-gates.self-test.mjs` has no named-battery roster to register against. Adding one would be a new constant under the freeze, so the cases sit in the existing sections. - `skip-changeset`: `scripts/pm/**` publishes nothing. --- _Generated by [Claude Code](https://claude.ai/code/session_01QBcLjVCmcWbZEk6qhYMib2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 53ef752 commit 93b4acb

2 files changed

Lines changed: 155 additions & 18 deletions

File tree

‎scripts/pm/dispatch-gates.mjs‎

Lines changed: 84 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,6 +1187,15 @@ export function extractTriggerPaths(workflowText) {
11871187
* of it before the red lands on `main`. Refusing it would drop the console and
11881188
* dogfood leads this function exists to give. Any other event term, a reordered
11891189
* clause or one event alone is still refused whole.
1190+
*
1191+
* ⛔ Stripped is not FORGOTTEN (#24073). The clause decides nothing about paths,
1192+
* but it decides WHEN: such a job never runs on the PR or in the merge queue.
1193+
* `jobIfReading` below returns that fact beside the refs, and every row a
1194+
* path-scheduled job renders carries it to the line the reader reads. Measured
1195+
* before it did: a `packages/runtime/src/route-ledger.ts` change derived the
1196+
* Dogfood Regression Gate as "scheduled BY YOUR PATHS" with nothing saying the
1197+
* PR would never run it, and the first run of its pins was `main`'s push
1198+
* battery, red.
11901199
*/
11911200
const JOB_IF_STATUS_TERM = /(?:^|\s)!\s*cancelled\(\)\s*&&\s*/g;
11921201
const JOB_IF_FULL_BATTERY_TERM =
@@ -1201,12 +1210,27 @@ const JOB_FILTER_OUTPUT_TERM =
12011210
* family keeps whatever verdict it had before.
12021211
*/
12031212
export function jobFilterOutputRefs(ifExpression) {
1213+
return jobIfReading(ifExpression)?.refs ?? null;
1214+
}
1215+
1216+
/**
1217+
* The whole reading of a job `if:` — `{ refs, fullBatteryOnly }`, or null on
1218+
* exactly the refusals `jobFilterOutputRefs` makes, because that function IS
1219+
* this one with the second half dropped. `fullBatteryOnly` is true when the
1220+
* full-battery clause was stripped as the last conjunct: the job runs on
1221+
* `main`'s push and hourly runs and NEVER on a `pull_request` or `merge_group`
1222+
* event. It is read in the same pass as the refs, from the same text, so the
1223+
* population and the event fact cannot describe two different `if:`s.
1224+
*/
1225+
export function jobIfReading(ifExpression) {
12041226
if (typeof ifExpression !== 'string') return null;
12051227
let expr = ifExpression.trim();
12061228
const wrapped = /^\$\{\{([\s\S]*)\}\}$/.exec(expr);
12071229
if (wrapped) expr = wrapped[1].trim();
12081230
expr = expr.replace(JOB_IF_STATUS_TERM, ' ').trim();
1209-
expr = expr.replace(JOB_IF_FULL_BATTERY_TERM, '').trim();
1231+
const eventFree = expr.replace(JOB_IF_FULL_BATTERY_TERM, '').trim();
1232+
const fullBatteryOnly = eventFree !== expr;
1233+
expr = eventFree;
12101234
// One balanced outer paren pair at a time — `(A || B)` is the live spelling.
12111235
for (;;) {
12121236
if (!expr.startsWith('(') || !expr.endsWith(')')) break;
@@ -1227,7 +1251,7 @@ export function jobFilterOutputRefs(ifExpression) {
12271251
if (!m) return null;
12281252
refs.push({ job: m[1], output: m[2] });
12291253
}
1230-
return refs.length ? refs : null;
1254+
return refs.length ? { refs, fullBatteryOnly } : null;
12311255
}
12321256

12331257
/**
@@ -1426,12 +1450,15 @@ const EXTGLOB_CONSTRUCT = /[!?*+@]\(/;
14261450
* The path population a job's `if:` resolves to, or null — the whole of
14271451
* direction 1, assembled from the three readings above.
14281452
*
1429-
* Returns `{ paths, outputs, dropped }`. `paths` is a UNION: the `if:` is an OR
1430-
* of filter outputs, so CI schedules the job when ANY of them matched.
1453+
* Returns `{ paths, outputs, dropped, fullBatteryOnly }`. `paths` is a UNION:
1454+
* the `if:` is an OR of filter outputs, so CI schedules the job when ANY of them
1455+
* matched. `fullBatteryOnly` is `jobIfReading`'s event fact, carried so a row
1456+
* built from this population can say WHEN CI schedules it (#24073).
14311457
*/
14321458
export function jobFilterPopulation(job, filterStepsByOutputSource) {
1433-
const refs = jobFilterOutputRefs(job.if);
1434-
if (!refs) return null;
1459+
const reading = jobIfReading(job.if);
1460+
if (!reading) return null;
1461+
const { refs, fullBatteryOnly } = reading;
14351462
const paths = [];
14361463
const outputs = [];
14371464
let dropped = 0;
@@ -1450,12 +1477,12 @@ export function jobFilterPopulation(job, filterStepsByOutputSource) {
14501477
}
14511478
outputs.push(`${ref.job}.${ref.output}`);
14521479
}
1453-
return paths.length ? { paths, outputs, dropped } : null;
1480+
return paths.length ? { paths, outputs, dropped, fullBatteryOnly } : null;
14541481
}
14551482

14561483
/**
14571484
* Every job in one workflow whose `if:` resolves to a path population, WITH the
1458-
* job's own text: `[{ job, name, text, outputs, paths, dropped }]`.
1485+
* job's own text: `[{ job, name, text, outputs, paths, dropped, fullBatteryOnly }]`.
14591486
*
14601487
* ⛔ No check filter here, and that is the whole reason this is its own
14611488
* function. The caller below drops a job that invokes no discoverable check
@@ -2811,6 +2838,13 @@ export function alwaysRunSteps(entries) {
28112838
* of it runs a command no discovered family accounts for. Both halves are
28122839
* read off the workflow text on every run; nothing here is listed.
28132840
*
2841+
* ⭐ And WHEN it is scheduled (#24073). A row whose job `if:` ended in the
2842+
* full-battery clause carries `fullBatteryOnly: true`, read by `jobIfReading`
2843+
* in the same pass as the population: CI schedules that job for your path on
2844+
* `main`'s push and hourly runs ONLY — never on this PR, never in the merge
2845+
* queue. Both renderings print it on the row's own line, so "scheduled BY YOUR
2846+
* PATHS" cannot be read as "this PR runs it".
2847+
*
28142848
* ⛔ NOT claimed: that a row is a test, a build or a setup step.
28152849
* `alwaysRunLines` refuses that classification for its own rows and the reason
28162850
* carries unchanged — every rule that separates them is a guess about a step's
@@ -2896,7 +2930,10 @@ export function jobFilteredSteps(entries, paths) {
28962930
}
28972931
if (steps.length === 0) continue;
28982932
counts.named += 1;
2899-
rows.push({ workflow: file, job: job.name, outputs: job.outputs, hits, dropped: job.dropped, steps });
2933+
rows.push({
2934+
workflow: file, job: job.name, outputs: job.outputs, hits, dropped: job.dropped,
2935+
fullBatteryOnly: job.fullBatteryOnly === true, steps,
2936+
});
29002937
}
29012938
}
29022939
return { rows, counts };
@@ -11460,6 +11497,37 @@ export function alwaysRunLines(rows, counts) {
1146011497
return lines;
1146111498
}
1146211499

11500+
/**
11501+
* The WHEN of a path-scheduled job row, as the one clause both renderings print
11502+
* on that row's own line (#24073) — or the empty string when the row's job runs
11503+
* on the PR like any other.
11504+
*
11505+
* Read off `row.fullBatteryOnly`, the fact `jobIfReading` took from the job's
11506+
* own `if:`; ⛔ never keyed on a job name, so a job that gains or loses the
11507+
* full-battery clause gains or loses the clause here with nothing to update.
11508+
* The reader it exists for: a dev whose path schedules the Dogfood Regression
11509+
* Gate, told only "scheduled BY YOUR PATHS", inferred that the PR would run its
11510+
* pins — and the first run was `main`'s push battery, red.
11511+
*/
11512+
export function fullBatteryOnlyClause(row) {
11513+
if (row?.fullBatteryOnly !== true) return '';
11514+
return '⛔ does NOT run on this PR or in the merge queue: its `if:` excludes the pull_request and merge_group events,' +
11515+
" so its first run on this change is `main`'s push battery, AFTER the merge";
11516+
}
11517+
11518+
/**
11519+
* One path-scheduled job row as `--commands` / `--json` print it on stderr — a
11520+
* named reader rather than an inline template so the line a dispatch is built
11521+
* from is the line the self-test pins. The WHEN clause sits directly after the
11522+
* job name, ahead of the step list, because a long step list is where a
11523+
* trailing clause goes unread.
11524+
*/
11525+
export function jobFilteredNotMeasuredLine(row) {
11526+
const when = fullBatteryOnlyClause(row);
11527+
return ` ⊘ NOT MEASURED — [${row.workflow} · ${row.job}]${when ? ` ${when} —` : ''} ${row.steps.length} step(s):` +
11528+
` ${row.steps.map((s) => s.step).join(' · ')}`;
11529+
}
11530+
1146311531
/**
1146411532
* The path-scheduled CI jobs, rendered (#16285) — printed BELOW the
1146511533
* reconciliation, because everything under that total names something OUTSIDE
@@ -11502,8 +11570,9 @@ export function jobFilteredStepLines(rows, counts) {
1150211570
' ⛔ NOT runnable and ⛔ NOT in --commands: every row is CI\'s own shell in CI\'s environment. They sit OUTSIDE the runnable',
1150311571
' total above — running every family named there does NOT cover them, which is the gap this block exists to close.',
1150411572
' ⛔ NOT classified into tests, builds and setup — the same refusal the always-runs tail makes, for the same reason: every rule',
11505-
' that tells them apart is a guess about a step\'s intent. What is claimed for a row is only what was measured — YOUR path puts',
11506-
' this job on the PR, and nothing above names a family for this step. The reader judges the rest.',
11573+
' that tells them apart is a guess about a step\'s intent. What is claimed for a row is only what was measured — YOUR path',
11574+
' schedules this job (on this PR unless its row says it does NOT), and nothing above names a family for this step.',
11575+
' The reader judges the rest.',
1150711576
' ⇒ The always-runs tail below is the OTHER half of this partition: it reads the jobs CI cannot narrow by path, this one reads',
1150811577
' the jobs your paths narrow it TO — and a job carrying an `if:` is excluded there, which is every job on this list.',
1150911578
];
@@ -11516,8 +11585,10 @@ export function jobFilteredStepLines(rows, counts) {
1151611585
for (const row of rows) {
1151711586
const via = row.hits.map((h) => `${h.path} ⇢ '${h.pattern}'`).join('; ');
1151811587
const dropped = row.dropped ? `, ${row.dropped} glob(s) of it untranslatable and dropped` : '';
11588+
const when = fullBatteryOnlyClause(row);
1151911589
lines.push(
11520-
` - [${row.workflow} · ${row.job}] scheduled by ${via} (job \`if:\` reads ${row.outputs.join(', ')}${dropped})`,
11590+
` - [${row.workflow} · ${row.job}] scheduled by ${via} (job \`if:\` reads ${row.outputs.join(', ')}${dropped})` +
11591+
`${when ? ` ${when}` : ''}`,
1152111592
);
1152211593
for (const { step, commands } of row.steps) {
1152311594
lines.push(` · ${step}`);
@@ -14346,12 +14417,7 @@ export function machineReadableOutput(mode, { paths, size = null, matchedRows, k
1434614417
` — ${mode === 'json' ? 'under jobFilteredSteps, not in commands' : 'NOT above'}. They are CI's own shell in CI's environment,` +
1434714418
' so there is no local invocation to hand you, and running every command on stdout does ⛔ NOT cover them.',
1434814419
);
14349-
for (const row of jobFiltered.rows) {
14350-
console.error(
14351-
` ⊘ NOT MEASURED — [${row.workflow} · ${row.job}] ${row.steps.length} step(s):` +
14352-
` ${row.steps.map((s) => s.step).join(' · ')}`,
14353-
);
14354-
}
14420+
for (const row of jobFiltered.rows) console.error(jobFilteredNotMeasuredLine(row));
1435514421
console.error(' ⇒ Run without --commands/--json to see each step printed as CI spells it.');
1435614422
}
1435714423
// ⭐ The SEVENTH thing stdout deliberately omits (#19172) — and the lane this

‎scripts/pm/dispatch-gates.self-test.mjs‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ import {
162162
firstPartyImportBindings,
163163
firstPartyImportTargets,
164164
followCompositeActions,
165+
fullBatteryOnlyClause,
165166
gateFamilyFiles,
166167
generatedModuleSources,
167168
globCarriesLiteralSuffix,
@@ -184,8 +185,10 @@ import {
184185
isTypeCheckInvocation,
185186
jobFilterOutputRefs,
186187
jobFilterPopulation,
188+
jobFilteredNotMeasuredLine,
187189
jobFilteredStepLines,
188190
jobFilteredSteps,
191+
jobIfReading,
189192
jobPathPopulations,
190193
joinLineContinuations,
191194
judgedAsPattern,
@@ -5670,6 +5673,17 @@ export function selfTest({ tier = 'full' } = {}) {
56705673
'...and the clause in front of the filter output is not the stripped spelling either',
56715674
jobFilterOutputRefs(`\${{ ${FULL_BATTERY} && needs.f.outputs.a != 'false' }}`) === null,
56725675
);
5676+
// #24073: stripped is not FORGOTTEN. The reading returns the event fact beside
5677+
// the refs it shares with `jobFilterOutputRefs`, and only when it stripped it.
5678+
const fullBatteryReading = jobIfReading(`\${{ !cancelled() && needs.filter.outputs.console != 'false' && ${FULL_BATTERY} }}`);
5679+
t(
5680+
'jobIfReading: the stripped full-battery clause is RETURNED as fullBatteryOnly, beside the same refs',
5681+
fullBatteryReading?.fullBatteryOnly === true && fullBatteryReading?.refs.map((r) => `${r.job}.${r.output}`).join() === 'filter.console',
5682+
);
5683+
t(
5684+
'...and an if: with no event clause reads fullBatteryOnly FALSE — the fact is read off the text, never defaulted on',
5685+
jobIfReading("${{ !cancelled() && needs.filter.outputs.console != 'false' }}")?.fullBatteryOnly === false,
5686+
);
56735687

56745688
const fixturePops = jobPathPopulations(jobFilterWf, 'fixture.yml');
56755689
t(
@@ -11928,6 +11942,63 @@ export function selfTest({ tier = 'full' } = {}) {
1192811942
outsideBlockCounts(familyReconciliation({ jobFilteredRows: [{}, {}] })).jobFilteredJobs === 2,
1192911943
);
1193011944

11945+
// ── WHEN a path-scheduled job runs, on the row the reader reads (#24073) ──
11946+
//
11947+
// The measured case: a `packages/runtime/src/route-ledger.ts` change derived
11948+
// the Dogfood Regression Gate as "scheduled BY YOUR PATHS", and nothing said
11949+
// its `if:` skips pull_request and merge_group — so the PR never ran the two
11950+
// dogfood pins that read that file, and their first run was `main`'s push
11951+
// battery, red. Pinned LIVE on ci.yml for both dogfood jobs (they carry the
11952+
// same `if:`), against a NEGATIVE control on a job that does run on the PR,
11953+
// and with an in-place ABLATION of the carried flag on every row, so the
11954+
// statement cannot pass as a string typed per job. If ci.yml moves either job
11955+
// back onto the PR, these go red: re-point them to whichever path-scheduled
11956+
// job still carries the clause — ⛔ do not delete them.
11957+
const ledgerCard = 'packages/runtime/src/route-ledger.ts';
11958+
const ledgerJobs = jobFilteredSteps(liveWorkflows, [ledgerCard]);
11959+
const ledgerRow = (prefix) => ledgerJobs.rows.find((r) => r.job.startsWith(prefix)) ?? null;
11960+
const NOT_ON_PR = 'does NOT run on this PR or in the merge queue';
11961+
// The plain rendering of ONE row, with counts that satisfy the renderer's own
11962+
// refusals, so the ablated copy below is rendered by the same function.
11963+
const plainRowLine = (row) => {
11964+
if (!row) return '';
11965+
const n = row.steps.length;
11966+
const lines = jobFilteredStepLines([row], { covering: 1, named: 1, steps: n, accounted: 0, unaccounted: n, conditionalSteps: 0 });
11967+
return lines.find((l) => l.startsWith(` - [${row.workflow} · ${row.job}]`)) ?? '';
11968+
};
11969+
for (const prefix of ['Dogfood Regression Gate', 'Dogfood Verify CLI']) {
11970+
const row = ledgerRow(prefix);
11971+
t(
11972+
`LIVE: a route-ledger.ts change schedules '${prefix}', and its row carries the full-battery fact read off its own if:`,
11973+
row?.fullBatteryOnly === true,
11974+
);
11975+
t(
11976+
'...its --commands line SAYS the job does not run on this PR or in the merge queue, ahead of the step list',
11977+
Boolean(row) && jobFilteredNotMeasuredLine(row).indexOf(NOT_ON_PR) > -1
11978+
&& jobFilteredNotMeasuredLine(row).indexOf(NOT_ON_PR) < jobFilteredNotMeasuredLine(row).indexOf(' step(s):'),
11979+
);
11980+
t("...and so does its row in the plain rendering, on the row's own line", plainRowLine(row).includes(NOT_ON_PR));
11981+
// ABLATION, in place: the same row with the carried fact stripped. The
11982+
// statement must vanish from both lines — rendered from the flag, or not at all.
11983+
const stripped = row ? Object.fromEntries(Object.entries(row).filter(([k]) => k !== 'fullBatteryOnly')) : null;
11984+
t(
11985+
'...ABLATED — the carried fact stripped from the row — the statement disappears from BOTH lines',
11986+
Boolean(stripped)
11987+
&& fullBatteryOnlyClause(stripped) === ''
11988+
&& !jobFilteredNotMeasuredLine(stripped).includes(NOT_ON_PR)
11989+
&& plainRowLine(stripped) !== ''
11990+
&& !plainRowLine(stripped).includes(NOT_ON_PR),
11991+
);
11992+
}
11993+
const prJobRow = ledgerRow('Build Core');
11994+
t(
11995+
'LIVE NEGATIVE CONTROL: Build Core, scheduled by the same path and run on the PR, carries the fact as FALSE and no statement',
11996+
prJobRow?.fullBatteryOnly === false
11997+
&& !jobFilteredNotMeasuredLine(prJobRow).includes(NOT_ON_PR)
11998+
&& plainRowLine(prJobRow) !== ''
11999+
&& !plainRowLine(prJobRow).includes(NOT_ON_PR),
12000+
);
12001+
1193112002
// ── Type-check lanes (#19172): the negatives are the live over-matches a SUBSTRING reading produces here ──
1193212003
t('a `tsc --noEmit` or `-p <config>` invocation is a lane',
1193312004
isTypeCheckInvocation('pnpm --filter @objectstack/spec exec tsc --noEmit') && isTypeCheckInvocation('npx tsc -p tsconfig.test.json'));

0 commit comments

Comments
 (0)