@remnic/core 9.52.2 → 9.53.0

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 (255) hide show
  1. package/dist/access-admin-ops-surface.d.ts +5 -5
  2. package/dist/access-admin-ops-surface.js +22 -22
  3. package/dist/access-authorization-probe.d.ts +5 -5
  4. package/dist/access-authorization-probe.js +23 -23
  5. package/dist/access-boundary.d.ts +5 -5
  6. package/dist/access-boundary.js +22 -22
  7. package/dist/access-cli.js +54 -54
  8. package/dist/access-extraction-force-flush.d.ts +5 -5
  9. package/dist/access-extraction-force-flush.js +22 -22
  10. package/dist/access-http-lcm-compaction.d.ts +5 -5
  11. package/dist/access-http-lifecycle-flush.d.ts +5 -5
  12. package/dist/access-http-offline-stream.d.ts +5 -5
  13. package/dist/access-http-query.js +22 -22
  14. package/dist/access-http.d.ts +5 -5
  15. package/dist/access-http.js +29 -29
  16. package/dist/access-identity-continuity-surface.d.ts +4 -4
  17. package/dist/access-identity-continuity-surface.js +22 -22
  18. package/dist/access-lcm-surface.d.ts +5 -5
  19. package/dist/access-lcm-surface.js +22 -22
  20. package/dist/access-mcp.d.ts +5 -5
  21. package/dist/access-mcp.js +27 -27
  22. package/dist/access-namespace-preflight.js +22 -22
  23. package/dist/access-observe-write-surface.d.ts +5 -5
  24. package/dist/access-observe-write-surface.js +22 -22
  25. package/dist/access-offline-manifest.d.ts +4 -4
  26. package/dist/access-offline-manifest.js +8 -4
  27. package/dist/access-operations-batch.js +24 -24
  28. package/dist/access-operations.d.ts +8 -8
  29. package/dist/access-operations.js +26 -26
  30. package/dist/access-recall-concurrency.d.ts +5 -5
  31. package/dist/access-recall-concurrency.js +22 -22
  32. package/dist/access-recall-response.d.ts +5 -5
  33. package/dist/access-recall-response.js +22 -22
  34. package/dist/access-recall-surface.d.ts +5 -5
  35. package/dist/access-recall-surface.js +22 -22
  36. package/dist/access-schema.d.ts +88 -88
  37. package/dist/{access-service-Cbk3bNbD.d.ts → access-service-CgCjvbQu.d.ts} +3 -3
  38. package/dist/access-service-helpers.d.ts +5 -5
  39. package/dist/access-service.d.ts +5 -5
  40. package/dist/access-service.js +22 -22
  41. package/dist/access-surface-catalog.d.ts +5 -5
  42. package/dist/access-wearables-meetings-surface.d.ts +2 -2
  43. package/dist/active-recall.js +4 -4
  44. package/dist/{auto-sync-QFQGOEO2.js → auto-sync-JXEW7444.js} +6 -6
  45. package/dist/bootstrap.d.ts +4 -4
  46. package/dist/briefing.d.ts +1 -1
  47. package/dist/briefing.js +12 -12
  48. package/dist/buffer.d.ts +1 -1
  49. package/dist/causal-consolidation.js +13 -13
  50. package/dist/{chunk-MLXZD4WM.js → chunk-2GFK22OZ.js} +2 -2
  51. package/dist/{chunk-D5YOZ66N.js → chunk-3DWTDC54.js} +20 -20
  52. package/dist/{chunk-RANKJI5T.js → chunk-3JLPGJ43.js} +2 -2
  53. package/dist/chunk-5FVVY2EB.js +27 -0
  54. package/dist/chunk-5FVVY2EB.js.map +1 -0
  55. package/dist/{chunk-GXSOUIA4.js → chunk-5WQMORU5.js} +4 -4
  56. package/dist/{chunk-J272WKX2.js → chunk-6HPHEFXT.js} +2 -2
  57. package/dist/{chunk-B6LZ4IBH.js → chunk-6LAPLZOS.js} +2 -2
  58. package/dist/{chunk-WD5B5AIE.js → chunk-6R4G7N4S.js} +118 -173
  59. package/dist/chunk-6R4G7N4S.js.map +1 -0
  60. package/dist/{chunk-VXCNQYTC.js → chunk-APPFNGX6.js} +2 -2
  61. package/dist/{chunk-RMHMUUBW.js → chunk-AY43LBE4.js} +104 -48
  62. package/dist/{chunk-RMHMUUBW.js.map → chunk-AY43LBE4.js.map} +1 -1
  63. package/dist/{chunk-GXFSQ6SD.js → chunk-C4ZNGVK3.js} +2 -2
  64. package/dist/{chunk-JSEJH2YQ.js → chunk-CGVHPCP2.js} +2 -2
  65. package/dist/{chunk-BQIFZ2MP.js → chunk-CX5SW2EW.js} +41 -24
  66. package/dist/chunk-CX5SW2EW.js.map +1 -0
  67. package/dist/{chunk-TEMSWHQD.js → chunk-DZD576ZU.js} +5 -5
  68. package/dist/{chunk-AMTEYNYK.js → chunk-EVWWT3KE.js} +4 -4
  69. package/dist/{chunk-XAPMEU3K.js → chunk-FHFERRYE.js} +2 -2
  70. package/dist/{chunk-P2KBDJJ6.js → chunk-G2LUMAUF.js} +4 -3
  71. package/dist/{chunk-P2KBDJJ6.js.map → chunk-G2LUMAUF.js.map} +1 -1
  72. package/dist/{chunk-AXIDQG5A.js → chunk-G4T7L557.js} +5 -5
  73. package/dist/{chunk-7M6NYQWB.js → chunk-G7Z6EENO.js} +4 -4
  74. package/dist/{chunk-YXKVLYXU.js → chunk-HWRD23DW.js} +2 -2
  75. package/dist/{chunk-SOEN6KCF.js → chunk-IBKR2TFR.js} +2 -2
  76. package/dist/{chunk-R3DJVOSM.js → chunk-JDSVLVCG.js} +8 -1
  77. package/dist/chunk-JDSVLVCG.js.map +1 -0
  78. package/dist/{chunk-F4EU6VUC.js → chunk-JDZPNCMA.js} +2 -2
  79. package/dist/{chunk-DLG5V6ND.js → chunk-K4QS7LXU.js} +4 -7
  80. package/dist/chunk-K4QS7LXU.js.map +1 -0
  81. package/dist/{chunk-BAKXNQ3C.js → chunk-KRTY3ENI.js} +2 -2
  82. package/dist/{chunk-NK5RGJ65.js → chunk-L4DLLKPN.js} +2 -2
  83. package/dist/{chunk-VFSAWRMZ.js → chunk-LJS7KUPV.js} +2 -2
  84. package/dist/{chunk-JJRVPWFY.js → chunk-MLPTFODV.js} +166 -37
  85. package/dist/chunk-MLPTFODV.js.map +1 -0
  86. package/dist/{chunk-NOEMGE56.js → chunk-NH5IR2WM.js} +2 -2
  87. package/dist/{chunk-5V6IXHPI.js → chunk-NSVDUN76.js} +2 -2
  88. package/dist/{chunk-IARXXZU4.js → chunk-P7XI2AQ3.js} +2 -2
  89. package/dist/{chunk-W7Y5JDVB.js → chunk-PDDTI7IC.js} +4 -4
  90. package/dist/{chunk-QINJITDS.js → chunk-PX2LSHYM.js} +2 -2
  91. package/dist/{chunk-O5FFBO2M.js → chunk-QAVH6X2A.js} +2 -2
  92. package/dist/{chunk-HDIUYMSB.js → chunk-QWGE2PUH.js} +6 -6
  93. package/dist/{chunk-X57M2Q4I.js → chunk-RDJRFDJY.js} +3 -3
  94. package/dist/{chunk-RYLEDKQ2.js → chunk-RUMNY5DT.js} +3 -3
  95. package/dist/{chunk-BKIFTXHR.js → chunk-RZ4XGKOJ.js} +2 -2
  96. package/dist/{chunk-66SE3BNH.js → chunk-SUPEYLH2.js} +2 -2
  97. package/dist/{chunk-BF6SXNQJ.js → chunk-T2JHZWH6.js} +20 -20
  98. package/dist/{chunk-VHFXP27N.js → chunk-THPPCISA.js} +60 -23
  99. package/dist/chunk-THPPCISA.js.map +1 -0
  100. package/dist/{chunk-QPOHMO6M.js → chunk-UES4BHA2.js} +2 -2
  101. package/dist/{chunk-X7XEDOSL.js → chunk-VLL43QEQ.js} +8 -8
  102. package/dist/{chunk-QBSFOKY6.js → chunk-VOELBUS4.js} +4 -4
  103. package/dist/{chunk-2LY6GIIE.js → chunk-WDBX6VER.js} +2 -2
  104. package/dist/{chunk-BMEYMKWX.js → chunk-WM65LU67.js} +2 -2
  105. package/dist/{chunk-WLRD2TXF.js → chunk-WOPA3SGI.js} +4 -4
  106. package/dist/{chunk-5UZ6UCOZ.js → chunk-XUAMCLY4.js} +3 -3
  107. package/dist/{chunk-7HXZJAID.js → chunk-YGLY6QFV.js} +3 -3
  108. package/dist/{chunk-2OIQDTDC.js → chunk-ZPJ4QSGP.js} +2 -2
  109. package/dist/{chunk-3OY7KXHM.js → chunk-ZYLC7NFO.js} +3 -3
  110. package/dist/{cli-DMAFI0qt.d.ts → cli-D-HiEYhl.d.ts} +3 -3
  111. package/dist/cli.d.ts +6 -6
  112. package/dist/cli.js +41 -41
  113. package/dist/compounding/engine.d.ts +1 -1
  114. package/dist/compounding/engine.js +13 -13
  115. package/dist/config.js +4 -4
  116. package/dist/connectors/codex-materialize-runner.js +12 -12
  117. package/dist/connectors/index.js +12 -12
  118. package/dist/consolidation-provenance-check.d.ts +1 -1
  119. package/dist/consolidation-undo.d.ts +1 -1
  120. package/dist/content-hash.d.ts +12 -2
  121. package/dist/content-hash.js +9 -3
  122. package/dist/contradiction/index.d.ts +1 -1
  123. package/dist/contradiction/index.js +3 -3
  124. package/dist/convergence-refresh.d.ts +1 -1
  125. package/dist/convergence-refresh.js +14 -14
  126. package/dist/corpus-watermark.js +14 -14
  127. package/dist/entity-retrieval.d.ts +1 -1
  128. package/dist/entity-retrieval.js +12 -12
  129. package/dist/explicit-capture.d.ts +4 -4
  130. package/dist/explicit-capture.js +8 -8
  131. package/dist/external-wiki-access.d.ts +5 -5
  132. package/dist/external-wiki-access.js +23 -23
  133. package/dist/external-wiki-mcp-tools.d.ts +5 -5
  134. package/dist/extraction-normalization.js +2 -2
  135. package/dist/extraction-source-grounding.js +3 -3
  136. package/dist/extraction.js +16 -16
  137. package/dist/{forget-HDS4N6CG.js → forget-EHGGJZLX.js} +3 -2
  138. package/dist/{forget-HDS4N6CG.js.map → forget-EHGGJZLX.js.map} +1 -1
  139. package/dist/harmonic-construction.js +2 -2
  140. package/dist/harmonic-retrieval.js +4 -4
  141. package/dist/index.d.ts +684 -684
  142. package/dist/index.js +66 -66
  143. package/dist/maintenance/memory-governance.js +12 -12
  144. package/dist/maintenance/rebuild-memory-lifecycle-ledger.d.ts +1 -1
  145. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +12 -12
  146. package/dist/maintenance/rebuild-memory-projection.d.ts +1 -1
  147. package/dist/maintenance/rebuild-memory-projection.js +13 -13
  148. package/dist/{maintenance-kBII2qUI.d.ts → maintenance-CN_Ct-Hr.d.ts} +1 -1
  149. package/dist/mcp-memory-inspector-app.d.ts +5 -5
  150. package/dist/memory-worth-outcomes.d.ts +1 -1
  151. package/dist/namespaces/migrate.d.ts +1 -1
  152. package/dist/namespaces/migrate.js +13 -13
  153. package/dist/namespaces/storage.d.ts +1 -1
  154. package/dist/namespaces/storage.js +12 -12
  155. package/dist/operator-doctor-corpus.js +15 -15
  156. package/dist/operator-toolkit.d.ts +1 -1
  157. package/dist/operator-toolkit.js +20 -20
  158. package/dist/orchestration/compression-guideline-coordinator.d.ts +1 -1
  159. package/dist/orchestration/maintenance.d.ts +2 -2
  160. package/dist/orchestration/maintenance.js +17 -17
  161. package/dist/{orchestrator-DHy5LtTy.d.ts → orchestrator-B3Xe8qu0.d.ts} +3 -3
  162. package/dist/orchestrator.d.ts +4 -4
  163. package/dist/orchestrator.js +54 -54
  164. package/dist/provenance.js +2 -2
  165. package/dist/reconcile/manifest.d.ts +3 -1
  166. package/dist/reconcile/manifest.js +7 -3
  167. package/dist/{resolution-MDPNOEG6.js → resolution-MMOD5WC4.js} +4 -4
  168. package/dist/resume-bundles.js +5 -5
  169. package/dist/salvage-envelope.js +3 -3
  170. package/dist/schemas.d.ts +116 -116
  171. package/dist/semantic-consolidation.js +13 -13
  172. package/dist/semantic-rule-promotion.js +12 -12
  173. package/dist/semantic-rule-verifier.js +12 -12
  174. package/dist/{service-PT2_z7xm.d.ts → service-CQGEqL1g.d.ts} +1 -1
  175. package/dist/shared-context/manager.d.ts +8 -8
  176. package/dist/source-agent-qualifier.js +14 -14
  177. package/dist/{storage-BmVudhk4.d.ts → storage-DmkCjo0l.d.ts} +36 -32
  178. package/dist/storage.d.ts +1 -1
  179. package/dist/storage.js +14 -14
  180. package/dist/structured-attributes.d.ts +3 -1
  181. package/dist/structured-attributes.js +3 -1
  182. package/dist/tier-migration.d.ts +1 -1
  183. package/dist/transfer/types.d.ts +66 -66
  184. package/dist/verified-recall.js +12 -12
  185. package/dist/write-envelope.js +2 -2
  186. package/package.json +2 -2
  187. package/src/access-offline-manifest.ts +1 -0
  188. package/src/access-service-offline-file-content.test.ts +1 -0
  189. package/src/content-hash.ts +34 -1
  190. package/src/dedup/index.test.ts +56 -38
  191. package/src/dedup/index.ts +85 -38
  192. package/src/lifecycle/tombstones.test.ts +763 -16
  193. package/src/lifecycle/tombstones.ts +217 -50
  194. package/src/reconcile/manifest.test.ts +158 -1
  195. package/src/reconcile/manifest.ts +76 -22
  196. package/src/storage/content-hash-index.ts +56 -40
  197. package/src/storage/entity-ref-repair.ts +3 -3
  198. package/src/storage/memory-read-store.ts +9 -0
  199. package/src/storage/tombstone-blocked-capture-index.ts +4 -9
  200. package/src/storage-contract/content-hash.test.ts +13 -0
  201. package/src/storage-fact-hash-index-defer.test.ts +1 -1
  202. package/src/storage.ts +107 -93
  203. package/src/structured-attributes.ts +11 -0
  204. package/dist/chunk-BQIFZ2MP.js.map +0 -1
  205. package/dist/chunk-DLG5V6ND.js.map +0 -1
  206. package/dist/chunk-JJRVPWFY.js.map +0 -1
  207. package/dist/chunk-R3DJVOSM.js.map +0 -1
  208. package/dist/chunk-VHFXP27N.js.map +0 -1
  209. package/dist/chunk-W5H6ZRPT.js +0 -14
  210. package/dist/chunk-W5H6ZRPT.js.map +0 -1
  211. package/dist/chunk-WD5B5AIE.js.map +0 -1
  212. /package/dist/{auto-sync-QFQGOEO2.js.map → auto-sync-JXEW7444.js.map} +0 -0
  213. /package/dist/{chunk-MLXZD4WM.js.map → chunk-2GFK22OZ.js.map} +0 -0
  214. /package/dist/{chunk-D5YOZ66N.js.map → chunk-3DWTDC54.js.map} +0 -0
  215. /package/dist/{chunk-RANKJI5T.js.map → chunk-3JLPGJ43.js.map} +0 -0
  216. /package/dist/{chunk-GXSOUIA4.js.map → chunk-5WQMORU5.js.map} +0 -0
  217. /package/dist/{chunk-J272WKX2.js.map → chunk-6HPHEFXT.js.map} +0 -0
  218. /package/dist/{chunk-B6LZ4IBH.js.map → chunk-6LAPLZOS.js.map} +0 -0
  219. /package/dist/{chunk-VXCNQYTC.js.map → chunk-APPFNGX6.js.map} +0 -0
  220. /package/dist/{chunk-GXFSQ6SD.js.map → chunk-C4ZNGVK3.js.map} +0 -0
  221. /package/dist/{chunk-JSEJH2YQ.js.map → chunk-CGVHPCP2.js.map} +0 -0
  222. /package/dist/{chunk-TEMSWHQD.js.map → chunk-DZD576ZU.js.map} +0 -0
  223. /package/dist/{chunk-AMTEYNYK.js.map → chunk-EVWWT3KE.js.map} +0 -0
  224. /package/dist/{chunk-XAPMEU3K.js.map → chunk-FHFERRYE.js.map} +0 -0
  225. /package/dist/{chunk-AXIDQG5A.js.map → chunk-G4T7L557.js.map} +0 -0
  226. /package/dist/{chunk-7M6NYQWB.js.map → chunk-G7Z6EENO.js.map} +0 -0
  227. /package/dist/{chunk-YXKVLYXU.js.map → chunk-HWRD23DW.js.map} +0 -0
  228. /package/dist/{chunk-SOEN6KCF.js.map → chunk-IBKR2TFR.js.map} +0 -0
  229. /package/dist/{chunk-F4EU6VUC.js.map → chunk-JDZPNCMA.js.map} +0 -0
  230. /package/dist/{chunk-BAKXNQ3C.js.map → chunk-KRTY3ENI.js.map} +0 -0
  231. /package/dist/{chunk-NK5RGJ65.js.map → chunk-L4DLLKPN.js.map} +0 -0
  232. /package/dist/{chunk-VFSAWRMZ.js.map → chunk-LJS7KUPV.js.map} +0 -0
  233. /package/dist/{chunk-NOEMGE56.js.map → chunk-NH5IR2WM.js.map} +0 -0
  234. /package/dist/{chunk-5V6IXHPI.js.map → chunk-NSVDUN76.js.map} +0 -0
  235. /package/dist/{chunk-IARXXZU4.js.map → chunk-P7XI2AQ3.js.map} +0 -0
  236. /package/dist/{chunk-W7Y5JDVB.js.map → chunk-PDDTI7IC.js.map} +0 -0
  237. /package/dist/{chunk-QINJITDS.js.map → chunk-PX2LSHYM.js.map} +0 -0
  238. /package/dist/{chunk-O5FFBO2M.js.map → chunk-QAVH6X2A.js.map} +0 -0
  239. /package/dist/{chunk-HDIUYMSB.js.map → chunk-QWGE2PUH.js.map} +0 -0
  240. /package/dist/{chunk-X57M2Q4I.js.map → chunk-RDJRFDJY.js.map} +0 -0
  241. /package/dist/{chunk-RYLEDKQ2.js.map → chunk-RUMNY5DT.js.map} +0 -0
  242. /package/dist/{chunk-BKIFTXHR.js.map → chunk-RZ4XGKOJ.js.map} +0 -0
  243. /package/dist/{chunk-66SE3BNH.js.map → chunk-SUPEYLH2.js.map} +0 -0
  244. /package/dist/{chunk-BF6SXNQJ.js.map → chunk-T2JHZWH6.js.map} +0 -0
  245. /package/dist/{chunk-QPOHMO6M.js.map → chunk-UES4BHA2.js.map} +0 -0
  246. /package/dist/{chunk-X7XEDOSL.js.map → chunk-VLL43QEQ.js.map} +0 -0
  247. /package/dist/{chunk-QBSFOKY6.js.map → chunk-VOELBUS4.js.map} +0 -0
  248. /package/dist/{chunk-2LY6GIIE.js.map → chunk-WDBX6VER.js.map} +0 -0
  249. /package/dist/{chunk-BMEYMKWX.js.map → chunk-WM65LU67.js.map} +0 -0
  250. /package/dist/{chunk-WLRD2TXF.js.map → chunk-WOPA3SGI.js.map} +0 -0
  251. /package/dist/{chunk-5UZ6UCOZ.js.map → chunk-XUAMCLY4.js.map} +0 -0
  252. /package/dist/{chunk-7HXZJAID.js.map → chunk-YGLY6QFV.js.map} +0 -0
  253. /package/dist/{chunk-2OIQDTDC.js.map → chunk-ZPJ4QSGP.js.map} +0 -0
  254. /package/dist/{chunk-3OY7KXHM.js.map → chunk-ZYLC7NFO.js.map} +0 -0
  255. /package/dist/{resolution-MDPNOEG6.js.map → resolution-MMOD5WC4.js.map} +0 -0
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdtemp, mkdir, rm, symlink, writeFile } from "node:fs/promises";
2
+ import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import path from "node:path";
5
5
  import test from "node:test";
