@gmickel/gno 2.1.0 → 2.1.1

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/README.md CHANGED
@@ -117,7 +117,7 @@ gno daemon --detach # headless indexing + resident MCP gateway
117
117
 
118
118
  <!-- public-truth:current-version -->
119
119
 
120
- > Current source version: **v2.1.0**. See [CHANGELOG.md](./CHANGELOG.md).
120
+ > Current source version: **v2.1.1**. See [CHANGELOG.md](./CHANGELOG.md).
121
121
 
122
122
  <!-- /public-truth -->
123
123
 
@@ -0,0 +1 @@
1
+ bf5fc1ea3dd699217e270114051b19e88b239265b3a33e395f5b5ba753bc9e47 gno-browser-clipper-v2.1.1.zip
@@ -21,5 +21,5 @@
21
21
  "content_security_policy": {
22
22
  "extension_pages": "script-src 'self'; object-src 'none'; connect-src http://127.0.0.1:*"
23
23
  },
24
- "version": "2.1.0"
24
+ "version": "2.1.1"
25
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
5
5
  "keywords": [
6
6
  "embeddings",
package/spec/cli.md CHANGED
@@ -224,6 +224,14 @@ setup and emits `setup-profile-result@1.0`.
224
224
 
225
225
  Display index status and health information.
226
226
 
227
+ Embedding backlog follows the last verified partition for the selected model
228
+ when exact-input storage is authoritative, counting pending document/chunk
229
+ owners. Per-collection chunk totals remain deduplicated by canonical chunk;
230
+ embedded counts require matching current inputs for every active owner within
231
+ that collection. Status reads persisted identity and coverage without loading
232
+ models. Legacy storage remains the fallback before variant authority; ambiguous
233
+ older partition selection is conservative until a normal embed records it.
234
+
227
235
  **Synopsis:**
228
236
 
229
237
  ```bash
@@ -272,6 +272,7 @@ export async function prepareEmbeddingBacklog(
272
272
  truncationPolicy: identity.truncationPolicy,
273
273
  dimensions,
274
274
  });
275
+ variantStore.selectForEmbedding();
275
276
  return ok({
276
277
  ...deps,
277
278
  variantStore,
@@ -144,6 +144,7 @@ import { getSchemaVersion, migrations, runMigrations } from "../migrations";
144
144
  import { err, ok } from "../types";
145
145
  import { getStoredEmbeddingFingerprint } from "../vector/freshness";
146
146
  import { modelTableName } from "../vector/sqlite-vec";
147
+ import { getVariantStatus } from "../vector/status";
147
148
  import {
148
149
  deleteSavedCapsuleRegistration as deleteStoredSavedCapsuleRegistration,
149
150
  getSavedCapsuleRegistration as getStoredSavedCapsuleRegistration,
@@ -5697,6 +5698,8 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
5697
5698
  options?.embedFingerprint ??
5698
5699
  (embedModel ? getStoredEmbeddingFingerprint(db, embedModel) : null);
5699
5700
 
5701
+ const variantStatus = getVariantStatus(db, options);
5702
+
5700
5703
  // Get version
5701
5704
  const versionRow = db
5702
5705
  .query<{ value: string }, []>(
@@ -5873,12 +5876,14 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
5873
5876
  errorDocuments: s.errored,
5874
5877
  chunkedDocuments: s.chunked,
5875
5878
  totalChunks: s.chunk_count,
5876
- embeddedChunks: s.embedded_count,
5879
+ embeddedChunks: variantStatus
5880
+ ? (variantStatus.embeddedByCollection.get(s.name) ?? 0)
5881
+ : s.embedded_count,
5877
5882
  })),
5878
5883
  totalDocuments: totalsRow?.total ?? 0,
5879
5884
  activeDocuments: totalsRow?.active ?? 0,
5880
5885
  totalChunks: chunkCount,
5881
- embeddingBacklog: backlogRow?.count ?? 0,
5886
+ embeddingBacklog: variantStatus?.backlog ?? backlogRow?.count ?? 0,
5882
5887
  recentErrors,
5883
5888
  lastUpdatedAt: lastUpdatedRow?.last_updated ?? null,
5884
5889
  healthy,
@@ -0,0 +1,196 @@
1
+ /** Read-only coverage of persisted variant authority; never initializes models. */
2
+ import type { Database } from "bun:sqlite";
3
+
4
+ import { getEmbeddingFingerprint } from "../../embed/fingerprint";
5
+ import { formatDocForEmbedding } from "../../pipeline/contextual";
6
+ import {
7
+ embeddingInputHash,
8
+ SELECTED_VECTOR_PARTITION_PREFIX,
9
+ } from "./variants";
10
+
11
+ interface Partition {
12
+ partition_id: string;
13
+ version: number;
14
+ model: string;
15
+ fingerprint: string;
16
+ dimensions: number;
17
+ state: string;
18
+ activated_epoch: number | null;
19
+ }
20
+
21
+ interface OwnerCoverage {
22
+ document_id: number;
23
+ partition_id: string | null;
24
+ collection: string;
25
+ mirror_hash: string;
26
+ seq: number;
27
+ text: string;
28
+ title: string | null;
29
+ input_hash: string | null;
30
+ embedding_bytes: number | null;
31
+ legacy_embedded: number;
32
+ }
33
+
34
+ /** Null retains legacy counts until a verified selection or activation exists. */
35
+ export function getVariantStatus(
36
+ db: Database,
37
+ options?: { embedModel?: string; embedFingerprint?: string }
38
+ ): { backlog: number; embeddedByCollection: Map<string, number> } | null {
39
+ return db.transaction(() => {
40
+ if (
41
+ !db
42
+ .query(
43
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'vector_partitions'"
44
+ )
45
+ .get()
46
+ )
47
+ return null;
48
+ const partitions = db
49
+ .query<Partition, [string | null, string | null]>(`
50
+ SELECT partition_id, version, model, fingerprint, dimensions, state, activated_epoch
51
+ FROM vector_partitions WHERE (? IS NULL OR model = ?)
52
+ `)
53
+ .all(options?.embedModel ?? null, options?.embedModel ?? null);
54
+ const selections = db
55
+ .query<{ key: string; value: string }, [string]>(
56
+ "SELECT key, value FROM schema_meta WHERE key GLOB ?"
57
+ )
58
+ .all(`${SELECTED_VECTOR_PARTITION_PREFIX}*`);
59
+ const selected = new Map(
60
+ selections.map((row) => [
61
+ row.key.slice(SELECTED_VECTOR_PARTITION_PREFIX.length),
62
+ row.value,
63
+ ])
64
+ );
65
+ const models = new Set(partitions.map((p) => p.model));
66
+ if (options?.embedModel) models.add(options.embedModel);
67
+ else for (const model of selected.keys()) models.add(model);
68
+ const authoritativeModels = new Set<string>();
69
+ const usablePartitions = new Map<string, Partition>();
70
+ for (const model of models) {
71
+ const candidates = partitions.filter((p) => p.model === model);
72
+ const selection = selected.get(model);
73
+ const activated = candidates.some(
74
+ (p) => p.state === "active" && p.activated_epoch !== null
75
+ );
76
+ if (selection === undefined && !activated) continue;
77
+ authoritativeModels.add(model);
78
+ // Resolve one persisted identity per model, never combine alternative
79
+ // partitions of the same model. Unscoped status may accept any model.
80
+ // Stale epochs do not revoke owners whose current inputs still match.
81
+ const partition =
82
+ selection === undefined
83
+ ? candidates.length === 1
84
+ ? candidates[0]
85
+ : undefined
86
+ : candidates.find((p) => p.partition_id === selection);
87
+ if (
88
+ partition &&
89
+ partition.version === 1 &&
90
+ partition.partition_id ===
91
+ embeddingInputHash(
92
+ JSON.stringify([
93
+ partition.model,
94
+ partition.fingerprint,
95
+ partition.dimensions,
96
+ ])
97
+ ) &&
98
+ (options?.embedFingerprint === undefined ||
99
+ options.embedFingerprint ===
100
+ getEmbeddingFingerprint({
101
+ modelUri: partition.model,
102
+ dimensions: partition.dimensions,
103
+ }))
104
+ )
105
+ usablePartitions.set(partition.partition_id, partition);
106
+ }
107
+ if (authoritativeModels.size === 0) return null;
108
+ // Unscoped legacy coverage remains valid only for models that have never
109
+ // selected or activated verified authority; legacy rows cannot repair it.
110
+ const statement = db.prepare<
111
+ OwnerCoverage,
112
+ [string | null, string, string]
113
+ >(`
114
+ WITH legacy_vectors AS (
115
+ SELECT mirror_hash, seq, MAX(embedded_at) AS embedded_at
116
+ FROM content_vectors
117
+ WHERE ? IS NULL AND model NOT IN (SELECT value FROM json_each(?))
118
+ GROUP BY mirror_hash, seq
119
+ )
120
+ SELECT d.id AS document_id, o.partition_id, d.collection, d.mirror_hash, c.seq, c.text, d.title,
121
+ v.input_hash, length(v.embedding) AS embedding_bytes,
122
+ CASE WHEN lv.embedded_at >= c.created_at THEN 1 ELSE 0 END AS legacy_embedded
123
+ FROM documents d
124
+ JOIN content_chunks c ON c.mirror_hash = d.mirror_hash
125
+ LEFT JOIN vector_owners o ON o.document_id = d.id AND o.seq = c.seq
126
+ AND o.mirror_hash = d.mirror_hash
127
+ AND o.partition_id IN (SELECT value FROM json_each(?))
128
+ LEFT JOIN vector_variants v ON v.variant_id = o.variant_id
129
+ AND v.partition_id = o.partition_id
130
+ LEFT JOIN legacy_vectors lv ON lv.mirror_hash = c.mirror_hash AND lv.seq = c.seq
131
+ WHERE d.active = 1
132
+ `);
133
+ const owners = new Map<
134
+ string,
135
+ { collection: string; chunk: string; embedded: boolean }
136
+ >();
137
+ try {
138
+ for (const row of statement.iterate(
139
+ options?.embedModel ?? null,
140
+ JSON.stringify([...authoritativeModels]),
141
+ JSON.stringify([...usablePartitions.keys()])
142
+ )) {
143
+ const partition = row.partition_id
144
+ ? usablePartitions.get(row.partition_id)
145
+ : undefined;
146
+ const embedded = Boolean(
147
+ row.legacy_embedded ||
148
+ (partition &&
149
+ row.embedding_bytes ===
150
+ partition.dimensions * Float32Array.BYTES_PER_ELEMENT &&
151
+ row.input_hash ===
152
+ embeddingInputHash(
153
+ formatDocForEmbedding(
154
+ row.text,
155
+ row.title ?? undefined,
156
+ partition.model
157
+ )
158
+ ))
159
+ );
160
+ const key = `${row.document_id}:${row.seq}`;
161
+ const previous = owners.get(key);
162
+ if (previous) previous.embedded ||= embedded;
163
+ else
164
+ owners.set(key, {
165
+ collection: row.collection,
166
+ chunk: JSON.stringify([row.mirror_hash, row.seq]),
167
+ embedded,
168
+ });
169
+ }
170
+ } finally {
171
+ statement.finalize();
172
+ }
173
+ const collections = new Map<string, Map<string, boolean>>();
174
+ let backlog = 0;
175
+ for (const owner of owners.values()) {
176
+ if (!owner.embedded) backlog++;
177
+ let chunks = collections.get(owner.collection);
178
+ if (!chunks) {
179
+ chunks = new Map();
180
+ collections.set(owner.collection, chunks);
181
+ }
182
+ // Distinct collection chunks are ready only when every active owner is.
183
+ chunks.set(
184
+ owner.chunk,
185
+ (chunks.get(owner.chunk) ?? true) && owner.embedded
186
+ );
187
+ }
188
+ const embeddedByCollection = new Map<string, number>();
189
+ for (const [collection, chunks] of collections) {
190
+ let count = 0;
191
+ for (const embedded of chunks.values()) if (embedded) count++;
192
+ embeddedByCollection.set(collection, count);
193
+ }
194
+ return { backlog, embeddedByCollection };
195
+ })();
196
+ }
@@ -6,6 +6,8 @@ import type { VectorOwnerInput, VectorVariantIdentity } from "./types";
6
6
  import { formatDocForEmbedding } from "../../pipeline/contextual";
7
7
  import { decodeEmbedding, encodeEmbedding } from "./sqlite-vec";
8
8
 
9
+ export const SELECTED_VECTOR_PARTITION_PREFIX = "vector_selected_partition:";
10
+
9
11
  export function embeddingInputHash(input: string): string {
10
12
  return new Bun.CryptoHasher("sha256").update(input).digest("hex");
11
13
  }
@@ -94,6 +96,15 @@ export class VectorVariantStore {
94
96
  }).immediate();
95
97
  }
96
98
 
99
+ /** Persist only after embedding has resolved the actual runtime identity. */
100
+ selectForEmbedding(): void {
101
+ this.db.run(
102
+ `INSERT INTO schema_meta (key, value) VALUES (?, ?)
103
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = datetime('now')`,
104
+ [SELECTED_VECTOR_PARTITION_PREFIX + this.identity.model, this.partitionId]
105
+ );
106
+ }
107
+
97
108
  epoch(): number {
98
109
  return this.db
99
110
  .query<{ epoch: number }, []>(
@@ -1 +0,0 @@
1
- 8dd833b01ebc0a0cb5747de65a9cd60b06cafcf645c2a74d697c918fe7c642d7 gno-browser-clipper-v2.1.0.zip