@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.
- package/README.md +59 -18
- package/dist/cloud-executor.d.ts +71 -0
- package/dist/cloud-executor.js +335 -0
- package/dist/cloud-sync.d.ts +8 -0
- package/dist/cloud-sync.js +52 -0
- package/dist/config.d.ts +30 -4
- package/dist/config.js +61 -2
- package/dist/context-builder.d.ts +2 -0
- package/dist/context-builder.js +11 -3
- package/dist/cost.js +1 -1
- package/dist/entitlements/jwt.d.ts +7 -0
- package/dist/entitlements/jwt.js +78 -0
- package/dist/entitlements/resolve.d.ts +17 -0
- package/dist/entitlements/resolve.js +49 -0
- package/dist/entitlements/types.d.ts +21 -0
- package/dist/entitlements/types.js +4 -0
- package/dist/executors/base.d.ts +1 -1
- package/dist/executors/bedrock-executor.d.ts +39 -0
- package/dist/executors/bedrock-executor.js +197 -0
- package/dist/executors/index.d.ts +1 -0
- package/dist/executors/index.js +24 -1
- package/dist/index.js +468 -23
- package/dist/intelligence/index.d.ts +44 -0
- package/dist/intelligence/index.js +160 -0
- package/dist/key-cache.d.ts +31 -0
- package/dist/key-cache.js +84 -0
- package/dist/login-helpers.d.ts +25 -0
- package/dist/login-helpers.js +54 -0
- package/dist/manifest.js +18 -1
- package/dist/mcp-instructions.d.ts +1 -0
- package/dist/mcp-instructions.js +84 -0
- package/dist/mcp-resources.d.ts +8 -0
- package/dist/mcp-resources.js +420 -0
- package/dist/model-cache.d.ts +18 -0
- package/dist/model-cache.js +62 -0
- package/dist/model-validator.d.ts +20 -0
- package/dist/model-validator.js +125 -0
- package/dist/orchestrator.d.ts +14 -0
- package/dist/orchestrator.js +191 -32
- package/dist/setup/ide-registry.d.ts +13 -0
- package/dist/setup/ide-registry.js +51 -0
- package/dist/setup/index.d.ts +1 -0
- package/dist/setup/index.js +111 -0
- package/dist/tier-gating.js +0 -16
- package/dist/tiers.d.ts +35 -5
- package/dist/tiers.js +39 -3
- package/dist/tools.d.ts +6 -1
- package/dist/tools.js +852 -95
- package/dist/types.d.ts +71 -60
- package/dist/types.js +3 -0
- package/dist/waves.d.ts +1 -1
- package/dist/waves.js +29 -2
- package/package.json +41 -5
- package/src/entitlements/public-key.pem +9 -0
- package/dist/intelligence/anti-pattern-detector.d.ts +0 -117
- package/dist/intelligence/anti-pattern-detector.js +0 -327
- package/dist/intelligence/budget-enforcer.d.ts +0 -119
- package/dist/intelligence/budget-enforcer.js +0 -226
- package/dist/intelligence/context-optimizer.d.ts +0 -111
- package/dist/intelligence/context-optimizer.js +0 -282
- package/dist/intelligence/cost-tracker.d.ts +0 -114
- package/dist/intelligence/cost-tracker.js +0 -183
- package/dist/intelligence/deliverable-extractor.d.ts +0 -134
- package/dist/intelligence/deliverable-extractor.js +0 -909
- package/dist/intelligence/dependency-inferrer.d.ts +0 -87
- package/dist/intelligence/dependency-inferrer.js +0 -403
- package/dist/intelligence/diagnostics.d.ts +0 -33
- package/dist/intelligence/diagnostics.js +0 -64
- package/dist/intelligence/error-analyzer.d.ts +0 -7
- package/dist/intelligence/error-analyzer.js +0 -76
- package/dist/intelligence/file-chunker.d.ts +0 -15
- package/dist/intelligence/file-chunker.js +0 -64
- package/dist/intelligence/fix-stream-manager.d.ts +0 -59
- package/dist/intelligence/fix-stream-manager.js +0 -212
- package/dist/intelligence/heuristics.d.ts +0 -23
- package/dist/intelligence/heuristics.js +0 -124
- package/dist/intelligence/learning-engine.d.ts +0 -157
- package/dist/intelligence/learning-engine.js +0 -433
- package/dist/intelligence/learning-feedback.d.ts +0 -96
- package/dist/intelligence/learning-feedback.js +0 -202
- package/dist/intelligence/pattern-analyzer.d.ts +0 -35
- package/dist/intelligence/pattern-analyzer.js +0 -189
- package/dist/intelligence/plan-parser.d.ts +0 -124
- package/dist/intelligence/plan-parser.js +0 -498
- package/dist/intelligence/planner.d.ts +0 -29
- package/dist/intelligence/planner.js +0 -86
- package/dist/intelligence/self-healer.d.ts +0 -16
- package/dist/intelligence/self-healer.js +0 -84
- package/dist/intelligence/slicing-metrics.d.ts +0 -62
- package/dist/intelligence/slicing-metrics.js +0 -202
- package/dist/intelligence/slicing-templates.d.ts +0 -81
- package/dist/intelligence/slicing-templates.js +0 -420
- package/dist/intelligence/split-suggester.d.ts +0 -69
- package/dist/intelligence/split-suggester.js +0 -176
- package/dist/intelligence/stream-generator.d.ts +0 -90
- package/dist/intelligence/stream-generator.js +0 -452
- package/dist/telemetry/telemetry-types.d.ts +0 -85
- 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
|
-
}
|