@quikit/quikpilot-runner 8.2.3 → 8.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,27 +1,27 @@
1
- # @quikit/quikpilot-runner
2
-
3
- QuikPilot's CI runner. Invoked automatically by the `.github/workflows/quikpilot.yml` file QuikPilot
4
- installs in an onboarded repository — it fetches the run's work packet, runs your setup/lint/test
5
- commands, reports results to QuikTest, enforces the denied-path guardrail, commits, pushes, and
6
- reports back to QuikTrack.
7
-
8
- This package is not meant to be installed or run by hand. It exists so a fix to QuikPilot's runner
9
- logic ships as a new package version rather than a new pull request into every onboarded repository
10
- — the workflow file itself lives in your repository (QuikIT cannot update it directly), so it stays a
11
- thin, ~25-line launcher that invokes an exact, pinned version of this package via `npx`.
12
-
13
- It carries no secrets and needs none of your credentials: it authenticates to QuikTrack and to GitHub
14
- using short-lived tokens exchanged at runtime, never a value baked into the workflow file or this
15
- package.
16
-
17
- ## Versioning and support
18
-
19
- Every workflow file pins an **exact** version of this package (`npx --yes
20
- @quikit/quikpilot-runner@<version>`, never a range or `latest`) — a repository never picks up a new
21
- release on its own. Older published versions are not removed from npm and keep working as long as a
22
- repository's workflow file keeps pinning them; upgrading to a newer version only happens through a new
23
- pull request into that repository (QuikTrack's "re-bootstrap" flow), never silently.
24
-
25
- Releases follow [SemVer](https://semver.org/): a major version bump means the workflow file's own
26
- shape changed (inputs, permissions) and requires a new pull request regardless of this package's
27
- version; minor/patch releases are runner-logic fixes safe to pick up on the next re-bootstrap.
1
+ # @quikit/quikpilot-runner
2
+
3
+ QuikPilot's CI runner. Invoked automatically by the `.github/workflows/quikpilot.yml` file QuikPilot
4
+ installs in an onboarded repository — it fetches the run's work packet, runs your setup/lint/test
5
+ commands, reports results to QuikTest, enforces the denied-path guardrail, commits, pushes, and
6
+ reports back to QuikTrack.
7
+
8
+ This package is not meant to be installed or run by hand. It exists so a fix to QuikPilot's runner
9
+ logic ships as a new package version rather than a new pull request into every onboarded repository
10
+ — the workflow file itself lives in your repository (QuikIT cannot update it directly), so it stays a
11
+ thin, ~25-line launcher that invokes an exact, pinned version of this package via `npx`.
12
+
13
+ It carries no secrets and needs none of your credentials: it authenticates to QuikTrack and to GitHub
14
+ using short-lived tokens exchanged at runtime, never a value baked into the workflow file or this
15
+ package.
16
+
17
+ ## Versioning and support
18
+
19
+ Every workflow file pins an **exact** version of this package (`npx --yes
20
+ @quikit/quikpilot-runner@<version>`, never a range or `latest`) — a repository never picks up a new
21
+ release on its own. Older published versions are not removed from npm and keep working as long as a
22
+ repository's workflow file keeps pinning them; upgrading to a newer version only happens through a new
23
+ pull request into that repository (QuikTrack's "re-bootstrap" flow), never silently.
24
+
25
+ Releases follow [SemVer](https://semver.org/): a major version bump means the workflow file's own
26
+ shape changed (inputs, permissions) and requires a new pull request regardless of this package's
27
+ version; minor/patch releases are runner-logic fixes safe to pick up on the next re-bootstrap.
package/dist/index.js CHANGED
@@ -73,10 +73,11 @@ async function describeQuikTrackFailure(res) {
73
73
 
74
74
  // src/callback.ts
75
75
  async function reportBack(params) {
76
- const { callbackUrl, runToken, status, branch, warnings } = params;
76
+ const { callbackUrl, runToken, status, branch, warnings, targeting } = params;
77
77
  const body = { status };
78
78
  if (branch) body.branch = branch;
79
79
  if (warnings && warnings.length > 0) body.warnings = warnings;
80
+ if (targeting) body.targeting = targeting;
80
81
  const res = await fetch(callbackUrl, {
81
82
  method: "POST",
82
83
  headers: { Authorization: `Bearer ${runToken}`, "Content-Type": "application/json" },
@@ -202,6 +203,26 @@ function commitAndPush(branch, runId, installationToken, targeted, issueKey, iss
202
203
  var import_node_fs = require("node:fs");
203
204
  var import_node_path = require("node:path");
204
205
 
206
+ // src/deniedPaths.ts
207
+ function globToRegExp(pattern) {
208
+ const escaped = pattern.split("").map((c) => /[.+^${}()|[\]\\]/.test(c) ? `\\${c}` : c).join("");
209
+ const withGlobs = escaped.replace(/\*\*\//g, " SLASH ").replace(/\*\*/g, " STAR ").replace(/\*/g, "[^/]*").replace(/ SLASH /g, "(?:.*/)?").replace(/ STAR /g, ".*");
210
+ return new RegExp(`^${withGlobs}$`);
211
+ }
212
+ function matchesDeniedPath(path, patterns) {
213
+ for (const pattern of patterns) {
214
+ if (globToRegExp(pattern).test(path)) return pattern;
215
+ }
216
+ return null;
217
+ }
218
+ function findDeniedContextPath(contextPaths, patterns) {
219
+ for (const path of contextPaths) {
220
+ const pattern = matchesDeniedPath(path, patterns);
221
+ if (pattern) return { path, pattern };
222
+ }
223
+ return null;
224
+ }
225
+
205
226
  // src/envelope.ts
206
227
  var HARD_FAILURE_CODES = /* @__PURE__ */ new Set(["SEMANTIC_MISMATCH", "CONTRACT_REJECTED"]);
207
228
  function checkHardFailure(envelope, useCase, target) {
@@ -256,6 +277,37 @@ var CONTEXT_TOO_LARGE_CODE = "CODEGEN_CONTEXT_TOO_LARGE";
256
277
  var OUTPUT_TOO_LARGE_CODE = "CODEGEN_OUTPUT_TOO_LARGE";
257
278
  var NOT_RETRIED_CODES = /* @__PURE__ */ new Set([FILE_TOO_LARGE_CODE, CONTEXT_TOO_LARGE_CODE, OUTPUT_TOO_LARGE_CODE]);
258
279
  var JUDGED_BY_TOO_LARGE_CODE = "CODEGEN_JUDGED_BY_TOO_LARGE";
280
+ var MAX_ADDED_FILE_CHARS = 4e4;
281
+ var MAX_ADDED_CONTEXT_CHARS = 6e4;
282
+ function collectRunContext(target, targets, alreadyNamed, deniedPaths, readFile = (p) => (0, import_node_fs.readFileSync)(p, "utf8")) {
283
+ const added = { entries: [], skipped: [] };
284
+ if (!target.automationIds?.length) return added;
285
+ const seen = new Set(alreadyNamed);
286
+ seen.add(target.path);
287
+ let total = 0;
288
+ for (const candidate of targets) {
289
+ if (candidate.kind !== "EXISTING" || candidate.automationIds?.length) continue;
290
+ if (seen.has(candidate.path)) continue;
291
+ seen.add(candidate.path);
292
+ if (matchesDeniedPath(candidate.path, deniedPaths)) continue;
293
+ let content;
294
+ try {
295
+ content = readFile(candidate.path);
296
+ } catch {
297
+ added.skipped.push({ path: candidate.path, reason: "unreadable" });
298
+ continue;
299
+ }
300
+ if (content.length > MAX_ADDED_FILE_CHARS) {
301
+ added.skipped.push({ path: candidate.path, reason: "too large" });
302
+ } else if (total + content.length > MAX_ADDED_CONTEXT_CHARS) {
303
+ added.skipped.push({ path: candidate.path, reason: "over budget" });
304
+ } else {
305
+ total += content.length;
306
+ added.entries.push({ path: candidate.path, content });
307
+ }
308
+ }
309
+ return added;
310
+ }
259
311
  var SUSPECT_PLACEHOLDER_RE = /rest(?: of the)? (?:file|code) unchanged|remainder unchanged|\/\/\s*\.\.\.\s*(?:rest|remaining)/i;
260
312
  var SUSPECT_LENGTH_DROP_RATIO = 0.6;
261
313
  var SUSPECT_REMOVED_LINE_RATIO = 0.3;
@@ -340,13 +392,19 @@ function narrowJudgedByForTarget(pool, targets, codeTargetPath) {
340
392
  function toWireJudgedBy(pool) {
341
393
  return pool.map(({ automationId: _automationId, ...rest }) => rest);
342
394
  }
343
- async function runCodegen(gateway, targets, workspaceListing, requiredTests, deniedPaths, judgedByPool, warnings) {
395
+ async function runCodegen(gateway, targets, workspaceListing, requiredTests, deniedPaths, judgedByPool, warnings, contextLog) {
344
396
  const results = [];
345
397
  const listing = new Set(workspaceListing);
346
398
  for (const target of targets) {
347
399
  const isNewFile = target.kind === "NEW_FILE";
348
400
  const currentContent = isNewFile ? "" : (0, import_node_fs.readFileSync)(target.path, "utf8");
349
401
  const contextPaths = target.contextPaths ?? [];
402
+ const deniedContext = findDeniedContextPath(contextPaths, deniedPaths);
403
+ if (deniedContext) {
404
+ throw new Error(
405
+ `pilot.codegen contextPaths cited a denied path (${deniedContext.pattern}): ${deniedContext.path} (target ${target.path})`
406
+ );
407
+ }
350
408
  for (const contextPath of contextPaths) {
351
409
  if (!listing.has(contextPath)) {
352
410
  throw new Error(
@@ -354,7 +412,20 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
354
412
  );
355
413
  }
356
414
  }
357
- const contextContents = contextPaths.map((path) => ({ path, content: (0, import_node_fs.readFileSync)(path, "utf8") }));
415
+ const namedContents = contextPaths.map((path) => ({ path, content: (0, import_node_fs.readFileSync)(path, "utf8") }));
416
+ const added = collectRunContext(target, targets, new Set(contextPaths), deniedPaths);
417
+ const contextContents = [...namedContents, ...added.entries];
418
+ for (const s of added.skipped) {
419
+ warnings.push(`not shown to the test for ${target.path}: ${s.path} (${s.reason})`);
420
+ }
421
+ const contextRecord = {
422
+ targetPath: target.path,
423
+ contextPaths,
424
+ addedPaths: added.entries.map((e) => e.path),
425
+ skipped: added.skipped
426
+ };
427
+ console.log(JSON.stringify({ event: "codegen.context", ...contextRecord }));
428
+ contextLog?.push(contextRecord);
358
429
  const contextData = {
359
430
  targetPath: target.path,
360
431
  currentContent,
@@ -409,6 +480,18 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
409
480
  envelope = await res.json();
410
481
  checkHardFailure(envelope, "pilot.codegen", target.path);
411
482
  }
483
+ if (envelope?.errorCode === CONTEXT_TOO_LARGE_CODE && added.entries.length > 0) {
484
+ contextData.contextContents = namedContents;
485
+ warnings.push(`context for ${target.path} was too large with the run's other files; retried without them`);
486
+ contextRecord.addedPaths = [];
487
+ contextRecord.retriedWithoutAdded = true;
488
+ res = await callCodegen();
489
+ if (!res.ok) {
490
+ throw new Error(`Codegen call failed for ${target.path}: ${res.status} ${res.statusText}`);
491
+ }
492
+ envelope = await res.json();
493
+ checkHardFailure(envelope, "pilot.codegen", target.path);
494
+ }
412
495
  if (typeof envelope?.errorCode === "string" && NOT_RETRIED_CODES.has(envelope.errorCode)) {
413
496
  const detail = envelope.error ? `: ${envelope.error}` : "";
414
497
  throw new Error(`Codegen refused ${target.path}: ${envelope.errorCode} (not retried)${detail}`);
@@ -1121,21 +1204,6 @@ function describeScopeViolations(check) {
1121
1204
 
1122
1205
  // src/targeting.ts
1123
1206
  var import_node_path4 = require("node:path");
1124
-
1125
- // src/deniedPaths.ts
1126
- function globToRegExp(pattern) {
1127
- const escaped = pattern.split("").map((c) => /[.+^${}()|[\]\\]/.test(c) ? `\\${c}` : c).join("");
1128
- const withGlobs = escaped.replace(/\*\*\//g, " SLASH ").replace(/\*\*/g, " STAR ").replace(/\*/g, "[^/]*").replace(/ SLASH /g, "(?:.*/)?").replace(/ STAR /g, ".*");
1129
- return new RegExp(`^${withGlobs}$`);
1130
- }
1131
- function matchesDeniedPath(path, patterns) {
1132
- for (const pattern of patterns) {
1133
- if (globToRegExp(pattern).test(path)) return pattern;
1134
- }
1135
- return null;
1136
- }
1137
-
1138
- // src/targeting.ts
1139
1207
  var CONTRACT_VERSION2 = "test-targets-1";
1140
1208
  function listWorkspaceFiles(workspaceRoot) {
1141
1209
  return listFiles(workspaceRoot ?? ".");
@@ -1147,6 +1215,12 @@ function validateTarget(target, workspaceRoot, workspaceListing, deniedPaths) {
1147
1215
  `pilot.targeting proposed a target matching a denied path (${denied}): ${target.path}`
1148
1216
  );
1149
1217
  }
1218
+ const deniedContext = findDeniedContextPath(target.contextPaths ?? [], deniedPaths);
1219
+ if (deniedContext) {
1220
+ throw new Error(
1221
+ `pilot.targeting cited a denied path as context (${deniedContext.pattern}): ${deniedContext.path} (target ${target.path})`
1222
+ );
1223
+ }
1150
1224
  if (target.kind !== "EXISTING" && target.kind !== "NEW_FILE") {
1151
1225
  throw new Error(`pilot.targeting returned an unknown kind "${target.kind}" for ${target.path}`);
1152
1226
  }
@@ -1216,6 +1290,108 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, re
1216
1290
  };
1217
1291
  }
1218
1292
 
1293
+ // src/targetingRecord.ts
1294
+ var MAX_TARGETS = 25;
1295
+ var MAX_IDS_PER_TARGET = 50;
1296
+ var MAX_CONTEXT_PATHS = 20;
1297
+ var MAX_ADDED_PATHS = 20;
1298
+ var MAX_SKIPPED_CONTEXT = 20;
1299
+ var MAX_SKIPPED_CASES = 200;
1300
+ var MAX_CONTEXT_RECORDS = 25;
1301
+ var MAX_PATH_CHARS = 300;
1302
+ var MAX_ID_CHARS = 100;
1303
+ var MAX_REASON_CHARS = 300;
1304
+ function clampList(values, max, maxChars) {
1305
+ let cut = false;
1306
+ const kept = [];
1307
+ for (const raw of values ?? []) {
1308
+ const v = typeof raw === "string" ? raw.trim() : "";
1309
+ if (v.length === 0) continue;
1310
+ if (v.length > maxChars || kept.length >= max) {
1311
+ cut = true;
1312
+ continue;
1313
+ }
1314
+ kept.push(v);
1315
+ }
1316
+ return { kept, cut };
1317
+ }
1318
+ function buildTargetingRecord(targeting, contextLog) {
1319
+ let truncated = false;
1320
+ const note = (cut) => {
1321
+ if (cut) truncated = true;
1322
+ };
1323
+ if (targeting.outcome === "NO_TARGET") {
1324
+ const record2 = { outcome: "NO_TARGET", targets: [], skippedCaseIds: [], codegenContext: [] };
1325
+ const reason = (targeting.reason ?? "").trim();
1326
+ if (reason.length > 0) {
1327
+ record2.noTargetReason = reason.slice(0, MAX_REASON_CHARS);
1328
+ if (reason.length > MAX_REASON_CHARS) record2.noTargetReasonTruncated = true;
1329
+ }
1330
+ return record2;
1331
+ }
1332
+ const targets = [];
1333
+ for (const t of targeting.targets) {
1334
+ if (targets.length >= MAX_TARGETS) {
1335
+ truncated = true;
1336
+ break;
1337
+ }
1338
+ const path = typeof t.path === "string" ? t.path.trim() : "";
1339
+ if (path.length === 0 || path.length > MAX_PATH_CHARS) {
1340
+ truncated = true;
1341
+ continue;
1342
+ }
1343
+ const contextPaths = clampList(t.contextPaths, MAX_CONTEXT_PATHS, MAX_PATH_CHARS);
1344
+ const automationIds = clampList(t.automationIds, MAX_IDS_PER_TARGET, MAX_ID_CHARS);
1345
+ note(contextPaths.cut);
1346
+ note(automationIds.cut);
1347
+ targets.push({ path, kind: t.kind, contextPaths: contextPaths.kept, automationIds: automationIds.kept });
1348
+ }
1349
+ const skipped = clampList(
1350
+ (targeting.skippedCases ?? []).map((c) => c.automationId),
1351
+ MAX_SKIPPED_CASES,
1352
+ MAX_ID_CHARS
1353
+ );
1354
+ note(skipped.cut);
1355
+ const codegenContext = [];
1356
+ for (const entry of contextLog) {
1357
+ if (codegenContext.length >= MAX_CONTEXT_RECORDS) {
1358
+ truncated = true;
1359
+ break;
1360
+ }
1361
+ const targetPath = typeof entry.targetPath === "string" ? entry.targetPath.trim() : "";
1362
+ if (targetPath.length === 0 || targetPath.length > MAX_PATH_CHARS) {
1363
+ truncated = true;
1364
+ continue;
1365
+ }
1366
+ const contextPaths = clampList(entry.contextPaths, MAX_CONTEXT_PATHS, MAX_PATH_CHARS);
1367
+ const addedPaths = clampList(entry.addedPaths, MAX_ADDED_PATHS, MAX_PATH_CHARS);
1368
+ const skippedPaths = clampList(
1369
+ (entry.skipped ?? []).map((s) => s.path),
1370
+ MAX_SKIPPED_CONTEXT,
1371
+ MAX_PATH_CHARS
1372
+ );
1373
+ note(contextPaths.cut);
1374
+ note(addedPaths.cut);
1375
+ note(skippedPaths.cut);
1376
+ const reasonByPath = new Map((entry.skipped ?? []).map((s) => [s.path.trim(), s.reason]));
1377
+ codegenContext.push({
1378
+ targetPath,
1379
+ contextPaths: contextPaths.kept,
1380
+ addedPaths: addedPaths.kept,
1381
+ skipped: skippedPaths.kept.map((path) => ({ path, reason: String(reasonByPath.get(path) ?? "skipped").slice(0, 40) })),
1382
+ ...entry.retriedWithoutAdded ? { retriedWithoutAdded: true } : {}
1383
+ });
1384
+ }
1385
+ const record = {
1386
+ outcome: "TARGETED",
1387
+ targets,
1388
+ skippedCaseIds: skipped.kept,
1389
+ codegenContext
1390
+ };
1391
+ if (truncated) record.truncated = true;
1392
+ return record;
1393
+ }
1394
+
1219
1395
  // src/testPlacementCheck.ts
1220
1396
  function isDiscovered(targetPath, discoveredFiles) {
1221
1397
  const normalized = targetPath.replace(/\\/g, "/");
@@ -1282,6 +1458,8 @@ async function run(env2) {
1282
1458
  let warnings = [];
1283
1459
  let targeted = false;
1284
1460
  let generatedFilePaths = [];
1461
+ let targetingResult;
1462
+ const contextLog = [];
1285
1463
  try {
1286
1464
  runToken = await exchangeForRunToken(env2.tokenUrl, await fetchGithubOidcToken());
1287
1465
  packet = await fetchPacket(env2.packetUrl, runToken, env2.githubRunId);
@@ -1296,6 +1474,7 @@ async function run(env2) {
1296
1474
  packet.guardrails.deniedPaths,
1297
1475
  packet.testDiscovery
1298
1476
  );
1477
+ targetingResult = targeting;
1299
1478
  if (targeting.outcome === "NO_TARGET") {
1300
1479
  writePhase0Marker(env2.runId);
1301
1480
  runCommand(packet.commands.setup, "Setup");
@@ -1339,7 +1518,8 @@ async function run(env2) {
1339
1518
  packet.requiredTests,
1340
1519
  packet.guardrails.deniedPaths,
1341
1520
  judgedByPool,
1342
- warnings
1521
+ warnings,
1522
+ contextLog
1343
1523
  )
1344
1524
  ]);
1345
1525
  const depCheck = checkImportDependencies(
@@ -1433,7 +1613,9 @@ async function run(env2) {
1433
1613
  runToken,
1434
1614
  status: hardFailure ? "failed" : "completed",
1435
1615
  branch,
1436
- warnings
1616
+ warnings,
1617
+ // Omitted when targeting itself never produced a result (nothing exists to record).
1618
+ targeting: targetingResult ? buildTargetingRecord(targetingResult, contextLog) : void 0
1437
1619
  });
1438
1620
  return { hardFailure };
1439
1621
  }