@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,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter module - Task and module filtering for APS plans
|
|
3
|
+
*
|
|
4
|
+
* Provides filtering capabilities for:
|
|
5
|
+
* - Scope-based filtering (by module scope or task scopes)
|
|
6
|
+
* - Module-based filtering (by module ID)
|
|
7
|
+
* - Task-based filtering (by task ID)
|
|
8
|
+
* - Metadata filtering (owner, tags, priority, confidence)
|
|
9
|
+
*
|
|
10
|
+
* Also provides context bundle generation for LLM consumption.
|
|
11
|
+
*/
|
|
12
|
+
// Re-export context bundle functions
|
|
13
|
+
export { buildContextBundleJSON, buildContextBundleText, buildTaskContext, } from './context-bundle.js';
|
|
14
|
+
/**
|
|
15
|
+
* Filter a loaded plan by the given criteria
|
|
16
|
+
*
|
|
17
|
+
* @param plan - The loaded plan to filter
|
|
18
|
+
* @param criteria - Filter criteria to apply
|
|
19
|
+
* @returns Filtered plan with matching modules and tasks
|
|
20
|
+
*/
|
|
21
|
+
export function filterPlan(plan, criteria) {
|
|
22
|
+
// Start with all modules and tasks
|
|
23
|
+
let filteredModules = Array.from(plan.modules.values());
|
|
24
|
+
let filteredTasks = [...plan.allTasks];
|
|
25
|
+
// Apply module-level filters first
|
|
26
|
+
if (criteria.modules && criteria.modules.length > 0) {
|
|
27
|
+
filteredModules = filteredModules.filter((m) => criteria.modules.includes(m.id));
|
|
28
|
+
// Also filter tasks to only those in matching modules
|
|
29
|
+
const moduleIds = new Set(criteria.modules);
|
|
30
|
+
filteredTasks = filteredTasks.filter((t) => {
|
|
31
|
+
const taskModule = findTaskModule(plan, t.id);
|
|
32
|
+
return taskModule && moduleIds.has(taskModule.id);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// Apply scope filter (matches module scope OR task scopes)
|
|
36
|
+
if (criteria.scopes && criteria.scopes.length > 0) {
|
|
37
|
+
const scopeSet = new Set(criteria.scopes.map((s) => s.toUpperCase()));
|
|
38
|
+
filteredModules = filteredModules.filter((m) => {
|
|
39
|
+
const moduleScope = m.metadata.scope?.toUpperCase();
|
|
40
|
+
return moduleScope && scopeSet.has(moduleScope);
|
|
41
|
+
});
|
|
42
|
+
filteredTasks = filteredTasks.filter((t) => {
|
|
43
|
+
// Check if any of the task's scopes match
|
|
44
|
+
if (t.scopes && t.scopes.length > 0) {
|
|
45
|
+
return t.scopes.some((s) => scopeSet.has(s.toUpperCase()));
|
|
46
|
+
}
|
|
47
|
+
// Fall back to checking if task ID prefix matches scope
|
|
48
|
+
const taskScope = t.id.split('-')[0];
|
|
49
|
+
return scopeSet.has(taskScope);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// Apply task ID filter
|
|
53
|
+
if (criteria.tasks && criteria.tasks.length > 0) {
|
|
54
|
+
const taskIdSet = new Set(criteria.tasks);
|
|
55
|
+
filteredTasks = filteredTasks.filter((t) => taskIdSet.has(t.id));
|
|
56
|
+
}
|
|
57
|
+
// Apply owner filter
|
|
58
|
+
if (criteria.owners && criteria.owners.length > 0) {
|
|
59
|
+
const ownerSet = new Set(criteria.owners.map((o) => o.toLowerCase()));
|
|
60
|
+
filteredModules = filteredModules.filter((m) => {
|
|
61
|
+
const owner = m.metadata.owner?.toLowerCase();
|
|
62
|
+
return owner && ownerSet.has(owner);
|
|
63
|
+
});
|
|
64
|
+
// For tasks, filter by the module owner (tasks don't have individual owners)
|
|
65
|
+
filteredTasks = filteredTasks.filter((t) => {
|
|
66
|
+
const taskModule = findTaskModule(plan, t.id);
|
|
67
|
+
if (!taskModule)
|
|
68
|
+
return false;
|
|
69
|
+
const owner = taskModule.metadata.owner?.toLowerCase();
|
|
70
|
+
return owner && ownerSet.has(owner);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// Apply tag filter (matches any tag)
|
|
74
|
+
if (criteria.tags && criteria.tags.length > 0) {
|
|
75
|
+
const tagSet = new Set(criteria.tags.map((t) => t.toLowerCase()));
|
|
76
|
+
filteredModules = filteredModules.filter((m) => {
|
|
77
|
+
const moduleTags = m.metadata.tags?.map((t) => t.toLowerCase()) ?? [];
|
|
78
|
+
return moduleTags.some((t) => tagSet.has(t));
|
|
79
|
+
});
|
|
80
|
+
filteredTasks = filteredTasks.filter((t) => {
|
|
81
|
+
const taskTags = t.tags?.map((tag) => tag.toLowerCase()) ?? [];
|
|
82
|
+
return taskTags.some((tag) => tagSet.has(tag));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// Apply priority filter
|
|
86
|
+
if (criteria.priorities && criteria.priorities.length > 0) {
|
|
87
|
+
const prioritySet = new Set(criteria.priorities);
|
|
88
|
+
filteredModules = filteredModules.filter((m) => {
|
|
89
|
+
return m.metadata.priority && prioritySet.has(m.metadata.priority);
|
|
90
|
+
});
|
|
91
|
+
// Tasks don't have priority, so filter by module priority
|
|
92
|
+
filteredTasks = filteredTasks.filter((t) => {
|
|
93
|
+
const taskModule = findTaskModule(plan, t.id);
|
|
94
|
+
return taskModule?.metadata.priority && prioritySet.has(taskModule.metadata.priority);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// Apply confidence filter
|
|
98
|
+
if (criteria.confidences && criteria.confidences.length > 0) {
|
|
99
|
+
const confidenceSet = new Set(criteria.confidences);
|
|
100
|
+
filteredTasks = filteredTasks.filter((t) => confidenceSet.has(t.confidence));
|
|
101
|
+
}
|
|
102
|
+
// Apply status filter
|
|
103
|
+
if (criteria.statuses && criteria.statuses.length > 0) {
|
|
104
|
+
const statusSet = new Set(criteria.statuses);
|
|
105
|
+
filteredTasks = filteredTasks.filter((t) => {
|
|
106
|
+
const status = t.status ?? 'open';
|
|
107
|
+
return statusSet.has(status);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
plan,
|
|
112
|
+
modules: filteredModules,
|
|
113
|
+
tasks: filteredTasks,
|
|
114
|
+
criteria,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Find the module that contains a task
|
|
119
|
+
*/
|
|
120
|
+
function findTaskModule(plan, taskId) {
|
|
121
|
+
for (const module of plan.modules.values()) {
|
|
122
|
+
if (module.tasks.some((t) => t.id === taskId)) {
|
|
123
|
+
return module;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Filter tasks by scope (convenience function)
|
|
130
|
+
*/
|
|
131
|
+
export function filterByScope(plan, scopes) {
|
|
132
|
+
return filterPlan(plan, { scopes }).tasks;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Filter tasks by module (convenience function)
|
|
136
|
+
*/
|
|
137
|
+
export function filterByModule(plan, moduleIds) {
|
|
138
|
+
return filterPlan(plan, { modules: moduleIds }).tasks;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Filter tasks by tags (convenience function)
|
|
142
|
+
*/
|
|
143
|
+
export function filterByTags(plan, tags) {
|
|
144
|
+
return filterPlan(plan, { tags }).tasks;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Filter tasks by owner (convenience function)
|
|
148
|
+
*/
|
|
149
|
+
export function filterByOwner(plan, owners) {
|
|
150
|
+
return filterPlan(plan, { owners }).tasks;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Filter tasks by priority (convenience function)
|
|
154
|
+
*/
|
|
155
|
+
export function filterByPriority(plan, priorities) {
|
|
156
|
+
return filterPlan(plan, { priorities }).tasks;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Filter tasks by confidence (convenience function)
|
|
160
|
+
*/
|
|
161
|
+
export function filterByConfidence(plan, confidences) {
|
|
162
|
+
return filterPlan(plan, { confidences }).tasks;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get tasks matching specific IDs
|
|
166
|
+
*/
|
|
167
|
+
export function getTasksById(plan, taskIds) {
|
|
168
|
+
return filterPlan(plan, { tasks: taskIds }).tasks;
|
|
169
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @eddacraft/anvil-aps - Anvil Planning Spec library
|
|
3
|
+
*
|
|
4
|
+
* This package provides functionality for parsing, loading, validating,
|
|
5
|
+
* and managing Anvil Planning Spec (APS) documents.
|
|
6
|
+
*
|
|
7
|
+
* @module @eddacraft/anvil-aps
|
|
8
|
+
*/
|
|
9
|
+
export * from './parser/index.js';
|
|
10
|
+
export * from './loader/index.js';
|
|
11
|
+
export * from './filter/index.js';
|
|
12
|
+
export * from './validator/index.js';
|
|
13
|
+
export * from './state/index.js';
|
|
14
|
+
export * from './types/index.js';
|
|
15
|
+
export * from './templates/index.js';
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,sBAAsB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @eddacraft/anvil-aps - Anvil Planning Spec library
|
|
3
|
+
*
|
|
4
|
+
* This package provides functionality for parsing, loading, validating,
|
|
5
|
+
* and managing Anvil Planning Spec (APS) documents.
|
|
6
|
+
*
|
|
7
|
+
* @module @eddacraft/anvil-aps
|
|
8
|
+
*/
|
|
9
|
+
export * from './parser/index.js';
|
|
10
|
+
export * from './loader/index.js';
|
|
11
|
+
export * from './filter/index.js';
|
|
12
|
+
export * from './validator/index.js';
|
|
13
|
+
export * from './state/index.js';
|
|
14
|
+
export * from './types/index.js';
|
|
15
|
+
export * from './templates/index.js';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan loader module
|
|
3
|
+
* Loads and resolves APS planning documents into a graph structure
|
|
4
|
+
*/
|
|
5
|
+
import { type Task, type ModuleMetadata } from '../types/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* A loaded module with its tasks and metadata
|
|
8
|
+
*/
|
|
9
|
+
export interface LoadedModule {
|
|
10
|
+
/** Module identifier */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Module metadata from index or leaf spec */
|
|
13
|
+
metadata: ModuleMetadata;
|
|
14
|
+
/** All tasks in this module */
|
|
15
|
+
tasks: Task[];
|
|
16
|
+
/** Resolved absolute path to the module file */
|
|
17
|
+
resolvedPath: string;
|
|
18
|
+
/** IDs of modules this module depends on */
|
|
19
|
+
dependsOn: string[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A complete loaded plan with all modules resolved
|
|
23
|
+
*/
|
|
24
|
+
export interface LoadedPlan {
|
|
25
|
+
/** Plan title */
|
|
26
|
+
title: string;
|
|
27
|
+
/** Root file path */
|
|
28
|
+
rootPath: string;
|
|
29
|
+
/** Whether this is a single-file plan or multi-module */
|
|
30
|
+
isMultiModule: boolean;
|
|
31
|
+
/** All loaded modules */
|
|
32
|
+
modules: Map<string, LoadedModule>;
|
|
33
|
+
/** All tasks from all modules (flattened) */
|
|
34
|
+
allTasks: Task[];
|
|
35
|
+
/** Dependency graph (module ID -> dependent module IDs) */
|
|
36
|
+
dependencyGraph: Map<string, string[]>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Options for loading a plan
|
|
40
|
+
*/
|
|
41
|
+
export interface LoadOptions {
|
|
42
|
+
/** Base directory for resolving relative paths (defaults to directory of root file) */
|
|
43
|
+
baseDir?: string;
|
|
44
|
+
/** Whether to recursively load linked modules (default: true) */
|
|
45
|
+
recursive?: boolean;
|
|
46
|
+
/** Maximum depth for recursive loading (default: 10) */
|
|
47
|
+
maxDepth?: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Load an APS plan from a file path
|
|
51
|
+
*
|
|
52
|
+
* @param filePath - Path to the root plan file (index or leaf spec)
|
|
53
|
+
* @param options - Loading options
|
|
54
|
+
* @returns Loaded plan with all modules resolved
|
|
55
|
+
*/
|
|
56
|
+
export declare function loadPlan(filePath: string, options?: LoadOptions): Promise<LoadedPlan>;
|
|
57
|
+
/**
|
|
58
|
+
* Resolve a relative path against a base directory.
|
|
59
|
+
* Rejects absolute paths and paths that escape the base directory.
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolvePath(relativePath: string, baseDir: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* Get all tasks for a specific module
|
|
64
|
+
*/
|
|
65
|
+
export declare function getModuleTasks(plan: LoadedPlan, moduleId: string): Task[];
|
|
66
|
+
/**
|
|
67
|
+
* Get all modules that depend on a specific module
|
|
68
|
+
*/
|
|
69
|
+
export declare function getDependentModules(plan: LoadedPlan, moduleId: string): string[];
|
|
70
|
+
/**
|
|
71
|
+
* Get modules in topological order (dependencies first)
|
|
72
|
+
*/
|
|
73
|
+
export declare function getModulesInOrder(plan: LoadedPlan): string[];
|
|
74
|
+
/**
|
|
75
|
+
* Check for circular dependencies in the plan
|
|
76
|
+
*/
|
|
77
|
+
export declare function detectCycles(plan: LoadedPlan): string[][];
|
|
78
|
+
export type { ParsedIndex } from '../parser/parse-index.js';
|
|
79
|
+
export type { ParsedDocument } from '../types/index.js';
|
|
80
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/loader/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAUH,OAAO,EAAc,KAAK,IAAI,EAAE,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAE/E;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,wBAAwB;IACxB,EAAE,EAAE,MAAM,CAAC;IAEX,8CAA8C;IAC9C,QAAQ,EAAE,cAAc,CAAC;IAEzB,+BAA+B;IAC/B,KAAK,EAAE,IAAI,EAAE,CAAC;IAEd,gDAAgD;IAChD,YAAY,EAAE,MAAM,CAAC;IAErB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,iBAAiB;IACjB,KAAK,EAAE,MAAM,CAAC;IAEd,qBAAqB;IACrB,QAAQ,EAAE,MAAM,CAAC;IAEjB,yDAAyD;IACzD,aAAa,EAAE,OAAO,CAAC;IAEvB,yBAAyB;IACzB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAEnC,6CAA6C;IAC7C,QAAQ,EAAE,IAAI,EAAE,CAAC;IAEjB,2DAA2D;IAC3D,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;CACxC;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,uFAAuF;IACvF,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,iEAAiE;IACjE,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CAgB/F;AAwID;;;GAGG;AACH,wBAAgB,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAiBzE;AAmBD;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,CAGzE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAUhF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,EAAE,CAqB5D;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,EAAE,EAAE,CAoCzD;AAGD,YAAY,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC5D,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plan loader module
|
|
3
|
+
* Loads and resolves APS planning documents into a graph structure
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from 'node:fs';
|
|
6
|
+
import { dirname, resolve, isAbsolute, sep } from 'node:path';
|
|
7
|
+
import { unified } from 'unified';
|
|
8
|
+
import remarkParse from 'remark-parse';
|
|
9
|
+
import { visit } from 'unist-util-visit';
|
|
10
|
+
import { parseDocument } from '../parser/parse-document.js';
|
|
11
|
+
import { parseIndex } from '../parser/parse-index.js';
|
|
12
|
+
import { ParseError } from '../types/index.js';
|
|
13
|
+
/**
|
|
14
|
+
* Load an APS plan from a file path
|
|
15
|
+
*
|
|
16
|
+
* @param filePath - Path to the root plan file (index or leaf spec)
|
|
17
|
+
* @param options - Loading options
|
|
18
|
+
* @returns Loaded plan with all modules resolved
|
|
19
|
+
*/
|
|
20
|
+
export async function loadPlan(filePath, options = {}) {
|
|
21
|
+
const absolutePath = isAbsolute(filePath) ? filePath : resolve(filePath);
|
|
22
|
+
const baseDir = options.baseDir ?? dirname(absolutePath);
|
|
23
|
+
const recursive = options.recursive ?? true;
|
|
24
|
+
const maxDepth = options.maxDepth ?? 10;
|
|
25
|
+
const content = await readFile(absolutePath);
|
|
26
|
+
// Try to detect if this is an index file or leaf spec
|
|
27
|
+
const isIndex = detectIndexFile(content);
|
|
28
|
+
if (isIndex) {
|
|
29
|
+
return loadMultiModulePlan(absolutePath, content, baseDir, recursive, maxDepth);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
return loadSingleFilePlan(absolutePath, content);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Detect if content is an index file (has ## Modules section)
|
|
37
|
+
* Uses AST parsing for robust detection (case-insensitive, handles variants)
|
|
38
|
+
*/
|
|
39
|
+
function detectIndexFile(content) {
|
|
40
|
+
const processor = unified().use(remarkParse);
|
|
41
|
+
const ast = processor.parse(content);
|
|
42
|
+
let hasModulesSection = false;
|
|
43
|
+
visit(ast, 'heading', (node) => {
|
|
44
|
+
if (node.depth === 2) {
|
|
45
|
+
// Extract heading text and normalize
|
|
46
|
+
let text = '';
|
|
47
|
+
visit(node, 'text', (textNode) => {
|
|
48
|
+
text += textNode.value;
|
|
49
|
+
});
|
|
50
|
+
// Check if heading starts with "modules" (case-insensitive)
|
|
51
|
+
// This handles "## Modules", "## modules", "## Modules & Scopes", etc.
|
|
52
|
+
const normalizedText = text.trim().toLowerCase();
|
|
53
|
+
if (normalizedText === 'modules' || normalizedText.startsWith('modules')) {
|
|
54
|
+
hasModulesSection = true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return hasModulesSection;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Load a single-file plan (leaf spec with tasks)
|
|
62
|
+
*/
|
|
63
|
+
async function loadSingleFilePlan(filePath, content) {
|
|
64
|
+
const doc = await parseDocument(content, filePath);
|
|
65
|
+
const moduleId = doc.metadata?.scope ?? 'main';
|
|
66
|
+
const module = {
|
|
67
|
+
id: moduleId,
|
|
68
|
+
metadata: doc.metadata ?? {},
|
|
69
|
+
tasks: doc.tasks,
|
|
70
|
+
resolvedPath: filePath,
|
|
71
|
+
dependsOn: [],
|
|
72
|
+
};
|
|
73
|
+
const modules = new Map();
|
|
74
|
+
modules.set(moduleId, module);
|
|
75
|
+
return {
|
|
76
|
+
title: doc.title,
|
|
77
|
+
rootPath: filePath,
|
|
78
|
+
isMultiModule: false,
|
|
79
|
+
modules,
|
|
80
|
+
allTasks: doc.tasks,
|
|
81
|
+
dependencyGraph: new Map([[moduleId, []]]),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Load a multi-module plan from an index file
|
|
86
|
+
*/
|
|
87
|
+
async function loadMultiModulePlan(indexPath, content, baseDir, recursive, _maxDepth // TODO: implement depth limiting for nested index files
|
|
88
|
+
) {
|
|
89
|
+
const index = await parseIndex(content, indexPath);
|
|
90
|
+
const modules = new Map();
|
|
91
|
+
const allTasks = [];
|
|
92
|
+
const dependencyGraph = new Map();
|
|
93
|
+
// Load each module
|
|
94
|
+
for (const moduleMeta of index.modules) {
|
|
95
|
+
const moduleId = moduleMeta.id;
|
|
96
|
+
if (!moduleId) {
|
|
97
|
+
throw new ParseError('Module is missing required id field', indexPath);
|
|
98
|
+
}
|
|
99
|
+
if (!moduleMeta.path) {
|
|
100
|
+
throw new ParseError(`Module "${moduleId}" is missing required Path field`, indexPath);
|
|
101
|
+
}
|
|
102
|
+
const resolvedPath = resolvePath(moduleMeta.path, baseDir);
|
|
103
|
+
if (recursive) {
|
|
104
|
+
const moduleContent = await readFile(resolvedPath);
|
|
105
|
+
const moduleDoc = await parseDocument(moduleContent, resolvedPath);
|
|
106
|
+
// Merge metadata from index with any from the leaf spec
|
|
107
|
+
const mergedMetadata = {
|
|
108
|
+
...moduleDoc.metadata,
|
|
109
|
+
...moduleMeta,
|
|
110
|
+
id: moduleId,
|
|
111
|
+
};
|
|
112
|
+
const loadedModule = {
|
|
113
|
+
id: moduleId,
|
|
114
|
+
metadata: mergedMetadata,
|
|
115
|
+
tasks: moduleDoc.tasks,
|
|
116
|
+
resolvedPath,
|
|
117
|
+
dependsOn: moduleMeta.dependencies ?? [],
|
|
118
|
+
};
|
|
119
|
+
modules.set(moduleId, loadedModule);
|
|
120
|
+
allTasks.push(...moduleDoc.tasks);
|
|
121
|
+
dependencyGraph.set(moduleId, moduleMeta.dependencies ?? []);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// Non-recursive: just record module metadata without loading content
|
|
125
|
+
const loadedModule = {
|
|
126
|
+
id: moduleId,
|
|
127
|
+
metadata: moduleMeta,
|
|
128
|
+
tasks: [],
|
|
129
|
+
resolvedPath,
|
|
130
|
+
dependsOn: moduleMeta.dependencies ?? [],
|
|
131
|
+
};
|
|
132
|
+
modules.set(moduleId, loadedModule);
|
|
133
|
+
dependencyGraph.set(moduleId, moduleMeta.dependencies ?? []);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
title: index.title,
|
|
138
|
+
rootPath: indexPath,
|
|
139
|
+
isMultiModule: true,
|
|
140
|
+
modules,
|
|
141
|
+
allTasks,
|
|
142
|
+
dependencyGraph,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Resolve a relative path against a base directory.
|
|
147
|
+
* Rejects absolute paths and paths that escape the base directory.
|
|
148
|
+
*/
|
|
149
|
+
export function resolvePath(relativePath, baseDir) {
|
|
150
|
+
// Reject absolute paths — module paths must be relative to baseDir
|
|
151
|
+
if (isAbsolute(relativePath)) {
|
|
152
|
+
throw new ParseError(`Absolute module paths are not allowed: ${relativePath}`, relativePath);
|
|
153
|
+
}
|
|
154
|
+
// Remove leading ./ if present
|
|
155
|
+
const cleanPath = relativePath.replace(/^\.\//, '');
|
|
156
|
+
const resolved = resolve(baseDir, cleanPath);
|
|
157
|
+
const resolvedBase = resolve(baseDir);
|
|
158
|
+
// Validate the resolved path stays within baseDir
|
|
159
|
+
if (resolved !== resolvedBase && !resolved.startsWith(resolvedBase + sep)) {
|
|
160
|
+
throw new ParseError(`Module path escapes base directory: ${relativePath}`, relativePath);
|
|
161
|
+
}
|
|
162
|
+
return resolved;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Read a file with proper error handling
|
|
166
|
+
*/
|
|
167
|
+
async function readFile(filePath) {
|
|
168
|
+
try {
|
|
169
|
+
return await fs.readFile(filePath, 'utf-8');
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
if (error.code === 'ENOENT') {
|
|
173
|
+
throw new ParseError(`File not found: ${filePath}`, filePath);
|
|
174
|
+
}
|
|
175
|
+
throw new ParseError(`Failed to read file: ${error instanceof Error ? error.message : String(error)}`, filePath);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Get all tasks for a specific module
|
|
180
|
+
*/
|
|
181
|
+
export function getModuleTasks(plan, moduleId) {
|
|
182
|
+
const module = plan.modules.get(moduleId);
|
|
183
|
+
return module?.tasks ?? [];
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Get all modules that depend on a specific module
|
|
187
|
+
*/
|
|
188
|
+
export function getDependentModules(plan, moduleId) {
|
|
189
|
+
const dependents = [];
|
|
190
|
+
for (const [id, deps] of plan.dependencyGraph) {
|
|
191
|
+
if (deps.includes(moduleId)) {
|
|
192
|
+
dependents.push(id);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return dependents;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Get modules in topological order (dependencies first)
|
|
199
|
+
*/
|
|
200
|
+
export function getModulesInOrder(plan) {
|
|
201
|
+
const visited = new Set();
|
|
202
|
+
const result = [];
|
|
203
|
+
function visit(moduleId) {
|
|
204
|
+
if (visited.has(moduleId))
|
|
205
|
+
return;
|
|
206
|
+
visited.add(moduleId);
|
|
207
|
+
const deps = plan.dependencyGraph.get(moduleId) ?? [];
|
|
208
|
+
for (const dep of deps) {
|
|
209
|
+
visit(dep);
|
|
210
|
+
}
|
|
211
|
+
result.push(moduleId);
|
|
212
|
+
}
|
|
213
|
+
for (const moduleId of plan.modules.keys()) {
|
|
214
|
+
visit(moduleId);
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Check for circular dependencies in the plan
|
|
220
|
+
*/
|
|
221
|
+
export function detectCycles(plan) {
|
|
222
|
+
const cycles = [];
|
|
223
|
+
const visited = new Set();
|
|
224
|
+
const recursionStack = new Set();
|
|
225
|
+
const path = [];
|
|
226
|
+
function dfs(moduleId) {
|
|
227
|
+
visited.add(moduleId);
|
|
228
|
+
recursionStack.add(moduleId);
|
|
229
|
+
path.push(moduleId);
|
|
230
|
+
const deps = plan.dependencyGraph.get(moduleId) ?? [];
|
|
231
|
+
for (const dep of deps) {
|
|
232
|
+
if (!visited.has(dep)) {
|
|
233
|
+
if (dfs(dep)) {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else if (recursionStack.has(dep)) {
|
|
238
|
+
// Found a cycle
|
|
239
|
+
const cycleStart = path.indexOf(dep);
|
|
240
|
+
cycles.push([...path.slice(cycleStart), dep]);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
path.pop();
|
|
244
|
+
recursionStack.delete(moduleId);
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
for (const moduleId of plan.modules.keys()) {
|
|
248
|
+
if (!visited.has(moduleId)) {
|
|
249
|
+
dfs(moduleId);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return cycles;
|
|
253
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
export { parseDocument } from './parse-document.js';
|
|
20
|
+
export { parseIndex, type ParsedIndex } from './parse-index.js';
|
|
21
|
+
export { parseTask, parseTaskHeading, parseTaskFields } from './parse-task.js';
|
|
22
|
+
export type { Task, ParsedDocument, ModuleMetadata, Confidence, TaskStatus, } from '../types/index.js';
|
|
23
|
+
export { ParseError } from '../types/index.js';
|
|
24
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/parser/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAC/E,YAAY,EACV,IAAI,EACJ,cAAc,EACd,cAAc,EACd,UAAU,EACV,UAAU,GACX,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
export { parseDocument } from './parse-document.js';
|
|
20
|
+
export { parseIndex } from './parse-index.js';
|
|
21
|
+
export { parseTask, parseTaskHeading, parseTaskFields } from './parse-task.js';
|
|
22
|
+
export { ParseError } from '../types/index.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document parsing utilities
|
|
3
|
+
* Converts APS Markdown documents to structured data
|
|
4
|
+
*/
|
|
5
|
+
import { type ParsedDocument } from '../types/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Parse an APS leaf spec document from Markdown content
|
|
8
|
+
*
|
|
9
|
+
* This function parses leaf specs (documents with tasks).
|
|
10
|
+
* For index files (documents with modules), use `parseIndex` instead.
|
|
11
|
+
*
|
|
12
|
+
* @param content - Markdown content of a leaf spec
|
|
13
|
+
* @param sourcePath - Optional source file path for error reporting
|
|
14
|
+
* @returns Parsed document with tasks
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseDocument(content: string, sourcePath?: string): Promise<ParsedDocument>;
|
|
17
|
+
//# sourceMappingURL=parse-document.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-document.d.ts","sourceRoot":"","sources":["../../src/parser/parse-document.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,OAAO,EAAyB,KAAK,cAAc,EAAuB,MAAM,mBAAmB,CAAC;AAEpG;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAmBjG"}
|