@@ -14,25 +14,13 @@ test("findContradictions detects can versus cannot statements", async () => {
14
14
  await mkdir(factsDir);
15
15
  await writeFile(
16
16
  path.join(factsDir, "can.md"),
17
- [
18
- "---",
19
- "id: mem-can",
20
- "category: fact",
21
- "---",
22
- "The user can access production.",
23
- ].join("\n"),
24
- "utf8",
17
+ ["---", "id: mem-can", "category: fact", "---", "The user can access production."].join("\n"),
18
+ "utf8"
25
19
  );
26
20
  await writeFile(
27
21
  path.join(factsDir, "cannot.md"),
28
- [
29
- "---",
30
- "id: mem-cannot",
31
- "category: fact",
32
- "---",
33
- "The user cannot access production.",
34
- ].join("\n"),
35
- "utf8",
22
+ ["---", "id: mem-cannot", "category: fact", "---", "The user cannot access production."].join("\n"),
23
+ "utf8"
36
24
  );
37
25
 
38
26
  const result = findContradictions({ memoryDir, categories: ["facts"] });
@@ -58,14 +46,8 @@ test("dedup public numeric options are normalized before scanning", async () =>
58
46
  ] as const) {
59
47
  await writeFile(
60
48
  path.join(factsDir, fileName),
61
- [
62
- "---",
63
- `id: ${id}`,
64
- "category: fact",
65
- "---",
66
- content,
67
- ].join("\n"),
68
- "utf8",
49
+ ["---", `id: ${id}`, "category: fact", "---", content].join("\n"),
50
+ "utf8"
69
51
  );
70
52
  }
71
53
 
@@ -95,6 +77,51 @@ test("dedup public numeric options are normalized before scanning", async () =>
95
77
  }
96
78
  });
