auditor-lambda 0.10.2 → 0.10.7

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.
Files changed (186) hide show
  1. package/audit-code-wrapper-build.mjs +198 -0
  2. package/audit-code-wrapper-install-hosts.mjs +1140 -0
  3. package/audit-code-wrapper-io.mjs +155 -0
  4. package/audit-code-wrapper-legacy.mjs +125 -0
  5. package/audit-code-wrapper-lib.mjs +17 -1801
  6. package/audit-code-wrapper-opencode.mjs +256 -0
  7. package/dispatch/merge-results.mjs +5 -3
  8. package/dispatch/validate-result.mjs +2 -2
  9. package/dist/adapters/coverageSummary.js +6 -2
  10. package/dist/adapters/normalizeExternal.js +16 -1
  11. package/dist/adapters/npmAudit.js +20 -9
  12. package/dist/adapters/semgrep.js +26 -1
  13. package/dist/cli/advanceAuditCommand.d.ts +1 -0
  14. package/dist/cli/advanceAuditCommand.js +95 -0
  15. package/dist/cli/args.js +1 -2
  16. package/dist/cli/auditStep.js +2 -2
  17. package/dist/cli/cleanup.d.ts +11 -1
  18. package/dist/cli/cleanup.js +25 -5
  19. package/dist/cli/cleanupCommand.d.ts +1 -0
  20. package/dist/cli/cleanupCommand.js +24 -0
  21. package/dist/cli/dispatch.d.ts +55 -31
  22. package/dist/cli/dispatch.js +298 -241
  23. package/dist/cli/dispatchStatusCommand.d.ts +1 -0
  24. package/dist/cli/dispatchStatusCommand.js +68 -0
  25. package/dist/cli/explainTaskCommand.d.ts +1 -0
  26. package/dist/cli/explainTaskCommand.js +33 -0
  27. package/dist/cli/importExternalAnalyzerCommand.d.ts +1 -0
  28. package/dist/cli/importExternalAnalyzerCommand.js +20 -0
  29. package/dist/cli/ingestResultsCommand.d.ts +1 -0
  30. package/dist/cli/ingestResultsCommand.js +34 -0
  31. package/dist/cli/intakeCommand.d.ts +1 -0
  32. package/dist/cli/intakeCommand.js +17 -0
  33. package/dist/cli/lineIndex.js +19 -12
  34. package/dist/cli/mergeAndIngestCommand.js +68 -26
  35. package/dist/cli/nextStepCommand.d.ts +139 -0
  36. package/dist/cli/nextStepCommand.js +281 -232
  37. package/dist/cli/planCommand.d.ts +1 -0
  38. package/dist/cli/planCommand.js +16 -0
  39. package/dist/cli/prepareDispatchCommand.d.ts +1 -0
  40. package/dist/cli/prepareDispatchCommand.js +25 -0
  41. package/dist/cli/quotaCommand.d.ts +1 -0
  42. package/dist/cli/quotaCommand.js +56 -0
  43. package/dist/cli/requeueCommand.d.ts +1 -0
  44. package/dist/cli/requeueCommand.js +10 -0
  45. package/dist/cli/runToCompletion.js +451 -412
  46. package/dist/cli/sampleRunCommand.d.ts +1 -0
  47. package/dist/cli/sampleRunCommand.js +93 -0
  48. package/dist/cli/statusCommand.js +1 -1
  49. package/dist/cli/steps.js +4 -1
  50. package/dist/cli/submitPacketCommand.js +16 -15
  51. package/dist/cli/synthesizeCommand.d.ts +1 -0
  52. package/dist/cli/synthesizeCommand.js +15 -0
  53. package/dist/cli/updateRuntimeValidationCommand.d.ts +1 -0
  54. package/dist/cli/updateRuntimeValidationCommand.js +16 -0
  55. package/dist/cli/validateCommand.d.ts +1 -0
  56. package/dist/cli/validateCommand.js +41 -0
  57. package/dist/cli/validateResultCommand.d.ts +1 -0
  58. package/dist/cli/validateResultCommand.js +63 -0
  59. package/dist/cli/validateResultsCommand.d.ts +1 -0
  60. package/dist/cli/validateResultsCommand.js +31 -0
  61. package/dist/cli/workerRunCommand.d.ts +15 -1
  62. package/dist/cli/workerRunCommand.js +40 -4
  63. package/dist/cli.d.ts +3 -2
  64. package/dist/cli.js +21 -628
  65. package/dist/coverage.js +7 -3
  66. package/dist/extractors/analyzers/css.js +2 -2
  67. package/dist/extractors/analyzers/html.js +2 -2
  68. package/dist/extractors/analyzers/python.js +2 -2
  69. package/dist/extractors/analyzers/registry.js +17 -36
  70. package/dist/extractors/analyzers/treeSitter.d.ts +10 -1
  71. package/dist/extractors/analyzers/treeSitter.js +28 -6
  72. package/dist/extractors/analyzers/typescript.js +104 -85
  73. package/dist/extractors/browserExtension.js +4 -1
  74. package/dist/extractors/designAssessment.js +21 -21
  75. package/dist/extractors/fsIntake.js +34 -10
  76. package/dist/extractors/graph.js +17 -7
  77. package/dist/extractors/graphManifestEdges/cargo.d.ts +4 -0
  78. package/dist/extractors/graphManifestEdges/cargo.js +107 -0
  79. package/dist/extractors/graphManifestEdges/go.d.ts +5 -0
  80. package/dist/extractors/graphManifestEdges/go.js +151 -0
  81. package/dist/extractors/graphManifestEdges/index.d.ts +8 -0
  82. package/dist/extractors/graphManifestEdges/index.js +11 -0
  83. package/dist/extractors/graphManifestEdges/jsonc.d.ts +3 -0
  84. package/dist/extractors/graphManifestEdges/jsonc.js +97 -0
  85. package/dist/extractors/graphManifestEdges/maven.d.ts +3 -0
  86. package/dist/extractors/graphManifestEdges/maven.js +73 -0
  87. package/dist/extractors/graphManifestEdges/packageJson.d.ts +19 -0
  88. package/dist/extractors/graphManifestEdges/packageJson.js +204 -0
  89. package/dist/extractors/graphManifestEdges/pnpm.d.ts +2 -0
  90. package/dist/extractors/graphManifestEdges/pnpm.js +42 -0
  91. package/dist/extractors/graphManifestEdges/pyproject.d.ts +3 -0
  92. package/dist/extractors/graphManifestEdges/pyproject.js +83 -0
  93. package/dist/extractors/graphManifestEdges/toml.d.ts +4 -0
  94. package/dist/extractors/graphManifestEdges/toml.js +68 -0
  95. package/dist/extractors/graphManifestEdges/typescript.d.ts +3 -0
  96. package/dist/extractors/graphManifestEdges/typescript.js +56 -0
  97. package/dist/extractors/graphManifestEdges/workspace.d.ts +10 -0
  98. package/dist/extractors/graphManifestEdges/workspace.js +72 -0
  99. package/dist/extractors/graphManifestEdges/yaml.d.ts +3 -0
  100. package/dist/extractors/graphManifestEdges/yaml.js +59 -0
  101. package/dist/extractors/graphManifestEdges/yamlPaths.d.ts +4 -0
  102. package/dist/extractors/graphManifestEdges/yamlPaths.js +89 -0
  103. package/dist/extractors/graphPythonImports.js +4 -20
  104. package/dist/extractors/pathPatterns.js +3 -13
  105. package/dist/io/artifacts.d.ts +1 -1
  106. package/dist/io/artifacts.js +4 -1
  107. package/dist/io/runArtifacts.d.ts +8 -2
  108. package/dist/io/runArtifacts.js +103 -69
  109. package/dist/io/toolingManifest.js +2 -1
  110. package/dist/orchestrator/advance.js +36 -0
  111. package/dist/orchestrator/artifactFreshness.d.ts +1 -1
  112. package/dist/orchestrator/artifactFreshness.js +1 -1
  113. package/dist/orchestrator/artifactMetadata.js +5 -5
  114. package/dist/orchestrator/auditTaskUtils.d.ts +4 -0
  115. package/dist/orchestrator/auditTaskUtils.js +8 -12
  116. package/dist/orchestrator/autoFixExecutor.js +40 -26
  117. package/dist/orchestrator/dependencyMap.js +1 -1
  118. package/dist/orchestrator/executorResult.d.ts +33 -0
  119. package/dist/orchestrator/executors.d.ts +7 -0
  120. package/dist/orchestrator/executors.js +24 -0
  121. package/dist/orchestrator/fileAnchors.js +42 -29
  122. package/dist/orchestrator/fileIntegrity.js +6 -1
  123. package/dist/orchestrator/flowCoverage.js +1 -2
  124. package/dist/orchestrator/flowPlanning.js +8 -4
  125. package/dist/orchestrator/graphEnrichmentExecutor.js +67 -45
  126. package/dist/orchestrator/ingestionExecutors.js +9 -1
  127. package/dist/orchestrator/intakeExecutors.d.ts +0 -4
  128. package/dist/orchestrator/intakeExecutors.js +24 -14
  129. package/dist/orchestrator/localCommands.d.ts +1 -0
  130. package/dist/orchestrator/localCommands.js +10 -17
  131. package/dist/orchestrator/nextStep.js +3 -1
  132. package/dist/orchestrator/requeueCommand.js +4 -0
  133. package/dist/orchestrator/reviewPacketGraph.js +50 -18
  134. package/dist/orchestrator/reviewPackets.js +10 -8
  135. package/dist/orchestrator/runtimeCommand.js +35 -7
  136. package/dist/orchestrator/runtimeValidationUpdate.js +6 -0
  137. package/dist/orchestrator/selectiveDeepening/highRiskClean.js +3 -2
  138. package/dist/orchestrator/selectiveDeepening/lensVerification.js +44 -18
  139. package/dist/orchestrator/staleness.js +3 -3
  140. package/dist/orchestrator/state.js +1 -1
  141. package/dist/orchestrator/syntaxResolutionExecutor.js +17 -24
  142. package/dist/orchestrator/synthesisExecutors.js +1 -0
  143. package/dist/orchestrator/taskBuilder.js +5 -4
  144. package/dist/providers/claudeCodeProvider.js +4 -1
  145. package/dist/providers/opencodeProvider.js +4 -1
  146. package/dist/quota/discoveredLimits.js +3 -3
  147. package/dist/quota/headerExtraction.js +5 -2
  148. package/dist/quota/headerExtractors/claudeCodeHeaderExtractor.js +3 -0
  149. package/dist/quota/headerExtractors/index.js +3 -3
  150. package/dist/quota/index.d.ts +3 -1
  151. package/dist/quota/index.js +3 -0
  152. package/dist/reporting/findingIdentity.d.ts +21 -0
  153. package/dist/reporting/findingIdentity.js +72 -0
  154. package/dist/reporting/findingRanks.d.ts +3 -0
  155. package/dist/reporting/findingRanks.js +24 -0
  156. package/dist/reporting/mergeFindings.js +1 -24
  157. package/dist/reporting/synthesis.d.ts +3 -1
  158. package/dist/reporting/synthesis.js +36 -7
  159. package/dist/reporting/synthesisNarrativePrompt.js +3 -0
  160. package/dist/reporting/workBlocks.js +1 -14
  161. package/dist/supervisor/operatorHandoff.js +2 -6
  162. package/dist/supervisor/runLedger.js +30 -41
  163. package/dist/types/activeDispatch.d.ts +31 -0
  164. package/dist/types/activeDispatch.js +2 -0
  165. package/dist/types.d.ts +21 -4
  166. package/dist/types.js +24 -16
  167. package/dist/validation/artifacts.js +3 -0
  168. package/dist/validation/auditResults.js +8 -2
  169. package/package.json +2 -2
  170. package/schemas/audit_findings.schema.json +5 -1
  171. package/schemas/audit_plan_metrics.schema.json +1 -1
  172. package/schemas/audit_result.schema.json +5 -6
  173. package/schemas/audit_task.schema.json +1 -4
  174. package/schemas/blind_spot_register.schema.json +1 -1
  175. package/schemas/coverage_matrix.schema.json +2 -8
  176. package/schemas/finding.schema.json +3 -17
  177. package/schemas/flow_coverage.schema.json +2 -8
  178. package/schemas/graph_bundle.schema.json +31 -0
  179. package/schemas/lens.schema.json +7 -0
  180. package/schemas/review_packets.schema.json +6 -17
  181. package/schemas/step_contract.schema.json +8 -2
  182. package/schemas/unit_manifest.schema.json +1 -4
  183. package/scripts/postinstall.mjs +3 -1
  184. package/skills/audit-code/audit-code.prompt.md +2 -3
  185. package/dist/extractors/graphManifestEdges.d.ts +0 -12
  186. package/dist/extractors/graphManifestEdges.js +0 -1135
