@graphorin/memory 0.6.0 → 0.7.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 (227) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +24 -7
  3. package/dist/conflict/index.js +0 -1
  4. package/dist/conflict/pipeline.js +0 -1
  5. package/dist/conflict/pipeline.js.map +1 -1
  6. package/dist/conflict/stages/stage3-heuristic-regex.js +0 -1
  7. package/dist/conflict/stages/stage3-heuristic-regex.js.map +1 -1
  8. package/dist/conflict/types.d.ts +1 -1
  9. package/dist/conflict/types.js.map +1 -1
  10. package/dist/consolidator/index.js +2 -0
  11. package/dist/consolidator/index.js.map +1 -1
  12. package/dist/consolidator/lock.js +7 -1
  13. package/dist/consolidator/lock.js.map +1 -1
  14. package/dist/consolidator/phases/deep.js +19 -12
  15. package/dist/consolidator/phases/deep.js.map +1 -1
  16. package/dist/consolidator/phases/induce.d.ts.map +1 -1
  17. package/dist/consolidator/phases/induce.js +2 -3
  18. package/dist/consolidator/phases/induce.js.map +1 -1
  19. package/dist/consolidator/phases/reflect.js +35 -8
  20. package/dist/consolidator/phases/reflect.js.map +1 -1
  21. package/dist/consolidator/phases/standard.js +199 -28
  22. package/dist/consolidator/phases/standard.js.map +1 -1
  23. package/dist/consolidator/reconcile.js +16 -15
  24. package/dist/consolidator/reconcile.js.map +1 -1
  25. package/dist/consolidator/runtime.d.ts.map +1 -1
  26. package/dist/consolidator/runtime.js +129 -47
  27. package/dist/consolidator/runtime.js.map +1 -1
  28. package/dist/consolidator/scheduler.d.ts +17 -3
  29. package/dist/consolidator/scheduler.d.ts.map +1 -1
  30. package/dist/consolidator/scheduler.js.map +1 -1
  31. package/dist/consolidator/types.d.ts +30 -1
  32. package/dist/consolidator/types.d.ts.map +1 -1
  33. package/dist/consolidator/types.js +10 -0
  34. package/dist/consolidator/types.js.map +1 -1
  35. package/dist/context-engine/compaction/clear-tool-results.d.ts +1 -1
  36. package/dist/context-engine/compaction/clear-tool-results.js +1 -1
  37. package/dist/context-engine/compaction/clear-tool-results.js.map +1 -1
  38. package/dist/context-engine/compaction/compactor.d.ts +1 -0
  39. package/dist/context-engine/compaction/compactor.d.ts.map +1 -1
  40. package/dist/context-engine/compaction/compactor.js +32 -8
  41. package/dist/context-engine/compaction/compactor.js.map +1 -1
  42. package/dist/context-engine/compaction/hooks/types.d.ts +1 -1
  43. package/dist/context-engine/compaction/index.d.ts +1 -0
  44. package/dist/context-engine/compaction/templates/marker.d.ts +33 -0
  45. package/dist/context-engine/compaction/templates/marker.d.ts.map +1 -0
  46. package/dist/context-engine/compaction/templates/marker.js +34 -0
  47. package/dist/context-engine/compaction/templates/marker.js.map +1 -0
  48. package/dist/context-engine/compaction/templates/summary-9-section.d.ts +1 -1
  49. package/dist/context-engine/compaction/templates/summary-9-section.d.ts.map +1 -1
  50. package/dist/context-engine/compaction/templates/summary-9-section.js +4 -3
  51. package/dist/context-engine/compaction/templates/summary-9-section.js.map +1 -1
  52. package/dist/context-engine/engine.d.ts +7 -176
  53. package/dist/context-engine/engine.d.ts.map +1 -1
  54. package/dist/context-engine/engine.js +0 -2
  55. package/dist/context-engine/engine.js.map +1 -1
  56. package/dist/context-engine/index.d.ts +8 -49
  57. package/dist/context-engine/io-types.d.ts +144 -0
  58. package/dist/context-engine/io-types.d.ts.map +1 -0
  59. package/dist/context-engine/locale-packs/types.d.ts +1 -1
  60. package/dist/context-engine/locale-packs/types.js.map +1 -1
  61. package/dist/context-engine/privacy-filter.d.ts +1 -2
  62. package/dist/context-engine/privacy-filter.d.ts.map +1 -1
  63. package/dist/context-engine/privacy-filter.js +1 -2
  64. package/dist/context-engine/privacy-filter.js.map +1 -1
  65. package/dist/errors/index.d.ts +2 -2
  66. package/dist/errors/index.js +2 -2
  67. package/dist/errors/index.js.map +1 -1
  68. package/dist/facade.d.ts +43 -52
  69. package/dist/facade.d.ts.map +1 -1
  70. package/dist/facade.js +5 -3
  71. package/dist/facade.js.map +1 -1
  72. package/dist/graph/entity-resolver.js +1 -1
  73. package/dist/graph/entity-resolver.js.map +1 -1
  74. package/dist/index.d.ts +17 -15
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +5 -7
  77. package/dist/index.js.map +1 -1
  78. package/dist/internal/envelope.js +57 -0
  79. package/dist/internal/envelope.js.map +1 -0
  80. package/dist/internal/injection-heuristics.js +0 -0
  81. package/dist/internal/injection-heuristics.js.map +1 -1
  82. package/dist/internal/llm-json.js +40 -0
  83. package/dist/internal/llm-json.js.map +1 -0
  84. package/dist/internal/storage-adapter.d.ts +23 -10
  85. package/dist/internal/storage-adapter.d.ts.map +1 -1
  86. package/dist/memory-interface.d.ts +131 -0
  87. package/dist/memory-interface.d.ts.map +1 -0
  88. package/dist/package.js +6 -0
  89. package/dist/package.js.map +1 -0
  90. package/dist/search/iterative.d.ts.map +1 -1
  91. package/dist/search/iterative.js +2 -3
  92. package/dist/search/iterative.js.map +1 -1
  93. package/dist/search/query-transform.d.ts.map +1 -1
  94. package/dist/search/query-transform.js +2 -3
  95. package/dist/search/query-transform.js.map +1 -1
  96. package/dist/tiers/episodic-memory.js +3 -3
  97. package/dist/tiers/episodic-memory.js.map +1 -1
  98. package/dist/tiers/index.d.ts +2 -2
  99. package/dist/tiers/insight-memory.js +1 -1
  100. package/dist/tiers/insight-memory.js.map +1 -1
  101. package/dist/tiers/procedural-memory.d.ts +1 -1
  102. package/dist/tiers/procedural-memory.js +2 -2
  103. package/dist/tiers/procedural-memory.js.map +1 -1
  104. package/dist/tiers/semantic-memory.d.ts +58 -4
  105. package/dist/tiers/semantic-memory.d.ts.map +1 -1
  106. package/dist/tiers/semantic-memory.js +103 -20
  107. package/dist/tiers/semantic-memory.js.map +1 -1
  108. package/dist/tools/block-tools.d.ts +19 -57
  109. package/dist/tools/block-tools.d.ts.map +1 -1
  110. package/dist/tools/block-tools.js.map +1 -1
  111. package/dist/tools/fact-tools.d.ts +53 -214
  112. package/dist/tools/fact-tools.d.ts.map +1 -1
  113. package/dist/tools/fact-tools.js +3 -2
  114. package/dist/tools/fact-tools.js.map +1 -1
  115. package/dist/tools/recall-tools.d.ts +34 -175
  116. package/dist/tools/recall-tools.d.ts.map +1 -1
  117. package/dist/tools/recall-tools.js +5 -4
  118. package/dist/tools/recall-tools.js.map +1 -1
  119. package/dist/tools/runbook-tools.d.ts +15 -58
  120. package/dist/tools/runbook-tools.d.ts.map +1 -1
  121. package/dist/tools/runbook-tools.js.map +1 -1
  122. package/dist/tools/types.d.ts +1 -1
  123. package/package.json +18 -17
  124. package/src/conflict/index.ts +29 -0
  125. package/src/conflict/locale-packs/en.ts +142 -0
  126. package/src/conflict/locale-packs/index.ts +17 -0
  127. package/src/conflict/locale-packs/types.ts +138 -0
  128. package/src/conflict/pipeline.ts +371 -0
  129. package/src/conflict/stages/helpers.ts +65 -0
  130. package/src/conflict/stages/stage1-exact-dedup.ts +40 -0
  131. package/src/conflict/stages/stage2-embedding-three-zone.ts +54 -0
  132. package/src/conflict/stages/stage3-heuristic-regex.ts +51 -0
  133. package/src/conflict/stages/stage4-subject-predicate.ts +46 -0
  134. package/src/conflict/stages/stage5-defer-to-deep.ts +34 -0
  135. package/src/conflict/types.ts +263 -0
  136. package/src/consolidator/budget.ts +312 -0
  137. package/src/consolidator/decay.ts +246 -0
  138. package/src/consolidator/dlq.ts +61 -0
  139. package/src/consolidator/errors.ts +87 -0
  140. package/src/consolidator/idempotency.ts +27 -0
  141. package/src/consolidator/index.ts +269 -0
  142. package/src/consolidator/lock.ts +141 -0
  143. package/src/consolidator/noise-filter.ts +235 -0
  144. package/src/consolidator/phases/deep.ts +308 -0
  145. package/src/consolidator/phases/induce.ts +423 -0
  146. package/src/consolidator/phases/learned-context.ts +233 -0
  147. package/src/consolidator/phases/light.ts +166 -0
  148. package/src/consolidator/phases/reflect.ts +504 -0
  149. package/src/consolidator/phases/standard.ts +1002 -0
  150. package/src/consolidator/reconcile.ts +273 -0
  151. package/src/consolidator/runtime.ts +1049 -0
  152. package/src/consolidator/scheduler.ts +182 -0
  153. package/src/consolidator/triggers.ts +134 -0
  154. package/src/consolidator/types.ts +622 -0
  155. package/src/context-engine/annotations.ts +161 -0
  156. package/src/context-engine/auto-recall.ts +92 -0
  157. package/src/context-engine/compaction/clear-tool-results.ts +232 -0
  158. package/src/context-engine/compaction/compactor.ts +403 -0
  159. package/src/context-engine/compaction/hooks/reanchor-persona-block.ts +55 -0
  160. package/src/context-engine/compaction/hooks/reanchor-pinned-facts.ts +66 -0
  161. package/src/context-engine/compaction/hooks/reanchor-project-rules.ts +57 -0
  162. package/src/context-engine/compaction/hooks/reanchor-recent-results.ts +116 -0
  163. package/src/context-engine/compaction/hooks/types.ts +68 -0
  164. package/src/context-engine/compaction/index.ts +60 -0
  165. package/src/context-engine/compaction/templates/marker.ts +33 -0
  166. package/src/context-engine/compaction/templates/summary-9-section.ts +223 -0
  167. package/src/context-engine/compaction/thresholds.ts +107 -0
  168. package/src/context-engine/compaction/types.ts +255 -0
  169. package/src/context-engine/engine.ts +970 -0
  170. package/src/context-engine/index.ts +118 -0
  171. package/src/context-engine/io-types.ts +162 -0
  172. package/src/context-engine/locale-packs/en.ts +107 -0
  173. package/src/context-engine/locale-packs/index.ts +19 -0
  174. package/src/context-engine/locale-packs/resolver.ts +197 -0
  175. package/src/context-engine/locale-packs/types.ts +177 -0
  176. package/src/context-engine/metadata.ts +114 -0
  177. package/src/context-engine/preambles/inbound-en.ts +19 -0
  178. package/src/context-engine/privacy-filter.ts +190 -0
  179. package/src/context-engine/templates/base-en.ts +19 -0
  180. package/src/context-engine/templates/composer.ts +91 -0
  181. package/src/context-engine/token-budget.ts +319 -0
  182. package/src/context-engine/token-counter.ts +157 -0
  183. package/src/errors/index.ts +273 -0
  184. package/src/facade.ts +750 -0
  185. package/src/graph/entity-resolver.ts +364 -0
  186. package/src/graph/index.ts +24 -0
  187. package/src/index.ts +259 -0
  188. package/src/internal/contextualize.ts +173 -0
  189. package/src/internal/embedder-binding.ts +53 -0
  190. package/src/internal/envelope.ts +70 -0
  191. package/src/internal/id.ts +23 -0
  192. package/src/internal/injection-heuristics.ts +0 -0
  193. package/src/internal/llm-json.ts +38 -0
  194. package/src/internal/spans.ts +35 -0
  195. package/src/internal/storage-adapter.ts +963 -0
  196. package/src/memory-interface.ts +158 -0
  197. package/src/migration/embedder-migration.ts +237 -0
  198. package/src/migration/index.ts +13 -0
  199. package/src/search/explain.ts +123 -0
  200. package/src/search/fit-weights.ts +109 -0
  201. package/src/search/graph-ppr.ts +38 -0
  202. package/src/search/index.ts +55 -0
  203. package/src/search/iterative.ts +658 -0
  204. package/src/search/query-transform.ts +285 -0
  205. package/src/search/rrf.ts +225 -0
  206. package/src/search/trust.ts +55 -0
  207. package/src/search/types.ts +50 -0
  208. package/src/tiers/episodic-memory.ts +463 -0
  209. package/src/tiers/index.ts +50 -0
  210. package/src/tiers/insight-memory.ts +208 -0
  211. package/src/tiers/procedural-memory.ts +482 -0
  212. package/src/tiers/semantic-memory.ts +1729 -0
  213. package/src/tiers/session-memory.ts +271 -0
  214. package/src/tiers/shared-memory.ts +62 -0
  215. package/src/tiers/working-memory.ts +368 -0
  216. package/src/tools/block-tools.ts +172 -0
  217. package/src/tools/fact-tools.ts +473 -0
  218. package/src/tools/index.ts +109 -0
  219. package/src/tools/recall-tools.ts +303 -0
  220. package/src/tools/runbook-tools.ts +107 -0
  221. package/src/tools/taint.ts +39 -0
  222. package/src/tools/types.ts +37 -0
  223. package/dist/conflict/locale-packs/index.js +0 -4
  224. package/dist/context-engine/compaction/index.js +0 -10
  225. package/dist/context-engine/index.d.ts.map +0 -1
  226. package/dist/context-engine/index.js +0 -25
  227. package/dist/context-engine/locale-packs/index.js +0 -4
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Bundled English locale pack used by the multi-stage conflict
3
+ * resolution pipeline (DEC-117 / ADR-018 ext / RB-02). The patterns
4
+ * cover the common change signals personal-assistant scenarios surface
5
+ * on a daily basis: relocation, job change, preference flips,
6
+ * relationship transitions, and health updates.
7
+ *
8
+ * Operators supplying a different locale register their own pack via
9
+ * {@link defineLocalePack}. The framework ships English by default so
10
+ * the pipeline always has a working baseline.
11
+ *
12
+ * @packageDocumentation
13
+ */
14
+
15
+ import { defineLocalePack, type LocalePack } from './types.js';
16
+
17
+ /**
18
+ * The default English `LocalePack`. Frozen - share the singleton.
19
+ *
20
+ * @stable
21
+ */
22
+ export const enLocalePack: LocalePack = defineLocalePack({
23
+ id: 'en',
24
+ supersedeMarkers: [
25
+ // Location.
26
+ { regex: /\bmoved (?:to|from)\b/i, kind: 'location', confidence: 0.9 },
27
+ {
28
+ regex: /\bnow (?:live|living|based|residing) (?:in|at)\b/i,
29
+ kind: 'location',
30
+ confidence: 0.9,
31
+ },
32
+ { regex: /\brelocated\b/i, kind: 'location', confidence: 0.85 },
33
+ { regex: /\bjust moved\b/i, kind: 'location', confidence: 0.85 },
34
+ { regex: /\bI live in\b/i, kind: 'location', confidence: 0.7 },
35
+
36
+ // Job.
37
+ { regex: /\b(?:got|started|landed)(?: a)?(?: new)? job\b/i, kind: 'job', confidence: 0.9 },
38
+ { regex: /\bgot promoted\b/i, kind: 'job', confidence: 0.9 },
39
+ { regex: /\bgot fired\b/i, kind: 'job', confidence: 0.9 },
40
+ { regex: /\b(?:I )?quit (?:my )?job\b/i, kind: 'job', confidence: 0.9 },
41
+ { regex: /\bnew employer\b/i, kind: 'job', confidence: 0.85 },
42
+ { regex: /\bnew job\b/i, kind: 'job', confidence: 0.8 },
43
+ { regex: /\bswitched (?:companies|jobs|employers)\b/i, kind: 'job', confidence: 0.9 },
44
+ { regex: /\bnow work(?:s|ing)? (?:at|for)\b/i, kind: 'job', confidence: 0.85 },
45
+
46
+ // Preference.
47
+ { regex: /\bswitched (?:from|to)\b/i, kind: 'preference', confidence: 0.85 },
48
+ { regex: /\bchanged my (?:mind|preference|opinion)\b/i, kind: 'preference', confidence: 0.85 },
49
+ {
50
+ regex: /\bused to (?:like|love|prefer|drink|eat|use)\b/i,
51
+ kind: 'preference',
52
+ confidence: 0.85,
53
+ },
54
+ { regex: /\bused to (?:live|work|study|play|travel)\b/i, kind: 'generic', confidence: 0.85 },
55
+ { regex: /\bI prefer\b/i, kind: 'preference', confidence: 0.6 },
56
+
57
+ // Relationship.
58
+ { regex: /\bgot (?:married|divorced|engaged)\b/i, kind: 'relationship', confidence: 0.9 },
59
+ { regex: /\bbroke up\b/i, kind: 'relationship', confidence: 0.85 },
60
+
61
+ // Health.
62
+ { regex: /\bdiagnosed (?:with|as)\b/i, kind: 'health', confidence: 0.85 },
63
+ { regex: /\brecovered from\b/i, kind: 'health', confidence: 0.85 },
64
+
65
+ // Generic.
66
+ { regex: /\bformerly\b/i, kind: 'generic', confidence: 0.7 },
67
+ { regex: /\bpreviously\b/i, kind: 'generic', confidence: 0.7 },
68
+ { regex: /\bused to be\b/i, kind: 'generic', confidence: 0.7 },
69
+ { regex: /\bis now\b/i, kind: 'generic', confidence: 0.7 },
70
+ ],
71
+ negationMarkers: [
72
+ { regex: /\bno longer\b/i, kind: 'preference', confidence: 0.85 },
73
+ { regex: /\bdoesn(?:'|\u2019)?t\b/i, confidence: 0.6 },
74
+ {
75
+ regex:
76
+ /\b(?:don(?:'|\u2019)?t|do not) (?:like|prefer|use|drink|eat|live|work|play|go|want)\b/i,
77
+ kind: 'preference',
78
+ confidence: 0.85,
79
+ },
80
+ { regex: /\bnever\b/i, confidence: 0.6 },
81
+ { regex: /\bnot anymore\b/i, kind: 'preference', confidence: 0.85 },
82
+ { regex: /\bisn(?:'|\u2019)?t\b/i, confidence: 0.6 },
83
+ {
84
+ regex: /\bnot (?:a fan|interested|willing|going|planning|in|into|looking) /i,
85
+ kind: 'preference',
86
+ confidence: 0.7,
87
+ },
88
+ { regex: /\b(?:is|are|am|was|were) not\b/i, confidence: 0.5 },
89
+ ],
90
+ predicateNormalisers: [
91
+ 'is',
92
+ 'are',
93
+ 'am',
94
+ 'was',
95
+ 'were',
96
+ 'be',
97
+ 'been',
98
+ 'being',
99
+ 'has',
100
+ 'have',
101
+ 'had',
102
+ 'do',
103
+ 'does',
104
+ 'did',
105
+ 'lives',
106
+ 'live',
107
+ 'lived',
108
+ 'living',
109
+ 'works',
110
+ 'work',
111
+ 'worked',
112
+ 'working',
113
+ 'plays',
114
+ 'play',
115
+ 'played',
116
+ 'playing',
117
+ 'likes',
118
+ 'like',
119
+ 'liked',
120
+ 'loves',
121
+ 'love',
122
+ 'loved',
123
+ 'prefers',
124
+ 'prefer',
125
+ 'preferred',
126
+ 'drinks',
127
+ 'drink',
128
+ 'drank',
129
+ 'drunk',
130
+ 'eats',
131
+ 'eat',
132
+ 'ate',
133
+ 'eaten',
134
+ 'in',
135
+ 'at',
136
+ 'on',
137
+ 'with',
138
+ 'for',
139
+ 'to',
140
+ ],
141
+ subjectStopWords: ['a', 'an', 'the', 'my', 'your', 'his', 'her', 'their', 'our', 'this', 'that'],
142
+ });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Public surface of the locale-pack module. The bundled English pack
3
+ * (`enLocalePack`) is the default; additional locales are
4
+ * community-supplied via {@link defineLocalePack}.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+
9
+ export { enLocalePack } from './en.js';
10
+ export {
11
+ defineLocalePack,
12
+ evaluateMarkers,
13
+ type LocaleMatch,
14
+ type LocalePack,
15
+ type LocalePatternEntry,
16
+ type LocaleSupersedeKind,
17
+ } from './types.js';
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Public types for the per-locale heuristic packs consumed by the
3
+ * Stage 3 (regex) and Stage 4 (subject/predicate) stages of the
4
+ * multi-stage conflict resolution pipeline (DEC-117 / ADR-018 ext).
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+
9
+ /**
10
+ * Categorisation of a supersede marker. Surfaced on the audit row so
11
+ * downstream tooling can group conflicts by lifecycle event
12
+ * (relocation / job / preference / relationship / health / generic).
13
+ *
14
+ * @stable
15
+ */
16
+ export type LocaleSupersedeKind =
17
+ | 'location'
18
+ | 'job'
19
+ | 'preference'
20
+ | 'relationship'
21
+ | 'health'
22
+ | 'generic';
23
+
24
+ /**
25
+ * Single regex pattern bundled inside a `LocalePack`. The pipeline
26
+ * applies the regex to the fact body in case-insensitive mode unless
27
+ * the pattern was constructed with explicit flags.
28
+ *
29
+ * @stable
30
+ */
31
+ export interface LocalePatternEntry {
32
+ readonly regex: RegExp;
33
+ readonly kind?: LocaleSupersedeKind;
34
+ /**
35
+ * Optional confidence ∈ `[0, 1]` surfaced through the audit row's
36
+ * `reason` field. Defaults to `0.8`.
37
+ */
38
+ readonly confidence?: number;
39
+ }
40
+
41
+ /**
42
+ * A `LocalePack` defines the regex sets Stage 3 and the predicate
43
+ * verb set Stage 4 use to evaluate a candidate fact pair against the
44
+ * existing conflicts.
45
+ *
46
+ * The bundled English pack (`enLocalePack`) covers the most common
47
+ * personal-assistant change signals (relocation / job change /
48
+ * preference flip / relationship / health). Additional locales are
49
+ * registered through {@link defineLocalePack}.
50
+ *
51
+ * @stable
52
+ */
53
+ export interface LocalePack {
54
+ /** Stable lowercase identifier (`'en'`, `'fr'`, …). */
55
+ readonly id: string;
56
+ /** Patterns that explicitly mark the candidate as superseding the existing fact. */
57
+ readonly supersedeMarkers: ReadonlyArray<LocalePatternEntry>;
58
+ /** Patterns that negate the existing fact (treated as supersede in Stage 3). */
59
+ readonly negationMarkers: ReadonlyArray<LocalePatternEntry>;
60
+ /**
61
+ * Verbs (or verb particles) Stage 4 strips while normalising a
62
+ * predicate so e.g. `'lives in'` and `'living in'` collapse to the
63
+ * same key. Lowercase tokens.
64
+ */
65
+ readonly predicateNormalisers: ReadonlyArray<string>;
66
+ /** Tokens dropped from the subject before comparison (`'a'`, `'the'`, …). */
67
+ readonly subjectStopWords: ReadonlyArray<string>;
68
+ }
69
+
70
+ /**
71
+ * Outcome of a single locale-pack regex evaluation. Returned by
72
+ * `evaluateMarkers(...)` so the pipeline can propagate the matched
73
+ * marker into the audit row's `reason` field.
74
+ *
75
+ * @stable
76
+ */
77
+ export interface LocaleMatch {
78
+ readonly matched: boolean;
79
+ readonly kind?: LocaleSupersedeKind;
80
+ readonly confidence?: number;
81
+ readonly excerpt?: string;
82
+ }
83
+
84
+ /**
85
+ * Builder used by application code that wants to ship a custom pack:
86
+ *
87
+ * ```ts
88
+ * const ru = defineLocalePack({
89
+ * id: 'ru',
90
+ * supersedeMarkers: [{ regex: /\bпереехал\b/iu, kind: 'location' }],
91
+ * negationMarkers: [{ regex: /\bне\b/iu }],
92
+ * predicateNormalisers: ['жив', 'жил', 'живёт'],
93
+ * subjectStopWords: ['я', 'ты'],
94
+ * });
95
+ * ```
96
+ *
97
+ * The builder freezes every input so the pack can be safely reused
98
+ * across multiple `Memory` instances without accidental mutation.
99
+ *
100
+ * @stable
101
+ */
102
+ export function defineLocalePack(input: LocalePack): LocalePack {
103
+ return Object.freeze({
104
+ id: input.id,
105
+ supersedeMarkers: Object.freeze([...input.supersedeMarkers]),
106
+ negationMarkers: Object.freeze([...input.negationMarkers]),
107
+ predicateNormalisers: Object.freeze([...input.predicateNormalisers]),
108
+ subjectStopWords: Object.freeze([...input.subjectStopWords]),
109
+ }) as LocalePack;
110
+ }
111
+
112
+ /**
113
+ * Apply a list of patterns to the supplied text and return the
114
+ * highest-confidence match (first match wins on ties). Surfaced for
115
+ * unit tests; the pipeline calls this internally.
116
+ *
117
+ * @stable
118
+ */
119
+ export function evaluateMarkers(
120
+ text: string,
121
+ patterns: ReadonlyArray<LocalePatternEntry>,
122
+ ): LocaleMatch {
123
+ let best: LocaleMatch = { matched: false };
124
+ for (const pattern of patterns) {
125
+ const m = pattern.regex.exec(text);
126
+ if (m === null) continue;
127
+ const confidence = pattern.confidence ?? 0.8;
128
+ if (best.matched && (best.confidence ?? 0) >= confidence) continue;
129
+ const candidate: LocaleMatch = {
130
+ matched: true,
131
+ ...(pattern.kind !== undefined ? { kind: pattern.kind } : {}),
132
+ confidence,
133
+ excerpt: m[0],
134
+ };
135
+ best = candidate;
136
+ }
137
+ return best;
138
+ }
@@ -0,0 +1,371 @@
1
+ /**
2
+ * `runConflictPipeline` - orchestrator for the multi-stage conflict
3
+ * resolution pipeline shipped in `@graphorin/memory` Phase 10b
4
+ * (DEC-117 / ADR-018 ext / RB-02).
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+
9
+ import type { Fact, MemoryHit } from '@graphorin/core';
10
+ import { withMemorySpan } from '../internal/spans.js';
11
+ import { enLocalePack, type LocalePack } from './locale-packs/index.js';
12
+ import { stage1ExactDedup } from './stages/stage1-exact-dedup.js';
13
+ import { stage2EmbeddingThreeZone } from './stages/stage2-embedding-three-zone.js';
14
+ import { stage3HeuristicRegex } from './stages/stage3-heuristic-regex.js';
15
+ import { stage4SubjectPredicate } from './stages/stage4-subject-predicate.js';
16
+ import { stage5DeferToDeep } from './stages/stage5-defer-to-deep.js';
17
+ import {
18
+ type ConflictDecision,
19
+ type ConflictPipeline,
20
+ type ConflictPipelineDeps,
21
+ type ConflictPipelineOptions,
22
+ type ConflictStage,
23
+ type ConflictThresholds,
24
+ DEFAULT_CONFLICT_THRESHOLDS,
25
+ type StageContext,
26
+ type StageOutcome,
27
+ } from './types.js';
28
+
29
+ let bypassWarnedOnce = false;
30
+
31
+ /**
32
+ * Reset the one-shot bypass-warning flag. Test-only helper - production
33
+ * callers never invoke this.
34
+ *
35
+ * @internal
36
+ */
37
+ export function _resetBypassWarningForTesting(): void {
38
+ bypassWarnedOnce = false;
39
+ }
40
+
41
+ /**
42
+ * One-shot helper that mirrors RB-02 §8.1's `runConflictPipeline({...})`
43
+ * spec - convenient for callers that do not need to pre-build + cache
44
+ * the pipeline. Production wiring should still go through
45
+ * {@link createConflictPipeline} (`SemanticMemory` re-uses the cached
46
+ * instance per `Memory`).
47
+ *
48
+ * @stable
49
+ */
50
+ export async function runConflictPipeline(args: {
51
+ readonly candidate: Fact;
52
+ readonly deps: ConflictPipelineDeps;
53
+ readonly options?: ConflictPipelineOptions;
54
+ }): Promise<ConflictDecision> {
55
+ const pipeline = createConflictPipeline(args.options ?? {});
56
+ return pipeline.run(args.deps, args.candidate);
57
+ }
58
+
59
+ /**
60
+ * Returns a frozen pipeline handle wired with the supplied options.
61
+ *
62
+ * @stable
63
+ */
64
+ export function createConflictPipeline(options: ConflictPipelineOptions = {}): ConflictPipeline {
65
+ const mode: 'on' | 'off' = options.mode ?? 'on';
66
+ const localePack: LocalePack = options.localePack ?? enLocalePack;
67
+ const thresholds: ConflictThresholds = Object.freeze({
68
+ hot: options.thresholds?.hot ?? DEFAULT_CONFLICT_THRESHOLDS.hot,
69
+ nearDup: options.thresholds?.nearDup ?? DEFAULT_CONFLICT_THRESHOLDS.nearDup,
70
+ cold: options.thresholds?.cold ?? DEFAULT_CONFLICT_THRESHOLDS.cold,
71
+ });
72
+ validateThresholds(thresholds);
73
+ const stage2TopK = options.stage2TopK ?? 5;
74
+ const now = options.now ?? ((): string => new Date().toISOString());
75
+
76
+ async function run(deps: ConflictPipelineDeps, candidate: Fact): Promise<ConflictDecision> {
77
+ if (mode === 'off') {
78
+ if (!bypassWarnedOnce) {
79
+ bypassWarnedOnce = true;
80
+ warnBypass();
81
+ }
82
+ return { kind: 'admit', stage: 'exact-dedup', reason: 'pipeline-mode-off' };
83
+ }
84
+
85
+ checkAbort(deps.signal);
86
+ const conflictStore = options.conflictStore ?? deps.store.conflicts;
87
+
88
+ return withMemorySpan(
89
+ deps.tracer,
90
+ 'memory.conflict',
91
+ {
92
+ userId: candidate.userId,
93
+ ...(candidate.sessionId !== undefined ? { sessionId: candidate.sessionId } : {}),
94
+ ...(candidate.agentId !== undefined ? { agentId: candidate.agentId } : {}),
95
+ },
96
+ {
97
+ 'memory.conflict.candidate_id': candidate.id,
98
+ 'memory.conflict.locale_pack': localePack.id,
99
+ },
100
+ async (span) => {
101
+ const existing = await collectVectorCandidates(deps, candidate, stage2TopK);
102
+ const ctx: StageContext = {
103
+ candidate,
104
+ existing,
105
+ localePack,
106
+ thresholds,
107
+ };
108
+ const decision = await orchestrate(ctx);
109
+ await persistDecision(decision, candidate, conflictStore, now);
110
+ span.setAttributes({
111
+ 'memory.conflict.stage': decision.stage,
112
+ 'memory.conflict.decision': decision.kind,
113
+ 'memory.conflict.candidate_count': existing.length,
114
+ ...(decision.kind === 'dedup' && decision.similarity !== undefined
115
+ ? { 'memory.conflict.similarity': decision.similarity }
116
+ : {}),
117
+ ...(decision.kind === 'pending' && decision.similarity !== undefined
118
+ ? { 'memory.conflict.similarity': decision.similarity }
119
+ : {}),
120
+ ...(decision.kind !== 'admit'
121
+ ? { 'memory.conflict.reason': decision.reason ?? 'unspecified' }
122
+ : decision.reason !== undefined
123
+ ? { 'memory.conflict.reason': decision.reason }
124
+ : {}),
125
+ });
126
+ return decision;
127
+ },
128
+ );
129
+ }
130
+
131
+ /**
132
+ * Visit the five stages in the order documented by RB-02 §8.1 and
133
+ * the conflict-resolution policy (DEC-117). Stage 1 short-circuits
134
+ * exact duplicates. Stage 2 always computes the zone but the
135
+ * orchestrator splits its decision in two halves: the HOT zone is
136
+ * the only zone that overrides Stage 3 / Stage 4 (semantic identity
137
+ * outranks any explicit user marker); every other zone runs Stages
138
+ * 3 and 4 first to give the heuristic markers a chance to supersede
139
+ * before falling back to Stage 2's default decision.
140
+ *
141
+ * Stage 3 is declared "orthogonal to Stage 2", which is why the
142
+ * heuristic stages run regardless of Stage 2's COLD / NEAR-DUP /
143
+ * CONFLICT-CHECK assignment as long as Stage 2 surfaced at least
144
+ * one candidate. The HOT-zone short-circuit preserves the dedup
145
+ * invariant for semantic identity.
146
+ */
147
+ async function orchestrate(ctx: StageContext): Promise<ConflictDecision> {
148
+ const stage1 = await stage1ExactDedup.evaluate(ctx);
149
+ if (stage1.kind !== 'continue') {
150
+ return mapOutcomeToDecision('exact-dedup', stage1, ctx.candidate);
151
+ }
152
+
153
+ const stage2 = await stage2EmbeddingThreeZone.evaluate(ctx);
154
+ if (stage2.kind === 'dedup' && stage2.reason === 'embedding-hot-zone') {
155
+ return mapOutcomeToDecision('embedding-three-zone', stage2, ctx.candidate);
156
+ }
157
+
158
+ if (ctx.existing.length > 0) {
159
+ const stage3 = await stage3HeuristicRegex.evaluate(ctx);
160
+ if (stage3.kind !== 'continue') {
161
+ return mapOutcomeToDecision('heuristic-regex', stage3, ctx.candidate);
162
+ }
163
+ const stage4 = await stage4SubjectPredicate.evaluate(ctx);
164
+ if (stage4.kind !== 'continue') {
165
+ return mapOutcomeToDecision('subject-predicate', stage4, ctx.candidate);
166
+ }
167
+ }
168
+
169
+ if (stage2.kind === 'dedup' || stage2.kind === 'admit') {
170
+ return mapOutcomeToDecision('embedding-three-zone', stage2, ctx.candidate);
171
+ }
172
+
173
+ const stage5 = await stage5DeferToDeep.evaluate(ctx);
174
+ return mapOutcomeToDecision('defer-to-deep', stage5, ctx.candidate);
175
+ }
176
+
177
+ return Object.freeze({
178
+ mode,
179
+ localePack,
180
+ thresholds,
181
+ run,
182
+ }) satisfies ConflictPipeline;
183
+ }
184
+
185
+ function mapOutcomeToDecision(
186
+ stage: ConflictStage,
187
+ outcome: StageOutcome,
188
+ candidate: Fact,
189
+ ): ConflictDecision {
190
+ switch (outcome.kind) {
191
+ case 'continue':
192
+ return { kind: 'admit', stage, reason: 'continue-fallthrough' };
193
+ case 'admit':
194
+ return {
195
+ kind: 'admit',
196
+ stage,
197
+ ...(outcome.reason !== undefined ? { reason: outcome.reason } : {}),
198
+ };
199
+ case 'dedup':
200
+ return {
201
+ kind: 'dedup',
202
+ stage,
203
+ existingId: outcome.existingId,
204
+ ...(outcome.similarity !== undefined ? { similarity: outcome.similarity } : {}),
205
+ ...(outcome.reason !== undefined ? { reason: outcome.reason } : {}),
206
+ };
207
+ case 'supersede':
208
+ return {
209
+ kind: 'supersede',
210
+ stage,
211
+ existingId: outcome.existingId,
212
+ reason: outcome.reason,
213
+ };
214
+ case 'pending':
215
+ return {
216
+ kind: 'pending',
217
+ stage,
218
+ candidateId: candidate.id,
219
+ conflictingIds: outcome.conflictingIds,
220
+ ...(outcome.similarity !== undefined ? { similarity: outcome.similarity } : {}),
221
+ ...(outcome.reason !== undefined ? { reason: outcome.reason } : {}),
222
+ };
223
+ }
224
+ }
225
+
226
+ async function persistDecision(
227
+ decision: ConflictDecision,
228
+ candidate: Fact,
229
+ conflictStore: ConflictPipelineDeps['store']['conflicts'],
230
+ now: () => string,
231
+ ): Promise<void> {
232
+ if (conflictStore === undefined) return;
233
+ const scope = {
234
+ userId: candidate.userId,
235
+ ...(candidate.sessionId !== undefined ? { sessionId: candidate.sessionId } : {}),
236
+ ...(candidate.agentId !== undefined ? { agentId: candidate.agentId } : {}),
237
+ };
238
+ const detectionZone = decisionToZone(decision);
239
+ const auditInput: Parameters<NonNullable<typeof conflictStore>['recordDecision']>[0] = {
240
+ scope,
241
+ candidateId: candidate.id,
242
+ decision: decision.kind,
243
+ stage: decision.stage,
244
+ ...(detectionZone !== undefined ? { detectionZone } : {}),
245
+ ...(decision.kind === 'dedup' && decision.existingId !== undefined
246
+ ? { existingId: decision.existingId }
247
+ : {}),
248
+ ...(decision.kind === 'supersede' ? { existingId: decision.existingId } : {}),
249
+ ...(decision.kind === 'dedup' && decision.similarity !== undefined
250
+ ? { similarity: decision.similarity }
251
+ : {}),
252
+ ...(decision.kind === 'pending' && decision.similarity !== undefined
253
+ ? { similarity: decision.similarity }
254
+ : {}),
255
+ ...(decision.kind !== 'admit' && decision.reason !== undefined
256
+ ? { reason: decision.reason }
257
+ : {}),
258
+ ...(decision.kind === 'admit' && decision.reason !== undefined
259
+ ? { reason: decision.reason }
260
+ : {}),
261
+ };
262
+ await conflictStore.recordDecision(auditInput);
263
+ if (decision.kind === 'pending') {
264
+ await conflictStore.enqueuePending({
265
+ scope,
266
+ factId: candidate.id,
267
+ candidateText: candidate.text,
268
+ stage: decision.stage,
269
+ conflictingIds: decision.conflictingIds,
270
+ ...(decision.reason !== undefined ? { reason: decision.reason } : {}),
271
+ });
272
+ }
273
+ void now;
274
+ }
275
+
276
+ function decisionToZone(decision: ConflictDecision): string | undefined {
277
+ switch (decision.kind) {
278
+ case 'admit':
279
+ if (decision.reason === 'embedding-cold-zone') return 'cold';
280
+ return undefined;
281
+ case 'dedup':
282
+ if (decision.reason === 'embedding-hot-zone') return 'hot';
283
+ if (decision.reason === 'embedding-near-dup-zone') return 'near-dup';
284
+ if (decision.reason === 'exact-hash-match') return 'exact';
285
+ if (decision.reason === 'subject-predicate-match-same-object') return 'subject-predicate';
286
+ return undefined;
287
+ case 'supersede':
288
+ if (decision.reason.startsWith('regex-supersede-marker')) return 'heuristic';
289
+ if (decision.reason.startsWith('regex-negation')) return 'heuristic';
290
+ if (decision.reason.startsWith('subject-predicate-match')) return 'subject-predicate';
291
+ return undefined;
292
+ case 'pending':
293
+ return 'conflict-check';
294
+ }
295
+ }
296
+
297
+ async function collectVectorCandidates(
298
+ deps: ConflictPipelineDeps,
299
+ candidate: Fact,
300
+ topK: number,
301
+ ): Promise<ReadonlyArray<MemoryHit<Fact>>> {
302
+ if (deps.embedder === null || deps.embedderId === null) return [];
303
+ const adapter = deps.store.semantic;
304
+ if (typeof adapter.searchVector !== 'function') return [];
305
+ checkAbort(deps.signal);
306
+ // PS-10: the candidate is compared against stored facts, which are embedded
307
+ // as `passage` - keep it in the same space for the asymmetric (E5) embedder.
308
+ const [vector] = await deps.embedder.embed([candidate.text], { taskType: 'passage' });
309
+ if (vector === undefined) return [];
310
+ checkAbort(deps.signal);
311
+ const scope = {
312
+ userId: candidate.userId,
313
+ ...(candidate.sessionId !== undefined ? { sessionId: candidate.sessionId } : {}),
314
+ ...(candidate.agentId !== undefined ? { agentId: candidate.agentId } : {}),
315
+ };
316
+ // Vector search may throw when the storage layer's vector extension
317
+ // is missing or mis-configured (e.g. `sqlite-vec` not loaded). Fall
318
+ // back to "no candidates" so the conflict pipeline degrades to the
319
+ // 10a straight-through write rather than blocking the agent loop.
320
+ // AbortError is re-raised so cancellation propagates up.
321
+ let hits: ReadonlyArray<MemoryHit<Fact>>;
322
+ try {
323
+ hits = await adapter.searchVector(scope, vector, deps.embedderId, topK);
324
+ } catch (err) {
325
+ if (err instanceof Error && err.name === 'AbortError') throw err;
326
+ return [];
327
+ }
328
+ return [...hits].filter((hit) => hit.record.id !== candidate.id);
329
+ }
330
+
331
+ function validateThresholds(thresholds: ConflictThresholds): void {
332
+ if (
333
+ !(
334
+ thresholds.hot >= thresholds.nearDup &&
335
+ thresholds.nearDup >= thresholds.cold &&
336
+ thresholds.hot <= 1 &&
337
+ thresholds.cold >= 0
338
+ )
339
+ ) {
340
+ throw new TypeError(
341
+ `[graphorin/memory] invalid conflict thresholds: expected 0 <= cold <= nearDup <= hot <= 1, got ${JSON.stringify(thresholds)}.`,
342
+ );
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Throw the canonical `AbortError` when the supplied signal is
348
+ * already aborted. Safe to call with `undefined`.
349
+ *
350
+ * @internal
351
+ */
352
+ function checkAbort(signal: AbortSignal | undefined): void {
353
+ if (signal === undefined) return;
354
+ if (signal.aborted) {
355
+ if (typeof signal.throwIfAborted === 'function') {
356
+ signal.throwIfAborted();
357
+ }
358
+ throw new DOMException('The operation was aborted.', 'AbortError');
359
+ }
360
+ }
361
+
362
+ function warnBypass(): void {
363
+ // Use the structured logger once it lands (Phase 04 follow-up). For
364
+ // now, surface a one-shot console.warn so operators notice the
365
+ // regression risk per the Phase 10b spec ("WARN-once per process").
366
+ console.warn(
367
+ '[graphorin/memory] conflict pipeline disabled (mode: "off"). ' +
368
+ 'Semantic writes fall back to a straight-through path - bi-temporal supersede / dedup ' +
369
+ 'will not fire until you re-enable the pipeline.',
370
+ );
371
+ }