97
79
 
80
+ test("dedup keeps duplicate and distinct pairs across Unicode scripts", async () => {
81
+ const memoryDir = await mkdtemp(path.join(tmpdir(), "remnic-dedup-unicode-"));
82
+
83
+ try {
84
+ const factsDir = path.join(memoryDir, "facts");
85
+ await mkdir(factsDir);
86
+ const entries = [
87
+ ["english-a.md", "mem-english-a", "The user prefers tea."],
88
+ ["english-b.md", "mem-english-b", "The user prefers tea"],
89
+ ["english-c.md", "mem-english-c", "The user prefers coffee."],
90
+ ["japanese-a.md", "mem-japanese-a", "利用者は紅茶を好む。"],
91
+ ["japanese-b.md", "mem-japanese-b", "利用者は紅茶を好む。"],
92
+ ["japanese-c.md", "mem-japanese-c", "利用者は珈琲を好む。"],
93
+ ["korean-a.md", "mem-korean-a", "사용자는홍차를좋아한다"],
94
+ ["korean-b.md", "mem-korean-b", "사용자는홍차를좋아한다"],
95
+ ["korean-c.md", "mem-korean-c", "사용자는커피를좋아한다"],
96
+ ["korean-punctuation.md", "mem-korean-punctuation", "홍차・커피"],
97
+ ["korean-hyphen.md", "mem-korean-hyphen", "홍차-커피"],
98
+ ["korean-joined.md", "mem-korean-joined", "홍차커피"],
99
+ ["japanese-boundary-a.md", "mem-japanese-boundary-a", "甲・乙丙"],
100
+ ["japanese-boundary-b.md", "mem-japanese-boundary-b", "甲乙・丙"],
101
+ ] as const;
102
+
103
+ for (const [fileName, id, content] of entries) {
104
+ await writeFile(
105
+ path.join(factsDir, fileName),
106
+ ["---", `id: ${id}`, "category: fact", "---", content].join("\n"),
107
+ "utf8"
108
+ );
109
+ }
110
+
111
+ const result = findDuplicates({ memoryDir, categories: ["facts"] });
112
+
113
+ assert.equal(result.scanned, entries.length);
114
+ assert.equal(result.duplicates.length, 3);
115
+ assert.deepEqual(result.duplicates.map(({ left, right }) => [left.id, right.id]).sort(), [
116
+ ["mem-english-a", "mem-english-b"],
117
+ ["mem-japanese-a", "mem-japanese-b"],
118
+ ["mem-korean-a", "mem-korean-b"],
119
+ ]);
120
+ } finally {
121
+ await rm(memoryDir, { recursive: true, force: true });
122
+ }
123
+ });
124
+
98
125
  test("dedup scans reject symlinked category directories outside memoryDir", async (t) => {
99
126
  if (process.platform === "win32") {
100
127
  t.skip("directory symlink setup is platform-specific");
@@ -107,24 +134,15 @@ test("dedup scans reject symlinked category directories outside memoryDir", asyn
107
134
  try {
108
135
  await writeFile(
109
136
  path.join(outsideDir, "outside.md"),
110
- [
111
- "---",
112
- "id: outside",
113
- "category: fact",
114
- "---",
115
- "Outside memory should not be scanned.",
116
- ].join("\n"),
117
- "utf8",
137
+ ["---", "id: outside", "category: fact", "---", "Outside memory should not be scanned."].join("\n"),
138
+ "utf8"
118
139
  );
119
140
  await symlink(outsideDir, path.join(memoryDir, "facts"), "dir");
120
141
 
121
- assert.throws(
122
- () => findDuplicates({ memoryDir, categories: ["facts"] }),
123
- /symlinked memory category directory/,
124
- );
142
+ assert.throws(() => findDuplicates({ memoryDir, categories: ["facts"] }), /symlinked memory category directory/);
125
143
  assert.throws(
126
144
  () => findContradictions({ memoryDir, categories: ["facts"] }),
127
- /symlinked memory category directory/,
145
+ /symlinked memory category directory/
128
146
  );
129
147
  } finally {
130
148
  await rm(memoryDir, { recursive: true, force: true });
@@ -5,9 +5,10 @@
5
5
  * against existing memories. Can be used standalone or via curation.
6
6
  */
7
7
 
8
+ import crypto from "node:crypto";
8
9
  import fs from "node:fs";
9
10
  import path from "node:path";
10
- import crypto from "node:crypto";
11
+ import { normalizeRecallTokenSet } from "../recall-tokenization.js";
11
12
  import { ALL_CATEGORY_DIRS } from "../utils/category-dir.js";
12
13
 
13
14
  // ── Types ────────────────────────────────────────────────────────────────────
@@ -89,20 +90,11 @@ const DEFAULT_DEDUP_THRESHOLD = 0.85;
89
90
  const DEFAULT_MAX_LOAD = 10000;
90
91
 
91
92
  function normalizeThreshold(value: number | undefined, defaultValue: number): number {
92
- return typeof value === "number" &&
93
- Number.isFinite(value) &&
94
- value >= 0 &&
95
- value <= 1
96
- ? value
97
- : defaultValue;
93
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1 ? value : defaultValue;
98
94
  }
99
95
 
100
96
  function normalizeMaxLoad(value: number | undefined, defaultValue: number): number {
101
- return typeof value === "number" &&
102
- Number.isInteger(value) &&
103
- value >= 0
104
- ? value
105
- : defaultValue;
97
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : defaultValue;
106
98
  }
107
99
 
108
100
  export function findDuplicates(options: DedupOptions): DedupResult {
@@ -163,26 +155,25 @@ export function findContradictions(options: ContradictionOptions): Contradiction
163
155
  // ── Similarity computation ───────────────────────────────────────────────────
164
156
 
165
157
  function computeSimilarity(a: string, b: string): number {
166
- // Normalize
167
158
  const normA = normalize(a);
168
159
  const normB = normalize(b);
160
+ const punctuationBoundariesA = embeddedPunctuationSignature(a);
161
+ const punctuationBoundariesB = embeddedPunctuationSignature(b);
162
+
163
+ // Compare punctuation and positions before normalization's exact/hash paths.
164
+ if (punctuationBoundariesA !== punctuationBoundariesB) return 0;
169
165
 
170
- // Exact match
171
166
  if (normA === normB) return 1;
172
167
 
173
- // Hash-based exact match
174
168
  if (hashContent(normA) === hashContent(normB)) return 0.99;
175
-
176
- // Substring containment
177
169
  if (normA.length > 50 && normB.length > 50) {
178
170
  if (normA.includes(normB.slice(0, 40)) || normB.includes(normA.slice(0, 40))) {
179
171
  return 0.9;
180
172
  }
181
173
  }
182
174
 
183
- // Word overlap (Jaccard)
184
- const wordsA = new Set(normA.split(/\s+/));
185
- const wordsB = new Set(normB.split(/\s+/));
175
+ const wordsA = tokenizeForSimilarity(normA);
176
+ const wordsB = tokenizeForSimilarity(normB);
186
177
  const intersection = new Set([...wordsA].filter((w) => wordsB.has(w)));
187
178
  const union = new Set([...wordsA, ...wordsB]);
188
179
 
@@ -190,25 +181,89 @@ function computeSimilarity(a: string, b: string): number {
190
181
  return intersection.size / union.size;
191
182
  }
192
183
 
184
+ function embeddedPunctuationSignature(text: string): string {
185
+ const chars = [...text.normalize("NFC")];
186
+ const significant = /[\p{L}\p{M}\p{N}]/u;
187
+ const ignored = /[\s']/u;
188
+ const boundaries: string[] = [];
189
+ let position = 0;
190
+ for (let index = 0; index < chars.length;) {
191
+ const char = chars[index];
192
+ if (char === undefined) break;
193
+ if (significant.test(char)) {
194
+ position++;
195
+ index++;
196
+ continue;
197
+ }
198
+ if (ignored.test(char)) {
199
+ index++;
200
+ continue;
201
+ }
202
+ const start = index;
203
+ while (index < chars.length) {
204
+ const next = chars[index];
205
+ if (next === undefined || significant.test(next) || ignored.test(next)) break;
206
+ index++;
207
+ }
208
+ const before = chars[start - 1];
209
+ const after = chars[index];
210
+ if (before && after && significant.test(before) && significant.test(after)) {
211
+ boundaries.push(`${position}:${chars.slice(start, index).join("")}`);
212
+ }
213
+ }
214
+ return boundaries.join("\u0000");
215
+ }
216
+
193
217
  function normalize(text: string): string {
194
218
  return text
219
+ .normalize("NFC")
195
220
  .toLowerCase()
196
- .replace(/[^\w\s']/g, "")
221
+ .replace(/[^\p{ASCII}\p{L}\p{M}\p{N}\s']/gu, " ")
222
+ .replace(/[^\p{L}\p{M}\p{N}\s']/gu, "")
197
223
  .replace(/\s+/g, " ")
198
224
  .trim();
199
225
  }
200
226
 
227
+ function tokenizeForSimilarity(text: string): Set<string> {
228
+ if (/\p{Script=Hangul}/u.test(text)) {
229
+ const tokens = normalizeRecallTokenSet(text);
230
+ for (const segment of text.match(/\p{Script=Hangul}+/gu) ?? []) {
231
+ const chars = [...segment];
232
+ for (const size of [1, 2, 3, 4]) {
233
+ for (let index = 0; index <= chars.length - size; index += 1) {
234
+ tokens.add(chars.slice(index, index + size).join(""));
235
+ }
236
+ }
237
+ }
238
+ return tokens;
239
+ }
240
+ if (/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u.test(text)) {
241
+ return normalizeRecallTokenSet(text);
242
+ }
243
+ return text.length === 0 ? new Set() : new Set(text.split(/\s+/));
244
+ }
245
+
201
246
  // ── Contradiction detection ──────────────────────────────────────────────────
202
247
 
203
248
  const NEGATION_WORDS = new Set([
204
- "not", "don't", "doesn't", "isn't", "aren't", "won't", "can't", "cannot",
205
- "never", "no", "none", "neither", "nor", "nothing", "nowhere",
249
+ "not",
250
+ "don't",
251
+ "doesn't",
252
+ "isn't",
253
+ "aren't",
254
+ "won't",
255
+ "can't",
256
+ "cannot",
257
+ "never",
258
+ "no",
259
+ "none",
260
+ "neither",
261
+ "nor",
262
+ "nothing",
263
+ "nowhere",
206
264
  ]);
207
265
 
208
- function detectContradiction(
209
- a: MemoryEntry,
210
- b: MemoryEntry,
211
- ): ContradictionPair | null {
266
+ function detectContradiction(a: MemoryEntry, b: MemoryEntry): ContradictionPair | null {
212
267
  const normA = normalize(a.content);
213
268
  const normB = normalize(b.content);
214
269
 
@@ -271,11 +326,7 @@ function stripNegation(text: string): string {
271
326
 
272
327
  // ── Memory loading ───────────────────────────────────────────────────────────
273
328
 
274
- function loadMemories(
275
- memoryDir: string,
276
- categories?: string[],
277
- maxLoad = 10000,
278
- ): MemoryEntry[] {
329
+ function loadMemories(memoryDir: string, categories?: string[], maxLoad = 10000): MemoryEntry[] {
279
330
  const result: MemoryEntry[] = [];
280
331
  const allCategories = categories ?? ALL_CATEGORY_DIRS;
281
332
  if (!fs.existsSync(memoryDir)) return result;
@@ -322,11 +373,7 @@ function hashContent(content: string): string {
322
373
  return crypto.createHash("sha256").update(content).digest("hex").slice(0, 16);
323
374
  }
324
375
 
325
- function readFileSafe(
326
- filePath: string,
327
- memoryRootReal: string,
328
- categoryRootReal: string,
329
- ): string | null {
376
+ function readFileSafe(filePath: string, memoryRootReal: string, categoryRootReal: string): string | null {
330
377
  try {
331
378
  const fileStat = fs.lstatSync(filePath);
332
379
  if (fileStat.isSymbolicLink()) {
@@ -373,7 +420,7 @@ function walkMdFiles(
373
420
  dir: string,
374
421
  memoryRootReal: string,
375
422
  categoryRootReal: string,
376
- callback: (filePath: string) => void,
423
+ callback: (filePath: string) => void
377
424
  ): void {
378
425
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
379
426
  const fullPath = path.join(dir, entry.name);