@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,832 @@
1
+ /**
2
+ * SpecKit Format Adapter Tests
3
+ * Tests for format detection, parsing, serialization, and validation
4
+ */
5
+
6
+ import { describe, it, expect, beforeEach } from 'vitest';
7
+ import { readFile } from 'node:fs/promises';
8
+ import { join, dirname } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { SpecKitFormatAdapter } from '../speckit/format-adapter.js';
11
+ import type { ParseContext, PathDetectionHint } from '../base/types.js';
12
+
13
+ // Get __dirname equivalent for ES modules
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+ const fixturesDir = join(__dirname, 'fixtures/speckit');
17
+
18
+ describe('SpecKitFormatAdapter', () => {
19
+ let adapter: SpecKitFormatAdapter;
20
+
21
+ beforeEach(() => {
22
+ adapter = new SpecKitFormatAdapter();
23
+ });
24
+
25
+ describe('metadata', () => {
26
+ it('should have correct name and version', () => {
27
+ expect(adapter.metadata.name).toBe('speckit');
28
+ expect(adapter.metadata.version).toBe('2.0.0');
29
+ });
30
+
31
+ it('should have correct display name', () => {
32
+ expect(adapter.metadata.displayName).toBe('GitHub SpecKit');
33
+ });
34
+
35
+ it('should support speckit formats', () => {
36
+ expect(adapter.metadata.formats).toContain('speckit');
37
+ expect(adapter.metadata.formats).toContain('spec-kit');
38
+ expect(adapter.metadata.formats).toContain('spec.md');
39
+ });
40
+
41
+ it('should support .md extension', () => {
42
+ expect(adapter.metadata.extensions).toContain('.md');
43
+ });
44
+ });
45
+
46
+ describe('canImport / canExport', () => {
47
+ it('should support importing speckit format', () => {
48
+ expect(adapter.canImport('speckit')).toBe(true);
49
+ expect(adapter.canImport('spec-kit')).toBe(true);
50
+ expect(adapter.canImport('spec.md')).toBe(true);
51
+ });
52
+
53
+ it('should support exporting to speckit format', () => {
54
+ expect(adapter.canExport('speckit')).toBe(true);
55
+ expect(adapter.canExport('spec.md')).toBe(true);
56
+ });
57
+
58
+ it('should not support unknown formats', () => {
59
+ expect(adapter.canImport('unknown')).toBe(false);
60
+ expect(adapter.canExport('unknown')).toBe(false);
61
+ });
62
+
63
+ it('should support .md extension', () => {
64
+ expect(adapter.canImport('.md')).toBe(true);
65
+ });
66
+ });
67
+
68
+ describe('detect', () => {
69
+ it('should detect valid spec document with high confidence', async () => {
70
+ const content = await readFile(join(fixturesDir, 'sample-spec.md'), 'utf-8');
71
+ const result = adapter.detect(content);
72
+
73
+ expect(result.detected).toBe(true);
74
+ expect(result.confidence).toBeGreaterThanOrEqual(50);
75
+ expect(result.reason).toContain('specification-header');
76
+ });
77
+
78
+ it('should detect Specification header', () => {
79
+ const content = `# Specification
80
+
81
+ ## Intent
82
+
83
+ Build a new feature`;
84
+
85
+ const result = adapter.detect(content);
86
+
87
+ expect(result.detected).toBe(true);
88
+ expect(result.reason).toContain('specification-header');
89
+ });
90
+
91
+ it('should detect Intent section', () => {
92
+ const content = `# Specification
93
+
94
+ ## Intent
95
+
96
+ This is the intent of the specification.`;
97
+
98
+ const result = adapter.detect(content);
99
+
100
+ expect(result.reason).toContain('intent-section');
101
+ });
102
+
103
+ it('should detect Changes section', () => {
104
+ const content = `# Specification
105
+
106
+ ## Changes
107
+
108
+ ### Files to Create
109
+
110
+ Create new file at path/to/file`;
111
+
112
+ const result = adapter.detect(content);
113
+
114
+ expect(result.reason).toContain('changes-section');
115
+ });
116
+
117
+ it('should detect file changes indicators', () => {
118
+ const content = `# Specification
119
+
120
+ ## Changes
121
+
122
+ ### Files to Create
123
+
124
+ Create auth controller
125
+
126
+ ### Files to Update
127
+
128
+ Update app.ts`;
129
+
130
+ const result = adapter.detect(content);
131
+
132
+ expect(result.reason).toContain('file-changes');
133
+ });
134
+
135
+ it('should detect code blocks', () => {
136
+ const content = `# Specification
137
+
138
+ ## Changes
139
+
140
+ \`\`\`typescript
141
+ export class Example {}
142
+ \`\`\``;
143
+
144
+ const result = adapter.detect(content);
145
+
146
+ expect(result.reason).toContain('code-blocks');
147
+ });
148
+
149
+ it('should not detect plain markdown without SpecKit indicators', () => {
150
+ const content = `# Regular Document
151
+
152
+ This is just plain markdown content without SpecKit structure.`;
153
+
154
+ const result = adapter.detect(content);
155
+
156
+ expect(result.detected).toBe(false);
157
+ expect(result.confidence).toBeLessThan(50);
158
+ });
159
+
160
+ it('should not detect BMAD format as SpecKit', () => {
161
+ const content = `---
162
+ name: Product Requirements Document
163
+ ---
164
+
165
+ # Product Requirements Document
166
+
167
+ FR-01: Some requirement
168
+ NFR-01: Another requirement`;
169
+
170
+ const result = adapter.detect(content);
171
+
172
+ expect(result.detected).toBe(false);
173
+ expect(result.confidence).toBeLessThan(50);
174
+ });
175
+ });
176
+
177
+ describe('parse', () => {
178
+ it('should parse valid spec document to APS', async () => {
179
+ const content = await readFile(join(fixturesDir, 'sample-spec.md'), 'utf-8');
180
+ const context: ParseContext = {
181
+ filePath: 'test-spec.md',
182
+ author: 'Test Author',
183
+ };
184
+
185
+ const result = await adapter.parse(content, context);
186
+
187
+ expect(result.success).toBe(true);
188
+ if (result.success) {
189
+ expect(result.data).toBeDefined();
190
+ expect(result.data?.schema_version).toBe('0.1.0');
191
+ expect(result.data?.intent).toBeDefined();
192
+ expect(result.data?.proposed_changes).toBeDefined();
193
+ expect(result.data?.provenance).toBeDefined();
194
+ expect(result.data?.hash).toBeDefined();
195
+ }
196
+ });
197
+
198
+ it('should extract intent from spec', async () => {
199
+ const content = await readFile(join(fixturesDir, 'sample-spec.md'), 'utf-8');
200
+ const result = await adapter.parse(content);
201
+
202
+ expect(result.success).toBe(true);
203
+ if (result.success && result.data) {
204
+ expect(result.data.intent).toBeDefined();
205
+ expect(result.data.intent.toLowerCase()).toContain('authentication');
206
+ }
207
+ });
208
+
209
+ it('should parse file changes from spec', async () => {
210
+ const content = await readFile(join(fixturesDir, 'sample-spec.md'), 'utf-8');
211
+ const result = await adapter.parse(content);
212
+
213
+ expect(result.success).toBe(true);
214
+ if (result.success) {
215
+ expect(result.data?.proposed_changes.length).toBeGreaterThan(0);
216
+ const fileCreates = result.data?.proposed_changes.filter((c) => c.type === 'file_create');
217
+ expect(fileCreates.length).toBeGreaterThan(0);
218
+ }
219
+ });
220
+
221
+ it('should use context for provenance when provided', async () => {
222
+ const content = await readFile(join(fixturesDir, 'sample-spec.md'), 'utf-8');
223
+ const context: ParseContext = {
224
+ filePath: '/path/to/spec.md',
225
+ author: 'Context Author',
226
+ repositoryPath: '/path/to/repo',
227
+ };
228
+
229
+ const result = await adapter.parse(content, context);
230
+
231
+ expect(result.success).toBe(true);
232
+ if (result.success) {
233
+ expect(result.data?.provenance.author).toBe('Context Author');
234
+ expect(result.data?.provenance.repository).toBe('/path/to/repo');
235
+ }
236
+ });
237
+
238
+ it('should handle minimal spec document', async () => {
239
+ const content = `# Specification
240
+
241
+ ## Intent
242
+
243
+ Build authentication
244
+
245
+ ## Changes
246
+
247
+ ### Files to Create
248
+
249
+ #### Create auth.ts
250
+
251
+ Authentication module`;
252
+
253
+ const result = await adapter.parse(content);
254
+
255
+ expect(result.success).toBe(true);
256
+ if (result.success) {
257
+ expect(result.data?.intent).toContain('authentication');
258
+ }
259
+ });
260
+
261
+ it('should generate consistent hashes for same content', async () => {
262
+ const content = await readFile(join(fixturesDir, 'sample-spec.md'), 'utf-8');
263
+ const fixedContext: ParseContext = {
264
+ author: 'Test Author',
265
+ timestamp: '2025-01-01T00:00:00Z',
266
+ };
267
+
268
+ const result1 = await adapter.parse(content, fixedContext);
269
+ const result2 = await adapter.parse(content, fixedContext);
270
+
271
+ expect(result1.success).toBe(true);
272
+ expect(result2.success).toBe(true);
273
+
274
+ if (result1.success && result2.success) {
275
+ expect(result1.data?.hash).toMatch(/^[a-f0-9]{64}$/);
276
+ expect(result2.data?.hash).toMatch(/^[a-f0-9]{64}$/);
277
+ }
278
+ });
279
+ });
280
+
281
+ describe('serialize', () => {
282
+ it('should serialize APS plan to SpecKit format', async () => {
283
+ const content = await readFile(join(fixturesDir, 'sample-spec.md'), 'utf-8');
284
+ const parseResult = await adapter.parse(content);
285
+
286
+ expect(parseResult.success).toBe(true);
287
+ if (!parseResult.success) return;
288
+
289
+ const serializeResult = await adapter.serialize(parseResult.data!);
290
+
291
+ expect(serializeResult.success).toBe(true);
292
+ if (serializeResult.success) {
293
+ expect(serializeResult.content).toBeDefined();
294
+ expect(serializeResult.content.length).toBeGreaterThan(0);
295
+ }
296
+ });
297
+
298
+ it('should include Specification header in serialized output', async () => {
299
+ const content = await readFile(join(fixturesDir, 'sample-spec.md'), 'utf-8');
300
+ const parseResult = await adapter.parse(content);
301
+
302
+ expect(parseResult.success).toBe(true);
303
+ if (!parseResult.success) return;
304
+
305
+ const serializeResult = await adapter.serialize(parseResult.data!);
306
+
307
+ expect(serializeResult.success).toBe(true);
308
+ if (serializeResult.success) {
309
+ expect(serializeResult.content).toMatch(/^#\s+Specification/m);
310
+ }
311
+ });
312
+
313
+ it('should include Intent section in serialized output', async () => {
314
+ const content = await readFile(join(fixturesDir, 'sample-spec.md'), 'utf-8');
315
+ const parseResult = await adapter.parse(content);
316
+
317
+ expect(parseResult.success).toBe(true);
318
+ if (!parseResult.success) return;
319
+
320
+ const serializeResult = await adapter.serialize(parseResult.data!);
321
+
322
+ expect(serializeResult.success).toBe(true);
323
+ if (serializeResult.success) {
324
+ expect(serializeResult.content).toMatch(/##\s+Intent/m);
325
+ }
326
+ });
327
+
328
+ it('should include Changes section in serialized output', async () => {
329
+ const content = await readFile(join(fixturesDir, 'sample-spec.md'), 'utf-8');
330
+ const parseResult = await adapter.parse(content);
331
+
332
+ expect(parseResult.success).toBe(true);
333
+ if (!parseResult.success) return;
334
+
335
+ const serializeResult = await adapter.serialize(parseResult.data!);
336
+
337
+ expect(serializeResult.success).toBe(true);
338
+ if (serializeResult.success) {
339
+ expect(serializeResult.content).toMatch(/##\s+Changes/m);
340
+ }
341
+ });
342
+
343
+ it('should maintain roundtrip fidelity for basic structure', async () => {
344
+ const originalContent = await readFile(join(fixturesDir, 'sample-spec.md'), 'utf-8');
345
+ const parseResult1 = await adapter.parse(originalContent);
346
+
347
+ expect(parseResult1.success).toBe(true);
348
+ if (!parseResult1.success) return;
349
+
350
+ const serializeResult = await adapter.serialize(parseResult1.data);
351
+
352
+ expect(serializeResult.success).toBe(true);
353
+ if (!serializeResult.success) return;
354
+
355
+ const parseResult2 = await adapter.parse(serializeResult.content);
356
+
357
+ expect(parseResult2.success).toBe(true);
358
+ if (!parseResult2.success) return;
359
+
360
+ // Check key properties are preserved
361
+ expect(parseResult2.data?.intent).toBeDefined();
362
+ expect(parseResult2.data?.proposed_changes.length).toBeGreaterThan(0);
363
+ });
364
+ });
365
+
366
+ describe('validate', () => {
367
+ it('should validate valid spec document', async () => {
368
+ const content = await readFile(join(fixturesDir, 'sample-spec.md'), 'utf-8');
369
+ const result = await adapter.validate(content);
370
+
371
+ expect(result.valid).toBe(true);
372
+ expect(result.summary).toContain('valid');
373
+ });
374
+
375
+ it('should reject document that is too short', () => {
376
+ const content = '# Short';
377
+ const result = adapter.validate(content);
378
+
379
+ return result.then((res) => {
380
+ expect(res.valid).toBe(false);
381
+ expect(res.issues).toBeDefined();
382
+ if (res.issues) {
383
+ const shortError = res.issues.find((i) => i.code === 'CONTENT_TOO_SHORT');
384
+ expect(shortError).toBeDefined();
385
+ }
386
+ });
387
+ });
388
+
389
+ it('should reject document with low confidence', async () => {
390
+ const content = `# Not a SpecKit Document
391
+
392
+ This is just regular markdown without any SpecKit indicators like Intent, Changes, or file structure.
393
+
394
+ It has enough content to pass the length check, but it should still fail validation.`;
395
+
396
+ const result = await adapter.validate(content);
397
+
398
+ expect(result.valid).toBe(false);
399
+ expect(result.issues).toBeDefined();
400
+ if (result.issues) {
401
+ const confidenceError = result.issues.find((i) => i.code === 'LOW_CONFIDENCE');
402
+ expect(confidenceError).toBeDefined();
403
+ }
404
+ });
405
+
406
+ it('should warn about missing Intent section', async () => {
407
+ const content = `# Specification
408
+
409
+ ## Changes
410
+
411
+ ### Files to Create
412
+
413
+ Create some file here.
414
+
415
+ This document has the basic structure but is missing the Intent section which is recommended.`;
416
+
417
+ const result = await adapter.validate(content);
418
+
419
+ expect(result.issues).toBeDefined();
420
+ if (result.issues) {
421
+ const missingIntent = result.issues.find((i) => i.code === 'MISSING_INTENT');
422
+ expect(missingIntent).toBeDefined();
423
+ if (missingIntent) {
424
+ expect(missingIntent.severity).toBe('warning');
425
+ }
426
+ }
427
+ });
428
+
429
+ it('should provide clear validation summary', async () => {
430
+ const content = await readFile(join(fixturesDir, 'sample-spec.md'), 'utf-8');
431
+ const result = await adapter.validate(content);
432
+
433
+ expect(result.summary).toBeDefined();
434
+ expect(result.summary.length).toBeGreaterThan(0);
435
+ });
436
+ });
437
+
438
+ describe('edge cases', () => {
439
+ describe('confidence scoring', () => {
440
+ it('should have high confidence with all sections', () => {
441
+ const content = `# Specification
442
+
443
+ ## Intent
444
+
445
+ Build authentication
446
+
447
+ ## Overview
448
+
449
+ Overview of the feature
450
+
451
+ ## Goals
452
+
453
+ - Goal 1
454
+ - Goal 2
455
+
456
+ ## Requirements
457
+
458
+ - Requirement 1
459
+
460
+ ## Changes
461
+
462
+ ### Files to Create
463
+
464
+ #### Create auth.ts
465
+
466
+ \`\`\`typescript
467
+ export class Auth {}
468
+ \`\`\``;
469
+
470
+ const result = adapter.detect(content);
471
+
472
+ expect(result.confidence).toBeGreaterThanOrEqual(90);
473
+ expect(result.detected).toBe(true);
474
+ });
475
+
476
+ it('should have 0 confidence for completely unrelated content', () => {
477
+ const content = 'Just some random text without any structure.';
478
+
479
+ const result = adapter.detect(content);
480
+
481
+ expect(result.confidence).toBe(0);
482
+ expect(result.detected).toBe(false);
483
+ });
484
+
485
+ it('should have partial confidence with only Specification header', () => {
486
+ const content = `# Specification
487
+
488
+ Some content but no other sections.`;
489
+
490
+ const result = adapter.detect(content);
491
+
492
+ expect(result.confidence).toBeLessThan(50);
493
+ expect(result.detected).toBe(false);
494
+ });
495
+
496
+ it('should detect "Spec" as alternative to "Specification"', () => {
497
+ const content = `# Spec
498
+
499
+ ## Intent
500
+
501
+ Build feature`;
502
+
503
+ const result = adapter.detect(content);
504
+
505
+ expect(result.detected).toBe(true);
506
+ expect(result.reason).toContain('specification-header');
507
+ });
508
+ });
509
+
510
+ describe('section variations', () => {
511
+ it('should detect "Goal" singular form', () => {
512
+ const content = `# Specification
513
+
514
+ ## Goal
515
+
516
+ Build authentication system`;
517
+
518
+ const result = adapter.detect(content);
519
+
520
+ expect(result.reason).toContain('goals-section');
521
+ });
522
+
523
+ it('should detect "Requirement" singular form', () => {
524
+ const content = `# Specification
525
+
526
+ ## Requirement
527
+
528
+ Node.js 18+`;
529
+
530
+ const result = adapter.detect(content);
531
+
532
+ expect(result.reason).toContain('requirements-section');
533
+ });
534
+
535
+ it('should detect "Change" singular form', () => {
536
+ const content = `# Specification
537
+
538
+ ## Change
539
+
540
+ ### Files to Create
541
+
542
+ New file`;
543
+
544
+ const result = adapter.detect(content);
545
+
546
+ expect(result.reason).toContain('changes');
547
+ });
548
+ });
549
+
550
+ describe('file change variations', () => {
551
+ it('should detect "create file" phrase', () => {
552
+ const content = `# Specification
553
+
554
+ ## Changes
555
+
556
+ Create file at src/auth.ts`;
557
+
558
+ const result = adapter.detect(content);
559
+
560
+ expect(result.reason).toContain('file-changes');
561
+ });
562
+
563
+ it('should detect "update file" phrase', () => {
564
+ const content = `# Specification
565
+
566
+ ## Changes
567
+
568
+ Update file src/app.ts`;
569
+
570
+ const result = adapter.detect(content);
571
+
572
+ expect(result.reason).toContain('file-changes');
573
+ });
574
+ });
575
+
576
+ describe('parsing edge cases', () => {
577
+ it('should handle spec with only Intent and Changes', async () => {
578
+ const content = `# Specification
579
+
580
+ ## Intent
581
+
582
+ Implement feature X
583
+
584
+ ## Changes
585
+
586
+ ### Files to Create
587
+
588
+ #### Create feature.ts
589
+
590
+ Feature implementation`;
591
+
592
+ const result = await adapter.parse(content);
593
+
594
+ expect(result.success).toBe(true);
595
+ });
596
+
597
+ it('should handle empty Changes section', async () => {
598
+ const content = `# Specification
599
+
600
+ ## Intent
601
+
602
+ Implement feature
603
+
604
+ ## Changes`;
605
+
606
+ const result = await adapter.parse(content);
607
+
608
+ expect(result.success).toBe(true);
609
+ });
610
+
611
+ it('should handle special characters in descriptions', async () => {
612
+ const content = `# Specification
613
+
614
+ ## Intent
615
+
616
+ Support **bold**, *italic*, and \`code\` in descriptions
617
+
618
+ ## Changes
619
+
620
+ ### Files to Create
621
+
622
+ #### Create test.ts with [links](http://example.com)
623
+
624
+ Implementation with | pipes |`;
625
+
626
+ const result = await adapter.parse(content);
627
+
628
+ expect(result.success).toBe(true);
629
+ });
630
+ });
631
+
632
+ describe('serialization edge cases', () => {
633
+ it('should handle APS plan with minimal changes', async () => {
634
+ const content = `# Specification
635
+
636
+ ## Intent
637
+
638
+ Minimal feature
639
+
640
+ ## Changes
641
+
642
+ ### Files to Create
643
+
644
+ #### Create minimal.ts
645
+
646
+ Minimal implementation`;
647
+
648
+ const parseResult = await adapter.parse(content);
649
+ expect(parseResult.success).toBe(true);
650
+ if (!parseResult.success) return;
651
+
652
+ const serializeResult = await adapter.serialize(parseResult.data!);
653
+ expect(serializeResult.success).toBe(true);
654
+ });
655
+ });
656
+ });
657
+
658
+ describe('ADAPTUP: SpecKit namespace detection', () => {
659
+ it('should detect speckit.* namespace in content', async () => {
660
+ const content = await readFile(join(fixturesDir, 'sample-spec-namespaced.md'), 'utf-8');
661
+ const result = adapter.detect(content);
662
+
663
+ expect(result.detected).toBe(true);
664
+ expect(result.reason).toContain('speckit-namespace');
665
+ });
666
+
667
+ it('should detect /speckit.clarify command', () => {
668
+ const content = `# Specification
669
+
670
+ ## Intent
671
+
672
+ Use /speckit.clarify to clarify requirements.
673
+
674
+ ## Changes
675
+
676
+ ### Files to Create
677
+
678
+ #### Create src/feature.ts
679
+
680
+ Feature implementation.`;
681
+
682
+ const result = adapter.detect(content);
683
+ expect(result.reason).toContain('speckit-namespace');
684
+ });
685
+
686
+ it('should detect speckit.analyze reference', () => {
687
+ const content = `# Specification
688
+
689
+ ## Intent
690
+
691
+ Run speckit.analyze for validation.
692
+
693
+ ## Changes
694
+
695
+ ### Files to Create
696
+
697
+ #### Create src/feature.ts
698
+
699
+ Feature implementation.`;
700
+
701
+ const result = adapter.detect(content);
702
+ expect(result.reason).toContain('speckit-namespace');
703
+ });
704
+
705
+ it('should boost confidence with namespace detection', () => {
706
+ const baseContent = `# Specification
707
+
708
+ ## Intent
709
+
710
+ Build feature
711
+
712
+ ## Changes
713
+
714
+ ### Files to Create
715
+
716
+ #### Create feature.ts
717
+
718
+ Implementation`;
719
+
720
+ const namespacedContent = `# Specification
721
+
722
+ ## Intent
723
+
724
+ Build feature with /speckit.clarify
725
+
726
+ ## Changes
727
+
728
+ ### Files to Create
729
+
730
+ #### Create feature.ts
731
+
732
+ Implementation`;
733
+
734
+ const baseResult = adapter.detect(baseContent);
735
+ const namespacedResult = adapter.detect(namespacedContent);
736
+
737
+ expect(namespacedResult.confidence).toBeGreaterThan(baseResult.confidence);
738
+ });
739
+
740
+ it('should parse namespaced spec successfully', async () => {
741
+ const content = await readFile(join(fixturesDir, 'sample-spec-namespaced.md'), 'utf-8');
742
+ const result = await adapter.parse(content);
743
+
744
+ expect(result.success).toBe(true);
745
+ if (result.success) {
746
+ expect(result.data?.intent).toBeDefined();
747
+ expect(result.data?.proposed_changes.length).toBeGreaterThan(0);
748
+ }
749
+ });
750
+ });
751
+
752
+ describe('ADAPTUP: SpecKit AGENTS.md detection', () => {
753
+ it('should boost confidence when AGENTS.md is a sibling', () => {
754
+ // Use content that has enough signals to score above 50 without the bonus rule
755
+ // Spec header (20) + Intent (15) + Changes (20) = 55 base, +15 AGENTS.md = 70
756
+ const content = `# Specification
757
+
758
+ ## Intent
759
+
760
+ Build feature
761
+
762
+ ## Changes
763
+
764
+ ### Files to Create
765
+
766
+ #### Create feature.ts
767
+
768
+ Implementation`;
769
+
770
+ const hint: PathDetectionHint = {
771
+ filePath: '/project/spec.md',
772
+ siblingFiles: ['AGENTS.md', 'README.md', 'package.json'],
773
+ };
774
+
775
+ const resultWithPath = adapter.detectWithPath(content, hint);
776
+ const resultWithout = adapter.detect(content);
777
+
778
+ expect(resultWithPath.confidence).toBeGreaterThan(resultWithout.confidence);
779
+ expect(resultWithPath.reason).toContain('agents-md');
780
+ });
781
+
782
+ it('should detect AGENTS.md case-insensitively', () => {
783
+ const content = `# Specification
784
+
785
+ ## Intent
786
+
787
+ Build feature`;
788
+
789
+ const hint: PathDetectionHint = {
790
+ filePath: '/project/spec.md',
791
+ siblingFiles: ['agents.md', 'README.md'],
792
+ };
793
+
794
+ const result = adapter.detectWithPath(content, hint);
795
+ expect(result.reason).toContain('agents-md');
796
+ });
797
+
798
+ it('should not boost when no AGENTS.md sibling', () => {
799
+ const content = `# Specification
800
+
801
+ ## Intent
802
+
803
+ Build feature`;
804
+
805
+ const hint: PathDetectionHint = {
806
+ filePath: '/project/spec.md',
807
+ siblingFiles: ['README.md', 'package.json'],
808
+ };
809
+
810
+ const result = adapter.detectWithPath(content, hint);
811
+ expect(result.reason).not.toContain('agents-md');
812
+ });
813
+
814
+ it('should combine namespace and AGENTS.md boosts', () => {
815
+ const content = `# Specification
816
+
817
+ ## Intent
818
+
819
+ Build feature with /speckit.clarify`;
820
+
821
+ const hint: PathDetectionHint = {
822
+ filePath: '/project/spec.md',
823
+ siblingFiles: ['AGENTS.md'],
824
+ };
825
+
826
+ const result = adapter.detectWithPath(content, hint);
827
+ expect(result.detected).toBe(true);
828
+ expect(result.reason).toContain('speckit-namespace');
829
+ expect(result.reason).toContain('agents-md');
830
+ });
831
+ });
832
+ });