@remnic/core 9.3.682 → 9.3.684
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 +4 -4
- package/dist/access-boundary.js +12 -11
- package/dist/access-cli.js +29 -29
- package/dist/access-http.d.ts +4 -4
- package/dist/access-http.js +17 -16
- package/dist/access-mcp.d.ts +4 -4
- package/dist/access-mcp.js +14 -13
- package/dist/access-operations.d.ts +4 -4
- package/dist/access-operations.js +13 -12
- package/dist/{access-service-DvA6jyHL.d.ts → access-service-D-siI-xJ.d.ts} +2 -2
- package/dist/access-service.d.ts +4 -4
- package/dist/access-service.js +11 -10
- package/dist/access-surface-catalog.d.ts +4 -4
- 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/active-recall.js +2 -2
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/behavior-learner.d.ts +1 -1
- package/dist/behavior-signals.d.ts +1 -1
- package/dist/bootstrap.d.ts +3 -3
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +4 -3
- 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/causal-behavior.d.ts +1 -1
- package/dist/causal-consolidation.d.ts +1 -1
- package/dist/causal-consolidation.js +5 -4
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/{chunk-O2WELT5C.js → chunk-2IBGHRIO.js} +2 -2
- package/dist/{chunk-I3HSKQT7.js → chunk-2L3KLWOV.js} +27 -27
- package/dist/{chunk-V254FAT5.js → chunk-3EVIMVQU.js} +2 -2
- package/dist/{chunk-H4BDNIKQ.js → chunk-3MY4W5V4.js} +8 -8
- package/dist/{chunk-3IND7N4X.js → chunk-3Z7NPD5T.js} +2 -2
- package/dist/{chunk-TY5NT3T3.js → chunk-53FDU4CE.js} +13 -8
- package/dist/chunk-53FDU4CE.js.map +1 -0
- package/dist/{chunk-QVWM4C24.js → chunk-5N5DXYDW.js} +6 -6
- package/dist/{chunk-FDSOMA6M.js → chunk-5OE4PYY5.js} +4 -4
- package/dist/{chunk-IUZWBCJX.js → chunk-6QM24CP7.js} +9 -6
- package/dist/chunk-6QM24CP7.js.map +1 -0
- package/dist/{chunk-H6PMGMNP.js → chunk-6VMIHVGO.js} +2 -2
- package/dist/{chunk-APJQ6UEA.js → chunk-AGNBY3VG.js} +4 -4
- package/dist/{chunk-GSTYVG5L.js → chunk-BFVPIKDY.js} +3 -3
- package/dist/{chunk-EG4TCVMU.js → chunk-DQY7NJ5L.js} +2 -2
- package/dist/{chunk-ARLRTZZZ.js → chunk-FMEKEF47.js} +2 -2
- package/dist/{chunk-NHFXF4ZO.js → chunk-FYEVFGJD.js} +2 -2
- package/dist/{chunk-OHX52AOS.js → chunk-GTDH3IUH.js} +2 -2
- package/dist/{chunk-ODWI5XU2.js → chunk-GWKCEM3S.js} +2 -2
- package/dist/{chunk-UAODC6GJ.js → chunk-J2FBJ63F.js} +3 -3
- package/dist/{chunk-KV6CX4ON.js → chunk-K6ZN34WC.js} +2 -2
- package/dist/{chunk-6VP3YUCS.js → chunk-LLONI6PY.js} +2 -2
- package/dist/{chunk-GNAMDNGT.js → chunk-LXH3DIF2.js} +4 -4
- package/dist/{chunk-TOQEZ63C.js → chunk-M3FWYURP.js} +5 -5
- package/dist/{chunk-B2B2IHUH.js → chunk-M6BVYHBU.js} +2 -2
- package/dist/{chunk-FMSDA2D3.js → chunk-NGFEWFNK.js} +1 -1
- package/dist/chunk-NGFEWFNK.js.map +1 -0
- package/dist/{chunk-QUA2JPH2.js → chunk-NHQGDVJF.js} +3 -3
- package/dist/{chunk-KACIOX42.js → chunk-OMLIFZ4I.js} +2 -2
- package/dist/{chunk-M4I3TREG.js → chunk-OXNOINIP.js} +21 -21
- package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
- package/dist/{chunk-UJDV2NLT.js → chunk-ROHLEUTH.js} +4 -4
- package/dist/{chunk-WEPMT6SC.js → chunk-V25ZAOSB.js} +5 -5
- package/dist/{chunk-L5MUA6Q7.js → chunk-WI7JKV2T.js} +2 -2
- package/dist/{chunk-NQMBSSWW.js → chunk-WRE3JPAW.js} +2 -2
- package/dist/{chunk-I75DF4FZ.js → chunk-XEA4Z7JU.js} +2 -2
- package/dist/{chunk-G7Z3C2X6.js → chunk-XWEXT4XU.js} +2 -2
- package/dist/chunk-ZPQVJEVQ.js +184 -0
- package/dist/chunk-ZPQVJEVQ.js.map +1 -0
- package/dist/{cli-feUe-x3I.d.ts → cli-ooj6JQBS.d.ts} +3 -3
- package/dist/cli.d.ts +5 -5
- package/dist/cli.js +32 -32
- package/dist/compounding/engine.d.ts +1 -1
- package/dist/compounding/engine.js +4 -3
- 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/config.js +2 -2
- package/dist/connectors/codex-materialize-runner.d.ts +1 -1
- package/dist/connectors/codex-materialize-runner.js +4 -3
- package/dist/connectors/codex-materialize.d.ts +1 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +7 -7
- 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-retrieval.js +5 -3
- package/dist/entity-schema.d.ts +1 -1
- package/dist/explicit-capture.d.ts +3 -3
- 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/{first-start-migration-FF7YFGRP.js → first-start-migration-PG5HBC3K.js} +4 -4
- package/dist/identity-continuity.d.ts +1 -1
- package/dist/importance.d.ts +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +79 -79
- package/dist/intent.d.ts +1 -1
- package/dist/lcm/engine.d.ts +1 -1
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.d.ts +1 -1
- package/dist/lcm/index.js +5 -5
- 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/maintenance/memory-governance.js +5 -3
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -3
- package/dist/maintenance/rebuild-memory-projection.js +6 -4
- package/dist/mcp-memory-inspector-app.d.ts +4 -4
- 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 +1 -1
- package/dist/namespaces/migrate.js +8 -7
- package/dist/namespaces/principal.d.ts +1 -1
- package/dist/namespaces/search.d.ts +1 -1
- package/dist/namespaces/search.js +3 -3
- package/dist/namespaces/storage.d.ts +1 -1
- package/dist/namespaces/storage.js +5 -3
- package/dist/native-knowledge.d.ts +1 -1
- package/dist/operator-toolkit.d.ts +1 -1
- package/dist/operator-toolkit.js +11 -11
- package/dist/{orchestrator-7zPqGupX.d.ts → orchestrator-DIDDvwDw.d.ts} +2 -2
- package/dist/orchestrator.d.ts +3 -3
- package/dist/orchestrator.js +23 -22
- package/dist/patterns-cli.d.ts +1 -1
- package/dist/policy-runtime.d.ts +1 -1
- package/dist/provenance.d.ts +94 -0
- package/dist/provenance.js +17 -0
- package/dist/provenance.js.map +1 -0
- 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 +3 -3
- 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 +4 -4
- package/dist/recall-xray-renderer.d.ts +1 -1
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.d.ts +1 -1
- package/dist/recall-xray.js +2 -2
- package/dist/resolve-auth-token.d.ts +1 -1
- package/dist/resume-bundles.js +3 -3
- 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/schemas.d.ts +22 -22
- package/dist/search/embed-helper.d.ts +1 -1
- package/dist/search/factory.d.ts +1 -1
- package/dist/search/factory.js +2 -2
- package/dist/search/index.d.ts +1 -1
- package/dist/search/index.js +4 -4
- 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-BX9Z9_aK.d.ts → semantic-consolidation-CWch5uM7.d.ts} +1 -1
- package/dist/semantic-consolidation.d.ts +2 -2
- package/dist/semantic-consolidation.js +5 -4
- package/dist/semantic-rule-promotion.js +5 -3
- package/dist/semantic-rule-verifier.d.ts +1 -1
- package/dist/semantic-rule-verifier.js +5 -3
- 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/storage.js +4 -2
- 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/transfer/import-sqlite.js +2 -2
- package/dist/transfer/types.d.ts +12 -12
- package/dist/{types-D3pm4NhH.d.ts → types-Dm5xxVrr.d.ts} +61 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/utility-runtime.d.ts +1 -1
- package/dist/verified-recall.js +5 -3
- package/package.json +2 -2
- package/src/config.test.ts +112 -0
- package/src/config.ts +5 -5
- package/src/provenance-frontmatter.test.ts +596 -0
- package/src/provenance.ts +305 -0
- package/src/storage.ts +5 -5
- package/src/types.ts +76 -0
- package/dist/chunk-FMSDA2D3.js.map +0 -1
- package/dist/chunk-IUZWBCJX.js.map +0 -1
- package/dist/chunk-PHK3HARR.js +0 -32
- package/dist/chunk-PHK3HARR.js.map +0 -1
- package/dist/chunk-TY5NT3T3.js.map +0 -1
- /package/dist/{chunk-O2WELT5C.js.map → chunk-2IBGHRIO.js.map} +0 -0
- /package/dist/{chunk-I3HSKQT7.js.map → chunk-2L3KLWOV.js.map} +0 -0
- /package/dist/{chunk-V254FAT5.js.map → chunk-3EVIMVQU.js.map} +0 -0
- /package/dist/{chunk-H4BDNIKQ.js.map → chunk-3MY4W5V4.js.map} +0 -0
- /package/dist/{chunk-3IND7N4X.js.map → chunk-3Z7NPD5T.js.map} +0 -0
- /package/dist/{chunk-QVWM4C24.js.map → chunk-5N5DXYDW.js.map} +0 -0
- /package/dist/{chunk-FDSOMA6M.js.map → chunk-5OE4PYY5.js.map} +0 -0
- /package/dist/{chunk-H6PMGMNP.js.map → chunk-6VMIHVGO.js.map} +0 -0
- /package/dist/{chunk-APJQ6UEA.js.map → chunk-AGNBY3VG.js.map} +0 -0
- /package/dist/{chunk-GSTYVG5L.js.map → chunk-BFVPIKDY.js.map} +0 -0
- /package/dist/{chunk-EG4TCVMU.js.map → chunk-DQY7NJ5L.js.map} +0 -0
- /package/dist/{chunk-ARLRTZZZ.js.map → chunk-FMEKEF47.js.map} +0 -0
- /package/dist/{chunk-NHFXF4ZO.js.map → chunk-FYEVFGJD.js.map} +0 -0
- /package/dist/{chunk-OHX52AOS.js.map → chunk-GTDH3IUH.js.map} +0 -0
- /package/dist/{chunk-ODWI5XU2.js.map → chunk-GWKCEM3S.js.map} +0 -0
- /package/dist/{chunk-UAODC6GJ.js.map → chunk-J2FBJ63F.js.map} +0 -0
- /package/dist/{chunk-KV6CX4ON.js.map → chunk-K6ZN34WC.js.map} +0 -0
- /package/dist/{chunk-6VP3YUCS.js.map → chunk-LLONI6PY.js.map} +0 -0
- /package/dist/{chunk-GNAMDNGT.js.map → chunk-LXH3DIF2.js.map} +0 -0
- /package/dist/{chunk-TOQEZ63C.js.map → chunk-M3FWYURP.js.map} +0 -0
- /package/dist/{chunk-B2B2IHUH.js.map → chunk-M6BVYHBU.js.map} +0 -0
- /package/dist/{chunk-QUA2JPH2.js.map → chunk-NHQGDVJF.js.map} +0 -0
- /package/dist/{chunk-KACIOX42.js.map → chunk-OMLIFZ4I.js.map} +0 -0
- /package/dist/{chunk-M4I3TREG.js.map → chunk-OXNOINIP.js.map} +0 -0
- /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
- /package/dist/{chunk-UJDV2NLT.js.map → chunk-ROHLEUTH.js.map} +0 -0
- /package/dist/{chunk-WEPMT6SC.js.map → chunk-V25ZAOSB.js.map} +0 -0
- /package/dist/{chunk-L5MUA6Q7.js.map → chunk-WI7JKV2T.js.map} +0 -0
- /package/dist/{chunk-NQMBSSWW.js.map → chunk-WRE3JPAW.js.map} +0 -0
- /package/dist/{chunk-I75DF4FZ.js.map → chunk-XEA4Z7JU.js.map} +0 -0
- /package/dist/{chunk-G7Z3C2X6.js.map → chunk-XWEXT4XU.js.map} +0 -0
- /package/dist/{first-start-migration-FF7YFGRP.js.map → first-start-migration-PG5HBC3K.js.map} +0 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { mkdtemp, mkdir, rm, writeFile, readFile } from "node:fs/promises";
|
|
6
|
+
|
|
7
|
+
import { StorageManager } from "./storage.js";
|
|
8
|
+
import type { ProvenanceSource } from "./types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Issue #1575 PR 1 — Claim-level provenance spans: frontmatter schema +
|
|
12
|
+
* storage round-trip.
|
|
13
|
+
*
|
|
14
|
+
* These tests pin the on-disk contract for the `sources` and `provenance`
|
|
15
|
+
* frontmatter fields. They live in the core package (not the root tests/
|
|
16
|
+
* directory) so they co-locate with storage.ts, where the parser/serializer
|
|
17
|
+
* they cover resides — mirroring the `memory-worth-frontmatter.test.ts`
|
|
18
|
+
* precedent (same package, same reason).
|
|
19
|
+
*
|
|
20
|
+
* Scope per PR 1:
|
|
21
|
+
* - Round-trip: explicit sources + provenance tag survive write → read
|
|
22
|
+
* intact, including optional offsets and a multi-source fact.
|
|
23
|
+
* - Legacy memories without the fields read cleanly (no crash) and return
|
|
24
|
+
* `undefined` (matching the accessCount / mw_* absent-field pattern).
|
|
25
|
+
* - Corrupt `sources` (string instead of array, entry missing `quote`)
|
|
26
|
+
* drop on read with the same "drop corrupt rather than poison" contract
|
|
27
|
+
* as `parseMemoryWorthCounterField`.
|
|
28
|
+
* - Serialized keys land in canonical order regardless of the in-memory
|
|
29
|
+
* object's key insertion order (rule 38 — deterministic output).
|
|
30
|
+
*
|
|
31
|
+
* Out of scope (later PRs in issue #1575):
|
|
32
|
+
* - Extraction prompt + per-fact `quote` output field (PR 2)
|
|
33
|
+
* - Post-parse validator that locates the quote in turn text (PR 2)
|
|
34
|
+
* - memory_get / x-ray / `remnic doctor` read surfaces (PR 3)
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build a fact file on disk with a bare-bones frontmatter plus arbitrary
|
|
39
|
+
* extra lines. Used to synthesize legacy and instrumented memories without
|
|
40
|
+
* going through `writeMemory`, which doesn't expose provenance options
|
|
41
|
+
* (by design — those are set by the extraction validator in PR 2, not at
|
|
42
|
+
* creation time).
|
|
43
|
+
*/
|
|
44
|
+
async function writeFactFile(
|
|
45
|
+
storage: StorageManager,
|
|
46
|
+
body: string,
|
|
47
|
+
extraFrontmatterLines: string[] = [],
|
|
48
|
+
): Promise<string> {
|
|
49
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
50
|
+
const id = `fact-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
51
|
+
const lines = [
|
|
52
|
+
"---",
|
|
53
|
+
`id: ${id}`,
|
|
54
|
+
"category: fact",
|
|
55
|
+
`created: ${new Date().toISOString()}`,
|
|
56
|
+
`updated: ${new Date().toISOString()}`,
|
|
57
|
+
"source: extraction",
|
|
58
|
+
"confidence: 0.8",
|
|
59
|
+
"confidenceTier: high",
|
|
60
|
+
"tags: []",
|
|
61
|
+
...extraFrontmatterLines,
|
|
62
|
+
"---",
|
|
63
|
+
];
|
|
64
|
+
const factsDir = path.join((storage as unknown as { baseDir: string }).baseDir, "facts", today);
|
|
65
|
+
await mkdir(factsDir, { recursive: true });
|
|
66
|
+
await writeFile(path.join(factsDir, `${id}.md`), `${lines.join("\n")}\n\n${body}\n`, "utf-8");
|
|
67
|
+
return id;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Resolve the on-disk path of a fact written by `writeFactFile`. */
|
|
71
|
+
function factFilePath(storage: StorageManager, id: string): string {
|
|
72
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
73
|
+
return path.join((storage as unknown as { baseDir: string }).baseDir, "facts", today, `${id}.md`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const TWO_SOURCES: ProvenanceSource[] = [
|
|
77
|
+
{
|
|
78
|
+
sessionKey: "project/acme/2026-05-03T10:00:00Z",
|
|
79
|
+
turnId: "turn-42",
|
|
80
|
+
observedAt: "2026-05-03T10:01:30Z",
|
|
81
|
+
quote: "we migrated the production database to pgBouncer",
|
|
82
|
+
charStart: 12,
|
|
83
|
+
charEnd: 60,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
sessionKey: "project/acme/2026-05-10T14:00:00Z",
|
|
87
|
+
observedAt: "2026-05-10T14:02:00Z",
|
|
88
|
+
quote: "the connection pool now caps at 100",
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
test("round-trip: two sources + provenance tag survive write → readAllMemories", async () => {
|
|
93
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-roundtrip-"));
|
|
94
|
+
try {
|
|
95
|
+
const storage = new StorageManager(dir);
|
|
96
|
+
await storage.ensureDirectories();
|
|
97
|
+
const id = await writeFactFile(storage, "Production DB uses pgBouncer with a 100-conn pool.");
|
|
98
|
+
|
|
99
|
+
await storage.updateMemoryFrontmatter(id, { sources: TWO_SOURCES, provenance: "verified" });
|
|
100
|
+
|
|
101
|
+
const memories = await storage.readAllMemories();
|
|
102
|
+
const written = memories.find((m) => m.frontmatter.id === id);
|
|
103
|
+
assert.ok(written, "fact must be discoverable after write");
|
|
104
|
+
assert.equal(written!.frontmatter.provenance, "verified");
|
|
105
|
+
const sources = written!.frontmatter.sources;
|
|
106
|
+
assert.ok(sources, "sources must round-trip");
|
|
107
|
+
assert.equal(sources!.length, 2, "both sources survive");
|
|
108
|
+
// First source retains every field, including optional offsets + turnId.
|
|
109
|
+
assert.deepEqual(sources![0], TWO_SOURCES[0]);
|
|
110
|
+
// Second source (no optional fields) round-trips without inventing any.
|
|
111
|
+
assert.deepEqual(sources![1], TWO_SOURCES[1]);
|
|
112
|
+
} finally {
|
|
113
|
+
await rm(dir, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("round-trip: each provenance enum value survives write → read", async () => {
|
|
118
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-enum-"));
|
|
119
|
+
try {
|
|
120
|
+
const storage = new StorageManager(dir);
|
|
121
|
+
await storage.ensureDirectories();
|
|
122
|
+
|
|
123
|
+
for (const tag of ["verified", "unverified", "none"] as const) {
|
|
124
|
+
const id = await writeFactFile(storage, `Fact tagged ${tag}.`);
|
|
125
|
+
await storage.updateMemoryFrontmatter(id, { provenance: tag, sources: [TWO_SOURCES[0]!] });
|
|
126
|
+
const memories = await storage.readAllMemories();
|
|
127
|
+
const written = memories.find((m) => m.frontmatter.id === id);
|
|
128
|
+
assert.ok(written);
|
|
129
|
+
assert.equal(written!.frontmatter.provenance, tag, `tag ${tag} must round-trip`);
|
|
130
|
+
}
|
|
131
|
+
} finally {
|
|
132
|
+
await rm(dir, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("legacy memory without provenance fields reads cleanly — both undefined", async () => {
|
|
137
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-legacy-"));
|
|
138
|
+
try {
|
|
139
|
+
const storage = new StorageManager(dir);
|
|
140
|
+
await storage.ensureDirectories();
|
|
141
|
+
const id = await writeFactFile(storage, "Legacy fact pre-dating provenance spans.");
|
|
142
|
+
|
|
143
|
+
const memories = await storage.readAllMemories();
|
|
144
|
+
const written = memories.find((m) => m.frontmatter.id === id);
|
|
145
|
+
assert.ok(written, "legacy fact must still be readable");
|
|
146
|
+
assert.equal(written!.frontmatter.sources, undefined);
|
|
147
|
+
assert.equal(written!.frontmatter.provenance, undefined);
|
|
148
|
+
} finally {
|
|
149
|
+
await rm(dir, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("corrupt sources (string instead of array) drops to undefined on read", async () => {
|
|
154
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-string-"));
|
|
155
|
+
try {
|
|
156
|
+
const storage = new StorageManager(dir);
|
|
157
|
+
await storage.ensureDirectories();
|
|
158
|
+
const id = await writeFactFile(storage, "Hand-edited fact with a string-typed sources.", [
|
|
159
|
+
'sources: "not an array"',
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
const memories = await storage.readAllMemories();
|
|
163
|
+
const written = memories.find((m) => m.frontmatter.id === id);
|
|
164
|
+
assert.ok(written);
|
|
165
|
+
// A non-array value must NOT round-trip — it fails safely to undefined
|
|
166
|
+
// so downstream surfaces aren't poisoned. See parseProvenanceSources.
|
|
167
|
+
assert.equal(written!.frontmatter.sources, undefined);
|
|
168
|
+
} finally {
|
|
169
|
+
await rm(dir, { recursive: true, force: true });
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("corrupt sources (not valid JSON) drops to undefined on read", async () => {
|
|
174
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-badjson-"));
|
|
175
|
+
try {
|
|
176
|
+
const storage = new StorageManager(dir);
|
|
177
|
+
await storage.ensureDirectories();
|
|
178
|
+
const id = await writeFactFile(storage, "Hand-edited fact with malformed JSON.", [
|
|
179
|
+
"sources: [{not closed",
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
const memories = await storage.readAllMemories();
|
|
183
|
+
const written = memories.find((m) => m.frontmatter.id === id);
|
|
184
|
+
assert.ok(written);
|
|
185
|
+
assert.equal(written!.frontmatter.sources, undefined);
|
|
186
|
+
} finally {
|
|
187
|
+
await rm(dir, { recursive: true, force: true });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("entry missing quote is dropped; sibling valid entry survives", async () => {
|
|
192
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-noquote-"));
|
|
193
|
+
try {
|
|
194
|
+
const storage = new StorageManager(dir);
|
|
195
|
+
await storage.ensureDirectories();
|
|
196
|
+
// One entry lacks the required `quote`; the other is well-formed. The
|
|
197
|
+
// parser must drop the corrupt entry and keep the valid one rather than
|
|
198
|
+
// poisoning the whole field or silently accepting bad data.
|
|
199
|
+
const rawSources = JSON.stringify([
|
|
200
|
+
{ sessionKey: "s", observedAt: "2026-05-03T10:00:00Z" }, // missing quote → dropped
|
|
201
|
+
TWO_SOURCES[0],
|
|
202
|
+
]);
|
|
203
|
+
const id = await writeFactFile(storage, "Mixed valid/corrupt sources.", [
|
|
204
|
+
`sources: ${rawSources}`,
|
|
205
|
+
]);
|
|
206
|
+
|
|
207
|
+
const memories = await storage.readAllMemories();
|
|
208
|
+
const written = memories.find((m) => m.frontmatter.id === id);
|
|
209
|
+
assert.ok(written);
|
|
210
|
+
const sources = written!.frontmatter.sources;
|
|
211
|
+
assert.ok(sources, "valid entry must survive sibling corruption");
|
|
212
|
+
assert.equal(sources!.length, 1, "corrupt entry is dropped, valid one kept");
|
|
213
|
+
assert.deepEqual(sources![0], TWO_SOURCES[0]);
|
|
214
|
+
} finally {
|
|
215
|
+
await rm(dir, { recursive: true, force: true });
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("entry missing required sessionKey/observedAt is dropped, field undefined when alone", async () => {
|
|
220
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-alonemissing-"));
|
|
221
|
+
try {
|
|
222
|
+
const storage = new StorageManager(dir);
|
|
223
|
+
await storage.ensureDirectories();
|
|
224
|
+
const rawSources = JSON.stringify([
|
|
225
|
+
{ observedAt: "2026-05-03T10:00:00Z", quote: "orphan quote with no session" },
|
|
226
|
+
]);
|
|
227
|
+
const id = await writeFactFile(storage, "Single corrupt entry.", [
|
|
228
|
+
`sources: ${rawSources}`,
|
|
229
|
+
]);
|
|
230
|
+
|
|
231
|
+
const memories = await storage.readAllMemories();
|
|
232
|
+
const written = memories.find((m) => m.frontmatter.id === id);
|
|
233
|
+
assert.ok(written);
|
|
234
|
+
// No valid entry survives → field is undefined (legacy-equivalent).
|
|
235
|
+
assert.equal(written!.frontmatter.sources, undefined);
|
|
236
|
+
} finally {
|
|
237
|
+
await rm(dir, { recursive: true, force: true });
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("unknown provenance tag value drops to undefined on read", async () => {
|
|
242
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-badtag-"));
|
|
243
|
+
try {
|
|
244
|
+
const storage = new StorageManager(dir);
|
|
245
|
+
await storage.ensureDirectories();
|
|
246
|
+
const id = await writeFactFile(storage, "Hand-edited fact with bogus provenance tag.", [
|
|
247
|
+
"provenance: definitely",
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
const memories = await storage.readAllMemories();
|
|
251
|
+
const written = memories.find((m) => m.frontmatter.id === id);
|
|
252
|
+
assert.ok(written);
|
|
253
|
+
assert.equal(written!.frontmatter.provenance, undefined);
|
|
254
|
+
} finally {
|
|
255
|
+
await rm(dir, { recursive: true, force: true });
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("serialized source keys land in canonical order regardless of insertion order", async () => {
|
|
260
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-keyorder-"));
|
|
261
|
+
try {
|
|
262
|
+
const storage = new StorageManager(dir);
|
|
263
|
+
await storage.ensureDirectories();
|
|
264
|
+
const id = await writeFactFile(storage, "Fact whose source object is keyed out of order.");
|
|
265
|
+
|
|
266
|
+
// Deliberately build the object with keys in a scrambled order. The
|
|
267
|
+
// serializer must emit them canonically: sessionKey, turnId, observedAt,
|
|
268
|
+
// quote, charStart, charEnd (rule 38 — deterministic output).
|
|
269
|
+
const scrambled: ProvenanceSource = {
|
|
270
|
+
charEnd: 9,
|
|
271
|
+
quote: "hello world",
|
|
272
|
+
charStart: 0,
|
|
273
|
+
observedAt: "2026-05-03T10:00:00Z",
|
|
274
|
+
turnId: "t1",
|
|
275
|
+
sessionKey: "s",
|
|
276
|
+
};
|
|
277
|
+
await storage.updateMemoryFrontmatter(id, { sources: [scrambled] });
|
|
278
|
+
|
|
279
|
+
const raw = await readFile(factFilePath(storage, id), "utf-8");
|
|
280
|
+
const sourcesLine = raw.split("\n").find((l) => l.startsWith("sources:"));
|
|
281
|
+
assert.ok(sourcesLine, "sources line must be present");
|
|
282
|
+
// The JSON object's keys must appear in canonical order.
|
|
283
|
+
const keyOrder = sourcesLine!
|
|
284
|
+
.slice("sources:".length)
|
|
285
|
+
.trim()
|
|
286
|
+
.replace(/^\[/, "")
|
|
287
|
+
.replace(/]$/, "");
|
|
288
|
+
const keys = [...keyOrder.matchAll(/"([A-Za-z]+)":/g)].map((m) => m[1]);
|
|
289
|
+
assert.deepEqual(keys, ["sessionKey", "turnId", "observedAt", "quote", "charStart", "charEnd"]);
|
|
290
|
+
} finally {
|
|
291
|
+
await rm(dir, { recursive: true, force: true });
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("characterization: legacy frontmatter re-serializes without provenance keys", async () => {
|
|
296
|
+
// When sources/provenance are absent, the serializer must emit NO
|
|
297
|
+
// provenance lines so a legacy memory round-trips byte-identical for
|
|
298
|
+
// these fields (rule 39 — byte-identical when the feature is off /
|
|
299
|
+
// unused). This is the guard against accidentally emitting empty
|
|
300
|
+
// `sources: []` or `provenance: none` placeholders.
|
|
301
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-char-"));
|
|
302
|
+
try {
|
|
303
|
+
const storage = new StorageManager(dir);
|
|
304
|
+
await storage.ensureDirectories();
|
|
305
|
+
const id = await writeFactFile(storage, "Untouched legacy fact.");
|
|
306
|
+
|
|
307
|
+
// Force a re-serialize by bumping an unrelated field.
|
|
308
|
+
await storage.updateMemoryFrontmatter(id, { accessCount: 1 });
|
|
309
|
+
|
|
310
|
+
const raw = await readFile(factFilePath(storage, id), "utf-8");
|
|
311
|
+
assert.ok(!/\nsources:/.test(raw), "no sources line when the field is absent");
|
|
312
|
+
assert.ok(!/\nprovenance:/.test(raw), "no provenance line when the field is absent");
|
|
313
|
+
} finally {
|
|
314
|
+
await rm(dir, { recursive: true, force: true });
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("empty sources array does not emit a sources line", async () => {
|
|
319
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-empty-"));
|
|
320
|
+
try {
|
|
321
|
+
const storage = new StorageManager(dir);
|
|
322
|
+
await storage.ensureDirectories();
|
|
323
|
+
const id = await writeFactFile(storage, "Fact with an empty sources array.");
|
|
324
|
+
|
|
325
|
+
await storage.updateMemoryFrontmatter(id, { sources: [] });
|
|
326
|
+
|
|
327
|
+
const raw = await readFile(factFilePath(storage, id), "utf-8");
|
|
328
|
+
assert.ok(!/\nsources:/.test(raw), "empty sources array must not emit a line");
|
|
329
|
+
const memories = await storage.readAllMemories();
|
|
330
|
+
const written = memories.find((m) => m.frontmatter.id === id);
|
|
331
|
+
assert.ok(written);
|
|
332
|
+
assert.equal(written!.frontmatter.sources, undefined);
|
|
333
|
+
} finally {
|
|
334
|
+
await rm(dir, { recursive: true, force: true });
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("write-path validation drops invalid in-memory sources (review thread 4)", async () => {
|
|
339
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-writeval-"));
|
|
340
|
+
try {
|
|
341
|
+
const storage = new StorageManager(dir);
|
|
342
|
+
await storage.ensureDirectories();
|
|
343
|
+
const id = await writeFactFile(storage, "Fact with mixed-validity sources.");
|
|
344
|
+
|
|
345
|
+
// One valid entry + one missing sessionKey + one missing observedAt.
|
|
346
|
+
await storage.updateMemoryFrontmatter(id, {
|
|
347
|
+
sources: [
|
|
348
|
+
{ sessionKey: "s/1", observedAt: "2026-01-01T00:00:00Z", quote: "valid" },
|
|
349
|
+
{ observedAt: "2026-01-01T00:00:00Z", quote: "no sessionKey" } as ProvenanceSource,
|
|
350
|
+
{ sessionKey: "s/3", quote: "no observedAt" } as ProvenanceSource,
|
|
351
|
+
],
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const memories = await storage.readAllMemories();
|
|
355
|
+
const written = memories.find((m) => m.frontmatter.id === id);
|
|
356
|
+
assert.ok(written, "fact must survive the write");
|
|
357
|
+
assert.ok(written!.frontmatter.sources, "sources must survive with valid entries");
|
|
358
|
+
assert.equal(written!.frontmatter.sources!.length, 1, "only the valid entry survives");
|
|
359
|
+
assert.equal(written!.frontmatter.sources![0]!.quote, "valid");
|
|
360
|
+
} finally {
|
|
361
|
+
await rm(dir, { recursive: true, force: true });
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test("write-path drops source with invalid observedAt timestamp (review round 4)", async () => {
|
|
366
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-ts-"));
|
|
367
|
+
try {
|
|
368
|
+
const storage = new StorageManager(dir);
|
|
369
|
+
await storage.ensureDirectories();
|
|
370
|
+
const id = await writeFactFile(storage, "Fact with bad timestamp.");
|
|
371
|
+
|
|
372
|
+
await storage.updateMemoryFrontmatter(id, {
|
|
373
|
+
sources: [
|
|
374
|
+
{ sessionKey: "s/1", observedAt: "not-a-date", quote: "bad ts" },
|
|
375
|
+
{ sessionKey: "s/2", observedAt: "2026-01-01T00:00:00Z", quote: "good" },
|
|
376
|
+
],
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const memories = await storage.readAllMemories();
|
|
380
|
+
const written = memories.find((m) => m.frontmatter.id === id);
|
|
381
|
+
assert.ok(written);
|
|
382
|
+
assert.equal(written!.frontmatter.sources!.length, 1, "only the valid-timestamp entry survives");
|
|
383
|
+
assert.equal(written!.frontmatter.sources![0]!.quote, "good");
|
|
384
|
+
} finally {
|
|
385
|
+
await rm(dir, { recursive: true, force: true });
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("write-path drops source with invalid span interval (review round 4)", async () => {
|
|
390
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-span-"));
|
|
391
|
+
try {
|
|
392
|
+
const storage = new StorageManager(dir);
|
|
393
|
+
await storage.ensureDirectories();
|
|
394
|
+
const id = await writeFactFile(storage, "Fact with bad span.");
|
|
395
|
+
|
|
396
|
+
await storage.updateMemoryFrontmatter(id, {
|
|
397
|
+
sources: [
|
|
398
|
+
{ sessionKey: "s/1", observedAt: "2026-01-01T00:00:00Z", quote: "bad span", charStart: 10, charEnd: 5 },
|
|
399
|
+
{ sessionKey: "s/2", observedAt: "2026-01-01T00:00:00Z", quote: "good", charStart: 0, charEnd: 10 },
|
|
400
|
+
],
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const memories = await storage.readAllMemories();
|
|
404
|
+
const written = memories.find((m) => m.frontmatter.id === id);
|
|
405
|
+
assert.ok(written);
|
|
406
|
+
assert.equal(written!.frontmatter.sources!.length, 1, "only the valid-span entry survives");
|
|
407
|
+
assert.equal(written!.frontmatter.sources![0]!.quote, "good");
|
|
408
|
+
} finally {
|
|
409
|
+
await rm(dir, { recursive: true, force: true });
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("write-path downgrades provenance tag to none when all sources dropped (review round 4)", async () => {
|
|
414
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-downgrade-"));
|
|
415
|
+
try {
|
|
416
|
+
const storage = new StorageManager(dir);
|
|
417
|
+
await storage.ensureDirectories();
|
|
418
|
+
const id = await writeFactFile(storage, "Fact with all-bad sources.");
|
|
419
|
+
|
|
420
|
+
await storage.updateMemoryFrontmatter(id, {
|
|
421
|
+
provenance: "verified",
|
|
422
|
+
sources: [
|
|
423
|
+
{ sessionKey: "s/1", observedAt: "not-a-date", quote: "bad" },
|
|
424
|
+
],
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const memories = await storage.readAllMemories();
|
|
428
|
+
const written = memories.find((m) => m.frontmatter.id === id);
|
|
429
|
+
assert.ok(written);
|
|
430
|
+
assert.equal(written!.frontmatter.sources, undefined, "all sources dropped");
|
|
431
|
+
assert.equal(written!.frontmatter.provenance, "none", "tag downgraded from verified to none");
|
|
432
|
+
} finally {
|
|
433
|
+
await rm(dir, { recursive: true, force: true });
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("write-path downgrades verified tag when sources field is absent (review round 5)", async () => {
|
|
438
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-absent-"));
|
|
439
|
+
try {
|
|
440
|
+
const storage = new StorageManager(dir);
|
|
441
|
+
await storage.ensureDirectories();
|
|
442
|
+
const id = await writeFactFile(storage, "Fact whose author set verified but supplied no sources.");
|
|
443
|
+
|
|
444
|
+
// verified tag with NO sources field at all — the earlier 3-branch logic
|
|
445
|
+
// kept the tag here (only all-invalid arrays downgraded).
|
|
446
|
+
await storage.updateMemoryFrontmatter(id, { provenance: "verified" });
|
|
447
|
+
|
|
448
|
+
const raw = await readFile(factFilePath(storage, id), "utf-8");
|
|
449
|
+
assert.ok(!/\nsources:/.test(raw), "no sources line written");
|
|
450
|
+
const memories = await storage.readAllMemories();
|
|
451
|
+
const written = memories.find((m) => m.frontmatter.id === id);
|
|
452
|
+
assert.ok(written);
|
|
453
|
+
assert.equal(written!.frontmatter.sources, undefined, "no sources present");
|
|
454
|
+
assert.equal(
|
|
455
|
+
written!.frontmatter.provenance,
|
|
456
|
+
"none",
|
|
457
|
+
"verified tag must downgrade to none without surviving sources",
|
|
458
|
+
);
|
|
459
|
+
} finally {
|
|
460
|
+
await rm(dir, { recursive: true, force: true });
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("write-path downgrades verified tag when sources array is empty (review round 5)", async () => {
|
|
465
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-emptytag-"));
|
|
466
|
+
try {
|
|
467
|
+
const storage = new StorageManager(dir);
|
|
468
|
+
await storage.ensureDirectories();
|
|
469
|
+
const id = await writeFactFile(storage, "Fact with verified tag and an empty sources array.");
|
|
470
|
+
|
|
471
|
+
// verified tag with sources: [] — fm.sources.length > 0 is false so the
|
|
472
|
+
// earlier "all-invalid" branch never ran and the tag persisted.
|
|
473
|
+
await storage.updateMemoryFrontmatter(id, { provenance: "verified", sources: [] });
|
|
474
|
+
|
|
475
|
+
const raw = await readFile(factFilePath(storage, id), "utf-8");
|
|
476
|
+
assert.ok(!/\nsources:/.test(raw), "empty sources array must not emit a line");
|
|
477
|
+
const memories = await storage.readAllMemories();
|
|
478
|
+
const written = memories.find((m) => m.frontmatter.id === id);
|
|
479
|
+
assert.ok(written);
|
|
480
|
+
assert.equal(written!.frontmatter.sources, undefined);
|
|
481
|
+
assert.equal(
|
|
482
|
+
written!.frontmatter.provenance,
|
|
483
|
+
"none",
|
|
484
|
+
"verified tag must downgrade to none when sources array is empty",
|
|
485
|
+
);
|
|
486
|
+
} finally {
|
|
487
|
+
await rm(dir, { recursive: true, force: true });
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("read-path downgrades hand-edited verified tag with no sources line (review round 5)", async () => {
|
|
492
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-handedited-"));
|
|
493
|
+
try {
|
|
494
|
+
const storage = new StorageManager(dir);
|
|
495
|
+
await storage.ensureDirectories();
|
|
496
|
+
// Synthesize a hand-edited / imported memory: provenance: verified with
|
|
497
|
+
// no sources line at all. parseProvenanceTag and parseProvenanceSources
|
|
498
|
+
// are independent, so without the read-path reconcile this would round-trip
|
|
499
|
+
// as an ungrounded "verified" fact.
|
|
500
|
+
const id = await writeFactFile(
|
|
501
|
+
storage,
|
|
502
|
+
"Hand-edited memory claiming verification it cannot back.",
|
|
503
|
+
["provenance: verified"],
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
const memories = await storage.readAllMemories();
|
|
507
|
+
const written = memories.find((m) => m.frontmatter.id === id);
|
|
508
|
+
assert.ok(written);
|
|
509
|
+
assert.equal(written!.frontmatter.sources, undefined, "no sources line on disk");
|
|
510
|
+
assert.equal(
|
|
511
|
+
written!.frontmatter.provenance,
|
|
512
|
+
"none",
|
|
513
|
+
"read-path reconcile must downgrade verified to none without sources",
|
|
514
|
+
);
|
|
515
|
+
} finally {
|
|
516
|
+
await rm(dir, { recursive: true, force: true });
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test("write-path drops source with non-integer character offsets (review round 6)", async () => {
|
|
521
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-intoffset-"));
|
|
522
|
+
try {
|
|
523
|
+
const storage = new StorageManager(dir);
|
|
524
|
+
await storage.ensureDirectories();
|
|
525
|
+
const id = await writeFactFile(storage, "Fact with fractional span offsets.");
|
|
526
|
+
|
|
527
|
+
await storage.updateMemoryFrontmatter(id, {
|
|
528
|
+
sources: [
|
|
529
|
+
{ sessionKey: "s/1", observedAt: "2026-01-01T00:00:00Z", quote: "half", charStart: 2.5, charEnd: 8.5 },
|
|
530
|
+
{ sessionKey: "s/2", observedAt: "2026-01-01T00:00:00Z", quote: "whole", charStart: 0, charEnd: 9 },
|
|
531
|
+
],
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
const memories = await storage.readAllMemories();
|
|
535
|
+
const written = memories.find((m) => m.frontmatter.id === id);
|
|
536
|
+
assert.ok(written);
|
|
537
|
+
assert.equal(written!.frontmatter.sources!.length, 1, "fractional-offset entry dropped, integer entry kept");
|
|
538
|
+
assert.equal(written!.frontmatter.sources![0]!.quote, "whole");
|
|
539
|
+
} finally {
|
|
540
|
+
await rm(dir, { recursive: true, force: true });
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("write-path drops source with overflow-normalized ISO timestamp (review round 6)", async () => {
|
|
545
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-strictts-"));
|
|
546
|
+
try {
|
|
547
|
+
const storage = new StorageManager(dir);
|
|
548
|
+
await storage.ensureDirectories();
|
|
549
|
+
const id = await writeFactFile(storage, "Fact with calendar-overflow timestamp.");
|
|
550
|
+
|
|
551
|
+
await storage.updateMemoryFrontmatter(id, {
|
|
552
|
+
sources: [
|
|
553
|
+
// 2026-02-30 silently normalizes to March 2 under Date.parse; the
|
|
554
|
+
// strict ISO check must reject it. A bare "123" (year 123) is also
|
|
555
|
+
// rejected by the format regex.
|
|
556
|
+
{ sessionKey: "s/1", observedAt: "2026-02-30T00:00:00Z", quote: "overflow" },
|
|
557
|
+
{ sessionKey: "s/2", observedAt: "123", quote: "bare-year" },
|
|
558
|
+
{ sessionKey: "s/3", observedAt: "2026-05-03T10:01:30Z", quote: "good" },
|
|
559
|
+
],
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const memories = await storage.readAllMemories();
|
|
563
|
+
const written = memories.find((m) => m.frontmatter.id === id);
|
|
564
|
+
assert.ok(written);
|
|
565
|
+
assert.equal(written!.frontmatter.sources!.length, 1, "overflow + bare-year dropped, valid ISO kept");
|
|
566
|
+
assert.equal(written!.frontmatter.sources![0]!.quote, "good");
|
|
567
|
+
} finally {
|
|
568
|
+
await rm(dir, { recursive: true, force: true });
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test("write-path drops source with impossible timezone offset (review round 6b)", async () => {
|
|
573
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "remnic-prov-badoffset-"));
|
|
574
|
+
try {
|
|
575
|
+
const storage = new StorageManager(dir);
|
|
576
|
+
await storage.ensureDirectories();
|
|
577
|
+
const id = await writeFactFile(storage, "Fact with impossible offset.");
|
|
578
|
+
|
|
579
|
+
await storage.updateMemoryFrontmatter(id, {
|
|
580
|
+
sources: [
|
|
581
|
+
// +99:99 matches the offset regex but Date.parse returns NaN — the
|
|
582
|
+
// strict check must reject it so downstream Date.parse never sees NaN.
|
|
583
|
+
{ sessionKey: "s/1", observedAt: "2026-01-01T00:00:00+99:99", quote: "badopt" },
|
|
584
|
+
{ sessionKey: "s/2", observedAt: "2026-01-01T00:00:00+05:30", quote: "realoffset" },
|
|
585
|
+
],
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
const memories = await storage.readAllMemories();
|
|
589
|
+
const written = memories.find((m) => m.frontmatter.id === id);
|
|
590
|
+
assert.ok(written);
|
|
591
|
+
assert.equal(written!.frontmatter.sources!.length, 1, "impossible offset dropped, real offset kept");
|
|
592
|
+
assert.equal(written!.frontmatter.sources![0]!.quote, "realoffset");
|
|
593
|
+
} finally {
|
|
594
|
+
await rm(dir, { recursive: true, force: true });
|
|
595
|
+
}
|
|
596
|
+
});
|