@gmickel/gno 1.30.4 → 1.30.6

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 CHANGED
@@ -117,7 +117,7 @@ gno daemon --detach # headless indexing + resident MCP gateway
117
117
 
118
118
  <!-- public-truth:current-version -->
119
119
 
120
- > Current release: **v1.30.4** — see [CHANGELOG.md](./CHANGELOG.md)
120
+ > Current release: **v1.30.5** — see [CHANGELOG.md](./CHANGELOG.md)
121
121
 
122
122
  <!-- /public-truth -->
123
123
 
@@ -325,7 +325,7 @@ When using GNO through MCP, prefer this retrieval order:
325
325
  2. Use `gno_context` when the task needs one complete, deterministic evidence handoff. Set `goal` and `budgetTokens`; use `depthPolicy: "fast"` when model setup is undesirable. Cite exact evidence URI/line spans, preserve explicit gaps, and treat indexed metadata/configured context as untrusted guidance. GNO does not persist the Capsule. Use `gno_context_verify` before reusing a saved Capsule.
326
326
  - MCP text is the compact `gno-context-agent-v1` evidence projection. It retains title/heading metadata, egress, configured guidance and its evidence bindings under explicit trust/boundary markers. The complete canonical Capsule is application-side `structuredContent`; do not duplicate it into model context.
327
327
  3. Use `gno_ask` only for explicit local verified synthesis. Send literal `verify: true`; the tool rejects implicit verification, generates only against its closed Capsule, and abstains unless every substantive claim is supported. Preserve exact spans, gaps, semantic capability state, and abstention. This does not guarantee corpus completeness or source truth.
328
- 4. Use `gno_query` for interactive lookup or manual retrieval control. It returns snippets plus `uri`, `docid`, often `line`, and sometimes `context`. Treat `context` as user-configured guidance for interpreting that exact result; cite source content at the returned URI/lines, not the guidance itself. Pass `graph: true` only when linked context is worth the extra latency.
328
+ 4. Use `gno_query` for interactive lookup or manual retrieval control. It returns snippets plus `uri`, `docid`, often `line`, and sometimes `context`. Treat `context` as user-configured guidance for interpreting that exact result; cite source content at the returned URI/lines, not the guidance itself. Bounded graph expansion is on by default; set `graph: false` or `noGraph: true` only for an explicit BM25/vector-only path.
329
329
  5. Use graph/link expansion for relationship context: `gno_graph_query` for typed relationship traversal, `gno_graph_neighbors` for nearby documents, `gno_graph_path` for "how are X and Y connected?", `gno_links`/`gno_backlinks` for one-document link expansion, and `gno_similar` for semantic neighbors. Prefer explicit or typed edges over inferred, ambiguous, or similarity edges when confidence matters.
330
330
  6. Use `gno_query_diagnose` when a known target document should have appeared but did not; it reports BM25/vector/fusion/graph/rerank stage presence and filter state.
331
331
  7. Use `gno_get` with `fromLine`/`lineCount` for targeted reads, or `gno_multi_get` to batch top refs.
@@ -233,19 +233,19 @@ gno query <query> [options]
233
233
 
234
234
  | Flag | Time | Description |
235
235
  | ------------ | ----- | ------------------------------ |
236
- | `--fast` | ~0.7s | Skip expansion and rerank |
237
- | (default) | ~2-3s | Skip expansion, with reranking |
238
- | `--thorough` | ~5-8s | Full pipeline with expansion |
236
+ | `--fast` | ~0.7s | Skip expansion, graph, rerank |
237
+ | (default) | ~2-3s | Balanced, graph + reranking |
238
+ | `--thorough` | ~5-8s | Wider expansion + graph/rerank |
239
239
 
240
240
  Additional options:
241
241
 
242
- | Option | Description |
243
- | ------------- | --------------------------------- |
244
- | `--no-expand` | Disable query expansion |
245
- | `--no-rerank` | Disable reranking |
246
- | `--graph` | Enable graph-neighbor candidates |
247
- | `--no-graph` | Compatibility no-op by default |
248
- | `--explain` | Print retrieval details to stderr |
242
+ | Option | Description |
243
+ | ------------- | ------------------------------------------ |
244
+ | `--no-expand` | Disable query expansion |
245
+ | `--no-rerank` | Disable reranking |
246
+ | `--graph` | Explicitly enable default graph candidates |
247
+ | `--no-graph` | Disable graph-neighbor candidates |
248
+ | `--explain` | Print retrieval details to stderr |
249
249
 
