auditor-lambda 0.3.0 → 0.3.2
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/audit-code-wrapper-lib.mjs +18 -0
- package/dispatch/merge-results.mjs +12 -18
- package/dispatch/validate-result.mjs +15 -29
- package/dispatch/validate.mjs +10 -82
- package/dist/cli.js +27 -106
- package/dist/prompts/renderWorkerPrompt.js +9 -4
- package/package.json +2 -2
- package/skills/audit-code/audit-code.prompt.md +6 -6
- package/dispatch/prepare-dispatch.mjs +0 -136
|
@@ -260,6 +260,9 @@ function printHelp({ usageName, preferredEntrypoint }) {
|
|
|
260
260
|
'- validate checks the current artifact bundle plus session-config/provider readiness and exits non-zero when issues exist',
|
|
261
261
|
'- validate-results --results FILE validates AuditResult payloads against the active task manifest without ingesting them',
|
|
262
262
|
'- explain-task <task_id> prints the resolved file coverage and current status for a task id',
|
|
263
|
+
'- prepare-dispatch --run-id <id> [--artifacts-dir <dir>] creates per-task prompt files and dispatch-plan.json for parallel subagent dispatch',
|
|
264
|
+
'- merge-and-ingest --run-id <id> [--root <dir>] [--artifacts-dir <dir>] merges per-task results and ingests them into the coverage matrix',
|
|
265
|
+
'- validate-result --run-id <id> --task-id <id> [--artifacts-dir <dir>] validates a single task result against the schema and line counts',
|
|
263
266
|
'',
|
|
264
267
|
'Primary usage:',
|
|
265
268
|
'- from the repository root, run the wrapper with no arguments',
|
|
@@ -2195,6 +2198,21 @@ export async function runAuditCodeWrapper({
|
|
|
2195
2198
|
return;
|
|
2196
2199
|
}
|
|
2197
2200
|
|
|
2201
|
+
if (argv[0] === 'prepare-dispatch') {
|
|
2202
|
+
await runDistCommand('prepare-dispatch', argv.slice(1));
|
|
2203
|
+
return;
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
if (argv[0] === 'validate-result') {
|
|
2207
|
+
await runDistCommand('validate-result', argv.slice(1));
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
if (argv[0] === 'merge-and-ingest') {
|
|
2212
|
+
await runDistCommand('merge-and-ingest', argv.slice(1), { ensureArtifactsDir: true });
|
|
2213
|
+
return;
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2198
2216
|
const wrapperArgs = [...argv];
|
|
2199
2217
|
if (defaultSingleStep && !hasFlag(wrapperArgs, '--single-step')) {
|
|
2200
2218
|
wrapperArgs.push('--single-step');
|
|
@@ -1,40 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
1
|
+
import { resolve, join } from "node:path";
|
|
3
2
|
import { readFileSync, writeFileSync, readdirSync, existsSync } from "node:fs";
|
|
4
3
|
import { validateResult } from "./validate.mjs";
|
|
5
4
|
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = dirname(__filename);
|
|
8
|
-
|
|
9
|
-
// Parse --run-id
|
|
10
5
|
const runIdIdx = process.argv.indexOf("--run-id");
|
|
11
6
|
if (runIdIdx === -1 || !process.argv[runIdIdx + 1]) {
|
|
12
7
|
console.error("Usage: node dispatch/merge-results.mjs --run-id <run_id> [--artifacts-dir <dir>]");
|
|
13
8
|
process.exit(1);
|
|
14
9
|
}
|
|
15
|
-
const
|
|
10
|
+
const runId = process.argv[runIdIdx + 1];
|
|
16
11
|
|
|
17
|
-
// Parse --artifacts-dir (default: CWD/.audit-artifacts)
|
|
18
12
|
const artifactsDirIdx = process.argv.indexOf("--artifacts-dir");
|
|
19
13
|
const artifactsDir = artifactsDirIdx !== -1 && process.argv[artifactsDirIdx + 1]
|
|
20
14
|
? resolve(process.argv[artifactsDirIdx + 1])
|
|
21
15
|
: join(process.cwd(), ".audit-artifacts");
|
|
22
16
|
|
|
23
|
-
const taskResultsDir = join(artifactsDir, "runs",
|
|
24
|
-
const auditResultsPath = join(artifactsDir, "runs",
|
|
25
|
-
const failedTasksPath = join(artifactsDir, "runs",
|
|
26
|
-
const tasksPath = join(artifactsDir, "runs",
|
|
17
|
+
const taskResultsDir = join(artifactsDir, "runs", runId, "task-results");
|
|
18
|
+
const auditResultsPath = join(artifactsDir, "runs", runId, "audit-results.json");
|
|
19
|
+
const failedTasksPath = join(artifactsDir, "runs", runId, "failed-tasks.json");
|
|
20
|
+
const tasksPath = join(artifactsDir, "runs", runId, "pending-audit-tasks.json");
|
|
27
21
|
|
|
28
|
-
// Build
|
|
29
|
-
const
|
|
22
|
+
// Build task map for validation context
|
|
23
|
+
const taskMap = {};
|
|
30
24
|
if (existsSync(tasksPath)) {
|
|
31
25
|
try {
|
|
32
26
|
const tasks = JSON.parse(readFileSync(tasksPath, "utf8"));
|
|
33
27
|
for (const task of tasks) {
|
|
34
|
-
|
|
28
|
+
taskMap[task.task_id] = task;
|
|
35
29
|
}
|
|
36
30
|
} catch {
|
|
37
|
-
// proceed
|
|
31
|
+
// proceed without task context
|
|
38
32
|
}
|
|
39
33
|
}
|
|
40
34
|
|
|
@@ -59,8 +53,8 @@ for (const filename of files) {
|
|
|
59
53
|
}
|
|
60
54
|
|
|
61
55
|
const taskId = resultObj?.task_id;
|
|
62
|
-
const
|
|
63
|
-
const { valid, errors } = validateResult(resultObj,
|
|
56
|
+
const task = (taskId && taskMap[taskId]) ? taskMap[taskId] : null;
|
|
57
|
+
const { valid, errors } = validateResult(resultObj, task);
|
|
64
58
|
|
|
65
59
|
if (valid) {
|
|
66
60
|
passing.push(resultObj);
|
|
@@ -1,38 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
1
|
+
import { resolve, join } from "node:path";
|
|
3
2
|
import { readFileSync, existsSync } from "node:fs";
|
|
4
3
|
import { validateResult } from "./validate.mjs";
|
|
5
4
|
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
// Support both named flags and legacy positional args:
|
|
10
|
-
// Named: --run-id <id> --task-id <id> [--artifacts-dir <dir>]
|
|
11
|
-
// Positional (legacy): <run_id> <task_id>
|
|
12
|
-
const runIdFlagIdx = process.argv.indexOf("--run-id");
|
|
13
|
-
const taskIdFlagIdx = process.argv.indexOf("--task-id");
|
|
5
|
+
const runIdIdx = process.argv.indexOf("--run-id");
|
|
6
|
+
const taskIdIdx = process.argv.indexOf("--task-id");
|
|
14
7
|
const artifactsDirIdx = process.argv.indexOf("--artifacts-dir");
|
|
15
8
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
: process.argv[2];
|
|
19
|
-
|
|
20
|
-
const task_id = taskIdFlagIdx !== -1
|
|
21
|
-
? process.argv[taskIdFlagIdx + 1]
|
|
22
|
-
: process.argv[3];
|
|
9
|
+
const runId = runIdIdx !== -1 ? process.argv[runIdIdx + 1] : undefined;
|
|
10
|
+
const taskId = taskIdIdx !== -1 ? process.argv[taskIdIdx + 1] : undefined;
|
|
23
11
|
|
|
24
|
-
if (!
|
|
12
|
+
if (!runId || !taskId) {
|
|
25
13
|
console.error("Usage: node dispatch/validate-result.mjs --run-id <run_id> --task-id <task_id> [--artifacts-dir <dir>]");
|
|
26
14
|
process.exit(1);
|
|
27
15
|
}
|
|
28
16
|
|
|
29
|
-
// Artifacts dir: explicit flag > CWD default
|
|
30
17
|
const artifactsDir = artifactsDirIdx !== -1 && process.argv[artifactsDirIdx + 1]
|
|
31
18
|
? resolve(process.argv[artifactsDirIdx + 1])
|
|
32
19
|
: join(process.cwd(), ".audit-artifacts");
|
|
33
20
|
|
|
34
|
-
const sanitized =
|
|
35
|
-
const resultPath = join(artifactsDir, "runs",
|
|
21
|
+
const sanitized = taskId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
22
|
+
const resultPath = join(artifactsDir, "runs", runId, "task-results", sanitized + ".json");
|
|
36
23
|
|
|
37
24
|
if (!existsSync(resultPath)) {
|
|
38
25
|
console.error(`File not found: ${resultPath}`);
|
|
@@ -47,25 +34,24 @@ try {
|
|
|
47
34
|
process.exit(1);
|
|
48
35
|
}
|
|
49
36
|
|
|
50
|
-
const tasksPath = join(artifactsDir, "runs",
|
|
51
|
-
let
|
|
37
|
+
const tasksPath = join(artifactsDir, "runs", runId, "pending-audit-tasks.json");
|
|
38
|
+
let task = null;
|
|
52
39
|
if (existsSync(tasksPath)) {
|
|
53
40
|
try {
|
|
54
41
|
const tasks = JSON.parse(readFileSync(tasksPath, "utf8"));
|
|
55
|
-
|
|
56
|
-
fileLineCounts = task?.file_line_counts ?? {};
|
|
42
|
+
task = tasks.find(t => t.task_id === taskId) ?? null;
|
|
57
43
|
} catch {
|
|
58
|
-
//
|
|
44
|
+
// proceed without task context
|
|
59
45
|
}
|
|
60
46
|
}
|
|
61
47
|
|
|
62
|
-
const { valid, errors } = validateResult(resultObj,
|
|
48
|
+
const { valid, errors } = validateResult(resultObj, task);
|
|
63
49
|
|
|
64
50
|
if (valid) {
|
|
65
|
-
console.log("✓ valid:",
|
|
51
|
+
console.log("✓ valid:", taskId);
|
|
66
52
|
process.exit(0);
|
|
67
53
|
} else {
|
|
68
|
-
console.error("✗ invalid:",
|
|
54
|
+
console.error("✗ invalid:", taskId);
|
|
69
55
|
console.error(JSON.stringify(errors, null, 2));
|
|
70
56
|
process.exit(1);
|
|
71
57
|
}
|
package/dispatch/validate.mjs
CHANGED
|
@@ -1,88 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
4
|
-
import Ajv2020 from "ajv/dist/2020.js";
|
|
5
|
-
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = dirname(__filename);
|
|
8
|
-
const PROJECT_ROOT = resolve(__dirname, "..");
|
|
9
|
-
const SCHEMAS_DIR = join(PROJECT_ROOT, "schemas");
|
|
10
|
-
|
|
11
|
-
function loadSchema(name) {
|
|
12
|
-
return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), "utf8"));
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
let _ajv = null;
|
|
16
|
-
let _validateFn = null;
|
|
17
|
-
|
|
18
|
-
function getValidator() {
|
|
19
|
-
if (_validateFn) return _validateFn;
|
|
20
|
-
_ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
21
|
-
_ajv.addSchema(loadSchema("finding.schema.json"));
|
|
22
|
-
_validateFn = _ajv.compile(loadSchema("audit_result.schema.json"));
|
|
23
|
-
return _validateFn;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function formatAjvError(e) {
|
|
27
|
-
const path = e.instancePath || "(root)";
|
|
28
|
-
return `${path}: ${e.message}${e.params ? " (" + JSON.stringify(e.params) + ")" : ""}`;
|
|
29
|
-
}
|
|
1
|
+
import { validateAuditResults } from "../dist/validation/auditResults.js";
|
|
30
2
|
|
|
31
3
|
/**
|
|
32
4
|
* @param {object} resultObj — parsed JSON from a task-results file
|
|
33
|
-
* @param {
|
|
5
|
+
* @param {object|null} task — the matching AuditTask from pending-audit-tasks.json, or null
|
|
34
6
|
* @returns {{ valid: boolean, errors: string[] }}
|
|
35
7
|
*/
|
|
36
|
-
export function validateResult(resultObj,
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const errors = [];
|
|
45
|
-
|
|
46
|
-
// Line range constraint
|
|
47
|
-
for (const finding of resultObj.findings) {
|
|
48
|
-
for (const entry of finding.affected_files) {
|
|
49
|
-
if (entry.line_end !== undefined) {
|
|
50
|
-
const coverage = resultObj.file_coverage.find(fc => fc.path === entry.path);
|
|
51
|
-
if (!coverage) {
|
|
52
|
-
errors.push(`affected_files path '${entry.path}' not in file_coverage`);
|
|
53
|
-
} else if (entry.line_end > coverage.total_lines) {
|
|
54
|
-
errors.push(
|
|
55
|
-
`finding '${finding.id}': line_end ${entry.line_end} exceeds total_lines ${coverage.total_lines} for ${entry.path}`
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Lens consistency
|
|
63
|
-
for (const finding of resultObj.findings) {
|
|
64
|
-
if (finding.lens !== resultObj.lens) {
|
|
65
|
-
errors.push(
|
|
66
|
-
`finding '${finding.id}': lens '${finding.lens}' does not match task lens '${resultObj.lens}'`
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// affected_files paths in scope
|
|
72
|
-
const allowedPaths = new Set(resultObj.file_coverage.map(fc => fc.path));
|
|
73
|
-
for (const finding of resultObj.findings) {
|
|
74
|
-
for (const entry of finding.affected_files) {
|
|
75
|
-
if (!allowedPaths.has(entry.path)) {
|
|
76
|
-
errors.push(
|
|
77
|
-
`finding '${finding.id}': affected path '${entry.path}' not in task file_coverage`
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (errors.length > 0) {
|
|
84
|
-
return { valid: false, errors };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return { valid: true, errors: [] };
|
|
8
|
+
export function validateResult(resultObj, task) {
|
|
9
|
+
const tasks = task ? [task] : [];
|
|
10
|
+
const lineIndex = task?.file_line_counts ?? {};
|
|
11
|
+
const issues = validateAuditResults([resultObj], tasks, { lineIndex });
|
|
12
|
+
const errors = issues
|
|
13
|
+
.filter(i => i.severity === "error")
|
|
14
|
+
.map(i => `${i.path}: ${i.message}`);
|
|
15
|
+
return { valid: errors.length === 0, errors };
|
|
88
16
|
}
|
package/dist/cli.js
CHANGED
|
@@ -112,7 +112,7 @@ function getExplicitProvider(argv) {
|
|
|
112
112
|
return getFlag(argv, "--provider");
|
|
113
113
|
}
|
|
114
114
|
function resolveRunProviderName(argv, sessionConfig) {
|
|
115
|
-
return resolveFreshSessionProviderName(getExplicitProvider(argv)
|
|
115
|
+
return resolveFreshSessionProviderName(getExplicitProvider(argv), sessionConfig);
|
|
116
116
|
}
|
|
117
117
|
function chunkArray(arr, size) {
|
|
118
118
|
const chunkSize = normalizePositiveInteger(size);
|
|
@@ -648,7 +648,7 @@ async function cmdRunToCompletion(argv) {
|
|
|
648
648
|
throw error;
|
|
649
649
|
}
|
|
650
650
|
const explicitProvider = getExplicitProvider(argv);
|
|
651
|
-
const provider = createFreshSessionProvider(explicitProvider
|
|
651
|
+
const provider = createFreshSessionProvider(explicitProvider, sessionConfig);
|
|
652
652
|
const uiMode = getUiMode(argv, sessionConfig.ui_mode ?? "headless");
|
|
653
653
|
const maxRuns = getMaxRuns(argv);
|
|
654
654
|
const agentBatchSize = getAgentBatchSize(argv, sessionConfig);
|
|
@@ -1479,7 +1479,7 @@ async function cmdPrepareDispatch(argv) {
|
|
|
1479
1479
|
"",
|
|
1480
1480
|
"## Validate",
|
|
1481
1481
|
"After writing your result, run:",
|
|
1482
|
-
` audit-code validate-result --run-id ${runId} --task-id ${task.task_id} --artifacts-dir ${artifactsDir}`,
|
|
1482
|
+
` "${process.execPath}" "${join(packageRoot, "audit-code.mjs")}" validate-result --run-id ${runId} --task-id ${task.task_id} --artifacts-dir "${artifactsDir}"`,
|
|
1483
1483
|
"",
|
|
1484
1484
|
"Exit 0 means valid. Non-zero: read the errors, fix your JSON, rewrite the file, run again. Retry up to 3 times.",
|
|
1485
1485
|
].join("\n");
|
|
@@ -1501,12 +1501,16 @@ async function cmdMergeAndIngest(argv) {
|
|
|
1501
1501
|
const taskResultsDir = join(runDir, "task-results");
|
|
1502
1502
|
const auditResultsPath = join(runDir, "audit-results.json");
|
|
1503
1503
|
const taskPath = join(runDir, "task.json");
|
|
1504
|
-
|
|
1504
|
+
const tasksPath = join(runDir, "pending-audit-tasks.json");
|
|
1505
|
+
let allTasks = [];
|
|
1506
|
+
try {
|
|
1507
|
+
allTasks = await readJsonFile(tasksPath);
|
|
1508
|
+
}
|
|
1509
|
+
catch { /* may not exist */ }
|
|
1510
|
+
const taskMap = new Map(allTasks.map(t => [t.task_id, t]));
|
|
1505
1511
|
let files;
|
|
1506
1512
|
try {
|
|
1507
|
-
files = (await readdir(taskResultsDir))
|
|
1508
|
-
.filter(f => f.endsWith(".json"))
|
|
1509
|
-
.sort();
|
|
1513
|
+
files = (await readdir(taskResultsDir)).filter(f => f.endsWith(".json")).sort();
|
|
1510
1514
|
}
|
|
1511
1515
|
catch {
|
|
1512
1516
|
files = [];
|
|
@@ -1523,13 +1527,16 @@ async function cmdMergeAndIngest(argv) {
|
|
|
1523
1527
|
failing.push({ task_id: filename, errors: [`Invalid JSON: ${e.message}`] });
|
|
1524
1528
|
continue;
|
|
1525
1529
|
}
|
|
1526
|
-
const
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
+
const taskId = typeof obj.task_id === "string"
|
|
1531
|
+
? String(obj.task_id) : undefined;
|
|
1532
|
+
const matchingTask = taskId ? taskMap.get(taskId) : undefined;
|
|
1533
|
+
const issues = validateAuditResults([obj], matchingTask ? [matchingTask] : [], { lineIndex: matchingTask?.file_line_counts ?? {} });
|
|
1534
|
+
const errors = issues.filter(i => i.severity === "error");
|
|
1535
|
+
if (errors.length === 0) {
|
|
1536
|
+
passing.push(obj);
|
|
1530
1537
|
}
|
|
1531
1538
|
else {
|
|
1532
|
-
|
|
1539
|
+
failing.push({ task_id: taskId ?? filename, errors: errors.map(i => i.message) });
|
|
1533
1540
|
}
|
|
1534
1541
|
}
|
|
1535
1542
|
await writeJsonFile(auditResultsPath, passing);
|
|
@@ -1541,12 +1548,6 @@ async function cmdMergeAndIngest(argv) {
|
|
|
1541
1548
|
// Ingest: run worker-run logic against the merged results file
|
|
1542
1549
|
await cmdWorkerRun([argv[0], argv[1], "worker-run", "--task", taskPath, "--artifacts-dir", artifactsDir]);
|
|
1543
1550
|
}
|
|
1544
|
-
const VALID_LENSES_SET = new Set([
|
|
1545
|
-
"correctness", "architecture", "maintainability", "security", "reliability",
|
|
1546
|
-
"performance", "data_integrity", "tests", "operability", "config_deployment",
|
|
1547
|
-
]);
|
|
1548
|
-
const VALID_SEVERITIES_SET = new Set(["critical", "high", "medium", "low", "info"]);
|
|
1549
|
-
const VALID_CONFIDENCES_SET = new Set(["high", "medium", "low"]);
|
|
1550
1551
|
async function cmdValidateResult(argv) {
|
|
1551
1552
|
const runId = getFlag(argv, "--run-id");
|
|
1552
1553
|
const taskId = getFlag(argv, "--task-id");
|
|
@@ -1574,102 +1575,22 @@ async function cmdValidateResult(argv) {
|
|
|
1574
1575
|
process.exitCode = 1;
|
|
1575
1576
|
return;
|
|
1576
1577
|
}
|
|
1577
|
-
|
|
1578
|
-
// Required top-level fields
|
|
1579
|
-
for (const field of ["task_id", "unit_id", "pass_id", "lens", "file_coverage", "findings"]) {
|
|
1580
|
-
if (!(field in obj))
|
|
1581
|
-
errors.push(`Missing required field: ${field}`);
|
|
1582
|
-
}
|
|
1583
|
-
if (errors.length > 0) {
|
|
1584
|
-
console.error(`✗ invalid: ${taskId}`);
|
|
1585
|
-
for (const e of errors)
|
|
1586
|
-
console.error(` ${e}`);
|
|
1587
|
-
process.exitCode = 1;
|
|
1588
|
-
return;
|
|
1589
|
-
}
|
|
1590
|
-
// Lens
|
|
1591
|
-
if (typeof obj.lens !== "string" || !VALID_LENSES_SET.has(obj.lens)) {
|
|
1592
|
-
errors.push(`lens must be one of: ${[...VALID_LENSES_SET].join("|")}`);
|
|
1593
|
-
}
|
|
1594
|
-
// file_coverage
|
|
1595
|
-
if (!Array.isArray(obj.file_coverage) || obj.file_coverage.length === 0) {
|
|
1596
|
-
errors.push("file_coverage must be a non-empty array");
|
|
1597
|
-
}
|
|
1598
|
-
else {
|
|
1599
|
-
for (const fc of obj.file_coverage) {
|
|
1600
|
-
const entry = fc;
|
|
1601
|
-
if (typeof entry.path !== "string")
|
|
1602
|
-
errors.push(`file_coverage entry missing string 'path'`);
|
|
1603
|
-
if (typeof entry.total_lines !== "number")
|
|
1604
|
-
errors.push(`file_coverage entry missing numeric 'total_lines'`);
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
// findings
|
|
1608
|
-
if (!Array.isArray(obj.findings)) {
|
|
1609
|
-
errors.push("findings must be an array");
|
|
1610
|
-
}
|
|
1611
|
-
else {
|
|
1612
|
-
for (const f of obj.findings) {
|
|
1613
|
-
const finding = f;
|
|
1614
|
-
for (const field of ["id", "title", "category", "severity", "confidence", "lens", "summary"]) {
|
|
1615
|
-
if (typeof finding[field] !== "string")
|
|
1616
|
-
errors.push(`finding missing string '${field}'`);
|
|
1617
|
-
}
|
|
1618
|
-
if (typeof finding.severity === "string" && !VALID_SEVERITIES_SET.has(finding.severity)) {
|
|
1619
|
-
errors.push(`finding '${finding.id}': invalid severity '${finding.severity}'`);
|
|
1620
|
-
}
|
|
1621
|
-
if (typeof finding.confidence === "string" && !VALID_CONFIDENCES_SET.has(finding.confidence)) {
|
|
1622
|
-
errors.push(`finding '${finding.id}': invalid confidence '${finding.confidence}'`);
|
|
1623
|
-
}
|
|
1624
|
-
if (!Array.isArray(finding.affected_files) || finding.affected_files.length === 0) {
|
|
1625
|
-
errors.push(`finding '${finding.id}': affected_files must be a non-empty array`);
|
|
1626
|
-
}
|
|
1627
|
-
else {
|
|
1628
|
-
for (const af of finding.affected_files) {
|
|
1629
|
-
if (typeof af.path !== "string") {
|
|
1630
|
-
errors.push(`finding '${finding.id}': affected_files entries must be objects with a 'path' key`);
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
if (!Array.isArray(finding.evidence) || finding.evidence.length === 0) {
|
|
1635
|
-
errors.push(`finding '${finding.id}': evidence must be a non-empty array`);
|
|
1636
|
-
}
|
|
1637
|
-
if (typeof finding.lens === "string" && finding.lens !== obj.lens) {
|
|
1638
|
-
errors.push(`finding '${finding.id}': lens '${finding.lens}' does not match task lens '${obj.lens}'`);
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
// Line range bounds (load from pending-audit-tasks.json if available)
|
|
1643
|
-
let fileLineCounts = {};
|
|
1578
|
+
let allTasks = [];
|
|
1644
1579
|
try {
|
|
1645
|
-
|
|
1646
|
-
const task = tasks.find(t => t.task_id === taskId);
|
|
1647
|
-
fileLineCounts = task?.file_line_counts ?? {};
|
|
1648
|
-
}
|
|
1649
|
-
catch { /* ignore */ }
|
|
1650
|
-
if (Array.isArray(obj.file_coverage) && Array.isArray(obj.findings)) {
|
|
1651
|
-
const coverageMap = new Map(obj.file_coverage.map(fc => [fc.path, fc.total_lines]));
|
|
1652
|
-
const allowedPaths = new Set(coverageMap.keys());
|
|
1653
|
-
for (const f of obj.findings) {
|
|
1654
|
-
for (const af of (f.affected_files ?? [])) {
|
|
1655
|
-
const p = af.path;
|
|
1656
|
-
if (!allowedPaths.has(p))
|
|
1657
|
-
errors.push(`finding '${f.id}': path '${p}' not in file_coverage`);
|
|
1658
|
-
if (typeof af.line_end === "number") {
|
|
1659
|
-
const max = coverageMap.get(p) ?? fileLineCounts[p] ?? Infinity;
|
|
1660
|
-
if (af.line_end > max)
|
|
1661
|
-
errors.push(`finding '${f.id}': line_end ${af.line_end} exceeds total_lines ${max} for ${p}`);
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1580
|
+
allTasks = await readJsonFile(tasksPath);
|
|
1665
1581
|
}
|
|
1582
|
+
catch { /* may not exist */ }
|
|
1583
|
+
const matchingTasks = allTasks.filter(t => t.task_id === taskId);
|
|
1584
|
+
const lineIndex = matchingTasks[0]?.file_line_counts ?? {};
|
|
1585
|
+
const issues = validateAuditResults([obj], matchingTasks, { lineIndex });
|
|
1586
|
+
const errors = issues.filter(i => i.severity === "error");
|
|
1666
1587
|
if (errors.length === 0) {
|
|
1667
1588
|
console.log(`✓ valid: ${taskId}`);
|
|
1668
1589
|
}
|
|
1669
1590
|
else {
|
|
1670
1591
|
console.error(`✗ invalid: ${taskId}`);
|
|
1671
1592
|
for (const e of errors)
|
|
1672
|
-
console.error(` ${e}`);
|
|
1593
|
+
console.error(` ${e.path}: ${e.message}`);
|
|
1673
1594
|
process.exitCode = 1;
|
|
1674
1595
|
}
|
|
1675
1596
|
}
|
|
@@ -11,10 +11,15 @@ export function renderWorkerPrompt(task) {
|
|
|
11
11
|
`Audit run: ${task.run_id}`,
|
|
12
12
|
`Read: ${tasksPath}`,
|
|
13
13
|
"For each task: read all file_paths in full, review under the specified lens,",
|
|
14
|
-
"and emit one AuditResult with:
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
|
|
14
|
+
"and emit one AuditResult with:",
|
|
15
|
+
" task_id, unit_id, pass_id, lens (copy from task),",
|
|
16
|
+
" file_coverage: [{path, total_lines}] — use file_line_counts[path] from the task for each file,",
|
|
17
|
+
" findings: [] or array of finding objects.",
|
|
18
|
+
"Each finding: id, title, category, severity, confidence, lens, summary,",
|
|
19
|
+
" affected_files [{path, line_start, line_end, symbol}] (objects, not strings; min 1 entry),",
|
|
20
|
+
" evidence [strings] (min 1 entry).",
|
|
21
|
+
"Constraint: line_end must not exceed total_lines for that file.",
|
|
22
|
+
`Write all results as a JSON array to: ${task.audit_results_path}`,
|
|
18
23
|
];
|
|
19
24
|
if (usesDeferredWorkerCommand(task)) {
|
|
20
25
|
lines.push("Deferred mode: write results, do not execute worker_command.");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auditor-lambda",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Portable hybrid code-auditing framework for arbitrary repositories.",
|
|
6
6
|
"type": "module",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"start": "node dist/index.js",
|
|
39
39
|
"audit-code": "node audit-code.mjs",
|
|
40
40
|
"sample-run": "node dist/index.js sample-run",
|
|
41
|
-
"dispatch:prepare": "node
|
|
41
|
+
"dispatch:prepare": "node audit-code.mjs prepare-dispatch",
|
|
42
42
|
"dispatch:merge": "node dispatch/merge-results.mjs",
|
|
43
43
|
"dispatch:validate": "node dispatch/validate-result.mjs"
|
|
44
44
|
},
|
|
@@ -36,13 +36,11 @@ Parse the JSON output. Check `audit_state.status`:
|
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
39
|
-
### Step 2 —
|
|
39
|
+
### Step 2 — Extract the Task IDs
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- `run_id` — identifies this batch of audit work
|
|
45
|
-
- `artifacts_dir` — base artifacts directory
|
|
41
|
+
Parse these fields directly from the Step 1 JSON output:
|
|
42
|
+
- `run_id` — from `handoff.active_review_run.run_id`
|
|
43
|
+
- `artifacts_dir` — from `handoff.artifacts_dir`
|
|
46
44
|
|
|
47
45
|
_(If `audit_state.blockers` contains a message that requires operator input rather than code review, stop and report the blocker verbatim to the user.)_
|
|
48
46
|
|
|
@@ -103,3 +101,5 @@ Read `audit-report.md` and present the completed audit to the user. Lead with th
|
|
|
103
101
|
**Large task warnings:** `prepare-dispatch` warns about tasks exceeding ~1500 lines. If a subagent hits a quota limit and fails to produce output, `merge-and-ingest` excludes it silently — those tasks remain pending and are picked up in the next loop iteration. No manual intervention needed.
|
|
104
102
|
|
|
105
103
|
**Failed validation:** Subagents self-validate and retry up to 3 times before finishing. `merge-and-ingest` excludes any results that still lack required fields and writes `failed-tasks.json`. Those tasks are requeued automatically in the next cycle.
|
|
104
|
+
|
|
105
|
+
**Command failures:** If `prepare-dispatch` or `merge-and-ingest` exits non-zero, **STOP immediately** and report the exact error output to the user. Do NOT improvise manual dispatch, manually split tasks, manually create directories, manually construct prompts, or manually merge results. These scripts are the canonical mechanism — operating without them produces incorrect output. Fix the underlying issue and re-run the failed command.
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { dirname, resolve, join } from "node:path";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
4
|
-
|
|
5
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
-
const __dirname = dirname(__filename);
|
|
7
|
-
const PACKAGE_ROOT = resolve(__dirname, "..");
|
|
8
|
-
|
|
9
|
-
// Parse --run-id
|
|
10
|
-
const runIdIdx = process.argv.indexOf("--run-id");
|
|
11
|
-
if (runIdIdx === -1 || !process.argv[runIdIdx + 1]) {
|
|
12
|
-
console.error("Usage: node dispatch/prepare-dispatch.mjs --run-id <run_id> [--artifacts-dir <dir>]");
|
|
13
|
-
process.exit(1);
|
|
14
|
-
}
|
|
15
|
-
const run_id = process.argv[runIdIdx + 1];
|
|
16
|
-
|
|
17
|
-
// Parse --artifacts-dir (default: CWD/.audit-artifacts)
|
|
18
|
-
const artifactsDirIdx = process.argv.indexOf("--artifacts-dir");
|
|
19
|
-
const artifactsDir = artifactsDirIdx !== -1 && process.argv[artifactsDirIdx + 1]
|
|
20
|
-
? resolve(process.argv[artifactsDirIdx + 1])
|
|
21
|
-
: join(process.cwd(), ".audit-artifacts");
|
|
22
|
-
|
|
23
|
-
const runDir = join(artifactsDir, "runs", run_id);
|
|
24
|
-
const tasksPath = join(runDir, "pending-audit-tasks.json");
|
|
25
|
-
const taskResultsDir = join(runDir, "task-results");
|
|
26
|
-
const dispatchPlanPath = join(runDir, "dispatch-plan.json");
|
|
27
|
-
|
|
28
|
-
let tasks;
|
|
29
|
-
try {
|
|
30
|
-
tasks = JSON.parse(readFileSync(tasksPath, "utf8"));
|
|
31
|
-
} catch (e) {
|
|
32
|
-
console.error(`Cannot read ${tasksPath}: ${e.message}`);
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const lensDefinitions = JSON.parse(
|
|
37
|
-
readFileSync(join(__dirname, "lens-definitions.json"), "utf8")
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
mkdirSync(taskResultsDir, { recursive: true });
|
|
41
|
-
|
|
42
|
-
function buildPrompt(task, lensDef, outputPath, runId, artifactsDir) {
|
|
43
|
-
const fileList = task.file_paths.map(p => {
|
|
44
|
-
const lines = task.file_line_counts?.[p] ?? 0;
|
|
45
|
-
return `- ${p} (${lines} lines)`;
|
|
46
|
-
}).join("\n");
|
|
47
|
-
|
|
48
|
-
return `You are a code auditor. Review the files below under the specified lens.
|
|
49
|
-
|
|
50
|
-
## Task
|
|
51
|
-
task_id: ${task.task_id}
|
|
52
|
-
unit_id: ${task.unit_id}
|
|
53
|
-
pass_id: ${task.pass_id}
|
|
54
|
-
lens: ${task.lens}
|
|
55
|
-
|
|
56
|
-
## Files to read
|
|
57
|
-
Use your Read tool. Paths are repo-relative from the current working directory.
|
|
58
|
-
${fileList}
|
|
59
|
-
|
|
60
|
-
## Lens: ${task.lens}
|
|
61
|
-
${lensDef?.description ?? task.lens}
|
|
62
|
-
|
|
63
|
-
Do NOT report: ${lensDef?.do_not_report ?? "N/A"}
|
|
64
|
-
|
|
65
|
-
## Output
|
|
66
|
-
Write a single JSON object to: ${outputPath}
|
|
67
|
-
|
|
68
|
-
Required fields:
|
|
69
|
-
task_id copy from task metadata above
|
|
70
|
-
unit_id copy from task metadata above
|
|
71
|
-
pass_id copy from task metadata above
|
|
72
|
-
lens copy from task metadata above
|
|
73
|
-
file_coverage [{path, total_lines}] — one entry per file; use the line counts listed above
|
|
74
|
-
findings [] or array of finding objects (see below)
|
|
75
|
-
|
|
76
|
-
Each finding object (omit optional fields if not applicable):
|
|
77
|
-
id unique ID, e.g. "SEC-001"
|
|
78
|
-
title short title
|
|
79
|
-
category correctness|architecture|maintainability|security|reliability|performance|data_integrity|tests|operability|config_deployment
|
|
80
|
-
severity critical|high|medium|low|info
|
|
81
|
-
confidence high|medium|low
|
|
82
|
-
lens "${task.lens}" — must match task lens exactly
|
|
83
|
-
summary 1–2 sentence description
|
|
84
|
-
affected_files [{path, line_start?, line_end?, symbol?}] — objects, not strings; min 1 entry
|
|
85
|
-
evidence ["path/to/file.ts:42 — description of what you see there"] — min 1 entry
|
|
86
|
-
|
|
87
|
-
Constraints:
|
|
88
|
-
1. line_end must not exceed the file's actual line count (use the counts listed above)
|
|
89
|
-
2. affected_files entries are OBJECTS with a "path" key — NOT plain strings
|
|
90
|
-
3. Only reference files from the list above
|
|
91
|
-
4. findings: [] is correct when you find nothing genuine — do not invent findings
|
|
92
|
-
|
|
93
|
-
## Validate
|
|
94
|
-
After writing your result, run:
|
|
95
|
-
audit-code validate-result --run-id ${runId} --task-id ${task.task_id} --artifacts-dir ${artifactsDir}
|
|
96
|
-
|
|
97
|
-
Exit 0 means valid. Non-zero: read the errors, fix your JSON, rewrite the file, run again. Retry up to 3 times.`;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const plan = [];
|
|
101
|
-
let largestTask = null;
|
|
102
|
-
let largestLines = 0;
|
|
103
|
-
|
|
104
|
-
for (const task of tasks) {
|
|
105
|
-
const sanitizedId = task.task_id.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
106
|
-
const outputPath = join(taskResultsDir, sanitizedId + ".json");
|
|
107
|
-
const promptPath = join(taskResultsDir, sanitizedId + ".prompt.md");
|
|
108
|
-
const lensDef = lensDefinitions[task.lens];
|
|
109
|
-
|
|
110
|
-
if (!lensDef) {
|
|
111
|
-
process.stderr.write(`Warning: no lens definition for '${task.lens}' (task ${task.task_id})\n`);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const totalFileLines = Object.values(task.file_line_counts ?? {}).reduce((a, b) => a + b, 0);
|
|
115
|
-
|
|
116
|
-
if (totalFileLines > largestLines) {
|
|
117
|
-
largestLines = totalFileLines;
|
|
118
|
-
largestTask = task.task_id;
|
|
119
|
-
}
|
|
120
|
-
if (totalFileLines > 1500) {
|
|
121
|
-
process.stderr.write(`Warning: large task ${task.task_id} (~${totalFileLines} lines) may hit quota limits\n`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const prompt = buildPrompt(task, lensDef, outputPath, run_id, artifactsDir);
|
|
125
|
-
writeFileSync(promptPath, prompt, "utf8");
|
|
126
|
-
|
|
127
|
-
const description = `Audit ${task.unit_id} (${task.file_paths.length} file(s), ~${totalFileLines} lines) — ${task.lens} lens`;
|
|
128
|
-
plan.push({ task_id: task.task_id, description, output_path: outputPath, prompt_path: promptPath });
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
writeFileSync(dispatchPlanPath, JSON.stringify(plan, null, 2));
|
|
132
|
-
|
|
133
|
-
console.log(`Wrote dispatch-plan.json — ${plan.length} tasks ready for dispatch`);
|
|
134
|
-
if (largestTask) {
|
|
135
|
-
console.log(`Largest task: ${largestTask} (~${largestLines} lines)`);
|
|
136
|
-
}
|