auditor-lambda 0.3.41 → 0.6.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 (78) hide show
  1. package/dist/cli/dispatch.js +5 -1
  2. package/dist/cli/prompts.d.ts +19 -0
  3. package/dist/cli/prompts.js +95 -0
  4. package/dist/cli/steps.d.ts +1 -1
  5. package/dist/cli.js +398 -78
  6. package/dist/extractors/analyzers/css.d.ts +2 -0
  7. package/dist/extractors/analyzers/css.js +101 -0
  8. package/dist/extractors/analyzers/html.d.ts +2 -0
  9. package/dist/extractors/analyzers/html.js +92 -0
  10. package/dist/extractors/analyzers/merge.d.ts +14 -0
  11. package/dist/extractors/analyzers/merge.js +85 -0
  12. package/dist/extractors/analyzers/python.d.ts +2 -0
  13. package/dist/extractors/analyzers/python.js +104 -0
  14. package/dist/extractors/analyzers/registry.d.ts +33 -0
  15. package/dist/extractors/analyzers/registry.js +100 -0
  16. package/dist/extractors/analyzers/resourceUrl.d.ts +7 -0
  17. package/dist/extractors/analyzers/resourceUrl.js +25 -0
  18. package/dist/extractors/analyzers/sql.d.ts +2 -0
  19. package/dist/extractors/analyzers/sql.js +19 -0
  20. package/dist/extractors/analyzers/treeSitter.d.ts +34 -0
  21. package/dist/extractors/analyzers/treeSitter.js +111 -0
  22. package/dist/extractors/analyzers/types.d.ts +53 -0
  23. package/dist/extractors/analyzers/types.js +1 -0
  24. package/dist/extractors/analyzers/typescript.d.ts +2 -0
  25. package/dist/extractors/analyzers/typescript.js +257 -0
  26. package/dist/extractors/disposition.js +8 -1
  27. package/dist/extractors/graph.d.ts +1 -0
  28. package/dist/extractors/graph.js +167 -1
  29. package/dist/extractors/graphPythonImports.d.ts +15 -0
  30. package/dist/extractors/graphPythonImports.js +36 -0
  31. package/dist/extractors/pathPatterns.d.ts +6 -0
  32. package/dist/extractors/pathPatterns.js +8 -0
  33. package/dist/io/artifacts.d.ts +13 -1
  34. package/dist/io/artifacts.js +19 -3
  35. package/dist/mcp/server.js +3 -3
  36. package/dist/orchestrator/advance.d.ts +20 -0
  37. package/dist/orchestrator/advance.js +61 -2
  38. package/dist/orchestrator/dependencyMap.js +27 -0
  39. package/dist/orchestrator/edgeReasoning.d.ts +39 -0
  40. package/dist/orchestrator/edgeReasoning.js +125 -0
  41. package/dist/orchestrator/executors.js +11 -1
  42. package/dist/orchestrator/graphEnrichmentExecutor.d.ts +29 -0
  43. package/dist/orchestrator/graphEnrichmentExecutor.js +196 -0
  44. package/dist/orchestrator/internalExecutors.d.ts +10 -1
  45. package/dist/orchestrator/internalExecutors.js +89 -11
  46. package/dist/orchestrator/localCommands.js +6 -25
  47. package/dist/orchestrator/nextStep.js +2 -0
  48. package/dist/orchestrator/reviewPackets.d.ts +37 -4
  49. package/dist/orchestrator/reviewPackets.js +93 -46
  50. package/dist/orchestrator/runtimeValidation.js +4 -31
  51. package/dist/orchestrator/scope.d.ts +62 -0
  52. package/dist/orchestrator/scope.js +227 -0
  53. package/dist/orchestrator/state.js +2 -0
  54. package/dist/reporting/synthesis.d.ts +37 -2
  55. package/dist/reporting/synthesis.js +95 -16
  56. package/dist/reporting/synthesisNarrativePrompt.d.ts +7 -0
  57. package/dist/reporting/synthesisNarrativePrompt.js +60 -0
  58. package/dist/reporting/workBlocks.d.ts +2 -10
  59. package/dist/supervisor/operatorHandoff.d.ts +1 -1
  60. package/dist/supervisor/operatorHandoff.js +26 -16
  61. package/dist/supervisor/sessionConfig.d.ts +8 -1
  62. package/dist/supervisor/sessionConfig.js +22 -1
  63. package/dist/types/analyzerCapability.d.ts +16 -0
  64. package/dist/types/analyzerCapability.js +1 -0
  65. package/dist/types/auditScope.d.ts +43 -0
  66. package/dist/types/auditScope.js +14 -0
  67. package/dist/types/synthesisNarrative.d.ts +7 -0
  68. package/dist/types/synthesisNarrative.js +5 -0
  69. package/dist/types.d.ts +2 -19
  70. package/dist/validation/artifacts.js +9 -0
  71. package/dist/validation/sessionConfig.js +24 -1
  72. package/docs/contracts.md +10 -3
  73. package/package.json +4 -2
  74. package/schemas/analyzer_capability.schema.json +47 -0
  75. package/schemas/audit_findings.schema.json +141 -0
  76. package/schemas/finding.schema.json +2 -1
  77. package/schemas/graph_bundle.schema.json +5 -0
  78. package/schemas/scope.schema.json +46 -0
@@ -1,24 +1,70 @@
1
1
  import { createHash } from "node:crypto";
