auditor-lambda 0.2.5 → 0.2.8
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 +35 -7
- package/audit-code-wrapper-lib.mjs +1612 -331
- package/dist/cli.js +397 -38
- package/dist/coverage.d.ts +2 -2
- package/dist/coverage.js +5 -5
- package/dist/extractors/disposition.js +10 -1
- package/dist/extractors/flows.js +7 -1
- package/dist/extractors/pathPatterns.d.ts +3 -0
- package/dist/extractors/pathPatterns.js +15 -0
- package/dist/extractors/risk.js +7 -1
- package/dist/io/artifacts.d.ts +6 -6
- package/dist/io/artifacts.js +14 -17
- package/dist/io/json.d.ts +2 -0
- package/dist/io/json.js +15 -0
- package/dist/io/runArtifacts.d.ts +3 -1
- package/dist/io/runArtifacts.js +20 -5
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +579 -0
- package/dist/orchestrator/advance.js +9 -2
- package/dist/orchestrator/dependencyMap.js +9 -13
- package/dist/orchestrator/executors.js +7 -2
- package/dist/orchestrator/flowRequeue.d.ts +2 -2
- package/dist/orchestrator/flowRequeue.js +16 -3
- package/dist/orchestrator/internalExecutors.d.ts +2 -1
- package/dist/orchestrator/internalExecutors.js +129 -48
- package/dist/orchestrator/requeue.js +10 -4
- package/dist/orchestrator/requeueCommand.js +15 -2
- package/dist/orchestrator/resultIngestion.d.ts +2 -1
- package/dist/orchestrator/resultIngestion.js +26 -6
- package/dist/orchestrator/runtimeValidation.d.ts +7 -2
- package/dist/orchestrator/runtimeValidation.js +61 -49
- package/dist/orchestrator/runtimeValidationUpdate.js +2 -4
- package/dist/orchestrator/state.js +28 -14
- package/dist/orchestrator/taskBuilder.js +4 -2
- package/dist/orchestrator/trivialAudit.d.ts +4 -0
- package/dist/orchestrator/trivialAudit.js +49 -0
- package/dist/prompts/renderWorkerPrompt.js +6 -2
- package/dist/providers/spawnLoggedCommand.js +17 -0
- package/dist/reporting/mergeFindings.js +3 -11
- package/dist/reporting/rootCause.js +92 -9
- package/dist/reporting/synthesis.d.ts +25 -22
- package/dist/reporting/synthesis.js +92 -59
- package/dist/reporting/workBlocks.d.ts +12 -3
- package/dist/reporting/workBlocks.js +124 -70
- package/dist/supervisor/sessionConfig.js +4 -2
- package/dist/types/flows.d.ts +2 -0
- package/dist/types/runtimeValidation.d.ts +2 -1
- package/dist/types.d.ts +8 -6
- package/dist/validation/auditResults.d.ts +5 -2
- package/dist/validation/auditResults.js +335 -43
- package/docs/agent-integrations.md +38 -29
- package/docs/artifacts.md +18 -51
- package/docs/bootstrap-install.md +60 -30
- package/docs/contract.md +25 -117
- package/docs/field-trial-bug-report.md +237 -0
- package/docs/next-steps.md +59 -44
- package/docs/packaging.md +13 -3
- package/docs/production-launch-bar.md +2 -2
- package/docs/production-readiness.md +9 -5
- package/docs/releasing.md +81 -0
- package/docs/session-config.md +20 -1
- package/docs/usage.md +22 -0
- package/package.json +4 -1
- package/schemas/audit_result.schema.json +4 -5
- package/schemas/audit_task.schema.json +10 -0
- package/schemas/runtime_validation_report.schema.json +1 -1
- package/skills/audit-code/SKILL.md +11 -2
- package/skills/audit-code/audit-code.prompt.md +11 -10
- package/schemas/merged_findings.schema.json +0 -19
- package/schemas/root_cause_clusters.schema.json +0 -28
- package/schemas/synthesis_report.schema.json +0 -61
|
@@ -38,11 +38,9 @@ export function updateRuntimeValidationReport(tasks, existing, updates) {
|
|
|
38
38
|
merged.set(task.id, {
|
|
39
39
|
task_id: task.id,
|
|
40
40
|
status: "pending",
|
|
41
|
-
summary: `
|
|
41
|
+
summary: `Deterministic runtime validation has not executed yet for ${task.id}.`,
|
|
42
42
|
evidence: [],
|
|
43
|
-
notes: [
|
|
44
|
-
"Placeholder entry generated from runtime validation task list.",
|
|
45
|
-
],
|
|
43
|
+
notes: [],
|
|
46
44
|
});
|
|
47
45
|
}
|
|
48
46
|
}
|
|
@@ -43,23 +43,40 @@ export function deriveAuditState(bundle) {
|
|
|
43
43
|
"requeue_tasks.json",
|
|
44
44
|
], planningReady)));
|
|
45
45
|
const hasRequiredCoverage = bundle.coverage_matrix?.files.every((f) => f.required_lenses.every((req) => f.completed_lenses.includes(req))) ?? true;
|
|
46
|
+
const hasCompletedTaskStatuses = bundle.audit_tasks?.length
|
|
47
|
+
? bundle.audit_tasks.every((task) => task.status === "complete")
|
|
48
|
+
: false;
|
|
49
|
+
const hasResultForEveryTask = bundle.audit_tasks?.length && bundle.audit_results
|
|
50
|
+
? bundle.audit_tasks.every((task) => bundle.audit_results?.some((result) => result.task_id === task.task_id))
|
|
51
|
+
: false;
|
|
46
52
|
if (!hasRequiredCoverage &&
|
|
53
|
+
!hasCompletedTaskStatuses &&
|
|
54
|
+
!hasResultForEveryTask &&
|
|
47
55
|
has(bundle.audit_tasks) &&
|
|
48
56
|
(bundle.audit_tasks?.length ?? 0) > 0) {
|
|
49
57
|
obligations.push(obligation("audit_tasks_completed", "missing"));
|
|
50
58
|
}
|
|
51
|
-
else if (hasRequiredCoverage
|
|
59
|
+
else if ((hasRequiredCoverage || hasCompletedTaskStatuses || hasResultForEveryTask) &&
|
|
60
|
+
has(bundle.audit_tasks)) {
|
|
52
61
|
obligations.push(obligation("audit_tasks_completed", "satisfied"));
|
|
53
62
|
}
|
|
54
|
-
obligations.push(obligation("audit_results_ingested",
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
obligations.push(obligation("audit_results_ingested", (bundle.audit_tasks?.length ?? 0) === 0 || has(bundle.audit_results)
|
|
64
|
+
? "present"
|
|
65
|
+
: "missing"));
|
|
66
|
+
const runtimeTasks = bundle.runtime_validation_tasks?.tasks ?? [];
|
|
67
|
+
const runtimeResults = bundle.runtime_validation_report?.results ?? [];
|
|
68
|
+
const runtimeReady = runtimeTasks.length === 0 ||
|
|
69
|
+
(runtimeTasks.length > 0 &&
|
|
70
|
+
runtimeTasks.every((task) => runtimeResults.some((result) => result.task_id === task.id &&
|
|
71
|
+
result.status !== "pending")));
|
|
72
|
+
obligations.push(obligation("runtime_validation_current", runtimeReady
|
|
73
|
+
? "satisfied"
|
|
74
|
+
: has(bundle.runtime_validation_report)
|
|
75
|
+
? "missing"
|
|
76
|
+
: "missing", runtimeTasks.length === 0
|
|
77
|
+
? "No deterministic runtime validation tasks were planned."
|
|
78
|
+
: undefined));
|
|
79
|
+
obligations.push(obligation("synthesis_current", staleOrSatisfied(staleArtifacts, ["audit-report.md"], has(bundle.audit_report))));
|
|
63
80
|
let status = "not_started";
|
|
64
81
|
if (!has(bundle.repo_manifest)) {
|
|
65
82
|
status = "not_started";
|
|
@@ -71,10 +88,7 @@ export function deriveAuditState(bundle) {
|
|
|
71
88
|
status = "active";
|
|
72
89
|
}
|
|
73
90
|
const incomplete = obligations.some((o) => o.state === "missing" || o.state === "stale");
|
|
74
|
-
if (!incomplete &&
|
|
75
|
-
has(bundle.synthesis_report) &&
|
|
76
|
-
has(bundle.merged_findings) &&
|
|
77
|
-
has(bundle.root_cause_clusters)) {
|
|
91
|
+
if (!incomplete && has(bundle.audit_report)) {
|
|
78
92
|
status = "complete";
|
|
79
93
|
}
|
|
80
94
|
return {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isTrivialAuditPath } from "./trivialAudit.js";
|
|
1
2
|
function taskPriority(hasExternalSignal, lens) {
|
|
2
3
|
if (hasExternalSignal &&
|
|
3
4
|
(lens === "security" || lens === "data_integrity" || lens === "reliability")) {
|
|
@@ -57,12 +58,13 @@ export function buildChunkedAuditTasks(unitManifest, unitLineIndex, options = {}
|
|
|
57
58
|
const hasExternalSignal = unit.files.some((f) => externalPaths.has(f));
|
|
58
59
|
const priority = taskPriority(hasExternalSignal, lens);
|
|
59
60
|
const tags = hasExternalSignal ? ["external_analyzer_signal"] : [];
|
|
61
|
+
const candidateFiles = unit.files.filter((filePath) => !isTrivialAuditPath(filePath, unitLineIndex[filePath] ?? 0, externalPaths.has(filePath)));
|
|
60
62
|
// Split files that are individually too large to group; everything else
|
|
61
63
|
// goes into one task so the agent can reason across file boundaries.
|
|
62
64
|
const oversizedFiles = fileSplitThreshold > 0
|
|
63
|
-
?
|
|
65
|
+
? candidateFiles.filter((f) => (unitLineIndex[f] ?? 0) > fileSplitThreshold)
|
|
64
66
|
: [];
|
|
65
|
-
const normalFiles =
|
|
67
|
+
const normalFiles = candidateFiles.filter((f) => !oversizedFiles.includes(f));
|
|
66
68
|
// One task for all normal-sized files in this unit under this lens.
|
|
67
69
|
if (normalFiles.length > 0) {
|
|
68
70
|
const id = `${unit.unit_id}:${lens}`;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { CoverageMatrix } from "../types.js";
|
|
2
|
+
import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
|
|
3
|
+
export declare function isTrivialAuditPath(path: string, lineCount: number, hasExternalSignal?: boolean): boolean;
|
|
4
|
+
export declare function autoCompleteTrivialCoverage(coverage: CoverageMatrix, lineIndex: Record<string, number>, externalAnalyzerResults?: ExternalAnalyzerResults): string[];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const TRIVIAL_DOTFILES = new Set([".gitignore", ".gitattributes"]);
|
|
2
|
+
function basename(path) {
|
|
3
|
+
const normalized = path.replaceAll("\\", "/");
|
|
4
|
+
const parts = normalized.split("/");
|
|
5
|
+
return parts[parts.length - 1] ?? normalized;
|
|
6
|
+
}
|
|
7
|
+
export function isTrivialAuditPath(path, lineCount, hasExternalSignal = false) {
|
|
8
|
+
if (hasExternalSignal) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (lineCount === 0) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
const name = basename(path).toLowerCase();
|
|
15
|
+
if (TRIVIAL_DOTFILES.has(name)) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
// Empty package markers and docstring-only __init__.py files create a lot of
|
|
19
|
+
// audit churn without adding meaningful coverage signal.
|
|
20
|
+
if (name === "__init__.py" && lineCount <= 3) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
if (lineCount <= 1) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
export function autoCompleteTrivialCoverage(coverage, lineIndex, externalAnalyzerResults) {
|
|
29
|
+
const externalPaths = new Set((externalAnalyzerResults?.results ?? []).map((item) => item.path));
|
|
30
|
+
const skipped = [];
|
|
31
|
+
for (const file of coverage.files) {
|
|
32
|
+
if (file.audit_status === "excluded") {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!isTrivialAuditPath(file.path, lineIndex[file.path] ?? 0, externalPaths.has(file.path))) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (file.required_lenses.length === 0) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
file.completed_lenses = [];
|
|
42
|
+
file.required_lenses = [];
|
|
43
|
+
file.audit_status = "excluded";
|
|
44
|
+
file.classification_status = "excluded_trivial";
|
|
45
|
+
file.unit_ids = [];
|
|
46
|
+
skipped.push(file.path);
|
|
47
|
+
}
|
|
48
|
+
return skipped.sort();
|
|
49
|
+
}
|
|
@@ -20,12 +20,16 @@ export function renderWorkerPrompt(task) {
|
|
|
20
20
|
" 2. Review the content under the specified lens.",
|
|
21
21
|
" 3. Emit one AuditResult with:",
|
|
22
22
|
" task_id, unit_id, pass_id, lens",
|
|
23
|
-
"
|
|
23
|
+
" file_coverage: [{path, total_lines}] for every assigned file you reviewed",
|
|
24
24
|
" findings: array (empty if nothing found)",
|
|
25
|
+
" total_lines must match the file's current total line count.",
|
|
25
26
|
" Each finding must include:",
|
|
26
27
|
" id, title, category, severity, confidence, lens, summary, affected_files,",
|
|
27
|
-
" evidence (at least one excerpt or line reference from the file you read)",
|
|
28
|
+
" evidence (an array of plain strings only, at least one excerpt or line reference from the file you read)",
|
|
29
|
+
" Example evidence entry: src/foo.ts:42 - variable overwritten before use",
|
|
28
30
|
" Optional finding fields: impact, likelihood, reproduction, systemic, related_findings",
|
|
31
|
+
" Low-priority tasks still require a real review. Use findings: [] only when you genuinely found nothing notable.",
|
|
32
|
+
`Reference schema: ${task.artifacts_dir}/dispatch/audit-result.schema.json`,
|
|
29
33
|
`Write the AuditResult[] JSON array to: ${task.audit_results_path}`,
|
|
30
34
|
];
|
|
31
35
|
if (task.skip_worker_command) {
|
|
@@ -11,14 +11,25 @@ export async function spawnLoggedCommand(command, args, input, env) {
|
|
|
11
11
|
return await new Promise((resolve, reject) => {
|
|
12
12
|
const stdoutLog = createWriteStream(input.stdoutPath, { flags: "a" });
|
|
13
13
|
const stderrLog = createWriteStream(input.stderrPath, { flags: "a" });
|
|
14
|
+
const startedAt = Date.now();
|
|
15
|
+
let timedOut = false;
|
|
14
16
|
const child = spawn(command, args, {
|
|
15
17
|
cwd: input.repoRoot,
|
|
16
18
|
env: { ...process.env, ...env },
|
|
17
19
|
stdio: ["ignore", "pipe", "pipe"],
|
|
18
20
|
});
|
|
19
21
|
const timer = setTimeout(() => {
|
|
22
|
+
timedOut = true;
|
|
20
23
|
child.kill("SIGTERM");
|
|
21
24
|
}, input.timeoutMs);
|
|
25
|
+
const heartbeat = setInterval(() => {
|
|
26
|
+
const elapsedMs = Date.now() - startedAt;
|
|
27
|
+
const message = `[provider] run ${input.runId} still running after ${elapsedMs}ms\n`;
|
|
28
|
+
tee(stderrLog, message);
|
|
29
|
+
if (input.uiMode === "visible") {
|
|
30
|
+
process.stderr.write(message);
|
|
31
|
+
}
|
|
32
|
+
}, 30_000);
|
|
22
33
|
child.stdout.on("data", (chunk) => {
|
|
23
34
|
tee(stdoutLog, chunk);
|
|
24
35
|
if (input.uiMode === "visible") {
|
|
@@ -33,14 +44,20 @@ export async function spawnLoggedCommand(command, args, input, env) {
|
|
|
33
44
|
});
|
|
34
45
|
child.on("error", (error) => {
|
|
35
46
|
clearTimeout(timer);
|
|
47
|
+
clearInterval(heartbeat);
|
|
36
48
|
stdoutLog.end();
|
|
37
49
|
stderrLog.end();
|
|
38
50
|
reject(error);
|
|
39
51
|
});
|
|
40
52
|
child.on("exit", (code, signal) => {
|
|
41
53
|
clearTimeout(timer);
|
|
54
|
+
clearInterval(heartbeat);
|
|
42
55
|
stdoutLog.end();
|
|
43
56
|
stderrLog.end();
|
|
57
|
+
if (timedOut) {
|
|
58
|
+
reject(new Error(`Fresh session timed out after ${input.timeoutMs}ms for run ${input.runId}.`));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
44
61
|
resolve({
|
|
45
62
|
accepted: true,
|
|
46
63
|
processId: child.pid,
|
|
@@ -42,7 +42,9 @@ function runtimeSummary(report) {
|
|
|
42
42
|
if (!report) {
|
|
43
43
|
return [];
|
|
44
44
|
}
|
|
45
|
-
return report.results
|
|
45
|
+
return report.results
|
|
46
|
+
.filter((result) => result.status !== "pending")
|
|
47
|
+
.map((result) => `${result.task_id}: ${result.status} — ${result.summary}`);
|
|
46
48
|
}
|
|
47
49
|
function externalSummary(results) {
|
|
48
50
|
if (!results) {
|
|
@@ -80,9 +82,6 @@ export function mergeFindings(results, runtimeReport, externalAnalyzerResults) {
|
|
|
80
82
|
...analyzerEvidence,
|
|
81
83
|
]),
|
|
82
84
|
],
|
|
83
|
-
related_findings: [
|
|
84
|
-
...new Set([...(finding.related_findings ?? []), finding.id]),
|
|
85
|
-
],
|
|
86
85
|
});
|
|
87
86
|
continue;
|
|
88
87
|
}
|
|
@@ -108,13 +107,6 @@ export function mergeFindings(results, runtimeReport, externalAnalyzerResults) {
|
|
|
108
107
|
...analyzerEvidence,
|
|
109
108
|
]),
|
|
110
109
|
];
|
|
111
|
-
existing.related_findings = [
|
|
112
|
-
...new Set([
|
|
113
|
-
...(existing.related_findings ?? []),
|
|
114
|
-
...(finding.related_findings ?? []),
|
|
115
|
-
finding.id,
|
|
116
|
-
]),
|
|
117
|
-
];
|
|
118
110
|
}
|
|
119
111
|
}
|
|
120
112
|
return [...merged.values()].sort((a, b) => {
|
|
@@ -29,14 +29,94 @@ function summarizeExternal(results) {
|
|
|
29
29
|
return "No external analyzer signals attached.";
|
|
30
30
|
return `${results.tool}:${results.results.length}`;
|
|
31
31
|
}
|
|
32
|
+
const STOP_WORDS = new Set([
|
|
33
|
+
"a",
|
|
34
|
+
"an",
|
|
35
|
+
"and",
|
|
36
|
+
"are",
|
|
37
|
+
"be",
|
|
38
|
+
"by",
|
|
39
|
+
"for",
|
|
40
|
+
"from",
|
|
41
|
+
"in",
|
|
42
|
+
"into",
|
|
43
|
+
"is",
|
|
44
|
+
"missing",
|
|
45
|
+
"not",
|
|
46
|
+
"of",
|
|
47
|
+
"on",
|
|
48
|
+
"or",
|
|
49
|
+
"that",
|
|
50
|
+
"the",
|
|
51
|
+
"this",
|
|
52
|
+
"to",
|
|
53
|
+
"under",
|
|
54
|
+
"when",
|
|
55
|
+
"with",
|
|
56
|
+
]);
|
|
57
|
+
function normalizeToken(token) {
|
|
58
|
+
if (token.endsWith("ies") && token.length > 4) {
|
|
59
|
+
return `${token.slice(0, -3)}y`;
|
|
60
|
+
}
|
|
61
|
+
if (token.endsWith("ing") && token.length > 6) {
|
|
62
|
+
return token.slice(0, -3);
|
|
63
|
+
}
|
|
64
|
+
if (token.endsWith("ed") && token.length > 5) {
|
|
65
|
+
return token.slice(0, -2);
|
|
66
|
+
}
|
|
67
|
+
if (token.endsWith("s") && token.length > 4) {
|
|
68
|
+
return token.slice(0, -1);
|
|
69
|
+
}
|
|
70
|
+
return token;
|
|
71
|
+
}
|
|
72
|
+
function extractSemanticTerms(finding) {
|
|
73
|
+
const source = [
|
|
74
|
+
finding.title,
|
|
75
|
+
finding.summary,
|
|
76
|
+
...(finding.evidence ?? []).slice(0, 2),
|
|
77
|
+
]
|
|
78
|
+
.join(" ")
|
|
79
|
+
.toLowerCase()
|
|
80
|
+
.replace(/[^a-z0-9]+/g, " ");
|
|
81
|
+
const terms = source
|
|
82
|
+
.split(/\s+/)
|
|
83
|
+
.map(normalizeToken)
|
|
84
|
+
.filter((token) => token.length >= 3 &&
|
|
85
|
+
!STOP_WORDS.has(token) &&
|
|
86
|
+
token !== finding.lens &&
|
|
87
|
+
token !== finding.category &&
|
|
88
|
+
!/^\d+$/.test(token));
|
|
89
|
+
return [...new Set(terms)];
|
|
90
|
+
}
|
|
32
91
|
function clusterKey(finding) {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
92
|
+
const semanticTerms = extractSemanticTerms(finding).slice(0, 3).join(" ");
|
|
93
|
+
return `${finding.lens}:${finding.category}:${semanticTerms || finding.title.toLowerCase()}`;
|
|
94
|
+
}
|
|
95
|
+
function representativeFinding(grouped) {
|
|
96
|
+
return [...grouped].sort((a, b) => {
|
|
97
|
+
const severityDelta = severityRank(b.severity) - severityRank(a.severity);
|
|
98
|
+
if (severityDelta !== 0)
|
|
99
|
+
return severityDelta;
|
|
100
|
+
const systemicDelta = Number(Boolean(b.systemic)) - Number(Boolean(a.systemic));
|
|
101
|
+
if (systemicDelta !== 0)
|
|
102
|
+
return systemicDelta;
|
|
103
|
+
return a.title.localeCompare(b.title);
|
|
104
|
+
})[0];
|
|
105
|
+
}
|
|
106
|
+
function titleForCluster(grouped) {
|
|
107
|
+
return representativeFinding(grouped).title;
|
|
36
108
|
}
|
|
37
|
-
function
|
|
38
|
-
const
|
|
39
|
-
|
|
109
|
+
function summarizeFiles(grouped) {
|
|
110
|
+
const paths = [
|
|
111
|
+
...new Set(grouped.flatMap((finding) => finding.affected_files.map((file) => file.path))),
|
|
112
|
+
].sort();
|
|
113
|
+
if (paths.length === 0) {
|
|
114
|
+
return "no representative files recorded";
|
|
115
|
+
}
|
|
116
|
+
if (paths.length <= 3) {
|
|
117
|
+
return paths.join(", ");
|
|
118
|
+
}
|
|
119
|
+
return `${paths.slice(0, 3).join(", ")}, +${paths.length - 3} more`;
|
|
40
120
|
}
|
|
41
121
|
export function buildRootCauseClusters(findings, runtimeReport, externalAnalyzerResults) {
|
|
42
122
|
const groups = new Map();
|
|
@@ -50,12 +130,15 @@ export function buildRootCauseClusters(findings, runtimeReport, externalAnalyzer
|
|
|
50
130
|
const externalSummary = summarizeExternal(externalAnalyzerResults);
|
|
51
131
|
return [...groups.entries()]
|
|
52
132
|
.map(([key, grouped], index) => {
|
|
53
|
-
const
|
|
133
|
+
const representative = representativeFinding(grouped);
|
|
54
134
|
const systemicCount = grouped.filter((finding) => finding.systemic).length;
|
|
135
|
+
const theme = key.split(":").slice(2).join(":") || representative.title;
|
|
55
136
|
return {
|
|
56
137
|
id: `cluster-${index + 1}`,
|
|
57
|
-
title: titleForCluster(
|
|
58
|
-
summary: `
|
|
138
|
+
title: titleForCluster(grouped),
|
|
139
|
+
summary: `Theme "${theme}" groups ${grouped.length} finding(s) across ${summarizeFiles(grouped)}; ` +
|
|
140
|
+
`highest severity ${representative.severity}; systemic flags ${systemicCount}. ` +
|
|
141
|
+
`Runtime validation status: ${runtimeSummary}. External analyzer summary: ${externalSummary}.`,
|
|
59
142
|
finding_ids: grouped.map((finding) => finding.id),
|
|
60
143
|
};
|
|
61
144
|
})
|
|
@@ -1,24 +1,27 @@
|
|
|
1
|
-
import type { AuditResult } from "../types.js";
|
|
2
|
-
import type {
|
|
1
|
+
import type { AuditResult, CoverageMatrix, Finding, UnitManifest } from "../types.js";
|
|
2
|
+
import type { CriticalFlowManifest } from "../types/flows.js";
|
|
3
|
+
import type { GraphBundle } from "../types/graph.js";
|
|
3
4
|
import type { RuntimeValidationReport } from "../types/runtimeValidation.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
runtime_validation_status_breakdown: Record<string, number>;
|
|
13
|
-
notes: string[];
|
|
14
|
-
severity_breakdown: Record<string, number>;
|
|
15
|
-
external_analyzer_summary: {
|
|
16
|
-
tool_count: number;
|
|
17
|
-
result_count: number;
|
|
18
|
-
};
|
|
19
|
-
};
|
|
20
|
-
merged_findings: ReturnType<typeof mergeFindings>;
|
|
21
|
-
root_cause_clusters: ReturnType<typeof buildRootCauseClusters>;
|
|
22
|
-
work_blocks: ReturnType<typeof buildWorkBlocks>;
|
|
5
|
+
import { type WorkBlock } from "./workBlocks.js";
|
|
6
|
+
export interface AuditReportSummary {
|
|
7
|
+
finding_count: number;
|
|
8
|
+
work_block_count: number;
|
|
9
|
+
severity_breakdown: Record<string, number>;
|
|
10
|
+
audited_file_count: number;
|
|
11
|
+
excluded_file_count: number;
|
|
12
|
+
runtime_validation_status_breakdown: Record<string, number>;
|
|
23
13
|
}
|
|
24
|
-
export
|
|
14
|
+
export interface AuditReportModel {
|
|
15
|
+
summary: AuditReportSummary;
|
|
16
|
+
findings: Finding[];
|
|
17
|
+
work_blocks: WorkBlock[];
|
|
18
|
+
}
|
|
19
|
+
export declare function buildAuditReportModel(params: {
|
|
20
|
+
results: AuditResult[];
|
|
21
|
+
unitManifest?: UnitManifest;
|
|
22
|
+
graphBundle?: GraphBundle;
|
|
23
|
+
criticalFlows?: CriticalFlowManifest;
|
|
24
|
+
coverageMatrix?: CoverageMatrix;
|
|
25
|
+
runtimeValidationReport?: RuntimeValidationReport;
|
|
26
|
+
}): AuditReportModel;
|
|
27
|
+
export declare function renderAuditReportMarkdown(model: AuditReportModel): string;
|
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
import { mergeFindings } from "./mergeFindings.js";
|
|
2
|
-
import { buildRootCauseClusters } from "./rootCause.js";
|
|
3
1
|
import { buildWorkBlocks } from "./workBlocks.js";
|
|
4
|
-
|
|
5
|
-
const breakdown = {};
|
|
6
|
-
for (const result of report?.results ?? []) {
|
|
7
|
-
breakdown[result.status] = (breakdown[result.status] ?? 0) + 1;
|
|
8
|
-
}
|
|
9
|
-
return breakdown;
|
|
10
|
-
}
|
|
2
|
+
import { mergeFindings } from "./mergeFindings.js";
|
|
11
3
|
function severityBreakdown(findings) {
|
|
12
4
|
const breakdown = {};
|
|
13
5
|
for (const finding of findings) {
|
|
@@ -15,63 +7,104 @@ function severityBreakdown(findings) {
|
|
|
15
7
|
}
|
|
16
8
|
return breakdown;
|
|
17
9
|
}
|
|
18
|
-
function
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
tasksByLens.set(result.lens, (tasksByLens.get(result.lens) ?? 0) + 1);
|
|
23
|
-
findingsByLens.set(result.lens, (findingsByLens.get(result.lens) ?? 0) + result.findings.length);
|
|
10
|
+
function runtimeStatusBreakdown(report) {
|
|
11
|
+
const breakdown = {};
|
|
12
|
+
for (const result of report?.results ?? []) {
|
|
13
|
+
breakdown[result.status] = (breakdown[result.status] ?? 0) + 1;
|
|
24
14
|
}
|
|
25
|
-
|
|
26
|
-
.filter(([lens, count]) => count > 0 && (findingsByLens.get(lens) ?? 0) === 0)
|
|
27
|
-
.map(([lens]) => lens)
|
|
28
|
-
.sort();
|
|
29
|
-
if (zeroLenses.length === 0)
|
|
30
|
-
return [];
|
|
31
|
-
return [
|
|
32
|
-
`Zero findings across all reviewed tasks for lens(es): ${zeroLenses.join(", ")}. Verify these tasks were genuinely reviewed rather than batch-generated.`,
|
|
33
|
-
];
|
|
15
|
+
return breakdown;
|
|
34
16
|
}
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
return { tool_count: 0, result_count: 0 };
|
|
38
|
-
}
|
|
17
|
+
function coverageSummary(coverage) {
|
|
18
|
+
const files = coverage?.files ?? [];
|
|
39
19
|
return {
|
|
40
|
-
|
|
41
|
-
|
|
20
|
+
audited_file_count: files.filter((file) => file.audit_status === "complete").length,
|
|
21
|
+
excluded_file_count: files.filter((file) => file.audit_status === "excluded").length,
|
|
42
22
|
};
|
|
43
23
|
}
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
24
|
+
function formatSeverityList(summary) {
|
|
25
|
+
const ordered = ["critical", "high", "medium", "low", "info"];
|
|
26
|
+
const parts = ordered
|
|
27
|
+
.filter((severity) => (summary[severity] ?? 0) > 0)
|
|
28
|
+
.map((severity) => `${severity}: ${summary[severity]}`);
|
|
29
|
+
return parts.length > 0 ? parts.join(", ") : "none";
|
|
30
|
+
}
|
|
31
|
+
export function buildAuditReportModel(params) {
|
|
32
|
+
const findings = mergeFindings(params.results, params.runtimeValidationReport);
|
|
33
|
+
const workBlocks = buildWorkBlocks({
|
|
34
|
+
findings,
|
|
35
|
+
unitManifest: params.unitManifest,
|
|
36
|
+
graphBundle: params.graphBundle,
|
|
37
|
+
criticalFlows: params.criticalFlows,
|
|
38
|
+
});
|
|
39
|
+
const coverage = coverageSummary(params.coverageMatrix);
|
|
51
40
|
return {
|
|
52
41
|
summary: {
|
|
53
|
-
finding_count:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
notes: [
|
|
60
|
-
...(Object.keys(runtimeBreakdown).length === 0
|
|
61
|
-
? ["No runtime validation evidence attached."]
|
|
62
|
-
: [
|
|
63
|
-
"Runtime validation evidence has been incorporated into synthesis.",
|
|
64
|
-
]),
|
|
65
|
-
...(extSummary.result_count > 0
|
|
66
|
-
? [
|
|
67
|
-
`External analyzer signals incorporated: ${extSummary.result_count} result(s) from ${externalAnalyzerResults?.tool}.`,
|
|
68
|
-
]
|
|
69
|
-
: []),
|
|
70
|
-
...zeroFindingLensNotes(results),
|
|
71
|
-
],
|
|
42
|
+
finding_count: findings.length,
|
|
43
|
+
work_block_count: workBlocks.length,
|
|
44
|
+
severity_breakdown: severityBreakdown(findings),
|
|
45
|
+
audited_file_count: coverage.audited_file_count,
|
|
46
|
+
excluded_file_count: coverage.excluded_file_count,
|
|
47
|
+
runtime_validation_status_breakdown: runtimeStatusBreakdown(params.runtimeValidationReport),
|
|
72
48
|
},
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
work_blocks,
|
|
49
|
+
findings,
|
|
50
|
+
work_blocks: workBlocks,
|
|
76
51
|
};
|
|
77
52
|
}
|
|
53
|
+
export function renderAuditReportMarkdown(model) {
|
|
54
|
+
const lines = [
|
|
55
|
+
"# Audit Report",
|
|
56
|
+
"",
|
|
57
|
+
"## Summary",
|
|
58
|
+
"",
|
|
59
|
+
`- Findings: ${model.summary.finding_count}`,
|
|
60
|
+
`- Work blocks: ${model.summary.work_block_count}`,
|
|
61
|
+
`- Severity breakdown: ${formatSeverityList(model.summary.severity_breakdown)}`,
|
|
62
|
+
`- Fully audited files: ${model.summary.audited_file_count}`,
|
|
63
|
+
`- Excluded non-auditable files: ${model.summary.excluded_file_count}`,
|
|
64
|
+
"",
|
|
65
|
+
"## Work Blocks",
|
|
66
|
+
"",
|
|
67
|
+
];
|
|
68
|
+
if (model.work_blocks.length === 0) {
|
|
69
|
+
lines.push("No remediation work blocks were generated.", "");
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
for (const block of model.work_blocks) {
|
|
73
|
+
lines.push(`### ${block.id}`);
|
|
74
|
+
lines.push("");
|
|
75
|
+
lines.push(`- Max severity: ${block.max_severity}`);
|
|
76
|
+
lines.push(`- Units: ${block.unit_ids.join(", ")}`);
|
|
77
|
+
lines.push(`- Owned files: ${block.owned_files.join(", ")}`);
|
|
78
|
+
lines.push(`- Findings: ${block.finding_ids.join(", ")}`);
|
|
79
|
+
lines.push(`- Depends on: ${block.depends_on.length > 0 ? block.depends_on.join(", ") : "none"}`);
|
|
80
|
+
lines.push(`- Rationale: ${block.rationale}`);
|
|
81
|
+
lines.push("");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
lines.push("## Findings", "");
|
|
85
|
+
if (model.findings.length === 0) {
|
|
86
|
+
lines.push("No findings were recorded.", "");
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
for (const finding of model.findings) {
|
|
90
|
+
lines.push(`### ${finding.id} — ${finding.title}`);
|
|
91
|
+
lines.push("");
|
|
92
|
+
lines.push(`- Severity: ${finding.severity}`);
|
|
93
|
+
lines.push(`- Confidence: ${finding.confidence}`);
|
|
94
|
+
lines.push(`- Lens: ${finding.lens}`);
|
|
95
|
+
lines.push(`- Files: ${finding.affected_files.map((file) => file.path).join(", ")}`);
|
|
96
|
+
lines.push(`- Summary: ${finding.summary}`);
|
|
97
|
+
if (finding.evidence && finding.evidence.length > 0) {
|
|
98
|
+
lines.push("- Evidence:");
|
|
99
|
+
for (const evidence of finding.evidence) {
|
|
100
|
+
lines.push(` - ${evidence}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
lines.push("");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
lines.push("## Scope and Coverage", "");
|
|
107
|
+
lines.push("This report is deterministic output from the completed audit. Non-auditable files were excluded from scope before task generation.");
|
|
108
|
+
lines.push("");
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import type { Finding } from "../types.js";
|
|
1
|
+
import type { Finding, UnitManifest } from "../types.js";
|
|
2
|
+
import type { CriticalFlowManifest } from "../types/flows.js";
|
|
3
|
+
import type { GraphBundle } from "../types/graph.js";
|
|
2
4
|
export interface WorkBlock {
|
|
3
5
|
id: string;
|
|
4
6
|
finding_ids: string[];
|
|
5
|
-
|
|
7
|
+
unit_ids: string[];
|
|
8
|
+
owned_files: string[];
|
|
6
9
|
max_severity: Finding["severity"];
|
|
7
10
|
rationale: string;
|
|
11
|
+
depends_on: string[];
|
|
8
12
|
}
|
|
9
|
-
export declare function buildWorkBlocks(
|
|
13
|
+
export declare function buildWorkBlocks(params: {
|
|
14
|
+
findings: Finding[];
|
|
15
|
+
unitManifest?: UnitManifest;
|
|
16
|
+
graphBundle?: GraphBundle;
|
|
17
|
+
criticalFlows?: CriticalFlowManifest;
|
|
18
|
+
}): WorkBlock[];
|