auditor-lambda 0.3.37 → 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.
Files changed (37) hide show
  1. package/dist/cli.js +102 -2
  2. package/dist/extractors/designAssessment.d.ts +11 -0
  3. package/dist/extractors/designAssessment.js +254 -0
  4. package/dist/io/artifacts.d.ts +3 -0
  5. package/dist/io/artifacts.js +1 -0
  6. package/dist/orchestrator/advance.js +7 -1
  7. package/dist/orchestrator/dependencyMap.js +5 -0
  8. package/dist/orchestrator/designReviewPrompt.d.ts +2 -0
  9. package/dist/orchestrator/designReviewPrompt.js +151 -0
  10. package/dist/orchestrator/executors.js +10 -0
  11. package/dist/orchestrator/internalExecutors.d.ts +2 -0
  12. package/dist/orchestrator/internalExecutors.js +43 -0
  13. package/dist/orchestrator/nextStep.js +2 -0
  14. package/dist/orchestrator/state.js +2 -0
  15. package/dist/providers/types.d.ts +6 -0
  16. package/dist/quota/discoveredLimits.d.ts +21 -0
  17. package/dist/quota/discoveredLimits.js +74 -0
  18. package/dist/quota/headerExtraction.d.ts +8 -0
  19. package/dist/quota/headerExtraction.js +140 -0
  20. package/dist/quota/headerExtractors/claudeCodeHeaderExtractor.d.ts +6 -0
  21. package/dist/quota/headerExtractors/claudeCodeHeaderExtractor.js +28 -0
  22. package/dist/quota/headerExtractors/genericHeaderExtractor.d.ts +9 -0
  23. package/dist/quota/headerExtractors/genericHeaderExtractor.js +7 -0
  24. package/dist/quota/headerExtractors/index.d.ts +5 -0
  25. package/dist/quota/headerExtractors/index.js +12 -0
  26. package/dist/quota/index.d.ts +6 -0
  27. package/dist/quota/index.js +3 -0
  28. package/dist/quota/scheduler.d.ts +3 -0
  29. package/dist/quota/scheduler.js +18 -1
  30. package/dist/reporting/mergeFindings.d.ts +2 -1
  31. package/dist/reporting/mergeFindings.js +13 -1
  32. package/dist/reporting/synthesis.d.ts +2 -0
  33. package/dist/reporting/synthesis.js +1 -1
  34. package/dist/types/designAssessment.d.ts +7 -0
  35. package/dist/types/designAssessment.js +1 -0
  36. package/dist/types/sessionConfig.d.ts +3 -0
  37. package/package.json +1 -1
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";
@@ -32,7 +33,7 @@ import { buildReviewPackets, orderTasksForPacketReview, estimateTaskGroupTokens,
32
33
  import { buildFileAnchorSummary, } from "./orchestrator/fileAnchors.js";
33
34
  import { LOCAL_SUBPROCESS_PROVIDER_NAME } from "./providers/constants.js";
34
35
  import { runAuditCodeMcpServer } from "./mcp/server.js";
35
- import { scheduleWave, buildProviderModelKey, readQuotaState, recordWaveOutcome, resolveLimits, resolveHostActiveSubagentLimit, probeProvider, computeMaxSafeConcurrency, getQuotaStatePath, detectRateLimitError, computeCooldownUntil, runSlidingWindow, LearnedQuotaSource, CompositeQuotaSource, } from "./quota/index.js";
36
+ import { scheduleWave, buildProviderModelKey, readQuotaState, recordWaveOutcome, resolveLimits, resolveHostActiveSubagentLimit, probeProvider, computeMaxSafeConcurrency, getQuotaStatePath, detectRateLimitError, computeCooldownUntil, runSlidingWindow, LearnedQuotaSource, CompositeQuotaSource, lookupDiscoveredLimits, updateDiscoveredLimits, mergeDiscoveredLimits, getHeaderExtractorForProvider, } from "./quota/index.js";
36
37
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
37
38
  const ADVANCE_AUDIT_CONTRACT_VERSION = "audit-code/v1alpha1";
38
39
  const WORKER_RESULT_CONTRACT_VERSION = "audit-code-worker-result/v1alpha1";
@@ -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);
@@ -1426,6 +1485,18 @@ async function cmdRunToCompletion(argv) {
1426
1485
  const allCandidateTasks = buildPendingAuditTasks(bundle);
1427
1486
  const candidateGroups = chunkArray(allCandidateTasks.slice(0, parallelWorkers * agentBatchSize), agentBatchSize);
1428
1487
  const slotTokenEstimates = candidateGroups.map((g) => estimateTaskGroupTokens(g));
1488
+ const providerLimits = await provider.queryLimits?.(hostModel)
1489
+ .then((r) => r ? { ...r, source: "provider_query" } : null)
1490
+ .catch(() => null)
1491
+ ?? null;
1492
+ const cachedLimits = await lookupDiscoveredLimits(providerModelKey).catch(() => null);
1493
+ const discoveredLimits = mergeDiscoveredLimits(providerLimits, cachedLimits);
1494
+ const halfLifeHours = sessionConfig.quota?.empirical_half_life_hours ?? 24;
1495
+ const quotaSource = new CompositeQuotaSource([new LearnedQuotaSource(halfLifeHours)]);
1496
+ const quotaSourceSnapshot = await quotaSource.queryCurrentUsage(providerModelKey).catch(() => null);
1497
+ const hostConcurrencyLimit = resolveHostActiveSubagentLimit({
1498
+ sessionConfig,
1499
+ });
1429
1500
  const waveSchedule = scheduleWave({
1430
1501
  providerName: resolveFreshSessionProviderName(getExplicitProvider(argv), sessionConfig),
1431
1502
  sessionConfig,
@@ -1433,6 +1504,9 @@ async function cmdRunToCompletion(argv) {
1433
1504
  requestedConcurrency: parallelWorkers,
1434
1505
  estimatedSlotTokens: slotTokenEstimates,
1435
1506
  quotaStateEntry,
1507
+ hostConcurrencyLimit,
1508
+ quotaSourceSnapshot,
1509
+ discoveredLimits,
1436
1510
  });
1437
1511
  const waveSize = waveSchedule.wave_size;
1438
1512
  if (waveSchedule.cooldown_until) {
@@ -1615,6 +1689,27 @@ async function cmdRunToCompletion(argv) {
1615
1689
  cooldown_until: rateLimitHit ? computeCooldownUntil(retryAfterMs) : null,
1616
1690
  }, sessionConfig.quota?.empirical_half_life_hours ?? 24).catch(() => undefined);
1617
1691
  }
1692
+ // Extract rate-limit headers from worker stderr (best-effort)
1693
+ {
1694
+ const extractor = getHeaderExtractorForProvider(provider.name);
1695
+ for (const slot of workerSlots) {
1696
+ try {
1697
+ const stderr = await readFile(slot.paths.stderrPath, "utf8");
1698
+ const extracted = extractor.extract(stderr);
1699
+ if (extracted && (extracted.requests_per_minute != null || extracted.input_tokens_per_minute != null)) {
1700
+ await updateDiscoveredLimits(providerModelKey, {
1701
+ requests_per_minute: extracted.requests_per_minute,
1702
+ input_tokens_per_minute: extracted.input_tokens_per_minute,
1703
+ source: "header_extraction",
1704
+ });
1705
+ break; // one successful extraction is enough
1706
+ }
1707
+ }
1708
+ catch {
1709
+ // stderr file missing or unreadable — skip
1710
+ }
1711
+ }
1712
+ }
1618
1713
  if (batchErrors.length > 0) {
1619
1714
  const bundleAfter = await loadArtifactBundle(artifactsDir);
1620
1715
  const blockedState = buildBlockedAuditState({
@@ -2470,6 +2565,7 @@ async function prepareDispatchArtifacts(params) {
2470
2565
  explicitLimit: params.hostActiveSubagentLimit,
2471
2566
  sessionConfig,
2472
2567
  });
2568
+ const dispatchCachedLimits = await lookupDiscoveredLimits(quotaProviderKey).catch(() => null);
2473
2569
  const waveSchedule = scheduleWave({
2474
2570
  providerName: quotaProviderName,
2475
2571
  sessionConfig,
@@ -2478,6 +2574,7 @@ async function prepareDispatchArtifacts(params) {
2478
2574
  estimatedSlotTokens: perPacketTokens,
2479
2575
  quotaStateEntry,
2480
2576
  hostConcurrencyLimit,
2577
+ discoveredLimits: dispatchCachedLimits,
2481
2578
  });
2482
2579
  const dispatchQuota = {
2483
2580
  contract_version: "audit-code-dispatch-quota/v1alpha2",
@@ -3227,6 +3324,7 @@ async function cmdQuota(argv) {
3227
3324
  });
3228
3325
  const quotaSource = new CompositeQuotaSource([new LearnedQuotaSource(halfLifeHours)]);
3229
3326
  const quotaSourceSnapshot = await quotaSource.queryCurrentUsage(providerModelKey).catch(() => null);
3327
+ const queryDiscoveredLimits = await lookupDiscoveredLimits(providerModelKey).catch(() => null);
3230
3328
  const waveSchedule = scheduleWave({
3231
3329
  providerName,
3232
3330
  sessionConfig,
@@ -3235,6 +3333,7 @@ async function cmdQuota(argv) {
3235
3333
  quotaStateEntry,
3236
3334
  hostConcurrencyLimit,
3237
3335
  quotaSourceSnapshot,
3336
+ discoveredLimits: queryDiscoveredLimits,
3238
3337
  });
3239
3338
  console.log(JSON.stringify({
3240
3339
  provider: providerName,
@@ -3253,6 +3352,7 @@ async function cmdQuota(argv) {
3253
3352
  }
3254
3353
  : null,
3255
3354
  quota_source_snapshot: quotaSourceSnapshot,
3355
+ discovered_limits: queryDiscoveredLimits,
3256
3356
  wave_schedule: waveSchedule,
3257
3357
  quota_state_path: getQuotaStatePath(),
3258
3358
  }, null, 2));
@@ -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;