@gmickel/gno 1.27.1 → 1.29.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 (195) hide show
  1. package/README.md +46 -20
  2. package/assets/skill/SKILL.md +59 -1
  3. package/browser-extension/artifacts/gno-browser-clipper-v1.29.0.zip +0 -0
  4. package/browser-extension/artifacts/gno-browser-clipper-v1.29.0.zip.sha256 +1 -0
  5. package/browser-extension/dist/chunk-b2zm0jjd.js +50 -0
  6. package/browser-extension/dist/manifest.json +1 -1
  7. package/browser-extension/dist/preview.html +1 -1
  8. package/browser-extension/dist/service-worker.js +6 -6
  9. package/package.json +2 -2
  10. package/spec/cli.md +82 -5
  11. package/spec/db/schema.sql +31 -0
  12. package/spec/mcp.md +71 -0
  13. package/spec/output-schemas/ask.schema.json +156 -1
  14. package/spec/output-schemas/browser-clip-preview.schema.json +30 -0
  15. package/spec/output-schemas/collection-egress-check.schema.json +91 -0
  16. package/spec/output-schemas/collection-egress-policy-set.schema.json +56 -0
  17. package/spec/output-schemas/collection-egress-policy.schema.json +41 -0
  18. package/spec/output-schemas/context-capsule-v1.schema.json +183 -3
  19. package/spec/output-schemas/context-capsule-verification.schema.json +1 -1
  20. package/spec/output-schemas/egress-audit-management.schema.json +88 -0
  21. package/spec/output-schemas/get.schema.json +94 -0
  22. package/spec/output-schemas/mcp-http-error.schema.json +113 -2
  23. package/spec/output-schemas/mcp-job-status.schema.json +28 -0
  24. package/spec/output-schemas/multi-get.schema.json +128 -0
  25. package/spec/output-schemas/publish-artifact.schema.json +32 -0
  26. package/spec/output-schemas/record-import.schema.json +223 -0
  27. package/spec/output-schemas/retrieval-trace-common.schema.json +31 -0
  28. package/spec/output-schemas/retrieval-trace-export.schema.json +3 -0
  29. package/spec/output-schemas/retrieval-trace-qrels.schema.json +3 -0
  30. package/spec/output-schemas/retrieval-trace-summary.schema.json +4 -0
  31. package/spec/output-schemas/search-result.schema.json +94 -0
  32. package/spec/output-schemas/search-results.schema.json +155 -0
  33. package/spec/project-profile.schema.json +66 -0
  34. package/src/app/context-format.ts +1 -1
  35. package/src/app/context-runtime-contract.ts +16 -7
  36. package/src/cli/commands/ask.ts +7 -0
  37. package/src/cli/commands/collection/index.ts +5 -0
  38. package/src/cli/commands/collection/policy.ts +126 -0
  39. package/src/cli/commands/context-build.ts +5 -0
  40. package/src/cli/commands/daemon.ts +45 -3
  41. package/src/cli/commands/egress-audit.ts +68 -0
  42. package/src/cli/commands/embed.ts +2 -0
  43. package/src/cli/commands/get.ts +12 -2
  44. package/src/cli/commands/index-cmd.ts +13 -0
  45. package/src/cli/commands/multi-get.ts +9 -2
  46. package/src/cli/commands/query.ts +12 -0
  47. package/src/cli/commands/replay.ts +13 -3
  48. package/src/cli/commands/shared.ts +28 -0
  49. package/src/cli/commands/update.ts +5 -0
  50. package/src/cli/commands/vsearch.ts +3 -1
  51. package/src/cli/program.ts +127 -0
  52. package/src/config/index.ts +9 -0
  53. package/src/config/project-profile.ts +2 -0
  54. package/src/config/types.ts +66 -0
  55. package/src/converters/adapters/browser-export/adapter.ts +199 -0
  56. package/src/converters/adapters/browser-export/formats.ts +358 -0
  57. package/src/converters/adapters/email/adapter.ts +429 -0
  58. package/src/converters/adapters/email/html.ts +162 -0
  59. package/src/converters/adapters/email/mime.ts +454 -0
  60. package/src/converters/adapters/email/parameters.ts +77 -0
  61. package/src/converters/adapters/ical/adapter.ts +475 -0
  62. package/src/converters/adapters/ical/recurrence.ts +186 -0
  63. package/src/converters/adapters/jsonl/adapter.ts +238 -0
  64. package/src/converters/adapters/jsonl/config.ts +105 -0
  65. package/src/converters/adapters/shared/html-text.ts +165 -0
  66. package/src/converters/adapters/shared/record-utils.ts +79 -0
  67. package/src/converters/adapters/shared/utf8-lines.ts +141 -0
  68. package/src/converters/adapters/transcript/adapter.ts +295 -0
  69. package/src/converters/adapters/transcript/json.ts +184 -0
  70. package/src/converters/adapters/transcript/model.ts +171 -0
  71. package/src/converters/adapters/transcript/text.ts +58 -0
  72. package/src/converters/adapters/transcript/timed.ts +152 -0
  73. package/src/converters/index.ts +11 -1
  74. package/src/converters/mime.ts +8 -0
  75. package/src/converters/pipeline.ts +15 -1
  76. package/src/converters/registry.ts +37 -1
  77. package/src/converters/types.ts +136 -0
  78. package/src/core/browser-clip.ts +16 -1
  79. package/src/core/collection-egress-policy-projection.ts +28 -0
  80. package/src/core/collection-egress-policy-service.ts +443 -0
  81. package/src/core/collection-egress-policy-validation.ts +242 -0
  82. package/src/core/config-mutation.ts +31 -19
  83. package/src/core/context-capsule-schema.ts +198 -1
  84. package/src/core/context-capsule-validation.ts +4 -4
  85. package/src/core/context-capsule-verification.ts +5 -1
  86. package/src/core/context-capsule.ts +177 -112
  87. package/src/core/context-evidence.ts +93 -15
  88. package/src/core/destination-classifier.ts +402 -0
  89. package/src/core/document-capabilities.ts +12 -0
  90. package/src/core/egress-audit.ts +239 -0
  91. package/src/core/egress-authorization.ts +50 -0
  92. package/src/core/egress-enforcement.ts +264 -0
  93. package/src/core/egress-policy.ts +440 -0
  94. package/src/core/egress-provenance.ts +336 -0
  95. package/src/core/job-manager.ts +37 -0
  96. package/src/core/network-boundary-inventory.ts +261 -0
  97. package/src/core/project-profile-apply-state.ts +5 -0
  98. package/src/core/project-profile.ts +4 -0
  99. package/src/core/record-metadata.ts +49 -0
  100. package/src/core/retrieval-qrels.ts +6 -0
  101. package/src/core/retrieval-replay.ts +18 -4
  102. package/src/core/retrieval-trace-export.ts +23 -4
  103. package/src/core/retrieval-trace-management-helpers.ts +1 -0
  104. package/src/core/retrieval-trace-management-types.ts +3 -0
  105. package/src/core/retrieval-trace-management.ts +9 -1
  106. package/src/core/retrieval-trace-session.ts +94 -0
  107. package/src/core/retrieval-trace.ts +22 -0
  108. package/src/ingestion/record-adapter-canonical.ts +433 -0
  109. package/src/ingestion/record-adapter.ts +448 -0
  110. package/src/ingestion/record-container.ts +704 -0
  111. package/src/ingestion/record-path.ts +20 -0
  112. package/src/ingestion/record-sync.ts +70 -0
  113. package/src/ingestion/sync.ts +243 -36
  114. package/src/ingestion/types.ts +71 -1
  115. package/src/ingestion/walker.ts +31 -11
  116. package/src/llm/errors.ts +10 -0
  117. package/src/llm/http-inference.ts +175 -0
  118. package/src/llm/http-policy.ts +537 -0
  119. package/src/llm/httpEmbedding.ts +47 -28
  120. package/src/llm/httpGeneration.ts +31 -18
  121. package/src/llm/httpRerank.ts +30 -18
  122. package/src/llm/index.ts +1 -0
  123. package/src/llm/nodeLlamaCpp/adapter.ts +55 -20
  124. package/src/llm/pinned-http-connection.ts +177 -0
  125. package/src/mcp/context.ts +53 -1
  126. package/src/mcp/http-egress.ts +220 -0
  127. package/src/mcp/http-security.ts +32 -21
  128. package/src/mcp/http-session.ts +13 -2
  129. package/src/mcp/http-transport.ts +96 -6
  130. package/src/mcp/sync-egress.ts +24 -0
  131. package/src/mcp/tools/add-collection.ts +4 -0
  132. package/src/mcp/tools/ask.ts +1 -1
  133. package/src/mcp/tools/context.ts +9 -2
  134. package/src/mcp/tools/egress.ts +247 -0
  135. package/src/mcp/tools/embed.ts +1 -0
  136. package/src/mcp/tools/get.ts +10 -2
  137. package/src/mcp/tools/index-cmd.ts +7 -0
  138. package/src/mcp/tools/index.ts +98 -0
  139. package/src/mcp/tools/multi-get.ts +9 -2
  140. package/src/mcp/tools/query.ts +10 -0
  141. package/src/mcp/tools/remove-collection.ts +4 -0
  142. package/src/mcp/tools/sync.ts +6 -0
  143. package/src/mcp/tools/trace.ts +7 -1
  144. package/src/mcp/tools/vsearch.ts +1 -0
  145. package/src/mcp/tools/workspace-write.ts +2 -0
  146. package/src/pipeline/egress-lineage.ts +124 -0
  147. package/src/pipeline/filters.ts +1 -1
  148. package/src/pipeline/graph-retrieval.ts +7 -4
  149. package/src/pipeline/hybrid.ts +21 -4
  150. package/src/pipeline/result-context.ts +12 -4
  151. package/src/pipeline/search.ts +23 -4
  152. package/src/pipeline/types.ts +6 -0
  153. package/src/pipeline/vsearch.ts +67 -28
  154. package/src/publish/artifact.ts +31 -3
  155. package/src/publish/export-service.ts +25 -0
  156. package/src/sdk/client.ts +119 -2
  157. package/src/sdk/documents.ts +13 -5
  158. package/src/sdk/embed.ts +1 -0
  159. package/src/sdk/types.ts +33 -0
  160. package/src/serve/browse-tree.ts +4 -2
  161. package/src/serve/clipper-capture.ts +5 -1
  162. package/src/serve/closed-json.ts +61 -0
  163. package/src/serve/config-sync.ts +4 -1
  164. package/src/serve/context.ts +1 -0
  165. package/src/serve/doc-events.ts +110 -39
  166. package/src/serve/public/components/CollectionEgressCheckPanel.tsx +411 -0
  167. package/src/serve/public/components/CollectionEgressPolicyEditor.tsx +78 -0
  168. package/src/serve/public/components/CollectionModelDialog.tsx +93 -9
  169. package/src/serve/public/components/EgressAuditPanel.tsx +278 -0
  170. package/src/serve/public/globals.built.css +1 -1
  171. package/src/serve/public/pages/Collections.tsx +3 -0
  172. package/src/serve/resident-request.ts +80 -3
  173. package/src/serve/resident-runtime.ts +63 -1
  174. package/src/serve/routes/api.ts +237 -65
  175. package/src/serve/routes/clipper.ts +64 -1
  176. package/src/serve/routes/mcp.ts +6 -0
  177. package/src/serve/routes/traces.ts +27 -4
  178. package/src/serve/server.ts +118 -7
  179. package/src/store/migrations/022-record-export-lineage.ts +42 -0
  180. package/src/store/migrations/023-collection-egress-policy.ts +61 -0
  181. package/src/store/migrations/024-egress-derived-lineage.ts +121 -0
  182. package/src/store/migrations/025-collection-egress-policy-revision.ts +40 -0
  183. package/src/store/migrations/index.ts +8 -0
  184. package/src/store/retrieval-trace-codec.ts +15 -0
  185. package/src/store/sqlite/adapter.ts +284 -14
  186. package/src/store/sqlite/change-journal-store.ts +41 -2
  187. package/src/store/sqlite/egress-audit-store.ts +485 -0
  188. package/src/store/sqlite/retrieval-trace-management-store.ts +47 -6
  189. package/src/store/sqlite/retrieval-trace-retention.ts +2 -0
  190. package/src/store/sqlite/retrieval-trace-rows.ts +8 -1
  191. package/src/store/sqlite/retrieval-trace-store.ts +104 -5
  192. package/src/store/types.ts +166 -1
  193. package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip +0 -0
  194. package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip.sha256 +0 -1
  195. package/browser-extension/dist/chunk-vn5f663b.js +0 -50
