export declare const vectorHelpersTemplate = "/**\n * Shared vector helpers for the docs embedding index.\n *\n * Both the build-time indexer (scripts/build-docs-index.mts) and the runtime\n * search path (services/mcp/server.ts) import these so query vectors and stored\n * vectors get the EXACT same transform - any mismatch would silently corrupt\n * ranking. Vectors are Matryoshka-truncated to a smaller dimension and stored\n * as int8, which shrinks each vector ~40x (and the overall index ~20x once\n * chunk text is counted) versus raw JSON floats. Large doc sets used to produce\n * 100MB+ indexes that OOM'd or timed out serverless cold starts (the chat would\n * connect, then idle forever).\n */\n\n/** L2-normalize a vector in place and return it. */\nexport function l2normalize(vector: number[]): number[] {\n let norm = 0;\n for (let i = 0; i < vector.length; i++) norm += vector[i] * vector[i];\n norm = Math.sqrt(norm);\n if (norm === 0) return vector;\n for (let i = 0; i < vector.length; i++) vector[i] = vector[i] / norm;\n return vector;\n}\n\n/**\n * Matryoshka dimension reduction: keep the first `dims` components and\n * renormalize. text-embedding-3-* and gemini-embedding-001 are MRL-trained, so\n * a renormalized prefix is a valid lower-dimension embedding. When `dims` is\n * <= 0 or >= the vector length, the vector is returned normalized at full\n * length. Operates on a copy so the caller's vector is left untouched.\n */\nexport function reduceDims(vector: number[], dims: number): number[] {\n const target = dims > 0 && dims < vector.length ? dims : vector.length;\n return l2normalize(vector.slice(0, target));\n}\n\n/**\n * Quantize a float vector to int8 using a per-vector max-abs scale. Cosine\n * similarity is scale-invariant, so the scale factor never has to be stored:\n * cosine(query, scale * q) === cosine(query, q). Callers score directly against\n * the int8 array via cosineFloatInt8, so there is no dequantization step.\n */\nexport function quantizeInt8(vector: number[]): Int8Array {\n let maxAbs = 0;\n for (let i = 0; i < vector.length; i++) {\n const a = Math.abs(vector[i]);\n if (a > maxAbs) maxAbs = a;\n }\n const scale = maxAbs > 0 ? 127 / maxAbs : 0;\n const q = new Int8Array(vector.length);\n for (let i = 0; i < vector.length; i++) {\n // Clamp before assignment: Int8Array wraps out-of-range values (200 -> -56)\n // instead of clamping, which would flip a component's sign.\n let s = Math.round(vector[i] * scale);\n if (s > 127) s = 127;\n else if (s < -127) s = -127;\n q[i] = s;\n }\n return q;\n}\n\n/** Encode an int8 vector as base64 for compact JSON storage. */\nexport function encodeInt8(q: Int8Array): string {\n return Buffer.from(q.buffer, q.byteOffset, q.byteLength).toString(\"base64\");\n}\n\n/** Decode a base64 int8 vector produced by encodeInt8. */\nexport function decodeInt8(b64: string): Int8Array {\n const buf = Buffer.from(b64, \"base64\");\n return new Int8Array(buf.buffer, buf.byteOffset, buf.byteLength);\n}\n\n/**\n * Cosine similarity between a float query vector and an int8 stored vector.\n * Both must share the same length. The int8 scale cancels out in the\n * normalization, so scoring against the raw int8 array is exact up to the\n * quantization rounding error (negligible for top-K retrieval).\n */\nexport function cosineFloatInt8(query: number[], stored: Int8Array): number {\n const len = Math.min(query.length, stored.length);\n let dot = 0;\n let na = 0;\n let nb = 0;\n for (let i = 0; i < len; i++) {\n const x = query[i];\n const y = stored[i];\n dot += x * y;\n na += x * x;\n nb += y * y;\n }\n if (na === 0 || nb === 0) return 0;\n return dot / (Math.sqrt(na) * Math.sqrt(nb));\n}\n";