auditor-lambda 0.3.38 → 0.3.40

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/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
1
+ import { mkdir, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
2
2
  import { createReadStream, existsSync } from "node:fs";
3
3
  import { Buffer } from "node:buffer";
4
4
  import { createHash } from "node:crypto";
@@ -22,6 +22,7 @@ import { buildAuditReportModel, renderAuditReportMarkdown, } from "./reporting/s
22
22
  import { deriveAuditState } from "./orchestrator/state.js";
23
23
  import { advanceAudit } from "./orchestrator/advance.js";
24
24
  import { decideNextStep } from "./orchestrator/nextStep.js";
25
+ import { renderDesignReviewPrompt } from "./orchestrator/designReviewPrompt.js";
25
26
  import { createFreshSessionProvider, resolveFreshSessionProviderName, } from "./providers/index.js";
26
27
  import { appendRunLedgerEntry, loadRunLedger } from "./supervisor/runLedger.js";
27
28
  import { buildAuditCodeHandoff, writeAuditCodeHandoffArtifacts, } from "./supervisor/operatorHandoff.js";
@@ -1038,6 +1039,32 @@ async function runDeterministicForNextStep(params) {
1038
1039
  : join(params.artifactsDir, "audit-report.md"),
1039
1040
  };
1040
1041
  }
1042
+ if (decision.selected_executor === "design_review") {
1043
+ const findingsPath = join(params.artifactsDir, "incoming", "design-review-findings.json");
1044
+ let reviewFindings;
1045
+ try {
1046
+ reviewFindings = await readJsonFile(findingsPath);
1047
+ }
1048
+ catch (error) {
1049
+ if (!isFileMissingError(error))
1050
+ throw error;
1051
+ }
1052
+ if (reviewFindings && Array.isArray(reviewFindings)) {
1053
+ const existing = bundle.design_assessment;
1054
+ if (existing) {
1055
+ existing.review_findings = reviewFindings;
1056
+ existing.reviewed = true;
1057
+ await writeJsonFile(join(params.artifactsDir, "design_assessment.json"), existing);
1058
+ await unlink(findingsPath).catch(() => { });
1059
+ continue;
1060
+ }
1061
+ }
1062
+ return {
1063
+ kind: "design_review",
1064
+ state,
1065
+ bundle,
1066
+ };
1067
+ }
1041
1068
  if (decision.selected_executor === "agent") {
1042
1069
  return {
1043
1070
  kind: "semantic_review",
@@ -1177,6 +1204,38 @@ async function cmdNextStep(argv) {
1177
1204
  console.log(JSON.stringify(step, null, 2));
1178
1205
  return;
1179
1206
  }
1207
+ if (result.kind === "design_review") {
1208
+ const designReviewResultsPath = join(artifactsDir, "incoming", "design-review-findings.json");
1209
+ await mkdir(join(artifactsDir, "incoming"), { recursive: true });
1210
+ const continueCommand = nextStepCommand(root, artifactsDir);
1211
+ const prompt = renderDesignReviewPrompt(result.bundle);
1212
+ const fullPrompt = [
1213
+ prompt,
1214
+ "## Results path",
1215
+ "",
1216
+ `Write the JSON array of findings to:`,
1217
+ "",
1218
+ ` ${designReviewResultsPath}`,
1219
+ "",
1220
+ `Then run: ${continueCommand}`,
1221
+ "",
1222
+ ].join("\n");
1223
+ const step = await writeCurrentStep({
1224
+ artifactsDir,
1225
+ stepKind: "design_review",
1226
+ status: "ready",
1227
+ runId: null,
1228
+ allowedCommands: [continueCommand],
1229
+ stopCondition: "Write design review findings to the results path, then run next-step.",
1230
+ repoRoot: root,
1231
+ artifactPaths: {
1232
+ design_review_results: designReviewResultsPath,
1233
+ },
1234
+ prompt: fullPrompt,
1235
+ });
1236
+ console.log(JSON.stringify(step, null, 2));
1237
+ return;
1238
+ }
1180
1239
  if (!hostCanDispatch) {
1181
1240
  const singleTaskPromptPath = join(artifactsDir, "dispatch", "current-single-task-prompt.md");
1182
1241
  const workerCommand = renderCommand(result.activeReviewRun.worker_command);
@@ -2083,6 +2142,7 @@ async function cmdWorkerRun(argv) {
2083
2142
  }
2084
2143
  }
2085
2144
  const DISPATCH_RESULT_MAP_FILENAME = "dispatch-result-map.json";
2145
+ const ACTIVE_DISPATCH_FILENAME = "active-dispatch.json";
2086
2146
  function dispatchResultMapPath(runDir) {
2087
2147
  return join(runDir, DISPATCH_RESULT_MAP_FILENAME);
2088
2148
  }
@@ -2378,6 +2438,10 @@ async function prepareDispatchArtifacts(params) {
2378
2438
  .map(([key, value]) => `input.${key}: ${value}`)
2379
2439
  : [];
2380
2440
  const isLensVerification = task.tags?.includes("lens_verification") ?? false;
2441
+ const coverageTemplate = task.file_paths.map((path) => ({
2442
+ path,
2443
+ total_lines: task.file_line_counts?.[path] ?? lineIndex[path] ?? 0,
2444
+ }));
2381
2445
  return [
2382
2446
  `### ${task.task_id}`,
2383
2447
  `unit_id: ${task.unit_id}`,
@@ -2398,6 +2462,11 @@ async function prepareDispatchArtifacts(params) {
2398
2462
  ]
2399
2463
  : []),
2400
2464
  "",
2465
+ "file_coverage (copy exactly into your AuditResult for this task):",
2466
+ "```json",
2467
+ JSON.stringify(coverageTemplate),
2468
+ "```",
2469
+ "",
2401
2470
  ];
2402
2471
  });
2403
2472
  const submitCommand = `"${process.execPath}" "${join(packageRoot, "audit-code.mjs")}" submit-packet ` +
