@warpgogol/forge 2.8.0 → 2.8.2

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 (271) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +23 -13
  3. package/README.uk.md +19 -12
  4. package/dist/os/session/handlers/validate.d.ts.map +1 -1
  5. package/dist/os/session/handlers/validate.js +23 -1
  6. package/dist/os/session/handlers/validate.js.map +1 -1
  7. package/dist/os/session/types.d.ts +31 -1
  8. package/dist/os/session/types.d.ts.map +1 -1
  9. package/dist/os/session/types.js +7 -0
  10. package/dist/os/session/types.js.map +1 -1
  11. package/os/adr/adr.module.ts +158 -0
  12. package/os/adr/frontmatter-io.ts +80 -0
  13. package/os/adr/handlers/archive.ts +244 -0
  14. package/os/adr/handlers/implement-stamp.ts +331 -0
  15. package/os/adr/handlers/list-create.ts +207 -0
  16. package/os/adr/handlers/validate.test.ts +232 -0
  17. package/os/adr/handlers/validate.ts +429 -0
  18. package/os/adr/index.ts +33 -0
  19. package/os/adr/types.ts +141 -0
  20. package/os/audit/audit.module.ts +48 -0
  21. package/os/audit/frontmatter-io.ts +84 -0
  22. package/os/audit/handlers/archive.ts +239 -0
  23. package/os/audit/index.ts +23 -0
  24. package/os/audit/types.ts +29 -0
  25. package/os/compass/compass.module.ts +186 -0
  26. package/os/compass/handlers/compass-audit-handler.ts +382 -0
  27. package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
  28. package/os/compass/handlers/compass-inventory-handler.ts +294 -0
  29. package/os/compass/handlers/compass-inventory.ts +521 -0
  30. package/os/compass/handlers/git-revision.ts +137 -0
  31. package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
  32. package/os/compass/handlers/resolve-scan-root.ts +79 -0
  33. package/os/compass/index.ts +33 -0
  34. package/os/core/core.module.ts +948 -0
  35. package/os/core/handlers/assets-check.ts +122 -0
  36. package/os/core/handlers/assets-helpers.ts +174 -0
  37. package/os/core/handlers/assets-list.ts +99 -0
  38. package/os/core/handlers/build.ts +155 -0
  39. package/os/core/handlers/determinism-check.ts +354 -0
  40. package/os/core/handlers/dev.ts +127 -0
  41. package/os/core/handlers/invariant-engine.test.ts +681 -0
  42. package/os/core/handlers/knowledge-compact.ts +248 -0
  43. package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
  44. package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
  45. package/os/core/handlers/note-link-validate.test.ts +117 -0
  46. package/os/core/handlers/note-orphan-detect.test.ts +94 -0
  47. package/os/core/handlers/package-health.test.ts +226 -0
  48. package/os/core/handlers/package-health.ts +231 -0
  49. package/os/core/handlers/pinned-check.ts +200 -0
  50. package/os/core/handlers/pinned-init.ts +333 -0
  51. package/os/core/handlers/pinned-types.ts +50 -0
  52. package/os/core/handlers/pinned-validate.ts +301 -0
  53. package/os/core/handlers/profile-resolve.ts +92 -0
  54. package/os/core/handlers/release-prepare.ts +294 -0
  55. package/os/core/handlers/release-publish.ts +211 -0
  56. package/os/core/handlers/validate.ts +262 -0
  57. package/os/core/handlers/workspace-deps.ts +55 -0
  58. package/os/core/index.ts +13 -0
  59. package/os/exploration/exploration.module.ts +90 -0
  60. package/os/exploration/frontmatter-io.ts +77 -0
  61. package/os/exploration/handlers/archive.ts +159 -0
  62. package/os/exploration/handlers/list.ts +77 -0
  63. package/os/exploration/handlers/show.ts +107 -0
  64. package/os/exploration/index.ts +25 -0
  65. package/os/exploration/types.ts +65 -0
  66. package/os/mission/handlers/archive.test.ts +391 -0
  67. package/os/mission/handlers/archive.ts +453 -0
  68. package/os/mission/index.ts +1 -0
  69. package/os/mission/mission.module.ts +48 -0
  70. package/os/mission/types.ts +41 -0
  71. package/os/naming/index.ts +14 -0
  72. package/os/naming/naming-convention.test.ts +163 -0
  73. package/os/naming/naming-convention.ts +378 -0
  74. package/os/naming/naming.module.ts +36 -0
  75. package/os/notes/index.ts +10 -0
  76. package/os/notes/notes.module.ts +109 -0
  77. package/os/plan/frontmatter-io.ts +82 -0
  78. package/os/plan/handlers/archive.ts +238 -0
  79. package/os/plan/index.ts +23 -0
  80. package/os/plan/plan.module.ts +48 -0
  81. package/os/plan/types.ts +29 -0
  82. package/os/program/discovery.ts +294 -0
  83. package/os/program/handlers/complete.ts +419 -0
  84. package/os/program/handlers/lease.ts +433 -0
  85. package/os/program/handlers/seal.ts +332 -0
  86. package/os/program/handlers/validate.ts +213 -0
  87. package/os/program/lease.ts +156 -0
  88. package/os/program/program.module.ts +212 -0
  89. package/os/program/schemas.ts +218 -0
  90. package/os/program/state.ts +264 -0
  91. package/os/rfc/acceptance.ts +344 -0
  92. package/os/rfc/decision-log.ts +310 -0
  93. package/os/rfc/dna-trace.ts +280 -0
  94. package/os/rfc/frontmatter-io.test.ts +132 -0
  95. package/os/rfc/frontmatter-io.ts +130 -0
  96. package/os/rfc/handlers/archive.ts +300 -0
  97. package/os/rfc/handlers/check.ts +238 -0
  98. package/os/rfc/handlers/implement-stamp.ts +515 -0
  99. package/os/rfc/handlers/index-graph.ts +237 -0
  100. package/os/rfc/handlers/lifecycle.test.ts +127 -0
  101. package/os/rfc/handlers/lifecycle.ts +249 -0
  102. package/os/rfc/handlers/list-create.ts +296 -0
  103. package/os/rfc/handlers/pipeline-status.ts +173 -0
  104. package/os/rfc/handlers/shared.ts +121 -0
  105. package/os/rfc/handlers/supersede-propose.ts +241 -0
  106. package/os/rfc/handlers/validate-rules.test.ts +891 -0
  107. package/os/rfc/handlers/validate-rules.ts +984 -0
  108. package/os/rfc/handlers/validate.ts +166 -0
  109. package/os/rfc/handlers.ts +19 -0
  110. package/os/rfc/index.ts +97 -0
  111. package/os/rfc/rfc.module.ts +416 -0
  112. package/os/rfc/types.ts +596 -0
  113. package/os/rfc/verification-evidence.ts +244 -0
  114. package/os/session/atif-parser.ts +166 -0
  115. package/os/session/frontmatter-io.ts +163 -0
  116. package/os/session/handlers/archive.ts +288 -0
  117. package/os/session/handlers/list.ts +174 -0
  118. package/os/session/handlers/save.ts +348 -0
  119. package/os/session/handlers/validate.ts +250 -0
  120. package/os/session/index.ts +50 -0
  121. package/os/session/session.module.ts +126 -0
  122. package/os/session/types.ts +203 -0
  123. package/os/spec/live-spec-list-show-validate.test.ts +257 -0
  124. package/os/spec/live-spec-list.ts +78 -0
  125. package/os/spec/live-spec-merge.test.ts +261 -0
  126. package/os/spec/live-spec-merge.ts +413 -0
  127. package/os/spec/live-spec-show.ts +117 -0
  128. package/os/spec/live-spec-types.ts +93 -0
  129. package/os/spec/live-spec-validate.ts +170 -0
  130. package/os/spec/spec-materialize.ts +390 -0
  131. package/os/spec/spec-schema.ts +174 -0
  132. package/os/spec/spec-status.ts +284 -0
  133. package/os/spec/spec-validate.ts +513 -0
  134. package/os/spec/spec.module.ts +135 -0
  135. package/os/werkstatt/handlers/lock.ts +168 -0
  136. package/os/werkstatt/handlers/schema.ts +47 -0
  137. package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
  138. package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
  139. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
  140. package/os/werkstatt/index.ts +39 -0
  141. package/os/werkstatt/werkstatt.module.ts +62 -0
  142. package/os/workflow/handlers.ts +304 -0
  143. package/os/workflow/index.ts +28 -0
  144. package/os/workflow/types.ts +99 -0
  145. package/os/workflow/workflow.module.ts +54 -0
  146. package/package.json +3 -3
  147. package/profiles/godot-csharp.yaml +4 -4
  148. package/profiles/knowledge-typescript-turborepo.yaml +110 -0
  149. package/profiles/phaser-turborepo.yaml +5 -5
  150. package/skills/_shared/fo-session-summary.md +132 -16
  151. package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
  152. package/skills/fo/fo-doc-audit/SKILL.md +6 -6
  153. package/skills/fo/fo-handoff/SKILL.md +5 -0
  154. package/skills/fo/fo-idea/SKILL.md +1 -1
  155. package/skills/fo/fo-review/SKILL.md +1 -1
  156. package/skills/fo/fo-session-retro/SKILL.md +8 -0
  157. package/skills/fo/fo-session-save/SKILL.md +8 -0
  158. package/src/cli-output.ts +59 -0
  159. package/src/config/__tests__/resolve-terminology.test.ts +45 -0
  160. package/src/config/forge-config.ts +524 -0
  161. package/src/forge-module.ts +39 -0
  162. package/src/index.ts +172 -0
  163. package/src/knowledge/__tests__/promote.test.ts +219 -0
  164. package/src/knowledge/__tests__/serialize.test.ts +192 -0
  165. package/src/knowledge/budgets.ts +202 -0
  166. package/src/knowledge/compact.ts +405 -0
  167. package/src/knowledge/index.ts +53 -0
  168. package/src/knowledge/parse.ts +227 -0
  169. package/src/knowledge/promote.ts +156 -0
  170. package/src/knowledge/schema.ts +112 -0
  171. package/src/knowledge/serialize.ts +72 -0
  172. package/src/migration-adapters/__tests__/ignored-files.test.ts +51 -0
  173. package/src/migration-adapters/__tests__/registry.test.ts +131 -0
  174. package/src/migration-adapters/__tests__/types.test.ts +86 -0
  175. package/src/migration-adapters/git-utils.ts +71 -0
  176. package/src/migration-adapters/ignored-files.ts +221 -0
  177. package/src/migration-adapters/index.ts +21 -0
  178. package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
  179. package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
  180. package/src/migration-adapters/registry.ts +61 -0
  181. package/src/migration-adapters/types.ts +68 -0
  182. package/src/onboarding/__tests__/workspace-discovery.test.ts +134 -0
  183. package/src/onboarding/agents-generate.ts +443 -0
  184. package/src/onboarding/create.ts +427 -0
  185. package/src/onboarding/doctor.ts +1297 -0
  186. package/src/onboarding/init.ts +337 -0
  187. package/src/onboarding/invariant-engine.ts +448 -0
  188. package/src/onboarding/memory-scaffold.ts +189 -0
  189. package/src/onboarding/nested-agents-generate.ts +93 -0
  190. package/src/onboarding/nested-agents-templates.ts +233 -0
  191. package/src/onboarding/profile-validate.ts +139 -0
  192. package/src/onboarding/scaffold-project.ts +299 -0
  193. package/src/onboarding/scaffold.ts +143 -0
  194. package/src/onboarding/upgrade.ts +558 -0
  195. package/src/onboarding/workspace-discovery.ts +167 -0
  196. package/src/profiles/__tests__/profile-schema.test.ts +228 -0
  197. package/src/profiles/__tests__/stack-profile.test.ts +190 -0
  198. package/src/profiles/__tests__/terminology-utils.test.ts +71 -0
  199. package/src/profiles/profile-schema.ts +350 -0
  200. package/src/profiles/stack-profile.ts +188 -0
  201. package/src/profiles/terminology-utils.ts +36 -0
  202. package/src/registry.ts +185 -0
  203. package/src/skill-schema.ts +37 -0
  204. package/src/tests/acceptance-criteria.test.ts +99 -0
  205. package/src/tests/adr-implement-stamp.test.ts +368 -0
  206. package/src/tests/agents-generate-domain.test.ts +317 -0
  207. package/src/tests/agents-generate.test.ts +458 -0
  208. package/src/tests/bindings-schema.test.ts +314 -0
  209. package/src/tests/budgets.test.ts +213 -0
  210. package/src/tests/cli-output.test.ts +196 -0
  211. package/src/tests/compact.test.ts +796 -0
  212. package/src/tests/create.test.ts +289 -0
  213. package/src/tests/doctor-autonomy.test.ts +98 -0
  214. package/src/tests/doctor-bindings.test.ts +286 -0
  215. package/src/tests/doctor-domain.test.ts +113 -0
  216. package/src/tests/exploration-handlers.test.ts +256 -0
  217. package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
  218. package/src/tests/forge-config.test.ts +420 -0
  219. package/src/tests/fs-atomic.test.ts +96 -0
  220. package/src/tests/fs.test.ts +103 -0
  221. package/src/tests/generated-marker.test.ts +161 -0
  222. package/src/tests/hash.test.ts +37 -0
  223. package/src/tests/implement-stamp.test.ts +505 -0
  224. package/src/tests/init-bindings.test.ts +183 -0
  225. package/src/tests/knowledge-parse.test.ts +307 -0
  226. package/src/tests/knowledge-pbt.test.ts +164 -0
  227. package/src/tests/memory-scaffold.test.ts +192 -0
  228. package/src/tests/migration-adapters.test.ts +480 -0
  229. package/src/tests/package-files.test.ts +54 -0
  230. package/src/tests/pinned-check.test.ts +194 -0
  231. package/src/tests/pinned-init.test.ts +126 -0
  232. package/src/tests/profile-schema.test.ts +194 -0
  233. package/src/tests/profile-validate.test.ts +120 -0
  234. package/src/tests/program-lease.test.ts +245 -0
  235. package/src/tests/program-paths-extended.test.ts +117 -0
  236. package/src/tests/program-paths.test.ts +98 -0
  237. package/src/tests/program-property.test.ts +139 -0
  238. package/src/tests/program-schemas.test.ts +445 -0
  239. package/src/tests/program-spec-node.test.ts +250 -0
  240. package/src/tests/promote.test.ts +481 -0
  241. package/src/tests/registry.test.ts +73 -0
  242. package/src/tests/scaffold-project.test.ts +150 -0
  243. package/src/tests/session-handlers.test.ts +503 -0
  244. package/src/tests/session-pbt.test.ts +190 -0
  245. package/src/tests/skill-schema.test.ts +146 -0
  246. package/src/tests/skill-validate-knowledge.test.ts +26 -0
  247. package/src/tests/skill-validate.test.ts +254 -0
  248. package/src/tests/stack-profile.test.ts +221 -0
  249. package/src/tests/string-utils.test.ts +42 -0
  250. package/src/tests/upgrade.test.ts +428 -0
  251. package/src/tests/werkstatt-lock.test.ts +343 -0
  252. package/src/tests/workspace-discovery-domain.test.ts +112 -0
  253. package/src/tests/workspace-discovery.test.ts +135 -0
  254. package/src/types.ts +221 -0
  255. package/src/utils/__tests__/fs-idempotent.test.ts +73 -0
  256. package/src/utils/__tests__/fs-trash.test.ts +45 -0
  257. package/src/utils/fs-atomic.ts +93 -0
  258. package/src/utils/fs-idempotent.ts +41 -0
  259. package/src/utils/fs-trash-sync.ts +37 -0
  260. package/src/utils/fs-trash.ts +24 -0
  261. package/src/utils/fs.ts +74 -0
  262. package/src/utils/generated-marker.ts +166 -0
  263. package/src/utils/hash.ts +19 -0
  264. package/src/utils/index.ts +29 -0
  265. package/src/utils/string-utils.ts +19 -0
  266. package/src/validators/__tests__/note-orphan-detect.test.ts +94 -0
  267. package/src/validators/note-frontmatter-validate.ts +142 -0
  268. package/src/validators/note-link-validate.ts +145 -0
  269. package/src/validators/note-orphan-detect.ts +146 -0
  270. package/src/validators/port-validate.ts +97 -0
  271. package/src/validators/skill-validate.ts +831 -0
