auditor-lambda 0.3.4 → 0.3.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/dist/cli.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { access, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
2
2
  import { createReadStream } from "node:fs";
3
- import { basename, dirname, join, resolve } from "node:path";
3
+ import { Buffer } from "node:buffer";
4
+ import { createHash } from "node:crypto";
5
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
6
  import { fileURLToPath } from "node:url";
5
7
  import { buildRepoManifest } from "./extractors/fileInventory.js";
6
8
  import { buildFileDisposition } from "./extractors/disposition.js";
@@ -11,7 +13,7 @@ import { buildFlowCoverage } from "./orchestrator/flowCoverage.js";
11
13
  import { buildRuntimeValidationTasks, } from "./orchestrator/runtimeValidation.js";
12
14
  import { initializeCoverageFromPlan } from "./orchestrator/planning.js";
13
15
  import { loadArtifactBundle, writeCoreArtifacts, promoteFinalAuditReport, } from "./io/artifacts.js";
14
- import { readJsonFile, writeJsonFile } from "./io/json.js";
16
+ import { isFileMissingError, readJsonFile, writeJsonFile } from "./io/json.js";
15
17
  import { validateArtifactBundle } from "./validation/artifacts.js";
16
18
  import { validateAuditResults, formatAuditResultIssues, } from "./validation/auditResults.js";
17
19
  import { prefixValidationIssues } from "./validation/basic.js";
@@ -27,11 +29,16 @@ import { getSessionConfigPath, loadSessionConfig, readSessionConfigFile, } from
27
29
  import { clearDispatchFiles, buildRunId, ensureSupervisorDirs, getRunPaths, writeDispatchBatchFiles, writeWorkerTaskFiles, } from "./io/runArtifacts.js";
28
30
  import { renderWorkerPrompt } from "./prompts/renderWorkerPrompt.js";
29
31
  import { buildReviewPackets, orderTasksForPacketReview, } from "./orchestrator/reviewPackets.js";
32
+ import { buildFileAnchorSummary, } from "./orchestrator/fileAnchors.js";
30
33
  import { LOCAL_SUBPROCESS_PROVIDER_NAME } from "./providers/constants.js";
31
34
  import { runAuditCodeMcpServer } from "./mcp/server.js";
32
35
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
33
36
  const ADVANCE_AUDIT_CONTRACT_VERSION = "audit-code/v1alpha1";
34
37
  const WORKER_RESULT_CONTRACT_VERSION = "audit-code-worker-result/v1alpha1";
38
+ const LARGE_FILE_PACKET_TARGET_LINES = 2500;
39
+ const SMALL_MODEL_HINT_MAX_LINES = 500;
40
+ const SMALL_MODEL_HINT_MAX_ESTIMATED_TOKENS = 3000;
41
+ const DEEP_MODEL_HINT_MIN_ESTIMATED_TOKENS = 9000;
35
42
  const DIRECT_CLI_DEFAULTS = {
36
43
  rootDir: ".",
37
44
  artifactsDir: ".artifacts",
@@ -65,6 +72,45 @@ function getFlag(argv, name, fallback) {
65
72
  function hasFlag(argv, name) {
66
73
  return argv.includes(name);
67
74
  }
75
+ function toBase64Url(value) {
76
+ return Buffer.from(value, "utf8").toString("base64url");
77
+ }
78
+ function fromBase64Url(value) {
79
+ return Buffer.from(value, "base64url").toString("utf8");
80
+ }
81
+ function digestId(value) {
82
+ return createHash("sha256").update(value).digest("hex").slice(0, 12);
83
+ }
84
+ function safeArtifactStem(value) {
85
+ const sanitized = value
86
+ .replace(/[^a-zA-Z0-9_-]+/g, "_")
87
+ .replace(/^_+|_+$/g, "")
88
+ .slice(0, 80);
89
+ return sanitized.length > 0 ? sanitized : "artifact";
90
+ }
91
+ function artifactNameForId(value, extension) {
92
+ return `${safeArtifactStem(value)}_${digestId(value)}.${extension}`;
93
+ }
94
+ function taskResultPath(taskResultsDir, taskId) {
95
+ return join(taskResultsDir, artifactNameForId(taskId, "json"));
96
+ }
97
+ function packetPromptPath(taskResultsDir, packetId) {
98
+ return join(taskResultsDir, artifactNameForId(packetId, "prompt.md"));
99
+ }
100
+ async function readStdinText() {
101
+ if (process.stdin.isTTY) {
102
+ return "";
103
+ }
104
+ return await new Promise((resolveInput, reject) => {
105
+ let input = "";
106
+ process.stdin.setEncoding("utf8");
107
+ process.stdin.on("data", (chunk) => {
108
+ input += chunk;
109
+ });
110
+ process.stdin.on("end", () => resolveInput(input));
111
+ process.stdin.on("error", reject);
112
+ });
113
+ }
68
114
  function resolveFlagPath(argv, name, fallback) {
69
115
  return resolve(getFlag(argv, name, fallback));
70
116
  }
@@ -1404,6 +1450,112 @@ async function cmdWorkerRun(argv) {
1404
1450
  process.exitCode = 1;
1405
1451
  }
1406
1452
  }
1453
+ const DISPATCH_RESULT_MAP_FILENAME = "dispatch-result-map.json";
1454
+ function dispatchResultMapPath(runDir) {
1455
+ return join(runDir, DISPATCH_RESULT_MAP_FILENAME);
1456
+ }
1457
+ function resolveRunScopedArg(argv, rawFlag, b64Flag) {
1458
+ const raw = getFlag(argv, rawFlag);
1459
+ const encoded = getFlag(argv, b64Flag);
1460
+ return raw ?? (encoded ? fromBase64Url(encoded) : undefined);
1461
+ }
1462
+ async function loadDispatchResultMap(runDir) {
1463
+ try {
1464
+ return await readJsonFile(dispatchResultMapPath(runDir));
1465
+ }
1466
+ catch (error) {
1467
+ if (!isFileMissingError(error)) {
1468
+ throw error;
1469
+ }
1470
+ return null;
1471
+ }
1472
+ }
1473
+ function entriesByTaskId(entries) {
1474
+ return new Map(entries.map((entry) => [entry.task_id, entry]));
1475
+ }
1476
+ function isIsolatedLargeFilePacket(packet) {
1477
+ return (packet.file_paths.length === 1 &&
1478
+ packet.total_lines > LARGE_FILE_PACKET_TARGET_LINES);
1479
+ }
1480
+ function buildDispatchComplexity(packet, largeFileMode) {
1481
+ return {
1482
+ priority: packet.priority,
1483
+ task_count: packet.task_ids.length,
1484
+ file_count: packet.file_paths.length,
1485
+ total_lines: packet.total_lines,
1486
+ estimated_tokens: packet.estimated_tokens,
1487
+ lenses: packet.lenses,
1488
+ tags: packet.tags ?? [],
1489
+ large_file_mode: largeFileMode,
1490
+ };
1491
+ }
1492
+ function buildDispatchModelHint(complexity) {
1493
+ const deepReasons = [];
1494
+ if (complexity.priority === "high")
1495
+ deepReasons.push("high_priority");
1496
+ if (complexity.large_file_mode)
1497
+ deepReasons.push("isolated_large_file");
1498
+ if (complexity.estimated_tokens >= DEEP_MODEL_HINT_MIN_ESTIMATED_TOKENS) {
1499
+ deepReasons.push("high_estimated_tokens");
1500
+ }
1501
+ if (complexity.tags.some((tag) => tag === "critical_flow" || tag.startsWith("critical_flow:"))) {
1502
+ deepReasons.push("critical_flow");
1503
+ }
1504
+ if (complexity.tags.some((tag) => tag === "external_analyzer_signal" || tag.startsWith("external_tool:"))) {
1505
+ deepReasons.push("external_analyzer_signal");
1506
+ }
1507
+ if (deepReasons.length > 0) {
1508
+ return { tier: "deep", reasons: deepReasons };
1509
+ }
1510
+ const sensitiveLenses = new Set(["security", "data_integrity", "reliability"]);
1511
+ const hasSensitiveLens = complexity.lenses.some((lens) => sensitiveLenses.has(lens));
1512
+ if (complexity.priority === "low" &&
1513
+ complexity.total_lines <= SMALL_MODEL_HINT_MAX_LINES &&
1514
+ complexity.estimated_tokens <= SMALL_MODEL_HINT_MAX_ESTIMATED_TOKENS &&
1515
+ !hasSensitiveLens &&
1516
+ complexity.tags.length === 0) {
1517
+ return { tier: "small", reasons: ["small_low_priority_packet"] };
1518
+ }
1519
+ const reasons = [];
1520
+ if (complexity.priority === "medium")
1521
+ reasons.push("medium_priority");
1522
+ if (hasSensitiveLens)
1523
+ reasons.push("sensitive_lens");
1524
+ if (complexity.total_lines > SMALL_MODEL_HINT_MAX_LINES) {
1525
+ reasons.push("moderate_size");
1526
+ }
1527
+ return {
1528
+ tier: "standard",
1529
+ reasons: reasons.length > 0 ? reasons : ["default_review_packet"],
1530
+ };
1531
+ }
1532
+ function withinRoot(root, path) {
1533
+ const rootPath = resolve(root);
1534
+ const absolutePath = resolve(rootPath, path);
1535
+ const relativePath = relative(rootPath, absolutePath);
1536
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
1537
+ throw new Error(`Path '${path}' escapes repository root '${rootPath}'.`);
1538
+ }
1539
+ return absolutePath;
1540
+ }
1541
+ function renderAnchorPreview(summary, anchorPath) {
1542
+ const preview = summary.anchors.slice(0, 24).map((anchor) => {
1543
+ const location = anchor.line ? `${summary.path}:${anchor.line}` : summary.path;
1544
+ const detail = anchor.detail ? ` - ${anchor.detail}` : "";
1545
+ return `- ${location} [${anchor.kind}] ${anchor.name}${detail}`;
1546
+ });
1547
+ return [
1548
+ "## Large File Review Mode",
1549
+ "This packet is intentionally isolated because it covers one large file.",
1550
+ "Use targeted reads/searches within this file, guided by the mechanical anchors.",
1551
+ "Do not read unrelated files unless a finding cannot be evidenced without a direct boundary check.",
1552
+ `Anchor file: ${anchorPath}`,
1553
+ `Anchor counts: symbols=${summary.counts.symbols}, routes=${summary.counts.routes}, keywords=${summary.counts.keywords}, graph_edges=${summary.counts.graph_edges}, analyzer_signals=${summary.counts.analyzer_signals}, omitted=${summary.omitted_anchor_count}`,
1554
+ "Anchor preview:",
1555
+ ...(preview.length > 0 ? preview : ["- no anchors extracted beyond file boundaries"]),
1556
+ "",
1557
+ ];
1558
+ }
1407
1559
  async function cmdPrepareDispatch(argv) {
1408
1560
  const runId = getFlag(argv, "--run-id");
1409
1561
  if (!runId)
@@ -1413,6 +1565,17 @@ async function cmdPrepareDispatch(argv) {
1413
1565
  const tasksPath = join(runDir, "pending-audit-tasks.json");
1414
1566
  const taskResultsDir = join(runDir, "task-results");
1415
1567
  const dispatchPlanPath = join(runDir, "dispatch-plan.json");
1568
+ const explicitRoot = getFlag(argv, "--root") ? getRootDir(argv) : undefined;
1569
+ let reviewRoot = explicitRoot;
1570
+ try {
1571
+ const workerTask = await readJsonFile(join(runDir, "task.json"));
1572
+ reviewRoot ??= workerTask.repo_root;
1573
+ }
1574
+ catch (error) {
1575
+ if (!isFileMissingError(error)) {
1576
+ throw error;
1577
+ }
1578
+ }
1416
1579
  const tasks = await readJsonFile(tasksPath);
1417
1580
  const bundle = await loadArtifactBundle(artifactsDir);
1418
1581
  const lensDefsPath = join(packageRoot, "dispatch", "lens-definitions.json");
@@ -1428,18 +1591,22 @@ async function cmdPrepareDispatch(argv) {
1428
1591
  lineIndex,
1429
1592
  });
1430
1593
  const tasksById = new Map(orderedTasks.map((task) => [task.task_id, task]));
1431
- const outputPathByTaskId = new Map(orderedTasks.map((task) => [
1594
+ const resultPathByTaskId = new Map(orderedTasks.map((task) => [
1432
1595
  task.task_id,
1433
- join(taskResultsDir, `${task.task_id.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`),
1596
+ taskResultPath(taskResultsDir, task.task_id),
1434
1597
  ]));
1598
+ const resultPathSet = new Set(resultPathByTaskId.values());
1599
+ if (resultPathSet.size !== resultPathByTaskId.size) {
1600
+ throw new Error("prepare-dispatch generated duplicate result paths; task ids must be uniquely addressable.");
1601
+ }
1435
1602
  const plan = [];
1603
+ const resultMapEntries = [];
1436
1604
  let largestPacketId = null;
1437
1605
  let largestLines = 0;
1438
1606
  let largestEstimatedTokens = 0;
1439
1607
  const warnings = [];
1440
1608
  for (const packet of packets) {
1441
- const sanitized = packet.packet_id.replace(/[^a-zA-Z0-9_-]/g, "_");
1442
- const promptPath = join(taskResultsDir, `${sanitized}.prompt.md`);
1609
+ const promptPath = packetPromptPath(taskResultsDir, packet.packet_id);
1443
1610
  const packetTasks = packet.task_ids
1444
1611
  .map((taskId) => tasksById.get(taskId))
1445
1612
  .filter((task) => task !== undefined);
@@ -1448,7 +1615,8 @@ async function cmdPrepareDispatch(argv) {
1448
1615
  largestEstimatedTokens = packet.estimated_tokens;
1449
1616
  largestPacketId = packet.packet_id;
1450
1617
  }
1451
- if (packet.total_lines > 2500) {
1618
+ const largeFileMode = isIsolatedLargeFilePacket(packet);
1619
+ if (packet.total_lines > LARGE_FILE_PACKET_TARGET_LINES && !largeFileMode) {
1452
1620
  warnings.push({
1453
1621
  code: "large_packet",
1454
1622
  message: `large packet ${packet.packet_id} (~${packet.total_lines} lines) may hit quota limits`,
@@ -1466,15 +1634,57 @@ async function cmdPrepareDispatch(argv) {
1466
1634
  const lines = packet.file_line_counts[path] ?? 0;
1467
1635
  return `- ${path} (${lines} lines)`;
1468
1636
  }).join("\n");
1637
+ let anchorPath = null;
1638
+ let anchorSummary = null;
1639
+ if (largeFileMode) {
1640
+ const filePath = packet.file_paths[0];
1641
+ if (!reviewRoot) {
1642
+ warnings.push({
1643
+ code: "large_file_anchor_unavailable",
1644
+ message: `large single-file packet ${packet.packet_id} has no repo root available for anchor extraction`,
1645
+ });
1646
+ }
1647
+ else {
1648
+ try {
1649
+ const totalLines = packet.file_line_counts[filePath] ?? packet.total_lines;
1650
+ const content = await readFile(withinRoot(reviewRoot, filePath), "utf8");
1651
+ anchorSummary = buildFileAnchorSummary({
1652
+ path: filePath,
1653
+ content,
1654
+ totalLines,
1655
+ graphBundle: bundle.graph_bundle,
1656
+ externalAnalyzerResults: bundle.external_analyzer_results,
1657
+ });
1658
+ anchorPath = join(taskResultsDir, artifactNameForId(packet.packet_id, "anchors.json"));
1659
+ await writeJsonFile(anchorPath, anchorSummary);
1660
+ }
1661
+ catch (error) {
1662
+ warnings.push({
1663
+ code: "large_file_anchor_failed",
1664
+ message: `large single-file packet ${packet.packet_id} could not be anchored mechanically: ` +
1665
+ (error instanceof Error ? error.message : String(error)),
1666
+ });
1667
+ }
1668
+ }
1669
+ }
1670
+ const largeFileSection = anchorSummary && anchorPath
1671
+ ? renderAnchorPreview(anchorSummary, anchorPath)
1672
+ : largeFileMode
1673
+ ? [
1674
+ "## Large File Review Mode",
1675
+ "This packet is intentionally isolated because it covers one large file.",
1676
+ "Use targeted reads/searches within this file only.",
1677
+ "No mechanical anchor file was available, so rely on targeted symbol and keyword searches before reading broad ranges.",
1678
+ "",
1679
+ ]
1680
+ : [];
1469
1681
  const taskSections = packetTasks.flatMap((task) => {
1470
1682
  const lensDef = lensDefs[task.lens];
1471
- const outputPath = outputPathByTaskId.get(task.task_id);
1472
1683
  return [
1473
1684
  `### ${task.task_id}`,
1474
1685
  `unit_id: ${task.unit_id}`,
1475
1686
  `pass_id: ${task.pass_id}`,
1476
1687
  `lens: ${task.lens}`,
1477
- `output_path: ${outputPath}`,
1478
1688
  `rationale: ${task.rationale}`,
1479
1689
  "",
1480
1690
  `Lens guidance: ${lensDef?.description ?? task.lens}`,
@@ -1482,13 +1692,20 @@ async function cmdPrepareDispatch(argv) {
1482
1692
  "",
1483
1693
  ];
1484
1694
  });
1485
- const validationCommands = packetTasks.map((task) => ` "${process.execPath}" "${join(packageRoot, "audit-code.mjs")}" validate-result --run-id ${runId} --task-id ${task.task_id} --artifacts-dir "${artifactsDir}"`);
1486
- const outputPaths = Object.fromEntries(packetTasks.map((task) => [
1487
- task.task_id,
1488
- outputPathByTaskId.get(task.task_id) ?? "",
1489
- ]));
1695
+ const submitCommand = `"${process.execPath}" "${join(packageRoot, "audit-code.mjs")}" submit-packet ` +
1696
+ `--run-id-b64 ${toBase64Url(runId)} ` +
1697
+ `--packet-id-b64 ${toBase64Url(packet.packet_id)} ` +
1698
+ `--artifacts-dir-b64 ${toBase64Url(artifactsDir)}`;
1699
+ const complexity = buildDispatchComplexity(packet, largeFileMode);
1700
+ for (const task of packetTasks) {
1701
+ resultMapEntries.push({
1702
+ packet_id: packet.packet_id,
1703
+ task_id: task.task_id,
1704
+ result_path: resultPathByTaskId.get(task.task_id),
1705
+ });
1706
+ }
1490
1707
  const prompt = [
1491
- "You are a code auditor. Review this packet once, then produce one result file per listed task.",
1708
+ "You are a code auditor. Review this packet once, then submit exactly one result per listed task.",
1492
1709
  "",
1493
1710
  "## Packet",
1494
1711
  `packet_id: ${packet.packet_id}`,
@@ -1497,15 +1714,18 @@ async function cmdPrepareDispatch(argv) {
1497
1714
  `estimated_tokens: ${packet.estimated_tokens}`,
1498
1715
  "",
1499
1716
  "## Files to read",
1500
- "Use your Read tool. Paths are repo-relative from the current working directory.",
1717
+ largeFileMode
1718
+ ? "Use targeted Read/Grep calls. Paths are repo-relative from the current working directory."
1719
+ : "Use your Read tool. Paths are repo-relative from the current working directory.",
1501
1720
  fileList,
1502
1721
  "",
1722
+ ...largeFileSection,
1503
1723
  "## Tasks",
1504
1724
  ...taskSections,
1505
1725
  "## Output",
1506
- "Write exactly one JSON object for each task to that task's output_path.",
1507
- "Do not combine the task results into one file. Do not edit source files,",
1726
+ "Do not write files directly. Do not use a Write tool, create temp files, edit source files,",
1508
1727
  "remediate findings, create extra task results, or run unrelated audits.",
1728
+ "Produce one JSON array containing exactly one AuditResult object for each listed task.",
1509
1729
  "",
1510
1730
  "Required AuditResult fields:",
1511
1731
  " task_id copy from the task metadata",
@@ -1532,30 +1752,32 @@ async function cmdPrepareDispatch(argv) {
1532
1752
  "3. Only reference files from the packet unless a finding genuinely crosses a boundary.",
1533
1753
  "4. findings: [] is correct when you find nothing genuine.",
1534
1754
  "",
1535
- "## Validate",
1536
- "After writing every result, run:",
1537
- ...validationCommands,
1755
+ "## Submit",
1756
+ "Pipe the JSON array on stdin to this command:",
1757
+ ` ${submitCommand}`,
1538
1758
  "",
1539
- "Exit 0 means valid. Non-zero: read the errors, fix the JSON, rewrite the file, run again. Retry up to 3 times.",
1759
+ "The command validates and writes the packet-owned result files. Exit 0 means accepted.",
1760
+ "Non-zero: read the errors, fix the JSON, and run the same submit command again. Retry up to 3 times.",
1540
1761
  "",
1541
1762
  "## Final response",
1542
- `After every validation command succeeds, reply exactly: valid: ${packet.packet_id}, findings=<total finding count>`,
1763
+ `After the submit command succeeds, reply exactly: valid: ${packet.packet_id}, findings=<total finding count>`,
1543
1764
  ].join("\n");
1544
1765
  await writeFile(promptPath, prompt, "utf8");
1545
1766
  plan.push({
1546
1767
  packet_id: packet.packet_id,
1547
- task_id: packet.task_ids.length === 1 ? packet.task_ids[0] : packet.packet_id,
1548
- task_ids: packet.task_ids,
1549
- description: `Audit ${packet.file_paths.length} file(s), ${packet.task_ids.length} task(s), ${packet.lenses.length} lens(es) (~${packet.total_lines} lines)`,
1550
- output_paths: outputPaths,
1768
+ description: `Audit ${packet.file_paths.length} file(s), ${packet.task_ids.length} task(s), ${packet.lenses.length} lens(es) (~${packet.total_lines} lines)` +
1769
+ (largeFileMode ? " [isolated large-file mode]" : ""),
1551
1770
  prompt_path: promptPath,
1552
- lenses: packet.lenses,
1553
- file_paths: packet.file_paths,
1554
- total_lines: packet.total_lines,
1555
- estimated_tokens: packet.estimated_tokens,
1771
+ complexity,
1772
+ model_hint: buildDispatchModelHint(complexity),
1556
1773
  });
1557
1774
  }
1558
1775
  await writeJsonFile(dispatchPlanPath, plan);
1776
+ await writeJsonFile(dispatchResultMapPath(runDir), {
1777
+ contract_version: "audit-code-dispatch-results/v1alpha1",
1778
+ run_id: runId,
1779
+ entries: resultMapEntries,
1780
+ });
1559
1781
  const warningsPath = warnings.length > 0
1560
1782
  ? join(runDir, "dispatch-warnings.json")
1561
1783
  : null;
@@ -1578,6 +1800,106 @@ async function cmdPrepareDispatch(argv) {
1578
1800
  dispatch_warnings_path: warningsPath,
1579
1801
  }, null, 2));
1580
1802
  }
1803
+ async function cmdSubmitPacket(argv) {
1804
+ const runId = resolveRunScopedArg(argv, "--run-id", "--run-id-b64");
1805
+ const packetId = resolveRunScopedArg(argv, "--packet-id", "--packet-id-b64");
1806
+ const artifactsDirB64 = getFlag(argv, "--artifacts-dir-b64");
1807
+ const artifactsDir = artifactsDirB64
1808
+ ? resolve(fromBase64Url(artifactsDirB64))
1809
+ : getArtifactsDir(argv);
1810
+ if (!runId || !packetId) {
1811
+ throw new Error("submit-packet requires --run-id and --packet-id (or --run-id-b64/--packet-id-b64)");
1812
+ }
1813
+ const runDir = join(artifactsDir, "runs", runId);
1814
+ const tasksPath = join(runDir, "pending-audit-tasks.json");
1815
+ const resultMap = await loadDispatchResultMap(runDir);
1816
+ if (!resultMap) {
1817
+ throw new Error(`No ${DISPATCH_RESULT_MAP_FILENAME} found for run ${runId}; run prepare-dispatch first.`);
1818
+ }
1819
+ const packetEntries = resultMap.entries.filter((entry) => entry.packet_id === packetId);
1820
+ if (packetEntries.length === 0) {
1821
+ throw new Error(`Unknown packet_id '${packetId}' for run ${runId}.`);
1822
+ }
1823
+ if (entriesByTaskId(packetEntries).size !== packetEntries.length) {
1824
+ throw new Error(`Dispatch result map has duplicate task entries for packet '${packetId}'.`);
1825
+ }
1826
+ const allTasks = await readJsonFile(tasksPath);
1827
+ const taskById = new Map(allTasks.map((task) => [task.task_id, task]));
1828
+ const packetTasks = packetEntries.map((entry) => taskById.get(entry.task_id));
1829
+ const missingTask = packetEntries.find((entry, index) => !packetTasks[index]);
1830
+ if (missingTask) {
1831
+ throw new Error(`Dispatch result map references unknown task '${missingTask.task_id}'.`);
1832
+ }
1833
+ const tasks = packetTasks;
1834
+ const expectedTaskIds = new Set(tasks.map((task) => task.task_id));
1835
+ const lineIndex = Object.fromEntries(tasks.flatMap((task) => Object.entries(task.file_line_counts ?? {})));
1836
+ const encodedResults = getFlag(argv, "--results-b64");
1837
+ const raw = encodedResults ? fromBase64Url(encodedResults) : await readStdinText();
1838
+ if (raw.trim().length === 0) {
1839
+ throw new Error("submit-packet requires an AuditResult[] JSON payload on stdin or --results-b64.");
1840
+ }
1841
+ let payload;
1842
+ try {
1843
+ payload = JSON.parse(raw);
1844
+ }
1845
+ catch (error) {
1846
+ throw new Error(`Invalid submit-packet JSON: ${error instanceof Error ? error.message : String(error)}`);
1847
+ }
1848
+ const resultErrors = [];
1849
+ const issues = validateAuditResults(payload, tasks, { lineIndex });
1850
+ const validationErrors = issues.filter((issue) => issue.severity === "error");
1851
+ const validationWarnings = issues.filter((issue) => issue.severity === "warning");
1852
+ if (validationWarnings.length > 0) {
1853
+ process.stderr.write(`audit-results validation: ${validationWarnings.length} warning(s):\n` +
1854
+ formatAuditResultIssues(validationWarnings) +
1855
+ "\n");
1856
+ }
1857
+ if (validationErrors.length > 0) {
1858
+ resultErrors.push(formatAuditResultIssues(validationErrors));
1859
+ }
1860
+ if (Array.isArray(payload)) {
1861
+ const seen = new Set();
1862
+ for (const [index, result] of payload.entries()) {
1863
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
1864
+ continue;
1865
+ }
1866
+ const taskId = result.task_id;
1867
+ if (typeof taskId !== "string" || taskId.trim().length === 0) {
1868
+ continue;
1869
+ }
1870
+ if (seen.has(taskId)) {
1871
+ resultErrors.push(`Duplicate audit result for assigned task '${taskId}'.`);
1872
+ }
1873
+ seen.add(taskId);
1874
+ if (!expectedTaskIds.has(taskId)) {
1875
+ resultErrors.push(`Result at index ${index} uses task_id '${taskId}', which is not assigned to packet '${packetId}'.`);
1876
+ }
1877
+ }
1878
+ for (const task of tasks) {
1879
+ if (!seen.has(task.task_id)) {
1880
+ resultErrors.push(`Missing audit result for assigned task '${task.task_id}'.`);
1881
+ }
1882
+ }
1883
+ }
1884
+ if (resultErrors.length > 0) {
1885
+ throw new Error(`submit-packet rejected ${packetId}:\n${resultErrors.join("\n")}`);
1886
+ }
1887
+ const entryByTaskId = entriesByTaskId(packetEntries);
1888
+ for (const result of payload) {
1889
+ const entry = entryByTaskId.get(result.task_id);
1890
+ if (!entry) {
1891
+ throw new Error(`Internal error: no result path for accepted task '${result.task_id}'.`);
1892
+ }
1893
+ await writeJsonFile(entry.result_path, result);
1894
+ }
1895
+ const findingCount = payload.reduce((sum, result) => sum + result.findings.length, 0);
1896
+ console.log(JSON.stringify({
1897
+ run_id: runId,
1898
+ packet_id: packetId,
1899
+ accepted_count: payload.length,
1900
+ finding_count: findingCount,
1901
+ }, null, 2));
1902
+ }
1581
1903
  async function cmdMergeAndIngest(argv) {
1582
1904
  const runId = getFlag(argv, "--run-id");
1583
1905
  if (!runId)
@@ -1589,12 +1911,20 @@ async function cmdMergeAndIngest(argv) {
1589
1911
  const taskPath = join(runDir, "task.json");
1590
1912
  const tasksPath = join(runDir, "pending-audit-tasks.json");
1591
1913
  const workerTask = await readJsonFile(taskPath);
1914
+ const resultMap = await loadDispatchResultMap(runDir);
1915
+ if (!resultMap) {
1916
+ throw new Error(`No ${DISPATCH_RESULT_MAP_FILENAME} found for run ${runId}; run prepare-dispatch first.`);
1917
+ }
1592
1918
  let allTasks = [];
1593
1919
  try {
1594
1920
  allTasks = await readJsonFile(tasksPath);
1595
1921
  }
1596
1922
  catch { /* may not exist */ }
1597
- const lineIndex = Object.fromEntries(allTasks.flatMap((task) => Object.entries(task.file_line_counts ?? {})));
1923
+ const entryByTaskId = entriesByTaskId(resultMap.entries);
1924
+ if (entryByTaskId.size !== resultMap.entries.length) {
1925
+ throw new Error(`Dispatch result map for run ${runId} contains duplicate task entries.`);
1926
+ }
1927
+ const expectedPaths = new Set(resultMap.entries.map((entry) => resolve(entry.result_path)));
1598
1928
  let files;
1599
1929
  try {
1600
1930
  files = (await readdir(taskResultsDir)).filter(f => f.endsWith(".json")).sort();
@@ -1606,13 +1936,38 @@ async function cmdMergeAndIngest(argv) {
1606
1936
  const failing = [];
1607
1937
  const seenTaskIds = new Set();
1608
1938
  for (const filename of files) {
1609
- const filePath = join(taskResultsDir, filename);
1939
+ const filePath = resolve(join(taskResultsDir, filename));
1940
+ if (!expectedPaths.has(filePath)) {
1941
+ failing.push({
1942
+ task_id: filename,
1943
+ errors: ["Unexpected task result file; only backend-assigned result paths may be ingested."],
1944
+ });
1945
+ }
1946
+ }
1947
+ for (const task of allTasks) {
1948
+ const entry = entryByTaskId.get(task.task_id);
1949
+ if (!entry) {
1950
+ failing.push({
1951
+ task_id: task.task_id,
1952
+ errors: ["Missing dispatch result-map entry for assigned task."],
1953
+ });
1954
+ continue;
1955
+ }
1956
+ const filePath = entry.result_path;
1610
1957
  let obj;
1611
1958
  try {
1612
1959
  obj = JSON.parse(await readFile(filePath, "utf8"));
1613
1960
  }
1614
1961
  catch (e) {
1615
- failing.push({ task_id: filename, errors: [`Invalid JSON: ${e.message}`] });
1962
+ if (isFileMissingError(e)) {
1963
+ failing.push({
1964
+ task_id: task.task_id,
1965
+ errors: ["Missing audit result for assigned task."],
1966
+ });
1967
+ }
1968
+ else {
1969
+ failing.push({ task_id: task.task_id, errors: [`Invalid JSON: ${e.message}`] });
1970
+ }
1616
1971
  continue;
1617
1972
  }
1618
1973
  const record = obj && typeof obj === "object" && !Array.isArray(obj)
@@ -1628,8 +1983,11 @@ async function cmdMergeAndIngest(argv) {
1628
1983
  else {
1629
1984
  seenTaskIds.add(taskId);
1630
1985
  }
1986
+ if (taskId !== task.task_id) {
1987
+ resultErrors.push(`Result file is assigned to '${task.task_id}' but contains task_id '${taskId}'.`);
1988
+ }
1631
1989
  }
1632
- const issues = validateAuditResults([obj], allTasks, { lineIndex });
1990
+ const issues = validateAuditResults([obj], [task], { lineIndex: task.file_line_counts ?? {} });
1633
1991
  resultErrors.push(...issues
1634
1992
  .filter(i => i.severity === "error")
1635
1993
  .map(i => i.message));
@@ -1637,15 +1995,7 @@ async function cmdMergeAndIngest(argv) {
1637
1995
  passing.push(obj);
1638
1996
  }
1639
1997
  else {
1640
- failing.push({ task_id: taskId ?? filename, errors: resultErrors });
1641
- }
1642
- }
1643
- for (const task of allTasks) {
1644
- if (!seenTaskIds.has(task.task_id)) {
1645
- failing.push({
1646
- task_id: task.task_id,
1647
- errors: ["Missing audit result for assigned task."],
1648
- });
1998
+ failing.push({ task_id: taskId ?? task.task_id, errors: resultErrors });
1649
1999
  }
1650
2000
  }
1651
2001
  await writeJsonFile(auditResultsPath, passing);
@@ -1687,14 +2037,25 @@ async function cmdMergeAndIngest(argv) {
1687
2037
  }, null, 2));
1688
2038
  }
1689
2039
  async function cmdValidateResult(argv) {
1690
- const runId = getFlag(argv, "--run-id");
1691
- const taskId = getFlag(argv, "--task-id");
1692
- if (!runId || !taskId)
1693
- throw new Error("validate-result requires --run-id and --task-id");
1694
- const artifactsDir = getArtifactsDir(argv);
1695
- const sanitized = taskId.replace(/[^a-zA-Z0-9_-]/g, "_");
1696
- const resultPath = join(artifactsDir, "runs", runId, "task-results", `${sanitized}.json`);
1697
- const tasksPath = join(artifactsDir, "runs", runId, "pending-audit-tasks.json");
2040
+ const rawRunId = getFlag(argv, "--run-id");
2041
+ const runIdB64 = getFlag(argv, "--run-id-b64");
2042
+ const rawTaskId = getFlag(argv, "--task-id");
2043
+ const artifactsDirB64 = getFlag(argv, "--artifacts-dir-b64");
2044
+ const runId = rawRunId ?? (runIdB64 ? fromBase64Url(runIdB64) : undefined);
2045
+ const taskIdB64 = getFlag(argv, "--task-id-b64");
2046
+ const taskId = rawTaskId ?? (taskIdB64 ? fromBase64Url(taskIdB64) : undefined);
2047
+ const artifactsDir = artifactsDirB64
2048
+ ? resolve(fromBase64Url(artifactsDirB64))
2049
+ : getArtifactsDir(argv);
2050
+ if (!runId || !taskId) {
2051
+ throw new Error("validate-result requires --run-id and --task-id (or --run-id-b64/--task-id-b64)");
2052
+ }
2053
+ const runDir = join(artifactsDir, "runs", runId);
2054
+ const taskResultsDir = join(runDir, "task-results");
2055
+ const resultMap = await loadDispatchResultMap(runDir);
2056
+ const resultPath = resultMap?.entries.find((entry) => entry.task_id === taskId)?.result_path ??
2057
+ taskResultPath(taskResultsDir, taskId);
2058
+ const tasksPath = join(runDir, "pending-audit-tasks.json");
1698
2059
  let raw;
1699
2060
  try {
1700
2061
  raw = await readFile(resultPath, "utf8");
@@ -1867,7 +2228,7 @@ async function cmdValidate(argv) {
1867
2228
  ...providerIssues,
1868
2229
  ];
1869
2230
  const resolvedProvider = rawSessionConfig === undefined
1870
- ? "auto"
2231
+ ? "local-subprocess"
1871
2232
  : sessionConfigIssues.length > 0
1872
2233
  ? null
1873
2234
  : resolveFreshSessionProviderName(undefined, rawSessionConfig);
@@ -1986,12 +2347,15 @@ async function main(argv) {
1986
2347
  case "merge-and-ingest":
1987
2348
  await cmdMergeAndIngest(argv);
1988
2349
  return;
2350
+ case "submit-packet":
2351
+ await cmdSubmitPacket(argv);
2352
+ return;
1989
2353
  case "validate-result":
1990
2354
  await cmdValidateResult(argv);
1991
2355
  return;
1992
2356
  default:
1993
2357
  console.error(`Unknown command: ${command}`);
1994
- console.error("Available commands: sample-run, advance-audit, run-to-completion, worker-run, import-external-analyzer, intake, plan, ingest-results, explain-task, update-runtime-validation, validate, validate-results, requeue, synthesize, mcp, prepare-dispatch, merge-and-ingest, validate-result");
2358
+ console.error("Available commands: sample-run, advance-audit, run-to-completion, worker-run, import-external-analyzer, intake, plan, ingest-results, explain-task, update-runtime-validation, validate, validate-results, requeue, synthesize, mcp, prepare-dispatch, merge-and-ingest, submit-packet, validate-result");
1995
2359
  process.exitCode = 1;
1996
2360
  }
1997
2361
  }