@remnic/core 9.3.710 → 9.3.712

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 (177) hide show
  1. package/dist/access-boundary.d.ts +5 -5
  2. package/dist/access-boundary.js +4 -4
  3. package/dist/access-cli.js +7 -7
  4. package/dist/access-http.d.ts +5 -5
  5. package/dist/access-http.js +8 -8
  6. package/dist/access-mcp.d.ts +5 -5
  7. package/dist/access-mcp.js +7 -7
  8. package/dist/access-operations-batch.js +5 -5
  9. package/dist/access-operations.d.ts +5 -5
  10. package/dist/access-operations.js +6 -6
  11. package/dist/{access-service-CoIA0NrG.d.ts → access-service-Dx2rSJjH.d.ts} +3 -3
  12. package/dist/access-service.d.ts +5 -5
  13. package/dist/access-service.js +3 -3
  14. package/dist/access-surface-catalog.d.ts +5 -5
  15. package/dist/action-confidence.d.ts +1 -1
  16. package/dist/active-memory-bridge.d.ts +1 -1
  17. package/dist/active-recall.d.ts +1 -1
  18. package/dist/behavior-learner.d.ts +1 -1
  19. package/dist/behavior-signals.d.ts +1 -1
  20. package/dist/bootstrap.d.ts +4 -4
  21. package/dist/briefing.d.ts +1 -1
  22. package/dist/buffer-surprise-report.d.ts +1 -1
  23. package/dist/buffer.d.ts +1 -1
  24. package/dist/calibration.d.ts +1 -1
  25. package/dist/capabilities.d.ts +1 -1
  26. package/dist/{catalog-DQCZrBjw.d.ts → catalog-CadPvH29.d.ts} +1 -1
  27. package/dist/causal-behavior.d.ts +1 -1
  28. package/dist/causal-consolidation.d.ts +1 -1
  29. package/dist/{chunk-OK7FUX6R.js → chunk-4P5UAL3C.js} +46 -1
  30. package/dist/chunk-4P5UAL3C.js.map +1 -0
  31. package/dist/{chunk-NK3SPJLM.js → chunk-5KQCOIPW.js} +7 -7
  32. package/dist/{chunk-Y6PIFKXX.js → chunk-5QUS24F7.js} +22 -2
  33. package/dist/chunk-5QUS24F7.js.map +1 -0
  34. package/dist/{chunk-VWB3HDY6.js → chunk-A2R5NV6O.js} +4 -4
  35. package/dist/{chunk-XD33EX2F.js → chunk-AMXOCZHE.js} +3 -3
  36. package/dist/{chunk-2T4TDXPC.js → chunk-GKPIY7EQ.js} +13 -3
  37. package/dist/chunk-GKPIY7EQ.js.map +1 -0
  38. package/dist/{chunk-KOEKDZ6A.js → chunk-I3BT2IDW.js} +3 -3
  39. package/dist/{chunk-AZVHBFI3.js → chunk-OWOKOFXK.js} +9 -1
  40. package/dist/chunk-OWOKOFXK.js.map +1 -0
  41. package/dist/{chunk-EVX52NCY.js → chunk-YVWZW37D.js} +3 -3
  42. package/dist/{chunk-P6PRSI3W.js → chunk-Z3B2YW56.js} +3 -3
  43. package/dist/{chunk-JQ7XVM4V.js → chunk-ZBG4K36I.js} +2 -2
  44. package/dist/{chunk-PKE7EJMX.js → chunk-ZVDRL6SK.js} +2 -2
  45. package/dist/{cli-qex-L3GT.d.ts → cli-DTWRoJEI.d.ts} +3 -3
  46. package/dist/cli.d.ts +6 -6
  47. package/dist/cli.js +11 -11
  48. package/dist/compounding/engine.d.ts +1 -1
  49. package/dist/compounding/preference-consolidator.d.ts +1 -1
  50. package/dist/compression-optimizer.d.ts +1 -1
  51. package/dist/config.d.ts +1 -1
  52. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  53. package/dist/connectors/codex-materialize.d.ts +1 -1
  54. package/dist/connectors/index.d.ts +1 -1
  55. package/dist/consolidation-provenance-check.d.ts +1 -1
  56. package/dist/consolidation-undo.d.ts +1 -1
  57. package/dist/contradiction/index.d.ts +1 -1
  58. package/dist/conversation-index/backend.d.ts +1 -1
  59. package/dist/conversation-index/chunker.d.ts +1 -1
  60. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  61. package/dist/conversation-index/indexer.d.ts +1 -1
  62. package/dist/conversation-index/search.d.ts +1 -1
  63. package/dist/day-summary.d.ts +1 -1
  64. package/dist/delinearize.d.ts +1 -1
  65. package/dist/direct-answer-wiring.d.ts +1 -1
  66. package/dist/direct-answer.d.ts +1 -1
  67. package/dist/embedding-fallback.d.ts +1 -1
  68. package/dist/enrichment/index.d.ts +1 -1
  69. package/dist/entity-retrieval.d.ts +1 -1
  70. package/dist/entity-schema.d.ts +1 -1
  71. package/dist/explicit-capture.d.ts +4 -4
  72. package/dist/extraction-faithfulness.d.ts +1 -1
  73. package/dist/extraction-judge-telemetry.d.ts +1 -1
  74. package/dist/extraction-judge-training.d.ts +1 -1
  75. package/dist/extraction-judge.d.ts +1 -1
  76. package/dist/extraction.d.ts +1 -1
  77. package/dist/fallback-llm.d.ts +1 -1
  78. package/dist/identity-continuity.d.ts +1 -1
  79. package/dist/importance.d.ts +1 -1
  80. package/dist/index.d.ts +9 -9
  81. package/dist/index.js +12 -12
  82. package/dist/intent.d.ts +1 -1
  83. package/dist/lcm/engine.d.ts +1 -1
  84. package/dist/lcm/index.d.ts +1 -1
  85. package/dist/lcm/tools.d.ts +1 -1
  86. package/dist/lifecycle.d.ts +1 -1
  87. package/dist/live-connectors-runner.d.ts +1 -1
  88. package/dist/local-llm.d.ts +1 -1
  89. package/dist/maintenance/memory-governance.d.ts +1 -1
  90. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  91. package/dist/memory-action-policy.d.ts +1 -1
  92. package/dist/memory-cache.d.ts +1 -1
  93. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  94. package/dist/memory-projection-store.d.ts +1 -1
  95. package/dist/memory-provenance.d.ts +1 -1
  96. package/dist/memory-worth-outcomes.d.ts +1 -1
  97. package/dist/models-json.d.ts +1 -1
  98. package/dist/namespaces/migrate.d.ts +2 -2
  99. package/dist/namespaces/principal.d.ts +1 -1
  100. package/dist/namespaces/search.d.ts +1 -1
  101. package/dist/namespaces/storage.d.ts +2 -2
  102. package/dist/native-knowledge.d.ts +1 -1
  103. package/dist/operator-toolkit.d.ts +20 -2
  104. package/dist/operator-toolkit.js +3 -1
  105. package/dist/orchestration/maintenance.d.ts +2 -2
  106. package/dist/{orchestrator-DsVKLEBk.d.ts → orchestrator-B1iVjEyc.d.ts} +3 -3
  107. package/dist/orchestrator.d.ts +4 -4
  108. package/dist/orchestrator.js +2 -2
  109. package/dist/patterns-cli.d.ts +1 -1
  110. package/dist/policy-runtime.d.ts +1 -1
  111. package/dist/provenance.d.ts +1 -1
  112. package/dist/qmd-recall-cache.d.ts +1 -1
  113. package/dist/qmd.d.ts +1 -1
  114. package/dist/recall-disclosure-escalation.d.ts +1 -1
  115. package/dist/recall-explain-renderer.d.ts +1 -1
  116. package/dist/recall-explain-renderer.js +2 -2
  117. package/dist/recall-planner-llm.d.ts +1 -1
  118. package/dist/recall-state.d.ts +1 -1
  119. package/dist/recall-tag-filter.d.ts +1 -1
  120. package/dist/recall-xray-cli.d.ts +1 -1
  121. package/dist/recall-xray-cli.js +3 -3
  122. package/dist/recall-xray-renderer.d.ts +1 -1
  123. package/dist/recall-xray-renderer.js +2 -2
  124. package/dist/recall-xray.d.ts +15 -1
  125. package/dist/recall-xray.js +1 -1
  126. package/dist/resolve-auth-token.d.ts +1 -1
  127. package/dist/retrieval-agents.d.ts +1 -1
  128. package/dist/retrieval-tiers.d.ts +1 -1
  129. package/dist/routing/engine.d.ts +1 -1
  130. package/dist/routing/store.d.ts +1 -1
  131. package/dist/search/embed-helper.d.ts +1 -1
  132. package/dist/search/factory.d.ts +1 -1
  133. package/dist/search/index.d.ts +1 -1
  134. package/dist/search/lancedb-backend.d.ts +1 -1
  135. package/dist/search/meilisearch-backend.d.ts +1 -1
  136. package/dist/search/noop-backend.d.ts +1 -1
  137. package/dist/search/orama-backend.d.ts +1 -1
  138. package/dist/search/port.d.ts +1 -1
  139. package/dist/search/remote-backend.d.ts +1 -1
  140. package/dist/{semantic-consolidation-_hVxkTuF.d.ts → semantic-consolidation-CY8uVRNJ.d.ts} +1 -1
  141. package/dist/semantic-consolidation.d.ts +2 -2
  142. package/dist/semantic-rule-verifier.d.ts +1 -1
  143. package/dist/session-observer-bands.d.ts +1 -1
  144. package/dist/session-observer-state.d.ts +1 -1
  145. package/dist/shared-context/manager.d.ts +1 -1
  146. package/dist/signal.d.ts +1 -1
  147. package/dist/storage.d.ts +1 -1
  148. package/dist/summarizer.d.ts +1 -1
  149. package/dist/summary-snapshot.d.ts +1 -1
  150. package/dist/temporal-supersession.d.ts +1 -1
  151. package/dist/temporal-validity.d.ts +1 -1
  152. package/dist/threading.d.ts +1 -1
  153. package/dist/tier-migration.d.ts +1 -1
  154. package/dist/tier-routing.d.ts +1 -1
  155. package/dist/topics.d.ts +1 -1
  156. package/dist/transcript.d.ts +1 -1
  157. package/dist/{types-DUK4vVnN.d.ts → types-Dr0Cw3L9.d.ts} +1 -1
  158. package/dist/types.d.ts +1 -1
  159. package/dist/utility-runtime.d.ts +1 -1
  160. package/package.json +2 -2
  161. package/src/operator-toolkit.ts +80 -0
  162. package/src/orchestrator.ts +13 -0
  163. package/src/provenance-surfaces.test.ts +254 -0
  164. package/src/recall-xray-renderer.ts +28 -0
  165. package/src/recall-xray.ts +32 -0
  166. package/dist/chunk-2T4TDXPC.js.map +0 -1
  167. package/dist/chunk-AZVHBFI3.js.map +0 -1
  168. package/dist/chunk-OK7FUX6R.js.map +0 -1
  169. package/dist/chunk-Y6PIFKXX.js.map +0 -1
  170. /package/dist/{chunk-NK3SPJLM.js.map → chunk-5KQCOIPW.js.map} +0 -0
  171. /package/dist/{chunk-VWB3HDY6.js.map → chunk-A2R5NV6O.js.map} +0 -0
  172. /package/dist/{chunk-XD33EX2F.js.map → chunk-AMXOCZHE.js.map} +0 -0
  173. /package/dist/{chunk-KOEKDZ6A.js.map → chunk-I3BT2IDW.js.map} +0 -0
  174. /package/dist/{chunk-EVX52NCY.js.map → chunk-YVWZW37D.js.map} +0 -0
  175. /package/dist/{chunk-P6PRSI3W.js.map → chunk-Z3B2YW56.js.map} +0 -0
  176. /package/dist/{chunk-JQ7XVM4V.js.map → chunk-ZBG4K36I.js.map} +0 -0
  177. /package/dist/{chunk-PKE7EJMX.js.map → chunk-ZVDRL6SK.js.map} +0 -0
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Issue #1575 PR 3 — Claim-level provenance surfaces: X-ray rendering +
3
+ * `remnic doctor` coverage distribution.
4
+ *
5
+ * PR 1 (types + storage round-trip) and PR 2 (extraction validator) are
6
+ * covered by provenance-frontmatter.test.ts and provenance-extraction.test.ts.
7
+ * This file pins the READ surfaces that operators and downstream features
8
+ * (#1576 faithfulness, #1577 TrustScore, #1580 correction) consume:
9
+ *
10
+ * - X-ray text/markdown rows render the first quote + observedAt.
11
+ * - cloneResult deep-copies sourceSpan so getLastXraySnapshot is independent.
12
+ * - `remnic doctor` reports the verified/unverified/none distribution.
13
+ */
14
+
15
+ import test from "node:test";
16
+ import assert from "node:assert/strict";
17
+
18
+ import type { RecallXraySnapshot, RecallXrayResult } from "./recall-xray.js";
19
+ import { buildXraySnapshot } from "./recall-xray.js";
20
+ import { renderXrayText, renderXrayMarkdown } from "./recall-xray-renderer.js";
21
+
22
+ // ─── X-ray rendering fixtures ────────────────────────────────────────────
23
+
24
+ function resultWithSourceSpan(): RecallXrayResult {
25
+ return {
26
+ memoryId: "fact-prov-1",
27
+ path: "facts/tech/database.md",
28
+ servedBy: "hybrid",
29
+ scoreDecomposition: { final: 0.82 },
30
+ admittedBy: ["namespace"],
31
+ sourceSpan: {
32
+ quote: "We migrated the production database to pgBouncer",
33
+ observedAt: "2026-05-03T10:00:00.000Z",
34
+ provenance: "verified",
35
+ },
36
+ };
37
+ }
38
+
39
+ function snapshotWithSourceSpan(): RecallXraySnapshot {
40
+ return {
41
+ schemaVersion: "1",
42
+ query: "what database?",
43
+ capturedAt: 1_700_000_000_000,
44
+ snapshotId: "22222222-2222-2222-2222-222222222222",
45
+ tierExplain: null,
46
+ results: [resultWithSourceSpan()],
47
+ filters: [{ name: "recall-result-limit", considered: 1, admitted: 1 }],
48
+ budget: { chars: 4096, used: 100 },
49
+ };
50
+ }
51
+
52
+ // ─── X-ray text rendering ─────────────────────────────────────────────────
53
+
54
+ test("renderXrayText: sourceSpan renders quote + observedAt + provenance tag", () => {
55
+ const text = renderXrayText(snapshotWithSourceSpan());
56
+ assert.match(
57
+ text,
58
+ /source: "We migrated the production database to pgBouncer" \(observed 2026-05-03T10:00:00\.000Z\) \[verified\]/,
59
+ "text renderer must surface the source span line",
60
+ );
61
+ });
62
+
63
+ test("renderXrayText: absent sourceSpan produces no source line", () => {
64
+ const snap = buildXraySnapshot({
65
+ query: "q",
66
+ tierExplain: null,
67
+ results: [
68
+ {
69
+ memoryId: "fact-legacy",
70
+ path: "facts/legacy.md",
71
+ servedBy: "hybrid",
72
+ scoreDecomposition: { final: 0.5 },
73
+ admittedBy: [],
74
+ },
75
+ ],
76
+ filters: [],
77
+ budget: { chars: 4096, used: 0 },
78
+ });
79
+ const text = renderXrayText(snap);
80
+ assert.doesNotMatch(text, /source:/, "no source line for legacy memory");
81
+ });
82
+
83
+ test("renderXrayText: long quote is truncated to ~120 chars with ellipsis", () => {
84
+ const longQuote = "A".repeat(300);
85
+ const snap = buildXraySnapshot({
86
+ query: "q",
87
+ tierExplain: null,
88
+ results: [
89
+ {
90
+ memoryId: "fact-long",
91
+ path: "facts/long.md",
92
+ servedBy: "hybrid",
93
+ scoreDecomposition: { final: 0.5 },
94
+ admittedBy: [],
95
+ sourceSpan: {
96
+ quote: longQuote,
97
+ observedAt: "2026-06-01T00:00:00.000Z",
98
+ provenance: "verified",
99
+ },
100
+ },
101
+ ],
102
+ filters: [],
103
+ budget: { chars: 4096, used: 0 },
104
+ });
105
+ const text = renderXrayText(snap);
106
+ // The truncated quote should end with an ellipsis and be well under 300 chars
107
+ const sourceLine = text.split("\n").find((l) => l.includes("source:"));
108
+ assert.ok(sourceLine, "source line present");
109
+ assert.match(sourceLine!, /\u2026|…/, "truncated with ellipsis marker");
110
+ // The quote between the first and second double-quotes
111
+ const match = sourceLine!.match(/source: "(.*)" \(observed/);
112
+ assert.ok(match);
113
+ assert.ok(
114
+ match![1].length <= 125,
115
+ `truncated quote should be ~120 chars, got ${match![1].length}`,
116
+ );
117
+ });
118
+
119
+ // ─── X-ray markdown rendering ─────────────────────────────────────────────
120
+
121
+ test("renderXrayText: quote with newlines is collapsed to a single line", () => {
122
+ const snap = buildXraySnapshot({
123
+ query: "q",
124
+ tierExplain: null,
125
+ results: [
126
+ {
127
+ memoryId: "fact-newline",
128
+ path: "facts/multi.md",
129
+ servedBy: "hybrid",
130
+ scoreDecomposition: { final: 0.5 },
131
+ admittedBy: [],
132
+ sourceSpan: {
133
+ quote: "line one\nline two\r\nline three",
134
+ observedAt: "2026-06-01T00:00:00.000Z",
135
+ provenance: "verified",
136
+ },
137
+ },
138
+ ],
139
+ filters: [],
140
+ budget: { chars: 4096, used: 0 },
141
+ });
142
+ const text = renderXrayText(snap);
143
+ const sourceLine = text.split("\n").find((l) => l.includes("source:"));
144
+ assert.ok(sourceLine);
145
+ // Newlines must be collapsed to spaces — the source line must be a single line
146
+ assert.match(sourceLine!, /line one line two line three/);
147
+ assert.doesNotMatch(sourceLine!, /\n|\r/);
148
+ });
149
+
150
+ test("renderXrayMarkdown: sourceSpan renders as a Source line", () => {
151
+ const md = renderXrayMarkdown(snapshotWithSourceSpan());
152
+ assert.match(
153
+ md,
154
+ /\*\*Source:\*\* "We migrated the production database to pgBouncer" \(`2026-05-03T10:00:00\.000Z`\) — verified/,
155
+ "markdown renderer must surface the source span",
156
+ );
157
+ });
158
+
159
+ test("renderXrayMarkdown: absent sourceSpan produces no Source line", () => {
160
+ const snap = buildXraySnapshot({
161
+ query: "q",
162
+ tierExplain: null,
163
+ results: [
164
+ {
165
+ memoryId: "fact-legacy-2",
166
+ path: "facts/legacy2.md",
167
+ servedBy: "hybrid",
168
+ scoreDecomposition: { final: 0.5 },
169
+ admittedBy: [],
170
+ },
171
+ ],
172
+ filters: [],
173
+ budget: { chars: 4096, used: 0 },
174
+ });
175
+ const md = renderXrayMarkdown(snap);
176
+ assert.doesNotMatch(md, /\*\*Source:\*\*/, "no Source line for legacy memory");
177
+ });
178
+
179
+ // ─── cloneResult preserves sourceSpan ─────────────────────────────────────
180
+
181
+ test("buildXraySnapshot deep-copies sourceSpan (independent of caller object)", () => {
182
+ const original = resultWithSourceSpan();
183
+ const snap = buildXraySnapshot({
184
+ query: "q",
185
+ tierExplain: null,
186
+ results: [original],
187
+ filters: [],
188
+ budget: { chars: 4096, used: 0 },
189
+ });
190
+ // Mutate the original — the snapshot must be unaffected
191
+ original.sourceSpan!.quote = "MUTATED";
192
+ assert.equal(
193
+ snap.results[0]!.sourceSpan!.quote,
194
+ "We migrated the production database to pgBouncer",
195
+ "snapshot sourceSpan must be a deep copy",
196
+ );
197
+ });
198
+
199
+ // ─── `remnic doctor` provenance coverage (pure helper) ────────────────────
200
+ //
201
+ // `runOperatorDoctor` calls `summarizeProvenanceCoverage` (a pure function)
202
+ // to build the verified/unverified/none distribution. Testing the helper
203
+ // directly avoids coupling to the full orchestrator mock while still pinning
204
+ // the counting contract the doctor check relies on.
205
+
206
+ import { summarizeProvenanceCoverage } from "./operator-toolkit.js";
207
+
208
+ function makeMemoryFile(
209
+ id: string,
210
+ provenance: "verified" | "unverified" | "none" | undefined,
211
+ ): { frontmatter: { provenance?: string } } {
212
+ return {
213
+ frontmatter: {
214
+ ...(provenance ? { provenance } : {}),
215
+ },
216
+ };
217
+ }
218
+
219
+ test("summarizeProvenanceCoverage: mixed distribution — verified/unverified/none/legacy", () => {
220
+ const memories = [
221
+ makeMemoryFile("v1", "verified"),
222
+ makeMemoryFile("v2", "verified"),
223
+ makeMemoryFile("u1", "unverified"),
224
+ makeMemoryFile("n1", "none"),
225
+ makeMemoryFile("legacy", undefined), // legacy — no field → counts as none
226
+ ];
227
+ const result = summarizeProvenanceCoverage(memories);
228
+ assert.deepEqual(result.counts, { verified: 2, unverified: 1, none: 2 });
229
+ assert.equal(result.total, 5);
230
+ });
231
+
232
+ test("summarizeProvenanceCoverage: all legacy → all none", () => {
233
+ const memories = [
234
+ makeMemoryFile("n1", undefined),
235
+ makeMemoryFile("n2", undefined),
236
+ ];
237
+ const result = summarizeProvenanceCoverage(memories);
238
+ assert.deepEqual(result.counts, { verified: 0, unverified: 0, none: 2 });
239
+ assert.equal(result.total, 2);
240
+ });
241
+
242
+ test("summarizeProvenanceCoverage: empty → zero counts", () => {
243
+ const result = summarizeProvenanceCoverage([]);
244
+ assert.deepEqual(result.counts, { verified: 0, unverified: 0, none: 0 });
245
+ assert.equal(result.total, 0);
246
+ });
247
+
248
+ test("summarizeProvenanceCoverage: unknown tag string → counts as none (defensive)", () => {
249
+ const memories = [
250
+ { frontmatter: { provenance: "bogus" } },
251
+ ];
252
+ const result = summarizeProvenanceCoverage(memories);
253
+ assert.deepEqual(result.counts, { verified: 0, unverified: 0, none: 1 });
254
+ });
@@ -174,6 +174,12 @@ function renderResultTextLines(
174
174
  lines.push(` safety: ${result.provenance.safety}${reasonSuffix}`);
175
175
  }
176
176
  }
177
+ if (result.sourceSpan) {
178
+ const q = truncateForXray(result.sourceSpan.quote, 120);
179
+ lines.push(
180
+ ` source: "${q}" (observed ${result.sourceSpan.observedAt}) [${result.sourceSpan.provenance}]`,
181
+ );
182
+ }
177
183
  if (result.admittedBy.length > 0) {
178
184
  lines.push(` admitted-by: ${result.admittedBy.join(", ")}`);
179
185
  }
@@ -374,6 +380,12 @@ function renderResultMarkdownLines(
374
380
  );
375
381
  }
