@gmickel/gno 2.11.0 → 2.12.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 +3 -2
- package/assets/spa-production.json.gz +0 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v2.11.0.zip → gno-browser-clipper-v2.12.1.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v2.12.1.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +8 -5
- package/spec/cli.md +100 -34
- package/spec/evals-agentic.md +3 -1
- package/spec/mcp.md +9 -3
- package/spec/output-schemas/status.schema.json +28 -0
- package/src/app/context-agent-projection.ts +10 -1
- package/src/cli/commands/agents/block.ts +2 -2
- 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/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/doctor.ts +37 -0
- package/src/cli/commands/mcp/paths.ts +2 -1
- package/src/cli/commands/setup-semantic.ts +6 -1
- package/src/cli/commands/shared.ts +26 -1
- package/src/cli/commands/skill/paths.ts +20 -2
- package/src/cli/commands/status.ts +89 -15
- package/src/cli/program.ts +25 -5
- package/src/core/activation-probe-plan.ts +0 -1
- package/src/core/config-mutation.ts +72 -55
- package/src/core/memory-remember.ts +0 -1
- 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 +5 -19
- package/src/llm/native-worker/client.ts +8 -4
- package/src/llm/native-worker/errors.ts +26 -3
- package/src/mcp/http-transport.ts +7 -2
- package/src/mcp/server.ts +3 -0
- package/src/mcp/tool-descriptions-core.ts +2 -2
- package/src/mcp/tools/index.ts +30 -1
- package/src/mcp/tools/status.ts +17 -2
- package/src/pipeline/query-snippet.ts +266 -0
- package/src/pipeline/search.ts +149 -19
- package/src/sdk/client.ts +14 -2
- package/src/sdk/types.ts +6 -1
- package/src/serve/public/components/ThoroughnessSelector.tsx +1 -1
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/index.html +5 -0
- 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/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 +349 -200
- package/src/store/types.ts +45 -7
- package/browser-extension/artifacts/gno-browser-clipper-v2.11.0.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,
|
|
@@ -4935,20 +5076,19 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
4935
5076
|
const edgeTypeFilterFor = (alias: string): string =>
|
|
4936
5077
|
edgeType ? `AND ${alias}.edge_type = ?` : "";
|
|
4937
5078
|
const candidateEdgeLimit = Math.min(frontierLimit * 4, 4_000);
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
const frontierJoinClause = (frontierName: string): string =>
|
|
5079
|
+
// Each level runs as its own statement over the previous frontier and
|
|
5080
|
+
// the visited set, passed as JSON. One statement chaining every level
|
|
5081
|
+
// as CTEs re-expands each earlier level at every reference, which
|
|
5082
|
+
// grows exponentially with depth: SQLite 3.54 refuses it from depth 3
|
|
5083
|
+
// ("nested too deep") and older versions fail at depth 6.
|
|
5084
|
+
const visitedIds = "SELECT doc_id FROM visited";
|
|
5085
|
+
const frontierJoinClause =
|
|
4946
5086
|
direction === "out"
|
|
4947
|
-
?
|
|
5087
|
+
? "e.src_doc_id = prev.doc_id"
|
|
4948
5088
|
: direction === "in"
|
|
4949
|
-
?
|
|
4950
|
-
:
|
|
4951
|
-
const boundedEdgePredicate = (
|
|
5089
|
+
? "e.dst_doc_id = prev.doc_id"
|
|
5090
|
+
: "(e.src_doc_id = prev.doc_id OR e.dst_doc_id = prev.doc_id)";
|
|
5091
|
+
const boundedEdgePredicate = (): string => {
|
|
4952
5092
|
if (direction === "out") {
|
|
4953
5093
|
return `
|
|
4954
5094
|
e.id IN (
|
|
@@ -4969,10 +5109,10 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
4969
5109
|
) AS next_rank
|
|
4970
5110
|
FROM doc_edges e2
|
|
4971
5111
|
JOIN documents next_doc ON next_doc.id = e2.dst_doc_id AND next_doc.active = 1${nextDocScope}
|
|
4972
|
-
WHERE e2.src_doc_id =
|
|
5112
|
+
WHERE e2.src_doc_id = prev.doc_id
|
|
4973
5113
|
${edgeTypeFilterFor("e2")}
|
|
4974
|
-
AND instr(
|
|
4975
|
-
AND e2.dst_doc_id NOT IN (${
|
|
5114
|
+
AND instr(prev.path, printf(',%d,', e2.dst_doc_id)) = 0
|
|
5115
|
+
AND e2.dst_doc_id NOT IN (${visitedIds})
|
|
4976
5116
|
)
|
|
4977
5117
|
WHERE next_rank = 1
|
|
4978
5118
|
ORDER BY edge_type ASC, next_doc_id ASC, edge_id ASC
|
|
@@ -5000,10 +5140,10 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
5000
5140
|
) AS next_rank
|
|
5001
5141
|
FROM doc_edges e2
|
|
5002
5142
|
JOIN documents next_doc ON next_doc.id = e2.src_doc_id AND next_doc.active = 1${nextDocScope}
|
|
5003
|
-
WHERE e2.dst_doc_id =
|
|
5143
|
+
WHERE e2.dst_doc_id = prev.doc_id
|
|
5004
5144
|
${edgeTypeFilterFor("e2")}
|
|
5005
|
-
AND instr(
|
|
5006
|
-
AND e2.src_doc_id NOT IN (${
|
|
5145
|
+
AND instr(prev.path, printf(',%d,', e2.src_doc_id)) = 0
|
|
5146
|
+
AND e2.src_doc_id NOT IN (${visitedIds})
|
|
5007
5147
|
)
|
|
5008
5148
|
WHERE next_rank = 1
|
|
5009
5149
|
ORDER BY edge_type ASC, next_doc_id ASC, edge_id ASC
|
|
@@ -5031,10 +5171,10 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
5031
5171
|
) AS next_rank
|
|
5032
5172
|
FROM doc_edges e2
|
|
5033
5173
|
JOIN documents next_doc ON next_doc.id = e2.dst_doc_id AND next_doc.active = 1${nextDocScope}
|
|
5034
|
-
WHERE e2.src_doc_id =
|
|
5174
|
+
WHERE e2.src_doc_id = prev.doc_id
|
|
5035
5175
|
${edgeTypeFilterFor("e2")}
|
|
5036
|
-
AND instr(
|
|
5037
|
-
AND e2.dst_doc_id NOT IN (${
|
|
5176
|
+
AND instr(prev.path, printf(',%d,', e2.dst_doc_id)) = 0
|
|
5177
|
+
AND e2.dst_doc_id NOT IN (${visitedIds})
|
|
5038
5178
|
)
|
|
5039
5179
|
WHERE next_rank = 1
|
|
5040
5180
|
ORDER BY edge_type ASC, next_doc_id ASC, edge_id ASC
|
|
@@ -5059,10 +5199,10 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
5059
5199
|
) AS next_rank
|
|
5060
5200
|
FROM doc_edges e3
|
|
5061
5201
|
JOIN documents next_doc ON next_doc.id = e3.src_doc_id AND next_doc.active = 1${nextDocScope}
|
|
5062
|
-
WHERE e3.dst_doc_id =
|
|
5202
|
+
WHERE e3.dst_doc_id = prev.doc_id
|
|
5063
5203
|
${edgeTypeFilterFor("e3")}
|
|
5064
|
-
AND instr(
|
|
5065
|
-
AND e3.src_doc_id NOT IN (${
|
|
5204
|
+
AND instr(prev.path, printf(',%d,', e3.src_doc_id)) = 0
|
|
5205
|
+
AND e3.src_doc_id NOT IN (${visitedIds})
|
|
5066
5206
|
)
|
|
5067
5207
|
WHERE next_rank = 1
|
|
5068
5208
|
ORDER BY edge_type ASC, next_doc_id ASC, edge_id ASC
|
|
@@ -5071,40 +5211,62 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
5071
5211
|
)
|
|
5072
5212
|
)`;
|
|
5073
5213
|
};
|
|
5074
|
-
const
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5214
|
+
const nextExpr =
|
|
5215
|
+
"CASE WHEN e.src_doc_id = prev.doc_id THEN e.dst_doc_id ELSE e.src_doc_id END";
|
|
5216
|
+
const levelParams: (number | string)[] = [];
|
|
5217
|
+
if (direction === "both") {
|
|
5218
|
+
if (edgeType) {
|
|
5219
|
+
levelParams.push(edgeType);
|
|
5220
|
+
}
|
|
5221
|
+
levelParams.push(candidateEdgeLimit);
|
|
5222
|
+
}
|
|
5223
|
+
if (edgeType) {
|
|
5224
|
+
levelParams.push(edgeType);
|
|
5225
|
+
}
|
|
5226
|
+
levelParams.push(candidateEdgeLimit);
|
|
5227
|
+
if (edgeType) {
|
|
5228
|
+
levelParams.push(edgeType);
|
|
5229
|
+
}
|
|
5230
|
+
levelParams.push(
|
|
5231
|
+
frontierLimit,
|
|
5232
|
+
frontierLimit,
|
|
5233
|
+
visitedLimit,
|
|
5234
|
+
frontierLimit
|
|
5235
|
+
);
|
|
5236
|
+
|
|
5237
|
+
const levelQuery = (depth: number) =>
|
|
5238
|
+
db.query<
|
|
5239
|
+
{
|
|
5240
|
+
candidate_count: number;
|
|
5241
|
+
has_candidates: number;
|
|
5242
|
+
expansion_overflow: number;
|
|
5243
|
+
doc_id: number | null;
|
|
5244
|
+
depth: number | null;
|
|
5245
|
+
path: string | null;
|
|
5246
|
+
sort_key: string | null;
|
|
5247
|
+
},
|
|
5248
|
+
(number | string)[]
|
|
5249
|
+
>(`
|
|
5250
|
+
WITH
|
|
5251
|
+
prev(doc_id, path) AS (
|
|
5252
|
+
SELECT json_extract(value, '$[0]'), json_extract(value, '$[1]')
|
|
5253
|
+
FROM json_each(?)
|
|
5254
|
+
),
|
|
5255
|
+
visited(doc_id) AS (
|
|
5256
|
+
SELECT value FROM json_each(?)
|
|
5257
|
+
),
|
|
5258
|
+
raw AS (
|
|
5089
5259
|
SELECT
|
|
5090
|
-
|
|
5260
|
+
prev.doc_id AS from_doc_id,
|
|
5091
5261
|
${nextExpr} AS doc_id,
|
|
5092
5262
|
${depth} AS depth,
|
|
5093
|
-
|
|
5263
|
+
prev.path || ${nextExpr} || ',' AS path,
|
|
5094
5264
|
printf('%06d|%s|%012d', ${depth}, e.edge_type, ${nextExpr}) AS sort_key,
|
|
5095
5265
|
e.src_doc_id,
|
|
5096
|
-
src.docid AS source_docid,
|
|
5097
|
-
src.uri AS source_uri,
|
|
5098
|
-
src.title AS source_title,
|
|
5099
5266
|
e.dst_doc_id,
|
|
5100
|
-
dst.docid AS target_docid,
|
|
5101
|
-
dst.uri AS target_uri,
|
|
5102
|
-
dst.title AS target_title,
|
|
5103
5267
|
e.edge_type,
|
|
5104
|
-
e.confidence,
|
|
5105
|
-
e.source,
|
|
5106
5268
|
row_number() OVER (
|
|
5107
|
-
PARTITION BY
|
|
5269
|
+
PARTITION BY prev.doc_id, e.src_doc_id, e.dst_doc_id, e.edge_type
|
|
5108
5270
|
ORDER BY
|
|
5109
5271
|
CASE e.confidence
|
|
5110
5272
|
WHEN 'manual' THEN 1
|
|
@@ -5117,47 +5279,47 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
5117
5279
|
src.docid ASC,
|
|
5118
5280
|
dst.docid ASC
|
|
5119
5281
|
) AS dedup_rank
|
|
5120
|
-
FROM
|
|
5121
|
-
JOIN doc_edges e ON ${frontierJoinClause
|
|
5122
|
-
AND ${boundedEdgePredicate(
|
|
5282
|
+
FROM prev
|
|
5283
|
+
JOIN doc_edges e ON ${frontierJoinClause}
|
|
5284
|
+
AND ${boundedEdgePredicate()}
|
|
5123
5285
|
JOIN documents src ON src.id = e.src_doc_id AND src.active = 1
|
|
5124
5286
|
JOIN documents dst ON dst.id = e.dst_doc_id AND dst.active = 1
|
|
5125
5287
|
WHERE 1 = 1
|
|
5126
5288
|
${edgeTypeFilter}
|
|
5127
|
-
AND instr(
|
|
5128
|
-
AND ${nextExpr} NOT IN (${
|
|
5289
|
+
AND instr(prev.path, printf(',%d,', ${nextExpr})) = 0
|
|
5290
|
+
AND ${nextExpr} NOT IN (${visitedIds})
|
|
5129
5291
|
),
|
|
5130
|
-
|
|
5292
|
+
deduped AS (
|
|
5131
5293
|
SELECT *
|
|
5132
|
-
FROM
|
|
5294
|
+
FROM raw
|
|
5133
5295
|
WHERE dedup_rank = 1
|
|
5134
5296
|
),
|
|
5135
|
-
|
|
5297
|
+
node_deduped AS (
|
|
5136
5298
|
SELECT
|
|
5137
5299
|
from_doc_id,
|
|
5138
5300
|
doc_id,
|
|
5139
5301
|
depth,
|
|
5140
5302
|
min(path) AS path,
|
|
5141
5303
|
min(sort_key) AS sort_key
|
|
5142
|
-
FROM
|
|
5304
|
+
FROM deduped
|
|
5143
5305
|
GROUP BY from_doc_id, doc_id, depth
|
|
5144
5306
|
),
|
|
5145
|
-
|
|
5307
|
+
ranked AS (
|
|
5146
5308
|
SELECT
|
|
5147
5309
|
*,
|
|
5148
5310
|
row_number() OVER (
|
|
5149
5311
|
PARTITION BY from_doc_id
|
|
5150
5312
|
ORDER BY sort_key ASC, doc_id ASC
|
|
5151
5313
|
) AS expansion_rank
|
|
5152
|
-
FROM
|
|
5314
|
+
FROM node_deduped
|
|
5153
5315
|
),
|
|
5154
|
-
|
|
5316
|
+
candidates AS (
|
|
5155
5317
|
SELECT doc_id, depth, min(path) AS path, min(sort_key) AS sort_key
|
|
5156
|
-
FROM
|
|
5318
|
+
FROM ranked
|
|
5157
5319
|
WHERE expansion_rank <= ?
|
|
5158
5320
|
GROUP BY doc_id, depth
|
|
5159
5321
|
),
|
|
5160
|
-
|
|
5322
|
+
frontier AS (
|
|
5161
5323
|
SELECT doc_id, depth, path, sort_key
|
|
5162
5324
|
FROM (
|
|
5163
5325
|
SELECT
|
|
@@ -5166,35 +5328,88 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
5166
5328
|
path,
|
|
5167
5329
|
sort_key,
|
|
5168
5330
|
row_number() OVER (ORDER BY sort_key ASC, doc_id ASC) AS depth_rank
|
|
5169
|
-
FROM
|
|
5331
|
+
FROM candidates
|
|
5170
5332
|
)
|
|
5171
5333
|
WHERE depth_rank <= ?
|
|
5172
|
-
AND depth_rank <= ? - (
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5334
|
+
AND depth_rank <= ? - (SELECT count(*) FROM visited)
|
|
5335
|
+
)
|
|
5336
|
+
SELECT
|
|
5337
|
+
stats.candidate_count,
|
|
5338
|
+
stats.has_candidates,
|
|
5339
|
+
stats.expansion_overflow,
|
|
5340
|
+
frontier.doc_id,
|
|
5341
|
+
frontier.depth,
|
|
5342
|
+
frontier.path,
|
|
5343
|
+
frontier.sort_key
|
|
5344
|
+
FROM (
|
|
5345
|
+
SELECT
|
|
5346
|
+
(SELECT count(*) FROM candidates) AS candidate_count,
|
|
5347
|
+
EXISTS (SELECT 1 FROM ranked) AS has_candidates,
|
|
5348
|
+
EXISTS (SELECT 1 FROM ranked WHERE expansion_rank > ?) AS expansion_overflow
|
|
5349
|
+
) stats
|
|
5350
|
+
LEFT JOIN frontier ON 1
|
|
5351
|
+
ORDER BY frontier.sort_key ASC, frontier.doc_id ASC`);
|
|
5352
|
+
|
|
5353
|
+
// Walk level by level; the extra level past maxDepth only reports
|
|
5354
|
+
// whether the depth cap cut anything off.
|
|
5355
|
+
type FrontierRow = {
|
|
5356
|
+
doc_id: number;
|
|
5357
|
+
depth: number;
|
|
5358
|
+
path: string;
|
|
5359
|
+
sort_key: string;
|
|
5360
|
+
};
|
|
5361
|
+
let previous: FrontierRow[] = [
|
|
5362
|
+
{
|
|
5363
|
+
doc_id: rootDocumentId,
|
|
5364
|
+
depth: 0,
|
|
5365
|
+
path: `,${rootDocumentId},`,
|
|
5366
|
+
sort_key: "",
|
|
5367
|
+
},
|
|
5368
|
+
];
|
|
5369
|
+
const walked: FrontierRow[] = [...previous];
|
|
5370
|
+
const visited = new Set([rootDocumentId]);
|
|
5371
|
+
let visitedOverflow = false;
|
|
5372
|
+
let frontierOverflow = false;
|
|
5373
|
+
let depthOverflow = false;
|
|
5374
|
+
for (let depth = 1; depth <= maxDepth + 1; depth += 1) {
|
|
5375
|
+
const rows = levelQuery(depth).all(
|
|
5376
|
+
JSON.stringify(previous.map((row) => [row.doc_id, row.path])),
|
|
5377
|
+
JSON.stringify([...visited]),
|
|
5378
|
+
...levelParams
|
|
5379
|
+
);
|
|
5380
|
+
const stats = rows[0];
|
|
5381
|
+
if (depth > maxDepth) {
|
|
5382
|
+
depthOverflow = Boolean(stats?.has_candidates);
|
|
5383
|
+
break;
|
|
5191
5384
|
}
|
|
5192
|
-
|
|
5193
|
-
|
|
5385
|
+
const candidateCount = stats?.candidate_count ?? 0;
|
|
5386
|
+
if (
|
|
5387
|
+
candidateCount > 0 &&
|
|
5388
|
+
candidateCount > visitedLimit - visited.size
|
|
5389
|
+
) {
|
|
5390
|
+
visitedOverflow = true;
|
|
5194
5391
|
}
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5392
|
+
if (
|
|
5393
|
+
(candidateCount > 0 && candidateCount > frontierLimit) ||
|
|
5394
|
+
stats?.expansion_overflow
|
|
5395
|
+
) {
|
|
5396
|
+
frontierOverflow = true;
|
|
5397
|
+
}
|
|
5398
|
+
previous = rows.flatMap((row) =>
|
|
5399
|
+
row.doc_id === null
|
|
5400
|
+
? []
|
|
5401
|
+
: [
|
|
5402
|
+
{
|
|
5403
|
+
doc_id: row.doc_id,
|
|
5404
|
+
depth: row.depth ?? depth,
|
|
5405
|
+
path: row.path ?? "",
|
|
5406
|
+
sort_key: row.sort_key ?? "",
|
|
5407
|
+
},
|
|
5408
|
+
]
|
|
5409
|
+
);
|
|
5410
|
+
for (const row of previous) {
|
|
5411
|
+
visited.add(row.doc_id);
|
|
5412
|
+
walked.push(row);
|
|
5198
5413
|
}
|
|
5199
5414
|
}
|
|
5200
5415
|
|
|
@@ -5204,62 +5419,18 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
5204
5419
|
: direction === "in"
|
|
5205
5420
|
? "AND ns.depth > nt.depth"
|
|
5206
5421
|
: "";
|
|
5207
|
-
const
|
|
5208
|
-
.map((
|
|
5209
|
-
|
|
5210
|
-
const
|
|
5211
|
-
{ length: maxDepth },
|
|
5212
|
-
(_, index) => {
|
|
5213
|
-
const previousFrontiers = Array.from(
|
|
5214
|
-
{ length: index + 1 },
|
|
5215
|
-
(__, frontierIndex) => `SELECT doc_id FROM f${frontierIndex}`
|
|
5216
|
-
).join(" UNION ");
|
|
5217
|
-
return `
|
|
5218
|
-
SELECT 1 AS has_more
|
|
5219
|
-
FROM c${index + 1}
|
|
5220
|
-
GROUP BY 1
|
|
5221
|
-
HAVING count(*) > ? - (
|
|
5222
|
-
SELECT count(*)
|
|
5223
|
-
FROM (${previousFrontiers})
|
|
5224
|
-
)`;
|
|
5225
|
-
}
|
|
5226
|
-
).join(" UNION ALL ");
|
|
5227
|
-
const frontierOverflowChecks = Array.from(
|
|
5228
|
-
{ length: maxDepth },
|
|
5229
|
-
(_, index) => `
|
|
5230
|
-
SELECT 1 AS has_more
|
|
5231
|
-
FROM c${index + 1}
|
|
5232
|
-
GROUP BY 1
|
|
5233
|
-
HAVING count(*) > ?
|
|
5234
|
-
UNION ALL
|
|
5235
|
-
SELECT 1 AS has_more
|
|
5236
|
-
FROM c${index + 1}_ranked
|
|
5237
|
-
WHERE expansion_rank > ?`
|
|
5238
|
-
).join(" UNION ALL ");
|
|
5239
|
-
const walkCte = `
|
|
5240
|
-
WITH RECURSIVE
|
|
5241
|
-
f0(doc_id, depth, path, sort_key) AS (
|
|
5242
|
-
SELECT ?, 0, printf(',%d,', ?), ''
|
|
5243
|
-
),
|
|
5244
|
-
${frontierCtes.join(",")},
|
|
5422
|
+
const walkedJson = JSON.stringify(
|
|
5423
|
+
walked.map((row) => [row.doc_id, row.depth, row.sort_key])
|
|
5424
|
+
);
|
|
5425
|
+
const nodeDepthCte = `
|
|
5245
5426
|
node_depth AS (
|
|
5246
|
-
SELECT doc_id, min(depth) AS depth, min(sort_key) AS sort_key
|
|
5247
|
-
FROM (${frontierUnion})
|
|
5248
|
-
GROUP BY doc_id
|
|
5249
|
-
),
|
|
5250
|
-
ranked_nodes AS (
|
|
5251
5427
|
SELECT
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
)
|
|
5259
|
-
`;
|
|
5260
|
-
|
|
5261
|
-
const baseParams: (number | string)[] = [];
|
|
5262
|
-
baseParams.push(rootDocumentId, rootDocumentId, ...frontierParams);
|
|
5428
|
+
json_extract(value, '$[0]') AS doc_id,
|
|
5429
|
+
min(json_extract(value, '$[1]')) AS depth,
|
|
5430
|
+
min(json_extract(value, '$[2]')) AS sort_key
|
|
5431
|
+
FROM json_each(?)
|
|
5432
|
+
GROUP BY 1
|
|
5433
|
+
)`;
|
|
5263
5434
|
|
|
5264
5435
|
const nodeRows = db
|
|
5265
5436
|
.query<
|
|
@@ -5270,14 +5441,23 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
5270
5441
|
},
|
|
5271
5442
|
(number | string)[]
|
|
5272
5443
|
>(
|
|
5273
|
-
|
|
5444
|
+
`WITH ${nodeDepthCte},
|
|
5445
|
+
ranked_nodes AS (
|
|
5446
|
+
SELECT
|
|
5447
|
+
d.*,
|
|
5448
|
+
nd.depth,
|
|
5449
|
+
row_number() OVER (ORDER BY nd.depth ASC, nd.sort_key ASC, d.id ASC) AS global_rank,
|
|
5450
|
+
count(*) OVER () AS total_count
|
|
5451
|
+
FROM node_depth nd
|
|
5452
|
+
JOIN documents d ON d.id = nd.doc_id AND d.active = 1
|
|
5453
|
+
)
|
|
5274
5454
|
SELECT *
|
|
5275
5455
|
FROM ranked_nodes
|
|
5276
5456
|
WHERE global_rank <= ?
|
|
5277
5457
|
ORDER BY depth ASC, uri ASC, docid ASC
|
|
5278
5458
|
LIMIT ?`
|
|
5279
5459
|
)
|
|
5280
|
-
.all(
|
|
5460
|
+
.all(walkedJson, nodeLimit + 1, nodeLimit + 1);
|
|
5281
5461
|
|
|
5282
5462
|
const returnedNodeRows = nodeRows.slice(0, nodeLimit);
|
|
5283
5463
|
const returnedIds = new Set(returnedNodeRows.map((row) => row.id));
|
|
@@ -5288,41 +5468,19 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
5288
5468
|
truncated = true;
|
|
5289
5469
|
warnings.push("maxNodes reached");
|
|
5290
5470
|
}
|
|
5291
|
-
if (totalNodes > visitedLimit) {
|
|
5471
|
+
if (totalNodes > visitedLimit || visitedOverflow) {
|
|
5292
5472
|
truncated = true;
|
|
5293
5473
|
warnings.push("visitedLimit reached");
|
|
5294
5474
|
}
|
|
5295
|
-
|
|
5296
|
-
const visitedRows = db
|
|
5297
|
-
.query<{ has_more: number }, (number | string)[]>(
|
|
5298
|
-
`${walkCte}
|
|
5299
|
-
SELECT 1 AS has_more
|
|
5300
|
-
FROM (${visitedOverflowChecks})
|
|
5301
|
-
LIMIT 1`
|
|
5302
|
-
)
|
|
5303
|
-
.get(...baseParams, ...Array(maxDepth).fill(visitedLimit));
|
|
5304
|
-
if (visitedRows) {
|
|
5305
|
-
truncated = true;
|
|
5306
|
-
warnings.push("visitedLimit reached");
|
|
5307
|
-
}
|
|
5308
|
-
|
|
5309
|
-
const frontierRows = db
|
|
5310
|
-
.query<{ has_more: number }, (number | string)[]>(
|
|
5311
|
-
`${walkCte}
|
|
5312
|
-
SELECT 1 AS has_more
|
|
5313
|
-
FROM (${frontierOverflowChecks})
|
|
5314
|
-
LIMIT 1`
|
|
5315
|
-
)
|
|
5316
|
-
.get(...baseParams, ...Array(maxDepth * 2).fill(frontierLimit));
|
|
5317
|
-
if (frontierRows) {
|
|
5475
|
+
if (frontierOverflow) {
|
|
5318
5476
|
truncated = true;
|
|
5319
5477
|
warnings.push("frontierLimit reached");
|
|
5320
5478
|
}
|
|
5321
5479
|
|
|
5322
5480
|
const edgeRows = db
|
|
5323
5481
|
.query<DbDocEdgeRow & { traversal_depth: number }, (number | string)[]>(
|
|
5324
|
-
|
|
5325
|
-
|
|
5482
|
+
`WITH ${nodeDepthCte},
|
|
5483
|
+
returned_edges AS (
|
|
5326
5484
|
SELECT
|
|
5327
5485
|
e.src_doc_id,
|
|
5328
5486
|
src.docid AS source_docid,
|
|
@@ -5369,22 +5527,13 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
5369
5527
|
ORDER BY traversal_depth ASC, edge_type ASC, source_docid ASC, target_docid ASC`
|
|
5370
5528
|
)
|
|
5371
5529
|
.all(
|
|
5372
|
-
|
|
5530
|
+
walkedJson,
|
|
5373
5531
|
...returnedIds,
|
|
5374
5532
|
...returnedIds,
|
|
5375
5533
|
...(edgeType ? [edgeType] : [])
|
|
5376
5534
|
);
|
|
5377
5535
|
|
|
5378
|
-
|
|
5379
|
-
.query<{ has_more: number }, (number | string)[]>(
|
|
5380
|
-
`${walkCte}
|
|
5381
|
-
SELECT 1 AS has_more
|
|
5382
|
-
FROM c${boundaryCandidateDepth}_ranked
|
|
5383
|
-
LIMIT 1`
|
|
5384
|
-
)
|
|
5385
|
-
.get(...baseParams);
|
|
5386
|
-
|
|
5387
|
-
if (boundaryRows) {
|
|
5536
|
+
if (depthOverflow) {
|
|
5388
5537
|
truncated = true;
|
|
5389
5538
|
warnings.push("maxDepth reached");
|
|
5390
5539
|
}
|