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