auditor-lambda 0.3.4 → 0.3.5

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.
@@ -260,9 +260,11 @@ 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',
263
+ '- prepare-dispatch --run-id <id> [--artifacts-dir <dir>] creates packet prompt files and a slim dispatch-plan.json for parallel subagent dispatch',
264
+ '- submit-packet --run-id <id> --packet-id <id> [--artifacts-dir <dir>] validates AuditResult[] from stdin and writes only backend-assigned result files',
265
+ '- merge-and-ingest --run-id <id> [--root <dir>] [--artifacts-dir <dir>] merges assigned packet results and ingests them into the coverage matrix',
265
266
  '- validate-result --run-id <id> --task-id <id> [--artifacts-dir <dir>] validates a single task result against the schema and line counts',
267
+ ' generated packet prompts may use --run-id-b64, --task-id-b64, and --artifacts-dir-b64 to avoid shell-sensitive raw ids',
266
268
  '',
267
269
  'Primary usage:',
268
270
  '- from the repository root, run the wrapper with no arguments',
@@ -2281,6 +2283,11 @@ export async function runAuditCodeWrapper({
2281
2283
  return;
2282
2284
  }
2283
2285
 
2286
+ if (argv[0] === 'submit-packet') {
2287
+ await runDistCommand('submit-packet', argv.slice(1));
2288
+ return;
2289
+ }
2290
+
2284
2291
  if (argv[0] === 'merge-and-ingest') {
2285
2292
  await runDistCommand('merge-and-ingest', argv.slice(1), { ensureArtifactsDir: true });
2286
2293
  return;
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,13 @@ 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;
35
39
  const DIRECT_CLI_DEFAULTS = {
36
40
  rootDir: ".",
37
41
  artifactsDir: ".artifacts",
@@ -65,6 +69,45 @@ function getFlag(argv, name, fallback) {
65
69
  function hasFlag(argv, name) {
66
70
  return argv.includes(name);
67
71
  }
72
+ function toBase64Url(value) {
73
+ return Buffer.from(value, "utf8").toString("base64url");
74
+ }
75
+ function fromBase64Url(value) {
76
+ return Buffer.from(value, "base64url").toString("utf8");
77
+ }
78
+ function digestId(value) {
79
+ return createHash("sha256").update(value).digest("hex").slice(0, 12);
80
+ }
81
+ function safeArtifactStem(value) {
82
+ const sanitized = value
83
+ .replace(/[^a-zA-Z0-9_-]+/g, "_")
84
+ .replace(/^_+|_+$/g, "")
85
+ .slice(0, 80);
86
+ return sanitized.length > 0 ? sanitized : "artifact";
87
+ }
88
+ function artifactNameForId(value, extension) {
89
+ return `${safeArtifactStem(value)}_${digestId(value)}.${extension}`;
90
+ }
91
+ function taskResultPath(taskResultsDir, taskId) {
92
+ return join(taskResultsDir, artifactNameForId(taskId, "json"));
93
+ }
94
+ function packetPromptPath(taskResultsDir, packetId) {
95
+ return join(taskResultsDir, artifactNameForId(packetId, "prompt.md"));
96
+ }
97
+ async function readStdinText() {
98
+ if (process.stdin.isTTY) {
99
+ return "";
100
+ }
101
+ return await new Promise((resolveInput, reject) => {
102
+ let input = "";
103
+ process.stdin.setEncoding("utf8");
104
+ process.stdin.on("data", (chunk) => {
105
+ input += chunk;
106
+ });
107
+ process.stdin.on("end", () => resolveInput(input));
108
+ process.stdin.on("error", reject);
109
+ });
110
+ }
68
111
  function resolveFlagPath(argv, name, fallback) {
69
112
  return resolve(getFlag(argv, name, fallback));
70
113
  }
@@ -1404,6 +1447,60 @@ async function cmdWorkerRun(argv) {
1404
1447
  process.exitCode = 1;
1405
1448
  }
1406
1449
  }
1450
+ const DISPATCH_RESULT_MAP_FILENAME = "dispatch-result-map.json";
1451
+ function dispatchResultMapPath(runDir) {
1452
+ return join(runDir, DISPATCH_RESULT_MAP_FILENAME);
1453
+ }
1454
+ function resolveRunScopedArg(argv, rawFlag, b64Flag) {
1455
+ const raw = getFlag(argv, rawFlag);
1456
+ const encoded = getFlag(argv, b64Flag);
1457
+ return raw ?? (encoded ? fromBase64Url(encoded) : undefined);
1458
+ }
1459
+ async function loadDispatchResultMap(runDir) {
1460
+ try {
1461
+ return await readJsonFile(dispatchResultMapPath(runDir));
1462
+ }
1463
+ catch (error) {
1464
+ if (!isFileMissingError(error)) {
1465
+ throw error;
1466
+ }
1467
+ return null;
1468
+ }
1469
+ }
1470
+ function entriesByTaskId(entries) {
1471
+ return new Map(entries.map((entry) => [entry.task_id, entry]));
1472
+ }
1473
+ function isIsolatedLargeFilePacket(packet) {
1474
+ return (packet.file_paths.length === 1 &&
1475
+ packet.total_lines > LARGE_FILE_PACKET_TARGET_LINES);
1476
+ }
1477
+ function withinRoot(root, path) {
1478
+ const rootPath = resolve(root);
1479
+ const absolutePath = resolve(rootPath, path);
1480
+ const relativePath = relative(rootPath, absolutePath);
1481
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
1482
+ throw new Error(`Path '${path}' escapes repository root '${rootPath}'.`);
1483
+ }
1484
+ return absolutePath;
1485
+ }
1486
+ function renderAnchorPreview(summary, anchorPath) {
1487
+ const preview = summary.anchors.slice(0, 24).map((anchor) => {
1488
+ const location = anchor.line ? `${summary.path}:${anchor.line}` : summary.path;
1489
+ const detail = anchor.detail ? ` - ${anchor.detail}` : "";
1490
+ return `- ${location} [${anchor.kind}] ${anchor.name}${detail}`;
1491
+ });
1492
+ return [
1493
+ "## Large File Review Mode",
1494
+ "This packet is intentionally isolated because it covers one large file.",
1495
+ "Use targeted reads/searches within this file, guided by the mechanical anchors.",
1496
+ "Do not read unrelated files unless a finding cannot be evidenced without a direct boundary check.",
1497
+ `Anchor file: ${anchorPath}`,
1498
+ `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}`,
1499
+ "Anchor preview:",
1500
+ ...(preview.length > 0 ? preview : ["- no anchors extracted beyond file boundaries"]),
1501
+ "",
1502
+ ];
1503
+ }
1407
1504
  async function cmdPrepareDispatch(argv) {
1408
1505
  const runId = getFlag(argv, "--run-id");
1409
1506
  if (!runId)
@@ -1413,6 +1510,17 @@ async function cmdPrepareDispatch(argv) {
1413
1510
  const tasksPath = join(runDir, "pending-audit-tasks.json");
1414
1511
  const taskResultsDir = join(runDir, "task-results");
1415
1512
  const dispatchPlanPath = join(runDir, "dispatch-plan.json");
1513
+ const explicitRoot = getFlag(argv, "--root") ? getRootDir(argv) : undefined;
1514
+ let reviewRoot = explicitRoot;
1515
+ try {
1516
+ const workerTask = await readJsonFile(join(runDir, "task.json"));
1517
+ reviewRoot ??= workerTask.repo_root;
1518
+ }
1519
+ catch (error) {
1520
+ if (!isFileMissingError(error)) {
1521
+ throw error;
1522
+ }
1523
+ }
1416
1524
  const tasks = await readJsonFile(tasksPath);
1417
1525
  const bundle = await loadArtifactBundle(artifactsDir);
1418
1526
  const lensDefsPath = join(packageRoot, "dispatch", "lens-definitions.json");
@@ -1428,18 +1536,22 @@ async function cmdPrepareDispatch(argv) {
1428
1536
  lineIndex,
1429
1537
  });
1430
1538
  const tasksById = new Map(orderedTasks.map((task) => [task.task_id, task]));
1431
- const outputPathByTaskId = new Map(orderedTasks.map((task) => [
1539
+ const resultPathByTaskId = new Map(orderedTasks.map((task) => [
1432
1540
  task.task_id,
1433
- join(taskResultsDir, `${task.task_id.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`),
1541
+ taskResultPath(taskResultsDir, task.task_id),
1434
1542
  ]));
1543
+ const resultPathSet = new Set(resultPathByTaskId.values());
1544
+ if (resultPathSet.size !== resultPathByTaskId.size) {
1545
+ throw new Error("prepare-dispatch generated duplicate result paths; task ids must be uniquely addressable.");
1546
+ }
1435
1547
  const plan = [];
1548
+ const resultMapEntries = [];
1436
1549
  let largestPacketId = null;
1437
1550
  let largestLines = 0;
1438
1551
  let largestEstimatedTokens = 0;
1439
1552
  const warnings = [];
1440
1553
  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`);
1554
+ const promptPath = packetPromptPath(taskResultsDir, packet.packet_id);
1443
1555
  const packetTasks = packet.task_ids
1444
1556
  .map((taskId) => tasksById.get(taskId))
1445
1557
  .filter((task) => task !== undefined);
@@ -1448,7 +1560,8 @@ async function cmdPrepareDispatch(argv) {
1448
1560
  largestEstimatedTokens = packet.estimated_tokens;
1449
1561
  largestPacketId = packet.packet_id;
1450
1562
  }
1451
- if (packet.total_lines > 2500) {
1563
+ const largeFileMode = isIsolatedLargeFilePacket(packet);
1564
+ if (packet.total_lines > LARGE_FILE_PACKET_TARGET_LINES && !largeFileMode) {
1452
1565
  warnings.push({
1453
1566
  code: "large_packet",
1454
1567
  message: `large packet ${packet.packet_id} (~${packet.total_lines} lines) may hit quota limits`,
@@ -1466,15 +1579,57 @@ async function cmdPrepareDispatch(argv) {
1466
1579
  const lines = packet.file_line_counts[path] ?? 0;
1467
1580
  return `- ${path} (${lines} lines)`;
1468
1581
  }).join("\n");
1582
+ let anchorPath = null;
1583
+ let anchorSummary = null;
1584
+ if (largeFileMode) {
1585
+ const filePath = packet.file_paths[0];
1586
+ if (!reviewRoot) {
1587
+ warnings.push({
1588
+ code: "large_file_anchor_unavailable",
1589
+ message: `large single-file packet ${packet.packet_id} has no repo root available for anchor extraction`,
1590
+ });
1591
+ }
1592
+ else {
1593
+ try {
1594
+ const totalLines = packet.file_line_counts[filePath] ?? packet.total_lines;
1595
+ const content = await readFile(withinRoot(reviewRoot, filePath), "utf8");
1596
+ anchorSummary = buildFileAnchorSummary({
1597
+ path: filePath,
1598
+ content,
1599
+ totalLines,
1600
+ graphBundle: bundle.graph_bundle,
1601
+ externalAnalyzerResults: bundle.external_analyzer_results,
1602
+ });
1603
+ anchorPath = join(taskResultsDir, artifactNameForId(packet.packet_id, "anchors.json"));
1604
+ await writeJsonFile(anchorPath, anchorSummary);
1605
+ }
1606
+ catch (error) {
1607
+ warnings.push({
1608
+ code: "large_file_anchor_failed",
1609
+ message: `large single-file packet ${packet.packet_id} could not be anchored mechanically: ` +
1610
+ (error instanceof Error ? error.message : String(error)),
1611
+ });
1612
+ }
1613
+ }
1614
+ }
1615
+ const largeFileSection = anchorSummary && anchorPath
1616
+ ? renderAnchorPreview(anchorSummary, anchorPath)
1617
+ : largeFileMode
1618
+ ? [
1619
+ "## Large File Review Mode",
1620
+ "This packet is intentionally isolated because it covers one large file.",
1621
+ "Use targeted reads/searches within this file only.",
1622
+ "No mechanical anchor file was available, so rely on targeted symbol and keyword searches before reading broad ranges.",
1623
+ "",
1624
+ ]
1625
+ : [];
1469
1626
  const taskSections = packetTasks.flatMap((task) => {
1470
1627
  const lensDef = lensDefs[task.lens];
1471
- const outputPath = outputPathByTaskId.get(task.task_id);
1472
1628
  return [
1473
1629
  `### ${task.task_id}`,
1474
1630
  `unit_id: ${task.unit_id}`,
1475
1631
  `pass_id: ${task.pass_id}`,
1476
1632
  `lens: ${task.lens}`,
1477
- `output_path: ${outputPath}`,
1478
1633
  `rationale: ${task.rationale}`,
1479
1634
  "",
1480
1635
  `Lens guidance: ${lensDef?.description ?? task.lens}`,
@@ -1482,13 +1637,19 @@ async function cmdPrepareDispatch(argv) {
1482
1637
  "",
1483
1638
  ];
1484
1639
  });
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
- ]));
1640
+ const submitCommand = `"${process.execPath}" "${join(packageRoot, "audit-code.mjs")}" submit-packet ` +
1641
+ `--run-id-b64 ${toBase64Url(runId)} ` +
1642
+ `--packet-id-b64 ${toBase64Url(packet.packet_id)} ` +
1643
+ `--artifacts-dir-b64 ${toBase64Url(artifactsDir)}`;
1644
+ for (const task of packetTasks) {
1645
+ resultMapEntries.push({
1646
+ packet_id: packet.packet_id,
1647
+ task_id: task.task_id,
1648
+ result_path: resultPathByTaskId.get(task.task_id),
1649
+ });
1650
+ }
1490
1651
  const prompt = [
1491
- "You are a code auditor. Review this packet once, then produce one result file per listed task.",
1652
+ "You are a code auditor. Review this packet once, then submit exactly one result per listed task.",
1492
1653
  "",
1493
1654
  "## Packet",
1494
1655
  `packet_id: ${packet.packet_id}`,
@@ -1497,15 +1658,18 @@ async function cmdPrepareDispatch(argv) {
1497
1658
  `estimated_tokens: ${packet.estimated_tokens}`,
1498
1659
  "",
1499
1660
  "## Files to read",
1500
- "Use your Read tool. Paths are repo-relative from the current working directory.",
1661
+ largeFileMode
1662
+ ? "Use targeted Read/Grep calls. Paths are repo-relative from the current working directory."
1663
+ : "Use your Read tool. Paths are repo-relative from the current working directory.",
1501
1664
  fileList,
1502
1665
  "",
1666
+ ...largeFileSection,
1503
1667
  "## Tasks",
1504
1668
  ...taskSections,
1505
1669
  "## 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,",
1670
+ "Do not write files directly. Do not use a Write tool, create temp files, edit source files,",
1508
1671
  "remediate findings, create extra task results, or run unrelated audits.",
1672
+ "Produce one JSON array containing exactly one AuditResult object for each listed task.",
1509
1673
  "",
1510
1674
  "Required AuditResult fields:",
1511
1675
  " task_id copy from the task metadata",
@@ -1532,30 +1696,30 @@ async function cmdPrepareDispatch(argv) {
1532
1696
  "3. Only reference files from the packet unless a finding genuinely crosses a boundary.",
1533
1697
  "4. findings: [] is correct when you find nothing genuine.",
1534
1698
  "",
1535
- "## Validate",
1536
- "After writing every result, run:",
1537
- ...validationCommands,
1699
+ "## Submit",
1700
+ "Pipe the JSON array on stdin to this command:",
1701
+ ` ${submitCommand}`,
1538
1702
  "",
1539
- "Exit 0 means valid. Non-zero: read the errors, fix the JSON, rewrite the file, run again. Retry up to 3 times.",
1703
+ "The command validates and writes the packet-owned result files. Exit 0 means accepted.",
1704
+ "Non-zero: read the errors, fix the JSON, and run the same submit command again. Retry up to 3 times.",
1540
1705
  "",
1541
1706
  "## Final response",
1542
- `After every validation command succeeds, reply exactly: valid: ${packet.packet_id}, findings=<total finding count>`,
1707
+ `After the submit command succeeds, reply exactly: valid: ${packet.packet_id}, findings=<total finding count>`,
1543
1708
  ].join("\n");
1544
1709
  await writeFile(promptPath, prompt, "utf8");
1545
1710
  plan.push({
1546
1711
  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,
1712
+ description: `Audit ${packet.file_paths.length} file(s), ${packet.task_ids.length} task(s), ${packet.lenses.length} lens(es) (~${packet.total_lines} lines)` +
1713
+ (largeFileMode ? " [isolated large-file mode]" : ""),
1551
1714
  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,
1556
1715
  });
1557
1716
  }
1558
1717
  await writeJsonFile(dispatchPlanPath, plan);
1718
+ await writeJsonFile(dispatchResultMapPath(runDir), {
1719
+ contract_version: "audit-code-dispatch-results/v1alpha1",
1720
+ run_id: runId,
1721
+ entries: resultMapEntries,
1722
+ });
1559
1723
  const warningsPath = warnings.length > 0
1560
1724
  ? join(runDir, "dispatch-warnings.json")
1561
1725
  : null;
@@ -1578,6 +1742,106 @@ async function cmdPrepareDispatch(argv) {
1578
1742
  dispatch_warnings_path: warningsPath,
1579
1743
  }, null, 2));
