auditor-lambda 0.1.0 → 0.2.2

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.
Files changed (87) hide show
  1. package/README.md +2 -1
  2. package/audit-code-wrapper-lib.mjs +205 -187
  3. package/dist/adapters/eslint.js +4 -2
  4. package/dist/adapters/npmAudit.js +1 -1
  5. package/dist/cli.js +296 -12
  6. package/dist/coverage.d.ts +0 -1
  7. package/dist/coverage.js +3 -34
  8. package/dist/extractors/bucketing.js +14 -35
  9. package/dist/extractors/disposition.js +8 -9
  10. package/dist/extractors/flows.js +14 -23
  11. package/dist/extractors/pathPatterns.d.ts +19 -0
  12. package/dist/extractors/pathPatterns.js +87 -0
  13. package/dist/extractors/surfaces.js +2 -7
  14. package/dist/io/artifacts.d.ts +23 -1
  15. package/dist/io/artifacts.js +3 -1
  16. package/dist/io/runArtifacts.js +1 -1
  17. package/dist/orchestrator/advance.js +1 -1
  18. package/dist/orchestrator/flowPlanning.d.ts +1 -1
  19. package/dist/orchestrator/flowPlanning.js +21 -28
  20. package/dist/orchestrator/internalExecutors.js +4 -7
  21. package/dist/orchestrator/planning.js +12 -20
  22. package/dist/orchestrator/resultIngestion.js +3 -2
  23. package/dist/orchestrator/runtimeValidation.js +5 -0
  24. package/dist/orchestrator/syntaxResolutionExecutor.js +10 -2
  25. package/dist/orchestrator/taskBuilder.d.ts +7 -2
  26. package/dist/orchestrator/taskBuilder.js +47 -52
  27. package/dist/prompts/renderWorkerPrompt.js +33 -0
  28. package/dist/providers/claudeCodeProvider.js +5 -0
  29. package/dist/providers/constants.d.ts +1 -0
  30. package/dist/providers/constants.js +1 -0
  31. package/dist/providers/index.js +9 -2
  32. package/dist/providers/spawnLoggedCommand.js +4 -0
  33. package/dist/reporting/mergeFindings.js +0 -7
  34. package/dist/reporting/rootCause.d.ts +0 -1
  35. package/dist/reporting/rootCause.js +0 -6
  36. package/dist/reporting/synthesis.js +18 -0
  37. package/dist/supervisor/operatorHandoff.d.ts +2 -0
  38. package/dist/supervisor/operatorHandoff.js +21 -9
  39. package/dist/supervisor/runLedger.js +6 -3
  40. package/dist/supervisor/sessionConfig.js +1 -0
  41. package/dist/types/flowCoverage.d.ts +1 -1
  42. package/dist/types/runLedger.d.ts +1 -1
  43. package/dist/types/runtimeValidation.d.ts +2 -1
  44. package/dist/types/sessionConfig.d.ts +2 -0
  45. package/dist/types/surfaces.d.ts +2 -1
  46. package/dist/types/workerSession.d.ts +4 -0
  47. package/dist/types.d.ts +0 -2
  48. package/dist/validation/auditResults.d.ts +11 -0
  49. package/dist/validation/auditResults.js +118 -0
  50. package/docs/agent-integrations.md +61 -56
  51. package/docs/agent-roles.md +69 -69
  52. package/docs/architecture.md +90 -90
  53. package/docs/artifacts.md +69 -69
  54. package/docs/bootstrap-install.md +1 -1
  55. package/docs/model-selection.md +86 -86
  56. package/docs/next-steps.md +11 -9
  57. package/docs/packaging.md +3 -3
  58. package/docs/pipeline.md +152 -152
  59. package/docs/production-readiness.md +6 -5
  60. package/docs/repo-layout.md +18 -18
  61. package/docs/run-flow.md +5 -5
  62. package/docs/session-config.md +216 -210
  63. package/docs/supervisor.md +70 -70
  64. package/docs/windows-setup.md +139 -139
  65. package/package.json +56 -56
  66. package/schemas/audit-code-v1alpha1.schema.json +80 -76
  67. package/schemas/audit_result.schema.json +54 -48
  68. package/schemas/audit_state.schema.json +2 -2
  69. package/schemas/audit_task.schema.json +60 -49
  70. package/schemas/blind_spot_register.schema.json +13 -3
  71. package/schemas/coverage_matrix.schema.json +14 -17
  72. package/schemas/critical_flows.schema.json +6 -3
  73. package/schemas/external_analyzer_results.schema.json +10 -4
  74. package/schemas/file_disposition.schema.json +33 -33
  75. package/schemas/finding.schema.json +86 -62
  76. package/schemas/flow_coverage.schema.json +53 -44
  77. package/schemas/graph_bundle.schema.json +12 -6
  78. package/schemas/merged_findings.schema.json +7 -2
  79. package/schemas/risk_register.schema.json +5 -1
  80. package/schemas/root_cause_clusters.schema.json +2 -5
  81. package/schemas/runtime_validation_report.schema.json +34 -34
  82. package/schemas/runtime_validation_tasks.schema.json +4 -1
  83. package/schemas/surface_manifest.schema.json +4 -1
  84. package/schemas/synthesis_report.schema.json +61 -61
  85. package/schemas/unit_manifest.schema.json +10 -3
  86. package/skills/audit-code/SKILL.md +37 -37
  87. package/skills/audit-code/audit-code.prompt.md +54 -54
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
- import { mkdir } from "node:fs/promises";
1
+ import { access, mkdir } from "node:fs/promises";
2
2
  import { createReadStream } from "node:fs";
