@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
@@ -492,6 +492,7 @@ export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
492
492
  }
493
493
  await llm.getManager().dispose(modelUri);
494
494
  const recreated = await llm.createEmbeddingPort(modelUri, {
495
+ egressCollections: options.collection ? [options.collection] : "all",
495
496
  policy,
496
497
  onProgress: downloadProgress
497
498
  ? (progress) => downloadProgress("embed", progress)
@@ -508,6 +509,7 @@ export async function embed(options: EmbedOptions = {}): Promise<EmbedResult> {
508
509
  return { ok: true as const, value: recreated.value };
509
510
  };
510
511
  const embedResult = await llm.createEmbeddingPort(modelUri, {
512
+ egressCollections: options.collection ? [options.collection] : "all",
511
513
  policy,
512
514
  onProgress: downloadProgress
513
515
  ? (progress) => downloadProgress("embed", progress)
@@ -21,6 +21,10 @@ import {
21
21
  getDocumentCapabilities,
22
22
  type DocumentCapabilities,
23
23
  } from "../../core/document-capabilities";
24
+ import {
25
+ projectRecordEvidenceMetadata,
26
+ type RecordEvidenceMetadata,
27
+ } from "../../core/record-metadata";
24
28
  import {
25
29
  evidenceFromExactDocument,
26
30
  RetrievalTraceSession as TraceSession,
@@ -85,6 +89,7 @@ export interface GetResponse {
85
89
  converterVersion?: string;
86
90
  mirrorHash?: string;
87
91
  };
92
+ record?: RecordEvidenceMetadata;
88
93
  capabilities: DocumentCapabilities;
89
94
  }
90
95
 
@@ -332,6 +337,7 @@ function buildResponse(ctx: BuildResponseContext): GetResult {
332
337
  language: doc.languageHint ?? undefined,
333
338
  source: buildSourceMeta(doc, config),
334
339
  conversion: buildConversionMeta(doc),
340
+ record: projectRecordEvidenceMetadata(doc),
335
341
  capabilities: buildCapabilities(doc),
336
342
  },
337
343
  };
@@ -349,6 +355,7 @@ interface DocRow {
349
355
  sourceSize: number;
350
356
  sourceMtime?: string;
351
357
  sourceHash: string;
358
+ recordSourcePath?: string | null;
352
359
  }
353
360
 
354
361
  function buildSourceMeta(
@@ -356,11 +363,12 @@ function buildSourceMeta(
356
363
  config: ConfigLike
357
364
  ): GetResponse["source"] {
358
365
  const coll = config.collections.find((c) => c.name === doc.collection);
359
- const absPath = coll ? `${coll.path}/${doc.relPath}` : undefined;
366
+ const relPath = doc.recordSourcePath ?? doc.relPath;
367
+ const absPath = coll ? `${coll.path}/${relPath}` : undefined;
360
368
 
361
369
  return {
362
370
  absPath,
363
- relPath: doc.relPath,
371
+ relPath,
364
372
  mime: doc.sourceMime,
365
373
  ext: doc.sourceExt,
366
374
  modifiedAt: doc.sourceMtime ?? undefined,
@@ -373,11 +381,13 @@ function buildCapabilities(doc: {
373
381
  sourceExt: string;
374
382
  sourceMime: string;
375
383
  mirrorHash?: string | null;
384
+ recordKey?: string | null;
376
385
  }): DocumentCapabilities {
377
386
  return getDocumentCapabilities({
378
387
  sourceExt: doc.sourceExt,
379
388
  sourceMime: doc.sourceMime,
380
389
  contentAvailable: doc.mirrorHash !== null,
390
+ recordKey: doc.recordKey,
381
391
  });
382
392
  }
383
393
 
@@ -32,6 +32,8 @@ export interface IndexOptions {
32
32
  yes?: boolean;
33
33
  /** Verbose output */
34
34
  verbose?: boolean;
35
+ /** Emit the complete structured index receipt. */
36
+ json?: boolean;
35
37
  }
36
38
 
37
39
  /**
@@ -124,6 +126,17 @@ export function formatIndex(
124
126
  return `Error: ${result.error}`;
125
127
  }
126
128
 
129
+ if (options.json) {
130
+ return JSON.stringify(
131
+ {
132
+ syncResult: result.syncResult,
133
+ embedSkipped: result.embedSkipped,
134
+ ...(result.embedResult ? { embedResult: result.embedResult } : {}),
135
+ },
136
+ null,
137
+ 2
138
+ );
139
+ }
127
140
  const { syncResult, embedSkipped } = result;
128
141
  const lines: string[] = ["Indexing complete.", ""];
129
142
 
@@ -17,6 +17,10 @@ import {
17
17
  indexNamesMatch,
18
18
  isValidIndexName,
19
19
  } from "../../app/index-name";
20
+ import {
21
+ projectRecordEvidenceMetadata,
22
+ type RecordEvidenceMetadata,
23
+ } from "../../core/record-metadata";
20
24
  import { isGlobPattern, parseRef, splitRefs } from "./ref-parser";
21
25
  import { initStore } from "./shared";
22
26
 
@@ -53,6 +57,7 @@ export interface MultiGetDocument {
53
57
  truncated?: boolean;
54
58
  totalLines?: number;
55
59
  source: { absPath?: string; relPath: string; mime: string; ext: string };
60
+ record?: RecordEvidenceMetadata;
56
61
  }
57
62
 
58
63
  export interface SkippedDoc {
@@ -230,6 +235,7 @@ async function fetchSingleDocument(
230
235
  );
231
236
  const coll = ctx.config.collections.find((c) => c.name === doc.collection);
232
237
 
238
+ const sourceRelPath = doc.recordSourcePath ?? doc.relPath;
233
239
  ctx.documents.push({
234
240
  docid: doc.docid,
235
241
  uri: decorateUriForIndex(doc.uri, ctx.indexName),
@@ -238,11 +244,12 @@ async function fetchSingleDocument(
238
244
  truncated: truncated || undefined,
239
245
  totalLines: content.split("\n").length,
240
246
  source: {
241
- absPath: coll ? `${coll.path}/${doc.relPath}` : undefined,
242
- relPath: doc.relPath,
247
+ absPath: coll ? `${coll.path}/${sourceRelPath}` : undefined,
248
+ relPath: sourceRelPath,
243
249
  mime: doc.sourceMime,
244
250
  ext: doc.sourceExt,
245
251
  },
252
+ record: projectRecordEvidenceMetadata(doc),
246
253
  });
247
254
  }
248
255
 
@@ -195,6 +195,9 @@ export async function query(
195
195
  }
196
196
  traceSession = traceStart.value ?? undefined;
197
197
  const llm = new LlmAdapter(config);
198
+ const egressCollections = options.collection
199
+ ? [options.collection]
200
+ : ("all" as const);
198
201
 
199
202
  // Resolve download policy from env/flags
200
203
  const globals = getGlobals();
@@ -210,6 +213,7 @@ export async function query(
210
213
 
211
214
  // Create embedding port (for vector search)
212
215
  const embedResult = await llm.createEmbeddingPort(embedUri, {
216
+ egressCollections,
213
217
  policy,
214
218
  onProgress: downloadProgress
215
219
  ? (progress) => downloadProgress("embed", progress)
@@ -223,6 +227,7 @@ export async function query(
223
227
  // Skip when structured query modes are provided.
224
228
  if (expandUri) {
225
229
  const genResult = await llm.createExpansionPort(expandUri, {
230
+ egressCollections,
226
231
  policy,
227
232
  onProgress: downloadProgress
228
233
  ? (progress) => downloadProgress("expand", progress)
@@ -236,6 +241,7 @@ export async function query(
236
241
  // Create rerank port - optional
237
242
  if (rerankUri) {
238
243
  const rerankResult = await llm.createRerankPort(rerankUri, {
244
+ egressCollections,
239
245
  policy,
240
246
  onProgress: downloadProgress
241
247
  ? (progress) => downloadProgress("rerank", progress)
@@ -354,12 +360,16 @@ export async function queryDiagnose(
354
360
  const shouldCreateAnyModel =
355
361
  shouldCreateEmbeddingPort || !options.noExpand || !options.noRerank;
356
362
  const llm = shouldCreateAnyModel ? new LlmAdapter(config) : null;
363
+ const egressCollections = options.collection
364
+ ? [options.collection]
365
+ : ("all" as const);
357
366
 
358
367
  const embedUri = shouldCreateEmbeddingPort
359
368
  ? resolveModelUri(config, "embed", options.embedModel, options.collection)
360
369
  : null;
361
370
  if (embedUri && llm) {
362
371
  const embedResult = await llm.createEmbeddingPort(embedUri, {
372
+ egressCollections,
363
373
  policy,
364
374
  onProgress: downloadProgress
365
375
  ? (progress) => downloadProgress("embed", progress)
@@ -378,6 +388,7 @@ export async function queryDiagnose(
378
388
  options.collection
379
389
  );
380
390
  const genResult = await llm.createExpansionPort(expandUri, {
391
+ egressCollections,
381
392
  policy,
382
393
  onProgress: downloadProgress
383
394
  ? (progress) => downloadProgress("expand", progress)
@@ -396,6 +407,7 @@ export async function queryDiagnose(
396
407
  options.collection
397
408
  );
398
409
  const rerankResult = await llm.createRerankPort(rerankUri, {
410
+ egressCollections,
399
411
  policy,
400
412
  onProgress: downloadProgress
401
413
  ? (progress) => downloadProgress("rerank", progress)
@@ -91,16 +91,26 @@ export const traceReplay = async (
91
91
  const policy = resolveDownloadPolicy(process.env, {
92
92
  offline: options.offline,
93
93
  });
94
+ const egressCollections = "all" as const;
94
95
  if (embedUri) {
95
- const created = await llm.createEmbeddingPort(embedUri, { policy });
96
+ const created = await llm.createEmbeddingPort(embedUri, {
97
+ egressCollections,
98
+ policy,
99
+ });
96
100
  if (created.ok) embedPort = created.value;
97
101
  }
98
102
  if (expandUri) {
99
- const created = await llm.createExpansionPort(expandUri, { policy });
103
+ const created = await llm.createExpansionPort(expandUri, {
104
+ egressCollections,
105
+ policy,
106
+ });
100
107
  if (created.ok) expandPort = created.value;
101
108
  }
102
109
  if (rerankUri) {
103
- const created = await llm.createRerankPort(rerankUri, { policy });
110
+ const created = await llm.createRerankPort(rerankUri, {
111
+ egressCollections,
112
+ policy,
113
+ });
104
114
  if (created.ok) rerankPort = created.value;
105
115
  }
106
116
  if (embedPort && embedUri) {
@@ -168,6 +168,34 @@ export function formatSyncResultLines(
168
168
  lines.push(` ${c.filesMarkedInactive} marked inactive`);
169
169
  }
170
170
 
171
+ for (const file of c.files ?? []) {
172
+ const receipt = file.recordImport;
173
+ if (
174
+ !receipt ||
175
+ (receipt.failures.length === 0 && receipt.warnings.length === 0)
176
+ )
177
+ continue;
178
+ const warningCount = receipt.failures.length + receipt.warnings.length;
179
+ lines.push(
180
+ ` ${file.relPath}: ${warningCount} record warning${warningCount === 1 ? "" : "s"} (${receipt.snapshotState} snapshot)`
181
+ );
182
+ if (options.verbose) {
183
+ for (const warning of receipt.warnings) {
184
+ lines.push(
185
+ ` [${warning.code}]: ${warning.message} (retryable=${warning.retryable ? "yes" : "no"})`
186
+ );
187
+ }
188
+ for (const failure of receipt.failures) {
189
+ const locator = failure.sourceLocator
190
+ ? ` at ${failure.sourceLocator}`
191
+ : "";
192
+ lines.push(
193
+ ` [${failure.code}]${locator}: ${failure.message} (retryable=${failure.retryable ? "yes" : "no"})`
194
+ );
195
+ }
196
+ }
197
+ }
198
+
171
199
  if (options.verbose && c.errors.length > 0) {
172
200
  for (const err of c.errors) {
173
201
  lines.push(` [${err.code}] ${err.relPath}: ${err.message}`);
@@ -24,6 +24,8 @@ export interface UpdateOptions {
24
24
  gitPull?: boolean;
25
25
  /** Verbose output */
26
26
  verbose?: boolean;
27
+ /** Emit the complete structured sync receipt. */
28
+ json?: boolean;
27
29
  }
28
30
 
29
31
  /**
@@ -80,5 +82,8 @@ export function formatUpdate(
80
82
  return `Error: ${result.error}`;
81
83
  }
82
84
 
85
+ if (options.json) {
86
+ return JSON.stringify(result.result, null, 2);
87
+ }
83
88
  return formatSyncResultLines(result.result, options).join("\n");
84
89
  }
@@ -128,7 +128,9 @@ export async function vsearch(
128
128
 
129
129
  // Create LLM adapter for embeddings
130
130
  const llm = new LlmAdapter(config);
131
- const embedResult = await llm.createEmbeddingPort(modelUri);
131
+ const embedResult = await llm.createEmbeddingPort(modelUri, {
132
+ egressCollections: options.collection ? [options.collection] : "all",
133
+ });
132
134
  if (!embedResult.ok) {
133
135
  await traceSession?.finish("failed");
134
136
  return { success: false, error: embedResult.error.message };
@@ -1509,6 +1509,7 @@ function wireOnboardingCommands(program: Command): void {
1509
1509
  .option("--no-embed", "skip embedding after sync")
1510
1510
  .option("--git-pull", "run git pull in git repositories")
1511
1511
  .option("--models-pull", "download models if missing")
1512
+ .option("--json", "JSON output")
1512
1513
  .action(
1513
1514
  async (
1514
1515
  collection: string | undefined,
@@ -1525,6 +1526,7 @@ function wireOnboardingCommands(program: Command): void {
1525
1526
  modelsPull: Boolean(cmdOpts.modelsPull),
1526
1527
  yes: globals.yes,
1527
1528
  verbose: globals.verbose,
1529
+ json: getFormat(cmdOpts) === "json",
1528
1530
  };
1529
1531
  const result = await index(opts);
1530
1532
 
@@ -2103,6 +2105,129 @@ function wireManagementCommands(program: Command): void {
2103
2105
  });
2104
2106
  });
2105
2107
 
2108
+ const collectionPolicyCmd = collectionCmd
2109
+ .command("policy")
2110
+ .description("Inspect and change collection egress policy");
2111
+
2112
+ collectionPolicyCmd
2113
+ .command("get <name>")
2114
+ .description("Show effective policy, source, and confirmation revision")
2115
+ .action(async (name: string) => {
2116
+ const { collectionPolicyGet } = await import("./commands/collection");
2117
+ await collectionPolicyGet(name, { configPath: getGlobals().config });
2118
+ });
2119
+
2120
+ collectionPolicyCmd
2121
+ .command("set <name> <policy>")
2122
+ .description("Set local_only, lan, or remote policy")
2123
+ .option(
2124
+ "--confirm-relaxation <current-revision>",
2125
+ "confirm relaxation against the current policy revision"
2126
+ )
2127
+ .action(
2128
+ async (
2129
+ name: string,
2130
+ policy: string,
2131
+ cmdOpts: Record<string, unknown>
2132
+ ) => {
2133
+ if (!["local_only", "lan", "remote"].includes(policy)) {
2134
+ throw new CliError(
2135
+ "VALIDATION",
2136
+ "Policy must be local_only, lan, or remote"
2137
+ );
2138
+ }
2139
+ const { collectionPolicySet } = await import("./commands/collection");
2140
+ await collectionPolicySet(
2141
+ name,
2142
+ policy as "local_only" | "lan" | "remote",
2143
+ {
2144
+ configPath: getGlobals().config,
2145
+ confirmRelaxation: cmdOpts.confirmRelaxation as string | undefined,
2146
+ }
2147
+ );
2148
+ }
2149
+ );
2150
+
2151
+ collectionPolicyCmd
2152
+ .command("check")
2153
+ .description("Check or explain an exact egress decision without acting")
2154
+ .requiredOption("--action <action>", "egress action")
2155
+ .requiredOption(
2156
+ "--destination <zone>",
2157
+ "local_process, loopback, lan, remote"
2158
+ )
2159
+ .requiredOption("--content-class <class>", "content class")
2160
+ .option(
2161
+ "-c, --collection <name>",
2162
+ "collection scope (repeatable)",
2163
+ collectRepeatableValue,
2164
+ []
2165
+ )
2166
+ .option("--authenticated", "caller authentication succeeded")
2167
+ .option("--authorized", "operation authorization succeeded")
2168
+ .option("--partial", "explicitly permit partial collection results")
2169
+ .option(
2170
+ "--explain-egress",
2171
+ "return the same decision contract without performing an action"
2172
+ )
2173
+ .action(async (cmdOpts: Record<string, unknown>) => {
2174
+ const { collectionPolicyCheck } = await import("./commands/collection");
2175
+ await collectionPolicyCheck(
2176
+ {
2177
+ collections: cmdOpts.collection as string[],
2178
+ action: cmdOpts.action as Parameters<
2179
+ typeof collectionPolicyCheck
2180
+ >[0]["action"],
2181
+ destinationZone: cmdOpts.destination as Parameters<
2182
+ typeof collectionPolicyCheck
2183
+ >[0]["destinationZone"],
2184
+ contentClass: cmdOpts.contentClass as Parameters<
2185
+ typeof collectionPolicyCheck
2186
+ >[0]["contentClass"],
2187
+ authenticated: Boolean(cmdOpts.authenticated),
2188
+ authorized: Boolean(cmdOpts.authorized),
2189
+ partial: Boolean(cmdOpts.partial),
2190
+ },
2191
+ { configPath: getGlobals().config }
2192
+ );
2193
+ });
2194
+
2195
+ const egressAuditCmd = program
2196
+ .command("egress-audit")
2197
+ .description("Manage local content-free egress audit receipts");
2198
+ egressAuditCmd
2199
+ .command("list")
2200
+ .option("--limit <count>", "page size", Number)
2201
+ .option("--cursor <cursor>", "opaque next-page cursor")
2202
+ .action(async (cmdOpts: Record<string, unknown>) => {
2203
+ const { egressAuditList } = await import("./commands/egress-audit");
2204
+ await egressAuditList(
2205
+ {
2206
+ limit: cmdOpts.limit as number | undefined,
2207
+ cursor: cmdOpts.cursor as string | undefined,
2208
+ },
2209
+ { configPath: getGlobals().config }
2210
+ );
2211
+ });
2212
+ egressAuditCmd.command("show <audit-id>").action(async (auditId: string) => {
2213
+ const { egressAuditShow } = await import("./commands/egress-audit");
2214
+ await egressAuditShow(auditId, { configPath: getGlobals().config });
2215
+ });
2216
+ egressAuditCmd.command("status").action(async () => {
2217
+ const { egressAuditStatus } = await import("./commands/egress-audit");
2218
+ await egressAuditStatus({ configPath: getGlobals().config });
2219
+ });
2220
+ egressAuditCmd
2221
+ .command("delete <audit-id>")
2222
+ .action(async (auditId: string) => {
2223
+ const { egressAuditDelete } = await import("./commands/egress-audit");
2224
+ await egressAuditDelete(auditId, { configPath: getGlobals().config });
2225
+ });
2226
+ egressAuditCmd.command("purge").action(async () => {
2227
+ const { egressAuditPurge } = await import("./commands/egress-audit");
2228
+ await egressAuditPurge({ configPath: getGlobals().config });
2229
+ });
2230
+
2106
2231
  // context subcommands
2107
2232
  const contextCmd = program
2108
2233
  .command("context")
@@ -2515,6 +2640,7 @@ function wireManagementCommands(program: Command): void {
2515
2640
  .command("update")
2516
2641
  .description("Sync files from disk into the index")
2517
2642
  .option("--git-pull", "run git pull in git repositories")
2643
+ .option("--json", "JSON output")
2518
2644
  .action(async (cmdOpts: Record<string, unknown>) => {
2519
2645
  const globals = getGlobals();
2520
2646
  const { update, formatUpdate } = await import("./commands/update");
@@ -2523,6 +2649,7 @@ function wireManagementCommands(program: Command): void {
2523
2649
  indexName: globals.index,
2524
2650
  gitPull: Boolean(cmdOpts.gitPull),
2525
2651
  verbose: globals.verbose,
2652
+ json: getFormat(cmdOpts) === "json",
2526
2653
  };
2527
2654
  const result = await update(opts);
2528
2655
 
@@ -59,6 +59,7 @@ export {
59
59
  CollectionSchema,
60
60
  type Config,
61
61
  ConfigSchema,
62
+ DEFAULT_EGRESS_POLICY,
62
63
  HttpGatewayConfigSchema,
63
64
  HttpGatewayLimitsSchema,
64
65
  CONTENT_TYPE_GRAPH_HINTS,
@@ -70,11 +71,19 @@ export {
70
71
  DEFAULT_EXCLUDES,
71
72
  DEFAULT_FTS_TOKENIZER,
72
73
  DEFAULT_PATTERN,
74
+ type EffectiveConfiguredEgressPolicy,
75
+ EGRESS_POLICIES,
76
+ EGRESS_POLICY_SOURCES,
77
+ type EgressPolicy,
78
+ EgressPolicySchema,
79
+ type EgressPolicySource,
80
+ EgressPolicySourceSchema,
73
81
  FTS_TOKENIZERS,
74
82
  type FtsTokenizer,
75
83
  getCollectionFromScope,
76
84
  isValidLanguageHint,
77
85
  parseScope,
86
+ resolveConfiguredEgressPolicy,
78
87
  type ProjectProfileBinding,
79
88
  ProjectProfileBindingSchema,
80
89
  type ScopeType,
@@ -5,6 +5,7 @@ import { hasLikelySecretPath } from "../core/path-rules";
5
5
  import {
6
6
  CONTENT_TYPE_SEARCH_BOOST_MAX,
7
7
  CONTENT_TYPE_SEARCH_BOOST_MIN,
8
+ CollectionSchema,
8
9
  isValidLanguageHint,
9
10
  PROJECT_AFFINITY_MAX_CONTRIBUTION,
10
11
  } from "./types";
@@ -218,6 +219,7 @@ export const ProjectProfileCollectionSchema = z
218
219
  .refine(isValidLanguageHint, "Invalid BCP-47 language hint")
219
220
  .optional(),
220
221
  modelPreset: z.string().regex(REFERENCE_PATTERN).optional(),
222
+ recordAdapters: CollectionSchema.shape.recordAdapters,
221
223
  })
222
224
  .strict();
223
225
 
@@ -10,6 +10,7 @@ import { isAbsolute } from "node:path";
10
10
  import { z } from "zod";
11
11
 
12
12
  import { URI_PREFIX } from "../app/constants";
13
+ import { JsonlFieldMappingSchema } from "../converters/adapters/jsonl/config";
13
14
  import { RetrievalTraceConfigSchema } from "./retrieval-traces";
14
15
 
15
16
  // ─────────────────────────────────────────────────────────────────────────────
@@ -47,6 +48,28 @@ export type FtsTokenizer = (typeof FTS_TOKENIZERS)[number];
47
48
  /** Default FTS tokenizer - snowball english for multilingual stemming */
48
49
  export const DEFAULT_FTS_TOKENIZER: FtsTokenizer = "snowball english";
49
50
 
51
+ /** Collection-owned boundary for where indexed content may travel. */
52
+ export const EGRESS_POLICIES = ["local_only", "lan", "remote"] as const;
53
+ export const EgressPolicySchema = z.enum(EGRESS_POLICIES);
54
+ export type EgressPolicy = z.infer<typeof EgressPolicySchema>;
55
+
56
+ /** Missing policy is always interpreted as the fail-closed local-only default. */
57
+ export const DEFAULT_EGRESS_POLICY: EgressPolicy = "local_only";
58
+
59
+ /** Provenance for an effective collection egress policy. */
60
+ export const EGRESS_POLICY_SOURCES = [
61
+ "explicit",
62
+ "config_default",
63
+ "legacy_default",
64
+ ] as const;
65
+ export const EgressPolicySourceSchema = z.enum(EGRESS_POLICY_SOURCES);
66
+ export type EgressPolicySource = z.infer<typeof EgressPolicySourceSchema>;
67
+
68
+ export interface EffectiveConfiguredEgressPolicy {
69
+ policy: EgressPolicy;
70
+ source: Extract<EgressPolicySource, "explicit" | "config_default">;
71
+ }
72
+
50
73
  /**
51
74
  * BCP-47 language tag pattern (simplified, case-insensitive).
52
75
  * Matches: en, de, fr, zh-CN, zh-Hans, und, en-US, etc.
@@ -103,6 +126,18 @@ export const CollectionSchema = z.object({
103
126
  })
104
127
  .optional(),
105
128
 
129
+ /**
130
+ * Explicit collection egress boundary. Absence remains valid for legacy
131
+ * configs and resolves to local_only rather than relaxing access.
132
+ */
133
+ egressPolicy: EgressPolicySchema.optional(),
134
+
135
+ /**
136
+ * Durable monotonic token for egress policy/source changes. Legacy configs
137
+ * start at zero; guarded mutations advance it under the config write lock.
138
+ */
139
+ egressPolicyRevision: z.number().int().nonnegative().optional(),
140
+
106
141
  /** Optional per-collection model overrides */
107
142
  models: z
108
143
  .object({
@@ -112,11 +147,42 @@ export const CollectionSchema = z.object({
112
147
  gen: z.string().min(1).optional(),
113
148
  })
114
149
  .optional(),
150
+
151
+ /** Optional declarative overrides for ambiguous export formats. */
152
+ recordAdapters: z
153
+ .object({
154
+ jsonl: z
155
+ .object({ fieldMapping: JsonlFieldMappingSchema.optional() })
156
+ .strict()
157
+ .optional(),
158
+ transcript: z
159
+ .object({ format: z.enum(["json", "srt", "text", "vtt"]) })
160
+ .strict()
161
+ .optional(),
162
+ })
163
+ .strict()
164
+ .optional(),
115
165
  });
116
166
 
117
167
  export type Collection = z.infer<typeof CollectionSchema>;
118
168
  export type CollectionModelOverrides = NonNullable<Collection["models"]>;
119
169
 
170
+ /** Resolve config input without erasing whether the user made an explicit choice. */
171
+ export function resolveConfiguredEgressPolicy(
172
+ collection: Pick<Collection, "egressPolicy">
173
+ ): EffectiveConfiguredEgressPolicy {
174
+ if (collection.egressPolicy === undefined) {
175
+ return {
176
+ policy: DEFAULT_EGRESS_POLICY,
177
+ source: "config_default",
178
+ };
179
+ }
180
+ return {
181
+ policy: collection.egressPolicy,
182
+ source: "explicit",
183
+ };
184
+ }
185
+
120
186
  // ─────────────────────────────────────────────────────────────────────────────
121
187
  // Project Affinity Input
122
188
  // ─────────────────────────────────────────────────────────────────────────────