@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.
- package/dist/access-boundary.d.ts +5 -5
- package/dist/access-boundary.js +4 -4
- package/dist/access-cli.js +7 -7
- package/dist/access-http.d.ts +5 -5
- package/dist/access-http.js +8 -8
- package/dist/access-mcp.d.ts +5 -5
- package/dist/access-mcp.js +7 -7
- package/dist/access-operations-batch.js +5 -5
- package/dist/access-operations.d.ts +5 -5
- package/dist/access-operations.js +6 -6
- package/dist/{access-service-CoIA0NrG.d.ts → access-service-Dx2rSJjH.d.ts} +3 -3
- package/dist/access-service.d.ts +5 -5
- package/dist/access-service.js +3 -3
- package/dist/access-surface-catalog.d.ts +5 -5
- package/dist/action-confidence.d.ts +1 -1
- package/dist/active-memory-bridge.d.ts +1 -1
- package/dist/active-recall.d.ts +1 -1
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +4 -4
- package/dist/briefing.d.ts +1 -1
- package/dist/buffer-surprise-report.d.ts +1 -1
- package/dist/buffer.d.ts +1 -1
- package/dist/calibration.d.ts +1 -1
- package/dist/capabilities.d.ts +1 -1
- package/dist/{catalog-DQCZrBjw.d.ts → catalog-CadPvH29.d.ts} +1 -1
- package/dist/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/{chunk-OK7FUX6R.js → chunk-4P5UAL3C.js} +46 -1
- package/dist/chunk-4P5UAL3C.js.map +1 -0
- package/dist/{chunk-NK3SPJLM.js → chunk-5KQCOIPW.js} +7 -7
- package/dist/{chunk-Y6PIFKXX.js → chunk-5QUS24F7.js} +22 -2
- package/dist/chunk-5QUS24F7.js.map +1 -0
- package/dist/{chunk-VWB3HDY6.js → chunk-A2R5NV6O.js} +4 -4
- package/dist/{chunk-XD33EX2F.js → chunk-AMXOCZHE.js} +3 -3
- package/dist/{chunk-2T4TDXPC.js → chunk-GKPIY7EQ.js} +13 -3
- package/dist/chunk-GKPIY7EQ.js.map +1 -0
- package/dist/{chunk-KOEKDZ6A.js → chunk-I3BT2IDW.js} +3 -3
- package/dist/{chunk-AZVHBFI3.js → chunk-OWOKOFXK.js} +9 -1
- package/dist/chunk-OWOKOFXK.js.map +1 -0
- package/dist/{chunk-EVX52NCY.js → chunk-YVWZW37D.js} +3 -3
- package/dist/{chunk-P6PRSI3W.js → chunk-Z3B2YW56.js} +3 -3
- package/dist/{chunk-JQ7XVM4V.js → chunk-ZBG4K36I.js} +2 -2
- package/dist/{chunk-PKE7EJMX.js → chunk-ZVDRL6SK.js} +2 -2
- package/dist/{cli-qex-L3GT.d.ts → cli-DTWRoJEI.d.ts} +3 -3
- package/dist/cli.d.ts +6 -6
- package/dist/cli.js +11 -11
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/preference-consolidator.d.ts +1 -1
- package/dist/compression-optimizer.d.ts +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/consolidation-provenance-check.d.ts +1 -1
- package/dist/consolidation-undo.d.ts +1 -1
- package/dist/contradiction/index.d.ts +1 -1
- package/dist/conversation-index/backend.d.ts +1 -1
- package/dist/conversation-index/chunker.d.ts +1 -1
- package/dist/conversation-index/faiss-adapter.d.ts +1 -1
- package/dist/conversation-index/indexer.d.ts +1 -1
- package/dist/conversation-index/search.d.ts +1 -1
- package/dist/day-summary.d.ts +1 -1
- package/dist/delinearize.d.ts +1 -1
- package/dist/direct-answer-wiring.d.ts +1 -1
- package/dist/direct-answer.d.ts +1 -1
- package/dist/embedding-fallback.d.ts +1 -1
- package/dist/enrichment/index.d.ts +1 -1
- package/dist/entity-retrieval.d.ts +1 -1
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +4 -4
- package/dist/extraction-faithfulness.d.ts +1 -1
- package/dist/extraction-judge-telemetry.d.ts +1 -1
- package/dist/extraction-judge-training.d.ts +1 -1
- package/dist/extraction-judge.d.ts +1 -1
- package/dist/extraction.d.ts +1 -1
- package/dist/fallback-llm.d.ts +1 -1
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.js +12 -12
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/tools.d.ts +1 -1
- package/dist/lifecycle.d.ts +1 -1
- package/dist/live-connectors-runner.d.ts +1 -1
- package/dist/local-llm.d.ts +1 -1
- package/dist/maintenance/memory-governance.d.ts +1 -1
- package/dist/mcp-memory-inspector-app.d.ts +5 -5
- package/dist/memory-action-policy.d.ts +1 -1
- package/dist/memory-cache.d.ts +1 -1
- package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
- package/dist/memory-projection-store.d.ts +1 -1
- package/dist/memory-provenance.d.ts +1 -1
- package/dist/memory-worth-outcomes.d.ts +1 -1
- package/dist/models-json.d.ts +1 -1
- package/dist/namespaces/migrate.d.ts +2 -2
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/storage.d.ts +2 -2
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +20 -2
- package/dist/operator-toolkit.js +3 -1
- package/dist/orchestration/maintenance.d.ts +2 -2
- package/dist/{orchestrator-DsVKLEBk.d.ts → orchestrator-B1iVjEyc.d.ts} +3 -3
- package/dist/orchestrator.d.ts +4 -4
- package/dist/orchestrator.js +2 -2
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +1 -1
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +1 -1
- package/dist/recall-disclosure-escalation.d.ts +1 -1
- package/dist/recall-explain-renderer.d.ts +1 -1
- package/dist/recall-explain-renderer.js +2 -2
- package/dist/recall-planner-llm.d.ts +1 -1
- package/dist/recall-state.d.ts +1 -1
- package/dist/recall-tag-filter.d.ts +1 -1
- package/dist/recall-xray-cli.d.ts +1 -1
- package/dist/recall-xray-cli.js +3 -3
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +2 -2
- package/dist/recall-xray.d.ts +15 -1
- package/dist/recall-xray.js +1 -1
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/retrieval-tiers.d.ts +1 -1
- package/dist/routing/engine.d.ts +1 -1
- package/dist/routing/store.d.ts +1 -1
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/index.d.ts +1 -1
- package/dist/search/lancedb-backend.d.ts +1 -1
- package/dist/search/meilisearch-backend.d.ts +1 -1
- package/dist/search/noop-backend.d.ts +1 -1
- package/dist/search/orama-backend.d.ts +1 -1
- package/dist/search/port.d.ts +1 -1
- package/dist/search/remote-backend.d.ts +1 -1
- package/dist/{semantic-consolidation-_hVxkTuF.d.ts → semantic-consolidation-CY8uVRNJ.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/session-observer-bands.d.ts +1 -1
- package/dist/session-observer-state.d.ts +1 -1
- package/dist/shared-context/manager.d.ts +1 -1
- package/dist/signal.d.ts +1 -1
- package/dist/storage.d.ts +1 -1
- package/dist/summarizer.d.ts +1 -1
- package/dist/summary-snapshot.d.ts +1 -1
- package/dist/temporal-supersession.d.ts +1 -1
- package/dist/temporal-validity.d.ts +1 -1
- package/dist/threading.d.ts +1 -1
- package/dist/tier-migration.d.ts +1 -1
- package/dist/tier-routing.d.ts +1 -1
- package/dist/topics.d.ts +1 -1
- package/dist/transcript.d.ts +1 -1
- package/dist/{types-DUK4vVnN.d.ts → types-Dr0Cw3L9.d.ts} +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/package.json +2 -2
- package/src/operator-toolkit.ts +80 -0
- package/src/orchestrator.ts +13 -0
- package/src/provenance-surfaces.test.ts +254 -0
- package/src/recall-xray-renderer.ts +28 -0
- package/src/recall-xray.ts +32 -0
- package/dist/chunk-2T4TDXPC.js.map +0 -1
- package/dist/chunk-AZVHBFI3.js.map +0 -1
- package/dist/chunk-OK7FUX6R.js.map +0 -1
- package/dist/chunk-Y6PIFKXX.js.map +0 -1
- /package/dist/{chunk-NK3SPJLM.js.map → chunk-5KQCOIPW.js.map} +0 -0
- /package/dist/{chunk-VWB3HDY6.js.map → chunk-A2R5NV6O.js.map} +0 -0
- /package/dist/{chunk-XD33EX2F.js.map → chunk-AMXOCZHE.js.map} +0 -0
- /package/dist/{chunk-KOEKDZ6A.js.map → chunk-I3BT2IDW.js.map} +0 -0
- /package/dist/{chunk-EVX52NCY.js.map → chunk-YVWZW37D.js.map} +0 -0
- /package/dist/{chunk-P6PRSI3W.js.map → chunk-Z3B2YW56.js.map} +0 -0
- /package/dist/{chunk-JQ7XVM4V.js.map → chunk-ZBG4K36I.js.map} +0 -0
- /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
|
package/src/recall-xray.ts
CHANGED
|
@@ -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
|
|