@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,54 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Verify that template files referenced by source code are included in the npm package
4
+ (files array in package.json). Prevents shipping bugs where template files exist on disk
5
+ but are missing from the published package.</purpose>
6
+ </MODULE_CONTRACT>
7
+ <CHANGE_SUMMARY>
8
+ <item>Initial test: verify onboarding templates are in package.json files array.</item>
9
+ </CHANGE_SUMMARY>
10
+ */
11
+
12
+ import { test, expect } from "vitest";
13
+ import { readFileSync, readdirSync } from "node:fs";
14
+ import { join } from "node:path";
15
+
16
+ const PACKAGE_ROOT = join(import.meta.dirname, "..", "..");
17
+
18
+ function readPackageFiles(): string[] {
19
+ const pkg = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8")) as {
20
+ files?: string[];
21
+ };
22
+ return pkg.files ?? [];
23
+ }
24
+
25
+ test("package.json files array includes src/onboarding/templates/", () => {
26
+ const files = readPackageFiles();
27
+ const hasTemplates = files.some(
28
+ (f) => f === "src/onboarding/templates/" || f === "src/onboarding/templates/*" || f === "src/",
29
+ );
30
+ expect(hasTemplates).toBe(true);
31
+ });
32
+
33
+ test("all template files in src/onboarding/templates/ are covered by files array", () => {
34
+ const files = readPackageFiles();
35
+ const hasTemplatesGlob = files.some(
36
+ (f) => f === "src/onboarding/templates/" || f === "src/onboarding/templates/*" || f === "src/",
37
+ );
38
+ expect(hasTemplatesGlob).toBe(true);
39
+ });
40
+
41
+ test("root AGENTS.md templates exist and are readable", () => {
42
+ const templatesDir = join(PACKAGE_ROOT, "src", "onboarding", "templates");
43
+
44
+ const templateFiles = readdirSync(templatesDir);
45
+ expect(templateFiles).toContain("root-agents-business.md");
46
+ expect(templateFiles).toContain("root-agents-creative.md");
47
+ expect(templateFiles).toContain("behavioral-layer-core.md");
48
+ expect(templateFiles).toContain("behavioral-layer-extended.md");
49
+
50
+ for (const file of templateFiles) {
51
+ const content = readFileSync(join(templatesDir, file), "utf8");
52
+ expect(content.length).toBeGreaterThan(0);
53
+ }
54
+ });
@@ -0,0 +1,194 @@
1
+ import { test, expect, describe } from "vitest";
2
+ import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import {
6
+ loadPinnedManifest,
7
+ isPinned,
8
+ isIntraDirMove,
9
+ checkFilesForPinned,
10
+ PinnedManifestMalformedError,
11
+ } from "../../os/core/handlers/pinned-check.ts";
12
+ import type { PinnedManifest } from "../../os/core/handlers/pinned-types.ts";
13
+
14
+ async function makeTempDir(): Promise<string> {
15
+ return mkdtemp(join(tmpdir(), "forge-pinned-test-"));
16
+ }
17
+
18
+ describe("loadPinnedManifest", () => {
19
+ test("returns null when manifest does not exist", async () => {
20
+ const dir = await makeTempDir();
21
+ try {
22
+ const result = await loadPinnedManifest(dir);
23
+ expect(result).toBeNull();
24
+ } finally {
25
+ await rm(dir, { recursive: true, force: true });
26
+ }
27
+ });
28
+
29
+ test("loads valid manifest with entries", async () => {
30
+ const dir = await makeTempDir();
31
+ try {
32
+ await mkdir(join(dir, ".forge"), { recursive: true });
33
+ await writeFile(
34
+ join(dir, ".forge", "pinned.yaml"),
35
+ "pinned:\n - path: docs/rfcs/rfc-0000-template.md\n mode: freeze\n reason: RFC template\n - path: forge.yaml\n mode: protect\n reason: Forge config\n",
36
+ );
37
+ const result = await loadPinnedManifest(dir);
38
+ expect(result).not.toBeNull();
39
+ expect(result!.pinned).toHaveLength(2);
40
+ expect(result!.pinned[0]!.path).toBe("docs/rfcs/rfc-0000-template.md");
41
+ expect(result!.pinned[0]!.mode).toBe("freeze");
42
+ expect(result!.pinned[1]!.path).toBe("forge.yaml");
43
+ expect(result!.pinned[1]!.mode).toBe("protect");
44
+ } finally {
45
+ await rm(dir, { recursive: true, force: true });
46
+ }
47
+ });
48
+
49
+ test("throws PinnedManifestMalformedError for invalid YAML", async () => {
50
+ const dir = await makeTempDir();
51
+ try {
52
+ await mkdir(join(dir, ".forge"), { recursive: true });
53
+ await writeFile(join(dir, ".forge", "pinned.yaml"), "pinned: [invalid yaml {{{");
54
+ await expect(loadPinnedManifest(dir)).rejects.toThrow(PinnedManifestMalformedError);
55
+ } finally {
56
+ await rm(dir, { recursive: true, force: true });
57
+ }
58
+ });
59
+
60
+ test("throws PinnedManifestMalformedError when pinned array is missing", async () => {
61
+ const dir = await makeTempDir();
62
+ try {
63
+ await mkdir(join(dir, ".forge"), { recursive: true });
64
+ await writeFile(join(dir, ".forge", "pinned.yaml"), "foo: bar\n");
65
+ await expect(loadPinnedManifest(dir)).rejects.toThrow(PinnedManifestMalformedError);
66
+ } finally {
67
+ await rm(dir, { recursive: true, force: true });
68
+ }
69
+ });
70
+
71
+ test("throws PinnedManifestMalformedError for invalid mode", async () => {
72
+ const dir = await makeTempDir();
73
+ try {
74
+ await mkdir(join(dir, ".forge"), { recursive: true });
75
+ await writeFile(
76
+ join(dir, ".forge", "pinned.yaml"),
77
+ "pinned:\n - path: foo.md\n mode: invalid\n reason: test\n",
78
+ );
79
+ await expect(loadPinnedManifest(dir)).rejects.toThrow(PinnedManifestMalformedError);
80
+ } finally {
81
+ await rm(dir, { recursive: true, force: true });
82
+ }
83
+ });
84
+ });
85
+
86
+ describe("isPinned", () => {
87
+ const manifest: PinnedManifest = {
88
+ pinned: [
89
+ { path: "docs/rfcs/rfc-0000-template.md", mode: "freeze", reason: "template" },
90
+ { path: "docs/rfcs/", mode: "protect", reason: "RFC directory" },
91
+ ],
92
+ };
93
+
94
+ test("exact match returns entry", () => {
95
+ const entry = isPinned(manifest, "docs/rfcs/rfc-0000-template.md");
96
+ expect(entry).not.toBeNull();
97
+ expect(entry!.mode).toBe("freeze");
98
+ });
99
+
100
+ test("directory prefix match returns entry", () => {
101
+ const entry = isPinned(manifest, "docs/rfcs/rfc-0733.md");
102
+ expect(entry).not.toBeNull();
103
+ expect(entry!.mode).toBe("protect");
104
+ });
105
+
106
+ test("non-matching path returns null", () => {
107
+ const entry = isPinned(manifest, "docs/adrs/adr-0001.md");
108
+ expect(entry).toBeNull();
109
+ });
110
+
111
+ test("path outside directory prefix does not match", () => {
112
+ const entry = isPinned(manifest, "docs/rfcs-archive/old.md");
113
+ expect(entry).toBeNull();
114
+ });
115
+ });
116
+
117
+ describe("checkFilesForPinned", () => {
118
+ const manifest: PinnedManifest = {
119
+ pinned: [
120
+ { path: "docs/rfcs/rfc-0000-template.md", mode: "freeze", reason: "template" },
121
+ { path: "docs/rfcs/", mode: "protect", reason: "RFC directory" },
122
+ ],
123
+ };
124
+
125
+ test("returns violations for pinned files", () => {
126
+ const violations = checkFilesForPinned(manifest, [
127
+ { relPath: "docs/rfcs/rfc-0000-template.md", operation: "delete" },
128
+ { relPath: "docs/adrs/adr-0001.md", operation: "move" },
129
+ ]);
130
+ expect(violations).toHaveLength(1);
131
+ expect(violations[0]!.path).toBe("docs/rfcs/rfc-0000-template.md");
132
+ expect(violations[0]!.operation).toBe("delete");
133
+ });
134
+
135
+ test("returns empty array when no files are pinned", () => {
136
+ const violations = checkFilesForPinned(manifest, [
137
+ { relPath: "docs/adrs/adr-0001.md", operation: "move" },
138
+ { relPath: "README.md", operation: "modify" },
139
+ ]);
140
+ expect(violations).toHaveLength(0);
141
+ });
142
+ });
143
+
144
+ describe("isIntraDirMove", () => {
145
+ const manifest: PinnedManifest = {
146
+ pinned: [
147
+ { path: "docs/rfcs/rfc-0000-template.md", mode: "freeze", reason: "template" },
148
+ { path: "docs/rfcs/", mode: "protect", reason: "RFC directory" },
149
+ { path: "docs/adrs/", mode: "protect", reason: "ADR directory" },
150
+ ],
151
+ };
152
+
153
+ test("returns true when both source and dest are inside the same pinned dir", () => {
154
+ expect(
155
+ isIntraDirMove(
156
+ manifest,
157
+ "docs/rfcs/rfc-0076.md",
158
+ "docs/rfcs/archive/implemented/rfc-0076.md",
159
+ ),
160
+ ).toBe(true);
161
+ });
162
+
163
+ test("returns true for reverse move (archive → active)", () => {
164
+ expect(
165
+ isIntraDirMove(
166
+ manifest,
167
+ "docs/rfcs/archive/implemented/rfc-0076.md",
168
+ "docs/rfcs/rfc-0076.md",
169
+ ),
170
+ ).toBe(true);
171
+ });
172
+
173
+ test("returns false when dest is outside the pinned dir", () => {
174
+ expect(isIntraDirMove(manifest, "docs/rfcs/rfc-0076.md", "docs/other/rfc-0076.md")).toBe(false);
175
+ });
176
+
177
+ test("returns false when source is not in any pinned dir", () => {
178
+ expect(isIntraDirMove(manifest, "README.md", "docs/rfcs/README.md")).toBe(false);
179
+ });
180
+
181
+ test("returns false for file-pinned entries (not directory-pinned)", () => {
182
+ expect(
183
+ isIntraDirMove(
184
+ manifest,
185
+ "docs/rfcs/rfc-0000-template.md",
186
+ "docs/rfcs/archive/rfc-0000-template.md",
187
+ ),
188
+ ).toBe(true);
189
+ });
190
+
191
+ test("returns false when source and dest are in different pinned dirs", () => {
192
+ expect(isIntraDirMove(manifest, "docs/rfcs/rfc-0076.md", "docs/adrs/adr-0001.md")).toBe(false);
193
+ });
194
+ });
@@ -0,0 +1,126 @@
1
+ import { test, expect, describe } from "vitest";
2
+ import { mkdtemp, rm, mkdir, writeFile, readFile } from "node:fs/promises";
3
+ import { existsSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { runPinnedInit, DEFAULT_PINNED_ENTRIES } from "../../os/core/handlers/pinned-init.ts";
7
+ import type { ForgeCommandInput, ForgeRuntimeContext, ForgeFlagValue } from "../../src/types.ts";
8
+
9
+ async function makeTempDir(): Promise<string> {
10
+ return mkdtemp(join(tmpdir(), "forge-pinned-init-test-"));
11
+ }
12
+
13
+ function makeContext(workspaceRoot: string): ForgeRuntimeContext {
14
+ return {
15
+ workspaceRoot,
16
+ logger: {
17
+ section: () => {},
18
+ info: () => {},
19
+ warn: () => {},
20
+ error: () => {},
21
+ success: () => {},
22
+ },
23
+ dryRun: false,
24
+ outputFormat: "json",
25
+ };
26
+ }
27
+
28
+ function makeInput(flags: Record<string, ForgeFlagValue> = {}): ForgeCommandInput {
29
+ return { argv: [], flags };
30
+ }
31
+
32
+ describe("runPinnedInit", () => {
33
+ test("creates manifest with default entries", async () => {
34
+ const dir = await makeTempDir();
35
+ try {
36
+ const result = await runPinnedInit(makeInput(), makeContext(dir));
37
+ expect(result.data?.status).toBe("ok");
38
+ expect(result.data?.manifestAction).toBe("created");
39
+ expect(result.data?.entriesCount).toBe(DEFAULT_PINNED_ENTRIES.length);
40
+
41
+ const manifestExists = existsSync(join(dir, ".forge", "pinned.yaml"));
42
+ expect(manifestExists).toBe(true);
43
+ } finally {
44
+ await rm(dir, { recursive: true, force: true });
45
+ }
46
+ });
47
+
48
+ test("creates .gitignore with audit log entry", async () => {
49
+ const dir = await makeTempDir();
50
+ try {
51
+ await runPinnedInit(makeInput(), makeContext(dir));
52
+ const gitignoreContent = await readFile(join(dir, ".gitignore"), "utf8");
53
+ expect(gitignoreContent).toContain(".forge/pinned-audit.log");
54
+ } finally {
55
+ await rm(dir, { recursive: true, force: true });
56
+ }
57
+ });
58
+
59
+ test("appends audit log to existing .gitignore", async () => {
60
+ const dir = await makeTempDir();
61
+ try {
62
+ await writeFile(join(dir, ".gitignore"), "node_modules/\ndist/\n");
63
+ await runPinnedInit(makeInput(), makeContext(dir));
64
+ const gitignoreContent = await readFile(join(dir, ".gitignore"), "utf8");
65
+ expect(gitignoreContent).toContain("node_modules/");
66
+ expect(gitignoreContent).toContain(".forge/pinned-audit.log");
67
+ } finally {
68
+ await rm(dir, { recursive: true, force: true });
69
+ }
70
+ });
71
+
72
+ test("does not duplicate audit log entry on re-run", async () => {
73
+ const dir = await makeTempDir();
74
+ try {
75
+ await runPinnedInit(makeInput(), makeContext(dir));
76
+ await runPinnedInit(makeInput(), makeContext(dir));
77
+ const gitignoreContent = await readFile(join(dir, ".gitignore"), "utf8");
78
+ const matches = gitignoreContent.match(/\.forge\/pinned-audit\.log/g);
79
+ expect(matches).toHaveLength(1);
80
+ } finally {
81
+ await rm(dir, { recursive: true, force: true });
82
+ }
83
+ });
84
+
85
+ test("merges defaults with existing manifest entries", async () => {
86
+ const dir = await makeTempDir();
87
+ try {
88
+ await mkdir(join(dir, ".forge"), { recursive: true });
89
+ await writeFile(
90
+ join(dir, ".forge", "pinned.yaml"),
91
+ "pinned:\n - path: custom-file.md\n mode: protect\n reason: custom entry\n",
92
+ );
93
+
94
+ const result = await runPinnedInit(makeInput(), makeContext(dir));
95
+ expect(result.data?.manifestAction).toBe("merged");
96
+ expect(result.data?.entriesCount).toBe(DEFAULT_PINNED_ENTRIES.length + 1);
97
+
98
+ const content = await readFile(join(dir, ".forge", "pinned.yaml"), "utf8");
99
+ expect(content).toContain("custom-file.md");
100
+ } finally {
101
+ await rm(dir, { recursive: true, force: true });
102
+ }
103
+ });
104
+
105
+ test("skips CI workflow when --ci not passed", async () => {
106
+ const dir = await makeTempDir();
107
+ try {
108
+ const result = await runPinnedInit(makeInput(), makeContext(dir));
109
+ expect(result.data?.ciWorkflowAction).toBe("skipped");
110
+ expect(existsSync(join(dir, ".github", "workflows", "pinned-check.yml"))).toBe(false);
111
+ } finally {
112
+ await rm(dir, { recursive: true, force: true });
113
+ }
114
+ });
115
+
116
+ test("creates CI workflow when --ci passed", async () => {
117
+ const dir = await makeTempDir();
118
+ try {
119
+ const result = await runPinnedInit(makeInput({ ci: true }), makeContext(dir));
120
+ expect(result.data?.ciWorkflowAction).toBe("created");
121
+ expect(existsSync(join(dir, ".github", "workflows", "pinned-check.yml"))).toBe(true);
122
+ } finally {
123
+ await rm(dir, { recursive: true, force: true });
124
+ }
125
+ });
126
+ });
@@ -0,0 +1,194 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for RFC-0638 domain-neutral profile schema extensions.</purpose>
4
+ </MODULE_CONTRACT>
5
+ <CHANGE_SUMMARY>
6
+ <item>RFC-0638: initial domain field tests — parsing, defaults, validation, backward compat.</item>
7
+ </CHANGE_SUMMARY>
8
+ */
9
+
10
+ import { test, expect } from "vitest";
11
+ import {
12
+ stackProfileSchema,
13
+ loadStackProfile,
14
+ listStackProfiles,
15
+ type StackProfile,
16
+ } from "../profiles/stack-profile.ts";
17
+ import {
18
+ profileInvariantSchema,
19
+ UNIVERSAL_TERMINOLOGY_KEYS,
20
+ TERMINOLOGY_DEFAULTS,
21
+ } from "../profiles/profile-schema.ts";
22
+ import { join } from "node:path";
23
+
24
+ const FORGE_ROOT = join(import.meta.dirname, "..", "..");
25
+
26
+ const baseProfile: StackProfile = {
27
+ schema: "forge/stack-profile@1",
28
+ id: "test-profile",
29
+ displayName: "Test Profile",
30
+ detect: { anyOf: ["astro.config.*"] },
31
+ workspace: {
32
+ dirs: ["sites"],
33
+ files: [{ path: "package.json", content: "{}" }],
34
+ },
35
+ install: [],
36
+ };
37
+
38
+ test("profile with all six domain fields parses successfully", () => {
39
+ const profile = {
40
+ ...baseProfile,
41
+ domain: "game",
42
+ register: "creative",
43
+ terminology: {
44
+ artifact: "game",
45
+ artifactPlural: "games",
46
+ module: "scene",
47
+ source: "game file",
48
+ output: "build",
49
+ verify: "playtest",
50
+ operator: "developer",
51
+ },
52
+ artifacts: [
53
+ {
54
+ id: "game",
55
+ extensions: [".ts"],
56
+ produce: { command: "ref(bindings.commands.produce)", output: "dist/{name}.js" },
57
+ validate: { command: "ref(bindings.commands.validate)" },
58
+ determinism: {
59
+ hashable: true,
60
+ inputs: ["game files", "assets", "vite version"],
61
+ },
62
+ },
63
+ ],
64
+ workspaceTypes: [
65
+ {
66
+ id: "game",
67
+ detect: { glob: "phaser.config.*" },
68
+ skills: ["fo-add-tests"],
69
+ agentsMdTemplate: "templates/game-agents.md",
70
+ },
71
+ ],
72
+ invariants: [
73
+ { id: "GAME-01", rule: "Games use kebab-case filenames", severity: "error" },
74
+ { id: "GAME-02", rule: "Scenes use contain mode by default", severity: "warning" },
75
+ ],
76
+ };
77
+ const result = stackProfileSchema.safeParse(profile);
78
+ expect(result.success).toBe(true);
79
+ if (result.success) {
80
+ expect(result.data.domain).toBe("game");
81
+ expect(result.data.register).toBe("creative");
82
+ expect(result.data.artifacts).toHaveLength(1);
83
+ expect(result.data.workspaceTypes).toHaveLength(1);
84
+ expect(result.data.invariants).toHaveLength(2);
85
+ }
86
+ });
87
+
88
+ test("profile with no domain fields parses successfully (backward compat)", () => {
89
+ const result = stackProfileSchema.safeParse(baseProfile);
90
+ expect(result.success).toBe(true);
91
+ if (result.success) {
92
+ expect(result.data.domain).toBeUndefined();
93
+ expect(result.data.terminology).toBeUndefined();
94
+ expect(result.data.artifacts).toBeUndefined();
95
+ expect(result.data.workspaceTypes).toBeUndefined();
96
+ expect(result.data.invariants).toBeUndefined();
97
+ expect(result.data.register).toBeUndefined();
98
+ }
99
+ });
100
+
101
+ test("profile with partial domain fields (only domain and register) parses", () => {
102
+ const profile = { ...baseProfile, domain: "book", register: "business" };
103
+ const result = stackProfileSchema.safeParse(profile);
104
+ expect(result.success).toBe(true);
105
+ });
106
+
107
+ test("terminology with unknown keys parses (open vocabulary)", () => {
108
+ const profile = {
109
+ ...baseProfile,
110
+ terminology: { artifact: "chapter", customKey: "custom-value" },
111
+ };
112
+ const result = stackProfileSchema.safeParse(profile);
113
+ expect(result.success).toBe(true);
114
+ });
115
+
116
+ test("invariants with invalid id format (lowercase) fails validation", () => {
117
+ const profile = {
118
+ ...baseProfile,
119
+ invariants: [{ id: "video-01", rule: "test rule", severity: "error" }],
120
+ };
121
+ const result = stackProfileSchema.safeParse(profile);
122
+ expect(result.success).toBe(false);
123
+ });
124
+
125
+ test("invariants with valid id format (VIDEO-01) passes", () => {
126
+ const profile = {
127
+ ...baseProfile,
128
+ invariants: [{ id: "VIDEO-01", rule: "test rule", severity: "error" }],
129
+ };
130
+ const result = stackProfileSchema.safeParse(profile);
131
+ expect(result.success).toBe(true);
132
+ });
133
+
134
+ test("profileInvariantSchema rejects invalid severity", () => {
135
+ const result = profileInvariantSchema.safeParse({
136
+ id: "VIDEO-01",
137
+ rule: "test",
138
+ severity: "critical",
139
+ });
140
+ expect(result.success).toBe(false);
141
+ });
142
+
143
+ test("register with invalid value fails validation", () => {
144
+ const profile = { ...baseProfile, register: "personal" };
145
+ const result = stackProfileSchema.safeParse(profile);
146
+ expect(result.success).toBe(false);
147
+ });
148
+
149
+ test("artifacts with missing required extensions field fails", () => {
150
+ const profile = {
151
+ ...baseProfile,
152
+ artifacts: [{ id: "composition" }],
153
+ };
154
+ const result = stackProfileSchema.safeParse(profile);
155
+ expect(result.success).toBe(false);
156
+ });
157
+
158
+ test("all shipped profiles parse without changes", () => {
159
+ const profiles = listStackProfiles(FORGE_ROOT);
160
+ expect(profiles.length).toBe(4);
161
+ for (const profile of profiles) {
162
+ expect(profile.id).toBeDefined();
163
+ expect(profile.workspace.dirs.length).toBeGreaterThan(0);
164
+ // Domain fields should be undefined for existing software-domain profiles
165
+ if (profile.id !== "godot-csharp") {
166
+ expect(profile.domain).toBeUndefined();
167
+ expect(profile.register).toBeUndefined();
168
+ }
169
+ }
170
+ });
171
+
172
+ test("shipped phaser-turborepo profile loads via loadStackProfile", () => {
173
+ const profile = loadStackProfile(join(FORGE_ROOT, "profiles", "phaser-turborepo.yaml"));
174
+ expect(profile.id).toBe("phaser-turborepo");
175
+ });
176
+
177
+ test("UNIVERSAL_TERMINOLOGY_KEYS contains the 7 documented keys", () => {
178
+ expect(UNIVERSAL_TERMINOLOGY_KEYS).toHaveLength(7);
179
+ expect(UNIVERSAL_TERMINOLOGY_KEYS).toContain("artifact");
180
+ expect(UNIVERSAL_TERMINOLOGY_KEYS).toContain("artifactPlural");
181
+ expect(UNIVERSAL_TERMINOLOGY_KEYS).toContain("module");
182
+ expect(UNIVERSAL_TERMINOLOGY_KEYS).toContain("source");
183
+ expect(UNIVERSAL_TERMINOLOGY_KEYS).toContain("output");
184
+ expect(UNIVERSAL_TERMINOLOGY_KEYS).toContain("verify");
185
+ expect(UNIVERSAL_TERMINOLOGY_KEYS).toContain("operator");
186
+ });
187
+
188
+ test("TERMINOLOGY_DEFAULTS maps each key to a non-empty default string", () => {
189
+ for (const key of UNIVERSAL_TERMINOLOGY_KEYS) {
190
+ const value = TERMINOLOGY_DEFAULTS[key];
191
+ expect(value).toBeDefined();
192
+ expect(value.length).toBeGreaterThan(0);
193
+ }
194
+ });
@@ -0,0 +1,120 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for forge.profile.validate command handler (RFC-0640).</purpose>
4
+ </MODULE_CONTRACT>
5
+ <CHANGE_SUMMARY>
6
+ <item>RFC-0640: initial profile-validate tests.</item>
7
+ </CHANGE_SUMMARY>
8
+ */
9
+
10
+ import { test, expect, beforeEach, afterEach } from "vitest";
11
+ import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+ import { runProfileValidate } from "../onboarding/profile-validate.ts";
15
+ import type { ForgeCommandInput, ForgeRuntimeContext } from "../types.ts";
16
+
17
+ let tempDir: string;
18
+ let profilesDir: string;
19
+
20
+ beforeEach(async () => {
21
+ tempDir = await mkdtemp(join(tmpdir(), "profile-validate-test-"));
22
+ profilesDir = join(tempDir, "profiles");
23
+ await mkdir(profilesDir, { recursive: true });
24
+ });
25
+
26
+ afterEach(async () => {
27
+ await rm(tempDir, { recursive: true, force: true });
28
+ });
29
+
30
+ function makeContext(forgeRoot: string): ForgeRuntimeContext {
31
+ return {
32
+ workspaceRoot: tempDir,
33
+ forgeRoot,
34
+ logger: {
35
+ section: () => {},
36
+ success: () => {},
37
+ warn: () => {},
38
+ error: () => {},
39
+ info: () => {},
40
+ },
41
+ dryRun: false,
42
+ outputFormat: "json",
43
+ };
44
+ }
45
+
46
+ test("profile.validate returns valid when profiles directory has valid YAML", async () => {
47
+ await writeFile(
48
+ join(profilesDir, "test-profile.yaml"),
49
+ `schema: "forge/stack-profile@1"\nid: test-profile\ndisplayName: Test Profile\ndetect:\n anyOf:\n - "package.json"\nworkspace:\n dirs:\n - "packages"\n files: []\ninstall: []\n`,
50
+ "utf8",
51
+ );
52
+
53
+ const input: ForgeCommandInput = { argv: [], flags: {} };
54
+ const result = await runProfileValidate(input, makeContext(tempDir));
55
+
56
+ expect(result.data!.valid).toBe(true);
57
+ expect(result.data!.profiles).toHaveLength(1);
58
+ expect(result.data!.profiles[0].id).toBe("test-profile");
59
+ expect(result.data!.profiles[0].valid).toBe(true);
60
+ expect(result.exitCode).toBe(0);
61
+ });
62
+
63
+ test("profile.validate returns invalid when profile has schema errors", async () => {
64
+ await writeFile(
65
+ join(profilesDir, "bad-profile.yaml"),
66
+ `schema: "forge/stack-profile@1"\nid: ""\ndisplayName: Bad\ndetect:\n anyOf: []\nworkspace:\n dirs: []\n files: []\n`,
67
+ "utf8",
68
+ );
69
+
70
+ const input: ForgeCommandInput = { argv: [], flags: {} };
71
+ const result = await runProfileValidate(input, makeContext(tempDir));
72
+
73
+ expect(result.data!.valid).toBe(false);
74
+ expect(result.data!.profiles).toHaveLength(1);
75
+ expect(result.data!.profiles[0].valid).toBe(false);
76
+ expect(result.data!.profiles[0].errors.length).toBeGreaterThan(0);
77
+ expect(result.exitCode).toBe(1);
78
+ });
79
+
80
+ test("profile.validate --id filters to single profile", async () => {
81
+ await writeFile(
82
+ join(profilesDir, "alpha.yaml"),
83
+ `schema: "forge/stack-profile@1"\nid: alpha\ndisplayName: Alpha\ndetect:\n anyOf: ["package.json"]\nworkspace:\n dirs: ["packages"]\n files: []\ninstall: []\n`,
84
+ "utf8",
85
+ );
86
+ await writeFile(
87
+ join(profilesDir, "beta.yaml"),
88
+ `schema: "forge/stack-profile@1"\nid: beta\ndisplayName: Beta\ndetect:\n anyOf: ["package.json"]\nworkspace:\n dirs: ["packages"]\n files: []\ninstall: []\n`,
89
+ "utf8",
90
+ );
91
+
92
+ const input: ForgeCommandInput = { argv: [], flags: { id: "alpha" } };
93
+ const result = await runProfileValidate(input, makeContext(tempDir));
94
+
95
+ expect(result.data!.profiles).toHaveLength(1);
96
+ expect(result.data!.profiles[0].id).toBe("alpha");
97
+ });
98
+
99
+ test("profile.validate returns valid=true with empty profiles when directory missing", async () => {
100
+ const input: ForgeCommandInput = { argv: [], flags: {} };
101
+ const result = await runProfileValidate(input, makeContext(join(tempDir, "nonexistent")));
102
+
103
+ expect(result.data!.valid).toBe(true);
104
+ expect(result.data!.profiles).toHaveLength(0);
105
+ expect(result.exitCode).toBe(0);
106
+ });
107
+
108
+ test("profile.validate validates profiles with domain fields (RFC-0638)", async () => {
109
+ await writeFile(
110
+ join(profilesDir, "domain-profile.yaml"),
111
+ `schema: "forge/stack-profile@1"\nid: domain-profile\ndisplayName: Domain Profile\ndetect:\n anyOf: ["package.json"]\nworkspace:\n dirs: ["packages"]\n files: []\ninstall: []\ndomain: video\nterminology:\n artifact: composition\nartifacts:\n - id: video\n extensions: [".mp4"]\nworkspaceTypes:\n - id: scene\n detect:\n glob: "*.scene"\ninvariants:\n - id: VIDEO-01\n rule: "All videos must have audio"\n severity: error\nregister: creative\n`,
112
+ "utf8",
113
+ );
114
+
115
+ const input: ForgeCommandInput = { argv: [], flags: {} };
116
+ const result = await runProfileValidate(input, makeContext(tempDir));
117
+
118
+ expect(result.data!.valid).toBe(true);
119
+ expect(result.data!.profiles[0].valid).toBe(true);
120
+ });