@wundam/orchex 1.0.0-rc.2 → 1.0.0-rc.21

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 (98) hide show
  1. package/README.md +59 -18
  2. package/dist/cloud-executor.d.ts +71 -0
  3. package/dist/cloud-executor.js +335 -0
  4. package/dist/cloud-sync.d.ts +8 -0
  5. package/dist/cloud-sync.js +52 -0
  6. package/dist/config.d.ts +30 -4
  7. package/dist/config.js +61 -2
  8. package/dist/context-builder.d.ts +2 -0
  9. package/dist/context-builder.js +11 -3
  10. package/dist/cost.js +1 -1
  11. package/dist/entitlements/jwt.d.ts +7 -0
  12. package/dist/entitlements/jwt.js +78 -0
  13. package/dist/entitlements/resolve.d.ts +17 -0
  14. package/dist/entitlements/resolve.js +49 -0
  15. package/dist/entitlements/types.d.ts +21 -0
  16. package/dist/entitlements/types.js +4 -0
  17. package/dist/executors/base.d.ts +1 -1
  18. package/dist/executors/bedrock-executor.d.ts +39 -0
  19. package/dist/executors/bedrock-executor.js +197 -0
  20. package/dist/executors/index.d.ts +1 -0
  21. package/dist/executors/index.js +24 -1
  22. package/dist/index.js +468 -23
  23. package/dist/intelligence/index.d.ts +44 -0
  24. package/dist/intelligence/index.js +160 -0
  25. package/dist/key-cache.d.ts +31 -0
  26. package/dist/key-cache.js +84 -0
  27. package/dist/login-helpers.d.ts +25 -0
  28. package/dist/login-helpers.js +54 -0
  29. package/dist/manifest.js +18 -1
  30. package/dist/mcp-instructions.d.ts +1 -0
  31. package/dist/mcp-instructions.js +84 -0
  32. package/dist/mcp-resources.d.ts +8 -0
  33. package/dist/mcp-resources.js +420 -0
  34. package/dist/model-cache.d.ts +18 -0
  35. package/dist/model-cache.js +62 -0
  36. package/dist/model-validator.d.ts +20 -0
  37. package/dist/model-validator.js +125 -0
  38. package/dist/orchestrator.d.ts +14 -0
  39. package/dist/orchestrator.js +191 -32
  40. package/dist/setup/ide-registry.d.ts +13 -0
  41. package/dist/setup/ide-registry.js +51 -0
  42. package/dist/setup/index.d.ts +1 -0
  43. package/dist/setup/index.js +111 -0
  44. package/dist/tier-gating.js +0 -16
  45. package/dist/tiers.d.ts +35 -5
  46. package/dist/tiers.js +39 -3
  47. package/dist/tools.d.ts +6 -1
  48. package/dist/tools.js +852 -95
  49. package/dist/types.d.ts +71 -60
  50. package/dist/types.js +3 -0
  51. package/dist/waves.d.ts +1 -1
  52. package/dist/waves.js +29 -2
  53. package/package.json +41 -5
  54. package/src/entitlements/public-key.pem +9 -0
  55. package/dist/intelligence/anti-pattern-detector.d.ts +0 -117
  56. package/dist/intelligence/anti-pattern-detector.js +0 -327
  57. package/dist/intelligence/budget-enforcer.d.ts +0 -119
  58. package/dist/intelligence/budget-enforcer.js +0 -226
  59. package/dist/intelligence/context-optimizer.d.ts +0 -111
  60. package/dist/intelligence/context-optimizer.js +0 -282
  61. package/dist/intelligence/cost-tracker.d.ts +0 -114
  62. package/dist/intelligence/cost-tracker.js +0 -183
  63. package/dist/intelligence/deliverable-extractor.d.ts +0 -134
  64. package/dist/intelligence/deliverable-extractor.js +0 -909
  65. package/dist/intelligence/dependency-inferrer.d.ts +0 -87
  66. package/dist/intelligence/dependency-inferrer.js +0 -403
  67. package/dist/intelligence/diagnostics.d.ts +0 -33
  68. package/dist/intelligence/diagnostics.js +0 -64
  69. package/dist/intelligence/error-analyzer.d.ts +0 -7
  70. package/dist/intelligence/error-analyzer.js +0 -76
  71. package/dist/intelligence/file-chunker.d.ts +0 -15
  72. package/dist/intelligence/file-chunker.js +0 -64
  73. package/dist/intelligence/fix-stream-manager.d.ts +0 -59
  74. package/dist/intelligence/fix-stream-manager.js +0 -212
  75. package/dist/intelligence/heuristics.d.ts +0 -23
  76. package/dist/intelligence/heuristics.js +0 -124
  77. package/dist/intelligence/learning-engine.d.ts +0 -157
  78. package/dist/intelligence/learning-engine.js +0 -433
  79. package/dist/intelligence/learning-feedback.d.ts +0 -96
  80. package/dist/intelligence/learning-feedback.js +0 -202
  81. package/dist/intelligence/pattern-analyzer.d.ts +0 -35
  82. package/dist/intelligence/pattern-analyzer.js +0 -189
  83. package/dist/intelligence/plan-parser.d.ts +0 -124
  84. package/dist/intelligence/plan-parser.js +0 -498
  85. package/dist/intelligence/planner.d.ts +0 -29
  86. package/dist/intelligence/planner.js +0 -86
  87. package/dist/intelligence/self-healer.d.ts +0 -16
  88. package/dist/intelligence/self-healer.js +0 -84
  89. package/dist/intelligence/slicing-metrics.d.ts +0 -62
  90. package/dist/intelligence/slicing-metrics.js +0 -202
  91. package/dist/intelligence/slicing-templates.d.ts +0 -81
  92. package/dist/intelligence/slicing-templates.js +0 -420
  93. package/dist/intelligence/split-suggester.d.ts +0 -69
  94. package/dist/intelligence/split-suggester.js +0 -176
  95. package/dist/intelligence/stream-generator.d.ts +0 -90
  96. package/dist/intelligence/stream-generator.js +0 -452
  97. package/dist/telemetry/telemetry-types.d.ts +0 -85
  98. package/dist/telemetry/telemetry-types.js +0 -1