376
382
  }
383
+ if (result.sourceSpan) {
384
+ const q = truncateForXray(result.sourceSpan.quote, 120);
385
+ lines.push(
386
+ `- **Source:** "${mdEscape(q)}" (${mdInlineCode(result.sourceSpan.observedAt)}) \u2014 ${result.sourceSpan.provenance}`,
387
+ );
388
+ }
377
389
  if (result.admittedBy.length > 0) {
378
390
  lines.push(
379
391
  `- **Admitted by:** ${result.admittedBy.map(mdInlineCode).join(", ")}`,
@@ -472,6 +484,22 @@ function mdEscape(value: string): string {
472
484
  return value.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
473
485
  }
474
486
 
487
+ /**
488
+ * Truncate a quote for X-ray display (issue #1575 PR 3). Collapses internal
489
+ * whitespace runs (including newlines/carriage returns) to single spaces so
490
+ * a multi-line source excerpt cannot split what is meant to be a single X-ray
491
+ * row, then caps at `maxChars` on a word boundary with an ellipsis marker.
492
+ * The JSON \`sourceSpan.quote\` stays verbatim — this is display-only.
493
+ */
494
+ function truncateForXray(text: string, maxChars: number): string {
495
+ const collapsed = text.replace(/\s+/g, " ").trim();
496
+ if (collapsed.length <= maxChars) return collapsed;
497
+ const slice = collapsed.slice(0, maxChars);
498
+ const lastSpace = slice.lastIndexOf(" ");
499
+ const cut = lastSpace > Math.floor(maxChars * 0.5) ? slice.slice(0, lastSpace) : slice;
500
+ return `${cut}\u2026`;
501
+ }
502
+
475
503
  function mdInlineCode(value: string): string {
476
504
  if (value.length === 0) return "``";
477
505
  // Use exactly enough backticks to unambiguously wrap content that
@@ -171,6 +171,20 @@ export interface RecallXrayResult {
171
171
  * capture when the memory frontmatter was already loaded by retrieval.
172
172
  */
173
173
  provenance?: RetrievedMemoryProvenance;
174
+ /**
175
+ * Claim-level provenance span (issue #1575 PR 3). The first source excerpt
176
+ * backing this memory, surfaced so X-ray consumers can see the literal
177
+ * utterance each fact derives from alongside retrieval provenance. Absent
178
+ * when the memory carries no source spans (legacy or `provenance: "none"`).
179
+ */
180
+ sourceSpan?: {
181
+ /** Verbatim quote excerpt (capped at write time by `provenance.maxQuoteChars`). */
182
+ quote: string;
183
+ /** ISO timestamp of the source turn. */
184
+ observedAt: string;
185
+ /** Coarse strength tag from the frontmatter. */
186
+ provenance: "verified" | "unverified" | "none";
187
+ };
174
188
  }
175
189
 
176
190
  /**
@@ -522,6 +536,24 @@ function cloneResult(result: RecallXrayResult): RecallXrayResult {
522
536
  if (provenance !== undefined) {
523
537
  out.provenance = provenance;
524
538
  }
539
+ // Claim-level provenance span (#1575 PR 3). Deep-copy the three scalar
540
+ // fields so the snapshot returned by getLastXraySnapshot is independent
541
+ // of the caller's object (matches the structuredClone contract).
542
+ const ss = result.sourceSpan;
543
+ if (
544
+ ss &&
545
+ typeof ss.quote === "string" &&
546
+ typeof ss.observedAt === "string" &&
547
+ (ss.provenance === "verified" ||
548
+ ss.provenance === "unverified" ||
549
+ ss.provenance === "none")
550
+ ) {
551
+ out.sourceSpan = {
552
+ quote: ss.quote,
553
+ observedAt: ss.observedAt,
554
+ provenance: ss.provenance,
555
+ };
556
+ }
525
557
  return out;
526
558
  }
527
559