1580
1744
  }
1745
+ async function cmdSubmitPacket(argv) {
1746
+ const runId = resolveRunScopedArg(argv, "--run-id", "--run-id-b64");
1747
+ const packetId = resolveRunScopedArg(argv, "--packet-id", "--packet-id-b64");
1748
+ const artifactsDirB64 = getFlag(argv, "--artifacts-dir-b64");
1749
+ const artifactsDir = artifactsDirB64
1750
+ ? resolve(fromBase64Url(artifactsDirB64))
1751
+ : getArtifactsDir(argv);
1752
+ if (!runId || !packetId) {
1753
+ throw new Error("submit-packet requires --run-id and --packet-id (or --run-id-b64/--packet-id-b64)");
1754
+ }
1755
+ const runDir = join(artifactsDir, "runs", runId);
1756
+ const tasksPath = join(runDir, "pending-audit-tasks.json");
1757
+ const resultMap = await loadDispatchResultMap(runDir);
1758
+ if (!resultMap) {
1759
+ throw new Error(`No ${DISPATCH_RESULT_MAP_FILENAME} found for run ${runId}; run prepare-dispatch first.`);
1760
+ }
1761
+ const packetEntries = resultMap.entries.filter((entry) => entry.packet_id === packetId);
1762
+ if (packetEntries.length === 0) {
1763
+ throw new Error(`Unknown packet_id '${packetId}' for run ${runId}.`);
1764
+ }
1765
+ if (entriesByTaskId(packetEntries).size !== packetEntries.length) {
1766
+ throw new Error(`Dispatch result map has duplicate task entries for packet '${packetId}'.`);
1767
+ }
1768
+ const allTasks = await readJsonFile(tasksPath);
1769
+ const taskById = new Map(allTasks.map((task) => [task.task_id, task]));
1770
+ const packetTasks = packetEntries.map((entry) => taskById.get(entry.task_id));
1771
+ const missingTask = packetEntries.find((entry, index) => !packetTasks[index]);
1772
+ if (missingTask) {
1773
+ throw new Error(`Dispatch result map references unknown task '${missingTask.task_id}'.`);
1774
+ }
1775
+ const tasks = packetTasks;
1776
+ const expectedTaskIds = new Set(tasks.map((task) => task.task_id));
1777
+ const lineIndex = Object.fromEntries(tasks.flatMap((task) => Object.entries(task.file_line_counts ?? {})));
1778
+ const encodedResults = getFlag(argv, "--results-b64");
1779
+ const raw = encodedResults ? fromBase64Url(encodedResults) : await readStdinText();
1780
+ if (raw.trim().length === 0) {
1781
+ throw new Error("submit-packet requires an AuditResult[] JSON payload on stdin or --results-b64.");
1782
+ }
1783
+ let payload;
1784
+ try {
1785
+ payload = JSON.parse(raw);
1786
+ }
1787
+ catch (error) {
1788
+ throw new Error(`Invalid submit-packet JSON: ${error instanceof Error ? error.message : String(error)}`);
1789
+ }
1790
+ const resultErrors = [];
1791
+ const issues = validateAuditResults(payload, tasks, { lineIndex });
1792
+ const validationErrors = issues.filter((issue) => issue.severity === "error");
1793
+ const validationWarnings = issues.filter((issue) => issue.severity === "warning");
1794
+ if (validationWarnings.length > 0) {
1795
+ process.stderr.write(`audit-results validation: ${validationWarnings.length} warning(s):\n` +
1796
+ formatAuditResultIssues(validationWarnings) +
1797
+ "\n");
1798
+ }
1799
+ if (validationErrors.length > 0) {
1800
+ resultErrors.push(formatAuditResultIssues(validationErrors));
1801
+ }
1802
+ if (Array.isArray(payload)) {
1803
+ const seen = new Set();
1804
+ for (const [index, result] of payload.entries()) {
1805
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
1806
+ continue;
1807
+ }
1808
+ const taskId = result.task_id;
1809
+ if (typeof taskId !== "string" || taskId.trim().length === 0) {
1810
+ continue;
1811
+ }
1812
+ if (seen.has(taskId)) {
1813
+ resultErrors.push(`Duplicate audit result for assigned task '${taskId}'.`);
1814
+ }
1815
+ seen.add(taskId);
1816
+ if (!expectedTaskIds.has(taskId)) {
1817
+ resultErrors.push(`Result at index ${index} uses task_id '${taskId}', which is not assigned to packet '${packetId}'.`);
1818
+ }
1819
+ }
1820
+ for (const task of tasks) {
1821
+ if (!seen.has(task.task_id)) {
1822
+ resultErrors.push(`Missing audit result for assigned task '${task.task_id}'.`);
1823
+ }
1824
+ }
1825
+ }
1826
+ if (resultErrors.length > 0) {
1827
+ throw new Error(`submit-packet rejected ${packetId}:\n${resultErrors.join("\n")}`);
1828
+ }
1829
+ const entryByTaskId = entriesByTaskId(packetEntries);
1830
+ for (const result of payload) {
1831
+ const entry = entryByTaskId.get(result.task_id);
1832
+ if (!entry) {
1833
+ throw new Error(`Internal error: no result path for accepted task '${result.task_id}'.`);
1834
+ }
1835
+ await writeJsonFile(entry.result_path, result);
1836
+ }
1837
+ const findingCount = payload.reduce((sum, result) => sum + result.findings.length, 0);
1838
+ console.log(JSON.stringify({
1839
+ run_id: runId,
1840
+ packet_id: packetId,
1841
+ accepted_count: payload.length,
1842
+ finding_count: findingCount,
1843
+ }, null, 2));
1844
+ }
1581
1845
  async function cmdMergeAndIngest(argv) {
1582
1846
  const runId = getFlag(argv, "--run-id");
1583
1847
  if (!runId)
@@ -1589,12 +1853,20 @@ async function cmdMergeAndIngest(argv) {
1589
1853
  const taskPath = join(runDir, "task.json");
1590
1854
  const tasksPath = join(runDir, "pending-audit-tasks.json");
1591
1855
  const workerTask = await readJsonFile(taskPath);
1856
+ const resultMap = await loadDispatchResultMap(runDir);
1857
+ if (!resultMap) {
1858
+ throw new Error(`No ${DISPATCH_RESULT_MAP_FILENAME} found for run ${runId}; run prepare-dispatch first.`);
1859
+ }
1592
1860
  let allTasks = [];
1593
1861
  try {
1594
1862
  allTasks = await readJsonFile(tasksPath);
1595
1863
  }
1596
1864
  catch { /* may not exist */ }
1597
- const lineIndex = Object.fromEntries(allTasks.flatMap((task) => Object.entries(task.file_line_counts ?? {})));
1865
+ const entryByTaskId = entriesByTaskId(resultMap.entries);
1866
+ if (entryByTaskId.size !== resultMap.entries.length) {
1867
+ throw new Error(`Dispatch result map for run ${runId} contains duplicate task entries.`);
1868
+ }
1869
+ const expectedPaths = new Set(resultMap.entries.map((entry) => resolve(entry.result_path)));
1598
1870
  let files;
1599
1871
  try {
1600
1872
  files = (await readdir(taskResultsDir)).filter(f => f.endsWith(".json")).sort();
@@ -1606,13 +1878,38 @@ async function cmdMergeAndIngest(argv) {
1606
1878
  const failing = [];
1607
1879
  const seenTaskIds = new Set();
1608
1880
  for (const filename of files) {
1609
- const filePath = join(taskResultsDir, filename);
1881
+ const filePath = resolve(join(taskResultsDir, filename));
1882
+ if (!expectedPaths.has(filePath)) {
1883
+ failing.push({
1884
+ task_id: filename,
1885
+ errors: ["Unexpected task result file; only backend-assigned result paths may be ingested."],
1886
+ });
1887
+ }
1888
+ }
1889
+ for (const task of allTasks) {
1890
+ const entry = entryByTaskId.get(task.task_id);
1891
+ if (!entry) {
1892
+ failing.push({
1893
+ task_id: task.task_id,
1894
+ errors: ["Missing dispatch result-map entry for assigned task."],
1895
+ });
1896
+ continue;
1897
+ }
1898
+ const filePath = entry.result_path;
1610
1899
  let obj;
1611
1900
  try {
1612
1901
  obj = JSON.parse(await readFile(filePath, "utf8"));
1613
1902
  }
1614
1903
  catch (e) {
1615
- failing.push({ task_id: filename, errors: [`Invalid JSON: ${e.message}`] });
1904
+ if (isFileMissingError(e)) {
1905
+ failing.push({
1906
+ task_id: task.task_id,
1907
+ errors: ["Missing audit result for assigned task."],
1908
+ });
1909
+ }
1910
+ else {
1911
+ failing.push({ task_id: task.task_id, errors: [`Invalid JSON: ${e.message}`] });
1912
+ }
1616
1913
  continue;
1617
1914
  }
1618
1915
  const record = obj && typeof obj === "object" && !Array.isArray(obj)
@@ -1628,8 +1925,11 @@ async function cmdMergeAndIngest(argv) {
1628
1925
  else {
1629
1926
  seenTaskIds.add(taskId);
1630
1927
  }
1928
+ if (taskId !== task.task_id) {
1929
+ resultErrors.push(`Result file is assigned to '${task.task_id}' but contains task_id '${taskId}'.`);
1930
+ }
1631
1931
  }
1632
- const issues = validateAuditResults([obj], allTasks, { lineIndex });
1932
+ const issues = validateAuditResults([obj], [task], { lineIndex: task.file_line_counts ?? {} });
1633
1933
  resultErrors.push(...issues
1634
1934
  .filter(i => i.severity === "error")
1635
1935
  .map(i => i.message));
@@ -1637,15 +1937,7 @@ async function cmdMergeAndIngest(argv) {
1637
1937
  passing.push(obj);
1638
1938
  }
1639
1939
  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
- });
1940
+ failing.push({ task_id: taskId ?? task.task_id, errors: resultErrors });
1649
1941
  }
1650
1942
  }
1651
1943
  await writeJsonFile(auditResultsPath, passing);
@@ -1687,14 +1979,25 @@ async function cmdMergeAndIngest(argv) {
1687
1979
  }, null, 2));
