auditor-lambda 0.2.5 → 0.2.8

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 (71) hide show
  1. package/README.md +35 -7
  2. package/audit-code-wrapper-lib.mjs +1612 -331
  3. package/dist/cli.js +397 -38
  4. package/dist/coverage.d.ts +2 -2
  5. package/dist/coverage.js +5 -5
  6. package/dist/extractors/disposition.js +10 -1
  7. package/dist/extractors/flows.js +7 -1
  8. package/dist/extractors/pathPatterns.d.ts +3 -0
  9. package/dist/extractors/pathPatterns.js +15 -0
  10. package/dist/extractors/risk.js +7 -1
  11. package/dist/io/artifacts.d.ts +6 -6
  12. package/dist/io/artifacts.js +14 -17
  13. package/dist/io/json.d.ts +2 -0
  14. package/dist/io/json.js +15 -0
  15. package/dist/io/runArtifacts.d.ts +3 -1
  16. package/dist/io/runArtifacts.js +20 -5
  17. package/dist/mcp/server.d.ts +1 -0
  18. package/dist/mcp/server.js +579 -0
  19. package/dist/orchestrator/advance.js +9 -2
  20. package/dist/orchestrator/dependencyMap.js +9 -13
  21. package/dist/orchestrator/executors.js +7 -2
  22. package/dist/orchestrator/flowRequeue.d.ts +2 -2
  23. package/dist/orchestrator/flowRequeue.js +16 -3
  24. package/dist/orchestrator/internalExecutors.d.ts +2 -1
  25. package/dist/orchestrator/internalExecutors.js +129 -48
  26. package/dist/orchestrator/requeue.js +10 -4
  27. package/dist/orchestrator/requeueCommand.js +15 -2
  28. package/dist/orchestrator/resultIngestion.d.ts +2 -1
  29. package/dist/orchestrator/resultIngestion.js +26 -6
  30. package/dist/orchestrator/runtimeValidation.d.ts +7 -2
  31. package/dist/orchestrator/runtimeValidation.js +61 -49
  32. package/dist/orchestrator/runtimeValidationUpdate.js +2 -4
  33. package/dist/orchestrator/state.js +28 -14
  34. package/dist/orchestrator/taskBuilder.js +4 -2
  35. package/dist/orchestrator/trivialAudit.d.ts +4 -0
  36. package/dist/orchestrator/trivialAudit.js +49 -0
  37. package/dist/prompts/renderWorkerPrompt.js +6 -2
  38. package/dist/providers/spawnLoggedCommand.js +17 -0
  39. package/dist/reporting/mergeFindings.js +3 -11
  40. package/dist/reporting/rootCause.js +92 -9
  41. package/dist/reporting/synthesis.d.ts +25 -22
  42. package/dist/reporting/synthesis.js +92 -59
  43. package/dist/reporting/workBlocks.d.ts +12 -3
  44. package/dist/reporting/workBlocks.js +124 -70
  45. package/dist/supervisor/sessionConfig.js +4 -2
  46. package/dist/types/flows.d.ts +2 -0
  47. package/dist/types/runtimeValidation.d.ts +2 -1
  48. package/dist/types.d.ts +8 -6
  49. package/dist/validation/auditResults.d.ts +5 -2
  50. package/dist/validation/auditResults.js +335 -43
  51. package/docs/agent-integrations.md +38 -29
  52. package/docs/artifacts.md +18 -51
  53. package/docs/bootstrap-install.md +60 -30
  54. package/docs/contract.md +25 -117
  55. package/docs/field-trial-bug-report.md +237 -0
  56. package/docs/next-steps.md +59 -44
  57. package/docs/packaging.md +13 -3
  58. package/docs/production-launch-bar.md +2 -2
  59. package/docs/production-readiness.md +9 -5
  60. package/docs/releasing.md +81 -0
  61. package/docs/session-config.md +20 -1
  62. package/docs/usage.md +22 -0
  63. package/package.json +4 -1
  64. package/schemas/audit_result.schema.json +4 -5
  65. package/schemas/audit_task.schema.json +10 -0
  66. package/schemas/runtime_validation_report.schema.json +1 -1
  67. package/skills/audit-code/SKILL.md +11 -2
  68. package/skills/audit-code/audit-code.prompt.md +11 -10
  69. package/schemas/merged_findings.schema.json +0 -19
  70. package/schemas/root_cause_clusters.schema.json +0 -28
  71. package/schemas/synthesis_report.schema.json +0 -61
