@eddacraft/anvil-adapters 0.1.0

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 (183) hide show
  1. package/AGENTS.md +180 -0
  2. package/BMAD_ADAPTER_SPEC.md +489 -0
  3. package/LICENSE +14 -0
  4. package/README.md +500 -0
  5. package/dist/aps-markdown/adapter.d.ts +102 -0
  6. package/dist/aps-markdown/adapter.d.ts.map +1 -0
  7. package/dist/aps-markdown/adapter.js +351 -0
  8. package/dist/aps-markdown/index.d.ts +8 -0
  9. package/dist/aps-markdown/index.d.ts.map +1 -0
  10. package/dist/aps-markdown/index.js +7 -0
  11. package/dist/base/file-discovery.d.ts +63 -0
  12. package/dist/base/file-discovery.d.ts.map +1 -0
  13. package/dist/base/file-discovery.js +246 -0
  14. package/dist/base/index.d.ts +10 -0
  15. package/dist/base/index.d.ts.map +1 -0
  16. package/dist/base/index.js +9 -0
  17. package/dist/base/registry.d.ts +155 -0
  18. package/dist/base/registry.d.ts.map +1 -0
  19. package/dist/base/registry.js +227 -0
  20. package/dist/base/testing.d.ts +102 -0
  21. package/dist/base/testing.d.ts.map +1 -0
  22. package/dist/base/testing.js +221 -0
  23. package/dist/base/types.d.ts +255 -0
  24. package/dist/base/types.d.ts.map +1 -0
  25. package/dist/base/types.js +78 -0
  26. package/dist/base/utils.d.ts +127 -0
  27. package/dist/base/utils.d.ts.map +1 -0
  28. package/dist/base/utils.js +254 -0
  29. package/dist/bmad/format-adapter.d.ts +76 -0
  30. package/dist/bmad/format-adapter.d.ts.map +1 -0
  31. package/dist/bmad/format-adapter.js +186 -0
  32. package/dist/bmad/index.d.ts +12 -0
  33. package/dist/bmad/index.d.ts.map +1 -0
  34. package/dist/bmad/index.js +10 -0
  35. package/dist/bmad/parser.d.ts +12 -0
  36. package/dist/bmad/parser.d.ts.map +1 -0
  37. package/dist/bmad/parser.js +181 -0
  38. package/dist/bmad/serializer.d.ts +16 -0
  39. package/dist/bmad/serializer.d.ts.map +1 -0
  40. package/dist/bmad/serializer.js +170 -0
  41. package/dist/bmad/types.d.ts +127 -0
  42. package/dist/bmad/types.d.ts.map +1 -0
  43. package/dist/bmad/types.js +47 -0
  44. package/dist/bmad/utils.d.ts +120 -0
  45. package/dist/bmad/utils.d.ts.map +1 -0
  46. package/dist/bmad/utils.js +480 -0
  47. package/dist/common/index.d.ts +3 -0
  48. package/dist/common/index.d.ts.map +1 -0
  49. package/dist/common/index.js +2 -0
  50. package/dist/common/registry.d.ts +18 -0
  51. package/dist/common/registry.d.ts.map +1 -0
  52. package/dist/common/registry.js +58 -0
  53. package/dist/common/types.d.ts +68 -0
  54. package/dist/common/types.d.ts.map +1 -0
  55. package/dist/common/types.js +12 -0
  56. package/dist/generic/format-adapter.d.ts +64 -0
  57. package/dist/generic/format-adapter.d.ts.map +1 -0
  58. package/dist/generic/format-adapter.js +159 -0
  59. package/dist/generic/index.d.ts +10 -0
  60. package/dist/generic/index.d.ts.map +1 -0
  61. package/dist/generic/index.js +9 -0
  62. package/dist/generic/parser.d.ts +11 -0
  63. package/dist/generic/parser.d.ts.map +1 -0
  64. package/dist/generic/parser.js +106 -0
  65. package/dist/generic/serializer.d.ts +11 -0
  66. package/dist/generic/serializer.d.ts.map +1 -0
  67. package/dist/generic/serializer.js +118 -0
  68. package/dist/generic/types.d.ts +52 -0
  69. package/dist/generic/types.d.ts.map +1 -0
  70. package/dist/generic/types.js +6 -0
  71. package/dist/generic/utils.d.ts +51 -0
  72. package/dist/generic/utils.d.ts.map +1 -0
  73. package/dist/generic/utils.js +232 -0
  74. package/dist/index.d.ts +15 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +31 -0
  77. package/dist/speckit/export.d.ts +22 -0
  78. package/dist/speckit/export.d.ts.map +1 -0
  79. package/dist/speckit/export.js +384 -0
  80. package/dist/speckit/format-adapter.d.ts +104 -0
  81. package/dist/speckit/format-adapter.d.ts.map +1 -0
  82. package/dist/speckit/format-adapter.js +488 -0
  83. package/dist/speckit/import-v2.d.ts +33 -0
  84. package/dist/speckit/import-v2.d.ts.map +1 -0
  85. package/dist/speckit/import-v2.js +361 -0
  86. package/dist/speckit/import.d.ts +16 -0
  87. package/dist/speckit/import.d.ts.map +1 -0
  88. package/dist/speckit/import.js +247 -0
  89. package/dist/speckit/index.d.ts +5 -0
  90. package/dist/speckit/index.d.ts.map +1 -0
  91. package/dist/speckit/index.js +4 -0
  92. package/dist/speckit/parser.d.ts +28 -0
  93. package/dist/speckit/parser.d.ts.map +1 -0
  94. package/dist/speckit/parser.js +283 -0
  95. package/dist/speckit/parsers/plan-parser.d.ts +71 -0
  96. package/dist/speckit/parsers/plan-parser.d.ts.map +1 -0
  97. package/dist/speckit/parsers/plan-parser.js +216 -0
  98. package/dist/speckit/parsers/spec-parser.d.ts +67 -0
  99. package/dist/speckit/parsers/spec-parser.d.ts.map +1 -0
  100. package/dist/speckit/parsers/spec-parser.js +255 -0
  101. package/dist/speckit/parsers/tasks-parser.d.ts +57 -0
  102. package/dist/speckit/parsers/tasks-parser.d.ts.map +1 -0
  103. package/dist/speckit/parsers/tasks-parser.js +157 -0
  104. package/package.json +23 -0
  105. package/project.json +29 -0
  106. package/src/__tests__/adapter-edge-cases.test.ts +937 -0
  107. package/src/__tests__/bmad-format-adapter.test.ts +1470 -0
  108. package/src/__tests__/fixtures/aps/expected-output.json +83 -0
  109. package/src/__tests__/fixtures/bmad/invalid-malformed-yaml.md +16 -0
  110. package/src/__tests__/fixtures/bmad/invalid-no-requirements.md +23 -0
  111. package/src/__tests__/fixtures/bmad/invalid-only-yaml.md +16 -0
  112. package/src/__tests__/fixtures/bmad/invalid-too-short.md +3 -0
  113. package/src/__tests__/fixtures/bmad/invalid-wrong-format.md +40 -0
  114. package/src/__tests__/fixtures/bmad/valid-agent.md +27 -0
  115. package/src/__tests__/fixtures/bmad/valid-architecture.md +116 -0
  116. package/src/__tests__/fixtures/bmad/valid-complex-prd.md +161 -0
  117. package/src/__tests__/fixtures/bmad/valid-epic.md +73 -0
  118. package/src/__tests__/fixtures/bmad/valid-minimal-prd.md +19 -0
  119. package/src/__tests__/fixtures/bmad/valid-prd.md +107 -0
  120. package/src/__tests__/fixtures/bmad/valid-story.md +107 -0
  121. package/src/__tests__/fixtures/bmad/valid-task.md +79 -0
  122. package/src/__tests__/fixtures/bmad/valid-v6-prd.md +35 -0
  123. package/src/__tests__/fixtures/generic/plan-detailed.md +39 -0
  124. package/src/__tests__/fixtures/generic/prd-simple.md +27 -0
  125. package/src/__tests__/fixtures/generic/rfc-example.md +26 -0
  126. package/src/__tests__/fixtures/generic/todo-list.md +23 -0
  127. package/src/__tests__/fixtures/speckit/sample-plan.md +63 -0
  128. package/src/__tests__/fixtures/speckit/sample-spec-namespaced.md +50 -0
  129. package/src/__tests__/fixtures/speckit/sample-spec.md +105 -0
  130. package/src/__tests__/fixtures/speckit/sample-tasks.md +87 -0
  131. package/src/__tests__/fixtures/speckit-official/auth-feature/plan.md +272 -0
  132. package/src/__tests__/fixtures/speckit-official/auth-feature/spec.md +149 -0
  133. package/src/__tests__/fixtures/speckit-official/auth-feature/tasks.md +169 -0
  134. package/src/__tests__/generic-format-adapter.test.ts +398 -0
  135. package/src/__tests__/speckit-export.test.ts +233 -0
  136. package/src/__tests__/speckit-format-adapter.test.ts +832 -0
  137. package/src/__tests__/speckit-import-v2.test.ts +253 -0
  138. package/src/__tests__/speckit-import.test.ts +209 -0
  139. package/src/__tests__/speckit-parser.test.ts +219 -0
  140. package/src/__tests__/speckit-spec-parser.test.ts +120 -0
  141. package/src/aps-markdown/__tests__/__fixtures__/simple-leaf.aps.md +17 -0
  142. package/src/aps-markdown/__tests__/adapter.test.ts +393 -0
  143. package/src/aps-markdown/adapter.ts +455 -0
  144. package/src/aps-markdown/index.ts +8 -0
  145. package/src/base/__tests__/registry.test.ts +515 -0
  146. package/src/base/file-discovery.ts +305 -0
  147. package/src/base/index.ts +10 -0
  148. package/src/base/registry.ts +263 -0
  149. package/src/base/testing.ts +334 -0
  150. package/src/base/types.ts +342 -0
  151. package/src/base/utils.ts +306 -0
  152. package/src/bmad/format-adapter.ts +227 -0
  153. package/src/bmad/index.ts +21 -0
  154. package/src/bmad/parser.ts +224 -0
  155. package/src/bmad/serializer.ts +206 -0
  156. package/src/bmad/types.ts +135 -0
  157. package/src/bmad/utils.ts +575 -0
  158. package/src/common/index.ts +2 -0
  159. package/src/common/registry.ts +72 -0
  160. package/src/common/types.ts +84 -0
  161. package/src/generic/__tests__/serializer.test.ts +167 -0
  162. package/src/generic/format-adapter.ts +200 -0
  163. package/src/generic/index.ts +11 -0
  164. package/src/generic/parser.ts +129 -0
  165. package/src/generic/serializer.ts +134 -0
  166. package/src/generic/types.ts +53 -0
  167. package/src/generic/utils.ts +270 -0
  168. package/src/index.ts +48 -0
  169. package/src/speckit/export.ts +489 -0
  170. package/src/speckit/format-adapter.ts +595 -0
  171. package/src/speckit/import-v2.ts +445 -0
  172. package/src/speckit/import.ts +305 -0
  173. package/src/speckit/index.ts +4 -0
  174. package/src/speckit/parser.ts +351 -0
  175. package/src/speckit/parsers/plan-parser.ts +342 -0
  176. package/src/speckit/parsers/spec-parser.ts +379 -0
  177. package/src/speckit/parsers/tasks-parser.ts +246 -0
  178. package/tsconfig.json +26 -0
  179. package/tsconfig.lib.json +21 -0
  180. package/tsconfig.lib.tsbuildinfo +1 -0
  181. package/tsconfig.spec.json +9 -0
  182. package/tsconfig.tsbuildinfo +1 -0
  183. package/vitest.config.ts +14 -0
