@quikit/quikpilot-runner 8.2.7 → 9.0.0

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/index.js CHANGED
@@ -73,12 +73,14 @@ 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, baselineFindings } = params;
76
+ const { callbackUrl, runToken, status, branch, warnings, targeting, baselineFindings, vanishedTests, unresolved } = params;
77
77
  const body = { status };
78
78
  if (branch) body.branch = branch;
79
79
  if (warnings && warnings.length > 0) body.warnings = warnings;
80
+ if (unresolved && unresolved.length > 0) body.unresolved = unresolved;
80
81
  if (targeting) body.targeting = targeting;
81
82
  if (baselineFindings) body.baselineFindings = baselineFindings;
83
+ if (vanishedTests) body.vanishedTests = vanishedTests;
82
84
  const res = await fetch(callbackUrl, {
83
85
  method: "POST",
84
86
  headers: { Authorization: `Bearer ${runToken}`, "Content-Type": "application/json" },
@@ -110,20 +112,20 @@ function runTestCommand(command) {
110
112
  }
111
113
  function runTestCommandAsync(command) {
112
114
  if (!command) return Promise.resolve("success");
113
- return new Promise((resolve4) => {
115
+ return new Promise((resolve5) => {
114
116
  const child = (0, import_node_child_process.spawn)(command, { shell: "/bin/bash", stdio: "inherit" });
115
- child.on("exit", (code) => resolve4(code === 0 ? "success" : "failure"));
116
- child.on("error", () => resolve4("failure"));
117
+ child.on("exit", (code) => resolve5(code === 0 ? "success" : "failure"));
118
+ child.on("error", () => resolve5("failure"));
117
119
  });
118
120
  }
119
121
  function runCommandWithTimeout(command, timeoutMs) {
120
- return new Promise((resolve4) => {
122
+ return new Promise((resolve5) => {
121
123
  let settled = false;
122
124
  const finish = (outcome) => {
123
125
  if (settled) return;
124
126
  settled = true;
125
127
  clearTimeout(timer);
126
- resolve4(outcome);
128
+ resolve5(outcome);
127
129
  };
128
130
  const child = (0, import_node_child_process.spawn)(command, { shell: "/bin/bash", stdio: "inherit", detached: true });
129
131
  const timer = setTimeout(() => {
@@ -250,6 +252,106 @@ function findDeniedContextPath(contextPaths, patterns) {
250
252
  return null;
251
253
  }
252
254
 
255
+ // src/editsApply.ts
256
+ function detectLineEnding(text) {
257
+ const crlf = text.split("\r\n").length - 1;
258
+ const bareLf = text.split("\n").length - 1 - crlf;
259
+ if (crlf > 0 && bareLf > 0) return "mixed";
260
+ return crlf > 0 ? "CRLF" : "LF";
261
+ }
262
+ function toLf(text) {
263
+ return text.replace(/\r\n/g, "\n");
264
+ }
265
+ function fromLf(text, ending) {
266
+ return ending === "CRLF" ? text.replace(/\n/g, "\r\n") : text;
267
+ }
268
+ function occurrences(haystack, needle) {
269
+ const found = [];
270
+ let index = haystack.indexOf(needle);
271
+ while (index !== -1) {
272
+ found.push(index);
273
+ index = haystack.indexOf(needle, index + 1);
274
+ }
275
+ return found;
276
+ }
277
+ function applyEdits(originalLf, edits) {
278
+ if (!Array.isArray(edits) || edits.length === 0) {
279
+ return {
280
+ ok: false,
281
+ editCount: 0,
282
+ refusals: [{ kind: "no_edits", edits: [], matchCount: null, message: "the response carried no edits" }]
283
+ };
284
+ }
285
+ const refusals = [];
286
+ const ranges = [];
287
+ edits.forEach((edit, index) => {
288
+ const number = index + 1;
289
+ const record = edit !== null && typeof edit === "object" ? edit : null;
290
+ const find = record?.find;
291
+ const replace = record?.replace;
292
+ if (typeof find !== "string" || typeof replace !== "string") {
293
+ refusals.push({
294
+ kind: "empty",
295
+ edits: [number],
296
+ matchCount: null,
297
+ message: `edit ${number} is missing a string find or replace`
298
+ });
299
+ return;
300
+ }
301
+ if (find === "") {
302
+ refusals.push({ kind: "empty", edits: [number], matchCount: null, message: `edit ${number} has an empty anchor` });
303
+ return;
304
+ }
305
+ const starts = occurrences(originalLf, find);
306
+ if (starts.length === 0) {
307
+ refusals.push({ kind: "no_match", edits: [number], matchCount: 0, message: `edit ${number}: the anchor matched nothing` });
308
+ } else if (starts.length > 1) {
309
+ refusals.push({
310
+ kind: "ambiguous",
311
+ edits: [number],
312
+ matchCount: starts.length,
313
+ message: `edit ${number}: the anchor matched ${starts.length} times`
314
+ });
315
+ } else {
316
+ ranges.push({ start: starts[0], end: starts[0] + find.length, number, replace });
317
+ }
318
+ });
319
+ for (let i = 0; i < ranges.length; i++) {
320
+ for (let j = i + 1; j < ranges.length; j++) {
321
+ const a = ranges[i];
322
+ const b = ranges[j];
323
+ if (a.start < b.end && b.start < a.end) {
324
+ const [first, second] = a.number < b.number ? [a.number, b.number] : [b.number, a.number];
325
+ refusals.push({
326
+ kind: "overlap",
327
+ edits: [first, second],
328
+ matchCount: null,
329
+ message: `edits ${first} and ${second} overlap`
330
+ });
331
+ }
332
+ }
333
+ }
334
+ if (refusals.length > 0) return { ok: false, refusals, editCount: edits.length };
335
+ const pieces = [];
336
+ let cursor = 0;
337
+ for (const range of [...ranges].sort((x, y) => x.start - y.start)) {
338
+ pieces.push(originalLf.slice(cursor, range.start), range.replace);
339
+ cursor = range.end;
340
+ }
341
+ pieces.push(originalLf.slice(cursor));
342
+ return { ok: true, content: pieces.join(""), editCount: edits.length };
343
+ }
344
+
345
+ // src/runtimeReplyError.ts
346
+ var RuntimeReplyError = class extends Error {
347
+ summary;
348
+ constructor(message, summary) {
349
+ super(message);
350
+ this.name = "RuntimeReplyError";
351
+ this.summary = summary;
352
+ }
353
+ };
354
+
253
355
  // src/envelope.ts
254
356
  var HARD_FAILURE_CODES = /* @__PURE__ */ new Set(["SEMANTIC_MISMATCH", "CONTRACT_REJECTED"]);
255
357
  function checkHardFailure(envelope, useCase, target) {
@@ -257,17 +359,25 @@ function checkHardFailure(envelope, useCase, target) {
257
359
  if (typeof code === "string" && HARD_FAILURE_CODES.has(code)) {
258
360
  const detail = envelope.error ?? envelope.normalizedText ?? "no detail";
259
361
  const where = target ? ` for ${target}` : "";
260
- throw new Error(`${useCase} failed${where} (${code}, not retried): ${detail}`);
362
+ throw new RuntimeReplyError(
363
+ `${useCase} failed${where} (${code}, not retried): ${detail}`,
364
+ `${useCase} failed${where} (${code}, not retried)`
365
+ );
261
366
  }
262
367
  }
263
368
  function unwrapExecute(envelope, useCase) {
264
369
  if (envelope?.success === false) {
265
- throw new Error(
266
- `${useCase} failed: ${envelope.errorCode ?? "UNKNOWN"} \u2014 ${envelope.error ?? envelope.normalizedText ?? "no message"}`
370
+ const code = envelope.errorCode ?? "UNKNOWN";
371
+ throw new RuntimeReplyError(
372
+ `${useCase} failed: ${code} \u2014 ${envelope.error ?? envelope.normalizedText ?? "no message"}`,
373
+ `${useCase} failed: ${code}`
267
374
  );
268
375
  }
269
376
  if (!envelope?.structuredJson || typeof envelope.structuredJson !== "object") {
270
- throw new Error(`${useCase} returned no structuredJson: ${JSON.stringify(envelope)}`);
377
+ throw new RuntimeReplyError(
378
+ `${useCase} returned no structuredJson: ${JSON.stringify(envelope)}`,
379
+ `${useCase} returned no structuredJson`
380
+ );
271
381
  }
272
382
  return envelope.structuredJson;
273
383
  }
@@ -394,6 +504,58 @@ function looksLikeTruncatedRewrite(target, currentContent, newContent) {
394
504
  return null;
395
505
  }
396
506
  var CONTRACT_VERSION = "test-targets-1";
507
+ var CONTRACT_VERSION_EDITS = "edits-1";
508
+ var ANCHOR_REFUSED_CODE = "CODEGEN_ANCHOR_REFUSED";
509
+ function summariseLocalRefusals(refusals) {
510
+ const parts = [];
511
+ const matchCounts = [];
512
+ for (const r of refusals) {
513
+ if (r.kind === "ambiguous") {
514
+ parts.push(`an anchor matched ${r.matchCount} times`);
515
+ if (r.matchCount !== null) matchCounts.push(r.matchCount);
516
+ } else if (r.kind === "no_match") {
517
+ parts.push("an anchor matched nothing");
518
+ } else if (r.kind === "overlap") {
519
+ parts.push(`edits ${r.edits.join(" and ")} overlap`);
520
+ } else if (r.kind === "no_edits") {
521
+ parts.push("the reply had no edits");
522
+ } else {
523
+ parts.push("an edit was missing its anchor or replacement");
524
+ }
525
+ }
526
+ return { text: [...new Set(parts)].join("; "), matchCounts };
527
+ }
528
+ function summariseRuntimeRefusal(error) {
529
+ const text = typeof error === "string" ? error : "";
530
+ const matchCounts = [...text.matchAll(/anchor matched (\d+) times/g)].map((m) => Number(m[1]));
531
+ const parts = matchCounts.map((n) => `an anchor matched ${n} times`);
532
+ if (/no match/i.test(text)) parts.push("an anchor matched nothing");
533
+ if (/overlap/i.test(text)) parts.push("edits overlapped");
534
+ if (/empty anchor|missing a string/i.test(text)) parts.push("an edit was missing its anchor or replacement");
535
+ return { text: parts.length > 0 ? [...new Set(parts)].join("; ") : "the runtime could not place the edits", matchCounts };
536
+ }
537
+ function throwOnTerminalRefusal(envelope, targetPath) {
538
+ if (typeof envelope?.errorCode === "string" && NOT_RETRIED_CODES.has(envelope.errorCode)) {
539
+ const detail = envelope.error ? `: ${envelope.error}` : "";
540
+ throw new RuntimeReplyError(
541
+ `Codegen refused ${targetPath}: ${envelope.errorCode} (not retried)${detail}`,
542
+ `Codegen refused ${targetPath}: ${envelope.errorCode} (not retried)`
543
+ );
544
+ }
545
+ if (envelope?.errorCode === JUDGED_BY_TOO_LARGE_CODE) {
546
+ const detail = envelope.error ? `: ${envelope.error}` : "";
547
+ throw new RuntimeReplyError(
548
+ `Codegen refused ${targetPath}: ${envelope.errorCode} (after narrowing)${detail}`,
549
+ `Codegen refused ${targetPath}: ${envelope.errorCode} (after narrowing)`
550
+ );
551
+ }
552
+ }
553
+ function codegenRefusedError(targetPath, body) {
554
+ return new RuntimeReplyError(
555
+ `Codegen refused ${targetPath}${body.refusalReason ? `: ${body.refusalReason}` : ""}`,
556
+ `Codegen refused ${targetPath}`
557
+ );
558
+ }
397
559
  function buildJudgedByPool(requiredTests, targets, skippedCases) {
398
560
  const claimedIds = /* @__PURE__ */ new Set();
399
561
  for (const t of targets) {
@@ -419,12 +581,14 @@ function narrowJudgedByForTarget(pool, targets, codeTargetPath) {
419
581
  function toWireJudgedBy(pool) {
420
582
  return pool.map(({ automationId: _automationId, ...rest }) => rest);
421
583
  }
422
- async function runCodegen(gateway, targets, workspaceListing, requiredTests, deniedPaths, judgedByPool, warnings, contextLog) {
584
+ async function runCodegen(gateway, targets, workspaceListing, requiredTests, deniedPaths, judgedByPool, warnings, contextLog, outOfScope = []) {
423
585
  const results = [];
424
586
  const listing = new Set(workspaceListing);
425
587
  for (const target of targets) {
426
588
  const isNewFile = target.kind === "NEW_FILE";
427
589
  const currentContent = isNewFile ? "" : (0, import_node_fs.readFileSync)(target.path, "utf8");
590
+ const lineEnding = isNewFile ? "LF" : detectLineEnding(currentContent);
591
+ const wantsEdits = !isNewFile && lineEnding !== "mixed";
428
592
  const contextPaths = target.contextPaths ?? [];
429
593
  const deniedContext = findDeniedContextPath(contextPaths, deniedPaths);
430
594
  if (deniedContext) {
@@ -439,7 +603,7 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
439
603
  );
440
604
  }
441
605
  }
442
- const namedContents = contextPaths.map((path) => ({ path, content: (0, import_node_fs.readFileSync)(path, "utf8") }));
606
+ const namedContents = contextPaths.map((path2) => ({ path: path2, content: (0, import_node_fs.readFileSync)(path2, "utf8") }));
443
607
  const added = collectRunContext(target, targets, new Set(contextPaths), deniedPaths);
444
608
  const contextContents = [...namedContents, ...added.entries];
445
609
  for (const s of added.skipped) {
@@ -455,12 +619,13 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
455
619
  contextLog?.push(contextRecord);
456
620
  const contextData = {
457
621
  targetPath: target.path,
458
- currentContent,
622
+ // Every anchor and replacement is LF, always (Doc 62 §2), so the file is sent as LF.
623
+ currentContent: wantsEdits ? toLf(currentContent) : currentContent,
459
624
  planSteps: target.planStepsImplemented,
460
625
  kind: target.kind,
461
626
  contextContents,
462
627
  // QUIKTR-1083 (§15.6) — required on every call on the versioned path.
463
- contractVersion: CONTRACT_VERSION
628
+ contractVersion: wantsEdits ? CONTRACT_VERSION_EDITS : CONTRACT_VERSION
464
629
  };
465
630
  if (isNewFile) {
466
631
  contextData.workspaceListing = workspaceListing;
@@ -468,6 +633,9 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
468
633
  if (deniedPaths.length > 0) {
469
634
  contextData.deniedPaths = deniedPaths;
470
635
  }
636
+ if (outOfScope.length > 0) {
637
+ contextData.outOfScope = outOfScope;
638
+ }
471
639
  if (target.automationIds && target.automationIds.length > 0) {
472
640
  const wanted = new Set(target.automationIds);
473
641
  contextData.requiredTests = requiredTests.filter((tc) => wanted.has(tc.automationId));
@@ -519,22 +687,91 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
519
687
  envelope = await res.json();
520
688
  checkHardFailure(envelope, "pilot.codegen", target.path);
521
689
  }
522
- if (typeof envelope?.errorCode === "string" && NOT_RETRIED_CODES.has(envelope.errorCode)) {
523
- const detail = envelope.error ? `: ${envelope.error}` : "";
524
- throw new Error(`Codegen refused ${target.path}: ${envelope.errorCode} (not retried)${detail}`);
690
+ throwOnTerminalRefusal(envelope, target.path);
691
+ let newContent = null;
692
+ let failedEdits = null;
693
+ let editCount;
694
+ let path = "whole_file";
695
+ let reason = isNewFile ? "new_file" : wantsEdits ? void 0 : "mixed_line_endings";
696
+ if (wantsEdits && envelope?.errorCode === ANCHOR_REFUSED_CODE) {
697
+ failedEdits = { reason: "anchor_refused", summary: summariseRuntimeRefusal(envelope.error) };
698
+ } else {
699
+ const body = unwrapExecute(envelope, "pilot.codegen");
700
+ if (wantsEdits) {
701
+ if (body.outcome === "REFUSED") throw codegenRefusedError(target.path, body);
702
+ if (body.outcome !== "GENERATED" || typeof body.newContent === "string") {
703
+ throw new RuntimeReplyError(
704
+ `Codegen reply for ${target.path} was not in the edits-1 shape and was not applied`,
705
+ `Codegen reply for ${target.path} was not in the edits-1 shape`
706
+ );
707
+ }
708
+ const applied = applyEdits(toLf(currentContent), body.edits);
709
+ if (applied.ok) {
710
+ newContent = fromLf(applied.content, lineEnding);
711
+ path = "edits";
712
+ editCount = applied.editCount;
713
+ } else if (applied.refusals.some((r) => r.kind === "no_edits")) {
714
+ throw new RuntimeReplyError(
715
+ `Codegen reply for ${target.path} carried no edits (not retried)`,
716
+ `Codegen reply for ${target.path} carried no edits`
717
+ );
718
+ } else {
719
+ failedEdits = { reason: "local_refusal", summary: summariseLocalRefusals(applied.refusals) };
720
+ }
721
+ } else {
722
+ if (body.outcome !== "GENERATED" || typeof body.newContent !== "string") {
723
+ throw codegenRefusedError(target.path, body);
724
+ }
725
+ newContent = body.newContent;
726
+ }
525
727
  }
526
- if (envelope?.errorCode === JUDGED_BY_TOO_LARGE_CODE) {
527
- const detail = envelope.error ? `: ${envelope.error}` : "";
528
- throw new Error(`Codegen refused ${target.path}: ${envelope.errorCode} (after narrowing)${detail}`);
728
+ if (failedEdits) {
729
+ warnings.push(
730
+ `edits for ${target.path} could not be placed (${failedEdits.summary.text}); asked again for the whole file`
731
+ );
732
+ contextData.currentContent = currentContent;
733
+ contextData.contractVersion = CONTRACT_VERSION;
734
+ try {
735
+ res = await callCodegen();
736
+ if (!res.ok) {
737
+ throw new Error(`Codegen call failed for ${target.path}: ${res.status} ${res.statusText}`);
738
+ }
739
+ envelope = await res.json();
740
+ checkHardFailure(envelope, "pilot.codegen", target.path);
741
+ throwOnTerminalRefusal(envelope, target.path);
742
+ const whole = unwrapExecute(envelope, "pilot.codegen");
743
+ if (whole.outcome !== "GENERATED" || typeof whole.newContent !== "string") {
744
+ throw codegenRefusedError(target.path, whole);
745
+ }
746
+ newContent = whole.newContent;
747
+ } catch (error) {
748
+ if (error instanceof RuntimeReplyError) {
749
+ throw new RuntimeReplyError(
750
+ `${error.message} (this was the whole-file request made after the edits for ${target.path} were refused: ${failedEdits.summary.text})`,
751
+ `${error.summary} (the whole-file request, made after the edits were refused)`
752
+ );
753
+ }
754
+ throw error;
755
+ }
756
+ path = "fallback";
757
+ reason = failedEdits.reason;
529
758
  }
530
- const body = unwrapExecute(envelope, "pilot.codegen");
531
- if (body.outcome !== "GENERATED" || typeof body.newContent !== "string") {
532
- throw new Error(
533
- `Codegen refused ${target.path}${body.refusalReason ? `: ${body.refusalReason}` : ""}`
759
+ if (newContent === null) throw new Error(`Codegen produced no content for ${target.path}`);
760
+ if (!isNewFile) {
761
+ console.log(
762
+ JSON.stringify({
763
+ event: "codegen.edits_path",
764
+ targetPath: target.path,
765
+ path,
766
+ ...reason ? { reason } : {},
767
+ ...editCount !== void 0 ? { editCount } : {},
768
+ ...failedEdits && failedEdits.summary.matchCounts.length > 0 ? { matchCounts: failedEdits.summary.matchCounts } : {},
769
+ lineEnding
770
+ })
534
771
  );
535
772
  }
536
773
  if (!isNewFile) {
537
- const suspectReason = looksLikeTruncatedRewrite(target, currentContent, body.newContent);
774
+ const suspectReason = looksLikeTruncatedRewrite(target, currentContent, newContent);
538
775
  if (suspectReason) {
539
776
  throw new Error(
540
777
  `Codegen output for ${target.path} looks like a truncated/abbreviated rewrite (${suspectReason}) \u2014 refusing to apply it. Interim guard per \xA716.7; the AI Runtime's own output-cap check is not live yet.`
@@ -547,11 +784,11 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
547
784
  targetPath: target.path,
548
785
  kind: target.kind,
549
786
  inputLength: currentContent.length,
550
- outputLength: body.newContent.length,
551
- removedLines: isNewFile ? null : countRemovedLines(currentContent, body.newContent)
787
+ outputLength: newContent.length,
788
+ removedLines: isNewFile ? null : countRemovedLines(currentContent, newContent)
552
789
  })
553
790
  );
554
- results.push({ targetPath: target.path, newContent: body.newContent });
791
+ results.push({ targetPath: target.path, newContent });
555
792
  }
556
793
  return results;
557
794
  }
@@ -961,6 +1198,14 @@ function firstXmlIn(dir, namePattern) {
961
1198
  const match = entries.find((e) => namePattern.test(e));
962
1199
  return match ? (0, import_node_path3.join)(dir, match) : null;
963
1200
  }
1201
+ function readTestReportBytes(cwd = process.cwd()) {
1202
+ try {
1203
+ const report = findJUnitReport(cwd);
1204
+ return report ? (0, import_node_fs4.readFileSync)(report) : null;
1205
+ } catch {
1206
+ return null;
1207
+ }
1208
+ }
964
1209
  function findJUnitReport(cwd = process.cwd()) {
965
1210
  return walkForJUnitByName(cwd) ?? firstXmlIn((0, import_node_path3.join)(cwd, "test-results"), /\.xml$/) ?? firstXmlIn((0, import_node_path3.join)(cwd, "reports"), /^junit.*\.xml$/) ?? firstXmlIn((0, import_node_path3.join)(cwd, "target", "surefire-reports"), /\.xml$/);
966
1211
  }
@@ -986,8 +1231,8 @@ async function reportToQuikTest(params) {
986
1231
  try {
987
1232
  console.log(`reporting ${report}`);
988
1233
  const ciUrl = buildCiUrl(githubServerUrl, githubRepository, githubRunId);
989
- const { readFileSync: readFileSync6 } = await import("node:fs");
990
- const body = readFileSync6(report);
1234
+ const { readFileSync: readFileSync7 } = await import("node:fs");
1235
+ const body = readFileSync7(report);
991
1236
  let url = `${resultsUrl}?ciUrl=${encodeURIComponent(ciUrl)}&automationIdFormat=bracket`;
992
1237
  if (generatedFiles) {
993
1238
  url += `&generatedFiles=${encodeURIComponent(generatedFiles.join(","))}`;
@@ -1077,9 +1322,11 @@ function parseJUnitOutcomes(xml) {
1077
1322
  TESTCASE_RE.lastIndex = 0;
1078
1323
  while (match = TESTCASE_RE.exec(xml)) {
1079
1324
  const [, attrs, body = ""] = match;
1080
- const name = NAME_ATTR_RE.exec(attrs)?.[1];
1081
- if (!name) continue;
1082
- const classname = CLASSNAME_ATTR_RE.exec(attrs)?.[1];
1325
+ const rawName = NAME_ATTR_RE.exec(attrs)?.[1];
1326
+ if (!rawName) continue;
1327
+ const name = unescapeXmlEntities(rawName);
1328
+ const rawClassname = CLASSNAME_ATTR_RE.exec(attrs)?.[1];
1329
+ const classname = rawClassname === void 0 ? void 0 : unescapeXmlEntities(rawClassname);
1083
1330
  const id = classname ? `${classname}::${name}` : name;
1084
1331
  const failed = FAILURE_CHILD_RE.test(body);
1085
1332
  outcomes.set(id, !failed);
@@ -1360,8 +1607,110 @@ async function runBaselineCheck(params) {
1360
1607
  }
1361
1608
  }
1362
1609
 
1363
- // src/scopeCheck.ts
1610
+ // src/failureArtifact.ts
1364
1611
  var import_node_fs7 = require("node:fs");
1612
+ var import_node_path5 = require("node:path");
1613
+
1614
+ // src/failureReason.ts
1615
+ var MAX_FAILURE_REASON = 1e3;
1616
+ var MIN_SECRET_LENGTH = 8;
1617
+ var CREDENTIAL_PATTERNS = [
1618
+ /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}/g,
1619
+ /\bgithub_pat_[A-Za-z0-9_]{20,}/g,
1620
+ /x-access-token:[^@\s'"]+/gi,
1621
+ // Only the header form ("Authorization: basic <token>"), or a bearer value that contains a digit: a bare
1622
+ // "basic authentication" or "bearer of the news" is prose, not a credential.
1623
+ /\bauthorization:\s*(?:basic|bearer|token)\s+[^\s'"]{8,}/gi,
1624
+ /\bbearer\s+(?=[A-Za-z0-9._~+/=-]*\d)[A-Za-z0-9._~+/=-]{12,}/gi,
1625
+ // A signed JWT is three dot-separated segments; QuikPilot's own run token is an encrypted JWT (alg dir) of five, the
1626
+ // second one empty. The run token is also matched by exact value, but one that has expired or rotated is not the
1627
+ // current value, so its shape has to catch it.
1628
+ /\beyJ[A-Za-z0-9_-]{8,}(?:\.[A-Za-z0-9_-]*){2,4}/g
1629
+ ];
1630
+ function failureSummary(err) {
1631
+ if (err instanceof RuntimeReplyError) return err.summary;
1632
+ return err instanceof Error ? err.message : String(err);
1633
+ }
1634
+ function scrubSecrets(message, secrets = []) {
1635
+ let text = message;
1636
+ for (const secret of secrets) {
1637
+ if (secret.length >= MIN_SECRET_LENGTH) text = text.split(secret).join("***");
1638
+ }
1639
+ for (const pattern of CREDENTIAL_PATTERNS) text = text.replace(pattern, "***");
1640
+ return text;
1641
+ }
1642
+ function describeFailureReason(message, secrets = []) {
1643
+ let text = scrubSecrets(message, secrets);
1644
+ text = text.replace(/\s*\n\s*/g, " | ").trim();
1645
+ return text.length > MAX_FAILURE_REASON ? `${text.slice(0, MAX_FAILURE_REASON)}\u2026` : text;
1646
+ }
1647
+
1648
+ // src/failureArtifact.ts
1649
+ var FAILURE_ARTIFACT_DIR = "quikpilot-failure";
1650
+ var REPORT_COPY = "final-test-report.xml";
1651
+ var MAX_REPORT_BYTES = 5 * 1024 * 1024;
1652
+ var MAX_FILES = 200;
1653
+ var RUNNER_VERSION = true ? "9.0.0" : "dev";
1654
+ function safeDestination(base, repoPath) {
1655
+ if (!repoPath || (0, import_node_path5.isAbsolute)(repoPath)) return null;
1656
+ const destination = (0, import_node_path5.resolve)(base, repoPath);
1657
+ const rel = (0, import_node_path5.relative)(base, destination);
1658
+ if (!rel || rel.startsWith("..") || (0, import_node_path5.isAbsolute)(rel)) return null;
1659
+ return destination;
1660
+ }
1661
+ function writeFailureArtifact(input) {
1662
+ if (input.results.length === 0) return { written: false };
1663
+ try {
1664
+ const base = (0, import_node_path5.resolve)(input.cwd, FAILURE_ARTIFACT_DIR);
1665
+ (0, import_node_fs7.rmSync)(base, { recursive: true, force: true });
1666
+ (0, import_node_fs7.mkdirSync)(base, { recursive: true });
1667
+ const files = [];
1668
+ const skipped = [];
1669
+ for (const result of input.results) {
1670
+ const destination = files.length < MAX_FILES ? safeDestination(base, result.targetPath) : null;
1671
+ if (!destination) {
1672
+ skipped.push(result.targetPath);
1673
+ continue;
1674
+ }
1675
+ (0, import_node_fs7.mkdirSync)((0, import_node_path5.dirname)(destination), { recursive: true });
1676
+ (0, import_node_fs7.writeFileSync)(destination, result.newContent);
1677
+ files.push({ path: result.targetPath, bytes: Buffer.byteLength(result.newContent) });
1678
+ }
1679
+ let testReport = "none";
1680
+ if (input.testReport) {
1681
+ if (input.testReport.byteLength > MAX_REPORT_BYTES) {
1682
+ testReport = "too-large";
1683
+ } else {
1684
+ (0, import_node_fs7.writeFileSync)((0, import_node_path5.join)(base, REPORT_COPY), input.testReport);
1685
+ testReport = "included";
1686
+ }
1687
+ }
1688
+ const manifest = {
1689
+ runId: input.runId,
1690
+ issueKey: input.issueKey,
1691
+ runnerVersion: RUNNER_VERSION,
1692
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1693
+ targets: input.targets.map((t) => ({
1694
+ path: t.path,
1695
+ kind: t.kind,
1696
+ ...t.automationIds && t.automationIds.length > 0 ? { automationIds: t.automationIds } : {}
1697
+ })),
1698
+ files,
1699
+ skipped,
1700
+ testReport,
1701
+ failureReasons: input.failureReasons
1702
+ };
1703
+ const text = JSON.stringify(manifest, (_key, value) => typeof value === "string" ? scrubSecrets(value, input.secrets) : value, 2);
1704
+ (0, import_node_fs7.writeFileSync)((0, import_node_path5.join)(base, "manifest.json"), text);
1705
+ return { written: (0, import_node_fs7.existsSync)((0, import_node_path5.join)(base, "manifest.json")) };
1706
+ } catch (err) {
1707
+ console.warn(`::warning::Could not keep the failed run's files: ${err instanceof Error ? err.message : String(err)}`);
1708
+ return { written: false };
1709
+ }
1710
+ }
1711
+
1712
+ // src/scopeCheck.ts
1713
+ var import_node_fs8 = require("node:fs");
1365
1714
  var ROUTE_METHOD_RE = /\b(GET|POST|PUT|PATCH|DELETE)\b/i;
1366
1715
  var ROUTE_PATH_RE = /\/[A-Za-z0-9_\-{}[\]]+(?:\/[A-Za-z0-9_\-:{}[\]]+)+/;
1367
1716
  var ROUTE_EXPORT_RE = /\bexport\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\s*\(/;
@@ -1418,7 +1767,7 @@ function checkApiScope(results, targets, apiChanges) {
1418
1767
  } else {
1419
1768
  let currentContent;
1420
1769
  try {
1421
- currentContent = (0, import_node_fs7.readFileSync)(targetPath, "utf8");
1770
+ currentContent = (0, import_node_fs8.readFileSync)(targetPath, "utf8");
1422
1771
  } catch {
1423
1772
  currentContent = "";
1424
1773
  }
@@ -1437,7 +1786,7 @@ function describeScopeViolations(check) {
1437
1786
  }
1438
1787
 
1439
1788
  // src/targeting.ts
1440
- var import_node_path5 = require("node:path");
1789
+ var import_node_path6 = require("node:path");
1441
1790
  var CONTRACT_VERSION2 = "test-targets-1";
1442
1791
  function listWorkspaceFiles(workspaceRoot) {
1443
1792
  return listFiles(workspaceRoot ?? ".");
@@ -1466,15 +1815,15 @@ function validateTarget(target, workspaceRoot, workspaceListing, deniedPaths) {
1466
1815
  }
1467
1816
  return;
1468
1817
  }
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)) {
1818
+ const root = (0, import_node_path6.resolve)(workspaceRoot ?? ".");
1819
+ const rel = (0, import_node_path6.relative)(root, (0, import_node_path6.resolve)(target.path));
1820
+ if (rel.startsWith("..") || (0, import_node_path6.isAbsolute)(rel)) {
1472
1821
  throw new Error(
1473
1822
  `pilot.targeting named a NEW_FILE path that escapes the workspace root: ${target.path}`
1474
1823
  );
1475
1824
  }
1476
1825
  }
1477
- async function runTargeting(gateway, design, workspaceRoot, workspaceListing, requiredTests, deniedPaths, testDiscovery) {
1826
+ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, requiredTests, deniedPaths, testDiscovery, outOfScope = []) {
1478
1827
  const contextData = {
1479
1828
  // NESTED, NOT FLATTENED — a second vocabulary for the design shape is exactly what
1480
1829
  // QUIKTR-1034 cost.
@@ -1489,6 +1838,9 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, re
1489
1838
  if (deniedPaths.length > 0) {
1490
1839
  contextData.deniedPaths = deniedPaths;
1491
1840
  }
1841
+ if (outOfScope.length > 0) {
1842
+ contextData.outOfScope = outOfScope;
1843
+ }
1492
1844
  contextData.testDiscovery = testDiscovery;
1493
1845
  const res = await fetchWithRetry(`${gateway.baseUrl}/ai/execute`, {
1494
1846
  method: "POST",
@@ -1511,16 +1863,28 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, re
1511
1863
  return { outcome: "NO_TARGET", reason: body.noTargetReason ?? null };
1512
1864
  }
1513
1865
  if (body.outcome !== "TARGETED" || !Array.isArray(body.targets)) {
1514
- throw new Error(`pilot.targeting returned an unexpected shape: ${JSON.stringify(body)}`);
1866
+ throw new RuntimeReplyError(
1867
+ `pilot.targeting returned an unexpected shape: ${JSON.stringify(body)}`,
1868
+ "pilot.targeting returned an unexpected shape"
1869
+ );
1515
1870
  }
1516
1871
  const listing = new Set(workspaceListing);
1517
1872
  for (const target of body.targets) {
1518
1873
  validateTarget(target, workspaceRoot, listing, deniedPaths);
1519
1874
  }
1875
+ const correctedKinds = [];
1876
+ const targets = body.targets.map((target) => {
1877
+ if (target.kind === "NEW_FILE" && listing.has(target.path)) {
1878
+ correctedKinds.push(target.path);
1879
+ return { ...target, kind: "EXISTING" };
1880
+ }
1881
+ return target;
1882
+ });
1520
1883
  return {
1521
1884
  outcome: "TARGETED",
1522
- targets: body.targets,
1523
- skippedCases: body.skippedCases ?? []
1885
+ targets,
1886
+ skippedCases: body.skippedCases ?? [],
1887
+ ...correctedKinds.length > 0 ? { correctedKinds } : {}
1524
1888
  };
1525
1889
  }
1526
1890
 
@@ -1683,6 +2047,19 @@ async function exchangeForRunToken(tokenUrl, oidcToken) {
1683
2047
  return body.data.runToken;
1684
2048
  }
1685
2049
 
2050
+ // src/vanishedTests.ts
2051
+ var MAX_VANISHED_NAMES = 50;
2052
+ function findVanishedTests(baseline, after) {
2053
+ if (!baseline.results || !after.results) return null;
2054
+ const missing = [...baseline.results.keys()].filter((id) => !after.results.has(id));
2055
+ if (missing.length === 0) return null;
2056
+ return { names: missing.slice(0, MAX_VANISHED_NAMES), total: missing.length };
2057
+ }
2058
+ function describeVanishedTests(vanished) {
2059
+ const noun = vanished.total === 1 ? "1 test" : `${vanished.total} tests`;
2060
+ return `${noun} that ran before this change ${vanished.total === 1 ? "is" : "are"} missing from the final test report; the pull request lists ${vanished.total === 1 ? "it" : "them"}.`;
2061
+ }
2062
+
1686
2063
  // src/run.ts
1687
2064
  async function run(env2) {
1688
2065
  let hardFailure = false;
@@ -1690,16 +2067,27 @@ async function run(env2) {
1690
2067
  let branch;
1691
2068
  let runToken;
1692
2069
  const warnings = [];
2070
+ const failureReasons = [];
2071
+ const recordFailure = (err) => {
2072
+ const message = err instanceof Error ? err.message : String(err);
2073
+ console.error(message);
2074
+ const reason = describeFailureReason(failureSummary(err), runToken ? [runToken] : []);
2075
+ if (reason) failureReasons.push(reason);
2076
+ };
1693
2077
  let targeted = false;
1694
2078
  let generatedFilePaths = [];
2079
+ let generatedResults = [];
2080
+ let finalTestReport = null;
1695
2081
  let targetingResult;
1696
2082
  let baselineFindings;
2083
+ let vanishedTests;
1697
2084
  const contextLog = [];
1698
2085
  try {
1699
2086
  runToken = await exchangeForRunToken(env2.tokenUrl, await fetchGithubOidcToken());
1700
2087
  packet = await fetchPacket(env2.packetUrl, runToken, env2.githubRunId);
1701
2088
  checkoutTargetBranch(packet.targetBranch);
1702
2089
  const workspaceListing = listWorkspaceFiles(packet.workspaceRoot);
2090
+ const outOfScope = (packet.outOfScope ?? []).map((entry) => entry.trim()).filter((entry) => entry.length > 0);
1703
2091
  const targeting = await runTargeting(
1704
2092
  packet.gateway,
1705
2093
  packet.design,
@@ -1707,9 +2095,18 @@ async function run(env2) {
1707
2095
  workspaceListing,
1708
2096
  packet.requiredTests,
1709
2097
  packet.guardrails.deniedPaths,
1710
- packet.testDiscovery
2098
+ packet.testDiscovery,
2099
+ outOfScope
1711
2100
  );
1712
2101
  targetingResult = targeting;
2102
+ if (targeting.outcome === "TARGETED") {
2103
+ for (const path of targeting.correctedKinds ?? []) {
2104
+ const w = `QuikPilot named ${path} as a new file, but it already exists in the repository. It was treated as an existing file so its current content was kept in view.`;
2105
+ warnings.push(w);
2106
+ console.warn(`QuikPilot targeting warning (not blocking): ${w}`);
2107
+ console.log(JSON.stringify({ event: "targeting.kind_corrected", path }));
2108
+ }
2109
+ }
1713
2110
  if (targeting.outcome === "NO_TARGET") {
1714
2111
  writePhase0Marker(env2.runId);
1715
2112
  runCommand(packet.commands.setup, "Setup");
@@ -1755,9 +2152,11 @@ async function run(env2) {
1755
2152
  packet.guardrails.deniedPaths,
1756
2153
  judgedByPool,
1757
2154
  warnings,
1758
- contextLog
2155
+ contextLog,
2156
+ outOfScope
1759
2157
  )
1760
2158
  ]);
2159
+ generatedResults = results;
1761
2160
  const depCheck = checkImportDependencies(
1762
2161
  results,
1763
2162
  readDeclaredDependencies(results, packet.workspaceRoot),
@@ -1803,6 +2202,13 @@ async function run(env2) {
1803
2202
  }
1804
2203
  runCommand(packet.commands.lint, "Lint");
1805
2204
  const after = captureTestSnapshot(packet.commands.test);
2205
+ finalTestReport = readTestReportBytes();
2206
+ vanishedTests = findVanishedTests(baseline, after) ?? void 0;
2207
+ if (vanishedTests) {
2208
+ const w = describeVanishedTests(vanishedTests);
2209
+ warnings.push(w);
2210
+ console.warn(`QuikPilot test warning (not blocking): ${w}`);
2211
+ }
1806
2212
  const check = checkForRegressions(baseline, after);
1807
2213
  if (check.blocked) {
1808
2214
  throw new Error(
@@ -1818,7 +2224,7 @@ async function run(env2) {
1818
2224
  }
1819
2225
  } catch (err) {
1820
2226
  hardFailure = true;
1821
- console.error(err instanceof Error ? err.message : String(err));
2227
+ recordFailure(err);
1822
2228
  }
1823
2229
  if (!runToken) {
1824
2230
  console.error("No run token \u2014 the OIDC exchange failed before QuikTrack could be reached.");
@@ -1855,7 +2261,23 @@ async function run(env2) {
1855
2261
  branch = result.branch;
1856
2262
  } catch (err) {
1857
2263
  hardFailure = true;
1858
- console.error(err instanceof Error ? err.message : String(err));
2264
+ recordFailure(err);
2265
+ }
2266
+ }
2267
+ if (hardFailure && generatedResults.length > 0) {
2268
+ try {
2269
+ writeFailureArtifact({
2270
+ cwd: process.cwd(),
2271
+ runId: env2.runId,
2272
+ issueKey: packet?.issueKey ?? null,
2273
+ targets: targetingResult?.outcome === "TARGETED" ? targetingResult.targets : [],
2274
+ results: generatedResults,
2275
+ testReport: finalTestReport,
2276
+ failureReasons,
2277
+ secrets: runToken ? [runToken] : []
2278
+ });
2279
+ } catch (err) {
2280
+ console.warn(`::warning::Could not keep the failed run's files: ${err instanceof Error ? err.message : String(err)}`);
1859
2281
  }
1860
2282
  }
1861
2283
  await reportBack({
@@ -1866,7 +2288,9 @@ async function run(env2) {
1866
2288
  warnings,
1867
2289
  // Omitted when targeting itself never produced a result (nothing exists to record).
1868
2290
  targeting: targetingResult ? buildTargetingRecord(targetingResult, contextLog) : void 0,
1869
- baselineFindings
2291
+ baselineFindings,
2292
+ vanishedTests,
2293
+ unresolved: failureReasons.length > 0 ? failureReasons : void 0
1870
2294
  });
1871
2295
  return { hardFailure };
1872
2296
  }