@gmickel/gno 1.27.1 → 1.28.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 (83) hide show
  1. package/README.md +2 -2
  2. package/assets/skill/SKILL.md +26 -1
  3. package/browser-extension/artifacts/{gno-browser-clipper-v1.27.1.zip → gno-browser-clipper-v1.28.0.zip} +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.28.0.zip.sha256 +1 -0
  5. package/browser-extension/dist/manifest.json +1 -1
  6. package/package.json +2 -2
  7. package/spec/cli.md +41 -4
  8. package/spec/db/schema.sql +12 -0
  9. package/spec/mcp.md +5 -0
  10. package/spec/output-schemas/ask.schema.json +125 -0
  11. package/spec/output-schemas/context-capsule-v1.schema.json +94 -1
  12. package/spec/output-schemas/get.schema.json +94 -0
  13. package/spec/output-schemas/mcp-job-status.schema.json +28 -0
  14. package/spec/output-schemas/multi-get.schema.json +128 -0
  15. package/spec/output-schemas/record-import.schema.json +193 -0
  16. package/spec/output-schemas/search-result.schema.json +94 -0
  17. package/spec/output-schemas/search-results.schema.json +125 -0
  18. package/spec/project-profile.schema.json +66 -0
  19. package/src/app/context-format.ts +1 -1
  20. package/src/cli/commands/get.ts +12 -2
  21. package/src/cli/commands/index-cmd.ts +13 -0
  22. package/src/cli/commands/multi-get.ts +9 -2
  23. package/src/cli/commands/shared.ts +28 -0
  24. package/src/cli/commands/update.ts +5 -0
  25. package/src/cli/program.ts +4 -0
  26. package/src/config/project-profile.ts +2 -0
  27. package/src/config/types.ts +16 -0
  28. package/src/converters/adapters/browser-export/adapter.ts +199 -0
  29. package/src/converters/adapters/browser-export/formats.ts +358 -0
  30. package/src/converters/adapters/email/adapter.ts +429 -0
  31. package/src/converters/adapters/email/html.ts +162 -0
  32. package/src/converters/adapters/email/mime.ts +454 -0
  33. package/src/converters/adapters/email/parameters.ts +77 -0
  34. package/src/converters/adapters/ical/adapter.ts +475 -0
  35. package/src/converters/adapters/ical/recurrence.ts +186 -0
  36. package/src/converters/adapters/jsonl/adapter.ts +238 -0
  37. package/src/converters/adapters/jsonl/config.ts +105 -0
  38. package/src/converters/adapters/shared/html-text.ts +165 -0
  39. package/src/converters/adapters/shared/record-utils.ts +79 -0
  40. package/src/converters/adapters/shared/utf8-lines.ts +141 -0
  41. package/src/converters/adapters/transcript/adapter.ts +295 -0
  42. package/src/converters/adapters/transcript/json.ts +184 -0
  43. package/src/converters/adapters/transcript/model.ts +171 -0
  44. package/src/converters/adapters/transcript/text.ts +58 -0
  45. package/src/converters/adapters/transcript/timed.ts +152 -0
  46. package/src/converters/index.ts +11 -1
  47. package/src/converters/mime.ts +8 -0
  48. package/src/converters/pipeline.ts +15 -1
  49. package/src/converters/registry.ts +37 -1
  50. package/src/converters/types.ts +136 -0
  51. package/src/core/context-capsule-schema.ts +87 -0
  52. package/src/core/context-capsule.ts +20 -0
  53. package/src/core/context-evidence.ts +7 -1
  54. package/src/core/document-capabilities.ts +12 -0
  55. package/src/core/project-profile-apply-state.ts +5 -0
  56. package/src/core/project-profile.ts +4 -0
  57. package/src/core/record-metadata.ts +49 -0
  58. package/src/ingestion/record-adapter-canonical.ts +433 -0
  59. package/src/ingestion/record-adapter.ts +437 -0
  60. package/src/ingestion/record-container.ts +688 -0
  61. package/src/ingestion/record-path.ts +20 -0
  62. package/src/ingestion/record-sync.ts +70 -0
  63. package/src/ingestion/sync.ts +228 -36
  64. package/src/ingestion/types.ts +69 -1
  65. package/src/ingestion/walker.ts +31 -11
  66. package/src/mcp/tools/get.ts +10 -2
  67. package/src/mcp/tools/multi-get.ts +9 -2
  68. package/src/mcp/tools/workspace-write.ts +2 -0
  69. package/src/pipeline/filters.ts +1 -1
  70. package/src/pipeline/graph-retrieval.ts +7 -4
  71. package/src/pipeline/hybrid.ts +7 -4
  72. package/src/pipeline/result-context.ts +12 -4
  73. package/src/pipeline/search.ts +11 -4
  74. package/src/pipeline/types.ts +3 -0
  75. package/src/pipeline/vsearch.ts +26 -8
  76. package/src/sdk/documents.ts +13 -5
  77. package/src/serve/browse-tree.ts +4 -2
  78. package/src/serve/routes/api.ts +63 -65
  79. package/src/store/migrations/022-record-export-lineage.ts +42 -0
  80. package/src/store/migrations/index.ts +2 -0
  81. package/src/store/sqlite/adapter.ts +114 -7
  82. package/src/store/types.ts +40 -0
  83. package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip.sha256 +0 -1
