auditor-lambda 0.3.40 → 0.5.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.
Files changed (193) hide show
  1. package/audit-code-wrapper-lib.mjs +20 -2
  2. package/dist/cli/args.d.ts +59 -0
  3. package/dist/cli/args.js +244 -0
  4. package/dist/cli/dispatch.d.ts +80 -0
  5. package/dist/cli/dispatch.js +532 -0
  6. package/dist/cli/prompts.d.ts +37 -0
  7. package/dist/cli/prompts.js +225 -0
  8. package/dist/cli/steps.d.ts +29 -0
  9. package/dist/cli/steps.js +30 -0
  10. package/dist/cli/waveManifest.d.ts +40 -0
  11. package/dist/cli/waveManifest.js +41 -0
  12. package/dist/cli/workerResult.d.ts +18 -0
  13. package/dist/cli/workerResult.js +42 -0
  14. package/dist/cli.d.ts +2 -22
  15. package/dist/cli.js +442 -975
  16. package/dist/extractors/analyzers/css.d.ts +2 -0
  17. package/dist/extractors/analyzers/css.js +101 -0
  18. package/dist/extractors/analyzers/html.d.ts +2 -0
  19. package/dist/extractors/analyzers/html.js +92 -0
  20. package/dist/extractors/analyzers/merge.d.ts +14 -0
  21. package/dist/extractors/analyzers/merge.js +85 -0
  22. package/dist/extractors/analyzers/python.d.ts +2 -0
  23. package/dist/extractors/analyzers/python.js +104 -0
  24. package/dist/extractors/analyzers/registry.d.ts +33 -0
  25. package/dist/extractors/analyzers/registry.js +100 -0
  26. package/dist/extractors/analyzers/resourceUrl.d.ts +7 -0
  27. package/dist/extractors/analyzers/resourceUrl.js +25 -0
  28. package/dist/extractors/analyzers/sql.d.ts +2 -0
  29. package/dist/extractors/analyzers/sql.js +19 -0
  30. package/dist/extractors/analyzers/treeSitter.d.ts +34 -0
  31. package/dist/extractors/analyzers/treeSitter.js +111 -0
  32. package/dist/extractors/analyzers/types.d.ts +53 -0
  33. package/dist/extractors/analyzers/typescript.d.ts +2 -0
  34. package/dist/extractors/analyzers/typescript.js +257 -0
  35. package/dist/extractors/browserExtension.d.ts +1 -3
  36. package/dist/extractors/browserExtension.js +2 -2
  37. package/dist/extractors/designAssessment.d.ts +1 -3
  38. package/dist/extractors/disposition.d.ts +2 -1
  39. package/dist/extractors/disposition.js +11 -1
  40. package/dist/extractors/flows.d.ts +1 -3
  41. package/dist/extractors/flows.js +2 -2
  42. package/dist/extractors/graph.d.ts +2 -2
  43. package/dist/extractors/graph.js +171 -327
  44. package/dist/extractors/graphManifestEdges.d.ts +1 -1
  45. package/dist/extractors/graphPathUtils.d.ts +1 -1
  46. package/dist/extractors/graphPythonImports.d.ts +18 -0
  47. package/dist/extractors/graphPythonImports.js +362 -0
  48. package/dist/extractors/pathPatterns.d.ts +6 -0
  49. package/dist/extractors/pathPatterns.js +8 -0
  50. package/dist/extractors/risk.d.ts +1 -2
  51. package/dist/extractors/surfaces.d.ts +1 -3
  52. package/dist/extractors/surfaces.js +2 -2
  53. package/dist/io/artifacts.d.ts +12 -5
  54. package/dist/io/artifacts.js +13 -1
  55. package/dist/io/runArtifacts.js +1 -1
  56. package/dist/mcp/server.js +1 -1
  57. package/dist/orchestrator/advance.d.ts +21 -0
  58. package/dist/orchestrator/advance.js +69 -7
  59. package/dist/orchestrator/auditTaskUtils.d.ts +4 -0
  60. package/dist/orchestrator/auditTaskUtils.js +27 -0
  61. package/dist/orchestrator/dependencyMap.js +27 -0
  62. package/dist/orchestrator/edgeReasoning.d.ts +39 -0
  63. package/dist/orchestrator/edgeReasoning.js +125 -0
  64. package/dist/orchestrator/executors.js +11 -1
  65. package/dist/orchestrator/fileAnchors.d.ts +1 -1
  66. package/dist/orchestrator/fileIntegrity.d.ts +7 -0
  67. package/dist/orchestrator/fileIntegrity.js +41 -0
  68. package/dist/orchestrator/flowCoverage.d.ts +1 -1
  69. package/dist/orchestrator/flowPlanning.d.ts +1 -1
  70. package/dist/orchestrator/flowRequeue.d.ts +1 -1
  71. package/dist/orchestrator/graphEnrichmentExecutor.d.ts +29 -0
  72. package/dist/orchestrator/graphEnrichmentExecutor.js +196 -0
  73. package/dist/orchestrator/internalExecutors.d.ts +13 -2
  74. package/dist/orchestrator/internalExecutors.js +112 -16
  75. package/dist/orchestrator/localCommands.js +6 -25
  76. package/dist/orchestrator/nextStep.d.ts +2 -1
  77. package/dist/orchestrator/nextStep.js +3 -1
  78. package/dist/orchestrator/planning.d.ts +1 -1
  79. package/dist/orchestrator/requeueCommand.d.ts +1 -1
  80. package/dist/orchestrator/reviewPackets.d.ts +37 -4
  81. package/dist/orchestrator/reviewPackets.js +113 -158
  82. package/dist/orchestrator/runtimeValidation.d.ts +1 -1
  83. package/dist/orchestrator/runtimeValidation.js +4 -31
  84. package/dist/orchestrator/scope.d.ts +62 -0
  85. package/dist/orchestrator/scope.js +227 -0
  86. package/dist/orchestrator/state.js +2 -0
  87. package/dist/orchestrator/taskBuilder.d.ts +1 -1
  88. package/dist/orchestrator/taskBuilder.js +1 -12
  89. package/dist/orchestrator/unionFind.d.ts +7 -0
  90. package/dist/orchestrator/unionFind.js +32 -0
  91. package/dist/orchestrator/unitBuilder.d.ts +2 -2
  92. package/dist/orchestrator/unitBuilder.js +4 -18
  93. package/dist/prompts/renderWorkerPrompt.js +18 -1
  94. package/dist/providers/claudeCodeProvider.d.ts +4 -4
  95. package/dist/providers/claudeCodeProvider.js +9 -3
  96. package/dist/providers/constants.d.ts +1 -1
  97. package/dist/providers/constants.js +1 -1
  98. package/dist/providers/index.d.ts +1 -2
  99. package/dist/providers/index.js +5 -4
  100. package/dist/providers/localSubprocessProvider.d.ts +2 -2
  101. package/dist/providers/localSubprocessProvider.js +1 -1
  102. package/dist/providers/opencodeProvider.d.ts +4 -4
  103. package/dist/providers/opencodeProvider.js +7 -2
  104. package/dist/providers/spawnLoggedCommand.d.ts +3 -1
  105. package/dist/providers/spawnLoggedCommand.js +21 -0
  106. package/dist/providers/subprocessTemplateProvider.d.ts +4 -4
  107. package/dist/providers/subprocessTemplateProvider.js +8 -3
  108. package/dist/providers/vscodeTaskProvider.d.ts +3 -4
  109. package/dist/providers/vscodeTaskProvider.js +2 -2
  110. package/dist/quota/discoveredLimits.js +1 -1
  111. package/dist/quota/hostLimits.d.ts +1 -2
  112. package/dist/quota/hostLimits.js +4 -46
  113. package/dist/quota/index.d.ts +18 -15
  114. package/dist/quota/index.js +4 -9
  115. package/dist/quota/scheduler.d.ts +1 -3
  116. package/dist/quota/scheduler.js +1 -2
  117. package/dist/reporting/synthesis.d.ts +37 -3
  118. package/dist/reporting/synthesis.js +97 -16
  119. package/dist/reporting/synthesisNarrativePrompt.d.ts +7 -0
  120. package/dist/reporting/synthesisNarrativePrompt.js +60 -0
  121. package/dist/reporting/workBlocks.d.ts +2 -11
  122. package/dist/supervisor/operatorHandoff.js +1 -1
  123. package/dist/supervisor/runLedger.d.ts +1 -1
  124. package/dist/supervisor/runLedger.js +2 -2
  125. package/dist/supervisor/sessionConfig.d.ts +8 -1
  126. package/dist/supervisor/sessionConfig.js +22 -3
  127. package/dist/types/analyzerCapability.d.ts +16 -0
  128. package/dist/types/auditScope.d.ts +43 -0
  129. package/dist/types/auditScope.js +14 -0
  130. package/dist/types/reviewPlanning.d.ts +1 -1
  131. package/dist/types/synthesisNarrative.d.ts +7 -0
  132. package/dist/types/synthesisNarrative.js +5 -0
  133. package/dist/types/workerSession.d.ts +6 -0
  134. package/dist/types.d.ts +2 -19
  135. package/dist/validation/artifacts.d.ts +1 -1
  136. package/dist/validation/artifacts.js +10 -1
  137. package/dist/validation/auditResults.d.ts +1 -1
  138. package/dist/validation/auditResults.js +1 -1
  139. package/dist/validation/sessionConfig.d.ts +2 -3
  140. package/dist/validation/sessionConfig.js +25 -3
  141. package/package.json +7 -3
  142. package/schemas/analyzer_capability.schema.json +47 -0
  143. package/schemas/audit_findings.schema.json +141 -0
  144. package/schemas/finding.schema.json +2 -1
  145. package/schemas/graph_bundle.schema.json +5 -0
  146. package/schemas/scope.schema.json +46 -0
  147. package/scripts/postinstall.mjs +0 -1
  148. package/dist/io/json.d.ts +0 -10
  149. package/dist/io/json.js +0 -142
  150. package/dist/providers/types.d.ts +0 -33
  151. package/dist/quota/compositeQuotaSource.d.ts +0 -7
  152. package/dist/quota/compositeQuotaSource.js +0 -20
  153. package/dist/quota/errorParsers/claudeCodeErrorParser.d.ts +0 -6
  154. package/dist/quota/errorParsers/claudeCodeErrorParser.js +0 -39
  155. package/dist/quota/errorParsers/genericErrorParser.d.ts +0 -9
  156. package/dist/quota/errorParsers/genericErrorParser.js +0 -7
  157. package/dist/quota/errorParsers/index.d.ts +0 -5
  158. package/dist/quota/errorParsers/index.js +0 -12
  159. package/dist/quota/errorParsing.d.ts +0 -7
  160. package/dist/quota/errorParsing.js +0 -69
  161. package/dist/quota/fileLock.d.ts +0 -6
  162. package/dist/quota/fileLock.js +0 -64
  163. package/dist/quota/learnedQuotaSource.d.ts +0 -7
  164. package/dist/quota/learnedQuotaSource.js +0 -25
  165. package/dist/quota/limits.d.ts +0 -16
  166. package/dist/quota/limits.js +0 -77
  167. package/dist/quota/quotaSource.d.ts +0 -12
  168. package/dist/quota/slidingWindow.d.ts +0 -4
  169. package/dist/quota/slidingWindow.js +0 -28
  170. package/dist/quota/state.d.ts +0 -15
  171. package/dist/quota/state.js +0 -148
  172. package/dist/quota/types.d.ts +0 -67
  173. package/dist/quota/types.js +0 -1
  174. package/dist/reporting/rootCause.d.ts +0 -10
  175. package/dist/reporting/rootCause.js +0 -146
  176. package/dist/types/disposition.d.ts +0 -9
  177. package/dist/types/disposition.js +0 -1
  178. package/dist/types/flows.d.ts +0 -17
  179. package/dist/types/flows.js +0 -1
  180. package/dist/types/graph.d.ts +0 -22
  181. package/dist/types/graph.js +0 -1
  182. package/dist/types/risk.d.ts +0 -9
  183. package/dist/types/risk.js +0 -1
  184. package/dist/types/runLedger.d.ts +0 -17
  185. package/dist/types/runLedger.js +0 -6
  186. package/dist/types/sessionConfig.d.ts +0 -79
  187. package/dist/types/sessionConfig.js +0 -15
  188. package/dist/types/surfaces.d.ts +0 -15
  189. package/dist/types/surfaces.js +0 -1
  190. package/dist/validation/basic.d.ts +0 -13
  191. package/dist/validation/basic.js +0 -46
  192. /package/dist/{providers → extractors/analyzers}/types.js +0 -0
  193. /package/dist/{quota/quotaSource.js → types/analyzerCapability.js} +0 -0
