@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,224 @@
1
+ /**
2
+ * Tests for plan loader module
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { dirname, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import {
9
+ loadPlan,
10
+ resolvePath,
11
+ getModuleTasks,
12
+ getDependentModules,
13
+ getModulesInOrder,
14
+ detectCycles,
15
+ } from './index.js';
16
+ import { ParseError } from '../types/index.js';
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const EXAMPLES_DIR = join(__dirname, '../../examples');
20
+
21
+ describe('loadPlan', () => {
22
+ describe('single-file plans', () => {
23
+ it('should load a single-file plan (leaf spec)', async () => {
24
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'feature-auth.aps.md'));
25
+
26
+ expect(plan.title).toBe('Feature: User Authentication');
27
+ expect(plan.isMultiModule).toBe(false);
28
+ expect(plan.modules.size).toBe(1);
29
+ expect(plan.allTasks.length).toBe(8);
30
+ });
31
+
32
+ it('should create a single module for leaf specs', async () => {
33
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'feature-auth.aps.md'));
34
+
35
+ const module = plan.modules.get('AUTH');
36
+ expect(module).toBeDefined();
37
+ expect(module!.id).toBe('AUTH');
38
+ expect(module!.tasks.length).toBe(8);
39
+ expect(module!.dependsOn).toEqual([]);
40
+ });
41
+ });
42
+
43
+ describe('multi-module plans', () => {
44
+ it('should load a multi-module plan (index file)', async () => {
45
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
46
+
47
+ expect(plan.title).toBe('E-commerce Platform MVP');
48
+ expect(plan.isMultiModule).toBe(true);
49
+ expect(plan.modules.size).toBe(4);
50
+ });
51
+
52
+ it('should load all modules recursively', async () => {
53
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
54
+
55
+ // Check all modules are loaded
56
+ expect(plan.modules.has('auth')).toBe(true);
57
+ expect(plan.modules.has('products')).toBe(true);
58
+ expect(plan.modules.has('cart')).toBe(true);
59
+ expect(plan.modules.has('payments')).toBe(true);
60
+
61
+ // Check tasks are loaded
62
+ expect(plan.allTasks.length).toBeGreaterThan(0);
63
+
64
+ // Check auth module has tasks
65
+ const authModule = plan.modules.get('auth');
66
+ expect(authModule!.tasks.length).toBeGreaterThan(0);
67
+ });
68
+
69
+ it('should build dependency graph correctly', async () => {
70
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
71
+
72
+ expect(plan.dependencyGraph.get('auth')).toEqual([]);
73
+ expect(plan.dependencyGraph.get('products')).toEqual(['auth']);
74
+ expect(plan.dependencyGraph.get('cart')).toEqual(['auth', 'products']);
75
+ expect(plan.dependencyGraph.get('payments')).toEqual(['auth', 'cart']);
76
+ });
77
+
78
+ it('should skip loading module content when recursive=false', async () => {
79
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'), {
80
+ recursive: false,
81
+ });
82
+
83
+ expect(plan.modules.size).toBe(4);
84
+ expect(plan.allTasks.length).toBe(0);
85
+
86
+ const authModule = plan.modules.get('auth');
87
+ expect(authModule!.tasks.length).toBe(0);
88
+ });
89
+ });
90
+
91
+ describe('error handling', () => {
92
+ it('should throw ParseError for non-existent file', async () => {
93
+ await expect(loadPlan('/non/existent/path.aps.md')).rejects.toThrow(ParseError);
94
+ await expect(loadPlan('/non/existent/path.aps.md')).rejects.toThrow(/File not found/);
95
+ });
96
+ });
97
+
98
+ describe('index file detection', () => {
99
+ it('should detect index with lowercase "modules" heading', async () => {
100
+ // Create a temporary test - we'll use inline content via the loader's behavior
101
+ // This tests that case-insensitive detection works
102
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
103
+ expect(plan.isMultiModule).toBe(true);
104
+ });
105
+ });
106
+ });
107
+
108
+ describe('resolvePath', () => {
109
+ // On Windows, path.resolve('/project/plan', ...) prepends the drive letter (e.g. 'D:')
110
+ const toFwd = (p: string): string => p.replace(/\\/g, '/').replace(/^[A-Z]:/, '');
111
+
112
+ it('should resolve relative paths', () => {
113
+ expect(toFwd(resolvePath('./modules/auth.aps.md', '/project/plan'))).toBe(
114
+ '/project/plan/modules/auth.aps.md'
115
+ );
116
+ });
117
+
118
+ it('should handle paths without ./', () => {
119
+ expect(toFwd(resolvePath('modules/auth.aps.md', '/project/plan'))).toBe(
120
+ '/project/plan/modules/auth.aps.md'
121
+ );
122
+ });
123
+
124
+ it('should reject absolute paths', () => {
125
+ expect(() => resolvePath('/absolute/path.md', '/project')).toThrow(
126
+ 'Absolute module paths are not allowed'
127
+ );
128
+ });
129
+ });
130
+
131
+ describe('getModuleTasks', () => {
132
+ it('should return tasks for a specific module', async () => {
133
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
134
+
135
+ const authTasks = getModuleTasks(plan, 'auth');
136
+ expect(authTasks.length).toBeGreaterThan(0);
137
+ expect(authTasks.every((t) => t.id.startsWith('AUTH-'))).toBe(true);
138
+ });
139
+
140
+ it('should return empty array for unknown module', async () => {
141
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
142
+
143
+ const tasks = getModuleTasks(plan, 'nonexistent');
144
+ expect(tasks).toEqual([]);
145
+ });
146
+ });
147
+
148
+ describe('getDependentModules', () => {
149
+ it('should find modules that depend on a given module', async () => {
150
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
151
+
152
+ const authDependents = getDependentModules(plan, 'auth');
153
+ expect(authDependents).toContain('products');
154
+ expect(authDependents).toContain('cart');
155
+ expect(authDependents).toContain('payments');
156
+ });
157
+
158
+ it('should return empty array for module with no dependents', async () => {
159
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
160
+
161
+ const paymentsDependents = getDependentModules(plan, 'payments');
162
+ expect(paymentsDependents).toEqual([]);
163
+ });
164
+ });
165
+
166
+ describe('getModulesInOrder', () => {
167
+ it('should return modules in topological order', async () => {
168
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
169
+
170
+ const order = getModulesInOrder(plan);
171
+
172
+ // Auth should come before everything
173
+ const authIndex = order.indexOf('auth');
174
+ const productsIndex = order.indexOf('products');
175
+ const cartIndex = order.indexOf('cart');
176
+ const paymentsIndex = order.indexOf('payments');
177
+
178
+ expect(authIndex).toBeLessThan(productsIndex);
179
+ expect(authIndex).toBeLessThan(cartIndex);
180
+ expect(authIndex).toBeLessThan(paymentsIndex);
181
+
182
+ // Products should come before cart
183
+ expect(productsIndex).toBeLessThan(cartIndex);
184
+
185
+ // Cart should come before payments
186
+ expect(cartIndex).toBeLessThan(paymentsIndex);
187
+ });
188
+ });
189
+
190
+ describe('detectCycles', () => {
191
+ it('should return empty array for acyclic graph', async () => {
192
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
193
+
194
+ const cycles = detectCycles(plan);
195
+ expect(cycles).toEqual([]);
196
+ });
197
+
198
+ it('should detect cycles in dependency graph', async () => {
199
+ // Create a plan with cycles manually
200
+ const plan = {
201
+ title: 'Cyclic Plan',
202
+ rootPath: '/test',
203
+ isMultiModule: true,
204
+ modules: new Map([
205
+ ['a', { id: 'a', metadata: {}, tasks: [], resolvedPath: '/a', dependsOn: ['b'] }],
206
+ ['b', { id: 'b', metadata: {}, tasks: [], resolvedPath: '/b', dependsOn: ['c'] }],
207
+ ['c', { id: 'c', metadata: {}, tasks: [], resolvedPath: '/c', dependsOn: ['a'] }],
208
+ ]),
209
+ allTasks: [],
210
+ dependencyGraph: new Map([
211
+ ['a', ['b']],
212
+ ['b', ['c']],
213
+ ['c', ['a']],
214
+ ]),
215
+ };
216
+
217
+ const cycles = detectCycles(plan);
218
+ expect(cycles.length).toBeGreaterThan(0);
219
+ // The cycle should include a, b, c
220
+ expect(cycles[0]).toContain('a');
221
+ expect(cycles[0]).toContain('b');
222
+ expect(cycles[0]).toContain('c');
223
+ });
224
+ });
@@ -0,0 +1,7 @@
1
+ # Invalid Task - Not Zero Padded
2
+
3
+ ## Tasks
4
+
5
+ ### AUTH-1: Task with invalid ID
6
+
7
+ **Intent:** This should fail because AUTH-1 is not zero-padded to 3 digits
@@ -0,0 +1,8 @@
1
+ # Invalid Task
2
+
3
+ ## Tasks
4
+
5
+ ### This heading has no ID
6
+
7
+ **Intent:** This should fail to parse because the heading doesn't have a proper
8
+ ID format
@@ -0,0 +1,7 @@
1
+ # Minimal Task
2
+
3
+ ## Tasks
4
+
5
+ ### MIN-001: Minimal task with only required fields
6
+
7
+ **Intent:** This task only has the required Intent field
@@ -0,0 +1,10 @@
1
+ # Test Non-scope Field
2
+
3
+ **Scope:** TEST **Owner:** @test
4
+
5
+ ## Tasks
6
+
7
+ ### TEST-001: Test hyphenated Non-scope field
8
+
9
+ **Intent:** Test that Non-scope field with hyphen is parsed correctly
10
+ **Non-scope:** database, external APIs, authentication
@@ -0,0 +1,35 @@
1
+ # Simple Plan
2
+
3
+ ## Overview
4
+
5
+ A simple plan with two modules for testing.
6
+
7
+ ## Modules
8
+
9
+ ### auth
10
+
11
+ - **Path:** [./modules/auth.aps.md](./modules/auth.aps.md)
12
+ - **Scope:** AUTH
13
+ - **Owner:** @alice
14
+ - **Priority:** high
15
+ - **Tags:** security, core
16
+ - **Dependencies:** (none)
17
+
18
+ ### api
19
+
20
+ - **Path:** [./modules/api.aps.md](./modules/api.aps.md)
21
+ - **Scope:** API
22
+ - **Owner:** @bob
23
+ - **Priority:** medium
24
+ - **Tags:** backend
25
+ - **Dependencies:** auth
26
+
27
+ ## Open Questions
28
+
29
+ - Should we add rate limiting?
30
+ - What authentication method to use?
31
+
32
+ ## Decisions
33
+
34
+ - Using JWT tokens (decided 2025-01-15)
35
+ - PostgreSQL database (decided 2025-01-10)
@@ -0,0 +1,19 @@
1
+ # Simple Feature Plan
2
+
3
+ **Scope:** TEST **Owner:** @test **Priority:** high
4
+
5
+ ## Tasks
6
+
7
+ ### TEST-001: First task
8
+
9
+ **Intent:** This is a simple task to test parsing **Expected Outcome:** Task
10
+ should be parsed correctly **Confidence:** high **Scopes:** TEST **Tags:**
11
+ example, simple **Dependencies:** TEST-000 **Inputs:**
12
+
13
+ - Input one
14
+ - Input two
15
+
16
+ ### TEST-002: Second task
17
+
18
+ **Intent:** Another task without all fields **Confidence:** medium **Scopes:**
19
+ TEST, API **Tags:** minimal
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Parser module - Markdown parsing for APS documents
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * import { parseDocument, parseIndex } from '@eddacraft/anvil-aps/parser';
7
+ *
8
+ * // Parse a leaf spec (tasks)
9
+ * const leafContent = await fs.readFile('feature.aps.md', 'utf-8');
10
+ * const doc = await parseDocument(leafContent, 'feature.aps.md');
11
+ * console.log(doc.tasks.length); // 8
12
+ *
13
+ * // Parse an index file (modules)
14
+ * const indexContent = await fs.readFile('plan/APS.md', 'utf-8');
15
+ * const index = await parseIndex(indexContent, 'plan/APS.md');
16
+ * console.log(index.modules.length); // 4
17
+ * ```
18
+ */
19
+
20
+ export { parseDocument } from './parse-document.js';
21
+ export { parseIndex, type ParsedIndex } from './parse-index.js';
22
+ export { parseTask, parseTaskHeading, parseTaskFields } from './parse-task.js';
23
+ export type {
24
+ Task,
25
+ ParsedDocument,
26
+ ModuleMetadata,
27
+ Confidence,
28
+ TaskStatus,
29
+ } from '../types/index.js';
30
+ export { ParseError } from '../types/index.js';