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