@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,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,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';
|