@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,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index file parsing utilities
|
|
3
|
+
* Parses APS index files that organise multiple leaf specs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { unified } from 'unified';
|
|
7
|
+
import remarkParse from 'remark-parse';
|
|
8
|
+
import { visit } from 'unist-util-visit';
|
|
9
|
+
import type { Root, Heading, List, ListItem, Paragraph, Link } from 'mdast';
|
|
10
|
+
import { ParseError, type ModuleMetadata, type Priority } from '../types/index.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parsed index file result
|
|
14
|
+
*/
|
|
15
|
+
export interface ParsedIndex {
|
|
16
|
+
/** Plan title from H1 */
|
|
17
|
+
title: string;
|
|
18
|
+
|
|
19
|
+
/** Overview text (optional) */
|
|
20
|
+
overview?: string;
|
|
21
|
+
|
|
22
|
+
/** Module definitions */
|
|
23
|
+
modules: ModuleMetadata[];
|
|
24
|
+
|
|
25
|
+
/** Open questions (optional) */
|
|
26
|
+
openQuestions?: string[];
|
|
27
|
+
|
|
28
|
+
/** Decisions with dates (optional) */
|
|
29
|
+
decisions?: string[];
|
|
30
|
+
|
|
31
|
+
/** Source file path */
|
|
32
|
+
sourcePath?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse an APS index file from Markdown content
|
|
37
|
+
*
|
|
38
|
+
* @param content - Markdown content
|
|
39
|
+
* @param sourcePath - Optional source file path for error reporting
|
|
40
|
+
* @returns Parsed index with modules
|
|
41
|
+
*/
|
|
42
|
+
export async function parseIndex(content: string, sourcePath?: string): Promise<ParsedIndex> {
|
|
43
|
+
const processor = unified().use(remarkParse);
|
|
44
|
+
const ast = processor.parse(content) as Root;
|
|
45
|
+
|
|
46
|
+
const result: ParsedIndex = {
|
|
47
|
+
title: '',
|
|
48
|
+
modules: [],
|
|
49
|
+
sourcePath,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let currentSection: 'root' | 'overview' | 'modules' | 'questions' | 'decisions' = 'root';
|
|
53
|
+
let currentModuleId: string | null = null;
|
|
54
|
+
let currentModuleContent: List[] = [];
|
|
55
|
+
|
|
56
|
+
visit(ast, (node) => {
|
|
57
|
+
// H1: Plan title
|
|
58
|
+
if (node.type === 'heading' && (node as Heading).depth === 1) {
|
|
59
|
+
result.title = extractPlainText(node as Heading);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// H2: Section headers
|
|
63
|
+
if (node.type === 'heading' && (node as Heading).depth === 2) {
|
|
64
|
+
// Save previous module if any
|
|
65
|
+
if (currentModuleId && currentModuleContent.length > 0) {
|
|
66
|
+
const moduleMetadata = parseModuleMetadata(currentModuleId, currentModuleContent);
|
|
67
|
+
result.modules.push(moduleMetadata);
|
|
68
|
+
currentModuleId = null;
|
|
69
|
+
currentModuleContent = [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const sectionTitle = extractPlainText(node as Heading).toLowerCase();
|
|
73
|
+
|
|
74
|
+
if (sectionTitle === 'overview') {
|
|
75
|
+
currentSection = 'overview';
|
|
76
|
+
} else if (sectionTitle === 'modules') {
|
|
77
|
+
currentSection = 'modules';
|
|
78
|
+
} else if (sectionTitle === 'open questions') {
|
|
79
|
+
currentSection = 'questions';
|
|
80
|
+
} else if (sectionTitle === 'decisions') {
|
|
81
|
+
currentSection = 'decisions';
|
|
82
|
+
} else {
|
|
83
|
+
currentSection = 'root';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// H3: Module headings (within Modules section)
|
|
88
|
+
if (node.type === 'heading' && (node as Heading).depth === 3 && currentSection === 'modules') {
|
|
89
|
+
// Save previous module if any
|
|
90
|
+
if (currentModuleId && currentModuleContent.length > 0) {
|
|
91
|
+
const moduleMetadata = parseModuleMetadata(currentModuleId, currentModuleContent);
|
|
92
|
+
result.modules.push(moduleMetadata);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
currentModuleId = extractPlainText(node as Heading);
|
|
96
|
+
currentModuleContent = [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Collect lists for current module
|
|
100
|
+
if (node.type === 'list' && currentSection === 'modules' && currentModuleId) {
|
|
101
|
+
currentModuleContent.push(node as List);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Collect overview paragraph
|
|
105
|
+
if (node.type === 'paragraph' && currentSection === 'overview') {
|
|
106
|
+
result.overview = extractPlainText(node as Paragraph);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Collect open questions from list
|
|
110
|
+
if (node.type === 'list' && currentSection === 'questions') {
|
|
111
|
+
result.openQuestions = extractListItemsAsStrings(node as List);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Collect decisions from list
|
|
115
|
+
if (node.type === 'list' && currentSection === 'decisions') {
|
|
116
|
+
result.decisions = extractListItemsAsStrings(node as List);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Save last module if any
|
|
121
|
+
if (currentModuleId && currentModuleContent.length > 0) {
|
|
122
|
+
const moduleMetadata = parseModuleMetadata(currentModuleId, currentModuleContent);
|
|
123
|
+
result.modules.push(moduleMetadata);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Validate
|
|
127
|
+
if (!result.title) {
|
|
128
|
+
throw new ParseError('Index file must have an H1 title', sourcePath);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Parse module metadata from list items
|
|
136
|
+
* Format: - **Field:** value
|
|
137
|
+
*/
|
|
138
|
+
function parseModuleMetadata(moduleId: string, lists: List[]): ModuleMetadata {
|
|
139
|
+
const metadata: ModuleMetadata = {
|
|
140
|
+
id: moduleId,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
for (const list of lists) {
|
|
144
|
+
for (const item of list.children) {
|
|
145
|
+
if (item.type !== 'listItem') continue;
|
|
146
|
+
|
|
147
|
+
const { key, value } = extractFieldFromListItem(item as ListItem);
|
|
148
|
+
if (!key) continue;
|
|
149
|
+
|
|
150
|
+
switch (key) {
|
|
151
|
+
case 'Path':
|
|
152
|
+
metadata.path = value;
|
|
153
|
+
break;
|
|
154
|
+
case 'Scope':
|
|
155
|
+
case 'ID':
|
|
156
|
+
// Support both 'Scope:' and 'ID:' per current APS spec
|
|
157
|
+
metadata.scope = value;
|
|
158
|
+
break;
|
|
159
|
+
case 'Owner':
|
|
160
|
+
metadata.owner = value;
|
|
161
|
+
break;
|
|
162
|
+
case 'Status': {
|
|
163
|
+
// Normalise status values - legacy values mapped to canonical equivalents
|
|
164
|
+
const statusMap: Record<string, ModuleMetadata['status']> = {
|
|
165
|
+
Draft: 'Proposed',
|
|
166
|
+
Proposed: 'Proposed',
|
|
167
|
+
Ready: 'Ready',
|
|
168
|
+
'In Progress': 'In Progress',
|
|
169
|
+
Complete: 'Done',
|
|
170
|
+
Done: 'Done',
|
|
171
|
+
Blocked: 'Blocked',
|
|
172
|
+
};
|
|
173
|
+
const mapped = statusMap[value.trim()];
|
|
174
|
+
if (mapped) {
|
|
175
|
+
metadata.status = mapped;
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case 'Priority':
|
|
180
|
+
if (value === 'low' || value === 'medium' || value === 'high') {
|
|
181
|
+
metadata.priority = value as Priority;
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
case 'Tags':
|
|
185
|
+
metadata.tags = parseCommaSeparated(value);
|
|
186
|
+
break;
|
|
187
|
+
case 'Dependencies':
|
|
188
|
+
if (value.toLowerCase() === '(none)' || value === '') {
|
|
189
|
+
metadata.dependencies = [];
|
|
190
|
+
} else {
|
|
191
|
+
metadata.dependencies = parseCommaSeparated(value);
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
194
|
+
case 'Packages':
|
|
195
|
+
// Monorepo support: list of affected packages
|
|
196
|
+
if (value.toLowerCase() === '(none)' || value === '') {
|
|
197
|
+
metadata.packages = [];
|
|
198
|
+
} else {
|
|
199
|
+
metadata.packages = parseCommaSeparated(value);
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return metadata;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Extract field key and value from a list item
|
|
211
|
+
* Format: **Key:** value or **Key:** [link](url)
|
|
212
|
+
*/
|
|
213
|
+
function extractFieldFromListItem(item: ListItem): { key: string; value: string } {
|
|
214
|
+
let key = '';
|
|
215
|
+
let value = '';
|
|
216
|
+
|
|
217
|
+
for (const child of item.children) {
|
|
218
|
+
if (child.type !== 'paragraph') continue;
|
|
219
|
+
|
|
220
|
+
const para = child as Paragraph;
|
|
221
|
+
let foundKey = false;
|
|
222
|
+
|
|
223
|
+
for (const node of para.children) {
|
|
224
|
+
if (node.type === 'strong') {
|
|
225
|
+
const strongText = extractPlainTextFromNode(node);
|
|
226
|
+
const match = strongText.match(/^(\w+):$/);
|
|
227
|
+
if (match) {
|
|
228
|
+
key = match[1];
|
|
229
|
+
foundKey = true;
|
|
230
|
+
}
|
|
231
|
+
} else if (foundKey) {
|
|
232
|
+
if (node.type === 'text') {
|
|
233
|
+
value += (node as { value: string }).value;
|
|
234
|
+
} else if (node.type === 'link') {
|
|
235
|
+
// For Path field, extract the URL from the link
|
|
236
|
+
const link = node as Link;
|
|
237
|
+
value += link.url;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { key, value: value.trim() };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Extract plain text from a heading or paragraph
|
|
248
|
+
*/
|
|
249
|
+
function extractPlainText(node: Heading | Paragraph): string {
|
|
250
|
+
let text = '';
|
|
251
|
+
visit(node, 'text', (textNode) => {
|
|
252
|
+
text += (textNode as { value: string }).value;
|
|
253
|
+
});
|
|
254
|
+
return text;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Extract plain text from any node
|
|
259
|
+
*/
|
|
260
|
+
function extractPlainTextFromNode(node: unknown): string {
|
|
261
|
+
let text = '';
|
|
262
|
+
visit(node as Root, 'text', (textNode) => {
|
|
263
|
+
text += (textNode as { value: string }).value;
|
|
264
|
+
});
|
|
265
|
+
return text;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Extract list items as strings
|
|
270
|
+
*/
|
|
271
|
+
function extractListItemsAsStrings(list: List): string[] {
|
|
272
|
+
const items: string[] = [];
|
|
273
|
+
|
|
274
|
+
for (const item of list.children) {
|
|
275
|
+
if (item.type !== 'listItem') continue;
|
|
276
|
+
|
|
277
|
+
let text = '';
|
|
278
|
+
visit(item, 'text', (textNode) => {
|
|
279
|
+
text += (textNode as { value: string }).value;
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
if (text.trim()) {
|
|
283
|
+
items.push(text.trim());
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return items;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Parse comma-separated list
|
|
292
|
+
*/
|
|
293
|
+
function parseCommaSeparated(value: string): string[] {
|
|
294
|
+
return value
|
|
295
|
+
.split(',')
|
|
296
|
+
.map((item) => item.trim())
|
|
297
|
+
.filter((item) => item.length > 0);
|
|
298
|
+
}
|