@nimiplatform/nimi-coding 0.2.8 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/CHANGELOG.md +10 -90
  2. package/CONTRIBUTING.md +5 -34
  3. package/README.md +47 -350
  4. package/README.zh-CN.md +33 -287
  5. package/cli/commands/start.mjs +48 -688
  6. package/cli/commands/validate-spec-audit.mjs +1 -5
  7. package/cli/commands/validate-spec-governance.mjs +20 -0
  8. package/cli/constants.mjs +9 -417
  9. package/cli/help.mjs +28 -145
  10. package/cli/index.mjs +5 -33
  11. package/cli/lib/blueprint-audit.mjs +5 -12
  12. package/cli/lib/bootstrap.mjs +0 -9
  13. package/cli/lib/contracts.mjs +0 -163
  14. package/cli/lib/doctor.mjs +155 -10
  15. package/cli/lib/entrypoints.mjs +63 -167
  16. package/cli/lib/internal/contracts-loaders.mjs +0 -85
  17. package/cli/lib/internal/contracts-parse.mjs +84 -520
  18. package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +13 -8
  19. package/cli/lib/internal/surface-taxonomy-validators.mjs +316 -66
  20. package/cli/lib/internal/validators-shared.mjs +0 -16
  21. package/cli/lib/internal/validators-spec-helpers.mjs +7 -140
  22. package/cli/lib/internal/validators-spec.mjs +103 -342
  23. package/cli/lib/shared.mjs +1 -42
  24. package/cli/lib/validators.mjs +0 -48
  25. package/cli/seeds/seed-policy.yaml +0 -3
  26. package/config/bootstrap.yaml +4 -4
  27. package/config/spec-generation-inputs.yaml +6 -12
  28. package/contracts/domain-admission.schema.yaml +3 -3
  29. package/contracts/migration-inventory.schema.yaml +12 -75
  30. package/contracts/negative-fixtures.yaml +19 -99
  31. package/contracts/placement-contract.schema.yaml +49 -78
  32. package/contracts/projection-edge.schema.yaml +37 -121
  33. package/contracts/shared-enums.yaml +4 -8
  34. package/contracts/spec-generation-inputs.schema.yaml +15 -112
  35. package/contracts/spec-layout.schema.yaml +32 -0
  36. package/contracts/surface-taxonomy.schema.yaml +43 -94
  37. package/contracts/table-family.schema.yaml +1 -1
  38. package/contracts/tracked-output-admission.schema.yaml +13 -66
  39. package/methodology/core.yaml +33 -25
  40. package/methodology/four-closure-policy.yaml +18 -26
  41. package/methodology/role-separation-policy.yaml +21 -27
  42. package/methodology/spec-reconstruction.yaml +17 -34
  43. package/package.json +2 -4
  44. package/spec/_meta/spec-tree-model.yaml +5 -101
  45. package/spec/product-scope.yaml +25 -57
  46. package/adapters/README.md +0 -25
  47. package/adapters/claude/README.md +0 -89
  48. package/adapters/claude/profile.yaml +0 -70
  49. package/adapters/codex/README.md +0 -53
  50. package/adapters/codex/profile.yaml +0 -78
  51. package/adapters/oh-my-codex/README.md +0 -184
  52. package/adapters/oh-my-codex/profile.yaml +0 -46
  53. package/cli/commands/admit-high-risk-decision.mjs +0 -108
  54. package/cli/commands/audit-sweep.mjs +0 -364
  55. package/cli/commands/closeout.mjs +0 -186
  56. package/cli/commands/decide-high-risk-execution.mjs +0 -124
  57. package/cli/commands/handoff.mjs +0 -123
  58. package/cli/commands/ingest-high-risk-execution.mjs +0 -95
  59. package/cli/commands/review-high-risk-execution.mjs +0 -95
  60. package/cli/commands/sweep-design.mjs +0 -295
  61. package/cli/commands/sweep.mjs +0 -22
  62. package/cli/commands/topic-formatters.mjs +0 -382
  63. package/cli/commands/topic-goal.mjs +0 -33
  64. package/cli/commands/topic-options-shared.mjs +0 -27
  65. package/cli/commands/topic-options-workflow.mjs +0 -767
  66. package/cli/commands/topic-options.mjs +0 -626
  67. package/cli/commands/topic-runner.mjs +0 -169
  68. package/cli/commands/topic.mjs +0 -795
  69. package/cli/commands/validate-acceptance.mjs +0 -5
  70. package/cli/commands/validate-execution-packet.mjs +0 -5
  71. package/cli/commands/validate-orchestration-state.mjs +0 -5
  72. package/cli/commands/validate-prompt.mjs +0 -5
  73. package/cli/commands/validate-worker-output.mjs +0 -5
  74. package/cli/lib/adapter-profiles.mjs +0 -403
  75. package/cli/lib/audit-execution.mjs +0 -52
  76. package/cli/lib/audit-sweep-runtime/admissions.mjs +0 -508
  77. package/cli/lib/audit-sweep-runtime/audit-validity.mjs +0 -356
  78. package/cli/lib/audit-sweep-runtime/chunks.mjs +0 -697
  79. package/cli/lib/audit-sweep-runtime/claude-auditor.mjs +0 -658
  80. package/cli/lib/audit-sweep-runtime/closeout.mjs +0 -144
  81. package/cli/lib/audit-sweep-runtime/codex-auditor-evidence.mjs +0 -654
  82. package/cli/lib/audit-sweep-runtime/codex-auditor.mjs +0 -527
  83. package/cli/lib/audit-sweep-runtime/common.mjs +0 -341
  84. package/cli/lib/audit-sweep-runtime/coverage-quality.mjs +0 -172
  85. package/cli/lib/audit-sweep-runtime/evidence-assignment.mjs +0 -155
  86. package/cli/lib/audit-sweep-runtime/format.mjs +0 -57
  87. package/cli/lib/audit-sweep-runtime/ingest.mjs +0 -486
  88. package/cli/lib/audit-sweep-runtime/inventory-spec-chunks.mjs +0 -425
  89. package/cli/lib/audit-sweep-runtime/inventory.mjs +0 -798
  90. package/cli/lib/audit-sweep-runtime/ledger.mjs +0 -315
  91. package/cli/lib/audit-sweep-runtime/p0p1-profile.mjs +0 -101
  92. package/cli/lib/audit-sweep-runtime/remediation.mjs +0 -349
  93. package/cli/lib/audit-sweep-runtime/rerun.mjs +0 -129
  94. package/cli/lib/audit-sweep-runtime/risk-budget.mjs +0 -300
  95. package/cli/lib/audit-sweep-runtime/status.mjs +0 -62
  96. package/cli/lib/audit-sweep-runtime/validators-ledger.mjs +0 -215
  97. package/cli/lib/audit-sweep-runtime/validators.mjs +0 -797
  98. package/cli/lib/audit-sweep.mjs +0 -19
  99. package/cli/lib/authority-convergence.mjs +0 -720
  100. package/cli/lib/closeout.mjs +0 -732
  101. package/cli/lib/codex-sdk-runner.mjs +0 -76
  102. package/cli/lib/external-execution.mjs +0 -101
  103. package/cli/lib/handoff.mjs +0 -812
  104. package/cli/lib/high-risk-admission.mjs +0 -512
  105. package/cli/lib/high-risk-decision.mjs +0 -353
  106. package/cli/lib/high-risk-ingest.mjs +0 -321
  107. package/cli/lib/high-risk-review.mjs +0 -267
  108. package/cli/lib/internal/contracts-parse-high-risk.mjs +0 -131
  109. package/cli/lib/internal/contracts-validators.mjs +0 -399
  110. package/cli/lib/internal/doctor-bootstrap-surface.mjs +0 -406
  111. package/cli/lib/internal/doctor-delegated-surface.mjs +0 -256
  112. package/cli/lib/internal/doctor-finalize.mjs +0 -383
  113. package/cli/lib/internal/doctor-format.mjs +0 -286
  114. package/cli/lib/internal/doctor-inspectors.mjs +0 -327
  115. package/cli/lib/internal/doctor-state.mjs +0 -205
  116. package/cli/lib/internal/validators-artifacts.mjs +0 -515
  117. package/cli/lib/sweep-design-runtime/common.mjs +0 -246
  118. package/cli/lib/sweep-design-runtime/engine.mjs +0 -733
  119. package/cli/lib/sweep-design-runtime/fix-topic.mjs +0 -414
  120. package/cli/lib/sweep-design-runtime/lifecycle.mjs +0 -54
  121. package/cli/lib/sweep-design-runtime/results.mjs +0 -324
  122. package/cli/lib/sweep-design.mjs +0 -8
  123. package/cli/lib/topic-artifacts.mjs +0 -186
  124. package/cli/lib/topic-authority-coverage.mjs +0 -73
  125. package/cli/lib/topic-closeout.mjs +0 -560
  126. package/cli/lib/topic-common.mjs +0 -404
  127. package/cli/lib/topic-decisions.mjs +0 -332
  128. package/cli/lib/topic-draft-packets.mjs +0 -167
  129. package/cli/lib/topic-execution.mjs +0 -533
  130. package/cli/lib/topic-goal.mjs +0 -440
  131. package/cli/lib/topic-ledger.mjs +0 -281
  132. package/cli/lib/topic-lifecycle-artifacts.mjs +0 -173
  133. package/cli/lib/topic-root-validation.mjs +0 -288
  134. package/cli/lib/topic-runner-commands.mjs +0 -174
  135. package/cli/lib/topic-runner-deferral.mjs +0 -532
  136. package/cli/lib/topic-runner-stale-gates.mjs +0 -114
  137. package/cli/lib/topic-runner-validation.mjs +0 -138
  138. package/cli/lib/topic-runner.mjs +0 -727
  139. package/cli/lib/topic-scaffold.mjs +0 -252
  140. package/cli/lib/topic-waves.mjs +0 -403
  141. package/cli/lib/topic.mjs +0 -81
  142. package/config/audit-execution-artifacts.yaml +0 -20
  143. package/config/external-execution-artifacts.yaml +0 -16
  144. package/config/host-adapter.yaml +0 -30
  145. package/config/host-profile.yaml +0 -29
  146. package/config/installer-evidence.yaml +0 -31
  147. package/config/skill-installer.yaml +0 -23
  148. package/config/skill-manifest.yaml +0 -48
  149. package/config/skills.yaml +0 -30
  150. package/contracts/acceptance.schema.yaml +0 -16
  151. package/contracts/admission-checklist.schema.yaml +0 -15
  152. package/contracts/audit-chunk.schema.yaml +0 -123
  153. package/contracts/audit-closeout.schema.yaml +0 -51
  154. package/contracts/audit-finding.schema.yaml +0 -61
  155. package/contracts/audit-ledger.schema.yaml +0 -138
  156. package/contracts/audit-plan.schema.yaml +0 -138
  157. package/contracts/audit-remediation-map.schema.yaml +0 -52
  158. package/contracts/audit-rerun.schema.yaml +0 -31
  159. package/contracts/audit-sweep-result.yaml +0 -53
  160. package/contracts/authority-convergence-audit.schema.yaml +0 -19
  161. package/contracts/closeout.schema.yaml +0 -25
  162. package/contracts/decision-review.schema.yaml +0 -16
  163. package/contracts/doc-spec-audit-result.yaml +0 -19
  164. package/contracts/execution-packet.schema.yaml +0 -49
  165. package/contracts/external-host-compatibility.yaml +0 -22
  166. package/contracts/forbidden-shortcuts.catalog.yaml +0 -23
  167. package/contracts/high-risk-admission.schema.yaml +0 -23
  168. package/contracts/high-risk-execution-result.yaml +0 -20
  169. package/contracts/orchestration-state.schema.yaml +0 -41
  170. package/contracts/overflow-continuation.schema.yaml +0 -12
  171. package/contracts/packet.schema.yaml +0 -30
  172. package/contracts/pending-note.schema.yaml +0 -17
  173. package/contracts/prompt.schema.yaml +0 -12
  174. package/contracts/remediation.schema.yaml +0 -16
  175. package/contracts/result.schema.yaml +0 -24
  176. package/contracts/spec-reconstruction-result.yaml +0 -41
  177. package/contracts/sweep-design-result.yaml +0 -349
  178. package/contracts/topic-goal.schema.yaml +0 -87
  179. package/contracts/topic-run-ledger.schema.yaml +0 -72
  180. package/contracts/topic-step-decision.schema.yaml +0 -45
  181. package/contracts/topic.schema.yaml +0 -65
  182. package/contracts/true-close.schema.yaml +0 -15
  183. package/contracts/wave.schema.yaml +0 -29
  184. package/contracts/worker-output.schema.yaml +0 -15
  185. package/contracts/workflow-consumer.schema.yaml +0 -110
  186. package/methodology/audit-sweep-p0p1-recall.yaml +0 -45
  187. package/methodology/authority-convergence-policy.yaml +0 -42
  188. package/methodology/overflow-continuation-policy.yaml +0 -14
  189. package/methodology/skill-exchange-projection.yaml +0 -114
  190. package/methodology/skill-handoff.yaml +0 -34
  191. package/methodology/skill-installer-result.yaml +0 -27
  192. package/methodology/skill-installer-summary-projection.yaml +0 -181
  193. package/methodology/skill-runtime.yaml +0 -23
  194. package/methodology/spec-target-truth-profile.yaml +0 -46
  195. package/methodology/topic-lifecycle-report.yaml +0 -144
  196. package/methodology/topic-lifecycle.yaml +0 -37
  197. package/methodology/topic-naming-ontology.yaml +0 -21
  198. package/methodology/topic-ontology.yaml +0 -38
  199. package/methodology/topic-validation-policy.yaml +0 -9
  200. package/methodology/wave-dag-policy.yaml +0 -14
  201. package/spec/_meta/command-gating-matrix.yaml +0 -143
  202. package/spec/_meta/generate-drift-migration-checklist.yaml +0 -137
  203. package/spec/_meta/governance-routing-cutover-checklist.yaml +0 -35
  204. package/spec/_meta/phase2-impacted-surface-matrix.yaml +0 -44
  205. package/spec/_meta/spec-authority-cutover-readiness.yaml +0 -102
  206. package/spec/bootstrap-state.yaml +0 -99
