auditor-lambda 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/audit-code-wrapper-lib.mjs +458 -380
- package/dist/cli.js +258 -11
- package/dist/coverage.d.ts +0 -1
- package/dist/coverage.js +3 -34
- package/dist/extractors/fileInventory.js +2 -0
- package/dist/io/artifacts.js +2 -1
- package/dist/orchestrator/advance.js +70 -52
- package/dist/orchestrator/flowCoverage.js +2 -1
- package/dist/orchestrator/flowPlanning.d.ts +1 -1
- package/dist/orchestrator/flowPlanning.js +21 -28
- package/dist/orchestrator/internalExecutors.js +0 -1
- package/dist/orchestrator/taskBuilder.d.ts +7 -2
- package/dist/orchestrator/taskBuilder.js +55 -47
- package/dist/prompts/renderWorkerPrompt.js +32 -0
- package/dist/providers/claudeCodeProvider.js +6 -0
- package/dist/providers/index.js +5 -2
- package/dist/providers/opencodeProvider.js +6 -1
- package/dist/providers/types.d.ts +1 -0
- package/dist/reporting/mergeFindings.js +0 -7
- package/dist/reporting/rootCause.d.ts +0 -1
- package/dist/reporting/rootCause.js +0 -6
- package/dist/reporting/synthesis.js +18 -0
- package/dist/supervisor/runLedger.js +6 -2
- package/dist/types/sessionConfig.d.ts +8 -0
- package/dist/types/workerSession.d.ts +2 -0
- package/dist/types.d.ts +1 -2
- package/dist/validation/auditResults.d.ts +11 -0
- package/dist/validation/auditResults.js +118 -0
- package/dist/validation/sessionConfig.js +15 -1
- package/docs/agent-integrations.md +61 -56
- package/docs/agent-roles.md +69 -69
- package/docs/architecture.md +90 -90
- package/docs/artifacts.md +69 -69
- package/docs/bootstrap-install.md +1 -1
- package/docs/model-selection.md +86 -86
- package/docs/next-steps.md +11 -9
- package/docs/packaging.md +3 -3
- package/docs/pipeline.md +152 -152
- package/docs/production-readiness.md +6 -5
- package/docs/repo-layout.md +18 -18
- package/docs/run-flow.md +5 -5
- package/docs/session-config.md +216 -210
- package/docs/supervisor.md +70 -70
- package/docs/windows-setup.md +139 -139
- package/package.json +56 -56
- package/schemas/audit-code-v1alpha1.schema.json +76 -76
- package/schemas/audit_result.schema.json +48 -48
- package/schemas/audit_task.schema.json +49 -49
- package/schemas/coverage_matrix.schema.json +0 -15
- package/schemas/file_disposition.schema.json +33 -33
- package/schemas/finding.schema.json +58 -62
- package/schemas/flow_coverage.schema.json +44 -44
- package/schemas/root_cause_clusters.schema.json +0 -4
- package/schemas/runtime_validation_report.schema.json +34 -34
- package/schemas/synthesis_report.schema.json +61 -61
- package/skills/audit-code/SKILL.md +37 -37
- package/skills/audit-code/audit-code.prompt.md +56 -54
|
@@ -3,39 +3,32 @@ const DEFAULT_FLOW_LENS_PRIORITY = [
|
|
|
3
3
|
"reliability",
|
|
4
4
|
"correctness",
|
|
5
5
|
];
|
|
6
|
-
function
|
|
7
|
-
const path = task.file_paths.join(",");
|
|
8
|
-
const range = task.line_ranges?.map((r) => `${r.path}:${r.start}-${r.end}`).join(",") ??
|
|
9
|
-
"full";
|
|
10
|
-
return `${task.lens}|${path}|${range}`;
|
|
11
|
-
}
|
|
12
|
-
export function buildFlowAwareTaskAugmentations(existingTasks, criticalFlows, lineIndex) {
|
|
6
|
+
export function buildFlowAwareTaskAugmentations(existingTasks, criticalFlows, _lineIndex) {
|
|
13
7
|
const seenTaskIds = new Set(existingTasks.map((task) => task.task_id));
|
|
14
|
-
|
|
8
|
+
// Signature: lens + sorted file list, so duplicate (flow, lens) combos across
|
|
9
|
+
// different flow groupings are still deduplicated.
|
|
10
|
+
const existingSignatures = new Set(existingTasks.map((t) => `${t.lens}|${[...t.file_paths].sort().join(",")}`));
|
|
15
11
|
const extraTasks = [];
|
|
16
12
|
for (const flow of criticalFlows.flows) {
|
|
17
13
|
const desiredLenses = flow.concerns.filter((concern) => DEFAULT_FLOW_LENS_PRIORITY.includes(concern));
|
|
18
|
-
for (const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
lens,
|
|
26
|
-
file_paths: [path],
|
|
27
|
-
line_ranges: totalLines > 0 ? [{ path, start: 1, end: totalLines }] : undefined,
|
|
28
|
-
rationale: `Flow-aware audit for ${path} because it participates in critical flow ${flow.id} under the ${lens} lens.`,
|
|
29
|
-
};
|
|
30
|
-
const signature = normalizeTaskSignature(candidate);
|
|
31
|
-
if (seenTaskIds.has(candidate.task_id) ||
|
|
32
|
-
existingSignatures.has(signature)) {
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
extraTasks.push(candidate);
|
|
36
|
-
seenTaskIds.add(candidate.task_id);
|
|
37
|
-
existingSignatures.add(signature);
|
|
14
|
+
for (const lens of desiredLenses) {
|
|
15
|
+
// One task per (flow, lens) with all flow paths together so the agent
|
|
16
|
+
// can trace data across file boundaries in a single pass.
|
|
17
|
+
const taskId = `flow:${flow.id}:${lens}`;
|
|
18
|
+
const signature = `${lens}|${[...flow.paths].sort().join(",")}`;
|
|
19
|
+
if (seenTaskIds.has(taskId) || existingSignatures.has(signature)) {
|
|
20
|
+
continue;
|
|
38
21
|
}
|
|
22
|
+
extraTasks.push({
|
|
23
|
+
task_id: taskId,
|
|
24
|
+
unit_id: `flow:${flow.id}`,
|
|
25
|
+
pass_id: `flow-pass:${lens}`,
|
|
26
|
+
lens,
|
|
27
|
+
file_paths: [...flow.paths],
|
|
28
|
+
rationale: `Flow-aware audit for critical flow "${flow.id}" (${flow.paths.length} file${flow.paths.length === 1 ? "" : "s"}) under the ${lens} lens.`,
|
|
29
|
+
});
|
|
30
|
+
seenTaskIds.add(taskId);
|
|
31
|
+
existingSignatures.add(signature);
|
|
39
32
|
}
|
|
40
33
|
}
|
|
41
34
|
return extraTasks;
|
|
@@ -87,7 +87,6 @@ export function runPlanningExecutor(bundle, lineIndex = {}) {
|
|
|
87
87
|
const runtimeValidationTasks = buildRuntimeValidationTasks(bundle.unit_manifest, bundle.critical_flows, flowCoverage);
|
|
88
88
|
const runtimeValidationReport = preserveOrPlaceholder(runtimeValidationTasks, bundle.runtime_validation_report);
|
|
89
89
|
const baseTasks = buildChunkedAuditTasks(bundle.unit_manifest, lineIndex, {
|
|
90
|
-
chunk_size: 200,
|
|
91
90
|
external_analyzer_results: externalAnalyzerResults,
|
|
92
91
|
});
|
|
93
92
|
const analyzerTasks = buildExternalSignalTasks(coverage, lineIndex, externalAnalyzerResults);
|
|
@@ -4,9 +4,14 @@ export interface UnitLineIndex {
|
|
|
4
4
|
[path: string]: number;
|
|
5
5
|
}
|
|
6
6
|
export interface BuildChunkedTaskOptions {
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Line count above which a single file gets its own task rather than being
|
|
9
|
+
* grouped with the rest of its unit. Default: 3000. Set to 0 to disable
|
|
10
|
+
* splitting entirely.
|
|
11
|
+
*/
|
|
12
|
+
file_split_threshold?: number;
|
|
8
13
|
limit_lenses?: Lens[];
|
|
9
14
|
external_analyzer_results?: ExternalAnalyzerResults;
|
|
10
15
|
}
|
|
11
16
|
export declare function buildChunkedAuditTasks(unitManifest: UnitManifest, unitLineIndex: UnitLineIndex, options?: BuildChunkedTaskOptions): AuditTask[];
|
|
12
|
-
export declare function buildExternalSignalTasks(coverageMatrix: CoverageMatrix,
|
|
17
|
+
export declare function buildExternalSignalTasks(coverageMatrix: CoverageMatrix, _unitLineIndex: UnitLineIndex, externalAnalyzerResults?: ExternalAnalyzerResults): AuditTask[];
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
function
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
function modelHintForLens(lens) {
|
|
2
|
+
switch (lens) {
|
|
3
|
+
case "security":
|
|
4
|
+
case "correctness":
|
|
5
|
+
case "reliability":
|
|
6
|
+
case "data_integrity":
|
|
7
|
+
return "capable";
|
|
8
|
+
case "architecture":
|
|
9
|
+
case "maintainability":
|
|
10
|
+
case "tests":
|
|
11
|
+
return "balanced";
|
|
12
|
+
default:
|
|
13
|
+
return "fast";
|
|
8
14
|
}
|
|
9
|
-
return ranges;
|
|
10
15
|
}
|
|
11
16
|
function taskPriority(hasExternalSignal, lens) {
|
|
12
17
|
if (hasExternalSignal &&
|
|
@@ -48,7 +53,7 @@ function pickAnalyzerLens(category) {
|
|
|
48
53
|
return "correctness";
|
|
49
54
|
}
|
|
50
55
|
export function buildChunkedAuditTasks(unitManifest, unitLineIndex, options = {}) {
|
|
51
|
-
const
|
|
56
|
+
const fileSplitThreshold = options.file_split_threshold ?? 3000;
|
|
52
57
|
const allowed = new Set(options.limit_lenses ?? []);
|
|
53
58
|
const enforceLensFilter = allowed.size > 0;
|
|
54
59
|
const tasks = [];
|
|
@@ -63,49 +68,54 @@ export function buildChunkedAuditTasks(unitManifest, unitLineIndex, options = {}
|
|
|
63
68
|
if (enforceLensFilter && !allowed.has(lens)) {
|
|
64
69
|
continue;
|
|
65
70
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
unit_id: unit.unit_id,
|
|
80
|
-
pass_id: `pass:${lens}`,
|
|
81
|
-
lens,
|
|
82
|
-
file_paths: [filePath],
|
|
83
|
-
rationale: `Audit ${filePath} under the ${lens} lens.${hasExternalSignal ? " External analyzer signals raise priority for this path." : ""}`,
|
|
84
|
-
priority,
|
|
85
|
-
tags,
|
|
86
|
-
});
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
for (const range of ranges) {
|
|
90
|
-
const id = `${unit.unit_id}:${lens}:${filePath}:${range.start}-${range.end}`;
|
|
91
|
-
if (seen.has(id))
|
|
92
|
-
continue;
|
|
71
|
+
const hasExternalSignal = unit.files.some((f) => externalPaths.has(f));
|
|
72
|
+
const priority = taskPriority(hasExternalSignal, lens);
|
|
73
|
+
const tags = hasExternalSignal ? ["external_analyzer_signal"] : [];
|
|
74
|
+
// Split files that are individually too large to group; everything else
|
|
75
|
+
// goes into one task so the agent can reason across file boundaries.
|
|
76
|
+
const oversizedFiles = fileSplitThreshold > 0
|
|
77
|
+
? unit.files.filter((f) => (unitLineIndex[f] ?? 0) > fileSplitThreshold)
|
|
78
|
+
: [];
|
|
79
|
+
const normalFiles = unit.files.filter((f) => !oversizedFiles.includes(f));
|
|
80
|
+
// One task for all normal-sized files in this unit under this lens.
|
|
81
|
+
if (normalFiles.length > 0) {
|
|
82
|
+
const id = `${unit.unit_id}:${lens}`;
|
|
83
|
+
if (!seen.has(id)) {
|
|
93
84
|
seen.add(id);
|
|
94
85
|
tasks.push({
|
|
95
86
|
task_id: id,
|
|
96
87
|
unit_id: unit.unit_id,
|
|
97
88
|
pass_id: `pass:${lens}`,
|
|
98
89
|
lens,
|
|
99
|
-
file_paths:
|
|
100
|
-
|
|
101
|
-
{ path: filePath, start: range.start, end: range.end },
|
|
102
|
-
],
|
|
103
|
-
rationale: `Audit ${filePath} lines ${range.start}-${range.end} under the ${lens} lens.${hasExternalSignal ? " External analyzer signals raise priority for this path." : ""}`,
|
|
90
|
+
file_paths: normalFiles,
|
|
91
|
+
rationale: `Audit ${unit.unit_id} (${normalFiles.length} file${normalFiles.length === 1 ? "" : "s"}) under the ${lens} lens.${hasExternalSignal ? " External analyzer signals raise priority." : ""}`,
|
|
104
92
|
priority,
|
|
105
93
|
tags,
|
|
94
|
+
model_hint: modelHintForLens(lens),
|
|
106
95
|
});
|
|
107
96
|
}
|
|
108
97
|
}
|
|
98
|
+
// Oversized files each get their own task so the agent isn't overwhelmed.
|
|
99
|
+
for (const filePath of oversizedFiles) {
|
|
100
|
+
const id = `${unit.unit_id}:${lens}:${filePath}`;
|
|
101
|
+
if (seen.has(id))
|
|
102
|
+
continue;
|
|
103
|
+
seen.add(id);
|
|
104
|
+
const fileHasSignal = externalPaths.has(filePath);
|
|
105
|
+
tasks.push({
|
|
106
|
+
task_id: id,
|
|
107
|
+
unit_id: unit.unit_id,
|
|
108
|
+
pass_id: `pass:${lens}`,
|
|
109
|
+
lens,
|
|
110
|
+
file_paths: [filePath],
|
|
111
|
+
rationale: `Audit ${filePath} (large file, split from unit) under the ${lens} lens.${fileHasSignal ? " External analyzer signals raise priority." : ""}`,
|
|
112
|
+
priority: taskPriority(fileHasSignal, lens),
|
|
113
|
+
tags: fileHasSignal
|
|
114
|
+
? ["external_analyzer_signal", "large_file"]
|
|
115
|
+
: ["large_file"],
|
|
116
|
+
model_hint: modelHintForLens(lens),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
109
119
|
}
|
|
110
120
|
}
|
|
111
121
|
return tasks.sort((a, b) => {
|
|
@@ -115,19 +125,19 @@ export function buildChunkedAuditTasks(unitManifest, unitLineIndex, options = {}
|
|
|
115
125
|
return a.task_id.localeCompare(b.task_id);
|
|
116
126
|
});
|
|
117
127
|
}
|
|
118
|
-
export function buildExternalSignalTasks(coverageMatrix,
|
|
128
|
+
export function buildExternalSignalTasks(coverageMatrix, _unitLineIndex, externalAnalyzerResults) {
|
|
119
129
|
if (!externalAnalyzerResults) {
|
|
120
130
|
return [];
|
|
121
131
|
}
|
|
122
132
|
const tasks = [];
|
|
123
133
|
const seen = new Set();
|
|
134
|
+
const coverageIndex = new Map(coverageMatrix.files.map((file) => [file.path, file]));
|
|
124
135
|
for (const result of externalAnalyzerResults.results) {
|
|
125
136
|
const lens = pickAnalyzerLens(result.category);
|
|
126
|
-
const coverage =
|
|
137
|
+
const coverage = coverageIndex.get(result.path);
|
|
127
138
|
if (!coverage || coverage.audit_status === "excluded") {
|
|
128
139
|
continue;
|
|
129
140
|
}
|
|
130
|
-
const lineCount = unitLineIndex[result.path] ?? 0;
|
|
131
141
|
const id = `analyzer:${externalAnalyzerResults.tool}:${lens}:${result.path}:${result.id}`;
|
|
132
142
|
if (seen.has(id)) {
|
|
133
143
|
continue;
|
|
@@ -139,15 +149,13 @@ export function buildExternalSignalTasks(coverageMatrix, unitLineIndex, external
|
|
|
139
149
|
pass_id: `analyzer:${externalAnalyzerResults.tool}:${lens}`,
|
|
140
150
|
lens,
|
|
141
151
|
file_paths: [result.path],
|
|
142
|
-
line_ranges: lineCount > 0
|
|
143
|
-
? [{ path: result.path, start: 1, end: lineCount }]
|
|
144
|
-
: undefined,
|
|
145
152
|
rationale: `Analyzer follow-up for ${result.path} under the ${lens} lens because ${externalAnalyzerResults.tool} reported: ${result.summary}`,
|
|
146
153
|
priority: "high",
|
|
147
154
|
tags: [
|
|
148
155
|
"external_analyzer_signal",
|
|
149
156
|
`external_tool:${externalAnalyzerResults.tool}`,
|
|
150
157
|
],
|
|
158
|
+
model_hint: modelHintForLens(lens),
|
|
151
159
|
});
|
|
152
160
|
}
|
|
153
161
|
return tasks.sort((a, b) => a.task_id.localeCompare(b.task_id));
|
|
@@ -3,6 +3,38 @@ function shellQuote(arg) {
|
|
|
3
3
|
}
|
|
4
4
|
export function renderWorkerPrompt(task) {
|
|
5
5
|
const command = task.worker_command.map(shellQuote).join(" ");
|
|
6
|
+
if (task.preferred_executor === "agent" && task.audit_results_path) {
|
|
7
|
+
const tasksPath = task.pending_audit_tasks_path ?? `${task.artifacts_dir}/audit_tasks.json`;
|
|
8
|
+
const lines = [
|
|
9
|
+
"You are executing one bounded audit task for audit-code.",
|
|
10
|
+
`Run ID: ${task.run_id}`,
|
|
11
|
+
`Repository root: ${task.repo_root}`,
|
|
12
|
+
"",
|
|
13
|
+
`Read the task file: ${tasksPath}`,
|
|
14
|
+
"It contains the task(s) assigned to this run.",
|
|
15
|
+
"",
|
|
16
|
+
"For each task:",
|
|
17
|
+
" 1. Read every file listed in file_paths in full using your file-reading tool.",
|
|
18
|
+
" If line_ranges are present, they are a focus hint — still read the whole file.",
|
|
19
|
+
" 2. Review the content under the specified lens.",
|
|
20
|
+
" 3. Emit one AuditResult with:",
|
|
21
|
+
" task_id, unit_id, pass_id, lens",
|
|
22
|
+
" reviewed_ranges: [{path, start, end}] covering what you read",
|
|
23
|
+
" findings: array (empty if nothing found)",
|
|
24
|
+
" Each finding must include:",
|
|
25
|
+
" id, title, category, severity, confidence, lens, summary, affected_files,",
|
|
26
|
+
" evidence (at least one excerpt or line reference from the file you read)",
|
|
27
|
+
" Optional finding fields: impact, likelihood, reproduction, systemic, related_findings",
|
|
28
|
+
`Write the AuditResult[] JSON array to: ${task.audit_results_path}`,
|
|
29
|
+
];
|
|
30
|
+
if (task.skip_worker_command) {
|
|
31
|
+
lines.push("", "Stop after writing the results file.");
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
lines.push("", "Then run this command exactly:", command, "Stop after the command completes.");
|
|
35
|
+
}
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
6
38
|
return [
|
|
7
39
|
"You are executing one bounded audit step for audit-code.",
|
|
8
40
|
`Run ID: ${task.run_id}`,
|
|
@@ -7,11 +7,17 @@ export class ClaudeCodeProvider {
|
|
|
7
7
|
this.config = config;
|
|
8
8
|
}
|
|
9
9
|
async launch(input) {
|
|
10
|
+
if (process.env.CLAUDECODE) {
|
|
11
|
+
throw new Error("claude-code provider cannot be used inside an active Claude Code session. " +
|
|
12
|
+
'Set provider to "local-subprocess" in .audit-artifacts/session-config.json, ' +
|
|
13
|
+
"then run /audit-code conversationally and follow the dispatch prompts manually.");
|
|
14
|
+
}
|
|
10
15
|
const prompt = await readFile(input.promptPath, "utf8");
|
|
11
16
|
const command = this.config.command ?? "claude";
|
|
12
17
|
const args = [
|
|
13
18
|
"-p",
|
|
14
19
|
prompt,
|
|
20
|
+
...(input.model ? ["--model", input.model] : []),
|
|
15
21
|
...(this.config.extra_args ?? []),
|
|
16
22
|
"--dangerously-skip-permissions",
|
|
17
23
|
];
|
package/dist/providers/index.js
CHANGED
|
@@ -28,6 +28,7 @@ export function resolveFreshSessionProviderName(name, sessionConfig = {}, option
|
|
|
28
28
|
const env = options.env ?? process.env;
|
|
29
29
|
const lookupCommand = options.commandExists ?? commandExists;
|
|
30
30
|
const inVSCode = (env.TERM_PROGRAM ?? "").toLowerCase() === "vscode";
|
|
31
|
+
const insideClaudeCode = Boolean(env.CLAUDECODE);
|
|
31
32
|
if (inVSCode && hasEntries(sessionConfig.vscode_task?.command_template)) {
|
|
32
33
|
return "vscode-task";
|
|
33
34
|
}
|
|
@@ -36,9 +37,11 @@ export function resolveFreshSessionProviderName(name, sessionConfig = {}, option
|
|
|
36
37
|
}
|
|
37
38
|
const claudeCommand = sessionConfig.claude_code?.command ?? "claude";
|
|
38
39
|
const opencodeCommand = sessionConfig.opencode?.command ?? "opencode";
|
|
39
|
-
const claudeAvailable = lookupCommand(claudeCommand);
|
|
40
|
+
const claudeAvailable = !insideClaudeCode && lookupCommand(claudeCommand);
|
|
40
41
|
const opencodeAvailable = lookupCommand(opencodeCommand);
|
|
41
|
-
if (
|
|
42
|
+
if (!insideClaudeCode &&
|
|
43
|
+
hasConfiguredClaudeCode(sessionConfig) &&
|
|
44
|
+
claudeAvailable) {
|
|
42
45
|
return "claude-code";
|
|
43
46
|
}
|
|
44
47
|
if (hasConfiguredOpenCode(sessionConfig) && opencodeAvailable) {
|
|
@@ -9,7 +9,12 @@ export class OpenCodeProvider {
|
|
|
9
9
|
async launch(input) {
|
|
10
10
|
const prompt = await readFile(input.promptPath, "utf8");
|
|
11
11
|
const command = this.config.command ?? "opencode";
|
|
12
|
-
const args = [
|
|
12
|
+
const args = [
|
|
13
|
+
"run",
|
|
14
|
+
prompt,
|
|
15
|
+
...(input.model ? ["--model", input.model] : []),
|
|
16
|
+
...(this.config.extra_args ?? []),
|
|
17
|
+
];
|
|
13
18
|
return await spawnLoggedCommand(command, args, input);
|
|
14
19
|
}
|
|
15
20
|
}
|
|
@@ -80,7 +80,6 @@ export function mergeFindings(results, runtimeReport, externalAnalyzerResults) {
|
|
|
80
80
|
...analyzerEvidence,
|
|
81
81
|
]),
|
|
82
82
|
],
|
|
83
|
-
remediation: [...new Set(finding.remediation ?? [])],
|
|
84
83
|
related_findings: [
|
|
85
84
|
...new Set([...(finding.related_findings ?? []), finding.id]),
|
|
86
85
|
],
|
|
@@ -109,12 +108,6 @@ export function mergeFindings(results, runtimeReport, externalAnalyzerResults) {
|
|
|
109
108
|
...analyzerEvidence,
|
|
110
109
|
]),
|
|
111
110
|
];
|
|
112
|
-
existing.remediation = [
|
|
113
|
-
...new Set([
|
|
114
|
-
...(existing.remediation ?? []),
|
|
115
|
-
...(finding.remediation ?? []),
|
|
116
|
-
]),
|
|
117
|
-
];
|
|
118
111
|
existing.related_findings = [
|
|
119
112
|
...new Set([
|
|
120
113
|
...(existing.related_findings ?? []),
|
|
@@ -6,6 +6,5 @@ export interface RootCauseCluster {
|
|
|
6
6
|
title: string;
|
|
7
7
|
summary: string;
|
|
8
8
|
finding_ids: string[];
|
|
9
|
-
recommended_actions: string[];
|
|
10
9
|
}
|
|
11
10
|
export declare function buildRootCauseClusters(findings: Finding[], runtimeReport?: RuntimeValidationReport, externalAnalyzerResults?: ExternalAnalyzerResults): RootCauseCluster[];
|
|
@@ -52,17 +52,11 @@ export function buildRootCauseClusters(findings, runtimeReport, externalAnalyzer
|
|
|
52
52
|
.map(([key, grouped], index) => {
|
|
53
53
|
const highestSeverity = grouped.reduce((max, finding) => Math.max(max, severityRank(finding.severity)), 1);
|
|
54
54
|
const systemicCount = grouped.filter((finding) => finding.systemic).length;
|
|
55
|
-
const uniqueRemediations = [
|
|
56
|
-
...new Set(grouped.flatMap((finding) => finding.remediation ?? [])),
|
|
57
|
-
].slice(0, 5);
|
|
58
55
|
return {
|
|
59
56
|
id: `cluster-${index + 1}`,
|
|
60
57
|
title: titleForCluster(key),
|
|
61
58
|
summary: `Grouped ${grouped.length} finding(s); highest severity ${highestSeverity}; systemic flags ${systemicCount}. Runtime validation status: ${runtimeSummary}. External analyzer summary: ${externalSummary}.`,
|
|
62
59
|
finding_ids: grouped.map((finding) => finding.id),
|
|
63
|
-
recommended_actions: uniqueRemediations.length > 0
|
|
64
|
-
? uniqueRemediations
|
|
65
|
-
: [`Review systemic causes behind ${titleForCluster(key)}.`],
|
|
66
60
|
};
|
|
67
61
|
})
|
|
68
62
|
.sort((a, b) => a.title.localeCompare(b.title));
|
|
@@ -14,6 +14,23 @@ function severityBreakdown(findings) {
|
|
|
14
14
|
}
|
|
15
15
|
return breakdown;
|
|
16
16
|
}
|
|
17
|
+
function zeroFindingLensNotes(results) {
|
|
18
|
+
const tasksByLens = new Map();
|
|
19
|
+
const findingsByLens = new Map();
|
|
20
|
+
for (const result of results) {
|
|
21
|
+
tasksByLens.set(result.lens, (tasksByLens.get(result.lens) ?? 0) + 1);
|
|
22
|
+
findingsByLens.set(result.lens, (findingsByLens.get(result.lens) ?? 0) + result.findings.length);
|
|
23
|
+
}
|
|
24
|
+
const zeroLenses = [...tasksByLens.entries()]
|
|
25
|
+
.filter(([lens, count]) => count > 0 && (findingsByLens.get(lens) ?? 0) === 0)
|
|
26
|
+
.map(([lens]) => lens)
|
|
27
|
+
.sort();
|
|
28
|
+
if (zeroLenses.length === 0)
|
|
29
|
+
return [];
|
|
30
|
+
return [
|
|
31
|
+
`Zero findings across all reviewed tasks for lens(es): ${zeroLenses.join(", ")}. Verify these tasks were genuinely reviewed rather than batch-generated.`,
|
|
32
|
+
];
|
|
33
|
+
}
|
|
17
34
|
function externalSummary(results) {
|
|
18
35
|
if (!results) {
|
|
19
36
|
return { tool_count: 0, result_count: 0 };
|
|
@@ -47,6 +64,7 @@ export function buildSynthesisReport(results, runtimeReport, externalAnalyzerRes
|
|
|
47
64
|
`External analyzer signals incorporated: ${extSummary.result_count} result(s) from ${externalAnalyzerResults?.tool}.`,
|
|
48
65
|
]
|
|
49
66
|
: []),
|
|
67
|
+
...zeroFindingLensNotes(results),
|
|
50
68
|
],
|
|
51
69
|
},
|
|
52
70
|
merged_findings,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { readJsonFile } from "../io/json.js";
|
|
2
3
|
function ledgerPath(artifactsDir) {
|
|
3
4
|
return `${artifactsDir}/run-ledger.json`;
|
|
4
5
|
}
|
|
@@ -13,5 +14,8 @@ export async function loadRunLedger(artifactsDir) {
|
|
|
13
14
|
export async function appendRunLedgerEntry(artifactsDir, entry) {
|
|
14
15
|
const ledger = await loadRunLedger(artifactsDir);
|
|
15
16
|
ledger.runs.push(entry);
|
|
16
|
-
|
|
17
|
+
const target = ledgerPath(artifactsDir);
|
|
18
|
+
const tmp = `${target}.tmp`;
|
|
19
|
+
await writeFile(tmp, JSON.stringify(ledger, null, 2) + "\n", "utf8");
|
|
20
|
+
await rename(tmp, target);
|
|
17
21
|
}
|
|
@@ -16,6 +16,11 @@ export interface VSCodeTaskConfig {
|
|
|
16
16
|
command_template: string[];
|
|
17
17
|
env?: Record<string, string>;
|
|
18
18
|
}
|
|
19
|
+
export interface ModelTiersConfig {
|
|
20
|
+
fast?: string;
|
|
21
|
+
balanced?: string;
|
|
22
|
+
capable?: string;
|
|
23
|
+
}
|
|
19
24
|
export interface SessionConfig {
|
|
20
25
|
provider?: ProviderName;
|
|
21
26
|
timeout_ms?: number;
|
|
@@ -24,4 +29,7 @@ export interface SessionConfig {
|
|
|
24
29
|
claude_code?: ClaudeCodeConfig;
|
|
25
30
|
opencode?: OpenCodeConfig;
|
|
26
31
|
vscode_task?: VSCodeTaskConfig;
|
|
32
|
+
agent_task_batch_size?: number;
|
|
33
|
+
parallel_workers?: number;
|
|
34
|
+
model_tiers?: ModelTiersConfig;
|
|
27
35
|
}
|
|
@@ -8,6 +8,8 @@ export interface WorkerTask {
|
|
|
8
8
|
result_path: string;
|
|
9
9
|
worker_command: string[];
|
|
10
10
|
audit_results_path?: string;
|
|
11
|
+
pending_audit_tasks_path?: string;
|
|
11
12
|
runtime_updates_path?: string;
|
|
12
13
|
external_analyzer_results_path?: string;
|
|
14
|
+
skip_worker_command?: boolean;
|
|
13
15
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -43,7 +43,6 @@ export interface CoverageFileRecord {
|
|
|
43
43
|
audit_status: string;
|
|
44
44
|
required_lenses: Lens[];
|
|
45
45
|
completed_lenses: Lens[];
|
|
46
|
-
reviewed_line_ranges: ReviewedLineRange[];
|
|
47
46
|
}
|
|
48
47
|
export interface CoverageMatrix {
|
|
49
48
|
files: CoverageFileRecord[];
|
|
@@ -63,6 +62,7 @@ export interface AuditTask {
|
|
|
63
62
|
rationale: string;
|
|
64
63
|
priority?: "high" | "medium" | "low";
|
|
65
64
|
tags?: string[];
|
|
65
|
+
model_hint?: "fast" | "balanced" | "capable";
|
|
66
66
|
}
|
|
67
67
|
export interface Finding {
|
|
68
68
|
id: string;
|
|
@@ -82,7 +82,6 @@ export interface Finding {
|
|
|
82
82
|
likelihood?: string;
|
|
83
83
|
evidence?: string[];
|
|
84
84
|
reproduction?: string[];
|
|
85
|
-
remediation?: string[];
|
|
86
85
|
systemic?: boolean;
|
|
87
86
|
related_findings?: string[];
|
|
88
87
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AuditResult, AuditTask } from "../types.js";
|
|
2
|
+
export type IssueSeverity = "error" | "warning";
|
|
3
|
+
export interface AuditResultIssue {
|
|
4
|
+
result_index: number;
|
|
5
|
+
task_id: string;
|
|
6
|
+
severity: IssueSeverity;
|
|
7
|
+
field: string;
|
|
8
|
+
message: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function validateAuditResults(results: AuditResult[], tasks: AuditTask[]): AuditResultIssue[];
|
|
11
|
+
export declare function formatAuditResultIssues(issues: AuditResultIssue[]): string;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const REQUIRED_FINDING_FIELDS = [
|
|
2
|
+
"id",
|
|
3
|
+
"title",
|
|
4
|
+
"category",
|
|
5
|
+
"severity",
|
|
6
|
+
"confidence",
|
|
7
|
+
"lens",
|
|
8
|
+
"summary",
|
|
9
|
+
];
|
|
10
|
+
const VALID_SEVERITIES = new Set(["critical", "high", "medium", "low", "info"]);
|
|
11
|
+
const VALID_CONFIDENCES = new Set(["high", "medium", "low"]);
|
|
12
|
+
function validateFinding(finding, label, taskId, resultIndex) {
|
|
13
|
+
const issues = [];
|
|
14
|
+
for (const field of REQUIRED_FINDING_FIELDS) {
|
|
15
|
+
const value = finding[field];
|
|
16
|
+
if (value === undefined || value === null || String(value).trim() === "") {
|
|
17
|
+
issues.push({
|
|
18
|
+
result_index: resultIndex,
|
|
19
|
+
task_id: taskId,
|
|
20
|
+
severity: "error",
|
|
21
|
+
field: `${label}.${field}`,
|
|
22
|
+
message: `Required field '${field}' is missing or empty.`,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (finding.severity && !VALID_SEVERITIES.has(finding.severity)) {
|
|
27
|
+
issues.push({
|
|
28
|
+
result_index: resultIndex,
|
|
29
|
+
task_id: taskId,
|
|
30
|
+
severity: "error",
|
|
31
|
+
field: `${label}.severity`,
|
|
32
|
+
message: `Invalid severity '${finding.severity}'. Must be one of: ${[...VALID_SEVERITIES].join(", ")}.`,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (finding.confidence && !VALID_CONFIDENCES.has(finding.confidence)) {
|
|
36
|
+
issues.push({
|
|
37
|
+
result_index: resultIndex,
|
|
38
|
+
task_id: taskId,
|
|
39
|
+
severity: "error",
|
|
40
|
+
field: `${label}.confidence`,
|
|
41
|
+
message: `Invalid confidence '${finding.confidence}'. Must be one of: ${[...VALID_CONFIDENCES].join(", ")}.`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (!finding.affected_files || finding.affected_files.length === 0) {
|
|
45
|
+
issues.push({
|
|
46
|
+
result_index: resultIndex,
|
|
47
|
+
task_id: taskId,
|
|
48
|
+
severity: "error",
|
|
49
|
+
field: `${label}.affected_files`,
|
|
50
|
+
message: "affected_files is empty — at least one file location is required.",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
for (let k = 0; k < finding.affected_files.length; k++) {
|
|
55
|
+
const af = finding.affected_files[k];
|
|
56
|
+
if (!af.path?.trim()) {
|
|
57
|
+
issues.push({
|
|
58
|
+
result_index: resultIndex,
|
|
59
|
+
task_id: taskId,
|
|
60
|
+
severity: "error",
|
|
61
|
+
field: `${label}.affected_files[${k}].path`,
|
|
62
|
+
message: "affected_files entry has an empty path.",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!finding.evidence || finding.evidence.length === 0) {
|
|
68
|
+
issues.push({
|
|
69
|
+
result_index: resultIndex,
|
|
70
|
+
task_id: taskId,
|
|
71
|
+
severity: "error",
|
|
72
|
+
field: `${label}.evidence`,
|
|
73
|
+
message: "evidence is empty — at least one quoted or referenced excerpt from the reviewed file is required for every finding.",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const hasSubstantiveEntry = finding.evidence.some((e) => e.trim().length > 0);
|
|
78
|
+
if (!hasSubstantiveEntry) {
|
|
79
|
+
issues.push({
|
|
80
|
+
result_index: resultIndex,
|
|
81
|
+
task_id: taskId,
|
|
82
|
+
severity: "error",
|
|
83
|
+
field: `${label}.evidence`,
|
|
84
|
+
message: "All evidence entries are empty strings.",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return issues;
|
|
89
|
+
}
|
|
90
|
+
export function validateAuditResults(results, tasks) {
|
|
91
|
+
const issues = [];
|
|
92
|
+
const taskMap = new Map(tasks.map((t) => [t.task_id, t]));
|
|
93
|
+
for (let i = 0; i < results.length; i++) {
|
|
94
|
+
const result = results[i];
|
|
95
|
+
const taskId = result.task_id ?? `result[${i}]`;
|
|
96
|
+
if (!result.reviewed_ranges || result.reviewed_ranges.length === 0) {
|
|
97
|
+
issues.push({
|
|
98
|
+
result_index: i,
|
|
99
|
+
task_id: taskId,
|
|
100
|
+
severity: "error",
|
|
101
|
+
field: "reviewed_ranges",
|
|
102
|
+
message: "reviewed_ranges is empty — no proof of file reading was recorded. " +
|
|
103
|
+
"Each result must include the line ranges actually read.",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
for (let j = 0; j < (result.findings ?? []).length; j++) {
|
|
107
|
+
const finding = result.findings[j];
|
|
108
|
+
const label = `findings[${j}]`;
|
|
109
|
+
issues.push(...validateFinding(finding, label, taskId, i));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return issues;
|
|
113
|
+
}
|
|
114
|
+
export function formatAuditResultIssues(issues) {
|
|
115
|
+
return issues
|
|
116
|
+
.map((issue) => ` [${issue.severity}] ${issue.task_id} / ${issue.field}: ${issue.message}`)
|
|
117
|
+
.join("\n");
|
|
118
|
+
}
|