@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,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document parsing utilities
|
|
3
|
+
* Converts APS Markdown documents to structured data
|
|
4
|
+
*/
|
|
5
|
+
import { unified } from 'unified';
|
|
6
|
+
import remarkParse from 'remark-parse';
|
|
7
|
+
import { visit } from 'unist-util-visit';
|
|
8
|
+
import { parseTask } from './parse-task.js';
|
|
9
|
+
import { ParseError } from '../types/index.js';
|
|
10
|
+
/**
|
|
11
|
+
* Parse an APS leaf spec document from Markdown content
|
|
12
|
+
*
|
|
13
|
+
* This function parses leaf specs (documents with tasks).
|
|
14
|
+
* For index files (documents with modules), use `parseIndex` instead.
|
|
15
|
+
*
|
|
16
|
+
* @param content - Markdown content of a leaf spec
|
|
17
|
+
* @param sourcePath - Optional source file path for error reporting
|
|
18
|
+
* @returns Parsed document with tasks
|
|
19
|
+
*/
|
|
20
|
+
export async function parseDocument(content, sourcePath) {
|
|
21
|
+
// Parse Markdown to AST
|
|
22
|
+
const processor = unified().use(remarkParse);
|
|
23
|
+
const ast = processor.parse(content);
|
|
24
|
+
// Extract document structure
|
|
25
|
+
const structure = extractStructure(ast, sourcePath);
|
|
26
|
+
// Validate structure
|
|
27
|
+
if (!structure.title) {
|
|
28
|
+
throw new ParseError('Document must have an H1 title', sourcePath);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
title: structure.title,
|
|
32
|
+
metadata: structure.metadata,
|
|
33
|
+
tasks: structure.tasks,
|
|
34
|
+
sourcePath,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function extractStructure(ast, sourcePath) {
|
|
38
|
+
const structure = {
|
|
39
|
+
title: null,
|
|
40
|
+
tasks: [],
|
|
41
|
+
};
|
|
42
|
+
let currentSection = 'root';
|
|
43
|
+
let currentTaskHeading = null;
|
|
44
|
+
let currentTaskContent = [];
|
|
45
|
+
visit(ast, (node, index, parent) => {
|
|
46
|
+
if (node.type === 'heading') {
|
|
47
|
+
const heading = node;
|
|
48
|
+
// H1: Document title
|
|
49
|
+
if (heading.depth === 1) {
|
|
50
|
+
structure.title = extractPlainText(heading);
|
|
51
|
+
// Check next sibling for metadata line
|
|
52
|
+
if (parent && index !== null && index !== undefined) {
|
|
53
|
+
const nextSibling = parent.children[index + 1];
|
|
54
|
+
if (nextSibling && nextSibling.type === 'paragraph') {
|
|
55
|
+
structure.metadata = parseMetadataLine(nextSibling);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// H2: Section headers
|
|
60
|
+
if (heading.depth === 2) {
|
|
61
|
+
// Save previous task if any
|
|
62
|
+
if (currentTaskHeading) {
|
|
63
|
+
saveTask(currentTaskHeading, currentTaskContent, sourcePath, structure);
|
|
64
|
+
currentTaskHeading = null;
|
|
65
|
+
currentTaskContent = [];
|
|
66
|
+
}
|
|
67
|
+
const sectionTitle = extractPlainText(heading).toLowerCase();
|
|
68
|
+
if (sectionTitle === 'tasks') {
|
|
69
|
+
currentSection = 'tasks';
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
currentSection = 'root';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// H3: Task headings
|
|
76
|
+
if (heading.depth === 3 && currentSection === 'tasks') {
|
|
77
|
+
// Save previous task if any
|
|
78
|
+
if (currentTaskHeading) {
|
|
79
|
+
saveTask(currentTaskHeading, currentTaskContent, sourcePath, structure);
|
|
80
|
+
currentTaskHeading = null;
|
|
81
|
+
currentTaskContent = [];
|
|
82
|
+
}
|
|
83
|
+
// Start new task
|
|
84
|
+
currentTaskHeading = {
|
|
85
|
+
heading,
|
|
86
|
+
lineNumber: node.position?.start.line ?? 0,
|
|
87
|
+
};
|
|
88
|
+
currentTaskContent = [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Collect content for current task
|
|
92
|
+
if (currentTaskHeading && (node.type === 'paragraph' || node.type === 'list')) {
|
|
93
|
+
currentTaskContent.push(node);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
// Save last task if any
|
|
97
|
+
if (currentTaskHeading) {
|
|
98
|
+
saveTask(currentTaskHeading, currentTaskContent, sourcePath, structure);
|
|
99
|
+
}
|
|
100
|
+
return structure;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Helper to save a task from heading and content
|
|
104
|
+
*/
|
|
105
|
+
function saveTask(taskHeading, taskContent, sourcePath, structure) {
|
|
106
|
+
try {
|
|
107
|
+
const task = parseTask(taskHeading.heading, taskContent, sourcePath, taskHeading.lineNumber);
|
|
108
|
+
structure.tasks.push(task);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (error instanceof ParseError) {
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
throw new ParseError(`Failed to parse task: ${error instanceof Error ? error.message : String(error)}`, sourcePath);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Extract plain text from heading or paragraph node
|
|
119
|
+
*/
|
|
120
|
+
function extractPlainText(node) {
|
|
121
|
+
let text = '';
|
|
122
|
+
visit(node, 'text', (textNode) => {
|
|
123
|
+
text += textNode.value;
|
|
124
|
+
});
|
|
125
|
+
return text;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Parse module metadata line (immediately after H1)
|
|
129
|
+
* Format: **Scope:** AUTH **Owner:** @alice **Priority:** high
|
|
130
|
+
*/
|
|
131
|
+
function parseMetadataLine(para) {
|
|
132
|
+
const metadata = {};
|
|
133
|
+
let currentKey = '';
|
|
134
|
+
let currentValue = '';
|
|
135
|
+
for (const child of para.children) {
|
|
136
|
+
if (child.type === 'strong') {
|
|
137
|
+
// Save previous field (even if value is empty, so handlers like Packages can default)
|
|
138
|
+
if (currentKey) {
|
|
139
|
+
assignMetadataField(metadata, currentKey, currentValue.trim());
|
|
140
|
+
}
|
|
141
|
+
// Extract new field key
|
|
142
|
+
let strongText = '';
|
|
143
|
+
visit(child, 'text', (textNode) => {
|
|
144
|
+
strongText += textNode.value;
|
|
145
|
+
});
|
|
146
|
+
const match = strongText.match(/^(\w+):$/);
|
|
147
|
+
if (match) {
|
|
148
|
+
currentKey = match[1];
|
|
149
|
+
currentValue = '';
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (child.type === 'text' && currentKey) {
|
|
153
|
+
currentValue += child.value;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Save last field (even if value is empty)
|
|
157
|
+
if (currentKey) {
|
|
158
|
+
assignMetadataField(metadata, currentKey, currentValue.trim());
|
|
159
|
+
}
|
|
160
|
+
return metadata;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Assign metadata field value
|
|
164
|
+
*/
|
|
165
|
+
function assignMetadataField(metadata, key, value) {
|
|
166
|
+
switch (key) {
|
|
167
|
+
case 'Scope':
|
|
168
|
+
case 'ID':
|
|
169
|
+
// Support both 'Scope:' and 'ID:' per current APS spec
|
|
170
|
+
metadata.scope = value;
|
|
171
|
+
break;
|
|
172
|
+
case 'Owner':
|
|
173
|
+
metadata.owner = value;
|
|
174
|
+
break;
|
|
175
|
+
case 'Status': {
|
|
176
|
+
// Normalise status values to match ModuleStatusSchema
|
|
177
|
+
// Legacy values are mapped to canonical equivalents: Draft→Proposed, Complete→Done
|
|
178
|
+
const statusMap = {
|
|
179
|
+
Draft: 'Proposed',
|
|
180
|
+
Proposed: 'Proposed',
|
|
181
|
+
Ready: 'Ready',
|
|
182
|
+
'In Progress': 'In Progress',
|
|
183
|
+
Complete: 'Done',
|
|
184
|
+
Done: 'Done',
|
|
185
|
+
Blocked: 'Blocked',
|
|
186
|
+
};
|
|
187
|
+
const mapped = statusMap[value.trim()];
|
|
188
|
+
if (mapped) {
|
|
189
|
+
metadata.status = mapped;
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case 'Priority':
|
|
194
|
+
if (value === 'low' || value === 'medium' || value === 'high') {
|
|
195
|
+
metadata.priority = value;
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
case 'Tags':
|
|
199
|
+
metadata.tags = value.split(',').map((t) => t.trim());
|
|
200
|
+
break;
|
|
201
|
+
case 'Dependencies':
|
|
202
|
+
metadata.dependencies = value.split(',').map((d) => d.trim());
|
|
203
|
+
break;
|
|
204
|
+
case 'Packages': {
|
|
205
|
+
// Monorepo support: list of affected packages
|
|
206
|
+
const trimmed = value.trim();
|
|
207
|
+
if (!trimmed || trimmed.toLowerCase() === '(none)') {
|
|
208
|
+
metadata.packages = [];
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
metadata.packages = trimmed
|
|
212
|
+
.split(',')
|
|
213
|
+
.map((p) => p.trim())
|
|
214
|
+
.filter((p) => p.length > 0);
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index file parsing utilities
|
|
3
|
+
* Parses APS index files that organise multiple leaf specs
|
|
4
|
+
*/
|
|
5
|
+
import { type ModuleMetadata } from '../types/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Parsed index file result
|
|
8
|
+
*/
|
|
9
|
+
export interface ParsedIndex {
|
|
10
|
+
/** Plan title from H1 */
|
|
11
|
+
title: string;
|
|
12
|
+
/** Overview text (optional) */
|
|
13
|
+
overview?: string;
|
|
14
|
+
/** Module definitions */
|
|
15
|
+
modules: ModuleMetadata[];
|
|
16
|
+
/** Open questions (optional) */
|
|
17
|
+
openQuestions?: string[];
|
|
18
|
+
/** Decisions with dates (optional) */
|
|
19
|
+
decisions?: string[];
|
|
20
|
+
/** Source file path */
|
|
21
|
+
sourcePath?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Parse an APS index file from Markdown content
|
|
25
|
+
*
|
|
26
|
+
* @param content - Markdown content
|
|
27
|
+
* @param sourcePath - Optional source file path for error reporting
|
|
28
|
+
* @returns Parsed index with modules
|
|
29
|
+
*/
|
|
30
|
+
export declare function parseIndex(content: string, sourcePath?: string): Promise<ParsedIndex>;
|
|
31
|
+
//# sourceMappingURL=parse-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-index.d.ts","sourceRoot":"","sources":["../../src/parser/parse-index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,EAAc,KAAK,cAAc,EAAiB,MAAM,mBAAmB,CAAC;AAEnF;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAC;IAEd,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,yBAAyB;IACzB,OAAO,EAAE,cAAc,EAAE,CAAC;IAE1B,gCAAgC;IAChC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IAEzB,sCAAsC;IACtC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IAErB,uBAAuB;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA0F3F"}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index file parsing utilities
|
|
3
|
+
* Parses APS index files that organise multiple leaf specs
|
|
4
|
+
*/
|
|
5
|
+
import { unified } from 'unified';
|
|
6
|
+
import remarkParse from 'remark-parse';
|
|
7
|
+
import { visit } from 'unist-util-visit';
|
|
8
|
+
import { ParseError } from '../types/index.js';
|
|
9
|
+
/**
|
|
10
|
+
* Parse an APS index file from Markdown content
|
|
11
|
+
*
|
|
12
|
+
* @param content - Markdown content
|
|
13
|
+
* @param sourcePath - Optional source file path for error reporting
|
|
14
|
+
* @returns Parsed index with modules
|
|
15
|
+
*/
|
|
16
|
+
export async function parseIndex(content, sourcePath) {
|
|
17
|
+
const processor = unified().use(remarkParse);
|
|
18
|
+
const ast = processor.parse(content);
|
|
19
|
+
const result = {
|
|
20
|
+
title: '',
|
|
21
|
+
modules: [],
|
|
22
|
+
sourcePath,
|
|
23
|
+
};
|
|
24
|
+
let currentSection = 'root';
|
|
25
|
+
let currentModuleId = null;
|
|
26
|
+
let currentModuleContent = [];
|
|
27
|
+
visit(ast, (node) => {
|
|
28
|
+
// H1: Plan title
|
|
29
|
+
if (node.type === 'heading' && node.depth === 1) {
|
|
30
|
+
result.title = extractPlainText(node);
|
|
31
|
+
}
|
|
32
|
+
// H2: Section headers
|
|
33
|
+
if (node.type === 'heading' && node.depth === 2) {
|
|
34
|
+
// Save previous module if any
|
|
35
|
+
if (currentModuleId && currentModuleContent.length > 0) {
|
|
36
|
+
const moduleMetadata = parseModuleMetadata(currentModuleId, currentModuleContent);
|
|
37
|
+
result.modules.push(moduleMetadata);
|
|
38
|
+
currentModuleId = null;
|
|
39
|
+
currentModuleContent = [];
|
|
40
|
+
}
|
|
41
|
+
const sectionTitle = extractPlainText(node).toLowerCase();
|
|
42
|
+
if (sectionTitle === 'overview') {
|
|
43
|
+
currentSection = 'overview';
|
|
44
|
+
}
|
|
45
|
+
else if (sectionTitle === 'modules') {
|
|
46
|
+
currentSection = 'modules';
|
|
47
|
+
}
|
|
48
|
+
else if (sectionTitle === 'open questions') {
|
|
49
|
+
currentSection = 'questions';
|
|
50
|
+
}
|
|
51
|
+
else if (sectionTitle === 'decisions') {
|
|
52
|
+
currentSection = 'decisions';
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
currentSection = 'root';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// H3: Module headings (within Modules section)
|
|
59
|
+
if (node.type === 'heading' && node.depth === 3 && currentSection === 'modules') {
|
|
60
|
+
// Save previous module if any
|
|
61
|
+
if (currentModuleId && currentModuleContent.length > 0) {
|
|
62
|
+
const moduleMetadata = parseModuleMetadata(currentModuleId, currentModuleContent);
|
|
63
|
+
result.modules.push(moduleMetadata);
|
|
64
|
+
}
|
|
65
|
+
currentModuleId = extractPlainText(node);
|
|
66
|
+
currentModuleContent = [];
|
|
67
|
+
}
|
|
68
|
+
// Collect lists for current module
|
|
69
|
+
if (node.type === 'list' && currentSection === 'modules' && currentModuleId) {
|
|
70
|
+
currentModuleContent.push(node);
|
|
71
|
+
}
|
|
72
|
+
// Collect overview paragraph
|
|
73
|
+
if (node.type === 'paragraph' && currentSection === 'overview') {
|
|
74
|
+
result.overview = extractPlainText(node);
|
|
75
|
+
}
|
|
76
|
+
// Collect open questions from list
|
|
77
|
+
if (node.type === 'list' && currentSection === 'questions') {
|
|
78
|
+
result.openQuestions = extractListItemsAsStrings(node);
|
|
79
|
+
}
|
|
80
|
+
// Collect decisions from list
|
|
81
|
+
if (node.type === 'list' && currentSection === 'decisions') {
|
|
82
|
+
result.decisions = extractListItemsAsStrings(node);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
// Save last module if any
|
|
86
|
+
if (currentModuleId && currentModuleContent.length > 0) {
|
|
87
|
+
const moduleMetadata = parseModuleMetadata(currentModuleId, currentModuleContent);
|
|
88
|
+
result.modules.push(moduleMetadata);
|
|
89
|
+
}
|
|
90
|
+
// Validate
|
|
91
|
+
if (!result.title) {
|
|
92
|
+
throw new ParseError('Index file must have an H1 title', sourcePath);
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Parse module metadata from list items
|
|
98
|
+
* Format: - **Field:** value
|
|
99
|
+
*/
|
|
100
|
+
function parseModuleMetadata(moduleId, lists) {
|
|
101
|
+
const metadata = {
|
|
102
|
+
id: moduleId,
|
|
103
|
+
};
|
|
104
|
+
for (const list of lists) {
|
|
105
|
+
for (const item of list.children) {
|
|
106
|
+
if (item.type !== 'listItem')
|
|
107
|
+
continue;
|
|
108
|
+
const { key, value } = extractFieldFromListItem(item);
|
|
109
|
+
if (!key)
|
|
110
|
+
continue;
|
|
111
|
+
switch (key) {
|
|
112
|
+
case 'Path':
|
|
113
|
+
metadata.path = value;
|
|
114
|
+
break;
|
|
115
|
+
case 'Scope':
|
|
116
|
+
case 'ID':
|
|
117
|
+
// Support both 'Scope:' and 'ID:' per current APS spec
|
|
118
|
+
metadata.scope = value;
|
|
119
|
+
break;
|
|
120
|
+
case 'Owner':
|
|
121
|
+
metadata.owner = value;
|
|
122
|
+
break;
|
|
123
|
+
case 'Status': {
|
|
124
|
+
// Normalise status values - legacy values mapped to canonical equivalents
|
|
125
|
+
const statusMap = {
|
|
126
|
+
Draft: 'Proposed',
|
|
127
|
+
Proposed: 'Proposed',
|
|
128
|
+
Ready: 'Ready',
|
|
129
|
+
'In Progress': 'In Progress',
|
|
130
|
+
Complete: 'Done',
|
|
131
|
+
Done: 'Done',
|
|
132
|
+
Blocked: 'Blocked',
|
|
133
|
+
};
|
|
134
|
+
const mapped = statusMap[value.trim()];
|
|
135
|
+
if (mapped) {
|
|
136
|
+
metadata.status = mapped;
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case 'Priority':
|
|
141
|
+
if (value === 'low' || value === 'medium' || value === 'high') {
|
|
142
|
+
metadata.priority = value;
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
case 'Tags':
|
|
146
|
+
metadata.tags = parseCommaSeparated(value);
|
|
147
|
+
break;
|
|
148
|
+
case 'Dependencies':
|
|
149
|
+
if (value.toLowerCase() === '(none)' || value === '') {
|
|
150
|
+
metadata.dependencies = [];
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
metadata.dependencies = parseCommaSeparated(value);
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
case 'Packages':
|
|
157
|
+
// Monorepo support: list of affected packages
|
|
158
|
+
if (value.toLowerCase() === '(none)' || value === '') {
|
|
159
|
+
metadata.packages = [];
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
metadata.packages = parseCommaSeparated(value);
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return metadata;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Extract field key and value from a list item
|
|
172
|
+
* Format: **Key:** value or **Key:** [link](url)
|
|
173
|
+
*/
|
|
174
|
+
function extractFieldFromListItem(item) {
|
|
175
|
+
let key = '';
|
|
176
|
+
let value = '';
|
|
177
|
+
for (const child of item.children) {
|
|
178
|
+
if (child.type !== 'paragraph')
|
|
179
|
+
continue;
|
|
180
|
+
const para = child;
|
|
181
|
+
let foundKey = false;
|
|
182
|
+
for (const node of para.children) {
|
|
183
|
+
if (node.type === 'strong') {
|
|
184
|
+
const strongText = extractPlainTextFromNode(node);
|
|
185
|
+
const match = strongText.match(/^(\w+):$/);
|
|
186
|
+
if (match) {
|
|
187
|
+
key = match[1];
|
|
188
|
+
foundKey = true;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else if (foundKey) {
|
|
192
|
+
if (node.type === 'text') {
|
|
193
|
+
value += node.value;
|
|
194
|
+
}
|
|
195
|
+
else if (node.type === 'link') {
|
|
196
|
+
// For Path field, extract the URL from the link
|
|
197
|
+
const link = node;
|
|
198
|
+
value += link.url;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { key, value: value.trim() };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Extract plain text from a heading or paragraph
|
|
207
|
+
*/
|
|
208
|
+
function extractPlainText(node) {
|
|
209
|
+
let text = '';
|
|
210
|
+
visit(node, 'text', (textNode) => {
|
|
211
|
+
text += textNode.value;
|
|
212
|
+
});
|
|
213
|
+
return text;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Extract plain text from any node
|
|
217
|
+
*/
|
|
218
|
+
function extractPlainTextFromNode(node) {
|
|
219
|
+
let text = '';
|
|
220
|
+
visit(node, 'text', (textNode) => {
|
|
221
|
+
text += textNode.value;
|
|
222
|
+
});
|
|
223
|
+
return text;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Extract list items as strings
|
|
227
|
+
*/
|
|
228
|
+
function extractListItemsAsStrings(list) {
|
|
229
|
+
const items = [];
|
|
230
|
+
for (const item of list.children) {
|
|
231
|
+
if (item.type !== 'listItem')
|
|
232
|
+
continue;
|
|
233
|
+
let text = '';
|
|
234
|
+
visit(item, 'text', (textNode) => {
|
|
235
|
+
text += textNode.value;
|
|
236
|
+
});
|
|
237
|
+
if (text.trim()) {
|
|
238
|
+
items.push(text.trim());
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return items;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Parse comma-separated list
|
|
245
|
+
*/
|
|
246
|
+
function parseCommaSeparated(value) {
|
|
247
|
+
return value
|
|
248
|
+
.split(',')
|
|
249
|
+
.map((item) => item.trim())
|
|
250
|
+
.filter((item) => item.length > 0);
|
|
251
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task parsing utilities
|
|
3
|
+
* Extracts task information from Markdown AST nodes
|
|
4
|
+
*/
|
|
5
|
+
import type { Heading, Paragraph, List } from 'mdast';
|
|
6
|
+
import { type Task } from '../types/index.js';
|
|
7
|
+
/**
|
|
8
|
+
* Extract task ID and title from H3 heading text
|
|
9
|
+
* Format: "SCOPE-NUMBER: Task title"
|
|
10
|
+
* - Scope: 1-10 uppercase alphanumeric characters
|
|
11
|
+
* - Number: 3-digit zero-padded (001-999)
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseTaskHeading(heading: Heading): {
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Parse task fields from paragraph and list nodes
|
|
19
|
+
* Fields are in format: **FieldName:** value
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseTaskFields(paragraphs: Paragraph[], lists: List[]): Partial<Task>;
|
|
22
|
+
/**
|
|
23
|
+
* Parse a complete task from AST nodes
|
|
24
|
+
* @param heading - H3 heading node with task ID and title
|
|
25
|
+
* @param content - Array of paragraph and list nodes containing task fields
|
|
26
|
+
* @param sourcePath - Optional source file path
|
|
27
|
+
* @param lineNumber - Optional line number where task starts
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseTask(heading: Heading, content: Array<Paragraph | List>, sourcePath?: string, lineNumber?: number): Task;
|
|
30
|
+
//# sourceMappingURL=parse-task.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-task.d.ts","sourceRoot":"","sources":["../../src/parser/parse-task.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAU,IAAI,EAAmB,MAAM,OAAO,CAAC;AAC/E,OAAO,EAAc,KAAK,IAAI,EAAoC,MAAM,mBAAmB,CAAC;AAE5F;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CA2BhF;AAiBD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsCrF;AA+KD;;;;;;GAMG;AACH,wBAAgB,SAAS,CACvB,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,EAChC,UAAU,CAAC,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,MAAM,GAClB,IAAI,CAqCN"}
|