@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.
- package/AGENTS.md +180 -0
- package/BMAD_ADAPTER_SPEC.md +489 -0
- package/LICENSE +14 -0
- package/README.md +500 -0
- package/dist/aps-markdown/adapter.d.ts +102 -0
- package/dist/aps-markdown/adapter.d.ts.map +1 -0
- package/dist/aps-markdown/adapter.js +351 -0
- package/dist/aps-markdown/index.d.ts +8 -0
- package/dist/aps-markdown/index.d.ts.map +1 -0
- package/dist/aps-markdown/index.js +7 -0
- package/dist/base/file-discovery.d.ts +63 -0
- package/dist/base/file-discovery.d.ts.map +1 -0
- package/dist/base/file-discovery.js +246 -0
- package/dist/base/index.d.ts +10 -0
- package/dist/base/index.d.ts.map +1 -0
- package/dist/base/index.js +9 -0
- package/dist/base/registry.d.ts +155 -0
- package/dist/base/registry.d.ts.map +1 -0
- package/dist/base/registry.js +227 -0
- package/dist/base/testing.d.ts +102 -0
- package/dist/base/testing.d.ts.map +1 -0
- package/dist/base/testing.js +221 -0
- package/dist/base/types.d.ts +255 -0
- package/dist/base/types.d.ts.map +1 -0
- package/dist/base/types.js +78 -0
- package/dist/base/utils.d.ts +127 -0
- package/dist/base/utils.d.ts.map +1 -0
- package/dist/base/utils.js +254 -0
- package/dist/bmad/format-adapter.d.ts +76 -0
- package/dist/bmad/format-adapter.d.ts.map +1 -0
- package/dist/bmad/format-adapter.js +186 -0
- package/dist/bmad/index.d.ts +12 -0
- package/dist/bmad/index.d.ts.map +1 -0
- package/dist/bmad/index.js +10 -0
- package/dist/bmad/parser.d.ts +12 -0
- package/dist/bmad/parser.d.ts.map +1 -0
- package/dist/bmad/parser.js +181 -0
- package/dist/bmad/serializer.d.ts +16 -0
- package/dist/bmad/serializer.d.ts.map +1 -0
- package/dist/bmad/serializer.js +170 -0
- package/dist/bmad/types.d.ts +127 -0
- package/dist/bmad/types.d.ts.map +1 -0
- package/dist/bmad/types.js +47 -0
- package/dist/bmad/utils.d.ts +120 -0
- package/dist/bmad/utils.d.ts.map +1 -0
- package/dist/bmad/utils.js +480 -0
- package/dist/common/index.d.ts +3 -0
- package/dist/common/index.d.ts.map +1 -0
- package/dist/common/index.js +2 -0
- package/dist/common/registry.d.ts +18 -0
- package/dist/common/registry.d.ts.map +1 -0
- package/dist/common/registry.js +58 -0
- package/dist/common/types.d.ts +68 -0
- package/dist/common/types.d.ts.map +1 -0
- package/dist/common/types.js +12 -0
- package/dist/generic/format-adapter.d.ts +64 -0
- package/dist/generic/format-adapter.d.ts.map +1 -0
- package/dist/generic/format-adapter.js +159 -0
- package/dist/generic/index.d.ts +10 -0
- package/dist/generic/index.d.ts.map +1 -0
- package/dist/generic/index.js +9 -0
- package/dist/generic/parser.d.ts +11 -0
- package/dist/generic/parser.d.ts.map +1 -0
- package/dist/generic/parser.js +106 -0
- package/dist/generic/serializer.d.ts +11 -0
- package/dist/generic/serializer.d.ts.map +1 -0
- package/dist/generic/serializer.js +118 -0
- package/dist/generic/types.d.ts +52 -0
- package/dist/generic/types.d.ts.map +1 -0
- package/dist/generic/types.js +6 -0
- package/dist/generic/utils.d.ts +51 -0
- package/dist/generic/utils.d.ts.map +1 -0
- package/dist/generic/utils.js +232 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/speckit/export.d.ts +22 -0
- package/dist/speckit/export.d.ts.map +1 -0
- package/dist/speckit/export.js +384 -0
- package/dist/speckit/format-adapter.d.ts +104 -0
- package/dist/speckit/format-adapter.d.ts.map +1 -0
- package/dist/speckit/format-adapter.js +488 -0
- package/dist/speckit/import-v2.d.ts +33 -0
- package/dist/speckit/import-v2.d.ts.map +1 -0
- package/dist/speckit/import-v2.js +361 -0
- package/dist/speckit/import.d.ts +16 -0
- package/dist/speckit/import.d.ts.map +1 -0
- package/dist/speckit/import.js +247 -0
- package/dist/speckit/index.d.ts +5 -0
- package/dist/speckit/index.d.ts.map +1 -0
- package/dist/speckit/index.js +4 -0
- package/dist/speckit/parser.d.ts +28 -0
- package/dist/speckit/parser.d.ts.map +1 -0
- package/dist/speckit/parser.js +283 -0
- package/dist/speckit/parsers/plan-parser.d.ts +71 -0
- package/dist/speckit/parsers/plan-parser.d.ts.map +1 -0
- package/dist/speckit/parsers/plan-parser.js +216 -0
- package/dist/speckit/parsers/spec-parser.d.ts +67 -0
- package/dist/speckit/parsers/spec-parser.d.ts.map +1 -0
- package/dist/speckit/parsers/spec-parser.js +255 -0
- package/dist/speckit/parsers/tasks-parser.d.ts +57 -0
- package/dist/speckit/parsers/tasks-parser.d.ts.map +1 -0
- package/dist/speckit/parsers/tasks-parser.js +157 -0
- package/package.json +23 -0
- package/project.json +29 -0
- package/src/__tests__/adapter-edge-cases.test.ts +937 -0
- package/src/__tests__/bmad-format-adapter.test.ts +1470 -0
- package/src/__tests__/fixtures/aps/expected-output.json +83 -0
- package/src/__tests__/fixtures/bmad/invalid-malformed-yaml.md +16 -0
- package/src/__tests__/fixtures/bmad/invalid-no-requirements.md +23 -0
- package/src/__tests__/fixtures/bmad/invalid-only-yaml.md +16 -0
- package/src/__tests__/fixtures/bmad/invalid-too-short.md +3 -0
- package/src/__tests__/fixtures/bmad/invalid-wrong-format.md +40 -0
- package/src/__tests__/fixtures/bmad/valid-agent.md +27 -0
- package/src/__tests__/fixtures/bmad/valid-architecture.md +116 -0
- package/src/__tests__/fixtures/bmad/valid-complex-prd.md +161 -0
- package/src/__tests__/fixtures/bmad/valid-epic.md +73 -0
- package/src/__tests__/fixtures/bmad/valid-minimal-prd.md +19 -0
- package/src/__tests__/fixtures/bmad/valid-prd.md +107 -0
- package/src/__tests__/fixtures/bmad/valid-story.md +107 -0
- package/src/__tests__/fixtures/bmad/valid-task.md +79 -0
- package/src/__tests__/fixtures/bmad/valid-v6-prd.md +35 -0
- package/src/__tests__/fixtures/generic/plan-detailed.md +39 -0
- package/src/__tests__/fixtures/generic/prd-simple.md +27 -0
- package/src/__tests__/fixtures/generic/rfc-example.md +26 -0
- package/src/__tests__/fixtures/generic/todo-list.md +23 -0
- package/src/__tests__/fixtures/speckit/sample-plan.md +63 -0
- package/src/__tests__/fixtures/speckit/sample-spec-namespaced.md +50 -0
- package/src/__tests__/fixtures/speckit/sample-spec.md +105 -0
- package/src/__tests__/fixtures/speckit/sample-tasks.md +87 -0
- package/src/__tests__/fixtures/speckit-official/auth-feature/plan.md +272 -0
- package/src/__tests__/fixtures/speckit-official/auth-feature/spec.md +149 -0
- package/src/__tests__/fixtures/speckit-official/auth-feature/tasks.md +169 -0
- package/src/__tests__/generic-format-adapter.test.ts +398 -0
- package/src/__tests__/speckit-export.test.ts +233 -0
- package/src/__tests__/speckit-format-adapter.test.ts +832 -0
- package/src/__tests__/speckit-import-v2.test.ts +253 -0
- package/src/__tests__/speckit-import.test.ts +209 -0
- package/src/__tests__/speckit-parser.test.ts +219 -0
- package/src/__tests__/speckit-spec-parser.test.ts +120 -0
- package/src/aps-markdown/__tests__/__fixtures__/simple-leaf.aps.md +17 -0
- package/src/aps-markdown/__tests__/adapter.test.ts +393 -0
- package/src/aps-markdown/adapter.ts +455 -0
- package/src/aps-markdown/index.ts +8 -0
- package/src/base/__tests__/registry.test.ts +515 -0
- package/src/base/file-discovery.ts +305 -0
- package/src/base/index.ts +10 -0
- package/src/base/registry.ts +263 -0
- package/src/base/testing.ts +334 -0
- package/src/base/types.ts +342 -0
- package/src/base/utils.ts +306 -0
- package/src/bmad/format-adapter.ts +227 -0
- package/src/bmad/index.ts +21 -0
- package/src/bmad/parser.ts +224 -0
- package/src/bmad/serializer.ts +206 -0
- package/src/bmad/types.ts +135 -0
- package/src/bmad/utils.ts +575 -0
- package/src/common/index.ts +2 -0
- package/src/common/registry.ts +72 -0
- package/src/common/types.ts +84 -0
- package/src/generic/__tests__/serializer.test.ts +167 -0
- package/src/generic/format-adapter.ts +200 -0
- package/src/generic/index.ts +11 -0
- package/src/generic/parser.ts +129 -0
- package/src/generic/serializer.ts +134 -0
- package/src/generic/types.ts +53 -0
- package/src/generic/utils.ts +270 -0
- package/src/index.ts +48 -0
- package/src/speckit/export.ts +489 -0
- package/src/speckit/format-adapter.ts +595 -0
- package/src/speckit/import-v2.ts +445 -0
- package/src/speckit/import.ts +305 -0
- package/src/speckit/index.ts +4 -0
- package/src/speckit/parser.ts +351 -0
- package/src/speckit/parsers/plan-parser.ts +342 -0
- package/src/speckit/parsers/spec-parser.ts +379 -0
- package/src/speckit/parsers/tasks-parser.ts +246 -0
- package/tsconfig.json +26 -0
- package/tsconfig.lib.json +21 -0
- package/tsconfig.lib.tsbuildinfo +1 -0
- package/tsconfig.spec.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
- 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
|
+
});
|