@warpgogol/forge 2.7.0 → 2.8.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 (268) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +23 -13
  3. package/README.uk.md +19 -12
  4. package/dist/src/config/forge-config.d.ts +1 -1
  5. package/dist/src/config/forge-config.d.ts.map +1 -1
  6. package/dist/src/config/forge-config.js +2 -2
  7. package/dist/src/config/forge-config.js.map +1 -1
  8. package/dist/src/onboarding/agents-generate.d.ts +2 -1
  9. package/dist/src/onboarding/agents-generate.d.ts.map +1 -1
  10. package/dist/src/onboarding/agents-generate.js +24 -4
  11. package/dist/src/onboarding/agents-generate.js.map +1 -1
  12. package/dist/src/profiles/profile-schema.d.ts +2 -0
  13. package/dist/src/profiles/profile-schema.d.ts.map +1 -1
  14. package/dist/src/profiles/profile-schema.js +1 -0
  15. package/dist/src/profiles/profile-schema.js.map +1 -1
  16. package/dist/src/profiles/stack-profile.d.ts +1 -0
  17. package/dist/src/profiles/stack-profile.d.ts.map +1 -1
  18. package/dist/src/profiles/stack-profile.js +2 -0
  19. package/dist/src/profiles/stack-profile.js.map +1 -1
  20. package/os/adr/adr.module.ts +158 -0
  21. package/os/adr/frontmatter-io.ts +80 -0
  22. package/os/adr/handlers/archive.ts +244 -0
  23. package/os/adr/handlers/implement-stamp.ts +331 -0
  24. package/os/adr/handlers/list-create.ts +207 -0
  25. package/os/adr/handlers/validate.test.ts +232 -0
  26. package/os/adr/handlers/validate.ts +429 -0
  27. package/os/adr/index.ts +33 -0
  28. package/os/adr/types.ts +141 -0
  29. package/os/audit/audit.module.ts +48 -0
  30. package/os/audit/frontmatter-io.ts +84 -0
  31. package/os/audit/handlers/archive.ts +239 -0
  32. package/os/audit/index.ts +23 -0
  33. package/os/audit/types.ts +29 -0
  34. package/os/compass/compass.module.ts +186 -0
  35. package/os/compass/handlers/compass-audit-handler.ts +382 -0
  36. package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
  37. package/os/compass/handlers/compass-inventory-handler.ts +294 -0
  38. package/os/compass/handlers/compass-inventory.ts +521 -0
  39. package/os/compass/handlers/git-revision.ts +137 -0
  40. package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
  41. package/os/compass/handlers/resolve-scan-root.ts +79 -0
  42. package/os/compass/index.ts +33 -0
  43. package/os/core/core.module.ts +948 -0
  44. package/os/core/handlers/assets-check.ts +122 -0
  45. package/os/core/handlers/assets-helpers.ts +174 -0
  46. package/os/core/handlers/assets-list.ts +99 -0
  47. package/os/core/handlers/build.ts +155 -0
  48. package/os/core/handlers/determinism-check.ts +354 -0
  49. package/os/core/handlers/dev.ts +127 -0
  50. package/os/core/handlers/invariant-engine.test.ts +681 -0
  51. package/os/core/handlers/knowledge-compact.ts +248 -0
  52. package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
  53. package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
  54. package/os/core/handlers/note-link-validate.test.ts +117 -0
  55. package/os/core/handlers/note-orphan-detect.test.ts +94 -0
  56. package/os/core/handlers/package-health.test.ts +226 -0
  57. package/os/core/handlers/package-health.ts +231 -0
  58. package/os/core/handlers/pinned-check.ts +200 -0
  59. package/os/core/handlers/pinned-init.ts +333 -0
  60. package/os/core/handlers/pinned-types.ts +50 -0
  61. package/os/core/handlers/pinned-validate.ts +301 -0
  62. package/os/core/handlers/profile-resolve.ts +92 -0
  63. package/os/core/handlers/release-prepare.ts +294 -0
  64. package/os/core/handlers/release-publish.ts +211 -0
  65. package/os/core/handlers/validate.ts +262 -0
  66. package/os/core/handlers/workspace-deps.ts +55 -0
  67. package/os/core/index.ts +13 -0
  68. package/os/exploration/exploration.module.ts +90 -0
  69. package/os/exploration/frontmatter-io.ts +77 -0
  70. package/os/exploration/handlers/archive.ts +159 -0
  71. package/os/exploration/handlers/list.ts +77 -0
  72. package/os/exploration/handlers/show.ts +107 -0
  73. package/os/exploration/index.ts +25 -0
  74. package/os/exploration/types.ts +65 -0
  75. package/os/mission/handlers/archive.test.ts +391 -0
  76. package/os/mission/handlers/archive.ts +453 -0
  77. package/os/mission/index.ts +1 -0
  78. package/os/mission/mission.module.ts +48 -0
  79. package/os/mission/types.ts +41 -0
  80. package/os/naming/index.ts +14 -0
  81. package/os/naming/naming-convention.test.ts +163 -0
  82. package/os/naming/naming-convention.ts +378 -0
  83. package/os/naming/naming.module.ts +36 -0
  84. package/os/notes/index.ts +10 -0
  85. package/os/notes/notes.module.ts +109 -0
  86. package/os/plan/frontmatter-io.ts +82 -0
  87. package/os/plan/handlers/archive.ts +238 -0
  88. package/os/plan/index.ts +23 -0
  89. package/os/plan/plan.module.ts +48 -0
  90. package/os/plan/types.ts +29 -0
  91. package/os/program/discovery.ts +294 -0
  92. package/os/program/handlers/complete.ts +419 -0
  93. package/os/program/handlers/lease.ts +433 -0
  94. package/os/program/handlers/seal.ts +332 -0
  95. package/os/program/handlers/validate.ts +213 -0
  96. package/os/program/lease.ts +156 -0
  97. package/os/program/program.module.ts +212 -0
  98. package/os/program/schemas.ts +218 -0
  99. package/os/program/state.ts +264 -0
  100. package/os/rfc/acceptance.ts +344 -0
  101. package/os/rfc/decision-log.ts +310 -0
  102. package/os/rfc/dna-trace.ts +280 -0
  103. package/os/rfc/frontmatter-io.test.ts +132 -0
  104. package/os/rfc/frontmatter-io.ts +130 -0
  105. package/os/rfc/handlers/archive.ts +300 -0
  106. package/os/rfc/handlers/check.ts +238 -0
  107. package/os/rfc/handlers/implement-stamp.ts +515 -0
  108. package/os/rfc/handlers/index-graph.ts +237 -0
  109. package/os/rfc/handlers/lifecycle.test.ts +127 -0
  110. package/os/rfc/handlers/lifecycle.ts +249 -0
  111. package/os/rfc/handlers/list-create.ts +296 -0
  112. package/os/rfc/handlers/pipeline-status.ts +173 -0
  113. package/os/rfc/handlers/shared.ts +121 -0
  114. package/os/rfc/handlers/supersede-propose.ts +241 -0
  115. package/os/rfc/handlers/validate-rules.test.ts +891 -0
  116. package/os/rfc/handlers/validate-rules.ts +984 -0
  117. package/os/rfc/handlers/validate.ts +166 -0
  118. package/os/rfc/handlers.ts +19 -0
  119. package/os/rfc/index.ts +97 -0
  120. package/os/rfc/rfc.module.ts +416 -0
  121. package/os/rfc/types.ts +596 -0
  122. package/os/rfc/verification-evidence.ts +244 -0
  123. package/os/session/atif-parser.ts +166 -0
  124. package/os/session/frontmatter-io.ts +163 -0
  125. package/os/session/handlers/archive.ts +288 -0
  126. package/os/session/handlers/list.ts +174 -0
  127. package/os/session/handlers/save.ts +348 -0
  128. package/os/session/handlers/validate.ts +250 -0
  129. package/os/session/index.ts +50 -0
  130. package/os/session/session.module.ts +126 -0
  131. package/os/session/types.ts +203 -0
  132. package/os/spec/live-spec-list-show-validate.test.ts +257 -0
  133. package/os/spec/live-spec-list.ts +78 -0
  134. package/os/spec/live-spec-merge.test.ts +261 -0
  135. package/os/spec/live-spec-merge.ts +413 -0
  136. package/os/spec/live-spec-show.ts +117 -0
  137. package/os/spec/live-spec-types.ts +93 -0
  138. package/os/spec/live-spec-validate.ts +170 -0
  139. package/os/spec/spec-materialize.ts +390 -0
  140. package/os/spec/spec-schema.ts +174 -0
  141. package/os/spec/spec-status.ts +284 -0
  142. package/os/spec/spec-validate.ts +513 -0
  143. package/os/spec/spec.module.ts +135 -0
  144. package/os/werkstatt/handlers/lock.ts +168 -0
  145. package/os/werkstatt/handlers/schema.ts +47 -0
  146. package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
  147. package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
  148. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
  149. package/os/werkstatt/index.ts +39 -0
  150. package/os/werkstatt/werkstatt.module.ts +62 -0
  151. package/os/workflow/handlers.ts +304 -0
  152. package/os/workflow/index.ts +28 -0
  153. package/os/workflow/types.ts +99 -0
  154. package/os/workflow/workflow.module.ts +54 -0
  155. package/package.json +3 -3
  156. package/profiles/godot-csharp.yaml +5 -4
  157. package/profiles/knowledge-typescript-turborepo.yaml +110 -0
  158. package/profiles/phaser-turborepo.yaml +5 -5
  159. package/profiles/root-agents-godot.md +133 -0
  160. package/skills/_shared/fo-session-summary.md +132 -16
  161. package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
  162. package/skills/fo/fo-doc-audit/SKILL.md +6 -6
  163. package/skills/fo/fo-handoff/SKILL.md +5 -0
  164. package/skills/fo/fo-idea/SKILL.md +1 -1
  165. package/skills/fo/fo-review/SKILL.md +1 -1
  166. package/skills/fo/fo-session-retro/SKILL.md +8 -0
  167. package/skills/fo/fo-session-save/SKILL.md +8 -0
  168. package/src/cli-output.ts +59 -0
  169. package/src/config/forge-config.ts +524 -0
  170. package/src/forge-module.ts +39 -0
  171. package/src/index.ts +172 -0
  172. package/src/knowledge/budgets.ts +202 -0
  173. package/src/knowledge/compact.ts +405 -0
  174. package/src/knowledge/index.ts +53 -0
  175. package/src/knowledge/parse.ts +227 -0
  176. package/src/knowledge/promote.ts +156 -0
  177. package/src/knowledge/schema.ts +112 -0
  178. package/src/knowledge/serialize.ts +72 -0
  179. package/src/migration-adapters/git-utils.ts +71 -0
  180. package/src/migration-adapters/ignored-files.ts +221 -0
  181. package/src/migration-adapters/index.ts +21 -0
  182. package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
  183. package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
  184. package/src/migration-adapters/registry.ts +61 -0
  185. package/src/migration-adapters/types.ts +68 -0
  186. package/src/onboarding/agents-generate.ts +443 -0
  187. package/src/onboarding/create.ts +427 -0
  188. package/src/onboarding/doctor.ts +1297 -0
  189. package/src/onboarding/init.ts +337 -0
  190. package/src/onboarding/invariant-engine.ts +448 -0
  191. package/src/onboarding/memory-scaffold.ts +189 -0
  192. package/src/onboarding/nested-agents-generate.ts +93 -0
  193. package/src/onboarding/nested-agents-templates.ts +233 -0
  194. package/src/onboarding/profile-validate.ts +139 -0
  195. package/src/onboarding/scaffold-project.ts +299 -0
  196. package/src/onboarding/scaffold.ts +143 -0
  197. package/src/onboarding/upgrade.ts +558 -0
  198. package/src/onboarding/workspace-discovery.ts +167 -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 +51 -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 +217 -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/fs-atomic.ts +93 -0
  256. package/src/utils/fs-idempotent.ts +41 -0
  257. package/src/utils/fs-trash-sync.ts +37 -0
  258. package/src/utils/fs-trash.ts +24 -0
  259. package/src/utils/fs.ts +74 -0
  260. package/src/utils/generated-marker.ts +166 -0
  261. package/src/utils/hash.ts +19 -0
  262. package/src/utils/index.ts +29 -0
  263. package/src/utils/string-utils.ts +19 -0
  264. package/src/validators/note-frontmatter-validate.ts +142 -0
  265. package/src/validators/note-link-validate.ts +145 -0
  266. package/src/validators/note-orphan-detect.ts +146 -0
  267. package/src/validators/port-validate.ts +97 -0
  268. package/src/validators/skill-validate.ts +831 -0