3
- import { resolve } from "node:path";
3
+ import { join, resolve } from "node:path";
4
4
  import { buildRepoManifest } from "./extractors/fileInventory.js";
5
5
  import { buildFileDisposition } from "./extractors/disposition.js";
6
6
  import { buildCriticalFlowManifest } from "./extractors/flows.js";
@@ -12,6 +12,7 @@ import { initializeCoverageFromPlan } from "./orchestrator/planning.js";
12
12
  import { loadArtifactBundle, writeCoreArtifacts, } from "./io/artifacts.js";
13
13
  import { readJsonFile, writeJsonFile } from "./io/json.js";
14
14
  import { validateArtifactBundle } from "./validation/artifacts.js";
15
+ import { validateAuditResults, formatAuditResultIssues, } from "./validation/auditResults.js";
15
16
  import { validateConfiguredProviderEnvironment, validateSessionConfig, } from "./validation/sessionConfig.js";
16
17
  import { buildSynthesisReport } from "./reporting/synthesis.js";
17
18
  import { deriveAuditState } from "./orchestrator/state.js";
@@ -23,8 +24,11 @@ import { buildAuditCodeHandoff, writeAuditCodeHandoffArtifacts, } from "./superv
23
24
  import { getSessionConfigPath, loadSessionConfig, readSessionConfigFile, } from "./supervisor/sessionConfig.js";
24
25
  import { buildRunId, ensureSupervisorDirs, getRunPaths, writeWorkerTaskFiles, } from "./io/runArtifacts.js";
25
26
  import { renderWorkerPrompt } from "./prompts/renderWorkerPrompt.js";
27
+ import { LOCAL_SUBPROCESS_PROVIDER_NAME } from "./providers/constants.js";
26
28
  const ADVANCE_AUDIT_CONTRACT_VERSION = "audit-code/v1alpha1";
27
29
  const WORKER_RESULT_CONTRACT_VERSION = "audit-code-worker-result/v1alpha1";
