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
package/src/bm25.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BM25 / BM25F (path ×8, body ×1, symbols ×12). Pure JS fallback when FTS5 is cold.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { tokenizePath, splitIdentifier } = require('./identifier');
|
|
8
|
+
|
|
9
|
+
const K1 = 1.2;
|
|
10
|
+
const B = 0.75;
|
|
11
|
+
const FIELD_WEIGHTS = { path: 8, body: 1, symbols: 12 };
|
|
12
|
+
|
|
13
|
+
function idf(N, df) {
|
|
14
|
+
return Math.log(1 + (N - df + 0.5) / (df + 0.5));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function tfComponent(tf, dl, avgdl, k1 = K1, b = B) {
|
|
18
|
+
if (tf <= 0) return 0;
|
|
19
|
+
return (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * (dl / Math.max(avgdl, 1))));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function termFreq(terms) {
|
|
23
|
+
const tf = new Map();
|
|
24
|
+
for (const t of terms) tf.set(t, (tf.get(t) || 0) + 1);
|
|
25
|
+
return tf;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildFieldCorpus(docs, field) {
|
|
29
|
+
const df = new Map();
|
|
30
|
+
let totalLen = 0;
|
|
31
|
+
for (const d of docs) {
|
|
32
|
+
const terms = d[field] || [];
|
|
33
|
+
totalLen += terms.length;
|
|
34
|
+
for (const t of new Set(terms)) df.set(t, (df.get(t) || 0) + 1);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
df,
|
|
38
|
+
N: docs.length,
|
|
39
|
+
avgdl: docs.length ? totalLen / docs.length : 1,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function bm25F(doc, queryTerms, corpora) {
|
|
44
|
+
let score = 0;
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
for (const q of queryTerms) {
|
|
47
|
+
if (!q || seen.has(q)) continue;
|
|
48
|
+
seen.add(q);
|
|
49
|
+
for (const field of ['path', 'body', 'symbols']) {
|
|
50
|
+
const corp = corpora[field];
|
|
51
|
+
if (!corp || !corp.N) continue;
|
|
52
|
+
const tfMap = termFreq(doc[field] || []);
|
|
53
|
+
const tf = tfMap.get(q) || 0;
|
|
54
|
+
if (!tf) continue;
|
|
55
|
+
const w = FIELD_WEIGHTS[field] || 1;
|
|
56
|
+
score += w * idf(corp.N, corp.df.get(q) || 0) * tfComponent(tf, (doc[field] || []).length, corp.avgdl);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return score;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function bm25Path(paths, queryTerms) {
|
|
63
|
+
const docs = (paths || []).map((p) => ({
|
|
64
|
+
path: p,
|
|
65
|
+
terms: tokenizePath(p),
|
|
66
|
+
}));
|
|
67
|
+
const df = new Map();
|
|
68
|
+
let total = 0;
|
|
69
|
+
for (const d of docs) {
|
|
70
|
+
total += d.terms.length;
|
|
71
|
+
for (const t of new Set(d.terms)) df.set(t, (df.get(t) || 0) + 1);
|
|
72
|
+
}
|
|
73
|
+
const corpus = { df, N: docs.length, avgdl: docs.length ? total / docs.length : 1 };
|
|
74
|
+
const q = (queryTerms || []).map((t) => String(t).toLowerCase()).filter((t) => t.length > 1);
|
|
75
|
+
return docs
|
|
76
|
+
.map((d) => {
|
|
77
|
+
let s = 0;
|
|
78
|
+
const tf = termFreq(d.terms);
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
for (const term of q) {
|
|
81
|
+
if (seen.has(term)) continue;
|
|
82
|
+
seen.add(term);
|
|
83
|
+
const f = tf.get(term) || 0;
|
|
84
|
+
if (!f) continue;
|
|
85
|
+
s += idf(corpus.N, df.get(term) || 0) * tfComponent(f, d.terms.length, corpus.avgdl);
|
|
86
|
+
}
|
|
87
|
+
return { path: d.path, score: s };
|
|
88
|
+
})
|
|
89
|
+
.sort((a, b) => b.score - a.score);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function tokenizeBody(text, maxTerms = 800) {
|
|
93
|
+
const raw = String(text || '')
|
|
94
|
+
.slice(0, 16000)
|
|
95
|
+
.split(/[^\p{L}\p{N}_]+/u)
|
|
96
|
+
.map((t) => t.toLowerCase())
|
|
97
|
+
.filter((t) => t.length > 1);
|
|
98
|
+
const extra = [];
|
|
99
|
+
for (const t of raw.slice(0, 120)) extra.push(...splitIdentifier(t));
|
|
100
|
+
return [...raw, ...extra].slice(0, maxTerms);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function scoreDocsBm25F(docs, queryTerms) {
|
|
104
|
+
if (!docs.length) return [];
|
|
105
|
+
const corpora = {
|
|
106
|
+
path: buildFieldCorpus(docs, 'pathTerms'),
|
|
107
|
+
body: buildFieldCorpus(docs, 'bodyTerms'),
|
|
108
|
+
symbols: buildFieldCorpus(docs, 'symbolTerms'),
|
|
109
|
+
};
|
|
110
|
+
const q = (queryTerms || []).map((t) => String(t).toLowerCase()).filter((t) => t.length > 1);
|
|
111
|
+
return docs
|
|
112
|
+
.map((d) => ({
|
|
113
|
+
path: d.path,
|
|
114
|
+
score: bm25F(
|
|
115
|
+
{ path: d.pathTerms, body: d.bodyTerms, symbols: d.symbolTerms },
|
|
116
|
+
q,
|
|
117
|
+
corpora,
|
|
118
|
+
),
|
|
119
|
+
}))
|
|
120
|
+
.sort((a, b) => b.score - a.score);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
FIELD_WEIGHTS,
|
|
125
|
+
idf,
|
|
126
|
+
tfComponent,
|
|
127
|
+
bm25Path,
|
|
128
|
+
bm25F,
|
|
129
|
+
scoreDocsBm25F,
|
|
130
|
+
tokenizeBody,
|
|
131
|
+
termFreq,
|
|
132
|
+
};
|
package/src/collector.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { tokens, safeSize } = require('./estimate');
|
|
6
|
+
const { isTokenBomb } = require('./pruner');
|
|
7
|
+
const { matchAny, rootFromInclude } = require('./glob');
|
|
8
|
+
const { getContextProfile } = require('./context-profiles');
|
|
9
|
+
const { splitIdentifier } = require('./identifier');
|
|
10
|
+
|
|
11
|
+
const SKIP_DIRS = new Set([
|
|
12
|
+
'node_modules',
|
|
13
|
+
'.git',
|
|
14
|
+
'dist',
|
|
15
|
+
'build',
|
|
16
|
+
'.next',
|
|
17
|
+
'coverage',
|
|
18
|
+
'public',
|
|
19
|
+
'.turbo',
|
|
20
|
+
'reports',
|
|
21
|
+
'fixtures',
|
|
22
|
+
'mock-data',
|
|
23
|
+
'__mocks__',
|
|
24
|
+
'seed',
|
|
25
|
+
'generated',
|
|
26
|
+
'.ziprin-context',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const SOURCE_EXT = new Set(['.ts', '.tsx', '.js', '.jsx', '.css', '.md', '.mdc']);
|
|
30
|
+
const MAX_FILES_TOTAL = 220;
|
|
31
|
+
const MAX_FILES_PER_ROOT = 90;
|
|
32
|
+
const MAX_SEED_FILES = 40;
|
|
33
|
+
|
|
34
|
+
function isSourceFile(rel) {
|
|
35
|
+
const ext = path.extname(String(rel || '')).toLowerCase();
|
|
36
|
+
return SOURCE_EXT.has(ext);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function pascalToKebab(s) {
|
|
40
|
+
return String(s).replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function collectRules(root) {
|
|
44
|
+
const dir = path.join(root, '.cursor', 'rules');
|
|
45
|
+
if (!fs.existsSync(dir)) return [];
|
|
46
|
+
let names;
|
|
47
|
+
try {
|
|
48
|
+
names = fs.readdirSync(dir);
|
|
49
|
+
} catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
const rules = [];
|
|
53
|
+
for (const name of names) {
|
|
54
|
+
if (!name.endsWith('.mdc')) continue;
|
|
55
|
+
const rel = `.cursor/rules/${name}`;
|
|
56
|
+
const abs = path.join(root, rel);
|
|
57
|
+
let text = '';
|
|
58
|
+
try {
|
|
59
|
+
text = fs.readFileSync(abs, 'utf8').slice(0, 400);
|
|
60
|
+
} catch {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
rules.push({
|
|
64
|
+
path: rel,
|
|
65
|
+
alwaysApply: /alwaysApply:\s*true/.test(text),
|
|
66
|
+
excerpt: text.split('\n').slice(0, 8).join('\n'),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return rules;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Hint-first only. No hardcoded product-status vs add-product rank. */
|
|
73
|
+
function sortEntries(entries, pathHints = []) {
|
|
74
|
+
return entries.sort((a, b) => {
|
|
75
|
+
const hintRank = (n) => {
|
|
76
|
+
if (pathHints.some((h) => n.includes(h) || h.includes(n))) return 0;
|
|
77
|
+
if (n === 'src') return 1;
|
|
78
|
+
if (n === 'public') return 9;
|
|
79
|
+
if (n === 'docs') return 8;
|
|
80
|
+
return 5;
|
|
81
|
+
};
|
|
82
|
+
const ha = hintRank(a.name);
|
|
83
|
+
const hb = hintRank(b.name);
|
|
84
|
+
if (ha !== hb) return ha - hb;
|
|
85
|
+
return a.name.localeCompare(b.name);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function walkSource(absDir, root, onFile, maxMatches, pathHints = []) {
|
|
90
|
+
let count = 0;
|
|
91
|
+
function walk(dir) {
|
|
92
|
+
if (count >= maxMatches) return;
|
|
93
|
+
let entries;
|
|
94
|
+
try {
|
|
95
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
96
|
+
} catch {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const ent of sortEntries(entries, pathHints)) {
|
|
100
|
+
if (count >= maxMatches) return;
|
|
101
|
+
if (ent.isDirectory()) {
|
|
102
|
+
if (SKIP_DIRS.has(ent.name)) continue;
|
|
103
|
+
walk(path.join(dir, ent.name));
|
|
104
|
+
} else {
|
|
105
|
+
const rel = path.relative(root, path.join(dir, ent.name)).replace(/\\/g, '/');
|
|
106
|
+
if (!isSourceFile(rel)) continue;
|
|
107
|
+
onFile(rel);
|
|
108
|
+
count++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
walk(absDir);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildSeedDirs(root, collectOpts = {}) {
|
|
116
|
+
const seeds = new Set();
|
|
117
|
+
const { domain, pathHints = [] } = collectOpts;
|
|
118
|
+
if (domain?.priorityPaths) {
|
|
119
|
+
for (const dp of domain.priorityPaths) seeds.add(dp.replace(/\\/g, '/'));
|
|
120
|
+
}
|
|
121
|
+
for (const hint of pathHints) {
|
|
122
|
+
for (const base of [
|
|
123
|
+
'apps/dashboard/bss/src/portals/vendor',
|
|
124
|
+
'apps/dashboard/bss/src',
|
|
125
|
+
'apps/marketplace/src',
|
|
126
|
+
'apps/dashboard',
|
|
127
|
+
]) {
|
|
128
|
+
seeds.add(`${base}/${hint}`);
|
|
129
|
+
}
|
|
130
|
+
if (hint.includes('/')) {
|
|
131
|
+
seeds.add(`apps/dashboard/bss/src/portals/${hint}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return [...seeds].filter((s) => {
|
|
135
|
+
const abs = path.join(root, s);
|
|
136
|
+
return fs.existsSync(abs);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function findFilesByBasename(root, relRoot, basenames, includeGlobs, excludeGlobs, limit = 10) {
|
|
141
|
+
const found = [];
|
|
142
|
+
const abs = path.join(root, relRoot);
|
|
143
|
+
if (!fs.existsSync(abs)) return found;
|
|
144
|
+
const want = new Set(basenames.map((b) => b.toLowerCase()));
|
|
145
|
+
walkSource(abs, root, (rel) => {
|
|
146
|
+
if (found.length >= limit) return;
|
|
147
|
+
const base = path.basename(rel).toLowerCase();
|
|
148
|
+
const stem = base.replace(/\.(tsx|ts|jsx|js)$/, '');
|
|
149
|
+
if (!want.has(base) && !want.has(stem)) return;
|
|
150
|
+
if (!matchAny(rel, includeGlobs)) return;
|
|
151
|
+
if (matchAny(rel, excludeGlobs || [])) return;
|
|
152
|
+
if (!found.includes(rel)) found.push(rel);
|
|
153
|
+
}, limit * 20, basenames);
|
|
154
|
+
return found;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function findSymbolFiles(root, symbols, includeGlobs, excludeGlobs, limit = 20) {
|
|
158
|
+
const found = [];
|
|
159
|
+
if (!symbols.length) return found;
|
|
160
|
+
|
|
161
|
+
for (const sym of symbols) {
|
|
162
|
+
const kebab = pascalToKebab(sym);
|
|
163
|
+
const parts = splitIdentifier(sym);
|
|
164
|
+
const basenames = [
|
|
165
|
+
`${kebab}.tsx`,
|
|
166
|
+
`${kebab}.ts`,
|
|
167
|
+
`${sym.toLowerCase()}.tsx`,
|
|
168
|
+
];
|
|
169
|
+
if (parts.includes('product') && parts.includes('status')) {
|
|
170
|
+
basenames.push('product-status-screen.tsx', 'product-status.api.ts');
|
|
171
|
+
}
|
|
172
|
+
for (const sr of [
|
|
173
|
+
'apps/dashboard/bss/src',
|
|
174
|
+
'apps/marketplace/src',
|
|
175
|
+
'packages/ui/src',
|
|
176
|
+
]) {
|
|
177
|
+
for (const rel of findFilesByBasename(root, sr, basenames, includeGlobs, excludeGlobs, limit)) {
|
|
178
|
+
if (!found.includes(rel)) found.push(rel);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const variants = [sym.toLowerCase(), kebab, kebab.replace(/-screen$/, ''), ...parts.filter((p) => p.length > 3)];
|
|
183
|
+
for (const sr of ['apps/dashboard/bss/src', 'apps/marketplace/src', 'packages/ui/src']) {
|
|
184
|
+
const abs = path.join(root, sr);
|
|
185
|
+
if (!fs.existsSync(abs)) continue;
|
|
186
|
+
walkSource(
|
|
187
|
+
abs,
|
|
188
|
+
root,
|
|
189
|
+
(rel) => {
|
|
190
|
+
if (found.length >= limit) return;
|
|
191
|
+
const lower = rel.toLowerCase();
|
|
192
|
+
if (!variants.some((v) => v.length > 2 && lower.includes(v))) return;
|
|
193
|
+
if (!matchAny(rel, includeGlobs)) return;
|
|
194
|
+
if (matchAny(rel, excludeGlobs || [])) return;
|
|
195
|
+
if (!found.includes(rel)) found.push(rel);
|
|
196
|
+
},
|
|
197
|
+
limit * 8,
|
|
198
|
+
variants,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return found.slice(0, limit);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function collectFromSeeds(root, seedDirs, includeGlobs, excludeGlobs, limit, pathHints = []) {
|
|
206
|
+
const bag = [];
|
|
207
|
+
for (const relDir of seedDirs) {
|
|
208
|
+
if (bag.length >= limit) break;
|
|
209
|
+
const abs = path.join(root, relDir);
|
|
210
|
+
if (!fs.existsSync(abs)) continue;
|
|
211
|
+
const st = fs.statSync(abs);
|
|
212
|
+
if (st.isFile()) {
|
|
213
|
+
const rel = relDir.replace(/\\/g, '/');
|
|
214
|
+
if (isSourceFile(rel) && matchAny(rel, includeGlobs) && !matchAny(rel, excludeGlobs || [])) {
|
|
215
|
+
bag.push(rel);
|
|
216
|
+
}
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
walkSource(abs, root, (rel) => {
|
|
220
|
+
if (bag.length >= limit) return;
|
|
221
|
+
if (!matchAny(rel, includeGlobs)) return;
|
|
222
|
+
if (matchAny(rel, excludeGlobs || [])) return;
|
|
223
|
+
if (!bag.includes(rel)) bag.push(rel);
|
|
224
|
+
}, limit - bag.length, pathHints);
|
|
225
|
+
}
|
|
226
|
+
return bag;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function addFileEntry(root, rel, seen, files, ignored, profile, charsPerToken, maxTotal) {
|
|
230
|
+
if (seen.has(rel)) return false;
|
|
231
|
+
seen.add(rel);
|
|
232
|
+
const bytes = safeSize(path.join(root, rel));
|
|
233
|
+
const tok = tokens(bytes, charsPerToken);
|
|
234
|
+
|
|
235
|
+
if (matchAny(rel, profile.exclude || [])) {
|
|
236
|
+
ignored.push({ file: rel, reason: 'ignored by project rules', tokenSaving: tok });
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
if (isTokenBomb(rel)) {
|
|
240
|
+
ignored.push({ file: rel, reason: 'token bomb / never-index', tokenSaving: tok });
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
files.push({ path: rel, bytes, tokens: tok, seeded: true });
|
|
244
|
+
return files.length < maxTotal;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Collect workspace files for scoring. Hint-first, no folder rank bias (v8).
|
|
249
|
+
*/
|
|
250
|
+
function collectWorkspace(root, profileId, charsPerToken = 4, collectOpts = {}) {
|
|
251
|
+
const profile = getContextProfile(profileId);
|
|
252
|
+
const seen = new Set();
|
|
253
|
+
const files = [];
|
|
254
|
+
const ignored = [];
|
|
255
|
+
let truncated = false;
|
|
256
|
+
|
|
257
|
+
const seedDirs = buildSeedDirs(root, collectOpts);
|
|
258
|
+
const symbolFiles = findSymbolFiles(
|
|
259
|
+
root,
|
|
260
|
+
collectOpts.symbols || [],
|
|
261
|
+
profile.include,
|
|
262
|
+
profile.exclude,
|
|
263
|
+
MAX_SEED_FILES,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const seedBag = [
|
|
267
|
+
...symbolFiles,
|
|
268
|
+
...collectFromSeeds(root, seedDirs, profile.include, profile.exclude, MAX_SEED_FILES, collectOpts.pathHints || []),
|
|
269
|
+
];
|
|
270
|
+
for (const rel of seedBag) {
|
|
271
|
+
if (files.length >= MAX_FILES_TOTAL) {
|
|
272
|
+
truncated = true;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
addFileEntry(root, rel, seen, files, ignored, profile, charsPerToken, MAX_FILES_TOTAL);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const roots = [...new Set(profile.include.map(rootFromInclude))];
|
|
279
|
+
for (const relRoot of roots) {
|
|
280
|
+
if (files.length >= MAX_FILES_TOTAL) {
|
|
281
|
+
truncated = true;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
const abs = relRoot === '.' ? root : path.join(root, relRoot);
|
|
285
|
+
if (!fs.existsSync(abs)) continue;
|
|
286
|
+
const st = fs.statSync(abs);
|
|
287
|
+
const perRootLimit = MAX_FILES_PER_ROOT;
|
|
288
|
+
const bag = [];
|
|
289
|
+
if (st.isFile()) {
|
|
290
|
+
const rel = relRoot.replace(/\\/g, '/');
|
|
291
|
+
if (isSourceFile(rel)) bag.push(rel);
|
|
292
|
+
} else {
|
|
293
|
+
walkSource(abs, root, (rel) => {
|
|
294
|
+
if (bag.length >= perRootLimit) return;
|
|
295
|
+
if (!matchAny(rel, profile.include)) return;
|
|
296
|
+
if (matchAny(rel, profile.exclude || [])) return;
|
|
297
|
+
if (!bag.includes(rel)) bag.push(rel);
|
|
298
|
+
}, perRootLimit, collectOpts.pathHints || []);
|
|
299
|
+
}
|
|
300
|
+
if (bag.length >= perRootLimit) truncated = true;
|
|
301
|
+
|
|
302
|
+
for (const rel of bag) {
|
|
303
|
+
if (files.length >= MAX_FILES_TOTAL) {
|
|
304
|
+
truncated = true;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
if (seen.has(rel)) continue;
|
|
308
|
+
seen.add(rel);
|
|
309
|
+
const bytes = safeSize(path.join(root, rel));
|
|
310
|
+
const tok = tokens(bytes, charsPerToken);
|
|
311
|
+
if (matchAny(rel, profile.exclude || [])) {
|
|
312
|
+
ignored.push({ file: rel, reason: 'ignored by project rules', tokenSaving: tok });
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (isTokenBomb(rel)) {
|
|
316
|
+
ignored.push({ file: rel, reason: 'token bomb / never-index', tokenSaving: tok });
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
files.push({ path: rel, bytes, tokens: tok });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
profileId: profile.id,
|
|
325
|
+
files,
|
|
326
|
+
ignored,
|
|
327
|
+
rules: collectRules(root),
|
|
328
|
+
truncated,
|
|
329
|
+
seedDirs,
|
|
330
|
+
symbolFiles,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = {
|
|
335
|
+
collectWorkspace,
|
|
336
|
+
collectRules,
|
|
337
|
+
MAX_FILES: MAX_FILES_TOTAL,
|
|
338
|
+
SKIP_DIRS,
|
|
339
|
+
isSourceFile,
|
|
340
|
+
buildSeedDirs,
|
|
341
|
+
findSymbolFiles,
|
|
342
|
+
sortEntries,
|
|
343
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { skeletonFile, extractSignatures } = require('./skeleton');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Smart compression v8 — function skeletons instead of head-55.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const EXPORT_RE = /^export\s+(?:async\s+)?(?:function|class|const|type|interface|enum)\s+([A-Za-z0-9_]+)/gm;
|
|
12
|
+
const FUNC_RE = /^(?:export\s+)?(?:async\s+)?function\s+([A-Za-z0-9_]+)/gm;
|
|
13
|
+
const TYPE_RE = /^export\s+(?:type|interface)\s+([A-Za-z0-9_]+)/gm;
|
|
14
|
+
|
|
15
|
+
function estimateTokensFromText(text) {
|
|
16
|
+
const chars = String(text || '').length;
|
|
17
|
+
const idents = (String(text || '').match(/[A-Za-z_][A-Za-z0-9_]*/g) || []).length;
|
|
18
|
+
return Math.ceil(chars / 3.6 + idents * 0.12);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function extractExports(text) {
|
|
22
|
+
const exports = new Set();
|
|
23
|
+
let m;
|
|
24
|
+
for (const re of [EXPORT_RE, FUNC_RE, TYPE_RE]) {
|
|
25
|
+
re.lastIndex = 0;
|
|
26
|
+
while ((m = re.exec(text))) exports.add(m[1]);
|
|
27
|
+
}
|
|
28
|
+
return exports;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function compressFile(root, rel, maxChars = 1800) {
|
|
32
|
+
const abs = path.join(root, rel);
|
|
33
|
+
let text = '';
|
|
34
|
+
try {
|
|
35
|
+
text = fs.readFileSync(abs, 'utf8');
|
|
36
|
+
} catch {
|
|
37
|
+
return { path: rel, mode: 'missing', text: '', tokenEstimate: 0, exports: [] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const exports = extractExports(text);
|
|
41
|
+
const sigs = extractSignatures(text);
|
|
42
|
+
|
|
43
|
+
if (text.length <= 8000) {
|
|
44
|
+
return {
|
|
45
|
+
path: rel,
|
|
46
|
+
mode: 'full',
|
|
47
|
+
text,
|
|
48
|
+
tokenEstimate: estimateTokensFromText(text),
|
|
49
|
+
exports: [...exports].slice(0, 60),
|
|
50
|
+
signatures: sigs,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const skel = skeletonFile(root, rel);
|
|
55
|
+
const clipped = skel.text.slice(0, Math.max(maxChars, 1200));
|
|
56
|
+
return {
|
|
57
|
+
path: rel,
|
|
58
|
+
mode: text.length <= 40000 ? 'skeleton' : 'compressed',
|
|
59
|
+
text: clipped,
|
|
60
|
+
tokenEstimate: estimateTokensFromText(clipped),
|
|
61
|
+
exports: [...exports].slice(0, 60),
|
|
62
|
+
signatures: skel.signatures,
|
|
63
|
+
removedSections: `${text.split(/\r?\n/).length} lines → ${skel.signatures.length} signatures`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { compressFile, extractExports, estimateTokensFromText };
|
package/src/config.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function getConfig(vscode) {
|
|
4
|
+
const defaults = {
|
|
5
|
+
profile: 'standard',
|
|
6
|
+
charsPerToken: 4,
|
|
7
|
+
stripOtherMdc: true,
|
|
8
|
+
liveRefresh: true,
|
|
9
|
+
refreshDebounceMs: 500,
|
|
10
|
+
autoRevealPanel: true,
|
|
11
|
+
maxSessions: 80,
|
|
12
|
+
recordOnStartOnly: true,
|
|
13
|
+
};
|
|
14
|
+
if (!vscode) return defaults;
|
|
15
|
+
const c = vscode.workspace.getConfiguration('ziprinContext');
|
|
16
|
+
return {
|
|
17
|
+
profile: c.get('profile', defaults.profile),
|
|
18
|
+
charsPerToken: c.get('charsPerToken', defaults.charsPerToken),
|
|
19
|
+
stripOtherMdc: c.get('stripOtherMdc', defaults.stripOtherMdc),
|
|
20
|
+
liveRefresh: c.get('liveRefresh', defaults.liveRefresh),
|
|
21
|
+
refreshDebounceMs: c.get('refreshDebounceMs', defaults.refreshDebounceMs),
|
|
22
|
+
autoRevealPanel: c.get('autoRevealPanel', defaults.autoRevealPanel),
|
|
23
|
+
maxSessions: c.get('maxSessions', defaults.maxSessions),
|
|
24
|
+
recordOnStartOnly: c.get('recordOnStartOnly', defaults.recordOnStartOnly),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { getConfig };
|