@fgv/ts-agent-memory 5.1.0-36 → 5.1.0-39

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 (197) hide show
  1. package/.rush/temp/{f6a88bfdd66517ccb98c2c7ae1be6e6fe9e15d38.tar.log → b82cf6bdece20481260e6bab946179eeec9d7b46.tar.log} +116 -2
  2. package/.rush/temp/chunked-rush-logs/ts-agent-memory.build.chunks.jsonl +4 -4
  3. package/.rush/temp/operation/build/all.log +4 -4
  4. package/.rush/temp/operation/build/log-chunks.jsonl +4 -4
  5. package/.rush/temp/operation/build/state.json +1 -1
  6. package/.rush/temp/shrinkwrap-deps.json +222 -221
  7. package/config/jest.config.json +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/packlets/ingest/cycleGuard.js +111 -0
  11. package/dist/packlets/ingest/cycleGuard.js.map +1 -0
  12. package/dist/packlets/ingest/hostStages.js +6 -0
  13. package/dist/packlets/ingest/hostStages.js.map +1 -0
  14. package/dist/packlets/ingest/index.js +9 -0
  15. package/dist/packlets/ingest/index.js.map +1 -0
  16. package/dist/packlets/ingest/model.js +6 -0
  17. package/dist/packlets/ingest/model.js.map +1 -0
  18. package/dist/packlets/ingest/orchestrator.js +438 -0
  19. package/dist/packlets/ingest/orchestrator.js.map +1 -0
  20. package/dist/packlets/retrieve/index.js +1 -0
  21. package/dist/packlets/retrieve/index.js.map +1 -1
  22. package/dist/packlets/retrieve/temporalRetrievers.js +172 -0
  23. package/dist/packlets/retrieve/temporalRetrievers.js.map +1 -0
  24. package/dist/packlets/store/fileTreeMemoryStore.js +323 -41
  25. package/dist/packlets/store/fileTreeMemoryStore.js.map +1 -1
  26. package/dist/packlets/tools/index.js +6 -0
  27. package/dist/packlets/tools/index.js.map +1 -0
  28. package/dist/packlets/tools/memoryTools.js +337 -0
  29. package/dist/packlets/tools/memoryTools.js.map +1 -0
  30. package/dist/packlets/types/identityCodec.js +115 -0
  31. package/dist/packlets/types/identityCodec.js.map +1 -1
  32. package/dist/packlets/types/index.js +1 -0
  33. package/dist/packlets/types/index.js.map +1 -1
  34. package/dist/packlets/types/temporal.js +85 -0
  35. package/dist/packlets/types/temporal.js.map +1 -0
  36. package/dist/packlets/types/writePolicy.js +96 -0
  37. package/dist/packlets/types/writePolicy.js.map +1 -1
  38. package/dist/test/unit/converters/antagonistRoundTrip.test.js +95 -0
  39. package/dist/test/unit/converters/antagonistRoundTrip.test.js.map +1 -0
  40. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js +273 -0
  41. package/dist/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -0
  42. package/dist/test/unit/ingest/cycleGuard.test.js +54 -0
  43. package/dist/test/unit/ingest/cycleGuard.test.js.map +1 -0
  44. package/dist/test/unit/ingest/orchestrator.test.js +913 -0
  45. package/dist/test/unit/ingest/orchestrator.test.js.map +1 -0
  46. package/dist/test/unit/retrieve/temporalRetrievers.test.js +182 -0
  47. package/dist/test/unit/retrieve/temporalRetrievers.test.js.map +1 -0
  48. package/dist/test/unit/store/antagonistTemporalBoundary.test.js +120 -0
  49. package/dist/test/unit/store/antagonistTemporalBoundary.test.js.map +1 -0
  50. package/dist/test/unit/store/fileTreeMemoryStore.test.js +74 -7
  51. package/dist/test/unit/store/fileTreeMemoryStore.test.js.map +1 -1
  52. package/dist/test/unit/store/temporalStore.test.js +398 -0
  53. package/dist/test/unit/store/temporalStore.test.js.map +1 -0
  54. package/dist/test/unit/tools/memoryTools.test.js +572 -0
  55. package/dist/test/unit/tools/memoryTools.test.js.map +1 -0
  56. package/dist/test/unit/types/temporalCodec.test.js +203 -0
  57. package/dist/test/unit/types/temporalCodec.test.js.map +1 -0
  58. package/dist/test/unit/types/temporalPolicy.test.js +62 -0
  59. package/dist/test/unit/types/temporalPolicy.test.js.map +1 -0
  60. package/dist/ts-agent-memory.d.ts +1042 -10
  61. package/dist/tsdoc-metadata.json +1 -1
  62. package/etc/ts-agent-memory.api.md +284 -0
  63. package/lib/index.d.ts +2 -0
  64. package/lib/index.d.ts.map +1 -1
  65. package/lib/index.js +2 -0
  66. package/lib/index.js.map +1 -1
  67. package/lib/packlets/ingest/cycleGuard.d.ts +41 -0
  68. package/lib/packlets/ingest/cycleGuard.d.ts.map +1 -0
  69. package/lib/packlets/ingest/cycleGuard.js +115 -0
  70. package/lib/packlets/ingest/cycleGuard.js.map +1 -0
  71. package/lib/packlets/ingest/hostStages.d.ts +88 -0
  72. package/lib/packlets/ingest/hostStages.d.ts.map +1 -0
  73. package/lib/packlets/ingest/hostStages.js +7 -0
  74. package/lib/packlets/ingest/hostStages.js.map +1 -0
  75. package/lib/packlets/ingest/index.d.ts +5 -0
  76. package/lib/packlets/ingest/index.d.ts.map +1 -0
  77. package/lib/packlets/ingest/index.js +25 -0
  78. package/lib/packlets/ingest/index.js.map +1 -0
  79. package/lib/packlets/ingest/model.d.ts +177 -0
  80. package/lib/packlets/ingest/model.d.ts.map +1 -0
  81. package/lib/packlets/ingest/model.js +7 -0
  82. package/lib/packlets/ingest/model.js.map +1 -0
  83. package/lib/packlets/ingest/orchestrator.d.ts +206 -0
  84. package/lib/packlets/ingest/orchestrator.d.ts.map +1 -0
  85. package/lib/packlets/ingest/orchestrator.js +442 -0
  86. package/lib/packlets/ingest/orchestrator.js.map +1 -0
  87. package/lib/packlets/retrieve/index.d.ts +1 -0
  88. package/lib/packlets/retrieve/index.d.ts.map +1 -1
  89. package/lib/packlets/retrieve/index.js +1 -0
  90. package/lib/packlets/retrieve/index.js.map +1 -1
  91. package/lib/packlets/retrieve/temporalRetrievers.d.ts +78 -0
  92. package/lib/packlets/retrieve/temporalRetrievers.d.ts.map +1 -0
  93. package/lib/packlets/retrieve/temporalRetrievers.js +178 -0
  94. package/lib/packlets/retrieve/temporalRetrievers.js.map +1 -0
  95. package/lib/packlets/store/fileTreeMemoryStore.d.ts +118 -9
  96. package/lib/packlets/store/fileTreeMemoryStore.d.ts.map +1 -1
  97. package/lib/packlets/store/fileTreeMemoryStore.js +322 -40
  98. package/lib/packlets/store/fileTreeMemoryStore.js.map +1 -1
  99. package/lib/packlets/tools/index.d.ts +2 -0
  100. package/lib/packlets/tools/index.d.ts.map +1 -0
  101. package/lib/packlets/tools/index.js +22 -0
  102. package/lib/packlets/tools/index.js.map +1 -0
  103. package/lib/packlets/tools/memoryTools.d.ts +139 -0
  104. package/lib/packlets/tools/memoryTools.d.ts.map +1 -0
  105. package/lib/packlets/tools/memoryTools.js +341 -0
  106. package/lib/packlets/tools/memoryTools.js.map +1 -0
  107. package/lib/packlets/types/identityCodec.d.ts +86 -0
  108. package/lib/packlets/types/identityCodec.d.ts.map +1 -1
  109. package/lib/packlets/types/identityCodec.js +118 -1
  110. package/lib/packlets/types/identityCodec.js.map +1 -1
  111. package/lib/packlets/types/index.d.ts +1 -0
  112. package/lib/packlets/types/index.d.ts.map +1 -1
  113. package/lib/packlets/types/index.js +1 -0
  114. package/lib/packlets/types/index.js.map +1 -1
  115. package/lib/packlets/types/temporal.d.ts +40 -0
  116. package/lib/packlets/types/temporal.d.ts.map +1 -0
  117. package/lib/packlets/types/temporal.js +92 -0
  118. package/lib/packlets/types/temporal.js.map +1 -0
  119. package/lib/packlets/types/writePolicy.d.ts +49 -0
  120. package/lib/packlets/types/writePolicy.d.ts.map +1 -1
  121. package/lib/packlets/types/writePolicy.js +98 -1
  122. package/lib/packlets/types/writePolicy.js.map +1 -1
  123. package/lib/test/unit/converters/antagonistRoundTrip.test.d.ts +10 -0
  124. package/lib/test/unit/converters/antagonistRoundTrip.test.d.ts.map +1 -0
  125. package/lib/test/unit/converters/antagonistRoundTrip.test.js +97 -0
  126. package/lib/test/unit/converters/antagonistRoundTrip.test.js.map +1 -0
  127. package/lib/test/unit/ingest/antagonistCycleAndParity.test.d.ts +9 -0
  128. package/lib/test/unit/ingest/antagonistCycleAndParity.test.d.ts.map +1 -0
  129. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js +275 -0
  130. package/lib/test/unit/ingest/antagonistCycleAndParity.test.js.map +1 -0
  131. package/lib/test/unit/ingest/cycleGuard.test.d.ts +2 -0
  132. package/lib/test/unit/ingest/cycleGuard.test.d.ts.map +1 -0
  133. package/lib/test/unit/ingest/cycleGuard.test.js +56 -0
  134. package/lib/test/unit/ingest/cycleGuard.test.js.map +1 -0
  135. package/lib/test/unit/ingest/orchestrator.test.d.ts +2 -0
  136. package/lib/test/unit/ingest/orchestrator.test.d.ts.map +1 -0
  137. package/lib/test/unit/ingest/orchestrator.test.js +915 -0
  138. package/lib/test/unit/ingest/orchestrator.test.js.map +1 -0
  139. package/lib/test/unit/retrieve/temporalRetrievers.test.d.ts +2 -0
  140. package/lib/test/unit/retrieve/temporalRetrievers.test.d.ts.map +1 -0
  141. package/lib/test/unit/retrieve/temporalRetrievers.test.js +184 -0
  142. package/lib/test/unit/retrieve/temporalRetrievers.test.js.map +1 -0
  143. package/lib/test/unit/store/antagonistTemporalBoundary.test.d.ts +9 -0
  144. package/lib/test/unit/store/antagonistTemporalBoundary.test.d.ts.map +1 -0
  145. package/lib/test/unit/store/antagonistTemporalBoundary.test.js +122 -0
  146. package/lib/test/unit/store/antagonistTemporalBoundary.test.js.map +1 -0
  147. package/lib/test/unit/store/fileTreeMemoryStore.test.js +74 -7
  148. package/lib/test/unit/store/fileTreeMemoryStore.test.js.map +1 -1
  149. package/lib/test/unit/store/temporalStore.test.d.ts +2 -0
  150. package/lib/test/unit/store/temporalStore.test.d.ts.map +1 -0
  151. package/lib/test/unit/store/temporalStore.test.js +400 -0
  152. package/lib/test/unit/store/temporalStore.test.js.map +1 -0
  153. package/lib/test/unit/tools/memoryTools.test.d.ts +2 -0
  154. package/lib/test/unit/tools/memoryTools.test.d.ts.map +1 -0
  155. package/lib/test/unit/tools/memoryTools.test.js +574 -0
  156. package/lib/test/unit/tools/memoryTools.test.js.map +1 -0
  157. package/lib/test/unit/types/temporalCodec.test.d.ts +2 -0
  158. package/lib/test/unit/types/temporalCodec.test.d.ts.map +1 -0
  159. package/lib/test/unit/types/temporalCodec.test.js +205 -0
  160. package/lib/test/unit/types/temporalCodec.test.js.map +1 -0
  161. package/lib/test/unit/types/temporalPolicy.test.d.ts +2 -0
  162. package/lib/test/unit/types/temporalPolicy.test.d.ts.map +1 -0
  163. package/lib/test/unit/types/temporalPolicy.test.js +64 -0
  164. package/lib/test/unit/types/temporalPolicy.test.js.map +1 -0
  165. package/package.json +19 -19
  166. package/rush-logs/ts-agent-memory.build.cache.log +1 -1
  167. package/rush-logs/ts-agent-memory.build.log +4 -4
  168. package/src/index.ts +2 -0
  169. package/src/packlets/ingest/cycleGuard.ts +142 -0
  170. package/src/packlets/ingest/hostStages.ts +111 -0
  171. package/src/packlets/ingest/index.ts +9 -0
  172. package/src/packlets/ingest/model.ts +184 -0
  173. package/src/packlets/ingest/orchestrator.ts +797 -0
  174. package/src/packlets/retrieve/index.ts +1 -0
  175. package/src/packlets/retrieve/temporalRetrievers.ts +210 -0
  176. package/src/packlets/store/fileTreeMemoryStore.ts +460 -66
  177. package/src/packlets/tools/index.ts +6 -0
  178. package/src/packlets/tools/memoryTools.ts +579 -0
  179. package/src/packlets/types/identityCodec.ts +184 -0
  180. package/src/packlets/types/index.ts +1 -0
  181. package/src/packlets/types/temporal.ts +96 -0
  182. package/src/packlets/types/writePolicy.ts +127 -0
  183. package/src/test/unit/converters/antagonistRoundTrip.test.ts +110 -0
  184. package/src/test/unit/ingest/antagonistCycleAndParity.test.ts +362 -0
  185. package/src/test/unit/ingest/cycleGuard.test.ts +68 -0
  186. package/src/test/unit/ingest/orchestrator.test.ts +1158 -0
  187. package/src/test/unit/retrieve/temporalRetrievers.test.ts +226 -0
  188. package/src/test/unit/store/antagonistTemporalBoundary.test.ts +158 -0
  189. package/src/test/unit/store/fileTreeMemoryStore.test.ts +98 -7
  190. package/src/test/unit/store/temporalStore.test.ts +469 -0
  191. package/src/test/unit/tools/memoryTools.test.ts +771 -0
  192. package/src/test/unit/types/temporalCodec.test.ts +259 -0
  193. package/src/test/unit/types/temporalPolicy.test.ts +96 -0
  194. package/temp/build/lint/_eslint-5eVG3S6w.json +122 -46
  195. package/temp/build/typescript/ts_8nwakTlr.json +1 -1
  196. package/temp/ts-agent-memory.api.json +11984 -6314
  197. package/temp/ts-agent-memory.api.md +284 -0
