auditor-lambda 0.8.0 → 0.9.1

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 (98) hide show
  1. package/audit-code-wrapper-lib.mjs +149 -129
  2. package/dist/adapters/normalizeExternal.js +6 -3
  3. package/dist/cli/args.d.ts +0 -1
  4. package/dist/cli/args.js +0 -6
  5. package/dist/cli/dispatch.js +3 -2
  6. package/dist/cli/lineIndex.js +4 -1
  7. package/dist/cli/mergeAndIngestCommand.d.ts +1 -0
  8. package/dist/cli/mergeAndIngestCommand.js +219 -0
  9. package/dist/cli/nextStepCommand.js +5 -1
  10. package/dist/cli/runToCompletion.d.ts +9 -0
  11. package/dist/cli/runToCompletion.js +655 -480
  12. package/dist/cli/statusCommand.d.ts +1 -0
  13. package/dist/cli/statusCommand.js +113 -0
  14. package/dist/cli/submitPacketCommand.d.ts +1 -0
  15. package/dist/cli/submitPacketCommand.js +155 -0
  16. package/dist/cli/workerResult.d.ts +1 -1
  17. package/dist/cli/workerRunCommand.d.ts +1 -0
  18. package/dist/cli/workerRunCommand.js +88 -0
  19. package/dist/cli.js +14 -563
  20. package/dist/extractors/analyzers/sql.js +4 -1
  21. package/dist/extractors/analyzers/treeSitter.js +29 -15
  22. package/dist/extractors/analyzers/typescript.js +10 -8
  23. package/dist/extractors/designAssessment.js +43 -24
  24. package/dist/extractors/graph.js +139 -73
  25. package/dist/extractors/pathPatterns.js +17 -5
  26. package/dist/io/runArtifactTypes.d.ts +18 -0
  27. package/dist/io/runArtifactTypes.js +1 -0
  28. package/dist/io/runArtifacts.d.ts +2 -18
  29. package/dist/io/runArtifacts.js +14 -3
  30. package/dist/mcp/server.js +9 -0
  31. package/dist/orchestrator/advance.js +37 -22
  32. package/dist/orchestrator/artifactFreshness.js +2 -2
  33. package/dist/orchestrator/autoFixExecutor.d.ts +1 -1
  34. package/dist/orchestrator/autoFixExecutor.js +16 -8
  35. package/dist/orchestrator/dependencyMap.d.ts +1 -1
  36. package/dist/orchestrator/dependencyMap.js +7 -1
  37. package/dist/orchestrator/fileAnchors.js +14 -3
  38. package/dist/orchestrator/flowCoverage.js +1 -0
  39. package/dist/orchestrator/flowRequeue.js +4 -1
  40. package/dist/orchestrator/{internalExecutors.d.ts → ingestionExecutors.d.ts} +0 -6
  41. package/dist/orchestrator/ingestionExecutors.js +237 -0
  42. package/dist/orchestrator/intakeExecutors.d.ts +3 -0
  43. package/dist/orchestrator/intakeExecutors.js +25 -0
  44. package/dist/orchestrator/planningExecutors.d.ts +4 -0
  45. package/dist/orchestrator/planningExecutors.js +95 -0
  46. package/dist/orchestrator/runtimeCommand.js +7 -15
  47. package/dist/orchestrator/selectiveDeepening/conflict.d.ts +8 -0
  48. package/dist/orchestrator/selectiveDeepening/conflict.js +71 -0
  49. package/dist/orchestrator/selectiveDeepening/findingFollowup.d.ts +10 -0
  50. package/dist/orchestrator/selectiveDeepening/findingFollowup.js +52 -0
  51. package/dist/orchestrator/selectiveDeepening/highRiskClean.d.ts +7 -0
  52. package/dist/orchestrator/selectiveDeepening/highRiskClean.js +44 -0
  53. package/dist/orchestrator/selectiveDeepening/index.d.ts +18 -0
  54. package/dist/orchestrator/selectiveDeepening/index.js +128 -0
  55. package/dist/orchestrator/selectiveDeepening/lensVerification.d.ts +12 -0
  56. package/dist/orchestrator/selectiveDeepening/lensVerification.js +242 -0
  57. package/dist/orchestrator/selectiveDeepening/runtimeValidation.d.ts +13 -0
  58. package/dist/orchestrator/selectiveDeepening/runtimeValidation.js +57 -0
  59. package/dist/orchestrator/selectiveDeepening/shared.d.ts +45 -0
  60. package/dist/orchestrator/selectiveDeepening/shared.js +128 -0
  61. package/dist/orchestrator/selectiveDeepening/stewardFollowup.d.ts +6 -0
  62. package/dist/orchestrator/selectiveDeepening/stewardFollowup.js +72 -0
  63. package/dist/orchestrator/selectiveDeepening.d.ts +2 -20
  64. package/dist/orchestrator/selectiveDeepening.js +6 -760
  65. package/dist/orchestrator/staleness.js +3 -3
  66. package/dist/orchestrator/structureExecutors.d.ts +5 -0
  67. package/dist/orchestrator/structureExecutors.js +94 -0
  68. package/dist/orchestrator/taskBuilder.d.ts +2 -2
  69. package/dist/orchestrator/taskBuilder.js +101 -82
  70. package/dist/providers/index.d.ts +7 -0
  71. package/dist/providers/index.js +14 -95
  72. package/dist/quota/discoveredLimits.d.ts +1 -0
  73. package/dist/quota/discoveredLimits.js +7 -1
  74. package/dist/quota/index.d.ts +0 -2
  75. package/dist/quota/index.js +1 -2
  76. package/dist/reporting/workBlocks.js +7 -4
  77. package/dist/types/reviewPlanning.d.ts +23 -16
  78. package/dist/validation/auditResults.js +97 -95
  79. package/dist/validation/sessionConfig.d.ts +2 -2
  80. package/dist/validation/sessionConfig.js +14 -7
  81. package/package.json +4 -3
  82. package/schemas/audit_findings.schema.json +3 -3
  83. package/schemas/critical_flows.schema.json +3 -2
  84. package/schemas/dispatch_quota.schema.json +1 -1
  85. package/schemas/graph_bundle.schema.json +1 -1
  86. package/schemas/review_packets.schema.json +1 -1
  87. package/schemas/step_contract.schema.json +80 -0
  88. package/scripts/postinstall.mjs +19 -2
  89. package/skills/audit-code/opencode-command-template.txt +3 -3
  90. package/dist/orchestrator/internalExecutors.js +0 -424
  91. package/dist/providers/localSubprocessProvider.d.ts +0 -9
  92. package/dist/providers/localSubprocessProvider.js +0 -18
  93. package/dist/providers/subprocessTemplateProvider.d.ts +0 -8
  94. package/dist/providers/subprocessTemplateProvider.js +0 -59
  95. package/dist/providers/vscodeTaskProvider.d.ts +0 -7
  96. package/dist/providers/vscodeTaskProvider.js +0 -14
  97. package/dist/quota/probe.d.ts +0 -10
  98. 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
