@gmickel/gno 1.30.7 → 1.32.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 (71) hide show
  1. package/README.md +6 -5
  2. package/assets/skill/SKILL.md +25 -0
  3. package/assets/skill/mcp-reference.md +6 -0
  4. package/browser-extension/artifacts/{gno-browser-clipper-v1.30.7.zip → gno-browser-clipper-v1.32.0.zip} +0 -0
  5. package/browser-extension/artifacts/gno-browser-clipper-v1.32.0.zip.sha256 +1 -0
  6. package/browser-extension/dist/manifest.json +1 -1
  7. package/package.json +1 -1
  8. package/spec/cli.md +19 -0
  9. package/spec/db/schema.sql +55 -0
  10. package/spec/mcp.md +176 -11
  11. package/spec/output-schemas/file-refactor-apply-result.schema.json +305 -0
  12. package/spec/output-schemas/file-refactor-preview.schema.json +393 -0
  13. package/spec/output-schemas/section-target-create-result.schema.json +20 -0
  14. package/spec/output-schemas/section-target-resolve-result.schema.json +194 -0
  15. package/spec/output-schemas/section-target.schema.json +118 -0
  16. package/spec/output-schemas/section.schema.json +113 -0
  17. package/src/core/document-capabilities.ts +13 -0
  18. package/src/core/file-ops.ts +129 -1
  19. package/src/core/file-refactor-adapter.ts +329 -0
  20. package/src/core/file-refactor-apply-edits.ts +61 -0
  21. package/src/core/file-refactor-apply-fs.ts +512 -0
  22. package/src/core/file-refactor-apply-safety.ts +340 -0
  23. package/src/core/file-refactor-apply-validate.ts +401 -0
  24. package/src/core/file-refactor-contract.ts +486 -0
  25. package/src/core/file-refactor-destination.ts +123 -0
  26. package/src/core/file-refactor-from-snapshot.ts +148 -0
  27. package/src/core/file-refactor-journal-port.ts +150 -0
  28. package/src/core/file-refactor-journal.ts +347 -0
  29. package/src/core/file-refactor-paths.ts +60 -0
  30. package/src/core/file-refactor-plan-classify.ts +208 -0
  31. package/src/core/file-refactor-plan-validate.ts +169 -0
  32. package/src/core/file-refactor-planner-types.ts +62 -0
  33. package/src/core/file-refactor-planner.ts +423 -0
  34. package/src/core/file-refactor-resolve.ts +280 -0
  35. package/src/core/file-refactor-service.ts +468 -0
  36. package/src/core/file-refactors.ts +84 -56
  37. package/src/core/link-destination-parse.ts +275 -0
  38. package/src/core/link-inventory-markdown.ts +454 -0
  39. package/src/core/link-inventory-opaque.ts +244 -0
  40. package/src/core/link-inventory-types.ts +47 -0
  41. package/src/core/link-inventory.ts +182 -0
  42. package/src/core/link-relevance.ts +150 -0
  43. package/src/core/section-parse.ts +187 -0
  44. package/src/core/section-target-link.ts +154 -0
  45. package/src/core/section-target-resolve.ts +351 -0
  46. package/src/core/section-target-transport.ts +519 -0
  47. package/src/core/section-target.ts +263 -0
  48. package/src/core/sections.ts +60 -115
  49. package/src/mcp/AGENTS.md +1 -0
  50. package/src/mcp/CLAUDE.md +1 -0
  51. package/src/mcp/http-egress.ts +1 -0
  52. package/src/mcp/tools/index.ts +37 -17
  53. package/src/mcp/tools/sections.ts +512 -0
  54. package/src/mcp/tools/workspace-write.ts +215 -97
  55. package/src/sdk/client.ts +238 -116
  56. package/src/sdk/index.ts +12 -0
  57. package/src/sdk/types.ts +61 -3
  58. package/src/serve/file-refactor-http.ts +239 -0
  59. package/src/serve/public/components/RefactorImpactPreview.tsx +227 -0
  60. package/src/serve/public/globals.built.css +1 -1
  61. package/src/serve/public/lib/section-links.ts +189 -0
  62. package/src/serve/public/pages/DocView.tsx +395 -77
  63. package/src/serve/routes/api.ts +191 -104
  64. package/src/serve/routes/section-targets.ts +221 -0
  65. package/src/serve/server.ts +34 -0
  66. package/src/store/migrations/026-file-refactor-recovery-journal.ts +72 -0
  67. package/src/store/migrations/index.ts +2 -0
  68. package/src/store/sqlite/adapter.ts +452 -0
  69. package/src/store/sqlite/file-refactor-journal-store.ts +275 -0
  70. package/src/store/types.ts +84 -0
  71. package/browser-extension/artifacts/gno-browser-clipper-v1.30.7.zip.sha256 +0 -1
