@quikit/quikpilot-runner 8.2.5 → 8.2.7

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/README.md CHANGED
@@ -1,27 +1,27 @@
1
- # @quikit/quikpilot-runner
2
-
3
- QuikPilot's CI runner. Invoked automatically by the `.github/workflows/quikpilot.yml` file QuikPilot
4
- installs in an onboarded repository — it fetches the run's work packet, runs your setup/lint/test
5
- commands, reports results to QuikTest, enforces the denied-path guardrail, commits, pushes, and
6
- reports back to QuikTrack.
7
-
8
- This package is not meant to be installed or run by hand. It exists so a fix to QuikPilot's runner
9
- logic ships as a new package version rather than a new pull request into every onboarded repository
10
- — the workflow file itself lives in your repository (QuikIT cannot update it directly), so it stays a
11
- thin, ~25-line launcher that invokes an exact, pinned version of this package via `npx`.
12
-
13
- It carries no secrets and needs none of your credentials: it authenticates to QuikTrack and to GitHub
14
- using short-lived tokens exchanged at runtime, never a value baked into the workflow file or this
15
- package.
16
-
17
- ## Versioning and support
18
-
19
- Every workflow file pins an **exact** version of this package (`npx --yes
20
- @quikit/quikpilot-runner@<version>`, never a range or `latest`) — a repository never picks up a new
21
- release on its own. Older published versions are not removed from npm and keep working as long as a
22
- repository's workflow file keeps pinning them; upgrading to a newer version only happens through a new
23
- pull request into that repository (QuikTrack's "re-bootstrap" flow), never silently.
24
-
25
- Releases follow [SemVer](https://semver.org/): a major version bump means the workflow file's own
26
- shape changed (inputs, permissions) and requires a new pull request regardless of this package's
27
- version; minor/patch releases are runner-logic fixes safe to pick up on the next re-bootstrap.
1
+ # @quikit/quikpilot-runner
2
+
3
+ QuikPilot's CI runner. Invoked automatically by the `.github/workflows/quikpilot.yml` file QuikPilot
4
+ installs in an onboarded repository — it fetches the run's work packet, runs your setup/lint/test
5
+ commands, reports results to QuikTest, enforces the denied-path guardrail, commits, pushes, and
6
+ reports back to QuikTrack.
7
+
8
+ This package is not meant to be installed or run by hand. It exists so a fix to QuikPilot's runner
9
+ logic ships as a new package version rather than a new pull request into every onboarded repository
10
+ — the workflow file itself lives in your repository (QuikIT cannot update it directly), so it stays a
11
+ thin, ~25-line launcher that invokes an exact, pinned version of this package via `npx`.
12
+
13
+ It carries no secrets and needs none of your credentials: it authenticates to QuikTrack and to GitHub
14
+ using short-lived tokens exchanged at runtime, never a value baked into the workflow file or this
15
+ package.
16
+
17
+ ## Versioning and support
18
+
19
+ Every workflow file pins an **exact** version of this package (`npx --yes
20
+ @quikit/quikpilot-runner@<version>`, never a range or `latest`) — a repository never picks up a new
21
+ release on its own. Older published versions are not removed from npm and keep working as long as a
22
+ repository's workflow file keeps pinning them; upgrading to a newer version only happens through a new
23
+ pull request into that repository (QuikTrack's "re-bootstrap" flow), never silently.
24
+
25
+ Releases follow [SemVer](https://semver.org/): a major version bump means the workflow file's own
26
+ shape changed (inputs, permissions) and requires a new pull request regardless of this package's
27
+ version; minor/patch releases are runner-logic fixes safe to pick up on the next re-bootstrap.
package/dist/index.js CHANGED
@@ -73,11 +73,12 @@ async function describeQuikTrackFailure(res) {
73
73
 
74
74
  // src/callback.ts
75
75
  async function reportBack(params) {
76
- const { callbackUrl, runToken, status, branch, warnings, targeting } = params;
76
+ const { callbackUrl, runToken, status, branch, warnings, targeting, baselineFindings } = params;
77
77
  const body = { status };
78
78
  if (branch) body.branch = branch;
79
79
  if (warnings && warnings.length > 0) body.warnings = warnings;
80
80
  if (targeting) body.targeting = targeting;
81
+ if (baselineFindings) body.baselineFindings = baselineFindings;
81
82
  const res = await fetch(callbackUrl, {
82
83
  method: "POST",
83
84
  headers: { Authorization: `Bearer ${runToken}`, "Content-Type": "application/json" },
@@ -109,10 +110,36 @@ function runTestCommand(command) {
109
110
  }
110
111
  function runTestCommandAsync(command) {
111
112
  if (!command) return Promise.resolve("success");
112
- return new Promise((resolve3) => {
113
+ return new Promise((resolve4) => {
113
114
  const child = (0, import_node_child_process.spawn)(command, { shell: "/bin/bash", stdio: "inherit" });
114
- child.on("exit", (code) => resolve3(code === 0 ? "success" : "failure"));
115
- child.on("error", () => resolve3("failure"));
115
+ child.on("exit", (code) => resolve4(code === 0 ? "success" : "failure"));
116
+ child.on("error", () => resolve4("failure"));
117
+ });
118
+ }
119
+ function runCommandWithTimeout(command, timeoutMs) {
120
+ return new Promise((resolve4) => {
121
+ let settled = false;
122
+ const finish = (outcome) => {
123
+ if (settled) return;
124
+ settled = true;
125
+ clearTimeout(timer);
126
+ resolve4(outcome);
127
+ };
128
+ const child = (0, import_node_child_process.spawn)(command, { shell: "/bin/bash", stdio: "inherit", detached: true });
129
+ const timer = setTimeout(() => {
130
+ try {
131
+ if (child.pid !== void 0) process.kill(-child.pid, "SIGKILL");
132
+ else child.kill("SIGKILL");
133
+ } catch {
134
+ try {
135
+ child.kill("SIGKILL");
136
+ } catch {
137
+ }
138
+ }
139
+ finish("timeout");
140
+ }, timeoutMs);
141
+ child.on("exit", () => finish("exited"));
142
+ child.on("error", () => finish("error"));
116
143
  });
117
144
  }
118
145
 
@@ -959,8 +986,8 @@ async function reportToQuikTest(params) {
959
986
  try {
960
987
  console.log(`reporting ${report}`);
961
988
  const ciUrl = buildCiUrl(githubServerUrl, githubRepository, githubRunId);
962
- const { readFileSync: readFileSync5 } = await import("node:fs");
963
- const body = readFileSync5(report);
989
+ const { readFileSync: readFileSync6 } = await import("node:fs");
990
+ const body = readFileSync6(report);
964
991
  let url = `${resultsUrl}?ciUrl=${encodeURIComponent(ciUrl)}&automationIdFormat=bracket`;
965
992
  if (generatedFiles) {
966
993
  url += `&generatedFiles=${encodeURIComponent(generatedFiles.join(","))}`;
@@ -987,12 +1014,18 @@ async function reportToQuikTest(params) {
987
1014
  }
988
1015
  }
989
1016
 
1017
+ // src/baselineCheck.ts
1018
+ var import_node_child_process3 = require("node:child_process");
1019
+ var import_node_fs6 = require("node:fs");
1020
+ var import_node_path4 = require("node:path");
1021
+
990
1022
  // src/regressionCheck.ts
991
1023
  var import_node_fs5 = require("node:fs");
992
1024
  var TESTCASE_RE = /<testcase\b([^>]*?)(?:\/>|>([\s\S]*?)<\/testcase>)/g;
993
1025
  var NAME_ATTR_RE = /\bname="([^"]*)"/;
994
1026
  var CLASSNAME_ATTR_RE = /\bclassname="([^"]*)"/;
995
1027
  var FAILURE_CHILD_RE = /<failure\b|<error\b/;
1028
+ var SKIPPED_CHILD_RE = /<skipped\b/;
996
1029
  var TESTSUITE_OPEN_RE = /<testsuite\b([^>]*?)>/g;
997
1030
  var FILE_ATTR_RE = /\bfile="([^"]*)"/;
998
1031
  var QP_TOKEN_BRACKET_RE = /\[(QP-[A-Z][A-Z0-9]*-\d+-\d+)\]/;
@@ -1005,17 +1038,32 @@ var MAX_FAILURE_TEXT = 500;
1005
1038
  function unescapeXmlEntities(s) {
1006
1039
  return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
1007
1040
  }
1041
+ var FAILURE_FILE_LINE_RE = /^[ \t]*File:[ \t]*(\S[^\r\n]*?:\d+(?::\d+)?)[ \t]*$/m;
1042
+ var MAX_FAILURE_LOCATION = 300;
1043
+ function parseFailureLocation(body) {
1044
+ const found = FAILURE_FILE_LINE_RE.exec(unescapeXmlEntities(body));
1045
+ if (!found) return null;
1046
+ const path = normalizeSlashes(found[1]);
1047
+ const root = `${normalizeSlashes(process.cwd()).replace(/\/+$/, "")}/`;
1048
+ const shown = path.startsWith(root) ? path.slice(root.length) : path;
1049
+ return shown.length > MAX_FAILURE_LOCATION ? `${shown.slice(0, MAX_FAILURE_LOCATION)}\u2026` : shown;
1050
+ }
1008
1051
  function extractFailureText(testcaseBody) {
1009
1052
  const tag = FAILURE_TAG_RE.exec(testcaseBody);
1010
1053
  if (!tag) return null;
1011
1054
  const [, attrs = "", inner = ""] = tag;
1012
- const raw = (FAILURE_MESSAGE_ATTR_RE.exec(attrs)?.[1] ?? inner).trim();
1055
+ const messageAttr = FAILURE_MESSAGE_ATTR_RE.exec(attrs)?.[1];
1056
+ const raw = (messageAttr ?? inner).trim();
1013
1057
  if (!raw) return null;
1014
1058
  const text = unescapeXmlEntities(raw);
1015
- return text.length > MAX_FAILURE_TEXT ? `${text.slice(0, MAX_FAILURE_TEXT)}\u2026` : text;
1059
+ const shortened = text.length > MAX_FAILURE_TEXT ? `${text.slice(0, MAX_FAILURE_TEXT)}\u2026` : text;
1060
+ const location = messageAttr === void 0 ? null : parseFailureLocation(inner);
1061
+ return location ? `${shortened} [at ${location}]` : shortened;
1016
1062
  }
1063
+ var RESULT_RANK = { pass: 0, skipped: 1, fail: 2 };
1017
1064
  function parseJUnitOutcomes(xml) {
1018
1065
  const outcomes = /* @__PURE__ */ new Map();
1066
+ const automationIdResults = /* @__PURE__ */ new Map();
1019
1067
  const failureText = /* @__PURE__ */ new Map();
1020
1068
  const discoveredFiles = /* @__PURE__ */ new Set();
1021
1069
  const discoveredAutomationIds = /* @__PURE__ */ new Set();
@@ -1043,9 +1091,14 @@ function parseJUnitOutcomes(xml) {
1043
1091
  if (file) discoveredFiles.add(normalizeSlashes(file));
1044
1092
  if (classname) discoveredFiles.add(normalizeSlashes(classname));
1045
1093
  const bracket = QP_TOKEN_BRACKET_RE.exec(name)?.[1];
1046
- if (bracket) discoveredAutomationIds.add(bracket);
1094
+ if (bracket) {
1095
+ discoveredAutomationIds.add(bracket);
1096
+ const result = failed ? "fail" : SKIPPED_CHILD_RE.test(body) ? "skipped" : "pass";
1097
+ const seen = automationIdResults.get(bracket);
1098
+ if (!seen || RESULT_RANK[result] > RESULT_RANK[seen]) automationIdResults.set(bracket, result);
1099
+ }
1047
1100
  }
1048
- return { outcomes, failureText, discoveredFiles, discoveredAutomationIds };
1101
+ return { outcomes, automationIdResults, failureText, discoveredFiles, discoveredAutomationIds };
1049
1102
  }
1050
1103
  function readJUnitOutcomes(cwd) {
1051
1104
  const report = findJUnitReport(cwd);
@@ -1056,6 +1109,9 @@ function readJUnitOutcomes(cwd) {
1056
1109
  return null;
1057
1110
  }
1058
1111
  }
1112
+ function readAutomationIdResults(cwd = process.cwd()) {
1113
+ return readJUnitOutcomes(cwd)?.automationIdResults ?? null;
1114
+ }
1059
1115
  function captureTestSnapshot(command, cwd = process.cwd()) {
1060
1116
  const outcome = runTestCommand(command);
1061
1117
  const parsed = readJUnitOutcomes(cwd);
@@ -1126,8 +1182,186 @@ function describeRegressions(check) {
1126
1182
  return parts.join("; ");
1127
1183
  }
1128
1184
 
1185
+ // src/baselineCheck.ts
1186
+ var BASELINE_CHECK_TIMEOUT_MS = 12e4;
1187
+ function shellQuote(value) {
1188
+ return `'${value.split("'").join("'\\''")}'`;
1189
+ }
1190
+ function buildSingleFileCommand(command, path) {
1191
+ const quoted = shellQuote(path);
1192
+ return command.includes("{file}") ? command.split("{file}").join(quoted) : `${command} ${quoted}`;
1193
+ }
1194
+ var isTestTarget2 = (t) => (t.automationIds?.length ?? 0) > 0;
1195
+ function checkableIds(ids, types) {
1196
+ if (!types) return [];
1197
+ return ids.filter((id) => {
1198
+ if (!Object.prototype.hasOwnProperty.call(types, id)) return false;
1199
+ const type = types[id];
1200
+ return typeof type === "string" && type.trim() !== "" && type.trim().toUpperCase() !== "REGRESSION";
1201
+ });
1202
+ }
1203
+ function insideCheckout(cwd, path) {
1204
+ const abs = (0, import_node_path4.resolve)(cwd, path);
1205
+ const rel = (0, import_node_path4.relative)(cwd, abs);
1206
+ return rel === "" || rel.startsWith("..") || (0, import_node_path4.isAbsolute)(rel) ? null : abs;
1207
+ }
1208
+ function writeForCheck(abs, content) {
1209
+ const original = (0, import_node_fs6.existsSync)(abs) ? (0, import_node_fs6.readFileSync)(abs) : null;
1210
+ const createdDirs = [];
1211
+ if (original === null) {
1212
+ const missing = [];
1213
+ for (let d = (0, import_node_path4.dirname)(abs); !(0, import_node_fs6.existsSync)(d); d = (0, import_node_path4.dirname)(d)) missing.push(d);
1214
+ for (const d of missing.reverse()) {
1215
+ (0, import_node_fs6.mkdirSync)(d);
1216
+ createdDirs.push(d);
1217
+ }
1218
+ }
1219
+ (0, import_node_fs6.writeFileSync)(abs, content);
1220
+ return { abs, original, createdDirs };
1221
+ }
1222
+ function restore(written) {
1223
+ for (const file of [...written].reverse()) {
1224
+ try {
1225
+ if (file.original !== null) (0, import_node_fs6.writeFileSync)(file.abs, file.original);
1226
+ else (0, import_node_fs6.rmSync)(file.abs, { force: true });
1227
+ for (const d of [...file.createdDirs].reverse()) {
1228
+ try {
1229
+ (0, import_node_fs6.rmdirSync)(d);
1230
+ } catch {
1231
+ }
1232
+ }
1233
+ } catch (err) {
1234
+ console.error(`Baseline check could not restore ${file.abs}: ${err instanceof Error ? err.message : String(err)}`);
1235
+ }
1236
+ }
1237
+ }
1238
+ function untrackedFiles(cwd) {
1239
+ try {
1240
+ const out = (0, import_node_child_process3.execFileSync)("git", ["ls-files", "--others", "--exclude-standard", "-z"], {
1241
+ cwd,
1242
+ encoding: "utf8",
1243
+ stdio: ["ignore", "pipe", "ignore"],
1244
+ maxBuffer: 64 * 1024 * 1024
1245
+ });
1246
+ return new Set(out.split("\0").filter(Boolean));
1247
+ } catch {
1248
+ return null;
1249
+ }
1250
+ }
1251
+ var mtimeOf = (path) => {
1252
+ try {
1253
+ return (0, import_node_fs6.statSync)(path).mtimeMs;
1254
+ } catch {
1255
+ return null;
1256
+ }
1257
+ };
1258
+ function decideCause(targets, results, cwd) {
1259
+ const codeTargets = targets.filter((t) => !isTestTarget2(t));
1260
+ if (codeTargets.length === 0) return "ALREADY_TRUE";
1261
+ for (const t of codeTargets) {
1262
+ if (t.kind === "NEW_FILE") return "ALREADY_TRUE";
1263
+ const next = results.get(t.path);
1264
+ const abs = insideCheckout(cwd, t.path);
1265
+ if (next === void 0 || abs === null) return "ALREADY_TRUE";
1266
+ try {
1267
+ if ((0, import_node_fs6.readFileSync)(abs, "utf8") !== next) return "ALREADY_TRUE";
1268
+ } catch {
1269
+ return "ALREADY_TRUE";
1270
+ }
1271
+ }
1272
+ return "NO_OP";
1273
+ }
1274
+ async function runBaselineCheck(params) {
1275
+ const { targets, results, testFileCmd, requiredTestTypes } = params;
1276
+ const cwd = params.cwd ?? process.cwd();
1277
+ const runFile = params.runFile ?? runCommandWithTimeout;
1278
+ const nothing = { findings: null, warnings: [] };
1279
+ if (!testFileCmd || testFileCmd.trim() === "") return nothing;
1280
+ const testTargets = targets.filter(isTestTarget2);
1281
+ const toCheck = testTargets.map((target) => ({ target, ids: checkableIds(target.automationIds ?? [], requiredTestTypes) })).filter((entry) => entry.ids.length > 0);
1282
+ if (toCheck.length === 0) {
1283
+ console.log("Baseline check: no case with a known, non-Regression label \u2014 nothing to check.");
1284
+ return nothing;
1285
+ }
1286
+ const warnings = [];
1287
+ const flagged = [];
1288
+ const written = [];
1289
+ const unsafe = /* @__PURE__ */ new Set();
1290
+ const skip = (path, why) => {
1291
+ const message = `Baseline check skipped for ${path}: ${why}`;
1292
+ console.warn(`::warning::${message}`);
1293
+ warnings.push(message);
1294
+ };
1295
+ try {
1296
+ const contentByPath = new Map(results.map((r) => [r.targetPath, r.newContent]));
1297
+ const cause = decideCause(targets, contentByPath, cwd);
1298
+ for (const target of testTargets) {
1299
+ const content = contentByPath.get(target.path);
1300
+ if (content === void 0) continue;
1301
+ const abs = insideCheckout(cwd, target.path);
1302
+ if (abs === null) {
1303
+ unsafe.add(target.path);
1304
+ continue;
1305
+ }
1306
+ written.push(writeForCheck(abs, content));
1307
+ }
1308
+ for (const { target, ids } of toCheck) {
1309
+ if (unsafe.has(target.path)) {
1310
+ skip(target.path, "the path is outside the checkout, so the file was not written or run.");
1311
+ continue;
1312
+ }
1313
+ if (!contentByPath.has(target.path)) continue;
1314
+ const reportBefore = findJUnitReport(cwd);
1315
+ const mtimeBefore = reportBefore ? mtimeOf(reportBefore) : null;
1316
+ const untrackedBefore = untrackedFiles(cwd);
1317
+ const outcome = await runFile(buildSingleFileCommand(testFileCmd, target.path), BASELINE_CHECK_TIMEOUT_MS);
1318
+ if (outcome === "timeout") {
1319
+ skip(
1320
+ target.path,
1321
+ `the single-file command did not finish within ${BASELINE_CHECK_TIMEOUT_MS / 1e3} seconds and was stopped. No test case from this file was flagged.`
1322
+ );
1323
+ } else if (outcome === "error") {
1324
+ skip(target.path, "the single-file command could not be started. No test case from this file was flagged.");
1325
+ } else {
1326
+ const reportAfter = findJUnitReport(cwd);
1327
+ const fresh = reportAfter !== null && (reportAfter !== reportBefore || mtimeOf(reportAfter) !== mtimeBefore);
1328
+ if (!fresh) {
1329
+ skip(
1330
+ target.path,
1331
+ "the single-file command wrote no new JUnit report (it must write the same JUnit report as the test command), so nothing could be checked."
1332
+ );
1333
+ } else {
1334
+ const byId = readAutomationIdResults(cwd);
1335
+ for (const id of ids) if (byId?.get(id) === "pass") flagged.push(id);
1336
+ }
1337
+ }
1338
+ const untrackedAfter = untrackedFiles(cwd);
1339
+ if (untrackedBefore && untrackedAfter) {
1340
+ for (const file of untrackedAfter) {
1341
+ if (untrackedBefore.has(file)) continue;
1342
+ const abs = insideCheckout(cwd, file);
1343
+ if (abs) (0, import_node_fs6.rmSync)(abs, { force: true });
1344
+ }
1345
+ }
1346
+ }
1347
+ if (flagged.length === 0) return { findings: null, warnings };
1348
+ const automationIds = [...new Set(flagged)].slice(0, 200);
1349
+ const list = automationIds.join(", ");
1350
+ warnings.push(
1351
+ cause === "NO_OP" ? `Baseline check: ${automationIds.length} test case(s) passed against the unmodified code, and every file the generated change edited came out unchanged, so the change itself may have been a no-op: ${list}.` : `Baseline check: ${automationIds.length} test case(s) passed against the unmodified code, so they may assert something already true: ${list}. This check cannot tell that from a change that did nothing.`
1352
+ );
1353
+ return { findings: { cause, automationIds }, warnings };
1354
+ } catch (err) {
1355
+ const message = `Baseline check could not run: ${err instanceof Error ? err.message : String(err)}`;
1356
+ console.warn(`::warning::${message}`);
1357
+ return { findings: null, warnings: [...warnings, message] };
1358
+ } finally {
1359
+ restore(written);
1360
+ }
1361
+ }
1362
+
1129
1363
  // src/scopeCheck.ts
1130
- var import_node_fs6 = require("node:fs");
1364
+ var import_node_fs7 = require("node:fs");
1131
1365
  var ROUTE_METHOD_RE = /\b(GET|POST|PUT|PATCH|DELETE)\b/i;
1132
1366
  var ROUTE_PATH_RE = /\/[A-Za-z0-9_\-{}[\]]+(?:\/[A-Za-z0-9_\-:{}[\]]+)+/;
1133
1367
  var ROUTE_EXPORT_RE = /\bexport\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\s*\(/;
@@ -1184,7 +1418,7 @@ function checkApiScope(results, targets, apiChanges) {
1184
1418
  } else {
1185
1419
  let currentContent;
1186
1420
  try {
1187
- currentContent = (0, import_node_fs6.readFileSync)(targetPath, "utf8");
1421
+ currentContent = (0, import_node_fs7.readFileSync)(targetPath, "utf8");
1188
1422
  } catch {
1189
1423
  currentContent = "";
1190
1424
  }
@@ -1203,7 +1437,7 @@ function describeScopeViolations(check) {
1203
1437
  }
1204
1438
 
1205
1439
  // src/targeting.ts
1206
- var import_node_path4 = require("node:path");
1440
+ var import_node_path5 = require("node:path");
1207
1441
  var CONTRACT_VERSION2 = "test-targets-1";
1208
1442
  function listWorkspaceFiles(workspaceRoot) {
1209
1443
  return listFiles(workspaceRoot ?? ".");
@@ -1232,9 +1466,9 @@ function validateTarget(target, workspaceRoot, workspaceListing, deniedPaths) {
1232
1466
  }
1233
1467
  return;
1234
1468
  }
1235
- const root = (0, import_node_path4.resolve)(workspaceRoot ?? ".");
1236
- const rel = (0, import_node_path4.relative)(root, (0, import_node_path4.resolve)(target.path));
1237
- if (rel.startsWith("..") || (0, import_node_path4.isAbsolute)(rel)) {
1469
+ const root = (0, import_node_path5.resolve)(workspaceRoot ?? ".");
1470
+ const rel = (0, import_node_path5.relative)(root, (0, import_node_path5.resolve)(target.path));
1471
+ if (rel.startsWith("..") || (0, import_node_path5.isAbsolute)(rel)) {
1238
1472
  throw new Error(
1239
1473
  `pilot.targeting named a NEW_FILE path that escapes the workspace root: ${target.path}`
1240
1474
  );
@@ -1455,10 +1689,11 @@ async function run(env2) {
1455
1689
  let packet;
1456
1690
  let branch;
1457
1691
  let runToken;
1458
- let warnings = [];
1692
+ const warnings = [];
1459
1693
  let targeted = false;
1460
1694
  let generatedFilePaths = [];
1461
1695
  let targetingResult;
1696
+ let baselineFindings;
1462
1697
  const contextLog = [];
1463
1698
  try {
1464
1699
  runToken = await exchangeForRunToken(env2.tokenUrl, await fetchGithubOidcToken());
@@ -1493,8 +1728,9 @@ async function run(env2) {
1493
1728
  const blockingOverlaps = overlaps.filter((o) => o.severity === "identical");
1494
1729
  const softOverlaps = overlaps.filter((o) => o.severity === "similar");
1495
1730
  if (softOverlaps.length > 0) {
1496
- warnings = describeOverlapWarnings(softOverlaps);
1497
- for (const w of warnings) {
1731
+ const overlapWarnings = describeOverlapWarnings(softOverlaps);
1732
+ warnings.push(...overlapWarnings);
1733
+ for (const w of overlapWarnings) {
1498
1734
  console.warn(`QuikPilot targeting warning (not blocking): ${w}`);
1499
1735
  }
1500
1736
  }
@@ -1544,6 +1780,20 @@ async function run(env2) {
1544
1780
  `QuikPilot's generated tests are not where the test runner will find them \u2014 ${describeTestPlacementViolations(placementCheck)}`
1545
1781
  );
1546
1782
  }
1783
+ try {
1784
+ const baselineCheck = await runBaselineCheck({
1785
+ targets: targeting.targets,
1786
+ results,
1787
+ testFileCmd: packet.commands.testFile,
1788
+ requiredTestTypes: packet.requiredTestTypes
1789
+ });
1790
+ warnings.push(...baselineCheck.warnings);
1791
+ baselineFindings = baselineCheck.findings ?? void 0;
1792
+ } catch (err) {
1793
+ const message = `Baseline check could not run: ${err instanceof Error ? err.message : String(err)}`;
1794
+ console.error(message);
1795
+ warnings.push(message);
1796
+ }
1547
1797
  generatedFilePaths = results.map((r) => r.targetPath);
1548
1798
  flushCodegenResults(results);
1549
1799
  try {
@@ -1615,7 +1865,8 @@ async function run(env2) {
1615
1865
  branch,
1616
1866
  warnings,
1617
1867
  // Omitted when targeting itself never produced a result (nothing exists to record).
1618
- targeting: targetingResult ? buildTargetingRecord(targetingResult, contextLog) : void 0
1868
+ targeting: targetingResult ? buildTargetingRecord(targetingResult, contextLog) : void 0,
1869
+ baselineFindings
1619
1870
  });
1620
1871
  return { hardFailure };
1621
1872
  }