@gmickel/gno 2.10.1 → 2.12.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 +27 -15
- package/assets/skill/cli-reference.md +6 -5
- package/assets/skill/examples.md +16 -12
- package/assets/skill/mcp-reference.md +2 -1
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.10.1.zip → gno-browser-clipper-v2.12.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.12.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +10 -5
- package/spec/cli.md +127 -35
- package/spec/evals-agentic.md +3 -1
- package/spec/mcp.md +36 -6
- 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/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/spec/output-schemas/status.schema.json +28 -0
- package/src/app/constants.ts +16 -0
- package/src/app/context-agent-projection.ts +10 -1
- package/src/app/context-format.ts +1 -0
- package/src/app/verified-ask.ts +2 -0
- package/src/cli/commands/agents/block.ts +10 -9
- package/src/cli/commands/agents/commands.ts +6 -5
- package/src/cli/commands/agents/engine.ts +15 -2
- package/src/cli/commands/agents/harnesses.ts +21 -14
- package/src/cli/commands/ask-format.ts +18 -1
- package/src/cli/commands/ask.ts +4 -5
- package/src/cli/commands/cleanup.ts +46 -6
- package/src/cli/commands/collection/remove.ts +1 -0
- package/src/cli/commands/collection/rename.ts +1 -0
- package/src/cli/commands/daemon.ts +5 -1
- package/src/cli/commands/doctor.ts +37 -0
- package/src/cli/commands/get.ts +11 -1
- package/src/cli/commands/mcp/paths.ts +2 -1
- package/src/cli/commands/metadata.ts +79 -0
- package/src/cli/commands/multi-get.ts +13 -0
- package/src/cli/commands/setup-semantic.ts +6 -1
- package/src/cli/commands/shared.ts +35 -8
- package/src/cli/commands/skill/paths.ts +20 -2
- package/src/cli/commands/status.ts +89 -15
- package/src/cli/format/search-results.ts +11 -0
- package/src/cli/options.ts +2 -0
- package/src/cli/program.ts +64 -3
- package/src/core/activation-probe-plan.ts +0 -1
- package/src/core/config-mutation.ts +72 -55
- 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/memory-remember.ts +0 -1
- package/src/core/metadata-discovery.ts +169 -0
- package/src/core/module-path.ts +33 -0
- package/src/core/runtime-entrypoint.ts +3 -1
- package/src/ingestion/file-processor.ts +6 -3
- package/src/ingestion/prepare-file.ts +14 -4
- package/src/ingestion/source-availability/root.ts +138 -0
- package/src/ingestion/sync.ts +35 -1
- package/src/ingestion/walker.ts +11 -3
- package/src/llm/native-worker/client.ts +8 -4
- package/src/llm/native-worker/errors.ts +26 -3
- package/src/mcp/http-egress.ts +1 -0
- package/src/mcp/http-transport.ts +7 -2
- package/src/mcp/server.ts +3 -0
- package/src/mcp/tool-descriptions-core.ts +3 -3
- package/src/mcp/tool-profile.ts +16 -1
- package/src/mcp/tools/get.ts +10 -0
- package/src/mcp/tools/index.ts +68 -4
- package/src/mcp/tools/multi-get.ts +15 -0
- package/src/mcp/tools/query.ts +5 -3
- package/src/mcp/tools/search.ts +5 -3
- package/src/mcp/tools/status.ts +17 -2
- 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/query-snippet.ts +266 -0
- package/src/pipeline/result-context.ts +29 -0
- package/src/pipeline/search.ts +154 -20
- package/src/pipeline/types.ts +4 -0
- package/src/pipeline/vsearch.ts +5 -1
- package/src/sdk/client.ts +46 -6
- package/src/sdk/documents.ts +5 -0
- package/src/sdk/types.ts +11 -1
- package/src/serve/public/components/ThoroughnessSelector.tsx +1 -1
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Collections.tsx +8 -0
- package/src/serve/public/pages/Search.tsx +9 -1
- package/src/serve/resident-request.ts +8 -0
- package/src/serve/resident-runtime.ts +25 -3
- package/src/serve/routes/api.ts +29 -1
- package/src/serve/server.ts +12 -0
- package/src/serve/spa-production-build.ts +7 -3
- package/src/serve/status-model.ts +2 -0
- package/src/serve/status.ts +29 -0
- package/src/sessions/config-refresh.ts +6 -0
- package/src/sessions/import-child.ts +4 -1
- package/src/store/sqlite/adapter.ts +241 -20
- package/src/store/types.ts +61 -7
- package/src/store/vector/variant-search.ts +112 -74
- package/browser-extension/artifacts/gno-browser-clipper-v2.10.1.zip.sha256 +0 -1
|
@@ -103,8 +103,10 @@ import type {
|
|
|
103
103
|
SavedCapsuleReverificationState,
|
|
104
104
|
SavedCapsuleVerificationExpectation,
|
|
105
105
|
SavedCapsuleVerificationRecord,
|
|
106
|
+
StaleCollection,
|
|
106
107
|
StorePort,
|
|
107
108
|
StoreResult,
|
|
109
|
+
SyncCollectionsOptions,
|
|
108
110
|
TagCount,
|
|
109
111
|
TagRow,
|
|
110
112
|
TagSource,
|
|
@@ -585,6 +587,27 @@ function resolveBusyTimeoutMs(busyTimeoutMs?: number): number {
|
|
|
585
587
|
return busyTimeoutMs;
|
|
586
588
|
}
|
|
587
589
|
|
|
590
|
+
/** The index holds collections the config omits; the message names them. */
|
|
591
|
+
export class StaleCollectionsError extends Error {}
|
|
592
|
+
|
|
593
|
+
/** Names the collections a config omits and how to resolve it. */
|
|
594
|
+
export function staleCollectionsMessage(
|
|
595
|
+
stale: readonly StaleCollection[]
|
|
596
|
+
): string {
|
|
597
|
+
const names = stale
|
|
598
|
+
.map(
|
|
599
|
+
(c) =>
|
|
600
|
+
`${c.name} (${c.documents} ${c.documents === 1 ? "document" : "documents"})`
|
|
601
|
+
)
|
|
602
|
+
.join(", ");
|
|
603
|
+
return (
|
|
604
|
+
`This index holds collections the config does not list: ${names}. ` +
|
|
605
|
+
"Nothing was changed. Add them back to the config, open a separate index " +
|
|
606
|
+
'(--index <name>, or dbPath in the SDK), or run "gno cleanup" with this ' +
|
|
607
|
+
"config to delete their index data."
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
588
611
|
export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
589
612
|
private db: Database | null = null;
|
|
590
613
|
private dbPath = "";
|
|
@@ -898,24 +921,36 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
898
921
|
// Config Sync
|
|
899
922
|
// ─────────────────────────────────────────────────────────────────────────
|
|
900
923
|
|
|
901
|
-
async syncCollections(
|
|
924
|
+
async syncCollections(
|
|
925
|
+
collections: Collection[],
|
|
926
|
+
options: SyncCollectionsOptions = {}
|
|
927
|
+
): Promise<StoreResult<void>> {
|
|
928
|
+
let stale: StaleCollection[] = [];
|
|
902
929
|
try {
|
|
903
930
|
const db = this.ensureOpen();
|
|
904
931
|
|
|
905
932
|
const transaction = db.transaction(() => {
|
|
906
|
-
//
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
//
|
|
917
|
-
|
|
918
|
-
|
|
933
|
+
// Opening an index with a config never deletes indexed collections
|
|
934
|
+
// that config omits (their documents follow by cascade): a different
|
|
935
|
+
// config on the same index would wipe it. Only explicit removals do.
|
|
936
|
+
const { remove } = options;
|
|
937
|
+
const removed = new Set(remove === "stale" ? [] : remove);
|
|
938
|
+
const unlisted = this.queryStaleCollections(
|
|
939
|
+
db,
|
|
940
|
+
collections.map((c) => c.name)
|
|
941
|
+
).filter((c) => remove !== "stale" && !removed.has(c.name));
|
|
942
|
+
// Unlisted collections without documents stay too; they only block
|
|
943
|
+
// the sync when they hold indexed documents.
|
|
944
|
+
stale = unlisted.filter((c) => c.documents > 0);
|
|
945
|
+
if (stale.length > 0 && !options.keepStale) {
|
|
946
|
+
throw new StaleCollectionsError();
|
|
947
|
+
}
|
|
948
|
+
const keep = new Set(unlisted.map((c) => c.name));
|
|
949
|
+
for (const { name } of this.queryStaleCollections(
|
|
950
|
+
db,
|
|
951
|
+
collections.map((c) => c.name)
|
|
952
|
+
)) {
|
|
953
|
+
if (!keep.has(name)) {
|
|
919
954
|
db.run("DELETE FROM collections WHERE name = ?", [name]);
|
|
920
955
|
}
|
|
921
956
|
}
|
|
@@ -985,6 +1020,9 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
985
1020
|
transaction.immediate();
|
|
986
1021
|
return ok(undefined);
|
|
987
1022
|
} catch (cause) {
|
|
1023
|
+
if (cause instanceof StaleCollectionsError) {
|
|
1024
|
+
return err("STALE_COLLECTIONS", staleCollectionsMessage(stale));
|
|
1025
|
+
}
|
|
988
1026
|
return err(
|
|
989
1027
|
"QUERY_FAILED",
|
|
990
1028
|
cause instanceof Error ? cause.message : "Failed to sync collections",
|
|
@@ -993,6 +1031,55 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
993
1031
|
}
|
|
994
1032
|
}
|
|
995
1033
|
|
|
1034
|
+
async listStaleCollections(
|
|
1035
|
+
configured: readonly string[]
|
|
1036
|
+
): Promise<StoreResult<StaleCollection[]>> {
|
|
1037
|
+
try {
|
|
1038
|
+
return ok(this.queryStaleCollections(this.ensureOpen(), configured));
|
|
1039
|
+
} catch (cause) {
|
|
1040
|
+
return err(
|
|
1041
|
+
"QUERY_FAILED",
|
|
1042
|
+
cause instanceof Error ? cause.message : "Failed to list collections",
|
|
1043
|
+
cause
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Indexed collections `configured` omits, checked cheaply on each read of a
|
|
1050
|
+
* long-lived client: another client may add a collection after it opened.
|
|
1051
|
+
*/
|
|
1052
|
+
staleIndexedCollections(configured: readonly string[]): StaleCollection[] {
|
|
1053
|
+
const db = this.ensureOpen();
|
|
1054
|
+
const listed = new Set(configured);
|
|
1055
|
+
const indexed = db
|
|
1056
|
+
.query<{ name: string }, []>(
|
|
1057
|
+
`SELECT c.name AS name FROM collections c
|
|
1058
|
+
WHERE EXISTS (SELECT 1 FROM documents d WHERE d.collection = c.name)`
|
|
1059
|
+
)
|
|
1060
|
+
.all();
|
|
1061
|
+
return indexed.some(({ name }) => !listed.has(name))
|
|
1062
|
+
? this.queryStaleCollections(db, configured).filter(
|
|
1063
|
+
(c) => c.documents > 0
|
|
1064
|
+
)
|
|
1065
|
+
: [];
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
private queryStaleCollections(
|
|
1069
|
+
db: Database,
|
|
1070
|
+
configured: readonly string[]
|
|
1071
|
+
): StaleCollection[] {
|
|
1072
|
+
const listed = new Set(configured);
|
|
1073
|
+
return db
|
|
1074
|
+
.query<StaleCollection, []>(
|
|
1075
|
+
`SELECT c.name AS name,
|
|
1076
|
+
(SELECT COUNT(*) FROM documents d WHERE d.collection = c.name) AS documents
|
|
1077
|
+
FROM collections c ORDER BY c.name`
|
|
1078
|
+
)
|
|
1079
|
+
.all()
|
|
1080
|
+
.filter((c) => !listed.has(c.name));
|
|
1081
|
+
}
|
|
1082
|
+
|
|
996
1083
|
async upsertCollections(
|
|
997
1084
|
collections: Collection[]
|
|
998
1085
|
): Promise<StoreResult<void>> {
|
|
@@ -3119,6 +3206,64 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
3119
3206
|
}
|
|
3120
3207
|
}
|
|
3121
3208
|
|
|
3209
|
+
async getChunkTermPresence(
|
|
3210
|
+
mirrorHashes: string[],
|
|
3211
|
+
terms: string[]
|
|
3212
|
+
): Promise<
|
|
3213
|
+
StoreResult<{ mirrorHash: string; seq: number; present: boolean[] }[]>
|
|
3214
|
+
> {
|
|
3215
|
+
try {
|
|
3216
|
+
const uniqueHashes = uniqueNonEmptyValues(mirrorHashes);
|
|
3217
|
+
if (uniqueHashes.length === 0 || terms.length === 0) return ok([]);
|
|
3218
|
+
const db = this.ensureOpen();
|
|
3219
|
+
// SQLite lower() folds ASCII only, so a non-ASCII term is also looked
|
|
3220
|
+
// up in its upper-case and capitalized spellings (ÜBERBLICK, Überblick).
|
|
3221
|
+
const spellings = terms.flatMap((term) => [
|
|
3222
|
+
term,
|
|
3223
|
+
term.toUpperCase(),
|
|
3224
|
+
`${term.charAt(0).toUpperCase()}${term.slice(1)}`,
|
|
3225
|
+
]);
|
|
3226
|
+
const columns = terms
|
|
3227
|
+
.map(
|
|
3228
|
+
() =>
|
|
3229
|
+
"(instr(lower(text), ?) > 0 OR instr(text, ?) > 0 OR instr(text, ?) > 0)"
|
|
3230
|
+
)
|
|
3231
|
+
.join(", ");
|
|
3232
|
+
const batchSize = Math.max(
|
|
3233
|
+
1,
|
|
3234
|
+
SQLITE_SAFE_PARAMETER_BATCH_SIZE - spellings.length
|
|
3235
|
+
);
|
|
3236
|
+
const rows: { mirrorHash: string; seq: number; present: boolean[] }[] =
|
|
3237
|
+
[];
|
|
3238
|
+
for (let i = 0; i < uniqueHashes.length; i += batchSize) {
|
|
3239
|
+
const batch = uniqueHashes.slice(i, i + batchSize);
|
|
3240
|
+
const values = db
|
|
3241
|
+
.query<unknown, string[]>(
|
|
3242
|
+
`SELECT mirror_hash, seq, ${columns} FROM content_chunks
|
|
3243
|
+
WHERE mirror_hash IN (${batch.map(() => "?").join(",")})
|
|
3244
|
+
ORDER BY mirror_hash, seq`
|
|
3245
|
+
)
|
|
3246
|
+
.values(...spellings, ...batch) as unknown[][];
|
|
3247
|
+
for (const [mirrorHash, seq, ...present] of values) {
|
|
3248
|
+
rows.push({
|
|
3249
|
+
mirrorHash: String(mirrorHash),
|
|
3250
|
+
seq: Number(seq),
|
|
3251
|
+
present: present.map((flag) => flag === 1),
|
|
3252
|
+
});
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
return ok(rows);
|
|
3256
|
+
} catch (cause) {
|
|
3257
|
+
return err(
|
|
3258
|
+
"QUERY_FAILED",
|
|
3259
|
+
cause instanceof Error
|
|
3260
|
+
? cause.message
|
|
3261
|
+
: "Failed to read chunk term presence",
|
|
3262
|
+
cause
|
|
3263
|
+
);
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3122
3267
|
// ─────────────────────────────────────────────────────────────────────────
|
|
3123
3268
|
// FTS Search
|
|
3124
3269
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -3143,7 +3288,6 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
3143
3288
|
WITH fts_matches AS (
|
|
3144
3289
|
SELECT
|
|
3145
3290
|
rowid,
|
|
3146
|
-
${options.snippet ? "snippet(documents_fts, 2, '<mark>', '</mark>', '...', 32) as snippet," : ""}
|
|
3147
3291
|
bm25(
|
|
3148
3292
|
documents_fts,
|
|
3149
3293
|
${FTS5_FIELD_WEIGHTS.filepath},
|
|
@@ -3163,7 +3307,6 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
3163
3307
|
d.mirror_hash,
|
|
3164
3308
|
${options.chunkLanguage ? "(SELECT min(lc.seq) FROM content_chunks lc WHERE lc.mirror_hash = d.mirror_hash AND lc.language = ?) as seq," : "0 as seq,"}
|
|
3165
3309
|
fm.score as score,
|
|
3166
|
-
${options.snippet ? "fm.snippet as snippet," : ""}
|
|
3167
3310
|
d.docid,
|
|
3168
3311
|
d.uri,
|
|
3169
3312
|
d.title,
|
|
@@ -3197,7 +3340,6 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
3197
3340
|
mirror_hash: string;
|
|
3198
3341
|
seq: number;
|
|
3199
3342
|
score: number;
|
|
3200
|
-
snippet?: string;
|
|
3201
3343
|
docid: string;
|
|
3202
3344
|
uri: string;
|
|
3203
3345
|
title: string | null;
|
|
@@ -3247,7 +3389,6 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
3247
3389
|
mirrorHash: r.mirror_hash,
|
|
3248
3390
|
seq: r.seq,
|
|
3249
3391
|
score: r.score, // Raw bm25() - smaller (more negative) is better
|
|
3250
|
-
snippet: r.snippet,
|
|
3251
3392
|
docid: r.docid,
|
|
3252
3393
|
uri: r.uri,
|
|
3253
3394
|
title: r.title ?? undefined,
|
|
@@ -3693,9 +3834,89 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
3693
3834
|
}
|
|
3694
3835
|
|
|
3695
3836
|
/**
|
|
3696
|
-
*
|
|
3697
|
-
*
|
|
3837
|
+
* Map each superseded memory fact's URI to the end of its supersession
|
|
3838
|
+
* chain. Input URIs may carry `?index=`; keys are the URIs as given and
|
|
3839
|
+
* successors are canonical (undecorated).
|
|
3698
3840
|
*/
|
|
3841
|
+
async getSupersedingUris(
|
|
3842
|
+
uris: string[]
|
|
3843
|
+
): Promise<StoreResult<Map<string, string>>> {
|
|
3844
|
+
try {
|
|
3845
|
+
const db = this.ensureOpen();
|
|
3846
|
+
const successorOf = db.query<
|
|
3847
|
+
{ uri: string; successor: string },
|
|
3848
|
+
[string]
|
|
3849
|
+
>(`
|
|
3850
|
+
SELECT p.uri AS uri, s.uri AS successor
|
|
3851
|
+
FROM documents p
|
|
3852
|
+
JOIN doc_edges e ON e.dst_doc_id = p.id AND e.edge_type = 'supersedes'
|
|
3853
|
+
JOIN documents s ON s.id = e.src_doc_id AND s.active = 1
|
|
3854
|
+
WHERE p.uri IN (SELECT value FROM json_each(?))
|
|
3855
|
+
ORDER BY s.id
|
|
3856
|
+
`);
|
|
3857
|
+
const canonical = new Map(uris.map((uri) => [uri, stripUriIndex(uri)]));
|
|
3858
|
+
const next = new Map<string, string>();
|
|
3859
|
+
const seen = new Set(canonical.values());
|
|
3860
|
+
let frontier = [...seen];
|
|
3861
|
+
while (frontier.length > 0) {
|
|
3862
|
+
const discovered: string[] = [];
|
|
3863
|
+
for (const row of successorOf.all(JSON.stringify(frontier))) {
|
|
3864
|
+
next.set(row.uri, row.successor);
|
|
3865
|
+
if (!seen.has(row.successor)) {
|
|
3866
|
+
seen.add(row.successor);
|
|
3867
|
+
discovered.push(row.successor);
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
frontier = discovered;
|
|
3871
|
+
}
|
|
3872
|
+
const result = new Map<string, string>();
|
|
3873
|
+
for (const [input, uri] of canonical) {
|
|
3874
|
+
const visited = new Set([uri]);
|
|
3875
|
+
let current = next.get(uri);
|
|
3876
|
+
while (current && next.has(current) && !visited.has(current)) {
|
|
3877
|
+
visited.add(current);
|
|
3878
|
+
current = next.get(current);
|
|
3879
|
+
}
|
|
3880
|
+
if (current && current !== uri) result.set(input, current);
|
|
3881
|
+
}
|
|
3882
|
+
return ok(result);
|
|
3883
|
+
} catch (cause) {
|
|
3884
|
+
return err(
|
|
3885
|
+
"QUERY_FAILED",
|
|
3886
|
+
cause instanceof Error ? cause.message : "Failed to read supersession",
|
|
3887
|
+
cause
|
|
3888
|
+
);
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3892
|
+
async getTypedMetadataDocuments(options?: {
|
|
3893
|
+
collection?: string;
|
|
3894
|
+
}): Promise<StoreResult<{ documents: number; metadata: string[] }>> {
|
|
3895
|
+
try {
|
|
3896
|
+
const db = this.ensureOpen();
|
|
3897
|
+
const scope = options?.collection ? " AND collection = ?" : "";
|
|
3898
|
+
const params = options?.collection ? [options.collection] : [];
|
|
3899
|
+
const documents = db
|
|
3900
|
+
.query<{ count: number }, string[]>(
|
|
3901
|
+
`SELECT count(*) AS count FROM documents WHERE active = 1${scope}`
|
|
3902
|
+
)
|
|
3903
|
+
.get(...params)!.count;
|
|
3904
|
+
const metadata = db
|
|
3905
|
+
.query<{ typed_metadata: string }, string[]>(
|
|
3906
|
+
`SELECT typed_metadata FROM documents WHERE active = 1 AND typed_metadata IS NOT NULL${scope}`
|
|
3907
|
+
)
|
|
3908
|
+
.all(...params)
|
|
3909
|
+
.map((row) => row.typed_metadata);
|
|
3910
|
+
return ok({ documents, metadata });
|
|
3911
|
+
} catch (cause) {
|
|
3912
|
+
return err(
|
|
3913
|
+
"QUERY_FAILED",
|
|
3914
|
+
cause instanceof Error ? cause.message : "Failed to read metadata",
|
|
3915
|
+
cause
|
|
3916
|
+
);
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3699
3920
|
async getTagCounts(options?: {
|
|
3700
3921
|
collection?: string;
|
|
3701
3922
|
prefix?: string;
|
package/src/store/types.ts
CHANGED
|
@@ -68,7 +68,23 @@ export type StoreErrorCode =
|
|
|
68
68
|
| "VEC_REBUILD_FAILED"
|
|
69
69
|
| "VEC_SYNC_FAILED"
|
|
70
70
|
/** Embedding would build a separate vector partition without confirmation. */
|
|
71
|
-
| "VECTOR_PARTITION_FORK"
|
|
71
|
+
| "VECTOR_PARTITION_FORK"
|
|
72
|
+
/** The index holds indexed collections the opening config does not list. */
|
|
73
|
+
| "STALE_COLLECTIONS";
|
|
74
|
+
|
|
75
|
+
/** How a config sync treats collections the config no longer lists. */
|
|
76
|
+
export interface SyncCollectionsOptions {
|
|
77
|
+
/** Explicitly removed names, or every unlisted collection ("stale"). */
|
|
78
|
+
remove?: readonly string[] | "stale";
|
|
79
|
+
/** Leave unlisted collections untouched instead of failing. */
|
|
80
|
+
keepStale?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A collection in the index that the opening config does not list. */
|
|
84
|
+
export interface StaleCollection {
|
|
85
|
+
name: string;
|
|
86
|
+
documents: number;
|
|
87
|
+
}
|
|
72
88
|
|
|
73
89
|
/** Store error with structured details */
|
|
74
90
|
export interface StoreError {
|
|
@@ -738,8 +754,6 @@ export interface FtsSearchOptions extends DocumentEligibilityOptions {
|
|
|
738
754
|
* so runtime language filtering is not currently implemented.
|
|
739
755
|
*/
|
|
740
756
|
language?: string;
|
|
741
|
-
/** Include snippet with highlights */
|
|
742
|
-
snippet?: boolean;
|
|
743
757
|
/** Match documents containing ANY positive term instead of ALL of them. */
|
|
744
758
|
anyTerm?: boolean;
|
|
745
759
|
/**
|
|
@@ -770,7 +784,6 @@ export interface FtsResult {
|
|
|
770
784
|
mirrorHash: string;
|
|
771
785
|
seq: number;
|
|
772
786
|
score: number;
|
|
773
|
-
snippet?: string;
|
|
774
787
|
// Joined from documents table
|
|
775
788
|
docid?: string;
|
|
776
789
|
uri?: string;
|
|
@@ -1614,10 +1627,21 @@ export interface StorePort {
|
|
|
1614
1627
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1615
1628
|
|
|
1616
1629
|
/**
|
|
1617
|
-
* Sync collections from config to DB.
|
|
1618
|
-
*
|
|
1630
|
+
* Sync collections from config to DB: adds new and updates existing ones.
|
|
1631
|
+
* A collection in the index but not in `collections` is deleted only when
|
|
1632
|
+
* `options.remove` names it (or is "stale"). One that holds documents
|
|
1633
|
+
* otherwise fails the sync with STALE_COLLECTIONS and changes nothing,
|
|
1634
|
+
* unless `keepStale` leaves it untouched.
|
|
1619
1635
|
*/
|
|
1620
|
-
syncCollections(
|
|
1636
|
+
syncCollections(
|
|
1637
|
+
collections: Collection[],
|
|
1638
|
+
options?: SyncCollectionsOptions
|
|
1639
|
+
): Promise<StoreResult<void>>;
|
|
1640
|
+
|
|
1641
|
+
/** Collections in the index that `configured` does not list, with counts. */
|
|
1642
|
+
listStaleCollections(
|
|
1643
|
+
configured: readonly string[]
|
|
1644
|
+
): Promise<StoreResult<StaleCollection[]>>;
|
|
1621
1645
|
|
|
1622
1646
|
/**
|
|
1623
1647
|
* Sync contexts from config to DB.
|
|
@@ -2159,6 +2183,20 @@ export interface StorePort {
|
|
|
2159
2183
|
keys: { mirrorHash: string; seq: number }[]
|
|
2160
2184
|
): Promise<StoreResult<Map<string, ChunkRow[]>>>;
|
|
2161
2185
|
|
|
2186
|
+
/**
|
|
2187
|
+
* Optional: for every chunk of the given documents, which of the given
|
|
2188
|
+
* lowercase terms occur in its text (substring match on ASCII-lowercased
|
|
2189
|
+
* text), computed in the database so chunk text is never loaded. Rows are
|
|
2190
|
+
* ordered by mirrorHash, then seq; `present[i]` answers `terms[i]`. Callers
|
|
2191
|
+
* fall back to getChunksBatch when this capability is absent.
|
|
2192
|
+
*/
|
|
2193
|
+
getChunkTermPresence?(
|
|
2194
|
+
mirrorHashes: string[],
|
|
2195
|
+
terms: string[]
|
|
2196
|
+
): Promise<
|
|
2197
|
+
StoreResult<{ mirrorHash: string; seq: number; present: boolean[] }[]>
|
|
2198
|
+
>;
|
|
2199
|
+
|
|
2162
2200
|
// ─────────────────────────────────────────────────────────────────────────
|
|
2163
2201
|
// FTS Search
|
|
2164
2202
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -2245,6 +2283,22 @@ export interface StorePort {
|
|
|
2245
2283
|
documentIds: number[]
|
|
2246
2284
|
): Promise<StoreResult<Map<number, TagRow[]>>>;
|
|
2247
2285
|
|
|
2286
|
+
/**
|
|
2287
|
+
* For each URI whose document an active document supersedes, the URI of
|
|
2288
|
+
* its current successor (the end of the supersession chain).
|
|
2289
|
+
*/
|
|
2290
|
+
getSupersedingUris?(
|
|
2291
|
+
uris: string[]
|
|
2292
|
+
): Promise<StoreResult<Map<string, string>>>;
|
|
2293
|
+
|
|
2294
|
+
/**
|
|
2295
|
+
* Indexed typed metadata JSON of active documents, optionally in one
|
|
2296
|
+
* collection, plus the active document count in that scope.
|
|
2297
|
+
*/
|
|
2298
|
+
getTypedMetadataDocuments(options?: {
|
|
2299
|
+
collection?: string;
|
|
2300
|
+
}): Promise<StoreResult<{ documents: number; metadata: string[] }>>;
|
|
2301
|
+
|
|
2248
2302
|
/**
|
|
2249
2303
|
* Get tag counts across all active documents.
|
|
2250
2304
|
* 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
|
-
e42897b28d0fc0fca7d1d7aca6ab016255cc3b5bf20d38bcac51535499eb7f25 gno-browser-clipper-v2.10.1.zip
|