@eddacraft/anvil-aps 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 +155 -0
- package/LICENSE +14 -0
- package/README.md +57 -0
- package/TODO.md +40 -0
- package/dist/filter/context-bundle.d.ts +81 -0
- package/dist/filter/context-bundle.d.ts.map +1 -0
- package/dist/filter/context-bundle.js +230 -0
- package/dist/filter/index.d.ts +85 -0
- package/dist/filter/index.d.ts.map +1 -0
- package/dist/filter/index.js +169 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/loader/index.d.ts +80 -0
- package/dist/loader/index.d.ts.map +1 -0
- package/dist/loader/index.js +253 -0
- package/dist/parser/index.d.ts +24 -0
- package/dist/parser/index.d.ts.map +1 -0
- package/dist/parser/index.js +22 -0
- package/dist/parser/parse-document.d.ts +17 -0
- package/dist/parser/parse-document.d.ts.map +1 -0
- package/dist/parser/parse-document.js +219 -0
- package/dist/parser/parse-index.d.ts +31 -0
- package/dist/parser/parse-index.d.ts.map +1 -0
- package/dist/parser/parse-index.js +251 -0
- package/dist/parser/parse-task.d.ts +30 -0
- package/dist/parser/parse-task.d.ts.map +1 -0
- package/dist/parser/parse-task.js +261 -0
- package/dist/state/index.d.ts +307 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +689 -0
- package/dist/templates/generator.d.ts +71 -0
- package/dist/templates/generator.d.ts.map +1 -0
- package/dist/templates/generator.js +723 -0
- package/dist/templates/index.d.ts +5 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +4 -0
- package/dist/types/index.d.ts +131 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +107 -0
- package/dist/validator/index.d.ts +83 -0
- package/dist/validator/index.d.ts.map +1 -0
- package/dist/validator/index.js +611 -0
- package/docs/APS-Anvil-Integration.md +750 -0
- package/docs/APS-Conventions.md +635 -0
- package/docs/APS-NonGoals.md +455 -0
- package/docs/APS-Planning-Spec-v0.1.md +362 -0
- package/examples/README.md +170 -0
- package/examples/feature-auth.aps.md +87 -0
- package/examples/refactor-error-handling.aps.md +119 -0
- package/examples/system-ecommerce/APS.md +57 -0
- package/examples/system-ecommerce/modules/auth.aps.md +38 -0
- package/examples/system-ecommerce/modules/cart.aps.md +53 -0
- package/examples/system-ecommerce/modules/payments.aps.md +68 -0
- package/examples/system-ecommerce/modules/products.aps.md +53 -0
- package/package.json +34 -0
- package/project.json +37 -0
- package/scripts/generate-templates.js +33 -0
- package/src/filter/context-bundle.ts +312 -0
- package/src/filter/filter.test.ts +317 -0
- package/src/filter/index.ts +249 -0
- package/src/index.ts +16 -0
- package/src/loader/index.ts +364 -0
- package/src/loader/loader.test.ts +224 -0
- package/src/parser/__fixtures__/invalid-task-id-not-padded.aps.md +7 -0
- package/src/parser/__fixtures__/invalid-task-id.aps.md +8 -0
- package/src/parser/__fixtures__/minimal-task.aps.md +7 -0
- package/src/parser/__fixtures__/non-scope-hyphenated.aps.md +10 -0
- package/src/parser/__fixtures__/simple-index.aps.md +35 -0
- package/src/parser/__fixtures__/simple-plan.aps.md +19 -0
- package/src/parser/index.ts +30 -0
- package/src/parser/parse-document.test.ts +603 -0
- package/src/parser/parse-document.ts +262 -0
- package/src/parser/parse-index.test.ts +316 -0
- package/src/parser/parse-index.ts +298 -0
- package/src/parser/parse-task.test.ts +476 -0
- package/src/parser/parse-task.ts +325 -0
- package/src/state/__fixtures__/invalid-plan.aps.md +9 -0
- package/src/state/__fixtures__/test-plan.aps.md +20 -0
- package/src/state/index.ts +879 -0
- package/src/state/state.test.ts +645 -0
- package/src/templates/generator.test.ts +378 -0
- package/src/templates/generator.ts +776 -0
- package/src/templates/index.ts +5 -0
- package/src/types/index.ts +168 -0
- package/src/validator/__fixtures__/broken-links.aps.md +10 -0
- package/src/validator/__fixtures__/circular-deps-index.aps.md +26 -0
- package/src/validator/__fixtures__/circular-modules/module-a.aps.md +9 -0
- package/src/validator/__fixtures__/circular-modules/module-b.aps.md +9 -0
- package/src/validator/__fixtures__/circular-modules/module-c.aps.md +9 -0
- package/src/validator/__fixtures__/dup-modules/module-a.aps.md +9 -0
- package/src/validator/__fixtures__/dup-modules/module-b.aps.md +9 -0
- package/src/validator/__fixtures__/duplicate-ids-index.aps.md +15 -0
- package/src/validator/__fixtures__/invalid-task-id.aps.md +17 -0
- package/src/validator/__fixtures__/missing-confidence.aps.md +9 -0
- package/src/validator/__fixtures__/missing-h1.aps.md +5 -0
- package/src/validator/__fixtures__/missing-intent.aps.md +9 -0
- package/src/validator/__fixtures__/missing-modules-section.aps.md +7 -0
- package/src/validator/__fixtures__/missing-tasks-section.aps.md +7 -0
- package/src/validator/__fixtures__/modules/auth.aps.md +17 -0
- package/src/validator/__fixtures__/modules/payments.aps.md +13 -0
- package/src/validator/__fixtures__/scope-mismatch.aps.md +14 -0
- package/src/validator/__fixtures__/valid-index.aps.md +24 -0
- package/src/validator/__fixtures__/valid-leaf.aps.md +22 -0
- package/src/validator/index.ts +776 -0
- package/src/validator/validator.test.ts +269 -0
- package/templates/index-full.md +94 -0
- package/templates/index-minimal.md +16 -0
- package/templates/index-template.md +63 -0
- package/templates/leaf-full.md +76 -0
- package/templates/leaf-minimal.md +14 -0
- package/templates/leaf-template.md +55 -0
- package/templates/simple-full.md +56 -0
- package/templates/simple-minimal.md +14 -0
- package/templates/simple-template.md +30 -0
- package/tsconfig.json +19 -0
- package/tsconfig.lib.json +14 -0
- package/tsconfig.lib.tsbuildinfo +1 -0
- package/tsconfig.spec.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for parse-document module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { promises as fs } from 'node:fs';
|
|
7
|
+
import { dirname, join } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { parseDocument } from './parse-document.js';
|
|
10
|
+
import { ParseError } from '../types/index.js';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const FIXTURES_DIR = join(__dirname, '__fixtures__');
|
|
14
|
+
|
|
15
|
+
async function loadFixture(filename: string): Promise<string> {
|
|
16
|
+
return fs.readFile(join(FIXTURES_DIR, filename), 'utf-8');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('parseDocument', () => {
|
|
20
|
+
describe('simple plans', () => {
|
|
21
|
+
it('should parse a simple plan with multiple tasks', async () => {
|
|
22
|
+
const content = await loadFixture('simple-plan.aps.md');
|
|
23
|
+
const doc = await parseDocument(content, 'simple-plan.aps.md');
|
|
24
|
+
|
|
25
|
+
expect(doc.title).toBe('Simple Feature Plan');
|
|
26
|
+
expect(doc.metadata).toEqual({
|
|
27
|
+
scope: 'TEST',
|
|
28
|
+
owner: '@test',
|
|
29
|
+
priority: 'high',
|
|
30
|
+
});
|
|
31
|
+
expect(doc.tasks).toHaveLength(2);
|
|
32
|
+
|
|
33
|
+
// First task with all fields
|
|
34
|
+
expect(doc.tasks[0]).toMatchObject({
|
|
35
|
+
id: 'TEST-001',
|
|
36
|
+
title: 'First task',
|
|
37
|
+
intent: 'This is a simple task to test parsing',
|
|
38
|
+
expectedOutcome: 'Task should be parsed correctly',
|
|
39
|
+
confidence: 'high',
|
|
40
|
+
scopes: ['TEST'],
|
|
41
|
+
tags: ['example', 'simple'],
|
|
42
|
+
dependencies: ['TEST-000'],
|
|
43
|
+
inputs: ['Input one', 'Input two'],
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Second task with minimal fields
|
|
47
|
+
expect(doc.tasks[1]).toMatchObject({
|
|
48
|
+
id: 'TEST-002',
|
|
49
|
+
title: 'Second task',
|
|
50
|
+
intent: 'Another task without all fields',
|
|
51
|
+
confidence: 'medium',
|
|
52
|
+
scopes: ['TEST', 'API'],
|
|
53
|
+
tags: ['minimal'],
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should parse a minimal task with only required fields', async () => {
|
|
58
|
+
const content = await loadFixture('minimal-task.aps.md');
|
|
59
|
+
const doc = await parseDocument(content, 'minimal-task.aps.md');
|
|
60
|
+
|
|
61
|
+
expect(doc.title).toBe('Minimal Task');
|
|
62
|
+
expect(doc.tasks).toHaveLength(1);
|
|
63
|
+
expect(doc.tasks[0]).toMatchObject({
|
|
64
|
+
id: 'MIN-001',
|
|
65
|
+
title: 'Minimal task with only required fields',
|
|
66
|
+
intent: 'This task only has the required Intent field',
|
|
67
|
+
confidence: 'medium', // default value
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should parse real example file (feature-auth.aps.md)', async () => {
|
|
72
|
+
const content = await fs.readFile(
|
|
73
|
+
join(__dirname, '../../examples/feature-auth.aps.md'),
|
|
74
|
+
'utf-8'
|
|
75
|
+
);
|
|
76
|
+
const doc = await parseDocument(content, 'feature-auth.aps.md');
|
|
77
|
+
|
|
78
|
+
expect(doc.title).toBe('Feature: User Authentication');
|
|
79
|
+
expect(doc.metadata).toEqual({
|
|
80
|
+
scope: 'AUTH',
|
|
81
|
+
owner: '@alice',
|
|
82
|
+
priority: 'high',
|
|
83
|
+
});
|
|
84
|
+
expect(doc.tasks).toHaveLength(8);
|
|
85
|
+
|
|
86
|
+
// Spot check a few tasks
|
|
87
|
+
expect(doc.tasks[0].id).toBe('AUTH-001');
|
|
88
|
+
expect(doc.tasks[0].title).toBe('Create user database model');
|
|
89
|
+
expect(doc.tasks[0].scopes).toEqual(['AUTH', 'DB']);
|
|
90
|
+
|
|
91
|
+
expect(doc.tasks[7].id).toBe('AUTH-008');
|
|
92
|
+
expect(doc.tasks[7].dependencies).toEqual(['AUTH-003', 'AUTH-005', 'AUTH-007']);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should parse hyphenated Non-scope field', async () => {
|
|
96
|
+
const content = await loadFixture('non-scope-hyphenated.aps.md');
|
|
97
|
+
const doc = await parseDocument(content, 'non-scope-hyphenated.aps.md');
|
|
98
|
+
|
|
99
|
+
expect(doc.title).toBe('Test Non-scope Field');
|
|
100
|
+
expect(doc.tasks).toHaveLength(1);
|
|
101
|
+
expect(doc.tasks[0]).toMatchObject({
|
|
102
|
+
id: 'TEST-001',
|
|
103
|
+
title: 'Test hyphenated Non-scope field',
|
|
104
|
+
intent: 'Test that Non-scope field with hyphen is parsed correctly',
|
|
105
|
+
nonScope: ['database', 'external APIs', 'authentication'],
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('error handling', () => {
|
|
111
|
+
it('should throw ParseError for invalid task ID format', async () => {
|
|
112
|
+
const content = await loadFixture('invalid-task-id.aps.md');
|
|
113
|
+
|
|
114
|
+
await expect(parseDocument(content, 'invalid-task-id.aps.md')).rejects.toThrow(ParseError);
|
|
115
|
+
await expect(parseDocument(content, 'invalid-task-id.aps.md')).rejects.toThrow(
|
|
116
|
+
/Invalid task heading format/
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should throw ParseError for non-zero-padded task ID', async () => {
|
|
121
|
+
const content = await loadFixture('invalid-task-id-not-padded.aps.md');
|
|
122
|
+
|
|
123
|
+
await expect(parseDocument(content, 'invalid-task-id-not-padded.aps.md')).rejects.toThrow(
|
|
124
|
+
ParseError
|
|
125
|
+
);
|
|
126
|
+
await expect(parseDocument(content, 'invalid-task-id-not-padded.aps.md')).rejects.toThrow(
|
|
127
|
+
/Invalid task heading format/
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('should throw ParseError for document without H1 title', async () => {
|
|
132
|
+
const content = '## This is H2\n\nNo H1 title';
|
|
133
|
+
|
|
134
|
+
await expect(parseDocument(content, 'no-title.md')).rejects.toThrow(ParseError);
|
|
135
|
+
await expect(parseDocument(content, 'no-title.md')).rejects.toThrow(
|
|
136
|
+
/Document must have an H1 title/
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should throw ParseError for task without Intent field', async () => {
|
|
141
|
+
const content = `# Plan
|
|
142
|
+
|
|
143
|
+
## Tasks
|
|
144
|
+
|
|
145
|
+
### TEST-001: Task without intent
|
|
146
|
+
|
|
147
|
+
**Expected Outcome:** This should fail`;
|
|
148
|
+
|
|
149
|
+
await expect(parseDocument(content, 'no-intent.md')).rejects.toThrow(ParseError);
|
|
150
|
+
await expect(parseDocument(content, 'no-intent.md')).rejects.toThrow(
|
|
151
|
+
/missing required field: Intent/
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('confidence levels', () => {
|
|
157
|
+
it('should parse all confidence levels correctly', async () => {
|
|
158
|
+
const content = `# Confidence Test
|
|
159
|
+
|
|
160
|
+
## Tasks
|
|
161
|
+
|
|
162
|
+
### LOW-001: Low confidence task
|
|
163
|
+
|
|
164
|
+
**Intent:** Low confidence task
|
|
165
|
+
**Confidence:** low
|
|
166
|
+
|
|
167
|
+
### MED-001: Medium confidence task
|
|
168
|
+
|
|
169
|
+
**Intent:** Medium confidence task
|
|
170
|
+
**Confidence:** medium
|
|
171
|
+
|
|
172
|
+
### HIGH-001: High confidence task
|
|
173
|
+
|
|
174
|
+
**Intent:** High confidence task
|
|
175
|
+
**Confidence:** high
|
|
176
|
+
`;
|
|
177
|
+
|
|
178
|
+
const doc = await parseDocument(content);
|
|
179
|
+
|
|
180
|
+
expect(doc.tasks[0].confidence).toBe('low');
|
|
181
|
+
expect(doc.tasks[1].confidence).toBe('medium');
|
|
182
|
+
expect(doc.tasks[2].confidence).toBe('high');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should default to medium confidence when not specified', async () => {
|
|
186
|
+
const content = `# Plan
|
|
187
|
+
|
|
188
|
+
## Tasks
|
|
189
|
+
|
|
190
|
+
### TEST-001: Task without confidence
|
|
191
|
+
|
|
192
|
+
**Intent:** No confidence specified`;
|
|
193
|
+
|
|
194
|
+
const doc = await parseDocument(content);
|
|
195
|
+
|
|
196
|
+
expect(doc.tasks[0].confidence).toBe('medium');
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('metadata parsing', () => {
|
|
201
|
+
it('should parse module metadata from line after H1', async () => {
|
|
202
|
+
const content = `# My Module
|
|
203
|
+
|
|
204
|
+
**Scope:** AUTH **Owner:** @alice **Priority:** high
|
|
205
|
+
|
|
206
|
+
## Tasks
|
|
207
|
+
|
|
208
|
+
### AUTH-001: Task
|
|
209
|
+
|
|
210
|
+
**Intent:** Do something`;
|
|
211
|
+
|
|
212
|
+
const doc = await parseDocument(content);
|
|
213
|
+
|
|
214
|
+
expect(doc.metadata).toEqual({
|
|
215
|
+
scope: 'AUTH',
|
|
216
|
+
owner: '@alice',
|
|
217
|
+
priority: 'high',
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should handle missing optional metadata fields', async () => {
|
|
222
|
+
const content = `# My Module
|
|
223
|
+
|
|
224
|
+
**Scope:** TEST
|
|
225
|
+
|
|
226
|
+
## Tasks
|
|
227
|
+
|
|
228
|
+
### TEST-001: Task
|
|
229
|
+
|
|
230
|
+
**Intent:** Do something`;
|
|
231
|
+
|
|
232
|
+
const doc = await parseDocument(content);
|
|
233
|
+
|
|
234
|
+
expect(doc.metadata).toEqual({
|
|
235
|
+
scope: 'TEST',
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('ID field parsing', () => {
|
|
241
|
+
it('should parse ID field as alias for Scope', async () => {
|
|
242
|
+
const content = `# My Module
|
|
243
|
+
|
|
244
|
+
**ID:** AUTH **Owner:** @alice
|
|
245
|
+
|
|
246
|
+
## Tasks
|
|
247
|
+
|
|
248
|
+
### AUTH-001: Task
|
|
249
|
+
|
|
250
|
+
**Intent:** Do something`;
|
|
251
|
+
|
|
252
|
+
const doc = await parseDocument(content);
|
|
253
|
+
|
|
254
|
+
expect(doc.metadata).toEqual({
|
|
255
|
+
scope: 'AUTH',
|
|
256
|
+
owner: '@alice',
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should parse Scope field (legacy) the same as ID', async () => {
|
|
261
|
+
const content = `# My Module
|
|
262
|
+
|
|
263
|
+
**Scope:** AUTH **Owner:** @alice
|
|
264
|
+
|
|
265
|
+
## Tasks
|
|
266
|
+
|
|
267
|
+
### AUTH-001: Task
|
|
268
|
+
|
|
269
|
+
**Intent:** Do something`;
|
|
270
|
+
|
|
271
|
+
const doc = await parseDocument(content);
|
|
272
|
+
|
|
273
|
+
expect(doc.metadata).toEqual({
|
|
274
|
+
scope: 'AUTH',
|
|
275
|
+
owner: '@alice',
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe('status normalization', () => {
|
|
281
|
+
it('should normalize legacy Draft to Proposed', async () => {
|
|
282
|
+
const content = `# My Module
|
|
283
|
+
|
|
284
|
+
**Scope:** TEST **Status:** Draft
|
|
285
|
+
|
|
286
|
+
## Tasks
|
|
287
|
+
|
|
288
|
+
### TEST-001: Task
|
|
289
|
+
|
|
290
|
+
**Intent:** Do something`;
|
|
291
|
+
|
|
292
|
+
const doc = await parseDocument(content);
|
|
293
|
+
expect(doc.metadata?.status).toBe('Proposed');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should normalize legacy Complete to Done', async () => {
|
|
297
|
+
const content = `# My Module
|
|
298
|
+
|
|
299
|
+
**Scope:** TEST **Status:** Complete
|
|
300
|
+
|
|
301
|
+
## Tasks
|
|
302
|
+
|
|
303
|
+
### TEST-001: Task
|
|
304
|
+
|
|
305
|
+
**Intent:** Do something`;
|
|
306
|
+
|
|
307
|
+
const doc = await parseDocument(content);
|
|
308
|
+
expect(doc.metadata?.status).toBe('Done');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should keep current spec values as-is', async () => {
|
|
312
|
+
const content = `# My Module
|
|
313
|
+
|
|
314
|
+
**Scope:** TEST **Status:** Proposed
|
|
315
|
+
|
|
316
|
+
## Tasks
|
|
317
|
+
|
|
318
|
+
### TEST-001: Task
|
|
319
|
+
|
|
320
|
+
**Intent:** Do something`;
|
|
321
|
+
|
|
322
|
+
const doc = await parseDocument(content);
|
|
323
|
+
expect(doc.metadata?.status).toBe('Proposed');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should accept all valid status values', async () => {
|
|
327
|
+
for (const [input, expected] of [
|
|
328
|
+
['Proposed', 'Proposed'],
|
|
329
|
+
['Ready', 'Ready'],
|
|
330
|
+
['In Progress', 'In Progress'],
|
|
331
|
+
['Done', 'Done'],
|
|
332
|
+
['Blocked', 'Blocked'],
|
|
333
|
+
] as const) {
|
|
334
|
+
const content = `# Module
|
|
335
|
+
|
|
336
|
+
**Scope:** TEST **Status:** ${input}
|
|
337
|
+
|
|
338
|
+
## Tasks
|
|
339
|
+
|
|
340
|
+
### TEST-001: Task
|
|
341
|
+
|
|
342
|
+
**Intent:** Do something`;
|
|
343
|
+
|
|
344
|
+
const doc = await parseDocument(content);
|
|
345
|
+
expect(doc.metadata?.status).toBe(expected);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
describe('packages parsing', () => {
|
|
351
|
+
it('should parse comma-separated Packages field', async () => {
|
|
352
|
+
const content = `# My Module
|
|
353
|
+
|
|
354
|
+
**Scope:** TEST **Packages:** @app/core, @app/utils
|
|
355
|
+
|
|
356
|
+
## Tasks
|
|
357
|
+
|
|
358
|
+
### TEST-001: Task
|
|
359
|
+
|
|
360
|
+
**Intent:** Do something`;
|
|
361
|
+
|
|
362
|
+
const doc = await parseDocument(content);
|
|
363
|
+
expect(doc.metadata?.packages).toEqual(['@app/core', '@app/utils']);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('should handle Packages: (none) as empty array', async () => {
|
|
367
|
+
const content = `# My Module
|
|
368
|
+
|
|
369
|
+
**Scope:** TEST **Packages:** (none)
|
|
370
|
+
|
|
371
|
+
## Tasks
|
|
372
|
+
|
|
373
|
+
### TEST-001: Task
|
|
374
|
+
|
|
375
|
+
**Intent:** Do something`;
|
|
376
|
+
|
|
377
|
+
const doc = await parseDocument(content);
|
|
378
|
+
expect(doc.metadata?.packages).toEqual([]);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('should handle empty Packages value as empty array', async () => {
|
|
382
|
+
const content = `# My Module
|
|
383
|
+
|
|
384
|
+
**Scope:** TEST **Packages:**
|
|
385
|
+
|
|
386
|
+
## Tasks
|
|
387
|
+
|
|
388
|
+
### TEST-001: Task
|
|
389
|
+
|
|
390
|
+
**Intent:** Do something`;
|
|
391
|
+
|
|
392
|
+
const doc = await parseDocument(content);
|
|
393
|
+
expect(doc.metadata?.packages).toEqual([]);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should filter empty entries from Packages', async () => {
|
|
397
|
+
const content = `# My Module
|
|
398
|
+
|
|
399
|
+
**Scope:** TEST **Packages:** @app/core,, @app/utils
|
|
400
|
+
|
|
401
|
+
## Tasks
|
|
402
|
+
|
|
403
|
+
### TEST-001: Task
|
|
404
|
+
|
|
405
|
+
**Intent:** Do something`;
|
|
406
|
+
|
|
407
|
+
const doc = await parseDocument(content);
|
|
408
|
+
expect(doc.metadata?.packages).toEqual(['@app/core', '@app/utils']);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
describe('task packages parsing', () => {
|
|
413
|
+
it('should parse Packages field on tasks', async () => {
|
|
414
|
+
const content = `# Plan
|
|
415
|
+
|
|
416
|
+
## Tasks
|
|
417
|
+
|
|
418
|
+
### TEST-001: Task with packages
|
|
419
|
+
|
|
420
|
+
**Intent:** Do something
|
|
421
|
+
**Packages:** @app/core, @app/utils`;
|
|
422
|
+
|
|
423
|
+
const doc = await parseDocument(content);
|
|
424
|
+
expect(doc.tasks[0].packages).toEqual(['@app/core', '@app/utils']);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('should handle Packages: (none) on tasks as empty array', async () => {
|
|
428
|
+
const content = `# Plan
|
|
429
|
+
|
|
430
|
+
## Tasks
|
|
431
|
+
|
|
432
|
+
### TEST-001: Task with no packages
|
|
433
|
+
|
|
434
|
+
**Intent:** Do something
|
|
435
|
+
**Packages:** (none)`;
|
|
436
|
+
|
|
437
|
+
const doc = await parseDocument(content);
|
|
438
|
+
expect(doc.tasks[0].packages).toEqual([]);
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
describe('source tracking', () => {
|
|
443
|
+
it('should include source path in parsed document', async () => {
|
|
444
|
+
const content = `# Plan
|
|
445
|
+
|
|
446
|
+
## Tasks
|
|
447
|
+
|
|
448
|
+
### TEST-001: Task
|
|
449
|
+
|
|
450
|
+
**Intent:** Test`;
|
|
451
|
+
|
|
452
|
+
const doc = await parseDocument(content, '/path/to/plan.aps.md');
|
|
453
|
+
|
|
454
|
+
expect(doc.sourcePath).toBe('/path/to/plan.aps.md');
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('should include source path and line number in tasks', async () => {
|
|
458
|
+
const content = `# Plan
|
|
459
|
+
|
|
460
|
+
## Tasks
|
|
461
|
+
|
|
462
|
+
### TEST-001: First task
|
|
463
|
+
|
|
464
|
+
**Intent:** First
|
|
465
|
+
|
|
466
|
+
### TEST-002: Second task
|
|
467
|
+
|
|
468
|
+
**Intent:** Second`;
|
|
469
|
+
|
|
470
|
+
const doc = await parseDocument(content, 'plan.aps.md');
|
|
471
|
+
|
|
472
|
+
expect(doc.tasks[0].sourcePath).toBe('plan.aps.md');
|
|
473
|
+
expect(doc.tasks[0].sourceLineNumber).toBeGreaterThan(0);
|
|
474
|
+
|
|
475
|
+
expect(doc.tasks[1].sourcePath).toBe('plan.aps.md');
|
|
476
|
+
expect(doc.tasks[1].sourceLineNumber).toBeGreaterThan(doc.tasks[0].sourceLineNumber!);
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
describe('inputs parsing', () => {
|
|
481
|
+
it('should parse inline Inputs text as single-item array', async () => {
|
|
482
|
+
const content = `# Plan
|
|
483
|
+
|
|
484
|
+
## Tasks
|
|
485
|
+
|
|
486
|
+
### TEST-001: Task with inline inputs
|
|
487
|
+
|
|
488
|
+
**Intent:** Do something
|
|
489
|
+
**Inputs:** Database credentials required
|
|
490
|
+
`;
|
|
491
|
+
|
|
492
|
+
const doc = await parseDocument(content);
|
|
493
|
+
|
|
494
|
+
expect(doc.tasks[0].inputs).toEqual(['Database credentials required']);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('should parse Inputs list', async () => {
|
|
498
|
+
const content = `# Plan
|
|
499
|
+
|
|
500
|
+
## Tasks
|
|
501
|
+
|
|
502
|
+
### TEST-001: Task with input list
|
|
503
|
+
|
|
504
|
+
**Intent:** Do something
|
|
505
|
+
**Inputs:**
|
|
506
|
+
|
|
507
|
+
- First input
|
|
508
|
+
- Second input
|
|
509
|
+
`;
|
|
510
|
+
|
|
511
|
+
const doc = await parseDocument(content);
|
|
512
|
+
|
|
513
|
+
expect(doc.tasks[0].inputs).toEqual(['First input', 'Second input']);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('should prefer list over inline text when both present', async () => {
|
|
517
|
+
// This is an edge case - if there's inline text but also a list follows
|
|
518
|
+
const content = `# Plan
|
|
519
|
+
|
|
520
|
+
## Tasks
|
|
521
|
+
|
|
522
|
+
### TEST-001: Task with both
|
|
523
|
+
|
|
524
|
+
**Intent:** Do something
|
|
525
|
+
**Inputs:** Inline text here
|
|
526
|
+
|
|
527
|
+
- List item one
|
|
528
|
+
- List item two
|
|
529
|
+
`;
|
|
530
|
+
|
|
531
|
+
const doc = await parseDocument(content);
|
|
532
|
+
|
|
533
|
+
// List should take precedence
|
|
534
|
+
expect(doc.tasks[0].inputs).toEqual(['List item one', 'List item two']);
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
describe('validation field parsing', () => {
|
|
539
|
+
it('should parse validation command with inline code (backticks)', async () => {
|
|
540
|
+
const content = `# Plan
|
|
541
|
+
|
|
542
|
+
## Tasks
|
|
543
|
+
|
|
544
|
+
### TEST-001: Task with validation command
|
|
545
|
+
|
|
546
|
+
**Intent:** Do something
|
|
547
|
+
**Validation:** \`pnpm test\`
|
|
548
|
+
`;
|
|
549
|
+
|
|
550
|
+
const doc = await parseDocument(content);
|
|
551
|
+
|
|
552
|
+
expect(doc.tasks[0].validation).toBe('pnpm test');
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it('should parse validation command with plain text', async () => {
|
|
556
|
+
const content = `# Plan
|
|
557
|
+
|
|
558
|
+
## Tasks
|
|
559
|
+
|
|
560
|
+
### TEST-001: Task with plain validation
|
|
561
|
+
|
|
562
|
+
**Intent:** Do something
|
|
563
|
+
**Validation:** npm run test
|
|
564
|
+
`;
|
|
565
|
+
|
|
566
|
+
const doc = await parseDocument(content);
|
|
567
|
+
|
|
568
|
+
expect(doc.tasks[0].validation).toBe('npm run test');
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it('should parse Test field as alias for Validation', async () => {
|
|
572
|
+
const content = `# Plan
|
|
573
|
+
|
|
574
|
+
## Tasks
|
|
575
|
+
|
|
576
|
+
### TEST-001: Task with Test field
|
|
577
|
+
|
|
578
|
+
**Intent:** Do something
|
|
579
|
+
**Test:** \`pnpm nx run test\`
|
|
580
|
+
`;
|
|
581
|
+
|
|
582
|
+
const doc = await parseDocument(content);
|
|
583
|
+
|
|
584
|
+
expect(doc.tasks[0].validation).toBe('pnpm nx run test');
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('should parse validation with mixed inline code and text', async () => {
|
|
588
|
+
const content = `# Plan
|
|
589
|
+
|
|
590
|
+
## Tasks
|
|
591
|
+
|
|
592
|
+
### TEST-001: Task with mixed validation
|
|
593
|
+
|
|
594
|
+
**Intent:** Do something
|
|
595
|
+
**Validation:** Run \`pnpm test\` after changes
|
|
596
|
+
`;
|
|
597
|
+
|
|
598
|
+
const doc = await parseDocument(content);
|
|
599
|
+
|
|
600
|
+
expect(doc.tasks[0].validation).toBe('Run pnpm test after changes');
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
});
|