@quikit/quikpilot-runner 7.0.6 → 8.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
@@ -23,6 +23,54 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  mod
24
24
  ));
25
25
 
26
+ // src/env.ts
27
+ function required(name, value) {
28
+ if (!value) {
29
+ throw new Error(`Missing required environment variable: ${name}`);
30
+ }
31
+ return value;
32
+ }
33
+ function readRunnerEnv(env2 = process.env) {
34
+ const githubRunId = required("GITHUB_RUN_ID", env2.GITHUB_RUN_ID);
35
+ const githubServerUrl = env2.GITHUB_SERVER_URL ?? "https://github.com";
36
+ const githubRepository = required("GITHUB_REPOSITORY", env2.GITHUB_REPOSITORY);
37
+ if (env2.MODE === "readiness_check") {
38
+ return {
39
+ mode: "readiness_check",
40
+ readinessToken: required("READINESS_TOKEN", env2.READINESS_TOKEN),
41
+ readinessCallbackUrl: required("READINESS_CALLBACK_URL", env2.READINESS_CALLBACK_URL),
42
+ setupCmd: env2.SETUP_CMD ?? "",
43
+ testCmd: required("TEST_CMD", env2.TEST_CMD),
44
+ githubRunId,
45
+ githubServerUrl,
46
+ githubRepository
47
+ };
48
+ }
49
+ return {
50
+ mode: "run",
51
+ runId: required("RUN_ID", env2.RUN_ID),
52
+ callbackUrl: required("CALLBACK_URL", env2.CALLBACK_URL),
53
+ packetUrl: required("PACKET_URL", env2.PACKET_URL),
54
+ tokenUrl: required("TOKEN_URL", env2.TOKEN_URL),
55
+ installationTokenUrl: required("INSTALLATION_TOKEN_URL", env2.INSTALLATION_TOKEN_URL),
56
+ githubRunId,
57
+ githubServerUrl,
58
+ githubRepository
59
+ };
60
+ }
61
+
62
+ // src/quiktrackError.ts
63
+ async function describeQuikTrackFailure(res) {
64
+ try {
65
+ const body = await res.json();
66
+ if (body && typeof body === "object" && typeof body.reason_code === "string") {
67
+ return `${res.status} ${res.statusText} (${body.reason_code})`;
68
+ }
69
+ } catch {
70
+ }
71
+ return `${res.status} ${res.statusText}`;
72
+ }
73
+
26
74
  // src/callback.ts
27
75
  async function reportBack(params) {
28
76
  const { callbackUrl, runToken, status, branch, warnings } = params;
@@ -35,7 +83,7 @@ async function reportBack(params) {
35
83
  body: JSON.stringify(body)
36
84
  });
37
85
  if (!res.ok) {
38
- throw new Error(`Report back to QuikTrack failed: ${res.status} ${res.statusText}`);
86
+ throw new Error(`Report back to QuikTrack failed: ${await describeQuikTrackFailure(res)}`);
39
87
  }
40
88
  }
41
89
 
@@ -155,6 +203,14 @@ var import_node_fs = require("node:fs");
155
203
  var import_node_path = require("node:path");
156
204
 
157
205
  // src/envelope.ts