@@ -131,6 +131,9 @@ function addTaskBlock(params, context) {
131
131
  });
132
132
  }
133
133
  }
134
+ function withSignalTag(baseTags, hasExternalSignal) {
135
+ return hasExternalSignal ? [...baseTags, "external_analyzer_signal"] : baseTags;
136
+ }
134
137
  function buildCoverageIndex(coverageMatrix) {
135
138
  return new Map(coverageMatrix.files.map((file) => [file.path, file]));
136
139
  }
@@ -206,9 +209,7 @@ export function buildChunkedAuditTasks(coverageMatrix, unitLineIndex, options =
206
209
  lens: block.lens,
207
210
  filePaths: block.file_paths,
208
211
  priority: taskPriority(hasExternalSignal, block.lens, true),
209
- tags: hasExternalSignal
210
- ? ["critical_flow", `critical_flow:${block.flow_id}`, "external_analyzer_signal"]
211
- : ["critical_flow", `critical_flow:${block.flow_id}`],
212
+ tags: withSignalTag(["critical_flow", `critical_flow:${block.flow_id}`], hasExternalSignal),
212
213
  rationale: (filePaths, splitKind) => splitKind === "large_file"
213
214
  ? `Audit ${filePaths[0]} (large file from critical flow ${block.flow_id}) under the ${block.lens} lens.${hasExternalSignal ? " External analyzer signals raise priority." : ""}`
214
215
  : splitKind === "budget"
@@ -259,7 +260,7 @@ export function buildChunkedAuditTasks(coverageMatrix, unitLineIndex, options =
259
260
  lens: block.lens,
260
261
  filePaths: block.filePaths,
261
262
  priority: taskPriority(hasExternalSignal, block.lens),
262
- tags: hasExternalSignal ? ["external_analyzer_signal"] : [],
263
+ tags: withSignalTag([], hasExternalSignal),
263
264
  rationale: (filePaths, splitKind) => splitKind === "large_file"
264
265
  ? `Audit ${filePaths[0]} (large file split from ${block.unitId}) under the ${block.lens} lens.${hasExternalSignal ? " External analyzer signals raise priority." : ""}`
265
266
  : splitKind === "budget"
@@ -29,9 +29,12 @@ export class ClaudeCodeProvider {
29
29
  ? ["--dangerously-skip-permissions"]
30
30
  : []),
31
31
  ];
32
- return await this.launchCommand(command, args, applyWorkerTaskLaunchSettings(input, task), undefined, {
32
+ process.stderr.write(JSON.stringify({ event: "provider_launch", provider: this.name, runId: input.runId, obligationId: input.obligationId, promptPath: input.promptPath, taskPath: input.taskPath }) + "\n");
33
+ const result = await this.launchCommand(command, args, applyWorkerTaskLaunchSettings(input, task), undefined, {
33
34
  opentoken: this.opentoken.enabled,
34
35
  opentokenCommand: this.opentoken.command,
35
36
  });
37
+ process.stderr.write(JSON.stringify({ event: "provider_done", provider: this.name, runId: input.runId, obligationId: input.obligationId, accepted: result.accepted, exitCode: result.exitCode ?? null }) + "\n");
38
+ return result;
36
39
  }
37
40
  }
@@ -16,9 +16,12 @@ export class OpenCodeProvider {
16
16
  // On Windows the `opencode` launcher is a `.cmd` shim that `spawn` cannot
17
17
  // run without a shell; resolve it through cmd.exe (no-op on other OSes).
18
18
  const { command, args } = resolveOpenCodeSpawnCommand(baseCommand, baseArgs);
19
- return await spawnLoggedCommand(command, args, applyWorkerTaskLaunchSettings(input, task), undefined, {
19
+ process.stderr.write(JSON.stringify({ event: "provider_launch", provider: this.name, runId: input.runId, obligationId: input.obligationId, promptPath: input.promptPath, taskPath: input.taskPath }) + "\n");
20
+ const result = await spawnLoggedCommand(command, args, applyWorkerTaskLaunchSettings(input, task), undefined, {
20
21
  opentoken: this.opentoken.enabled,
21
22
  opentokenCommand: this.opentoken.command,
22
23
  });
24
+ process.stderr.write(JSON.stringify({ event: "provider_done", provider: this.name, runId: input.runId, obligationId: input.obligationId, accepted: result.accepted, exitCode: result.exitCode ?? null }) + "\n");
25
+ return result;
23
26
  }
24
27
  }
@@ -1,8 +1,8 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
- import { dirname } from "node:path";
2
+ import { dirname, join } from "node:path";
3
3
  import { getQuotaStatePath } from "@audit-tools/shared";
4
4
  function getCachePath() {
5
- return getQuotaStatePath().replace(/quota-state\.json$/, "discovered-limits.json");
5
+ return join(dirname(getQuotaStatePath()), "discovered-limits.json");
6
6
  }
7
7
  export async function readDiscoveredLimitsCache() {
8
8
  try {
@@ -17,7 +17,7 @@ export async function readDiscoveredLimitsCache() {
17
17
  }
18
18
  catch (error) {
19
19
  if (error.code !== "ENOENT") {
20
- process.stderr.write(`[quota] ignoring unreadable discovered-limits cache: ${error instanceof Error ? error.message : String(error)}\n`);
20
+ process.stderr.write(`[quota] ignoring unreadable discovered-limits cache (${getCachePath()}): ${error instanceof Error ? error.message : String(error)}\n`);
21
21
  }
22
22
  }
23
23
  return { version: 1, entries: {} };
@@ -30,7 +30,7 @@ function parseResetValue(value) {
30
30
  }
31
31
  function parseNumericValue(value) {
32
32
  const n = parseInt(value, 10);
33
- return Number.isFinite(n) && n > 0 ? n : null;
33
+ return Number.isFinite(n) && n >= 0 ? n : null;
34
34
  }
35
35
  export function extractRateLimitHeaders(text) {
36
36
  const result = {
@@ -68,6 +68,9 @@ export function extractRateLimitHeaders(text) {
68
68
  if (jsonResult)
69
69
  return jsonResult;
70
70
  }
71
+ if (!found && text.trim().length > 0) {
72
+ process.stderr.write("[quota] header extraction: no rate-limit data found in non-empty stderr text (possible provider format change)\n");
73
+ }
71
74
  return found ? result : null;
72
75
  }
73
76
  function extractFromJson(text) {
@@ -108,7 +111,7 @@ function extractFromHeaderObject(headers) {
108
111
  const val = headers[key] ?? headers[key.toLowerCase()];
109
112
  if (val != null) {
110
113
  const n = typeof val === "number" ? val : parseInt(String(val), 10);
111
- if (Number.isFinite(n) && n > 0)
114
+ if (Number.isFinite(n) && n >= 0)
112
115
  return n;
113
116
  }
114
117
  }
@@ -23,6 +23,9 @@ export class ClaudeCodeHeaderExtractor {
23
23
  return extractRateLimitHeaders(candidates.join("\n"));
24
24
  }
25
25
  // Fall back to scanning the full text for raw header lines
26
+ if (stderr.trim().length > 0) {
27
+ process.stderr.write("[quota] claude-code header extractor: no structured JSON lines with headers/response_headers found in non-empty stderr; falling back to raw-text scan\n");
28
+ }
26
29
  return extractRateLimitHeaders(stderr);
27
30
  }
28
31
  }
@@ -3,10 +3,10 @@ export { ClaudeCodeHeaderExtractor } from "./claudeCodeHeaderExtractor.js";
3
3
  import { GenericHeaderExtractor } from "./genericHeaderExtractor.js";
4
4
  import { ClaudeCodeHeaderExtractor } from "./claudeCodeHeaderExtractor.js";
5
5
  const PROVIDER_EXTRACTORS = {
6
- "claude-code": () => new ClaudeCodeHeaderExtractor(),
6
+ "claude-code": new ClaudeCodeHeaderExtractor(),
7
7
  };
8
8
  const genericExtractor = new GenericHeaderExtractor();
9
9
  export function getHeaderExtractorForProvider(providerName) {
10
- const factory = PROVIDER_EXTRACTORS[providerName];
11
- return factory ? factory() : genericExtractor;
10
+ const extractor = PROVIDER_EXTRACTORS[providerName];
11
+ return extractor ?? genericExtractor;
12
12
  }
@@ -12,8 +12,10 @@ export { extractRateLimitHeaders } from "./headerExtraction.js";
12
12
  export type { ExtractedRateLimits } from "./headerExtraction.js";
13
13
  export type { HeaderExtractor } from "./headerExtractors/index.js";
14
14
  export { GenericHeaderExtractor, ClaudeCodeHeaderExtractor, getHeaderExtractorForProvider } from "./headerExtractors/index.js";
15
+ export declare const DISPATCH_QUOTA_V1ALPHA1: "audit-code-dispatch-quota/v1alpha1";
16
+ export declare const DISPATCH_QUOTA_V1ALPHA2: "audit-code-dispatch-quota/v1alpha2";
15
17
  export interface DispatchQuota {
16
- contract_version: "audit-code-dispatch-quota/v1alpha1" | "audit-code-dispatch-quota/v1alpha2";
18
+ contract_version: typeof DISPATCH_QUOTA_V1ALPHA1 | typeof DISPATCH_QUOTA_V1ALPHA2;
17
19
  run_id: string;
18
20
  model: string | null;
19
21
  resolved_limits: _ResolvedLimits;
@@ -13,3 +13,6 @@ export { detectHostActiveSubagentLimit, resolveHostActiveSubagentLimit, } from "
13
13
  export { lookupDiscoveredLimits, updateDiscoveredLimits, mergeDiscoveredLimits, readDiscoveredLimitsCache, writeDiscoveredLimitsCache, } from "./discoveredLimits.js";
14
14
  export { extractRateLimitHeaders } from "./headerExtraction.js";
15
15
  export { GenericHeaderExtractor, ClaudeCodeHeaderExtractor, getHeaderExtractorForProvider } from "./headerExtractors/index.js";
16
+ // Auditor-only type (not in shared)
17
+ export const DISPATCH_QUOTA_V1ALPHA1 = "audit-code-dispatch-quota/v1alpha1";
18
+ export const DISPATCH_QUOTA_V1ALPHA2 = "audit-code-dispatch-quota/v1alpha2";
@@ -0,0 +1,21 @@
1
+ import type { Finding } from "../types.js";
2
+ /**
3
+ * Re-key finalized findings with globally-unique, content-derived ids at the
4
+ * synthesis boundary.
5
+ *
6
+ * Worker packets assign locally-scoped ids (e.g. `MNT-001`) that collide across
7
+ * packets once merged, which breaks `audit-findings.json` as a machine contract:
8
+ * `buildWorkBlocks` keys its union-find on `id` (so colliding ids fuse unrelated
9
+ * findings into one block), and `work_blocks.finding_ids` / theme `finding_ids` /
10
+ * the remediator's per-finding addressing can no longer resolve a single finding.
11
+ *
12
+ * The id is `<LENS_PREFIX>-<sha256(content)[:8]>`, deterministic and stable so a
13
+ * re-synthesis of the same findings produces the same ids. A vanishingly rare
14
+ * hash collision between two *distinct* findings is broken deterministically with
15
+ * a numeric suffix (findings arrive in mergeFindings()' stable order).
16
+ *
17
+ * `related_findings`, when present, referenced the old colliding ids and cannot
18
+ * be remapped unambiguously, so it is dropped rather than left dangling. (It is
19
+ * unpopulated by every current extractor.)
20
+ */
21
+ export declare function assignStableFindingIds(findings: Finding[]): Finding[];
@@ -0,0 +1,72 @@
1
+ import { createHash } from "node:crypto";
2
+ // Stable lens -> id prefix. The lens is the canonical addressing axis, so the
3
+ // prefix always matches it (no convention drift) and the content hash that
4
+ // follows guarantees global uniqueness.
5
+ const LENS_ID_PREFIX = {
6
+ correctness: "COR",
7
+ architecture: "ARC",
8
+ maintainability: "MNT",
9
+ security: "SEC",
10
+ reliability: "REL",
11
+ performance: "PRF",
12
+ data_integrity: "DAT",
13
+ tests: "TST",
14
+ operability: "OPR",
15
+ config_deployment: "CFG",
16
+ observability: "OBS",
17
+ };
18
+ /**
19
+ * A stable signature of a finding's identity-bearing content. The same logical
20
+ * finding yields the same signature across runs (so its id is reproducible),
21
+ * while two distinct findings — which only coexist after surviving merge and
22
+ * dedup with different content — yield different signatures.
23
+ */
24
+ function contentSignature(finding) {
25
+ const files = finding.affected_files
26
+ .map((file) => `${file.path}:${file.line_start ?? ""}:${file.line_end ?? ""}:${file.symbol ?? ""}`)
27
+ .sort()
28
+ .join(",");
29
+ return [
30
+ finding.lens.trim().toLowerCase(),
31
+ finding.category.trim().toLowerCase(),
32
+ finding.title.trim().toLowerCase(),
33
+ files,
34
+ ].join("|");
35
+ }
36
+ /**
37
+ * Re-key finalized findings with globally-unique, content-derived ids at the
38
+ * synthesis boundary.
39
+ *
40
+ * Worker packets assign locally-scoped ids (e.g. `MNT-001`) that collide across
41
+ * packets once merged, which breaks `audit-findings.json` as a machine contract:
42
+ * `buildWorkBlocks` keys its union-find on `id` (so colliding ids fuse unrelated
43
+ * findings into one block), and `work_blocks.finding_ids` / theme `finding_ids` /
44
+ * the remediator's per-finding addressing can no longer resolve a single finding.
45
+ *
46
+ * The id is `<LENS_PREFIX>-<sha256(content)[:8]>`, deterministic and stable so a
47
+ * re-synthesis of the same findings produces the same ids. A vanishingly rare
48
+ * hash collision between two *distinct* findings is broken deterministically with
49
+ * a numeric suffix (findings arrive in mergeFindings()' stable order).
50
+ *
51
+ * `related_findings`, when present, referenced the old colliding ids and cannot
52
+ * be remapped unambiguously, so it is dropped rather than left dangling. (It is
53
+ * unpopulated by every current extractor.)
54
+ */
55
+ export function assignStableFindingIds(findings) {
56
+ const used = new Set();
57
+ return findings.map((finding) => {
58
+ const prefix = LENS_ID_PREFIX[finding.lens.trim().toLowerCase()] ?? "FND";
59
+ const hash = createHash("sha256")
60
+ .update(contentSignature(finding))
61
+ .digest("hex")
62
+ .slice(0, 8);
63
+ let id = `${prefix}-${hash}`;
64
+ for (let n = 2; used.has(id); n++) {
65
+ id = `${prefix}-${hash}-${n}`;
66
+ }
67
+ used.add(id);
68
+ const reKeyed = { ...finding, id };
69
+ delete reKeyed.related_findings;
70
+ return reKeyed;
71
+ });
72
+ }
@@ -0,0 +1,3 @@
1
+ import type { Finding } from "../types.js";
2
+ export declare function severityRank(severity: Finding["severity"]): number;
3
+ export declare function confidenceRank(confidence: Finding["confidence"]): number;
@@ -0,0 +1,24 @@
1
+ export function severityRank(severity) {
2
+ switch (severity) {
3
+ case "critical":
4
+ return 5;
5
+ case "high":
6
+ return 4;
7
+ case "medium":
8
+ return 3;
9
+ case "low":
10
+ return 2;
11
+ case "info":
12
+ return 1;
13
+ }
14
+ }
15
+ export function confidenceRank(confidence) {
16
+ switch (confidence) {
17
+ case "high":
18
+ return 3;
19
+ case "medium":
20
+ return 2;
21
+ case "low":
22
+ return 1;
23
+ }
24
+ }
@@ -1,3 +1,4 @@
1
+ import { severityRank, confidenceRank } from "./findingRanks.js";
1
2
  function normalizeText(value) {
2
3
  return (value ?? "").trim().toLowerCase();
3
4
  }
@@ -43,30 +44,6 @@ function findingKey(finding) {
43
44
  String(finding.affected_files[0]?.line_end ?? ""),
44
45
  ].join("|");
45
46
  }
46
- function severityRank(severity) {
47
- switch (severity) {
48
- case "critical":
49
- return 5;
50
- case "high":
51
- return 4;
52
- case "medium":
53
- return 3;
54
- case "low":
55
- return 2;
56
- case "info":
57
- return 1;
58
- }
59
- }
60
- function confidenceRank(confidence) {
61
- switch (confidence) {
62
- case "high":
63
- return 3;
64
- case "medium":
65
- return 2;
66
- case "low":
67
- return 1;
68
- }
69
- }
70
47
  function runtimeSummary(report) {
71
48
  if (!report) {
72
49
  return [];
@@ -3,7 +3,7 @@ import type { AuditScopeManifest } from "../types/auditScope.js";
3
3
  import type { DesignAssessment } from "../types/designAssessment.js";
4
4
  import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
5
5
  import type { AuditFindingsReport, CriticalFlowManifest, Finding as SharedFinding, FindingTheme, GraphBundle, SynthesisNarrative } from "@audit-tools/shared";
6
- import type { RuntimeValidationReport } from "../types/runtimeValidation.js";
6
+ import type { RuntimeValidationReport, RuntimeValidationTaskManifest } from "../types/runtimeValidation.js";
7
7
  import { type WorkBlock } from "./workBlocks.js";
8
8
  /** Contract version stamped onto the canonical `audit-findings.json`. */
9
9
  export declare const AUDIT_FINDINGS_CONTRACT_VERSION = "audit-tools/audit-findings/v1";
@@ -25,6 +25,7 @@ export interface AuditReportSummary {
25
25
  finding_count: number;
26
26
  work_block_count: number;
27
27
  severity_breakdown: Record<string, number>;
28
+ lens_breakdown?: Record<string, number>;
28
29
  audited_file_count: number;
29
30
  excluded_file_count: number;
30
31
  runtime_validation_status_breakdown: Record<string, number>;
@@ -49,6 +50,7 @@ export declare function buildAuditReportModel(params: {
49
50
  criticalFlows?: CriticalFlowManifest;
50
51
  coverageMatrix?: CoverageMatrix;
51
52
  runtimeValidationReport?: RuntimeValidationReport;
53
+ runtimeValidationTaskManifest?: RuntimeValidationTaskManifest;
52
54
  externalAnalyzerResults?: ExternalAnalyzerResults;
53
55
  designAssessment?: DesignAssessment;
54
56
  }): AuditReportModel;
@@ -1,6 +1,7 @@
1
1
  import { AUDITOR_REPORT_MARKER } from "@audit-tools/shared";
2
2
  import { buildWorkBlocks } from "./workBlocks.js";
3
3
  import { mergeFindings } from "./mergeFindings.js";
4
+ import { assignStableFindingIds } from "./findingIdentity.js";
4
5
  /** Contract version stamped onto the canonical `audit-findings.json`. */
5
6
  export const AUDIT_FINDINGS_CONTRACT_VERSION = "audit-tools/audit-findings/v1";
6
7
  function countBy(items, selectKey) {
@@ -17,8 +18,18 @@ function countBy(items, selectKey) {
17
18
  function severityBreakdown(findings) {
18
19
  return countBy(findings, (finding) => finding.severity);
19
20
  }
20
- function runtimeStatusBreakdown(report) {
21
- return countBy(report?.results ?? [], (result) => result.status);
21
+ function lensBreakdown(findings) {
22
+ return countBy(findings, (finding) => finding.lens);
23
+ }
24
+ function runtimeStatusBreakdown(report, taskManifest) {
25
+ const breakdown = countBy(report?.results ?? [], (result) => result.status);
26
+ const resultTaskIds = new Set((report?.results ?? []).map((result) => result.task_id));
27
+ for (const task of taskManifest?.tasks ?? []) {
28
+ if (!resultTaskIds.has(task.id)) {
29
+ breakdown.pending = (breakdown.pending ?? 0) + 1;
30
+ }
31
+ }
32
+ return breakdown;
22
33
  }
23
34
  function coverageSummary(coverage) {
24
35
  const files = coverage?.files ?? [];
@@ -36,8 +47,19 @@ function formatSeverityList(summary) {
36
47
  .map((severity) => `${severity}: ${summary[severity]}`);
37
48
  return parts.length > 0 ? parts.join(", ") : "none";
38
49
  }
50
+ function formatCountList(summary) {
51
+ const parts = Object.entries(summary)
52
+ .filter(([, count]) => count > 0)
53
+ .sort(([left], [right]) => left.localeCompare(right))
54
+ .map(([key, count]) => `${key}: ${count}`);
55
+ return parts.length > 0 ? parts.join(", ") : "none";
56
+ }
39
57
  export function buildAuditReportModel(params) {
40
- const findings = mergeFindings(params.results, params.runtimeValidationReport, params.externalAnalyzerResults, params.designAssessment);
58
+ // Re-key the finalized findings with globally-unique, content-derived ids
59
+ // before anything addresses them by id. buildWorkBlocks keys its union-find on
60
+ // finding.id, so the locally-scoped, collision-prone ids worker packets emit
61
+ // must be replaced here or unrelated findings fuse into one block.
62
+ const findings = assignStableFindingIds(mergeFindings(params.results, params.runtimeValidationReport, params.externalAnalyzerResults, params.designAssessment));
41
63
  const workBlocks = buildWorkBlocks({
42
64
  findings,
43
65
  unitManifest: params.unitManifest,
@@ -45,19 +67,22 @@ export function buildAuditReportModel(params) {
45
67
  criticalFlows: params.criticalFlows,
46
68
  });
47
69
  const coverage = coverageSummary(params.coverageMatrix);
48
- return {
70
+ const model = {
49
71
  summary: {
50
72
  finding_count: findings.length,
51
73
  work_block_count: workBlocks.length,
52
74
  severity_breakdown: severityBreakdown(findings),
75
+ lens_breakdown: lensBreakdown(findings),
53
76
  audited_file_count: coverage.audited_file_count,
54
77
  excluded_file_count: coverage.excluded_file_count,
55
78
  budget_deferred_task_count: coverage.budget_deferred_task_count,
56
- runtime_validation_status_breakdown: runtimeStatusBreakdown(params.runtimeValidationReport),
79
+ runtime_validation_status_breakdown: runtimeStatusBreakdown(params.runtimeValidationReport, params.runtimeValidationTaskManifest),
57
80
  },
58
81
  findings,
59
82
  work_blocks: workBlocks,
60
83
  };
84
+ console.error(JSON.stringify({ tag: 'synthesis_complete', finding_count: findings.length, work_block_count: workBlocks.length, severity_breakdown: severityBreakdown(findings), audited_file_count: coverage.audited_file_count, excluded_file_count: coverage.excluded_file_count, budget_deferred_task_count: coverage.budget_deferred_task_count }));
85
+ return model;
61
86
  }
62
87
  /**
63
88
  * Wrap the deterministic report model in the canonical `audit-findings.json`
@@ -65,12 +90,14 @@ export function buildAuditReportModel(params) {
65
90
  * are absent here; they are layered on later by {@link applyNarrative}.
66
91
  */
67
92
  export function buildAuditFindingsReport(model) {
68
- return {
93
+ const report = {
69
94
  contract_version: AUDIT_FINDINGS_CONTRACT_VERSION,
70
95
  summary: { ...model.summary },
71
96
  findings: model.findings,
72
97
  work_blocks: model.work_blocks,
73
98
  };
99
+ console.error(JSON.stringify({ tag: 'audit_findings_report_built', contract_version: AUDIT_FINDINGS_CONTRACT_VERSION, finding_count: model.findings.length, work_block_count: model.work_blocks.length }));
100
+ return report;
74
101
  }
75
102
  /**
76
103
  * Merge an LLM synthesis narrative into the canonical findings report: keep only
@@ -120,7 +147,9 @@ export function renderAuditReportMarkdown(report, options = {}) {
120
147
  if (report.executive_summary && report.executive_summary.trim().length > 0) {
121
148
  lines.push("## Executive Summary", "", report.executive_summary.trim(), "");
122
149
  }
123
- lines.push("## Summary", "", `- Findings: ${report.summary.finding_count}`, `- Work blocks: ${report.summary.work_block_count}`, `- Severity breakdown: ${formatSeverityList(report.summary.severity_breakdown)}`, `- Fully audited files: ${report.summary.audited_file_count}`, `- Excluded non-auditable files: ${report.summary.excluded_file_count}`, ...((report.summary.budget_deferred_task_count ?? 0) > 0
150
+ lines.push("## Summary", "", `- Findings: ${report.summary.finding_count}`, `- Work blocks: ${report.summary.work_block_count}`, `- Severity breakdown: ${formatSeverityList(report.summary.severity_breakdown)}`, ...(report.summary.lens_breakdown && Object.keys(report.summary.lens_breakdown).length > 0
151
+ ? [`- Lens breakdown: ${formatCountList(report.summary.lens_breakdown)}`]
152
+ : []), `- Fully audited files: ${report.summary.audited_file_count}`, `- Excluded non-auditable files: ${report.summary.excluded_file_count}`, ...((report.summary.budget_deferred_task_count ?? 0) > 0
124
153
  ? [
125
154
  `- Not audited (budget): ${report.summary.budget_deferred_task_count} task(s) skipped by packet budget cap`,
126
155
  ]
@@ -17,6 +17,9 @@ export function renderSynthesisNarrativePrompt(report) {
17
17
  const overflowNote = findings.length > MAX_RENDERED_FINDINGS
18
18
  ? [` ... and ${findings.length - MAX_RENDERED_FINDINGS} more findings (see audit-findings.json).`]
19
19
  : [];
20
+ if (findings.length > MAX_RENDERED_FINDINGS) {
21
+ console.warn(`[audit-code] synthesisNarrative: truncated findings list to ${MAX_RENDERED_FINDINGS} of ${findings.length} total — remaining findings omitted from narrative prompt (see audit-findings.json)`);
22
+ }
20
23
  return [
21
24
  "# Synthesis narrative",
22
25
  "",
@@ -1,17 +1,4 @@
1
- function severityRank(severity) {
2
- switch (severity) {
3
- case "critical":
4
- return 5;
5
- case "high":
6
- return 4;
7
- case "medium":
8
- return 3;
9
- case "low":
10
- return 2;
11
- case "info":
12
- return 1;
13
- }
14
- }
1
+ import { severityRank } from "./findingRanks.js";
15
2
  function buildFileUnitMap(unitManifest) {
16
3
  const map = new Map();
17
4
  for (const unit of unitManifest?.units ?? []) {
@@ -35,11 +35,8 @@ function quoteShellPath(filePath) {
35
35
  // double-quote wrapping plus escaping embedded double quotes.
36
36
  return `"${filePath.replace(/"/g, '\\"')}"`;
37
37
  }
38
- function quoteShellArg(value) {
39
- return quoteShellPath(value);
40
- }
41
38
  function renderShellCommand(argv) {
42
- return argv.map((item) => quoteShellArg(item)).join(" ");
39
+ return argv.map((item) => quoteShellPath(item)).join(" ");
43
40
  }
44
41
  function buildPendingObligations(state) {
45
42
  return state.obligations
@@ -274,7 +271,6 @@ export async function writeAuditCodeHandoffArtifacts(handoff) {
274
271
  await writeFile(handoff.artifact_paths.operator_handoff_markdown, renderMarkdown(handoff), "utf8");
275
272
  }
276
273
  catch (error) {
277
- const message = error instanceof Error ? error.message : String(error);
278
- throw new Error(`Failed to write operator handoff artifacts: ${message}`);
274
+ throw new Error(`Failed to write operator handoff artifacts: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
279
275
  }
280
276
  }
@@ -1,16 +1,36 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { mkdir, open, rename, rm } from "node:fs/promises";
2
+ import { open, mkdir, rename, rm } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { RUN_LEDGER_STATUSES, } from "@audit-tools/shared";
5
- import { isFileMissingError, readJsonFile, writeJsonFile } from "@audit-tools/shared";
5
+ import { isFileMissingError, readJsonFile, writeJsonFile, withFileLock, withFsRetry } from "@audit-tools/shared";
6
6
  const RUN_LEDGER_FILENAME = "run-ledger.json";
7
7
  const RUN_LEDGER_LOCK_FILENAME = "run-ledger.lock";
8
- const LOCK_RETRY_DELAY_MS = 20;
9
- const LOCK_RETRY_LIMIT = 100;
10
8
  const VALID_RUN_LEDGER_STATUSES = new Set(RUN_LEDGER_STATUSES);
11
9
  function ledgerPath(artifactsDir) {
12
10
  return join(artifactsDir, RUN_LEDGER_FILENAME);
13
11
  }
12
+ /**
13
+ * Wrap withFileLock for the run ledger, emitting a stderr message on the first
14
+ * contention event so operators can observe prolonged lock waits before the
15
+ * full timeout expires.
16
+ */
17
+ async function withLedgerLock(lockPath, fn) {
18
+ // Probe for an immediate lock acquisition; if the lock file already exists,
19
+ // log contention before delegating to withFileLock for the full retry loop.
20
+ try {
21
+ const fd = await open(lockPath, "wx");
22
+ await fd.close();
23
+ // Lock acquired on the first attempt — no contention; release and let
24
+ // withFileLock manage the full acquire + release lifecycle.
25
+ await rm(lockPath, { force: true });
26
+ }
27
+ catch (err) {
28
+ if (err.code === "EEXIST") {
29
+ process.stderr.write(`[audit-code] runLedger: lock contention detected on ${lockPath}, waiting...\n`);
30
+ }
31
+ }
32
+ return withFileLock(lockPath, fn);
33
+ }
14
34
  function ledgerLockPath(artifactsDir) {
15
35
  return join(artifactsDir, RUN_LEDGER_LOCK_FILENAME);
16
36
  }
@@ -20,12 +40,6 @@ function buildTempLedgerPath(artifactsDir) {
20
40
  function isRecord(value) {
21
41
  return typeof value === "object" && value !== null && !Array.isArray(value);
22
42
  }
23
- function isFileExistsError(error) {
24
- return (typeof error === "object" &&
25
- error !== null &&
26
- "code" in error &&
27
- error.code === "EEXIST");
28
- }
29
43
  function assertRunLedgerEntry(value, fieldPath) {
30
44
  if (!isRecord(value)) {
31
45
  throw new Error(`Invalid run ledger in ${fieldPath}: expected an object.`);
@@ -43,7 +57,7 @@ function assertRunLedgerEntry(value, fieldPath) {
43
57
  return null;
44
58
  }
45
59
  if (typeof entry !== "string" || entry.trim().length === 0) {
46
- throw new Error(`Invalid run ledger in ${fieldPath}.${field}: expected a string or null.`);
60
+ throw new Error(`Invalid run ledger in ${fieldPath}.${field}: expected a non-empty string or null.`);
47
61
  }
48
62
  return entry;
49
63
  };
@@ -74,28 +88,6 @@ function parseRunLedger(value, path) {
74
88
  runs: value.runs.map((entry, index) => assertRunLedgerEntry(entry, `${path}.runs[${index}]`)),
75
89
  };
76
90
  }
77
- function sleep(ms) {
78
- return new Promise((resolve) => setTimeout(resolve, ms));
79
- }
80
- async function acquireLedgerLock(artifactsDir) {
81
- const lockPath = ledgerLockPath(artifactsDir);
82
- await mkdir(artifactsDir, { recursive: true });
83
- for (let attempt = 0; attempt < LOCK_RETRY_LIMIT; attempt += 1) {
84
- try {
85
- return await open(lockPath, "wx");
86
- }
87
- catch (error) {
88
- if (!isFileExistsError(error)) {
89
- throw error;
90
- }
91
- if (attempt === LOCK_RETRY_LIMIT - 1) {
92
- throw new Error(`Timed out waiting to update ${ledgerPath(artifactsDir)} because ${lockPath} is locked.`);
93
- }
94
- await sleep(LOCK_RETRY_DELAY_MS);
95
- }
96
- }
97
- throw new Error(`Failed to acquire lock for ${ledgerPath(artifactsDir)}.`);
98
- }
99
91
  export async function loadRunLedger(artifactsDir) {
100
92
  const path = ledgerPath(artifactsDir);
101
93
  try {
@@ -110,18 +102,15 @@ export async function loadRunLedger(artifactsDir) {
110
102
  }
111
103
  }
112
104
  export async function appendRunLedgerEntry(artifactsDir, entry) {
113
- const lockHandle = await acquireLedgerLock(artifactsDir);
114
105
  const path = ledgerPath(artifactsDir);
106
+ const lockPath = ledgerLockPath(artifactsDir);
115
107
  const tempPath = buildTempLedgerPath(artifactsDir);
116
- try {
108
+ await mkdir(artifactsDir, { recursive: true });
109
+ await withLedgerLock(lockPath, async () => {
117
110
  const ledger = await loadRunLedger(artifactsDir);
118
111
  ledger.runs.push(entry);
119
112
  await writeJsonFile(tempPath, ledger);
120
- await rename(tempPath, path);
121
- }
122
- finally {
123
- await lockHandle.close();
124
- await rm(ledgerLockPath(artifactsDir), { force: true });
113
+ await withFsRetry(() => rename(tempPath, path));
125
114
  await rm(tempPath, { force: true }).catch(() => undefined);
126
- }
115
+ });
127
116
  }
@@ -0,0 +1,31 @@
1
+ export declare const DISPATCH_RESULT_MAP_FILENAME = "dispatch-result-map.json";
2
+ export declare const ACTIVE_DISPATCH_FILENAME = "active-dispatch.json";
3
+ export interface ActiveDispatchState {
4
+ run_id: string;
5
+ created_at: string;
6
+ /** Emitted packets only (after canary/budget filtering). */
7
+ packet_count: number;
8
+ /** Tasks remaining this round (not-yet-done), not just emitted-packet tasks. */
9
+ task_count: number;
10
+ status: "active" | "merged";
11
+ /** "canary" on first contact when only the top packet was emitted; "fan_out" otherwise. */
12
+ phase: "canary" | "fan_out";
13
+ /** packet_id of the emitted canary packet when phase==="canary", else null. */
14
+ canary_packet_id: string | null;
15
+ /** Total packets that would have been emitted before a budget cap (present only when capped). */
16
+ budget_packet_count?: number;
17
+ /** packet_ids NOT emitted due to the budget cap. */
18
+ deferred_packet_ids?: string[];
19
+ /** task_ids NOT emitted due to the budget cap. */
20
+ deferred_task_ids?: string[];
21
+ }
22
+ export interface DispatchResultMapEntry {
23
+ packet_id: string;
24
+ task_id: string;
25
+ result_path: string;
26
+ }
27
+ export interface DispatchResultMap {
28
+ contract_version: "audit-code-dispatch-results/v1alpha1";
29
+ run_id: string;
30
+ entries: DispatchResultMapEntry[];
31
+ }
@@ -0,0 +1,2 @@
1
+ export const DISPATCH_RESULT_MAP_FILENAME = "dispatch-result-map.json";
2
+ export const ACTIVE_DISPATCH_FILENAME = "active-dispatch.json";