@nimiplatform/nimi-coding 0.2.8 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/CHANGELOG.md +10 -90
  2. package/CONTRIBUTING.md +5 -34
  3. package/README.md +47 -350
  4. package/README.zh-CN.md +33 -287
  5. package/cli/commands/start.mjs +48 -688
  6. package/cli/commands/validate-spec-audit.mjs +1 -5
  7. package/cli/commands/validate-spec-governance.mjs +20 -0
  8. package/cli/constants.mjs +9 -417
  9. package/cli/help.mjs +28 -145
  10. package/cli/index.mjs +5 -33
  11. package/cli/lib/blueprint-audit.mjs +5 -12
  12. package/cli/lib/bootstrap.mjs +0 -9
  13. package/cli/lib/contracts.mjs +0 -163
  14. package/cli/lib/doctor.mjs +155 -10
  15. package/cli/lib/entrypoints.mjs +63 -167
  16. package/cli/lib/internal/contracts-loaders.mjs +0 -85
  17. package/cli/lib/internal/contracts-parse.mjs +84 -520
  18. package/cli/lib/internal/governance/ai/check-agents-freshness.mjs +13 -8
  19. package/cli/lib/internal/surface-taxonomy-validators.mjs +316 -66
  20. package/cli/lib/internal/validators-shared.mjs +0 -16
  21. package/cli/lib/internal/validators-spec-helpers.mjs +7 -140
  22. package/cli/lib/internal/validators-spec.mjs +103 -342
  23. package/cli/lib/shared.mjs +1 -42
  24. package/cli/lib/validators.mjs +0 -48
  25. package/cli/seeds/seed-policy.yaml +0 -3
  26. package/config/bootstrap.yaml +4 -4
  27. package/config/spec-generation-inputs.yaml +6 -12
  28. package/contracts/domain-admission.schema.yaml +3 -3
  29. package/contracts/migration-inventory.schema.yaml +12 -75
  30. package/contracts/negative-fixtures.yaml +19 -99
  31. package/contracts/placement-contract.schema.yaml +49 -78
  32. package/contracts/projection-edge.schema.yaml +37 -121
  33. package/contracts/shared-enums.yaml +4 -8
  34. package/contracts/spec-generation-inputs.schema.yaml +15 -112
  35. package/contracts/spec-layout.schema.yaml +32 -0
  36. package/contracts/surface-taxonomy.schema.yaml +43 -94
  37. package/contracts/table-family.schema.yaml +1 -1
  38. package/contracts/tracked-output-admission.schema.yaml +13 -66
  39. package/methodology/core.yaml +33 -25
  40. package/methodology/four-closure-policy.yaml +18 -26
  41. package/methodology/role-separation-policy.yaml +21 -27
  42. package/methodology/spec-reconstruction.yaml +17 -34
  43. package/package.json +2 -4
  44. package/spec/_meta/spec-tree-model.yaml +5 -101
  45. package/spec/product-scope.yaml +25 -57
  46. package/adapters/README.md +0 -25
  47. package/adapters/claude/README.md +0 -89
  48. package/adapters/claude/profile.yaml +0 -70
  49. package/adapters/codex/README.md +0 -53
  50. package/adapters/codex/profile.yaml +0 -78
  51. package/adapters/oh-my-codex/README.md +0 -184
  52. package/adapters/oh-my-codex/profile.yaml +0 -46
  53. package/cli/commands/admit-high-risk-decision.mjs +0 -108
  54. package/cli/commands/audit-sweep.mjs +0 -364
  55. package/cli/commands/closeout.mjs +0 -186
  56. package/cli/commands/decide-high-risk-execution.mjs +0 -124
  57. package/cli/commands/handoff.mjs +0 -123
  58. package/cli/commands/ingest-high-risk-execution.mjs +0 -95
  59. package/cli/commands/review-high-risk-execution.mjs +0 -95
  60. package/cli/commands/sweep-design.mjs +0 -295
  61. package/cli/commands/sweep.mjs +0 -22
  62. package/cli/commands/topic-formatters.mjs +0 -382
  63. package/cli/commands/topic-goal.mjs +0 -33
  64. package/cli/commands/topic-options-shared.mjs +0 -27
  65. package/cli/commands/topic-options-workflow.mjs +0 -767
  66. package/cli/commands/topic-options.mjs +0 -626
  67. package/cli/commands/topic-runner.mjs +0 -169
  68. package/cli/commands/topic.mjs +0 -795
  69. package/cli/commands/validate-acceptance.mjs +0 -5
  70. package/cli/commands/validate-execution-packet.mjs +0 -5
  71. package/cli/commands/validate-orchestration-state.mjs +0 -5
  72. package/cli/commands/validate-prompt.mjs +0 -5
  73. package/cli/commands/validate-worker-output.mjs +0 -5
  74. package/cli/lib/adapter-profiles.mjs +0 -403
  75. package/cli/lib/audit-execution.mjs +0 -52
  76. package/cli/lib/audit-sweep-runtime/admissions.mjs +0 -508
  77. package/cli/lib/audit-sweep-runtime/audit-validity.mjs +0 -356
  78. package/cli/lib/audit-sweep-runtime/chunks.mjs +0 -697
  79. package/cli/lib/audit-sweep-runtime/claude-auditor.mjs +0 -658
  80. package/cli/lib/audit-sweep-runtime/closeout.mjs +0 -144
  81. package/cli/lib/audit-sweep-runtime/codex-auditor-evidence.mjs +0 -654
  82. package/cli/lib/audit-sweep-runtime/codex-auditor.mjs +0 -527
  83. package/cli/lib/audit-sweep-runtime/common.mjs +0 -341
  84. package/cli/lib/audit-sweep-runtime/coverage-quality.mjs +0 -172
  85. package/cli/lib/audit-sweep-runtime/evidence-assignment.mjs +0 -155
  86. package/cli/lib/audit-sweep-runtime/format.mjs +0 -57
  87. package/cli/lib/audit-sweep-runtime/ingest.mjs +0 -486
  88. package/cli/lib/audit-sweep-runtime/inventory-spec-chunks.mjs +0 -425
  89. package/cli/lib/audit-sweep-runtime/inventory.mjs +0 -798
  90. package/cli/lib/audit-sweep-runtime/ledger.mjs +0 -315
  91. package/cli/lib/audit-sweep-runtime/p0p1-profile.mjs +0 -101
  92. package/cli/lib/audit-sweep-runtime/remediation.mjs +0 -349
  93. package/cli/lib/audit-sweep-runtime/rerun.mjs +0 -129
  94. package/cli/lib/audit-sweep-runtime/risk-budget.mjs +0 -300
  95. package/cli/lib/audit-sweep-runtime/status.mjs +0 -62
  96. package/cli/lib/audit-sweep-runtime/validators-ledger.mjs +0 -215
  97. package/cli/lib/audit-sweep-runtime/validators.mjs +0 -797
  98. package/cli/lib/audit-sweep.mjs +0 -19
  99. package/cli/lib/authority-convergence.mjs +0 -720
  100. package/cli/lib/closeout.mjs +0 -732
  101. package/cli/lib/codex-sdk-runner.mjs +0 -76
  102. package/cli/lib/external-execution.mjs +0 -101
  103. package/cli/lib/handoff.mjs +0 -812
  104. package/cli/lib/high-risk-admission.mjs +0 -512
  105. package/cli/lib/high-risk-decision.mjs +0 -353
  106. package/cli/lib/high-risk-ingest.mjs +0 -321
  107. package/cli/lib/high-risk-review.mjs +0 -267
  108. package/cli/lib/internal/contracts-parse-high-risk.mjs +0 -131
  109. package/cli/lib/internal/contracts-validators.mjs +0 -399
  110. package/cli/lib/internal/doctor-bootstrap-surface.mjs +0 -406
  111. package/cli/lib/internal/doctor-delegated-surface.mjs +0 -256
  112. package/cli/lib/internal/doctor-finalize.mjs +0 -383
  113. package/cli/lib/internal/doctor-format.mjs +0 -286
  114. package/cli/lib/internal/doctor-inspectors.mjs +0 -327
  115. package/cli/lib/internal/doctor-state.mjs +0 -205
  116. package/cli/lib/internal/validators-artifacts.mjs +0 -515
  117. package/cli/lib/sweep-design-runtime/common.mjs +0 -246
  118. package/cli/lib/sweep-design-runtime/engine.mjs +0 -733
  119. package/cli/lib/sweep-design-runtime/fix-topic.mjs +0 -414
  120. package/cli/lib/sweep-design-runtime/lifecycle.mjs +0 -54
  121. package/cli/lib/sweep-design-runtime/results.mjs +0 -324
  122. package/cli/lib/sweep-design.mjs +0 -8
  123. package/cli/lib/topic-artifacts.mjs +0 -186
  124. package/cli/lib/topic-authority-coverage.mjs +0 -73
  125. package/cli/lib/topic-closeout.mjs +0 -560
  126. package/cli/lib/topic-common.mjs +0 -404
  127. package/cli/lib/topic-decisions.mjs +0 -332
  128. package/cli/lib/topic-draft-packets.mjs +0 -167
  129. package/cli/lib/topic-execution.mjs +0 -533
  130. package/cli/lib/topic-goal.mjs +0 -440
  131. package/cli/lib/topic-ledger.mjs +0 -281
  132. package/cli/lib/topic-lifecycle-artifacts.mjs +0 -173
  133. package/cli/lib/topic-root-validation.mjs +0 -288
  134. package/cli/lib/topic-runner-commands.mjs +0 -174
  135. package/cli/lib/topic-runner-deferral.mjs +0 -532
  136. package/cli/lib/topic-runner-stale-gates.mjs +0 -114
  137. package/cli/lib/topic-runner-validation.mjs +0 -138
  138. package/cli/lib/topic-runner.mjs +0 -727
  139. package/cli/lib/topic-scaffold.mjs +0 -252
  140. package/cli/lib/topic-waves.mjs +0 -403
  141. package/cli/lib/topic.mjs +0 -81
  142. package/config/audit-execution-artifacts.yaml +0 -20
  143. package/config/external-execution-artifacts.yaml +0 -16
  144. package/config/host-adapter.yaml +0 -30
  145. package/config/host-profile.yaml +0 -29
  146. package/config/installer-evidence.yaml +0 -31
  147. package/config/skill-installer.yaml +0 -23
  148. package/config/skill-manifest.yaml +0 -48
  149. package/config/skills.yaml +0 -30
  150. package/contracts/acceptance.schema.yaml +0 -16
  151. package/contracts/admission-checklist.schema.yaml +0 -15
  152. package/contracts/audit-chunk.schema.yaml +0 -123
  153. package/contracts/audit-closeout.schema.yaml +0 -51
  154. package/contracts/audit-finding.schema.yaml +0 -61
  155. package/contracts/audit-ledger.schema.yaml +0 -138
  156. package/contracts/audit-plan.schema.yaml +0 -138
  157. package/contracts/audit-remediation-map.schema.yaml +0 -52
  158. package/contracts/audit-rerun.schema.yaml +0 -31
  159. package/contracts/audit-sweep-result.yaml +0 -53
  160. package/contracts/authority-convergence-audit.schema.yaml +0 -19
  161. package/contracts/closeout.schema.yaml +0 -25
  162. package/contracts/decision-review.schema.yaml +0 -16
  163. package/contracts/doc-spec-audit-result.yaml +0 -19
  164. package/contracts/execution-packet.schema.yaml +0 -49
  165. package/contracts/external-host-compatibility.yaml +0 -22
  166. package/contracts/forbidden-shortcuts.catalog.yaml +0 -23
  167. package/contracts/high-risk-admission.schema.yaml +0 -23
  168. package/contracts/high-risk-execution-result.yaml +0 -20
  169. package/contracts/orchestration-state.schema.yaml +0 -41
  170. package/contracts/overflow-continuation.schema.yaml +0 -12
  171. package/contracts/packet.schema.yaml +0 -30
  172. package/contracts/pending-note.schema.yaml +0 -17
  173. package/contracts/prompt.schema.yaml +0 -12
  174. package/contracts/remediation.schema.yaml +0 -16
  175. package/contracts/result.schema.yaml +0 -24
  176. package/contracts/spec-reconstruction-result.yaml +0 -41
  177. package/contracts/sweep-design-result.yaml +0 -349
  178. package/contracts/topic-goal.schema.yaml +0 -87
  179. package/contracts/topic-run-ledger.schema.yaml +0 -72
  180. package/contracts/topic-step-decision.schema.yaml +0 -45
  181. package/contracts/topic.schema.yaml +0 -65
  182. package/contracts/true-close.schema.yaml +0 -15
  183. package/contracts/wave.schema.yaml +0 -29
  184. package/contracts/worker-output.schema.yaml +0 -15
  185. package/contracts/workflow-consumer.schema.yaml +0 -110
  186. package/methodology/audit-sweep-p0p1-recall.yaml +0 -45
  187. package/methodology/authority-convergence-policy.yaml +0 -42
  188. package/methodology/overflow-continuation-policy.yaml +0 -14
  189. package/methodology/skill-exchange-projection.yaml +0 -114
  190. package/methodology/skill-handoff.yaml +0 -34
  191. package/methodology/skill-installer-result.yaml +0 -27
  192. package/methodology/skill-installer-summary-projection.yaml +0 -181
  193. package/methodology/skill-runtime.yaml +0 -23
  194. package/methodology/spec-target-truth-profile.yaml +0 -46
  195. package/methodology/topic-lifecycle-report.yaml +0 -144
  196. package/methodology/topic-lifecycle.yaml +0 -37
  197. package/methodology/topic-naming-ontology.yaml +0 -21
  198. package/methodology/topic-ontology.yaml +0 -38
  199. package/methodology/topic-validation-policy.yaml +0 -9
  200. package/methodology/wave-dag-policy.yaml +0 -14
  201. package/spec/_meta/command-gating-matrix.yaml +0 -143
  202. package/spec/_meta/generate-drift-migration-checklist.yaml +0 -137
  203. package/spec/_meta/governance-routing-cutover-checklist.yaml +0 -35
  204. package/spec/_meta/phase2-impacted-surface-matrix.yaml +0 -44
  205. package/spec/_meta/spec-authority-cutover-readiness.yaml +0 -102
  206. package/spec/bootstrap-state.yaml +0 -99
@@ -1,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
- }