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.
Files changed (125) hide show
  1. package/README.md +29 -7
  2. package/audit-code-wrapper-lib.mjs +1605 -330
  3. package/dist/adapters/eslint.js +9 -5
  4. package/dist/cli.d.ts +42 -1
  5. package/dist/cli.js +192 -80
  6. package/dist/coverage.d.ts +2 -2
  7. package/dist/coverage.js +5 -5
  8. package/dist/extractors/bucketing.d.ts +4 -0
  9. package/dist/extractors/bucketing.js +6 -2
  10. package/dist/extractors/disposition.d.ts +4 -0
  11. package/dist/extractors/disposition.js +15 -2
  12. package/dist/extractors/fileInventory.js +24 -28
  13. package/dist/extractors/flows.d.ts +5 -0
  14. package/dist/extractors/flows.js +25 -39
  15. package/dist/extractors/pathPatterns.d.ts +13 -3
  16. package/dist/extractors/pathPatterns.js +116 -53
  17. package/dist/extractors/risk.js +7 -1
  18. package/dist/extractors/surfaces.d.ts +4 -0
  19. package/dist/extractors/surfaces.js +11 -11
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +2 -1
  22. package/dist/io/artifacts.d.ts +59 -44
  23. package/dist/io/artifacts.js +80 -120
  24. package/dist/io/json.d.ts +2 -0
  25. package/dist/io/json.js +65 -19
  26. package/dist/io/runArtifacts.d.ts +2 -1
  27. package/dist/io/runArtifacts.js +44 -7
  28. package/dist/mcp/server.d.ts +1 -0
  29. package/dist/mcp/server.js +579 -0
  30. package/dist/orchestrator/advance.js +84 -56
  31. package/dist/orchestrator/dependencyMap.js +9 -13
  32. package/dist/orchestrator/executors.js +7 -2
  33. package/dist/orchestrator/flowCoverage.js +11 -5
  34. package/dist/orchestrator/flowPlanning.d.ts +7 -2
  35. package/dist/orchestrator/flowPlanning.js +46 -21
  36. package/dist/orchestrator/flowRequeue.js +29 -9
  37. package/dist/orchestrator/internalExecutors.d.ts +2 -1
  38. package/dist/orchestrator/internalExecutors.js +130 -69
  39. package/dist/orchestrator/planning.js +25 -3
  40. package/dist/orchestrator/requeue.js +20 -5
  41. package/dist/orchestrator/resultIngestion.js +5 -6
  42. package/dist/orchestrator/runtimeValidation.d.ts +7 -2
  43. package/dist/orchestrator/runtimeValidation.js +61 -49
  44. package/dist/orchestrator/runtimeValidationUpdate.js +2 -4
  45. package/dist/orchestrator/state.js +18 -13
  46. package/dist/orchestrator/taskBuilder.d.ts +4 -2
  47. package/dist/orchestrator/taskBuilder.js +153 -52
  48. package/dist/orchestrator/trivialAudit.js +8 -5
  49. package/dist/orchestrator/unitBuilder.d.ts +3 -1
  50. package/dist/orchestrator/unitBuilder.js +24 -16
  51. package/dist/prompts/renderWorkerPrompt.d.ts +1 -1
  52. package/dist/prompts/renderWorkerPrompt.js +19 -10
  53. package/dist/providers/claudeCodeProvider.d.ts +4 -1
  54. package/dist/providers/claudeCodeProvider.js +8 -5
  55. package/dist/providers/localSubprocessProvider.d.ts +4 -0
  56. package/dist/providers/localSubprocessProvider.js +7 -2
  57. package/dist/providers/spawnLoggedCommand.d.ts +9 -1
  58. package/dist/providers/spawnLoggedCommand.js +77 -29
  59. package/dist/reporting/mergeFindings.js +0 -11
  60. package/dist/reporting/synthesis.d.ts +26 -21
  61. package/dist/reporting/synthesis.js +97 -61
  62. package/dist/reporting/workBlocks.d.ts +12 -3
  63. package/dist/reporting/workBlocks.js +124 -70
  64. package/dist/supervisor/operatorHandoff.js +48 -18
  65. package/dist/supervisor/runLedger.d.ts +1 -1
  66. package/dist/supervisor/runLedger.js +112 -5
  67. package/dist/supervisor/sessionConfig.js +10 -10
  68. package/dist/types/externalAnalyzer.d.ts +3 -0
  69. package/dist/types/flowCoverage.d.ts +5 -1
  70. package/dist/types/flowCoverage.js +5 -1
  71. package/dist/types/flows.d.ts +6 -0
  72. package/dist/types/flows.js +1 -1
  73. package/dist/types/runLedger.d.ts +5 -1
  74. package/dist/types/runLedger.js +6 -1
  75. package/dist/types/runtimeValidation.d.ts +13 -3
  76. package/dist/types/runtimeValidation.js +16 -1
  77. package/dist/types/sessionConfig.d.ts +15 -2
  78. package/dist/types/sessionConfig.js +15 -1
  79. package/dist/types/surfaces.d.ts +4 -1
  80. package/dist/types/surfaces.js +1 -1
  81. package/dist/types/workerSession.d.ts +9 -0
  82. package/dist/types/workerSession.js +5 -1
  83. package/dist/types.d.ts +4 -7
  84. package/dist/validation/artifacts.d.ts +1 -1
  85. package/dist/validation/artifacts.js +33 -20
  86. package/dist/validation/auditResults.d.ts +2 -2
  87. package/dist/validation/auditResults.js +71 -114
  88. package/dist/validation/basic.d.ts +9 -1
  89. package/dist/validation/basic.js +40 -3
  90. package/dist/validation/sessionConfig.d.ts +4 -2
  91. package/dist/validation/sessionConfig.js +62 -15
  92. package/docs/agent-integrations.md +67 -38
  93. package/docs/artifacts.md +16 -56
  94. package/docs/bootstrap-install.md +60 -30
  95. package/docs/contract.md +22 -205
  96. package/docs/next-steps.md +76 -44
  97. package/docs/packaging.md +27 -3
  98. package/docs/product-direction.md +22 -0
  99. package/docs/production-launch-bar.md +4 -2
  100. package/docs/production-readiness.md +9 -5
  101. package/docs/releasing.md +98 -0
  102. package/docs/remediation-baseline.md +75 -0
  103. package/docs/run-flow.md +23 -11
  104. package/docs/session-config.md +50 -5
  105. package/docs/supervisor.md +7 -0
  106. package/docs/workflow-refactor-brief.md +177 -0
  107. package/package.json +4 -1
  108. package/schemas/audit_result.schema.json +8 -7
  109. package/schemas/audit_task.schema.json +3 -1
  110. package/schemas/coverage_matrix.schema.json +3 -3
  111. package/schemas/critical_flows.schema.json +6 -2
  112. package/schemas/file_disposition.schema.json +2 -2
  113. package/schemas/finding.schema.json +9 -4
  114. package/schemas/flow_coverage.schema.json +2 -2
  115. package/schemas/repo_manifest.schema.json +4 -4
  116. package/schemas/risk_register.schema.json +2 -2
  117. package/schemas/runtime_validation_report.schema.json +3 -3
  118. package/schemas/runtime_validation_tasks.schema.json +8 -2
  119. package/schemas/surface_manifest.schema.json +6 -3
  120. package/schemas/unit_manifest.schema.json +3 -2
  121. package/skills/audit-code/SKILL.md +16 -2
  122. package/skills/audit-code/audit-code.prompt.md +5 -8
  123. package/schemas/merged_findings.schema.json +0 -19
  124. package/schemas/root_cause_clusters.schema.json +0 -28
  125. 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.push({ path, message });
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 repoPaths = new Set(bundle.repo_manifest?.files.map((file) => file.path) ?? []);
41
- const dispositionMap = new Map(bundle.file_disposition?.files.map((item) => [item.path, item.status]) ??
42
- []);
43
- const unitIds = new Set(bundle.unit_manifest?.units.map((unit) => unit.unit_id) ?? []);
44
- const flowIds = new Set(bundle.critical_flows?.flows.map((flow) => flow.id) ?? []);
45
- const runtimeTaskIds = new Set(bundle.runtime_validation_tasks?.tasks.map((task) => task.id) ?? []);
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(bundle.coverage_matrix.files.map((file) => file.path));
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(bundle.file_disposition.files.map((file) => file.path));
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 bundle.unit_manifest.units) {
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 bundle.coverage_matrix.files) {
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 bundle.critical_flows.flows) {
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 bundle.flow_coverage.flows) {
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(bundle.risk_register.items.map((item) => item.unit_id));
147
- for (const unit of bundle.unit_manifest.units) {
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 bundle.surface_manifest.surfaces) {
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 bundle.runtime_validation_tasks.tasks) {
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 bundle.runtime_validation_report.results) {
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 bundle.external_analyzer_results.results) {
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(ranges, path, start, end) {
207
- return ranges.some((range) => range.path === path &&
208
- range.start <= start &&
209
- range.end >= end);
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 reviewedRanges = result.reviewed_ranges;
258
- const normalizedReviewedRanges = [];
259
- if (!Array.isArray(reviewedRanges) || reviewedRanges.length === 0) {
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: "reviewed_ranges",
264
- message: "reviewed_ranges is empty — no proof of file reading was recorded. Each result must include the line ranges actually read.",
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
- for (let j = 0; j < reviewedRanges.length; j++) {
269
- const range = reviewedRanges[j];
270
- if (!isRecord(range)) {
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: `reviewed_ranges[${j}]`,
275
- message: `reviewed_ranges[${j}] must be an object, got ${describeValue(range)}.`,
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(range.path)) {
272
+ if (!isNonEmptyString(entry.path)) {
280
273
  pushIssue(issues, {
281
274
  result_index: i,
282
275
  task_id: taskId,
283
- field: `reviewed_ranges[${j}].path`,
284
- message: "reviewed_ranges entry has an empty path.",
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(range.path)) {
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: `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)}.`,
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 (Number.isInteger(range.start) &&
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: `reviewed_ranges[${j}]`,
327
- message: "reviewed_ranges start must be less than or equal to end.",
293
+ field: `file_coverage[${j}].path`,
294
+ message: `file_coverage path '${entry.path}' is duplicated. Declare each file once.`,
328
295
  });
329
296
  }
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
- });
297
+ else {
298
+ seenCoveragePaths.add(entry.path);
338
299
  }
339
- if (Number.isInteger(range.start) &&
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: `reviewed_ranges[${j}].start`,
345
- message: "reviewed_ranges start must be greater than zero.",
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(range.end) &&
349
- Number(range.end) <= 0) {
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: `reviewed_ranges[${j}].end`,
354
- message: "reviewed_ranges end must be greater than zero.",
313
+ field: `file_coverage[${j}].total_lines`,
314
+ message: "file_coverage total_lines must be greater than zero.",
355
315
  });
356
316
  }
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]
317
+ const expectedLineCount = typeof entry.path === "string"
318
+ ? options.lineIndex?.[entry.path]
369
319
  : undefined;
370
- if (Number.isInteger(range.line_count) &&
320
+ if (Number.isInteger(entry.total_lines) &&
371
321
  typeof expectedLineCount === "number" &&
372
- Number(range.line_count) !== expectedLineCount) {
322
+ Number(entry.total_lines) !== expectedLineCount) {
373
323
  pushIssue(issues, {
374
324
  result_index: i,
375
325
  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}).`,
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(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),
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(normalizedReviewedRanges, affected.path, start, end)) {
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 reviewed_ranges. ` +
433
- "Expand reviewed_ranges to cover the cited lines or fix the affected_files location.",
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
- .map((issue) => ` [${issue.severity}] ${issue.task_id} / ${issue.field}: ${issue.message}`)
444
- .join("\n");
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 requireKeys(record: Record<string, unknown>, path: string, keys: string[]): ValidationIssue[];
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[];
@@ -1,8 +1,45 @@
1
- export function requireKeys(record, path, keys) {
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 record)) {
5
- issues.push({ path, message: `Missing required key: ${key}` });
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 { SessionConfig } from "../types/sessionConfig.js";
2
- import type { ValidationIssue } from "./basic.js";
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
- const VALID_PROVIDERS = new Set([
3
- "auto",
4
- "local-subprocess",
5
- "subprocess-template",
6
- "claude-code",
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.push({ path, message });
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";