@gmickel/gno 1.30.6 → 1.31.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 (32) hide show
  1. package/README.md +2 -2
  2. package/assets/skill/SKILL.md +2 -0
  3. package/assets/skill/mcp-reference.md +6 -0
  4. package/browser-extension/artifacts/{gno-browser-clipper-v1.30.6.zip → gno-browser-clipper-v1.31.0.zip} +0 -0
  5. package/browser-extension/artifacts/gno-browser-clipper-v1.31.0.zip.sha256 +1 -0
  6. package/browser-extension/dist/manifest.json +1 -1
  7. package/package.json +1 -1
  8. package/spec/mcp.md +62 -0
  9. package/spec/output-schemas/section-target-create-result.schema.json +20 -0
  10. package/spec/output-schemas/section-target-resolve-result.schema.json +194 -0
  11. package/spec/output-schemas/section-target.schema.json +118 -0
  12. package/spec/output-schemas/section.schema.json +113 -0
  13. package/src/core/section-parse.ts +187 -0
  14. package/src/core/section-target-link.ts +154 -0
  15. package/src/core/section-target-resolve.ts +351 -0
  16. package/src/core/section-target-transport.ts +519 -0
  17. package/src/core/section-target.ts +263 -0
  18. package/src/core/sections.ts +60 -115
  19. package/src/mcp/AGENTS.md +1 -0
  20. package/src/mcp/CLAUDE.md +1 -0
  21. package/src/mcp/http-egress.ts +1 -0
  22. package/src/mcp/tools/index.ts +19 -0
  23. package/src/mcp/tools/sections.ts +512 -0
  24. package/src/sdk/client.ts +71 -1
  25. package/src/sdk/index.ts +5 -0
  26. package/src/sdk/types.ts +29 -1
  27. package/src/serve/public/globals.built.css +1 -1
  28. package/src/serve/public/lib/section-links.ts +189 -0
  29. package/src/serve/public/pages/DocView.tsx +219 -36
  30. package/src/serve/routes/section-targets.ts +221 -0
  31. package/src/serve/server.ts +34 -0
  32. package/browser-extension/artifacts/gno-browser-clipper-v1.30.6.zip.sha256 +0 -1