@@ -38,11 +38,9 @@ export function updateRuntimeValidationReport(tasks, existing, updates) {
38
38
  merged.set(task.id, {
39
39
  task_id: task.id,
40
40
  status: "pending",
41
- summary: `No runtime evidence recorded yet for ${task.id}.`,
41
+ summary: `Deterministic runtime validation has not executed yet for ${task.id}.`,
42
42
  evidence: [],
43
- notes: [
44
- "Placeholder entry generated from runtime validation task list.",
45
- ],
43
+ notes: [],
46
44
  });
47
45
  }
48
46
  }
@@ -43,23 +43,40 @@ export function deriveAuditState(bundle) {
43
43
  "requeue_tasks.json",
44
44
  ], planningReady)));
45
45
  const hasRequiredCoverage = bundle.coverage_matrix?.files.every((f) => f.required_lenses.every((req) => f.completed_lenses.includes(req))) ?? true;
46
+ const hasCompletedTaskStatuses = bundle.audit_tasks?.length
47
+ ? bundle.audit_tasks.every((task) => task.status === "complete")
48
+ : false;
49
+ const hasResultForEveryTask = bundle.audit_tasks?.length && bundle.audit_results
50
+ ? bundle.audit_tasks.every((task) => bundle.audit_results?.some((result) => result.task_id === task.task_id))
51
+ : false;
46
52
  if (!hasRequiredCoverage &&
53
+ !hasCompletedTaskStatuses &&
54
+ !hasResultForEveryTask &&
47
55
  has(bundle.audit_tasks) &&
48
56
  (bundle.audit_tasks?.length ?? 0) > 0) {
49
57
  obligations.push(obligation("audit_tasks_completed", "missing"));
50
58
  }