@@ -1 +1 @@
1
- {"version":3,"file":"identityCodec.js","sourceRoot":"","sources":["../../../src/packlets/types/identityCodec.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { Convert, EntityId, MemoryScopeKey } from './ids';\nimport { assertPortableFilenameStem } from './filenameSafety';\n\n/**\n * The FileTree storage address an {@link IIdentityCodec} maps a domain key to.\n * @public\n */\nexport interface IIdentityCodecResult {\n /** Scope path segment (may be multi-level, e.g. `conversations/<id>`). */\n readonly scope: MemoryScopeKey;\n /** Filename stem (the part before `.md`). Filename-safe after encoding. */\n readonly idStem: string;\n /**\n * Whether this kind uses a versioned layout (temporal: multiple files per\n * entity) vs. a flat layout (one file per entity). Non-temporal = always\n * `false`.\n */\n readonly isVersioned: boolean;\n}\n\n/**\n * Maps a consumer-supplied domain key ⇄ a FileTree storage address. Injected\n * per kind so the store never touches raw domain keys: the codec owns all\n * filename escaping and the flat-vs-versioned layout dispatch.\n * @public\n */\nexport interface IIdentityCodec {\n /**\n * Encode a consumer-supplied entity id to a FileTree address. Deterministic\n * and pure — no I/O.\n */\n encode(entityId: EntityId): Result<IIdentityCodecResult>;\n\n /**\n * Decode a FileTree address back to the original {@link EntityId}. The exact\n * inverse of {@link IIdentityCodec.encode | encode} for non-versioned kinds.\n */\n decode(scope: MemoryScopeKey, encodedStem: string): Result<EntityId>;\n\n /**\n * Assert that `encode(decode(scope, stem)).idStem === stem`. Used by the\n * store's `verifyFilenameId` check on load.\n */\n verifyRoundTrip(scope: MemoryScopeKey, stem: string): Result<true>;\n}\n\n/**\n/**\n * Identity codec for the knowledge kind family. A knowledge entity is keyed\n * by its consumer-supplied `docId`, which is used verbatim as the filename\n * stem under the flat `knowledge` scope.\n *\n * @remarks\n * - `encode`: scope = `knowledge`, idStem = `docId`, `isVersioned = false`.\n * - `decode`: brands the stem back to an {@link EntityId}.\n * - Escaping: the `docId` must match the POSIX portable filename set.\n * - Layout: `vault/knowledge/<docId>.md`.\n * @public\n */\nexport class KnowledgeIdentityCodec implements IIdentityCodec {\n /** The fixed scope for every knowledge entity. */\n public static readonly scope: MemoryScopeKey = 'knowledge' as MemoryScopeKey;\n\n /** {@inheritDoc IIdentityCodec.encode} */\n public encode(entityId: EntityId): Result<IIdentityCodecResult> {\n return assertPortableFilenameStem(entityId).onSuccess((idStem) =>\n succeed({ scope: KnowledgeIdentityCodec.scope, idStem, isVersioned: false })\n );\n }\n\n /** {@inheritDoc IIdentityCodec.decode} */\n public decode(scope: MemoryScopeKey, encodedStem: string): Result<EntityId> {\n if (scope !== KnowledgeIdentityCodec.scope) {\n return fail(\n `knowledge codec: scope '${scope}' does not match expected scope '${KnowledgeIdentityCodec.scope}'`\n );\n }\n return assertPortableFilenameStem(encodedStem).onSuccess((stem) => Convert.entityId.convert(stem));\n }\n\n /** {@inheritDoc IIdentityCodec.verifyRoundTrip} */\n public verifyRoundTrip(scope: MemoryScopeKey, stem: string): Result<true> {\n return this.decode(scope, stem)\n .onSuccess((entityId) => this.encode(entityId))\n .onSuccess((encoded) => {\n /* c8 ignore start -- defensive: for the identity (knowledge) codec, encode(decode(stem)).idStem always equals stem when both succeed; the guard exists for the non-identity codecs (LTM/MTM, Phase C) that reuse this contract */\n if (encoded.idStem !== stem) {\n return fail(\n `knowledge codec: round-trip mismatch for stem '${stem}' (re-encoded to '${encoded.idStem}')`\n );\n }\n /* c8 ignore stop */\n return succeed(true);\n });\n }\n}\n\n/**\n * Identity codec for the long-term-memory (LTM) kind family. An LTM entity is\n * keyed by its `conversationId`, used verbatim as the filename stem under the\n * flat `conversations` scope.\n *\n * @remarks\n * - `encode`: scope = `conversations`, idStem = `conversationId`, `isVersioned = false`.\n * - `decode`: brands the stem back to an {@link EntityId}.\n * - Escaping: the `conversationId` must match the POSIX portable filename set.\n * - Layout: `vault/conversations/<conversationId>.md`.\n *\n * An LTM file (`conversations/<id>.md`) and the MTM subtree\n * (`conversations/<id>/turn-N.md`) coexist — a file and a same-named directory\n * are independent on every supported FileTree backend.\n * @public\n */\nexport class LtmIdentityCodec implements IIdentityCodec {\n /** The fixed scope for every LTM entity. */\n public static readonly scope: MemoryScopeKey = 'conversations' as MemoryScopeKey;\n\n /** {@inheritDoc IIdentityCodec.encode} */\n public encode(entityId: EntityId): Result<IIdentityCodecResult> {\n return assertPortableFilenameStem(entityId).onSuccess((idStem) =>\n succeed({ scope: LtmIdentityCodec.scope, idStem, isVersioned: false })\n );\n }\n\n /** {@inheritDoc IIdentityCodec.decode} */\n public decode(scope: MemoryScopeKey, encodedStem: string): Result<EntityId> {\n if (scope !== LtmIdentityCodec.scope) {\n return fail(`LTM codec: scope '${scope}' does not match expected scope '${LtmIdentityCodec.scope}'`);\n }\n return assertPortableFilenameStem(encodedStem).onSuccess((stem) => Convert.entityId.convert(stem));\n }\n\n /** {@inheritDoc IIdentityCodec.verifyRoundTrip} */\n public verifyRoundTrip(scope: MemoryScopeKey, stem: string): Result<true> {\n return this.decode(scope, stem)\n .onSuccess((entityId) => this.encode(entityId))\n .onSuccess((encoded) => {\n /* c8 ignore start -- defensive: LTM encode(decode(stem)).idStem always equals stem when both succeed (identity mapping); guard preserves the contract for direct callers */\n if (encoded.idStem !== stem) {\n return fail(\n `LTM codec: round-trip mismatch for stem '${stem}' (re-encoded to '${encoded.idStem}')`\n );\n }\n /* c8 ignore stop */\n return succeed(true);\n });\n }\n}\n\n/**\n * Identity codec for the medium-term-memory (MTM) kind family. An MTM entity is\n * keyed by the colon-composite `<conversationId>:<turnIndex>` and maps into a\n * per-conversation subtree.\n *\n * @remarks\n * - `encode`: splits the entity id on `:`; scope =\n * `conversations/<conversationId>` (multi-segment), idStem = `turn-<turnIndex>`,\n * `isVersioned = false`.\n * - `decode`: reverses scope `conversations/<id>` + stem `turn-<N>` to the\n * composite `<id>:<N>`.\n * - Escaping: `conversationId` must match the POSIX portable filename set (so it\n * contains no `/` or `:`); `turnIndex` must be a non-negative integer string,\n * preserved verbatim so the round-trip is exact.\n * - Layout: `vault/conversations/<conversationId>/turn-<N>.md`. The `/` in the\n * scope is handled by the store's multi-segment scope resolver.\n *\n * **Verbatim turn index (caller-canonicalization note).** The turn index is\n * preserved exactly — `conv-1:7` and `conv-1:007` are DISTINCT entities mapping\n * to distinct files (`turn-7.md` / `turn-007.md`). Verbatim preservation is what\n * makes the round-trip exact, but it means a caller that formats turn indices\n * inconsistently (some zero-padded, some not) will silently mint separate\n * entities. Callers should canonicalize to one form (e.g. no leading zeros).\n * @public\n */\nexport class MtmIdentityCodec implements IIdentityCodec {\n /** The top-level scope segment under which every MTM subtree lives. */\n public static readonly rootScopeSegment: string = 'conversations';\n /** The fixed filename-stem prefix for a turn record. */\n public static readonly turnStemPrefix: string = 'turn-';\n\n /** A non-negative integer string (the turn index), preserved verbatim. */\n private static readonly _turnIndexRe: RegExp = /^\\d+$/;\n\n /** {@inheritDoc IIdentityCodec.encode} */\n public encode(entityId: EntityId): Result<IIdentityCodecResult> {\n const parts: string[] = entityId.split(':');\n if (parts.length !== 2) {\n return fail(`MTM codec: entity id '${entityId}' must be a '<conversationId>:<turnIndex>' composite`);\n }\n const [conversationId, turnIndex] = parts;\n return assertPortableFilenameStem(conversationId)\n .withErrorFormat((msg) => `MTM codec: conversationId '${conversationId}': ${msg}`)\n .onSuccess(() => MtmIdentityCodec._assertTurnIndex(turnIndex))\n .onSuccess(() =>\n succeed({\n scope: `${MtmIdentityCodec.rootScopeSegment}/${conversationId}` as MemoryScopeKey,\n idStem: `${MtmIdentityCodec.turnStemPrefix}${turnIndex}`,\n isVersioned: false\n })\n );\n }\n\n /** {@inheritDoc IIdentityCodec.decode} */\n public decode(scope: MemoryScopeKey, encodedStem: string): Result<EntityId> {\n return MtmIdentityCodec._conversationIdFromScope(scope).onSuccess((conversationId) =>\n MtmIdentityCodec._turnIndexFromStem(encodedStem).onSuccess((turnIndex) =>\n Convert.entityId.convert(`${conversationId}:${turnIndex}`)\n )\n );\n }\n\n /** {@inheritDoc IIdentityCodec.verifyRoundTrip} */\n public verifyRoundTrip(scope: MemoryScopeKey, stem: string): Result<true> {\n return this.decode(scope, stem)\n .onSuccess((entityId) => this.encode(entityId))\n .onSuccess((encoded) => {\n /* c8 ignore start -- defensive: MTM encode(decode(scope, stem)) reproduces the same scope+stem whenever both succeed (no normalization); guard preserves the contract for direct callers */\n if (encoded.idStem !== stem || encoded.scope !== scope) {\n return fail(\n `MTM codec: round-trip mismatch for scope '${scope}' stem '${stem}' (re-encoded to scope '${encoded.scope}' stem '${encoded.idStem}')`\n );\n }\n /* c8 ignore stop */\n return succeed(true);\n });\n }\n\n /** Validate and extract the `conversationId` from a `conversations/<id>` scope. */\n private static _conversationIdFromScope(scope: MemoryScopeKey): Result<string> {\n const segments: string[] = scope.split('/');\n if (segments.length !== 2 || segments[0] !== MtmIdentityCodec.rootScopeSegment) {\n return fail(\n `MTM codec: scope '${scope}' must be '${MtmIdentityCodec.rootScopeSegment}/<conversationId>'`\n );\n }\n const conversationId: string = segments[1];\n return assertPortableFilenameStem(conversationId)\n .withErrorFormat((msg) => `MTM codec: conversationId '${conversationId}': ${msg}`)\n .onSuccess(() => succeed(conversationId));\n }\n\n /** Validate and extract the turn-index digits from a `turn-<N>` stem. */\n private static _turnIndexFromStem(stem: string): Result<string> {\n if (!stem.startsWith(MtmIdentityCodec.turnStemPrefix)) {\n return fail(`MTM codec: stem '${stem}' must begin with '${MtmIdentityCodec.turnStemPrefix}'`);\n }\n return MtmIdentityCodec._assertTurnIndex(stem.slice(MtmIdentityCodec.turnStemPrefix.length));\n }\n\n /** Assert a string is a non-negative integer (the turn index). */\n private static _assertTurnIndex(turnIndex: string): Result<string> {\n if (!MtmIdentityCodec._turnIndexRe.test(turnIndex)) {\n return fail(`MTM codec: turnIndex '${turnIndex}' must be a non-negative integer`);\n }\n return succeed(turnIndex);\n }\n}\n"]}
1
+ {"version":3,"file":"identityCodec.js","sourceRoot":"","sources":["../../../src/packlets/types/identityCodec.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { Convert, EntityId, MemoryScopeKey } from './ids';\nimport { assertPortableFilenameStem } from './filenameSafety';\n\n/**\n * The FileTree storage address an {@link IIdentityCodec} maps a domain key to.\n * @public\n */\nexport interface IIdentityCodecResult {\n /** Scope path segment (may be multi-level, e.g. `conversations/<id>`). */\n readonly scope: MemoryScopeKey;\n /** Filename stem (the part before `.md`). Filename-safe after encoding. */\n readonly idStem: string;\n /**\n * Whether this kind uses a versioned layout (temporal: multiple files per\n * entity) vs. a flat layout (one file per entity). Non-temporal = always\n * `false`.\n */\n readonly isVersioned: boolean;\n}\n\n/**\n * Maps a consumer-supplied domain key ⇄ a FileTree storage address. Injected\n * per kind so the store never touches raw domain keys: the codec owns all\n * filename escaping and the flat-vs-versioned layout dispatch.\n * @public\n */\nexport interface IIdentityCodec {\n /**\n * Encode a consumer-supplied entity id to a FileTree address. Deterministic\n * and pure — no I/O.\n */\n encode(entityId: EntityId): Result<IIdentityCodecResult>;\n\n /**\n * Decode a FileTree address back to the original {@link EntityId}. The exact\n * inverse of {@link IIdentityCodec.encode | encode} for non-versioned kinds.\n */\n decode(scope: MemoryScopeKey, encodedStem: string): Result<EntityId>;\n\n /**\n * Assert that `encode(decode(scope, stem)).idStem === stem`. Used by the\n * store's `verifyFilenameId` check on load.\n */\n verifyRoundTrip(scope: MemoryScopeKey, stem: string): Result<true>;\n}\n\n/**\n * The `(entityId, version seq)` an {@link ITemporalIdentityCodec.decodeVersion}\n * recovers from a version file address.\n * @public\n */\nexport interface ITemporalVersionAddress {\n /** The stable consumer-supplied domain key. */\n readonly entityId: EntityId;\n /** The version's monotonic `seq` (the `v<seq>` component of the file stem). */\n readonly seq: number;\n}\n\n/**\n * Additive extension of {@link IIdentityCodec} for versioned (temporal) kinds. A\n * codec whose {@link IIdentityCodec.encode | encode} reports `isVersioned: true`\n * implements this so the store can form and parse per-version filenames without\n * knowing the layout. Non-versioned codecs do NOT implement it (probe with\n * {@link isTemporalIdentityCodec}).\n * @public\n */\nexport interface ITemporalIdentityCodec extends IIdentityCodec {\n /**\n * Form the version filename stem for a specific `(entityId, seq)`. The store\n * appends the extension and writes it under the entity subtree returned by\n * {@link IIdentityCodec.encode | encode}.\n */\n encodeVersion(entityId: EntityId, seq: number): Result<string>;\n\n /**\n * Parse a `(subtree scope, version stem)` back to its\n * {@link ITemporalVersionAddress}. The subtree scope is authoritative for the\n * `entityId`, disambiguating a stem whose `entityId` itself ends in\n * `-v<digits>`.\n */\n decodeVersion(scope: MemoryScopeKey, stem: string): Result<ITemporalVersionAddress>;\n}\n\n/**\n * Narrow an {@link IIdentityCodec} to {@link ITemporalIdentityCodec} by probing\n * for the versioned methods. Used by the store when an `encode` result reports\n * `isVersioned: true`.\n * @public\n */\nexport function isTemporalIdentityCodec(codec: IIdentityCodec): codec is ITemporalIdentityCodec {\n const candidate: Partial<ITemporalIdentityCodec> = codec as Partial<ITemporalIdentityCodec>;\n return typeof candidate.encodeVersion === 'function' && typeof candidate.decodeVersion === 'function';\n}\n\n/**\n * Identity codec for a versioned (temporal) kind family, resolving OQ-11 to the\n * subtree-per-entity layout: every version of an entity is a distinct file under\n * a per-entity subtree.\n *\n * @remarks\n * - `encode`: scope = `<baseScope>/entities/<entityId>`, idStem = `<entityId>`,\n * `isVersioned = true`. The idStem is the stable entity prefix; the store forms\n * each version's filename via {@link TemporalIdentityCodec.encodeVersion}.\n * - `encodeVersion`: version stem = `<entityId>-v<seq>`.\n * - `decode` / `decodeVersion`: recover `entityId` (and `seq`) from a\n * `(subtree scope, version stem)` pair; the scope is authoritative for the\n * `entityId`.\n * - Escaping: `baseScope` and `entityId` must each match the POSIX portable\n * filename set (they become path segments); `seq` is a non-negative integer.\n * - Layout: `vault/<baseScope>/entities/<entityId>/<entityId>-v<seq>.md`.\n * @public\n */\nexport class TemporalIdentityCodec implements ITemporalIdentityCodec {\n /** The fixed subtree segment separating an entity's versions from its scope. */\n public static readonly entitiesSegment: string = 'entities';\n /** The version-stem infix: `<entityId>` + this + `<seq>`. */\n public static readonly versionInfix: string = '-v';\n\n /** A non-negative integer string (the version seq). */\n private static readonly _versionSeqRe: RegExp = /^\\d+$/;\n\n /** The base scope segment this codec's entities live under. */\n public readonly baseScope: string;\n\n private constructor(baseScope: string) {\n this.baseScope = baseScope;\n }\n\n /**\n * Family-convention factory. Validates that `baseScope` is a single portable\n * filename segment (it becomes the top-level path component).\n */\n public static create(baseScope: string): Result<TemporalIdentityCodec> {\n return assertPortableFilenameStem(baseScope)\n .withErrorFormat((msg) => `temporal codec: baseScope '${baseScope}': ${msg}`)\n .onSuccess(() => succeed(new TemporalIdentityCodec(baseScope)));\n }\n\n /** {@inheritDoc IIdentityCodec.encode} */\n public encode(entityId: EntityId): Result<IIdentityCodecResult> {\n return assertPortableFilenameStem(entityId)\n .withErrorFormat((msg) => `temporal codec: entityId '${entityId}': ${msg}`)\n .onSuccess((stem) =>\n succeed({\n scope: `${this.baseScope}/${TemporalIdentityCodec.entitiesSegment}/${stem}` as MemoryScopeKey,\n idStem: stem,\n isVersioned: true\n })\n );\n }\n\n /** {@inheritDoc ITemporalIdentityCodec.encodeVersion} */\n public encodeVersion(entityId: EntityId, seq: number): Result<string> {\n return assertPortableFilenameStem(entityId)\n .withErrorFormat((msg) => `temporal codec: entityId '${entityId}': ${msg}`)\n .onSuccess((stem) => {\n if (!Number.isInteger(seq) || seq < 0) {\n return fail(`temporal codec: version seq '${seq}' must be a non-negative integer`);\n }\n return succeed(`${stem}${TemporalIdentityCodec.versionInfix}${seq}`);\n });\n }\n\n /** {@inheritDoc IIdentityCodec.decode} */\n public decode(scope: MemoryScopeKey, encodedStem: string): Result<EntityId> {\n return this.decodeVersion(scope, encodedStem).onSuccess((addr) =>\n Convert.entityId.convert(addr.entityId)\n );\n }\n\n /** {@inheritDoc ITemporalIdentityCodec.decodeVersion} */\n public decodeVersion(scope: MemoryScopeKey, stem: string): Result<ITemporalVersionAddress> {\n return this._entityIdFromScope(scope).onSuccess((entityId) => {\n const prefix: string = `${entityId}${TemporalIdentityCodec.versionInfix}`;\n if (!stem.startsWith(prefix)) {\n return fail(\n `temporal codec: version stem '${stem}' must begin with '${prefix}' (from scope '${scope}')`\n );\n }\n const seqText: string = stem.slice(prefix.length);\n if (!TemporalIdentityCodec._versionSeqRe.test(seqText)) {\n return fail(`temporal codec: version stem '${stem}' has a non-integer version suffix '${seqText}'`);\n }\n // The regex admits digit strings of unbounded length; `parseInt` would silently\n // lose precision past MAX_SAFE_INTEGER, decoding a corrupt/tampered filename to a\n // plausible-but-wrong `seq` that drives version ordering. Reject rather than corrupt.\n const seq: number = Number.parseInt(seqText, 10);\n if (!Number.isSafeInteger(seq)) {\n return fail(\n `temporal codec: version stem '${stem}' has a version suffix '${seqText}' outside the safe integer range`\n );\n }\n return Convert.entityId.convert(entityId).onSuccess((branded) => succeed({ entityId: branded, seq }));\n });\n }\n\n /** {@inheritDoc IIdentityCodec.verifyRoundTrip} */\n public verifyRoundTrip(scope: MemoryScopeKey, stem: string): Result<true> {\n return this.decodeVersion(scope, stem).onSuccess((addr) =>\n this.encode(addr.entityId).onSuccess((encoded) =>\n this.encodeVersion(addr.entityId, addr.seq).onSuccess((reStem) => {\n if (encoded.scope !== scope || reStem !== stem) {\n return fail(\n `temporal codec: round-trip mismatch for scope '${scope}' stem '${stem}' (re-encoded to scope '${encoded.scope}' stem '${reStem}')`\n );\n }\n return succeed(true);\n })\n )\n );\n }\n\n /** Validate and extract the `entityId` from a `<baseScope>/entities/<entityId>` scope. */\n private _entityIdFromScope(scope: MemoryScopeKey): Result<string> {\n const segments: string[] = scope.split('/');\n if (\n segments.length !== 3 ||\n segments[0] !== this.baseScope ||\n segments[1] !== TemporalIdentityCodec.entitiesSegment\n ) {\n return fail(\n `temporal codec: scope '${scope}' must be '${this.baseScope}/${TemporalIdentityCodec.entitiesSegment}/<entityId>'`\n );\n }\n const entityId: string = segments[2];\n return assertPortableFilenameStem(entityId)\n .withErrorFormat((msg) => `temporal codec: entityId '${entityId}': ${msg}`)\n .onSuccess(() => succeed(entityId));\n }\n}\n\n/**\n * Identity codec for the knowledge kind family. A knowledge entity is keyed\n * by its consumer-supplied `docId`, which is used verbatim as the filename\n * stem under the flat `knowledge` scope.\n *\n * @remarks\n * - `encode`: scope = `knowledge`, idStem = `docId`, `isVersioned = false`.\n * - `decode`: brands the stem back to an {@link EntityId}.\n * - Escaping: the `docId` must match the POSIX portable filename set.\n * - Layout: `vault/knowledge/<docId>.md`.\n * @public\n */\nexport class KnowledgeIdentityCodec implements IIdentityCodec {\n /** The fixed scope for every knowledge entity. */\n public static readonly scope: MemoryScopeKey = 'knowledge' as MemoryScopeKey;\n\n /** {@inheritDoc IIdentityCodec.encode} */\n public encode(entityId: EntityId): Result<IIdentityCodecResult> {\n return assertPortableFilenameStem(entityId).onSuccess((idStem) =>\n succeed({ scope: KnowledgeIdentityCodec.scope, idStem, isVersioned: false })\n );\n }\n\n /** {@inheritDoc IIdentityCodec.decode} */\n public decode(scope: MemoryScopeKey, encodedStem: string): Result<EntityId> {\n if (scope !== KnowledgeIdentityCodec.scope) {\n return fail(\n `knowledge codec: scope '${scope}' does not match expected scope '${KnowledgeIdentityCodec.scope}'`\n );\n }\n return assertPortableFilenameStem(encodedStem).onSuccess((stem) => Convert.entityId.convert(stem));\n }\n\n /** {@inheritDoc IIdentityCodec.verifyRoundTrip} */\n public verifyRoundTrip(scope: MemoryScopeKey, stem: string): Result<true> {\n return this.decode(scope, stem)\n .onSuccess((entityId) => this.encode(entityId))\n .onSuccess((encoded) => {\n /* c8 ignore start -- defensive: for the identity (knowledge) codec, encode(decode(stem)).idStem always equals stem when both succeed; the guard exists for the non-identity codecs (LTM/MTM, Phase C) that reuse this contract */\n if (encoded.idStem !== stem) {\n return fail(\n `knowledge codec: round-trip mismatch for stem '${stem}' (re-encoded to '${encoded.idStem}')`\n );\n }\n /* c8 ignore stop */\n return succeed(true);\n });\n }\n}\n\n/**\n * Identity codec for the long-term-memory (LTM) kind family. An LTM entity is\n * keyed by its `conversationId`, used verbatim as the filename stem under the\n * flat `conversations` scope.\n *\n * @remarks\n * - `encode`: scope = `conversations`, idStem = `conversationId`, `isVersioned = false`.\n * - `decode`: brands the stem back to an {@link EntityId}.\n * - Escaping: the `conversationId` must match the POSIX portable filename set.\n * - Layout: `vault/conversations/<conversationId>.md`.\n *\n * An LTM file (`conversations/<id>.md`) and the MTM subtree\n * (`conversations/<id>/turn-N.md`) coexist — a file and a same-named directory\n * are independent on every supported FileTree backend.\n * @public\n */\nexport class LtmIdentityCodec implements IIdentityCodec {\n /** The fixed scope for every LTM entity. */\n public static readonly scope: MemoryScopeKey = 'conversations' as MemoryScopeKey;\n\n /** {@inheritDoc IIdentityCodec.encode} */\n public encode(entityId: EntityId): Result<IIdentityCodecResult> {\n return assertPortableFilenameStem(entityId).onSuccess((idStem) =>\n succeed({ scope: LtmIdentityCodec.scope, idStem, isVersioned: false })\n );\n }\n\n /** {@inheritDoc IIdentityCodec.decode} */\n public decode(scope: MemoryScopeKey, encodedStem: string): Result<EntityId> {\n if (scope !== LtmIdentityCodec.scope) {\n return fail(`LTM codec: scope '${scope}' does not match expected scope '${LtmIdentityCodec.scope}'`);\n }\n return assertPortableFilenameStem(encodedStem).onSuccess((stem) => Convert.entityId.convert(stem));\n }\n\n /** {@inheritDoc IIdentityCodec.verifyRoundTrip} */\n public verifyRoundTrip(scope: MemoryScopeKey, stem: string): Result<true> {\n return this.decode(scope, stem)\n .onSuccess((entityId) => this.encode(entityId))\n .onSuccess((encoded) => {\n /* c8 ignore start -- defensive: LTM encode(decode(stem)).idStem always equals stem when both succeed (identity mapping); guard preserves the contract for direct callers */\n if (encoded.idStem !== stem) {\n return fail(\n `LTM codec: round-trip mismatch for stem '${stem}' (re-encoded to '${encoded.idStem}')`\n );\n }\n /* c8 ignore stop */\n return succeed(true);\n });\n }\n}\n\n/**\n * Identity codec for the medium-term-memory (MTM) kind family. An MTM entity is\n * keyed by the colon-composite `<conversationId>:<turnIndex>` and maps into a\n * per-conversation subtree.\n *\n * @remarks\n * - `encode`: splits the entity id on `:`; scope =\n * `conversations/<conversationId>` (multi-segment), idStem = `turn-<turnIndex>`,\n * `isVersioned = false`.\n * - `decode`: reverses scope `conversations/<id>` + stem `turn-<N>` to the\n * composite `<id>:<N>`.\n * - Escaping: `conversationId` must match the POSIX portable filename set (so it\n * contains no `/` or `:`); `turnIndex` must be a non-negative integer string,\n * preserved verbatim so the round-trip is exact.\n * - Layout: `vault/conversations/<conversationId>/turn-<N>.md`. The `/` in the\n * scope is handled by the store's multi-segment scope resolver.\n *\n * **Verbatim turn index (caller-canonicalization note).** The turn index is\n * preserved exactly — `conv-1:7` and `conv-1:007` are DISTINCT entities mapping\n * to distinct files (`turn-7.md` / `turn-007.md`). Verbatim preservation is what\n * makes the round-trip exact, but it means a caller that formats turn indices\n * inconsistently (some zero-padded, some not) will silently mint separate\n * entities. Callers should canonicalize to one form (e.g. no leading zeros).\n * @public\n */\nexport class MtmIdentityCodec implements IIdentityCodec {\n /** The top-level scope segment under which every MTM subtree lives. */\n public static readonly rootScopeSegment: string = 'conversations';\n /** The fixed filename-stem prefix for a turn record. */\n public static readonly turnStemPrefix: string = 'turn-';\n\n /** A non-negative integer string (the turn index), preserved verbatim. */\n private static readonly _turnIndexRe: RegExp = /^\\d+$/;\n\n /** {@inheritDoc IIdentityCodec.encode} */\n public encode(entityId: EntityId): Result<IIdentityCodecResult> {\n const parts: string[] = entityId.split(':');\n if (parts.length !== 2) {\n return fail(`MTM codec: entity id '${entityId}' must be a '<conversationId>:<turnIndex>' composite`);\n }\n const [conversationId, turnIndex] = parts;\n return assertPortableFilenameStem(conversationId)\n .withErrorFormat((msg) => `MTM codec: conversationId '${conversationId}': ${msg}`)\n .onSuccess(() => MtmIdentityCodec._assertTurnIndex(turnIndex))\n .onSuccess(() =>\n succeed({\n scope: `${MtmIdentityCodec.rootScopeSegment}/${conversationId}` as MemoryScopeKey,\n idStem: `${MtmIdentityCodec.turnStemPrefix}${turnIndex}`,\n isVersioned: false\n })\n );\n }\n\n /** {@inheritDoc IIdentityCodec.decode} */\n public decode(scope: MemoryScopeKey, encodedStem: string): Result<EntityId> {\n return MtmIdentityCodec._conversationIdFromScope(scope).onSuccess((conversationId) =>\n MtmIdentityCodec._turnIndexFromStem(encodedStem).onSuccess((turnIndex) =>\n Convert.entityId.convert(`${conversationId}:${turnIndex}`)\n )\n );\n }\n\n /** {@inheritDoc IIdentityCodec.verifyRoundTrip} */\n public verifyRoundTrip(scope: MemoryScopeKey, stem: string): Result<true> {\n return this.decode(scope, stem)\n .onSuccess((entityId) => this.encode(entityId))\n .onSuccess((encoded) => {\n /* c8 ignore start -- defensive: MTM encode(decode(scope, stem)) reproduces the same scope+stem whenever both succeed (no normalization); guard preserves the contract for direct callers */\n if (encoded.idStem !== stem || encoded.scope !== scope) {\n return fail(\n `MTM codec: round-trip mismatch for scope '${scope}' stem '${stem}' (re-encoded to scope '${encoded.scope}' stem '${encoded.idStem}')`\n );\n }\n /* c8 ignore stop */\n return succeed(true);\n });\n }\n\n /** Validate and extract the `conversationId` from a `conversations/<id>` scope. */\n private static _conversationIdFromScope(scope: MemoryScopeKey): Result<string> {\n const segments: string[] = scope.split('/');\n if (segments.length !== 2 || segments[0] !== MtmIdentityCodec.rootScopeSegment) {\n return fail(\n `MTM codec: scope '${scope}' must be '${MtmIdentityCodec.rootScopeSegment}/<conversationId>'`\n );\n }\n const conversationId: string = segments[1];\n return assertPortableFilenameStem(conversationId)\n .withErrorFormat((msg) => `MTM codec: conversationId '${conversationId}': ${msg}`)\n .onSuccess(() => succeed(conversationId));\n }\n\n /** Validate and extract the turn-index digits from a `turn-<N>` stem. */\n private static _turnIndexFromStem(stem: string): Result<string> {\n if (!stem.startsWith(MtmIdentityCodec.turnStemPrefix)) {\n return fail(`MTM codec: stem '${stem}' must begin with '${MtmIdentityCodec.turnStemPrefix}'`);\n }\n return MtmIdentityCodec._assertTurnIndex(stem.slice(MtmIdentityCodec.turnStemPrefix.length));\n }\n\n /** Assert a string is a non-negative integer (the turn index). */\n private static _assertTurnIndex(turnIndex: string): Result<string> {\n if (!MtmIdentityCodec._turnIndexRe.test(turnIndex)) {\n return fail(`MTM codec: turnIndex '${turnIndex}' must be a non-negative integer`);\n }\n return succeed(turnIndex);\n }\n}\n"]}
@@ -6,5 +6,6 @@ export * from './ids';
6
6
  export * from './envelope';
