@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,207 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>ADR list and create handlers mirroring the RFC domain.</purpose>
4
+ <non-goals>
5
+ <item>Do not introduce app-specific runtime composition or deployment behavior.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0366: create ADR list and create handlers mirroring the RFC domain.</item>
10
+ <item>RFC-0521: migrated from packages/os/site-kernel/src/adr/ to packages/forge/os/adr/.</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ import fs from "node:fs/promises";
15
+ import path from "node:path";
16
+ import { toKebabCase } from "../../../src/utils/string-utils.ts";
17
+ import { listAdrFiles, readAndParseAdr } from "../frontmatter-io.ts";
18
+ import type {
19
+ ForgeCommandInput,
20
+ ForgeCommandResult,
21
+ ForgeRuntimeContext,
22
+ } from "../../../src/types.ts";
23
+ import type {
24
+ AdrListEntry,
25
+ AdrListResult,
26
+ AdrCreateResult,
27
+ AdrStatus,
28
+ AdrScope,
29
+ } from "../types.ts";
30
+ import { ADR_DIR, ADR_TEMPLATE_FILE, ADR_SCOPES, ADR_STATUSES } from "../types.ts";
31
+
32
+ function toIsoDate(d: Date): string {
33
+ return d.toISOString().slice(0, 10);
34
+ }
35
+
36
+ export async function runAdrList(
37
+ input: ForgeCommandInput,
38
+ context: ForgeRuntimeContext,
39
+ ): Promise<ForgeCommandResult<AdrListResult>> {
40
+ const { workspaceRoot, logger, outputFormat } = context;
41
+ const adrDirPath = path.join(workspaceRoot, ADR_DIR);
42
+
43
+ const filterStatus = input.flags["status"] as string | undefined;
44
+ const filterScope = input.flags["scope"] as string | undefined;
45
+ const filterDecider = input.flags["decider"] as string | undefined;
46
+
47
+ const files = await listAdrFiles(adrDirPath);
48
+ const entries: AdrListEntry[] = [];
49
+
50
+ for (const fileName of files) {
51
+ const result = await readAndParseAdr(adrDirPath, fileName);
52
+ if (!result) continue;
53
+
54
+ const fm = result.parsed.frontmatter;
55
+ const id = String(fm["id"] ?? "");
56
+ const status = String(fm["status"] ?? "") as AdrStatus;
57
+ const scope = String(fm["scope"] ?? "") as AdrScope;
58
+ const decider = String(fm["decider"] ?? "");
59
+
60
+ if (filterStatus && status !== filterStatus) continue;
61
+ if (filterScope && scope !== filterScope) continue;
62
+ if (filterDecider && decider !== filterDecider) continue;
63
+
64
+ entries.push({
65
+ id,
66
+ title: String(fm["title"] ?? ""),
67
+ status,
68
+ scope,
69
+ decider,
70
+ updatedAt: String(fm["updatedAt"] ?? ""),
71
+ file: path.join(ADR_DIR, fileName),
72
+ });
73
+ }
74
+
75
+ if (outputFormat === "pretty") {
76
+ if (entries.length === 0) {
77
+ logger.info("No ADRs found matching the given filters.");
78
+ } else {
79
+ logger.section(`ADRs (${entries.length})`);
80
+ for (const entry of entries) {
81
+ logger.info(
82
+ `${entry.id} ${entry.status.padEnd(12)} ${entry.scope.padEnd(10)} ${entry.decider.padEnd(20)} ${entry.title}`,
83
+ );
84
+ }
85
+ }
86
+ }
87
+
88
+ return {
89
+ data: {
90
+ command: "adr.list",
91
+ status: "ok",
92
+ count: entries.length,
93
+ entries,
94
+ },
95
+ summary: `${entries.length} ADR(s) found`,
96
+ };
97
+ }
98
+
99
+ export async function runAdrCreate(
100
+ input: ForgeCommandInput,
101
+ context: ForgeRuntimeContext,
102
+ ): Promise<ForgeCommandResult<AdrCreateResult>> {
103
+ const { workspaceRoot, logger, outputFormat } = context;
104
+ const adrDirPath = path.join(workspaceRoot, ADR_DIR);
105
+
106
+ const title = input.flags["title"] as string | undefined;
107
+ if (!title) {
108
+ throw new Error(
109
+ 'adr.create requires --title flag, e.g. --title="Use DuckDB for local analytics"',
110
+ );
111
+ }
112
+
113
+ const scope = (input.flags["scope"] as string | undefined) ?? "package";
114
+ const decider = (input.flags["decider"] as string | undefined) ?? "architecture";
115
+ const status = (input.flags["status"] as string | undefined) ?? "proposed";
116
+ const relatedRaw = input.flags["related"] as string | undefined;
117
+
118
+ if (!ADR_SCOPES.includes(scope as AdrScope)) {
119
+ throw new Error(`Invalid scope: ${scope}. Must be one of: ${ADR_SCOPES.join(", ")}`);
120
+ }
121
+ if (!ADR_STATUSES.includes(status as AdrStatus)) {
122
+ throw new Error(`Invalid status: ${status}. Must be one of: ${ADR_STATUSES.join(", ")}`);
123
+ }
124
+
125
+ const files = await listAdrFiles(adrDirPath);
126
+ let maxId = 0;
127
+ for (const f of files) {
128
+ const basename = path.basename(f);
129
+ const match = basename.match(/^adr-(\d{4})/);
130
+ if (match) {
131
+ const num = parseInt(match[1]!, 10);
132
+ if (num > maxId) maxId = num;
133
+ }
134
+ }
135
+ const nextNum = maxId + 1;
136
+ const paddedNum = String(nextNum).padStart(4, "0");
137
+ const nextSlug = `adr-${paddedNum}`;
138
+ const nextId = `ADR-${paddedNum}`;
139
+
140
+ const templateRelPath = ADR_TEMPLATE_FILE;
141
+ const templatePath = path.join(workspaceRoot, templateRelPath);
142
+ let templateContent: string;
143
+ try {
144
+ templateContent = await fs.readFile(templatePath, "utf-8");
145
+ } catch {
146
+ throw new Error(
147
+ `ADR template not found at ${templateRelPath}. Ensure docs/adrs/ contains the template.`,
148
+ );
149
+ }
150
+
151
+ const today = toIsoDate(new Date());
152
+ const kebabTitle = toKebabCase(title);
153
+ const fileName = `${nextSlug}-${kebabTitle}.md`;
154
+
155
+ let content = templateContent;
156
+ content = content.replace(/^id: ADR-0000$/m, `id: ${nextId}`);
157
+ const escapedTitle = title.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
158
+ content = content.replace(/^title: ".*"$/m, `title: "${escapedTitle}"`);
159
+ content = content.replace(/^status: \w+$/m, `status: ${status}`);
160
+ content = content.replace(/^scope: \w+$/m, `scope: ${scope}`);
161
+ content = content.replace(/^decider: \w+$/m, `decider: ${decider}`);
162
+ content = content.replace(/^createdAt: YYYY-MM-DD$/m, `createdAt: ${today}`);
163
+ content = content.replace(/^updatedAt: YYYY-MM-DD$/m, `updatedAt: ${today}`);
164
+ content = content.replace(/^# ADR-0000: .+$/m, `# ${nextId}: ${title}`);
165
+
166
+ const relatedIds = relatedRaw
167
+ ? relatedRaw
168
+ .split(",")
169
+ .map((s) => s.trim())
170
+ .filter(Boolean)
171
+ : [];
172
+ if (relatedIds.length > 0) {
173
+ content = content.replace(
174
+ /^related:\s*\n\s*- RFC-0001\s*$/m,
175
+ `related:\n${relatedIds.map((id) => ` - ${id}`).join("\n")}`,
176
+ );
177
+ } else {
178
+ content = content.replace(/^related:\s*\n\s*- RFC-0001\s*$/m, `related: []`);
179
+ }
180
+
181
+ try {
182
+ await fs.mkdir(adrDirPath, { recursive: true });
183
+ } catch {
184
+ // ignore
185
+ }
186
+
187
+ const targetPath = path.join(adrDirPath, fileName);
188
+ await fs.writeFile(targetPath, content, "utf-8");
189
+
190
+ const relativeFile = path.join(ADR_DIR, fileName);
191
+
192
+ if (outputFormat === "pretty") {
193
+ logger.success(`Created ${nextId}: ${title}`);
194
+ logger.info(`File: ${relativeFile}`);
195
+ logger.info(`Status: ${status} (only the named decider may change status)`);
196
+ }
197
+
198
+ return {
199
+ data: {
200
+ command: "adr.create",
201
+ status: "ok",
202
+ file: relativeFile,
203
+ id: nextId,
204
+ },
205
+ summary: `Created ${nextId}: ${title}`,
206
+ };
207
+ }
@@ -0,0 +1,232 @@
1
+ import { test, expect, describe } from "vitest";
2
+ import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
3
+ import { execSync, execFileSync } from "node:child_process";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { runAdrValidate } from "./validate.ts";
7
+ import type { ForgeCommandInput, ForgeRuntimeContext } from "../../../src/types.ts";
8
+
9
+ const ADR_BODY = `
10
+ # ADR-9999: Test ADR
11
+
12
+ ## Context
13
+
14
+ Test context.
15
+
16
+ ## Decision
17
+
18
+ Test decision.
19
+
20
+ ## Consequences
21
+
22
+ Test consequences.
23
+ `;
24
+
25
+ function createAdrFile(
26
+ workspaceRoot: string,
27
+ id: string,
28
+ status: string,
29
+ body: string,
30
+ extraFm: Record<string, unknown> = {},
31
+ ): void {
32
+ const adrDir = join(workspaceRoot, "docs", "adrs");
33
+ mkdirSync(adrDir, { recursive: true });
34
+ const slug = id.toLowerCase();
35
+ const fm = [
36
+ "---",
37
+ `id: ${id}`,
38
+ `title: "Test ADR"`,
39
+ `status: ${status}`,
40
+ `scope: package`,
41
+ `decider: human:test`,
42
+ `createdAt: 2026-01-01`,
43
+ `updatedAt: 2026-01-01`,
44
+ ...Object.entries(extraFm).map(([k, v]) => `${k}: ${v}`),
45
+ "---",
46
+ "",
47
+ body,
48
+ ].join("\n");
49
+ writeFileSync(join(adrDir, `${slug}-test.md`), fm);
50
+ }
51
+
52
+ function createGitRepoWithCommits(commits: { message: string; date: string }[]): string {
53
+ const dir = mkdtempSync(join(tmpdir(), "av16-test-"));
54
+ execSync("git init", { cwd: dir, timeout: 5000 });
55
+ execSync("git config user.email test@test.com", { cwd: dir, timeout: 5000 });
56
+ execSync("git config user.name Test", { cwd: dir, timeout: 5000 });
57
+ for (const c of commits) {
58
+ execFileSync("git", ["commit", "--allow-empty", "-m", c.message], {
59
+ cwd: dir,
60
+ timeout: 5000,
61
+ env: { ...process.env, GIT_AUTHOR_DATE: c.date, GIT_COMMITTER_DATE: c.date },
62
+ stdio: "pipe",
63
+ });
64
+ }
65
+ return dir;
66
+ }
67
+
68
+ async function runValidate(
69
+ workspaceRoot: string,
70
+ targetId?: string,
71
+ ): Promise<{ rule: string; message: string; severity: string }[]> {
72
+ const input: ForgeCommandInput = {
73
+ argv: [],
74
+ flags: targetId ? { id: targetId } : {},
75
+ };
76
+ const context: ForgeRuntimeContext = {
77
+ workspaceRoot,
78
+ logger: {
79
+ info: () => {},
80
+ success: () => {},
81
+ warn: () => {},
82
+ error: () => {},
83
+ section: () => {},
84
+ },
85
+ dryRun: false,
86
+ outputFormat: "json",
87
+ };
88
+ const result = await runAdrValidate(input, context);
89
+ return (result.data?.violations ?? []).map((v) => ({
90
+ rule: v.rule,
91
+ message: v.message,
92
+ severity: v.severity,
93
+ }));
94
+ }
95
+
96
+ function filterRule(
97
+ violations: { rule: string; message: string; severity: string }[],
98
+ rule: string,
99
+ ): { rule: string; message: string; severity: string }[] {
100
+ return violations.filter((v) => v.rule === rule);
101
+ }
102
+
103
+ describe("AV-16: implementation commit drift detection", () => {
104
+ test("AV-16 warning when accepted ADR has implement: commits", async () => {
105
+ const dir = createGitRepoWithCommits([
106
+ { message: "implement: ADR-9999 — step 1", date: "2026-01-02T10:00:00" },
107
+ ]);
108
+ try {
109
+ createAdrFile(dir, "ADR-9999", "accepted", ADR_BODY);
110
+ const violations = await runValidate(dir, "ADR-9999");
111
+ const av16 = filterRule(violations, "AV-16");
112
+ expect(av16).toHaveLength(1);
113
+ expect(av16[0]!.severity).toBe("warning");
114
+ expect(av16[0]!.message).toContain("ADR-9999");
115
+ expect(av16[0]!.message).toContain("accepted");
116
+ } finally {
117
+ rmSync(dir, { recursive: true, force: true });
118
+ }
119
+ });
120
+
121
+ test("no AV-16 when status is implemented", async () => {
122
+ const dir = createGitRepoWithCommits([
123
+ { message: "implement: ADR-9999 — step 1", date: "2026-01-02T10:00:00" },
124
+ ]);
125
+ try {
126
+ createAdrFile(dir, "ADR-9999", "implemented", ADR_BODY, {
127
+ implementedAt: "2026-01-03",
128
+ });
129
+ const violations = await runValidate(dir, "ADR-9999");
130
+ const av16 = filterRule(violations, "AV-16");
131
+ expect(av16).toHaveLength(0);
132
+ } finally {
133
+ rmSync(dir, { recursive: true, force: true });
134
+ }
135
+ });
136
+
137
+ test("no AV-16 when no implement: commits exist", async () => {
138
+ const dir = createGitRepoWithCommits([
139
+ { message: "feat: add some feature", date: "2026-01-02T10:00:00" },
140
+ ]);
141
+ try {
142
+ createAdrFile(dir, "ADR-9999", "accepted", ADR_BODY);
143
+ const violations = await runValidate(dir, "ADR-9999");
144
+ const av16 = filterRule(violations, "AV-16");
145
+ expect(av16).toHaveLength(0);
146
+ } finally {
147
+ rmSync(dir, { recursive: true, force: true });
148
+ }
149
+ });
150
+
151
+ test("no AV-16 in non-git directory", async () => {
152
+ const dir = mkdtempSync(join(tmpdir(), "av16-nogit-"));
153
+ try {
154
+ createAdrFile(dir, "ADR-9999", "accepted", ADR_BODY);
155
+ const violations = await runValidate(dir, "ADR-9999");
156
+ const av16 = filterRule(violations, "AV-16");
157
+ expect(av16).toHaveLength(0);
158
+ } finally {
159
+ rmSync(dir, { recursive: true, force: true });
160
+ }
161
+ });
162
+ });
163
+
164
+ describe("ADR-DIR-01: directory structure convention (RFC-0722)", () => {
165
+ function createAdrInSubdir(
166
+ workspaceRoot: string,
167
+ subdir: string,
168
+ id: string,
169
+ status: string,
170
+ body: string,
171
+ extraFm: Record<string, unknown> = {},
172
+ ): void {
173
+ const adrDir = join(workspaceRoot, "docs", "adrs", subdir);
174
+ mkdirSync(adrDir, { recursive: true });
175
+ const slug = id.toLowerCase();
176
+ const fm = [
177
+ "---",
178
+ `id: ${id}`,
179
+ `title: "Test ADR"`,
180
+ `status: ${status}`,
181
+ `scope: package`,
182
+ `decider: human:test`,
183
+ `createdAt: 2026-01-01`,
184
+ `updatedAt: 2026-01-01`,
185
+ ...Object.entries(extraFm).map(([k, v]) => `${k}: ${v}`),
186
+ "---",
187
+ "",
188
+ body,
189
+ ].join("\n");
190
+ writeFileSync(join(adrDir, `${slug}-test.md`), fm);
191
+ }
192
+
193
+ test("warning when ADR file is in an unsanctioned subdirectory", async () => {
194
+ const dir = mkdtempSync(join(tmpdir(), "adr-dir01-bad-"));
195
+ try {
196
+ createAdrInSubdir(dir, "draft", "ADR-9999", "accepted", ADR_BODY);
197
+ const violations = await runValidate(dir, "ADR-9999");
198
+ const dir01 = filterRule(violations, "ADR-DIR-01");
199
+ expect(dir01).toHaveLength(1);
200
+ expect(dir01[0]!.severity).toBe("warning");
201
+ expect(dir01[0]!.message).toContain("unsanctioned subdirectory");
202
+ } finally {
203
+ rmSync(dir, { recursive: true, force: true });
204
+ }
205
+ });
206
+
207
+ test("no warning when ADR file is at root", async () => {
208
+ const dir = mkdtempSync(join(tmpdir(), "adr-dir01-root-"));
209
+ try {
210
+ createAdrFile(dir, "ADR-9999", "accepted", ADR_BODY);
211
+ const violations = await runValidate(dir, "ADR-9999");
212
+ const dir01 = filterRule(violations, "ADR-DIR-01");
213
+ expect(dir01).toHaveLength(0);
214
+ } finally {
215
+ rmSync(dir, { recursive: true, force: true });
216
+ }
217
+ });
218
+
219
+ test("no warning when ADR file is in archive/ subdirectory", async () => {
220
+ const dir = mkdtempSync(join(tmpdir(), "adr-dir01-archive-"));
221
+ try {
222
+ createAdrInSubdir(dir, "archive", "ADR-9999", "implemented", ADR_BODY, {
223
+ implementedAt: "2026-01-02",
224
+ });
225
+ const violations = await runValidate(dir, "ADR-9999");
226
+ const dir01 = filterRule(violations, "ADR-DIR-01");
227
+ expect(dir01).toHaveLength(0);
228
+ } finally {
229
+ rmSync(dir, { recursive: true, force: true });
230
+ }
231
+ });
232
+ });