- let modulePromise;
6
- let initPromise;
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
- // Try the next specifier.
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
- if (!modulePromise) {
37
- modulePromise = importParserModule(dependencyPath);
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 modulePromise;
46
+ return cached;
40
47
  }
41
48
  async function ensureInit(parserModule) {
42
- if (!initPromise) {
43
- initPromise = parserModule.Parser.init()
49
+ let cached = initCache.get(parserModule);
50
+ if (!cached) {
51
+ cached = parserModule.Parser.init()
44
52
  .then(() => true)
45
- .catch(() => false);
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 initPromise;
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
- modulePromise = undefined;
109
- initPromise = undefined;
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
- // Fall through to the bundled compiler.
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
- // Keep the un-aliased symbol on failure.
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
- // Any compiler failure degrades cleanly to the regex floor.
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
- let nextFindingId = 1;
2
- function findingId() {
3
- return `DA-${String(nextFindingId++).padStart(3, "0")}`;
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 = [...cycle].sort().join("\0");
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: findingId(),
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: findingId(),
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: findingId(),
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: findingId(),
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: findingId(),
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: findingId(),
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: findingId(),
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
- nextFindingId = 1;
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(),
@@ -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: "analyzer-ownership-root-link",
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
- ? "relative-string-reference"
216
- : "repo-path-reference",
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: "conftest-link",
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 imports = [];
298
- const calls = [];
299
- const references = [];
300
- const routes = [];
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
- const parts = file.path.split("/");
309
- if (parts.length > 2) {
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
- imports.push(...extractImportEdges(file.path, content, pathLookup));
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
- return {
379
- graphs: {
380
- imports: uniqueSortedEdges(imports),
381
- calls: uniqueSortedEdges(calls),
382
- references: uniqueSortedEdges(references),
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
- hasSegment(normalized, "models") ||
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
- export interface RunPaths {
4
- runDir: string;
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>;