@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,440 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { readdir } from "node:fs/promises";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- import { pathExists, readTextIfFile } from "./fs-helpers.mjs";
7
- import { loadTopicReport, validateTopicGraph, validateTopicRoot } from "./topic.mjs";
8
- import { parseYamlText } from "./yaml-helpers.mjs";
9
- import { loadGovernanceConfig } from "./internal/governance/config.mjs";
10
-
11
- const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
12
- const PACKAGE_REF_PREFIX = "package://@nimiplatform/nimi-coding/";
13
- const TOPIC_GOAL_CONTRACT_REF = `${PACKAGE_REF_PREFIX}contracts/topic-goal.schema.yaml`;
14
- const HOST_TOPIC_GOAL_CONTRACT_REF = ".nimi/contracts/topic-goal.schema.yaml";
15
- const FORBIDDEN_SHORTCUTS_REF = `${PACKAGE_REF_PREFIX}contracts/forbidden-shortcuts.catalog.yaml`;
16
- const HOST_FORBIDDEN_SHORTCUTS_REF = ".nimi/contracts/forbidden-shortcuts.catalog.yaml";
17
- const GOAL_COMMAND_MAX_CHARS = 1500;
18
-
19
- const REQUIRED_ARTIFACTS = [
20
- "topic.yaml",
21
- "design.md",
22
- "waves.md",
23
- "candidate-wave-plan.md",
24
- "admission-checklists.md",
25
- "preflight.md",
26
- "implementation-doctrine.md",
27
- "manager-session-protocol.md",
28
- "manager-prompts.md",
29
- "closeout.md",
30
- ];
31
-
32
- const REQUIRED_STOP_KEYS = [
33
- "mvp_subset_contract",
34
- "legacy_alias",
35
- "compat_shim",
36
- "dual_read",
37
- "dual_write",
38
- "placeholder_success",
39
- "happy_path_only_closure",
40
- "time_phased_layering",
41
- "app_local_shadow_truth",
42
- "silent_owner_cut_reopen",
43
- ];
44
-
45
- const EXECUTION_STAGE_WAVE_STATES = new Set(["preflight_admitted", "implementation_admitted", "implementation_active"]);
46
- const ADMISSION_READY_WAVE_STATES = new Set(["candidate", "preflight_draft", "needs_revision"]);
47
- const TERMINAL_WAVE_STATES = new Set(["closed", "retired", "superseded"]);
48
- const WAVE_ID_PATTERN = /^wave-[a-z0-9]+(?:-[a-z0-9]+)*$/;
49
-
50
- const CHECK_REASON = {
51
- topic_validate_passed: "topic_validate_failed",
52
- topic_graph_validate_passed: "topic_graph_validate_failed",
53
- topic_lifecycle_ongoing: "topic_not_ongoing",
54
- true_close_not_started: "true_close_not_started_required",
55
- strict_policy_active: "strict_policy_ignored",
56
- parallel_truth_forbidden: "parallel_truth_not_forbidden",
57
- profile_resolves: "unknown_profile",
58
- selected_target_wave_resolves: "selected_target_not_wave",
59
- selected_wave_single_source: "selected_wave_mismatch",
60
- wave_option_matches_selected: "wave_override_forbidden",
61
- selected_wave_executable: "selected_wave_not_executable",
62
- selected_wave_dependencies_terminal: "dependency_not_terminal",
63
- selected_wave_goal_present: "missing_primary_closure_goal",
64
- forbidden_shortcuts_present: "forbidden_shortcuts_incomplete",
65
- forbidden_shortcuts_catalog_aligned: "forbidden_shortcuts_catalog_drift",
66
- required_artifacts_present: "required_artifact_missing",
67
- no_unresolved_placeholders: "unresolved_placeholder",
68
- stop_line_declared: "stop_line_missing",
69
- human_gates_declared: "human_gates_missing",
70
- validation_commands_declared: "validation_commands_missing",
71
- closeout_criteria_declared: "closeout_criteria_missing",
72
- authority_owner_declared: "authority_owner_missing",
73
- work_type_declared: "work_type_missing",
74
- authority_change_admitted: "authority_alignment_missing",
75
- host_projection_aligned: "host_projection_drift",
76
- goal_size_within_limit: "goal_too_large",
77
- };
78
-
79
- function toPortableRelativePath(filePath) {
80
- return filePath.split(path.sep).join("/");
81
- }
82
-
83
- function projectRef(projectRoot, absolutePath) {
84
- return toPortableRelativePath(path.relative(projectRoot, absolutePath));
85
- }
86
-
87
- function packagePath(relativePath) {
88
- return path.join(PACKAGE_ROOT, relativePath.replace(PACKAGE_REF_PREFIX, ""));
89
- }
90
-
91
- function check(id, ok, message, extra = {}) {
92
- return {
93
- id,
94
- status: ok ? "pass" : "fail",
95
- severity: ok ? "info" : "blocking",
96
- message,
97
- ...extra,
98
- };
99
- }
100
-
101
- function sectionText(markdown, sectionTitle) {
102
- const escaped = sectionTitle.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
103
- const match = markdown.match(new RegExp(`^##\\s+${escaped}\\s*$([\\s\\S]*?)(?=^##\\s+|$(?![\\s\\S]))`, "im"));
104
- return match ? match[1].trim() : "";
105
- }
106
-
107
- function hasSectionBullet(markdown, sectionTitle) {
108
- return /^[-*]\s+\S/m.test(sectionText(markdown, sectionTitle));
109
- }
110
-
111
- function hasUnresolvedPlaceholder(text) {
112
- return /(?<![A-Za-z-])(?:TODO|TBD|FIXME|XXX)(?![A-Za-z-])/i.test(text)
113
- || /<\s*(?:placeholder|place-holder|fill-me|missing)\s*>/i.test(text)
114
- || /\?\?\?/.test(text);
115
- }
116
-
117
- function normalizeCommandText(command) {
118
- return String(command ?? "").replace(/\s+/g, " ").trim();
119
- }
120
-
121
- function validationCommandScope(command) {
122
- const scopeMatch = command.match(/(?:^|\s)--scope\s+([^\s]+)/u);
123
- if (scopeMatch) {
124
- return scopeMatch[1];
125
- }
126
- if (command.includes("topic validate graph")) {
127
- return "graph";
128
- }
129
- if (command.includes("topic validate")) {
130
- return "topic";
131
- }
132
- if (command.includes("test")) {
133
- return "test";
134
- }
135
- return "selected wave";
136
- }
137
-
138
- function validationCommandEntry(command) {
139
- const normalized = normalizeCommandText(command);
140
- return {
141
- command: normalized,
142
- cwd: ".",
143
- profile: null,
144
- scope: validationCommandScope(normalized),
145
- required: true,
146
- expected_exit_code: 0,
147
- };
148
- }
149
-
150
- function mergeValidationCommandEntries(entries) {
151
- const seen = new Set();
152
- const output = [];
153
- for (const entry of entries) {
154
- if (!entry.command || seen.has(entry.command)) {
155
- continue;
156
- }
157
- seen.add(entry.command);
158
- output.push(entry);
159
- }
160
- return output;
161
- }
162
-
163
- function parseValidationCommands(artifactTexts) {
164
- const commands = [];
165
- const commandPattern = /\b(?:pnpm|npm|npx|node|go|cargo)\s+[^\n`]+/g;
166
- for (const { text } of artifactTexts) {
167
- for (const match of text.matchAll(commandPattern)) {
168
- const command = normalizeCommandText(match[0].replace(/[.)\]]+$/u, ""));
169
- if (command.includes("<") || command.includes("topic goal")) {
170
- continue;
171
- }
172
- if (command.length > 0 && !commands.includes(command)) {
173
- commands.push(command);
174
- }
175
- }
176
- }
177
- return commands.map(validationCommandEntry);
178
- }
179
-
180
- function selectedWaveValidationCommands(selectedWave) {
181
- const commands = [
182
- ...(Array.isArray(selectedWave?.validation_commands) ? selectedWave.validation_commands : []),
183
- ...(Array.isArray(selectedWave?.source_sweep_design?.validation_commands) ? selectedWave.source_sweep_design.validation_commands : []),
184
- ];
185
- return commands
186
- .map(normalizeCommandText)
187
- .filter((command) => command.length > 0)
188
- .map(validationCommandEntry);
189
- }
190
-
191
- function parseHumanGates(preflightText) {
192
- return sectionText(preflightText, "Human Gates")
193
- .split("\n")
194
- .map((line) => line.match(/^[-*]\s+(.+)$/)?.[1]?.trim())
195
- .filter(Boolean);
196
- }
197
-
198
- async function resolveProfile(projectRoot, requestedProfile) {
199
- const governance = await loadGovernanceConfig(projectRoot);
200
- const hostText = await readTextIfFile(path.join(projectRoot, ".nimi", "config", "host-profile.yaml"))
201
- ?? await readTextIfFile(path.join(PACKAGE_ROOT, "config", "host-profile.yaml"));
202
- const hostProfile = parseYamlText(hostText)?.host_profile?.id ?? null;
203
- const defaultProfile = governance.ok ? governance.config.profileId : hostProfile;
204
- const effectiveProfile = requestedProfile ?? defaultProfile;
205
- return {
206
- ok: typeof effectiveProfile === "string"
207
- && effectiveProfile.length > 0
208
- && effectiveProfile === defaultProfile,
209
- profile: effectiveProfile,
210
- defaultProfile,
211
- };
212
- }
213
-
214
- async function loadForbiddenShortcutsCatalog(projectRoot) {
215
- const canonicalText = await readTextIfFile(packagePath(FORBIDDEN_SHORTCUTS_REF));
216
- const hostText = await readTextIfFile(path.join(projectRoot, HOST_FORBIDDEN_SHORTCUTS_REF));
217
- const catalog = parseYamlText(canonicalText);
218
- const keys = Array.isArray(catalog?.entries)
219
- ? catalog.entries.map((entry) => entry?.key).filter((key) => typeof key === "string")
220
- : [];
221
- return {
222
- keys,
223
- aligned: hostText === null || hostText === canonicalText,
224
- };
225
- }
226
-
227
- async function loadSourceArtifacts(topicDir) {
228
- const artifactTexts = [];
229
- const missing = [];
230
- for (const fileName of REQUIRED_ARTIFACTS) {
231
- const artifactPath = path.join(topicDir, fileName);
232
- const text = await readTextIfFile(artifactPath);
233
- if (text === null) {
234
- missing.push(fileName);
235
- } else {
236
- artifactTexts.push({ ref: fileName, text });
237
- }
238
- }
239
- return { artifactTexts, missing };
240
- }
241
-
242
- function fileReferencesWave(fileName, waveId) {
243
- return fileName.includes(waveId) || fileName.includes(waveId.replace(/^wave-/, "wave-"));
244
- }
245
-
246
- async function collectLineageArtifacts(topicDir, waveIds) {
247
- const entries = await readdir(topicDir, { withFileTypes: true });
248
- return entries
249
- .filter((entry) => entry.isFile())
250
- .map((entry) => entry.name)
251
- .filter((name) => /^(packet|result|closeout)-/.test(name))
252
- .filter((name) => waveIds.some((waveId) => fileReferencesWave(name, waveId)))
253
- .sort();
254
- }
255
-
256
- function selectedWaveResolution(topic) {
257
- const waves = Array.isArray(topic.waves) ? topic.waves : [];
258
- const selectedTarget = typeof topic.selected_next_target === "string" ? topic.selected_next_target : null;
259
- const matchingWaves = selectedTarget === null ? [] : waves.filter((wave) => wave.wave_id === selectedTarget);
260
- const selectedWaves = waves.filter((wave) => wave.selected === true);
261
- const terminalIds = new Set(waves.filter((wave) => TERMINAL_WAVE_STATES.has(wave.state)).map((wave) => wave.wave_id));
262
- const allWavesTerminal = waves.length > 0 && waves.every((wave) => TERMINAL_WAVE_STATES.has(wave.state));
263
- const deterministicNextWave = allWavesTerminal ? null : waves.find((wave) => {
264
- if (TERMINAL_WAVE_STATES.has(wave.state)) return false;
265
- if (!ADMISSION_READY_WAVE_STATES.has(wave.state)) return false;
266
- const deps = Array.isArray(wave.deps) ? wave.deps : [];
267
- return deps.every((dep) => terminalIds.has(dep));
268
- }) ?? null;
269
- const selectedWave = matchingWaves.length === 1 ? matchingWaves[0] : null;
270
- const executionStartWave = selectedWave ?? deterministicNextWave;
271
- return {
272
- waves,
273
- selectedTarget,
274
- selectedWave,
275
- executionStartWave,
276
- deterministicNextWave,
277
- allWavesTerminal,
278
- matchingWaveCount: matchingWaves.length,
279
- selectedWaves,
280
- };
281
- }
282
-
283
- function dependencyEvidence(lineageRefs, depId) {
284
- return lineageRefs.some((ref) => fileReferencesWave(ref, depId) && /^(result|closeout)-/.test(ref));
285
- }
286
-
287
- function buildGoalCommand(topicId, executionStartWave, sourceArtifacts) {
288
- const artifactList = sourceArtifacts.map((ref) => path.basename(ref)).join(", ");
289
- const cursorClause = executionStartWave
290
- ? `, starting at execution cursor ${executionStartWave.wave_id}`
291
- : "";
292
- return `/goal Execute topic ${topicId} to completion${cursorClause}. This is a topic-level goal: do not mark complete after a single wave closeout. Use nimicoding topic-runner run ${topicId} --run-id <run-id> --adapter codex to advance deterministic wave admission, preflight, implementation, validation, result recording, wave closeout, and next-wave selection. Treat ${artifactList} as the execution contract. Wave closeout is a validation boundary only. Complete only after all waves are terminal and topic true-close/closeout evidence is recorded. Stop only for declared human gates, authority/scope changes, lowered gates, destructive evidence deletion, unresolved blockers, or required contract changes.`;
293
- }
294
-
295
- async function checkHostProjection(projectRoot) {
296
- const canonicalText = await readTextIfFile(packagePath(TOPIC_GOAL_CONTRACT_REF));
297
- const hostPath = path.join(projectRoot, HOST_TOPIC_GOAL_CONTRACT_REF);
298
- const hostInfo = await pathExists(hostPath);
299
- if (!hostInfo) {
300
- return { ok: true, message: "host topic-goal projection is absent" };
301
- }
302
- const hostText = await readTextIfFile(hostPath);
303
- return {
304
- ok: canonicalText !== null && hostText === canonicalText,
305
- message: canonicalText !== null && hostText === canonicalText
306
- ? "host topic-goal schema projection matches the package contract"
307
- : "host topic-goal schema projection differs from the package contract",
308
- };
309
- }
310
-
311
- function buildStateHash(artifactTexts, extraTexts) {
312
- const hash = createHash("sha256");
313
- for (const artifact of [...artifactTexts, ...extraTexts].sort((a, b) => a.ref.localeCompare(b.ref))) {
314
- hash.update(artifact.ref);
315
- hash.update("\0");
316
- hash.update(artifact.text ?? "");
317
- hash.update("\0");
318
- }
319
- return hash.digest("hex");
320
- }
321
-
322
- export async function buildTopicGoal(projectRoot, options) {
323
- const loaded = await loadTopicReport(projectRoot, options.topicInput);
324
- if (!loaded.ok) {
325
- return {
326
- inputError: true,
327
- ok: false,
328
- error: loaded.error,
329
- };
330
- }
331
-
332
- const topicReport = await validateTopicRoot(projectRoot, options.topicInput);
333
- const graphReport = await validateTopicGraph(projectRoot, options.topicInput);
334
- const profile = await resolveProfile(projectRoot, options.profile);
335
- const { artifactTexts, missing } = await loadSourceArtifacts(loaded.topicDir);
336
- const textByRef = new Map(artifactTexts.map((artifact) => [artifact.ref, artifact.text]));
337
- const resolution = selectedWaveResolution(loaded.topic);
338
- const lineageWaveIds = [
339
- ...new Set([
340
- resolution.selectedTarget,
341
- resolution.executionStartWave?.wave_id,
342
- ...resolution.waves.flatMap((wave) => Array.isArray(wave.deps) ? wave.deps : []),
343
- ].filter(Boolean)),
344
- ];
345
- const lineageRefs = await collectLineageArtifacts(loaded.topicDir, lineageWaveIds);
346
- const lineageTexts = [];
347
- for (const ref of lineageRefs) {
348
- const text = await readTextIfFile(path.join(loaded.topicDir, ref));
349
- lineageTexts.push({ ref, text: text ?? "" });
350
- }
351
- const sourceArtifacts = [...REQUIRED_ARTIFACTS, ...lineageRefs];
352
- const sourceArtifactTexts = [...artifactTexts, ...lineageTexts];
353
- const validationCommands = mergeValidationCommandEntries([
354
- ...selectedWaveValidationCommands(resolution.executionStartWave),
355
- ...parseValidationCommands(sourceArtifactTexts.filter((artifact) => artifact.ref !== "topic.yaml")),
356
- ]);
357
- const humanGates = parseHumanGates(textByRef.get("preflight.md") ?? "");
358
- const forbiddenCatalog = await loadForbiddenShortcutsCatalog(projectRoot);
359
- const hostProjection = await checkHostProjection(projectRoot);
360
- const executionStartWave = resolution.executionStartWave;
361
- const deps = Array.isArray(executionStartWave?.deps) ? executionStartWave.deps : [];
362
- const depFailures = deps.filter((depId) => {
363
- const depWave = resolution.waves.find((wave) => wave.wave_id === depId);
364
- return !depWave || !TERMINAL_WAVE_STATES.has(depWave.state) || !dependencyEvidence(lineageRefs, depId);
365
- });
366
- const selectedTargetReady = resolution.selectedTarget === null || resolution.selectedTarget === "topic_design_baseline"
367
- ? resolution.deterministicNextWave !== null || resolution.allWavesTerminal
368
- : resolution.matchingWaveCount === 1 && WAVE_ID_PATTERN.test(resolution.selectedTarget);
369
- const selectedWaveSourceReady = resolution.selectedTarget === null || resolution.selectedTarget === "topic_design_baseline"
370
- ? resolution.selectedWaves.length === 0
371
- : resolution.selectedWaves.length === 1 && resolution.selectedWaves[0]?.wave_id === resolution.selectedTarget;
372
- const executionStartWaveReady = executionStartWave
373
- ? EXECUTION_STAGE_WAVE_STATES.has(executionStartWave.state) || ADMISSION_READY_WAVE_STATES.has(executionStartWave.state)
374
- : resolution.allWavesTerminal;
375
- const executionStartGoalPresent = executionStartWave
376
- ? typeof executionStartWave.primary_closure_goal === "string" && executionStartWave.primary_closure_goal.trim().length > 0
377
- : resolution.allWavesTerminal;
378
- const commands = validationCommands.map((entry) => entry.command);
379
-
380
- const checks = [
381
- check("topic_validate_passed", topicReport.ok === true, topicReport.ok ? "topic validate passes" : topicReport.error ?? "topic validate failed"),
382
- check("topic_graph_validate_passed", graphReport.ok === true, graphReport.ok ? "topic validate graph passes" : graphReport.error ?? "topic validate graph failed"),
383
- check("topic_lifecycle_ongoing", loaded.topic.state === "ongoing", `topic state is ${loaded.topic.state ?? "missing"}`),
384
- check("true_close_not_started", loaded.topic.current_true_close_status === "not_started", `current_true_close_status is ${loaded.topic.current_true_close_status ?? "missing"}`),
385
- check("strict_policy_active", topicReport.ignoredByPolicy !== true, topicReport.ignoredByPolicy ? "topic root is ignored by strict validation policy" : "topic root is under strict validation policy"),
386
- check("parallel_truth_forbidden", loaded.topic.parallel_truth === "forbidden", `parallel_truth is ${loaded.topic.parallel_truth ?? "missing"}`),
387
- check("profile_resolves", profile.ok, profile.ok ? `profile resolves as ${profile.profile}` : `unknown or mismatched profile ${profile.profile ?? "missing"}`),
388
- check("selected_target_wave_resolves", selectedTargetReady, resolution.selectedTarget ? `selected_next_target is ${resolution.selectedTarget}` : `execution cursor is ${executionStartWave?.wave_id ?? (resolution.allWavesTerminal ? "topic closeout" : "missing")}`),
389
- check("selected_wave_single_source", selectedWaveSourceReady, `selected waves: ${resolution.selectedWaves.map((wave) => wave.wave_id).join(", ") || "none"}`),
390
- check("wave_option_matches_selected", options.wave === null || options.wave === executionStartWave?.wave_id, options.wave === null ? "no wave assertion provided" : `--wave is ${options.wave}`),
391
- check("selected_wave_executable", executionStartWaveReady, executionStartWave ? `execution cursor wave state is ${executionStartWave.state}` : resolution.allWavesTerminal ? "topic is ready for topic closeout" : "execution cursor does not resolve"),
392
- check("selected_wave_dependencies_terminal", depFailures.length === 0, depFailures.length === 0 ? "selected wave dependencies are terminal by lifecycle evidence" : `dependencies are not terminal by evidence: ${depFailures.join(", ")}`),
393
- check("selected_wave_goal_present", executionStartGoalPresent, executionStartWave?.primary_closure_goal ? "execution cursor declares primary_closure_goal" : resolution.allWavesTerminal ? "all waves terminal; topic closeout is the next goal" : "execution cursor is missing primary_closure_goal"),
394
- check("forbidden_shortcuts_present", REQUIRED_STOP_KEYS.every((key) => (loaded.topic.forbidden_shortcuts ?? []).includes(key)) && REQUIRED_STOP_KEYS.every((key) => forbiddenCatalog.keys.includes(key)), "topic forbidden_shortcuts include required package catalog keys"),
395
- check("forbidden_shortcuts_catalog_aligned", forbiddenCatalog.aligned, forbiddenCatalog.aligned ? "host forbidden-shortcuts projection is aligned or absent" : "host forbidden-shortcuts projection differs from package catalog"),
396
- check("required_artifacts_present", missing.length === 0, missing.length === 0 ? "all required artifacts are present" : `missing artifacts: ${missing.join(", ")}`),
397
- check("no_unresolved_placeholders", sourceArtifactTexts.every(({ text }) => !hasUnresolvedPlaceholder(text)), "required artifacts contain no frozen placeholder markers"),
398
- check("stop_line_declared", hasSectionBullet(textByRef.get("preflight.md") ?? "", "Stop Line"), "preflight.md declares Stop Line bullets"),
399
- check("human_gates_declared", humanGates.length > 0, humanGates.length > 0 ? "preflight.md declares Human Gates bullets" : "preflight.md does not declare Human Gates bullets"),
400
- check("validation_commands_declared", commands.some((command) => command.includes("topic validate ")) && commands.some((command) => command.includes("topic validate graph")), "validation command evidence includes topic validate and topic validate graph"),
401
- check("closeout_criteria_declared", /Wave-1 Closeout Requirements/i.test(textByRef.get("closeout.md") ?? "") && /\bcomplete\b/i.test(textByRef.get("closeout.md") ?? "") && /\bpartial\b/i.test(textByRef.get("closeout.md") ?? "") && (/\bblocked\b/i.test(textByRef.get("closeout.md") ?? "") || /Non-Closure Conditions/i.test(textByRef.get("closeout.md") ?? "") || /Do not close if/i.test(textByRef.get("closeout.md") ?? "")) && /\bpending\b/i.test(textByRef.get("closeout.md") ?? ""), "closeout.md declares selected-wave criteria and complete/partial/blocked/pending states"),
402
- check("authority_owner_declared", sectionText(textByRef.get("preflight.md") ?? "", "Authority Owner").length > 0, "preflight.md declares Authority Owner"),
403
- check("work_type_declared", sectionText(textByRef.get("preflight.md") ?? "", "Work Type").length > 0, "preflight.md declares Work Type"),
404
- check("authority_change_admitted", !/authority ownership changes? (?:are )?in scope/i.test(textByRef.get("preflight.md") ?? "") || /\.nimi\/spec\//.test(textByRef.get("preflight.md") ?? ""), "authority-changing work is either out of scope or cites .nimi/spec alignment"),
405
- check("host_projection_aligned", hostProjection.ok, hostProjection.message),
406
- ];
407
-
408
- const preliminaryOk = checks.every((entry) => entry.status === "pass");
409
- const preliminaryGoal = preliminaryOk ? buildGoalCommand(loaded.topicId, executionStartWave, sourceArtifacts) : null;
410
- checks.push(check("goal_size_within_limit", preliminaryGoal === null || preliminaryGoal.length <= GOAL_COMMAND_MAX_CHARS, preliminaryGoal === null ? "goal size check skipped until readiness passes" : `goal command length is ${preliminaryGoal.length}`));
411
-
412
- const readinessOk = checks.every((entry) => entry.status === "pass");
413
- const goalCommand = readinessOk ? preliminaryGoal : null;
414
- return {
415
- ok: readinessOk,
416
- topic_id: loaded.topicId,
417
- topic_ref: projectRef(projectRoot, loaded.topicDir),
418
- topic_state: loaded.topic.state ?? null,
419
- true_close_status: loaded.topic.current_true_close_status ?? null,
420
- profile: profile.profile ?? null,
421
- selected_next_target: loaded.topic.selected_next_target ?? null,
422
- selected_wave_id: executionStartWave?.wave_id ?? null,
423
- execution_start_wave_id: executionStartWave?.wave_id ?? null,
424
- topic_state_hash: buildStateHash(sourceArtifactTexts, [
425
- { ref: TOPIC_GOAL_CONTRACT_REF, text: await readTextIfFile(packagePath(TOPIC_GOAL_CONTRACT_REF)) ?? "" },
426
- ]),
427
- readiness: {
428
- ok: readinessOk,
429
- checks,
430
- },
431
- goal_command: goalCommand,
432
- source_artifacts: sourceArtifacts,
433
- validation_commands: validationCommands,
434
- human_gates: humanGates,
435
- refusal_reasons: checks
436
- .filter((entry) => entry.status === "fail")
437
- .map((entry) => CHECK_REASON[entry.id])
438
- .filter(Boolean),
439
- };
440
- }