@@ -0,0 +1,519 @@
1
+ /**
2
+ * Closed validation and transport projection for section targets.
3
+ * Private helper — import public API from `./sections`.
4
+ *
5
+ * Shared by REST and SDK so response shapes stay identical without
6
+ * duplicating parser/resolver logic.
7
+ *
8
+ * @module src/core/section-target-transport
9
+ */
10
+
11
+ import {
12
+ SECTION_TARGET_BOUNDS,
13
+ SECTION_TARGET_SCHEMA_VERSION,
14
+ isBoundedSectionTarget,
15
+ type SectionTargetV1,
16
+ } from "./section-target";
17
+ import {
18
+ isNavigableSectionResolution,
19
+ type SectionResolution,
20
+ type SectionResolutionCandidate,
21
+ type SectionResolutionStatus,
22
+ } from "./section-target-resolve";
23
+
24
+ export type TransportValidationResult<T> =
25
+ | { ok: true; value: T }
26
+ | { ok: false; error: string };
27
+
28
+ export interface SectionTargetCreateSelector {
29
+ anchor?: string;
30
+ line?: number;
31
+ }
32
+
33
+ /** REST/SDK create response — canonical stored URI + target. */
34
+ export interface SectionTargetCreateResult {
35
+ uri: string;
36
+ target: SectionTargetV1;
37
+ }
38
+
39
+ /** Citation evidence exposed only for navigable resolutions. */
40
+ export interface SectionCitationV1 {
41
+ uri: string;
42
+ anchor: string;
43
+ title: string;
44
+ lineStart: number;
45
+ lineEnd: number;
46
+ sourceFingerprint: string;
47
+ }
48
+
49
+ /**
50
+ * Transport diagnostics. Candidates are filtered/projected to schema bounds
51
+ * without truncating identity strings; `candidateCount` is the pre-filter
52
+ * total and `candidatesTruncated` is true when any candidate was omitted.
53
+ */
54
+ export interface SectionResolutionDiagnostics {
55
+ reason?: string;
56
+ candidates?: SectionResolutionCandidate[];
57
+ candidateCount?: number;
58
+ candidatesTruncated?: boolean;
59
+ }
60
+
61
+ /**
62
+ * REST/SDK resolve response. Non-navigable results omit citation and any
63
+ * navigation fields.
64
+ */
65
+ export interface SectionTargetResolveResult {
66
+ uri: string;
67
+ status: SectionResolutionStatus;
68
+ currentFingerprint: string;
69
+ target: SectionTargetV1;
70
+ diagnostics: SectionResolutionDiagnostics;
71
+ citation?: SectionCitationV1;
72
+ }
73
+
74
+ /** Transport-only bounds for resolve diagnostics / citation projection. */
75
+ export const SECTION_TARGET_TRANSPORT_BOUNDS = {
76
+ diagnosticsCandidatesMaxItems: 32,
77
+ reasonMaxChars: 256,
78
+ titleMaxChars: SECTION_TARGET_BOUNDS.anchorMaxChars,
79
+ } as const;
80
+
81
+ /** Stable reason when a navigable citation cannot fit transport bounds. */
82
+ export const CITATION_EXCEEDS_TRANSPORT_BOUNDS =
83
+ "citation_exceeds_transport_bounds" as const;
84
+
85
+ /** Stable VALIDATION message when a stored canonical URI cannot fit transport. */
86
+ export const CANONICAL_URI_EXCEEDS_TRANSPORT_BOUNDS =
87
+ `Canonical document URI exceeds the maximum length of ${SECTION_TARGET_BOUNDS.uriMaxChars}` as const;
88
+
89
+ const FINGERPRINT_PATTERN = /^[a-f0-9]{64}$/;
90
+
91
+ const invalid = <T>(error: string): TransportValidationResult<T> => ({
92
+ ok: false,
93
+ error,
94
+ });
95
+ /**
96
+ * Snapshot only own data properties. Accessors, exotic prototypes, symbols,
97
+ * proxy traps, arrays, and unknown keys are rejected without invoking getters.
98
+ */
99
+ const closedRecord = (
100
+ value: unknown,
101
+ allowedKeys: readonly string[]
102
+ ): TransportValidationResult<Record<string, unknown>> => {
103
+ try {
104
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
105
+ return invalid("Expected a JSON object");
106
+ }
107
+ const prototype = Object.getPrototypeOf(value);
108
+ if (prototype !== Object.prototype && prototype !== null) {
109
+ return invalid("Expected a plain JSON object");
110
+ }
111
+ const keys = Reflect.ownKeys(value);
112
+ if (
113
+ keys.length > allowedKeys.length ||
114
+ keys.some((key) => typeof key !== "string" || !allowedKeys.includes(key))
115
+ ) {
116
+ return invalid("Object contains unknown fields");
117
+ }
118
+ const result: Record<string, unknown> = {};
119
+ for (const key of keys) {
120
+ if (typeof key !== "string") return invalid("Invalid object field");
121
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
122
+ if (!descriptor || !("value" in descriptor)) {
123
+ return invalid("Accessor fields are not allowed");
124
+ }
125
+ result[key] = descriptor.value;
126
+ }
127
+ return { ok: true, value: result };
128
+ } catch {
129
+ return invalid("Unreadable input object");
130
+ }
131
+ };
132
+
133
+ const parseNonEmptyString = (
134
+ value: unknown,
135
+ field: string,
136
+ maxChars: number
137
+ ): TransportValidationResult<string> => {
138
+ if (typeof value !== "string") {
139
+ return invalid(`${field} must be a string`);
140
+ }
141
+ if (value.length < 1) {
142
+ return invalid(`${field} must be a non-empty string`);
143
+ }
144
+ if (value.length > maxChars) {
145
+ return invalid(`${field} exceeds the maximum length of ${maxChars}`);
146
+ }
147
+ return { ok: true, value };
148
+ };
149
+
150
+ const parseBoundedString = (
151
+ value: unknown,
152
+ field: string,
153
+ maxChars: number
154
+ ): TransportValidationResult<string> => {
155
+ if (typeof value !== "string") {
156
+ return invalid(`${field} must be a string`);
157
+ }
158
+ if (value.length > maxChars) {
159
+ return invalid(`${field} exceeds the maximum length of ${maxChars}`);
160
+ }
161
+ return { ok: true, value };
162
+ };
163
+
164
+ const parsePositiveInt = (
165
+ value: unknown,
166
+ field: string
167
+ ): TransportValidationResult<number> => {
168
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) {
169
+ return invalid(`${field} must be a positive safe integer`);
170
+ }
171
+ return { ok: true, value };
172
+ };
173
+
174
+ const parseNonNegativeInt = (
175
+ value: unknown,
176
+ field: string
177
+ ): TransportValidationResult<number> => {
178
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
179
+ return invalid(`${field} must be a non-negative safe integer`);
180
+ }
181
+ return { ok: true, value };
182
+ };
183
+ /**
184
+ * Require exactly one selector: `anchor` XOR `line`.
185
+ * Rejects unknown fields and empty/oversized values.
186
+ */
187
+ export const parseSectionTargetCreateSelector = (
188
+ value: unknown
189
+ ): TransportValidationResult<SectionTargetCreateSelector> => {
190
+ const record = closedRecord(value, ["anchor", "line"]);
191
+ if (!record.ok) return record;
192
+
193
+ const hasAnchor = Object.hasOwn(record.value, "anchor");
194
+ const hasLine = Object.hasOwn(record.value, "line");
195
+ if (hasAnchor === hasLine) {
196
+ return invalid("Provide exactly one of anchor or line");
197
+ }
198
+
199
+ if (hasAnchor) {
200
+ const anchor = parseNonEmptyString(
201
+ record.value.anchor,
202
+ "anchor",
203
+ SECTION_TARGET_BOUNDS.anchorMaxChars
204
+ );
205
+ if (!anchor.ok) return anchor;
206
+ return { ok: true, value: { anchor: anchor.value } };
207
+ }
208
+
209
+ const line = parsePositiveInt(record.value.line, "line");
210
+ if (!line.ok) return line;
211
+ return { ok: true, value: { line: line.value } };
212
+ };
213
+
214
+ /**
215
+ * Validate an untrusted SectionTargetV1 before core resolve.
216
+ * Enforces closed shape, bounds, fingerprint format, and serialized size.
217
+ */
218
+ export const parseSectionTargetV1 = (
219
+ value: unknown
220
+ ): TransportValidationResult<SectionTargetV1> => {
221
+ const record = closedRecord(value, [
222
+ "schemaVersion",
223
+ "document",
224
+ "anchor",
225
+ "headingPath",
226
+ "occurrence",
227
+ "quote",
228
+ "sourceFingerprint",
229
+ "hints",
230
+ ]);
231
+ if (!record.ok) return record;
232
+
233
+ if (record.value.schemaVersion !== SECTION_TARGET_SCHEMA_VERSION) {
234
+ return invalid('schemaVersion must be "1"');
235
+ }
236
+
237
+ const document = closedRecord(record.value.document, ["uri"]);
238
+ if (!document.ok) return invalid(`document: ${document.error}`);
239
+ const uri = parseNonEmptyString(
240
+ document.value.uri,
241
+ "document.uri",
242
+ SECTION_TARGET_BOUNDS.uriMaxChars
243
+ );
244
+ if (!uri.ok) return uri;
245
+
246
+ const anchor = parseNonEmptyString(
247
+ record.value.anchor,
248
+ "anchor",
249
+ SECTION_TARGET_BOUNDS.anchorMaxChars
250
+ );
251
+ if (!anchor.ok) return anchor;
252
+
253
+ if (!Array.isArray(record.value.headingPath)) {
254
+ return invalid("headingPath must be an array");
255
+ }
256
+ if (
257
+ record.value.headingPath.length < 1 ||
258
+ record.value.headingPath.length > SECTION_TARGET_BOUNDS.headingPathMaxItems
259
+ ) {
260
+ return invalid(
261
+ `headingPath must contain 1 to ${SECTION_TARGET_BOUNDS.headingPathMaxItems} items`
262
+ );
263
+ }
264
+ const headingPath: string[] = [];
265
+ for (const [index, item] of record.value.headingPath.entries()) {
266
+ const parsed = parseNonEmptyString(
267
+ item,
268
+ `headingPath[${index}]`,
269
+ SECTION_TARGET_BOUNDS.headingPathItemMaxChars
270
+ );
271
+ if (!parsed.ok) return parsed;
272
+ headingPath.push(parsed.value);
273
+ }
274
+
275
+ const occurrence = parsePositiveInt(record.value.occurrence, "occurrence");
276
+ if (!occurrence.ok) return occurrence;
277
+
278
+ const quoteRecord = closedRecord(record.value.quote, [
279
+ "exact",
280
+ "prefix",
281
+ "suffix",
282
+ ]);
283
+ if (!quoteRecord.ok) return invalid(`quote: ${quoteRecord.error}`);
284
+ const exact = parseBoundedString(
285
+ quoteRecord.value.exact,
286
+ "quote.exact",
287
+ SECTION_TARGET_BOUNDS.exactMaxChars
288
+ );
289
+ if (!exact.ok) return exact;
290
+ const prefix = parseBoundedString(
291
+ quoteRecord.value.prefix,
292
+ "quote.prefix",
293
+ SECTION_TARGET_BOUNDS.prefixMaxChars
294
+ );
295
+ if (!prefix.ok) return prefix;
296
+ const suffix = parseBoundedString(
297
+ quoteRecord.value.suffix,
298
+ "quote.suffix",
299
+ SECTION_TARGET_BOUNDS.suffixMaxChars
300
+ );
301
+ if (!suffix.ok) return suffix;
302
+
303
+ if (
304
+ typeof record.value.sourceFingerprint !== "string" ||
305
+ !FINGERPRINT_PATTERN.test(record.value.sourceFingerprint)
306
+ ) {
307
+ return invalid("sourceFingerprint must be a lowercase SHA-256 hex digest");
308
+ }
309
+
310
+ const hintsRecord = closedRecord(record.value.hints, [
311
+ "line",
312
+ "startOffset",
313
+ "endOffset",
314
+ ]);
315
+ if (!hintsRecord.ok) return invalid(`hints: ${hintsRecord.error}`);
316
+ const hintsLine = parsePositiveInt(hintsRecord.value.line, "hints.line");
317
+ if (!hintsLine.ok) return hintsLine;
318
+ const startOffset = parseNonNegativeInt(
319
+ hintsRecord.value.startOffset,
320
+ "hints.startOffset"
321
+ );
322
+ if (!startOffset.ok) return startOffset;
323
+ const endOffset = parseNonNegativeInt(
324
+ hintsRecord.value.endOffset,
325
+ "hints.endOffset"
326
+ );
327
+ if (!endOffset.ok) return endOffset;
328
+ if (endOffset.value < startOffset.value) {
329
+ return invalid("hints.endOffset must be >= hints.startOffset");
330
+ }
331
+
332
+ const target: SectionTargetV1 = {
333
+ schemaVersion: SECTION_TARGET_SCHEMA_VERSION,
334
+ document: { uri: uri.value },
335
+ anchor: anchor.value,
336
+ headingPath,
337
+ occurrence: occurrence.value,
338
+ quote: {
339
+ exact: exact.value,
340
+ prefix: prefix.value,
341
+ suffix: suffix.value,
342
+ },
343
+ sourceFingerprint: record.value.sourceFingerprint,
344
+ hints: {
345
+ line: hintsLine.value,
346
+ startOffset: startOffset.value,
347
+ endOffset: endOffset.value,
348
+ },
349
+ };
350
+
351
+ // Serialized-byte budget is owned by isBoundedSectionTarget — no recheck.
352
+ if (!isBoundedSectionTarget(target)) {
353
+ return invalid("Section target exceeds size bounds");
354
+ }
355
+
356
+ return { ok: true, value: target };
357
+ };
358
+ /** Resolve-body wrapper: `{ target: SectionTargetV1 }` only. */
359
+ export const parseSectionTargetResolveBody = (
360
+ value: unknown
361
+ ): TransportValidationResult<{ target: SectionTargetV1 }> => {
362
+ const record = closedRecord(value, ["target"]);
363
+ if (!record.ok) return record;
364
+ if (!Object.hasOwn(record.value, "target")) {
365
+ return invalid("target is required");
366
+ }
367
+ const target = parseSectionTargetV1(record.value.target);
368
+ if (!target.ok) return target;
369
+ return { ok: true, value: { target: target.value } };
370
+ };
371
+
372
+ export const projectSectionTargetCreateResult = (
373
+ uri: string,
374
+ target: SectionTargetV1
375
+ ): SectionTargetCreateResult => ({
376
+ uri,
377
+ target,
378
+ });
379
+
380
+ const isBoundedTransportString = (
381
+ value: string,
382
+ minChars: number,
383
+ maxChars: number
384
+ ): boolean => value.length >= minChars && value.length <= maxChars;
385
+
386
+ const isBoundedPositiveSafeInt = (value: number): boolean =>
387
+ Number.isSafeInteger(value) && value >= 1;
388
+
389
+ /**
390
+ * True when a canonical document URI fits top-level transport `uri` bounds.
391
+ * Adapters must reject before create/projection — never truncate or rewrite.
392
+ */
393
+ export const isTransportBoundedCanonicalUri = (uri: string): boolean =>
394
+ isBoundedTransportString(uri, 1, SECTION_TARGET_BOUNDS.uriMaxChars);
395
+
396
+ /** True when a candidate fits transport schema bounds without truncation. */
397
+ export const isTransportBoundedCandidate = (
398
+ candidate: SectionResolutionCandidate
399
+ ): boolean =>
400
+ isBoundedTransportString(
401
+ candidate.anchor,
402
+ 1,
403
+ SECTION_TARGET_BOUNDS.anchorMaxChars
404
+ ) &&
405
+ isBoundedTransportString(
406
+ candidate.title,
407
+ 1,
408
+ SECTION_TARGET_TRANSPORT_BOUNDS.titleMaxChars
409
+ ) &&
410
+ isBoundedPositiveSafeInt(candidate.line) &&
411
+ isBoundedPositiveSafeInt(candidate.occurrence) &&
412
+ candidate.headingPath.length >= 1 &&
413
+ candidate.headingPath.length <= SECTION_TARGET_BOUNDS.headingPathMaxItems &&
414
+ candidate.headingPath.every((item) =>
415
+ isBoundedTransportString(
416
+ item,
417
+ 1,
418
+ SECTION_TARGET_BOUNDS.headingPathItemMaxChars
419
+ )
420
+ );
421
+
422
+ /** True when a citation fits transport schema bounds without truncation. */
423
+ export const isTransportBoundedCitation = (
424
+ citation: SectionCitationV1
425
+ ): boolean =>
426
+ isBoundedTransportString(
427
+ citation.uri,
428
+ 1,
429
+ SECTION_TARGET_BOUNDS.uriMaxChars
430
+ ) &&
431
+ isBoundedTransportString(
432
+ citation.anchor,
433
+ 1,
434
+ SECTION_TARGET_BOUNDS.anchorMaxChars
435
+ ) &&
436
+ isBoundedTransportString(
437
+ citation.title,
438
+ 1,
439
+ SECTION_TARGET_TRANSPORT_BOUNDS.titleMaxChars
440
+ ) &&
441
+ isBoundedPositiveSafeInt(citation.lineStart) &&
442
+ isBoundedPositiveSafeInt(citation.lineEnd) &&
443
+ citation.lineEnd >= citation.lineStart &&
444
+ FINGERPRINT_PATTERN.test(citation.sourceFingerprint);
445
+
446
+ const projectDiagnostics = (
447
+ resolution: SectionResolution
448
+ ): SectionResolutionDiagnostics => {
449
+ const diagnostics: SectionResolutionDiagnostics = {};
450
+ if (
451
+ resolution.reason !== undefined &&
452
+ resolution.reason.length >= 1 &&
453
+ resolution.reason.length <= SECTION_TARGET_TRANSPORT_BOUNDS.reasonMaxChars
454
+ ) {
455
+ diagnostics.reason = resolution.reason;
456
+ }
457
+
458
+ if (resolution.candidates === undefined) {
459
+ return diagnostics;
460
+ }
461
+
462
+ const candidateCount = resolution.candidates.length;
463
+ const fitting = resolution.candidates.filter(isTransportBoundedCandidate);
464
+ const emitted = fitting.slice(
465
+ 0,
466
+ SECTION_TARGET_TRANSPORT_BOUNDS.diagnosticsCandidatesMaxItems
467
+ );
468
+ diagnostics.candidates = emitted;
469
+ diagnostics.candidateCount = candidateCount;
470
+ diagnostics.candidatesTruncated = emitted.length < candidateCount;
471
+ return diagnostics;
472
+ };
473
+
474
+ export const projectSectionTargetResolveResult = (
475
+ uri: string,
476
+ resolution: SectionResolution
477
+ ): SectionTargetResolveResult => {
478
+ const diagnostics = projectDiagnostics(resolution);
479
+
480
+ if (isNavigableSectionResolution(resolution)) {
481
+ const citation: SectionCitationV1 = {
482
+ uri,
483
+ anchor: resolution.section.anchor,
484
+ title: resolution.section.title,
485
+ lineStart: resolution.section.line,
486
+ lineEnd: resolution.section.endLine,
487
+ sourceFingerprint: resolution.currentFingerprint,
488
+ };
489
+ if (isTransportBoundedCitation(citation)) {
490
+ return {
491
+ uri,
492
+ status: resolution.status,
493
+ currentFingerprint: resolution.currentFingerprint,
494
+ target: resolution.target,
495
+ diagnostics,
496
+ citation,
497
+ };
498
+ }
499
+ // Fail closed: never truncate identity or emit an unbound citation.
500
+ return {
501
+ uri,
502
+ status: "stale",
503
+ currentFingerprint: resolution.currentFingerprint,
504
+ target: resolution.target,
505
+ diagnostics: {
506
+ ...diagnostics,
507
+ reason: CITATION_EXCEEDS_TRANSPORT_BOUNDS,
508
+ },
509
+ };
510
+ }
511
+
512
+ return {
513
+ uri,
514
+ status: resolution.status,
515
+ currentFingerprint: resolution.currentFingerprint,
516
+ target: resolution.target,
517
+ diagnostics,
518
+ };
519
+ };