@@ -2442,7 +2511,7 @@ async function prepareDispatchArtifacts(params) {
2442
2511
  " unit_id copy from the task metadata",
2443
2512
  " pass_id copy from the task metadata",
2444
2513
  " lens copy from the task metadata",
2445
- " file_coverage [{path, total_lines}] - one entry per assigned file; use the line counts listed above",
2514
+ " file_coverage [{path, total_lines}] - copy the exact template from each task section above",
2446
2515
  " findings [] or array of finding objects",
2447
2516
  "",
2448
2517
  "Lens verification tasks:",
@@ -2555,6 +2624,14 @@ async function prepareDispatchArtifacts(params) {
2555
2624
  if (warningsPath) {
2556
2625
  await writeJsonFile(warningsPath, warnings);
2557
2626
  }
2627
+ const activeDispatch = {
2628
+ run_id: runId,
2629
+ created_at: new Date().toISOString(),
2630
+ packet_count: plan.length,
2631
+ task_count: orderedTasks.length,
2632
+ status: "active",
2633
+ };
2634
+ await writeJsonFile(join(artifactsDir, ACTIVE_DISPATCH_FILENAME), activeDispatch);
2558
2635
  return {
2559
2636
  run_id: runId,
2560
2637
  dispatch_plan_path: dispatchPlanPath,
@@ -2602,12 +2679,23 @@ async function cmdSubmitPacket(argv) {
2602
2679
  if (!resultMap) {
2603
2680
  throw new Error(`No ${DISPATCH_RESULT_MAP_FILENAME} found for run ${runId}; run prepare-dispatch first.`);
2604
2681
  }
2605
- const packetEntries = resultMap.entries.filter((entry) => entry.packet_id === packetId);
2682
+ let packetEntries = resultMap.entries.filter((entry) => entry.packet_id === packetId);
2683
+ let resolvedPacketId = packetId;
2606
2684
  if (packetEntries.length === 0) {
2607
- throw new Error(`Unknown packet_id '${packetId}' for run ${runId}.`);
2685
+ const trimmed = packetId.trim();
2686
+ packetEntries = resultMap.entries.filter((entry) => entry.packet_id.trim().toLowerCase() === trimmed.toLowerCase());
2687
+ if (packetEntries.length > 0) {
2688
+ resolvedPacketId = packetEntries[0].packet_id;
2689
+ process.stderr.write(`[submit-packet] Resolved packet_id '${packetId}' → '${resolvedPacketId}' (case/whitespace normalization)\n`);
2690
+ }
2691
+ }
2692
+ if (packetEntries.length === 0) {
2693
+ const knownIds = [...new Set(resultMap.entries.map((e) => e.packet_id))];
2694
+ throw new Error(`Unknown packet_id '${packetId}' for run ${runId}.\n` +
2695
+ `Valid packet IDs: ${knownIds.join(", ")}`);
2608
2696
  }
2609
2697
  if (entriesByTaskId(packetEntries).size !== packetEntries.length) {
2610
- throw new Error(`Dispatch result map has duplicate task entries for packet '${packetId}'.`);
2698
+ throw new Error(`Dispatch result map has duplicate task entries for packet '${resolvedPacketId}'.`);
2611
2699
  }
2612
2700
  const allTasks = await readJsonFile(tasksPath);
2613
2701
  const taskById = new Map(allTasks.map((task) => [task.task_id, task]));
@@ -2658,7 +2746,7 @@ async function cmdSubmitPacket(argv) {
2658
2746
  }
2659
2747
  seen.add(taskId);
2660
2748
  if (!expectedTaskIds.has(taskId)) {
2661
- resultErrors.push(`Result at index ${index} uses task_id '${taskId}', which is not assigned to packet '${packetId}'.`);
2749
+ resultErrors.push(`Result at index ${index} uses task_id '${taskId}', which is not assigned to packet '${resolvedPacketId}'.`);
2662
2750
  }
2663
2751
  }
2664
2752
  for (const task of tasks) {
@@ -2668,7 +2756,44 @@ async function cmdSubmitPacket(argv) {
2668
2756
  }
2669
2757
  }
2670
2758
  if (resultErrors.length > 0) {
2671
- throw new Error(`submit-packet rejected ${packetId}:\n${resultErrors.join("\n")}`);
2759
+ throw new Error(`submit-packet rejected ${resolvedPacketId}:\n${resultErrors.join("\n")}`);
2760
+ }
2761
+ // Check for duplicate findings against already-submitted results in this run
2762
+ const existingFindingKeys = new Set();
2763
+ const otherEntries = resultMap.entries.filter((e) => e.packet_id !== resolvedPacketId);
2764
+ for (const other of otherEntries) {
2765
+ try {
2766
+ const existing = JSON.parse(await readFile(other.result_path, "utf8"));
2767
+ if (existing?.findings) {
2768
+ for (const f of existing.findings) {
2769
+ const key = [
2770
+ (f.lens ?? "").trim().toLowerCase(),
2771
+ (f.category ?? "").trim().toLowerCase(),
2772
+ (f.title ?? "").trim().toLowerCase(),
2773
+ f.affected_files?.[0]?.path ?? "",
2774
+ ].join("|");
2775
+ existingFindingKeys.add(key);
2776
+ }
2777
+ }
2778
+ }
2779
+ catch { /* file doesn't exist yet or invalid — skip */ }
2780
+ }
2781
+ let dupCount = 0;
2782
+ for (const result of payload) {
2783
+ for (const f of result.findings ?? []) {
2784
+ const key = [
2785
+ (f.lens ?? "").trim().toLowerCase(),
2786
+ (f.category ?? "").trim().toLowerCase(),
2787
+ (f.title ?? "").trim().toLowerCase(),
2788
+ f.affected_files?.[0]?.path ?? "",
2789
+ ].join("|");
2790
+ if (existingFindingKeys.has(key)) {
2791
+ dupCount++;
2792
+ }
2793
+ }
2794
+ }
2795
+ if (dupCount > 0) {
2796
+ process.stderr.write(`[submit-packet] Warning: ${dupCount} finding(s) appear to duplicate findings from other packets in this run.\n`);
2672
2797
  }
2673
2798
  const entryByTaskId = entriesByTaskId(packetEntries);
2674
2799
  for (const result of payload) {
@@ -2681,9 +2806,10 @@ async function cmdSubmitPacket(argv) {
2681
2806
  const findingCount = payload.reduce((sum, result) => sum + result.findings.length, 0);
2682
2807
  console.log(JSON.stringify({
2683
2808
  run_id: runId,
2684
- packet_id: packetId,
2809
+ packet_id: resolvedPacketId,
2685
2810
  accepted_count: payload.length,
2686
2811
  finding_count: findingCount,
2812
+ ...(dupCount > 0 ? { duplicate_warning_count: dupCount } : {}),
2687
2813
  }, null, 2));
2688
2814
  }
2689
2815
  async function cmdMergeAndIngest(argv) {
@@ -2722,11 +2848,24 @@ async function cmdMergeAndIngest(argv) {
2722
2848
  const failing = [];
2723
2849
  const seenTaskIds = new Set();
2724
2850
  let spuriousFileCount = 0;
2851
+ const fallbackByTaskId = new Map();
2725
2852
  for (const filename of files) {
2726
2853
  const filePath = resolve(join(taskResultsDir, filename));
2727
2854
  if (!expectedPaths.has(filePath)) {
2728
2855
  spuriousFileCount++;
2729
- process.stderr.write(`[merge-and-ingest] Warning: ignoring unexpected file in task-results/: ${filename}\n`);
2856
+ try {
2857
+ const raw = await readFile(filePath, "utf8");
2858
+ const parsed = JSON.parse(raw);
2859
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2860
+ const tid = typeof parsed.task_id === "string"
2861
+ ? String(parsed.task_id) : undefined;
2862
+ if (tid && !fallbackByTaskId.has(tid)) {
2863
+ fallbackByTaskId.set(tid, parsed);
2864
+ }
2865
+ }
2866
+ }
2867
+ catch { /* not parseable — skip */ }
2868
+ process.stderr.write(`[merge-and-ingest] Warning: unexpected file in task-results/: ${filename}\n`);
2730
2869
  }
2731
2870
  }
2732
2871
  for (const task of allTasks) {
@@ -2745,15 +2884,23 @@ async function cmdMergeAndIngest(argv) {
2745
2884
  }
2746
2885
  catch (e) {
2747
2886
  if (isFileMissingError(e)) {
2748
- failing.push({
2749
- task_id: task.task_id,
2750
- errors: ["Missing audit result for assigned task."],
2751
- });
2887
+ const fallback = fallbackByTaskId.get(task.task_id);
2888
+ if (fallback) {
2889
+ process.stderr.write(`[merge-and-ingest] Recovered result for '${task.task_id}' from unexpected file (matched by task_id)\n`);
2890
+ obj = fallback;
2891
+ }
2892
+ else {
2893
+ failing.push({
2894
+ task_id: task.task_id,
2895
+ errors: ["Missing audit result for assigned task."],
2896
+ });
2897
+ continue;
2898
+ }
2752
2899
  }
2753
2900
  else {
2754
2901
  failing.push({ task_id: task.task_id, errors: [`Invalid JSON: ${e.message}`] });
2902
+ continue;
2755
2903
  }
2756
- continue;
2757
2904
  }
2758
2905
  const record = obj && typeof obj === "object" && !Array.isArray(obj)
2759
2906
  ? obj
@@ -2784,45 +2931,87 @@ async function cmdMergeAndIngest(argv) {
2784
2931
  }
2785
2932
  }
2786
2933
  await writeJsonFile(auditResultsPath, passing);
2934
+ const failedTasksPath = join(runDir, "failed-tasks.json");
2787
2935
  if (failing.length > 0) {
2788
- const failedTasksPath = join(runDir, "failed-tasks.json");
2789
2936
  await writeJsonFile(failedTasksPath, failing);
2790
- throw new Error(`${failing.length} assigned task result(s) were missing or invalid; blocked before ingestion. See ${failedTasksPath}`);
2937
+ }
2938
+ if (passing.length === 0 && failing.length > 0) {
2939
+ throw new Error(`All ${failing.length} assigned task result(s) were missing or invalid; blocked before ingestion. See ${failedTasksPath}`);
2791
2940
  }
2792
2941
  const findingCount = passing.reduce((sum, result) => sum + result.findings.length, 0);
2793
- const result = await runAuditStep({
2794
- root: workerTask.repo_root,
2795
- artifactsDir,
2796
- preferredExecutor: "result_ingestion_executor",
2797
- auditResultsPath,
2798
- });
2799
- const updatedPendingTasks = await addFileLineCountHints(workerTask.repo_root, buildPendingAuditTasks(result.updated_bundle));
2800
- await writeJsonFile(tasksPath, updatedPendingTasks);
2942
+ let result = null;
2943
+ if (passing.length > 0) {
2944
+ result = await runAuditStep({
2945
+ root: workerTask.repo_root,
2946
+ artifactsDir,
2947
+ preferredExecutor: "result_ingestion_executor",
2948
+ auditResultsPath,
2949
+ });
2950
+ const updatedPendingTasks = await addFileLineCountHints(workerTask.repo_root, buildPendingAuditTasks(result.updated_bundle));
2951
+ await writeJsonFile(tasksPath, updatedPendingTasks);
2952
+ }
2953
+ const activeDispatchPath = join(artifactsDir, ACTIVE_DISPATCH_FILENAME);
2954
+ try {
2955
+ const dispatch = await readJsonFile(activeDispatchPath);
2956
+ if (dispatch.run_id === runId) {
2957
+ dispatch.status = failing.length > 0 ? "active" : "merged";
2958
+ await writeJsonFile(activeDispatchPath, dispatch);
2959
+ }
2960
+ }
2961
+ catch { /* no active dispatch file — skip */ }
2962
+ let retryDispatchPath = null;
2963
+ if (failing.length > 0) {
2964
+ const failedTaskIds = new Set(failing.map((f) => f.task_id));
2965
+ const failedPacketIds = [
2966
+ ...new Set(resultMap.entries
2967
+ .filter((e) => failedTaskIds.has(e.task_id))
2968
+ .map((e) => e.packet_id)),
2969
+ ];
2970
+ const retryDispatch = {
2971
+ run_id: runId,
2972
+ retry_packet_ids: failedPacketIds,
2973
+ failed_task_count: failing.length,
2974
+ accepted_task_count: passing.length,
2975
+ };
2976
+ retryDispatchPath = join(runDir, "retry-dispatch.json");
2977
+ await writeJsonFile(retryDispatchPath, retryDispatch);
2978
+ process.stderr.write(`[merge-and-ingest] ${passing.length} accepted, ${failing.length} failed. ` +
2979
+ `Retry packets: ${failedPacketIds.join(", ")}\n`);
2980
+ }
2981
+ const status = failing.length > 0
2982
+ ? "partial"
2983
+ : (result?.progress_made ? "completed" : "no_progress");
2801
2984
  const workerResult = buildWorkerResult({
2802
2985
  runId,
2803
2986
  obligationId: workerTask.obligation_id,
2804
- status: result.progress_made ? "completed" : "no_progress",
2805
- progressMade: result.progress_made,
2806
- selectedExecutor: result.selected_executor,
2807
- artifactsWritten: result.artifacts_written,
2808
- summary: result.progress_summary,
2809
- nextLikelyStep: result.next_likely_step,
2987
+ status: failing.length > 0 ? "no_progress" : (result?.progress_made ? "completed" : "no_progress"),
2988
+ progressMade: result?.progress_made ?? false,
2989
+ selectedExecutor: result?.selected_executor ?? null,
2990
+ artifactsWritten: result?.artifacts_written ?? [],
2991
+ summary: result?.progress_summary ?? `${failing.length} task(s) failed`,
2992
+ nextLikelyStep: result?.next_likely_step ?? null,
2810
2993
  errors: [],
2811
2994
  });
2812
2995
  await writeJsonFile(workerTask.result_path, workerResult);
2813
2996
  console.log(JSON.stringify({
2814
2997
  run_id: runId,
2815
- status: workerResult.status,
2998
+ status,
2816
2999
  accepted_count: passing.length,
2817
- rejected_count: 0,
3000
+ rejected_count: failing.length,
2818
3001
  spurious_file_count: spuriousFileCount,
2819
3002
  finding_count: findingCount,
2820
3003
  audit_results_path: auditResultsPath,
2821
- selected_executor: workerResult.selected_executor,
2822
- progress_made: workerResult.progress_made,
2823
- progress_summary: workerResult.summary,
2824
- next_likely_step: workerResult.next_likely_step,
3004
+ ...(retryDispatchPath ? { retry_dispatch_path: retryDispatchPath } : {}),
3005
+ ...(result ? {
3006
+ selected_executor: workerResult.selected_executor,
3007
+ progress_made: workerResult.progress_made,
3008
+ progress_summary: workerResult.summary,
3009
+ next_likely_step: workerResult.next_likely_step,
3010
+ } : {}),
2825
3011
  }, null, 2));
3012
+ if (failing.length > 0) {
3013
+ process.exitCode = 2;
3014
+ }
2826
3015
  }
2827
3016
  async function cmdValidateResult(argv) {
2828
3017
  const rawRunId = getFlag(argv, "--run-id");
@@ -3298,6 +3487,69 @@ async function cmdQuota(argv) {
3298
3487
  quota_state_path: getQuotaStatePath(),
3299
3488
  }, null, 2));
3300
3489
  }
3490
+ async function cmdDispatchStatus(argv) {
3491
+ const artifactsDir = getArtifactsDir(argv);
3492
+ const activeDispatchPath = join(artifactsDir, ACTIVE_DISPATCH_FILENAME);
3493
+ let activeDispatch = null;
3494
+ try {
3495
+ activeDispatch = await readJsonFile(activeDispatchPath);
3496
+ }
3497
+ catch (e) {
3498
+ if (!isFileMissingError(e))
3499
+ throw e;
3500
+ }
3501
+ if (!activeDispatch) {
3502
+ console.log(JSON.stringify({ status: "no_active_dispatch" }, null, 2));
3503
+ return;
3504
+ }
3505
+ const runDir = join(artifactsDir, "runs", activeDispatch.run_id);
3506
+ const resultMap = await loadDispatchResultMap(runDir);
3507
+ if (!resultMap) {
3508
+ console.log(JSON.stringify({
3509
+ status: "missing_result_map",
3510
+ run_id: activeDispatch.run_id,
3511
+ }, null, 2));
3512
+ return;
3513
+ }
3514
+ const packetIds = [...new Set(resultMap.entries.map((e) => e.packet_id))];
3515
+ const packetStatus = [];
3516
+ for (const pid of packetIds) {
3517
+ if (pid === "__prior_dispatch__")
3518
+ continue;
3519
+ const entries = resultMap.entries.filter((e) => e.packet_id === pid);
3520
+ let completed = 0;
3521
+ const missing = [];
3522
+ for (const entry of entries) {
3523
+ try {
3524
+ await readFile(entry.result_path, "utf8");
3525
+ completed++;
3526
+ }
3527
+ catch {
3528
+ missing.push(entry.task_id);
3529
+ }
3530
+ }
3531
+ packetStatus.push({
3532
+ packet_id: pid,
3533
+ task_count: entries.length,
3534
+ completed_count: completed,
3535
+ missing_task_ids: missing,
3536
+ });
3537
+ }
3538
+ const totalTasks = packetStatus.reduce((s, p) => s + p.task_count, 0);
3539
+ const completedTasks = packetStatus.reduce((s, p) => s + p.completed_count, 0);
3540
+ const completedPackets = packetStatus.filter((p) => p.missing_task_ids.length === 0).length;
3541
+ console.log(JSON.stringify({
3542
+ run_id: activeDispatch.run_id,
3543
+ dispatch_status: activeDispatch.status,
3544
+ created_at: activeDispatch.created_at,
3545
+ total_packets: packetStatus.length,
3546
+ completed_packets: completedPackets,
3547
+ total_tasks: totalTasks,
3548
+ completed_tasks: completedTasks,
3549
+ missing_tasks: totalTasks - completedTasks,
3550
+ packets: packetStatus,
3551
+ }, null, 2));
3552
+ }
3301
3553
  async function main(argv) {
3302
3554
  const command = argv[2] ?? "sample-run";
3303
3555
  switch (command) {
@@ -3370,9 +3622,12 @@ async function main(argv) {
3370
3622
  case "status":
3371
3623
  await cmdStatus(argv);
3372
3624
  return;
3625
+ case "dispatch-status":
3626
+ await cmdDispatchStatus(argv);
3627
+ return;
3373
3628
  default:
3374
3629
  console.error(`Unknown command: ${command}`);
3375
- console.error("Available commands: sample-run, advance-audit, next-step, run-to-completion, worker-run, import-external-analyzer, intake, plan, ingest-results, explain-task, update-runtime-validation, validate, validate-results, requeue, synthesize, cleanup, mcp, prepare-dispatch, merge-and-ingest, submit-packet, validate-result, quota, status");
3630
+ console.error("Available commands: sample-run, advance-audit, next-step, run-to-completion, worker-run, import-external-analyzer, intake, plan, ingest-results, explain-task, update-runtime-validation, validate, validate-results, requeue, synthesize, cleanup, mcp, prepare-dispatch, merge-and-ingest, submit-packet, validate-result, quota, status, dispatch-status");
3376
3631
  process.exitCode = 1;
3377
3632
  }
3378
3633
  }
@@ -0,0 +1,11 @@
1
+ import type { UnitManifest } from "../types.js";
2
+ import type { DesignAssessment } from "../types/designAssessment.js";
3
+ import type { GraphBundle } from "../types/graph.js";
4
+ import type { CriticalFlowManifest } from "../types/flows.js";
5
+ import type { RiskRegister } from "../types/risk.js";
6
+ export declare function buildDesignAssessment(params: {
7
+ unitManifest: UnitManifest;
8
+ graphBundle: GraphBundle;
9
+ criticalFlows: CriticalFlowManifest;
10
+ riskRegister: RiskRegister;
11
+ }): DesignAssessment;
@@ -0,0 +1,254 @@
1
+ let nextFindingId = 1;
2
+ function findingId() {
3
+ return `DA-${String(nextFindingId++).padStart(3, "0")}`;
4
+ }
5
+ function allEdges(graphBundle) {
6
+ const edges = [];
7
+ for (const [key, value] of Object.entries(graphBundle.graphs)) {
8
+ if (key === "routes" || !Array.isArray(value))
9
+ continue;
10
+ for (const edge of value) {
11
+ if (edge && typeof edge.from === "string" && typeof edge.to === "string") {
12
+ edges.push(edge);
13
+ }
14
+ }
15
+ }
16
+ return edges;
17
+ }
18
+ function detectCycles(edges) {
19
+ const adjacency = new Map();
20
+ for (const edge of edges) {
21
+ if (!adjacency.has(edge.from))
22
+ adjacency.set(edge.from, new Set());
23
+ adjacency.get(edge.from).add(edge.to);
24
+ }
25
+ const cycles = [];
26
+ const visited = new Set();
27
+ const stack = new Set();
28
+ function dfs(node, path) {
29
+ if (stack.has(node)) {
30
+ const cycleStart = path.indexOf(node);
31
+ if (cycleStart >= 0) {
32
+ cycles.push(path.slice(cycleStart));
33
+ }
34
+ return;
35
+ }
36
+ if (visited.has(node))
37
+ return;
38
+ visited.add(node);
39
+ stack.add(node);
40
+ path.push(node);
41
+ for (const neighbor of adjacency.get(node) ?? []) {
42
+ dfs(neighbor, path);
43
+ }
44
+ path.pop();
45
+ stack.delete(node);
46
+ }
47
+ for (const node of adjacency.keys()) {
48
+ dfs(node, []);
49
+ }
50
+ return cycles;
51
+ }
52
+ function deduplicateCycles(cycles) {
53
+ const seen = new Set();
54
+ const unique = [];
55
+ for (const cycle of cycles) {
56
+ const normalized = [...cycle].sort().join("\0");
57
+ if (!seen.has(normalized)) {
58
+ seen.add(normalized);
59
+ unique.push(cycle);
60
+ }
61
+ }
62
+ return unique;
63
+ }
64
+ function detectCycleFindings(graphBundle) {
65
+ const edges = allEdges(graphBundle);
66
+ const cycles = deduplicateCycles(detectCycles(edges));
67
+ if (cycles.length === 0)
68
+ return [];
69
+ return cycles.slice(0, 10).map((cycle) => ({
70
+ id: findingId(),
71
+ title: `Dependency cycle: ${cycle.length} modules`,
72
+ category: "dependency_cycle",
73
+ severity: cycle.length > 4 ? "high" : "medium",
74
+ confidence: "high",
75
+ lens: "architecture",
76
+ summary: `Circular dependency among ${cycle.join(" → ")} → ${cycle[0]}. Cycles increase coupling, complicate testing, and can cause initialization-order bugs.`,
77
+ affected_files: cycle.map((path) => ({ path })),
78
+ systemic: true,
79
+ }));
80
+ }
81
+ function detectHubModules(graphBundle) {
82
+ const edges = allEdges(graphBundle);
83
+ const fanIn = new Map();
84
+ const fanOut = new Map();
85
+ for (const edge of edges) {
86
+ fanOut.set(edge.from, (fanOut.get(edge.from) ?? 0) + 1);
87
+ fanIn.set(edge.to, (fanIn.get(edge.to) ?? 0) + 1);
88
+ }
89
+ const allNodes = new Set([...fanIn.keys(), ...fanOut.keys()]);
90
+ const hubThreshold = Math.max(8, Math.ceil(allNodes.size * 0.15));
91
+ const findings = [];
92
+ for (const node of allNodes) {
93
+ const inCount = fanIn.get(node) ?? 0;
94
+ const outCount = fanOut.get(node) ?? 0;
95
+ if (inCount >= hubThreshold && outCount >= hubThreshold) {
96
+ findings.push({
97
+ id: findingId(),
98
+ title: `Hub module: ${node}`,
99
+ category: "hub_module",
100
+ severity: "medium",
101
+ confidence: "high",
102
+ lens: "architecture",
103
+ summary: `${node} has ${inCount} incoming and ${outCount} outgoing dependencies. Hub modules become change bottlenecks and make the dependency graph fragile.`,
104
+ affected_files: [{ path: node }],
105
+ systemic: true,
106
+ });
107
+ }
108
+ }
109
+ return findings;
110
+ }
111
+ function detectOrphanUnits(unitManifest, graphBundle) {
112
+ const edges = allEdges(graphBundle);
113
+ const connected = new Set();
114
+ for (const edge of edges) {
115
+ connected.add(edge.from);
116
+ connected.add(edge.to);
117
+ }
118
+ if (connected.size === 0)
119
+ return [];
120
+ const orphans = [];
121
+ for (const unit of unitManifest.units) {
122
+ const hasConnection = unit.files.some((file) => connected.has(file));
123
+ if (!hasConnection && unit.files.length > 0) {
124
+ orphans.push(unit.unit_id);
125
+ }
126
+ }
127
+ if (orphans.length === 0)
128
+ return [];
129
+ if (orphans.length > unitManifest.units.length * 0.5)
130
+ return [];
131
+ return [{
132
+ id: findingId(),
133
+ title: `${orphans.length} orphan unit(s) with no graph connections`,
134
+ category: "orphan_units",
135
+ severity: "low",
136
+ confidence: "medium",
137
+ lens: "architecture",
138
+ summary: `Units [${orphans.join(", ")}] have no import, call, or reference edges in the dependency graph. They may be dead code, or the graph extraction missed their connections.`,
139
+ affected_files: orphans.map((id) => {
140
+ const unit = unitManifest.units.find((u) => u.unit_id === id);
141
+ return { path: unit?.files[0] ?? id };
142
+ }),
143
+ systemic: true,
144
+ }];
145
+ }
146
+ function detectRiskConcentration(riskRegister, unitManifest) {
147
+ if (riskRegister.items.length < 4)
148
+ return [];
149
+ const sorted = [...riskRegister.items].sort((a, b) => b.risk_score - a.risk_score);
150
+ const topQuartileSize = Math.max(1, Math.ceil(sorted.length * 0.25));
151
+ const topQuartile = sorted.slice(0, topQuartileSize);
152
+ const totalRisk = sorted.reduce((sum, item) => sum + item.risk_score, 0);
153
+ const topRisk = topQuartile.reduce((sum, item) => sum + item.risk_score, 0);
154
+ if (totalRisk === 0)
155
+ return [];
156
+ const concentration = topRisk / totalRisk;
157
+ if (concentration < 0.6)
158
+ return [];
159
+ return [{
160
+ id: findingId(),
161
+ title: "Risk concentrated in top quartile of units",
162
+ category: "risk_concentration",
163
+ severity: concentration > 0.8 ? "high" : "medium",
164
+ confidence: "high",
165
+ lens: "architecture",
166
+ summary: `${Math.round(concentration * 100)}% of total risk score is concentrated in the top ${topQuartileSize} of ${sorted.length} units: ${topQuartile.map((i) => i.unit_id).join(", ")}. Consider decomposing high-risk units or adding isolation boundaries.`,
167
+ affected_files: topQuartile.flatMap((item) => {
168
+ const unit = unitManifest.units.find((u) => u.unit_id === item.unit_id);
169
+ return (unit?.files ?? [item.unit_id]).map((path) => ({ path }));
170
+ }),
171
+ systemic: true,
172
+ }];
173
+ }
174
+ function detectUnitSprawl(unitManifest) {
175
+ if (unitManifest.units.length < 3)
176
+ return [];
177
+ const fileCounts = unitManifest.units.map((u) => u.files.length);
178
+ const totalFiles = fileCounts.reduce((a, b) => a + b, 0);
179
+ const maxFiles = Math.max(...fileCounts);
180
+ const findings = [];
181
+ const dominantUnit = unitManifest.units.find((u) => u.files.length === maxFiles);
182
+ if (dominantUnit && maxFiles > totalFiles * 0.5 && totalFiles > 10) {
183
+ findings.push({
184
+ id: findingId(),
185
+ title: `Dominant unit: ${dominantUnit.unit_id}`,
186
+ category: "monolith_unit",
187
+ severity: "medium",
188
+ confidence: "medium",
189
+ lens: "architecture",
190
+ summary: `Unit ${dominantUnit.unit_id} contains ${maxFiles} of ${totalFiles} files (${Math.round((maxFiles / totalFiles) * 100)}%). A single unit this large suggests insufficient decomposition.`,
191
+ affected_files: dominantUnit.files.slice(0, 10).map((path) => ({ path })),
192
+ systemic: true,
193
+ });
194
+ }
195
+ if (unitManifest.units.length > 50) {
196
+ const smallUnits = unitManifest.units.filter((u) => u.files.length === 1);
197
+ if (smallUnits.length > unitManifest.units.length * 0.6) {
198
+ findings.push({
199
+ id: findingId(),
200
+ title: "Excessive single-file units",
201
+ category: "unit_fragmentation",
202
+ severity: "low",
203
+ confidence: "medium",
204
+ lens: "architecture",
205
+ summary: `${smallUnits.length} of ${unitManifest.units.length} units contain only a single file. This fragmentation may indicate that the unit grouping is too granular to reflect meaningful architectural boundaries.`,
206
+ affected_files: smallUnits.slice(0, 5).map((u) => ({ path: u.files[0] })),
207
+ systemic: true,
208
+ });
209
+ }
210
+ }
211
+ return findings;
212
+ }
213
+ function detectFlowGaps(criticalFlows, graphBundle) {
214
+ const edges = allEdges(graphBundle);
215
+ const connected = new Set();
216
+ for (const edge of edges) {
217
+ connected.add(edge.from);
218
+ connected.add(edge.to);
219
+ }
220
+ const findings = [];
221
+ for (const flow of criticalFlows.flows) {
222
+ const disconnected = flow.paths.filter((path) => !connected.has(path));
223
+ if (disconnected.length > 0 &&
224
+ disconnected.length > flow.paths.length * 0.5) {
225
+ findings.push({
226
+ id: findingId(),
227
+ title: `Critical flow "${flow.name}" has weak graph coverage`,
228
+ category: "flow_gap",
229
+ severity: "medium",
230
+ confidence: "low",
231
+ lens: "architecture",
232
+ summary: `${disconnected.length} of ${flow.paths.length} files in flow "${flow.name}" have no dependency graph edges. The flow's structural integrity cannot be verified through static analysis alone.`,
233
+ affected_files: disconnected.map((path) => ({ path })),
234
+ systemic: true,
235
+ });
236
+ }
237
+ }
238
+ return findings;
239
+ }
240
+ export function buildDesignAssessment(params) {
241
+ nextFindingId = 1;
242
+ const findings = [
243
+ ...detectCycleFindings(params.graphBundle),
244
+ ...detectHubModules(params.graphBundle),
245
+ ...detectOrphanUnits(params.unitManifest, params.graphBundle),
246
+ ...detectRiskConcentration(params.riskRegister, params.unitManifest),
247
+ ...detectUnitSprawl(params.unitManifest),
248
+ ...detectFlowGaps(params.criticalFlows, params.graphBundle),
249
+ ];
250
+ return {
251
+ generated_at: new Date().toISOString(),
252
+ findings,
253
+ };
254
+ }
@@ -11,6 +11,7 @@ import type { RiskRegister } from "../types/risk.js";
11
11
  import type { AuditPlanMetrics, ReviewPacket } from "../types/reviewPlanning.js";
12
12
  import type { RuntimeValidationReport, RuntimeValidationTaskManifest } from "../types/runtimeValidation.js";
13
13
  import type { SurfaceManifest } from "../types/surfaces.js";
14
+ import type { DesignAssessment } from "../types/designAssessment.js";
14
15
  import type { ToolingManifest } from "../types/toolingManifest.js";
15
16
  type ArtifactPayloadMap = {
16
17
  repo_manifest: RepoManifest;
@@ -22,6 +23,7 @@ type ArtifactPayloadMap = {
22
23
  critical_flows: CriticalFlowManifest;
23
24
  flow_coverage: FlowCoverageManifest;
24
25
  risk_register: RiskRegister;
26
+ design_assessment: DesignAssessment;
25
27
  coverage_matrix: CoverageMatrix;
26
28
  runtime_validation_tasks: RuntimeValidationTaskManifest;
27
29
  runtime_validation_report: RuntimeValidationReport;
@@ -60,6 +62,7 @@ export declare const ARTIFACT_DEFINITIONS: {
60
62
  readonly critical_flows: ArtifactDefinition<"critical_flows">;
61
63
  readonly flow_coverage: ArtifactDefinition<"flow_coverage">;
62
64
  readonly risk_register: ArtifactDefinition<"risk_register">;
65
+ readonly design_assessment: ArtifactDefinition<"design_assessment">;
63
66
  readonly coverage_matrix: ArtifactDefinition<"coverage_matrix">;
64
67
  readonly runtime_validation_tasks: ArtifactDefinition<"runtime_validation_tasks">;
65
68
  readonly runtime_validation_report: ArtifactDefinition<"runtime_validation_report">;
@@ -36,6 +36,7 @@ export const ARTIFACT_DEFINITIONS = {
36
36
  critical_flows: jsonArtifact("critical_flows.json", "analysis"),
37
37
  flow_coverage: jsonArtifact("flow_coverage.json", "analysis"),
38
38
  risk_register: jsonArtifact("risk_register.json", "analysis"),
39
+ design_assessment: jsonArtifact("design_assessment.json", "analysis"),
39
40
  coverage_matrix: jsonArtifact("coverage_matrix.json", "execution"),
40
41
  runtime_validation_tasks: jsonArtifact("runtime_validation_tasks.json", "execution"),
41
42
  runtime_validation_report: jsonArtifact("runtime_validation_report.json", "execution"),
@@ -1,7 +1,7 @@
1
1
  import { decideNextStep } from "./nextStep.js";
2
2
  import { deriveAuditState } from "./state.js";
3
3
  import { computeArtifactMetadata } from "./artifactMetadata.js";
4
- import { runIntakeExecutor, runStructureExecutor, runPlanningExecutor, runResultIngestionExecutor, runRuntimeValidationExecutor, runRuntimeValidationUpdateExecutor, runSynthesisExecutor, runExternalAnalyzerImportExecutor, } from "./internalExecutors.js";
4
+ import { runIntakeExecutor, runStructureExecutor, runPlanningExecutor, runResultIngestionExecutor, runRuntimeValidationExecutor, runRuntimeValidationUpdateExecutor, runSynthesisExecutor, runDesignAssessmentExecutor, runDesignReviewAutoComplete, runExternalAnalyzerImportExecutor, } from "./internalExecutors.js";
5
5
  import { runAutoFixExecutor } from "./autoFixExecutor.js";
6
6
  import { runSyntaxResolutionExecutor } from "./syntaxResolutionExecutor.js";
7
7
  function cloneState(state) {
@@ -53,6 +53,12 @@ export async function advanceAudit(bundle, options = {}) {
53
53
  case "structure_executor":
54
54
  run = await runStructureExecutor(bundle, options.root);
55
55
  break;
56
+ case "design_assessment_executor":
57
+ run = runDesignAssessmentExecutor(bundle);
58
+ break;
59
+ case "design_review":
60
+ run = runDesignReviewAutoComplete(bundle);
61
+ break;
56
62
  case "planning_executor":
57
63
  if (!options.root)
58
64
  throw new Error("advanceAudit planning_executor requires root");
@@ -37,6 +37,7 @@ export const ARTIFACT_DEPENDENCY_MAP = {
37
37
  ],
38
38
  "unit_manifest.json": [
39
39
  "risk_register.json",
40
+ "design_assessment.json",
40
41
  "coverage_matrix.json",
41
42
  "audit_tasks.json",
42
43
  "audit_plan_metrics.json",
@@ -54,6 +55,7 @@ export const ARTIFACT_DEPENDENCY_MAP = {
54
55
  "critical_flows.json": [
55
56
  "flow_coverage.json",
56
57
  "risk_register.json",
58
+ "design_assessment.json",
57
59
  "audit_tasks.json",
58
60
  "audit_plan_metrics.json",
59
61
  "review_packets.json",
@@ -62,6 +64,9 @@ export const ARTIFACT_DEPENDENCY_MAP = {
62
64
  "runtime_validation_report.json",
63
65
  "audit-report.md",
64
66
  ],
67
+ "design_assessment.json": [
68
+ "audit-report.md",
69
+ ],
65
70
  "external_analyzer_results.json": [
66
71
  "coverage_matrix.json",
67
72
  "flow_coverage.json",
@@ -0,0 +1,2 @@
1
+ import type { ArtifactBundle } from "../io/artifacts.js";
2
+ export declare function renderDesignReviewPrompt(bundle: ArtifactBundle): string;
@@ -0,0 +1,151 @@
1
+ function summarizeUnits(bundle) {
2
+ const units = bundle.unit_manifest?.units ?? [];
3
+ if (units.length === 0)
4
+ return "No units identified.";
5
+ const lines = units.map((unit) => {
6
+ const lenses = unit.required_lenses.join(", ") || "none";
7
+ return `- ${unit.unit_id} (${unit.files.length} files, lenses: ${lenses})`;
8
+ });
9
+ return [
10
+ `${units.length} units:`,
11
+ ...lines.slice(0, 40),
12
+ ...(units.length > 40 ? [` ... and ${units.length - 40} more`] : []),
13
+ ].join("\n");
14
+ }
15
+ function summarizeGraph(bundle) {
16
+ const graphs = bundle.graph_bundle?.graphs;
17
+ if (!graphs)
18
+ return "No dependency graph available.";
19
+ const counts = [];
20
+ for (const [kind, edges] of Object.entries(graphs)) {
21
+ if (Array.isArray(edges) && edges.length > 0) {
22
+ counts.push(`${kind}: ${edges.length} edges`);
23
+ }
24
+ }
25
+ if (counts.length === 0)
26
+ return "Dependency graph is empty.";
27
+ return `Dependency graph: ${counts.join(", ")}.`;
28
+ }
29
+ function summarizeFlows(bundle) {
30
+ const flows = bundle.critical_flows?.flows ?? [];
31
+ if (flows.length === 0)
32
+ return "No critical flows identified.";
33
+ const lines = flows.map((flow) => `- ${flow.name}: ${flow.paths.length} files, concerns: ${flow.concerns.join(", ") || "none"}`);
34
+ return [`${flows.length} critical flows:`, ...lines].join("\n");
35
+ }
36
+ function summarizeRisk(bundle) {
37
+ const items = bundle.risk_register?.items ?? [];
38
+ if (items.length === 0)
39
+ return "No risk items.";
40
+ const sorted = [...items].sort((a, b) => b.risk_score - a.risk_score);
41
+ const top = sorted.slice(0, 10);
42
+ const lines = top.map((item) => `- ${item.unit_id}: score ${item.risk_score}, signals: ${item.signals.join(", ") || "none"}`);
43
+ return [
44
+ `${items.length} risk items (top ${top.length} by score):`,
45
+ ...lines,
46
+ ].join("\n");
47
+ }
48
+ function summarizeSurfaces(bundle) {
49
+ const surfaces = bundle.surface_manifest?.surfaces ?? [];
50
+ if (surfaces.length === 0)
51
+ return "No externally reachable surfaces identified.";
52
+ const lines = surfaces.map((surface) => `- ${surface.id} (${surface.kind}): ${surface.entrypoint}${surface.methods?.length ? ` [${surface.methods.join(", ")}]` : ""}`);
53
+ return [`${surfaces.length} surfaces:`, ...lines].join("\n");
54
+ }
55
+ function summarizeFiles(bundle) {
56
+ const files = bundle.repo_manifest?.files ?? [];
57
+ if (files.length === 0)
58
+ return "No files in manifest.";
59
+ const byLanguage = new Map();
60
+ for (const file of files) {
61
+ const lang = file.language || "unknown";
62
+ byLanguage.set(lang, (byLanguage.get(lang) ?? 0) + 1);
63
+ }
64
+ const langSummary = [...byLanguage.entries()]
65
+ .sort((a, b) => b[1] - a[1])
66
+ .map(([lang, count]) => `${lang}: ${count}`)
67
+ .join(", ");
68
+ return `${files.length} files (${langSummary}).`;
69
+ }
70
+ function formatDeterministicFindings(findings) {
71
+ if (findings.length === 0)
72
+ return "No structural issues detected by deterministic analysis.";
73
+ const lines = findings.map((finding) => `- [${finding.severity}] ${finding.title}: ${finding.summary}`);
74
+ return [
75
+ `${findings.length} structural findings from deterministic analysis:`,
76
+ ...lines,
77
+ ].join("\n");
78
+ }
79
+ export function renderDesignReviewPrompt(bundle) {
80
+ const deterministicFindings = bundle.design_assessment?.findings ?? [];
81
+ return [
82
+ "# Project design review",
83
+ "",
84
+ "You are reviewing the overall design of this project. The deterministic audit pipeline has already analyzed the codebase structure. Your job is to provide qualitative, big-picture design observations that static analysis cannot produce.",
85
+ "",
86
+ "## Project context",
87
+ "",
88
+ `Repository: ${bundle.repo_manifest?.repository?.name ?? "unknown"}`,
89
+ "",
90
+ "### File inventory",
91
+ "",
92
+ summarizeFiles(bundle),
93
+ "",
94
+ "### Unit structure",
95
+ "",
96
+ summarizeUnits(bundle),
97
+ "",
98
+ "### Dependency graph",
99
+ "",
100
+ summarizeGraph(bundle),
101
+ "",
102
+ "### Externally reachable surfaces",
103
+ "",
104
+ summarizeSurfaces(bundle),
105
+ "",
106
+ "### Critical flows",
107
+ "",
108
+ summarizeFlows(bundle),
109
+ "",
110
+ "### Risk profile",
111
+ "",
112
+ summarizeRisk(bundle),
113
+ "",
114
+ "### Deterministic structural findings",
115
+ "",
116
+ formatDeterministicFindings(deterministicFindings),
117
+ "",
118
+ "## What to assess",
119
+ "",
120
+ "Read the project source to understand what it does and how it works, then produce findings about:",
121
+ "",
122
+ "- **Tool and library opportunities**: third-party tools, libraries, or frameworks that would improve the project. Concrete suggestions with rationale, not generic advice.",
123
+ "- **Architecture pattern improvements**: structural changes that would improve extensibility, testability, or maintainability. Consider whether the current abstractions match the problem domain.",
124
+ "- **Design simplification**: areas where the design is over-engineered or where simpler alternatives would work. Conversely, areas that are under-designed for their importance.",
125
+ "- **Integration and generalization**: opportunities to make the project more portable, composable, or protocol-aligned (e.g., MCP, standard APIs, plugin architectures).",
126
+ "- **Missing capabilities**: gaps in the design that would become pain points as the project evolves.",
127
+ "",
128
+ "## Output format",
129
+ "",
130
+ "Produce a JSON array of findings. Each finding must conform to:",
131
+ "",
132
+ "```json",
133
+ "{",
134
+ ' "id": "DR-001",',
135
+ ' "title": "short descriptive title",',
136
+ ' "category": "one of: tool_opportunity, architecture_pattern, design_simplification, integration, missing_capability",',
137
+ ' "severity": "one of: critical, high, medium, low, info",',
138
+ ' "confidence": "one of: high, medium, low",',
139
+ ' "lens": "architecture",',
140
+ ' "summary": "detailed explanation of the observation and the recommended change",',
141
+ ' "affected_files": [{"path": "relevant/file.ts"}],',
142
+ ' "systemic": true',
143
+ "}",
144
+ "```",
145
+ "",
146
+ "Write the JSON array to the design review results path provided below. Use finding IDs starting with DR-001.",
147
+ "",
148
+ "Focus on substantive, actionable observations. Prefer fewer high-quality findings over many surface-level ones.",
149
+ "",
150
+ ].join("\n");
151
+ }
@@ -9,6 +9,16 @@ export const EXECUTOR_REGISTRY = [
9
9
  obligation_ids: ["structure_artifacts"],
10
10
  description: "Build structure artifacts such as units, surfaces, graphs, flows, and risk.",
11
11
  },
12
+ {
13
+ id: "design_assessment_executor",
14
+ obligation_ids: ["design_assessment_current"],
15
+ description: "Run deterministic structural analysis to assess overall project design.",
16
+ },
17
+ {
18
+ id: "design_review",
19
+ obligation_ids: ["design_review_completed"],
20
+ description: "Pause the pipeline and delegate a holistic project design review to the active LLM agent.",
21
+ },
12
22
  {
13
23
  id: "planning_executor",
14
24
  obligation_ids: ["planning_artifacts"],
@@ -13,6 +13,8 @@ export declare function resolveRuntimeValidationSpawnCommand(command: string[],
13
13
  };
14
14
  export declare function runIntakeExecutor(bundle: ArtifactBundle, root: string): Promise<ExecutorRunResult>;
15
15
  export declare function runStructureExecutor(bundle: ArtifactBundle, root?: string): Promise<ExecutorRunResult>;
16
+ export declare function runDesignAssessmentExecutor(bundle: ArtifactBundle): ExecutorRunResult;
17
+ export declare function runDesignReviewAutoComplete(bundle: ArtifactBundle): ExecutorRunResult;
16
18
  export declare function runPlanningExecutor(bundle: ArtifactBundle, root: string, lineIndex?: Record<string, number>): Promise<ExecutorRunResult>;
17
19
  export declare function runResultIngestionExecutor(bundle: ArtifactBundle, results: AuditResult[]): ExecutorRunResult;
18
20
  export declare function runRuntimeValidationExecutor(bundle: ArtifactBundle, root: string): Promise<ExecutorRunResult>;
@@ -15,6 +15,7 @@ import { buildUnitManifest } from "./unitBuilder.js";
15
15
  import { buildRepoManifestFromFs } from "../extractors/fsIntake.js";
16
16
  import { loadIgnoreFile } from "../extractors/ignore.js";
17
17
  import { ingestAuditResults, updateAuditTaskStatuses, } from "./resultIngestion.js";
18
+ import { buildDesignAssessment } from "../extractors/designAssessment.js";
18
19
  import { buildSelectiveDeepeningTasks } from "./selectiveDeepening.js";
19
20
  import { updateRuntimeValidationReport } from "./runtimeValidationUpdate.js";
20
21
  import { autoCompleteTrivialCoverage } from "./trivialAudit.js";
@@ -178,6 +179,47 @@ export async function runStructureExecutor(bundle, root) {
178
179
  : ""),
179
180
  };
180
181
  }
182
+ export function runDesignAssessmentExecutor(bundle) {
183
+ if (!bundle.unit_manifest ||
184
+ !bundle.graph_bundle ||
185
+ !bundle.critical_flows ||
186
+ !bundle.risk_register) {
187
+ throw new Error("Cannot run design assessment executor without structure artifacts");
188
+ }
189
+ const designAssessment = buildDesignAssessment({
190
+ unitManifest: bundle.unit_manifest,
191
+ graphBundle: bundle.graph_bundle,
192
+ criticalFlows: bundle.critical_flows,
193
+ riskRegister: bundle.risk_register,
194
+ });
195
+ return {
196
+ updated: {
197
+ ...bundle,
198
+ design_assessment: designAssessment,
199
+ },
200
+ artifacts_written: ["design_assessment.json"],
201
+ progress_summary: `Design assessment complete: ${designAssessment.findings.length} structural finding(s).`,
202
+ };
203
+ }
204
+ export function runDesignReviewAutoComplete(bundle) {
205
+ const existing = bundle.design_assessment;
206
+ if (!existing) {
207
+ throw new Error("Cannot auto-complete design review without design_assessment artifact");
208
+ }
209
+ const updated = {
210
+ ...existing,
211
+ reviewed: true,
212
+ review_findings: existing.review_findings ?? [],
213
+ };
214
+ return {
215
+ updated: {
216
+ ...bundle,
217
+ design_assessment: updated,
218
+ },
219
+ artifacts_written: ["design_assessment.json"],
220
+ progress_summary: "Design review auto-completed (host-agent review available via next-step).",
221
+ };
222
+ }
181
223
  export async function runPlanningExecutor(bundle, root, lineIndex = {}) {
182
224
  if (!bundle.repo_manifest) {
183
225
  throw new Error("Cannot run planning executor without repo_manifest");
@@ -409,6 +451,7 @@ export function runSynthesisExecutor(bundle, results) {
409
451
  coverageMatrix: bundle.coverage_matrix,
410
452
  runtimeValidationReport: bundle.runtime_validation_report,
411
453
  externalAnalyzerResults: bundle.external_analyzer_results,
454
+ designAssessment: bundle.design_assessment,
412
455
  });
413
456
  return {
414
457
  updated: {
@@ -6,6 +6,8 @@ const PRIORITY = [
6
6
  "auto_fixes_applied",
7
7
  "syntax_resolved",
8
8
  "structure_artifacts",
9
+ "design_assessment_current",
10
+ "design_review_completed",
9
11
  "planning_artifacts",
10
12
  "audit_tasks_completed",
11
13
  "audit_results_ingested",
@@ -29,6 +29,8 @@ export function deriveAuditState(bundle) {
29
29
  "critical_flows.json",
30
30
  "risk_register.json",
31
31
  ], structureReady)));
32
+ obligations.push(obligation("design_assessment_current", staleOrSatisfied(staleArtifacts, ["design_assessment.json"], has(bundle.design_assessment))));
33
+ obligations.push(obligation("design_review_completed", bundle.design_assessment?.reviewed ? "satisfied" : "missing"));
32
34
  const planningReady = has(bundle.coverage_matrix) &&
33
35
  has(bundle.flow_coverage) &&
34
36
  has(bundle.runtime_validation_tasks) &&
@@ -1,4 +1,5 @@
1
1
  import type { AuditResult, Finding } from "../types.js";
2
+ import type { DesignAssessment } from "../types/designAssessment.js";
2
3
  import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
3
4
  import type { RuntimeValidationReport } from "../types/runtimeValidation.js";
4
- export declare function mergeFindings(results: AuditResult[], runtimeReport?: RuntimeValidationReport, externalAnalyzerResults?: ExternalAnalyzerResults): Finding[];
5
+ export declare function mergeFindings(results: AuditResult[], runtimeReport?: RuntimeValidationReport, externalAnalyzerResults?: ExternalAnalyzerResults, designAssessment?: DesignAssessment): Finding[];
@@ -236,8 +236,20 @@ function relevantExternalEvidence(finding, results) {
236
236
  .filter((item) => findingPaths.has(item.path))
237
237
  .map((item) => `external:${results.tool}:${item.path}:${item.summary}`);
238
238
  }
239
- export function mergeFindings(results, runtimeReport, externalAnalyzerResults) {
239
+ export function mergeFindings(results, runtimeReport, externalAnalyzerResults, designAssessment) {
240
240
  const merged = new Map();
241
+ const allDesignFindings = [
242
+ ...(designAssessment?.findings ?? []),
243
+ ...(designAssessment?.review_findings ?? []),
244
+ ];
245
+ for (const finding of allDesignFindings) {
246
+ const key = findingKey(finding);
247
+ merged.set(key, {
248
+ ...finding,
249
+ affected_files: [...finding.affected_files],
250
+ evidence: [...(finding.evidence ?? [])],
251
+ });
252
+ }
241
253
  for (const result of results) {
242
254
  for (const finding of result.findings) {
243
255
  const key = findingKey(finding);
@@ -1,4 +1,5 @@
1
1
  import type { AuditResult, CoverageMatrix, Finding, UnitManifest } from "../types.js";
2
+ import type { DesignAssessment } from "../types/designAssessment.js";
2
3
  import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
3
4
  import type { CriticalFlowManifest } from "../types/flows.js";
4
5
  import type { GraphBundle } from "../types/graph.js";
@@ -25,5 +26,6 @@ export declare function buildAuditReportModel(params: {
25
26
  coverageMatrix?: CoverageMatrix;
26
27
  runtimeValidationReport?: RuntimeValidationReport;
27
28
  externalAnalyzerResults?: ExternalAnalyzerResults;
29
+ designAssessment?: DesignAssessment;
28
30
  }): AuditReportModel;
29
31
  export declare function renderAuditReportMarkdown(model: AuditReportModel): string;
@@ -32,7 +32,7 @@ function formatSeverityList(summary) {
32
32
  return parts.length > 0 ? parts.join(", ") : "none";
33
33
  }
34
34
  export function buildAuditReportModel(params) {
35
- const findings = mergeFindings(params.results, params.runtimeValidationReport, params.externalAnalyzerResults);
35
+ const findings = mergeFindings(params.results, params.runtimeValidationReport, params.externalAnalyzerResults, params.designAssessment);
36
36
  const workBlocks = buildWorkBlocks({
37
37
  findings,
38
38
  unitManifest: params.unitManifest,
@@ -0,0 +1,7 @@
1
+ import type { Finding } from "../types.js";
2
+ export interface DesignAssessment {
3
+ generated_at: string;
4
+ findings: Finding[];
5
+ review_findings?: Finding[];
6
+ reviewed?: boolean;
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,7 @@
1
1
  import type { AuditTask } from "../types.js";
2
2
  import { type ValidationIssue } from "./basic.js";
3
3
  export type IssueSeverity = "error" | "warning";
4
+ export declare function normalizeCoveragePath(path: string): string;
4
5
  export interface AuditResultIssue extends ValidationIssue {
5
6
  result_index: number;
6
7
  task_id: string;
@@ -1,4 +1,7 @@
1
1
  import { describeValue, formatValidationIssues, isRecord, } from "./basic.js";
2
+ export function normalizeCoveragePath(path) {
3
+ return path.replace(/\\/g, "/").replace(/^\.\//, "");
4
+ }
2
5
  const REQUIRED_FINDING_FIELDS = [
3
6
  "id",
4
7
  "title",
@@ -423,6 +426,18 @@ export function validateAuditResults(results, tasks, options = {}) {
423
426
  tasks.map((item) => item.task_id).join(", "),
424
427
  });
425
428
  }
429
+ const taskNormMap = new Map();
430
+ if (task) {
431
+ for (const fp of task.file_paths) {
432
+ taskNormMap.set(normalizeCoveragePath(fp), fp);
433
+ }
434
+ }
435
+ const normLineIndex = new Map();
436
+ if (options.lineIndex) {
437
+ for (const [k, v] of Object.entries(options.lineIndex)) {
438
+ normLineIndex.set(normalizeCoveragePath(k), v);
439
+ }
440
+ }
426
441
  const fileCoverage = result.file_coverage;
427
442
  const normalizedFileCoverage = [];
428
443
  const declaredAssignedCoveragePaths = new Set();
@@ -447,6 +462,10 @@ export function validateAuditResults(results, tasks, options = {}) {
447
462
  });
448
463
  continue;
449
464
  }
465
+ const entryNorm = isNonEmptyString(entry.path)
466
+ ? normalizeCoveragePath(entry.path)
467
+ : "";
468
+ const canonicalPath = taskNormMap.get(entryNorm);
450
469
  if (!isNonEmptyString(entry.path)) {
451
470
  pushIssue(issues, {
452
471
  result_index: i,
@@ -455,7 +474,7 @@ export function validateAuditResults(results, tasks, options = {}) {
455
474
  message: "file_coverage entry has an empty path.",
456
475
  });
457
476
  }
458
- else if (task && !task.file_paths.includes(entry.path)) {
477
+ else if (task && !canonicalPath) {
459
478
  pushIssue(issues, {
460
479
  result_index: i,
461
480
  task_id: taskId,
@@ -463,7 +482,7 @@ export function validateAuditResults(results, tasks, options = {}) {
463
482
  message: `file_coverage path '${entry.path}' is not listed in the task file_paths.`,
464
483
  });
465
484
  }
466
- else if (seenCoveragePaths.has(entry.path)) {
485
+ else if (seenCoveragePaths.has(entryNorm)) {
467
486
  pushIssue(issues, {
468
487
  result_index: i,
469
488
  task_id: taskId,
@@ -472,11 +491,10 @@ export function validateAuditResults(results, tasks, options = {}) {
472
491
  });
473
492
  }
474
493
  else {
475
- seenCoveragePaths.add(entry.path);
494
+ seenCoveragePaths.add(entryNorm);
476
495
  }
477
- if (isNonEmptyString(entry.path) &&
478
- (!task || task.file_paths.includes(entry.path))) {
479
- declaredAssignedCoveragePaths.add(entry.path);
496
+ if (entryNorm.length > 0 && (!task || canonicalPath)) {
497
+ declaredAssignedCoveragePaths.add(canonicalPath ?? entryNorm);
480
498
  }
481
499
  if (!Number.isInteger(entry.total_lines)) {
482
500
  pushIssue(issues, {
@@ -495,8 +513,8 @@ export function validateAuditResults(results, tasks, options = {}) {
495
513
  message: "file_coverage total_lines must be zero or greater.",
496
514
  });
497
515
  }
498
- const expectedLineCount = typeof entry.path === "string"
499
- ? options.lineIndex?.[entry.path]
516
+ const expectedLineCount = entryNorm.length > 0
517
+ ? normLineIndex.get(entryNorm)
500
518
  : undefined;
501
519
  if (Number.isInteger(entry.total_lines) &&
502
520
  typeof expectedLineCount === "number" &&
@@ -509,19 +527,19 @@ export function validateAuditResults(results, tasks, options = {}) {
509
527
  `(expected ${expectedLineCount}, got ${entry.total_lines}).`,
510
528
  });
511
529
  }
512
- if (isNonEmptyString(entry.path) &&
530
+ if (entryNorm.length > 0 &&
513
531
  Number.isInteger(entry.total_lines) &&
514
532
  Number(entry.total_lines) >= 0 &&
515
- (!task || task.file_paths.includes(entry.path))) {
533
+ (!task || canonicalPath)) {
516
534
  normalizedFileCoverage.push({
517
- path: entry.path,
535
+ path: canonicalPath ?? entryNorm,
518
536
  total_lines: Number(entry.total_lines),
519
537
  });
520
538
  }
521
539
  }
522
540
  if (task) {
523
541
  for (const path of task.file_paths) {
524
- if (!seenCoveragePaths.has(path)) {
542
+ if (!seenCoveragePaths.has(normalizeCoveragePath(path))) {
525
543
  pushIssue(issues, {
526
544
  result_index: i,
527
545
  task_id: taskId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditor-lambda",
3
- "version": "0.3.38",
3
+ "version": "0.3.40",
4
4
  "private": false,
5
5
  "description": "Portable hybrid code-auditing framework for arbitrary repositories.",
6
6
  "type": "module",