@yagni-app/code-staging 1.2.10-staging.2108.1 → 1.2.10-staging.2111.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/checkAttribution.d.ts +85 -0
- package/dist/extension/pipeline/checkAttribution.js +131 -0
- package/dist/extension/pipeline/headlessGo.d.ts +5 -5
- package/dist/extension/pipeline/headlessGo.js +112 -87
- package/dist/extension/pipeline/missionChecks.d.ts +11 -1
- package/dist/extension/pipeline/missionChecks.js +24 -2
- package/dist/extension/pipeline/verify.d.ts +137 -138
- package/dist/extension/pipeline/verify.js +297 -372
- package/package.json +2 -2
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one rule for "did this change cause this failure?", as the session's
|
|
3
|
+
* verify gate reads it (`verify.ts`). This package does not depend on
|
|
4
|
+
* `@yagni/shared`, so the region between the markers below is a byte-for-byte
|
|
5
|
+
* copy of the same region of `packages/shared/src/checkAttribution.ts`, where
|
|
6
|
+
* the rule is written out; `test/pipeline/checkAttribution.test.ts` fails when
|
|
7
|
+
* the two differ, and both packages' tests read one corpus,
|
|
8
|
+
* `test/fixtures/checkAttribution.json`. Change the shared file and copy the
|
|
9
|
+
* region here.
|
|
10
|
+
*
|
|
11
|
+
* Copied rather than bundled: the extension is built by `tsc` alone and
|
|
12
|
+
* shipped inside the CLI's own package, and the identical copy keeps that so
|
|
13
|
+
* while the test keeps one rule.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* A command as two lists compare it: the same words, whatever spaces sit
|
|
17
|
+
* between them, and a package manager's script named the one way it runs,
|
|
18
|
+
* so `yarn test` and `yarn run test`, `npm test` and `npm run test`, `pnpm
|
|
19
|
+
* lint` and `pnpm run lint` are one check. Only the leading form: a
|
|
20
|
+
* manager's flags before the script leave the command as it is. Both
|
|
21
|
+
* packages' tests read one corpus,
|
|
22
|
+
* pi-extension-yagni/test/fixtures/equivalentCheckCommands.json.
|
|
23
|
+
*/
|
|
24
|
+
export declare function canonicalCheckCommand(command: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* One check's identity across the change's run and the base's, and across
|
|
27
|
+
* sessions: the folder it runs in, relative to the repository root, and its
|
|
28
|
+
* command as {@link canonicalCheckCommand} reads it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function checkId(command: string, cwd: string): string;
|
|
31
|
+
/** A failing test's key: its name, without the duration a runner prints after it. */
|
|
32
|
+
export declare function testKey(name: string): string;
|
|
33
|
+
/** A located diagnostic's key: its file and message, never its line, which a change above it moves. */
|
|
34
|
+
export declare function diagKey(file: string, message: string): string;
|
|
35
|
+
/** A check annotation's key: its path and message, never its line. */
|
|
36
|
+
export declare function annotationKey(path: string, message: string): string;
|
|
37
|
+
/**
|
|
38
|
+
* What the change did to a failure: `caused` it (nothing of it was on the
|
|
39
|
+
* base), found it `preexisting` (every key was on the base at least as many
|
|
40
|
+
* times), `partly` caused it (some keys were, some were not), or there is
|
|
41
|
+
* `no_baseline` to compare with, and it counts as the change's.
|
|
42
|
+
*/
|
|
43
|
+
export type FailureAttribution = "caused" | "preexisting" | "partly" | "no_baseline";
|
|
44
|
+
/**
|
|
45
|
+
* One run of a check as the rule reads it: its failure keys, null for a
|
|
46
|
+
* failure that names nothing, and the step that failed when the runner says.
|
|
47
|
+
* A run that passed has no keys.
|
|
48
|
+
*/
|
|
49
|
+
export interface FailureRun {
|
|
50
|
+
keys: ReadonlyArray<string | null>;
|
|
51
|
+
step?: string | null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The rule. `excused` is index for index with `head.keys`: true where the
|
|
55
|
+
* base already had that failure, each base key excusing one head key at most.
|
|
56
|
+
* Pure.
|
|
57
|
+
*/
|
|
58
|
+
export declare function attributeFailure(head: FailureRun, base: FailureRun | null | undefined): {
|
|
59
|
+
verdict: FailureAttribution;
|
|
60
|
+
excused: boolean[];
|
|
61
|
+
};
|
|
62
|
+
/** What one check did on the base: it passed, or it failed with these keys. */
|
|
63
|
+
export interface BaseMeasurement {
|
|
64
|
+
id: string;
|
|
65
|
+
outcome: "passed" | "failed";
|
|
66
|
+
keys: string[];
|
|
67
|
+
}
|
|
68
|
+
/** The file the session writes beside its `result.json`, and the bounds both sides keep. */
|
|
69
|
+
export declare const BASE_MEASUREMENTS_FILE_NAME = "base-measurements.json";
|
|
70
|
+
export declare const MAX_BASE_MEASUREMENTS = 100;
|
|
71
|
+
export declare const MAX_BASE_KEYS = 1000;
|
|
72
|
+
export declare const MAX_BASE_KEY_CHARS = 500;
|
|
73
|
+
/**
|
|
74
|
+
* One measurement within the bounds: a key longer than the bound is left
|
|
75
|
+
* out, and keys past the count bound are dropped. Either only makes fewer
|
|
76
|
+
* failures excused, never more.
|
|
77
|
+
*/
|
|
78
|
+
export declare function boundedMeasurement(measurement: BaseMeasurement): BaseMeasurement;
|
|
79
|
+
/**
|
|
80
|
+
* `base-measurements.json` as the host reads it (`parseBaseMeasurements` in
|
|
81
|
+
* `@yagni/shared`): the commit the checks ran on, and what each did there,
|
|
82
|
+
* each held to the bounds.
|
|
83
|
+
*/
|
|
84
|
+
export declare function renderBaseMeasurements(baseSha: string, measured: ReadonlyMap<string, Omit<BaseMeasurement, "id">>): string;
|
|
85
|
+
//# sourceMappingURL=checkAttribution.d.ts.map
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one rule for "did this change cause this failure?", as the session's
|
|
3
|
+
* verify gate reads it (`verify.ts`). This package does not depend on
|
|
4
|
+
* `@yagni/shared`, so the region between the markers below is a byte-for-byte
|
|
5
|
+
* copy of the same region of `packages/shared/src/checkAttribution.ts`, where
|
|
6
|
+
* the rule is written out; `test/pipeline/checkAttribution.test.ts` fails when
|
|
7
|
+
* the two differ, and both packages' tests read one corpus,
|
|
8
|
+
* `test/fixtures/checkAttribution.json`. Change the shared file and copy the
|
|
9
|
+
* region here.
|
|
10
|
+
*
|
|
11
|
+
* Copied rather than bundled: the extension is built by `tsc` alone and
|
|
12
|
+
* shipped inside the CLI's own package, and the identical copy keeps that so
|
|
13
|
+
* while the test keeps one rule.
|
|
14
|
+
*/
|
|
15
|
+
// --- mirrored region: begin (pi-extension-yagni/src/pipeline/checkAttribution.ts)
|
|
16
|
+
/** Package-manager words that are the manager's own command, never a script. */
|
|
17
|
+
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(" "));
|
|
18
|
+
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(" "));
|
|
19
|
+
/** The scripts npm runs without `run`: its lifecycle shorthands. */
|
|
20
|
+
const NPM_SCRIPT_SHORTHANDS = new Set(["test", "start", "stop", "restart"]);
|
|
21
|
+
/**
|
|
22
|
+
* A command as two lists compare it: the same words, whatever spaces sit
|
|
23
|
+
* between them, and a package manager's script named the one way it runs,
|
|
24
|
+
* so `yarn test` and `yarn run test`, `npm test` and `npm run test`, `pnpm
|
|
25
|
+
* lint` and `pnpm run lint` are one check. Only the leading form: a
|
|
26
|
+
* manager's flags before the script leave the command as it is. Both
|
|
27
|
+
* packages' tests read one corpus,
|
|
28
|
+
* pi-extension-yagni/test/fixtures/equivalentCheckCommands.json.
|
|
29
|
+
*/
|
|
30
|
+
export function canonicalCheckCommand(command) {
|
|
31
|
+
const words = command.trim().split(/\s+/).filter(Boolean);
|
|
32
|
+
const [manager, next, ...rest] = words;
|
|
33
|
+
const script = /^[A-Za-z0-9][\w:.@/-]*$/;
|
|
34
|
+
if (manager === "npm" || manager === "yarn" || manager === "pnpm") {
|
|
35
|
+
const runs = next === "run" || (manager === "npm" && next === "run-script");
|
|
36
|
+
if (runs && rest[0] && script.test(rest[0]))
|
|
37
|
+
return [manager, "run", ...rest].join(" ");
|
|
38
|
+
const shorthand = next !== undefined &&
|
|
39
|
+
script.test(next) &&
|
|
40
|
+
(manager === "npm"
|
|
41
|
+
? NPM_SCRIPT_SHORTHANDS.has(next)
|
|
42
|
+
: !(manager === "yarn" ? YARN_COMMANDS : PNPM_COMMANDS).has(next));
|
|
43
|
+
if (shorthand)
|
|
44
|
+
return [manager, "run", next, ...rest].join(" ");
|
|
45
|
+
}
|
|
46
|
+
return words.join(" ");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* One check's identity across the change's run and the base's, and across
|
|
50
|
+
* sessions: the folder it runs in, relative to the repository root, and its
|
|
51
|
+
* command as {@link canonicalCheckCommand} reads it.
|
|
52
|
+
*/
|
|
53
|
+
export function checkId(command, cwd) {
|
|
54
|
+
const dir = cwd
|
|
55
|
+
.replace(/\\/g, "/")
|
|
56
|
+
.replace(/^(?:\.\/+)+/, "")
|
|
57
|
+
.replace(/\/+$/, "") || ".";
|
|
58
|
+
return `${dir}\u0000${canonicalCheckCommand(command)}`;
|
|
59
|
+
}
|
|
60
|
+
/** A failing test's key: its name, without the duration a runner prints after it. */
|
|
61
|
+
export function testKey(name) {
|
|
62
|
+
return `test:${name.replace(/\s*\(\d+(?:\.\d+)?\s*m?s\)\s*$/, "").trim()}`;
|
|
63
|
+
}
|
|
64
|
+
/** A located diagnostic's key: its file and message, never its line, which a change above it moves. */
|
|
65
|
+
export function diagKey(file, message) {
|
|
66
|
+
return `diag:${file.replace(/\\/g, "/").replace(/^(?:\.\/+)+/, "")}\u0000${message.trim()}`;
|
|
67
|
+
}
|
|
68
|
+
/** A check annotation's key: its path and message, never its line. */
|
|
69
|
+
export function annotationKey(path, message) {
|
|
70
|
+
return `ann:${path}\u0000${message.trim()}`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The rule. `excused` is index for index with `head.keys`: true where the
|
|
74
|
+
* base already had that failure, each base key excusing one head key at most.
|
|
75
|
+
* Pure.
|
|
76
|
+
*/
|
|
77
|
+
export function attributeFailure(head, base) {
|
|
78
|
+
const excused = head.keys.map(() => false);
|
|
79
|
+
if (!base)
|
|
80
|
+
return { verdict: "no_baseline", excused };
|
|
81
|
+
if (head.step && base.step && head.step !== base.step)
|
|
82
|
+
return { verdict: "caused", excused };
|
|
83
|
+
const left = new Map();
|
|
84
|
+
for (const key of base.keys)
|
|
85
|
+
if (key !== null)
|
|
86
|
+
left.set(key, (left.get(key) ?? 0) + 1);
|
|
87
|
+
head.keys.forEach((key, index) => {
|
|
88
|
+
const count = key === null ? 0 : (left.get(key) ?? 0);
|
|
89
|
+
if (key === null || count === 0)
|
|
90
|
+
return;
|
|
91
|
+
left.set(key, count - 1);
|
|
92
|
+
excused[index] = true;
|
|
93
|
+
});
|
|
94
|
+
const count = excused.filter(Boolean).length;
|
|
95
|
+
const verdict = count === 0 ? "caused" : count === head.keys.length ? "preexisting" : "partly";
|
|
96
|
+
return { verdict, excused };
|
|
97
|
+
}
|
|
98
|
+
/** The file the session writes beside its `result.json`, and the bounds both sides keep. */
|
|
99
|
+
export const BASE_MEASUREMENTS_FILE_NAME = "base-measurements.json";
|
|
100
|
+
export const MAX_BASE_MEASUREMENTS = 100;
|
|
101
|
+
export const MAX_BASE_KEYS = 1_000;
|
|
102
|
+
export const MAX_BASE_KEY_CHARS = 500;
|
|
103
|
+
/**
|
|
104
|
+
* One measurement within the bounds: a key longer than the bound is left
|
|
105
|
+
* out, and keys past the count bound are dropped. Either only makes fewer
|
|
106
|
+
* failures excused, never more.
|
|
107
|
+
*/
|
|
108
|
+
export function boundedMeasurement(measurement) {
|
|
109
|
+
return {
|
|
110
|
+
id: measurement.id,
|
|
111
|
+
outcome: measurement.outcome,
|
|
112
|
+
keys: measurement.outcome === "passed"
|
|
113
|
+
? []
|
|
114
|
+
: measurement.keys
|
|
115
|
+
.filter((key) => key.length <= MAX_BASE_KEY_CHARS)
|
|
116
|
+
.slice(0, MAX_BASE_KEYS),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
// --- mirrored region: end
|
|
120
|
+
/**
|
|
121
|
+
* `base-measurements.json` as the host reads it (`parseBaseMeasurements` in
|
|
122
|
+
* `@yagni/shared`): the commit the checks ran on, and what each did there,
|
|
123
|
+
* each held to the bounds.
|
|
124
|
+
*/
|
|
125
|
+
export function renderBaseMeasurements(baseSha, measured) {
|
|
126
|
+
const checks = [...measured]
|
|
127
|
+
.slice(0, MAX_BASE_MEASUREMENTS)
|
|
128
|
+
.map(([id, measurement]) => boundedMeasurement({ id, ...measurement }));
|
|
129
|
+
return `${JSON.stringify({ version: 1, baseSha, checks }, null, 2)}\n`;
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=checkAttribution.js.map
|
|
@@ -75,7 +75,7 @@ import { snapshotWorkspace } from "./workspace.js";
|
|
|
75
75
|
import type { WorktreeExec } from "./worktree.js";
|
|
76
76
|
import { type TreeSnapshotDeps } from "./treeSnapshot.js";
|
|
77
77
|
import { runPipeline as defaultRunPipeline } from "./orchestrator.js";
|
|
78
|
-
import {
|
|
78
|
+
import { baseTargets, makeMeasureOnBase, makeRunVerify } from "./verify.js";
|
|
79
79
|
import { type RunBudget } from "./budget.js";
|
|
80
80
|
import type { Finding, JsonEvent, ModelTier, PipelineStage, StageId, StageTag, StageUsage, StopReason } from "./types.js";
|
|
81
81
|
/** One-line usage copy, shared by every argument error. */
|
|
@@ -182,14 +182,14 @@ export interface HeadlessGoDeps {
|
|
|
182
182
|
* there is none). Defaults to the real gate; injected in tests.
|
|
183
183
|
*/
|
|
184
184
|
makeRunVerify?: typeof makeRunVerify;
|
|
185
|
-
/** The
|
|
186
|
-
|
|
185
|
+
/** The checks' run on the unchanged code (YAG-1003). Defaults to the real one; injected in tests. */
|
|
186
|
+
measureOnBase?: ReturnType<typeof makeMeasureOnBase>;
|
|
187
|
+
/** What the run on the unchanged code measures. Defaults to the real detection; injected in tests. */
|
|
188
|
+
baseTargets?: typeof baseTargets;
|
|
187
189
|
/** A resumed session's switch to the attempt's base commit and back (baseTree.ts). Defaults to real git; injected in tests. */
|
|
188
190
|
measureOnBaseTree?: typeof measureOnBaseTree;
|
|
189
191
|
/** The paths the session's HEAD changed since the attempt's base, null when git cannot say. Defaults to real git; injected in tests. */
|
|
190
192
|
pathsCommittedSince?: (cwd: string, base: string) => Promise<string[] | null>;
|
|
191
|
-
/** The verify gate's clean-tree baseline run (YAG-1003). Defaults to the real one; injected in tests. */
|
|
192
|
-
takeVerifyBaseline?: (cwd: string, dirs: string[], signal?: AbortSignal, budgetMs?: number) => Promise<VerifyBaselineResult>;
|
|
193
193
|
/** stdout sink; one call per NDJSON line, newline added by the caller. */
|
|
194
194
|
write?: (line: string) => void;
|
|
195
195
|
/** stderr sink for human-readable problems. */
|
|
@@ -86,7 +86,8 @@ import { normalizeMission } from "./mission.js";
|
|
|
86
86
|
import { MISSION_CHECKS_FILE_NAME, MISSION_CI_RESULTS_FILE_NAME, readMissionChecksFile, renderCiResults, } from "./missionChecks.js";
|
|
87
87
|
import { startTreeSnapshots } from "./treeSnapshot.js";
|
|
88
88
|
import { PipelineStageError, runPipeline as defaultRunPipeline, } from "./orchestrator.js";
|
|
89
|
-
import { baselineDirs,
|
|
89
|
+
import { baselineDirs, baseTargets, makeMeasureOnBase, makeRunVerify, makeWorkstreamCheck, parseChangedPaths, planPaths, } from "./verify.js";
|
|
90
|
+
import { BASE_MEASUREMENTS_FILE_NAME, renderBaseMeasurements, } from "./checkAttribution.js";
|
|
90
91
|
import { loadGroundingEnabled } from "../grounding.js";
|
|
91
92
|
import { codeStateHome } from "../stateHome.js";
|
|
92
93
|
import { parseTierCap, TIER_CAP_ENV } from "./tierCap.js";
|
|
@@ -448,13 +449,16 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
448
449
|
...(mission ? { mission } : {}),
|
|
449
450
|
}, lane ? { maxRounds: lane.maxReviewRounds, committed: true } : {})
|
|
450
451
|
: undefined;
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
// the attempt's
|
|
457
|
-
|
|
452
|
+
// What the gate compares a failure with: each check it will judge, run once
|
|
453
|
+
// on the unchanged code before the build (YAG-1003, `makeMeasureOnBase`), so
|
|
454
|
+
// a failure the base already had is a note, never a finding
|
|
455
|
+
// (`attributeFailure`, checkAttribution.ts). A fresh build's tree is the
|
|
456
|
+
// base; a continuation or a fix round already carries a change, so taking
|
|
457
|
+
// them on its own tree would excuse the attempt's own breakage, and it
|
|
458
|
+
// measures on the attempt's base commit instead (baseTree.ts), when its host
|
|
459
|
+
// names one. What the attempt's earlier sessions measured on that base
|
|
460
|
+
// comes in `checks.json` (`baseMeasured`) and is not run again; what this
|
|
461
|
+
// session measures goes to `base-measurements.json` for the host to keep.
|
|
458
462
|
const logPipeline = (event, data) => {
|
|
459
463
|
deps.logger?.(event, data);
|
|
460
464
|
if (stream)
|
|
@@ -502,9 +506,9 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
502
506
|
}
|
|
503
507
|
return { exitCode: HEADLESS_GO_EXIT.unverified, error: message };
|
|
504
508
|
};
|
|
505
|
-
// A measurement before the build
|
|
506
|
-
//
|
|
507
|
-
//
|
|
509
|
+
// A measurement before the build gets at most a sixth of the run's time
|
|
510
|
+
// left when it starts, and none below a minute: the build itself must
|
|
511
|
+
// still fit. No deadline, no cap.
|
|
508
512
|
const preBuildShare = () => {
|
|
509
513
|
const leftMs = budget?.deadlineMs === undefined ? undefined : budget.deadlineMs - (deps.now ?? Date.now)();
|
|
510
514
|
const shareMs = leftMs === undefined ? undefined : Math.floor(leftMs / BASELINE_SHARE_OF_TIME_LEFT);
|
|
@@ -514,8 +518,15 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
514
518
|
tooLittle: shareMs !== undefined && shareMs < MIN_BASELINE_BUDGET_MS,
|
|
515
519
|
};
|
|
516
520
|
};
|
|
517
|
-
|
|
518
|
-
|
|
521
|
+
const measureFile = gateOpts?.repoChecks?.() ?? null;
|
|
522
|
+
// The host matched them to the attempt's base: the tree a fresh build
|
|
523
|
+
// starts on, and the commit a resumed session measures on.
|
|
524
|
+
const measuredBase = carriedMeasurements(measureFile);
|
|
525
|
+
const carried = measuredBase.size;
|
|
526
|
+
const measuredHere = new Map();
|
|
527
|
+
const targetsOf = deps.baseTargets ?? baseTargets;
|
|
528
|
+
/** The targets no session measured yet, on the tree as it is; never throws. */
|
|
529
|
+
const measure = async (targets) => {
|
|
519
530
|
const share = preBuildShare();
|
|
520
531
|
if (share.tooLittle) {
|
|
521
532
|
logPipeline("verify_baseline", {
|
|
@@ -526,79 +537,53 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
526
537
|
return;
|
|
527
538
|
}
|
|
528
539
|
try {
|
|
529
|
-
const
|
|
540
|
+
const result = await (deps.measureOnBase ?? makeMeasureOnBase(heard))(cwd, targets, deps.signal, share.shareMs);
|
|
530
541
|
logPipeline("verify_baseline", {
|
|
531
|
-
ran:
|
|
532
|
-
failures:
|
|
533
|
-
notes:
|
|
542
|
+
ran: result.ran,
|
|
543
|
+
failures: result.failures,
|
|
544
|
+
notes: result.notes,
|
|
545
|
+
treeUndone: result.treeUndone,
|
|
546
|
+
carried,
|
|
534
547
|
});
|
|
535
|
-
|
|
536
|
-
|
|
548
|
+
for (const [id, measurement] of result.measured) {
|
|
549
|
+
measuredBase.set(id, measurement);
|
|
550
|
+
measuredHere.set(id, measurement);
|
|
551
|
+
}
|
|
537
552
|
}
|
|
538
553
|
catch (err) {
|
|
539
554
|
const message = err instanceof Error ? err.message : String(err);
|
|
540
555
|
logPipeline("verify_baseline", { error: message });
|
|
541
|
-
writeErr(`The
|
|
556
|
+
writeErr(`The base measurement could not run (every failure counts): ${message}`);
|
|
542
557
|
}
|
|
543
558
|
};
|
|
559
|
+
const missing = (targets) => targets.filter((target) => !measuredBase.has(target.id));
|
|
544
560
|
const resumed = Boolean(continuation || fixRound);
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
/** The approved checks `file` marks to measure, on the tree as it is; never throws. */
|
|
558
|
-
const measureChecks = async (file) => {
|
|
559
|
-
const share = preBuildShare();
|
|
560
|
-
if (share.tooLittle) {
|
|
561
|
-
logPipeline("verify_checks_on_base", {
|
|
562
|
-
skipped: "too little of the run's time left",
|
|
563
|
-
leftMs: share.leftMs,
|
|
564
|
-
shareMs: share.shareMs,
|
|
565
|
-
});
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
try {
|
|
569
|
-
const measured = await (deps.measureChecksOnBase ?? makeMeasureChecksOnBase(heard))(cwd, file, deps.signal, share.shareMs);
|
|
570
|
-
logPipeline("verify_checks_on_base", {
|
|
571
|
-
ran: measured.ran,
|
|
572
|
-
passed: [...measured.passed],
|
|
573
|
-
failed: [...measured.failed.keys()],
|
|
574
|
-
notes: measured.notes,
|
|
575
|
-
treeUndone: measured.treeUndone,
|
|
576
|
-
});
|
|
577
|
-
if (measured.passed.size > 0 || measured.failed.size > 0)
|
|
578
|
-
checksOnBase = measured;
|
|
579
|
-
}
|
|
580
|
-
catch (err) {
|
|
581
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
582
|
-
logPipeline("verify_checks_on_base", { error: message });
|
|
583
|
-
writeErr(`The approved checks could not run on the unchanged code (each keeps its checks.json standing): ${message}`);
|
|
584
|
-
}
|
|
585
|
-
};
|
|
586
|
-
if (measureFile?.checks.some((check) => check.measureOnBase) &&
|
|
587
|
-
!resumed &&
|
|
588
|
-
!deps.signal?.aborted) {
|
|
589
|
-
await measureChecks(measureFile);
|
|
561
|
+
// The commit a resumed session measured on; a fresh build's is its start.
|
|
562
|
+
let measuredOn;
|
|
563
|
+
if (!resumed && !deps.signal?.aborted) {
|
|
564
|
+
// The packages the approved plan names, and the approved checks the host
|
|
565
|
+
// has not seen pass on the unchanged code (`measureOnBase`: the plan's,
|
|
566
|
+
// and CI's when the repository's record has none of its own).
|
|
567
|
+
const targets = missing(targetsOf(cwd, {
|
|
568
|
+
dirs: mission?.plan ? baselineDirs(cwd, planPaths(mission.plan), readFile) : [],
|
|
569
|
+
checks: measureFile,
|
|
570
|
+
}, gateOpts ?? {}));
|
|
571
|
+
if (targets.length > 0)
|
|
572
|
+
await measure(targets);
|
|
590
573
|
}
|
|
591
574
|
// A one-attempt lane continuation or fix round starts past the attempt's
|
|
592
|
-
// earlier commits, so it measures
|
|
575
|
+
// earlier commits, so it measures on the attempt's base commit
|
|
593
576
|
// (`continue.json` names it, YAG-1042), with the tree put back after (YAG-1056).
|
|
594
577
|
// Before, its host had nothing to hand it for the gate's own checks and only
|
|
595
578
|
// a stale record for a plan check, so every failure main already had was its
|
|
596
579
|
// to fix, and COREX-1291's sessions ended `round_cap` on main's lint errors
|
|
597
580
|
// one after another (production, 2026-10-26). It measures every approved
|
|
598
581
|
// check it can run there, not only those marked: the record's green is what
|
|
599
|
-
// went stale.
|
|
600
|
-
//
|
|
601
|
-
//
|
|
582
|
+
// went stale. It covers the plan's packages and those the attempt changed.
|
|
583
|
+
// What an earlier session measured on the same base is not run again, and
|
|
584
|
+
// with nothing missing the base is never checked out. A base it cannot
|
|
585
|
+
// measure on leaves the run as it was; a tree it cannot put back stops the
|
|
586
|
+
// session before it builds.
|
|
602
587
|
if (cloud && resumed && !deps.signal?.aborted) {
|
|
603
588
|
const base = baseCommitOf(continuation?.baseSha) ?? baseCommitOf(fixRound?.baseRef);
|
|
604
589
|
const share = preBuildShare();
|
|
@@ -614,24 +599,19 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
614
599
|
});
|
|
615
600
|
}
|
|
616
601
|
else {
|
|
617
|
-
const onBaseFile = measureFile && {
|
|
618
|
-
...measureFile,
|
|
619
|
-
checks: measureFile.checks.map((check) => check.status === "cannot_run" ? check : { ...check, measureOnBase: true }),
|
|
620
|
-
};
|
|
621
|
-
const checksToMeasure = Boolean(onBaseFile?.checks.some((check) => check.status !== "cannot_run"));
|
|
622
602
|
try {
|
|
623
|
-
// A list git cannot read narrows the
|
|
603
|
+
// A list git cannot read narrows the measurement to the plan's packages: said.
|
|
624
604
|
const changed = await (deps.pathsCommittedSince ?? pathsCommittedSince)(cwd, base);
|
|
625
605
|
const unread = changed === null ? { changedPathsUnread: true } : {};
|
|
626
606
|
const dirs = baselineDirs(cwd, [...planPaths(mission?.plan ?? ""), ...(changed ?? [])], readFile);
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
607
|
+
const targets = missing(targetsOf(cwd, { dirs, checks: measureFile, allChecks: true }, gateOpts ?? {}));
|
|
608
|
+
measuredOn = base;
|
|
609
|
+
const outcome = targets.length === 0
|
|
610
|
+
? {
|
|
611
|
+
kind: "skipped",
|
|
612
|
+
reason: carried > 0 ? "measured by an earlier session" : "nothing to measure",
|
|
613
|
+
}
|
|
614
|
+
: await (deps.measureOnBaseTree ?? measureOnBaseTree)(cwd, base, () => measure(targets));
|
|
635
615
|
logPipeline("verify_base_tree", outcome.kind === "measured"
|
|
636
616
|
? { base, measured: true, ...unread }
|
|
637
617
|
: { base, skipped: outcome.reason, ...unread });
|
|
@@ -682,11 +662,22 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
682
662
|
const scan = cloud
|
|
683
663
|
? (scanCwd, signal, skip) => scanChange(scanCwd, { exec, sessionSecrets, ...onBase }, skip, signal)
|
|
684
664
|
: undefined;
|
|
685
|
-
|
|
665
|
+
// What this session measured, for the host to keep: on the base a resumed
|
|
666
|
+
// session checked out, or the commit a fresh build starts on.
|
|
667
|
+
if (cloud && measuredHere.size > 0) {
|
|
668
|
+
writeBaseMeasurements({
|
|
669
|
+
outDir: cloud.outDir,
|
|
670
|
+
sha: measuredOn ?? baseSha,
|
|
671
|
+
measured: measuredHere,
|
|
672
|
+
writeFile: deps.writeFile ?? writeFileSync,
|
|
673
|
+
log: logPipeline,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
const onBaseOpts = measuredBase.size > 0 ? { onBase: measuredBase } : {};
|
|
677
|
+
if (gateOpts || measuredBase.size > 0 || cloud) {
|
|
686
678
|
const made = (deps.makeRunVerify ?? makeRunVerify)({
|
|
687
679
|
...gateOpts,
|
|
688
|
-
...
|
|
689
|
-
...(checksOnBase ? { checksOnBase } : {}),
|
|
680
|
+
...onBaseOpts,
|
|
690
681
|
...(scan ? { scanChange: scan } : {}),
|
|
691
682
|
// The one-attempt lane's host runs no checks after the build.
|
|
692
683
|
...(cloud
|
|
@@ -725,7 +716,9 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
725
716
|
...(reviewLane ? { lane: reviewLane } : {}),
|
|
726
717
|
...(mission ? { mission } : {}),
|
|
727
718
|
...(runVerify ? { runVerify } : {}),
|
|
728
|
-
...(
|
|
719
|
+
...(measuredBase.size > 0
|
|
720
|
+
? { runWorkstreamCheck: makeWorkstreamCheck({ onBase: measuredBase }) }
|
|
721
|
+
: {}),
|
|
729
722
|
// A cloud execution never starts on an earlier attempt's uncommitted
|
|
730
723
|
// edits: that work is on the branch, so its tree baselines as it is.
|
|
731
724
|
...(continuation && !cloud ? { continuation: { stageAtStop: continuation.stageAtStop } } : {}),
|
|
@@ -859,4 +852,36 @@ export async function runHeadlessGo(argv, deps = {}) {
|
|
|
859
852
|
function treeKey(snapshot) {
|
|
860
853
|
return `${snapshot.head}\n${snapshot.status}`;
|
|
861
854
|
}
|
|
855
|
+
/** What the attempt's earlier sessions measured on its base, as `checks.json` carries it, by check id. */
|
|
856
|
+
function carriedMeasurements(file) {
|
|
857
|
+
return new Map((file?.baseMeasured?.checks ?? []).map((check) => [
|
|
858
|
+
check.id,
|
|
859
|
+
{ outcome: check.outcome, keys: check.keys },
|
|
860
|
+
]));
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* `base-measurements.json` beside `result.json`, for the host to keep. With
|
|
864
|
+
* no commit to name, or a write that fails, it is logged and dropped: the
|
|
865
|
+
* next session measures again.
|
|
866
|
+
*/
|
|
867
|
+
function writeBaseMeasurements(input) {
|
|
868
|
+
const path = join(input.outDir, BASE_MEASUREMENTS_FILE_NAME);
|
|
869
|
+
if (!input.sha) {
|
|
870
|
+
input.log("base_measurements_write_failed", {
|
|
871
|
+
path,
|
|
872
|
+
error: "no commit to name what was measured on",
|
|
873
|
+
measured: input.measured.size,
|
|
874
|
+
});
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
try {
|
|
878
|
+
input.writeFile(path, renderBaseMeasurements(input.sha, input.measured));
|
|
879
|
+
}
|
|
880
|
+
catch (err) {
|
|
881
|
+
input.log("base_measurements_write_failed", {
|
|
882
|
+
path,
|
|
883
|
+
error: err instanceof Error ? err.message : String(err),
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
}
|
|
862
887
|
//# sourceMappingURL=headlessGo.js.map
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* parser. A change there is a change here. A file that is missing or does not
|
|
16
16
|
* read as version 1 means no repository checks, never a stopped build.
|
|
17
17
|
*/
|
|
18
|
+
import { type BaseMeasurement } from "./checkAttribution.js";
|
|
18
19
|
export declare const MISSION_CHECKS_FILE_NAME = "checks.json";
|
|
19
20
|
export type MissionCheckStatus = "runnable" | "fails_on_base" | "cannot_run";
|
|
20
21
|
export interface MissionCheck {
|
|
@@ -25,7 +26,7 @@ export interface MissionCheck {
|
|
|
25
26
|
* The host has not seen it pass on the unchanged code (an approved plan's
|
|
26
27
|
* check, or one CI runs that has no baseline): the session runs it there
|
|
27
28
|
* once before the build, and what that
|
|
28
|
-
* measures stands over `status` (`
|
|
29
|
+
* measures stands over `status` (`makeMeasureOnBase` in verify.ts).
|
|
29
30
|
*/
|
|
30
31
|
measureOnBase?: boolean;
|
|
31
32
|
}
|
|
@@ -61,6 +62,15 @@ export interface MissionChecksFile {
|
|
|
61
62
|
name: string;
|
|
62
63
|
reason: string;
|
|
63
64
|
}>;
|
|
65
|
+
/**
|
|
66
|
+
* What the attempt's earlier sessions measured on its base commit, by check
|
|
67
|
+
* id (`checkId`): the session measures only the rest. A field that does
|
|
68
|
+
* not read is left out, never the file.
|
|
69
|
+
*/
|
|
70
|
+
baseMeasured?: {
|
|
71
|
+
baseSha: string;
|
|
72
|
+
checks: BaseMeasurement[];
|
|
73
|
+
};
|
|
64
74
|
}
|
|
65
75
|
export declare const MISSION_CI_RESULTS_FILE_NAME = "ci-results.json";
|
|
66
76
|
/** How one failed CI check's job went the last time the gate ran it. */
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* read as version 1 means no repository checks, never a stopped build.
|
|
17
17
|
*/
|
|
18
18
|
import { readFileSync } from "node:fs";
|
|
19
|
+
import { boundedMeasurement } from "./checkAttribution.js";
|
|
19
20
|
export const MISSION_CHECKS_FILE_NAME = "checks.json";
|
|
20
21
|
export const MISSION_CI_RESULTS_FILE_NAME = "ci-results.json";
|
|
21
22
|
/** `ci-results.json` (version 1), as the host reads it. */
|
|
@@ -98,6 +99,20 @@ function parseJob(value) {
|
|
|
98
99
|
}
|
|
99
100
|
return job;
|
|
100
101
|
}
|
|
102
|
+
function parseBaseMeasured(value) {
|
|
103
|
+
if (!isRecord(value) || !isString(value.baseSha) || !Array.isArray(value.checks))
|
|
104
|
+
return undefined;
|
|
105
|
+
const checks = [];
|
|
106
|
+
for (const raw of value.checks) {
|
|
107
|
+
if (!isRecord(raw) || !isString(raw.id) || !raw.id)
|
|
108
|
+
continue;
|
|
109
|
+
if (raw.outcome !== "passed" && raw.outcome !== "failed")
|
|
110
|
+
continue;
|
|
111
|
+
const keys = Array.isArray(raw.keys) ? raw.keys.filter(isString) : [];
|
|
112
|
+
checks.push(boundedMeasurement({ id: raw.id, outcome: raw.outcome, keys }));
|
|
113
|
+
}
|
|
114
|
+
return { baseSha: value.baseSha, checks };
|
|
115
|
+
}
|
|
101
116
|
function parseCannotRunChecks(value) {
|
|
102
117
|
if (!Array.isArray(value))
|
|
103
118
|
return null;
|
|
@@ -145,12 +160,19 @@ export function parseMissionChecksFile(text) {
|
|
|
145
160
|
return null;
|
|
146
161
|
ciJobs.push(job);
|
|
147
162
|
}
|
|
163
|
+
const baseMeasured = parseBaseMeasured(parsed.baseMeasured);
|
|
164
|
+
const file = {
|
|
165
|
+
version: 1,
|
|
166
|
+
checks,
|
|
167
|
+
ciJobs,
|
|
168
|
+
...(baseMeasured ? { baseMeasured } : {}),
|
|
169
|
+
};
|
|
148
170
|
if (parsed.failedChecksCannotRun === undefined)
|
|
149
|
-
return
|
|
171
|
+
return file;
|
|
150
172
|
const failedChecksCannotRun = parseCannotRunChecks(parsed.failedChecksCannotRun);
|
|
151
173
|
if (!failedChecksCannotRun)
|
|
152
174
|
return null;
|
|
153
|
-
return {
|
|
175
|
+
return { ...file, failedChecksCannotRun };
|
|
154
176
|
}
|
|
155
177
|
/**
|
|
156
178
|
* Read the file. `missing` (no such file: a host that predates it, or a run
|