@@ -0,0 +1,390 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>spec.materialize handler — scaffolds RFC files for front nodes of a spec roadmap (RFC-0396).</purpose>
4
+ <non-goals>
5
+ <item>Do not fill RFC content — the agent fills sections during the fo pipeline.</item>
6
+ <item>Do not bulk-materialize past the front or above the --next cap.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>RFC-0396: initial spec.materialize handler — front computation, scaffold via rfc.create, materializedAs write-back.</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ import fs from "node:fs/promises";
15
+ import { existsSync } from "node:fs";
16
+ import path from "node:path";
17
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
18
+ import { toKebabCase } from "../../src/utils/string-utils.ts";
19
+ import { loadForgeConfig } from "../../src/config/forge-config.ts";
20
+ import { resolveRfcTemplate } from "../rfc/handlers/shared.ts";
21
+ import type {
22
+ ForgeCommandInput,
23
+ ForgeCommandResult,
24
+ ForgeRuntimeContext,
25
+ } from "../../src/types.ts";
26
+ import {
27
+ forgeSpecSchema,
28
+ specAmendmentSchema,
29
+ type ForgeSpec,
30
+ type SpecRfcNode,
31
+ type SpecAmendment,
32
+ resolveAmendedNode,
33
+ } from "./spec-schema.ts";
34
+ import { listRfcFiles } from "../rfc/frontmatter-io.ts";
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Types
38
+ // ---------------------------------------------------------------------------
39
+
40
+ interface MaterializedNode {
41
+ node: string;
42
+ rfc: string;
43
+ file: string;
44
+ }
45
+
46
+ interface SkippedNode {
47
+ node: string;
48
+ reason: string;
49
+ }
50
+
51
+ interface SpecMaterializeResult {
52
+ command: "spec.materialize";
53
+ status: "pass" | "fail";
54
+ spec: string;
55
+ created: MaterializedNode[];
56
+ skipped: SkippedNode[];
57
+ front: string[];
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Helpers
62
+ // ---------------------------------------------------------------------------
63
+
64
+ async function readYaml<T>(filePath: string): Promise<T | null> {
65
+ try {
66
+ const content = await fs.readFile(filePath, "utf8");
67
+ return parseYaml(content) as T;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ function toIsoDate(d: Date): string {
74
+ return d.toISOString().slice(0, 10);
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Main handler
79
+ // ---------------------------------------------------------------------------
80
+
81
+ export async function runSpecMaterialize(
82
+ input: ForgeCommandInput,
83
+ context: ForgeRuntimeContext,
84
+ ): Promise<ForgeCommandResult<SpecMaterializeResult>> {
85
+ const { workspaceRoot, logger, outputFormat } = context;
86
+
87
+ const specId = input.flags["spec"] as string | undefined;
88
+ if (!specId) {
89
+ return {
90
+ data: {
91
+ command: "spec.materialize",
92
+ status: "fail",
93
+ spec: "",
94
+ created: [],
95
+ skipped: [],
96
+ front: [],
97
+ },
98
+ exitCode: 1,
99
+ summary: "spec.materialize: --spec=<id> is required",
100
+ };
101
+ }
102
+
103
+ let specsDir = "docs/specs";
104
+ try {
105
+ const config = loadForgeConfig(workspaceRoot);
106
+ specsDir = config.paths.specsDir;
107
+ } catch {
108
+ // Use default
109
+ }
110
+
111
+ const specDir = path.join(workspaceRoot, specsDir, specId);
112
+ const forgeSpecPath = path.join(specDir, "forge-spec.yaml");
113
+ const rawSpec = await readYaml<unknown>(forgeSpecPath);
114
+ if (!rawSpec) {
115
+ return {
116
+ data: {
117
+ command: "spec.materialize",
118
+ status: "fail",
119
+ spec: specId,
120
+ created: [],
121
+ skipped: [],
122
+ front: [],
123
+ },
124
+ exitCode: 1,
125
+ summary: `spec.materialize: forge-spec.yaml not found for spec '${specId}'`,
126
+ };
127
+ }
128
+
129
+ const parsed = forgeSpecSchema.safeParse(rawSpec);
130
+ if (!parsed.success) {
131
+ return {
132
+ data: {
133
+ command: "spec.materialize",
134
+ status: "fail",
135
+ spec: specId,
136
+ created: [],
137
+ skipped: [],
138
+ front: [],
139
+ },
140
+ exitCode: 1,
141
+ summary: `spec.materialize: forge-spec.yaml schema violation for spec '${specId}'`,
142
+ };
143
+ }
144
+
145
+ const spec: ForgeSpec = parsed.data;
146
+
147
+ if (spec.status !== "accepted") {
148
+ return {
149
+ data: {
150
+ command: "spec.materialize",
151
+ status: "fail",
152
+ spec: specId,
153
+ created: [],
154
+ skipped: [],
155
+ front: [],
156
+ },
157
+ exitCode: 1,
158
+ summary: `spec.materialize: spec '${specId}' is not accepted (status: ${spec.status}). Run /fo-spec-ingest to complete acceptance.`,
159
+ };
160
+ }
161
+
162
+ // Load amendments (RFC-0397)
163
+ const amendmentsDir = path.join(specDir, "amendments");
164
+ const amendments: SpecAmendment[] = [];
165
+ if (existsSync(amendmentsDir)) {
166
+ const amdEntries = await fs.readdir(amendmentsDir);
167
+ for (const entry of amdEntries) {
168
+ if (!entry.endsWith(".md")) continue;
169
+ try {
170
+ const content = await fs.readFile(path.join(amendmentsDir, entry), "utf8");
171
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
172
+ if (!fmMatch) continue;
173
+ const raw = parseYaml(fmMatch[1]) as unknown;
174
+ const amdParsed = specAmendmentSchema.safeParse(raw);
175
+ if (amdParsed.success) {
176
+ amendments.push(amdParsed.data);
177
+ }
178
+ } catch {
179
+ // skip
180
+ }
181
+ }
182
+ }
183
+
184
+ const proposedAmendmentNodes = new Set<string>();
185
+ for (const amendment of amendments) {
186
+ if (amendment.status === "proposed") {
187
+ for (const target of amendment.targets) {
188
+ if (target.kind === "node") proposedAmendmentNodes.add(target.id);
189
+ }
190
+ }
191
+ }
192
+
193
+ const rfcDir = path.join(workspaceRoot, "docs", "rfcs");
194
+ const rfcFiles = await listRfcFiles(rfcDir);
195
+
196
+ // Load RFC statuses
197
+ const rfcStatuses = new Map<string, string>();
198
+ for (const file of rfcFiles) {
199
+ try {
200
+ const content = await fs.readFile(path.join(rfcDir, file), "utf8");
201
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
202
+ if (!fmMatch) continue;
203
+ const fm = parseYaml(fmMatch[1]) as Record<string, unknown>;
204
+ const id = String(fm["id"] ?? "");
205
+ const status = String(fm["status"] ?? "");
206
+ if (id && status) rfcStatuses.set(id, status);
207
+ } catch {
208
+ // skip
209
+ }
210
+ }
211
+
212
+ // Compute node states
213
+ const nodeState = new Map<string, "unmaterialized" | "implemented">();
214
+ for (const node of spec.rfcs) {
215
+ if (node.materializedAs) {
216
+ const status = rfcStatuses.get(node.materializedAs);
217
+ nodeState.set(node.id, status === "implemented" ? "implemented" : "unmaterialized");
218
+ } else {
219
+ nodeState.set(node.id, "unmaterialized");
220
+ }
221
+ }
222
+
223
+ // Compute front: unmaterialized nodes whose deps are all implemented
224
+ function isFront(node: SpecRfcNode): boolean {
225
+ if (node.materializedAs) return false;
226
+ return node.dependsOn.every((dep) => nodeState.get(dep) === "implemented");
227
+ }
228
+
229
+ const frontNodes = spec.rfcs.filter(isFront);
230
+
231
+ // Determine which nodes to materialize
232
+ const explicitNodes = input.flags["nodes"] as string | undefined;
233
+ let toMaterialize: SpecRfcNode[];
234
+
235
+ if (explicitNodes) {
236
+ const requested = explicitNodes.split(",").map((s) => s.trim()).filter(Boolean);
237
+ toMaterialize = [];
238
+ const skipped: SkippedNode[] = [];
239
+ for (const nodeId of requested) {
240
+ const node = spec.rfcs.find((n) => n.id === nodeId);
241
+ if (!node) {
242
+ skipped.push({ node: nodeId, reason: "node not found in spec" });
243
+ } else if (!isFront(node)) {
244
+ const blockers = node.dependsOn.filter((d) => nodeState.get(d) !== "implemented");
245
+ skipped.push({ node: nodeId, reason: `blocked by ${blockers.join(", ")}` });
246
+ } else {
247
+ toMaterialize.push(node);
248
+ }
249
+ }
250
+ if (skipped.length > 0 && toMaterialize.length === 0) {
251
+ return {
252
+ data: {
253
+ command: "spec.materialize",
254
+ status: "fail",
255
+ spec: specId,
256
+ created: [],
257
+ skipped,
258
+ front: frontNodes.map((n) => n.id),
259
+ },
260
+ exitCode: 1,
261
+ summary: `spec.materialize: all requested nodes blocked or not found`,
262
+ };
263
+ }
264
+ } else {
265
+ const nextStr = input.flags["next"] as string | undefined;
266
+ let next = nextStr ? parseInt(nextStr, 10) : 8;
267
+ if (isNaN(next) || next < 1) next = 8;
268
+ if (next > 12) next = 12;
269
+ toMaterialize = frontNodes.slice(0, next);
270
+ }
271
+
272
+ // Find max RFC number
273
+ let maxId = 0;
274
+ for (const file of rfcFiles) {
275
+ const match = path.basename(file).match(/^rfc-(\d{4})/);
276
+ if (match) {
277
+ const num = parseInt(match[1]!, 10);
278
+ if (num > maxId) maxId = num;
279
+ }
280
+ }
281
+
282
+ // Load template via resolveRfcTemplate (project-level first, then forge package fallback)
283
+ const templatePath = resolveRfcTemplate(workspaceRoot);
284
+ let templateContent: string;
285
+ try {
286
+ templateContent = await fs.readFile(templatePath, "utf-8");
287
+ } catch {
288
+ return {
289
+ data: {
290
+ command: "spec.materialize",
291
+ status: "fail",
292
+ spec: specId,
293
+ created: [],
294
+ skipped: [],
295
+ front: frontNodes.map((n) => n.id),
296
+ },
297
+ exitCode: 1,
298
+ summary: `spec.materialize: RFC template not found at ${templatePath}`,
299
+ };
300
+ }
301
+
302
+ const today = toIsoDate(new Date());
303
+ const created: MaterializedNode[] = [];
304
+ const skipped: SkippedNode[] = [];
305
+
306
+ for (const node of toMaterialize) {
307
+ // Use amended node if amendments exist (RFC-0397)
308
+ const effectiveNode = resolveAmendedNode(spec, node.id, amendments);
309
+ const nextNum = maxId + 1;
310
+ const paddedNum = String(nextNum).padStart(4, "0");
311
+ const nextRfcId = `RFC-${paddedNum}`;
312
+ const kebabTitle = toKebabCase(effectiveNode.title);
313
+ const fileName = `rfc-${paddedNum}-${kebabTitle}.md`;
314
+
315
+ // Warn if node has a proposed amendment (RFC-0397)
316
+ if (proposedAmendmentNodes.has(node.id)) {
317
+ logger.warn(`Warning: node ${node.id} has a proposed amendment — node knowledge may be about to change`);
318
+ }
319
+
320
+ let content = templateContent;
321
+ content = content.replace(/^id: RFC-0000$/m, `id: ${nextRfcId}`);
322
+ content = content.replace(/^title: ".*"$/m, `title: "${effectiveNode.title}"`);
323
+ content = content.replace(/^status: draft$/m, `status: draft`);
324
+ content = content.replace(/^kind: \w+$/m, `kind: contract`);
325
+ content = content.replace(/^createdAt: YYYY-MM-DD$/m, `createdAt: ${today}`);
326
+ content = content.replace(/^updatedAt: YYYY-MM-DD$/m, `updatedAt: ${today}`);
327
+ content = content.replace(/^# specRef:$/m, `specRef: "${specId}/${node.id}"`);
328
+ content = content.replace(/^# RFC-0000: .+$/m, `# ${nextRfcId}: ${effectiveNode.title}`);
329
+
330
+ // Add normative source references in the body
331
+ const sourceRefs = effectiveNode.sources.length > 0
332
+ ? effectiveNode.sources.map((s) => `- ${s}`).join("\n")
333
+ : "(none)";
334
+ const designSection = `\n## Design\n\n**Normative source references:**\n${sourceRefs}\n\n_Fill in the implementation design for this repository. Reference vendored snapshot sections — do not copy model content._\n`;
335
+ content = content.replace(/^---\n\n# /m, `---\n${designSection}\n# `);
336
+
337
+ const targetPath = path.join(rfcDir, fileName);
338
+ await fs.writeFile(targetPath, content, "utf-8");
339
+ maxId = nextNum;
340
+
341
+ created.push({
342
+ node: node.id,
343
+ rfc: nextRfcId,
344
+ file: path.join("docs", "rfcs", fileName),
345
+ });
346
+ }
347
+
348
+ // Write back materializedAs into forge-spec.yaml
349
+ if (created.length > 0) {
350
+ const updatedSpec: ForgeSpec = {
351
+ ...spec,
352
+ rfcs: spec.rfcs.map((node) => {
353
+ const created_entry = created.find((c) => c.node === node.id);
354
+ return created_entry ? { ...node, materializedAs: created_entry.rfc } : node;
355
+ }),
356
+ };
357
+ const yamlContent = stringifyYaml(updatedSpec);
358
+ await fs.writeFile(forgeSpecPath, yamlContent, "utf-8");
359
+ }
360
+
361
+ // Report skipped front nodes (beyond the materialized batch)
362
+ const remainingFront = frontNodes
363
+ .filter((n) => !created.some((c) => c.node === n.id))
364
+ .map((n) => n.id);
365
+
366
+ if (outputFormat === "pretty") {
367
+ for (const c of created) {
368
+ logger.success(`Materialized ${c.node} → ${c.rfc} (${c.file})`);
369
+ }
370
+ for (const s of skipped) {
371
+ logger.warn(`Skipped ${s.node}: ${s.reason}`);
372
+ }
373
+ if (remainingFront.length > 0) {
374
+ logger.info(`Remaining front: ${remainingFront.join(", ")}`);
375
+ }
376
+ }
377
+
378
+ return {
379
+ data: {
380
+ command: "spec.materialize",
381
+ status: "pass",
382
+ spec: specId,
383
+ created,
384
+ skipped,
385
+ front: remainingFront,
386
+ },
387
+ exitCode: 0,
388
+ summary: `spec.materialize: created ${created.length} RFC(s) from spec '${specId}'`,
389
+ };
390
+ }
@@ -0,0 +1,174 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Zod schemas and TypeScript types for the forge spec vendoring contract (RFC-0394).</purpose>
4
+ <non-goals>
5
+ <item>Do not implement validation logic — that lives in spec-validate.ts.</item>
6
+ <item>Do not implement materialization or status logic — that is RFC-0396.</item>
7
+ </non-goals>
8
+ </MODULE_CONTRACT>
9
+ <CHANGE_SUMMARY>
10
+ <item>RFC-0394: initial spec schema — forge/spec@1, integrity manifest, decision/rfc/wave types.</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ import { z } from "zod";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Spec decision
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export const specDecisionSchema = z.object({
21
+ id: z.string().min(1),
22
+ title: z.string().min(1),
23
+ status: z.enum(["accepted", "amended", "rejected"]),
24
+ rationale: z.string().min(1),
25
+ promotedTo: z.string().optional(),
26
+ });
27
+
28
+ export type SpecDecision = z.infer<typeof specDecisionSchema>;
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Spec RFC node
32
+ // ---------------------------------------------------------------------------
33
+
34
+ export const specRfcNodeSchema = z.object({
35
+ id: z.string().min(1),
36
+ title: z.string().min(1),
37
+ dependsOn: z.array(z.string()).default([]),
38
+ wave: z.number().int().min(1),
39
+ sources: z.array(z.string()).default([]),
40
+ materializedAs: z.string().optional(),
41
+ });
42
+
43
+ export type SpecRfcNode = z.infer<typeof specRfcNodeSchema>;
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Spec wave
47
+ // ---------------------------------------------------------------------------
48
+
49
+ export const specWaveSchema = z.object({
50
+ id: z.number().int().min(1),
51
+ name: z.string().min(1),
52
+ goal: z.string().min(1),
53
+ });
54
+
55
+ export type SpecWave = z.infer<typeof specWaveSchema>;
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Forge spec (forge/spec@1)
59
+ // ---------------------------------------------------------------------------
60
+
61
+ export const forgeSpecSchema = z.object({
62
+ schema: z.literal("forge/spec@1"),
63
+ id: z.string().min(1),
64
+ title: z.string().min(1),
65
+ version: z.string().min(1),
66
+ status: z.enum(["vendored", "accepted", "superseded"]),
67
+ reviewers: z.array(z.string()).default([]),
68
+ sourceNote: z.string().min(1),
69
+ vendoredAt: z.string().min(1),
70
+ documents: z.record(z.string(), z.string()).default({}),
71
+ decisions: z.array(specDecisionSchema).default([]),
72
+ rfcs: z.array(specRfcNodeSchema).default([]),
73
+ waves: z.array(specWaveSchema).default([]),
74
+ /** Set when this spec supersedes an older version (RFC-0397). */
75
+ supersedes: z.string().optional(),
76
+ });
77
+
78
+ export type ForgeSpec = z.infer<typeof forgeSpecSchema>;
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Integrity manifest (forge/spec-integrity@1)
82
+ // ---------------------------------------------------------------------------
83
+
84
+ export const specIntegritySchema = z.object({
85
+ schema: z.literal("forge/spec-integrity@1"),
86
+ files: z.record(z.string(), z.string()).default({}),
87
+ });
88
+
89
+ export type SpecIntegrity = z.infer<typeof specIntegritySchema>;
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Validation result types
93
+ // ---------------------------------------------------------------------------
94
+
95
+ export interface SpecViolation {
96
+ rule: string;
97
+ message: string;
98
+ }
99
+
100
+ export interface SpecValidateResult {
101
+ command: "spec.validate";
102
+ status: "pass" | "fail";
103
+ specs: Array<{
104
+ id: string;
105
+ status: string;
106
+ violations: SpecViolation[];
107
+ }>;
108
+ }
109
+
110
+ // ---------------------------------------------------------------------------
111
+ // Spec amendment (RFC-0397)
112
+ // ---------------------------------------------------------------------------
113
+
114
+ export const specAmendmentTargetSchema = z.union([
115
+ z.object({
116
+ kind: z.literal("section"),
117
+ document: z.string().min(1),
118
+ anchor: z.string().min(1),
119
+ }),
120
+ z.object({
121
+ kind: z.literal("decision"),
122
+ id: z.string().min(1),
123
+ }),
124
+ z.object({
125
+ kind: z.literal("node"),
126
+ id: z.string().min(1),
127
+ }),
128
+ ]);
129
+
130
+ export type SpecAmendmentTarget = z.infer<typeof specAmendmentTargetSchema>;
131
+
132
+ export const specAmendmentSchema = z.object({
133
+ schema: z.literal("forge/spec-amendment@1"),
134
+ id: z.string().min(1),
135
+ title: z.string().min(1),
136
+ status: z.enum(["proposed", "accepted", "rejected"]),
137
+ createdAt: z.string().min(1),
138
+ reviewers: z.array(z.string()).default([]),
139
+ targets: z.array(specAmendmentTargetSchema).min(1),
140
+ discoveredBy: z.string().min(1),
141
+ });
142
+
143
+ export type SpecAmendment = z.infer<typeof specAmendmentSchema>;
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // resolveAmendedNode — apply accepted amendments to a roadmap node
147
+ // ---------------------------------------------------------------------------
148
+
149
+ export function resolveAmendedNode(
150
+ spec: ForgeSpec,
151
+ nodeId: string,
152
+ amendments: SpecAmendment[],
153
+ ): SpecRfcNode {
154
+ const node = spec.rfcs.find((n) => n.id === nodeId);
155
+ if (!node) throw new Error(`Node not found: ${nodeId}`);
156
+
157
+ const acceptedAmendments = amendments.filter(
158
+ (a) => a.status === "accepted" && a.targets.some((t) => t.kind === "node" && t.id === nodeId),
159
+ );
160
+
161
+ if (acceptedAmendments.length === 0) return node;
162
+
163
+ // Apply amendments — later amendments override earlier ones
164
+ let resolved = { ...node };
165
+ for (const amendment of acceptedAmendments) {
166
+ // Amendments compose at read time; the node's title gets an annotation
167
+ resolved = {
168
+ ...resolved,
169
+ title: `${resolved.title} (as amended by ${amendment.id})`,
170
+ };
171
+ }
172
+
173
+ return resolved;
174
+ }