@quikit/quikpilot-runner 8.2.4 → 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" },
@@ -214,6 +215,13 @@ function matchesDeniedPath(path, patterns) {
214
215
  }
215
216
  return null;
216
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
+ }
217
225
 
218
226
  // src/envelope.ts
219
227
  var HARD_FAILURE_CODES = /* @__PURE__ */ new Set(["SEMANTIC_MISMATCH", "CONTRACT_REJECTED"]);
@@ -384,13 +392,19 @@ function narrowJudgedByForTarget(pool, targets, codeTargetPath) {
384
392
  function toWireJudgedBy(pool) {
385
393
  return pool.map(({ automationId: _automationId, ...rest }) => rest);
386
394
  }
387
- async function runCodegen(gateway, targets, workspaceListing, requiredTests, deniedPaths, judgedByPool, warnings) {
395
+ async function runCodegen(gateway, targets, workspaceListing, requiredTests, deniedPaths, judgedByPool, warnings, contextLog) {
388
396
  const results = [];
389
397
  const listing = new Set(workspaceListing);
390
398
  for (const target of targets) {
391
399
  const isNewFile = target.kind === "NEW_FILE";
392
400
  const currentContent = isNewFile ? "" : (0, import_node_fs.readFileSync)(target.path, "utf8");
393
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
+ }
394
408
  for (const contextPath of contextPaths) {
395
409
  if (!listing.has(contextPath)) {
396
410
  throw new Error(
@@ -404,15 +418,14 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
404
418
  for (const s of added.skipped) {
405
419
  warnings.push(`not shown to the test for ${target.path}: ${s.path} (${s.reason})`);
406
420
  }
407
- console.log(
408
- JSON.stringify({
409
- event: "codegen.context",
410
- targetPath: target.path,
411
- contextPaths,
412
- addedPaths: added.entries.map((e) => e.path),
413
- skipped: added.skipped
414
- })
415
- );
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);
416
429
  const contextData = {
417
430
  targetPath: target.path,
418
431
  currentContent,
@@ -470,6 +483,8 @@ async function runCodegen(gateway, targets, workspaceListing, requiredTests, den
470
483
  if (envelope?.errorCode === CONTEXT_TOO_LARGE_CODE && added.entries.length > 0) {
471
484
  contextData.contextContents = namedContents;
472
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;
473
488
  res = await callCodegen();
474
489
  if (!res.ok) {
475
490
  throw new Error(`Codegen call failed for ${target.path}: ${res.status} ${res.statusText}`);
@@ -1200,6 +1215,12 @@ function validateTarget(target, workspaceRoot, workspaceListing, deniedPaths) {
1200
1215
  `pilot.targeting proposed a target matching a denied path (${denied}): ${target.path}`
1201
1216
  );
1202
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
+ }
1203
1224
  if (target.kind !== "EXISTING" && target.kind !== "NEW_FILE") {
1204
1225
  throw new Error(`pilot.targeting returned an unknown kind "${target.kind}" for ${target.path}`);
1205
1226
  }
@@ -1269,6 +1290,108 @@ async function runTargeting(gateway, design, workspaceRoot, workspaceListing, re
1269
1290
  };
1270
1291
  }
1271
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
+
1272
1395
  // src/testPlacementCheck.ts
1273
1396
  function isDiscovered(targetPath, discoveredFiles) {
1274
1397
  const normalized = targetPath.replace(/\\/g, "/");
@@ -1335,6 +1458,8 @@ async function run(env2) {
1335
1458
  let warnings = [];
1336
1459
  let targeted = false;
1337
1460
  let generatedFilePaths = [];
1461
+ let targetingResult;
1462
+ const contextLog = [];
1338
1463
  try {
1339
1464
  runToken = await exchangeForRunToken(env2.tokenUrl, await fetchGithubOidcToken());
1340
1465
  packet = await fetchPacket(env2.packetUrl, runToken, env2.githubRunId);
@@ -1349,6 +1474,7 @@ async function run(env2) {
1349
1474
  packet.guardrails.deniedPaths,
1350
1475
  packet.testDiscovery
1351
1476
  );
1477
+ targetingResult = targeting;
1352
1478
  if (targeting.outcome === "NO_TARGET") {
1353
1479
  writePhase0Marker(env2.runId);
1354
1480
  runCommand(packet.commands.setup, "Setup");
@@ -1392,7 +1518,8 @@ async function run(env2) {
1392
1518
  packet.requiredTests,
1393
1519
  packet.guardrails.deniedPaths,
1394
1520
  judgedByPool,
1395
- warnings
1521
+ warnings,
1522
+ contextLog
1396
1523
  )
1397
1524
  ]);
1398
1525
  const depCheck = checkImportDependencies(
@@ -1486,7 +1613,9 @@ async function run(env2) {
1486
1613
  runToken,
1487
1614
  status: hardFailure ? "failed" : "completed",
1488
1615
  branch,
1489
- warnings
1616
+ warnings,
1617
+ // Omitted when targeting itself never produced a result (nothing exists to record).
1618
+ targeting: targetingResult ? buildTargetingRecord(targetingResult, contextLog) : void 0
1490
1619
  });
1491
1620
  return { hardFailure };
1492
1621
  }