@nimiplatform/nimi-coding 0.2.7 → 0.3.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 (207) hide show
  1. package/CHANGELOG.md +8 -80
  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/constants.mjs +9 -417
  8. package/cli/dev/run-cli-smoke.mjs +24 -0
  9. package/cli/dev/run-suite.mjs +17 -0
  10. package/cli/help.mjs +28 -145
  11. package/cli/index.mjs +5 -33
  12. package/cli/lib/blueprint-audit.mjs +5 -12
  13. package/cli/lib/bootstrap.mjs +0 -9
  14. package/cli/lib/contracts.mjs +0 -163
  15. package/cli/lib/doctor.mjs +155 -10
  16. package/cli/lib/entrypoints.mjs +63 -167
  17. package/cli/lib/internal/contracts-loaders.mjs +0 -85
  18. package/cli/lib/internal/contracts-parse.mjs +84 -520
  19. package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +13 -8
  20. package/cli/lib/internal/surface-taxonomy-validators.mjs +316 -66
  21. package/cli/lib/internal/validators-shared.mjs +0 -16
  22. package/cli/lib/internal/validators-spec-helpers.mjs +7 -140
  23. package/cli/lib/internal/validators-spec.mjs +103 -342
  24. package/cli/lib/shared.mjs +1 -42
  25. package/cli/lib/validators.mjs +0 -48
  26. package/cli/seeds/seed-policy.yaml +0 -3
  27. package/config/bootstrap.yaml +4 -4
  28. package/config/spec-generation-inputs.yaml +6 -12
  29. package/contracts/domain-admission.schema.yaml +3 -3
  30. package/contracts/migration-inventory.schema.yaml +12 -75
  31. package/contracts/negative-fixtures.yaml +19 -99
  32. package/contracts/placement-contract.schema.yaml +49 -78
  33. package/contracts/projection-edge.schema.yaml +37 -121
  34. package/contracts/shared-enums.yaml +4 -8
  35. package/contracts/spec-generation-inputs.schema.yaml +15 -112
  36. package/contracts/spec-layout.schema.yaml +32 -0
  37. package/contracts/surface-taxonomy.schema.yaml +43 -94
  38. package/contracts/table-family.schema.yaml +1 -1
  39. package/contracts/tracked-output-admission.schema.yaml +13 -66
  40. package/methodology/core.yaml +33 -25
  41. package/methodology/four-closure-policy.yaml +18 -26
  42. package/methodology/role-separation-policy.yaml +21 -27
  43. package/methodology/spec-reconstruction.yaml +17 -34
  44. package/package.json +4 -6
  45. package/spec/_meta/spec-tree-model.yaml +5 -101
  46. package/spec/product-scope.yaml +25 -57
  47. package/adapters/README.md +0 -25
  48. package/adapters/claude/README.md +0 -89
  49. package/adapters/claude/profile.yaml +0 -70
  50. package/adapters/codex/README.md +0 -53
  51. package/adapters/codex/profile.yaml +0 -78
  52. package/adapters/oh-my-codex/README.md +0 -184
  53. package/adapters/oh-my-codex/profile.yaml +0 -46
  54. package/cli/commands/admit-high-risk-decision.mjs +0 -108
  55. package/cli/commands/audit-sweep.mjs +0 -364
  56. package/cli/commands/closeout.mjs +0 -186
  57. package/cli/commands/decide-high-risk-execution.mjs +0 -124
  58. package/cli/commands/handoff.mjs +0 -123
  59. package/cli/commands/ingest-high-risk-execution.mjs +0 -95
  60. package/cli/commands/review-high-risk-execution.mjs +0 -95
  61. package/cli/commands/sweep-design.mjs +0 -295
  62. package/cli/commands/sweep.mjs +0 -22
  63. package/cli/commands/topic-formatters.mjs +0 -382
  64. package/cli/commands/topic-goal.mjs +0 -33
  65. package/cli/commands/topic-options-shared.mjs +0 -27
  66. package/cli/commands/topic-options-workflow.mjs +0 -767
  67. package/cli/commands/topic-options.mjs +0 -626
  68. package/cli/commands/topic-runner.mjs +0 -169
  69. package/cli/commands/topic.mjs +0 -795
  70. package/cli/commands/validate-acceptance.mjs +0 -5
  71. package/cli/commands/validate-execution-packet.mjs +0 -5
  72. package/cli/commands/validate-orchestration-state.mjs +0 -5
  73. package/cli/commands/validate-prompt.mjs +0 -5
  74. package/cli/commands/validate-worker-output.mjs +0 -5
  75. package/cli/lib/adapter-profiles.mjs +0 -403
  76. package/cli/lib/audit-execution.mjs +0 -52
  77. package/cli/lib/audit-sweep-runtime/admissions.mjs +0 -508
  78. package/cli/lib/audit-sweep-runtime/audit-validity.mjs +0 -356
  79. package/cli/lib/audit-sweep-runtime/chunks.mjs +0 -697
  80. package/cli/lib/audit-sweep-runtime/claude-auditor.mjs +0 -647
  81. package/cli/lib/audit-sweep-runtime/closeout.mjs +0 -144
  82. package/cli/lib/audit-sweep-runtime/codex-auditor-evidence.mjs +0 -654
  83. package/cli/lib/audit-sweep-runtime/codex-auditor.mjs +0 -516
  84. package/cli/lib/audit-sweep-runtime/common.mjs +0 -341
  85. package/cli/lib/audit-sweep-runtime/coverage-quality.mjs +0 -172
  86. package/cli/lib/audit-sweep-runtime/evidence-assignment.mjs +0 -155
  87. package/cli/lib/audit-sweep-runtime/format.mjs +0 -57
  88. package/cli/lib/audit-sweep-runtime/ingest.mjs +0 -486
  89. package/cli/lib/audit-sweep-runtime/inventory-spec-chunks.mjs +0 -425
  90. package/cli/lib/audit-sweep-runtime/inventory.mjs +0 -798
  91. package/cli/lib/audit-sweep-runtime/ledger.mjs +0 -315
  92. package/cli/lib/audit-sweep-runtime/p0p1-profile.mjs +0 -101
  93. package/cli/lib/audit-sweep-runtime/remediation.mjs +0 -349
  94. package/cli/lib/audit-sweep-runtime/rerun.mjs +0 -129
  95. package/cli/lib/audit-sweep-runtime/risk-budget.mjs +0 -300
  96. package/cli/lib/audit-sweep-runtime/status.mjs +0 -62
  97. package/cli/lib/audit-sweep-runtime/validators-ledger.mjs +0 -215
  98. package/cli/lib/audit-sweep-runtime/validators.mjs +0 -797
  99. package/cli/lib/audit-sweep.mjs +0 -19
  100. package/cli/lib/authority-convergence.mjs +0 -704
  101. package/cli/lib/closeout.mjs +0 -732
  102. package/cli/lib/codex-sdk-runner.mjs +0 -76
  103. package/cli/lib/external-execution.mjs +0 -101
  104. package/cli/lib/handoff.mjs +0 -812
  105. package/cli/lib/high-risk-admission.mjs +0 -512
  106. package/cli/lib/high-risk-decision.mjs +0 -353
  107. package/cli/lib/high-risk-ingest.mjs +0 -321
  108. package/cli/lib/high-risk-review.mjs +0 -267
  109. package/cli/lib/internal/contracts-parse-high-risk.mjs +0 -131
  110. package/cli/lib/internal/contracts-validators.mjs +0 -399
  111. package/cli/lib/internal/doctor-bootstrap-surface.mjs +0 -406
  112. package/cli/lib/internal/doctor-delegated-surface.mjs +0 -256
  113. package/cli/lib/internal/doctor-finalize.mjs +0 -383
  114. package/cli/lib/internal/doctor-format.mjs +0 -286
  115. package/cli/lib/internal/doctor-inspectors.mjs +0 -327
  116. package/cli/lib/internal/doctor-state.mjs +0 -205
  117. package/cli/lib/internal/validators-artifacts.mjs +0 -515
  118. package/cli/lib/sweep-design-runtime/common.mjs +0 -246
  119. package/cli/lib/sweep-design-runtime/engine.mjs +0 -733
  120. package/cli/lib/sweep-design-runtime/fix-topic.mjs +0 -414
  121. package/cli/lib/sweep-design-runtime/lifecycle.mjs +0 -54
  122. package/cli/lib/sweep-design-runtime/results.mjs +0 -324
  123. package/cli/lib/sweep-design.mjs +0 -8
  124. package/cli/lib/topic-artifacts.mjs +0 -186
  125. package/cli/lib/topic-authority-coverage.mjs +0 -73
  126. package/cli/lib/topic-closeout.mjs +0 -560
  127. package/cli/lib/topic-common.mjs +0 -404
  128. package/cli/lib/topic-decisions.mjs +0 -332
  129. package/cli/lib/topic-draft-packets.mjs +0 -167
  130. package/cli/lib/topic-execution.mjs +0 -515
  131. package/cli/lib/topic-goal.mjs +0 -440
  132. package/cli/lib/topic-ledger.mjs +0 -281
  133. package/cli/lib/topic-lifecycle-artifacts.mjs +0 -173
  134. package/cli/lib/topic-root-validation.mjs +0 -288
  135. package/cli/lib/topic-runner-commands.mjs +0 -174
  136. package/cli/lib/topic-runner-deferral.mjs +0 -532
  137. package/cli/lib/topic-runner-stale-gates.mjs +0 -114
  138. package/cli/lib/topic-runner-validation.mjs +0 -138
  139. package/cli/lib/topic-runner.mjs +0 -727
  140. package/cli/lib/topic-scaffold.mjs +0 -252
  141. package/cli/lib/topic-waves.mjs +0 -403
  142. package/cli/lib/topic.mjs +0 -81
  143. package/config/audit-execution-artifacts.yaml +0 -20
  144. package/config/external-execution-artifacts.yaml +0 -16
  145. package/config/host-adapter.yaml +0 -30
  146. package/config/host-profile.yaml +0 -29
  147. package/config/installer-evidence.yaml +0 -31
  148. package/config/skill-installer.yaml +0 -23
  149. package/config/skill-manifest.yaml +0 -48
  150. package/config/skills.yaml +0 -30
  151. package/contracts/acceptance.schema.yaml +0 -16
  152. package/contracts/admission-checklist.schema.yaml +0 -15
  153. package/contracts/audit-chunk.schema.yaml +0 -123
  154. package/contracts/audit-closeout.schema.yaml +0 -51
  155. package/contracts/audit-finding.schema.yaml +0 -61
  156. package/contracts/audit-ledger.schema.yaml +0 -138
  157. package/contracts/audit-plan.schema.yaml +0 -138
  158. package/contracts/audit-remediation-map.schema.yaml +0 -52
  159. package/contracts/audit-rerun.schema.yaml +0 -31
  160. package/contracts/audit-sweep-result.yaml +0 -53
  161. package/contracts/authority-convergence-audit.schema.yaml +0 -19
  162. package/contracts/closeout.schema.yaml +0 -25
  163. package/contracts/decision-review.schema.yaml +0 -16
  164. package/contracts/doc-spec-audit-result.yaml +0 -19
  165. package/contracts/execution-packet.schema.yaml +0 -49
  166. package/contracts/external-host-compatibility.yaml +0 -22
  167. package/contracts/forbidden-shortcuts.catalog.yaml +0 -23
  168. package/contracts/high-risk-admission.schema.yaml +0 -23
  169. package/contracts/high-risk-execution-result.yaml +0 -20
  170. package/contracts/orchestration-state.schema.yaml +0 -41
  171. package/contracts/overflow-continuation.schema.yaml +0 -12
  172. package/contracts/packet.schema.yaml +0 -30
  173. package/contracts/pending-note.schema.yaml +0 -17
  174. package/contracts/prompt.schema.yaml +0 -12
  175. package/contracts/remediation.schema.yaml +0 -16
  176. package/contracts/result.schema.yaml +0 -24
  177. package/contracts/spec-reconstruction-result.yaml +0 -41
  178. package/contracts/sweep-design-result.yaml +0 -349
  179. package/contracts/topic-goal.schema.yaml +0 -87
  180. package/contracts/topic-run-ledger.schema.yaml +0 -72
  181. package/contracts/topic-step-decision.schema.yaml +0 -45
  182. package/contracts/topic.schema.yaml +0 -65
  183. package/contracts/true-close.schema.yaml +0 -15
  184. package/contracts/wave.schema.yaml +0 -29
  185. package/contracts/worker-output.schema.yaml +0 -15
  186. package/contracts/workflow-consumer.schema.yaml +0 -110
  187. package/methodology/audit-sweep-p0p1-recall.yaml +0 -45
  188. package/methodology/authority-convergence-policy.yaml +0 -42
  189. package/methodology/overflow-continuation-policy.yaml +0 -14
  190. package/methodology/skill-exchange-projection.yaml +0 -114
  191. package/methodology/skill-handoff.yaml +0 -34
  192. package/methodology/skill-installer-result.yaml +0 -27
  193. package/methodology/skill-installer-summary-projection.yaml +0 -181
  194. package/methodology/skill-runtime.yaml +0 -23
  195. package/methodology/spec-target-truth-profile.yaml +0 -46
  196. package/methodology/topic-lifecycle-report.yaml +0 -144
  197. package/methodology/topic-lifecycle.yaml +0 -37
  198. package/methodology/topic-naming-ontology.yaml +0 -21
  199. package/methodology/topic-ontology.yaml +0 -38
  200. package/methodology/topic-validation-policy.yaml +0 -9
  201. package/methodology/wave-dag-policy.yaml +0 -14
  202. package/spec/_meta/command-gating-matrix.yaml +0 -143
  203. package/spec/_meta/generate-drift-migration-checklist.yaml +0 -137
  204. package/spec/_meta/governance-routing-cutover-checklist.yaml +0 -35
  205. package/spec/_meta/phase2-impacted-surface-matrix.yaml +0 -44
  206. package/spec/_meta/spec-authority-cutover-readiness.yaml +0 -102
  207. 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
- }