@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,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
- }