@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.
- package/README.md +1 -1
- package/assets/skill/SKILL.md +5 -2
- package/assets/skill/recipes/memory-scoped-recall.md +10 -5
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.8.0.zip → gno-browser-clipper-v2.8.1.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.8.1.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +41 -6
- package/spec/mcp.md +8 -4
- package/spec/output-schemas/memory-recall.schema.json +1 -1
- package/spec/output-schemas/status.schema.json +3 -3
- package/src/cli/commands/graph.ts +3 -1
- package/src/cli/commands/links.ts +27 -49
- package/src/cli/commands/status.ts +1 -0
- package/src/core/audit-links.ts +56 -4
- package/src/core/audit-outside-index.ts +215 -0
- package/src/core/audit-workspace.ts +13 -7
- package/src/core/audit.ts +9 -1
- package/src/core/link-inventory-markdown.ts +2 -3
- package/src/core/links.ts +40 -17
- package/src/core/memory-recall.ts +254 -15
- package/src/core/memory-types.ts +12 -0
- package/src/core/memory.ts +2 -0
- package/src/ingestion/sync.ts +5 -3
- package/src/mcp/tools/links.ts +71 -93
- package/src/mcp/tools/status.ts +1 -0
- package/src/pipeline/search.ts +2 -0
- package/src/pipeline/types.ts +4 -0
- package/src/sdk/client.ts +1 -0
- package/src/serve/public/components/editor/MarkdownPreview.tsx +5 -3
- package/src/serve/routes/graph.ts +3 -1
- package/src/serve/routes/links.ts +32 -50
- package/src/serve/server.ts +2 -1
- package/src/serve/status.ts +1 -0
- package/src/store/sqlite/adapter.ts +87 -106
- package/src/store/sqlite/graph-link-resolver.ts +7 -0
- package/src/store/sqlite/graph-similarity.ts +96 -0
- package/src/store/sqlite/workspace-link-resolver.ts +119 -31
- package/src/store/types.ts +15 -2
- package/src/store/vector/status.ts +27 -0
- package/src/store/vector/stored-vectors.ts +158 -0
- package/src/store/vector/types.ts +6 -0
- package/src/store/vector/variant-search.ts +30 -14
- 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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|