@gmickel/gno 2.7.1 → 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 +3 -2
- package/assets/skill/SKILL.md +11 -1
- package/assets/skill/cli-reference.md +8 -1
- package/assets/skill/examples.md +2 -1
- package/assets/skill/mcp-reference.md +3 -1
- 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.7.1.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 +86 -9
- package/spec/db/schema.sql +0 -1
- package/spec/mcp.md +26 -7
- package/spec/output-schemas/audit-report.schema.json +18 -4
- package/spec/output-schemas/backlinks.schema.json +4 -0
- package/spec/output-schemas/collection-list.schema.json +13 -0
- package/spec/output-schemas/graph.schema.json +2 -0
- package/spec/output-schemas/links-list.schema.json +4 -0
- package/spec/output-schemas/memory-recall.schema.json +1 -1
- package/spec/output-schemas/status.schema.json +18 -3
- package/src/cli/commands/audit.ts +23 -4
- package/src/cli/commands/collection/list.ts +39 -5
- package/src/cli/commands/embed.ts +3 -3
- package/src/cli/commands/graph.ts +3 -1
- package/src/cli/commands/links.ts +61 -180
- package/src/cli/commands/shared.ts +7 -0
- package/src/cli/commands/status.ts +6 -0
- package/src/cli/program.ts +12 -2
- package/src/config/loader.ts +43 -0
- package/src/config/types.ts +8 -0
- package/src/core/audit-contract.ts +16 -4
- package/src/core/audit-freshness.ts +11 -1
- package/src/core/audit-links.ts +197 -25
- package/src/core/audit-outside-index.ts +215 -0
- package/src/core/audit-provenance.ts +11 -4
- package/src/core/audit-workspace.ts +30 -9
- package/src/core/audit.ts +76 -16
- package/src/core/context-compiler.ts +3 -0
- package/src/core/context-evidence.ts +11 -0
- package/src/core/graph-edge-confidence.ts +23 -1
- package/src/core/host-paths.ts +1 -0
- package/src/core/knowledge-impact.ts +28 -0
- package/src/core/link-inventory-markdown.ts +2 -3
- package/src/core/link-workspace.ts +324 -0
- 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/core/retrieval-replay-candidate.ts +6 -0
- package/src/core/retrieval-trace-request.ts +3 -0
- package/src/index.ts +14 -1
- package/src/ingestion/graph-reconciliation.ts +77 -15
- package/src/ingestion/source-availability/darwin-path.ts +9 -3
- package/src/ingestion/sync.ts +27 -4
- package/src/ingestion/types.ts +14 -0
- package/src/llm/inference-scope.ts +4 -3
- package/src/mcp/http-egress.ts +42 -3
- package/src/mcp/tools/audit.ts +11 -2
- package/src/mcp/tools/changes.ts +1 -0
- package/src/mcp/tools/links.ts +74 -93
- package/src/mcp/tools/sessions.ts +33 -4
- package/src/mcp/tools/status.ts +4 -0
- package/src/pipeline/expansion.ts +19 -31
- package/src/pipeline/graph-retrieval.ts +22 -2
- package/src/pipeline/hybrid.ts +1 -1
- package/src/pipeline/search.ts +2 -0
- package/src/pipeline/types.ts +10 -3
- package/src/sdk/client.ts +1 -0
- package/src/serve/findings-pass.ts +1 -1
- package/src/serve/public/components/editor/MarkdownPreview.tsx +5 -3
- package/src/serve/public/pages/GraphView.tsx +2 -0
- package/src/serve/routes/changes.ts +6 -1
- package/src/serve/routes/graph.ts +3 -1
- package/src/serve/routes/links.ts +45 -50
- package/src/serve/routes/sessions.ts +41 -53
- package/src/serve/server.ts +2 -1
- package/src/serve/status.ts +1 -0
- package/src/sessions/config-refresh.ts +111 -0
- package/src/store/migrations/033-drop-documents-active-index.ts +30 -0
- package/src/store/migrations/034-collection-link-workspace.ts +47 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +477 -337
- package/src/store/sqlite/eligibility.ts +8 -2
- package/src/store/sqlite/graph-link-resolver.ts +259 -5
- package/src/store/sqlite/graph-neighbors.ts +147 -40
- package/src/store/sqlite/graph-reference-state.ts +13 -2
- package/src/store/sqlite/graph-similarity.ts +96 -0
- package/src/store/sqlite/workspace-link-resolver.ts +742 -0
- package/src/store/types.ts +64 -5
- package/src/store/vector/stats.ts +1 -1
- 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.7.1.zip.sha256 +0 -1
package/src/store/types.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
FileRefactorRecoveryReceipt,
|
|
21
21
|
FileRefactorRecoveryReceiptDraft,
|
|
22
22
|
} from "../core/file-refactor-journal";
|
|
23
|
+
import type { LinkWorkspaceSource } from "../core/link-workspace";
|
|
23
24
|
import type { MetadataPredicate, TypedMetadata } from "../core/typed-metadata";
|
|
24
25
|
import type {
|
|
25
26
|
ChunkingPolicyToken,
|
|
@@ -113,6 +114,14 @@ export interface CollectionRow {
|
|
|
113
114
|
egressPolicy: EgressPolicy;
|
|
114
115
|
/** Whether policy was explicit or supplied by a safe default. */
|
|
115
116
|
egressPolicySource: EgressPolicySource;
|
|
117
|
+
/** Canonical collection root (real path), when resolvable. */
|
|
118
|
+
realPath?: string | null;
|
|
119
|
+
/** Effective link workspace root; null means collection-scoped links. */
|
|
120
|
+
workspaceRoot?: string | null;
|
|
121
|
+
/** How the workspace root was established. */
|
|
122
|
+
workspaceSource?: LinkWorkspaceSource;
|
|
123
|
+
/** Collection-relative prefixes of nested vaults with their own workspace. */
|
|
124
|
+
workspaceNested?: string[];
|
|
116
125
|
syncedAt: string;
|
|
117
126
|
}
|
|
118
127
|
|
|
@@ -351,6 +360,8 @@ export interface BacklinkRow {
|
|
|
351
360
|
sourceDocUri: string;
|
|
352
361
|
/** Source document title */
|
|
353
362
|
sourceDocTitle: string | null;
|
|
363
|
+
/** Collection of the source document */
|
|
364
|
+
sourceCollection?: string;
|
|
354
365
|
/** Link display text */
|
|
355
366
|
linkText: string | null;
|
|
356
367
|
/** 1-based line number in source */
|
|
@@ -731,6 +742,11 @@ export interface FtsSearchOptions extends DocumentEligibilityOptions {
|
|
|
731
742
|
snippet?: boolean;
|
|
732
743
|
/** Match documents containing ANY positive term instead of ALL of them. */
|
|
733
744
|
anyTerm?: boolean;
|
|
745
|
+
/**
|
|
746
|
+
* Drop rows whose raw BM25 score is below this fraction (0-1) of the best
|
|
747
|
+
* row's score, so matches carried only by near-zero-IDF terms fall away.
|
|
748
|
+
*/
|
|
749
|
+
minRelativeScore?: number;
|
|
734
750
|
}
|
|
735
751
|
|
|
736
752
|
/** Managed-memory eligibility query (unbounded, executed in one SQL query). */
|
|
@@ -793,6 +809,10 @@ export interface CollectionStatus {
|
|
|
793
809
|
egressPolicy: EgressPolicy;
|
|
794
810
|
/** Whether policy was explicit or supplied by a safe default. */
|
|
795
811
|
egressPolicySource: EgressPolicySource;
|
|
812
|
+
/** Effective link workspace root (host path); null when collection-scoped. */
|
|
813
|
+
workspaceRoot?: string | null;
|
|
814
|
+
/** How the link workspace was established. */
|
|
815
|
+
workspaceSource?: LinkWorkspaceSource;
|
|
796
816
|
totalDocuments: number;
|
|
797
817
|
activeDocuments: number;
|
|
798
818
|
errorDocuments: number;
|
|
@@ -818,11 +838,11 @@ export interface IndexStatus {
|
|
|
818
838
|
ftsTokenizer: FtsTokenizer;
|
|
819
839
|
/** Per-collection status */
|
|
820
840
|
collections: CollectionStatus[];
|
|
821
|
-
/** Total documents across
|
|
841
|
+
/** Total documents across the reported collections */
|
|
822
842
|
totalDocuments: number;
|
|
823
843
|
/** Active (non-deleted) documents */
|
|
824
844
|
activeDocuments: number;
|
|
825
|
-
/**
|
|
845
|
+
/** Distinct chunks of active documents across the reported collections */
|
|
826
846
|
totalChunks: number;
|
|
827
847
|
/** Chunks without embeddings */
|
|
828
848
|
embeddingBacklog: number;
|
|
@@ -876,6 +896,10 @@ export interface GraphEdgeAudit {
|
|
|
876
896
|
resolution:
|
|
877
897
|
| "exact-title"
|
|
878
898
|
| "exact-path"
|
|
899
|
+
/** Only file with that name (or path suffix) in its link workspace. */
|
|
900
|
+
| "exact-name"
|
|
901
|
+
/** Won over same-named files by the same-folder / shallower tie-break. */
|
|
902
|
+
| "tie-break"
|
|
879
903
|
| "path-fallback"
|
|
880
904
|
| "ambiguous-fallback"
|
|
881
905
|
| "similarity";
|
|
@@ -1058,6 +1082,8 @@ export interface GetGraphOptions {
|
|
|
1058
1082
|
linkedOnly?: boolean;
|
|
1059
1083
|
/** Top-K similar docs per node (default 5, clamped 1-20) */
|
|
1060
1084
|
similarTopK?: number;
|
|
1085
|
+
/** Embedding model whose stored vectors score similarity edges */
|
|
1086
|
+
embedModel?: string;
|
|
1061
1087
|
}
|
|
1062
1088
|
|
|
1063
1089
|
/** Options for seed-scoped one-hop graph neighbor lookup (query-time expansion). */
|
|
@@ -1066,6 +1092,12 @@ export interface GetGraphNeighborsOptions {
|
|
|
1066
1092
|
seedDocumentIds: number[];
|
|
1067
1093
|
/** Filter neighbors to a single collection */
|
|
1068
1094
|
collection?: string;
|
|
1095
|
+
/**
|
|
1096
|
+
* Graph allowlist: every edge's resolved source and target must be in one
|
|
1097
|
+
* of these collections. Takes precedence over `collection`; empty denies
|
|
1098
|
+
* all. Undefined (with no `collection`) is unrestricted.
|
|
1099
|
+
*/
|
|
1100
|
+
collections?: string[];
|
|
1069
1101
|
/** Max edges to return (default 10000) */
|
|
1070
1102
|
limitEdges?: number;
|
|
1071
1103
|
}
|
|
@@ -1113,6 +1145,11 @@ export interface GraphQueryEdge {
|
|
|
1113
1145
|
export interface GraphQueryOptions {
|
|
1114
1146
|
direction?: GraphQueryDirection;
|
|
1115
1147
|
edgeType?: DocEdgeType;
|
|
1148
|
+
/**
|
|
1149
|
+
* Collection allowlist: only documents in these collections are visited
|
|
1150
|
+
* (the root must be in scope too). Undefined means every collection.
|
|
1151
|
+
*/
|
|
1152
|
+
collections?: string[];
|
|
1116
1153
|
maxDepth?: number;
|
|
1117
1154
|
maxNodes?: number;
|
|
1118
1155
|
frontierLimit?: number;
|
|
@@ -1599,6 +1636,14 @@ export interface StorePort {
|
|
|
1599
1636
|
*/
|
|
1600
1637
|
getCollections(): Promise<StoreResult<CollectionRow[]>>;
|
|
1601
1638
|
|
|
1639
|
+
/**
|
|
1640
|
+
* Re-detect nested vaults below a collection root from its indexed
|
|
1641
|
+
* document directories; returns whether stored membership changed.
|
|
1642
|
+
*/
|
|
1643
|
+
refreshCollectionNestedWorkspaces?(
|
|
1644
|
+
collection: string
|
|
1645
|
+
): Promise<StoreResult<boolean>>;
|
|
1646
|
+
|
|
1602
1647
|
/**
|
|
1603
1648
|
* Get all contexts from DB.
|
|
1604
1649
|
*/
|
|
@@ -2242,18 +2287,26 @@ export interface StorePort {
|
|
|
2242
2287
|
): Promise<StoreResult<BacklinkRow[]>>;
|
|
2243
2288
|
|
|
2244
2289
|
/**
|
|
2245
|
-
* Resolve link targets to their documents.
|
|
2246
|
-
*
|
|
2290
|
+
* Resolve link targets to their documents with the shared link resolver.
|
|
2291
|
+
* With `source`, plain wiki links from a document inside a link workspace
|
|
2292
|
+
* resolve across the workspace. Returns resolved docs (or null for
|
|
2293
|
+
* unresolved and tied links) matching input order.
|
|
2247
2294
|
*/
|
|
2248
2295
|
resolveLinks(
|
|
2249
2296
|
targets: Array<{
|
|
2250
2297
|
targetRefNorm: string;
|
|
2251
2298
|
targetCollection: string;
|
|
2252
2299
|
linkType: "wiki" | "markdown";
|
|
2300
|
+
source?: { collection: string; relPath: string; explicit: boolean };
|
|
2253
2301
|
}>
|
|
2254
2302
|
): Promise<
|
|
2255
2303
|
StoreResult<
|
|
2256
|
-
Array<{
|
|
2304
|
+
Array<{
|
|
2305
|
+
docid: string;
|
|
2306
|
+
uri: string;
|
|
2307
|
+
title: string | null;
|
|
2308
|
+
collection?: string;
|
|
2309
|
+
} | null>
|
|
2257
2310
|
>
|
|
2258
2311
|
>;
|
|
2259
2312
|
|
|
@@ -2340,6 +2393,12 @@ export interface StorePort {
|
|
|
2340
2393
|
embedModel?: string;
|
|
2341
2394
|
embedFingerprint?: string;
|
|
2342
2395
|
chunking?: Partial<ChunkingParams>;
|
|
2396
|
+
/**
|
|
2397
|
+
* Configured collection names. When given, the collection list and the
|
|
2398
|
+
* document/chunk totals cover only these collections, so a collection
|
|
2399
|
+
* removed from config stops being reported before its rows are pruned.
|
|
2400
|
+
*/
|
|
2401
|
+
configuredCollections?: readonly string[];
|
|
2343
2402
|
}): Promise<StoreResult<IndexStatus>>;
|
|
2344
2403
|
|
|
2345
2404
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -34,7 +34,7 @@ export function createVectorStatsPort(db: Database): VectorStatsPort {
|
|
|
34
34
|
return {
|
|
35
35
|
sql: `
|
|
36
36
|
EXISTS (
|
|
37
|
-
SELECT 1 FROM documents d
|
|
37
|
+
SELECT 1 FROM documents d INDEXED BY idx_documents_mirror_hash
|
|
38
38
|
WHERE d.mirror_hash = c.mirror_hash AND d.active = 1 AND d.collection = ?
|
|
39
39
|
)
|
|
40
40
|
`,
|
|
@@ -148,6 +148,33 @@ function retrievalPartition(
|
|
|
148
148
|
: candidates.find((p) => p.partition_id === selection);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* The activated partition that stored-vector readers (document similarity)
|
|
153
|
+
* use for `model`, chosen as status counts it. Undefined leaves legacy
|
|
154
|
+
* `content_vectors` authority, exactly as vector search does before any
|
|
155
|
+
* partition activates.
|
|
156
|
+
*/
|
|
157
|
+
export function storedVectorPartition(
|
|
158
|
+
db: Database,
|
|
159
|
+
model: string
|
|
160
|
+
): { partitionId: string; dimensions: number } | undefined {
|
|
161
|
+
return db.transaction(() => {
|
|
162
|
+
if (!hasPartitionTable(db)) return undefined;
|
|
163
|
+
const partition = countedPartition(
|
|
164
|
+
db,
|
|
165
|
+
model,
|
|
166
|
+
readPartitions(db, model),
|
|
167
|
+
readSelections(db).get(model)
|
|
168
|
+
);
|
|
169
|
+
return partition && activated(partition)
|
|
170
|
+
? {
|
|
171
|
+
partitionId: partition.partition_id,
|
|
172
|
+
dimensions: partition.dimensions,
|
|
173
|
+
}
|
|
174
|
+
: undefined;
|
|
175
|
+
})();
|
|
176
|
+
}
|
|
177
|
+
|
|
151
178
|
function readPartitions(db: Database, model: string | null): Partition[] {
|
|
152
179
|
return db
|
|
153
180
|
.query<Partition, [string | null, string | null]>(`
|
|
@@ -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
|
-
0566f9eb5eb5db6a1632063e63f37dab01a9d2a0aa3dde5658356926a53c395e gno-browser-clipper-v2.7.1.zip
|