@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,312 @@
1
+ /**
2
+ * Context Bundle Builder
3
+ *
4
+ * Generates context bundles for LLM consumption from filtered plans.
5
+ * Outputs both text (Markdown) and JSON formats.
6
+ */
7
+
8
+ import type { FilteredPlan } from './index.js';
9
+
10
+ /**
11
+ * Context bundle in JSON format
12
+ */
13
+ export interface ContextBundleJSON {
14
+ /** Plan title */
15
+ title: string;
16
+
17
+ /** Filter criteria that was applied */
18
+ filter: {
19
+ scopes?: string[];
20
+ modules?: string[];
21
+ tasks?: string[];
22
+ owners?: string[];
23
+ tags?: string[];
24
+ priorities?: string[];
25
+ confidences?: string[];
26
+ };
27
+
28
+ /** Summary statistics */
29
+ summary: {
30
+ totalModules: number;
31
+ totalTasks: number;
32
+ tasksByConfidence: {
33
+ high: number;
34
+ medium: number;
35
+ low: number;
36
+ };
37
+ tasksByStatus: {
38
+ open: number;
39
+ locked: number;
40
+ completed: number;
41
+ cancelled: number;
42
+ };
43
+ };
44
+
45
+ /** Modules with their tasks */
46
+ modules: Array<{
47
+ id: string;
48
+ scope?: string;
49
+ owner?: string;
50
+ priority?: string;
51
+ tags?: string[];
52
+ tasks: Array<{
53
+ id: string;
54
+ title: string;
55
+ intent: string;
56
+ confidence: string;
57
+ status: string;
58
+ scopes?: string[];
59
+ tags?: string[];
60
+ dependencies?: string[];
61
+ inputs?: string[];
62
+ expectedOutcome?: string;
63
+ }>;
64
+ }>;
65
+
66
+ /** Dependency graph (module -> dependencies) */
67
+ dependencyGraph: Record<string, string[]>;
68
+ }
69
+
70
+ /**
71
+ * Build a context bundle from a filtered plan
72
+ *
73
+ * @param filtered - The filtered plan
74
+ * @returns Context bundle in JSON format
75
+ */
76
+ export function buildContextBundleJSON(filtered: FilteredPlan): ContextBundleJSON {
77
+ const { plan, modules, tasks, criteria } = filtered;
78
+
79
+ // Build summary statistics
80
+ const summary = {
81
+ totalModules: modules.length,
82
+ totalTasks: tasks.length,
83
+ tasksByConfidence: {
84
+ high: tasks.filter((t) => t.confidence === 'high').length,
85
+ medium: tasks.filter((t) => t.confidence === 'medium').length,
86
+ low: tasks.filter((t) => t.confidence === 'low').length,
87
+ },
88
+ tasksByStatus: {
89
+ open: tasks.filter((t) => (t.status ?? 'open') === 'open').length,
90
+ locked: tasks.filter((t) => t.status === 'locked').length,
91
+ completed: tasks.filter((t) => t.status === 'completed').length,
92
+ cancelled: tasks.filter((t) => t.status === 'cancelled').length,
93
+ },
94
+ };
95
+
96
+ // Build module data with tasks
97
+ const moduleData = modules.map((module) => {
98
+ const moduleTasks = tasks.filter((t) => module.tasks.some((mt) => mt.id === t.id));
99
+
100
+ return {
101
+ id: module.id,
102
+ scope: module.metadata.scope,
103
+ owner: module.metadata.owner,
104
+ priority: module.metadata.priority,
105
+ tags: module.metadata.tags,
106
+ tasks: moduleTasks.map((t) => ({
107
+ id: t.id,
108
+ title: t.title,
109
+ intent: t.intent,
110
+ confidence: t.confidence,
111
+ status: t.status ?? 'open',
112
+ scopes: t.scopes,
113
+ tags: t.tags,
114
+ dependencies: t.dependencies,
115
+ inputs: t.inputs,
116
+ expectedOutcome: t.expectedOutcome,
117
+ })),
118
+ };
119
+ });
120
+
121
+ // Build dependency graph for filtered modules
122
+ const dependencyGraph: Record<string, string[]> = {};
123
+ for (const module of modules) {
124
+ const deps = plan.dependencyGraph.get(module.id) ?? [];
125
+ // Only include dependencies that are in the filtered set
126
+ const filteredDeps = deps.filter((d) => modules.some((m) => m.id === d));
127
+ dependencyGraph[module.id] = filteredDeps;
128
+ }
129
+
130
+ return {
131
+ title: plan.title,
132
+ filter: {
133
+ scopes: criteria.scopes,
134
+ modules: criteria.modules,
135
+ tasks: criteria.tasks,
136
+ owners: criteria.owners,
137
+ tags: criteria.tags,
138
+ priorities: criteria.priorities,
139
+ confidences: criteria.confidences,
140
+ },
141
+ summary,
142
+ modules: moduleData,
143
+ dependencyGraph,
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Build a context bundle in Markdown text format
149
+ *
150
+ * @param filtered - The filtered plan
151
+ * @returns Markdown text suitable for LLM context
152
+ */
153
+ export function buildContextBundleText(filtered: FilteredPlan): string {
154
+ const { plan, modules, tasks, criteria } = filtered;
155
+ const lines: string[] = [];
156
+
157
+ // Header
158
+ lines.push(`# ${plan.title}`);
159
+ lines.push('');
160
+
161
+ // Filter info
162
+ const filterParts: string[] = [];
163
+ if (criteria.scopes?.length) filterParts.push(`scopes: ${criteria.scopes.join(', ')}`);
164
+ if (criteria.modules?.length) filterParts.push(`modules: ${criteria.modules.join(', ')}`);
165
+ if (criteria.tasks?.length) filterParts.push(`tasks: ${criteria.tasks.join(', ')}`);
166
+ if (criteria.owners?.length) filterParts.push(`owners: ${criteria.owners.join(', ')}`);
167
+ if (criteria.tags?.length) filterParts.push(`tags: ${criteria.tags.join(', ')}`);
168
+ if (criteria.priorities?.length)
169
+ filterParts.push(`priorities: ${criteria.priorities.join(', ')}`);
170
+ if (criteria.confidences?.length)
171
+ filterParts.push(`confidences: ${criteria.confidences.join(', ')}`);
172
+
173
+ if (filterParts.length > 0) {
174
+ lines.push(`> Filtered by: ${filterParts.join('; ')}`);
175
+ lines.push('');
176
+ }
177
+
178
+ // Summary
179
+ lines.push('## Summary');
180
+ lines.push('');
181
+ lines.push(`- **Modules:** ${modules.length}`);
182
+ lines.push(`- **Tasks:** ${tasks.length}`);
183
+
184
+ const highConfidence = tasks.filter((t) => t.confidence === 'high').length;
185
+ const mediumConfidence = tasks.filter((t) => t.confidence === 'medium').length;
186
+ const lowConfidence = tasks.filter((t) => t.confidence === 'low').length;
187
+ lines.push(
188
+ `- **Confidence:** ${highConfidence} high, ${mediumConfidence} medium, ${lowConfidence} low`
189
+ );
190
+
191
+ const openTasks = tasks.filter((t) => (t.status ?? 'open') === 'open').length;
192
+ const lockedTasks = tasks.filter((t) => t.status === 'locked').length;
193
+ const completedTasks = tasks.filter((t) => t.status === 'completed').length;
194
+ lines.push(`- **Status:** ${openTasks} open, ${lockedTasks} locked, ${completedTasks} completed`);
195
+ lines.push('');
196
+
197
+ // Modules and tasks
198
+ for (const module of modules) {
199
+ lines.push(`## Module: ${module.id}`);
200
+ lines.push('');
201
+
202
+ if (module.metadata.scope) lines.push(`**Scope:** ${module.metadata.scope}`);
203
+ if (module.metadata.owner) lines.push(`**Owner:** ${module.metadata.owner}`);
204
+ if (module.metadata.priority) lines.push(`**Priority:** ${module.metadata.priority}`);
205
+ if (module.metadata.tags?.length) lines.push(`**Tags:** ${module.metadata.tags.join(', ')}`);
206
+
207
+ const deps = plan.dependencyGraph.get(module.id) ?? [];
208
+ if (deps.length > 0) lines.push(`**Dependencies:** ${deps.join(', ')}`);
209
+ lines.push('');
210
+
211
+ // Tasks for this module
212
+ const moduleTasks = tasks.filter((t) => module.tasks.some((mt) => mt.id === t.id));
213
+
214
+ if (moduleTasks.length === 0) {
215
+ lines.push('*No tasks match the filter criteria.*');
216
+ lines.push('');
217
+ continue;
218
+ }
219
+
220
+ lines.push('### Tasks');
221
+ lines.push('');
222
+
223
+ for (const task of moduleTasks) {
224
+ lines.push(`#### ${task.id}: ${task.title}`);
225
+ lines.push('');
226
+ lines.push(`**Intent:** ${task.intent}`);
227
+ lines.push(`**Confidence:** ${task.confidence}`);
228
+ lines.push(`**Status:** ${task.status ?? 'open'}`);
229
+
230
+ if (task.scopes?.length) lines.push(`**Scopes:** ${task.scopes.join(', ')}`);
231
+ if (task.tags?.length) lines.push(`**Tags:** ${task.tags.join(', ')}`);
232
+ if (task.dependencies?.length)
233
+ lines.push(`**Dependencies:** ${task.dependencies.join(', ')}`);
234
+ if (task.expectedOutcome) lines.push(`**Expected Outcome:** ${task.expectedOutcome}`);
235
+
236
+ if (task.inputs?.length) {
237
+ lines.push('**Inputs:**');
238
+ for (const input of task.inputs) {
239
+ lines.push(`- ${input}`);
240
+ }
241
+ }
242
+
243
+ lines.push('');
244
+ }
245
+ }
246
+
247
+ return lines.join('\n');
248
+ }
249
+
250
+ /**
251
+ * Build context for a single task (focused view)
252
+ */
253
+ export function buildTaskContext(filtered: FilteredPlan, taskId: string): string | null {
254
+ const task = filtered.tasks.find((t) => t.id === taskId);
255
+ if (!task) return null;
256
+
257
+ const lines: string[] = [];
258
+
259
+ lines.push(`# Task: ${task.id}`);
260
+ lines.push('');
261
+ lines.push(`## ${task.title}`);
262
+ lines.push('');
263
+ lines.push(`**Intent:** ${task.intent}`);
264
+ lines.push('');
265
+ lines.push(`**Confidence:** ${task.confidence}`);
266
+ lines.push(`**Status:** ${task.status ?? 'open'}`);
267
+
268
+ if (task.scopes?.length) {
269
+ lines.push('');
270
+ lines.push(`**Scopes:** ${task.scopes.join(', ')}`);
271
+ lines.push('');
272
+ lines.push('> These scopes define what files/modules this task is allowed to modify.');
273
+ }
274
+
275
+ if (task.tags?.length) {
276
+ lines.push('');
277
+ lines.push(`**Tags:** ${task.tags.join(', ')}`);
278
+ }
279
+
280
+ if (task.dependencies?.length) {
281
+ lines.push('');
282
+ lines.push('## Dependencies');
283
+ lines.push('');
284
+ lines.push('This task depends on:');
285
+ for (const dep of task.dependencies) {
286
+ const depTask = filtered.plan.allTasks.find((t) => t.id === dep);
287
+ if (depTask) {
288
+ lines.push(`- **${dep}:** ${depTask.title} (${depTask.status ?? 'open'})`);
289
+ } else {
290
+ lines.push(`- **${dep}:** (not found)`);
291
+ }
292
+ }
293
+ }
294
+
295
+ if (task.inputs?.length) {
296
+ lines.push('');
297
+ lines.push('## Inputs');
298
+ lines.push('');
299
+ for (const input of task.inputs) {
300
+ lines.push(`- ${input}`);
301
+ }
302
+ }
303
+
304
+ if (task.expectedOutcome) {
305
+ lines.push('');
306
+ lines.push('## Expected Outcome');
307
+ lines.push('');
308
+ lines.push(task.expectedOutcome);
309
+ }
310
+
311
+ return lines.join('\n');
312
+ }
@@ -0,0 +1,317 @@
1
+ /**
2
+ * Tests for filter module
3
+ */
4
+
5
+ import { describe, it, expect } from 'vitest';
6
+ import { dirname, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { loadPlan } from '../loader/index.js';
9
+ import {
10
+ filterPlan,
11
+ filterByScope,
12
+ filterByModule,
13
+ filterByTags,
14
+ filterByOwner,
15
+ filterByPriority,
16
+ filterByConfidence,
17
+ getTasksById,
18
+ buildContextBundleJSON,
19
+ buildContextBundleText,
20
+ buildTaskContext,
21
+ } from './index.js';
22
+
23
+ const __dirname = dirname(fileURLToPath(import.meta.url));
24
+ const EXAMPLES_DIR = join(__dirname, '../../examples');
25
+
26
+ describe('filterPlan', () => {
27
+ describe('scope filtering', () => {
28
+ it('should filter tasks by scope', async () => {
29
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
30
+ const filtered = filterPlan(plan, { scopes: ['AUTH'] });
31
+
32
+ expect(filtered.modules.length).toBe(1);
33
+ expect(filtered.modules[0].id).toBe('auth');
34
+ // Tasks are filtered by having AUTH in their scopes array, not just ID prefix
35
+ // This includes AUTH-* tasks and any task that lists AUTH in its scopes
36
+ expect(
37
+ filtered.tasks.every((t) => t.scopes?.includes('AUTH') || t.id.startsWith('AUTH-'))
38
+ ).toBe(true);
39
+ });
40
+
41
+ it('should filter by multiple scopes', async () => {
42
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
43
+ const filtered = filterPlan(plan, { scopes: ['AUTH', 'PROD'] });
44
+
45
+ expect(filtered.modules.length).toBe(2);
46
+ const moduleIds = filtered.modules.map((m) => m.id);
47
+ expect(moduleIds).toContain('auth');
48
+ expect(moduleIds).toContain('products');
49
+ });
50
+
51
+ it('should be case-insensitive for scopes', async () => {
52
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
53
+ const filtered = filterPlan(plan, { scopes: ['auth'] });
54
+
55
+ expect(filtered.modules.length).toBe(1);
56
+ expect(filtered.modules[0].id).toBe('auth');
57
+ });
58
+ });
59
+
60
+ describe('module filtering', () => {
61
+ it('should filter by module ID', async () => {
62
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
63
+ const filtered = filterPlan(plan, { modules: ['cart'] });
64
+
65
+ expect(filtered.modules.length).toBe(1);
66
+ expect(filtered.modules[0].id).toBe('cart');
67
+ expect(filtered.tasks.every((t) => t.id.startsWith('CART-'))).toBe(true);
68
+ });
69
+
70
+ it('should filter by multiple modules', async () => {
71
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
72
+ const filtered = filterPlan(plan, { modules: ['auth', 'payments'] });
73
+
74
+ expect(filtered.modules.length).toBe(2);
75
+ });
76
+ });
77
+
78
+ describe('task filtering', () => {
79
+ it('should filter by specific task IDs', async () => {
80
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
81
+ const filtered = filterPlan(plan, { tasks: ['AUTH-001', 'AUTH-002'] });
82
+
83
+ expect(filtered.tasks.length).toBe(2);
84
+ expect(filtered.tasks.map((t) => t.id)).toEqual(['AUTH-001', 'AUTH-002']);
85
+ });
86
+ });
87
+
88
+ describe('owner filtering', () => {
89
+ it('should filter by owner', async () => {
90
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
91
+ const filtered = filterPlan(plan, { owners: ['@alice'] });
92
+
93
+ expect(filtered.modules.length).toBe(1);
94
+ expect(filtered.modules[0].metadata.owner).toBe('@alice');
95
+ });
96
+
97
+ it('should be case-insensitive for owners', async () => {
98
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
99
+ const filtered = filterPlan(plan, { owners: ['@ALICE'] });
100
+
101
+ expect(filtered.modules.length).toBe(1);
102
+ });
103
+ });
104
+
105
+ describe('tag filtering', () => {
106
+ it('should filter by tags', async () => {
107
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
108
+ const filtered = filterPlan(plan, { tags: ['security'] });
109
+
110
+ // Auth module has 'security' tag
111
+ expect(filtered.modules.some((m) => m.id === 'auth')).toBe(true);
112
+ });
113
+
114
+ it('should match any tag (OR logic)', async () => {
115
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
116
+ const filtered = filterPlan(plan, { tags: ['security', 'billing'] });
117
+
118
+ // Should match both auth (security) and payments (billing)
119
+ expect(filtered.modules.length).toBeGreaterThanOrEqual(2);
120
+ });
121
+ });
122
+
123
+ describe('priority filtering', () => {
124
+ it('should filter by priority', async () => {
125
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
126
+ const filtered = filterPlan(plan, { priorities: ['high'] });
127
+
128
+ expect(filtered.modules.every((m) => m.metadata.priority === 'high')).toBe(true);
129
+ });
130
+
131
+ it('should filter by multiple priorities', async () => {
132
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
133
+ const filtered = filterPlan(plan, { priorities: ['high', 'medium'] });
134
+
135
+ expect(filtered.modules.length).toBeGreaterThan(0);
136
+ });
137
+ });
138
+
139
+ describe('confidence filtering', () => {
140
+ it('should filter tasks by confidence', async () => {
141
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'feature-auth.aps.md'));
142
+ const filtered = filterPlan(plan, { confidences: ['high'] });
143
+
144
+ expect(filtered.tasks.every((t) => t.confidence === 'high')).toBe(true);
145
+ });
146
+
147
+ it('should filter by multiple confidence levels', async () => {
148
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'feature-auth.aps.md'));
149
+ const filtered = filterPlan(plan, { confidences: ['high', 'medium'] });
150
+
151
+ expect(
152
+ filtered.tasks.every((t) => t.confidence === 'high' || t.confidence === 'medium')
153
+ ).toBe(true);
154
+ });
155
+ });
156
+
157
+ describe('combined filters', () => {
158
+ it('should apply multiple filters (AND logic)', async () => {
159
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
160
+ const filtered = filterPlan(plan, {
161
+ scopes: ['AUTH'],
162
+ priorities: ['high'],
163
+ });
164
+
165
+ expect(filtered.modules.length).toBe(1);
166
+ expect(filtered.modules[0].id).toBe('auth');
167
+ expect(filtered.modules[0].metadata.priority).toBe('high');
168
+ });
169
+ });
170
+ });
171
+
172
+ describe('convenience filter functions', () => {
173
+ it('filterByScope should return tasks', async () => {
174
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
175
+ const tasks = filterByScope(plan, ['AUTH']);
176
+
177
+ expect(tasks.length).toBeGreaterThan(0);
178
+ // Tasks are filtered by having AUTH in their scopes array
179
+ expect(tasks.every((t) => t.scopes?.includes('AUTH') || t.id.startsWith('AUTH-'))).toBe(true);
180
+ });
181
+
182
+ it('filterByModule should return tasks', async () => {
183
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
184
+ const tasks = filterByModule(plan, ['cart']);
185
+
186
+ expect(tasks.length).toBeGreaterThan(0);
187
+ expect(tasks.every((t) => t.id.startsWith('CART-'))).toBe(true);
188
+ });
189
+
190
+ it('filterByTags should return tasks', async () => {
191
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'feature-auth.aps.md'));
192
+ const tasks = filterByTags(plan, ['security']);
193
+
194
+ expect(tasks.length).toBeGreaterThan(0);
195
+ });
196
+
197
+ it('filterByOwner should return tasks', async () => {
198
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
199
+ const tasks = filterByOwner(plan, ['@alice']);
200
+
201
+ expect(tasks.length).toBeGreaterThan(0);
202
+ });
203
+
204
+ it('filterByPriority should return tasks', async () => {
205
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
206
+ const tasks = filterByPriority(plan, ['high']);
207
+
208
+ expect(tasks.length).toBeGreaterThan(0);
209
+ });
210
+
211
+ it('filterByConfidence should return tasks', async () => {
212
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'feature-auth.aps.md'));
213
+ const tasks = filterByConfidence(plan, ['high']);
214
+
215
+ expect(tasks.length).toBeGreaterThan(0);
216
+ expect(tasks.every((t) => t.confidence === 'high')).toBe(true);
217
+ });
218
+
219
+ it('getTasksById should return specific tasks', async () => {
220
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'feature-auth.aps.md'));
221
+ const tasks = getTasksById(plan, ['AUTH-001', 'AUTH-003']);
222
+
223
+ expect(tasks.length).toBe(2);
224
+ expect(tasks.map((t) => t.id).sort()).toEqual(['AUTH-001', 'AUTH-003']);
225
+ });
226
+ });
227
+
228
+ describe('context bundle builders', () => {
229
+ describe('buildContextBundleJSON', () => {
230
+ it('should build JSON bundle with summary', async () => {
231
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
232
+ const filtered = filterPlan(plan, { scopes: ['AUTH'] });
233
+ const bundle = buildContextBundleJSON(filtered);
234
+
235
+ expect(bundle.title).toBe('E-commerce Platform MVP');
236
+ expect(bundle.summary.totalModules).toBe(1);
237
+ expect(bundle.summary.totalTasks).toBeGreaterThan(0);
238
+ expect(bundle.modules.length).toBe(1);
239
+ expect(bundle.modules[0].id).toBe('auth');
240
+ });
241
+
242
+ it('should include filter criteria in bundle', async () => {
243
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
244
+ const filtered = filterPlan(plan, { scopes: ['AUTH'], tags: ['security'] });
245
+ const bundle = buildContextBundleJSON(filtered);
246
+
247
+ expect(bundle.filter.scopes).toEqual(['AUTH']);
248
+ expect(bundle.filter.tags).toEqual(['security']);
249
+ });
250
+
251
+ it('should include task details', async () => {
252
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'feature-auth.aps.md'));
253
+ const filtered = filterPlan(plan, {});
254
+ const bundle = buildContextBundleJSON(filtered);
255
+
256
+ expect(bundle.modules[0].tasks.length).toBeGreaterThan(0);
257
+ const task = bundle.modules[0].tasks[0];
258
+ expect(task.id).toBeDefined();
259
+ expect(task.title).toBeDefined();
260
+ expect(task.intent).toBeDefined();
261
+ expect(task.confidence).toBeDefined();
262
+ });
263
+ });
264
+
265
+ describe('buildContextBundleText', () => {
266
+ it('should build Markdown text bundle', async () => {
267
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
268
+ const filtered = filterPlan(plan, { scopes: ['AUTH'] });
269
+ const text = buildContextBundleText(filtered);
270
+
271
+ expect(text).toContain('# E-commerce Platform MVP');
272
+ expect(text).toContain('## Summary');
273
+ expect(text).toContain('## Module: auth');
274
+ expect(text).toContain('### Tasks');
275
+ });
276
+
277
+ it('should include filter info in text', async () => {
278
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'system-ecommerce/APS.md'));
279
+ const filtered = filterPlan(plan, { scopes: ['AUTH'] });
280
+ const text = buildContextBundleText(filtered);
281
+
282
+ expect(text).toContain('Filtered by:');
283
+ expect(text).toContain('scopes: AUTH');
284
+ });
285
+ });
286
+
287
+ describe('buildTaskContext', () => {
288
+ it('should build focused context for a single task', async () => {
289
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'feature-auth.aps.md'));
290
+ const filtered = filterPlan(plan, {});
291
+ const context = buildTaskContext(filtered, 'AUTH-001');
292
+
293
+ expect(context).not.toBeNull();
294
+ expect(context).toContain('# Task: AUTH-001');
295
+ expect(context).toContain('**Intent:**');
296
+ });
297
+
298
+ it('should return null for non-existent task', async () => {
299
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'feature-auth.aps.md'));
300
+ const filtered = filterPlan(plan, {});
301
+ const context = buildTaskContext(filtered, 'NONEXISTENT-999');
302
+
303
+ expect(context).toBeNull();
304
+ });
305
+
306
+ it('should include dependencies with status', async () => {
307
+ const plan = await loadPlan(join(EXAMPLES_DIR, 'feature-auth.aps.md'));
308
+ const filtered = filterPlan(plan, {});
309
+ // AUTH-003 depends on AUTH-001 and AUTH-002
310
+ const context = buildTaskContext(filtered, 'AUTH-003');
311
+
312
+ expect(context).toContain('## Dependencies');
313
+ expect(context).toContain('AUTH-001');
314
+ expect(context).toContain('AUTH-002');
315
+ });
316
+ });
317
+ });