@@ -0,0 +1,433 @@
1
+ import type {
2
+ RecordAdapter,
3
+ RecordAdapterFailure,
4
+ RecordAdapterFailureCode,
5
+ RecordAdapterInput,
6
+ RecordAdapterRecord,
7
+ RecordAnchor,
8
+ RecordMetadata,
9
+ } from "../converters/types";
10
+
11
+ import { canonicalize, mirrorHash } from "../converters/canonicalize";
12
+ import { RECORD_METADATA_LIMITS } from "../converters/types";
13
+
14
+ const HASH_PATTERN = /^[a-f\d]{64}$/;
15
+ const CONTROL_PATTERN = new RegExp(
16
+ `[${String.fromCharCode(0)}-${String.fromCharCode(31)}${String.fromCharCode(127)}]`
17
+ );
18
+ const CONTROL_REPLACE_PATTERN = new RegExp(CONTROL_PATTERN.source, "g");
19
+ const WINDOWS_ABSOLUTE_PATTERN = /^[a-zA-Z]:[\\/]/;
20
+ const MAX_ID_CHARS = 512;
21
+ const MAX_LOCATOR_CHARS = 512;
22
+ const MAX_ERROR_CHARS = 240;
23
+ const RECORD_ANCHOR_KINDS = new Set<RecordAnchor["kind"]>([
24
+ "line",
25
+ "cue",
26
+ "timestamp",
27
+ "message",
28
+ "event",
29
+ "record",
30
+ ]);
31
+ const RECORD_ATTACHMENT_DISPOSITIONS = new Set<
32
+ NonNullable<NonNullable<RecordMetadata["attachments"]>[number]["disposition"]>
33
+ >(["inline", "attachment"]);
34
+
35
+ export interface CanonicalRecord {
36
+ recordKey: string;
37
+ stableId: string;
38
+ sourceLocator: string;
39
+ sourceHash: string;
40
+ mirrorHash: string;
41
+ markdown: string;
42
+ adapterId: string;
43
+ adapterVersion: string;
44
+ adapterFingerprint: string;
45
+ title?: string;
46
+ languageHint?: string;
47
+ metadata?: RecordMetadata;
48
+ anchors?: RecordAnchor[];
49
+ }
50
+
51
+ export interface AccountedCanonicalRecord extends CanonicalRecord {
52
+ accountingChars: number;
53
+ metadataChars: number;
54
+ }
55
+
56
+ export interface RecordAdapterIdentity {
57
+ id: string;
58
+ version: string;
59
+ }
60
+
61
+ const hashText = (value: string): string => {
62
+ const hasher = new Bun.CryptoHasher("sha256");
63
+ hasher.update(value);
64
+ return hasher.digest("hex");
65
+ };
66
+
67
+ const canonicalJsonValue = (value: unknown): unknown => {
68
+ if (Array.isArray(value)) return value.map(canonicalJsonValue);
69
+ if (value && typeof value === "object") {
70
+ const result: Record<string, unknown> = {};
71
+ for (const key of Object.keys(value).sort()) {
72
+ const child = canonicalJsonValue((value as Record<string, unknown>)[key]);
73
+ if (child !== undefined) result[key] = child;
74
+ }
75
+ return result;
76
+ }
77
+ return value;
78
+ };
79
+
80
+ const normalizeText = (value: string): string => value.normalize("NFC").trim();
81
+
82
+ export const normalizeRecordAdapterIdentity = (
83
+ adapter: Pick<RecordAdapter, "id" | "version">
84
+ ): RecordAdapterIdentity => {
85
+ const id =
86
+ typeof adapter.id === "string" ? normalizeText(adapter.id) : undefined;
87
+ const version =
88
+ typeof adapter.version === "string"
89
+ ? normalizeText(adapter.version)
90
+ : undefined;
91
+ if (
92
+ !id ||
93
+ id.length > RECORD_METADATA_LIMITS.maxAdapterIdChars ||
94
+ CONTROL_PATTERN.test(id)
95
+ ) {
96
+ throw new Error("invalid record adapter ID");
97
+ }
98
+ if (
99
+ !version ||
100
+ version.length > RECORD_METADATA_LIMITS.maxAdapterVersionChars ||
101
+ CONTROL_PATTERN.test(version)
102
+ ) {
103
+ throw new Error("invalid record adapter version");
104
+ }
105
+ return { id, version };
106
+ };
107
+
108
+ export const recordAdapterFingerprint = (adapter: RecordAdapter): string => {
109
+ const identity = normalizeRecordAdapterIdentity(adapter);
110
+ return hashText(
111
+ `gno-record-adapter-v1\0${identity.id}\0${identity.version}\0${adapter.configurationFingerprint ?? "default"}`
112
+ );
113
+ };
114
+
115
+ const normalizeStableId = (value: string): string => {
116
+ const normalized = normalizeText(value);
117
+ if (
118
+ normalized.length === 0 ||
119
+ normalized.length > MAX_ID_CHARS ||
120
+ CONTROL_PATTERN.test(normalized) ||
121
+ normalized.startsWith("/") ||
122
+ WINDOWS_ABSOLUTE_PATTERN.test(normalized)
123
+ ) {
124
+ throw new Error("invalid stable ID");
125
+ }
126
+ return normalized;
127
+ };
128
+
129
+ const normalizeLocator = (value: string): string => {
130
+ const normalized = normalizeText(value);
131
+ const pathSegments = normalized.replaceAll("\\", "/").split("/");
132
+ if (
133
+ normalized.length === 0 ||
134
+ normalized.length > MAX_LOCATOR_CHARS ||
135
+ CONTROL_PATTERN.test(normalized) ||
136
+ normalized.startsWith("/") ||
137
+ WINDOWS_ABSOLUTE_PATTERN.test(normalized) ||
138
+ pathSegments.includes("..")
139
+ ) {
140
+ throw new Error("invalid source locator");
141
+ }
142
+ return normalized;
143
+ };
144
+
145
+ const normalizeOptionalText = (
146
+ value: string | undefined
147
+ ): string | undefined => {
148
+ if (value === undefined) return undefined;
149
+ const normalized = normalizeText(value).replace(CONTROL_REPLACE_PATTERN, "");
150
+ return normalized || undefined;
151
+ };
152
+
153
+ const boundedText = (
154
+ value: string | undefined,
155
+ maxChars: number
156
+ ): string | undefined => {
157
+ const normalized = normalizeOptionalText(value);
158
+ if (normalized && normalized.length > maxChars) {
159
+ throw new Error("record metadata out of bounds");
160
+ }
161
+ return normalized;
162
+ };
163
+
164
+ const normalizeSha256 = (value: string | undefined): string | undefined => {
165
+ if (value === undefined) return undefined;
166
+ if (!HASH_PATTERN.test(value)) {
167
+ throw new Error("record metadata out of bounds");
168
+ }
169
+ return value;
170
+ };
171
+
172
+ const normalizeFailureReference = (
173
+ value: string | undefined
174
+ ): string | undefined => {
175
+ const normalized = normalizeOptionalText(value);
176
+ if (!normalized) return undefined;
177
+ const pathSegments = normalized.replaceAll("\\", "/").split("/");
178
+ if (
179
+ normalized.startsWith("/") ||
180
+ WINDOWS_ABSOLUTE_PATTERN.test(normalized) ||
181
+ pathSegments.includes("..")
182
+ ) {
183
+ return undefined;
184
+ }
185
+ return normalized.slice(0, MAX_LOCATOR_CHARS);
186
+ };
187
+
188
+ const normalizeMetadata = (
189
+ metadata: RecordMetadata | undefined
190
+ ): RecordMetadata | undefined => {
191
+ if (!metadata) return undefined;
192
+ const normalizeList = (
193
+ values: string[] | undefined,
194
+ maxChars: number
195
+ ): string[] | undefined => {
196
+ if (!values) return undefined;
197
+ if (values.length > RECORD_METADATA_LIMITS.maxItems) {
198
+ throw new Error("record metadata out of bounds");
199
+ }
200
+ return values
201
+ .map((value) => boundedText(value, maxChars))
202
+ .filter((value): value is string => Boolean(value));
203
+ };
204
+ const dateEntries = Object.entries(metadata.dateFields ?? {});
205
+ if (dateEntries.length > RECORD_METADATA_LIMITS.maxItems) {
206
+ throw new Error("record metadata out of bounds");
207
+ }
208
+ const dateFields = metadata.dateFields
209
+ ? Object.fromEntries(
210
+ dateEntries
211
+ .map(([key, value]) => [
212
+ boundedText(key, RECORD_METADATA_LIMITS.maxDateFieldKeyChars),
213
+ boundedText(value, RECORD_METADATA_LIMITS.maxDateFieldValueChars),
214
+ ])
215
+ .filter(
216
+ (entry): entry is [string, string] =>
217
+ Boolean(entry[0]) && Boolean(entry[1])
218
+ )
219
+ .sort(([left], [right]) => left.localeCompare(right))
220
+ )
221
+ : undefined;
222
+ if (
223
+ metadata.attachments &&
224
+ metadata.attachments.length > RECORD_METADATA_LIMITS.maxItems
225
+ ) {
226
+ throw new Error("record metadata out of bounds");
227
+ }
228
+ return {
229
+ author: boundedText(metadata.author, RECORD_METADATA_LIMITS.maxPersonChars),
230
+ participants: normalizeList(
231
+ metadata.participants,
232
+ RECORD_METADATA_LIMITS.maxPersonChars
233
+ ),
234
+ categories: normalizeList(
235
+ metadata.categories,
236
+ RECORD_METADATA_LIMITS.maxCategoryChars
237
+ ),
238
+ dateFields,
239
+ messageId: boundedText(
240
+ metadata.messageId,
241
+ RECORD_METADATA_LIMITS.maxIdentifierChars
242
+ ),
243
+ inReplyTo: boundedText(
244
+ metadata.inReplyTo,
245
+ RECORD_METADATA_LIMITS.maxIdentifierChars
246
+ ),
247
+ references: normalizeList(
248
+ metadata.references,
249
+ RECORD_METADATA_LIMITS.maxIdentifierChars
250
+ ),
251
+ threadId: boundedText(
252
+ metadata.threadId,
253
+ RECORD_METADATA_LIMITS.maxIdentifierChars
254
+ ),
255
+ eventId: boundedText(
256
+ metadata.eventId,
257
+ RECORD_METADATA_LIMITS.maxIdentifierChars
258
+ ),
259
+ sessionId: boundedText(
260
+ metadata.sessionId,
261
+ RECORD_METADATA_LIMITS.maxIdentifierChars
262
+ ),
263
+ attachments: metadata.attachments?.map((attachment) => {
264
+ if (
265
+ attachment.bytes !== undefined &&
266
+ (!Number.isSafeInteger(attachment.bytes) || attachment.bytes < 0)
267
+ ) {
268
+ throw new Error("record metadata out of bounds");
269
+ }
270
+ if (
271
+ attachment.disposition !== undefined &&
272
+ !RECORD_ATTACHMENT_DISPOSITIONS.has(attachment.disposition)
273
+ ) {
274
+ throw new Error("record metadata out of bounds");
275
+ }
276
+ return {
277
+ name:
278
+ boundedText(
279
+ attachment.name,
280
+ RECORD_METADATA_LIMITS.maxAttachmentNameChars
281
+ ) ?? "attachment",
282
+ mime: boundedText(
283
+ attachment.mime,
284
+ RECORD_METADATA_LIMITS.maxAttachmentMimeChars
285
+ ),
286
+ bytes: attachment.bytes,
287
+ disposition: attachment.disposition,
288
+ sha256: normalizeSha256(attachment.sha256),
289
+ };
290
+ }),
291
+ };
292
+ };
293
+
294
+ const normalizeAnchors = (
295
+ anchors: RecordAnchor[] | undefined
296
+ ): RecordAnchor[] | undefined => {
297
+ if (!anchors) return undefined;
298
+ if (anchors.length > RECORD_METADATA_LIMITS.maxItems) {
299
+ throw new Error("record metadata out of bounds");
300
+ }
301
+ return anchors.map((anchor) => {
302
+ if (!RECORD_ANCHOR_KINDS.has(anchor.kind)) {
303
+ throw new Error("record metadata out of bounds");
304
+ }
305
+ return {
306
+ kind: anchor.kind,
307
+ value:
308
+ boundedText(anchor.value, RECORD_METADATA_LIMITS.maxAnchorChars) ??
309
+ "unknown",
310
+ endValue: boundedText(
311
+ anchor.endValue,
312
+ RECORD_METADATA_LIMITS.maxAnchorChars
313
+ ),
314
+ };
315
+ });
316
+ };
317
+
318
+ export const recordKeyFor = (adapterId: string, stableId: string): string =>
319
+ hashText(`gno-record-v1\0${adapterId}\0${normalizeStableId(stableId)}`);
320
+
321
+ export const safeFailure = (
322
+ input: RecordAdapterInput,
323
+ code: RecordAdapterFailureCode,
324
+ message: string,
325
+ retryable: boolean,
326
+ stableId?: string,
327
+ sourceLocator?: string
328
+ ): RecordAdapterFailure => {
329
+ const withoutPath = message.replaceAll(input.sourcePath, "<source>");
330
+ const sanitized = withoutPath
331
+ .replace(CONTROL_REPLACE_PATTERN, " ")
332
+ .slice(0, MAX_ERROR_CHARS);
333
+ return {
334
+ code,
335
+ message: sanitized || "Record adapter failure.",
336
+ retryable,
337
+ stableId: normalizeFailureReference(stableId),
338
+ sourceLocator: normalizeFailureReference(sourceLocator),
339
+ };
340
+ };
341
+
342
+ export const canonicalRecord = (
343
+ adapter: RecordAdapter,
344
+ record: RecordAdapterRecord
345
+ ): AccountedCanonicalRecord => {
346
+ const adapterIdentity = normalizeRecordAdapterIdentity(adapter);
347
+ const stableId = normalizeStableId(record.stableId);
348
+ const sourceLocator = normalizeLocator(record.sourceLocator);
349
+ const markdown = canonicalize(record.markdown);
350
+ const metadata = normalizeMetadata(record.metadata);
351
+ const anchors = normalizeAnchors(record.anchors);
352
+ const title = boundedText(record.title, RECORD_METADATA_LIMITS.maxTitleChars);
353
+ const languageHint = boundedText(
354
+ record.languageHint,
355
+ RECORD_METADATA_LIMITS.maxLanguageHintChars
356
+ );
357
+ const derivedSource = JSON.stringify(
358
+ canonicalJsonValue({
359
+ anchors,
360
+ languageHint,
361
+ markdown,
362
+ metadata,
363
+ sourceLocator,
364
+ stableId,
365
+ title,
366
+ })
367
+ );
368
+ const metadataChars = JSON.stringify(
369
+ canonicalJsonValue({
370
+ anchors,
371
+ languageHint,
372
+ metadata,
373
+ sourceLocator,
374
+ stableId,
375
+ title,
376
+ })
377
+ ).length;
378
+ if (
379
+ record.sourceHash !== undefined &&
380
+ !HASH_PATTERN.test(record.sourceHash)
381
+ ) {
382
+ throw new Error("invalid source hash");
383
+ }
384
+ return {
385
+ recordKey: recordKeyFor(adapterIdentity.id, stableId),
386
+ stableId,
387
+ sourceLocator,
388
+ sourceHash:
389
+ record.sourceHash ?? hashText(`gno-record-source-v1\0${derivedSource}`),
390
+ mirrorHash: mirrorHash(markdown),
391
+ markdown,
392
+ adapterId: adapterIdentity.id,
393
+ adapterVersion: adapterIdentity.version,
394
+ adapterFingerprint: recordAdapterFingerprint(adapter),
395
+ title,
396
+ languageHint,
397
+ metadata,
398
+ anchors,
399
+ accountingChars: derivedSource.length,
400
+ metadataChars,
401
+ };
402
+ };
403
+
404
+ export const adapterFailureMessage = (
405
+ code: RecordAdapterFailureCode
406
+ ): string => {
407
+ switch (code) {
408
+ case "MALFORMED_RECORD":
409
+ return "Record adapter reported a malformed record.";
410
+ case "MISSING_ID":
411
+ return "Record adapter reported a missing stable identity.";
412
+ case "DUPLICATE_ID":
413
+ return "Record adapter reported a duplicate stable identity.";
414
+ case "RECORD_TOO_LARGE":
415
+ return "Record adapter reported an oversized record.";
416
+ case "SOURCE_TOO_LARGE":
417
+ return "Record adapter reported an oversized source.";
418
+ case "TIMEOUT":
419
+ return "Record adapter exceeded its time limit.";
420
+ case "RECORD_LIMIT":
421
+ return "Record adapter reported that its record limit was reached.";
422
+ case "FAILURE_LIMIT":
423
+ return "Record adapter reported that its failure limit was reached.";
424
+ case "INVALID_LOCATOR":
425
+ return "Record adapter reported an invalid source locator.";
426
+ case "INVALID_SOURCE_HASH":
427
+ return "Record adapter reported an invalid source hash.";
428
+ case "INVALID_SNAPSHOT":
429
+ return "Record adapter reported an invalid snapshot.";
430
+ default:
431
+ return "Record adapter reported a conversion failure.";
432
+ }
433
+ };