@quikit/quikpilot-runner 8.2.8 → 9.1.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,13 +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, unresolved } = 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
80
  if (unresolved && unresolved.length > 0) body.unresolved = unresolved;
81
81
  if (targeting) body.targeting = targeting;
82
82
  if (baselineFindings) body.baselineFindings = baselineFindings;
83
+ if (vanishedTests) body.vanishedTests = vanishedTests;
83
84
  const res = await fetch(callbackUrl, {
84
85
  method: "POST",
85
86
  headers: { Authorization: `Bearer ${runToken}`, "Content-Type": "application/json" },
@@ -111,20 +112,20 @@ function runTestCommand(command) {
111
112
  }
112
113
  function runTestCommandAsync(command) {
113
114
  if (!command) return Promise.resolve("success");
114
- return new Promise((resolve4) => {
115
+ return new Promise((resolve5) => {
115
116
  const child = (0, import_node_child_process.spawn)(command, { shell: "/bin/bash", stdio: "inherit" });
116
- child.on("exit", (code) => resolve4(code === 0 ? "success" : "failure"));
117
- child.on("error", () => resolve4("failure"));
117
+ child.on("exit", (code) => resolve5(code === 0 ? "success" : "failure"));
118
+ child.on("error", () => resolve5("failure"));
118
119
  });
119
120
  }
120
121
  function runCommandWithTimeout(command, timeoutMs) {
121
- return new Promise((resolve4) => {
122
+ return new Promise((resolve5) => {
122
123
  let settled = false;
123
124
  const finish = (outcome) => {
124
125
  if (settled) return;
125
126
  settled = true;
126
127
  clearTimeout(timer);
127
- resolve4(outcome);
128
+ resolve5(outcome);
128
129
  };
129
130
  const child = (0, import_node_child_process.spawn)(command, { shell: "/bin/bash", stdio: "inherit", detached: true });
130
131
  const timer = setTimeout(() => {
@@ -229,7 +230,7 @@ function commitAndPush(branch, runId, installationToken, targeted, issueKey, iss
229
230
 
230
231
  // src/codegen.ts
231
232
  var import_node_fs = require("node:fs");
232
- var import_node_path = require("node:path");
233
+ var import_node_path2 = require("node:path");
233
234
 
234
235
  // src/deniedPaths.ts
235
236
  function globToRegExp(pattern) {
@@ -251,6 +252,96 @@ function findDeniedContextPath(contextPaths, patterns) {
251
252
  return null;
252
253
  }
253
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
+
254
345
  // src/runtimeReplyError.ts
255
346
  var RuntimeReplyError = class extends Error {
256
347
  summary;
@@ -291,6 +382,315 @@ function unwrapExecute(envelope, useCase) {
291
382
  return envelope.structuredJson;
292
383
  }
293
384
 
385
+ // src/importContext.ts
386
+ var import_node_path = require("node:path");
387
+ var REGEX_PRECEDING_KEYWORDS = /* @__PURE__ */ new Set([
388
+ "return",
389
+ "typeof",
390
+ "instanceof",
391
+ "in",
392
+ "of",
393
+ "new",
394
+ "delete",
395
+ "void",
396
+ "throw",
397
+ "case",
398
+ "do",
399
+ "else",
400
+ "yield",
401
+ "await"
402
+ ]);
403
+ var SPACE_CODES = /* @__PURE__ */ new Set([32, 9, 10, 11, 12, 13, 160, 5760, 8232, 8233, 8239, 8287, 12288, 65279]);
404
+ var isSpace = (c) => {
405
+ const code = c.charCodeAt(0);
406
+ return SPACE_CODES.has(code) || code >= 8192 && code <= 8202;
407
+ };
408
+ var isIdStart = (c) => !isSpace(c) && (/[A-Za-z_$]/.test(c) || c.charCodeAt(0) > 127);
409
+ var isIdPart = (c) => !isSpace(c) && (/[A-Za-z0-9_$]/.test(c) || c.charCodeAt(0) > 127);
410
+ function tokenize(src) {
411
+ const out = [];
412
+ const n = src.length;
413
+ function regexAllowed() {
414
+ const prev = out[out.length - 1];
415
+ if (!prev) return true;
416
+ if (prev.k === "p") return prev.v !== ")" && prev.v !== "]" && prev.v !== "}";
417
+ if (prev.k === "id") return REGEX_PRECEDING_KEYWORDS.has(prev.v);
418
+ return false;
419
+ }
420
+ function scan(start, inSubstitution) {
421
+ let i = start;
422
+ let depth = 0;
423
+ while (i < n) {
424
+ const c = src[i];
425
+ if (isSpace(c)) {
426
+ i++;
427
+ continue;
428
+ }
429
+ if (c === "/" && src[i + 1] === "/") {
430
+ while (i < n && src[i] !== "\n") i++;
431
+ continue;
432
+ }
433
+ if (c === "/" && src[i + 1] === "*") {
434
+ const end = src.indexOf("*/", i + 2);
435
+ i = end === -1 ? n : end + 2;
436
+ continue;
437
+ }
438
+ if (c === "'" || c === '"') {
439
+ let j = i + 1;
440
+ let closed = false;
441
+ while (j < n) {
442
+ const d = src[j];
443
+ if (d === "\\") {
444
+ j += 2;
445
+ continue;
446
+ }
447
+ if (d === c) {
448
+ closed = true;
449
+ break;
450
+ }
451
+ if (d === "\n" || d === "\r") break;
452
+ j++;
453
+ }
454
+ if (closed) out.push({ k: "str", v: src.slice(i + 1, j) });
455
+ i = closed ? j + 1 : j;
456
+ continue;
457
+ }
458
+ if (c === "`") {
459
+ let j = i + 1;
460
+ let hasSubstitution = false;
461
+ let closed = false;
462
+ const textStart = j;
463
+ let textEnd = j;
464
+ while (j < n) {
465
+ const d = src[j];
466
+ if (d === "\\") {
467
+ j += 2;
468
+ continue;
469
+ }
470
+ if (d === "`") {
471
+ closed = true;
472
+ textEnd = hasSubstitution ? textEnd : j;
473
+ break;
474
+ }
475
+ if (d === "$" && src[j + 1] === "{") {
476
+ hasSubstitution = true;
477
+ j = scan(j + 2, true);
478
+ continue;
479
+ }
480
+ j++;
481
+ }
482
+ if (closed) out.push(hasSubstitution ? { k: "tpl", v: "" } : { k: "str", v: src.slice(textStart, textEnd) });
483
+ i = closed ? j + 1 : n;
484
+ continue;
485
+ }
486
+ if (c === "/" && regexAllowed()) {
487
+ let j = i + 1;
488
+ let inClass = false;
489
+ let closed = false;
490
+ while (j < n) {
491
+ const d = src[j];
492
+ if (d === "\\") {
493
+ j += 2;
494
+ continue;
495
+ }
496
+ if (d === "\n" || d === "\r") break;
497
+ if (d === "[") inClass = true;
498
+ else if (d === "]") inClass = false;
499
+ else if (d === "/" && !inClass) {
500
+ closed = true;
501
+ break;
502
+ }
503
+ j++;
504
+ }
505
+ if (closed) {
506
+ j++;
507
+ while (j < n && /[a-z]/i.test(src[j])) j++;
508
+ out.push({ k: "num", v: "/re/" });
509
+ i = j;
510
+ continue;
511
+ }
512
+ out.push({ k: "p", v: "/" });
513
+ i++;
514
+ continue;
515
+ }
516
+ if (/[0-9]/.test(c)) {
517
+ let j = i + 1;
518
+ while (j < n && /[0-9A-Za-z_.]/.test(src[j])) j++;
519
+ out.push({ k: "num", v: src.slice(i, j) });
520
+ i = j;
521
+ continue;
522
+ }
523
+ if (isIdStart(c)) {
524
+ let j = i + 1;
525
+ while (j < n && isIdPart(src[j])) j++;
526
+ out.push({ k: "id", v: src.slice(i, j) });
527
+ i = j;
528
+ continue;
529
+ }
530
+ if (inSubstitution) {
531
+ if (c === "{") depth++;
532
+ else if (c === "}") {
533
+ if (depth === 0) return i + 1;
534
+ depth--;
535
+ }
536
+ }
537
+ out.push({ k: "p", v: c });
538
+ i++;
539
+ }
540
+ return n;
541
+ }
542
+ scan(0, false);
543
+ return out;
544
+ }
545
+ function extractImportSpecifiers(source) {
546
+ const t = tokenize(source);
547
+ const found = [];
548
+ const seen = /* @__PURE__ */ new Set();
549
+ const add = (s) => {
550
+ if (!seen.has(s)) {
551
+ seen.add(s);
552
+ found.push(s);
553
+ }
554
+ };
555
+ const isP = (tok, v) => tok?.k === "p" && tok.v === v;
556
+ for (let i = 0; i < t.length; i++) {
557
+ const tok = t[i];
558
+ if (tok.k !== "id") continue;
559
+ if (isP(t[i - 1], ".")) continue;
560
+ if (tok.v === "require") {
561
+ if (isP(t[i + 1], "(") && t[i + 2]?.k === "str" && isP(t[i + 3], ")")) add(t[i + 2].v);
562
+ continue;
563
+ }
564
+ if (tok.v !== "import" && tok.v !== "export") continue;
565
+ const next = t[i + 1];
566
+ if (tok.v === "import") {
567
+ if (next?.k === "str") {
568
+ add(next.v);
569
+ continue;
570
+ }
571
+ if (isP(next, "(")) {
572
+ if (t[i + 2]?.k === "str" && (isP(t[i + 3], ")") || isP(t[i + 3], ","))) add(t[i + 2].v);
573
+ continue;
574
+ }
575
+ }
576
+ for (let j = i + 1; j < t.length; j++) {
577
+ const c = t[j];
578
+ if (c.k === "id") {
579
+ if (c.v === "import" || c.v === "export") break;
580
+ if (c.v === "from" && t[j + 1]?.k === "str") {
581
+ add(t[j + 1].v);
582
+ break;
583
+ }
584
+ continue;
585
+ }
586
+ if (c.k === "p" && (c.v === "{" || c.v === "}" || c.v === "," || c.v === "*")) continue;
587
+ break;
588
+ }
589
+ }
590
+ return found;
591
+ }
592
+ function supportsImportScan(path) {
593
+ return /\.(?:[cm]?[jt]sx?)$/i.test(path);
594
+ }
595
+ var CODE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".json"];
596
+ var ASSET_EXTS = /* @__PURE__ */ new Set([
597
+ ".css",
598
+ ".scss",
599
+ ".sass",
600
+ ".less",
601
+ ".svg",
602
+ ".png",
603
+ ".jpg",
604
+ ".jpeg",
605
+ ".gif",
606
+ ".webp",
607
+ ".avif",
608
+ ".ico",
609
+ ".bmp",
610
+ ".woff",
611
+ ".woff2",
612
+ ".ttf",
613
+ ".otf",
614
+ ".eot",
615
+ ".mp3",
616
+ ".mp4",
617
+ ".webm",
618
+ ".wav",
619
+ ".pdf",
620
+ ".zip",
621
+ ".wasm"
622
+ ]);
623
+ var TS_SIBLINGS = { ".js": [".ts", ".tsx"], ".jsx": [".tsx"], ".mjs": [".mts"], ".cjs": [".cts"] };
624
+ function resolveImport(fromPath, specifier, listing) {
625
+ if (/^(?:@\/|~\/|#|\$|\/)/.test(specifier)) return { kind: "unresolved", reason: "alias" };
626
+ const clean = specifier.replace(/[?#].*$/, "");
627
+ const relative4 = clean === "." || clean === ".." || clean.startsWith("./") || clean.startsWith("../");
628
+ if (!relative4) return { kind: "external" };
629
+ const base = import_node_path.posix.normalize(import_node_path.posix.join(import_node_path.posix.dirname(fromPath), clean));
630
+ if (base === ".." || base.startsWith("../") || import_node_path.posix.isAbsolute(base)) return { kind: "unresolved", reason: "not found" };
631
+ const ext = import_node_path.posix.extname(base);
632
+ if (ASSET_EXTS.has(ext.toLowerCase())) return { kind: "unresolved", reason: "asset" };
633
+ const candidates = [];
634
+ if (CODE_EXTS.includes(ext)) {
635
+ candidates.push(base);
636
+ for (const sibling of TS_SIBLINGS[ext] ?? []) candidates.push(base.slice(0, -ext.length) + sibling);
637
+ }
638
+ for (const e of CODE_EXTS) candidates.push(base + e);
639
+ candidates.push(base + ".d.ts");
640
+ for (const e of CODE_EXTS) candidates.push(`${base}/index${e}`);
641
+ for (const candidate of candidates) {
642
+ if (listing.has(candidate)) return { kind: "repo", path: candidate };
643
+ }
644
+ return { kind: "unresolved", reason: "not found", attempted: base };
645
+ }
646
+ var ACTIONABLE_IMPORT_SKIPS = /* @__PURE__ */ new Set(["import: too large", "import: over budget", "import: unreadable"]);
647
+ function collectImportContext(p) {
648
+ const out = { entries: [], skipped: [], importedPaths: [], aliasImports: 0 };
649
+ if (!supportsImportScan(p.targetPath)) return out;
650
+ const seen = new Set(p.alreadyNamed);
651
+ seen.add(p.targetPath);
652
+ let total = p.spentChars;
653
+ for (const specifier of extractImportSpecifiers(p.source)) {
654
+ const resolved = resolveImport(p.targetPath, specifier, p.listing);
655
+ if (resolved.kind === "external") continue;
656
+ if (resolved.kind === "unresolved") {
657
+ if (resolved.reason === "alias") out.aliasImports++;
658
+ else if (resolved.reason === "not found") {
659
+ const path2 = resolved.attempted ?? specifier;
660
+ if (!seen.has(path2)) {
661
+ seen.add(path2);
662
+ out.skipped.push({ path: path2, reason: "import: not in workspace" });
663
+ }
664
+ }
665
+ continue;
666
+ }
667
+ const path = resolved.path;
668
+ if (seen.has(path)) continue;
669
+ seen.add(path);
670
+ if (matchesDeniedPath(path, p.deniedPaths)) {
671
+ out.skipped.push({ path, reason: "import: denied" });
672
+ continue;
673
+ }
674
+ let content;
675
+ try {
676
+ content = p.readFile(path);
677
+ } catch {
678
+ out.skipped.push({ path, reason: "import: unreadable" });
679
+ continue;
680
+ }
681
+ if (content.length > p.maxFileChars) {
682
+ out.skipped.push({ path, reason: "import: too large" });
683
+ } else if (total + content.length > p.maxTotalChars) {
684
+ out.skipped.push({ path, reason: "import: over budget" });
685
+ } else {
686
+ total += content.length;
687
+ out.entries.push({ path, content });
688
+ out.importedPaths.push(path);
689
+ }
690
+ }
691
+ return out;
692
+ }
693
+
294
694
  // src/retryFetch.ts
295
695
  var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([502, 503, 504]);
296
696
  var RETRYABLE_NETWORK_ERROR_CODES = /* @__PURE__ */ new Set(["ECONNRESET", "ETIMEDOUT"]);
@@ -413,6 +813,58 @@ function looksLikeTruncatedRewrite(target, currentContent, newContent) {
413
813
  return null;
414
814
  }
415
815
  var CONTRACT_VERSION = "test-targets-1";
816
+ var CONTRACT_VERSION_EDITS = "edits-1";
817
+ var ANCHOR_REFUSED_CODE = "CODEGEN_ANCHOR_REFUSED";
818
+ function summariseLocalRefusals(refusals) {
819
+ const parts = [];
820
+ const matchCounts = [];
821
+ for (const r of refusals) {
822
+ if (r.kind === "ambiguous") {
823
+ parts.push(`an anchor matched ${r.matchCount} times`);
824
+ if (r.matchCount !== null) matchCounts.push(r.matchCount);
825
+ } else if (r.kind === "no_match") {
826
+ parts.push("an anchor matched nothing");
827
+ } else if (r.kind === "overlap") {
828
+ parts.push(`edits ${r.edits.join(" and ")} overlap`);
829
+ } else if (r.kind === "no_edits") {
830
+ parts.push("the reply had no edits");
831
+ } else {
832
+ parts.push("an edit was missing its anchor or replacement");
833
+ }
834
+ }
835
+ return { text: [...new Set(parts)].join("; "), matchCounts };
836
+ }
837
+ function summariseRuntimeRefusal(error) {
838
+ const text = typeof error === "string" ? error : "";
839
+ const matchCounts = [...text.matchAll(/anchor matched (\d+) times/g)].map((m) => Number(m[1]));
840
+ const parts = matchCounts.map((n) => `an anchor matched ${n} times`);
841
+ if (/no match/i.test(text)) parts.push("an anchor matched nothing");
842
+ if (/overlap/i.test(text)) parts.push("edits overlapped");
843
+ if (/empty anchor|missing a string/i.test(text)) parts.push("an edit was missing its anchor or replacement");
844
+ return { text: parts.length > 0 ? [...new Set(parts)].join("; ") : "the runtime could not place the edits", matchCounts };
845
+ }
846
+ function throwOnTerminalRefusal(envelope, targetPath) {
847
+ if (typeof envelope?.errorCode === "string" && NOT_RETRIED_CODES.has(envelope.errorCode)) {
848
+ const detail = envelope.error ? `: ${envelope.error}` : "";
849
+ throw new RuntimeReplyError(
850
+ `Codegen refused ${targetPath}: ${envelope.errorCode} (not retried)${detail}`,
851
+ `Codegen refused ${targetPath}: ${envelope.errorCode} (not retried)`
852
+ );
853
+ }
854
+ if (envelope?.errorCode === JUDGED_BY_TOO_LARGE_CODE) {
855
+ const detail = envelope.error ? `: ${envelope.error}` : "";
856
+ throw new RuntimeReplyError(
857
+ `Codegen refused ${targetPath}: ${envelope.errorCode} (after narrowing)${detail}`,
858
+ `Codegen refused ${targetPath}: ${envelope.errorCode} (after narrowing)`
859
+ );
860
+ }
861
+ }
862
+ function codegenRefusedError(targetPath, body) {
863
+ return new RuntimeReplyError(
864
+ `Codegen refused ${targetPath}${body.refusalReason ? `: ${body.refusalReason}` : ""}`,
865
+ `Codegen refused ${targetPath}`
866
+ );
867
+ }
416
868
  function buildJudgedByPool(requiredTests, targets, skippedCases) {
417
869
  const claimedIds = /* @__PURE__ */ new Set();
418
870
  for (const t of targets) {
@@ -444,6 +896,8 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
444
896
  for (const target of targets) {
445
897
  const isNewFile = target.kind === "NEW_FILE";
446
898
  const currentContent = isNewFile ? "" : (0, import_node_fs.readFileSync)(target.path, "utf8");
899
+ const lineEnding = isNewFile ? "LF" : detectLineEnding(currentContent);
900
+ const wantsEdits = !isNewFile && lineEnding !== "mixed";
447
901
  const contextPaths = target.contextPaths ?? [];
448
902
  const deniedContext = findDeniedContextPath(contextPaths, deniedPaths);
449
903
  if (deniedContext) {
@@ -458,28 +912,51 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
458
912
  );
459
913
  }
460
914
  }
461
- const namedContents = contextPaths.map((path) => ({ path, content: (0, import_node_fs.readFileSync)(path, "utf8") }));
462
- const added = collectRunContext(target, targets, new Set(contextPaths), deniedPaths);
463
- const contextContents = [...namedContents, ...added.entries];
464
- for (const s of added.skipped) {
915
+ const namedContents = contextPaths.map((path2) => ({ path: path2, content: (0, import_node_fs.readFileSync)(path2, "utf8") }));
916
+ const runFiles = collectRunContext(target, targets, new Set(contextPaths), deniedPaths);
917
+ for (const s of runFiles.skipped) {
465
918
  warnings.push(`not shown to the test for ${target.path}: ${s.path} (${s.reason})`);
466
919
  }
920
+ const imports = isNewFile ? { entries: [], skipped: [], importedPaths: [], aliasImports: 0 } : collectImportContext({
921
+ targetPath: target.path,
922
+ source: currentContent,
923
+ alreadyNamed: /* @__PURE__ */ new Set([...contextPaths, ...runFiles.entries.map((e) => e.path)]),
924
+ deniedPaths,
925
+ listing,
926
+ readFile: (p) => (0, import_node_fs.readFileSync)(p, "utf8"),
927
+ maxFileChars: MAX_ADDED_FILE_CHARS,
928
+ maxTotalChars: MAX_ADDED_CONTEXT_CHARS,
929
+ spentChars: runFiles.entries.reduce((n, e) => n + e.content.length, 0)
930
+ });
931
+ for (const s of imports.skipped) {
932
+ if (ACTIONABLE_IMPORT_SKIPS.has(s.reason)) {
933
+ warnings.push(`not shown with ${target.path}: ${s.path} (imported by it; ${s.reason.replace(/^import: /, "")})`);
934
+ }
935
+ }
936
+ const added = {
937
+ entries: [...runFiles.entries, ...imports.entries],
938
+ skipped: [...runFiles.skipped, ...imports.skipped]
939
+ };
940
+ const contextContents = [...namedContents, ...added.entries];
467
941
  const contextRecord = {
468
942
  targetPath: target.path,
469
943
  contextPaths,
470
944
  addedPaths: added.entries.map((e) => e.path),
471
- skipped: added.skipped
945
+ skipped: added.skipped,
946
+ ...imports.importedPaths.length > 0 ? { importedPaths: imports.importedPaths } : {},
947
+ ...imports.aliasImports > 0 ? { aliasImports: imports.aliasImports } : {}
472
948
  };
473
949
  console.log(JSON.stringify({ event: "codegen.context", ...contextRecord }));
474
950
  contextLog?.push(contextRecord);
475
951
  const contextData = {
476
952
  targetPath: target.path,
477
- currentContent,
953
+ // Every anchor and replacement is LF, always (Doc 62 §2), so the file is sent as LF.
954
+ currentContent: wantsEdits ? toLf(currentContent) : currentContent,
478
955
  planSteps: target.planStepsImplemented,
479
956
  kind: target.kind,
480
957
  contextContents,
481
958
  // QUIKTR-1083 (§15.6) — required on every call on the versioned path.
482
- contractVersion: CONTRACT_VERSION
959
+ contractVersion: wantsEdits ? CONTRACT_VERSION_EDITS : CONTRACT_VERSION
483
960
  };
484
961
  if (isNewFile) {
485
962
  contextData.workspaceListing = workspaceListing;
@@ -533,6 +1010,7 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
533
1010
  contextData.contextContents = namedContents;
534
1011
  warnings.push(`context for ${target.path} was too large with the run's other files; retried without them`);
535
1012
  contextRecord.addedPaths = [];
1013
+ delete contextRecord.importedPaths;
536
1014
  contextRecord.retriedWithoutAdded = true;
537
1015
  res = await callCodegen();
538
1016
  if (!res.ok) {
@@ -541,29 +1019,91 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
541
1019
  envelope = await res.json();
542
1020
  checkHardFailure(envelope, "pilot.codegen", target.path);
543
1021
  }
544
- if (typeof envelope?.errorCode === "string" && NOT_RETRIED_CODES.has(envelope.errorCode)) {
545
- const detail = envelope.error ? `: ${envelope.error}` : "";
546
- throw new RuntimeReplyError(
547
- `Codegen refused ${target.path}: ${envelope.errorCode} (not retried)${detail}`,
548
- `Codegen refused ${target.path}: ${envelope.errorCode} (not retried)`
549
- );
1022
+ throwOnTerminalRefusal(envelope, target.path);
1023
+ let newContent = null;
1024
+ let failedEdits = null;
1025
+ let editCount;
1026
+ let path = "whole_file";
1027
+ let reason = isNewFile ? "new_file" : wantsEdits ? void 0 : "mixed_line_endings";
1028
+ if (wantsEdits && envelope?.errorCode === ANCHOR_REFUSED_CODE) {
1029
+ failedEdits = { reason: "anchor_refused", summary: summariseRuntimeRefusal(envelope.error) };
1030
+ } else {
1031
+ const body = unwrapExecute(envelope, "pilot.codegen");
1032
+ if (wantsEdits) {
1033
+ if (body.outcome === "REFUSED") throw codegenRefusedError(target.path, body);
1034
+ if (body.outcome !== "GENERATED" || typeof body.newContent === "string") {
1035
+ throw new RuntimeReplyError(
1036
+ `Codegen reply for ${target.path} was not in the edits-1 shape and was not applied`,
1037
+ `Codegen reply for ${target.path} was not in the edits-1 shape`
1038
+ );
1039
+ }
1040
+ const applied = applyEdits(toLf(currentContent), body.edits);
1041
+ if (applied.ok) {
1042
+ newContent = fromLf(applied.content, lineEnding);
1043
+ path = "edits";
1044
+ editCount = applied.editCount;
1045
+ } else if (applied.refusals.some((r) => r.kind === "no_edits")) {
1046
+ throw new RuntimeReplyError(
1047
+ `Codegen reply for ${target.path} carried no edits (not retried)`,
1048
+ `Codegen reply for ${target.path} carried no edits`
1049
+ );
1050
+ } else {
1051
+ failedEdits = { reason: "local_refusal", summary: summariseLocalRefusals(applied.refusals) };
1052
+ }
1053
+ } else {
1054
+ if (body.outcome !== "GENERATED" || typeof body.newContent !== "string") {
1055
+ throw codegenRefusedError(target.path, body);
1056
+ }
1057
+ newContent = body.newContent;
1058
+ }
550
1059
  }
551
- if (envelope?.errorCode === JUDGED_BY_TOO_LARGE_CODE) {
552
- const detail = envelope.error ? `: ${envelope.error}` : "";
553
- throw new RuntimeReplyError(
554
- `Codegen refused ${target.path}: ${envelope.errorCode} (after narrowing)${detail}`,
555
- `Codegen refused ${target.path}: ${envelope.errorCode} (after narrowing)`
1060
+ if (failedEdits) {
1061
+ warnings.push(
1062
+ `edits for ${target.path} could not be placed (${failedEdits.summary.text}); asked again for the whole file`
556
1063
  );
1064
+ contextData.currentContent = currentContent;
1065
+ contextData.contractVersion = CONTRACT_VERSION;
1066
+ try {
1067
+ res = await callCodegen();
1068
+ if (!res.ok) {
1069
+ throw new Error(`Codegen call failed for ${target.path}: ${res.status} ${res.statusText}`);
1070
+ }
1071
+ envelope = await res.json();
1072
+ checkHardFailure(envelope, "pilot.codegen", target.path);
1073
+ throwOnTerminalRefusal(envelope, target.path);
1074
+ const whole = unwrapExecute(envelope, "pilot.codegen");
1075
+ if (whole.outcome !== "GENERATED" || typeof whole.newContent !== "string") {
1076
+ throw codegenRefusedError(target.path, whole);
1077
+ }
1078
+ newContent = whole.newContent;
1079
+ } catch (error) {
1080
+ if (error instanceof RuntimeReplyError) {
1081
+ throw new RuntimeReplyError(
1082
+ `${error.message} (this was the whole-file request made after the edits for ${target.path} were refused: ${failedEdits.summary.text})`,
1083
+ `${error.summary} (the whole-file request, made after the edits were refused)`
1084
+ );
1085
+ }
1086
+ throw error;
1087
+ }
1088
+ path = "fallback";
1089
+ reason = failedEdits.reason;
557
1090
  }
558
- const body = unwrapExecute(envelope, "pilot.codegen");
559
- if (body.outcome !== "GENERATED" || typeof body.newContent !== "string") {
560
- throw new RuntimeReplyError(
561
- `Codegen refused ${target.path}${body.refusalReason ? `: ${body.refusalReason}` : ""}`,
562
- `Codegen refused ${target.path}`
1091
+ if (newContent === null) throw new Error(`Codegen produced no content for ${target.path}`);
1092
+ if (!isNewFile) {
1093
+ console.log(
1094
+ JSON.stringify({
1095
+ event: "codegen.edits_path",
1096
+ targetPath: target.path,
1097
+ path,
1098
+ ...reason ? { reason } : {},
1099
+ ...editCount !== void 0 ? { editCount } : {},
1100
+ ...failedEdits && failedEdits.summary.matchCounts.length > 0 ? { matchCounts: failedEdits.summary.matchCounts } : {},
1101
+ lineEnding
1102
+ })
563
1103
  );
564
1104
  }
565
1105
  if (!isNewFile) {
566
- const suspectReason = looksLikeTruncatedRewrite(target, currentContent, body.newContent);
1106
+ const suspectReason = looksLikeTruncatedRewrite(target, currentContent, newContent);
567
1107
  if (suspectReason) {
568
1108
  throw new Error(
569
1109
  `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.`
@@ -576,24 +1116,24 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
576
1116
  targetPath: target.path,
577
1117
  kind: target.kind,
578
1118
  inputLength: currentContent.length,
579
- outputLength: body.newContent.length,
580
- removedLines: isNewFile ? null : countRemovedLines(currentContent, body.newContent)
1119
+ outputLength: newContent.length,
1120
+ removedLines: isNewFile ? null : countRemovedLines(currentContent, newContent)
581
1121
  })
582
1122
  );
583
- results.push({ targetPath: target.path, newContent: body.newContent });
1123
+ results.push({ targetPath: target.path, newContent });
584
1124
  }
585
1125
  return results;
586
1126
  }
587
1127
  function flushCodegenResults(results) {
588
1128
  for (const { targetPath, newContent } of results) {
589
- (0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(targetPath), { recursive: true });
1129
+ (0, import_node_fs.mkdirSync)((0, import_node_path2.dirname)(targetPath), { recursive: true });
590
1130
  (0, import_node_fs.writeFileSync)(targetPath, newContent);
591
1131
  }
592
1132
  }
593
1133
 
594
1134
  // src/dependencyCheck.ts
595
1135
  var import_node_fs2 = require("node:fs");
596
- var import_node_path2 = require("node:path");
1136
+ var import_node_path3 = require("node:path");
597
1137
  var SCANNABLE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"];
598
1138
  var IMPORT_RE = /(?:\bimport\b[^'"]*?\bfrom\s*|\bimport\s*\(\s*|\bexport\b[^'"]*?\bfrom\s*|\brequire\s*\(\s*|\bimport\s+)(['"])([^'"]+)\1/g;
599
1139
  var NODE_BUILTINS = /* @__PURE__ */ new Set([
@@ -672,8 +1212,8 @@ function dependencyNames(pkg) {
672
1212
  ]);
673
1213
  }
674
1214
  function readDeclaredDependencies(results, workspaceRoot) {
675
- const packageJsonPath = (0, import_node_path2.join)((0, import_node_path2.resolve)(workspaceRoot ?? "."), "package.json");
676
- const generated = results.find((r) => (0, import_node_path2.resolve)(r.targetPath) === packageJsonPath);
1215
+ const packageJsonPath = (0, import_node_path3.join)((0, import_node_path3.resolve)(workspaceRoot ?? "."), "package.json");
1216
+ const generated = results.find((r) => (0, import_node_path3.resolve)(r.targetPath) === packageJsonPath);
677
1217
  let raw;
678
1218
  if (generated) {
679
1219
  raw = generated.newContent;
@@ -743,10 +1283,10 @@ function stripJsonComments(text) {
743
1283
  return out.replace(/,(\s*[}\]])/g, "$1");
744
1284
  }
745
1285
  function resolveAliasPrefixes(results, workspaceRoot) {
746
- const root = (0, import_node_path2.resolve)(workspaceRoot ?? ".");
1286
+ const root = (0, import_node_path3.resolve)(workspaceRoot ?? ".");
747
1287
  for (const filename of ["tsconfig.json", "jsconfig.json"]) {
748
- const configPath = (0, import_node_path2.join)(root, filename);
749
- const generated = results.find((r) => (0, import_node_path2.resolve)(r.targetPath) === configPath);
1288
+ const configPath = (0, import_node_path3.join)(root, filename);
1289
+ const generated = results.find((r) => (0, import_node_path3.resolve)(r.targetPath) === configPath);
750
1290
  let raw;
751
1291
  if (generated) {
752
1292
  raw = generated.newContent;
@@ -953,7 +1493,7 @@ function writePhase0Marker(runId) {
953
1493
 
954
1494
  // src/quiktest.ts
955
1495
  var import_node_fs4 = require("node:fs");
956
- var import_node_path3 = require("node:path");
1496
+ var import_node_path4 = require("node:path");
957
1497
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
958
1498
  function walkForJUnitByName(dir) {
959
1499
  let entries;
@@ -964,7 +1504,7 @@ function walkForJUnitByName(dir) {
964
1504
  }
965
1505
  for (const entry of entries) {
966
1506
  if (SKIP_DIRS.has(entry)) continue;
967
- const full = (0, import_node_path3.join)(dir, entry);
1507
+ const full = (0, import_node_path4.join)(dir, entry);
968
1508
  let stat;
969
1509
  try {
970
1510
  stat = (0, import_node_fs4.statSync)(full);
@@ -988,10 +1528,18 @@ function firstXmlIn(dir, namePattern) {
988
1528
  return null;
989
1529
  }
990
1530
  const match = entries.find((e) => namePattern.test(e));
991
- return match ? (0, import_node_path3.join)(dir, match) : null;
1531
+ return match ? (0, import_node_path4.join)(dir, match) : null;
1532
+ }
1533
+ function readTestReportBytes(cwd = process.cwd()) {
1534
+ try {
1535
+ const report = findJUnitReport(cwd);
1536
+ return report ? (0, import_node_fs4.readFileSync)(report) : null;
1537
+ } catch {
1538
+ return null;
1539
+ }
992
1540
  }
993
1541
  function findJUnitReport(cwd = process.cwd()) {
994
- 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$/);
1542
+ return walkForJUnitByName(cwd) ?? firstXmlIn((0, import_node_path4.join)(cwd, "test-results"), /\.xml$/) ?? firstXmlIn((0, import_node_path4.join)(cwd, "reports"), /^junit.*\.xml$/) ?? firstXmlIn((0, import_node_path4.join)(cwd, "target", "surefire-reports"), /\.xml$/);
995
1543
  }
996
1544
  function buildCiUrl(serverUrl, repository, githubRunId) {
997
1545
  return `${serverUrl}/${repository}/actions/runs/${githubRunId}`;
@@ -1015,8 +1563,8 @@ async function reportToQuikTest(params) {
1015
1563
  try {
1016
1564
  console.log(`reporting ${report}`);
1017
1565
  const ciUrl = buildCiUrl(githubServerUrl, githubRepository, githubRunId);
1018
- const { readFileSync: readFileSync6 } = await import("node:fs");
1019
- const body = readFileSync6(report);
1566
+ const { readFileSync: readFileSync7 } = await import("node:fs");
1567
+ const body = readFileSync7(report);
1020
1568
  let url = `${resultsUrl}?ciUrl=${encodeURIComponent(ciUrl)}&automationIdFormat=bracket`;
1021
1569
  if (generatedFiles) {
1022
1570
  url += `&generatedFiles=${encodeURIComponent(generatedFiles.join(","))}`;
@@ -1046,7 +1594,7 @@ async function reportToQuikTest(params) {
1046
1594
  // src/baselineCheck.ts
1047
1595
  var import_node_child_process3 = require("node:child_process");
1048
1596
  var import_node_fs6 = require("node:fs");
1049
- var import_node_path4 = require("node:path");
1597
+ var import_node_path5 = require("node:path");
1050
1598
 
1051
1599
  // src/regressionCheck.ts
1052
1600
  var import_node_fs5 = require("node:fs");
@@ -1106,9 +1654,11 @@ function parseJUnitOutcomes(xml) {
1106
1654
  TESTCASE_RE.lastIndex = 0;
1107
1655
  while (match = TESTCASE_RE.exec(xml)) {
1108
1656
  const [, attrs, body = ""] = match;
1109
- const name = NAME_ATTR_RE.exec(attrs)?.[1];
1110
- if (!name) continue;
1111
- const classname = CLASSNAME_ATTR_RE.exec(attrs)?.[1];
1657
+ const rawName = NAME_ATTR_RE.exec(attrs)?.[1];
1658
+ if (!rawName) continue;
1659
+ const name = unescapeXmlEntities(rawName);
1660
+ const rawClassname = CLASSNAME_ATTR_RE.exec(attrs)?.[1];
1661
+ const classname = rawClassname === void 0 ? void 0 : unescapeXmlEntities(rawClassname);
1112
1662
  const id = classname ? `${classname}::${name}` : name;
1113
1663
  const failed = FAILURE_CHILD_RE.test(body);
1114
1664
  outcomes.set(id, !failed);
@@ -1230,16 +1780,16 @@ function checkableIds(ids, types) {
1230
1780
  });
1231
1781
  }
1232
1782
  function insideCheckout(cwd, path) {
1233
- const abs = (0, import_node_path4.resolve)(cwd, path);
1234
- const rel = (0, import_node_path4.relative)(cwd, abs);
1235
- return rel === "" || rel.startsWith("..") || (0, import_node_path4.isAbsolute)(rel) ? null : abs;
1783
+ const abs = (0, import_node_path5.resolve)(cwd, path);
1784
+ const rel = (0, import_node_path5.relative)(cwd, abs);
1785
+ return rel === "" || rel.startsWith("..") || (0, import_node_path5.isAbsolute)(rel) ? null : abs;
1236
1786
  }
1237
1787
  function writeForCheck(abs, content) {
1238
1788
  const original = (0, import_node_fs6.existsSync)(abs) ? (0, import_node_fs6.readFileSync)(abs) : null;
1239
1789
  const createdDirs = [];
1240
1790
  if (original === null) {
1241
1791
  const missing = [];
1242
- 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);
1792
+ for (let d = (0, import_node_path5.dirname)(abs); !(0, import_node_fs6.existsSync)(d); d = (0, import_node_path5.dirname)(d)) missing.push(d);
1243
1793
  for (const d of missing.reverse()) {
1244
1794
  (0, import_node_fs6.mkdirSync)(d);
1245
1795
  createdDirs.push(d);
@@ -1389,6 +1939,10 @@ async function runBaselineCheck(params) {
1389
1939
  }
1390
1940
  }
1391
1941
 
1942
+ // src/failureArtifact.ts
1943
+ var import_node_fs7 = require("node:fs");
1944
+ var import_node_path6 = require("node:path");
1945
+
1392
1946
  // src/failureReason.ts
1393
1947
  var MAX_FAILURE_REASON = 1e3;
1394
1948
  var MIN_SECRET_LENGTH = 8;
@@ -1409,18 +1963,86 @@ function failureSummary(err) {
1409
1963
  if (err instanceof RuntimeReplyError) return err.summary;
1410
1964
  return err instanceof Error ? err.message : String(err);
1411
1965
  }
1412
- function describeFailureReason(message, secrets = []) {
1966
+ function scrubSecrets(message, secrets = []) {
1413
1967
  let text = message;
1414
1968
  for (const secret of secrets) {
1415
1969
  if (secret.length >= MIN_SECRET_LENGTH) text = text.split(secret).join("***");
1416
1970
  }
1417
1971
  for (const pattern of CREDENTIAL_PATTERNS) text = text.replace(pattern, "***");
1972
+ return text;
1973
+ }
1974
+ function describeFailureReason(message, secrets = []) {
1975
+ let text = scrubSecrets(message, secrets);
1418
1976
  text = text.replace(/\s*\n\s*/g, " | ").trim();
1419
1977
  return text.length > MAX_FAILURE_REASON ? `${text.slice(0, MAX_FAILURE_REASON)}\u2026` : text;
1420
1978
  }
1421
1979
 
1980
+ // src/failureArtifact.ts
1981
+ var FAILURE_ARTIFACT_DIR = "quikpilot-failure";
1982
+ var REPORT_COPY = "final-test-report.xml";
1983
+ var MAX_REPORT_BYTES = 5 * 1024 * 1024;
1984
+ var MAX_FILES = 200;
1985
+ var RUNNER_VERSION = true ? "9.1.0" : "dev";
1986
+ function safeDestination(base, repoPath) {
1987
+ if (!repoPath || (0, import_node_path6.isAbsolute)(repoPath)) return null;
1988
+ const destination = (0, import_node_path6.resolve)(base, repoPath);
1989
+ const rel = (0, import_node_path6.relative)(base, destination);
1990
+ if (!rel || rel.startsWith("..") || (0, import_node_path6.isAbsolute)(rel)) return null;
1991
+ return destination;
1992
+ }
1993
+ function writeFailureArtifact(input) {
1994
+ if (input.results.length === 0) return { written: false };
1995
+ try {
1996
+ const base = (0, import_node_path6.resolve)(input.cwd, FAILURE_ARTIFACT_DIR);
1997
+ (0, import_node_fs7.rmSync)(base, { recursive: true, force: true });
1998
+ (0, import_node_fs7.mkdirSync)(base, { recursive: true });
1999
+ const files = [];
2000
+ const skipped = [];
2001
+ for (const result of input.results) {
2002
+ const destination = files.length < MAX_FILES ? safeDestination(base, result.targetPath) : null;
2003
+ if (!destination) {
2004
+ skipped.push(result.targetPath);
2005
+ continue;
2006
+ }
2007
+ (0, import_node_fs7.mkdirSync)((0, import_node_path6.dirname)(destination), { recursive: true });
2008
+ (0, import_node_fs7.writeFileSync)(destination, result.newContent);
2009
+ files.push({ path: result.targetPath, bytes: Buffer.byteLength(result.newContent) });
2010
+ }
2011
+ let testReport = "none";
2012
+ if (input.testReport) {
2013
+ if (input.testReport.byteLength > MAX_REPORT_BYTES) {
2014
+ testReport = "too-large";
2015
+ } else {
2016
+ (0, import_node_fs7.writeFileSync)((0, import_node_path6.join)(base, REPORT_COPY), input.testReport);
2017
+ testReport = "included";
2018
+ }
2019
+ }
2020
+ const manifest = {
2021
+ runId: input.runId,
2022
+ issueKey: input.issueKey,
2023
+ runnerVersion: RUNNER_VERSION,
2024
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2025
+ targets: input.targets.map((t) => ({
2026
+ path: t.path,
2027
+ kind: t.kind,
2028
+ ...t.automationIds && t.automationIds.length > 0 ? { automationIds: t.automationIds } : {}
2029
+ })),
2030
+ files,
2031
+ skipped,
2032
+ testReport,
2033
+ failureReasons: input.failureReasons
2034
+ };
2035
+ const text = JSON.stringify(manifest, (_key, value) => typeof value === "string" ? scrubSecrets(value, input.secrets) : value, 2);
2036
+ (0, import_node_fs7.writeFileSync)((0, import_node_path6.join)(base, "manifest.json"), text);
2037
+ return { written: (0, import_node_fs7.existsSync)((0, import_node_path6.join)(base, "manifest.json")) };
2038
+ } catch (err) {
2039
+ console.warn(`::warning::Could not keep the failed run's files: ${err instanceof Error ? err.message : String(err)}`);
2040
+ return { written: false };
2041
+ }
2042
+ }
2043
+
1422
2044
  // src/scopeCheck.ts
1423
- var import_node_fs7 = require("node:fs");
2045
+ var import_node_fs8 = require("node:fs");
1424
2046
  var ROUTE_METHOD_RE = /\b(GET|POST|PUT|PATCH|DELETE)\b/i;
1425
2047
  var ROUTE_PATH_RE = /\/[A-Za-z0-9_\-{}[\]]+(?:\/[A-Za-z0-9_\-:{}[\]]+)+/;
1426
2048
  var ROUTE_EXPORT_RE = /\bexport\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\s*\(/;
@@ -1477,7 +2099,7 @@ function checkApiScope(results, targets, apiChanges) {
1477
2099
  } else {
1478
2100
  let currentContent;
1479
2101
  try {
1480
- currentContent = (0, import_node_fs7.readFileSync)(targetPath, "utf8");
2102
+ currentContent = (0, import_node_fs8.readFileSync)(targetPath, "utf8");
1481
2103
  } catch {
1482
2104
  currentContent = "";
1483
2105
  }
@@ -1496,7 +2118,7 @@ function describeScopeViolations(check) {
1496
2118
  }
1497
2119
 
1498
2120
  // src/targeting.ts
1499
- var import_node_path5 = require("node:path");
2121
+ var import_node_path7 = require("node:path");
1500
2122
  var CONTRACT_VERSION2 = "test-targets-1";
1501
2123
  function listWorkspaceFiles(workspaceRoot) {
1502
2124
  return listFiles(workspaceRoot ?? ".");
@@ -1525,9 +2147,9 @@ function validateTarget(target, workspaceRoot, workspaceListing, deniedPaths) {
1525
2147
  }
1526
2148
  return;
1527
2149
  }
1528
- const root = (0, import_node_path5.resolve)(workspaceRoot ?? ".");
1529
- const rel = (0, import_node_path5.relative)(root, (0, import_node_path5.resolve)(target.path));
1530
- if (rel.startsWith("..") || (0, import_node_path5.isAbsolute)(rel)) {
2150
+ const root = (0, import_node_path7.resolve)(workspaceRoot ?? ".");
2151
+ const rel = (0, import_node_path7.relative)(root, (0, import_node_path7.resolve)(target.path));
2152
+ if (rel.startsWith("..") || (0, import_node_path7.isAbsolute)(rel)) {
1531
2153
  throw new Error(
1532
2154
  `pilot.targeting named a NEW_FILE path that escapes the workspace root: ${target.path}`
1533
2155
  );
@@ -1582,10 +2204,19 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, re
1582
2204
  for (const target of body.targets) {
1583
2205
  validateTarget(target, workspaceRoot, listing, deniedPaths);
1584
2206
  }
2207
+ const correctedKinds = [];
2208
+ const targets = body.targets.map((target) => {
2209
+ if (target.kind === "NEW_FILE" && listing.has(target.path)) {
2210
+ correctedKinds.push(target.path);
2211
+ return { ...target, kind: "EXISTING" };
2212
+ }
2213
+ return target;
2214
+ });
1585
2215
  return {
1586
2216
  outcome: "TARGETED",
1587
- targets: body.targets,
1588
- skippedCases: body.skippedCases ?? []
2217
+ targets,
2218
+ skippedCases: body.skippedCases ?? [],
2219
+ ...correctedKinds.length > 0 ? { correctedKinds } : {}
1589
2220
  };
1590
2221
  }
1591
2222
 
@@ -1748,6 +2379,19 @@ async function exchangeForRunToken(tokenUrl, oidcToken) {
1748
2379
  return body.data.runToken;
1749
2380
  }
1750
2381
 
2382
+ // src/vanishedTests.ts
2383
+ var MAX_VANISHED_NAMES = 50;
2384
+ function findVanishedTests(baseline, after) {
2385
+ if (!baseline.results || !after.results) return null;
2386
+ const missing = [...baseline.results.keys()].filter((id) => !after.results.has(id));
2387
+ if (missing.length === 0) return null;
2388
+ return { names: missing.slice(0, MAX_VANISHED_NAMES), total: missing.length };
2389
+ }
2390
+ function describeVanishedTests(vanished) {
2391
+ const noun = vanished.total === 1 ? "1 test" : `${vanished.total} tests`;
2392
+ 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"}.`;
2393
+ }
2394
+
1751
2395
  // src/run.ts
1752
2396
  async function run(env2) {
1753
2397
  let hardFailure = false;
@@ -1764,8 +2408,11 @@ async function run(env2) {
1764
2408
  };
1765
2409
  let targeted = false;
1766
2410
  let generatedFilePaths = [];
2411
+ let generatedResults = [];
2412
+ let finalTestReport = null;
1767
2413
  let targetingResult;
1768
2414
  let baselineFindings;
2415
+ let vanishedTests;
1769
2416
  const contextLog = [];
1770
2417
  try {
1771
2418
  runToken = await exchangeForRunToken(env2.tokenUrl, await fetchGithubOidcToken());
@@ -1784,6 +2431,14 @@ async function run(env2) {
1784
2431
  outOfScope
1785
2432
  );
1786
2433
  targetingResult = targeting;
2434
+ if (targeting.outcome === "TARGETED") {
2435
+ for (const path of targeting.correctedKinds ?? []) {
2436
+ 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.`;
2437
+ warnings.push(w);
2438
+ console.warn(`QuikPilot targeting warning (not blocking): ${w}`);
2439
+ console.log(JSON.stringify({ event: "targeting.kind_corrected", path }));
2440
+ }
2441
+ }
1787
2442
  if (targeting.outcome === "NO_TARGET") {
1788
2443
  writePhase0Marker(env2.runId);
1789
2444
  runCommand(packet.commands.setup, "Setup");
@@ -1833,6 +2488,7 @@ async function run(env2) {
1833
2488
  outOfScope
1834
2489
  )
1835
2490
  ]);
2491
+ generatedResults = results;
1836
2492
  const depCheck = checkImportDependencies(
1837
2493
  results,
1838
2494
  readDeclaredDependencies(results, packet.workspaceRoot),
@@ -1878,6 +2534,13 @@ async function run(env2) {
1878
2534
  }
1879
2535
  runCommand(packet.commands.lint, "Lint");
1880
2536
  const after = captureTestSnapshot(packet.commands.test);
2537
+ finalTestReport = readTestReportBytes();
2538
+ vanishedTests = findVanishedTests(baseline, after) ?? void 0;
2539
+ if (vanishedTests) {
2540
+ const w = describeVanishedTests(vanishedTests);
2541
+ warnings.push(w);
2542
+ console.warn(`QuikPilot test warning (not blocking): ${w}`);
2543
+ }
1881
2544
  const check = checkForRegressions(baseline, after);
1882
2545
  if (check.blocked) {
1883
2546
  throw new Error(
@@ -1933,6 +2596,22 @@ async function run(env2) {
1933
2596
  recordFailure(err);
1934
2597
  }
1935
2598
  }
2599
+ if (hardFailure && generatedResults.length > 0) {
2600
+ try {
2601
+ writeFailureArtifact({
2602
+ cwd: process.cwd(),
2603
+ runId: env2.runId,
2604
+ issueKey: packet?.issueKey ?? null,
2605
+ targets: targetingResult?.outcome === "TARGETED" ? targetingResult.targets : [],
2606
+ results: generatedResults,
2607
+ testReport: finalTestReport,
2608
+ failureReasons,
2609
+ secrets: runToken ? [runToken] : []
2610
+ });
2611
+ } catch (err) {
2612
+ console.warn(`::warning::Could not keep the failed run's files: ${err instanceof Error ? err.message : String(err)}`);
2613
+ }
2614
+ }
1936
2615
  await reportBack({
1937
2616
  callbackUrl: env2.callbackUrl,
1938
2617
  runToken,
@@ -1942,6 +2621,7 @@ async function run(env2) {
1942
2621
  // Omitted when targeting itself never produced a result (nothing exists to record).
1943
2622
  targeting: targetingResult ? buildTargetingRecord(targetingResult, contextLog) : void 0,
1944
2623
  baselineFindings,
2624
+ vanishedTests,
1945
2625
  unresolved: failureReasons.length > 0 ? failureReasons : void 0
1946
2626
  });
1947
2627
  return { hardFailure };