@gmickel/gno 2.8.0 → 2.8.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.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/assets/skill/SKILL.md +5 -2
  3. package/assets/skill/recipes/memory-scoped-recall.md +10 -5
  4. package/assets/spa-production.json.gz +0 -0
  5. package/browser-extension/artifacts/{gno-browser-clipper-v2.8.0.zip → gno-browser-clipper-v2.8.1.zip} +0 -0
  6. package/browser-extension/artifacts/gno-browser-clipper-v2.8.1.zip.sha256 +1 -0
  7. package/browser-extension/dist/manifest.json +1 -1
  8. package/package.json +1 -1
  9. package/spec/cli.md +41 -6
  10. package/spec/mcp.md +8 -4
  11. package/spec/output-schemas/memory-recall.schema.json +1 -1
  12. package/spec/output-schemas/status.schema.json +3 -3
  13. package/src/cli/commands/graph.ts +3 -1
  14. package/src/cli/commands/links.ts +27 -49
  15. package/src/cli/commands/status.ts +1 -0
  16. package/src/core/audit-links.ts +56 -4
  17. package/src/core/audit-outside-index.ts +215 -0
  18. package/src/core/audit-workspace.ts +13 -7
  19. package/src/core/audit.ts +9 -1
  20. package/src/core/link-inventory-markdown.ts +2 -3
  21. package/src/core/links.ts +40 -17
  22. package/src/core/memory-recall.ts +254 -15
  23. package/src/core/memory-types.ts +12 -0
  24. package/src/core/memory.ts +2 -0
  25. package/src/ingestion/sync.ts +5 -3
  26. package/src/mcp/tools/links.ts +71 -93
  27. package/src/mcp/tools/status.ts +1 -0
  28. package/src/pipeline/search.ts +2 -0
  29. package/src/pipeline/types.ts +4 -0
  30. package/src/sdk/client.ts +1 -0
  31. package/src/serve/public/components/editor/MarkdownPreview.tsx +5 -3
  32. package/src/serve/routes/graph.ts +3 -1
  33. package/src/serve/routes/links.ts +32 -50
  34. package/src/serve/server.ts +2 -1
  35. package/src/serve/status.ts +1 -0
  36. package/src/store/sqlite/adapter.ts +87 -106
  37. package/src/store/sqlite/graph-link-resolver.ts +7 -0
  38. package/src/store/sqlite/graph-similarity.ts +96 -0
  39. package/src/store/sqlite/workspace-link-resolver.ts +119 -31
  40. package/src/store/types.ts +15 -2
  41. package/src/store/vector/status.ts +27 -0
  42. package/src/store/vector/stored-vectors.ts +158 -0
  43. package/src/store/vector/types.ts +6 -0
  44. package/src/store/vector/variant-search.ts +30 -14
  45. package/browser-extension/artifacts/gno-browser-clipper-v2.8.0.zip.sha256 +0 -1
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Stored document vectors for document-to-document similarity. Reads the
3
+ * activated vector partition (legacy `content_vectors` only before any
4
+ * partition activates) and never loads an embedding model.
5
+ *
6
+ * @module src/store/vector/stored-vectors
7
+ */
8
+ import type { Database } from "bun:sqlite";
9
+
10
+ import type { VectorSearchResult } from "./types";
11
+
12
+ import { formatDocForEmbedding } from "../../pipeline/contextual";
13
+ import { decodeEmbedding } from "./sqlite-vec";
14
+ import { storedVectorPartition } from "./status";
15
+ import { embeddingInputHash } from "./variants";
16
+
17
+ export type StoredVectorSource =
18
+ | {
19
+ kind: "partition";
20
+ model: string;
21
+ partitionId: string;
22
+ dimensions: number;
23
+ }
24
+ | { kind: "legacy"; model: string };
25
+
26
+ export function resolveStoredVectorSource(
27
+ db: Database,
28
+ model: string
29
+ ): StoredVectorSource {
30
+ const partition = storedVectorPartition(db, model);
31
+ return partition
32
+ ? { kind: "partition", model, ...partition }
33
+ : { kind: "legacy", model };
34
+ }
35
+
36
+ /** Search options that keep a similarity search in the source's vector space. */
37
+ export function storedVectorSearchOptions(source: StoredVectorSource): {
38
+ partitionId?: string;
39
+ } {
40
+ return source.kind === "partition" ? { partitionId: source.partitionId } : {};
41
+ }
42
+
43
+ export interface StoredVectorDocument {
44
+ id: number;
45
+ mirrorHash: string;
46
+ }
47
+
48
+ interface PartitionVectorRow {
49
+ documentId: number;
50
+ text: string;
51
+ title: string | null;
52
+ inputHash: string;
53
+ embedding: Uint8Array;
54
+ }
55
+
56
+ /**
57
+ * Chunk vectors of current active documents, ordered by chunk seq, keyed by
58
+ * document id. Partition vectors count only while their owner still yields
59
+ * the stored input (the rule vector search applies). `firstChunkOnly` keeps
60
+ * the lowest-seq vector per document.
61
+ */
62
+ export function readStoredDocumentVectors(
63
+ db: Database,
64
+ source: StoredVectorSource,
65
+ documents: StoredVectorDocument[],
66
+ options: { firstChunkOnly?: boolean } = {}
67
+ ): Map<number, Float32Array[]> {
68
+ const vectors = new Map<number, Float32Array[]>();
69
+ const add = (documentId: number, blob: Uint8Array): void => {
70
+ const existing = vectors.get(documentId);
71
+ if (existing && options.firstChunkOnly) return;
72
+ const embedding = decodeEmbedding(blob);
73
+ if (existing) existing.push(embedding);
74
+ else vectors.set(documentId, [embedding]);
75
+ };
76
+ if (documents.length === 0) return vectors;
77
+
78
+ if (source.kind === "partition") {
79
+ const rows = db
80
+ .query<PartitionVectorRow, [string, string]>(`
81
+ SELECT o.document_id AS documentId, c.text, d.title,
82
+ v.input_hash AS inputHash, v.embedding
83
+ FROM vector_owners o
84
+ JOIN documents d ON d.id = o.document_id AND d.active = 1
85
+ AND d.mirror_hash = o.mirror_hash
86
+ JOIN content_chunks c ON c.mirror_hash = o.mirror_hash AND c.seq = o.seq
87
+ JOIN vector_variants v ON v.variant_id = o.variant_id
88
+ AND v.partition_id = o.partition_id
89
+ WHERE o.partition_id = ?
90
+ AND o.document_id IN (SELECT value FROM json_each(?))
91
+ ORDER BY o.document_id, o.seq
92
+ `)
93
+ .all(
94
+ source.partitionId,
95
+ JSON.stringify(documents.map((document) => document.id))
96
+ );
97
+ for (const row of rows) {
98
+ if (options.firstChunkOnly && vectors.has(row.documentId)) continue;
99
+ const input = formatDocForEmbedding(
100
+ row.text,
101
+ row.title ?? undefined,
102
+ source.model
103
+ );
104
+ if (row.inputHash === embeddingInputHash(input))
105
+ add(row.documentId, row.embedding);
106
+ }
107
+ return vectors;
108
+ }
109
+
110
+ const idsByMirror = new Map<string, number[]>();
111
+ for (const document of documents) {
112
+ const ids = idsByMirror.get(document.mirrorHash);
113
+ if (ids) ids.push(document.id);
114
+ else idsByMirror.set(document.mirrorHash, [document.id]);
115
+ }
116
+ const rows = db
117
+ .query<{ mirrorHash: string; embedding: Uint8Array }, [string, string]>(`
118
+ SELECT mirror_hash AS mirrorHash, embedding FROM content_vectors
119
+ WHERE model = ? AND mirror_hash IN (SELECT value FROM json_each(?))
120
+ ORDER BY mirror_hash, seq
121
+ `)
122
+ .all(source.model, JSON.stringify([...idsByMirror.keys()]));
123
+ for (const row of rows) {
124
+ for (const id of idsByMirror.get(row.mirrorHash) ?? [])
125
+ add(id, row.embedding);
126
+ }
127
+ return vectors;
128
+ }
129
+
130
+ /**
131
+ * Candidate documents of similarity search hits, in hit order. Partition hits
132
+ * name their exact owners (a title is part of the embedded input, so documents
133
+ * sharing content can hold different vectors); only those owners count, and
134
+ * an owner without its own vector never inherits another's score. Legacy
135
+ * `content_vectors` hits carry no owners and map to the first document with
136
+ * that content, as before.
137
+ */
138
+ export function similarityHitDocuments<
139
+ T extends { id: number; mirrorHash: string | null },
140
+ >(
141
+ hits: VectorSearchResult[],
142
+ documents: T[]
143
+ ): Array<{ document: T; distance: number }> {
144
+ const byId = new Map(documents.map((document) => [document.id, document]));
145
+ const byHash = new Map<string, T>();
146
+ for (const document of documents) {
147
+ if (document.mirrorHash && !byHash.has(document.mirrorHash))
148
+ byHash.set(document.mirrorHash, document);
149
+ }
150
+ return hits.flatMap((hit) => {
151
+ const owners = hit.documentIds
152
+ ? hit.documentIds.map((id) => byId.get(id))
153
+ : [byHash.get(hit.mirrorHash)];
154
+ return owners.flatMap((document) =>
155
+ document ? [{ document, distance: hit.distance }] : []
156
+ );
157
+ });
158
+ }
@@ -43,6 +43,12 @@ export interface VectorRow {
43
43
 
44
44
  export interface VectorSearchOptions {
45
45
  embeddingIdentity?: VectorVariantIdentity;
46
+ /**
47
+ * Search this activated stored partition directly. Document-to-document
48
+ * similarity reuses a stored vector, so no runtime identity applies; the
49
+ * partition must match the port's model and dimensions.
50
+ */
51
+ partitionId?: string;
46
52
  minScore?: number;
47
53
  allowedMirrorHashes?: string[];
48
54
  /** Exact active owner/chunk domain, applied before the nearest-neighbor budget. */
@@ -103,27 +103,43 @@ function searchVectorVariantsInSnapshot(
103
103
  )
104
104
  .get(model);
105
105
  const identity = options.embeddingIdentity;
106
- if (!identity) {
107
- if (activated)
108
- throw new Error(
109
- "Effective embedding identity unavailable after variant activation"
110
- );
111
- return null;
106
+ let partitionId: string;
107
+ if (options.partitionId !== undefined) {
108
+ const stored = db
109
+ .query<{ model: string; dimensions: number }, [string]>(
110
+ "SELECT model, dimensions FROM vector_partitions WHERE partition_id = ?"
111
+ )
112
+ .get(options.partitionId);
113
+ if (
114
+ stored?.model !== model ||
115
+ stored.dimensions !== dimensions ||
116
+ embedding.length !== dimensions
117
+ )
118
+ throw new Error("Stored vector partition does not match vector index");
119
+ partitionId = options.partitionId;
120
+ } else {
121
+ if (!identity) {
122
+ if (activated)
123
+ throw new Error(
124
+ "Effective embedding identity unavailable after variant activation"
125
+ );
126
+ return null;
127
+ }
128
+ if (
129
+ identity.model !== model ||
130
+ identity.dimensions !== dimensions ||
131
+ embedding.length !== dimensions
132
+ )
133
+ throw new Error("Query embedding identity does not match vector index");
134
+ partitionId = identityPartitionId(identity);
112
135
  }
113
- if (
114
- identity.model !== model ||
115
- identity.dimensions !== dimensions ||
116
- embedding.length !== dimensions
117
- )
118
- throw new Error("Query embedding identity does not match vector index");
119
- const partitionId = identityPartitionId(identity);
120
136
  const partition = db
121
137
  .query<{ state: string; activated_epoch: number | null }, [string]>(
122
138
  "SELECT state, activated_epoch FROM vector_partitions WHERE partition_id = ?"
123
139
  )
124
140
  .get(partitionId);
125
141
  if (partition?.state !== "active" || partition.activated_epoch === null) {
126
- if (activated)
142
+ if (activated || options.partitionId !== undefined)
127
143
  throw new Error(
128
144
  "Selected embedding variant partition has not activated; run gno embed"
129
145
  );
@@ -1 +0,0 @@
1
- 2066003653182eb93964c5c058c86f691abf536f5093d1d0e8db2f382aedce19 gno-browser-clipper-v2.8.0.zip