@@ -1,10 +1,11 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { isAbsolute, relative, resolve } from "node:path";
3
3
  import { posix } from "node:path";
4
- import { isAuditExcludedStatus } from "./disposition.js";
4
+ import { buildDispositionMap, isAuditExcludedStatus } from "./disposition.js";
5
5
  import { extractChromeExtensionManifestEdges, extractHtmlResourceEdges, } from "./browserExtension.js";
6
6
  import { extractCargoWorkspaceMemberEdges, extractGoWorkspaceModuleEdges, extractMavenModuleEdges, extractPackageEntrypointEdges, extractPackageScriptEdges, extractPyprojectTestpathLinks, extractTypescriptProjectReferenceEdges, extractWorkspacePackageEdges, extractYamlPathReferenceEdges, isCargoManifestPath, isGoWorkspaceManifestPath, isMavenPomPath, isPyprojectPath, } from "./graphManifestEdges.js";
7
7
  import { graphEdge, graphLookupKey, normalizeGraphPath, resolveCandidate, } from "./graphPathUtils.js";
8
+ import { extractPythonImportEdges, isPythonSourcePath, } from "./graphPythonImports.js";
8
9
  import { isTestPath, normalizeExtractorPath } from "./pathPatterns.js";
9
10
  const MAX_GRAPH_SOURCE_BYTES = 512 * 1024;