@@ -0,0 +1,124 @@
1
+ /** Attach complete collection policy ownership to observable search results. */
2
+
3
+ import type { EgressLineage } from "../core/egress-provenance";
4
+ import type {
5
+ CollectionRow,
6
+ DocumentRow,
7
+ StorePort,
8
+ StoreResult,
9
+ } from "../store/types";
10
+ import type { SearchResult } from "./types";
11
+
12
+ import { parseUri } from "../app/constants";
13
+ import { createEgressLineage } from "../core/egress-provenance";
14
+ import { err, ok } from "../store/types";
15
+
16
+ type EgressLineageStore = Pick<
17
+ StorePort,
18
+ "getCollections" | "getDocumentsByMirrorHashes"
19
+ >;
20
+
21
+ export const attachSearchResultEgressLineage = async (
22
+ store: EgressLineageStore,
23
+ results: SearchResult[],
24
+ options: {
25
+ ownershipHashes?: readonly string[];
26
+ ownershipDocuments?: readonly Pick<
27
+ DocumentRow,
28
+ "collection" | "mirrorHash"
29
+ >[];
30
+ collections?: readonly CollectionRow[];
31
+ } = {}
32
+ ): Promise<StoreResult<void>> => {
33
+ if (results.length === 0) return ok(undefined);
34
+ const mirrorHashes = [
35
+ ...new Set(
36
+ options.ownershipHashes ??
37
+ results
38
+ .map((result) => result.conversion?.mirrorHash)
39
+ .filter((hash): hash is string => Boolean(hash))
40
+ ),
41
+ ];
42
+ let ownershipDocuments = options.ownershipDocuments;
43
+ if (ownershipDocuments === undefined) {
44
+ if (typeof store.getDocumentsByMirrorHashes !== "function") {
45
+ ownershipDocuments = [];
46
+ } else {
47
+ const documentsResult = await store.getDocumentsByMirrorHashes(
48
+ mirrorHashes,
49
+ {
50
+ activeOnly: true,
51
+ }
52
+ );
53
+ if (!documentsResult.ok) return documentsResult;
54
+ ownershipDocuments = documentsResult.value;
55
+ }
56
+ }
57
+ let collectionRows = options.collections;
58
+ if (collectionRows === undefined) {
59
+ const collectionsResult = await store.getCollections();
60
+ if (!collectionsResult.ok) return collectionsResult;
61
+ collectionRows = collectionsResult.value;
62
+ }
63
+ const policyByCollection = new Map(
64
+ collectionRows.map((collection) => [
65
+ collection.name,
66
+ {
67
+ collection: collection.name,
68
+ policy: collection.egressPolicy,
69
+ source: collection.egressPolicySource,
70
+ },
71
+ ])
72
+ );
73
+ const lineageByMirrorHash = new Map<string, EgressLineage>();
74
+ for (const mirrorHash of mirrorHashes) {
75
+ const ownerCollections = [
76
+ ...new Set([
77
+ ...ownershipDocuments
78
+ .filter((document) => document.mirrorHash === mirrorHash)
79
+ .map((owner) => owner.collection),
80
+ ...results
81
+ .filter((result) => result.conversion?.mirrorHash === mirrorHash)
82
+ .map((result) => parseUri(result.uri)?.collection)
83
+ .filter((collection): collection is string => Boolean(collection)),
84
+ ]),
85
+ ];
86
+ const policies = ownerCollections.map((collection) =>
87
+ policyByCollection.get(collection)
88
+ );
89
+ if (policies.some((policy) => policy === undefined)) {
90
+ return err(
91
+ "INVALID_INPUT",
92
+ "Search result policy lineage references an unknown collection"
93
+ );
94
+ }
95
+ if (policies.length === 0) continue;
96
+ try {
97
+ lineageByMirrorHash.set(
98
+ mirrorHash,
99
+ createEgressLineage(
100
+ policies as NonNullable<(typeof policies)[number]>[]
101
+ )
102
+ );
103
+ } catch (error) {
104
+ return err(
105
+ "INVALID_INPUT",
106
+ "Search result policy lineage is incomplete",
107
+ error
108
+ );
109
+ }
110
+ }
111
+ for (const result of results) {
112
+ const lineage = result.conversion?.mirrorHash
113
+ ? lineageByMirrorHash.get(result.conversion.mirrorHash)
114
+ : undefined;
115
+ if (!lineage) {
116
+ return err(
117
+ "INVALID_INPUT",
118
+ "Search result policy lineage could not be resolved"
119
+ );
120
+ }
121
+ result.egressLineage = lineage;
122
+ }
123
+ return ok(undefined);
124
+ };
@@ -62,7 +62,7 @@ export function evaluateDocumentChunkFilters(
62
62
  matchesExcludedText(
63
63
  [
64
64
  doc.title ?? "",
65
- doc.relPath,
65
+ doc.recordSourcePath ?? doc.relPath,
66
66
  doc.author ?? "",
67
67
  doc.contentType ?? "",
68
68
  ...(doc.categories ?? []),
@@ -42,6 +42,9 @@ const EMPTY_EDGE_CONFIDENCE: Record<GraphEdgeConfidence, number> = {
42
42
  similarity: 0,
43
43
  };
44
44
 
45
+ const sourceRelPath = (doc: DocumentRow): string =>
46
+ doc.recordSourcePath ?? doc.relPath;
47
+
45
48
  const confidenceWeight = (confidence: GraphEdgeConfidence): number => {
46
49
  switch (confidence) {
47
50
  case "explicit":
@@ -253,8 +256,8 @@ export async function expandGraphCandidates(
253
256
  }
254
257
  if (
255
258
  options.relPathPrefix !== undefined &&
256
- doc.relPath !== options.relPathPrefix &&
257
- !doc.relPath.startsWith(`${options.relPathPrefix}/`)
259
+ sourceRelPath(doc) !== options.relPathPrefix &&
260
+ !sourceRelPath(doc).startsWith(`${options.relPathPrefix}/`)
258
261
  ) {
259
262
  continue;
260
263
  }
@@ -337,8 +340,8 @@ export async function expandGraphCandidates(
337
340
  (doc) =>
338
341
  doc.mirrorHash &&
339
342
  (options.relPathPrefix === undefined ||
340
- doc.relPath === options.relPathPrefix ||
341
- doc.relPath.startsWith(`${options.relPathPrefix}/`)) &&
343
+ sourceRelPath(doc) === options.relPathPrefix ||
344
+ sourceRelPath(doc).startsWith(`${options.relPathPrefix}/`)) &&
342
345
  matchesDocumentFilters(doc, options)
343
346
  );
344
347
  const docs = await filterDocsByTags(store, metadataFilteredDocs, options);
@@ -21,6 +21,7 @@ import type {
21
21
  } from "./types";
22
22
 
23
23
  import { normalizeContentTypes } from "../config/content-types";
24
+ import { projectRecordEvidenceMetadata } from "../core/record-metadata";
24
25
  import { embedTextsWithRecovery } from "../embed/batch";
25
26
  import { err, ok } from "../store/types";
26
27
  import { createChunkLookup } from "./chunk-lookup";
@@ -31,6 +32,7 @@ import {
31
32
  sortByFinalScoreStable,
32
33
  } from "./content-type-boost";
33
34
  import { formatQueryForEmbedding } from "./contextual";
35
+ import { attachSearchResultEgressLineage } from "./egress-lineage";
34
36
  import { expandQuery } from "./expansion";
35
37
  import {
36
38
  buildExplainResults,
@@ -840,10 +842,11 @@ export async function searchHybrid(
840
842
  };
841
843
  const matchesMetadataFilters = (doc: DocumentRow): boolean => {
842
844
  const relPathPrefix = options.retrievalScope?.relPathPrefix;
845
+ const sourceRelPath = doc.recordSourcePath ?? doc.relPath;
843
846
  if (
844
847
  relPathPrefix !== undefined &&
845
- doc.relPath !== relPathPrefix &&
846
- !doc.relPath.startsWith(`${relPathPrefix}/`)
848
+ sourceRelPath !== relPathPrefix &&
849
+ !sourceRelPath.startsWith(`${relPathPrefix}/`)
847
850
  ) {
848
851
  return false;
849
852
  }
@@ -1052,9 +1055,9 @@ export async function searchHybrid(
1052
1055
  snippetLanguage: chunk.language ?? undefined,
1053
1056
  snippetRange,
1054
1057
  source: {
1055
- relPath: doc.relPath,
1058
+ relPath: doc.recordSourcePath ?? doc.relPath,
1056
1059
  absPath: collectionPath
1057
- ? `${collectionPath}/${doc.relPath}`
1060
+ ? `${collectionPath}/${doc.recordSourcePath ?? doc.relPath}`
1058
1061
  : undefined,
1059
1062
  mime: doc.sourceMime,
1060
1063
  ext: doc.sourceExt,
@@ -1068,6 +1071,7 @@ export async function searchHybrid(
1068
1071
  converterId: doc.converterId ?? undefined,
1069
1072
  converterVersion: doc.converterVersion ?? undefined,
1070
1073
  },
1074
+ record: projectRecordEvidenceMetadata(doc),
1071
1075
  };
1072
1076
  const auxiliaryBaseScore =
1073
1077
  auxiliaryBaseScores.get(`${candidate.mirrorHash}:${candidate.seq}`) ??
@@ -1166,6 +1170,19 @@ export async function searchHybrid(
1166
1170
  }
1167
1171
  : undefined;
1168
1172
  await attachSearchResultContexts(store, finalResults);
1173
+ const lineageResult = await attachSearchResultEgressLineage(
1174
+ store,
1175
+ finalResults,
1176
+ {
1177
+ ownershipHashes: [...neededHashes],
1178
+ ownershipDocuments:
1179
+ options.collection === undefined ? documents : undefined,
1180
+ collections: collectionsResult.ok ? collectionsResult.value : undefined,
1181
+ }
1182
+ );
1183
+ if (!lineageResult.ok) {
1184
+ return err("QUERY_FAILED", lineageResult.error.message);
1185
+ }
1169
1186
 
1170
1187
  const output: SearchResults = {
1171
1188
  results: finalResults,
@@ -16,10 +16,18 @@ export async function attachSearchResultContexts(
16
16
  results: SearchResult[]
17
17
  ): Promise<void> {
18
18
  const validResults = results
19
- .map((result) => ({
20
- identity: contextIdentityFromUri(result.uri),
21
- result,
22
- }))
19
+ .map((result) => {
20
+ const uriIdentity = contextIdentityFromUri(result.uri);
21
+ return {
22
+ identity: uriIdentity
23
+ ? {
24
+ collection: uriIdentity.collection,
25
+ relPath: result.source.relPath,
26
+ }
27
+ : null,
28
+ result,
29
+ };
30
+ })
23
31
  .filter(
24
32
  (
25
33
  entry
@@ -15,6 +15,7 @@ import type {
15
15
  SearchResults,
16
16
  } from "./types";
17
17
 
18
+ import { projectRecordEvidenceMetadata } from "../core/record-metadata";
18
19
  import { getContentBatch } from "../store/content-batch";
19
20
  import { err, ok } from "../store/types";
20
21
  import { createChunkLookup } from "./chunk-lookup";
@@ -23,6 +24,7 @@ import {
23
24
  hasAuxiliaryRanking,
24
25
  sortByFinalScoreStable,
25
26
  } from "./content-type-boost";
27
+ import { attachSearchResultEgressLineage } from "./egress-lineage";
26
28
  import { matchesExcludedChunks, matchesExcludedText } from "./exclude";
27
29
  import { selectBestChunkForSteering } from "./intent";
28
30
  import { hasProjectAffinity } from "./project-affinity";
@@ -85,8 +87,9 @@ interface BuildResultContext {
85
87
  /** Build SearchResult from FtsResult and related data */
86
88
  function buildSearchResult(ctx: BuildResultContext): SearchResult {
87
89
  const { fts, chunk, collectionPath, options, fullContent } = ctx;
90
+ const sourceRelPath = fts.recordSourcePath ?? fts.relPath ?? "";
88
91
  const source: SearchResultSource = {
89
- relPath: fts.relPath ?? "",
92
+ relPath: sourceRelPath,
90
93
  // Use actual source metadata with fallback to markdown defaults
91
94
  mime: fts.sourceMime ?? "text/markdown",
92
95
  ext: fts.sourceExt ?? ".md",
@@ -97,8 +100,8 @@ function buildSearchResult(ctx: BuildResultContext): SearchResult {
97
100
  };
98
101
 
99
102
  // Add absPath if we have collection path (cross-platform safe)
100
- if (collectionPath && fts.relPath) {
101
- source.absPath = pathJoin(collectionPath, fts.relPath);
103
+ if (collectionPath && sourceRelPath) {
104
+ source.absPath = pathJoin(collectionPath, sourceRelPath);
102
105
  }
103
106
 
104
107
  // Determine snippet content and range
@@ -134,6 +137,7 @@ function buildSearchResult(ctx: BuildResultContext): SearchResult {
134
137
  snippetRange,
135
138
  source,
136
139
  conversion: fts.mirrorHash ? { mirrorHash: fts.mirrorHash } : undefined,
140
+ record: projectRecordEvidenceMetadata(fts),
137
141
  };
138
142
  if (!(chunk && fts.mirrorHash)) return result;
139
143
  return attachSearchResultPlannerMetadata(result, {
@@ -287,7 +291,11 @@ export async function searchBm25(
287
291
 
288
292
  const excluded =
289
293
  matchesExcludedText(
290
- [fts.title ?? "", fts.relPath ?? "", fts.snippet ?? ""],
294
+ [
295
+ fts.title ?? "",
296
+ fts.recordSourcePath ?? fts.relPath ?? "",
297
+ fts.snippet ?? "",
298
+ ],
291
299
  options.exclude
292
300
  ) ||
293
301
  matchesExcludedChunks(
@@ -426,6 +434,17 @@ export async function searchBm25(
426
434
  if (metadata) metadata.retrievalRank = index + 1;
427
435
  }
428
436
  await attachSearchResultContexts(store, finalResults);
437
+ const lineageResult = await attachSearchResultEgressLineage(
438
+ store,
439
+ finalResults,
440
+ {
441
+ ownershipHashes: uniqueHashes,
442
+ collections: collectionsResult.ok ? collectionsResult.value : undefined,
443
+ }
444
+ );
445
+ if (!lineageResult.ok) {
446
+ return err("QUERY_FAILED", lineageResult.error.message);
447
+ }
429
448
 
430
449
  const output: SearchResults = {
431
450
  results: finalResults,
@@ -10,6 +10,8 @@ import type {
10
10
  ContextCapsuleV1,
11
11
  ContextCapsuleVerification,
12
12
  } from "../core/context-capsule";
13
+ import type { EgressLineage } from "../core/egress-provenance";
14
+ import type { RecordEvidenceMetadata } from "../core/record-metadata";
13
15
  import type { RetrievalTraceSession } from "../core/retrieval-trace-session";
14
16
  import type { StoreResult } from "../store/types";
15
17
  import type { ClaimVerificationResult } from "./claim-verification";
@@ -86,6 +88,10 @@ export interface SearchResult {
86
88
  context?: string;
87
89
  source: SearchResultSource;
88
90
  conversion?: SearchResultConversion;
91
+ /** Bounded logical-record provenance for file/export adapters. */
92
+ record?: RecordEvidenceMetadata;
93
+ /** Complete, most-restrictive collection ownership for this derived result. */
94
+ egressLineage?: EgressLineage;
89
95
  [SEARCH_RESULT_PLANNER_METADATA]?: SearchResultPlannerMetadata;
90
96
  }
91
97
 
@@ -7,11 +7,12 @@
7
7
 
8
8
  import type { Config } from "../config/types";
9
9
  import type { EmbeddingPort } from "../llm/types";
10
- import type { StorePort } from "../store/types";
10
+ import type { DocumentRow, StorePort } from "../store/types";
11
11
  import type { VectorIndexPort } from "../store/vector/types";
12
12
  import type { SearchOptions, SearchResult, SearchResults } from "./types";
13
13
 
14
14
  import { normalizeContentTypes } from "../config/content-types";
15
+ import { projectRecordEvidenceMetadata } from "../core/record-metadata";
15
16
  import { getContentBatch } from "../store/content-batch";
16
17
  import { err, ok } from "../store/types";
17
18
  import { createChunkLookup } from "./chunk-lookup";
@@ -21,6 +22,7 @@ import {
21
22
  sortByFinalScoreStable,
22
23
  } from "./content-type-boost";
23
24
  import { formatQueryForEmbedding } from "./contextual";
25
+ import { attachSearchResultEgressLineage } from "./egress-lineage";
24
26
  import { matchesExcludedChunks, matchesExcludedText } from "./exclude";
25
27
  import { selectBestChunkForSteering } from "./intent";
26
28
  import { hasProjectAffinity } from "./project-affinity";
@@ -142,17 +144,18 @@ export async function searchVectorWithEmbedding(
142
144
  }
143
145
 
144
146
  // Cache docs to avoid N+1 queries (filtered by collection and tags)
145
- const docsByMirrorHash = await buildDocumentMap(store, {
146
- collection: options.collection,
147
- relPathPrefix: options.retrievalScope?.relPathPrefix,
148
- tagsAll: options.tagsAll,
149
- tagsAny: options.tagsAny,
150
- since: temporalRange.since,
151
- until: temporalRange.until,
152
- categories: options.categories,
153
- author: options.author,
154
- mirrorHashes: uniqueHashes,
155
- });
147
+ const { documents: docsByMirrorHash, ownershipDocuments } =
148
+ await buildDocumentMap(store, {
149
+ collection: options.collection,
150
+ relPathPrefix: options.retrievalScope?.relPathPrefix,
151
+ tagsAll: options.tagsAll,
152
+ tagsAny: options.tagsAny,
153
+ since: temporalRange.since,
154
+ until: temporalRange.until,
155
+ categories: options.categories,
156
+ author: options.author,
157
+ mirrorHashes: uniqueHashes,
158
+ });
156
159
 
157
160
  // Pre-fetch all chunks in one batch query (eliminates N+1)
158
161
  const chunksMapResult = await store.getChunksBatch(uniqueHashes);
@@ -213,11 +216,12 @@ export async function searchVectorWithEmbedding(
213
216
  const docs = auxiliaryRankingActive ? matchingDocs : [matchingDocs.at(-1)!];
214
217
  for (const doc of docs) {
215
218
  const collectionPath = collectionPaths.get(doc.collection);
219
+ const sourceRelPath = doc.recordSourcePath ?? doc.relPath;
216
220
  const excluded =
217
221
  matchesExcludedText(
218
222
  [
219
223
  doc.title ?? "",
220
- doc.relPath,
224
+ sourceRelPath,
221
225
  doc.author ?? "",
222
226
  doc.contentType ?? "",
223
227
  ...(doc.categories ?? []),
@@ -248,9 +252,9 @@ export async function searchVectorWithEmbedding(
248
252
  endLine: chunk.endLine,
249
253
  },
250
254
  source: {
251
- relPath: doc.relPath,
255
+ relPath: sourceRelPath,
252
256
  absPath: collectionPath
253
- ? `${collectionPath}/${doc.relPath}`
257
+ ? `${collectionPath}/${sourceRelPath}`
254
258
  : undefined,
255
259
  mime: doc.sourceMime,
256
260
  ext: doc.sourceExt,
@@ -266,6 +270,7 @@ export async function searchVectorWithEmbedding(
266
270
  converterVersion: doc.converterVersion ?? undefined,
267
271
  }
268
272
  : undefined,
273
+ record: projectRecordEvidenceMetadata(doc),
269
274
  },
270
275
  doc.collection,
271
276
  contentTypeRules,
@@ -332,6 +337,7 @@ export async function searchVectorWithEmbedding(
332
337
  : undefined;
333
338
 
334
339
  const collectionPath = collectionPaths.get(doc.collection);
340
+ const sourceRelPath = doc.recordSourcePath ?? doc.relPath;
335
341
 
336
342
  const result = applyContentTypeBoost(
337
343
  {
@@ -349,9 +355,9 @@ export async function searchVectorWithEmbedding(
349
355
  ? undefined
350
356
  : { startLine: chunk.startLine, endLine: chunk.endLine },
351
357
  source: {
352
- relPath: doc.relPath,
358
+ relPath: sourceRelPath,
353
359
  absPath: collectionPath
354
- ? `${collectionPath}/${doc.relPath}`
360
+ ? `${collectionPath}/${sourceRelPath}`
355
361
  : undefined,
356
362
  mime: doc.sourceMime,
357
363
  ext: doc.sourceExt,
@@ -367,6 +373,7 @@ export async function searchVectorWithEmbedding(
367
373
  converterVersion: doc.converterVersion ?? undefined,
368
374
  }
369
375
  : undefined,
376
+ record: projectRecordEvidenceMetadata(doc),
370
377
  },
371
378
  doc.collection,
372
379
  contentTypeRules,
@@ -418,6 +425,18 @@ export async function searchVectorWithEmbedding(
418
425
  if (metadata) metadata.retrievalRank = index + 1;
419
426
  }
420
427
  await attachSearchResultContexts(store, finalResults);
428
+ const lineageResult = await attachSearchResultEgressLineage(
429
+ store,
430
+ finalResults,
431
+ {
432
+ ownershipHashes: uniqueHashes,
433
+ ownershipDocuments,
434
+ collections: collectionsResult.ok ? collectionsResult.value : undefined,
435
+ }
436
+ );
437
+ if (!lineageResult.ok) {
438
+ return err("QUERY_FAILED", lineageResult.error.message);
439
+ }
421
440
 
422
441
  const output: SearchResults = {
423
442
  results: finalResults,
@@ -514,6 +533,12 @@ interface DocumentInfo {
514
533
  mirrorHash: string | null;
515
534
  converterId: string | null;
516
535
  converterVersion: string | null;
536
+ recordKey: DocumentRow["recordKey"];
537
+ recordSourcePath: DocumentRow["recordSourcePath"];
538
+ recordSourceLocator: DocumentRow["recordSourceLocator"];
539
+ recordMetadata: DocumentRow["recordMetadata"];
540
+ recordAnchors: DocumentRow["recordAnchors"];
541
+ recordAdapterFingerprint: DocumentRow["recordAdapterFingerprint"];
517
542
  }
518
543
 
519
544
  // ─────────────────────────────────────────────────────────────────────────────
@@ -549,28 +574,35 @@ function matchesCategoryFilter(
549
574
  async function buildDocumentMap(
550
575
  store: StorePort,
551
576
  options: DocumentMapOptions = {}
552
- ): Promise<Map<string, DocumentInfo[]>> {
553
- const result = new Map<string, DocumentInfo[]>();
577
+ ): Promise<{
578
+ documents: Map<string, DocumentInfo[]>;
579
+ ownershipDocuments: DocumentRow[];
580
+ }> {
581
+ const documents = new Map<string, DocumentInfo[]>();
554
582
 
555
583
  if (options.mirrorHashes && options.mirrorHashes.length === 0) {
556
- return result;
584
+ return { documents, ownershipDocuments: [] };
557
585
  }
558
586
 
559
587
  const docs = options.mirrorHashes
560
588
  ? await store.getDocumentsByMirrorHashes(options.mirrorHashes, {
561
- collection: options.collection,
562
589
  activeOnly: true,
563
590
  })
564
591
  : await store.listDocuments(options.collection);
565
592
  if (!docs.ok) {
566
- return result;
593
+ return { documents, ownershipDocuments: [] };
567
594
  }
568
595
 
569
596
  // Filter docs with mirrorHash.
570
597
  // listDocuments path still needs explicit active filter.
571
- const activeDocs = options.mirrorHashes
598
+ const ownershipDocuments = options.mirrorHashes
572
599
  ? docs.value.filter((d) => d.mirrorHash)
573
600
  : docs.value.filter((d) => d.mirrorHash && d.active);
601
+ const activeDocs = options.collection
602
+ ? ownershipDocuments.filter(
603
+ (document) => document.collection === options.collection
604
+ )
605
+ : ownershipDocuments;
574
606
  const temporalRange: TemporalRange = {
575
607
  since: options.since,
576
608
  until: options.until,
@@ -611,10 +643,11 @@ async function buildDocumentMap(
611
643
  }
612
644
 
613
645
  for (const doc of activeDocs) {
646
+ const sourceRelPath = doc.recordSourcePath ?? doc.relPath;
614
647
  if (
615
648
  options.relPathPrefix !== undefined &&
616
- doc.relPath !== options.relPathPrefix &&
617
- !doc.relPath.startsWith(`${options.relPathPrefix}/`)
649
+ sourceRelPath !== options.relPathPrefix &&
650
+ !sourceRelPath.startsWith(`${options.relPathPrefix}/`)
618
651
  ) {
619
652
  continue;
620
653
  }
@@ -655,11 +688,17 @@ async function buildDocumentMap(
655
688
  mirrorHash: doc.mirrorHash,
656
689
  converterId: doc.converterId,
657
690
  converterVersion: doc.converterVersion,
691
+ recordKey: doc.recordKey,
692
+ recordSourcePath: doc.recordSourcePath,
693
+ recordSourceLocator: doc.recordSourceLocator,
694
+ recordMetadata: doc.recordMetadata,
695
+ recordAnchors: doc.recordAnchors,
696
+ recordAdapterFingerprint: doc.recordAdapterFingerprint,
658
697
  };
659
- const matchingDocuments = result.get(doc.mirrorHash!) ?? [];
698
+ const matchingDocuments = documents.get(doc.mirrorHash!) ?? [];
660
699
  matchingDocuments.push(documentInfo);
661
- result.set(doc.mirrorHash!, matchingDocuments);
700
+ documents.set(doc.mirrorHash!, matchingDocuments);
662
701
  }
663
702
 
664
- return result;
703
+ return { documents, ownershipDocuments };
665
704
  }