@yagni-app/code-staging 1.2.10-staging.2105.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
|
@@ -36,8 +36,9 @@
|
|
|
36
36
|
* failures (connection refused, missing env, DB/auth, command not found) and
|
|
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
|
-
* UNCERTAIN failure a finding carrying the output tail
|
|
40
|
-
*
|
|
39
|
+
* UNCERTAIN failure a finding carrying the output tail: it names nothing the
|
|
40
|
+
* unchanged code could already have, so it is never excused. On a build that
|
|
41
|
+
* named the repository's toolchain
|
|
41
42
|
* (`YAGNI_TOOLCHAIN_FILE`, a sandbox) both halves run on it, as the model's
|
|
42
43
|
* shell and the approved checks do ({@link buildGateVerifyEnv},
|
|
43
44
|
* {@link buildGateTestEnv}).
|
|
@@ -60,6 +61,7 @@ import { readFileSync } from "node:fs";
|
|
|
60
61
|
// and would break the walk-up termination check.
|
|
61
62
|
import { basename, dirname, join, relative } from "node:path/posix";
|
|
62
63
|
import { readToolchainFile } from "../toolchain/spawnHook.js";
|
|
64
|
+
import { attributeFailure, canonicalCheckCommand, checkId, diagKey, testKey, } from "./checkAttribution.js";
|
|
63
65
|
import { killTreeSync, trackChild } from "./childRegistry.js";
|
|
64
66
|
import { claimCovers } from "./fanout.js";
|
|
65
67
|
import { ciJobInScope } from "./missionChecks.js";
|
|
@@ -115,43 +117,13 @@ export function checkLabel(command) {
|
|
|
115
117
|
return command.length > 80 ? `${command.slice(0, 77)}...` : command;
|
|
116
118
|
}
|
|
117
119
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
120
|
+
* A command as the gate compares two (staging bench 2026-10-25: the gate's
|
|
121
|
+
* own `test` script and two approved twins ran one suite three times):
|
|
122
|
+
* `checkAttribution.ts`, the one rule the host's attempts/inputs.ts keeps
|
|
123
|
+
* too; both test files read one corpus,
|
|
124
|
+
* test/fixtures/equivalentCheckCommands.json.
|
|
121
125
|
*/
|
|
122
|
-
|
|
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
|
-
}
|
|
126
|
+
export { canonicalCheckCommand };
|
|
155
127
|
/** Git subcommands that only read: a check may run them as often as it likes. */
|
|
156
128
|
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(" "));
|
|
157
129
|
/**
|
|
@@ -781,12 +753,28 @@ function stripDuration(name) {
|
|
|
781
753
|
}
|
|
782
754
|
/** A failing-test bullet: jest `✕ name`, vitest `×/✗ name`, node spec `✖ name`. */
|
|
783
755
|
const FAIL_BULLET = /^[✕✖✗×]\s+(.+)$/;
|
|
756
|
+
/**
|
|
757
|
+
* A bullet that names no test: node:test's footer header, and ESLint's
|
|
758
|
+
* `✖ 3 problems (3 errors, 0 warnings)`, whose rows are diagnostics.
|
|
759
|
+
*/
|
|
760
|
+
const NOT_A_TEST = /^(?:failing tests:?|\d+ problems? \(.*\))$/i;
|
|
784
761
|
/** node:test spec-reporter location line printed right above the footer's ✖ line. */
|
|
785
762
|
const NODE_TEST_AT = /^test at (\S+?):(\d+)(?::\d+)?$/;
|
|
786
763
|
/** TAP failing line (node:test tap reporter): `not ok 1 - name`. */
|
|
787
764
|
const TAP_NOT_OK = /^not ok\s+\d+\s+-\s+(.+)$/;
|
|
788
765
|
/** vitest per-test failure header: ` FAIL file > suite > name` (jest's has no `>`). */
|
|
789
766
|
const RUNNER_FAIL = /^FAIL\s+(\S+)(?:\s+>\s+(.+))?/;
|
|
767
|
+
/**
|
|
768
|
+
* Other runners' failing-test lines, each naming its test: pytest's
|
|
769
|
+
* `FAILED file::name` and Pest's `FAILED Class > name` (without pytest's
|
|
770
|
+
* ` - reason`; a bare `FAILED to start` is no test), PHPUnit's
|
|
771
|
+
* `1) Class::method`, go's `--- FAIL: TestName`.
|
|
772
|
+
*/
|
|
773
|
+
const NAMED_TEST_FAILURE = [
|
|
774
|
+
/^FAILED\s+(\S+(?:::|\s+>\s+)\S.*?)(?:\s+-\s.*)?$/,
|
|
775
|
+
/^\d+\)\s+(\S+::\S+)(?:\s+with data set .*)?$/,
|
|
776
|
+
/^--- FAIL:\s+(\S+)/,
|
|
777
|
+
];
|
|
790
778
|
/** Evidence that a test RUN happened and failed, even when no per-test line parsed. */
|
|
791
779
|
const FAILURE_SUMMARY = [
|
|
792
780
|
/(?:^|\n)\s*# fail\s+[1-9]\d*\b/, // node:test TAP footer
|
|
@@ -810,7 +798,8 @@ const FAILURE_SUMMARY = [
|
|
|
810
798
|
* prerequisite ({@link classifyMissingPrerequisite}, given the `env` the
|
|
811
799
|
* command ran under) is `infra` with `needs` and the cause "needs <it>".
|
|
812
800
|
* - recognisable failing-test lines (pnpm-wrapped vitest / jest / node:test
|
|
813
|
-
* TAP + spec reporters) → `failures` with
|
|
801
|
+
* TAP + spec reporters, pytest, Pest, PHPUnit, go test) → `failures` with
|
|
802
|
+
* located `critical` findings
|
|
814
803
|
* (capped at 25, deduped, locations attached when the runner printed them).
|
|
815
804
|
* - a failure summary with no parseable per-test lines → one capped tail
|
|
816
805
|
* finding (still real evidence tests ran and failed).
|
|
@@ -854,7 +843,7 @@ export function classifyTestFailure(output, exitCode, limit = MAX_LOCATED_FINDIN
|
|
|
854
843
|
const bullet = line.match(FAIL_BULLET) ?? line.match(TAP_NOT_OK);
|
|
855
844
|
if (bullet) {
|
|
856
845
|
const name = stripDuration(bullet[1]);
|
|
857
|
-
if (name && name.length <= 200 &&
|
|
846
|
+
if (name && name.length <= 200 && !NOT_A_TEST.test(name)) {
|
|
858
847
|
parsed = { message: name };
|
|
859
848
|
if (pendingLocation) {
|
|
860
849
|
parsed.file = pendingLocation.file;
|
|
@@ -876,6 +865,12 @@ export function classifyTestFailure(output, exitCode, limit = MAX_LOCATED_FINDIN
|
|
|
876
865
|
else
|
|
877
866
|
currentFile = fail[1];
|
|
878
867
|
}
|
|
868
|
+
else {
|
|
869
|
+
const named = NAMED_TEST_FAILURE.map((re) => line.match(re)).find(Boolean);
|
|
870
|
+
const name = named ? stripDuration(named[1]) : "";
|
|
871
|
+
if (name && name.length <= 200)
|
|
872
|
+
parsed = { message: name };
|
|
873
|
+
}
|
|
879
874
|
}
|
|
880
875
|
if (!parsed)
|
|
881
876
|
continue;
|
|
@@ -1024,62 +1019,39 @@ function liftFinding(finding, repoRoot, commandCwd) {
|
|
|
1024
1019
|
const file = toRepoRelativePath(finding.file, repoRoot, commandCwd);
|
|
1025
1020
|
return file === finding.file ? finding : { ...finding, file };
|
|
1026
1021
|
}
|
|
1027
|
-
/**
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
* it never excuses a finding.
|
|
1031
|
-
*/
|
|
1032
|
-
const UNREAD_FAILURE_KEY = "\u0000unread failure";
|
|
1033
|
-
/** One command's identity across the baseline run and the build's runs: where it runs and what it runs. */
|
|
1034
|
-
export function verifyCommandKey(command) {
|
|
1035
|
-
return `${command.cwd}\u0000${command.argv.join(" ")}`;
|
|
1022
|
+
/** A detected command's id, as the base measurement and the gate both key it. */
|
|
1023
|
+
export function commandCheckId(repoRoot, command) {
|
|
1024
|
+
return checkId(command.argv.join(" "), relative(repoRoot, command.cwd) || ".");
|
|
1036
1025
|
}
|
|
1037
1026
|
/**
|
|
1038
|
-
* One
|
|
1039
|
-
* repo-relative file and
|
|
1040
|
-
* failure moves it. A
|
|
1041
|
-
*
|
|
1027
|
+
* One finding's failure key, in `checkAttribution.ts`'s shapes: a failing
|
|
1028
|
+
* test's name, or a located diagnostic's repo-relative file and message,
|
|
1029
|
+
* never its line, since a change above a failure moves it. A finding that is
|
|
1030
|
+
* only the output's tail names nothing stable: null, and never excused. Read
|
|
1031
|
+
* before the finding is prefixed with what ran. Pure.
|
|
1042
1032
|
*/
|
|
1043
|
-
export function
|
|
1033
|
+
export function findingKey(finding) {
|
|
1044
1034
|
if (finding.message.includes("Output tail:"))
|
|
1045
1035
|
return null;
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
const counts = new Map();
|
|
1051
|
-
for (const finding of findings) {
|
|
1052
|
-
const key = verifyFailureKey(finding);
|
|
1053
|
-
if (key !== null)
|
|
1054
|
-
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1055
|
-
}
|
|
1056
|
-
return counts;
|
|
1036
|
+
const test = /^verify: test failed: ([\s\S]+)$/.exec(finding.message);
|
|
1037
|
+
if (test)
|
|
1038
|
+
return testKey(test[1]);
|
|
1039
|
+
return finding.file ? diagKey(finding.file, finding.message.replace(/^verify: /, "")) : null;
|
|
1057
1040
|
}
|
|
1058
1041
|
/**
|
|
1059
|
-
* The failures this change caused
|
|
1060
|
-
*
|
|
1061
|
-
*
|
|
1062
|
-
*
|
|
1063
|
-
*
|
|
1042
|
+
* The failures this change caused, by `attributeFailure`: each failure the
|
|
1043
|
+
* unchanged code already had is excused once, so a second copy of an old
|
|
1044
|
+
* error is new, and a failure that names nothing never is. No measurement on
|
|
1045
|
+
* the base leaves every failure in place. `keys` is index for index with
|
|
1046
|
+
* `findings`. Pure.
|
|
1064
1047
|
*/
|
|
1065
|
-
export function
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
const key = verifyFailureKey(finding);
|
|
1073
|
-
const count = key === null ? 0 : (left.get(key) ?? 0);
|
|
1074
|
-
if (key !== null && count > 0) {
|
|
1075
|
-
left.set(key, count - 1);
|
|
1076
|
-
excused += 1;
|
|
1077
|
-
}
|
|
1078
|
-
else {
|
|
1079
|
-
fresh.push(finding);
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
return { findings: fresh, excused };
|
|
1048
|
+
export function attributeFindings(findings, keys, base) {
|
|
1049
|
+
const { verdict, excused } = attributeFailure({ keys }, base);
|
|
1050
|
+
return {
|
|
1051
|
+
findings: findings.filter((_, index) => !excused[index]),
|
|
1052
|
+
excused: excused.filter(Boolean).length,
|
|
1053
|
+
verdict,
|
|
1054
|
+
};
|
|
1083
1055
|
}
|
|
1084
1056
|
/** The note for failures the clean tree already had, by command label. Pure. */
|
|
1085
1057
|
export function preexistingNote(byLabel) {
|
|
@@ -1432,7 +1404,8 @@ export function makeWorkstreamCheck(opts = {}) {
|
|
|
1432
1404
|
anyRan = true;
|
|
1433
1405
|
labels.push(command.label);
|
|
1434
1406
|
if (run.code !== 0) {
|
|
1435
|
-
const
|
|
1407
|
+
const lifted = parseVerifyFailures(run.output, run.code, command.label, Number.POSITIVE_INFINITY).map((f) => liftFinding(f, cwd, command.cwd));
|
|
1408
|
+
const parsed = attributeFindings(lifted, lifted.map(findingKey), opts.onBase?.get(commandCheckId(cwd, command))).findings.slice(0, MAX_LOCATED_FINDINGS);
|
|
1436
1409
|
const covered = parsed.filter((f) => f.file != null && target.files.some((claim) => claimCovers(claim, f.file)));
|
|
1437
1410
|
findings.push(...covered.map((f) => tagFinding(f, target.name)));
|
|
1438
1411
|
if (!uncoveredEmitted.has(memoKey)) {
|
|
@@ -1455,34 +1428,36 @@ export function makeWorkstreamCheck(opts = {}) {
|
|
|
1455
1428
|
}
|
|
1456
1429
|
/**
|
|
1457
1430
|
* A failing repository command's findings, located from the folder it ran in
|
|
1458
|
-
* and prefixed with what ran
|
|
1459
|
-
*
|
|
1460
|
-
* keys the same on
|
|
1431
|
+
* and prefixed with what ran, each with its failure key ({@link findingKey},
|
|
1432
|
+
* index for index): the gate's on the build's tree, and the unchanged code's
|
|
1433
|
+
* measurement ({@link makeMeasureOnBase}), so a failure keys the same on
|
|
1434
|
+
* both. Pure.
|
|
1461
1435
|
*/
|
|
1462
|
-
function repoCommandFindings(failure, at) {
|
|
1436
|
+
export function repoCommandFindings(failure, at) {
|
|
1463
1437
|
const limit = at.limit ?? MAX_LOCATED_FINDINGS;
|
|
1464
1438
|
const cls = classifyTestFailure(failure.output, failure.code, limit, failure.env);
|
|
1465
|
-
const parsed = cls.kind === "failures"
|
|
1439
|
+
const parsed = (cls.kind === "failures"
|
|
1466
1440
|
? cls.findings
|
|
1467
|
-
: parseVerifyFailures(failure.output, failure.code, at.label, limit);
|
|
1468
|
-
return
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
}
|
|
1441
|
+
: parseVerifyFailures(failure.output, failure.code, at.label, limit)).map((f) => liftFinding(f, at.repoRoot, at.dir));
|
|
1442
|
+
return {
|
|
1443
|
+
findings: parsed.map((f) => ({ ...f, message: `${at.prefix}: ${f.message}` })),
|
|
1444
|
+
keys: parsed.map(findingKey),
|
|
1445
|
+
};
|
|
1472
1446
|
}
|
|
1473
1447
|
/** What an approved check's failure is prefixed with, on the build's tree and on the unchanged code. */
|
|
1474
1448
|
const approvedPrefix = (label) => `approved check \`${label}\``;
|
|
1449
|
+
/** An approved check's id: the command, run from the repository root. */
|
|
1450
|
+
const approvedId = (check) => checkId(check.command, ".");
|
|
1475
1451
|
/**
|
|
1476
|
-
* An approved check's standing on the unchanged code
|
|
1477
|
-
* measured there stands over what the host handed
|
|
1478
|
-
* may be stale.
|
|
1452
|
+
* An approved check's standing on the unchanged code, for its record before
|
|
1453
|
+
* it runs. What the session measured there stands over what the host handed
|
|
1454
|
+
* it: a red the record kept may be stale. A label only: whether a failure is
|
|
1455
|
+
* the change's is `attributeFindings`'s, never this.
|
|
1479
1456
|
*/
|
|
1480
1457
|
function measuredBaseline(onBase, check) {
|
|
1481
|
-
const
|
|
1482
|
-
if (
|
|
1483
|
-
return "fails_on_base";
|
|
1484
|
-
if (onBase?.passed.has(command))
|
|
1485
|
-
return "runnable";
|
|
1458
|
+
const measured = onBase?.get(approvedId(check));
|
|
1459
|
+
if (measured)
|
|
1460
|
+
return measured.outcome === "failed" ? "fails_on_base" : "runnable";
|
|
1486
1461
|
return check.status;
|
|
1487
1462
|
}
|
|
1488
1463
|
/**
|
|
@@ -1573,7 +1548,7 @@ export function makeRunVerify(opts = {}) {
|
|
|
1573
1548
|
cwd: ".",
|
|
1574
1549
|
exitCode: null,
|
|
1575
1550
|
durationMs: 0,
|
|
1576
|
-
baseline: last?.baseline ?? measuredBaseline(opts.
|
|
1551
|
+
baseline: last?.baseline ?? measuredBaseline(opts.onBase, check),
|
|
1577
1552
|
note: `${CHECK_NOT_RUN_NOTE}: ${why}${earlierRunWords(last)}`,
|
|
1578
1553
|
},
|
|
1579
1554
|
];
|
|
@@ -1584,10 +1559,10 @@ export function makeRunVerify(opts = {}) {
|
|
|
1584
1559
|
* request's CI steps its changed paths trigger. A failure becomes `critical`
|
|
1585
1560
|
* findings the fix turn works on, except where it can never clear: a check
|
|
1586
1561
|
* the sandbox cannot start, an infra failure, a path the check reads that is
|
|
1587
|
-
* not there ({@link classifyMissingInput}),
|
|
1588
|
-
*
|
|
1589
|
-
*
|
|
1590
|
-
*
|
|
1562
|
+
* not there ({@link classifyMissingInput}), the failures of an approved
|
|
1563
|
+
* check the unchanged code already had (`attributeFindings`), and a CI step
|
|
1564
|
+
* that fails exactly as it did before a fix turn (it may fail on the base
|
|
1565
|
+
* too, and the pull request's CI shows it). Those are notes.
|
|
1591
1566
|
*/
|
|
1592
1567
|
async function runRepoChecks(input) {
|
|
1593
1568
|
const { file, cwd, signal } = input;
|
|
@@ -1732,12 +1707,10 @@ export function makeRunVerify(opts = {}) {
|
|
|
1732
1707
|
/**
|
|
1733
1708
|
* One repository command's verdict: passed, a note (it could not run), or a
|
|
1734
1709
|
* failure with its output. A check that passed on the unchanged code
|
|
1735
|
-
* (`passedOnBase`, measured in this environment
|
|
1736
|
-
*
|
|
1737
|
-
*
|
|
1738
|
-
*
|
|
1739
|
-
* failed there from its host, never what passed, so there the same check
|
|
1740
|
-
* is could not run, failing open.
|
|
1710
|
+
* (`passedOnBase`, measured in this environment, by this session or an
|
|
1711
|
+
* earlier one of the attempt) and now says it lacks a credential failed:
|
|
1712
|
+
* the change made it need one, and that is the change's (`needs` says
|
|
1713
|
+
* what). With no measurement the same check is could not run, failing open.
|
|
1741
1714
|
* The stream hears the command start, and its end here unless it failed:
|
|
1742
1715
|
* a failure's caller tells its end once it decided whose the failure is.
|
|
1743
1716
|
*/
|
|
@@ -1775,10 +1748,10 @@ export function makeRunVerify(opts = {}) {
|
|
|
1775
1748
|
// A path the check reads that is not there (a log an earlier plan step
|
|
1776
1749
|
// writes, a folder named from the wrong root) gave no verdict either,
|
|
1777
1750
|
// unless the check passed on the unchanged code: then the change lost it.
|
|
1778
|
-
//
|
|
1751
|
+
// A red label on the check changes nothing: it excuses no failure.
|
|
1779
1752
|
const cause = cls.kind === "infra" && !(cls.needs && check.passedOnBase)
|
|
1780
1753
|
? cls.reason
|
|
1781
|
-
: cls.kind === "uncertain" &&
|
|
1754
|
+
: cls.kind === "uncertain" && !check.passedOnBase
|
|
1782
1755
|
? classifyMissingInput(command, output, {
|
|
1783
1756
|
repoRoot: run.cwd,
|
|
1784
1757
|
dir,
|
|
@@ -1816,15 +1789,19 @@ export function makeRunVerify(opts = {}) {
|
|
|
1816
1789
|
return { kind: "note", reason: `could not run: ${scrubSecrets(msg)}` };
|
|
1817
1790
|
}
|
|
1818
1791
|
}
|
|
1819
|
-
/** A failure's findings, located from the folder the command ran in and prefixed with what ran. */
|
|
1820
|
-
function repoFailureFindings(run, failure,
|
|
1821
|
-
return repoCommandFindings(failure, {
|
|
1792
|
+
/** A failure's findings and their keys, located from the folder the command ran in and prefixed with what ran. */
|
|
1793
|
+
function repoFailureFindings(run, failure, at) {
|
|
1794
|
+
return repoCommandFindings(failure, { ...at, repoRoot: run.cwd });
|
|
1822
1795
|
}
|
|
1823
1796
|
const findingRoom = (run) => Math.max(0, MAX_LOCATED_FINDINGS - run.findings.length);
|
|
1824
1797
|
/**
|
|
1825
1798
|
* The plan's approved checks, at the repository root as the host runs them.
|
|
1826
|
-
*
|
|
1827
|
-
*
|
|
1799
|
+
* Of a failure, only what the unchanged code did not already have is this
|
|
1800
|
+
* change's (`attributeFindings` against the check's measurement there): a
|
|
1801
|
+
* check every failure of which the base had is a note, labelled
|
|
1802
|
+
* `fails_on_base`; any other failure reads as the change's, for the host
|
|
1803
|
+
* too. A check `checks.json` calls red with no measurement here excuses
|
|
1804
|
+
* nothing.
|
|
1828
1805
|
*/
|
|
1829
1806
|
async function runApprovedChecks(run, approved) {
|
|
1830
1807
|
for (const check of approved) {
|
|
@@ -1841,14 +1818,13 @@ export function makeRunVerify(opts = {}) {
|
|
|
1841
1818
|
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`);
|
|
1842
1819
|
continue;
|
|
1843
1820
|
}
|
|
1844
|
-
//
|
|
1845
|
-
const
|
|
1846
|
-
const status = measuredBaseline(opts.checksOnBase, check);
|
|
1821
|
+
// How it went on the unchanged code, when a session measured it there.
|
|
1822
|
+
const base = opts.onBase?.get(approvedId(check));
|
|
1847
1823
|
const recorded = run.checks.length;
|
|
1848
1824
|
const verdict = await runRepoCommand(run, check.command, run.cwd, undefined, {
|
|
1849
1825
|
name: label,
|
|
1850
|
-
baseline:
|
|
1851
|
-
passedOnBase:
|
|
1826
|
+
baseline: measuredBaseline(opts.onBase, check),
|
|
1827
|
+
passedOnBase: base?.outcome === "passed",
|
|
1852
1828
|
});
|
|
1853
1829
|
// `runRepoCommand` records every run once, whatever its verdict.
|
|
1854
1830
|
const ran = run.checks[recorded];
|
|
@@ -1864,32 +1840,28 @@ export function makeRunVerify(opts = {}) {
|
|
|
1864
1840
|
if (verdict.kind === "note") {
|
|
1865
1841
|
run.reasons.push(`approved check \`${label}\` ${verdict.reason}`);
|
|
1866
1842
|
}
|
|
1867
|
-
else if (verdict.kind === "failed"
|
|
1868
|
-
//
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1843
|
+
else if (verdict.kind === "failed") {
|
|
1844
|
+
// Every failure read, so none past the findings' cap goes unweighed.
|
|
1845
|
+
const found = repoFailureFindings(run, verdict, {
|
|
1846
|
+
label,
|
|
1847
|
+
prefix: approvedPrefix(label),
|
|
1848
|
+
dir: run.cwd,
|
|
1849
|
+
limit: Number.POSITIVE_INFINITY,
|
|
1850
|
+
});
|
|
1851
|
+
const fresh = attributeFindings(found.findings, found.keys, base);
|
|
1852
|
+
// Its record says how it stands, for the host too: a check with a
|
|
1853
|
+
// failure of its own, or none to compare with, is the change's.
|
|
1854
|
+
verdict.record.baseline = fresh.verdict === "preexisting" ? "fails_on_base" : "runnable";
|
|
1855
|
+
if (fresh.verdict === "preexisting") {
|
|
1875
1856
|
run.reasons.push(`approved check \`${label}\` failed as it did on the unchanged code before the build; not this change's`);
|
|
1876
1857
|
}
|
|
1877
1858
|
else {
|
|
1878
|
-
// Its record reads as the change's failure, for the host too.
|
|
1879
|
-
verdict.record.baseline = "runnable";
|
|
1880
1859
|
run.labels.push(label);
|
|
1881
|
-
run.findings.push(...fresh.slice(0, findingRoom(run)));
|
|
1860
|
+
run.findings.push(...fresh.findings.slice(0, findingRoom(run)));
|
|
1882
1861
|
}
|
|
1883
1862
|
}
|
|
1884
|
-
else if (verdict.kind === "failed" && status === "fails_on_base") {
|
|
1885
|
-
run.reasons.push(`approved check \`${label}\` failed, and it already failed on the unchanged code; the host compares the two after the build`);
|
|
1886
|
-
}
|
|
1887
1863
|
else {
|
|
1888
1864
|
run.labels.push(label);
|
|
1889
|
-
if (verdict.kind === "failed") {
|
|
1890
|
-
const found = repoFailureFindings(run, verdict, label, approvedPrefix(label), run.cwd);
|
|
1891
|
-
run.findings.push(...found.slice(0, findingRoom(run)));
|
|
1892
|
-
}
|
|
1893
1865
|
}
|
|
1894
1866
|
if (verdict.kind === "failed")
|
|
1895
1867
|
opts.onCheck?.(checkEnded(verdict.record, verdict.output));
|
|
@@ -1968,7 +1940,11 @@ export function makeRunVerify(opts = {}) {
|
|
|
1968
1940
|
}
|
|
1969
1941
|
settle(job, "failed");
|
|
1970
1942
|
opts.onCheck?.(checkEnded(verdict.record, verdict.output));
|
|
1971
|
-
const stepFindings = repoFailureFindings(run, verdict,
|
|
1943
|
+
const stepFindings = repoFailureFindings(run, verdict, {
|
|
1944
|
+
label: step.name,
|
|
1945
|
+
prefix: where,
|
|
1946
|
+
dir,
|
|
1947
|
+
}).findings;
|
|
1972
1948
|
const keys = new Set(stepFindings.map((f) => f.message));
|
|
1973
1949
|
const previous = lastCiFailure.get(key);
|
|
1974
1950
|
lastCiFailure.set(key, keys);
|
|
@@ -2012,10 +1988,17 @@ export function makeRunVerify(opts = {}) {
|
|
|
2012
1988
|
preexisting.set(label, (preexisting.get(label) ?? 0) + n);
|
|
2013
1989
|
};
|
|
2014
1990
|
const checks = [];
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
1991
|
+
/**
|
|
1992
|
+
* A detected command's failures this change caused, against its
|
|
1993
|
+
* measurement on the unchanged code; its record's label is
|
|
1994
|
+
* `fails_on_base` only when the base had every one of them.
|
|
1995
|
+
*/
|
|
1996
|
+
const judged = (command, failed) => {
|
|
1997
|
+
const lifted = failed.map((f) => liftFinding(f, cwd, command.cwd));
|
|
1998
|
+
const fresh = attributeFindings(lifted, lifted.map(findingKey), opts.onBase?.get(commandCheckId(cwd, command)));
|
|
1999
|
+
countPreexisting(command.label, fresh.excused);
|
|
2000
|
+
const baseline = fresh.verdict === "preexisting" ? "fails_on_base" : "runnable";
|
|
2001
|
+
return { findings: fresh.findings, baseline };
|
|
2019
2002
|
};
|
|
2020
2003
|
/** A detected command starting, as the stream hears it. */
|
|
2021
2004
|
const commandStarted = (command) => opts.onCheck?.({
|
|
@@ -2066,18 +2049,17 @@ export function makeRunVerify(opts = {}) {
|
|
|
2066
2049
|
continue;
|
|
2067
2050
|
}
|
|
2068
2051
|
}
|
|
2069
|
-
|
|
2052
|
+
const fresh = code === 0
|
|
2053
|
+
? null
|
|
2054
|
+
: judged(command, parseVerifyFailures(output, code, command.label, Number.POSITIVE_INFINITY));
|
|
2055
|
+
recordCommand(command, code, startedAt, fresh?.baseline ?? "runnable", { output });
|
|
2070
2056
|
anyRan = true;
|
|
2071
2057
|
typecheckLabels.push(command.label);
|
|
2072
|
-
if (
|
|
2073
|
-
const parsed = parseVerifyFailures(output, code, command.label, Number.POSITIVE_INFINITY).map((f) => liftFinding(f, cwd, command.cwd));
|
|
2074
|
-
const fresh = subtractBaseline(parsed, opts.baseline?.get(verifyCommandKey(command)));
|
|
2075
|
-
countPreexisting(command.label, fresh.excused);
|
|
2058
|
+
if (fresh)
|
|
2076
2059
|
findings.push(...fresh.findings.slice(0, MAX_LOCATED_FINDINGS));
|
|
2077
|
-
}
|
|
2078
2060
|
}
|
|
2079
2061
|
catch (err) {
|
|
2080
|
-
recordCommand(command, null, startedAt,
|
|
2062
|
+
recordCommand(command, null, startedAt, "runnable", {
|
|
2081
2063
|
note: unexitedNote(err, signal, composed),
|
|
2082
2064
|
});
|
|
2083
2065
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -2129,28 +2111,19 @@ export function makeRunVerify(opts = {}) {
|
|
|
2129
2111
|
reasons.push(`tests (${where}) could not run: ${cls.reason}; 'clean' reflects typecheck + review only`);
|
|
2130
2112
|
continue;
|
|
2131
2113
|
}
|
|
2132
|
-
const base = standing(command);
|
|
2133
2114
|
if (cls.kind === "uncertain" && opts.unreadTestFailureIsFinding) {
|
|
2134
2115
|
anyRan = true;
|
|
2135
2116
|
testLabels.push(command.label);
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
reasons.push(`tests (${where}) failed on the unchanged code too, so not a finding: ${summary}`);
|
|
2143
|
-
}
|
|
2144
|
-
else {
|
|
2145
|
-
// One finding per command, past the cap too: it is the only
|
|
2146
|
-
// word of this command's failure.
|
|
2147
|
-
recordCommand(command, code, startedAt, base, { output });
|
|
2148
|
-
testFindingCount += 1;
|
|
2149
|
-
findings.push(tailFinding(command.label, code, output));
|
|
2150
|
-
}
|
|
2117
|
+
// One finding per command, past the cap too: it is the only
|
|
2118
|
+
// word of this command's failure, and it names nothing the
|
|
2119
|
+
// unchanged code could already have, so it is never excused.
|
|
2120
|
+
recordCommand(command, code, startedAt, "runnable", { output });
|
|
2121
|
+
testFindingCount += 1;
|
|
2122
|
+
findings.push(tailFinding(command.label, code, output));
|
|
2151
2123
|
continue;
|
|
2152
2124
|
}
|
|
2153
|
-
|
|
2125
|
+
const fresh = cls.kind === "failures" ? judged(command, cls.findings) : null;
|
|
2126
|
+
recordCommand(command, code, startedAt, fresh?.baseline ?? "runnable", { output });
|
|
2154
2127
|
if (cls.kind === "uncertain") {
|
|
2155
2128
|
// Failing OPEN on an uninterpretable failure (§3d): an honest note,
|
|
2156
2129
|
// never a guessed finding the fix stage cannot clear.
|
|
@@ -2159,17 +2132,15 @@ export function makeRunVerify(opts = {}) {
|
|
|
2159
2132
|
}
|
|
2160
2133
|
anyRan = true;
|
|
2161
2134
|
testLabels.push(command.label);
|
|
2162
|
-
if (
|
|
2135
|
+
if (fresh) {
|
|
2163
2136
|
const room = Math.max(0, MAX_LOCATED_FINDINGS - testFindingCount);
|
|
2164
|
-
const fresh = subtractBaseline(cls.findings.map((f) => liftFinding(f, cwd, command.cwd)), opts.baseline?.get(verifyCommandKey(command)));
|
|
2165
|
-
countPreexisting(command.label, fresh.excused);
|
|
2166
2137
|
const take = fresh.findings.slice(0, room);
|
|
2167
2138
|
testFindingCount += take.length;
|
|
2168
2139
|
findings.push(...take);
|
|
2169
2140
|
}
|
|
2170
2141
|
}
|
|
2171
2142
|
catch (err) {
|
|
2172
|
-
recordCommand(command, null, startedAt,
|
|
2143
|
+
recordCommand(command, null, startedAt, "runnable", {
|
|
2173
2144
|
note: unexitedNote(err, signal, composed),
|
|
2174
2145
|
});
|
|
2175
2146
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -2259,242 +2230,192 @@ export function makeRunVerify(opts = {}) {
|
|
|
2259
2230
|
};
|
|
2260
2231
|
};
|
|
2261
2232
|
}
|
|
2262
|
-
/** The whole
|
|
2263
|
-
export const
|
|
2233
|
+
/** The whole base measurement's cap, before the build starts: it costs minutes at most. */
|
|
2234
|
+
export const BASE_MEASUREMENT_BUDGET_MS = 10 * 60_000;
|
|
2264
2235
|
/**
|
|
2265
|
-
*
|
|
2266
|
-
*
|
|
2267
|
-
*
|
|
2268
|
-
* then
|
|
2269
|
-
*
|
|
2270
|
-
*
|
|
2271
|
-
*
|
|
2272
|
-
*
|
|
2273
|
-
* has no baseline, and its failures all count, as before. What the commands
|
|
2274
|
-
* write into the tree is undone, so none of it reaches the candidate.
|
|
2236
|
+
* What the gate will judge, in the order it is measured: the approved
|
|
2237
|
+
* checks `checks.json` marks `measureOnBase` (every one it can run with
|
|
2238
|
+
* `allChecks`, a resumed session's rule: the record's green is what went
|
|
2239
|
+
* stale), then the gate's own typecheck and test commands in `dirs`, the
|
|
2240
|
+
* packages the build will change. Each once, by id; a check that changes the
|
|
2241
|
+
* checkout's git state or that the sandbox cannot run is never one. Read on
|
|
2242
|
+
* the tree as it is: the gate detects its commands on the build's tree, so
|
|
2243
|
+
* these are the ids it will look up.
|
|
2275
2244
|
*/
|
|
2276
|
-
export function
|
|
2277
|
-
const
|
|
2278
|
-
const
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
return result;
|
|
2294
|
-
const startedAt = now();
|
|
2295
|
-
const budgetMs = Math.min(opts.budgetMs ?? VERIFY_BASELINE_BUDGET_MS, budgetOverrideMs ?? Number.POSITIVE_INFINITY);
|
|
2296
|
-
const before = await treeStatus(cwd, signal).catch((err) => {
|
|
2297
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2298
|
-
result.notes.push(`could not read the tree before the baseline, so what it writes is not undone: ${scrubSecrets(msg)}`);
|
|
2299
|
-
return null;
|
|
2245
|
+
export function baseTargets(cwd, input, opts = {}) {
|
|
2246
|
+
const targets = new Map();
|
|
2247
|
+
for (const check of input.checks?.checks ?? []) {
|
|
2248
|
+
if (!input.allChecks && check.measureOnBase !== true)
|
|
2249
|
+
continue;
|
|
2250
|
+
if (check.status === "cannot_run" || changesGitState(check.command))
|
|
2251
|
+
continue;
|
|
2252
|
+
const id = approvedId(check);
|
|
2253
|
+
if (targets.has(id))
|
|
2254
|
+
continue;
|
|
2255
|
+
targets.set(id, {
|
|
2256
|
+
id,
|
|
2257
|
+
label: checkLabel(check.command),
|
|
2258
|
+
kind: "check",
|
|
2259
|
+
command: check.command,
|
|
2260
|
+
argv: repoCheckArgv(check.command),
|
|
2261
|
+
dir: cwd,
|
|
2300
2262
|
});
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
const
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
for (const
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
const commandStartedAt = now();
|
|
2314
|
-
const left = budgetMs - (commandStartedAt - startedAt);
|
|
2315
|
-
if (left < REPO_CHECK_MIN_START_MS) {
|
|
2316
|
-
result.notes.push(`${command.label} (${command.cwd}): not reached in the baseline budget`);
|
|
2263
|
+
}
|
|
2264
|
+
if (input.dirs.length > 0) {
|
|
2265
|
+
const commands = (opts.detect ?? defaultDetect)(cwd, input.dirs);
|
|
2266
|
+
const overridden = commands.some((c) => c.source === "override");
|
|
2267
|
+
const tests = overridden ? [] : (opts.detectTests ?? defaultDetectTests)(cwd, input.dirs);
|
|
2268
|
+
for (const [kind, list] of [
|
|
2269
|
+
["typecheck", commands],
|
|
2270
|
+
["test", tests],
|
|
2271
|
+
]) {
|
|
2272
|
+
for (const command of list) {
|
|
2273
|
+
const id = commandCheckId(cwd, command);
|
|
2274
|
+
if (targets.has(id))
|
|
2317
2275
|
continue;
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
const composed = composeAbortSignal(signal, AbortSignal.timeout(cap));
|
|
2323
|
-
const heard = {
|
|
2324
|
-
name: command.label,
|
|
2276
|
+
targets.set(id, {
|
|
2277
|
+
id,
|
|
2278
|
+
label: command.label,
|
|
2279
|
+
kind,
|
|
2325
2280
|
command: command.argv.join(" "),
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
};
|
|
2329
|
-
opts.onCheck?.({ phase: "start", ...heard });
|
|
2330
|
-
const ended = (exitCode, output) => opts.onCheck?.({
|
|
2331
|
-
phase: "end",
|
|
2332
|
-
...heard,
|
|
2333
|
-
exitCode,
|
|
2334
|
-
durationMs: now() - commandStartedAt,
|
|
2335
|
-
...(output && exitCode !== 0 ? { tail: checkTail(output) } : {}),
|
|
2281
|
+
argv: command.argv,
|
|
2282
|
+
dir: command.cwd,
|
|
2336
2283
|
});
|
|
2337
|
-
try {
|
|
2338
|
-
const { code, output: rawOutput } = await exec(command.argv, command.cwd, composed, test ? testEnv : verifyEnv);
|
|
2339
|
-
ended(code, rawOutput);
|
|
2340
|
-
const output = scrubSecrets(rawOutput);
|
|
2341
|
-
let failed;
|
|
2342
|
-
if (test) {
|
|
2343
|
-
const cls = classifyTestFailure(output, code, Number.POSITIVE_INFINITY, testEnv);
|
|
2344
|
-
if (cls.kind === "infra" || cls.kind === "uncertain") {
|
|
2345
|
-
result.notes.push(`${command.label} (${command.cwd}): ${cls.kind === "infra" ? cls.reason : "output not interpretable"}`);
|
|
2346
|
-
if (cls.kind === "uncertain") {
|
|
2347
|
-
baseline.set(verifyCommandKey(command), new Map([[UNREAD_FAILURE_KEY, 1]]));
|
|
2348
|
-
}
|
|
2349
|
-
continue;
|
|
2350
|
-
}
|
|
2351
|
-
failed = cls.kind === "failures" ? cls.findings : [];
|
|
2352
|
-
}
|
|
2353
|
-
else {
|
|
2354
|
-
const infra = code === 0 ? null : classifyVerifyInfraFailure(output, code);
|
|
2355
|
-
if (infra) {
|
|
2356
|
-
result.notes.push(`${command.label} (${command.cwd}): ${infra}`);
|
|
2357
|
-
continue;
|
|
2358
|
-
}
|
|
2359
|
-
failed = parseVerifyFailures(output, code, command.label, Number.POSITIVE_INFINITY);
|
|
2360
|
-
}
|
|
2361
|
-
const counts = failureKeyCounts(failed.map((f) => liftFinding(f, cwd, command.cwd)));
|
|
2362
|
-
baseline.set(verifyCommandKey(command), counts);
|
|
2363
|
-
result.ran.push(command.label);
|
|
2364
|
-
for (const n of counts.values())
|
|
2365
|
-
result.failures += n;
|
|
2366
|
-
}
|
|
2367
|
-
catch (err) {
|
|
2368
|
-
ended(null);
|
|
2369
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2370
|
-
result.notes.push(`${command.label} (${command.cwd}): could not run: ${scrubSecrets(msg)}`);
|
|
2371
|
-
}
|
|
2372
|
-
}
|
|
2373
|
-
}
|
|
2374
|
-
finally {
|
|
2375
|
-
if (before !== null) {
|
|
2376
|
-
try {
|
|
2377
|
-
const written = newTreeEntries(before, await treeStatus(cwd));
|
|
2378
|
-
if (written.length > 0)
|
|
2379
|
-
await undoTreeWrites(cwd, written);
|
|
2380
|
-
}
|
|
2381
|
-
catch (err) {
|
|
2382
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2383
|
-
result.notes.push(`could not undo what the baseline wrote into the tree: ${scrubSecrets(msg)}`);
|
|
2384
|
-
}
|
|
2385
2284
|
}
|
|
2386
2285
|
}
|
|
2387
|
-
|
|
2388
|
-
|
|
2286
|
+
}
|
|
2287
|
+
return [...targets.values()];
|
|
2389
2288
|
}
|
|
2390
2289
|
/**
|
|
2391
|
-
*
|
|
2392
|
-
*
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
/**
|
|
2396
|
-
* Run each approved check `checks.json` marks `measureOnBase` once on the
|
|
2397
|
-
* unchanged code, before the build changes anything: a plan's check the
|
|
2398
|
-
* host has not seen pass there (none of the record's, or a red one the
|
|
2399
|
-
* record may hold from long ago), and a command the repository's CI runs
|
|
2400
|
-
* when the record names no check of its own. A check the plan wrote for the
|
|
2401
|
-
* change often fails there by design (its test file is not written yet), and a
|
|
2402
|
-
* failure that says only that (a file not found, no tests to run, output no
|
|
2403
|
-
* parser reads) is no verdict: its failures all count after the build, as
|
|
2404
|
-
* before. A failure the gate reads (failing tests, located diagnostics, a
|
|
2405
|
-
* test runner's failure summary) is one the unchanged code already has.
|
|
2290
|
+
* Run each target ({@link baseTargets}) once on the unchanged code, before
|
|
2291
|
+
* the build changes anything (YAG-1003), and keep what it did: passed, or
|
|
2292
|
+
* failed with its failure keys ({@link findingKey}). The build's gate then
|
|
2293
|
+
* counts only the failures the change added ({@link attributeFindings}).
|
|
2406
2294
|
*
|
|
2407
|
-
*
|
|
2408
|
-
*
|
|
2409
|
-
*
|
|
2410
|
-
*
|
|
2411
|
-
*
|
|
2295
|
+
* Each runs as the gate runs it, with the same command line, environment
|
|
2296
|
+
* and toolchain, so a failure keys the same on both trees. A run that says
|
|
2297
|
+
* only that it could not run (an infra failure, a path it reads not there,
|
|
2298
|
+
* a command not found) gives no verdict, and its failures all count after
|
|
2299
|
+
* the build. A test run or check whose output no parser reads failed there
|
|
2300
|
+
* with no keys: nothing after the build is excused by it. Bounded: each by
|
|
2301
|
+
* its cap, the whole by the budget, and a target not reached in it gives no
|
|
2302
|
+
* verdict. What the commands write into the tree is undone.
|
|
2412
2303
|
*/
|
|
2413
|
-
export function
|
|
2304
|
+
export function makeMeasureOnBase(opts = {}) {
|
|
2414
2305
|
const exec = opts.exec ?? defaultExec;
|
|
2415
2306
|
const now = opts.now ?? Date.now;
|
|
2416
|
-
const toolchain = opts.toolchain ?? sessionToolchain;
|
|
2417
2307
|
const treeStatus = opts.treeStatus ??
|
|
2418
2308
|
(async (cwd, signal) => (await snapshotWorkspace(cwd, signal)).status);
|
|
2419
2309
|
const undoTreeWrites = opts.undoTreeWrites ?? defaultUndoTreeWrites;
|
|
2420
|
-
return async function
|
|
2421
|
-
const
|
|
2422
|
-
const
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2310
|
+
return async function measureOnBase(cwd, targets, signal, budgetOverrideMs) {
|
|
2311
|
+
const measured = new Map();
|
|
2312
|
+
const result = {
|
|
2313
|
+
measured,
|
|
2314
|
+
ran: [],
|
|
2315
|
+
failures: 0,
|
|
2316
|
+
notes: [],
|
|
2317
|
+
treeUndone: true,
|
|
2318
|
+
};
|
|
2427
2319
|
if (targets.length === 0)
|
|
2428
2320
|
return result;
|
|
2429
2321
|
const startedAt = now();
|
|
2430
|
-
const budgetMs = Math.min(opts.budgetMs ??
|
|
2431
|
-
|
|
2322
|
+
const budgetMs = Math.min(opts.budgetMs ?? BASE_MEASUREMENT_BUDGET_MS, budgetOverrideMs ?? Number.POSITIVE_INFINITY);
|
|
2323
|
+
// The gate's environments, so a failure keys the same on both trees.
|
|
2324
|
+
const toolchainFile = (opts.toolchain ?? sessionToolchain)();
|
|
2325
|
+
const envs = {
|
|
2326
|
+
check: opts.repoChecksEnv ?? buildRepoCheckEnv(process.env, toolchainFile),
|
|
2327
|
+
typecheck: buildGateVerifyEnv(process.env, toolchainFile),
|
|
2328
|
+
test: opts.testEnv ?? buildGateTestEnv(process.env, toolchainFile),
|
|
2329
|
+
};
|
|
2330
|
+
const caps = {
|
|
2331
|
+
check: opts.repoCheckTimeoutMs ?? REPO_CHECK_TIMEOUT_MS,
|
|
2332
|
+
typecheck: opts.timeoutMs ?? VERIFY_TIMEOUT_MS,
|
|
2333
|
+
test: opts.testTimeoutMs ?? VERIFY_TEST_TIMEOUT_MS,
|
|
2334
|
+
};
|
|
2432
2335
|
const before = await treeStatus(cwd, signal).catch((err) => {
|
|
2433
2336
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2434
2337
|
result.treeUndone = false;
|
|
2435
|
-
result.notes.push(`could not read the tree before the
|
|
2338
|
+
result.notes.push(`could not read the tree before the base measurement, so what it writes is not undone: ${scrubSecrets(msg)}`);
|
|
2436
2339
|
return null;
|
|
2437
2340
|
});
|
|
2341
|
+
/** The keys of a failure, or why it gave no verdict. */
|
|
2342
|
+
const failureOf = (target, output, code) => {
|
|
2343
|
+
const env = envs[target.kind];
|
|
2344
|
+
if (target.kind === "typecheck") {
|
|
2345
|
+
const infra = classifyVerifyInfraFailure(output, code);
|
|
2346
|
+
if (infra)
|
|
2347
|
+
return { note: infra };
|
|
2348
|
+
const failed = parseVerifyFailures(output, code, target.label, Number.POSITIVE_INFINITY);
|
|
2349
|
+
return { keys: keysOf(failed.map((f) => liftFinding(f, cwd, target.dir))) };
|
|
2350
|
+
}
|
|
2351
|
+
const cls = classifyTestFailure(output, code, Number.POSITIVE_INFINITY, env);
|
|
2352
|
+
if (cls.kind === "infra")
|
|
2353
|
+
return { note: cls.reason };
|
|
2354
|
+
if (target.kind === "test") {
|
|
2355
|
+
return {
|
|
2356
|
+
keys: cls.kind === "failures"
|
|
2357
|
+
? keysOf(cls.findings.map((f) => liftFinding(f, cwd, target.dir)))
|
|
2358
|
+
: [],
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
const missing = cls.kind === "uncertain"
|
|
2362
|
+
? classifyMissingInput(target.command, output, { repoRoot: cwd, dir: cwd })
|
|
2363
|
+
: null;
|
|
2364
|
+
if (missing)
|
|
2365
|
+
return { note: `could not run: ${missing}` };
|
|
2366
|
+
const found = repoCommandFindings({ output, code, env }, {
|
|
2367
|
+
label: target.label,
|
|
2368
|
+
prefix: approvedPrefix(target.label),
|
|
2369
|
+
repoRoot: cwd,
|
|
2370
|
+
dir: cwd,
|
|
2371
|
+
limit: Number.POSITIVE_INFINITY,
|
|
2372
|
+
});
|
|
2373
|
+
return { keys: found.keys.filter((key) => key !== null) };
|
|
2374
|
+
};
|
|
2438
2375
|
try {
|
|
2439
|
-
for (const
|
|
2376
|
+
for (const target of targets) {
|
|
2440
2377
|
if (signal?.aborted)
|
|
2441
2378
|
break;
|
|
2442
|
-
const
|
|
2443
|
-
const
|
|
2444
|
-
const
|
|
2445
|
-
const left = budgetMs - (checkStartedAt - startedAt);
|
|
2379
|
+
const targetStartedAt = now();
|
|
2380
|
+
const left = budgetMs - (targetStartedAt - startedAt);
|
|
2381
|
+
const where = relative(cwd, target.dir) || ".";
|
|
2446
2382
|
if (left < REPO_CHECK_MIN_START_MS) {
|
|
2447
|
-
result.notes.push(`\`${label}
|
|
2383
|
+
result.notes.push(`\`${target.label}\` (${where}): not reached in the budget`);
|
|
2448
2384
|
continue;
|
|
2449
2385
|
}
|
|
2450
|
-
const composed = composeAbortSignal(signal, AbortSignal.timeout(Math.min(
|
|
2451
|
-
const heard = {
|
|
2386
|
+
const composed = composeAbortSignal(signal, AbortSignal.timeout(Math.min(caps[target.kind], left)));
|
|
2387
|
+
const heard = {
|
|
2388
|
+
name: target.label,
|
|
2389
|
+
command: target.command,
|
|
2390
|
+
cwd: where,
|
|
2391
|
+
base: true,
|
|
2392
|
+
};
|
|
2452
2393
|
opts.onCheck?.({ phase: "start", ...heard });
|
|
2453
2394
|
const ended = (exitCode, output) => opts.onCheck?.({
|
|
2454
2395
|
phase: "end",
|
|
2455
2396
|
...heard,
|
|
2456
2397
|
exitCode,
|
|
2457
|
-
durationMs: now() -
|
|
2398
|
+
durationMs: now() - targetStartedAt,
|
|
2458
2399
|
...(output && exitCode !== 0 ? { tail: checkTail(output) } : {}),
|
|
2459
2400
|
});
|
|
2460
2401
|
try {
|
|
2461
|
-
const { code, output: rawOutput } = await exec(
|
|
2402
|
+
const { code, output: rawOutput } = await exec(target.argv, target.dir, composed, envs[target.kind]);
|
|
2462
2403
|
ended(code, rawOutput);
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
result.
|
|
2466
|
-
continue;
|
|
2467
|
-
}
|
|
2468
|
-
const output = scrubSecrets(rawOutput);
|
|
2469
|
-
const cls = classifyTestFailure(output, code, Number.POSITIVE_INFINITY, env);
|
|
2470
|
-
const cause = cls.kind === "infra"
|
|
2471
|
-
? cls.reason
|
|
2472
|
-
: cls.kind === "uncertain"
|
|
2473
|
-
? classifyMissingInput(check.command, output, { repoRoot: cwd, dir: cwd })
|
|
2474
|
-
: null;
|
|
2475
|
-
if (cause) {
|
|
2476
|
-
result.notes.push(`\`${label}\`: could not run: ${cause}`);
|
|
2404
|
+
const failure = code === 0 ? { keys: [] } : failureOf(target, scrubSecrets(rawOutput), code);
|
|
2405
|
+
if ("note" in failure) {
|
|
2406
|
+
result.notes.push(`\`${target.label}\` (${where}): ${failure.note}`);
|
|
2477
2407
|
continue;
|
|
2478
2408
|
}
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
repoRoot: cwd,
|
|
2483
|
-
dir: cwd,
|
|
2484
|
-
limit: Number.POSITIVE_INFINITY,
|
|
2409
|
+
measured.set(target.id, {
|
|
2410
|
+
outcome: code === 0 ? "passed" : "failed",
|
|
2411
|
+
keys: failure.keys,
|
|
2485
2412
|
});
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
if (cls.kind === "uncertain" && found.every((f) => verifyFailureKey(f) === null)) {
|
|
2489
|
-
result.notes.push(`\`${label}\`: exited ${code} with output no parser reads`);
|
|
2490
|
-
continue;
|
|
2491
|
-
}
|
|
2492
|
-
failed.set(command, failureKeyCounts(found));
|
|
2493
|
-
result.ran.push(label);
|
|
2413
|
+
result.ran.push(target.label);
|
|
2414
|
+
result.failures += failure.keys.length;
|
|
2494
2415
|
}
|
|
2495
2416
|
catch (err) {
|
|
2496
2417
|
ended(null);
|
|
2497
|
-
result.notes.push(`\`${label}
|
|
2418
|
+
result.notes.push(`\`${target.label}\` (${where}): ${unexitedNote(err, signal, composed)}`);
|
|
2498
2419
|
}
|
|
2499
2420
|
}
|
|
2500
2421
|
}
|
|
@@ -2508,11 +2429,15 @@ export function makeMeasureChecksOnBase(opts = {}) {
|
|
|
2508
2429
|
catch (err) {
|
|
2509
2430
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2510
2431
|
result.treeUndone = false;
|
|
2511
|
-
result.notes.push(`could not undo what the
|
|
2432
|
+
result.notes.push(`could not undo what the base measurement wrote into the tree: ${scrubSecrets(msg)}`);
|
|
2512
2433
|
}
|
|
2513
2434
|
}
|
|
2514
2435
|
}
|
|
2515
2436
|
return result;
|
|
2516
2437
|
};
|
|
2517
2438
|
}
|
|
2439
|
+
/** The failure keys of findings that name what failed. */
|
|
2440
|
+
function keysOf(findings) {
|
|
2441
|
+
return findings.map(findingKey).filter((key) => key !== null);
|
|
2442
|
+
}
|
|
2518
2443
|
//# sourceMappingURL=verify.js.map
|