@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.
Files changed (121) hide show
  1. package/AGENTS.md +155 -0
  2. package/LICENSE +14 -0
  3. package/README.md +57 -0
  4. package/TODO.md +40 -0
  5. package/dist/filter/context-bundle.d.ts +81 -0
  6. package/dist/filter/context-bundle.d.ts.map +1 -0
  7. package/dist/filter/context-bundle.js +230 -0
  8. package/dist/filter/index.d.ts +85 -0
  9. package/dist/filter/index.d.ts.map +1 -0
  10. package/dist/filter/index.js +169 -0
  11. package/dist/index.d.ts +16 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +15 -0
  14. package/dist/loader/index.d.ts +80 -0
  15. package/dist/loader/index.d.ts.map +1 -0
  16. package/dist/loader/index.js +253 -0
  17. package/dist/parser/index.d.ts +24 -0
  18. package/dist/parser/index.d.ts.map +1 -0
  19. package/dist/parser/index.js +22 -0
  20. package/dist/parser/parse-document.d.ts +17 -0
  21. package/dist/parser/parse-document.d.ts.map +1 -0
  22. package/dist/parser/parse-document.js +219 -0
  23. package/dist/parser/parse-index.d.ts +31 -0
  24. package/dist/parser/parse-index.d.ts.map +1 -0
  25. package/dist/parser/parse-index.js +251 -0
  26. package/dist/parser/parse-task.d.ts +30 -0
  27. package/dist/parser/parse-task.d.ts.map +1 -0
  28. package/dist/parser/parse-task.js +261 -0
  29. package/dist/state/index.d.ts +307 -0
  30. package/dist/state/index.d.ts.map +1 -0
  31. package/dist/state/index.js +689 -0
  32. package/dist/templates/generator.d.ts +71 -0
  33. package/dist/templates/generator.d.ts.map +1 -0
  34. package/dist/templates/generator.js +723 -0
  35. package/dist/templates/index.d.ts +5 -0
  36. package/dist/templates/index.d.ts.map +1 -0
  37. package/dist/templates/index.js +4 -0
  38. package/dist/types/index.d.ts +131 -0
  39. package/dist/types/index.d.ts.map +1 -0
  40. package/dist/types/index.js +107 -0
  41. package/dist/validator/index.d.ts +83 -0
  42. package/dist/validator/index.d.ts.map +1 -0
  43. package/dist/validator/index.js +611 -0
  44. package/docs/APS-Anvil-Integration.md +750 -0
  45. package/docs/APS-Conventions.md +635 -0
  46. package/docs/APS-NonGoals.md +455 -0
  47. package/docs/APS-Planning-Spec-v0.1.md +362 -0
  48. package/examples/README.md +170 -0
  49. package/examples/feature-auth.aps.md +87 -0
  50. package/examples/refactor-error-handling.aps.md +119 -0
  51. package/examples/system-ecommerce/APS.md +57 -0
  52. package/examples/system-ecommerce/modules/auth.aps.md +38 -0
  53. package/examples/system-ecommerce/modules/cart.aps.md +53 -0
  54. package/examples/system-ecommerce/modules/payments.aps.md +68 -0
  55. package/examples/system-ecommerce/modules/products.aps.md +53 -0
  56. package/package.json +34 -0
  57. package/project.json +37 -0
  58. package/scripts/generate-templates.js +33 -0
  59. package/src/filter/context-bundle.ts +312 -0
  60. package/src/filter/filter.test.ts +317 -0
  61. package/src/filter/index.ts +249 -0
  62. package/src/index.ts +16 -0
  63. package/src/loader/index.ts +364 -0
  64. package/src/loader/loader.test.ts +224 -0
  65. package/src/parser/__fixtures__/invalid-task-id-not-padded.aps.md +7 -0
  66. package/src/parser/__fixtures__/invalid-task-id.aps.md +8 -0
  67. package/src/parser/__fixtures__/minimal-task.aps.md +7 -0
  68. package/src/parser/__fixtures__/non-scope-hyphenated.aps.md +10 -0
  69. package/src/parser/__fixtures__/simple-index.aps.md +35 -0
  70. package/src/parser/__fixtures__/simple-plan.aps.md +19 -0
  71. package/src/parser/index.ts +30 -0
  72. package/src/parser/parse-document.test.ts +603 -0
  73. package/src/parser/parse-document.ts +262 -0
  74. package/src/parser/parse-index.test.ts +316 -0
  75. package/src/parser/parse-index.ts +298 -0
  76. package/src/parser/parse-task.test.ts +476 -0
  77. package/src/parser/parse-task.ts +325 -0
  78. package/src/state/__fixtures__/invalid-plan.aps.md +9 -0
  79. package/src/state/__fixtures__/test-plan.aps.md +20 -0
  80. package/src/state/index.ts +879 -0
  81. package/src/state/state.test.ts +645 -0
  82. package/src/templates/generator.test.ts +378 -0
  83. package/src/templates/generator.ts +776 -0
  84. package/src/templates/index.ts +5 -0
  85. package/src/types/index.ts +168 -0
  86. package/src/validator/__fixtures__/broken-links.aps.md +10 -0
  87. package/src/validator/__fixtures__/circular-deps-index.aps.md +26 -0
  88. package/src/validator/__fixtures__/circular-modules/module-a.aps.md +9 -0
  89. package/src/validator/__fixtures__/circular-modules/module-b.aps.md +9 -0
  90. package/src/validator/__fixtures__/circular-modules/module-c.aps.md +9 -0
  91. package/src/validator/__fixtures__/dup-modules/module-a.aps.md +9 -0
  92. package/src/validator/__fixtures__/dup-modules/module-b.aps.md +9 -0
  93. package/src/validator/__fixtures__/duplicate-ids-index.aps.md +15 -0
  94. package/src/validator/__fixtures__/invalid-task-id.aps.md +17 -0
  95. package/src/validator/__fixtures__/missing-confidence.aps.md +9 -0
  96. package/src/validator/__fixtures__/missing-h1.aps.md +5 -0
  97. package/src/validator/__fixtures__/missing-intent.aps.md +9 -0
  98. package/src/validator/__fixtures__/missing-modules-section.aps.md +7 -0
  99. package/src/validator/__fixtures__/missing-tasks-section.aps.md +7 -0
  100. package/src/validator/__fixtures__/modules/auth.aps.md +17 -0
  101. package/src/validator/__fixtures__/modules/payments.aps.md +13 -0
  102. package/src/validator/__fixtures__/scope-mismatch.aps.md +14 -0
  103. package/src/validator/__fixtures__/valid-index.aps.md +24 -0
  104. package/src/validator/__fixtures__/valid-leaf.aps.md +22 -0
  105. package/src/validator/index.ts +776 -0
  106. package/src/validator/validator.test.ts +269 -0
  107. package/templates/index-full.md +94 -0
  108. package/templates/index-minimal.md +16 -0
  109. package/templates/index-template.md +63 -0
  110. package/templates/leaf-full.md +76 -0
  111. package/templates/leaf-minimal.md +14 -0
  112. package/templates/leaf-template.md +55 -0
  113. package/templates/simple-full.md +56 -0
  114. package/templates/simple-minimal.md +14 -0
  115. package/templates/simple-template.md +30 -0
  116. package/tsconfig.json +19 -0
  117. package/tsconfig.lib.json +14 -0
  118. package/tsconfig.lib.tsbuildinfo +1 -0
  119. package/tsconfig.spec.json +9 -0
  120. package/tsconfig.tsbuildinfo +1 -0
  121. 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
+ });