@alucard_24/pi-codeindex 0.0.0-stage → 1.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/src/config.js ADDED
@@ -0,0 +1,126 @@
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
+ };
16
+
17
+ export function configPath(env = process.env, home = os.homedir()) {
18
+ if (env.CODEINDEX_CONFIG) return expandPath(env.CODEINDEX_CONFIG, home);
19
+ const base = env.XDG_CONFIG_HOME || (process.platform === 'win32' ? env.APPDATA : null) || path.join(home, '.config');
20
+ return path.resolve(base, 'pi-codeindex', 'config.json');
21
+ }
22
+
23
+ function expandPath(value, home = os.homedir()) {
24
+ return path.resolve(value === '~' ? home : value.startsWith('~/') ? path.join(home, value.slice(2)) : value);
25
+ }
26
+
27
+ export function defaults(home = os.homedir()) {
28
+ return { device: 'auto', dtype: 'auto', webgpuBackend: 'auto', fallback: 'error', offline: false,
29
+ cacheDir: path.join(home, '.cache', 'pi-codeindex', 'text-model'), cpuThreads: 0 };
30
+ }
31
+
32
+ export function validateConfig(value, source = 'config') {
33
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${source}: atteso un oggetto JSON`);
34
+ for (const [key, v] of Object.entries(value)) {
35
+ if (key === '$schema') {
36
+ if (typeof v !== 'string') throw new Error(`${source}: $schema deve essere una stringa`);
37
+ continue;
38
+ }
39
+ if (!Object.hasOwn(ENV_KEYS, key)) throw new Error(`${source}: chiave sconosciuta "${key}"`);
40
+ if (key === 'device' && v === 'vulkan') {
41
+ throw new Error(`${source}: Vulkan non è un provider ONNX autonomo. Usa device webgpu e webgpuBackend vulkan per forzarlo (setup-vulkan richiesto).`);
42
+ }
43
+ const choices = key === 'device' ? DEVICES : key === 'dtype' ? DTYPES
44
+ : key === 'webgpuBackend' ? WEBGPU_BACKENDS : key === 'fallback' ? ['error', 'cpu'] : null;
45
+ if (choices && !choices.includes(v)) throw new Error(`${source}: ${key} deve essere uno di: ${choices.join(', ')}`);
46
+ if (key === 'offline' && typeof v !== 'boolean') throw new Error(`${source}: offline deve essere booleano`);
47
+ if (key === 'cacheDir' && (typeof v !== 'string' || !v.trim())) throw new Error(`${source}: cacheDir deve essere un percorso non vuoto`);
48
+ if (key === 'cpuThreads' && (!Number.isInteger(v) || v < 0 || v > 256)) throw new Error(`${source}: cpuThreads deve essere un intero tra 0 e 256`);
49
+ }
50
+ return value;
51
+ }
52
+
53
+ export function parseSetting(key, text) {
54
+ if (!Object.hasOwn(ENV_KEYS, key)) throw new Error(`chiave sconosciuta "${key}"`);
55
+ let value = text;
56
+ if (key === 'offline') {
57
+ if (!['true', 'false', '1', '0'].includes(text)) throw new Error('offline: usa true oppure false');
58
+ value = text === 'true' || text === '1';
59
+ } else if (key === 'cpuThreads') {
60
+ if (!/^\d+$/.test(text)) throw new Error('cpuThreads: atteso un intero tra 0 e 256');
61
+ value = Number(text);
62
+ }
63
+ validateConfig({ [key]: value });
64
+ return value;
65
+ }
66
+
67
+ async function readConfig(file) {
68
+ try {
69
+ return validateConfig(JSON.parse(await fs.readFile(file, 'utf8')), file);
70
+ } catch (e) {
71
+ if (e.code === 'ENOENT') return {};
72
+ throw new Error(`configurazione non valida: ${e.message}`);
73
+ }
74
+ }
75
+
76
+ export async function loadConfig({ env = process.env, home = os.homedir() } = {}) {
77
+ const file = configPath(env, home);
78
+ const stored = await readConfig(file);
79
+ const config = { ...defaults(home), ...stored };
80
+ delete config.$schema;
81
+ const sources = Object.fromEntries(Object.keys(config).map((key) => [key, key in stored ? file : 'default']));
82
+ for (const [key, name] of Object.entries(ENV_KEYS)) {
83
+ if (env[name] === undefined) continue;
84
+ config[key] = parseSetting(key, env[name]);
85
+ sources[key] = name;
86
+ }
87
+ validateConfig(config, file);
88
+ config.cacheDir = expandPath(config.cacheDir, home);
89
+ return { config: Object.freeze(config), sources, file };
90
+ }
91
+
92
+ // Salva solo valori espliciti, mai gli override dell'ambiente. Temp unico per
93
+ // non interferire con altre sessioni; rename atomico e permessi privati.
94
+ export async function updateConfig(patch, { unset = [], env = process.env, home = os.homedir() } = {}) {
95
+ validateConfig(patch);
96
+ for (const key of unset) if (!Object.hasOwn(ENV_KEYS, key)) throw new Error(`chiave sconosciuta "${key}"`);
97
+ const file = configPath(env, home);
98
+ const stored = { ...await readConfig(file), ...patch };
99
+ for (const key of unset) delete stored[key];
100
+ validateConfig(stored, file);
101
+ await fs.mkdir(path.dirname(file), { recursive: true });
102
+ const tmp = `${file}.${randomUUID()}.tmp`;
103
+ try {
104
+ await fs.writeFile(tmp, `${JSON.stringify(stored, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
105
+ await fs.rename(tmp, file);
106
+ } finally {
107
+ await fs.rm(tmp, { force: true });
108
+ }
109
+ return file;
110
+ }
111
+
112
+ export function formatConfig({ config, sources, file }) {
113
+ return [`file: ${file}`, ...Object.entries(config).map(([key, value]) =>
114
+ `${key.padEnd(12)}: ${value} (${sources[key]})`)].join('\n');
115
+ }
116
+
117
+ export async function configCommand(args, options = {}) {
118
+ const [action = 'show', key, ...rest] = args;
119
+ if (action === 'path' && args.length === 1) return configPath(options.env, options.home);
120
+ if (action === 'show' && args.length <= 1) return formatConfig(await loadConfig(options));
121
+ let file;
122
+ if (action === 'set' && key && rest.length) file = await updateConfig({ [key]: parseSetting(key, rest.join(' ')) }, options);
123
+ else if (action === 'unset' && key && !rest.length) file = await updateConfig({}, { ...options, unset: [key] });
124
+ else throw new Error('uso: config [show|path|set CHIAVE VALORE|unset CHIAVE]');
125
+ return `salvato: ${file}\nRiavvia pi (nuovo processo) per applicare. Gli override CODEINDEX_* hanno precedenza.\n${formatConfig(await loadConfig(options))}`;
126
+ }
package/src/doctor.js ADDED
@@ -0,0 +1,96 @@
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
+
9
+ export function parseDoctorArgs(args) {
10
+ const options = { probe: false, all: false, json: false };
11
+ for (let i = 0; i < args.length; i++) {
12
+ const flag = args[i];
13
+ if (flag === '--probe') options.probe = true;
14
+ else if (flag === '--all') options.all = true;
15
+ else if (flag === '--json') options.json = true;
16
+ else if (flag === '--device' || flag === '--dtype' || flag === '--webgpu-backend') {
17
+ const value = args[++i];
18
+ if (!value || value.startsWith('--')) throw new Error(`manca il valore di ${flag}`);
19
+ const key = flag === '--webgpu-backend' ? 'webgpuBackend' : flag.slice(2);
20
+ options[key] = parseSetting(key, value);
21
+ } else throw new Error(`opzione doctor sconosciuta: ${flag}`);
22
+ }
23
+ if (options.all && !options.probe) throw new Error('--all richiede --probe');
24
+ if (options.all && options.device) throw new Error('scegli --all oppure --device, non entrambi');
25
+ if (options.webgpuBackend && !options.all) {
26
+ if (options.device && options.device !== 'webgpu') throw new Error('--webgpu-backend richiede --device webgpu oppure --all');
27
+ options.device = 'webgpu';
28
+ }
29
+ return options;
30
+ }
31
+
32
+ export function probeBackend({ device, dtype, webgpuBackend, timeoutMs = 120_000, env = process.env } = {}) {
33
+ return new Promise((resolve) => {
34
+ const childEnv = { ...env, CODEINDEX_FALLBACK: 'error' };
35
+ if (device) childEnv.CODEINDEX_DEVICE = device;
36
+ if (dtype) childEnv.CODEINDEX_DTYPE = dtype;
37
+ if (webgpuBackend) childEnv.CODEINDEX_WEBGPU_BACKEND = webgpuBackend;
38
+ const child = fork(fileURLToPath(new URL('./probe.js', import.meta.url)), [], { env: childEnv, silent: true, execArgv: [] });
39
+ let response = null, stderr = '', spawnError = null, timedOut = false;
40
+ const timer = setTimeout(() => { timedOut = true; child.kill('SIGKILL'); }, timeoutMs);
41
+ child.stderr.on('data', (s) => { stderr = (stderr + s).slice(-6000); });
42
+ child.stdout.on('data', () => {}); // drena eventuali messaggi del binding
43
+ child.on('message', (value) => { if (value && typeof value.ok === 'boolean') response = value; });
44
+ child.on('error', (e) => { spawnError = e.message; });
45
+ child.on('close', (code, signal) => {
46
+ clearTimeout(timer);
47
+ const ok = !timedOut && code === 0 && response?.ok === true;
48
+ resolve({ ...response, requested: device ?? 'auto', ok, code, signal,
49
+ ...(!ok ? { error: timedOut ? `timeout dopo ${timeoutMs} ms` : response?.error ?? spawnError ?? `processo terminato (${signal ?? code})`, stderr } : {}) });
50
+ });
51
+ });
52
+ }
53
+
54
+ export async function doctor({ probe = false, all = false, device, dtype, webgpuBackend, timeoutMs, env = process.env } = {}) {
55
+ if (device) parseSetting('device', device);
56
+ if (dtype) parseSetting('dtype', dtype);
57
+ if (webgpuBackend) {
58
+ parseSetting('webgpuBackend', webgpuBackend);
59
+ if (!all) {
60
+ if (device && device !== 'webgpu') throw new Error('--webgpu-backend richiede --device webgpu oppure --all');
61
+ device = 'webgpu';
62
+ }
63
+ }
64
+ const settings = await loadConfig({ env });
65
+ const native = nativeBackends();
66
+ const report = { ok: true, platform: process.platform, arch: process.arch, node: process.version,
67
+ runtime: native.version, settings,
68
+ requested: { device: device ?? settings.config.device, dtype: dtype ?? settings.config.dtype,
69
+ webgpuBackend: webgpuBackend ?? settings.config.webgpuBackend },
70
+ backends: native.backends, vulkan: vulkanStatus(), probes: [] };
71
+ if (probe) {
72
+ const devices = all ? native.backends.filter((b) => b.available).map((b) => b.device) : [device ?? settings.config.device];
73
+ for (const selected of devices) {
74
+ report.probes.push(await probeBackend({ device: selected, dtype, webgpuBackend, timeoutMs, env }));
75
+ }
76
+ report.ok = report.probes.length > 0 && report.probes.every((p) => p.ok);
77
+ }
78
+ return report;
79
+ }
80
+
81
+ export function formatDoctor(report) {
82
+ const lines = [`${report.platform}/${report.arch} · Node ${report.node} · ONNX Runtime ${report.runtime}`,
83
+ `config: ${report.settings.file}`,
84
+ `richiesto: ${(report.requested ?? report.settings.config).device}/${(report.requested ?? report.settings.config).dtype} · fallback ${report.settings.config.fallback} · offline ${report.settings.config.offline}`,
85
+ ...report.backends.map((b) => `${b.device.padEnd(8)}: ${b.available ? 'presente' : 'assente'} — ${b.reason}`),
86
+ `backend WebGPU richiesto: ${(report.requested ?? report.settings.config).webgpuBackend} (solo quando il provider selezionato è WebGPU)`,
87
+ `Vulkan forzato: ${report.vulkan?.available ? 'presente' : 'assente'} — ${report.vulkan?.reason ?? 'non verificato'}`];
88
+ if (!report.probes.length) lines.push('Nessuna inferenza eseguita. Usa doctor --probe (può scaricare i pesi), oppure --probe --all.');
89
+ for (const p of report.probes) {
90
+ lines.push(p.ok ? `PASS ${p.requested}: ${formatProfile(p.profile)} · ${p.profile.dims}d · ${p.elapsedMs} ms · cos(stesso) ${p.cosSame.toFixed(6)}`
91
+ : `FAIL ${p.requested}: ${p.error}`);
92
+ for (const a of p.attempts ?? []) if (!a.ok) lines.push(` ${formatProfile(a)}: ${a.error}`);
93
+ }
94
+ lines.push('Un provider presente non prova compatibilità/velocità del modello; ORT può eseguire alcuni operatori su CPU.');
95
+ return lines.join('\n');
96
+ }
package/src/embed.js ADDED
@@ -0,0 +1,88 @@
1
+ // Embedding locale in-process. Selezione del provider verificata con il
2
+ // modello reale; il profilo resta fisso per tutta la vita del processo.
3
+ import { loadConfig } from './config.js';
4
+ import { createEmbeddingRuntime, nativeBackends, validateEmbeddings, MODEL_ID, DIMS, QUERY_PREFIX, docPrefix } from './backend.js';
5
+ import { ensureTextModel, modelPath } from './model.js';
6
+
7
+ export { MODEL_ID, DIMS, QUERY_PREFIX, docPrefix };
8
+
9
+ const runtime = createEmbeddingRuntime({
10
+ settings: () => loadConfig(),
11
+ inventory: async () => nativeBackends().backends,
12
+ log: (s) => process.stderr.write(`${s}\n`),
13
+ initialize: async (profile, config) => {
14
+ const sessionOptions = {};
15
+ if (profile.webgpuBackend === 'vulkan') {
16
+ // Il setup mancante fallisce PRIMA di scaricare il modello. Backend unico,
17
+ // esplicito e non registrato come fallback dei provider standard.
18
+ const { registerVulkanBackend } = await import('./vulkan.js');
19
+ sessionOptions.executionProviders = [await registerVulkanBackend()];
20
+ }
21
+ const dir = await ensureTextModel(profile, config);
22
+ // Import lazy: config/status/doctor senza --probe non caricano il modello.
23
+ const { pipeline } = await import('@huggingface/transformers');
24
+ if (config.cpuThreads) sessionOptions.intraOpNumThreads = config.cpuThreads;
25
+ if (profile.device === 'dml') {
26
+ sessionOptions.executionMode = 'sequential';
27
+ sessionOptions.enableMemPattern = false;
28
+ }
29
+ let pipe;
30
+ try {
31
+ pipe = await pipeline('feature-extraction', dir, {
32
+ dtype: profile.dtype, device: profile.device, local_files_only: true,
33
+ session_options: sessionOptions,
34
+ });
35
+ const out = await pipe([QUERY_PREFIX + 'code retrieval smoke test'], { pooling: 'mean', normalize: true });
36
+ validateEmbeddings(out.tolist(), 1);
37
+ return pipe;
38
+ } catch (e) {
39
+ try { await pipe?.dispose(); } catch { /* conserva l'errore originale */ }
40
+ throw e;
41
+ }
42
+ },
43
+ });
44
+
45
+ export async function getEmbedder() {
46
+ return (await runtime.get()).pipe;
47
+ }
48
+
49
+ export async function getEmbeddingProfile() {
50
+ return (await runtime.get()).profile;
51
+ }
52
+
53
+ export function embeddingStatus() {
54
+ return runtime.info();
55
+ }
56
+
57
+ /** Testi -> Float32Array[] finiti, normalizzati L2 per default. */
58
+ export async function embed(texts, { normalize = true } = {}) {
59
+ if (!Array.isArray(texts) || texts.length === 0) return [];
60
+ const { pipe } = await runtime.get();
61
+ // Non fare fallback qui: l'indice è già associato al profilo selezionato.
62
+ const out = await pipe(texts, { pooling: 'mean', normalize });
63
+ const rows = validateEmbeddings(out.tolist(), texts.length);
64
+ return rows.map((row) => Float32Array.from(row));
65
+ }
66
+
67
+ export function embedQuery(q) {
68
+ return embed([QUERY_PREFIX + q]);
69
+ }
70
+
71
+ export function embedDocs(docs) {
72
+ return embed(docs.map((d) => typeof d === 'string' ? d : docPrefix(d.path, d.title) + d.text));
73
+ }
74
+
75
+ export function cosine(a, b) {
76
+ let dot = 0, na = 0, nb = 0;
77
+ for (let i = 0; i < a.length; i++) {
78
+ dot += a[i] * b[i];
79
+ na += a[i] * a[i];
80
+ nb += b[i] * b[i];
81
+ }
82
+ return dot / (Math.sqrt(na) * Math.sqrt(nb) || 1);
83
+ }
84
+
85
+ export async function modelDir() {
86
+ const { profile, config } = await runtime.get();
87
+ return modelPath(config, profile.dtype);
88
+ }
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
+ }