@nimiplatform/nimi-coding 0.2.8 → 0.3.1

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 (206) hide show
  1. package/CHANGELOG.md +10 -90
  2. package/CONTRIBUTING.md +5 -34
  3. package/README.md +47 -350
  4. package/README.zh-CN.md +33 -287
  5. package/cli/commands/start.mjs +48 -688
  6. package/cli/commands/validate-spec-audit.mjs +1 -5
  7. package/cli/commands/validate-spec-governance.mjs +20 -0
  8. package/cli/constants.mjs +9 -417
  9. package/cli/help.mjs +28 -145
  10. package/cli/index.mjs +5 -33
  11. package/cli/lib/blueprint-audit.mjs +5 -12
  12. package/cli/lib/bootstrap.mjs +0 -9
  13. package/cli/lib/contracts.mjs +0 -163
  14. package/cli/lib/doctor.mjs +155 -10
  15. package/cli/lib/entrypoints.mjs +63 -167
  16. package/cli/lib/internal/contracts-loaders.mjs +0 -85
  17. package/cli/lib/internal/contracts-parse.mjs +84 -520
  18. package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +13 -8
  19. package/cli/lib/internal/surface-taxonomy-validators.mjs +316 -66
  20. package/cli/lib/internal/validators-shared.mjs +0 -16
  21. package/cli/lib/internal/validators-spec-helpers.mjs +7 -140
  22. package/cli/lib/internal/validators-spec.mjs +103 -342
  23. package/cli/lib/shared.mjs +1 -42
  24. package/cli/lib/validators.mjs +0 -48
  25. package/cli/seeds/seed-policy.yaml +0 -3
  26. package/config/bootstrap.yaml +4 -4
  27. package/config/spec-generation-inputs.yaml +6 -12
  28. package/contracts/domain-admission.schema.yaml +3 -3
  29. package/contracts/migration-inventory.schema.yaml +12 -75
  30. package/contracts/negative-fixtures.yaml +19 -99
  31. package/contracts/placement-contract.schema.yaml +49 -78
  32. package/contracts/projection-edge.schema.yaml +37 -121
  33. package/contracts/shared-enums.yaml +4 -8
  34. package/contracts/spec-generation-inputs.schema.yaml +15 -112
  35. package/contracts/spec-layout.schema.yaml +32 -0
  36. package/contracts/surface-taxonomy.schema.yaml +43 -94
  37. package/contracts/table-family.schema.yaml +1 -1
  38. package/contracts/tracked-output-admission.schema.yaml +13 -66
  39. package/methodology/core.yaml +33 -25
  40. package/methodology/four-closure-policy.yaml +18 -26
  41. package/methodology/role-separation-policy.yaml +21 -27
  42. package/methodology/spec-reconstruction.yaml +17 -34
  43. package/package.json +2 -4
  44. package/spec/_meta/spec-tree-model.yaml +5 -101
  45. package/spec/product-scope.yaml +25 -57
  46. package/adapters/README.md +0 -25
  47. package/adapters/claude/README.md +0 -89
  48. package/adapters/claude/profile.yaml +0 -70
  49. package/adapters/codex/README.md +0 -53
  50. package/adapters/codex/profile.yaml +0 -78
  51. package/adapters/oh-my-codex/README.md +0 -184
  52. package/adapters/oh-my-codex/profile.yaml +0 -46
  53. package/cli/commands/admit-high-risk-decision.mjs +0 -108
  54. package/cli/commands/audit-sweep.mjs +0 -364
  55. package/cli/commands/closeout.mjs +0 -186
  56. package/cli/commands/decide-high-risk-execution.mjs +0 -124
  57. package/cli/commands/handoff.mjs +0 -123
  58. package/cli/commands/ingest-high-risk-execution.mjs +0 -95
  59. package/cli/commands/review-high-risk-execution.mjs +0 -95
  60. package/cli/commands/sweep-design.mjs +0 -295
  61. package/cli/commands/sweep.mjs +0 -22
  62. package/cli/commands/topic-formatters.mjs +0 -382
  63. package/cli/commands/topic-goal.mjs +0 -33
  64. package/cli/commands/topic-options-shared.mjs +0 -27
  65. package/cli/commands/topic-options-workflow.mjs +0 -767
  66. package/cli/commands/topic-options.mjs +0 -626
  67. package/cli/commands/topic-runner.mjs +0 -169
  68. package/cli/commands/topic.mjs +0 -795
  69. package/cli/commands/validate-acceptance.mjs +0 -5
  70. package/cli/commands/validate-execution-packet.mjs +0 -5
  71. package/cli/commands/validate-orchestration-state.mjs +0 -5
  72. package/cli/commands/validate-prompt.mjs +0 -5
  73. package/cli/commands/validate-worker-output.mjs +0 -5
  74. package/cli/lib/adapter-profiles.mjs +0 -403
  75. package/cli/lib/audit-execution.mjs +0 -52
  76. package/cli/lib/audit-sweep-runtime/admissions.mjs +0 -508
  77. package/cli/lib/audit-sweep-runtime/audit-validity.mjs +0 -356
  78. package/cli/lib/audit-sweep-runtime/chunks.mjs +0 -697
  79. package/cli/lib/audit-sweep-runtime/claude-auditor.mjs +0 -658
  80. package/cli/lib/audit-sweep-runtime/closeout.mjs +0 -144
  81. package/cli/lib/audit-sweep-runtime/codex-auditor-evidence.mjs +0 -654
  82. package/cli/lib/audit-sweep-runtime/codex-auditor.mjs +0 -527
  83. package/cli/lib/audit-sweep-runtime/common.mjs +0 -341
  84. package/cli/lib/audit-sweep-runtime/coverage-quality.mjs +0 -172
  85. package/cli/lib/audit-sweep-runtime/evidence-assignment.mjs +0 -155
  86. package/cli/lib/audit-sweep-runtime/format.mjs +0 -57
  87. package/cli/lib/audit-sweep-runtime/ingest.mjs +0 -486
  88. package/cli/lib/audit-sweep-runtime/inventory-spec-chunks.mjs +0 -425
  89. package/cli/lib/audit-sweep-runtime/inventory.mjs +0 -798
  90. package/cli/lib/audit-sweep-runtime/ledger.mjs +0 -315
  91. package/cli/lib/audit-sweep-runtime/p0p1-profile.mjs +0 -101
  92. package/cli/lib/audit-sweep-runtime/remediation.mjs +0 -349
  93. package/cli/lib/audit-sweep-runtime/rerun.mjs +0 -129
  94. package/cli/lib/audit-sweep-runtime/risk-budget.mjs +0 -300
  95. package/cli/lib/audit-sweep-runtime/status.mjs +0 -62
  96. package/cli/lib/audit-sweep-runtime/validators-ledger.mjs +0 -215
  97. package/cli/lib/audit-sweep-runtime/validators.mjs +0 -797
  98. package/cli/lib/audit-sweep.mjs +0 -19
  99. package/cli/lib/authority-convergence.mjs +0 -720
  100. package/cli/lib/closeout.mjs +0 -732
  101. package/cli/lib/codex-sdk-runner.mjs +0 -76
  102. package/cli/lib/external-execution.mjs +0 -101
  103. package/cli/lib/handoff.mjs +0 -812
  104. package/cli/lib/high-risk-admission.mjs +0 -512
  105. package/cli/lib/high-risk-decision.mjs +0 -353
  106. package/cli/lib/high-risk-ingest.mjs +0 -321
  107. package/cli/lib/high-risk-review.mjs +0 -267
  108. package/cli/lib/internal/contracts-parse-high-risk.mjs +0 -131
  109. package/cli/lib/internal/contracts-validators.mjs +0 -399
  110. package/cli/lib/internal/doctor-bootstrap-surface.mjs +0 -406
  111. package/cli/lib/internal/doctor-delegated-surface.mjs +0 -256
  112. package/cli/lib/internal/doctor-finalize.mjs +0 -383
  113. package/cli/lib/internal/doctor-format.mjs +0 -286
  114. package/cli/lib/internal/doctor-inspectors.mjs +0 -327
  115. package/cli/lib/internal/doctor-state.mjs +0 -205
  116. package/cli/lib/internal/validators-artifacts.mjs +0 -515
  117. package/cli/lib/sweep-design-runtime/common.mjs +0 -246
  118. package/cli/lib/sweep-design-runtime/engine.mjs +0 -733
  119. package/cli/lib/sweep-design-runtime/fix-topic.mjs +0 -414
  120. package/cli/lib/sweep-design-runtime/lifecycle.mjs +0 -54
  121. package/cli/lib/sweep-design-runtime/results.mjs +0 -324
  122. package/cli/lib/sweep-design.mjs +0 -8
  123. package/cli/lib/topic-artifacts.mjs +0 -186
  124. package/cli/lib/topic-authority-coverage.mjs +0 -73
  125. package/cli/lib/topic-closeout.mjs +0 -560
  126. package/cli/lib/topic-common.mjs +0 -404
  127. package/cli/lib/topic-decisions.mjs +0 -332
  128. package/cli/lib/topic-draft-packets.mjs +0 -167
  129. package/cli/lib/topic-execution.mjs +0 -533
  130. package/cli/lib/topic-goal.mjs +0 -440
  131. package/cli/lib/topic-ledger.mjs +0 -281
  132. package/cli/lib/topic-lifecycle-artifacts.mjs +0 -173
  133. package/cli/lib/topic-root-validation.mjs +0 -288
  134. package/cli/lib/topic-runner-commands.mjs +0 -174
  135. package/cli/lib/topic-runner-deferral.mjs +0 -532
  136. package/cli/lib/topic-runner-stale-gates.mjs +0 -114
  137. package/cli/lib/topic-runner-validation.mjs +0 -138
  138. package/cli/lib/topic-runner.mjs +0 -727
  139. package/cli/lib/topic-scaffold.mjs +0 -252
  140. package/cli/lib/topic-waves.mjs +0 -403
  141. package/cli/lib/topic.mjs +0 -81
  142. package/config/audit-execution-artifacts.yaml +0 -20
  143. package/config/external-execution-artifacts.yaml +0 -16
  144. package/config/host-adapter.yaml +0 -30
  145. package/config/host-profile.yaml +0 -29
  146. package/config/installer-evidence.yaml +0 -31
  147. package/config/skill-installer.yaml +0 -23
  148. package/config/skill-manifest.yaml +0 -48
  149. package/config/skills.yaml +0 -30
  150. package/contracts/acceptance.schema.yaml +0 -16
  151. package/contracts/admission-checklist.schema.yaml +0 -15
  152. package/contracts/audit-chunk.schema.yaml +0 -123
  153. package/contracts/audit-closeout.schema.yaml +0 -51
  154. package/contracts/audit-finding.schema.yaml +0 -61
  155. package/contracts/audit-ledger.schema.yaml +0 -138
  156. package/contracts/audit-plan.schema.yaml +0 -138
  157. package/contracts/audit-remediation-map.schema.yaml +0 -52
  158. package/contracts/audit-rerun.schema.yaml +0 -31
  159. package/contracts/audit-sweep-result.yaml +0 -53
  160. package/contracts/authority-convergence-audit.schema.yaml +0 -19
  161. package/contracts/closeout.schema.yaml +0 -25
  162. package/contracts/decision-review.schema.yaml +0 -16
  163. package/contracts/doc-spec-audit-result.yaml +0 -19
  164. package/contracts/execution-packet.schema.yaml +0 -49
  165. package/contracts/external-host-compatibility.yaml +0 -22
  166. package/contracts/forbidden-shortcuts.catalog.yaml +0 -23
  167. package/contracts/high-risk-admission.schema.yaml +0 -23
  168. package/contracts/high-risk-execution-result.yaml +0 -20
  169. package/contracts/orchestration-state.schema.yaml +0 -41
  170. package/contracts/overflow-continuation.schema.yaml +0 -12
  171. package/contracts/packet.schema.yaml +0 -30
  172. package/contracts/pending-note.schema.yaml +0 -17
  173. package/contracts/prompt.schema.yaml +0 -12
  174. package/contracts/remediation.schema.yaml +0 -16
  175. package/contracts/result.schema.yaml +0 -24
  176. package/contracts/spec-reconstruction-result.yaml +0 -41
  177. package/contracts/sweep-design-result.yaml +0 -349
  178. package/contracts/topic-goal.schema.yaml +0 -87
  179. package/contracts/topic-run-ledger.schema.yaml +0 -72
  180. package/contracts/topic-step-decision.schema.yaml +0 -45
  181. package/contracts/topic.schema.yaml +0 -65
  182. package/contracts/true-close.schema.yaml +0 -15
  183. package/contracts/wave.schema.yaml +0 -29
  184. package/contracts/worker-output.schema.yaml +0 -15
  185. package/contracts/workflow-consumer.schema.yaml +0 -110
  186. package/methodology/audit-sweep-p0p1-recall.yaml +0 -45
  187. package/methodology/authority-convergence-policy.yaml +0 -42
  188. package/methodology/overflow-continuation-policy.yaml +0 -14
  189. package/methodology/skill-exchange-projection.yaml +0 -114
  190. package/methodology/skill-handoff.yaml +0 -34
  191. package/methodology/skill-installer-result.yaml +0 -27
  192. package/methodology/skill-installer-summary-projection.yaml +0 -181
  193. package/methodology/skill-runtime.yaml +0 -23
  194. package/methodology/spec-target-truth-profile.yaml +0 -46
  195. package/methodology/topic-lifecycle-report.yaml +0 -144
  196. package/methodology/topic-lifecycle.yaml +0 -37
  197. package/methodology/topic-naming-ontology.yaml +0 -21
  198. package/methodology/topic-ontology.yaml +0 -38
  199. package/methodology/topic-validation-policy.yaml +0 -9
  200. package/methodology/wave-dag-policy.yaml +0 -14
  201. package/spec/_meta/command-gating-matrix.yaml +0 -143
  202. package/spec/_meta/generate-drift-migration-checklist.yaml +0 -137
  203. package/spec/_meta/governance-routing-cutover-checklist.yaml +0 -35
  204. package/spec/_meta/phase2-impacted-surface-matrix.yaml +0 -44
  205. package/spec/_meta/spec-authority-cutover-readiness.yaml +0 -102
  206. package/spec/bootstrap-state.yaml +0 -99