206
+ var HARD_FAILURE_CODES = /* @__PURE__ */ new Set(["SEMANTIC_MISMATCH", "CONTRACT_REJECTED"]);
207
+ function checkHardFailure(envelope, useCase) {
208
+ const code = envelope?.errorCode;
209
+ if (typeof code === "string" && HARD_FAILURE_CODES.has(code)) {
210
+ const detail = envelope.error ?? envelope.normalizedText ?? "no detail";
211
+ throw new Error(`${useCase} failed (${code}, not retried): ${detail}`);
212
+ }
213
+ }
158
214
  function unwrapExecute(envelope, useCase) {
159
215
  if (envelope?.success === false) {
160
216
  throw new Error(
@@ -167,11 +223,97 @@ function unwrapExecute(envelope, useCase) {
167
223
  return envelope.structuredJson;
168
224
  }
169
225
 
226
+ // src/retryFetch.ts
227
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([502, 503, 504]);
228
+ var RETRYABLE_NETWORK_ERROR_CODES = /* @__PURE__ */ new Set(["ECONNRESET", "ETIMEDOUT"]);
229
+ function isRetryableNetworkError(err) {
230
+ const code = err?.cause?.code ?? err?.code;
231
+ return typeof code === "string" && RETRYABLE_NETWORK_ERROR_CODES.has(code);
232
+ }
233
+ async function fetchWithRetry(url, init, { retries = 2, backoffMs = 250 } = {}) {
234
+ for (let attempt = 0; ; attempt++) {
235
+ try {
236
+ const res = await fetch(url, init);
237
+ if (RETRYABLE_STATUS_CODES.has(res.status) && attempt < retries) {
238
+ await new Promise((r) => setTimeout(r, backoffMs));
239
+ continue;
240
+ }
241
+ return res;
242
+ } catch (err) {
243
+ if (isRetryableNetworkError(err) && attempt < retries) {
244
+ await new Promise((r) => setTimeout(r, backoffMs));
245
+ continue;
246
+ }
247
+ throw err;
248
+ }
249
+ }
250
+ }
251
+
170
252
  // src/codegen.ts
171
253
  var FILE_TOO_LARGE_CODE = "CODEGEN_FILE_TOO_LARGE";
172
254
  var CONTEXT_TOO_LARGE_CODE = "CODEGEN_CONTEXT_TOO_LARGE";
173
- var NOT_RETRIED_CODES = /* @__PURE__ */ new Set([FILE_TOO_LARGE_CODE, CONTEXT_TOO_LARGE_CODE]);
174
- async function runCodegen(gateway, targets, workspaceListing) {
255
+ var OUTPUT_TOO_LARGE_CODE = "CODEGEN_OUTPUT_TOO_LARGE";
256
+ var NOT_RETRIED_CODES = /* @__PURE__ */ new Set([FILE_TOO_LARGE_CODE, CONTEXT_TOO_LARGE_CODE, OUTPUT_TOO_LARGE_CODE]);
257
+ var SUSPECT_PLACEHOLDER_RE = /rest(?: of the)? (?:file|code) unchanged|remainder unchanged|\/\/\s*\.\.\.\s*(?:rest|remaining)/i;
258
+ var SUSPECT_LENGTH_DROP_RATIO = 0.6;
259
+ var SUSPECT_REMOVED_LINE_RATIO = 0.3;
260
+ var MIN_LINES_FOR_REMOVED_LINE_CHECK = 10;
261
+ var JS_TS_EXTENSION_RE = /\.(?:ts|tsx|js|jsx|mjs|cjs)$/;
262
+ var EXPORTED_SYMBOL_RE = /^export\s+(?:default\s+)?(?:async\s+)?(?:function\*?|class|const|let|var|interface|type|enum)\s+([A-Za-z_$][\w$]*)/gm;
263
+ function countRemovedLines(currentContent, newContent) {
264
+ const remaining = /* @__PURE__ */ new Map();
265
+ for (const line of newContent.split("\n")) {
266
+ remaining.set(line, (remaining.get(line) ?? 0) + 1);
267
+ }
268
+ let removed = 0;
269
+ for (const line of currentContent.split("\n")) {
270
+ const count = remaining.get(line) ?? 0;
271
+ if (count > 0) {
272
+ remaining.set(line, count - 1);
273
+ } else {
274
+ removed++;
275
+ }
276
+ }
277
+ return removed;
278
+ }
279
+ function extractExportedSymbols(content) {
280
+ const symbols = /* @__PURE__ */ new Set();
281
+ for (const match of content.matchAll(EXPORTED_SYMBOL_RE)) {
282
+ symbols.add(match[1]);
283
+ }
284
+ return symbols;
285
+ }
286
+ function removedUnexplainedSymbols(targetPath, planText, currentContent, newContent) {
287
+ if (!JS_TS_EXTENSION_RE.test(targetPath)) return [];
288
+ const before = extractExportedSymbols(currentContent);
289
+ const after = extractExportedSymbols(newContent);
290
+ const lowerPlan = planText.toLowerCase();
291
+ return [...before].filter((symbol) => !after.has(symbol) && !lowerPlan.includes(symbol.toLowerCase()));
292
+ }
293
+ function looksLikeTruncatedRewrite(target, currentContent, newContent) {
294
+ if (SUSPECT_PLACEHOLDER_RE.test(newContent)) {
295
+ return "contains placeholder text suggesting an abbreviated rewrite";
296
+ }
297
+ if (currentContent.length > 0 && newContent.length < currentContent.length * SUSPECT_LENGTH_DROP_RATIO) {
298
+ return `sharp length drop (${currentContent.length} -> ${newContent.length} chars)`;
299
+ }
300
+ const currentLineCount = currentContent.length > 0 ? currentContent.split("\n").length : 0;
301
+ if (currentLineCount >= MIN_LINES_FOR_REMOVED_LINE_CHECK) {
302
+ const removedLines = countRemovedLines(currentContent, newContent);
303
+ if (removedLines > currentLineCount * SUSPECT_REMOVED_LINE_RATIO) {
304
+ const pct = (removedLines / currentLineCount * 100).toFixed(0);
305
+ return `removed ${removedLines} of ${currentLineCount} lines (${pct}%) without a matching drop in character count`;
306
+ }
307
+ }
308
+ const planText = target.planStepsImplemented.join(" ");
309
+ const unexplainedSymbols = removedUnexplainedSymbols(target.path, planText, currentContent, newContent);
310
+ if (unexplainedSymbols.length > 0) {
311
+ return `removed exported symbol(s) the plan steps don't mention: ${unexplainedSymbols.join(", ")}`;
312
+ }
313
+ return null;
314
+ }
315
+ var CONTRACT_VERSION = "test-targets-1";
316
+ async function runCodegen(gateway, targets, workspaceListing, requiredTests, deniedPaths) {
175
317
  const results = [];
176
318
  const listing = new Set(workspaceListing);
177
319
  for (const target of targets) {
@@ -191,12 +333,21 @@ async function runCodegen(gateway, targets, workspaceListing) {
191
333
  currentContent,
192
334
  planSteps: target.planStepsImplemented,
193
335
  kind: target.kind,
194
- contextContents
336
+ contextContents,
337
+ // QUIKTR-1083 (§15.6) — required on every call on the versioned path.
338
+ contractVersion: CONTRACT_VERSION
195
339
  };
196
340
  if (isNewFile) {
197
341
  contextData.workspaceListing = workspaceListing;
198
342
  }
199
- const res = await fetch(`${gateway.baseUrl}/ai/execute`, {
343
+ if (deniedPaths.length > 0) {
344
+ contextData.deniedPaths = deniedPaths;
345
+ }
346
+ if (target.automationIds && target.automationIds.length > 0) {
347
+ const wanted = new Set(target.automationIds);
348
+ contextData.requiredTests = requiredTests.filter((tc) => wanted.has(tc.automationId));
349
+ }
350
+ const res = await fetchWithRetry(`${gateway.baseUrl}/ai/execute`, {
200
351
  method: "POST",
201
352
  headers: { "content-type": "application/json", "x-run-token": gateway.token },
202
353
  body: JSON.stringify({
@@ -211,6 +362,7 @@ async function runCodegen(gateway, targets, workspaceListing) {
211
362
  throw new Error(`Codegen call failed for ${target.path}: ${res.status} ${res.statusText}`);
212
363
  }
213
364
  const envelope = await res.json();
365
+ checkHardFailure(envelope, "pilot.codegen");
214
366
  if (typeof envelope?.errorCode === "string" && NOT_RETRIED_CODES.has(envelope.errorCode)) {
215
367
  const detail = envelope.error ? `: ${envelope.error}` : "";
216
368
  throw new Error(`Codegen refused ${target.path}: ${envelope.errorCode} (not retried)${detail}`);
@@ -221,6 +373,24 @@ async function runCodegen(gateway, targets, workspaceListing) {
221
373
  `Codegen refused ${target.path}${body.refusalReason ? `: ${body.refusalReason}` : ""}`
222
374
  );
223
375
  }
376
+ if (!isNewFile) {
377
+ const suspectReason = looksLikeTruncatedRewrite(target, currentContent, body.newContent);
378
+ if (suspectReason) {
379
+ throw new Error(
380
+ `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.`
381
+ );
382
+ }
383
+ }
384
+ console.log(
385
+ JSON.stringify({
386
+ event: "codegen.metrics",
387
+ targetPath: target.path,
388
+ kind: target.kind,
389
+ inputLength: currentContent.length,
390
+ outputLength: body.newContent.length,
391
+ removedLines: isNewFile ? null : countRemovedLines(currentContent, body.newContent)
392
+ })
393
+ );
224
394
  results.push({ targetPath: target.path, newContent: body.newContent });
225
395
  }
226
396
  return results;
@@ -414,26 +584,6 @@ function describeDependencyViolations(check) {
414
584
  return check.violations.map((v) => `${v.file} imports ${v.packages.join(", ")}, not a dependency of this repository`).join("; ");
415
585
  }
416
586
 
417
- // src/env.ts
418
- function required(name, value) {
419
- if (!value) {
420
- throw new Error(`Missing required environment variable: ${name}`);
421
- }
422
- return value;
423
- }
424
- function readRunnerEnv(env = process.env) {
425
- return {
426
- runId: required("RUN_ID", env.RUN_ID),
427
- callbackUrl: required("CALLBACK_URL", env.CALLBACK_URL),
428
- packetUrl: required("PACKET_URL", env.PACKET_URL),
429
- tokenUrl: required("TOKEN_URL", env.TOKEN_URL),
430
- installationTokenUrl: required("INSTALLATION_TOKEN_URL", env.INSTALLATION_TOKEN_URL),
431
- githubRunId: required("GITHUB_RUN_ID", env.GITHUB_RUN_ID),
432
- githubServerUrl: env.GITHUB_SERVER_URL ?? "https://github.com",
433
- githubRepository: required("GITHUB_REPOSITORY", env.GITHUB_REPOSITORY)
434
- };
435
- }
436
-
437
587
  // src/guardrail.ts
438
588
  function checkGuardrail(deniedPaths) {
439
589
  addAll();
@@ -451,7 +601,7 @@ async function exchangeForInstallationToken(installationTokenUrl, runToken) {
451
601
  headers: { Authorization: `Bearer ${runToken}` }
452
602
  });
453
603
  if (!res.ok) {
454
- throw new Error(`Exchange installation token failed: ${res.status} ${res.statusText}`);
604
+ throw new Error(`Exchange installation token failed: ${await describeQuikTrackFailure(res)}`);
455
605
  }
456
606
  const body = await res.json();
457
607
  if (body.success !== true) {
@@ -462,14 +612,14 @@ async function exchangeForInstallationToken(installationTokenUrl, runToken) {
462
612
 
463
613
  // src/oidc.ts
464
614
  var QUIKPILOT_OIDC_AUDIENCE = "quiktrack-quikpilot";
465
- async function fetchGithubOidcToken(env = process.env) {
615
+ async function fetchGithubOidcToken(env2 = process.env) {
466
616
  const requestUrl = required(
467
617
  "ACTIONS_ID_TOKEN_REQUEST_URL",
468
- env.ACTIONS_ID_TOKEN_REQUEST_URL
618
+ env2.ACTIONS_ID_TOKEN_REQUEST_URL
469
619
  );
470
620
  const requestToken = required(
471
621
  "ACTIONS_ID_TOKEN_REQUEST_TOKEN",
472
- env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
622
+ env2.ACTIONS_ID_TOKEN_REQUEST_TOKEN
473
623
  );
474
624
  const separator = requestUrl.includes("?") ? "&" : "?";
475
625
  const url = `${requestUrl}${separator}audience=${encodeURIComponent(QUIKPILOT_OIDC_AUDIENCE)}`;
@@ -487,6 +637,10 @@ async function fetchGithubOidcToken(env = process.env) {
487
637
  }
488
638
 
489
639
  // src/overlapCheck.ts
640
+ var TEST_PATH_RE = /(\.(test|spec)\.[jt]sx?$)|(^|\/)(__tests__|tests?)\//i;
641
+ function isTestTarget(path) {
642
+ return TEST_PATH_RE.test(path);
643
+ }
490
644
  var STOPWORDS = /* @__PURE__ */ new Set([
491
645
  "a",
492
646
  "an",
@@ -548,6 +702,9 @@ function jaccard(a, b) {
548
702
  const union = a.size + b.size - intersection;
549
703
  return intersection / union;
550
704
  }
705
+ function normalizedStep(step) {
706
+ return step.trim().toLowerCase().replace(/\s+/g, " ");
707
+ }
551
708
  var OVERLAP_THRESHOLD = 0.5;
552
709
  function findOverlappingSteps(targets) {
553
710
  const items = targets.flatMap(
@@ -561,12 +718,14 @@ function findOverlappingSteps(targets) {
561
718
  for (let i = 0; i < items.length; i++) {
562
719
  for (let j = i + 1; j < items.length; j++) {
563
720
  if (items[i].target === items[j].target) continue;
721
+ if (isTestTarget(items[i].target) || isTestTarget(items[j].target)) continue;
564
722
  if (jaccard(items[i].words, items[j].words) >= OVERLAP_THRESHOLD) {
565
723
  warnings.push({
566
724
  targetA: items[i].target,
567
725
  stepA: items[i].step,
568
726
  targetB: items[j].target,
569
- stepB: items[j].step
727
+ stepB: items[j].step,
728
+ severity: normalizedStep(items[i].step) === normalizedStep(items[j].step) ? "identical" : "similar"
570
729
  });
571
730
  }
572
731
  }
@@ -586,7 +745,7 @@ async function fetchPacket(packetUrl, runToken, githubRunId) {
586
745
  headers: { Authorization: `Bearer ${runToken}` }
587
746
  });
588
747
  if (!res.ok) {
589
- throw new Error(`Fetch work packet failed: ${res.status} ${res.statusText}`);
748
+ throw new Error(`Fetch work packet failed: ${await describeQuikTrackFailure(res)}`);
590
749
  }
591
750
  const body = await res.json();
592
751
  if (body.success !== true) {
@@ -662,9 +821,9 @@ async function reportToQuikTest(params) {
662
821
  try {
663
822
  console.log(`reporting ${report}`);
664
823
  const ciUrl = buildCiUrl(githubServerUrl, githubRepository, githubRunId);
665
- const { readFileSync: readFileSync4 } = await import("node:fs");
666
- const body = readFileSync4(report);
667
- const url = `${resultsUrl}?ciUrl=${encodeURIComponent(ciUrl)}`;
824
+ const { readFileSync: readFileSync5 } = await import("node:fs");
825
+ const body = readFileSync5(report);
826
+ const url = `${resultsUrl}?ciUrl=${encodeURIComponent(ciUrl)}&automationIdFormat=bracket`;
668
827
  const res = await fetch(url, {
669
828
  method: "POST",
670
829
  headers: { Authorization: `Bearer ${runToken}`, "Content-Type": "text/xml" },
@@ -685,8 +844,24 @@ var TESTCASE_RE = /<testcase\b([^>]*?)(?:\/>|>([\s\S]*?)<\/testcase>)/g;
685
844
  var NAME_ATTR_RE = /\bname="([^"]*)"/;
686
845
  var CLASSNAME_ATTR_RE = /\bclassname="([^"]*)"/;
687
846
  var FAILURE_CHILD_RE = /<failure\b|<error\b/;
847
+ var FAILURE_TAG_RE = /<(?:failure|error)\b([^>]*?)(?:\/>|>([\s\S]*?)<\/(?:failure|error)>)/;
848
+ var FAILURE_MESSAGE_ATTR_RE = /\bmessage="([^"]*)"/;
849
+ var MAX_FAILURE_TEXT = 500;
850
+ function unescapeXmlEntities(s) {
851
+ return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
852
+ }
853
+ function extractFailureText(testcaseBody) {
854
+ const tag = FAILURE_TAG_RE.exec(testcaseBody);
855
+ if (!tag) return null;
856
+ const [, attrs = "", inner = ""] = tag;
857
+ const raw = (FAILURE_MESSAGE_ATTR_RE.exec(attrs)?.[1] ?? inner).trim();
858
+ if (!raw) return null;
859
+ const text = unescapeXmlEntities(raw);
860
+ return text.length > MAX_FAILURE_TEXT ? `${text.slice(0, MAX_FAILURE_TEXT)}\u2026` : text;
861
+ }
688
862
  function parseJUnitOutcomes(xml) {
689
- const results = /* @__PURE__ */ new Map();
863
+ const outcomes = /* @__PURE__ */ new Map();
864
+ const failureText = /* @__PURE__ */ new Map();
690
865
  let match;
691
866
  TESTCASE_RE.lastIndex = 0;
692
867
  while (match = TESTCASE_RE.exec(xml)) {
@@ -695,9 +870,14 @@ function parseJUnitOutcomes(xml) {
695
870
  if (!name) continue;
696
871
  const classname = CLASSNAME_ATTR_RE.exec(attrs)?.[1];
697
872
  const id = classname ? `${classname}::${name}` : name;
698
- results.set(id, !FAILURE_CHILD_RE.test(body));
873
+ const failed = FAILURE_CHILD_RE.test(body);
874
+ outcomes.set(id, !failed);
875
+ if (failed) {
876
+ const text = extractFailureText(body);
877
+ if (text) failureText.set(id, text);
878
+ }
699
879
  }
700
- return results;
880
+ return { outcomes, failureText };
701
881
  }
702
882
  function readJUnitOutcomes(cwd) {
703
883
  const report = findJUnitReport(cwd);
@@ -710,35 +890,55 @@ function readJUnitOutcomes(cwd) {
710
890
  }
711
891
  function captureTestSnapshot(command, cwd = process.cwd()) {
712
892
  const outcome = runTestCommand(command);
713
- return { outcome, results: readJUnitOutcomes(cwd) };
893
+ const parsed = readJUnitOutcomes(cwd);
894
+ return { outcome, results: parsed?.outcomes ?? null, failureText: parsed?.failureText };
714
895
  }
715
896
  async function captureBaselineSnapshot(command, cwd = process.cwd()) {
716
897
  const outcome = await runTestCommandAsync(command);
717
- return { outcome, results: readJUnitOutcomes(cwd) };
898
+ const parsed = readJUnitOutcomes(cwd);
899
+ return { outcome, results: parsed?.outcomes ?? null, failureText: parsed?.failureText };
718
900
  }
719
901
  function checkForRegressions(baseline, after) {
720
902
  if (!baseline.results || !after.results) {
721
- return { blocked: false, regressions: [], newFailures: [], preExisting: [] };
903
+ return { blocked: false, regressions: [], newFailures: [], preExisting: [], failureText: /* @__PURE__ */ new Map() };
722
904
  }
723
905
  const regressions = [];
724
906
  const newFailures = [];
725
907
  const preExisting = [];
908
+ const failureText = /* @__PURE__ */ new Map();
909
+ const afterFailureText = after.failureText;
726
910
  for (const [testId, passed] of after.results) {
727
911
  if (passed) continue;
728
912
  const before = baseline.results.get(testId);
729
- if (before === true) regressions.push(testId);
730
- else if (before === false) preExisting.push(testId);
731
- else newFailures.push(testId);
913
+ if (before === true) {
914
+ regressions.push(testId);
915
+ const text = afterFailureText?.get(testId);
916
+ if (text) failureText.set(testId, text);
917
+ } else if (before === false) {
918
+ preExisting.push(testId);
919
+ } else {
920
+ newFailures.push(testId);
921
+ const text = afterFailureText?.get(testId);
922
+ if (text) failureText.set(testId, text);
923
+ }
732
924
  }
733
- return { blocked: regressions.length > 0 || newFailures.length > 0, regressions, newFailures, preExisting };
925
+ return { blocked: regressions.length > 0 || newFailures.length > 0, regressions, newFailures, preExisting, failureText };
734
926
  }
735
927
  function describeRegressions(check) {
928
+ const withText = (id) => {
929
+ const text = check.failureText.get(id);
930
+ return text ? `${id} (${text})` : id;
931
+ };
736
932
  const parts = [];
737
933
  if (check.regressions.length > 0) {
738
- parts.push(`${check.regressions.length} regression(s) (passed before, now fail): ${check.regressions.join(", ")}`);
934
+ parts.push(
935
+ `${check.regressions.length} regression(s) (passed before, now fail): ${check.regressions.map(withText).join(", ")}`
936
+ );
739
937
  }
740
938
  if (check.newFailures.length > 0) {
741
- parts.push(`${check.newFailures.length} new failure(s) (did not exist before): ${check.newFailures.join(", ")}`);
939
+ parts.push(
940
+ `${check.newFailures.length} new failure(s) (did not exist before): ${check.newFailures.map(withText).join(", ")}`
941
+ );
742
942
  }
743
943
  if (check.preExisting.length > 0) {
744
944
  parts.push(`${check.preExisting.length} pre-existing failure(s), unrelated to this change, not blocking`);
@@ -746,12 +946,99 @@ function describeRegressions(check) {
746
946
  return parts.join("; ");
747
947
  }
748
948
 
949
+ // src/scopeCheck.ts
950
+ var import_node_fs6 = require("node:fs");
951
+ var ROUTE_METHOD_RE = /\b(GET|POST|PUT|PATCH|DELETE)\b/i;
952
+ var ROUTE_PATH_RE = /\/[A-Za-z0-9_\-{}[\]]+(?:\/[A-Za-z0-9_\-:{}[\]]+)+/;
953
+ var ROUTE_EXPORT_RE = /\bexport\s+(?:async\s+)?function\s+(GET|POST|PUT|PATCH|DELETE)\s*\(/;
954
+ var NO_PATH = "(this file's route)";
955
+ function detectMentions(text) {
956
+ const found = [];
957
+ for (const line of text.split("\n")) {
958
+ const methodMatch = ROUTE_METHOD_RE.exec(line);
959
+ const pathMatch = ROUTE_PATH_RE.exec(line);
960
+ if (methodMatch && pathMatch) {
961
+ found.push({ method: methodMatch[1].toUpperCase(), path: pathMatch[0] });
962
+ }
963
+ const exportMatch = ROUTE_EXPORT_RE.exec(line);
964
+ if (exportMatch) {
965
+ found.push({ method: exportMatch[1].toUpperCase(), path: NO_PATH });
966
+ }
967
+ }
968
+ return found;
969
+ }
970
+ function normalizePath(path) {
971
+ return path.replace(/:[A-Za-z0-9_]+|\{[^}]+\}|\[[^\]]+\]/g, "*");
972
+ }
973
+ function surfaceCovers(surface, mention) {
974
+ if (!new RegExp(`\\b${mention.method}\\b`, "i").test(surface)) return false;
975
+ if (mention.path === NO_PATH) return true;
976
+ const declaredPath = normalizePath(surface);
977
+ const mentionPath = normalizePath(mention.path);
978
+ return declaredPath.includes(mentionPath) || mentionPath.includes(declaredPath);
979
+ }
980
+ function addedLines(before, after) {
981
+ const beforeLines = new Set(before.split("\n"));
982
+ return after.split("\n").filter((line) => !beforeLines.has(line)).join("\n");
983
+ }
984
+ function checkApiScope(results, targets, apiChanges) {
985
+ const targetsByPath = new Map(targets.map((t) => [t.path, t]));
986
+ const violations = [];
987
+ for (const { targetPath, newContent } of results) {
988
+ const target = targetsByPath.get(targetPath);
989
+ const isNewFile = target?.kind === "NEW_FILE";
990
+ let scanned;
991
+ if (isNewFile) {
992
+ scanned = newContent;
993
+ } else {
994
+ let currentContent;
995
+ try {
996
+ currentContent = (0, import_node_fs6.readFileSync)(targetPath, "utf8");
997
+ } catch {
998
+ currentContent = "";
999
+ }
1000
+ scanned = addedLines(currentContent, newContent);
1001
+ }
1002
+ for (const mention of detectMentions(scanned)) {
1003
+ if (!apiChanges.some((c) => surfaceCovers(c.surface, mention))) {
1004
+ violations.push({ file: targetPath, method: mention.method, path: mention.path });
1005
+ }
1006
+ }
1007
+ }
1008
+ return { blocked: violations.length > 0, violations };
1009
+ }
1010
+ function describeScopeViolations(check) {
1011
+ return check.violations.map((v) => `${v.file} adds ${v.method} ${v.path}, not declared in the design's API changes`).join("; ");
1012
+ }
1013
+
749
1014
  // src/targeting.ts
750
1015
  var import_node_path4 = require("node:path");
1016
+
1017
+ // src/deniedPaths.ts
1018
+ function globToRegExp(pattern) {
1019
+ const escaped = pattern.split("").map((c) => /[.+^${}()|[\]\\]/.test(c) ? `\\${c}` : c).join("");
1020
+ const withGlobs = escaped.replace(/\*\*\//g, " SLASH ").replace(/\*\*/g, " STAR ").replace(/\*/g, "[^/]*").replace(/ SLASH /g, "(?:.*/)?").replace(/ STAR /g, ".*");
1021
+ return new RegExp(`^${withGlobs}$`);
1022
+ }
1023
+ function matchesDeniedPath(path, patterns) {
1024
+ for (const pattern of patterns) {
1025
+ if (globToRegExp(pattern).test(path)) return pattern;
1026
+ }
1027
+ return null;
1028
+ }
1029
+
1030
+ // src/targeting.ts
1031
+ var CONTRACT_VERSION2 = "test-targets-1";
751
1032
  function listWorkspaceFiles(workspaceRoot) {
752
1033
  return listFiles(workspaceRoot ?? ".");
753
1034
  }
754
- function validateTarget(target, workspaceRoot, workspaceListing) {
1035
+ function validateTarget(target, workspaceRoot, workspaceListing, deniedPaths) {
1036
+ const denied = matchesDeniedPath(target.path, deniedPaths);
1037
+ if (denied) {
1038
+ throw new Error(
1039
+ `pilot.targeting proposed a target matching a denied path (${denied}): ${target.path}`
1040
+ );
1041
+ }
755
1042
  if (target.kind !== "EXISTING" && target.kind !== "NEW_FILE") {
756
1043
  throw new Error(`pilot.targeting returned an unknown kind "${target.kind}" for ${target.path}`);
757
1044
  }
@@ -771,8 +1058,22 @@ function validateTarget(target, workspaceRoot, workspaceListing) {
771
1058
  );
772
1059
  }
773
1060
  }
774
- async function runTargeting(gateway, design, workspaceRoot, workspaceListing, requiredTests) {
775
- const res = await fetch(`${gateway.baseUrl}/ai/execute`, {
1061
+ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, requiredTests, deniedPaths) {
1062
+ const contextData = {
1063
+ // NESTED, NOT FLATTENED — a second vocabulary for the design shape is exactly what
1064
+ // QUIKTR-1034 cost.
1065
+ workspaceListing,
1066
+ design,
1067
+ // QUIKTR-1065/1083 — requiredTests carried through so targeting can propose a test-file
1068
+ // target naming which cases it covers (§15.1's Case shape, unchanged).
1069
+ requiredTests,
1070
+ // QUIKTR-1083 (§15.6) — required on the versioned path.
1071
+ contractVersion: CONTRACT_VERSION2
1072
+ };
1073
+ if (deniedPaths.length > 0) {
1074
+ contextData.deniedPaths = deniedPaths;
1075
+ }
1076
+ const res = await fetchWithRetry(`${gateway.baseUrl}/ai/execute`, {
776
1077
  method: "POST",
777
1078
  headers: { "content-type": "application/json", "x-run-token": gateway.token },
778
1079
  body: JSON.stringify({
@@ -780,18 +1081,14 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, re
780
1081
  useCase: "pilot.targeting",
781
1082
  expectedOutputType: "json",
782
1083
  userPrompt: "",
783
- // NESTED, NOT FLATTENED — a second vocabulary for the design shape is exactly what
784
- // QUIKTR-1034 cost.
785
- // QUIKTR-1065 — requiredTests carried through so targeting CAN propose a test-file
786
- // target naming which cases it covers. The AI Runtime prompt doesn't read this yet
787
- // (that half is theirs, not this package's) — sending it now is what makes it possible.
788
- contextData: { workspaceListing, design, requiredTests }
1084
+ contextData
789
1085
  })
790
1086
  });
791
1087
  if (!res.ok) {
792
1088
  throw new Error(`Targeting call failed: ${res.status} ${res.statusText}`);
793
1089
  }
794
1090
  const envelope = await res.json();
1091
+ checkHardFailure(envelope, "pilot.targeting");
795
1092
  const body = unwrapExecute(envelope, "pilot.targeting");
796
1093
  if (body.outcome === "NO_TARGET") {
797
1094
  return { outcome: "NO_TARGET", reason: body.noTargetReason ?? null };
@@ -801,9 +1098,13 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, re
801
1098
  }
802
1099
  const listing = new Set(workspaceListing);
803
1100
  for (const target of body.targets) {
804
- validateTarget(target, workspaceRoot, listing);
1101
+ validateTarget(target, workspaceRoot, listing, deniedPaths);
805
1102
  }
806
- return { outcome: "TARGETED", targets: body.targets };
1103
+ return {
1104
+ outcome: "TARGETED",
1105
+ targets: body.targets,
1106
+ skippedCases: body.skippedCases ?? []
1107
+ };
807
1108
  }
808
1109
 
809
1110
  // src/tokenExchange.ts
@@ -813,7 +1114,7 @@ async function exchangeForRunToken(tokenUrl, oidcToken) {
813
1114
  headers: { Authorization: `Bearer ${oidcToken}` }
814
1115
  });
815
1116
  if (!res.ok) {
816
- throw new Error(`Exchange run token failed: ${res.status} ${res.statusText}`);
1117
+ throw new Error(`Exchange run token failed: ${await describeQuikTrackFailure(res)}`);
817
1118
  }
818
1119
  const body = await res.json();
819
1120
  if (body.success !== true) {
@@ -823,7 +1124,7 @@ async function exchangeForRunToken(tokenUrl, oidcToken) {
823
1124
  }
824
1125
 
825
1126
  // src/run.ts
826
- async function run(env = readRunnerEnv()) {
1127
+ async function run(env2) {
827
1128
  let hardFailure = false;
828
1129
  let packet;
829
1130
  let branch;
@@ -831,8 +1132,8 @@ async function run(env = readRunnerEnv()) {
831
1132
  let warnings = [];
832
1133
  let targeted = false;
833
1134
  try {
834
- runToken = await exchangeForRunToken(env.tokenUrl, await fetchGithubOidcToken());
835
- packet = await fetchPacket(env.packetUrl, runToken, env.githubRunId);
1135
+ runToken = await exchangeForRunToken(env2.tokenUrl, await fetchGithubOidcToken());
1136
+ packet = await fetchPacket(env2.packetUrl, runToken, env2.githubRunId);
836
1137
  checkoutTargetBranch(packet.targetBranch);
837
1138
  const workspaceListing = listWorkspaceFiles(packet.workspaceRoot);
838
1139
  const targeting = await runTargeting(
@@ -840,26 +1141,47 @@ async function run(env = readRunnerEnv()) {
840
1141
  packet.design,
841
1142
  packet.workspaceRoot,
842
1143
  workspaceListing,
843
- packet.requiredTests
1144
+ packet.requiredTests,
1145
+ packet.guardrails.deniedPaths
844
1146
  );
845
1147
  if (targeting.outcome === "NO_TARGET") {
846
- writePhase0Marker(env.runId);
1148
+ writePhase0Marker(env2.runId);
847
1149
  runCommand(packet.commands.setup, "Setup");
848
1150
  runCommand(packet.commands.lint, "Lint");
849
1151
  runTestCommand(packet.commands.test);
850
1152
  } else {
851
1153
  targeted = true;
1154
+ if (targeting.skippedCases?.length) {
1155
+ for (const skipped of targeting.skippedCases) {
1156
+ const w = `Test case ${skipped.automationId} has no given/when/then \u2014 skipped, no test was written for it.`;
1157
+ warnings.push(w);
1158
+ console.warn(`QuikPilot targeting warning (not blocking): ${w}`);
1159
+ }
1160
+ }
852
1161
  const overlaps = findOverlappingSteps(targeting.targets);
853
- if (overlaps.length > 0) {
854
- warnings = describeOverlapWarnings(overlaps);
1162
+ const blockingOverlaps = overlaps.filter((o) => o.severity === "identical");
1163
+ const softOverlaps = overlaps.filter((o) => o.severity === "similar");
1164
+ if (softOverlaps.length > 0) {
1165
+ warnings = describeOverlapWarnings(softOverlaps);
855
1166
  for (const w of warnings) {
856
1167
  console.warn(`QuikPilot targeting warning (not blocking): ${w}`);
857
1168
  }
858
1169
  }
1170
+ if (blockingOverlaps.length > 0) {
1171
+ throw new Error(
1172
+ `QuikPilot's generated plan gives two files identical instructions \u2014 ${describeOverlapWarnings(blockingOverlaps).join("; ")}`
1173
+ );
1174
+ }
859
1175
  runCommand(packet.commands.setup, "Setup");
860
1176
  const [baseline, results] = await Promise.all([
861
1177
  captureBaselineSnapshot(packet.commands.test),
862
- runCodegen(packet.gateway, targeting.targets, workspaceListing)
1178
+ runCodegen(
1179
+ packet.gateway,
1180
+ targeting.targets,
1181
+ workspaceListing,
1182
+ packet.requiredTests,
1183
+ packet.guardrails.deniedPaths
1184
+ )
863
1185
  ]);
864
1186
  const depCheck = checkImportDependencies(
865
1187
  results,
@@ -871,6 +1193,12 @@ async function run(env = readRunnerEnv()) {
871
1193
  `QuikPilot's generated code imports packages this repository doesn't have \u2014 ${describeDependencyViolations(depCheck)}`
872
1194
  );
873
1195
  }
1196
+ const scopeCheck = checkApiScope(results, targeting.targets, packet.design?.apiChanges ?? []);
1197
+ if (scopeCheck.blocked) {
1198
+ throw new Error(
1199
+ `QuikPilot's generated code exceeds the approved design \u2014 ${describeScopeViolations(scopeCheck)}`
1200
+ );
1201
+ }
874
1202
  flushCodegenResults(results);
875
1203
  try {
876
1204
  runCommand(packet.commands.format, "Format");
@@ -897,20 +1225,20 @@ async function run(env = readRunnerEnv()) {
897
1225
  await reportToQuikTest({
898
1226
  resultsUrl: packet?.quikTest?.resultsUrl,
899
1227
  runToken,
900
- githubServerUrl: env.githubServerUrl,
901
- githubRepository: env.githubRepository,
902
- githubRunId: env.githubRunId
1228
+ githubServerUrl: env2.githubServerUrl,
1229
+ githubRepository: env2.githubRepository,
1230
+ githubRunId: env2.githubRunId
903
1231
  });
904
1232
  if (!hardFailure && packet) {
905
1233
  try {
906
1234
  checkGuardrail(packet.guardrails.deniedPaths);
907
1235
  const installationToken = await exchangeForInstallationToken(
908
- env.installationTokenUrl,
1236
+ env2.installationTokenUrl,
909
1237
  runToken
910
1238
  );
911
1239
  const result = commitAndPush(
912
1240
  packet.branch,
913
- env.runId,
1241
+ env2.runId,
914
1242
  installationToken,
915
1243
  targeted,
916
1244
  packet.issueKey,
@@ -923,7 +1251,7 @@ async function run(env = readRunnerEnv()) {
923
1251
  }
924
1252
  }
925
1253
  await reportBack({
926
- callbackUrl: env.callbackUrl,
1254
+ callbackUrl: env2.callbackUrl,
927
1255
  runToken,
928
1256
  status: hardFailure ? "failed" : "completed",
929
1257
  branch,
@@ -932,8 +1260,44 @@ async function run(env = readRunnerEnv()) {
932
1260
  return { hardFailure };
933
1261
  }
934
1262
 
1263
+ // src/readinessCheck.ts
1264
+ async function runReadinessCheck(env2) {
1265
+ let setupOk = true;
1266
+ try {
1267
+ runCommand(env2.setupCmd || null, "Setup");
1268
+ } catch (err) {
1269
+ setupOk = false;
1270
+ console.error(err instanceof Error ? err.message : String(err));
1271
+ }
1272
+ const testOutcome = runTestCommand(env2.testCmd);
1273
+ const testOk = testOutcome === "success";
1274
+ const junitPath = findJUnitReport();
1275
+ const result = {
1276
+ setupOk,
1277
+ testOk,
1278
+ junitFound: junitPath !== null,
1279
+ junitPath
1280
+ };
1281
+ try {
1282
+ const res = await fetch(env2.readinessCallbackUrl, {
1283
+ method: "POST",
1284
+ headers: { Authorization: `Bearer ${env2.readinessToken}`, "Content-Type": "application/json" },
1285
+ body: JSON.stringify(result)
1286
+ });
1287
+ if (!res.ok) {
1288
+ throw new Error(`Readiness callback POST failed: ${await describeQuikTrackFailure(res)}`);
1289
+ }
1290
+ } catch (err) {
1291
+ console.warn(
1292
+ `::warning::QuikPilot readiness check failed to report its result: ${err instanceof Error ? err.message : String(err)}`
1293
+ );
1294
+ }
1295
+ }
1296
+
935
1297
  // src/index.ts
936
- run().then(({ hardFailure }) => {
1298
+ var env = readRunnerEnv();
1299
+ var task = env.mode === "readiness_check" ? runReadinessCheck(env).then(() => ({ hardFailure: false })) : run(env);
1300
+ task.then(({ hardFailure }) => {
937
1301
  process.exitCode = hardFailure ? 1 : 0;
938
1302
  }).catch((err) => {
939
1303
  console.error(err instanceof Error ? err.message : String(err));