@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.
@@ -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, unless the clean tree
40
- * failed the same command. On a build that named the repository's toolchain
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
- * 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.
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
- 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
- }
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 located `critical` findings
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 && !/^failing tests:?$/i.test(name)) {
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
- * A test command's baseline entry when the clean tree failed it with output
1029
- * no parser reads: it already failed there. It matches no failure's key, so
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 failure's key, the shape #2601's `failureKeyOf` gives a CI failure: the
1039
- * repo-relative file and the message, never the line, since a change above a
1040
- * failure moves it. A failure that is only the output's tail names nothing
1041
- * stable, so it has no key and is never excused. Pure.
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 verifyFailureKey(finding) {
1033
+ export function findingKey(finding) {
1044
1034
  if (finding.message.includes("Output tail:"))
1045
1035
  return null;
1046
- return `${finding.file ?? ""}\u0000${finding.message}`;
1047
- }
1048
- /** How many times each failure key occurs. Pure. */
1049
- export function failureKeyCounts(findings) {
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: each failure key is excused at most as many
1060
- * times as the clean tree had it, so a second copy of an old error is new. A
1061
- * failure in a file the build did not touch still counts when the baseline did
1062
- * not have it, since a change can break another file's types. No baseline for
1063
- * the command leaves every failure in place. Pure.
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 subtractBaseline(findings, baseline) {
1066
- if (!baseline)
1067
- return { findings: [...findings], excused: 0 };
1068
- const left = new Map(baseline);
1069
- const fresh = [];
1070
- let excused = 0;
1071
- for (const finding of findings) {
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 parsed = subtractBaseline(parseVerifyFailures(run.output, run.code, command.label, Number.POSITIVE_INFINITY).map((f) => liftFinding(f, cwd, command.cwd)), opts.baseline?.get(verifyCommandKey(command))).findings.slice(0, MAX_LOCATED_FINDINGS);
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: the gate's on the build's tree, and the
1459
- * unchanged code's measurement ({@link makeMeasureChecksOnBase}), so a failure
1460
- * keys the same on both. Pure.
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 parsed.map((f) => ({
1469
- ...liftFinding(f, at.repoRoot, at.dir),
1470
- message: `${at.prefix}: ${f.message}`,
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. What the session
1477
- * measured there stands over what the host handed it: a red the record kept
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 command = check.command.trim();
1482
- if (onBase?.failed.has(command))
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.checksOnBase, check),
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}), an approved check that already
1588
- * failed on the unchanged code (the host compares it after the build), and a
1589
- * CI step that fails exactly as it did before a fix turn (it may fail on the
1590
- * base too, and the pull request's CI shows it). Those are notes.
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) and now says it lacks a
1736
- * credential failed: the change made it need one, and that is the change's
1737
- * (`needs` says what). Only a session that measured knows it passed: a
1738
- * resumed session that could not measure on the attempt's base gets what
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
- // One already failing on the base is a note as it is, its record kept.
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" && check.baseline === "runnable" && !check.passedOnBase
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, label, prefix, dir) {
1821
- return repoCommandFindings(failure, { label, prefix, repoRoot: run.cwd, dir });
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
- * One that already failed on the unchanged code is a note: the host compares
1827
- * the two after the build.
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
- // The failures it had there, when the session measured it failing.
1845
- const failedOnBase = opts.checksOnBase?.failed.get(check.command.trim());
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: status,
1851
- passedOnBase: opts.checksOnBase?.passed.has(check.command.trim()) ?? false,
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" && failedOnBase) {
1868
- // Only what the unchanged code did not already fail is this change's.
1869
- // A failure no parser reads is excused only when the unchanged code's
1870
- // was unread too (a summary line, as PHPUnit prints); after readable
1871
- // failures there, it says the change broke the run itself.
1872
- const found = repoFailureFindings(run, verdict, label, approvedPrefix(label), run.cwd);
1873
- const fresh = subtractBaseline(found, failedOnBase).findings.filter((finding) => failedOnBase.size > 0 || verifyFailureKey(finding) !== null);
1874
- if (fresh.length === 0) {
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, step.name, where, dir);
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
- // A detected command's standing on the clean tree, from the baseline run.
2016
- const standing = (command) => {
2017
- const counts = opts.baseline?.get(verifyCommandKey(command));
2018
- return counts && [...counts.values()].some((n) => n > 0) ? "fails_on_base" : "runnable";
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
- recordCommand(command, code, startedAt, standing(command), { output });
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 (code !== 0) {
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, standing(command), {
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
- if (base === "fails_on_base") {
2137
- const summary = exitSummary(command.label, code, output);
2138
- recordCommand(command, code, startedAt, base, {
2139
- note: `failed on the unchanged code too: ${summary}`,
2140
- output,
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
- recordCommand(command, code, startedAt, base, { output });
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 (cls.kind === "failures") {
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, standing(command), {
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 baseline run's cap, before the build starts: it costs minutes at most. */
2263
- export const VERIFY_BASELINE_BUDGET_MS = 10 * 60_000;
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
- * Run the gate's own typecheck and test commands ONCE on the clean tree,
2266
- * before the build changes anything (YAG-1003), in the package dirs the
2267
- * approved plan names, and keep each command's failure keys. The build's gate
2268
- * then counts only the failures this change added ({@link subtractBaseline}).
2269
- *
2270
- * The same detection, exec seam and environments as {@link makeRunVerify}, so
2271
- * a command's key and its failures' keys match the build's runs. Fail-open per
2272
- * command: one that cannot run, times out, or is not reached in the budget
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 makeTakeVerifyBaseline(opts = {}) {
2277
- const detect = opts.detect ?? defaultDetect;
2278
- const detectTests = opts.detectTests ?? defaultDetectTests;
2279
- const exec = opts.exec ?? defaultExec;
2280
- const now = opts.now ?? Date.now;
2281
- const treeStatus = opts.treeStatus ??
2282
- (async (cwd, signal) => (await snapshotWorkspace(cwd, signal)).status);
2283
- const undoTreeWrites = opts.undoTreeWrites ?? defaultUndoTreeWrites;
2284
- return async function takeVerifyBaseline(cwd, dirs, signal, budgetOverrideMs) {
2285
- const baseline = new Map();
2286
- const result = { baseline, ran: [], failures: 0, notes: [] };
2287
- if (dirs.length === 0)
2288
- return result;
2289
- const commands = detect(cwd, dirs);
2290
- const overridden = commands.some((c) => c.source === "override");
2291
- const tests = overridden ? [] : detectTests(cwd, dirs);
2292
- if (commands.length === 0 && tests.length === 0)
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
- const runs = [
2302
- ...commands.map((command) => ({ command, test: false })),
2303
- ...tests.map((command) => ({ command, test: true })),
2304
- ];
2305
- // The gate's environments, so a failure keys the same on the clean tree and the build's.
2306
- const toolchainFile = (opts.toolchain ?? sessionToolchain)();
2307
- const verifyEnv = buildGateVerifyEnv(process.env, toolchainFile);
2308
- const testEnv = opts.testEnv ?? buildGateTestEnv(process.env, toolchainFile);
2309
- try {
2310
- for (const { command, test } of runs) {
2311
- if (signal?.aborted)
2312
- break;
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
- const cap = Math.min(test
2320
- ? (opts.testTimeoutMs ?? VERIFY_TEST_TIMEOUT_MS)
2321
- : (opts.timeoutMs ?? VERIFY_TIMEOUT_MS), left);
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
- cwd: relative(cwd, command.cwd) || ".",
2327
- base: true,
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
- return result;
2388
- };
2286
+ }
2287
+ return [...targets.values()];
2389
2288
  }
2390
2289
  /**
2391
- * The whole measurement's cap, before the build starts: each check marked to
2392
- * measure, once, the plan's first.
2393
- */
2394
- export const CHECKS_ON_BASE_BUDGET_MS = 10 * 60_000;
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
- * Run as the gate runs the check ({@link makeRunVerify}): the same command
2408
- * line, environment and toolchain, from the repository root, so a failure
2409
- * keys the same on both trees. Bounded: each check by its cap, the whole by
2410
- * the budget, and one not reached in it has no verdict. What the checks
2411
- * write into the tree is undone.
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 makeMeasureChecksOnBase(opts = {}) {
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 measureChecksOnBase(cwd, file, signal, budgetOverrideMs) {
2421
- const passed = new Set();
2422
- const failed = new Map();
2423
- const result = { passed, failed, ran: [], notes: [], treeUndone: true };
2424
- const targets = file.checks.filter((check) => check.measureOnBase === true &&
2425
- check.status !== "cannot_run" &&
2426
- !changesGitState(check.command));
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 ?? CHECKS_ON_BASE_BUDGET_MS, budgetOverrideMs ?? Number.POSITIVE_INFINITY);
2431
- const env = opts.repoChecksEnv ?? buildRepoCheckEnv(process.env, toolchain());
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 checks, so what they write is not undone: ${scrubSecrets(msg)}`);
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 check of targets) {
2376
+ for (const target of targets) {
2440
2377
  if (signal?.aborted)
2441
2378
  break;
2442
- const command = check.command.trim();
2443
- const label = checkLabel(check.command);
2444
- const checkStartedAt = now();
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}\`: not reached in the budget`);
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(opts.timeoutMs ?? REPO_CHECK_TIMEOUT_MS, left)));
2451
- const heard = { name: label, command: check.command, cwd: ".", base: true };
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() - checkStartedAt,
2398
+ durationMs: now() - targetStartedAt,
2458
2399
  ...(output && exitCode !== 0 ? { tail: checkTail(output) } : {}),
2459
2400
  });
2460
2401
  try {
2461
- const { code, output: rawOutput } = await exec(repoCheckArgv(check.command), cwd, composed, env);
2402
+ const { code, output: rawOutput } = await exec(target.argv, target.dir, composed, envs[target.kind]);
2462
2403
  ended(code, rawOutput);
2463
- if (code === 0) {
2464
- passed.add(command);
2465
- result.ran.push(label);
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
- const found = repoCommandFindings({ output, code, env }, {
2480
- label,
2481
- prefix: approvedPrefix(label),
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
- // Output no parser reads says nothing of what failed (a test file
2487
- // the change writes, not there yet): no verdict.
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}\`: ${unexitedNote(err, signal, composed)}`);
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 checks wrote into the tree: ${scrubSecrets(msg)}`);
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