@gmickel/gno 1.45.1 → 2.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.
Files changed (236) hide show
  1. package/README.md +1 -1
  2. package/THIRD_PARTY_NOTICES.md +46 -0
  3. package/assets/skill/SKILL.md +7 -6
  4. package/assets/skill/cli-reference.md +14 -6
  5. package/assets/skill/mcp-reference.md +4 -1
  6. package/assets/spa-production.json.gz +0 -0
  7. package/browser-extension/artifacts/gno-browser-clipper-v2.0.0.zip +0 -0
  8. package/browser-extension/artifacts/gno-browser-clipper-v2.0.0.zip.sha256 +1 -0
  9. package/browser-extension/dist/chunk-4tc9v0ja.js +74 -0
  10. package/browser-extension/dist/manifest.json +1 -1
  11. package/browser-extension/dist/preview.html +1 -1
  12. package/browser-extension/dist/service-worker.js +32 -33
  13. package/bunfig.toml +2 -0
  14. package/package.json +40 -26
  15. package/spec/cli.md +30 -11
  16. package/spec/db/schema.sql +146 -1
  17. package/spec/mcp.md +26 -0
  18. package/src/app/context-runtime-types.ts +3 -0
  19. package/src/app/context-runtime.ts +2 -0
  20. package/src/cli/commands/ask.ts +6 -1
  21. package/src/cli/commands/daemon.ts +21 -8
  22. package/src/cli/commands/embed.ts +77 -41
  23. package/src/cli/commands/mcp/install.ts +20 -0
  24. package/src/cli/commands/mcp/paths.ts +25 -0
  25. package/src/cli/commands/mcp/status.ts +6 -0
  26. package/src/cli/detach.ts +3 -2
  27. package/src/cli/program.ts +6 -0
  28. package/src/config/types.ts +3 -3
  29. package/src/converters/adapters/markitdownTs/adapter.ts +1 -2
  30. package/src/converters/adapters/officeparser/adapter.ts +1 -2
  31. package/src/converters/versions.ts +6 -8
  32. package/src/core/context-evidence.ts +8 -4
  33. package/src/core/job-manager.ts +95 -13
  34. package/src/core/network-boundary-inventory.ts +10 -0
  35. package/src/core/shutdown-budget.ts +45 -0
  36. package/src/embed/backlog.ts +107 -4
  37. package/src/embed/batch.ts +42 -2
  38. package/src/embed/fingerprint.ts +16 -0
  39. package/src/embed/retry.ts +113 -5
  40. package/src/embed/variant-backlog.ts +105 -0
  41. package/src/embed/variant-plan.ts +62 -0
  42. package/src/embed/variant-retry.ts +113 -0
  43. package/src/ingestion/graph-reconciliation.ts +327 -0
  44. package/src/ingestion/sync.ts +9 -272
  45. package/src/llm/http-inference.ts +6 -0
  46. package/src/llm/httpEmbedding.ts +37 -6
  47. package/src/llm/httpGeneration.ts +18 -3
  48. package/src/llm/httpRerank.ts +23 -5
  49. package/src/llm/inference-cancellation.ts +168 -0
  50. package/src/llm/inference-scope.ts +202 -0
  51. package/src/llm/lazy-ports.ts +115 -0
  52. package/src/llm/native-worker/client.ts +541 -0
  53. package/src/llm/native-worker/dispatcher.ts +228 -0
  54. package/src/llm/native-worker/embedding-identity.ts +33 -0
  55. package/src/llm/native-worker/entry.ts +173 -0
  56. package/src/llm/native-worker/errors.ts +32 -0
  57. package/src/llm/native-worker/evaluation.ts +16 -0
  58. package/src/llm/native-worker/owned-exit.ts +108 -0
  59. package/src/llm/native-worker/owner.ts +141 -0
  60. package/src/llm/native-worker/ports.ts +317 -0
  61. package/src/llm/native-worker/protocol.ts +442 -0
  62. package/src/llm/native-worker/runtime-config.ts +92 -0
  63. package/src/llm/nodeLlamaCpp/adapter.ts +77 -20
  64. package/src/llm/nodeLlamaCpp/embedding.ts +130 -46
  65. package/src/llm/nodeLlamaCpp/generation.ts +34 -5
  66. package/src/llm/nodeLlamaCpp/lifecycle-options.ts +99 -0
  67. package/src/llm/nodeLlamaCpp/lifecycle.ts +209 -204
  68. package/src/llm/nodeLlamaCpp/rerank-capacity.ts +111 -0
  69. package/src/llm/nodeLlamaCpp/rerank.ts +118 -27
  70. package/src/llm/nodeLlamaCpp/simulator-handle.ts +73 -0
  71. package/src/llm/nodeLlamaCpp/simulator-install.ts +124 -0
  72. package/src/llm/nodeLlamaCpp/simulator-session.ts +240 -0
  73. package/src/llm/nodeLlamaCpp/simulator-types.ts +80 -0
  74. package/src/llm/types.ts +35 -5
  75. package/src/mcp/context.ts +27 -0
  76. package/src/mcp/http-transport.ts +12 -10
  77. package/src/mcp/server.ts +3 -0
  78. package/src/mcp/tool-profile.ts +30 -8
  79. package/src/mcp/tools/context.ts +8 -11
  80. package/src/mcp/tools/embed.ts +1 -1
  81. package/src/mcp/tools/index-cmd.ts +1 -1
  82. package/src/mcp/tools/index.ts +10 -8
  83. package/src/mcp/tools/query.ts +14 -30
  84. package/src/mcp/tools/vsearch.ts +1 -1
  85. package/src/pipeline/answer.ts +23 -3
  86. package/src/pipeline/claim-verifier.ts +6 -0
  87. package/src/pipeline/expansion.ts +43 -40
  88. package/src/pipeline/explain.ts +6 -2
  89. package/src/pipeline/filters.ts +63 -0
  90. package/src/pipeline/fusion.ts +29 -9
  91. package/src/pipeline/graph-retrieval.ts +29 -9
  92. package/src/pipeline/hybrid.ts +198 -55
  93. package/src/pipeline/hydration.ts +161 -0
  94. package/src/pipeline/owner-fusion.ts +87 -0
  95. package/src/pipeline/rerank.ts +35 -11
  96. package/src/pipeline/search.ts +13 -2
  97. package/src/pipeline/types.ts +5 -3
  98. package/src/pipeline/vsearch.ts +87 -7
  99. package/src/sdk/client.ts +47 -3
  100. package/src/sdk/embed.ts +63 -39
  101. package/src/serve/background-runtime.ts +1 -1
  102. package/src/serve/context.ts +41 -56
  103. package/src/serve/embed-scheduler.ts +58 -35
  104. package/src/serve/public/components/IndexingProgress.tsx +46 -60
  105. package/src/serve/public/globals.built.css +1 -1
  106. package/src/serve/public/lib/shiki-language-ids.ts +14 -0
  107. package/src/serve/resident-admission.ts +36 -36
  108. package/src/serve/resident-background-work.ts +20 -2
  109. package/src/serve/resident-request.ts +11 -5
  110. package/src/serve/resident-runtime.ts +97 -61
  111. package/src/serve/resident-shutdown.ts +153 -0
  112. package/src/serve/routes/api.ts +3 -1
  113. package/src/serve/server.ts +47 -26
  114. package/src/store/migrations/028-vector-variants.ts +54 -0
  115. package/src/store/migrations/029-graph-reference-state.ts +77 -0
  116. package/src/store/migrations/index.ts +4 -0
  117. package/src/store/sqlite/adapter.ts +251 -183
  118. package/src/store/sqlite/eligibility.ts +174 -0
  119. package/src/store/sqlite/graph-edge-application.ts +66 -0
  120. package/src/store/sqlite/graph-reference-state.ts +194 -0
  121. package/src/store/sqlite/legacy-vector-ownership.ts +79 -0
  122. package/src/store/types.ts +80 -12
  123. package/src/store/vector/eligibility.ts +36 -0
  124. package/src/store/vector/freshness.ts +33 -6
  125. package/src/store/vector/lazy.ts +81 -0
  126. package/src/store/vector/sqlite-vec.ts +106 -54
  127. package/src/store/vector/stats.ts +14 -3
  128. package/src/store/vector/types.ts +35 -2
  129. package/src/store/vector/variant-search.ts +192 -0
  130. package/src/store/vector/variants.ts +451 -0
  131. package/vendor/converters/markitdown-ts/LICENSE +21 -0
  132. package/vendor/converters/markitdown-ts/dist/index.cjs +1180 -0
  133. package/vendor/converters/markitdown-ts/dist/index.d.cts +46 -0
  134. package/vendor/converters/markitdown-ts/dist/index.d.mts +46 -0
  135. package/vendor/converters/markitdown-ts/dist/index.d.ts +46 -0
  136. package/vendor/converters/markitdown-ts/dist/index.mjs +1152 -0
  137. package/vendor/converters/markitdown-ts/package.json +77 -0
  138. package/vendor/converters/officeparser/LICENSE +21 -0
  139. package/vendor/converters/officeparser/dist/OfficeConverter.d.ts +47 -0
  140. package/vendor/converters/officeparser/dist/OfficeConverter.js +76 -0
  141. package/vendor/converters/officeparser/dist/OfficeGenerator.d.ts +23 -0
  142. package/vendor/converters/officeparser/dist/OfficeGenerator.js +73 -0
  143. package/vendor/converters/officeparser/dist/OfficeParser.d.ts +106 -0
  144. package/vendor/converters/officeparser/dist/OfficeParser.js +332 -0
  145. package/vendor/converters/officeparser/dist/cli.d.ts +28 -0
  146. package/vendor/converters/officeparser/dist/cli.js +381 -0
  147. package/vendor/converters/officeparser/dist/defaults.d.ts +41 -0
  148. package/vendor/converters/officeparser/dist/defaults.js +218 -0
  149. package/vendor/converters/officeparser/dist/generators/BaseGenerator.d.ts +107 -0
  150. package/vendor/converters/officeparser/dist/generators/BaseGenerator.js +248 -0
  151. package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.d.ts +82 -0
  152. package/vendor/converters/officeparser/dist/generators/ChunkingGenerator.js +797 -0
  153. package/vendor/converters/officeparser/dist/generators/CsvGenerator.d.ts +38 -0
  154. package/vendor/converters/officeparser/dist/generators/CsvGenerator.js +245 -0
  155. package/vendor/converters/officeparser/dist/generators/EpubGenerator.d.ts +43 -0
  156. package/vendor/converters/officeparser/dist/generators/EpubGenerator.js +315 -0
  157. package/vendor/converters/officeparser/dist/generators/HtmlGenerator.d.ts +59 -0
  158. package/vendor/converters/officeparser/dist/generators/HtmlGenerator.js +1942 -0
  159. package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.d.ts +96 -0
  160. package/vendor/converters/officeparser/dist/generators/MarkdownGenerator.js +1175 -0
  161. package/vendor/converters/officeparser/dist/generators/PdfGenerator.d.ts +22 -0
  162. package/vendor/converters/officeparser/dist/generators/PdfGenerator.js +194 -0
  163. package/vendor/converters/officeparser/dist/generators/RtfGenerator.d.ts +29 -0
  164. package/vendor/converters/officeparser/dist/generators/RtfGenerator.js +316 -0
  165. package/vendor/converters/officeparser/dist/generators/TextGenerator.d.ts +13 -0
  166. package/vendor/converters/officeparser/dist/generators/TextGenerator.js +201 -0
  167. package/vendor/converters/officeparser/dist/index.d.ts +60 -0
  168. package/vendor/converters/officeparser/dist/index.js +72 -0
  169. package/vendor/converters/officeparser/dist/index.mjs +18 -0
  170. package/vendor/converters/officeparser/dist/officeparser.browser.d.ts +2621 -0
  171. package/vendor/converters/officeparser/dist/officeparser.browser.iife.js +1336 -0
  172. package/vendor/converters/officeparser/dist/officeparser.browser.mjs +1335 -0
  173. package/vendor/converters/officeparser/dist/officeparser.browser.slim.d.ts +2621 -0
  174. package/vendor/converters/officeparser/dist/officeparser.browser.slim.iife.js +1336 -0
  175. package/vendor/converters/officeparser/dist/officeparser.browser.slim.mjs +1335 -0
  176. package/vendor/converters/officeparser/dist/parsers/CsvParser.d.ts +9 -0
  177. package/vendor/converters/officeparser/dist/parsers/CsvParser.js +115 -0
  178. package/vendor/converters/officeparser/dist/parsers/EpubParser.d.ts +8 -0
  179. package/vendor/converters/officeparser/dist/parsers/EpubParser.js +217 -0
  180. package/vendor/converters/officeparser/dist/parsers/ExcelParser.d.ts +32 -0
  181. package/vendor/converters/officeparser/dist/parsers/ExcelParser.js +736 -0
  182. package/vendor/converters/officeparser/dist/parsers/HtmlParser.d.ts +2 -0
  183. package/vendor/converters/officeparser/dist/parsers/HtmlParser.js +1287 -0
  184. package/vendor/converters/officeparser/dist/parsers/MarkdownParser.d.ts +2 -0
  185. package/vendor/converters/officeparser/dist/parsers/MarkdownParser.js +1272 -0
  186. package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.d.ts +31 -0
  187. package/vendor/converters/officeparser/dist/parsers/OpenOfficeParser.js +1819 -0
  188. package/vendor/converters/officeparser/dist/parsers/PdfParser.d.ts +67 -0
  189. package/vendor/converters/officeparser/dist/parsers/PdfParser.js +848 -0
  190. package/vendor/converters/officeparser/dist/parsers/PowerPointParser.d.ts +32 -0
  191. package/vendor/converters/officeparser/dist/parsers/PowerPointParser.js +950 -0
  192. package/vendor/converters/officeparser/dist/parsers/RtfParser.d.ts +187 -0
  193. package/vendor/converters/officeparser/dist/parsers/RtfParser.js +1801 -0
  194. package/vendor/converters/officeparser/dist/parsers/WordParser.d.ts +79 -0
  195. package/vendor/converters/officeparser/dist/parsers/WordParser.js +1177 -0
  196. package/vendor/converters/officeparser/dist/sbom.cdx.json +1763 -0
  197. package/vendor/converters/officeparser/dist/types.d.ts +2507 -0
  198. package/vendor/converters/officeparser/dist/types.js +107 -0
  199. package/vendor/converters/officeparser/dist/utils/astUtils.d.ts +16 -0
  200. package/vendor/converters/officeparser/dist/utils/astUtils.js +33 -0
  201. package/vendor/converters/officeparser/dist/utils/chartUtils.d.ts +6 -0
  202. package/vendor/converters/officeparser/dist/utils/chartUtils.js +257 -0
  203. package/vendor/converters/officeparser/dist/utils/configUtils.d.ts +44 -0
  204. package/vendor/converters/officeparser/dist/utils/configUtils.js +315 -0
  205. package/vendor/converters/officeparser/dist/utils/dateUtils.d.ts +17 -0
  206. package/vendor/converters/officeparser/dist/utils/dateUtils.js +69 -0
  207. package/vendor/converters/officeparser/dist/utils/envUtils.d.ts +29 -0
  208. package/vendor/converters/officeparser/dist/utils/envUtils.js +152 -0
  209. package/vendor/converters/officeparser/dist/utils/errorUtils.d.ts +72 -0
  210. package/vendor/converters/officeparser/dist/utils/errorUtils.js +245 -0
  211. package/vendor/converters/officeparser/dist/utils/imageUtils.d.ts +66 -0
  212. package/vendor/converters/officeparser/dist/utils/imageUtils.js +133 -0
  213. package/vendor/converters/officeparser/dist/utils/mathUtils.d.ts +42 -0
  214. package/vendor/converters/officeparser/dist/utils/mathUtils.js +385 -0
  215. package/vendor/converters/officeparser/dist/utils/moduleLoader.d.ts +18 -0
  216. package/vendor/converters/officeparser/dist/utils/moduleLoader.js +106 -0
  217. package/vendor/converters/officeparser/dist/utils/ocrUtils.d.ts +42 -0
  218. package/vendor/converters/officeparser/dist/utils/ocrUtils.js +428 -0
  219. package/vendor/converters/officeparser/dist/utils/sanitize.d.ts +148 -0
  220. package/vendor/converters/officeparser/dist/utils/sanitize.js +344 -0
  221. package/vendor/converters/officeparser/dist/utils/sheetUtils.d.ts +7 -0
  222. package/vendor/converters/officeparser/dist/utils/sheetUtils.js +35 -0
  223. package/vendor/converters/officeparser/dist/utils/styleMapper.d.ts +36 -0
  224. package/vendor/converters/officeparser/dist/utils/styleMapper.js +224 -0
  225. package/vendor/converters/officeparser/dist/utils/xmlUtils.d.ts +163 -0
  226. package/vendor/converters/officeparser/dist/utils/xmlUtils.js +461 -0
  227. package/vendor/converters/officeparser/dist/utils/zipUtils.d.ts +134 -0
  228. package/vendor/converters/officeparser/dist/utils/zipUtils.js +337 -0
  229. package/vendor/converters/officeparser/package.json +147 -0
  230. package/vendor/converters/upstream-manifest.json +124 -0
  231. package/vendor/dependency-fixes/README.md +77 -0
  232. package/vendor/dependency-fixes/vendor-converters.py +83 -0
  233. package/browser-extension/artifacts/gno-browser-clipper-v1.45.1.zip +0 -0
  234. package/browser-extension/artifacts/gno-browser-clipper-v1.45.1.zip.sha256 +0 -1
  235. package/browser-extension/dist/chunk-627emwpj.js +0 -75
  236. /package/browser-extension/dist/{chunk-ydfx5d7p.css → chunk-z74y8n8c.css} +0 -0