10
11
  const SOURCE_LANGUAGES = new Set([
@@ -40,8 +41,6 @@ const SOURCE_EXTENSIONS = [
40
41
  ".java",
41
42
  ".cs",
42
43
  ];
43
- const PYTHON_SOURCE_EXTENSIONS = [".py", ".pyi"];
44
- const PYTHON_PACKAGE_INDEX_FILES = ["__init__.py", "__init__.pyi"];
45
44
  const TYPESCRIPT_TYPE_CONTRACT_EXTENSIONS = [
46
45
  ".ts",
47
46
  ".tsx",
@@ -135,7 +134,7 @@ function shouldReadForGraph(file) {
135
134
  isMavenPomPath(normalized) ||
136
135
  isPyprojectPath(normalized)));
137
136
  }
138
- function buildPathLookup(repoManifest, dispositionMap) {
137
+ export function buildPathLookup(repoManifest, dispositionMap) {
139
138
  return new Map(repoManifest.files
140
139
  .filter((file) => {
141
140
  const status = dispositionMap.get(file.path);
@@ -339,327 +338,6 @@ function extractImportBindings(fromPath, content, pathLookup) {
339
338
  }
340
339
  return bindings;
341
340
  }
342
- function isPythonSourcePath(path) {
343
- const normalized = normalizeGraphPath(path).toLowerCase();
344
- return PYTHON_SOURCE_EXTENSIONS.some((extension) => normalized.endsWith(extension));
345
- }
346
- function stripPythonLineComment(line) {
347
- let quote;
348
- let escaped = false;
349
- for (let index = 0; index < line.length; index++) {
350
- const char = line[index];
351
- if (escaped) {
352
- escaped = false;
353
- continue;
354
- }
355
- if (quote) {
356
- if (char === "\\") {
357
- escaped = true;
358
- continue;
359
- }
360
- if (char === quote) {
361
- quote = undefined;
362
- }
363
- continue;
364
- }
365
- if (char === "'" || char === '"') {
366
- quote = char;
367
- continue;
368
- }
369
- if (char === "#") {
370
- return line.slice(0, index);
371
- }
372
- }
373
- return line;
374
- }
375
- function pythonParenDelta(line) {
376
- let quote;
377
- let escaped = false;
378
- let delta = 0;
379
- for (const char of line) {
380
- if (escaped) {
381
- escaped = false;
382
- continue;
383
- }
384
- if (quote) {
385
- if (char === "\\") {
386
- escaped = true;
387
- continue;
388
- }
389
- if (char === quote) {
390
- quote = undefined;
391
- }
392
- continue;
393
- }
394
- if (char === "'" || char === '"') {
395
- quote = char;
396
- continue;
397
- }
398
- if (char === "(") {
399
- delta += 1;
400
- }
401
- else if (char === ")") {
402
- delta -= 1;
403
- }
404
- }
405
- return delta;
406
- }
407
- function pythonLogicalLines(content) {
408
- const logicalLines = [];
409
- let pending = "";
410
- let parenDepth = 0;
411
- for (const rawLine of content.split(/\r?\n/)) {
412
- const stripped = stripPythonLineComment(rawLine).trim();
413
- if (stripped.length === 0) {
414
- continue;
415
- }
416
- if (pending.length === 0 && !/^(?:import|from)\s+/i.test(stripped)) {
417
- continue;
418
- }
419
- const continued = stripped.endsWith("\\");
420
- const line = continued ? stripped.slice(0, -1).trimEnd() : stripped;
421
- pending = pending.length > 0 ? `${pending} ${line}` : line;
422
- parenDepth += pythonParenDelta(line);
423
- if (!continued && parenDepth <= 0) {
424
- logicalLines.push(pending.replace(/\s+/g, " ").trim());
425
- pending = "";
426
- parenDepth = 0;
427
- }
428
- }
429
- if (pending.length > 0) {
430
- logicalLines.push(pending.replace(/\s+/g, " ").trim());
431
- }
432
- return logicalLines;
433
- }
434
- function unwrapPythonImportList(value) {
435
- let trimmed = value.trim();
436
- if (trimmed.startsWith("(") && trimmed.endsWith(")")) {
437
- trimmed = trimmed.slice(1, -1).trim();
438
- }
439
- return trimmed;
440
- }
441
- function splitPythonImportList(value) {
442
- const items = [];
443
- let current = "";
444
- let quote;
445
- let escaped = false;
446
- let parenDepth = 0;
447
- for (const char of unwrapPythonImportList(value)) {
448
- if (escaped) {
449
- current += char;
450
- escaped = false;
451
- continue;
452
- }
453
- if (quote) {
454
- current += char;
455
- if (char === "\\") {
456
- escaped = true;
457
- }
458
- else if (char === quote) {
459
- quote = undefined;
460
- }
461
- continue;
462
- }
463
- if (char === "'" || char === '"') {
464
- current += char;
465
- quote = char;
466
- continue;
467
- }
468
- if (char === "(") {
469
- parenDepth += 1;
470
- current += char;
471
- continue;
472
- }
473
- if (char === ")") {
474
- parenDepth -= 1;
475
- current += char;
476
- continue;
477
- }
478
- if (char === "," && parenDepth === 0) {
479
- const item = current.trim();
480
- if (item.length > 0) {
481
- items.push(item);
482
- }
483
- current = "";
484
- continue;
485
- }
486
- current += char;
487
- }
488
- const item = current.trim();
489
- if (item.length > 0) {
490
- items.push(item);
491
- }
492
- return items;
493
- }
494
- function stripPythonAlias(value) {
495
- return value.replace(/\s+as\s+[A-Za-z_]\w*$/i, "").trim();
496
- }
497
- function isPythonIdentifier(value) {
498
- return /^[A-Za-z_]\w*$/.test(value);
499
- }
500
- function isPythonAbsoluteModuleSpecifier(value) {
501
- return /^[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*$/.test(value);
502
- }
503
- function isPythonRelativeModuleSpecifier(value) {
504
- return /^\.+(?:[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)?$/.test(value);
505
- }
506
- function isPythonModuleSpecifier(value) {
507
- return (isPythonAbsoluteModuleSpecifier(value) ||
508
- isPythonRelativeModuleSpecifier(value));
509
- }
510
- function pythonModulePath(specifier) {
511
- return specifier.split(".").filter(Boolean).join("/");
512
- }
513
- function resolvePythonPathCandidate(candidate, pathLookup) {
514
- const normalized = normalizeGraphPath(candidate).replace(/\/+$/, "");
515
- if (normalized.length === 0 || normalized === "." || normalized === "..") {
516
- return undefined;
517
- }
518
- return resolveCandidate(normalized, pathLookup);
519
- }
520
- function pythonPathMatchesModule(path, modulePath) {
521
- const normalizedPath = normalizeGraphPath(path).toLowerCase();
522
- const normalizedModulePath = normalizeGraphPath(modulePath).toLowerCase();
523
- return (PYTHON_SOURCE_EXTENSIONS.some((extension) => {
524
- const moduleFile = `${normalizedModulePath}${extension}`;
525
- return (normalizedPath === moduleFile ||
526
- normalizedPath.endsWith(`/${moduleFile}`));
527
- }) ||
528
- PYTHON_PACKAGE_INDEX_FILES.some((indexFile) => {
529
- const packageFile = posix.join(normalizedModulePath, indexFile);
530
- return (normalizedPath === packageFile ||
531
- normalizedPath.endsWith(`/${packageFile}`));
532
- }));
533
- }
534
- function commonDirectoryPrefixLength(left, right) {
535
- const leftParts = normalizeGraphPath(left).split("/").filter(Boolean);
536
- const rightParts = normalizeGraphPath(right).split("/").filter(Boolean);
537
- let count = 0;
538
- while (count < leftParts.length &&
539
- count < rightParts.length &&
540
- leftParts[count].toLowerCase() === rightParts[count].toLowerCase()) {
541
- count += 1;
542
- }
543
- return count;
544
- }
545
- function resolvePythonAbsoluteModuleSpecifier(fromPath, specifier, pathLookup) {
546
- const modulePath = pythonModulePath(specifier);
547
- const direct = resolvePythonPathCandidate(modulePath, pathLookup);
548
- if (direct) {
549
- return direct;
550
- }
551
- const matches = [...new Set(pathLookup.values())].filter((path) => isPythonSourcePath(path) && pythonPathMatchesModule(path, modulePath));
552
- if (matches.length === 1) {
553
- return matches[0];
554
- }
555
- if (matches.length === 0) {
556
- return undefined;
557
- }
558
- const fromDir = posix.dirname(normalizeGraphPath(fromPath));
559
- const scored = matches
560
- .map((target) => ({
561
- target,
562
- score: commonDirectoryPrefixLength(fromDir, posix.dirname(normalizeGraphPath(target))),
563
- }))
564
- .sort((a, b) => b.score - a.score || a.target.localeCompare(b.target));
565
- const bestScore = scored[0]?.score ?? 0;
566
- const bestMatches = scored.filter((item) => item.score === bestScore);
567
- if (bestScore > 0 && bestMatches.length === 1) {
568
- return bestMatches[0].target;
569
- }
570
- const srcMatches = matches.filter((target) => normalizeGraphPath(target).toLowerCase().startsWith("src/"));
571
- return srcMatches.length === 1 ? srcMatches[0] : undefined;
572
- }
573
- function resolvePythonRelativeModuleSpecifier(fromPath, specifier, pathLookup) {
574
- const match = /^(\.+)(.*)$/.exec(specifier);
575
- if (!match) {
576
- return undefined;
577
- }
578
- const level = match[1].length;
579
- const remainder = match[2] ?? "";
580
- let baseDir = posix.dirname(normalizeGraphPath(fromPath));
581
- for (let index = 1; index < level; index++) {
582
- const next = posix.dirname(baseDir);
583
- if (next === baseDir) {
584
- return undefined;
585
- }
586
- baseDir = next;
587
- }
588
- const modulePath = pythonModulePath(remainder);
589
- const candidate = modulePath.length > 0 ? posix.join(baseDir, modulePath) : baseDir;
590
- return resolvePythonPathCandidate(candidate, pathLookup);
591
- }
592
- function resolvePythonModuleSpecifier(fromPath, specifier, pathLookup) {
593
- if (isPythonRelativeModuleSpecifier(specifier)) {
594
- return resolvePythonRelativeModuleSpecifier(fromPath, specifier, pathLookup);
595
- }
596
- if (isPythonAbsoluteModuleSpecifier(specifier)) {
597
- return resolvePythonAbsoluteModuleSpecifier(fromPath, specifier, pathLookup);
598
- }
599
- return undefined;
600
- }
601
- function appendPythonImportedSpecifier(moduleSpecifier, importedName) {
602
- return moduleSpecifier.endsWith(".")
603
- ? `${moduleSpecifier}${importedName}`
604
- : `${moduleSpecifier}.${importedName}`;
605
- }
606
- function addPythonImportEdge(edges, fromPath, target, kind, specifier) {
607
- if (!target || target === fromPath) {
608
- return;
609
- }
610
- edges.push(graphEdge({
611
- from: fromPath,
612
- to: target,
613
- kind,
614
- confidence: IMPORT_EDGE_CONFIDENCE,
615
- reason: `Resolved Python import specifier '${specifier}'.`,
616
- }));
617
- }
618
- function extractPythonImportEdges(fromPath, content, pathLookup) {
619
- if (!isPythonSourcePath(fromPath)) {
620
- return [];
621
- }
622
- const edges = [];
623
- for (const line of pythonLogicalLines(content)) {
624
- const importMatch = /^import\s+(.+)$/i.exec(line);
625
- if (importMatch) {
626
- for (const rawSpecifier of splitPythonImportList(importMatch[1] ?? "")) {
627
- const specifier = stripPythonAlias(rawSpecifier);
628
- if (!isPythonAbsoluteModuleSpecifier(specifier)) {
629
- continue;
630
- }
631
- addPythonImportEdge(edges, fromPath, resolvePythonModuleSpecifier(fromPath, specifier, pathLookup), "python-import", specifier);
632
- }
633
- continue;
634
- }
635
- const fromImportMatch = /^from\s+([.\w]+)\s+import\s+(.+)$/i.exec(line);
636
- if (!fromImportMatch) {
637
- continue;
638
- }
639
- const moduleSpecifier = fromImportMatch[1] ?? "";
640
- if (!isPythonModuleSpecifier(moduleSpecifier)) {
641
- continue;
642
- }
643
- const importedNames = splitPythonImportList(fromImportMatch[2] ?? "")
644
- .map(stripPythonAlias)
645
- .filter((name) => name !== "*" && isPythonIdentifier(name));
646
- const submoduleTargets = importedNames
647
- .map((name) => appendPythonImportedSpecifier(moduleSpecifier, name))
648
- .map((specifier) => ({
649
- specifier,
650
- target: resolvePythonModuleSpecifier(fromPath, specifier, pathLookup),
651
- }))
652
- .filter((item) => item.target);
653
- if (submoduleTargets.length > 0) {
654
- for (const { specifier, target } of submoduleTargets) {
655
- addPythonImportEdge(edges, fromPath, target, "python-from-import", specifier);
656
- }
657
- continue;
658
- }
659
- addPythonImportEdge(edges, fromPath, resolvePythonModuleSpecifier(fromPath, moduleSpecifier, pathLookup), "python-from-import", moduleSpecifier);
660
- }
661
- return edges;
662
- }
663
341
  function extractImportEdges(fromPath, content, pathLookup) {
664
342
  const edges = [];
665
343
  for (const { pattern, kind } of IMPORT_PATTERNS) {
@@ -1105,6 +783,169 @@ function extractConventionalRouteEvidence(fromPath, content) {
1105
783
  }
1106
784
  return routes.length > 0 ? routes : [{ path: routePath, handler: fromPath }];
1107
785
  }
786
+ // ---- Phase 4A: decorator / framework route detection ----
787
+ // Deterministic route patterns for NestJS, FastAPI, Flask, and Angular. These
788
+ // emit only the existing RouteEdge / route-handler-link shapes — no new
789
+ // planning-topology edge kinds. Each branch is gated on a framework marker so
790
+ // the patterns do not fire on unrelated decorators or object literals. An
791
+ // AST-based version can later move behind the analyzer seam; this is the
792
+ // regex floor for these frameworks.
793
+ const NEST_CONTROLLER_PATTERN = /@Controller\s*\(([\s\S]{0,200}?)\)/g;
794
+ const NEST_METHOD_DECORATOR_PATTERN = /@(Get|Post|Put|Patch|Delete|Options|Head|All)\s*\(\s*(?:["'`]([^"'`]*)["'`])?/g;
795
+ const PY_DECORATOR_METHOD_PATTERN = /@\s*[A-Za-z_]\w*\s*\.\s*(get|post|put|patch|delete|options|head|trace|websocket)\s*\(\s*["']([^"']+)["']/g;
796
+ const PY_ROUTE_DECORATOR_PATTERN = /@\s*[A-Za-z_]\w*\s*\.\s*(api_route|route)\s*\(\s*["']([^"']+)["']([\s\S]{0,200}?)\)/g;
797
+ const PY_METHODS_LIST_PATTERN = /methods\s*=\s*\[([^\]]*)\]/;
798
+ const PY_METHOD_LITERAL_PATTERN = /["']([A-Za-z]+)["']/g;
799
+ const ANGULAR_FILE_MARKER_PATTERN = /\b(?:RouterModule|provideRouter|loadChildren|loadComponent)\b|:\s*Routes\b/;
800
+ const ANGULAR_ROUTE_OBJECT_PATTERN = /\{[^{}]*?\bpath\s*:\s*["'`]([^"'`]*)["'`][^{}]*?\}/g;
801
+ const ANGULAR_ROUTE_KEY_PATTERN = /\b(?:component|loadChildren|loadComponent|redirectTo)\s*:/;
802
+ const ANGULAR_COMPONENT_PATTERN = /\b(?:component|loadComponent)\s*:\s*([A-Za-z_$][\w$]*)/;
803
+ const ANGULAR_LAZY_IMPORT_PATTERN = /\b(?:loadChildren|loadComponent)\s*:[\s\S]*?import\s*\(\s*["']([^"']+)["']\s*\)/;
804
+ const TS_LIKE_EXTENSION_PATTERN = /\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs)$/;
805
+ /** Join route segments (controller prefix + method path) into one clean path. */
806
+ function joinRouteSegments(...segments) {
807
+ return segments
808
+ .map((segment) => segment.trim().replace(/^\/+|\/+$/g, ""))
809
+ .filter((segment) => segment.length > 0)
810
+ .join("/");
811
+ }
812
+ /** Controller prefixes in document order, so each method can take the nearest. */
813
+ function nestControllerPrefixes(content) {
814
+ const prefixes = [];
815
+ NEST_CONTROLLER_PATTERN.lastIndex = 0;
816
+ for (const match of content.matchAll(NEST_CONTROLLER_PATTERN)) {
817
+ const arg = match[1] ?? "";
818
+ const pathProp = arg.match(/\bpath\s*:\s*["'`]([^"'`]*)["'`]/);
819
+ const firstString = arg.match(/["'`]([^"'`]*)["'`]/);
820
+ const prefix = pathProp?.[1] ?? firstString?.[1] ?? "";
821
+ prefixes.push({ index: match.index ?? 0, prefix });
822
+ }
823
+ return prefixes;
824
+ }
825
+ function collectNestRoutes(fromPath, content, routes) {
826
+ if (!content.includes("@Controller")) {
827
+ return;
828
+ }
829
+ const controllers = nestControllerPrefixes(content);
830
+ if (controllers.length === 0) {
831
+ return;
832
+ }
833
+ NEST_METHOD_DECORATOR_PATTERN.lastIndex = 0;
834
+ for (const match of content.matchAll(NEST_METHOD_DECORATOR_PATTERN)) {
835
+ const method = match[1];
836
+ if (!method)
837
+ continue;
838
+ const subPath = match[2] ?? "";
839
+ const at = match.index ?? 0;
840
+ let prefix = "";
841
+ for (const controller of controllers) {
842
+ if (controller.index <= at)
843
+ prefix = controller.prefix;
844
+ else
845
+ break;
846
+ }
847
+ routes.push({
848
+ path: normalizeRoutePath(joinRouteSegments(prefix, subPath)),
849
+ handler: fromPath,
850
+ method: method.toUpperCase(),
851
+ });
852
+ }
853
+ }
854
+ function pythonRouteMethods(args) {
855
+ const listMatch = args.match(PY_METHODS_LIST_PATTERN);
856
+ if (!listMatch?.[1])
857
+ return [];
858
+ PY_METHOD_LITERAL_PATTERN.lastIndex = 0;
859
+ return [...listMatch[1].matchAll(PY_METHOD_LITERAL_PATTERN)].map((method) => method[1].toUpperCase());
860
+ }
861
+ function collectPythonFrameworkRoutes(fromPath, content, routes) {
862
+ // FastAPI / Starlette: @app.get("/x"), @router.post("/y"), @router.websocket("/ws")
863
+ PY_DECORATOR_METHOD_PATTERN.lastIndex = 0;
864
+ for (const match of content.matchAll(PY_DECORATOR_METHOD_PATTERN)) {
865
+ const verb = match[1];
866
+ const routePath = match[2];
867
+ if (!verb || !routePath)
868
+ continue;
869
+ const method = verb.toUpperCase();
870
+ routes.push({
871
+ path: normalizeRoutePath(routePath),
872
+ handler: fromPath,
873
+ method: method === "WEBSOCKET" ? "WS" : method,
874
+ });
875
+ }
876
+ // FastAPI api_route + Flask route: @app.route("/x", methods=["GET","POST"])
877
+ PY_ROUTE_DECORATOR_PATTERN.lastIndex = 0;
878
+ for (const match of content.matchAll(PY_ROUTE_DECORATOR_PATTERN)) {
879
+ const routePath = match[2];
880
+ if (!routePath)
881
+ continue;
882
+ const methods = pythonRouteMethods(match[3] ?? "");
883
+ const path = normalizeRoutePath(routePath);
884
+ if (methods.length === 0) {
885
+ routes.push({ path, handler: fromPath, method: "GET" });
886
+ continue;
887
+ }
888
+ for (const method of methods) {
889
+ routes.push({ path, handler: fromPath, method });
890
+ }
891
+ }
892
+ }
893
+ function collectAngularRoutes(fromPath, content, pathLookup, calls, routes) {
894
+ if (!ANGULAR_FILE_MARKER_PATTERN.test(content)) {
895
+ return;
896
+ }
897
+ const bindings = extractImportBindings(fromPath, content, pathLookup);
898
+ ANGULAR_ROUTE_OBJECT_PATTERN.lastIndex = 0;
899
+ for (const match of content.matchAll(ANGULAR_ROUTE_OBJECT_PATTERN)) {
900
+ const body = match[0];
901
+ if (!ANGULAR_ROUTE_KEY_PATTERN.test(body)) {
902
+ continue;
903
+ }
904
+ const routePath = normalizeRoutePath(match[1] ?? "");
905
+ let handlerPath = fromPath;
906
+ let handlerExpression;
907
+ const lazyImport = body.match(ANGULAR_LAZY_IMPORT_PATTERN);
908
+ const component = body.match(ANGULAR_COMPONENT_PATTERN);
909
+ if (lazyImport?.[1]) {
910
+ const target = resolveSpecifier(fromPath, lazyImport[1], pathLookup) ??
911
+ resolveReferenceLiteral(fromPath, lazyImport[1], pathLookup);
912
+ if (target) {
913
+ handlerPath = target;
914
+ handlerExpression = lazyImport[1];
915
+ }
916
+ }
917
+ else if (component?.[1]) {
918
+ const binding = bindings.get(component[1]);
919
+ if (binding) {
920
+ handlerPath = binding.target;
921
+ handlerExpression = component[1];
922
+ }
923
+ }
924
+ routes.push({ path: routePath, handler: handlerPath });
925
+ if (handlerPath !== fromPath) {
926
+ calls.push(graphEdge({
927
+ from: fromPath,
928
+ to: handlerPath,
929
+ kind: "route-handler-link",
930
+ confidence: ROUTE_HANDLER_EDGE_CONFIDENCE,
931
+ reason: `Angular route '${routePath}' maps to '${handlerExpression ?? handlerPath}'.`,
932
+ }));
933
+ }
934
+ }
935
+ }
936
+ function extractFrameworkRouteEvidence(fromPath, content, pathLookup) {
937
+ const normalized = normalizeGraphPath(fromPath).toLowerCase();
938
+ const calls = [];
939
+ const routes = [];
940
+ if (normalized.endsWith(".py")) {
941
+ collectPythonFrameworkRoutes(fromPath, content, routes);
942
+ }
943
+ else if (TS_LIKE_EXTENSION_PATTERN.test(normalized)) {
944
+ collectNestRoutes(fromPath, content, routes);
945
+ collectAngularRoutes(fromPath, content, pathLookup, calls, routes);
946
+ }
947
+ return { calls, routes };
948
+ }
1108
949
  function fallbackRouteEdge(filePath) {
1109
950
  const normalized = filePath.toLowerCase();
1110
951
  if (normalized.includes("api/") || normalized.includes("route")) {
@@ -1239,7 +1080,7 @@ function extractPytestConftestLinks(pathLookup) {
1239
1080
  }
1240
1081
  export async function buildGraphBundleFromFs(repoManifest, root, disposition, options = {}) {
1241
1082
  const rootPath = resolve(root);
1242
- const dispositionMap = new Map(disposition?.files.map((item) => [item.path, item.status]) ?? []);
1083
+ const dispositionMap = buildDispositionMap(disposition);
1243
1084
  const fileContents = {};
1244
1085
  await Promise.all(repoManifest.files.map(async (file) => {
1245
1086
  const status = dispositionMap.get(file.path);
@@ -1267,7 +1108,7 @@ export function buildGraphBundle(repoManifest, disposition, options = {}) {
1267
1108
  const calls = [];
1268
1109
  const references = [];
1269
1110
  const routes = [];
1270
- const dispositionMap = new Map(disposition?.files.map((item) => [item.path, item.status]) ?? []);
1111
+ const dispositionMap = buildDispositionMap(disposition);
1271
1112
  const pathLookup = buildPathLookup(repoManifest, dispositionMap);
1272
1113
  for (const file of repoManifest.files) {
1273
1114
  const status = dispositionMap.get(file.path);
@@ -1327,6 +1168,9 @@ export function buildGraphBundle(repoManifest, disposition, options = {}) {
1327
1168
  const registeredRoutes = extractRegisteredRouteEvidence(file.path, content, pathLookup);
1328
1169
  calls.push(...registeredRoutes.calls);
1329
1170
  fileRoutes.push(...registeredRoutes.routes);
1171
+ const frameworkRoutes = extractFrameworkRouteEvidence(file.path, content, pathLookup);
1172
+ calls.push(...frameworkRoutes.calls);
1173
+ fileRoutes.push(...frameworkRoutes.routes);
1330
1174
  }
1331
1175
  fileRoutes.push(...extractConventionalRouteEvidence(file.path, content));
1332
1176
  if (fileRoutes.length === 0) {
@@ -1,4 +1,4 @@
1
- import type { GraphEdge } from "../types/graph.js";
1
+ import type { GraphEdge } from "@audit-tools/shared";
2
2
  export declare function extractPackageEntrypointEdges(fromPath: string, content: string, pathLookup: Map<string, string>): GraphEdge[];
3
3
  export declare function extractPackageScriptEdges(fromPath: string, content: string, pathLookup: Map<string, string>): GraphEdge[];
4
4
  export declare function isGoWorkspaceManifestPath(path: string): boolean;
@@ -1,4 +1,4 @@
1
- import type { GraphEdge } from "../types/graph.js";
1
+ import type { GraphEdge } from "@audit-tools/shared";
2
2
  export declare function normalizeGraphPath(path: string): string;
3
3
  export declare function graphLookupKey(path: string): string;
4
4
  export declare function resolveCandidate(candidate: string, pathLookup: Map<string, string>): string | undefined;
@@ -0,0 +1,18 @@
1
+ import type { GraphEdge } from "@audit-tools/shared";
2
+ export declare function isPythonSourcePath(path: string): boolean;
3
+ /**
4
+ * Resolve a single `import <spec>` module specifier to a repo file, or
5
+ * undefined. Shared with the tree-sitter Python analyzer so AST-extracted
6
+ * imports resolve to exactly the same targets as the regex floor.
7
+ */
8
+ export declare function resolvePythonImportTarget(fromPath: string, specifier: string, pathLookup: Map<string, string>): string | undefined;
9
+ /**
10
+ * Resolve a `from <module> import <names>` statement to repo files. Mirrors the
11
+ * floor: prefer submodule files (`module.name`), else the module itself. Shared
12
+ * with the tree-sitter Python analyzer.
13
+ */
14
+ export declare function resolvePythonFromImportTargets(fromPath: string, moduleSpecifier: string, importedNames: string[], pathLookup: Map<string, string>): Array<{
15
+ specifier: string;
16
+ target: string;
17
+ }>;
18
+ export declare function extractPythonImportEdges(fromPath: string, content: string, pathLookup: Map<string, string>): GraphEdge[];