@gmickel/gno 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +6 -2
  2. package/assets/skill/SKILL.md +41 -16
  3. package/assets/skill/cli-reference.md +39 -0
  4. package/assets/skill/examples.md +41 -0
  5. package/assets/skill/mcp-reference.md +13 -1
  6. package/package.json +1 -1
  7. package/src/cli/commands/capture.ts +9 -6
  8. package/src/cli/commands/graph.ts +89 -2
  9. package/src/cli/commands/index-cmd.ts +17 -6
  10. package/src/cli/commands/links.ts +237 -54
  11. package/src/cli/commands/query.ts +212 -0
  12. package/src/cli/commands/ref-parser.ts +8 -103
  13. package/src/cli/commands/shared.ts +17 -1
  14. package/src/cli/commands/update.ts +17 -6
  15. package/src/cli/options.ts +4 -0
  16. package/src/cli/program.ts +176 -9
  17. package/src/config/content-types.ts +154 -0
  18. package/src/config/defaults.ts +1 -0
  19. package/src/config/index.ts +14 -0
  20. package/src/config/loader.ts +11 -2
  21. package/src/config/types.ts +37 -1
  22. package/src/core/config-mutation.ts +14 -2
  23. package/src/core/graph-query.ts +137 -0
  24. package/src/core/graph-resolver.ts +117 -0
  25. package/src/core/note-presets.ts +61 -5
  26. package/src/core/ref-parser.ts +145 -0
  27. package/src/ingestion/frontmatter.ts +170 -2
  28. package/src/ingestion/index.ts +2 -0
  29. package/src/ingestion/sync-options.ts +29 -0
  30. package/src/ingestion/sync.ts +385 -17
  31. package/src/ingestion/types.ts +14 -0
  32. package/src/mcp/tools/add-collection.ts +8 -5
  33. package/src/mcp/tools/capture.ts +5 -2
  34. package/src/mcp/tools/get.ts +1 -1
  35. package/src/mcp/tools/index-cmd.ts +13 -7
  36. package/src/mcp/tools/index.ts +97 -10
  37. package/src/mcp/tools/links.ts +83 -1
  38. package/src/mcp/tools/multi-get.ts +1 -1
  39. package/src/mcp/tools/query.ts +207 -0
  40. package/src/mcp/tools/sync.ts +12 -6
  41. package/src/mcp/tools/workspace-write.ts +16 -10
  42. package/src/pipeline/diagnose.ts +302 -0
  43. package/src/pipeline/filters.ts +119 -0
  44. package/src/pipeline/hybrid.ts +92 -17
  45. package/src/pipeline/search.ts +2 -0
  46. package/src/pipeline/types.ts +34 -0
  47. package/src/pipeline/vsearch.ts +4 -0
  48. package/src/publish/export-service.ts +1 -1
  49. package/src/sdk/client.ts +51 -24
  50. package/src/sdk/documents.ts +2 -2
  51. package/src/serve/background-runtime.ts +18 -4
  52. package/src/serve/config-sync.ts +9 -2
  53. package/src/serve/routes/api.ts +244 -24
  54. package/src/serve/routes/graph.ts +173 -1
  55. package/src/serve/server.ts +24 -1
  56. package/src/serve/watch-service.ts +12 -2
  57. package/src/store/migrations/009-content-type-rule-fingerprint.ts +36 -0
  58. package/src/store/migrations/010-typed-edges.ts +67 -0
  59. package/src/store/migrations/011-doc-edge-traversal-indexes.ts +30 -0
  60. package/src/store/migrations/index.ts +16 -1
  61. package/src/store/sqlite/adapter.ts +853 -114
  62. package/src/store/types.ts +147 -0
@@ -7,10 +7,15 @@
7
7
 
8
8
  import { basename } from "node:path";
9
9
 
10
- import type { DocLinkRow, DocumentRow, StorePort } from "../../store/types";
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 { parseRef } from "./ref-parser";
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
+ }