@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
@@ -12,7 +12,7 @@ import { acquireWriteLock, type WriteLockHandle } from "../../core/file-lock";
12
12
  import { JobError } from "../../core/job-manager";
13
13
  import { normalizeCollectionName } from "../../core/validation";
14
14
  import { embedBacklog } from "../../embed";
15
- import { defaultSyncService } from "../../ingestion";
15
+ import { defaultSyncService, withContentTypeRules } from "../../ingestion";
16
16
  import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
17
17
  import { resolveModelUri } from "../../llm/registry";
18
18
  import {
@@ -95,11 +95,14 @@ export function handleIndex(
95
95
  ? [collection.name]
96
96
  : ctx.collections.map((entry) => entry.name);
97
97
 
98
- const options = {
99
- gitPull: args.gitPull ?? false,
100
- // Security: MCP never runs updateCmd by default
101
- runUpdateCmd: false,
102
- };
98
+ const options = withContentTypeRules(
99
+ {
100
+ gitPull: args.gitPull ?? false,
101
+ // Security: MCP never runs updateCmd by default
102
+ runUpdateCmd: false,
103
+ },
104
+ ctx.config
105
+ );
103
106
 
104
107
  const modelUri = resolveModelUri(
105
108
  ctx.config,
@@ -203,7 +206,10 @@ export function handleIndex(
203
206
  collections,
204
207
  status: "started",
205
208
  phases: ["sync", "embed"],
206
- options,
209
+ options: {
210
+ gitPull: options.gitPull ?? false,
211
+ runUpdateCmd: options.runUpdateCmd ?? false,
212
+ },
207
213
  };
208
214
 
209
215
  return result;
@@ -11,6 +11,7 @@ import { z } from "zod";
11
11
  import type { ToolContext } from "../server";
12
12
 
13
13
  import { CAPTURE_MAX_TEXT_BYTES } from "../../core/capture";
14
+ import { NOTE_PRESETS, type NotePresetId } from "../../core/note-presets";
14
15
  import { normalizeTag } from "../../core/tags";
15
16
  import { handleAddCollection } from "./add-collection";
16
17
  import { handleCapture } from "./capture";
@@ -22,6 +23,7 @@ import { handleJobStatus } from "./job-status";
22
23
  import {
23
24
  handleBacklinks,
24
25
  handleGraph,
26
+ handleGraphQuery,
25
27
  handleGraphNeighbors,
26
28
  handleGraphPath,
27
29
  handleLinks,
@@ -30,7 +32,7 @@ import {
30
32
  import { handleListJobs } from "./list-jobs";
31
33
  import { handleListTags } from "./list-tags";
32
34
  import { handleMultiGet } from "./multi-get";
33
- import { handleQuery } from "./query";
35
+ import { handleQuery, handleQueryDiagnose } from "./query";
34
36
  import { handleRemoveCollection } from "./remove-collection";
35
37
  import { handleSearch } from "./search";
36
38
  import { handleStatus } from "./status";
@@ -63,6 +65,8 @@ export const MCP_TOOL_DESCRIPTIONS = {
63
65
  "Vector semantic search. Finds conceptually similar docs with different wording. Best after embeddings are current; use intent to disambiguate short terms. Use gno_query for default hybrid retrieval.",
64
66
  query:
65
67
  "Hybrid search (BM25 + vector + optional expansion/reranking). Recommended default. Use intent for ambiguous terms, queryModes to combine term/intent/hyde strategies, fast=true for quick lookup, thorough=true when recall matters, and candidateLimit to trade latency for coverage.",
68
+ queryDiagnose:
69
+ "Diagnose why one target document does or does not appear for a query. Use when an important doc is missing, a filter may exclude it, or you need stage-by-stage BM25/vector/fusion/graph/rerank evidence before changing retrieval strategy.",
66
70
  get: "Retrieve one document by gno:// URI, docid (#abc123), or collection/path. After search results include line, pass fromLine and lineCount to fetch only the relevant range before expanding to the full document.",
67
71
  multiGet:
68
72
  "Retrieve multiple documents by refs array or glob pattern. Use after gno_search/gno_query to batch top result URIs/docids; set maxBytes and lineNumbers to control context size.",
@@ -142,7 +146,12 @@ const searchInputSchema = z.object({
142
146
  .describe("Require ANY of these tags (OR filter)"),
143
147
  });
144
148
 
145
- const captureInputSchema = z.object({
149
+ const notePresetIds = NOTE_PRESETS.map((preset) => preset.id) as [
150
+ NotePresetId,
151
+ ...NotePresetId[],
152
+ ];
153
+
154
+ export const captureInputSchema = z.object({
146
155
  collection: z
147
156
  .string()
148
157
  .min(1, "Collection cannot be empty")
@@ -173,14 +182,7 @@ const captureInputSchema = z.object({
173
182
  .optional()
174
183
  .describe("How to handle name collisions"),
175
184
  presetId: z
176
- .enum([
177
- "blank",
178
- "project-note",
179
- "research-note",
180
- "decision-note",
181
- "prompt-pattern",
182
- "source-summary",
183
- ])
185
+ .enum(notePresetIds)
184
186
  .optional()
185
187
  .describe("Optional note preset scaffold"),
186
188
  overwrite: z
@@ -500,6 +502,16 @@ export const queryInputSchema = z.object({
500
502
  tagsAny: z.array(z.string()).optional().describe("Require ANY of these tags"),
501
503
  });
502
504
 
505
+ export const queryDiagnoseInputSchema = queryInputSchema.extend({
506
+ target: z
507
+ .string()
508
+ .trim()
509
+ .min(1, "Target reference cannot be empty")
510
+ .describe(
511
+ "Target document reference to diagnose (gno URI, docid, or collection/path)"
512
+ ),
513
+ });
514
+
503
515
  const getInputSchema = z.object({
504
516
  ref: z
505
517
  .string()
@@ -716,6 +728,67 @@ const graphPathInputSchema = graphInputSchema.extend({
716
728
  .describe("Maximum relationship hops to search"),
717
729
  });
718
730
 
731
+ export const graphQueryInputSchema = z.object({
732
+ ref: z
733
+ .string()
734
+ .trim()
735
+ .min(1, "Reference cannot be empty")
736
+ .describe(
737
+ "Root document reference for typed-edge traversal: gno URI, docid, or collection/path"
738
+ ),
739
+ direction: z
740
+ .enum(["both", "out", "in"])
741
+ .default("both")
742
+ .describe("Which typed edges to traverse"),
743
+ edgeType: z
744
+ .string()
745
+ .trim()
746
+ .min(1)
747
+ .optional()
748
+ .describe("Semantic edge type filter, e.g. mentions or works_at"),
749
+ relation: z
750
+ .string()
751
+ .trim()
752
+ .min(1)
753
+ .optional()
754
+ .describe("Alias for edgeType; must match edgeType when both are set"),
755
+ maxDepth: z
756
+ .number()
757
+ .int()
758
+ .min(1)
759
+ .max(6)
760
+ .optional()
761
+ .describe("Maximum typed-edge hops to traverse"),
762
+ depth: z
763
+ .number()
764
+ .int()
765
+ .min(1)
766
+ .max(6)
767
+ .optional()
768
+ .describe("Alias for maxDepth"),
769
+ maxNodes: z
770
+ .number()
771
+ .int()
772
+ .min(1)
773
+ .max(1000)
774
+ .default(100)
775
+ .describe("Returned node cap"),
776
+ frontierLimit: z
777
+ .number()
778
+ .int()
779
+ .min(1)
780
+ .max(1000)
781
+ .default(100)
782
+ .describe("Per-depth frontier cap"),
783
+ visitedLimit: z
784
+ .number()
785
+ .int()
786
+ .min(1)
787
+ .max(5000)
788
+ .default(500)
789
+ .describe("SQL traversal visited-row cap"),
790
+ });
791
+
719
792
  // ─────────────────────────────────────────────────────────────────────────────
720
793
  // Tool Result Type
721
794
  // ─────────────────────────────────────────────────────────────────────────────
@@ -847,6 +920,13 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
847
920
  (args) => handleQuery(args, ctx)
848
921
  );
849
922
 
923
+ server.tool(
924
+ "gno_query_diagnose",
925
+ MCP_TOOL_DESCRIPTIONS.queryDiagnose,
926
+ queryDiagnoseInputSchema.shape,
927
+ (args) => handleQueryDiagnose(args, ctx)
928
+ );
929
+
850
930
  server.tool(
851
931
  "gno_get",
852
932
  MCP_TOOL_DESCRIPTIONS.get,
@@ -903,6 +983,13 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
903
983
  (args) => handleGraph(args, ctx)
904
984
  );
905
985
 
986
+ server.tool(
987
+ "gno_graph_query",
988
+ "Run bounded traversal over typed doc_edges from one root document. Use for explicit relationship questions like 'what does Alice work_at within 2 hops?' or to inspect typed graph hints; returns schemaVersion, root, nodes, edges, caps, and truncation.",
989
+ graphQueryInputSchema.shape,
990
+ (args) => handleGraphQuery(args, ctx)
991
+ );
992
+
906
993
  server.tool(
907
994
  "gno_graph_neighbors",
908
995
  "Find graph neighbors around a document/node. Use for relationship questions, missed obvious related docs, or unfamiliar corpus navigation after gno_query identifies a seed; returns incoming/outgoing wiki, markdown, and optional similarity edges. Follow with gno_get for targeted reads.",
@@ -10,14 +10,18 @@ import type {
10
10
  BacklinkRow,
11
11
  DocLinkRow,
12
12
  DocumentRow,
13
+ GraphQueryDirection,
14
+ GraphQueryResult,
13
15
  GraphLink,
14
16
  GraphNode,
15
17
  GraphResult,
16
18
  } from "../../store/types";
17
19
  import type { ToolContext } from "../server";
18
20
 
19
- import { parseRef } from "../../cli/commands/ref-parser";
21
+ import { normalizeContentTypes } from "../../config";
20
22
  import { MCP_ERRORS } from "../../core/errors";
23
+ import { diagnoseGraphQuery } from "../../core/graph-query";
24
+ import { parseRef } from "../../core/ref-parser";
21
25
  import { normalizeCollectionName } from "../../core/validation";
22
26
  import { getActivePreset } from "../../llm/registry";
23
27
  import { createVectorIndexPort } from "../../store/vector";
@@ -648,6 +652,18 @@ interface GraphPathInput extends GraphInput {
648
652
  maxDepth?: number;
649
653
  }
650
654
 
655
+ interface GraphQueryInput {
656
+ ref: string;
657
+ direction?: GraphQueryDirection;
658
+ edgeType?: string;
659
+ relation?: string;
660
+ maxDepth?: number;
661
+ depth?: number;
662
+ maxNodes?: number;
663
+ frontierLimit?: number;
664
+ visitedLimit?: number;
665
+ }
666
+
651
667
  interface GraphNeighbor {
652
668
  node: GraphNode;
653
669
  direction: "out" | "in";
@@ -860,6 +876,72 @@ function formatGraphNeighborsResult(data: GraphNeighborsResult): string {
860
876
  return lines.join("\n");
861
877
  }
862
878
 
879
+ function formatGraphQueryResult(data: GraphQueryResult): string {
880
+ const lines = [
881
+ `Typed graph query from ${data.root.uri}: ${data.nodes.length} nodes, ${data.edges.length} edges`,
882
+ `Direction: ${data.meta.direction}, depth: ${data.meta.maxDepth}`,
883
+ ];
884
+ if (data.meta.edgeType) {
885
+ lines.push(`Edge type: ${data.meta.edgeType}`);
886
+ }
887
+ if (data.meta.truncated) {
888
+ lines.push("Traversal truncated by configured caps");
889
+ }
890
+ for (const edge of data.edges.slice(0, 20)) {
891
+ lines.push(
892
+ ` ${edge.source} -> ${edge.target} (${edge.edgeType}, ${edge.confidence}, ${edge.edgeSource})`
893
+ );
894
+ }
895
+ return lines.join("\n");
896
+ }
897
+
898
+ export function handleGraphQuery(
899
+ args: GraphQueryInput,
900
+ ctx: ToolContext
901
+ ): Promise<ToolResult> {
902
+ return runTool(
903
+ ctx,
904
+ "gno_graph_query",
905
+ async () => {
906
+ const edgeTypeValue = args.edgeType?.trim();
907
+ const relationValue = args.relation?.trim();
908
+ if (edgeTypeValue && relationValue && edgeTypeValue !== relationValue) {
909
+ throw new Error(
910
+ "VALIDATION: edgeType and relation are aliases and must match when both are provided"
911
+ );
912
+ }
913
+ if (
914
+ args.maxDepth !== undefined &&
915
+ args.depth !== undefined &&
916
+ args.maxDepth !== args.depth
917
+ ) {
918
+ throw new Error(
919
+ "VALIDATION: maxDepth and depth are aliases and must match when both are provided"
920
+ );
921
+ }
922
+
923
+ const result = await diagnoseGraphQuery(ctx.store, args.ref, {
924
+ direction: args.direction ?? "both",
925
+ edgeType: edgeTypeValue || relationValue || undefined,
926
+ maxDepth: args.maxDepth ?? args.depth,
927
+ maxNodes: args.maxNodes,
928
+ frontierLimit: args.frontierLimit,
929
+ visitedLimit: args.visitedLimit,
930
+ contentTypeRules: normalizeContentTypes(ctx.config.contentTypes ?? [])
931
+ .rules,
932
+ });
933
+ if (!result.success) {
934
+ throw new Error(
935
+ `${result.isValidation ? "VALIDATION" : "RUNTIME"}: ${result.error}`
936
+ );
937
+ }
938
+
939
+ return result.data;
940
+ },
941
+ formatGraphQueryResult
942
+ );
943
+ }
944
+
863
945
  export function handleGraphNeighbors(
864
946
  args: GraphNeighborsInput,
865
947
  ctx: ToolContext
@@ -10,7 +10,7 @@ import type { DocumentRow, StorePort } from "../../store/types";
10
10
  import type { ToolContext } from "../server";
11
11
 
12
12
  import { decorateUriForIndex, parseUri } from "../../app/constants";
13
- import { parseRef } from "../../cli/commands/ref-parser";
13
+ import { parseRef } from "../../core/ref-parser";
14
14
  import { runTool, type ToolResult } from "./index";
15
15
 
16
16
  interface MultiGetInput {
@@ -11,6 +11,7 @@ import type {
11
11
  GenerationPort,
12
12
  RerankPort,
13
13
  } from "../../llm/types";
14
+ import type { QueryDiagnoseResult } from "../../pipeline/diagnose";
14
15
  import type {
15
16
  QueryModeInput,
16
17
  SearchResult,
@@ -20,11 +21,16 @@ import type { ToolContext } from "../server";
20
21
 
21
22
  import { decorateUriForIndex, parseUri } from "../../app/constants";
22
23
  import { createNonTtyProgressRenderer } from "../../cli/progress";
24
+ import {
25
+ fingerprintContentTypeRules,
26
+ normalizeContentTypes,
27
+ } from "../../config";
23
28
  import { resolveDepthPolicy } from "../../core/depth-policy";
24
29
  import { normalizeStructuredQueryInput } from "../../core/structured-query";
25
30
  import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
26
31
  import { resolveDownloadPolicy } from "../../llm/policy";
27
32
  import { getActivePreset, resolveModelUri } from "../../llm/registry";
33
+ import { diagnoseQueryTarget } from "../../pipeline/diagnose";
28
34
  import { type HybridSearchDeps, searchHybrid } from "../../pipeline/hybrid";
29
35
  import {
30
36
  createVectorIndexPort,
@@ -56,6 +62,10 @@ interface QueryInput {
56
62
  tagsAny?: string[];
57
63
  }
58
64
 
65
+ interface QueryDiagnoseInput extends QueryInput {
66
+ target: string;
67
+ }
68
+
59
69
  /**
60
70
  * Enrich results with absPath derived from each result's URI.
61
71
  */
@@ -128,6 +138,29 @@ function formatSearchResults(data: SearchResults): string {
128
138
  return lines.join("\n");
129
139
  }
130
140
 
141
+ function formatQueryDiagnoseResult(data: QueryDiagnoseResult): string {
142
+ if (data.target.status !== "diagnosed") {
143
+ return `Target ${data.target.ref}: ${data.target.status}`;
144
+ }
145
+
146
+ const lines = [
147
+ `Query diagnose for ${data.target.uri ?? data.target.ref}`,
148
+ `Mode: ${data.meta.mode}, results: ${data.meta.totalResults}`,
149
+ "",
150
+ ];
151
+
152
+ for (const stage of data.stages) {
153
+ const rank = stage.rank === null ? "-" : `#${stage.rank}`;
154
+ const score = stage.score === null ? "-" : Number(stage.score).toFixed(3);
155
+ const reason = stage.dropReason ? `, ${stage.dropReason}` : "";
156
+ lines.push(
157
+ `${stage.id}: ${stage.status}, present=${stage.present}, rank=${rank}, score=${score}${reason}`
158
+ );
159
+ }
160
+
161
+ return lines.join("\n");
162
+ }
163
+
131
164
  /**
132
165
  * Handle gno_query tool call.
133
166
  */
@@ -311,3 +344,177 @@ export function handleQuery(
311
344
  formatSearchResults
312
345
  );
313
346
  }
347
+
348
+ /**
349
+ * Handle gno_query_diagnose tool call.
350
+ */
351
+ export function handleQueryDiagnose(
352
+ args: QueryDiagnoseInput,
353
+ ctx: ToolContext
354
+ ): Promise<ToolResult> {
355
+ return runTool(
356
+ ctx,
357
+ "gno_query_diagnose",
358
+ // oxlint-disable-next-line max-lines-per-function -- mirrors query setup for diagnostic trace ports
359
+ async () => {
360
+ let collection: string | undefined;
361
+ if (args.collection) {
362
+ const canonical = ctx.collections.find(
363
+ (c) => c.name.toLowerCase() === args.collection?.toLowerCase()
364
+ );
365
+ if (!canonical) {
366
+ throw new Error(`Collection not found: ${args.collection}`);
367
+ }
368
+ collection = canonical.name;
369
+ }
370
+
371
+ const normalizedInput = normalizeStructuredQueryInput(
372
+ args.query,
373
+ args.queryModes ?? []
374
+ );
375
+ if (!normalizedInput.ok) {
376
+ throw new Error(normalizedInput.error.message);
377
+ }
378
+ const queryText = normalizedInput.value.query;
379
+ const queryModes =
380
+ normalizedInput.value.queryModes.length > 0
381
+ ? normalizedInput.value.queryModes
382
+ : undefined;
383
+
384
+ const preset = getActivePreset(ctx.config);
385
+ const llm = new LlmAdapter(ctx.config);
386
+ const policy = resolveDownloadPolicy(process.env, {});
387
+ const downloadProgress = createNonTtyProgressRenderer();
388
+
389
+ let embedPort: EmbeddingPort | null = null;
390
+ let expandPort: GenerationPort | null = null;
391
+ let rerankPort: RerankPort | null = null;
392
+ let vectorIndex: VectorIndexPort | null = null;
393
+ const embedUri = resolveModelUri(
394
+ ctx.config,
395
+ "embed",
396
+ undefined,
397
+ collection
398
+ );
399
+
400
+ try {
401
+ const hasStructuredModes = Boolean(queryModes?.length);
402
+ const depthPolicy = resolveDepthPolicy({
403
+ presetId: preset.id,
404
+ fast: args.fast,
405
+ thorough: args.thorough,
406
+ expand: args.expand,
407
+ rerank: args.rerank,
408
+ candidateLimit: args.candidateLimit,
409
+ hasStructuredModes,
410
+ });
411
+ const { noExpand, noRerank } = depthPolicy;
412
+
413
+ if (!args.fast) {
414
+ const embedResult = await llm.createEmbeddingPort(embedUri, {
415
+ policy,
416
+ onProgress: (progress) => downloadProgress("embed", progress),
417
+ });
418
+ if (embedResult.ok) {
419
+ embedPort = embedResult.value;
420
+ }
421
+ }
422
+
423
+ if (!noExpand && !hasStructuredModes) {
424
+ const genResult = await llm.createExpansionPort(
425
+ resolveModelUri(ctx.config, "expand", undefined, collection),
426
+ {
427
+ policy,
428
+ onProgress: (progress) => downloadProgress("expand", progress),
429
+ }
430
+ );
431
+ if (genResult.ok) {
432
+ expandPort = genResult.value;
433
+ }
434
+ }
435
+
436
+ if (!noRerank) {
437
+ const rerankResult = await llm.createRerankPort(
438
+ resolveModelUri(ctx.config, "rerank", undefined, collection),
439
+ {
440
+ policy,
441
+ onProgress: (progress) => downloadProgress("rerank", progress),
442
+ }
443
+ );
444
+ if (rerankResult.ok) {
445
+ rerankPort = rerankResult.value;
446
+ }
447
+ }
448
+
449
+ if (embedPort) {
450
+ const embedInitResult = await embedPort.init();
451
+ if (embedInitResult.ok) {
452
+ const dimensions = embedPort.dimensions();
453
+ const db = ctx.store.getRawDb();
454
+ const vectorResult = await createVectorIndexPort(db, {
455
+ model: embedUri,
456
+ dimensions,
457
+ });
458
+ if (vectorResult.ok) {
459
+ vectorIndex = vectorResult.value;
460
+ }
461
+ }
462
+ }
463
+
464
+ const deps: HybridSearchDeps = {
465
+ store: ctx.store,
466
+ config: ctx.config,
467
+ vectorIndex,
468
+ embedPort,
469
+ expandPort,
470
+ rerankPort,
471
+ };
472
+ const contentTypeRules = normalizeContentTypes(
473
+ ctx.config.contentTypes ?? []
474
+ ).rules;
475
+
476
+ const result = await diagnoseQueryTarget(deps, queryText, {
477
+ target: args.target,
478
+ limit: args.limit ?? 5,
479
+ minScore: args.minScore,
480
+ collection,
481
+ queryLanguageHint: args.lang,
482
+ intent: args.intent,
483
+ candidateLimit: depthPolicy.candidateLimit,
484
+ exclude: args.exclude,
485
+ since: args.since,
486
+ until: args.until,
487
+ categories: args.categories,
488
+ author: args.author,
489
+ noExpand,
490
+ noRerank,
491
+ graph: args.graph === true,
492
+ noGraph: args.noGraph || args.fast,
493
+ queryModes,
494
+ tagsAll: normalizeTagFilters(args.tagsAll),
495
+ tagsAny: normalizeTagFilters(args.tagsAny),
496
+ contentTypeRules,
497
+ contentTypeRulesFingerprint:
498
+ fingerprintContentTypeRules(contentTypeRules),
499
+ });
500
+
501
+ if (!result.ok) {
502
+ throw new Error(result.error.message);
503
+ }
504
+
505
+ return result.value;
506
+ } finally {
507
+ if (embedPort) {
508
+ await embedPort.dispose();
509
+ }
510
+ if (expandPort) {
511
+ await expandPort.dispose();
512
+ }
513
+ if (rerankPort) {
514
+ await rerankPort.dispose();
515
+ }
516
+ }
517
+ },
518
+ formatQueryDiagnoseResult
519
+ );
520
+ }
@@ -12,7 +12,7 @@ import { MCP_ERRORS } from "../../core/errors";
12
12
  import { acquireWriteLock, type WriteLockHandle } from "../../core/file-lock";
13
13
  import { JobError } from "../../core/job-manager";
14
14
  import { normalizeCollectionName } from "../../core/validation";
15
- import { defaultSyncService } from "../../ingestion";
15
+ import { defaultSyncService, withContentTypeRules } from "../../ingestion";
16
16
  import { runTool, type ToolResult } from "./index";
17
17
 
18
18
  interface SyncInput {
@@ -101,10 +101,13 @@ export function handleSync(
101
101
  ? [collection.name]
102
102
  : ctx.collections.map((entry) => entry.name);
103
103
 
104
- const options = {
105
- gitPull: args.gitPull ?? false,
106
- runUpdateCmd: args.runUpdateCmd ?? false,
107
- };
104
+ const options = withContentTypeRules(
105
+ {
106
+ gitPull: args.gitPull ?? false,
107
+ runUpdateCmd: args.runUpdateCmd ?? false,
108
+ },
109
+ ctx.config
110
+ );
108
111
 
109
112
  const jobId = await ctx.jobManager.startJobWithLock(
110
113
  "sync",
@@ -133,7 +136,10 @@ export function handleSync(
133
136
  jobId,
134
137
  collections,
135
138
  status: "started",
136
- options,
139
+ options: {
140
+ gitPull: options.gitPull ?? false,
141
+ runUpdateCmd: options.runUpdateCmd ?? false,
142
+ },
137
143
  };
138
144
 
139
145
  return result;
@@ -27,7 +27,7 @@ import {
27
27
  planMoveRefactor,
28
28
  planRenameRefactor,
29
29
  } from "../../core/file-refactors";
30
- import { defaultSyncService } from "../../ingestion";
30
+ import { defaultSyncService, withContentTypeRules } from "../../ingestion";
31
31
  import { runTool, type ToolResult } from "./index";
32
32
 
33
33
  interface CreateFolderInput {
@@ -210,9 +210,11 @@ export function handleRenameNote(
210
210
  const currentPath = join(collection.path, doc.relPath);
211
211
  const nextPath = join(collection.path, plan.nextRelPath);
212
212
  await renameFilePath(currentPath, nextPath);
213
- await defaultSyncService.syncCollection(collection, ctx.store, {
214
- runUpdateCmd: false,
215
- });
213
+ await defaultSyncService.syncCollection(
214
+ collection,
215
+ ctx.store,
216
+ withContentTypeRules({ runUpdateCmd: false }, ctx.config)
217
+ );
216
218
  return {
217
219
  uri: plan.nextUri,
218
220
  relPath: plan.nextRelPath,
@@ -253,9 +255,11 @@ export function handleMoveNote(
253
255
  const nextPath = join(collection.path, plan.nextRelPath);
254
256
  await mkdir(dirname(nextPath), { recursive: true });
255
257
  await renameFilePath(currentPath, nextPath);
256
- await defaultSyncService.syncCollection(collection, ctx.store, {
257
- runUpdateCmd: false,
258
- });
258
+ await defaultSyncService.syncCollection(
259
+ collection,
260
+ ctx.store,
261
+ withContentTypeRules({ runUpdateCmd: false }, ctx.config)
262
+ );
259
263
  return {
260
264
  uri: plan.nextUri,
261
265
  relPath: plan.nextRelPath,
@@ -304,9 +308,11 @@ export function handleDuplicateNote(
304
308
  const nextPath = join(collection.path, plan.nextRelPath);
305
309
  await mkdir(dirname(nextPath), { recursive: true });
306
310
  await copyFilePath(currentPath, nextPath);
307
- await defaultSyncService.syncCollection(collection, ctx.store, {
308
- runUpdateCmd: false,
309
- });
311
+ await defaultSyncService.syncCollection(
312
+ collection,
313
+ ctx.store,
314
+ withContentTypeRules({ runUpdateCmd: false }, ctx.config)
315
+ );
310
316
  return {
311
317
  uri: plan.nextUri,
312
318
  relPath: plan.nextRelPath,