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.
Files changed (56) hide show
  1. package/README.md +48 -0
  2. package/bin/ziprin-context-mcp.js +34 -0
  3. package/bin/ziprin-context-setup.js +10 -0
  4. package/install-source.json +5 -0
  5. package/media/context-activity.svg +7 -0
  6. package/package.json +154 -0
  7. package/releases/ziprin-context-optimizer-8.0.0.vsix +0 -0
  8. package/scripts/install.mjs +204 -0
  9. package/scripts/package-vsix.mjs +38 -0
  10. package/scripts/paths.mjs +40 -0
  11. package/scripts/publish.mjs +37 -0
  12. package/scripts/update.mjs +101 -0
  13. package/src/adaptive-budget.js +46 -0
  14. package/src/analyzer.js +456 -0
  15. package/src/audit-history.js +137 -0
  16. package/src/bm25.js +132 -0
  17. package/src/collector.js +343 -0
  18. package/src/compression.js +67 -0
  19. package/src/config.js +28 -0
  20. package/src/context-memory.js +151 -0
  21. package/src/context-profiles.js +124 -0
  22. package/src/dependency-graph.js +90 -0
  23. package/src/estimate.js +109 -0
  24. package/src/eval-harness.js +76 -0
  25. package/src/extension.js +322 -0
  26. package/src/firewall.js +35 -0
  27. package/src/fts-index.js +319 -0
  28. package/src/gateway-api.js +404 -0
  29. package/src/git-recency.js +43 -0
  30. package/src/glob.js +42 -0
  31. package/src/identifier.js +37 -0
  32. package/src/inspector-panel.js +470 -0
  33. package/src/language.js +157 -0
  34. package/src/mcp-event-stream.js +179 -0
  35. package/src/mcp-health-cli.js +34 -0
  36. package/src/mcp-lifecycle-manager.js +427 -0
  37. package/src/mcp-server.js +372 -0
  38. package/src/mmr.js +57 -0
  39. package/src/pagerank.js +176 -0
  40. package/src/profiles.js +74 -0
  41. package/src/pruner.js +130 -0
  42. package/src/quality-guard.js +122 -0
  43. package/src/query-rewrite.js +32 -0
  44. package/src/relevance-scorer.js +197 -0
  45. package/src/repo-map.js +134 -0
  46. package/src/retrieve.js +350 -0
  47. package/src/serena.js +126 -0
  48. package/src/session-ledger.js +83 -0
  49. package/src/session-store.js +78 -0
  50. package/src/skeleton.js +129 -0
  51. package/src/slice-pack.js +70 -0
  52. package/src/supervisor.js +113 -0
  53. package/src/task-analyzer.js +189 -0
  54. package/src/tool-router.js +92 -0
  55. package/src/version.js +5 -0
  56. 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 };