auditor-lambda 0.3.12 → 0.3.14

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 (61) hide show
  1. package/README.md +20 -24
  2. package/audit-code-wrapper-lib.mjs +52 -53
  3. package/dist/cli.js +43 -6
  4. package/dist/coverage.js +3 -1
  5. package/dist/extractors/disposition.js +8 -1
  6. package/dist/extractors/graph.d.ts +3 -1
  7. package/dist/extractors/graph.js +1147 -67
  8. package/dist/extractors/graphManifestEdges.d.ts +14 -0
  9. package/dist/extractors/graphManifestEdges.js +1158 -0
  10. package/dist/extractors/graphPathUtils.d.ts +5 -0
  11. package/dist/extractors/graphPathUtils.js +75 -0
  12. package/dist/extractors/pathPatterns.d.ts +1 -0
  13. package/dist/extractors/pathPatterns.js +3 -0
  14. package/dist/io/artifacts.d.ts +10 -1
  15. package/dist/io/artifacts.js +23 -3
  16. package/dist/orchestrator/internalExecutors.d.ts +4 -0
  17. package/dist/orchestrator/internalExecutors.js +35 -6
  18. package/dist/orchestrator/reviewPackets.js +1003 -31
  19. package/dist/orchestrator/syntaxResolutionExecutor.js +34 -0
  20. package/dist/types/externalAnalyzer.d.ts +9 -0
  21. package/dist/types/graph.d.ts +3 -0
  22. package/dist/types/reviewPlanning.d.ts +39 -0
  23. package/docs/contracts.md +215 -0
  24. package/docs/development.md +210 -0
  25. package/docs/handoff.md +204 -0
  26. package/docs/history.md +40 -0
  27. package/docs/operator-guide.md +189 -0
  28. package/docs/product.md +185 -0
  29. package/docs/release.md +131 -0
  30. package/package.json +1 -1
  31. package/schemas/audit_plan_metrics.schema.json +347 -0
  32. package/schemas/external_analyzer_results.schema.json +35 -0
  33. package/schemas/graph_bundle.schema.json +47 -2
  34. package/schemas/review_packets.schema.json +160 -0
  35. package/skills/audit-code/SKILL.md +7 -3
  36. package/skills/audit-code/audit-code.prompt.md +4 -1
  37. package/docs/agent-integrations.md +0 -317
  38. package/docs/agent-roles.md +0 -69
  39. package/docs/architecture.md +0 -90
  40. package/docs/artifacts.md +0 -36
  41. package/docs/bootstrap-install.md +0 -139
  42. package/docs/contract.md +0 -54
  43. package/docs/dispatch-implementation-plan.md +0 -302
  44. package/docs/field-trial-bug-report.md +0 -237
  45. package/docs/github-copilot.md +0 -66
  46. package/docs/model-selection.md +0 -97
  47. package/docs/next-steps.md +0 -202
  48. package/docs/packaging.md +0 -120
  49. package/docs/pipeline.md +0 -152
  50. package/docs/product-direction.md +0 -154
  51. package/docs/production-launch-bar.md +0 -92
  52. package/docs/production-readiness.md +0 -58
  53. package/docs/releasing.md +0 -145
  54. package/docs/remediation-baseline.md +0 -75
  55. package/docs/repo-layout.md +0 -30
  56. package/docs/run-flow.md +0 -56
  57. package/docs/session-config.md +0 -319
  58. package/docs/supervisor.md +0 -100
  59. package/docs/usage.md +0 -215
  60. package/docs/windows-setup.md +0 -146
  61. package/docs/workflow-refactor-brief.md +0 -124
@@ -2,30 +2,10 @@ import { readFile } from "node:fs/promises";
2
2
  import { isAbsolute, relative, resolve } from "node:path";
3
3
  import { posix } from "node:path";
4
4
  import { isAuditExcludedStatus } from "./disposition.js";
5
+ import { extractCargoWorkspaceMemberEdges, extractGoWorkspaceModuleEdges, extractMavenModuleEdges, extractPackageEntrypointEdges, extractPackageScriptEdges, extractPyprojectTestpathLinks, extractTypescriptProjectReferenceEdges, extractWorkspacePackageEdges, extractYamlPathReferenceEdges, isCargoManifestPath, isGoWorkspaceManifestPath, isMavenPomPath, isPyprojectPath, } from "./graphManifestEdges.js";
6
+ import { graphEdge, graphLookupKey, normalizeGraphPath, resolveCandidate, } from "./graphPathUtils.js";
7
+ import { isTestPath, normalizeExtractorPath } from "./pathPatterns.js";
5
8
  const MAX_GRAPH_SOURCE_BYTES = 512 * 1024;
