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.
@@ -9,77 +9,193 @@ const REQUIRED_FINDING_FIELDS = [
9
9
  ];
10
10
  const VALID_SEVERITIES = new Set(["critical", "high", "medium", "low", "info"]);
11
11
  const VALID_CONFIDENCES = new Set(["high", "medium", "low"]);
12
+ const VALID_LENSES = new Set([
13
+ "correctness",
14
+ "architecture",
15
+ "maintainability",
16
+ "security",
17
+ "reliability",
18
+ "performance",
19
+ "data_integrity",
20
+ "tests",
21
+ "operability",
22
+ "config_deployment",
23
+ ]);
24
+ function pushIssue(issues, params) {
25
+ issues.push({
26
+ ...params,
27
+ severity: params.severity ?? "error",
28
+ });
29
+ }
30
+ function describeValue(value) {
31
+ if (Array.isArray(value)) {
32
+ return "array";
33
+ }
34
+ if (value === null) {
35
+ return "null";
36
+ }
37
+ return typeof value;
38
+ }
39
+ function isRecord(value) {
40
+ return typeof value === "object" && value !== null && !Array.isArray(value);
41
+ }
42
+ function isNonEmptyString(value) {
43
+ return typeof value === "string" && value.trim().length > 0;
44
+ }
45
+ function issueTaskId(record, resultIndex) {
46
+ const taskId = record.task_id;
47
+ return typeof taskId === "string" && taskId.trim().length > 0
48
+ ? taskId
49
+ : `result[${resultIndex}]`;
50
+ }
51
+ function validateRequiredStringField(value, label, taskId, resultIndex, issues) {
52
+ if (typeof value !== "string") {
53
+ pushIssue(issues, {
54
+ result_index: resultIndex,
55
+ task_id: taskId,
56
+ field: label,
57
+ message: `${label} must be a string, got ${describeValue(value)}.`,
58
+ });
59
+ return;
60
+ }
61
+ if (value.trim().length === 0) {
62
+ pushIssue(issues, {
63
+ result_index: resultIndex,
64
+ task_id: taskId,
65
+ field: label,
66
+ message: `${label} must not be empty.`,
67
+ });
68
+ }
69
+ }
12
70
  function validateFinding(finding, label, taskId, resultIndex) {
13
71
  const issues = [];
72
+ if (!isRecord(finding)) {
73
+ pushIssue(issues, {
74
+ result_index: resultIndex,
75
+ task_id: taskId,
76
+ field: label,
77
+ message: `${label} must be an object, got ${describeValue(finding)}.`,
78
+ });
79
+ return issues;
80
+ }
14
81
  for (const field of REQUIRED_FINDING_FIELDS) {
15
- const value = finding[field];
16
- if (value === undefined || value === null || String(value).trim() === "") {
17
- issues.push({
18
- result_index: resultIndex,
19
- task_id: taskId,
20
- severity: "error",
21
- field: `${label}.${field}`,
22
- message: `Required field '${field}' is missing or empty.`,
23
- });
24
- }
82
+ validateRequiredStringField(finding[field], `${label}.${field}`, taskId, resultIndex, issues);
25
83
  }
26
- if (finding.severity && !VALID_SEVERITIES.has(finding.severity)) {
27
- issues.push({
84
+ if (typeof finding.severity === "string" &&
85
+ !VALID_SEVERITIES.has(finding.severity)) {
86
+ pushIssue(issues, {
28
87
  result_index: resultIndex,
29
88
  task_id: taskId,
30
- severity: "error",
31
89
  field: `${label}.severity`,
32
90
  message: `Invalid severity '${finding.severity}'. Must be one of: ${[...VALID_SEVERITIES].join(", ")}.`,
33
91
  });
34
92
  }
35
- if (finding.confidence && !VALID_CONFIDENCES.has(finding.confidence)) {
36
- issues.push({
93
+ if (typeof finding.confidence === "string" &&
94
+ !VALID_CONFIDENCES.has(finding.confidence)) {
95
+ pushIssue(issues, {
37
96
  result_index: resultIndex,
38
97
  task_id: taskId,
39
- severity: "error",
40
98
  field: `${label}.confidence`,
41
99
  message: `Invalid confidence '${finding.confidence}'. Must be one of: ${[...VALID_CONFIDENCES].join(", ")}.`,
42
100
  });
43
101
  }
44
- if (!finding.affected_files || finding.affected_files.length === 0) {
45
- issues.push({
102
+ if (typeof finding.lens === "string" && !VALID_LENSES.has(finding.lens)) {
103
+ pushIssue(issues, {
104
+ result_index: resultIndex,
105
+ task_id: taskId,
106
+ field: `${label}.lens`,
107
+ message: `Invalid lens '${finding.lens}'. Must be one of: ${[...VALID_LENSES].join(", ")}.`,
108
+ });
109
+ }
110
+ const affectedFiles = finding.affected_files;
111
+ if (!Array.isArray(affectedFiles) || affectedFiles.length === 0) {
112
+ pushIssue(issues, {
46
113
  result_index: resultIndex,
47
114
  task_id: taskId,
48
- severity: "error",
49
115
  field: `${label}.affected_files`,
50
- message: "affected_files is empty at least one file location is required.",
116
+ message: "affected_files must be a non-empty array.",
51
117
  });
52
118
  }
53
119
  else {
54
- for (let k = 0; k < finding.affected_files.length; k++) {
55
- const af = finding.affected_files[k];
56
- if (!af.path?.trim()) {
57
- issues.push({
120
+ for (let k = 0; k < affectedFiles.length; k++) {
121
+ const item = affectedFiles[k];
122
+ if (!isRecord(item)) {
123
+ pushIssue(issues, {
124
+ result_index: resultIndex,
125
+ task_id: taskId,
126
+ field: `${label}.affected_files[${k}]`,
127
+ message: `affected_files[${k}] must be an object, got ${describeValue(item)}.`,
128
+ });
129
+ continue;
130
+ }
131
+ if (!isNonEmptyString(item.path)) {
132
+ pushIssue(issues, {
58
133
  result_index: resultIndex,
59
134
  task_id: taskId,
60
- severity: "error",
61
135
  field: `${label}.affected_files[${k}].path`,
62
136
  message: "affected_files entry has an empty path.",
63
137
  });
64
138
  }
139
+ if (item.line_start !== undefined &&
140
+ !Number.isInteger(item.line_start)) {
141
+ pushIssue(issues, {
142
+ result_index: resultIndex,
143
+ task_id: taskId,
144
+ field: `${label}.affected_files[${k}].line_start`,
145
+ message: `affected_files[${k}].line_start must be an integer, got ${describeValue(item.line_start)}.`,
146
+ });
147
+ }
148
+ if (item.line_end !== undefined &&
149
+ !Number.isInteger(item.line_end)) {
150
+ pushIssue(issues, {
151
+ result_index: resultIndex,
152
+ task_id: taskId,
153
+ field: `${label}.affected_files[${k}].line_end`,
154
+ message: `affected_files[${k}].line_end must be an integer, got ${describeValue(item.line_end)}.`,
155
+ });
156
+ }
157
+ if (Number.isInteger(item.line_start) &&
158
+ Number.isInteger(item.line_end) &&
159
+ Number(item.line_start) > Number(item.line_end)) {
160
+ pushIssue(issues, {
161
+ result_index: resultIndex,
162
+ task_id: taskId,
163
+ field: `${label}.affected_files[${k}]`,
164
+ message: "affected_files line_start must be less than or equal to line_end.",
165
+ });
166
+ }
65
167
  }
66
168
  }
67
- if (!finding.evidence || finding.evidence.length === 0) {
68
- issues.push({
169
+ const evidence = finding.evidence;
170
+ if (!Array.isArray(evidence) || evidence.length === 0) {
171
+ pushIssue(issues, {
69
172
  result_index: resultIndex,
70
173
  task_id: taskId,
71
- severity: "error",
72
174
  field: `${label}.evidence`,
73
- message: "evidence is empty — at least one quoted or referenced excerpt from the reviewed file is required for every finding.",
175
+ message: "evidence is empty — provide an array of plain strings such as \"src/foo.ts:42 - variable overwritten before use\".",
74
176
  });
75
177
  }
76
178
  else {
77
- const hasSubstantiveEntry = finding.evidence.some((e) => e.trim().length > 0);
179
+ let hasSubstantiveEntry = false;
180
+ for (let k = 0; k < evidence.length; k++) {
181
+ const entry = evidence[k];
182
+ if (typeof entry !== "string") {
183
+ pushIssue(issues, {
184
+ result_index: resultIndex,
185
+ task_id: taskId,
186
+ field: `${label}.evidence[${k}]`,
187
+ message: `evidence[${k}] must be a string, got ${describeValue(entry)}.`,
188
+ });
189
+ continue;
190
+ }
191
+ if (entry.trim().length > 0) {
192
+ hasSubstantiveEntry = true;
193
+ }
194
+ }
78
195
  if (!hasSubstantiveEntry) {
79
- issues.push({
196
+ pushIssue(issues, {
80
197
  result_index: resultIndex,
81
198
  task_id: taskId,
82
- severity: "error",
83
199
  field: `${label}.evidence`,
84
200
  message: "All evidence entries are empty strings.",
85
201
  });
@@ -87,26 +203,237 @@ function validateFinding(finding, label, taskId, resultIndex) {
87
203
  }
88
204
  return issues;
89
205
  }
90
- export function validateAuditResults(results, tasks) {
206
+ function coversAffectedSpan(ranges, path, start, end) {
207
+ return ranges.some((range) => range.path === path &&
208
+ range.start <= start &&
209
+ range.end >= end);
210
+ }
211
+ export function validateAuditResults(results, tasks, options = {}) {
91
212
  const issues = [];
92
- const taskMap = new Map(tasks.map((t) => [t.task_id, t]));
213
+ const taskMap = new Map(tasks.map((task) => [task.task_id, task]));
214
+ if (!Array.isArray(results)) {
215
+ pushIssue(issues, {
216
+ result_index: -1,
217
+ task_id: "results",
218
+ field: "results",
219
+ message: `Audit results payload must be a JSON array, got ${describeValue(results)}.`,
220
+ });
221
+ return issues;
222
+ }
93
223
  for (let i = 0; i < results.length; i++) {
94
224
  const result = results[i];
95
- const taskId = result.task_id ?? `result[${i}]`;
96
- if (!result.reviewed_ranges || result.reviewed_ranges.length === 0) {
97
- issues.push({
225
+ if (!isRecord(result)) {
226
+ pushIssue(issues, {
227
+ result_index: i,
228
+ task_id: `result[${i}]`,
229
+ field: `results[${i}]`,
230
+ message: `Each audit result must be an object, got ${describeValue(result)}.`,
231
+ });
232
+ continue;
233
+ }
234
+ const taskId = issueTaskId(result, i);
235
+ const task = taskMap.get(taskId);
236
+ validateRequiredStringField(result.task_id, "task_id", taskId, i, issues);
237
+ validateRequiredStringField(result.unit_id, "unit_id", taskId, i, issues);
238
+ validateRequiredStringField(result.pass_id, "pass_id", taskId, i, issues);
239
+ validateRequiredStringField(result.lens, "lens", taskId, i, issues);
240
+ if (typeof result.lens === "string" &&
241
+ !VALID_LENSES.has(result.lens)) {
242
+ pushIssue(issues, {
243
+ result_index: i,
244
+ task_id: taskId,
245
+ field: "lens",
246
+ message: `Invalid lens '${result.lens}'. Must be one of: ${[...VALID_LENSES].join(", ")}.`,
247
+ });
248
+ }
249
+ if (tasks.length > 0 && !task) {
250
+ pushIssue(issues, {
251
+ result_index: i,
252
+ task_id: taskId,
253
+ field: "task_id",
254
+ message: `Unknown task_id '${taskId}'.`,
255
+ });
256
+ }
257
+ const reviewedRanges = result.reviewed_ranges;
258
+ const normalizedReviewedRanges = [];
259
+ if (!Array.isArray(reviewedRanges) || reviewedRanges.length === 0) {
260
+ pushIssue(issues, {
98
261
  result_index: i,
99
262
  task_id: taskId,
100
- severity: "error",
101
263
  field: "reviewed_ranges",
102
- message: "reviewed_ranges is empty — no proof of file reading was recorded. " +
103
- "Each result must include the line ranges actually read.",
264
+ message: "reviewed_ranges is empty — no proof of file reading was recorded. Each result must include the line ranges actually read.",
104
265
  });
105
266
  }
106
- for (let j = 0; j < (result.findings ?? []).length; j++) {
107
- const finding = result.findings[j];
267
+ else {
268
+ for (let j = 0; j < reviewedRanges.length; j++) {
269
+ const range = reviewedRanges[j];
270
+ if (!isRecord(range)) {
271
+ pushIssue(issues, {
272
+ result_index: i,
273
+ task_id: taskId,
274
+ field: `reviewed_ranges[${j}]`,
275
+ message: `reviewed_ranges[${j}] must be an object, got ${describeValue(range)}.`,
276
+ });
277
+ continue;
278
+ }
279
+ if (!isNonEmptyString(range.path)) {
280
+ pushIssue(issues, {
281
+ result_index: i,
282
+ task_id: taskId,
283
+ field: `reviewed_ranges[${j}].path`,
284
+ message: "reviewed_ranges entry has an empty path.",
285
+ });
286
+ }
287
+ else if (task && !task.file_paths.includes(range.path)) {
288
+ pushIssue(issues, {
289
+ result_index: i,
290
+ task_id: taskId,
291
+ severity: "warning",
292
+ field: `reviewed_ranges[${j}].path`,
293
+ message: `reviewed_ranges path '${range.path}' is not listed in the task file_paths.`,
294
+ });
295
+ }
296
+ if (!Number.isInteger(range.start)) {
297
+ pushIssue(issues, {
298
+ result_index: i,
299
+ task_id: taskId,
300
+ field: `reviewed_ranges[${j}].start`,
301
+ message: `reviewed_ranges[${j}].start must be an integer, got ${describeValue(range.start)}.`,
302
+ });
303
+ }
304
+ if (!Number.isInteger(range.end)) {
305
+ pushIssue(issues, {
306
+ result_index: i,
307
+ task_id: taskId,
308
+ field: `reviewed_ranges[${j}].end`,
309
+ message: `reviewed_ranges[${j}].end must be an integer, got ${describeValue(range.end)}.`,
310
+ });
311
+ }
312
+ if (!Number.isInteger(range.line_count)) {
313
+ pushIssue(issues, {
314
+ result_index: i,
315
+ task_id: taskId,
316
+ field: `reviewed_ranges[${j}].line_count`,
317
+ message: `reviewed_ranges[${j}].line_count must be an integer, got ${describeValue(range.line_count)}.`,
318
+ });
319
+ }
320
+ if (Number.isInteger(range.start) &&
321
+ Number.isInteger(range.end) &&
322
+ Number(range.start) > Number(range.end)) {
323
+ pushIssue(issues, {
324
+ result_index: i,
325
+ task_id: taskId,
326
+ field: `reviewed_ranges[${j}]`,
327
+ message: "reviewed_ranges start must be less than or equal to end.",
328
+ });
329
+ }
330
+ if (Number.isInteger(range.line_count) &&
331
+ Number(range.line_count) <= 0) {
332
+ pushIssue(issues, {
333
+ result_index: i,
334
+ task_id: taskId,
335
+ field: `reviewed_ranges[${j}].line_count`,
336
+ message: "reviewed_ranges line_count must be greater than zero.",
337
+ });
338
+ }
339
+ if (Number.isInteger(range.start) &&
340
+ Number(range.start) <= 0) {
341
+ pushIssue(issues, {
342
+ result_index: i,
343
+ task_id: taskId,
344
+ field: `reviewed_ranges[${j}].start`,
345
+ message: "reviewed_ranges start must be greater than zero.",
346
+ });
347
+ }
348
+ if (Number.isInteger(range.end) &&
349
+ Number(range.end) <= 0) {
350
+ pushIssue(issues, {
351
+ result_index: i,
352
+ task_id: taskId,
353
+ field: `reviewed_ranges[${j}].end`,
354
+ message: "reviewed_ranges end must be greater than zero.",
355
+ });
356
+ }
357
+ if (Number.isInteger(range.end) &&
358
+ Number.isInteger(range.line_count) &&
359
+ Number(range.end) > Number(range.line_count)) {
360
+ pushIssue(issues, {
361
+ result_index: i,
362
+ task_id: taskId,
363
+ field: `reviewed_ranges[${j}]`,
364
+ message: "reviewed_ranges end must not exceed the declared file line_count.",
365
+ });
366
+ }
367
+ const expectedLineCount = typeof range.path === "string"
368
+ ? options.lineIndex?.[range.path]
369
+ : undefined;
370
+ if (Number.isInteger(range.line_count) &&
371
+ typeof expectedLineCount === "number" &&
372
+ Number(range.line_count) !== expectedLineCount) {
373
+ pushIssue(issues, {
374
+ result_index: i,
375
+ task_id: taskId,
376
+ field: `reviewed_ranges[${j}].line_count`,
377
+ message: `reviewed_ranges[${j}].line_count must match the current file line count for '${range.path}' ` +
378
+ `(expected ${expectedLineCount}, got ${range.line_count}).`,
379
+ });
380
+ }
381
+ if (isNonEmptyString(range.path) &&
382
+ Number.isInteger(range.start) &&
383
+ Number.isInteger(range.end) &&
384
+ Number.isInteger(range.line_count) &&
385
+ Number(range.start) > 0 &&
386
+ Number(range.end) > 0 &&
387
+ Number(range.start) <= Number(range.end) &&
388
+ Number(range.end) <= Number(range.line_count)) {
389
+ normalizedReviewedRanges.push({
390
+ path: range.path,
391
+ start: Number(range.start),
392
+ end: Number(range.end),
393
+ line_count: Number(range.line_count),
394
+ });
395
+ }
396
+ }
397
+ }
398
+ const findings = result.findings;
399
+ if (!Array.isArray(findings)) {
400
+ pushIssue(issues, {
401
+ result_index: i,
402
+ task_id: taskId,
403
+ field: "findings",
404
+ message: `findings must be an array, got ${describeValue(findings)}.`,
405
+ });
406
+ continue;
407
+ }
408
+ for (let j = 0; j < findings.length; j++) {
108
409
  const label = `findings[${j}]`;
410
+ const finding = findings[j];
109
411
  issues.push(...validateFinding(finding, label, taskId, i));
412
+ if (!isRecord(finding) || !Array.isArray(finding.affected_files)) {
413
+ continue;
414
+ }
415
+ for (let k = 0; k < finding.affected_files.length; k++) {
416
+ const affected = finding.affected_files[k];
417
+ if (!isRecord(affected) || !isNonEmptyString(affected.path)) {
418
+ continue;
419
+ }
420
+ if (!Number.isInteger(affected.line_start)) {
421
+ continue;
422
+ }
423
+ const start = Number(affected.line_start);
424
+ const end = Number.isInteger(affected.line_end)
425
+ ? Number(affected.line_end)
426
+ : start;
427
+ if (!coversAffectedSpan(normalizedReviewedRanges, affected.path, start, end)) {
428
+ pushIssue(issues, {
429
+ result_index: i,
430
+ task_id: taskId,
431
+ field: `${label}.affected_files[${k}]`,
432
+ message: `affected_files line span ${affected.path}:${start}-${end} falls outside the declared reviewed_ranges. ` +
433
+ "Expand reviewed_ranges to cover the cited lines or fix the affected_files location.",
434
+ });
435
+ }
436
+ }
110
437
  }
111
438
  }
112
439
  return issues;
package/docs/artifacts.md CHANGED
@@ -16,7 +16,8 @@ These JSON artifacts are the stable contract between deterministic tooling and L
16
16
  - `runtime_validation_tasks.json`
17
17
  - `runtime_validation_report.json`
18
18
  - `external_analyzer_results.json`
19
- - `audit_results.json`
19
+ - `audit_tasks.json`
20
+ - `audit_results.jsonl`
20
21
  - `requeue_tasks.json`
21
22
  - `merged_findings.json`
22
23
  - `root_cause_clusters.json`
@@ -67,3 +68,9 @@ External analyzer results now also influence:
67
68
  - dedicated analyzer follow-up tasks
68
69
  - requeue priority
69
70
  - synthesis evidence and summaries
71
+
72
+ ## Key schema notes
73
+
74
+ - `audit_tasks.json`: each task already contains the resolved `file_paths` it covers. Use `audit-code explain-task <task_id>` when you want that task joined back to current `coverage_matrix.json` state.
75
+ - `coverage_matrix.json`: each file records `classification_status`, `audit_status`, `required_lenses`, and `completed_lenses`.
76
+ - `synthesis_report.json`: this is the top-level synthesis artifact. It includes `merged_findings`, semantic `root_cause_clusters`, and `work_blocks`; `work_blocks` are the primary remediation grouping.
package/docs/contract.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # audit-code response contract
2
2
 
3
- This document describes the backend fallback JSON response contract for the `audit-code` wrapper.
3
+ This document describes the backend fallback JSON contract for `audit-code`.
4
4
 
5
5
  The canonical product remains `/audit-code` in conversation.
6
6
 
@@ -12,16 +12,13 @@ Repo-local fallback command from the target repository root:
12
12
  audit-code
13
13
  ```
14
14
 
15
- Installed helper for locating the packaged conversation prompt asset:
15
+ Useful helpers:
16
16
 
17
17
  ```bash
18
18
  audit-code prompt-path
19
- ```
20
-
21
- Installed helper for validating the current backend artifact bundle:
22
-
23
- ```bash
24
19
  audit-code validate
20
+ audit-code explain-task <task_id>
21
+ audit-code --batch-results /path/to/audit-results-dir
25
22
  ```
26
23
 
27
24
  Repository-local wrapper equivalent:
@@ -42,30 +39,21 @@ Consumers should verify this value before assuming the response shape.
42
39
 
43
40
  ## Source of truth
44
41
 
45
- The versioned JSON schema is:
42
+ The versioned wrapper schema is:
46
43
 
47
44
  ```text
48
45
  schemas/audit-code-v1alpha1.schema.json
49
46
  ```
50
47
 
51
- Product tests validate live wrapper output against that schema.
52
-
53
- ## Reproducible installed-command smoke check
54
-
55
- From the repository root:
56
-
57
- ```bash
58
- npm install
59
- npm run build
60
- npm link
61
- npm run smoke:linked-audit-code
62
- ```
48
+ Artifact-level schemas also live in `schemas/`, including:
63
49
 
64
- This exercises the installed backend fallback command end-to-end and validates the emitted JSON against the versioned schema.
50
+ - `schemas/audit_task.schema.json`
51
+ - `schemas/audit_result.schema.json`
52
+ - `schemas/finding.schema.json`
65
53
 
66
- ## Top-level fields
54
+ ## Top-level wrapper fields
67
55
 
68
- The current v1alpha1 contract includes these top-level fields:
56
+ The current v1alpha1 wrapper contract includes:
69
57
 
70
58
  - `contract_version`
71
59
  - `audit_state`
@@ -79,11 +67,11 @@ The current v1alpha1 contract includes these top-level fields:
79
67
 
80
68
  `handoff` is a companion operator-context object. It includes:
81
69
 
82
- - current top-level status
83
70
  - repo and artifacts paths
84
71
  - pending obligations
85
- - suggested evidence-import paths and commands when manual continuation is required
86
- - stable paths to companion handoff files under `.audit-artifacts`
72
+ - suggested evidence-import paths and commands
73
+ - provider guidance
74
+ - stable paths to operator handoff artifacts
87
75
 
88
76
  ## Terminal states
89
77
 
@@ -102,7 +90,110 @@ When the wrapper emits a response, it also refreshes:
102
90
  - `.audit-artifacts/operator-handoff.json`
103
91
  - `.audit-artifacts/operator-handoff.md`
104
92
 
105
- Those files mirror the structured `handoff` guidance in machine-readable and human-readable forms.
93
+ ## Artifact contract
94
+
95
+ These are the primary machine-readable artifacts:
96
+
97
+ - `repo_manifest.json`
98
+ - `file_disposition.json`
99
+ - `unit_manifest.json`
100
+ - `critical_flows.json`
101
+ - `coverage_matrix.json`
102
+ - `runtime_validation_tasks.json`
103
+ - `runtime_validation_report.json`
104
+ - `audit_tasks.json`
105
+ - `audit_results.jsonl`
106
+ - `requeue_tasks.json`
107
+ - `merged_findings.json`
108
+ - `root_cause_clusters.json`
109
+ - `synthesis_report.json`
110
+
111
+ Important shape notes:
112
+
113
+ - `audit_tasks.json`: each task already contains its resolved `file_paths`.
114
+ - `coverage_matrix.json`: each file records `classification_status`, `audit_status`, `required_lenses`, and `completed_lenses`.
115
+ - `synthesis_report.json`: includes `merged_findings`, semantic `root_cause_clusters`, and `work_blocks`.
116
+
117
+ ## Task id conventions
118
+
119
+ Current task ids follow a few stable patterns:
120
+
121
+ - Standard unit/lens task: `<unit_id>:<lens>`
122
+ - Large-file split task: `<unit_id>:<lens>:<file_path>`
123
+ - External analyzer follow-up: `analyzer:<tool>:<lens>:<path>:<result_id>`
124
+ - Requeue task: `requeue:<lens>:<path>`
125
+
126
+ If you need the resolved task payload plus current coverage state, use:
127
+
128
+ ```bash
129
+ audit-code explain-task <task_id>
130
+ ```
131
+
132
+ ## AuditResult contract
133
+
134
+ `AuditResult.findings[].evidence` is an array of plain strings only. Use entries like:
135
+
136
+ ```text
137
+ src/foo.ts:42 - variable overwritten before use
138
+ ```
139
+
140
+ `reviewed_ranges` now carries mechanical verification metadata:
141
+
142
+ - `path`
143
+ - `start`
144
+ - `end`
145
+ - `line_count`
146
+
147
+ `line_count` must match the current total line count of the file. Ingestion also checks that any cited `affected_files` line span falls inside the declared `reviewed_ranges`.
148
+
149
+ Worked example:
150
+
151
+ ```json
152
+ [
153
+ {
154
+ "task_id": "src-api-auth:security",
155
+ "unit_id": "src-api-auth",
156
+ "pass_id": "pass:security",
157
+ "lens": "security",
158
+ "agent_role": "security-auditor",
159
+ "reviewed_ranges": [
160
+ {
161
+ "path": "src/api/auth.ts",
162
+ "start": 1,
163
+ "end": 48,
164
+ "line_count": 48
165
+ }
166
+ ],
167
+ "findings": [
168
+ {
169
+ "id": "finding-auth-1",
170
+ "title": "Authentication failures are not logged",
171
+ "category": "security",
172
+ "severity": "medium",
173
+ "confidence": "high",
174
+ "lens": "security",
175
+ "summary": "Rejected authentication attempts bypass structured audit logging.",
176
+ "affected_files": [
177
+ {
178
+ "path": "src/api/auth.ts",
179
+ "line_start": 18,
180
+ "line_end": 26
181
+ }
182
+ ],
183
+ "evidence": [
184
+ "src/api/auth.ts:18 - failure branch returns without emitting audit telemetry"
185
+ ]
186
+ }
187
+ ],
188
+ "notes": [
189
+ "Reviewed the entire file under the security lens."
190
+ ],
191
+ "requires_followup": false
192
+ }
193
+ ]
194
+ ```
195
+
196
+ Workers should validate against these schemas before submission instead of discovering shape errors during ingestion.
106
197
 
107
198
  ## Audit state shape
108
199