@@ -1,797 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
-
3
- import {
4
- CHUNK_STATES,
5
- FINDING_ACTIONABILITY,
6
- FINDING_CONFIDENCE,
7
- FINDING_DISPOSITION,
8
- FINDING_SEVERITY,
9
- RERUN_VERDICT,
10
- artifactPath,
11
- auditCloseoutRef,
12
- chunkRef,
13
- findingsRef,
14
- inputError,
15
- loadChunk,
16
- loadFindings,
17
- loadPlan,
18
- loadYamlRef,
19
- runLedgerRef,
20
- safeSweepId,
21
- sha256Object,
22
- } from "./common.mjs";
23
- import { ensureClusterStore } from "./risk-budget.mjs";
24
- import { buildAuditValidityForEvidence } from "./audit-validity.mjs";
25
- import {
26
- validateClusterShape,
27
- deriveLedgerSnapshotId,
28
- validateLatestLedger,
29
- validateRemediationMap,
30
- } from "./validators-ledger.mjs";
31
- import { pathExists } from "../fs-helpers.mjs";
32
- import { isIsoUtcTimestamp, isPlainObject } from "../value-helpers.mjs";
33
-
34
- const RUN_EVENT_TYPES = new Set([
35
- "plan_created",
36
- "chunk_dispatched",
37
- "chunk_ingested",
38
- "chunk_reviewed",
39
- "chunk_frozen",
40
- "chunk_failed",
41
- "chunk_skipped",
42
- "chunk_codex_audit_prepared",
43
- "chunk_codex_audit_failed",
44
- "chunk_codex_auditor_output_rejected",
45
- "chunk_codex_auditor_output_accepted",
46
- "chunk_claude_audit_prepared",
47
- "chunk_claude_audit_failed",
48
- "chunk_claude_auditor_output_rejected",
49
- "chunk_claude_auditor_output_accepted",
50
- "ledger_snapshot_created",
51
- "remediation_map_created",
52
- "remediation_map_admitted",
53
- "finding_resolved",
54
- "closeout_summary_projected",
55
- ]);
56
-
57
- const VALIDATION_SCOPES = new Set(["all", "plan", "chunks", "findings", "ledger", "remediation", "rerun", "closeout"]);
58
-
59
- export { deriveLedgerSnapshotId };
60
-
61
- function check(checks, id, ok, reason) {
62
- checks.push({ id, ok, reason });
63
- }
64
-
65
- function validationResult(sweepId, scope, checks) {
66
- const ok = checks.every((entry) => entry.ok);
67
- return {
68
- ok,
69
- exitCode: ok ? 0 : 2,
70
- sweepId,
71
- scope,
72
- checks,
73
- };
74
- }
75
-
76
- async function refExists(projectRoot, ref) {
77
- const info = await pathExists(artifactPath(projectRoot, ref));
78
- return Boolean(info?.isFile());
79
- }
80
-
81
- function hasRequiredFields(value, fields) {
82
- return fields.every((field) => field in value);
83
- }
84
-
85
- function nonEmptyString(value) {
86
- return typeof value === "string" && value.trim().length > 0;
87
- }
88
-
89
- function nonNegativeInteger(value) {
90
- return Number.isInteger(value) && value >= 0;
91
- }
92
-
93
- function sortedArrayEquals(left, right) {
94
- if (!Array.isArray(left) || !Array.isArray(right)) {
95
- return false;
96
- }
97
- const normalizedLeft = [...left].sort();
98
- const normalizedRight = [...right].sort();
99
- return normalizedLeft.length === normalizedRight.length
100
- && normalizedLeft.every((value, index) => value === normalizedRight[index]);
101
- }
102
-
103
- function validatePlanShape(plan, sweepId, checks) {
104
- const required = [
105
- "version",
106
- "kind",
107
- "sweep_id",
108
- "target_root",
109
- "inventory_hash",
110
- "inventory",
111
- "chunks",
112
- "coverage",
113
- "created_at",
114
- "updated_at",
115
- ];
116
- check(checks, "plan_required_fields", isPlainObject(plan) && hasRequiredFields(plan, required), "audit plan has required top-level fields");
117
- if (!isPlainObject(plan)) {
118
- return;
119
- }
120
- check(checks, "plan_identity", plan.kind === "audit-plan" && plan.sweep_id === sweepId, "audit plan kind and sweep_id match");
121
- check(checks, "plan_timestamps", isIsoUtcTimestamp(plan.created_at) && isIsoUtcTimestamp(plan.updated_at), "audit plan timestamps are ISO UTC");
122
- check(checks, "plan_arrays", Array.isArray(plan.inventory) && Array.isArray(plan.chunks), "audit plan inventory and chunks are arrays");
123
- if (!Array.isArray(plan.inventory) || !Array.isArray(plan.chunks)) {
124
- return;
125
- }
126
-
127
- const inventoryFieldsOk = plan.inventory.every((entry) => isPlainObject(entry)
128
- && hasRequiredFields(entry, ["file_ref", "sha256", "bytes", "extension", "owner_domain", "classification", "included", "exclusion_reason"])
129
- && nonEmptyString(entry.file_ref)
130
- && nonEmptyString(entry.sha256)
131
- && nonNegativeInteger(entry.bytes)
132
- && typeof entry.included === "boolean"
133
- && (entry.included ? entry.exclusion_reason === null : nonEmptyString(entry.exclusion_reason)));
134
- check(checks, "plan_inventory_entries_valid", inventoryFieldsOk, "audit plan inventory entries are complete and explicit");
135
-
136
- const recomputedInventoryHash = sha256Object(plan.inventory.map((entry) => ({
137
- file_ref: entry.file_ref,
138
- sha256: entry.sha256,
139
- included: entry.included,
140
- exclusion_reason: entry.exclusion_reason,
141
- })));
142
- check(checks, "plan_inventory_hash_matches", plan.inventory_hash === recomputedInventoryHash, "audit plan inventory_hash covers all inventory entries");
143
-
144
- const includedFiles = plan.inventory.filter((entry) => entry.included).map((entry) => entry.file_ref);
145
- const chunkFiles = plan.chunks.flatMap((chunk) => Array.isArray(chunk.files) ? chunk.files : []);
146
- check(checks, "plan_included_files_mapped_once", new Set(chunkFiles).size === chunkFiles.length
147
- && includedFiles.length === chunkFiles.length
148
- && includedFiles.every((fileRef) => chunkFiles.includes(fileRef)), "every included file belongs to exactly one chunk");
149
- check(checks, "plan_coverage_counts_match", plan.coverage?.total_files === plan.inventory.length
150
- && plan.coverage?.included_files === includedFiles.length
151
- && plan.coverage?.excluded_files === plan.inventory.length - includedFiles.length
152
- && plan.coverage?.chunk_count === plan.chunks.length, "audit plan coverage counts match inventory and chunks");
153
-
154
- const chunkSummariesOk = plan.chunks.every((chunk) => isPlainObject(chunk)
155
- && hasRequiredFields(chunk, ["chunk_id", "state", "owner_domain", "criteria", "files", "file_count"])
156
- && nonEmptyString(chunk.chunk_id)
157
- && CHUNK_STATES.has(chunk.state)
158
- && Array.isArray(chunk.criteria)
159
- && Array.isArray(chunk.files)
160
- && chunk.file_count === chunk.files.length);
161
- check(checks, "plan_chunk_summaries_valid", chunkSummariesOk, "audit plan chunk summaries are valid");
162
- if (plan.planning_basis?.mode === "spec_authority") {
163
- const evidenceInventoryOk = Array.isArray(plan.evidence_inventory)
164
- && Array.isArray(plan.unmapped_evidence_files)
165
- && nonEmptyString(plan.evidence_inventory_hash);
166
- check(checks, "plan_spec_evidence_inventory_present", evidenceInventoryOk, "spec-authority plan declares evidence inventory and unmapped evidence files");
167
- const evidenceInventoryEntriesOk = Array.isArray(plan.evidence_inventory) && plan.evidence_inventory.every((entry) => isPlainObject(entry)
168
- && hasRequiredFields(entry, ["file_ref", "sha256", "bytes", "extension", "owner_domain", "classification", "included", "exclusion_reason"])
169
- && nonEmptyString(entry.file_ref)
170
- && nonEmptyString(entry.sha256)
171
- && nonNegativeInteger(entry.bytes)
172
- && entry.included === true
173
- && entry.exclusion_reason === null);
174
- check(checks, "plan_spec_evidence_inventory_entries_valid", evidenceInventoryEntriesOk, "spec-authority evidence inventory entries are complete included files");
175
- const recomputedEvidenceInventoryHash = Array.isArray(plan.evidence_inventory)
176
- ? sha256Object(plan.evidence_inventory.map((entry) => ({
177
- file_ref: entry.file_ref,
178
- sha256: entry.sha256,
179
- included: entry.included,
180
- exclusion_reason: entry.exclusion_reason,
181
- })))
182
- : null;
183
- check(checks, "plan_spec_evidence_inventory_hash_matches", plan.evidence_inventory_hash === recomputedEvidenceInventoryHash, "spec-authority plan evidence_inventory_hash covers all evidence entries");
184
- const specChunksOk = plan.chunks.every((chunk) => Array.isArray(chunk.authority_refs)
185
- && chunk.authority_refs.length === chunk.files.length
186
- && chunk.authority_refs.every((fileRef) => chunk.files.includes(fileRef))
187
- && Array.isArray(chunk.evidence_roots)
188
- && Array.isArray(chunk.evidence_inventory)
189
- && (chunk.evidence_inventory.length > 0
190
- || (chunk.evidence_inventory_status === "empty" && nonEmptyString(chunk.evidence_inventory_empty_reason)))
191
- && isPlainObject(chunk.coverage_contract)
192
- && chunk.coverage_contract.authority_refs_required === true
193
- && chunk.coverage_contract.evidence_inventory_required === true
194
- && chunk.coverage_contract.evidence_files_must_cover_inventory === true
195
- && chunk.coverage_contract.empty_evidence_inventory_requires_reason === true
196
- && nonEmptyString(chunk.spec_surface));
197
- check(checks, "plan_spec_authority_chunks_valid", specChunksOk, "spec-authority audit chunks declare authority refs, evidence roots, empty-evidence posture, and coverage contract");
198
- const appSliceAdmissions = Array.isArray(plan.app_slice_admissions) ? plan.app_slice_admissions : [];
199
- const appAdmissionByRef = new Map(appSliceAdmissions
200
- .filter((entry) => isPlainObject(entry) && nonEmptyString(entry.admission_ref))
201
- .map((entry) => [entry.admission_ref, entry]));
202
- const appAuthorityChunks = plan.chunks.filter((chunk) => (
203
- chunk.authority_kind === "admitted_app_slice"
204
- || (Array.isArray(chunk.authority_refs) && chunk.authority_refs.some((fileRef) => String(fileRef).startsWith("apps/")))
205
- ));
206
- const appAuthorityChunksOk = appAuthorityChunks.every((chunk) => {
207
- const admission = appAdmissionByRef.get(chunk.admission_ref);
208
- return chunk.authority_kind === "admitted_app_slice"
209
- && nonEmptyString(chunk.app_id)
210
- && nonEmptyString(chunk.admission_ref)
211
- && nonEmptyString(chunk.authority_root)
212
- && admission
213
- && admission.app_id === chunk.app_id
214
- && admission.authority_root === chunk.authority_root
215
- && Array.isArray(admission.evidence_roots)
216
- && sortedArrayEquals(chunk.evidence_roots, admission.evidence_roots)
217
- && chunk.authority_refs.every((fileRef) => String(fileRef).startsWith(`${chunk.authority_root}/`));
218
- });
219
- check(checks, "plan_spec_app_slice_authority_admitted", appAuthorityChunksOk, "app-local authority chunks are admitted through .nimi/spec app-slice admissions");
220
- const packageAuthorityAdmissions = Array.isArray(plan.package_authority_admissions) ? plan.package_authority_admissions : [];
221
- const packageAdmissionByRef = new Map(packageAuthorityAdmissions
222
- .filter((entry) => isPlainObject(entry) && nonEmptyString(entry.admission_ref))
223
- .map((entry) => [entry.admission_ref, entry]));
224
- const packageAuthorityChunks = plan.chunks.filter((chunk) => (
225
- chunk.authority_kind === "admitted_package_authority"
226
- || (Array.isArray(chunk.authority_refs) && chunk.authority_refs.some((fileRef) => String(fileRef).includes("/spec/") && !String(fileRef).startsWith(".nimi/spec/") && !String(fileRef).startsWith("apps/")))
227
- ));
228
- const packageAuthorityChunksOk = packageAuthorityChunks.every((chunk) => {
229
- const admission = packageAdmissionByRef.get(chunk.admission_ref);
230
- const declaredProjections = Array.isArray(admission?.host_authority_projection_refs)
231
- ? admission.host_authority_projection_refs
232
- : [];
233
- const declaredProjectionByHost = new Map(declaredProjections
234
- .filter((entry) => isPlainObject(entry) && nonEmptyString(entry.host_ref) && nonEmptyString(entry.package_ref))
235
- .map((entry) => [entry.host_ref, entry.package_ref]));
236
- const chunkProjections = Array.isArray(chunk.host_authority_projection_refs) ? chunk.host_authority_projection_refs : [];
237
- const chunkProjectionByHost = new Map(chunkProjections
238
- .filter((entry) => isPlainObject(entry) && nonEmptyString(entry.host_ref) && nonEmptyString(entry.package_ref))
239
- .map((entry) => [entry.host_ref, entry.package_ref]));
240
- const chunkProjectionRefsOk = chunkProjections.every((entry) => isPlainObject(entry)
241
- && nonEmptyString(entry.host_ref)
242
- && nonEmptyString(entry.package_ref)
243
- && declaredProjectionByHost.get(entry.host_ref) === entry.package_ref
244
- && chunk.authority_refs.includes(entry.host_ref)
245
- && chunk.authority_refs.includes(entry.package_ref));
246
- return chunk.authority_kind === "admitted_package_authority"
247
- && nonEmptyString(chunk.package_authority_id)
248
- && nonEmptyString(chunk.admission_ref)
249
- && nonEmptyString(chunk.authority_root)
250
- && admission
251
- && admission.id === chunk.package_authority_id
252
- && admission.authority_root === chunk.authority_root
253
- && Array.isArray(admission.evidence_roots)
254
- && sortedArrayEquals(chunk.evidence_roots, admission.evidence_roots)
255
- && chunkProjectionRefsOk
256
- && chunk.authority_refs.every((fileRef) => (
257
- String(fileRef).startsWith(`${chunk.authority_root}/`)
258
- || chunkProjectionByHost.get(String(fileRef)) === declaredProjectionByHost.get(String(fileRef))
259
- ));
260
- });
261
- check(checks, "plan_spec_package_authority_admitted", packageAuthorityChunksOk, "package-local authority chunks are admitted through .nimi/spec package authority admissions");
262
- const delegatedProjectionAdmissions = Array.isArray(plan.delegated_projection_admissions) ? plan.delegated_projection_admissions : [];
263
- const delegatedAdmissionByRef = new Map(delegatedProjectionAdmissions
264
- .filter((entry) => isPlainObject(entry) && nonEmptyString(entry.admission_ref))
265
- .map((entry) => [entry.admission_ref, entry]));
266
- const delegatedProjectionChunks = plan.chunks.filter((chunk) => chunk.authority_kind === "delegated_projection");
267
- const delegatedProjectionChunksOk = delegatedProjectionChunks.every((chunk) => {
268
- const admission = delegatedAdmissionByRef.get(chunk.admission_ref);
269
- const prefixes = Array.isArray(admission?.delegated_declared_evidence_prefixes)
270
- ? admission.delegated_declared_evidence_prefixes
271
- : [];
272
- const delegatedRefs = Array.isArray(chunk.delegated_declared_evidence_refs)
273
- ? chunk.delegated_declared_evidence_refs
274
- : [];
275
- const delegatedRefsOk = delegatedRefs.every((ref) => prefixes.some((prefix) => (
276
- String(ref) === String(prefix).replace(/\/$/u, "")
277
- || String(ref).startsWith(`${String(prefix).replace(/\/$/u, "")}/`)
278
- )));
279
- return nonEmptyString(chunk.delegated_projection_id)
280
- && nonEmptyString(chunk.admission_ref)
281
- && nonEmptyString(chunk.authority_root)
282
- && nonEmptyString(chunk.source_authority_root)
283
- && admission
284
- && admission.id === chunk.delegated_projection_id
285
- && admission.authority_root === chunk.authority_root
286
- && admission.source_authority_root === chunk.source_authority_root
287
- && Array.isArray(admission.local_projection_evidence_roots)
288
- && sortedArrayEquals(chunk.evidence_roots, admission.local_projection_evidence_roots)
289
- && sortedArrayEquals(chunk.admitted_evidence_roots, admission.local_projection_evidence_roots)
290
- && sortedArrayEquals(chunk.delegated_evidence_roots, admission.delegated_evidence_roots)
291
- && sortedArrayEquals(chunk.delegated_declared_evidence_prefixes, admission.delegated_declared_evidence_prefixes)
292
- && delegatedRefsOk
293
- && chunk.authority_refs.every((fileRef) => String(fileRef).startsWith(`${chunk.authority_root}/`));
294
- });
295
- check(checks, "plan_spec_delegated_projection_authority_admitted", delegatedProjectionChunksOk, "delegated projection chunks are admitted through .nimi/spec delegated projection admissions");
296
- const evidenceRootAdmissionsOk = plan.chunks.every((chunk) => (
297
- !Array.isArray(chunk.evidence_root_admission_refs)
298
- || chunk.evidence_root_admission_refs.every((ref) => (
299
- nonEmptyString(ref)
300
- && ref.startsWith(".nimi/spec/")
301
- && ref.includes("/kernel/tables/audit-evidence-roots.yaml#")
302
- ))
303
- ));
304
- check(checks, "plan_spec_evidence_root_admissions_valid", evidenceRootAdmissionsOk, "authority-specific evidence roots are admitted through .nimi/spec audit evidence root tables");
305
- const evidenceInventoryFiles = Array.isArray(plan.evidence_inventory) ? plan.evidence_inventory.map((entry) => entry.file_ref) : [];
306
- const unmappedEvidenceFiles = Array.isArray(plan.unmapped_evidence_files) ? plan.unmapped_evidence_files : [];
307
- const mappedEvidenceFiles = plan.chunks.flatMap((chunk) => Array.isArray(chunk.evidence_inventory) ? chunk.evidence_inventory : []);
308
- const unresolvedDeclaredEvidenceChunks = plan.chunks
309
- .filter((chunk) => Array.isArray(chunk.declared_evidence_unresolved) && chunk.declared_evidence_unresolved.length > 0)
310
- .map((chunk) => chunk.chunk_id);
311
- const mappedEvidenceSet = new Set(mappedEvidenceFiles);
312
- const expectedMappedFiles = evidenceInventoryFiles.filter((fileRef) => !unmappedEvidenceFiles.includes(fileRef));
313
- check(checks, "plan_spec_evidence_inventory_mapped", expectedMappedFiles.every((fileRef) => mappedEvidenceSet.has(fileRef))
314
- && mappedEvidenceFiles.every((fileRef) => evidenceInventoryFiles.includes(fileRef)), "every mapped evidence inventory file belongs to at least one chunk");
315
- check(checks, "plan_spec_unmapped_evidence_declared", unmappedEvidenceFiles.every((fileRef) => evidenceInventoryFiles.includes(fileRef)), "unmapped evidence files belong to the evidence inventory");
316
- check(checks, "plan_spec_unmapped_evidence_fail_closed", unmappedEvidenceFiles.length === 0, "spec-authority plans have no unmapped evidence files");
317
- const declaredEvidenceBlockerPresent = plan.coverage_quality?.blockers?.some((blocker) => (
318
- blocker?.id === "declared_evidence_target_unresolved"
319
- && Array.isArray(blocker.chunk_ids)
320
- && unresolvedDeclaredEvidenceChunks.every((chunkId) => blocker.chunk_ids.includes(chunkId))
321
- )) === true;
322
- check(checks, "plan_spec_declared_evidence_resolved", unresolvedDeclaredEvidenceChunks.length === 0 || declaredEvidenceBlockerPresent, "spec-authority unresolved declared evidence targets are either resolved or represented as coverage-quality blockers");
323
- check(checks, "plan_spec_coverage_counts_match", plan.coverage?.authority_files === includedFiles.length
324
- && plan.coverage?.evidence_files === evidenceInventoryFiles.length
325
- && plan.coverage?.unmapped_evidence_files === unmappedEvidenceFiles.length
326
- && plan.coverage?.authority_chunks_without_evidence_inventory === plan.chunks.filter((chunk) => (chunk.evidence_inventory ?? []).length === 0).length,
327
- "spec-authority coverage counts split authority and evidence inventory");
328
- }
329
- }
330
-
331
- function validateChunkShape(chunk, plan, checks) {
332
- const required = [
333
- "version",
334
- "kind",
335
- "sweep_id",
336
- "chunk_id",
337
- "state",
338
- "owner_domain",
339
- "criteria",
340
- "files",
341
- "file_hashes",
342
- "lifecycle",
343
- "created_at",
344
- "updated_at",
345
- ];
346
- check(checks, `chunk_${chunk?.chunk_id ?? "unknown"}_required_fields`, isPlainObject(chunk) && hasRequiredFields(chunk, required), "audit chunk has required top-level fields");
347
- if (!isPlainObject(chunk)) {
348
- return;
349
- }
350
- const planChunk = plan.chunks.find((entry) => entry.chunk_id === chunk.chunk_id) ?? null;
351
- check(checks, `chunk_${chunk.chunk_id}_plan_link`, chunk.kind === "audit-chunk" && planChunk !== null && planChunk.state === chunk.state, "audit chunk links back to plan state");
352
- check(checks, `chunk_${chunk.chunk_id}_state_valid`, CHUNK_STATES.has(chunk.state), "audit chunk state is valid");
353
- check(checks, `chunk_${chunk.chunk_id}_files_match_plan`, Array.isArray(chunk.files)
354
- && chunk.file_count === chunk.files.length
355
- && planChunk !== null
356
- && JSON.stringify([...chunk.files].sort()) === JSON.stringify([...planChunk.files].sort()), "audit chunk files match plan");
357
- const inventoryByFile = new Map(plan.inventory.map((entry) => [entry.file_ref, entry]));
358
- const hashesOk = Array.isArray(chunk.files) && isPlainObject(chunk.file_hashes)
359
- && chunk.files.every((fileRef) => chunk.file_hashes[fileRef] === inventoryByFile.get(fileRef)?.sha256);
360
- check(checks, `chunk_${chunk.chunk_id}_hashes_match_inventory`, hashesOk, "audit chunk file hashes match inventory");
361
- if (chunk.planning_basis === "spec_authority") {
362
- const specChunkOk = Array.isArray(chunk.authority_refs)
363
- && chunk.authority_refs.length === chunk.files.length
364
- && chunk.authority_refs.every((fileRef) => chunk.files.includes(fileRef))
365
- && Array.isArray(chunk.evidence_roots)
366
- && Array.isArray(chunk.evidence_inventory)
367
- && (chunk.evidence_inventory.length > 0
368
- || (chunk.evidence_inventory_status === "empty" && nonEmptyString(chunk.evidence_inventory_empty_reason)))
369
- && isPlainObject(chunk.coverage_contract)
370
- && chunk.coverage_contract.authority_refs_required === true
371
- && chunk.coverage_contract.evidence_inventory_required === true
372
- && chunk.coverage_contract.evidence_files_must_cover_inventory === true
373
- && chunk.coverage_contract.empty_evidence_inventory_requires_reason === true
374
- && nonEmptyString(chunk.spec_surface);
375
- check(checks, `chunk_${chunk.chunk_id}_spec_authority_fields`, specChunkOk, "spec-authority chunk declares authority refs, evidence inventory, empty-evidence posture, and coverage contract");
376
- check(checks, `chunk_${chunk.chunk_id}_evidence_inventory_matches_plan`, planChunk !== null
377
- && sortedArrayEquals(chunk.evidence_inventory, planChunk.evidence_inventory), "spec-authority chunk evidence inventory matches plan");
378
- const evidenceInventoryByFile = new Map((plan.evidence_inventory ?? []).map((entry) => [entry.file_ref, entry]));
379
- const evidenceHashesOk = Array.isArray(chunk.evidence_inventory)
380
- && isPlainObject(chunk.evidence_file_hashes)
381
- && chunk.evidence_inventory.every((fileRef) => chunk.evidence_file_hashes[fileRef] === evidenceInventoryByFile.get(fileRef)?.sha256);
382
- check(checks, `chunk_${chunk.chunk_id}_evidence_hashes_match_inventory`, evidenceHashesOk, "spec-authority chunk evidence hashes match evidence inventory");
383
- }
384
- const lifecycle = chunk.lifecycle;
385
- const lifecycleOk = isPlainObject(lifecycle)
386
- && ["planned_at", "dispatched_at", "ingested_at", "reviewed_at", "frozen_at", "failed_at", "skipped_at"].every((field) => field in lifecycle)
387
- && isIsoUtcTimestamp(lifecycle.planned_at);
388
- check(checks, `chunk_${chunk.chunk_id}_lifecycle_valid`, lifecycleOk, "audit chunk lifecycle is explicit");
389
- const lifecycleMatchesState = lifecycleOk && chunkLifecycleMatchesState(chunk);
390
- check(checks, `chunk_${chunk.chunk_id}_lifecycle_matches_state`, lifecycleMatchesState, "audit chunk lifecycle timestamps match current state");
391
- check(checks, `chunk_${chunk.chunk_id}_dispatch_posture`, chunk.state === "planned" || chunk.state === "skipped" || isPlainObject(chunk.dispatch), "non-planned, non-skipped chunks have dispatch packet posture");
392
- check(checks, `chunk_${chunk.chunk_id}_ingest_posture`, !["ingested", "reviewed", "frozen"].includes(chunk.state) || nonEmptyString(chunk.evidence_ref), "ingested or frozen chunks reference audit evidence");
393
- check(checks, `chunk_${chunk.chunk_id}_frozen_review`, chunk.state !== "frozen" || chunk.review?.verdict === "pass", "frozen chunks have passing manager review");
394
- check(checks, `chunk_${chunk.chunk_id}_failure_or_skip_reason`, !["failed", "skipped"].includes(chunk.state)
395
- || nonEmptyString(chunk.failure?.reason)
396
- || nonEmptyString(chunk.skip?.reason)
397
- || nonEmptyString(chunk.review?.summary), "failed or skipped chunks have an explicit reason");
398
- }
399
-
400
- function timestampPresent(value) {
401
- return isIsoUtcTimestamp(value);
402
- }
403
-
404
- function timestampAbsent(value) {
405
- return value === null || value === undefined;
406
- }
407
-
408
- function chunkLifecycleMatchesState(chunk) {
409
- const lifecycle = chunk.lifecycle;
410
- const afterPlanned = ["dispatched_at", "ingested_at", "reviewed_at", "frozen_at", "failed_at", "skipped_at"];
411
- if (chunk.state === "planned") {
412
- return afterPlanned.every((field) => timestampAbsent(lifecycle[field]));
413
- }
414
- if (chunk.state === "dispatched") {
415
- return timestampPresent(lifecycle.dispatched_at)
416
- && ["ingested_at", "reviewed_at", "frozen_at", "failed_at", "skipped_at"].every((field) => timestampAbsent(lifecycle[field]));
417
- }
418
- if (chunk.state === "ingested") {
419
- return timestampPresent(lifecycle.dispatched_at)
420
- && timestampPresent(lifecycle.ingested_at)
421
- && ["reviewed_at", "frozen_at", "failed_at", "skipped_at"].every((field) => timestampAbsent(lifecycle[field]));
422
- }
423
- if (chunk.state === "reviewed") {
424
- return timestampPresent(lifecycle.dispatched_at)
425
- && timestampPresent(lifecycle.ingested_at)
426
- && timestampPresent(lifecycle.reviewed_at)
427
- && ["frozen_at", "failed_at", "skipped_at"].every((field) => timestampAbsent(lifecycle[field]));
428
- }
429
- if (chunk.state === "frozen") {
430
- return timestampPresent(lifecycle.dispatched_at)
431
- && timestampPresent(lifecycle.ingested_at)
432
- && timestampPresent(lifecycle.reviewed_at)
433
- && timestampPresent(lifecycle.frozen_at)
434
- && ["failed_at", "skipped_at"].every((field) => timestampAbsent(lifecycle[field]));
435
- }
436
- if (chunk.state === "failed") {
437
- return timestampPresent(lifecycle.dispatched_at)
438
- && timestampPresent(lifecycle.failed_at)
439
- && timestampAbsent(lifecycle.skipped_at);
440
- }
441
- if (chunk.state === "skipped") {
442
- return timestampPresent(lifecycle.skipped_at)
443
- && ["dispatched_at", "ingested_at", "reviewed_at", "frozen_at", "failed_at"].every((field) => timestampAbsent(lifecycle[field]));
444
- }
445
- return false;
446
- }
447
-
448
- function validateSpecAuthorityCoverageEnvelope(evidence, chunk) {
449
- if (!isPlainObject(evidence) || evidence.chunk_id !== chunk.chunk_id) {
450
- return { ok: false, reason: "audit evidence chunk_id matches chunk" };
451
- }
452
- if (!isPlainObject(evidence.coverage)) {
453
- return { ok: false, reason: "audit evidence coverage is an object" };
454
- }
455
- if (!Array.isArray(evidence.coverage.authority_refs)) {
456
- return { ok: false, reason: "spec-authority evidence declares authority_refs" };
457
- }
458
- const coveredAuthority = [...evidence.coverage.authority_refs].sort();
459
- const expectedAuthority = [...(chunk.authority_refs ?? chunk.files)].sort();
460
- if (coveredAuthority.length !== expectedAuthority.length || coveredAuthority.some((fileRef, index) => fileRef !== expectedAuthority[index])) {
461
- return { ok: false, reason: "spec-authority evidence covers exactly the chunk authority refs" };
462
- }
463
- if (!Array.isArray(evidence.coverage.files) || !sortedArrayEquals(evidence.coverage.files, expectedAuthority)) {
464
- return { ok: false, reason: "spec-authority evidence coverage.files matches authority_refs only" };
465
- }
466
- if (!Array.isArray(evidence.coverage.evidence_files)) {
467
- return { ok: false, reason: "spec-authority evidence declares examined evidence_files" };
468
- }
469
- const evidenceFiles = evidence.coverage.evidence_files.map((fileRef) => typeof fileRef === "string" ? fileRef.replace(/\\/g, "/") : fileRef);
470
- if (evidenceFiles.some((fileRef) => typeof fileRef !== "string")) {
471
- return { ok: false, reason: "spec-authority evidence_files are file refs" };
472
- }
473
- if (!sortedArrayEquals(evidenceFiles, chunk.evidence_inventory ?? [])) {
474
- return { ok: false, reason: "spec-authority evidence_files exactly cover chunk evidence inventory" };
475
- }
476
- if (!Array.isArray(evidence.coverage.authority_outcomes)) {
477
- return { ok: false, reason: "spec-authority evidence declares authority_outcomes" };
478
- }
479
- const expectedAuthoritySet = new Set(expectedAuthority);
480
- const seenAuthorityRefs = new Set();
481
- for (const outcome of evidence.coverage.authority_outcomes) {
482
- if (!isPlainObject(outcome)) {
483
- return { ok: false, reason: "authority_outcomes entries are objects" };
484
- }
485
- const authorityRef = typeof outcome.authority_ref === "string" ? outcome.authority_ref.replace(/\\/g, "/") : "";
486
- if (!expectedAuthoritySet.has(authorityRef) || seenAuthorityRefs.has(authorityRef)) {
487
- return { ok: false, reason: "authority_outcomes map one-to-one to chunk authority refs" };
488
- }
489
- seenAuthorityRefs.add(authorityRef);
490
- if (!["audited", "blocked", "not_applicable"].includes(outcome.status)) {
491
- return { ok: false, reason: "authority_outcomes status is valid" };
492
- }
493
- if (!Array.isArray(outcome.evidence_refs)) {
494
- return { ok: false, reason: "authority_outcomes evidence_refs are arrays" };
495
- }
496
- for (const evidenceRef of outcome.evidence_refs) {
497
- if (typeof evidenceRef !== "string" || !chunkAllowsFindingFile(chunk, evidenceRef.replace(/\\/g, "/"))) {
498
- return { ok: false, reason: "authority_outcomes evidence_refs belong to authority refs or evidence inventory" };
499
- }
500
- }
501
- if (outcome.status === "audited" && outcome.evidence_refs.length === 0) {
502
- return { ok: false, reason: "audited authority_outcomes declare evidence_refs" };
503
- }
504
- if (outcome.status !== "audited" && !nonEmptyString(outcome.reason)) {
505
- return { ok: false, reason: "non-audited authority_outcomes declare reason" };
506
- }
507
- }
508
- const auditValidity = buildAuditValidityForEvidence(chunk, evidence);
509
- if (auditValidity.posture === "invalid") {
510
- return {
511
- ok: false,
512
- reason: `spec-authority evidence audit_validity is invalid: ${auditValidity.blockers.map((blocker) => blocker.id).join(", ")}`,
513
- };
514
- }
515
- return {
516
- ok: seenAuthorityRefs.size === expectedAuthority.length,
517
- reason: "spec-authority evidence declares one authority outcome per authority ref",
518
- };
519
- }
520
-
521
- async function validateChunkEvidenceArtifact(projectRoot, chunk, checks) {
522
- if (!["ingested", "reviewed", "frozen", "failed"].includes(chunk.state)) {
523
- return;
524
- }
525
- if (!nonEmptyString(chunk.evidence_ref)) {
526
- return;
527
- }
528
- let evidence = null;
529
- try {
530
- evidence = JSON.parse(await readFile(artifactPath(projectRoot, chunk.evidence_ref), "utf8"));
531
- } catch {
532
- check(checks, `chunk_${chunk.chunk_id}_evidence_json_valid`, false, "chunk evidence artifact is valid JSON");
533
- return;
534
- }
535
- check(checks, `chunk_${chunk.chunk_id}_evidence_json_valid`, true, "chunk evidence artifact is valid JSON");
536
- if (chunk.planning_basis === "spec_authority") {
537
- const coverageCheck = validateSpecAuthorityCoverageEnvelope(evidence, chunk);
538
- check(checks, `chunk_${chunk.chunk_id}_spec_authority_evidence_coverage`, coverageCheck.ok, coverageCheck.reason);
539
- }
540
- }
541
-
542
- function isInsideRef(rootRef, fileRef) {
543
- const normalizedRoot = rootRef.replace(/\\/g, "/").replace(/\/$/, "");
544
- return fileRef === normalizedRoot || fileRef.startsWith(`${normalizedRoot}/`);
545
- }
546
-
547
- function chunkAllowsFindingFile(chunk, fileRef) {
548
- if (chunk?.files?.includes(fileRef)) {
549
- return true;
550
- }
551
- if (chunk?.planning_basis !== "spec_authority") {
552
- return false;
553
- }
554
- return Array.isArray(chunk.evidence_inventory) && chunk.evidence_inventory.includes(fileRef);
555
- }
556
-
557
- function validateFindingShape(finding, chunksById, checks) {
558
- const required = [
559
- "id",
560
- "sweep_id",
561
- "chunk_id",
562
- "fingerprint",
563
- "severity",
564
- "category",
565
- "actionability",
566
- "confidence",
567
- "impact",
568
- "location",
569
- "title",
570
- "description",
571
- "evidence",
572
- "disposition",
573
- "evidence_ref",
574
- ];
575
- check(checks, `finding_${finding?.id ?? "unknown"}_required_fields`, isPlainObject(finding) && hasRequiredFields(finding, required), "audit finding has required fields");
576
- if (!isPlainObject(finding)) {
577
- return;
578
- }
579
- const chunk = chunksById.get(finding.chunk_id) ?? null;
580
- check(checks, `finding_${finding.id}_enums_valid`, FINDING_SEVERITY.has(finding.severity)
581
- && FINDING_ACTIONABILITY.has(finding.actionability)
582
- && FINDING_CONFIDENCE.has(finding.confidence)
583
- && FINDING_DISPOSITION.has(finding.disposition), "audit finding enums are valid");
584
- check(checks, `finding_${finding.id}_location_in_chunk`, chunk !== null
585
- && nonEmptyString(finding.location?.file)
586
- && chunkAllowsFindingFile(chunk, finding.location.file), "audit finding location belongs to its source chunk");
587
- check(checks, `finding_${finding.id}_evidence_valid`, nonEmptyString(finding.evidence?.summary)
588
- && nonEmptyString(finding.evidence?.auditor_reasoning)
589
- && nonEmptyString(finding.evidence_ref), "audit finding evidence is explicit");
590
- check(checks, `finding_${finding.id}_resolution_required`, finding.disposition === "open" || isPlainObject(finding.resolution), "non-open findings have resolution evidence");
591
- if (finding.disposition !== "open" && isPlainObject(finding.resolution)) {
592
- const rerun = finding.resolution.rerun;
593
- check(checks, `finding_${finding.id}_rerun_valid`, nonEmptyString(finding.resolution.evidence_ref)
594
- && isPlainObject(rerun)
595
- && Array.isArray(rerun.covered_files)
596
- && rerun.covered_files.includes(finding.location.file)
597
- && RERUN_VERDICT.has(rerun.verdict), "resolved finding has valid rerun evidence");
598
- check(checks, `finding_${finding.id}_rerun_disposition_match`, finding.disposition !== "remediated" || rerun?.verdict === "not_reproduced", "remediated findings require not_reproduced rerun verdict");
599
- }
600
- }
601
-
602
- async function loadChunksForPlan(projectRoot, sweepId, plan, checks) {
603
- const chunks = [];
604
- for (const chunkSummary of Array.isArray(plan.chunks) ? plan.chunks : []) {
605
- const loaded = await loadChunk(projectRoot, sweepId, chunkSummary.chunk_id);
606
- check(checks, `chunk_${chunkSummary.chunk_id}_artifact_exists`, loaded.ok, `chunk artifact exists for ${chunkSummary.chunk_id}`);
607
- if (loaded.ok) {
608
- chunks.push(loaded.chunk);
609
- }
610
- }
611
- return chunks;
612
- }
613
-
614
- async function loadRunLedgerEvents(projectRoot, sweepId, checks) {
615
- const ref = runLedgerRef(sweepId);
616
- let text = "";
617
- try {
618
- text = await readFile(artifactPath(projectRoot, ref), "utf8");
619
- } catch {
620
- check(checks, "run_ledger_exists", false, "audit run ledger exists");
621
- return [];
622
- }
623
- const events = [];
624
- for (const [index, line] of text.split(/\r?\n/).filter(Boolean).entries()) {
625
- try {
626
- const event = JSON.parse(line);
627
- const valid = event.sweep_id === sweepId
628
- && RUN_EVENT_TYPES.has(event.event_type)
629
- && nonEmptyString(event.event_id)
630
- && isIsoUtcTimestamp(event.recorded_at);
631
- check(checks, `run_ledger_event_${index + 1}_valid`, valid, `run ledger event ${index + 1} is structurally valid`);
632
- events.push(event);
633
- } catch {
634
- check(checks, `run_ledger_event_${index + 1}_valid`, false, `run ledger event ${index + 1} is valid JSON`);
635
- }
636
- }
637
- check(checks, "run_ledger_non_empty", events.length > 0, "audit run ledger has events");
638
- return events;
639
- }
640
-
641
- function validateRunLedgerReplay(events, plan, chunks, findings, latestLedger, checks) {
642
- const eventsByType = new Map();
643
- for (const event of events) {
644
- const list = eventsByType.get(event.event_type) ?? [];
645
- list.push(event);
646
- eventsByType.set(event.event_type, list);
647
- }
648
- check(checks, "run_replay_plan_created", eventsByType.get("plan_created")?.some((event) => event.plan_ref === planRefFromPlan(plan)) === true, "run ledger records plan_created for this plan");
649
- for (const chunk of chunks) {
650
- const dispatched = eventsByType.get("chunk_dispatched")?.some((event) => event.chunk_id === chunk.chunk_id) === true
651
- || eventsByType.get("chunk_codex_audit_prepared")?.some((event) => event.chunk_id === chunk.chunk_id) === true
652
- || eventsByType.get("chunk_claude_audit_prepared")?.some((event) => event.chunk_id === chunk.chunk_id) === true;
653
- const ingested = eventsByType.get("chunk_ingested")?.some((event) => event.chunk_id === chunk.chunk_id && event.evidence_ref === chunk.evidence_ref) === true;
654
- const frozen = eventsByType.get("chunk_frozen")?.some((event) => event.chunk_id === chunk.chunk_id) === true;
655
- const failed = eventsByType.get("chunk_failed")?.some((event) => event.chunk_id === chunk.chunk_id) === true;
656
- const skipped = eventsByType.get("chunk_skipped")?.some((event) => event.chunk_id === chunk.chunk_id) === true;
657
- const dispatchRequired = chunk.state !== "planned" && chunk.state !== "skipped";
658
- const ingestRequired = ["ingested", "reviewed", "frozen"].includes(chunk.state);
659
- const terminalRequired = ["frozen", "failed", "skipped"].includes(chunk.state);
660
- check(checks, `run_replay_${chunk.chunk_id}_dispatch`, !dispatchRequired || dispatched, dispatchRequired ? `run ledger records dispatch for ${chunk.chunk_id}` : `run ledger dispatch not required for planned chunk ${chunk.chunk_id}`);
661
- check(checks, `run_replay_${chunk.chunk_id}_ingest`, !ingestRequired || ingested, ingestRequired ? `run ledger records ingest for ${chunk.chunk_id}` : `run ledger ingest not required for ${chunk.state} chunk ${chunk.chunk_id}`);
662
- check(checks, `run_replay_${chunk.chunk_id}_terminal`, !terminalRequired || ((chunk.state !== "frozen" || frozen) && (chunk.state !== "failed" || failed) && (chunk.state !== "skipped" || skipped)), terminalRequired ? `run ledger records terminal state for ${chunk.chunk_id}` : `run ledger terminal event not required for ${chunk.state} chunk ${chunk.chunk_id}`);
663
- }
664
- for (const finding of findings.filter((entry) => entry.disposition !== "open")) {
665
- check(checks, `run_replay_${finding.id}_resolution`, eventsByType.get("finding_resolved")?.some((event) => event.finding_id === finding.id && event.evidence_ref === finding.resolution?.evidence_ref) === true, `run ledger records resolution for ${finding.id}`);
666
- }
667
- if (latestLedger) {
668
- check(checks, "run_replay_latest_ledger", eventsByType.get("ledger_snapshot_created")?.some((event) => event.ledger_ref === latestLedger.ledger_ref && event.snapshot_id === latestLedger.snapshot_id) === true, "run ledger records latest ledger snapshot");
669
- }
670
- }
671
-
672
- function planRefFromPlan(plan) {
673
- return `.nimi/local/audit/plans/${plan.sweep_id}.yaml`;
674
- }
675
-
676
- function deriveExpectedCloseoutPosture(closeout, openCount) {
677
- if (closeout.audit_validity?.posture === "invalid") {
678
- return "audit_invalid_no_finding_evidence";
679
- }
680
- if (closeout.coverage_status === "blocked") {
681
- return "blocked";
682
- }
683
- if (closeout.coverage_status === "partial") {
684
- return openCount > 0 ? "partial_coverage_findings_open" : "partial_coverage_all_findings_postured";
685
- }
686
- return openCount > 0 ? "audit_complete_findings_open" : "audit_complete_all_findings_postured";
687
- }
688
-
689
- async function validateEvidenceRefs(projectRoot, refs, checks, prefix) {
690
- for (const ref of refs.filter((entry) => typeof entry === "string" && entry.trim())) {
691
- check(checks, `${prefix}_${ref.replace(/[^a-zA-Z0-9]+/g, "_")}_exists`, await refExists(projectRoot, ref), `referenced artifact exists: ${ref}`);
692
- }
693
- }
694
-
695
- async function validateCloseoutArtifact(projectRoot, sweepId, ledgerInfo, remediationInfo, findings, checks) {
696
- if (!ledgerInfo) {
697
- check(checks, "closeout_ledger_available", false, "closeout validation requires latest ledger");
698
- return null;
699
- }
700
- const closeoutRef = auditCloseoutRef(sweepId, ledgerInfo.snapshot_id);
701
- const closeout = await loadYamlRef(projectRoot, closeoutRef);
702
- if (!isPlainObject(closeout)) {
703
- check(checks, "audit_closeout_exists", false, "audit closeout artifact exists");
704
- return null;
705
- }
706
- const openCount = findings.filter((finding) => finding.disposition === "open").length;
707
- check(checks, "audit_closeout_identity", closeout.kind === "audit-closeout"
708
- && closeout.sweep_id === sweepId
709
- && closeout.ledger_ref === ledgerInfo.ledger_ref
710
- && closeout.remediation_map_ref === remediationInfo?.remediation_map_ref
711
- && closeout.audit_closeout_ref === closeoutRef, "audit closeout references latest ledger and remediation map");
712
- check(checks, "audit_closeout_posture", closeout.closeout_posture === deriveExpectedCloseoutPosture(closeout, openCount), "audit closeout posture matches coverage and finding state");
713
- check(checks, "audit_closeout_coverage_status", closeout.coverage_status !== "full"
714
- || ledgerInfo.ledger?.status === "candidate_ready", "audit closeout full coverage requires candidate_ready ledger");
715
- check(checks, "audit_closeout_partial_not_complete", closeout.coverage_status !== "partial"
716
- || !String(closeout.closeout_posture).startsWith("audit_complete_"), "partial coverage closeout cannot use audit_complete posture");
717
- check(checks, "audit_closeout_invalid_no_finding_posture", closeout.audit_validity?.posture !== "invalid"
718
- || closeout.closeout_posture === "audit_invalid_no_finding_evidence", "invalid audit validity requires audit_invalid_no_finding_evidence closeout posture");
719
- check(checks, "audit_closeout_coverage_quality_present", !ledgerInfo.ledger?.coverage?.authority_coverage
720
- || isPlainObject(closeout.coverage_quality), "spec-authority closeout exposes coverage_quality");
721
- check(checks, "audit_closeout_audit_validity_present", !ledgerInfo.ledger?.coverage?.authority_coverage
722
- || isPlainObject(closeout.audit_validity), "spec-authority closeout exposes audit_validity");
723
- check(checks, "audit_closeout_verified_at", isIsoUtcTimestamp(closeout.verified_at), "audit closeout verified_at is ISO UTC");
724
- return { closeout, audit_closeout_ref: closeoutRef };
725
- }
726
-
727
- export async function validateAuditSweepArtifacts(projectRoot, options) {
728
- const sweepId = safeSweepId(options.sweepId);
729
- if (!sweepId) {
730
- return inputError("nimicoding sweep audit refused: --sweep-id is required.\n");
731
- }
732
- const scope = options.scope ?? "all";
733
- if (!VALIDATION_SCOPES.has(scope)) {
734
- return inputError("nimicoding sweep audit refused: --scope must be one of all, plan, chunks, findings, ledger, remediation, rerun, closeout.\n");
735
- }
736
-
737
- const checks = [];
738
- const planResult = await loadPlan(projectRoot, sweepId);
739
- check(checks, "plan_loadable", planResult.ok, "audit plan is loadable");
740
- if (!planResult.ok) {
741
- return validationResult(sweepId, scope, checks);
742
- }
743
-
744
- validatePlanShape(planResult.plan, sweepId, checks);
745
- if (scope === "plan") {
746
- return validationResult(sweepId, scope, checks);
747
- }
748
-
749
- const chunks = await loadChunksForPlan(projectRoot, sweepId, planResult.plan, checks);
750
- if (scope === "chunks" || scope === "all") {
751
- for (const chunk of chunks) {
752
- validateChunkShape(chunk, planResult.plan, checks);
753
- await validateChunkEvidenceArtifact(projectRoot, chunk, checks);
754
- }
755
- }
756
-
757
- const findingsResult = await loadFindings(projectRoot, sweepId);
758
- ensureClusterStore(findingsResult.store);
759
- const findings = findingsResult.store.findings;
760
- const clusters = findingsResult.store.clusters;
761
- if (scope === "findings" || scope === "rerun" || scope === "all") {
762
- const chunksById = new Map(chunks.map((chunk) => [chunk.chunk_id, chunk]));
763
- check(checks, "findings_store_valid", findingsResult.store.kind === "audit-findings" && findingsResult.store.sweep_id === sweepId && Array.isArray(findings), "findings store is valid");
764
- for (const finding of findings) {
765
- validateFindingShape(finding, chunksById, checks);
766
- }
767
- const findingIds = new Set(findings.map((finding) => finding.id));
768
- for (const cluster of clusters) {
769
- validateClusterShape(cluster, findingIds, checks);
770
- }
771
- await validateEvidenceRefs(projectRoot, [findingsResult.findingsRef, ...findings.map((finding) => finding.evidence_ref), ...findings.map((finding) => finding.resolution?.evidence_ref).filter(Boolean)], checks, "finding_ref");
772
- }
773
- if (scope === "findings" || scope === "rerun") {
774
- return validationResult(sweepId, scope, checks);
775
- }
776
-
777
- const events = await loadRunLedgerEvents(projectRoot, sweepId, checks);
778
- const ledgerInfo = scope === "ledger" || scope === "remediation" || scope === "closeout" || scope === "all"
779
- ? await validateLatestLedger(projectRoot, sweepId, planResult.plan, chunks, findings, clusters, checks)
780
- : null;
781
- validateRunLedgerReplay(events, planResult.plan, chunks, findings, ledgerInfo, checks);
782
- if (scope === "ledger") {
783
- return validationResult(sweepId, scope, checks);
784
- }
785
-
786
- const remediationInfo = scope === "remediation" || scope === "closeout" || scope === "all"
787
- ? await validateRemediationMap(projectRoot, sweepId, ledgerInfo, findings, clusters, checks)
788
- : null;
789
- if (scope === "remediation") {
790
- return validationResult(sweepId, scope, checks);
791
- }
792
-
793
- if (scope === "closeout" || scope === "all") {
794
- await validateCloseoutArtifact(projectRoot, sweepId, ledgerInfo, remediationInfo, findings, checks);
795
- }
796
- return validationResult(sweepId, scope, checks);
797
- }