@gmickel/gno 1.9.0 → 1.10.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/assets/skill/SKILL.md +28 -16
- package/assets/skill/cli-reference.md +30 -0
- package/assets/skill/examples.md +20 -0
- package/assets/skill/mcp-reference.md +7 -1
- package/package.json +1 -1
- package/src/cli/commands/graph.ts +89 -2
- package/src/cli/commands/links.ts +237 -54
- package/src/cli/commands/query.ts +212 -0
- package/src/cli/commands/ref-parser.ts +8 -103
- package/src/cli/options.ts +4 -0
- package/src/cli/program.ts +176 -9
- package/src/config/content-types.ts +14 -0
- package/src/core/file-lock.ts +38 -3
- package/src/core/graph-query.ts +137 -0
- package/src/core/graph-resolver.ts +117 -0
- package/src/core/ref-parser.ts +145 -0
- package/src/ingestion/frontmatter.ts +95 -2
- package/src/ingestion/sync.ts +281 -1
- package/src/mcp/tools/get.ts +1 -1
- package/src/mcp/tools/index.ts +89 -1
- package/src/mcp/tools/links.ts +83 -1
- package/src/mcp/tools/multi-get.ts +1 -1
- package/src/mcp/tools/query.ts +207 -0
- package/src/pipeline/diagnose.ts +302 -0
- package/src/pipeline/filters.ts +119 -0
- package/src/pipeline/hybrid.ts +90 -17
- package/src/pipeline/types.ts +32 -0
- package/src/publish/export-service.ts +1 -1
- package/src/sdk/documents.ts +2 -2
- package/src/serve/routes/api.ts +194 -0
- package/src/serve/routes/graph.ts +173 -1
- package/src/serve/server.ts +24 -1
- package/src/store/migrations/010-typed-edges.ts +67 -0
- package/src/store/migrations/011-doc-edge-traversal-indexes.ts +30 -0
- package/src/store/migrations/index.ts +4 -0
- package/src/store/sqlite/adapter.ts +826 -102
- package/src/store/types.ts +141 -0
|
@@ -7,10 +7,15 @@
|
|
|
7
7
|
|
|
8
8
|
import { basename } from "node:path";
|
|
9
9
|
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
DocEdgeRow,
|
|
12
|
+
DocLinkRow,
|
|
13
|
+
DocumentRow,
|
|
14
|
+
StorePort,
|
|
15
|
+
} from "../../store/types";
|
|
11
16
|
|
|
12
17
|
import { normalizeWikiName } from "../../core/links";
|
|
13
|
-
import {
|
|
18
|
+
import { resolveDocRef } from "../../core/ref-parser";
|
|
14
19
|
import { initStore } from "./shared";
|
|
15
20
|
|
|
16
21
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -22,6 +27,10 @@ export interface LinksListOptions {
|
|
|
22
27
|
configPath?: string;
|
|
23
28
|
/** Filter by link type */
|
|
24
29
|
type?: "wiki" | "markdown";
|
|
30
|
+
/** Filter semantic edges by relationship type */
|
|
31
|
+
edgeType?: string;
|
|
32
|
+
/** Alias for edgeType */
|
|
33
|
+
relation?: string;
|
|
25
34
|
/** JSON output */
|
|
26
35
|
json?: boolean;
|
|
27
36
|
/** Markdown output */
|
|
@@ -44,8 +53,18 @@ export interface LinkWithResolution {
|
|
|
44
53
|
resolvedUri?: string;
|
|
45
54
|
}
|
|
46
55
|
|
|
56
|
+
export interface SemanticLinkItem {
|
|
57
|
+
targetDocid: string;
|
|
58
|
+
targetUri: string;
|
|
59
|
+
targetTitle?: string;
|
|
60
|
+
edgeType: string;
|
|
61
|
+
relationType: string;
|
|
62
|
+
confidence: DocEdgeRow["confidence"];
|
|
63
|
+
edgeSource: DocEdgeRow["edgeSource"];
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
export interface LinksListResponse {
|
|
48
|
-
links: LinkWithResolution
|
|
67
|
+
links: Array<LinkWithResolution | SemanticLinkItem>;
|
|
49
68
|
meta: {
|
|
50
69
|
docid: string;
|
|
51
70
|
uri: string;
|
|
@@ -53,6 +72,9 @@ export interface LinksListResponse {
|
|
|
53
72
|
totalLinks: number;
|
|
54
73
|
resolvedCount: number;
|
|
55
74
|
typeFilter?: "wiki" | "markdown";
|
|
75
|
+
edgeTypeFilter?: string;
|
|
76
|
+
relationFilter?: string;
|
|
77
|
+
semantic: boolean;
|
|
56
78
|
};
|
|
57
79
|
}
|
|
58
80
|
|
|
@@ -60,6 +82,56 @@ export type LinksListResult =
|
|
|
60
82
|
| { success: true; data: LinksListResponse }
|
|
61
83
|
| { success: false; error: string; isValidation?: boolean };
|
|
62
84
|
|
|
85
|
+
function resolveSemanticEdgeFilter(options: {
|
|
86
|
+
edgeType?: string;
|
|
87
|
+
relation?: string;
|
|
88
|
+
}): string | undefined {
|
|
89
|
+
if (
|
|
90
|
+
options.edgeType &&
|
|
91
|
+
options.relation &&
|
|
92
|
+
options.edgeType !== options.relation
|
|
93
|
+
) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
return options.edgeType ?? options.relation;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function mapEdgeToLinkOutput(edge: DocEdgeRow): SemanticLinkItem {
|
|
100
|
+
return {
|
|
101
|
+
targetDocid: edge.targetDocid,
|
|
102
|
+
targetUri: edge.targetUri,
|
|
103
|
+
...(edge.targetTitle && { targetTitle: edge.targetTitle }),
|
|
104
|
+
edgeType: edge.edgeType,
|
|
105
|
+
relationType: edge.relationType,
|
|
106
|
+
confidence: edge.confidence,
|
|
107
|
+
edgeSource: edge.edgeSource,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function mapEdgeToBacklinkOutput(edge: DocEdgeRow): SemanticBacklinkItem {
|
|
112
|
+
return {
|
|
113
|
+
sourceDocid: edge.sourceDocid,
|
|
114
|
+
sourceUri: edge.sourceUri,
|
|
115
|
+
...(edge.sourceTitle && { sourceTitle: edge.sourceTitle }),
|
|
116
|
+
edgeType: edge.edgeType,
|
|
117
|
+
relationType: edge.relationType,
|
|
118
|
+
confidence: edge.confidence,
|
|
119
|
+
edgeSource: edge.edgeSource,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isSemanticLink(
|
|
124
|
+
link: LinkWithResolution | SemanticLinkItem
|
|
125
|
+
): link is SemanticLinkItem {
|
|
126
|
+
return "edgeType" in link;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isSemanticBacklink(
|
|
130
|
+
backlink: BacklinkItem | SemanticBacklinkItem
|
|
131
|
+
): backlink is SemanticBacklinkItem {
|
|
132
|
+
return "edgeType" in backlink;
|
|
133
|
+
}
|
|
134
|
+
|
|
63
135
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
136
|
// Types - Backlinks
|
|
65
137
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -69,6 +141,10 @@ export interface BacklinksOptions {
|
|
|
69
141
|
configPath?: string;
|
|
70
142
|
/** Filter by collection */
|
|
71
143
|
collection?: string;
|
|
144
|
+
/** Filter semantic edges by relationship type */
|
|
145
|
+
edgeType?: string;
|
|
146
|
+
/** Alias for edgeType */
|
|
147
|
+
relation?: string;
|
|
72
148
|
/** JSON output */
|
|
73
149
|
json?: boolean;
|
|
74
150
|
/** Markdown output */
|
|
@@ -84,14 +160,27 @@ export interface BacklinkItem {
|
|
|
84
160
|
startCol: number;
|
|
85
161
|
}
|
|
86
162
|
|
|
163
|
+
export interface SemanticBacklinkItem {
|
|
164
|
+
sourceDocid: string;
|
|
165
|
+
sourceUri: string;
|
|
166
|
+
sourceTitle?: string;
|
|
167
|
+
edgeType: string;
|
|
168
|
+
relationType: string;
|
|
169
|
+
confidence: DocEdgeRow["confidence"];
|
|
170
|
+
edgeSource: DocEdgeRow["edgeSource"];
|
|
171
|
+
}
|
|
172
|
+
|
|
87
173
|
export interface BacklinksResponse {
|
|
88
|
-
backlinks: BacklinkItem
|
|
174
|
+
backlinks: Array<BacklinkItem | SemanticBacklinkItem>;
|
|
89
175
|
meta: {
|
|
90
176
|
docid: string;
|
|
91
177
|
uri: string;
|
|
92
178
|
title?: string;
|
|
93
179
|
totalBacklinks: number;
|
|
94
180
|
collection?: string;
|
|
181
|
+
edgeTypeFilter?: string;
|
|
182
|
+
relationFilter?: string;
|
|
183
|
+
semantic: boolean;
|
|
95
184
|
};
|
|
96
185
|
}
|
|
97
186
|
|
|
@@ -144,56 +233,6 @@ export type SimilarResult =
|
|
|
144
233
|
| { success: true; data: SimilarResponse }
|
|
145
234
|
| { success: false; error: string; isValidation?: boolean };
|
|
146
235
|
|
|
147
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
148
|
-
// Helper: Resolve document reference (supports all ref formats)
|
|
149
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
150
|
-
|
|
151
|
-
async function resolveDocRef(
|
|
152
|
-
store: StorePort,
|
|
153
|
-
docRef: string
|
|
154
|
-
): Promise<{ doc: DocumentRow } | { error: string; isValidation: boolean }> {
|
|
155
|
-
// Parse the ref to determine type
|
|
156
|
-
const parsed = parseRef(docRef);
|
|
157
|
-
|
|
158
|
-
if ("error" in parsed) {
|
|
159
|
-
return { error: parsed.error, isValidation: true };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
let doc: DocumentRow | null = null;
|
|
163
|
-
|
|
164
|
-
switch (parsed.type) {
|
|
165
|
-
case "docid": {
|
|
166
|
-
const result = await store.getDocumentByDocid(parsed.value);
|
|
167
|
-
if (result.ok && result.value) {
|
|
168
|
-
doc = result.value;
|
|
169
|
-
}
|
|
170
|
-
break;
|
|
171
|
-
}
|
|
172
|
-
case "uri": {
|
|
173
|
-
const result = await store.getDocumentByUri(parsed.value);
|
|
174
|
-
if (result.ok && result.value) {
|
|
175
|
-
doc = result.value;
|
|
176
|
-
}
|
|
177
|
-
break;
|
|
178
|
-
}
|
|
179
|
-
case "collPath": {
|
|
180
|
-
// Build URI from collection/path
|
|
181
|
-
const uri = `gno://${parsed.collection}/${parsed.relPath}`;
|
|
182
|
-
const result = await store.getDocumentByUri(uri);
|
|
183
|
-
if (result.ok && result.value) {
|
|
184
|
-
doc = result.value;
|
|
185
|
-
}
|
|
186
|
-
break;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (!doc) {
|
|
191
|
-
return { error: `Document not found: ${docRef}`, isValidation: true };
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return { doc };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
236
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
198
237
|
// Helper: Build resolution indexes (cached per collection)
|
|
199
238
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -315,6 +354,26 @@ export async function linksList(
|
|
|
315
354
|
docRef: string,
|
|
316
355
|
options: LinksListOptions = {}
|
|
317
356
|
): Promise<LinksListResult> {
|
|
357
|
+
if (options.type && (options.edgeType || options.relation)) {
|
|
358
|
+
return {
|
|
359
|
+
success: false,
|
|
360
|
+
error: "--type cannot be combined with --edge-type or --relation",
|
|
361
|
+
isValidation: true,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
if (
|
|
365
|
+
options.edgeType &&
|
|
366
|
+
options.relation &&
|
|
367
|
+
options.edgeType !== options.relation
|
|
368
|
+
) {
|
|
369
|
+
return {
|
|
370
|
+
success: false,
|
|
371
|
+
error:
|
|
372
|
+
"--edge-type and --relation are aliases and must match when both are provided",
|
|
373
|
+
isValidation: true,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
318
377
|
const initResult = await initStore({
|
|
319
378
|
configPath: options.configPath,
|
|
320
379
|
syncConfig: false,
|
|
@@ -336,6 +395,35 @@ export async function linksList(
|
|
|
336
395
|
}
|
|
337
396
|
const { doc } = resolved;
|
|
338
397
|
|
|
398
|
+
const semanticEdgeType = resolveSemanticEdgeFilter(options);
|
|
399
|
+
if (semanticEdgeType) {
|
|
400
|
+
const edgesResult = await store.getEdgesForDoc(doc.id, {
|
|
401
|
+
edgeType: semanticEdgeType,
|
|
402
|
+
});
|
|
403
|
+
if (!edgesResult.ok) {
|
|
404
|
+
return { success: false, error: edgesResult.error.message };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const linksWithResolution = edgesResult.value.map(mapEdgeToLinkOutput);
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
success: true,
|
|
411
|
+
data: {
|
|
412
|
+
links: linksWithResolution,
|
|
413
|
+
meta: {
|
|
414
|
+
docid: doc.docid,
|
|
415
|
+
uri: doc.uri,
|
|
416
|
+
...(doc.title && { title: doc.title }),
|
|
417
|
+
totalLinks: linksWithResolution.length,
|
|
418
|
+
resolvedCount: linksWithResolution.length,
|
|
419
|
+
...(options.edgeType && { edgeTypeFilter: options.edgeType }),
|
|
420
|
+
...(options.relation && { relationFilter: options.relation }),
|
|
421
|
+
semantic: true,
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
339
427
|
// Get links
|
|
340
428
|
const linksResult = await store.getLinksForDoc(doc.id);
|
|
341
429
|
if (!linksResult.ok) {
|
|
@@ -411,6 +499,7 @@ export async function linksList(
|
|
|
411
499
|
totalLinks: linksWithResolution.length,
|
|
412
500
|
resolvedCount,
|
|
413
501
|
...(options.type && { typeFilter: options.type }),
|
|
502
|
+
semantic: false,
|
|
414
503
|
},
|
|
415
504
|
},
|
|
416
505
|
};
|
|
@@ -431,6 +520,19 @@ export async function backlinks(
|
|
|
431
520
|
docRef: string,
|
|
432
521
|
options: BacklinksOptions = {}
|
|
433
522
|
): Promise<BacklinksResult> {
|
|
523
|
+
if (
|
|
524
|
+
options.edgeType &&
|
|
525
|
+
options.relation &&
|
|
526
|
+
options.edgeType !== options.relation
|
|
527
|
+
) {
|
|
528
|
+
return {
|
|
529
|
+
success: false,
|
|
530
|
+
error:
|
|
531
|
+
"--edge-type and --relation are aliases and must match when both are provided",
|
|
532
|
+
isValidation: true,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
434
536
|
const initResult = await initStore({
|
|
435
537
|
configPath: options.configPath,
|
|
436
538
|
syncConfig: false,
|
|
@@ -452,6 +554,36 @@ export async function backlinks(
|
|
|
452
554
|
}
|
|
453
555
|
const { doc } = resolved;
|
|
454
556
|
|
|
557
|
+
const semanticEdgeType = resolveSemanticEdgeFilter(options);
|
|
558
|
+
if (semanticEdgeType) {
|
|
559
|
+
const edgesResult = await store.getEdgeBacklinksForDoc(doc.id, {
|
|
560
|
+
collection: options.collection,
|
|
561
|
+
edgeType: semanticEdgeType,
|
|
562
|
+
});
|
|
563
|
+
if (!edgesResult.ok) {
|
|
564
|
+
return { success: false, error: edgesResult.error.message };
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const backlinkItems = edgesResult.value.map(mapEdgeToBacklinkOutput);
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
success: true,
|
|
571
|
+
data: {
|
|
572
|
+
backlinks: backlinkItems,
|
|
573
|
+
meta: {
|
|
574
|
+
docid: doc.docid,
|
|
575
|
+
uri: doc.uri,
|
|
576
|
+
...(doc.title && { title: doc.title }),
|
|
577
|
+
totalBacklinks: backlinkItems.length,
|
|
578
|
+
...(options.collection && { collection: options.collection }),
|
|
579
|
+
...(options.edgeType && { edgeTypeFilter: options.edgeType }),
|
|
580
|
+
...(options.relation && { relationFilter: options.relation }),
|
|
581
|
+
semantic: true,
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
455
587
|
// Get backlinks
|
|
456
588
|
const backlinksResult = await store.getBacklinksForDoc(doc.id, {
|
|
457
589
|
collection: options.collection,
|
|
@@ -489,6 +621,7 @@ export async function backlinks(
|
|
|
489
621
|
...(doc.title && { title: doc.title }),
|
|
490
622
|
totalBacklinks: backlinkItems.length,
|
|
491
623
|
...(options.collection && { collection: options.collection }),
|
|
624
|
+
semantic: false,
|
|
492
625
|
},
|
|
493
626
|
},
|
|
494
627
|
};
|
|
@@ -746,6 +879,24 @@ export function formatLinksList(
|
|
|
746
879
|
if (data.links.length === 0) {
|
|
747
880
|
return `# Links from ${data.meta.docid}\n\nNo outgoing links found.`;
|
|
748
881
|
}
|
|
882
|
+
if (data.meta.semantic) {
|
|
883
|
+
const lines: string[] = [
|
|
884
|
+
`# Links from ${escapeTableCell(data.meta.title ?? data.meta.docid)}`,
|
|
885
|
+
"",
|
|
886
|
+
`*${data.meta.totalLinks} semantic links*`,
|
|
887
|
+
"",
|
|
888
|
+
"| Target | Relation | Confidence | Source |",
|
|
889
|
+
"|--------|----------|------------|--------|",
|
|
890
|
+
];
|
|
891
|
+
for (const l of data.links) {
|
|
892
|
+
if (!isSemanticLink(l)) continue;
|
|
893
|
+
const target = escapeTableCell(l.targetTitle ?? l.targetDocid);
|
|
894
|
+
lines.push(
|
|
895
|
+
`| ${target} | ${l.edgeType} | ${l.confidence} | ${l.edgeSource} |`
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
return lines.join("\n");
|
|
899
|
+
}
|
|
749
900
|
const lines: string[] = [
|
|
750
901
|
`# Links from ${escapeTableCell(data.meta.title ?? data.meta.docid)}`,
|
|
751
902
|
"",
|
|
@@ -755,6 +906,7 @@ export function formatLinksList(
|
|
|
755
906
|
"|-----------|------|------|------|----------|",
|
|
756
907
|
];
|
|
757
908
|
for (const l of data.links) {
|
|
909
|
+
if (isSemanticLink(l)) continue;
|
|
758
910
|
const targetRef = escapeTableCell(l.targetRef);
|
|
759
911
|
const text = l.linkText ? escapeTableCell(l.linkText) : "-";
|
|
760
912
|
const resolved = l.resolved ? `\`${l.resolvedDocid}\`` : "-";
|
|
@@ -772,6 +924,11 @@ export function formatLinksList(
|
|
|
772
924
|
|
|
773
925
|
const lines: string[] = [];
|
|
774
926
|
for (const l of data.links) {
|
|
927
|
+
if (isSemanticLink(l)) {
|
|
928
|
+
const target = l.targetTitle ?? l.targetDocid;
|
|
929
|
+
lines.push(`${l.edgeType}\t${target}\t${l.confidence}\t${l.edgeSource}`);
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
775
932
|
const target = l.targetRef;
|
|
776
933
|
const status = l.resolved ? `-> ${l.resolvedDocid}` : "(unresolved)";
|
|
777
934
|
lines.push(
|
|
@@ -811,6 +968,24 @@ export function formatBacklinks(
|
|
|
811
968
|
if (data.backlinks.length === 0) {
|
|
812
969
|
return `# Backlinks to ${data.meta.docid}\n\nNo backlinks found.`;
|
|
813
970
|
}
|
|
971
|
+
if (data.meta.semantic) {
|
|
972
|
+
const lines: string[] = [
|
|
973
|
+
`# Backlinks to ${escapeTableCell(data.meta.title ?? data.meta.docid)}`,
|
|
974
|
+
"",
|
|
975
|
+
`*${data.meta.totalBacklinks} semantic backlinks*`,
|
|
976
|
+
"",
|
|
977
|
+
"| Source | Relation | Confidence | Edge Source |",
|
|
978
|
+
"|--------|----------|------------|-------------|",
|
|
979
|
+
];
|
|
980
|
+
for (const bl of data.backlinks) {
|
|
981
|
+
if (!isSemanticBacklink(bl)) continue;
|
|
982
|
+
const source = escapeTableCell(bl.sourceTitle ?? bl.sourceDocid);
|
|
983
|
+
lines.push(
|
|
984
|
+
`| ${source} | ${bl.edgeType} | ${bl.confidence} | ${bl.edgeSource} |`
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
return lines.join("\n");
|
|
988
|
+
}
|
|
814
989
|
const lines: string[] = [
|
|
815
990
|
`# Backlinks to ${escapeTableCell(data.meta.title ?? data.meta.docid)}`,
|
|
816
991
|
"",
|
|
@@ -820,6 +995,7 @@ export function formatBacklinks(
|
|
|
820
995
|
"|--------|------|-----------|",
|
|
821
996
|
];
|
|
822
997
|
for (const bl of data.backlinks) {
|
|
998
|
+
if (isSemanticBacklink(bl)) continue;
|
|
823
999
|
const source = escapeTableCell(bl.sourceTitle ?? bl.sourceDocid);
|
|
824
1000
|
const text = bl.linkText ? escapeTableCell(bl.linkText) : "-";
|
|
825
1001
|
lines.push(`| ${source} | ${bl.startLine} | ${text} |`);
|
|
@@ -834,6 +1010,13 @@ export function formatBacklinks(
|
|
|
834
1010
|
|
|
835
1011
|
const lines: string[] = [];
|
|
836
1012
|
for (const bl of data.backlinks) {
|
|
1013
|
+
if (isSemanticBacklink(bl)) {
|
|
1014
|
+
const source = bl.sourceTitle ?? bl.sourceDocid;
|
|
1015
|
+
lines.push(
|
|
1016
|
+
`${source}\t${bl.edgeType}\t${bl.confidence}\t${bl.edgeSource}`
|
|
1017
|
+
);
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
837
1020
|
const source = bl.sourceTitle ?? bl.sourceDocid;
|
|
838
1021
|
const text = bl.linkText ? `"${bl.linkText}"` : "";
|
|
839
1022
|
lines.push(`${source}\t${bl.startLine}:${bl.startCol}\t${text}`);
|
|
@@ -12,9 +12,17 @@ import type {
|
|
|
12
12
|
} from "../../llm/types";
|
|
13
13
|
import type { HybridSearchOptions, SearchResults } from "../../pipeline/types";
|
|
14
14
|
|
|
15
|
+
import {
|
|
16
|
+
fingerprintContentTypeRules,
|
|
17
|
+
normalizeContentTypes,
|
|
18
|
+
} from "../../config";
|
|
15
19
|
import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
|
|
16
20
|
import { resolveDownloadPolicy } from "../../llm/policy";
|
|
17
21
|
import { resolveModelUri } from "../../llm/registry";
|
|
22
|
+
import {
|
|
23
|
+
diagnoseQueryTarget,
|
|
24
|
+
type QueryDiagnoseResult as PipelineQueryDiagnoseResult,
|
|
25
|
+
} from "../../pipeline/diagnose";
|
|
18
26
|
import { type HybridSearchDeps, searchHybrid } from "../../pipeline/hybrid";
|
|
19
27
|
import {
|
|
20
28
|
createVectorIndexPort,
|
|
@@ -67,6 +75,25 @@ export type QueryResult =
|
|
|
67
75
|
| { success: true; data: SearchResults }
|
|
68
76
|
| { success: false; error: string };
|
|
69
77
|
|
|
78
|
+
export type QueryDiagnoseCommandOptions = HybridSearchOptions & {
|
|
79
|
+
target: string;
|
|
80
|
+
configPath?: string;
|
|
81
|
+
indexName?: string;
|
|
82
|
+
embedModel?: string;
|
|
83
|
+
expandModel?: string;
|
|
84
|
+
genModel?: string;
|
|
85
|
+
rerankModel?: string;
|
|
86
|
+
json?: boolean;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export interface QueryDiagnoseFormatOptions {
|
|
90
|
+
format: "terminal" | "json";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type QueryDiagnoseResult =
|
|
94
|
+
| { success: true; data: PipelineQueryDiagnoseResult }
|
|
95
|
+
| { success: false; error: string };
|
|
96
|
+
|
|
70
97
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
71
98
|
// Command Implementation
|
|
72
99
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -229,6 +256,149 @@ export async function query(
|
|
|
229
256
|
}
|
|
230
257
|
}
|
|
231
258
|
|
|
259
|
+
// oxlint-disable-next-line max-lines-per-function -- mirrors query setup for diagnose-specific shared core
|
|
260
|
+
export async function queryDiagnose(
|
|
261
|
+
queryText: string,
|
|
262
|
+
options: QueryDiagnoseCommandOptions
|
|
263
|
+
): Promise<QueryDiagnoseResult> {
|
|
264
|
+
const initResult = await initStore({
|
|
265
|
+
configPath: options.configPath,
|
|
266
|
+
indexName: options.indexName,
|
|
267
|
+
collection: options.collection,
|
|
268
|
+
syncConfig: false,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (!initResult.ok) {
|
|
272
|
+
return { success: false, error: initResult.error };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const { store, config } = initResult;
|
|
276
|
+
let embedPort: EmbeddingPort | null = null;
|
|
277
|
+
let expandPort: GenerationPort | null = null;
|
|
278
|
+
let rerankPort: RerankPort | null = null;
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
const globals = getGlobals();
|
|
282
|
+
const policy = resolveDownloadPolicy(process.env, {
|
|
283
|
+
offline: globals.offline,
|
|
284
|
+
});
|
|
285
|
+
const showProgress = !options.json && process.stderr.isTTY;
|
|
286
|
+
const downloadProgress = showProgress
|
|
287
|
+
? createThrottledProgressRenderer(createProgressRenderer())
|
|
288
|
+
: undefined;
|
|
289
|
+
const shouldCreateEmbeddingPort =
|
|
290
|
+
!options.noExpand || !options.noRerank || Boolean(options.graph);
|
|
291
|
+
const shouldCreateAnyModel =
|
|
292
|
+
shouldCreateEmbeddingPort || !options.noExpand || !options.noRerank;
|
|
293
|
+
const llm = shouldCreateAnyModel ? new LlmAdapter(config) : null;
|
|
294
|
+
|
|
295
|
+
const embedUri = shouldCreateEmbeddingPort
|
|
296
|
+
? resolveModelUri(config, "embed", options.embedModel, options.collection)
|
|
297
|
+
: null;
|
|
298
|
+
if (embedUri && llm) {
|
|
299
|
+
const embedResult = await llm.createEmbeddingPort(embedUri, {
|
|
300
|
+
policy,
|
|
301
|
+
onProgress: downloadProgress
|
|
302
|
+
? (progress) => downloadProgress("embed", progress)
|
|
303
|
+
: undefined,
|
|
304
|
+
});
|
|
305
|
+
if (embedResult.ok) {
|
|
306
|
+
embedPort = embedResult.value;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (llm && !options.noExpand && !options.queryModes?.length) {
|
|
311
|
+
const expandUri = resolveModelUri(
|
|
312
|
+
config,
|
|
313
|
+
"expand",
|
|
314
|
+
options.expandModel ?? options.genModel,
|
|
315
|
+
options.collection
|
|
316
|
+
);
|
|
317
|
+
const genResult = await llm.createExpansionPort(expandUri, {
|
|
318
|
+
policy,
|
|
319
|
+
onProgress: downloadProgress
|
|
320
|
+
? (progress) => downloadProgress("expand", progress)
|
|
321
|
+
: undefined,
|
|
322
|
+
});
|
|
323
|
+
if (genResult.ok) {
|
|
324
|
+
expandPort = genResult.value;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (llm && !options.noRerank) {
|
|
329
|
+
const rerankUri = resolveModelUri(
|
|
330
|
+
config,
|
|
331
|
+
"rerank",
|
|
332
|
+
options.rerankModel,
|
|
333
|
+
options.collection
|
|
334
|
+
);
|
|
335
|
+
const rerankResult = await llm.createRerankPort(rerankUri, {
|
|
336
|
+
policy,
|
|
337
|
+
onProgress: downloadProgress
|
|
338
|
+
? (progress) => downloadProgress("rerank", progress)
|
|
339
|
+
: undefined,
|
|
340
|
+
});
|
|
341
|
+
if (rerankResult.ok) {
|
|
342
|
+
rerankPort = rerankResult.value;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (showProgress && downloadProgress) {
|
|
347
|
+
process.stderr.write("\n");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let vectorIndex: VectorIndexPort | null = null;
|
|
351
|
+
if (embedPort && embedUri) {
|
|
352
|
+
const embedInitResult = await embedPort.init();
|
|
353
|
+
if (embedInitResult.ok) {
|
|
354
|
+
const dimensions = embedPort.dimensions();
|
|
355
|
+
const db = store.getRawDb();
|
|
356
|
+
const vectorResult = await createVectorIndexPort(db, {
|
|
357
|
+
model: embedUri,
|
|
358
|
+
dimensions,
|
|
359
|
+
});
|
|
360
|
+
if (vectorResult.ok) {
|
|
361
|
+
vectorIndex = vectorResult.value;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const contentTypeRules = normalizeContentTypes(
|
|
367
|
+
config.contentTypes ?? []
|
|
368
|
+
).rules;
|
|
369
|
+
const deps: HybridSearchDeps = {
|
|
370
|
+
store,
|
|
371
|
+
config,
|
|
372
|
+
vectorIndex,
|
|
373
|
+
embedPort,
|
|
374
|
+
expandPort,
|
|
375
|
+
rerankPort,
|
|
376
|
+
};
|
|
377
|
+
const result = await diagnoseQueryTarget(deps, queryText, {
|
|
378
|
+
...options,
|
|
379
|
+
contentTypeRules,
|
|
380
|
+
contentTypeRulesFingerprint:
|
|
381
|
+
fingerprintContentTypeRules(contentTypeRules),
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
if (!result.ok) {
|
|
385
|
+
return { success: false, error: result.error.message };
|
|
386
|
+
}
|
|
387
|
+
return { success: true, data: result.value };
|
|
388
|
+
} finally {
|
|
389
|
+
if (embedPort) {
|
|
390
|
+
await embedPort.dispose();
|
|
391
|
+
}
|
|
392
|
+
if (expandPort) {
|
|
393
|
+
await expandPort.dispose();
|
|
394
|
+
}
|
|
395
|
+
if (rerankPort) {
|
|
396
|
+
await rerankPort.dispose();
|
|
397
|
+
}
|
|
398
|
+
await store.close();
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
232
402
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
233
403
|
// Formatters
|
|
234
404
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -283,3 +453,45 @@ export function formatQuery(
|
|
|
283
453
|
terminalLinks: options.terminalLinks,
|
|
284
454
|
});
|
|
285
455
|
}
|
|
456
|
+
|
|
457
|
+
export function formatQueryDiagnose(
|
|
458
|
+
result: QueryDiagnoseResult,
|
|
459
|
+
options: QueryDiagnoseFormatOptions
|
|
460
|
+
): string {
|
|
461
|
+
if (!result.success) {
|
|
462
|
+
return options.format === "json"
|
|
463
|
+
? JSON.stringify({
|
|
464
|
+
error: { code: "QUERY_DIAGNOSE_FAILED", message: result.error },
|
|
465
|
+
})
|
|
466
|
+
: `Error: ${result.error}`;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (options.format === "json") {
|
|
470
|
+
return JSON.stringify(result.data, null, 2);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const target = result.data.target;
|
|
474
|
+
const lines = [
|
|
475
|
+
`Target: ${target.uri ?? target.ref}`,
|
|
476
|
+
`Status: ${target.status}`,
|
|
477
|
+
`Mode: ${result.data.meta.mode}`,
|
|
478
|
+
];
|
|
479
|
+
if (target.filterReasons.length > 0) {
|
|
480
|
+
lines.push(`Filters: ${target.filterReasons.join(", ")}`);
|
|
481
|
+
}
|
|
482
|
+
if (target.graphHints.length > 0) {
|
|
483
|
+
lines.push(`Graph hints: ${target.graphHints.join(", ")}`);
|
|
484
|
+
}
|
|
485
|
+
if (result.data.stages.length > 0) {
|
|
486
|
+
lines.push("", "Stages:");
|
|
487
|
+
for (const stage of result.data.stages) {
|
|
488
|
+
const rank = stage.rank === null ? "-" : `#${stage.rank}`;
|
|
489
|
+
const score = stage.score === null ? "-" : stage.score.toFixed(4);
|
|
490
|
+
const reason = stage.dropReason ? ` ${stage.dropReason}` : "";
|
|
491
|
+
lines.push(
|
|
492
|
+
` ${stage.id}: ${stage.status} present=${stage.present} rank=${rank} score=${score}${reason}`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return lines.join("\n");
|
|
497
|
+
}
|