@quikit/quikpilot-runner 7.0.5 → 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 +657 -87
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -23,18 +23,67 @@ 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
|
-
const { callbackUrl, runToken, status, branch } = params;
|
|
76
|
+
const { callbackUrl, runToken, status, branch, warnings } = params;
|
|
29
77
|
const body = { status };
|
|
30
78
|
if (branch) body.branch = branch;
|
|
79
|
+
if (warnings && warnings.length > 0) body.warnings = warnings;
|
|
31
80
|
const res = await fetch(callbackUrl, {
|
|
32
81
|
method: "POST",
|
|
33
82
|
headers: { Authorization: `Bearer ${runToken}`, "Content-Type": "application/json" },
|
|
34
83
|
body: JSON.stringify(body)
|
|
35
84
|
});
|
|
36
85
|
if (!res.ok) {
|
|
37
|
-
throw new Error(`Report back to QuikTrack failed: ${
|
|
86
|
+
throw new Error(`Report back to QuikTrack failed: ${await describeQuikTrackFailure(res)}`);
|
|
38
87
|
}
|
|
39
88
|
}
|
|
40
89
|
|
|
@@ -130,14 +179,21 @@ function push(branch, installationToken) {
|
|
|
130
179
|
}
|
|
131
180
|
|
|
132
181
|
// src/commit.ts
|
|
133
|
-
function
|
|
182
|
+
function buildCommitMessage(runId, targeted, issueKey, issueTitle) {
|
|
183
|
+
if (!targeted) return `QuikPilot Phase 0 skeleton (run ${runId})`;
|
|
184
|
+
if (issueKey && issueTitle) return `${issueKey}: ${issueTitle}
|
|
185
|
+
|
|
186
|
+
QuikPilot run ${runId}`;
|
|
187
|
+
return `QuikPilot generated changes (run ${runId})`;
|
|
188
|
+
}
|
|
189
|
+
function commitAndPush(branch, runId, installationToken, targeted, issueKey, issueTitle) {
|
|
134
190
|
if (!hasStagedChanges()) return { changed: false };
|
|
135
191
|
if (!branch) {
|
|
136
192
|
throw new Error("packet carried no branch name");
|
|
137
193
|
}
|
|
138
194
|
configureCommitIdentity();
|
|
139
195
|
checkoutNewBranch(branch);
|
|
140
|
-
commit(
|
|
196
|
+
commit(buildCommitMessage(runId, targeted, issueKey, issueTitle));
|
|
141
197
|
push(branch, installationToken);
|
|
142
198
|
return { changed: true, branch };
|
|
143
199
|
}
|
|
@@ -147,6 +203,14 @@ var import_node_fs = require("node:fs");
|
|
|
147
203
|
var import_node_path = require("node:path");
|
|
148
204
|
|
|
149
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
|
+
}
|
|
150
214
|
function unwrapExecute(envelope, useCase) {
|
|
151
215
|
if (envelope?.success === false) {
|
|
152
216
|
throw new Error(
|
|
@@ -159,35 +223,131 @@ function unwrapExecute(envelope, useCase) {
|
|
|
159
223
|
return envelope.structuredJson;
|
|
160
224
|
}
|
|
161
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
|
+
|
|
162
252
|
// src/codegen.ts
|
|
163
253
|
var FILE_TOO_LARGE_CODE = "CODEGEN_FILE_TOO_LARGE";
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
254
|
+
var CONTEXT_TOO_LARGE_CODE = "CODEGEN_CONTEXT_TOO_LARGE";
|
|
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(", ")}`;
|
|
172
312
|
}
|
|
313
|
+
return null;
|
|
173
314
|
}
|
|
174
|
-
|
|
315
|
+
var CONTRACT_VERSION = "test-targets-1";
|
|
316
|
+
async function runCodegen(gateway, targets, workspaceListing, requiredTests, deniedPaths) {
|
|
175
317
|
const results = [];
|
|
318
|
+
const listing = new Set(workspaceListing);
|
|
176
319
|
for (const target of targets) {
|
|
177
320
|
const isNewFile = target.kind === "NEW_FILE";
|
|
178
321
|
const currentContent = isNewFile ? "" : (0, import_node_fs.readFileSync)(target.path, "utf8");
|
|
322
|
+
const contextPaths = target.contextPaths ?? [];
|
|
323
|
+
for (const contextPath of contextPaths) {
|
|
324
|
+
if (!listing.has(contextPath)) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
`pilot.codegen contextPaths named a file not found in the checkout: ${contextPath} (target ${target.path})`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const contextContents = contextPaths.map((path) => ({ path, content: (0, import_node_fs.readFileSync)(path, "utf8") }));
|
|
179
331
|
const contextData = {
|
|
180
332
|
targetPath: target.path,
|
|
181
333
|
currentContent,
|
|
182
334
|
planSteps: target.planStepsImplemented,
|
|
183
|
-
kind: target.kind
|
|
335
|
+
kind: target.kind,
|
|
336
|
+
contextContents,
|
|
337
|
+
// QUIKTR-1083 (§15.6) — required on every call on the versioned path.
|
|
338
|
+
contractVersion: CONTRACT_VERSION
|
|
184
339
|
};
|
|
185
340
|
if (isNewFile) {
|
|
186
341
|
contextData.workspaceListing = workspaceListing;
|
|
187
|
-
const sibling = findNearestSibling(target.path, workspaceListing);
|
|
188
|
-
if (sibling) contextData.siblingContent = (0, import_node_fs.readFileSync)(sibling, "utf8");
|
|
189
342
|
}
|
|
190
|
-
|
|
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`, {
|
|
191
351
|
method: "POST",
|
|
192
352
|
headers: { "content-type": "application/json", "x-run-token": gateway.token },
|
|
193
353
|
body: JSON.stringify({
|
|
@@ -202,8 +362,10 @@ async function runCodegen(gateway, targets, workspaceListing) {
|
|
|
202
362
|
throw new Error(`Codegen call failed for ${target.path}: ${res.status} ${res.statusText}`);
|
|
203
363
|
}
|
|
204
364
|
const envelope = await res.json();
|
|
205
|
-
|
|
206
|
-
|
|
365
|
+
checkHardFailure(envelope, "pilot.codegen");
|
|
366
|
+
if (typeof envelope?.errorCode === "string" && NOT_RETRIED_CODES.has(envelope.errorCode)) {
|
|
367
|
+
const detail = envelope.error ? `: ${envelope.error}` : "";
|
|
368
|
+
throw new Error(`Codegen refused ${target.path}: ${envelope.errorCode} (not retried)${detail}`);
|
|
207
369
|
}
|
|
208
370
|
const body = unwrapExecute(envelope, "pilot.codegen");
|
|
209
371
|
if (body.outcome !== "GENERATED" || typeof body.newContent !== "string") {
|
|
@@ -211,6 +373,24 @@ async function runCodegen(gateway, targets, workspaceListing) {
|
|
|
211
373
|
`Codegen refused ${target.path}${body.refusalReason ? `: ${body.refusalReason}` : ""}`
|
|
212
374
|
);
|
|
213
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
|
+
);
|
|
214
394
|
results.push({ targetPath: target.path, newContent: body.newContent });
|
|
215
395
|
}
|
|
216
396
|
return results;
|
|
@@ -265,30 +445,32 @@ var NODE_BUILTINS = /* @__PURE__ */ new Set([
|
|
|
265
445
|
"inspector",
|
|
266
446
|
"async_hooks"
|
|
267
447
|
]);
|
|
268
|
-
|
|
448
|
+
var DEFAULT_ALIAS_PREFIXES = ["@/", "~/"];
|
|
449
|
+
function packageNameFromSpecifier(specifier, aliasPrefixes = DEFAULT_ALIAS_PREFIXES) {
|
|
269
450
|
if (specifier.startsWith(".") || specifier.startsWith("/")) return null;
|
|
270
451
|
if (specifier.startsWith("node:")) return null;
|
|
452
|
+
if (aliasPrefixes.some((prefix) => specifier.startsWith(prefix))) return null;
|
|
271
453
|
const segments = specifier.split("/");
|
|
272
454
|
const name = specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0];
|
|
273
455
|
if (NODE_BUILTINS.has(name)) return null;
|
|
274
456
|
return name;
|
|
275
457
|
}
|
|
276
|
-
function scanImports(source) {
|
|
458
|
+
function scanImports(source, aliasPrefixes = DEFAULT_ALIAS_PREFIXES) {
|
|
277
459
|
const found = /* @__PURE__ */ new Set();
|
|
278
460
|
let match;
|
|
279
461
|
IMPORT_RE.lastIndex = 0;
|
|
280
462
|
while (match = IMPORT_RE.exec(source)) {
|
|
281
|
-
const name = packageNameFromSpecifier(match[2]);
|
|
463
|
+
const name = packageNameFromSpecifier(match[2], aliasPrefixes);
|
|
282
464
|
if (name) found.add(name);
|
|
283
465
|
}
|
|
284
466
|
return [...found];
|
|
285
467
|
}
|
|
286
|
-
function checkImportDependencies(results, declaredDependencies) {
|
|
468
|
+
function checkImportDependencies(results, declaredDependencies, aliasPrefixes = DEFAULT_ALIAS_PREFIXES) {
|
|
287
469
|
if (!declaredDependencies) return { blocked: false, violations: [] };
|
|
288
470
|
const violations = [];
|
|
289
471
|
for (const { targetPath, newContent } of results) {
|
|
290
472
|
if (!SCANNABLE_EXTENSIONS.some((ext) => targetPath.endsWith(ext))) continue;
|
|
291
|
-
const invented = scanImports(newContent).filter((pkg) => !declaredDependencies.has(pkg));
|
|
473
|
+
const invented = scanImports(newContent, aliasPrefixes).filter((pkg) => !declaredDependencies.has(pkg));
|
|
292
474
|
if (invented.length > 0) violations.push({ file: targetPath, packages: invented });
|
|
293
475
|
}
|
|
294
476
|
return { blocked: violations.length > 0, violations };
|
|
@@ -320,28 +502,86 @@ function readDeclaredDependencies(results, workspaceRoot) {
|
|
|
320
502
|
return null;
|
|
321
503
|
}
|
|
322
504
|
}
|
|
323
|
-
function
|
|
324
|
-
|
|
505
|
+
function stripJsonComments(text) {
|
|
506
|
+
let out = "";
|
|
507
|
+
let inString = false;
|
|
508
|
+
let inLineComment = false;
|
|
509
|
+
let inBlockComment = false;
|
|
510
|
+
for (let i = 0; i < text.length; i++) {
|
|
511
|
+
const c = text[i];
|
|
512
|
+
const next = text[i + 1];
|
|
513
|
+
if (inLineComment) {
|
|
514
|
+
if (c === "\n") {
|
|
515
|
+
inLineComment = false;
|
|
516
|
+
out += c;
|
|
517
|
+
}
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (inBlockComment) {
|
|
521
|
+
if (c === "*" && next === "/") {
|
|
522
|
+
inBlockComment = false;
|
|
523
|
+
i++;
|
|
524
|
+
}
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
if (inString) {
|
|
528
|
+
out += c;
|
|
529
|
+
if (c === "\\") {
|
|
530
|
+
out += next;
|
|
531
|
+
i++;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (c === '"') inString = false;
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
if (c === '"') {
|
|
538
|
+
inString = true;
|
|
539
|
+
out += c;
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
if (c === "/" && next === "/") {
|
|
543
|
+
inLineComment = true;
|
|
544
|
+
i++;
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (c === "/" && next === "*") {
|
|
548
|
+
inBlockComment = true;
|
|
549
|
+
i++;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
out += c;
|
|
553
|
+
}
|
|
554
|
+
return out.replace(/,(\s*[}\]])/g, "$1");
|
|
325
555
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
556
|
+
function resolveAliasPrefixes(results, workspaceRoot) {
|
|
557
|
+
const root = (0, import_node_path2.resolve)(workspaceRoot ?? ".");
|
|
558
|
+
for (const filename of ["tsconfig.json", "jsconfig.json"]) {
|
|
559
|
+
const configPath = (0, import_node_path2.join)(root, filename);
|
|
560
|
+
const generated = results.find((r) => (0, import_node_path2.resolve)(r.targetPath) === configPath);
|
|
561
|
+
let raw;
|
|
562
|
+
if (generated) {
|
|
563
|
+
raw = generated.newContent;
|
|
564
|
+
} else {
|
|
565
|
+
try {
|
|
566
|
+
raw = (0, import_node_fs2.readFileSync)(configPath, "utf8");
|
|
567
|
+
} catch {
|
|
568
|
+
raw = null;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (raw === null) continue;
|
|
572
|
+
try {
|
|
573
|
+
const parsed = JSON.parse(stripJsonComments(raw));
|
|
574
|
+
const paths = parsed.compilerOptions?.paths;
|
|
575
|
+
const fromConfig = paths && typeof paths === "object" ? Object.keys(paths).map((k) => k.endsWith("*") ? k.slice(0, -1) : k) : [];
|
|
576
|
+
return [.../* @__PURE__ */ new Set([...DEFAULT_ALIAS_PREFIXES, ...fromConfig])];
|
|
577
|
+
} catch {
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
331
580
|
}
|
|
332
|
-
return
|
|
581
|
+
return DEFAULT_ALIAS_PREFIXES;
|
|
333
582
|
}
|
|
334
|
-
function
|
|
335
|
-
return {
|
|
336
|
-
runId: required("RUN_ID", env.RUN_ID),
|
|
337
|
-
callbackUrl: required("CALLBACK_URL", env.CALLBACK_URL),
|
|
338
|
-
packetUrl: required("PACKET_URL", env.PACKET_URL),
|
|
339
|
-
tokenUrl: required("TOKEN_URL", env.TOKEN_URL),
|
|
340
|
-
installationTokenUrl: required("INSTALLATION_TOKEN_URL", env.INSTALLATION_TOKEN_URL),
|
|
341
|
-
githubRunId: required("GITHUB_RUN_ID", env.GITHUB_RUN_ID),
|
|
342
|
-
githubServerUrl: env.GITHUB_SERVER_URL ?? "https://github.com",
|
|
343
|
-
githubRepository: required("GITHUB_REPOSITORY", env.GITHUB_REPOSITORY)
|
|
344
|
-
};
|
|
583
|
+
function describeDependencyViolations(check) {
|
|
584
|
+
return check.violations.map((v) => `${v.file} imports ${v.packages.join(", ")}, not a dependency of this repository`).join("; ");
|
|
345
585
|
}
|
|
346
586
|
|
|
347
587
|
// src/guardrail.ts
|
|
@@ -361,7 +601,7 @@ async function exchangeForInstallationToken(installationTokenUrl, runToken) {
|
|
|
361
601
|
headers: { Authorization: `Bearer ${runToken}` }
|
|
362
602
|
});
|
|
363
603
|
if (!res.ok) {
|
|
364
|
-
throw new Error(`Exchange installation token failed: ${
|
|
604
|
+
throw new Error(`Exchange installation token failed: ${await describeQuikTrackFailure(res)}`);
|
|
365
605
|
}
|
|
366
606
|
const body = await res.json();
|
|
367
607
|
if (body.success !== true) {
|
|
@@ -372,14 +612,14 @@ async function exchangeForInstallationToken(installationTokenUrl, runToken) {
|
|
|
372
612
|
|
|
373
613
|
// src/oidc.ts
|
|
374
614
|
var QUIKPILOT_OIDC_AUDIENCE = "quiktrack-quikpilot";
|
|
375
|
-
async function fetchGithubOidcToken(
|
|
615
|
+
async function fetchGithubOidcToken(env2 = process.env) {
|
|
376
616
|
const requestUrl = required(
|
|
377
617
|
"ACTIONS_ID_TOKEN_REQUEST_URL",
|
|
378
|
-
|
|
618
|
+
env2.ACTIONS_ID_TOKEN_REQUEST_URL
|
|
379
619
|
);
|
|
380
620
|
const requestToken = required(
|
|
381
621
|
"ACTIONS_ID_TOKEN_REQUEST_TOKEN",
|
|
382
|
-
|
|
622
|
+
env2.ACTIONS_ID_TOKEN_REQUEST_TOKEN
|
|
383
623
|
);
|
|
384
624
|
const separator = requestUrl.includes("?") ? "&" : "?";
|
|
385
625
|
const url = `${requestUrl}${separator}audience=${encodeURIComponent(QUIKPILOT_OIDC_AUDIENCE)}`;
|
|
@@ -396,6 +636,108 @@ async function fetchGithubOidcToken(env = process.env) {
|
|
|
396
636
|
return body.value;
|
|
397
637
|
}
|
|
398
638
|
|
|
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
|
+
}
|
|
644
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
645
|
+
"a",
|
|
646
|
+
"an",
|
|
647
|
+
"the",
|
|
648
|
+
"is",
|
|
649
|
+
"are",
|
|
650
|
+
"was",
|
|
651
|
+
"were",
|
|
652
|
+
"be",
|
|
653
|
+
"been",
|
|
654
|
+
"being",
|
|
655
|
+
"of",
|
|
656
|
+
"to",
|
|
657
|
+
"in",
|
|
658
|
+
"on",
|
|
659
|
+
"for",
|
|
660
|
+
"with",
|
|
661
|
+
"at",
|
|
662
|
+
"by",
|
|
663
|
+
"from",
|
|
664
|
+
"as",
|
|
665
|
+
"and",
|
|
666
|
+
"or",
|
|
667
|
+
"not",
|
|
668
|
+
"no",
|
|
669
|
+
"nor",
|
|
670
|
+
"this",
|
|
671
|
+
"that",
|
|
672
|
+
"these",
|
|
673
|
+
"those",
|
|
674
|
+
"it",
|
|
675
|
+
"its",
|
|
676
|
+
"cannot",
|
|
677
|
+
"can't",
|
|
678
|
+
"should",
|
|
679
|
+
"must",
|
|
680
|
+
"may",
|
|
681
|
+
"will",
|
|
682
|
+
"shall",
|
|
683
|
+
"when",
|
|
684
|
+
"if",
|
|
685
|
+
"than",
|
|
686
|
+
"then",
|
|
687
|
+
"so",
|
|
688
|
+
"do",
|
|
689
|
+
"does",
|
|
690
|
+
"did"
|
|
691
|
+
]);
|
|
692
|
+
var MIN_WORD_LENGTH = 3;
|
|
693
|
+
function significantWords(step) {
|
|
694
|
+
return new Set(
|
|
695
|
+
step.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter((w) => w.length >= MIN_WORD_LENGTH && !STOPWORDS.has(w))
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
function jaccard(a, b) {
|
|
699
|
+
if (a.size === 0 || b.size === 0) return 0;
|
|
700
|
+
let intersection = 0;
|
|
701
|
+
for (const w of a) if (b.has(w)) intersection++;
|
|
702
|
+
const union = a.size + b.size - intersection;
|
|
703
|
+
return intersection / union;
|
|
704
|
+
}
|
|
705
|
+
function normalizedStep(step) {
|
|
706
|
+
return step.trim().toLowerCase().replace(/\s+/g, " ");
|
|
707
|
+
}
|
|
708
|
+
var OVERLAP_THRESHOLD = 0.5;
|
|
709
|
+
function findOverlappingSteps(targets) {
|
|
710
|
+
const items = targets.flatMap(
|
|
711
|
+
(target) => target.planStepsImplemented.map((step) => ({
|
|
712
|
+
target: target.path,
|
|
713
|
+
step,
|
|
714
|
+
words: significantWords(step)
|
|
715
|
+
}))
|
|
716
|
+
);
|
|
717
|
+
const warnings = [];
|
|
718
|
+
for (let i = 0; i < items.length; i++) {
|
|
719
|
+
for (let j = i + 1; j < items.length; j++) {
|
|
720
|
+
if (items[i].target === items[j].target) continue;
|
|
721
|
+
if (isTestTarget(items[i].target) || isTestTarget(items[j].target)) continue;
|
|
722
|
+
if (jaccard(items[i].words, items[j].words) >= OVERLAP_THRESHOLD) {
|
|
723
|
+
warnings.push({
|
|
724
|
+
targetA: items[i].target,
|
|
725
|
+
stepA: items[i].step,
|
|
726
|
+
targetB: items[j].target,
|
|
727
|
+
stepB: items[j].step,
|
|
728
|
+
severity: normalizedStep(items[i].step) === normalizedStep(items[j].step) ? "identical" : "similar"
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return warnings;
|
|
734
|
+
}
|
|
735
|
+
function describeOverlapWarnings(warnings) {
|
|
736
|
+
return warnings.map(
|
|
737
|
+
(w) => `${w.targetA} and ${w.targetB} both implement what looks like the same rule \u2014 "${w.stepA}" / "${w.stepB}". Check these aren't two independent implementations of the same behavior.`
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
|
|
399
741
|
// src/packet.ts
|
|
400
742
|
async function fetchPacket(packetUrl, runToken, githubRunId) {
|
|
401
743
|
const url = `${packetUrl}?githubRunId=${encodeURIComponent(githubRunId)}`;
|
|
@@ -403,7 +745,7 @@ async function fetchPacket(packetUrl, runToken, githubRunId) {
|
|
|
403
745
|
headers: { Authorization: `Bearer ${runToken}` }
|
|
404
746
|
});
|
|
405
747
|
if (!res.ok) {
|
|
406
|
-
throw new Error(`Fetch work packet failed: ${
|
|
748
|
+
throw new Error(`Fetch work packet failed: ${await describeQuikTrackFailure(res)}`);
|
|
407
749
|
}
|
|
408
750
|
const body = await res.json();
|
|
409
751
|
if (body.success !== true) {
|
|
@@ -479,9 +821,9 @@ async function reportToQuikTest(params) {
|
|
|
479
821
|
try {
|
|
480
822
|
console.log(`reporting ${report}`);
|
|
481
823
|
const ciUrl = buildCiUrl(githubServerUrl, githubRepository, githubRunId);
|
|
482
|
-
const { readFileSync:
|
|
483
|
-
const body =
|
|
484
|
-
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`;
|
|
485
827
|
const res = await fetch(url, {
|
|
486
828
|
method: "POST",
|
|
487
829
|
headers: { Authorization: `Bearer ${runToken}`, "Content-Type": "text/xml" },
|
|
@@ -502,8 +844,24 @@ var TESTCASE_RE = /<testcase\b([^>]*?)(?:\/>|>([\s\S]*?)<\/testcase>)/g;
|
|
|
502
844
|
var NAME_ATTR_RE = /\bname="([^"]*)"/;
|
|
503
845
|
var CLASSNAME_ATTR_RE = /\bclassname="([^"]*)"/;
|
|
504
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(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/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
|
+
}
|
|
505
862
|
function parseJUnitOutcomes(xml) {
|
|
506
|
-
const
|
|
863
|
+
const outcomes = /* @__PURE__ */ new Map();
|
|
864
|
+
const failureText = /* @__PURE__ */ new Map();
|
|
507
865
|
let match;
|
|
508
866
|
TESTCASE_RE.lastIndex = 0;
|
|
509
867
|
while (match = TESTCASE_RE.exec(xml)) {
|
|
@@ -512,9 +870,14 @@ function parseJUnitOutcomes(xml) {
|
|
|
512
870
|
if (!name) continue;
|
|
513
871
|
const classname = CLASSNAME_ATTR_RE.exec(attrs)?.[1];
|
|
514
872
|
const id = classname ? `${classname}::${name}` : name;
|
|
515
|
-
|
|
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
|
+
}
|
|
516
879
|
}
|
|
517
|
-
return
|
|
880
|
+
return { outcomes, failureText };
|
|
518
881
|
}
|
|
519
882
|
function readJUnitOutcomes(cwd) {
|
|
520
883
|
const report = findJUnitReport(cwd);
|
|
@@ -527,35 +890,55 @@ function readJUnitOutcomes(cwd) {
|
|
|
527
890
|
}
|
|
528
891
|
function captureTestSnapshot(command, cwd = process.cwd()) {
|
|
529
892
|
const outcome = runTestCommand(command);
|
|
530
|
-
|
|
893
|
+
const parsed = readJUnitOutcomes(cwd);
|
|
894
|
+
return { outcome, results: parsed?.outcomes ?? null, failureText: parsed?.failureText };
|
|
531
895
|
}
|
|
532
896
|
async function captureBaselineSnapshot(command, cwd = process.cwd()) {
|
|
533
897
|
const outcome = await runTestCommandAsync(command);
|
|
534
|
-
|
|
898
|
+
const parsed = readJUnitOutcomes(cwd);
|
|
899
|
+
return { outcome, results: parsed?.outcomes ?? null, failureText: parsed?.failureText };
|
|
535
900
|
}
|
|
536
901
|
function checkForRegressions(baseline, after) {
|
|
537
902
|
if (!baseline.results || !after.results) {
|
|
538
|
-
return { blocked: false, regressions: [], newFailures: [], preExisting: [] };
|
|
903
|
+
return { blocked: false, regressions: [], newFailures: [], preExisting: [], failureText: /* @__PURE__ */ new Map() };
|
|
539
904
|
}
|
|
540
905
|
const regressions = [];
|
|
541
906
|
const newFailures = [];
|
|
542
907
|
const preExisting = [];
|
|
908
|
+
const failureText = /* @__PURE__ */ new Map();
|
|
909
|
+
const afterFailureText = after.failureText;
|
|
543
910
|
for (const [testId, passed] of after.results) {
|
|
544
911
|
if (passed) continue;
|
|
545
912
|
const before = baseline.results.get(testId);
|
|
546
|
-
if (before === true)
|
|
547
|
-
|
|
548
|
-
|
|
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
|
+
}
|
|
549
924
|
}
|
|
550
|
-
return { blocked: regressions.length > 0 || newFailures.length > 0, regressions, newFailures, preExisting };
|
|
925
|
+
return { blocked: regressions.length > 0 || newFailures.length > 0, regressions, newFailures, preExisting, failureText };
|
|
551
926
|
}
|
|
552
927
|
function describeRegressions(check) {
|
|
928
|
+
const withText = (id) => {
|
|
929
|
+
const text = check.failureText.get(id);
|
|
930
|
+
return text ? `${id} (${text})` : id;
|
|
931
|
+
};
|
|
553
932
|
const parts = [];
|
|
554
933
|
if (check.regressions.length > 0) {
|
|
555
|
-
parts.push(
|
|
934
|
+
parts.push(
|
|
935
|
+
`${check.regressions.length} regression(s) (passed before, now fail): ${check.regressions.map(withText).join(", ")}`
|
|
936
|
+
);
|
|
556
937
|
}
|
|
557
938
|
if (check.newFailures.length > 0) {
|
|
558
|
-
parts.push(
|
|
939
|
+
parts.push(
|
|
940
|
+
`${check.newFailures.length} new failure(s) (did not exist before): ${check.newFailures.map(withText).join(", ")}`
|
|
941
|
+
);
|
|
559
942
|
}
|
|
560
943
|
if (check.preExisting.length > 0) {
|
|
561
944
|
parts.push(`${check.preExisting.length} pre-existing failure(s), unrelated to this change, not blocking`);
|
|
@@ -563,12 +946,99 @@ function describeRegressions(check) {
|
|
|
563
946
|
return parts.join("; ");
|
|
564
947
|
}
|
|
565
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
|
+
|
|
566
1014
|
// src/targeting.ts
|
|
567
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";
|
|
568
1032
|
function listWorkspaceFiles(workspaceRoot) {
|
|
569
1033
|
return listFiles(workspaceRoot ?? ".");
|
|
570
1034
|
}
|
|
571
|
-
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
|
+
}
|
|
572
1042
|
if (target.kind !== "EXISTING" && target.kind !== "NEW_FILE") {
|
|
573
1043
|
throw new Error(`pilot.targeting returned an unknown kind "${target.kind}" for ${target.path}`);
|
|
574
1044
|
}
|
|
@@ -588,8 +1058,22 @@ function validateTarget(target, workspaceRoot, workspaceListing) {
|
|
|
588
1058
|
);
|
|
589
1059
|
}
|
|
590
1060
|
}
|
|
591
|
-
async function runTargeting(gateway, design, workspaceRoot, workspaceListing) {
|
|
592
|
-
const
|
|
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`, {
|
|
593
1077
|
method: "POST",
|
|
594
1078
|
headers: { "content-type": "application/json", "x-run-token": gateway.token },
|
|
595
1079
|
body: JSON.stringify({
|
|
@@ -597,15 +1081,14 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing) {
|
|
|
597
1081
|
useCase: "pilot.targeting",
|
|
598
1082
|
expectedOutputType: "json",
|
|
599
1083
|
userPrompt: "",
|
|
600
|
-
|
|
601
|
-
// QUIKTR-1034 cost.
|
|
602
|
-
contextData: { workspaceListing, design }
|
|
1084
|
+
contextData
|
|
603
1085
|
})
|
|
604
1086
|
});
|
|
605
1087
|
if (!res.ok) {
|
|
606
1088
|
throw new Error(`Targeting call failed: ${res.status} ${res.statusText}`);
|
|
607
1089
|
}
|
|
608
1090
|
const envelope = await res.json();
|
|
1091
|
+
checkHardFailure(envelope, "pilot.targeting");
|
|
609
1092
|
const body = unwrapExecute(envelope, "pilot.targeting");
|
|
610
1093
|
if (body.outcome === "NO_TARGET") {
|
|
611
1094
|
return { outcome: "NO_TARGET", reason: body.noTargetReason ?? null };
|
|
@@ -615,9 +1098,13 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing) {
|
|
|
615
1098
|
}
|
|
616
1099
|
const listing = new Set(workspaceListing);
|
|
617
1100
|
for (const target of body.targets) {
|
|
618
|
-
validateTarget(target, workspaceRoot, listing);
|
|
1101
|
+
validateTarget(target, workspaceRoot, listing, deniedPaths);
|
|
619
1102
|
}
|
|
620
|
-
return {
|
|
1103
|
+
return {
|
|
1104
|
+
outcome: "TARGETED",
|
|
1105
|
+
targets: body.targets,
|
|
1106
|
+
skippedCases: body.skippedCases ?? []
|
|
1107
|
+
};
|
|
621
1108
|
}
|
|
622
1109
|
|
|
623
1110
|
// src/tokenExchange.ts
|
|
@@ -627,7 +1114,7 @@ async function exchangeForRunToken(tokenUrl, oidcToken) {
|
|
|
627
1114
|
headers: { Authorization: `Bearer ${oidcToken}` }
|
|
628
1115
|
});
|
|
629
1116
|
if (!res.ok) {
|
|
630
|
-
throw new Error(`Exchange run token failed: ${
|
|
1117
|
+
throw new Error(`Exchange run token failed: ${await describeQuikTrackFailure(res)}`);
|
|
631
1118
|
}
|
|
632
1119
|
const body = await res.json();
|
|
633
1120
|
if (body.success !== true) {
|
|
@@ -637,42 +1124,81 @@ async function exchangeForRunToken(tokenUrl, oidcToken) {
|
|
|
637
1124
|
}
|
|
638
1125
|
|
|
639
1126
|
// src/run.ts
|
|
640
|
-
async function run(
|
|
1127
|
+
async function run(env2) {
|
|
641
1128
|
let hardFailure = false;
|
|
642
1129
|
let packet;
|
|
643
1130
|
let branch;
|
|
644
1131
|
let runToken;
|
|
1132
|
+
let warnings = [];
|
|
1133
|
+
let targeted = false;
|
|
645
1134
|
try {
|
|
646
|
-
runToken = await exchangeForRunToken(
|
|
647
|
-
packet = await fetchPacket(
|
|
1135
|
+
runToken = await exchangeForRunToken(env2.tokenUrl, await fetchGithubOidcToken());
|
|
1136
|
+
packet = await fetchPacket(env2.packetUrl, runToken, env2.githubRunId);
|
|
648
1137
|
checkoutTargetBranch(packet.targetBranch);
|
|
649
1138
|
const workspaceListing = listWorkspaceFiles(packet.workspaceRoot);
|
|
650
1139
|
const targeting = await runTargeting(
|
|
651
1140
|
packet.gateway,
|
|
652
1141
|
packet.design,
|
|
653
1142
|
packet.workspaceRoot,
|
|
654
|
-
workspaceListing
|
|
1143
|
+
workspaceListing,
|
|
1144
|
+
packet.requiredTests,
|
|
1145
|
+
packet.guardrails.deniedPaths
|
|
655
1146
|
);
|
|
656
1147
|
if (targeting.outcome === "NO_TARGET") {
|
|
657
|
-
writePhase0Marker(
|
|
1148
|
+
writePhase0Marker(env2.runId);
|
|
658
1149
|
runCommand(packet.commands.setup, "Setup");
|
|
659
1150
|
runCommand(packet.commands.lint, "Lint");
|
|
660
1151
|
runTestCommand(packet.commands.test);
|
|
661
1152
|
} else {
|
|
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
|
+
}
|
|
1161
|
+
const overlaps = findOverlappingSteps(targeting.targets);
|
|
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);
|
|
1166
|
+
for (const w of warnings) {
|
|
1167
|
+
console.warn(`QuikPilot targeting warning (not blocking): ${w}`);
|
|
1168
|
+
}
|
|
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
|
+
}
|
|
662
1175
|
runCommand(packet.commands.setup, "Setup");
|
|
663
1176
|
const [baseline, results] = await Promise.all([
|
|
664
1177
|
captureBaselineSnapshot(packet.commands.test),
|
|
665
|
-
runCodegen(
|
|
1178
|
+
runCodegen(
|
|
1179
|
+
packet.gateway,
|
|
1180
|
+
targeting.targets,
|
|
1181
|
+
workspaceListing,
|
|
1182
|
+
packet.requiredTests,
|
|
1183
|
+
packet.guardrails.deniedPaths
|
|
1184
|
+
)
|
|
666
1185
|
]);
|
|
667
1186
|
const depCheck = checkImportDependencies(
|
|
668
1187
|
results,
|
|
669
|
-
readDeclaredDependencies(results, packet.workspaceRoot)
|
|
1188
|
+
readDeclaredDependencies(results, packet.workspaceRoot),
|
|
1189
|
+
resolveAliasPrefixes(results, packet.workspaceRoot)
|
|
670
1190
|
);
|
|
671
1191
|
if (depCheck.blocked) {
|
|
672
1192
|
throw new Error(
|
|
673
1193
|
`QuikPilot's generated code imports packages this repository doesn't have \u2014 ${describeDependencyViolations(depCheck)}`
|
|
674
1194
|
);
|
|
675
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
|
+
}
|
|
676
1202
|
flushCodegenResults(results);
|
|
677
1203
|
try {
|
|
678
1204
|
runCommand(packet.commands.format, "Format");
|
|
@@ -699,18 +1225,25 @@ async function run(env = readRunnerEnv()) {
|
|
|
699
1225
|
await reportToQuikTest({
|
|
700
1226
|
resultsUrl: packet?.quikTest?.resultsUrl,
|
|
701
1227
|
runToken,
|
|
702
|
-
githubServerUrl:
|
|
703
|
-
githubRepository:
|
|
704
|
-
githubRunId:
|
|
1228
|
+
githubServerUrl: env2.githubServerUrl,
|
|
1229
|
+
githubRepository: env2.githubRepository,
|
|
1230
|
+
githubRunId: env2.githubRunId
|
|
705
1231
|
});
|
|
706
1232
|
if (!hardFailure && packet) {
|
|
707
1233
|
try {
|
|
708
1234
|
checkGuardrail(packet.guardrails.deniedPaths);
|
|
709
1235
|
const installationToken = await exchangeForInstallationToken(
|
|
710
|
-
|
|
1236
|
+
env2.installationTokenUrl,
|
|
711
1237
|
runToken
|
|
712
1238
|
);
|
|
713
|
-
const result = commitAndPush(
|
|
1239
|
+
const result = commitAndPush(
|
|
1240
|
+
packet.branch,
|
|
1241
|
+
env2.runId,
|
|
1242
|
+
installationToken,
|
|
1243
|
+
targeted,
|
|
1244
|
+
packet.issueKey,
|
|
1245
|
+
packet.issueTitle
|
|
1246
|
+
);
|
|
714
1247
|
branch = result.branch;
|
|
715
1248
|
} catch (err) {
|
|
716
1249
|
hardFailure = true;
|
|
@@ -718,16 +1251,53 @@ async function run(env = readRunnerEnv()) {
|
|
|
718
1251
|
}
|
|
719
1252
|
}
|
|
720
1253
|
await reportBack({
|
|
721
|
-
callbackUrl:
|
|
1254
|
+
callbackUrl: env2.callbackUrl,
|
|
722
1255
|
runToken,
|
|
723
1256
|
status: hardFailure ? "failed" : "completed",
|
|
724
|
-
branch
|
|
1257
|
+
branch,
|
|
1258
|
+
warnings
|
|
725
1259
|
});
|
|
726
1260
|
return { hardFailure };
|
|
727
1261
|
}
|
|
728
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
|
+
|
|
729
1297
|
// src/index.ts
|
|
730
|
-
|
|
1298
|
+
var env = readRunnerEnv();
|
|
1299
|
+
var task = env.mode === "readiness_check" ? runReadinessCheck(env).then(() => ({ hardFailure: false })) : run(env);
|
|
1300
|
+
task.then(({ hardFailure }) => {
|
|
731
1301
|
process.exitCode = hardFailure ? 1 : 0;
|
|
732
1302
|
}).catch((err) => {
|
|
733
1303
|
console.error(err instanceof Error ? err.message : String(err));
|