@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,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
|
+
});
|