hrr-memory 0.1.1 → 0.2.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.
package/package.json CHANGED
@@ -1,17 +1,19 @@
1
1
  {
2
2
  "name": "hrr-memory",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Holographic Reduced Representations for structured agent memory. Zero dependencies. Complements RAG with algebraic fact queries.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "exports": {
8
8
  ".": "./src/index.js"
9
9
  },
10
+ "types": "types/index.d.ts",
10
11
  "files": [
11
- "src/"
12
+ "src/",
13
+ "types/"
12
14
  ],
13
15
  "scripts": {
14
- "test": "node --test test/",
16
+ "test": "node --test test/memory.test.js",
15
17
  "bench": "node bench/benchmark.js"
16
18
  },
17
19
  "keywords": [
package/src/bucket.js CHANGED
@@ -3,6 +3,8 @@
3
3
  * Auto-splits are managed by HRRMemory, not by the bucket itself.
4
4
  */
5
5
 
6
+ import { bind } from './ops.js';
7
+
6
8
  export const MAX_BUCKET_SIZE = 25;
7
9
 
8
10
  export class Bucket {
@@ -24,6 +26,22 @@ export class Bucket {
24
26
  /** Whether the bucket has reached max capacity */
25
27
  get isFull() { return this.count >= MAX_BUCKET_SIZE; }
26
28
 
29
+ /**
30
+ * Rebuild the memory vector from remaining triples.
31
+ * Called after removing a triple — can't subtract cleanly due to superposition noise.
32
+ */
33
+ rebuild(symbols) {
34
+ this.memory = new Float32Array(this.d);
35
+ for (const t of this.triples) {
36
+ const s = symbols.get(t.subject);
37
+ const r = symbols.get(t.relation);
38
+ const o = symbols.get(t.object);
39
+ const association = bind(bind(s, r), o);
40
+ for (let i = 0; i < this.d; i++) this.memory[i] += association[i];
41
+ }
42
+ this.count = this.triples.length;
43
+ }
44
+
27
45
  /** Serialize */
28
46
  toJSON() {
29
47
  return {
package/src/memory.js CHANGED
@@ -6,6 +6,18 @@
6
6
  * is created automatically. Queries scan all buckets for a subject.
7
7
  *
8
8
  * Symbol vectors are shared across all buckets.
9
+ * All values are lowercased on store. If you need case-sensitive values,
10
+ * normalize them yourself before calling store().
11
+ *
12
+ * @typedef {{ match: string|null, score: number, confident: boolean, bucket: string|null }} QueryResult
13
+ * @typedef {{ relation: string, object: string }} Fact
14
+ * @typedef {{ subject: string, relation: string, object: string }} Triple
15
+ * @typedef {{ type: 'direct', match: string, score: number, confident: boolean, subject: string, relation: string, bucket: string|null }} DirectAskResult
16
+ * @typedef {{ type: 'subject', subject: string, facts: Fact[] }} SubjectAskResult
17
+ * @typedef {{ type: 'search', term: string, results: Triple[] }} SearchAskResult
18
+ * @typedef {{ type: 'miss', query: string }} MissAskResult
19
+ * @typedef {DirectAskResult | SubjectAskResult | SearchAskResult | MissAskResult} AskResult
20
+ * @typedef {{ dimensions: number, maxBucketSize: number, symbols: number, buckets: number, subjects: number, totalFacts: number, ramBytes: number, ramMB: number, perBucket: Array<{name: string, facts: number, full: boolean}> }} Stats
9
21
  */
10
22
 
11
23
  import { readFileSync, writeFileSync, existsSync } from 'fs';
@@ -13,6 +25,12 @@ import { bind, unbind, similarity } from './ops.js';
13
25
  import { SymbolTable } from './symbols.js';
14
26
  import { Bucket, MAX_BUCKET_SIZE } from './bucket.js';
15
27
 
28
+ const STOP_WORDS = new Set([
29
+ 'what', 'is', 'the', 'a', 'an', 'does', 'do', 'where', 'who', 'how',
30
+ 'which', 'of', 'for', 'in', 'at', 'to', 'my', 'your', 'their', 'has',
31
+ 'have', 'was', 'were', 'are', 'been',
32
+ ]);
33
+
16
34
  export class HRRMemory {
17
35
  /**
18
36
  * Create a new HRR memory store.
@@ -92,6 +110,44 @@ export class HRRMemory {
92
110
  return true;
93
111
  }
94
112
 
113
+ // ── Forget ─────────────────────────────────────────
114
+
115
+ /**
116
+ * Remove a fact from memory.
117
+ * Rebuilds the affected bucket's memory vector from remaining triples.
118
+ * @param {string} subject
119
+ * @param {string} relation
120
+ * @param {string} object
121
+ * @returns {boolean} true if found and removed, false if not found
122
+ */
123
+ forget(subject, relation, object) {
124
+ const s = subject.toLowerCase().trim();
125
+ const r = relation.toLowerCase().trim();
126
+ const o = object.toLowerCase().trim();
127
+
128
+ const ids = this.routing.get(s);
129
+ if (!ids) return false;
130
+
131
+ for (let i = 0; i < ids.length; i++) {
132
+ const bucket = this.buckets.get(ids[i]);
133
+ const idx = bucket.triples.findIndex(t =>
134
+ t.subject === s && t.relation === r && t.object === o
135
+ );
136
+ if (idx === -1) continue;
137
+
138
+ bucket.triples.splice(idx, 1);
139
+ bucket.rebuild(this.symbols);
140
+
141
+ // Clean up empty overflow buckets
142
+ if (bucket.count === 0 && ids[i].includes('#')) {
143
+ this.buckets.delete(ids[i]);
144
+ ids.splice(i, 1);
145
+ }
146
+ return true;
147
+ }
148
+ return false;
149
+ }
150
+
95
151
  // ── Query ──────────────────────────────────────────
96
152
 
97
153
  /**
@@ -167,11 +223,22 @@ export class HRRMemory {
167
223
  }
168
224
 
169
225
  /**
170
- * Free-form query: tries subject+relation, then subject lookup, then cross-bucket search.
171
- * @param {string} question
226
+ * Free-form query: strips stop words and possessives, then tries subject+relation pairs,
227
+ * subject lookup, and cross-bucket search in that order.
228
+ * @param {string} question - Natural language question (e.g., "What is alice's timezone?")
229
+ * @returns {AskResult} One of: DirectAskResult, SubjectAskResult, SearchAskResult, or MissAskResult
230
+ * @example
231
+ * mem.store('alice', 'timezone', 'cet');
232
+ * mem.ask("What is alice's timezone?"); // → { type: 'direct', match: 'cet', ... }
233
+ * mem.ask('alice'); // → { type: 'subject', facts: [...] }
172
234
  */
173
235
  ask(question) {
174
- const parts = question.toLowerCase().trim().replace(/[?.,!]/g, '').split(/\s+/);
236
+ const parts = question.toLowerCase().trim()
237
+ .replace(/[?.,!]/g, '')
238
+ .replace(/'s\b/g, '') // possessive: alice's → alice
239
+ .replace(/-/g, '_') // lives-in → lives_in
240
+ .split(/\s+/)
241
+ .filter(w => !STOP_WORDS.has(w) && w.length > 0);
175
242
 
176
243
  // Try consecutive word pairs as subject+relation
177
244
  for (let i = 0; i < parts.length - 1; i++) {
@@ -196,7 +263,10 @@ export class HRRMemory {
196
263
 
197
264
  // ── Stats ──────────────────────────────────────────
198
265
 
199
- /** Get memory statistics */
266
+ /**
267
+ * Get memory statistics: dimensions, bucket count, total facts, RAM usage.
268
+ * @returns {Stats}
269
+ */
200
270
  stats() {
201
271
  let totalFacts = 0;
202
272
  const bucketInfo = [];
@@ -221,7 +291,10 @@ export class HRRMemory {
221
291
 
222
292
  // ── Persistence ────────────────────────────────────
223
293
 
224
- /** Serialize to JSON */
294
+ /**
295
+ * Serialize the entire memory store to a plain object.
296
+ * @returns {{ version: number, d: number, symbols: object, buckets: object, routing: object }}
297
+ */
225
298
  toJSON() {
226
299
  const buckets = {};
227
300
  for (const [k, v] of this.buckets) buckets[k] = v.toJSON();
@@ -230,7 +303,11 @@ export class HRRMemory {
230
303
  return { version: 3, d: this.d, symbols: this.symbols.toJSON(), buckets, routing };
231
304
  }
232
305
 
233
- /** Deserialize from JSON */
306
+ /**
307
+ * Deserialize from a plain object (as produced by toJSON).
308
+ * @param {object} data - Serialized memory data
309
+ * @returns {HRRMemory}
310
+ */
234
311
  static fromJSON(data) {
235
312
  const d = data.d || 2048;
236
313
  const mem = new HRRMemory(d);
@@ -244,12 +321,20 @@ export class HRRMemory {
244
321
  return mem;
245
322
  }
246
323
 
247
- /** Save to a JSON file */
324
+ /**
325
+ * Save the memory store to a JSON file.
326
+ * @param {string} filePath - Path to write the JSON file
327
+ */
248
328
  save(filePath) {
249
329
  writeFileSync(filePath, JSON.stringify(this.toJSON()));
250
330
  }
251
331
 
252
- /** Load from a JSON file (returns new empty store if file doesn't exist) */
332
+ /**
333
+ * Load a memory store from a JSON file. Returns a new empty store if the file doesn't exist.
334
+ * @param {string} filePath - Path to the JSON file
335
+ * @param {number} [d=2048] - Vector dimensions (used only if creating new store)
336
+ * @returns {HRRMemory}
337
+ */
253
338
  static load(filePath, d = 2048) {
254
339
  if (!existsSync(filePath)) return new HRRMemory(d);
255
340
  try { return HRRMemory.fromJSON(JSON.parse(readFileSync(filePath, 'utf8'))); }
@@ -0,0 +1,122 @@
1
+ export interface QueryResult {
2
+ match: string | null;
3
+ score: number;
4
+ confident: boolean;
5
+ bucket: string | null;
6
+ }
7
+
8
+ export interface Fact {
9
+ relation: string;
10
+ object: string;
11
+ }
12
+
13
+ export interface Triple {
14
+ subject: string;
15
+ relation: string;
16
+ object: string;
17
+ }
18
+
19
+ export interface DirectAskResult {
20
+ type: 'direct';
21
+ match: string;
22
+ score: number;
23
+ confident: boolean;
24
+ subject: string;
25
+ relation: string;
26
+ bucket: string | null;
27
+ }
28
+
29
+ export interface SubjectAskResult {
30
+ type: 'subject';
31
+ subject: string;
32
+ facts: Fact[];
33
+ }
34
+
35
+ export interface SearchAskResult {
36
+ type: 'search';
37
+ term: string;
38
+ results: Triple[];
39
+ }
40
+
41
+ export interface MissAskResult {
42
+ type: 'miss';
43
+ query: string;
44
+ }
45
+
46
+ export type AskResult = DirectAskResult | SubjectAskResult | SearchAskResult | MissAskResult;
47
+
48
+ export interface BucketStats {
49
+ name: string;
50
+ facts: number;
51
+ full: boolean;
52
+ }
53
+
54
+ export interface Stats {
55
+ dimensions: number;
56
+ maxBucketSize: number;
57
+ symbols: number;
58
+ buckets: number;
59
+ subjects: number;
60
+ totalFacts: number;
61
+ ramBytes: number;
62
+ ramMB: number;
63
+ perBucket: BucketStats[];
64
+ }
65
+
66
+ export interface SerializedMemory {
67
+ version: number;
68
+ d: number;
69
+ symbols: Record<string, number[]>;
70
+ buckets: Record<string, unknown>;
71
+ routing: Record<string, string[]>;
72
+ }
73
+
74
+ export class HRRMemory {
75
+ constructor(dimensions?: number);
76
+
77
+ store(subject: string, relation: string, object: string): boolean;
78
+ forget(subject: string, relation: string, object: string): boolean;
79
+ query(subject: string, relation: string): QueryResult;
80
+ querySubject(subject: string): Fact[];
81
+ search(relation: string | null, object: string | null): Triple[];
82
+ ask(question: string): AskResult;
83
+ stats(): Stats;
84
+
85
+ save(filePath: string): void;
86
+ static load(filePath: string, dimensions?: number): HRRMemory;
87
+
88
+ toJSON(): SerializedMemory;
89
+ static fromJSON(data: SerializedMemory): HRRMemory;
90
+ }
91
+
92
+ export class SymbolTable {
93
+ constructor(dimensions?: number);
94
+ get(name: string): Float32Array;
95
+ has(name: string): boolean;
96
+ readonly size: number;
97
+ readonly bytes: number;
98
+ nearest(
99
+ vec: Float32Array,
100
+ candidates?: Map<string, Float32Array> | null,
101
+ topK?: number
102
+ ): { name: string; score: number } | { name: string; score: number }[] | null;
103
+ toJSON(): Record<string, number[]>;
104
+ static fromJSON(data: Record<string, number[]>, dimensions: number): SymbolTable;
105
+ }
106
+
107
+ export class Bucket {
108
+ constructor(name: string, dimensions?: number);
109
+ readonly name: string;
110
+ readonly count: number;
111
+ readonly isFull: boolean;
112
+ storeVector(association: Float32Array, triple: Triple): void;
113
+ rebuild(symbols: SymbolTable): void;
114
+ toJSON(): unknown;
115
+ static fromJSON(data: unknown, dimensions: number): Bucket;
116
+ }
117
+
118
+ export function bind(a: Float32Array, b: Float32Array): Float32Array;
119
+ export function unbind(key: Float32Array, memory: Float32Array): Float32Array;
120
+ export function similarity(a: Float32Array, b: Float32Array): number;
121
+ export function randomVector(dimensions: number): Float32Array;
122
+ export function normalize(v: Float32Array): Float32Array;