51
- else if (hasRequiredCoverage && has(bundle.audit_tasks)) {
59
+ else if ((hasRequiredCoverage || hasCompletedTaskStatuses || hasResultForEveryTask) &&
60
+ has(bundle.audit_tasks)) {
52
61
  obligations.push(obligation("audit_tasks_completed", "satisfied"));
53
62
  }
54
- obligations.push(obligation("audit_results_ingested", has(bundle.audit_results) ? "present" : "missing"));
55
- obligations.push(obligation("runtime_validation_current", staleOrSatisfied(staleArtifacts, ["runtime_validation_report.json"], has(bundle.runtime_validation_report))));
56
- obligations.push(obligation("synthesis_current", staleOrSatisfied(staleArtifacts, [
57
- "merged_findings.json",
58
- "root_cause_clusters.json",
59
- "synthesis_report.json",
60
- ], has(bundle.merged_findings) &&
61
- has(bundle.root_cause_clusters) &&
62
- has(bundle.synthesis_report))));
63
+ obligations.push(obligation("audit_results_ingested", (bundle.audit_tasks?.length ?? 0) === 0 || has(bundle.audit_results)
64
+ ? "present"
65
+ : "missing"));
66
+ const runtimeTasks = bundle.runtime_validation_tasks?.tasks ?? [];
67
+ const runtimeResults = bundle.runtime_validation_report?.results ?? [];
68
+ const runtimeReady = runtimeTasks.length === 0 ||
69
+ (runtimeTasks.length > 0 &&
70
+ runtimeTasks.every((task) => runtimeResults.some((result) => result.task_id === task.id &&
71
+ result.status !== "pending")));
72
+ obligations.push(obligation("runtime_validation_current", runtimeReady
73
+ ? "satisfied"
74
+ : has(bundle.runtime_validation_report)
75
+ ? "missing"
76
+ : "missing", runtimeTasks.length === 0
77
+ ? "No deterministic runtime validation tasks were planned."
78
+ : undefined));
79
+ obligations.push(obligation("synthesis_current", staleOrSatisfied(staleArtifacts, ["audit-report.md"], has(bundle.audit_report))));
63
80
  let status = "not_started";
64
81
  if (!has(bundle.repo_manifest)) {
65
82
  status = "not_started";
@@ -71,10 +88,7 @@ export function deriveAuditState(bundle) {
71
88
  status = "active";
72
89
  }
73
90
  const incomplete = obligations.some((o) => o.state === "missing" || o.state === "stale");
74
- if (!incomplete &&
75
- has(bundle.synthesis_report) &&
76
- has(bundle.merged_findings) &&
77
- has(bundle.root_cause_clusters)) {
91
+ if (!incomplete && has(bundle.audit_report)) {
78
92
  status = "complete";
79
93
  }
80
94
  return {
@@ -1,3 +1,4 @@
1
+ import { isTrivialAuditPath } from "./trivialAudit.js";
1
2
  function taskPriority(hasExternalSignal, lens) {
2
3
  if (hasExternalSignal &&
3
4
  (lens === "security" || lens === "data_integrity" || lens === "reliability")) {
@@ -57,12 +58,13 @@ export function buildChunkedAuditTasks(unitManifest, unitLineIndex, options = {}
57
58
  const hasExternalSignal = unit.files.some((f) => externalPaths.has(f));
58
59
  const priority = taskPriority(hasExternalSignal, lens);
59
60
  const tags = hasExternalSignal ? ["external_analyzer_signal"] : [];
61
+ const candidateFiles = unit.files.filter((filePath) => !isTrivialAuditPath(filePath, unitLineIndex[filePath] ?? 0, externalPaths.has(filePath)));
60
62
  // Split files that are individually too large to group; everything else
61
63
  // goes into one task so the agent can reason across file boundaries.
62
64
  const oversizedFiles = fileSplitThreshold > 0
63
- ? unit.files.filter((f) => (unitLineIndex[f] ?? 0) > fileSplitThreshold)
65
+ ? candidateFiles.filter((f) => (unitLineIndex[f] ?? 0) > fileSplitThreshold)
64
66
  : [];
65
- const normalFiles = unit.files.filter((f) => !oversizedFiles.includes(f));
67
+ const normalFiles = candidateFiles.filter((f) => !oversizedFiles.includes(f));
66
68
  // One task for all normal-sized files in this unit under this lens.
67
69
  if (normalFiles.length > 0) {
68
70
  const id = `${unit.unit_id}:${lens}`;
@@ -0,0 +1,4 @@
1
+ import type { CoverageMatrix } from "../types.js";
2
+ import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
3
+ export declare function isTrivialAuditPath(path: string, lineCount: number, hasExternalSignal?: boolean): boolean;
4
+ export declare function autoCompleteTrivialCoverage(coverage: CoverageMatrix, lineIndex: Record<string, number>, externalAnalyzerResults?: ExternalAnalyzerResults): string[];
@@ -0,0 +1,49 @@
1
+ const TRIVIAL_DOTFILES = new Set([".gitignore", ".gitattributes"]);
2
+ function basename(path) {
3
+ const normalized = path.replaceAll("\\", "/");
4
+ const parts = normalized.split("/");
5
+ return parts[parts.length - 1] ?? normalized;
6
+ }
7
+ export function isTrivialAuditPath(path, lineCount, hasExternalSignal = false) {
8
+ if (hasExternalSignal) {
9
+ return false;
10
+ }
11
+ if (lineCount === 0) {
12
+ return true;
13
+ }
14
+ const name = basename(path).toLowerCase();
15
+ if (TRIVIAL_DOTFILES.has(name)) {
16
+ return true;
17
+ }
18
+ // Empty package markers and docstring-only __init__.py files create a lot of
19
+ // audit churn without adding meaningful coverage signal.
20
+ if (name === "__init__.py" && lineCount <= 3) {
21
+ return true;
22
+ }
23
+ if (lineCount <= 1) {
24
+ return true;
25
+ }
26
+ return false;
27
+ }
28
+ export function autoCompleteTrivialCoverage(coverage, lineIndex, externalAnalyzerResults) {
29
+ const externalPaths = new Set((externalAnalyzerResults?.results ?? []).map((item) => item.path));
30
+ const skipped = [];
31
+ for (const file of coverage.files) {
32
+ if (file.audit_status === "excluded") {
33
+ continue;
34
+ }
35
+ if (!isTrivialAuditPath(file.path, lineIndex[file.path] ?? 0, externalPaths.has(file.path))) {
36
+ continue;
37
+ }
38
+ if (file.required_lenses.length === 0) {
39
+ continue;
40
+ }
41
+ file.completed_lenses = [];
42
+ file.required_lenses = [];
43
+ file.audit_status = "excluded";
44
+ file.classification_status = "excluded_trivial";
45
+ file.unit_ids = [];
46
+ skipped.push(file.path);
47
+ }
48
+ return skipped.sort();
49
+ }
@@ -20,12 +20,16 @@ export function renderWorkerPrompt(task) {
20
20
  " 2. Review the content under the specified lens.",
21
21
  " 3. Emit one AuditResult with:",
22
22
  " task_id, unit_id, pass_id, lens",
23
- " reviewed_ranges: [{path, start, end}] covering what you read",
23
+ " file_coverage: [{path, total_lines}] for every assigned file you reviewed",
24
24
  " findings: array (empty if nothing found)",
25
+ " total_lines must match the file's current total line count.",
25
26
  " Each finding must include:",
26
27
  " id, title, category, severity, confidence, lens, summary, affected_files,",
27
- " evidence (at least one excerpt or line reference from the file you read)",
28
+ " evidence (an array of plain strings only, at least one excerpt or line reference from the file you read)",
29
+ " Example evidence entry: src/foo.ts:42 - variable overwritten before use",
28
30
  " Optional finding fields: impact, likelihood, reproduction, systemic, related_findings",
31
+ " Low-priority tasks still require a real review. Use findings: [] only when you genuinely found nothing notable.",
32
+ `Reference schema: ${task.artifacts_dir}/dispatch/audit-result.schema.json`,
29
33
  `Write the AuditResult[] JSON array to: ${task.audit_results_path}`,
30
34
  ];
31
35
  if (task.skip_worker_command) {
@@ -11,14 +11,25 @@ export async function spawnLoggedCommand(command, args, input, env) {
11
11
  return await new Promise((resolve, reject) => {
12
12
  const stdoutLog = createWriteStream(input.stdoutPath, { flags: "a" });
13
13
  const stderrLog = createWriteStream(input.stderrPath, { flags: "a" });
14
+ const startedAt = Date.now();
15
+ let timedOut = false;
14
16
  const child = spawn(command, args, {
15
17
  cwd: input.repoRoot,
16
18
  env: { ...process.env, ...env },
17
19
  stdio: ["ignore", "pipe", "pipe"],
18
20
  });
19
21
  const timer = setTimeout(() => {
22
+ timedOut = true;
20
23
  child.kill("SIGTERM");
21
24
  }, input.timeoutMs);
25
+ const heartbeat = setInterval(() => {
26
+ const elapsedMs = Date.now() - startedAt;
27
+ const message = `[provider] run ${input.runId} still running after ${elapsedMs}ms\n`;
28
+ tee(stderrLog, message);
29
+ if (input.uiMode === "visible") {
30
+ process.stderr.write(message);
31
+ }
32
+ }, 30_000);
22
33
  child.stdout.on("data", (chunk) => {
23
34
  tee(stdoutLog, chunk);
24
35
  if (input.uiMode === "visible") {
@@ -33,14 +44,20 @@ export async function spawnLoggedCommand(command, args, input, env) {
33
44
  });
34
45
  child.on("error", (error) => {
35
46
  clearTimeout(timer);
47
+ clearInterval(heartbeat);
36
48
  stdoutLog.end();
37
49
  stderrLog.end();
38
50
  reject(error);
39
51
  });
40
52
  child.on("exit", (code, signal) => {
41
53
  clearTimeout(timer);
54
+ clearInterval(heartbeat);
42
55
  stdoutLog.end();
43
56
  stderrLog.end();
57
+ if (timedOut) {
58
+ reject(new Error(`Fresh session timed out after ${input.timeoutMs}ms for run ${input.runId}.`));
59
+ return;
60
+ }
44
61
  resolve({
45
62
  accepted: true,
46
63
  processId: child.pid,
@@ -42,7 +42,9 @@ function runtimeSummary(report) {
42
42
  if (!report) {
43
43
  return [];
44
44
  }
45
- return report.results.map((result) => `${result.task_id}: ${result.status} — ${result.summary}`);
45
+ return report.results
46
+ .filter((result) => result.status !== "pending")
47
+ .map((result) => `${result.task_id}: ${result.status} — ${result.summary}`);
46
48
  }
47
49
  function externalSummary(results) {
48
50
  if (!results) {
@@ -80,9 +82,6 @@ export function mergeFindings(results, runtimeReport, externalAnalyzerResults) {
80
82
  ...analyzerEvidence,
81
83
  ]),
82
84
  ],
83
- related_findings: [
84
- ...new Set([...(finding.related_findings ?? []), finding.id]),
85
- ],
86
85
  });
87
86
  continue;
88
87
  }
@@ -108,13 +107,6 @@ export function mergeFindings(results, runtimeReport, externalAnalyzerResults) {
108
107
  ...analyzerEvidence,
109
108
  ]),
110
109
  ];
111
- existing.related_findings = [
112
- ...new Set([
113
- ...(existing.related_findings ?? []),
114
- ...(finding.related_findings ?? []),
115
- finding.id,
116
- ]),
117
- ];
118
110
  }
119
111
  }
120
112
  return [...merged.values()].sort((a, b) => {
@@ -29,14 +29,94 @@ function summarizeExternal(results) {
29
29
  return "No external analyzer signals attached.";
30
30
  return `${results.tool}:${results.results.length}`;
31
31
  }
32
+ const STOP_WORDS = new Set([
33
+ "a",
34
+ "an",
35
+ "and",
36
+ "are",
37
+ "be",
38
+ "by",
39
+ "for",
40
+ "from",
41
+ "in",
42
+ "into",
43
+ "is",
44
+ "missing",
45
+ "not",
46
+ "of",
47
+ "on",
48
+ "or",
49
+ "that",
50
+ "the",
51
+ "this",
52
+ "to",
53
+ "under",
54
+ "when",
55
+ "with",
56
+ ]);
57
+ function normalizeToken(token) {
58
+ if (token.endsWith("ies") && token.length > 4) {
59
+ return `${token.slice(0, -3)}y`;
60
+ }
61
+ if (token.endsWith("ing") && token.length > 6) {
62
+ return token.slice(0, -3);
63
+ }
64
+ if (token.endsWith("ed") && token.length > 5) {
65
+ return token.slice(0, -2);
66
+ }
67
+ if (token.endsWith("s") && token.length > 4) {
68
+ return token.slice(0, -1);
69
+ }
70
+ return token;
71
+ }
72
+ function extractSemanticTerms(finding) {
73
+ const source = [
74
+ finding.title,
75
+ finding.summary,
76
+ ...(finding.evidence ?? []).slice(0, 2),
77
+ ]
78
+ .join(" ")
79
+ .toLowerCase()
80
+ .replace(/[^a-z0-9]+/g, " ");
81
+ const terms = source
82
+ .split(/\s+/)
83
+ .map(normalizeToken)
84
+ .filter((token) => token.length >= 3 &&
85
+ !STOP_WORDS.has(token) &&
86
+ token !== finding.lens &&
87
+ token !== finding.category &&
88
+ !/^\d+$/.test(token));
89
+ return [...new Set(terms)];
90
+ }
32
91
  function clusterKey(finding) {
33
- const primaryPath = finding.affected_files[0]?.path ?? "";
34
- const pathPrefix = primaryPath.split("/").slice(0, 2).join("/");
35
- return `${finding.lens}:${finding.category}:${pathPrefix}`;
92
+ const semanticTerms = extractSemanticTerms(finding).slice(0, 3).join(" ");
93
+ return `${finding.lens}:${finding.category}:${semanticTerms || finding.title.toLowerCase()}`;
94
+ }
95
+ function representativeFinding(grouped) {
96
+ return [...grouped].sort((a, b) => {
97
+ const severityDelta = severityRank(b.severity) - severityRank(a.severity);
98
+ if (severityDelta !== 0)
99
+ return severityDelta;
100
+ const systemicDelta = Number(Boolean(b.systemic)) - Number(Boolean(a.systemic));
101
+ if (systemicDelta !== 0)
102
+ return systemicDelta;
103
+ return a.title.localeCompare(b.title);
104
+ })[0];
105
+ }
106
+ function titleForCluster(grouped) {
107
+ return representativeFinding(grouped).title;
36
108
  }
37
- function titleForCluster(key) {
38
- const [lens, category, scope] = key.split(":");
39
- return `${lens}/${category}${scope ? ` in ${scope}` : ""}`;
109
+ function summarizeFiles(grouped) {
110
+ const paths = [
111
+ ...new Set(grouped.flatMap((finding) => finding.affected_files.map((file) => file.path))),
112
+ ].sort();
113
+ if (paths.length === 0) {
114
+ return "no representative files recorded";
115
+ }
116
+ if (paths.length <= 3) {
117
+ return paths.join(", ");
118
+ }
119
+ return `${paths.slice(0, 3).join(", ")}, +${paths.length - 3} more`;
40
120
  }
41
121
  export function buildRootCauseClusters(findings, runtimeReport, externalAnalyzerResults) {
42
122
  const groups = new Map();
@@ -50,12 +130,15 @@ export function buildRootCauseClusters(findings, runtimeReport, externalAnalyzer
50
130
  const externalSummary = summarizeExternal(externalAnalyzerResults);
51
131
  return [...groups.entries()]
52
132
  .map(([key, grouped], index) => {
53
- const highestSeverity = grouped.reduce((max, finding) => Math.max(max, severityRank(finding.severity)), 1);
133
+ const representative = representativeFinding(grouped);
54
134
  const systemicCount = grouped.filter((finding) => finding.systemic).length;
135
+ const theme = key.split(":").slice(2).join(":") || representative.title;
55
136
  return {
56
137
  id: `cluster-${index + 1}`,
57
- title: titleForCluster(key),
58
- summary: `Grouped ${grouped.length} finding(s); highest severity ${highestSeverity}; systemic flags ${systemicCount}. Runtime validation status: ${runtimeSummary}. External analyzer summary: ${externalSummary}.`,
138
+ title: titleForCluster(grouped),
139
+ summary: `Theme "${theme}" groups ${grouped.length} finding(s) across ${summarizeFiles(grouped)}; ` +
140
+ `highest severity ${representative.severity}; systemic flags ${systemicCount}. ` +
141
+ `Runtime validation status: ${runtimeSummary}. External analyzer summary: ${externalSummary}.`,
59
142
  finding_ids: grouped.map((finding) => finding.id),
60
143
  };
61
144
  })
@@ -1,24 +1,27 @@
1
- import type { AuditResult } from "../types.js";
2
- import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
1
+ import type { AuditResult, CoverageMatrix, Finding, UnitManifest } from "../types.js";
2
+ import type { CriticalFlowManifest } from "../types/flows.js";
3
+ import type { GraphBundle } from "../types/graph.js";
3
4
  import type { RuntimeValidationReport } from "../types/runtimeValidation.js";
4
- import { mergeFindings } from "./mergeFindings.js";
5
- import { buildRootCauseClusters } from "./rootCause.js";
6
- import { buildWorkBlocks } from "./workBlocks.js";
7
- export interface SynthesisReport {
8
- summary: {
9
- finding_count: number;
10
- cluster_count: number;
11
- work_block_count: number;
12
- runtime_validation_status_breakdown: Record<string, number>;
13
- notes: string[];
14
- severity_breakdown: Record<string, number>;
15
- external_analyzer_summary: {
16
- tool_count: number;
17
- result_count: number;
18
- };
19
- };
20
- merged_findings: ReturnType<typeof mergeFindings>;
21
- root_cause_clusters: ReturnType<typeof buildRootCauseClusters>;
22
- work_blocks: ReturnType<typeof buildWorkBlocks>;
5
+ import { type WorkBlock } from "./workBlocks.js";
6
+ export interface AuditReportSummary {
7
+ finding_count: number;
8
+ work_block_count: number;
9
+ severity_breakdown: Record<string, number>;
10
+ audited_file_count: number;
11
+ excluded_file_count: number;
12
+ runtime_validation_status_breakdown: Record<string, number>;
23
13
  }
24
- export declare function buildSynthesisReport(results: AuditResult[], runtimeReport?: RuntimeValidationReport, externalAnalyzerResults?: ExternalAnalyzerResults): SynthesisReport;
14
+ export interface AuditReportModel {
15
+ summary: AuditReportSummary;
16
+ findings: Finding[];
17
+ work_blocks: WorkBlock[];
18
+ }
19
+ export declare function buildAuditReportModel(params: {
20
+ results: AuditResult[];
21
+ unitManifest?: UnitManifest;
22
+ graphBundle?: GraphBundle;
23
+ criticalFlows?: CriticalFlowManifest;
24
+ coverageMatrix?: CoverageMatrix;
25
+ runtimeValidationReport?: RuntimeValidationReport;
26
+ }): AuditReportModel;
27
+ export declare function renderAuditReportMarkdown(model: AuditReportModel): string;
@@ -1,13 +1,5 @@
1
- import { mergeFindings } from "./mergeFindings.js";
2
- import { buildRootCauseClusters } from "./rootCause.js";
3
1
  import { buildWorkBlocks } from "./workBlocks.js";
4
- function statusBreakdown(report) {
5
- const breakdown = {};
6
- for (const result of report?.results ?? []) {
7
- breakdown[result.status] = (breakdown[result.status] ?? 0) + 1;
8
- }
9
- return breakdown;
10
- }
2
+ import { mergeFindings } from "./mergeFindings.js";
11
3
  function severityBreakdown(findings) {
12
4
  const breakdown = {};
13
5
  for (const finding of findings) {
@@ -15,63 +7,104 @@ function severityBreakdown(findings) {
15
7
  }
16
8
  return breakdown;
17
9
  }
18
- function zeroFindingLensNotes(results) {
19
- const tasksByLens = new Map();
20
- const findingsByLens = new Map();
21
- for (const result of results) {
22
- tasksByLens.set(result.lens, (tasksByLens.get(result.lens) ?? 0) + 1);
23
- findingsByLens.set(result.lens, (findingsByLens.get(result.lens) ?? 0) + result.findings.length);
10
+ function runtimeStatusBreakdown(report) {
11
+ const breakdown = {};
12
+ for (const result of report?.results ?? []) {
13
+ breakdown[result.status] = (breakdown[result.status] ?? 0) + 1;
24
14
  }
25
- const zeroLenses = [...tasksByLens.entries()]
26
- .filter(([lens, count]) => count > 0 && (findingsByLens.get(lens) ?? 0) === 0)
27
- .map(([lens]) => lens)
28
- .sort();
29
- if (zeroLenses.length === 0)
30
- return [];
31
- return [
32
- `Zero findings across all reviewed tasks for lens(es): ${zeroLenses.join(", ")}. Verify these tasks were genuinely reviewed rather than batch-generated.`,
33
- ];
15
+ return breakdown;
34
16
  }
35
- function externalSummary(results) {
36
- if (!results) {
37
- return { tool_count: 0, result_count: 0 };
38
- }
17
+ function coverageSummary(coverage) {
18
+ const files = coverage?.files ?? [];
39
19
  return {
40
- tool_count: results.tool ? 1 : 0,
41
- result_count: results.results.length,
20
+ audited_file_count: files.filter((file) => file.audit_status === "complete").length,
21
+ excluded_file_count: files.filter((file) => file.audit_status === "excluded").length,
42
22
  };
43
23
  }
44
- export function buildSynthesisReport(results, runtimeReport, externalAnalyzerResults) {
45
- const merged_findings = mergeFindings(results, runtimeReport, externalAnalyzerResults);
46
- const root_cause_clusters = buildRootCauseClusters(merged_findings, runtimeReport, externalAnalyzerResults);
47
- const work_blocks = buildWorkBlocks(merged_findings);
48
- const runtimeBreakdown = statusBreakdown(runtimeReport);
49
- const sevBreakdown = severityBreakdown(merged_findings);
50
- const extSummary = externalSummary(externalAnalyzerResults);
24
+ function formatSeverityList(summary) {
25
+ const ordered = ["critical", "high", "medium", "low", "info"];
26
+ const parts = ordered
27
+ .filter((severity) => (summary[severity] ?? 0) > 0)
28
+ .map((severity) => `${severity}: ${summary[severity]}`);
29
+ return parts.length > 0 ? parts.join(", ") : "none";
30
+ }
31
+ export function buildAuditReportModel(params) {
32
+ const findings = mergeFindings(params.results, params.runtimeValidationReport);
33
+ const workBlocks = buildWorkBlocks({
34
+ findings,
35
+ unitManifest: params.unitManifest,
36
+ graphBundle: params.graphBundle,
37
+ criticalFlows: params.criticalFlows,
38
+ });
39
+ const coverage = coverageSummary(params.coverageMatrix);
51
40
  return {
52
41
  summary: {
53
- finding_count: merged_findings.length,
54
- cluster_count: root_cause_clusters.length,
55
- work_block_count: work_blocks.length,
56
- runtime_validation_status_breakdown: runtimeBreakdown,
57
- severity_breakdown: sevBreakdown,
58
- external_analyzer_summary: extSummary,
59
- notes: [
60
- ...(Object.keys(runtimeBreakdown).length === 0
61
- ? ["No runtime validation evidence attached."]
62
- : [
63
- "Runtime validation evidence has been incorporated into synthesis.",
64
- ]),
65
- ...(extSummary.result_count > 0
66
- ? [
67
- `External analyzer signals incorporated: ${extSummary.result_count} result(s) from ${externalAnalyzerResults?.tool}.`,
68
- ]
69
- : []),
70
- ...zeroFindingLensNotes(results),
71
- ],
42
+ finding_count: findings.length,
43
+ work_block_count: workBlocks.length,
44
+ severity_breakdown: severityBreakdown(findings),
45
+ audited_file_count: coverage.audited_file_count,
46
+ excluded_file_count: coverage.excluded_file_count,
47
+ runtime_validation_status_breakdown: runtimeStatusBreakdown(params.runtimeValidationReport),
72
48
  },
73
- merged_findings,
74
- root_cause_clusters,
75
- work_blocks,
49
+ findings,
50
+ work_blocks: workBlocks,
76
51
  };
77
52
  }
53
+ export function renderAuditReportMarkdown(model) {
54
+ const lines = [
55
+ "# Audit Report",
56
+ "",
57
+ "## Summary",
58
+ "",
59
+ `- Findings: ${model.summary.finding_count}`,
60
+ `- Work blocks: ${model.summary.work_block_count}`,
61
+ `- Severity breakdown: ${formatSeverityList(model.summary.severity_breakdown)}`,
62
+ `- Fully audited files: ${model.summary.audited_file_count}`,
63
+ `- Excluded non-auditable files: ${model.summary.excluded_file_count}`,
64
+ "",
65
+ "## Work Blocks",
66
+ "",
67
+ ];
68
+ if (model.work_blocks.length === 0) {
69
+ lines.push("No remediation work blocks were generated.", "");
70
+ }
71
+ else {
72
+ for (const block of model.work_blocks) {
73
+ lines.push(`### ${block.id}`);
74
+ lines.push("");
75
+ lines.push(`- Max severity: ${block.max_severity}`);
76
+ lines.push(`- Units: ${block.unit_ids.join(", ")}`);
77
+ lines.push(`- Owned files: ${block.owned_files.join(", ")}`);
78
+ lines.push(`- Findings: ${block.finding_ids.join(", ")}`);
79
+ lines.push(`- Depends on: ${block.depends_on.length > 0 ? block.depends_on.join(", ") : "none"}`);
80
+ lines.push(`- Rationale: ${block.rationale}`);
81
+ lines.push("");
82
+ }
83
+ }
84
+ lines.push("## Findings", "");
85
+ if (model.findings.length === 0) {
86
+ lines.push("No findings were recorded.", "");
87
+ }
88
+ else {
89
+ for (const finding of model.findings) {
90
+ lines.push(`### ${finding.id} — ${finding.title}`);
91
+ lines.push("");
92
+ lines.push(`- Severity: ${finding.severity}`);
93
+ lines.push(`- Confidence: ${finding.confidence}`);
94
+ lines.push(`- Lens: ${finding.lens}`);
95
+ lines.push(`- Files: ${finding.affected_files.map((file) => file.path).join(", ")}`);
96
+ lines.push(`- Summary: ${finding.summary}`);
97
+ if (finding.evidence && finding.evidence.length > 0) {
98
+ lines.push("- Evidence:");
99
+ for (const evidence of finding.evidence) {
100
+ lines.push(` - ${evidence}`);
101
+ }
102
+ }
103
+ lines.push("");
104
+ }
105
+ }
106
+ lines.push("## Scope and Coverage", "");
107
+ lines.push("This report is deterministic output from the completed audit. Non-auditable files were excluded from scope before task generation.");
108
+ lines.push("");
109
+ return lines.join("\n");
110
+ }
@@ -1,9 +1,18 @@
1
- import type { Finding } from "../types.js";
1
+ import type { Finding, UnitManifest } from "../types.js";
2
+ import type { CriticalFlowManifest } from "../types/flows.js";
3
+ import type { GraphBundle } from "../types/graph.js";
2
4
  export interface WorkBlock {
3
5
  id: string;
4
6
  finding_ids: string[];
5
- affected_files: string[];
7
+ unit_ids: string[];
8
+ owned_files: string[];
6
9
  max_severity: Finding["severity"];
7
10
  rationale: string;
11
+ depends_on: string[];
8
12
  }
9
- export declare function buildWorkBlocks(findings: Finding[]): WorkBlock[];
13
+ export declare function buildWorkBlocks(params: {
14
+ findings: Finding[];
15
+ unitManifest?: UnitManifest;
16
+ graphBundle?: GraphBundle;
17
+ criticalFlows?: CriticalFlowManifest;
18
+ }): WorkBlock[];