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