250
250
  ### gno ask
251
251
 
@@ -101,8 +101,9 @@ exact evidence handoff. Use `gno_ask` only when a local closed-evidence answer
101
101
  is specifically useful, and pass the literal boolean `verify: true`; it
102
102
  abstains unless every substantive claim is supported. This is a support
103
103
  classification against the retained Capsule, not a guarantee that the corpus
104
- is complete or its sources are true. Pass `graph: true` only when linked context
105
- is worth the extra latency. Check `gno_status` first when freshness or
104
+ is complete or its sources are true. Bounded graph expansion is on by default;
105
+ set `graph: false` or `noGraph: true` only for an explicit BM25/vector-only path.
106
+ Check `gno_status` first when freshness or
106
107
  embeddings may be stale. Use `gno_query_diagnose` when a known target document
107
108
  should have appeared but did not.
108
109
 
@@ -0,0 +1 @@
1
+ 20b500022a0f94f573aab83b4c2dd1626263f66141ffb3630fd7fd82cbc4254d gno-browser-clipper-v1.30.6.zip
@@ -21,5 +21,5 @@
21
21
  "content_security_policy": {
22
22
  "extension_pages": "script-src 'self'; object-src 'none'; connect-src http://127.0.0.1:*"
23
23
  },
24
- "version": "1.30.4"
24
+ "version": "1.30.6"
25
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "1.30.4",
3
+ "version": "1.30.6",
4
4
  "description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
