auditor-lambda 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,7 +19,14 @@ function taskPriority(hasExternalSignal, lens) {
19
19
  }
20
20
  return hasExternalSignal ? "medium" : "low";
21
21
  }
22
- export function buildFlowRequeueTasks(criticalFlows, flowCoverage, externalAnalyzerResults) {
22
+ function fileStillNeedsLens(coverageMatrix, path, lens) {
23
+ const record = coverageMatrix.files.find((file) => file.path === path);
24
+ if (!record || record.audit_status === "excluded") {
25
+ return false;
26
+ }
27
+ return !record.completed_lenses.includes(lens);
28
+ }
29
+ export function buildFlowRequeueTasks(criticalFlows, flowCoverage, coverageMatrix, externalAnalyzerResults) {
23
30
  const flowMap = new Map(criticalFlows.flows.map((flow) => [flow.id, flow]));
24
31
  const tasks = [];
25
32
  const seen = new Set();
@@ -35,6 +42,9 @@ export function buildFlowRequeueTasks(criticalFlows, flowCoverage, externalAnaly
35
42
  continue;
36
43
  }
37
44
  for (const path of flow.paths) {
45
+ if (!fileStillNeedsLens(coverageMatrix, path, lensName)) {
46
+ continue;
47
+ }
38
48
  const signature = `${flow.id}|${lensName}|${path}`;
39
49
  if (seen.has(signature)) {
40
50
  continue;
@@ -49,7 +59,10 @@ export function buildFlowRequeueTasks(criticalFlows, flowCoverage, externalAnaly
49
59
  file_paths: [path],
50
60
  rationale: `Requeue ${path} because critical flow ${flow.id} is still missing the ${lensName} lens.${hasExternalSignal ? " External analyzer signals make this follow-up higher priority." : ""}`,
51
61
  priority: taskPriority(hasExternalSignal, lensName),
52
- tags: hasExternalSignal ? ["external_analyzer_signal"] : [],
62
+ tags: hasExternalSignal
63
+ ? ["critical_flow_followup", "external_analyzer_signal"]
64
+ : ["critical_flow_followup"],
65
+ status: "pending",
53
66
  });
54
67
  }
55
68
  }
@@ -13,8 +13,16 @@ import { buildChunkedAuditTasks, buildExternalSignalTasks, } from "./taskBuilder
13
13
  import { buildUnitManifest } from "./unitBuilder.js";
14
14
  import { buildRepoManifestFromFs } from "../extractors/fsIntake.js";
15
15
  import { loadIgnoreFile } from "../extractors/ignore.js";
16
- import { ingestAuditResults } from "./resultIngestion.js";
16
+ import { ingestAuditResults, updateAuditTaskStatuses, } from "./resultIngestion.js";
17
17
  import { updateRuntimeValidationReport } from "./runtimeValidationUpdate.js";
18
+ import { autoCompleteTrivialCoverage } from "./trivialAudit.js";
19
+ function buildSynthesisArtifacts(synthesisReport) {
20
+ return {
21
+ synthesis_report: synthesisReport,
22
+ merged_findings: { findings: synthesisReport.merged_findings },
23
+ root_cause_clusters: { clusters: synthesisReport.root_cause_clusters },
24
+ };
25
+ }
18
26
  function preserveOrPlaceholder(tasks, existing) {
19
27
  return existing
20
28
  ? mergeRuntimeValidationReport(tasks, existing)
@@ -83,6 +91,7 @@ export function runPlanningExecutor(bundle, lineIndex = {}) {
83
91
  }
84
92
  const externalAnalyzerResults = bundle.external_analyzer_results;
85
93
  const coverage = initializeCoverageFromPlan(bundle.repo_manifest, bundle.unit_manifest, bundle.file_disposition, externalAnalyzerResults);
94
+ const autoSkippedTrivialFiles = autoCompleteTrivialCoverage(coverage, lineIndex, externalAnalyzerResults);
86
95
  const flowCoverage = buildFlowCoverage(bundle.critical_flows, coverage);
87
96
  const runtimeValidationTasks = buildRuntimeValidationTasks(bundle.unit_manifest, bundle.critical_flows, flowCoverage);
88
97
  const runtimeValidationReport = preserveOrPlaceholder(runtimeValidationTasks, bundle.runtime_validation_report);
@@ -91,7 +100,10 @@ export function runPlanningExecutor(bundle, lineIndex = {}) {
91
100
  });
92
101
  const analyzerTasks = buildExternalSignalTasks(coverage, lineIndex, externalAnalyzerResults);
93
102
  const flowTasks = buildFlowAwareTaskAugmentations([...baseTasks, ...analyzerTasks], bundle.critical_flows, lineIndex);
94
- const auditTasks = [...baseTasks, ...analyzerTasks, ...flowTasks];
103
+ const auditTasks = [...baseTasks, ...analyzerTasks, ...flowTasks].map((task) => ({
104
+ ...task,
105
+ status: task.status ?? "pending",
106
+ }));
95
107
  const requeuePayload = buildRequeuePayload(coverage, bundle.critical_flows, flowCoverage, externalAnalyzerResults);
96
108
  return {
97
109
  updated: {
@@ -111,7 +123,13 @@ export function runPlanningExecutor(bundle, lineIndex = {}) {
111
123
  "audit_tasks.json",
112
124
  "requeue_tasks.json",
113
125
  ],
114
- progress_summary: `Built planning artifacts; generated ${auditTasks.length} tasks and ${requeuePayload.task_count} requeue tasks.${externalAnalyzerResults?.results.length ? ` External analyzer signals influenced lenses and produced ${analyzerTasks.length} dedicated follow-up task(s).` : ""}`,
126
+ progress_summary: `Built planning artifacts; generated ${auditTasks.length} tasks and ${requeuePayload.task_count} requeue tasks.` +
127
+ (autoSkippedTrivialFiles.length > 0
128
+ ? ` Auto-completed ${autoSkippedTrivialFiles.length} trivial file${autoSkippedTrivialFiles.length === 1 ? "" : "s"} without dispatch.`
129
+ : "") +
130
+ (externalAnalyzerResults?.results.length
131
+ ? ` External analyzer signals influenced lenses and produced ${analyzerTasks.length} dedicated follow-up task(s).`
132
+ : ""),
115
133
  };
116
134
  }
117
135
  export function runResultIngestionExecutor(bundle, results) {
@@ -130,7 +148,9 @@ export function runResultIngestionExecutor(bundle, results) {
130
148
  : bundle.runtime_validation_report;
131
149
  const requeuePayload = buildRequeuePayload(updatedCoverageMatrix, bundle.critical_flows, flowCoverage, bundle.external_analyzer_results);
132
150
  const mergedResults = [...(bundle.audit_results ?? []), ...results];
151
+ const updatedAuditTasks = updateAuditTaskStatuses(bundle.audit_tasks, mergedResults);
133
152
  const synthesisReport = buildSynthesisReport(mergedResults, runtimeValidationReport, bundle.external_analyzer_results);
153
+ const synthesisArtifacts = buildSynthesisArtifacts(synthesisReport);
134
154
  return {
135
155
  updated: {
136
156
  ...bundle,
@@ -139,8 +159,9 @@ export function runResultIngestionExecutor(bundle, results) {
139
159
  runtime_validation_tasks: runtimeValidationTasks,
140
160
  runtime_validation_report: runtimeValidationReport,
141
161
  audit_results: mergedResults,
162
+ audit_tasks: updatedAuditTasks,
142
163
  requeue_tasks: requeuePayload.tasks,
143
- synthesis_report: synthesisReport,
164
+ ...synthesisArtifacts,
144
165
  },
145
166
  artifacts_written: [
146
167
  "coverage_matrix.json",
@@ -148,7 +169,10 @@ export function runResultIngestionExecutor(bundle, results) {
148
169
  "runtime_validation_tasks.json",
149
170
  "runtime_validation_report.json",
150
171
  "audit_results.jsonl",
172
+ "audit_tasks.json",
151
173
  "requeue_tasks.json",
174
+ "merged_findings.json",
175
+ "root_cause_clusters.json",
152
176
  "synthesis_report.json",
153
177
  ],
154
178
  progress_summary: `Ingested ${results.length} audit result entries and refreshed dependent artifacts.`,
@@ -162,14 +186,17 @@ export function runRuntimeValidationUpdateExecutor(bundle, updates) {
162
186
  buildPlaceholderRuntimeValidationReport(bundle.runtime_validation_tasks);
163
187
  const mergedReport = updateRuntimeValidationReport(bundle.runtime_validation_tasks, existingReport, updates);
164
188
  const synthesisReport = buildSynthesisReport(bundle.audit_results ?? [], mergedReport, bundle.external_analyzer_results);
189
+ const synthesisArtifacts = buildSynthesisArtifacts(synthesisReport);
165
190
  return {
166
191
  updated: {
167
192
  ...bundle,
168
193
  runtime_validation_report: mergedReport,
169
- synthesis_report: synthesisReport,
194
+ ...synthesisArtifacts,
170
195
  },
171
196
  artifacts_written: [
172
197
  "runtime_validation_report.json",
198
+ "merged_findings.json",
199
+ "root_cause_clusters.json",
173
200
  "synthesis_report.json",
174
201
  ],
175
202
  progress_summary: `Merged ${updates.results.length} runtime validation updates.`,
@@ -178,15 +205,12 @@ export function runRuntimeValidationUpdateExecutor(bundle, updates) {
178
205
  export function runSynthesisExecutor(bundle, results) {
179
206
  const finalResults = results ?? bundle.audit_results ?? [];
180
207
  const synthesisReport = buildSynthesisReport(finalResults, bundle.runtime_validation_report, bundle.external_analyzer_results);
181
- const mergedFindings = { findings: synthesisReport.merged_findings };
182
- const rootCauseClusters = { clusters: synthesisReport.root_cause_clusters };
208
+ const synthesisArtifacts = buildSynthesisArtifacts(synthesisReport);
183
209
  return {
184
210
  updated: {
185
211
  ...bundle,
186
212
  audit_results: finalResults,
187
- merged_findings: mergedFindings,
188
- root_cause_clusters: rootCauseClusters,
189
- synthesis_report: synthesisReport,
213
+ ...synthesisArtifacts,
190
214
  },
191
215
  artifacts_written: [
192
216
  "merged_findings.json",
@@ -18,6 +18,7 @@ export function buildRequeueTasks(matrix, externalAnalyzerResults) {
18
18
  rationale: `Requeue ${target.path} because the ${lens} lens is still missing.${hasExternalSignal ? " External analyzer signals make this follow-up higher priority." : ""}`,
19
19
  priority: taskPriority(hasExternalSignal),
20
20
  tags: hasExternalSignal ? ["external_analyzer_signal"] : [],
21
+ status: "pending",
21
22
  });
22
23
  }
23
24
  }
@@ -12,12 +12,25 @@ function dedupeTasks(tasks) {
12
12
  }
13
13
  return deduped;
14
14
  }
15
+ function dedupeByScope(tasks) {
16
+ const seen = new Set();
17
+ const deduped = [];
18
+ for (const task of tasks) {
19
+ const signature = `${task.lens}:${[...task.file_paths].sort().join(",")}`;
20
+ if (seen.has(signature)) {
21
+ continue;
22
+ }
23
+ seen.add(signature);
24
+ deduped.push(task);
25
+ }
26
+ return deduped;
27
+ }
15
28
  export function buildRequeuePayload(matrix, criticalFlows, flowCoverage, externalAnalyzerResults) {
16
29
  const fileTasks = dedupeTasks(buildRequeueTasks(matrix, externalAnalyzerResults));
17
30
  const flowTasks = criticalFlows && flowCoverage
18
- ? dedupeTasks(buildFlowRequeueTasks(criticalFlows, flowCoverage, externalAnalyzerResults))
31
+ ? dedupeTasks(buildFlowRequeueTasks(criticalFlows, flowCoverage, matrix, externalAnalyzerResults))
19
32
  : [];
20
- const tasks = dedupeTasks([...fileTasks, ...flowTasks]);
33
+ const tasks = dedupeByScope(dedupeTasks([...fileTasks, ...flowTasks]));
21
34
  return {
22
35
  task_count: tasks.length,
23
36
  file_task_count: fileTasks.length,
@@ -1,2 +1,3 @@
1
- import type { AuditResult, CoverageMatrix } from "../types.js";
1
+ import type { AuditResult, AuditTask, CoverageMatrix } from "../types.js";
2
2
  export declare function ingestAuditResults(coverageMatrix: CoverageMatrix, results: AuditResult[]): CoverageMatrix;
3
+ export declare function updateAuditTaskStatuses(tasks: AuditTask[] | undefined, results: AuditResult[]): AuditTask[] | undefined;
@@ -12,3 +12,24 @@ export function ingestAuditResults(coverageMatrix, results) {
12
12
  applyReviewedRanges(matrix, reviewedRanges);
13
13
  return matrix;
14
14
  }
15
+ export function updateAuditTaskStatuses(tasks, results) {
16
+ if (!tasks) {
17
+ return undefined;
18
+ }
19
+ const completedTaskIds = new Set(results.map((result) => result.task_id));
20
+ const completedAt = new Date().toISOString();
21
+ return tasks.map((task) => {
22
+ if (completedTaskIds.has(task.task_id)) {
23
+ return {
24
+ ...task,
25
+ status: "complete",
26
+ completed_at: task.completed_at ?? completedAt,
27
+ completion_reason: task.completion_reason ?? "result_ingested",
28
+ };
29
+ }
30
+ return {
31
+ ...task,
32
+ status: task.status ?? "pending",
33
+ };
34
+ });
35
+ }
@@ -43,12 +43,21 @@ 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
63
  obligations.push(obligation("audit_results_ingested", has(bundle.audit_results) ? "present" : "missing"));
@@ -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,46 @@
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
+ return false;
24
+ }
25
+ export function autoCompleteTrivialCoverage(coverage, lineIndex, externalAnalyzerResults) {
26
+ const externalPaths = new Set((externalAnalyzerResults?.results ?? []).map((item) => item.path));
27
+ const skipped = [];
28
+ for (const file of coverage.files) {
29
+ if (file.audit_status === "excluded") {
30
+ continue;
31
+ }
32
+ if (!isTrivialAuditPath(file.path, lineIndex[file.path] ?? 0, externalPaths.has(file.path))) {
33
+ continue;
34
+ }
35
+ if (file.required_lenses.length === 0) {
36
+ continue;
37
+ }
38
+ file.completed_lenses = [...new Set(file.required_lenses)];
39
+ file.audit_status = "complete";
40
+ if (file.classification_status === "unclassified") {
41
+ file.classification_status = "classified";
42
+ }
43
+ skipped.push(file.path);
44
+ }
45
+ return skipped.sort();
46
+ }
@@ -20,12 +20,15 @@ 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
+ " reviewed_ranges: [{path, start, end, line_count}] covering what you read",
24
24
  " findings: array (empty if nothing found)",
25
+ " line_count must match the file's current total line count so ingestion can verify the cited ranges.",
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.",
29
32
  `Write the AuditResult[] JSON array to: ${task.audit_results_path}`,
30
33
  ];
31
34
  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,9 @@ 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
- ],
85
+ related_findings: finding.related_findings && finding.related_findings.length > 0
86
+ ? [...new Set(finding.related_findings)]
87
+ : undefined,
86
88
  });
87
89
  continue;
88
90
  }
@@ -108,13 +110,14 @@ export function mergeFindings(results, runtimeReport, externalAnalyzerResults) {
108
110
  ...analyzerEvidence,
109
111
  ]),
110
112
  ];
111
- existing.related_findings = [
112
- ...new Set([
113
- ...(existing.related_findings ?? []),
114
- ...(finding.related_findings ?? []),
115
- finding.id,
116
- ]),
117
- ];
113
+ const related = new Set([
114
+ ...(existing.related_findings ?? []),
115
+ ...(finding.related_findings ?? []),
116
+ existing.id,
117
+ finding.id,
118
+ ]);
119
+ existing.related_findings =
120
+ related.size > 0 ? [...related].sort() : undefined;
118
121
  }
119
122
  }
