@emeryld/manager 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -0
- package/dist/create-package/cli-args.js +78 -0
- package/dist/create-package/prompts.js +138 -0
- package/dist/create-package/shared/configs.js +309 -0
- package/dist/create-package/shared/constants.js +5 -0
- package/dist/create-package/shared/fs-utils.js +69 -0
- package/dist/create-package/tasks.js +89 -0
- package/dist/create-package/types.js +1 -0
- package/dist/create-package/variant-info.js +67 -0
- package/dist/create-package/variants/client/expo-react-native/lib-files.js +168 -0
- package/dist/create-package/variants/client/expo-react-native/package-files.js +94 -0
- package/dist/create-package/variants/client/expo-react-native/scaffold.js +59 -0
- package/dist/create-package/variants/client/expo-react-native/ui-files.js +215 -0
- package/dist/create-package/variants/client/vite-react/health-page.js +251 -0
- package/dist/create-package/variants/client/vite-react/lib-files.js +176 -0
- package/dist/create-package/variants/client/vite-react/package-files.js +79 -0
- package/dist/create-package/variants/client/vite-react/scaffold.js +68 -0
- package/dist/create-package/variants/client/vite-react/ui-files.js +154 -0
- package/dist/create-package/variants/fullstack/files.js +129 -0
- package/dist/create-package/variants/fullstack/index.js +86 -0
- package/dist/create-package/variants/fullstack/utils.js +241 -0
- package/dist/llm-pack.js +2 -0
- package/dist/robot/cli/prompts.js +84 -27
- package/dist/robot/cli/settings.js +131 -56
- package/dist/robot/config.js +123 -50
- package/dist/robot/coordinator.js +10 -105
- package/dist/robot/extractors/classes.js +14 -13
- package/dist/robot/extractors/components.js +17 -10
- package/dist/robot/extractors/constants.js +9 -6
- package/dist/robot/extractors/functions.js +11 -8
- package/dist/robot/extractors/shared.js +6 -1
- package/dist/robot/extractors/types.js +5 -8
- package/dist/robot/llm-pack.js +1226 -0
- package/dist/robot/pack/builder.js +374 -0
- package/dist/robot/pack/cli.js +65 -0
- package/dist/robot/pack/exemplars.js +573 -0
- package/dist/robot/pack/globs.js +119 -0
- package/dist/robot/pack/selection.js +44 -0
- package/dist/robot/pack/symbols.js +309 -0
- package/dist/robot/pack/type-registry.js +285 -0
- package/dist/robot/pack/types.js +48 -0
- package/dist/robot/pack/utils.js +36 -0
- package/dist/robot/serializer.js +97 -0
- package/dist/robot/v2/cli.js +86 -0
- package/dist/robot/v2/globs.js +103 -0
- package/dist/robot/v2/parser/bundles.js +55 -0
- package/dist/robot/v2/parser/candidates.js +63 -0
- package/dist/robot/v2/parser/exemplars.js +114 -0
- package/dist/robot/v2/parser/exports.js +57 -0
- package/dist/robot/v2/parser/symbols.js +179 -0
- package/dist/robot/v2/parser.js +114 -0
- package/dist/robot/v2/types.js +42 -0
- package/dist/utils/export.js +39 -18
- package/package.json +2 -1
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
import { buildSymbolId, estimateTokens, normalizeRelativePath } from './utils.js';
|
|
4
|
+
const MAX_CHUNK_TOKENS = 300;
|
|
5
|
+
const CALL_SITE_LIMIT = 2;
|
|
6
|
+
const BUCKET_KEYWORDS = {
|
|
7
|
+
orchestration: ['pipeline', 'orchestrate', 'flow', 'step', 'sequence', 'dispatch', 'run', 'handle', 'context'],
|
|
8
|
+
configuration: ['config', 'setting', 'settings', 'normalize', 'defaults', 'option', 'resolve', 'merge', 'env', 'pref'],
|
|
9
|
+
parsing: ['parse', 'validation', 'validate', 'schema', 'input', 'payload', 'deserialize', 'decode', 'token', 'assert'],
|
|
10
|
+
selection: ['filter', 'select', 'choose', 'pick', 'match', 'find', 'sort', 'candidate', 'decide', 'rank'],
|
|
11
|
+
core: ['compute', 'calculate', 'algorithm', 'strategy', 'engine', 'transform', 'score', 'aggregate', 'map', 'reduce'],
|
|
12
|
+
io: ['fs', 'readfile', 'writefile', 'console', 'log', 'stdout', 'stderr', 'process', 'argv', 'cli', 'command', 'output', 'serialize', 'deserialize', 'fetch', 'http', 'request', 'response', 'file', 'stream'],
|
|
13
|
+
};
|
|
14
|
+
const BUCKET_LABELS = {
|
|
15
|
+
orchestration: 'orchestration / pipeline',
|
|
16
|
+
configuration: 'configuration / normalization',
|
|
17
|
+
parsing: 'parsing / validation',
|
|
18
|
+
selection: 'selection / filtering',
|
|
19
|
+
core: 'core domain algorithm',
|
|
20
|
+
io: 'I/O boundary',
|
|
21
|
+
};
|
|
22
|
+
const BOUNDARY_KEYWORDS = [
|
|
23
|
+
'config',
|
|
24
|
+
'setting',
|
|
25
|
+
'cli',
|
|
26
|
+
'command',
|
|
27
|
+
'option',
|
|
28
|
+
'env',
|
|
29
|
+
'fs',
|
|
30
|
+
'read',
|
|
31
|
+
'write',
|
|
32
|
+
'console',
|
|
33
|
+
'output',
|
|
34
|
+
'serialize',
|
|
35
|
+
'deserialize',
|
|
36
|
+
'file',
|
|
37
|
+
'http',
|
|
38
|
+
'request',
|
|
39
|
+
'response',
|
|
40
|
+
'stream',
|
|
41
|
+
];
|
|
42
|
+
export function buildExemplarCandidates(symbols, candidates, settings, checker) {
|
|
43
|
+
const recordBySymbol = new Map();
|
|
44
|
+
const recordById = new Map();
|
|
45
|
+
const declarationOwners = new Map();
|
|
46
|
+
for (const record of symbols.values()) {
|
|
47
|
+
const identifier = buildSymbolId(record.symbol, record.declaration);
|
|
48
|
+
recordBySymbol.set(record.symbol, record);
|
|
49
|
+
recordById.set(identifier, record);
|
|
50
|
+
declarationOwners.set(record.declaration, record);
|
|
51
|
+
}
|
|
52
|
+
const { callSites, callGraph } = collectCallSiteData(candidates, declarationOwners, recordBySymbol, checker, settings.rootDir);
|
|
53
|
+
const flowPath = buildFlowPath(recordById, callGraph);
|
|
54
|
+
const flowSymbolIds = new Set(flowPath?.map((entry) => buildSymbolId(entry.symbol, entry.declaration)) ?? []);
|
|
55
|
+
const chunks = [];
|
|
56
|
+
if (flowPath && flowPath.length >= 3) {
|
|
57
|
+
const flowChunk = createFlowChunk(flowPath, settings.exemplarHeuristics);
|
|
58
|
+
if (flowChunk)
|
|
59
|
+
chunks.push(flowChunk);
|
|
60
|
+
}
|
|
61
|
+
for (const record of symbols.values()) {
|
|
62
|
+
const recordChunks = buildChunksForRecord(record, callSites, flowSymbolIds, settings.exemplarHeuristics);
|
|
63
|
+
chunks.push(...recordChunks);
|
|
64
|
+
}
|
|
65
|
+
return chunks.sort((a, b) => b.finalScore - a.finalScore);
|
|
66
|
+
}
|
|
67
|
+
export function selectExemplars(candidates, settings) {
|
|
68
|
+
const heuristics = settings.exemplarHeuristics;
|
|
69
|
+
const maxTokens = settings.tokenBudget ?? Number.POSITIVE_INFINITY;
|
|
70
|
+
const maxExemplars = settings.maxExemplars;
|
|
71
|
+
const highPriorityBuckets = ['orchestration', 'configuration', 'core', 'io'];
|
|
72
|
+
const bucketCounts = new Map();
|
|
73
|
+
const bucketCoverage = new Set();
|
|
74
|
+
const selectedSymbols = new Set();
|
|
75
|
+
const selected = [];
|
|
76
|
+
let usedTokens = 0;
|
|
77
|
+
const pool = [...candidates];
|
|
78
|
+
const flowIndex = pool.findIndex((chunk) => chunk.isFlow);
|
|
79
|
+
if (flowIndex >= 0) {
|
|
80
|
+
const [flowChunk] = pool.splice(flowIndex, 1);
|
|
81
|
+
if (tryAddChunk(flowChunk)) {
|
|
82
|
+
bucketCoverage.add('orchestration');
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
pool.push(flowChunk);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const bucket of highPriorityBuckets) {
|
|
89
|
+
if (bucket === 'orchestration' && bucketCoverage.has('orchestration'))
|
|
90
|
+
continue;
|
|
91
|
+
if (selected.length >= maxExemplars)
|
|
92
|
+
break;
|
|
93
|
+
const candidate = pool.find((chunk) => chunk.purposeBuckets.includes(bucket) && canAddChunk(chunk));
|
|
94
|
+
if (candidate) {
|
|
95
|
+
if (tryAddChunk(candidate)) {
|
|
96
|
+
bucketCoverage.add(bucket);
|
|
97
|
+
const index = pool.indexOf(candidate);
|
|
98
|
+
if (index >= 0)
|
|
99
|
+
pool.splice(index, 1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const candidate of [...pool]) {
|
|
104
|
+
if (selected.length >= maxExemplars)
|
|
105
|
+
break;
|
|
106
|
+
if (!canAddChunk(candidate))
|
|
107
|
+
continue;
|
|
108
|
+
const penalty = calcRedundancyPenalty(candidate, selectedSymbols, heuristics);
|
|
109
|
+
if (penalty <= 0.1)
|
|
110
|
+
continue;
|
|
111
|
+
if (!tryAddChunk(candidate))
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
return selected.map((chunk) => ({
|
|
115
|
+
name: chunk.name,
|
|
116
|
+
kind: chunk.kind,
|
|
117
|
+
modulePath: chunk.modulePath,
|
|
118
|
+
score: chunk.finalScore,
|
|
119
|
+
body: chunk.body,
|
|
120
|
+
tokenCount: chunk.tokenCount,
|
|
121
|
+
purposeBuckets: compactPurposeBuckets(chunk.purposeBuckets),
|
|
122
|
+
metadata: {
|
|
123
|
+
purpose: chunk.metadata.purpose,
|
|
124
|
+
whenRelevant: chunk.metadata.whenRelevant,
|
|
125
|
+
},
|
|
126
|
+
}));
|
|
127
|
+
function canAddChunk(chunk) {
|
|
128
|
+
if (selected.length >= maxExemplars)
|
|
129
|
+
return false;
|
|
130
|
+
if (usedTokens + chunk.tokenCount > maxTokens)
|
|
131
|
+
return false;
|
|
132
|
+
const bucket = getPrimaryBucket(chunk);
|
|
133
|
+
if ((bucketCounts.get(bucket) ?? 0) >= 2)
|
|
134
|
+
return false;
|
|
135
|
+
return chunk.finalScore > 0;
|
|
136
|
+
}
|
|
137
|
+
function tryAddChunk(chunk) {
|
|
138
|
+
if (!canAddChunk(chunk))
|
|
139
|
+
return false;
|
|
140
|
+
selected.push(chunk);
|
|
141
|
+
usedTokens += chunk.tokenCount;
|
|
142
|
+
const bucket = getPrimaryBucket(chunk);
|
|
143
|
+
bucketCounts.set(bucket, (bucketCounts.get(bucket) ?? 0) + 1);
|
|
144
|
+
for (const purpose of chunk.purposeBuckets) {
|
|
145
|
+
if (highPriorityBuckets.includes(purpose)) {
|
|
146
|
+
bucketCoverage.add(purpose);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const symbol of chunk.symbolsShown) {
|
|
150
|
+
selectedSymbols.add(symbol);
|
|
151
|
+
}
|
|
152
|
+
const poolIndex = pool.indexOf(chunk);
|
|
153
|
+
if (poolIndex >= 0) {
|
|
154
|
+
pool.splice(poolIndex, 1);
|
|
155
|
+
}
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
function getPrimaryBucket(chunk) {
|
|
159
|
+
return chunk.purposeBuckets[0] ?? 'core';
|
|
160
|
+
}
|
|
161
|
+
function calcRedundancyPenalty(chunk, selectedSymbols, heuristics) {
|
|
162
|
+
if (!selectedSymbols.size)
|
|
163
|
+
return 1;
|
|
164
|
+
const overlap = chunk.symbolsShown.filter((symbol) => selectedSymbols.has(symbol)).length;
|
|
165
|
+
if (!overlap)
|
|
166
|
+
return 1;
|
|
167
|
+
const ratio = overlap / Math.max(1, chunk.symbolsShown.length);
|
|
168
|
+
const penalty = 1 - heuristics.redundancy * ratio;
|
|
169
|
+
return Math.max(0.1, penalty);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function collectCallSiteData(candidates, declarationOwners, exportedSymbolBySymbol, checker, rootDir) {
|
|
173
|
+
const callSites = new Map();
|
|
174
|
+
const callGraph = new Map();
|
|
175
|
+
for (const candidate of candidates) {
|
|
176
|
+
const relativePath = normalizeRelativePath(path.relative(rootDir, candidate.absolutePath));
|
|
177
|
+
visit(candidate.sourceFile, undefined, relativePath);
|
|
178
|
+
}
|
|
179
|
+
return { callSites, callGraph };
|
|
180
|
+
function visit(node, owner, modulePath) {
|
|
181
|
+
const nextOwner = declarationOwners.get(node) ?? owner;
|
|
182
|
+
if (ts.isCallExpression(node)) {
|
|
183
|
+
recordCall(node, nextOwner, modulePath);
|
|
184
|
+
}
|
|
185
|
+
ts.forEachChild(node, (child) => visit(child, nextOwner, modulePath));
|
|
186
|
+
}
|
|
187
|
+
function recordCall(node, owner, modulePath) {
|
|
188
|
+
const symbol = checker.getSymbolAtLocation(node.expression);
|
|
189
|
+
if (!symbol)
|
|
190
|
+
return;
|
|
191
|
+
const resolved = (symbol.flags & ts.SymbolFlags.Alias) ? checker.getAliasedSymbol(symbol) : symbol;
|
|
192
|
+
if (!resolved)
|
|
193
|
+
return;
|
|
194
|
+
const target = exportedSymbolBySymbol.get(resolved);
|
|
195
|
+
if (!target)
|
|
196
|
+
return;
|
|
197
|
+
const calleeId = buildSymbolId(target.symbol, target.declaration);
|
|
198
|
+
const callerId = owner ? buildSymbolId(owner.symbol, owner.declaration) : undefined;
|
|
199
|
+
if (owner && callerId && callerId !== calleeId) {
|
|
200
|
+
const edges = callGraph.get(callerId) ?? new Set();
|
|
201
|
+
edges.add(calleeId);
|
|
202
|
+
callGraph.set(callerId, edges);
|
|
203
|
+
}
|
|
204
|
+
const anchor = findStatementAncestor(node) ?? node;
|
|
205
|
+
const statementText = limitSnippet(normalizeSnippet(anchor.getText()));
|
|
206
|
+
const argumentPreview = buildArgumentPreview(node.arguments);
|
|
207
|
+
const hasGuard = hasGuardingAncestor(node);
|
|
208
|
+
const mentionsError = /error|throw|fail/i.test(statementText);
|
|
209
|
+
const info = {
|
|
210
|
+
callerId,
|
|
211
|
+
callerName: owner ? owner.summary.name : `module:${modulePath}`,
|
|
212
|
+
sourcePath: modulePath,
|
|
213
|
+
statementText,
|
|
214
|
+
argumentPreview,
|
|
215
|
+
hasGuard,
|
|
216
|
+
mentionsError,
|
|
217
|
+
priority: calcCallSitePriority(hasGuard, mentionsError, argumentPreview, node.arguments.length),
|
|
218
|
+
};
|
|
219
|
+
const bucket = callSites.get(calleeId) ?? [];
|
|
220
|
+
bucket.push(info);
|
|
221
|
+
callSites.set(calleeId, bucket);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function calcCallSitePriority(hasGuard, mentionsError, argumentPreview, argumentCount) {
|
|
225
|
+
let score = hasGuard ? 1 : 0;
|
|
226
|
+
if (mentionsError)
|
|
227
|
+
score += 0.7;
|
|
228
|
+
if (argumentCount > 0)
|
|
229
|
+
score += 0.4;
|
|
230
|
+
if (argumentPreview)
|
|
231
|
+
score += Math.min(0.3, argumentPreview.length / 80);
|
|
232
|
+
return score;
|
|
233
|
+
}
|
|
234
|
+
function buildFlowPath(recordById, callGraph) {
|
|
235
|
+
let best;
|
|
236
|
+
for (const [id, record] of recordById.entries()) {
|
|
237
|
+
if (!record.entrypoints || record.entrypoints.size === 0)
|
|
238
|
+
continue;
|
|
239
|
+
const path = walkFlow(record, new Set([id]), recordById, callGraph, 0);
|
|
240
|
+
if (path.length >= 3 && (!best || path.length > best.length)) {
|
|
241
|
+
best = path;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return best?.slice(0, 6);
|
|
245
|
+
}
|
|
246
|
+
function walkFlow(record, visited, recordById, callGraph, depth) {
|
|
247
|
+
const currentId = buildSymbolId(record.symbol, record.declaration);
|
|
248
|
+
if (depth >= 5)
|
|
249
|
+
return [record];
|
|
250
|
+
const neighbors = callGraph.get(currentId);
|
|
251
|
+
let best;
|
|
252
|
+
if (neighbors) {
|
|
253
|
+
for (const neighborId of neighbors) {
|
|
254
|
+
if (visited.has(neighborId))
|
|
255
|
+
continue;
|
|
256
|
+
const neighbor = recordById.get(neighborId);
|
|
257
|
+
if (!neighbor)
|
|
258
|
+
continue;
|
|
259
|
+
visited.add(neighborId);
|
|
260
|
+
const path = walkFlow(neighbor, visited, recordById, callGraph, depth + 1);
|
|
261
|
+
visited.delete(neighborId);
|
|
262
|
+
if (path.length && (!best || path.length > best.length)) {
|
|
263
|
+
best = path;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (best && best.length) {
|
|
268
|
+
return [record, ...best];
|
|
269
|
+
}
|
|
270
|
+
return [record];
|
|
271
|
+
}
|
|
272
|
+
function createFlowChunk(path, heuristics) {
|
|
273
|
+
if (!path.length)
|
|
274
|
+
return undefined;
|
|
275
|
+
const stepBudget = Math.max(20, Math.floor((MAX_CHUNK_TOKENS - path.length * 5) / path.length));
|
|
276
|
+
const snippets = path.map((record, index) => {
|
|
277
|
+
const snippet = limitSnippet(normalizeSnippet(record.declaration.getText()), stepBudget);
|
|
278
|
+
return `// Step ${index + 1}: ${record.summary.name}\n${snippet}`;
|
|
279
|
+
});
|
|
280
|
+
const body = limitSnippet(snippets.join('\n\n'), MAX_CHUNK_TOKENS);
|
|
281
|
+
if (!body)
|
|
282
|
+
return undefined;
|
|
283
|
+
const entryName = path[0].summary.name;
|
|
284
|
+
const exitName = path[path.length - 1].summary.name;
|
|
285
|
+
const symbols = path.map((record) => record.summary.name);
|
|
286
|
+
const metadata = buildChunkMetadata(path[0], 'flow', ['orchestration'], { entrypoint: entryName, exit: exitName, symbols });
|
|
287
|
+
const chunkBase = {
|
|
288
|
+
id: `flow:${symbols.join('→')}`,
|
|
289
|
+
name: `${entryName} flow`,
|
|
290
|
+
kind: path[0].summary.kind,
|
|
291
|
+
modulePath: path[0].summary.sourcePath,
|
|
292
|
+
chunkType: 'flow',
|
|
293
|
+
body,
|
|
294
|
+
tokenCount: Math.max(1, estimateTokens(body)),
|
|
295
|
+
purposeBuckets: ['orchestration'],
|
|
296
|
+
metadata,
|
|
297
|
+
usageScore: 2.2,
|
|
298
|
+
boundaryScore: 0.7,
|
|
299
|
+
flowScore: 1,
|
|
300
|
+
clarityScore: 0.8,
|
|
301
|
+
symbolsShown: symbols,
|
|
302
|
+
isFlow: true,
|
|
303
|
+
};
|
|
304
|
+
return createChunkCandidate(chunkBase, heuristics);
|
|
305
|
+
}
|
|
306
|
+
function buildChunksForRecord(record, callSites, flowSymbolIds, heuristics) {
|
|
307
|
+
const recordId = buildSymbolId(record.symbol, record.declaration);
|
|
308
|
+
const modulePath = record.summary.sourcePath;
|
|
309
|
+
const symbolCallSites = callSites.get(recordId) ?? [];
|
|
310
|
+
const hasEntrypoint = record.entrypoints.size > 0;
|
|
311
|
+
const chunks = [];
|
|
312
|
+
const signatureBody = limitSnippet(normalizeSnippet(record.declaration.getText()));
|
|
313
|
+
if (signatureBody) {
|
|
314
|
+
const purposeBuckets = classifyPurpose(signatureBody);
|
|
315
|
+
const metadata = buildChunkMetadata(record, 'signature', purposeBuckets, {});
|
|
316
|
+
const chunkBase = {
|
|
317
|
+
id: `${recordId}:signature`,
|
|
318
|
+
name: record.summary.name,
|
|
319
|
+
kind: record.summary.kind,
|
|
320
|
+
modulePath,
|
|
321
|
+
chunkType: 'signature',
|
|
322
|
+
body: signatureBody,
|
|
323
|
+
tokenCount: estimateTokens(signatureBody),
|
|
324
|
+
purposeBuckets,
|
|
325
|
+
metadata,
|
|
326
|
+
usageScore: calcUsageScore(symbolCallSites.length, 'signature', 0, hasEntrypoint),
|
|
327
|
+
boundaryScore: calcBoundaryScore(signatureBody),
|
|
328
|
+
flowScore: flowSymbolIds.has(recordId) ? 1 : 0,
|
|
329
|
+
clarityScore: calcClarityScore(signatureBody, record, 'signature'),
|
|
330
|
+
symbolsShown: [record.summary.name],
|
|
331
|
+
recordId,
|
|
332
|
+
};
|
|
333
|
+
chunks.push(createChunkCandidate(chunkBase, heuristics));
|
|
334
|
+
}
|
|
335
|
+
const decisionNode = findDecisionNode(record.declaration);
|
|
336
|
+
if (decisionNode) {
|
|
337
|
+
const decisionBody = limitSnippet(normalizeSnippet(decisionNode.getText()));
|
|
338
|
+
if (decisionBody) {
|
|
339
|
+
const purposeBuckets = classifyPurpose(decisionBody);
|
|
340
|
+
const metadata = buildChunkMetadata(record, 'decision', purposeBuckets, {});
|
|
341
|
+
const chunkBase = {
|
|
342
|
+
id: `${recordId}:decision`,
|
|
343
|
+
name: record.summary.name,
|
|
344
|
+
kind: record.summary.kind,
|
|
345
|
+
modulePath,
|
|
346
|
+
chunkType: 'decision',
|
|
347
|
+
body: decisionBody,
|
|
348
|
+
tokenCount: estimateTokens(decisionBody),
|
|
349
|
+
purposeBuckets,
|
|
350
|
+
metadata,
|
|
351
|
+
usageScore: calcUsageScore(symbolCallSites.length, 'decision', 0, hasEntrypoint),
|
|
352
|
+
boundaryScore: calcBoundaryScore(decisionBody),
|
|
353
|
+
flowScore: flowSymbolIds.has(recordId) ? 1 : 0,
|
|
354
|
+
clarityScore: calcClarityScore(decisionBody, record, 'decision'),
|
|
355
|
+
symbolsShown: [record.summary.name],
|
|
356
|
+
recordId,
|
|
357
|
+
};
|
|
358
|
+
chunks.push(createChunkCandidate(chunkBase, heuristics));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (symbolCallSites.length) {
|
|
362
|
+
const topSites = [...symbolCallSites]
|
|
363
|
+
.sort((a, b) => b.priority - a.priority)
|
|
364
|
+
.slice(0, CALL_SITE_LIMIT);
|
|
365
|
+
for (const site of topSites) {
|
|
366
|
+
const snippet = limitSnippet(site.statementText);
|
|
367
|
+
if (!snippet)
|
|
368
|
+
continue;
|
|
369
|
+
const purposeBuckets = classifyPurpose(snippet);
|
|
370
|
+
const contextSymbols = [record.summary.name];
|
|
371
|
+
if (site.callerName)
|
|
372
|
+
contextSymbols.push(site.callerName);
|
|
373
|
+
const metadata = buildChunkMetadata(record, 'usage', purposeBuckets, {
|
|
374
|
+
callerName: site.callerName,
|
|
375
|
+
argumentPreview: site.argumentPreview,
|
|
376
|
+
symbols: contextSymbols,
|
|
377
|
+
});
|
|
378
|
+
const uniqueSymbols = Array.from(new Set(contextSymbols));
|
|
379
|
+
const chunkBase = {
|
|
380
|
+
id: `${recordId}:usage:${site.sourcePath}:${snippet.slice(0, 16)}`,
|
|
381
|
+
name: record.summary.name,
|
|
382
|
+
kind: record.summary.kind,
|
|
383
|
+
modulePath: site.sourcePath,
|
|
384
|
+
chunkType: 'usage',
|
|
385
|
+
body: snippet,
|
|
386
|
+
tokenCount: estimateTokens(snippet),
|
|
387
|
+
purposeBuckets,
|
|
388
|
+
metadata,
|
|
389
|
+
usageScore: calcUsageScore(symbolCallSites.length, 'usage', site.priority, hasEntrypoint),
|
|
390
|
+
boundaryScore: calcBoundaryScore(snippet),
|
|
391
|
+
flowScore: site.callerId && flowSymbolIds.has(site.callerId) ? 1 : 0,
|
|
392
|
+
clarityScore: calcClarityScore(snippet, record, 'usage'),
|
|
393
|
+
symbolsShown: uniqueSymbols,
|
|
394
|
+
recordId,
|
|
395
|
+
};
|
|
396
|
+
chunks.push(createChunkCandidate(chunkBase, heuristics));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return chunks;
|
|
400
|
+
}
|
|
401
|
+
function createChunkCandidate(options, heuristics) {
|
|
402
|
+
const chunk = {
|
|
403
|
+
...options,
|
|
404
|
+
finalScore: 0,
|
|
405
|
+
};
|
|
406
|
+
chunk.finalScore = scoreChunk(chunk, heuristics);
|
|
407
|
+
return chunk;
|
|
408
|
+
}
|
|
409
|
+
function scoreChunk(chunk, heuristics) {
|
|
410
|
+
const baseScore = chunk.usageScore * heuristics.usage +
|
|
411
|
+
chunk.boundaryScore * heuristics.boundary +
|
|
412
|
+
chunk.flowScore * heuristics.flow +
|
|
413
|
+
chunk.clarityScore * heuristics.clarity;
|
|
414
|
+
const penalty = 1 + chunk.tokenCount / heuristics.tau;
|
|
415
|
+
return baseScore / penalty;
|
|
416
|
+
}
|
|
417
|
+
function classifyPurpose(body) {
|
|
418
|
+
const normalized = body.toLowerCase();
|
|
419
|
+
const buckets = [];
|
|
420
|
+
for (const bucket of Object.keys(BUCKET_KEYWORDS)) {
|
|
421
|
+
const keywords = BUCKET_KEYWORDS[bucket];
|
|
422
|
+
if (keywords.some((keyword) => normalized.includes(keyword))) {
|
|
423
|
+
buckets.push(bucket);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (!buckets.length)
|
|
427
|
+
buckets.push('core');
|
|
428
|
+
return buckets;
|
|
429
|
+
}
|
|
430
|
+
function describePurpose(buckets) {
|
|
431
|
+
const unique = Array.from(new Set(buckets));
|
|
432
|
+
return unique.map((bucket) => BUCKET_LABELS[bucket]).join(' · ');
|
|
433
|
+
}
|
|
434
|
+
function buildChunkMetadata(record, chunkType, purposeBuckets, context) {
|
|
435
|
+
const purpose = describePurpose(purposeBuckets);
|
|
436
|
+
const whenRelevant = buildWhenRelevant(record, chunkType, purpose, context);
|
|
437
|
+
const symbolSet = new Set(context.symbols ?? []);
|
|
438
|
+
symbolSet.add(record.summary.name);
|
|
439
|
+
if (context.callerName)
|
|
440
|
+
symbolSet.add(context.callerName);
|
|
441
|
+
return {
|
|
442
|
+
purpose,
|
|
443
|
+
whenRelevant,
|
|
444
|
+
symbolsShown: Array.from(symbolSet),
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function buildWhenRelevant(record, chunkType, purposeLabel, context) {
|
|
448
|
+
switch (chunkType) {
|
|
449
|
+
case 'signature':
|
|
450
|
+
return `When you need to recall how ${record.summary.name} is declared for ${purposeLabel}.`;
|
|
451
|
+
case 'decision':
|
|
452
|
+
return `When you want to inspect the ${purposeLabel} guard or validation inside ${record.summary.name}.`;
|
|
453
|
+
case 'usage': {
|
|
454
|
+
const caller = context.callerName ?? 'an upstream module';
|
|
455
|
+
const args = context.argumentPreview ? ` with ${context.argumentPreview}` : '';
|
|
456
|
+
return `When you need to see how ${record.summary.name} is invoked by ${caller}${args}.`;
|
|
457
|
+
}
|
|
458
|
+
case 'flow':
|
|
459
|
+
return `When you need a compact ${purposeLabel} tour from ${context.entrypoint ?? 'entrypoint'} → ${context.exit ?? 'output'}.`;
|
|
460
|
+
default:
|
|
461
|
+
return `When you need to understand ${record.summary.name} for ${purposeLabel}.`;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function calcUsageScore(callSiteCount, chunkType, callSitePriority, hasEntrypoint) {
|
|
465
|
+
let score = Math.min(1, callSiteCount * 0.2);
|
|
466
|
+
if (chunkType === 'usage')
|
|
467
|
+
score += 1;
|
|
468
|
+
score += callSitePriority ?? 0;
|
|
469
|
+
if (hasEntrypoint)
|
|
470
|
+
score += 0.3;
|
|
471
|
+
return Math.min(3, score);
|
|
472
|
+
}
|
|
473
|
+
function calcBoundaryScore(body) {
|
|
474
|
+
const snapshot = body.toLowerCase();
|
|
475
|
+
return BOUNDARY_KEYWORDS.some((keyword) => snapshot.includes(keyword)) ? 1 : 0;
|
|
476
|
+
}
|
|
477
|
+
function calcClarityScore(body, record, chunkType) {
|
|
478
|
+
const docBonus = Math.min(1, record.summary.docTags.length * 0.25);
|
|
479
|
+
const errorBonus = /error|throw|fail|warn/i.test(body) ? 0.25 : 0;
|
|
480
|
+
const usageBonus = chunkType === 'usage' ? 0.1 : 0;
|
|
481
|
+
return Math.min(1, docBonus + errorBonus + usageBonus);
|
|
482
|
+
}
|
|
483
|
+
function normalizeSnippet(text) {
|
|
484
|
+
return text
|
|
485
|
+
.replace(/\r?\n/g, ' ')
|
|
486
|
+
.replace(/\s+/g, ' ')
|
|
487
|
+
.trim();
|
|
488
|
+
}
|
|
489
|
+
function limitSnippet(text, maxTokens = MAX_CHUNK_TOKENS) {
|
|
490
|
+
if (!text)
|
|
491
|
+
return '';
|
|
492
|
+
if (estimateTokens(text) <= maxTokens)
|
|
493
|
+
return text;
|
|
494
|
+
const maxChars = Math.floor(maxTokens * 4);
|
|
495
|
+
return `${text.slice(0, maxChars).trimEnd()}…`;
|
|
496
|
+
}
|
|
497
|
+
function truncateText(text, maxLength) {
|
|
498
|
+
if (text.length <= maxLength)
|
|
499
|
+
return text;
|
|
500
|
+
return `${text.slice(0, maxLength).trimEnd()}…`;
|
|
501
|
+
}
|
|
502
|
+
function compactPurposeBuckets(buckets) {
|
|
503
|
+
const seen = new Set();
|
|
504
|
+
const compacted = [];
|
|
505
|
+
for (const bucket of buckets) {
|
|
506
|
+
if (seen.has(bucket))
|
|
507
|
+
continue;
|
|
508
|
+
seen.add(bucket);
|
|
509
|
+
compacted.push(bucket);
|
|
510
|
+
if (compacted.length >= 3)
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
return compacted;
|
|
514
|
+
}
|
|
515
|
+
function findDecisionNode(node) {
|
|
516
|
+
let found;
|
|
517
|
+
ts.forEachChild(node, (child) => {
|
|
518
|
+
if (found)
|
|
519
|
+
return;
|
|
520
|
+
if (isDecisionNode(child)) {
|
|
521
|
+
found = child;
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const nested = findDecisionNode(child);
|
|
525
|
+
if (nested)
|
|
526
|
+
found = nested;
|
|
527
|
+
});
|
|
528
|
+
return found;
|
|
529
|
+
}
|
|
530
|
+
function isDecisionNode(node) {
|
|
531
|
+
return (ts.isIfStatement(node) ||
|
|
532
|
+
ts.isSwitchStatement(node) ||
|
|
533
|
+
ts.isTryStatement(node) ||
|
|
534
|
+
ts.isCatchClause(node) ||
|
|
535
|
+
ts.isForStatement(node) ||
|
|
536
|
+
ts.isForOfStatement(node) ||
|
|
537
|
+
ts.isForInStatement(node) ||
|
|
538
|
+
ts.isWhileStatement(node) ||
|
|
539
|
+
ts.isDoStatement(node) ||
|
|
540
|
+
ts.isConditionalExpression(node) ||
|
|
541
|
+
ts.isThrowStatement(node));
|
|
542
|
+
}
|
|
543
|
+
function findStatementAncestor(node) {
|
|
544
|
+
let current = node.parent;
|
|
545
|
+
while (current) {
|
|
546
|
+
if (ts.isStatement(current))
|
|
547
|
+
return current;
|
|
548
|
+
current = current.parent;
|
|
549
|
+
}
|
|
550
|
+
return undefined;
|
|
551
|
+
}
|
|
552
|
+
function hasGuardingAncestor(node) {
|
|
553
|
+
let current = node.parent;
|
|
554
|
+
while (current) {
|
|
555
|
+
if (ts.isIfStatement(current) ||
|
|
556
|
+
ts.isTryStatement(current) ||
|
|
557
|
+
ts.isCatchClause(current) ||
|
|
558
|
+
ts.isConditionalExpression(current)) {
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
current = current.parent;
|
|
562
|
+
}
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
function buildArgumentPreview(argumentsArray) {
|
|
566
|
+
if (!argumentsArray.length)
|
|
567
|
+
return '';
|
|
568
|
+
const preview = argumentsArray
|
|
569
|
+
.map((arg) => normalizeSnippet(arg.getText()))
|
|
570
|
+
.filter(Boolean)
|
|
571
|
+
.join(', ');
|
|
572
|
+
return truncateText(preview, 60);
|
|
573
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// globs.ts
|
|
2
|
+
const ESCAPE_REGEX = /[.+^${}()|[\]\\]/g;
|
|
3
|
+
function escapeLiteral(value) {
|
|
4
|
+
return value.replace(ESCAPE_REGEX, '\\$&');
|
|
5
|
+
}
|
|
6
|
+
function normalizePatternInput(value) {
|
|
7
|
+
let normalized = value.replace(/\\/g, '/').trim();
|
|
8
|
+
if (normalized.startsWith('./'))
|
|
9
|
+
normalized = normalized.slice(2);
|
|
10
|
+
while (normalized.startsWith('/'))
|
|
11
|
+
normalized = normalized.slice(1);
|
|
12
|
+
while (normalized.endsWith('/') && normalized.length > 1)
|
|
13
|
+
normalized = normalized.slice(0, -1);
|
|
14
|
+
return normalized;
|
|
15
|
+
}
|
|
16
|
+
function normalizeTargetPath(value) {
|
|
17
|
+
let normalized = value.replace(/\\/g, '/');
|
|
18
|
+
if (normalized.startsWith('./'))
|
|
19
|
+
normalized = normalized.slice(2);
|
|
20
|
+
while (normalized.startsWith('/'))
|
|
21
|
+
normalized = normalized.slice(1);
|
|
22
|
+
while (normalized.endsWith('/') && normalized.length > 1)
|
|
23
|
+
normalized = normalized.slice(0, -1);
|
|
24
|
+
return normalized;
|
|
25
|
+
}
|
|
26
|
+
// --- brace expansion: a{b,c}d -> [abd, acd]
|
|
27
|
+
function splitAlternatives(value) {
|
|
28
|
+
const parts = [];
|
|
29
|
+
let depth = 0;
|
|
30
|
+
let start = 0;
|
|
31
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
32
|
+
const ch = value[i];
|
|
33
|
+
if (ch === '{')
|
|
34
|
+
depth += 1;
|
|
35
|
+
else if (ch === '}')
|
|
36
|
+
depth = Math.max(0, depth - 1);
|
|
37
|
+
else if (ch === ',' && depth === 0) {
|
|
38
|
+
parts.push(value.slice(start, i));
|
|
39
|
+
start = i + 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
parts.push(value.slice(start));
|
|
43
|
+
return parts.map((p) => p.trim()).filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
function expandBraces(pattern) {
|
|
46
|
+
const start = pattern.indexOf('{');
|
|
47
|
+
if (start === -1)
|
|
48
|
+
return [pattern];
|
|
49
|
+
let depth = 0;
|
|
50
|
+
for (let i = start; i < pattern.length; i += 1) {
|
|
51
|
+
const ch = pattern[i];
|
|
52
|
+
if (ch === '{')
|
|
53
|
+
depth += 1;
|
|
54
|
+
else if (ch === '}') {
|
|
55
|
+
depth -= 1;
|
|
56
|
+
if (depth === 0) {
|
|
57
|
+
const inner = pattern.slice(start + 1, i);
|
|
58
|
+
const alts = splitAlternatives(inner);
|
|
59
|
+
const prefix = pattern.slice(0, start);
|
|
60
|
+
const suffix = pattern.slice(i + 1);
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const alt of alts) {
|
|
63
|
+
out.push(...expandBraces(`${prefix}${alt}${suffix}`));
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return [pattern]; // unbalanced braces: treat literally
|
|
70
|
+
}
|
|
71
|
+
function globToRegExp(pattern) {
|
|
72
|
+
let regex = '';
|
|
73
|
+
let index = 0;
|
|
74
|
+
while (index < pattern.length) {
|
|
75
|
+
const char = pattern[index];
|
|
76
|
+
if (char === '*') {
|
|
77
|
+
const next = pattern[index + 1];
|
|
78
|
+
if (next === '*') {
|
|
79
|
+
regex += '.*';
|
|
80
|
+
index += 2;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
regex += '[^/]*';
|
|
84
|
+
index += 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (char === '?') {
|
|
88
|
+
regex += '[^/]';
|
|
89
|
+
index += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (char === '/') {
|
|
93
|
+
regex += '/';
|
|
94
|
+
index += 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
regex += escapeLiteral(char);
|
|
98
|
+
index += 1;
|
|
99
|
+
}
|
|
100
|
+
return regex;
|
|
101
|
+
}
|
|
102
|
+
export function createGlobMatchers(patterns) {
|
|
103
|
+
const matchers = [];
|
|
104
|
+
for (const raw of patterns) {
|
|
105
|
+
const normalized = normalizePatternInput(raw);
|
|
106
|
+
if (!normalized)
|
|
107
|
+
continue;
|
|
108
|
+
for (const expanded of expandBraces(normalized)) {
|
|
109
|
+
matchers.push(new RegExp(`^${globToRegExp(expanded)}$`));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return matchers;
|
|
113
|
+
}
|
|
114
|
+
export function matchesAnyGlob(value, matchers) {
|
|
115
|
+
if (!matchers.length)
|
|
116
|
+
return true;
|
|
117
|
+
const normalized = normalizeTargetPath(value);
|
|
118
|
+
return matchers.some((matcher) => matcher.test(normalized));
|
|
119
|
+
}
|