@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
|
@@ -55,10 +55,12 @@ import type {
|
|
|
55
55
|
EgressAuditStatusResult,
|
|
56
56
|
FtsResult,
|
|
57
57
|
FtsSearchOptions,
|
|
58
|
+
GetGraphNeighborsOptions,
|
|
58
59
|
GetGraphOptions,
|
|
59
60
|
GraphEdgeConfidence,
|
|
60
61
|
GraphEdgeAudit,
|
|
61
62
|
GraphLinkType,
|
|
63
|
+
GraphNeighborsResult,
|
|
62
64
|
GraphQueryOptions,
|
|
63
65
|
GraphQueryTraversalRows,
|
|
64
66
|
GraphReportNode,
|
|
@@ -111,6 +113,10 @@ import {
|
|
|
111
113
|
resolveConfiguredEgressPolicy,
|
|
112
114
|
} from "../../config/types";
|
|
113
115
|
import { analyzeGraphCommunities } from "../../core/graph-analysis";
|
|
116
|
+
import {
|
|
117
|
+
classifyResolvedGraphEdge,
|
|
118
|
+
mergeGraphEdgeAudit,
|
|
119
|
+
} from "../../core/graph-edge-confidence";
|
|
114
120
|
import {
|
|
115
121
|
buildWikiBestMatchSubquery,
|
|
116
122
|
buildWikiBestRankMatchCountSubquery,
|
|
@@ -155,6 +161,7 @@ import {
|
|
|
155
161
|
purgeEgressAuditReceipts as purgeStoredEgressAuditReceipts,
|
|
156
162
|
} from "./egress-audit-store";
|
|
157
163
|
import { loadFts5Snowball } from "./fts5-snowball";
|
|
164
|
+
import { queryGraphNeighborsForSeeds } from "./graph-neighbors";
|
|
158
165
|
import {
|
|
159
166
|
appendExportManifest as appendStoredTraceExportManifest,
|
|
160
167
|
getBoundedTrace as getBoundedStoredTrace,
|
|
@@ -4154,6 +4161,27 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
4154
4161
|
// Graph
|
|
4155
4162
|
// ─────────────────────────────────────────────────────────────────────────
|
|
4156
4163
|
|
|
4164
|
+
/**
|
|
4165
|
+
* Seed-scoped one-hop neighbors for query-time graph expansion.
|
|
4166
|
+
* Does not rebuild the full collection graph or similarity edges.
|
|
4167
|
+
*/
|
|
4168
|
+
async getGraphNeighborsForSeeds(
|
|
4169
|
+
options: GetGraphNeighborsOptions
|
|
4170
|
+
): Promise<StoreResult<GraphNeighborsResult>> {
|
|
4171
|
+
try {
|
|
4172
|
+
const db = this.ensureOpen();
|
|
4173
|
+
return ok(queryGraphNeighborsForSeeds(db, options));
|
|
4174
|
+
} catch (cause) {
|
|
4175
|
+
return err(
|
|
4176
|
+
"QUERY_FAILED",
|
|
4177
|
+
cause instanceof Error
|
|
4178
|
+
? cause.message
|
|
4179
|
+
: "Failed to get seed graph neighbors",
|
|
4180
|
+
cause
|
|
4181
|
+
);
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4157
4185
|
async getGraph(options?: GetGraphOptions): Promise<StoreResult<GraphResult>> {
|
|
4158
4186
|
try {
|
|
4159
4187
|
const db = this.ensureOpen();
|
|
@@ -4460,87 +4488,6 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
4460
4488
|
const selectedDocids = new Set(nodes.map((node) => node.id));
|
|
4461
4489
|
const nodeDocids = new Set(nodes.map((node) => node.id));
|
|
4462
4490
|
|
|
4463
|
-
const confidenceRank: Record<GraphEdgeConfidence, number> = {
|
|
4464
|
-
explicit: 1,
|
|
4465
|
-
inferred: 2,
|
|
4466
|
-
ambiguous: 3,
|
|
4467
|
-
similarity: 4,
|
|
4468
|
-
};
|
|
4469
|
-
const classifyResolvedEdge = (
|
|
4470
|
-
linkType: "wiki" | "markdown",
|
|
4471
|
-
matchRank: number | null,
|
|
4472
|
-
matchCount: number | null
|
|
4473
|
-
): { confidence: GraphEdgeConfidence; audit: GraphEdgeAudit } => {
|
|
4474
|
-
if (linkType === "markdown") {
|
|
4475
|
-
return {
|
|
4476
|
-
confidence: "explicit",
|
|
4477
|
-
audit: { resolution: "exact-path", matchCount: 1 },
|
|
4478
|
-
};
|
|
4479
|
-
}
|
|
4480
|
-
|
|
4481
|
-
const count = matchCount ?? 0;
|
|
4482
|
-
if (count > 1) {
|
|
4483
|
-
return {
|
|
4484
|
-
confidence: "ambiguous",
|
|
4485
|
-
audit: {
|
|
4486
|
-
resolution: "ambiguous-fallback",
|
|
4487
|
-
matchCount: count,
|
|
4488
|
-
},
|
|
4489
|
-
};
|
|
4490
|
-
}
|
|
4491
|
-
|
|
4492
|
-
if (matchRank === 1 || matchRank === 2) {
|
|
4493
|
-
return {
|
|
4494
|
-
confidence: "explicit",
|
|
4495
|
-
audit: { resolution: "exact-title", matchCount: count || 1 },
|
|
4496
|
-
};
|
|
4497
|
-
}
|
|
4498
|
-
if (matchRank === 5 || matchRank === 6) {
|
|
4499
|
-
return {
|
|
4500
|
-
confidence: "explicit",
|
|
4501
|
-
audit: { resolution: "exact-path", matchCount: count || 1 },
|
|
4502
|
-
};
|
|
4503
|
-
}
|
|
4504
|
-
|
|
4505
|
-
return {
|
|
4506
|
-
confidence: "inferred",
|
|
4507
|
-
audit: { resolution: "path-fallback", matchCount: count || 1 },
|
|
4508
|
-
};
|
|
4509
|
-
};
|
|
4510
|
-
const mergeAudit = (
|
|
4511
|
-
current: {
|
|
4512
|
-
type: GraphLinkType;
|
|
4513
|
-
weight: number;
|
|
4514
|
-
confidence: GraphEdgeConfidence;
|
|
4515
|
-
audit: GraphEdgeAudit;
|
|
4516
|
-
},
|
|
4517
|
-
nextConfidence: GraphEdgeConfidence,
|
|
4518
|
-
nextAudit: GraphEdgeAudit
|
|
4519
|
-
): void => {
|
|
4520
|
-
if (
|
|
4521
|
-
confidenceRank[nextConfidence] < confidenceRank[current.confidence]
|
|
4522
|
-
) {
|
|
4523
|
-
current.confidence = nextConfidence;
|
|
4524
|
-
current.audit = {
|
|
4525
|
-
...nextAudit,
|
|
4526
|
-
matchCount: Math.max(
|
|
4527
|
-
current.audit.matchCount ?? 0,
|
|
4528
|
-
nextAudit.matchCount ?? 0
|
|
4529
|
-
),
|
|
4530
|
-
};
|
|
4531
|
-
return;
|
|
4532
|
-
}
|
|
4533
|
-
if (
|
|
4534
|
-
nextAudit.matchCount !== undefined &&
|
|
4535
|
-
(current.audit.matchCount ?? 0) < nextAudit.matchCount
|
|
4536
|
-
) {
|
|
4537
|
-
current.audit = {
|
|
4538
|
-
...current.audit,
|
|
4539
|
-
matchCount: nextAudit.matchCount,
|
|
4540
|
-
};
|
|
4541
|
-
}
|
|
4542
|
-
};
|
|
4543
|
-
|
|
4544
4491
|
const edgeMap = new Map<
|
|
4545
4492
|
string,
|
|
4546
4493
|
{
|
|
@@ -4558,7 +4505,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
4558
4505
|
continue;
|
|
4559
4506
|
}
|
|
4560
4507
|
const key = `${row.source_docid}:${row.target_docid}:${row.link_type}`;
|
|
4561
|
-
const { confidence, audit } =
|
|
4508
|
+
const { confidence, audit } = classifyResolvedGraphEdge(
|
|
4562
4509
|
row.link_type,
|
|
4563
4510
|
row.match_rank,
|
|
4564
4511
|
row.match_count
|
|
@@ -4566,7 +4513,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
4566
4513
|
const existing = edgeMap.get(key);
|
|
4567
4514
|
if (existing) {
|
|
4568
4515
|
existing.weight += 1;
|
|
4569
|
-
|
|
4516
|
+
mergeGraphEdgeAudit(existing, confidence, audit);
|
|
4570
4517
|
} else {
|
|
4571
4518
|
edgeMap.set(key, {
|
|
4572
4519
|
type: row.link_type,
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batched wiki/markdown target resolution for seed-scoped graph expansion.
|
|
3
|
+
*
|
|
4
|
+
* @module src/store/sqlite/graph-link-resolver
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Database } from "bun:sqlite";
|
|
8
|
+
|
|
9
|
+
import { stripWikiMdExt } from "../../core/links";
|
|
10
|
+
|
|
11
|
+
export interface GraphLinkTarget {
|
|
12
|
+
targetRefNorm: string;
|
|
13
|
+
targetCollection: string;
|
|
14
|
+
linkType: "wiki" | "markdown";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ResolvedGraphLinkTarget {
|
|
18
|
+
targetId: number;
|
|
19
|
+
targetDocid: string;
|
|
20
|
+
matchRank: number;
|
|
21
|
+
matchCount: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const MAX_SQL_PARAMS = 900;
|
|
25
|
+
|
|
26
|
+
const chunkArray = <T>(items: T[], chunkSize: number): T[][] => {
|
|
27
|
+
const chunks: T[][] = [];
|
|
28
|
+
for (let offset = 0; offset < items.length; offset += chunkSize) {
|
|
29
|
+
chunks.push(items.slice(offset, offset + chunkSize));
|
|
30
|
+
}
|
|
31
|
+
return chunks;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const suffixMatch = (targetExpr: string, valueExpr: string): string =>
|
|
35
|
+
`(substr(${targetExpr}, -length(${valueExpr})) = ${valueExpr}
|
|
36
|
+
AND (length(${targetExpr}) = length(${valueExpr})
|
|
37
|
+
OR substr(${targetExpr}, -length(${valueExpr}) - 1, 1) = '/'))`;
|
|
38
|
+
|
|
39
|
+
const resolveUniqueGraphLinkTargets = (
|
|
40
|
+
db: Database,
|
|
41
|
+
targets: GraphLinkTarget[]
|
|
42
|
+
): Array<ResolvedGraphLinkTarget | null> => {
|
|
43
|
+
const results: Array<ResolvedGraphLinkTarget | null> = Array.from(
|
|
44
|
+
{ length: targets.length },
|
|
45
|
+
() => null
|
|
46
|
+
);
|
|
47
|
+
const wikiTargets: Array<{
|
|
48
|
+
idx: number;
|
|
49
|
+
collection: string;
|
|
50
|
+
baseRef: string;
|
|
51
|
+
baseRefMd: string;
|
|
52
|
+
}> = [];
|
|
53
|
+
const markdownTargets: Array<{
|
|
54
|
+
idx: number;
|
|
55
|
+
collection: string;
|
|
56
|
+
relPath: string;
|
|
57
|
+
}> = [];
|
|
58
|
+
|
|
59
|
+
for (const [idx, target] of targets.entries()) {
|
|
60
|
+
if (target.linkType === "wiki") {
|
|
61
|
+
const baseRef = stripWikiMdExt(target.targetRefNorm);
|
|
62
|
+
wikiTargets.push({
|
|
63
|
+
idx,
|
|
64
|
+
collection: target.targetCollection,
|
|
65
|
+
baseRef,
|
|
66
|
+
baseRefMd: `${baseRef}.md`,
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
markdownTargets.push({
|
|
70
|
+
idx,
|
|
71
|
+
collection: target.targetCollection,
|
|
72
|
+
relPath: target.targetRefNorm,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const titleExpr = "lower(trim(d.title))";
|
|
78
|
+
const relExpr = "lower(d.rel_path)";
|
|
79
|
+
const baseRefExpr = "t.base_ref";
|
|
80
|
+
const baseRefMdExpr = "t.base_ref_md";
|
|
81
|
+
const wikiWhere = `
|
|
82
|
+
${titleExpr} = ${baseRefExpr}
|
|
83
|
+
OR ${titleExpr} = ${baseRefMdExpr}
|
|
84
|
+
OR ${suffixMatch(baseRefExpr, titleExpr)}
|
|
85
|
+
OR ${suffixMatch(baseRefMdExpr, `${titleExpr} || '.md'`)}
|
|
86
|
+
OR ${relExpr} = ${baseRefExpr}
|
|
87
|
+
OR ${relExpr} = ${baseRefMdExpr}
|
|
88
|
+
OR ${suffixMatch(relExpr, baseRefMdExpr)}
|
|
89
|
+
OR ${suffixMatch(relExpr, baseRefExpr)}
|
|
90
|
+
OR ${suffixMatch(baseRefMdExpr, relExpr)}
|
|
91
|
+
OR ${suffixMatch(baseRefExpr, relExpr)}
|
|
92
|
+
`;
|
|
93
|
+
const wikiRank = `CASE
|
|
94
|
+
WHEN ${titleExpr} = ${baseRefExpr} THEN 1
|
|
95
|
+
WHEN ${titleExpr} = ${baseRefMdExpr} THEN 2
|
|
96
|
+
WHEN ${suffixMatch(baseRefExpr, titleExpr)} THEN 3
|
|
97
|
+
WHEN ${suffixMatch(baseRefMdExpr, `${titleExpr} || '.md'`)} THEN 4
|
|
98
|
+
WHEN ${relExpr} = ${baseRefExpr} THEN 5
|
|
99
|
+
WHEN ${relExpr} = ${baseRefMdExpr} THEN 6
|
|
100
|
+
WHEN ${suffixMatch(relExpr, baseRefMdExpr)} THEN 7
|
|
101
|
+
WHEN ${suffixMatch(relExpr, baseRefExpr)} THEN 8
|
|
102
|
+
WHEN ${suffixMatch(baseRefMdExpr, relExpr)} THEN 9
|
|
103
|
+
WHEN ${suffixMatch(baseRefExpr, relExpr)} THEN 10
|
|
104
|
+
ELSE 99
|
|
105
|
+
END`;
|
|
106
|
+
|
|
107
|
+
const wikiBatchSize = Math.max(1, Math.floor(MAX_SQL_PARAMS / 4));
|
|
108
|
+
for (const batch of chunkArray(wikiTargets, wikiBatchSize)) {
|
|
109
|
+
const valuesClause = batch.map(() => "(?, ?, ?, ?)").join(", ");
|
|
110
|
+
const params = batch.flatMap((target) => [
|
|
111
|
+
target.idx,
|
|
112
|
+
target.collection,
|
|
113
|
+
target.baseRef,
|
|
114
|
+
target.baseRefMd,
|
|
115
|
+
]);
|
|
116
|
+
const rows = db
|
|
117
|
+
.query<
|
|
118
|
+
{
|
|
119
|
+
idx: number;
|
|
120
|
+
target_id: number;
|
|
121
|
+
target_docid: string;
|
|
122
|
+
match_rank: number;
|
|
123
|
+
match_count: number;
|
|
124
|
+
},
|
|
125
|
+
(string | number)[]
|
|
126
|
+
>(
|
|
127
|
+
`WITH targets(idx, collection, base_ref, base_ref_md) AS (
|
|
128
|
+
VALUES ${valuesClause}
|
|
129
|
+
),
|
|
130
|
+
candidates AS (
|
|
131
|
+
SELECT t.idx, d.id AS target_id, d.docid AS target_docid,
|
|
132
|
+
${wikiRank} AS match_rank
|
|
133
|
+
FROM targets t
|
|
134
|
+
JOIN documents d ON d.active = 1 AND d.collection = t.collection
|
|
135
|
+
WHERE ${wikiWhere}
|
|
136
|
+
),
|
|
137
|
+
best_candidates AS (
|
|
138
|
+
SELECT candidates.*
|
|
139
|
+
FROM candidates
|
|
140
|
+
JOIN (
|
|
141
|
+
SELECT idx, MIN(match_rank) AS best_rank
|
|
142
|
+
FROM candidates
|
|
143
|
+
GROUP BY idx
|
|
144
|
+
) best
|
|
145
|
+
ON best.idx = candidates.idx
|
|
146
|
+
AND best.best_rank = candidates.match_rank
|
|
147
|
+
),
|
|
148
|
+
ranked AS (
|
|
149
|
+
SELECT *,
|
|
150
|
+
COUNT(*) OVER (PARTITION BY idx) AS match_count,
|
|
151
|
+
ROW_NUMBER() OVER (PARTITION BY idx ORDER BY target_id) AS rn
|
|
152
|
+
FROM best_candidates
|
|
153
|
+
)
|
|
154
|
+
SELECT idx, target_id, target_docid, match_rank, match_count
|
|
155
|
+
FROM ranked
|
|
156
|
+
WHERE rn = 1`
|
|
157
|
+
)
|
|
158
|
+
.all(...params);
|
|
159
|
+
|
|
160
|
+
for (const row of rows) {
|
|
161
|
+
results[row.idx] = {
|
|
162
|
+
targetId: row.target_id,
|
|
163
|
+
targetDocid: row.target_docid,
|
|
164
|
+
matchRank: row.match_rank,
|
|
165
|
+
matchCount: row.match_count,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const markdownBatchSize = Math.max(1, Math.floor(MAX_SQL_PARAMS / 3));
|
|
171
|
+
for (const batch of chunkArray(markdownTargets, markdownBatchSize)) {
|
|
172
|
+
const valuesClause = batch.map(() => "(?, ?, ?)").join(", ");
|
|
173
|
+
const params = batch.flatMap((target) => [
|
|
174
|
+
target.idx,
|
|
175
|
+
target.collection,
|
|
176
|
+
target.relPath,
|
|
177
|
+
]);
|
|
178
|
+
const rows = db
|
|
179
|
+
.query<
|
|
180
|
+
{ idx: number; target_id: number; target_docid: string },
|
|
181
|
+
(string | number)[]
|
|
182
|
+
>(
|
|
183
|
+
`WITH targets(idx, collection, rel_path) AS (
|
|
184
|
+
VALUES ${valuesClause}
|
|
185
|
+
),
|
|
186
|
+
ranked AS (
|
|
187
|
+
SELECT t.idx, d.id AS target_id, d.docid AS target_docid,
|
|
188
|
+
ROW_NUMBER() OVER (PARTITION BY t.idx ORDER BY d.id) AS rn
|
|
189
|
+
FROM targets t
|
|
190
|
+
JOIN documents d ON d.active = 1
|
|
191
|
+
AND d.collection = t.collection
|
|
192
|
+
AND d.rel_path = t.rel_path
|
|
193
|
+
)
|
|
194
|
+
SELECT idx, target_id, target_docid
|
|
195
|
+
FROM ranked
|
|
196
|
+
WHERE rn = 1`
|
|
197
|
+
)
|
|
198
|
+
.all(...params);
|
|
199
|
+
|
|
200
|
+
for (const row of rows) {
|
|
201
|
+
results[row.idx] = {
|
|
202
|
+
targetId: row.target_id,
|
|
203
|
+
targetDocid: row.target_docid,
|
|
204
|
+
matchRank: 5,
|
|
205
|
+
matchCount: 1,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return results;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/** Resolve all targets in bounded SQL batches while retaining confidence inputs. */
|
|
214
|
+
export function resolveGraphLinkTargets(
|
|
215
|
+
db: Database,
|
|
216
|
+
targets: GraphLinkTarget[]
|
|
217
|
+
): Array<ResolvedGraphLinkTarget | null> {
|
|
218
|
+
const uniqueTargets: GraphLinkTarget[] = [];
|
|
219
|
+
const uniqueIndexByKey = new Map<string, number>();
|
|
220
|
+
const originalToUniqueIndex: number[] = [];
|
|
221
|
+
|
|
222
|
+
for (const target of targets) {
|
|
223
|
+
const key = JSON.stringify([
|
|
224
|
+
target.linkType,
|
|
225
|
+
target.targetCollection,
|
|
226
|
+
target.targetRefNorm,
|
|
227
|
+
]);
|
|
228
|
+
let uniqueIndex = uniqueIndexByKey.get(key);
|
|
229
|
+
if (uniqueIndex === undefined) {
|
|
230
|
+
uniqueIndex = uniqueTargets.length;
|
|
231
|
+
uniqueIndexByKey.set(key, uniqueIndex);
|
|
232
|
+
uniqueTargets.push(target);
|
|
233
|
+
}
|
|
234
|
+
originalToUniqueIndex.push(uniqueIndex);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const uniqueResults = resolveUniqueGraphLinkTargets(db, uniqueTargets);
|
|
238
|
+
return originalToUniqueIndex.map(
|
|
239
|
+
(uniqueIndex) => uniqueResults[uniqueIndex] ?? null
|
|
240
|
+
);
|
|
241
|
+
}
|