@@ -1,909 +0,0 @@
1
- /**
2
- * Atomic deliverable extraction from parsed planning documents.
3
- * Transforms parsed sections into stream-ready deliverables.
4
- *
5
- * Phase 8B-C: Semantic splitting based on file content classification.
6
- * YAML-sourced deliverables are never auto-split (author knows best).
7
- */
8
- import { getSectionsAtLevel, flattenSections, extractYamlStreamDefinitions } from './plan-parser.js';
9
- import { categorizeStream } from './learning-engine.js';
10
- /**
11
- * Priority order for file classification (highest priority first).
12
- * When a file matches multiple patterns (e.g., types.test.ts), higher priority wins.
13
- */
14
- const CONCERN_PRIORITY = ['tests', 'migrations', 'docs', 'types', 'styles', 'core'];
15
- /**
16
- * Dependency order for split streams.
17
- * Defines execution sequence: types must exist before core, core before tests, etc.
18
- */
19
- const CONCERN_ORDER = ['types', 'migrations', 'styles', 'core', 'tests', 'docs'];
20
- /**
21
- * Classify a file path into a concern bucket based on filename and path.
22
- * Uses CONCERN_PRIORITY to resolve ambiguous matches.
23
- */
24
- export function classifyFile(filePath) {
25
- const lower = filePath.toLowerCase();
26
- const basename = lower.split('/').pop() ?? lower;
27
- // Check in priority order - first match wins
28
- for (const concern of CONCERN_PRIORITY) {
29
- switch (concern) {
30
- case 'tests':
31
- if (lower.includes('/test') ||
32
- lower.includes('/__tests__/') ||
33
- basename.includes('.test.') ||
34
- basename.includes('.spec.') ||
35
- basename.startsWith('test_') ||
36
- basename.endsWith('_test.ts') ||
37
- basename.endsWith('_test.js')) {
38
- return 'tests';
39
- }
40
- break;
41
- case 'migrations':
42
- if (lower.includes('/migration') ||
43
- basename.includes('migration') ||
44
- basename.endsWith('.sql')) {
45
- return 'migrations';
46
- }
47
- break;
48
- case 'docs':
49
- if (lower.endsWith('.md') ||
50
- lower.includes('/docs/') ||
51
- lower.includes('/documentation/')) {
52
- return 'docs';
53
- }
54
- break;
55
- case 'types':
56
- if (basename.includes('types') ||
57
- basename.includes('schema') ||
58
- basename.includes('interface') ||
59
- basename === 'index.d.ts') {
60
- return 'types';
61
- }
62
- break;
63
- case 'styles':
64
- if (lower.endsWith('.css') ||
65
- lower.endsWith('.scss') ||
66
- lower.endsWith('.less') ||
67
- lower.endsWith('.sass') ||
68
- lower.includes('/styles/')) {
69
- return 'styles';
70
- }
71
- break;
72
- }
73
- }
74
- // Default: everything else is core implementation
75
- return 'core';
76
- }
77
- /**
78
- * Keywords associated with each concern for plan extraction.
79
- */
80
- const CONCERN_KEYWORDS = {
81
- types: ['type', 'interface', 'schema', 'define', 'definition', 'typescript', 'zod'],
82
- migrations: ['migrate', 'migration', 'schema', 'table', 'column', 'database', 'sql', 'alter'],
83
- core: ['implement', 'create', 'add', 'configure', 'setup', 'install', 'build'],
84
- tests: ['test', 'verify', 'check', 'assert', 'coverage', 'spec', 'expect'],
85
- docs: ['document', 'readme', 'guide', 'example', 'usage', 'api reference'],
86
- styles: ['style', 'css', 'scss', 'theme', 'design', 'token', 'color', 'layout'],
87
- };
88
- /**
89
- * Extract relevant plan lines for a specific concern.
90
- * Matches lines containing concern keywords or file names.
91
- * Never returns generic template text - always preserves original content.
92
- */
93
- export function extractPlanForConcern(originalPlan, concern, files) {
94
- const lines = originalPlan.split('\n');
95
- const keywords = CONCERN_KEYWORDS[concern];
96
- // Extract file basenames for matching
97
- const fileBasenames = files.map(f => {
98
- const basename = f.split('/').pop() ?? f;
99
- return basename.toLowerCase().replace(/\.[^.]+$/, ''); // Remove extension
100
- });
101
- // Find lines mentioning this concern's keywords or files
102
- const relevantLines = lines.filter(line => {
103
- const lower = line.toLowerCase();
104
- // Check if line mentions any keyword
105
- if (keywords.some(kw => lower.includes(kw))) {
106
- return true;
107
- }
108
- // Check if line mentions any of the files (by basename)
109
- if (fileBasenames.some(basename => lower.includes(basename))) {
110
- return true;
111
- }
112
- return false;
113
- });
114
- // If we found relevant lines, use them
115
- if (relevantLines.length > 0) {
116
- return relevantLines.join('\n');
117
- }
118
- // Fallback: return original plan with file context (never generic template)
119
- return `${originalPlan}\n\nFiles: ${files.join(', ')}`;
120
- }
121
- /**
122
- * Generate a stream ID from a section title.
123
- */
124
- export function generateStreamId(title, prefix) {
125
- const base = title
126
- .toLowerCase()
127
- .replace(/[^a-z0-9]+/g, '-')
128
- .replace(/^-+|-+$/g, '')
129
- .slice(0, 40);
130
- return prefix ? `${prefix}-${base}` : base;
131
- }
132
- /**
133
- * Extract files from "**Files:**" structured lists in markdown.
134
- * Matches patterns like:
135
- * - Create: `src/foo.ts`
136
- * - Modify: `src/bar.ts:42-50`
137
- * - Test: `tests/foo.test.ts`
138
- *
139
- * "Create" and "Modify" → owned files
140
- * "Test" → owned files (the test file itself)
141
- * Line number suffixes (`:42-50`) are stripped.
142
- */
143
- function extractStructuredFiles(content) {
144
- const owned = [];
145
- const reads = [];
146
- // Match Create:/Modify:/Test: with optional bullet prefix, anchored to start of line
147
- const createModifyPattern = /^\s*[-*]?\s*(?:Create|Modify|Test):\s*`([^`]+)`/gim;
148
- let match;
149
- while ((match = createModifyPattern.exec(content)) !== null) {
150
- // Strip line number suffixes like :42-50 or :123
151
- const filePath = match[1].replace(/:\d+(-\d+)?$/, '').trim();
152
- if (filePath && filePath.includes('.')) {
153
- owned.push(filePath);
154
- }
155
- }
156
- // Match "**File:** `path`" or "**New file:** `path`" → owned
157
- // Common pattern in design docs: bold "File:" followed by backtick-quoted path
158
- const filePattern = /\*\*(?:New\s+)?[Ff]ile:\*\*\s*`([^`]+)`/gi;
159
- while ((match = filePattern.exec(content)) !== null) {
160
- const filePath = match[1].replace(/:\d+(-\d+)?$/, '').trim();
161
- if (filePath && filePath.includes('.')) {
162
- owned.push(filePath);
163
- }
164
- }
165
- // Match Read:/Reads:/Import:/Imports: with optional bullet prefix, anchored to start of line
166
- const readPattern = /^\s*[-*]?\s*(?:Reads?|Imports?):\s*`([^`]+)`/gim;
167
- while ((match = readPattern.exec(content)) !== null) {
168
- const filePath = match[1].replace(/:\d+(-\d+)?$/, '').trim();
169
- if (filePath && filePath.includes('.')) {
170
- reads.push(filePath);
171
- }
172
- }
173
- return { owned, reads };
174
- }
175
- /**
176
- * Extract imported file paths from code blocks.
177
- * Parses ES module imports and CommonJS require() calls.
178
- *
179
- * Path resolution:
180
- * - Relative imports ('./path'): resolved against code block filename directory.
181
- * Skipped if no filename context available.
182
- * - Absolute-style imports ('src/path'): used as-is.
183
- * - Bare specifiers ('express', 'pg'): skipped (node_modules).
184
- * - .js extensions normalized to .ts (TypeScript ESM convention).
185
- */
186
- function extractImportsFromCodeBlocks(codeBlocks, excludeFiles) {
187
- const imports = new Set();
188
- const excludeSet = new Set(excludeFiles);
189
- // Match: import ... from '...' or import '...'
190
- const esImportRe = /(?:import\s+(?:[\s\S]*?\s+from\s+)?|import\s*\()['"]([^'"]+)['"]/g;
191
- // Match: require('...')
192
- const requireRe = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
193
- for (const block of codeBlocks) {
194
- const specifiers = [];
195
- // Extract all import specifiers from this code block
196
- let match;
197
- esImportRe.lastIndex = 0;
198
- while ((match = esImportRe.exec(block.code)) !== null) {
199
- specifiers.push(match[1]);
200
- }
201
- requireRe.lastIndex = 0;
202
- while ((match = requireRe.exec(block.code)) !== null) {
203
- specifiers.push(match[1]);
204
- }
205
- for (const specifier of specifiers) {
206
- const resolved = resolveImportSpecifier(specifier, block.filename);
207
- if (!resolved)
208
- continue;
209
- if (excludeSet.has(resolved))
210
- continue;
211
- // Suffix match: resolved may be a shorter path (e.g. "routes/landing.ts")
212
- // while excludeFiles has the full path (e.g. "src/v2/routes/landing.ts")
213
- if (excludeFiles.some(f => f.endsWith('/' + resolved)))
214
- continue;
215
- imports.add(resolved);
216
- }
217
- }
218
- return [...imports];
219
- }
220
- /**
221
- * Resolve an import specifier to a project file path.
222
- * Returns null for unresolvable or external imports.
223
- */
224
- function resolveImportSpecifier(specifier, contextFile) {
225
- // Skip bare specifiers (node_modules): no '.' or '/' prefix, not starting with 'src/' etc.
226
- if (!specifier.startsWith('.') && !specifier.startsWith('src/') && !specifier.startsWith('tests/') && !specifier.startsWith('lib/')) {
227
- return null;
228
- }
229
- // Skip node: protocol
230
- if (specifier.startsWith('node:')) {
231
- return null;
232
- }
233
- let resolved;
234
- if (specifier.startsWith('.')) {
235
- // Relative import — needs filename context
236
- if (!contextFile)
237
- return null;
238
- // Get directory of the context file
239
- const parts = contextFile.split('/');
240
- parts.pop(); // remove filename
241
- const dir = parts.join('/');
242
- // Resolve relative path
243
- const segments = [...(dir ? dir.split('/') : [])];
244
- for (const segment of specifier.split('/')) {
245
- if (segment === '.')
246
- continue;
247
- if (segment === '..') {
248
- segments.pop();
249
- }
250
- else {
251
- segments.push(segment);
252
- }
253
- }
254
- resolved = segments.join('/');
255
- }
256
- else {
257
- // Absolute-style import (src/..., tests/...)
258
- resolved = specifier;
259
- }
260
- // Normalize .js -> .ts (TypeScript ESM convention)
261
- if (resolved.endsWith('.js')) {
262
- resolved = resolved.replace(/\.js$/, '.ts');
263
- }
264
- // Add .ts if no extension
265
- if (!resolved.includes('.')) {
266
- resolved = resolved + '.ts';
267
- }
268
- return resolved;
269
- }
270
- /**
271
- * Infer owned files from section content and its children.
272
- * Priority order:
273
- * 1. "Create:/Modify:/Test:" in **Files:** sections (from section + children)
274
- * 2. Code block filename comments (// filename on first line)
275
- * 3. Explicit `owns: [files]` pattern in content
276
- * 4. NO blind fallback to fileReferences — that causes false ownership
277
- *
278
- * Files in explicit `reads:` declarations are excluded from ownership.
279
- */
280
- export function inferOwnedFiles(section, codeBlocks) {
281
- const owned = new Set();
282
- // First, extract files explicitly marked as reads-only
283
- const readsFiles = new Set();
284
- const readsMatch = section.content.match(/reads:\s*\[([^\]]+)\]/i);
285
- if (readsMatch) {
286
- const files = readsMatch[1].split(',').map(f => f.trim().replace(/[`"']/g, ''));
287
- for (const f of files) {
288
- if (f && f.includes('.')) {
289
- readsFiles.add(f);
290
- }
291
- }
292
- }
293
- // Check structured file lists ("- Create: `path`", "- Modify: `path`")
294
- // Search both section content and children content
295
- const allContent = collectAllContent(section);
296
- const proseContent = stripFencedCodeBlocks(allContent);
297
- const structured = extractStructuredFiles(proseContent);
298
- for (const f of structured.owned) {
299
- if (!readsFiles.has(f)) {
300
- owned.add(f);
301
- }
302
- }
303
- // Add files from code block filename comments (indicates file is being created/modified)
304
- // Also check children's code blocks
305
- const allCodeBlocks = collectAllCodeBlocks(section, codeBlocks);
306
- for (const block of allCodeBlocks) {
307
- // Validate that filename looks like an actual file path (must have a file extension).
308
- // This filters out code comments like "// free:" or "// Before:" that the parser
309
- // mistakes for filenames via the `// <word>` heuristic.
310
- if (block.filename && block.filename.includes('.') && !readsFiles.has(block.filename)) {
311
- owned.add(block.filename);
312
- }
313
- }
314
- // Check for "owns: [files]" pattern in content (explicit ownership)
315
- // Search both section and children — strip code blocks to avoid matching examples
316
- const ownsPattern = /owns:\s*\[([^\]]+)\]/gi;
317
- let ownsMatch;
318
- while ((ownsMatch = ownsPattern.exec(proseContent)) !== null) {
319
- const files = ownsMatch[1].split(',').map(f => f.trim().replace(/[`"']/g, ''));
320
- for (const f of files) {
321
- if (f && f.includes('.') && !readsFiles.has(f)) {
322
- owned.add(f);
323
- }
324
- }
325
- }
326
- // NO blind fallback to fileReferences — that causes false ownership.
327
- // If no ownership signals were found, the section genuinely doesn't own files.
328
- return [...owned];
329
- }
330
- /**
331
- * Remove fenced code block content from markdown text.
332
- * Preserves text outside fences. Handles unclosed fences (strips to end).
333
- */
334
- function stripFencedCodeBlocks(content) {
335
- return content.replace(/^```[^\n]*\n[\s\S]*?^```\s*$/gm, '');
336
- }
337
- /**
338
- * Collect content from a section and all its children recursively.
339
- * Includes children titles for richer context (plan descriptions, categorization).
340
- */
341
- function collectAllContent(section) {
342
- let content = section.content;
343
- for (const child of section.children) {
344
- // Include child title for context (e.g., "### 1.2 Provider Validation on Save")
345
- content += '\n' + child.title + '\n' + collectAllContent(child);
346
- }
347
- return content;
348
- }
349
- /**
350
- * Collect code blocks from a section and all its children recursively.
351
- */
352
- function collectAllCodeBlocks(section, topLevelBlocks) {
353
- const blocks = [...topLevelBlocks];
354
- for (const child of section.children) {
355
- blocks.push(...child.codeBlocks);
356
- for (const grandchild of child.children) {
357
- blocks.push(...collectAllCodeBlocks(grandchild, []));
358
- }
359
- }
360
- return blocks;
361
- }
362
- /**
363
- * Extract the first meaningful line of prose from section content.
364
- * Skips empty lines, markdown formatting (---, **bold-only**), and short labels.
365
- * Returns empty string if no meaningful line found.
366
- */
367
- function extractFirstMeaningfulLine(content) {
368
- const lines = content.split('\n');
369
- for (const line of lines) {
370
- const trimmed = line.trim();
371
- // Skip empty lines
372
- if (!trimmed)
373
- continue;
374
- // Skip horizontal rules
375
- if (/^-{3,}$/.test(trimmed))
376
- continue;
377
- // Skip lines that are only markdown formatting (bold label, etc.)
378
- if (/^\*\*[^*]+\*\*$/.test(trimmed) && trimmed.split(/\s+/).length <= 3)
379
- continue;
380
- // Must have more than 3 words to be "meaningful"
381
- const wordCount = trimmed.split(/\s+/).filter(w => w.length > 0).length;
382
- if (wordCount > 3) {
383
- // Truncate to ~150 chars to keep items compact
384
- if (trimmed.length > 150) {
385
- return trimmed.slice(0, 147) + '...';
386
- }
387
- return trimmed;
388
- }
389
- }
390
- return '';
391
- }
392
- /**
393
- * Build a description from a section, using structured extraction for large sections.
394
- *
395
- * When content exceeds budget AND the section has >= 3 children, switches from
396
- * raw prose dump to a numbered task list of H3 titles + first meaningful line.
397
- * This prevents silent data loss from .slice(0, budget) on large sections.
398
- */
399
- function buildStructuredDescription(section, ownedFiles, budget = 3000) {
400
- const fullContent = collectAllContent(section);
401
- // Simple sections: existing behavior
402
- if (fullContent.length <= budget || section.children.length < 3) {
403
- return fullContent.slice(0, budget);
404
- }
405
- // Complex sections: structured extraction
406
- const parts = [];
407
- // 1. Include intro (section's direct content before children, first 300 chars)
408
- const intro = section.content.trim();
409
- if (intro) {
410
- parts.push(intro.slice(0, 300));
411
- }
412
- // 2. Build numbered task list from H3 children
413
- for (let i = 0; i < section.children.length; i++) {
414
- const child = section.children[i];
415
- const title = child.title;
416
- const detail = extractFirstMeaningfulLine(child.content);
417
- // Find owned file paths mentioned in this child's content/references
418
- const childFileRefs = child.fileReferences || [];
419
- const relevantFiles = childFileRefs.filter(f => ownedFiles.includes(f));
420
- let line = `${i + 1}. ${title}`;
421
- if (relevantFiles.length > 0) {
422
- line += ` (${relevantFiles.join(', ')})`;
423
- }
424
- if (detail) {
425
- line += ` \u2014 ${detail}`;
426
- }
427
- parts.push(line);
428
- }
429
- let totalLen = 0;
430
- const completeParts = [];
431
- for (const part of parts) {
432
- // Always include at least one part (the intro)
433
- if (totalLen + part.length + 1 > budget && completeParts.length > 0)
434
- break;
435
- completeParts.push(part);
436
- totalLen += part.length + 1; // +1 for newline join
437
- }
438
- return completeParts.join('\n');
439
- }
440
- /**
441
- * Infer read files from section content (files referenced but not owned).
442
- */
443
- export function inferReadFiles(section, ownedFiles) {
444
- const reads = new Set();
445
- // Check all content (section + children) for read markers — strip code blocks
446
- // to avoid matching YAML patterns inside fenced examples
447
- const allContent = collectAllContent(section);
448
- const proseContent = stripFencedCodeBlocks(allContent);
449
- // Check for "reads: [files]" YAML-like pattern
450
- const readsMatch = proseContent.match(/reads:\s*\[([^\]]+)\]/i);
451
- if (readsMatch) {
452
- const files = readsMatch[1].split(',').map(f => f.trim().replace(/[`"']/g, ''));
453
- for (const f of files) {
454
- if (f && f.includes('.') && !ownedFiles.includes(f)) {
455
- reads.add(f);
456
- }
457
- }
458
- }
459
- // Check for structured "- Read: `path`" markers (from Files: sections)
460
- const structured = extractStructuredFiles(proseContent);
461
- for (const f of structured.reads) {
462
- if (!ownedFiles.includes(f)) {
463
- reads.add(f);
464
- }
465
- }
466
- // Extract imports from code blocks (section + children)
467
- const allCodeBlocks = collectAllCodeBlocks(section, section.codeBlocks);
468
- const importedFiles = extractImportsFromCodeBlocks(allCodeBlocks, ownedFiles);
469
- for (const f of importedFiles) {
470
- if (!ownedFiles.includes(f)) {
471
- reads.add(f);
472
- }
473
- }
474
- return [...reads];
475
- }
476
- /**
477
- * Check if files in a deliverable belong to multiple concerns.
478
- * Returns the concern groups for potential splitting.
479
- */
480
- function getConcernGroups(files) {
481
- const groups = new Map();
482
- for (const file of files) {
483
- const concern = classifyFile(file);
484
- if (!groups.has(concern)) {
485
- groups.set(concern, []);
486
- }
487
- groups.get(concern).push(file);
488
- }
489
- return groups;
490
- }
491
- /**
492
- * Check if a deliverable should be split.
493
- * YAML-sourced deliverables are never split (author knows best).
494
- * Uses semantic concern-based analysis instead of templates.
495
- */
496
- export function shouldSplit(deliverable) {
497
- // YAML-sourced deliverables are sacred - never auto-split
498
- const meta = deliverable;
499
- if (meta._fromYaml) {
500
- return { split: false, reasons: [] };
501
- }
502
- const reasons = [];
503
- // Too many owned files
504
- if (deliverable.ownedFiles.length > 4) {
505
- reasons.push(`Owns ${deliverable.ownedFiles.length} files (max recommended: 4)`);
506
- }
507
- // Check for compound description (multiple "and" conjunctions)
508
- const andCount = (deliverable.description.match(/\band\b/gi) ?? []).length;
509
- if (andCount > 2) {
510
- reasons.push(`Description has ${andCount} "and" conjunctions (suggests multiple tasks)`);
511
- }
512
- // Check if files belong to multiple concerns (semantic splitting)
513
- if (deliverable.ownedFiles.length > 1) {
514
- const concernGroups = getConcernGroups(deliverable.ownedFiles);
515
- if (concernGroups.size > 1) {
516
- const concerns = [...concernGroups.keys()].join(', ');
517
- reasons.push(`Files belong to ${concernGroups.size} different concerns (${concerns})`);
518
- }
519
- }
520
- return { split: reasons.length > 0, reasons };
521
- }
522
- /**
523
- * Convert a YAML stream definition to a Deliverable.
524
- * Marks with _fromYaml: true so it's never auto-split.
525
- */
526
- function yamlToDeliverable(def) {
527
- const category = categorizeStream(def.name, def.plan || '');
528
- return {
529
- id: def.id,
530
- name: def.name,
531
- description: def.plan || '',
532
- category,
533
- ownedFiles: def.owns || [],
534
- readFiles: def.reads || [],
535
- explicitDeps: def.deps || [],
536
- codeExamples: [],
537
- isAtomic: true,
538
- // Mark as YAML-sourced - never auto-split
539
- _fromYaml: true,
540
- // Preserve verify and setup from YAML
541
- _verify: def.verify,
542
- _setup: def.setup,
543
- };
544
- }
545
- /**
546
- * Extract deliverables from YAML stream definitions in all sections.
547
- * Scans entire document for yaml code blocks with stream definitions.
548
- */
549
- export function extractFromYamlBlocks(plan, prefix, diagnostics) {
550
- const deliverables = [];
551
- const allSections = flattenSections(plan.sections);
552
- for (const section of allSections) {
553
- const yamlDefs = extractYamlStreamDefinitions(section);
554
- // Track YAML blocks for diagnostics
555
- if (diagnostics) {
556
- const yamlBlocks = section.codeBlocks.filter(b => b.language === 'yaml' || b.language === 'yml');
557
- diagnostics.yamlBlocksFound += yamlBlocks.length;
558
- diagnostics.yamlBlocksParsed += yamlDefs.length;
559
- }
560
- for (const def of yamlDefs) {
561
- // Apply prefix to ID if provided
562
- if (prefix && !def.id.startsWith(prefix)) {
563
- def.id = `${prefix}-${def.id}`;
564
- }
565
- const deliverable = yamlToDeliverable(def);
566
- // Check if should split
567
- const splitCheck = shouldSplit(deliverable);
568
- if (splitCheck.split) {
569
- deliverable.isAtomic = false;
570
- deliverable.suggestedSplit = splitCheck.reasons;
571
- }
572
- deliverables.push(deliverable);
573
- }
574
- }
575
- return deliverables;
576
- }
577
- /**
578
- * Check if a plan contains YAML stream definitions.
579
- */
580
- export function hasYamlStreamDefinitions(plan) {
581
- const allSections = flattenSections(plan.sections);
582
- for (const section of allSections) {
583
- const defs = extractYamlStreamDefinitions(section);
584
- if (defs.length > 0) {
585
- return true;
586
- }
587
- }
588
- return false;
589
- }
590
- /**
591
- * Detect meta/non-deliverable sections by title pattern.
592
- * These are organizational sections that don't represent implementation work:
593
- * inventories, checklists, test plans, implementation order, etc.
594
- */
595
- function isMetaSection(titleLower) {
596
- // Exact multi-word meta patterns
597
- const metaPhrases = [
598
- 'test plan',
599
- 'implementation order',
600
- 'security checklist',
601
- 'gap inventory',
602
- 'task order',
603
- 'dependency order',
604
- 'execution order',
605
- 'deployment order',
606
- 'wave order',
607
- ];
608
- if (metaPhrases.some(phrase => titleLower.includes(phrase))) {
609
- return true;
610
- }
611
- // Titles that ARE the meta topic (not just containing the word)
612
- // e.g., "Checklist" or "Inventory" as standalone section titles
613
- const metaWords = ['checklist', 'inventory', 'roadmap', 'timeline', 'schedule'];
614
- const words = titleLower.split(/[\s:]+/).filter(w => w.length > 0);
615
- const lastWord = words[words.length - 1] ?? '';
616
- if (metaWords.includes(lastWord)) {
617
- return true;
618
- }
619
- return false;
620
- }
621
- /**
622
- * Extract deliverables from a parsed plan.
623
- * Automatically detects YAML stream definitions and uses them if found.
624
- * Falls back to header-based extraction if no YAML streams are present.
625
- */
626
- export function extractDeliverables(plan, options = {}) {
627
- const { deliverableLevel = 2, prefix, preferYaml = true, diagnostics } = options;
628
- // Check for YAML stream definitions first
629
- if (preferYaml && hasYamlStreamDefinitions(plan)) {
630
- if (diagnostics)
631
- diagnostics.extractionPath = 'yaml';
632
- const result = extractFromYamlBlocks(plan, prefix, diagnostics);
633
- if (diagnostics)
634
- diagnostics.deliverableCount = result.length;
635
- return result;
636
- }
637
- // YAML check failed — count blocks for diagnostics (F2 auto-detection)
638
- if (diagnostics) {
639
- const allSections = flattenSections(plan.sections);
640
- for (const section of allSections) {
641
- diagnostics.yamlBlocksFound += section.codeBlocks.filter(b => b.language === 'yaml' || b.language === 'yml').length;
642
- }
643
- diagnostics.extractionPath = 'markdown';
644
- }
645
- // Fall back to header-based extraction
646
- const deliverables = [];
647
- const sections = getSectionsAtLevel(plan.sections, deliverableLevel);
648
- if (diagnostics) {
649
- for (const level of [2, 3, 4]) {
650
- diagnostics.sectionsFound[level] = getSectionsAtLevel(plan.sections, level).length;
651
- }
652
- }
653
- for (const section of sections) {
654
- // Skip meta sections — match only when the title IS the meta topic,
655
- // not when it merely contains the word (e.g., "Context Chunking" is NOT meta)
656
- const titleLower = section.title.toLowerCase();
657
- const titleWords = titleLower.split(/[\s:]+/).filter(w => w.length > 0);
658
- const firstWord = titleWords[0] ?? '';
659
- if ((titleLower.includes('overview') && sections.indexOf(section) === 0) ||
660
- firstWord === 'summary' || titleLower === 'summary: task order' ||
661
- firstWord === 'conclusion' ||
662
- firstWord === 'introduction' ||
663
- firstWord === 'context' ||
664
- firstWord === 'background' ||
665
- firstWord === 'appendix' ||
666
- firstWord === 'references' ||
667
- firstWord === 'changelog' ||
668
- firstWord === 'prerequisites' ||
669
- isMetaSection(titleLower)) {
670
- if (diagnostics)
671
- diagnostics.sectionsFilteredAsMeta.push(section.title);
672
- continue;
673
- }
674
- const id = generateStreamId(section.title, prefix);
675
- const ownedFiles = inferOwnedFiles(section, section.codeBlocks);
676
- const readFiles = inferReadFiles(section, ownedFiles);
677
- // Collect code blocks from children too (not just direct section)
678
- const allCodeBlocks = collectAllCodeBlocks(section, section.codeBlocks);
679
- // Collect read files from children too
680
- // Conservative: when code blocks exist with imports, only add prose refs
681
- // that match import targets. When no code blocks, keep all prose refs.
682
- const importSignals = new Set(extractImportsFromCodeBlocks(allCodeBlocks, ownedFiles));
683
- const hasImportSignals = importSignals.size > 0;
684
- function collectChildReads(s) {
685
- for (const ref of s.fileReferences) {
686
- if (ownedFiles.includes(ref) || readFiles.includes(ref))
687
- continue;
688
- // When code blocks provide import signals, only add prose refs that
689
- // match an import (confirmed dependency). Otherwise keep all prose refs.
690
- if (hasImportSignals && !importSignals.has(ref))
691
- continue;
692
- readFiles.push(ref);
693
- }
694
- for (const child of s.children) {
695
- collectChildReads(child);
696
- }
697
- }
698
- for (const child of section.children) {
699
- collectChildReads(child);
700
- }
701
- // Collect full content including children for richer descriptions and categorization
702
- const fullContent = collectAllContent(section);
703
- // Categorize the stream based on name and full content
704
- const category = categorizeStream(section.title, fullContent);
705
- const deliverable = {
706
- id,
707
- name: section.title,
708
- description: buildStructuredDescription(section, ownedFiles),
709
- category,
710
- ownedFiles,
711
- readFiles,
712
- explicitDeps: section.explicitDeps,
713
- codeExamples: allCodeBlocks,
714
- isAtomic: true,
715
- childCount: section.children.length,
716
- };
717
- // Check if should split
718
- const splitCheck = shouldSplit(deliverable);
719
- if (splitCheck.split) {
720
- deliverable.isAtomic = false;
721
- deliverable.suggestedSplit = splitCheck.reasons;
722
- }
723
- deliverables.push(deliverable);
724
- }
725
- if (diagnostics)
726
- diagnostics.deliverableCount = deliverables.length;
727
- return deliverables;
728
- }
729
- /**
730
- * Match a code block to a file concern based on language and content.
731
- * Used when code blocks lack explicit filename annotations.
732
- */
733
- function matchesCodeBlockToConcern(cb, concern) {
734
- const lang = (cb.language || '').toLowerCase();
735
- const code = cb.code.toLowerCase();
736
- switch (concern) {
737
- case 'migrations':
738
- return lang === 'sql' || code.includes('create table') || code.includes('alter table');
739
- case 'types':
740
- return ((lang === 'typescript' || lang === 'ts') &&
741
- (code.includes('export type ') || code.includes('export interface ') || code.includes('z.object')));
742
- case 'tests':
743
- return code.includes('describe(') || code.includes('it(') || code.includes('expect(');
744
- case 'docs':
745
- return lang === 'markdown' || lang === 'md';
746
- case 'styles':
747
- return lang === 'css' || lang === 'scss' || lang === 'less' || lang === 'sass';
748
- case 'core':
749
- // Core gets TypeScript/JavaScript blocks that aren't types or tests
750
- return ((lang === 'typescript' || lang === 'ts' || lang === 'javascript' || lang === 'js') &&
751
- !code.includes('export type ') &&
752
- !code.includes('export interface ') &&
753
- !code.includes('describe(') &&
754
- !code.includes('it('));
755
- default:
756
- return false;
757
- }
758
- }
759
- /**
760
- * Split a non-atomic deliverable using semantic file classification.
761
- *
762
- * Phase 8B-C: Files are grouped by concern (types, migrations, core, tests, docs).
763
- * Each group becomes a separate deliverable with:
764
- * - Relevant plan content extracted from original (not generic templates)
765
- * - Proper dependency chaining (types → migrations → core → tests → docs)
766
- * - Parent's explicit dependencies inherited by first split
767
- */
768
- export function splitDeliverable(deliverable) {
769
- // YAML-sourced deliverables are never split
770
- const meta = deliverable;
771
- if (meta._fromYaml) {
772
- return [{ ...deliverable, isAtomic: true }];
773
- }
774
- // Group files by concern
775
- const concernGroups = getConcernGroups(deliverable.ownedFiles);
776
- // If all files are same concern, don't split
777
- if (concernGroups.size <= 1) {
778
- return [{ ...deliverable, isAtomic: true }];
779
- }
780
- const result = [];
781
- let prevId = null;
782
- let idx = 0;
783
- // Create splits in CONCERN_ORDER for proper dependency sequence
784
- for (const concern of CONCERN_ORDER) {
785
- const files = concernGroups.get(concern);
786
- if (!files || files.length === 0)
787
- continue;
788
- idx++;
789
- const splitId = `${deliverable.id}-${concern}`;
790
- const splitName = `${deliverable.name} (${concern})`;
791
- // Extract relevant plan content for this concern
792
- const plan = extractPlanForConcern(deliverable.description, concern, files);
793
- // Build dependencies
794
- const deps = [];
795
- // First split inherits parent's explicit dependencies
796
- if (idx === 1 && deliverable.explicitDeps.length > 0) {
797
- deps.push(...deliverable.explicitDeps);
798
- }
799
- // Subsequent splits depend on previous split
800
- if (prevId) {
801
- deps.push(prevId);
802
- }
803
- // Assign read files based on concern
804
- let readFiles = [];
805
- switch (concern) {
806
- case 'types':
807
- // Types don't need to read anything
808
- break;
809
- case 'core':
810
- case 'migrations':
811
- // Core/migrations inherit parent's read files
812
- readFiles = [...deliverable.readFiles];
813
- break;
814
- case 'tests':
815
- // Tests read the core files they're testing
816
- const coreFiles = concernGroups.get('core') || [];
817
- readFiles = [...coreFiles];
818
- break;
819
- case 'docs':
820
- // Docs read the files they're documenting
821
- const implFiles = concernGroups.get('core') || concernGroups.get('types') || [];
822
- readFiles = [...implFiles];
823
- break;
824
- }
825
- // Distribute code examples to splits
826
- // Priority: filename match > concern-based matching (language + content keywords)
827
- const codeExamples = deliverable.codeExamples.filter(cb => {
828
- // Priority 1: filename match against owned files
829
- if (cb.filename && files.some(f => cb.filename === f)) {
830
- return true;
831
- }
832
- // Priority 2: concern-based matching (no filename)
833
- if (!cb.filename) {
834
- return matchesCodeBlockToConcern(cb, concern);
835
- }
836
- return false;
837
- });
838
- result.push({
839
- id: splitId,
840
- name: splitName,
841
- description: plan,
842
- category: deliverable.category,
843
- ownedFiles: files,
844
- readFiles,
845
- explicitDeps: deps,
846
- codeExamples,
847
- isAtomic: true,
848
- });
849
- prevId = splitId;
850
- }
851
- return result;
852
- }
853
- /**
854
- * Process all deliverables, splitting non-atomic ones.
855
- */
856
- export function processDeliverables(deliverables) {
857
- const result = [];
858
- for (const d of deliverables) {
859
- if (d.isAtomic) {
860
- result.push(d);
861
- }
862
- else {
863
- result.push(...splitDeliverable(d));
864
- }
865
- }
866
- return result;
867
- }
868
- /**
869
- * Format deliverable as human-readable summary.
870
- */
871
- export function formatDeliverable(deliverable) {
872
- const lines = [];
873
- lines.push(`${deliverable.id}:`);
874
- lines.push(` Name: ${deliverable.name}`);
875
- lines.push(` Category: ${deliverable.category}`);
876
- lines.push(` Owns: ${deliverable.ownedFiles.join(', ') || '(none)'}`);
877
- if (deliverable.readFiles.length > 0) {
878
- lines.push(` Reads: ${deliverable.readFiles.join(', ')}`);
879
- }
880
- if (deliverable.explicitDeps.length > 0) {
881
- lines.push(` Deps: ${deliverable.explicitDeps.join(', ')}`);
882
- }
883
- if (!deliverable.isAtomic) {
884
- lines.push(` ⚠️ Should split: ${deliverable.suggestedSplit?.join('; ')}`);
885
- }
886
- return lines.join('\n');
887
- }
888
- /**
889
- * Format all deliverables as a report.
890
- */
891
- export function formatDeliverablesReport(deliverables) {
892
- const atomic = deliverables.filter(d => d.isAtomic);
893
- const nonAtomic = deliverables.filter(d => !d.isAtomic);
894
- let report = `=== Deliverables Report ===\n\n`;
895
- report += `Total: ${deliverables.length} deliverables\n`;
896
- report += ` ${atomic.length} atomic (ready for streams)\n`;
897
- report += ` ${nonAtomic.length} need splitting\n\n`;
898
- if (nonAtomic.length > 0) {
899
- report += `--- Needs Splitting ---\n\n`;
900
- for (const d of nonAtomic) {
901
- report += formatDeliverable(d) + '\n\n';
902
- }
903
- }
904
- report += `--- Atomic Deliverables ---\n\n`;
905
- for (const d of atomic) {
906
- report += formatDeliverable(d) + '\n\n';
907
- }
908
- return report;
909
- }