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.
- package/dist/cli.js +102 -2
- package/dist/extractors/designAssessment.d.ts +11 -0
- package/dist/extractors/designAssessment.js +254 -0
- package/dist/io/artifacts.d.ts +3 -0
- package/dist/io/artifacts.js +1 -0
- package/dist/orchestrator/advance.js +7 -1
- package/dist/orchestrator/dependencyMap.js +5 -0
- package/dist/orchestrator/designReviewPrompt.d.ts +2 -0
- package/dist/orchestrator/designReviewPrompt.js +151 -0
- package/dist/orchestrator/executors.js +10 -0
- package/dist/orchestrator/internalExecutors.d.ts +2 -0
- package/dist/orchestrator/internalExecutors.js +43 -0
- package/dist/orchestrator/nextStep.js +2 -0
- package/dist/orchestrator/state.js +2 -0
- package/dist/providers/types.d.ts +6 -0
- package/dist/quota/discoveredLimits.d.ts +21 -0
- package/dist/quota/discoveredLimits.js +74 -0
- package/dist/quota/headerExtraction.d.ts +8 -0
- package/dist/quota/headerExtraction.js +140 -0
- package/dist/quota/headerExtractors/claudeCodeHeaderExtractor.d.ts +6 -0
- package/dist/quota/headerExtractors/claudeCodeHeaderExtractor.js +28 -0
- package/dist/quota/headerExtractors/genericHeaderExtractor.d.ts +9 -0
- package/dist/quota/headerExtractors/genericHeaderExtractor.js +7 -0
- package/dist/quota/headerExtractors/index.d.ts +5 -0
- package/dist/quota/headerExtractors/index.js +12 -0
- package/dist/quota/index.d.ts +6 -0
- package/dist/quota/index.js +3 -0
- package/dist/quota/scheduler.d.ts +3 -0
- package/dist/quota/scheduler.js +18 -1
- package/dist/reporting/mergeFindings.d.ts +2 -1
- package/dist/reporting/mergeFindings.js +13 -1
- package/dist/reporting/synthesis.d.ts +2 -0
- package/dist/reporting/synthesis.js +1 -1
- package/dist/types/designAssessment.d.ts +7 -0
- package/dist/types/designAssessment.js +1 -0
- package/dist/types/sessionConfig.d.ts +3 -0
- 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
|
+
}
|
package/dist/io/artifacts.d.ts
CHANGED
|
@@ -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">;
|
package/dist/io/artifacts.js
CHANGED
|
@@ -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",
|