@@ -0,0 +1,512 @@
1
+ /**
2
+ * MCP gno_section — read-only section target create/resolve.
3
+ *
4
+ * Always registered. No writes, no target persistence, no parser fork.
5
+ * Consumes shared core create/resolve + transport projection (fn-61.6).
6
+ *
7
+ * @module src/mcp/tools/sections
8
+ */
9
+
10
+ import { z } from "zod";
11
+
12
+ import type { DocumentRow } from "../../store/types";
13
+ import type { ToolContext } from "../server";
14
+
15
+ import { MCP_ERRORS } from "../../core/errors";
16
+ import { parseRef } from "../../core/ref-parser";
17
+ import {
18
+ CANONICAL_URI_EXCEEDS_TRANSPORT_BOUNDS,
19
+ createSectionTarget,
20
+ extractSections,
21
+ isBoundedSectionTarget,
22
+ isTransportBoundedCanonicalUri,
23
+ parseSectionTargetCreateSelector,
24
+ parseSectionTargetV1,
25
+ projectSectionTargetCreateResult,
26
+ projectSectionTargetResolveResult,
27
+ resolveSectionTarget,
28
+ SECTION_TARGET_BOUNDS,
29
+ type SectionCitationV1,
30
+ type SectionResolutionDiagnostics,
31
+ type SectionResolutionStatus,
32
+ type SectionTargetV1,
33
+ } from "../../core/sections";
34
+ import { runTool, type ToolResult } from "./index";
35
+
36
+ export const SECTION_MCP_SCHEMA_VERSION = "1.0" as const;
37
+
38
+ const FINGERPRINT_PATTERN = /^[a-f0-9]{64}$/;
39
+ const REF_MAX_CHARS = 2048;
40
+
41
+ const positiveSafeInt = z.number().int().min(1).max(Number.MAX_SAFE_INTEGER);
42
+
43
+ const boundedNonEmpty = (max: number) => z.string().min(1).max(max);
44
+
45
+ /**
46
+ * Closed SectionTargetV1 Zod mirror for MCP SDK validation.
47
+ * Object (not union) so McpServer can publish/normalize JSON Schema.
48
+ * Field bounds stay JSON-schema-compatible; cross-field rules via superRefine.
49
+ */
50
+ export const sectionTargetInputSchema = z
51
+ .object({
52
+ schemaVersion: z.literal("1"),
53
+ document: z
54
+ .object({
55
+ uri: boundedNonEmpty(SECTION_TARGET_BOUNDS.uriMaxChars),
56
+ })
57
+ .strict(),
58
+ anchor: boundedNonEmpty(SECTION_TARGET_BOUNDS.anchorMaxChars),
59
+ headingPath: z
60
+ .array(boundedNonEmpty(SECTION_TARGET_BOUNDS.headingPathItemMaxChars))
61
+ .min(1)
62
+ .max(SECTION_TARGET_BOUNDS.headingPathMaxItems),
63
+ occurrence: positiveSafeInt,
64
+ quote: z
65
+ .object({
66
+ exact: z.string().max(SECTION_TARGET_BOUNDS.exactMaxChars),
67
+ prefix: z.string().max(SECTION_TARGET_BOUNDS.prefixMaxChars),
68
+ suffix: z.string().max(SECTION_TARGET_BOUNDS.suffixMaxChars),
69
+ })
70
+ .strict(),
71
+ sourceFingerprint: z.string().regex(FINGERPRINT_PATTERN),
72
+ hints: z
73
+ .object({
74
+ line: positiveSafeInt,
75
+ startOffset: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
76
+ endOffset: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
77
+ })
78
+ .strict(),
79
+ })
80
+ .strict()
81
+ .superRefine((target, ctx) => {
82
+ if (target.hints.endOffset < target.hints.startOffset) {
83
+ ctx.addIssue({
84
+ code: z.ZodIssueCode.custom,
85
+ path: ["hints", "endOffset"],
86
+ message: "hints.endOffset must be >= hints.startOffset",
87
+ });
88
+ }
89
+ if (!isBoundedSectionTarget(target as SectionTargetV1)) {
90
+ ctx.addIssue({
91
+ code: z.ZodIssueCode.custom,
92
+ message: "Section target exceeds size bounds",
93
+ });
94
+ }
95
+ });
96
+
97
+ const refSchema = boundedNonEmpty(REF_MAX_CHARS).describe(
98
+ "Document reference: gno:// URI, collection/path, or #docid"
99
+ );
100
+
101
+ /**
102
+ * Closed create/resolve input. Single object schema (MCP SDK cannot list/validate
103
+ * Zod unions as tool inputSchema/outputSchema).
104
+ */
105
+ export const sectionInputSchema = z
106
+ .object({
107
+ action: z.enum(["create", "resolve"]),
108
+ ref: refSchema,
109
+ anchor: boundedNonEmpty(SECTION_TARGET_BOUNDS.anchorMaxChars).optional(),
110
+ line: positiveSafeInt.optional(),
111
+ target: sectionTargetInputSchema.optional(),
112
+ })
113
+ .strict()
114
+ .superRefine((value, ctx) => {
115
+ if (value.action === "create") {
116
+ const hasAnchor = value.anchor !== undefined;
117
+ const hasLine = value.line !== undefined;
118
+ if (hasAnchor === hasLine) {
119
+ ctx.addIssue({
120
+ code: z.ZodIssueCode.custom,
121
+ message: "create requires exactly one of anchor|line",
122
+ });
123
+ }
124
+ if (value.target !== undefined) {
125
+ ctx.addIssue({
126
+ code: z.ZodIssueCode.custom,
127
+ path: ["target"],
128
+ message: "create forbids target",
129
+ });
130
+ }
131
+ return;
132
+ }
133
+ if (value.target === undefined) {
134
+ ctx.addIssue({
135
+ code: z.ZodIssueCode.custom,
136
+ path: ["target"],
137
+ message: "resolve requires target",
138
+ });
139
+ }
140
+ if (value.anchor !== undefined || value.line !== undefined) {
141
+ ctx.addIssue({
142
+ code: z.ZodIssueCode.custom,
143
+ message: "resolve forbids anchor|line",
144
+ });
145
+ }
146
+ });
147
+
148
+ export type SectionInput = z.infer<typeof sectionInputSchema>;
149
+
150
+ const citationSchema = z
151
+ .object({
152
+ uri: boundedNonEmpty(SECTION_TARGET_BOUNDS.uriMaxChars),
153
+ anchor: boundedNonEmpty(SECTION_TARGET_BOUNDS.anchorMaxChars),
154
+ title: boundedNonEmpty(SECTION_TARGET_BOUNDS.anchorMaxChars),
155
+ lineStart: positiveSafeInt,
156
+ lineEnd: positiveSafeInt,
157
+ sourceFingerprint: z.string().regex(FINGERPRINT_PATTERN),
158
+ })
159
+ .strict()
160
+ .superRefine((citation, ctx) => {
161
+ if (citation.lineEnd < citation.lineStart) {
162
+ ctx.addIssue({
163
+ code: z.ZodIssueCode.custom,
164
+ path: ["lineEnd"],
165
+ message: "citation.lineEnd must be >= citation.lineStart",
166
+ });
167
+ }
168
+ });
169
+
170
+ const diagnosticsSchema = z
171
+ .object({
172
+ reason: z.string().min(1).max(256).optional(),
173
+ candidates: z
174
+ .array(
175
+ z
176
+ .object({
177
+ anchor: boundedNonEmpty(SECTION_TARGET_BOUNDS.anchorMaxChars),
178
+ line: positiveSafeInt,
179
+ title: boundedNonEmpty(SECTION_TARGET_BOUNDS.anchorMaxChars),
180
+ headingPath: z
181
+ .array(
182
+ boundedNonEmpty(SECTION_TARGET_BOUNDS.headingPathItemMaxChars)
183
+ )
184
+ .min(1)
185
+ .max(SECTION_TARGET_BOUNDS.headingPathMaxItems),
186
+ occurrence: positiveSafeInt,
187
+ })
188
+ .strict()
189
+ )
190
+ .max(32)
191
+ .optional(),
192
+ candidateCount: z
193
+ .number()
194
+ .int()
195
+ .min(0)
196
+ .max(Number.MAX_SAFE_INTEGER)
197
+ .optional(),
198
+ candidatesTruncated: z.boolean().optional(),
199
+ })
200
+ .strict()
201
+ .superRefine((diagnostics, ctx) => {
202
+ const hasCandidates = diagnostics.candidates !== undefined;
203
+ const hasCount = diagnostics.candidateCount !== undefined;
204
+ const hasTruncated = diagnostics.candidatesTruncated !== undefined;
205
+ if (hasCandidates || hasCount || hasTruncated) {
206
+ if (!(hasCandidates && hasCount && hasTruncated)) {
207
+ ctx.addIssue({
208
+ code: z.ZodIssueCode.custom,
209
+ message:
210
+ "diagnostics candidates, candidateCount, and candidatesTruncated are co-required",
211
+ });
212
+ }
213
+ }
214
+ });
215
+
216
+ /**
217
+ * SDK-validated structured output for gno_section.
218
+ * Single object (not union) so McpServer can publish/validate outputSchema.
219
+ */
220
+ export const sectionOutputSchema = z
221
+ .object({
222
+ schemaVersion: z.literal(SECTION_MCP_SCHEMA_VERSION),
223
+ action: z.enum(["create", "resolve"]),
224
+ uri: boundedNonEmpty(SECTION_TARGET_BOUNDS.uriMaxChars),
225
+ target: sectionTargetInputSchema,
226
+ status: z
227
+ .enum(["exact", "recovered", "ambiguous", "stale", "missing"])
228
+ .optional(),
229
+ currentFingerprint: z.string().regex(FINGERPRINT_PATTERN).optional(),
230
+ diagnostics: diagnosticsSchema.optional(),
231
+ citation: citationSchema.optional(),
232
+ })
233
+ .strict()
234
+ .superRefine((result, ctx) => {
235
+ if (result.action === "create") {
236
+ if (
237
+ result.status !== undefined ||
238
+ result.currentFingerprint !== undefined ||
239
+ result.diagnostics !== undefined ||
240
+ result.citation !== undefined
241
+ ) {
242
+ ctx.addIssue({
243
+ code: z.ZodIssueCode.custom,
244
+ message: "create result forbids resolve-only fields",
245
+ });
246
+ }
247
+ return;
248
+ }
249
+ if (
250
+ result.status === undefined ||
251
+ result.currentFingerprint === undefined
252
+ ) {
253
+ ctx.addIssue({
254
+ code: z.ZodIssueCode.custom,
255
+ message: "resolve requires status and currentFingerprint",
256
+ });
257
+ return;
258
+ }
259
+ if (result.diagnostics === undefined) {
260
+ ctx.addIssue({
261
+ code: z.ZodIssueCode.custom,
262
+ path: ["diagnostics"],
263
+ message: "resolve requires diagnostics",
264
+ });
265
+ }
266
+ const navigable =
267
+ result.status === "exact" || result.status === "recovered";
268
+ if (navigable && result.citation === undefined) {
269
+ ctx.addIssue({
270
+ code: z.ZodIssueCode.custom,
271
+ path: ["citation"],
272
+ message: "exact/recovered require citation",
273
+ });
274
+ }
275
+ if (!navigable && result.citation !== undefined) {
276
+ ctx.addIssue({
277
+ code: z.ZodIssueCode.custom,
278
+ path: ["citation"],
279
+ message: "ambiguous/stale/missing forbid citation",
280
+ });
281
+ }
282
+ });
283
+
284
+ export type SectionMcpResult = z.infer<typeof sectionOutputSchema>;
285
+
286
+ export const SECTION_MCP_ANNOTATIONS = {
287
+ readOnlyHint: true,
288
+ destructiveHint: false,
289
+ idempotentHint: true,
290
+ openWorldHint: false,
291
+ } as const;
292
+
293
+ async function lookupDocument(
294
+ ctx: ToolContext,
295
+ ref: string
296
+ ): Promise<
297
+ { ok: true; doc: DocumentRow } | { ok: false; code: string; message: string }
298
+ > {
299
+ const parsed = parseRef(ref);
300
+ if ("error" in parsed) {
301
+ return {
302
+ ok: false,
303
+ code: MCP_ERRORS.INVALID_INPUT.code,
304
+ message: `Invalid ref format: ${parsed.error}`,
305
+ };
306
+ }
307
+
308
+ let doc: DocumentRow | null = null;
309
+ switch (parsed.type) {
310
+ case "docid": {
311
+ const result = await ctx.store.getDocumentByDocid(parsed.value);
312
+ doc = result.ok ? result.value : null;
313
+ break;
314
+ }
315
+ case "uri": {
316
+ const result = await ctx.store.getDocumentByUri(parsed.value);
317
+ doc = result.ok ? result.value : null;
318
+ break;
319
+ }
320
+ case "collPath": {
321
+ if (!(parsed.collection && parsed.relPath)) {
322
+ return {
323
+ ok: false,
324
+ code: MCP_ERRORS.INVALID_INPUT.code,
325
+ message: "Invalid collection/path format",
326
+ };
327
+ }
328
+ const canonical = ctx.collections.find(
329
+ (c) => c.name.toLowerCase() === parsed.collection!.toLowerCase()
330
+ );
331
+ const collectionName = canonical?.name ?? parsed.collection;
332
+ const result = await ctx.store.getDocument(
333
+ collectionName,
334
+ parsed.relPath
335
+ );
336
+ doc = result.ok ? result.value : null;
337
+ break;
338
+ }
339
+ }
340
+
341
+ if (!doc) {
342
+ return {
343
+ ok: false,
344
+ code: MCP_ERRORS.NOT_FOUND.code,
345
+ message: `Document not found: ${ref}`,
346
+ };
347
+ }
348
+ return { ok: true, doc };
349
+ }
350
+
351
+ async function loadIndexedContent(
352
+ ctx: ToolContext,
353
+ ref: string
354
+ ): Promise<{ doc: DocumentRow; content: string }> {
355
+ const lookup = await lookupDocument(ctx, ref);
356
+ if (!lookup.ok) {
357
+ throw new Error(`${lookup.code}: ${lookup.message}`);
358
+ }
359
+ const { doc } = lookup;
360
+ if (!doc.mirrorHash) {
361
+ throw new Error(
362
+ `${MCP_ERRORS.NOT_FOUND.code}: Document content unavailable`
363
+ );
364
+ }
365
+ const contentResult = await ctx.store.getContent(doc.mirrorHash);
366
+ if (!contentResult.ok || contentResult.value === null) {
367
+ throw new Error("RUNTIME: Mirror content unavailable");
368
+ }
369
+ if (!isTransportBoundedCanonicalUri(doc.uri)) {
370
+ throw new Error(`VALIDATION: ${CANONICAL_URI_EXCEEDS_TRANSPORT_BOUNDS}`);
371
+ }
372
+ return { doc, content: contentResult.value };
373
+ }
374
+
375
+ function formatSectionResult(data: SectionMcpResult): string {
376
+ const json = JSON.stringify(data, null, 2);
377
+ if (data.action === "create") {
378
+ return [
379
+ `Created section target for ${data.uri} (anchor=${data.target.anchor}, line=${data.target.hints.line}).`,
380
+ "",
381
+ json,
382
+ ].join("\n");
383
+ }
384
+
385
+ if (data.citation) {
386
+ const citation: SectionCitationV1 = data.citation;
387
+ const lineCount = citation.lineEnd - citation.lineStart + 1;
388
+ return [
389
+ `Resolved section (${data.status}) in ${citation.uri}: ${citation.title} (#${citation.anchor}), lines ${citation.lineStart}-${citation.lineEnd}.`,
390
+ `Follow up with gno_get: {"ref":${JSON.stringify(citation.uri)},"fromLine":${citation.lineStart},"lineCount":${lineCount}}`,
391
+ "",
392
+ json,
393
+ ].join("\n");
394
+ }
395
+
396
+ const status = data.status as SectionResolutionStatus;
397
+ return [
398
+ `Section resolution status is ${status}; this result is not safe to navigate or cite.`,
399
+ "",
400
+ json,
401
+ ].join("\n");
402
+ }
403
+
404
+ async function handleCreate(
405
+ ctx: ToolContext,
406
+ ref: string,
407
+ selectorRaw: { anchor?: string; line?: number }
408
+ ): Promise<SectionMcpResult> {
409
+ const selector = parseSectionTargetCreateSelector(selectorRaw);
410
+ if (!selector.ok) {
411
+ throw new Error(`VALIDATION: ${selector.error}`);
412
+ }
413
+
414
+ const { doc, content } = await loadIndexedContent(ctx, ref);
415
+ const target = await createSectionTarget({
416
+ content,
417
+ uri: doc.uri,
418
+ ...selector.value,
419
+ });
420
+
421
+ if (!target) {
422
+ const sections = extractSections(content);
423
+ const matched =
424
+ selector.value.anchor !== undefined
425
+ ? sections.some((section) => section.anchor === selector.value.anchor)
426
+ : sections.some((section) => section.line === selector.value.line);
427
+ if (!matched) {
428
+ throw new Error(`${MCP_ERRORS.NOT_FOUND.code}: Section not found`);
429
+ }
430
+ throw new Error("VALIDATION: Section target exceeds size bounds");
431
+ }
432
+
433
+ const projected = projectSectionTargetCreateResult(doc.uri, target);
434
+ return {
435
+ schemaVersion: SECTION_MCP_SCHEMA_VERSION,
436
+ action: "create",
437
+ ...projected,
438
+ };
439
+ }
440
+
441
+ async function handleResolve(
442
+ ctx: ToolContext,
443
+ ref: string,
444
+ targetRaw: unknown
445
+ ): Promise<SectionMcpResult> {
446
+ const parsed = parseSectionTargetV1(targetRaw);
447
+ if (!parsed.ok) {
448
+ throw new Error(`VALIDATION: ${parsed.error}`);
449
+ }
450
+
451
+ const { doc, content } = await loadIndexedContent(ctx, ref);
452
+ const resolution = await resolveSectionTarget({
453
+ content,
454
+ target: parsed.value,
455
+ uri: doc.uri,
456
+ });
457
+ const projected = projectSectionTargetResolveResult(doc.uri, resolution);
458
+
459
+ if (projected.citation) {
460
+ return {
461
+ schemaVersion: SECTION_MCP_SCHEMA_VERSION,
462
+ action: "resolve",
463
+ uri: projected.uri,
464
+ status: projected.status as "exact" | "recovered",
465
+ currentFingerprint: projected.currentFingerprint,
466
+ target: projected.target,
467
+ diagnostics: projected.diagnostics as SectionResolutionDiagnostics,
468
+ citation: projected.citation,
469
+ };
470
+ }
471
+
472
+ return {
473
+ schemaVersion: SECTION_MCP_SCHEMA_VERSION,
474
+ action: "resolve",
475
+ uri: projected.uri,
476
+ status: projected.status as "ambiguous" | "stale" | "missing",
477
+ currentFingerprint: projected.currentFingerprint,
478
+ target: projected.target,
479
+ diagnostics: projected.diagnostics as SectionResolutionDiagnostics,
480
+ };
481
+ }
482
+
483
+ /**
484
+ * Handle gno_section tool call. Read-only — never mutates store or disk.
485
+ */
486
+ export function handleSection(
487
+ args: SectionInput,
488
+ ctx: ToolContext
489
+ ): Promise<ToolResult> {
490
+ return runTool(
491
+ ctx,
492
+ "gno_section",
493
+ async () => {
494
+ if (args.action === "create") {
495
+ if (args.anchor !== undefined) {
496
+ return handleCreate(ctx, args.ref, { anchor: args.anchor });
497
+ }
498
+ if (args.line !== undefined) {
499
+ return handleCreate(ctx, args.ref, { line: args.line });
500
+ }
501
+ throw new Error(
502
+ "VALIDATION: create requires exactly one of anchor|line"
503
+ );
504
+ }
505
+ if (args.target === undefined) {
506
+ throw new Error("VALIDATION: resolve requires target");
507
+ }
508
+ return handleResolve(ctx, args.ref, args.target);
509
+ },
510
+ formatSectionResult
511
+ );
512
+ }
package/src/sdk/client.ts CHANGED
@@ -47,6 +47,10 @@ import type {
47
47
  KnowledgeImpactInput,
48
48
  KnowledgeImpactResult,
49
49
  ListKnowledgeChangesInput,
50
+ SectionTargetCreateResult,
51
+ SectionTargetCreateSelector,
52
+ SectionTargetResolveResult,
53
+ SectionTargetV1,
50
54
  } from "./types";