@@ -1,252 +0,0 @@
1
- import { mkdir, rename, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import YAML from "yaml";
4
-
5
- import { pathExists, readTextIfFile } from "./fs-helpers.mjs";
6
- import { parseYamlText } from "./yaml-helpers.mjs";
7
- import {
8
- TOPIC_ID_PATTERN,
9
- TOPIC_ROOT,
10
- TOPIC_SLUG_PATTERN,
11
- formatDate,
12
- loadTopicRuntimeAuthority,
13
- toPortableRelativePath,
14
- } from "./topic-common.mjs";
15
-
16
- export function titleFromSlug(slug) {
17
- return slug
18
- .split("-")
19
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
20
- .join(" ");
21
- }
22
- export function deriveTopicId(slug, date = new Date()) {
23
- return TOPIC_ID_PATTERN.test(slug) ? slug : `${formatDate(date)}-${slug}`;
24
- }
25
- export function getTopicRoot(projectRoot) {
26
- return path.join(projectRoot, TOPIC_ROOT);
27
- }
28
- export function getTopicStateRoot(projectRoot, state) {
29
- return path.join(getTopicRoot(projectRoot), state);
30
- }
31
- export function isTopicPathInput(value) {
32
- return typeof value == "string" && (value.includes("/") || value.startsWith("."));
33
- }
34
- export function buildCreatePayload(options, authority) {
35
- return {
36
- topic_id: options.topicId,
37
- state: "proposal",
38
- created_at: options.today,
39
- last_transition_at: options.today,
40
- last_transition_reason: "topic_created_via_nimicoding_topic_create",
41
- title: options.title,
42
- mode: options.mode,
43
- posture: options.posture,
44
- design_policy: options.designPolicy,
45
- parallel_truth: options.parallelTruth,
46
- layering: options.layering,
47
- risk: options.risk,
48
- applicability: options.applicability,
49
- entry_justification: options.justification,
50
- execution_mode: options.executionMode,
51
- selected_next_target: "topic_design_baseline",
52
- current_true_close_status: "not_started",
53
- forbidden_shortcuts: authority.defaultForbiddenShortcuts,
54
- waves: [],
55
- };
56
- }
57
- export function buildReadme(topic) {
58
- return `# ${topic.title}
59
- State: \`${topic.state}\`
60
- This topic was created by \`nimicoding topic create\`.
61
- ## Purpose
62
- TODO: explain why this work needs topic-level governance rather than the ordinary non-topic path.
63
- ## Entry Posture
64
- - mode: \`${topic.mode}\`
65
- - posture: \`${topic.posture}\`
66
- - design policy: \`${topic.design_policy}\`
67
- - applicability: \`${topic.applicability}\`
68
- - execution mode: \`${topic.execution_mode}\`
69
- ## Current Next Action
70
- - selected_next_target: \`${topic.selected_next_target}\`
71
- - TODO: freeze the first bounded wave target before admission
72
- `;
73
- }
74
- export function buildDesign(topicId) {
75
- return `# Design
76
- Topic: \`${topicId}\`
77
- This file is the index for split design companions.
78
- - TODO: add subtopic design files as the topic grows
79
- - TODO: keep this file as an index rather than collapsing the whole topic into one document
80
- `;
81
- }
82
- export function buildSimpleCompanion(title, topicId, bullets) {
83
- return `# ${title}
84
- Topic: \`${topicId}\`
85
- ${bullets.map((item) => `- ${item}`).join(`
86
- `)}
87
- `;
88
- }
89
- export async function writeTopicScaffold(topicDir, topic) {
90
- const files = new Map([
91
- ["topic.yaml", YAML.stringify(topic)],
92
- ["README.md", buildReadme(topic)],
93
- ["design.md", buildDesign(topic.topic_id)],
94
- ["preflight.md", buildSimpleCompanion("Preflight", topic.topic_id, ["TODO", "TODO"])],
95
- ["waves.md", buildSimpleCompanion("Waves", topic.topic_id, ["TODO", "TODO"])],
96
- [
97
- "candidate-wave-plan.md",
98
- buildSimpleCompanion("Candidate Wave Plan", topic.topic_id, ["TODO", "TODO"]),
99
- ],
100
- ["closeout.md", buildSimpleCompanion("Closeout", topic.topic_id, ["TODO", "TODO"])],
101
- [
102
- "implementation-doctrine.md",
103
- buildSimpleCompanion("Implementation Doctrine", topic.topic_id, ["TODO", "TODO"]),
104
- ],
105
- [
106
- "admission-checklists.md",
107
- buildSimpleCompanion("Admission Checklists", topic.topic_id, ["TODO", "TODO"]),
108
- ],
109
- [
110
- "manager-session-protocol.md",
111
- buildSimpleCompanion("Manager Session Protocol", topic.topic_id, ["TODO", "TODO"]),
112
- ],
113
- ["manager-prompts.md", buildSimpleCompanion("Manager Prompts", topic.topic_id, ["TODO"])],
114
- ]);
115
- await mkdir(topicDir, { recursive: false });
116
- for (const [fileName, contents] of files.entries())
117
- await writeFile(path.join(topicDir, fileName), contents, "utf8");
118
- }
119
- export function validateTopicSlug(value) {
120
- return TOPIC_SLUG_PATTERN.test(value);
121
- }
122
- export function validateTopicId(value) {
123
- return TOPIC_ID_PATTERN.test(value);
124
- }
125
- export async function findTopicDirectory(projectRoot, input = null) {
126
- const authority = await loadTopicRuntimeAuthority(projectRoot),
127
- topicStatePattern = authority.topicStates.join("|");
128
- if (!input) {
129
- const current = process.cwd(),
130
- match = toPortableRelativePath(path.relative(projectRoot, current)).match(
131
- new RegExp(
132
- `^\\.nimi/topics/(${topicStatePattern})/(\\d{4}-\\d{2}-\\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*)`,
133
- ),
134
- );
135
- return match
136
- ? {
137
- ok: true,
138
- topicDir: path.join(projectRoot, ".nimi", "topics", match[1], match[2]),
139
- topicId: match[2],
140
- state: match[1],
141
- }
142
- : {
143
- ok: false,
144
- error:
145
- "No topic id or topic path was provided, and the current working directory is not inside a topic root.",
146
- };
147
- }
148
- if (isTopicPathInput(input)) {
149
- const topicDir = path.resolve(projectRoot, input),
150
- match = toPortableRelativePath(path.relative(projectRoot, topicDir)).match(
151
- new RegExp(
152
- `^\\.nimi/topics/(${topicStatePattern})/(\\d{4}-\\d{2}-\\d{2}-[a-z0-9]+(?:-[a-z0-9]+)*)$`,
153
- ),
154
- );
155
- return match
156
- ? { ok: true, topicDir, topicId: match[2], state: match[1] }
157
- : { ok: false, error: `Topic path must resolve to .nimi/topics/<state>/<topic-id>: ${input}` };
158
- }
159
- const matches = [];
160
- for (const state of authority.topicStates) {
161
- const candidate = path.join(getTopicStateRoot(projectRoot, state), input);
162
- (await pathExists(candidate))?.isDirectory() &&
163
- matches.push({ state, topicDir: candidate, topicId: input });
164
- }
165
- return matches.length === 1
166
- ? { ok: true, ...matches[0] }
167
- : matches.length > 1
168
- ? {
169
- ok: false,
170
- error: `Topic id resolves to multiple lifecycle roots and must be disambiguated by path: ${input}`,
171
- }
172
- : { ok: false, error: `Topic not found under ${TOPIC_ROOT}: ${input}` };
173
- }
174
- export async function resolveTopicProjectRoot(startDir) {
175
- let currentDir = path.resolve(startDir);
176
- for (;;) {
177
- if ((await pathExists(path.join(currentDir, ".nimi")))?.isDirectory()) return currentDir;
178
- const parentDir = path.dirname(currentDir);
179
- if (parentDir === currentDir) return path.resolve(startDir);
180
- currentDir = parentDir;
181
- }
182
- }
183
- export async function loadTopicReport(projectRoot, input = null) {
184
- const resolved = await findTopicDirectory(projectRoot, input);
185
- if (!resolved.ok) return resolved;
186
- const topicYamlPath = path.join(resolved.topicDir, "topic.yaml"),
187
- topicYamlText = await readTextIfFile(topicYamlPath);
188
- if (topicYamlText === null)
189
- return {
190
- ok: false,
191
- error: `Missing topic.yaml at ${toPortableRelativePath(path.relative(projectRoot, topicYamlPath))}`,
192
- };
193
- const topic = parseYamlText(topicYamlText);
194
- return !topic || typeof topic != "object"
195
- ? {
196
- ok: false,
197
- error: `topic.yaml is not valid YAML at ${toPortableRelativePath(path.relative(projectRoot, topicYamlPath))}`,
198
- }
199
- : { ok: true, ...resolved, topicYamlPath, topicYamlText, topic };
200
- }
201
- export function getTopicWaves(topic) {
202
- return Array.isArray(topic.waves) ? topic.waves.map((entry) => ({ ...entry })) : [];
203
- }
204
- export function findDeterministicNextWave(topic) {
205
- const waves = getTopicWaves(topic),
206
- terminalIds = new Set(
207
- waves
208
- .filter((entry) => ["closed", "retired", "superseded"].includes(entry.state))
209
- .map((entry) => entry.wave_id),
210
- ),
211
- ready = waves.filter(
212
- (entry) =>
213
- !["closed", "retired", "superseded"].includes(entry.state) &&
214
- ["candidate", "preflight_draft"].includes(entry.state) &&
215
- (Array.isArray(entry.deps) ? entry.deps : []).every((dep) => terminalIds.has(dep)),
216
- );
217
- return ready.length > 0 ? ready[0] : null;
218
- }
219
- export async function writeTopicYaml(topicYamlPath, topic) {
220
- await writeFile(topicYamlPath, YAML.stringify(topic), "utf8");
221
- }
222
- export async function moveTopicDirectoryForState(projectRoot, currentDir, topicId, targetState) {
223
- const targetDir = path.join(getTopicStateRoot(projectRoot, targetState), topicId);
224
- return currentDir === targetDir
225
- ? { topicDir: currentDir, topicYamlPath: path.join(currentDir, "topic.yaml") }
226
- : (await mkdir(path.dirname(targetDir), { recursive: true }),
227
- await rename(currentDir, targetDir),
228
- { topicDir: targetDir, topicYamlPath: path.join(targetDir, "topic.yaml") });
229
- }
230
- export function topicHasEnrichedShape(topic, authority) {
231
- return authority.enrichedRequiredFields.every((field) => {
232
- const value = topic[field];
233
- return field === "selected_next_target"
234
- ? value === null ||
235
- value === "topic_design_baseline" ||
236
- (typeof value == "string" && value.length > 0)
237
- : value != null && value !== "" && (!Array.isArray(value) || value.length > 0);
238
- });
239
- }
240
- export function buildTopicNow() {
241
- return formatDate(new Date());
242
- }
243
- export function isIsoUtcTimestamp(value) {
244
- if (
245
- typeof value !== "string" ||
246
- !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/.test(value)
247
- )
248
- return false;
249
- const parsed = new Date(value),
250
- canonicalValue = value.includes(".") ? value : value.replace("Z", ".000Z");
251
- return !Number.isNaN(parsed.getTime()) && parsed.toISOString() === canonicalValue;
252
- }
@@ -1,403 +0,0 @@
1
- import { readdir, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
-
4
- import { loadAuthorityConvergencePolicy } from "./authority-convergence.mjs";
5
- import { loadTopicRuntimeContracts } from "./contracts.mjs";
6
- import { readTextIfFile } from "./fs-helpers.mjs";
7
- import { fileReferencesWave } from "./topic-lifecycle-artifacts.mjs";
8
- import {
9
- DEFAULT_TOPIC_RUNTIME_AUTHORITY,
10
- PENDING_ENTRY_BLOCKER_STATES,
11
- WAVE_ID_PATTERN,
12
- loadTopicRuntimeAuthority,
13
- toPortableRelativePath,
14
- } from "./topic-common.mjs";
15
- import { pendingNoteFilename, readFrontmatterObject } from "./topic-artifacts.mjs";
16
- import {
17
- buildTopicNow,
18
- findDeterministicNextWave,
19
- getTopicWaves,
20
- isIsoUtcTimestamp,
21
- loadTopicReport,
22
- moveTopicDirectoryForState,
23
- topicHasEnrichedShape,
24
- writeTopicYaml,
25
- } from "./topic-scaffold.mjs";
26
-
27
- export async function collectWaveArtifactEvidence(topicDir, waveId) {
28
- const files = (await readdir(topicDir, { withFileTypes: true }))
29
- .filter((entry) => entry.isFile())
30
- .map((entry) => entry.name);
31
- return {
32
- packetRefs: files.filter(
33
- (name) => name.startsWith("packet-") && fileReferencesWave(name, waveId),
34
- ),
35
- resultRefs: files.filter(
36
- (name) => name.startsWith("result-") && fileReferencesWave(name, waveId),
37
- ),
38
- closeoutRefs: files.filter(
39
- (name) => name.startsWith("closeout-") && fileReferencesWave(name, waveId),
40
- ),
41
- remediationRefs: files.filter(
42
- (name) => name.includes("remediation") && fileReferencesWave(name, waveId),
43
- ),
44
- overflowRefs: files.filter(
45
- (name) => name.includes("overflow-continuation") && fileReferencesWave(name, waveId),
46
- ),
47
- };
48
- }
49
- export async function loadPendingNote(topicDir) {
50
- const notePath = path.join(topicDir, pendingNoteFilename()),
51
- noteText = await readTextIfFile(notePath);
52
- if (noteText === null)
53
- return { ok: false, notePath, error: `Missing pending note artifact: ${pendingNoteFilename()}` };
54
- const note = readFrontmatterObject(noteText);
55
- return note
56
- ? { ok: true, notePath, note }
57
- : { ok: false, notePath, error: "Pending note artifact frontmatter is invalid" };
58
- }
59
- export function getPendingEntryBlockers(topic) {
60
- return getTopicWaves(topic)
61
- .filter((entry) => PENDING_ENTRY_BLOCKER_STATES.has(entry.state))
62
- .map((entry) => `${entry.wave_id}:${entry.state}`);
63
- }
64
- export async function loadTopicValidationPolicy(projectRoot) {
65
- const parsed = (await loadTopicRuntimeContracts(projectRoot)).validationPolicy.data,
66
- entries = Array.isArray(parsed?.topic_validation_policy?.ignore_for_default_validate)
67
- ? parsed.topic_validation_policy.ignore_for_default_validate
68
- : [],
69
- ignoredTopicIds = new Map();
70
- for (const entry of entries)
71
- entry &&
72
- typeof entry.topic_id == "string" &&
73
- entry.topic_id.length > 0 &&
74
- ignoredTopicIds.set(entry.topic_id, {
75
- reason: typeof entry.reason == "string" ? entry.reason : null,
76
- posture: typeof entry.posture == "string" ? entry.posture : null,
77
- });
78
- const semantics = parsed?.topic_validation_policy?.ignored_topic_validate_semantics ?? {};
79
- return {
80
- ignoredTopicIds,
81
- ignoredTopicValidateSemantics: {
82
- status:
83
- typeof semantics.status == "string"
84
- ? semantics.status
85
- : DEFAULT_TOPIC_RUNTIME_AUTHORITY.ignoredTopicValidateSemantics.status,
86
- canonicalSuccess:
87
- typeof semantics.canonical_success == "boolean"
88
- ? semantics.canonical_success
89
- : DEFAULT_TOPIC_RUNTIME_AUTHORITY.ignoredTopicValidateSemantics.canonicalSuccess,
90
- },
91
- };
92
- }
93
- export function validateWaveId(value) {
94
- return WAVE_ID_PATTERN.test(value);
95
- }
96
- export function normalizeDeps(value) {
97
- return Array.isArray(value) ? value.map((entry) => String(entry)) : [];
98
- }
99
- export function validateGraphFromTopic(topic) {
100
- const waves = getTopicWaves(topic),
101
- checks = [],
102
- warnings = [],
103
- waveIds = waves.map((entry) => entry.wave_id),
104
- uniqueWaveIds = new Set(waveIds);
105
- checks.push({
106
- id: "wave_ids_unique",
107
- ok: uniqueWaveIds.size === waveIds.length,
108
- reason:
109
- uniqueWaveIds.size === waveIds.length
110
- ? "wave ids are unique"
111
- : "duplicate wave ids exist in topic.yaml waves[]",
112
- });
113
- const invalidWaveIds = waveIds.filter((entry) => !validateWaveId(entry));
114
- checks.push({
115
- id: "wave_ids_valid",
116
- ok: invalidWaveIds.length === 0,
117
- reason:
118
- invalidWaveIds.length === 0
119
- ? "wave ids use the canonical wave-<n>-slug shape"
120
- : `invalid wave ids: ${invalidWaveIds.join(", ")}`,
121
- });
122
- const missingDeps = [],
123
- selectedWaveIds = [],
124
- retiredSelected = [];
125
- for (const wave of waves) {
126
- const deps = normalizeDeps(wave.deps);
127
- for (const dep of deps) uniqueWaveIds.has(dep) || missingDeps.push(`${wave.wave_id}->${dep}`);
128
- (wave.selected === true && selectedWaveIds.push(wave.wave_id),
129
- wave.selected === true &&
130
- ["retired", "superseded"].includes(wave.state) &&
131
- retiredSelected.push(wave.wave_id));
132
- }
133
- (checks.push({
134
- id: "wave_dependencies_resolve",
135
- ok: missingDeps.length === 0,
136
- reason:
137
- missingDeps.length === 0
138
- ? "all wave dependencies resolve inside the topic"
139
- : `missing dependency refs: ${missingDeps.join(", ")}`,
140
- }),
141
- checks.push({
142
- id: "selected_wave_unique",
143
- ok: selectedWaveIds.length <= 1,
144
- reason:
145
- selectedWaveIds.length <= 1
146
- ? "selected wave is unique"
147
- : `multiple selected waves exist: ${selectedWaveIds.join(", ")}`,
148
- }));
149
- const selectedMatchesTopicTarget =
150
- selectedWaveIds.length === 0
151
- ? topic.selected_next_target === "topic_design_baseline" ||
152
- topic.selected_next_target === null
153
- : selectedWaveIds[0] === topic.selected_next_target;
154
- (checks.push({
155
- id: "selected_wave_matches_topic_target",
156
- ok: selectedMatchesTopicTarget,
157
- reason: selectedMatchesTopicTarget
158
- ? "selected wave matches topic.selected_next_target"
159
- : `selected wave and topic.selected_next_target diverge (${selectedWaveIds[0] ?? "none"} vs ${topic.selected_next_target ?? "none"})`,
160
- }),
161
- checks.push({
162
- id: "retired_or_superseded_not_selected",
163
- ok: retiredSelected.length === 0,
164
- reason:
165
- retiredSelected.length === 0
166
- ? "retired or superseded waves are not selected"
167
- : `retired/superseded waves remain selected: ${retiredSelected.join(", ")}`,
168
- }));
169
- const visiting = new Set(),
170
- visited = new Set();
171
- let cycleRef = null;
172
- const waveMap = new Map(waves.map((wave) => [wave.wave_id, wave]));
173
- function dfs(waveId, trail = []) {
174
- if (cycleRef) return;
175
- if (visiting.has(waveId)) {
176
- cycleRef = [...trail, waveId].join(" -> ");
177
- return;
178
- }
179
- if (visited.has(waveId)) return;
180
- visiting.add(waveId);
181
- const wave = waveMap.get(waveId);
182
- if (wave) for (const dep of normalizeDeps(wave.deps)) dfs(dep, [...trail, waveId]);
183
- (visiting.delete(waveId), visited.add(waveId));
184
- }
185
- for (const waveId of waveIds) dfs(waveId);
186
- return (
187
- checks.push({
188
- id: "graph_acyclic",
189
- ok: cycleRef === null,
190
- reason:
191
- cycleRef === null ? "wave graph is acyclic" : `wave graph contains a cycle: ${cycleRef}`,
192
- }),
193
- waves.length === 0 && warnings.push("topic has no machine wave registry yet"),
194
- { ok: checks.every((entry) => entry.ok), checks, warnings, waves }
195
- );
196
- }
197
- export async function validateTopicGraph(projectRoot, input = null) {
198
- const loaded = await loadTopicReport(projectRoot, input);
199
- if (!loaded.ok) return { ok: false, error: loaded.error, checks: [], warnings: [] };
200
- const { validateTopicRoot } = await import("./topic-root-validation.mjs");
201
- const rootValidation = await validateTopicRoot(projectRoot, input);
202
- if (!rootValidation.ok) return rootValidation;
203
- const authority = await loadTopicRuntimeAuthority(projectRoot);
204
- if (!topicHasEnrichedShape(loaded.topic, authority))
205
- return {
206
- ...rootValidation,
207
- ok: false,
208
- checks: [
209
- ...rootValidation.checks,
210
- {
211
- id: "enriched_topic_required_for_wave_graph",
212
- ok: false,
213
- reason: "wave graph commands require an enriched topic root",
214
- },
215
- ],
216
- warnings: rootValidation.warnings,
217
- };
218
- const graph = validateGraphFromTopic(loaded.topic);
219
- return {
220
- ...rootValidation,
221
- ok: rootValidation.ok && graph.ok,
222
- checks: [...rootValidation.checks, ...graph.checks],
223
- warnings: [...rootValidation.warnings, ...graph.warnings],
224
- waveCount: graph.waves.length,
225
- };
226
- }
227
- export async function validateWaveAdmission(projectRoot, input, waveId) {
228
- const loaded = await loadTopicReport(projectRoot, input);
229
- if (!loaded.ok) return { ok: false, error: loaded.error, checks: [], warnings: [] };
230
- const graphReport = await validateTopicGraph(projectRoot, input),
231
- wave = getTopicWaves(loaded.topic).find((entry) => entry.wave_id === waveId) ?? null,
232
- checks = [...(graphReport.checks ?? [])],
233
- warnings = [...(graphReport.warnings ?? [])];
234
- if (
235
- (checks.push({
236
- id: "wave_exists",
237
- ok: wave !== null,
238
- reason: wave ? "wave exists in topic.yaml waves[]" : `wave does not exist: ${waveId}`,
239
- }),
240
- !wave)
241
- )
242
- return { ...graphReport, ok: false, checks, warnings };
243
- const dispatchableState = !["retired", "superseded", "closed", "overflowed"].includes(wave.state);
244
- (checks.push({
245
- id: "wave_state_dispatchable",
246
- ok: dispatchableState,
247
- reason: dispatchableState
248
- ? "wave state is eligible for admission"
249
- : `wave state is not admissible: ${wave.state}`,
250
- }),
251
- checks.push({
252
- id: "wave_selected",
253
- ok: wave.selected === true,
254
- reason: wave.selected === true ? "wave is selected" : "wave must be selected before admission",
255
- }),
256
- checks.push({
257
- id: "selected_target_matches_wave",
258
- ok: loaded.topic.selected_next_target === waveId,
259
- reason:
260
- loaded.topic.selected_next_target === waveId
261
- ? "topic.selected_next_target matches the wave"
262
- : `topic.selected_next_target does not match wave (${loaded.topic.selected_next_target ?? "none"} vs ${waveId})`,
263
- }));
264
- const waveMap = new Map(getTopicWaves(loaded.topic).map((entry) => [entry.wave_id, entry])),
265
- unmetDeps = normalizeDeps(wave.deps).filter((dep) => waveMap.get(dep)?.state !== "closed");
266
- checks.push({
267
- id: "upstream_dependencies_closed",
268
- ok: unmetDeps.length === 0,
269
- reason:
270
- unmetDeps.length === 0
271
- ? "all upstream dependencies are closed"
272
- : `upstream dependencies are not closed: ${unmetDeps.join(", ")}`,
273
- });
274
- const waveStateAllowedForAdmit = ["candidate", "preflight_draft", "needs_revision"].includes(
275
- wave.state,
276
- );
277
- return (
278
- checks.push({
279
- id: "wave_state_allows_preflight_admission",
280
- ok: waveStateAllowedForAdmit,
281
- reason: waveStateAllowedForAdmit
282
- ? "wave state can move to preflight_admitted"
283
- : `wave state cannot move to preflight_admitted from ${wave.state}`,
284
- }),
285
- { ...graphReport, ok: graphReport.ok && checks.every((entry) => entry.ok), checks, warnings }
286
- );
287
- }
288
- export async function addWaveToTopic(projectRoot, input, wave) {
289
- const loaded = await loadTopicReport(projectRoot, input);
290
- if (!loaded.ok) return loaded;
291
- const authority = await loadTopicRuntimeAuthority(projectRoot);
292
- if (!topicHasEnrichedShape(loaded.topic, authority))
293
- return { ok: false, error: "Wave commands require an enriched topic root." };
294
- const waves = getTopicWaves(loaded.topic);
295
- if (waves.some((entry) => entry.wave_id === wave.wave_id))
296
- return { ok: false, error: `Wave already exists: ${wave.wave_id}` };
297
- waves.push(wave);
298
- const graphPreview = validateGraphFromTopic({ ...loaded.topic, waves }),
299
- failedCheck = graphPreview.checks.find((entry) => !entry.ok);
300
- return failedCheck
301
- ? {
302
- ok: false,
303
- error: `Wave add refused: ${failedCheck.reason}`,
304
- checks: graphPreview.checks,
305
- warnings: graphPreview.warnings,
306
- }
307
- : ((loaded.topic.waves = waves),
308
- await writeTopicYaml(loaded.topicYamlPath, loaded.topic),
309
- {
310
- ok: true,
311
- topicId: loaded.topicId,
312
- topicRef: toPortableRelativePath(path.relative(projectRoot, loaded.topicDir)),
313
- waveId: wave.wave_id,
314
- waveState: wave.state,
315
- });
316
- }
317
- export async function selectWaveInTopic(projectRoot, input, waveId) {
318
- const loaded = await loadTopicReport(projectRoot, input);
319
- if (!loaded.ok) return loaded;
320
- const authority = await loadTopicRuntimeAuthority(projectRoot);
321
- if (!topicHasEnrichedShape(loaded.topic, authority))
322
- return { ok: false, error: "Wave commands require an enriched topic root." };
323
- const waves = getTopicWaves(loaded.topic),
324
- wave = waves.find((entry) => entry.wave_id === waveId);
325
- if (!wave) return { ok: false, error: `Wave not found: ${waveId}` };
326
- if (["retired", "superseded", "closed", "overflowed"].includes(wave.state))
327
- return {
328
- ok: false,
329
- error: `Wave select refused: ${waveId} is not selectable from state ${wave.state}`,
330
- };
331
- for (const entry of waves) entry.selected = entry.wave_id === waveId;
332
- return (
333
- (loaded.topic.waves = waves),
334
- (loaded.topic.selected_next_target = waveId),
335
- (loaded.topic.last_transition_at = buildTopicNow()),
336
- (loaded.topic.last_transition_reason = `selected_${waveId}_as_next_execution_target`),
337
- await writeTopicYaml(loaded.topicYamlPath, loaded.topic),
338
- {
339
- ok: true,
340
- topicId: loaded.topicId,
341
- topicRef: toPortableRelativePath(path.relative(projectRoot, loaded.topicDir)),
342
- waveId,
343
- selectedNextTarget: loaded.topic.selected_next_target,
344
- }
345
- );
346
- }
347
- export async function admitWaveInTopic(projectRoot, input, waveId) {
348
- let validation = await validateWaveAdmission(projectRoot, input, waveId);
349
- if (!validation.ok) {
350
- const loadedForSelection = await loadTopicReport(projectRoot, input),
351
- wavesForSelection = getTopicWaves(loadedForSelection.topic),
352
- waveForSelection = wavesForSelection.find((entry) => entry.wave_id === waveId),
353
- terminalIds = new Set(
354
- wavesForSelection
355
- .filter((entry) => ["closed", "retired", "superseded"].includes(entry.state))
356
- .map((entry) => entry.wave_id),
357
- ),
358
- depsClosed =
359
- waveForSelection &&
360
- (Array.isArray(waveForSelection.deps) ? waveForSelection.deps : []).every((dep) =>
361
- terminalIds.has(dep),
362
- ),
363
- canSelectForAdmission =
364
- loadedForSelection.ok &&
365
- (loadedForSelection.topic.selected_next_target === null ||
366
- loadedForSelection.topic.selected_next_target === "topic_design_baseline") &&
367
- waveForSelection &&
368
- ["candidate", "preflight_draft", "needs_revision"].includes(waveForSelection.state) &&
369
- depsClosed;
370
- if (canSelectForAdmission) {
371
- const selected = await selectWaveInTopic(projectRoot, input, waveId);
372
- if (!selected.ok) return selected;
373
- validation = await validateWaveAdmission(projectRoot, input, waveId);
374
- }
375
- }
376
- if (!validation.ok) return validation;
377
- const loaded = await loadTopicReport(projectRoot, input),
378
- waves = getTopicWaves(loaded.topic),
379
- wave = waves.find((entry) => entry.wave_id === waveId);
380
- ((wave.state = "preflight_admitted"), (loaded.topic.waves = waves));
381
- let nextState = loaded.topic.state;
382
- (["proposal", "pending"].includes(loaded.topic.state) &&
383
- ((nextState = "ongoing"), (loaded.topic.state = nextState)),
384
- (loaded.topic.last_transition_at = buildTopicNow()),
385
- (loaded.topic.last_transition_reason = `wave_${waveId}_preflight_admitted`));
386
- const moved = await moveTopicDirectoryForState(
387
- projectRoot,
388
- loaded.topicDir,
389
- loaded.topicId,
390
- nextState,
391
- );
392
- return (
393
- await writeTopicYaml(moved.topicYamlPath, loaded.topic),
394
- {
395
- ok: true,
396
- topicId: loaded.topicId,
397
- topicRef: toPortableRelativePath(path.relative(projectRoot, moved.topicDir)),
398
- waveId,
399
- waveState: wave.state,
400
- state: loaded.topic.state,
401
- }
402
- );
403
- }