ziprin-context-optimizer 8.0.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 +48 -0
- package/bin/ziprin-context-mcp.js +34 -0
- package/bin/ziprin-context-setup.js +10 -0
- package/install-source.json +5 -0
- package/media/context-activity.svg +7 -0
- package/package.json +154 -0
- package/releases/ziprin-context-optimizer-8.0.0.vsix +0 -0
- package/scripts/install.mjs +204 -0
- package/scripts/package-vsix.mjs +38 -0
- package/scripts/paths.mjs +40 -0
- package/scripts/publish.mjs +37 -0
- package/scripts/update.mjs +101 -0
- package/src/adaptive-budget.js +46 -0
- package/src/analyzer.js +456 -0
- package/src/audit-history.js +137 -0
- package/src/bm25.js +132 -0
- package/src/collector.js +343 -0
- package/src/compression.js +67 -0
- package/src/config.js +28 -0
- package/src/context-memory.js +151 -0
- package/src/context-profiles.js +124 -0
- package/src/dependency-graph.js +90 -0
- package/src/estimate.js +109 -0
- package/src/eval-harness.js +76 -0
- package/src/extension.js +322 -0
- package/src/firewall.js +35 -0
- package/src/fts-index.js +319 -0
- package/src/gateway-api.js +404 -0
- package/src/git-recency.js +43 -0
- package/src/glob.js +42 -0
- package/src/identifier.js +37 -0
- package/src/inspector-panel.js +470 -0
- package/src/language.js +157 -0
- package/src/mcp-event-stream.js +179 -0
- package/src/mcp-health-cli.js +34 -0
- package/src/mcp-lifecycle-manager.js +427 -0
- package/src/mcp-server.js +372 -0
- package/src/mmr.js +57 -0
- package/src/pagerank.js +176 -0
- package/src/profiles.js +74 -0
- package/src/pruner.js +130 -0
- package/src/quality-guard.js +122 -0
- package/src/query-rewrite.js +32 -0
- package/src/relevance-scorer.js +197 -0
- package/src/repo-map.js +134 -0
- package/src/retrieve.js +350 -0
- package/src/serena.js +126 -0
- package/src/session-ledger.js +83 -0
- package/src/session-store.js +78 -0
- package/src/skeleton.js +129 -0
- package/src/slice-pack.js +70 -0
- package/src/supervisor.js +113 -0
- package/src/task-analyzer.js +189 -0
- package/src/tool-router.js +92 -0
- package/src/version.js +5 -0
- package/templates/ziprin-context-mcp.mjs +68 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared facade for extension dashboard + MCP server (v8).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { analyzeTask } = require('./task-analyzer');
|
|
10
|
+
const { scoreCandidates } = require('./relevance-scorer');
|
|
11
|
+
const { buildDependencyBoost, protectDependencies } = require('./dependency-graph');
|
|
12
|
+
const { compressFile } = require('./compression');
|
|
13
|
+
const { budgetForComplexity } = require('./adaptive-budget');
|
|
14
|
+
const { qualityCheck, applyQualityActions } = require('./quality-guard');
|
|
15
|
+
const { loadMemory, learnFromSession, saveMemory, effectivePathBoost } = require('./context-memory');
|
|
16
|
+
const { firewallDecision } = require('./firewall');
|
|
17
|
+
const { collectWorkspace } = require('./collector');
|
|
18
|
+
const { budgetFor, getContextProfile } = require('./context-profiles');
|
|
19
|
+
const { buildSnapshot, toSession } = require('./analyzer');
|
|
20
|
+
const { appendSession, listSessions } = require('./session-store');
|
|
21
|
+
const { getOrBuildRepoMap } = require('./repo-map');
|
|
22
|
+
const { publishEvent, EVENT_TYPES } = require('./mcp-event-stream');
|
|
23
|
+
const { localFindSymbol, emitFindSymbol, emitForTask } = require('./serena');
|
|
24
|
+
const { unfoldSymbol, skeletonFile } = require('./skeleton');
|
|
25
|
+
const { ensureIndex, indexStats, patchFile } = require('./fts-index');
|
|
26
|
+
const { VERSION } = require('./version');
|
|
27
|
+
|
|
28
|
+
const SKIP_RE =
|
|
29
|
+
/^(سلام|درود|hello|hi|hey|صبح بخیر|good\s*(morning|evening)|thanks|thank you|مرسی|ممنون)[\s!.؟?]*$/i;
|
|
30
|
+
|
|
31
|
+
const SKIP_TRIVIAL_RE =
|
|
32
|
+
/^(what is|what's|explain briefly|توضیح بده|یعنی چی)\b.{0,80}$/i;
|
|
33
|
+
|
|
34
|
+
function shouldSkipOptimize(prompt) {
|
|
35
|
+
const p = String(prompt || '').trim();
|
|
36
|
+
if (!p) return { skip: true, reason: 'empty prompt' };
|
|
37
|
+
if (SKIP_RE.test(p)) return { skip: true, reason: 'greeting / social message' };
|
|
38
|
+
if (p.length < 12 && !/[./\\]|\.(ts|tsx|js|jsx|md|css)\b/i.test(p)) {
|
|
39
|
+
return { skip: true, reason: 'too short and no code path signal' };
|
|
40
|
+
}
|
|
41
|
+
if (SKIP_TRIVIAL_RE.test(p) && !/\b(file|code|bug|fix|refactor|component|api|route)\b/i.test(p)) {
|
|
42
|
+
return { skip: true, reason: 'simple explanation without code work' };
|
|
43
|
+
}
|
|
44
|
+
return { skip: false };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function scorerOptsFromTask(taskInfo, memory, prompt) {
|
|
48
|
+
return {
|
|
49
|
+
taskTokens: taskInfo.tokens,
|
|
50
|
+
category: taskInfo.category,
|
|
51
|
+
memoryBoost: effectivePathBoost(memory),
|
|
52
|
+
domain: taskInfo.domain,
|
|
53
|
+
symbols: taskInfo.symbols || [],
|
|
54
|
+
pathHints: taskInfo.pathHints || [],
|
|
55
|
+
action: taskInfo.intent?.action || 'general',
|
|
56
|
+
prompt: String(prompt || ''),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function analyzeTaskApi(prompt, preferredProfileId) {
|
|
61
|
+
const info = analyzeTask(prompt, preferredProfileId);
|
|
62
|
+
const profileCap = budgetFor(info.profileId).maxOnDemandTokens;
|
|
63
|
+
const budget = budgetForComplexity(info.complexity, profileCap, info.intent);
|
|
64
|
+
return { ...info, budget };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function rankContext(root, prompt, opts = {}) {
|
|
68
|
+
const profileId = opts.profileId || 'standard';
|
|
69
|
+
const cpt = opts.charsPerToken || 4;
|
|
70
|
+
const taskInfo = analyzeTask(prompt, profileId);
|
|
71
|
+
const effectiveProfile = opts.lockProfile ? profileId : taskInfo.profileId;
|
|
72
|
+
const collected = collectWorkspace(root, effectiveProfile, cpt, {
|
|
73
|
+
domain: taskInfo.domain,
|
|
74
|
+
symbols: taskInfo.symbols || [],
|
|
75
|
+
pathHints: taskInfo.pathHints || [],
|
|
76
|
+
});
|
|
77
|
+
const memory = loadMemory(root);
|
|
78
|
+
const sOpts = scorerOptsFromTask(taskInfo, memory, prompt);
|
|
79
|
+
|
|
80
|
+
const survivors = [];
|
|
81
|
+
const blocked = [];
|
|
82
|
+
for (const f of collected.files) {
|
|
83
|
+
const d = firewallDecision(f.path, f.bytes);
|
|
84
|
+
if (d.action === 'block') blocked.push({ path: f.path, reason: d.reason, tokens: f.tokens });
|
|
85
|
+
else survivors.push({ ...f, firewall: d });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let scored = scoreCandidates(survivors, sOpts);
|
|
89
|
+
const topSeeds = scored.filter((s) => s.final_score > 20).slice(0, 25).map((s) => s.path);
|
|
90
|
+
const { boost } = buildDependencyBoost(root, topSeeds, 40);
|
|
91
|
+
scored = scoreCandidates(survivors, { ...sOpts, dependencyBoost: boost });
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
taskInfo,
|
|
95
|
+
profileId: effectiveProfile,
|
|
96
|
+
scored: scored.slice(0, opts.limit || 80).map((s) => ({
|
|
97
|
+
path: s.path,
|
|
98
|
+
final_score: s.final_score,
|
|
99
|
+
keep: s.keep,
|
|
100
|
+
breakdown: s.breakdown,
|
|
101
|
+
confidence: s.confidence,
|
|
102
|
+
whySelected: s.whySelected,
|
|
103
|
+
})),
|
|
104
|
+
blocked,
|
|
105
|
+
keep: scored.filter((s) => s.keep).slice(0, 30).map((s) => s.path),
|
|
106
|
+
drop: scored.filter((s) => !s.keep).slice(0, 40).map((s) => ({
|
|
107
|
+
path: s.path,
|
|
108
|
+
score: s.final_score,
|
|
109
|
+
whyRemoved: s.whySelected,
|
|
110
|
+
})),
|
|
111
|
+
seedDirs: collected.seedDirs,
|
|
112
|
+
symbolFiles: collected.symbolFiles,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function dependencyGraph(root, seedPaths = []) {
|
|
117
|
+
const seeds = (seedPaths || []).slice(0, 40);
|
|
118
|
+
const { boost, edges } = buildDependencyBoost(root, seeds, 40);
|
|
119
|
+
const fakeScored = [...boost].map((p) => ({ path: p, final_score: 25 }));
|
|
120
|
+
for (const s of seeds) fakeScored.push({ path: s, final_score: 50 });
|
|
121
|
+
const protectedSet = protectDependencies(seeds, boost, fakeScored);
|
|
122
|
+
return {
|
|
123
|
+
seeds,
|
|
124
|
+
edges: edges.slice(0, 80),
|
|
125
|
+
boost: [...boost].slice(0, 60),
|
|
126
|
+
protected: protectedSet.selected,
|
|
127
|
+
restored: protectedSet.restored,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function compressContext(root, paths = [], maxChars = 1800) {
|
|
132
|
+
return (paths || []).slice(0, 20).map((p) => compressFile(root, p, maxChars));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function findSymbol(root, name, searchPaths = []) {
|
|
136
|
+
const emit = emitFindSymbol(name);
|
|
137
|
+
let seeds = Array.isArray(searchPaths) ? searchPaths.slice(0, 40) : [];
|
|
138
|
+
if (!seeds.length) {
|
|
139
|
+
try {
|
|
140
|
+
const collected = collectWorkspace(root, 'standard', 4, { symbols: [name], pathHints: [] });
|
|
141
|
+
seeds = (collected.symbolFiles || []).concat(collected.files.map((f) => f.path)).slice(0, 40);
|
|
142
|
+
} catch {
|
|
143
|
+
seeds = [];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const local = localFindSymbol(root, name, seeds);
|
|
147
|
+
return { ...local, serena: emit, call: emit };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function unfoldSymbolApi(root, filePath, name) {
|
|
151
|
+
if (!name && filePath) {
|
|
152
|
+
const skel = skeletonFile(root, filePath);
|
|
153
|
+
return { path: filePath, mode: 'skeleton', signatures: skel.signatures, text: skel.text, tokenEstimate: skel.tokenEstimate };
|
|
154
|
+
}
|
|
155
|
+
return unfoldSymbol(root, filePath, name);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function validateContext(opts = {}) {
|
|
159
|
+
const quality = qualityCheck(opts);
|
|
160
|
+
const selected = applyQualityActions(opts.selected || [], opts.scores || [], quality);
|
|
161
|
+
return { quality, selected, restored: quality.actions || [] };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function contextMemory(root, action = 'read', session) {
|
|
165
|
+
if (action === 'learn' && session) {
|
|
166
|
+
return learnFromSession(root, session);
|
|
167
|
+
}
|
|
168
|
+
if (action === 'clear') {
|
|
169
|
+
const empty = {
|
|
170
|
+
version: 3,
|
|
171
|
+
pathBoost: {},
|
|
172
|
+
categoryHits: {},
|
|
173
|
+
domainHits: {},
|
|
174
|
+
taskActions: {},
|
|
175
|
+
cooccur: {},
|
|
176
|
+
lastSeen: {},
|
|
177
|
+
};
|
|
178
|
+
saveMemory(root, empty);
|
|
179
|
+
return empty;
|
|
180
|
+
}
|
|
181
|
+
return loadMemory(root);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function healthInfo(root) {
|
|
185
|
+
const sessions = listSessions(root, 5);
|
|
186
|
+
const mem = loadMemory(root);
|
|
187
|
+
let repoMap = null;
|
|
188
|
+
try {
|
|
189
|
+
repoMap = getOrBuildRepoMap(root, []);
|
|
190
|
+
} catch {
|
|
191
|
+
repoMap = null;
|
|
192
|
+
}
|
|
193
|
+
let fts = { backend: 'unknown', fileCount: 0 };
|
|
194
|
+
try {
|
|
195
|
+
ensureIndex(root);
|
|
196
|
+
fts = indexStats(root);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
fts = { backend: 'error', error: err.message };
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
version: VERSION,
|
|
202
|
+
root,
|
|
203
|
+
rootExists: fs.existsSync(root),
|
|
204
|
+
sessionCount: sessions.length,
|
|
205
|
+
lastSession: sessions[0]
|
|
206
|
+
? { id: sessions[0].id, timestamp: sessions[0].timestamp, task: sessions[0].task }
|
|
207
|
+
: null,
|
|
208
|
+
memory: {
|
|
209
|
+
version: mem.version,
|
|
210
|
+
updatedAt: mem.updatedAt,
|
|
211
|
+
pathBoostKeys: Object.keys(mem.pathBoost || {}).length,
|
|
212
|
+
cooccurKeys: Object.keys(mem.cooccur || {}).length,
|
|
213
|
+
},
|
|
214
|
+
repoMap: repoMap ? { fileCount: repoMap.fileCount, builtAt: repoMap.builtAt } : null,
|
|
215
|
+
fts,
|
|
216
|
+
tools: 8,
|
|
217
|
+
status: fs.existsSync(root) ? 'ready' : 'no_workspace',
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function agentBrief(snapshot) {
|
|
222
|
+
const ta = snapshot.tokenAnalysis || {};
|
|
223
|
+
const lines = [
|
|
224
|
+
`## Ziprin optimized context (v8 map)`,
|
|
225
|
+
`- class: ${snapshot.taskLabel || snapshot.taskClassification} (${snapshot.profileId})`,
|
|
226
|
+
`- intent: ${snapshot.intent?.action || '?'} / scope=${snapshot.intent?.scope || '?'} / risk=${snapshot.intent?.risk || '?'}`,
|
|
227
|
+
`- tokens map: ${snapshot.tokensAfterPrune} (naive collect ${snapshot.tokensBeforePrune}, −${ta.reductionPct ?? '?'}%)`,
|
|
228
|
+
`- vsAlwaysApply extra: ${ta.vsAlwaysApplyTokens ?? 0}`,
|
|
229
|
+
`- readNow (${(snapshot.readNow || snapshot.filesIncluded || []).length}):`,
|
|
230
|
+
...(snapshot.readNow || snapshot.filesIncluded || []).slice(0, 8).map((f) => ` - ${f}`),
|
|
231
|
+
snapshot.alreadyInContext?.length
|
|
232
|
+
? `- alreadyInContext: ${snapshot.alreadyInContext.slice(0, 6).join(', ')}`
|
|
233
|
+
: '',
|
|
234
|
+
`- serena: ${(snapshot.serena || []).join(', ') || 'none'}`,
|
|
235
|
+
`- serenaEmit: ${JSON.stringify((snapshot.serenaEmit || []).slice(0, 4))}`,
|
|
236
|
+
`- doNotRead: ${(snapshot.doNotRead || []).slice(0, 4).join(', ')}`,
|
|
237
|
+
'',
|
|
238
|
+
'Call Serena find_symbol BEFORE read_file. Unfold one body with ziprin_unfold_symbol. Do not dump @folder.',
|
|
239
|
+
];
|
|
240
|
+
return lines.filter((x) => x !== '').join('\n');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function toLeanResult(full) {
|
|
244
|
+
if (full.skipped) {
|
|
245
|
+
return { skipped: true, reason: full.reason, brief: full.agentBrief };
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
v: VERSION,
|
|
249
|
+
sessionId: full.sessionId,
|
|
250
|
+
filesIncluded: full.filesIncluded,
|
|
251
|
+
readNow: full.readNow,
|
|
252
|
+
alreadyInContext: full.alreadyInContext || [],
|
|
253
|
+
skeletons: (full.selectedMeta || []).map((m) => ({
|
|
254
|
+
p: m.path,
|
|
255
|
+
sig: m.signatures || m.exports || [],
|
|
256
|
+
t: m.tokens,
|
|
257
|
+
})),
|
|
258
|
+
serena: full.serenaEmit || full.serena,
|
|
259
|
+
tokens: {
|
|
260
|
+
before: full.tokenAnalysis?.before,
|
|
261
|
+
after: full.tokenAnalysis?.after,
|
|
262
|
+
budget: full.tokenAnalysis?.budget,
|
|
263
|
+
reductionPct: full.tokenAnalysis?.reductionPct,
|
|
264
|
+
},
|
|
265
|
+
brief: full.agentBrief,
|
|
266
|
+
doNotRead: full.doNotRead,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function optimizeContext(root, opts = {}) {
|
|
271
|
+
const prompt = String(opts.prompt || opts.task || '').trim();
|
|
272
|
+
const skip = shouldSkipOptimize(prompt);
|
|
273
|
+
if (skip.skip && !opts.force) {
|
|
274
|
+
publishEvent(root, {
|
|
275
|
+
service: 'ziprin-context',
|
|
276
|
+
type: EVENT_TYPES.context_analysis_finished,
|
|
277
|
+
status: 'skipped',
|
|
278
|
+
health: 'healthy',
|
|
279
|
+
reason: skip.reason,
|
|
280
|
+
});
|
|
281
|
+
return {
|
|
282
|
+
skipped: true,
|
|
283
|
+
reason: skip.reason,
|
|
284
|
+
prompt,
|
|
285
|
+
agentBrief: `Ziprin MCP skipped: ${skip.reason}. Answer without loading repo context.`,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
publishEvent(root, {
|
|
290
|
+
service: 'ziprin-context',
|
|
291
|
+
type: EVENT_TYPES.context_analysis_started,
|
|
292
|
+
status: 'running',
|
|
293
|
+
health: 'healthy',
|
|
294
|
+
profileId: opts.profileId || 'standard',
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
let snapshot;
|
|
298
|
+
try {
|
|
299
|
+
snapshot = buildSnapshot(root, {
|
|
300
|
+
profileId: opts.profileId || 'standard',
|
|
301
|
+
prompt,
|
|
302
|
+
task: prompt,
|
|
303
|
+
charsPerToken: opts.charsPerToken || 4,
|
|
304
|
+
lockProfile: opts.lockProfile,
|
|
305
|
+
sessionId: opts.sessionId || opts.sessionKey,
|
|
306
|
+
openFiles: opts.openFiles || [],
|
|
307
|
+
});
|
|
308
|
+
} catch (err) {
|
|
309
|
+
publishEvent(root, {
|
|
310
|
+
service: 'ziprin-context',
|
|
311
|
+
type: EVENT_TYPES.server_failed,
|
|
312
|
+
status: 'failed',
|
|
313
|
+
health: 'failed',
|
|
314
|
+
error: err.message,
|
|
315
|
+
});
|
|
316
|
+
throw err;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const session = toSession(snapshot);
|
|
320
|
+
session.source = opts.source || 'mcp';
|
|
321
|
+
appendSession(root, session, opts.maxSessions || 80);
|
|
322
|
+
learnFromSession(root, session);
|
|
323
|
+
|
|
324
|
+
publishEvent(root, {
|
|
325
|
+
service: 'ziprin-context',
|
|
326
|
+
type: EVENT_TYPES.files_selected,
|
|
327
|
+
status: 'connected',
|
|
328
|
+
health: 'healthy',
|
|
329
|
+
toolsCount: (snapshot.filesIncluded || []).length,
|
|
330
|
+
filesIncludedCount: (snapshot.filesIncluded || []).length,
|
|
331
|
+
sessionId: session.id,
|
|
332
|
+
});
|
|
333
|
+
publishEvent(root, {
|
|
334
|
+
service: 'ziprin-context',
|
|
335
|
+
type: EVENT_TYPES.compression_finished,
|
|
336
|
+
status: 'connected',
|
|
337
|
+
health: 'healthy',
|
|
338
|
+
tokensBefore: snapshot.tokensBeforePrune,
|
|
339
|
+
tokensAfter: snapshot.tokensAfterPrune,
|
|
340
|
+
reductionPct: snapshot.tokenAnalysis?.reductionPct,
|
|
341
|
+
});
|
|
342
|
+
publishEvent(root, {
|
|
343
|
+
service: 'ziprin-context',
|
|
344
|
+
type: EVENT_TYPES.context_analysis_finished,
|
|
345
|
+
status: 'connected',
|
|
346
|
+
health: 'healthy',
|
|
347
|
+
sessionId: session.id,
|
|
348
|
+
profileId: snapshot.profileId,
|
|
349
|
+
filesIncludedCount: (snapshot.filesIncluded || []).length,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
skipped: false,
|
|
354
|
+
sessionId: session.id,
|
|
355
|
+
version: VERSION,
|
|
356
|
+
taskClassification: snapshot.taskClassification,
|
|
357
|
+
taskLabel: snapshot.taskLabel,
|
|
358
|
+
intent: snapshot.intent,
|
|
359
|
+
domainId: snapshot.domainId,
|
|
360
|
+
profileId: snapshot.profileId,
|
|
361
|
+
filesIncluded: snapshot.filesIncluded,
|
|
362
|
+
filesExcluded: snapshot.filesExcluded,
|
|
363
|
+
readNow: snapshot.readNow,
|
|
364
|
+
alreadyInContext: snapshot.alreadyInContext,
|
|
365
|
+
serena: snapshot.serena,
|
|
366
|
+
serenaEmit: snapshot.serenaEmit,
|
|
367
|
+
doNotRead: snapshot.doNotRead,
|
|
368
|
+
rulesApplied: snapshot.rulesApplied,
|
|
369
|
+
removedItems: (snapshot.removedItems || []).slice(0, 40).map((r) => ({
|
|
370
|
+
file: r.file,
|
|
371
|
+
reason: r.reason,
|
|
372
|
+
tokenSaving: r.tokenSaving,
|
|
373
|
+
score: r.score,
|
|
374
|
+
confidence: r.confidence,
|
|
375
|
+
})),
|
|
376
|
+
tokenAnalysis: snapshot.tokenAnalysis,
|
|
377
|
+
qualityReport: snapshot.qualityReport,
|
|
378
|
+
decisionReport: snapshot.decisionReport,
|
|
379
|
+
selectedMeta: snapshot.selectedMeta,
|
|
380
|
+
finalContext: snapshot.finalContext,
|
|
381
|
+
agentBrief: agentBrief(snapshot),
|
|
382
|
+
result: snapshot.result,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
module.exports = {
|
|
387
|
+
VERSION,
|
|
388
|
+
shouldSkipOptimize,
|
|
389
|
+
analyzeTask: analyzeTaskApi,
|
|
390
|
+
rankContext,
|
|
391
|
+
dependencyGraph,
|
|
392
|
+
compressContext,
|
|
393
|
+
validateContext,
|
|
394
|
+
contextMemory,
|
|
395
|
+
optimizeContext,
|
|
396
|
+
agentBrief,
|
|
397
|
+
healthInfo,
|
|
398
|
+
getContextProfile,
|
|
399
|
+
findSymbol,
|
|
400
|
+
unfoldSymbol: unfoldSymbolApi,
|
|
401
|
+
toLeanResult,
|
|
402
|
+
patchIndexFile: patchFile,
|
|
403
|
+
emitForTask,
|
|
404
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { isTokenBomb } = require('./pruner');
|
|
6
|
+
|
|
7
|
+
const SOURCE_EXT = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.css', '.md', '.mdc']);
|
|
8
|
+
|
|
9
|
+
function gitRecency(root, opts = {}) {
|
|
10
|
+
const n = opts.n || 60;
|
|
11
|
+
try {
|
|
12
|
+
const r = spawnSync(
|
|
13
|
+
'git',
|
|
14
|
+
['-C', root, 'log', '--name-only', '--pretty=format:%ct', `-n`, String(n)],
|
|
15
|
+
{ encoding: 'utf8', timeout: 4000 },
|
|
16
|
+
);
|
|
17
|
+
if (r.status !== 0) return [];
|
|
18
|
+
const now = Math.floor(Date.now() / 1000);
|
|
19
|
+
const scores = new Map();
|
|
20
|
+
let ts = now;
|
|
21
|
+
for (const line of String(r.stdout || '').split('\n')) {
|
|
22
|
+
const t = line.trim();
|
|
23
|
+
if (!t) continue;
|
|
24
|
+
if (/^\d{10,}$/.test(t)) {
|
|
25
|
+
ts = Number(t);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const rel = t.replace(/\\/g, '/');
|
|
29
|
+
if (!SOURCE_EXT.has(path.extname(rel))) continue;
|
|
30
|
+
if (isTokenBomb(rel)) continue;
|
|
31
|
+
const days = Math.max(0, (now - ts) / 86400);
|
|
32
|
+
const decay = Math.exp(-days / 21);
|
|
33
|
+
scores.set(rel, Math.max(scores.get(rel) || 0, decay));
|
|
34
|
+
}
|
|
35
|
+
return [...scores.entries()]
|
|
36
|
+
.sort((a, b) => b[1] - a[1])
|
|
37
|
+
.map(([p, score]) => ({ path: p, score }));
|
|
38
|
+
} catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { gitRecency };
|
package/src/glob.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function globToRegExp(glob) {
|
|
4
|
+
const g = String(glob || '').replace(/\\/g, '/');
|
|
5
|
+
let re = '';
|
|
6
|
+
for (let i = 0; i < g.length; i++) {
|
|
7
|
+
const ch = g[i];
|
|
8
|
+
if (ch === '*' && g[i + 1] === '*') {
|
|
9
|
+
if (g[i + 2] === '/') {
|
|
10
|
+
re += '(?:.*/)?';
|
|
11
|
+
i += 2;
|
|
12
|
+
} else {
|
|
13
|
+
re += '.*';
|
|
14
|
+
i += 1;
|
|
15
|
+
}
|
|
16
|
+
} else if (ch === '*') {
|
|
17
|
+
re += '[^/]*';
|
|
18
|
+
} else if (ch === '?') {
|
|
19
|
+
re += '[^/]';
|
|
20
|
+
} else {
|
|
21
|
+
re += ch.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return new RegExp('^' + re + '$');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function matchGlob(rel, glob) {
|
|
28
|
+
return globToRegExp(glob).test(String(rel || '').replace(/\\/g, '/'));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function matchAny(rel, globs) {
|
|
32
|
+
return (globs || []).some((g) => matchGlob(rel, g));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function rootFromInclude(glob) {
|
|
36
|
+
const g = String(glob || '').replace(/\\/g, '/');
|
|
37
|
+
const star = g.search(/[*?]/);
|
|
38
|
+
const base = star === -1 ? g : g.slice(0, star).replace(/\/$/, '');
|
|
39
|
+
return base || '.';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { globToRegExp, matchGlob, matchAny, rootFromInclude };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Split Pascal/camel/snake/kebab identifiers so FA+EN prompts hit repo paths.
|
|
5
|
+
* ProductStatusDetailScreen → product, status, detail, screen
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function splitIdentifier(s) {
|
|
9
|
+
const str = String(s || '').trim();
|
|
10
|
+
if (!str) return [];
|
|
11
|
+
const parts = str
|
|
12
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
13
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
14
|
+
.split(/[\s_./\\:@-]+/)
|
|
15
|
+
.map((x) => x.toLowerCase())
|
|
16
|
+
.filter((x) => x.length > 1);
|
|
17
|
+
return [...new Set(parts)];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function splitMany(values) {
|
|
21
|
+
const out = [];
|
|
22
|
+
for (const v of values || []) out.push(...splitIdentifier(v));
|
|
23
|
+
return [...new Set(out)];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function tokenizePath(rel) {
|
|
27
|
+
const p = String(rel || '').replace(/\\/g, '/');
|
|
28
|
+
const segs = p.split('/').filter(Boolean);
|
|
29
|
+
const bag = [];
|
|
30
|
+
for (const seg of segs) {
|
|
31
|
+
bag.push(seg.toLowerCase());
|
|
32
|
+
bag.push(...splitIdentifier(seg.replace(/\.(tsx?|jsx?|mjs|cjs|css|mdc?)$/i, '')));
|
|
33
|
+
}
|
|
34
|
+
return [...new Set(bag.filter((t) => t.length > 1))];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { splitIdentifier, splitMany, tokenizePath };
|