auditor-lambda 0.2.6 → 0.2.9
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 +29 -7
- package/audit-code-wrapper-lib.mjs +1605 -330
- package/dist/adapters/eslint.js +9 -5
- package/dist/cli.d.ts +42 -1
- package/dist/cli.js +192 -80
- package/dist/coverage.d.ts +2 -2
- package/dist/coverage.js +5 -5
- package/dist/extractors/bucketing.d.ts +4 -0
- package/dist/extractors/bucketing.js +6 -2
- package/dist/extractors/disposition.d.ts +4 -0
- package/dist/extractors/disposition.js +15 -2
- package/dist/extractors/fileInventory.js +24 -28
- package/dist/extractors/flows.d.ts +5 -0
- package/dist/extractors/flows.js +25 -39
- package/dist/extractors/pathPatterns.d.ts +13 -3
- package/dist/extractors/pathPatterns.js +116 -53
- package/dist/extractors/risk.js +7 -1
- package/dist/extractors/surfaces.d.ts +4 -0
- package/dist/extractors/surfaces.js +11 -11
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/io/artifacts.d.ts +59 -44
- package/dist/io/artifacts.js +80 -120
- package/dist/io/json.d.ts +2 -0
- package/dist/io/json.js +65 -19
- package/dist/io/runArtifacts.d.ts +2 -1
- package/dist/io/runArtifacts.js +44 -7
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +579 -0
- package/dist/orchestrator/advance.js +84 -56
- package/dist/orchestrator/dependencyMap.js +9 -13
- package/dist/orchestrator/executors.js +7 -2
- package/dist/orchestrator/flowCoverage.js +11 -5
- package/dist/orchestrator/flowPlanning.d.ts +7 -2
- package/dist/orchestrator/flowPlanning.js +46 -21
- package/dist/orchestrator/flowRequeue.js +29 -9
- package/dist/orchestrator/internalExecutors.d.ts +2 -1
- package/dist/orchestrator/internalExecutors.js +130 -69
- package/dist/orchestrator/planning.js +25 -3
- package/dist/orchestrator/requeue.js +20 -5
- package/dist/orchestrator/resultIngestion.js +5 -6
- package/dist/orchestrator/runtimeValidation.d.ts +7 -2
- package/dist/orchestrator/runtimeValidation.js +61 -49
- package/dist/orchestrator/runtimeValidationUpdate.js +2 -4
- package/dist/orchestrator/state.js +18 -13
- package/dist/orchestrator/taskBuilder.d.ts +4 -2
- package/dist/orchestrator/taskBuilder.js +153 -52
- package/dist/orchestrator/trivialAudit.js +8 -5
- package/dist/orchestrator/unitBuilder.d.ts +3 -1
- package/dist/orchestrator/unitBuilder.js +24 -16
- package/dist/prompts/renderWorkerPrompt.d.ts +1 -1
- package/dist/prompts/renderWorkerPrompt.js +19 -10
- package/dist/providers/claudeCodeProvider.d.ts +4 -1
- package/dist/providers/claudeCodeProvider.js +8 -5
- package/dist/providers/localSubprocessProvider.d.ts +4 -0
- package/dist/providers/localSubprocessProvider.js +7 -2
- package/dist/providers/spawnLoggedCommand.d.ts +9 -1
- package/dist/providers/spawnLoggedCommand.js +77 -29
- package/dist/reporting/mergeFindings.js +0 -11
- package/dist/reporting/synthesis.d.ts +26 -21
- package/dist/reporting/synthesis.js +97 -61
- package/dist/reporting/workBlocks.d.ts +12 -3
- package/dist/reporting/workBlocks.js +124 -70
- package/dist/supervisor/operatorHandoff.js +48 -18
- package/dist/supervisor/runLedger.d.ts +1 -1
- package/dist/supervisor/runLedger.js +112 -5
- package/dist/supervisor/sessionConfig.js +10 -10
- package/dist/types/externalAnalyzer.d.ts +3 -0
- package/dist/types/flowCoverage.d.ts +5 -1
- package/dist/types/flowCoverage.js +5 -1
- package/dist/types/flows.d.ts +6 -0
- package/dist/types/flows.js +1 -1
- package/dist/types/runLedger.d.ts +5 -1
- package/dist/types/runLedger.js +6 -1
- package/dist/types/runtimeValidation.d.ts +13 -3
- package/dist/types/runtimeValidation.js +16 -1
- package/dist/types/sessionConfig.d.ts +15 -2
- package/dist/types/sessionConfig.js +15 -1
- package/dist/types/surfaces.d.ts +4 -1
- package/dist/types/surfaces.js +1 -1
- package/dist/types/workerSession.d.ts +9 -0
- package/dist/types/workerSession.js +5 -1
- package/dist/types.d.ts +4 -7
- package/dist/validation/artifacts.d.ts +1 -1
- package/dist/validation/artifacts.js +33 -20
- package/dist/validation/auditResults.d.ts +2 -2
- package/dist/validation/auditResults.js +71 -114
- package/dist/validation/basic.d.ts +9 -1
- package/dist/validation/basic.js +40 -3
- package/dist/validation/sessionConfig.d.ts +4 -2
- package/dist/validation/sessionConfig.js +62 -15
- package/docs/agent-integrations.md +67 -38
- package/docs/artifacts.md +16 -56
- package/docs/bootstrap-install.md +60 -30
- package/docs/contract.md +22 -205
- package/docs/next-steps.md +76 -44
- package/docs/packaging.md +27 -3
- package/docs/product-direction.md +22 -0
- package/docs/production-launch-bar.md +4 -2
- package/docs/production-readiness.md +9 -5
- package/docs/releasing.md +98 -0
- package/docs/remediation-baseline.md +75 -0
- package/docs/run-flow.md +23 -11
- package/docs/session-config.md +50 -5
- package/docs/supervisor.md +7 -0
- package/docs/workflow-refactor-brief.md +177 -0
- package/package.json +4 -1
- package/schemas/audit_result.schema.json +8 -7
- package/schemas/audit_task.schema.json +3 -1
- package/schemas/coverage_matrix.schema.json +3 -3
- package/schemas/critical_flows.schema.json +6 -2
- package/schemas/file_disposition.schema.json +2 -2
- package/schemas/finding.schema.json +9 -4
- package/schemas/flow_coverage.schema.json +2 -2
- package/schemas/repo_manifest.schema.json +4 -4
- package/schemas/risk_register.schema.json +2 -2
- package/schemas/runtime_validation_report.schema.json +3 -3
- package/schemas/runtime_validation_tasks.schema.json +8 -2
- package/schemas/surface_manifest.schema.json +6 -3
- package/schemas/unit_manifest.schema.json +3 -2
- package/skills/audit-code/SKILL.md +16 -2
- package/skills/audit-code/audit-code.prompt.md +5 -8
- package/schemas/merged_findings.schema.json +0 -19
- package/schemas/root_cause_clusters.schema.json +0 -28
- package/schemas/synthesis_report.schema.json +0 -61
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { requireKeys } from "./basic.js";
|
|
1
|
+
import { pushValidationIssue, requireKeys, } from "./basic.js";
|
|
2
2
|
function pushIssue(issues, path, message) {
|
|
3
|
-
issues
|
|
3
|
+
pushValidationIssue(issues, path, message);
|
|
4
|
+
}
|
|
5
|
+
function asArray(value) {
|
|
6
|
+
return Array.isArray(value) ? value : [];
|
|
4
7
|
}
|
|
5
8
|
export function validateArtifactBundle(bundle) {
|
|
6
9
|
const issues = [];
|
|
@@ -37,14 +40,24 @@ export function validateArtifactBundle(bundle) {
|
|
|
37
40
|
if (bundle.external_analyzer_results) {
|
|
38
41
|
issues.push(...requireKeys(bundle.external_analyzer_results, "external_analyzer_results", ["tool", "results"]));
|
|
39
42
|
}
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
43
|
+
const repoManifestFiles = asArray(bundle.repo_manifest?.files);
|
|
44
|
+
const fileDispositionEntries = asArray(bundle.file_disposition?.files);
|
|
45
|
+
const unitManifestUnits = asArray(bundle.unit_manifest?.units);
|
|
46
|
+
const criticalFlows = asArray(bundle.critical_flows?.flows);
|
|
47
|
+
const flowCoverageEntries = asArray(bundle.flow_coverage?.flows);
|
|
48
|
+
const riskRegisterItems = asArray(bundle.risk_register?.items);
|
|
49
|
+
const surfaceEntries = asArray(bundle.surface_manifest?.surfaces);
|
|
50
|
+
const runtimeValidationTasks = asArray(bundle.runtime_validation_tasks?.tasks);
|
|
51
|
+
const runtimeValidationResults = asArray(bundle.runtime_validation_report?.results);
|
|
52
|
+
const externalAnalyzerResults = asArray(bundle.external_analyzer_results?.results);
|
|
53
|
+
const coverageFiles = asArray(bundle.coverage_matrix?.files);
|
|
54
|
+
const repoPaths = new Set(repoManifestFiles.map((file) => file.path));
|
|
55
|
+
const dispositionMap = new Map(fileDispositionEntries.map((item) => [item.path, item.status]));
|
|
56
|
+
const unitIds = new Set(unitManifestUnits.map((unit) => unit.unit_id));
|
|
57
|
+
const flowIds = new Set(criticalFlows.map((flow) => flow.id));
|
|
58
|
+
const runtimeTaskIds = new Set(runtimeValidationTasks.map((task) => task.id));
|
|
46
59
|
if (bundle.repo_manifest && bundle.coverage_matrix) {
|
|
47
|
-
const coveragePaths = new Set(
|
|
60
|
+
const coveragePaths = new Set(coverageFiles.map((file) => file.path));
|
|
48
61
|
for (const path of repoPaths) {
|
|
49
62
|
if (!coveragePaths.has(path)) {
|
|
50
63
|
pushIssue(issues, "coverage_matrix", `Missing coverage entry for ${path}`);
|
|
@@ -52,7 +65,7 @@ export function validateArtifactBundle(bundle) {
|
|
|
52
65
|
}
|
|
53
66
|
}
|
|
54
67
|
if (bundle.repo_manifest && bundle.file_disposition) {
|
|
55
|
-
const dispositionPaths = new Set(
|
|
68
|
+
const dispositionPaths = new Set(fileDispositionEntries.map((file) => file.path));
|
|
56
69
|
for (const path of repoPaths) {
|
|
57
70
|
if (!dispositionPaths.has(path)) {
|
|
58
71
|
pushIssue(issues, "file_disposition", `Missing disposition entry for ${path}`);
|
|
@@ -60,7 +73,7 @@ export function validateArtifactBundle(bundle) {
|
|
|
60
73
|
}
|
|
61
74
|
}
|
|
62
75
|
if (bundle.unit_manifest) {
|
|
63
|
-
for (const unit of
|
|
76
|
+
for (const unit of unitManifestUnits) {
|
|
64
77
|
if (unit.files.length === 0) {
|
|
65
78
|
pushIssue(issues, `unit_manifest:${unit.unit_id}`, "Unit has no files");
|
|
66
79
|
}
|
|
@@ -79,7 +92,7 @@ export function validateArtifactBundle(bundle) {
|
|
|
79
92
|
}
|
|
80
93
|
}
|
|
81
94
|
if (bundle.coverage_matrix && bundle.unit_manifest) {
|
|
82
|
-
for (const file of
|
|
95
|
+
for (const file of coverageFiles) {
|
|
83
96
|
if (!repoPaths.has(file.path)) {
|
|
84
97
|
pushIssue(issues, "coverage_matrix", `Coverage contains unknown file ${file.path}`);
|
|
85
98
|
}
|
|
@@ -103,7 +116,7 @@ export function validateArtifactBundle(bundle) {
|
|
|
103
116
|
}
|
|
104
117
|
}
|
|
105
118
|
if (bundle.critical_flows) {
|
|
106
|
-
for (const flow of
|
|
119
|
+
for (const flow of criticalFlows) {
|
|
107
120
|
if (flow.paths.length === 0) {
|
|
108
121
|
pushIssue(issues, `critical_flows:${flow.id}`, "Flow has no paths");
|
|
109
122
|
}
|
|
@@ -122,7 +135,7 @@ export function validateArtifactBundle(bundle) {
|
|
|
122
135
|
}
|
|
123
136
|
}
|
|
124
137
|
if (bundle.flow_coverage && bundle.critical_flows) {
|
|
125
|
-
for (const flow of
|
|
138
|
+
for (const flow of flowCoverageEntries) {
|
|
126
139
|
if (!flowIds.has(flow.flow_id)) {
|
|
127
140
|
pushIssue(issues, `flow_coverage:${flow.flow_id}`, `Flow coverage references unknown flow ${flow.flow_id}`);
|
|
128
141
|
}
|
|
@@ -143,15 +156,15 @@ export function validateArtifactBundle(bundle) {
|
|
|
143
156
|
}
|
|
144
157
|
}
|
|
145
158
|
if (bundle.risk_register && bundle.unit_manifest) {
|
|
146
|
-
const riskUnitIds = new Set(
|
|
147
|
-
for (const unit of
|
|
159
|
+
const riskUnitIds = new Set(riskRegisterItems.map((item) => item.unit_id));
|
|
160
|
+
for (const unit of unitManifestUnits) {
|
|
148
161
|
if (!riskUnitIds.has(unit.unit_id)) {
|
|
149
162
|
pushIssue(issues, "risk_register", `Missing risk entry for unit ${unit.unit_id}`);
|
|
150
163
|
}
|
|
151
164
|
}
|
|
152
165
|
}
|
|
153
166
|
if (bundle.surface_manifest) {
|
|
154
|
-
for (const surface of
|
|
167
|
+
for (const surface of surfaceEntries) {
|
|
155
168
|
if (!repoPaths.has(surface.entrypoint)) {
|
|
156
169
|
pushIssue(issues, `surface_manifest:${surface.id}`, `Surface references unknown entrypoint ${surface.entrypoint}`);
|
|
157
170
|
}
|
|
@@ -162,7 +175,7 @@ export function validateArtifactBundle(bundle) {
|
|
|
162
175
|
}
|
|
163
176
|
}
|
|
164
177
|
if (bundle.runtime_validation_tasks) {
|
|
165
|
-
for (const task of
|
|
178
|
+
for (const task of runtimeValidationTasks) {
|
|
166
179
|
if (task.target_paths.length === 0) {
|
|
167
180
|
pushIssue(issues, `runtime_validation_tasks:${task.id}`, "Runtime validation task has no target paths");
|
|
168
181
|
}
|
|
@@ -174,14 +187,14 @@ export function validateArtifactBundle(bundle) {
|
|
|
174
187
|
}
|
|
175
188
|
}
|
|
176
189
|
if (bundle.runtime_validation_report) {
|
|
177
|
-
for (const result of
|
|
190
|
+
for (const result of runtimeValidationResults) {
|
|
178
191
|
if (!runtimeTaskIds.has(result.task_id)) {
|
|
179
192
|
pushIssue(issues, `runtime_validation_report:${result.task_id}`, `Runtime validation result references unknown task ${result.task_id}`);
|
|
180
193
|
}
|
|
181
194
|
}
|
|
182
195
|
}
|
|
183
196
|
if (bundle.external_analyzer_results) {
|
|
184
|
-
for (const item of
|
|
197
|
+
for (const item of externalAnalyzerResults) {
|
|
185
198
|
if (!repoPaths.has(item.path) && bundle.repo_manifest) {
|
|
186
199
|
pushIssue(issues, `external_analyzer_results:${item.id}`, `External analyzer result references unknown path ${item.path}`);
|
|
187
200
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { AuditTask } from "../types.js";
|
|
2
|
+
import { type ValidationIssue } from "./basic.js";
|
|
2
3
|
export type IssueSeverity = "error" | "warning";
|
|
3
|
-
export interface AuditResultIssue {
|
|
4
|
+
export interface AuditResultIssue extends ValidationIssue {
|
|
4
5
|
result_index: number;
|
|
5
6
|
task_id: string;
|
|
6
7
|
severity: IssueSeverity;
|
|
7
8
|
field: string;
|
|
8
|
-
message: string;
|
|
9
9
|
}
|
|
10
10
|
export interface ValidateAuditResultOptions {
|
|
11
11
|
lineIndex?: Record<string, number>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describeValue, formatValidationIssues, isRecord, } from "./basic.js";
|
|
1
2
|
const REQUIRED_FINDING_FIELDS = [
|
|
2
3
|
"id",
|
|
3
4
|
"title",
|
|
@@ -24,21 +25,10 @@ const VALID_LENSES = new Set([
|
|
|
24
25
|
function pushIssue(issues, params) {
|
|
25
26
|
issues.push({
|
|
26
27
|
...params,
|
|
28
|
+
path: params.path ?? params.field,
|
|
27
29
|
severity: params.severity ?? "error",
|
|
28
30
|
});
|
|
29
31
|
}
|
|
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
32
|
function isNonEmptyString(value) {
|
|
43
33
|
return typeof value === "string" && value.trim().length > 0;
|
|
44
34
|
}
|
|
@@ -203,10 +193,11 @@ function validateFinding(finding, label, taskId, resultIndex) {
|
|
|
203
193
|
}
|
|
204
194
|
return issues;
|
|
205
195
|
}
|
|
206
|
-
function coversAffectedSpan(
|
|
207
|
-
return
|
|
208
|
-
|
|
209
|
-
|
|
196
|
+
function coversAffectedSpan(coverage, path, start, end) {
|
|
197
|
+
return coverage.some((entry) => entry.path === path &&
|
|
198
|
+
start > 0 &&
|
|
199
|
+
end > 0 &&
|
|
200
|
+
end <= entry.total_lines);
|
|
210
201
|
}
|
|
211
202
|
export function validateAuditResults(results, tasks, options = {}) {
|
|
212
203
|
const issues = [];
|
|
@@ -251,149 +242,113 @@ export function validateAuditResults(results, tasks, options = {}) {
|
|
|
251
242
|
result_index: i,
|
|
252
243
|
task_id: taskId,
|
|
253
244
|
field: "task_id",
|
|
254
|
-
message: `Unknown task_id '${taskId}'
|
|
245
|
+
message: `Unknown task_id '${taskId}'. Use the active task manifest for valid ids: ` +
|
|
246
|
+
tasks.map((item) => item.task_id).join(", "),
|
|
255
247
|
});
|
|
256
248
|
}
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
-
if (!Array.isArray(
|
|
249
|
+
const fileCoverage = result.file_coverage;
|
|
250
|
+
const normalizedFileCoverage = [];
|
|
251
|
+
if (!Array.isArray(fileCoverage) || fileCoverage.length === 0) {
|
|
260
252
|
pushIssue(issues, {
|
|
261
253
|
result_index: i,
|
|
262
254
|
task_id: taskId,
|
|
263
|
-
field: "
|
|
264
|
-
message: "
|
|
255
|
+
field: "file_coverage",
|
|
256
|
+
message: "file_coverage is empty — each result must declare every assigned file it reviewed and the file's total line count.",
|
|
265
257
|
});
|
|
266
258
|
}
|
|
267
259
|
else {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
260
|
+
const seenCoveragePaths = new Set();
|
|
261
|
+
for (let j = 0; j < fileCoverage.length; j++) {
|
|
262
|
+
const entry = fileCoverage[j];
|
|
263
|
+
if (!isRecord(entry)) {
|
|
271
264
|
pushIssue(issues, {
|
|
272
265
|
result_index: i,
|
|
273
266
|
task_id: taskId,
|
|
274
|
-
field: `
|
|
275
|
-
message: `
|
|
267
|
+
field: `file_coverage[${j}]`,
|
|
268
|
+
message: `file_coverage[${j}] must be an object, got ${describeValue(entry)}.`,
|
|
276
269
|
});
|
|
277
270
|
continue;
|
|
278
271
|
}
|
|
279
|
-
if (!isNonEmptyString(
|
|
272
|
+
if (!isNonEmptyString(entry.path)) {
|
|
280
273
|
pushIssue(issues, {
|
|
281
274
|
result_index: i,
|
|
282
275
|
task_id: taskId,
|
|
283
|
-
field: `
|
|
284
|
-
message: "
|
|
276
|
+
field: `file_coverage[${j}].path`,
|
|
277
|
+
message: "file_coverage entry has an empty path.",
|
|
285
278
|
});
|
|
286
279
|
}
|
|
287
|
-
else if (task && !task.file_paths.includes(
|
|
280
|
+
else if (task && !task.file_paths.includes(entry.path)) {
|
|
288
281
|
pushIssue(issues, {
|
|
289
282
|
result_index: i,
|
|
290
283
|
task_id: taskId,
|
|
291
284
|
severity: "warning",
|
|
292
|
-
field: `
|
|
293
|
-
message: `
|
|
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)}.`,
|
|
285
|
+
field: `file_coverage[${j}].path`,
|
|
286
|
+
message: `file_coverage path '${entry.path}' is not listed in the task file_paths.`,
|
|
318
287
|
});
|
|
319
288
|
}
|
|
320
|
-
if (
|
|
321
|
-
Number.isInteger(range.end) &&
|
|
322
|
-
Number(range.start) > Number(range.end)) {
|
|
289
|
+
else if (seenCoveragePaths.has(entry.path)) {
|
|
323
290
|
pushIssue(issues, {
|
|
324
291
|
result_index: i,
|
|
325
292
|
task_id: taskId,
|
|
326
|
-
field: `
|
|
327
|
-
message:
|
|
293
|
+
field: `file_coverage[${j}].path`,
|
|
294
|
+
message: `file_coverage path '${entry.path}' is duplicated. Declare each file once.`,
|
|
328
295
|
});
|
|
329
296
|
}
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
});
|
|
297
|
+
else {
|
|
298
|
+
seenCoveragePaths.add(entry.path);
|
|
338
299
|
}
|
|
339
|
-
if (Number.isInteger(
|
|
340
|
-
Number(range.start) <= 0) {
|
|
300
|
+
if (!Number.isInteger(entry.total_lines)) {
|
|
341
301
|
pushIssue(issues, {
|
|
342
302
|
result_index: i,
|
|
343
303
|
task_id: taskId,
|
|
344
|
-
field: `
|
|
345
|
-
message:
|
|
304
|
+
field: `file_coverage[${j}].total_lines`,
|
|
305
|
+
message: `file_coverage[${j}].total_lines must be an integer, got ${describeValue(entry.total_lines)}.`,
|
|
346
306
|
});
|
|
347
307
|
}
|
|
348
|
-
if (Number.isInteger(
|
|
349
|
-
Number(
|
|
308
|
+
if (Number.isInteger(entry.total_lines) &&
|
|
309
|
+
Number(entry.total_lines) <= 0) {
|
|
350
310
|
pushIssue(issues, {
|
|
351
311
|
result_index: i,
|
|
352
312
|
task_id: taskId,
|
|
353
|
-
field: `
|
|
354
|
-
message: "
|
|
313
|
+
field: `file_coverage[${j}].total_lines`,
|
|
314
|
+
message: "file_coverage total_lines must be greater than zero.",
|
|
355
315
|
});
|
|
356
316
|
}
|
|
357
|
-
|
|
358
|
-
|
|
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]
|
|
317
|
+
const expectedLineCount = typeof entry.path === "string"
|
|
318
|
+
? options.lineIndex?.[entry.path]
|
|
369
319
|
: undefined;
|
|
370
|
-
if (Number.isInteger(
|
|
320
|
+
if (Number.isInteger(entry.total_lines) &&
|
|
371
321
|
typeof expectedLineCount === "number" &&
|
|
372
|
-
Number(
|
|
322
|
+
Number(entry.total_lines) !== expectedLineCount) {
|
|
373
323
|
pushIssue(issues, {
|
|
374
324
|
result_index: i,
|
|
375
325
|
task_id: taskId,
|
|
376
|
-
field: `
|
|
377
|
-
message: `
|
|
378
|
-
`(expected ${expectedLineCount}, got ${
|
|
326
|
+
field: `file_coverage[${j}].total_lines`,
|
|
327
|
+
message: `file_coverage[${j}].total_lines must match the current file line count for '${entry.path}' ` +
|
|
328
|
+
`(expected ${expectedLineCount}, got ${entry.total_lines}).`,
|
|
379
329
|
});
|
|
380
330
|
}
|
|
381
|
-
if (isNonEmptyString(
|
|
382
|
-
Number.isInteger(
|
|
383
|
-
Number
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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),
|
|
331
|
+
if (isNonEmptyString(entry.path) &&
|
|
332
|
+
Number.isInteger(entry.total_lines) &&
|
|
333
|
+
Number(entry.total_lines) > 0) {
|
|
334
|
+
normalizedFileCoverage.push({
|
|
335
|
+
path: entry.path,
|
|
336
|
+
total_lines: Number(entry.total_lines),
|
|
394
337
|
});
|
|
395
338
|
}
|
|
396
339
|
}
|
|
340
|
+
if (task) {
|
|
341
|
+
for (const path of task.file_paths) {
|
|
342
|
+
if (!seenCoveragePaths.has(path)) {
|
|
343
|
+
pushIssue(issues, {
|
|
344
|
+
result_index: i,
|
|
345
|
+
task_id: taskId,
|
|
346
|
+
field: "file_coverage",
|
|
347
|
+
message: `file_coverage must include every assigned file. Missing '${path}'.`,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
397
352
|
}
|
|
398
353
|
const findings = result.findings;
|
|
399
354
|
if (!Array.isArray(findings)) {
|
|
@@ -424,13 +379,13 @@ export function validateAuditResults(results, tasks, options = {}) {
|
|
|
424
379
|
const end = Number.isInteger(affected.line_end)
|
|
425
380
|
? Number(affected.line_end)
|
|
426
381
|
: start;
|
|
427
|
-
if (!coversAffectedSpan(
|
|
382
|
+
if (!coversAffectedSpan(normalizedFileCoverage, affected.path, start, end)) {
|
|
428
383
|
pushIssue(issues, {
|
|
429
384
|
result_index: i,
|
|
430
385
|
task_id: taskId,
|
|
431
386
|
field: `${label}.affected_files[${k}]`,
|
|
432
|
-
message: `affected_files line span ${affected.path}:${start}-${end} falls outside the declared
|
|
433
|
-
"
|
|
387
|
+
message: `affected_files line span ${affected.path}:${start}-${end} falls outside the declared file_coverage. ` +
|
|
388
|
+
"Fix the affected_files location or correct file_coverage.total_lines.",
|
|
434
389
|
});
|
|
435
390
|
}
|
|
436
391
|
}
|
|
@@ -439,7 +394,9 @@ export function validateAuditResults(results, tasks, options = {}) {
|
|
|
439
394
|
return issues;
|
|
440
395
|
}
|
|
441
396
|
export function formatAuditResultIssues(issues) {
|
|
442
|
-
return issues
|
|
443
|
-
|
|
444
|
-
.
|
|
397
|
+
return formatValidationIssues(issues.map((issue) => ({
|
|
398
|
+
path: `${issue.task_id} / ${issue.field}`,
|
|
399
|
+
message: issue.message,
|
|
400
|
+
severity: issue.severity,
|
|
401
|
+
})));
|
|
445
402
|
}
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
export type ValidationSeverity = "error" | "warning";
|
|
1
2
|
export interface ValidationIssue {
|
|
2
3
|
path: string;
|
|
3
4
|
message: string;
|
|
5
|
+
severity: ValidationSeverity;
|
|
4
6
|
}
|
|
5
|
-
export declare function
|
|
7
|
+
export declare function describeValue(value: unknown): string;
|
|
8
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
9
|
+
export declare function createValidationIssue(path: string, message: string, severity?: ValidationSeverity): ValidationIssue;
|
|
10
|
+
export declare function pushValidationIssue(issues: ValidationIssue[], path: string, message: string, severity?: ValidationSeverity): void;
|
|
11
|
+
export declare function prefixValidationIssues(prefix: string, issues: ValidationIssue[]): ValidationIssue[];
|
|
12
|
+
export declare function formatValidationIssues(issues: ValidationIssue[]): string;
|
|
13
|
+
export declare function requireKeys(value: unknown, path: string, keys: readonly string[]): ValidationIssue[];
|
package/dist/validation/basic.js
CHANGED
|
@@ -1,8 +1,45 @@
|
|
|
1
|
-
export function
|
|
1
|
+
export function describeValue(value) {
|
|
2
|
+
if (Array.isArray(value)) {
|
|
3
|
+
return "array";
|
|
4
|
+
}
|
|
5
|
+
if (value === null) {
|
|
6
|
+
return "null";
|
|
7
|
+
}
|
|
8
|
+
return typeof value;
|
|
9
|
+
}
|
|
10
|
+
export function isRecord(value) {
|
|
11
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
export function createValidationIssue(path, message, severity = "error") {
|
|
14
|
+
return { path, message, severity };
|
|
15
|
+
}
|
|
16
|
+
export function pushValidationIssue(issues, path, message, severity = "error") {
|
|
17
|
+
issues.push(createValidationIssue(path, message, severity));
|
|
18
|
+
}
|
|
19
|
+
export function prefixValidationIssues(prefix, issues) {
|
|
20
|
+
return issues.map((issue) => ({
|
|
21
|
+
...issue,
|
|
22
|
+
path: issue.path.length === 0
|
|
23
|
+
? prefix
|
|
24
|
+
: issue.path === prefix || issue.path.startsWith(`${prefix}.`)
|
|
25
|
+
? issue.path
|
|
26
|
+
: `${prefix}.${issue.path}`,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
export function formatValidationIssues(issues) {
|
|
30
|
+
return issues
|
|
31
|
+
.map((issue) => ` [${issue.severity}] ${issue.path}: ${issue.message}`)
|
|
32
|
+
.join("\n");
|
|
33
|
+
}
|
|
34
|
+
export function requireKeys(value, path, keys) {
|
|
2
35
|
const issues = [];
|
|
36
|
+
if (!isRecord(value)) {
|
|
37
|
+
pushValidationIssue(issues, path, `Expected an object, got ${describeValue(value)}.`);
|
|
38
|
+
return issues;
|
|
39
|
+
}
|
|
3
40
|
for (const key of keys) {
|
|
4
|
-
if (!(key in
|
|
5
|
-
issues
|
|
41
|
+
if (!(key in value)) {
|
|
42
|
+
pushValidationIssue(issues, path, `Missing required key: ${key}`);
|
|
6
43
|
}
|
|
7
44
|
}
|
|
8
45
|
return issues;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import type
|
|
1
|
+
import { type SessionConfig } from "../types/sessionConfig.js";
|
|
2
|
+
import { type ValidationIssue } from "./basic.js";
|
|
3
3
|
export declare function validateSessionConfig(value: unknown): ValidationIssue[];
|
|
4
4
|
export declare function validateConfiguredProviderEnvironment(sessionConfig: SessionConfig, options?: {
|
|
5
5
|
commandExists?: (command: string) => boolean;
|
|
6
|
+
pathExists?: (commandPath: string) => boolean;
|
|
6
7
|
}): ValidationIssue[];
|
|
8
|
+
export { formatValidationIssues } from "./basic.js";
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
"opencode",
|
|
8
|
-
"vscode-task",
|
|
9
|
-
]);
|
|
10
|
-
const VALID_UI_MODES = new Set(["headless", "visible"]);
|
|
2
|
+
import { accessSync, constants } from "node:fs";
|
|
3
|
+
import { PROVIDER_NAMES, SESSION_UI_MODES, } from "../types/sessionConfig.js";
|
|
4
|
+
import { isRecord, pushValidationIssue, } from "./basic.js";
|
|
5
|
+
const VALID_PROVIDERS = new Set(PROVIDER_NAMES);
|
|
6
|
+
const VALID_UI_MODES = new Set(SESSION_UI_MODES);
|
|
11
7
|
function pushIssue(issues, path, message) {
|
|
12
|
-
issues
|
|
13
|
-
}
|
|
14
|
-
function isRecord(value) {
|
|
15
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
pushValidationIssue(issues, path, message);
|
|
16
9
|
}
|
|
17
10
|
function validateStringArray(value, path, label, issues, options = {}) {
|
|
18
11
|
if (!Array.isArray(value)) {
|
|
@@ -74,6 +67,9 @@ function validateAgentProviderSection(value, path, issues) {
|
|
|
74
67
|
if (typeof value.command !== "string" || value.command.trim().length === 0) {
|
|
75
68
|
pushIssue(issues, `${path}.command`, "command must be a non-empty string when provided.");
|
|
76
69
|
}
|
|
70
|
+
else if (!isSupportedConfiguredCommand(value.command)) {
|
|
71
|
+
pushIssue(issues, `${path}.command`, "command must be a bare executable name or direct executable path. Put CLI flags in extra_args.");
|
|
72
|
+
}
|
|
77
73
|
}
|
|
78
74
|
if (value.extra_args !== undefined) {
|
|
79
75
|
validateStringArray(value.extra_args, `${path}.extra_args`, "extra_args", issues, { allowEmptyArray: true });
|
|
@@ -84,6 +80,43 @@ function commandExists(command) {
|
|
|
84
80
|
const result = spawnSync(lookupCommand, [command], { stdio: "ignore" });
|
|
85
81
|
return result.status === 0;
|
|
86
82
|
}
|
|
83
|
+
function configuredPathExists(commandPath) {
|
|
84
|
+
try {
|
|
85
|
+
accessSync(commandPath, constants.F_OK);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function startsWithPathPrefix(command) {
|
|
93
|
+
return (command.startsWith(".") ||
|
|
94
|
+
command.startsWith("/") ||
|
|
95
|
+
command.startsWith("\\\\") ||
|
|
96
|
+
/^[A-Za-z]:[\\/]/.test(command));
|
|
97
|
+
}
|
|
98
|
+
function containsForbiddenCommandSyntax(command) {
|
|
99
|
+
return /[\r\n"'`|&;<>]/.test(command);
|
|
100
|
+
}
|
|
101
|
+
function isBareExecutableName(command) {
|
|
102
|
+
return (command.length > 0 &&
|
|
103
|
+
!/\s/.test(command) &&
|
|
104
|
+
!containsForbiddenCommandSyntax(command) &&
|
|
105
|
+
!/[\\/]/.test(command) &&
|
|
106
|
+
!/^[A-Za-z]:/.test(command));
|
|
107
|
+
}
|
|
108
|
+
function isDirectExecutablePath(command) {
|
|
109
|
+
return (command.length > 0 &&
|
|
110
|
+
!containsForbiddenCommandSyntax(command) &&
|
|
111
|
+
startsWithPathPrefix(command));
|
|
112
|
+
}
|
|
113
|
+
function isSupportedConfiguredCommand(command) {
|
|
114
|
+
const trimmed = command.trim();
|
|
115
|
+
if (trimmed.length === 0 || trimmed !== command) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return isBareExecutableName(trimmed) || isDirectExecutablePath(trimmed);
|
|
119
|
+
}
|
|
87
120
|
export function validateSessionConfig(value) {
|
|
88
121
|
const issues = [];
|
|
89
122
|
if (value === undefined) {
|
|
@@ -122,18 +155,32 @@ export function validateSessionConfig(value) {
|
|
|
122
155
|
export function validateConfiguredProviderEnvironment(sessionConfig, options = {}) {
|
|
123
156
|
const issues = [];
|
|
124
157
|
const lookupCommand = options.commandExists ?? commandExists;
|
|
158
|
+
const lookupPath = options.pathExists ?? configuredPathExists;
|
|
125
159
|
const provider = sessionConfig.provider ?? "local-subprocess";
|
|
126
160
|
if (provider === "claude-code") {
|
|
127
161
|
const command = sessionConfig.claude_code?.command ?? "claude";
|
|
128
|
-
if (!lookupCommand(command)) {
|
|
162
|
+
if (isBareExecutableName(command) && !lookupCommand(command)) {
|
|
129
163
|
pushIssue(issues, "claude_code.command", `Configured claude-code executable was not found on PATH: ${command}.`);
|
|
130
164
|
}
|
|
165
|
+
else if (isDirectExecutablePath(command) && !lookupPath(command)) {
|
|
166
|
+
pushIssue(issues, "claude_code.command", `Configured claude-code executable path does not exist: ${command}.`);
|
|
167
|
+
}
|
|
168
|
+
else if (!isSupportedConfiguredCommand(command)) {
|
|
169
|
+
pushIssue(issues, "claude_code.command", "Configured claude-code command must be a bare executable name or direct path. Put CLI flags in extra_args.");
|
|
170
|
+
}
|
|
131
171
|
}
|
|
132
172
|
if (provider === "opencode") {
|
|
133
173
|
const command = sessionConfig.opencode?.command ?? "opencode";
|
|
134
|
-
if (!lookupCommand(command)) {
|
|
174
|
+
if (isBareExecutableName(command) && !lookupCommand(command)) {
|
|
135
175
|
pushIssue(issues, "opencode.command", `Configured opencode executable was not found on PATH: ${command}.`);
|
|
136
176
|
}
|
|
177
|
+
else if (isDirectExecutablePath(command) && !lookupPath(command)) {
|
|
178
|
+
pushIssue(issues, "opencode.command", `Configured opencode executable path does not exist: ${command}.`);
|
|
179
|
+
}
|
|
180
|
+
else if (!isSupportedConfiguredCommand(command)) {
|
|
181
|
+
pushIssue(issues, "opencode.command", "Configured opencode command must be a bare executable name or direct path. Put CLI flags in extra_args.");
|
|
182
|
+
}
|
|
137
183
|
}
|
|
138
184
|
return issues;
|
|
139
185
|
}
|
|
186
|
+
export { formatValidationIssues } from "./basic.js";
|