auditor-lambda 0.2.4 → 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.
- package/README.md +12 -0
- package/audit-code-wrapper-lib.mjs +7 -1
- package/dist/cli.js +324 -27
- package/dist/io/runArtifacts.d.ts +2 -1
- package/dist/io/runArtifacts.js +2 -1
- package/dist/orchestrator/flowRequeue.d.ts +2 -2
- package/dist/orchestrator/flowRequeue.js +15 -2
- package/dist/orchestrator/internalExecutors.js +34 -10
- package/dist/orchestrator/requeue.js +1 -0
- package/dist/orchestrator/requeueCommand.js +15 -2
- package/dist/orchestrator/resultIngestion.d.ts +2 -1
- package/dist/orchestrator/resultIngestion.js +21 -0
- package/dist/orchestrator/state.js +10 -1
- package/dist/orchestrator/taskBuilder.js +4 -2
- package/dist/orchestrator/trivialAudit.d.ts +4 -0
- package/dist/orchestrator/trivialAudit.js +46 -0
- package/dist/prompts/renderWorkerPrompt.js +5 -2
- package/dist/providers/spawnLoggedCommand.js +17 -0
- package/dist/reporting/mergeFindings.js +14 -11
- package/dist/reporting/rootCause.js +92 -9
- package/dist/supervisor/sessionConfig.js +4 -2
- package/dist/types.d.ts +5 -0
- package/dist/validation/auditResults.d.ts +5 -2
- package/dist/validation/auditResults.js +369 -42
- package/docs/artifacts.md +8 -1
- package/docs/contract.md +118 -27
- package/docs/field-trial-bug-report.md +237 -0
- package/docs/session-config.md +20 -1
- package/docs/usage.md +22 -0
- package/package.json +3 -1
- package/schemas/audit_result.schema.json +3 -2
- package/schemas/audit_task.schema.json +10 -0
- package/scripts/postinstall.mjs +19 -0
- package/skills/audit-code/audit-code.prompt.md +15 -11
|
@@ -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
|
-
|
|
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
|
|
27
|
-
|
|
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
|
|
36
|
-
|
|
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 (
|
|
45
|
-
issues
|
|
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
|
|
116
|
+
message: "affected_files must be a non-empty array.",
|
|
51
117
|
});
|
|
52
118
|
}
|
|
53
119
|
else {
|
|
54
|
-
for (let k = 0; k <
|
|
55
|
-
const
|
|
56
|
-
if (!
|
|
57
|
-
issues
|
|
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
|
-
|
|
68
|
-
|
|
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 —
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
- `
|
|
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
|
|
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
|
-
|
|
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
|
|
42
|
+
The versioned wrapper schema is:
|
|
46
43
|
|
|
47
44
|
```text
|
|
48
45
|
schemas/audit-code-v1alpha1.schema.json
|
|
49
46
|
```
|
|
50
47
|
|
|
51
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
86
|
-
-
|
|
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
|
-
|
|
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
|
|