@gmickel/gno 1.30.5 → 1.30.7
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 +1 -1
- package/assets/skill/cli-reference.md +10 -10
- package/assets/skill/mcp-reference.md +3 -2
- package/browser-extension/artifacts/{gno-browser-clipper-v1.30.5.zip → gno-browser-clipper-v1.30.7.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.30.7.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +10 -7
- package/spec/mcp.md +4 -3
- package/src/app/context-runtime-input.ts +1 -1
- package/src/app/verified-ask.ts +1 -1
- package/src/cli/program.ts +14 -11
- package/src/core/graph-edge-confidence.ts +90 -0
- package/src/mcp/tools/index.ts +6 -7
- package/src/mcp/tools/query.ts +2 -2
- package/src/pipeline/graph-retrieval.ts +72 -16
- package/src/pipeline/hybrid.ts +1 -1
- package/src/pipeline/types.ts +1 -1
- package/src/serve/routes/api.ts +2 -2
- package/src/store/sqlite/adapter.ts +30 -83
- package/src/store/sqlite/graph-link-resolver.ts +241 -0
- package/src/store/sqlite/graph-neighbors.ts +368 -0
- package/src/store/types.ts +33 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.30.5.zip.sha256 +0 -1
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed-scoped one-hop graph neighbor resolution for query-time expansion.
|
|
3
|
+
* Avoids collection-wide getGraph correlated link resolution.
|
|
4
|
+
*
|
|
5
|
+
* @module src/store/sqlite/graph-neighbors
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Database } from "bun:sqlite";
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
GetGraphNeighborsOptions,
|
|
12
|
+
GraphEdgeAudit,
|
|
13
|
+
GraphEdgeConfidence,
|
|
14
|
+
GraphLink,
|
|
15
|
+
GraphLinkType,
|
|
16
|
+
GraphNeighborsResult,
|
|
17
|
+
} from "../types";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
classifyResolvedGraphEdge,
|
|
21
|
+
mergeGraphEdgeAudit,
|
|
22
|
+
} from "../../core/graph-edge-confidence";
|
|
23
|
+
import { normalizeWikiName, stripWikiMdExt } from "../../core/links";
|
|
24
|
+
import { resolveGraphLinkTargets } from "./graph-link-resolver";
|
|
25
|
+
|
|
26
|
+
const MAX_SEED_DOCUMENTS = 5;
|
|
27
|
+
const DEFAULT_EDGE_LIMIT = 10_000;
|
|
28
|
+
|
|
29
|
+
interface SeedDocRow {
|
|
30
|
+
id: number;
|
|
31
|
+
docid: string;
|
|
32
|
+
title: string | null;
|
|
33
|
+
rel_path: string;
|
|
34
|
+
collection: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface ResolvedEdgeRow {
|
|
38
|
+
source_docid: string;
|
|
39
|
+
target_docid: string;
|
|
40
|
+
link_type: "wiki" | "markdown";
|
|
41
|
+
match_rank: number | null;
|
|
42
|
+
match_count: number | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface RawLinkRow {
|
|
46
|
+
id: number;
|
|
47
|
+
source_docid: string;
|
|
48
|
+
source_collection: string;
|
|
49
|
+
target_ref_norm: string;
|
|
50
|
+
target_collection: string | null;
|
|
51
|
+
link_type: "wiki" | "markdown";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const addWikiKeyVariants = (keySet: Set<string>, value: string): void => {
|
|
55
|
+
if (!value) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const base = stripWikiMdExt(value);
|
|
59
|
+
const md = `${base}.md`;
|
|
60
|
+
keySet.add(value);
|
|
61
|
+
keySet.add(base);
|
|
62
|
+
keySet.add(md);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const wikiKeysForSeed = (seed: SeedDocRow): Set<string> => {
|
|
66
|
+
const keySet = new Set<string>();
|
|
67
|
+
addWikiKeyVariants(keySet, normalizeWikiName(seed.title ?? ""));
|
|
68
|
+
const relPathKey = normalizeWikiName(seed.rel_path);
|
|
69
|
+
addWikiKeyVariants(keySet, relPathKey);
|
|
70
|
+
const basename = relPathKey.split("/").pop() ?? relPathKey;
|
|
71
|
+
if (basename !== relPathKey) {
|
|
72
|
+
addWikiKeyVariants(keySet, basename);
|
|
73
|
+
}
|
|
74
|
+
return keySet;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const matchesWikiKey = (targetRefNorm: string, keys: Set<string>): boolean => {
|
|
78
|
+
for (const key of keys) {
|
|
79
|
+
if (targetRefNorm === key || targetRefNorm.endsWith(`/${key}`)) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const loadSeeds = (db: Database, seedDocumentIds: number[]): SeedDocRow[] => {
|
|
87
|
+
const uniqueIds = [...new Set(seedDocumentIds)]
|
|
88
|
+
.filter((id) => Number.isInteger(id) && id > 0)
|
|
89
|
+
.slice(0, MAX_SEED_DOCUMENTS);
|
|
90
|
+
if (uniqueIds.length === 0) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
const placeholders = uniqueIds.map(() => "?").join(",");
|
|
94
|
+
return db
|
|
95
|
+
.query<SeedDocRow, number[]>(
|
|
96
|
+
`SELECT id, docid, title, rel_path, collection
|
|
97
|
+
FROM documents
|
|
98
|
+
WHERE active = 1 AND id IN (${placeholders})
|
|
99
|
+
ORDER BY id ASC`
|
|
100
|
+
)
|
|
101
|
+
.all(...uniqueIds);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const collectIncomingCandidateLinks = (
|
|
105
|
+
db: Database,
|
|
106
|
+
seeds: SeedDocRow[],
|
|
107
|
+
collection: string | undefined
|
|
108
|
+
): { links: RawLinkRow[]; examinedLinkRows: number } => {
|
|
109
|
+
const linksById = new Map<number, RawLinkRow>();
|
|
110
|
+
let examinedLinkRows = 0;
|
|
111
|
+
const seedIdSet = new Set(seeds.map((seed) => seed.id));
|
|
112
|
+
|
|
113
|
+
const wikiKeysByCollection = new Map<string, Set<string>>();
|
|
114
|
+
for (const seed of seeds) {
|
|
115
|
+
const keys = wikiKeysByCollection.get(seed.collection) ?? new Set<string>();
|
|
116
|
+
for (const key of wikiKeysForSeed(seed)) {
|
|
117
|
+
keys.add(key);
|
|
118
|
+
}
|
|
119
|
+
wikiKeysByCollection.set(seed.collection, keys);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const targetCollections = [...wikiKeysByCollection.keys()];
|
|
123
|
+
if (targetCollections.length > 0) {
|
|
124
|
+
const collectionPlaceholders = targetCollections.map(() => "?").join(",");
|
|
125
|
+
const wikiRows = db
|
|
126
|
+
.query<RawLinkRow & { source_doc_id: number }, string[]>(
|
|
127
|
+
`SELECT dl.id, dl.source_doc_id, src.docid AS source_docid,
|
|
128
|
+
src.collection AS source_collection, dl.target_ref_norm,
|
|
129
|
+
dl.target_collection, dl.link_type
|
|
130
|
+
FROM doc_links dl
|
|
131
|
+
JOIN documents src ON src.id = dl.source_doc_id AND src.active = 1
|
|
132
|
+
WHERE dl.link_type = 'wiki'
|
|
133
|
+
AND (
|
|
134
|
+
(dl.target_collection IS NULL
|
|
135
|
+
AND src.collection IN (${collectionPlaceholders}))
|
|
136
|
+
OR dl.target_collection IN (${collectionPlaceholders})
|
|
137
|
+
)
|
|
138
|
+
${collection ? "AND src.collection = ?" : ""}`
|
|
139
|
+
)
|
|
140
|
+
.all(
|
|
141
|
+
...targetCollections,
|
|
142
|
+
...targetCollections,
|
|
143
|
+
...(collection ? [collection] : [])
|
|
144
|
+
);
|
|
145
|
+
examinedLinkRows += wikiRows.length;
|
|
146
|
+
|
|
147
|
+
for (const row of wikiRows) {
|
|
148
|
+
const targetCollection = row.target_collection ?? row.source_collection;
|
|
149
|
+
const keys = wikiKeysByCollection.get(targetCollection);
|
|
150
|
+
if (
|
|
151
|
+
keys &&
|
|
152
|
+
!seedIdSet.has(row.source_doc_id) &&
|
|
153
|
+
matchesWikiKey(row.target_ref_norm, keys)
|
|
154
|
+
) {
|
|
155
|
+
linksById.set(row.id, row);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const seed of seeds) {
|
|
161
|
+
const mdRows = db
|
|
162
|
+
.query<RawLinkRow & { source_doc_id: number }, string[]>(
|
|
163
|
+
`SELECT dl.id, dl.source_doc_id, src.docid AS source_docid,
|
|
164
|
+
src.collection AS source_collection, dl.target_ref_norm,
|
|
165
|
+
dl.target_collection, dl.link_type
|
|
166
|
+
FROM doc_links dl
|
|
167
|
+
JOIN documents src ON src.id = dl.source_doc_id AND src.active = 1
|
|
168
|
+
WHERE dl.link_type = 'markdown'
|
|
169
|
+
AND dl.target_ref_norm = ?
|
|
170
|
+
AND (
|
|
171
|
+
(dl.target_collection IS NULL AND src.collection = ?)
|
|
172
|
+
OR dl.target_collection = ?
|
|
173
|
+
)
|
|
174
|
+
${collection ? "AND src.collection = ?" : ""}`
|
|
175
|
+
)
|
|
176
|
+
.all(
|
|
177
|
+
seed.rel_path,
|
|
178
|
+
seed.collection,
|
|
179
|
+
seed.collection,
|
|
180
|
+
...(collection ? [collection] : [])
|
|
181
|
+
);
|
|
182
|
+
examinedLinkRows += mdRows.length;
|
|
183
|
+
for (const row of mdRows) {
|
|
184
|
+
if (!seedIdSet.has(row.source_doc_id)) {
|
|
185
|
+
linksById.set(row.id, row);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { links: [...linksById.values()], examinedLinkRows };
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const loadRawLinksForSources = (
|
|
194
|
+
db: Database,
|
|
195
|
+
sourceIds: number[],
|
|
196
|
+
collection: string | undefined
|
|
197
|
+
): RawLinkRow[] => {
|
|
198
|
+
if (sourceIds.length === 0) {
|
|
199
|
+
return [];
|
|
200
|
+
}
|
|
201
|
+
const sourcePlaceholders = sourceIds.map(() => "?").join(",");
|
|
202
|
+
const params: (string | number)[] = [...sourceIds];
|
|
203
|
+
if (collection) {
|
|
204
|
+
params.push(collection);
|
|
205
|
+
}
|
|
206
|
+
return db
|
|
207
|
+
.query<RawLinkRow, (string | number)[]>(
|
|
208
|
+
`SELECT dl.id, src.docid AS source_docid,
|
|
209
|
+
src.collection AS source_collection,
|
|
210
|
+
dl.target_ref_norm, dl.target_collection, dl.link_type
|
|
211
|
+
FROM documents src
|
|
212
|
+
JOIN doc_links dl ON dl.source_doc_id = src.id
|
|
213
|
+
WHERE src.active = 1
|
|
214
|
+
AND src.id IN (${sourcePlaceholders})
|
|
215
|
+
${collection ? "AND src.collection = ?" : ""}
|
|
216
|
+
ORDER BY src.id ASC, dl.id ASC`
|
|
217
|
+
)
|
|
218
|
+
.all(...params);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const resolveRawEdges = (
|
|
222
|
+
db: Database,
|
|
223
|
+
rawRows: RawLinkRow[],
|
|
224
|
+
incomingLinkIds: Set<number>,
|
|
225
|
+
seedIds: Set<number>,
|
|
226
|
+
collection: string | undefined
|
|
227
|
+
): ResolvedEdgeRow[] => {
|
|
228
|
+
const inScopeRows = rawRows.filter(
|
|
229
|
+
(row) =>
|
|
230
|
+
!collection ||
|
|
231
|
+
(row.target_collection ?? row.source_collection) === collection
|
|
232
|
+
);
|
|
233
|
+
const resolvedTargets = resolveGraphLinkTargets(
|
|
234
|
+
db,
|
|
235
|
+
inScopeRows.map((row) => ({
|
|
236
|
+
targetRefNorm: row.target_ref_norm,
|
|
237
|
+
targetCollection: row.target_collection ?? row.source_collection,
|
|
238
|
+
linkType: row.link_type,
|
|
239
|
+
}))
|
|
240
|
+
);
|
|
241
|
+
const rows: ResolvedEdgeRow[] = [];
|
|
242
|
+
for (const [index, rawRow] of inScopeRows.entries()) {
|
|
243
|
+
const target = resolvedTargets[index];
|
|
244
|
+
if (
|
|
245
|
+
!target ||
|
|
246
|
+
(incomingLinkIds.has(rawRow.id) && !seedIds.has(target.targetId))
|
|
247
|
+
) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
rows.push({
|
|
251
|
+
source_docid: rawRow.source_docid,
|
|
252
|
+
target_docid: target.targetDocid,
|
|
253
|
+
link_type: rawRow.link_type,
|
|
254
|
+
match_rank: target.matchRank,
|
|
255
|
+
match_count: target.matchCount,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
return rows;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const toGraphLinks = (
|
|
262
|
+
rows: ResolvedEdgeRow[],
|
|
263
|
+
limitEdges: number
|
|
264
|
+
): GraphLink[] => {
|
|
265
|
+
const edgeMap = new Map<
|
|
266
|
+
string,
|
|
267
|
+
{
|
|
268
|
+
type: GraphLinkType;
|
|
269
|
+
weight: number;
|
|
270
|
+
confidence: GraphEdgeConfidence;
|
|
271
|
+
audit: GraphEdgeAudit;
|
|
272
|
+
}
|
|
273
|
+
>();
|
|
274
|
+
|
|
275
|
+
for (const row of rows) {
|
|
276
|
+
const key = `${row.source_docid}:${row.target_docid}:${row.link_type}`;
|
|
277
|
+
const { confidence, audit } = classifyResolvedGraphEdge(
|
|
278
|
+
row.link_type,
|
|
279
|
+
row.match_rank,
|
|
280
|
+
row.match_count
|
|
281
|
+
);
|
|
282
|
+
const existing = edgeMap.get(key);
|
|
283
|
+
if (existing) {
|
|
284
|
+
existing.weight += 1;
|
|
285
|
+
mergeGraphEdgeAudit(existing, confidence, audit);
|
|
286
|
+
} else {
|
|
287
|
+
edgeMap.set(key, {
|
|
288
|
+
type: row.link_type,
|
|
289
|
+
weight: 1,
|
|
290
|
+
confidence,
|
|
291
|
+
audit,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return [...edgeMap.entries()]
|
|
297
|
+
.map(([key, val]) => {
|
|
298
|
+
const parts = key.split(":");
|
|
299
|
+
return {
|
|
300
|
+
source: parts[0] ?? "",
|
|
301
|
+
target: parts[1] ?? "",
|
|
302
|
+
type: val.type,
|
|
303
|
+
weight: val.weight,
|
|
304
|
+
confidence: val.confidence,
|
|
305
|
+
audit: val.audit,
|
|
306
|
+
};
|
|
307
|
+
})
|
|
308
|
+
.sort(
|
|
309
|
+
(left, right) =>
|
|
310
|
+
left.source.localeCompare(right.source) ||
|
|
311
|
+
left.target.localeCompare(right.target) ||
|
|
312
|
+
left.type.localeCompare(right.type)
|
|
313
|
+
)
|
|
314
|
+
.slice(0, limitEdges);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Resolve one-hop explicit/inferred/ambiguous neighbors for a small seed set.
|
|
319
|
+
* Does not compute similarity edges (vector retrieval already supplies those).
|
|
320
|
+
*/
|
|
321
|
+
export function queryGraphNeighborsForSeeds(
|
|
322
|
+
db: Database,
|
|
323
|
+
options: GetGraphNeighborsOptions
|
|
324
|
+
): GraphNeighborsResult {
|
|
325
|
+
const limitEdges = Math.max(
|
|
326
|
+
1,
|
|
327
|
+
Math.min(50_000, options.limitEdges ?? DEFAULT_EDGE_LIMIT)
|
|
328
|
+
);
|
|
329
|
+
const seeds = loadSeeds(db, options.seedDocumentIds);
|
|
330
|
+
if (seeds.length === 0) {
|
|
331
|
+
return {
|
|
332
|
+
links: [],
|
|
333
|
+
meta: {
|
|
334
|
+
seedDocumentIds: [],
|
|
335
|
+
examinedLinkRows: 0,
|
|
336
|
+
returnedEdges: 0,
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const seedIds = seeds.map((seed) => seed.id);
|
|
342
|
+
const outgoingLinks = loadRawLinksForSources(db, seedIds, options.collection);
|
|
343
|
+
const { links: incomingLinks, examinedLinkRows: incomingCandidates } =
|
|
344
|
+
collectIncomingCandidateLinks(db, seeds, options.collection);
|
|
345
|
+
const incomingLinkIds = new Set(incomingLinks.map((link) => link.id));
|
|
346
|
+
const rawLinksById = new Map(
|
|
347
|
+
[...outgoingLinks, ...incomingLinks].map((link) => [link.id, link])
|
|
348
|
+
);
|
|
349
|
+
const resolvedRows = resolveRawEdges(
|
|
350
|
+
db,
|
|
351
|
+
[...rawLinksById.values()],
|
|
352
|
+
incomingLinkIds,
|
|
353
|
+
new Set(seedIds),
|
|
354
|
+
options.collection
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
const examinedLinkRows = outgoingLinks.length + incomingCandidates;
|
|
358
|
+
const links = toGraphLinks(resolvedRows, limitEdges);
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
links,
|
|
362
|
+
meta: {
|
|
363
|
+
seedDocumentIds: seedIds,
|
|
364
|
+
examinedLinkRows,
|
|
365
|
+
returnedEdges: links.length,
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
}
|
package/src/store/types.ts
CHANGED
|
@@ -889,6 +889,30 @@ export interface GetGraphOptions {
|
|
|
889
889
|
similarTopK?: number;
|
|
890
890
|
}
|
|
891
891
|
|
|
892
|
+
/** Options for seed-scoped one-hop graph neighbor lookup (query-time expansion). */
|
|
893
|
+
export interface GetGraphNeighborsOptions {
|
|
894
|
+
/** Seed document primary keys; implementations clamp to a small bound (≤5). */
|
|
895
|
+
seedDocumentIds: number[];
|
|
896
|
+
/** Filter neighbors to a single collection */
|
|
897
|
+
collection?: string;
|
|
898
|
+
/** Max edges to return (default 10000) */
|
|
899
|
+
limitEdges?: number;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/** Result of seed-scoped one-hop graph neighbor lookup. */
|
|
903
|
+
export interface GraphNeighborsResult {
|
|
904
|
+
/** One-hop wiki/markdown edges touching the seeds (no similarity edges). */
|
|
905
|
+
links: GraphLink[];
|
|
906
|
+
meta: {
|
|
907
|
+
/** Seeds that were actually resolved (active docs only). */
|
|
908
|
+
seedDocumentIds: number[];
|
|
909
|
+
/** Link rows examined during scoped resolution (for latency regressions). */
|
|
910
|
+
examinedLinkRows: number;
|
|
911
|
+
/** Edges returned after merge/cap. */
|
|
912
|
+
returnedEdges: number;
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
892
916
|
/** Direction for bounded typed-edge graph traversal. */
|
|
893
917
|
export type GraphQueryDirection = "out" | "in" | "both";
|
|
894
918
|
|
|
@@ -1973,6 +1997,15 @@ export interface StorePort {
|
|
|
1973
1997
|
*/
|
|
1974
1998
|
getGraph(options?: GetGraphOptions): Promise<StoreResult<GraphResult>>;
|
|
1975
1999
|
|
|
2000
|
+
/**
|
|
2001
|
+
* Seed-scoped one-hop graph neighbors for query-time expansion.
|
|
2002
|
+
* Resolves only outgoing/backlink edges for ≤5 seed document IDs.
|
|
2003
|
+
* Optional: mocks may omit this and fall back to getGraph.
|
|
2004
|
+
*/
|
|
2005
|
+
getGraphNeighborsForSeeds?(
|
|
2006
|
+
options: GetGraphNeighborsOptions
|
|
2007
|
+
): Promise<StoreResult<GraphNeighborsResult>>;
|
|
2008
|
+
|
|
1976
2009
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1977
2010
|
// Status
|
|
1978
2011
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
d6a1283a006b52517fdef4aaaa25098351f3ffa730273869e78f1d7906998bfd gno-browser-clipper-v1.30.5.zip
|