2
- import { isRecord } from "@audit-tools/shared";
2
+ import { estimateTokensFromBytes, isRecord } from "@audit-tools/shared";
3
3
  import { LENS_ORDER, priorityRank, sortLenses } from "./auditTaskUtils.js";
4
4
  import { UnionFind } from "./unionFind.js";
5
5
  const DEFAULT_MAX_TASKS_PER_PACKET = 0;
6
6
  const DEFAULT_TARGET_PACKET_LINES = 8000;
7
7
  export const ESTIMATED_TOKENS_PER_LINE = 4;
8
8
  export const ESTIMATED_PACKET_PROMPT_TOKENS = 900;
9
- export function estimateTaskGroupTokens(tasks) {
10
- let totalLines = 0;
9
+ // Default per-packet content-token budget. Kept equal to the legacy
10
+ // line-target × per-line estimate so byte-derived sizing lands on the same
11
+ // thresholds as the old line-based sizing when the line fallback is in effect.
12
+ const DEFAULT_TARGET_PACKET_TOKENS = DEFAULT_TARGET_PACKET_LINES * ESTIMATED_TOKENS_PER_LINE;
13
+ /**
14
+ * Build a path → size_bytes index from a repo manifest. Byte counts are
15
+ * recorded during intake, so this never reads files. Review packet token
16
+ * estimates are derived from these bytes (Phase 2) instead of counted lines.
17
+ */
18
+ export function sizeIndexFromManifest(repoManifest) {
19
+ if (!repoManifest)
20
+ return {};
21
+ return Object.fromEntries(repoManifest.files.map((file) => [file.path, file.size_bytes]));
22
+ }
23
+ /**
24
+ * Estimated content tokens for a single file. Prefers a byte-based estimate
25
+ * from `sizeIndex` (sourced from the repo manifest); falls back to the legacy
26
+ * line-based estimate when no positive byte count is available (e.g. manually
27
+ * built tasks in tests, or paths absent from the manifest).
28
+ */
29
+ function pathContentTokens(owner, path, sizeIndex, lineIndex) {
30
+ const bytes = sizeIndex?.[path];
31
+ if (typeof bytes === "number" && bytes > 0) {
32
+ return estimateTokensFromBytes(bytes);
33
+ }
34
+ const lines = owner?.file_line_counts?.[path] ?? lineIndex?.[path] ?? 0;
35
+ return lines * ESTIMATED_TOKENS_PER_LINE;
36
+ }
37
+ /** Estimated content tokens for one task across all of its files. */
38
+ function taskContentTokens(task, sizeIndex, lineIndex) {
39
+ return task.file_paths.reduce((sum, path) => sum + pathContentTokens(task, path, sizeIndex, lineIndex), 0);
40
+ }
41
+ /**
42
+ * Estimated content tokens across a set of file paths, resolving an owning task
43
+ * per path so the line fallback can read its `file_line_counts`. Shared files
44
+ * are counted once.
45
+ */
46
+ function fileGroupContentTokens(filePaths, tasks, sizeIndex, lineIndex) {
47
+ let total = 0;
48
+ for (const path of filePaths) {
49
+ const owner = tasks.find((task) => task.file_paths.includes(path));
50
+ total += pathContentTokens(owner, path, sizeIndex, lineIndex);
51
+ }
52
+ return total;
53
+ }
54
+ export function estimateTaskGroupTokens(tasks, sizeIndex, lineIndex) {
55
+ let contentTokens = 0;
11
56
  for (const task of tasks) {
12
- if (task.file_line_counts) {
13
- for (const count of Object.values(task.file_line_counts)) {
14
- totalLines += count;
15
- }
16
- }
57
+ contentTokens += taskContentTokens(task, sizeIndex, lineIndex);
17
58
  }
18
- return ESTIMATED_PACKET_PROMPT_TOKENS + totalLines * ESTIMATED_TOKENS_PER_LINE;
59
+ return ESTIMATED_PACKET_PROMPT_TOKENS + contentTokens;
19
60
  }
20
61
  const PACKET_EXPANSION_MIN_CONFIDENCE = 0.65;
21
- const HIGH_FAN_DEGREE_THRESHOLD = 12;
62
+ /**
63
+ * Fan-in / fan-out degree above which a node is treated as a hub. Exported so
64
+ * the Phase 3 delta-scope expansion skips the same hubs that packet planning
65
+ * skips, preventing scope blow-up through highly-connected modules.
66
+ */
67
+ export const HIGH_FAN_DEGREE_THRESHOLD = 12;
22
68
  const HIGH_FAN_EXPANSION_CONFIDENCE = 0.99;
23
69
  const MAX_PACKET_KEY_EDGES = 8;
24
70
  const MAX_PACKET_BOUNDARY_FILES = 12;
@@ -86,10 +132,10 @@ function buildTaskGroups(tasks) {
86
132
  }
87
133
  return groups;
88
134
  }
