@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,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
+ });
@@ -0,0 +1,429 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>ADR validation handler — frontmatter, section, referential integrity checks, implementation commit drift detection (AV-16), and directory structure convention (ADR-DIR-01).</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: implement fail-hard ADR validation for the new adrModule.</item>
10
+ <item>Post-refactor hardening: allow ADRs to be superseded by existing RFC decisions.</item>
11
+ <item>RFC-0521: migrated from packages/os/site-kernel/src/adr/ to packages/forge/os/adr/.</item>
12
+ <item>RFC-0722: add ADR-DIR-01 directory structure warning rule for unsanctioned subdirectories.</item>
13
+ </CHANGE_SUMMARY>
14
+ */
15
+
16
+ import path from "node:path";
17
+ import { execFile } from "node:child_process";
18
+ import {
19
+ listAdrFiles,
20
+ readAndParseAdr,
21
+ adrFileMatchesId,
22
+ type ParsedAdr,
23
+ } from "../frontmatter-io.ts";
24
+ import { listRfcFiles, readAndParseRfc } from "../../rfc/frontmatter-io.ts";
25
+ import type {
26
+ ForgeCommandInput,
27
+ ForgeCommandResult,
28
+ ForgeRuntimeContext,
29
+ } from "../../../src/types.ts";
30
+ import type { AdrValidationViolation, AdrValidationResult, AdrStatus, AdrScope } from "../types.ts";
31
+ import {
32
+ ADR_DIR,
33
+ ADR_ID_PATTERN,
34
+ ADR_KNOWN_KEYS,
35
+ ADR_REQUIRED_SECTIONS,
36
+ ADR_SCOPES,
37
+ ADR_STATUSES,
38
+ } from "../types.ts";
39
+ import { RFC_DIR } from "../../rfc/types.ts";
40
+
41
+ async function loadRfcIds(workspaceRoot: string): Promise<Set<string>> {
42
+ const rfcIds = new Set<string>();
43
+ const rfcDirPath = path.join(workspaceRoot, RFC_DIR);
44
+ const files = await listRfcFiles(rfcDirPath);
45
+ for (const fileName of files) {
46
+ const result = await readAndParseRfc(rfcDirPath, fileName);
47
+ const id = result && "parsed" in result ? String(result.parsed.frontmatter["id"] ?? "") : "";
48
+ if (id) rfcIds.add(id);
49
+ }
50
+ return rfcIds;
51
+ }
52
+
53
+ export async function runAdrValidate(
54
+ input: ForgeCommandInput,
55
+ context: ForgeRuntimeContext,
56
+ ): Promise<ForgeCommandResult<AdrValidationResult>> {
57
+ const { workspaceRoot, logger, outputFormat } = context;
58
+ const adrDirPath = path.join(workspaceRoot, ADR_DIR);
59
+
60
+ const targetId = input.flags["id"] as string | undefined;
61
+
62
+ const allFiles = await listAdrFiles(adrDirPath);
63
+ const filesToValidate = targetId
64
+ ? allFiles.filter((f) => adrFileMatchesId(f, targetId))
65
+ : allFiles;
66
+
67
+ if (filesToValidate.length === 0) {
68
+ if (targetId) {
69
+ throw new Error(`No ADR file found for id ${targetId} in ${ADR_DIR}/`);
70
+ }
71
+ if (outputFormat === "pretty") {
72
+ logger.info("No ADR files found to validate.");
73
+ }
74
+ return {
75
+ data: { command: "adr.validate", status: "pass", count: 0, violations: [] },
76
+ summary: "No ADR files found to validate",
77
+ };
78
+ }
79
+
80
+ const allParsed: Map<string, { fileName: string; parsed: ParsedAdr }> = new Map();
81
+ const allParsedByFile: Map<string, { fileName: string; parsed: ParsedAdr }> = new Map();
82
+ for (const f of allFiles) {
83
+ const result = await readAndParseAdr(adrDirPath, f);
84
+ if (result) {
85
+ const id = String(result.parsed.frontmatter["id"] ?? "");
86
+ allParsed.set(id, result);
87
+ allParsedByFile.set(f, result);
88
+ }
89
+ }
90
+
91
+ const knownKeys = new Set<string>(ADR_KNOWN_KEYS);
92
+ const knownRfcIds = await loadRfcIds(workspaceRoot);
93
+ const violations: AdrValidationViolation[] = [];
94
+ const seenIds = new Map<string, string>();
95
+
96
+ function addViolation(
97
+ adrId: string,
98
+ file: string,
99
+ rule: string,
100
+ message: string,
101
+ severity: "error" | "warning" = "error",
102
+ ): void {
103
+ violations.push({ adrId, file, rule, message, severity });
104
+ }
105
+
106
+ for (const fileName of filesToValidate) {
107
+ const result = allParsedByFile.get(fileName);
108
+ if (!result) continue;
109
+
110
+ await validateSingleAdr(
111
+ fileName,
112
+ result.parsed,
113
+ allParsed,
114
+ knownRfcIds,
115
+ seenIds,
116
+ knownKeys,
117
+ workspaceRoot,
118
+ addViolation,
119
+ );
120
+ }
121
+
122
+ const hasErrors = violations.some((v) => v.severity === "error");
123
+ const resultStatus = hasErrors ? "fail" : "pass";
124
+
125
+ if (outputFormat === "pretty") {
126
+ if (violations.length === 0) {
127
+ logger.success(`All ${filesToValidate.length} ADR(s) passed validation.`);
128
+ } else {
129
+ logger.section(`ADR Validation (${filesToValidate.length} file(s))`);
130
+ for (const v of violations) {
131
+ const prefix = v.severity === "error" ? "ERROR" : "WARN";
132
+ logger[v.severity === "error" ? "error" : "warn"](
133
+ `[${prefix}] ${v.adrId} ${v.rule}: ${v.message}`,
134
+ );
135
+ }
136
+ const errorCount = violations.filter((v) => v.severity === "error").length;
137
+ const warnCount = violations.filter((v) => v.severity === "warning").length;
138
+ if (errorCount > 0) {
139
+ logger.error(`${errorCount} error(s), ${warnCount} warning(s)`);
140
+ } else {
141
+ logger.warn(`${warnCount} warning(s), 0 errors — passed`);
142
+ }
143
+ }
144
+ }
145
+
146
+ return {
147
+ data: {
148
+ command: "adr.validate",
149
+ status: resultStatus,
150
+ count: filesToValidate.length,
151
+ violations,
152
+ },
153
+ exitCode: hasErrors ? 1 : 0,
154
+ summary: hasErrors
155
+ ? `${violations.filter((v) => v.severity === "error").length} error(s) found`
156
+ : `All ${filesToValidate.length} ADR(s) passed validation`,
157
+ };
158
+ }
159
+
160
+ async function validateSingleAdr(
161
+ fileName: string,
162
+ parsed: ParsedAdr,
163
+ allParsed: Map<string, { fileName: string; parsed: ParsedAdr }>,
164
+ knownRfcIds: Set<string>,
165
+ seenIds: Map<string, string>,
166
+ knownKeys: Set<string>,
167
+ workspaceRoot: string,
168
+ addViolation: (
169
+ adrId: string,
170
+ file: string,
171
+ rule: string,
172
+ message: string,
173
+ severity?: "error" | "warning",
174
+ ) => void,
175
+ ): Promise<void> {
176
+ const fm = parsed.frontmatter;
177
+ const body = parsed.body;
178
+ const relFile = path.join(ADR_DIR, fileName);
179
+ const adrId = String(fm["id"] ?? "UNKNOWN");
180
+
181
+ if (!ADR_ID_PATTERN.test(adrId)) {
182
+ addViolation(adrId, relFile, "AV-01", `id "${adrId}" does not match format ADR-XXXX`);
183
+ }
184
+
185
+ // ADR-DIR-01: directory structure convention (RFC-0722)
186
+ // Warn when ADR files are found in subdirectories other than archive/
187
+ const slashIdx = fileName.indexOf("/");
188
+ if (slashIdx > 0) {
189
+ const subDir = fileName.slice(0, slashIdx);
190
+ if (subDir !== "archive") {
191
+ addViolation(
192
+ adrId,
193
+ relFile,
194
+ "ADR-DIR-01",
195
+ `${relFile} is in an unsanctioned subdirectory. Only archive/ is allowed. Move the file to docs/adrs/ root or write an ADR to formalize the subdirectory.`,
196
+ "warning",
197
+ );
198
+ }
199
+ }
200
+
201
+ const prevFile = seenIds.get(adrId);
202
+ if (prevFile) {
203
+ addViolation(adrId, relFile, "AV-02", `Duplicate id "${adrId}" — also in ${prevFile}`);
204
+ } else {
205
+ seenIds.set(adrId, relFile);
206
+ }
207
+
208
+ const status = String(fm["status"] ?? "");
209
+ if (!ADR_STATUSES.includes(status as AdrStatus)) {
210
+ addViolation(
211
+ adrId,
212
+ relFile,
213
+ "AV-03",
214
+ `Invalid status "${status}". Must be one of: ${ADR_STATUSES.join(", ")}`,
215
+ );
216
+ }
217
+
218
+ const scope = String(fm["scope"] ?? "");
219
+ if (!ADR_SCOPES.includes(scope as AdrScope)) {
220
+ addViolation(
221
+ adrId,
222
+ relFile,
223
+ "AV-04",
224
+ `Invalid scope "${scope}". Must be one of: ${ADR_SCOPES.join(", ")}`,
225
+ );
226
+ }
227
+
228
+ const decider = String(fm["decider"] ?? "");
229
+ if (!decider) {
230
+ addViolation(adrId, relFile, "AV-05", "decider must be a non-empty string");
231
+ }
232
+
233
+ const createdAt = String(fm["createdAt"] ?? "");
234
+ const updatedAt = String(fm["updatedAt"] ?? "");
235
+ const datePattern = /^\d{4}-\d{2}-\d{2}$/;
236
+ if (!datePattern.test(createdAt)) {
237
+ addViolation(
238
+ adrId,
239
+ relFile,
240
+ "AV-06",
241
+ `createdAt "${createdAt}" is not a valid ISO 8601 date (YYYY-MM-DD)`,
242
+ );
243
+ }
244
+ if (!datePattern.test(updatedAt)) {
245
+ addViolation(
246
+ adrId,
247
+ relFile,
248
+ "AV-06",
249
+ `updatedAt "${updatedAt}" is not a valid ISO 8601 date (YYYY-MM-DD)`,
250
+ );
251
+ }
252
+
253
+ const supersedes = fm["supersedes"];
254
+ if (Array.isArray(supersedes)) {
255
+ for (const ref of supersedes) {
256
+ const refStr = String(ref);
257
+ if (!refStr) continue;
258
+ if (!allParsed.has(refStr)) {
259
+ addViolation(
260
+ adrId,
261
+ relFile,
262
+ "AV-07",
263
+ `supersedes "${refStr}" does not match any existing ADR`,
264
+ );
265
+ } else {
266
+ const otherBy = String(allParsed.get(refStr)!.parsed.frontmatter["supersededBy"] ?? "");
267
+ if (otherBy !== adrId) {
268
+ addViolation(
269
+ adrId,
270
+ relFile,
271
+ "AV-08",
272
+ `${adrId}.supersedes includes ${refStr}, but ${refStr}.supersededBy is "${otherBy || "(empty)"}" (expected ${adrId})`,
273
+ "warning",
274
+ );
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ const supersededByRaw = fm["supersededBy"];
281
+ const supersededByList: string[] = Array.isArray(supersededByRaw)
282
+ ? supersededByRaw.map((s) => String(s))
283
+ : supersededByRaw
284
+ ? [String(supersededByRaw)]
285
+ : [];
286
+ for (const supersededBy of supersededByList) {
287
+ if (/^ADR-\d{4}$/.test(supersededBy) && !allParsed.has(supersededBy)) {
288
+ addViolation(
289
+ adrId,
290
+ relFile,
291
+ "AV-09",
292
+ `supersededBy "${supersededBy}" does not match any existing ADR`,
293
+ );
294
+ } else if (/^RFC-\d{4}$/.test(supersededBy) && !knownRfcIds.has(supersededBy)) {
295
+ addViolation(
296
+ adrId,
297
+ relFile,
298
+ "AV-09",
299
+ `supersededBy "${supersededBy}" does not match any existing RFC`,
300
+ );
301
+ } else if (!/^(ADR|RFC)-\d{4}$/.test(supersededBy)) {
302
+ addViolation(
303
+ adrId,
304
+ relFile,
305
+ "AV-09",
306
+ `supersededBy "${supersededBy}" is not a recognized ADR/RFC id`,
307
+ );
308
+ }
309
+ }
310
+ const supersededBy = supersededByList[0];
311
+
312
+ if (status === "superseded" && !supersededBy) {
313
+ addViolation(adrId, relFile, "AV-10", `status is "superseded" but supersededBy is empty`);
314
+ }
315
+
316
+ const related = fm["related"];
317
+ if (Array.isArray(related)) {
318
+ for (const ref of related) {
319
+ const refStr = String(ref).trim();
320
+ if (!refStr) continue;
321
+ if (/^ADR-\d{4}$/.test(refStr)) {
322
+ if (!allParsed.has(refStr)) {
323
+ addViolation(
324
+ adrId,
325
+ relFile,
326
+ "AV-11",
327
+ `related "${refStr}" does not match any existing ADR`,
328
+ "warning",
329
+ );
330
+ }
331
+ } else if (!/^(RFC|DNA|AP)-\d+$/.test(refStr)) {
332
+ addViolation(
333
+ adrId,
334
+ relFile,
335
+ "AV-11",
336
+ `related "${refStr}" is not a recognized RFC/ADR/DNA/AP reference`,
337
+ "warning",
338
+ );
339
+ }
340
+ }
341
+ }
342
+
343
+ const headings = [...body.matchAll(/^##\s+(.+)$/gm)].map((m) => m[1]!.trim());
344
+ for (const section of ADR_REQUIRED_SECTIONS) {
345
+ if (!headings.includes(section)) {
346
+ addViolation(adrId, relFile, "AV-12", `Missing required section "## ${section}"`);
347
+ }
348
+ }
349
+
350
+ const titleStr = String(fm["title"] ?? "");
351
+ const headingMatch = body.match(/^#\s+(?:ADR-\d{4}:\s*)?(.+)$/m);
352
+ if (headingMatch && titleStr) {
353
+ const headingTitle = headingMatch[1]!.trim();
354
+ if (headingTitle !== titleStr) {
355
+ addViolation(
356
+ adrId,
357
+ relFile,
358
+ "AV-13",
359
+ `Frontmatter title "${titleStr}" does not match body heading "${headingTitle}"`,
360
+ "warning",
361
+ );
362
+ }
363
+ }
364
+
365
+ const lowerAdrId = adrId.toLowerCase();
366
+ const fileBasename = fileName.split("/").pop()!.split("\\").pop()!;
367
+ if (!fileBasename.startsWith(lowerAdrId)) {
368
+ addViolation(
369
+ adrId,
370
+ relFile,
371
+ "AV-14",
372
+ `Filename "${fileBasename}" must start with lowercase "${lowerAdrId}"`,
373
+ );
374
+ } else if (!/^[a-z0-9-]+\.md$/.test(fileBasename)) {
375
+ addViolation(
376
+ adrId,
377
+ relFile,
378
+ "AV-14",
379
+ `Filename "${fileName}" is not lowercase kebab-case (only a-z, 0-9, hyphens allowed)`,
380
+ );
381
+ }
382
+
383
+ for (const key of Object.keys(fm)) {
384
+ if (!knownKeys.has(key)) {
385
+ addViolation(
386
+ adrId,
387
+ relFile,
388
+ "AV-15",
389
+ `unknown frontmatter key "${key}" (not in the ADR schema)`,
390
+ "warning",
391
+ );
392
+ }
393
+ }
394
+
395
+ // AV-16: implementation commit drift detection (RFC-0625)
396
+ // Warns when implement: ADR-XXXX commits exist in git history since createdAt
397
+ // but the ADR status is not yet "implemented".
398
+ if (status !== "implemented") {
399
+ const datePattern = /^\d{4}-\d{2}-\d{2}$/;
400
+ if (datePattern.test(createdAt)) {
401
+ const log = await execGitLog(workspaceRoot, ["log", `--since=${createdAt}`, "--oneline"]);
402
+ if (log) {
403
+ const pattern = new RegExp(`implement:\\s+${adrId}\\b`, "i");
404
+ const matchingLines = log.split("\n").filter((line) => pattern.test(line.trim()));
405
+ if (matchingLines.length > 0) {
406
+ addViolation(
407
+ adrId,
408
+ relFile,
409
+ "AV-16",
410
+ `${adrId} has ${matchingLines.length} implement: commit(s) in git history since ${createdAt} but status is still "${status}". Run: site-kernel run adr.implement.stamp --id ${adrId} --implementation-commit <sha> to transition to implemented.`,
411
+ "warning",
412
+ );
413
+ }
414
+ }
415
+ }
416
+ }
417
+ }
418
+
419
+ function execGitLog(workspaceRoot: string, args: string[]): Promise<string> {
420
+ return new Promise((resolve) => {
421
+ execFile("git", args, { cwd: workspaceRoot, timeout: 10000 }, (err, stdout) => {
422
+ if (err) {
423
+ resolve("");
424
+ return;
425
+ }
426
+ resolve(stdout.trim());
427
+ });
428
+ });
429
+ }
@@ -0,0 +1,33 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Barrel export for the ADR domain — types, constants, module, and handlers.</purpose>
4
+ <non-goals>
5
+ <item>Do not implement ADR logic here; delegate to handlers/ and adr.module.ts.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0366: expose adrModule and ADR types from the ADR 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
+ export { forgeAdrModule } from "./adr.module.ts";
15
+ export { runAdrList, runAdrCreate } from "./handlers/list-create.ts";
16
+ export { runAdrValidate } from "./handlers/validate.ts";
17
+ export { runAdrArchive } from "./handlers/archive.ts";
18
+ export type {
19
+ AdrArchiveResult,
20
+ ArchiveMove as AdrArchiveMove,
21
+ ArchiveSkip as AdrArchiveSkip,
22
+ } from "./handlers/archive.ts";
23
+ export type {
24
+ AdrStatus,
25
+ AdrScope,
26
+ AdrFrontmatter,
27
+ AdrListEntry,
28
+ AdrListResult,
29
+ AdrCreateResult,
30
+ AdrValidationViolation,
31
+ AdrValidationResult,
32
+ } from "./types.ts";
33
+ export { ADR_STATUSES, ADR_SCOPES, ADR_DIR, ADR_TEMPLATE_FILE, ADR_ID_PATTERN } from "./types.ts";