@yagni-app/code-staging 1.2.7-staging.2022.1 → 1.2.7-staging.2027.1
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/extension/pipeline/headlessGo.d.ts +3 -1
- package/dist/extension/pipeline/headlessGo.js +68 -19
- package/dist/extension/pipeline/missionChecks.d.ts +6 -0
- package/dist/extension/pipeline/missionChecks.js +6 -1
- package/dist/extension/pipeline/verify.d.ts +73 -0
- package/dist/extension/pipeline/verify.js +235 -26
- package/package.json +2 -2
|
@@ -74,7 +74,7 @@ import { snapshotWorkspace } from "./workspace.js";
|
|
|
74
74
|
import type { WorktreeExec } from "./worktree.js";
|
|
75
75
|
import { type TreeSnapshotDeps } from "./treeSnapshot.js";
|
|
76
76
|
import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
|
|
77
|
-
import { makeRunVerify, type VerifyBaselineResult } from "./verify.js";
|
|
77
|
+
import { makeMeasureChecksOnBase, makeRunVerify, type VerifyBaselineResult } from "./verify.js";
|
|
78
78
|
import { type RunBudget } from "./budget.js";
|
|
79
79
|
import type { Finding, JsonEvent, ModelTier, PipelineStage, StageId, StageTag, StageUsage, StopReason } from "./types.js";
|
|
80
80
|
/** One-line usage copy, shared by every argument error. */
|
|
@@ -181,6 +181,8 @@ export interface HeadlessGoDeps {
|
|
|
181
181
|
* there is none). Defaults to the real gate; injected in tests.
|
|
182
182
|
*/
|
|
183
183
|
makeRunVerify?: typeof makeRunVerify;
|
|
184
|
+
/** The approved checks' run on the unchanged code (`measureOnBase`). Defaults to the real one; injected in tests. */
|
|
185
|
+
measureChecksOnBase?: ReturnType<typeof makeMeasureChecksOnBase>;
|
|
184
186
|
/** The verify gate's clean-tree baseline run (YAG-1003). Defaults to the real one; injected in tests. */
|
|
185
187
|
takeVerifyBaseline?: (cwd: string, dirs: string[], signal?: AbortSignal, budgetMs?: number) => Promise<VerifyBaselineResult>;
|
|
186
188
|
/** stdout sink; one call per NDJSON line, newline added by the caller. */
|
|
@@ -85,7 +85,7 @@ import { normalizeMission } from "./mission.js";
|
|
|
85
85
|
import { MISSION_CHECKS_FILE_NAME, MISSION_CI_RESULTS_FILE_NAME, readMissionChecksFile, renderCiResults, } from "./missionChecks.js";
|
|
86
86
|
import { startTreeSnapshots } from "./treeSnapshot.js";
|
|
87
87
|
import { PipelineStageError, runPipeline as defaultRunPipeline, } from "./orchestrator.js";
|
|
88
|
-
import { baselineDirs, makeRunVerify, makeTakeVerifyBaseline, makeWorkstreamCheck, parseChangedPaths, planPaths, } from "./verify.js";
|
|
88
|
+
import { baselineDirs, makeMeasureChecksOnBase, makeRunVerify, makeTakeVerifyBaseline, makeWorkstreamCheck, parseChangedPaths, planPaths, } from "./verify.js";
|
|
89
89
|
import { loadGroundingEnabled } from "../grounding.js";
|
|
90
90
|
import { codeStateHome } from "../stateHome.js";
|
|
91
91
|
import { parseTierCap, TIER_CAP_ENV } from "./tierCap.js";
|
|
@@ -451,42 +451,90 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
451
451
|
// the baseline there would excuse the attempt's own breakage). It then leans
|
|
452
452
|
// on `checks.json`, whose base standing the host took on the real base.
|
|
453
453
|
let baseline;
|
|
454
|
-
const
|
|
455
|
-
deps.logger?.(
|
|
454
|
+
const logPipeline = (event, data) => {
|
|
455
|
+
deps.logger?.(event, data);
|
|
456
456
|
if (stream)
|
|
457
|
-
emit({ type: "pipeline_log", event
|
|
457
|
+
emit({ type: "pipeline_log", event, data });
|
|
458
|
+
};
|
|
459
|
+
// A measurement before the build (the baseline, the plan's checks on the
|
|
460
|
+
// base) gets at most a sixth of the run's time left when it starts, and
|
|
461
|
+
// none below a minute: the build itself must still fit. No deadline, no cap.
|
|
462
|
+
const preBuildShare = () => {
|
|
463
|
+
const leftMs = budget?.deadlineMs === undefined ? undefined : budget.deadlineMs - (deps.now ?? Date.now)();
|
|
464
|
+
const shareMs = leftMs === undefined ? undefined : Math.floor(leftMs / BASELINE_SHARE_OF_TIME_LEFT);
|
|
465
|
+
return {
|
|
466
|
+
leftMs,
|
|
467
|
+
shareMs,
|
|
468
|
+
tooLittle: shareMs !== undefined && shareMs < MIN_BASELINE_BUDGET_MS,
|
|
469
|
+
};
|
|
458
470
|
};
|
|
459
|
-
// With a deadline, the baseline gets at most a sixth of the run's time left,
|
|
460
|
-
// and none below a minute: the build itself must still fit.
|
|
461
|
-
const deadlineLeftMs = budget?.deadlineMs === undefined ? undefined : budget.deadlineMs - (deps.now ?? Date.now)();
|
|
462
|
-
const baselineBudgetMs = deadlineLeftMs === undefined
|
|
463
|
-
? undefined
|
|
464
|
-
: Math.floor(deadlineLeftMs / BASELINE_SHARE_OF_TIME_LEFT);
|
|
465
471
|
if (mission?.plan && !continuation && !fixRound && !deps.signal?.aborted) {
|
|
466
472
|
const dirs = baselineDirs(cwd, planPaths(mission.plan), readFile);
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
logBaseline({
|
|
473
|
+
const share = preBuildShare();
|
|
474
|
+
if (dirs.length > 0 && share.tooLittle) {
|
|
475
|
+
logPipeline("verify_baseline", {
|
|
471
476
|
skipped: "too little of the run's time left",
|
|
472
|
-
deadlineLeftMs,
|
|
473
|
-
baselineBudgetMs,
|
|
477
|
+
deadlineLeftMs: share.leftMs,
|
|
478
|
+
baselineBudgetMs: share.shareMs,
|
|
474
479
|
});
|
|
475
480
|
}
|
|
476
481
|
else if (dirs.length > 0) {
|
|
477
482
|
try {
|
|
478
|
-
const taken = await (deps.takeVerifyBaseline ?? makeTakeVerifyBaseline())(cwd, dirs, deps.signal,
|
|
479
|
-
|
|
483
|
+
const taken = await (deps.takeVerifyBaseline ?? makeTakeVerifyBaseline())(cwd, dirs, deps.signal, share.shareMs);
|
|
484
|
+
logPipeline("verify_baseline", {
|
|
485
|
+
ran: taken.ran,
|
|
486
|
+
failures: taken.failures,
|
|
487
|
+
notes: taken.notes,
|
|
488
|
+
});
|
|
480
489
|
if (taken.baseline.size > 0)
|
|
481
490
|
baseline = taken.baseline;
|
|
482
491
|
}
|
|
483
492
|
catch (err) {
|
|
484
493
|
const message = err instanceof Error ? err.message : String(err);
|
|
485
|
-
|
|
494
|
+
logPipeline("verify_baseline", { error: message });
|
|
486
495
|
writeErr(`The verify baseline could not run (every failure counts): ${message}`);
|
|
487
496
|
}
|
|
488
497
|
}
|
|
489
498
|
}
|
|
499
|
+
// The approved plan's checks the host has not seen pass on the unchanged
|
|
500
|
+
// code (`measureOnBase`), each run once there before the build, on the same
|
|
501
|
+
// fresh tree the baseline above reads: a check that already failed there is
|
|
502
|
+
// not this change's to fix. A continuation or a fix round carries a change,
|
|
503
|
+
// so it measures nothing; its host hands it what an earlier session found.
|
|
504
|
+
let checksOnBase;
|
|
505
|
+
const measureFile = gateOpts?.repoChecks?.() ?? null;
|
|
506
|
+
if (measureFile?.checks.some((check) => check.measureOnBase) &&
|
|
507
|
+
!continuation &&
|
|
508
|
+
!fixRound &&
|
|
509
|
+
!deps.signal?.aborted) {
|
|
510
|
+
const share = preBuildShare();
|
|
511
|
+
if (share.tooLittle) {
|
|
512
|
+
logPipeline("verify_checks_on_base", {
|
|
513
|
+
skipped: "too little of the run's time left",
|
|
514
|
+
leftMs: share.leftMs,
|
|
515
|
+
shareMs: share.shareMs,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
try {
|
|
520
|
+
const measured = await (deps.measureChecksOnBase ?? makeMeasureChecksOnBase())(cwd, measureFile, deps.signal, share.shareMs);
|
|
521
|
+
logPipeline("verify_checks_on_base", {
|
|
522
|
+
ran: measured.ran,
|
|
523
|
+
passed: [...measured.passed],
|
|
524
|
+
failed: [...measured.failed.keys()],
|
|
525
|
+
notes: measured.notes,
|
|
526
|
+
treeUndone: measured.treeUndone,
|
|
527
|
+
});
|
|
528
|
+
if (measured.passed.size > 0 || measured.failed.size > 0)
|
|
529
|
+
checksOnBase = measured;
|
|
530
|
+
}
|
|
531
|
+
catch (err) {
|
|
532
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
533
|
+
logPipeline("verify_checks_on_base", { error: message });
|
|
534
|
+
writeErr(`The approved checks could not run on the unchanged code (each keeps its checks.json standing): ${message}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
490
538
|
// The last gate verdict's count of failures the clean tree already had, for the result.
|
|
491
539
|
let preexistingFailures = 0;
|
|
492
540
|
let runVerify;
|
|
@@ -506,6 +554,7 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
506
554
|
const made = (deps.makeRunVerify ?? makeRunVerify)({
|
|
507
555
|
...gateOpts,
|
|
508
556
|
...(baseline ? { baseline } : {}),
|
|
557
|
+
...(checksOnBase ? { checksOnBase } : {}),
|
|
509
558
|
...(scan ? { scanChange: scan } : {}),
|
|
510
559
|
...(cloud ? { unreadTestFailureIsFinding: true } : {}),
|
|
511
560
|
});
|
|
@@ -21,6 +21,12 @@ export interface MissionCheck {
|
|
|
21
21
|
command: string;
|
|
22
22
|
status: MissionCheckStatus;
|
|
23
23
|
cause?: string;
|
|
24
|
+
/**
|
|
25
|
+
* The host has not seen it pass on the unchanged code (an approved plan's
|
|
26
|
+
* check): the session runs it there once before the build, and what that
|
|
27
|
+
* measures stands over `status` (`makeMeasureChecksOnBase` in verify.ts).
|
|
28
|
+
*/
|
|
29
|
+
measureOnBase?: boolean;
|
|
24
30
|
}
|
|
25
31
|
export interface MissionCiStep {
|
|
26
32
|
name: string;
|
|
@@ -91,7 +91,10 @@ function parseJob(value) {
|
|
|
91
91
|
return null;
|
|
92
92
|
if (failed.step !== undefined && !isString(failed.step))
|
|
93
93
|
return null;
|
|
94
|
-
job.failedCheck = {
|
|
94
|
+
job.failedCheck = {
|
|
95
|
+
name: failed.name,
|
|
96
|
+
...(isString(failed.step) ? { step: failed.step } : {}),
|
|
97
|
+
};
|
|
95
98
|
}
|
|
96
99
|
return job;
|
|
97
100
|
}
|
|
@@ -131,6 +134,8 @@ export function parseMissionChecksFile(text) {
|
|
|
131
134
|
const check = { command: raw.command, status: raw.status };
|
|
132
135
|
if (isString(raw.cause))
|
|
133
136
|
check.cause = raw.cause;
|
|
137
|
+
if (raw.measureOnBase === true)
|
|
138
|
+
check.measureOnBase = true;
|
|
134
139
|
checks.push(check);
|
|
135
140
|
}
|
|
136
141
|
const ciJobs = [];
|
|
@@ -108,6 +108,18 @@ export interface VerifyOutcome {
|
|
|
108
108
|
}
|
|
109
109
|
/** An approved check's name on the report: its command, cut at 80 characters. */
|
|
110
110
|
export declare function checkLabel(command: string): string;
|
|
111
|
+
/**
|
|
112
|
+
* A command as the gate compares two: the same words, whatever spaces sit
|
|
113
|
+
* between them, and a package manager's script named the one way it runs,
|
|
114
|
+
* so `yarn test` and `yarn run test`, `npm test` and `npm run test`, `pnpm
|
|
115
|
+
* lint` and `pnpm run lint` are one check, run once (staging bench
|
|
116
|
+
* 2026-10-25: the gate's own `test` script and two approved twins ran one
|
|
117
|
+
* suite three times). Only the leading form: a manager's flags before the
|
|
118
|
+
* script leave the command as it is. The same rule as
|
|
119
|
+
* `canonicalCheckCommand` in the backend's attempts/inputs.ts; both test
|
|
120
|
+
* files read one corpus, test/fixtures/equivalentCheckCommands.json. Pure.
|
|
121
|
+
*/
|
|
122
|
+
export declare function canonicalCheckCommand(command: string): string;
|
|
111
123
|
/**
|
|
112
124
|
* True when a command changes the checkout's git state: a commit, a branch
|
|
113
125
|
* created or switched, a reset, a stash, a push. `git branch`, `git remote`
|
|
@@ -455,6 +467,13 @@ export interface MakeRunVerifyOpts {
|
|
|
455
467
|
* findings. Absent: every failure is a finding, as before.
|
|
456
468
|
*/
|
|
457
469
|
baseline?: VerifyBaseline | null;
|
|
470
|
+
/**
|
|
471
|
+
* What the approved checks `checks.json` marks `measureOnBase` did on the
|
|
472
|
+
* unchanged code ({@link makeMeasureChecksOnBase}). It stands over each
|
|
473
|
+
* one's `status`: one that passed there must pass, and one that failed
|
|
474
|
+
* there is the change's only where it fails anew. Absent: `status` alone.
|
|
475
|
+
*/
|
|
476
|
+
checksOnBase?: ChecksOnBase | null;
|
|
458
477
|
/**
|
|
459
478
|
* The cloud session's rule (spec decision 3): a test run that exits
|
|
460
479
|
* non-zero with output no parser reads is a finding carrying its output
|
|
@@ -584,5 +603,59 @@ export interface VerifyBaselineResult {
|
|
|
584
603
|
* write into the tree is undone, so none of it reaches the candidate.
|
|
585
604
|
*/
|
|
586
605
|
export declare function makeTakeVerifyBaseline(opts?: MakeTakeVerifyBaselineOpts): (cwd: string, dirs: string[], signal?: AbortSignal, budgetOverrideMs?: number) => Promise<VerifyBaselineResult>;
|
|
606
|
+
/**
|
|
607
|
+
* What the approved checks marked `measureOnBase` did on the unchanged code,
|
|
608
|
+
* by command (trimmed): each that passed there, and each that failed there
|
|
609
|
+
* with failures the gate reads, with how often each failure key was seen
|
|
610
|
+
* (none for a failure only a summary line names). A command in neither gave
|
|
611
|
+
* no verdict there, and keeps the status `checks.json` gave it.
|
|
612
|
+
*/
|
|
613
|
+
export interface ChecksOnBase {
|
|
614
|
+
passed: ReadonlySet<string>;
|
|
615
|
+
failed: ReadonlyMap<string, ReadonlyMap<string, number>>;
|
|
616
|
+
}
|
|
617
|
+
/** {@link ChecksOnBase}, with what ran and why the rest gave no verdict, for the run's log. */
|
|
618
|
+
export interface ChecksOnBaseResult extends ChecksOnBase {
|
|
619
|
+
ran: string[];
|
|
620
|
+
notes: string[];
|
|
621
|
+
/**
|
|
622
|
+
* What the checks wrote into the tree was undone (or they wrote nothing).
|
|
623
|
+
* False when the tree could not be read before them or the undo failed:
|
|
624
|
+
* the build then starts on a tree the checks may have dirtied.
|
|
625
|
+
*/
|
|
626
|
+
treeUndone: boolean;
|
|
627
|
+
}
|
|
628
|
+
/** The whole measurement's cap, before the build starts: the approved plan's own checks, once each. */
|
|
629
|
+
export declare const CHECKS_ON_BASE_BUDGET_MS: number;
|
|
630
|
+
export interface MakeMeasureChecksOnBaseOpts {
|
|
631
|
+
exec?: VerifyExec;
|
|
632
|
+
/** Repository-checks env override for determinism in tests (default: the gate's own). */
|
|
633
|
+
repoChecksEnv?: NodeJS.ProcessEnv;
|
|
634
|
+
toolchain?: () => ToolchainFile | null;
|
|
635
|
+
/** One check's cap (default {@link REPO_CHECK_TIMEOUT_MS}). */
|
|
636
|
+
timeoutMs?: number;
|
|
637
|
+
budgetMs?: number;
|
|
638
|
+
now?: () => number;
|
|
639
|
+
treeStatus?: (cwd: string, signal?: AbortSignal) => Promise<string>;
|
|
640
|
+
undoTreeWrites?: (cwd: string, entries: TreeEntry[]) => Promise<void>;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Run each approved check `checks.json` marks `measureOnBase` once on the
|
|
644
|
+
* unchanged code, before the build changes anything: a plan's check the
|
|
645
|
+
* host has not seen pass there (none of the record's, or a red one the
|
|
646
|
+
* record may hold from long ago). A check the plan wrote for the change
|
|
647
|
+
* often fails there by design (its test file is not written yet), and a
|
|
648
|
+
* failure that says only that (a file not found, no tests to run, output no
|
|
649
|
+
* parser reads) is no verdict: its failures all count after the build, as
|
|
650
|
+
* before. A failure the gate reads (failing tests, located diagnostics, a
|
|
651
|
+
* test runner's failure summary) is one the unchanged code already has.
|
|
652
|
+
*
|
|
653
|
+
* Run as the gate runs the check ({@link makeRunVerify}): the same command
|
|
654
|
+
* line, environment and toolchain, from the repository root, so a failure
|
|
655
|
+
* keys the same on both trees. Bounded: each check by its cap, the whole by
|
|
656
|
+
* the budget, and one not reached in it has no verdict. What the checks
|
|
657
|
+
* write into the tree is undone.
|
|
658
|
+
*/
|
|
659
|
+
export declare function makeMeasureChecksOnBase(opts?: MakeMeasureChecksOnBaseOpts): (cwd: string, file: MissionChecksFile, signal?: AbortSignal, budgetOverrideMs?: number) => Promise<ChecksOnBaseResult>;
|
|
587
660
|
export {};
|
|
588
661
|
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -111,6 +111,44 @@ const VERIFY_LENS = "does_it_hold";
|
|
|
111
111
|
export function checkLabel(command) {
|
|
112
112
|
return command.length > 80 ? `${command.slice(0, 77)}...` : command;
|
|
113
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Package-manager words that are the manager's own command, never a script:
|
|
116
|
+
* `yarn add`, `pnpm install`. Any other word after `yarn` or `pnpm` runs the
|
|
117
|
+
* script of that name, as `run` does.
|
|
118
|
+
*/
|
|
119
|
+
const YARN_COMMANDS = new Set("add audit autoclean bin cache check config constraints create dedupe dlx exec explain generate-lock-entry global help import info init install licenses link list login logout node npm outdated owner pack patch patch-commit plugin policies publish rebuild remove run set stage tag team unlink unplug up upgrade upgrade-interactive version versions why workspace workspaces".split(" "));
|
|
120
|
+
const PNPM_COMMANDS = new Set("add audit bin cache cat-file cat-index config create dedupe deploy dlx doctor env exec fetch find-hash i import init install install-test it licenses link list ln ls outdated pack patch patch-commit patch-remove prune publish rb rebuild recursive remove rm root run self-update server setup store un uninstall unlink up update why".split(" "));
|
|
121
|
+
/** The scripts npm runs without `run`: its lifecycle shorthands. */
|
|
122
|
+
const NPM_SCRIPT_SHORTHANDS = new Set(["test", "start", "stop", "restart"]);
|
|
123
|
+
/**
|
|
124
|
+
* A command as the gate compares two: the same words, whatever spaces sit
|
|
125
|
+
* between them, and a package manager's script named the one way it runs,
|
|
126
|
+
* so `yarn test` and `yarn run test`, `npm test` and `npm run test`, `pnpm
|
|
127
|
+
* lint` and `pnpm run lint` are one check, run once (staging bench
|
|
128
|
+
* 2026-10-25: the gate's own `test` script and two approved twins ran one
|
|
129
|
+
* suite three times). Only the leading form: a manager's flags before the
|
|
130
|
+
* script leave the command as it is. The same rule as
|
|
131
|
+
* `canonicalCheckCommand` in the backend's attempts/inputs.ts; both test
|
|
132
|
+
* files read one corpus, test/fixtures/equivalentCheckCommands.json. Pure.
|
|
133
|
+
*/
|
|
134
|
+
export function canonicalCheckCommand(command) {
|
|
135
|
+
const words = command.trim().split(/\s+/).filter(Boolean);
|
|
136
|
+
const [manager, next, ...rest] = words;
|
|
137
|
+
const script = /^[A-Za-z0-9][\w:.@/-]*$/;
|
|
138
|
+
if (manager === "npm" || manager === "yarn" || manager === "pnpm") {
|
|
139
|
+
const runs = next === "run" || (manager === "npm" && next === "run-script");
|
|
140
|
+
if (runs && rest[0] && script.test(rest[0]))
|
|
141
|
+
return [manager, "run", ...rest].join(" ");
|
|
142
|
+
const shorthand = next !== undefined &&
|
|
143
|
+
script.test(next) &&
|
|
144
|
+
(manager === "npm"
|
|
145
|
+
? NPM_SCRIPT_SHORTHANDS.has(next)
|
|
146
|
+
: !(manager === "yarn" ? YARN_COMMANDS : PNPM_COMMANDS).has(next));
|
|
147
|
+
if (shorthand)
|
|
148
|
+
return [manager, "run", next, ...rest].join(" ");
|
|
149
|
+
}
|
|
150
|
+
return words.join(" ");
|
|
151
|
+
}
|
|
114
152
|
/** Git subcommands that only read: a check may run them as often as it likes. */
|
|
115
153
|
const READ_ONLY_GIT = new Set("status diff log show ls-files ls-tree rev-parse rev-list check-ignore grep blame describe cat-file shortlog diff-tree merge-base for-each-ref show-ref name-rev count-objects var version help".split(" "));
|
|
116
154
|
/**
|
|
@@ -1139,6 +1177,38 @@ export function makeWorkstreamCheck(opts = {}) {
|
|
|
1139
1177
|
return results;
|
|
1140
1178
|
};
|
|
1141
1179
|
}
|
|
1180
|
+
/**
|
|
1181
|
+
* A failing repository command's findings, located from the folder it ran in
|
|
1182
|
+
* and prefixed with what ran: the gate's on the build's tree, and the
|
|
1183
|
+
* unchanged code's measurement ({@link makeMeasureChecksOnBase}), so a failure
|
|
1184
|
+
* keys the same on both. Pure.
|
|
1185
|
+
*/
|
|
1186
|
+
function repoCommandFindings(failure, at) {
|
|
1187
|
+
const limit = at.limit ?? MAX_LOCATED_FINDINGS;
|
|
1188
|
+
const cls = classifyTestFailure(failure.output, failure.code, limit);
|
|
1189
|
+
const parsed = cls.kind === "failures"
|
|
1190
|
+
? cls.findings
|
|
1191
|
+
: parseVerifyFailures(failure.output, failure.code, at.label, limit);
|
|
1192
|
+
return parsed.map((f) => ({
|
|
1193
|
+
...liftFinding(f, at.repoRoot, at.dir),
|
|
1194
|
+
message: `${at.prefix}: ${f.message}`,
|
|
1195
|
+
}));
|
|
1196
|
+
}
|
|
1197
|
+
/** What an approved check's failure is prefixed with, on the build's tree and on the unchanged code. */
|
|
1198
|
+
const approvedPrefix = (label) => `approved check \`${label}\``;
|
|
1199
|
+
/**
|
|
1200
|
+
* An approved check's standing on the unchanged code. What the session
|
|
1201
|
+
* measured there stands over what the host handed it: a red the record kept
|
|
1202
|
+
* may be stale.
|
|
1203
|
+
*/
|
|
1204
|
+
function measuredBaseline(onBase, check) {
|
|
1205
|
+
const command = check.command.trim();
|
|
1206
|
+
if (onBase?.failed.has(command))
|
|
1207
|
+
return "fails_on_base";
|
|
1208
|
+
if (onBase?.passed.has(command))
|
|
1209
|
+
return "runnable";
|
|
1210
|
+
return check.status;
|
|
1211
|
+
}
|
|
1142
1212
|
/**
|
|
1143
1213
|
* Build the runVerify function injected into the orchestrator. It discovers the
|
|
1144
1214
|
* changed package(s) (scoped to this run's diff via `baselinePaths`), detects a
|
|
@@ -1292,7 +1362,7 @@ export function makeRunVerify(opts = {}) {
|
|
|
1292
1362
|
});
|
|
1293
1363
|
try {
|
|
1294
1364
|
await runApprovedChecks(run, approved);
|
|
1295
|
-
await runCiSteps(run, steps, new Set(file.checks.map((check) => check.command
|
|
1365
|
+
await runCiSteps(run, steps, new Set(file.checks.map((check) => canonicalCheckCommand(check.command))));
|
|
1296
1366
|
}
|
|
1297
1367
|
finally {
|
|
1298
1368
|
// Whatever ran (an aborted half included) is what the host reads. A half
|
|
@@ -1329,15 +1399,19 @@ export function makeRunVerify(opts = {}) {
|
|
|
1329
1399
|
const startedAt = now();
|
|
1330
1400
|
try {
|
|
1331
1401
|
const { code, output: rawOutput } = await exec(repoCheckArgv(command), dir, composed, env);
|
|
1332
|
-
const record = (baseline, note) =>
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1402
|
+
const record = (baseline, note) => {
|
|
1403
|
+
const entry = {
|
|
1404
|
+
name: check.name,
|
|
1405
|
+
command,
|
|
1406
|
+
cwd: relative(run.cwd, dir) || ".",
|
|
1407
|
+
exitCode: note ? null : code,
|
|
1408
|
+
durationMs: now() - startedAt,
|
|
1409
|
+
baseline,
|
|
1410
|
+
...(note ? { note } : {}),
|
|
1411
|
+
};
|
|
1412
|
+
run.checks.push(entry);
|
|
1413
|
+
return entry;
|
|
1414
|
+
};
|
|
1341
1415
|
if (code === 0) {
|
|
1342
1416
|
record(check.baseline);
|
|
1343
1417
|
return { kind: "passed" };
|
|
@@ -1349,8 +1423,7 @@ export function makeRunVerify(opts = {}) {
|
|
|
1349
1423
|
record("cannot_run", `${reason}; ${exitSummary(check.name, code, output)}`);
|
|
1350
1424
|
return { kind: "note", reason };
|
|
1351
1425
|
}
|
|
1352
|
-
record(check.baseline);
|
|
1353
|
-
return { kind: "failed", output, code };
|
|
1426
|
+
return { kind: "failed", output, code, record: record(check.baseline) };
|
|
1354
1427
|
}
|
|
1355
1428
|
catch (err) {
|
|
1356
1429
|
run.checks.push({
|
|
@@ -1368,14 +1441,7 @@ export function makeRunVerify(opts = {}) {
|
|
|
1368
1441
|
}
|
|
1369
1442
|
/** A failure's findings, located from the folder the command ran in and prefixed with what ran. */
|
|
1370
1443
|
function repoFailureFindings(run, failure, label, prefix, dir) {
|
|
1371
|
-
|
|
1372
|
-
const parsed = cls.kind === "failures"
|
|
1373
|
-
? cls.findings
|
|
1374
|
-
: parseVerifyFailures(failure.output, failure.code, label);
|
|
1375
|
-
return parsed.map((f) => ({
|
|
1376
|
-
...liftFinding(f, run.cwd, dir),
|
|
1377
|
-
message: `${prefix}: ${f.message}`,
|
|
1378
|
-
}));
|
|
1444
|
+
return repoCommandFindings(failure, { label, prefix, repoRoot: run.cwd, dir });
|
|
1379
1445
|
}
|
|
1380
1446
|
const findingRoom = (run) => Math.max(0, MAX_LOCATED_FINDINGS - run.findings.length);
|
|
1381
1447
|
/**
|
|
@@ -1398,22 +1464,42 @@ export function makeRunVerify(opts = {}) {
|
|
|
1398
1464
|
run.reasons.push(`approved check \`${label}\` not run: it changes the checkout's git state (a branch, a commit or a reset), which a check must never do`);
|
|
1399
1465
|
continue;
|
|
1400
1466
|
}
|
|
1467
|
+
// The failures it had there, when the session measured it failing.
|
|
1468
|
+
const failedOnBase = opts.checksOnBase?.failed.get(check.command.trim());
|
|
1469
|
+
const status = measuredBaseline(opts.checksOnBase, check);
|
|
1401
1470
|
const verdict = await runRepoCommand(run, check.command, run.cwd, undefined, {
|
|
1402
1471
|
name: label,
|
|
1403
|
-
baseline:
|
|
1472
|
+
baseline: status,
|
|
1404
1473
|
});
|
|
1405
1474
|
if (verdict.kind === "failed")
|
|
1406
|
-
run.approvedFailed.add(check.command
|
|
1475
|
+
run.approvedFailed.add(canonicalCheckCommand(check.command));
|
|
1407
1476
|
if (verdict.kind === "note") {
|
|
1408
1477
|
run.reasons.push(`approved check \`${label}\` ${verdict.reason}`);
|
|
1409
1478
|
}
|
|
1410
|
-
else if (verdict.kind === "failed" &&
|
|
1479
|
+
else if (verdict.kind === "failed" && failedOnBase) {
|
|
1480
|
+
// Only what the unchanged code did not already fail is this change's.
|
|
1481
|
+
// A failure no parser reads is excused only when the unchanged code's
|
|
1482
|
+
// was unread too (a summary line, as PHPUnit prints); after readable
|
|
1483
|
+
// failures there, it says the change broke the run itself.
|
|
1484
|
+
const found = repoFailureFindings(run, verdict, label, approvedPrefix(label), run.cwd);
|
|
1485
|
+
const fresh = subtractBaseline(found, failedOnBase).findings.filter((finding) => failedOnBase.size > 0 || verifyFailureKey(finding) !== null);
|
|
1486
|
+
if (fresh.length === 0) {
|
|
1487
|
+
run.reasons.push(`approved check \`${label}\` failed as it did on the unchanged code before the build; not this change's`);
|
|
1488
|
+
}
|
|
1489
|
+
else {
|
|
1490
|
+
// Its record reads as the change's failure, for the host too.
|
|
1491
|
+
verdict.record.baseline = "runnable";
|
|
1492
|
+
run.labels.push(label);
|
|
1493
|
+
run.findings.push(...fresh.slice(0, findingRoom(run)));
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
else if (verdict.kind === "failed" && status === "fails_on_base") {
|
|
1411
1497
|
run.reasons.push(`approved check \`${label}\` failed, and it already failed on the unchanged code; the host compares the two after the build`);
|
|
1412
1498
|
}
|
|
1413
1499
|
else {
|
|
1414
1500
|
run.labels.push(label);
|
|
1415
1501
|
if (verdict.kind === "failed") {
|
|
1416
|
-
const found = repoFailureFindings(run, verdict, label,
|
|
1502
|
+
const found = repoFailureFindings(run, verdict, label, approvedPrefix(label), run.cwd);
|
|
1417
1503
|
run.findings.push(...found.slice(0, findingRoom(run)));
|
|
1418
1504
|
}
|
|
1419
1505
|
}
|
|
@@ -1450,8 +1536,8 @@ export function makeRunVerify(opts = {}) {
|
|
|
1450
1536
|
return;
|
|
1451
1537
|
if (stoppedJobs.has(job))
|
|
1452
1538
|
continue;
|
|
1453
|
-
if (approvedCommands.has(step.run
|
|
1454
|
-
if (run.approvedFailed.has(step.run
|
|
1539
|
+
if (approvedCommands.has(canonicalCheckCommand(step.run))) {
|
|
1540
|
+
if (run.approvedFailed.has(canonicalCheckCommand(step.run)))
|
|
1455
1541
|
settle(job, "failed");
|
|
1456
1542
|
else
|
|
1457
1543
|
stepPassed(job);
|
|
@@ -1602,8 +1688,20 @@ export function makeRunVerify(opts = {}) {
|
|
|
1602
1688
|
else {
|
|
1603
1689
|
const testEnv = opts.testEnv ?? buildTestEnv();
|
|
1604
1690
|
let testFindingCount = 0;
|
|
1691
|
+
// A root test script an approved check runs too (`yarn run test`
|
|
1692
|
+
// beside `yarn test`) runs once, as that check, on the repository's
|
|
1693
|
+
// toolchain: the staging bench (2026-10-25) ran one suite as both and
|
|
1694
|
+
// read two verdicts of it.
|
|
1695
|
+
const approvedTwins = new Set((repoFile?.checks ?? [])
|
|
1696
|
+
.filter((check) => check.status !== "cannot_run")
|
|
1697
|
+
.map((check) => canonicalCheckCommand(check.command)));
|
|
1605
1698
|
for (const command of testCommands) {
|
|
1606
1699
|
const where = basename(command.cwd) || command.cwd;
|
|
1700
|
+
const line = command.argv.join(" ");
|
|
1701
|
+
if (relative(cwd, command.cwd) === "" && approvedTwins.has(canonicalCheckCommand(line))) {
|
|
1702
|
+
reasons.push(`tests (${where}): \`${line}\` runs once, as the approved check it is`);
|
|
1703
|
+
continue;
|
|
1704
|
+
}
|
|
1607
1705
|
const composed = composeAbortSignal(signal, AbortSignal.timeout(testTimeoutMs));
|
|
1608
1706
|
const startedAt = now();
|
|
1609
1707
|
try {
|
|
@@ -1847,4 +1945,115 @@ export function makeTakeVerifyBaseline(opts = {}) {
|
|
|
1847
1945
|
return result;
|
|
1848
1946
|
};
|
|
1849
1947
|
}
|
|
1948
|
+
/** The whole measurement's cap, before the build starts: the approved plan's own checks, once each. */
|
|
1949
|
+
export const CHECKS_ON_BASE_BUDGET_MS = 10 * 60_000;
|
|
1950
|
+
/**
|
|
1951
|
+
* Run each approved check `checks.json` marks `measureOnBase` once on the
|
|
1952
|
+
* unchanged code, before the build changes anything: a plan's check the
|
|
1953
|
+
* host has not seen pass there (none of the record's, or a red one the
|
|
1954
|
+
* record may hold from long ago). A check the plan wrote for the change
|
|
1955
|
+
* often fails there by design (its test file is not written yet), and a
|
|
1956
|
+
* failure that says only that (a file not found, no tests to run, output no
|
|
1957
|
+
* parser reads) is no verdict: its failures all count after the build, as
|
|
1958
|
+
* before. A failure the gate reads (failing tests, located diagnostics, a
|
|
1959
|
+
* test runner's failure summary) is one the unchanged code already has.
|
|
1960
|
+
*
|
|
1961
|
+
* Run as the gate runs the check ({@link makeRunVerify}): the same command
|
|
1962
|
+
* line, environment and toolchain, from the repository root, so a failure
|
|
1963
|
+
* keys the same on both trees. Bounded: each check by its cap, the whole by
|
|
1964
|
+
* the budget, and one not reached in it has no verdict. What the checks
|
|
1965
|
+
* write into the tree is undone.
|
|
1966
|
+
*/
|
|
1967
|
+
export function makeMeasureChecksOnBase(opts = {}) {
|
|
1968
|
+
const exec = opts.exec ?? defaultExec;
|
|
1969
|
+
const now = opts.now ?? Date.now;
|
|
1970
|
+
const toolchain = opts.toolchain ??
|
|
1971
|
+
(() => {
|
|
1972
|
+
const path = process.env.YAGNI_TOOLCHAIN_FILE?.trim();
|
|
1973
|
+
return path ? readToolchainFile(path) : null;
|
|
1974
|
+
});
|
|
1975
|
+
const treeStatus = opts.treeStatus ??
|
|
1976
|
+
(async (cwd, signal) => (await snapshotWorkspace(cwd, signal)).status);
|
|
1977
|
+
const undoTreeWrites = opts.undoTreeWrites ?? defaultUndoTreeWrites;
|
|
1978
|
+
return async function measureChecksOnBase(cwd, file, signal, budgetOverrideMs) {
|
|
1979
|
+
const passed = new Set();
|
|
1980
|
+
const failed = new Map();
|
|
1981
|
+
const result = { passed, failed, ran: [], notes: [], treeUndone: true };
|
|
1982
|
+
const targets = file.checks.filter((check) => check.measureOnBase === true &&
|
|
1983
|
+
check.status !== "cannot_run" &&
|
|
1984
|
+
!changesGitState(check.command));
|
|
1985
|
+
if (targets.length === 0)
|
|
1986
|
+
return result;
|
|
1987
|
+
const startedAt = now();
|
|
1988
|
+
const budgetMs = Math.min(opts.budgetMs ?? CHECKS_ON_BASE_BUDGET_MS, budgetOverrideMs ?? Number.POSITIVE_INFINITY);
|
|
1989
|
+
const env = opts.repoChecksEnv ?? buildRepoCheckEnv(process.env, toolchain());
|
|
1990
|
+
const before = await treeStatus(cwd, signal).catch((err) => {
|
|
1991
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1992
|
+
result.treeUndone = false;
|
|
1993
|
+
result.notes.push(`could not read the tree before the checks, so what they write is not undone: ${scrubSecrets(msg)}`);
|
|
1994
|
+
return null;
|
|
1995
|
+
});
|
|
1996
|
+
try {
|
|
1997
|
+
for (const check of targets) {
|
|
1998
|
+
if (signal?.aborted)
|
|
1999
|
+
break;
|
|
2000
|
+
const command = check.command.trim();
|
|
2001
|
+
const label = checkLabel(check.command);
|
|
2002
|
+
const left = budgetMs - (now() - startedAt);
|
|
2003
|
+
if (left < REPO_CHECK_MIN_START_MS) {
|
|
2004
|
+
result.notes.push(`\`${label}\`: not reached in the budget`);
|
|
2005
|
+
continue;
|
|
2006
|
+
}
|
|
2007
|
+
const composed = composeAbortSignal(signal, AbortSignal.timeout(Math.min(opts.timeoutMs ?? REPO_CHECK_TIMEOUT_MS, left)));
|
|
2008
|
+
try {
|
|
2009
|
+
const { code, output: rawOutput } = await exec(repoCheckArgv(check.command), cwd, composed, env);
|
|
2010
|
+
if (code === 0) {
|
|
2011
|
+
passed.add(command);
|
|
2012
|
+
result.ran.push(label);
|
|
2013
|
+
continue;
|
|
2014
|
+
}
|
|
2015
|
+
const output = scrubSecrets(rawOutput);
|
|
2016
|
+
const cls = classifyTestFailure(output, code, Number.POSITIVE_INFINITY);
|
|
2017
|
+
if (cls.kind === "infra") {
|
|
2018
|
+
result.notes.push(`\`${label}\`: could not run: ${cls.reason}`);
|
|
2019
|
+
continue;
|
|
2020
|
+
}
|
|
2021
|
+
const found = repoCommandFindings({ output, code }, {
|
|
2022
|
+
label,
|
|
2023
|
+
prefix: approvedPrefix(label),
|
|
2024
|
+
repoRoot: cwd,
|
|
2025
|
+
dir: cwd,
|
|
2026
|
+
limit: Number.POSITIVE_INFINITY,
|
|
2027
|
+
});
|
|
2028
|
+
// Output no parser reads says nothing of what failed (a test file
|
|
2029
|
+
// the change writes, not there yet): no verdict.
|
|
2030
|
+
if (cls.kind === "uncertain" && found.every((f) => verifyFailureKey(f) === null)) {
|
|
2031
|
+
result.notes.push(`\`${label}\`: exited ${code} with output no parser reads`);
|
|
2032
|
+
continue;
|
|
2033
|
+
}
|
|
2034
|
+
failed.set(command, failureKeyCounts(found));
|
|
2035
|
+
result.ran.push(label);
|
|
2036
|
+
}
|
|
2037
|
+
catch (err) {
|
|
2038
|
+
result.notes.push(`\`${label}\`: ${unexitedNote(err, signal, composed)}`);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
finally {
|
|
2043
|
+
if (before !== null) {
|
|
2044
|
+
try {
|
|
2045
|
+
const written = newTreeEntries(before, await treeStatus(cwd));
|
|
2046
|
+
if (written.length > 0)
|
|
2047
|
+
await undoTreeWrites(cwd, written);
|
|
2048
|
+
}
|
|
2049
|
+
catch (err) {
|
|
2050
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2051
|
+
result.treeUndone = false;
|
|
2052
|
+
result.notes.push(`could not undo what the checks wrote into the tree: ${scrubSecrets(msg)}`);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
return result;
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
1850
2059
|
//# sourceMappingURL=verify.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.2.7-staging.
|
|
3
|
+
"version": "1.2.7-staging.2027.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -58,5 +58,5 @@
|
|
|
58
58
|
"turndown": "^7.2.4",
|
|
59
59
|
"typebox": "^1.3.15"
|
|
60
60
|
},
|
|
61
|
-
"yagniSourceSha": "
|
|
61
|
+
"yagniSourceSha": "1dd7310c6a9ff69891182d936aa792a4d375970b"
|
|
62
62
|
}
|