7
7
  export * from './filenameSafety';
8
8
  export * from './identityCodec';
9
+ export * from './temporal';
9
10
  export * from './writePolicy';
10
11
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/types/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,OAAO,CAAC;AACtB,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './ids';\nexport * from './envelope';\nexport * from './filenameSafety';\nexport * from './identityCodec';\nexport * from './writePolicy';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/packlets/types/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,OAAO,CAAC;AACtB,cAAc,YAAY,CAAC;AAC3B,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nexport * from './ids';\nexport * from './envelope';\nexport * from './filenameSafety';\nexport * from './identityCodec';\nexport * from './temporal';\nexport * from './writePolicy';\n"]}
@@ -0,0 +1,85 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ /**
6
+ * Whether a record participates in the versioned (temporal) layout. A temporal
7
+ * record always carries a {@link ITemporalBlock | temporal} block (the store
8
+ * stamps `valid_at` on every versioned write); an atemporal record never does,
9
+ * so presence of `temporal` is the discriminator. Independent of `id`/`entityId`
10
+ * divergence (MTM is flat yet has `entityId !== id`).
11
+ * @public
12
+ */
13
+ export function isTemporalRecord(record) {
14
+ return record.envelope.temporal !== undefined;
15
+ }
16
+ /**
17
+ * Whether a temporal record is a *current* version — its `temporal.invalid_at`
18
+ * is `null` or absent (the still-valid sentinel). A non-temporal record is never
19
+ * current in this sense (returns `false`).
20
+ * @public
21
+ */
22
+ export function isVersionCurrent(record) {
23
+ const temporal = record.envelope.temporal;
24
+ if (temporal === undefined) {
25
+ return false;
26
+ }
27
+ return temporal.invalid_at === null || temporal.invalid_at === undefined;
28
+ }
29
+ /**
30
+ * Whether a temporal record's validity interval contains `asOf` (epoch ms):
31
+ * `valid_at <= asOf` and (`invalid_at` is null/absent OR `asOf < invalid_at`).
32
+ * The version's `valid_at` defaults to its `created` when absent; a non-temporal
33
+ * record is never "valid at" a point (returns `false`).
34
+ * @public
35
+ */
36
+ export function isVersionValidAt(record, asOf) {
37
+ var _a;
38
+ const temporal = record.envelope.temporal;
39
+ if (temporal === undefined) {
40
+ return false;
41
+ }
42
+ const start = (_a = temporal.valid_at) !== null && _a !== void 0 ? _a : record.envelope.created;
43
+ if (start > asOf) {
44
+ return false;
45
+ }
46
+ const end = temporal.invalid_at;
47
+ if (end === null || end === undefined) {
48
+ return true;
49
+ }
50
+ return asOf < end;
51
+ }
52
+ /**
53
+ * The version with the highest `seq` among `candidates` (undefined when empty).
54
+ * `seq` is the store's monotonic write counter, so highest `seq` is the newest
55
+ * version. Shared tiebreak for {@link selectCurrentVersion} /
56
+ * {@link selectVersionAsOf}.
57
+ */
58
+ function highestSeq(candidates) {
59
+ let best;
60
+ for (const candidate of candidates) {
61
+ if (best === undefined || candidate.envelope.seq > best.envelope.seq) {
62
+ best = candidate;
63
+ }
64
+ }
65
+ return best;
66
+ }
67
+ /**
68
+ * Select the current version from a set of an entity's versions: the newest
69
+ * (highest `seq`) version whose `invalid_at` is null/absent. `undefined` when the
70
+ * entity has no current version (fully invalidated / soft-deleted, or empty).
71
+ * @public
72
+ */
73
+ export function selectCurrentVersion(versions) {
74
+ return highestSeq(versions.filter(isVersionCurrent));
75
+ }
76
+ /**
77
+ * Select the version of an entity valid at `asOf` (epoch ms): the newest
78
+ * (highest `seq`) version whose validity interval contains `asOf`. `undefined`
79
+ * when no version was valid at that instant.
80
+ * @public
81
+ */
82
+ export function selectVersionAsOf(versions, asOf) {
83
+ return highestSeq(versions.filter((version) => isVersionValidAt(version, asOf)));
84
+ }
85
+ //# sourceMappingURL=temporal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"temporal.js","sourceRoot":"","sources":["../../../src/packlets/types/temporal.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAA8B;IAC7D,OAAO,MAAM,CAAC,QAAQ,CAAC,QAAQ,KAAK,SAAS,CAAC;AAChD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAA8B;IAC7D,MAAM,QAAQ,GAAmD,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAC1F,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,QAAQ,CAAC,UAAU,KAAK,IAAI,IAAI,QAAQ,CAAC,UAAU,KAAK,SAAS,CAAC;AAC3E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAA8B,EAAE,IAAY;;IAC3E,MAAM,QAAQ,GAAmD,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAC1F,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,KAAK,GAAW,MAAA,QAAQ,CAAC,QAAQ,mCAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;IACnE,IAAI,KAAK,GAAG,IAAI,EAAE,CAAC;QACjB,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,GAAG,GAA8B,QAAQ,CAAC,UAAU,CAAC;IAC3D,IAAI,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,IAAI,GAAG,GAAG,CAAC;AACpB,CAAC;AAED;;;;;GAKG;AACH,SAAS,UAAU,CAAC,UAAiD;IACnE,IAAI,IAAwC,CAAC;IAC7C,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,IAAI,KAAK,SAAS,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;YACrE,IAAI,GAAG,SAAS,CAAC;QACnB,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAClC,QAA+C;IAE/C,OAAO,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC;AACvD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAC/B,QAA+C,EAC/C,IAAY;IAEZ,OAAO,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;AACnF,CAAC","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { IMemoryRecord } from './envelope';\n\n/**\n * Whether a record participates in the versioned (temporal) layout. A temporal\n * record always carries a {@link ITemporalBlock | temporal} block (the store\n * stamps `valid_at` on every versioned write); an atemporal record never does,\n * so presence of `temporal` is the discriminator. Independent of `id`/`entityId`\n * divergence (MTM is flat yet has `entityId !== id`).\n * @public\n */\nexport function isTemporalRecord(record: IMemoryRecord<unknown>): boolean {\n return record.envelope.temporal !== undefined;\n}\n\n/**\n * Whether a temporal record is a *current* version — its `temporal.invalid_at`\n * is `null` or absent (the still-valid sentinel). A non-temporal record is never\n * current in this sense (returns `false`).\n * @public\n */\nexport function isVersionCurrent(record: IMemoryRecord<unknown>): boolean {\n const temporal: IMemoryRecord<unknown>['envelope']['temporal'] = record.envelope.temporal;\n if (temporal === undefined) {\n return false;\n }\n return temporal.invalid_at === null || temporal.invalid_at === undefined;\n}\n\n/**\n * Whether a temporal record's validity interval contains `asOf` (epoch ms):\n * `valid_at <= asOf` and (`invalid_at` is null/absent OR `asOf < invalid_at`).\n * The version's `valid_at` defaults to its `created` when absent; a non-temporal\n * record is never \"valid at\" a point (returns `false`).\n * @public\n */\nexport function isVersionValidAt(record: IMemoryRecord<unknown>, asOf: number): boolean {\n const temporal: IMemoryRecord<unknown>['envelope']['temporal'] = record.envelope.temporal;\n if (temporal === undefined) {\n return false;\n }\n const start: number = temporal.valid_at ?? record.envelope.created;\n if (start > asOf) {\n return false;\n }\n const end: number | null | undefined = temporal.invalid_at;\n if (end === null || end === undefined) {\n return true;\n }\n return asOf < end;\n}\n\n/**\n * The version with the highest `seq` among `candidates` (undefined when empty).\n * `seq` is the store's monotonic write counter, so highest `seq` is the newest\n * version. Shared tiebreak for {@link selectCurrentVersion} /\n * {@link selectVersionAsOf}.\n */\nfunction highestSeq(candidates: ReadonlyArray<IMemoryRecord<unknown>>): IMemoryRecord<unknown> | undefined {\n let best: IMemoryRecord<unknown> | undefined;\n for (const candidate of candidates) {\n if (best === undefined || candidate.envelope.seq > best.envelope.seq) {\n best = candidate;\n }\n }\n return best;\n}\n\n/**\n * Select the current version from a set of an entity's versions: the newest\n * (highest `seq`) version whose `invalid_at` is null/absent. `undefined` when the\n * entity has no current version (fully invalidated / soft-deleted, or empty).\n * @public\n */\nexport function selectCurrentVersion(\n versions: ReadonlyArray<IMemoryRecord<unknown>>\n): IMemoryRecord<unknown> | undefined {\n return highestSeq(versions.filter(isVersionCurrent));\n}\n\n/**\n * Select the version of an entity valid at `asOf` (epoch ms): the newest\n * (highest `seq`) version whose validity interval contains `asOf`. `undefined`\n * when no version was valid at that instant.\n * @public\n */\nexport function selectVersionAsOf(\n versions: ReadonlyArray<IMemoryRecord<unknown>>,\n asOf: number\n): IMemoryRecord<unknown> | undefined {\n return highestSeq(versions.filter((version) => isVersionValidAt(version, asOf)));\n}\n"]}
@@ -260,4 +260,100 @@ export class MemoryCapCullPolicy {
260
260
  return succeed({ envelope, body: 'body' in merged ? merged.body : existing.body });
261
261
  }