@@ -12,15 +12,42 @@ export function getStoredEmbeddingDimensions(
12
12
  db: Database,
13
13
  model: string
14
14
  ): number | undefined {
15
+ // Activated metadata supplies dimensions only, never query eligibility.
16
+ // The vector search still validates the actual inference identity and epoch.
17
+ const hasVariants = db
18
+ .prepare(
19
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'vector_partitions'"
20
+ )
21
+ .get();
22
+ if (hasVariants) {
23
+ const partitions = db
24
+ .prepare(`SELECT dimensions FROM vector_partitions
25
+ WHERE model = ? AND state = 'active' AND activated_epoch IS NOT NULL`)
26
+ .all(model) as { dimensions: number }[];
27
+ if (partitions.length) {
28
+ const dimensions = partitions[0]?.dimensions;
29
+ return dimensions !== undefined &&
30
+ Number.isSafeInteger(dimensions) &&
31
+ dimensions > 0 &&
32
+ partitions.every((partition) => partition.dimensions === dimensions)
33
+ ? dimensions
34
+ : undefined;
35
+ }
36
+ }
37
+ // Validate the entire model partition: never trust an arbitrary first blob.
15
38
  const row = db
16
- .prepare("SELECT embedding FROM content_vectors WHERE model = ? LIMIT 1")
17
- .get(model) as { embedding: Uint8Array } | undefined;
18
-
19
- if (!row?.embedding) {
39
+ .prepare(`SELECT MIN(length(embedding)) AS smallest,
40
+ MAX(length(embedding)) AS largest FROM content_vectors WHERE model = ?`)
41
+ .get(model) as { smallest: number | null; largest: number | null };
42
+ const bytes = row.smallest;
43
+ if (
44
+ !bytes ||
45
+ bytes !== row.largest ||
46
+ bytes % Float32Array.BYTES_PER_ELEMENT !== 0
47
+ ) {
20
48
  return undefined;
21
49
  }
22
-
23
- return row.embedding.byteLength / Float32Array.BYTES_PER_ELEMENT;
50
+ return bytes / Float32Array.BYTES_PER_ELEMENT;
24
51
  }
25
52
 
26
53
  export function getStoredEmbeddingFingerprint(
@@ -0,0 +1,81 @@
1
+ /** Native-free startup; dimensions are discovered only when vector work needs them. */
2
+ import type { Database } from "bun:sqlite";
3
+
4
+ import type { EmbeddingPort } from "../../llm/types";
5
+ import type { VectorIndexPort } from "./types";
6
+
7
+ import { getStoredEmbeddingDimensions } from "./freshness";
8
+ import { createVectorIndexPort } from "./sqlite-vec";
9
+
10
+ export async function createLazyVectorIndex(
11
+ db: Database,
12
+ model: string,
13
+ embedPort: EmbeddingPort
14
+ ): Promise<VectorIndexPort> {
15
+ const stored = getStoredEmbeddingDimensions(db, model);
16
+ let current: VectorIndexPort | undefined;
17
+ let loading: Promise<VectorIndexPort> | undefined;
18
+ let dirty = false;
19
+ const create = async (dimensions: number): Promise<VectorIndexPort> => {
20
+ const result = await createVectorIndexPort(db, { model, dimensions });
21
+ if (!result.ok) throw new Error(result.error.message);
22
+ current = result.value;
23
+ current.vecDirty ||= dirty;
24
+ return current;
25
+ };
26
+ if (stored !== undefined) return create(stored);
27
+ const get = (): Promise<VectorIndexPort> => {
28
+ if (current) return Promise.resolve(current);
29
+ loading ??= (async () => {
30
+ const result = await embedPort.init();
31
+ if (!result.ok) throw new Error(result.error.message);
32
+ return create(embedPort.dimensions());
33
+ })().finally(() => {
34
+ loading = undefined;
35
+ });
36
+ return loading;
37
+ };
38
+ return {
39
+ model,
40
+ get dimensions() {
41
+ return current?.dimensions ?? embedPort.dimensions();
42
+ },
43
+ get searchAvailable() {
44
+ return current?.searchAvailable ?? true;
45
+ },
46
+ get loadError() {
47
+ return current?.loadError;
48
+ },
49
+ get guidance() {
50
+ return current?.guidance;
51
+ },
52
+ get vecDirty() {
53
+ return current?.vecDirty ?? dirty;
54
+ },
55
+ set vecDirty(value) {
56
+ dirty = value;
57
+ if (current) current.vecDirty = value;
58
+ },
59
+ async upsertVectors(rows) {
60
+ return (await get()).upsertVectors(rows);
61
+ },
62
+ async upsertVectorsChecked(rows, checkpoint) {
63
+ const port = await get();
64
+ if (!port.upsertVectorsChecked)
65
+ throw new Error("Atomic vector checkpoint unavailable");
66
+ return port.upsertVectorsChecked(rows, checkpoint);
67
+ },
68
+ async deleteVectorsForMirror(hash) {
69
+ return (await get()).deleteVectorsForMirror(hash);
70
+ },
71
+ async searchNearest(embedding, k, options) {
72
+ return (await get()).searchNearest(embedding, k, options);
73
+ },
74
+ async rebuildVecIndex() {
75
+ return (await get()).rebuildVecIndex();
76
+ },
77
+ async syncVecIndex() {
78
+ return (await get()).syncVecIndex();
79
+ },
80
+ };
81
+ }
@@ -10,9 +10,16 @@ import type { Database } from "bun:sqlite";
10
10
  import { createHash } from "node:crypto";
11
11
 
12
12
  import type { StoreResult } from "../types";
13
- import type { VectorIndexPort, VectorRow, VectorSearchResult } from "./types";
13
+ import type {
14
+ VectorIndexPort,
15
+ VectorRow,
16
+ VectorSearchOptions,
17
+ VectorSearchResult,
18
+ } from "./types";
14
19
 
15
20
  import { err, ok } from "../types";
21
+ import { buildEligibleVectorQuery } from "./eligibility";
22
+ import { searchVectorVariants } from "./variant-search";
16
23
 
17
24
  // ─────────────────────────────────────────────────────────────────────────────
18
25
  // BLOB Encoding Helpers (avoid Buffer.buffer footgun)
@@ -162,6 +169,61 @@ export async function createVectorIndexPort(
162
169
  let vecDirty = false;
163
170
  let vecErrorLogged = false; // Rate-limit warning to once per run
164
171
 
172
+ function upsert(
173
+ rows: VectorRow[],
174
+ checkpoint?: (rows: VectorRow[]) => VectorRow[]
175
+ ): Promise<StoreResult<number>> {
176
+ // 1. Always store in content_vectors first (critical path)
177
+ try {
178
+ db.transaction(() => {
179
+ rows = checkpoint?.(rows) ?? rows;
180
+ for (const row of rows) {
181
+ upsertVectorStmt.run(
182
+ row.mirrorHash,
183
+ row.seq,
184
+ row.model,
185
+ row.embedFingerprint,
186
+ encodeEmbedding(row.embedding)
187
+ );
188
+ }
189
+ })();
190
+ } catch (e) {
191
+ return Promise.resolve(
192
+ err(
193
+ "VECTOR_WRITE_FAILED",
194
+ `Vector write failed: ${e instanceof Error ? e.message : String(e)}`,
195
+ e
196
+ )
197
+ );
198
+ }
199
+
200
+ // 2. Best-effort update vec0 (graceful degradation)
201
+ if (deleteVecChunkStmt && insertVecStmt) {
202
+ try {
203
+ db.transaction(() => {
204
+ for (const row of rows) {
205
+ const chunkId = `${row.mirrorHash}:${row.seq}`;
206
+ // sqlite-vec vec0 tables do not reliably support OR REPLACE semantics.
207
+ // Delete first, then insert the fresh vector row.
208
+ deleteVecChunkStmt.run(chunkId);
209
+ insertVecStmt.run(chunkId, encodeEmbedding(row.embedding));
210
+ }
211
+ })();
212
+ } catch (e) {
213
+ // Vec0 write failed - storage succeeded, search needs sync
214
+ vecDirty = true;
215
+ if (!vecErrorLogged) {
216
+ vecErrorLogged = true;
217
+ console.warn(
218
+ `[vec] Index write failed, will sync after embed: ${e instanceof Error ? e.message : String(e)}`
219
+ );
220
+ }
221
+ }
222
+ }
223
+
224
+ return Promise.resolve(ok(rows.length));
225
+ }
226
+
165
227
  return ok({
166
228
  searchAvailable,
167
229
  model,
@@ -175,56 +237,12 @@ export async function createVectorIndexPort(
175
237
  vecDirty = v;
176
238
  },
177
239
 
178
- upsertVectors(rows: VectorRow[]): Promise<StoreResult<void>> {
179
- // 1. Always store in content_vectors first (critical path)
180
- try {
181
- db.transaction(() => {
182
- for (const row of rows) {
183
- upsertVectorStmt.run(
184
- row.mirrorHash,
185
- row.seq,
186
- row.model,
187
- row.embedFingerprint,
188
- encodeEmbedding(row.embedding)
189
- );
190
- }
191
- })();
192
- } catch (e) {
193
- return Promise.resolve(
194
- err(
195
- "VECTOR_WRITE_FAILED",
196
- `Vector write failed: ${e instanceof Error ? e.message : String(e)}`,
197
- e
198
- )
199
- );
200
- }
201
-
202
- // 2. Best-effort update vec0 (graceful degradation)
203
- if (deleteVecChunkStmt && insertVecStmt) {
204
- try {
205
- db.transaction(() => {
206
- for (const row of rows) {
207
- const chunkId = `${row.mirrorHash}:${row.seq}`;
208
- // sqlite-vec vec0 tables do not reliably support OR REPLACE semantics.
209
- // Delete first, then insert the fresh vector row.
210
- deleteVecChunkStmt.run(chunkId);
211
- insertVecStmt.run(chunkId, encodeEmbedding(row.embedding));
212
- }
213
- })();
214
- } catch (e) {
215
- // Vec0 write failed - storage succeeded, search needs sync
216
- vecDirty = true;
217
- if (!vecErrorLogged) {
218
- vecErrorLogged = true;
219
- console.warn(
220
- `[vec] Index write failed, will sync after embed: ${e instanceof Error ? e.message : String(e)}`
221
- );
222
- }
223
- }
224
- }
225
-
226
- return Promise.resolve(ok(undefined));
240
+ upsertVectors(rows) {
241
+ return upsert(rows).then((result) =>
242
+ result.ok ? ok(undefined) : result
243
+ );
227
244
  },
245
+ upsertVectorsChecked: upsert,
228
246
 
229
247
  deleteVectorsForMirror(mirrorHash: string): Promise<StoreResult<void>> {
230
248
  // 1. Always delete from content_vectors first
@@ -254,10 +272,7 @@ export async function createVectorIndexPort(
254
272
  searchNearest(
255
273
  embedding: Float32Array,
256
274
  k: number,
257
- searchOptions?: {
258
- minScore?: number;
259
- allowedMirrorHashes?: string[];
260
- }
275
+ searchOptions?: VectorSearchOptions
261
276
  ): Promise<StoreResult<VectorSearchResult[]>> {
262
277
  if (!(searchAvailable && searchStmt)) {
263
278
  return Promise.resolve(
@@ -271,6 +286,43 @@ export async function createVectorIndexPort(
271
286
  }
272
287
 
273
288
  try {
289
+ const variants = searchVectorVariants(
290
+ db,
291
+ model,
292
+ dimensions,
293
+ embedding,
294
+ k,
295
+ searchOptions
296
+ );
297
+ if (variants !== null) return Promise.resolve(ok(variants));
298
+ if (searchOptions?.eligibility) {
299
+ const eligible = buildEligibleVectorQuery(db, searchOptions);
300
+ const distanceFunction =
301
+ distanceMetric === "cosine"
302
+ ? "vec_distance_cosine"
303
+ : "vec_distance_L2";
304
+ const rows = db
305
+ .query<
306
+ { mirrorHash: string; seq: number; distance: number },
307
+ (Uint8Array | string | number)[]
308
+ >(`
309
+ SELECT v.mirror_hash AS mirrorHash, v.seq,
310
+ ${distanceFunction}(v.embedding, ?) AS distance
311
+ FROM content_vectors v
312
+ WHERE v.model = ? AND EXISTS (${eligible.sql})
313
+ ORDER BY distance, v.mirror_hash, v.seq LIMIT ?
314
+ `)
315
+ .all(encodeEmbedding(embedding), model, ...eligible.params, k);
316
+ return Promise.resolve(
317
+ ok(
318
+ rows.filter(
319
+ (row) =>
320
+ searchOptions.minScore === undefined ||
321
+ 1 - row.distance >= searchOptions.minScore
322
+ )
323
+ )
324
+ );
325
+ }
274
326
  const allowed = searchOptions?.allowedMirrorHashes;
275
327
  if (allowed) {
276
328
  const hashes = [
@@ -12,6 +12,15 @@ import type { BacklogItem, VectorStatsPort } from "./types";
12
12
 
13
13
  import { err, ok } from "../types";
14
14
 
15
+ const statsDatabases = new WeakMap<VectorStatsPort, Database>();
16
+
17
+ /** Internal bridge for exact-input backlog; mock/third-party stats have no database. */
18
+ export function getVectorStatsDatabase(
19
+ stats: VectorStatsPort
20
+ ): Database | undefined {
21
+ return statsDatabases.get(stats);
22
+ }
23
+
15
24
  /**
16
25
  * Create a VectorStatsPort for backlog detection and vector stats.
17
26
  * Uses EXISTS-based queries to avoid duplicates from multiple docs sharing mirror_hash.
@@ -44,7 +53,7 @@ export function createVectorStatsPort(db: Database): VectorStatsPort {
44
53
  };
45
54
  }
46
55
 
47
- return {
56
+ const port: VectorStatsPort = {
48
57
  countVectors(model: string): Promise<StoreResult<number>> {
49
58
  try {
50
59
  const result = db
@@ -121,7 +130,7 @@ export function createVectorStatsPort(db: Database): VectorStatsPort {
121
130
  const sql = after
122
131
  ? `
123
132
  SELECT c.mirror_hash as mirrorHash, c.seq, c.text,
124
- (SELECT d.title FROM documents d WHERE d.mirror_hash = c.mirror_hash AND d.active = 1 LIMIT 1) as title,
133
+ (SELECT d.title FROM documents d WHERE d.mirror_hash = c.mirror_hash AND d.active = 1 ORDER BY d.id LIMIT 1) as title,
125
134
  CASE
126
135
  WHEN NOT EXISTS (
127
136
  SELECT 1 FROM content_vectors v
@@ -148,7 +157,7 @@ export function createVectorStatsPort(db: Database): VectorStatsPort {
148
157
  `
149
158
  : `
150
159
  SELECT c.mirror_hash as mirrorHash, c.seq, c.text,
151
- (SELECT d.title FROM documents d WHERE d.mirror_hash = c.mirror_hash AND d.active = 1 LIMIT 1) as title,
160
+ (SELECT d.title FROM documents d WHERE d.mirror_hash = c.mirror_hash AND d.active = 1 ORDER BY d.id LIMIT 1) as title,
152
161
  CASE
153
162
  WHEN NOT EXISTS (
154
163
  SELECT 1 FROM content_vectors v
@@ -206,4 +215,6 @@ export function createVectorStatsPort(db: Database): VectorStatsPort {
206
215
  }
207
216
  },
208
217
  };
218
+ statsDatabases.set(port, db);
219
+ return port;
209
220
  }
@@ -5,7 +5,25 @@
5
5
  * @module src/store/vector/types
6
6
  */
7
7
 
8
- import type { StoreResult } from "../types";
8
+ import type { DocumentEligibilityOptions, StoreResult } from "../types";
9
+
10
+ /** Full effective embedding identity, including truncation/runtime policy. */
11
+ export interface VectorVariantIdentity {
12
+ model: string;
13
+ modelFingerprint: string;
14
+ contextSize: number;
15
+ truncationPolicy: string;
16
+ dimensions: number;
17
+ }
18
+
19
+ /** Snapshot carried through asynchronous embedding; write revalidates input. */
20
+ export interface VectorOwnerInput {
21
+ documentId: number;
22
+ mirrorHash: string;
23
+ seq: number;
24
+ formattedInput: string;
25
+ inputHash: string;
26
+ }
9
27
 
10
28
  // ─────────────────────────────────────────────────────────────────────────────
11
29
  // Row Types
@@ -21,8 +39,18 @@ export interface VectorRow {
21
39
  // embeddedAt is set by DB via datetime('now')
22
40
  }
23
41
 
42
+ export interface VectorSearchOptions {
43
+ embeddingIdentity?: VectorVariantIdentity;
44
+ minScore?: number;
45
+ allowedMirrorHashes?: string[];
46
+ /** Exact active owner/chunk domain, applied before the nearest-neighbor budget. */
47
+ eligibility?: DocumentEligibilityOptions & { language?: string };
48
+ }
49
+
24
50
  /** Vector search result */
25
51
  export interface VectorSearchResult {
52
+ /** Exact current owners for variant hits; undefined preserves legacy semantics. */
53
+ documentIds?: number[];
26
54
  mirrorHash: string;
27
55
  seq: number;
28
56
  distance: number;
@@ -72,6 +100,11 @@ export interface VectorIndexPort {
72
100
 
73
101
  /** Upsert vectors into storage and vec index */
74
102
  upsertVectors(rows: VectorRow[]): Promise<StoreResult<void>>;
103
+ /** Internal checkpoint validation inside the storage transaction; returns committed rows. */
104
+ upsertVectorsChecked?(
105
+ rows: VectorRow[],
106
+ checkpoint: (rows: VectorRow[]) => VectorRow[]
107
+ ): Promise<StoreResult<number>>;
75
108
 
76
109
  /** Delete all vectors for a mirror hash (for this model) */
77
110
  deleteVectorsForMirror(mirrorHash: string): Promise<StoreResult<void>>;
@@ -84,7 +117,7 @@ export interface VectorIndexPort {
84
117
  searchNearest(
85
118
  embedding: Float32Array,
86
119
  k: number,
87
- options?: { minScore?: number; allowedMirrorHashes?: string[] }
120
+ options?: VectorSearchOptions
88
121
  ): Promise<StoreResult<VectorSearchResult[]>>;
89
122
 
90
123
  // ─────────────────────────────────────────────────────────────────────────
@@ -0,0 +1,192 @@
1
+ import type { Database } from "bun:sqlite";
2
+
3
+ import type { EmbeddingPort } from "../../llm/types";
4
+ import type {
5
+ VectorSearchOptions,
6
+ VectorSearchResult,
7
+ VectorVariantIdentity,
8
+ } from "./types";
9
+
10
+ import { getVariantModelFingerprint } from "../../embed/fingerprint";
11
+ import { formatDocForEmbedding } from "../../pipeline/contextual";
12
+ import { buildEligibleDocumentQuery } from "../sqlite/eligibility";
13
+ import { encodeEmbedding } from "./sqlite-vec";
14
+ import { embeddingInputHash, vectorVariantFingerprint } from "./variants";
15
+
16
+ /** Call after query embedding initialized the port; never infer runtime policy. */
17
+ export function resolveVectorSearchIdentity(
18
+ port: EmbeddingPort
19
+ ): VectorVariantIdentity | undefined {
20
+ const identity = port.getIdentity?.();
21
+ if (!identity) return undefined;
22
+ const dimensions = port.dimensions();
23
+ return {
24
+ model: port.modelUri,
25
+ modelFingerprint: getVariantModelFingerprint(
26
+ { modelUri: port.modelUri, dimensions },
27
+ identity
28
+ ),
29
+ contextSize: identity.contextSize,
30
+ truncationPolicy: identity.truncationPolicy,
31
+ dimensions,
32
+ };
33
+ }
34
+
35
+ /** Null means legacy authority. Once promoted, unavailable provenance fails closed. */
36
+ function searchVectorVariantsInSnapshot(
37
+ db: Database,
38
+ model: string,
39
+ dimensions: number,
40
+ embedding: Float32Array,
41
+ k: number,
42
+ options: VectorSearchOptions = {}
43
+ ): VectorSearchResult[] | null {
44
+ if (
45
+ !db
46
+ .query("SELECT 1 FROM sqlite_master WHERE name = 'vector_partitions'")
47
+ .get()
48
+ )
49
+ return null;
50
+ const activated = db
51
+ .query(
52
+ "SELECT 1 FROM vector_partitions WHERE model = ? AND state = 'active' AND activated_epoch IS NOT NULL LIMIT 1"
53
+ )
54
+ .get(model);
55
+ const identity = options.embeddingIdentity;
56
+ if (!identity) {
57
+ if (activated)
58
+ throw new Error(
59
+ "Effective embedding identity unavailable after variant activation"
60
+ );
61
+ return null;
62
+ }
63
+ if (
64
+ identity.model !== model ||
65
+ identity.dimensions !== dimensions ||
66
+ embedding.length !== dimensions
67
+ )
68
+ throw new Error("Query embedding identity does not match vector index");
69
+ const partitionId = embeddingInputHash(
70
+ JSON.stringify([model, vectorVariantFingerprint(identity), dimensions])
71
+ );
72
+ const partition = db
73
+ .query<{ state: string; activated_epoch: number | null }, [string]>(
74
+ "SELECT state, activated_epoch FROM vector_partitions WHERE partition_id = ?"
75
+ )
76
+ .get(partitionId);
77
+ if (partition?.state !== "active" || partition.activated_epoch === null) {
78
+ if (activated)
79
+ throw new Error(
80
+ "Selected embedding variant partition has not activated; run gno embed"
81
+ );
82
+ return null;
83
+ }
84
+ const tableName = `vec_v1_${partitionId}`;
85
+ if (!db.query("SELECT 1 FROM sqlite_master WHERE name = ?").get(tableName))
86
+ throw new Error("Activated variant index unavailable; run gno embed");
87
+ if (
88
+ db
89
+ .query(
90
+ `SELECT 1 FROM vector_variants v LEFT JOIN ${tableName} x ON x.variant_id = v.variant_id WHERE v.partition_id = ? AND (x.variant_id IS NULL OR x.embedding != v.embedding) LIMIT 1`
91
+ )
92
+ .get(partitionId)
93
+ )
94
+ throw new Error("Activated variant index inconsistent; run gno embed");
95
+ const eligible = buildEligibleDocumentQuery(
96
+ { ...options.eligibility, semanticMetadata: true },
97
+ db
98
+ );
99
+ const conditions = ["o.partition_id = ?"];
100
+ const params: (string | number)[] = [partitionId];
101
+ if (options.eligibility?.language) {
102
+ conditions.push("c.language = ?");
103
+ params.push(options.eligibility.language);
104
+ }
105
+ if (options.allowedMirrorHashes !== undefined) {
106
+ conditions.push("o.mirror_hash IN (SELECT value FROM json_each(?))");
107
+ params.push(JSON.stringify(options.allowedMirrorHashes));
108
+ }
109
+ const bindings = db
110
+ .query<
111
+ {
112
+ document_id: number;
113
+ seq: number;
114
+ text: string;
115
+ title: string | null;
116
+ input_hash: string;
117
+ },
118
+ (string | number)[]
119
+ >(`
120
+ SELECT o.document_id, o.seq, c.text, d.title, v.input_hash
121
+ FROM vector_owners o
122
+ JOIN (${eligible.sql}) e ON e.id = o.document_id AND e.mirror_hash = o.mirror_hash
123
+ JOIN documents d ON d.id = o.document_id
124
+ JOIN content_chunks c ON c.mirror_hash = o.mirror_hash AND c.seq = o.seq
125
+ JOIN vector_variants v ON v.variant_id = o.variant_id AND v.partition_id = o.partition_id
126
+ WHERE ${conditions.join(" AND ")}
127
+ `)
128
+ .all(...eligible.params, ...params);
129
+ const proven = bindings
130
+ .filter(
131
+ (row) =>
132
+ row.input_hash ===
133
+ embeddingInputHash(
134
+ formatDocForEmbedding(row.text, row.title ?? undefined, model)
135
+ )
136
+ )
137
+ .map((row) => `${row.document_id}:${row.seq}`);
138
+ // Both owner eligibility and formatted-input validation precede ranking/LIMIT.
139
+ const rows = db
140
+ .query<
141
+ {
142
+ mirrorHash: string;
143
+ seq: number;
144
+ distance: number;
145
+ documentIds: string;
146
+ },
147
+ (Uint8Array | string | number)[]
148
+ >(`
149
+ SELECT o.mirror_hash AS mirrorHash, o.seq, vec_distance_cosine(v.embedding, ?) AS distance,
150
+ json_group_array(o.document_id) AS documentIds
151
+ FROM vector_owners o
152
+ JOIN (${eligible.sql}) e ON e.id = o.document_id AND e.mirror_hash = o.mirror_hash
153
+ JOIN documents d ON d.id = o.document_id
154
+ JOIN content_chunks c ON c.mirror_hash = o.mirror_hash AND c.seq = o.seq
155
+ JOIN vector_variants v ON v.variant_id = o.variant_id AND v.partition_id = o.partition_id
156
+ JOIN ${tableName} x ON x.variant_id = v.variant_id AND x.embedding = v.embedding
157
+ WHERE ${conditions.join(" AND ")}
158
+ AND (o.document_id || ':' || o.seq) IN (SELECT value FROM json_each(?))
159
+ GROUP BY v.variant_id, o.mirror_hash, o.seq
160
+ ORDER BY distance, o.mirror_hash, o.seq, v.variant_id LIMIT ?
161
+ `)
162
+ .all(
163
+ encodeEmbedding(embedding),
164
+ ...eligible.params,
165
+ ...params,
166
+ JSON.stringify(proven),
167
+ k
168
+ );
169
+ return rows
170
+ .filter(
171
+ (row) =>
172
+ options.minScore === undefined || 1 - row.distance >= options.minScore
173
+ )
174
+ .map((row) => ({
175
+ ...row,
176
+ documentIds: JSON.parse(row.documentIds) as number[],
177
+ }));
178
+ }
179
+
180
+ /** Keep eligibility, provenance validation and ranking on one SQLite snapshot. */
181
+ export function searchVectorVariants(
182
+ db: Database,
183
+ model: string,
184
+ dimensions: number,
185
+ embedding: Float32Array,
186
+ k: number,
187
+ options: VectorSearchOptions = {}
188
+ ): VectorSearchResult[] | null {
189
+ return db.transaction(() =>
190
+ searchVectorVariantsInSnapshot(db, model, dimensions, embedding, k, options)
191
+ )();
192
+ }