@gmickel/gno 2.10.0 → 2.11.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.
- package/README.md +11 -11
- package/assets/skill/SKILL.md +25 -14
- package/assets/skill/cli-reference.md +6 -5
- package/assets/skill/examples.md +16 -12
- package/assets/skill/mcp-reference.md +2 -1
- package/browser-extension/artifacts/{gno-browser-clipper-v2.10.0.zip → gno-browser-clipper-v2.11.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.11.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/bunfig.toml +5 -1
- package/package.json +4 -1
- package/spec/cli.md +59 -11
- package/spec/mcp.md +28 -4
- package/spec/output-schemas/ask.schema.json +8 -0
- package/spec/output-schemas/claim-verification.schema.json +8 -2
- package/spec/output-schemas/context-capsule-v1.schema.json +6 -1
- package/spec/output-schemas/context-capsule-verification.schema.json +4 -2
- package/spec/output-schemas/get.schema.json +4 -0
- package/spec/output-schemas/mcp-remove-result.schema.json +2 -2
- package/spec/output-schemas/metadata-summary.schema.json +60 -0
- package/spec/output-schemas/multi-get.schema.json +4 -0
- package/spec/output-schemas/search-result.schema.json +4 -0
- package/spec/output-schemas/search-results.schema.json +4 -0
- package/src/app/constants.ts +16 -0
- package/src/app/context-format.ts +1 -0
- package/src/app/verified-ask.ts +2 -0
- package/src/cli/commands/agents/block.ts +9 -8
- package/src/cli/commands/ask-format.ts +18 -1
- package/src/cli/commands/ask.ts +4 -5
- package/src/cli/commands/cleanup.ts +9 -1
- package/src/cli/commands/collection/remove.ts +50 -31
- package/src/cli/commands/collection/rename.ts +36 -8
- package/src/cli/commands/daemon.ts +5 -1
- package/src/cli/commands/get.ts +11 -1
- package/src/cli/commands/metadata.ts +79 -0
- package/src/cli/commands/multi-get.ts +13 -0
- package/src/cli/commands/shared.ts +9 -7
- package/src/cli/format/search-results.ts +11 -0
- package/src/cli/options.ts +2 -0
- package/src/cli/program.ts +101 -16
- package/src/core/context-capsule-schema.ts +2 -0
- package/src/core/context-capsule-verification.ts +2 -0
- package/src/core/context-compiler.ts +2 -5
- package/src/core/context-evidence.ts +1 -0
- package/src/core/context-verifier.ts +14 -1
- package/src/core/metadata-discovery.ts +169 -0
- package/src/ingestion/sync.ts +49 -1
- package/src/ingestion/walker.ts +11 -3
- package/src/mcp/http-egress.ts +1 -0
- package/src/mcp/tool-descriptions-core.ts +2 -2
- package/src/mcp/tool-profile.ts +16 -1
- package/src/mcp/tools/get.ts +10 -0
- package/src/mcp/tools/index.ts +39 -4
- package/src/mcp/tools/multi-get.ts +15 -0
- package/src/mcp/tools/query.ts +5 -3
- package/src/mcp/tools/remove-collection.ts +2 -1
- package/src/mcp/tools/search.ts +5 -3
- package/src/mcp/tools/vsearch.ts +5 -3
- package/src/mcp/tools/workspace-write.ts +42 -34
- package/src/pipeline/answer-prompt.ts +15 -2
- package/src/pipeline/answer.ts +3 -0
- package/src/pipeline/claim-verification-schema.ts +6 -1
- package/src/pipeline/claim-verification.ts +54 -1
- package/src/pipeline/explain.ts +1 -1
- package/src/pipeline/fusion.ts +24 -4
- package/src/pipeline/hybrid.ts +5 -1
- package/src/pipeline/result-context.ts +29 -0
- package/src/pipeline/search.ts +5 -1
- package/src/pipeline/types.ts +4 -0
- package/src/pipeline/vsearch.ts +5 -1
- package/src/sdk/client.ts +32 -4
- package/src/sdk/documents.ts +5 -0
- package/src/sdk/types.ts +5 -0
- package/src/serve/routes/api.ts +32 -5
- package/src/serve/server.ts +12 -0
- package/src/store/sqlite/adapter.ts +99 -6
- package/src/store/types.ts +16 -0
- package/src/store/vector/variant-search.ts +112 -74
- package/browser-extension/artifacts/gno-browser-clipper-v2.10.0.zip.sha256 +0 -1
package/src/pipeline/vsearch.ts
CHANGED
|
@@ -38,7 +38,10 @@ import { typedMetadataWarnings, typedMetadataFilterReason } from "./filters";
|
|
|
38
38
|
import { selectBestChunkForSteering } from "./intent";
|
|
39
39
|
import { hasProjectAffinity } from "./project-affinity";
|
|
40
40
|
import { detectQueryLanguage } from "./query-language";
|
|
41
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
attachSearchResultContexts,
|
|
43
|
+
attachSupersession,
|
|
44
|
+
} from "./result-context";
|
|
42
45
|
import { cleanDisplaySnippet } from "./snippet";
|
|
43
46
|
import {
|
|
44
47
|
resolveRecencyTimestamp,
|
|
@@ -520,6 +523,7 @@ async function searchVectorWithEmbeddingOwned(
|
|
|
520
523
|
if (metadata) metadata.retrievalRank = index + 1;
|
|
521
524
|
}
|
|
522
525
|
await attachSearchResultContexts(store, finalResults);
|
|
526
|
+
await attachSupersession(store, finalResults);
|
|
523
527
|
const lineageResult = await attachSearchResultEgressLineage(
|
|
524
528
|
store,
|
|
525
529
|
finalResults,
|
package/src/sdk/client.ts
CHANGED
|
@@ -78,6 +78,7 @@ import {
|
|
|
78
78
|
refreshContextFile,
|
|
79
79
|
} from "../app/compiled-context-files";
|
|
80
80
|
import {
|
|
81
|
+
decorateResultUris,
|
|
81
82
|
decorateUriForIndex,
|
|
82
83
|
DEFAULT_INDEX_NAME,
|
|
83
84
|
getIndexDbPath,
|
|
@@ -140,6 +141,7 @@ import {
|
|
|
140
141
|
type MemoryErrorCode,
|
|
141
142
|
MemoryService,
|
|
142
143
|
} from "../core/memory";
|
|
144
|
+
import { readMetadataSummary } from "../core/metadata-discovery";
|
|
143
145
|
import { resolveNoteCreatePlan } from "../core/note-creation";
|
|
144
146
|
import { resolveNotePreset } from "../core/note-presets";
|
|
145
147
|
import {
|
|
@@ -701,10 +703,9 @@ class GnoClientImpl implements GnoClient {
|
|
|
701
703
|
private decorateSearchResults(results: SearchResults): SearchResults {
|
|
702
704
|
return {
|
|
703
705
|
...results,
|
|
704
|
-
results: results.results.map((result) =>
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
})),
|
|
706
|
+
results: results.results.map((result) =>
|
|
707
|
+
decorateResultUris(result, this.indexName)
|
|
708
|
+
),
|
|
708
709
|
};
|
|
709
710
|
}
|
|
710
711
|
|
|
@@ -1415,6 +1416,14 @@ class GnoClientImpl implements GnoClient {
|
|
|
1415
1416
|
const decorated = {
|
|
1416
1417
|
...result,
|
|
1417
1418
|
uri: decorateUriForIndex(result.uri, scoped.indexName),
|
|
1419
|
+
...(result.supersededBy
|
|
1420
|
+
? {
|
|
1421
|
+
supersededBy: decorateUriForIndex(
|
|
1422
|
+
result.supersededBy,
|
|
1423
|
+
scoped.indexName
|
|
1424
|
+
),
|
|
1425
|
+
}
|
|
1426
|
+
: {}),
|
|
1418
1427
|
};
|
|
1419
1428
|
const traceMetadata = getRetrievalTraceMetadata(result);
|
|
1420
1429
|
if (traceMetadata) {
|
|
@@ -1458,6 +1467,14 @@ class GnoClientImpl implements GnoClient {
|
|
|
1458
1467
|
documents: result.documents.map((doc) => ({
|
|
1459
1468
|
...doc,
|
|
1460
1469
|
uri: decorateUriForIndex(doc.uri, scoped.indexName),
|
|
1470
|
+
...(doc.supersededBy
|
|
1471
|
+
? {
|
|
1472
|
+
supersededBy: decorateUriForIndex(
|
|
1473
|
+
doc.supersededBy,
|
|
1474
|
+
scoped.indexName
|
|
1475
|
+
),
|
|
1476
|
+
}
|
|
1477
|
+
: {}),
|
|
1461
1478
|
})),
|
|
1462
1479
|
};
|
|
1463
1480
|
} finally {
|
|
@@ -1477,6 +1494,17 @@ class GnoClientImpl implements GnoClient {
|
|
|
1477
1494
|
};
|
|
1478
1495
|
}
|
|
1479
1496
|
|
|
1497
|
+
async listMetadata(options: { collection?: string; values?: number } = {}) {
|
|
1498
|
+
this.assertOpen();
|
|
1499
|
+
try {
|
|
1500
|
+
return await readMetadataSummary(this.store, options);
|
|
1501
|
+
} catch (cause) {
|
|
1502
|
+
if (cause instanceof RangeError)
|
|
1503
|
+
throw sdkError("VALIDATION", cause.message);
|
|
1504
|
+
throw cause;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1480
1508
|
async changes(
|
|
1481
1509
|
options: ListKnowledgeChangesInput = {}
|
|
1482
1510
|
): Promise<KnowledgeChangesResult> {
|
package/src/sdk/documents.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
evidenceFromExactDocument,
|
|
31
31
|
RetrievalTraceSession,
|
|
32
32
|
} from "../core/retrieval-trace-session";
|
|
33
|
+
import { supersedingUri } from "../pipeline/result-context";
|
|
33
34
|
import { sdkError } from "./errors";
|
|
34
35
|
|
|
35
36
|
const URI_PREFIX_PATTERN = /^gno:\/\/[^/]+\//;
|
|
@@ -177,6 +178,8 @@ export async function getDocumentByRef(
|
|
|
177
178
|
options: GnoGetOptions = {}
|
|
178
179
|
): Promise<GetResponse> {
|
|
179
180
|
const response = await getDocumentByRefUntraced(store, config, ref, options);
|
|
181
|
+
const successor = await supersedingUri(store, response.uri);
|
|
182
|
+
if (successor) response.supersededBy = successor;
|
|
180
183
|
if (!options.traceId) return response;
|
|
181
184
|
const resumed = await RetrievalTraceSession.resume({
|
|
182
185
|
store,
|
|
@@ -325,7 +328,9 @@ export async function multiGetDocuments(
|
|
|
325
328
|
);
|
|
326
329
|
const coll = config.collections.find((c) => c.name === doc.collection);
|
|
327
330
|
const sourceRelPath = doc.recordSourcePath ?? doc.relPath;
|
|
331
|
+
const successor = await supersedingUri(store, doc.uri);
|
|
328
332
|
documents.push({
|
|
333
|
+
...(successor ? { supersededBy: successor } : {}),
|
|
329
334
|
docid: doc.docid,
|
|
330
335
|
uri: doc.uri,
|
|
331
336
|
title: doc.title ?? undefined,
|
package/src/sdk/types.ts
CHANGED
|
@@ -394,6 +394,11 @@ export interface GnoClient {
|
|
|
394
394
|
list(
|
|
395
395
|
options?: GnoListOptions
|
|
396
396
|
): Promise<import("../cli/commands/ls").LsResponse>;
|
|
397
|
+
/** Typed metadata keys with types, document counts and common values. */
|
|
398
|
+
listMetadata(options?: {
|
|
399
|
+
collection?: string;
|
|
400
|
+
values?: number;
|
|
401
|
+
}): Promise<import("../core/metadata-discovery").MetadataSummary>;
|
|
397
402
|
changes(options?: ListKnowledgeChangesInput): Promise<KnowledgeChangesResult>;
|
|
398
403
|
diff(ref: string, changeId?: string): Promise<KnowledgeDiffResult>;
|
|
399
404
|
impact(
|
package/src/serve/routes/api.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
* @module src/serve/routes/api
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
// node:fs/promises structure ops + realpath have no Bun equivalent
|
|
9
8
|
import { readdir, realpath } from "node:fs/promises";
|
|
10
9
|
// node:path has no Bun equivalent
|
|
11
10
|
import { posix as pathPosix } from "node:path";
|
|
@@ -90,6 +89,7 @@ import {
|
|
|
90
89
|
type RecallInput,
|
|
91
90
|
type RememberInput,
|
|
92
91
|
} from "../../core/memory";
|
|
92
|
+
import { readMetadataSummary } from "../../core/metadata-discovery";
|
|
93
93
|
import {
|
|
94
94
|
hasContentMutation,
|
|
95
95
|
recordContentMutation,
|
|
@@ -167,6 +167,8 @@ import {
|
|
|
167
167
|
import { diagnoseQueryTarget } from "../../pipeline/diagnose";
|
|
168
168
|
import { searchHybrid } from "../../pipeline/hybrid";
|
|
169
169
|
import { validateQueryModes } from "../../pipeline/query-modes";
|
|
170
|
+
// node:fs/promises structure ops + realpath have no Bun equivalent
|
|
171
|
+
import { supersedingUri } from "../../pipeline/result-context";
|
|
170
172
|
import { searchBm25 } from "../../pipeline/search";
|
|
171
173
|
import {
|
|
172
174
|
derivePublishArtifactFilename,
|
|
@@ -1574,9 +1576,8 @@ export async function handleImportPreview(
|
|
|
1574
1576
|
|
|
1575
1577
|
/**
|
|
1576
1578
|
* DELETE /api/collections/:name
|
|
1577
|
-
* Remove a collection from config.
|
|
1578
|
-
*
|
|
1579
|
-
* or manual cleanup. This preserves data for potential recovery.
|
|
1579
|
+
* Remove a collection from config. The config sync deletes the collection
|
|
1580
|
+
* row and its documents; content, chunks and vectors stay until cleanup.
|
|
1580
1581
|
*/
|
|
1581
1582
|
export async function handleDeleteCollection(
|
|
1582
1583
|
ctxHolder: ContextHolder,
|
|
@@ -1612,7 +1613,7 @@ export async function handleDeleteCollection(
|
|
|
1612
1613
|
return jsonResponse({
|
|
1613
1614
|
success: true,
|
|
1614
1615
|
collection: name,
|
|
1615
|
-
note: "Collection removed from config
|
|
1616
|
+
note: "Collection removed from config and its documents deleted from the index. Content, chunks and vectors stay until gno cleanup.",
|
|
1616
1617
|
});
|
|
1617
1618
|
}
|
|
1618
1619
|
|
|
@@ -2147,7 +2148,9 @@ export async function handleDoc(
|
|
|
2147
2148
|
const record = projectRecordEvidenceMetadata(doc);
|
|
2148
2149
|
const relPath = doc.recordSourcePath ?? doc.relPath;
|
|
2149
2150
|
|
|
2151
|
+
const successor = await supersedingUri(store, doc.uri);
|
|
2150
2152
|
const responseData = {
|
|
2153
|
+
...(successor ? { supersededBy: successor } : {}),
|
|
2151
2154
|
docid: doc.docid,
|
|
2152
2155
|
uri: doc.uri,
|
|
2153
2156
|
title: doc.title,
|
|
@@ -2464,6 +2467,30 @@ export async function handleDocAsset(
|
|
|
2464
2467
|
* Query params: collection, prefix
|
|
2465
2468
|
* Returns tag list with document counts.
|
|
2466
2469
|
*/
|
|
2470
|
+
export async function handleMetadata(
|
|
2471
|
+
store: Pick<StorePort, "getTypedMetadataDocuments">,
|
|
2472
|
+
url: URL
|
|
2473
|
+
): Promise<Response> {
|
|
2474
|
+
const collection = url.searchParams.get("collection") ?? undefined;
|
|
2475
|
+
const valuesParam = url.searchParams.get("values");
|
|
2476
|
+
try {
|
|
2477
|
+
return jsonResponse(
|
|
2478
|
+
await readMetadataSummary(store, {
|
|
2479
|
+
collection,
|
|
2480
|
+
values: valuesParam === null ? undefined : Number(valuesParam),
|
|
2481
|
+
})
|
|
2482
|
+
);
|
|
2483
|
+
} catch (cause) {
|
|
2484
|
+
return cause instanceof RangeError
|
|
2485
|
+
? errorResponse("VALIDATION", cause.message)
|
|
2486
|
+
: errorResponse(
|
|
2487
|
+
"RUNTIME",
|
|
2488
|
+
cause instanceof Error ? cause.message : String(cause),
|
|
2489
|
+
500
|
|
2490
|
+
);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2467
2494
|
export async function handleTags(
|
|
2468
2495
|
store: SqliteAdapter,
|
|
2469
2496
|
url: URL
|
package/src/serve/server.ts
CHANGED
|
@@ -83,6 +83,7 @@ import {
|
|
|
83
83
|
handleSetPreset,
|
|
84
84
|
handleStatus,
|
|
85
85
|
handleSync,
|
|
86
|
+
handleMetadata,
|
|
86
87
|
handleTags,
|
|
87
88
|
handleTrashDoc,
|
|
88
89
|
handleUpdateCollection,
|
|
@@ -1331,6 +1332,17 @@ export async function startServer(
|
|
|
1331
1332
|
}
|
|
1332
1333
|
},
|
|
1333
1334
|
},
|
|
1335
|
+
"/api/metadata": {
|
|
1336
|
+
GET: async (req: Request) => {
|
|
1337
|
+
const url = new URL(req.url);
|
|
1338
|
+
return withSecurityHeaders(
|
|
1339
|
+
await handleResidentRead(runtime as ResidentRuntime, req, () =>
|
|
1340
|
+
handleMetadata(store, url)
|
|
1341
|
+
),
|
|
1342
|
+
isDev
|
|
1343
|
+
);
|
|
1344
|
+
},
|
|
1345
|
+
},
|
|
1334
1346
|
"/api/tags": {
|
|
1335
1347
|
GET: async (req: Request) => {
|
|
1336
1348
|
const url = new URL(req.url);
|
|
@@ -3693,9 +3693,89 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
3693
3693
|
}
|
|
3694
3694
|
|
|
3695
3695
|
/**
|
|
3696
|
-
*
|
|
3697
|
-
*
|
|
3696
|
+
* Map each superseded memory fact's URI to the end of its supersession
|
|
3697
|
+
* chain. Input URIs may carry `?index=`; keys are the URIs as given and
|
|
3698
|
+
* successors are canonical (undecorated).
|
|
3698
3699
|
*/
|
|
3700
|
+
async getSupersedingUris(
|
|
3701
|
+
uris: string[]
|
|
3702
|
+
): Promise<StoreResult<Map<string, string>>> {
|
|
3703
|
+
try {
|
|
3704
|
+
const db = this.ensureOpen();
|
|
3705
|
+
const successorOf = db.query<
|
|
3706
|
+
{ uri: string; successor: string },
|
|
3707
|
+
[string]
|
|
3708
|
+
>(`
|
|
3709
|
+
SELECT p.uri AS uri, s.uri AS successor
|
|
3710
|
+
FROM documents p
|
|
3711
|
+
JOIN doc_edges e ON e.dst_doc_id = p.id AND e.edge_type = 'supersedes'
|
|
3712
|
+
JOIN documents s ON s.id = e.src_doc_id AND s.active = 1
|
|
3713
|
+
WHERE p.uri IN (SELECT value FROM json_each(?))
|
|
3714
|
+
ORDER BY s.id
|
|
3715
|
+
`);
|
|
3716
|
+
const canonical = new Map(uris.map((uri) => [uri, stripUriIndex(uri)]));
|
|
3717
|
+
const next = new Map<string, string>();
|
|
3718
|
+
const seen = new Set(canonical.values());
|
|
3719
|
+
let frontier = [...seen];
|
|
3720
|
+
while (frontier.length > 0) {
|
|
3721
|
+
const discovered: string[] = [];
|
|
3722
|
+
for (const row of successorOf.all(JSON.stringify(frontier))) {
|
|
3723
|
+
next.set(row.uri, row.successor);
|
|
3724
|
+
if (!seen.has(row.successor)) {
|
|
3725
|
+
seen.add(row.successor);
|
|
3726
|
+
discovered.push(row.successor);
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
frontier = discovered;
|
|
3730
|
+
}
|
|
3731
|
+
const result = new Map<string, string>();
|
|
3732
|
+
for (const [input, uri] of canonical) {
|
|
3733
|
+
const visited = new Set([uri]);
|
|
3734
|
+
let current = next.get(uri);
|
|
3735
|
+
while (current && next.has(current) && !visited.has(current)) {
|
|
3736
|
+
visited.add(current);
|
|
3737
|
+
current = next.get(current);
|
|
3738
|
+
}
|
|
3739
|
+
if (current && current !== uri) result.set(input, current);
|
|
3740
|
+
}
|
|
3741
|
+
return ok(result);
|
|
3742
|
+
} catch (cause) {
|
|
3743
|
+
return err(
|
|
3744
|
+
"QUERY_FAILED",
|
|
3745
|
+
cause instanceof Error ? cause.message : "Failed to read supersession",
|
|
3746
|
+
cause
|
|
3747
|
+
);
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
|
|
3751
|
+
async getTypedMetadataDocuments(options?: {
|
|
3752
|
+
collection?: string;
|
|
3753
|
+
}): Promise<StoreResult<{ documents: number; metadata: string[] }>> {
|
|
3754
|
+
try {
|
|
3755
|
+
const db = this.ensureOpen();
|
|
3756
|
+
const scope = options?.collection ? " AND collection = ?" : "";
|
|
3757
|
+
const params = options?.collection ? [options.collection] : [];
|
|
3758
|
+
const documents = db
|
|
3759
|
+
.query<{ count: number }, string[]>(
|
|
3760
|
+
`SELECT count(*) AS count FROM documents WHERE active = 1${scope}`
|
|
3761
|
+
)
|
|
3762
|
+
.get(...params)!.count;
|
|
3763
|
+
const metadata = db
|
|
3764
|
+
.query<{ typed_metadata: string }, string[]>(
|
|
3765
|
+
`SELECT typed_metadata FROM documents WHERE active = 1 AND typed_metadata IS NOT NULL${scope}`
|
|
3766
|
+
)
|
|
3767
|
+
.all(...params)
|
|
3768
|
+
.map((row) => row.typed_metadata);
|
|
3769
|
+
return ok({ documents, metadata });
|
|
3770
|
+
} catch (cause) {
|
|
3771
|
+
return err(
|
|
3772
|
+
"QUERY_FAILED",
|
|
3773
|
+
cause instanceof Error ? cause.message : "Failed to read metadata",
|
|
3774
|
+
cause
|
|
3775
|
+
);
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
|
|
3699
3779
|
async getTagCounts(options?: {
|
|
3700
3780
|
collection?: string;
|
|
3701
3781
|
prefix?: string;
|
|
@@ -6124,18 +6204,25 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
6124
6204
|
)
|
|
6125
6205
|
.get(configuredJson, configuredJson)?.count ?? 0;
|
|
6126
6206
|
|
|
6127
|
-
// Embedding backlog: chunks from active docs
|
|
6128
|
-
//
|
|
6207
|
+
// Embedding backlog: chunks from active docs of configured collections
|
|
6208
|
+
// without vectors. DISTINCT avoids duplicates when docs share mirror_hash.
|
|
6129
6209
|
const backlogRow = db
|
|
6130
6210
|
.query<
|
|
6131
6211
|
{ count: number },
|
|
6132
|
-
[
|
|
6212
|
+
[
|
|
6213
|
+
string | null,
|
|
6214
|
+
string | null,
|
|
6215
|
+
string | null,
|
|
6216
|
+
string | null,
|
|
6217
|
+
string | null,
|
|
6218
|
+
]
|
|
6133
6219
|
>(
|
|
6134
6220
|
`
|
|
6135
6221
|
WITH active_mirrors AS (
|
|
6136
6222
|
SELECT DISTINCT mirror_hash
|
|
6137
6223
|
FROM documents
|
|
6138
6224
|
WHERE active = 1 AND mirror_hash IS NOT NULL
|
|
6225
|
+
AND (? IS NULL OR collection IN (SELECT value FROM json_each(?)))
|
|
6139
6226
|
),
|
|
6140
6227
|
matching_vectors AS (
|
|
6141
6228
|
SELECT mirror_hash, seq, MAX(embedded_at) AS embedded_at
|
|
@@ -6155,7 +6242,13 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
6155
6242
|
WHERE mv.mirror_hash IS NULL
|
|
6156
6243
|
`
|
|
6157
6244
|
)
|
|
6158
|
-
.get(
|
|
6245
|
+
.get(
|
|
6246
|
+
configuredJson,
|
|
6247
|
+
configuredJson,
|
|
6248
|
+
embedModel,
|
|
6249
|
+
embedModel,
|
|
6250
|
+
embedFingerprint
|
|
6251
|
+
);
|
|
6159
6252
|
|
|
6160
6253
|
// Recent errors (last 24h)
|
|
6161
6254
|
const recentErrorsRow = db
|
package/src/store/types.ts
CHANGED
|
@@ -2245,6 +2245,22 @@ export interface StorePort {
|
|
|
2245
2245
|
documentIds: number[]
|
|
2246
2246
|
): Promise<StoreResult<Map<number, TagRow[]>>>;
|
|
2247
2247
|
|
|
2248
|
+
/**
|
|
2249
|
+
* For each URI whose document an active document supersedes, the URI of
|
|
2250
|
+
* its current successor (the end of the supersession chain).
|
|
2251
|
+
*/
|
|
2252
|
+
getSupersedingUris?(
|
|
2253
|
+
uris: string[]
|
|
2254
|
+
): Promise<StoreResult<Map<string, string>>>;
|
|
2255
|
+
|
|
2256
|
+
/**
|
|
2257
|
+
* Indexed typed metadata JSON of active documents, optionally in one
|
|
2258
|
+
* collection, plus the active document count in that scope.
|
|
2259
|
+
*/
|
|
2260
|
+
getTypedMetadataDocuments(options?: {
|
|
2261
|
+
collection?: string;
|
|
2262
|
+
}): Promise<StoreResult<{ documents: number; metadata: string[] }>>;
|
|
2263
|
+
|
|
2248
2264
|
/**
|
|
2249
2265
|
* Get tag counts across all active documents.
|
|
2250
2266
|
* Optionally filter by collection or tag prefix.
|
|
@@ -82,6 +82,32 @@ export function vectorSearchUnavailableMessage(
|
|
|
82
82
|
}`;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
const MAX_CACHED_PROOFS = 100_000;
|
|
86
|
+
const proofCaches = new WeakMap<
|
|
87
|
+
Database,
|
|
88
|
+
{ marker: string; proofs: Map<string, boolean> }
|
|
89
|
+
>();
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Input-hash proofs cached per connection. Any committed write, from this
|
|
93
|
+
* connection or another, changes the marker and drops the cache, so a cached
|
|
94
|
+
* proof never outlives the titles and chunk text it was computed from.
|
|
95
|
+
*/
|
|
96
|
+
function provenOwners(db: Database): Map<string, boolean> {
|
|
97
|
+
const { version, changes } = db
|
|
98
|
+
.query<{ version: number; changes: number }, []>(
|
|
99
|
+
"SELECT (SELECT data_version FROM pragma_data_version) AS version, total_changes() AS changes"
|
|
100
|
+
)
|
|
101
|
+
.get()!;
|
|
102
|
+
const marker = `${version}:${changes}`;
|
|
103
|
+
let cache = proofCaches.get(db);
|
|
104
|
+
if (cache?.marker !== marker || cache.proofs.size > MAX_CACHED_PROOFS) {
|
|
105
|
+
cache = { marker, proofs: new Map() };
|
|
106
|
+
proofCaches.set(db, cache);
|
|
107
|
+
}
|
|
108
|
+
return cache.proofs;
|
|
109
|
+
}
|
|
110
|
+
|
|
85
111
|
/** Null means legacy authority. Once promoted, unavailable provenance fails closed. */
|
|
86
112
|
function searchVectorVariantsInSnapshot(
|
|
87
113
|
db: Database,
|
|
@@ -148,14 +174,9 @@ function searchVectorVariantsInSnapshot(
|
|
|
148
174
|
const tableName = `vec_v1_${partitionId}`;
|
|
149
175
|
if (!db.query("SELECT 1 FROM sqlite_master WHERE name = ?").get(tableName))
|
|
150
176
|
throw new Error("Activated variant index unavailable; run gno embed");
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
`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`
|
|
155
|
-
)
|
|
156
|
-
.get(partitionId)
|
|
157
|
-
)
|
|
158
|
-
throw new Error("Activated variant index inconsistent; run gno embed");
|
|
177
|
+
// The vec0 mirror is written in the same transactions as vector_variants and
|
|
178
|
+
// checked at activation and by `gno vec sync`; ranking below reads only
|
|
179
|
+
// vector_variants, so the mirror is never trusted here.
|
|
159
180
|
const eligible = buildEligibleDocumentQuery(
|
|
160
181
|
{ ...options.eligibility, semanticMetadata: true },
|
|
161
182
|
db
|
|
@@ -170,75 +191,92 @@ function searchVectorVariantsInSnapshot(
|
|
|
170
191
|
conditions.push("o.mirror_hash IN (SELECT value FROM json_each(?))");
|
|
171
192
|
params.push(JSON.stringify(options.allowedMirrorHashes));
|
|
172
193
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
JOIN documents d ON d.id = o.document_id
|
|
218
|
-
JOIN content_chunks c ON c.mirror_hash = o.mirror_hash AND c.seq = o.seq
|
|
219
|
-
JOIN vector_variants v ON v.variant_id = o.variant_id AND v.partition_id = o.partition_id
|
|
220
|
-
JOIN ${tableName} x ON x.variant_id = v.variant_id AND x.embedding = v.embedding
|
|
221
|
-
WHERE ${conditions.join(" AND ")}
|
|
222
|
-
AND (o.document_id || ':' || o.seq) IN (SELECT value FROM json_each(?))
|
|
223
|
-
GROUP BY v.variant_id, o.mirror_hash, o.seq
|
|
224
|
-
ORDER BY distance, o.mirror_hash, o.seq, v.variant_id LIMIT ?
|
|
225
|
-
`)
|
|
226
|
-
.all(
|
|
227
|
-
encodeEmbedding(embedding),
|
|
194
|
+
// Owner eligibility precedes ranking/LIMIT. Grouping before the distance
|
|
195
|
+
// keeps embedding blobs out of the group sorter.
|
|
196
|
+
const ranked = db.query<
|
|
197
|
+
{
|
|
198
|
+
mirrorHash: string;
|
|
199
|
+
seq: number;
|
|
200
|
+
inputHash: string;
|
|
201
|
+
distance: number;
|
|
202
|
+
documentIds: string;
|
|
203
|
+
},
|
|
204
|
+
(Uint8Array | string | number)[]
|
|
205
|
+
>(`
|
|
206
|
+
SELECT g.mirrorHash, g.seq, v.input_hash AS inputHash,
|
|
207
|
+
vec_distance_cosine(v.embedding, ?) AS distance, g.documentIds
|
|
208
|
+
FROM (
|
|
209
|
+
SELECT o.variant_id AS variantId, o.mirror_hash AS mirrorHash, o.seq,
|
|
210
|
+
json_group_array(o.document_id) AS documentIds
|
|
211
|
+
FROM vector_owners o
|
|
212
|
+
JOIN (${eligible.sql}) e ON e.id = o.document_id AND e.mirror_hash = o.mirror_hash
|
|
213
|
+
JOIN content_chunks c ON c.mirror_hash = o.mirror_hash AND c.seq = o.seq
|
|
214
|
+
WHERE ${conditions.join(" AND ")}
|
|
215
|
+
GROUP BY o.variant_id, o.mirror_hash, o.seq
|
|
216
|
+
) g
|
|
217
|
+
JOIN vector_variants v ON v.variant_id = g.variantId AND v.partition_id = ?
|
|
218
|
+
ORDER BY distance, g.mirrorHash, g.seq, g.variantId LIMIT ?
|
|
219
|
+
`);
|
|
220
|
+
const owners = db.query<
|
|
221
|
+
{ id: number; title: string | null; text: string },
|
|
222
|
+
[string, number, string]
|
|
223
|
+
>(`
|
|
224
|
+
SELECT d.id, d.title, c.text FROM documents d
|
|
225
|
+
JOIN content_chunks c ON c.mirror_hash = ? AND c.seq = ?
|
|
226
|
+
WHERE d.id IN (SELECT value FROM json_each(?))
|
|
227
|
+
`);
|
|
228
|
+
// A vector counts only while it matches its owner's current formatted
|
|
229
|
+
// input. Proving candidates in rank order and skipping unproven owners
|
|
230
|
+
// returns exactly what filtering every owner before ranking would.
|
|
231
|
+
const proofs = provenOwners(db);
|
|
232
|
+
const queryVector = encodeEmbedding(embedding);
|
|
233
|
+
let limit = k;
|
|
234
|
+
let results: VectorSearchResult[] = [];
|
|
235
|
+
for (;;) {
|
|
236
|
+
const rows = ranked.all(
|
|
237
|
+
queryVector,
|
|
228
238
|
...eligible.params,
|
|
229
239
|
...params,
|
|
230
|
-
|
|
231
|
-
|
|
240
|
+
partitionId,
|
|
241
|
+
limit
|
|
232
242
|
);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
(row)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
243
|
+
results = [];
|
|
244
|
+
for (const row of rows) {
|
|
245
|
+
const documentIds = JSON.parse(row.documentIds) as number[];
|
|
246
|
+
const key = (id: number) =>
|
|
247
|
+
`${model}\0${id}\0${row.mirrorHash}\0${row.seq}\0${row.inputHash}`;
|
|
248
|
+
const unknown = documentIds.filter((id) => !proofs.has(key(id)));
|
|
249
|
+
if (unknown.length > 0) {
|
|
250
|
+
for (const owner of owners.all(
|
|
251
|
+
row.mirrorHash,
|
|
252
|
+
row.seq,
|
|
253
|
+
JSON.stringify(unknown)
|
|
254
|
+
)) {
|
|
255
|
+
proofs.set(
|
|
256
|
+
key(owner.id),
|
|
257
|
+
embeddingInputHash(
|
|
258
|
+
formatDocForEmbedding(owner.text, owner.title ?? undefined, model)
|
|
259
|
+
) === row.inputHash
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const proven = documentIds.filter((id) => proofs.get(key(id)));
|
|
264
|
+
if (proven.length === 0) continue;
|
|
265
|
+
results.push({
|
|
266
|
+
mirrorHash: row.mirrorHash,
|
|
267
|
+
seq: row.seq,
|
|
268
|
+
distance: row.distance,
|
|
269
|
+
documentIds: proven,
|
|
270
|
+
});
|
|
271
|
+
if (results.length === k) break;
|
|
272
|
+
}
|
|
273
|
+
if (results.length === k || rows.length < limit) break;
|
|
274
|
+
limit *= 4;
|
|
275
|
+
}
|
|
276
|
+
return results.filter(
|
|
277
|
+
(row) =>
|
|
278
|
+
options.minScore === undefined || 1 - row.distance >= options.minScore
|
|
279
|
+
);
|
|
242
280
|
}
|
|
243
281
|
|
|
244
282
|
/** Keep eligibility, provenance validation and ranking on one SQLite snapshot. */
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
56174f38783259cef05d81ae53adceebc8467799b735c2f3026b928f7070ae76 gno-browser-clipper-v2.10.0.zip
|