@gmickel/gno 1.12.2 → 1.12.4

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.
@@ -75,6 +75,7 @@ const MAX_CONCURRENCY = 16;
75
75
  export const INGEST_VERSION = 6;
76
76
  const EMPTY_CONTENT_TYPE_RULES_FINGERPRINT = fingerprintContentTypeRules([]);
77
77
  const RELATION_EDGE_TYPE_PATTERN = /^[a-z][a-z0-9_]*$/;
78
+ const PROJECTION_YIELD_INTERVAL = 25;
78
79
  const NON_RETRYABLE_CONVERSION_ERROR_CODES = new Set([
79
80
  "CORRUPT",
80
81
  "PERMISSION",
@@ -1000,19 +1001,83 @@ export class SyncService {
1000
1001
  relPaths: string[],
1001
1002
  options: SyncOptions = {}
1002
1003
  ): Promise<FileSyncResult[]> {
1004
+ const result = await this.syncPaths(collection, store, relPaths, options);
1005
+ return result.files ?? [];
1006
+ }
1007
+
1008
+ async syncPaths(
1009
+ collection: Collection,
1010
+ store: StorePort,
1011
+ relPaths: string[],
1012
+ options: SyncOptions = {}
1013
+ ): Promise<CollectionSyncResult> {
1014
+ const startedAt = Date.now();
1015
+ const syncOptions: SyncOptions = {
1016
+ ...options,
1017
+ contentTypeRules: options.contentTypeRules ?? [],
1018
+ contentTypeRulesFingerprint:
1019
+ options.contentTypeRulesFingerprint ??
1020
+ fingerprintContentTypeRules(options.contentTypeRules ?? []),
1021
+ };
1003
1022
  const results: FileSyncResult[] = [];
1023
+ const projectionSourceIds = new Set<number>();
1024
+ let markedInactive = 0;
1004
1025
 
1005
1026
  for (const relPath of relPaths) {
1027
+ const existingResult = await store.getDocument(collection.name, relPath);
1028
+ const existingDoc = existingResult.ok ? existingResult.value : null;
1029
+ if (existingDoc) {
1030
+ await this.collectProjectionSourceIds(
1031
+ store,
1032
+ existingDoc.id,
1033
+ projectionSourceIds
1034
+ );
1035
+ }
1036
+
1006
1037
  const absPath = join(collection.path, relPath);
1007
1038
  let stats: Awaited<ReturnType<typeof stat>>;
1008
1039
  try {
1009
1040
  stats = await stat(absPath);
1010
- } catch {
1041
+ } catch (error) {
1042
+ const errorCode =
1043
+ error && typeof error === "object" && "code" in error
1044
+ ? String(error.code)
1045
+ : undefined;
1046
+ if (errorCode !== "ENOENT") {
1047
+ results.push({
1048
+ relPath,
1049
+ status: "error",
1050
+ errorCode: "STAT_FAILED",
1051
+ errorMessage:
1052
+ error instanceof Error ? error.message : "Failed to stat file",
1053
+ });
1054
+ continue;
1055
+ }
1056
+ if (existingDoc?.active) {
1057
+ const inactiveResult = await store.markInactive(collection.name, [
1058
+ relPath,
1059
+ ]);
1060
+ if (!inactiveResult.ok) {
1061
+ results.push({
1062
+ relPath,
1063
+ status: "error",
1064
+ errorCode: inactiveResult.error.code,
1065
+ errorMessage: inactiveResult.error.message,
1066
+ });
1067
+ continue;
1068
+ }
1069
+ markedInactive += inactiveResult.value;
1070
+ results.push({
1071
+ relPath,
1072
+ status: "updated",
1073
+ docid: existingDoc.docid,
1074
+ });
1075
+ continue;
1076
+ }
1011
1077
  results.push({
1012
1078
  relPath,
1013
- status: "error",
1014
- errorCode: "NOT_FOUND",
1015
- errorMessage: "File not found",
1079
+ status: existingDoc ? "unchanged" : "skipped",
1080
+ docid: existingDoc?.docid,
1016
1081
  });
1017
1082
  continue;
1018
1083
  }
@@ -1035,24 +1100,91 @@ export class SyncService {
1035
1100
  ctime: (stats.birthtime ?? stats.ctime ?? stats.mtime).toISOString(),
1036
1101
  };
1037
1102
 
1038
- const result = await this.processFile(collection, entry, store, options);
1103
+ const result = await this.processFile(
1104
+ collection,
1105
+ entry,
1106
+ store,
1107
+ syncOptions
1108
+ );
1039
1109
  results.push(result);
1110
+ const currentResult = await store.getDocument(collection.name, relPath);
1111
+ const currentDoc = currentResult.ok ? currentResult.value : null;
1112
+ if (currentDoc?.active) {
1113
+ await this.collectProjectionSourceIds(
1114
+ store,
1115
+ currentDoc.id,
1116
+ projectionSourceIds
1117
+ );
1118
+ }
1040
1119
  }
1041
1120
 
1042
- await this.projectTypedEdges(collection, store, options);
1121
+ const errors =
1122
+ syncOptions.projectTypedEdges === false
1123
+ ? []
1124
+ : await this.projectTypedEdges(store, syncOptions, projectionSourceIds);
1125
+ const added = results.filter((result) => result.status === "added").length;
1126
+ const updated = results.filter(
1127
+ (result) => result.status === "updated"
1128
+ ).length;
1129
+ const unchanged = results.filter(
1130
+ (result) => result.status === "unchanged"
1131
+ ).length;
1132
+ const errored = results.filter(
1133
+ (result) => result.status === "error"
1134
+ ).length;
1135
+ const skipped = results.filter(
1136
+ (result) => result.status === "skipped"
1137
+ ).length;
1043
1138
 
1044
- return results;
1139
+ return {
1140
+ collection: collection.name,
1141
+ filesProcessed: results.length,
1142
+ filesAdded: added,
1143
+ filesUpdated: updated,
1144
+ filesUnchanged: unchanged,
1145
+ filesErrored: errored,
1146
+ filesSkipped: skipped,
1147
+ filesMarkedInactive: markedInactive,
1148
+ durationMs: Date.now() - startedAt,
1149
+ files: results,
1150
+ errors,
1151
+ };
1152
+ }
1153
+
1154
+ private async collectProjectionSourceIds(
1155
+ store: StorePort,
1156
+ documentId: number,
1157
+ sourceIds: Set<number>
1158
+ ): Promise<void> {
1159
+ sourceIds.add(documentId);
1160
+ const [linkBacklinks, edgeBacklinks] = await Promise.all([
1161
+ store.getBacklinksForDoc(documentId),
1162
+ store.getEdgeBacklinksForDoc(documentId),
1163
+ ]);
1164
+ if (linkBacklinks.ok) {
1165
+ for (const backlink of linkBacklinks.value) {
1166
+ sourceIds.add(backlink.sourceDocId);
1167
+ }
1168
+ }
1169
+ if (edgeBacklinks.ok) {
1170
+ for (const backlink of edgeBacklinks.value) {
1171
+ sourceIds.add(backlink.sourceDocId);
1172
+ }
1173
+ }
1045
1174
  }
1046
1175
 
1047
1176
  private async projectTypedEdges(
1048
- collection: Collection,
1049
1177
  store: StorePort,
1050
- options: SyncOptions
1178
+ options: SyncOptions,
1179
+ sourceDocumentIds?: Set<number>
1051
1180
  ): Promise<Array<{ relPath: string; code: string; message: string }>> {
1052
1181
  const errors: Array<{ relPath: string; code: string; message: string }> =
1053
1182
  [];
1054
1183
 
1055
- const backfillResult = await store.backfillDocEdges();
1184
+ const selectedSourceIds = sourceDocumentIds
1185
+ ? [...sourceDocumentIds]
1186
+ : undefined;
1187
+ const backfillResult = await store.backfillDocEdges(selectedSourceIds);
1056
1188
  if (!backfillResult.ok) {
1057
1189
  return [
1058
1190
  {
@@ -1075,8 +1207,22 @@ export class SyncService {
1075
1207
  }
1076
1208
 
1077
1209
  const activeDocs = docsResult.value.filter((doc) => doc.active);
1210
+ const activeIds = new Set(activeDocs.map((doc) => doc.id));
1211
+ if (selectedSourceIds) {
1212
+ for (const documentId of selectedSourceIds) {
1213
+ if (!activeIds.has(documentId)) {
1214
+ await store.setDocEdges(documentId, [], "frontmatter-relation");
1215
+ }
1216
+ }
1217
+ }
1218
+ const projectedDocs = sourceDocumentIds
1219
+ ? activeDocs.filter((doc) => sourceDocumentIds.has(doc.id))
1220
+ : activeDocs;
1078
1221
 
1079
- for (const doc of activeDocs) {
1222
+ for (const [docIndex, doc] of projectedDocs.entries()) {
1223
+ if (docIndex > 0 && docIndex % PROJECTION_YIELD_INTERVAL === 0) {
1224
+ await Bun.sleep(0);
1225
+ }
1080
1226
  if (!doc.mirrorHash) {
1081
1227
  continue;
1082
1228
  }
@@ -1193,6 +1339,14 @@ export class SyncService {
1193
1339
  return errors;
1194
1340
  }
1195
1341
 
1342
+ /** Run an exact global typed-edge reconciliation with cooperative yields. */
1343
+ reconcileTypedEdges(
1344
+ store: StorePort,
1345
+ options: SyncOptions = {}
1346
+ ): Promise<Array<{ relPath: string; code: string; message: string }>> {
1347
+ return this.projectTypedEdges(store, options);
1348
+ }
1349
+
1196
1350
  /**
1197
1351
  * Sync a single collection.
1198
1352
  */
@@ -1419,9 +1573,9 @@ export class SyncService {
1419
1573
  }
1420
1574
  }
1421
1575
 
1422
- errors.push(
1423
- ...(await this.projectTypedEdges(collection, store, syncOptions))
1424
- );
1576
+ if (syncOptions.projectTypedEdges !== false) {
1577
+ errors.push(...(await this.projectTypedEdges(store, syncOptions)));
1578
+ }
1425
1579
 
1426
1580
  return {
1427
1581
  collection: collection.name,
@@ -1448,12 +1602,25 @@ export class SyncService {
1448
1602
  ): Promise<SyncResult> {
1449
1603
  const startTime = Date.now();
1450
1604
  const results: CollectionSyncResult[] = [];
1605
+ const deferredProjectionOptions: SyncOptions = {
1606
+ ...options,
1607
+ projectTypedEdges: false,
1608
+ };
1451
1609
 
1452
1610
  for (const collection of collections) {
1453
- const result = await this.syncCollection(collection, store, options);
1611
+ const result = await this.syncCollection(
1612
+ collection,
1613
+ store,
1614
+ deferredProjectionOptions
1615
+ );
1454
1616
  results.push(result);
1455
1617
  }
1456
1618
 
1619
+ if (results.length > 0) {
1620
+ const projectionErrors = await this.projectTypedEdges(store, options);
1621
+ results.at(-1)?.errors.push(...projectionErrors);
1622
+ }
1623
+
1457
1624
  // Aggregate totals
1458
1625
  const totals = results.reduce(
1459
1626
  (acc, r) => ({
@@ -137,6 +137,8 @@ export interface SyncOptions {
137
137
  contentTypeRules?: NormalizedContentTypeRule[];
138
138
  /** Stable hash of the normalized content type rules, used for re-derivation. */
139
139
  contentTypeRulesFingerprint?: string;
140
+ /** Internal orchestration flag: defer graph projection to an outer sync. */
141
+ projectTypedEdges?: boolean;
140
142
  }
141
143
 
142
144
  export type ContentTypeSource =
@@ -20,8 +20,10 @@ import {
20
20
  URI_PREFIX,
21
21
  } from "../../app/constants";
22
22
  import { MCP_ERRORS } from "../../core/errors";
23
+ import { resolveEffectiveIndex } from "../../core/indexed-reference";
23
24
  import { normalizeTag, validateTag } from "../../core/tags";
24
25
  import { normalizeCollectionName } from "../../core/validation";
26
+ import { openScopedIndexStore } from "../../store/sqlite/scoped-index";
25
27
 
26
28
  // Tags resource URI prefix
27
29
  const TAGS_URI = `${URI_PREFIX}tags`;
@@ -51,7 +53,8 @@ function formatTagsContent(
51
53
  function formatResourceContent(
52
54
  doc: DocumentRow,
53
55
  content: string,
54
- ctx: ToolContext
56
+ ctx: ToolContext,
57
+ indexName = ctx.indexName
55
58
  ): string {
56
59
  // Find collection for absPath
57
60
  const uriParsed = parseUri(doc.uri);
@@ -69,7 +72,7 @@ function formatResourceContent(
69
72
  const langLine = doc.languageHint
70
73
  ? `\n language: ${doc.languageHint}`
71
74
  : "";
72
- const displayUri = decorateUriForIndex(doc.uri, ctx.indexName);
75
+ const displayUri = decorateUriForIndex(doc.uri, indexName);
73
76
  const header = `<!-- ${displayUri}
74
77
  docid: ${doc.docid}
75
78
  source: ${absPath}
@@ -125,61 +128,82 @@ export function registerResources(server: McpServer, ctx: ToolContext): void {
125
128
  throw new Error(`Invalid gno:// URI: ${uri.href}`);
126
129
  }
127
130
 
128
- const { collection, path } = parsed;
129
-
130
- // Validate collection exists
131
- const collectionExists = ctx.collections.some(
132
- (c) => c.name === collection
133
- );
134
- if (!collectionExists) {
135
- throw new Error(`Collection not found: ${collection}`);
131
+ const resolution = resolveEffectiveIndex([uri.href], ctx.indexName);
132
+ if (!resolution.ok) {
133
+ throw new Error(resolution.error);
136
134
  }
135
+ const scoped = await openScopedIndexStore({
136
+ activeStore: ctx.store,
137
+ activeIndexName: ctx.indexName,
138
+ requestedIndexName: resolution.value.indexName,
139
+ config: ctx.config,
140
+ configPath: ctx.actualConfigPath,
141
+ });
142
+
143
+ try {
144
+ const { collection, path } = parsed;
137
145
 
138
- // Look up document (path is properly decoded by parseUri)
139
- const docResult = await ctx.store.getDocument(collection, path);
140
- if (!docResult.ok) {
141
- throw new Error(
142
- `Failed to lookup document: ${docResult.error.message}`
146
+ // Validate collection exists
147
+ const collectionExists = ctx.collections.some(
148
+ (c) => c.name === collection
143
149
  );
144
- }
150
+ if (!collectionExists) {
151
+ throw new Error(`Collection not found: ${collection}`);
152
+ }
145
153
 
146
- const doc = docResult.value;
147
- if (!doc) {
148
- throw new Error(`Document not found: ${uri.href}`);
149
- }
154
+ // Look up document (path is properly decoded by parseUri)
155
+ const docResult = await scoped.store.getDocument(collection, path);
156
+ if (!docResult.ok) {
157
+ throw new Error(
158
+ `Failed to lookup document: ${docResult.error.message}`
159
+ );
160
+ }
150
161
 
151
- // Get content
152
- if (!doc.mirrorHash) {
153
- throw new Error(`Document has no indexed content: ${uri.href}`);
154
- }
162
+ const doc = docResult.value;
163
+ if (!doc) {
164
+ throw new Error(`Document not found: ${uri.href}`);
165
+ }
155
166
 
156
- const contentResult = await ctx.store.getContent(doc.mirrorHash);
157
- if (!contentResult.ok) {
158
- throw new Error(
159
- `Failed to read content: ${contentResult.error.message}`
160
- );
161
- }
167
+ // Get content
168
+ if (!doc.mirrorHash) {
169
+ throw new Error(`Document has no indexed content: ${uri.href}`);
170
+ }
162
171
 
163
- const content = contentResult.value ?? "";
172
+ const contentResult = await scoped.store.getContent(doc.mirrorHash);
173
+ if (!contentResult.ok) {
174
+ throw new Error(
175
+ `Failed to read content: ${contentResult.error.message}`
176
+ );
177
+ }
164
178
 
165
- // Format with header and line numbers
166
- const formattedContent = formatResourceContent(doc, content, ctx);
179
+ const content = contentResult.value ?? "";
167
180
 
168
- // Build canonical URI
169
- const canonicalUri = decorateUriForIndex(
170
- buildUri(collection, path),
171
- parsed.indexName ?? ctx.indexName
172
- );
181
+ // Format with header and line numbers
182
+ const formattedContent = formatResourceContent(
183
+ doc,
184
+ content,
185
+ ctx,
186
+ scoped.indexName
187
+ );
173
188
 
174
- return {
175
- contents: [
176
- {
177
- uri: canonicalUri,
178
- mimeType: "text/markdown",
179
- text: formattedContent,
180
- },
181
- ],
182
- };
189
+ // Build canonical URI
190
+ const canonicalUri = decorateUriForIndex(
191
+ buildUri(collection, path),
192
+ scoped.indexName
193
+ );
194
+
195
+ return {
196
+ contents: [
197
+ {
198
+ uri: canonicalUri,
199
+ mimeType: "text/markdown",
200
+ text: formattedContent,
201
+ },
202
+ ],
203
+ };
204
+ } finally {
205
+ await scoped.close();
206
+ }
183
207
  } finally {
184
208
  release();
185
209
  }
@@ -14,7 +14,9 @@ import {
14
14
  getDocumentCapabilities,
15
15
  type DocumentCapabilities,
16
16
  } from "../../core/document-capabilities";
17
+ import { resolveEffectiveIndex } from "../../core/indexed-reference";
17
18
  import { parseRef } from "../../core/ref-parser";
19
+ import { openScopedIndexStore } from "../../store/sqlite/scoped-index";
18
20
  import { runTool, type ToolResult } from "./index";
19
21
 
20
22
  interface GetInput {
@@ -128,109 +130,122 @@ export function handleGet(
128
130
  throw new Error(parsed.error);
129
131
  }
130
132
 
131
- // Lookup document
132
- const doc = await lookupDocument(ctx.store, parsed);
133
- if (!doc) {
134
- throw new Error(`Document not found: ${args.ref}`);
133
+ const resolution = resolveEffectiveIndex([args.ref], ctx.indexName);
134
+ if (!resolution.ok) {
135
+ throw new Error(resolution.error);
135
136
  }
137
+ const scoped = await openScopedIndexStore({
138
+ activeStore: ctx.store,
139
+ activeIndexName: ctx.indexName,
140
+ requestedIndexName: resolution.value.indexName,
141
+ config: ctx.config,
142
+ configPath: ctx.actualConfigPath,
143
+ });
144
+
145
+ try {
146
+ // Lookup document
147
+ const doc = await lookupDocument(scoped.store, parsed);
148
+ if (!doc) {
149
+ throw new Error(`Document not found: ${args.ref}`);
150
+ }
136
151
 
137
- // Get content
138
- if (!doc.mirrorHash) {
139
- throw new Error("Document has no indexed content");
140
- }
152
+ // Get content
153
+ if (!doc.mirrorHash) {
154
+ throw new Error("Document has no indexed content");
155
+ }
141
156
 
142
- const contentResult = await ctx.store.getContent(doc.mirrorHash);
143
- if (!contentResult.ok) {
144
- throw new Error(contentResult.error.message);
145
- }
157
+ const contentResult = await scoped.store.getContent(doc.mirrorHash);
158
+ if (!contentResult.ok) {
159
+ throw new Error(contentResult.error.message);
160
+ }
146
161
 
147
- const fullContent = contentResult.value ?? "";
148
- const contentLines = fullContent.split("\n");
149
- const totalLines = contentLines.length;
150
-
151
- // Apply line range if specified
152
- let content = fullContent;
153
- let returnedLines: { start: number; end: number } | undefined;
154
-
155
- // lineNumbers defaults to true per spec
156
- const showLineNumbers = args.lineNumbers !== false;
157
-
158
- if (args.fromLine || args.lineCount) {
159
- const startLine = args.fromLine ?? 1;
160
- // Clamp startLine to valid range
161
- if (startLine > totalLines) {
162
- // Return empty content for out-of-range request
163
- content = "";
164
- returnedLines = undefined;
165
- } else {
166
- const count = args.lineCount ?? totalLines - startLine + 1;
167
- const endLine = Math.min(startLine + count - 1, totalLines);
168
-
169
- const slicedLines = contentLines.slice(startLine - 1, endLine);
170
-
171
- if (showLineNumbers) {
172
- content = slicedLines
173
- .map((line, i) => `${startLine + i}: ${line}`)
174
- .join("\n");
162
+ const fullContent = contentResult.value ?? "";
163
+ const contentLines = fullContent.split("\n");
164
+ const totalLines = contentLines.length;
165
+
166
+ // Apply line range if specified
167
+ let content = fullContent;
168
+ let returnedLines: { start: number; end: number } | undefined;
169
+
170
+ // lineNumbers defaults to true per spec
171
+ const showLineNumbers = args.lineNumbers !== false;
172
+
173
+ if (args.fromLine || args.lineCount) {
174
+ const startLine = args.fromLine ?? 1;
175
+ // Clamp startLine to valid range
176
+ if (startLine > totalLines) {
177
+ // Return empty content for out-of-range request
178
+ content = "";
179
+ returnedLines = undefined;
175
180
  } else {
176
- content = slicedLines.join("\n");
177
- }
181
+ const count = args.lineCount ?? totalLines - startLine + 1;
182
+ const endLine = Math.min(startLine + count - 1, totalLines);
178
183
 
179
- returnedLines = { start: startLine, end: endLine };
184
+ const slicedLines = contentLines.slice(startLine - 1, endLine);
185
+
186
+ if (showLineNumbers) {
187
+ content = slicedLines
188
+ .map((line, i) => `${startLine + i}: ${line}`)
189
+ .join("\n");
190
+ } else {
191
+ content = slicedLines.join("\n");
192
+ }
193
+
194
+ returnedLines = { start: startLine, end: endLine };
195
+ }
196
+ } else if (showLineNumbers) {
197
+ content = contentLines
198
+ .map((line, i) => `${i + 1}: ${line}`)
199
+ .join("\n");
180
200
  }
181
- } else if (showLineNumbers) {
182
- content = contentLines.map((line, i) => `${i + 1}: ${line}`).join("\n");
183
- }
184
201
 
185
- // Build absPath
186
- const uriParsed = parseUri(doc.uri);
187
- let absPath: string | undefined;
188
- if (uriParsed) {
189
- const collection = ctx.collections.find(
190
- (c) => c.name === uriParsed.collection
191
- );
192
- if (collection) {
193
- absPath = pathJoin(collection.path, doc.relPath);
202
+ // Build absPath
203
+ const uriParsed = parseUri(doc.uri);
204
+ let absPath: string | undefined;
205
+ if (uriParsed) {
206
+ const collection = ctx.collections.find(
207
+ (c) => c.name === uriParsed.collection
208
+ );
209
+ if (collection) {
210
+ absPath = pathJoin(collection.path, doc.relPath);
211
+ }
194
212
  }
195
- }
196
213
 
197
- const response: GetResponse = {
198
- docid: doc.docid,
199
- uri: decorateUriForIndex(
200
- doc.uri,
201
- parsed.type === "uri"
202
- ? (parseUri(parsed.value)?.indexName ?? ctx.indexName)
203
- : ctx.indexName
204
- ),
205
- title: doc.title ?? undefined,
206
- content,
207
- totalLines,
208
- returnedLines,
209
- language: doc.languageHint ?? undefined,
210
- source: {
211
- absPath,
212
- relPath: doc.relPath,
213
- mime: doc.sourceMime,
214
- ext: doc.sourceExt,
215
- modifiedAt: doc.sourceMtime,
216
- sizeBytes: doc.sourceSize,
217
- sourceHash: doc.sourceHash,
218
- },
219
- conversion: doc.mirrorHash
220
- ? {
221
- converterId: doc.converterId ?? undefined,
222
- converterVersion: doc.converterVersion ?? undefined,
223
- mirrorHash: doc.mirrorHash,
224
- }
225
- : undefined,
226
- capabilities: getDocumentCapabilities({
227
- sourceExt: doc.sourceExt,
228
- sourceMime: doc.sourceMime,
229
- contentAvailable: doc.mirrorHash !== null,
230
- }),
231
- };
232
-
233
- return response;
214
+ const response: GetResponse = {
215
+ docid: doc.docid,
216
+ uri: decorateUriForIndex(doc.uri, scoped.indexName),
217
+ title: doc.title ?? undefined,
218
+ content,
219
+ totalLines,
220
+ returnedLines,
221
+ language: doc.languageHint ?? undefined,
222
+ source: {
223
+ absPath,
224
+ relPath: doc.relPath,
225
+ mime: doc.sourceMime,
226
+ ext: doc.sourceExt,
227
+ modifiedAt: doc.sourceMtime,
228
+ sizeBytes: doc.sourceSize,
229
+ sourceHash: doc.sourceHash,
230
+ },
231
+ conversion: doc.mirrorHash
232
+ ? {
233
+ converterId: doc.converterId ?? undefined,
234
+ converterVersion: doc.converterVersion ?? undefined,
235
+ mirrorHash: doc.mirrorHash,
236
+ }
237
+ : undefined,
238
+ capabilities: getDocumentCapabilities({
239
+ sourceExt: doc.sourceExt,
240
+ sourceMime: doc.sourceMime,
241
+ contentAvailable: doc.mirrorHash !== null,
242
+ }),
243
+ };
244
+
245
+ return response;
246
+ } finally {
247
+ await scoped.close();
248
+ }
234
249
  },
235
250
  formatGetResponse
236
251
  );
@@ -60,11 +60,11 @@ export function normalizeTagFilters(tags?: string[]): string[] | undefined {
60
60
 
61
61
  export const MCP_TOOL_DESCRIPTIONS = {
62
62
  search:
63
- "BM25 keyword search. Fast exact-term lookup for names, identifiers, error text, and known phrases. Results include uri/docid and line when available; use gno_get with fromLine/lineCount or gno_multi_get for full context. Use gno_query when wording is uncertain.",
63
+ "BM25 keyword search. Fast exact-term lookup for names, identifiers, error text, and known phrases. Structured results include uri/docid, line when available, and optional user-configured context guidance; use gno_get with fromLine/lineCount or gno_multi_get for full context. Use gno_query when wording is uncertain.",
64
64
  vsearch:
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.",
65
+ "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.",
66
66
  query:
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.",
67
+ "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.",
68
68
  queryDiagnose:
69
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.",
70
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.",