@remnic/core 9.3.683 → 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.
Files changed (254) hide show
  1. package/dist/access-boundary.d.ts +4 -4
  2. package/dist/access-boundary.js +12 -11
  3. package/dist/access-cli.js +29 -29
  4. package/dist/access-http.d.ts +4 -4
  5. package/dist/access-http.js +17 -16
  6. package/dist/access-mcp.d.ts +4 -4
  7. package/dist/access-mcp.js +14 -13
  8. package/dist/access-operations.d.ts +4 -4
  9. package/dist/access-operations.js +13 -12
  10. package/dist/{access-service-DvA6jyHL.d.ts → access-service-D-siI-xJ.d.ts} +2 -2
  11. package/dist/access-service.d.ts +4 -4
  12. package/dist/access-service.js +11 -10
  13. package/dist/access-surface-catalog.d.ts +4 -4
  14. package/dist/action-confidence.d.ts +1 -1
  15. package/dist/active-memory-bridge.d.ts +1 -1
  16. package/dist/active-recall.d.ts +1 -1
  17. package/dist/active-recall.js +2 -2
  18. package/dist/adapters/index.js +4 -4
  19. package/dist/adapters/registry.js +2 -2
  20. package/dist/behavior-learner.d.ts +1 -1
  21. package/dist/behavior-signals.d.ts +1 -1
  22. package/dist/bootstrap.d.ts +3 -3
  23. package/dist/briefing.d.ts +1 -1
  24. package/dist/briefing.js +4 -3
  25. package/dist/buffer-surprise-report.d.ts +1 -1
  26. package/dist/buffer.d.ts +1 -1
  27. package/dist/calibration.d.ts +1 -1
  28. package/dist/capabilities.d.ts +1 -1
  29. package/dist/causal-behavior.d.ts +1 -1
  30. package/dist/causal-consolidation.d.ts +1 -1
  31. package/dist/causal-consolidation.js +5 -4
  32. package/dist/causal-consolidation.js.map +1 -1
  33. package/dist/{chunk-O2WELT5C.js → chunk-2IBGHRIO.js} +2 -2
  34. package/dist/{chunk-I3HSKQT7.js → chunk-2L3KLWOV.js} +27 -27
  35. package/dist/{chunk-V254FAT5.js → chunk-3EVIMVQU.js} +2 -2
  36. package/dist/{chunk-H4BDNIKQ.js → chunk-3MY4W5V4.js} +8 -8
  37. package/dist/{chunk-3IND7N4X.js → chunk-3Z7NPD5T.js} +2 -2
  38. package/dist/{chunk-TY5NT3T3.js → chunk-53FDU4CE.js} +13 -8
  39. package/dist/chunk-53FDU4CE.js.map +1 -0
  40. package/dist/{chunk-QVWM4C24.js → chunk-5N5DXYDW.js} +6 -6
  41. package/dist/{chunk-FDSOMA6M.js → chunk-5OE4PYY5.js} +4 -4
  42. package/dist/{chunk-IUZWBCJX.js → chunk-6QM24CP7.js} +9 -6
  43. package/dist/chunk-6QM24CP7.js.map +1 -0
  44. package/dist/{chunk-H6PMGMNP.js → chunk-6VMIHVGO.js} +2 -2
  45. package/dist/{chunk-APJQ6UEA.js → chunk-AGNBY3VG.js} +4 -4
  46. package/dist/{chunk-GSTYVG5L.js → chunk-BFVPIKDY.js} +3 -3
  47. package/dist/{chunk-EG4TCVMU.js → chunk-DQY7NJ5L.js} +2 -2
  48. package/dist/{chunk-ARLRTZZZ.js → chunk-FMEKEF47.js} +2 -2
  49. package/dist/{chunk-NHFXF4ZO.js → chunk-FYEVFGJD.js} +2 -2
  50. package/dist/{chunk-OHX52AOS.js → chunk-GTDH3IUH.js} +2 -2
  51. package/dist/{chunk-ODWI5XU2.js → chunk-GWKCEM3S.js} +2 -2
  52. package/dist/{chunk-UAODC6GJ.js → chunk-J2FBJ63F.js} +3 -3
  53. package/dist/{chunk-KV6CX4ON.js → chunk-K6ZN34WC.js} +2 -2
  54. package/dist/{chunk-6VP3YUCS.js → chunk-LLONI6PY.js} +2 -2
  55. package/dist/{chunk-GNAMDNGT.js → chunk-LXH3DIF2.js} +4 -4
  56. package/dist/{chunk-TOQEZ63C.js → chunk-M3FWYURP.js} +5 -5
  57. package/dist/{chunk-B2B2IHUH.js → chunk-M6BVYHBU.js} +2 -2
  58. package/dist/{chunk-FMSDA2D3.js → chunk-NGFEWFNK.js} +1 -1
  59. package/dist/chunk-NGFEWFNK.js.map +1 -0
  60. package/dist/{chunk-QUA2JPH2.js → chunk-NHQGDVJF.js} +3 -3
  61. package/dist/{chunk-KACIOX42.js → chunk-OMLIFZ4I.js} +2 -2
  62. package/dist/{chunk-M4I3TREG.js → chunk-OXNOINIP.js} +21 -21
  63. package/dist/{chunk-2QSZNTDO.js → chunk-RKNJBZ55.js} +4 -4
  64. package/dist/{chunk-UJDV2NLT.js → chunk-ROHLEUTH.js} +4 -4
  65. package/dist/{chunk-WEPMT6SC.js → chunk-V25ZAOSB.js} +5 -5
  66. package/dist/{chunk-L5MUA6Q7.js → chunk-WI7JKV2T.js} +2 -2
  67. package/dist/{chunk-NQMBSSWW.js → chunk-WRE3JPAW.js} +2 -2
  68. package/dist/{chunk-I75DF4FZ.js → chunk-XEA4Z7JU.js} +2 -2
  69. package/dist/{chunk-G7Z3C2X6.js → chunk-XWEXT4XU.js} +2 -2
  70. package/dist/chunk-ZPQVJEVQ.js +184 -0
  71. package/dist/chunk-ZPQVJEVQ.js.map +1 -0
  72. package/dist/{cli-feUe-x3I.d.ts → cli-ooj6JQBS.d.ts} +3 -3
  73. package/dist/cli.d.ts +5 -5
  74. package/dist/cli.js +32 -32
  75. package/dist/compounding/engine.d.ts +1 -1
  76. package/dist/compounding/engine.js +4 -3
  77. package/dist/compounding/preference-consolidator.d.ts +1 -1
  78. package/dist/compression-optimizer.d.ts +1 -1
  79. package/dist/config.d.ts +1 -1
  80. package/dist/config.js +2 -2
  81. package/dist/connectors/codex-materialize-runner.d.ts +1 -1
  82. package/dist/connectors/codex-materialize-runner.js +4 -3
  83. package/dist/connectors/codex-materialize.d.ts +1 -1
  84. package/dist/connectors/index.d.ts +1 -1
  85. package/dist/connectors/index.js +7 -7
  86. package/dist/consolidation-provenance-check.d.ts +1 -1
  87. package/dist/consolidation-undo.d.ts +1 -1
  88. package/dist/contradiction/index.d.ts +1 -1
  89. package/dist/conversation-index/backend.d.ts +1 -1
  90. package/dist/conversation-index/chunker.d.ts +1 -1
  91. package/dist/conversation-index/faiss-adapter.d.ts +1 -1
  92. package/dist/conversation-index/indexer.d.ts +1 -1
  93. package/dist/conversation-index/search.d.ts +1 -1
  94. package/dist/day-summary.d.ts +1 -1
  95. package/dist/delinearize.d.ts +1 -1
  96. package/dist/direct-answer-wiring.d.ts +1 -1
  97. package/dist/direct-answer.d.ts +1 -1
  98. package/dist/embedding-fallback.d.ts +1 -1
  99. package/dist/enrichment/index.d.ts +1 -1
  100. package/dist/entity-retrieval.d.ts +1 -1
  101. package/dist/entity-retrieval.js +5 -3
  102. package/dist/entity-schema.d.ts +1 -1
  103. package/dist/explicit-capture.d.ts +3 -3
  104. package/dist/extraction-judge-telemetry.d.ts +1 -1
  105. package/dist/extraction-judge-training.d.ts +1 -1
  106. package/dist/extraction-judge.d.ts +1 -1
  107. package/dist/extraction.d.ts +1 -1
  108. package/dist/fallback-llm.d.ts +1 -1
  109. package/dist/{first-start-migration-FF7YFGRP.js → first-start-migration-PG5HBC3K.js} +4 -4
  110. package/dist/identity-continuity.d.ts +1 -1
  111. package/dist/importance.d.ts +1 -1
  112. package/dist/index.d.ts +8 -8
  113. package/dist/index.js +64 -64
  114. package/dist/intent.d.ts +1 -1
  115. package/dist/lcm/engine.d.ts +1 -1
  116. package/dist/lcm/engine.js +2 -2
  117. package/dist/lcm/index.d.ts +1 -1
  118. package/dist/lcm/index.js +5 -5
  119. package/dist/lcm/tools.d.ts +1 -1
  120. package/dist/lifecycle.d.ts +1 -1
  121. package/dist/live-connectors-runner.d.ts +1 -1
  122. package/dist/local-llm.d.ts +1 -1
  123. package/dist/maintenance/memory-governance.d.ts +1 -1
  124. package/dist/maintenance/memory-governance.js +5 -3
  125. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -3
  126. package/dist/maintenance/rebuild-memory-projection.js +6 -4
  127. package/dist/mcp-memory-inspector-app.d.ts +4 -4
  128. package/dist/memory-action-policy.d.ts +1 -1
  129. package/dist/memory-cache.d.ts +1 -1
  130. package/dist/memory-lifecycle-ledger-utils.d.ts +1 -1
  131. package/dist/memory-projection-store.d.ts +1 -1
  132. package/dist/memory-provenance.d.ts +1 -1
  133. package/dist/memory-worth-outcomes.d.ts +1 -1
  134. package/dist/models-json.d.ts +1 -1
  135. package/dist/namespaces/migrate.d.ts +1 -1
  136. package/dist/namespaces/migrate.js +8 -7
  137. package/dist/namespaces/principal.d.ts +1 -1
  138. package/dist/namespaces/search.d.ts +1 -1
  139. package/dist/namespaces/search.js +3 -3
  140. package/dist/namespaces/storage.d.ts +1 -1
  141. package/dist/namespaces/storage.js +5 -3
  142. package/dist/native-knowledge.d.ts +1 -1
  143. package/dist/operator-toolkit.d.ts +1 -1
  144. package/dist/operator-toolkit.js +11 -11
  145. package/dist/{orchestrator-7zPqGupX.d.ts → orchestrator-DIDDvwDw.d.ts} +2 -2
  146. package/dist/orchestrator.d.ts +3 -3
  147. package/dist/orchestrator.js +23 -22
  148. package/dist/patterns-cli.d.ts +1 -1
  149. package/dist/policy-runtime.d.ts +1 -1
  150. package/dist/provenance.d.ts +94 -0
  151. package/dist/provenance.js +17 -0
  152. package/dist/provenance.js.map +1 -0
  153. package/dist/qmd-recall-cache.d.ts +1 -1
  154. package/dist/qmd.d.ts +1 -1
  155. package/dist/recall-disclosure-escalation.d.ts +1 -1
  156. package/dist/recall-explain-renderer.d.ts +1 -1
  157. package/dist/recall-explain-renderer.js +3 -3
  158. package/dist/recall-planner-llm.d.ts +1 -1
  159. package/dist/recall-state.d.ts +1 -1
  160. package/dist/recall-tag-filter.d.ts +1 -1
  161. package/dist/recall-xray-cli.d.ts +1 -1
  162. package/dist/recall-xray-cli.js +4 -4
  163. package/dist/recall-xray-renderer.d.ts +1 -1
  164. package/dist/recall-xray-renderer.js +3 -3
  165. package/dist/recall-xray.d.ts +1 -1
  166. package/dist/recall-xray.js +2 -2
  167. package/dist/resolve-auth-token.d.ts +1 -1
  168. package/dist/resume-bundles.js +3 -3
  169. package/dist/retrieval-agents.d.ts +1 -1
  170. package/dist/retrieval-tiers.d.ts +1 -1
  171. package/dist/routing/engine.d.ts +1 -1
  172. package/dist/routing/store.d.ts +1 -1
  173. package/dist/search/embed-helper.d.ts +1 -1
  174. package/dist/search/factory.d.ts +1 -1
  175. package/dist/search/factory.js +2 -2
  176. package/dist/search/index.d.ts +1 -1
  177. package/dist/search/index.js +4 -4
  178. package/dist/search/lancedb-backend.d.ts +1 -1
  179. package/dist/search/meilisearch-backend.d.ts +1 -1
  180. package/dist/search/noop-backend.d.ts +1 -1
  181. package/dist/search/orama-backend.d.ts +1 -1
  182. package/dist/search/port.d.ts +1 -1
  183. package/dist/search/remote-backend.d.ts +1 -1
  184. package/dist/{semantic-consolidation-BX9Z9_aK.d.ts → semantic-consolidation-CWch5uM7.d.ts} +1 -1
  185. package/dist/semantic-consolidation.d.ts +2 -2
  186. package/dist/semantic-consolidation.js +5 -4
  187. package/dist/semantic-rule-promotion.js +5 -3
  188. package/dist/semantic-rule-verifier.d.ts +1 -1
  189. package/dist/semantic-rule-verifier.js +5 -3
  190. package/dist/session-observer-bands.d.ts +1 -1
  191. package/dist/session-observer-state.d.ts +1 -1
  192. package/dist/shared-context/manager.d.ts +1 -1
  193. package/dist/signal.d.ts +1 -1
  194. package/dist/storage.d.ts +1 -1
  195. package/dist/storage.js +4 -2
  196. package/dist/summarizer.d.ts +1 -1
  197. package/dist/summary-snapshot.d.ts +1 -1
  198. package/dist/temporal-supersession.d.ts +1 -1
  199. package/dist/temporal-validity.d.ts +1 -1
  200. package/dist/threading.d.ts +1 -1
  201. package/dist/tier-migration.d.ts +1 -1
  202. package/dist/tier-routing.d.ts +1 -1
  203. package/dist/topics.d.ts +1 -1
  204. package/dist/transcript.d.ts +1 -1
  205. package/dist/transfer/import-sqlite.js +2 -2
  206. package/dist/{types-D3pm4NhH.d.ts → types-Dm5xxVrr.d.ts} +61 -1
  207. package/dist/types.d.ts +1 -1
  208. package/dist/types.js +1 -1
  209. package/dist/utility-runtime.d.ts +1 -1
  210. package/dist/verified-recall.js +5 -3
  211. package/package.json +2 -2
  212. package/src/config.test.ts +112 -0
  213. package/src/config.ts +5 -5
  214. package/src/provenance-frontmatter.test.ts +596 -0
  215. package/src/provenance.ts +305 -0
  216. package/src/storage.ts +5 -5
  217. package/src/types.ts +76 -0
  218. package/dist/chunk-FMSDA2D3.js.map +0 -1
  219. package/dist/chunk-IUZWBCJX.js.map +0 -1
  220. package/dist/chunk-PHK3HARR.js +0 -32
  221. package/dist/chunk-PHK3HARR.js.map +0 -1
  222. package/dist/chunk-TY5NT3T3.js.map +0 -1
  223. /package/dist/{chunk-O2WELT5C.js.map → chunk-2IBGHRIO.js.map} +0 -0
  224. /package/dist/{chunk-I3HSKQT7.js.map → chunk-2L3KLWOV.js.map} +0 -0
  225. /package/dist/{chunk-V254FAT5.js.map → chunk-3EVIMVQU.js.map} +0 -0
  226. /package/dist/{chunk-H4BDNIKQ.js.map → chunk-3MY4W5V4.js.map} +0 -0
  227. /package/dist/{chunk-3IND7N4X.js.map → chunk-3Z7NPD5T.js.map} +0 -0
  228. /package/dist/{chunk-QVWM4C24.js.map → chunk-5N5DXYDW.js.map} +0 -0
  229. /package/dist/{chunk-FDSOMA6M.js.map → chunk-5OE4PYY5.js.map} +0 -0
  230. /package/dist/{chunk-H6PMGMNP.js.map → chunk-6VMIHVGO.js.map} +0 -0
  231. /package/dist/{chunk-APJQ6UEA.js.map → chunk-AGNBY3VG.js.map} +0 -0
  232. /package/dist/{chunk-GSTYVG5L.js.map → chunk-BFVPIKDY.js.map} +0 -0
  233. /package/dist/{chunk-EG4TCVMU.js.map → chunk-DQY7NJ5L.js.map} +0 -0
  234. /package/dist/{chunk-ARLRTZZZ.js.map → chunk-FMEKEF47.js.map} +0 -0
  235. /package/dist/{chunk-NHFXF4ZO.js.map → chunk-FYEVFGJD.js.map} +0 -0
  236. /package/dist/{chunk-OHX52AOS.js.map → chunk-GTDH3IUH.js.map} +0 -0
  237. /package/dist/{chunk-ODWI5XU2.js.map → chunk-GWKCEM3S.js.map} +0 -0
  238. /package/dist/{chunk-UAODC6GJ.js.map → chunk-J2FBJ63F.js.map} +0 -0
  239. /package/dist/{chunk-KV6CX4ON.js.map → chunk-K6ZN34WC.js.map} +0 -0
  240. /package/dist/{chunk-6VP3YUCS.js.map → chunk-LLONI6PY.js.map} +0 -0
  241. /package/dist/{chunk-GNAMDNGT.js.map → chunk-LXH3DIF2.js.map} +0 -0
  242. /package/dist/{chunk-TOQEZ63C.js.map → chunk-M3FWYURP.js.map} +0 -0
  243. /package/dist/{chunk-B2B2IHUH.js.map → chunk-M6BVYHBU.js.map} +0 -0
  244. /package/dist/{chunk-QUA2JPH2.js.map → chunk-NHQGDVJF.js.map} +0 -0
  245. /package/dist/{chunk-KACIOX42.js.map → chunk-OMLIFZ4I.js.map} +0 -0
  246. /package/dist/{chunk-M4I3TREG.js.map → chunk-OXNOINIP.js.map} +0 -0
  247. /package/dist/{chunk-2QSZNTDO.js.map → chunk-RKNJBZ55.js.map} +0 -0
  248. /package/dist/{chunk-UJDV2NLT.js.map → chunk-ROHLEUTH.js.map} +0 -0
  249. /package/dist/{chunk-WEPMT6SC.js.map → chunk-V25ZAOSB.js.map} +0 -0
  250. /package/dist/{chunk-L5MUA6Q7.js.map → chunk-WI7JKV2T.js.map} +0 -0
  251. /package/dist/{chunk-NQMBSSWW.js.map → chunk-WRE3JPAW.js.map} +0 -0
  252. /package/dist/{chunk-I75DF4FZ.js.map → chunk-XEA4Z7JU.js.map} +0 -0
  253. /package/dist/{chunk-G7Z3C2X6.js.map → chunk-XWEXT4XU.js.map} +0 -0
  254. /package/dist/{first-start-migration-FF7YFGRP.js.map → first-start-migration-PG5HBC3K.js.map} +0 -0
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Claim-level provenance spans (issue #1575 PR 1).
3
+ *
4
+ * Centralizes the parse/serialize logic for the `sources` array and the
5
+ * coarse `provenance` strength tag, plus the `provenance` config-block
6
+ * parser. Extracted from `storage.ts` and `config.ts` so those files keep
7
+ * only thin delegation call-sites — the frontmatter round-trip and config
8
+ * parsing growth lives here (issue #1520 ratchet discipline).
9
+ *
10
+ * Contract:
11
+ * - When no provenance fields are present, output is byte-identical to
12
+ * pre-feature behavior (rule 39).
13
+ * - Corrupt `sources` lines / unknown `provenance` tags drop to
14
+ * `undefined` on read so a malformed frontmatter never poisons
15
+ * downstream readers (rule 34 spirit — drop corrupt rather than poison).
16
+ * - Validation lives on the write path; this module only parses.
17
+ * - Invariant (review round 5, cursor thread KQN): a `verified` or
18
+ * `unverified` tag is NEVER persisted/read without surviving sources —
19
+ * without excerpts the tag is indistinguishable from a grounded fact to
20
+ * downstream faithfulness/correction/TrustScore surfaces. The invariant
21
+ * is enforced symmetrically: `serializeProvenanceFields` (write) and
22
+ * `reconcileProvenanceRead` (read) both downgrade the tag to `none`
23
+ * when no source survives.
24
+ */
25
+
26
+ import { z } from "zod";
27
+
28
+ import { coerceBool, coerceNumber } from "./connectors/coerce.js";
29
+ import { readEnvVar } from "./runtime/env.js";
30
+ import type { MemoryFrontmatter, ProvenanceConfig, ProvenanceSource } from "./types.js";
31
+
32
+ /**
33
+ * Canonical key order for a serialized `ProvenanceSource` (issue #1575).
34
+ * Deterministic emission (rule 38) — readers and the byte-identical-when-off
35
+ * contract (rule 39) depend on this order never drifting.
36
+ */
37
+ const PROVENANCE_SOURCE_KEY_ORDER = [
38
+ "sessionKey",
39
+ "turnId",
40
+ "observedAt",
41
+ "quote",
42
+ "charStart",
43
+ "charEnd",
44
+ ] as const;
45
+
46
+ /**
47
+ * Build a single `ProvenanceSource` object whose keys appear in the canonical
48
+ * order, omitting absent optional fields. The result is what gets fed to
49
+ * `JSON.stringify` so the on-disk line is deterministic.
50
+ */
51
+ function canonicalProvenanceSource(src: ProvenanceSource): Record<string, unknown> {
52
+ const out: Record<string, unknown> = {};
53
+ for (const key of PROVENANCE_SOURCE_KEY_ORDER) {
54
+ const value = src[key];
55
+ if (value !== undefined) out[key] = value;
56
+ }
57
+ return out;
58
+ }
59
+
60
+ /**
61
+ * Serialize the `sources` array (issue #1575) as a single JSON line, matching
62
+ * the `structuredAttributes` precedent. Each entry is rebuilt in canonical key
63
+ * order (rule 38) so the output is byte-stable. The `provenance` enum is
64
+ * emitted bare (same style as `status`) — only the three documented values
65
+ * are ever written.
66
+ *
67
+ * Verified-requires-evidence invariant (review round 5, cursor thread KQN):
68
+ * a `verified`/`unverified` tag is downgraded to `none` whenever no source
69
+ * survives write validation — whether sources were absent, an empty array,
70
+ * or all entries failed the schema. This covers all three failure shapes the
71
+ * earlier 3-branch logic left open (`{provenance:"verified"}`,
72
+ * `{sources:[],provenance:"verified"}`, `{sources:[invalid…],provenance:"verified"}`).
73
+ */
74
+ export function serializeProvenanceFields(fm: MemoryFrontmatter, lines: string[]): void {
75
+ let hasValidSources = false;
76
+ if (fm.sources && fm.sources.length > 0) {
77
+ // Validate each entry against the same schema used on read so invalid
78
+ // in-memory sources are dropped at write time, not silently lost on the
79
+ // next read (review thread 4 — write-path validation parity).
80
+ const canonical: Record<string, unknown>[] = [];
81
+ for (const src of fm.sources) {
82
+ const result = ProvenanceSourceSchema.safeParse(src);
83
+ if (result.success) canonical.push(canonicalProvenanceSource(result.data));
84
+ }
85
+ if (canonical.length > 0) {
86
+ lines.push(`sources: ${JSON.stringify(canonical)}`);
87
+ hasValidSources = true;
88
+ }
89
+ }
90
+ // A verified/unverified tag requires surviving evidence; without it the
91
+ // tag is meaningless downstream (faithfulness/TrustScore cannot distinguish
92
+ // it from a grounded fact). Downgrade to "none" regardless of WHY no source
93
+ // survived (absent / empty / all-invalid) — single invariant, all cases.
94
+ const tag =
95
+ (fm.provenance === "verified" || fm.provenance === "unverified") && !hasValidSources
96
+ ? "none"
97
+ : fm.provenance;
98
+ if (tag) {
99
+ lines.push(`provenance: ${tag}`);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Parse the coarse `provenance` strength tag (issue #1575). Returns
105
+ * `undefined` for missing/blank/unknown values so a corrupt or hand-edited
106
+ * field fails safely to the legacy-equivalent `"none"` semantics on read
107
+ * (rule 34 spirit — drop corrupt rather than poison).
108
+ */
109
+ export function parseProvenanceTag(
110
+ raw: string | undefined,
111
+ ): "verified" | "unverified" | "none" | undefined {
112
+ if (raw === undefined) return undefined;
113
+ const trimmed = raw.trim();
114
+ if (trimmed.length === 0) return undefined;
115
+ if (trimmed === "verified" || trimmed === "unverified" || trimmed === "none") {
116
+ return trimmed;
117
+ }
118
+ return undefined;
119
+ }
120
+
121
+ /**
122
+ * Enforce the verified-requires-evidence invariant on the READ path.
123
+ * `parseProvenanceTag` and `parseProvenanceSources` are independent (they
124
+ * parse separate frontmatter lines), so a hand-edited or imported memory may
125
+ * carry `provenance: verified` with no surviving `sources` — a corrupt line,
126
+ * an empty array, or all-invalid entries. Downgrade such a tag to `none` so
127
+ * the in-memory object never exposes an ungrounded "verified" fact
128
+ * (review round 5, cursor thread KQN — read-path parity with the write-path
129
+ * downgrade in `serializeProvenanceFields`). `none`/`undefined` tags pass
130
+ * through unchanged.
131
+ */
132
+ export function reconcileProvenanceRead(
133
+ tag: "verified" | "unverified" | "none" | undefined,
134
+ sources: ProvenanceSource[] | undefined,
135
+ ): "verified" | "unverified" | "none" | undefined {
136
+ if ((tag === "verified" || tag === "unverified") && (!sources || sources.length === 0)) {
137
+ return "none";
138
+ }
139
+ return tag;
140
+ }
141
+
142
+ /**
143
+ * Strict ISO-8601 timestamp check (review round 6, codex thread OXPAp).
144
+ * `Date.parse` accepts non-ISO strings (bare years like `"123"`) and
145
+ * silently normalizes calendar overflow (`2026-02-30` -> March 2, hour 25
146
+ * -> next day), so malformed provenance survives as if valid. Require the
147
+ * full `YYYY-MM-DDTHH:MM:SS[.fff](Z|±HH:MM)` shape and reject overflow via a
148
+ * `Date.UTC` round-trip component check — the offset does not affect whether
149
+ * a wall-clock field overflows, so this is correct for any timezone suffix.
150
+ */
151
+ function isStrictIsoTimestamp(s: string): boolean {
152
+ const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.exec(s);
153
+ if (!m) return false;
154
+ const y = Number(m[1]), mo = Number(m[2]), da = Number(m[3]);
155
+ const h = Number(m[4]), mi = Number(m[5]), se = Number(m[6]);
156
+ // Date.UTC normalizes overflow (Feb 30 -> Mar 2); a component round-trip
157
+ // catches what Date.parse silently accepts.
158
+ const d = new Date(Date.UTC(y, mo - 1, da, h, mi, se));
159
+ // The component round-trip validates wall-clock overflow (Feb 30 -> Mar 2);
160
+ // Date.parse additionally rejects impossible timezone offsets such as
161
+ // +99:99, which the regex accepts but the runtime treats as NaN
162
+ // (codex thread OXQ0e).
163
+ return (
164
+ !Number.isNaN(Date.parse(s)) &&
165
+ d.getUTCFullYear() === y &&
166
+ d.getUTCMonth() === mo - 1 &&
167
+ d.getUTCDate() === da &&
168
+ d.getUTCHours() === h &&
169
+ d.getUTCMinutes() === mi &&
170
+ d.getUTCSeconds() === se
171
+ );
172
+ }
173
+
174
+ /**
175
+ * Zod schema for a single `ProvenanceSource` entry (issue #1575). Parsed
176
+ * JSON from frontmatter is external data, so each entry is validated here
177
+ * rather than trusted via a cast (rule: no inline-cast-access on parsed
178
+ * blobs). `safeParse` lets us drop corrupt entries individually instead of
179
+ * failing the whole field.
180
+ */
181
+ const ProvenanceSourceSchema = z
182
+ .object({
183
+ sessionKey: z.string().min(1),
184
+ observedAt: z
185
+ .string()
186
+ .min(1)
187
+ .refine(isStrictIsoTimestamp, "must be a valid ISO 8601 timestamp (YYYY-MM-DDTHH:MM:SS[Z|±HH:MM], no calendar overflow)"),
188
+ quote: z.string().min(1),
189
+ turnId: z.string().min(1).optional(),
190
+ charStart: z.number().finite().nonnegative().int().optional(),
191
+ charEnd: z.number().finite().nonnegative().int().optional(),
192
+ })
193
+ .refine(
194
+ (src) => src.charStart === undefined || src.charEnd === undefined || src.charEnd >= src.charStart,
195
+ { message: "charEnd must be >= charStart (half-open interval, rule 35)" },
196
+ );
197
+
198
+ /**
199
+ * Parse the `sources` array (issue #1575) from its single-line JSON form.
200
+ * Mirrors `parseStructuredAttributes` (JSON.parse) but validates each entry
201
+ * against `ProvenanceSourceSchema` and DROPS corrupt ones rather than
202
+ * poisoning downstream readers — the same "drop corrupt rather than poison"
203
+ * contract as `parseMemoryWorthCounterField`. If no entry survives, the
204
+ * whole field is `undefined` (legacy-equivalent).
205
+ */
206
+ export function parseProvenanceSources(raw: string | undefined): ProvenanceSource[] | undefined {
207
+ if (raw === undefined) return undefined;
208
+ const trimmed = raw.trim();
209
+ if (trimmed.length === 0) return undefined;
210
+ let parsed: unknown;
211
+ try {
212
+ parsed = JSON.parse(trimmed);
213
+ } catch {
214
+ return undefined;
215
+ }
216
+ if (!Array.isArray(parsed)) return undefined;
217
+ const sources: ProvenanceSource[] = [];
218
+ for (const entry of parsed) {
219
+ const result = ProvenanceSourceSchema.safeParse(entry);
220
+ if (result.success) sources.push(result.data);
221
+ }
222
+ return sources.length > 0 ? sources : undefined;
223
+ }
224
+
225
+ /**
226
+ * Parse the `provenance` config block (issue #1575 PR 1). Validates the
227
+ * shape before applying defaults — a shorthand like `provenance: false` must
228
+ * reject loudly rather than normalize to `{}` and silently enable the feature
229
+ * (rule 51). Booleans coerce via `coerceBool` (rule 36); numeric cap clamps
230
+ * at 1 (rule 28). `REMNIC_PROVENANCE_ENABLED` / `ENGRAM_PROVENANCE_ENABLED`
231
+ * are honored only when the `enabled` key is omitted (explicit config wins).
232
+ *
233
+ * Schema-default note (review rounds 1–3, settled): `provenance.enabled` has
234
+ * NO `"default"` in any plugin.json schema. OpenClaw's loader runs
235
+ * `applyDefaults: true` before exposing `api.pluginConfig` (src/index.ts:1345,
236
+ * PR #1593 round 8), so a schema default would be materialized into the
237
+ * merged config and mask the `REMNIC_`/`ENGRAM_` env override. The code-level
238
+ * default-on here (`return true` when `enabled` is omitted) supplies the
239
+ * default-on behavior without that materialization. This matches the
240
+ * emitLegacyTools/namespaceCatalogEnabled precedent, which omits the env
241
+ * override only after a raw-vs-effective split — overkill for a single
242
+ * boolean, so this field uses the simpler omit-the-default approach.
243
+ */
244
+ export function parseProvenanceConfig(raw: unknown): ProvenanceConfig {
245
+ if (
246
+ raw !== undefined &&
247
+ (raw === null || typeof raw !== "object" || Array.isArray(raw))
248
+ ) {
249
+ throw new Error(
250
+ `provenance must be an object (got ${JSON.stringify(raw)}). Use provenance: { enabled: false } to opt out; omit the key to use the default-on behavior (issue #1575).`,
251
+ );
252
+ }
253
+ const rawProvenance =
254
+ raw && typeof raw === "object" && !Array.isArray(raw)
255
+ ? (raw as Record<string, unknown>)
256
+ : {};
257
+ return {
258
+ enabled: (() => {
259
+ if (rawProvenance.enabled === undefined) {
260
+ const envEnabled =
261
+ readEnvVar("REMNIC_PROVENANCE_ENABLED") ?? readEnvVar("ENGRAM_PROVENANCE_ENABLED");
262
+ if (envEnabled !== undefined) {
263
+ const coerced = coerceBool(envEnabled);
264
+ if (coerced === undefined) {
265
+ throw new Error(
266
+ `REMNIC_PROVENANCE_ENABLED must be a boolean-like value (true/false/1/0/yes/no/on/off); got ${JSON.stringify(envEnabled)}`,
267
+ );
268
+ }
269
+ return coerced;
270
+ }
271
+ return true;
272
+ }
273
+ const coerced = coerceBool(rawProvenance.enabled);
274
+ if (coerced === undefined) {
275
+ throw new Error(
276
+ `provenance.enabled must be a boolean or one of "true"/"false"/"1"/"0"/"yes"/"no"/"on"/"off" (got ${JSON.stringify(rawProvenance.enabled)}). Omit the key to use the default-on behavior (issue #1575).`,
277
+ );
278
+ }
279
+ return coerced;
280
+ })(),
281
+ maxQuoteChars: (() => {
282
+ if (rawProvenance.maxQuoteChars === undefined) return 300;
283
+ const rawCap = coerceNumber(rawProvenance.maxQuoteChars);
284
+ // Reject present-but-invalid rather than silently widening the cap
285
+ // (AGENTS.md input-validation rule — a typo should not persist more
286
+ // text than the operator configured).
287
+ if (rawCap === undefined || !Number.isFinite(rawCap) || rawCap < 1 || !Number.isInteger(rawCap)) {
288
+ throw new Error(
289
+ `provenance.maxQuoteChars must be a positive integer >= 1 (got ${JSON.stringify(rawProvenance.maxQuoteChars)}).`,
290
+ );
291
+ }
292
+ return rawCap;
293
+ })(),
294
+ requireSpans: (() => {
295
+ if (rawProvenance.requireSpans === undefined) return false;
296
+ const coerced = coerceBool(rawProvenance.requireSpans);
297
+ if (coerced === undefined) {
298
+ throw new Error(
299
+ `provenance.requireSpans must be a boolean or one of "true"/"false"/"1"/"0"/"yes"/"no"/"on"/"off" (got ${JSON.stringify(rawProvenance.requireSpans)}).`,
300
+ );
301
+ }
302
+ return coerced;
303
+ })(),
304
+ };
305
+ }
package/src/storage.ts CHANGED
@@ -9,6 +9,7 @@ import { assertPathInsideRoot } from "./utils/path-containment.js";
9
9
  import { getCachedEntities, invalidateAllForDir, setCachedEntities } from "./memory-cache.js";
10
10
  import { rotateMarkdownFileToArchive } from "./hygiene.js";
11
11
  import { sanitizeMemoryContent } from "./sanitize.js";
12
+ import { serializeProvenanceFields, parseProvenanceSources, parseProvenanceTag, reconcileProvenanceRead } from "./provenance.js";
12
13
  import { createVersion as createPageVersion, type VersioningConfig, type VersionTrigger } from "./page-versioning.js";
13
14
  import { isValidTranscriptDate, WEARABLES_DIR_NAME } from "./wearables/day-store.js";
14
15
  import {
@@ -440,6 +441,7 @@ function serializeFrontmatter(fm: MemoryFrontmatter): string {
440
441
  if (fm.last_reinforced_at) {
441
442
  lines.push(`last_reinforced_at: ${fm.last_reinforced_at}`);
442
443
  }
444
+ serializeProvenanceFields(fm, lines);
443
445
  lines.push("---");
444
446
  return lines.join("\n");
445
447
  }
@@ -843,13 +845,11 @@ function parseFrontmatter(
843
845
  // PR; no code produces these fields yet.
844
846
  derived_from,
845
847
  derived_via,
846
- // Pattern-reinforcement metadata (issue #687 PR 2/4). Parse
847
- // permissively: invalid values (negative, non-integer, blank
848
- // ISO-strings) are dropped to undefined so a corrupt frontmatter
849
- // never poisons downstream scoring. Validation lives on the
850
- // write path in serializeFrontmatter.
848
+ // Pattern-reinforcement metadata (issue #687 PR 2/4) — drop corrupt values on read (rule 34).
851
849
  reinforcement_count: parseReinforcementCountField(fm.reinforcement_count),
852
850
  last_reinforced_at: fm.last_reinforced_at || undefined,
851
+ sources: parseProvenanceSources(fm.sources),
852
+ provenance: reconcileProvenanceRead(parseProvenanceTag(fm.provenance), parseProvenanceSources(fm.sources)),
853
853
  },
854
854
  content,
855
855
  };
package/src/types.ts CHANGED
@@ -387,6 +387,29 @@ export interface ProceduralConfig {
387
387
  recallMaxProcedures: number;
388
388
  }
389
389
 
390
+ /**
391
+ * Claim-level provenance spans (issue #1575). Controls whether extracted
392
+ * facts carry verbatim source-utterance excerpts and the strength tag that
393
+ * downstream features (faithfulness gate #1576, correction UX #1580/#1583,
394
+ * tombstone matching #1579, TrustScore #1577) consume.
395
+ *
396
+ * PR 1 parses the block + schema only; the extraction prompt and validator
397
+ * land in PR 2. When `enabled` is false, extraction prompt/output are
398
+ * byte-identical to pre-feature behavior (rule 39).
399
+ */
400
+ export interface ProvenanceConfig {
401
+ /** Emit provenance spans on new extractions. Default true. */
402
+ enabled: boolean;
403
+ /** Maximum characters stored per quote; longer quotes truncate at a word boundary. Default 300. */
404
+ maxQuoteChars: number;
405
+ /**
406
+ * When true, facts whose quote cannot be located are routed to
407
+ * `pending_review` instead of `active`. Stays false until #1576 lands and
408
+ * the bench shows the gate is safe (rule 48).
409
+ */
410
+ requireSpans: boolean;
411
+ }
412
+
390
413
  /**
391
414
  * Coding-agent mode config (issue #569).
392
415
  *
@@ -951,6 +974,12 @@ export interface PluginConfig {
951
974
  */
952
975
  dreamsPhases: DreamsPhasesConfig;
953
976
  procedural: ProceduralConfig;
977
+ /**
978
+ * Claim-level provenance spans (issue #1575). Parsed from the
979
+ * `provenance` config block; see `ProvenanceConfig` for the documented
980
+ * defaults. PR 1 parses + persists only; extraction wiring lands in PR 2.
981
+ */
982
+ provenance: ProvenanceConfig;
954
983
  /**
955
984
  * Wearable transcript ingestion (Limitless / Bee / Omi connectors).
956
985
  * Disabled by default; see docs/wearables.md.
@@ -2514,6 +2543,28 @@ export interface MemoryFrontmatter {
2514
2543
  * is absent.
2515
2544
  */
2516
2545
  last_reinforced_at?: string;
2546
+ // Claim-level provenance spans (issue #1575).
2547
+ //
2548
+ // Verbatim source-utterance excerpts that ground each extracted fact, plus
2549
+ // a coarse `provenance` tag recording whether the span was located in the
2550
+ // buffered turn text. Absent on legacy memories written before #1575;
2551
+ // readers MUST treat both `undefined` (legacy) and `provenance: "none"`
2552
+ // uniformly — never crash, never drop the fact (rule 34 spirit).
2553
+ //
2554
+ // `charStart` / `charEnd` are best-effort debugging aids — consumers MUST
2555
+ // tolerate their absence (turn text is not persisted forever). The span
2556
+ // interval is half-open [charStart, charEnd) per rule 35.
2557
+ //
2558
+ // PR 1 wires only the schema + storage round-trip — no extraction prompt,
2559
+ // validator, or read surfaces yet.
2560
+ /** Literal source-utterance excerpts backing this fact (issue #1575). */
2561
+ sources?: ProvenanceSource[];
2562
+ /**
2563
+ * Coarse provenance strength: `"verified"` (span located in source text),
2564
+ * `"unverified"` (span present but not locatable), or `"none"` (no span
2565
+ * recorded). Readers treat absent as `"none"` for legacy memories.
2566
+ */
2567
+ provenance?: "verified" | "unverified" | "none";
2517
2568
  }
2518
2569
 
2519
2570
  /** Memory link relationship types */
@@ -2527,6 +2578,31 @@ export interface MemoryLink {
2527
2578
  reason?: string;
2528
2579
  }
2529
2580
 
2581
+ /**
2582
+ * A verbatim source-utterance excerpt that grounds an extracted fact
2583
+ * (issue #1575). Emitted by the PR 2 extraction validator; round-tripped
2584
+ * through storage in PR 1.
2585
+ *
2586
+ * `quote` is the only strictly-required field — without it the entry is
2587
+ * meaningless and is dropped on read. `charStart` / `charEnd` are
2588
+ * best-effort offsets within the buffered turn text at write time and
2589
+ * form a half-open interval [charStart, charEnd) (rule 35).
2590
+ */
2591
+ export interface ProvenanceSource {
2592
+ /** Session key of the source turn (e.g. `project/<name>/<ts>`). */
2593
+ sessionKey: string;
2594
+ /** Host-supplied turn identifier, when one was provided. */
2595
+ turnId?: string;
2596
+ /** ISO 8601 timestamp of the source turn. */
2597
+ observedAt: string;
2598
+ /** Verbatim utterance excerpt, capped at `provenance.maxQuoteChars`. */
2599
+ quote: string;
2600
+ /** Offset within the buffered turn text where the quote begins, when located. */
2601
+ charStart?: number;
2602
+ /** Half-open end offset within the buffered turn text (rule 35). */
2603
+ charEnd?: number;
2604
+ }
2605
+
2530
2606
  // Conversation Threading (Phase 3B)
2531
2607
  export interface ConversationThread {
2532
2608
  id: string;