@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,420 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for forge config module — loadForgeConfig, resolveForgeRoot, defaultForgeConfig.</purpose>
4
+ </MODULE_CONTRACT>
5
+ <CHANGE_SUMMARY>
6
+ <item>RFC-0391: initial config tests.</item>
7
+ <item>RFC-0538: added compass binding section tests.</item>
8
+ <item>RFC-0540: added CLI binding defaults, pm-runner, and package-manager-aware defaultForgeConfig tests.</item>
9
+ </CHANGE_SUMMARY>
10
+ */
11
+
12
+ import { test, expect, beforeEach, afterEach } from "vitest";
13
+ import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+ import {
17
+ loadForgeConfig,
18
+ resolveForgeRoot,
19
+ defaultForgeConfig,
20
+ forgeConfigSchema,
21
+ forgeBindingsSchema,
22
+ resolveBinding,
23
+ FORGE_CLI_BINDING_DEFAULTS,
24
+ resolvePmRunner,
25
+ applyCliBindingDefaults,
26
+ type ForgeConfig,
27
+ } from "../config/forge-config.ts";
28
+
29
+ let tempDir: string;
30
+
31
+ beforeEach(async () => {
32
+ tempDir = await mkdtemp(join(tmpdir(), "forge-config-test-"));
33
+ });
34
+
35
+ afterEach(async () => {
36
+ await rm(tempDir, { recursive: true, force: true });
37
+ });
38
+
39
+ test("defaultForgeConfig produces valid config", () => {
40
+ const config = defaultForgeConfig("my-project");
41
+ expect(config.schema).toBe("forge/config@1");
42
+ expect(config.project.name).toBe("my-project");
43
+ expect(config.project.packageManager).toBe("pnpm");
44
+ expect(config.paths.rfcsDir).toBe("docs/rfcs");
45
+ expect(config.paths.plansDir).toBe("docs/plans");
46
+ expect(config.paths.auditsDir).toBe("docs/audits");
47
+ });
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // CLI binding defaults tests (RFC-0540)
51
+ // ---------------------------------------------------------------------------
52
+
53
+ test("resolvePmRunner maps known package managers", () => {
54
+ expect(resolvePmRunner("pnpm")).toBe("pnpm exec");
55
+ expect(resolvePmRunner("npm")).toBe("npx");
56
+ expect(resolvePmRunner("yarn")).toBe("yarn exec");
57
+ expect(resolvePmRunner("bun")).toBe("bunx");
58
+ expect(resolvePmRunner("none")).toBe("npx");
59
+ });
60
+
61
+ test("resolvePmRunner falls back to npx for unknown", () => {
62
+ expect(resolvePmRunner("unknown")).toBe("npx");
63
+ expect(resolvePmRunner("")).toBe("npx");
64
+ });
65
+
66
+ test("FORGE_CLI_BINDING_DEFAULTS has 5 entries with correct keys", () => {
67
+ expect(FORGE_CLI_BINDING_DEFAULTS).toHaveLength(5);
68
+ const keys = FORGE_CLI_BINDING_DEFAULTS.map((e) => e.key);
69
+ expect(keys).toContain("commands.validateRfc");
70
+ expect(keys).toContain("commands.validateAdr");
71
+ expect(keys).toContain("commands.implementStamp");
72
+ expect(keys).toContain("commands.specValidate");
73
+ expect(keys).toContain("commands.sessionSave");
74
+ });
75
+
76
+ test("defaultForgeConfig with pnpm produces forge-CLI bindings with pnpm exec prefix", () => {
77
+ const config = defaultForgeConfig("test", "pnpm");
78
+ expect(config.bindings?.commands.validateRfc).toBe(
79
+ "pnpm exec forge rfc.validate --id {id} --json",
80
+ );
81
+ expect(config.bindings?.commands.validateAdr).toBe(
82
+ "pnpm exec forge adr.validate --id {id} --json",
83
+ );
84
+ expect(config.bindings?.commands.implementStamp).toBe(
85
+ "pnpm exec forge rfc.implement.stamp --id {id} --implementation-commit {commit}",
86
+ );
87
+ expect(config.bindings?.commands.specValidate).toBe(
88
+ "pnpm exec forge spec.validate --spec={id} --json",
89
+ );
90
+ });
91
+
92
+ test("defaultForgeConfig with npm produces npx-prefixed bindings", () => {
93
+ const config = defaultForgeConfig("test", "npm");
94
+ expect(config.bindings?.commands.validateRfc).toBe("npx forge rfc.validate --id {id} --json");
95
+ expect(config.bindings?.commands.implementStamp).toBe(
96
+ "npx forge rfc.implement.stamp --id {id} --implementation-commit {commit}",
97
+ );
98
+ });
99
+
100
+ test("defaultForgeConfig with bun produces bunx-prefixed bindings", () => {
101
+ const config = defaultForgeConfig("test", "bun");
102
+ expect(config.bindings?.commands.validateRfc).toBe("bunx forge rfc.validate --id {id} --json");
103
+ });
104
+
105
+ test("defaultForgeConfig without packageManager defaults to pnpm", () => {
106
+ const config = defaultForgeConfig("test");
107
+ expect(config.project.packageManager).toBe("pnpm");
108
+ expect(config.bindings?.commands.validateRfc).toBe(
109
+ "pnpm exec forge rfc.validate --id {id} --json",
110
+ );
111
+ });
112
+
113
+ test("defaultForgeConfig keeps stack-dependent bindings null", () => {
114
+ const config = defaultForgeConfig("test", "pnpm");
115
+ expect(config.bindings?.commands.typecheck).toBeNull();
116
+ expect(config.bindings?.commands.test).toBeNull();
117
+ expect(config.bindings?.commands.scopedBuild).toBeNull();
118
+ });
119
+
120
+ test("defaultForgeConfig with CLI defaults passes schema validation", () => {
121
+ const config = defaultForgeConfig("test", "pnpm");
122
+ const result = forgeConfigSchema.safeParse(config);
123
+ expect(result.success).toBe(true);
124
+ });
125
+
126
+ test("forgeBindingsSchema accepts config with implementStamp field", () => {
127
+ const config = defaultForgeConfig("test", "pnpm");
128
+ const result = forgeBindingsSchema.safeParse(config.bindings);
129
+ expect(result.success).toBe(true);
130
+ });
131
+
132
+ test("applyCliBindingDefaults returns forge-CLI bindings non-null and stack bindings null", () => {
133
+ const commands = applyCliBindingDefaults("pnpm");
134
+ expect(commands.validateRfc).not.toBeNull();
135
+ expect(commands.validateAdr).not.toBeNull();
136
+ expect(commands.implementStamp).not.toBeNull();
137
+ expect(commands.specValidate).not.toBeNull();
138
+ expect(commands.sessionSave).not.toBeNull();
139
+ expect(commands.typecheck).toBeNull();
140
+ expect(commands.test).toBeNull();
141
+ expect(commands.scopedBuild).toBeNull();
142
+ });
143
+
144
+ test("defaultForgeConfig passes schema validation", () => {
145
+ const config = defaultForgeConfig("test-project");
146
+ const result = forgeConfigSchema.safeParse(config);
147
+ expect(result.success).toBe(true);
148
+ });
149
+
150
+ test("loadForgeConfig parses valid forge.yaml", async () => {
151
+ const yaml = `
152
+ schema: forge/config@1
153
+ project:
154
+ name: test-app
155
+ stack: [typescript]
156
+ packageManager: pnpm
157
+ paths:
158
+ rfcsDir: docs/rfcs
159
+ adrsDir: docs/adrs
160
+ plansDir: docs/plans
161
+ auditsDir: docs/audits
162
+ specsDir: docs/specs
163
+ skillsDir: .agents/skills
164
+ `;
165
+ await writeFile(join(tempDir, "forge.yaml"), yaml, "utf8");
166
+ const config = loadForgeConfig(tempDir);
167
+ expect(config.project.name).toBe("test-app");
168
+ expect(config.project.stack).toEqual(["typescript"]);
169
+ expect(config.project.packageManager).toBe("pnpm");
170
+ });
171
+
172
+ test("loadForgeConfig throws on missing forge.yaml", () => {
173
+ expect(() => loadForgeConfig(tempDir)).toThrow("forge.yaml not found");
174
+ });
175
+
176
+ test("loadForgeConfig throws on invalid schema", async () => {
177
+ const yaml = `
178
+ schema: forge/config@1
179
+ project:
180
+ name: ""
181
+ `;
182
+ await writeFile(join(tempDir, "forge.yaml"), yaml, "utf8");
183
+ expect(() => loadForgeConfig(tempDir)).toThrow("failed schema validation");
184
+ });
185
+
186
+ test("resolveForgeRoot finds packages/forge in monorepo layout", async () => {
187
+ await mkdir(join(tempDir, "packages", "forge"), { recursive: true });
188
+ await writeFile(
189
+ join(tempDir, "packages", "forge", "package.json"),
190
+ '{"name":"@warpgogol/forge"}',
191
+ "utf8",
192
+ );
193
+ const root = resolveForgeRoot(tempDir);
194
+ expect(root).toBe(join(tempDir, "packages", "forge"));
195
+ });
196
+
197
+ test("resolveForgeRoot finds node_modules/@warpgogol/forge in npm layout", async () => {
198
+ await mkdir(join(tempDir, "node_modules", "@warpgogol", "forge"), { recursive: true });
199
+ await writeFile(
200
+ join(tempDir, "node_modules", "@warpgogol", "forge", "package.json"),
201
+ '{"name":"@warpgogol/forge"}',
202
+ "utf8",
203
+ );
204
+ const root = resolveForgeRoot(tempDir);
205
+ expect(root).toBe(join(tempDir, "node_modules", "@warpgogol", "forge"));
206
+ });
207
+
208
+ test("resolveForgeRoot throws when forge not found", () => {
209
+ expect(() => resolveForgeRoot(tempDir)).toThrow("Could not resolve forge root");
210
+ });
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // Bindings tests (RFC-0393)
214
+ // ---------------------------------------------------------------------------
215
+
216
+ const configWithBindings: ForgeConfig = {
217
+ schema: "forge/config@1",
218
+ project: { name: "test", stack: [], packageManager: "pnpm" },
219
+ paths: {
220
+ rfcsDir: "docs/rfcs",
221
+ adrsDir: "docs/adrs",
222
+ plansDir: "docs/plans",
223
+ auditsDir: "docs/audits",
224
+ specsDir: "docs/specs",
225
+ skillsDir: ".agents/skills",
226
+ sessionsDir: "docs/sessions",
227
+ },
228
+ bindings: {
229
+ schema: "forge/bindings@1",
230
+ commands: {
231
+ validateRfc: "pnpm exec werkstatt run rfc.validate --id {id} --json",
232
+ validateAdr: null,
233
+ implementStamp: null,
234
+ typecheck: "pnpm --filter {workspace} run build:check",
235
+ test: "pnpm --filter {workspace} run test",
236
+ scopedBuild: null,
237
+ specValidate: null,
238
+ sessionSave: null,
239
+ validate: null,
240
+ produce: null,
241
+ verify: null,
242
+ preview: null,
243
+ lint: null,
244
+ },
245
+ paths: {
246
+ invariantsFile: "docs/architecture-dna.md",
247
+ compassDocs: ["docs/requirements.xml", "docs/technology.xml"],
248
+ reviewsDir: "docs/reviews",
249
+ handoffsDir: null,
250
+ sessionsDir: "docs/sessions",
251
+ },
252
+ terminology: { invariants: "DNA" },
253
+ },
254
+ };
255
+
256
+ test("forgeBindingsSchema validates a valid bindings object", () => {
257
+ const result = forgeBindingsSchema.safeParse(configWithBindings.bindings);
258
+ expect(result.success).toBe(true);
259
+ });
260
+
261
+ test("forgeBindingsSchema rejects missing schema field", () => {
262
+ const bad = { ...configWithBindings.bindings!, schema: "wrong" };
263
+ const result = forgeBindingsSchema.safeParse(bad);
264
+ expect(result.success).toBe(false);
265
+ });
266
+
267
+ test("resolveBinding returns a string for a valid key", () => {
268
+ const result = resolveBinding(configWithBindings, "commands.validateRfc");
269
+ expect(result).toBe("pnpm exec werkstatt run rfc.validate --id {id} --json");
270
+ });
271
+
272
+ test("resolveBinding returns null for an explicitly null binding", () => {
273
+ const result = resolveBinding(configWithBindings, "commands.validateAdr");
274
+ expect(result).toBeNull();
275
+ });
276
+
277
+ test("resolveBinding returns an array for compassDocs", () => {
278
+ const result = resolveBinding(configWithBindings, "paths.compassDocs");
279
+ expect(result).toEqual(["docs/requirements.xml", "docs/technology.xml"]);
280
+ });
281
+
282
+ test("resolveBinding returns null when bindings section is absent", () => {
283
+ const configWithoutBindings: ForgeConfig = {
284
+ ...configWithBindings,
285
+ bindings: undefined,
286
+ };
287
+ const result = resolveBinding(configWithoutBindings, "commands.validateRfc");
288
+ expect(result).toBeNull();
289
+ });
290
+
291
+ test("resolveBinding returns null for an unknown key", () => {
292
+ const result = resolveBinding(configWithBindings, "commands.nonexistent");
293
+ expect(result).toBeNull();
294
+ });
295
+
296
+ test("resolveBinding substitutes placeholders", () => {
297
+ const result = resolveBinding(configWithBindings, "commands.validateRfc", { id: "RFC-0393" });
298
+ expect(result).toBe("pnpm exec werkstatt run rfc.validate --id RFC-0393 --json");
299
+ });
300
+
301
+ test("resolveBinding substitutes multiple placeholders", () => {
302
+ const result = resolveBinding(configWithBindings, "commands.typecheck", {
303
+ workspace: "@warpgogol/forge",
304
+ });
305
+ expect(result).toBe("pnpm --filter @warpgogol/forge run build:check");
306
+ });
307
+
308
+ test("loadForgeConfig parses forge.yaml with bindings", async () => {
309
+ const yaml = `
310
+ schema: forge/config@1
311
+ project:
312
+ name: test-app
313
+ stack: [typescript]
314
+ packageManager: pnpm
315
+ paths:
316
+ rfcsDir: docs/rfcs
317
+ adrsDir: docs/adrs
318
+ plansDir: docs/plans
319
+ auditsDir: docs/audits
320
+ specsDir: docs/specs
321
+ skillsDir: .agents/skills
322
+ bindings:
323
+ schema: forge/bindings@1
324
+ commands:
325
+ validateRfc: "pnpm exec werkstatt run rfc.validate --id {id} --json"
326
+ validateAdr: null
327
+ typecheck: "pnpm --filter {workspace} run build:check"
328
+ test: "pnpm --filter {workspace} run test"
329
+ scopedBuild: null
330
+ specValidate: null
331
+ paths:
332
+ invariantsFile: docs/architecture-dna.md
333
+ compassDocs: [docs/requirements.xml]
334
+ reviewsDir: docs/reviews
335
+ handoffsDir: null
336
+ terminology:
337
+ invariants: DNA
338
+ `;
339
+ await writeFile(join(tempDir, "forge.yaml"), yaml, "utf8");
340
+ const config = loadForgeConfig(tempDir);
341
+ expect(config.bindings).toBeDefined();
342
+ expect(config.bindings?.commands.validateRfc).toBe(
343
+ "pnpm exec werkstatt run rfc.validate --id {id} --json",
344
+ );
345
+ expect(config.bindings?.paths.invariantsFile).toBe("docs/architecture-dna.md");
346
+ });
347
+
348
+ // ---------------------------------------------------------------------------
349
+ // Compass binding tests (RFC-0538)
350
+ // ---------------------------------------------------------------------------
351
+
352
+ const configWithCompassBindings: ForgeConfig = {
353
+ ...configWithBindings,
354
+ bindings: {
355
+ ...configWithBindings.bindings!,
356
+ compass: {
357
+ fileExtensions: [".ts", ".astro"],
358
+ testPatterns: ["*.test.ts", "*.spec.ts", "**/test/**", "**/tests/**"],
359
+ },
360
+ },
361
+ };
362
+
363
+ test("forgeBindingsSchema validates compass section", () => {
364
+ const result = forgeBindingsSchema.safeParse(configWithCompassBindings.bindings);
365
+ expect(result.success).toBe(true);
366
+ });
367
+
368
+ test("resolveBinding returns array for compass.fileExtensions", () => {
369
+ const result = resolveBinding(configWithCompassBindings, "compass.fileExtensions");
370
+ expect(result).toEqual([".ts", ".astro"]);
371
+ });
372
+
373
+ test("resolveBinding returns array for compass.testPatterns", () => {
374
+ const result = resolveBinding(configWithCompassBindings, "compass.testPatterns");
375
+ expect(result).toEqual(["*.test.ts", "*.spec.ts", "**/test/**", "**/tests/**"]);
376
+ });
377
+
378
+ test("resolveBinding returns null when compass section is absent", () => {
379
+ const result = resolveBinding(configWithBindings, "compass.fileExtensions");
380
+ expect(result).toBeNull();
381
+ });
382
+
383
+ test("loadForgeConfig parses forge.yaml with compass bindings", async () => {
384
+ const yaml = `
385
+ schema: forge/config@1
386
+ project:
387
+ name: test-app
388
+ stack: [typescript]
389
+ packageManager: pnpm
390
+ paths:
391
+ rfcsDir: docs/rfcs
392
+ adrsDir: docs/adrs
393
+ plansDir: docs/plans
394
+ auditsDir: docs/audits
395
+ specsDir: docs/specs
396
+ skillsDir: .agents/skills
397
+ bindings:
398
+ schema: forge/bindings@1
399
+ commands:
400
+ validateRfc: null
401
+ validateAdr: null
402
+ typecheck: null
403
+ test: null
404
+ scopedBuild: null
405
+ specValidate: null
406
+ paths:
407
+ invariantsFile: docs/architecture-dna.md
408
+ compassDocs: []
409
+ reviewsDir: null
410
+ handoffsDir: null
411
+ compass:
412
+ fileExtensions: [".ts", ".astro"]
413
+ testPatterns: ["*.test.ts", "*.spec.ts"]
414
+ `;
415
+ await writeFile(join(tempDir, "forge.yaml"), yaml, "utf8");
416
+ const config = loadForgeConfig(tempDir);
417
+ expect(config.bindings?.compass).toBeDefined();
418
+ expect(config.bindings?.compass?.fileExtensions).toEqual([".ts", ".astro"]);
419
+ expect(config.bindings?.compass?.testPatterns).toEqual(["*.test.ts", "*.spec.ts"]);
420
+ });
@@ -0,0 +1,96 @@
1
+ import { test, expect, describe } from "vitest";
2
+ import { writeFileAtomic, __setRenameImplForTests } from "../utils/fs-atomic.ts";
3
+ import { join } from "node:path";
4
+ import { mkdtemp, rm, readFile } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+
7
+ async function makeTempDir(): Promise<string> {
8
+ return mkdtemp(join(tmpdir(), "forge-atomic-"));
9
+ }
10
+
11
+ describe("writeFileAtomic", () => {
12
+ test("writes content to the target path", async () => {
13
+ const dir = await makeTempDir();
14
+ try {
15
+ const filePath = join(dir, "output.txt");
16
+ await writeFileAtomic(filePath, "hello world");
17
+ const content = await readFile(filePath, "utf8");
18
+ expect(content).toBe("hello world");
19
+ } finally {
20
+ await rm(dir, { recursive: true, force: true });
21
+ }
22
+ });
23
+
24
+ test("overwrites existing file", async () => {
25
+ const dir = await makeTempDir();
26
+ try {
27
+ const filePath = join(dir, "output.txt");
28
+ await writeFileAtomic(filePath, "first");
29
+ await writeFileAtomic(filePath, "second");
30
+ expect(await readFile(filePath, "utf8")).toBe("second");
31
+ } finally {
32
+ await rm(dir, { recursive: true, force: true });
33
+ }
34
+ });
35
+
36
+ test("accepts Uint8Array content", async () => {
37
+ const dir = await makeTempDir();
38
+ try {
39
+ const filePath = join(dir, "output.bin");
40
+ const data = new Uint8Array([1, 2, 3]);
41
+ await writeFileAtomic(filePath, data);
42
+ const buf = await readFile(filePath);
43
+ expect(new Uint8Array(buf)).toEqual(data);
44
+ } finally {
45
+ await rm(dir, { recursive: true, force: true });
46
+ }
47
+ });
48
+
49
+ test("retries on EPERM then succeeds", async () => {
50
+ const dir = await makeTempDir();
51
+ try {
52
+ const filePath = join(dir, "output.txt");
53
+ let calls = 0;
54
+ const fakeRename = async (src: string, dest: string) => {
55
+ calls++;
56
+ if (calls < 3) {
57
+ const err = new Error("EPERM") as NodeJS.ErrnoException;
58
+ err.code = "EPERM";
59
+ throw err;
60
+ }
61
+ const { rename: realRename } = await import("node:fs/promises");
62
+ return realRename(src, dest);
63
+ };
64
+ __setRenameImplForTests(fakeRename as never);
65
+ try {
66
+ await writeFileAtomic(filePath, "retried", { retries: 5 });
67
+ expect(await readFile(filePath, "utf8")).toBe("retried");
68
+ expect(calls).toBe(3);
69
+ } finally {
70
+ __setRenameImplForTests(undefined);
71
+ }
72
+ } finally {
73
+ await rm(dir, { recursive: true, force: true });
74
+ }
75
+ });
76
+
77
+ test("throws non-retryable error immediately", async () => {
78
+ const dir = await makeTempDir();
79
+ try {
80
+ const filePath = join(dir, "output.txt");
81
+ const fakeRename = async () => {
82
+ const err = new Error("ENOENT") as NodeJS.ErrnoException;
83
+ err.code = "ENOENT";
84
+ throw err;
85
+ };
86
+ __setRenameImplForTests(fakeRename as never);
87
+ try {
88
+ await expect(writeFileAtomic(filePath, "fail")).rejects.toThrow("ENOENT");
89
+ } finally {
90
+ __setRenameImplForTests(undefined);
91
+ }
92
+ } finally {
93
+ await rm(dir, { recursive: true, force: true });
94
+ }
95
+ });
96
+ });
@@ -0,0 +1,103 @@
1
+ import { test, expect, describe } from "vitest";
2
+ import { collectFiles, fileExists } from "../utils/fs.ts";
3
+ import { join } from "node:path";
4
+ import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+
7
+ async function makeTempDir(): Promise<string> {
8
+ return mkdtemp(join(tmpdir(), "forge-test-"));
9
+ }
10
+
11
+ describe("fileExists", () => {
12
+ test("returns true for existing file", async () => {
13
+ const dir = await makeTempDir();
14
+ try {
15
+ const filePath = join(dir, "test.txt");
16
+ await writeFile(filePath, "hello");
17
+ expect(await fileExists(filePath)).toBe(true);
18
+ } finally {
19
+ await rm(dir, { recursive: true, force: true });
20
+ }
21
+ });
22
+
23
+ test("returns false for missing file", async () => {
24
+ expect(await fileExists(join(tmpdir(), "nonexistent-12345.txt"))).toBe(false);
25
+ });
26
+
27
+ test("returns true for a directory", async () => {
28
+ const dir = await makeTempDir();
29
+ try {
30
+ expect(await fileExists(dir)).toBe(true);
31
+ } finally {
32
+ await rm(dir, { recursive: true, force: true });
33
+ }
34
+ });
35
+ });
36
+
37
+ describe("collectFiles", () => {
38
+ test("collects all files recursively", async () => {
39
+ const dir = await makeTempDir();
40
+ try {
41
+ await mkdir(join(dir, "sub"));
42
+ await writeFile(join(dir, "a.ts"), "");
43
+ await writeFile(join(dir, "sub", "b.ts"), "");
44
+ await writeFile(join(dir, "c.md"), "");
45
+ const files = await collectFiles(dir);
46
+ expect(files.length).toBe(3);
47
+ } finally {
48
+ await rm(dir, { recursive: true, force: true });
49
+ }
50
+ });
51
+
52
+ test("filters by extension", async () => {
53
+ const dir = await makeTempDir();
54
+ try {
55
+ await writeFile(join(dir, "a.ts"), "");
56
+ await writeFile(join(dir, "b.md"), "");
57
+ await writeFile(join(dir, "c.ts"), "");
58
+ const files = await collectFiles(dir, { extensions: [".ts"] });
59
+ expect(files.length).toBe(2);
60
+ expect(files.every((f) => f.endsWith(".ts"))).toBe(true);
61
+ } finally {
62
+ await rm(dir, { recursive: true, force: true });
63
+ }
64
+ });
65
+
66
+ test("skips ignored names", async () => {
67
+ const dir = await makeTempDir();
68
+ try {
69
+ await mkdir(join(dir, "old-stuff"));
70
+ await writeFile(join(dir, "old-stuff", "old.ts"), "");
71
+ await writeFile(join(dir, "-draft.ts"), "");
72
+ await writeFile(join(dir, "keep.ts"), "");
73
+ const files = await collectFiles(dir);
74
+ const names = files.map((f) => f.split(/[/\\]/).pop());
75
+ expect(names).toContain("keep.ts");
76
+ expect(names).not.toContain("old.ts");
77
+ expect(names).not.toContain("-draft.ts");
78
+ } finally {
79
+ await rm(dir, { recursive: true, force: true });
80
+ }
81
+ });
82
+
83
+ test("includes directories when withDirs is true", async () => {
84
+ const dir = await makeTempDir();
85
+ try {
86
+ await mkdir(join(dir, "sub"));
87
+ await writeFile(join(dir, "a.ts"), "");
88
+ await writeFile(join(dir, "sub", "b.ts"), "");
89
+ const files = await collectFiles(dir, { withDirs: true });
90
+ const names = files.map((f) => f.split(/[/\\]/).pop());
91
+ expect(names).toContain("sub");
92
+ expect(names).toContain("a.ts");
93
+ expect(names).toContain("b.ts");
94
+ } finally {
95
+ await rm(dir, { recursive: true, force: true });
96
+ }
97
+ });
98
+
99
+ test("returns empty for nonexistent directory", async () => {
100
+ const files = await collectFiles(join(tmpdir(), "nonexistent-xyz-999"));
101
+ expect(files).toEqual([]);
102
+ });
103
+ });