120
123
  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,5 +1,6 @@
1
1
  import { readOptionalJsonFile } from "../io/json.js";
2
2
  import { validateSessionConfig } from "../validation/sessionConfig.js";
3
+ import { writeJsonFile } from "../io/json.js";
3
4
  export function getSessionConfigPath(artifactsDir) {
4
5
  return `${artifactsDir}/session-config.json`;
5
6
  }
@@ -16,8 +17,9 @@ export async function loadSessionConfig(artifactsDir) {
16
17
  const configPath = getSessionConfigPath(artifactsDir);
17
18
  const rawConfig = await readOptionalJsonFile(configPath);
18
19
  if (rawConfig === undefined) {
19
- process.stderr.write(`[session-config] no session-config.json found at ${configPath}; using empty defaults\n`);
20
- return {};
20
+ const defaultConfig = { provider: "local-subprocess" };
21
+ await writeJsonFile(configPath, defaultConfig);
22
+ return defaultConfig;
21
23
  }
22
24
  const issues = validateSessionConfig(rawConfig);
23
25
  if (issues.length > 0) {
package/dist/types.d.ts CHANGED
@@ -47,6 +47,7 @@ export interface CoverageFileRecord {
47
47
  export interface CoverageMatrix {
48
48
  files: CoverageFileRecord[];
49
49
  }
50
+ export type AuditTaskStatus = "pending" | "complete";
50
51
  export interface AuditTask {
51
52
  task_id: string;
52
53
  unit_id: string;
@@ -62,6 +63,9 @@ export interface AuditTask {
62
63
  rationale: string;
63
64
  priority?: "high" | "medium" | "low";
64
65
  tags?: string[];
66
+ status?: AuditTaskStatus;
67
+ completed_at?: string;
68
+ completion_reason?: string;
65
69
  }
66
70
  export interface Finding {
67
71
  id: string;
@@ -94,6 +98,7 @@ export interface AuditResult {
94
98
  path: string;
95
99
  start: number;
96
100
  end: number;
101
+ line_count: number;
97
102
  }>;
98
103
  findings: Finding[];
99
104
  notes?: string[];
@@ -1,4 +1,4 @@
1
- import type { AuditResult, AuditTask } from "../types.js";
1
+ import type { AuditTask } from "../types.js";
2
2
  export type IssueSeverity = "error" | "warning";
3
3
  export interface AuditResultIssue {
4
4
  result_index: number;
@@ -7,5 +7,8 @@ export interface AuditResultIssue {
7
7
  field: string;
8
8
  message: string;
9
9
  }
10
- export declare function validateAuditResults(results: AuditResult[], tasks: AuditTask[]): AuditResultIssue[];
10
+ export interface ValidateAuditResultOptions {
11
+ lineIndex?: Record<string, number>;
12
+ }
13
+ export declare function validateAuditResults(results: unknown, tasks: AuditTask[], options?: ValidateAuditResultOptions): AuditResultIssue[];
11
14
  export declare function formatAuditResultIssues(issues: AuditResultIssue[]): string;