1688
1980
  }
1689
1981
  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");
1982
+ const rawRunId = getFlag(argv, "--run-id");
1983
+ const runIdB64 = getFlag(argv, "--run-id-b64");
1984
+ const rawTaskId = getFlag(argv, "--task-id");
1985
+ const artifactsDirB64 = getFlag(argv, "--artifacts-dir-b64");
1986
+ const runId = rawRunId ?? (runIdB64 ? fromBase64Url(runIdB64) : undefined);
1987
+ const taskIdB64 = getFlag(argv, "--task-id-b64");
1988
+ const taskId = rawTaskId ?? (taskIdB64 ? fromBase64Url(taskIdB64) : undefined);
1989
+ const artifactsDir = artifactsDirB64
1990
+ ? resolve(fromBase64Url(artifactsDirB64))
1991
+ : getArtifactsDir(argv);
1992
+ if (!runId || !taskId) {
1993
+ throw new Error("validate-result requires --run-id and --task-id (or --run-id-b64/--task-id-b64)");
1994
+ }
1995
+ const runDir = join(artifactsDir, "runs", runId);
1996
+ const taskResultsDir = join(runDir, "task-results");
1997
+ const resultMap = await loadDispatchResultMap(runDir);
1998
+ const resultPath = resultMap?.entries.find((entry) => entry.task_id === taskId)?.result_path ??
1999
+ taskResultPath(taskResultsDir, taskId);
2000
+ const tasksPath = join(runDir, "pending-audit-tasks.json");
1698
2001
  let raw;
