@alucard_24/pi-codeindex 0.0.0-stage → 1.1.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/src/cli.js ADDED
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+ // CLI: embedding, indice, configurazione persistente e diagnostica.
3
+ // Uso: node src/cli.js <comando> [opzioni]
4
+
5
+ import fs from 'node:fs';
6
+ import { spawnSync } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { embed, embedQuery, embedDocs, cosine, MODEL_ID, getEmbeddingProfile, modelDir, getServiceStatus, serviceCommand, embeddingStatus } from './embed.js';
9
+ import { configCommand } from './config.js';
10
+ import { formatProfile } from './backend.js';
11
+ import { doctor, formatDoctor, parseDoctorArgs } from './doctor.js';
12
+ import { indexRepo, statusOf } from './indexer.js';
13
+ import { loadSearcher, search } from './search.js';
14
+ import { formatBundle, formatHits } from './present.js';
15
+ import { buildBundle, buildGraph } from './graph.js';
16
+ import { runEval } from './eval.js';
17
+ import { indexDir } from './store.js';
18
+ import { fmtBytes, fmtDuration } from './util.js';
19
+
20
+ function arg(name, def = null) {
21
+ const i = process.argv.indexOf(`--${name}`);
22
+ if (i === -1) return def;
23
+ const v = process.argv[i + 1];
24
+ return v && !v.startsWith('--') ? v : true;
25
+ }
26
+
27
+ function rssMb() {
28
+ return Math.round(process.memoryUsage().rss / 1024 / 1024);
29
+ }
30
+
31
+ async function embedTest() {
32
+ const SAME = 'export function parseArgs(argv) { return argv.slice(2); }';
33
+ const OTHER =
34
+ 'async function waitForIdle(timeoutMs) { await new Promise((r) => setTimeout(r, timeoutMs)); }';
35
+
36
+ const t0 = performance.now();
37
+ const [sameA, sameB, other] = await embed([SAME, SAME, OTHER]);
38
+ const tCold = Math.round(performance.now() - t0);
39
+
40
+ const profile = await getEmbeddingProfile();
41
+ console.log('runtime :', MODEL_ID, formatProfile(profile));
42
+ console.log('cache :', await modelDir());
43
+ console.log('dims :', sameA.length, '(atteso 768)');
44
+ console.log('cos(same) :', cosine(sameA, sameB).toFixed(6), '(atteso ~1.0)');
45
+ console.log('cos(diverse) :', cosine(sameA, other).toFixed(6), '(atteso < 0.95)');
46
+ console.log('latency cold :', tCold, 'ms per 3 testi');
47
+ console.log('rss client :', rssMb(), 'MiB');
48
+ const service = await getServiceStatus();
49
+ console.log('servizio :', service.pid, '· RSS', Math.round(service.rssBytes / 1024 / 1024), 'MiB');
50
+
51
+ const batch = Array.from({ length: 16 }, (_, i) => `${SAME} // variante ${i}`);
52
+ const t1 = performance.now();
53
+ await embed(batch);
54
+ const warm = Math.round(performance.now() - t1);
55
+ console.log('latency warm :', warm, `ms per 16 testi (${(warm / 16).toFixed(1)} ms/testo)`);
56
+
57
+ const q = await embedQuery('dove vengono parsati gli argomenti da riga di comando');
58
+ const docs = await embedDocs([
59
+ { path: 'src/cli/args.ts', text: SAME },
60
+ { path: 'src/async/idle.ts', text: OTHER },
61
+ ]);
62
+ console.log('query->args :', cosine(q[0], docs[0]).toFixed(4));
63
+ console.log('query->idle :', cosine(q[0], docs[1]).toFixed(4));
64
+ console.log('rss client finale:', rssMb(), 'MiB');
65
+
66
+ const dump = arg('dump');
67
+ if (dump && dump !== true) {
68
+ fs.writeFileSync(dump, JSON.stringify({ ...profile, vectors: { same: sameA, other } }));
69
+ console.log('dump :', dump);
70
+ }
71
+ }
72
+
73
+ async function cmdIndex() {
74
+ const root = arg('root') ?? process.cwd();
75
+ const maxFiles = Number(arg('max-files', 5000));
76
+ const maxBytes = Math.round(Number(arg('max-bytes', 200)) * 1024 * 1024);
77
+ const batchSize = Number(arg('batch', 16));
78
+ const force = Boolean(arg('force', false));
79
+
80
+ const r = await indexRepo({ root, maxFiles, maxBytes, batchSize, force });
81
+ const out = {
82
+ repo: r.repo,
83
+ profile: { model: r.index.model, dims: r.index.dims, device: r.index.device, dtype: r.index.dtype, webgpuBackend: r.index.webgpuBackend },
84
+ files: r.index.stats.files,
85
+ chunks: r.total,
86
+ embedded: r.embedded,
87
+ reused: r.reused,
88
+ unchangedFiles: r.unchanged,
89
+ bytes: r.index.stats.bytes,
90
+ durationMs: Math.round(r.duration),
91
+ skip: r.index.skipped,
92
+ };
93
+ if (arg('json')) {
94
+ console.log(JSON.stringify(out));
95
+ } else {
96
+ console.log(`repo : ${r.repo}`);
97
+ console.log(`runtime : ${formatProfile(r.index)} · servizio condiviso`);
98
+ console.log(`file : ${out.files} (${fmtBytes(out.bytes)}) · invariati ${out.unchangedFiles}`);
99
+ console.log(`chunk : ${out.chunks} · riusati ${out.reused} · embeddati ${out.embedded}`);
100
+ console.log(`esclusi : estensione ${out.skip.ext}, dimensione ${out.skip.big}, binari ${out.skip.binary}, minified ${out.skip.minified}, cap ${out.skip.cap}`);
101
+ console.log(`durata : ${fmtDuration(out.durationMs)}`);
102
+ }
103
+ }
104
+
105
+ async function cmdStatus() {
106
+ const root = arg('root') ?? process.cwd();
107
+ const { repo, index, hasVectors } = await statusOf(root);
108
+ if (!index) {
109
+ console.log(`nessun indice per ${repo}`);
110
+ return;
111
+ }
112
+ const info = {
113
+ repo,
114
+ files: index.stats?.files,
115
+ chunks: index.chunks.length,
116
+ dims: index.dims,
117
+ model: `${index.model} (${index.dtype})`,
118
+ device: index.device ?? 'legacy (reindex richiesto)',
119
+ ...(index.device === 'webgpu' ? { webgpuBackend: index.webgpuBackend ?? 'auto' } : {}),
120
+ chunkerVersion: index.chunkerVersion,
121
+ head: index.head,
122
+ updatedAt: index.updatedAt,
123
+ vectors: hasVectors,
124
+ };
125
+ if (arg('json')) {
126
+ console.log(JSON.stringify(info));
127
+ return;
128
+ }
129
+ for (const [k, v] of Object.entries(info)) console.log(`${k.padEnd(14)}: ${v}`);
130
+ console.log(`${'cache'.padEnd(14)}: ${indexDir(repo)}`);
131
+ }
132
+
133
+ async function cmdSearch() {
134
+ const query = process.argv[3];
135
+ if (!query) throw new Error('uso: search "query" [--k 8] [--path-prefix src/] [--json]');
136
+ const res = await search(query, {
137
+ root: arg('root') ?? process.cwd(),
138
+ k: Number(arg('k', 8)),
139
+ pathPrefix: arg('path-prefix'),
140
+ checkStale: Boolean(arg('stale', false)),
141
+ });
142
+ if (arg('bundle')) {
143
+ const { index } = await loadSearcher(res.repo);
144
+ const graph = await buildGraph(res.repo, index);
145
+ await buildBundle(res.repo, index, res.results, graph);
146
+ console.log(formatBundle(query, res));
147
+ return;
148
+ }
149
+ if (arg('json')) console.log(JSON.stringify({ ...res, repo: undefined }));
150
+ else console.log(formatHits(query, res));
151
+ }
152
+
153
+ async function cmdEval() {
154
+ const file = arg('file') ?? fileURLToPath(new URL('../eval/queries.jsonl', import.meta.url));
155
+ const r = await runEval({
156
+ root: arg('root') ?? process.cwd(),
157
+ file,
158
+ k: Number(arg('k', 8)),
159
+ });
160
+ console.log('');
161
+ console.log(`recall@3: ${(r.recall3 * 100).toFixed(0)}% recall@8: ${(r.recall8 * 100).toFixed(0)}% (${r.queries} query)`);
162
+ console.log(`latenza: p50 ${r.latencyP50} ms · p95 ${r.latencyP95} ms · cold ${r.coldMs} ms`);
163
+ if (arg('json')) console.log(JSON.stringify(r));
164
+ const ok = r.recall3 >= 0.625 && r.latencyP50 < 300;
165
+ if (!ok) process.exitCode = 2;
166
+ }
167
+
168
+ const cmd = process.argv[2];
169
+ try {
170
+ if (cmd === 'config') console.log(await configCommand(process.argv.slice(3)));
171
+ else if (cmd === 'service') console.log(await serviceCommand(process.argv.slice(3)));
172
+ else if (cmd === 'setup-vulkan') {
173
+ const { setupVulkan } = await import('../scripts/setup-vulkan.mjs');
174
+ await setupVulkan(process.argv.slice(3));
175
+ }
176
+ else if (cmd === 'setup-gpu') {
177
+ if (process.platform !== 'linux' || process.arch !== 'x64') throw new Error('setup-gpu CUDA disponibile solo su Linux x64; usa doctor per i backend nativi della tua piattaforma');
178
+ const result = spawnSync('bash', [fileURLToPath(new URL('../scripts/setup-gpu.sh', import.meta.url))], { stdio: 'inherit' });
179
+ if (result.error) throw result.error;
180
+ process.exitCode = result.status ?? 1;
181
+ }
182
+ else if (cmd === 'doctor') {
183
+ const options = parseDoctorArgs(process.argv.slice(3));
184
+ const report = await doctor(options);
185
+ console.log(options.json ? JSON.stringify(report) : formatDoctor(report));
186
+ if (!report.ok) process.exitCode = 1;
187
+ }
188
+ else if (cmd === 'embed-test') await embedTest();
189
+ else if (cmd === 'index') await cmdIndex();
190
+ else if (cmd === 'status') await cmdStatus();
191
+ else if (cmd === 'search') await cmdSearch();
192
+ else if (cmd === 'eval') await cmdEval();
193
+ else {
194
+ console.error('comandi: embed-test | index | status | search | eval | setup-gpu | setup-vulkan | service [status [--json]|stop] | config [show|path|set CHIAVE VALORE|unset CHIAVE] | doctor [--probe] [--all] [--device DEVICE] [--dtype DTYPE] [--webgpu-backend auto|vulkan] [--json]');
195
+ process.exit(1);
196
+ }
197
+ const runtime = embeddingStatus();
198
+ const warnings = [...runtime.warnings, ...runtime.attempts.filter((a) => !a.ok).map((a) => `${a.device}: ${a.error}`)];
199
+ if (runtime.profile && warnings.length) console.error(`codeindex: selezionato ${formatProfile(runtime.profile)}; ${warnings.join('; ')}`);
200
+ } catch (e) {
201
+ console.error(`errore: ${e.message}`);
202
+ if (process.env.CODEINDEX_DEBUG) console.error(e.stack);
203
+ process.exit(1);
204
+ }
@@ -0,0 +1,37 @@
1
+ // Solo dialoghi standard pi: utilizzabili anche tramite RPC. Nessun salvataggio
2
+ // prima della conferma; gli override d'ambiente non vengono copiati nel file.
3
+ import { DEVICES, DTYPES, WEBGPU_BACKENDS, ENV_KEYS, formatConfig, loadConfig, parseSetting, updateConfig } from './config.js';
4
+
5
+ export async function configure(ctx, options = {}) {
6
+ if (!ctx.hasUI) throw new Error('configurazione interattiva non disponibile; usa /codeindex config set CHIAVE VALORE');
7
+ const loaded = await loadConfig(options);
8
+ const draft = { ...loaded.config };
9
+ const patch = {};
10
+ const keys = Object.keys(ENV_KEYS);
11
+ while (true) {
12
+ const entries = [...keys.map((key) => `${key}: ${draft[key]}`), 'Salva', 'Annulla'];
13
+ const selected = await ctx.ui.select('Embedding locali — configurazione (riavvio richiesto)', entries);
14
+ if (!selected || selected === 'Annulla') return null;
15
+ if (selected === 'Salva') {
16
+ if (!Object.keys(patch).length) return null;
17
+ if (!(await ctx.ui.confirm('Salvare la configurazione?', `${loaded.file}\n${JSON.stringify(patch, null, 2)}\nRiavvia pi dopo il salvataggio. Per cambiare un servizio già attivo usa /codeindex service stop quando inattivo. Cambiare device/dtype/backend WebGPU richiede un re-index.`))) return null;
18
+ const file = await updateConfig(patch, options);
19
+ const overrides = Object.keys(patch).filter((key) => loaded.sources[key] === ENV_KEYS[key]);
20
+ return `salvato: ${file}\nRiavvia pi (nuovo processo) per applicare; per cambiare un servizio già attivo usa /codeindex service stop quando inattivo.` +
21
+ (overrides.length ? `\nAttenzione: override d'ambiente attivi per ${overrides.join(', ')}.` : '') +
22
+ `\n${formatConfig(await loadConfig(options))}`;
23
+ }
24
+ const key = keys[entries.indexOf(selected)];
25
+ if (!key) continue;
26
+ const choices = key === 'device' ? DEVICES : key === 'dtype' ? DTYPES
27
+ : key === 'webgpuBackend' ? WEBGPU_BACKENDS : key === 'fallback' ? ['error', 'cpu'] : key === 'offline' ? ['false', 'true'] : null;
28
+ const hint = key === 'webgpuBackend' ? ' — solo WebGPU; vulkan richiede setup-vulkan'
29
+ : key === 'serviceIdleMs' ? ' — millisecondi; 300000 = 5 minuti'
30
+ : key === 'serviceDir' || key === 'servicePort' ? ' — lascia uguale fra sessioni per condividere un solo modello' : '';
31
+ const text = choices ? await ctx.ui.select(`${key}${hint} (attuale: ${draft[key]})`, choices)
32
+ : await ctx.ui.input(`${key}${hint} (attuale: ${draft[key]})`, String(draft[key]));
33
+ if (text === undefined) continue;
34
+ try { draft[key] = patch[key] = parseSetting(key, text); }
35
+ catch (e) { ctx.ui.notify(e.message, 'error'); }
36
+ }
37
+ }
package/src/config.js ADDED
@@ -0,0 +1,131 @@
1
+ // Configurazione della macchina: mai lettura implicita di file nel repository.
2
+ // Precedenza: default < file utente (o CODEINDEX_CONFIG) < ambiente.
3
+ import fs from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { randomUUID } from 'node:crypto';
7
+
8
+ export const DEVICES = ['auto', 'cpu', 'cuda', 'webgpu', 'dml', 'coreml'];
9
+ export const DTYPES = ['auto', 'q8', 'q4', 'fp16'];
10
+ export const WEBGPU_BACKENDS = ['auto', 'vulkan'];
11
+ export const ENV_KEYS = {
12
+ device: 'CODEINDEX_DEVICE', dtype: 'CODEINDEX_DTYPE', fallback: 'CODEINDEX_FALLBACK',
13
+ offline: 'CODEINDEX_OFFLINE', cacheDir: 'CODEINDEX_MODEL_CACHE', cpuThreads: 'CODEINDEX_CPU_THREADS',
14
+ webgpuBackend: 'CODEINDEX_WEBGPU_BACKEND',
15
+ serviceDir: 'CODEINDEX_SERVICE_DIR', servicePort: 'CODEINDEX_SERVICE_PORT', serviceIdleMs: 'CODEINDEX_SERVICE_IDLE_MS',
16
+ };
17
+
18
+ export function configPath(env = process.env, home = os.homedir()) {
19
+ if (env.CODEINDEX_CONFIG) return expandPath(env.CODEINDEX_CONFIG, home);
20
+ const base = env.XDG_CONFIG_HOME || (process.platform === 'win32' ? env.APPDATA : null) || path.join(home, '.config');
21
+ return path.resolve(base, 'pi-codeindex', 'config.json');
22
+ }
23
+
24
+ function expandPath(value, home = os.homedir()) {
25
+ return path.resolve(value === '~' ? home : value.startsWith('~/') ? path.join(home, value.slice(2)) : value);
26
+ }
27
+
28
+ export function defaults(home = os.homedir()) {
29
+ return { device: 'auto', dtype: 'auto', webgpuBackend: 'auto', fallback: 'error', offline: false,
30
+ cacheDir: path.join(home, '.cache', 'pi-codeindex', 'text-model'), cpuThreads: 0,
31
+ serviceDir: path.join(home, '.cache', 'pi-codeindex', 'service'), servicePort: 0, serviceIdleMs: 300_000 };
32
+ }
33
+
34
+ export function validateConfig(value, source = 'config') {
35
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${source}: atteso un oggetto JSON`);
36
+ for (const [key, v] of Object.entries(value)) {
37
+ if (key === '$schema') {
38
+ if (typeof v !== 'string') throw new Error(`${source}: $schema deve essere una stringa`);
39
+ continue;
40
+ }
41
+ if (!Object.hasOwn(ENV_KEYS, key)) throw new Error(`${source}: chiave sconosciuta "${key}"`);
42
+ if (key === 'device' && v === 'vulkan') {
43
+ throw new Error(`${source}: Vulkan non è un provider ONNX autonomo. Usa device webgpu e webgpuBackend vulkan per forzarlo (setup-vulkan richiesto).`);
44
+ }
45
+ const choices = key === 'device' ? DEVICES : key === 'dtype' ? DTYPES
46
+ : key === 'webgpuBackend' ? WEBGPU_BACKENDS : key === 'fallback' ? ['error', 'cpu'] : null;
47
+ if (choices && !choices.includes(v)) throw new Error(`${source}: ${key} deve essere uno di: ${choices.join(', ')}`);
48
+ if (key === 'offline' && typeof v !== 'boolean') throw new Error(`${source}: offline deve essere booleano`);
49
+ if (['cacheDir', 'serviceDir'].includes(key) && (typeof v !== 'string' || !v.trim())) throw new Error(`${source}: ${key} deve essere un percorso non vuoto`);
50
+ if (key === 'cpuThreads' && (!Number.isInteger(v) || v < 0 || v > 256)) throw new Error(`${source}: cpuThreads deve essere un intero tra 0 e 256`);
51
+ if (key === 'servicePort' && (!Number.isInteger(v) || (v !== 0 && (v < 1024 || v > 65535)))) throw new Error(`${source}: servicePort deve essere 0 (automatico) oppure un intero tra 1024 e 65535`);
52
+ if (key === 'serviceIdleMs' && (!Number.isInteger(v) || v < 1000 || v > 86_400_000)) throw new Error(`${source}: serviceIdleMs deve essere un intero tra 1000 e 86400000`);
53
+ }
54
+ return value;
55
+ }
56
+
57
+ export function parseSetting(key, text) {
58
+ if (!Object.hasOwn(ENV_KEYS, key)) throw new Error(`chiave sconosciuta "${key}"`);
59
+ let value = text;
60
+ if (key === 'offline') {
61
+ if (!['true', 'false', '1', '0'].includes(text)) throw new Error('offline: usa true oppure false');
62
+ value = text === 'true' || text === '1';
63
+ } else if (['cpuThreads', 'servicePort', 'serviceIdleMs'].includes(key)) {
64
+ if (!/^\d+$/.test(text)) throw new Error(`${key}: atteso un intero`);
65
+ value = Number(text);
66
+ }
67
+ validateConfig({ [key]: value });
68
+ return value;
69
+ }
70
+
71
+ async function readConfig(file) {
72
+ try {
73
+ return validateConfig(JSON.parse(await fs.readFile(file, 'utf8')), file);
74
+ } catch (e) {
75
+ if (e.code === 'ENOENT') return {};
76
+ throw new Error(`configurazione non valida: ${e.message}`);
77
+ }
78
+ }
79
+
80
+ export async function loadConfig({ env = process.env, home = os.homedir() } = {}) {
81
+ const file = configPath(env, home);
82
+ const stored = await readConfig(file);
83
+ const config = { ...defaults(home), ...stored };
84
+ delete config.$schema;
85
+ const sources = Object.fromEntries(Object.keys(config).map((key) => [key, key in stored ? file : 'default']));
86
+ for (const [key, name] of Object.entries(ENV_KEYS)) {
87
+ if (env[name] === undefined) continue;
88
+ config[key] = parseSetting(key, env[name]);
89
+ sources[key] = name;
90
+ }
91
+ validateConfig(config, file);
92
+ config.cacheDir = expandPath(config.cacheDir, home);
93
+ config.serviceDir = expandPath(config.serviceDir, home);
94
+ return { config: Object.freeze(config), sources, file };
95
+ }
96
+
97
+ // Salva solo valori espliciti, mai gli override dell'ambiente. Temp unico per
98
+ // non interferire con altre sessioni; rename atomico e permessi privati.
99
+ export async function updateConfig(patch, { unset = [], env = process.env, home = os.homedir() } = {}) {
100
+ validateConfig(patch);
101
+ for (const key of unset) if (!Object.hasOwn(ENV_KEYS, key)) throw new Error(`chiave sconosciuta "${key}"`);
102
+ const file = configPath(env, home);
103
+ const stored = { ...await readConfig(file), ...patch };
104
+ for (const key of unset) delete stored[key];
105
+ validateConfig(stored, file);
106
+ await fs.mkdir(path.dirname(file), { recursive: true });
107
+ const tmp = `${file}.${randomUUID()}.tmp`;
108
+ try {
109
+ await fs.writeFile(tmp, `${JSON.stringify(stored, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
110
+ await fs.rename(tmp, file);
111
+ } finally {
112
+ await fs.rm(tmp, { force: true });
113
+ }
114
+ return file;
115
+ }
116
+
117
+ export function formatConfig({ config, sources, file }) {
118
+ return [`file: ${file}`, ...Object.entries(config).map(([key, value]) =>
119
+ `${key.padEnd(12)}: ${value} (${sources[key]})`)].join('\n');
120
+ }
121
+
122
+ export async function configCommand(args, options = {}) {
123
+ const [action = 'show', key, ...rest] = args;
124
+ if (action === 'path' && args.length === 1) return configPath(options.env, options.home);
125
+ if (action === 'show' && args.length <= 1) return formatConfig(await loadConfig(options));
126
+ let file;
127
+ if (action === 'set' && key && rest.length) file = await updateConfig({ [key]: parseSetting(key, rest.join(' ')) }, options);
128
+ else if (action === 'unset' && key && !rest.length) file = await updateConfig({}, { ...options, unset: [key] });
129
+ else throw new Error('uso: config [show|path|set CHIAVE VALORE|unset CHIAVE]');
130
+ return `salvato: ${file}\nRiavvia pi (nuovo processo) per applicare. Per cambiare un profilo già attivo usa anche /codeindex service stop quando il servizio è inattivo. Gli override CODEINDEX_* hanno precedenza.\n${formatConfig(await loadConfig(options))}`;
131
+ }
package/src/doctor.js ADDED
@@ -0,0 +1,101 @@
1
+ // Diagnostica senza download per default. Le prove reali sono isolate in un
2
+ // processo figlio con timeout: un crash nativo non abbatte la sessione pi.
3
+ import { fork } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { loadConfig, parseSetting } from './config.js';
6
+ import { nativeBackends, formatProfile } from './backend.js';
7
+ import { vulkanStatus } from './vulkan.js';
8
+ import { createEmbeddingClient } from './service-client.js';
9
+
10
+ export function parseDoctorArgs(args) {
11
+ const options = { probe: false, all: false, json: false };
12
+ for (let i = 0; i < args.length; i++) {
13
+ const flag = args[i];
14
+ if (flag === '--probe') options.probe = true;
15
+ else if (flag === '--all') options.all = true;
16
+ else if (flag === '--json') options.json = true;
17
+ else if (flag === '--device' || flag === '--dtype' || flag === '--webgpu-backend') {
18
+ const value = args[++i];
19
+ if (!value || value.startsWith('--')) throw new Error(`manca il valore di ${flag}`);
20
+ const key = flag === '--webgpu-backend' ? 'webgpuBackend' : flag.slice(2);
21
+ options[key] = parseSetting(key, value);
22
+ } else throw new Error(`opzione doctor sconosciuta: ${flag}`);
23
+ }
24
+ if (options.all && !options.probe) throw new Error('--all richiede --probe');
25
+ if (options.all && options.device) throw new Error('scegli --all oppure --device, non entrambi');
26
+ if (options.webgpuBackend && !options.all) {
27
+ if (options.device && options.device !== 'webgpu') throw new Error('--webgpu-backend richiede --device webgpu oppure --all');
28
+ options.device = 'webgpu';
29
+ }
30
+ return options;
31
+ }
32
+
33
+ export function probeBackend({ device, dtype, webgpuBackend, timeoutMs = 120_000, env = process.env } = {}) {
34
+ return new Promise((resolve) => {
35
+ const childEnv = { ...env, CODEINDEX_FALLBACK: 'error' };
36
+ if (device) childEnv.CODEINDEX_DEVICE = device;
37
+ if (dtype) childEnv.CODEINDEX_DTYPE = dtype;
38
+ if (webgpuBackend) childEnv.CODEINDEX_WEBGPU_BACKEND = webgpuBackend;
39
+ const child = fork(fileURLToPath(new URL('./probe.js', import.meta.url)), [], { env: childEnv, silent: true, execArgv: [] });
40
+ let response = null, stderr = '', spawnError = null, timedOut = false;
41
+ const timer = setTimeout(() => { timedOut = true; child.kill('SIGKILL'); }, timeoutMs);
42
+ child.stderr.on('data', (s) => { stderr = (stderr + s).slice(-6000); });
43
+ child.stdout.on('data', () => {}); // drena eventuali messaggi del binding
44
+ child.on('message', (value) => { if (value && typeof value.ok === 'boolean') response = value; });
45
+ child.on('error', (e) => { spawnError = e.message; });
46
+ child.on('close', (code, signal) => {
47
+ clearTimeout(timer);
48
+ const ok = !timedOut && code === 0 && response?.ok === true;
49
+ resolve({ ...response, requested: device ?? 'auto', ok, code, signal,
50
+ ...(!ok ? { error: timedOut ? `timeout dopo ${timeoutMs} ms` : response?.error ?? spawnError ?? `processo terminato (${signal ?? code})`, stderr } : {}) });
51
+ });
52
+ });
53
+ }
54
+
55
+ export async function doctor({ probe = false, all = false, device, dtype, webgpuBackend, timeoutMs, env = process.env } = {}) {
56
+ if (device) parseSetting('device', device);
57
+ if (dtype) parseSetting('dtype', dtype);
58
+ if (webgpuBackend) {
59
+ parseSetting('webgpuBackend', webgpuBackend);
60
+ if (!all) {
61
+ if (device && device !== 'webgpu') throw new Error('--webgpu-backend richiede --device webgpu oppure --all');
62
+ device = 'webgpu';
63
+ }
64
+ }
65
+ const settings = await loadConfig({ env });
66
+ const native = nativeBackends();
67
+ const service = await createEmbeddingClient({ settings: () => settings, env }).status()
68
+ .catch((e) => ({ running: false, error: e.message }));
69
+ const report = { ok: !service.error, service, platform: process.platform, arch: process.arch, node: process.version,
70
+ runtime: native.version, settings,
71
+ requested: { device: device ?? settings.config.device, dtype: dtype ?? settings.config.dtype,
72
+ webgpuBackend: webgpuBackend ?? settings.config.webgpuBackend },
73
+ backends: native.backends, vulkan: vulkanStatus(), probes: [] };
74
+ if (probe) {
75
+ const devices = all ? native.backends.filter((b) => b.available).map((b) => b.device) : [device ?? settings.config.device];
76
+ for (const selected of devices) {
77
+ report.probes.push(await probeBackend({ device: selected, dtype, webgpuBackend, timeoutMs, env }));
78
+ }
79
+ report.ok = !service.error && report.probes.length > 0 && report.probes.every((p) => p.ok);
80
+ }
81
+ return report;
82
+ }
83
+
84
+ export function formatDoctor(report) {
85
+ const lines = [`${report.platform}/${report.arch} · Node ${report.node} · ONNX Runtime ${report.runtime}`,
86
+ `config: ${report.settings.file}`,
87
+ `richiesto: ${(report.requested ?? report.settings.config).device}/${(report.requested ?? report.settings.config).dtype} · fallback ${report.settings.config.fallback} · offline ${report.settings.config.offline}`,
88
+ ...report.backends.map((b) => `${b.device.padEnd(8)}: ${b.available ? 'presente' : 'assente'} — ${b.reason}`),
89
+ `backend WebGPU richiesto: ${(report.requested ?? report.settings.config).webgpuBackend} (solo quando il provider selezionato è WebGPU)`,
90
+ `Vulkan forzato: ${report.vulkan?.available ? 'presente' : 'assente'} — ${report.vulkan?.reason ?? 'non verificato'}`];
91
+ lines.push(`servizio condiviso: ${report.service?.error ?? (report.service?.running ? `PID ${report.service.pid} · ${report.service.pending} richieste · ${Math.round(report.service.rssBytes / 1024 / 1024)} MiB RSS` : report.service?.stopping ? 'in arresto' : 'fermo (nessun avvio da doctor)')}`);
92
+ if (!report.probes.length) lines.push('Nessuna inferenza eseguita. Usa doctor --probe (può scaricare i pesi), oppure --probe --all.');
93
+ for (const p of report.probes) {
94
+ lines.push(p.ok ? `PASS ${p.requested}: ${formatProfile(p.profile)} · ${p.profile.dims}d · ${p.elapsedMs} ms · cos(stesso) ${p.cosSame.toFixed(6)}`
95
+ : `FAIL ${p.requested}: ${p.error}`);
96
+ for (const a of p.attempts ?? []) if (!a.ok) lines.push(` ${formatProfile(a)}: ${a.error}`);
97
+ }
98
+ if (report.probes.length) lines.push('Probe isolati: possono caricare una copia aggiuntiva del modello mentre il servizio condiviso è attivo.');
99
+ lines.push('Un provider presente non prova compatibilità/velocità del modello; ORT può eseguire alcuni operatori su CPU.');
100
+ return lines.join('\n');
101
+ }
package/src/embed.js ADDED
@@ -0,0 +1,57 @@
1
+ // Facciata dei client pi/CLI. L'unica pipeline vive nel servizio condiviso;
2
+ // nessun errore di IPC attiva un fallback in-process.
3
+ import { createEmbeddingClient } from './service-client.js';
4
+ import { modelPath } from './model.js';
5
+ import { MODEL_ID, DIMS, QUERY_PREFIX, docPrefix } from './backend.js';
6
+
7
+ export { MODEL_ID, DIMS, QUERY_PREFIX, docPrefix };
8
+ const client = createEmbeddingClient();
9
+
10
+ export const getEmbeddingProfile = () => client.getProfile();
11
+ export const embeddingStatus = () => client.info();
12
+ export const getServiceStatus = () => client.status();
13
+ export const stopEmbeddingService = () => client.stop();
14
+
15
+ /** Testi -> Float32Array[] finiti, normalizzati L2 per default. */
16
+ export async function embed(texts, options = {}) {
17
+ if (!Array.isArray(texts) || texts.length === 0) return [];
18
+ return client.embed(texts, options);
19
+ }
20
+ export function embedQuery(q) { return embed([QUERY_PREFIX + q]); }
21
+ export function embedDocs(docs) {
22
+ return embed(docs.map((d) => typeof d === 'string' ? d : docPrefix(d.path, d.title) + d.text));
23
+ }
24
+ export function cosine(a, b) {
25
+ let dot = 0, na = 0, nb = 0;
26
+ for (let i = 0; i < a.length; i++) {
27
+ dot += a[i] * b[i]; na += a[i] * a[i]; nb += b[i] * b[i];
28
+ }
29
+ return dot / (Math.sqrt(na) * Math.sqrt(nb) || 1);
30
+ }
31
+ export async function modelDir() {
32
+ const profile = await client.getProfile();
33
+ return modelPath(client.info().config, profile.dtype);
34
+ }
35
+
36
+ export function formatServiceStatus(status) {
37
+ if (status.stopping) return `servizio condiviso: in arresto · PID ${status.pid} (rilascio delle risorse in corso)`;
38
+ if (!status.running) return `servizio condiviso: fermo (avvio automatico al prossimo embedding) · porta ${status.port}`;
39
+ const profile = status.profile;
40
+ return [`servizio condiviso: attivo · PID ${status.pid} · 127.0.0.1:${status.port}`,
41
+ `modello: ${profile ? `${profile.device}${profile.device === 'webgpu' ? `→${profile.webgpuBackend}` : ''}/${profile.dtype}` : status.initializing ? 'caricamento' : 'non caricato'}`,
42
+ `richieste attive/in coda: ${status.pending} · RSS servizio: ${Math.round(status.rssBytes / 1024 / 1024)} MiB`,
43
+ `uscita dopo inattività: ${status.idleMs} ms`].join('\n');
44
+ }
45
+ export async function serviceCommand(args, { service = client } = {}) {
46
+ const [action = 'status', ...rest] = args;
47
+ if (rest.some((arg) => arg !== '--json') || rest.length > 1 || (action !== 'status' && rest.length)) throw new Error('uso: service [status [--json]|stop]');
48
+ if (action === 'status') {
49
+ const status = await service.status();
50
+ return rest.includes('--json') ? JSON.stringify(status) : formatServiceStatus(status);
51
+ }
52
+ if (action === 'stop') {
53
+ const result = await service.stop();
54
+ return result.stopping ? `arresto del servizio condiviso richiesto (PID ${result.pid}); si riavvierà al prossimo embedding` : 'servizio condiviso già fermo';
55
+ }
56
+ throw new Error('uso: service [status [--json]|stop]');
57
+ }
package/src/eval.js ADDED
@@ -0,0 +1,72 @@
1
+ // Eval minima: query a risposta nota -> recall@k + latenza.
2
+ // Fixture JSONL: {"query": "...", "expects": ["substring/path"], "why": "..."}
3
+
4
+ import fs from 'node:fs/promises';
5
+ import { search } from './search.js';
6
+ import { repoRoot } from './util.js';
7
+
8
+ const percentile = (arr, p) => {
9
+ if (arr.length === 0) return 0;
10
+ const s = [...arr].sort((a, b) => a - b);
11
+ return s[Math.min(s.length - 1, Math.floor((s.length - 1) * p))];
12
+ };
13
+
14
+ export async function loadQueries(file) {
15
+ const text = await fs.readFile(file, 'utf8');
16
+ return text
17
+ .split('\n')
18
+ .filter((l) => l.trim() && !l.trim().startsWith('//'))
19
+ .map((l, i) => {
20
+ try {
21
+ return JSON.parse(l);
22
+ } catch (e) {
23
+ throw new Error(`riga ${i + 1} di ${file}: JSON non valido (${e.message})`);
24
+ }
25
+ });
26
+ }
27
+
28
+ export async function runEval({ root, file, k = 8, quiet = false } = {}) {
29
+ const repo = root ?? repoRoot(process.cwd());
30
+ if (!repo) throw new Error('non è un repo git: passa --root');
31
+ const queries = await loadQueries(file);
32
+
33
+ const t0 = performance.now();
34
+ await search('warmup', { root: repo, k: 1 }); // carica modello e indice
35
+ const coldMs = Math.round(performance.now() - t0);
36
+
37
+ const rows = [];
38
+ const latencies = [];
39
+ for (const q of queries) {
40
+ const res = await search(q.query, { root: repo, k });
41
+ latencies.push(res.tookMs);
42
+ const top = res.results;
43
+ const matches = (n) =>
44
+ top.slice(0, n).some((r) => q.expects.some((e) => r.path.toLowerCase().includes(e.toLowerCase())));
45
+ rows.push({
46
+ query: q.query,
47
+ hit3: matches(3),
48
+ hit8: matches(8),
49
+ rank: top.findIndex((r) => q.expects.some((e) => r.path.toLowerCase().includes(e.toLowerCase()))) + 1,
50
+ top: top.slice(0, 3).map((r) => `${r.path}:${r.start}-${r.end}`),
51
+ tookMs: res.tookMs,
52
+ why: q.why,
53
+ });
54
+ if (!quiet) {
55
+ const mark = rows.at(-1).hit3 ? 'OK ' : rows.at(-1).hit8 ? 'ok@8' : 'MISS';
56
+ console.log(`${mark} ${res.tookMs.toFixed(0).padStart(4)} ms · ${q.query}`);
57
+ console.log(` attesi: ${q.expects.join(' | ')}`);
58
+ console.log(` top: ${rows.at(-1).top.join(' · ')}`);
59
+ }
60
+ }
61
+
62
+ return {
63
+ repo,
64
+ queries: rows.length,
65
+ recall3: rows.filter((r) => r.hit3).length / rows.length,
66
+ recall8: rows.filter((r) => r.hit8).length / rows.length,
67
+ coldMs,
68
+ latencyP50: Math.round(percentile(latencies, 0.5)),
69
+ latencyP95: Math.round(percentile(latencies, 0.95)),
70
+ rows,
71
+ };
72
+ }