@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,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"}