1699
2002
  try {
1700
2003
  raw = await readFile(resultPath, "utf8");
@@ -1986,12 +2289,15 @@ async function main(argv) {
1986
2289
  case "merge-and-ingest":
1987
2290
  await cmdMergeAndIngest(argv);
1988
2291
  return;
2292
+ case "submit-packet":
2293
+ await cmdSubmitPacket(argv);
2294
+ return;
1989
2295
  case "validate-result":
1990
2296
  await cmdValidateResult(argv);
1991
2297
  return;
1992
2298
  default:
1993
2299
  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");
2300
+ 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
2301
  process.exitCode = 1;
1996
2302
  }
1997
2303
  }
@@ -0,0 +1,32 @@
1
+ import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
2
+ import type { GraphBundle } from "../types/graph.js";
3
+ export type FileAnchorKind = "boundary" | "import" | "export" | "symbol" | "route" | "keyword" | "graph" | "analyzer_signal";
4
+ export interface FileAnchor {
5
+ kind: FileAnchorKind;
6
+ name: string;
7
+ line?: number;
8
+ detail?: string;
9
+ }
10
+ export interface FileAnchorSummary {
11
+ contract_version: "audit-code-file-anchors/v1alpha1";
12
+ path: string;
13
+ total_lines: number;
14
+ review_mode: "isolated_large_file";
15
+ scope_basis: string[];
16
+ anchors: FileAnchor[];
17
+ omitted_anchor_count: number;
18
+ counts: {
19
+ symbols: number;
20
+ routes: number;
21
+ keywords: number;
22
+ graph_edges: number;
23
+ analyzer_signals: number;
24
+ };
25
+ }
26
+ export declare function buildFileAnchorSummary(params: {
27
+ path: string;
28
+ content: string;
29
+ totalLines: number;
30
+ graphBundle?: GraphBundle;
31
+ externalAnalyzerResults?: ExternalAnalyzerResults;
32
+ }): FileAnchorSummary;