@@ -0,0 +1,398 @@
1
+ /**
2
+ * Generic Format Adapter Tests
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach } from 'vitest';
6
+ import { readFile } from 'node:fs/promises';
7
+ import { join, dirname } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { GenericMarkdownAdapter } from '../generic/format-adapter.js';
10
+ import type { ParseContext } from '../base/types.js';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
14
+ const fixturesDir = join(__dirname, 'fixtures/generic');
15
+
16
+ describe('GenericMarkdownAdapter', () => {
17
+ let adapter: GenericMarkdownAdapter;
18
+
19
+ beforeEach(() => {
20
+ adapter = new GenericMarkdownAdapter();
21
+ });
22
+
23
+ describe('metadata', () => {
24
+ it('should have correct name and version', () => {
25
+ expect(adapter.metadata.name).toBe('generic-markdown');
26
+ expect(adapter.metadata.version).toBe('1.0.0');
27
+ });
28
+
29
+ it('should have correct display name', () => {
30
+ expect(adapter.metadata.displayName).toBe('Generic Markdown');
31
+ });
32
+
33
+ it('should support generic formats', () => {
34
+ expect(adapter.metadata.formats).toContain('generic');
35
+ expect(adapter.metadata.formats).toContain('prd');
36
+ expect(adapter.metadata.formats).toContain('plan');
37
+ expect(adapter.metadata.formats).toContain('todo');
38
+ });
39
+
40
+ it('should support markdown extensions', () => {
41
+ expect(adapter.metadata.extensions).toContain('.md');
42
+ expect(adapter.metadata.extensions).toContain('.markdown');
43
+ });
44
+ });
45
+
46
+ describe('canImport / canExport', () => {
47
+ it('should support importing generic formats', () => {
48
+ expect(adapter.canImport('generic')).toBe(true);
49
+ expect(adapter.canImport('prd')).toBe(true);
50
+ expect(adapter.canImport('plan')).toBe(true);
51
+ expect(adapter.canImport('todo')).toBe(true);
52
+ });
53
+
54
+ it('should support exporting to generic format', () => {
55
+ expect(adapter.canExport('generic')).toBe(true);
56
+ expect(adapter.canExport('markdown')).toBe(true);
57
+ });
58
+
59
+ it('should support markdown extensions', () => {
60
+ expect(adapter.canImport('.md')).toBe(true);
61
+ expect(adapter.canImport('.markdown')).toBe(true);
62
+ });
63
+ });
64
+
65
+ describe('detect', () => {
66
+ it('should detect simple PRD with moderate confidence', async () => {
67
+ const content = await readFile(join(fixturesDir, 'prd-simple.md'), 'utf-8');
68
+ const result = adapter.detect(content);
69
+
70
+ expect(result.detected).toBe(true);
71
+ expect(result.confidence).toBeGreaterThanOrEqual(30);
72
+ expect(result.confidence).toBeLessThanOrEqual(45); // Capped for fallback
73
+ expect(result.reason).toContain('markdown-headings');
74
+ });
75
+
76
+ it('should detect TODO list', async () => {
77
+ const content = await readFile(join(fixturesDir, 'todo-list.md'), 'utf-8');
78
+ const result = adapter.detect(content);
79
+
80
+ expect(result.detected).toBe(true);
81
+ expect(result.confidence).toBeGreaterThanOrEqual(30);
82
+ });
83
+
84
+ it('should detect detailed plan', async () => {
85
+ const content = await readFile(join(fixturesDir, 'plan-detailed.md'), 'utf-8');
86
+ const result = adapter.detect(content);
87
+
88
+ expect(result.detected).toBe(true);
89
+ expect(result.confidence).toBeGreaterThanOrEqual(30);
90
+ expect(result.reason).toContain('requirements-section');
91
+ });
92
+
93
+ it('should detect RFC document', async () => {
94
+ const content = await readFile(join(fixturesDir, 'rfc-example.md'), 'utf-8');
95
+ const result = adapter.detect(content);
96
+
97
+ expect(result.detected).toBe(true);
98
+ expect(result.confidence).toBeGreaterThanOrEqual(30);
99
+ });
100
+
101
+ it('should have lower confidence than specific formats', async () => {
102
+ const content = await readFile(join(fixturesDir, 'prd-simple.md'), 'utf-8');
103
+ const result = adapter.detect(content);
104
+
105
+ // Generic adapter confidence should be capped at 45%
106
+ // so BMAD (50%+) and SpecKit (50%+) win
107
+ expect(result.confidence).toBeLessThanOrEqual(45);
108
+ });
109
+
110
+ it('should not detect very short content', () => {
111
+ const content = '# Title\n\nShort content.';
112
+ const result = adapter.detect(content);
113
+
114
+ expect(result.detected).toBe(false);
115
+ });
116
+ });
117
+
118
+ describe('parse', () => {
119
+ it('should parse simple PRD', async () => {
120
+ const content = await readFile(join(fixturesDir, 'prd-simple.md'), 'utf-8');
121
+ const result = await adapter.parse(content);
122
+
123
+ expect(result.success).toBe(true);
124
+ if (result.success) {
125
+ expect(result.data?.schema_version).toBe('0.1.0');
126
+ expect(result.data?.intent).toBeDefined();
127
+ expect(result.data?.proposed_changes).toBeDefined();
128
+ expect(result.data?.hash).toBeDefined();
129
+ }
130
+ });
131
+
132
+ it('should extract requirements as changes', async () => {
133
+ const content = await readFile(join(fixturesDir, 'prd-simple.md'), 'utf-8');
134
+ const result = await adapter.parse(content);
135
+
136
+ expect(result.success).toBe(true);
137
+ if (result.success) {
138
+ expect(result.data?.proposed_changes.length).toBeGreaterThan(0);
139
+ // Should have changes from requirements
140
+ const hasRequirements = result.data?.proposed_changes.some((c) =>
141
+ c.description.toLowerCase().includes('metric')
142
+ );
143
+ expect(hasRequirements).toBe(true);
144
+ }
145
+ });
146
+
147
+ it('should extract tasks as changes', async () => {
148
+ const content = await readFile(join(fixturesDir, 'todo-list.md'), 'utf-8');
149
+ const result = await adapter.parse(content);
150
+
151
+ expect(result.success).toBe(true);
152
+ if (result.success) {
153
+ expect(result.data?.proposed_changes.length).toBeGreaterThan(0);
154
+ // Should have tasks
155
+ const hasTasks = result.data?.proposed_changes.some((c) =>
156
+ c.description.toLowerCase().includes('pipeline')
157
+ );
158
+ expect(hasTasks).toBe(true);
159
+ }
160
+ });
161
+
162
+ it('should extract features as changes', async () => {
163
+ const content = await readFile(join(fixturesDir, 'todo-list.md'), 'utf-8');
164
+ const result = await adapter.parse(content);
165
+
166
+ expect(result.success).toBe(true);
167
+ if (result.success) {
168
+ // Should have features
169
+ const hasFeatures = result.data?.proposed_changes.some((c) =>
170
+ c.description.toLowerCase().includes('oauth')
171
+ );
172
+ expect(hasFeatures).toBe(true);
173
+ }
174
+ });
175
+
176
+ it('should include goals in metadata', async () => {
177
+ const content = await readFile(join(fixturesDir, 'prd-simple.md'), 'utf-8');
178
+ const result = await adapter.parse(content);
179
+
180
+ expect(result.success).toBe(true);
181
+ if (result.success) {
182
+ expect(result.data?.metadata?.goals).toBeDefined();
183
+ expect(Array.isArray(result.data?.metadata?.goals)).toBe(true);
184
+ }
185
+ });
186
+
187
+ it('should use context for provenance', async () => {
188
+ const content = await readFile(join(fixturesDir, 'prd-simple.md'), 'utf-8');
189
+ const context: ParseContext = {
190
+ filePath: '/path/to/prd.md',
191
+ author: 'Test Author',
192
+ repositoryPath: '/path/to/repo',
193
+ };
194
+
195
+ const result = await adapter.parse(content, context);
196
+
197
+ expect(result.success).toBe(true);
198
+ if (result.success) {
199
+ expect(result.data?.provenance.author).toBe('Test Author');
200
+ expect(result.data?.provenance.repository).toBe('/path/to/repo');
201
+ }
202
+ });
203
+
204
+ it('should handle documents with only goals', () => {
205
+ const content = `# Project Goals
206
+
207
+ ## Goals
208
+
209
+ - Improve performance
210
+ - Reduce costs
211
+ - Enhance security`;
212
+
213
+ return adapter.parse(content).then((result) => {
214
+ expect(result.success).toBe(true);
215
+ if (result.success) {
216
+ expect(result.data?.metadata?.goals).toBeDefined();
217
+ }
218
+ });
219
+ });
220
+
221
+ it('should extract intent from purpose section', async () => {
222
+ const content = await readFile(join(fixturesDir, 'prd-simple.md'), 'utf-8');
223
+ const result = await adapter.parse(content);
224
+
225
+ expect(result.success).toBe(true);
226
+ if (result.success) {
227
+ expect(result.data?.intent).toContain('dashboard');
228
+ }
229
+ });
230
+ });
231
+
232
+ describe('serialize', () => {
233
+ it('should serialize plan to generic markdown', async () => {
234
+ const content = await readFile(join(fixturesDir, 'prd-simple.md'), 'utf-8');
235
+ const parseResult = await adapter.parse(content);
236
+
237
+ expect(parseResult.success).toBe(true);
238
+ if (!parseResult.success) return;
239
+
240
+ const serializeResult = await adapter.serialize(parseResult.data!);
241
+
242
+ expect(serializeResult.success).toBe(true);
243
+ if (serializeResult.success) {
244
+ expect(serializeResult.content).toContain('# ');
245
+ expect(serializeResult.content).toContain('## ');
246
+ }
247
+ });
248
+
249
+ it('should include changes section', async () => {
250
+ const content = await readFile(join(fixturesDir, 'prd-simple.md'), 'utf-8');
251
+ const parseResult = await adapter.parse(content);
252
+
253
+ expect(parseResult.success).toBe(true);
254
+ if (!parseResult.success) return;
255
+
256
+ const serializeResult = await adapter.serialize(parseResult.data!);
257
+
258
+ expect(serializeResult.success).toBe(true);
259
+ if (serializeResult.success) {
260
+ expect(serializeResult.content).toContain('## Changes');
261
+ }
262
+ });
263
+
264
+ it('should include metadata section', async () => {
265
+ const content = await readFile(join(fixturesDir, 'prd-simple.md'), 'utf-8');
266
+ const parseResult = await adapter.parse(content);
267
+
268
+ expect(parseResult.success).toBe(true);
269
+ if (!parseResult.success) return;
270
+
271
+ const serializeResult = await adapter.serialize(parseResult.data!);
272
+
273
+ expect(serializeResult.success).toBe(true);
274
+ if (serializeResult.success) {
275
+ expect(serializeResult.content).toContain('## Metadata');
276
+ expect(serializeResult.content).toContain('Author');
277
+ }
278
+ });
279
+
280
+ it('should maintain roundtrip fidelity', async () => {
281
+ const content = await readFile(join(fixturesDir, 'prd-simple.md'), 'utf-8');
282
+ const parse1 = await adapter.parse(content);
283
+
284
+ expect(parse1.success).toBe(true);
285
+ if (!parse1.success) return;
286
+
287
+ const serialize = await adapter.serialize(parse1.data!);
288
+
289
+ expect(serialize.success).toBe(true);
290
+ if (!serialize.success) return;
291
+
292
+ const parse2 = await adapter.parse(serialize.content);
293
+
294
+ expect(parse2.success).toBe(true);
295
+ if (!parse2.success) return;
296
+
297
+ // Serialized format uses list items under "Files to Create/Update"
298
+ // which won't be detected as requirements/tasks by the parser
299
+ // Check that document has valid structure
300
+ expect(parse2.data?.intent).toBeDefined();
301
+ expect(parse2.data?.provenance.author).toBeDefined();
302
+ });
303
+ });
304
+
305
+ describe('validate', () => {
306
+ it('should validate simple PRD', async () => {
307
+ const content = await readFile(join(fixturesDir, 'prd-simple.md'), 'utf-8');
308
+ const result = await adapter.validate(content);
309
+
310
+ expect(result.valid).toBe(true);
311
+ expect(result.summary).toContain('valid');
312
+ });
313
+
314
+ it('should reject very short content', async () => {
315
+ const content = '# Short';
316
+ const result = await adapter.validate(content);
317
+
318
+ expect(result.valid).toBe(false);
319
+ expect(result.issues).toBeDefined();
320
+ if (result.issues) {
321
+ expect(result.issues.some((i) => i.code === 'CONTENT_TOO_SHORT')).toBe(true);
322
+ }
323
+ });
324
+
325
+ it('should warn about missing planning sections', async () => {
326
+ const content = `# Random Document
327
+
328
+ This is just random content without any planning sections like requirements,
329
+ tasks, or features. It should still be valid but with warnings.
330
+
331
+ ## Introduction
332
+
333
+ Some introduction text here.
334
+
335
+ ## Conclusion
336
+
337
+ Some conclusion text here.`;
338
+
339
+ const result = await adapter.validate(content);
340
+
341
+ expect(result.issues).toBeDefined();
342
+ if (result.issues) {
343
+ expect(result.issues.some((i) => i.code === 'NO_PLANNING_SECTIONS')).toBe(true);
344
+ }
345
+ });
346
+
347
+ it('should validate detailed plan', async () => {
348
+ const content = await readFile(join(fixturesDir, 'plan-detailed.md'), 'utf-8');
349
+ const result = await adapter.validate(content);
350
+
351
+ expect(result.valid).toBe(true);
352
+ });
353
+
354
+ it('should validate RFC document', async () => {
355
+ const content = await readFile(join(fixturesDir, 'rfc-example.md'), 'utf-8');
356
+ const result = await adapter.validate(content);
357
+
358
+ expect(result.valid).toBe(true);
359
+ });
360
+ });
361
+
362
+ describe('integration', () => {
363
+ it('should complete full workflow: detect → parse → validate → serialize', async () => {
364
+ const content = await readFile(join(fixturesDir, 'prd-simple.md'), 'utf-8');
365
+
366
+ // Detect
367
+ const detectResult = adapter.detect(content);
368
+ expect(detectResult.detected).toBe(true);
369
+
370
+ // Parse
371
+ const parseResult = await adapter.parse(content);
372
+ expect(parseResult.success).toBe(true);
373
+ if (!parseResult.success) return;
374
+
375
+ // Validate
376
+ const validateResult = await adapter.validate(content);
377
+ expect(validateResult.valid).toBe(true);
378
+
379
+ // Serialize
380
+ const serializeResult = await adapter.serialize(parseResult.data!);
381
+ expect(serializeResult.success).toBe(true);
382
+ });
383
+
384
+ it('should handle multiple document types', async () => {
385
+ const files = ['prd-simple.md', 'todo-list.md', 'plan-detailed.md', 'rfc-example.md'];
386
+
387
+ for (const file of files) {
388
+ const content = await readFile(join(fixturesDir, file), 'utf-8');
389
+
390
+ const detectResult = adapter.detect(content);
391
+ expect(detectResult.detected).toBe(true);
392
+
393
+ const parseResult = await adapter.parse(content);
394
+ expect(parseResult.success).toBe(true);
395
+ }
396
+ });
397
+ });
398
+ });
@@ -0,0 +1,233 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { SpecKitExportAdapter } from '../speckit/export.js';
3
+ import type { APSPlan } from '@eddacraft/anvil-core';
4
+
5
+ interface SpecKitContent {
6
+ specContent: string;
7
+ planContent: string;
8
+ tasksContent: string;
9
+ }
10
+
11
+ describe('SpecKitExportAdapter', () => {
12
+ let adapter: SpecKitExportAdapter;
13
+ let sampleAPSPlan: APSPlan;
14
+
15
+ beforeEach(() => {
16
+ adapter = new SpecKitExportAdapter();
17
+
18
+ sampleAPSPlan = {
19
+ id: 'aps-12345678',
20
+ hash: '0'.repeat(64),
21
+ intent: 'Implement a user authentication system with JWT tokens to secure API endpoints',
22
+ schema_version: '0.1.0',
23
+ proposed_changes: [
24
+ {
25
+ type: 'file_create',
26
+ path: 'src/auth/controller.ts',
27
+ description: 'Create authentication controller',
28
+ content: 'export class AuthController { /* ... */ }',
29
+ },
30
+ {
31
+ type: 'file_update',
32
+ path: 'src/app.ts',
33
+ description: 'Update main app to include auth routes',
34
+ },
35
+ {
36
+ type: 'dependency_add',
37
+ path: 'package.json',
38
+ description: 'Add jsonwebtoken dependency',
39
+ },
40
+ ],
41
+ provenance: {
42
+ timestamp: '2024-01-15T10:00:00Z',
43
+ source: 'cli',
44
+ version: '1.0.0',
45
+ author: 'test@example.com',
46
+ },
47
+ validations: {
48
+ required_checks: ['lint', 'test'],
49
+ skip_checks: [],
50
+ },
51
+ metadata: {
52
+ goals: ['Secure API endpoints', 'Implement JWT authentication'],
53
+ requirements: ['Node.js 18+', 'Express.js'],
54
+ overview: 'JWT-based authentication system',
55
+ },
56
+ };
57
+ });
58
+
59
+ describe('basic properties', () => {
60
+ it('should have correct name and version', () => {
61
+ expect(adapter.name).toBe('speckit-export');
62
+ expect(adapter.version).toBe('1.0.0');
63
+ });
64
+
65
+ it('should support speckit export formats', () => {
66
+ expect(adapter.canExport('speckit')).toBe(true);
67
+ expect(adapter.canExport('spec.md')).toBe(true);
68
+ expect(adapter.canExport('unknown')).toBe(false);
69
+ });
70
+ });
71
+
72
+ describe('convertFromAPS', () => {
73
+ it('should convert APS to SpecKit format', async () => {
74
+ const result = await adapter.convertFromAPS(sampleAPSPlan);
75
+
76
+ expect(result.success).toBe(true);
77
+ expect(result.data).toBeDefined();
78
+
79
+ if (result.success && result.data) {
80
+ expect(result.data.format).toBe('speckit');
81
+ expect(result.data.version).toBe('1.0.0');
82
+
83
+ const content = result.data.content as SpecKitContent;
84
+ expect(content.specContent).toBeDefined();
85
+ expect(content.planContent).toBeDefined();
86
+ expect(content.tasksContent).toBeDefined();
87
+
88
+ // Check spec content includes intent
89
+ expect(content.specContent).toContain('## Intent');
90
+ expect(content.specContent).toContain(sampleAPSPlan.intent);
91
+
92
+ // Check goals and requirements are included
93
+ expect(content.specContent).toContain('## Goals');
94
+ expect(content.specContent).toContain('Secure API endpoints');
95
+
96
+ // Check changes are properly formatted
97
+ expect(content.specContent).toContain('## Changes');
98
+ expect(content.specContent).toContain('### Files to Create');
99
+ expect(content.specContent).toContain('src/auth/controller.ts');
100
+ }
101
+ });
102
+
103
+ it('should generate valid markdown for spec.md', async () => {
104
+ const result = await adapter.convertFromAPS(sampleAPSPlan);
105
+
106
+ expect(result.success).toBe(true);
107
+ const content = result.data?.content as SpecKitContent;
108
+ const specMarkdown = content.specContent;
109
+
110
+ // Check markdown structure
111
+ expect(specMarkdown).toMatch(/^# Specification/);
112
+ expect(specMarkdown).toContain('## Intent');
113
+ expect(specMarkdown).toContain('## Goals');
114
+ expect(specMarkdown).toContain('## Requirements');
115
+ expect(specMarkdown).toContain('## Changes');
116
+
117
+ // Check code blocks are properly formatted
118
+ expect(specMarkdown).toMatch(/```\w+\n[\s\S]*?```/);
119
+ });
120
+
121
+ it('should generate valid plan.md', async () => {
122
+ const result = await adapter.convertFromAPS(sampleAPSPlan);
123
+
124
+ expect(result.success).toBe(true);
125
+ const content = result.data?.content as SpecKitContent;
126
+ const planMarkdown = content.planContent;
127
+
128
+ // Check plan structure
129
+ expect(planMarkdown).toContain('# Implementation Plan');
130
+ expect(planMarkdown).toContain(`Generated from APS: ${sampleAPSPlan.id}`);
131
+ expect(planMarkdown).toContain('## Summary');
132
+ expect(planMarkdown).toContain('## Implementation Steps');
133
+
134
+ // Check steps are numbered
135
+ expect(planMarkdown).toMatch(/1\. \*\*.+\*\*/);
136
+ expect(planMarkdown).toMatch(/2\. \*\*.+\*\*/);
137
+ });
138
+
139
+ it('should generate valid tasks.md', async () => {
140
+ const result = await adapter.convertFromAPS(sampleAPSPlan);
141
+
142
+ expect(result.success).toBe(true);
143
+ const content = result.data?.content as SpecKitContent;
144
+ const tasksMarkdown = content.tasksContent;
145
+
146
+ // Check tasks structure
147
+ expect(tasksMarkdown).toContain('# Tasks');
148
+ expect(tasksMarkdown).toContain(`Generated from APS: ${sampleAPSPlan.id}`);
149
+ expect(tasksMarkdown).toContain('## Task List');
150
+ expect(tasksMarkdown).toContain('## Progress');
151
+
152
+ // Check task formatting
153
+ expect(tasksMarkdown).toMatch(/- \[ \] ⏳ .+/);
154
+
155
+ // Check progress calculation
156
+ expect(tasksMarkdown).toContain('Total tasks:');
157
+ expect(tasksMarkdown).toContain('Progress: 0%');
158
+ });
159
+
160
+ it('should handle APS with execution history', async () => {
161
+ const planWithExecutions: APSPlan = {
162
+ ...sampleAPSPlan,
163
+ executions: [
164
+ {
165
+ operation: 'apply' as const,
166
+ timestamp: '2024-01-16T12:00:00Z',
167
+ status: 'success',
168
+ executed_by: 'ci-bot',
169
+ changes_applied: ['change-1', 'change-2'],
170
+ },
171
+ {
172
+ operation: 'apply' as const,
173
+ timestamp: '2024-01-16T13:00:00Z',
174
+ status: 'failed',
175
+ executed_by: 'developer@example.com',
176
+ logs: ['Error: Build failed'],
177
+ },
178
+ ],
179
+ };
180
+
181
+ const result = await adapter.convertFromAPS(planWithExecutions);
182
+
183
+ expect(result.success).toBe(true);
184
+ const content = result.data?.content as SpecKitContent;
185
+ const tasksMarkdown = content.tasksContent;
186
+
187
+ expect(tasksMarkdown).toContain('## Execution History');
188
+ expect(tasksMarkdown).toContain('Status: success');
189
+ expect(tasksMarkdown).toContain('Status: failed');
190
+ expect(tasksMarkdown).toContain('Error: Build failed');
191
+ });
192
+
193
+ it('should preserve metadata in export', async () => {
194
+ const result = await adapter.convertFromAPS(sampleAPSPlan);
195
+
196
+ expect(result.success).toBe(true);
197
+ expect(result.data?.metadata).toBeDefined();
198
+ expect(result.data?.metadata?.aps_id).toBe(sampleAPSPlan.id);
199
+ expect(result.data?.metadata?.aps_hash).toBe(sampleAPSPlan.hash);
200
+ expect(result.data?.metadata?.generator).toBe('speckit-export');
201
+ });
202
+ });
203
+
204
+ describe('validateSpec', () => {
205
+ it('should validate specs before export', async () => {
206
+ const invalidSpec: APSPlan = {
207
+ ...sampleAPSPlan,
208
+ intent: 'Short', // Too short
209
+ };
210
+
211
+ const result = await adapter.validateSpec(invalidSpec);
212
+
213
+ expect(result.valid).toBe(false);
214
+ expect(result.issues).toBeDefined();
215
+ expect(result.issues?.[0].path).toBe('intent');
216
+ expect(result.issues?.[0].severity).toBe('error');
217
+ });
218
+
219
+ it('should warn about empty changes', async () => {
220
+ const emptySpec: APSPlan = {
221
+ ...sampleAPSPlan,
222
+ proposed_changes: [],
223
+ };
224
+
225
+ const result = await adapter.validateSpec(emptySpec);
226
+
227
+ expect(result.valid).toBe(true);
228
+ expect(result.issues).toBeDefined();
229
+ expect(result.issues?.[0].severity).toBe('warning');
230
+ expect(result.issues?.[0].message).toContain('No changes');
231
+ });
232
+ });
233
+ });