tickmarkr 2.8.7 → 2.8.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/compile.js +1 -1
- package/dist/cli/commands/report.js +21 -12
- package/dist/cli/commands/status.js +14 -2
- package/dist/cli/commands/verify.js +29 -5
- package/dist/compile/collateral.d.ts +3 -1
- package/dist/compile/collateral.js +8 -16
- package/dist/compile/native.js +7 -2
- package/dist/gates/acceptance.d.ts +8 -0
- package/dist/gates/acceptance.js +54 -37
- package/dist/gates/run-gates.js +31 -22
- package/dist/report/bundle.d.ts +5 -2
- package/dist/report/bundle.js +11 -6
- package/dist/report/compare.d.ts +9 -0
- package/dist/report/compare.js +58 -40
- package/dist/report/operator-record.js +4 -3
- package/dist/run/daemon.d.ts +7 -3
- package/dist/run/daemon.js +64 -27
- package/dist/run/journal.js +11 -2
- package/dist/run/repair-disposition.d.ts +1 -1
- package/dist/run/repair-disposition.js +33 -2
- package/package.json +1 -1
|
@@ -67,7 +67,7 @@ export async function compile(argv, cwd = process.cwd(), harnessFrom = process.a
|
|
|
67
67
|
// Both modes reach the same pure compiler; --dry-run removes every state write below.
|
|
68
68
|
const g = compileSource(sourcePath, values.type, cwd, // repo root: gsd stores context[0] repo-relative so workers resolve it inside their worktree
|
|
69
69
|
(plan) => plan, { strict: values.strict });
|
|
70
|
-
const sourceFindings = sourceScopeFindings(g.tasks, cwd);
|
|
70
|
+
const sourceFindings = sourceScopeFindings(g.tasks, cwd, { failClosed: g.spec.source === "native" });
|
|
71
71
|
const scopeLints = [
|
|
72
72
|
...collateralLints(g.tasks, cwd),
|
|
73
73
|
...sourceScopeLints(g.tasks, cwd, sourceFindings),
|
|
@@ -13,6 +13,7 @@ import { assignmentChannel, buildOperatorRecord, formatChannelMoney, formatChann
|
|
|
13
13
|
import { cellsOf, cellSummary } from "../../route/profile.js";
|
|
14
14
|
import { effectiveEvents, fatalRunEndCause, foldOwedChecks, Journal, loadRoutingProfile, OWED_DISCHARGE_EVENT, } from "../../run/journal.js";
|
|
15
15
|
import { baselineProvenanceOf, fingerprintsOf, forgivenFingerprints, forgivenGateRow, formatBaselineProvenance, formatFingerprints, formatForgiven, formatTipProof, runEndTipProof, } from "../../run/daemon.js";
|
|
16
|
+
import { normalizeGateOutcome } from "../../run/outcome.js";
|
|
16
17
|
import { formatSpan, WALL_PRIORITY, WALL_PRIORITY_TEXT, wallBudget, wallBudgetFacts } from "../../run/wall-budget.js";
|
|
17
18
|
import { deriveRunCockpitData } from "../../tui/cockpit/derive.js";
|
|
18
19
|
import { cellWidth, wrapCells } from "../../tui/cockpit/width.js";
|
|
@@ -382,12 +383,15 @@ const verificationOf = (runId, events) => {
|
|
|
382
383
|
return "failed";
|
|
383
384
|
if (tip?.state !== "pass")
|
|
384
385
|
return "absent";
|
|
385
|
-
//
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
386
|
+
// Queue 35 (D-2134 R6): the same per-gate proof names both the lead and verification line.
|
|
387
|
+
const proof = runEndTipProof(cycle);
|
|
388
|
+
if (proof.gates?.length) {
|
|
389
|
+
if (proof.gates.every(({ kind }) => kind === "reused"))
|
|
390
|
+
return "cached";
|
|
391
|
+
if (proof.gates.some(({ kind }) => kind === "reused"))
|
|
392
|
+
return "mixed";
|
|
393
|
+
}
|
|
394
|
+
return "passed";
|
|
391
395
|
};
|
|
392
396
|
// Absent is a state to render, never a zero to invent — and no reading here calls a run green.
|
|
393
397
|
// NOTE: every reading below is byte-pinned by tests/fixtures/brand-surfaces/report-md.md — the
|
|
@@ -396,6 +400,7 @@ const verificationOf = (runId, events) => {
|
|
|
396
400
|
const VERIFICATION_READING = {
|
|
397
401
|
passed: "passed — tickmarkr verified this run's integration tip",
|
|
398
402
|
cached: "cached — carried forward from an earlier verified tip, not re-run for this one",
|
|
403
|
+
mixed: "mixed — some tip gates ran fresh and others were carried forward",
|
|
399
404
|
failed: "FAILED — the run did not verify its own tip",
|
|
400
405
|
absent: "absent — no tip verification recorded: neither passed nor failed",
|
|
401
406
|
};
|
|
@@ -406,9 +411,9 @@ function forgivenLines(events) {
|
|
|
406
411
|
function verificationReading(runId, events) {
|
|
407
412
|
const state = verificationOf(runId, events);
|
|
408
413
|
const proof = runEndTipProof(closedCycle(events));
|
|
409
|
-
if ((state === "passed" || state === "cached") && proof.gates?.length) {
|
|
410
|
-
|
|
411
|
-
return `${
|
|
414
|
+
if ((state === "passed" || state === "cached" || state === "mixed") && proof.gates?.length) {
|
|
415
|
+
// Queue 35 (D-2134 R6): mixed describes the lead's provenance; per-gate details retain it here.
|
|
416
|
+
return `${state === "mixed" ? "passed" : state} — ${formatTipProof(proof)}`;
|
|
412
417
|
}
|
|
413
418
|
return VERIFICATION_READING[state];
|
|
414
419
|
}
|
|
@@ -520,9 +525,13 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = [], suit
|
|
|
520
525
|
lines.push("- **tickmarks:**");
|
|
521
526
|
if (gates.length) {
|
|
522
527
|
for (const g of gates) {
|
|
523
|
-
//
|
|
524
|
-
let pass =
|
|
528
|
+
// Queue 35 (D-2134 R6): only held changes the existing tickmark vocabulary.
|
|
529
|
+
let pass = normalizeGateOutcome(g.data).kind === "held" ? "held"
|
|
530
|
+
: gateDeclined(g.data) ? "declined" : g.data.pass === true ? "pass" : g.data.pass === false ? "fail" : EM;
|
|
525
531
|
const gate = typeof g.data.gate === "string" ? g.data.gate : EM;
|
|
532
|
+
if (gate === "acceptance" && g.data.judgeIndependent === false) {
|
|
533
|
+
pass += " (independence not established)";
|
|
534
|
+
}
|
|
526
535
|
if (gate === "review" && pass === "pass") {
|
|
527
536
|
const reviewer = reviewerIdentity(g.data);
|
|
528
537
|
const before = events.slice(0, events.indexOf(g) + 1).reverse()
|
|
@@ -616,7 +625,7 @@ function textReport(runId, events, rows, cwd, suite) {
|
|
|
616
625
|
// T11: gates that declined never ran — out of both the pass count and the rate base.
|
|
617
626
|
const declinedCount = gateResults.filter((e) => gateDeclined(e.data)).length;
|
|
618
627
|
const gateRan = gateResults.length - declinedCount;
|
|
619
|
-
const gatePass = gateResults.filter((e) => e.data.
|
|
628
|
+
const gatePass = gateResults.filter((e) => normalizeGateOutcome(e.data).kind === "passed" && !gateDeclined(e.data)).length;
|
|
620
629
|
const escalations = events.filter((e) => e.event === "escalation").length;
|
|
621
630
|
const consults = events.filter((e) => e.event === "consult-verdict").length;
|
|
622
631
|
const failovers = events.filter((e) => e.event === "quota-failover").length;
|
|
@@ -968,6 +968,16 @@ const recordedTasks = (graph, rows) => graph.tasks.filter((task) => {
|
|
|
968
968
|
|| row.parkKind !== undefined);
|
|
969
969
|
});
|
|
970
970
|
const recordedDone = (tasks, rows) => tasks.filter((task) => graphTaskStatus(rows.get(task.id)?.state, task.status) === "done").length;
|
|
971
|
+
const judgeIndependenceNotes = (events) => {
|
|
972
|
+
// Queue row 34(a) (D-2134 R5): only the newest acceptance row owns this note.
|
|
973
|
+
const acceptance = new Map();
|
|
974
|
+
for (const event of events) {
|
|
975
|
+
if (event.taskId && event.event === "gate-result" && event.data.gate === "acceptance")
|
|
976
|
+
acceptance.set(event.taskId, event);
|
|
977
|
+
}
|
|
978
|
+
return [...acceptance].filter(([, event]) => event.data.judgeIndependent === false)
|
|
979
|
+
.map(([id]) => `${sanitizeTaskText(id)} judge not independent`);
|
|
980
|
+
};
|
|
971
981
|
const renderFrame = (cwd,
|
|
972
982
|
// No default: the version is the binary's, resolved once by its caller, and a frame that had to
|
|
973
983
|
// invent one would be naming a version nobody can check.
|
|
@@ -976,6 +986,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
976
986
|
const record = readRunRecord(cwd, g, namedRunId);
|
|
977
987
|
const runId = record?.runId;
|
|
978
988
|
const events = record?.events ?? [];
|
|
989
|
+
const judgeNotes = judgeIndependenceNotes(events);
|
|
979
990
|
const runLine = runId && record ? runStatusLine(cwd, runId, events) : null;
|
|
980
991
|
// The header and every live-phase consumer share this exact process reading. Proved death retires
|
|
981
992
|
// stale phase markers; unknown identity deliberately does not, preserving pre-identity journals.
|
|
@@ -1171,7 +1182,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1171
1182
|
// At its machine width and wider the plain print keeps each machine row whole — one row per task and
|
|
1172
1183
|
// its evidence line after it, which consumers parse. A configured width narrower than that holds for
|
|
1173
1184
|
// EVERY row: one that overflows wraps beneath its own indent, its identity kept on the first.
|
|
1174
|
-
const body = [header, legendLine, ` ${supervisionText(supervision, divider)}`, ...rows];
|
|
1185
|
+
const body = [header, legendLine, ` ${supervisionText(supervision, divider)}`, ...rows, ...judgeNotes.map((note) => ` ${note}`)];
|
|
1175
1186
|
const wrapRow = (line) => wrapCells(line, width, { continuationPrefix: `${/^ */u.exec(line)[0]} ` });
|
|
1176
1187
|
return {
|
|
1177
1188
|
content: [
|
|
@@ -1383,6 +1394,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
|
|
|
1383
1394
|
"",
|
|
1384
1395
|
...tableRows(),
|
|
1385
1396
|
...recoverySection,
|
|
1397
|
+
...judgeNotes.flatMap((note) => wrapCells(` ${note}`, boardColumns, { continuationPrefix: " " })),
|
|
1386
1398
|
gatesLegend,
|
|
1387
1399
|
"",
|
|
1388
1400
|
rule(boardColumns),
|
|
@@ -1436,7 +1448,7 @@ const oneLine = (cwd, namedRunId) => {
|
|
|
1436
1448
|
// A fatal run-end leads right after the run id: no completion fraction or verify state reads first.
|
|
1437
1449
|
const crash = fatalRunEndCause(events);
|
|
1438
1450
|
return sanitizeTaskText([
|
|
1439
|
-
record.runId, ...(crash ? [`run crashed — ${crash}`] : []), ...claims, firstPassText(endToEndFirstPass(events)), wallText(events), needsYouText(events, currentOwed(events, cwd)),
|
|
1451
|
+
record.runId, ...(crash ? [`run crashed — ${crash}`] : []), ...claims, firstPassText(endToEndFirstPass(events)), wallText(events), needsYouText(events, currentOwed(events, cwd)), ...judgeIndependenceNotes(events),
|
|
1440
1452
|
].join(" · "));
|
|
1441
1453
|
};
|
|
1442
1454
|
export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
@@ -370,8 +370,8 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
370
370
|
}
|
|
371
371
|
// Baseline: --baseline file > cached capture for this merge-base > fresh capture on a detached
|
|
372
372
|
// temp worktree of the merge-base (so pre-existing failures on base are forgiven, exactly as a run).
|
|
373
|
-
//
|
|
374
|
-
//
|
|
373
|
+
// Queue row 31 / D-2134: gate-time state lives outside the repo; a validated discharge's durable
|
|
374
|
+
// proof is copied into its run only after the battery's final dirty-worktree check.
|
|
375
375
|
// ponytail: tmpdir means the baseline cache dies on reboot/cleanup — worst case is one re-capture.
|
|
376
376
|
const stateDir = verifyStateDir(cwd);
|
|
377
377
|
mkdirSync(stateDir, { recursive: true });
|
|
@@ -600,6 +600,7 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
600
600
|
console.error(`verify: evidence capture failed for ${artifactPath}`);
|
|
601
601
|
}
|
|
602
602
|
const review = reviewRows.at(-1);
|
|
603
|
+
let recordedArtifactPath = artifactPath;
|
|
603
604
|
// C1: ONE discharge row names what the fold re-reads — the artifact by hash, the range mapping, the
|
|
604
605
|
// criteria and each resolved channel — and it is appended only when the fold, the single decider,
|
|
605
606
|
// already accepts it: verify never writes a proof that would turn known debt into unknown debt.
|
|
@@ -624,8 +625,31 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
624
625
|
dischargeRefusal = "the results artifact is unavailable";
|
|
625
626
|
else if (!candidate.known || !owed.every((o) => candidate.discharged.includes(o.id)))
|
|
626
627
|
dischargeRefusal = candidate.unknown.at(-1)?.reason ?? "the proof does not discharge the check";
|
|
627
|
-
else
|
|
628
|
-
|
|
628
|
+
else {
|
|
629
|
+
// Queue row 31 / D-2134: validate before writing under the run, then retain the exact bytes.
|
|
630
|
+
// Exclusive creation accepts an identical existing proof, never overwrites different bytes.
|
|
631
|
+
const relativePath = join("discharges", `${artifactSha256}.json`);
|
|
632
|
+
const durablePath = join(recordJournal.dir, relativePath);
|
|
633
|
+
try {
|
|
634
|
+
mkdirSync(dirname(durablePath), { recursive: true });
|
|
635
|
+
try {
|
|
636
|
+
writeFileSync(durablePath, artifactBytes, { flag: "wx" });
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
if (error.code !== "EEXIST")
|
|
640
|
+
throw error;
|
|
641
|
+
if (!readFileSync(durablePath).equals(artifactBytes))
|
|
642
|
+
throw new Error("existing durable proof has different bytes");
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
dischargeRefusal = "durable results artifact unavailable or has different bytes";
|
|
647
|
+
}
|
|
648
|
+
if (!dischargeRefusal) {
|
|
649
|
+
recordedArtifactPath = relativePath;
|
|
650
|
+
recordJournal.append(OWED_DISCHARGE_EVENT, values.task, { ...data, artifactPath: recordedArtifactPath });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
629
653
|
}
|
|
630
654
|
const owedAfter = recordJournal ? foldOwedChecks(recordJournal.read(), cwd) : undefined;
|
|
631
655
|
const code = green && !dischargeRefusal ? 0 : 2;
|
|
@@ -633,7 +657,7 @@ export async function verify(argv, cwd = process.cwd(), options = {}) {
|
|
|
633
657
|
? [...(dischargeRefusal ? [`verify --record refused discharge: ${dischargeRefusal}`] : []), debtLine(owedAfter)] : [];
|
|
634
658
|
if (recordJournal && review) {
|
|
635
659
|
recordJournal.append("review-leg2", values.task ?? "VERIFY", {
|
|
636
|
-
base: baseTip, head, mergeBase, author: channelKey(author), artifactPath, artifactSha256, artifactAvailability,
|
|
660
|
+
base: baseTip, head, mergeBase, author: channelKey(author), artifactPath: recordedArtifactPath, artifactSha256, artifactAvailability,
|
|
637
661
|
...review,
|
|
638
662
|
});
|
|
639
663
|
}
|
|
@@ -55,7 +55,9 @@ export interface SourceScopeFinding {
|
|
|
55
55
|
paths: string[];
|
|
56
56
|
}
|
|
57
57
|
/** Structured OBS-76 findings shared by human rendering and native compile enforcement. */
|
|
58
|
-
export declare function sourceScopeFindings(tasks: ReadonlyArray<Pick<Task, "id" | "files" | "acceptance">>, repoRoot: string
|
|
58
|
+
export declare function sourceScopeFindings(tasks: ReadonlyArray<Pick<Task, "id" | "files" | "acceptance">>, repoRoot: string, options?: {
|
|
59
|
+
failClosed?: boolean;
|
|
60
|
+
}): SourceScopeFinding[];
|
|
59
61
|
/** Human display is derived from the structured finding; no enforcement path parses this text. */
|
|
60
62
|
export declare function renderSourceScopeFinding(finding: SourceScopeFinding): string;
|
|
61
63
|
/**
|
|
@@ -3,18 +3,15 @@ import { extname, join, posix, relative } from "node:path";
|
|
|
3
3
|
import { filesGlob } from "../graph/files-glob.js";
|
|
4
4
|
import { criticalPathHits, DEFAULT_CONFIG, DEFAULT_REVIEW_CRITICAL_PATHS, effectiveReviewPolicy, loadConfig, } from "../config/config.js";
|
|
5
5
|
import { renderAcceptanceItem } from "../graph/schema.js";
|
|
6
|
+
import { CompileError } from "./common.js";
|
|
6
7
|
// Advisory scan (OBS-12/13/14/21, OBS-76). NEVER expands files[] or fails compile — a warning the
|
|
7
8
|
// author acts on. Plain-text (no AST), capped + sorted for DISPLAY only.
|
|
8
9
|
// OBS-547: the collateral prediction is no longer plan-time-only. The daemon computes one full
|
|
9
10
|
// (uncapped) map per run and hands each task's slice to its scope gate, which classifies a red
|
|
10
11
|
// against it — the prediction never causes a refusal, it only says whether a red the gate already
|
|
11
12
|
// found was foreseeable (authoring defect) or not (chargeable quality failure).
|
|
12
|
-
/** Max files walked per root (sorted walk; rest ignored). */
|
|
13
|
-
const MAX_WALK_FILES = 400;
|
|
14
13
|
/** Max collateral test paths listed per task. */
|
|
15
14
|
const MAX_HITS_PER_TASK = 20;
|
|
16
|
-
/** Skip giant fixtures / snapshots. */
|
|
17
|
-
const MAX_READ_BYTES = 512 * 1024;
|
|
18
15
|
const EXPORT_MANIFEST_TEST = "tests/repo/export-manifest.test.ts";
|
|
19
16
|
const SRC_EXT = /\.(ts|tsx|js|jsx|mts|cts)$/;
|
|
20
17
|
const CODE_EXT = /\.(ts|tsx|js|jsx|mts|cts)$/;
|
|
@@ -33,8 +30,6 @@ function walkCode(repoRoot, subdir) {
|
|
|
33
30
|
const root = join(repoRoot, subdir);
|
|
34
31
|
const out = [];
|
|
35
32
|
const walk = (dir) => {
|
|
36
|
-
if (out.length >= MAX_WALK_FILES)
|
|
37
|
-
return;
|
|
38
33
|
let entries;
|
|
39
34
|
try {
|
|
40
35
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
@@ -45,8 +40,6 @@ function walkCode(repoRoot, subdir) {
|
|
|
45
40
|
// deterministic order
|
|
46
41
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
47
42
|
for (const e of entries) {
|
|
48
|
-
if (out.length >= MAX_WALK_FILES)
|
|
49
|
-
return;
|
|
50
43
|
if (e.name === "node_modules" || e.name === ".git" || e.name.startsWith("."))
|
|
51
44
|
continue;
|
|
52
45
|
const full = join(dir, e.name);
|
|
@@ -78,22 +71,21 @@ function mentions(content, needles) {
|
|
|
78
71
|
return false;
|
|
79
72
|
}
|
|
80
73
|
// cache file bodies (one read per path)
|
|
81
|
-
function makeReader(repoRoot) {
|
|
74
|
+
function makeReader(repoRoot, failClosed = false) {
|
|
82
75
|
const body = new Map();
|
|
83
76
|
return (rel) => {
|
|
84
77
|
if (body.has(rel))
|
|
85
78
|
return body.get(rel);
|
|
86
79
|
try {
|
|
87
|
-
const st = statSync(join(repoRoot, rel));
|
|
88
|
-
if (st.size > MAX_READ_BYTES) {
|
|
89
|
-
body.set(rel, null);
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
80
|
const text = readFileSync(join(repoRoot, rel), "utf8");
|
|
93
81
|
body.set(rel, text);
|
|
94
82
|
return text;
|
|
95
83
|
}
|
|
96
84
|
catch {
|
|
85
|
+
if (failClosed) {
|
|
86
|
+
throw new CompileError(`${rel} is unreadable, so the source-scope scan is incomplete — restore read access `
|
|
87
|
+
+ `or remove the path and recompile (queue row 27, D-2134).`);
|
|
88
|
+
}
|
|
97
89
|
body.set(rel, null);
|
|
98
90
|
return null;
|
|
99
91
|
}
|
|
@@ -502,7 +494,7 @@ export function newDirectoryLints(tasks, repoRoot) {
|
|
|
502
494
|
return lines;
|
|
503
495
|
}
|
|
504
496
|
/** Structured OBS-76 findings shared by human rendering and native compile enforcement. */
|
|
505
|
-
export function sourceScopeFindings(tasks, repoRoot) {
|
|
497
|
+
export function sourceScopeFindings(tasks, repoRoot, options = {}) {
|
|
506
498
|
const perTask = tasks
|
|
507
499
|
.map((t) => ({ t, needles: criteriaSymbols(t.acceptance) }))
|
|
508
500
|
.filter((x) => x.needles.length);
|
|
@@ -511,7 +503,7 @@ export function sourceScopeFindings(tasks, repoRoot) {
|
|
|
511
503
|
const srcFiles = walkCode(repoRoot, "src");
|
|
512
504
|
if (!srcFiles.length)
|
|
513
505
|
return [];
|
|
514
|
-
const read = makeReader(repoRoot);
|
|
506
|
+
const read = makeReader(repoRoot, options.failClosed);
|
|
515
507
|
const findings = [];
|
|
516
508
|
for (const { t, needles } of perTask) {
|
|
517
509
|
// OBS-22: scopeGate accepts picomatch globs; advisory warnings must agree.
|
package/dist/compile/native.js
CHANGED
|
@@ -272,6 +272,11 @@ function testsAtHead(root) {
|
|
|
272
272
|
if (cached)
|
|
273
273
|
return cached;
|
|
274
274
|
const grep = gitSync(root, ["grep", "-I", "-n", "-e", "", "HEAD", "--", "tests/*.test.ts", "tests/**/*.test.ts"]);
|
|
275
|
+
if (grep.error || grep.signal || grep.status === null) {
|
|
276
|
+
const reason = grep.error?.message ?? (grep.signal ? `signal ${grep.signal}` : "no exit status");
|
|
277
|
+
throw new CompileError(`cannot complete the test corpus read (git grep HEAD tests) in ${root}: ${reason} — `
|
|
278
|
+
+ `restore the corpus read and recompile (queue row 27, D-2134).`);
|
|
279
|
+
}
|
|
275
280
|
if (grep.status !== 0 || typeof grep.stdout !== "string") {
|
|
276
281
|
headTestCache.set(root, []);
|
|
277
282
|
return [];
|
|
@@ -387,8 +392,8 @@ function criterionScopeFinding(task, text, criterion, id, tests) {
|
|
|
387
392
|
// OBS-898: the fence lint's corpus is listed ONCE per compile by git (tracked plus untracked-not-ignored,
|
|
388
393
|
// the same set a checkout walk sees minus the ignored trees) and filtered per task. The walk it replaces
|
|
389
394
|
// descended every dot-directory (.tickmarkr/runs alone held 3 799 files here) once PER TASK, so a
|
|
390
|
-
// 20-task spec compiled in seconds and the committed-spec corpus test timed out.
|
|
391
|
-
//
|
|
395
|
+
// 20-task spec compiled in seconds and the committed-spec corpus test timed out.
|
|
396
|
+
// Queue row 27 (D-2134): this advisory listing keeps its empty answer on git failure.
|
|
392
397
|
function repoFileList(root) {
|
|
393
398
|
const ls = gitSync(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"]);
|
|
394
399
|
if (ls.status !== 0 || typeof ls.stdout !== "string")
|
|
@@ -21,6 +21,14 @@ export interface JudgeVerdict {
|
|
|
21
21
|
}>;
|
|
22
22
|
}
|
|
23
23
|
export declare function judgeCriterionId(index: number): string;
|
|
24
|
+
export interface OracleFailure {
|
|
25
|
+
index: number;
|
|
26
|
+
kind: "command" | "test";
|
|
27
|
+
criterion: string;
|
|
28
|
+
landing?: string;
|
|
29
|
+
failure: "absent" | "elsewhere" | "untitled" | "red" | "unverified" | "no-runner";
|
|
30
|
+
output: string;
|
|
31
|
+
}
|
|
24
32
|
export declare function testFilterPattern(name: string): string;
|
|
25
33
|
export declare function testFiltered(testCmd: string, name: string): string;
|
|
26
34
|
export interface VitestListedTest {
|
package/dist/gates/acceptance.js
CHANGED
|
@@ -249,18 +249,18 @@ function oracleExecutionFailure(label, code, stdout, stderr) {
|
|
|
249
249
|
const zeroRun = testsRan(output) === 0 && classification !== "regression";
|
|
250
250
|
const zeroRunEvidence = zeroRun ? "the runner reported zero tests run and no failure identity; " : "";
|
|
251
251
|
if (classification === "infra" || signalKilled || zeroRun) {
|
|
252
|
-
return {
|
|
252
|
+
return { failure: "unverified", output, result: {
|
|
253
|
+
gate: "acceptance",
|
|
254
|
+
pass: false,
|
|
255
|
+
details: `oracle failed: ${label} (exit ${code}) — ${signalEvidence || zeroRunEvidence}infrastructure blocked execution before a verdict; this oracle verified nothing${tail(output)}`,
|
|
256
|
+
meta: { cause: "oracle-execution", classification: "infra", infra: true, retryable: false },
|
|
257
|
+
} };
|
|
258
|
+
}
|
|
259
|
+
return { failure: "red", output, result: {
|
|
253
260
|
gate: "acceptance",
|
|
254
261
|
pass: false,
|
|
255
|
-
details: `oracle failed: ${label} (exit ${code})
|
|
256
|
-
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
return {
|
|
260
|
-
gate: "acceptance",
|
|
261
|
-
pass: false,
|
|
262
|
-
details: `oracle failed: ${label} (exit ${code})${tail(stderr || stdout)}`,
|
|
263
|
-
};
|
|
262
|
+
details: `oracle failed: ${label} (exit ${code})${tail(stderr || stdout)}`,
|
|
263
|
+
} };
|
|
264
264
|
}
|
|
265
265
|
// today's rule for a test oracle's run: exit 0 AND a positive printed run count (OBS-55: a zero-match run is vacuous).
|
|
266
266
|
function countedTestRun(label, filter, r) {
|
|
@@ -269,8 +269,8 @@ function countedTestRun(label, filter, r) {
|
|
|
269
269
|
const out = (r.stderr || "") + "\n" + (r.stdout || "");
|
|
270
270
|
const ran = testsRan(out);
|
|
271
271
|
if (ran === null || ran < 1) {
|
|
272
|
-
return { gate: "acceptance", pass: false,
|
|
273
|
-
|
|
272
|
+
return { failure: "untitled", output: out, result: { gate: "acceptance", pass: false,
|
|
273
|
+
details: `oracle failed: ${label} — name filter matched zero tests (filter: ${filter})${tail(out)}` } };
|
|
274
274
|
}
|
|
275
275
|
return undefined;
|
|
276
276
|
}
|
|
@@ -290,11 +290,11 @@ function countedTestRun(label, filter, r) {
|
|
|
290
290
|
*/
|
|
291
291
|
async function landedTestOracle(testCmd, title, landing, worktree) {
|
|
292
292
|
const label = `test "${title}" in ${landing}`;
|
|
293
|
-
const passed = { gate: "acceptance", pass: true, details: `✓ test: ${title} (exit 0) in ${landing}` };
|
|
294
|
-
const work = (why) => ({ gate: "acceptance", pass: false, details: `oracle failed: ${label} — ${why}` });
|
|
295
|
-
const unverified = (why) => ({ gate: "acceptance", pass: false,
|
|
296
|
-
|
|
297
|
-
|
|
293
|
+
const passed = { output: "", result: { gate: "acceptance", pass: true, details: `✓ test: ${title} (exit 0) in ${landing}` } };
|
|
294
|
+
const work = (failure, why, output = "") => ({ failure, output, result: { gate: "acceptance", pass: false, details: `oracle failed: ${label} — ${why}` } });
|
|
295
|
+
const unverified = (why, output = "") => ({ failure: "unverified", output, result: { gate: "acceptance", pass: false,
|
|
296
|
+
details: `oracle failed: ${label} — ${why}; unverified, failing closed`,
|
|
297
|
+
meta: { cause: "oracle-execution", classification: "infra", infra: true, retryable: false } } });
|
|
298
298
|
const path = resolve(worktree, landing);
|
|
299
299
|
const within = relative(worktree, path);
|
|
300
300
|
let regular = false;
|
|
@@ -303,7 +303,7 @@ async function landedTestOracle(testCmd, title, landing, worktree) {
|
|
|
303
303
|
}
|
|
304
304
|
catch { /* absent */ }
|
|
305
305
|
if (!regular)
|
|
306
|
-
return work(`suite file ${landing} is absent from the worktree or is not a regular file; the test command never started`);
|
|
306
|
+
return work("absent", `suite file ${landing} is absent from the worktree or is not a regular file; the test command never started`);
|
|
307
307
|
if (!isVitestTestCommand(testCmd, worktree)) {
|
|
308
308
|
return countedTestRun(label, title, await sh(fileFiltered(npmForwarded(testCmd), landing, title), worktree)) ?? passed;
|
|
309
309
|
}
|
|
@@ -326,17 +326,18 @@ async function landedTestOracle(testCmd, title, landing, worktree) {
|
|
|
326
326
|
const r = await shell(`${cmd} --reporter=${shq(reporterPath)} --outputFile=${shq(reportPath)}`, worktree, DEFAULT_SHELL_TIMEOUT_MS, false, { env: { ...manifestChildEnv(worktree), TICKMARKR_TEST_REPORT: reportPath, TICKMARKR_TEST_NONCE: nonce } });
|
|
327
327
|
const raw = readTestReport(reportPath);
|
|
328
328
|
const report = raw?.nonce === nonce ? raw : undefined;
|
|
329
|
+
const output = [r.stderr, r.stdout].filter(Boolean).join("\n");
|
|
329
330
|
if (r.code !== 0) {
|
|
330
331
|
// the reporter replaces the runner's own output: its failure identities are the evidence of a red.
|
|
331
332
|
const named = report ? [...Object.values(report.completed).flatMap((c) => c.failures ?? []), ...(report.certificate?.diagnostics ?? [])] : [];
|
|
332
333
|
return oracleExecutionFailure(label, r.code, r.stdout, [r.stderr, ...named].filter(Boolean).join("\n"));
|
|
333
334
|
}
|
|
334
335
|
if (!raw)
|
|
335
|
-
return unverified("the runner exited 0 but wrote no per-file report");
|
|
336
|
+
return unverified("the runner exited 0 but wrote no per-file report", output);
|
|
336
337
|
if (!report)
|
|
337
|
-
return unverified(`the per-file report carries nonce "${raw.nonce}", not this invocation's
|
|
338
|
+
return unverified(`the per-file report carries nonce "${raw.nonce}", not this invocation's`, output);
|
|
338
339
|
if (report.certificate?.exitCode !== 0)
|
|
339
|
-
return unverified("the per-file report never reached its run-end certificate with exit 0");
|
|
340
|
+
return unverified("the per-file report never reached its run-end certificate with exit 0", output);
|
|
340
341
|
const key = toManifestPath(within, worktree);
|
|
341
342
|
// a module completed more than once (one path in several projects) keeps only its LAST counters, so its record
|
|
342
343
|
// cannot show that no earlier completion ran a body: it counts as run.
|
|
@@ -346,9 +347,9 @@ async function landedTestOracle(testCmd, title, landing, worktree) {
|
|
|
346
347
|
return file !== key && (repeated.has(file) || !t || t.passed + t.failed > 0);
|
|
347
348
|
}).sort();
|
|
348
349
|
if (elsewhere.length)
|
|
349
|
-
return work(`the runner's per-file report shows a test body run outside ${landing}, in ${elsewhere.join(", ")} — only ${landing} can satisfy this criterion
|
|
350
|
+
return work("elsewhere", `the runner's per-file report shows a test body run outside ${landing}, in ${elsewhere.join(", ")} — only ${landing} can satisfy this criterion`, output);
|
|
350
351
|
if (!((report.completed[key]?.tests?.passed ?? 0) > 0))
|
|
351
|
-
return work(`the runner's per-file report shows no passing test of that title in ${landing}
|
|
352
|
+
return work("untitled", `the runner's per-file report shows no passing test of that title in ${landing}`, output);
|
|
352
353
|
return passed;
|
|
353
354
|
}
|
|
354
355
|
finally {
|
|
@@ -456,38 +457,54 @@ function citesChangedLocation(evidence, changed) {
|
|
|
456
457
|
export async function acceptanceGate(task, worktree, baseRef,
|
|
457
458
|
// OBS-1182: the judge seat's OWN effort (absent = CLI default) — never the worker's.
|
|
458
459
|
judge, via, opts = {}) {
|
|
459
|
-
//
|
|
460
|
-
// A failure returns here, before any runLlm() call: a judge can never override it.
|
|
460
|
+
// Queue row 234(b) (D-2134): measure every deterministic oracle in declaration order before judging.
|
|
461
461
|
const passedDet = [];
|
|
462
|
-
|
|
462
|
+
const failures = [];
|
|
463
|
+
const record = (index, a, run) => {
|
|
464
|
+
if (run.result.pass)
|
|
465
|
+
passedDet.push(run.result.details);
|
|
466
|
+
else
|
|
467
|
+
failures.push({ run, oracle: {
|
|
468
|
+
index, kind: a.oracle, criterion: isCommand(a) ? a.command : a.test,
|
|
469
|
+
...(isTest(a) && a.landing !== undefined ? { landing: a.landing } : {}),
|
|
470
|
+
failure: run.failure, output: run.output,
|
|
471
|
+
} });
|
|
472
|
+
};
|
|
473
|
+
for (const [index, a] of task.acceptance.entries()) {
|
|
463
474
|
if (isCommand(a)) {
|
|
464
475
|
const r = await sh(a.command, worktree);
|
|
465
476
|
if (r.code !== 0) {
|
|
466
|
-
|
|
477
|
+
record(index, a, oracleExecutionFailure(`$ ${a.command}`, r.code, r.stdout, r.stderr));
|
|
467
478
|
}
|
|
468
|
-
|
|
479
|
+
else
|
|
480
|
+
passedDet.push(`✓ $ ${a.command} (exit 0)`);
|
|
469
481
|
}
|
|
470
482
|
else if (isTest(a)) {
|
|
471
483
|
if (!opts.testCmd) {
|
|
472
|
-
|
|
473
|
-
|
|
484
|
+
record(index, a, { failure: "no-runner", output: "", result: { gate: "acceptance", pass: false,
|
|
485
|
+
details: `oracle failed: test "${a.test}" — no test command configured to run it (failing closed)` } });
|
|
486
|
+
continue;
|
|
474
487
|
}
|
|
475
488
|
// queue row 179: a declared suite file is the only file that can satisfy its criterion.
|
|
476
489
|
if (a.landing !== undefined) {
|
|
477
|
-
|
|
478
|
-
if (!landed.pass)
|
|
479
|
-
return landed;
|
|
480
|
-
passedDet.push(landed.details);
|
|
490
|
+
record(index, a, await landedTestOracle(opts.testCmd, a.test, a.landing, worktree));
|
|
481
491
|
continue;
|
|
482
492
|
}
|
|
483
493
|
// OBS-55: exit 0 alone is vacuous when the name filter matched zero tests — fail closed.
|
|
484
494
|
const red = countedTestRun(`test "${a.test}"`, a.test, await sh(testFiltered(opts.testCmd, a.test), worktree));
|
|
485
495
|
if (red)
|
|
486
|
-
|
|
487
|
-
|
|
496
|
+
record(index, a, red);
|
|
497
|
+
else
|
|
498
|
+
passedDet.push(`✓ test: ${a.test} (exit 0)`);
|
|
488
499
|
}
|
|
489
500
|
}
|
|
490
|
-
|
|
501
|
+
if (failures.length) {
|
|
502
|
+
const first = failures[0].run.result;
|
|
503
|
+
const infra = failures.find(({ run }) => run.result.meta?.infra === true)?.run.result.meta;
|
|
504
|
+
return { ...first, details: failures.map(({ run }) => run.result.details).join("\n"),
|
|
505
|
+
meta: { ...first.meta, ...infra, oracles: failures.map(({ oracle }) => oracle) } };
|
|
506
|
+
}
|
|
507
|
+
// Queue row 234(b) (D-2134): only an entirely green deterministic round reaches the judge.
|
|
491
508
|
const judgeItems = task.acceptance.filter(isJudge);
|
|
492
509
|
const detBlock = passedDet.length ? passedDet.join("\n") + "\n" : "";
|
|
493
510
|
if (!judgeItems.length) {
|
package/dist/gates/run-gates.js
CHANGED
|
@@ -1147,7 +1147,21 @@ export async function runGates(task, ctx) {
|
|
|
1147
1147
|
invocations: [],
|
|
1148
1148
|
};
|
|
1149
1149
|
}
|
|
1150
|
-
|
|
1150
|
+
// Queue row 34(a) (D-2134 R5): resolve every lifetime author through both role pools.
|
|
1151
|
+
const channels = [...(ctx.judgeChannels ?? []), ...ctx.channels];
|
|
1152
|
+
const authors = ctx.carriedAuthors?.length ? ctx.carriedAuthors : [channelKey(ctx.author)];
|
|
1153
|
+
const authorVendors = carriedAuthorVendors(channels, authors);
|
|
1154
|
+
const independent = (seat) => !!seat.vendor
|
|
1155
|
+
&& !authorVendors.has(seat.vendor) && !authors.includes(channelKey(seat));
|
|
1156
|
+
const judgePool = () => (ctx.judgeChannels ?? []).filter((c) => disallowedBy(c, ctx.cfg.routing, "judge") === null);
|
|
1157
|
+
const rankJudges = (pool) => [...pool]
|
|
1158
|
+
.sort((x, y) => TIER_RANK[y.tier] - TIER_RANK[x.tier] || marginalCostRank(x) - marginalCostRank(y));
|
|
1159
|
+
const pickJudge = (pool, adapter) => rankJudges(pool.filter((c) => c.adapter !== adapter))[0] ?? rankJudges(pool)[0];
|
|
1160
|
+
const configured = channels.find((c) => channelKey(c) === channelKey(ctx.cfg.judge));
|
|
1161
|
+
const primary = authors.includes(channelKey(ctx.cfg.judge)) || (configured && authorVendors.has(configured.vendor))
|
|
1162
|
+
? pickJudge(judgePool().filter(independent), ctx.cfg.judge.adapter) ?? ctx.cfg.judge
|
|
1163
|
+
: ctx.cfg.judge;
|
|
1164
|
+
const judgeAdapter = getAdapter(primary.adapter, ctx.adapters);
|
|
1151
1165
|
const jvia = via
|
|
1152
1166
|
? { driver: via.driver, keep: via.keep, onSlot: via.onSlot, name: via.nameFor("judge", judgeAdapter.id), label: via.labelFor("judge") }
|
|
1153
1167
|
: undefined;
|
|
@@ -1181,11 +1195,11 @@ export async function runGates(task, ctx) {
|
|
|
1181
1195
|
...(unparseable ? { transcript: output } : {}),
|
|
1182
1196
|
});
|
|
1183
1197
|
}
|
|
1184
|
-
|
|
1198
|
+
const result = captured.value;
|
|
1199
|
+
const seat = channels.find((c) => channelKey(c) === result.meta?.judge);
|
|
1200
|
+
return typeof result.meta?.judge === "string" && result.meta.unparseable !== true && (!seat || !independent(seat))
|
|
1201
|
+
? { ...result, meta: { ...result.meta, judgeIndependent: false } } : result;
|
|
1185
1202
|
};
|
|
1186
|
-
const judgePool = () => (ctx.judgeChannels ?? []).filter((c) => disallowedBy(c, ctx.cfg.routing, "judge") === null);
|
|
1187
|
-
const rankJudges = (pool) => [...pool]
|
|
1188
|
-
.sort((x, y) => TIER_RANK[y.tier] - TIER_RANK[x.tier] || marginalCostRank(x) - marginalCostRank(y));
|
|
1189
1203
|
// OBS-1151 (+add.1): the fresh judgment always stands on its own reading — a prior PASS is never
|
|
1190
1204
|
// reused. Only a criterion that REVERSES the newest prior ruling on the same subject key over
|
|
1191
1205
|
// identical cited blobs needs a second, distinct judge; agreement stands (a sound FAIL included),
|
|
@@ -1210,16 +1224,18 @@ export async function runGates(task, ctx) {
|
|
|
1210
1224
|
if (!disputed.length)
|
|
1211
1225
|
return stamped;
|
|
1212
1226
|
await ctx.onGate?.({ phase: "note", gate: "acceptance", name: "judge-disagreement", payload: { primary, commit, disputed } });
|
|
1227
|
+
let notIndependent = fresh.meta?.judgeIndependent === false;
|
|
1228
|
+
const independence = () => notIndependent ? { judgeIndependent: false } : {};
|
|
1213
1229
|
const park = (why, adjudicator) => ({
|
|
1214
1230
|
gate: "acceptance", pass: false,
|
|
1215
1231
|
details: `judge disagreement on ${disputed.map((d) => d.id).join(", ")}: ${primary} reverses an earlier ruling over identical cited blobs (${[...new Set(disputed.flatMap((d) => d.paths))].join(", ")}) — ${why}; parked for an operator ruling, no worker charge`,
|
|
1216
|
-
meta: { classification: "infra", infra: true, retryable: false, cause: "judge-disagreement", judge: primary,
|
|
1232
|
+
meta: { classification: "infra", infra: true, retryable: false, cause: "judge-disagreement", judge: primary, ...independence(),
|
|
1217
1233
|
judgeDisagreement: { primary, ...(adjudicator ? { adjudicator } : {}), disputed, outcome: why } },
|
|
1218
1234
|
});
|
|
1219
1235
|
const primaryAdapter = primary.slice(0, primary.indexOf(":"));
|
|
1220
1236
|
// One DISTINCT seat: never the primary channel, a different adapter when the pool has one.
|
|
1221
1237
|
const pool = judgePool().filter((c) => channelKey(c) !== primary);
|
|
1222
|
-
const seat =
|
|
1238
|
+
const seat = pickJudge(pool.filter(independent), primaryAdapter) ?? pickJudge(pool, primaryAdapter);
|
|
1223
1239
|
if (!seat)
|
|
1224
1240
|
return park("no distinct eligible judge is available");
|
|
1225
1241
|
const adjudicator = channelKey(seat);
|
|
@@ -1230,13 +1246,14 @@ export async function runGates(task, ctx) {
|
|
|
1230
1246
|
const subjectPark = (leg, found, ran) => ({
|
|
1231
1247
|
gate: "acceptance", pass: false,
|
|
1232
1248
|
details: `the adjudication subject ${SUBJECT_LEG_TEXT[leg]}: sampled ${commit.slice(0, 12)}, found ${found.slice(0, 12)} (${leg}); parked for an operator ruling, no worker charge`,
|
|
1233
|
-
meta: { classification: "infra", infra: true, retryable: false, cause: "subject-moved", judge: primary,
|
|
1249
|
+
meta: { classification: "infra", infra: true, retryable: false, cause: "subject-moved", judge: primary, ...independence(),
|
|
1234
1250
|
adjudication: { primary, ...(ran ? { adjudicator: ran } : {}), sampled: commit, found, leg } },
|
|
1235
1251
|
});
|
|
1236
1252
|
const s0 = await openSubjectWitness(ctx.worktree, commit);
|
|
1237
1253
|
if ("leg" in s0)
|
|
1238
1254
|
return subjectPark(s0.leg, s0.leg === "unreadable" ? "unreadable" : commit);
|
|
1239
1255
|
const second = await invokeJudge(seatAdapter, seat.model, seatVia, configuredEffort(ctx.cfg, seat));
|
|
1256
|
+
notIndependent ||= second.meta?.judgeIndependent === false;
|
|
1240
1257
|
const changed = await adjudicationSubjectChange(ctx.worktree, commit, s0);
|
|
1241
1258
|
if (changed)
|
|
1242
1259
|
return subjectPark(changed.leg, changed.found, adjudicator);
|
|
@@ -1248,10 +1265,10 @@ export async function runGates(task, ctx) {
|
|
|
1248
1265
|
const agreed = disputed.every((d) => rulings.some((r) => r.id === d.id && r.met === d.met && r.paths.length > 0));
|
|
1249
1266
|
if (!agreed)
|
|
1250
1267
|
return park("the adjudicating judge split from the fresh ruling", adjudicator);
|
|
1251
|
-
return { ...stamped, meta: { ...stamped.meta, adjudication: { primary, adjudicator, criteria: disputed.map((d) => d.id), agreed: true } } };
|
|
1268
|
+
return { ...stamped, meta: { ...stamped.meta, ...independence(), adjudication: { primary, adjudicator, criteria: disputed.map((d) => d.id), agreed: true } } };
|
|
1252
1269
|
};
|
|
1253
1270
|
// OBS-1182: every judge seat launches at its OWN configured effort, never the worker's.
|
|
1254
|
-
let a = await invokeJudge(judgeAdapter,
|
|
1271
|
+
let a = await invokeJudge(judgeAdapter, primary.model, jvia, configuredEffort(ctx.cfg, primary));
|
|
1255
1272
|
// GATE-09: an unparseable judge verdict retries the JUDGE exactly once on a failover channel — never
|
|
1256
1273
|
// the worker (run-20260711-185020 P43-03 L70-72 billed a judge flake as a worker attempt). The flaked
|
|
1257
1274
|
// first verdict NEVER enters results (no false gate-result journal event, no operator notify, no stale
|
|
@@ -1268,18 +1285,10 @@ export async function runGates(task, ctx) {
|
|
|
1268
1285
|
if (!cancelled && a.meta?.unparseable === true && typeof a.meta.judge === "string") {
|
|
1269
1286
|
const flakedKey = a.meta.judge;
|
|
1270
1287
|
const flakedAdapter = flakedKey.slice(0, flakedKey.indexOf(":"));
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
// v1.87 T2: the failover seat obeys the same policy the primary judge just passed — a denied
|
|
1276
|
-
// channel is refused here too, never reached by falling through the exclusion arms below.
|
|
1277
|
-
const judgePool = (ctx.judgeChannels ?? []).filter((c) => disallowedBy(c, ctx.cfg.routing, "judge") === null);
|
|
1278
|
-
const crossAdapter = pick(judgePool.filter((c) => c.adapter !== flakedAdapter));
|
|
1279
|
-
const sameAdapter = pick(judgePool.filter((c) => c.adapter === flakedAdapter && channelKey(c) !== flakedKey));
|
|
1280
|
-
// Prefer a different adapter; if the fleet only has one adapter, retry on a different channel of
|
|
1281
|
-
// that adapter; if the fleet has only one channel, fall back to the original judge config.
|
|
1282
|
-
const retry = crossAdapter ?? sameAdapter ?? judgeSeat;
|
|
1288
|
+
// Queue row 34(a) (D-2134 R5): keep GATE-09's exclusions and fallback when independence is unavailable.
|
|
1289
|
+
const pool = judgePool().filter((c) => channelKey(c) !== flakedKey);
|
|
1290
|
+
const retry = pickJudge(pool.filter(independent), flakedAdapter)
|
|
1291
|
+
?? pickJudge(pool, flakedAdapter) ?? judgeSeat;
|
|
1283
1292
|
const retryAdapter = getAdapter(retry.adapter, ctx.adapters);
|
|
1284
1293
|
const retryJvia = via
|
|
1285
1294
|
// unconditional -r1 suffix: under keepPanes:forever a same-channel retry cannot collide with the
|