6
- const RESOLVABLE_EXTENSIONS = [
7
- "",
8
- ".ts",
9
- ".tsx",
10
- ".mts",
11
- ".cts",
12
- ".js",
13
- ".jsx",
14
- ".mjs",
15
- ".cjs",
16
- ".json",
17
- ];
18
- const INDEX_EXTENSIONS = [
19
- "index.ts",
20
- "index.tsx",
21
- "index.mts",
22
- "index.cts",
23
- "index.js",
24
- "index.jsx",
25
- "index.mjs",
26
- "index.cjs",
27
- "index.json",
28
- ];
29
9
  const SOURCE_LANGUAGES = new Set([
30
10
  "typescript",
31
11
  "javascript",
@@ -50,11 +30,30 @@ const SOURCE_EXTENSIONS = [
50
30
  ".yml",
51
31
  ".yaml",
52
32
  ".py",
33
+ ".pyi",
53
34
  ".go",
54
35
  ".rs",
55
36
  ".java",
56
37
  ".cs",
57
38
  ];
39
+ const PYTHON_SOURCE_EXTENSIONS = [".py", ".pyi"];
40
+ const PYTHON_PACKAGE_INDEX_FILES = ["__init__.py", "__init__.pyi"];
41
+ const TYPESCRIPT_TYPE_CONTRACT_EXTENSIONS = [
42
+ ".ts",
43
+ ".tsx",
44
+ ".mts",
45
+ ".cts",
46
+ ];
47
+ const PACKAGE_SCRIPT_SUITE_EXTENSIONS = [
48
+ ".js",
49
+ ".jsx",
50
+ ".mjs",
51
+ ".cjs",
52
+ ".ts",
53
+ ".tsx",
54
+ ".mts",
55
+ ".cts",
56
+ ];
58
57
  const IMPORT_PATTERNS = [
59
58
  {
60
59
  pattern: /\bimport\s+(?:type\s+)?(?:[^"'()]*?\s+from\s+)?["']([^"']+)["']/g,
@@ -74,19 +73,63 @@ const IMPORT_PATTERNS = [
74
73
  },
75
74
  ];
76
75
  const STRING_LITERAL_PATTERN = /["'`]([^"'`\r\n]{1,260})["'`]/g;
77
- function normalizeGraphPath(path) {
78
- return posix
79
- .normalize(path.replace(/\\/g, "/"))
80
- .replace(/^\.\//, "");
81
- }
82
- function graphLookupKey(path) {
83
- return normalizeGraphPath(path).toLowerCase();
84
- }
76
+ const IMPORT_EDGE_CONFIDENCE = 0.95;
77
+ const REFERENCE_EDGE_CONFIDENCE = 0.72;
78
+ const RELATIVE_REFERENCE_EDGE_CONFIDENCE = 0.82;
79
+ const TEST_SOURCE_EDGE_CONFIDENCE = 0.88;
80
+ const CONFTEST_LINK_CONFIDENCE = 0.85;
81
+ const ANALYZER_OWNERSHIP_EDGE_CONFIDENCE = 0.84;
82
+ const JSON_SCHEMA_REF_EDGE_CONFIDENCE = 0.93;
83
+ const SCHEMA_CONTRACT_TEST_EDGE_CONFIDENCE = 0.86;
84
+ const SCHEMA_SUITE_EDGE_CONFIDENCE = 0.78;
85
+ const GITHUB_WORKFLOW_SUITE_EDGE_CONFIDENCE = 0.78;
86
+ const PACKAGE_SCRIPT_SUITE_EDGE_CONFIDENCE = 0.78;
87
+ const TYPESCRIPT_TYPE_SUITE_EDGE_CONFIDENCE = 0.78;
88
+ const PYTHON_TEST_UTIL_SUITE_EDGE_CONFIDENCE = 0.72;
89
+ const PYTHON_TEST_UTIL_SEGMENT_NAMES = new Set(["utils", "helpers", "support"]);
90
+ const ROUTE_HANDLER_EDGE_CONFIDENCE = 0.92;
91
+ const CONTAINER_EDGE_CONFIDENCE = 0.25;
92
+ const AUTH_SESSION_EDGE_CONFIDENCE = 0.55;
93
+ const MAX_BOUNDED_SUITE_EDGE_FILES = 12;
94
+ const MAX_BOUNDED_TYPE_SUITE_EDGE_FILES = 16;
95
+ const MAX_TYPE_CONTRACT_SOURCE_BYTES = 64 * 1024;
96
+ const TOP_LEVEL_TEST_SEGMENTS = new Set(["test", "tests", "spec", "specs"]);
97
+ const COLOCATED_TEST_SEGMENTS = new Set([
98
+ "__test__",
99
+ "__tests__",
100
+ "__spec__",
101
+ "__specs__",
102
+ "test",
103
+ "tests",
104
+ "spec",
105
+ "specs",
106
+ ]);
107
+ const ROUTE_REGISTRATION_PATTERN = /\b(?:app|router|server|fastify)\s*\.\s*(get|post|put|patch|delete|del|options|head|all)\s*\(\s*["'`]([^"'`]+)["'`]\s*,\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?)/gi;
108
+ const ROUTE_OBJECT_PATTERN = /\b(?:app|router|server|fastify)\s*\.\s*route\s*\(\s*\{([\s\S]{0,1200}?)\}\s*\)/gi;
109
+ const ROUTE_METHOD_EXPORT_PATTERN = /\bexport\s+(?:async\s+)?(?:function|const)\s+(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\b/g;
110
+ const ROUTE_METHODS = new Set([
111
+ "GET",
112
+ "POST",
113
+ "PUT",
114
+ "PATCH",
115
+ "DELETE",
116
+ "OPTIONS",
117
+ "HEAD",
118
+ "ALL",
119
+ ]);
120
+ const IMPORT_BINDING_PATTERN = /\bimport\s+(?:type\s+)?([^;"'](?:[^;]*?))\s+from\s+["']([^"']+)["']/g;
121
+ const REQUIRE_BINDING_PATTERN = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\s*\(\s*["']([^"']+)["']\s*\)/g;
122
+ const REQUIRE_DESTRUCTURING_PATTERN = /\b(?:const|let|var)\s*\{([^}]+)\}\s*=\s*require\s*\(\s*["']([^"']+)["']\s*\)/g;
123
+ const IDENTIFIER_PATTERN = /^[A-Za-z_$][\w$]*$/;
85
124
  function shouldReadForGraph(file) {
86
125
  const normalized = normalizeGraphPath(file.path);
87
126
  return (file.size_bytes <= MAX_GRAPH_SOURCE_BYTES &&
88
127
  (SOURCE_LANGUAGES.has(file.language) ||
89
- SOURCE_EXTENSIONS.some((extension) => normalized.endsWith(extension))));
128
+ SOURCE_EXTENSIONS.some((extension) => normalized.endsWith(extension)) ||
129
+ isGoWorkspaceManifestPath(normalized) ||
130
+ isCargoManifestPath(normalized) ||
131
+ isMavenPomPath(normalized) ||
132
+ isPyprojectPath(normalized)));
90
133
  }
91
134
  function buildPathLookup(repoManifest, dispositionMap) {
92
135
  return new Map(repoManifest.files
@@ -96,24 +139,6 @@ function buildPathLookup(repoManifest, dispositionMap) {
96
139
  })
97
140
  .map((file) => [graphLookupKey(file.path), file.path]));
98
141
  }
99
- function resolveCandidate(candidate, pathLookup) {
100
- const normalized = normalizeGraphPath(candidate);
101
- const direct = pathLookup.get(normalized.toLowerCase());
102
- if (direct)
103
- return direct;
104
- for (const extension of RESOLVABLE_EXTENSIONS) {
105
- const withExtension = `${normalized}${extension}`;
106
- const match = pathLookup.get(withExtension.toLowerCase());
107
- if (match)
108
- return match;
109
- }
110
- for (const indexFile of INDEX_EXTENSIONS) {
111
- const match = pathLookup.get(posix.join(normalized, indexFile).toLowerCase());
112
- if (match)
113
- return match;
114
- }
115
- return undefined;
116
- }
117
142
  function resolveSpecifier(fromPath, specifier, pathLookup) {
118
143
  if (!specifier.startsWith(".")) {
119
144
  return undefined;
@@ -134,6 +159,11 @@ function resolveReferenceLiteral(fromPath, literal, pathLookup) {
134
159
  function edgeSignature(edge) {
135
160
  return `${edge.from}\0${edge.to}\0${edge.kind ?? ""}`;
136
161
  }
162
+ function clampConfidence(value, fallback) {
163
+ return typeof value === "number" && Number.isFinite(value)
164
+ ? Math.min(1, Math.max(0, value))
165
+ : fallback;
166
+ }
137
167
  function uniqueSortedEdges(edges) {
138
168
  const deduped = new Map();
139
169
  for (const edge of edges) {
@@ -145,6 +175,487 @@ function uniqueSortedEdges(edges) {
145
175
  a.to.localeCompare(b.to) ||
146
176
  (a.kind ?? "").localeCompare(b.kind ?? ""));
147
177
  }
178
+ function normalizeOwnershipRoot(root) {
179
+ const normalized = normalizeGraphPath(root.trim()).replace(/\/+$/, "");
180
+ if (normalized.length === 0 ||
181
+ normalized === "." ||
182
+ normalized === ".." ||
183
+ normalized.startsWith("../") ||
184
+ normalized.startsWith("/") ||
185
+ isAbsolute(normalized)) {
186
+ return undefined;
187
+ }
188
+ return normalized;
189
+ }
190
+ function extractAnalyzerOwnershipEdges(externalAnalyzerResults, pathLookup) {
191
+ const roots = Array.isArray(externalAnalyzerResults?.ownership_roots)
192
+ ? externalAnalyzerResults.ownership_roots
193
+ : [];
194
+ const edges = [];
195
+ for (const rootHint of roots) {
196
+ if (!rootHint ||
197
+ typeof rootHint.root !== "string" ||
198
+ !Array.isArray(rootHint.paths)) {
199
+ continue;
200
+ }
201
+ const root = normalizeOwnershipRoot(rootHint.root);
202
+ if (!root) {
203
+ continue;
204
+ }
205
+ const normalizedRoot = root.toLowerCase();
206
+ const confidence = clampConfidence(rootHint.confidence, ANALYZER_OWNERSHIP_EDGE_CONFIDENCE);
207
+ const ownershipKind = typeof rootHint.kind === "string" && rootHint.kind.trim().length > 0
208
+ ? rootHint.kind.trim()
209
+ : undefined;
210
+ const providedReason = typeof rootHint.reason === "string" && rootHint.reason.trim().length > 0
211
+ ? rootHint.reason.trim()
212
+ : undefined;
213
+ for (const rawPath of rootHint.paths) {
214
+ if (typeof rawPath !== "string" || rawPath.trim().length === 0) {
215
+ continue;
216
+ }
217
+ const target = resolveCandidate(rawPath, pathLookup);
218
+ if (!target) {
219
+ continue;
220
+ }
221
+ const normalizedTarget = normalizeGraphPath(target).toLowerCase();
222
+ if (normalizedTarget !== normalizedRoot &&
223
+ !normalizedTarget.startsWith(`${normalizedRoot}/`)) {
224
+ continue;
225
+ }
226
+ edges.push(graphEdge({
227
+ from: root,
228
+ to: target,
229
+ kind: "analyzer-ownership-root-link",
230
+ direction: "undirected",
231
+ confidence,
232
+ reason: providedReason ??
233
+ (ownershipKind
234
+ ? `${externalAnalyzerResults?.tool ?? "analyzer"} reports ${ownershipKind} ownership root '${root}' contains '${target}'.`
235
+ : `${externalAnalyzerResults?.tool ?? "analyzer"} reports ownership root '${root}' contains '${target}'.`),
236
+ }));
237
+ }
238
+ }
239
+ return edges;
240
+ }
241
+ function routeSignature(route) {
242
+ return `${route.method ?? ""}\0${route.path}\0${route.handler}`;
243
+ }
244
+ function uniqueSortedRoutes(routes) {
245
+ const deduped = new Map();
246
+ for (const route of routes) {
247
+ deduped.set(routeSignature(route), route);
248
+ }
249
+ return [...deduped.values()].sort((a, b) => a.path.localeCompare(b.path) ||
250
+ a.handler.localeCompare(b.handler) ||
251
+ (a.method ?? "").localeCompare(b.method ?? ""));
252
+ }
253
+ function normalizeRoutePath(routePath) {
254
+ const trimmed = routePath.trim();
255
+ if (trimmed === "*" || trimmed === "/*") {
256
+ return trimmed;
257
+ }
258
+ const prefixed = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
259
+ return prefixed.replace(/\/{2,}/g, "/");
260
+ }
261
+ function normalizeHttpMethod(method) {
262
+ const upper = method.toUpperCase();
263
+ return upper === "DEL" ? "DELETE" : upper;
264
+ }
265
+ function isIdentifier(value) {
266
+ return typeof value === "string" && IDENTIFIER_PATTERN.test(value);
267
+ }
268
+ function addImportBinding(bindings, localName, binding) {
269
+ if (isIdentifier(localName)) {
270
+ bindings.set(localName, binding);
271
+ }
272
+ }
273
+ function parseNamedImportLocal(rawName) {
274
+ const normalized = rawName.trim().replace(/^type\s+/i, "").trim();
275
+ if (!normalized) {
276
+ return undefined;
277
+ }
278
+ const [, aliasedName] = normalized.split(/\s+as\s+/i);
279
+ const localName = (aliasedName ?? normalized.split(/\s*:\s*/).at(-1) ?? "")
280
+ .trim()
281
+ .replace(/=.*$/, "")
282
+ .trim();
283
+ return isIdentifier(localName) ? localName : undefined;
284
+ }
285
+ function addNamedImportBindings(bindings, rawBindings, binding) {
286
+ for (const rawName of rawBindings.split(",")) {
287
+ addImportBinding(bindings, parseNamedImportLocal(rawName), binding);
288
+ }
289
+ }
290
+ function extractImportBindings(fromPath, content, pathLookup) {
291
+ const bindings = new Map();
292
+ IMPORT_BINDING_PATTERN.lastIndex = 0;
293
+ for (const match of content.matchAll(IMPORT_BINDING_PATTERN)) {
294
+ const clause = match[1]?.trim();
295
+ const specifier = match[2];
296
+ if (!clause || !specifier)
297
+ continue;
298
+ const target = resolveSpecifier(fromPath, specifier, pathLookup);
299
+ if (!target)
300
+ continue;
301
+ const binding = { target, specifier };
302
+ const namespaceMatch = clause.match(/\*\s+as\s+([A-Za-z_$][\w$]*)/);
303
+ addImportBinding(bindings, namespaceMatch?.[1], binding);
304
+ const namedMatch = clause.match(/\{([^}]*)\}/);
305
+ if (namedMatch?.[1]) {
306
+ addNamedImportBindings(bindings, namedMatch[1], binding);
307
+ }
308
+ const defaultCandidate = clause
309
+ .split(/[,{]/, 1)[0]
310
+ ?.trim()
311
+ .replace(/^type\s+/i, "");
312
+ addImportBinding(bindings, defaultCandidate, binding);
313
+ }
314
+ REQUIRE_BINDING_PATTERN.lastIndex = 0;
315
+ for (const match of content.matchAll(REQUIRE_BINDING_PATTERN)) {
316
+ const localName = match[1];
317
+ const specifier = match[2];
318
+ if (!localName || !specifier)
319
+ continue;
320
+ const target = resolveSpecifier(fromPath, specifier, pathLookup);
321
+ if (target) {
322
+ addImportBinding(bindings, localName, { target, specifier });
323
+ }
324
+ }
325
+ REQUIRE_DESTRUCTURING_PATTERN.lastIndex = 0;
326
+ for (const match of content.matchAll(REQUIRE_DESTRUCTURING_PATTERN)) {
327
+ const rawBindings = match[1];
328
+ const specifier = match[2];
329
+ if (!rawBindings || !specifier)
330
+ continue;
331
+ const target = resolveSpecifier(fromPath, specifier, pathLookup);
332
+ if (target) {
333
+ addNamedImportBindings(bindings, rawBindings, { target, specifier });
334
+ }
335
+ }
336
+ return bindings;
337
+ }
338
+ function isPythonSourcePath(path) {
339
+ const normalized = normalizeGraphPath(path).toLowerCase();
340
+ return PYTHON_SOURCE_EXTENSIONS.some((extension) => normalized.endsWith(extension));
341
+ }
342
+ function stripPythonLineComment(line) {
343
+ let quote;
344
+ let escaped = false;
345
+ for (let index = 0; index < line.length; index++) {
346
+ const char = line[index];
347
+ if (escaped) {
348
+ escaped = false;
349
+ continue;
350
+ }
351
+ if (quote) {
352
+ if (char === "\\") {
353
+ escaped = true;
354
+ continue;
355
+ }
356
+ if (char === quote) {
357
+ quote = undefined;
358
+ }
359
+ continue;
360
+ }
361
+ if (char === "'" || char === '"') {
362
+ quote = char;
363
+ continue;
364
+ }
365
+ if (char === "#") {
366
+ return line.slice(0, index);
367
+ }
368
+ }
369
+ return line;
370
+ }
371
+ function pythonParenDelta(line) {
372
+ let quote;
373
+ let escaped = false;
374
+ let delta = 0;
375
+ for (const char of line) {
376
+ if (escaped) {
377
+ escaped = false;
378
+ continue;
379
+ }
380
+ if (quote) {
381
+ if (char === "\\") {
382
+ escaped = true;
383
+ continue;
384
+ }
385
+ if (char === quote) {
386
+ quote = undefined;
387
+ }
388
+ continue;
389
+ }
390
+ if (char === "'" || char === '"') {
391
+ quote = char;
392
+ continue;
393
+ }
394
+ if (char === "(") {
395
+ delta += 1;
396
+ }
397
+ else if (char === ")") {
398
+ delta -= 1;
399
+ }
400
+ }
401
+ return delta;
402
+ }
403
+ function pythonLogicalLines(content) {
404
+ const logicalLines = [];
405
+ let pending = "";
406
+ let parenDepth = 0;
407
+ for (const rawLine of content.split(/\r?\n/)) {
408
+ const stripped = stripPythonLineComment(rawLine).trim();
409
+ if (stripped.length === 0) {
410
+ continue;
411
+ }
412
+ if (pending.length === 0 && !/^(?:import|from)\s+/i.test(stripped)) {
413
+ continue;
414
+ }
415
+ const continued = stripped.endsWith("\\");
416
+ const line = continued ? stripped.slice(0, -1).trimEnd() : stripped;
417
+ pending = pending.length > 0 ? `${pending} ${line}` : line;
418
+ parenDepth += pythonParenDelta(line);
419
+ if (!continued && parenDepth <= 0) {
420
+ logicalLines.push(pending.replace(/\s+/g, " ").trim());
421
+ pending = "";
422
+ parenDepth = 0;
423
+ }
424
+ }
425
+ if (pending.length > 0) {
426
+ logicalLines.push(pending.replace(/\s+/g, " ").trim());
427
+ }
428
+ return logicalLines;
429
+ }
430
+ function unwrapPythonImportList(value) {
431
+ let trimmed = value.trim();
432
+ if (trimmed.startsWith("(") && trimmed.endsWith(")")) {
433
+ trimmed = trimmed.slice(1, -1).trim();
434
+ }
435
+ return trimmed;
436
+ }
437
+ function splitPythonImportList(value) {
438
+ const items = [];
439
+ let current = "";
440
+ let quote;
441
+ let escaped = false;
442
+ let parenDepth = 0;
443
+ for (const char of unwrapPythonImportList(value)) {
444
+ if (escaped) {
445
+ current += char;
446
+ escaped = false;
447
+ continue;
448
+ }
449
+ if (quote) {
450
+ current += char;
451
+ if (char === "\\") {
452
+ escaped = true;
453
+ }
454
+ else if (char === quote) {
455
+ quote = undefined;
456
+ }
457
+ continue;
458
+ }
459
+ if (char === "'" || char === '"') {
460
+ current += char;
461
+ quote = char;
462
+ continue;
463
+ }
464
+ if (char === "(") {
465
+ parenDepth += 1;
466
+ current += char;
467
+ continue;
468
+ }
469
+ if (char === ")") {
470
+ parenDepth -= 1;
471
+ current += char;
472
+ continue;
473
+ }
474
+ if (char === "," && parenDepth === 0) {
475
+ const item = current.trim();
476
+ if (item.length > 0) {
477
+ items.push(item);
478
+ }
479
+ current = "";
480
+ continue;
481
+ }
482
+ current += char;
483
+ }
484
+ const item = current.trim();
485
+ if (item.length > 0) {
486
+ items.push(item);
487
+ }
488
+ return items;
489
+ }
490
+ function stripPythonAlias(value) {
491
+ return value.replace(/\s+as\s+[A-Za-z_]\w*$/i, "").trim();
492
+ }
493
+ function isPythonIdentifier(value) {
494
+ return /^[A-Za-z_]\w*$/.test(value);
495
+ }
496
+ function isPythonAbsoluteModuleSpecifier(value) {
497
+ return /^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*$/.test(value);
498
+ }
499
+ function isPythonRelativeModuleSpecifier(value) {
500
+ return /^\.+(?:[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)?$/.test(value);
501
+ }
502
+ function isPythonModuleSpecifier(value) {
503
+ return (isPythonAbsoluteModuleSpecifier(value) ||
504
+ isPythonRelativeModuleSpecifier(value));
505
+ }
506
+ function pythonModulePath(specifier) {
507
+ return specifier.split(".").filter(Boolean).join("/");
508
+ }
509
+ function resolvePythonPathCandidate(candidate, pathLookup) {
510
+ const normalized = normalizeGraphPath(candidate).replace(/\/+$/, "");
511
+ if (normalized.length === 0 || normalized === "." || normalized === "..") {
512
+ return undefined;
513
+ }
514
+ return resolveCandidate(normalized, pathLookup);
515
+ }
516
+ function pythonPathMatchesModule(path, modulePath) {
517
+ const normalizedPath = normalizeGraphPath(path).toLowerCase();
518
+ const normalizedModulePath = normalizeGraphPath(modulePath).toLowerCase();
519
+ return (PYTHON_SOURCE_EXTENSIONS.some((extension) => {
520
+ const moduleFile = `${normalizedModulePath}${extension}`;
521
+ return (normalizedPath === moduleFile ||
522
+ normalizedPath.endsWith(`/${moduleFile}`));
523
+ }) ||
524
+ PYTHON_PACKAGE_INDEX_FILES.some((indexFile) => {
525
+ const packageFile = posix.join(normalizedModulePath, indexFile);
526
+ return (normalizedPath === packageFile ||
527
+ normalizedPath.endsWith(`/${packageFile}`));
528
+ }));
529
+ }
530
+ function commonDirectoryPrefixLength(left, right) {
531
+ const leftParts = normalizeGraphPath(left).split("/").filter(Boolean);
532
+ const rightParts = normalizeGraphPath(right).split("/").filter(Boolean);
533
+ let count = 0;
534
+ while (count < leftParts.length &&
535
+ count < rightParts.length &&
536
+ leftParts[count].toLowerCase() === rightParts[count].toLowerCase()) {
537
+ count += 1;
538
+ }
539
+ return count;
540
+ }
541
+ function resolvePythonAbsoluteModuleSpecifier(fromPath, specifier, pathLookup) {
542
+ const modulePath = pythonModulePath(specifier);
543
+ const direct = resolvePythonPathCandidate(modulePath, pathLookup);
544
+ if (direct) {
545
+ return direct;
546
+ }
547
+ const matches = [...new Set(pathLookup.values())].filter((path) => isPythonSourcePath(path) && pythonPathMatchesModule(path, modulePath));
548
+ if (matches.length === 1) {
549
+ return matches[0];
550
+ }
551
+ if (matches.length === 0) {
552
+ return undefined;
553
+ }
554
+ const fromDir = posix.dirname(normalizeGraphPath(fromPath));
555
+ const scored = matches
556
+ .map((target) => ({
557
+ target,
558
+ score: commonDirectoryPrefixLength(fromDir, posix.dirname(normalizeGraphPath(target))),
559
+ }))
560
+ .sort((a, b) => b.score - a.score || a.target.localeCompare(b.target));
561
+ const bestScore = scored[0]?.score ?? 0;
562
+ const bestMatches = scored.filter((item) => item.score === bestScore);
563
+ if (bestScore > 0 && bestMatches.length === 1) {
564
+ return bestMatches[0].target;
565
+ }
566
+ const srcMatches = matches.filter((target) => normalizeGraphPath(target).toLowerCase().startsWith("src/"));
567
+ return srcMatches.length === 1 ? srcMatches[0] : undefined;
568
+ }
569
+ function resolvePythonRelativeModuleSpecifier(fromPath, specifier, pathLookup) {
570
+ const match = /^(\.+)(.*)$/.exec(specifier);
571
+ if (!match) {
572
+ return undefined;
573
+ }
574
+ const level = match[1].length;
575
+ const remainder = match[2] ?? "";
576
+ let baseDir = posix.dirname(normalizeGraphPath(fromPath));
577
+ for (let index = 1; index < level; index++) {
578
+ const next = posix.dirname(baseDir);
579
+ if (next === baseDir) {
580
+ return undefined;
581
+ }
582
+ baseDir = next;
583
+ }
584
+ const modulePath = pythonModulePath(remainder);
585
+ const candidate = modulePath.length > 0 ? posix.join(baseDir, modulePath) : baseDir;
586
+ return resolvePythonPathCandidate(candidate, pathLookup);
587
+ }
588
+ function resolvePythonModuleSpecifier(fromPath, specifier, pathLookup) {
589
+ if (isPythonRelativeModuleSpecifier(specifier)) {
590
+ return resolvePythonRelativeModuleSpecifier(fromPath, specifier, pathLookup);
591
+ }
592
+ if (isPythonAbsoluteModuleSpecifier(specifier)) {
593
+ return resolvePythonAbsoluteModuleSpecifier(fromPath, specifier, pathLookup);
594
+ }
595
+ return undefined;
596
+ }
597
+ function appendPythonImportedSpecifier(moduleSpecifier, importedName) {
598
+ return moduleSpecifier.endsWith(".")
599
+ ? `${moduleSpecifier}${importedName}`
600
+ : `${moduleSpecifier}.${importedName}`;
601
+ }
602
+ function addPythonImportEdge(edges, fromPath, target, kind, specifier) {
603
+ if (!target || target === fromPath) {
604
+ return;
605
+ }
606
+ edges.push(graphEdge({
607
+ from: fromPath,
608
+ to: target,
609
+ kind,
610
+ confidence: IMPORT_EDGE_CONFIDENCE,
611
+ reason: `Resolved Python import specifier '${specifier}'.`,
612
+ }));
613
+ }
614
+ function extractPythonImportEdges(fromPath, content, pathLookup) {
615
+ if (!isPythonSourcePath(fromPath)) {
616
+ return [];
617
+ }
618
+ const edges = [];
619
+ for (const line of pythonLogicalLines(content)) {
620
+ const importMatch = /^import\s+(.+)$/i.exec(line);
621
+ if (importMatch) {
622
+ for (const rawSpecifier of splitPythonImportList(importMatch[1] ?? "")) {
623
+ const specifier = stripPythonAlias(rawSpecifier);
624
+ if (!isPythonAbsoluteModuleSpecifier(specifier)) {
625
+ continue;
626
+ }
627
+ addPythonImportEdge(edges, fromPath, resolvePythonModuleSpecifier(fromPath, specifier, pathLookup), "python-import", specifier);
628
+ }
629
+ continue;
630
+ }
631
+ const fromImportMatch = /^from\s+([.\w]+)\s+import\s+(.+)$/i.exec(line);
632
+ if (!fromImportMatch) {
633
+ continue;
634
+ }
635
+ const moduleSpecifier = fromImportMatch[1] ?? "";
636
+ if (!isPythonModuleSpecifier(moduleSpecifier)) {
637
+ continue;
638
+ }
639
+ const importedNames = splitPythonImportList(fromImportMatch[2] ?? "")
640
+ .map(stripPythonAlias)
641
+ .filter((name) => name !== "*" && isPythonIdentifier(name));
642
+ const submoduleTargets = importedNames
643
+ .map((name) => appendPythonImportedSpecifier(moduleSpecifier, name))
644
+ .map((specifier) => ({
645
+ specifier,
646
+ target: resolvePythonModuleSpecifier(fromPath, specifier, pathLookup),
647
+ }))
648
+ .filter((item) => item.target);
649
+ if (submoduleTargets.length > 0) {
650
+ for (const { specifier, target } of submoduleTargets) {
651
+ addPythonImportEdge(edges, fromPath, target, "python-from-import", specifier);
652
+ }
653
+ continue;
654
+ }
655
+ addPythonImportEdge(edges, fromPath, resolvePythonModuleSpecifier(fromPath, moduleSpecifier, pathLookup), "python-from-import", moduleSpecifier);
656
+ }
657
+ return edges;
658
+ }
148
659
  function extractImportEdges(fromPath, content, pathLookup) {
149
660
  const edges = [];
150
661
  for (const { pattern, kind } of IMPORT_PATTERNS) {
@@ -155,33 +666,574 @@ function extractImportEdges(fromPath, content, pathLookup) {
155
666
  continue;
156
667
  const target = resolveSpecifier(fromPath, specifier, pathLookup);
157
668
  if (target) {
158
- edges.push({ from: fromPath, to: target, kind });
669
+ edges.push(graphEdge({
670
+ from: fromPath,
671
+ to: target,
672
+ kind,
673
+ confidence: IMPORT_EDGE_CONFIDENCE,
674
+ reason: `Resolved ${kind} specifier '${specifier}'.`,
675
+ }));
159
676
  }
160
677
  }
161
678
  }
162
679
  return edges;
163
680
  }
681
+ function importSpecifierRanges(content) {
682
+ const ranges = [];
683
+ for (const { pattern } of IMPORT_PATTERNS) {
684
+ pattern.lastIndex = 0;
685
+ for (const match of content.matchAll(pattern)) {
686
+ const specifier = match[1];
687
+ if (!specifier)
688
+ continue;
689
+ const fullMatch = match[0];
690
+ const doubleQuotedOffset = fullMatch.lastIndexOf(`"${specifier}"`);
691
+ const singleQuotedOffset = fullMatch.lastIndexOf(`'${specifier}'`);
692
+ const quotedOffset = doubleQuotedOffset >= 0 ? doubleQuotedOffset : singleQuotedOffset;
693
+ if (quotedOffset < 0)
694
+ continue;
695
+ const start = (match.index ?? 0) + quotedOffset + 1;
696
+ ranges.push({ start, end: start + specifier.length });
697
+ }
698
+ }
699
+ return ranges;
700
+ }
701
+ function isImportSpecifierRange(start, end, ranges) {
702
+ return ranges.some((range) => range.start === start && range.end === end);
703
+ }
164
704
  function extractReferenceEdges(fromPath, content, pathLookup) {
165
705
  const edges = [];
706
+ const importRanges = importSpecifierRanges(content);
166
707
  STRING_LITERAL_PATTERN.lastIndex = 0;
167
708
  for (const match of content.matchAll(STRING_LITERAL_PATTERN)) {
168
709
  const literal = match[1];
169
710
  if (!literal)
170
711
  continue;
712
+ const literalStart = (match.index ?? 0) + 1;
713
+ if (isImportSpecifierRange(literalStart, literalStart + literal.length, importRanges)) {
714
+ continue;
715
+ }
171
716
  const target = resolveReferenceLiteral(fromPath, literal, pathLookup);
172
717
  if (target) {
718
+ const relativeReference = literal.startsWith(".");
173
719
  edges.push({
174
720
  from: fromPath,
175
721
  to: target,
176
- kind: literal.startsWith(".")
722
+ kind: relativeReference
177
723
  ? "relative-string-reference"
178
724
  : "repo-path-reference",
725
+ direction: "directed",
726
+ confidence: relativeReference
727
+ ? RELATIVE_REFERENCE_EDGE_CONFIDENCE
728
+ : REFERENCE_EDGE_CONFIDENCE,
729
+ reason: relativeReference
730
+ ? `Resolved relative string literal '${literal}'.`
731
+ : `Resolved repository path string literal '${literal}'.`,
179
732
  });
180
733
  }
181
734
  }
182
735
  return edges;
183
736
  }
184
- export async function buildGraphBundleFromFs(repoManifest, root, disposition) {
737
+ function isJsonSchemaPath(path) {
738
+ return posix
739
+ .basename(normalizeGraphPath(path))
740
+ .toLowerCase()
741
+ .endsWith(".schema.json");
742
+ }
743
+ function collectJsonSchemaRefs(value, refs) {
744
+ if (Array.isArray(value)) {
745
+ for (const item of value) {
746
+ collectJsonSchemaRefs(item, refs);
747
+ }
748
+ return;
749
+ }
750
+ if (value === null || typeof value !== "object") {
751
+ return;
752
+ }
753
+ for (const [key, item] of Object.entries(value)) {
754
+ if (key === "$ref" && typeof item === "string" && item.trim().length > 0) {
755
+ refs.add(item.trim());
756
+ continue;
757
+ }
758
+ collectJsonSchemaRefs(item, refs);
759
+ }
760
+ }
761
+ function resolveJsonSchemaRef(fromPath, ref, pathLookup) {
762
+ const targetSpecifier = (ref.split("#", 1)[0] ?? "").trim();
763
+ if (targetSpecifier.length === 0) {
764
+ return undefined;
765
+ }
766
+ const normalizedSpecifier = normalizeGraphPath(targetSpecifier);
767
+ if (normalizedSpecifier.startsWith("/") ||
768
+ /^[a-z][a-z0-9+.-]*:/i.test(normalizedSpecifier)) {
769
+ return undefined;
770
+ }
771
+ const baseDir = posix.dirname(normalizeGraphPath(fromPath));
772
+ const candidate = targetSpecifier.startsWith(".") || !normalizedSpecifier.includes("/")
773
+ ? posix.join(baseDir, normalizedSpecifier)
774
+ : normalizedSpecifier;
775
+ return resolveCandidate(candidate, pathLookup);
776
+ }
777
+ function extractJsonSchemaReferenceEdges(fromPath, content, pathLookup) {
778
+ if (!isJsonSchemaPath(fromPath)) {
779
+ return [];
780
+ }
781
+ let parsed;
782
+ try {
783
+ parsed = JSON.parse(content);
784
+ }
785
+ catch {
786
+ return [];
787
+ }
788
+ const refs = new Set();
789
+ collectJsonSchemaRefs(parsed, refs);
790
+ const edges = [];
791
+ for (const ref of refs) {
792
+ const target = resolveJsonSchemaRef(fromPath, ref, pathLookup);
793
+ if (!target || target === fromPath) {
794
+ continue;
795
+ }
796
+ edges.push(graphEdge({
797
+ from: fromPath,
798
+ to: target,
799
+ kind: "json-schema-ref",
800
+ confidence: JSON_SCHEMA_REF_EDGE_CONFIDENCE,
801
+ reason: `JSON Schema $ref '${ref}' resolves to '${target}'.`,
802
+ }));
803
+ }
804
+ return edges;
805
+ }
806
+ function extractSchemaContractTestEdges(fromPath, content, pathLookup) {
807
+ if (!isTestPath(normalizeExtractorPath(fromPath)) ||
808
+ !/schema/i.test(fromPath) ||
809
+ !/\.schema\.json/i.test(content)) {
810
+ return [];
811
+ }
812
+ const literalBasenames = new Set();
813
+ STRING_LITERAL_PATTERN.lastIndex = 0;
814
+ for (const match of content.matchAll(STRING_LITERAL_PATTERN)) {
815
+ const literal = match[1];
816
+ if (!literal || !literal.toLowerCase().endsWith(".schema.json")) {
817
+ continue;
818
+ }
819
+ literalBasenames.add(posix.basename(normalizeGraphPath(literal)).toLowerCase());
820
+ }
821
+ if (literalBasenames.size === 0 ||
822
+ literalBasenames.size > MAX_BOUNDED_SUITE_EDGE_FILES) {
823
+ return [];
824
+ }
825
+ const targets = [...new Set(pathLookup.values())]
826
+ .filter((path) => {
827
+ const normalized = normalizeGraphPath(path);
828
+ return (isJsonSchemaPath(normalized) &&
829
+ literalBasenames.has(posix.basename(normalized).toLowerCase()));
830
+ })
831
+ .sort((a, b) => a.localeCompare(b));
832
+ if (targets.length > MAX_BOUNDED_SUITE_EDGE_FILES) {
833
+ return [];
834
+ }
835
+ return targets.map((target) => graphEdge({
836
+ from: fromPath,
837
+ to: target,
838
+ kind: "schema-contract-test-link",
839
+ confidence: SCHEMA_CONTRACT_TEST_EDGE_CONFIDENCE,
840
+ reason: `Schema contract test references '${posix.basename(target)}'.`,
841
+ }));
842
+ }
843
+ function isGithubWorkflowPath(path) {
844
+ const normalized = normalizeGraphPath(path).toLowerCase();
845
+ return (normalized.startsWith(".github/workflows/") &&
846
+ (normalized.endsWith(".yml") || normalized.endsWith(".yaml")));
847
+ }
848
+ function isTypescriptTypeContractPath(path, fileContents) {
849
+ const normalized = normalizeGraphPath(path);
850
+ const segments = normalized.split("/").filter(Boolean);
851
+ if (!segments.includes("types") ||
852
+ isTestPath(normalizeExtractorPath(normalized)) ||
853
+ !TYPESCRIPT_TYPE_CONTRACT_EXTENSIONS.some((extension) => normalized.endsWith(extension))) {
854
+ return false;
855
+ }
856
+ const content = fileContents[path];
857
+ if (!content || content.length > MAX_TYPE_CONTRACT_SOURCE_BYTES) {
858
+ return false;
859
+ }
860
+ return /\bexport\s+(?:declare\s+)?(?:interface|type|enum|const)\b/.test(content);
861
+ }
862
+ function packageScriptSuiteDirectories(graphEdges) {
863
+ const directories = new Set();
864
+ for (const edge of graphEdges) {
865
+ if (edge.kind !== "package-script-link") {
866
+ continue;
867
+ }
868
+ const directory = posix.dirname(normalizeGraphPath(edge.to));
869
+ const basename = posix.basename(directory);
870
+ if (basename === "scripts" || basename === "bin") {
871
+ directories.add(directory);
872
+ }
873
+ }
874
+ return directories;
875
+ }
876
+ function isPackageScriptSuitePath(path, suiteDirectories) {
877
+ const normalized = normalizeGraphPath(path);
878
+ return (suiteDirectories.has(posix.dirname(normalized)) &&
879
+ PACKAGE_SCRIPT_SUITE_EXTENSIONS.some((extension) => normalized.endsWith(extension)));
880
+ }
881
+ function isPythonTestUtilSuitePath(path) {
882
+ const normalized = normalizeGraphPath(path);
883
+ if (!normalized.endsWith(".py"))
884
+ return false;
885
+ if (isPytestConftestPath(normalized))
886
+ return false;
887
+ const dir = posix.dirname(normalized);
888
+ if (!PYTHON_TEST_UTIL_SEGMENT_NAMES.has(posix.basename(dir).toLowerCase()))
889
+ return false;
890
+ return isTestPath(normalizeExtractorPath(dir));
891
+ }
892
+ function extractBoundedSuiteEdges(pathLookup, fileContents, graphEdges) {
893
+ const files = [...new Set(pathLookup.values())].sort((a, b) => a.localeCompare(b));
894
+ const edges = [];
895
+ const scriptSuiteDirectories = packageScriptSuiteDirectories(graphEdges);
896
+ const addSuiteEdges = (params) => {
897
+ const groups = new Map();
898
+ for (const file of files) {
899
+ if (!params.predicate(file)) {
900
+ continue;
901
+ }
902
+ const directory = posix.dirname(normalizeGraphPath(file));
903
+ const group = groups.get(directory) ?? [];
904
+ group.push(file);
905
+ groups.set(directory, group);
906
+ }
907
+ for (const [directory, group] of groups) {
908
+ const maxFiles = params.maxFiles ?? MAX_BOUNDED_SUITE_EDGE_FILES;
909
+ if (group.length < 2 ||
910
+ group.length > maxFiles) {
911
+ continue;
912
+ }
913
+ const suiteName = directory === "." ? "repository root" : directory;
914
+ for (let index = 1; index < group.length; index++) {
915
+ edges.push(graphEdge({
916
+ from: group[index - 1],
917
+ to: group[index],
918
+ kind: params.kind,
919
+ direction: "undirected",
920
+ confidence: params.confidence,
921
+ reason: `${params.label} suite '${suiteName}' groups ${group.length} related file(s).`,
922
+ }));
923
+ }
924
+ }
925
+ };
926
+ addSuiteEdges({
927
+ predicate: isJsonSchemaPath,
928
+ kind: "schema-suite-link",
929
+ confidence: SCHEMA_SUITE_EDGE_CONFIDENCE,
930
+ label: "JSON Schema",
931
+ });
932
+ addSuiteEdges({
933
+ predicate: isGithubWorkflowPath,
934
+ kind: "github-workflow-suite-link",
935
+ confidence: GITHUB_WORKFLOW_SUITE_EDGE_CONFIDENCE,
936
+ label: "GitHub Actions workflow",
937
+ });
938
+ addSuiteEdges({
939
+ predicate: (path) => isPackageScriptSuitePath(path, scriptSuiteDirectories),
940
+ kind: "package-script-suite-link",
941
+ confidence: PACKAGE_SCRIPT_SUITE_EDGE_CONFIDENCE,
942
+ label: "Package script",
943
+ });
944
+ addSuiteEdges({
945
+ predicate: (path) => isTypescriptTypeContractPath(path, fileContents),
946
+ kind: "typescript-type-suite-link",
947
+ confidence: TYPESCRIPT_TYPE_SUITE_EDGE_CONFIDENCE,
948
+ label: "TypeScript type contract",
949
+ maxFiles: MAX_BOUNDED_TYPE_SUITE_EDGE_FILES,
950
+ });
951
+ addSuiteEdges({
952
+ predicate: isPythonTestUtilSuitePath,
953
+ kind: "python-test-util-suite-link",
954
+ confidence: PYTHON_TEST_UTIL_SUITE_EDGE_CONFIDENCE,
955
+ label: "Python test utility",
956
+ });
957
+ return edges;
958
+ }
959
+ function importedHandlerBinding(handlerExpression, bindings) {
960
+ const rootIdentifier = handlerExpression.split(".")[0];
961
+ return rootIdentifier ? bindings.get(rootIdentifier) : undefined;
962
+ }
963
+ function addRouteEvidence(params) {
964
+ const method = params.method ? normalizeHttpMethod(params.method) : undefined;
965
+ if (method && !ROUTE_METHODS.has(method)) {
966
+ return;
967
+ }
968
+ const handlerBinding = params.handlerExpression
969
+ ? importedHandlerBinding(params.handlerExpression, params.bindings)
970
+ : undefined;
971
+ const handlerPath = handlerBinding?.target ?? params.fromPath;
972
+ const route = {
973
+ path: normalizeRoutePath(params.routePath),
974
+ handler: handlerPath,
975
+ };
976
+ if (method) {
977
+ route.method = method;
978
+ }
979
+ params.routes.push(route);
980
+ if (handlerBinding && handlerPath !== params.fromPath) {
981
+ params.calls.push(graphEdge({
982
+ from: params.fromPath,
983
+ to: handlerPath,
984
+ kind: "route-handler-link",
985
+ confidence: ROUTE_HANDLER_EDGE_CONFIDENCE,
986
+ reason: `Route ${method ?? "handler"} '${route.path}' passes handler '${params.handlerExpression}' from '${handlerBinding.specifier}'.`,
987
+ }));
988
+ }
989
+ }
990
+ function extractRegisteredRouteEvidence(fromPath, content, pathLookup) {
991
+ const bindings = extractImportBindings(fromPath, content, pathLookup);
992
+ const calls = [];
993
+ const routes = [];
994
+ ROUTE_REGISTRATION_PATTERN.lastIndex = 0;
995
+ for (const match of content.matchAll(ROUTE_REGISTRATION_PATTERN)) {
996
+ const method = match[1];
997
+ const routePath = match[2];
998
+ const handlerExpression = match[3];
999
+ if (!method || !routePath)
1000
+ continue;
1001
+ addRouteEvidence({
1002
+ fromPath,
1003
+ routes,
1004
+ calls,
1005
+ method,
1006
+ routePath,
1007
+ handlerExpression,
1008
+ bindings,
1009
+ });
1010
+ }
1011
+ ROUTE_OBJECT_PATTERN.lastIndex = 0;
1012
+ for (const match of content.matchAll(ROUTE_OBJECT_PATTERN)) {
1013
+ const body = match[1];
1014
+ if (!body)
1015
+ continue;
1016
+ const method = body.match(/\bmethod\s*:\s*["'`]([A-Za-z]+)["'`]/i)?.[1];
1017
+ const routePath = body.match(/\b(?:url|path)\s*:\s*["'`]([^"'`]+)["'`]/i)?.[1];
1018
+ const handlerExpression = body.match(/\bhandler\s*:\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?)/)?.[1];
1019
+ if (!routePath)
1020
+ continue;
1021
+ addRouteEvidence({
1022
+ fromPath,
1023
+ routes,
1024
+ calls,
1025
+ method,
1026
+ routePath,
1027
+ handlerExpression,
1028
+ bindings,
1029
+ });
1030
+ }
1031
+ return { calls, routes };
1032
+ }
1033
+ function stripSourceExtension(path) {
1034
+ const lowerPath = path.toLowerCase();
1035
+ const extension = SOURCE_EXTENSIONS.find((item) => lowerPath.endsWith(item));
1036
+ return extension ? path.slice(0, -extension.length) : path;
1037
+ }
1038
+ function nextRouteSegment(segment) {
1039
+ if (!segment || (segment.startsWith("(") && segment.endsWith(")"))) {
1040
+ return undefined;
1041
+ }
1042
+ const catchAll = segment.match(/^\[\.\.\.(.+)\]$/);
1043
+ if (catchAll?.[1]) {
1044
+ return `:${catchAll[1]}*`;
1045
+ }
1046
+ const dynamic = segment.match(/^\[(.+)\]$/);
1047
+ if (dynamic?.[1]) {
1048
+ return `:${dynamic[1]}`;
1049
+ }
1050
+ return segment;
1051
+ }
1052
+ function routePathFromSegments(segments) {
1053
+ const routeSegments = segments
1054
+ .map(nextRouteSegment)
1055
+ .filter((segment) => segment !== undefined);
1056
+ if (routeSegments.length === 0) {
1057
+ return undefined;
1058
+ }
1059
+ return normalizeRoutePath(routeSegments.join("/"));
1060
+ }
1061
+ function conventionalRoutePath(filePath) {
1062
+ const normalized = normalizeGraphPath(filePath);
1063
+ const parts = normalized.split("/").filter(Boolean);
1064
+ const lowerParts = parts.map((part) => part.toLowerCase());
1065
+ const fileName = lowerParts.at(-1);
1066
+ if (!fileName) {
1067
+ return undefined;
1068
+ }
1069
+ const appIndex = lowerParts.lastIndexOf("app");
1070
+ if (appIndex >= 0 && fileName.startsWith("route.")) {
1071
+ return routePathFromSegments(parts.slice(appIndex + 1, -1));
1072
+ }
1073
+ const pagesIndex = lowerParts.lastIndexOf("pages");
1074
+ const apiIndex = pagesIndex >= 0
1075
+ ? lowerParts.indexOf("api", pagesIndex + 1)
1076
+ : lowerParts.indexOf("api");
1077
+ if (apiIndex >= 0 && apiIndex < parts.length - 1) {
1078
+ const withoutExtension = stripSourceExtension(parts.at(-1) ?? "");
1079
+ return routePathFromSegments([...parts.slice(apiIndex, -1), withoutExtension]);
1080
+ }
1081
+ return undefined;
1082
+ }
1083
+ function extractConventionalRouteEvidence(fromPath, content) {
1084
+ const routePath = conventionalRoutePath(fromPath);
1085
+ if (!routePath) {
1086
+ return [];
1087
+ }
1088
+ const routes = [];
1089
+ if (content) {
1090
+ ROUTE_METHOD_EXPORT_PATTERN.lastIndex = 0;
1091
+ for (const match of content.matchAll(ROUTE_METHOD_EXPORT_PATTERN)) {
1092
+ const method = match[1];
1093
+ if (method) {
1094
+ routes.push({
1095
+ path: routePath,
1096
+ handler: fromPath,
1097
+ method,
1098
+ });
1099
+ }
1100
+ }
1101
+ }
1102
+ return routes.length > 0 ? routes : [{ path: routePath, handler: fromPath }];
1103
+ }
1104
+ function fallbackRouteEdge(filePath) {
1105
+ const normalized = filePath.toLowerCase();
1106
+ if (normalized.includes("api/") || normalized.includes("route")) {
1107
+ return {
1108
+ path: `/${filePath.replaceAll("/", "_")}`,
1109
+ handler: filePath,
1110
+ method: "GET",
1111
+ };
1112
+ }
1113
+ return undefined;
1114
+ }
1115
+ function stripKnownSourceExtension(path) {
1116
+ const lowerPath = path.toLowerCase();
1117
+ const extension = SOURCE_EXTENSIONS.find((item) => lowerPath.endsWith(item));
1118
+ if (!extension) {
1119
+ return undefined;
1120
+ }
1121
+ return path.slice(0, -extension.length);
1122
+ }
1123
+ function stripTestSuffix(pathWithoutExtension) {
1124
+ const stripped = pathWithoutExtension.replace(/[._-](?:test|spec)$/i, "");
1125
+ return stripped === pathWithoutExtension ? undefined : stripped;
1126
+ }
1127
+ function stripPythonTestPrefix(pathWithoutExtension) {
1128
+ const basename = posix.basename(pathWithoutExtension);
1129
+ const match = /^test[._-](.+)$/i.exec(basename);
1130
+ if (!match?.[1]) {
1131
+ return undefined;
1132
+ }
1133
+ const directory = posix.dirname(pathWithoutExtension);
1134
+ return directory === "." ? match[1] : posix.join(directory, match[1]);
1135
+ }
1136
+ function addTestSourceCandidatesForBase(basePath, candidates) {
1137
+ candidates.add(basePath);
1138
+ const parts = basePath.split("/").filter(Boolean);
1139
+ const topLevelSegment = parts[0]?.toLowerCase();
1140
+ if (topLevelSegment && TOP_LEVEL_TEST_SEGMENTS.has(topLevelSegment)) {
1141
+ const mirroredParts = parts.slice(1);
1142
+ if (mirroredParts.length > 0) {
1143
+ candidates.add(posix.join("src", ...mirroredParts));
1144
+ }
1145
+ }
1146
+ for (let index = 1; index < parts.length; index++) {
1147
+ if (COLOCATED_TEST_SEGMENTS.has(parts[index].toLowerCase())) {
1148
+ const colocatedParts = [
1149
+ ...parts.slice(0, index),
1150
+ ...parts.slice(index + 1),
1151
+ ];
1152
+ if (colocatedParts.length > 0) {
1153
+ candidates.add(posix.join(...colocatedParts));
1154
+ }
1155
+ }
1156
+ }
1157
+ }
1158
+ function testSourceCandidates(testPath) {
1159
+ const normalizedPath = normalizeGraphPath(testPath);
1160
+ const withoutExtension = stripKnownSourceExtension(normalizedPath);
1161
+ if (!withoutExtension) {
1162
+ return [];
1163
+ }
1164
+ const baseCandidates = new Set();
1165
+ const withoutTestSuffix = stripTestSuffix(withoutExtension);
1166
+ if (withoutTestSuffix) {
1167
+ baseCandidates.add(withoutTestSuffix);
1168
+ }
1169
+ if (isPythonSourcePath(normalizedPath)) {
1170
+ const withoutPythonPrefix = stripPythonTestPrefix(withoutExtension);
1171
+ if (withoutPythonPrefix) {
1172
+ baseCandidates.add(withoutPythonPrefix);
1173
+ }
1174
+ }
1175
+ const candidates = new Set();
1176
+ for (const basePath of baseCandidates) {
1177
+ addTestSourceCandidatesForBase(basePath, candidates);
1178
+ }
1179
+ return [...candidates];
1180
+ }
1181
+ function extractTestSourceEdges(fromPath, pathLookup) {
1182
+ if (!isTestPath(normalizeExtractorPath(fromPath))) {
1183
+ return [];
1184
+ }
1185
+ const edges = [];
1186
+ for (const candidate of testSourceCandidates(fromPath)) {
1187
+ const target = resolveCandidate(candidate, pathLookup);
1188
+ if (!target || isTestPath(normalizeExtractorPath(target))) {
1189
+ continue;
1190
+ }
1191
+ edges.push(graphEdge({
1192
+ from: fromPath,
1193
+ to: target,
1194
+ kind: "test-source-link",
1195
+ confidence: TEST_SOURCE_EDGE_CONFIDENCE,
1196
+ reason: `Test path naming maps to source path '${target}'.`,
1197
+ }));
1198
+ }
1199
+ return edges;
1200
+ }
1201
+ function isPytestConftestPath(path) {
1202
+ return posix.basename(normalizeGraphPath(path)).toLowerCase() === "conftest.py";
1203
+ }
1204
+ function extractPytestConftestLinks(pathLookup) {
1205
+ const allPaths = [...new Set(pathLookup.values())];
1206
+ const conftestPaths = allPaths.filter((p) => isPytestConftestPath(p));
1207
+ if (conftestPaths.length === 0)
1208
+ return [];
1209
+ const edges = [];
1210
+ for (const conftestPath of conftestPaths) {
1211
+ const conftestDir = posix.dirname(normalizeGraphPath(conftestPath));
1212
+ if (!isTestPath(normalizeExtractorPath(conftestDir)))
1213
+ continue;
1214
+ const scopePrefix = `${conftestDir}/`;
1215
+ for (const targetPath of allPaths) {
1216
+ if (targetPath === conftestPath)
1217
+ continue;
1218
+ const normalizedTarget = normalizeGraphPath(targetPath);
1219
+ if (!normalizedTarget.startsWith(scopePrefix))
1220
+ continue;
1221
+ if (!normalizedTarget.endsWith(".py"))
1222
+ continue;
1223
+ if (isPytestConftestPath(normalizedTarget))
1224
+ continue;
1225
+ edges.push(graphEdge({
1226
+ from: conftestPath,
1227
+ to: targetPath,
1228
+ kind: "conftest-link",
1229
+ confidence: CONFTEST_LINK_CONFIDENCE,
1230
+ reason: `Pytest conftest '${conftestPath}' applies to all Python files in its scope directory.`,
1231
+ }));
1232
+ }
1233
+ }
1234
+ return edges;
1235
+ }
1236
+ export async function buildGraphBundleFromFs(repoManifest, root, disposition, options = {}) {
185
1237
  const rootPath = resolve(root);
186
1238
  const dispositionMap = new Map(disposition?.files.map((item) => [item.path, item.status]) ?? []);
187
1239
  const fileContents = {};
@@ -204,10 +1256,11 @@ export async function buildGraphBundleFromFs(repoManifest, root, disposition) {
204
1256
  // Best-effort graph extraction should not block structure planning.
205
1257
  }
206
1258
  }));
207
- return buildGraphBundle(repoManifest, disposition, { fileContents });
1259
+ return buildGraphBundle(repoManifest, disposition, { ...options, fileContents });
208
1260
  }
209
1261
  export function buildGraphBundle(repoManifest, disposition, options = {}) {
210
1262
  const imports = [];
1263
+ const calls = [];
211
1264
  const references = [];
212
1265
  const routes = [];
213
1266
  const dispositionMap = new Map(disposition?.files.map((item) => [item.path, item.status]) ?? []);
@@ -217,22 +1270,18 @@ export function buildGraphBundle(repoManifest, disposition, options = {}) {
217
1270
  if (file.excluded || (status && isAuditExcludedStatus(status))) {
218
1271
  continue;
219
1272
  }
220
- const normalized = file.path.toLowerCase();
221
- if (normalized.includes("api/") || normalized.includes("route")) {
222
- routes.push({
223
- path: `/${file.path.replaceAll("/", "_")}`,
224
- handler: file.path,
225
- method: "GET",
226
- });
227
- }
228
1273
  const parts = file.path.split("/");
229
1274
  if (parts.length > 2) {
230
- imports.push({
1275
+ imports.push(graphEdge({
231
1276
  from: file.path,
232
1277
  to: `${parts[0]}/${parts[1]}`,
233
1278
  kind: "heuristic-container-edge",
234
- });
1279
+ direction: "undirected",
1280
+ confidence: CONTAINER_EDGE_CONFIDENCE,
1281
+ reason: "Path hierarchy suggests shared module ownership.",
1282
+ }));
235
1283
  }
1284
+ const normalized = file.path.toLowerCase();
236
1285
  if (normalized.includes("auth") &&
237
1286
  normalized.includes("session") === false) {
238
1287
  for (const other of repoManifest.files) {
@@ -242,25 +1291,56 @@ export function buildGraphBundle(repoManifest, disposition, options = {}) {
242
1291
  if (otherStatus && isAuditExcludedStatus(otherStatus))
243
1292
  continue;
244
1293
  if (other.path.toLowerCase().includes("session")) {
245
- imports.push({
1294
+ imports.push(graphEdge({
246
1295
  from: file.path,
247
1296
  to: other.path,
248
1297
  kind: "heuristic-auth-session-link",
249
- });
1298
+ confidence: AUTH_SESSION_EDGE_CONFIDENCE,
1299
+ reason: "Security-sensitive auth path appears coupled to a session path by naming convention.",
1300
+ }));
250
1301
  }
251
1302
  }
252
1303
  }
253
1304
  const content = options.fileContents?.[file.path];
1305
+ const fileRoutes = [];
254
1306
  if (content) {
255
1307
  imports.push(...extractImportEdges(file.path, content, pathLookup));
1308
+ imports.push(...extractPythonImportEdges(file.path, content, pathLookup));
256
1309
  references.push(...extractReferenceEdges(file.path, content, pathLookup));
1310
+ references.push(...extractJsonSchemaReferenceEdges(file.path, content, pathLookup));
1311
+ references.push(...extractPackageEntrypointEdges(file.path, content, pathLookup));
1312
+ references.push(...extractPackageScriptEdges(file.path, content, pathLookup));
1313
+ references.push(...extractWorkspacePackageEdges(file.path, content, pathLookup));
1314
+ references.push(...extractTypescriptProjectReferenceEdges(file.path, content, pathLookup));
1315
+ references.push(...extractGoWorkspaceModuleEdges(file.path, content, pathLookup));
1316
+ references.push(...extractCargoWorkspaceMemberEdges(file.path, content, pathLookup));
1317
+ references.push(...extractMavenModuleEdges(file.path, content, pathLookup));
1318
+ references.push(...extractPyprojectTestpathLinks(file.path, content, pathLookup));
1319
+ references.push(...extractYamlPathReferenceEdges(file.path, content, pathLookup));
1320
+ references.push(...extractSchemaContractTestEdges(file.path, content, pathLookup));
1321
+ const registeredRoutes = extractRegisteredRouteEvidence(file.path, content, pathLookup);
1322
+ calls.push(...registeredRoutes.calls);
1323
+ fileRoutes.push(...registeredRoutes.routes);
257
1324
  }
1325
+ fileRoutes.push(...extractConventionalRouteEvidence(file.path, content));
1326
+ if (fileRoutes.length === 0) {
1327
+ const fallbackRoute = fallbackRouteEdge(file.path);
1328
+ if (fallbackRoute) {
1329
+ fileRoutes.push(fallbackRoute);
1330
+ }
1331
+ }
1332
+ routes.push(...fileRoutes);
1333
+ references.push(...extractTestSourceEdges(file.path, pathLookup));
258
1334
  }
1335
+ references.push(...extractAnalyzerOwnershipEdges(options.externalAnalyzerResults, pathLookup));
1336
+ references.push(...extractPytestConftestLinks(pathLookup));
1337
+ references.push(...extractBoundedSuiteEdges(pathLookup, options.fileContents ?? {}, references));
259
1338
  return {
260
1339
  graphs: {
261
1340
  imports: uniqueSortedEdges(imports),
1341
+ calls: uniqueSortedEdges(calls),
262
1342
  references: uniqueSortedEdges(references),
263
- routes,
1343
+ routes: uniqueSortedRoutes(routes),
264
1344
  },
265
1345
  };
266
1346
  }