@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.
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env node
2
+ // Setup esplicito, nessun postinstall. Compila solo il binding Node, non ORT/Dawn.
3
+ // Sorgenti MIT upstream fissati; copia ORT privata con SONAME distinto, senza
4
+ // toccare node_modules o il contesto WebGPU di altre estensioni.
5
+ import fs from 'node:fs/promises';
6
+ import { existsSync } from 'node:fs';
7
+ import { createHash, randomUUID } from 'node:crypto';
8
+ import { createRequire } from 'node:module';
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { spawnSync } from 'node:child_process';
12
+ import { ORT_VERSION, ORT_SOURCE, VULKAN_REVISION, fileHash, ortLibrary, vulkanCacheDir,
13
+ vulkanStatus, verifyVulkanArtifacts } from '../src/vulkan.js';
14
+
15
+ const require = createRequire(import.meta.url);
16
+ const ADDON_URL = 'https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz';
17
+ const ADDON_INTEGRITY = '+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==';
18
+
19
+ export function patchBindingSource(source) {
20
+ const anchor = 'if (name == "preferredLayout" ||';
21
+ if (source.split(anchor).length !== 2) throw new Error('patch Vulkan: sorgente upstream inatteso');
22
+ return source.replace(anchor, 'if (name == "dawnBackendType" ||\n name == "preferredLayout" ||');
23
+ }
24
+
25
+ function run(command, args, options = {}) {
26
+ const result = spawnSync(command, args, { stdio: 'inherit', timeout: 180_000, ...options });
27
+ if (result.error) throw result.error;
28
+ if (result.status !== 0) throw new Error(`${command} terminato con ${result.signal ?? result.status}`);
29
+ }
30
+ async function download(url) {
31
+ const response = await fetch(url, { signal: AbortSignal.timeout(60_000) });
32
+ if (!response.ok) throw new Error(`download ${response.status}: ${url}`);
33
+ return Buffer.from(await response.arrayBuffer());
34
+ }
35
+
36
+ export async function setupVulkan(args = []) {
37
+ let headers, force = false;
38
+ for (let i = 0; i < args.length; i++) {
39
+ if (args[i] === '--force') force = true;
40
+ else if (args[i] === '--node-headers' && args[i + 1] && !args[i + 1].startsWith('--')) headers = path.resolve(args[++i]);
41
+ else throw new Error('uso: setup-vulkan [--force] [--node-headers DIR]');
42
+ }
43
+ if (process.platform !== 'linux' || process.arch !== 'x64') throw new Error('setup-vulkan inizialmente disponibile solo su Linux x64; Windows/macOS non verificati');
44
+ if (require('onnxruntime-node/package.json').version !== ORT_VERSION) throw new Error(`setup-vulkan richiede onnxruntime-node ${ORT_VERSION}`);
45
+ if (!force) {
46
+ const status = vulkanStatus();
47
+ if (status.available) {
48
+ try { await verifyVulkanArtifacts(status); console.log(`Vulkan: binding già pronto in ${status.dir}`); return; }
49
+ catch (e) { console.log(e.message); }
50
+ }
51
+ }
52
+ const compiler = process.env.CXX || 'c++';
53
+ run(compiler, ['--version'], { stdio: 'ignore' });
54
+ run('patchelf', ['--version'], { stdio: 'ignore' });
55
+ run('tar', ['--version'], { stdio: 'ignore' });
56
+ headers ??= [path.resolve(path.dirname(process.execPath), '../include/node'), '/usr/include/node']
57
+ .find((dir) => existsSync(path.join(dir, 'node_api.h')));
58
+ if (!headers || !existsSync(path.join(headers, 'node_api.h'))) throw new Error('header Node mancanti: installa gli header di sviluppo o usa --node-headers DIR');
59
+
60
+ const dir = vulkanCacheDir();
61
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
62
+ const work = await fs.mkdtemp(path.join(dir, '.build-'));
63
+ try {
64
+ const sourceManifest = JSON.parse(await fs.readFile(new URL('./vulkan-sources.json', import.meta.url), 'utf8'));
65
+ if (sourceManifest.revision !== ORT_SOURCE) throw new Error('revision sorgenti Vulkan incoerente');
66
+ console.log(`Vulkan: sorgenti ORT ${ORT_VERSION} @ ${ORT_SOURCE.slice(0, 12)} (hash Git verificati)`);
67
+ const sourceDir = path.join(work, 'source');
68
+ const entries = Object.entries(sourceManifest.files);
69
+ // Download limitato: solo binding e header pubblici, niente clone del repository.
70
+ for (let i = 0; i < entries.length; i += 4) await Promise.all(entries.slice(i, i + 4).map(async ([file, expected]) => {
71
+ const bytes = await download(`https://raw.githubusercontent.com/microsoft/onnxruntime/${ORT_SOURCE}/${file}`);
72
+ const actual = createHash('sha1').update(`blob ${bytes.length}\0`).update(bytes).digest('hex');
73
+ if (actual !== expected) throw new Error(`hash sorgente inatteso: ${file}`);
74
+ const dest = path.join(sourceDir, file);
75
+ await fs.mkdir(path.dirname(dest), { recursive: true });
76
+ await fs.writeFile(dest, bytes);
77
+ }));
78
+ const helper = path.join(sourceDir, 'js/node/src/session_options_helper.cc');
79
+ await fs.writeFile(helper, patchBindingSource(await fs.readFile(helper, 'utf8')));
80
+ const tarball = await download(ADDON_URL);
81
+ if (createHash('sha512').update(tarball).digest('base64') !== ADDON_INTEGRITY) throw new Error('integrità node-addon-api non valida');
82
+ const archive = path.join(work, 'addon-api.tgz');
83
+ await fs.writeFile(archive, tarball);
84
+ const addonDir = path.join(work, 'addon-api');
85
+ await fs.mkdir(addonDir);
86
+ run('tar', ['-xzf', archive, '--strip-components=1', '-C', addonDir]);
87
+
88
+ const out = path.join(work, 'artifact');
89
+ await fs.mkdir(out);
90
+ const sourceLibraryHash = await fileHash(ortLibrary());
91
+ const library = `libpi-codeindex-ort-vulkan-${sourceLibraryHash.slice(0, 16)}.so.1`;
92
+ const libraryFile = path.join(out, library);
93
+ await fs.copyFile(ortLibrary(), libraryFile);
94
+ run('patchelf', ['--set-soname', library, libraryFile]);
95
+ const nodeDir = path.join(sourceDir, 'js/node/src');
96
+ const cpp = (await fs.readdir(nodeDir)).filter((f) => f.endsWith('.cc')).sort().map((f) => path.join(nodeDir, f));
97
+ console.log('Vulkan: compilazione binding isolato (C++/N-API 6), runtime precompilato riutilizzato');
98
+ const bindingFile = path.join(out, 'onnxruntime_binding.node');
99
+ run(compiler, ['-shared', '-fPIC', '-fvisibility=hidden', '-fvisibility-inlines-hidden', '-std=c++17', '-O2',
100
+ '-DNAPI_VERSION=6', '-DUSE_WEBGPU', '-DNAPI_CPP_EXCEPTIONS', '-DORT_API_MANUAL_INIT',
101
+ `-I${headers}`, `-I${addonDir}`, `-I${path.join(sourceDir, 'include/onnxruntime')}`,
102
+ `-I${path.join(sourceDir, 'include/onnxruntime/core/session')}`, ...cpp,
103
+ `-L${out}`, `-l:${library}`, '-Wl,-rpath,$ORIGIN', '-o', bindingFile]);
104
+ await fs.copyFile(path.join(sourceDir, 'LICENSE'), path.join(out, 'LICENSE-onnxruntime'));
105
+ await fs.copyFile(path.join(addonDir, 'LICENSE.md'), path.join(out, 'LICENSE-node-addon-api'));
106
+ // Il controllo negativo non richiede GPU o pesi. Nuovo processo, nessun
107
+ // addon tenuto aperto durante la pubblicazione degli artefatti.
108
+ const moduleUrl = new URL('../src/vulkan.js', import.meta.url).href;
109
+ run(process.execPath, ['--input-type=module', '-e', `
110
+ import { createRequire } from 'node:module';
111
+ import { assertVulkanSelector } from ${JSON.stringify(moduleUrl)};
112
+ process.env.ORT_DISABLE_TELEMETRY = '1';
113
+ const req = createRequire(${JSON.stringify(import.meta.url)});
114
+ const ort = req('onnxruntime-node');
115
+ const binding = req(${JSON.stringify(bindingFile)});
116
+ binding.initOrtOnce(2, ort.Tensor, true);
117
+ assertVulkanSelector(binding);
118
+ console.log('Vulkan: controllo negativo del selettore superato');
119
+ `]);
120
+ const bindingHash = await fileHash(bindingFile);
121
+ const libraryHash = await fileHash(libraryFile);
122
+ // Directory nuova ad ogni build: non riusare/sovrascrivere artefatti
123
+ // corrotti o ancora mappati da un altro processo. Il manifest si pubblica ultimo.
124
+ const artifact = createHash('sha256').update(bindingHash + libraryHash + randomUUID()).digest('hex');
125
+ const target = path.join(dir, artifact);
126
+ await fs.rename(out, target);
127
+ const manifest = { runtimeVersion: ORT_VERSION, sourceRevision: ORT_SOURCE, revision: VULKAN_REVISION,
128
+ platform: process.platform, arch: process.arch, artifact, library, sourceLibraryHash, bindingHash, libraryHash };
129
+ const tmp = path.join(dir, `manifest.${randomUUID()}.tmp`);
130
+ try {
131
+ await fs.writeFile(tmp, JSON.stringify(manifest, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
132
+ await fs.rename(tmp, path.join(dir, 'manifest.json'));
133
+ } finally { await fs.rm(tmp, { force: true }); }
134
+ console.log(`Vulkan: installato in ${target}\nNessun file in node_modules modificato. Riavvia pi se già aperto. Ora esegui doctor --probe --device webgpu --webgpu-backend vulkan.`);
135
+ } finally { await fs.rm(work, { recursive: true, force: true }); }
136
+ }
137
+
138
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
139
+ setupVulkan(process.argv.slice(2)).catch((e) => { console.error(`setup-vulkan: ${e.message}`); process.exitCode = 1; });
140
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "revision": "f2c39fe2f838cf35ce7da92824f5a5e3ee6e88a7",
3
+ "files": {
4
+ "js/node/src/common.h": "b60d059bb673b9685b132a6189ff32976b492c04",
5
+ "js/node/src/inference_session_wrap.cc": "648a4586ababc855be8ca77a0ee655552c1eb71c",
6
+ "js/node/src/inference_session_wrap.h": "13132a8a2d744d5e471db72037c825928f19c7c9",
7
+ "js/node/src/main.cc": "c6c2a5cb8dc42c7e92b0c87181663b68c7d7c639",
8
+ "js/node/src/ort_instance_data.cc": "825cb953e01371baec5ab627e6f1ad92ecaee0cf",
9
+ "js/node/src/ort_instance_data.h": "68cb6028548d4cb2656d8f9e0d7cb44bfac75681",
10
+ "js/node/src/ort_singleton_data.cc": "d1d57a10340c5a90996b6bc5affbff8869a7a85c",
11
+ "js/node/src/ort_singleton_data.h": "a0577cb71e98bd37851852040ffc7e244b7d41dd",
12
+ "js/node/src/run_options_helper.cc": "352f828970c66767b1a80a34a29da06a272d111d",
13
+ "js/node/src/run_options_helper.h": "104fae150bb0eb6661b670926974c81eabc454fa",
14
+ "js/node/src/session_options_helper.cc": "9cfb44eb9acb75d38e845b30f847c6fdd3632592",
15
+ "js/node/src/session_options_helper.h": "c6338f28e03c6f21f657c1ff654864d696a5bbb8",
16
+ "js/node/src/tensor_helper.cc": "c4bf5628ce6fe0e662f60f2340adb83773dabc90",
17
+ "js/node/src/tensor_helper.h": "4a51e5240602accae3d5e75fd249f756ef34dea7",
18
+ "include/onnxruntime/core/session/onnxruntime_c_api.h": "a254e990237d5575186cf3871392db8c70835a56",
19
+ "include/onnxruntime/core/session/onnxruntime_cxx_api.h": "8a64ec9159063d97f3c5085716dd8b456bd9b768",
20
+ "include/onnxruntime/core/session/onnxruntime_cxx_inline.h": "0db4a7029a16389c4140c497840b83492313d0fb",
21
+ "include/onnxruntime/core/session/onnxruntime_ep_c_api.h": "0424a95c2d2c1102f328b4e3aeb77ec72601a120",
22
+ "include/onnxruntime/core/session/onnxruntime_float16.h": "625a1e82a6504577da24e83074643f7dfc13303d",
23
+ "include/onnxruntime/core/session/onnxruntime_error_code.h": "732fcd7a82eaa0208c7c97aa5f2fb2b0c694456f",
24
+ "include/onnxruntime/core/session/onnxruntime_run_options_config_keys.h": "f40ea6591059eb4afb574037732de8f7bc7575c1",
25
+ "include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h": "84e16a692d33178bcf29de7eb410c45680ab84b5",
26
+ "include/onnxruntime/core/providers/webgpu/webgpu_provider_factory.h": "0b45b847d651ff922a8e72856354e4ea6f95f439",
27
+ "LICENSE": "48bc6bb4996ac924359e8e28b9ae88970e5ed3fc"
28
+ }
29
+ }
package/src/backend.js ADDED
@@ -0,0 +1,120 @@
1
+ // Policy dei backend separata dall'inferenza: testabile senza GPU né download.
2
+ import { createRequire } from 'node:module';
3
+ import { existsSync } from 'node:fs';
4
+ import path from 'node:path';
5
+
6
+ export const MODEL_ID = 'onnx-community/embeddinggemma-2-ONNX';
7
+ export const DIMS = 768;
8
+ export const QUERY_PREFIX = 'task: code retrieval | query: ';
9
+ export const docPrefix = (filePath, title) => `title: ${title ?? filePath ?? 'none'} | text: `;
10
+ export const BACKENDS = ['cpu', 'cuda', 'webgpu', 'dml', 'coreml'];
11
+ const PLATFORMS = { cpu: ['linux', 'darwin', 'win32'], cuda: ['linux'],
12
+ webgpu: ['linux', 'darwin', 'win32'], dml: ['win32'], coreml: ['darwin'] };
13
+
14
+ export function nativeBackends() {
15
+ const req = createRequire(import.meta.url);
16
+ const ort = req('onnxruntime-node');
17
+ const dir = path.dirname(req.resolve('onnxruntime-node/package.json'));
18
+ const cudaLibrary = existsSync(path.join(dir, 'bin', 'napi-v6', process.platform, process.arch, 'libonnxruntime_providers_cuda.so'));
19
+ return { version: ort.env.versions.node, backends: backendInventory(ort.listSupportedBackends(), { cudaLibrary }) };
20
+ }
21
+
22
+ export function backendInventory(listed, { platform = process.platform, arch = process.arch, cudaLibrary = false } = {}) {
23
+ return BACKENDS.map((device) => {
24
+ const entry = listed.find((b) => b.name === device);
25
+ const platformOk = PLATFORMS[device].includes(platform) && (device !== 'cuda' || arch === 'x64');
26
+ const available = platformOk && Boolean(entry && (entry.bundled || (device === 'cuda' && cudaLibrary)));
27
+ const reason = !platformOk ? `non supportato su ${platform}/${arch}` : !entry ? 'non esposto dal binding nativo'
28
+ : !available ? 'provider non installato (CUDA/Linux: npm run setup-gpu)' : 'provider presente; inferenza da verificare';
29
+ return { device, available, reason };
30
+ });
31
+ }
32
+
33
+ export function backendPlan(config, inventory, platform = process.platform) {
34
+ const order = platform === 'win32' ? ['dml', 'webgpu', 'cpu']
35
+ : platform === 'darwin' ? ['coreml', 'webgpu', 'cpu'] : ['cuda', 'webgpu', 'cpu'];
36
+ const devices = config.device === 'auto' ? order.filter((d) => inventory.some((b) => b.device === d && b.available))
37
+ : [config.device, ...(config.fallback === 'cpu' && config.device !== 'cpu' ? ['cpu'] : [])];
38
+ return devices.map((device) => ({
39
+ device,
40
+ dtype: config.dtype === 'auto' ? (device === 'cpu' ? 'q8' : 'fp16') : config.dtype,
41
+ ...inventory.find((b) => b.device === device),
42
+ }));
43
+ }
44
+
45
+ export function webgpuBackendOf(profile) {
46
+ return profile.device === 'webgpu' ? (profile.webgpuBackend ?? 'auto') : null;
47
+ }
48
+
49
+ export function formatProfile(profile) {
50
+ return `${profile.device}${profile.device === 'webgpu' ? `→${webgpuBackendOf(profile)}` : ''}/${profile.dtype}`;
51
+ }
52
+
53
+ export function assertCompatibleIndex(index, profile) {
54
+ const mismatches = ['model', 'dtype', 'dims', 'device'].filter((key) => index[key] !== profile[key]);
55
+ if (webgpuBackendOf(index) !== webgpuBackendOf(profile)) mismatches.push('webgpuBackend');
56
+ if (mismatches.length) throw new Error(`indice incompatibile (${mismatches.join(', ')}): esegui /codeindex reindex o il comando index con questo profilo`);
57
+ }
58
+
59
+ export function validateEmbeddings(rows, count, dims = DIMS) {
60
+ if (!Array.isArray(rows) || rows.length !== count) throw new Error('embedding: numero di righe inatteso');
61
+ for (const row of rows) {
62
+ if (!row || row.length !== dims) throw new Error(`embedding: attese ${dims} dimensioni`);
63
+ let norm = 0;
64
+ for (const n of row) {
65
+ if (!Number.isFinite(n)) throw new Error('embedding: valori non finiti');
66
+ norm += n * n;
67
+ }
68
+ if (norm < 1e-12) throw new Error('embedding: vettore nullo');
69
+ }
70
+ return rows;
71
+ }
72
+
73
+ // Il profilo si fissa SOLO dopo un'inferenza riuscita. Non cambia in seguito,
74
+ // neanche dopo OOM/errori: nessun cambio di dtype durante un'indicizzazione.
75
+ export function createEmbeddingRuntime({ settings, inventory, initialize, platform = process.platform, log = () => {} }) {
76
+ let ready = null;
77
+ let loading = null;
78
+ let attempts = [];
79
+ let error = null;
80
+ let activeConfig = null;
81
+ async function get() {
82
+ if (ready) return ready;
83
+ if (loading) return loading;
84
+ loading = (async () => {
85
+ attempts = [];
86
+ error = null;
87
+ try {
88
+ const loaded = await settings();
89
+ activeConfig = loaded.config;
90
+ const plan = backendPlan(activeConfig, await inventory(), platform);
91
+ for (const candidate of plan) {
92
+ const profile = Object.freeze({ model: MODEL_ID, dims: DIMS, device: candidate.device, dtype: candidate.dtype,
93
+ webgpuBackend: candidate.device === 'webgpu' ? (activeConfig.webgpuBackend ?? 'auto') : null });
94
+ try {
95
+ if (!candidate.available) throw new Error(candidate.reason ?? 'provider assente');
96
+ const pipe = await initialize(profile, activeConfig);
97
+ attempts.push({ ...profile, ok: true });
98
+ ready = { pipe, profile, config: activeConfig };
99
+ log(`[codeindex] modello pronto · ${formatProfile(profile)}`);
100
+ return ready;
101
+ } catch (e) {
102
+ const message = String(e.message ?? e).slice(0, 1000);
103
+ attempts.push({ ...profile, ok: false, error: message });
104
+ log(`[codeindex] ${formatProfile(profile)} non disponibile: ${message}`);
105
+ }
106
+ }
107
+ throw new Error(`nessun backend utilizzabile: ${attempts.map((a) => `${formatProfile(a)}: ${a.error}`).join('; ')}. Usa doctor --probe per la diagnostica.`);
108
+ } catch (e) {
109
+ error = e.message;
110
+ throw e;
111
+ }
112
+ })();
113
+ try { return await loading; } finally { loading = null; }
114
+ }
115
+ return {
116
+ get,
117
+ info: () => ({ profile: ready?.profile ?? null, config: activeConfig, loading: Boolean(loading),
118
+ attempts: attempts.map((a) => ({ ...a })), error }),
119
+ };
120
+ }
package/src/chunker.js ADDED
@@ -0,0 +1,276 @@
1
+ // Chunker declaration-aware + fallback a finestre.
2
+ // Puro: nessun I/O, nessuna dipendenza esterna. Versionato: cambiare la logica
3
+ // richiede un bump di CHUNKER_VERSION (invalida i vettori esistenti).
4
+
5
+ export const CHUNKER_VERSION = 1;
6
+ export const MAX_LINES = 120; // oltre: split a finestre
7
+ export const SPLIT_LINES = 60;
8
+ export const MAX_EMBED_CHARS = 4000;
9
+
10
+ export const LANG_BY_EXT = {
11
+ '.js': 'js', '.mjs': 'js', '.cjs': 'js', '.jsx': 'js',
12
+ '.ts': 'js', '.tsx': 'js', '.mts': 'js', '.cts': 'js',
13
+ '.py': 'py', '.pyi': 'py',
14
+ '.go': 'go',
15
+ '.rs': 'rust',
16
+ '.c': 'c', '.h': 'c',
17
+ '.cc': 'cpp', '.cpp': 'cpp', '.cxx': 'cpp', '.hpp': 'cpp', '.hh': 'cpp', '.hxx': 'cpp',
18
+ '.cs': 'csharp',
19
+ '.srw': 'powerbuilder', '.sru': 'powerbuilder', '.srf': 'powerbuilder',
20
+ '.srs': 'powerbuilder', '.srm': 'powerbuilder', '.srd': 'powerbuilder',
21
+ '.sra': 'powerbuilder',
22
+ '.md': 'md', '.mdx': 'md',
23
+ };
24
+
25
+ export function langOf(filePath) {
26
+ const m = /\.[^./]+$/.exec(filePath);
27
+ return m ? (LANG_BY_EXT[m[0].toLowerCase()] ?? 'generic') : 'generic';
28
+ }
29
+
30
+ const COMMENT = /^\s*(?:\/\/|\/\*|\*|#|--|;|'|<!--)/;
31
+
32
+ const JS_KEYWORDS = new Set([
33
+ 'if', 'for', 'while', 'switch', 'catch', 'return', 'function', 'else', 'do',
34
+ 'try', 'with', 'await', 'yield', 'typeof', 'new', 'super', 'this',
35
+ ]);
36
+
37
+ const C_KEYWORDS = new Set([
38
+ 'if', 'for', 'while', 'switch', 'return', 'sizeof', 'do', 'else', 'case',
39
+ 'typedef', 'struct', 'union', 'enum', 'class', 'namespace', 'template',
40
+ ]);
41
+
42
+ /** Riconosce l'inizio di una dichiarazione. Ritorna {symbol, kind} o null. */
43
+ export function matchBoundary(lang, line) {
44
+ let m;
45
+ switch (lang) {
46
+ case 'js': {
47
+ if ((m = /^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)/.exec(line)))
48
+ return { symbol: m[1], kind: 'function' };
49
+ if ((m = /^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)/.exec(line)))
50
+ return { symbol: m[1], kind: 'class' };
51
+ if ((m = /^\s*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(?:async\s*)?(?:function\b|\()/.exec(line)))
52
+ return { symbol: m[1], kind: 'function' };
53
+ if ((m = /^\s*(?:export\s+)?(?:declare\s+)?(?:interface|type|enum)\s+([A-Za-z_$][\w$]*)/.exec(line)))
54
+ return { symbol: m[1], kind: 'type' };
55
+ if ((m = /^ {2,}(?:(?:public|private|protected|static|async|readonly|override|get|set)\s+)*([A-Za-z_$][\w$]*)\s*\([^;]*\)\s*(?::[^{]+)?\{/.exec(line))) {
56
+ // `if (x) {`, `for (...) {` ecc. non sono metodi: spezzerebbero il corpo.
57
+ if (JS_KEYWORDS.has(m[1])) return null;
58
+ return { symbol: m[1], kind: 'method' };
59
+ }
60
+ return null;
61
+ }
62
+ case 'py': {
63
+ if (/^\s*(?:async\s+)?def\s+(\w+)/.test(line)) {
64
+ const mm = /^\s*(?:async\s+)?def\s+(\w+)/.exec(line);
65
+ return { symbol: mm[1], kind: 'function' };
66
+ }
67
+ if ((m = /^class\s+(\w+)/.exec(line))) return { symbol: m[1], kind: 'class' };
68
+ return null;
69
+ }
70
+ case 'go': {
71
+ if ((m = /^func\s+(?:\([^)]*\)\s*)?(\w+)/.exec(line))) return { symbol: m[1], kind: 'function' };
72
+ if ((m = /^type\s+(\w+)/.exec(line))) return { symbol: m[1], kind: 'type' };
73
+ return null;
74
+ }
75
+ case 'rust': {
76
+ if ((m = /^\s*(?:pub(?:\([^)]*\))?\s+)?(?:const\s+)?(?:unsafe\s+)?(?:async\s+)?(?:extern\s+"[^"]*"\s+)?fn\s+(\w+)/.exec(line)))
77
+ return { symbol: m[1], kind: 'function' };
78
+ if ((m = /^\s*(?:pub(?:\([^)]*\))?\s+)?(struct|enum|union|trait|mod)\s+(\w+)/.exec(line)))
79
+ return { symbol: m[2], kind: m[1] };
80
+ if ((m = /^\s*(?:pub(?:\([^)]*\))?\s+)?impl\b[^{]*?(?:for\s+)?([\w:<>, ]+?)\s*(?:where|$|\{)/.exec(line)))
81
+ return { symbol: m[1].trim(), kind: 'impl' };
82
+ return null;
83
+ }
84
+ case 'c':
85
+ case 'cpp': {
86
+ if (line.startsWith('#') || /^\s*\/[/*]/.test(line)) return null;
87
+ if ((m = /^\s*(?:typedef\s+)?(?:class|struct|enum(?:\s+class)?|namespace|union)\s+(\w+)/.exec(line)))
88
+ return { symbol: m[1], kind: 'type' };
89
+ if ((m = /^([A-Za-z_][\w\s\*&:<>,]*?[\s\*&])([A-Za-z_]\w*)\s*\([^;]*$/.exec(line))) {
90
+ if (C_KEYWORDS.has(m[2])) return null;
91
+ if (/^\s*(if|for|while|switch|return|else|do|case)\b/.test(line)) return null;
92
+ return { symbol: m[2], kind: 'function' };
93
+ }
94
+ return null;
95
+ }
96
+ case 'csharp': {
97
+ if ((m = /^\s*(?:\[[^\]]*\]\s*)*(?:(?:public|private|protected|internal|static|sealed|abstract|partial|readonly|unsafe|file)\s+)*(?:class|struct|interface|enum|record)\s+(\w+)/.exec(line)))
98
+ return { symbol: m[1], kind: 'type' };
99
+ if ((m = /^\s*(?:(?:public|private|protected|internal|static|async|virtual|override|sealed|new|extern|unsafe|partial|abstract)\s+)+[\w<>\[\],\.\?]+\s+(\w+)\s*\(/.exec(line)))
100
+ return { symbol: m[1], kind: 'function' };
101
+ if ((m = /^\s*(?:(?:public|private|protected|internal|static|virtual|override|abstract|sealed)\s+)+[\w<>\[\],\.\?]+\s+(\w+)\s*\{\s*(?:get|set)/.exec(line)))
102
+ return { symbol: m[1], kind: 'property' };
103
+ return null;
104
+ }
105
+ case 'powerbuilder': {
106
+ if ((m = /^global\s+type\s+(\w+)/i.exec(line))) return { symbol: m[1], kind: 'type' };
107
+ if (/^forward\s+prototypes/i.test(line)) return { symbol: 'forward prototypes', kind: 'prototypes' };
108
+ if ((m = /^(?:public|private|protected|global)\s+function\s+[\w\[\]]+\s+(\w+)/i.exec(line)))
109
+ return { symbol: m[1], kind: 'function' };
110
+ if ((m = /^event\s+(\w+)/i.exec(line))) return { symbol: m[1], kind: 'event' };
111
+ if ((m = /^on\s+(\w+)/i.exec(line))) return { symbol: m[1], kind: 'event-handler' };
112
+ return null;
113
+ }
114
+ case 'md': {
115
+ if ((m = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line)))
116
+ return { symbol: m[2].slice(0, 80), kind: `h${m[1].length}` };
117
+ return null;
118
+ }
119
+ default:
120
+ return null;
121
+ }
122
+ }
123
+
124
+ function trimRange(lines, start, end) {
125
+ while (start <= end && lines[start - 1].trim() === '') start++;
126
+ while (end >= start && lines[end - 1].trim() === '') end--;
127
+ return { start, end };
128
+ }
129
+
130
+ /** Estende l'inizio verso l'alto includendo il blocco di commento adiacente. */
131
+ function includeLeadingComments(lines, start) {
132
+ let s = start;
133
+ while (s > 1 && COMMENT.test(lines[s - 2]) && lines[s - 2].trim() !== '') {
134
+ s--;
135
+ if (s > 1 && lines[s - 2].trim() === '') break; // una riga vuota chiude il blocco
136
+ }
137
+ return s;
138
+ }
139
+
140
+ /** Split di un range troppo lungo: finestre a ridosso di righe vuote. */
141
+ function splitRange(lines, start, end, kind) {
142
+ const out = [];
143
+ let cur = start;
144
+ while (cur <= end) {
145
+ const targetEnd = Math.min(cur + SPLIT_LINES - 1, end);
146
+ let cut = targetEnd;
147
+ if (targetEnd < end) {
148
+ for (let l = targetEnd; l > cur + 10; l--) {
149
+ if (lines[l - 1].trim() === '') {
150
+ cut = l;
151
+ break;
152
+ }
153
+ }
154
+ }
155
+ out.push({ start: cur, end: cut, kind });
156
+ cur = cut + 1;
157
+ while (cur <= end && lines[cur - 1].trim() === '') cur++;
158
+ }
159
+ return out;
160
+ }
161
+
162
+ /**
163
+ * Fonduzione di chunk adiacenti: riduce il numero di forward pass (le sequenze
164
+ * corte sprecano padding) senza perdere righe. Con `smallOnly`, fonde solo se
165
+ * ENTRAMBI i chunk sono più corti della soglia (per non perdere granularità
166
+ * sulle dichiarazioni di codice).
167
+ */
168
+ export function mergeRanges(lines, ranges, { maxLines = 40, maxChars = 1000, smallOnly = 0 } = {}) {
169
+ const len = (r) => r.end - r.start + 1;
170
+ const out = [];
171
+ for (const r of ranges) {
172
+ const prev = out[out.length - 1];
173
+ if (!prev) {
174
+ out.push({ ...r, symbols: r.symbol ? [r.symbol] : [] });
175
+ continue;
176
+ }
177
+ const joinedLines = r.end - prev.start + 1;
178
+ const joinedChars = lines.slice(prev.start - 1, r.end).join('\n').length;
179
+ if (prev.kind === 'preamble') {
180
+ out.push({ ...r, symbols: r.symbol ? [r.symbol] : [] });
181
+ continue;
182
+ }
183
+ const smallEnough = !smallOnly || (len(prev) <= smallOnly && len(r) <= smallOnly);
184
+ if (smallEnough && joinedLines <= maxLines && joinedChars <= maxChars) {
185
+ prev.end = r.end;
186
+ if (r.symbol) prev.symbols.push(r.symbol);
187
+ if (prev.kind !== r.kind) prev.kind = 'group';
188
+ prev.symbol = prev.symbols[0] ?? null;
189
+ } else {
190
+ out.push({ ...r, symbols: r.symbol ? [r.symbol] : [] });
191
+ }
192
+ }
193
+ return out.map((c) => ({
194
+ start: c.start,
195
+ end: c.end,
196
+ symbol: c.symbol,
197
+ symbols: c.symbols,
198
+ kind: c.kind,
199
+ }));
200
+ }
201
+
202
+ /** Politica di merge per linguaggio. Markdown: sezioni intere. Codice: solo micro-chunk. */
203
+ const MERGE_POLICY = {
204
+ md: { maxLines: 80, maxChars: 1500 },
205
+ generic: { maxLines: 0, maxChars: 0 }, // finestre da 60 righe: già ottimali
206
+ default: { maxLines: 30, maxChars: 900, smallOnly: 6 },
207
+ };
208
+
209
+ /**
210
+ * Testo -> chunk. Copertura completa del file (nessuna riga persa).
211
+ * @returns {{start:number,end:number,symbol:string|null,kind:string}[]}
212
+ */
213
+ export function chunkText(text, lang = 'generic') {
214
+ const lines = text.split('\n');
215
+ if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
216
+ if (lines.length === 0) return [];
217
+ return chunkLines(lines, lang);
218
+ }
219
+
220
+ /** Variante su array di righe (per test e per il chunker di file già letti). */
221
+ export function chunkLines(lines, lang = 'generic') {
222
+ if (lines.length === 0) return [];
223
+
224
+ const boundaries = [];
225
+ for (let i = 0; i < lines.length; i++) {
226
+ const hit = matchBoundary(lang, lines[i]);
227
+ if (hit) boundaries.push({ line: i + 1, ...hit });
228
+ }
229
+
230
+ if (boundaries.length === 0) {
231
+ return splitRange(lines, 1, lines.length, 'window').map((r) => ({ ...r, symbol: null }));
232
+ }
233
+
234
+ const ranges = [];
235
+
236
+ // Preambolo (import, header, prime righe) prima della prima dichiarazione.
237
+ // Il confine include l'eventuale blocco di commento della dichiarazione:
238
+ // altrimenti il commento finirebbe nel chunk sbagliato.
239
+ const firstStart = includeLeadingComments(lines, boundaries[0].line);
240
+ const preamble = trimRange(lines, 1, firstStart - 1);
241
+ if (preamble.start <= preamble.end && lines.slice(preamble.start - 1, preamble.end).some((l) => l.trim() !== ''))
242
+ ranges.push({ ...preamble, symbol: null, kind: 'preamble' });
243
+
244
+ for (let i = 0; i < boundaries.length; i++) {
245
+ const b = boundaries[i];
246
+ const nextLine = i + 1 < boundaries.length ? boundaries[i + 1].line : lines.length + 1;
247
+ let start = includeLeadingComments(lines, b.line);
248
+ const prevEnd = ranges.length ? ranges[ranges.length - 1].end : 0;
249
+ if (start <= prevEnd) start = b.line; // non sovrapporre al chunk precedente
250
+ const r = trimRange(lines, start, Math.min(nextLine - 1, lines.length));
251
+ if (r.start > r.end) continue;
252
+ const body = r.end - r.start + 1;
253
+ if (body > MAX_LINES) {
254
+ for (const s of splitRange(lines, r.start, r.end, b.kind)) ranges.push({ ...s, symbol: b.symbol });
255
+ } else {
256
+ ranges.push({ ...r, symbol: b.symbol, kind: b.kind });
257
+ }
258
+ }
259
+ const policy = MERGE_POLICY[lang] ?? MERGE_POLICY.default;
260
+ if (!policy.maxLines) return mergeRanges(lines, ranges, { maxLines: 1, maxChars: 0 });
261
+ return mergeRanges(lines, ranges, policy);
262
+ }
263
+
264
+ /** Righe del chunk + eventuale troncamento a MAX_EMBED_CHARS. */
265
+ export function chunkCode(lines, chunk) {
266
+ const code = lines.slice(chunk.start - 1, chunk.end).join('\n');
267
+ return code.length > MAX_EMBED_CHARS ? code.slice(0, MAX_EMBED_CHARS) : code;
268
+ }
269
+
270
+ /** Payload testuale per l'embedding (senza prefisso modello: lo aggiunge il chiamante). */
271
+ export function chunkPayload(chunk, code) {
272
+ const symbols = chunk.symbols?.length ? chunk.symbols : chunk.symbol ? [chunk.symbol] : [];
273
+ const head = `${chunk.start}-${chunk.end} ${chunk.kind}${symbols.length ? ` ${symbols.slice(0, 5).join(', ')}` : ''}`;
274
+ const body = code.length > MAX_EMBED_CHARS ? code.slice(0, MAX_EMBED_CHARS) : code;
275
+ return `${head}\n${body}`;
276
+ }