auditor-lambda 0.3.1 → 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.
@@ -1,40 +1,34 @@
1
- import { dirname, resolve, join } from "node:path";
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 run_id = process.argv[runIdIdx + 1];
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", run_id, "task-results");
24
- const auditResultsPath = join(artifactsDir, "runs", run_id, "audit-results.json");
25
- const failedTasksPath = join(artifactsDir, "runs", run_id, "failed-tasks.json");
26
- const tasksPath = join(artifactsDir, "runs", run_id, "pending-audit-tasks.json");
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 fileLineCounts map
29
- const lineCounts = {};
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
- lineCounts[task.task_id] = task.file_line_counts;
28
+ taskMap[task.task_id] = task;
35
29
  }
36
30
  } catch {
37
- // proceed with empty map
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 fileLineCounts = (taskId && lineCounts[taskId]) ? lineCounts[taskId] : {};
63
- const { valid, errors } = validateResult(resultObj, fileLineCounts);
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 { dirname, resolve, join } from "node:path";
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 __filename = fileURLToPath(import.meta.url);
7
- const __dirname = dirname(__filename);
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 run_id = runIdFlagIdx !== -1
17
- ? process.argv[runIdFlagIdx + 1]
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 (!run_id || !task_id) {
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 = task_id.replace(/[^a-zA-Z0-9_-]/g, "_");
35
- const resultPath = join(artifactsDir, "runs", run_id, "task-results", sanitized + ".json");
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", run_id, "pending-audit-tasks.json");
51
- let fileLineCounts = {};
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
- const task = tasks.find(t => t.task_id === task_id);
56
- fileLineCounts = task?.file_line_counts ?? {};
42
+ task = tasks.find(t => t.task_id === taskId) ?? null;
57
43
  } catch {
58
- // use empty
44
+ // proceed without task context
59
45
  }
60
46
  }
61
47
 
62
- const { valid, errors } = validateResult(resultObj, fileLineCounts);
48
+ const { valid, errors } = validateResult(resultObj, task);
63
49
 
64
50
  if (valid) {
65
- console.log("✓ valid:", task_id);
51
+ console.log("✓ valid:", taskId);
66
52
  process.exit(0);
67
53
  } else {
68
- console.error("✗ invalid:", task_id);
54
+ console.error("✗ invalid:", taskId);
69
55
  console.error(JSON.stringify(errors, null, 2));
70
56
  process.exit(1);
71
57
  }
@@ -1,88 +1,16 @@
1
- import { dirname, resolve, join } from "node:path";
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 {Record<string, number>} fileLineCounts — from the task's file_line_counts
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, fileLineCounts) {
37
- const validate = getValidator();
38
- const schemaValid = validate(resultObj);
39
-
40
- if (!schemaValid) {
41
- return { valid: false, errors: validate.errors.map(formatAjvError) };
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
@@ -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
- // Merge: collect all per-task result files (skip .prompt.md files)
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 r = obj;
1527
- const missing = ["task_id", "unit_id", "pass_id", "lens", "file_coverage", "findings"].filter(f => !(f in r));
1528
- if (missing.length > 0) {
1529
- failing.push({ task_id: String(r.task_id ?? filename), errors: [`Missing required fields: ${missing.join(", ")}`] });
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
- passing.push(obj);
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
- const errors = [];
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
- const tasks = await readJsonFile(tasksPath);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auditor-lambda",
3
- "version": "0.3.1",
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 dispatch/prepare-dispatch.mjs",
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
  },
@@ -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
- }