@yagni-app/code-staging 1.2.7-staging.2022.1 → 1.2.7-staging.2031.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 +69 -19
- package/dist/extension/pipeline/missionChecks.d.ts +7 -0
- package/dist/extension/pipeline/missionChecks.js +6 -1
- package/dist/extension/pipeline/verify.d.ts +139 -6
- package/dist/extension/pipeline/verify.js +439 -60
- 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,91 @@ 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 checks the host has not seen pass on the unchanged code
|
|
500
|
+
// (`measureOnBase`: the plan's, and CI's when the repository's record has
|
|
501
|
+
// none of its own), each run once there before the build, on the same
|
|
502
|
+
// fresh tree the baseline above reads: a check that already failed there is
|
|
503
|
+
// not this change's to fix. A continuation or a fix round carries a change,
|
|
504
|
+
// so it measures nothing; its host hands it what an earlier session found.
|
|
505
|
+
let checksOnBase;
|
|
506
|
+
const measureFile = gateOpts?.repoChecks?.() ?? null;
|
|
507
|
+
if (measureFile?.checks.some((check) => check.measureOnBase) &&
|
|
508
|
+
!continuation &&
|
|
509
|
+
!fixRound &&
|
|
510
|
+
!deps.signal?.aborted) {
|
|
511
|
+
const share = preBuildShare();
|
|
512
|
+
if (share.tooLittle) {
|
|
513
|
+
logPipeline("verify_checks_on_base", {
|
|
514
|
+
skipped: "too little of the run's time left",
|
|
515
|
+
leftMs: share.leftMs,
|
|
516
|
+
shareMs: share.shareMs,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
try {
|
|
521
|
+
const measured = await (deps.measureChecksOnBase ?? makeMeasureChecksOnBase())(cwd, measureFile, deps.signal, share.shareMs);
|
|
522
|
+
logPipeline("verify_checks_on_base", {
|
|
523
|
+
ran: measured.ran,
|
|
524
|
+
passed: [...measured.passed],
|
|
525
|
+
failed: [...measured.failed.keys()],
|
|
526
|
+
notes: measured.notes,
|
|
527
|
+
treeUndone: measured.treeUndone,
|
|
528
|
+
});
|
|
529
|
+
if (measured.passed.size > 0 || measured.failed.size > 0)
|
|
530
|
+
checksOnBase = measured;
|
|
531
|
+
}
|
|
532
|
+
catch (err) {
|
|
533
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
534
|
+
logPipeline("verify_checks_on_base", { error: message });
|
|
535
|
+
writeErr(`The approved checks could not run on the unchanged code (each keeps its checks.json standing): ${message}`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
490
539
|
// The last gate verdict's count of failures the clean tree already had, for the result.
|
|
491
540
|
let preexistingFailures = 0;
|
|
492
541
|
let runVerify;
|
|
@@ -506,6 +555,7 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
506
555
|
const made = (deps.makeRunVerify ?? makeRunVerify)({
|
|
507
556
|
...gateOpts,
|
|
508
557
|
...(baseline ? { baseline } : {}),
|
|
558
|
+
...(checksOnBase ? { checksOnBase } : {}),
|
|
509
559
|
...(scan ? { scanChange: scan } : {}),
|
|
510
560
|
...(cloud ? { unreadTestFailureIsFinding: true } : {}),
|
|
511
561
|
});
|
|
@@ -21,6 +21,13 @@ 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, or one CI runs that has no baseline): the session runs it there
|
|
27
|
+
* once before the build, and what that
|
|
28
|
+
* measures stands over `status` (`makeMeasureChecksOnBase` in verify.ts).
|
|
29
|
+
*/
|
|
30
|
+
measureOnBase?: boolean;
|
|
24
31
|
}
|
|
25
32
|
export interface MissionCiStep {
|
|
26
33
|
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 = [];
|
|
@@ -37,7 +37,10 @@
|
|
|
37
37
|
* anything UNCERTAIN fail OPEN with an honest note, never a never-clearable
|
|
38
38
|
* critical. A cloud session's gate (`unreadTestFailureIsFinding`) makes an
|
|
39
39
|
* UNCERTAIN failure a finding carrying the output tail, unless the clean tree
|
|
40
|
-
* failed the same command.
|
|
40
|
+
* failed the same command. On a build that named the repository's toolchain
|
|
41
|
+
* (`YAGNI_TOOLCHAIN_FILE`, a sandbox) both halves run on it, as the model's
|
|
42
|
+
* shell and the approved checks do ({@link buildGateVerifyEnv},
|
|
43
|
+
* {@link buildGateTestEnv}).
|
|
41
44
|
*
|
|
42
45
|
* Safe-by-default and FAIL-OPEN: if no scoped, non-fanned-out command is
|
|
43
46
|
* detectable, or the command cannot be spawned, or it times out, the gate
|
|
@@ -108,6 +111,18 @@ export interface VerifyOutcome {
|
|
|
108
111
|
}
|
|
109
112
|
/** An approved check's name on the report: its command, cut at 80 characters. */
|
|
110
113
|
export declare function checkLabel(command: string): string;
|
|
114
|
+
/**
|
|
115
|
+
* A command as the gate compares two: the same words, whatever spaces sit
|
|
116
|
+
* between them, and a package manager's script named the one way it runs,
|
|
117
|
+
* so `yarn test` and `yarn run test`, `npm test` and `npm run test`, `pnpm
|
|
118
|
+
* lint` and `pnpm run lint` are one check, run once (staging bench
|
|
119
|
+
* 2026-10-25: the gate's own `test` script and two approved twins ran one
|
|
120
|
+
* suite three times). Only the leading form: a manager's flags before the
|
|
121
|
+
* script leave the command as it is. The same rule as
|
|
122
|
+
* `canonicalCheckCommand` in the backend's attempts/inputs.ts; both test
|
|
123
|
+
* files read one corpus, test/fixtures/equivalentCheckCommands.json. Pure.
|
|
124
|
+
*/
|
|
125
|
+
export declare function canonicalCheckCommand(command: string): string;
|
|
111
126
|
/**
|
|
112
127
|
* True when a command changes the checkout's git state: a commit, a branch
|
|
113
128
|
* created or switched, a reset, a stash, a push. `git branch`, `git remote`
|
|
@@ -246,15 +261,36 @@ export declare function detectVerifyTestCommands(cwd: string, opts?: DetectOpts)
|
|
|
246
261
|
* verify failure.
|
|
247
262
|
*/
|
|
248
263
|
export declare function classifyVerifyInfraFailure(output: string, exitCode: number): string | null;
|
|
264
|
+
/**
|
|
265
|
+
* What a run could not start without, when its output says so in a way the
|
|
266
|
+
* gate recognises: a credential or variable the environment lacks, or a
|
|
267
|
+
* prerequisite the sandbox has not installed. Null for anything else, which
|
|
268
|
+
* stays a real failure. Pure.
|
|
269
|
+
*
|
|
270
|
+
* Narrow on purpose, since a match makes the run "could not run" rather than
|
|
271
|
+
* the change's failure:
|
|
272
|
+
* - a named variable counts only when `env` (the environment the command ran
|
|
273
|
+
* under) does not have it set: one it has is not why the run failed, and
|
|
274
|
+
* no unnamed phrase then stands in for it;
|
|
275
|
+
* - a line that quotes a test's expectation or a diff never counts, nor
|
|
276
|
+
* one a logger wrote below error level;
|
|
277
|
+
* - a repository's own "`A_B` is missing" counts only on an error's line.
|
|
278
|
+
*
|
|
279
|
+
* Without `env`, a named variable is taken as unset.
|
|
280
|
+
*/
|
|
281
|
+
export declare function classifyMissingPrerequisite(output: string, env?: NodeJS.ProcessEnv): string | null;
|
|
249
282
|
/** How the pure test classifier read one test run (spec §3d). */
|
|
250
283
|
export type TestFailureClassification = {
|
|
251
284
|
kind: "clean";
|
|
252
285
|
} | {
|
|
253
286
|
kind: "failures";
|
|
254
287
|
findings: Finding[];
|
|
255
|
-
}
|
|
288
|
+
}
|
|
289
|
+
/** `needs`: the credential or prerequisite the run lacked ({@link classifyMissingPrerequisite}). */
|
|
290
|
+
| {
|
|
256
291
|
kind: "infra";
|
|
257
292
|
reason: string;
|
|
293
|
+
needs?: string;
|
|
258
294
|
} | {
|
|
259
295
|
kind: "uncertain";
|
|
260
296
|
};
|
|
@@ -269,7 +305,9 @@ export type TestFailureClassification = {
|
|
|
269
305
|
* - infra/env shapes (command not found, module resolution, connection
|
|
270
306
|
* refused, missing env var, DB/auth, connect timeout) → `infra` with a short
|
|
271
307
|
* cause; checked FIRST so a suite whose assertions failed only because its
|
|
272
|
-
* DB was down fails open rather than blocking.
|
|
308
|
+
* DB was down fails open rather than blocking. A missing credential or
|
|
309
|
+
* prerequisite ({@link classifyMissingPrerequisite}, given the `env` the
|
|
310
|
+
* command ran under) is `infra` with `needs` and the cause "needs <it>".
|
|
273
311
|
* - recognisable failing-test lines (pnpm-wrapped vitest / jest / node:test
|
|
274
312
|
* TAP + spec reporters) → `failures` with located `critical` findings
|
|
275
313
|
* (capped at 25, deduped, locations attached when the runner printed them).
|
|
@@ -281,7 +319,7 @@ export type TestFailureClassification = {
|
|
|
281
319
|
* Callers scrub the output through {@link scrubSecrets} BEFORE classification
|
|
282
320
|
* so findings/notes never carry secrets (§0.9).
|
|
283
321
|
*/
|
|
284
|
-
export declare function classifyTestFailure(output: string, exitCode: number, limit?: number): TestFailureClassification;
|
|
322
|
+
export declare function classifyTestFailure(output: string, exitCode: number, limit?: number, env?: NodeJS.ProcessEnv): TestFailureClassification;
|
|
285
323
|
/**
|
|
286
324
|
* Turn a failing verify run into `critical` Findings. A clean (exit 0) run yields
|
|
287
325
|
* none. Located diagnostics (TypeScript first, then a generic file:line form, then
|
|
@@ -382,6 +420,22 @@ export declare function buildTestEnv(source?: NodeJS.ProcessEnv): NodeJS.Process
|
|
|
382
420
|
* own literal variables.
|
|
383
421
|
*/
|
|
384
422
|
export declare function buildRepoCheckEnv(source?: NodeJS.ProcessEnv, toolchain?: ToolchainFile | null, stepEnv?: Record<string, string>): NodeJS.ProcessEnv;
|
|
423
|
+
/**
|
|
424
|
+
* The environment the gate's own detected test script runs under. On a build
|
|
425
|
+
* that named a toolchain (a sandbox), an approved check's
|
|
426
|
+
* ({@link buildRepoCheckEnv}), so the suite runs on the repository's Node,
|
|
427
|
+
* Ruby or Go as the model's shell does, never the image's, and gives the same
|
|
428
|
+
* verdict it gives as an approved check. Without one (a developer's terminal),
|
|
429
|
+
* the user's own ({@link buildTestEnv}), as before.
|
|
430
|
+
*/
|
|
431
|
+
export declare function buildGateTestEnv(source?: NodeJS.ProcessEnv, toolchain?: ToolchainFile | null): NodeJS.ProcessEnv;
|
|
432
|
+
/**
|
|
433
|
+
* The environment the gate's own detected typecheck or lint script runs
|
|
434
|
+
* under: the scrubbed {@link buildVerifyEnv} on the build's toolchain, when it
|
|
435
|
+
* named one. Undefined without one, so the exec seam's default applies, as
|
|
436
|
+
* before.
|
|
437
|
+
*/
|
|
438
|
+
export declare function buildGateVerifyEnv(source?: NodeJS.ProcessEnv, toolchain?: ToolchainFile | null): NodeJS.ProcessEnv | undefined;
|
|
385
439
|
/** One porcelain line's path, and whether it is untracked. */
|
|
386
440
|
interface TreeEntry {
|
|
387
441
|
path: string;
|
|
@@ -419,7 +473,7 @@ export interface MakeRunVerifyOpts {
|
|
|
419
473
|
detectTests?: (cwd: string, changedDirs: string[]) => VerifyCommand[];
|
|
420
474
|
/** Per-package wall-clock cap for the test half (default VERIFY_TEST_TIMEOUT_MS). */
|
|
421
475
|
testTimeoutMs?: number;
|
|
422
|
-
/** Test-half env override for determinism in tests (default:
|
|
476
|
+
/** Test-half env override for determinism in tests (default: buildGateTestEnv() per run). */
|
|
423
477
|
testEnv?: NodeJS.ProcessEnv;
|
|
424
478
|
/**
|
|
425
479
|
* The mission's `checks.json` (the approved checks and the pull request's CI
|
|
@@ -439,7 +493,11 @@ export interface MakeRunVerifyOpts {
|
|
|
439
493
|
changedPaths?: (cwd: string, signal?: AbortSignal, baselinePaths?: string[]) => Promise<string[]>;
|
|
440
494
|
/** Repository-checks env override for determinism in tests (default: buildRepoCheckEnv() per run). */
|
|
441
495
|
repoChecksEnv?: NodeJS.ProcessEnv;
|
|
442
|
-
/**
|
|
496
|
+
/**
|
|
497
|
+
* The toolchain every command the gate runs is on: the repository checks,
|
|
498
|
+
* and its own detected typecheck and test scripts (default:
|
|
499
|
+
* `YAGNI_TOOLCHAIN_FILE`, when it reads).
|
|
500
|
+
*/
|
|
443
501
|
toolchain?: () => ToolchainFile | null;
|
|
444
502
|
repoChecksBuildBudgetMs?: number;
|
|
445
503
|
repoChecksCallBudgetMs?: number;
|
|
@@ -455,6 +513,13 @@ export interface MakeRunVerifyOpts {
|
|
|
455
513
|
* findings. Absent: every failure is a finding, as before.
|
|
456
514
|
*/
|
|
457
515
|
baseline?: VerifyBaseline | null;
|
|
516
|
+
/**
|
|
517
|
+
* What the approved checks `checks.json` marks `measureOnBase` did on the
|
|
518
|
+
* unchanged code ({@link makeMeasureChecksOnBase}). It stands over each
|
|
519
|
+
* one's `status`: one that passed there must pass, and one that failed
|
|
520
|
+
* there is the change's only where it fails anew. Absent: `status` alone.
|
|
521
|
+
*/
|
|
522
|
+
checksOnBase?: ChecksOnBase | null;
|
|
458
523
|
/**
|
|
459
524
|
* The cloud session's rule (spec decision 3): a test run that exits
|
|
460
525
|
* non-zero with output no parser reads is a finding carrying its output
|
|
@@ -510,6 +575,11 @@ export interface MakeWorkstreamCheckOpts {
|
|
|
510
575
|
timeoutMs?: number;
|
|
511
576
|
/** What the clean tree already failed on (YAG-1003): those failures are not a workstream's. */
|
|
512
577
|
baseline?: VerifyBaseline | null;
|
|
578
|
+
/**
|
|
579
|
+
* The toolchain the checks run on, as the full gate's do (default:
|
|
580
|
+
* `YAGNI_TOOLCHAIN_FILE`, when it reads).
|
|
581
|
+
*/
|
|
582
|
+
toolchain?: () => ToolchainFile | null;
|
|
513
583
|
}
|
|
514
584
|
/**
|
|
515
585
|
* The per-workstream scoped check the implement diamond runs right after the fan
|
|
@@ -556,6 +626,11 @@ export interface MakeTakeVerifyBaselineOpts {
|
|
|
556
626
|
timeoutMs?: number;
|
|
557
627
|
testTimeoutMs?: number;
|
|
558
628
|
testEnv?: NodeJS.ProcessEnv;
|
|
629
|
+
/**
|
|
630
|
+
* The toolchain the commands run on, as the gate's do (default:
|
|
631
|
+
* `YAGNI_TOOLCHAIN_FILE`, when it reads).
|
|
632
|
+
*/
|
|
633
|
+
toolchain?: () => ToolchainFile | null;
|
|
559
634
|
budgetMs?: number;
|
|
560
635
|
now?: () => number;
|
|
561
636
|
treeStatus?: (cwd: string, signal?: AbortSignal) => Promise<string>;
|
|
@@ -584,5 +659,63 @@ export interface VerifyBaselineResult {
|
|
|
584
659
|
* write into the tree is undone, so none of it reaches the candidate.
|
|
585
660
|
*/
|
|
586
661
|
export declare function makeTakeVerifyBaseline(opts?: MakeTakeVerifyBaselineOpts): (cwd: string, dirs: string[], signal?: AbortSignal, budgetOverrideMs?: number) => Promise<VerifyBaselineResult>;
|
|
662
|
+
/**
|
|
663
|
+
* What the approved checks marked `measureOnBase` did on the unchanged code,
|
|
664
|
+
* by command (trimmed): each that passed there, and each that failed there
|
|
665
|
+
* with failures the gate reads, with how often each failure key was seen
|
|
666
|
+
* (none for a failure only a summary line names). A command in neither gave
|
|
667
|
+
* no verdict there, and keeps the status `checks.json` gave it.
|
|
668
|
+
*/
|
|
669
|
+
export interface ChecksOnBase {
|
|
670
|
+
passed: ReadonlySet<string>;
|
|
671
|
+
failed: ReadonlyMap<string, ReadonlyMap<string, number>>;
|
|
672
|
+
}
|
|
673
|
+
/** {@link ChecksOnBase}, with what ran and why the rest gave no verdict, for the run's log. */
|
|
674
|
+
export interface ChecksOnBaseResult extends ChecksOnBase {
|
|
675
|
+
ran: string[];
|
|
676
|
+
notes: string[];
|
|
677
|
+
/**
|
|
678
|
+
* What the checks wrote into the tree was undone (or they wrote nothing).
|
|
679
|
+
* False when the tree could not be read before them or the undo failed:
|
|
680
|
+
* the build then starts on a tree the checks may have dirtied.
|
|
681
|
+
*/
|
|
682
|
+
treeUndone: boolean;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* The whole measurement's cap, before the build starts: each check marked to
|
|
686
|
+
* measure, once, the plan's first.
|
|
687
|
+
*/
|
|
688
|
+
export declare const CHECKS_ON_BASE_BUDGET_MS: number;
|
|
689
|
+
export interface MakeMeasureChecksOnBaseOpts {
|
|
690
|
+
exec?: VerifyExec;
|
|
691
|
+
/** Repository-checks env override for determinism in tests (default: the gate's own). */
|
|
692
|
+
repoChecksEnv?: NodeJS.ProcessEnv;
|
|
693
|
+
toolchain?: () => ToolchainFile | null;
|
|
694
|
+
/** One check's cap (default {@link REPO_CHECK_TIMEOUT_MS}). */
|
|
695
|
+
timeoutMs?: number;
|
|
696
|
+
budgetMs?: number;
|
|
697
|
+
now?: () => number;
|
|
698
|
+
treeStatus?: (cwd: string, signal?: AbortSignal) => Promise<string>;
|
|
699
|
+
undoTreeWrites?: (cwd: string, entries: TreeEntry[]) => Promise<void>;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Run each approved check `checks.json` marks `measureOnBase` once on the
|
|
703
|
+
* unchanged code, before the build changes anything: a plan's check the
|
|
704
|
+
* host has not seen pass there (none of the record's, or a red one the
|
|
705
|
+
* record may hold from long ago), and a command the repository's CI runs
|
|
706
|
+
* when the record names no check of its own. A check the plan wrote for the
|
|
707
|
+
* change often fails there by design (its test file is not written yet), and a
|
|
708
|
+
* failure that says only that (a file not found, no tests to run, output no
|
|
709
|
+
* parser reads) is no verdict: its failures all count after the build, as
|
|
710
|
+
* before. A failure the gate reads (failing tests, located diagnostics, a
|
|
711
|
+
* test runner's failure summary) is one the unchanged code already has.
|
|
712
|
+
*
|
|
713
|
+
* Run as the gate runs the check ({@link makeRunVerify}): the same command
|
|
714
|
+
* line, environment and toolchain, from the repository root, so a failure
|
|
715
|
+
* keys the same on both trees. Bounded: each check by its cap, the whole by
|
|
716
|
+
* the budget, and one not reached in it has no verdict. What the checks
|
|
717
|
+
* write into the tree is undone.
|
|
718
|
+
*/
|
|
719
|
+
export declare function makeMeasureChecksOnBase(opts?: MakeMeasureChecksOnBaseOpts): (cwd: string, file: MissionChecksFile, signal?: AbortSignal, budgetOverrideMs?: number) => Promise<ChecksOnBaseResult>;
|
|
587
720
|
export {};
|
|
588
721
|
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -37,7 +37,10 @@
|
|
|
37
37
|
* anything UNCERTAIN fail OPEN with an honest note, never a never-clearable
|
|
38
38
|
* critical. A cloud session's gate (`unreadTestFailureIsFinding`) makes an
|
|
39
39
|
* UNCERTAIN failure a finding carrying the output tail, unless the clean tree
|
|
40
|
-
* failed the same command.
|
|
40
|
+
* failed the same command. On a build that named the repository's toolchain
|
|
41
|
+
* (`YAGNI_TOOLCHAIN_FILE`, a sandbox) both halves run on it, as the model's
|
|
42
|
+
* shell and the approved checks do ({@link buildGateVerifyEnv},
|
|
43
|
+
* {@link buildGateTestEnv}).
|
|
41
44
|
*
|
|
42
45
|
* Safe-by-default and FAIL-OPEN: if no scoped, non-fanned-out command is
|
|
43
46
|
* detectable, or the command cannot be spawned, or it times out, the gate
|
|
@@ -111,6 +114,44 @@ const VERIFY_LENS = "does_it_hold";
|
|
|
111
114
|
export function checkLabel(command) {
|
|
112
115
|
return command.length > 80 ? `${command.slice(0, 77)}...` : command;
|
|
113
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Package-manager words that are the manager's own command, never a script:
|
|
119
|
+
* `yarn add`, `pnpm install`. Any other word after `yarn` or `pnpm` runs the
|
|
120
|
+
* script of that name, as `run` does.
|
|
121
|
+
*/
|
|
122
|
+
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(" "));
|
|
123
|
+
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(" "));
|
|
124
|
+
/** The scripts npm runs without `run`: its lifecycle shorthands. */
|
|
125
|
+
const NPM_SCRIPT_SHORTHANDS = new Set(["test", "start", "stop", "restart"]);
|
|
126
|
+
/**
|
|
127
|
+
* A command as the gate compares two: the same words, whatever spaces sit
|
|
128
|
+
* between them, and a package manager's script named the one way it runs,
|
|
129
|
+
* so `yarn test` and `yarn run test`, `npm test` and `npm run test`, `pnpm
|
|
130
|
+
* lint` and `pnpm run lint` are one check, run once (staging bench
|
|
131
|
+
* 2026-10-25: the gate's own `test` script and two approved twins ran one
|
|
132
|
+
* suite three times). Only the leading form: a manager's flags before the
|
|
133
|
+
* script leave the command as it is. The same rule as
|
|
134
|
+
* `canonicalCheckCommand` in the backend's attempts/inputs.ts; both test
|
|
135
|
+
* files read one corpus, test/fixtures/equivalentCheckCommands.json. Pure.
|
|
136
|
+
*/
|
|
137
|
+
export function canonicalCheckCommand(command) {
|
|
138
|
+
const words = command.trim().split(/\s+/).filter(Boolean);
|
|
139
|
+
const [manager, next, ...rest] = words;
|
|
140
|
+
const script = /^[A-Za-z0-9][\w:.@/-]*$/;
|
|
141
|
+
if (manager === "npm" || manager === "yarn" || manager === "pnpm") {
|
|
142
|
+
const runs = next === "run" || (manager === "npm" && next === "run-script");
|
|
143
|
+
if (runs && rest[0] && script.test(rest[0]))
|
|
144
|
+
return [manager, "run", ...rest].join(" ");
|
|
145
|
+
const shorthand = next !== undefined &&
|
|
146
|
+
script.test(next) &&
|
|
147
|
+
(manager === "npm"
|
|
148
|
+
? NPM_SCRIPT_SHORTHANDS.has(next)
|
|
149
|
+
: !(manager === "yarn" ? YARN_COMMANDS : PNPM_COMMANDS).has(next));
|
|
150
|
+
if (shorthand)
|
|
151
|
+
return [manager, "run", next, ...rest].join(" ");
|
|
152
|
+
}
|
|
153
|
+
return words.join(" ");
|
|
154
|
+
}
|
|
114
155
|
/** Git subcommands that only read: a check may run them as often as it likes. */
|
|
115
156
|
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
157
|
/**
|
|
@@ -471,18 +512,6 @@ const TEST_INFRA_PATTERNS = [
|
|
|
471
512
|
[/\bENOTFOUND\b|\bEAI_AGAIN\b/, "DNS/host resolution failed for a test dependency"],
|
|
472
513
|
[/\bEADDRINUSE\b/, "a port the tests need is already in use"],
|
|
473
514
|
[/\bETIMEDOUT\b|\bconnect(?:ion)? timed out\b/i, "connection to a test dependency timed out"],
|
|
474
|
-
[
|
|
475
|
-
/\bmissing (?:required )?env(?:ironment)? (?:var(?:iable)?s?)\b/i,
|
|
476
|
-
"missing environment variable",
|
|
477
|
-
],
|
|
478
|
-
[
|
|
479
|
-
/\benvironment variable\b[^\n]{0,80}\b(?:is )?(?:not set|missing|required|must be set)\b/i,
|
|
480
|
-
"missing environment variable",
|
|
481
|
-
],
|
|
482
|
-
[/\bprocess\.env\.[A-Z0-9_]+ is (?:undefined|not set)\b/, "missing environment variable"],
|
|
483
|
-
// Yarn Berry reading a `${TOKEN}` in .yarnrc.yml that the environment lacks
|
|
484
|
-
// (staging bench 2026-10-23: GITHUB_PACKAGES_TOKEN): the suite never started.
|
|
485
|
-
[/\bEnvironment variable not found \([A-Za-z0-9_]+\)/, "missing environment variable"],
|
|
486
515
|
[/\bSASL\b|\bSCRAM-SERVER\b/, "database auth failed (missing DB credentials?)"],
|
|
487
516
|
[/\bpassword authentication failed\b/i, "database auth failed"],
|
|
488
517
|
[/\brole "[^"]*" does not exist\b/i, "database role does not exist"],
|
|
@@ -499,6 +528,126 @@ const TEST_INFRA_PATTERNS = [
|
|
|
499
528
|
[/\bCannot connect to the Docker daemon\b/i, "Docker is not running"],
|
|
500
529
|
[/\bno space left on device\b/i, "the disk is full"],
|
|
501
530
|
];
|
|
531
|
+
/**
|
|
532
|
+
* A variable's name as an output names it: `SUPABASE_SECRET_KEY`, `PORT`.
|
|
533
|
+
* Two characters at least and upper case only, on purpose: a single letter or
|
|
534
|
+
* a lower-case word beside "is missing" is prose, and reading it as a
|
|
535
|
+
* variable would make a real failure "could not run".
|
|
536
|
+
*/
|
|
537
|
+
const VAR_NAME = "[A-Z][A-Z0-9_]*[A-Z0-9]";
|
|
538
|
+
/** Words a missing variable is said with after its name. */
|
|
539
|
+
const UNSET_WORDS = "(?:is |was )?(?:not set|not defined|missing|required|undefined|empty|must be set)";
|
|
540
|
+
const ENV_VARIABLE = "[Ee]nv(?:ironment)?[ _-]?[Vv]ar(?:iable)?";
|
|
541
|
+
/**
|
|
542
|
+
* The ways a run's output says it lacks an environment variable, each with the
|
|
543
|
+
* name in its first group. Each names the variable as one: a phrase about an
|
|
544
|
+
* environment variable around it, or `process.env.` before it.
|
|
545
|
+
*/
|
|
546
|
+
const NAMED_MISSING_VARIABLE = [
|
|
547
|
+
// "Missing required environment variable: SUPABASE_SECRET_KEY", "missing env var FOO"
|
|
548
|
+
new RegExp(`\\b[Mm]issing (?:[Rr]equired )?${ENV_VARIABLE}s?\\b[\\s:=\`'"([{]*(${VAR_NAME})\\b`, "g"),
|
|
549
|
+
// "Environment variable SUPABASE_URL is not set"
|
|
550
|
+
new RegExp(`\\b${ENV_VARIABLE} [\`'"]?(${VAR_NAME})[\`'"]? ${UNSET_WORDS}\\b`, "g"),
|
|
551
|
+
// "SUPABASE_URL environment variable is not set"
|
|
552
|
+
new RegExp(`\\b(${VAR_NAME}) ${ENV_VARIABLE} ${UNSET_WORDS}\\b`, "g"),
|
|
553
|
+
// "process.env.FOO is undefined"
|
|
554
|
+
new RegExp(`\\bprocess\\.env\\.(${VAR_NAME}) ${UNSET_WORDS}\\b`, "g"),
|
|
555
|
+
// Yarn Berry reading a `${TOKEN}` in .yarnrc.yml that the environment lacks
|
|
556
|
+
// (staging bench 2026-10-23: GITHUB_PACKAGES_TOKEN): the suite never started.
|
|
557
|
+
/\bEnvironment variable not found \(([A-Za-z_][A-Za-z0-9_]*)\)/g,
|
|
558
|
+
// django-environ: "ImproperlyConfigured: Set the SECRET_KEY environment variable"
|
|
559
|
+
new RegExp(`\\bSet the (${VAR_NAME}) environment variable\\b`, "g"),
|
|
560
|
+
];
|
|
561
|
+
/**
|
|
562
|
+
* A repository's own check of its environment, on a line that reads as the
|
|
563
|
+
* error the run stopped on (staging 2026-10-24, NearMyTown's e2e:
|
|
564
|
+
* "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY is missing"). The name must look like
|
|
565
|
+
* a variable's (`A_B`), and the line an error's: a log line that a variable is
|
|
566
|
+
* missing is not why a run failed. Never "is not defined" or "is undefined":
|
|
567
|
+
* that is JavaScript's word for a name the code never declared.
|
|
568
|
+
*/
|
|
569
|
+
const BARE_MISSING_VARIABLE = /\b([A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+) (?:is |was )?(?:missing|not set|must be set)\b/g;
|
|
570
|
+
const ERROR_LINE = /(?:Error|Exception)\b|^\s*(?:error|ERROR|fatal|FATAL)\b/;
|
|
571
|
+
/** At most this many missing variables are named in one cause. */
|
|
572
|
+
const MAX_NAMED_VARIABLES = 3;
|
|
573
|
+
/**
|
|
574
|
+
* A line that quotes what a test expected or received, or a diff of it: a
|
|
575
|
+
* test of an environment check, never the run's own missing variable.
|
|
576
|
+
*/
|
|
577
|
+
const QUOTED_LINE = /^\s*[-+]\s|\b(?:[Ee]xpected|[Rr]eceived|toThrow|[Aa]ssert)/;
|
|
578
|
+
/**
|
|
579
|
+
* A line a logger wrote below error level (`warn:`, `[info]`, `DEBUG`,
|
|
580
|
+
* Next.js's `⚠`): a variable it says is missing is one the run went on
|
|
581
|
+
* without, never why it failed.
|
|
582
|
+
*/
|
|
583
|
+
const LOG_LINE = /^\s*(?:⚠|[[(]?(?:warn(?:ing)?|info|debug|trace|notice)\b)/i;
|
|
584
|
+
/** Frameworks that say which variable they lack only by what it is for. */
|
|
585
|
+
const MISSING_FRAMEWORK_KEY = [
|
|
586
|
+
// Rails credentials: "Missing encryption key to decrypt file with. ... ENV["RAILS_MASTER_KEY"]"
|
|
587
|
+
[/\bMissing encryption key to decrypt file with\b/, "RAILS_MASTER_KEY"],
|
|
588
|
+
// Laravel's MissingAppKeyException
|
|
589
|
+
[/\bNo application encryption key has been specified\b/, "APP_KEY"],
|
|
590
|
+
];
|
|
591
|
+
/** Phrases that say a variable is missing without naming it. */
|
|
592
|
+
const UNNAMED_MISSING_VARIABLE = [
|
|
593
|
+
/\bmissing (?:required )?env(?:ironment)? (?:var(?:iable)?s?)\b/i,
|
|
594
|
+
/\benvironment variable\b[^\n]{0,80}\b(?:is )?(?:not set|missing|required|must be set)\b/i,
|
|
595
|
+
];
|
|
596
|
+
/**
|
|
597
|
+
* What a run could not start without, when its output says so in a way the
|
|
598
|
+
* gate recognises: a credential or variable the environment lacks, or a
|
|
599
|
+
* prerequisite the sandbox has not installed. Null for anything else, which
|
|
600
|
+
* stays a real failure. Pure.
|
|
601
|
+
*
|
|
602
|
+
* Narrow on purpose, since a match makes the run "could not run" rather than
|
|
603
|
+
* the change's failure:
|
|
604
|
+
* - a named variable counts only when `env` (the environment the command ran
|
|
605
|
+
* under) does not have it set: one it has is not why the run failed, and
|
|
606
|
+
* no unnamed phrase then stands in for it;
|
|
607
|
+
* - a line that quotes a test's expectation or a diff never counts, nor
|
|
608
|
+
* one a logger wrote below error level;
|
|
609
|
+
* - a repository's own "`A_B` is missing" counts only on an error's line.
|
|
610
|
+
*
|
|
611
|
+
* Without `env`, a named variable is taken as unset.
|
|
612
|
+
*/
|
|
613
|
+
export function classifyMissingPrerequisite(output, env) {
|
|
614
|
+
const unset = (name) => !env?.[name];
|
|
615
|
+
const lines = output
|
|
616
|
+
.split("\n")
|
|
617
|
+
.filter((line) => !QUOTED_LINE.test(line) && !LOG_LINE.test(line));
|
|
618
|
+
const named = new Set(lines.flatMap((line) => [
|
|
619
|
+
...NAMED_MISSING_VARIABLE.flatMap((re) => [...line.matchAll(re)]),
|
|
620
|
+
...(ERROR_LINE.test(line) ? [...line.matchAll(BARE_MISSING_VARIABLE)] : []),
|
|
621
|
+
].map((match) => match[1])));
|
|
622
|
+
const missing = [...named].filter(unset);
|
|
623
|
+
if (missing.length > 0) {
|
|
624
|
+
const more = missing.length - MAX_NAMED_VARIABLES;
|
|
625
|
+
return `${missing.slice(0, MAX_NAMED_VARIABLES).join(", ")}${more > 0 ? ` and ${more} more` : ""}`;
|
|
626
|
+
}
|
|
627
|
+
const text = lines.join("\n");
|
|
628
|
+
for (const [re, name] of MISSING_FRAMEWORK_KEY) {
|
|
629
|
+
if (re.test(text) && unset(name))
|
|
630
|
+
return name;
|
|
631
|
+
}
|
|
632
|
+
// supabase-js `createClient` with no URL or key: the variable it was read from is unset.
|
|
633
|
+
const supabase = text.match(/\bsupabase(Url|Key) is required\b/);
|
|
634
|
+
if (supabase) {
|
|
635
|
+
return `a Supabase ${supabase[1] === "Url" ? "URL" : "key"} (supabase${supabase[1]} is required)`;
|
|
636
|
+
}
|
|
637
|
+
// Playwright with no browser of its own version (a prepare installs one; #2758).
|
|
638
|
+
if (/\bExecutable doesn't exist at /.test(text) &&
|
|
639
|
+
/\bplaywright install\b|\bbrowserType\.launch\b|\bms-playwright\b/.test(text)) {
|
|
640
|
+
return "Playwright's browsers (npx playwright install)";
|
|
641
|
+
}
|
|
642
|
+
// A variable the output names that the environment has is not missing, and
|
|
643
|
+
// no unnamed phrase stands in for it. A log line is not read at all, so one
|
|
644
|
+
// that names a variable never stops an error's own unnamed phrase from
|
|
645
|
+
// counting: that error says the run lacked a variable, only not which.
|
|
646
|
+
if (named.size === 0 && UNNAMED_MISSING_VARIABLE.some((re) => re.test(text))) {
|
|
647
|
+
return "an environment variable the output does not name";
|
|
648
|
+
}
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
502
651
|
/** Strip a trailing `(4 ms)` / `(0.82ms)` duration off a failing-test name. */
|
|
503
652
|
function stripDuration(name) {
|
|
504
653
|
return name.replace(/\s*\(\d+(?:\.\d+)?\s*m?s\)\s*$/, "").trim();
|
|
@@ -530,7 +679,9 @@ const FAILURE_SUMMARY = [
|
|
|
530
679
|
* - infra/env shapes (command not found, module resolution, connection
|
|
531
680
|
* refused, missing env var, DB/auth, connect timeout) → `infra` with a short
|
|
532
681
|
* cause; checked FIRST so a suite whose assertions failed only because its
|
|
533
|
-
* DB was down fails open rather than blocking.
|
|
682
|
+
* DB was down fails open rather than blocking. A missing credential or
|
|
683
|
+
* prerequisite ({@link classifyMissingPrerequisite}, given the `env` the
|
|
684
|
+
* command ran under) is `infra` with `needs` and the cause "needs <it>".
|
|
534
685
|
* - recognisable failing-test lines (pnpm-wrapped vitest / jest / node:test
|
|
535
686
|
* TAP + spec reporters) → `failures` with located `critical` findings
|
|
536
687
|
* (capped at 25, deduped, locations attached when the runner printed them).
|
|
@@ -542,12 +693,15 @@ const FAILURE_SUMMARY = [
|
|
|
542
693
|
* Callers scrub the output through {@link scrubSecrets} BEFORE classification
|
|
543
694
|
* so findings/notes never carry secrets (§0.9).
|
|
544
695
|
*/
|
|
545
|
-
export function classifyTestFailure(output, exitCode, limit = MAX_LOCATED_FINDINGS) {
|
|
696
|
+
export function classifyTestFailure(output, exitCode, limit = MAX_LOCATED_FINDINGS, env) {
|
|
546
697
|
if (exitCode === 0)
|
|
547
698
|
return { kind: "clean" };
|
|
548
699
|
const spawnInfra = classifyVerifyInfraFailure(output, exitCode);
|
|
549
700
|
if (spawnInfra)
|
|
550
701
|
return { kind: "infra", reason: spawnInfra };
|
|
702
|
+
const needs = classifyMissingPrerequisite(output, env);
|
|
703
|
+
if (needs)
|
|
704
|
+
return { kind: "infra", reason: `needs ${needs}`, needs };
|
|
551
705
|
for (const [re, reason] of TEST_INFRA_PATTERNS) {
|
|
552
706
|
if (re.test(output))
|
|
553
707
|
return { kind: "infra", reason };
|
|
@@ -913,8 +1067,40 @@ export function buildRepoCheckEnv(source = process.env, toolchain = null, stepEn
|
|
|
913
1067
|
const env = buildTestEnv(source);
|
|
914
1068
|
for (const key of REPO_CHECK_SECRET_KEYS)
|
|
915
1069
|
delete env[key];
|
|
916
|
-
|
|
917
|
-
|
|
1070
|
+
return { ...onToolchain(env, toolchain), ...stepEnv, CI: "true" };
|
|
1071
|
+
}
|
|
1072
|
+
/**
|
|
1073
|
+
* `env` on the build's toolchain, as the model's shell gets it
|
|
1074
|
+
* (`makeToolchainSpawnHook`): the toolchain's PATH, and its other variables
|
|
1075
|
+
* beneath `env`'s own. No toolchain: `env` as it is.
|
|
1076
|
+
*/
|
|
1077
|
+
function onToolchain(env, toolchain) {
|
|
1078
|
+
return toolchain ? { ...toolchain.env, ...env, PATH: toolchain.path } : env;
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* The environment the gate's own detected test script runs under. On a build
|
|
1082
|
+
* that named a toolchain (a sandbox), an approved check's
|
|
1083
|
+
* ({@link buildRepoCheckEnv}), so the suite runs on the repository's Node,
|
|
1084
|
+
* Ruby or Go as the model's shell does, never the image's, and gives the same
|
|
1085
|
+
* verdict it gives as an approved check. Without one (a developer's terminal),
|
|
1086
|
+
* the user's own ({@link buildTestEnv}), as before.
|
|
1087
|
+
*/
|
|
1088
|
+
export function buildGateTestEnv(source = process.env, toolchain = null) {
|
|
1089
|
+
return toolchain ? buildRepoCheckEnv(source, toolchain) : buildTestEnv(source);
|
|
1090
|
+
}
|
|
1091
|
+
/**
|
|
1092
|
+
* The environment the gate's own detected typecheck or lint script runs
|
|
1093
|
+
* under: the scrubbed {@link buildVerifyEnv} on the build's toolchain, when it
|
|
1094
|
+
* named one. Undefined without one, so the exec seam's default applies, as
|
|
1095
|
+
* before.
|
|
1096
|
+
*/
|
|
1097
|
+
export function buildGateVerifyEnv(source = process.env, toolchain = null) {
|
|
1098
|
+
return toolchain ? onToolchain(buildVerifyEnv(source), toolchain) : undefined;
|
|
1099
|
+
}
|
|
1100
|
+
/** The toolchain the session's harness named (`YAGNI_TOOLCHAIN_FILE`), when it reads. */
|
|
1101
|
+
function sessionToolchain() {
|
|
1102
|
+
const path = process.env.YAGNI_TOOLCHAIN_FILE?.trim();
|
|
1103
|
+
return path ? readToolchainFile(path) : null;
|
|
918
1104
|
}
|
|
919
1105
|
/** One repository command as the gate runs it: through bash, as Actions runs a `run:` step. */
|
|
920
1106
|
function repoCheckArgv(run) {
|
|
@@ -1073,6 +1259,7 @@ export function makeWorkstreamCheck(opts = {}) {
|
|
|
1073
1259
|
// command, untagged, by whichever target reaches the command first.
|
|
1074
1260
|
const uncoveredEmitted = new Set();
|
|
1075
1261
|
const results = [];
|
|
1262
|
+
const env = buildGateVerifyEnv(process.env, (opts.toolchain ?? sessionToolchain)());
|
|
1076
1263
|
for (const target of targets) {
|
|
1077
1264
|
const dirs = claimDirs(cwd, target.files);
|
|
1078
1265
|
const commands = dirs.length > 0 ? detect(cwd, dirs).filter((c) => dirs.includes(c.cwd)) : [];
|
|
@@ -1096,7 +1283,7 @@ export function makeWorkstreamCheck(opts = {}) {
|
|
|
1096
1283
|
if (!run) {
|
|
1097
1284
|
const composed = composeAbortSignal(signal, AbortSignal.timeout(timeoutMs));
|
|
1098
1285
|
try {
|
|
1099
|
-
const { code, output } = await exec(command.argv, command.cwd, composed);
|
|
1286
|
+
const { code, output } = await exec(command.argv, command.cwd, composed, env);
|
|
1100
1287
|
run = { code, output: scrubSecrets(output) };
|
|
1101
1288
|
}
|
|
1102
1289
|
catch (err) {
|
|
@@ -1139,6 +1326,38 @@ export function makeWorkstreamCheck(opts = {}) {
|
|
|
1139
1326
|
return results;
|
|
1140
1327
|
};
|
|
1141
1328
|
}
|
|
1329
|
+
/**
|
|
1330
|
+
* A failing repository command's findings, located from the folder it ran in
|
|
1331
|
+
* and prefixed with what ran: the gate's on the build's tree, and the
|
|
1332
|
+
* unchanged code's measurement ({@link makeMeasureChecksOnBase}), so a failure
|
|
1333
|
+
* keys the same on both. Pure.
|
|
1334
|
+
*/
|
|
1335
|
+
function repoCommandFindings(failure, at) {
|
|
1336
|
+
const limit = at.limit ?? MAX_LOCATED_FINDINGS;
|
|
1337
|
+
const cls = classifyTestFailure(failure.output, failure.code, limit, failure.env);
|
|
1338
|
+
const parsed = cls.kind === "failures"
|
|
1339
|
+
? cls.findings
|
|
1340
|
+
: parseVerifyFailures(failure.output, failure.code, at.label, limit);
|
|
1341
|
+
return parsed.map((f) => ({
|
|
1342
|
+
...liftFinding(f, at.repoRoot, at.dir),
|
|
1343
|
+
message: `${at.prefix}: ${f.message}`,
|
|
1344
|
+
}));
|
|
1345
|
+
}
|
|
1346
|
+
/** What an approved check's failure is prefixed with, on the build's tree and on the unchanged code. */
|
|
1347
|
+
const approvedPrefix = (label) => `approved check \`${label}\``;
|
|
1348
|
+
/**
|
|
1349
|
+
* An approved check's standing on the unchanged code. What the session
|
|
1350
|
+
* measured there stands over what the host handed it: a red the record kept
|
|
1351
|
+
* may be stale.
|
|
1352
|
+
*/
|
|
1353
|
+
function measuredBaseline(onBase, check) {
|
|
1354
|
+
const command = check.command.trim();
|
|
1355
|
+
if (onBase?.failed.has(command))
|
|
1356
|
+
return "fails_on_base";
|
|
1357
|
+
if (onBase?.passed.has(command))
|
|
1358
|
+
return "runnable";
|
|
1359
|
+
return check.status;
|
|
1360
|
+
}
|
|
1142
1361
|
/**
|
|
1143
1362
|
* Build the runVerify function injected into the orchestrator. It discovers the
|
|
1144
1363
|
* changed package(s) (scoped to this run's diff via `baselinePaths`), detects a
|
|
@@ -1173,11 +1392,7 @@ export function makeRunVerify(opts = {}) {
|
|
|
1173
1392
|
const baseline = new Set(baselinePaths ?? []);
|
|
1174
1393
|
return parseChangedPaths(snapshot.status).filter((p) => !baseline.has(p));
|
|
1175
1394
|
});
|
|
1176
|
-
const toolchain = opts.toolchain ??
|
|
1177
|
-
(() => {
|
|
1178
|
-
const path = process.env.YAGNI_TOOLCHAIN_FILE?.trim();
|
|
1179
|
-
return path ? readToolchainFile(path) : null;
|
|
1180
|
-
});
|
|
1395
|
+
const toolchain = opts.toolchain ?? sessionToolchain;
|
|
1181
1396
|
const treeStatus = opts.treeStatus ??
|
|
1182
1397
|
(async (cwd, signal) => (await snapshotWorkspace(cwd, signal)).status);
|
|
1183
1398
|
const undoTreeWrites = opts.undoTreeWrites ?? defaultUndoTreeWrites;
|
|
@@ -1276,7 +1491,7 @@ export function makeRunVerify(opts = {}) {
|
|
|
1276
1491
|
cwd,
|
|
1277
1492
|
signal,
|
|
1278
1493
|
left: () => callBudget - (now() - halfStartedAt),
|
|
1279
|
-
toolchainFile:
|
|
1494
|
+
toolchainFile: input.toolchainFile,
|
|
1280
1495
|
findings,
|
|
1281
1496
|
reasons,
|
|
1282
1497
|
labels,
|
|
@@ -1292,7 +1507,7 @@ export function makeRunVerify(opts = {}) {
|
|
|
1292
1507
|
});
|
|
1293
1508
|
try {
|
|
1294
1509
|
await runApprovedChecks(run, approved);
|
|
1295
|
-
await runCiSteps(run, steps, new Set(file.checks.map((check) => check.command
|
|
1510
|
+
await runCiSteps(run, steps, new Set(file.checks.map((check) => canonicalCheckCommand(check.command))));
|
|
1296
1511
|
}
|
|
1297
1512
|
finally {
|
|
1298
1513
|
// Whatever ran (an aborted half included) is what the host reads. A half
|
|
@@ -1319,7 +1534,15 @@ export function makeRunVerify(opts = {}) {
|
|
|
1319
1534
|
}
|
|
1320
1535
|
return { findings, reasons, labels, checks: [...unrunnable, ...run.checks] };
|
|
1321
1536
|
}
|
|
1322
|
-
/**
|
|
1537
|
+
/**
|
|
1538
|
+
* One repository command's verdict: passed, a note (it could not run), or a
|
|
1539
|
+
* failure with its output. A check that passed on the unchanged code
|
|
1540
|
+
* (`passedOnBase`, measured in this environment) and now says it lacks a
|
|
1541
|
+
* credential failed: the change made it need one, and that is the change's
|
|
1542
|
+
* (`needs` says what). Only the session that measured knows it passed: a
|
|
1543
|
+
* resumed session's host carries forward what failed on the base, never
|
|
1544
|
+
* what passed, so there the same check is could not run, failing open.
|
|
1545
|
+
*/
|
|
1323
1546
|
async function runRepoCommand(run, command, dir, stepEnv, check) {
|
|
1324
1547
|
const capMs = Math.min(opts.repoCheckTimeoutMs ?? REPO_CHECK_TIMEOUT_MS, run.left());
|
|
1325
1548
|
const composed = composeAbortSignal(run.signal, AbortSignal.timeout(Math.max(1, capMs)));
|
|
@@ -1329,28 +1552,38 @@ export function makeRunVerify(opts = {}) {
|
|
|
1329
1552
|
const startedAt = now();
|
|
1330
1553
|
try {
|
|
1331
1554
|
const { code, output: rawOutput } = await exec(repoCheckArgv(command), dir, composed, env);
|
|
1332
|
-
const record = (baseline, note) =>
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1555
|
+
const record = (baseline, note) => {
|
|
1556
|
+
const entry = {
|
|
1557
|
+
name: check.name,
|
|
1558
|
+
command,
|
|
1559
|
+
cwd: relative(run.cwd, dir) || ".",
|
|
1560
|
+
exitCode: note ? null : code,
|
|
1561
|
+
durationMs: now() - startedAt,
|
|
1562
|
+
baseline,
|
|
1563
|
+
...(note ? { note } : {}),
|
|
1564
|
+
};
|
|
1565
|
+
run.checks.push(entry);
|
|
1566
|
+
return entry;
|
|
1567
|
+
};
|
|
1341
1568
|
if (code === 0) {
|
|
1342
1569
|
record(check.baseline);
|
|
1343
1570
|
return { kind: "passed" };
|
|
1344
1571
|
}
|
|
1345
1572
|
const output = scrubSecrets(rawOutput);
|
|
1346
|
-
const cls = classifyTestFailure(output, code);
|
|
1347
|
-
if (cls.kind === "infra") {
|
|
1573
|
+
const cls = classifyTestFailure(output, code, MAX_LOCATED_FINDINGS, env);
|
|
1574
|
+
if (cls.kind === "infra" && !(cls.needs && check.passedOnBase)) {
|
|
1348
1575
|
const reason = `could not run: ${cls.reason}`;
|
|
1349
1576
|
record("cannot_run", `${reason}; ${exitSummary(check.name, code, output)}`);
|
|
1350
1577
|
return { kind: "note", reason };
|
|
1351
1578
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1579
|
+
return {
|
|
1580
|
+
kind: "failed",
|
|
1581
|
+
output,
|
|
1582
|
+
code,
|
|
1583
|
+
env,
|
|
1584
|
+
record: record(check.baseline),
|
|
1585
|
+
...(cls.kind === "infra" && cls.needs ? { needs: cls.needs } : {}),
|
|
1586
|
+
};
|
|
1354
1587
|
}
|
|
1355
1588
|
catch (err) {
|
|
1356
1589
|
run.checks.push({
|
|
@@ -1368,14 +1601,7 @@ export function makeRunVerify(opts = {}) {
|
|
|
1368
1601
|
}
|
|
1369
1602
|
/** A failure's findings, located from the folder the command ran in and prefixed with what ran. */
|
|
1370
1603
|
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
|
-
}));
|
|
1604
|
+
return repoCommandFindings(failure, { label, prefix, repoRoot: run.cwd, dir });
|
|
1379
1605
|
}
|
|
1380
1606
|
const findingRoom = (run) => Math.max(0, MAX_LOCATED_FINDINGS - run.findings.length);
|
|
1381
1607
|
/**
|
|
@@ -1398,22 +1624,46 @@ export function makeRunVerify(opts = {}) {
|
|
|
1398
1624
|
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
1625
|
continue;
|
|
1400
1626
|
}
|
|
1627
|
+
// The failures it had there, when the session measured it failing.
|
|
1628
|
+
const failedOnBase = opts.checksOnBase?.failed.get(check.command.trim());
|
|
1629
|
+
const status = measuredBaseline(opts.checksOnBase, check);
|
|
1401
1630
|
const verdict = await runRepoCommand(run, check.command, run.cwd, undefined, {
|
|
1402
1631
|
name: label,
|
|
1403
|
-
baseline:
|
|
1632
|
+
baseline: status,
|
|
1633
|
+
passedOnBase: opts.checksOnBase?.passed.has(check.command.trim()) ?? false,
|
|
1404
1634
|
});
|
|
1405
1635
|
if (verdict.kind === "failed")
|
|
1406
|
-
run.approvedFailed.add(check.command
|
|
1636
|
+
run.approvedFailed.add(canonicalCheckCommand(check.command));
|
|
1637
|
+
if (verdict.kind === "failed" && verdict.needs) {
|
|
1638
|
+
run.reasons.push(`approved check \`${label}\` passed on the unchanged code and now needs ${verdict.needs}: the change's failure`);
|
|
1639
|
+
}
|
|
1407
1640
|
if (verdict.kind === "note") {
|
|
1408
1641
|
run.reasons.push(`approved check \`${label}\` ${verdict.reason}`);
|
|
1409
1642
|
}
|
|
1410
|
-
else if (verdict.kind === "failed" &&
|
|
1643
|
+
else if (verdict.kind === "failed" && failedOnBase) {
|
|
1644
|
+
// Only what the unchanged code did not already fail is this change's.
|
|
1645
|
+
// A failure no parser reads is excused only when the unchanged code's
|
|
1646
|
+
// was unread too (a summary line, as PHPUnit prints); after readable
|
|
1647
|
+
// failures there, it says the change broke the run itself.
|
|
1648
|
+
const found = repoFailureFindings(run, verdict, label, approvedPrefix(label), run.cwd);
|
|
1649
|
+
const fresh = subtractBaseline(found, failedOnBase).findings.filter((finding) => failedOnBase.size > 0 || verifyFailureKey(finding) !== null);
|
|
1650
|
+
if (fresh.length === 0) {
|
|
1651
|
+
run.reasons.push(`approved check \`${label}\` failed as it did on the unchanged code before the build; not this change's`);
|
|
1652
|
+
}
|
|
1653
|
+
else {
|
|
1654
|
+
// Its record reads as the change's failure, for the host too.
|
|
1655
|
+
verdict.record.baseline = "runnable";
|
|
1656
|
+
run.labels.push(label);
|
|
1657
|
+
run.findings.push(...fresh.slice(0, findingRoom(run)));
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
else if (verdict.kind === "failed" && status === "fails_on_base") {
|
|
1411
1661
|
run.reasons.push(`approved check \`${label}\` failed, and it already failed on the unchanged code; the host compares the two after the build`);
|
|
1412
1662
|
}
|
|
1413
1663
|
else {
|
|
1414
1664
|
run.labels.push(label);
|
|
1415
1665
|
if (verdict.kind === "failed") {
|
|
1416
|
-
const found = repoFailureFindings(run, verdict, label,
|
|
1666
|
+
const found = repoFailureFindings(run, verdict, label, approvedPrefix(label), run.cwd);
|
|
1417
1667
|
run.findings.push(...found.slice(0, findingRoom(run)));
|
|
1418
1668
|
}
|
|
1419
1669
|
}
|
|
@@ -1450,8 +1700,8 @@ export function makeRunVerify(opts = {}) {
|
|
|
1450
1700
|
return;
|
|
1451
1701
|
if (stoppedJobs.has(job))
|
|
1452
1702
|
continue;
|
|
1453
|
-
if (approvedCommands.has(step.run
|
|
1454
|
-
if (run.approvedFailed.has(step.run
|
|
1703
|
+
if (approvedCommands.has(canonicalCheckCommand(step.run))) {
|
|
1704
|
+
if (run.approvedFailed.has(canonicalCheckCommand(step.run)))
|
|
1455
1705
|
settle(job, "failed");
|
|
1456
1706
|
else
|
|
1457
1707
|
stepPassed(job);
|
|
@@ -1526,6 +1776,8 @@ export function makeRunVerify(opts = {}) {
|
|
|
1526
1776
|
const findings = [];
|
|
1527
1777
|
const reasons = [];
|
|
1528
1778
|
let anyRan = false;
|
|
1779
|
+
// Every half runs on the build's toolchain, as the model's shell does.
|
|
1780
|
+
const toolchainFile = toolchain();
|
|
1529
1781
|
// Failures the clean tree already had, by command label (YAG-1003).
|
|
1530
1782
|
const preexisting = new Map();
|
|
1531
1783
|
const countPreexisting = (label, n) => {
|
|
@@ -1552,11 +1804,12 @@ export function makeRunVerify(opts = {}) {
|
|
|
1552
1804
|
// recorded reason, no finding) without sinking the verdict for the packages
|
|
1553
1805
|
// that did run. Output is scrubbed before it can enter a finding (§0.9).
|
|
1554
1806
|
const typecheckLabels = [];
|
|
1807
|
+
const typecheckEnv = buildGateVerifyEnv(process.env, toolchainFile);
|
|
1555
1808
|
for (const command of commands) {
|
|
1556
1809
|
const composed = composeAbortSignal(signal, AbortSignal.timeout(timeoutMs));
|
|
1557
1810
|
const startedAt = now();
|
|
1558
1811
|
try {
|
|
1559
|
-
const { code, output: rawOutput } = await exec(command.argv, command.cwd, composed);
|
|
1812
|
+
const { code, output: rawOutput } = await exec(command.argv, command.cwd, composed, typecheckEnv);
|
|
1560
1813
|
const output = scrubSecrets(rawOutput);
|
|
1561
1814
|
if (code !== 0) {
|
|
1562
1815
|
// Infra/env failure (command not found in a fresh worktree, Node loader
|
|
@@ -1600,16 +1853,28 @@ export function makeRunVerify(opts = {}) {
|
|
|
1600
1853
|
reasons.push("tests skipped: the typecheck half failed; 'clean' still requires a later round's tests");
|
|
1601
1854
|
}
|
|
1602
1855
|
else {
|
|
1603
|
-
const testEnv = opts.testEnv ??
|
|
1856
|
+
const testEnv = opts.testEnv ?? buildGateTestEnv(process.env, toolchainFile);
|
|
1604
1857
|
let testFindingCount = 0;
|
|
1858
|
+
// A root test script an approved check runs too (`yarn run test`
|
|
1859
|
+
// beside `yarn test`) runs once, as that check, on the repository's
|
|
1860
|
+
// toolchain: the staging bench (2026-10-25) ran one suite as both and
|
|
1861
|
+
// read two verdicts of it.
|
|
1862
|
+
const approvedTwins = new Set((repoFile?.checks ?? [])
|
|
1863
|
+
.filter((check) => check.status !== "cannot_run")
|
|
1864
|
+
.map((check) => canonicalCheckCommand(check.command)));
|
|
1605
1865
|
for (const command of testCommands) {
|
|
1606
1866
|
const where = basename(command.cwd) || command.cwd;
|
|
1867
|
+
const line = command.argv.join(" ");
|
|
1868
|
+
if (relative(cwd, command.cwd) === "" && approvedTwins.has(canonicalCheckCommand(line))) {
|
|
1869
|
+
reasons.push(`tests (${where}): \`${line}\` runs once, as the approved check it is`);
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1607
1872
|
const composed = composeAbortSignal(signal, AbortSignal.timeout(testTimeoutMs));
|
|
1608
1873
|
const startedAt = now();
|
|
1609
1874
|
try {
|
|
1610
1875
|
const { code, output: rawOutput } = await exec(command.argv, command.cwd, composed, testEnv);
|
|
1611
1876
|
const output = scrubSecrets(rawOutput);
|
|
1612
|
-
const cls = classifyTestFailure(output, code, Number.POSITIVE_INFINITY);
|
|
1877
|
+
const cls = classifyTestFailure(output, code, Number.POSITIVE_INFINITY, testEnv);
|
|
1613
1878
|
if (cls.kind === "infra") {
|
|
1614
1879
|
recordCommand(command, null, startedAt, "cannot_run", `could not run: ${cls.reason}; ${exitSummary(command.label, code, output)}`);
|
|
1615
1880
|
reasons.push(`tests (${where}) could not run: ${cls.reason}; 'clean' reflects typecheck + review only`);
|
|
@@ -1674,6 +1939,7 @@ export function makeRunVerify(opts = {}) {
|
|
|
1674
1939
|
signal,
|
|
1675
1940
|
baselinePaths,
|
|
1676
1941
|
callStartedAt,
|
|
1942
|
+
toolchainFile,
|
|
1677
1943
|
});
|
|
1678
1944
|
reasons.push(...repo.reasons);
|
|
1679
1945
|
repoLabels.push(...repo.labels);
|
|
@@ -1781,6 +2047,10 @@ export function makeTakeVerifyBaseline(opts = {}) {
|
|
|
1781
2047
|
...commands.map((command) => ({ command, test: false })),
|
|
1782
2048
|
...tests.map((command) => ({ command, test: true })),
|
|
1783
2049
|
];
|
|
2050
|
+
// The gate's environments, so a failure keys the same on the clean tree and the build's.
|
|
2051
|
+
const toolchainFile = (opts.toolchain ?? sessionToolchain)();
|
|
2052
|
+
const verifyEnv = buildGateVerifyEnv(process.env, toolchainFile);
|
|
2053
|
+
const testEnv = opts.testEnv ?? buildGateTestEnv(process.env, toolchainFile);
|
|
1784
2054
|
try {
|
|
1785
2055
|
for (const { command, test } of runs) {
|
|
1786
2056
|
if (signal?.aborted)
|
|
@@ -1795,13 +2065,11 @@ export function makeTakeVerifyBaseline(opts = {}) {
|
|
|
1795
2065
|
: (opts.timeoutMs ?? VERIFY_TIMEOUT_MS), left);
|
|
1796
2066
|
const composed = composeAbortSignal(signal, AbortSignal.timeout(cap));
|
|
1797
2067
|
try {
|
|
1798
|
-
const { code, output: rawOutput } = test
|
|
1799
|
-
? await exec(command.argv, command.cwd, composed, opts.testEnv ?? buildTestEnv())
|
|
1800
|
-
: await exec(command.argv, command.cwd, composed);
|
|
2068
|
+
const { code, output: rawOutput } = await exec(command.argv, command.cwd, composed, test ? testEnv : verifyEnv);
|
|
1801
2069
|
const output = scrubSecrets(rawOutput);
|
|
1802
2070
|
let failed;
|
|
1803
2071
|
if (test) {
|
|
1804
|
-
const cls = classifyTestFailure(output, code, Number.POSITIVE_INFINITY);
|
|
2072
|
+
const cls = classifyTestFailure(output, code, Number.POSITIVE_INFINITY, testEnv);
|
|
1805
2073
|
if (cls.kind === "infra" || cls.kind === "uncertain") {
|
|
1806
2074
|
result.notes.push(`${command.label} (${command.cwd}): ${cls.kind === "infra" ? cls.reason : "output not interpretable"}`);
|
|
1807
2075
|
if (cls.kind === "uncertain") {
|
|
@@ -1847,4 +2115,115 @@ export function makeTakeVerifyBaseline(opts = {}) {
|
|
|
1847
2115
|
return result;
|
|
1848
2116
|
};
|
|
1849
2117
|
}
|
|
2118
|
+
/**
|
|
2119
|
+
* The whole measurement's cap, before the build starts: each check marked to
|
|
2120
|
+
* measure, once, the plan's first.
|
|
2121
|
+
*/
|
|
2122
|
+
export const CHECKS_ON_BASE_BUDGET_MS = 10 * 60_000;
|
|
2123
|
+
/**
|
|
2124
|
+
* Run each approved check `checks.json` marks `measureOnBase` once on the
|
|
2125
|
+
* unchanged code, before the build changes anything: a plan's check the
|
|
2126
|
+
* host has not seen pass there (none of the record's, or a red one the
|
|
2127
|
+
* record may hold from long ago), and a command the repository's CI runs
|
|
2128
|
+
* when the record names no check of its own. A check the plan wrote for the
|
|
2129
|
+
* change often fails there by design (its test file is not written yet), and a
|
|
2130
|
+
* failure that says only that (a file not found, no tests to run, output no
|
|
2131
|
+
* parser reads) is no verdict: its failures all count after the build, as
|
|
2132
|
+
* before. A failure the gate reads (failing tests, located diagnostics, a
|
|
2133
|
+
* test runner's failure summary) is one the unchanged code already has.
|
|
2134
|
+
*
|
|
2135
|
+
* Run as the gate runs the check ({@link makeRunVerify}): the same command
|
|
2136
|
+
* line, environment and toolchain, from the repository root, so a failure
|
|
2137
|
+
* keys the same on both trees. Bounded: each check by its cap, the whole by
|
|
2138
|
+
* the budget, and one not reached in it has no verdict. What the checks
|
|
2139
|
+
* write into the tree is undone.
|
|
2140
|
+
*/
|
|
2141
|
+
export function makeMeasureChecksOnBase(opts = {}) {
|
|
2142
|
+
const exec = opts.exec ?? defaultExec;
|
|
2143
|
+
const now = opts.now ?? Date.now;
|
|
2144
|
+
const toolchain = opts.toolchain ?? sessionToolchain;
|
|
2145
|
+
const treeStatus = opts.treeStatus ??
|
|
2146
|
+
(async (cwd, signal) => (await snapshotWorkspace(cwd, signal)).status);
|
|
2147
|
+
const undoTreeWrites = opts.undoTreeWrites ?? defaultUndoTreeWrites;
|
|
2148
|
+
return async function measureChecksOnBase(cwd, file, signal, budgetOverrideMs) {
|
|
2149
|
+
const passed = new Set();
|
|
2150
|
+
const failed = new Map();
|
|
2151
|
+
const result = { passed, failed, ran: [], notes: [], treeUndone: true };
|
|
2152
|
+
const targets = file.checks.filter((check) => check.measureOnBase === true &&
|
|
2153
|
+
check.status !== "cannot_run" &&
|
|
2154
|
+
!changesGitState(check.command));
|
|
2155
|
+
if (targets.length === 0)
|
|
2156
|
+
return result;
|
|
2157
|
+
const startedAt = now();
|
|
2158
|
+
const budgetMs = Math.min(opts.budgetMs ?? CHECKS_ON_BASE_BUDGET_MS, budgetOverrideMs ?? Number.POSITIVE_INFINITY);
|
|
2159
|
+
const env = opts.repoChecksEnv ?? buildRepoCheckEnv(process.env, toolchain());
|
|
2160
|
+
const before = await treeStatus(cwd, signal).catch((err) => {
|
|
2161
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2162
|
+
result.treeUndone = false;
|
|
2163
|
+
result.notes.push(`could not read the tree before the checks, so what they write is not undone: ${scrubSecrets(msg)}`);
|
|
2164
|
+
return null;
|
|
2165
|
+
});
|
|
2166
|
+
try {
|
|
2167
|
+
for (const check of targets) {
|
|
2168
|
+
if (signal?.aborted)
|
|
2169
|
+
break;
|
|
2170
|
+
const command = check.command.trim();
|
|
2171
|
+
const label = checkLabel(check.command);
|
|
2172
|
+
const left = budgetMs - (now() - startedAt);
|
|
2173
|
+
if (left < REPO_CHECK_MIN_START_MS) {
|
|
2174
|
+
result.notes.push(`\`${label}\`: not reached in the budget`);
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
const composed = composeAbortSignal(signal, AbortSignal.timeout(Math.min(opts.timeoutMs ?? REPO_CHECK_TIMEOUT_MS, left)));
|
|
2178
|
+
try {
|
|
2179
|
+
const { code, output: rawOutput } = await exec(repoCheckArgv(check.command), cwd, composed, env);
|
|
2180
|
+
if (code === 0) {
|
|
2181
|
+
passed.add(command);
|
|
2182
|
+
result.ran.push(label);
|
|
2183
|
+
continue;
|
|
2184
|
+
}
|
|
2185
|
+
const output = scrubSecrets(rawOutput);
|
|
2186
|
+
const cls = classifyTestFailure(output, code, Number.POSITIVE_INFINITY, env);
|
|
2187
|
+
if (cls.kind === "infra") {
|
|
2188
|
+
result.notes.push(`\`${label}\`: could not run: ${cls.reason}`);
|
|
2189
|
+
continue;
|
|
2190
|
+
}
|
|
2191
|
+
const found = repoCommandFindings({ output, code, env }, {
|
|
2192
|
+
label,
|
|
2193
|
+
prefix: approvedPrefix(label),
|
|
2194
|
+
repoRoot: cwd,
|
|
2195
|
+
dir: cwd,
|
|
2196
|
+
limit: Number.POSITIVE_INFINITY,
|
|
2197
|
+
});
|
|
2198
|
+
// Output no parser reads says nothing of what failed (a test file
|
|
2199
|
+
// the change writes, not there yet): no verdict.
|
|
2200
|
+
if (cls.kind === "uncertain" && found.every((f) => verifyFailureKey(f) === null)) {
|
|
2201
|
+
result.notes.push(`\`${label}\`: exited ${code} with output no parser reads`);
|
|
2202
|
+
continue;
|
|
2203
|
+
}
|
|
2204
|
+
failed.set(command, failureKeyCounts(found));
|
|
2205
|
+
result.ran.push(label);
|
|
2206
|
+
}
|
|
2207
|
+
catch (err) {
|
|
2208
|
+
result.notes.push(`\`${label}\`: ${unexitedNote(err, signal, composed)}`);
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
finally {
|
|
2213
|
+
if (before !== null) {
|
|
2214
|
+
try {
|
|
2215
|
+
const written = newTreeEntries(before, await treeStatus(cwd));
|
|
2216
|
+
if (written.length > 0)
|
|
2217
|
+
await undoTreeWrites(cwd, written);
|
|
2218
|
+
}
|
|
2219
|
+
catch (err) {
|
|
2220
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2221
|
+
result.treeUndone = false;
|
|
2222
|
+
result.notes.push(`could not undo what the checks wrote into the tree: ${scrubSecrets(msg)}`);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
return result;
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
1850
2229
|
//# 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.2031.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": "38e1c8e1dced29e7f90bc2502b9aaef9fa7415db"
|
|
62
62
|
}
|