@@ -0,0 +1,237 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Maintains packages/os/site-kernel/src/rfc/handlers/index-graph.ts as an authored site-kernel authored module so agents can evolve it without rediscovering local boundaries.</purpose>
4
+ <non-goals>
5
+ <item>Do not introduce app-specific runtime composition or deployment behavior into this reusable package source file.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0303: extracted index and graph handlers from handlers.ts into handlers/index-graph.ts.</item>
10
+ <item>Post-refactor hardening: rfc.graph resolves archived RFC files by basename id.</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ import fs from "node:fs/promises";
15
+ import path from "node:path";
16
+ import { parse as yamlParse, stringify as yamlStringify } from "yaml";
17
+
18
+ import { listRfcFiles, readAndParseRfc, rfcFileMatchesId } from "../frontmatter-io.ts";
19
+ import type {
20
+ ForgeCommandInput,
21
+ ForgeCommandResult,
22
+ ForgeRuntimeContext,
23
+ } from "../../../src/types.ts";
24
+ import type {
25
+ RfcStatus,
26
+ RfcKind,
27
+ RfcIndexEntry,
28
+ RfcIndexResult,
29
+ RfcGraphResult,
30
+ } from "../types.ts";
31
+ import { RFC_DIR } from "../types.ts";
32
+ import { toIsoDate } from "./shared.ts";
33
+
34
+ export async function runRfcIndexGenerate(
35
+ input: ForgeCommandInput,
36
+ context: ForgeRuntimeContext,
37
+ ): Promise<ForgeCommandResult<RfcIndexResult>> {
38
+ const { workspaceRoot, logger, outputFormat } = context;
39
+ const rfcDirPath = path.join(workspaceRoot, RFC_DIR);
40
+
41
+ const files = await listRfcFiles(rfcDirPath);
42
+ const entries: RfcIndexEntry[] = [];
43
+ for (const fileName of files) {
44
+ const result = await readAndParseRfc(rfcDirPath, fileName);
45
+ if (!result || "error" in result) continue;
46
+ const fm = result.parsed.frontmatter;
47
+ const arr = (k: string): string[] =>
48
+ Array.isArray(fm[k]) ? (fm[k] as unknown[]).map(String) : [];
49
+ entries.push({
50
+ id: String(fm["id"] ?? ""),
51
+ title: String(fm["title"] ?? ""),
52
+ status: String(fm["status"] ?? "") as RfcStatus,
53
+ kind: String(fm["kind"] ?? "") as RfcKind,
54
+ createdAt: String(fm["createdAt"] ?? ""),
55
+ implementedAt: String(fm["implementedAt"] ?? ""),
56
+ closedAt: String(fm["closedAt"] ?? ""),
57
+ supersedes: arr("supersedes"),
58
+ supersededBy: String(fm["supersededBy"] ?? ""),
59
+ amends: arr("amends"),
60
+ amendedBy: arr("amendedBy"),
61
+ related: arr("related"),
62
+ file: path.join(RFC_DIR, fileName),
63
+ });
64
+ }
65
+ entries.sort((a, b) => a.id.localeCompare(b.id));
66
+
67
+ let written: string | undefined;
68
+ if (input.flags["write"] === true) {
69
+ const outRel = path.join(RFC_DIR, "index.yaml");
70
+ const payload = {
71
+ command: "rfc.index.generate",
72
+ generatedAt: toIsoDate(new Date()),
73
+ count: entries.length,
74
+ entries,
75
+ };
76
+ await fs.writeFile(path.join(workspaceRoot, outRel), `${yamlStringify(payload)}`, "utf-8");
77
+ written = outRel;
78
+ }
79
+
80
+ if (outputFormat === "pretty") {
81
+ logger.section(`RFC index (${entries.length} RFC(s))`);
82
+ if (written) logger.success(`Wrote ${written}`);
83
+ else logger.info("Use --json for the index, or --write to persist docs/rfcs/index.yaml.");
84
+ }
85
+
86
+ return {
87
+ data: { command: "rfc.index.generate", status: "ok", count: entries.length, entries, written },
88
+ summary: written
89
+ ? `Wrote ${written} (${entries.length} RFCs)`
90
+ : `Indexed ${entries.length} RFC(s)`,
91
+ };
92
+ }
93
+
94
+ export async function runRfcGraph(
95
+ input: ForgeCommandInput,
96
+ context: ForgeRuntimeContext,
97
+ ): Promise<ForgeCommandResult<RfcGraphResult>> {
98
+ const { workspaceRoot, logger, outputFormat } = context;
99
+ const rfcDirPath = path.join(workspaceRoot, RFC_DIR);
100
+
101
+ const targetId = input.flags["id"] as string | undefined;
102
+ if (!targetId) {
103
+ throw new Error("rfc.graph requires an RFC id, e.g. site-kernel run rfc.graph --id RFC-0152");
104
+ }
105
+
106
+ const files = await listRfcFiles(rfcDirPath);
107
+ const fileName = files.find((f) => rfcFileMatchesId(f, targetId));
108
+ if (!fileName) {
109
+ throw new Error(`No RFC file found for id ${targetId} in ${RFC_DIR}/`);
110
+ }
111
+ const result = await readAndParseRfc(rfcDirPath, fileName);
112
+ if (!result || "error" in result) {
113
+ throw new Error(`Could not parse RFC ${targetId}: ${result?.error ?? "file not found"}`);
114
+ }
115
+ const fm = result.parsed.frontmatter;
116
+ const arr = (k: string): string[] =>
117
+ Array.isArray(fm[k]) ? (fm[k] as unknown[]).map(String) : [];
118
+
119
+ const data: RfcGraphResult = {
120
+ command: "rfc.graph",
121
+ status: "ok",
122
+ id: targetId,
123
+ supersedes: arr("supersedes"),
124
+ supersededBy: String(fm["supersededBy"] ?? ""),
125
+ amends: arr("amends"),
126
+ amendedBy: arr("amendedBy"),
127
+ related: arr("related"),
128
+ };
129
+
130
+ if (outputFormat === "pretty") {
131
+ logger.section(`${targetId} relationships`);
132
+ logger.info(`supersedes: ${data.supersedes.join(", ") || "—"}`);
133
+ logger.info(`supersededBy: ${data.supersededBy || "—"}`);
134
+ logger.info(`amends: ${data.amends.join(", ") || "—"}`);
135
+ logger.info(`amendedBy: ${data.amendedBy.join(", ") || "—"}`);
136
+ logger.info(`related: ${data.related.join(", ") || "—"}`);
137
+ }
138
+
139
+ return {
140
+ data,
141
+ summary: `${targetId}: ${data.supersedes.length} supersedes, ${data.amends.length} amends, ${data.related.length} related`,
142
+ };
143
+ }
144
+
145
+ export async function runRfcIndexValidate(
146
+ _input: ForgeCommandInput,
147
+ context: ForgeRuntimeContext,
148
+ ): Promise<
149
+ ForgeCommandResult<{
150
+ command: string;
151
+ status: string;
152
+ violations: { rule: string; message: string }[];
153
+ }>
154
+ > {
155
+ const { workspaceRoot, logger, outputFormat } = context;
156
+ const rfcDirPath = path.join(workspaceRoot, RFC_DIR);
157
+ const indexPath = path.join(rfcDirPath, "index.yaml");
158
+
159
+ const violations: { rule: string; message: string }[] = [];
160
+
161
+ // RFC-IDX-01: missing index file
162
+ let indexContent: string;
163
+ try {
164
+ indexContent = await fs.readFile(indexPath, "utf-8");
165
+ } catch {
166
+ violations.push({
167
+ rule: "RFC-IDX-01",
168
+ message:
169
+ "docs/rfcs/index.yaml not found. Run `pnpm exec werkstatt run rfc.index.generate --write` to generate it.",
170
+ });
171
+ const result = { command: "rfc.index.validate", status: "fail", violations };
172
+ if (outputFormat === "pretty") {
173
+ logger.error("RFC-IDX-01: docs/rfcs/index.yaml not found.");
174
+ }
175
+ return {
176
+ data: result,
177
+ exitCode: 1,
178
+ summary: "1 error(s) found",
179
+ };
180
+ }
181
+
182
+ // RFC-IDX-02: unparseable YAML
183
+ let parsed: { entries?: unknown[] } | null;
184
+ try {
185
+ parsed = yamlParse(indexContent) as { entries?: unknown[] } | null;
186
+ } catch (err) {
187
+ const msg = err instanceof Error ? err.message : String(err);
188
+ violations.push({
189
+ rule: "RFC-IDX-02",
190
+ message: `Failed to parse docs/rfcs/index.yaml: ${msg}`,
191
+ });
192
+ const result = { command: "rfc.index.validate", status: "fail", violations };
193
+ if (outputFormat === "pretty") {
194
+ logger.error(`RFC-IDX-02: Failed to parse index.yaml: ${msg}`);
195
+ }
196
+ return {
197
+ data: result,
198
+ exitCode: 1,
199
+ summary: "1 error(s) found",
200
+ };
201
+ }
202
+
203
+ // RFC-IDX-03: count mismatch
204
+ const indexCount = Array.isArray(parsed?.entries) ? parsed!.entries!.length : 0;
205
+ const allFiles = await listRfcFiles(rfcDirPath);
206
+ const fileCount = allFiles.length;
207
+
208
+ if (indexCount !== fileCount) {
209
+ violations.push({
210
+ rule: "RFC-IDX-03",
211
+ message: `Index entry count (${indexCount}) does not match RFC file count (${fileCount}). Run \`pnpm exec werkstatt run rfc.index.generate --write\` to refresh.`,
212
+ });
213
+ }
214
+
215
+ const hasErrors = violations.length > 0;
216
+ const resultStatus = hasErrors ? "fail" : "pass";
217
+ const result = { command: "rfc.index.validate", status: resultStatus, violations };
218
+
219
+ if (outputFormat === "pretty") {
220
+ if (hasErrors) {
221
+ for (const v of violations) {
222
+ logger.error(`[${v.rule}] ${v.message}`);
223
+ }
224
+ logger.error(`${violations.length} error(s) found`);
225
+ } else {
226
+ logger.success(`Index valid — ${indexCount} entries match ${fileCount} RFC files.`);
227
+ }
228
+ }
229
+
230
+ return {
231
+ data: result,
232
+ exitCode: hasErrors ? 1 : 0,
233
+ summary: hasErrors
234
+ ? `${violations.length} error(s) found`
235
+ : `Index valid — ${indexCount} entries match ${fileCount} RFC files`,
236
+ };
237
+ }
@@ -0,0 +1,127 @@
1
+ import { test, expect, describe } from "vitest";
2
+ import { collectRfcCommandLifecycleViolations } from "./lifecycle.ts";
3
+ import type { ParsedRfc } from "../frontmatter-io.ts";
4
+ import type { CommandRegistry } from "../../../src/types.ts";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+
8
+ const testWorkspaceRoot = join(tmpdir(), "test");
9
+
10
+ function makeParsed(
11
+ status: string,
12
+ createdAt: string,
13
+ commands: { added?: string[]; changed?: string[] },
14
+ ): ParsedRfc {
15
+ return {
16
+ frontmatter: {
17
+ id: "RFC-9999",
18
+ title: "Test RFC",
19
+ status,
20
+ kind: "policy",
21
+ scope: "workspace",
22
+ owners: ["architecture"],
23
+ createdAt,
24
+ updatedAt: createdAt,
25
+ commands,
26
+ },
27
+ body: "",
28
+ };
29
+ }
30
+
31
+ function mockRegistry(commandNames: string[]): CommandRegistry {
32
+ return {
33
+ listCommands: () => commandNames.map((name) => ({ name, description: "" })),
34
+ } as unknown as CommandRegistry;
35
+ }
36
+
37
+ describe("RFC-CMD-02/03 cutoff behavior", () => {
38
+ test("pre-cutoff RFC with unregistered command.added → no violation", async () => {
39
+ const parsed = makeParsed("implemented", "2026-01-01", {
40
+ added: ["nonexistent.command"],
41
+ });
42
+ const preParsed = new Map([["rfc-9999-test.md", { fileName: "rfc-9999-test.md", parsed }]]);
43
+ const { violations } = await collectRfcCommandLifecycleViolations(
44
+ testWorkspaceRoot,
45
+ ["rfc-9999-test.md"],
46
+ preParsed,
47
+ mockRegistry([]),
48
+ );
49
+ const cmd02 = violations.filter((v) => v.rule === "RFC-CMD-02");
50
+ expect(cmd02).toHaveLength(0);
51
+ });
52
+
53
+ test("post-cutoff RFC with unregistered command.added → violation", async () => {
54
+ const parsed = makeParsed("implemented", "2026-07-08", {
55
+ added: ["nonexistent.command"],
56
+ });
57
+ const preParsed = new Map([["rfc-9999-test.md", { fileName: "rfc-9999-test.md", parsed }]]);
58
+ const { violations } = await collectRfcCommandLifecycleViolations(
59
+ testWorkspaceRoot,
60
+ ["rfc-9999-test.md"],
61
+ preParsed,
62
+ mockRegistry([]),
63
+ );
64
+ const cmd02 = violations.filter((v) => v.rule === "RFC-CMD-02");
65
+ expect(cmd02).toHaveLength(1);
66
+ });
67
+
68
+ test("pre-cutoff RFC with unregistered command.changed → no violation", async () => {
69
+ const parsed = makeParsed("implemented", "2026-01-01", {
70
+ changed: ["nonexistent.command"],
71
+ });
72
+ const preParsed = new Map([["rfc-9999-test.md", { fileName: "rfc-9999-test.md", parsed }]]);
73
+ const { violations } = await collectRfcCommandLifecycleViolations(
74
+ testWorkspaceRoot,
75
+ ["rfc-9999-test.md"],
76
+ preParsed,
77
+ mockRegistry([]),
78
+ );
79
+ const cmd03 = violations.filter((v) => v.rule === "RFC-CMD-03");
80
+ expect(cmd03).toHaveLength(0);
81
+ });
82
+
83
+ test("post-cutoff RFC with unregistered command.changed → violation", async () => {
84
+ const parsed = makeParsed("implemented", "2026-07-08", {
85
+ changed: ["nonexistent.command"],
86
+ });
87
+ const preParsed = new Map([["rfc-9999-test.md", { fileName: "rfc-9999-test.md", parsed }]]);
88
+ const { violations } = await collectRfcCommandLifecycleViolations(
89
+ testWorkspaceRoot,
90
+ ["rfc-9999-test.md"],
91
+ preParsed,
92
+ mockRegistry([]),
93
+ );
94
+ const cmd03 = violations.filter((v) => v.rule === "RFC-CMD-03");
95
+ expect(cmd03).toHaveLength(1);
96
+ });
97
+
98
+ test("exact cutoff date (2026-07-07) → violation (>= cutoff)", async () => {
99
+ const parsed = makeParsed("implemented", "2026-07-07", {
100
+ added: ["nonexistent.command"],
101
+ });
102
+ const preParsed = new Map([["rfc-9999-test.md", { fileName: "rfc-9999-test.md", parsed }]]);
103
+ const { violations } = await collectRfcCommandLifecycleViolations(
104
+ testWorkspaceRoot,
105
+ ["rfc-9999-test.md"],
106
+ preParsed,
107
+ mockRegistry([]),
108
+ );
109
+ const cmd02 = violations.filter((v) => v.rule === "RFC-CMD-02");
110
+ expect(cmd02).toHaveLength(1);
111
+ });
112
+
113
+ test("day before cutoff (2026-07-06) → no violation", async () => {
114
+ const parsed = makeParsed("implemented", "2026-07-06", {
115
+ added: ["nonexistent.command"],
116
+ });
117
+ const preParsed = new Map([["rfc-9999-test.md", { fileName: "rfc-9999-test.md", parsed }]]);
118
+ const { violations } = await collectRfcCommandLifecycleViolations(
119
+ testWorkspaceRoot,
120
+ ["rfc-9999-test.md"],
121
+ preParsed,
122
+ mockRegistry([]),
123
+ );
124
+ const cmd02 = violations.filter((v) => v.rule === "RFC-CMD-02");
125
+ expect(cmd02).toHaveLength(0);
126
+ });
127
+ });
@@ -0,0 +1,249 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>RFC command lifecycle validation — collects and reports RFC-CMD-* violations.</purpose>
4
+ <non-goals>
5
+ <item>Do not introduce app-specific runtime composition or deployment behavior into this reusable package source file.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0303: extracted lifecycle logic from handlers.ts into handlers/lifecycle.ts.</item>
10
+ <item>Post-refactor hardening: targeted lifecycle validation resolves RFC ids inside archive subdirectories.</item>
11
+ <item>RFC-0465: added RFC_METADATA_CUTOFF check to RFC-CMD-02 and RFC-CMD-03 rules — pre-cutoff RFCs are exempt from command registration checks.</item>
12
+ <item>RFC-0465 fix: added manifest-based fallback in getLiveCommands — reads docs/command-manifest.generated.yaml when commandRegistry returns empty (site-kernel mode).</item>
13
+ </CHANGE_SUMMARY>
14
+ */
15
+
16
+ import { readFile } from "node:fs/promises";
17
+ import path from "node:path";
18
+ import { parse as yamlParse } from "yaml";
19
+
20
+ import type { CommandRegistry } from "../../../src/types.ts";
21
+ import {
22
+ listRfcFiles,
23
+ readAndParseRfc,
24
+ rfcFileMatchesId,
25
+ type ParsedRfc,
26
+ } from "../frontmatter-io.ts";
27
+ import type {
28
+ ForgeCommandInput,
29
+ ForgeCommandResult,
30
+ ForgeRuntimeContext,
31
+ } from "../../../src/types.ts";
32
+ import type {
33
+ RfcStatus,
34
+ RfcCommandLifecycleViolation,
35
+ RfcCommandLifecycleValidationResult,
36
+ } from "../types.ts";
37
+ import { RFC_DIR, RFC_METADATA_CUTOFF } from "../types.ts";
38
+ import { commandBuckets } from "./shared.ts";
39
+
40
+ async function loadManifestCommandNames(workspaceRoot: string): Promise<Set<string>> {
41
+ const manifestPath = path.join(workspaceRoot, "docs", "command-manifest.generated.yaml");
42
+ try {
43
+ const raw = await readFile(manifestPath, "utf-8");
44
+ const manifest = yamlParse(raw) as { commands?: Array<{ name: string }> };
45
+ return new Set((manifest.commands ?? []).map((c) => c.name));
46
+ } catch {
47
+ return new Set();
48
+ }
49
+ }
50
+
51
+ export async function collectRfcCommandLifecycleViolations(
52
+ workspaceRoot: string,
53
+ filesToValidate?: string[],
54
+ preParsed?: Map<string, { fileName: string; parsed: ParsedRfc }>,
55
+ commandRegistry?: CommandRegistry,
56
+ ): Promise<{ checkedCount: number; violations: RfcCommandLifecycleViolation[] }> {
57
+ const rfcDirPath = path.join(workspaceRoot, RFC_DIR);
58
+ const files = filesToValidate ?? (await listRfcFiles(rfcDirPath));
59
+
60
+ let liveCommandsPromise: Promise<Set<string>> | undefined;
61
+ async function getLiveCommands(): Promise<Set<string>> {
62
+ if (!liveCommandsPromise) {
63
+ const cmds = commandRegistry?.listCommands() ?? [];
64
+ const registryNames = new Set(cmds.map((c) => c.name));
65
+ const manifestNames = await loadManifestCommandNames(workspaceRoot);
66
+ const names = new Set([...registryNames, ...manifestNames]);
67
+ liveCommandsPromise = Promise.resolve(names);
68
+ }
69
+ return liveCommandsPromise;
70
+ }
71
+
72
+ const violations: RfcCommandLifecycleViolation[] = [];
73
+
74
+ const addViolation = (
75
+ rfcId: string,
76
+ file: string,
77
+ rule: RfcCommandLifecycleViolation["rule"],
78
+ message: string,
79
+ severity: "error" | "warning",
80
+ data: RfcCommandLifecycleViolation["data"],
81
+ ): void => {
82
+ violations.push({ rfcId, file, rule, message, severity, data });
83
+ };
84
+
85
+ let checkedCount = 0;
86
+ for (const fileName of files) {
87
+ const result = preParsed?.get(fileName) ?? (await readAndParseRfc(rfcDirPath, fileName));
88
+ if (!result || "error" in result) continue;
89
+
90
+ checkedCount += 1;
91
+ const fm = result.parsed.frontmatter;
92
+ const rfcId = String(fm["id"] ?? "UNKNOWN");
93
+ const status = String(fm["status"] ?? "") as RfcStatus;
94
+ const relFile = path.join(RFC_DIR, fileName);
95
+ const buckets = commandBuckets(fm);
96
+ const removed = new Set(buckets.removed);
97
+ const createdAt = String(fm["createdAt"] ?? "");
98
+ const isPostCutoff = createdAt >= RFC_METADATA_CUTOFF;
99
+ const isArchived = fileName.startsWith("archive/");
100
+
101
+ // Skip command lifecycle checks for archived RFCs — commands were
102
+ // renamed/removed during ecosystem consolidation (RFC-0776) and archived
103
+ // RFCs are frozen historical records that cannot be updated.
104
+ if (isArchived) continue;
105
+
106
+ for (const command of buckets.proposed) {
107
+ if (
108
+ status === "implemented" &&
109
+ (await getLiveCommands()).has(command) &&
110
+ !buckets.added.includes(command)
111
+ ) {
112
+ addViolation(
113
+ rfcId,
114
+ relFile,
115
+ "RFC-CMD-01",
116
+ `implemented RFC lists live command "${command}" under commands.proposed but not commands.added`,
117
+ "error",
118
+ { rfcId, command, status, bucket: "proposed" },
119
+ );
120
+ }
121
+ }
122
+
123
+ for (const command of buckets.added) {
124
+ if (
125
+ isPostCutoff &&
126
+ status === "implemented" &&
127
+ !(await getLiveCommands()).has(command) &&
128
+ !removed.has(command)
129
+ ) {
130
+ addViolation(
131
+ rfcId,
132
+ relFile,
133
+ "RFC-CMD-02",
134
+ `implemented RFC lists "${command}" under commands.added, but no live command is registered`,
135
+ "error",
136
+ { rfcId, command, status, bucket: "added" },
137
+ );
138
+ }
139
+ if ((status === "draft" || status === "reviewing") && !result.parsed.body.includes(command)) {
140
+ addViolation(
141
+ rfcId,
142
+ relFile,
143
+ "RFC-CMD-04",
144
+ `non-accepted RFC lists "${command}" under commands.added without body rationale`,
145
+ "warning",
146
+ { rfcId, command, status, bucket: "added" },
147
+ );
148
+ }
149
+ }
150
+
151
+ for (const command of buckets.changed) {
152
+ if (
153
+ isPostCutoff &&
154
+ status === "implemented" &&
155
+ !(await getLiveCommands()).has(command) &&
156
+ !removed.has(command)
157
+ ) {
158
+ addViolation(
159
+ rfcId,
160
+ relFile,
161
+ "RFC-CMD-03",
162
+ `implemented RFC lists "${command}" under commands.changed, but no live command is registered`,
163
+ "error",
164
+ { rfcId, command, status, bucket: "changed" },
165
+ );
166
+ }
167
+ }
168
+
169
+ for (const command of new Set([...buckets.added, ...buckets.changed])) {
170
+ if (removed.has(command)) continue;
171
+ const bucketCount =
172
+ Number(buckets.added.includes(command)) + Number(buckets.changed.includes(command));
173
+ if (bucketCount > 1) {
174
+ addViolation(
175
+ rfcId,
176
+ relFile,
177
+ "RFC-CMD-05",
178
+ `command "${command}" appears in conflicting lifecycle buckets without commands.removed transition`,
179
+ "warning",
180
+ { rfcId, command, status, bucket: buckets.added.includes(command) ? "added" : "changed" },
181
+ );
182
+ }
183
+ }
184
+ }
185
+
186
+ return { checkedCount, violations };
187
+ }
188
+
189
+ export async function runRfcCommandLifecycleValidate(
190
+ input: ForgeCommandInput,
191
+ context: ForgeRuntimeContext,
192
+ ): Promise<ForgeCommandResult<RfcCommandLifecycleValidationResult>> {
193
+ const { workspaceRoot, logger, outputFormat } = context;
194
+ const rfcDirPath = path.join(workspaceRoot, RFC_DIR);
195
+ const targetId = input.flags["id"] as string | undefined;
196
+ const allFiles = await listRfcFiles(rfcDirPath);
197
+ const filesToValidate = targetId
198
+ ? allFiles.filter((f) => rfcFileMatchesId(f, targetId))
199
+ : allFiles;
200
+
201
+ if (filesToValidate.length === 0) {
202
+ if (targetId) {
203
+ throw new Error(`No RFC file found for id ${targetId} in ${RFC_DIR}/`);
204
+ }
205
+ return {
206
+ data: {
207
+ command: "rfc.command-lifecycle.validate",
208
+ status: "pass",
209
+ count: 0,
210
+ violations: [],
211
+ },
212
+ summary: "No RFC files found to validate",
213
+ };
214
+ }
215
+
216
+ const { checkedCount, violations } = await collectRfcCommandLifecycleViolations(
217
+ workspaceRoot,
218
+ filesToValidate,
219
+ undefined,
220
+ context.commandRegistry,
221
+ );
222
+ const hasErrors = violations.some((v) => v.severity === "error");
223
+
224
+ if (outputFormat === "pretty") {
225
+ if (violations.length === 0) {
226
+ logger.success(`All ${checkedCount} RFC(s) passed command lifecycle validation.`);
227
+ } else {
228
+ logger.section(`RFC Command Lifecycle Validation (${checkedCount} file(s))`);
229
+ for (const v of violations) {
230
+ logger[v.severity === "error" ? "error" : "warn"](
231
+ `[${v.severity === "error" ? "ERROR" : "WARN"}] ${v.rfcId} ${v.rule}: ${v.message}`,
232
+ );
233
+ }
234
+ }
235
+ }
236
+
237
+ return {
238
+ data: {
239
+ command: "rfc.command-lifecycle.validate",
240
+ status: hasErrors ? "fail" : "pass",
241
+ count: checkedCount,
242
+ violations,
243
+ },
244
+ exitCode: hasErrors ? 1 : 0,
245
+ summary: hasErrors
246
+ ? `${violations.filter((v) => v.severity === "error").length} command lifecycle error(s) found`
247
+ : `All ${checkedCount} RFC(s) passed command lifecycle validation`,
248
+ };
249
+ }