89
- function normalizeGraphPath(path) {
135
+ export function normalizeGraphPath(path) {
90
136
  return path.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
91
137
  }
92
- function collectGraphEdges(graphBundle) {
138
+ export function collectGraphEdges(graphBundle) {
93
139
  if (!graphBundle?.graphs) {
94
140
  return [];
95
141
  }
@@ -125,7 +171,7 @@ function collectGraphEdges(graphBundle) {
125
171
  }
126
172
  return edges;
127
173
  }
128
- function graphEdgeConfidence(edge) {
174
+ export function graphEdgeConfidence(edge) {
129
175
  if (typeof edge.confidence === "number" && Number.isFinite(edge.confidence)) {
130
176
  return Math.min(1, Math.max(0, edge.confidence));
131
177
  }
@@ -140,7 +186,7 @@ function graphEdgeConfidence(edge) {
140
186
  function isConcreteGraphEdge(edge) {
141
187
  return edge.kind !== "heuristic-container-edge";
142
188
  }
143
- function buildGraphDegreeIndex(edges) {
189
+ export function buildGraphDegreeIndex(edges) {
144
190
  const fanIn = new Map();
145
191
  const fanOut = new Map();
146
192
  for (const edge of edges) {
@@ -346,13 +392,11 @@ function buildBoundedClusterEdges(params) {
346
392
  const allFiles = new Set(componentEntries.flatMap((entry) => [...entry.filePaths]));
347
393
  const totalTasks = componentEntries.reduce((sum, entry) => sum + entry.taskCount, 0);
348
394
  const clusterTasks = entries.flatMap((entry) => entry.tasks);
349
- const totalLines = [...allFiles].reduce((sum, path) => {
350
- const owner = clusterTasks.find((task) => task.file_paths.includes(path));
351
- return sum + (owner ? lineCountForPath(owner, path, params.lineIndex) : 0);
352
- }, 0);
395
+ const totalContentTokens = fileGroupContentTokens(allFiles, clusterTasks, params.sizeIndex, params.lineIndex);
353
396
  if (allFiles.size > MAX_SUBSYSTEM_CLUSTER_FILES ||
354
397
  totalTasks > MAX_SUBSYSTEM_CLUSTER_TASKS ||
355
- totalLines > (params.targetPacketLines ?? DEFAULT_TARGET_PACKET_LINES)) {
398
+ totalContentTokens >
399
+ (params.targetPacketTokens ?? DEFAULT_TARGET_PACKET_TOKENS)) {
356
400
  continue;
357
401
  }
358
402
  for (let index = 1; index < componentEntries.length; index++) {
@@ -370,7 +414,7 @@ function buildBoundedClusterEdges(params) {
370
414
  }
371
415
  return clusterEdges.sort(compareGraphEdges);
372
416
  }
373
- function buildSubsystemClusterEdges(groups, graphEdges, lineIndex, targetPacketLines = DEFAULT_TARGET_PACKET_LINES) {
417
+ function buildSubsystemClusterEdges(groups, graphEdges, lineIndex, sizeIndex, targetPacketTokens = DEFAULT_TARGET_PACKET_TOKENS) {
374
418
  return buildBoundedClusterEdges({
375
419
  groups,
376
420
  graphEdges,
@@ -379,7 +423,8 @@ function buildSubsystemClusterEdges(groups, graphEdges, lineIndex, targetPacketL
379
423
  edgeConfidence: SUBSYSTEM_CLUSTER_CONFIDENCE,
380
424
  reasonForCluster: (root, fileCount) => `Bounded subsystem cluster '${root}' groups ${fileCount} file(s) without stronger graph evidence.`,
381
425
  lineIndex,
382
- targetPacketLines,
426
+ sizeIndex,
427
+ targetPacketTokens,
383
428
  });
384
429
  }
385
430
  function packageManifestRoot(path) {
@@ -479,7 +524,7 @@ function packageOwnershipRootForTasks(tasks, packageRoots) {
479
524
  const roots = new Set(rootsForFiles);
480
525
  return roots.size === 1 ? [...roots][0] : undefined;
481
526
  }
482
- function buildPackageOwnershipClusterEdges(groups, graphEdges, lineIndex, targetPacketLines = DEFAULT_TARGET_PACKET_LINES) {
527
+ function buildPackageOwnershipClusterEdges(groups, graphEdges, lineIndex, sizeIndex, targetPacketTokens = DEFAULT_TARGET_PACKET_TOKENS) {
483
528
  const packageRoots = collectPackageOwnershipRoots(groups, graphEdges);
484
529
  if (packageRoots.size === 0) {
485
530
  return [];
@@ -492,7 +537,8 @@ function buildPackageOwnershipClusterEdges(groups, graphEdges, lineIndex, target
492
537
  edgeConfidence: PACKAGE_OWNERSHIP_CLUSTER_CONFIDENCE,
493
538
  reasonForCluster: (root, fileCount) => `Package ownership root '${root}' groups ${fileCount} file(s) across bounded package subdirectories.`,
494
539
  lineIndex,
495
- targetPacketLines,
540
+ sizeIndex,
541
+ targetPacketTokens,
496
542
  });
497
543
  }
498
544
  function collectModuleOwnershipRoots(groups, graphEdges) {
@@ -538,7 +584,7 @@ function moduleOwnershipRootForTasks(tasks, moduleRoots) {
538
584
  const roots = new Set(rootsForFiles);
539
585
  return roots.size === 1 ? [...roots][0] : undefined;
540
586
  }
541
- function buildModuleOwnershipClusterEdges(groups, graphEdges, lineIndex, targetPacketLines = DEFAULT_TARGET_PACKET_LINES) {
587
+ function buildModuleOwnershipClusterEdges(groups, graphEdges, lineIndex, sizeIndex, targetPacketTokens = DEFAULT_TARGET_PACKET_TOKENS) {
542
588
  const moduleRoots = collectModuleOwnershipRoots(groups, graphEdges);
543
589
  if (moduleRoots.size === 0) {
544
590
  return [];
@@ -557,7 +603,8 @@ function buildModuleOwnershipClusterEdges(groups, graphEdges, lineIndex, targetP
557
603
  : `Module ownership root '${root}' from project configuration groups ${fileCount} file(s) across bounded subdirectories.`;
558
604
  },
559
605
  lineIndex,
560
- targetPacketLines,
606
+ sizeIndex,
607
+ targetPacketTokens,
561
608
  });
562
609
  }
563
610
  function buildEntrypointFlowBridgeEdges(groups, graphEdges, graphBundle) {
@@ -644,18 +691,18 @@ function buildEntrypointFlowBridgeEdges(groups, graphEdges, graphBundle) {
644
691
  }
645
692
  return [...bridgeEdges.values()].sort(compareGraphEdges);
646
693
  }
647
- function buildPlanningGraphEdges(groups, graphEdges, graphBundle, lineIndex, targetPacketLines = DEFAULT_TARGET_PACKET_LINES) {
694
+ function buildPlanningGraphEdges(groups, graphEdges, graphBundle, lineIndex, sizeIndex, targetPacketTokens = DEFAULT_TARGET_PACKET_TOKENS) {
648
695
  const bridgeEdges = buildEntrypointFlowBridgeEdges(groups, graphEdges, graphBundle);
649
696
  const graphWithBridges = bridgeEdges.length > 0 ? [...graphEdges, ...bridgeEdges] : graphEdges;
650
- const subsystemEdges = buildSubsystemClusterEdges(groups, graphWithBridges, lineIndex, targetPacketLines);
697
+ const subsystemEdges = buildSubsystemClusterEdges(groups, graphWithBridges, lineIndex, sizeIndex, targetPacketTokens);
651
698
  const graphWithSubsystems = subsystemEdges.length > 0
652
699
  ? [...graphWithBridges, ...subsystemEdges]
653
700
  : graphWithBridges;
654
- const packageOwnershipEdges = buildPackageOwnershipClusterEdges(groups, graphWithSubsystems, lineIndex, targetPacketLines);
701
+ const packageOwnershipEdges = buildPackageOwnershipClusterEdges(groups, graphWithSubsystems, lineIndex, sizeIndex, targetPacketTokens);
655
702
  const graphWithPackageOwnership = packageOwnershipEdges.length > 0
656
703
  ? [...graphWithSubsystems, ...packageOwnershipEdges]
657
704
  : graphWithSubsystems;
658
- const moduleOwnershipEdges = buildModuleOwnershipClusterEdges(groups, graphWithPackageOwnership, lineIndex, targetPacketLines);
705
+ const moduleOwnershipEdges = buildModuleOwnershipClusterEdges(groups, graphWithPackageOwnership, lineIndex, sizeIndex, targetPacketTokens);
659
706
  return moduleOwnershipEdges.length > 0
660
707
  ? [...graphWithPackageOwnership, ...moduleOwnershipEdges]
661
708
  : graphWithPackageOwnership;
@@ -781,7 +828,8 @@ function chunkPacketTasks(tasks, options) {
781
828
  let current = [];
782
829
  for (const task of tasks.sort(compareTasksForPacket)) {
783
830
  const isolatedLargeFileTask = task.file_paths.length === 1 &&
784
- taskLineCount(task, options.lineIndex) > options.targetPacketLines;
831
+ taskContentTokens(task, options.sizeIndex, options.lineIndex) >
832
+ options.targetPacketTokens;
785
833
  if (isolatedLargeFileTask) {
786
834
  if (current.length > 0) {
787
835
  chunks.push(current);
@@ -792,13 +840,10 @@ function chunkPacketTasks(tasks, options) {
792
840
  }
793
841
  const candidate = [...current, task];
794
842
  const uniquePaths = new Set(candidate.flatMap((item) => item.file_paths));
795
- const candidateLines = [...uniquePaths].reduce((sum, path) => {
796
- const owner = candidate.find((item) => item.file_paths.includes(path));
797
- return sum + (owner ? lineCountForPath(owner, path, options.lineIndex) : 0);
798
- }, 0);
843
+ const candidateContentTokens = fileGroupContentTokens(uniquePaths, candidate, options.sizeIndex, options.lineIndex);
799
844
  const wouldExceedTaskCount = options.maxTasksPerPacket > 0 && current.length > 0 && candidate.length > options.maxTasksPerPacket;
800
- const wouldExceedLines = current.length > 0 && candidateLines > options.targetPacketLines;
801
- if (wouldExceedTaskCount || wouldExceedLines) {
845
+ const wouldExceedTokens = current.length > 0 && candidateContentTokens > options.targetPacketTokens;
846
+ if (wouldExceedTaskCount || wouldExceedTokens) {
802
847
  chunks.push(current);
803
848
  current = [];
804
849
  }
@@ -820,7 +865,7 @@ function mergeGraphConnectedGroups(groups, graphEdges) {
820
865
  }
821
866
  return [...merged.values()];
822
867
  }
823
- function buildPacket(tasks, packetIndex, lineIndex, graphEdges = [], graphBundle) {
868
+ function buildPacket(tasks, packetIndex, lineIndex, sizeIndex, graphEdges = [], graphBundle) {
824
869
  const filePaths = [...new Set(tasks.flatMap((task) => task.file_paths))].sort((a, b) => a.localeCompare(b));
825
870
  const graphContext = buildPacketGraphContext(filePaths, graphEdges, graphBundle);
826
871
  const fileLineCounts = Object.fromEntries(filePaths.map((path) => {
@@ -828,6 +873,8 @@ function buildPacket(tasks, packetIndex, lineIndex, graphEdges = [], graphBundle
828
873
  return [path, owner ? lineCountForPath(owner, path, lineIndex) : 0];
829
874
  }));
830
875
  const totalLines = Object.values(fileLineCounts).reduce((sum, value) => sum + value, 0);
876
+ const estimatedTokens = ESTIMATED_PACKET_PROMPT_TOKENS +
877
+ fileGroupContentTokens(filePaths, tasks, sizeIndex, lineIndex);
831
878
  const priority = tasks.reduce((highest, task) => priorityRank(task.priority) > priorityRank(highest)
832
879
  ? normalizePriority(task.priority)
833
880
  : highest, "low");
@@ -863,19 +910,18 @@ function buildPacket(tasks, packetIndex, lineIndex, graphEdges = [], graphBundle
863
910
  : undefined,
864
911
  quality: graphContext.quality,
865
912
  rationale: `${baseRationale}${graphRationale}`,
866
- estimated_tokens: ESTIMATED_PACKET_PROMPT_TOKENS + totalLines * ESTIMATED_TOKENS_PER_LINE,
913
+ estimated_tokens: estimatedTokens,
867
914
  };
868
915
  }
869
916
  function buildReviewPacketPlanningData(tasks, options = {}) {
870
917
  const maxTasksPerPacket = options.maxTasksPerPacket ?? DEFAULT_MAX_TASKS_PER_PACKET;
871
- const configuredTargetLines = options.targetPacketLines ?? DEFAULT_TARGET_PACKET_LINES;
872
- const targetPacketLines = options.maxContextTokens != null
873
- ? Math.min(configuredTargetLines, Math.max(1, Math.floor((options.maxContextTokens - ESTIMATED_PACKET_PROMPT_TOKENS) /
874
- ESTIMATED_TOKENS_PER_LINE)))
875
- : configuredTargetLines;
918
+ const configuredTargetTokens = options.targetPacketTokens ?? DEFAULT_TARGET_PACKET_TOKENS;
919
+ const targetPacketTokens = options.maxContextTokens != null
920
+ ? Math.min(configuredTargetTokens, Math.max(1, options.maxContextTokens - ESTIMATED_PACKET_PROMPT_TOKENS))
921
+ : configuredTargetTokens;
876
922
  const graphEdges = collectGraphEdges(options.graphBundle);
877
923
  const groups = buildTaskGroups(tasks);
878
- const planningGraphEdges = buildPlanningGraphEdges(groups, graphEdges, options.graphBundle, options.lineIndex, targetPacketLines);
924
+ const planningGraphEdges = buildPlanningGraphEdges(groups, graphEdges, options.graphBundle, options.lineIndex, options.sizeIndex, targetPacketTokens);
879
925
  const packets = [];
880
926
  let packetIndex = 0;
881
927
  const groupedTasks = mergeGraphConnectedGroups(groups, planningGraphEdges).sort((a, b) => {
@@ -888,10 +934,11 @@ function buildReviewPacketPlanningData(tasks, options = {}) {
888
934
  for (const group of groupedTasks) {
889
935
  for (const chunk of chunkPacketTasks(group, {
890
936
  lineIndex: options.lineIndex,
937
+ sizeIndex: options.sizeIndex,
891
938
  maxTasksPerPacket,
892
- targetPacketLines,
939
+ targetPacketTokens,
893
940
  })) {
894
- packets.push(buildPacket(chunk, packetIndex, options.lineIndex, planningGraphEdges, options.graphBundle));
941
+ packets.push(buildPacket(chunk, packetIndex, options.lineIndex, options.sizeIndex, planningGraphEdges, options.graphBundle));
895
942
  packetIndex += 1;
896
943
  }
897
944
  }
@@ -1,4 +1,4 @@
1
- import { access, readFile } from "node:fs/promises";
1
+ import { discoverProjectCommands } from "@audit-tools/shared";
2
2
  function checksForFlow(requiredLenses) {
3
3
  const checks = [];
4
4
  if (requiredLenses.includes("security")) {
@@ -15,37 +15,10 @@ function checksForFlow(requiredLenses) {
15
15
  }
16
16
  return checks;
17
17
  }
18
- async function exists(path) {
19
- try {
20
- await access(path);
21
- return true;
22
- }
23
- catch {
24
- return false;
25
- }
26
- }
27
18
  export async function discoverRuntimeValidationCommand(root) {
28
- const packageJsonPath = `${root}/package.json`;
29
- if (await exists(packageJsonPath)) {
30
- try {
31
- const packageJson = JSON.parse(await readFile(packageJsonPath, "utf8"));
32
- const testScript = packageJson.scripts?.test?.trim();
33
- if (testScript &&
34
- !/no test specified/i.test(testScript)) {
35
- return ["npm", "test"];
36
- }
37
- }
38
- catch {
39
- // ignore unreadable package.json for runtime discovery
40
- }
41
- }
42
- if (await exists(`${root}/go.mod`)) {
43
- return ["go", "test", "./..."];
44
- }
45
- if (await exists(`${root}/pyproject.toml`) || await exists(`${root}/pytest.ini`)) {
46
- return ["python", "-m", "pytest"];
47
- }
48
- return undefined;
19
+ // Shared discovery (Node test script → Go → Python) is the single source of
20
+ // truth; the runtime-validation command is the discovered test command.
21
+ return discoverProjectCommands(root).test;
49
22
  }
50
23
  export function buildRuntimeValidationTasks(params) {
51
24
  if (!params.command) {
@@ -0,0 +1,62 @@
1
+ import type { GraphBundle } from "@audit-tools/shared";
2
+ import type { ArtifactBundle } from "../io/artifacts.js";
3
+ import type { CoverageMatrix } from "../types.js";
4
+ import type { AuditScopeBudget, AuditScopeManifest } from "../types/auditScope.js";
5
+ /** Default cap on in-scope files (seeds + expanded) before expansion stops. */
6
+ export declare const DEFAULT_SCOPE_MAX_FILES = 200;
7
+ /** Graph edges below this confidence are never traversed during expansion. */
8
+ export declare const SCOPE_EDGE_CONFIDENCE_FLOOR = 0.5;
9
+ /**
10
+ * Expansion stops along a path once the accumulated path-confidence (the product
11
+ * of the traversed edge confidences) drops below this floor. With no fixed hop
12
+ * count, this — together with hub-skipping and the file budget — bounds the
13
+ * frontier deterministically.
14
+ */
15
+ export declare const SCOPE_MIN_FRONTIER_CONFIDENCE = 0.5;
16
+ export interface ComputeAuditScopeInput {
17
+ /** The git ref the delta is measured against. */
18
+ since: string;
19
+ /** Raw changed paths (git output, posix-relative). */
20
+ changed: string[];
21
+ /** Canonical auditable file paths (repo-manifest paths, non-excluded). */
22
+ includedFiles: string[];
23
+ /** Dependency graph used to expand from seeds to neighbours. */
24
+ graphBundle?: GraphBundle;
25
+ budget?: AuditScopeBudget;
26
+ }
27
+ /**
28
+ * Deterministic priority-frontier expansion (Phase 3). Starting from the changed
29
+ * files (seeds), walk the dependency graph outward, always visiting the neighbour
30
+ * with the highest accumulated path-confidence first (tie-broken by path). High
31
+ * fan-in/out hubs are skipped so a single change near a hub does not drag the
32
+ * whole repo into scope, low-confidence edges are dropped, and expansion halts at
33
+ * the file budget or when the best remaining frontier confidence falls below the
34
+ * floor. Same inputs → identical scope.
35
+ */
36
+ export declare function computeAuditScope(input: ComputeAuditScopeInput): AuditScopeManifest;
37
+ /** A full-audit scope (the default, and every fallback). */
38
+ export declare function fullAuditScope(budget?: AuditScopeBudget, droppedNote?: string): AuditScopeManifest;
39
+ export interface ResolveAuditScopeInput {
40
+ root?: string;
41
+ /** The `--since` ref, if any. Absent/empty → full audit. */
42
+ since?: string;
43
+ bundle: ArtifactBundle;
44
+ budget?: AuditScopeBudget;
45
+ }
46
+ /**
47
+ * Resolve the scope for a planning run. Returns a full-audit scope unless a
48
+ * `--since` ref was supplied against a real git repository; an unusable ref or
49
+ * missing root degrades to a full audit with an honest note. Reads the auditable
50
+ * file set from the repo manifest + disposition (the same lookup the graph
51
+ * extractor uses) and the dependency graph from the bundle.
52
+ */
53
+ export declare function resolveAuditScope(input: ResolveAuditScopeInput): AuditScopeManifest;
54
+ /**
55
+ * Apply a delta scope to a freshly-built coverage matrix. In-scope files (seeds
56
+ * + expanded neighbours) keep their fresh `pending` status to be re-audited.
57
+ * Out-of-scope files inherit a prior `complete` record verbatim when present (so
58
+ * previously-finished work is preserved, not re-run), and are otherwise excluded
59
+ * from this run with `classification_status: "out_of_scope_delta"`. Deterministic
60
+ * exclusions (non-auditable/trivial) are left untouched. A full scope is a no-op.
61
+ */
62
+ export declare function applyScopeToCoverage(coverage: CoverageMatrix, scope: AuditScopeManifest, priorCoverage?: CoverageMatrix): CoverageMatrix;
@@ -0,0 +1,227 @@
1
+ import { changedFiles, gitRefExists, isGitRepo } from "@audit-tools/shared";
2
+ import { buildDispositionMap } from "../extractors/disposition.js";
3
+ import { buildPathLookup } from "../extractors/graph.js";
4
+ import { HIGH_FAN_DEGREE_THRESHOLD, buildGraphDegreeIndex, collectGraphEdges, graphEdgeConfidence, normalizeGraphPath, } from "./reviewPackets.js";
5
+ /** Default cap on in-scope files (seeds + expanded) before expansion stops. */
6
+ export const DEFAULT_SCOPE_MAX_FILES = 200;
7
+ /** Graph edges below this confidence are never traversed during expansion. */
8
+ export const SCOPE_EDGE_CONFIDENCE_FLOOR = 0.5;
9
+ /**
10
+ * Expansion stops along a path once the accumulated path-confidence (the product
11
+ * of the traversed edge confidences) drops below this floor. With no fixed hop
12
+ * count, this — together with hub-skipping and the file budget — bounds the
13
+ * frontier deterministically.
14
+ */
15
+ export const SCOPE_MIN_FRONTIER_CONFIDENCE = 0.5;
16
+ /**
17
+ * Deterministic priority-frontier expansion (Phase 3). Starting from the changed
18
+ * files (seeds), walk the dependency graph outward, always visiting the neighbour
19
+ * with the highest accumulated path-confidence first (tie-broken by path). High
20
+ * fan-in/out hubs are skipped so a single change near a hub does not drag the
21
+ * whole repo into scope, low-confidence edges are dropped, and expansion halts at
22
+ * the file budget or when the best remaining frontier confidence falls below the
23
+ * floor. Same inputs → identical scope.
24
+ */
25
+ export function computeAuditScope(input) {
26
+ const maxFiles = input.budget?.max_files ?? DEFAULT_SCOPE_MAX_FILES;
27
+ // normalized graph key -> canonical (repo-manifest) path for auditable files.
28
+ const canonicalByNorm = new Map();
29
+ for (const file of input.includedFiles) {
30
+ const key = normalizeGraphPath(file);
31
+ if (!canonicalByNorm.has(key)) {
32
+ canonicalByNorm.set(key, file);
33
+ }
34
+ }
35
+ // Seeds = changed files that are auditable (present in the manifest). Changed
36
+ // files that are excluded, deleted, or otherwise absent simply drop out.
37
+ const seedKeys = [];
38
+ const seedSeen = new Set();
39
+ for (const path of input.changed) {
40
+ const key = normalizeGraphPath(path);
41
+ if (canonicalByNorm.has(key) && !seedSeen.has(key)) {
42
+ seedSeen.add(key);
43
+ seedKeys.push(key);
44
+ }
45
+ }
46
+ const edges = collectGraphEdges(input.graphBundle);
47
+ const degree = buildGraphDegreeIndex(edges);
48
+ const isHub = (key) => (degree.fanIn.get(key) ?? 0) > HIGH_FAN_DEGREE_THRESHOLD ||
49
+ (degree.fanOut.get(key) ?? 0) > HIGH_FAN_DEGREE_THRESHOLD;
50
+ // Bidirectional adjacency: a change to a file is relevant to what it depends
51
+ // on AND to what depends on it. Edges below the confidence floor are dropped.
52
+ const adjacency = new Map();
53
+ const addEdge = (from, to, confidence) => {
54
+ const list = adjacency.get(from) ?? [];
55
+ list.push({ to, confidence });
56
+ adjacency.set(from, list);
57
+ };
58
+ for (const edge of edges) {
59
+ const confidence = graphEdgeConfidence(edge);
60
+ if (confidence < SCOPE_EDGE_CONFIDENCE_FLOOR) {
61
+ continue;
62
+ }
63
+ const from = normalizeGraphPath(edge.from);
64
+ const to = normalizeGraphPath(edge.to);
65
+ addEdge(from, to, confidence);
66
+ addEdge(to, from, confidence);
67
+ }
68
+ // Max-product shortest-path frontier. `best` holds the highest accumulated
69
+ // confidence discovered for each node; seeds start at 1.
70
+ const best = new Map();
71
+ for (const key of seedKeys) {
72
+ best.set(key, 1);
73
+ }
74
+ const visited = new Set();
75
+ const inScope = new Set(seedKeys);
76
+ const expandedKeys = [];
77
+ let budgetHit = false;
78
+ for (;;) {
79
+ let pick;
80
+ let pickConfidence = -1;
81
+ for (const [key, confidence] of best) {
82
+ if (visited.has(key))
83
+ continue;
84
+ if (confidence > pickConfidence ||
85
+ (confidence === pickConfidence && (pick === undefined || key < pick))) {
86
+ pick = key;
87
+ pickConfidence = confidence;
88
+ }
89
+ }
90
+ if (pick === undefined || pickConfidence < SCOPE_MIN_FRONTIER_CONFIDENCE) {
91
+ break;
92
+ }
93
+ visited.add(pick);
94
+ // Record newly-reached auditable files (seeds are already in scope).
95
+ if (canonicalByNorm.has(pick) && !inScope.has(pick)) {
96
+ if (inScope.size >= maxFiles) {
97
+ budgetHit = true;
98
+ break;
99
+ }
100
+ inScope.add(pick);
101
+ expandedKeys.push(pick);
102
+ }
103
+ // Relax neighbours, skipping hubs (never traverse through or into a hub) and
104
+ // non-auditable nodes.
105
+ for (const neighbour of adjacency.get(pick) ?? []) {
106
+ if (isHub(neighbour.to) || !canonicalByNorm.has(neighbour.to)) {
107
+ continue;
108
+ }
109
+ const candidate = pickConfidence * neighbour.confidence;
110
+ if (candidate < SCOPE_MIN_FRONTIER_CONFIDENCE) {
111
+ continue;
112
+ }
113
+ if (candidate > (best.get(neighbour.to) ?? 0)) {
114
+ best.set(neighbour.to, candidate);
115
+ }
116
+ }
117
+ }
118
+ const seedFiles = seedKeys
119
+ .map((key) => canonicalByNorm.get(key))
120
+ .sort((a, b) => a.localeCompare(b));
121
+ const expandedFiles = expandedKeys
122
+ .map((key) => canonicalByNorm.get(key))
123
+ .sort((a, b) => a.localeCompare(b));
124
+ const notes = [];
125
+ if (seedFiles.length === 0) {
126
+ notes.push(`No auditable files changed since ${input.since}.`);
127
+ }
128
+ if (budgetHit) {
129
+ notes.push(`Expansion stopped at the ${maxFiles}-file budget; some graph neighbours were left out of scope.`);
130
+ }
131
+ return {
132
+ mode: "delta",
133
+ since: input.since,
134
+ seed_files: seedFiles,
135
+ expanded_files: expandedFiles,
136
+ budget: { max_files: maxFiles },
137
+ ...(notes.length > 0 ? { dropped_note: notes.join(" ") } : {}),
138
+ };
139
+ }
140
+ /** A full-audit scope (the default, and every fallback). */
141
+ export function fullAuditScope(budget, droppedNote) {
142
+ return {
143
+ mode: "full",
144
+ since: null,
145
+ seed_files: [],
146
+ expanded_files: [],
147
+ budget: { max_files: budget?.max_files ?? DEFAULT_SCOPE_MAX_FILES },
148
+ ...(droppedNote ? { dropped_note: droppedNote } : {}),
149
+ };
150
+ }
151
+ /**
152
+ * Resolve the scope for a planning run. Returns a full-audit scope unless a
153
+ * `--since` ref was supplied against a real git repository; an unusable ref or
154
+ * missing root degrades to a full audit with an honest note. Reads the auditable
155
+ * file set from the repo manifest + disposition (the same lookup the graph
156
+ * extractor uses) and the dependency graph from the bundle.
157
+ */
158
+ export function resolveAuditScope(input) {
159
+ const since = input.since?.trim();
160
+ if (!since) {
161
+ return fullAuditScope(input.budget);
162
+ }
163
+ if (!input.root) {
164
+ return fullAuditScope(input.budget, `--since '${since}' was ignored: no repository root was available, so a full audit ran.`);
165
+ }
166
+ if (!isGitRepo(input.root)) {
167
+ return fullAuditScope(input.budget, `--since '${since}' was ignored: '${input.root}' is not a git repository, so a full audit ran.`);
168
+ }
169
+ if (!gitRefExists(input.root, since)) {
170
+ return fullAuditScope(input.budget, `--since '${since}' could not be resolved to a commit, so a full audit ran.`);
171
+ }
172
+ const dispositionMap = buildDispositionMap(input.bundle.file_disposition);
173
+ const includedFiles = input.bundle.repo_manifest
174
+ ? [
175
+ ...new Set(buildPathLookup(input.bundle.repo_manifest, dispositionMap).values()),
176
+ ].sort((a, b) => a.localeCompare(b))
177
+ : [];
178
+ return computeAuditScope({
179
+ since,
180
+ changed: changedFiles(input.root, since),
181
+ includedFiles,
182
+ graphBundle: input.bundle.graph_bundle,
183
+ budget: input.budget,
184
+ });
185
+ }
186
+ /**
187
+ * Apply a delta scope to a freshly-built coverage matrix. In-scope files (seeds
188
+ * + expanded neighbours) keep their fresh `pending` status to be re-audited.
189
+ * Out-of-scope files inherit a prior `complete` record verbatim when present (so
190
+ * previously-finished work is preserved, not re-run), and are otherwise excluded
191
+ * from this run with `classification_status: "out_of_scope_delta"`. Deterministic
192
+ * exclusions (non-auditable/trivial) are left untouched. A full scope is a no-op.
193
+ */
194
+ export function applyScopeToCoverage(coverage, scope, priorCoverage) {
195
+ if (scope.mode !== "delta") {
196
+ return coverage;
197
+ }
198
+ const inScope = new Set([
199
+ ...scope.seed_files,
200
+ ...scope.expanded_files,
201
+ ]);
202
+ const priorByPath = new Map((priorCoverage?.files ?? []).map((file) => [file.path, file]));
203
+ for (const file of coverage.files) {
204
+ if (file.audit_status === "excluded") {
205
+ continue;
206
+ }
207
+ if (inScope.has(file.path)) {
208
+ continue;
209
+ }
210
+ const prior = priorByPath.get(file.path);
211
+ if (prior && prior.audit_status === "complete") {
212
+ file.required_lenses = [...prior.required_lenses];
213
+ file.completed_lenses = [...prior.completed_lenses];
214
+ file.unit_ids = [...prior.unit_ids];
215
+ file.audit_status = "complete";
216
+ file.classification_status = prior.classification_status;
217
+ }
218
+ else {
219
+ file.required_lenses = [];
220
+ file.completed_lenses = [];
221
+ file.unit_ids = [];
222
+ file.audit_status = "excluded";
223
+ file.classification_status = "out_of_scope_delta";
224
+ }
225
+ }
226
+ return coverage;
227
+ }
@@ -29,6 +29,7 @@ export function deriveAuditState(bundle) {
29
29
  "critical_flows.json",
30
30
  "risk_register.json",
31
31
  ], structureReady)));
32
+ obligations.push(obligation("graph_enrichment_current", staleOrSatisfied(staleArtifacts, ["analyzer_capability.json"], has(bundle.analyzer_capability))));
32
33
  obligations.push(obligation("design_assessment_current", staleOrSatisfied(staleArtifacts, ["design_assessment.json"], has(bundle.design_assessment))));
33
34
  obligations.push(obligation("design_review_completed", bundle.design_assessment?.reviewed ? "satisfied" : "missing"));
34
35
  const planningReady = has(bundle.coverage_matrix) &&
@@ -69,6 +70,7 @@ export function deriveAuditState(bundle) {
69
70
  ? "No deterministic runtime validation tasks were planned."
70
71
  : undefined));
71
72
  obligations.push(obligation("synthesis_current", staleOrSatisfied(staleArtifacts, ["audit-report.md"], has(bundle.audit_report))));
73
+ obligations.push(obligation("synthesis_narrative_current", staleOrSatisfied(staleArtifacts, ["synthesis-narrative.json"], has(bundle.synthesis_narrative))));
72
74
  let status = "not_started";
73
75
  if (!has(bundle.repo_manifest)) {
74
76
  status = "not_started";