auditor-lambda 0.3.38 → 0.3.39

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);
@@ -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 {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditor-lambda",
3
- "version": "0.3.38",
3
+ "version": "0.3.39",
4
4
  "private": false,
5
5
  "description": "Portable hybrid code-auditing framework for arbitrary repositories.",
6
6
  "type": "module",