@nimiplatform/nimi-coding 0.1.0

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