@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
package/src/index.ts ADDED
@@ -0,0 +1,172 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Public entrypoint for @warpgogol/forge — exports ForgeModule types, canonical types, utilities, skill schema, registry, validators, onboarding, and OS modules.</purpose>
4
+ <non-goals>
5
+ <item>Do not export internal implementation details — only public API.</item>
6
+ </non-goals>
7
+ </MODULE_CONTRACT>
8
+ <CHANGE_SUMMARY>
9
+ <item>RFC-0374: initial public exports for forge package.</item>
10
+ <item>Forge autonomy refactor: add canonical types and utils exports.</item>
11
+ </CHANGE_SUMMARY>
12
+ */
13
+
14
+ // Canonical types
15
+ export type {
16
+ ForgeCommandInput,
17
+ ForgeCommandResult,
18
+ ForgeCommandTiming,
19
+ ForgeNextStep,
20
+ ForgeFlagValue,
21
+ ForgeFlagSpec,
22
+ ForgeCommandMetadata,
23
+ ForgeRegisteredCommandInfo,
24
+ ForgeCommandDefinition,
25
+ ForgeCommandScope,
26
+ Diagnostic,
27
+ DiagnosticSeverity,
28
+ DiagnosticEvidence,
29
+ CheckResult,
30
+ ForgeLogger,
31
+ CommandRegistry,
32
+ ForgeRuntimeContext,
33
+ ForgeSiteContext,
34
+ ForgeOutputFormat,
35
+ GateMetadata,
36
+ GateSeverity,
37
+ GatePhase,
38
+ GateConditional,
39
+ } from "./types.ts";
40
+
41
+ // Canonical utilities
42
+ export {
43
+ writeFileAtomic,
44
+ type WriteFileAtomicOptions,
45
+ GENERATED_MARKER,
46
+ hasGeneratedMarker,
47
+ stripGeneratedMarker,
48
+ buildGeneratedHeader,
49
+ isGeneratedMarkerTextCandidate,
50
+ type GeneratedHeaderInput,
51
+ type StripGeneratedMarkerResult,
52
+ toKebabCase,
53
+ collectFiles,
54
+ fileExists,
55
+ type CollectFilesOptions,
56
+ byteHash,
57
+ } from "./utils/index.ts";
58
+
59
+ // ForgeModule types
60
+ export type { ForgeModule, ForgeModuleRegistry, ForgePipelineStep } from "./forge-module.ts";
61
+
62
+ // Skill schema
63
+ export { skillFrontmatterSchema, type SkillFrontmatter } from "./skill-schema.ts";
64
+
65
+ // Skill registry
66
+ export { FORGE_SKILLS, type ForgeSkillEntry } from "./registry.ts";
67
+
68
+ // Forge config (RFC-0391)
69
+ export {
70
+ forgeConfigSchema,
71
+ forgeBindingsSchema,
72
+ defaultForgeConfig,
73
+ loadForgeConfig,
74
+ resolveForgeRoot,
75
+ resolveBinding,
76
+ resolveTerminology,
77
+ FORGE_CLI_BINDING_DEFAULTS,
78
+ PM_RUNNER_MAP,
79
+ resolvePmRunner,
80
+ applyCliBindingDefaults,
81
+ resolvePackageManager,
82
+ type ForgeConfig,
83
+ type ForgeBindings,
84
+ type ForgeCliBindingDefault,
85
+ type ForgePackageManager,
86
+ type ForgeMigrationAdapter,
87
+ } from "./config/forge-config.ts";
88
+
89
+ // Stack profiles (RFC-0392, RFC-0638)
90
+ export {
91
+ stackProfileSchema,
92
+ loadStackProfile,
93
+ listStackProfiles,
94
+ detectStack,
95
+ type StackProfile,
96
+ type ProfileFile,
97
+ } from "./profiles/stack-profile.ts";
98
+
99
+ // Profile domain fields (RFC-0638)
100
+ export {
101
+ stackProfileDomainFieldsSchema,
102
+ profileArtifactSchema,
103
+ profileWorkspaceTypeSchema,
104
+ profileInvariantSchema,
105
+ UNIVERSAL_TERMINOLOGY_KEYS,
106
+ TERMINOLOGY_DEFAULTS,
107
+ type StackProfileDomainFields,
108
+ type ProfileArtifact,
109
+ type ProfileWorkspaceType,
110
+ type ProfileInvariant,
111
+ } from "./profiles/profile-schema.ts";
112
+
113
+ // Knowledge module (RFC-0660)
114
+ export {
115
+ parseKnowledgeFile,
116
+ serializeKnowledgeFile,
117
+ knowledgeEntryMetaSchema,
118
+ type KnowledgeLayer,
119
+ type KnowledgeEntryStatus,
120
+ type KnowledgeEntryMeta,
121
+ type KnowledgeEntry,
122
+ type LegacySection,
123
+ type ParseIssue,
124
+ type ParsedKnowledgeFile,
125
+ } from "./knowledge/index.ts";
126
+
127
+ // Validators
128
+ export { runSkillValidate } from "./validators/skill-validate.ts";
129
+ export { runPortValidate } from "./validators/port-validate.ts";
130
+
131
+ // CLI output rendering (RFC-0542)
132
+ export { renderNextSteps, renderIdeRecommendation, generateHelp } from "./cli-output.ts";
133
+
134
+ // Onboarding
135
+ export { runInit } from "./onboarding/init.ts";
136
+ export { runScaffold } from "./onboarding/scaffold.ts";
137
+ export { runDoctor } from "./onboarding/doctor.ts";
138
+ export { runAgentsGenerate } from "./onboarding/agents-generate.ts";
139
+ export { runScaffoldProject } from "./onboarding/scaffold-project.ts";
140
+ export { scaffoldMemoryLayer, type MemoryScaffoldResult } from "./onboarding/memory-scaffold.ts";
141
+
142
+ // Migration adapters (RFC-0546)
143
+ export type {
144
+ MigrationAdapter,
145
+ AdapterAnalysis,
146
+ MigrationResult,
147
+ Conflict,
148
+ } from "./migration-adapters/types.ts";
149
+ export { FORGE_PROTECTED_PATHS, DEFAULT_EXCLUDE_PATTERNS } from "./migration-adapters/types.ts";
150
+ export {
151
+ nodeTypescriptPnpmAdapter,
152
+ phaserPnpmAdapter,
153
+ getAdapters,
154
+ detectAdapter,
155
+ detectAdapters,
156
+ } from "./migration-adapters/index.ts";
157
+
158
+ // OS modules
159
+ export { forgeCoreModule } from "../os/core/core.module.ts";
160
+ export { forgeRfcModule } from "../os/rfc/rfc.module.ts";
161
+ export { forgeWorkflowModule } from "../os/workflow/workflow.module.ts";
162
+ export { forgeNamingModule } from "../os/naming/naming.module.ts";
163
+ export { forgeCompassModule } from "../os/compass/compass.module.ts";
164
+ export { forgeWerkstattModule } from "../os/werkstatt/werkstatt.module.ts";
165
+ export { forgeSpecModule } from "../os/spec/spec.module.ts";
166
+ export { forgeAdrModule } from "../os/adr/adr.module.ts";
167
+ export { forgePlanModule } from "../os/plan/plan.module.ts";
168
+ export { forgeAuditModule } from "../os/audit/audit.module.ts";
169
+ export { forgeMissionModule } from "../os/mission/mission.module.ts";
170
+ export { forgeExplorationModule } from "../os/exploration/exploration.module.ts";
171
+ export { forgeNotesModule } from "../os/notes/notes.module.ts";
172
+ export { forgeProgramModule } from "../os/program/program.module.ts";
@@ -0,0 +1,219 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { normalizeTitle, detectDuplicatePrinciples, planPromotion } from "../promote.ts";
3
+ import type {
4
+ ParsedKnowledgeFile,
5
+ KnowledgeEntry,
6
+ KnowledgeEntryMeta,
7
+ ParseIssue,
8
+ } from "../schema.ts";
9
+
10
+ function makeMeta(overrides: Partial<KnowledgeEntryMeta> = {}): KnowledgeEntryMeta {
11
+ return {
12
+ id: "K-0001",
13
+ layer: "L1",
14
+ created: "2026-01-01",
15
+ lastConfirmedAt: "2026-01-01",
16
+ confirmations: 1,
17
+ status: "active",
18
+ ...overrides,
19
+ };
20
+ }
21
+
22
+ function makeEntry(title: string, meta?: Partial<KnowledgeEntryMeta>): KnowledgeEntry {
23
+ return { meta: makeMeta(meta), title, body: "", lineStart: 0 };
24
+ }
25
+
26
+ function makeParsedFile(entries: KnowledgeEntry[], issues: ParseIssue[] = []): ParsedKnowledgeFile {
27
+ return {
28
+ path: "qa-log.md",
29
+ layer: "L1",
30
+ preamble: "",
31
+ entries,
32
+ legacySections: [],
33
+ isKnowledgeAdjacent: false,
34
+ parseIssues: issues,
35
+ };
36
+ }
37
+
38
+ describe("normalizeTitle", () => {
39
+ it("lowercases and removes punctuation", () => {
40
+ expect(normalizeTitle("Hello, World!")).toBe("hello world");
41
+ });
42
+
43
+ it("strips stop words", () => {
44
+ expect(normalizeTitle("The Quick Brown Fox")).toBe("quick brown fox");
45
+ expect(normalizeTitle("A Cat on a Mat")).toBe("cat on mat");
46
+ });
47
+
48
+ it("collapses whitespace", () => {
49
+ expect(normalizeTitle(" Multiple Spaces ")).toBe("multiple spaces");
50
+ });
51
+
52
+ it("handles unicode letters", () => {
53
+ expect(normalizeTitle("Übergröße Straße")).toBe("übergröße straße");
54
+ });
55
+
56
+ it("returns empty for all-stop-word input", () => {
57
+ expect(normalizeTitle("the a an")).toBe("");
58
+ });
59
+ });
60
+
61
+ describe("detectDuplicatePrinciples", () => {
62
+ it("detects exact title matches across skills", () => {
63
+ const files = [
64
+ { skill: "skill-a", parsed: makeParsedFile([makeEntry("Always Check Types")]) },
65
+ { skill: "skill-b", parsed: makeParsedFile([makeEntry("Always Check Types")]) },
66
+ ];
67
+ const dupes = detectDuplicatePrinciples(files);
68
+ expect(dupes).toHaveLength(1);
69
+ expect(dupes[0].kind).toBe("exact");
70
+ });
71
+
72
+ it("skips stale and archived entries", () => {
73
+ const files = [
74
+ {
75
+ skill: "skill-a",
76
+ parsed: makeParsedFile([makeEntry("Same Title", { id: "K-0001", status: "stale" })]),
77
+ },
78
+ {
79
+ skill: "skill-b",
80
+ parsed: makeParsedFile([makeEntry("Same Title", { id: "K-0002", status: "archived" })]),
81
+ },
82
+ ];
83
+ expect(detectDuplicatePrinciples(files)).toHaveLength(0);
84
+ });
85
+
86
+ it("skips already-promoted entries", () => {
87
+ const files = [
88
+ {
89
+ skill: "skill-a",
90
+ parsed: makeParsedFile([makeEntry("Title", { id: "K-0001", promotedTo: "shared/K-0002" })]),
91
+ },
92
+ { skill: "skill-b", parsed: makeParsedFile([makeEntry("Title", { id: "K-0002" })]) },
93
+ ];
94
+ expect(detectDuplicatePrinciples(files)).toHaveLength(0);
95
+ });
96
+
97
+ it("skips linked pairs via supersedes", () => {
98
+ const files = [
99
+ {
100
+ skill: "skill-a",
101
+ parsed: makeParsedFile([makeEntry("Same Title", { supersedes: ["K-0002"] })]),
102
+ },
103
+ { skill: "skill-b", parsed: makeParsedFile([makeEntry("Same Title", { id: "K-0002" })]) },
104
+ ];
105
+ expect(detectDuplicatePrinciples(files)).toHaveLength(0);
106
+ });
107
+
108
+ it("skips knowledge-adjacent files", () => {
109
+ const parsed = makeParsedFile([makeEntry("Title", { id: "K-0001" })]);
110
+ parsed.isKnowledgeAdjacent = true;
111
+ const files = [
112
+ { skill: "skill-a", parsed },
113
+ { skill: "skill-b", parsed: makeParsedFile([makeEntry("Title", { id: "K-0002" })]) },
114
+ ];
115
+ expect(detectDuplicatePrinciples(files)).toHaveLength(0);
116
+ });
117
+
118
+ it("skips files with parse issues", () => {
119
+ const files = [
120
+ {
121
+ skill: "skill-a",
122
+ parsed: makeParsedFile(
123
+ [makeEntry("Title", { id: "K-0001" })],
124
+ [{ line: 1, message: "bad" }],
125
+ ),
126
+ },
127
+ { skill: "skill-b", parsed: makeParsedFile([makeEntry("Title", { id: "K-0002" })]) },
128
+ ];
129
+ expect(detectDuplicatePrinciples(files)).toHaveLength(0);
130
+ });
131
+
132
+ it("detects containment when shorter title is substring of longer", () => {
133
+ const files = [
134
+ {
135
+ skill: "skill-a",
136
+ parsed: makeParsedFile([
137
+ makeEntry("Always validate input before processing", { id: "K-0001" }),
138
+ ]),
139
+ },
140
+ {
141
+ skill: "skill-b",
142
+ parsed: makeParsedFile([
143
+ makeEntry("Always validate input before processing and after", { id: "K-0002" }),
144
+ ]),
145
+ },
146
+ ];
147
+ const dupes = detectDuplicatePrinciples(files);
148
+ expect(dupes).toHaveLength(1);
149
+ expect(dupes[0].kind).toBe("containment");
150
+ });
151
+
152
+ it("does not detect containment for very short titles", () => {
153
+ const files = [
154
+ { skill: "skill-a", parsed: makeParsedFile([makeEntry("Check types", { id: "K-0001" })]) },
155
+ {
156
+ skill: "skill-b",
157
+ parsed: makeParsedFile([makeEntry("Check types and more", { id: "K-0002" })]),
158
+ },
159
+ ];
160
+ expect(detectDuplicatePrinciples(files)).toHaveLength(0);
161
+ });
162
+
163
+ it("returns empty for no duplicates", () => {
164
+ const files = [
165
+ { skill: "skill-a", parsed: makeParsedFile([makeEntry("Unique Title A", { id: "K-0001" })]) },
166
+ { skill: "skill-b", parsed: makeParsedFile([makeEntry("Unique Title B", { id: "K-0002" })]) },
167
+ ];
168
+ expect(detectDuplicatePrinciples(files)).toHaveLength(0);
169
+ });
170
+ });
171
+
172
+ describe("planPromotion", () => {
173
+ it("creates shared entry with summed confirmations", () => {
174
+ const sources = [
175
+ {
176
+ skill: "skill-a",
177
+ file: "qa-log.md",
178
+ entry: makeEntry("Shared Principle", { id: "K-0001", confirmations: 3 }),
179
+ },
180
+ {
181
+ skill: "skill-b",
182
+ file: "qa-log.md",
183
+ entry: makeEntry("Shared Principle", { id: "K-0002", confirmations: 5 }),
184
+ },
185
+ ];
186
+ const plan = planPromotion(
187
+ sources,
188
+ { title: "Merged", body: "Combined body" },
189
+ "K-0099",
190
+ "2026-08-20",
191
+ );
192
+ expect(plan.sharedEntry.meta.id).toBe("K-0099");
193
+ expect(plan.sharedEntry.meta.layer).toBe("L2");
194
+ expect(plan.sharedEntry.meta.confirmations).toBe(8);
195
+ expect(plan.sharedEntry.meta.promotedFrom).toEqual(["skill-a/K-0001", "skill-b/K-0002"]);
196
+ expect(plan.sharedEntry.title).toBe("Merged");
197
+ expect(plan.sharedEntry.body).toBe("Combined body");
198
+ });
199
+
200
+ it("creates local pointers for each source", () => {
201
+ const sources = [
202
+ { skill: "skill-a", file: "qa-log.md", entry: makeEntry("Title", { id: "K-0001" }) },
203
+ { skill: "skill-b", file: "learned.md", entry: makeEntry("Title", { id: "K-0002" }) },
204
+ ];
205
+ const plan = planPromotion(sources, { title: "M", body: "B" }, "K-0099", "2026-01-01");
206
+ expect(plan.localPointers).toEqual([
207
+ { skill: "skill-a", file: "qa-log.md", entryId: "K-0001" },
208
+ { skill: "skill-b", file: "learned.md", entryId: "K-0002" },
209
+ ]);
210
+ });
211
+
212
+ it("handles undefined confirmations as zero", () => {
213
+ const sources = [
214
+ { skill: "s", file: "f.md", entry: makeEntry("T", { confirmations: undefined }) },
215
+ ];
216
+ const plan = planPromotion(sources, { title: "M", body: "B" }, "K-0099", "2026-01-01");
217
+ expect(plan.sharedEntry.meta.confirmations).toBe(0);
218
+ });
219
+ });
@@ -0,0 +1,192 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { serializeKnowledgeFile } from "../serialize.ts";
3
+ import type { ParsedKnowledgeFile, KnowledgeEntryMeta } from "../schema.ts";
4
+
5
+ function makeMeta(overrides: Partial<KnowledgeEntryMeta> = {}): KnowledgeEntryMeta {
6
+ return {
7
+ id: "entry-001",
8
+ layer: "L1",
9
+ created: "2026-01-01",
10
+ lastConfirmedAt: "2026-01-01",
11
+ confirmations: 1,
12
+ status: "active",
13
+ ...overrides,
14
+ };
15
+ }
16
+
17
+ describe("serializeKnowledgeFile", () => {
18
+ it("serializes empty parsed file to single newline", () => {
19
+ const parsed: ParsedKnowledgeFile = {
20
+ path: "test.md",
21
+ layer: "L1",
22
+ preamble: "",
23
+ entries: [],
24
+ legacySections: [],
25
+ isKnowledgeAdjacent: false,
26
+ parseIssues: [],
27
+ };
28
+ expect(serializeKnowledgeFile(parsed)).toBe("\n");
29
+ });
30
+
31
+ it("serializes preamble only", () => {
32
+ const parsed: ParsedKnowledgeFile = {
33
+ path: "test.md",
34
+ layer: "L1",
35
+ preamble: "# My Knowledge File",
36
+ entries: [],
37
+ legacySections: [],
38
+ isKnowledgeAdjacent: false,
39
+ parseIssues: [],
40
+ };
41
+ expect(serializeKnowledgeFile(parsed)).toBe("# My Knowledge File\n");
42
+ });
43
+
44
+ it("serializes a single entry with meta and body", () => {
45
+ const parsed: ParsedKnowledgeFile = {
46
+ path: "test.md",
47
+ layer: "L1",
48
+ preamble: "",
49
+ entries: [
50
+ {
51
+ meta: makeMeta(),
52
+ title: "First Principle",
53
+ body: "This is the body text.",
54
+ lineStart: 0,
55
+ },
56
+ ],
57
+ legacySections: [],
58
+ isKnowledgeAdjacent: false,
59
+ parseIssues: [],
60
+ };
61
+ const result = serializeKnowledgeFile(parsed);
62
+ expect(result).toContain("### entry-001: First Principle");
63
+ expect(result).toContain("```knowledge-entry");
64
+ expect(result).toContain("id: entry-001");
65
+ expect(result).toContain("layer: L1");
66
+ expect(result).toContain("status: active");
67
+ expect(result).toContain("This is the body text.");
68
+ expect(result).toContain("```");
69
+ });
70
+
71
+ it("omits undefined meta fields", () => {
72
+ const parsed: ParsedKnowledgeFile = {
73
+ path: "test.md",
74
+ layer: "L1",
75
+ preamble: "",
76
+ entries: [
77
+ {
78
+ meta: makeMeta({ supersedes: undefined, promotedTo: undefined, expiresAt: undefined }),
79
+ title: "Minimal",
80
+ body: "",
81
+ lineStart: 0,
82
+ },
83
+ ],
84
+ legacySections: [],
85
+ isKnowledgeAdjacent: false,
86
+ parseIssues: [],
87
+ };
88
+ const result = serializeKnowledgeFile(parsed);
89
+ expect(result).not.toContain("supersedes");
90
+ expect(result).not.toContain("promotedTo");
91
+ expect(result).not.toContain("expiresAt");
92
+ });
93
+
94
+ it("serializes array meta values as inline arrays", () => {
95
+ const parsed: ParsedKnowledgeFile = {
96
+ path: "test.md",
97
+ layer: "L1",
98
+ preamble: "",
99
+ entries: [
100
+ {
101
+ meta: makeMeta({ supersedes: ["old-001", "old-002"] }),
102
+ title: "Merged",
103
+ body: "",
104
+ lineStart: 0,
105
+ },
106
+ ],
107
+ legacySections: [],
108
+ isKnowledgeAdjacent: false,
109
+ parseIssues: [],
110
+ };
111
+ const result = serializeKnowledgeFile(parsed);
112
+ expect(result).toContain("supersedes: [old-001, old-002]");
113
+ });
114
+
115
+ it("serializes empty array as []", () => {
116
+ const parsed: ParsedKnowledgeFile = {
117
+ path: "test.md",
118
+ layer: "L1",
119
+ preamble: "",
120
+ entries: [
121
+ {
122
+ meta: makeMeta({ supersedes: [] }),
123
+ title: "Empty",
124
+ body: "",
125
+ lineStart: 0,
126
+ },
127
+ ],
128
+ legacySections: [],
129
+ isKnowledgeAdjacent: false,
130
+ parseIssues: [],
131
+ };
132
+ const result = serializeKnowledgeFile(parsed);
133
+ expect(result).toContain("supersedes: []");
134
+ });
135
+
136
+ it("serializes null meta value as null", () => {
137
+ const parsed: ParsedKnowledgeFile = {
138
+ path: "test.md",
139
+ layer: "L1",
140
+ preamble: "",
141
+ entries: [
142
+ {
143
+ meta: makeMeta({ promotedTo: null }),
144
+ title: "Null",
145
+ body: "",
146
+ lineStart: 0,
147
+ },
148
+ ],
149
+ legacySections: [],
150
+ isKnowledgeAdjacent: false,
151
+ parseIssues: [],
152
+ };
153
+ const result = serializeKnowledgeFile(parsed);
154
+ expect(result).toContain("promotedTo: null");
155
+ });
156
+
157
+ it("serializes legacy sections", () => {
158
+ const parsed: ParsedKnowledgeFile = {
159
+ path: "test.md",
160
+ layer: "L1",
161
+ preamble: "",
162
+ entries: [],
163
+ legacySections: [{ text: "## Old Section\n\nSome content", lineStart: 0 }],
164
+ isKnowledgeAdjacent: false,
165
+ parseIssues: [],
166
+ };
167
+ const result = serializeKnowledgeFile(parsed);
168
+ expect(result).toContain("## Old Section");
169
+ expect(result).toContain("Some content");
170
+ });
171
+
172
+ it("collapses excessive newlines to max two", () => {
173
+ const parsed: ParsedKnowledgeFile = {
174
+ path: "test.md",
175
+ layer: "L1",
176
+ preamble: "preamble",
177
+ entries: [
178
+ {
179
+ meta: makeMeta(),
180
+ title: "Title",
181
+ body: "body",
182
+ lineStart: 0,
183
+ },
184
+ ],
185
+ legacySections: [],
186
+ isKnowledgeAdjacent: false,
187
+ parseIssues: [],
188
+ };
189
+ const result = serializeKnowledgeFile(parsed);
190
+ expect(result).not.toMatch(/\n{3,}/);
191
+ });
192
+ });