262
262
  }
263
+ /**
264
+ * Write policy for a versioned (temporal) kind family, implementing
265
+ * invalidate-don't-delete. Admission always accepts — history is retained, never
266
+ * culled — and updates apply the same RFC-7386 merge patch as
267
+ * {@link KnowledgeLwwPolicy}, restricted to the temporal mutable surface.
268
+ *
269
+ * @remarks
270
+ * The policy does NOT perform the version file writes or set `invalid_at` — that
271
+ * is the store's versioned write branch, driven by the kind's
272
+ * {@link ITemporalIdentityCodec}. The policy's role is limited to admission and
273
+ * the merge that forms the **new version's** content from the **current**
274
+ * version plus the incoming patch (the merge-patch-under-versioning contract).
275
+ *
276
+ * - **Dedup scope.** `'entity'` — an identical re-put of the current content is a
277
+ * no-op (the store compares the incoming content hash against the current
278
+ * version), so identical writes do not spawn redundant versions.
279
+ * - **Mutable surface.** `body` + the envelope metadata a consumer may revise
280
+ * (`tags` / `links` / `provenance` / `embeddingRef`). `temporal` is NOT mutable
281
+ * here — `valid_at` / `invalid_at` are set by the store's versioned branch.
282
+ * @public
283
+ */
284
+ export class TemporalVersionedPolicy {
285
+ constructor(cloneEditor, mergeEditor) {
286
+ /** The temporal mutable surface (mirrors {@link KnowledgeLwwPolicy}). */
287
+ this.mutableFields = [
288
+ 'body',
289
+ 'tags',
290
+ 'links',
291
+ 'provenance',
292
+ 'embeddingRef'
293
+ ];
294
+ /** Versioned kinds dedup per-entity against the current version (see the class remarks). */
295
+ this.dedupScope = 'entity';
296
+ this._cloneEditor = cloneEditor;
297
+ this._mergeEditor = mergeEditor;
298
+ }
299
+ /**
300
+ * Family-convention factory. Constructs the shared `JsonEditor` instances (one
301
+ * for cloning, one for the RFC-7386 merge), rules disabled — the same merge
302
+ * config as the shipped policies.
303
+ */
304
+ static create() {
305
+ return JsonEditor.create({}, []).onSuccess((cloneEditor) => JsonEditor.create(MERGE_PATCH_OPTIONS, []).onSuccess((mergeEditor) => succeed(new TemporalVersionedPolicy(cloneEditor, mergeEditor))));
306
+ }
307
+ /** {@inheritDoc IWritePolicy.admit} */
308
+ admit(__incoming, __existing) {
309
+ // Invalidate-don't-delete: always accept. Superseded versions are retained
310
+ // (invalidated), never culled.
311
+ return succeed({ decision: 'accept' });
312
+ }
313
+ /** {@inheritDoc IWritePolicy.applyUpdate} */
314
+ applyUpdate(existing, patch) {
315
+ // Project the mutable fields into a single record-level view, each sourced
316
+ // from its canonical location. `embeddingRef` is omitted when `undefined`.
317
+ const view = {
318
+ body: existing.body,
319
+ tags: existing.envelope.tags,
320
+ links: existing.envelope.links,
321
+ provenance: existing.envelope.provenance
322
+ };
323
+ if (existing.envelope.embeddingRef !== undefined) {
324
+ view.embeddingRef = existing.envelope.embeddingRef;
325
+ }
326
+ // Restrict the incoming patch to the declared mutable fields.
327
+ const scopedPatch = {};
328
+ for (const field of this.mutableFields) {
329
+ if (field in patch) {
330
+ scopedPatch[field] = patch[field];
331
+ }
332
+ }
333
+ // Clone the view (no null-deletion), then apply the RFC-7386 merge patch onto
334
+ // the clone so the current version is never mutated in place.
335
+ return this._cloneEditor
336
+ .mergeObjectInPlace({}, view)
337
+ .onSuccess((clone) => this._mergeEditor.mergeObjectInPlace(clone, scopedPatch))
338
+ .onSuccess((merged) => this._rebuild(existing, merged));
339
+ }
340
+ /**
341
+ * Reassemble a record from the merged mutable view. `body` / `tags` / `links` /
342
+ * `provenance` are required and may not be deleted by a patch; `embeddingRef`,
343
+ * when dropped by the merge, is restored as `undefined` (absent) — the same
344
+ * hash-stable semantics as {@link KnowledgeLwwPolicy}.
345
+ */
346
+ _rebuild(existing, merged) {
347
+ const required = ['body', 'tags', 'links', 'provenance'];
348
+ const missing = required.filter((field) => !(field in merged));
349
+ if (missing.length > 0) {
350
+ return fail(`temporal versioned: merge patch may not delete required field(s): ${missing.join(', ')}`);
351
+ }
352
+ // The merged values are JSON projections of the already-validated typed
353
+ // record; restore the domain types (structural restorations, not fresh
354
+ // untrusted input — mirrors KnowledgeLwwPolicy._rebuild).
355
+ const envelope = Object.assign(Object.assign({}, existing.envelope), { tags: merged.tags, links: merged.links, provenance: merged.provenance, embeddingRef: 'embeddingRef' in merged ? merged.embeddingRef : undefined });
356
+ return succeed({ envelope, body: merged.body });
357
+ }
358
+ }
263
359
  //# sourceMappingURL=writePolicy.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"writePolicy.js","sourceRoot":"","sources":["../../../src/packlets/types/writePolicy.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAU,IAAI,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAEtD,OAAO,EAAsB,UAAU,EAAE,MAAM,cAAc,CAAC;AAoB9D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAe,QAAQ,CAAC;AAuExD;;;;;;;;;GASG;AACH,MAAM,mBAAmB,GAAgC;IACvD,KAAK,EAAE;QACL,YAAY,EAAE,IAAI;QAClB,kBAAkB,EAAE,SAAS;KAC9B;CACF,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,OAAO,kBAAkB;IA0B7B,YAAoB,WAAuB,EAAE,WAAuB;QAzBpE;;;WAGG;QACa,kBAAa,GAA0B;YACrD,MAAM;YACN,MAAM;YACN,OAAO;YACP,YAAY;YACZ,cAAc;SACf,CAAC;QAEF;;;;;WAKG;QACa,eAAU,GAAe,SAAS,CAAC;QAQjD,IAAI,CAAC,YAAY,GAAG,WAAW,CAAC;QAChC,IAAI,CAAC,YAAY,GAAG,WAAW,CAAC;IAClC,CAAC;IAED;;;;OAIG;IACI,MAAM,CAAC,MAAM;QAClB,OAAO,UAAU,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,EAAE,CACzD,UAAU,CAAC,MAAM,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,EAAE,CACnE,OAAO,CAAC,IAAI,kBAAkB,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAC1D,CACF,CAAC;IACJ,CAAC;IAED,uCAAuC;IAChC,KAAK,CACV,UAAkC,EAClC,UAAiD;QAEjD,mDAAmD;QACnD,OAAO,OAAO,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;IACzC,CAAC;IAED,6CAA6C;IACtC,WAAW,CAChB,QAAgC,EAChC,KAA8B;QAE9B,2EAA2E;QAC3E,0EAA0E;QAC1E,kDAAkD;QAClD,MAAM,IAAI,GAA4B;YACpC,IAAI,EAAE,QAAQ,CAAC,IAAI;YACnB,IAAI,EAAE,QAAQ,CAAC,QAAQ,CAAC,IAAI;YAC5B,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,KAAK;YAC9B,UAAU,EAAE,QAAQ,CAAC,QAAQ,CAAC,UAAU;SACzC,CAAC;QACF,IAAI,QAAQ,CAAC,QAAQ,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;YACjD,IAAI,CAAC,YAAY,GAAG,QAAQ,CAAC,QAAQ,CAAC,YAAY,CAAC;QACrD,CAAC;QAED,2EAA2E;QAC3E,mEAAmE;QACnE,MAAM,WAAW,GAA4B,EAAE,CAAC;QAChD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACvC,IAAI,KAAK,IAAI,KAAK,EAAE,CAAC;gBACnB,WAAW,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;YACpC,CAAC;QACH,CAAC;QAED,yEAAyE;QACzE,oEAAoE;QACpE,OAAO,IAAI,CAAC,YAAY;aACrB,kBAAkB,CAAC,EAAE,EAAE,IAAkB,CAAC;aAC1C,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,kBAAkB,CAAC,KAAK,EAAE,WAAyB,CAAC,CAAC;aAC5F,SAAS,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED;;;;;;;;;OASG;IACK,QAAQ,CAAC,QAAgC,EAAE,MAAkB;QACnE,MAAM,QAAQ,GAA0B,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC;QAChF,MAAM,OAAO,GAA0B,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,MAAM,CAAC,CAAC,CAAC;QACtF,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,OAAO,IAAI,CAAC,gEAAgE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACpG,CAAC;QAED,wEAAwE;QACxE,yEAAyE;QACzE,2EAA2E;QAC3E,6DAA6D;QAC7D,MAAM,QAAQ,mCACT,QAAQ,CAAC,QAAQ,KACpB,IAAI,EAAE,MAAM,CAAC,IAAqC,EAClD,KAAK,EAAE,MAAM,CAAC,KAAwC,EACtD,UAAU,EAAE,MAAM,CAAC,UAAoC,EACvD,YAAY,EAAE,cAAc,IAAI,MAAM,CAAC,CAAC,CAAE,MAAM,CAAC,YAA8B,CAAC,CAAC,CAAC,SAAS,GAC5F,CAAC;QACF,OAAO,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;IAClD,CAAC;CACF;AAqBD;;;;;GAKG;AACH,MAAM,sBAAsB,GAAqE,IAAI,GAAG,CAGtG;IACA,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;IACvB,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;IAChC,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;IAClC,CAAC,YAAY,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC;IAC5C,CAAC,cAAc,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC;CACjD,CAAC,CAAC;AAEH,kFAAkF;AAClF,MAAM,wBAAwB,GAAwB,IAAI,GAAG,CAAS;IACpE,MAAM;IACN,MAAM;IACN,OAAO;IACP,YAAY;CACb,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,OAAO,mBAAmB;IAc9B,YAAoB,MAAkC,EAAE,WAAuB,EAAE,WAAuB;QAVxG,iEAAiE;QACjD,eAAU,GAAe,QAAQ,CAAC;QAUhD,IAAI,CAAC,aAAa,GAAG,MAAM,CAAC,aAAa,CAAC;QAC1C,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,UAAU,CAAC;QACrC,IAAI,CAAC,YAAY,GAAG,WAAW,CAAC;QAChC,IAAI,CAAC,YAAY,GAAG,WAAW,CAAC;IAClC,CAAC;IAED;;;;;OAKG;IACI,MAAM,CAAC,MAAM,CAAC,MAAkC;QACrD,OAAO,UAAU,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,EAAE,CACzD,UAAU,CAAC,MAAM,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,EAAE,CACnE,OAAO,CAAC,IAAI,mBAAmB,CAAC,MAAM,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC,CACnE,CACF,CAAC;IACJ,CAAC;IAED,uCAAuC;IAChC,KAAK,CACV,UAAkC,EAClC,QAA+C;QAE/C,IAAI,IAAI,CAAC,WAAW,KAAK,SAAS,IAAI,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;YACzE,OAAO,OAAO,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;QACzC,CAAC;QACD,yEAAyE;QACzE,4EAA4E;QAC5E,MAAM,KAAK,GAA4B,CAAC,GAAG,QAAQ,CAAC;aACjD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,GAAG,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC;aACvD,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,MAAM,GAAG,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC;aAChD,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QACvC,OAAO,OAAO,CAAC,EAAE,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,CAAC;IACrD,CAAC;IAED,6CAA6C;IACtC,WAAW,CAChB,QAAgC,EAChC,KAA8B;QAE9B,4EAA4E;QAC5E,mEAAmE;QACnE,2EAA2E;QAC3E,oBAAoB;QACpB,MAAM,IAAI,GAA4B,EAAE,CAAC;QACzC,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACvC,MAAM,MAAM,GACV,sBAAsB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACpC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBACzB,MAAM,KAAK,GAAY,MAAM,CAAC,QAAQ,CAAC,CAAC;gBACxC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;oBACxB,IAAI,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC;gBACtB,CAAC;YACH,CAAC;QACH,CAAC;QAED,8DAA8D;QAC9D,MAAM,WAAW,GAA4B,EAAE,CAAC;QAChD,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACvC,IAAI,KAAK,IAAI,KAAK,EAAE,CAAC;gBACnB,WAAW,CAAC,KAAK,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;YACpC,CAAC;QACH,CAAC;QAED,yEAAyE;QACzE,oEAAoE;QACpE,OAAO,IAAI,CAAC,YAAY;aACrB,kBAAkB,CAAC,EAAE,EAAE,IAAkB,CAAC;aAC1C,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,YAAY,CAAC,kBAAkB,CAAC,KAAK,EAAE,WAAyB,CAAC,CAAC;aAC5F,SAAS,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED;;;;;;;;;OASG;IACK,QAAQ,CAAC,QAAgC,EAAE,MAAkB;QACnE,MAAM,OAAO,GAA0B,IAAI,CAAC,aAAa,CAAC,MAAM,CAC9D,CAAC,KAAK,EAAE,EAAE,CAAC,wBAAwB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,MAAM,CAAC,CACrE,CAAC;QACF,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,OAAO,IAAI,CAAC,kEAAkE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACtG,CAAC;QAED,wEAAwE;QACxE,yEAAyE;QACzE,2EAA2E;QAC3E,qEAAqE;QACrE,gCAAgC;QAChC,MAAM,mBAAmB,GAAY,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;QACjF,MAAM,QAAQ,mCACT,QAAQ,CAAC,QAAQ,KACpB,IAAI,EAAE,MAAM,IAAI,MAAM,CAAC,CAAC,CAAE,MAAM,CAAC,IAAsC,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAChG,KAAK,EAAE,OAAO,IAAI,MAAM,CAAC,CAAC,CAAE,MAAM,CAAC,KAAyC,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,EACtG,UAAU,EACR,YAAY,IAAI,MAAM,CAAC,CAAC,CAAE,MAAM,CAAC,UAAqC,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,UAAU,EACvG,YAAY,EAAE,mBAAmB;gBAC/B,CAAC,CAAC,cAAc,IAAI,MAAM;oBACxB,CAAC,CAAE,MAAM,CAAC,YAA8B;oBACxC,CAAC,CAAC,SAAS;gBACb,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,YAAY,GACnC,CAAC;QACF,OAAO,OAAO,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,IAAI,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACrF,CAAC;CACF","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { JsonObject } from '@fgv/ts-json-base';\nimport { IJsonEditorOptions, JsonEditor } from '@fgv/ts-json';\nimport { IEdge, IMemoryEnvelope, IMemoryRecord, IProvenance } from './envelope';\nimport { MemoryId, Tag } from './ids';\n\n/**\n * The granularity at which the store deduplicates an incoming write against the\n * existing vault, declared per kind via {@link IWritePolicy.dedupScope}.\n *\n * - `'content'`: scope-wide cross-id content dedup. An identical\n * `{ kind, body, links }` triple ANYWHERE in the scope — even under a\n * different id — is a no-op that returns the existing record. The knowledge\n * kind family uses this.\n * - `'entity'`: same-id content dedup only. An identical re-put of the SAME\n * entity is a no-op, but two DISTINCT entities with identical content never\n * collapse. The experience (memory) kind families use this so that, e.g.,\n * two turns whose summaries happen to be byte-identical both persist.\n * @public\n */\nexport type DedupScope = 'content' | 'entity';\n\n/**\n * The default {@link DedupScope} applied when a policy does not declare one.\n * Entity-scoped dedup is the safe default — it never silently collapses two\n * distinct entities with coincidentally-identical content.\n * @public\n */\nexport const DEFAULT_DEDUP_SCOPE: DedupScope = 'entity';\n\n/**\n * The admission decision returned by {@link IWritePolicy.admit}.\n * @public\n */\nexport type AdmissionDecision =\n | { readonly decision: 'accept' }\n | { readonly decision: 'reject'; readonly reason: string }\n | {\n readonly decision: 'cull-oldest';\n /** {@link MemoryId}s to evict before writing the incoming record. */\n readonly evict: ReadonlyArray<MemoryId>;\n };\n\n/**\n * A per-kind write policy, injected at store construction. Invoked AFTER\n * content-hash dedup (dedup is always pre-policy). The policy decides\n * admission and, on update, applies a JSON Merge Patch (RFC-7386) restricted\n * to the declared mutable fields.\n * @public\n */\nexport interface IWritePolicy {\n /**\n * Names of the fields a merge-patch update may touch. Fields outside this\n * list are immutable; a change to one constitutes a new entity (its\n * `contentHash` differs). Used by {@link IWritePolicy.applyUpdate} to\n * restrict which fields the patch applies.\n */\n readonly mutableFields: ReadonlyArray<string>;\n\n /**\n * The granularity at which the store deduplicates an incoming write for this\n * kind. Optional; when absent the store applies {@link DEFAULT_DEDUP_SCOPE}\n * (`'entity'`). See {@link DedupScope}.\n */\n readonly dedupScope?: DedupScope;\n\n /**\n * Determine whether the incoming record is admitted.\n * @param incoming - The record about to be written.\n * @param existing - The admission cohort the kind's cap applies to: the\n * records in the same scope of the same kind, EXCLUDING the record at\n * `incoming`'s target id. Empty on a first write into an empty cohort.\n * Excluding the target id makes the post-write count uniform\n * (`existing.length + 1`) across first-writes and same-id updates. A\n * last-write-wins policy that has no cap ignores this argument.\n * @returns A {@link AdmissionDecision}.\n */\n admit(\n incoming: IMemoryRecord<unknown>,\n existing: ReadonlyArray<IMemoryRecord<unknown>>\n ): Result<AdmissionDecision>;\n\n /**\n * Apply a JSON Merge Patch (RFC-7386) to the mutable fields of an existing\n * record. Called when admission is `accept` AND a record with the same\n * `entityId` already exists (an update, not a first write).\n *\n * @param existing - The current persisted record.\n * @param patch - A partial JSON object in Merge Patch format. `null`\n * deletes the corresponding key; arrays replace wholesale; nested objects\n * deep-merge. Only keys in {@link IWritePolicy.mutableFields} are applied.\n * @returns The updated record (envelope + body).\n */\n applyUpdate(\n existing: IMemoryRecord<unknown>,\n patch: Record<string, unknown>\n ): Result<IMemoryRecord<unknown>>;\n}\n\n/**\n * RFC-7386-compliant merge options for {@link IWritePolicy.applyUpdate}.\n *\n * @remarks\n * Phase-A verification (design-lock §5.1) confirmed `@fgv/ts-json`'s\n * `JsonEditor` diverges from RFC-7386 under its defaults on two axes —\n * `nullAsDelete` defaults `false` and `arrayMergeBehavior` defaults\n * `'append'`. Both are corrected here via the existing option surface, so the\n * primitive is composed (not extended, not hand-rolled).\n */\nconst MERGE_PATCH_OPTIONS: Partial<IJsonEditorOptions> = {\n merge: {\n nullAsDelete: true,\n arrayMergeBehavior: 'replace'\n }\n};\n\n/**\n * Last-write-wins write policy for the knowledge kind family. Admission\n * always accepts (no cap, no cull); updates apply an RFC-7386 merge patch\n * restricted to the knowledge mutable surface.\n *\n * @remarks\n * **Merge-surface pin (resolves design-lock §5.3's body-vs-envelope muddle).**\n * The declared `mutableFields` span both axes of a record: `body` is the\n * per-kind body, while `tags` / `links` / `provenance` / `embeddingRef` live\n * on the {@link IMemoryEnvelope}. `applyUpdate` projects exactly those fields\n * — each read from its canonical location — into a single record-level JSON\n * view, runs the merge over that view, then rebuilds a coherent record. The\n * identity and transaction-time envelope fields (`id`, `entityId`, `kind`,\n * `created`, `updated`, `seq`, `contentHash`) are NOT mutable and are\n * preserved verbatim; the store stamps `updated` / `seq` on write.\n * @public\n */\nexport class KnowledgeLwwPolicy implements IWritePolicy {\n /**\n * The knowledge mutable surface: the body plus the envelope metadata a\n * consumer may revise without minting a new entity.\n */\n public readonly mutableFields: ReadonlyArray<string> = [\n 'body',\n 'tags',\n 'links',\n 'provenance',\n 'embeddingRef'\n ];\n\n /**\n * Knowledge dedups scope-wide: an identical `{ kind, body, links }` triple\n * anywhere in the `knowledge` scope — even under a different `docId` — is a\n * no-op. Declared explicitly so the B1 content-dedup behavior (and its tests)\n * are unchanged by the {@link DedupScope} amendment.\n */\n public readonly dedupScope: DedupScope = 'content';\n\n /** Deep-clones the mutable view without RFC-7386 null-deletion semantics. */\n private readonly _cloneEditor: JsonEditor;\n /** Applies the RFC-7386 merge patch. */\n private readonly _mergeEditor: JsonEditor;\n\n private constructor(cloneEditor: JsonEditor, mergeEditor: JsonEditor) {\n this._cloneEditor = cloneEditor;\n this._mergeEditor = mergeEditor;\n }\n\n /**\n * Family-convention factory. Constructs the shared `JsonEditor` instances\n * (one for cloning, one for the RFC-7386 merge), both with the template /\n * conditional / multivalue / reference rules disabled (empty rules array).\n */\n public static create(): Result<KnowledgeLwwPolicy> {\n return JsonEditor.create({}, []).onSuccess((cloneEditor) =>\n JsonEditor.create(MERGE_PATCH_OPTIONS, []).onSuccess((mergeEditor) =>\n succeed(new KnowledgeLwwPolicy(cloneEditor, mergeEditor))\n )\n );\n }\n\n /** {@inheritDoc IWritePolicy.admit} */\n public admit(\n __incoming: IMemoryRecord<unknown>,\n __existing: ReadonlyArray<IMemoryRecord<unknown>>\n ): Result<AdmissionDecision> {\n // Last-write-wins: always accept. No cap, no cull.\n return succeed({ decision: 'accept' });\n }\n\n /** {@inheritDoc IWritePolicy.applyUpdate} */\n public applyUpdate(\n existing: IMemoryRecord<unknown>,\n patch: Record<string, unknown>\n ): Result<IMemoryRecord<unknown>> {\n // Project the mutable fields into a single record-level view, each sourced\n // from its canonical location. `embeddingRef` is omitted when `undefined`\n // (the editor rejects undefined property values).\n const view: Record<string, unknown> = {\n body: existing.body,\n tags: existing.envelope.tags,\n links: existing.envelope.links,\n provenance: existing.envelope.provenance\n };\n if (existing.envelope.embeddingRef !== undefined) {\n view.embeddingRef = existing.envelope.embeddingRef;\n }\n\n // Restrict the incoming patch to the declared mutable fields; out-of-scope\n // keys are never applied (the store enforces the same constraint).\n const scopedPatch: Record<string, unknown> = {};\n for (const field of this.mutableFields) {\n if (field in patch) {\n scopedPatch[field] = patch[field];\n }\n }\n\n // Clone the view (no null-deletion), then apply the RFC-7386 merge patch\n // onto the clone so the persisted record is never mutated in place.\n return this._cloneEditor\n .mergeObjectInPlace({}, view as JsonObject)\n .onSuccess((clone) => this._mergeEditor.mergeObjectInPlace(clone, scopedPatch as JsonObject))\n .onSuccess((merged) => this._rebuild(existing, merged));\n }\n\n /**\n * Reassemble a record from the merged mutable view. `body` / `tags` /\n * `links` / `provenance` are required and may not be deleted by a patch.\n * `embeddingRef` is optional: when it is absent from the merged view —\n * because the existing record never carried it OR a `null` patch deleted it\n * (RFC-7386) — it is restored as `undefined` (absent), NOT `null`. This keeps\n * an originally-absent `embeddingRef` from silently flipping to `null` on an\n * unrelated update, so the field round-trips hash-stably through the store's\n * content-hash recomputation.\n */\n private _rebuild(existing: IMemoryRecord<unknown>, merged: JsonObject): Result<IMemoryRecord<unknown>> {\n const required: ReadonlyArray<string> = ['body', 'tags', 'links', 'provenance'];\n const missing: ReadonlyArray<string> = required.filter((field) => !(field in merged));\n if (missing.length > 0) {\n return fail(`knowledge LWW: merge patch may not delete required field(s): ${missing.join(', ')}`);\n }\n\n // The merged values are JSON projections of the already-validated typed\n // record; restore the domain types. (The types packlet cannot import the\n // converters packlet without a cycle, so these are structural restorations\n // of fields the merge preserved, not fresh untrusted input.)\n const envelope: IMemoryEnvelope = {\n ...existing.envelope,\n tags: merged.tags as unknown as ReadonlyArray<Tag>,\n links: merged.links as unknown as ReadonlyArray<IEdge>,\n provenance: merged.provenance as unknown as IProvenance,\n embeddingRef: 'embeddingRef' in merged ? (merged.embeddingRef as string | null) : undefined\n };\n return succeed({ envelope, body: merged.body });\n }\n}\n\n/**\n * Parameters for {@link MemoryCapCullPolicy.create}.\n * @public\n */\nexport interface IMemoryCapCullPolicyParams {\n /**\n * Maximum number of records the policy admits before culling the oldest.\n * Counted over the `existing` cohort passed to {@link IWritePolicy.admit}.\n * Absent = no cap (admission always accepts).\n */\n readonly maxRecords?: number;\n /**\n * The fields a merge-patch update may touch (drawn from the record-level\n * mutable vocabulary: `body` / `tags` / `links` / `provenance` /\n * `embeddingRef`). Fields outside this list are immutable.\n */\n readonly mutableFields: ReadonlyArray<string>;\n}\n\n/**\n * The record-level fields a {@link MemoryCapCullPolicy} merge-patch may project,\n * mapped to their canonical location on a record. Mirrors the store's\n * mutable-field accessor vocabulary; a declared mutable field outside this set\n * is inert (the store cannot project it either).\n */\nconst CAP_CULL_FIELD_READERS: ReadonlyMap<string, (record: IMemoryRecord<unknown>) => unknown> = new Map<\n string,\n (record: IMemoryRecord<unknown>) => unknown\n>([\n ['body', (r) => r.body],\n ['tags', (r) => r.envelope.tags],\n ['links', (r) => r.envelope.links],\n ['provenance', (r) => r.envelope.provenance],\n ['embeddingRef', (r) => r.envelope.embeddingRef]\n]);\n\n/** The record-level mutable fields that may never be deleted by a merge patch. */\nconst CAP_CULL_REQUIRED_FIELDS: ReadonlySet<string> = new Set<string>([\n 'body',\n 'tags',\n 'links',\n 'provenance'\n]);\n\n/**\n * Bounded-ring write policy for the experience (memory) kind families.\n * Admission accepts until `maxRecords` is reached, then evicts the oldest\n * record(s) by `created` ascending (design-lock §5.3); updates apply the same\n * RFC-7386 merge patch as {@link KnowledgeLwwPolicy}, restricted to the declared\n * {@link IMemoryCapCullPolicyParams.mutableFields | mutableFields}.\n *\n * @remarks\n * - **Dedup scope.** Declares `dedupScope: 'entity'` — two distinct memory\n * entities (e.g. `turn-5` / `turn-9`) with identical `{ kind, body, links }`\n * never collapse; only an identical re-put of the SAME entity is a no-op.\n * - **Eviction boundary.** `admit` only DECIDES (returns the `MemoryId`s to\n * evict); the store executes the file deletions and index patches. The\n * `existing` cohort the cap counts against is whatever the store supplies to\n * `admit`.\n * @public\n */\nexport class MemoryCapCullPolicy implements IWritePolicy {\n /** {@inheritDoc IWritePolicy.mutableFields} */\n public readonly mutableFields: ReadonlyArray<string>;\n\n /** Experience kinds dedup per-entity (see the class remarks). */\n public readonly dedupScope: DedupScope = 'entity';\n\n /** The admission cap; `undefined` = no cap. */\n private readonly _maxRecords: number | undefined;\n /** Deep-clones the mutable view without RFC-7386 null-deletion semantics. */\n private readonly _cloneEditor: JsonEditor;\n /** Applies the RFC-7386 merge patch. */\n private readonly _mergeEditor: JsonEditor;\n\n private constructor(params: IMemoryCapCullPolicyParams, cloneEditor: JsonEditor, mergeEditor: JsonEditor) {\n this.mutableFields = params.mutableFields;\n this._maxRecords = params.maxRecords;\n this._cloneEditor = cloneEditor;\n this._mergeEditor = mergeEditor;\n }\n\n /**\n * Family-convention factory. Constructs the shared `JsonEditor` instances\n * (one for cloning, one for the RFC-7386 merge) with the same merge config as\n * {@link KnowledgeLwwPolicy} (`nullAsDelete` true, `arrayMergeBehavior`\n * `'replace'`, rules disabled).\n */\n public static create(params: IMemoryCapCullPolicyParams): Result<MemoryCapCullPolicy> {\n return JsonEditor.create({}, []).onSuccess((cloneEditor) =>\n JsonEditor.create(MERGE_PATCH_OPTIONS, []).onSuccess((mergeEditor) =>\n succeed(new MemoryCapCullPolicy(params, cloneEditor, mergeEditor))\n )\n );\n }\n\n /** {@inheritDoc IWritePolicy.admit} */\n public admit(\n __incoming: IMemoryRecord<unknown>,\n existing: ReadonlyArray<IMemoryRecord<unknown>>\n ): Result<AdmissionDecision> {\n if (this._maxRecords === undefined || existing.length < this._maxRecords) {\n return succeed({ decision: 'accept' });\n }\n // Cap reached: evict the oldest by `created` ascending so the post-write\n // count is exactly `maxRecords` (existing.length - maxRecords + 1 victims).\n const evict: ReadonlyArray<MemoryId> = [...existing]\n .sort((a, b) => a.envelope.created - b.envelope.created)\n .slice(0, existing.length - this._maxRecords + 1)\n .map((record) => record.envelope.id);\n return succeed({ decision: 'cull-oldest', evict });\n }\n\n /** {@inheritDoc IWritePolicy.applyUpdate} */\n public applyUpdate(\n existing: IMemoryRecord<unknown>,\n patch: Record<string, unknown>\n ): Result<IMemoryRecord<unknown>> {\n // Project the declared mutable fields (restricted to the known record-level\n // vocabulary) into a single record-level view from their canonical\n // locations. An `undefined` value is omitted (the editor rejects undefined\n // property values).\n const view: Record<string, unknown> = {};\n for (const field of this.mutableFields) {\n const reader: ((record: IMemoryRecord<unknown>) => unknown) | undefined =\n CAP_CULL_FIELD_READERS.get(field);\n if (reader !== undefined) {\n const value: unknown = reader(existing);\n if (value !== undefined) {\n view[field] = value;\n }\n }\n }\n\n // Restrict the incoming patch to the declared mutable fields.\n const scopedPatch: Record<string, unknown> = {};\n for (const field of this.mutableFields) {\n if (field in patch) {\n scopedPatch[field] = patch[field];\n }\n }\n\n // Clone the view (no null-deletion), then apply the RFC-7386 merge patch\n // onto the clone so the persisted record is never mutated in place.\n return this._cloneEditor\n .mergeObjectInPlace({}, view as JsonObject)\n .onSuccess((clone) => this._mergeEditor.mergeObjectInPlace(clone, scopedPatch as JsonObject))\n .onSuccess((merged) => this._rebuild(existing, merged));\n }\n\n /**\n * Reassemble a record from the merged mutable view. Only the declared mutable\n * fields are taken from the merge; undeclared fields are preserved verbatim\n * from `existing`. A `null` patch that deletes a *declared mutable* required\n * field (`body` / `tags` / `links` / `provenance`) is an error — a required\n * field that is NOT declared mutable simply falls through to its `existing.*`\n * value and is never at risk. `embeddingRef`, when mutable, is restored as\n * `undefined` (absent) if the merge dropped it — same hash-stable semantics as\n * {@link KnowledgeLwwPolicy}.\n */\n private _rebuild(existing: IMemoryRecord<unknown>, merged: JsonObject): Result<IMemoryRecord<unknown>> {\n const deleted: ReadonlyArray<string> = this.mutableFields.filter(\n (field) => CAP_CULL_REQUIRED_FIELDS.has(field) && !(field in merged)\n );\n if (deleted.length > 0) {\n return fail(`memory cap-cull: merge patch may not delete required field(s): ${deleted.join(', ')}`);\n }\n\n // The merged values are JSON projections of the already-validated typed\n // record; restore the domain types. (The types packlet cannot import the\n // converters packlet without a cycle, so these are structural restorations\n // of fields the merge preserved, not fresh untrusted input — mirrors\n // KnowledgeLwwPolicy._rebuild.)\n const embeddingRefMutable: boolean = this.mutableFields.includes('embeddingRef');\n const envelope: IMemoryEnvelope = {\n ...existing.envelope,\n tags: 'tags' in merged ? (merged.tags as unknown as ReadonlyArray<Tag>) : existing.envelope.tags,\n links: 'links' in merged ? (merged.links as unknown as ReadonlyArray<IEdge>) : existing.envelope.links,\n provenance:\n 'provenance' in merged ? (merged.provenance as unknown as IProvenance) : existing.envelope.provenance,\n embeddingRef: embeddingRefMutable\n ? 'embeddingRef' in merged\n ? (merged.embeddingRef as string | null)\n : undefined\n : existing.envelope.embeddingRef\n };\n return succeed({ envelope, body: 'body' in merged ? merged.body : existing.body });\n }\n}\n"]}
