@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,560 +0,0 @@
1
- import { readdir, rename, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import YAML from "yaml";
4
-
5
- import { readTextIfFile } from "./fs-helpers.mjs";
6
- import { loadTopicRuntimeAuthority, toPortableRelativePath } from "./topic-common.mjs";
7
- import {
8
- buildTopicNow,
9
- getTopicWaves,
10
- loadTopicReport,
11
- moveTopicDirectoryForState,
12
- topicHasEnrichedShape,
13
- writeTopicYaml,
14
- } from "./topic-scaffold.mjs";
15
- import {
16
- listWavePackets,
17
- listWaveResults,
18
- pendingNoteFilename,
19
- pendingNoteMarkdown,
20
- readFrontmatterObject,
21
- topicCloseoutFilename,
22
- topicTrueCloseAuditFilename,
23
- topicTrueCloseJudgementFilename,
24
- topicTrueCloseRecordFilename,
25
- waveCloseoutFilename,
26
- } from "./topic-artifacts.mjs";
27
- import { collectWaveArtifactEvidence, loadPendingNote, validateWaveId } from "./topic-waves.mjs";
28
-
29
- export function closeoutMarkdown(closeout, title) {
30
- return `---
31
- ${YAML.stringify(closeout).trimEnd()}
32
- ---
33
-
34
- # ${title}
35
-
36
- Recorded by \`nimicoding topic closeout\`.
37
- `;
38
- }
39
- export function trueCloseAuditMarkdown(audit, judgementText) {
40
- return `---
41
- ${YAML.stringify(audit).trimEnd()}
42
- ---
43
-
44
- # Topic True-Close Audit
45
-
46
- ${judgementText}
47
- `;
48
- }
49
- export function trueCloseRecordMarkdown(record) {
50
- return `---
51
- ${YAML.stringify(record).trimEnd()}
52
- ---
53
-
54
- # Topic True-Close
55
-
56
- Recorded by \`nimicoding topic closeout topic\`.
57
- `;
58
- }
59
- async function collectActiveDeferredBlockers(topicDir) {
60
- const entries = await readdir(topicDir, { withFileTypes: true }),
61
- blockers = [];
62
- for (const entry of entries) {
63
- if (!entry.isFile() || !entry.name.startsWith("deferred-blocker-") || !entry.name.endsWith(".md"))
64
- continue;
65
- const blockerPath = path.join(topicDir, entry.name),
66
- blockerText = await readTextIfFile(blockerPath),
67
- blocker = readFrontmatterObject(blockerText ?? "");
68
- if (!blocker || !["resolved", "closed"].includes(blocker.status)) {
69
- blockers.push(entry.name);
70
- }
71
- }
72
- return blockers.sort();
73
- }
74
-
75
- function packetRequiresPlacementCloseoutEvidence(packet) {
76
- const searchableFields = [
77
- ...(Array.isArray(packet?.authority_owner) ? packet.authority_owner : []),
78
- ...(Array.isArray(packet?.canonical_seams) ? packet.canonical_seams : []),
79
- ...(Array.isArray(packet?.acceptance_invariants) ? packet.acceptance_invariants : []),
80
- ...(Array.isArray(packet?.negative_tests) ? packet.negative_tests : []),
81
- ...(Array.isArray(packet?.reopen_conditions) ? packet.reopen_conditions : []),
82
- ].join("\n");
83
- return /placement|surface[-_ ]taxonomy|surface[-_ ]class|classify-spec-tree|validate-placement|closeout_drift_resistance_requires_placement_report/i.test(
84
- searchableFields,
85
- );
86
- }
87
-
88
- async function waveHasPlacementReportEvidence(projectRoot, topicDir, waveId) {
89
- const packets = await listWavePackets(topicDir, waveId);
90
- if (!packets.some(({ packet }) => packetRequiresPlacementCloseoutEvidence(packet))) {
91
- return { required: false, found: false };
92
- }
93
-
94
- const results = await listWaveResults(topicDir, waveId);
95
- const evidenceTexts = [];
96
- for (const { result, resultPath } of results) {
97
- const resultText = await readTextIfFile(resultPath);
98
- evidenceTexts.push(resultText ?? "");
99
- if (typeof result?.source_ref === "string" && result.source_ref.length > 0) {
100
- const sourcePath = path.resolve(projectRoot, result.source_ref);
101
- const sourceText = await readTextIfFile(sourcePath);
102
- if (sourceText !== null) {
103
- evidenceTexts.push(sourceText);
104
- }
105
- }
106
- }
107
-
108
- const hasReport = evidenceTexts.some((text) =>
109
- /nimicoding\.surface-validator-result\.v1|nimicoding\.spec-migration-plan\.v1|classify-spec-tree|generate-spec-migration-plan|validate-placement|validate-table-family|validate-projection-edges|validate-guidance-bodies|validate-domain-admission|validate-tracked-output-admission/i.test(
110
- text,
111
- ),
112
- );
113
- return { required: true, found: hasReport };
114
- }
115
-
116
- export async function buildWaveClosureChecks(projectRoot, topicDir, topic, wave, closeout) {
117
- const authority = await loadTopicRuntimeAuthority(projectRoot),
118
- evidence = await collectWaveArtifactEvidence(topicDir, wave.wave_id),
119
- placementEvidence = closeout.drift_resistance_closure === "closed"
120
- ? await waveHasPlacementReportEvidence(projectRoot, topicDir, wave.wave_id)
121
- : { required: false, found: false },
122
- checks = [];
123
- (checks.push({
124
- id: "closeout_scope_wave",
125
- ok: closeout.scope === "wave" && authority.closeoutScopes.includes(closeout.scope),
126
- reason:
127
- closeout.scope === "wave"
128
- ? "closeout scope is wave"
129
- : `closeout scope must be wave, found ${closeout.scope ?? "missing"}`,
130
- }),
131
- checks.push({
132
- id: "closeout_topic_matches",
133
- ok: closeout.topic_id === topic.topic_id,
134
- reason:
135
- closeout.topic_id === topic.topic_id
136
- ? "closeout topic_id matches the topic"
137
- : `closeout topic_id does not match topic (${closeout.topic_id ?? "missing"} vs ${topic.topic_id})`,
138
- }));
139
- const closurePairs = authority.closureDimensions.map((dimension) => [
140
- `${dimension}_closure`,
141
- closeout[`${dimension}_closure`],
142
- ]);
143
- for (const [field, value] of closurePairs)
144
- checks.push({
145
- id: `${field}_explicit_closed`,
146
- ok: value === "closed" && authority.closureStates.includes(value),
147
- reason:
148
- value === "closed"
149
- ? `${field} is explicitly closed`
150
- : `${field} must be closed for wave closeout, found ${value ?? "missing"}`,
151
- });
152
- checks.push({
153
- id: "closeout_disposition_complete",
154
- ok:
155
- closeout.disposition === "complete" &&
156
- authority.closeoutDispositions.includes(closeout.disposition),
157
- reason:
158
- closeout.disposition === "complete"
159
- ? "closeout disposition is complete"
160
- : `closeout disposition must be complete for wave closeout, found ${closeout.disposition ?? "missing"}`,
161
- });
162
- if (placementEvidence.required) {
163
- checks.push({
164
- id: "drift_resistance_has_placement_report",
165
- ok: placementEvidence.found,
166
- reason: placementEvidence.found
167
- ? "drift-resistance closure has recorded placement validation evidence"
168
- : "drift-resistance closure requires recorded placement validation evidence for this packet",
169
- });
170
- }
171
- const activeBlockers = ["needs_revision", "overflowed", "continuation_packet_open"].includes(
172
- wave.state,
173
- );
174
- checks.push({
175
- id: "wave_has_no_active_blockers",
176
- ok: !activeBlockers,
177
- reason: activeBlockers
178
- ? `wave remains in an active blocker state: ${wave.state}`
179
- : "wave has no active blocker state",
180
- });
181
- const closeableState = ["implementation_active", "preflight_admitted", "closed"].includes(
182
- wave.state,
183
- );
184
- return (
185
- checks.push({
186
- id: "wave_state_closeable",
187
- ok: closeableState,
188
- reason: closeableState
189
- ? "wave state remains eligible for closeout"
190
- : `wave closeout requires implementation_active, preflight_admitted, or closed, found ${wave.state}`,
191
- }),
192
- authority.waveCloseoutEvidence.requirePacketLineage &&
193
- checks.push({
194
- id: "wave_packet_lineage_exists",
195
- ok: evidence.packetRefs.length > 0,
196
- reason:
197
- evidence.packetRefs.length > 0
198
- ? "wave closeout has packet lineage evidence"
199
- : `wave closeout requires packet lineage evidence for ${wave.wave_id}`,
200
- }),
201
- authority.waveCloseoutEvidence.requireResultLineage &&
202
- checks.push({
203
- id: "wave_result_lineage_exists",
204
- ok: evidence.resultRefs.length > 0,
205
- reason:
206
- evidence.resultRefs.length > 0
207
- ? "wave closeout has result lineage evidence"
208
- : `wave closeout requires result lineage evidence for ${wave.wave_id}`,
209
- }),
210
- checks
211
- );
212
- }
213
- export async function validateWaveClosure(projectRoot, input, waveId) {
214
- const loaded = await loadTopicReport(projectRoot, input);
215
- if (!loaded.ok) return { ok: false, error: loaded.error, checks: [], warnings: [] };
216
- const { validateTopicRoot } = await import("./topic-root-validation.mjs");
217
- const rootValidation = await validateTopicRoot(projectRoot, input),
218
- wave = getTopicWaves(loaded.topic).find((entry) => entry.wave_id === waveId) ?? null,
219
- checks = [...(rootValidation.checks ?? [])],
220
- warnings = [...(rootValidation.warnings ?? [])];
221
- if (
222
- (checks.push({
223
- id: "wave_exists",
224
- ok: wave !== null,
225
- reason: wave ? "wave exists in topic.yaml waves[]" : `wave does not exist: ${waveId}`,
226
- }),
227
- !wave)
228
- )
229
- return { ...rootValidation, ok: false, checks, warnings };
230
- const closeoutPath = path.join(loaded.topicDir, waveCloseoutFilename(waveId)),
231
- closeoutText = await readTextIfFile(closeoutPath);
232
- if (
233
- (checks.push({
234
- id: "wave_closeout_artifact_exists",
235
- ok: closeoutText !== null,
236
- reason:
237
- closeoutText !== null
238
- ? "wave closeout artifact exists"
239
- : `missing wave closeout artifact: ${waveCloseoutFilename(waveId)}`,
240
- }),
241
- closeoutText === null)
242
- )
243
- return { ...rootValidation, ok: false, checks, warnings, waveId };
244
- const closeout = readFrontmatterObject(closeoutText);
245
- return (
246
- checks.push({
247
- id: "wave_closeout_frontmatter_valid",
248
- ok: closeout !== null,
249
- reason:
250
- closeout !== null
251
- ? "wave closeout frontmatter is valid"
252
- : "wave closeout artifact frontmatter is invalid",
253
- }),
254
- closeout === null
255
- ? { ...rootValidation, ok: false, checks, warnings, waveId }
256
- : (checks.push(
257
- ...(await buildWaveClosureChecks(
258
- projectRoot,
259
- loaded.topicDir,
260
- loaded.topic,
261
- wave,
262
- closeout,
263
- )),
264
- ),
265
- {
266
- ...rootValidation,
267
- ok: rootValidation.ok && checks.every((entry) => entry.ok),
268
- checks,
269
- warnings,
270
- waveId,
271
- closeoutRef: toPortableRelativePath(path.relative(projectRoot, closeoutPath)),
272
- })
273
- );
274
- }
275
- export async function closeoutWaveInTopic(projectRoot, input, waveId, options) {
276
- const loaded = await loadTopicReport(projectRoot, input);
277
- if (!loaded.ok) return loaded;
278
- const authority = await loadTopicRuntimeAuthority(projectRoot);
279
- if (!topicHasEnrichedShape(loaded.topic, authority))
280
- return { ok: false, error: "Wave closeout requires an enriched topic root." };
281
- const wave = getTopicWaves(loaded.topic).find((entry) => entry.wave_id === waveId) ?? null;
282
- if (!wave) return { ok: false, error: `Wave not found: ${waveId}` };
283
- const closeout = {
284
- closeout_id: waveId,
285
- topic_id: loaded.topicId,
286
- scope: "wave",
287
- authority_closure: options.authorityClosure,
288
- semantic_closure: options.semanticClosure,
289
- consumer_closure: options.consumerClosure,
290
- drift_resistance_closure: options.driftResistanceClosure,
291
- disposition: options.disposition,
292
- },
293
- checks = await buildWaveClosureChecks(
294
- projectRoot,
295
- loaded.topicDir,
296
- loaded.topic,
297
- wave,
298
- closeout,
299
- );
300
- if (!checks.every((entry) => entry.ok))
301
- return {
302
- ok: false,
303
- error: `Wave closeout refused: ${checks.find((entry) => !entry.ok)?.reason ?? "closure validation failed"}`,
304
- checks,
305
- warnings: [],
306
- topicId: loaded.topicId,
307
- topicRef: toPortableRelativePath(path.relative(projectRoot, loaded.topicDir)),
308
- waveId,
309
- };
310
- const closeoutPath = path.join(loaded.topicDir, waveCloseoutFilename(waveId));
311
- return (
312
- await writeFile(closeoutPath, closeoutMarkdown(closeout, `Wave Closeout ${waveId}`), "utf8"),
313
- (wave.state = "closed"),
314
- (wave.selected = false),
315
- (loaded.topic.waves = getTopicWaves(loaded.topic).map((entry) =>
316
- entry.wave_id === waveId ? wave : entry,
317
- )),
318
- loaded.topic.selected_next_target === waveId && (loaded.topic.selected_next_target = null),
319
- (loaded.topic.last_transition_at = buildTopicNow()),
320
- (loaded.topic.last_transition_reason = `closed_${waveId}`),
321
- await writeTopicYaml(loaded.topicYamlPath, loaded.topic),
322
- {
323
- ok: true,
324
- topicId: loaded.topicId,
325
- topicRef: toPortableRelativePath(path.relative(projectRoot, loaded.topicDir)),
326
- waveId,
327
- waveState: wave.state,
328
- closeoutRef: toPortableRelativePath(path.relative(projectRoot, closeoutPath)),
329
- }
330
- );
331
- }
332
- export async function buildTrueCloseAuditChecks(projectRoot, topicDir, topic) {
333
- const authority = await loadTopicRuntimeAuthority(projectRoot),
334
- waves = getTopicWaves(topic),
335
- checks = [],
336
- activeDeferredBlockers = await collectActiveDeferredBlockers(topicDir),
337
- nonTerminalWaves = waves
338
- .filter((entry) => !["closed", "retired", "superseded"].includes(entry.state))
339
- .map((entry) => `${entry.wave_id}:${entry.state}`);
340
- checks.push({
341
- id: "all_waves_terminal",
342
- ok: nonTerminalWaves.length === 0,
343
- reason:
344
- nonTerminalWaves.length === 0
345
- ? "all waves are closed, retired, or superseded"
346
- : `non-terminal waves remain: ${nonTerminalWaves.join(", ")}`,
347
- });
348
- checks.push({
349
- id: "no_active_deferred_blockers",
350
- ok: activeDeferredBlockers.length === 0,
351
- reason:
352
- activeDeferredBlockers.length === 0
353
- ? "no active deferred blocker artifacts remain"
354
- : `active deferred blocker artifacts remain: ${activeDeferredBlockers.join(", ")}`,
355
- });
356
- const selectedActive = waves
357
- .filter((entry) => entry.selected === true)
358
- .map((entry) => entry.wave_id);
359
- (checks.push({
360
- id: "no_selected_wave_remains",
361
- ok: selectedActive.length === 0,
362
- reason:
363
- selectedActive.length === 0
364
- ? "no selected wave remains active"
365
- : `selected waves remain: ${selectedActive.join(", ")}`,
366
- }),
367
- checks.push({
368
- id: "selected_target_cleared",
369
- ok:
370
- topic.selected_next_target === null ||
371
- topic.selected_next_target === "topic_design_baseline",
372
- reason:
373
- topic.selected_next_target === null ||
374
- topic.selected_next_target === "topic_design_baseline"
375
- ? "selected_next_target is cleared for topic closeout"
376
- : `selected_next_target remains active: ${topic.selected_next_target}`,
377
- }));
378
- for (const wave of waves.filter((entry) => entry.state === "closed")) {
379
- const evidence = await collectWaveArtifactEvidence(topicDir, wave.wave_id);
380
- (authority.trueCloseAuditEvidence.requireWaveCloseoutForClosedWaves &&
381
- checks.push({
382
- id: `wave_closeout_exists_${wave.wave_id}`,
383
- ok: evidence.closeoutRefs.length > 0,
384
- reason:
385
- evidence.closeoutRefs.length > 0
386
- ? `${wave.wave_id} has closeout evidence`
387
- : `${wave.wave_id} is closed but has no wave closeout evidence`,
388
- }),
389
- authority.trueCloseAuditEvidence.requirePacketLineageForClosedWaves &&
390
- checks.push({
391
- id: `wave_packet_lineage_exists_${wave.wave_id}`,
392
- ok: evidence.packetRefs.length > 0,
393
- reason:
394
- evidence.packetRefs.length > 0
395
- ? `${wave.wave_id} has packet lineage evidence`
396
- : `${wave.wave_id} is closed but has no packet lineage evidence`,
397
- }),
398
- authority.trueCloseAuditEvidence.requireResultLineageForClosedWaves &&
399
- checks.push({
400
- id: `wave_result_lineage_exists_${wave.wave_id}`,
401
- ok: evidence.resultRefs.length > 0,
402
- reason:
403
- evidence.resultRefs.length > 0
404
- ? `${wave.wave_id} has result lineage evidence`
405
- : `${wave.wave_id} is closed but has no result lineage evidence`,
406
- }));
407
- }
408
- return checks;
409
- }
410
- export async function runTopicTrueCloseAudit(projectRoot, input, judgementText) {
411
- const loaded = await loadTopicReport(projectRoot, input);
412
- if (!loaded.ok) return loaded;
413
- const authority = await loadTopicRuntimeAuthority(projectRoot);
414
- if (!topicHasEnrichedShape(loaded.topic, authority))
415
- return { ok: false, error: "True-close audit requires an enriched topic root." };
416
- const checks = await buildTrueCloseAuditChecks(projectRoot, loaded.topicDir, loaded.topic),
417
- passed = checks.every((entry) => entry.ok),
418
- auditPath = path.join(loaded.topicDir, topicTrueCloseAuditFilename()),
419
- judgementPath = path.join(loaded.topicDir, topicTrueCloseJudgementFilename()),
420
- audit = { topic_id: loaded.topicId, status: passed ? "passed" : "pending", checks };
421
- return (
422
- await writeFile(auditPath, trueCloseAuditMarkdown(audit, judgementText), "utf8"),
423
- await writeFile(
424
- judgementPath,
425
- `---
426
- ${YAML.stringify({ topic_id: loaded.topicId, status: passed ? "passed" : "pending", judgement: judgementText }).trimEnd()}
427
- ---
428
-
429
- # Topic True-Close Audit Result
430
- `,
431
- "utf8",
432
- ),
433
- (loaded.topic.last_transition_at = buildTopicNow()),
434
- (loaded.topic.last_transition_reason = "ran_topic_true_close_audit"),
435
- passed && (loaded.topic.current_true_close_status = "pending"),
436
- await writeTopicYaml(loaded.topicYamlPath, loaded.topic),
437
- {
438
- ok: passed,
439
- topicId: loaded.topicId,
440
- topicRef: toPortableRelativePath(path.relative(projectRoot, loaded.topicDir)),
441
- status: passed ? "passed" : "pending",
442
- auditRef: toPortableRelativePath(path.relative(projectRoot, auditPath)),
443
- judgementRef: toPortableRelativePath(path.relative(projectRoot, judgementPath)),
444
- checks,
445
- warnings: [],
446
- }
447
- );
448
- }
449
- export async function closeoutTopicInTopic(projectRoot, input, options) {
450
- const loaded = await loadTopicReport(projectRoot, input);
451
- if (!loaded.ok) return loaded;
452
- const authority = await loadTopicRuntimeAuthority(projectRoot);
453
- if (!topicHasEnrichedShape(loaded.topic, authority))
454
- return { ok: false, error: "Topic closeout requires an enriched topic root." };
455
- let pendingNoteLoaded = null;
456
- if (loaded.topic.state === "pending") {
457
- if (((pendingNoteLoaded = await loadPendingNote(loaded.topicDir)), !pendingNoteLoaded.ok))
458
- return {
459
- ok: false,
460
- error: `Topic closeout from pending requires a pending note artifact: ${pendingNoteLoaded.error}`,
461
- };
462
- if (
463
- typeof pendingNoteLoaded.note.close_trigger != "string" ||
464
- pendingNoteLoaded.note.close_trigger.length === 0
465
- )
466
- return {
467
- ok: false,
468
- error: "Topic closeout from pending requires an explicit close trigger in pending-note.md.",
469
- };
470
- }
471
- const auditPath = path.join(loaded.topicDir, topicTrueCloseAuditFilename()),
472
- judgementPath = path.join(loaded.topicDir, topicTrueCloseJudgementFilename()),
473
- auditText = await readTextIfFile(auditPath),
474
- judgementText = await readTextIfFile(judgementPath);
475
- if (auditText === null || judgementText === null)
476
- return { ok: false, error: "Topic closeout requires a recorded true-close audit and judgement." };
477
- const audit = readFrontmatterObject(auditText);
478
- if (!audit || audit.status !== "passed")
479
- return { ok: false, error: "Topic closeout requires a passed true-close audit." };
480
- const auditChecks = await buildTrueCloseAuditChecks(projectRoot, loaded.topicDir, loaded.topic);
481
- if (!auditChecks.every((entry) => entry.ok))
482
- return {
483
- ok: false,
484
- error: `Topic closeout refused: ${auditChecks.find((entry) => !entry.ok)?.reason ?? "true-close checks failed"}`,
485
- checks: auditChecks,
486
- warnings: [],
487
- topicId: loaded.topicId,
488
- topicRef: toPortableRelativePath(path.relative(projectRoot, loaded.topicDir)),
489
- };
490
- const closeout = {
491
- closeout_id: loaded.topicId,
492
- topic_id: loaded.topicId,
493
- scope: "topic",
494
- authority_closure: options.authorityClosure,
495
- semantic_closure: options.semanticClosure,
496
- consumer_closure: options.consumerClosure,
497
- drift_resistance_closure: options.driftResistanceClosure,
498
- disposition: options.disposition,
499
- };
500
- if (
501
- [
502
- closeout.authority_closure,
503
- closeout.semantic_closure,
504
- closeout.consumer_closure,
505
- closeout.drift_resistance_closure,
506
- ].some((entry) => entry !== "closed") ||
507
- closeout.disposition !== "complete"
508
- )
509
- return {
510
- ok: false,
511
- error: "Topic closeout requires all four closures to be closed and disposition=complete.",
512
- };
513
- const closeoutPath = path.join(loaded.topicDir, topicCloseoutFilename());
514
- await writeFile(closeoutPath, closeoutMarkdown(closeout, "Topic Closeout"), "utf8");
515
- const trueCloseRecordPath = path.join(loaded.topicDir, topicTrueCloseRecordFilename());
516
- (await writeFile(
517
- trueCloseRecordPath,
518
- trueCloseRecordMarkdown({
519
- topic_id: loaded.topicId,
520
- status: "passed",
521
- audit_ref: toPortableRelativePath(path.relative(projectRoot, auditPath)),
522
- judgement_ref: toPortableRelativePath(path.relative(projectRoot, judgementPath)),
523
- }),
524
- "utf8",
525
- ),
526
- (loaded.topic.state = "closed"),
527
- (loaded.topic.current_true_close_status = "true_closed"),
528
- (loaded.topic.last_transition_at = buildTopicNow()),
529
- (loaded.topic.last_transition_reason = "closed_topic_after_true_close"));
530
- const moved = await moveTopicDirectoryForState(
531
- projectRoot,
532
- loaded.topicDir,
533
- loaded.topicId,
534
- "closed",
535
- );
536
- return (
537
- await writeTopicYaml(moved.topicYamlPath, loaded.topic),
538
- pendingNoteLoaded?.ok &&
539
- ((pendingNoteLoaded.note.status = "closed"),
540
- (pendingNoteLoaded.note.closed_at = buildTopicNow()),
541
- await writeFile(
542
- path.join(moved.topicDir, pendingNoteFilename()),
543
- pendingNoteMarkdown(pendingNoteLoaded.note),
544
- "utf8",
545
- )),
546
- {
547
- ok: true,
548
- topicId: loaded.topicId,
549
- topicRef: toPortableRelativePath(path.relative(projectRoot, moved.topicDir)),
550
- state: loaded.topic.state,
551
- closeoutRef: toPortableRelativePath(
552
- path.relative(projectRoot, path.join(moved.topicDir, topicCloseoutFilename())),
553
- ),
554
- trueCloseRef: toPortableRelativePath(
555
- path.relative(projectRoot, path.join(moved.topicDir, topicTrueCloseRecordFilename())),
556
- ),
557
- currentTrueCloseStatus: loaded.topic.current_true_close_status,
558
- }
559
- );
560
- }