@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,314 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Unit tests for RFC-0639 semantic bindings schema extensions — semantic command keys, terminology resolution chain, applyCliBindingDefaults with 5 new keys.</purpose>
4
+ <non-goals>
5
+ <item>Do not test resolveBinding — covered in forge-config.test.ts.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0639: initial test suite for semantic command keys, terminology resolution, and applyCliBindingDefaults.</item>
10
+ </CHANGE_SUMMARY>
11
+ */
12
+
13
+ import { describe, expect, test } from "vitest";
14
+ import {
15
+ forgeBindingsSchema,
16
+ applyCliBindingDefaults,
17
+ resolveTerminology,
18
+ defaultForgeConfig,
19
+ type ForgeConfig,
20
+ } from "../config/forge-config.ts";
21
+ import { TERMINOLOGY_DEFAULTS } from "../profiles/profile-schema.ts";
22
+
23
+ describe("RFC-0639: Semantic command keys", () => {
24
+ test("forgeBindingsSchema accepts semantic keys with string values", () => {
25
+ const bindings = {
26
+ schema: "forge/bindings@1",
27
+ commands: {
28
+ validateRfc: null,
29
+ validateAdr: null,
30
+ implementStamp: null,
31
+ typecheck: null,
32
+ test: null,
33
+ scopedBuild: null,
34
+ specValidate: null,
35
+ sessionSave: null,
36
+ validate: "pnpm exec validator check",
37
+ produce: "pnpm exec builder build -o {output}",
38
+ verify: "pnpm exec builder build --dry-run",
39
+ preview: "pnpm exec preview serve",
40
+ lint: "pnpm exec validator check --strict",
41
+ },
42
+ paths: {
43
+ invariantsFile: null,
44
+ compassDocs: [],
45
+ reviewsDir: null,
46
+ handoffsDir: null,
47
+ sessionsDir: null,
48
+ },
49
+ };
50
+ const result = forgeBindingsSchema.safeParse(bindings);
51
+ expect(result.success).toBe(true);
52
+ if (result.success) {
53
+ expect(result.data.commands.validate).toBe("pnpm exec validator check");
54
+ expect(result.data.commands.produce).toBe("pnpm exec builder build -o {output}");
55
+ }
56
+ });
57
+
58
+ test("forgeBindingsSchema defaults semantic keys to null when absent", () => {
59
+ const bindings = {
60
+ schema: "forge/bindings@1",
61
+ commands: {
62
+ validateRfc: null,
63
+ validateAdr: null,
64
+ implementStamp: null,
65
+ typecheck: null,
66
+ test: null,
67
+ scopedBuild: null,
68
+ specValidate: null,
69
+ sessionSave: null,
70
+ },
71
+ paths: {
72
+ invariantsFile: null,
73
+ compassDocs: [],
74
+ reviewsDir: null,
75
+ handoffsDir: null,
76
+ sessionsDir: null,
77
+ },
78
+ };
79
+ const result = forgeBindingsSchema.safeParse(bindings);
80
+ expect(result.success).toBe(true);
81
+ if (result.success) {
82
+ expect(result.data.commands.validate).toBeNull();
83
+ expect(result.data.commands.produce).toBeNull();
84
+ expect(result.data.commands.verify).toBeNull();
85
+ expect(result.data.commands.preview).toBeNull();
86
+ expect(result.data.commands.lint).toBeNull();
87
+ }
88
+ });
89
+
90
+ test("existing forge.yaml with only original 8 keys parses without changes", () => {
91
+ const bindings = {
92
+ schema: "forge/bindings@1",
93
+ commands: {
94
+ validateRfc: "forge rfc.validate --id {id} --json",
95
+ validateAdr: "forge adr.validate --id {id} --json",
96
+ implementStamp: "forge rfc.implement.stamp --id {id} --implementation-commit {commit}",
97
+ typecheck: "pnpm --filter {workspace} run build:check",
98
+ test: "pnpm --filter {workspace} run test",
99
+ scopedBuild: "pnpm --filter {workspace} run build",
100
+ specValidate: "forge spec.validate --spec={id} --json",
101
+ sessionSave: "forge session.save --json",
102
+ },
103
+ paths: {
104
+ invariantsFile: "docs/architecture-dna.md",
105
+ compassDocs: [],
106
+ reviewsDir: "docs/reviews",
107
+ handoffsDir: "docs/handoffs",
108
+ sessionsDir: "docs/sessions",
109
+ },
110
+ };
111
+ const result = forgeBindingsSchema.safeParse(bindings);
112
+ expect(result.success).toBe(true);
113
+ });
114
+ });
115
+
116
+ describe("RFC-0639: Terminology schema change", () => {
117
+ test("terminology defaults to empty record when absent", () => {
118
+ const bindings = {
119
+ schema: "forge/bindings@1",
120
+ commands: {
121
+ validateRfc: null,
122
+ validateAdr: null,
123
+ implementStamp: null,
124
+ typecheck: null,
125
+ test: null,
126
+ scopedBuild: null,
127
+ specValidate: null,
128
+ sessionSave: null,
129
+ },
130
+ paths: {
131
+ invariantsFile: null,
132
+ compassDocs: [],
133
+ reviewsDir: null,
134
+ handoffsDir: null,
135
+ sessionsDir: null,
136
+ },
137
+ };
138
+ const result = forgeBindingsSchema.safeParse(bindings);
139
+ expect(result.success).toBe(true);
140
+ if (result.success) {
141
+ expect(result.data.terminology).toEqual({});
142
+ }
143
+ });
144
+
145
+ test("terminology accepts a record of string-to-string", () => {
146
+ const bindings = {
147
+ schema: "forge/bindings@1",
148
+ commands: {
149
+ validateRfc: null,
150
+ validateAdr: null,
151
+ implementStamp: null,
152
+ typecheck: null,
153
+ test: null,
154
+ scopedBuild: null,
155
+ specValidate: null,
156
+ sessionSave: null,
157
+ },
158
+ paths: {
159
+ invariantsFile: null,
160
+ compassDocs: [],
161
+ reviewsDir: null,
162
+ handoffsDir: null,
163
+ sessionsDir: null,
164
+ },
165
+ terminology: { artifact: "composition", module: "scene" },
166
+ };
167
+ const result = forgeBindingsSchema.safeParse(bindings);
168
+ expect(result.success).toBe(true);
169
+ if (result.success) {
170
+ expect(result.data.terminology.artifact).toBe("composition");
171
+ expect(result.data.terminology.module).toBe("scene");
172
+ }
173
+ });
174
+ });
175
+
176
+ describe("RFC-0639: applyCliBindingDefaults", () => {
177
+ test("returns 13 keys with semantic keys as null", () => {
178
+ const commands = applyCliBindingDefaults("pnpm");
179
+ // CLI-backed keys are non-null
180
+ expect(commands.validateRfc).not.toBeNull();
181
+ expect(commands.validateAdr).not.toBeNull();
182
+ expect(commands.implementStamp).not.toBeNull();
183
+ expect(commands.specValidate).not.toBeNull();
184
+ expect(commands.sessionSave).not.toBeNull();
185
+ // Software-domain keys are null
186
+ expect(commands.typecheck).toBeNull();
187
+ expect(commands.test).toBeNull();
188
+ expect(commands.scopedBuild).toBeNull();
189
+ // Semantic keys (RFC-0639) are null
190
+ expect(commands.validate).toBeNull();
191
+ expect(commands.produce).toBeNull();
192
+ expect(commands.verify).toBeNull();
193
+ expect(commands.preview).toBeNull();
194
+ expect(commands.lint).toBeNull();
195
+ });
196
+ });
197
+
198
+ describe("RFC-0639: resolveTerminology", () => {
199
+ const baseConfig: ForgeConfig = {
200
+ schema: "forge/config@1",
201
+ project: { name: "test", stack: [], packageManager: "pnpm" },
202
+ paths: {
203
+ rfcsDir: "docs/rfcs",
204
+ adrsDir: "docs/adrs",
205
+ plansDir: "docs/plans",
206
+ auditsDir: "docs/audits",
207
+ specsDir: "docs/specs",
208
+ skillsDir: ".agents/skills",
209
+ sessionsDir: "docs/sessions",
210
+ },
211
+ bindings: {
212
+ schema: "forge/bindings@1",
213
+ commands: {
214
+ validateRfc: null,
215
+ validateAdr: null,
216
+ implementStamp: null,
217
+ typecheck: null,
218
+ test: null,
219
+ scopedBuild: null,
220
+ specValidate: null,
221
+ sessionSave: null,
222
+ validate: null,
223
+ produce: null,
224
+ verify: null,
225
+ preview: null,
226
+ lint: null,
227
+ },
228
+ paths: {
229
+ invariantsFile: null,
230
+ compassDocs: [],
231
+ reviewsDir: null,
232
+ handoffsDir: null,
233
+ sessionsDir: null,
234
+ },
235
+ terminology: {},
236
+ },
237
+ };
238
+
239
+ test("tier 1: returns bindings terminology override when present", () => {
240
+ const config: ForgeConfig = {
241
+ ...baseConfig,
242
+ bindings: {
243
+ ...baseConfig.bindings!,
244
+ terminology: { artifact: "composition" },
245
+ },
246
+ };
247
+ expect(resolveTerminology(config, undefined, "artifact")).toBe("composition");
248
+ });
249
+
250
+ test("tier 2: returns caller-provided terminology when bindings don't have the key", () => {
251
+ const config: ForgeConfig = {
252
+ ...baseConfig,
253
+ bindings: {
254
+ ...baseConfig.bindings!,
255
+ terminology: {},
256
+ },
257
+ };
258
+ const profileTerminology = { artifact: "scene" };
259
+ expect(resolveTerminology(config, profileTerminology, "artifact")).toBe("scene");
260
+ });
261
+
262
+ test("tier 3: returns universal default when neither bindings nor caller-provided have the key", () => {
263
+ const config: ForgeConfig = {
264
+ ...baseConfig,
265
+ bindings: {
266
+ ...baseConfig.bindings!,
267
+ terminology: {},
268
+ },
269
+ };
270
+ expect(resolveTerminology(config, undefined, "artifact")).toBe(TERMINOLOGY_DEFAULTS.artifact);
271
+ expect(resolveTerminology(config, undefined, "module")).toBe(TERMINOLOGY_DEFAULTS.module);
272
+ expect(resolveTerminology(config, undefined, "operator")).toBe(TERMINOLOGY_DEFAULTS.operator);
273
+ });
274
+
275
+ test("fallback: returns the key itself when not found in any tier", () => {
276
+ const config: ForgeConfig = {
277
+ ...baseConfig,
278
+ bindings: {
279
+ ...baseConfig.bindings!,
280
+ terminology: {},
281
+ },
282
+ };
283
+ expect(resolveTerminology(config, undefined, "nonexistent")).toBe("nonexistent");
284
+ });
285
+
286
+ test("works with undefined terminology parameter (skips tier 2)", () => {
287
+ const config: ForgeConfig = {
288
+ ...baseConfig,
289
+ bindings: {
290
+ ...baseConfig.bindings!,
291
+ terminology: {},
292
+ },
293
+ };
294
+ expect(resolveTerminology(config, undefined, "artifact")).toBe("artifact");
295
+ });
296
+
297
+ test("bindings override takes precedence over caller-provided terminology", () => {
298
+ const config: ForgeConfig = {
299
+ ...baseConfig,
300
+ bindings: {
301
+ ...baseConfig.bindings!,
302
+ terminology: { artifact: "composition" },
303
+ },
304
+ };
305
+ const profileTerminology = { artifact: "scene" };
306
+ expect(resolveTerminology(config, profileTerminology, "artifact")).toBe("composition");
307
+ });
308
+
309
+ test("works with defaultForgeConfig", () => {
310
+ const config = defaultForgeConfig("test-project", "pnpm");
311
+ expect(resolveTerminology(config, undefined, "artifact")).toBe("artifact");
312
+ expect(resolveTerminology(config, undefined, "source")).toBe("source file");
313
+ });
314
+ });
@@ -0,0 +1,213 @@
1
+ import { test, expect, describe } from "vitest";
2
+ import {
3
+ computeLayerBudgets,
4
+ resolveKnowledgeBudgets,
5
+ DEFAULT_KNOWLEDGE_BUDGETS,
6
+ } from "../knowledge/budgets.ts";
7
+ import type { ParsedKnowledgeFile, KnowledgeLayer } from "../knowledge/schema.ts";
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import os from "node:os";
11
+
12
+ const testDir = path.join(os.tmpdir(), "test");
13
+
14
+ // Helper: create a minimal ParsedKnowledgeFile for testing
15
+ function makeParsedFile(overrides: Partial<ParsedKnowledgeFile> = {}): ParsedKnowledgeFile {
16
+ return {
17
+ path: path.join(testDir, "learned-principles.md"),
18
+ layer: "L2",
19
+ preamble: "",
20
+ entries: [],
21
+ legacySections: [],
22
+ parseIssues: [],
23
+ isKnowledgeAdjacent: false,
24
+ ...overrides,
25
+ };
26
+ }
27
+
28
+ // Helper: create an active L2 entry
29
+ function makeActiveEntry(id: string, body: string) {
30
+ return {
31
+ meta: {
32
+ id,
33
+ layer: "L2" as KnowledgeLayer,
34
+ created: "2026-01-01",
35
+ lastConfirmedAt: "2026-01-10",
36
+ confirmations: 3,
37
+ status: "active" as const,
38
+ },
39
+ title: `Entry ${id}`,
40
+ body,
41
+ lineStart: 1,
42
+ };
43
+ }
44
+
45
+ describe("resolveKnowledgeBudgets", () => {
46
+ test("returns defaults when forge.yaml not found", () => {
47
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "budget-test-"));
48
+ try {
49
+ const budgets = resolveKnowledgeBudgets(tmpDir);
50
+ expect(budgets).toEqual(DEFAULT_KNOWLEDGE_BUDGETS);
51
+ } finally {
52
+ fs.rmSync(tmpDir, { recursive: true, force: true });
53
+ }
54
+ });
55
+
56
+ test("returns defaults when no knowledge.budgets override", () => {
57
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "budget-test-"));
58
+ try {
59
+ fs.writeFileSync(
60
+ path.join(tmpDir, "forge.yaml"),
61
+ "schema: forge/bindings@1\nbindings:\n commands:\n validateRfc: null\n",
62
+ );
63
+ const budgets = resolveKnowledgeBudgets(tmpDir);
64
+ expect(budgets).toEqual(DEFAULT_KNOWLEDGE_BUDGETS);
65
+ } finally {
66
+ fs.rmSync(tmpDir, { recursive: true, force: true });
67
+ }
68
+ });
69
+
70
+ test("applies override when present and valid", () => {
71
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "budget-test-"));
72
+ try {
73
+ fs.writeFileSync(
74
+ path.join(tmpDir, "forge.yaml"),
75
+ "schema: forge/bindings@1\nbindings:\n knowledge:\n budgets:\n hot: 2048\n warm: 4096\n",
76
+ );
77
+ const budgets = resolveKnowledgeBudgets(tmpDir);
78
+ expect(budgets).toEqual({ hot: 2048, warm: 4096 });
79
+ } finally {
80
+ fs.rmSync(tmpDir, { recursive: true, force: true });
81
+ }
82
+ });
83
+
84
+ test("falls back to defaults when override values are invalid (non-positive)", () => {
85
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "budget-test-"));
86
+ try {
87
+ fs.writeFileSync(
88
+ path.join(tmpDir, "forge.yaml"),
89
+ "schema: forge/bindings@1\nbindings:\n knowledge:\n budgets:\n hot: 0\n warm: -1\n",
90
+ );
91
+ const budgets = resolveKnowledgeBudgets(tmpDir);
92
+ expect(budgets).toEqual(DEFAULT_KNOWLEDGE_BUDGETS);
93
+ } finally {
94
+ fs.rmSync(tmpDir, { recursive: true, force: true });
95
+ }
96
+ });
97
+
98
+ test("falls back to defaults when override values are non-integer", () => {
99
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "budget-test-"));
100
+ try {
101
+ fs.writeFileSync(
102
+ path.join(tmpDir, "forge.yaml"),
103
+ "schema: forge/bindings@1\nbindings:\n knowledge:\n budgets:\n hot: 3.5\n warm: 8192\n",
104
+ );
105
+ const budgets = resolveKnowledgeBudgets(tmpDir);
106
+ expect(budgets).toEqual({ hot: DEFAULT_KNOWLEDGE_BUDGETS.hot, warm: 8192 });
107
+ } finally {
108
+ fs.rmSync(tmpDir, { recursive: true, force: true });
109
+ }
110
+ });
111
+ });
112
+
113
+ describe("computeLayerBudgets", () => {
114
+ test("counts only active entries", () => {
115
+ const file = makeParsedFile({
116
+ entries: [
117
+ makeActiveEntry("K-0001", "Short body."),
118
+ {
119
+ meta: {
120
+ id: "K-0002",
121
+ layer: "L2",
122
+ created: "2026-01-01",
123
+ lastConfirmedAt: "2026-01-10",
124
+ confirmations: 2,
125
+ status: "stale",
126
+ },
127
+ title: "Stale entry",
128
+ body: "This should not be counted.",
129
+ lineStart: 10,
130
+ },
131
+ ],
132
+ });
133
+ const skillNames = new Map([[path.join(testDir, "learned-principles.md"), "test-skill"]]);
134
+ const reports = computeLayerBudgets([file], DEFAULT_KNOWLEDGE_BUDGETS, skillNames);
135
+ expect(reports).toHaveLength(1);
136
+ // Only K-0001 (active) should be counted
137
+ expect(reports[0].activeChars).toBeLessThan(DEFAULT_KNOWLEDGE_BUDGETS.hot);
138
+ expect(reports[0].exceededBy).toBe(0);
139
+ });
140
+
141
+ test("reports exceeded budget for hot layer", () => {
142
+ const longBody = "x".repeat(5000);
143
+ const file = makeParsedFile({
144
+ entries: [makeActiveEntry("K-0001", longBody)],
145
+ });
146
+ const skillNames = new Map([[path.join(testDir, "learned-principles.md"), "test-skill"]]);
147
+ const reports = computeLayerBudgets([file], DEFAULT_KNOWLEDGE_BUDGETS, skillNames);
148
+ expect(reports).toHaveLength(1);
149
+ expect(reports[0].layer).toBe("L2");
150
+ expect(reports[0].activeChars).toBeGreaterThan(DEFAULT_KNOWLEDGE_BUDGETS.hot);
151
+ expect(reports[0].exceededBy).toBeGreaterThan(0);
152
+ });
153
+
154
+ test("skips files with undeterminable layer (null)", () => {
155
+ const file = makeParsedFile({ layer: null });
156
+ const skillNames = new Map([[path.join(testDir, "unknown.md"), "test-skill"]]);
157
+ const reports = computeLayerBudgets([file], DEFAULT_KNOWLEDGE_BUDGETS, skillNames);
158
+ expect(reports).toHaveLength(0);
159
+ });
160
+
161
+ test("skips L0 (cold) files — no budget by design", () => {
162
+ const file = makeParsedFile({ layer: "L0" });
163
+ const skillNames = new Map([[path.join(testDir, "qa-log.md"), "test-skill"]]);
164
+ const reports = computeLayerBudgets([file], DEFAULT_KNOWLEDGE_BUDGETS, skillNames);
165
+ expect(reports).toHaveLength(0);
166
+ });
167
+
168
+ test("skips files with parse issues", () => {
169
+ const file = makeParsedFile({
170
+ parseIssues: [{ line: 1, message: "Bad YAML" }],
171
+ });
172
+ const skillNames = new Map([[path.join(testDir, "learned-principles.md"), "test-skill"]]);
173
+ const reports = computeLayerBudgets([file], DEFAULT_KNOWLEDGE_BUDGETS, skillNames);
174
+ expect(reports).toHaveLength(0);
175
+ });
176
+
177
+ test("skips knowledge-adjacent files", () => {
178
+ const file = makeParsedFile({ isKnowledgeAdjacent: true });
179
+ const skillNames = new Map([[path.join(testDir, "learned-principles.md"), "test-skill"]]);
180
+ const reports = computeLayerBudgets([file], DEFAULT_KNOWLEDGE_BUDGETS, skillNames);
181
+ expect(reports).toHaveLength(0);
182
+ });
183
+
184
+ test("uses warm budget for L1 files", () => {
185
+ const file = makeParsedFile({
186
+ layer: "L1",
187
+ path: path.join(testDir, "fix-patterns.md"),
188
+ entries: [
189
+ {
190
+ meta: { id: "K-0001", layer: "L1", created: "2026-01-01", status: "active" },
191
+ title: "Fix pattern",
192
+ body: "x".repeat(9000),
193
+ lineStart: 1,
194
+ },
195
+ ],
196
+ });
197
+ const skillNames = new Map([[path.join(testDir, "fix-patterns.md"), "test-skill"]]);
198
+ const reports = computeLayerBudgets([file], DEFAULT_KNOWLEDGE_BUDGETS, skillNames);
199
+ expect(reports).toHaveLength(1);
200
+ expect(reports[0].layer).toBe("L1");
201
+ expect(reports[0].budget).toBe(DEFAULT_KNOWLEDGE_BUDGETS.warm);
202
+ expect(reports[0].exceededBy).toBeGreaterThan(0);
203
+ });
204
+
205
+ test("empty file produces zero-size report", () => {
206
+ const file = makeParsedFile({ entries: [] });
207
+ const skillNames = new Map([[path.join(testDir, "learned-principles.md"), "test-skill"]]);
208
+ const reports = computeLayerBudgets([file], DEFAULT_KNOWLEDGE_BUDGETS, skillNames);
209
+ expect(reports).toHaveLength(1);
210
+ expect(reports[0].activeChars).toBe(0);
211
+ expect(reports[0].exceededBy).toBe(0);
212
+ });
213
+ });