1
+ {"version":3,"file":"writePolicy.js","sourceRoot":"","sources":["../../../src/packlets/types/writePolicy.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\nimport { Result, fail, succeed } from '@fgv/ts-utils';\nimport { JsonObject } from '@fgv/ts-json-base';\nimport { IJsonEditorOptions, JsonEditor } from '@fgv/ts-json';\nimport { IEdge, IMemoryEnvelope, IMemoryRecord, IProvenance } from './envelope';\nimport { MemoryId, Tag } from './ids';\n\n/**\n * The granularity at which the store deduplicates an incoming write against the\n * existing vault, declared per kind via {@link IWritePolicy.dedupScope}.\n *\n * - `'content'`: scope-wide cross-id content dedup. An identical\n * `{ kind, body, links }` triple ANYWHERE in the scope — even under a\n * different id — is a no-op that returns the existing record. The knowledge\n * kind family uses this.\n * - `'entity'`: same-id content dedup only. An identical re-put of the SAME\n * entity is a no-op, but two DISTINCT entities with identical content never\n * collapse. The experience (memory) kind families use this so that, e.g.,\n * two turns whose summaries happen to be byte-identical both persist.\n * @public\n */\nexport type DedupScope = 'content' | 'entity';\n\n/**\n * The default {@link DedupScope} applied when a policy does not declare one.\n * Entity-scoped dedup is the safe default — it never silently collapses two\n * distinct entities with coincidentally-identical content.\n * @public\n */\nexport const DEFAULT_DEDUP_SCOPE: DedupScope = 'entity';\n\n/**\n * The admission decision returned by {@link IWritePolicy.admit}.\n * @public\n */\nexport type AdmissionDecision =\n | { readonly decision: 'accept' }\n | { readonly decision: 'reject'; readonly reason: string }\n | {\n readonly decision: 'cull-oldest';\n /** {@link MemoryId}s to evict before writing the incoming record. */\n readonly evict: ReadonlyArray<MemoryId>;\n };\n\n/**\n * A per-kind write policy, injected at store construction. Invoked AFTER\n * content-hash dedup (dedup is always pre-policy). The policy decides\n * admission and, on update, applies a JSON Merge Patch (RFC-7386) restricted\n * to the declared mutable fields.\n * @public\n */\nexport interface IWritePolicy {\n /**\n * Names of the fields a merge-patch update may touch. Fields outside this\n * list are immutable; a change to one constitutes a new entity (its\n * `contentHash` differs). Used by {@link IWritePolicy.applyUpdate} to\n * restrict which fields the patch applies.\n */\n readonly mutableFields: ReadonlyArray<string>;\n\n /**\n * The granularity at which the store deduplicates an incoming write for this\n * kind. Optional; when absent the store applies {@link DEFAULT_DEDUP_SCOPE}\n * (`'entity'`). See {@link DedupScope}.\n */\n readonly dedupScope?: DedupScope;\n\n /**\n * Determine whether the incoming record is admitted.\n * @param incoming - The record about to be written.\n * @param existing - The admission cohort the kind's cap applies to: the\n * records in the same scope of the same kind, EXCLUDING the record at\n * `incoming`'s target id. Empty on a first write into an empty cohort.\n * Excluding the target id makes the post-write count uniform\n * (`existing.length + 1`) across first-writes and same-id updates. A\n * last-write-wins policy that has no cap ignores this argument.\n * @returns A {@link AdmissionDecision}.\n */\n admit(\n incoming: IMemoryRecord<unknown>,\n existing: ReadonlyArray<IMemoryRecord<unknown>>\n ): Result<AdmissionDecision>;\n\n /**\n * Apply a JSON Merge Patch (RFC-7386) to the mutable fields of an existing\n * record. Called when admission is `accept` AND a record with the same\n * `entityId` already exists (an update, not a first write).\n *\n * @param existing - The current persisted record.\n * @param patch - A partial JSON object in Merge Patch format. `null`\n * deletes the corresponding key; arrays replace wholesale; nested objects\n * deep-merge. Only keys in {@link IWritePolicy.mutableFields} are applied.\n * @returns The updated record (envelope + body).\n */\n applyUpdate(\n existing: IMemoryRecord<unknown>,\n patch: Record<string, unknown>\n ): Result<IMemoryRecord<unknown>>;\n}\n\n/**\n * RFC-7386-compliant merge options for {@link IWritePolicy.applyUpdate}.\n *\n * @remarks\n * Phase-A verification (design-lock §5.1) confirmed `@fgv/ts-json`'s\n * `JsonEditor` diverges from RFC-7386 under its defaults on two axes —\n * `nullAsDelete` defaults `false` and `arrayMergeBehavior` defaults\n * `'append'`. Both are corrected here via the existing option surface, so the\n * primitive is composed (not extended, not hand-rolled).\n */\nconst MERGE_PATCH_OPTIONS: Partial<IJsonEditorOptions> = {\n merge: {\n nullAsDelete: true,\n arrayMergeBehavior: 'replace'\n }\n};\n\n/**\n * Last-write-wins write policy for the knowledge kind family. Admission\n * always accepts (no cap, no cull); updates apply an RFC-7386 merge patch\n * restricted to the knowledge mutable surface.\n *\n * @remarks\n * **Merge-surface pin (resolves design-lock §5.3's body-vs-envelope muddle).**\n * The declared `mutableFields` span both axes of a record: `body` is the\n * per-kind body, while `tags` / `links` / `provenance` / `embeddingRef` live\n * on the {@link IMemoryEnvelope}. `applyUpdate` projects exactly those fields\n * — each read from its canonical location — into a single record-level JSON\n * view, runs the merge over that view, then rebuilds a coherent record. The\n * identity and transaction-time envelope fields (`id`, `entityId`, `kind`,\n * `created`, `updated`, `seq`, `contentHash`) are NOT mutable and are\n * preserved verbatim; the store stamps `updated` / `seq` on write.\n * @public\n */\nexport class KnowledgeLwwPolicy implements IWritePolicy {\n /**\n * The knowledge mutable surface: the body plus the envelope metadata a\n * consumer may revise without minting a new entity.\n */\n public readonly mutableFields: ReadonlyArray<string> = [\n 'body',\n 'tags',\n 'links',\n 'provenance',\n 'embeddingRef'\n ];\n\n /**\n * Knowledge dedups scope-wide: an identical `{ kind, body, links }` triple\n * anywhere in the `knowledge` scope — even under a different `docId` — is a\n * no-op. Declared explicitly so the B1 content-dedup behavior (and its tests)\n * are unchanged by the {@link DedupScope} amendment.\n */\n public readonly dedupScope: DedupScope = 'content';\n\n /** Deep-clones the mutable view without RFC-7386 null-deletion semantics. */\n private readonly _cloneEditor: JsonEditor;\n /** Applies the RFC-7386 merge patch. */\n private readonly _mergeEditor: JsonEditor;\n\n private constructor(cloneEditor: JsonEditor, mergeEditor: JsonEditor) {\n this._cloneEditor = cloneEditor;\n this._mergeEditor = mergeEditor;\n }\n\n /**\n * Family-convention factory. Constructs the shared `JsonEditor` instances\n * (one for cloning, one for the RFC-7386 merge), both with the template /\n * conditional / multivalue / reference rules disabled (empty rules array).\n */\n public static create(): Result<KnowledgeLwwPolicy> {\n return JsonEditor.create({}, []).onSuccess((cloneEditor) =>\n JsonEditor.create(MERGE_PATCH_OPTIONS, []).onSuccess((mergeEditor) =>\n succeed(new KnowledgeLwwPolicy(cloneEditor, mergeEditor))\n )\n );\n }\n\n /** {@inheritDoc IWritePolicy.admit} */\n public admit(\n __incoming: IMemoryRecord<unknown>,\n __existing: ReadonlyArray<IMemoryRecord<unknown>>\n ): Result<AdmissionDecision> {\n // Last-write-wins: always accept. No cap, no cull.\n return succeed({ decision: 'accept' });\n }\n\n /** {@inheritDoc IWritePolicy.applyUpdate} */\n public applyUpdate(\n existing: IMemoryRecord<unknown>,\n patch: Record<string, unknown>\n ): Result<IMemoryRecord<unknown>> {\n // Project the mutable fields into a single record-level view, each sourced\n // from its canonical location. `embeddingRef` is omitted when `undefined`\n // (the editor rejects undefined property values).\n const view: Record<string, unknown> = {\n body: existing.body,\n tags: existing.envelope.tags,\n links: existing.envelope.links,\n provenance: existing.envelope.provenance\n };\n if (existing.envelope.embeddingRef !== undefined) {\n view.embeddingRef = existing.envelope.embeddingRef;\n }\n\n // Restrict the incoming patch to the declared mutable fields; out-of-scope\n // keys are never applied (the store enforces the same constraint).\n const scopedPatch: Record<string, unknown> = {};\n for (const field of this.mutableFields) {\n if (field in patch) {\n scopedPatch[field] = patch[field];\n }\n }\n\n // Clone the view (no null-deletion), then apply the RFC-7386 merge patch\n // onto the clone so the persisted record is never mutated in place.\n return this._cloneEditor\n .mergeObjectInPlace({}, view as JsonObject)\n .onSuccess((clone) => this._mergeEditor.mergeObjectInPlace(clone, scopedPatch as JsonObject))\n .onSuccess((merged) => this._rebuild(existing, merged));\n }\n\n /**\n * Reassemble a record from the merged mutable view. `body` / `tags` /\n * `links` / `provenance` are required and may not be deleted by a patch.\n * `embeddingRef` is optional: when it is absent from the merged view —\n * because the existing record never carried it OR a `null` patch deleted it\n * (RFC-7386) — it is restored as `undefined` (absent), NOT `null`. This keeps\n * an originally-absent `embeddingRef` from silently flipping to `null` on an\n * unrelated update, so the field round-trips hash-stably through the store's\n * content-hash recomputation.\n */\n private _rebuild(existing: IMemoryRecord<unknown>, merged: JsonObject): Result<IMemoryRecord<unknown>> {\n const required: ReadonlyArray<string> = ['body', 'tags', 'links', 'provenance'];\n const missing: ReadonlyArray<string> = required.filter((field) => !(field in merged));\n if (missing.length > 0) {\n return fail(`knowledge LWW: merge patch may not delete required field(s): ${missing.join(', ')}`);\n }\n\n // The merged values are JSON projections of the already-validated typed\n // record; restore the domain types. (The types packlet cannot import the\n // converters packlet without a cycle, so these are structural restorations\n // of fields the merge preserved, not fresh untrusted input.)\n const envelope: IMemoryEnvelope = {\n ...existing.envelope,\n tags: merged.tags as unknown as ReadonlyArray<Tag>,\n links: merged.links as unknown as ReadonlyArray<IEdge>,\n provenance: merged.provenance as unknown as IProvenance,\n embeddingRef: 'embeddingRef' in merged ? (merged.embeddingRef as string | null) : undefined\n };\n return succeed({ envelope, body: merged.body });\n }\n}\n\n/**\n * Parameters for {@link MemoryCapCullPolicy.create}.\n * @public\n */\nexport interface IMemoryCapCullPolicyParams {\n /**\n * Maximum number of records the policy admits before culling the oldest.\n * Counted over the `existing` cohort passed to {@link IWritePolicy.admit}.\n * Absent = no cap (admission always accepts).\n */\n readonly maxRecords?: number;\n /**\n * The fields a merge-patch update may touch (drawn from the record-level\n * mutable vocabulary: `body` / `tags` / `links` / `provenance` /\n * `embeddingRef`). Fields outside this list are immutable.\n */\n readonly mutableFields: ReadonlyArray<string>;\n}\n\n/**\n * The record-level fields a {@link MemoryCapCullPolicy} merge-patch may project,\n * mapped to their canonical location on a record. Mirrors the store's\n * mutable-field accessor vocabulary; a declared mutable field outside this set\n * is inert (the store cannot project it either).\n */\nconst CAP_CULL_FIELD_READERS: ReadonlyMap<string, (record: IMemoryRecord<unknown>) => unknown> = new Map<\n string,\n (record: IMemoryRecord<unknown>) => unknown\n>([\n ['body', (r) => r.body],\n ['tags', (r) => r.envelope.tags],\n ['links', (r) => r.envelope.links],\n ['provenance', (r) => r.envelope.provenance],\n ['embeddingRef', (r) => r.envelope.embeddingRef]\n]);\n\n/** The record-level mutable fields that may never be deleted by a merge patch. */\nconst CAP_CULL_REQUIRED_FIELDS: ReadonlySet<string> = new Set<string>([\n 'body',\n 'tags',\n 'links',\n 'provenance'\n]);\n\n/**\n * Bounded-ring write policy for the experience (memory) kind families.\n * Admission accepts until `maxRecords` is reached, then evicts the oldest\n * record(s) by `created` ascending (design-lock §5.3); updates apply the same\n * RFC-7386 merge patch as {@link KnowledgeLwwPolicy}, restricted to the declared\n * {@link IMemoryCapCullPolicyParams.mutableFields | mutableFields}.\n *\n * @remarks\n * - **Dedup scope.** Declares `dedupScope: 'entity'` — two distinct memory\n * entities (e.g. `turn-5` / `turn-9`) with identical `{ kind, body, links }`\n * never collapse; only an identical re-put of the SAME entity is a no-op.\n * - **Eviction boundary.** `admit` only DECIDES (returns the `MemoryId`s to\n * evict); the store executes the file deletions and index patches. The\n * `existing` cohort the cap counts against is whatever the store supplies to\n * `admit`.\n * @public\n */\nexport class MemoryCapCullPolicy implements IWritePolicy {\n /** {@inheritDoc IWritePolicy.mutableFields} */\n public readonly mutableFields: ReadonlyArray<string>;\n\n /** Experience kinds dedup per-entity (see the class remarks). */\n public readonly dedupScope: DedupScope = 'entity';\n\n /** The admission cap; `undefined` = no cap. */\n private readonly _maxRecords: number | undefined;\n /** Deep-clones the mutable view without RFC-7386 null-deletion semantics. */\n private readonly _cloneEditor: JsonEditor;\n /** Applies the RFC-7386 merge patch. */\n private readonly _mergeEditor: JsonEditor;\n\n private constructor(params: IMemoryCapCullPolicyParams, cloneEditor: JsonEditor, mergeEditor: JsonEditor) {\n this.mutableFields = params.mutableFields;\n this._maxRecords = params.maxRecords;\n this._cloneEditor = cloneEditor;\n this._mergeEditor = mergeEditor;\n }\n\n /**\n * Family-convention factory. Constructs the shared `JsonEditor` instances\n * (one for cloning, one for the RFC-7386 merge) with the same merge config as\n * {@link KnowledgeLwwPolicy} (`nullAsDelete` true, `arrayMergeBehavior`\n * `'replace'`, rules disabled).\n */\n public static create(params: IMemoryCapCullPolicyParams): Result<MemoryCapCullPolicy> {\n return JsonEditor.create({}, []).onSuccess((cloneEditor) =>\n JsonEditor.create(MERGE_PATCH_OPTIONS, []).onSuccess((mergeEditor) =>\n succeed(new MemoryCapCullPolicy(params, cloneEditor, mergeEditor))\n )\n );\n }\n\n /** {@inheritDoc IWritePolicy.admit} */\n public admit(\n __incoming: IMemoryRecord<unknown>,\n existing: ReadonlyArray<IMemoryRecord<unknown>>\n ): Result<AdmissionDecision> {\n if (this._maxRecords === undefined || existing.length < this._maxRecords) {\n return succeed({ decision: 'accept' });\n }\n // Cap reached: evict the oldest by `created` ascending so the post-write\n // count is exactly `maxRecords` (existing.length - maxRecords + 1 victims).\n const evict: ReadonlyArray<MemoryId> = [...existing]\n .sort((a, b) => a.envelope.created - b.envelope.created)\n .slice(0, existing.length - this._maxRecords + 1)\n .map((record) => record.envelope.id);\n return succeed({ decision: 'cull-oldest', evict });\n }\n\n /** {@inheritDoc IWritePolicy.applyUpdate} */\n public applyUpdate(\n existing: IMemoryRecord<unknown>,\n patch: Record<string, unknown>\n ): Result<IMemoryRecord<unknown>> {\n // Project the declared mutable fields (restricted to the known record-level\n // vocabulary) into a single record-level view from their canonical\n // locations. An `undefined` value is omitted (the editor rejects undefined\n // property values).\n const view: Record<string, unknown> = {};\n for (const field of this.mutableFields) {\n const reader: ((record: IMemoryRecord<unknown>) => unknown) | undefined =\n CAP_CULL_FIELD_READERS.get(field);\n if (reader !== undefined) {\n const value: unknown = reader(existing);\n if (value !== undefined) {\n view[field] = value;\n }\n }\n }\n\n // Restrict the incoming patch to the declared mutable fields.\n const scopedPatch: Record<string, unknown> = {};\n for (const field of this.mutableFields) {\n if (field in patch) {\n scopedPatch[field] = patch[field];\n }\n }\n\n // Clone the view (no null-deletion), then apply the RFC-7386 merge patch\n // onto the clone so the persisted record is never mutated in place.\n return this._cloneEditor\n .mergeObjectInPlace({}, view as JsonObject)\n .onSuccess((clone) => this._mergeEditor.mergeObjectInPlace(clone, scopedPatch as JsonObject))\n .onSuccess((merged) => this._rebuild(existing, merged));\n }\n\n /**\n * Reassemble a record from the merged mutable view. Only the declared mutable\n * fields are taken from the merge; undeclared fields are preserved verbatim\n * from `existing`. A `null` patch that deletes a *declared mutable* required\n * field (`body` / `tags` / `links` / `provenance`) is an error — a required\n * field that is NOT declared mutable simply falls through to its `existing.*`\n * value and is never at risk. `embeddingRef`, when mutable, is restored as\n * `undefined` (absent) if the merge dropped it — same hash-stable semantics as\n * {@link KnowledgeLwwPolicy}.\n */\n private _rebuild(existing: IMemoryRecord<unknown>, merged: JsonObject): Result<IMemoryRecord<unknown>> {\n const deleted: ReadonlyArray<string> = this.mutableFields.filter(\n (field) => CAP_CULL_REQUIRED_FIELDS.has(field) && !(field in merged)\n );\n if (deleted.length > 0) {\n return fail(`memory cap-cull: merge patch may not delete required field(s): ${deleted.join(', ')}`);\n }\n\n // The merged values are JSON projections of the already-validated typed\n // record; restore the domain types. (The types packlet cannot import the\n // converters packlet without a cycle, so these are structural restorations\n // of fields the merge preserved, not fresh untrusted input — mirrors\n // KnowledgeLwwPolicy._rebuild.)\n const embeddingRefMutable: boolean = this.mutableFields.includes('embeddingRef');\n const envelope: IMemoryEnvelope = {\n ...existing.envelope,\n tags: 'tags' in merged ? (merged.tags as unknown as ReadonlyArray<Tag>) : existing.envelope.tags,\n links: 'links' in merged ? (merged.links as unknown as ReadonlyArray<IEdge>) : existing.envelope.links,\n provenance:\n 'provenance' in merged ? (merged.provenance as unknown as IProvenance) : existing.envelope.provenance,\n embeddingRef: embeddingRefMutable\n ? 'embeddingRef' in merged\n ? (merged.embeddingRef as string | null)\n : undefined\n : existing.envelope.embeddingRef\n };\n return succeed({ envelope, body: 'body' in merged ? merged.body : existing.body });\n }\n}\n\n/**\n * Write policy for a versioned (temporal) kind family, implementing\n * invalidate-don't-delete. Admission always accepts — history is retained, never\n * culled — and updates apply the same RFC-7386 merge patch as\n * {@link KnowledgeLwwPolicy}, restricted to the temporal mutable surface.\n *\n * @remarks\n * The policy does NOT perform the version file writes or set `invalid_at` — that\n * is the store's versioned write branch, driven by the kind's\n * {@link ITemporalIdentityCodec}. The policy's role is limited to admission and\n * the merge that forms the **new version's** content from the **current**\n * version plus the incoming patch (the merge-patch-under-versioning contract).\n *\n * - **Dedup scope.** `'entity'` — an identical re-put of the current content is a\n * no-op (the store compares the incoming content hash against the current\n * version), so identical writes do not spawn redundant versions.\n * - **Mutable surface.** `body` + the envelope metadata a consumer may revise\n * (`tags` / `links` / `provenance` / `embeddingRef`). `temporal` is NOT mutable\n * here — `valid_at` / `invalid_at` are set by the store's versioned branch.\n * @public\n */\nexport class TemporalVersionedPolicy implements IWritePolicy {\n /** The temporal mutable surface (mirrors {@link KnowledgeLwwPolicy}). */\n public readonly mutableFields: ReadonlyArray<string> = [\n 'body',\n 'tags',\n 'links',\n 'provenance',\n 'embeddingRef'\n ];\n\n /** Versioned kinds dedup per-entity against the current version (see the class remarks). */\n public readonly dedupScope: DedupScope = 'entity';\n\n /** Deep-clones the mutable view without RFC-7386 null-deletion semantics. */\n private readonly _cloneEditor: JsonEditor;\n /** Applies the RFC-7386 merge patch. */\n private readonly _mergeEditor: JsonEditor;\n\n private constructor(cloneEditor: JsonEditor, mergeEditor: JsonEditor) {\n this._cloneEditor = cloneEditor;\n this._mergeEditor = mergeEditor;\n }\n\n /**\n * Family-convention factory. Constructs the shared `JsonEditor` instances (one\n * for cloning, one for the RFC-7386 merge), rules disabled — the same merge\n * config as the shipped policies.\n */\n public static create(): Result<TemporalVersionedPolicy> {\n return JsonEditor.create({}, []).onSuccess((cloneEditor) =>\n JsonEditor.create(MERGE_PATCH_OPTIONS, []).onSuccess((mergeEditor) =>\n succeed(new TemporalVersionedPolicy(cloneEditor, mergeEditor))\n )\n );\n }\n\n /** {@inheritDoc IWritePolicy.admit} */\n public admit(\n __incoming: IMemoryRecord<unknown>,\n __existing: ReadonlyArray<IMemoryRecord<unknown>>\n ): Result<AdmissionDecision> {\n // Invalidate-don't-delete: always accept. Superseded versions are retained\n // (invalidated), never culled.\n return succeed({ decision: 'accept' });\n }\n\n /** {@inheritDoc IWritePolicy.applyUpdate} */\n public applyUpdate(\n existing: IMemoryRecord<unknown>,\n patch: Record<string, unknown>\n ): Result<IMemoryRecord<unknown>> {\n // Project the mutable fields into a single record-level view, each sourced\n // from its canonical location. `embeddingRef` is omitted when `undefined`.\n const view: Record<string, unknown> = {\n body: existing.body,\n tags: existing.envelope.tags,\n links: existing.envelope.links,\n provenance: existing.envelope.provenance\n };\n if (existing.envelope.embeddingRef !== undefined) {\n view.embeddingRef = existing.envelope.embeddingRef;\n }\n\n // Restrict the incoming patch to the declared mutable fields.\n const scopedPatch: Record<string, unknown> = {};\n for (const field of this.mutableFields) {\n if (field in patch) {\n scopedPatch[field] = patch[field];\n }\n }\n\n // Clone the view (no null-deletion), then apply the RFC-7386 merge patch onto\n // the clone so the current version is never mutated in place.\n return this._cloneEditor\n .mergeObjectInPlace({}, view as JsonObject)\n .onSuccess((clone) => this._mergeEditor.mergeObjectInPlace(clone, scopedPatch as JsonObject))\n .onSuccess((merged) => this._rebuild(existing, merged));\n }\n\n /**\n * Reassemble a record from the merged mutable view. `body` / `tags` / `links` /\n * `provenance` are required and may not be deleted by a patch; `embeddingRef`,\n * when dropped by the merge, is restored as `undefined` (absent) — the same\n * hash-stable semantics as {@link KnowledgeLwwPolicy}.\n */\n private _rebuild(existing: IMemoryRecord<unknown>, merged: JsonObject): Result<IMemoryRecord<unknown>> {\n const required: ReadonlyArray<string> = ['body', 'tags', 'links', 'provenance'];\n const missing: ReadonlyArray<string> = required.filter((field) => !(field in merged));\n if (missing.length > 0) {\n return fail(`temporal versioned: merge patch may not delete required field(s): ${missing.join(', ')}`);\n }\n\n // The merged values are JSON projections of the already-validated typed\n // record; restore the domain types (structural restorations, not fresh\n // untrusted input — mirrors KnowledgeLwwPolicy._rebuild).\n const envelope: IMemoryEnvelope = {\n ...existing.envelope,\n tags: merged.tags as unknown as ReadonlyArray<Tag>,\n links: merged.links as unknown as ReadonlyArray<IEdge>,\n provenance: merged.provenance as unknown as IProvenance,\n embeddingRef: 'embeddingRef' in merged ? (merged.embeddingRef as string | null) : undefined\n };\n return succeed({ envelope, body: merged.body });\n }\n}\n"]}
@@ -0,0 +1,95 @@
1
+ /*
2
+ * Copyright (c) 2026 Erik Fortune
3
+ * SPDX-License-Identifier: MIT
4
+ */
5
+ /**
6
+ * Antagonist torture test — convert/validate round-trip symmetry (target class
7
+ * 7): every OPTIONAL field the envelope/edge/provenance shapes carry must
8
+ * survive a full on-disk round-trip (serialize → YAML text → parse), not just a
9
+ * single `Converter.convert` pass in memory. This exercises the exact path a
10
+ * corrupted or field-dropping serializer/parser pair would break, mirroring the
11
+ * `aiClientToolConfig`/`annotations` field-drop class named in the brief.
12
+ */
13
+ import '@fgv/ts-utils-jest';
14
+ import { Converters } from '@fgv/ts-utils';
15
+ import { BodyConverterRegistry, parseMemoryFile, serializeMemoryFile } from '../../../index';
16
+ const kind = 'note';
17
+ function registry() {
18
+ const reg = BodyConverterRegistry.create().orThrow();
19
+ reg.register(kind, Converters.string);
20
+ return reg;
21
+ }
22
+ describe('antagonist — full on-disk round-trip preserves every optional field', () => {
23
+ // Wrong impl this catches: a serializer/parser pair where one side silently
24
+ // drops an optional field (the exact class of bug the brief calls out for
25
+ // `aiClientToolConfig`/`annotations`-shaped converters) — e.g. omitting
26
+ // `temporal.invalid_at: null`, an edge's `valid_at`/`invalid_at`/`provenance`,
27
+ // a `null` `embeddingRef`, or a provenance extension key, because the author
28
+ // forgot to thread it through both the YAML emit AND the envelope Converter.
29
+ test('every optional envelope/edge/provenance field set simultaneously survives a full YAML round-trip', () => {
30
+ const envelope = {
31
+ id: 'doc-1',
32
+ entityId: 'doc-1',
33
+ kind,
34
+ tags: ['t1', 't2'],
35
+ links: [
36
+ {
37
+ type: 'rel',
38
+ target: 'doc-2',
39
+ confidence: 0.42,
40
+ provenance: { source: 'agent', by: 'curator', extra: { nested: true } },
41
+ valid_at: 111,
42
+ invalid_at: null
43
+ }
44
+ ],
45
+ created: 1000,
46
+ updated: 2000,
47
+ seq: 7,
48
+ contentHash: 'abc123',
49
+ provenance: {
50
+ source: 'host-ingest',
51
+ by: 'erik',
52
+ model: 'gpt-5',
53
+ confidence: 0.87,
54
+ derivedFrom: 'turn-3',
55
+ // Opaque extension keys (per IProvenance's `[key: string]: unknown` arm).
56
+ sentiment: { score: 0.5 },
57
+ epistemic: 'belief'
58
+ },
59
+ temporal: { valid_at: 500, invalid_at: null },
60
+ embeddingRef: null
61
+ };
62
+ const raw = serializeMemoryFile(envelope, 'the body text').orThrow();
63
+ expect(parseMemoryFile(raw, registry())).toSucceedAndSatisfy((record) => {
64
+ // Deep-equal the ENTIRE envelope, not field-by-field — a partial assertion
65
+ // list is exactly how a single dropped field slips through review.
66
+ expect(record.envelope).toEqual(envelope);
67
+ expect(record.body).toBe('the body text');
68
+ });
69
+ });
70
+ test('an absent temporal/embeddingRef/edge-optionals round-trips to fully absent (no null-vs-undefined drift)', () => {
71
+ const envelope = {
72
+ id: 'doc-2',
73
+ entityId: 'doc-2',
74
+ kind,
75
+ tags: [],
76
+ links: [{ type: 'rel', target: 'doc-3' }],
77
+ created: 0,
78
+ updated: 0,
79
+ seq: 0,
80
+ contentHash: '',
81
+ provenance: { source: 'agent' }
82
+ };
83
+ const raw = serializeMemoryFile(envelope, 'body').orThrow();
84
+ expect(parseMemoryFile(raw, registry())).toSucceedAndSatisfy((record) => {
85
+ expect(record.envelope.temporal).toBeUndefined();
86
+ expect(record.envelope.embeddingRef).toBeUndefined();
87
+ expect(record.envelope.links[0].confidence).toBeUndefined();
88
+ expect(record.envelope.links[0].provenance).toBeUndefined();
89
+ expect(record.envelope.links[0].valid_at).toBeUndefined();
90
+ expect(record.envelope.links[0].invalid_at).toBeUndefined();
91
+ expect(record.envelope).toEqual(envelope);
92
+ });
93
+ });
94
+ });
95
+ //# sourceMappingURL=antagonistRoundTrip.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"antagonistRoundTrip.test.js","sourceRoot":"","sources":["../../../../src/test/unit/converters/antagonistRoundTrip.test.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright (c) 2026 Erik Fortune\n * SPDX-License-Identifier: MIT\n */\n\n/**\n * Antagonist torture test — convert/validate round-trip symmetry (target class\n * 7): every OPTIONAL field the envelope/edge/provenance shapes carry must\n * survive a full on-disk round-trip (serialize → YAML text → parse), not just a\n * single `Converter.convert` pass in memory. This exercises the exact path a\n * corrupted or field-dropping serializer/parser pair would break, mirroring the\n * `aiClientToolConfig`/`annotations` field-drop class named in the brief.\n */\n\nimport '@fgv/ts-utils-jest';\nimport { Converters } from '@fgv/ts-utils';\nimport {\n BodyConverterRegistry,\n IBodyConverterRegistry,\n IMemoryEnvelope,\n parseMemoryFile,\n serializeMemoryFile\n} from '../../../index';\n\nconst kind = 'note' as IMemoryEnvelope['kind'];\n\nfunction registry(): IBodyConverterRegistry {\n const reg = BodyConverterRegistry.create().orThrow();\n reg.register(kind, Converters.string);\n return reg;\n}\n\ndescribe('antagonist — full on-disk round-trip preserves every optional field', () => {\n // Wrong impl this catches: a serializer/parser pair where one side silently\n // drops an optional field (the exact class of bug the brief calls out for\n // `aiClientToolConfig`/`annotations`-shaped converters) — e.g. omitting\n // `temporal.invalid_at: null`, an edge's `valid_at`/`invalid_at`/`provenance`,\n // a `null` `embeddingRef`, or a provenance extension key, because the author\n // forgot to thread it through both the YAML emit AND the envelope Converter.\n test('every optional envelope/edge/provenance field set simultaneously survives a full YAML round-trip', () => {\n const envelope: IMemoryEnvelope = {\n id: 'doc-1' as IMemoryEnvelope['id'],\n entityId: 'doc-1' as IMemoryEnvelope['entityId'],\n kind,\n tags: ['t1', 't2'] as unknown as IMemoryEnvelope['tags'],\n links: [\n {\n type: 'rel' as never,\n target: 'doc-2' as never,\n confidence: 0.42,\n provenance: { source: 'agent', by: 'curator', extra: { nested: true } },\n valid_at: 111,\n\n invalid_at: null\n }\n ],\n created: 1000,\n updated: 2000,\n seq: 7,\n contentHash: 'abc123',\n provenance: {\n source: 'host-ingest',\n by: 'erik',\n model: 'gpt-5',\n confidence: 0.87,\n derivedFrom: 'turn-3' as never,\n // Opaque extension keys (per IProvenance's `[key: string]: unknown` arm).\n sentiment: { score: 0.5 },\n epistemic: 'belief'\n },\n\n temporal: { valid_at: 500, invalid_at: null },\n\n embeddingRef: null\n };\n\n const raw = serializeMemoryFile(envelope, 'the body text').orThrow();\n expect(parseMemoryFile(raw, registry())).toSucceedAndSatisfy((record) => {\n // Deep-equal the ENTIRE envelope, not field-by-field — a partial assertion\n // list is exactly how a single dropped field slips through review.\n expect(record.envelope).toEqual(envelope);\n expect(record.body).toBe('the body text');\n });\n });\n\n test('an absent temporal/embeddingRef/edge-optionals round-trips to fully absent (no null-vs-undefined drift)', () => {\n const envelope: IMemoryEnvelope = {\n id: 'doc-2' as IMemoryEnvelope['id'],\n entityId: 'doc-2' as IMemoryEnvelope['entityId'],\n kind,\n tags: [],\n links: [{ type: 'rel' as never, target: 'doc-3' as never }],\n created: 0,\n updated: 0,\n seq: 0,\n contentHash: '',\n provenance: { source: 'agent' }\n };\n const raw = serializeMemoryFile(envelope, 'body').orThrow();\n expect(parseMemoryFile(raw, registry())).toSucceedAndSatisfy((record) => {\n expect(record.envelope.temporal).toBeUndefined();\n expect(record.envelope.embeddingRef).toBeUndefined();\n expect(record.envelope.links[0].confidence).toBeUndefined();\n expect(record.envelope.links[0].provenance).toBeUndefined();\n expect(record.envelope.links[0].valid_at).toBeUndefined();\n expect(record.envelope.links[0].invalid_at).toBeUndefined();\n expect(record.envelope).toEqual(envelope);\n });\n });\n});\n"]}