5
5
  "keywords": [
6
6
  "embeddings",
package/spec/cli.md CHANGED
@@ -1173,8 +1173,8 @@ gno query diagnose <query> --target <doc> [-n <num>] [--min-score <num>] [-c <co
1173
1173
  |--------|------|-------------|
1174
1174
  | `--no-expand` | boolean | Disable query expansion |
1175
1175
  | `--no-rerank` | boolean | Disable cross-encoder reranking |
1176
- | `--graph` | boolean | Enable bounded one-hop graph neighbor expansion |
1177
- | `--no-graph` | boolean | Compatibility no-op; graph expansion is off unless `--graph` is passed |
1176
+ | `--graph` | boolean | Explicitly enable the default bounded one-hop graph neighbor expansion |
1177
+ | `--no-graph` | boolean | Disable graph neighbor expansion |
1178
1178
  | `--intent` | string | Disambiguating context for ambiguous queries; steers expansion, rerank chunk/snippet choice, and disables strong-signal bypass without being searched directly |
1179
1179
  | `--exclude` | string | Hard-prune docs containing any comma-separated term in title/path/body |
1180
1180
  | `-C, --candidate-limit` | integer | Max candidates passed to reranking (default 20) |
@@ -1188,11 +1188,11 @@ gno query diagnose <query> --target <doc> [-n <num>] [--min-score <num>] [-c <co
1188
1188
  **Compatibility / Migration:**
1189
1189
 
1190
1190
  - Legacy query invocations remain valid (`gno query "<text>"`, `--fast`, `--thorough`, `--no-expand`, `--no-rerank`).
1191
- - `--fast` skips query expansion and reranking. Graph expansion is already off unless `--graph` is passed.
1191
+ - `--fast` skips query expansion, graph expansion, and reranking.
1192
1192
  - `--intent` is orthogonal to `--query-mode`: intent steers scoring/prompting, while query modes inject caller-provided retrieval expansions.
1193
1193
  - `--query-mode` is optional and additive to the command surface.
1194
1194
  - If one or more `--query-mode` entries are provided, generated expansion is bypassed and provided entries are used as retrieval intents.
1195
- - By default, `gno query` does not expand through the document graph. Use `--graph` to add a capped one-hop graph-neighbor candidate set after BM25/vector retrieval. Explicit links are weighted above inferred, ambiguous, or similarity edges.
1195
+ - By default, balanced and thorough `gno query` retrieval adds a capped one-hop wiki/markdown-neighbor candidate set after BM25/vector retrieval. Use `--no-graph` to disable it. Query-time expansion resolves only links touching the top seeds; semantic similarity remains the vector stage's responsibility. Explicit links are weighted above inferred or ambiguous link matches.
1196
1196
 
1197
1197
  **Diagnose Output:**
1198
1198
 
@@ -1285,7 +1285,7 @@ Human-friendly query with citations-first output and optional grounded answer.
1285
1285
  **Synopsis:**
1286
1286
 
1287
1287
  ```bash
1288
- gno ask <query> [-n <num>] [-c <collection>] [--lang <bcp47>] [--since <date>] [--until <date>] [--category <values>] [--author <text>] [--intent <text>] [--exclude <values>] [--query-mode <mode:text>]... [-C <num>] [--answer|--verify] [--no-answer] [--max-answer-tokens <n>] [--context-budget-tokens <n>] [--context-budget-bytes <n>] [--min-score <score>] [--graph] [--no-expand] [--no-rerank] [--explain] [--show-sources] [--json|--md]
1288
+ gno ask <query> [-n <num>] [-c <collection>] [--lang <bcp47>] [--since <date>] [--until <date>] [--category <values>] [--author <text>] [--intent <text>] [--exclude <values>] [--query-mode <mode:text>]... [-C <num>] [--answer|--verify] [--no-answer] [--max-answer-tokens <n>] [--context-budget-tokens <n>] [--context-budget-bytes <n>] [--min-score <score>] [--graph|--no-graph] [--no-expand] [--no-rerank] [--explain] [--show-sources] [--json|--md]
1289
1289
  ```
1290
1290
 
1291
1291
  **Options:**
@@ -1299,7 +1299,8 @@ gno ask <query> [-n <num>] [-c <collection>] [--lang <bcp47>] [--since <date>] [
1299
1299
  | `--context-budget-tokens` | integer | 12000 | Global token budget for verified Context evidence |
1300
1300
  | `--context-budget-bytes` | integer | none | Optional global byte budget for verified Context evidence |
1301
1301
  | `--min-score` | number | none | Minimum retrieval score from 0 through 1 |
1302
- | `--graph` | boolean | false | Include bounded graph expansion in verified Context retrieval |
1302
+ | `--graph` | boolean | true | Include bounded graph expansion in verified Context retrieval |
1303
+ | `--no-graph` | boolean | false | Disable graph expansion |
1303
1304
  | `--since` | string | none | Modified-at lower bound (ISO date/time or relative token) |
1304
1305
  | `--until` | string | none | Modified-at upper bound (ISO date/time or relative token) |
1305
1306
  | `--category` | string | none | Filter to docs with matching category/content type (comma-separated) |
@@ -1666,7 +1667,7 @@ written to stderr.
1666
1667
  **Synopsis:**
1667
1668
 
1668
1669
  ```bash
1669
- gno context build "<goal>" --budget <tokens> [--collection <name>] [--project-root <path>]... [--no-project-affinity] [--fast|--thorough] [--json|--md] [--output <file>]
1670
+ gno context build "<goal>" --budget <tokens> [--collection <name>] [--project-root <path>]... [--no-project-affinity] [--graph|--no-graph] [--fast|--thorough] [--json|--md] [--output <file>]
1670
1671
  ```
1671
1672
 
1672
1673
  `--budget` is the global token ceiling. `--bytes` optionally sets a separate
@@ -1683,6 +1684,8 @@ Project affinity defaults to the trusted process cwd/repository. Repeatable
1683
1684
  `--project-root` values replace that default, are normalized/deduplicated, and
1684
1685
  are capped at 16. `--no-project-affinity` disables the soft signal and cannot
1685
1686
  be combined with explicit roots.
1687
+ Balanced and thorough Context retrieval request bounded graph expansion by
1688
+ default. `--no-graph` disables it; `--fast` also keeps the graph stage off.
1686
1689
 
1687
1690
  JSON is the canonical V1 payload. Markdown is a readable projection of that
1688
1691
  same payload and hard-delimits each untrusted evidence passage. Passage,
package/spec/mcp.md CHANGED
@@ -158,6 +158,7 @@ Collection names are case-insensitive on input and normalized to lowercase in re
158
158
  for one goal. It compiles exact source spans, coverage gaps, omissions, and
159
159
  verification fingerprints in one call.
160
160
  - Prefer `gno_query` for normal questions. It is the default hybrid path and returns `uri`, `docid`, snippets, and `line` anchors for follow-up reads.
161
+ - Bounded graph expansion is part of normal `gno_query` retrieval. Set `graph: false`, `noGraph: true`, or `fast: true` only when the caller explicitly wants to skip it.
161
162
  - Use `gno_search` for exact phrases, filenames, identifiers, error messages, and known symbols.
162
163
  - Use `gno_vsearch` for semantic similarity when wording differs and embeddings are current.
163
164
  - Use `intent` to disambiguate short or overloaded terms without changing the searched text.
@@ -675,13 +676,13 @@ Hybrid search combining BM25 and vector retrieval with optional expansion and re
675
676
  },
676
677
  "noGraph": {
677
678
  "type": "boolean",
678
- "description": "Compatibility no-op unless graph is also true",
679
+ "description": "Disable graph neighbor expansion",
679
680
  "default": false
680
681
  },
681
682
  "graph": {
682
683
  "type": "boolean",
683
684
  "description": "Enable bounded one-hop graph neighbor expansion",
684
- "default": false
685
+ "default": true
685
686
  },
686
687
  "fast": {
687
688
  "type": "boolean",
@@ -734,7 +735,7 @@ Compatibility / migration notes:
734
735
  - `intent` is orthogonal to `queryModes`: intent steers scoring/prompting, while query modes inject caller-provided retrieval expansions.
735
736
  - `candidateLimit` tunes rerank cost without changing retrieval contracts.
736
737
  - `exclude` hard-prunes matching docs after retrieval using title/path/body text.
737
- - `gno_query` does not use graph expansion by default. Set `graph: true` to add capped one-hop graph neighbors after initial retrieval. Explicit links receive stronger treatment than inferred, ambiguous, or similarity edges.
738
+ - `gno_query` uses graph expansion by default. Set `graph: false` or `noGraph: true` to disable capped one-hop wiki/markdown neighbors. Query-time expansion resolves only links touching the top seeds; semantic similarity remains in vector retrieval. Explicit links receive stronger treatment than inferred or ambiguous link matches.
738
739
  - `queryModes` is optional; use it only when clients need explicit retrieval intent control.
739
740
  - When `queryModes` is present, generated expansion is skipped and provided entries are used directly.
740
741
 
@@ -373,7 +373,7 @@ export const normalizeContextBuildInput = (
373
373
  minScore: input.minScore ?? null,
374
374
  since: temporalRange.since,
375
375
  until: temporalRange.until,
376
- graph: input.graph ?? false,
376
+ graph: depthPolicy === "fast" ? false : (input.graph ?? true),
377
377
  noRerank: input.noRerank ?? false,
378
378
  limit,
379
379
  candidateLimit,
@@ -191,7 +191,7 @@ export const buildVerifiedAsk = async (
191
191
  minScore: options.minScore,
192
192
  since: options.since,
193
193
  until: options.until,
194
- graph: Boolean(options.graph && !options.noGraph),
194
+ graph: options.graph !== false && options.noGraph !== true,
195
195
  noRerank: options.noRerank,
196
196
  limit: options.limit ?? DEFAULT_VERIFIED_ASK_LIMIT,
197
197
  candidateLimit: options.candidateLimit,
@@ -856,15 +856,15 @@ function wireSearchCommands(program: Command): void {
856
856
  .option("--no-project-affinity", "disable project-aware ranking")
857
857
  .option("--full", "include full content")
858
858
  .option("--line-numbers", "include line numbers in output")
859
- .option("--fast", "skip expansion and reranking (fastest, ~0.7s)")
859
+ .option("--fast", "skip query expansion, graph expansion, and reranking")
860
860
  .option(
861
861
  "--thorough",
862
862
  "use expansion with a wider rerank pool (slowest, best recall)"
863
863
  )
864
864
  .option("--no-expand", "disable query expansion")
865
865
  .option("--no-rerank", "disable reranking")
866
- .option("--graph", "enable graph neighbor expansion")
867
- .option("--no-graph", "compatibility no-op; graph is off by default")
866
+ .option("--graph", "enable graph neighbor expansion (default)")
867
+ .option("--no-graph", "disable graph neighbor expansion")
868
868
  .option("--target <doc>", "diagnose target document ref")
869
869
  .option(
870
870
  "--query-mode <mode:text>",
@@ -991,7 +991,7 @@ function wireSearchCommands(program: Command): void {
991
991
  ...projectAffinity,
992
992
  noExpand: depthPolicy.noExpand,
993
993
  noRerank: depthPolicy.noRerank,
994
- graph: Boolean(cmdOpts.graph),
994
+ graph: cmdOpts.graph !== false,
995
995
  noGraph: Boolean(cmdOpts.fast) || cmdOpts.graph === false,
996
996
  candidateLimit: depthPolicy.candidateLimit,
997
997
  queryModes,
@@ -1029,7 +1029,7 @@ function wireSearchCommands(program: Command): void {
1029
1029
  lineNumbers: Boolean(cmdOpts.lineNumbers),
1030
1030
  noExpand: depthPolicy.noExpand,
1031
1031
  noRerank: depthPolicy.noRerank,
1032
- graph: Boolean(cmdOpts.graph),
1032
+ graph: cmdOpts.graph !== false,
1033
1033
  noGraph: Boolean(cmdOpts.fast) || cmdOpts.graph === false,
1034
1034
  candidateLimit: depthPolicy.candidateLimit,
1035
1035
  queryModes,
@@ -1134,7 +1134,7 @@ function wireSearchCommands(program: Command): void {
1134
1134
  (value: string, previous: string[] = []) => [...previous, value],
1135
1135
  []
1136
1136
  )
1137
- .option("--fast", "skip expansion and reranking (fastest)")
1137
+ .option("--fast", "skip query expansion, graph expansion, and reranking")
1138
1138
  .option(
1139
1139
  "--thorough",
1140
1140
  "use expansion with a wider rerank pool (slowest, best recall)"
@@ -1150,7 +1150,8 @@ function wireSearchCommands(program: Command): void {
1150
1150
  .option("--context-budget-tokens <num>", "verified Context token budget")
1151
1151
  .option("--context-budget-bytes <num>", "verified Context byte budget")
1152
1152
  .option("--min-score <score>", "minimum retrieval score (0-1)")
1153
- .option("--graph", "include bounded graph expansion")
1153
+ .option("--graph", "include bounded graph expansion (default)")
1154
+ .option("--no-graph", "disable graph neighbor expansion")
1154
1155
  .option("--explain", "include retrieval scoring explanation")
1155
1156
  .option(
1156
1157
  "--project-root <path>",
@@ -1255,7 +1256,8 @@ function wireSearchCommands(program: Command): void {
1255
1256
  intent: cmdOpts.intent as string | undefined,
1256
1257
  exclude,
1257
1258
  minScore,
1258
- graph: Boolean(cmdOpts.graph),
1259
+ graph: !cmdOpts.fast && cmdOpts.graph !== false,
1260
+ noGraph: Boolean(cmdOpts.fast) || cmdOpts.graph === false,
1259
1261
  queryModes,
1260
1262
  noExpand: depthPolicy.noExpand,
1261
1263
  noRerank: depthPolicy.noRerank,
@@ -2298,7 +2300,8 @@ function wireManagementCommands(program: Command): void {
2298
2300
  .option("--lang <code>", "language hint (BCP-47)")
2299
2301
  .option("--since <date>", "modified-at lower bound")
2300
2302
  .option("--until <date>", "modified-at upper bound")
2301
- .option("--graph", "enable graph neighbor expansion")
2303
+ .option("--graph", "enable graph neighbor expansion (default)")
2304
+ .option("--no-graph", "disable graph neighbor expansion")
2302
2305
  .option(
2303
2306
  "--project-root <path>",
2304
2307
  "trusted project root (repeatable; replaces cwd affinity)",
@@ -2306,7 +2309,7 @@ function wireManagementCommands(program: Command): void {
2306
2309
  []
2307
2310
  )
2308
2311
  .option("--no-project-affinity", "disable project-aware ranking")
2309
- .option("--fast", "use lexical-first fast retrieval")
2312
+ .option("--fast", "use lexical-first retrieval without graph or rerank")
2310
2313
  .option("--thorough", "use a wider retrieval pool")
2311
2314
  .option("-n, --limit <num>", "maximum retrieved results")
2312
2315
  .option("-C, --candidate-limit <num>", "maximum rerank candidates")
@@ -2366,7 +2369,7 @@ function wireManagementCommands(program: Command): void {
2366
2369
  lang: cmdOpts.lang as string | undefined,
2367
2370
  since: cmdOpts.since as string | undefined,
2368
2371
  until: cmdOpts.until as string | undefined,
2369
- graph: Boolean(cmdOpts.graph),
2372
+ graph: !cmdOpts.fast && cmdOpts.graph !== false,
2370
2373
  limit:
2371
2374
  cmdOpts.limit === undefined
2372
2375
  ? undefined
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Shared confidence classification for resolved wiki/markdown graph edges.
3
+ * Kept in sync with getGraph so query-time seed expansion matches CLI/API graph.
4
+ *
5
+ * @module src/core/graph-edge-confidence
6
+ */
7
+
8
+ import type { GraphEdgeAudit, GraphEdgeConfidence } from "../store/types";
9
+
10
+ export const GRAPH_EDGE_CONFIDENCE_RANK: Record<GraphEdgeConfidence, number> = {
11
+ explicit: 1,
12
+ inferred: 2,
13
+ ambiguous: 3,
14
+ similarity: 4,
15
+ };
16
+
17
+ export function classifyResolvedGraphEdge(
18
+ linkType: "wiki" | "markdown",
19
+ matchRank: number | null,
20
+ matchCount: number | null
21
+ ): { confidence: GraphEdgeConfidence; audit: GraphEdgeAudit } {
22
+ if (linkType === "markdown") {
23
+ return {
24
+ confidence: "explicit",
25
+ audit: { resolution: "exact-path", matchCount: 1 },
26
+ };
27
+ }
28
+
29
+ const count = matchCount ?? 0;
30
+ if (count > 1) {
31
+ return {
32
+ confidence: "ambiguous",
33
+ audit: {
34
+ resolution: "ambiguous-fallback",
35
+ matchCount: count,
36
+ },
37
+ };
38
+ }
39
+
40
+ if (matchRank === 1 || matchRank === 2) {
41
+ return {
42
+ confidence: "explicit",
43
+ audit: { resolution: "exact-title", matchCount: count || 1 },
44
+ };
45
+ }
46
+ if (matchRank === 5 || matchRank === 6) {
47
+ return {
48
+ confidence: "explicit",
49
+ audit: { resolution: "exact-path", matchCount: count || 1 },
50
+ };
51
+ }
52
+
53
+ return {
54
+ confidence: "inferred",
55
+ audit: { resolution: "path-fallback", matchCount: count || 1 },
56
+ };
57
+ }
58
+
59
+ export function mergeGraphEdgeAudit(
60
+ current: {
61
+ confidence: GraphEdgeConfidence;
62
+ audit: GraphEdgeAudit;
63
+ },
64
+ nextConfidence: GraphEdgeConfidence,
65
+ nextAudit: GraphEdgeAudit
66
+ ): void {
67
+ if (
68
+ GRAPH_EDGE_CONFIDENCE_RANK[nextConfidence] <
69
+ GRAPH_EDGE_CONFIDENCE_RANK[current.confidence]
70
+ ) {
71
+ current.confidence = nextConfidence;
72
+ current.audit = {
73
+ ...nextAudit,
74
+ matchCount: Math.max(
75
+ current.audit.matchCount ?? 0,
76
+ nextAudit.matchCount ?? 0
77
+ ),
78
+ };
79
+ return;
80
+ }
81
+ if (
82
+ nextAudit.matchCount !== undefined &&
83
+ (current.audit.matchCount ?? 0) < nextAudit.matchCount
84
+ ) {
85
+ current.audit = {
86
+ ...current.audit,
87
+ matchCount: nextAudit.matchCount,
88
+ };
89
+ }
90
+ }
@@ -110,7 +110,7 @@ export const MCP_TOOL_DESCRIPTIONS = {
110
110
  vsearch:
111
111
  "Vector semantic search. Finds conceptually similar docs with different wording. Structured results preserve optional user-configured context guidance. Best after embeddings are current; use intent to disambiguate short terms. Use gno_query for default hybrid retrieval.",
112
112
  query:
113
- "Hybrid search (BM25 + vector + optional expansion/reranking). Recommended default. Structured results preserve optional user-configured context guidance with source identity. 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.",
113
+ "Hybrid search (BM25 + vector + default bounded graph expansion + optional query expansion/reranking). Recommended default. Structured results preserve optional user-configured context guidance with source identity. Use intent for ambiguous terms, queryModes to combine term/intent/hyde strategies, fast=true or graph=false for quick lookup, thorough=true when recall matters, and candidateLimit to trade latency for coverage.",
114
114
  queryDiagnose:
115
115
  "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.",
116
116
  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.",
@@ -559,7 +559,9 @@ export const queryInputSchema = z.object({
559
559
  fast: z
560
560
  .boolean()
561
561
  .default(false)
562
- .describe("Skip expansion and reranking (~0.7s). Use for quick lookups"),
562
+ .describe(
563
+ "Skip query expansion, graph expansion, and reranking (~0.7s). Use for quick lookups"
564
+ ),
563
565
  thorough: z
564
566
  .boolean()
565
567
  .default(false)
@@ -574,14 +576,11 @@ export const queryInputSchema = z.object({
574
576
  .boolean()
575
577
  .optional()
576
578
  .describe("Override: enable/disable cross-encoder reranking"),
577
- noGraph: z
578
- .boolean()
579
- .optional()
580
- .describe("Compatibility no-op unless graph is also true"),
579
+ noGraph: z.boolean().optional().describe("Disable graph neighbor expansion"),
581
580
  graph: z
582
581
  .boolean()
583
582
  .optional()
584
- .describe("Enable bounded one-hop graph neighbor expansion"),
583
+ .describe("Enable bounded one-hop graph neighbor expansion (default)"),
585
584
  explain: z
586
585
  .boolean()
587
586
  .optional()
@@ -255,7 +255,7 @@ export function handleQuery(
255
255
  author: args.author,
256
256
  noExpand,
257
257
  noRerank,
258
- graph: args.graph === true,
258
+ graph: args.graph,
259
259
  noGraph: args.noGraph || args.fast,
260
260
  queryModes,
261
261
  tagsAll: normalizeTagFilters(args.tagsAll),
@@ -542,7 +542,7 @@ export function handleQueryDiagnose(
542
542
  author: args.author,
543
543
  noExpand,
544
544
  noRerank,
545
- graph: args.graph === true,
545
+ graph: args.graph,
546
546
  noGraph: args.noGraph || args.fast,
547
547
  queryModes,
548
548
  tagsAll: normalizeTagFilters(args.tagsAll),
@@ -186,6 +186,46 @@ const addEdgeCandidate = (
186
186
  scores.set(neighborDocid, current + score);
187
187
  };
188
188
 
189
+ const loadGraphLinks = async (
190
+ store: StorePort,
191
+ seedDocumentIds: number[],
192
+ options: {
193
+ collection?: string;
194
+ includeSimilar?: boolean;
195
+ }
196
+ ): Promise<
197
+ { ok: true; links: GraphLink[] } | { ok: false; reason: string }
198
+ > => {
199
+ if (typeof store.getGraphNeighborsForSeeds === "function") {
200
+ const neighborsResult = await store.getGraphNeighborsForSeeds({
201
+ seedDocumentIds,
202
+ collection: options.collection,
203
+ limitEdges: GRAPH_EDGE_LIMIT,
204
+ });
205
+ if (!neighborsResult.ok) {
206
+ return { ok: false, reason: "graph_query_failed" };
207
+ }
208
+ return { ok: true, links: neighborsResult.value.links };
209
+ }
210
+
211
+ if (typeof store.getGraph !== "function") {
212
+ return { ok: false, reason: "graph_unavailable" };
213
+ }
214
+
215
+ // Compatibility fallback for mocks/adapters without the scoped method.
216
+ const graphResult = await store.getGraph({
217
+ collection: options.collection,
218
+ limitNodes: GRAPH_NODE_LIMIT,
219
+ limitEdges: GRAPH_EDGE_LIMIT,
220
+ includeSimilar: options.includeSimilar ?? false,
221
+ linkedOnly: true,
222
+ });
223
+ if (!graphResult.ok) {
224
+ return { ok: false, reason: "graph_query_failed" };
225
+ }
226
+ return { ok: true, links: graphResult.value.links };
227
+ };
228
+
189
229
  /**
190
230
  * Expand top retrieval candidates through one-hop graph neighbors.
191
231
  */
@@ -225,7 +265,10 @@ export async function expandGraphCandidates(
225
265
  meta.fallbackReasons.push("graph_no_seed_candidates");
226
266
  return { candidates: [], meta };
227
267
  }
228
- if (typeof store.getGraph !== "function") {
268
+ const hasScopedNeighbors =
269
+ typeof store.getGraphNeighborsForSeeds === "function";
270
+ const hasFullGraph = typeof store.getGraph === "function";
271
+ if (!hasScopedNeighbors && !hasFullGraph) {
229
272
  meta.fallbackReasons.push("graph_unavailable");
230
273
  return { candidates: [], meta };
231
274
  }
@@ -248,8 +291,7 @@ export async function expandGraphCandidates(
248
291
  return { candidates: [], meta };
249
292
  }
250
293
 
251
- const seedByDocid = new Map<string, { doc: DocumentRow; rank: number }>();
252
- const seedDocids = new Set<string>();
294
+ const rankedSeedDocs: Array<{ doc: DocumentRow; rank: number }> = [];
253
295
  for (const doc of seedDocsResult.value) {
254
296
  if (!doc.mirrorHash) {
255
297
  continue;
@@ -268,34 +310,48 @@ export async function expandGraphCandidates(
268
310
  if (rank <= 0) {
269
311
  continue;
270
312
  }
271
- seedByDocid.set(doc.docid, { doc, rank });
272
- seedDocids.add(doc.docid);
313
+ rankedSeedDocs.push({ doc, rank });
314
+ }
315
+ rankedSeedDocs.sort(
316
+ (left, right) =>
317
+ left.rank - right.rank ||
318
+ left.doc.id - right.doc.id ||
319
+ left.doc.docid.localeCompare(right.doc.docid)
320
+ );
321
+
322
+ const seedByDocid = new Map<string, { doc: DocumentRow; rank: number }>();
323
+ for (const seed of rankedSeedDocs) {
324
+ if (seedByDocid.size >= GRAPH_SEED_LIMIT) {
325
+ break;
326
+ }
327
+ if (!seedByDocid.has(seed.doc.docid)) {
328
+ seedByDocid.set(seed.doc.docid, seed);
329
+ }
273
330
  }
274
- meta.seedCount = seedDocids.size;
275
- if (seedDocids.size === 0) {
331
+ const seedDocids = new Set(seedByDocid.keys());
332
+ meta.seedCount = seedByDocid.size;
333
+ if (seedByDocid.size === 0) {
276
334
  meta.fallbackReasons.push("graph_seed_lookup_empty");
277
335
  return { candidates: [], meta };
278
336
  }
279
337
 
280
- const graphResult = await store.getGraph({
338
+ const seedDocumentIds = [...seedByDocid.values()].map(({ doc }) => doc.id);
339
+ const linksResult = await loadGraphLinks(store, seedDocumentIds, {
281
340
  collection: options.collection,
282
- limitNodes: GRAPH_NODE_LIMIT,
283
- limitEdges: GRAPH_EDGE_LIMIT,
284
- includeSimilar: options.includeSimilar ?? false,
285
- linkedOnly: true,
341
+ includeSimilar: options.includeSimilar,
286
342
  });
287
- if (!graphResult.ok) {
288
- meta.fallbackReasons.push("graph_query_failed");
343
+ if (!linksResult.ok) {
344
+ meta.fallbackReasons.push(linksResult.reason);
289
345
  return { candidates: [], meta };
290
346
  }
291
- if (graphResult.value.links.length === 0) {
347
+ if (linksResult.links.length === 0) {
292
348
  meta.fallbackReasons.push("graph_empty");
293
349
  return { candidates: [], meta };
294
350
  }
295
351
 
296
352
  const neighborScores = new Map<string, number>();
297
353
  const edgeConfidence = { ...EMPTY_EDGE_CONFIDENCE };
298
- for (const link of graphResult.value.links) {
354
+ for (const link of linksResult.links) {
299
355
  const sourceSeed = seedByDocid.get(link.source);
300
356
  const targetSeed = seedByDocid.get(link.target);
301
357
  if (sourceSeed && !seedDocids.has(link.target)) {
@@ -655,7 +655,7 @@ export async function searchHybrid(
655
655
  includeSimilar: vectorAvailable,
656
656
  limit,
657
657
  candidateLimit,
658
- disabled: !options.graph || options.noGraph,
658
+ disabled: options.graph === false || options.noGraph === true,
659
659
  relPathPrefix: options.retrievalScope?.relPathPrefix,
660
660
  lang: options.lang,
661
661
  tagsAll: options.tagsAll,
@@ -244,7 +244,7 @@ export type HybridSearchOptions = SearchOptions & {
244
244
  explain?: boolean;
245
245
  /** Enable bounded one-hop graph candidate expansion */
246
246
  graph?: boolean;
247
- /** Compatibility no-op unless graph is also true */
247
+ /** Explicitly disable graph neighbor expansion */
248
248
  noGraph?: boolean;
249
249
  /** Language hint for prompt selection (does NOT filter retrieval, only affects expansion prompts) */
250
250
  queryLanguageHint?: string;
@@ -4079,7 +4079,7 @@ export async function handleQuery(
4079
4079
  queryModes: normalizedQueryModes,
4080
4080
  noExpand: body.noExpand,
4081
4081
  noRerank: body.noRerank,
4082
- graph: body.graph === true,
4082
+ graph: body.graph,
4083
4083
  noGraph: body.noGraph,
4084
4084
  tagsAll,
4085
4085
  tagsAny,
@@ -4308,7 +4308,7 @@ export async function handleQueryDiagnose(
4308
4308
  queryModes: normalizedQueryModes,
4309
4309
  noExpand: body.noExpand,
4310
4310
  noRerank: body.noRerank,
4311
- graph: body.graph === true,
4311
+ graph: body.graph,
4312
4312
  noGraph: body.noGraph,
4313
4313
  tagsAll,
4314
4314
  tagsAny,