51
55
 
52
56
  import {
@@ -122,7 +126,17 @@ import {
122
126
  RETRIEVAL_TRACE_METADATA,
123
127
  type RetrievalTraceSession,
124
128
  } from "../core/retrieval-trace-session";
125
- import { extractSections } from "../core/sections";
129
+ import {
130
+ CANONICAL_URI_EXCEEDS_TRANSPORT_BOUNDS,
131
+ createSectionTarget as createSectionTargetCore,
132
+ extractSections,
133
+ isTransportBoundedCanonicalUri,
134
+ parseSectionTargetCreateSelector,
135
+ parseSectionTargetV1,
136
+ projectSectionTargetCreateResult,
137
+ projectSectionTargetResolveResult,
138
+ resolveSectionTarget as resolveSectionTargetCore,
139
+ } from "../core/sections";
126
140
  import { normalizeStructuredQueryInput } from "../core/structured-query";
127
141
  import { parseAndValidateTagFilter } from "../core/tags";
128
142
  import {
@@ -1835,6 +1849,62 @@ class GnoClientImpl implements GnoClient {
1835
1849
  return extractSections(document.content);
1836
1850
  }
1837
1851
 
1852
+ async createSectionTarget(
1853
+ ref: string,
1854
+ selector: SectionTargetCreateSelector
1855
+ ): Promise<SectionTargetCreateResult> {
1856
+ this.assertOpen();
1857
+ const parsed = parseSectionTargetCreateSelector(selector);
1858
+ if (!parsed.ok) {
1859
+ throw sdkError("VALIDATION", parsed.error);
1860
+ }
1861
+ const document = await getDocumentByRef(this.store, this.config, ref, {});
1862
+ // Top-level response uri shares schema maxLength — reject before create.
1863
+ if (!isTransportBoundedCanonicalUri(document.uri)) {
1864
+ throw sdkError("VALIDATION", CANONICAL_URI_EXCEEDS_TRANSPORT_BOUNDS);
1865
+ }
1866
+ // Canonical identity from stored document — never from caller.
1867
+ const target = await createSectionTargetCore({
1868
+ content: document.content,
1869
+ uri: document.uri,
1870
+ ...parsed.value,
1871
+ });
1872
+ if (!target) {
1873
+ const sections = extractSections(document.content);
1874
+ const matched =
1875
+ parsed.value.anchor !== undefined
1876
+ ? sections.some((section) => section.anchor === parsed.value.anchor)
1877
+ : sections.some((section) => section.line === parsed.value.line);
1878
+ if (!matched) {
1879
+ throw sdkError("NOT_FOUND", "Section not found");
1880
+ }
1881
+ throw sdkError("VALIDATION", "Section target exceeds size bounds");
1882
+ }
1883
+ return projectSectionTargetCreateResult(document.uri, target);
1884
+ }
1885
+
1886
+ async resolveSectionTarget(
1887
+ ref: string,
1888
+ target: SectionTargetV1
1889
+ ): Promise<SectionTargetResolveResult> {
1890
+ this.assertOpen();
1891
+ const parsed = parseSectionTargetV1(target);
1892
+ if (!parsed.ok) {
1893
+ throw sdkError("VALIDATION", parsed.error);
1894
+ }
1895
+ const document = await getDocumentByRef(this.store, this.config, ref, {});
1896
+ // Top-level uri must fit schema — citation fail-closed does not repair it.
1897
+ if (!isTransportBoundedCanonicalUri(document.uri)) {
1898
+ throw sdkError("VALIDATION", CANONICAL_URI_EXCEEDS_TRANSPORT_BOUNDS);
1899
+ }
1900
+ const resolution = await resolveSectionTargetCore({
1901
+ content: document.content,
1902
+ target: parsed.value,
1903
+ uri: document.uri,
1904
+ });
1905
+ return projectSectionTargetResolveResult(document.uri, resolution);
1906
+ }
1907
+
1838
1908
  async close(): Promise<void> {
1839
1909
  if (this.closed) {
1840
1910
  return;
package/src/sdk/index.ts CHANGED
@@ -28,6 +28,7 @@ export type { IndexStatus } from "../store/types";
28
28
  export { GnoSdkError, sdkError } from "./errors";
29
29
  export { createGnoClient } from "./client";
30
30
  export type {
31
+ DocumentSection,
31
32
  GnoAskOptions,
32
33
  GnoCaptureOptions,
33
34
  GnoCaptureResult,
@@ -64,6 +65,10 @@ export type {
64
65
  KnowledgeImpactInput,
65
66
  KnowledgeImpactResult,
66
67
  ListKnowledgeChangesInput,
68
+ SectionTargetCreateResult,
69
+ SectionTargetCreateSelector,
70
+ SectionTargetResolveResult,
71
+ SectionTargetV1,
67
72
  } from "./types";
68
73
  export {
69
74
  ContextCapsuleContractError,
package/src/sdk/types.ts CHANGED
@@ -52,7 +52,13 @@ import type {
52
52
  RetrievalTraceListResult,
53
53
  RetrievalTracePurgeResult as RetrievalTraceManagementPurgeResult,
54
54
  } from "../core/retrieval-trace-management";
55
- import type { DocumentSection } from "../core/sections";
55
+ import type {
56
+ DocumentSection,
57
+ SectionTargetCreateResult,
58
+ SectionTargetCreateSelector,
59
+ SectionTargetResolveResult,
60
+ SectionTargetV1,
61
+ } from "../core/sections";
56
62
  import type { SyncResult } from "../ingestion";
57
63
  import type { DownloadPolicy } from "../llm/policy";
58
64
  import type {
@@ -67,10 +73,15 @@ import type { IndexStatus } from "../store/types";
67
73
  export type {
68
74
  AskResult,
69
75
  Config,
76
+ DocumentSection,
70
77
  DownloadPolicy,
71
78
  IndexStatus,
72
79
  SearchOptions,
73
80
  SearchResults,
81
+ SectionTargetCreateResult,
82
+ SectionTargetCreateSelector,
83
+ SectionTargetResolveResult,
84
+ SectionTargetV1,
74
85
  SyncResult,
75
86
  };
76
87
  export type { AskOptions, HybridSearchOptions } from "../pipeline/types";
@@ -345,5 +356,22 @@ export interface GnoClient {
345
356
  options: GnoDuplicateNoteOptions
346
357
  ): Promise<GnoRefactorNoteResult>;
347
358
  getSections(ref: string): Promise<DocumentSection[]>;
359
+ /**
360
+ * Create a durable SectionTargetV1 for a heading in a stored document.
361
+ * Provide exactly one of `anchor` or `line`. Canonical URI comes from the
362
+ * resolved stored document, never from the caller.
363
+ */
364
+ createSectionTarget(
365
+ ref: string,
366
+ selector: SectionTargetCreateSelector
367
+ ): Promise<SectionTargetCreateResult>;
368
+ /**
369
+ * Conservatively resolve a SectionTargetV1 against a stored document.
370
+ * Navigable results include citation evidence; ambiguous/stale/missing do not.
371
+ */
372
+ resolveSectionTarget(
373
+ ref: string,
374
+ target: SectionTargetV1
375
+ ): Promise<SectionTargetResolveResult>;
348
376
  close(): Promise<void>;
349
377
  }