auditor-lambda 0.8.0 → 0.9.0
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/audit-code-wrapper-lib.mjs +149 -129
- package/dist/adapters/normalizeExternal.js +6 -3
- package/dist/cli/args.d.ts +0 -1
- package/dist/cli/args.js +0 -6
- package/dist/cli/dispatch.js +3 -2
- package/dist/cli/lineIndex.js +4 -1
- package/dist/cli/mergeAndIngestCommand.d.ts +1 -0
- package/dist/cli/mergeAndIngestCommand.js +219 -0
- package/dist/cli/nextStepCommand.js +5 -1
- package/dist/cli/runToCompletion.d.ts +9 -0
- package/dist/cli/runToCompletion.js +655 -480
- package/dist/cli/statusCommand.d.ts +1 -0
- package/dist/cli/statusCommand.js +113 -0
- package/dist/cli/submitPacketCommand.d.ts +1 -0
- package/dist/cli/submitPacketCommand.js +155 -0
- package/dist/cli/workerResult.d.ts +1 -1
- package/dist/cli/workerRunCommand.d.ts +1 -0
- package/dist/cli/workerRunCommand.js +88 -0
- package/dist/cli.js +14 -563
- package/dist/extractors/analyzers/sql.js +4 -1
- package/dist/extractors/analyzers/treeSitter.js +29 -15
- package/dist/extractors/analyzers/typescript.js +10 -8
- package/dist/extractors/designAssessment.js +43 -24
- package/dist/extractors/graph.js +139 -73
- package/dist/extractors/pathPatterns.js +17 -5
- package/dist/io/runArtifactTypes.d.ts +18 -0
- package/dist/io/runArtifactTypes.js +1 -0
- package/dist/io/runArtifacts.d.ts +2 -18
- package/dist/io/runArtifacts.js +14 -3
- package/dist/mcp/server.js +9 -0
- package/dist/orchestrator/advance.js +37 -22
- package/dist/orchestrator/artifactFreshness.js +2 -2
- package/dist/orchestrator/autoFixExecutor.d.ts +1 -1
- package/dist/orchestrator/autoFixExecutor.js +16 -8
- package/dist/orchestrator/dependencyMap.d.ts +1 -1
- package/dist/orchestrator/dependencyMap.js +7 -1
- package/dist/orchestrator/fileAnchors.js +14 -3
- package/dist/orchestrator/flowCoverage.js +1 -0
- package/dist/orchestrator/flowRequeue.js +4 -1
- package/dist/orchestrator/{internalExecutors.d.ts → ingestionExecutors.d.ts} +0 -6
- package/dist/orchestrator/ingestionExecutors.js +237 -0
- package/dist/orchestrator/intakeExecutors.d.ts +3 -0
- package/dist/orchestrator/intakeExecutors.js +25 -0
- package/dist/orchestrator/planningExecutors.d.ts +4 -0
- package/dist/orchestrator/planningExecutors.js +95 -0
- package/dist/orchestrator/runtimeCommand.js +7 -15
- package/dist/orchestrator/selectiveDeepening/conflict.d.ts +8 -0
- package/dist/orchestrator/selectiveDeepening/conflict.js +71 -0
- package/dist/orchestrator/selectiveDeepening/findingFollowup.d.ts +10 -0
- package/dist/orchestrator/selectiveDeepening/findingFollowup.js +52 -0
- package/dist/orchestrator/selectiveDeepening/highRiskClean.d.ts +7 -0
- package/dist/orchestrator/selectiveDeepening/highRiskClean.js +44 -0
- package/dist/orchestrator/selectiveDeepening/index.d.ts +18 -0
- package/dist/orchestrator/selectiveDeepening/index.js +128 -0
- package/dist/orchestrator/selectiveDeepening/lensVerification.d.ts +12 -0
- package/dist/orchestrator/selectiveDeepening/lensVerification.js +242 -0
- package/dist/orchestrator/selectiveDeepening/runtimeValidation.d.ts +13 -0
- package/dist/orchestrator/selectiveDeepening/runtimeValidation.js +57 -0
- package/dist/orchestrator/selectiveDeepening/shared.d.ts +45 -0
- package/dist/orchestrator/selectiveDeepening/shared.js +128 -0
- package/dist/orchestrator/selectiveDeepening/stewardFollowup.d.ts +6 -0
- package/dist/orchestrator/selectiveDeepening/stewardFollowup.js +72 -0
- package/dist/orchestrator/selectiveDeepening.d.ts +2 -20
- package/dist/orchestrator/selectiveDeepening.js +6 -760
- package/dist/orchestrator/staleness.js +3 -3
- package/dist/orchestrator/structureExecutors.d.ts +5 -0
- package/dist/orchestrator/structureExecutors.js +94 -0
- package/dist/orchestrator/taskBuilder.d.ts +2 -2
- package/dist/orchestrator/taskBuilder.js +101 -82
- package/dist/providers/index.d.ts +7 -0
- package/dist/providers/index.js +14 -95
- package/dist/quota/discoveredLimits.d.ts +1 -0
- package/dist/quota/discoveredLimits.js +7 -1
- package/dist/quota/index.d.ts +0 -2
- package/dist/quota/index.js +1 -2
- package/dist/reporting/workBlocks.js +7 -4
- package/dist/types/reviewPlanning.d.ts +23 -16
- package/dist/validation/auditResults.js +97 -95
- package/dist/validation/sessionConfig.d.ts +2 -2
- package/dist/validation/sessionConfig.js +14 -7
- package/package.json +3 -2
- package/schemas/audit_findings.schema.json +3 -3
- package/schemas/critical_flows.schema.json +3 -2
- package/schemas/dispatch_quota.schema.json +1 -1
- package/schemas/graph_bundle.schema.json +1 -1
- package/schemas/review_packets.schema.json +1 -1
- package/schemas/step_contract.schema.json +80 -0
- package/scripts/postinstall.mjs +19 -2
- package/skills/audit-code/opencode-command-template.txt +3 -3
- package/dist/orchestrator/internalExecutors.js +0 -424
- package/dist/providers/localSubprocessProvider.d.ts +0 -9
- package/dist/providers/localSubprocessProvider.js +0 -18
- package/dist/providers/subprocessTemplateProvider.d.ts +0 -8
- package/dist/providers/subprocessTemplateProvider.js +0 -59
- package/dist/providers/vscodeTaskProvider.d.ts +0 -7
- package/dist/providers/vscodeTaskProvider.js +0 -14
- package/dist/quota/probe.d.ts +0 -10
- package/dist/quota/probe.js +0 -18
|
@@ -2,8 +2,12 @@ import { createRequire } from "node:module";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
const requireFromHere = createRequire(import.meta.url);
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// The parser module is resolved per `dependencyPath`: a call with a different
|
|
6
|
+
// dependencyPath must resolve its own module rather than reusing the first
|
|
7
|
+
// resolution. Keyed by `dependencyPath ?? ""` so the bare-specifier path
|
|
8
|
+
// (no dependencyPath) gets a stable cache slot too.
|
|
9
|
+
const moduleCache = new Map();
|
|
10
|
+
const initCache = new Map();
|
|
7
11
|
const languageCache = new Map();
|
|
8
12
|
async function importParserModule(dependencyPath) {
|
|
9
13
|
const specifiers = [];
|
|
@@ -26,25 +30,33 @@ async function importParserModule(dependencyPath) {
|
|
|
26
30
|
return resolved;
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
|
-
catch {
|
|
30
|
-
|
|
33
|
+
catch (e) {
|
|
34
|
+
process.stderr.write(`[audit-code] tree-sitter: failed to import '${specifier}': ${e.message ?? String(e)}\n`);
|
|
31
35
|
}
|
|
32
36
|
}
|
|
33
37
|
return undefined;
|
|
34
38
|
}
|
|
35
39
|
async function getModule(dependencyPath) {
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
const key = dependencyPath ?? "";
|
|
41
|
+
let cached = moduleCache.get(key);
|
|
42
|
+
if (!cached) {
|
|
43
|
+
cached = importParserModule(dependencyPath);
|
|
44
|
+
moduleCache.set(key, cached);
|
|
38
45
|
}
|
|
39
|
-
return
|
|
46
|
+
return cached;
|
|
40
47
|
}
|
|
41
48
|
async function ensureInit(parserModule) {
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
let cached = initCache.get(parserModule);
|
|
50
|
+
if (!cached) {
|
|
51
|
+
cached = parserModule.Parser.init()
|
|
44
52
|
.then(() => true)
|
|
45
|
-
.catch(() =>
|
|
53
|
+
.catch((e) => {
|
|
54
|
+
process.stderr.write(`[audit-code] tree-sitter: Parser.init() failed: ${e.message ?? String(e)}\n`);
|
|
55
|
+
return false;
|
|
56
|
+
});
|
|
57
|
+
initCache.set(parserModule, cached);
|
|
46
58
|
}
|
|
47
|
-
return
|
|
59
|
+
return cached;
|
|
48
60
|
}
|
|
49
61
|
function resolveGrammarPath(grammar) {
|
|
50
62
|
// tree-sitter-wasms ships prebuilt grammars under out/tree-sitter-<lang>.wasm.
|
|
@@ -76,7 +88,8 @@ async function loadLanguage(parserModule, grammar) {
|
|
|
76
88
|
languageCache.set(grammar, language);
|
|
77
89
|
return language;
|
|
78
90
|
}
|
|
79
|
-
catch {
|
|
91
|
+
catch (e) {
|
|
92
|
+
process.stderr.write(`[audit-code] tree-sitter: failed to load grammar '${grammar}' from '${grammarPath}': ${e.message ?? String(e)}\n`);
|
|
80
93
|
languageCache.set(grammar, null);
|
|
81
94
|
return undefined;
|
|
82
95
|
}
|
|
@@ -99,13 +112,14 @@ export async function getTreeSitterParser(grammar, dependencyPath) {
|
|
|
99
112
|
parser.setLanguage(language);
|
|
100
113
|
return parser;
|
|
101
114
|
}
|
|
102
|
-
catch {
|
|
115
|
+
catch (e) {
|
|
116
|
+
process.stderr.write(`[audit-code] tree-sitter: failed to instantiate parser for grammar '${grammar}': ${e.message ?? String(e)}\n`);
|
|
103
117
|
return undefined;
|
|
104
118
|
}
|
|
105
119
|
}
|
|
106
120
|
/** Test seam: reset the memoised runtime/grammar caches. */
|
|
107
121
|
export function __resetTreeSitterForTests() {
|
|
108
|
-
|
|
109
|
-
|
|
122
|
+
moduleCache.clear();
|
|
123
|
+
initCache.clear();
|
|
110
124
|
languageCache.clear();
|
|
111
125
|
}
|
|
@@ -33,8 +33,8 @@ async function loadTypescript(dependencyPath) {
|
|
|
33
33
|
const mod = (await import(pathToFileURL(mainPath).href));
|
|
34
34
|
return (mod.default ?? mod);
|
|
35
35
|
}
|
|
36
|
-
catch {
|
|
37
|
-
|
|
36
|
+
catch (e) {
|
|
37
|
+
process.stderr.write(`[audit-code] typescript-analyzer: failed to load TypeScript from '${dependencyPath}', falling back to bundled: ${e.message ?? String(e)}\n`);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
const mod = (await import("typescript"));
|
|
@@ -50,7 +50,8 @@ function loadCompilerOptions(ts, root) {
|
|
|
50
50
|
options = parsed.options;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
catch {
|
|
53
|
+
catch (e) {
|
|
54
|
+
process.stderr.write(`[audit-code] typescript-analyzer: failed to load compiler options from '${root}', using defaults: ${e.message ?? String(e)}\n`);
|
|
54
55
|
options = {};
|
|
55
56
|
}
|
|
56
57
|
// Force a lenient, emit-free, JS-aware program: we only want resolution + the
|
|
@@ -97,8 +98,8 @@ function resolveSymbolToIncluded(state, symbol) {
|
|
|
97
98
|
try {
|
|
98
99
|
resolved = state.checker.getAliasedSymbol(resolved);
|
|
99
100
|
}
|
|
100
|
-
catch {
|
|
101
|
-
//
|
|
101
|
+
catch (_e) {
|
|
102
|
+
// getAliasedSymbol can throw for malformed programs; keep the un-aliased symbol.
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
for (const declaration of resolved.declarations ?? []) {
|
|
@@ -221,7 +222,8 @@ async function analyze(files, context) {
|
|
|
221
222
|
try {
|
|
222
223
|
ts = await loadTypescript(context.dependencyPath);
|
|
223
224
|
}
|
|
224
|
-
catch {
|
|
225
|
+
catch (e) {
|
|
226
|
+
process.stderr.write(`[audit-code] typescript-analyzer: failed to load TypeScript compiler, skipping ${files.length} file(s): ${e.message ?? String(e)}\n`);
|
|
225
227
|
return { edges: [] };
|
|
226
228
|
}
|
|
227
229
|
try {
|
|
@@ -244,8 +246,8 @@ async function analyze(files, context) {
|
|
|
244
246
|
}
|
|
245
247
|
return { edges: [...imports, ...references, ...calls] };
|
|
246
248
|
}
|
|
247
|
-
catch {
|
|
248
|
-
|
|
249
|
+
catch (e) {
|
|
250
|
+
process.stderr.write(`[audit-code] typescript-analyzer: program analysis failed for ${files.length} file(s) under '${context.root}', degrading to regex floor: ${e.message ?? String(e)}\n`);
|
|
249
251
|
return { edges: [] };
|
|
250
252
|
}
|
|
251
253
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// ID generation is instance-scoped per build (no shared mutable module state),
|
|
2
|
+
// so repeated/concurrent buildDesignAssessment calls produce independent,
|
|
3
|
+
// non-colliding DA-### sequences.
|
|
4
|
+
function createFindingIdGenerator() {
|
|
5
|
+
let n = 1;
|
|
6
|
+
return () => `DA-${String(n++).padStart(3, "0")}`;
|
|
4
7
|
}
|
|
5
8
|
function allEdges(graphBundle) {
|
|
6
9
|
const edges = [];
|
|
@@ -49,11 +52,27 @@ function detectCycles(edges) {
|
|
|
49
52
|
}
|
|
50
53
|
return cycles;
|
|
51
54
|
}
|
|
55
|
+
// Canonicalize a directed cycle by rotating it so the lexicographically
|
|
56
|
+
// smallest node leads, preserving order/direction. Rotation (not sort) keeps
|
|
57
|
+
// distinct directed cycles over the same node set apart (A→B→C→A vs A→C→B→A)
|
|
58
|
+
// while still deduping the same cycle discovered from different DFS start nodes
|
|
59
|
+
// (which differ only by rotation).
|
|
60
|
+
function canonicalCycleKey(cycle) {
|
|
61
|
+
if (cycle.length === 0)
|
|
62
|
+
return "";
|
|
63
|
+
let minIdx = 0;
|
|
64
|
+
for (let i = 1; i < cycle.length; i++) {
|
|
65
|
+
if (cycle[i] < cycle[minIdx])
|
|
66
|
+
minIdx = i;
|
|
67
|
+
}
|
|
68
|
+
const rotated = [...cycle.slice(minIdx), ...cycle.slice(0, minIdx)];
|
|
69
|
+
return rotated.join("\0");
|
|
70
|
+
}
|
|
52
71
|
function deduplicateCycles(cycles) {
|
|
53
72
|
const seen = new Set();
|
|
54
73
|
const unique = [];
|
|
55
74
|
for (const cycle of cycles) {
|
|
56
|
-
const normalized =
|
|
75
|
+
const normalized = canonicalCycleKey(cycle);
|
|
57
76
|
if (!seen.has(normalized)) {
|
|
58
77
|
seen.add(normalized);
|
|
59
78
|
unique.push(cycle);
|
|
@@ -61,13 +80,13 @@ function deduplicateCycles(cycles) {
|
|
|
61
80
|
}
|
|
62
81
|
return unique;
|
|
63
82
|
}
|
|
64
|
-
function detectCycleFindings(graphBundle) {
|
|
83
|
+
function detectCycleFindings(graphBundle, nextId) {
|
|
65
84
|
const edges = allEdges(graphBundle);
|
|
66
85
|
const cycles = deduplicateCycles(detectCycles(edges));
|
|
67
86
|
if (cycles.length === 0)
|
|
68
87
|
return [];
|
|
69
88
|
return cycles.slice(0, 10).map((cycle) => ({
|
|
70
|
-
id:
|
|
89
|
+
id: nextId(),
|
|
71
90
|
title: `Dependency cycle: ${cycle.length} modules`,
|
|
72
91
|
category: "dependency_cycle",
|
|
73
92
|
severity: cycle.length > 4 ? "high" : "medium",
|
|
@@ -78,7 +97,7 @@ function detectCycleFindings(graphBundle) {
|
|
|
78
97
|
systemic: true,
|
|
79
98
|
}));
|
|
80
99
|
}
|
|
81
|
-
function detectHubModules(graphBundle) {
|
|
100
|
+
function detectHubModules(graphBundle, nextId) {
|
|
82
101
|
const edges = allEdges(graphBundle);
|
|
83
102
|
const fanIn = new Map();
|
|
84
103
|
const fanOut = new Map();
|
|
@@ -94,7 +113,7 @@ function detectHubModules(graphBundle) {
|
|
|
94
113
|
const outCount = fanOut.get(node) ?? 0;
|
|
95
114
|
if (inCount >= hubThreshold && outCount >= hubThreshold) {
|
|
96
115
|
findings.push({
|
|
97
|
-
id:
|
|
116
|
+
id: nextId(),
|
|
98
117
|
title: `Hub module: ${node}`,
|
|
99
118
|
category: "hub_module",
|
|
100
119
|
severity: "medium",
|
|
@@ -108,7 +127,7 @@ function detectHubModules(graphBundle) {
|
|
|
108
127
|
}
|
|
109
128
|
return findings;
|
|
110
129
|
}
|
|
111
|
-
function detectOrphanUnits(unitManifest, graphBundle) {
|
|
130
|
+
function detectOrphanUnits(unitManifest, graphBundle, nextId) {
|
|
112
131
|
const edges = allEdges(graphBundle);
|
|
113
132
|
const connected = new Set();
|
|
114
133
|
for (const edge of edges) {
|
|
@@ -129,7 +148,7 @@ function detectOrphanUnits(unitManifest, graphBundle) {
|
|
|
129
148
|
if (orphans.length > unitManifest.units.length * 0.5)
|
|
130
149
|
return [];
|
|
131
150
|
return [{
|
|
132
|
-
id:
|
|
151
|
+
id: nextId(),
|
|
133
152
|
title: `${orphans.length} orphan unit(s) with no graph connections`,
|
|
134
153
|
category: "orphan_units",
|
|
135
154
|
severity: "low",
|
|
@@ -143,7 +162,7 @@ function detectOrphanUnits(unitManifest, graphBundle) {
|
|
|
143
162
|
systemic: true,
|
|
144
163
|
}];
|
|
145
164
|
}
|
|
146
|
-
function detectRiskConcentration(riskRegister, unitManifest) {
|
|
165
|
+
function detectRiskConcentration(riskRegister, unitManifest, nextId) {
|
|
147
166
|
if (riskRegister.items.length < 4)
|
|
148
167
|
return [];
|
|
149
168
|
const sorted = [...riskRegister.items].sort((a, b) => b.risk_score - a.risk_score);
|
|
@@ -157,7 +176,7 @@ function detectRiskConcentration(riskRegister, unitManifest) {
|
|
|
157
176
|
if (concentration < 0.6)
|
|
158
177
|
return [];
|
|
159
178
|
return [{
|
|
160
|
-
id:
|
|
179
|
+
id: nextId(),
|
|
161
180
|
title: "Risk concentrated in top quartile of units",
|
|
162
181
|
category: "risk_concentration",
|
|
163
182
|
severity: concentration > 0.8 ? "high" : "medium",
|
|
@@ -171,7 +190,7 @@ function detectRiskConcentration(riskRegister, unitManifest) {
|
|
|
171
190
|
systemic: true,
|
|
172
191
|
}];
|
|
173
192
|
}
|
|
174
|
-
function detectUnitSprawl(unitManifest) {
|
|
193
|
+
function detectUnitSprawl(unitManifest, nextId) {
|
|
175
194
|
if (unitManifest.units.length < 3)
|
|
176
195
|
return [];
|
|
177
196
|
const fileCounts = unitManifest.units.map((u) => u.files.length);
|
|
@@ -181,7 +200,7 @@ function detectUnitSprawl(unitManifest) {
|
|
|
181
200
|
const dominantUnit = unitManifest.units.find((u) => u.files.length === maxFiles);
|
|
182
201
|
if (dominantUnit && maxFiles > totalFiles * 0.5 && totalFiles > 10) {
|
|
183
202
|
findings.push({
|
|
184
|
-
id:
|
|
203
|
+
id: nextId(),
|
|
185
204
|
title: `Dominant unit: ${dominantUnit.unit_id}`,
|
|
186
205
|
category: "monolith_unit",
|
|
187
206
|
severity: "medium",
|
|
@@ -196,7 +215,7 @@ function detectUnitSprawl(unitManifest) {
|
|
|
196
215
|
const smallUnits = unitManifest.units.filter((u) => u.files.length === 1);
|
|
197
216
|
if (smallUnits.length > unitManifest.units.length * 0.6) {
|
|
198
217
|
findings.push({
|
|
199
|
-
id:
|
|
218
|
+
id: nextId(),
|
|
200
219
|
title: "Excessive single-file units",
|
|
201
220
|
category: "unit_fragmentation",
|
|
202
221
|
severity: "low",
|
|
@@ -210,7 +229,7 @@ function detectUnitSprawl(unitManifest) {
|
|
|
210
229
|
}
|
|
211
230
|
return findings;
|
|
212
231
|
}
|
|
213
|
-
function detectFlowGaps(criticalFlows, graphBundle) {
|
|
232
|
+
function detectFlowGaps(criticalFlows, graphBundle, nextId) {
|
|
214
233
|
const edges = allEdges(graphBundle);
|
|
215
234
|
const connected = new Set();
|
|
216
235
|
for (const edge of edges) {
|
|
@@ -223,7 +242,7 @@ function detectFlowGaps(criticalFlows, graphBundle) {
|
|
|
223
242
|
if (disconnected.length > 0 &&
|
|
224
243
|
disconnected.length > flow.paths.length * 0.5) {
|
|
225
244
|
findings.push({
|
|
226
|
-
id:
|
|
245
|
+
id: nextId(),
|
|
227
246
|
title: `Critical flow "${flow.name}" has weak graph coverage`,
|
|
228
247
|
category: "flow_gap",
|
|
229
248
|
severity: "medium",
|
|
@@ -238,14 +257,14 @@ function detectFlowGaps(criticalFlows, graphBundle) {
|
|
|
238
257
|
return findings;
|
|
239
258
|
}
|
|
240
259
|
export function buildDesignAssessment(params) {
|
|
241
|
-
|
|
260
|
+
const nextId = createFindingIdGenerator();
|
|
242
261
|
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),
|
|
262
|
+
...detectCycleFindings(params.graphBundle, nextId),
|
|
263
|
+
...detectHubModules(params.graphBundle, nextId),
|
|
264
|
+
...detectOrphanUnits(params.unitManifest, params.graphBundle, nextId),
|
|
265
|
+
...detectRiskConcentration(params.riskRegister, params.unitManifest, nextId),
|
|
266
|
+
...detectUnitSprawl(params.unitManifest, nextId),
|
|
267
|
+
...detectFlowGaps(params.criticalFlows, params.graphBundle, nextId),
|
|
249
268
|
];
|
|
250
269
|
return {
|
|
251
270
|
generated_at: new Date().toISOString(),
|
package/dist/extractors/graph.js
CHANGED
|
@@ -48,6 +48,15 @@ const CONFTEST_LINK_CONFIDENCE = 0.85;
|
|
|
48
48
|
const ANALYZER_OWNERSHIP_EDGE_CONFIDENCE = 0.84;
|
|
49
49
|
const CONTAINER_EDGE_CONFIDENCE = 0.25;
|
|
50
50
|
const AUTH_SESSION_EDGE_CONFIDENCE = 0.55;
|
|
51
|
+
/** Named graph edge-kind keys (was a scatter of inline string literals). */
|
|
52
|
+
const EDGE_KIND = {
|
|
53
|
+
heuristicContainer: "heuristic-container-edge",
|
|
54
|
+
heuristicAuthSession: "heuristic-auth-session-link",
|
|
55
|
+
conftestLink: "conftest-link",
|
|
56
|
+
analyzerOwnershipRootLink: "analyzer-ownership-root-link",
|
|
57
|
+
relativeStringReference: "relative-string-reference",
|
|
58
|
+
repoPathReference: "repo-path-reference",
|
|
59
|
+
};
|
|
51
60
|
function shouldReadForGraph(file) {
|
|
52
61
|
const normalized = normalizeGraphPath(file.path);
|
|
53
62
|
return (file.size_bytes <= MAX_GRAPH_SOURCE_BYTES &&
|
|
@@ -136,7 +145,7 @@ function extractAnalyzerOwnershipEdges(externalAnalyzerResults, pathLookup) {
|
|
|
136
145
|
edges.push(graphEdge({
|
|
137
146
|
from: root,
|
|
138
147
|
to: target,
|
|
139
|
-
kind:
|
|
148
|
+
kind: EDGE_KIND.analyzerOwnershipRootLink,
|
|
140
149
|
direction: "undirected",
|
|
141
150
|
confidence,
|
|
142
151
|
reason: providedReason ??
|
|
@@ -212,8 +221,8 @@ function extractReferenceEdges(fromPath, content, pathLookup) {
|
|
|
212
221
|
from: fromPath,
|
|
213
222
|
to: target,
|
|
214
223
|
kind: relativeReference
|
|
215
|
-
?
|
|
216
|
-
:
|
|
224
|
+
? EDGE_KIND.relativeStringReference
|
|
225
|
+
: EDGE_KIND.repoPathReference,
|
|
217
226
|
direction: "directed",
|
|
218
227
|
confidence: relativeReference
|
|
219
228
|
? RELATIVE_REFERENCE_EDGE_CONFIDENCE
|
|
@@ -250,7 +259,7 @@ function extractPytestConftestLinks(pathLookup) {
|
|
|
250
259
|
edges.push(graphEdge({
|
|
251
260
|
from: conftestPath,
|
|
252
261
|
to: targetPath,
|
|
253
|
-
kind:
|
|
262
|
+
kind: EDGE_KIND.conftestLink,
|
|
254
263
|
confidence: CONFTEST_LINK_CONFIDENCE,
|
|
255
264
|
reason: `Pytest conftest '${conftestPath}' applies to all Python files in its scope directory.`,
|
|
256
265
|
}));
|
|
@@ -293,11 +302,118 @@ export async function buildGraphBundleFromFs(repoManifest, root, disposition, op
|
|
|
293
302
|
}
|
|
294
303
|
return buildGraphBundle(repoManifest, disposition, { ...options, fileContents });
|
|
295
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Heuristic "container" edge: a file two-or-more directories deep is linked to
|
|
307
|
+
* its top-two-segment module root, suggesting shared module ownership.
|
|
308
|
+
*/
|
|
309
|
+
function extractHeuristicContainerEdges(filePath) {
|
|
310
|
+
const parts = filePath.split("/");
|
|
311
|
+
if (parts.length <= 2)
|
|
312
|
+
return [];
|
|
313
|
+
return [
|
|
314
|
+
graphEdge({
|
|
315
|
+
from: filePath,
|
|
316
|
+
to: `${parts[0]}/${parts[1]}`,
|
|
317
|
+
kind: EDGE_KIND.heuristicContainer,
|
|
318
|
+
direction: "undirected",
|
|
319
|
+
confidence: CONTAINER_EDGE_CONFIDENCE,
|
|
320
|
+
reason: "Path hierarchy suggests shared module ownership.",
|
|
321
|
+
}),
|
|
322
|
+
];
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Heuristic security edge: an auth-named (non-session) file is linked to every
|
|
326
|
+
* session-named file by naming convention, flagging likely auth↔session coupling.
|
|
327
|
+
*/
|
|
328
|
+
function extractHeuristicAuthSessionEdges(filePath, repoManifest, dispositionMap) {
|
|
329
|
+
const normalized = filePath.toLowerCase();
|
|
330
|
+
if (!(normalized.includes("auth") && normalized.includes("session") === false)) {
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
const edges = [];
|
|
334
|
+
for (const other of repoManifest.files) {
|
|
335
|
+
if (other.path === filePath)
|
|
336
|
+
continue;
|
|
337
|
+
const otherStatus = dispositionMap.get(other.path);
|
|
338
|
+
if (otherStatus && isAuditExcludedStatus(otherStatus))
|
|
339
|
+
continue;
|
|
340
|
+
if (other.path.toLowerCase().includes("session")) {
|
|
341
|
+
edges.push(graphEdge({
|
|
342
|
+
from: filePath,
|
|
343
|
+
to: other.path,
|
|
344
|
+
kind: EDGE_KIND.heuristicAuthSession,
|
|
345
|
+
confidence: AUTH_SESSION_EDGE_CONFIDENCE,
|
|
346
|
+
reason: "Security-sensitive auth path appears coupled to a session path by naming convention.",
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return edges;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Run every content-driven edge extractor over one file's source, appending its
|
|
354
|
+
* import / call / reference / route edges into the accumulator. Mirrors the
|
|
355
|
+
* original inline body exactly (including push order) so the deduped/sorted
|
|
356
|
+
* result is byte-identical.
|
|
357
|
+
*/
|
|
358
|
+
function extractContentEdgesForFile(filePath, content, pathLookup, acc, fileRoutes) {
|
|
359
|
+
acc.imports.push(...extractImportEdges(filePath, content, pathLookup));
|
|
360
|
+
acc.imports.push(...extractPythonImportEdges(filePath, content, pathLookup));
|
|
361
|
+
acc.references.push(...extractReferenceEdges(filePath, content, pathLookup));
|
|
362
|
+
acc.references.push(...extractJsonSchemaReferenceEdges(filePath, content, pathLookup));
|
|
363
|
+
acc.references.push(...extractPackageEntrypointEdges(filePath, content, pathLookup));
|
|
364
|
+
acc.references.push(...extractChromeExtensionManifestEdges(filePath, content, pathLookup));
|
|
365
|
+
acc.references.push(...extractHtmlResourceEdges(filePath, content, pathLookup));
|
|
366
|
+
acc.references.push(...extractPackageScriptEdges(filePath, content, pathLookup));
|
|
367
|
+
acc.references.push(...extractWorkspacePackageEdges(filePath, content, pathLookup));
|
|
368
|
+
acc.references.push(...extractTypescriptProjectReferenceEdges(filePath, content, pathLookup));
|
|
369
|
+
acc.references.push(...extractGoWorkspaceModuleEdges(filePath, content, pathLookup));
|
|
370
|
+
acc.references.push(...extractCargoWorkspaceMemberEdges(filePath, content, pathLookup));
|
|
371
|
+
acc.references.push(...extractMavenModuleEdges(filePath, content, pathLookup));
|
|
372
|
+
acc.references.push(...extractPyprojectTestpathLinks(filePath, content, pathLookup));
|
|
373
|
+
acc.references.push(...extractYamlPathReferenceEdges(filePath, content, pathLookup));
|
|
374
|
+
acc.references.push(...extractSchemaContractTestEdges(filePath, content, pathLookup));
|
|
375
|
+
const registeredRoutes = extractRegisteredRouteEvidence(filePath, content, pathLookup);
|
|
376
|
+
acc.calls.push(...registeredRoutes.calls);
|
|
377
|
+
fileRoutes.push(...registeredRoutes.routes);
|
|
378
|
+
const frameworkRoutes = extractFrameworkRouteEvidence(filePath, content, pathLookup);
|
|
379
|
+
acc.calls.push(...frameworkRoutes.calls);
|
|
380
|
+
fileRoutes.push(...frameworkRoutes.routes);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Emit the OBS-003 graph-extraction metric. No RunLogger is in scope in this
|
|
384
|
+
* leaf extractor, so it uses the established structured-stderr summary
|
|
385
|
+
* (FINDING-012 pattern): node count, total edge count, and the number of
|
|
386
|
+
* non-empty graph types.
|
|
387
|
+
*/
|
|
388
|
+
function logGraphExtractionMetric(graphs) {
|
|
389
|
+
const edgeCount = graphs.imports.length +
|
|
390
|
+
graphs.calls.length +
|
|
391
|
+
graphs.references.length +
|
|
392
|
+
graphs.routes.length;
|
|
393
|
+
const nodes = new Set();
|
|
394
|
+
for (const edge of [...graphs.imports, ...graphs.calls, ...graphs.references]) {
|
|
395
|
+
nodes.add(edge.from);
|
|
396
|
+
nodes.add(edge.to);
|
|
397
|
+
}
|
|
398
|
+
for (const route of graphs.routes) {
|
|
399
|
+
nodes.add(route.path);
|
|
400
|
+
nodes.add(route.handler);
|
|
401
|
+
}
|
|
402
|
+
const graphTypeCount = [
|
|
403
|
+
graphs.imports,
|
|
404
|
+
graphs.calls,
|
|
405
|
+
graphs.references,
|
|
406
|
+
graphs.routes,
|
|
407
|
+
].filter((edges) => edges.length > 0).length;
|
|
408
|
+
process.stderr.write(`[audit-code] graph: built bundle — ${nodes.size} nodes, ${edgeCount} edges across ${graphTypeCount} graph type(s)\n`);
|
|
409
|
+
}
|
|
296
410
|
export function buildGraphBundle(repoManifest, disposition, options = {}) {
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
411
|
+
const acc = {
|
|
412
|
+
imports: [],
|
|
413
|
+
calls: [],
|
|
414
|
+
references: [],
|
|
415
|
+
routes: [],
|
|
416
|
+
};
|
|
301
417
|
const dispositionMap = buildDispositionMap(disposition);
|
|
302
418
|
const pathLookup = buildPathLookup(repoManifest, dispositionMap);
|
|
303
419
|
for (const file of repoManifest.files) {
|
|
@@ -305,62 +421,12 @@ export function buildGraphBundle(repoManifest, disposition, options = {}) {
|
|
|
305
421
|
if (file.excluded || (status && isAuditExcludedStatus(status))) {
|
|
306
422
|
continue;
|
|
307
423
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
imports.push(graphEdge({
|
|
311
|
-
from: file.path,
|
|
312
|
-
to: `${parts[0]}/${parts[1]}`,
|
|
313
|
-
kind: "heuristic-container-edge",
|
|
314
|
-
direction: "undirected",
|
|
315
|
-
confidence: CONTAINER_EDGE_CONFIDENCE,
|
|
316
|
-
reason: "Path hierarchy suggests shared module ownership.",
|
|
317
|
-
}));
|
|
318
|
-
}
|
|
319
|
-
const normalized = file.path.toLowerCase();
|
|
320
|
-
if (normalized.includes("auth") &&
|
|
321
|
-
normalized.includes("session") === false) {
|
|
322
|
-
for (const other of repoManifest.files) {
|
|
323
|
-
if (other.path === file.path)
|
|
324
|
-
continue;
|
|
325
|
-
const otherStatus = dispositionMap.get(other.path);
|
|
326
|
-
if (otherStatus && isAuditExcludedStatus(otherStatus))
|
|
327
|
-
continue;
|
|
328
|
-
if (other.path.toLowerCase().includes("session")) {
|
|
329
|
-
imports.push(graphEdge({
|
|
330
|
-
from: file.path,
|
|
331
|
-
to: other.path,
|
|
332
|
-
kind: "heuristic-auth-session-link",
|
|
333
|
-
confidence: AUTH_SESSION_EDGE_CONFIDENCE,
|
|
334
|
-
reason: "Security-sensitive auth path appears coupled to a session path by naming convention.",
|
|
335
|
-
}));
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
424
|
+
acc.imports.push(...extractHeuristicContainerEdges(file.path));
|
|
425
|
+
acc.imports.push(...extractHeuristicAuthSessionEdges(file.path, repoManifest, dispositionMap));
|
|
339
426
|
const content = options.fileContents?.[file.path];
|
|
340
427
|
const fileRoutes = [];
|
|
341
428
|
if (content) {
|
|
342
|
-
|
|
343
|
-
imports.push(...extractPythonImportEdges(file.path, content, pathLookup));
|
|
344
|
-
references.push(...extractReferenceEdges(file.path, content, pathLookup));
|
|
345
|
-
references.push(...extractJsonSchemaReferenceEdges(file.path, content, pathLookup));
|
|
346
|
-
references.push(...extractPackageEntrypointEdges(file.path, content, pathLookup));
|
|
347
|
-
references.push(...extractChromeExtensionManifestEdges(file.path, content, pathLookup));
|
|
348
|
-
references.push(...extractHtmlResourceEdges(file.path, content, pathLookup));
|
|
349
|
-
references.push(...extractPackageScriptEdges(file.path, content, pathLookup));
|
|
350
|
-
references.push(...extractWorkspacePackageEdges(file.path, content, pathLookup));
|
|
351
|
-
references.push(...extractTypescriptProjectReferenceEdges(file.path, content, pathLookup));
|
|
352
|
-
references.push(...extractGoWorkspaceModuleEdges(file.path, content, pathLookup));
|
|
353
|
-
references.push(...extractCargoWorkspaceMemberEdges(file.path, content, pathLookup));
|
|
354
|
-
references.push(...extractMavenModuleEdges(file.path, content, pathLookup));
|
|
355
|
-
references.push(...extractPyprojectTestpathLinks(file.path, content, pathLookup));
|
|
356
|
-
references.push(...extractYamlPathReferenceEdges(file.path, content, pathLookup));
|
|
357
|
-
references.push(...extractSchemaContractTestEdges(file.path, content, pathLookup));
|
|
358
|
-
const registeredRoutes = extractRegisteredRouteEvidence(file.path, content, pathLookup);
|
|
359
|
-
calls.push(...registeredRoutes.calls);
|
|
360
|
-
fileRoutes.push(...registeredRoutes.routes);
|
|
361
|
-
const frameworkRoutes = extractFrameworkRouteEvidence(file.path, content, pathLookup);
|
|
362
|
-
calls.push(...frameworkRoutes.calls);
|
|
363
|
-
fileRoutes.push(...frameworkRoutes.routes);
|
|
429
|
+
extractContentEdgesForFile(file.path, content, pathLookup, acc, fileRoutes);
|
|
364
430
|
}
|
|
365
431
|
fileRoutes.push(...extractConventionalRouteEvidence(file.path, content));
|
|
366
432
|
if (fileRoutes.length === 0) {
|
|
@@ -369,18 +435,18 @@ export function buildGraphBundle(repoManifest, disposition, options = {}) {
|
|
|
369
435
|
fileRoutes.push(fallbackRoute);
|
|
370
436
|
}
|
|
371
437
|
}
|
|
372
|
-
routes.push(...fileRoutes);
|
|
373
|
-
references.push(...extractTestSourceEdges(file.path, pathLookup));
|
|
438
|
+
acc.routes.push(...fileRoutes);
|
|
439
|
+
acc.references.push(...extractTestSourceEdges(file.path, pathLookup));
|
|
374
440
|
}
|
|
375
|
-
references.push(...extractAnalyzerOwnershipEdges(options.externalAnalyzerResults, pathLookup));
|
|
376
|
-
references.push(...extractPytestConftestLinks(pathLookup));
|
|
377
|
-
references.push(...extractBoundedSuiteEdges(pathLookup, options.fileContents ?? {}, references));
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
routes: uniqueSortedRoutes(routes),
|
|
384
|
-
},
|
|
441
|
+
acc.references.push(...extractAnalyzerOwnershipEdges(options.externalAnalyzerResults, pathLookup));
|
|
442
|
+
acc.references.push(...extractPytestConftestLinks(pathLookup));
|
|
443
|
+
acc.references.push(...extractBoundedSuiteEdges(pathLookup, options.fileContents ?? {}, acc.references));
|
|
444
|
+
const graphs = {
|
|
445
|
+
imports: uniqueSortedEdges(acc.imports),
|
|
446
|
+
calls: uniqueSortedEdges(acc.calls),
|
|
447
|
+
references: uniqueSortedEdges(acc.references),
|
|
448
|
+
routes: uniqueSortedRoutes(acc.routes),
|
|
385
449
|
};
|
|
450
|
+
logGraphExtractionMetric(graphs);
|
|
451
|
+
return { graphs };
|
|
386
452
|
}
|
|
@@ -136,6 +136,15 @@ function splitSegments(normalized) {
|
|
|
136
136
|
function hasSegment(normalized, segment) {
|
|
137
137
|
return splitSegments(normalized).includes(segment);
|
|
138
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* True when any of `segments` appears as a path segment. Splits the path once
|
|
141
|
+
* and tests all candidates against that single set, instead of re-splitting per
|
|
142
|
+
* segment as repeated `hasSegment` calls would.
|
|
143
|
+
*/
|
|
144
|
+
function hasAnySegment(normalized, segments) {
|
|
145
|
+
const present = new Set(splitSegments(normalized));
|
|
146
|
+
return segments.some((segment) => present.has(segment));
|
|
147
|
+
}
|
|
139
148
|
function includesAny(normalized, values) {
|
|
140
149
|
return values.some((value) => normalized.includes(value));
|
|
141
150
|
}
|
|
@@ -208,13 +217,16 @@ export function isTestPath(normalized) {
|
|
|
208
217
|
export function isInterfacePath(normalized) {
|
|
209
218
|
return hasToken(normalized, INTERFACE_KEYWORDS) || hasSegment(normalized, "api");
|
|
210
219
|
}
|
|
220
|
+
const DATA_LAYER_SEGMENTS = [
|
|
221
|
+
"models",
|
|
222
|
+
"schemas",
|
|
223
|
+
"migrations",
|
|
224
|
+
"seeds",
|
|
225
|
+
"db",
|
|
226
|
+
];
|
|
211
227
|
export function isDataLayerPath(normalized) {
|
|
212
228
|
return (hasToken(normalized, DATA_LAYER_KEYWORDS) ||
|
|
213
|
-
|
|
214
|
-
hasSegment(normalized, "schemas") ||
|
|
215
|
-
hasSegment(normalized, "migrations") ||
|
|
216
|
-
hasSegment(normalized, "seeds") ||
|
|
217
|
-
hasSegment(normalized, "db"));
|
|
229
|
+
hasAnySegment(normalized, DATA_LAYER_SEGMENTS));
|
|
218
230
|
}
|
|
219
231
|
export function isSecuritySensitivePath(normalized) {
|
|
220
232
|
return hasToken(normalized, SECURITY_KEYWORDS);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface RunPaths {
|
|
2
|
+
runDir: string;
|
|
3
|
+
taskPath: string;
|
|
4
|
+
promptPath: string;
|
|
5
|
+
resultPath: string;
|
|
6
|
+
stdoutPath: string;
|
|
7
|
+
stderrPath: string;
|
|
8
|
+
statusPath: string;
|
|
9
|
+
}
|
|
10
|
+
export interface DispatchBatchRun {
|
|
11
|
+
run_id: string;
|
|
12
|
+
task_path: string;
|
|
13
|
+
prompt_path: string;
|
|
14
|
+
result_path: string;
|
|
15
|
+
status_path: string;
|
|
16
|
+
audit_results_path?: string;
|
|
17
|
+
pending_audit_tasks_path?: string;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,23 +1,7 @@
|
|
|
1
1
|
import type { AuditTask } from "../types.js";
|
|
2
2
|
import type { WorkerTask } from "../types/workerSession.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
taskPath: string;
|
|
6
|
-
promptPath: string;
|
|
7
|
-
resultPath: string;
|
|
8
|
-
stdoutPath: string;
|
|
9
|
-
stderrPath: string;
|
|
10
|
-
statusPath: string;
|
|
11
|
-
}
|
|
12
|
-
export interface DispatchBatchRun {
|
|
13
|
-
run_id: string;
|
|
14
|
-
task_path: string;
|
|
15
|
-
prompt_path: string;
|
|
16
|
-
result_path: string;
|
|
17
|
-
status_path: string;
|
|
18
|
-
audit_results_path?: string;
|
|
19
|
-
pending_audit_tasks_path?: string;
|
|
20
|
-
}
|
|
3
|
+
import type { RunPaths, DispatchBatchRun } from "./runArtifactTypes.js";
|
|
4
|
+
export type { RunPaths, DispatchBatchRun } from "./runArtifactTypes.js";
|
|
21
5
|
export declare function buildRunId(obligationId: string | null, index: number, now?: Date): string;
|
|
22
6
|
export declare function getRunPaths(artifactsDir: string, runId: string): RunPaths;
|
|
23
7
|
export declare function ensureSupervisorDirs(artifactsDir: string): Promise<void>;
|