auditor-lambda 0.3.41 → 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.
- package/dist/cli/dispatch.js +5 -1
- package/dist/cli/prompts.d.ts +19 -0
- package/dist/cli/prompts.js +95 -0
- package/dist/cli/steps.d.ts +1 -1
- package/dist/cli.js +287 -7
- package/dist/extractors/analyzers/css.d.ts +2 -0
- package/dist/extractors/analyzers/css.js +101 -0
- package/dist/extractors/analyzers/html.d.ts +2 -0
- package/dist/extractors/analyzers/html.js +92 -0
- package/dist/extractors/analyzers/merge.d.ts +14 -0
- package/dist/extractors/analyzers/merge.js +85 -0
- package/dist/extractors/analyzers/python.d.ts +2 -0
- package/dist/extractors/analyzers/python.js +104 -0
- package/dist/extractors/analyzers/registry.d.ts +33 -0
- package/dist/extractors/analyzers/registry.js +100 -0
- package/dist/extractors/analyzers/resourceUrl.d.ts +7 -0
- package/dist/extractors/analyzers/resourceUrl.js +25 -0
- package/dist/extractors/analyzers/sql.d.ts +2 -0
- package/dist/extractors/analyzers/sql.js +19 -0
- package/dist/extractors/analyzers/treeSitter.d.ts +34 -0
- package/dist/extractors/analyzers/treeSitter.js +111 -0
- package/dist/extractors/analyzers/types.d.ts +53 -0
- package/dist/extractors/analyzers/types.js +1 -0
- package/dist/extractors/analyzers/typescript.d.ts +2 -0
- package/dist/extractors/analyzers/typescript.js +257 -0
- package/dist/extractors/disposition.js +8 -1
- package/dist/extractors/graph.d.ts +1 -0
- package/dist/extractors/graph.js +167 -1
- package/dist/extractors/graphPythonImports.d.ts +15 -0
- package/dist/extractors/graphPythonImports.js +36 -0
- package/dist/extractors/pathPatterns.d.ts +6 -0
- package/dist/extractors/pathPatterns.js +8 -0
- package/dist/io/artifacts.d.ts +12 -1
- package/dist/io/artifacts.js +12 -0
- package/dist/orchestrator/advance.d.ts +20 -0
- package/dist/orchestrator/advance.js +61 -2
- package/dist/orchestrator/dependencyMap.js +27 -0
- package/dist/orchestrator/edgeReasoning.d.ts +39 -0
- package/dist/orchestrator/edgeReasoning.js +125 -0
- package/dist/orchestrator/executors.js +11 -1
- package/dist/orchestrator/graphEnrichmentExecutor.d.ts +29 -0
- package/dist/orchestrator/graphEnrichmentExecutor.js +196 -0
- package/dist/orchestrator/internalExecutors.d.ts +10 -1
- package/dist/orchestrator/internalExecutors.js +89 -11
- package/dist/orchestrator/localCommands.js +6 -25
- package/dist/orchestrator/nextStep.js +2 -0
- package/dist/orchestrator/reviewPackets.d.ts +37 -4
- package/dist/orchestrator/reviewPackets.js +93 -46
- package/dist/orchestrator/runtimeValidation.js +4 -31
- package/dist/orchestrator/scope.d.ts +62 -0
- package/dist/orchestrator/scope.js +227 -0
- package/dist/orchestrator/state.js +2 -0
- package/dist/reporting/synthesis.d.ts +37 -2
- package/dist/reporting/synthesis.js +95 -16
- package/dist/reporting/synthesisNarrativePrompt.d.ts +7 -0
- package/dist/reporting/synthesisNarrativePrompt.js +60 -0
- package/dist/reporting/workBlocks.d.ts +2 -10
- package/dist/supervisor/sessionConfig.d.ts +8 -1
- package/dist/supervisor/sessionConfig.js +22 -1
- package/dist/types/analyzerCapability.d.ts +16 -0
- package/dist/types/analyzerCapability.js +1 -0
- package/dist/types/auditScope.d.ts +43 -0
- package/dist/types/auditScope.js +14 -0
- package/dist/types/synthesisNarrative.d.ts +7 -0
- package/dist/types/synthesisNarrative.js +5 -0
- package/dist/types.d.ts +2 -19
- package/dist/validation/artifacts.js +9 -0
- package/dist/validation/sessionConfig.js +24 -1
- package/package.json +4 -2
- package/schemas/analyzer_capability.schema.json +47 -0
- package/schemas/audit_findings.schema.json +141 -0
- package/schemas/finding.schema.json +2 -1
- package/schemas/graph_bundle.schema.json +5 -0
- 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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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 +
|
|
59
|
+
return ESTIMATED_PACKET_PROMPT_TOKENS + contentTokens;
|
|
19
60
|
}
|
|
20
61
|
const PACKET_EXPANSION_MIN_CONFIDENCE = 0.65;
|
|
21
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
801
|
-
if (wouldExceedTaskCount ||
|
|
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:
|
|
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
|
|
872
|
-
const
|
|
873
|
-
? Math.min(
|
|
874
|
-
|
|
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,
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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";
|