30
+ const DEFAULT_MAX_RUNS = 1000;
31
+ const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
28
32
  const sampleFiles = [
29
33
  { path: "src/api/auth.ts", size_bytes: 1240, hash: "abc123" },
30
34
  { path: "src/lib/session.ts", size_bytes: 980, hash: "def456" },
@@ -47,8 +51,39 @@ function getRootDir(argv) {
47
51
  return resolve(getFlag(argv, "--root", "."));
48
52
  }
49
53
  function getMaxRuns(argv) {
50
- const raw = Number(getFlag(argv, "--max-runs", "25"));
51
- return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 25;
54
+ const raw = Number(getFlag(argv, "--max-runs", String(DEFAULT_MAX_RUNS)));
55
+ return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : DEFAULT_MAX_RUNS;
56
+ }
57
+ function getAgentBatchSize(argv, sessionConfig) {
58
+ const fromArg = getFlag(argv, "--agent-batch-size");
59
+ if (fromArg !== undefined) {
60
+ const parsed = Number(fromArg);
61
+ if (Number.isFinite(parsed) && parsed > 0)
62
+ return Math.floor(parsed);
63
+ }
64
+ if (typeof sessionConfig.agent_task_batch_size === "number" && sessionConfig.agent_task_batch_size > 0) {
65
+ return Math.floor(sessionConfig.agent_task_batch_size);
66
+ }
67
+ return 1;
68
+ }
69
+ function getParallelWorkers(argv, sessionConfig) {
70
+ const fromArg = getFlag(argv, "--parallel");
71
+ if (fromArg !== undefined) {
72
+ const parsed = Number(fromArg);
73
+ if (Number.isFinite(parsed) && parsed > 0)
74
+ return Math.floor(parsed);
75
+ }
76
+ if (typeof sessionConfig.parallel_workers === "number" && sessionConfig.parallel_workers > 0) {
77
+ return Math.floor(sessionConfig.parallel_workers);
78
+ }
79
+ return 1;
80
+ }
81
+ function chunkArray(arr, size) {
82
+ const chunks = [];
83
+ for (let i = 0; i < arr.length; i += size) {
84
+ chunks.push(arr.slice(i, i + size));
85
+ }
86
+ return chunks;
52
87
  }
53
88
  function getUiMode(argv, fallback = "headless") {
54
89
  const raw = getFlag(argv, "--ui");
@@ -79,6 +114,7 @@ async function emitEnvelope(params) {
79
114
  bundle: params.bundle,
80
115
  providerName: params.providerName,
81
116
  progressSummary: params.progress_summary,
117
+ isConfigError: params.isConfigError,
82
118
  });
83
119
  await writeAuditCodeHandoffArtifacts(handoff);
84
120
  console.log(JSON.stringify(buildEnvelope({
@@ -93,7 +129,7 @@ async function emitEnvelope(params) {
93
129
  }), null, 2));
94
130
  }
95
131
  function buildManualReviewBlocker(providerName) {
96
- return providerName === "local-subprocess"
132
+ return providerName === LOCAL_SUBPROCESS_PROVIDER_NAME
97
133
  ? "Automatic local-subprocess work is exhausted. Remaining audit tasks require explicit audit results or an interactive provider such as claude-code, opencode, or subprocess-template."
98
134
  : "Automatic work is exhausted. Remaining audit tasks require explicit audit results or an interactive provider.";
99
135
  }
@@ -125,8 +161,9 @@ function buildBlockedAuditState(params) {
125
161
  }
126
162
  async function countLines(path) {
127
163
  return new Promise((resolve, reject) => {
128
- let lines = 1;
164
+ let lines = 0;
129
165
  let byteCount = 0;
166
+ let lastByte = -1;
130
167
  const stream = createReadStream(path);
131
168
  stream.on("data", (chunk) => {
132
169
  const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
@@ -134,9 +171,15 @@ async function countLines(path) {
134
171
  for (let i = 0; i < buffer.length; ++i) {
135
172
  if (buffer[i] === 10)
136
173
  lines++;
174
+ lastByte = buffer[i];
137
175
  }
138
176
  });
139
- stream.on("end", () => resolve(byteCount === 0 ? 0 : lines));
177
+ stream.on("end", () => {
178
+ if (byteCount === 0)
179
+ return resolve(0);
180
+ // Files not ending with \n have one final line not counted above
181
+ resolve(lastByte !== 10 ? lines + 1 : lines);
182
+ });
140
183
  stream.on("error", reject);
141
184
  });
142
185
  }
@@ -160,6 +203,34 @@ async function buildLineIndex(root, repoManifest) {
160
203
  }
161
204
  return Object.fromEntries(entries);
162
205
  }
206
+ const PROJECT_SIGNALS = [
207
+ "package.json",
208
+ "go.mod",
209
+ "Cargo.toml",
210
+ "pom.xml",
211
+ "build.gradle",
212
+ "pyproject.toml",
213
+ "setup.py",
214
+ "setup.cfg",
215
+ "Makefile",
216
+ "CMakeLists.txt",
217
+ ];
218
+ async function detectProjectRoot(root) {
219
+ for (const signal of PROJECT_SIGNALS) {
220
+ try {
221
+ await access(join(root, signal));
222
+ return signal;
223
+ }
224
+ catch {
225
+ // not found, try next
226
+ }
227
+ }
228
+ return null;
229
+ }
230
+ function buildPendingAuditTasks(bundle) {
231
+ const completedTaskIds = new Set((bundle.audit_results ?? []).map((result) => result.task_id));
232
+ return (bundle.audit_tasks ?? []).filter((task) => !completedTaskIds.has(task.task_id));
233
+ }
163
234
  async function runAuditStep(options) {
164
235
  const bundle = await loadArtifactBundle(options.artifactsDir);
165
236
  const auditResults = options.auditResultsPath
@@ -282,10 +353,41 @@ async function cmdRunToCompletion(argv) {
282
353
  const provider = createFreshSessionProvider(getFlag(argv, "--provider"), sessionConfig);
283
354
  const uiMode = getUiMode(argv, sessionConfig.ui_mode ?? "headless");
284
355
  const maxRuns = getMaxRuns(argv);
285
- const timeoutMs = sessionConfig.timeout_ms ?? 30 * 60 * 1000;
356
+ const agentBatchSize = getAgentBatchSize(argv, sessionConfig);
357
+ const parallelWorkers = getParallelWorkers(argv, sessionConfig);
358
+ const timeoutMs = sessionConfig.timeout_ms ?? DEFAULT_TIMEOUT_MS;
286
359
  const selfCliPath = resolve(process.argv[1] ?? "");
287
360
  await mkdir(artifactsDir, { recursive: true });
288
361
  await ensureSupervisorDirs(artifactsDir);
362
+ const earlyBundle = await loadArtifactBundle(artifactsDir);
363
+ if (!earlyBundle.unit_manifest) {
364
+ const foundSignal = await detectProjectRoot(root);
365
+ if (!foundSignal) {
366
+ const blocker = `No recognisable project signals found in ${root}. Expected one of: ${PROJECT_SIGNALS.join(", ")}. Check that --root points to the repository root, not a subdirectory or an unrelated path.`;
367
+ const earlyState = deriveAuditState(earlyBundle);
368
+ const blockedState = buildBlockedAuditState({
369
+ state: earlyState,
370
+ obligationId: null,
371
+ executor: null,
372
+ blocker,
373
+ });
374
+ await emitEnvelope({
375
+ root,
376
+ artifactsDir,
377
+ bundle: { ...earlyBundle, audit_state: blockedState },
378
+ audit_state: blockedState,
379
+ selected_obligation: null,
380
+ selected_executor: null,
381
+ progress_made: false,
382
+ artifacts_written: [],
383
+ progress_summary: blocker,
384
+ next_likely_step: null,
385
+ providerName: provider.name,
386
+ isConfigError: true,
387
+ });
388
+ return;
389
+ }
390
+ }
289
391
  let pendingAuditResultsPath = getFlag(argv, "--results");
290
392
  let pendingRuntimeUpdatesPath = getFlag(argv, "--updates");
291
393
  let pendingExternalAnalyzerPath = getFlag(argv, "--external-analyzer-results");
@@ -316,7 +418,7 @@ async function cmdRunToCompletion(argv) {
316
418
  obligationId = "runtime_validation_current";
317
419
  runtimeUpdatesPath = pendingRuntimeUpdatesPath;
318
420
  }
319
- if (preferredExecutor === "agent" && provider.name === "local-subprocess") {
421
+ if (preferredExecutor === "agent" && provider.name === LOCAL_SUBPROCESS_PROVIDER_NAME) {
320
422
  const blocker = buildManualReviewBlocker(provider.name);
321
423
  const blockedState = buildBlockedAuditState({
322
424
  state: bundle.audit_state ?? decision.state,
@@ -328,6 +430,32 @@ async function cmdRunToCompletion(argv) {
328
430
  ...bundle,
329
431
  audit_state: blockedState,
330
432
  });
433
+ const blockRunId = buildRunId(obligationId, runCount + 1);
434
+ const blockPaths = getRunPaths(artifactsDir, blockRunId);
435
+ const blockPendingTasks = buildPendingAuditTasks(bundle).slice(0, agentBatchSize);
436
+ const blockPendingTasksPath = join(blockPaths.runDir, "pending-audit-tasks.json");
437
+ const blockAuditResultsPath = join(blockPaths.runDir, "audit-results.json");
438
+ const blockTask = {
439
+ contract_version: "audit-code-worker/v1alpha1",
440
+ run_id: blockRunId,
441
+ repo_root: root,
442
+ artifacts_dir: artifactsDir,
443
+ obligation_id: obligationId,
444
+ preferred_executor: preferredExecutor,
445
+ result_path: blockPaths.resultPath,
446
+ worker_command: [
447
+ process.execPath,
448
+ selfCliPath,
449
+ "worker-run",
450
+ "--task",
451
+ blockPaths.taskPath,
452
+ ],
453
+ audit_results_path: blockAuditResultsPath,
454
+ pending_audit_tasks_path: blockPendingTasksPath,
455
+ };
456
+ const blockPrompt = renderWorkerPrompt(blockTask);
457
+ await writeWorkerTaskFiles(blockTask, blockPrompt, blockPaths, artifactsDir);
458
+ await writeJsonFile(blockPendingTasksPath, blockPendingTasks);
331
459
  await emitEnvelope({
332
460
  root,
333
461
  artifactsDir,
@@ -369,9 +497,132 @@ async function cmdRunToCompletion(argv) {
369
497
  });
370
498
  return;
371
499
  }
500
+ if (preferredExecutor === "agent" && parallelWorkers > 1) {
501
+ const allPendingTasks = buildPendingAuditTasks(bundle);
502
+ const taskGroups = chunkArray(allPendingTasks.slice(0, parallelWorkers * agentBatchSize), agentBatchSize);
503
+ const workerSlots = [];
504
+ for (const group of taskGroups) {
505
+ runCount += 1;
506
+ const slotRunId = buildRunId(obligationId, runCount);
507
+ const slotPaths = getRunPaths(artifactsDir, slotRunId);
508
+ const slotAuditResultsPath = join(slotPaths.runDir, "audit-results.json");
509
+ const slotPendingTasksPath = join(slotPaths.runDir, "pending-audit-tasks.json");
510
+ const slotTask = {
511
+ contract_version: "audit-code-worker/v1alpha1",
512
+ run_id: slotRunId,
513
+ repo_root: root,
514
+ artifacts_dir: artifactsDir,
515
+ obligation_id: obligationId,
516
+ preferred_executor: "agent",
517
+ result_path: slotPaths.resultPath,
518
+ worker_command: [process.execPath, selfCliPath, "worker-run", "--task", slotPaths.taskPath],
519
+ audit_results_path: slotAuditResultsPath,
520
+ pending_audit_tasks_path: slotPendingTasksPath,
521
+ skip_worker_command: true,
522
+ };
523
+ const slotPrompt = renderWorkerPrompt(slotTask);
524
+ await writeWorkerTaskFiles(slotTask, slotPrompt, slotPaths, artifactsDir);
525
+ await writeJsonFile(slotPendingTasksPath, group);
526
+ workerSlots.push({ runId: slotRunId, paths: slotPaths, auditResultsPath: slotAuditResultsPath, pendingTasksPath: slotPendingTasksPath, group });
527
+ }
528
+ const parallelStartedAt = new Date().toISOString();
529
+ await Promise.allSettled(workerSlots.map((slot) => provider.launch({
530
+ repoRoot: root,
531
+ runId: slot.runId,
532
+ obligationId,
533
+ promptPath: slot.paths.promptPath,
534
+ taskPath: slot.paths.taskPath,
535
+ resultPath: slot.paths.resultPath,
536
+ stdoutPath: slot.paths.stdoutPath,
537
+ stderrPath: slot.paths.stderrPath,
538
+ uiMode,
539
+ timeoutMs,
540
+ })));
541
+ // Result ingestion is intentionally sequential even though agent launch
542
+ // was parallel. Writing to coverage_matrix.json is not atomic, so
543
+ // concurrent ingest calls would race and corrupt coverage state.
544
+ let batchProgress = false;
545
+ for (const slot of workerSlots) {
546
+ const parallelEndedAt = new Date().toISOString();
547
+ let slotStatus = "no_progress";
548
+ try {
549
+ const auditResults = await readJsonFile(slot.auditResultsPath);
550
+ const pendingTaskIds = new Set(slot.group.map((t) => t.task_id));
551
+ const matchedCount = auditResults.filter((r) => pendingTaskIds.has(r.task_id)).length;
552
+ if (slot.group.length > 0 && matchedCount === 0) {
553
+ throw new Error("Worker did not emit any audit results for the assigned tasks.");
554
+ }
555
+ const issues = validateAuditResults(auditResults, slot.group);
556
+ const errors = issues.filter((issue) => issue.severity === "error");
557
+ const warnings = issues.filter((issue) => issue.severity === "warning");
558
+ if (warnings.length > 0) {
559
+ process.stderr.write(`audit-results validation: ${warnings.length} warning(s) for ${slot.runId}:\n` +
560
+ formatAuditResultIssues(warnings) + "\n");
561
+ }
562
+ if (errors.length > 0) {
563
+ throw new Error(`audit-results validation failed with ${errors.length} error(s):\n` +
564
+ formatAuditResultIssues(errors));
565
+ }
566
+ const stepResult = await runAuditStep({
567
+ root,
568
+ artifactsDir,
569
+ preferredExecutor: "result_ingestion_executor",
570
+ auditResultsPath: slot.auditResultsPath,
571
+ });
572
+ slotStatus = stepResult.progress_made ? "completed" : "no_progress";
573
+ batchProgress ||= stepResult.progress_made;
574
+ if (stepResult.progress_made)
575
+ anyProgress = true;
576
+ for (const a of stepResult.artifacts_written)
577
+ artifactsWritten.add(a);
578
+ }
579
+ catch {
580
+ slotStatus = "failed";
581
+ }
582
+ await appendRunLedgerEntry(artifactsDir, {
583
+ run_id: slot.runId,
584
+ provider: provider.name,
585
+ obligation_id: obligationId,
586
+ selected_executor: "agent",
587
+ status: slotStatus,
588
+ started_at: parallelStartedAt,
589
+ ended_at: parallelEndedAt,
590
+ result_path: slot.paths.resultPath,
591
+ });
592
+ artifactsWritten.add("run-ledger.json");
593
+ }
594
+ if (!batchProgress) {
595
+ const bundleAfter = await loadArtifactBundle(artifactsDir);
596
+ const state = bundleAfter.audit_state ?? deriveAuditState(bundleAfter);
597
+ await emitEnvelope({
598
+ root,
599
+ artifactsDir,
600
+ bundle: bundleAfter,
601
+ audit_state: state,
602
+ selected_obligation: obligationId,
603
+ selected_executor: "agent",
604
+ progress_made: anyProgress,
605
+ artifacts_written: Array.from(artifactsWritten),
606
+ progress_summary: "Parallel worker batch made no progress.",
607
+ next_likely_step: obligationId,
608
+ providerName: provider.name,
609
+ });
610
+ return;
611
+ }
612
+ continue;
613
+ }
372
614
  runCount += 1;
373
615
  const runId = buildRunId(obligationId, runCount);
374
616
  const paths = getRunPaths(artifactsDir, runId);
617
+ const pendingAuditTasks = preferredExecutor === "agent"
618
+ ? buildPendingAuditTasks(bundle).slice(0, agentBatchSize)
619
+ : undefined;
620
+ const pendingAuditTasksPath = preferredExecutor === "agent"
621
+ ? join(paths.runDir, "pending-audit-tasks.json")
622
+ : undefined;
623
+ const providerAuditResultsPath = preferredExecutor === "agent"
624
+ ? join(paths.runDir, "audit-results.json")
625
+ : auditResultsPath;
375
626
  const task = {
376
627
  contract_version: "audit-code-worker/v1alpha1",
377
628
  run_id: runId,
@@ -387,12 +638,16 @@ async function cmdRunToCompletion(argv) {
387
638
  "--task",
388
639
  paths.taskPath,
389
640
  ],
390
- audit_results_path: auditResultsPath,
641
+ audit_results_path: providerAuditResultsPath,
642
+ pending_audit_tasks_path: pendingAuditTasksPath,
391
643
  runtime_updates_path: runtimeUpdatesPath,
392
644
  external_analyzer_results_path: externalAnalyzerPath,
393
645
  };
394
646
  const prompt = renderWorkerPrompt(task);
395
647
  await writeWorkerTaskFiles(task, prompt, paths, artifactsDir);
648
+ if (pendingAuditTasksPath && pendingAuditTasks) {
649
+ await writeJsonFile(pendingAuditTasksPath, pendingAuditTasks);
650
+ }
396
651
  const startedAt = new Date().toISOString();
397
652
  let workerResult;
398
653
  try {
@@ -459,7 +714,7 @@ async function cmdRunToCompletion(argv) {
459
714
  artifactsWritten.add("run-ledger.json");
460
715
  if (externalAnalyzerPath)
461
716
  pendingExternalAnalyzerPath = undefined;
462
- if (auditResultsPath)
717
+ if (providerAuditResultsPath)
463
718
  pendingAuditResultsPath = undefined;
464
719
  if (runtimeUpdatesPath)
465
720
  pendingRuntimeUpdatesPath = undefined;
@@ -509,10 +764,39 @@ async function cmdWorkerRun(argv) {
509
764
  const task = await readJsonFile(taskPath);
510
765
  let workerResult;
511
766
  try {
767
+ if (task.preferred_executor === "agent" && !task.audit_results_path) {
768
+ throw new Error("agent worker-run requires audit_results_path so provider-assisted review can be ingested.");
769
+ }
770
+ if (task.preferred_executor === "agent" && task.audit_results_path) {
771
+ const pendingTasks = task.pending_audit_tasks_path
772
+ ? await readJsonFile(task.pending_audit_tasks_path)
773
+ : [];
774
+ const auditResults = await readJsonFile(task.audit_results_path);
775
+ const pendingTaskIds = new Set(pendingTasks.map((item) => item.task_id));
776
+ const matchedResultCount = auditResults.filter((result) => pendingTaskIds.has(result.task_id)).length;
777
+ if (pendingTasks.length > 0 && matchedResultCount === 0) {
778
+ throw new Error("Provider-assisted review did not emit any audit results for the pending audit tasks.");
779
+ }
780
+ const issues = validateAuditResults(auditResults, pendingTasks);
781
+ const errors = issues.filter((issue) => issue.severity === "error");
782
+ const warnings = issues.filter((issue) => issue.severity === "warning");
783
+ if (warnings.length > 0) {
784
+ process.stderr.write(`audit-results validation: ${warnings.length} warning(s):\n` +
785
+ formatAuditResultIssues(warnings) +
786
+ "\n");
787
+ }
788
+ if (errors.length > 0) {
789
+ throw new Error(`audit-results validation failed with ${errors.length} error(s):\n` +
790
+ formatAuditResultIssues(errors));
791
+ }
792
+ }
793
+ const preferredExecutor = task.preferred_executor === "agent"
794
+ ? "result_ingestion_executor"
795
+ : task.preferred_executor;
512
796
  const result = await runAuditStep({
513
797
  root: task.repo_root,
514
798
  artifactsDir: task.artifacts_dir,
515
- preferredExecutor: task.preferred_executor,
799
+ preferredExecutor,
516
800
  auditResultsPath: task.audit_results_path,
517
801
  runtimeUpdatesPath: task.runtime_updates_path,
518
802
  externalAnalyzerPath: task.external_analyzer_results_path,
@@ -1,5 +1,4 @@
1
1
  import type { CoverageFileRecord, CoverageMatrix, Lens, ReviewedLineRange } from "./types.js";
2
- export declare function mergeRanges(ranges: ReviewedLineRange[]): ReviewedLineRange[];
3
2
  export declare function createCoverageMatrix(paths: string[]): CoverageMatrix;
4
3
  export declare function markExcludedPath(matrix: CoverageMatrix, path: string, classificationStatus: string): void;
5
4
  export declare function applyUnitCoverage(matrix: CoverageMatrix, path: string, unitId: string, requiredLenses: Lens[]): void;
package/dist/coverage.js CHANGED
@@ -1,29 +1,3 @@
1
- function sortRanges(ranges) {
2
- return [...ranges].sort((a, b) => {
3
- if (a.path !== b.path)
4
- return a.path.localeCompare(b.path);
5
- if (a.start !== b.start)
6
- return a.start - b.start;
7
- return a.end - b.end;
8
- });
9
- }
10
- export function mergeRanges(ranges) {
11
- const sorted = sortRanges(ranges);
12
- const merged = [];
13
- for (const range of sorted) {
14
- const last = merged[merged.length - 1];
15
- if (!last || last.path !== range.path || range.start > last.end + 1) {
16
- merged.push({ ...range });
17
- continue;
18
- }
19
- last.end = Math.max(last.end, range.end);
20
- last.pass_id = `${last.pass_id},${range.pass_id}`;
21
- last.agent_role = [last.agent_role, range.agent_role]
22
- .filter(Boolean)
23
- .join(",");
24
- }
25
- return merged;
26
- }
27
1
  export function createCoverageMatrix(paths) {
28
2
  return {
29
3
  files: paths.map((path) => ({
@@ -33,7 +7,6 @@ export function createCoverageMatrix(paths) {
33
7
  audit_status: "pending",
34
8
  required_lenses: [],
35
9
  completed_lenses: [],
36
- reviewed_line_ranges: [],
37
10
  })),
38
11
  };
39
12
  }
@@ -45,7 +18,6 @@ export function markExcludedPath(matrix, path, classificationStatus) {
45
18
  record.audit_status = "excluded";
46
19
  record.required_lenses = [];
47
20
  record.completed_lenses = [];
48
- record.reviewed_line_ranges = [];
49
21
  record.unit_ids = [];
50
22
  }
51
23
  export function applyUnitCoverage(matrix, path, unitId, requiredLenses) {
@@ -65,21 +37,18 @@ export function applyReviewedRanges(matrix, reviewedRanges) {
65
37
  const record = matrix.files.find((file) => file.path === range.path);
66
38
  if (!record || record.audit_status === "excluded")
67
39
  continue;
68
- record.reviewed_line_ranges.push(range);
69
- record.reviewed_line_ranges = mergeRanges(record.reviewed_line_ranges);
70
40
  if (range.lens && !record.completed_lenses.includes(range.lens)) {
71
41
  record.completed_lenses.push(range.lens);
72
42
  }
73
43
  }
74
44
  for (const file of matrix.files) {
75
- const hasAllRequired = file.required_lenses.every((lens) => file.completed_lenses.includes(lens));
76
- if (file.audit_status === "excluded") {
45
+ if (file.audit_status === "excluded")
77
46
  continue;
78
- }
47
+ const hasAllRequired = file.required_lenses.every((lens) => file.completed_lenses.includes(lens));
79
48
  if (hasAllRequired && file.required_lenses.length > 0) {
80
49
  file.audit_status = "complete";
81
50
  }
82
- else if (file.reviewed_line_ranges.length > 0) {
51
+ else if (file.completed_lenses.length > 0) {
83
52
  file.audit_status = "partial";
84
53
  }
85
54
  }
@@ -1,3 +1,4 @@
1
+ import { isNodeModulesOrGit, isTestPath, isInterfacePath, isDataLayerPath, isSecuritySensitivePath, isConcurrencyPath, isScriptPath, isDeploymentConfigPath, isDocPath, isGeneratedPath, } from "./pathPatterns.js";
1
2
  function addBucket(buckets, rationale, bucket, reason) {
2
3
  if (!buckets.has(bucket)) {
3
4
  buckets.add(bucket);
@@ -8,57 +9,35 @@ export function bucketFile(path) {
8
9
  const normalized = path.toLowerCase();
9
10
  const buckets = new Set();
10
11
  const rationale = [];
11
- if (normalized.includes("test") ||
12
- normalized.includes("spec") ||
13
- normalized.includes("__tests__")) {
12
+ if (isNodeModulesOrGit(normalized)) {
13
+ addBucket(buckets, rationale, "generated_vendor", "node_modules or .git excluded by convention");
14
+ return { path, buckets: [...buckets], rationale };
15
+ }
16
+ if (isTestPath(normalized)) {
14
17
  addBucket(buckets, rationale, "tests", "path suggests tests");
15
18
  }
16
- if (normalized.includes("route") ||
17
- normalized.includes("controller") ||
18
- normalized.includes("handler") ||
19
- normalized.includes("api/")) {
19
+ if (isInterfacePath(normalized)) {
20
20
  addBucket(buckets, rationale, "interface", "path suggests interface code");
21
21
  }
22
- if (normalized.includes("model") ||
23
- normalized.includes("schema") ||
24
- normalized.includes("migration") ||
25
- normalized.includes("seed") ||
26
- normalized.includes("db/")) {
22
+ if (isDataLayerPath(normalized)) {
27
23
  addBucket(buckets, rationale, "data_layer", "path suggests data-layer code");
28
24
  }
29
- if (normalized.includes("auth") ||
30
- normalized.includes("secret") ||
31
- normalized.includes("token") ||
32
- normalized.includes("permission") ||
33
- normalized.includes("session")) {
25
+ if (isSecuritySensitivePath(normalized)) {
34
26
  addBucket(buckets, rationale, "security_sensitive", "path suggests security-sensitive code");
35
27
  }
36
- if (normalized.includes("queue") ||
37
- normalized.includes("worker") ||
38
- normalized.includes("job") ||
39
- normalized.includes("cache") ||
40
- normalized.includes("retry") ||
41
- normalized.includes("lock")) {
28
+ if (isConcurrencyPath(normalized)) {
42
29
  addBucket(buckets, rationale, "concurrency_state", "path suggests concurrency or stateful behavior");
43
30
  }
44
- if (normalized.includes("script") ||
45
- normalized.startsWith("scripts/") ||
46
- normalized.startsWith("bin/")) {
31
+ if (isScriptPath(normalized)) {
47
32
  addBucket(buckets, rationale, "tooling_scripts", "path suggests tooling or scripts");
48
33
  }
49
- if (normalized.includes("docker") ||
50
- normalized.includes("terraform") ||
51
- normalized.includes("deploy") ||
52
- normalized.includes("workflow") ||
53
- normalized.includes("k8s") ||
54
- normalized.endsWith(".yml") ||
55
- normalized.endsWith(".yaml")) {
34
+ if (isDeploymentConfigPath(normalized)) {
56
35
  addBucket(buckets, rationale, "config_deployment", "path suggests config or deployment artifact");
57
36
  }
58
- if (normalized.endsWith(".md") || normalized.startsWith("docs/")) {
37
+ if (isDocPath(normalized)) {
59
38
  addBucket(buckets, rationale, "docs_specs", "path suggests documentation");
60
39
  }
61
- if (normalized.includes("vendor") || normalized.includes("generated")) {
40
+ if (isGeneratedPath(normalized)) {
62
41
  addBucket(buckets, rationale, "generated_vendor", "path suggests generated or vendored content");
63
42
  }
64
43
  if (buckets.size === 0) {
@@ -1,24 +1,23 @@
1
+ import { isNodeModulesOrGit, isBuildOutput, isVendorPath, isBinaryArtifact, isDocPath, } from "./pathPatterns.js";
1
2
  function inferDisposition(path) {
2
3
  const normalized = path.toLowerCase();
3
- if (normalized.startsWith("dist/") || normalized.startsWith("build/")) {
4
+ if (isNodeModulesOrGit(normalized)) {
5
+ return { path, status: "excluded", reason: "node_modules or .git excluded by convention." };
6
+ }
7
+ if (isBuildOutput(normalized)) {
4
8
  return { path, status: "generated", reason: "Build output path." };
5
9
  }
6
- if (normalized.includes("vendor") || normalized.includes("third_party")) {
10
+ if (isVendorPath(normalized)) {
7
11
  return { path, status: "vendor", reason: "Vendor or third-party path." };
8
12
  }
9
- if (normalized.endsWith(".png") ||
10
- normalized.endsWith(".jpg") ||
11
- normalized.endsWith(".jpeg") ||
12
- normalized.endsWith(".gif") ||
13
- normalized.endsWith(".pdf") ||
14
- normalized.endsWith(".zip")) {
13
+ if (isBinaryArtifact(normalized)) {
15
14
  return {
16
15
  path,
17
16
  status: "binary",
18
17
  reason: "Non-source binary-like artifact.",
19
18
  };
20
19
  }
21
- if (normalized.endsWith(".md") || normalized.startsWith("docs/")) {
20
+ if (isDocPath(normalized)) {
22
21
  return { path, status: "doc_only", reason: "Documentation artifact." };
23
22
  }
24
23
  return {