auditor-lambda 0.2.5 → 0.2.6
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 +12 -0
- package/audit-code-wrapper-lib.mjs +7 -1
- package/dist/cli.js +324 -27
- package/dist/io/runArtifacts.d.ts +2 -1
- package/dist/io/runArtifacts.js +2 -1
- package/dist/orchestrator/flowRequeue.d.ts +2 -2
- package/dist/orchestrator/flowRequeue.js +15 -2
- package/dist/orchestrator/internalExecutors.js +34 -10
- package/dist/orchestrator/requeue.js +1 -0
- package/dist/orchestrator/requeueCommand.js +15 -2
- package/dist/orchestrator/resultIngestion.d.ts +2 -1
- package/dist/orchestrator/resultIngestion.js +21 -0
- package/dist/orchestrator/state.js +10 -1
- package/dist/orchestrator/taskBuilder.js +4 -2
- package/dist/orchestrator/trivialAudit.d.ts +4 -0
- package/dist/orchestrator/trivialAudit.js +46 -0
- package/dist/prompts/renderWorkerPrompt.js +5 -2
- package/dist/providers/spawnLoggedCommand.js +17 -0
- package/dist/reporting/mergeFindings.js +14 -11
- package/dist/reporting/rootCause.js +92 -9
- package/dist/supervisor/sessionConfig.js +4 -2
- package/dist/types.d.ts +5 -0
- package/dist/validation/auditResults.d.ts +5 -2
- package/dist/validation/auditResults.js +369 -42
- package/docs/artifacts.md +8 -1
- package/docs/contract.md +118 -27
- package/docs/field-trial-bug-report.md +237 -0
- package/docs/session-config.md +20 -1
- package/docs/usage.md +22 -0
- package/package.json +1 -1
- package/schemas/audit_result.schema.json +3 -2
- package/schemas/audit_task.schema.json +10 -0
- package/skills/audit-code/audit-code.prompt.md +12 -8
|
@@ -19,7 +19,14 @@ function taskPriority(hasExternalSignal, lens) {
|
|
|
19
19
|
}
|
|
20
20
|
return hasExternalSignal ? "medium" : "low";
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
function fileStillNeedsLens(coverageMatrix, path, lens) {
|
|
23
|
+
const record = coverageMatrix.files.find((file) => file.path === path);
|
|
24
|
+
if (!record || record.audit_status === "excluded") {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
return !record.completed_lenses.includes(lens);
|
|
28
|
+
}
|
|
29
|
+
export function buildFlowRequeueTasks(criticalFlows, flowCoverage, coverageMatrix, externalAnalyzerResults) {
|
|
23
30
|
const flowMap = new Map(criticalFlows.flows.map((flow) => [flow.id, flow]));
|
|
24
31
|
const tasks = [];
|
|
25
32
|
const seen = new Set();
|
|
@@ -35,6 +42,9 @@ export function buildFlowRequeueTasks(criticalFlows, flowCoverage, externalAnaly
|
|
|
35
42
|
continue;
|
|
36
43
|
}
|
|
37
44
|
for (const path of flow.paths) {
|
|
45
|
+
if (!fileStillNeedsLens(coverageMatrix, path, lensName)) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
38
48
|
const signature = `${flow.id}|${lensName}|${path}`;
|
|
39
49
|
if (seen.has(signature)) {
|
|
40
50
|
continue;
|
|
@@ -49,7 +59,10 @@ export function buildFlowRequeueTasks(criticalFlows, flowCoverage, externalAnaly
|
|
|
49
59
|
file_paths: [path],
|
|
50
60
|
rationale: `Requeue ${path} because critical flow ${flow.id} is still missing the ${lensName} lens.${hasExternalSignal ? " External analyzer signals make this follow-up higher priority." : ""}`,
|
|
51
61
|
priority: taskPriority(hasExternalSignal, lensName),
|
|
52
|
-
tags: hasExternalSignal
|
|
62
|
+
tags: hasExternalSignal
|
|
63
|
+
? ["critical_flow_followup", "external_analyzer_signal"]
|
|
64
|
+
: ["critical_flow_followup"],
|
|
65
|
+
status: "pending",
|
|
53
66
|
});
|
|
54
67
|
}
|
|
55
68
|
}
|
|
@@ -13,8 +13,16 @@ import { buildChunkedAuditTasks, buildExternalSignalTasks, } from "./taskBuilder
|
|
|
13
13
|
import { buildUnitManifest } from "./unitBuilder.js";
|
|
14
14
|
import { buildRepoManifestFromFs } from "../extractors/fsIntake.js";
|
|
15
15
|
import { loadIgnoreFile } from "../extractors/ignore.js";
|
|
16
|
-
import { ingestAuditResults } from "./resultIngestion.js";
|
|
16
|
+
import { ingestAuditResults, updateAuditTaskStatuses, } from "./resultIngestion.js";
|
|
17
17
|
import { updateRuntimeValidationReport } from "./runtimeValidationUpdate.js";
|
|
18
|
+
import { autoCompleteTrivialCoverage } from "./trivialAudit.js";
|
|
19
|
+
function buildSynthesisArtifacts(synthesisReport) {
|
|
20
|
+
return {
|
|
21
|
+
synthesis_report: synthesisReport,
|
|
22
|
+
merged_findings: { findings: synthesisReport.merged_findings },
|
|
23
|
+
root_cause_clusters: { clusters: synthesisReport.root_cause_clusters },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
18
26
|
function preserveOrPlaceholder(tasks, existing) {
|
|
19
27
|
return existing
|
|
20
28
|
? mergeRuntimeValidationReport(tasks, existing)
|
|
@@ -83,6 +91,7 @@ export function runPlanningExecutor(bundle, lineIndex = {}) {
|
|
|
83
91
|
}
|
|
84
92
|
const externalAnalyzerResults = bundle.external_analyzer_results;
|
|
85
93
|
const coverage = initializeCoverageFromPlan(bundle.repo_manifest, bundle.unit_manifest, bundle.file_disposition, externalAnalyzerResults);
|
|
94
|
+
const autoSkippedTrivialFiles = autoCompleteTrivialCoverage(coverage, lineIndex, externalAnalyzerResults);
|
|
86
95
|
const flowCoverage = buildFlowCoverage(bundle.critical_flows, coverage);
|
|
87
96
|
const runtimeValidationTasks = buildRuntimeValidationTasks(bundle.unit_manifest, bundle.critical_flows, flowCoverage);
|
|
88
97
|
const runtimeValidationReport = preserveOrPlaceholder(runtimeValidationTasks, bundle.runtime_validation_report);
|
|
@@ -91,7 +100,10 @@ export function runPlanningExecutor(bundle, lineIndex = {}) {
|
|
|
91
100
|
});
|
|
92
101
|
const analyzerTasks = buildExternalSignalTasks(coverage, lineIndex, externalAnalyzerResults);
|
|
93
102
|
const flowTasks = buildFlowAwareTaskAugmentations([...baseTasks, ...analyzerTasks], bundle.critical_flows, lineIndex);
|
|
94
|
-
const auditTasks = [...baseTasks, ...analyzerTasks, ...flowTasks]
|
|
103
|
+
const auditTasks = [...baseTasks, ...analyzerTasks, ...flowTasks].map((task) => ({
|
|
104
|
+
...task,
|
|
105
|
+
status: task.status ?? "pending",
|
|
106
|
+
}));
|
|
95
107
|
const requeuePayload = buildRequeuePayload(coverage, bundle.critical_flows, flowCoverage, externalAnalyzerResults);
|
|
96
108
|
return {
|
|
97
109
|
updated: {
|
|
@@ -111,7 +123,13 @@ export function runPlanningExecutor(bundle, lineIndex = {}) {
|
|
|
111
123
|
"audit_tasks.json",
|
|
112
124
|
"requeue_tasks.json",
|
|
113
125
|
],
|
|
114
|
-
progress_summary: `Built planning artifacts; generated ${auditTasks.length} tasks and ${requeuePayload.task_count} requeue tasks
|
|
126
|
+
progress_summary: `Built planning artifacts; generated ${auditTasks.length} tasks and ${requeuePayload.task_count} requeue tasks.` +
|
|
127
|
+
(autoSkippedTrivialFiles.length > 0
|
|
128
|
+
? ` Auto-completed ${autoSkippedTrivialFiles.length} trivial file${autoSkippedTrivialFiles.length === 1 ? "" : "s"} without dispatch.`
|
|
129
|
+
: "") +
|
|
130
|
+
(externalAnalyzerResults?.results.length
|
|
131
|
+
? ` External analyzer signals influenced lenses and produced ${analyzerTasks.length} dedicated follow-up task(s).`
|
|
132
|
+
: ""),
|
|
115
133
|
};
|
|
116
134
|
}
|
|
117
135
|
export function runResultIngestionExecutor(bundle, results) {
|
|
@@ -130,7 +148,9 @@ export function runResultIngestionExecutor(bundle, results) {
|
|
|
130
148
|
: bundle.runtime_validation_report;
|
|
131
149
|
const requeuePayload = buildRequeuePayload(updatedCoverageMatrix, bundle.critical_flows, flowCoverage, bundle.external_analyzer_results);
|
|
132
150
|
const mergedResults = [...(bundle.audit_results ?? []), ...results];
|
|
151
|
+
const updatedAuditTasks = updateAuditTaskStatuses(bundle.audit_tasks, mergedResults);
|
|
133
152
|
const synthesisReport = buildSynthesisReport(mergedResults, runtimeValidationReport, bundle.external_analyzer_results);
|
|
153
|
+
const synthesisArtifacts = buildSynthesisArtifacts(synthesisReport);
|
|
134
154
|
return {
|
|
135
155
|
updated: {
|
|
136
156
|
...bundle,
|
|
@@ -139,8 +159,9 @@ export function runResultIngestionExecutor(bundle, results) {
|
|
|
139
159
|
runtime_validation_tasks: runtimeValidationTasks,
|
|
140
160
|
runtime_validation_report: runtimeValidationReport,
|
|
141
161
|
audit_results: mergedResults,
|
|
162
|
+
audit_tasks: updatedAuditTasks,
|
|
142
163
|
requeue_tasks: requeuePayload.tasks,
|
|
143
|
-
|
|
164
|
+
...synthesisArtifacts,
|
|
144
165
|
},
|
|
145
166
|
artifacts_written: [
|
|
146
167
|
"coverage_matrix.json",
|
|
@@ -148,7 +169,10 @@ export function runResultIngestionExecutor(bundle, results) {
|
|
|
148
169
|
"runtime_validation_tasks.json",
|
|
149
170
|
"runtime_validation_report.json",
|
|
150
171
|
"audit_results.jsonl",
|
|
172
|
+
"audit_tasks.json",
|
|
151
173
|
"requeue_tasks.json",
|
|
174
|
+
"merged_findings.json",
|
|
175
|
+
"root_cause_clusters.json",
|
|
152
176
|
"synthesis_report.json",
|
|
153
177
|
],
|
|
154
178
|
progress_summary: `Ingested ${results.length} audit result entries and refreshed dependent artifacts.`,
|
|
@@ -162,14 +186,17 @@ export function runRuntimeValidationUpdateExecutor(bundle, updates) {
|
|
|
162
186
|
buildPlaceholderRuntimeValidationReport(bundle.runtime_validation_tasks);
|
|
163
187
|
const mergedReport = updateRuntimeValidationReport(bundle.runtime_validation_tasks, existingReport, updates);
|
|
164
188
|
const synthesisReport = buildSynthesisReport(bundle.audit_results ?? [], mergedReport, bundle.external_analyzer_results);
|
|
189
|
+
const synthesisArtifacts = buildSynthesisArtifacts(synthesisReport);
|
|
165
190
|
return {
|
|
166
191
|
updated: {
|
|
167
192
|
...bundle,
|
|
168
193
|
runtime_validation_report: mergedReport,
|
|
169
|
-
|
|
194
|
+
...synthesisArtifacts,
|
|
170
195
|
},
|
|
171
196
|
artifacts_written: [
|
|
172
197
|
"runtime_validation_report.json",
|
|
198
|
+
"merged_findings.json",
|
|
199
|
+
"root_cause_clusters.json",
|
|
173
200
|
"synthesis_report.json",
|
|
174
201
|
],
|
|
175
202
|
progress_summary: `Merged ${updates.results.length} runtime validation updates.`,
|
|
@@ -178,15 +205,12 @@ export function runRuntimeValidationUpdateExecutor(bundle, updates) {
|
|
|
178
205
|
export function runSynthesisExecutor(bundle, results) {
|
|
179
206
|
const finalResults = results ?? bundle.audit_results ?? [];
|
|
180
207
|
const synthesisReport = buildSynthesisReport(finalResults, bundle.runtime_validation_report, bundle.external_analyzer_results);
|
|
181
|
-
const
|
|
182
|
-
const rootCauseClusters = { clusters: synthesisReport.root_cause_clusters };
|
|
208
|
+
const synthesisArtifacts = buildSynthesisArtifacts(synthesisReport);
|
|
183
209
|
return {
|
|
184
210
|
updated: {
|
|
185
211
|
...bundle,
|
|
186
212
|
audit_results: finalResults,
|
|
187
|
-
|
|
188
|
-
root_cause_clusters: rootCauseClusters,
|
|
189
|
-
synthesis_report: synthesisReport,
|
|
213
|
+
...synthesisArtifacts,
|
|
190
214
|
},
|
|
191
215
|
artifacts_written: [
|
|
192
216
|
"merged_findings.json",
|
|
@@ -18,6 +18,7 @@ export function buildRequeueTasks(matrix, externalAnalyzerResults) {
|
|
|
18
18
|
rationale: `Requeue ${target.path} because the ${lens} lens is still missing.${hasExternalSignal ? " External analyzer signals make this follow-up higher priority." : ""}`,
|
|
19
19
|
priority: taskPriority(hasExternalSignal),
|
|
20
20
|
tags: hasExternalSignal ? ["external_analyzer_signal"] : [],
|
|
21
|
+
status: "pending",
|
|
21
22
|
});
|
|
22
23
|
}
|
|
23
24
|
}
|
|
@@ -12,12 +12,25 @@ function dedupeTasks(tasks) {
|
|
|
12
12
|
}
|
|
13
13
|
return deduped;
|
|
14
14
|
}
|
|
15
|
+
function dedupeByScope(tasks) {
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
const deduped = [];
|
|
18
|
+
for (const task of tasks) {
|
|
19
|
+
const signature = `${task.lens}:${[...task.file_paths].sort().join(",")}`;
|
|
20
|
+
if (seen.has(signature)) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
seen.add(signature);
|
|
24
|
+
deduped.push(task);
|
|
25
|
+
}
|
|
26
|
+
return deduped;
|
|
27
|
+
}
|
|
15
28
|
export function buildRequeuePayload(matrix, criticalFlows, flowCoverage, externalAnalyzerResults) {
|
|
16
29
|
const fileTasks = dedupeTasks(buildRequeueTasks(matrix, externalAnalyzerResults));
|
|
17
30
|
const flowTasks = criticalFlows && flowCoverage
|
|
18
|
-
? dedupeTasks(buildFlowRequeueTasks(criticalFlows, flowCoverage, externalAnalyzerResults))
|
|
31
|
+
? dedupeTasks(buildFlowRequeueTasks(criticalFlows, flowCoverage, matrix, externalAnalyzerResults))
|
|
19
32
|
: [];
|
|
20
|
-
const tasks = dedupeTasks([...fileTasks, ...flowTasks]);
|
|
33
|
+
const tasks = dedupeByScope(dedupeTasks([...fileTasks, ...flowTasks]));
|
|
21
34
|
return {
|
|
22
35
|
task_count: tasks.length,
|
|
23
36
|
file_task_count: fileTasks.length,
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import type { AuditResult, CoverageMatrix } from "../types.js";
|
|
1
|
+
import type { AuditResult, AuditTask, CoverageMatrix } from "../types.js";
|
|
2
2
|
export declare function ingestAuditResults(coverageMatrix: CoverageMatrix, results: AuditResult[]): CoverageMatrix;
|
|
3
|
+
export declare function updateAuditTaskStatuses(tasks: AuditTask[] | undefined, results: AuditResult[]): AuditTask[] | undefined;
|
|
@@ -12,3 +12,24 @@ export function ingestAuditResults(coverageMatrix, results) {
|
|
|
12
12
|
applyReviewedRanges(matrix, reviewedRanges);
|
|
13
13
|
return matrix;
|
|
14
14
|
}
|
|
15
|
+
export function updateAuditTaskStatuses(tasks, results) {
|
|
16
|
+
if (!tasks) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const completedTaskIds = new Set(results.map((result) => result.task_id));
|
|
20
|
+
const completedAt = new Date().toISOString();
|
|
21
|
+
return tasks.map((task) => {
|
|
22
|
+
if (completedTaskIds.has(task.task_id)) {
|
|
23
|
+
return {
|
|
24
|
+
...task,
|
|
25
|
+
status: "complete",
|
|
26
|
+
completed_at: task.completed_at ?? completedAt,
|
|
27
|
+
completion_reason: task.completion_reason ?? "result_ingested",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
...task,
|
|
32
|
+
status: task.status ?? "pending",
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -43,12 +43,21 @@ 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
63
|
obligations.push(obligation("audit_results_ingested", has(bundle.audit_results) ? "present" : "missing"));
|
|
@@ -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,46 @@
|
|
|
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
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
export function autoCompleteTrivialCoverage(coverage, lineIndex, externalAnalyzerResults) {
|
|
26
|
+
const externalPaths = new Set((externalAnalyzerResults?.results ?? []).map((item) => item.path));
|
|
27
|
+
const skipped = [];
|
|
28
|
+
for (const file of coverage.files) {
|
|
29
|
+
if (file.audit_status === "excluded") {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (!isTrivialAuditPath(file.path, lineIndex[file.path] ?? 0, externalPaths.has(file.path))) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (file.required_lenses.length === 0) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
file.completed_lenses = [...new Set(file.required_lenses)];
|
|
39
|
+
file.audit_status = "complete";
|
|
40
|
+
if (file.classification_status === "unclassified") {
|
|
41
|
+
file.classification_status = "classified";
|
|
42
|
+
}
|
|
43
|
+
skipped.push(file.path);
|
|
44
|
+
}
|
|
45
|
+
return skipped.sort();
|
|
46
|
+
}
|
|
@@ -20,12 +20,15 @@ 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
|
-
" reviewed_ranges: [{path, start, end}] covering what you read",
|
|
23
|
+
" reviewed_ranges: [{path, start, end, line_count}] covering what you read",
|
|
24
24
|
" findings: array (empty if nothing found)",
|
|
25
|
+
" line_count must match the file's current total line count so ingestion can verify the cited ranges.",
|
|
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.",
|
|
29
32
|
`Write the AuditResult[] JSON array to: ${task.audit_results_path}`,
|
|
30
33
|
];
|
|
31
34
|
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,9 @@ export function mergeFindings(results, runtimeReport, externalAnalyzerResults) {
|
|
|
80
82
|
...analyzerEvidence,
|
|
81
83
|
]),
|
|
82
84
|
],
|
|
83
|
-
related_findings:
|
|
84
|
-
...new Set(
|
|
85
|
-
|
|
85
|
+
related_findings: finding.related_findings && finding.related_findings.length > 0
|
|
86
|
+
? [...new Set(finding.related_findings)]
|
|
87
|
+
: undefined,
|
|
86
88
|
});
|
|
87
89
|
continue;
|
|
88
90
|
}
|
|
@@ -108,13 +110,14 @@ export function mergeFindings(results, runtimeReport, externalAnalyzerResults) {
|
|
|
108
110
|
...analyzerEvidence,
|
|
109
111
|
]),
|
|
110
112
|
];
|
|
111
|
-
|
|
112
|
-
...
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
const related = new Set([
|
|
114
|
+
...(existing.related_findings ?? []),
|
|
115
|
+
...(finding.related_findings ?? []),
|
|
116
|
+
existing.id,
|
|
117
|
+
finding.id,
|
|
118
|
+
]);
|
|
119
|
+
existing.related_findings =
|
|
120
|
+
related.size > 0 ? [...related].sort() : undefined;
|
|
118
121
|
}
|
|
119
122
|
}
|
|
120
123
|
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,5 +1,6 @@
|
|
|
1
1
|
import { readOptionalJsonFile } from "../io/json.js";
|
|
2
2
|
import { validateSessionConfig } from "../validation/sessionConfig.js";
|
|
3
|
+
import { writeJsonFile } from "../io/json.js";
|
|
3
4
|
export function getSessionConfigPath(artifactsDir) {
|
|
4
5
|
return `${artifactsDir}/session-config.json`;
|
|
5
6
|
}
|
|
@@ -16,8 +17,9 @@ export async function loadSessionConfig(artifactsDir) {
|
|
|
16
17
|
const configPath = getSessionConfigPath(artifactsDir);
|
|
17
18
|
const rawConfig = await readOptionalJsonFile(configPath);
|
|
18
19
|
if (rawConfig === undefined) {
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
const defaultConfig = { provider: "local-subprocess" };
|
|
21
|
+
await writeJsonFile(configPath, defaultConfig);
|
|
22
|
+
return defaultConfig;
|
|
21
23
|
}
|
|
22
24
|
const issues = validateSessionConfig(rawConfig);
|
|
23
25
|
if (issues.length > 0) {
|
package/dist/types.d.ts
CHANGED
|
@@ -47,6 +47,7 @@ export interface CoverageFileRecord {
|
|
|
47
47
|
export interface CoverageMatrix {
|
|
48
48
|
files: CoverageFileRecord[];
|
|
49
49
|
}
|
|
50
|
+
export type AuditTaskStatus = "pending" | "complete";
|
|
50
51
|
export interface AuditTask {
|
|
51
52
|
task_id: string;
|
|
52
53
|
unit_id: string;
|
|
@@ -62,6 +63,9 @@ export interface AuditTask {
|
|
|
62
63
|
rationale: string;
|
|
63
64
|
priority?: "high" | "medium" | "low";
|
|
64
65
|
tags?: string[];
|
|
66
|
+
status?: AuditTaskStatus;
|
|
67
|
+
completed_at?: string;
|
|
68
|
+
completion_reason?: string;
|
|
65
69
|
}
|
|
66
70
|
export interface Finding {
|
|
67
71
|
id: string;
|
|
@@ -94,6 +98,7 @@ export interface AuditResult {
|
|
|
94
98
|
path: string;
|
|
95
99
|
start: number;
|
|
96
100
|
end: number;
|
|
101
|
+
line_count: number;
|
|
97
102
|
}>;
|
|
98
103
|
findings: Finding[];
|
|
99
104
|
notes?: string[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { AuditTask } from "../types.js";
|
|
2
2
|
export type IssueSeverity = "error" | "warning";
|
|
3
3
|
export interface AuditResultIssue {
|
|
4
4
|
result_index: number;
|
|
@@ -7,5 +7,8 @@ export interface AuditResultIssue {
|
|
|
7
7
|
field: string;
|
|
8
8
|
message: string;
|
|
9
9
|
}
|
|
10
|
-
export
|
|
10
|
+
export interface ValidateAuditResultOptions {
|
|
11
|
+
lineIndex?: Record<string, number>;
|
|
12
|
+
}
|
|
13
|
+
export declare function validateAuditResults(results: unknown, tasks: AuditTask[], options?: ValidateAuditResultOptions): AuditResultIssue[];
|
|
11
14
|
export declare function formatAuditResultIssues(issues: AuditResultIssue[]): string;
|