@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
@@ -93,6 +93,7 @@ import {
93
93
  ProjectAffinityInputError,
94
94
  resolveRemoteProjectAffinity,
95
95
  } from "../../core/project-affinity-surface";
96
+ import { projectRecordEvidenceMetadata } from "../../core/record-metadata";
96
97
  import {
97
98
  retrievalTraceFilters,
98
99
  startRetrievalTraceRequest,
@@ -607,6 +608,7 @@ function readRequestedUriFromUrl(req: Request): string | undefined {
607
608
 
608
609
  interface SourceMeta {
609
610
  absPath?: string;
611
+ relPath: string;
610
612
  mime: string;
611
613
  ext: string;
612
614
  modifiedAt?: string;
@@ -625,7 +627,11 @@ function getCollectionByName(
625
627
 
626
628
  async function resolveAbsoluteDocPath(
627
629
  collections: Config["collections"],
628
- doc: { collection: string; relPath: string }
630
+ doc: {
631
+ collection: string;
632
+ relPath: string;
633
+ recordSourcePath?: string | null;
634
+ }
629
635
  ): Promise<{
630
636
  collection: Config["collections"][number];
631
637
  fullPath: string;
@@ -638,7 +644,7 @@ async function resolveAbsoluteDocPath(
638
644
  const nodePath = await import("node:path"); // no bun equivalent
639
645
  let safeRelPath: string;
640
646
  try {
641
- safeRelPath = validateRelPath(doc.relPath);
647
+ safeRelPath = validateRelPath(doc.recordSourcePath ?? doc.relPath);
642
648
  } catch {
643
649
  return null;
644
650
  }
@@ -763,6 +769,7 @@ async function buildSourceMeta(
763
769
  doc: {
764
770
  collection: string;
765
771
  relPath: string;
772
+ recordSourcePath?: string | null;
766
773
  sourceMime: string;
767
774
  sourceExt: string;
768
775
  sourceMtime?: string | null;
@@ -770,9 +777,14 @@ async function buildSourceMeta(
770
777
  sourceHash?: string;
771
778
  }
772
779
  ): Promise<SourceMeta> {
773
- const resolved = await resolveAbsoluteDocPath(collections, doc);
780
+ const relPath = doc.recordSourcePath ?? doc.relPath;
781
+ const resolved = await resolveAbsoluteDocPath(collections, {
782
+ collection: doc.collection,
783
+ relPath,
784
+ });
774
785
  return {
775
786
  absPath: resolved?.fullPath,
787
+ relPath,
776
788
  mime: doc.sourceMime,
777
789
  ext: doc.sourceExt,
778
790
  modifiedAt: doc.sourceMtime ?? undefined,
@@ -781,6 +793,20 @@ async function buildSourceMeta(
781
793
  };
782
794
  }
783
795
 
796
+ const capabilitiesForDocument = (
797
+ doc: Pick<
798
+ DocumentRow,
799
+ "sourceExt" | "sourceMime" | "mirrorHash" | "recordKey"
800
+ >,
801
+ contentAvailable = doc.mirrorHash !== null
802
+ ) =>
803
+ getDocumentCapabilities({
804
+ sourceExt: doc.sourceExt,
805
+ sourceMime: doc.sourceMime,
806
+ contentAvailable,
807
+ recordKey: doc.recordKey,
808
+ });
809
+
784
810
  async function resolveDocumentReference(
785
811
  store: Pick<SqliteAdapter, "getDocumentByDocid" | "getDocumentByUri">,
786
812
  docId: string,
@@ -1613,7 +1639,7 @@ export async function handleDocs(
1613
1639
  uri: doc.uri,
1614
1640
  title: doc.title,
1615
1641
  collection: doc.collection,
1616
- relPath: doc.relPath,
1642
+ relPath: doc.recordSourcePath ?? doc.relPath,
1617
1643
  sourceExt: doc.sourceExt,
1618
1644
  sourceMime: doc.sourceMime,
1619
1645
  updatedAt: doc.updatedAt,
@@ -1790,12 +1816,10 @@ export async function handleDoc(
1790
1816
  }
1791
1817
 
1792
1818
  const contentAvailable = content !== null;
1793
- const capabilities = getDocumentCapabilities({
1794
- sourceExt: doc.sourceExt,
1795
- sourceMime: doc.sourceMime,
1796
- contentAvailable,
1797
- });
1819
+ const capabilities = capabilitiesForDocument(doc, contentAvailable);
1798
1820
  const source = await buildSourceMeta(config.collections, doc);
1821
+ const record = projectRecordEvidenceMetadata(doc);
1822
+ const relPath = doc.recordSourcePath ?? doc.relPath;
1799
1823
 
1800
1824
  const responseData = {
1801
1825
  docid: doc.docid,
@@ -1804,9 +1828,10 @@ export async function handleDoc(
1804
1828
  content,
1805
1829
  contentAvailable,
1806
1830
  collection: doc.collection,
1807
- relPath: doc.relPath,
1831
+ relPath,
1808
1832
  tags,
1809
1833
  source,
1834
+ record,
1810
1835
  capabilities,
1811
1836
  };
1812
1837
  const continuationTraceId = request
@@ -2079,6 +2104,15 @@ export async function handleRenameDoc(
2079
2104
  }
2080
2105
 
2081
2106
  const doc = docResult.value;
2107
+ const capabilities = capabilitiesForDocument(doc);
2108
+ if (!capabilities.editable) {
2109
+ return errorResponse(
2110
+ "READ_ONLY",
2111
+ capabilities.reason ??
2112
+ "This document cannot be renamed in place from GNO.",
2113
+ 409
2114
+ );
2115
+ }
2082
2116
  const resolvedDocPath = await resolveAbsoluteDocPath(
2083
2117
  ctxHolder.config.collections,
2084
2118
  doc
@@ -2096,20 +2130,6 @@ export async function handleRenameDoc(
2096
2130
  return errorResponse("FILE_NOT_FOUND", "Source file no longer exists", 404);
2097
2131
  }
2098
2132
 
2099
- const capabilities = getDocumentCapabilities({
2100
- sourceExt: doc.sourceExt,
2101
- sourceMime: doc.sourceMime,
2102
- contentAvailable: doc.mirrorHash !== null,
2103
- });
2104
- if (!capabilities.editable) {
2105
- return errorResponse(
2106
- "READ_ONLY",
2107
- capabilities.reason ??
2108
- "This document cannot be renamed in place from GNO.",
2109
- 409
2110
- );
2111
- }
2112
-
2113
2133
  const nodePath = await import("node:path"); // no bun equivalent
2114
2134
  const directory = nodePath.dirname(doc.relPath);
2115
2135
  const currentExt = nodePath.extname(doc.relPath);
@@ -2206,11 +2226,7 @@ export async function handleRefactorPlan(
2206
2226
  }
2207
2227
 
2208
2228
  const doc = docResult.value;
2209
- const capabilities = getDocumentCapabilities({
2210
- sourceExt: doc.sourceExt,
2211
- sourceMime: doc.sourceMime,
2212
- contentAvailable: doc.mirrorHash !== null,
2213
- });
2229
+ const capabilities = capabilitiesForDocument(doc);
2214
2230
  if (!capabilities.editable) {
2215
2231
  return errorResponse(
2216
2232
  "READ_ONLY",
@@ -2333,11 +2349,7 @@ export async function handleTrashDoc(
2333
2349
  }
2334
2350
  const { collection, fullPath } = resolvedDocPath;
2335
2351
 
2336
- const capabilities = getDocumentCapabilities({
2337
- sourceExt: doc.sourceExt,
2338
- sourceMime: doc.sourceMime,
2339
- contentAvailable: doc.mirrorHash !== null,
2340
- });
2352
+ const capabilities = capabilitiesForDocument(doc);
2341
2353
  if (!capabilities.editable) {
2342
2354
  return errorResponse(
2343
2355
  "READ_ONLY",
@@ -2432,11 +2444,7 @@ export async function handleMoveDoc(
2432
2444
  return errorResponse("NOT_FOUND", "Document not found", 404);
2433
2445
  }
2434
2446
  const doc = docResult.value;
2435
- const capabilities = getDocumentCapabilities({
2436
- sourceExt: doc.sourceExt,
2437
- sourceMime: doc.sourceMime,
2438
- contentAvailable: doc.mirrorHash !== null,
2439
- });
2447
+ const capabilities = capabilitiesForDocument(doc);
2440
2448
  if (!capabilities.editable) {
2441
2449
  return errorResponse(
2442
2450
  "READ_ONLY",
@@ -2563,11 +2571,7 @@ export async function handleDuplicateDoc(
2563
2571
  return errorResponse("NOT_FOUND", "Document not found", 404);
2564
2572
  }
2565
2573
  const doc = docResult.value;
2566
- const capabilities = getDocumentCapabilities({
2567
- sourceExt: doc.sourceExt,
2568
- sourceMime: doc.sourceMime,
2569
- contentAvailable: doc.mirrorHash !== null,
2570
- });
2574
+ const capabilities = capabilitiesForDocument(doc);
2571
2575
  if (!capabilities.editable) {
2572
2576
  return errorResponse(
2573
2577
  "READ_ONLY",
@@ -2837,6 +2841,15 @@ export async function handleUpdateDoc(
2837
2841
  }
2838
2842
 
2839
2843
  const doc = docResult.value;
2844
+ const capabilities = capabilitiesForDocument(doc);
2845
+ if (hasContent && !capabilities.editable) {
2846
+ return errorResponse(
2847
+ "READ_ONLY",
2848
+ capabilities.reason ??
2849
+ "This document cannot be edited in place. Create an editable markdown copy instead.",
2850
+ 409
2851
+ );
2852
+ }
2840
2853
 
2841
2854
  const resolvedDocPath = await resolveAbsoluteDocPath(
2842
2855
  ctxHolder.config.collections,
@@ -2886,20 +2899,6 @@ export async function handleUpdateDoc(
2886
2899
  }
2887
2900
  }
2888
2901
 
2889
- const capabilities = getDocumentCapabilities({
2890
- sourceExt: doc.sourceExt,
2891
- sourceMime: doc.sourceMime,
2892
- contentAvailable: doc.mirrorHash !== null,
2893
- });
2894
- if (hasContent && !capabilities.editable) {
2895
- return errorResponse(
2896
- "READ_ONLY",
2897
- capabilities.reason ??
2898
- "This document cannot be edited in place. Create an editable markdown copy instead.",
2899
- 409
2900
- );
2901
- }
2902
-
2903
2902
  let writeBack: "applied" | "skipped_unsupported" | undefined;
2904
2903
 
2905
2904
  try {
@@ -3044,11 +3043,7 @@ export async function handleCreateEditableCopy(
3044
3043
 
3045
3044
  const doc = docResult.value;
3046
3045
  const contentAvailable = doc.mirrorHash !== null;
3047
- const capabilities = getDocumentCapabilities({
3048
- sourceExt: doc.sourceExt,
3049
- sourceMime: doc.sourceMime,
3050
- contentAvailable,
3051
- });
3046
+ const capabilities = capabilitiesForDocument(doc, contentAvailable);
3052
3047
  if (capabilities.editable) {
3053
3048
  return errorResponse(
3054
3049
  "VALIDATION",
@@ -3094,12 +3089,15 @@ export async function handleCreateEditableCopy(
3094
3089
  const existingRelPaths = listResult.ok
3095
3090
  ? listResult.value.map((entry) => entry.relPath)
3096
3091
  : [];
3097
- relPath = deriveEditableCopyRelPath(doc.relPath, existingRelPaths);
3092
+ relPath = deriveEditableCopyRelPath(
3093
+ doc.recordSourcePath ?? doc.relPath,
3094
+ existingRelPaths
3095
+ );
3098
3096
  }
3099
3097
 
3100
3098
  const title =
3101
3099
  doc.title ??
3102
- doc.relPath
3100
+ (doc.recordSourcePath ?? doc.relPath)
3103
3101
  .split("/")
3104
3102
  .pop()
3105
3103
  ?.replace(/\.[^.]+$/, "") ??
@@ -0,0 +1,42 @@
1
+ /** Persist bounded logical-record lineage for file/export adapters. */
2
+ import type { Migration } from "./runner";
3
+
4
+ export const migration: Migration = {
5
+ version: 22,
6
+ name: "record_export_lineage",
7
+
8
+ up(db): void {
9
+ const columns = new Set(
10
+ db
11
+ .query<{ name: string }, []>("PRAGMA table_info(documents)")
12
+ .all()
13
+ .map((row) => row.name)
14
+ );
15
+ const additions = [
16
+ ["record_key", "TEXT"],
17
+ ["record_source_path", "TEXT"],
18
+ ["record_source_locator", "TEXT"],
19
+ ["record_metadata", "TEXT"],
20
+ ["record_anchors", "TEXT"],
21
+ ["record_adapter_fingerprint", "TEXT"],
22
+ ] as const;
23
+ for (const [name, type] of additions) {
24
+ if (!columns.has(name)) {
25
+ db.exec(`ALTER TABLE documents ADD COLUMN ${name} ${type}`);
26
+ }
27
+ }
28
+ db.exec(`
29
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_documents_record_key
30
+ ON documents(collection, record_source_path, record_key)
31
+ WHERE record_source_path IS NOT NULL AND record_key IS NOT NULL;
32
+ CREATE INDEX IF NOT EXISTS idx_documents_record_source_path
33
+ ON documents(collection, record_source_path)
34
+ WHERE record_source_path IS NOT NULL;
35
+ `);
36
+ },
37
+
38
+ down(db): void {
39
+ db.exec("DROP INDEX IF EXISTS idx_documents_record_key");
40
+ db.exec("DROP INDEX IF EXISTS idx_documents_record_source_path");
41
+ },
42
+ };
@@ -35,6 +35,7 @@ import { migration as m018 } from "./018-saved-capsule-registration-epoch";
35
35
  import { migration as m019 } from "./019-saved-capsule-registration-generation";
36
36
  import { migration as m020 } from "./020-browser-clipper-security";
37
37
  import { migration as m021 } from "./021-multi-context-identity";
38
+ import { migration as m022 } from "./022-record-export-lineage";
38
39
 
39
40
  /** All migrations in order */
40
41
  export const migrations = [
@@ -59,4 +60,5 @@ export const migrations = [
59
60
  m019,
60
61
  m020,
61
62
  m021,
63
+ m022,
62
64
  ];
@@ -1067,10 +1067,13 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1067
1067
  collection, rel_path, source_hash, source_mime, source_ext,
1068
1068
  source_size, source_mtime, source_ctime, docid, uri, title, mirror_hash,
1069
1069
  converter_id, converter_version, language_hint, content_type, categories,
1070
- content_type_source, author, frontmatter_date, date_fields, content_type_rules_fingerprint,
1070
+ content_type_source, author, frontmatter_date, date_fields,
1071
+ record_key, record_source_path, record_source_locator, record_metadata, record_anchors,
1072
+ record_adapter_fingerprint,
1073
+ content_type_rules_fingerprint,
1071
1074
  active, indexed_at, last_error_code, last_error_message, last_error_at,
1072
1075
  ingest_version, updated_at
1073
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, datetime('now'), ?, ?, ?, ?, datetime('now'))
1076
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, datetime('now'), ?, ?, ?, ?, datetime('now'))
1074
1077
  ON CONFLICT(collection, rel_path) DO UPDATE SET
1075
1078
  source_hash = excluded.source_hash,
1076
1079
  source_mime = excluded.source_mime,
@@ -1091,6 +1094,12 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1091
1094
  author = excluded.author,
1092
1095
  frontmatter_date = excluded.frontmatter_date,
1093
1096
  date_fields = excluded.date_fields,
1097
+ record_key = excluded.record_key,
1098
+ record_source_path = excluded.record_source_path,
1099
+ record_source_locator = excluded.record_source_locator,
1100
+ record_metadata = excluded.record_metadata,
1101
+ record_anchors = excluded.record_anchors,
1102
+ record_adapter_fingerprint = excluded.record_adapter_fingerprint,
1094
1103
  content_type_rules_fingerprint = excluded.content_type_rules_fingerprint,
1095
1104
  active = 1,
1096
1105
  indexed_at = datetime('now'),
@@ -1122,6 +1131,12 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1122
1131
  doc.author ?? null,
1123
1132
  doc.frontmatterDate ?? null,
1124
1133
  doc.dateFields ? JSON.stringify(doc.dateFields) : null,
1134
+ doc.recordKey ?? null,
1135
+ doc.recordSourcePath ?? null,
1136
+ doc.recordSourceLocator ?? null,
1137
+ doc.recordMetadata ? JSON.stringify(doc.recordMetadata) : null,
1138
+ doc.recordAnchors ? JSON.stringify(doc.recordAnchors) : null,
1139
+ doc.recordAdapterFingerprint ?? null,
1125
1140
  doc.contentTypeRulesFingerprint ?? null,
1126
1141
  doc.lastErrorCode ?? null,
1127
1142
  doc.lastErrorMessage ?? null,
@@ -1366,6 +1381,32 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1366
1381
  }
1367
1382
  }
1368
1383
 
1384
+ async listRecordDocuments(
1385
+ collection: string,
1386
+ sourcePath: string
1387
+ ): Promise<StoreResult<DocumentRow[]>> {
1388
+ try {
1389
+ const db = this.ensureOpen();
1390
+ const rows = db
1391
+ .query<DbDocumentRow, [string, string]>(
1392
+ `SELECT * FROM documents
1393
+ WHERE collection = ? AND record_source_path = ?
1394
+ ORDER BY rel_path ASC, id ASC`
1395
+ )
1396
+ .all(collection, sourcePath);
1397
+
1398
+ return ok(rows.map(mapDocumentRow));
1399
+ } catch (cause) {
1400
+ return err(
1401
+ "QUERY_FAILED",
1402
+ cause instanceof Error
1403
+ ? cause.message
1404
+ : "Failed to list record documents",
1405
+ cause
1406
+ );
1407
+ }
1408
+ }
1409
+
1369
1410
  async listActiveDocumentsForBrowse(
1370
1411
  collection?: string
1371
1412
  ): Promise<StoreResult<DocumentRow[]>> {
@@ -1547,17 +1588,18 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
1547
1588
  const normalizedPathPrefix = pathPrefix
1548
1589
  ?.replaceAll("\\", "/")
1549
1590
  .replace(/^\/+|\/+$/g, "");
1591
+ const browsePathSql = "COALESCE(d.record_source_path, d.rel_path)";
1550
1592
 
1551
1593
  if (normalizedPathPrefix) {
1552
- conditions.push("d.rel_path LIKE ?");
1594
+ conditions.push(`${browsePathSql} LIKE ?`);
1553
1595
  params.push(`${normalizedPathPrefix}/%`);
1554
1596
 
1555
1597
  if (directChildrenOnly) {
1556
- conditions.push("substr(d.rel_path, ?) NOT LIKE '%/%'");
1598
+ conditions.push(`substr(${browsePathSql}, ?) NOT LIKE '%/%'`);
1557
1599
  params.push(normalizedPathPrefix.length + 2);
1558
1600
  }
1559
1601
  } else if (directChildrenOnly) {
1560
- conditions.push("d.rel_path NOT LIKE '%/%'");
1602
+ conditions.push(`${browsePathSql} NOT LIKE '%/%'`);
1561
1603
  }
1562
1604
 
1563
1605
  if (options.since) {
@@ -2162,7 +2204,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
2162
2204
  ${options.collection ? "AND collection = ?" : ""}
2163
2205
  ${
2164
2206
  options.relPathPrefix !== undefined
2165
- ? "AND (rel_path = ? OR substr(rel_path, 1, length(?) + 1) = ? || '/')"
2207
+ ? "AND (COALESCE(NULLIF(record_source_path, ''), rel_path) = ? OR substr(COALESCE(NULLIF(record_source_path, ''), rel_path), 1, length(?) + 1) = ? || '/')"
2166
2208
  : ""
2167
2209
  }
2168
2210
  )
@@ -2187,7 +2229,15 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
2187
2229
  d.source_hash,
2188
2230
  d.content_type,
2189
2231
  d.content_type_source,
2190
- d.categories
2232
+ d.categories,
2233
+ d.converter_id,
2234
+ d.converter_version,
2235
+ d.record_key,
2236
+ d.record_source_path,
2237
+ d.record_source_locator,
2238
+ d.record_metadata,
2239
+ d.record_anchors,
2240
+ d.record_adapter_fingerprint
2191
2241
  FROM fts_matches fm
2192
2242
  JOIN documents d ON d.id = fm.rowid AND d.active = 1
2193
2243
  WHERE 1 = 1
@@ -2215,6 +2265,14 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
2215
2265
  content_type: string | null;
2216
2266
  content_type_source: string | null;
2217
2267
  categories: string | null;
2268
+ converter_id: string | null;
2269
+ converter_version: string | null;
2270
+ record_key: string | null;
2271
+ record_source_path: string | null;
2272
+ record_source_locator: string | null;
2273
+ record_metadata: string | null;
2274
+ record_anchors: string | null;
2275
+ record_adapter_fingerprint: string | null;
2218
2276
  }
2219
2277
 
2220
2278
  const queryParams = [
@@ -2254,6 +2312,15 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
2254
2312
  contentType: r.content_type ?? undefined,
2255
2313
  contentTypeSource: r.content_type_source ?? undefined,
2256
2314
  categories: parseCategoriesJson(r.categories) ?? undefined,
2315
+ converterId: r.converter_id ?? undefined,
2316
+ converterVersion: r.converter_version ?? undefined,
2317
+ recordKey: r.record_key ?? undefined,
2318
+ recordSourcePath: r.record_source_path ?? undefined,
2319
+ recordSourceLocator: r.record_source_locator ?? undefined,
2320
+ recordMetadata:
2321
+ parseRecordMetadataJson(r.record_metadata) ?? undefined,
2322
+ recordAnchors: parseRecordAnchorsJson(r.record_anchors) ?? undefined,
2323
+ recordAdapterFingerprint: r.record_adapter_fingerprint ?? undefined,
2257
2324
  }))
2258
2325
  );
2259
2326
  } catch (cause) {
@@ -5098,6 +5165,12 @@ interface DbDocumentRow {
5098
5165
  author: string | null;
5099
5166
  frontmatter_date: string | null;
5100
5167
  date_fields: string | null;
5168
+ record_key: string | null;
5169
+ record_source_path: string | null;
5170
+ record_source_locator: string | null;
5171
+ record_metadata: string | null;
5172
+ record_anchors: string | null;
5173
+ record_adapter_fingerprint: string | null;
5101
5174
  content_type_rules_fingerprint: string | null;
5102
5175
  indexed_at: string | null;
5103
5176
  active: number;
@@ -5185,6 +5258,34 @@ function parseCategoriesJson(raw: string | null): string[] | null {
5185
5258
  return null;
5186
5259
  }
5187
5260
 
5261
+ function parseRecordMetadataJson(
5262
+ raw: string | null
5263
+ ): DocumentRow["recordMetadata"] {
5264
+ if (!raw) return null;
5265
+ try {
5266
+ const parsed: unknown = JSON.parse(raw);
5267
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
5268
+ ? (parsed as NonNullable<DocumentRow["recordMetadata"]>)
5269
+ : null;
5270
+ } catch {
5271
+ return null;
5272
+ }
5273
+ }
5274
+
5275
+ function parseRecordAnchorsJson(
5276
+ raw: string | null
5277
+ ): DocumentRow["recordAnchors"] {
5278
+ if (!raw) return null;
5279
+ try {
5280
+ const parsed: unknown = JSON.parse(raw);
5281
+ return Array.isArray(parsed)
5282
+ ? (parsed as NonNullable<DocumentRow["recordAnchors"]>)
5283
+ : null;
5284
+ } catch {
5285
+ return null;
5286
+ }
5287
+ }
5288
+
5188
5289
  function mapDocumentRow(row: DbDocumentRow): DocumentRow {
5189
5290
  const categories = parseCategoriesJson(row.categories);
5190
5291
  let dateFields: Record<string, string> | null = null;
@@ -5230,6 +5331,12 @@ function mapDocumentRow(row: DbDocumentRow): DocumentRow {
5230
5331
  author: row.author,
5231
5332
  frontmatterDate: row.frontmatter_date,
5232
5333
  dateFields,
5334
+ recordKey: row.record_key,
5335
+ recordSourcePath: row.record_source_path,
5336
+ recordSourceLocator: row.record_source_locator,
5337
+ recordMetadata: parseRecordMetadataJson(row.record_metadata),
5338
+ recordAnchors: parseRecordAnchorsJson(row.record_anchors),
5339
+ recordAdapterFingerprint: row.record_adapter_fingerprint,
5233
5340
  indexedAt: row.indexed_at,
5234
5341
  active: row.active === 1,
5235
5342
  ingestVersion: row.ingest_version,
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import type { Collection, Context, FtsTokenizer } from "../config/types";
9
+ import type { RecordAnchor, RecordMetadata } from "../converters/types";
9
10
 
10
11
  // ─────────────────────────────────────────────────────────────────────────────
11
12
  // Error Types
@@ -113,6 +114,12 @@ export interface DocumentRow {
113
114
  author?: string | null;
114
115
  frontmatterDate?: string | null;
115
116
  dateFields?: Record<string, string> | null;
117
+ recordKey?: string | null;
118
+ recordSourcePath?: string | null;
119
+ recordSourceLocator?: string | null;
120
+ recordMetadata?: RecordMetadata | null;
121
+ recordAnchors?: RecordAnchor[] | null;
122
+ recordAdapterFingerprint?: string | null;
116
123
  indexedAt?: string | null;
117
124
 
118
125
  // Status
@@ -132,6 +139,16 @@ export interface DocumentRow {
132
139
  updatedAt: string;
133
140
  }
134
141
 
142
+ /** Minimal persisted record identity used by export snapshot reconciliation. */
143
+ export interface StoredRecordState {
144
+ recordKey: string;
145
+ sourceHash: string;
146
+ adapterVersion: string;
147
+ adapterFingerprint: string;
148
+ active: boolean;
149
+ relativePath: string;
150
+ }
151
+
135
152
  /** Chunk row from DB */
136
153
  export interface ChunkRow {
137
154
  mirrorHash: string;
@@ -300,6 +317,12 @@ export interface DocumentInput {
300
317
  author?: string;
301
318
  frontmatterDate?: string;
302
319
  dateFields?: Record<string, string>;
320
+ recordKey?: string;
321
+ recordSourcePath?: string;
322
+ recordSourceLocator?: string;
323
+ recordMetadata?: RecordMetadata;
324
+ recordAnchors?: RecordAnchor[];
325
+ recordAdapterFingerprint?: string;
303
326
  lastErrorCode?: string;
304
327
  lastErrorMessage?: string;
305
328
  /** Ingest schema version for backfill detection */
@@ -571,6 +594,14 @@ export interface FtsResult {
571
594
  contentType?: string;
572
595
  contentTypeSource?: string;
573
596
  categories?: string[];
597
+ converterId?: string;
598
+ converterVersion?: string;
599
+ recordKey?: string;
600
+ recordSourcePath?: string;
601
+ recordSourceLocator?: string;
602
+ recordMetadata?: RecordMetadata;
603
+ recordAnchors?: RecordAnchor[];
604
+ recordAdapterFingerprint?: string;
574
605
  }
575
606
 
576
607
  // ─────────────────────────────────────────────────────────────────────────────
@@ -1448,6 +1479,15 @@ export interface StorePort {
1448
1479
  */
1449
1480
  listDocuments(collection?: string): Promise<StoreResult<DocumentRow[]>>;
1450
1481
 
1482
+ /**
1483
+ * List logical record documents produced from one source container.
1484
+ * Uses the record-source index so targeted syncs do not scan a collection.
1485
+ */
1486
+ listRecordDocuments(
1487
+ collection: string,
1488
+ sourcePath: string
1489
+ ): Promise<StoreResult<DocumentRow[]>>;
1490
+
1451
1491
  /**
1452
1492
  * Fetch documents by mirror hashes in batch.
1453
1493
  * Useful for retrieval pipelines to avoid full document scans.
@@ -1 +0,0 @@
1
- 8161a2f2b8bb643dc2768c59a60783752b04dfedb0f5a1cf369042f623afaaa0 gno-browser-clipper-v1.27.1.zip