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,151 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Context Memory Engine v8 — path boost + co-occurrence + exponential decay.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const MEMORY_REL = '.cursor/context-gateway-memory.json';
|
|
11
|
+
const HALF_LIFE_DAYS = 21;
|
|
12
|
+
|
|
13
|
+
function memoryPath(root) {
|
|
14
|
+
return path.join(root, MEMORY_REL);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function emptyMemory() {
|
|
18
|
+
return {
|
|
19
|
+
version: 3,
|
|
20
|
+
pathBoost: {},
|
|
21
|
+
negativeBoost: {},
|
|
22
|
+
categoryHits: {},
|
|
23
|
+
domainHits: {},
|
|
24
|
+
taskActions: {},
|
|
25
|
+
cooccur: {},
|
|
26
|
+
lastSeen: {},
|
|
27
|
+
lastDomain: null,
|
|
28
|
+
openFiles: [],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function loadMemory(root) {
|
|
33
|
+
const file = memoryPath(root);
|
|
34
|
+
if (!fs.existsSync(file)) return emptyMemory();
|
|
35
|
+
try {
|
|
36
|
+
const mem = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
37
|
+
return {
|
|
38
|
+
...emptyMemory(),
|
|
39
|
+
...mem,
|
|
40
|
+
version: mem.version || 3,
|
|
41
|
+
pathBoost: mem.pathBoost || {},
|
|
42
|
+
negativeBoost: mem.negativeBoost || {},
|
|
43
|
+
categoryHits: mem.categoryHits || {},
|
|
44
|
+
domainHits: mem.domainHits || {},
|
|
45
|
+
taskActions: mem.taskActions || {},
|
|
46
|
+
cooccur: mem.cooccur || {},
|
|
47
|
+
lastSeen: mem.lastSeen || {},
|
|
48
|
+
lastDomain: mem.lastDomain || null,
|
|
49
|
+
openFiles: mem.openFiles || [],
|
|
50
|
+
updatedAt: mem.updatedAt,
|
|
51
|
+
};
|
|
52
|
+
} catch {
|
|
53
|
+
return emptyMemory();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function saveMemory(root, mem) {
|
|
58
|
+
fs.mkdirSync(path.dirname(memoryPath(root)), { recursive: true });
|
|
59
|
+
fs.writeFileSync(memoryPath(root), JSON.stringify(mem, null, 2), 'utf8');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function decayFactor(iso, now = Date.now()) {
|
|
63
|
+
if (!iso) return 1;
|
|
64
|
+
const t = Date.parse(iso);
|
|
65
|
+
if (!Number.isFinite(t)) return 1;
|
|
66
|
+
const days = Math.max(0, (now - t) / 86400000);
|
|
67
|
+
return Math.exp(-Math.log(2) * days / HALF_LIFE_DAYS);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function decayedBoostMap(boost, lastSeen) {
|
|
71
|
+
const out = {};
|
|
72
|
+
for (const [k, v] of Object.entries(boost || {})) {
|
|
73
|
+
out[k] = (Number(v) || 0) * decayFactor(lastSeen?.[k]);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function learnFromSession(root, session) {
|
|
79
|
+
if (!root || !session) return;
|
|
80
|
+
const mem = loadMemory(root);
|
|
81
|
+
if (!mem.negativeBoost) mem.negativeBoost = {};
|
|
82
|
+
if (!mem.cooccur) mem.cooccur = {};
|
|
83
|
+
if (!mem.lastSeen) mem.lastSeen = {};
|
|
84
|
+
const nowIso = new Date().toISOString();
|
|
85
|
+
const files = session.filesIncluded || [];
|
|
86
|
+
for (const f of files) {
|
|
87
|
+
mem.pathBoost[f] = Math.min(25, (mem.pathBoost[f] || 0) + 2);
|
|
88
|
+
const dir = path.posix.dirname(f.replace(/\\/g, '/'));
|
|
89
|
+
mem.pathBoost[dir] = Math.min(20, (mem.pathBoost[dir] || 0) + 1);
|
|
90
|
+
mem.lastSeen[f] = nowIso;
|
|
91
|
+
}
|
|
92
|
+
for (let i = 0; i < files.length; i++) {
|
|
93
|
+
for (let j = i + 1; j < files.length; j++) {
|
|
94
|
+
const a = files[i];
|
|
95
|
+
const b = files[j];
|
|
96
|
+
mem.cooccur[a] = mem.cooccur[a] || {};
|
|
97
|
+
mem.cooccur[b] = mem.cooccur[b] || {};
|
|
98
|
+
mem.cooccur[a][b] = Math.min(20, (mem.cooccur[a][b] || 0) + 1);
|
|
99
|
+
mem.cooccur[b][a] = Math.min(20, (mem.cooccur[b][a] || 0) + 1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const unused = session.filesUnused || session.unusedSelected || [];
|
|
103
|
+
for (const f of unused) {
|
|
104
|
+
mem.negativeBoost[f] = Math.min(20, (mem.negativeBoost[f] || 0) + 3);
|
|
105
|
+
mem.pathBoost[f] = Math.max(0, (mem.pathBoost[f] || 0) - 4);
|
|
106
|
+
}
|
|
107
|
+
const cat = session.taskClassification || session.category;
|
|
108
|
+
if (cat) mem.categoryHits[cat] = (mem.categoryHits[cat] || 0) + 1;
|
|
109
|
+
const domainId = session.intent?.domain || session.domainId;
|
|
110
|
+
if (domainId) {
|
|
111
|
+
mem.domainHits[domainId] = (mem.domainHits[domainId] || 0) + 1;
|
|
112
|
+
mem.lastDomain = domainId;
|
|
113
|
+
}
|
|
114
|
+
const action = session.intent?.action;
|
|
115
|
+
if (action) mem.taskActions[action] = (mem.taskActions[action] || 0) + 1;
|
|
116
|
+
if (Array.isArray(session.openFiles)) mem.openFiles = session.openFiles.slice(0, 20);
|
|
117
|
+
mem.updatedAt = nowIso;
|
|
118
|
+
mem.version = 3;
|
|
119
|
+
saveMemory(root, mem);
|
|
120
|
+
return mem;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function domainBoostFromMemory(mem, domainId) {
|
|
124
|
+
if (!domainId || !mem?.domainHits?.[domainId]) return 0;
|
|
125
|
+
return Math.min(15, mem.domainHits[domainId]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function cooccurBoost(mem, path, selectedSeeds = []) {
|
|
129
|
+
const row = mem?.cooccur?.[path];
|
|
130
|
+
if (!row) return 0;
|
|
131
|
+
let s = 0;
|
|
132
|
+
for (const seed of selectedSeeds) s += (row[seed] || 0);
|
|
133
|
+
return Math.min(12, s);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function effectivePathBoost(mem) {
|
|
137
|
+
return decayedBoostMap(mem.pathBoost, mem.lastSeen);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
MEMORY_REL,
|
|
142
|
+
emptyMemory,
|
|
143
|
+
loadMemory,
|
|
144
|
+
saveMemory,
|
|
145
|
+
learnFromSession,
|
|
146
|
+
domainBoostFromMemory,
|
|
147
|
+
cooccurBoost,
|
|
148
|
+
decayFactor,
|
|
149
|
+
effectivePathBoost,
|
|
150
|
+
HALF_LIFE_DAYS,
|
|
151
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getProfile } = require('./profiles');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_EXCLUDE = [
|
|
6
|
+
'**/node_modules/**',
|
|
7
|
+
'**/.git/**',
|
|
8
|
+
'**/dist/**',
|
|
9
|
+
'**/build/**',
|
|
10
|
+
'**/.next/**',
|
|
11
|
+
'**/*.vsix',
|
|
12
|
+
'.cursor/agents/**',
|
|
13
|
+
'.cursor/skills/**',
|
|
14
|
+
'tooling/project-index.json',
|
|
15
|
+
'tooling/governance/project-must-follow-catalog.json',
|
|
16
|
+
'**/reports/**/*.json',
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const CONTEXT_PROFILES = {
|
|
20
|
+
frontend: {
|
|
21
|
+
id: 'frontend',
|
|
22
|
+
label: 'Frontend UI',
|
|
23
|
+
description: 'React, components, design system, CSS',
|
|
24
|
+
budgetProfileId: 'standard',
|
|
25
|
+
include: [
|
|
26
|
+
'apps/marketplace/src/**',
|
|
27
|
+
'apps/dashboard/**/*.tsx',
|
|
28
|
+
'apps/dashboard/**/*.css',
|
|
29
|
+
'packages/ui/src/**',
|
|
30
|
+
],
|
|
31
|
+
exclude: [...DEFAULT_EXCLUDE, 'apps/marketplace/src/server/**', '**/database/**'],
|
|
32
|
+
},
|
|
33
|
+
backend: {
|
|
34
|
+
id: 'backend',
|
|
35
|
+
label: 'Backend',
|
|
36
|
+
description: 'Services, API, shared contracts — not UI chrome',
|
|
37
|
+
budgetProfileId: 'standard',
|
|
38
|
+
include: [
|
|
39
|
+
'apps/marketplace/src/server/**',
|
|
40
|
+
'apps/dashboard/bss/src/**/*.ts',
|
|
41
|
+
'packages/shared/src/**',
|
|
42
|
+
],
|
|
43
|
+
exclude: [...DEFAULT_EXCLUDE, '**/*.tsx', '**/frontend-build/**'],
|
|
44
|
+
},
|
|
45
|
+
architecture: {
|
|
46
|
+
id: 'architecture',
|
|
47
|
+
label: 'Architecture',
|
|
48
|
+
description: 'System design, governance, dependencies',
|
|
49
|
+
budgetProfileId: 'maximum',
|
|
50
|
+
include: [
|
|
51
|
+
'AGENTS.md',
|
|
52
|
+
'.cursor/rules/**',
|
|
53
|
+
'apps/marketplace/docs/governance/**',
|
|
54
|
+
'apps/dashboard/bss/docs/**',
|
|
55
|
+
'tooling/governance/**',
|
|
56
|
+
'packages/ui/src/**',
|
|
57
|
+
'packages/shared/src/**',
|
|
58
|
+
],
|
|
59
|
+
exclude: DEFAULT_EXCLUDE,
|
|
60
|
+
},
|
|
61
|
+
standard: {
|
|
62
|
+
id: 'standard',
|
|
63
|
+
label: 'Standard',
|
|
64
|
+
description: 'Balanced product sources',
|
|
65
|
+
budgetProfileId: 'standard',
|
|
66
|
+
include: [
|
|
67
|
+
'apps/dashboard/bss/src/**',
|
|
68
|
+
'apps/dashboard/my/src/**',
|
|
69
|
+
'apps/marketplace/src/**',
|
|
70
|
+
'packages/ui/src/**',
|
|
71
|
+
'packages/shared/src/**',
|
|
72
|
+
'AGENTS.md',
|
|
73
|
+
'.cursor/rules/*.mdc',
|
|
74
|
+
],
|
|
75
|
+
exclude: DEFAULT_EXCLUDE,
|
|
76
|
+
},
|
|
77
|
+
minimal: {
|
|
78
|
+
id: 'minimal',
|
|
79
|
+
label: 'Minimal',
|
|
80
|
+
description: 'Supervisor + public APIs only',
|
|
81
|
+
budgetProfileId: 'minimal',
|
|
82
|
+
include: [
|
|
83
|
+
'AGENTS.md',
|
|
84
|
+
'.cursor/rules/ziprin-supervisor.mdc',
|
|
85
|
+
'packages/ui/src/**',
|
|
86
|
+
'packages/shared/src/**',
|
|
87
|
+
],
|
|
88
|
+
exclude: DEFAULT_EXCLUDE,
|
|
89
|
+
},
|
|
90
|
+
strict: {
|
|
91
|
+
id: 'strict',
|
|
92
|
+
label: 'Strict',
|
|
93
|
+
description: 'Tight budget; task-matching paths only',
|
|
94
|
+
budgetProfileId: 'strict',
|
|
95
|
+
include: [
|
|
96
|
+
'apps/dashboard/bss/src/**',
|
|
97
|
+
'apps/marketplace/src/**',
|
|
98
|
+
'packages/ui/src/**',
|
|
99
|
+
'packages/shared/src/**',
|
|
100
|
+
],
|
|
101
|
+
exclude: DEFAULT_EXCLUDE,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
function getContextProfile(id) {
|
|
106
|
+
return CONTEXT_PROFILES[id] || CONTEXT_PROFILES.standard;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function listContextProfiles() {
|
|
110
|
+
return Object.values(CONTEXT_PROFILES);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function budgetFor(profileId) {
|
|
114
|
+
const ctx = getContextProfile(profileId);
|
|
115
|
+
return getProfile(ctx.budgetProfileId);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = {
|
|
119
|
+
CONTEXT_PROFILES,
|
|
120
|
+
DEFAULT_EXCLUDE,
|
|
121
|
+
getContextProfile,
|
|
122
|
+
listContextProfiles,
|
|
123
|
+
budgetFor,
|
|
124
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Lightweight import graph — regex only, capped reads (no full LSP).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const IMPORT_RE = /(?:import|export)\s+(?:type\s+)?(?:[^'"\n]+from\s+)?['"]([^'"]+)['"]|require\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
11
|
+
|
|
12
|
+
function resolveImport(fromRel, spec, root) {
|
|
13
|
+
if (!spec || spec.startsWith('http')) return null;
|
|
14
|
+
if (spec.startsWith('@ziprin/ui')) return 'packages/ui/src';
|
|
15
|
+
if (spec.startsWith('@ziprin/shared')) return 'packages/shared/src';
|
|
16
|
+
if (!spec.startsWith('.') && !spec.startsWith('/')) return null;
|
|
17
|
+
|
|
18
|
+
const fromDir = path.posix.dirname(fromRel.replace(/\\/g, '/'));
|
|
19
|
+
let target = path.posix.normalize(path.posix.join(fromDir, spec));
|
|
20
|
+
const absBase = path.join(root, target);
|
|
21
|
+
const tries = [
|
|
22
|
+
target,
|
|
23
|
+
target + '.ts',
|
|
24
|
+
target + '.tsx',
|
|
25
|
+
target + '.js',
|
|
26
|
+
target + '.jsx',
|
|
27
|
+
target + '/index.ts',
|
|
28
|
+
target + '/index.tsx',
|
|
29
|
+
];
|
|
30
|
+
for (const t of tries) {
|
|
31
|
+
if (fs.existsSync(path.join(root, t))) return t.replace(/\\/g, '/');
|
|
32
|
+
}
|
|
33
|
+
if (fs.existsSync(absBase)) return target;
|
|
34
|
+
return target;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function extractImports(text) {
|
|
38
|
+
const out = [];
|
|
39
|
+
IMPORT_RE.lastIndex = 0;
|
|
40
|
+
let m;
|
|
41
|
+
while ((m = IMPORT_RE.exec(text))) {
|
|
42
|
+
out.push(m[1] || m[2]);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildDependencyBoost(root, seedPaths, maxFiles = 40) {
|
|
48
|
+
const boost = new Set();
|
|
49
|
+
const edges = [];
|
|
50
|
+
const seeds = seedPaths.slice(0, maxFiles);
|
|
51
|
+
|
|
52
|
+
for (const rel of seeds) {
|
|
53
|
+
const abs = path.join(root, rel);
|
|
54
|
+
let text = '';
|
|
55
|
+
try {
|
|
56
|
+
text = fs.readFileSync(abs, 'utf8').slice(0, 120_000);
|
|
57
|
+
} catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const specs = extractImports(text);
|
|
61
|
+
for (const spec of specs) {
|
|
62
|
+
const resolved = resolveImport(rel, spec, root);
|
|
63
|
+
if (!resolved) continue;
|
|
64
|
+
boost.add(resolved);
|
|
65
|
+
edges.push({ from: rel, to: resolved, spec });
|
|
66
|
+
// package roots as soft deps
|
|
67
|
+
if (resolved.startsWith('packages/ui')) boost.add('packages/ui/src');
|
|
68
|
+
if (resolved.startsWith('packages/shared')) boost.add('packages/shared/src');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { boost, edges };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function protectDependencies(selectedPaths, boost, allScored) {
|
|
76
|
+
const selected = new Set(selectedPaths);
|
|
77
|
+
const restored = [];
|
|
78
|
+
for (const dep of boost) {
|
|
79
|
+
if (selected.has(dep)) continue;
|
|
80
|
+
// restore only concrete files present in scored list
|
|
81
|
+
const hit = allScored.find((s) => s.path === dep || s.path.startsWith(dep + '/'));
|
|
82
|
+
if (hit && hit.final_score > -50) {
|
|
83
|
+
selected.add(hit.path);
|
|
84
|
+
restored.push(hit.path);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { selected: [...selected], restored };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { extractImports, resolveImport, buildDependencyBoost, protectDependencies };
|
package/src/estimate.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function walk(dir, acc = []) {
|
|
7
|
+
if (!fs.existsSync(dir)) return acc;
|
|
8
|
+
let entries;
|
|
9
|
+
try {
|
|
10
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
11
|
+
} catch {
|
|
12
|
+
return acc;
|
|
13
|
+
}
|
|
14
|
+
for (const ent of entries) {
|
|
15
|
+
const p = path.join(dir, ent.name);
|
|
16
|
+
if (ent.isDirectory()) {
|
|
17
|
+
if (ent.name === 'node_modules' || ent.name === '.git' || ent.name === 'dist' || ent.name === '.next') continue;
|
|
18
|
+
walk(p, acc);
|
|
19
|
+
} else {
|
|
20
|
+
acc.push(p);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return acc;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function tokens(bytes, charsPerToken = 4) {
|
|
27
|
+
return Math.ceil(Math.max(0, bytes) / Math.max(1, charsPerToken));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function safeSize(p) {
|
|
31
|
+
try {
|
|
32
|
+
return fs.statSync(p).size;
|
|
33
|
+
} catch {
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function estimateContext(root, opts = {}) {
|
|
39
|
+
const cpt = opts.charsPerToken || 4;
|
|
40
|
+
const groups = {
|
|
41
|
+
'cursor/rules': walk(path.join(root, '.cursor/rules')),
|
|
42
|
+
'cursor/agents': walk(path.join(root, '.cursor/agents')),
|
|
43
|
+
'cursor/skills': walk(path.join(root, '.cursor/skills')),
|
|
44
|
+
'cursor/prompts': walk(path.join(root, '.cursor/prompts')),
|
|
45
|
+
catalogs: [
|
|
46
|
+
path.join(root, 'tooling/project-index.json'),
|
|
47
|
+
path.join(root, 'tooling/context-optimizer/project-index.json'),
|
|
48
|
+
path.join(root, 'tooling/governance/project-must-follow-catalog.json'),
|
|
49
|
+
].filter((p) => fs.existsSync(p)),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const out = {
|
|
53
|
+
generatedAt: new Date().toISOString(),
|
|
54
|
+
root,
|
|
55
|
+
heuristic: `bytes/${cpt}`,
|
|
56
|
+
groups: {},
|
|
57
|
+
alwaysApplyTrue: [],
|
|
58
|
+
total: { bytes: 0, tokens: 0 },
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
for (const [name, files] of Object.entries(groups)) {
|
|
62
|
+
let bytes = 0;
|
|
63
|
+
for (const f of files) bytes += safeSize(f);
|
|
64
|
+
const tok = tokens(bytes, cpt);
|
|
65
|
+
out.groups[name] = { files: files.length, bytes, tokens: tok };
|
|
66
|
+
out.total.bytes += bytes;
|
|
67
|
+
out.total.tokens += tok;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const rulesDir = path.join(root, '.cursor/rules');
|
|
71
|
+
if (fs.existsSync(rulesDir)) {
|
|
72
|
+
for (const f of walk(rulesDir)) {
|
|
73
|
+
if (!f.endsWith('.mdc')) continue;
|
|
74
|
+
try {
|
|
75
|
+
const text = fs.readFileSync(f, 'utf8');
|
|
76
|
+
if (/alwaysApply:\s*true/.test(text)) {
|
|
77
|
+
out.alwaysApplyTrue.push(path.relative(root, f).replace(/\\/g, '/'));
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
/* skip */
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const DEFAULT_LOAD_POLICY = {
|
|
89
|
+
version: 2,
|
|
90
|
+
alwaysLoad: ['.cursor/rules/ziprin-supervisor.mdc'],
|
|
91
|
+
onDemand: [
|
|
92
|
+
'tooling/context-optimizer/project-index.summary.json',
|
|
93
|
+
'AGENTS.md',
|
|
94
|
+
],
|
|
95
|
+
neverIndex: [
|
|
96
|
+
'tooling/project-index.json',
|
|
97
|
+
'tooling/context-optimizer/project-index.json',
|
|
98
|
+
'tooling/governance/project-must-follow-catalog.json',
|
|
99
|
+
'**/reports/**/*.json',
|
|
100
|
+
'.cursor/_tmp_*',
|
|
101
|
+
'.cursor/agents/**',
|
|
102
|
+
'.cursor/skills/**',
|
|
103
|
+
'**/node_modules/**',
|
|
104
|
+
'**/.git/**',
|
|
105
|
+
],
|
|
106
|
+
preferTools: ['serena', 'grep', 'read_file', 'ziprin-super-guard'],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
module.exports = { estimateContext, tokens, walk, DEFAULT_LOAD_POLICY, safeSize };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* File-level golden eval: P@k + latency.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { optimizeContext } = require('./gateway-api');
|
|
8
|
+
|
|
9
|
+
function precisionAtK(included, gold, k = 3) {
|
|
10
|
+
const top = (included || []).slice(0, k);
|
|
11
|
+
if (!gold.length) return 1;
|
|
12
|
+
const hits = gold.filter((g) => top.some((p) => p.includes(g))).length;
|
|
13
|
+
return hits / Math.min(k, gold.length);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const GOLD = [
|
|
17
|
+
{
|
|
18
|
+
name: 'FA add-product',
|
|
19
|
+
prompt: 'باگ فرم افزودن محصول فروشنده',
|
|
20
|
+
gold: ['add-product-screen', 'add-product.api', 'add-product.mapper'],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'EN add-product',
|
|
24
|
+
prompt: 'fix vendor add-product form validation',
|
|
25
|
+
gold: ['add-product-screen', 'add-product.api', 'add-product.mapper'],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'FA marketplace cart',
|
|
29
|
+
prompt: 'سبد خرید مارکت باز نمیشود',
|
|
30
|
+
gold: ['cart-drawer', 'checkout'],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'EN checkout',
|
|
34
|
+
prompt: 'fix checkout mapper on marketplace',
|
|
35
|
+
gold: ['checkout.mapper', 'checkout-page'],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'symbol screen',
|
|
39
|
+
prompt: 'review ProductStatusDetailScreen',
|
|
40
|
+
gold: ['product-status-screen'],
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
function runGold(root, opts = {}) {
|
|
45
|
+
const { closeIndex } = require('./fts-index');
|
|
46
|
+
closeIndex(root);
|
|
47
|
+
const k = opts.k || 3;
|
|
48
|
+
const rows = [];
|
|
49
|
+
try {
|
|
50
|
+
for (const g of GOLD) {
|
|
51
|
+
const t0 = Date.now();
|
|
52
|
+
const r = optimizeContext(root, {
|
|
53
|
+
prompt: g.prompt,
|
|
54
|
+
profileId: 'frontend',
|
|
55
|
+
sessionId: `eval-${g.name}`,
|
|
56
|
+
source: 'eval',
|
|
57
|
+
});
|
|
58
|
+
const ms = Date.now() - t0;
|
|
59
|
+
const p = precisionAtK(r.filesIncluded, g.gold, k);
|
|
60
|
+
rows.push({
|
|
61
|
+
name: g.name,
|
|
62
|
+
pAtK: p,
|
|
63
|
+
ms,
|
|
64
|
+
files: r.filesIncluded || [],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
} finally {
|
|
68
|
+
closeIndex(root);
|
|
69
|
+
}
|
|
70
|
+
const mean = rows.reduce((s, x) => s + x.pAtK, 0) / rows.length;
|
|
71
|
+
const latencies = rows.map((x) => x.ms).sort((a, b) => a - b);
|
|
72
|
+
const p95 = latencies[Math.min(latencies.length - 1, Math.floor(latencies.length * 0.95))];
|
|
73
|
+
return { rows, mean, p95, k };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { precisionAtK, GOLD, runGold };
|