@wundam/orchex 1.0.0-rc.1
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/LICENSE +65 -0
- package/README.md +332 -0
- package/bin/orchex.js +2 -0
- package/dist/artifacts.d.ts +132 -0
- package/dist/artifacts.js +832 -0
- package/dist/claude-executor.d.ts +31 -0
- package/dist/claude-executor.js +200 -0
- package/dist/commands.d.ts +36 -0
- package/dist/commands.js +264 -0
- package/dist/config.d.ts +100 -0
- package/dist/config.js +172 -0
- package/dist/context-builder.d.ts +46 -0
- package/dist/context-builder.js +506 -0
- package/dist/cost.d.ts +29 -0
- package/dist/cost.js +60 -0
- package/dist/execution-broadcaster.d.ts +18 -0
- package/dist/execution-broadcaster.js +17 -0
- package/dist/executors/base.d.ts +99 -0
- package/dist/executors/base.js +206 -0
- package/dist/executors/circuit-breaker.d.ts +36 -0
- package/dist/executors/circuit-breaker.js +109 -0
- package/dist/executors/deepseek-executor.d.ts +22 -0
- package/dist/executors/deepseek-executor.js +145 -0
- package/dist/executors/gemini-executor.d.ts +20 -0
- package/dist/executors/gemini-executor.js +176 -0
- package/dist/executors/index.d.ts +81 -0
- package/dist/executors/index.js +193 -0
- package/dist/executors/ollama-executor.d.ts +25 -0
- package/dist/executors/ollama-executor.js +184 -0
- package/dist/executors/openai-executor.d.ts +22 -0
- package/dist/executors/openai-executor.js +142 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +115 -0
- package/dist/intelligence/anti-pattern-detector.d.ts +117 -0
- package/dist/intelligence/anti-pattern-detector.js +327 -0
- package/dist/intelligence/budget-enforcer.d.ts +119 -0
- package/dist/intelligence/budget-enforcer.js +226 -0
- package/dist/intelligence/context-optimizer.d.ts +111 -0
- package/dist/intelligence/context-optimizer.js +282 -0
- package/dist/intelligence/cost-tracker.d.ts +114 -0
- package/dist/intelligence/cost-tracker.js +183 -0
- package/dist/intelligence/deliverable-extractor.d.ts +134 -0
- package/dist/intelligence/deliverable-extractor.js +909 -0
- package/dist/intelligence/dependency-inferrer.d.ts +87 -0
- package/dist/intelligence/dependency-inferrer.js +403 -0
- package/dist/intelligence/diagnostics.d.ts +25 -0
- package/dist/intelligence/diagnostics.js +36 -0
- package/dist/intelligence/error-analyzer.d.ts +7 -0
- package/dist/intelligence/error-analyzer.js +76 -0
- package/dist/intelligence/file-chunker.d.ts +15 -0
- package/dist/intelligence/file-chunker.js +64 -0
- package/dist/intelligence/fix-stream-manager.d.ts +59 -0
- package/dist/intelligence/fix-stream-manager.js +212 -0
- package/dist/intelligence/heuristics.d.ts +23 -0
- package/dist/intelligence/heuristics.js +124 -0
- package/dist/intelligence/learning-engine.d.ts +157 -0
- package/dist/intelligence/learning-engine.js +433 -0
- package/dist/intelligence/learning-feedback.d.ts +96 -0
- package/dist/intelligence/learning-feedback.js +202 -0
- package/dist/intelligence/pattern-analyzer.d.ts +35 -0
- package/dist/intelligence/pattern-analyzer.js +189 -0
- package/dist/intelligence/plan-parser.d.ts +124 -0
- package/dist/intelligence/plan-parser.js +498 -0
- package/dist/intelligence/planner.d.ts +29 -0
- package/dist/intelligence/planner.js +86 -0
- package/dist/intelligence/self-healer.d.ts +16 -0
- package/dist/intelligence/self-healer.js +84 -0
- package/dist/intelligence/slicing-metrics.d.ts +62 -0
- package/dist/intelligence/slicing-metrics.js +202 -0
- package/dist/intelligence/slicing-templates.d.ts +81 -0
- package/dist/intelligence/slicing-templates.js +420 -0
- package/dist/intelligence/split-suggester.d.ts +69 -0
- package/dist/intelligence/split-suggester.js +176 -0
- package/dist/intelligence/stream-generator.d.ts +90 -0
- package/dist/intelligence/stream-generator.js +452 -0
- package/dist/logger.d.ts +34 -0
- package/dist/logger.js +83 -0
- package/dist/logging.d.ts +5 -0
- package/dist/logging.js +38 -0
- package/dist/manifest.d.ts +56 -0
- package/dist/manifest.js +254 -0
- package/dist/metrics.d.ts +35 -0
- package/dist/metrics.js +75 -0
- package/dist/orchestrator.d.ts +35 -0
- package/dist/orchestrator.js +723 -0
- package/dist/ownership.d.ts +44 -0
- package/dist/ownership.js +250 -0
- package/dist/semaphore.d.ts +12 -0
- package/dist/semaphore.js +34 -0
- package/dist/telemetry/telemetry-types.d.ts +85 -0
- package/dist/telemetry/telemetry-types.js +1 -0
- package/dist/tier-gating.d.ts +24 -0
- package/dist/tier-gating.js +88 -0
- package/dist/tiers.d.ts +92 -0
- package/dist/tiers.js +108 -0
- package/dist/tools.d.ts +18 -0
- package/dist/tools.js +1363 -0
- package/dist/types.d.ts +740 -0
- package/dist/types.js +160 -0
- package/dist/utils/ownership-validator.d.ts +6 -0
- package/dist/utils/ownership-validator.js +21 -0
- package/dist/waves.d.ts +21 -0
- package/dist/waves.js +146 -0
- package/package.json +120 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// file-chunker.ts
|
|
2
|
+
// Extracts relevant sections from large files based on keywords (stream plan)
|
|
3
|
+
/**
|
|
4
|
+
* Chunks a file's content, selecting only the most relevant sections if it's large.
|
|
5
|
+
* For files over minLines, returns sections around lines matching any of the keywords (case-insensitive).
|
|
6
|
+
*
|
|
7
|
+
* - Keeps several lines of surrounding context (`contextRadius`).
|
|
8
|
+
* - If no matches, falls back to the first and last section slices.
|
|
9
|
+
*
|
|
10
|
+
* @param content The full file content
|
|
11
|
+
* @param keywords List of relevant keywords (from the stream plan)
|
|
12
|
+
* @param minLines Minimum lines above which chunking is triggered (default: 300)
|
|
13
|
+
* @param contextRadius How many lines before/after a match to include (default: 10)
|
|
14
|
+
* @param maxTotalLines Max total lines to include from this file (default: 120)
|
|
15
|
+
* @returns string (either chunks joined by \n, or full content if not large enough)
|
|
16
|
+
*/
|
|
17
|
+
export function extractRelevantChunks(content, keywords, minLines = 300, contextRadius = 10, maxTotalLines = 120, withLineNumbers = false) {
|
|
18
|
+
const lines = content.split(/\r?\n/);
|
|
19
|
+
if (lines.length < minLines || keywords.length === 0)
|
|
20
|
+
return content;
|
|
21
|
+
const keywordRegexes = keywords.map(kw => new RegExp(escapeRegExp(kw), 'i'));
|
|
22
|
+
// Find lines matching any keyword
|
|
23
|
+
const matches = [];
|
|
24
|
+
for (let i = 0; i < lines.length; ++i) {
|
|
25
|
+
if (keywordRegexes.some(re => re.test(lines[i]))) {
|
|
26
|
+
matches.push(i);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const included = new Set();
|
|
30
|
+
for (const idx of matches) {
|
|
31
|
+
for (let j = Math.max(0, idx - contextRadius); j <= Math.min(lines.length - 1, idx + contextRadius); ++j) {
|
|
32
|
+
included.add(j);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// If too few (or no) matches, provide fallback: first + last N lines
|
|
36
|
+
if (included.size < Math.min(20, contextRadius * 2)) {
|
|
37
|
+
for (let i = 0; i < Math.min(contextRadius * 2, lines.length); ++i)
|
|
38
|
+
included.add(i);
|
|
39
|
+
for (let i = Math.max(0, lines.length - contextRadius * 2); i < lines.length; ++i)
|
|
40
|
+
included.add(i);
|
|
41
|
+
}
|
|
42
|
+
// Sort and slice to max lines
|
|
43
|
+
const selected = Array.from(included).sort((a, b) => a - b);
|
|
44
|
+
const clipped = selected.slice(0, maxTotalLines);
|
|
45
|
+
let prev = -2;
|
|
46
|
+
const chunks = [];
|
|
47
|
+
const padWidth = withLineNumbers ? String(lines.length).length : 0;
|
|
48
|
+
for (const i of clipped) {
|
|
49
|
+
if (i > prev + 1 && chunks.length > 0)
|
|
50
|
+
chunks.push('...'); // Ellipsis between non-contiguous blocks
|
|
51
|
+
if (withLineNumbers) {
|
|
52
|
+
chunks.push(`${String(i + 1).padStart(padWidth)}: ${lines[i]}`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
chunks.push(lines[i]);
|
|
56
|
+
}
|
|
57
|
+
prev = i;
|
|
58
|
+
}
|
|
59
|
+
return chunks.join('\n');
|
|
60
|
+
}
|
|
61
|
+
function escapeRegExp(str) {
|
|
62
|
+
// For safe regex construction
|
|
63
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
64
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fix Stream Manager
|
|
3
|
+
*
|
|
4
|
+
* Handles lifecycle management of self-healing fix streams:
|
|
5
|
+
* - Cleanup of orphan fix streams when parent completes
|
|
6
|
+
* - Detection of fix stream chains
|
|
7
|
+
* - Status synchronization between parent and fix streams
|
|
8
|
+
*/
|
|
9
|
+
import type { Manifest, StreamStatus } from '../types.js';
|
|
10
|
+
export interface CleanupResult {
|
|
11
|
+
/** Fix stream IDs that were marked as skipped */
|
|
12
|
+
skipped: string[];
|
|
13
|
+
/** Warning messages (e.g., in_progress fix streams) */
|
|
14
|
+
warnings: string[];
|
|
15
|
+
}
|
|
16
|
+
export interface FixChainInfo {
|
|
17
|
+
/** The original stream ID at the root of the chain */
|
|
18
|
+
rootStreamId: string;
|
|
19
|
+
/** All fix stream IDs in the chain, in order */
|
|
20
|
+
fixChain: string[];
|
|
21
|
+
/** Current status of the root stream */
|
|
22
|
+
rootStatus: StreamStatus;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Clean up orphan fix streams.
|
|
26
|
+
*
|
|
27
|
+
* An orphan fix stream is one where:
|
|
28
|
+
* - It has a parentStreamId pointing to another stream
|
|
29
|
+
* - That parent (or any ancestor) is now 'complete'
|
|
30
|
+
* - The fix stream itself is still 'pending'
|
|
31
|
+
*
|
|
32
|
+
* This function should be called:
|
|
33
|
+
* - Before wave calculation (to prevent orphans from appearing in waves)
|
|
34
|
+
* - After any stream is marked complete
|
|
35
|
+
*/
|
|
36
|
+
export declare function cleanupOrphanFixStreams(projectDir: string): Promise<CleanupResult>;
|
|
37
|
+
/**
|
|
38
|
+
* Called when a stream is marked complete.
|
|
39
|
+
* Automatically cleans up any pending fix streams descended from this stream.
|
|
40
|
+
*
|
|
41
|
+
* This handles:
|
|
42
|
+
* - Direct fix streams (scaffold-fix-1 depends on scaffold)
|
|
43
|
+
* - Nested chains (scaffold-fix-2 -> scaffold-fix-1 -> scaffold)
|
|
44
|
+
*/
|
|
45
|
+
export declare function onStreamComplete(projectDir: string, completedStreamId: string): Promise<CleanupResult>;
|
|
46
|
+
/**
|
|
47
|
+
* Get information about a fix stream chain.
|
|
48
|
+
* Useful for debugging and display.
|
|
49
|
+
*/
|
|
50
|
+
export declare function getFixChainInfo(manifest: Manifest, streamId: string): FixChainInfo | null;
|
|
51
|
+
/**
|
|
52
|
+
* Check if this stream is a fix stream (has a parent).
|
|
53
|
+
*/
|
|
54
|
+
export declare function isFixStream(manifest: Manifest, streamId: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Get the original stream ID for a fix stream.
|
|
57
|
+
* Returns the input ID if it's not a fix stream.
|
|
58
|
+
*/
|
|
59
|
+
export declare function getOriginalStreamId(manifest: Manifest, streamId: string): string;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fix Stream Manager
|
|
3
|
+
*
|
|
4
|
+
* Handles lifecycle management of self-healing fix streams:
|
|
5
|
+
* - Cleanup of orphan fix streams when parent completes
|
|
6
|
+
* - Detection of fix stream chains
|
|
7
|
+
* - Status synchronization between parent and fix streams
|
|
8
|
+
*/
|
|
9
|
+
import { loadManifest, saveManifest } from '../manifest.js';
|
|
10
|
+
import { createLogger } from '../logging.js';
|
|
11
|
+
const log = createLogger('fix-stream-manager');
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Core Functions
|
|
14
|
+
// ============================================================================
|
|
15
|
+
/**
|
|
16
|
+
* Clean up orphan fix streams.
|
|
17
|
+
*
|
|
18
|
+
* An orphan fix stream is one where:
|
|
19
|
+
* - It has a parentStreamId pointing to another stream
|
|
20
|
+
* - That parent (or any ancestor) is now 'complete'
|
|
21
|
+
* - The fix stream itself is still 'pending'
|
|
22
|
+
*
|
|
23
|
+
* This function should be called:
|
|
24
|
+
* - Before wave calculation (to prevent orphans from appearing in waves)
|
|
25
|
+
* - After any stream is marked complete
|
|
26
|
+
*/
|
|
27
|
+
export async function cleanupOrphanFixStreams(projectDir) {
|
|
28
|
+
const manifest = await loadManifest(projectDir);
|
|
29
|
+
const result = { skipped: [], warnings: [] };
|
|
30
|
+
// Find all fix streams (streams with parentStreamId)
|
|
31
|
+
const fixStreams = Object.entries(manifest.streams).filter(([_, stream]) => stream.parentStreamId !== undefined);
|
|
32
|
+
for (const [id, stream] of fixStreams) {
|
|
33
|
+
// Only process pending fix streams
|
|
34
|
+
if (stream.status !== 'pending')
|
|
35
|
+
continue;
|
|
36
|
+
// Check if any ancestor is complete
|
|
37
|
+
if (isAncestorComplete(manifest, stream.parentStreamId)) {
|
|
38
|
+
manifest.streams[id].status = 'skipped';
|
|
39
|
+
manifest.streams[id].error = 'Parent stream completed; fix no longer needed';
|
|
40
|
+
result.skipped.push(id);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Save if any changes were made
|
|
44
|
+
if (result.skipped.length > 0) {
|
|
45
|
+
await saveManifest(projectDir, manifest);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Called when a stream is marked complete.
|
|
51
|
+
* Automatically cleans up any pending fix streams descended from this stream.
|
|
52
|
+
*
|
|
53
|
+
* This handles:
|
|
54
|
+
* - Direct fix streams (scaffold-fix-1 depends on scaffold)
|
|
55
|
+
* - Nested chains (scaffold-fix-2 -> scaffold-fix-1 -> scaffold)
|
|
56
|
+
*/
|
|
57
|
+
export async function onStreamComplete(projectDir, completedStreamId) {
|
|
58
|
+
const manifest = await loadManifest(projectDir);
|
|
59
|
+
const result = { skipped: [], warnings: [] };
|
|
60
|
+
// Find all descendant fix streams
|
|
61
|
+
const descendants = findDescendantFixStreams(manifest, completedStreamId);
|
|
62
|
+
for (const id of descendants) {
|
|
63
|
+
const stream = manifest.streams[id];
|
|
64
|
+
if (stream.status === 'pending') {
|
|
65
|
+
// Safe to skip pending fix streams
|
|
66
|
+
manifest.streams[id].status = 'skipped';
|
|
67
|
+
manifest.streams[id].error = `Ancestor '${completedStreamId}' completed; fix no longer needed`;
|
|
68
|
+
result.skipped.push(id);
|
|
69
|
+
}
|
|
70
|
+
else if (stream.status === 'in_progress') {
|
|
71
|
+
// Can't skip in_progress streams - they may be actively running
|
|
72
|
+
// Just warn about the inconsistent state
|
|
73
|
+
result.warnings.push(`Fix stream '${id}' is in_progress but ancestor '${completedStreamId}' completed. ` +
|
|
74
|
+
`Manual intervention may be needed.`);
|
|
75
|
+
}
|
|
76
|
+
// complete, failed, skipped, blocked - no action needed
|
|
77
|
+
}
|
|
78
|
+
// Backward propagation: if the completing stream is a fix stream,
|
|
79
|
+
// mark the root original stream as complete to unblock its dependents
|
|
80
|
+
let propagated = false;
|
|
81
|
+
const completedStream = manifest.streams[completedStreamId];
|
|
82
|
+
if (completedStream?.parentStreamId) {
|
|
83
|
+
let rootId = completedStream.parentStreamId;
|
|
84
|
+
while (manifest.streams[rootId]?.parentStreamId) {
|
|
85
|
+
rootId = manifest.streams[rootId].parentStreamId;
|
|
86
|
+
}
|
|
87
|
+
const root = manifest.streams[rootId];
|
|
88
|
+
if (root && root.status === 'failed') {
|
|
89
|
+
manifest.streams[rootId].status = 'complete';
|
|
90
|
+
delete manifest.streams[rootId].error;
|
|
91
|
+
propagated = true;
|
|
92
|
+
log.info({ fixStreamId: completedStreamId, rootStreamId: rootId }, 'fix_stream_propagated_completion_to_root');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (result.skipped.length > 0 || propagated) {
|
|
96
|
+
await saveManifest(projectDir, manifest);
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get information about a fix stream chain.
|
|
102
|
+
* Useful for debugging and display.
|
|
103
|
+
*/
|
|
104
|
+
export function getFixChainInfo(manifest, streamId) {
|
|
105
|
+
const stream = manifest.streams[streamId];
|
|
106
|
+
if (!stream)
|
|
107
|
+
return null;
|
|
108
|
+
// If this stream has no parent, it's not a fix stream
|
|
109
|
+
if (!stream.parentStreamId) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
// Walk up the chain to find the root
|
|
113
|
+
const chain = [streamId];
|
|
114
|
+
let currentId = stream.parentStreamId;
|
|
115
|
+
const visited = new Set();
|
|
116
|
+
while (currentId && !visited.has(currentId)) {
|
|
117
|
+
visited.add(currentId);
|
|
118
|
+
const current = manifest.streams[currentId];
|
|
119
|
+
if (!current)
|
|
120
|
+
break;
|
|
121
|
+
chain.unshift(currentId);
|
|
122
|
+
if (!current.parentStreamId) {
|
|
123
|
+
// Found the root
|
|
124
|
+
return {
|
|
125
|
+
rootStreamId: currentId,
|
|
126
|
+
fixChain: chain.slice(1), // Exclude the root from the fix chain
|
|
127
|
+
rootStatus: current.status ?? 'pending',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
currentId = current.parentStreamId;
|
|
131
|
+
}
|
|
132
|
+
// If we get here, we couldn't find a valid root (broken chain)
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Check if this stream is a fix stream (has a parent).
|
|
137
|
+
*/
|
|
138
|
+
export function isFixStream(manifest, streamId) {
|
|
139
|
+
return manifest.streams[streamId]?.parentStreamId !== undefined;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get the original stream ID for a fix stream.
|
|
143
|
+
* Returns the input ID if it's not a fix stream.
|
|
144
|
+
*/
|
|
145
|
+
export function getOriginalStreamId(manifest, streamId) {
|
|
146
|
+
const info = getFixChainInfo(manifest, streamId);
|
|
147
|
+
return info?.rootStreamId ?? streamId;
|
|
148
|
+
}
|
|
149
|
+
// ============================================================================
|
|
150
|
+
// Helper Functions
|
|
151
|
+
// ============================================================================
|
|
152
|
+
/**
|
|
153
|
+
* Check if any ancestor in the fix chain is complete.
|
|
154
|
+
*/
|
|
155
|
+
function isAncestorComplete(manifest, parentStreamId) {
|
|
156
|
+
let currentId = parentStreamId;
|
|
157
|
+
const visited = new Set();
|
|
158
|
+
while (currentId) {
|
|
159
|
+
// Prevent infinite loops from circular references
|
|
160
|
+
if (visited.has(currentId)) {
|
|
161
|
+
log.warn({ streamId: currentId }, 'circular_reference_in_fix_chain');
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
visited.add(currentId);
|
|
165
|
+
const stream = manifest.streams[currentId];
|
|
166
|
+
if (!stream) {
|
|
167
|
+
// Orphaned reference - parent doesn't exist
|
|
168
|
+
log.warn({ parentId: currentId }, 'fix_stream_orphaned_parent');
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
if (stream.status === 'complete') {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
currentId = stream.parentStreamId;
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Find all fix streams that descend from a given stream.
|
|
180
|
+
* This includes:
|
|
181
|
+
* - Direct children (fix streams where parentStreamId = ancestorId)
|
|
182
|
+
* - Indirect descendants (fix streams of fix streams)
|
|
183
|
+
*/
|
|
184
|
+
function findDescendantFixStreams(manifest, ancestorId) {
|
|
185
|
+
const descendants = [];
|
|
186
|
+
for (const [id, stream] of Object.entries(manifest.streams)) {
|
|
187
|
+
if (!stream.parentStreamId)
|
|
188
|
+
continue;
|
|
189
|
+
// Check if this stream's parent chain includes the ancestor
|
|
190
|
+
if (isDescendantOf(manifest, id, ancestorId)) {
|
|
191
|
+
descendants.push(id);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return descendants;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Check if a stream is a descendant of another stream.
|
|
198
|
+
*/
|
|
199
|
+
function isDescendantOf(manifest, streamId, ancestorId) {
|
|
200
|
+
let currentId = manifest.streams[streamId]?.parentStreamId;
|
|
201
|
+
const visited = new Set();
|
|
202
|
+
while (currentId) {
|
|
203
|
+
if (visited.has(currentId))
|
|
204
|
+
break;
|
|
205
|
+
visited.add(currentId);
|
|
206
|
+
if (currentId === ancestorId) {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
currentId = manifest.streams[currentId]?.parentStreamId;
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface FileGroup {
|
|
2
|
+
files: string[];
|
|
3
|
+
reason: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ComplexityScore {
|
|
6
|
+
score: number;
|
|
7
|
+
factors: string[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Analyze import/require statements to find files commonly modified together.
|
|
11
|
+
* Takes a map of filePath -> fileContent.
|
|
12
|
+
*/
|
|
13
|
+
export declare function detectFileCoChanges(fileContents: Record<string, string>): FileGroup[];
|
|
14
|
+
/**
|
|
15
|
+
* Estimate stream complexity from plan text, file count, and dependency count.
|
|
16
|
+
*/
|
|
17
|
+
export declare function estimateComplexity(stream: {
|
|
18
|
+
plan?: string;
|
|
19
|
+
owns?: string[];
|
|
20
|
+
deps?: string[];
|
|
21
|
+
}): ComplexityScore;
|
|
22
|
+
export declare function extractImports(content: string): string[];
|
|
23
|
+
export declare function resolveImportPath(fromFile: string, importPath: string): string | null;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyze import/require statements to find files commonly modified together.
|
|
3
|
+
* Takes a map of filePath -> fileContent.
|
|
4
|
+
*/
|
|
5
|
+
export function detectFileCoChanges(fileContents) {
|
|
6
|
+
const importGraph = new Map();
|
|
7
|
+
for (const [filePath, content] of Object.entries(fileContents)) {
|
|
8
|
+
const imports = extractImports(content);
|
|
9
|
+
importGraph.set(filePath, new Set(imports));
|
|
10
|
+
}
|
|
11
|
+
// Group files that import each other (bidirectional edges)
|
|
12
|
+
const groups = [];
|
|
13
|
+
const visited = new Set();
|
|
14
|
+
for (const [file, imports] of importGraph) {
|
|
15
|
+
if (visited.has(file))
|
|
16
|
+
continue;
|
|
17
|
+
const group = [file];
|
|
18
|
+
visited.add(file);
|
|
19
|
+
for (const imported of imports) {
|
|
20
|
+
const resolvedImport = resolveImportPath(file, imported);
|
|
21
|
+
if (resolvedImport && importGraph.has(resolvedImport) && !visited.has(resolvedImport)) {
|
|
22
|
+
// Check if it imports back
|
|
23
|
+
const reverseImports = importGraph.get(resolvedImport);
|
|
24
|
+
if (reverseImports) {
|
|
25
|
+
const resolvedBack = Array.from(reverseImports).some((imp) => resolveImportPath(resolvedImport, imp) === file);
|
|
26
|
+
if (resolvedBack) {
|
|
27
|
+
group.push(resolvedImport);
|
|
28
|
+
visited.add(resolvedImport);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (group.length > 1) {
|
|
34
|
+
groups.push({
|
|
35
|
+
files: group,
|
|
36
|
+
reason: 'Bidirectional imports detected \u2014 these files are tightly coupled.',
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return groups;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Estimate stream complexity from plan text, file count, and dependency count.
|
|
44
|
+
*/
|
|
45
|
+
export function estimateComplexity(stream) {
|
|
46
|
+
const factors = [];
|
|
47
|
+
let score = 1;
|
|
48
|
+
// Plan length factor
|
|
49
|
+
const planLength = stream.plan?.length ?? 0;
|
|
50
|
+
if (planLength > 1000) {
|
|
51
|
+
score += 3;
|
|
52
|
+
factors.push(`Long plan (${planLength} chars)`);
|
|
53
|
+
}
|
|
54
|
+
else if (planLength > 500) {
|
|
55
|
+
score += 2;
|
|
56
|
+
factors.push(`Medium plan (${planLength} chars)`);
|
|
57
|
+
}
|
|
58
|
+
else if (planLength > 200) {
|
|
59
|
+
score += 1;
|
|
60
|
+
factors.push(`Short plan (${planLength} chars)`);
|
|
61
|
+
}
|
|
62
|
+
// File count factor
|
|
63
|
+
const fileCount = stream.owns?.length ?? 0;
|
|
64
|
+
if (fileCount > 5) {
|
|
65
|
+
score += 3;
|
|
66
|
+
factors.push(`Many files (${fileCount})`);
|
|
67
|
+
}
|
|
68
|
+
else if (fileCount > 2) {
|
|
69
|
+
score += 2;
|
|
70
|
+
factors.push(`Several files (${fileCount})`);
|
|
71
|
+
}
|
|
72
|
+
else if (fileCount > 0) {
|
|
73
|
+
score += 1;
|
|
74
|
+
factors.push(`Few files (${fileCount})`);
|
|
75
|
+
}
|
|
76
|
+
// Dependency count factor
|
|
77
|
+
const depCount = stream.deps?.length ?? 0;
|
|
78
|
+
if (depCount > 3) {
|
|
79
|
+
score += 2;
|
|
80
|
+
factors.push(`Many deps (${depCount})`);
|
|
81
|
+
}
|
|
82
|
+
else if (depCount > 0) {
|
|
83
|
+
score += 1;
|
|
84
|
+
factors.push(`Some deps (${depCount})`);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
score: Math.min(score, 10),
|
|
88
|
+
factors,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// --- Exported helpers ---
|
|
92
|
+
export function extractImports(content) {
|
|
93
|
+
const imports = [];
|
|
94
|
+
// ESM: import ... from '...'
|
|
95
|
+
const esmRegex = /(?:import|export)\s+.*?from\s+['"](.+?)['"]/g;
|
|
96
|
+
let match;
|
|
97
|
+
while ((match = esmRegex.exec(content)) !== null) {
|
|
98
|
+
imports.push(match[1]);
|
|
99
|
+
}
|
|
100
|
+
// CJS: require('...')
|
|
101
|
+
const cjsRegex = /require\s*\(\s*['"](.+?)['"]\s*\)/g;
|
|
102
|
+
while ((match = cjsRegex.exec(content)) !== null) {
|
|
103
|
+
imports.push(match[1]);
|
|
104
|
+
}
|
|
105
|
+
return imports;
|
|
106
|
+
}
|
|
107
|
+
export function resolveImportPath(fromFile, importPath) {
|
|
108
|
+
if (!importPath.startsWith('.'))
|
|
109
|
+
return null; // skip node_modules
|
|
110
|
+
const dir = fromFile.substring(0, fromFile.lastIndexOf('/'));
|
|
111
|
+
// Normalize: remove .js extension, add .ts for matching
|
|
112
|
+
const normalized = importPath.replace(/\.js$/, '');
|
|
113
|
+
const parts = [...dir.split('/'), ...normalized.split('/')];
|
|
114
|
+
const resolved = [];
|
|
115
|
+
for (const part of parts) {
|
|
116
|
+
if (part === '..')
|
|
117
|
+
resolved.pop();
|
|
118
|
+
else if (part !== '.')
|
|
119
|
+
resolved.push(part);
|
|
120
|
+
}
|
|
121
|
+
const result = resolved.join('/');
|
|
122
|
+
// Try .ts extension
|
|
123
|
+
return result.endsWith('.ts') ? result : result + '.ts';
|
|
124
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Learning engine for adaptive threshold adjustment in Orchex Learn.
|
|
3
|
+
* Correlates context characteristics with execution success and adapts thresholds.
|
|
4
|
+
*/
|
|
5
|
+
import type { TelemetryEvent } from '../telemetry/telemetry-types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Stream categories for per-category learning.
|
|
8
|
+
* Canonical source of truth for stream categorization across all intelligence modules.
|
|
9
|
+
*/
|
|
10
|
+
export type StreamCategory = 'code' | 'docs' | 'tutorial' | 'integration-guide' | 'test' | 'migration' | 'api-reference' | 'other';
|
|
11
|
+
/**
|
|
12
|
+
* Confidence level for learned thresholds.
|
|
13
|
+
*/
|
|
14
|
+
export type ConfidenceLevel = 'low' | 'medium' | 'high';
|
|
15
|
+
/**
|
|
16
|
+
* Learned thresholds that adapt based on execution history.
|
|
17
|
+
*/
|
|
18
|
+
export interface LearnedThresholds {
|
|
19
|
+
/** Maximum recommended owns count per category */
|
|
20
|
+
maxOwnsCount: Record<StreamCategory, number>;
|
|
21
|
+
/** Maximum recommended reads count per category */
|
|
22
|
+
maxReadsCount: Record<StreamCategory, number>;
|
|
23
|
+
/** Maximum recommended context tokens per category */
|
|
24
|
+
maxContextTokens: Record<StreamCategory, number>;
|
|
25
|
+
/** Global soft limit derived from learning */
|
|
26
|
+
globalSoftLimit: number;
|
|
27
|
+
/** Global hard limit derived from learning */
|
|
28
|
+
globalHardLimit: number;
|
|
29
|
+
/** Number of samples used to derive these thresholds */
|
|
30
|
+
sampleCount: number;
|
|
31
|
+
/** Confidence level based on sample count */
|
|
32
|
+
confidence: ConfidenceLevel;
|
|
33
|
+
/** Last update timestamp */
|
|
34
|
+
lastUpdated: string;
|
|
35
|
+
/** Version for schema evolution */
|
|
36
|
+
version: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Correlation between a factor and execution success.
|
|
40
|
+
*/
|
|
41
|
+
export interface LearningCorrelation {
|
|
42
|
+
/** The factor being analyzed */
|
|
43
|
+
factor: string;
|
|
44
|
+
/** Success rate (0-1) for this factor */
|
|
45
|
+
successRate: number;
|
|
46
|
+
/** Number of samples */
|
|
47
|
+
sampleSize: number;
|
|
48
|
+
/** Recommended threshold value */
|
|
49
|
+
threshold: number;
|
|
50
|
+
/** Human-readable recommendation */
|
|
51
|
+
recommendation: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Result of learning analysis.
|
|
55
|
+
*/
|
|
56
|
+
export interface LearningResult {
|
|
57
|
+
/** Correlations discovered */
|
|
58
|
+
correlations: LearningCorrelation[];
|
|
59
|
+
/** Suggested threshold updates */
|
|
60
|
+
suggestedThresholds: Partial<LearnedThresholds>;
|
|
61
|
+
/** Human-readable insights */
|
|
62
|
+
insights: string[];
|
|
63
|
+
/** Whether enough data exists for meaningful learning */
|
|
64
|
+
hasEnoughData: boolean;
|
|
65
|
+
}
|
|
66
|
+
/** Default thresholds before learning */
|
|
67
|
+
export declare const DEFAULT_THRESHOLDS: LearnedThresholds;
|
|
68
|
+
/** Minimum samples required for learning */
|
|
69
|
+
export declare const MIN_SAMPLES_FOR_LEARNING = 20;
|
|
70
|
+
/** Minimum samples for high confidence */
|
|
71
|
+
export declare const HIGH_CONFIDENCE_THRESHOLD = 100;
|
|
72
|
+
/** Minimum samples for medium confidence */
|
|
73
|
+
export declare const MEDIUM_CONFIDENCE_THRESHOLD = 50;
|
|
74
|
+
/**
|
|
75
|
+
* Determine confidence level from sample count.
|
|
76
|
+
*/
|
|
77
|
+
export declare function getConfidenceLevel(sampleCount: number): ConfidenceLevel;
|
|
78
|
+
/**
|
|
79
|
+
* Categorize a stream based on its name and plan.
|
|
80
|
+
* Order matters: more specific patterns before general ones.
|
|
81
|
+
* Returns 'code' as default since most uncategorized streams are implementation work.
|
|
82
|
+
*/
|
|
83
|
+
export declare function categorizeStream(name: string, plan?: string): StreamCategory;
|
|
84
|
+
/**
|
|
85
|
+
* Correlate context characteristics with success from telemetry events.
|
|
86
|
+
*/
|
|
87
|
+
export declare function correlateContextWithSuccess(events: TelemetryEvent[], minSamples?: number): LearningResult;
|
|
88
|
+
/**
|
|
89
|
+
* Update thresholds based on learning results.
|
|
90
|
+
*/
|
|
91
|
+
export declare function updateThresholds(current: LearnedThresholds, learningResult: LearningResult, learningRate?: number): LearnedThresholds;
|
|
92
|
+
/**
|
|
93
|
+
* Save learned thresholds to project directory.
|
|
94
|
+
*/
|
|
95
|
+
export declare function saveLearnedThresholds(projectDir: string, thresholds: LearnedThresholds): Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Load learned thresholds from project directory.
|
|
98
|
+
*/
|
|
99
|
+
export declare function loadLearnedThresholds(projectDir: string): Promise<LearnedThresholds | null>;
|
|
100
|
+
/**
|
|
101
|
+
* Get thresholds for a project, falling back to defaults.
|
|
102
|
+
*/
|
|
103
|
+
export declare function getThresholds(projectDir: string): Promise<LearnedThresholds>;
|
|
104
|
+
/**
|
|
105
|
+
* Get recommended limit for a stream category.
|
|
106
|
+
*/
|
|
107
|
+
export declare function getRecommendedLimit(thresholds: LearnedThresholds, category: StreamCategory, limitType: 'owns' | 'reads' | 'tokens'): number;
|
|
108
|
+
/**
|
|
109
|
+
* Minimal stream result shape needed for learning event conversion.
|
|
110
|
+
* Avoids importing full StreamResultWithBudget to prevent circular deps.
|
|
111
|
+
*/
|
|
112
|
+
export interface LearningStreamResult {
|
|
113
|
+
id: string;
|
|
114
|
+
name: string;
|
|
115
|
+
status: string;
|
|
116
|
+
error?: string;
|
|
117
|
+
tokensUsed?: {
|
|
118
|
+
input?: number;
|
|
119
|
+
output?: number;
|
|
120
|
+
};
|
|
121
|
+
executionTimeMs?: number;
|
|
122
|
+
contextMetrics?: {
|
|
123
|
+
estimatedTokens: number;
|
|
124
|
+
budgetUtilization: number;
|
|
125
|
+
violationType: string;
|
|
126
|
+
budgetLimit: number;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Convert a stream result to a TelemetryEvent for learning.
|
|
131
|
+
*/
|
|
132
|
+
export declare function streamResultToTelemetryEvent(result: LearningStreamResult, provider?: string, model?: string): TelemetryEvent;
|
|
133
|
+
/**
|
|
134
|
+
* Append telemetry events to the local events file (.orchex/learn/events.jsonl).
|
|
135
|
+
* Uses JSONL (one JSON object per line) for append-friendly storage.
|
|
136
|
+
*/
|
|
137
|
+
export declare function appendLocalEvents(projectDir: string, events: TelemetryEvent[]): Promise<void>;
|
|
138
|
+
/**
|
|
139
|
+
* Load all local telemetry events from the JSONL file.
|
|
140
|
+
* Skips malformed lines silently.
|
|
141
|
+
*/
|
|
142
|
+
export declare function loadLocalEvents(projectDir: string): Promise<TelemetryEvent[]>;
|
|
143
|
+
/**
|
|
144
|
+
* Run a complete learning cycle: load events → correlate → update → save.
|
|
145
|
+
* Returns the learning result (or null if not enough data).
|
|
146
|
+
* This is the main entry point called after an orchestration completes.
|
|
147
|
+
*/
|
|
148
|
+
export declare function runLearningCycle(projectDir: string): Promise<LearningResult | null>;
|
|
149
|
+
/**
|
|
150
|
+
* Run learning cycle from telemetry store events (cloud mode).
|
|
151
|
+
* Aggregates across all users for global threshold learning.
|
|
152
|
+
*/
|
|
153
|
+
export declare function runCloudLearningCycle(events: TelemetryEvent[], projectDir: string): Promise<LearningResult | null>;
|
|
154
|
+
/**
|
|
155
|
+
* Format a learning report for display.
|
|
156
|
+
*/
|
|
157
|
+
export declare function formatLearningReport(result: LearningResult): string;
|