@geravant/sinain 1.0.1

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 (53) hide show
  1. package/README.md +183 -0
  2. package/index.ts +2096 -0
  3. package/install.js +155 -0
  4. package/openclaw.plugin.json +59 -0
  5. package/package.json +21 -0
  6. package/sinain-memory/common.py +403 -0
  7. package/sinain-memory/demo_knowledge_transfer.sh +85 -0
  8. package/sinain-memory/embedder.py +268 -0
  9. package/sinain-memory/eval/__init__.py +0 -0
  10. package/sinain-memory/eval/assertions.py +288 -0
  11. package/sinain-memory/eval/judges/__init__.py +0 -0
  12. package/sinain-memory/eval/judges/base_judge.py +61 -0
  13. package/sinain-memory/eval/judges/curation_judge.py +46 -0
  14. package/sinain-memory/eval/judges/insight_judge.py +48 -0
  15. package/sinain-memory/eval/judges/mining_judge.py +42 -0
  16. package/sinain-memory/eval/judges/signal_judge.py +45 -0
  17. package/sinain-memory/eval/schemas.py +247 -0
  18. package/sinain-memory/eval_delta.py +109 -0
  19. package/sinain-memory/eval_reporter.py +642 -0
  20. package/sinain-memory/feedback_analyzer.py +221 -0
  21. package/sinain-memory/git_backup.sh +19 -0
  22. package/sinain-memory/insight_synthesizer.py +181 -0
  23. package/sinain-memory/memory/2026-03-01.md +11 -0
  24. package/sinain-memory/memory/playbook-archive/sinain-playbook-2026-03-01-1418.md +15 -0
  25. package/sinain-memory/memory/playbook-logs/2026-03-01.jsonl +1 -0
  26. package/sinain-memory/memory/sinain-playbook.md +21 -0
  27. package/sinain-memory/memory-config.json +39 -0
  28. package/sinain-memory/memory_miner.py +183 -0
  29. package/sinain-memory/module_manager.py +695 -0
  30. package/sinain-memory/playbook_curator.py +225 -0
  31. package/sinain-memory/requirements.txt +3 -0
  32. package/sinain-memory/signal_analyzer.py +141 -0
  33. package/sinain-memory/test_local.py +402 -0
  34. package/sinain-memory/tests/__init__.py +0 -0
  35. package/sinain-memory/tests/conftest.py +189 -0
  36. package/sinain-memory/tests/test_curator_helpers.py +94 -0
  37. package/sinain-memory/tests/test_embedder.py +210 -0
  38. package/sinain-memory/tests/test_extract_json.py +124 -0
  39. package/sinain-memory/tests/test_feedback_computation.py +121 -0
  40. package/sinain-memory/tests/test_miner_helpers.py +71 -0
  41. package/sinain-memory/tests/test_module_management.py +458 -0
  42. package/sinain-memory/tests/test_parsers.py +96 -0
  43. package/sinain-memory/tests/test_tick_evaluator.py +430 -0
  44. package/sinain-memory/tests/test_triple_extractor.py +255 -0
  45. package/sinain-memory/tests/test_triple_ingest.py +191 -0
  46. package/sinain-memory/tests/test_triple_migrate.py +138 -0
  47. package/sinain-memory/tests/test_triplestore.py +248 -0
  48. package/sinain-memory/tick_evaluator.py +392 -0
  49. package/sinain-memory/triple_extractor.py +402 -0
  50. package/sinain-memory/triple_ingest.py +290 -0
  51. package/sinain-memory/triple_migrate.py +275 -0
  52. package/sinain-memory/triple_query.py +184 -0
  53. package/sinain-memory/triplestore.py +498 -0
@@ -0,0 +1,498 @@
1
+ #!/usr/bin/env python3
2
+ """EAV Triple Store — SQLite-backed entity-attribute-value store.
3
+
4
+ Provides a Datomic/RhizomeDB-inspired immutable triple store with 4 covering
5
+ indexes (EAVT, AEVT, VAET, AVET) for fast graph traversal and lookup.
6
+
7
+ Each fact is a (entity_id, attribute, value) triple with a value_type tag.
8
+ Triples are asserted within transactions; retraction marks triples as logically
9
+ deleted without physically removing them (until GC).
10
+
11
+ Usage:
12
+ from triplestore import TripleStore
13
+ store = TripleStore("memory/triplestore.db")
14
+ tx = store.begin_tx("signal_analyzer", session_key="abc")
15
+ store.assert_triple(tx, "signal:2026-03-01T10:00:00Z", "description", "OCR backpressure")
16
+ store.assert_triple(tx, "signal:2026-03-01T10:00:00Z", "related_to", "concept:ocr", value_type="ref")
17
+ entity = store.entity("signal:2026-03-01T10:00:00Z")
18
+
19
+ Self-test:
20
+ python3 triplestore.py --self-test
21
+ """
22
+
23
+ import json
24
+ import os
25
+ import sqlite3
26
+ import sys
27
+ from datetime import datetime, timedelta, timezone
28
+ from pathlib import Path
29
+
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Schema
33
+ # ---------------------------------------------------------------------------
34
+
35
+ _SCHEMA_SQL = """
36
+ CREATE TABLE IF NOT EXISTS transactions (
37
+ tx_id INTEGER PRIMARY KEY AUTOINCREMENT,
38
+ source TEXT NOT NULL,
39
+ session_key TEXT,
40
+ parent_tx INTEGER,
41
+ metadata TEXT,
42
+ created_at TEXT NOT NULL
43
+ );
44
+
45
+ CREATE TABLE IF NOT EXISTS triples (
46
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
47
+ tx_id INTEGER NOT NULL REFERENCES transactions(tx_id),
48
+ entity_id TEXT NOT NULL,
49
+ attribute TEXT NOT NULL,
50
+ value TEXT NOT NULL,
51
+ value_type TEXT NOT NULL DEFAULT 'string',
52
+ retracted INTEGER NOT NULL DEFAULT 0,
53
+ retracted_tx INTEGER,
54
+ created_at TEXT NOT NULL
55
+ );
56
+
57
+ CREATE TABLE IF NOT EXISTS entity_types (
58
+ entity_id TEXT PRIMARY KEY,
59
+ entity_type TEXT NOT NULL
60
+ );
61
+
62
+ -- EAVT: "what does entity X look like?"
63
+ CREATE INDEX IF NOT EXISTS idx_eavt
64
+ ON triples(entity_id, attribute, value, tx_id);
65
+
66
+ -- AEVT: "which entities have attribute Y?"
67
+ CREATE INDEX IF NOT EXISTS idx_aevt
68
+ ON triples(attribute, entity_id, value, tx_id);
69
+
70
+ -- VAET: "what references entity Z?" (ref edges only)
71
+ CREATE INDEX IF NOT EXISTS idx_vaet
72
+ ON triples(value, attribute, entity_id, tx_id)
73
+ WHERE value_type = 'ref';
74
+
75
+ -- AVET: "find entity by unique attribute+value"
76
+ CREATE INDEX IF NOT EXISTS idx_avet
77
+ ON triples(attribute, value, entity_id, tx_id);
78
+ """
79
+
80
+
81
+ def _now_iso() -> str:
82
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
83
+
84
+
85
+ def _entity_type(entity_id: str) -> str:
86
+ """Extract type prefix from entity_id (e.g. 'signal:...' → 'signal')."""
87
+ colon = entity_id.find(":")
88
+ return entity_id[:colon] if colon > 0 else "unknown"
89
+
90
+
91
+ class TripleStore:
92
+ """SQLite-backed EAV triple store with WAL mode and 4 covering indexes."""
93
+
94
+ def __init__(self, db_path: str | Path) -> None:
95
+ self.db_path = str(db_path)
96
+ os.makedirs(os.path.dirname(self.db_path) or ".", exist_ok=True)
97
+ self._conn = sqlite3.connect(self.db_path, timeout=10)
98
+ self._conn.row_factory = sqlite3.Row
99
+ self._conn.execute("PRAGMA journal_mode=WAL")
100
+ self._conn.execute("PRAGMA busy_timeout=10000")
101
+ self._conn.executescript(_SCHEMA_SQL)
102
+ self._conn.commit()
103
+
104
+ def close(self) -> None:
105
+ self._conn.close()
106
+
107
+ # ----- Transactions -----
108
+
109
+ def begin_tx(
110
+ self,
111
+ source: str,
112
+ session_key: str | None = None,
113
+ parent_tx: int | None = None,
114
+ metadata: dict | None = None,
115
+ ) -> int:
116
+ """Begin a new transaction, returns tx_id."""
117
+ cur = self._conn.execute(
118
+ "INSERT INTO transactions (source, session_key, parent_tx, metadata, created_at) "
119
+ "VALUES (?, ?, ?, ?, ?)",
120
+ (
121
+ source,
122
+ session_key,
123
+ parent_tx,
124
+ json.dumps(metadata) if metadata else None,
125
+ _now_iso(),
126
+ ),
127
+ )
128
+ self._conn.commit()
129
+ return cur.lastrowid
130
+
131
+ def latest_tx(self) -> int:
132
+ """Return the latest transaction id, or 0 if none."""
133
+ row = self._conn.execute(
134
+ "SELECT MAX(tx_id) FROM transactions"
135
+ ).fetchone()
136
+ return row[0] or 0
137
+
138
+ # ----- Assert / Retract -----
139
+
140
+ def assert_triple(
141
+ self,
142
+ tx_id: int,
143
+ entity_id: str,
144
+ attribute: str,
145
+ value: str,
146
+ value_type: str = "string",
147
+ ) -> int:
148
+ """Assert a triple within a transaction. Returns the triple id.
149
+
150
+ Auto-populates entity_types from entity_id prefix.
151
+ """
152
+ now = _now_iso()
153
+ cur = self._conn.execute(
154
+ "INSERT INTO triples (tx_id, entity_id, attribute, value, value_type, retracted, created_at) "
155
+ "VALUES (?, ?, ?, ?, ?, 0, ?)",
156
+ (tx_id, entity_id, attribute, value, value_type, now),
157
+ )
158
+ # Upsert entity type
159
+ etype = _entity_type(entity_id)
160
+ self._conn.execute(
161
+ "INSERT OR IGNORE INTO entity_types (entity_id, entity_type) VALUES (?, ?)",
162
+ (entity_id, etype),
163
+ )
164
+ self._conn.commit()
165
+ return cur.lastrowid
166
+
167
+ def retract_triple(
168
+ self,
169
+ tx_id: int,
170
+ entity_id: str,
171
+ attribute: str,
172
+ value: str | None = None,
173
+ ) -> int:
174
+ """Retract triples matching entity+attribute (and optionally value).
175
+
176
+ Sets retracted=1 and retracted_tx to the retraction transaction.
177
+ The original tx_id is preserved for temporal (as_of_tx) queries.
178
+ Returns the count of triples retracted.
179
+ """
180
+ if value is not None:
181
+ cur = self._conn.execute(
182
+ "UPDATE triples SET retracted = 1, retracted_tx = ? "
183
+ "WHERE entity_id = ? AND attribute = ? AND value = ? AND retracted = 0",
184
+ (tx_id, entity_id, attribute, value),
185
+ )
186
+ else:
187
+ cur = self._conn.execute(
188
+ "UPDATE triples SET retracted = 1, retracted_tx = ? "
189
+ "WHERE entity_id = ? AND attribute = ? AND retracted = 0",
190
+ (tx_id, entity_id, attribute),
191
+ )
192
+ self._conn.commit()
193
+ return cur.rowcount
194
+
195
+ # ----- Query: EAVT (entity view) -----
196
+
197
+ def entity(self, entity_id: str, as_of_tx: int | None = None) -> dict[str, list[str]]:
198
+ """Return all active attributes for an entity as {attr: [values]}.
199
+
200
+ Uses EAVT index. When as_of_tx is set, shows triples that were
201
+ asserted on or before that tx AND not yet retracted at that point.
202
+ """
203
+ if as_of_tx is not None:
204
+ rows = self._conn.execute(
205
+ "SELECT attribute, value FROM triples "
206
+ "WHERE entity_id = ? AND tx_id <= ? "
207
+ "AND (retracted = 0 OR retracted_tx > ?) "
208
+ "ORDER BY attribute, id",
209
+ (entity_id, as_of_tx, as_of_tx),
210
+ ).fetchall()
211
+ else:
212
+ rows = self._conn.execute(
213
+ "SELECT attribute, value FROM triples "
214
+ "WHERE entity_id = ? AND retracted = 0 "
215
+ "ORDER BY attribute, id",
216
+ (entity_id,),
217
+ ).fetchall()
218
+ result: dict[str, list[str]] = {}
219
+ for row in rows:
220
+ result.setdefault(row["attribute"], []).append(row["value"])
221
+ return result
222
+
223
+ # ----- Query: AEVT (attribute scan) -----
224
+
225
+ def entities_with_attr(
226
+ self, attribute: str, as_of_tx: int | None = None
227
+ ) -> list[tuple[str, str]]:
228
+ """Return [(entity_id, value)] for all entities having the given attribute.
229
+
230
+ Uses AEVT index.
231
+ """
232
+ if as_of_tx is not None:
233
+ rows = self._conn.execute(
234
+ "SELECT entity_id, value FROM triples "
235
+ "WHERE attribute = ? AND tx_id <= ? "
236
+ "AND (retracted = 0 OR retracted_tx > ?) "
237
+ "ORDER BY entity_id",
238
+ (attribute, as_of_tx, as_of_tx),
239
+ ).fetchall()
240
+ else:
241
+ rows = self._conn.execute(
242
+ "SELECT entity_id, value FROM triples "
243
+ "WHERE attribute = ? AND retracted = 0 "
244
+ "ORDER BY entity_id",
245
+ (attribute,),
246
+ ).fetchall()
247
+ return [(r["entity_id"], r["value"]) for r in rows]
248
+
249
+ # ----- Query: VAET (backrefs) -----
250
+
251
+ def backrefs(
252
+ self,
253
+ target: str,
254
+ attribute: str | None = None,
255
+ as_of_tx: int | None = None,
256
+ ) -> list[tuple[str, str]]:
257
+ """Return [(entity_id, attribute)] for ref triples pointing to target.
258
+
259
+ Uses VAET index (partial index on value_type='ref').
260
+ """
261
+ conditions = ["value = ?", "value_type = 'ref'"]
262
+ params: list = [target]
263
+ if attribute:
264
+ conditions.append("attribute = ?")
265
+ params.append(attribute)
266
+ if as_of_tx is not None:
267
+ conditions.append("tx_id <= ?")
268
+ conditions.append("(retracted = 0 OR retracted_tx > ?)")
269
+ params.append(as_of_tx)
270
+ params.append(as_of_tx)
271
+ else:
272
+ conditions.append("retracted = 0")
273
+ where = " AND ".join(conditions)
274
+ rows = self._conn.execute(
275
+ f"SELECT entity_id, attribute FROM triples WHERE {where} ORDER BY entity_id",
276
+ params,
277
+ ).fetchall()
278
+ return [(r["entity_id"], r["attribute"]) for r in rows]
279
+
280
+ # ----- Query: AVET (lookup by attribute+value) -----
281
+
282
+ def lookup(
283
+ self, attribute: str, value: str, as_of_tx: int | None = None
284
+ ) -> list[str]:
285
+ """Return entity_ids that have the exact attribute=value.
286
+
287
+ Uses AVET index.
288
+ """
289
+ if as_of_tx is not None:
290
+ rows = self._conn.execute(
291
+ "SELECT DISTINCT entity_id FROM triples "
292
+ "WHERE attribute = ? AND value = ? AND tx_id <= ? "
293
+ "AND (retracted = 0 OR retracted_tx > ?)",
294
+ (attribute, value, as_of_tx, as_of_tx),
295
+ ).fetchall()
296
+ else:
297
+ rows = self._conn.execute(
298
+ "SELECT DISTINCT entity_id FROM triples "
299
+ "WHERE attribute = ? AND value = ? AND retracted = 0",
300
+ (attribute, value),
301
+ ).fetchall()
302
+ return [r["entity_id"] for r in rows]
303
+
304
+ # ----- Graph traversal: BFS neighbors -----
305
+
306
+ def neighbors(
307
+ self, entity_id: str, depth: int = 1, as_of_tx: int | None = None
308
+ ) -> dict[str, dict[str, list[str]]]:
309
+ """BFS traversal via ref edges. Returns {entity_id: {attr: [vals]}} for all
310
+ entities reachable within `depth` hops."""
311
+ visited: set[str] = set()
312
+ frontier = {entity_id}
313
+ result: dict[str, dict[str, list[str]]] = {}
314
+
315
+ for _ in range(depth + 1):
316
+ next_frontier: set[str] = set()
317
+ for eid in frontier:
318
+ if eid in visited:
319
+ continue
320
+ visited.add(eid)
321
+ attrs = self.entity(eid, as_of_tx)
322
+ if attrs:
323
+ result[eid] = attrs
324
+ # Follow outgoing refs
325
+ for attr, vals in attrs.items():
326
+ # Check which values are refs
327
+ for val in vals:
328
+ ref_rows = self._conn.execute(
329
+ "SELECT 1 FROM triples "
330
+ "WHERE entity_id = ? AND attribute = ? AND value = ? "
331
+ "AND value_type = 'ref' AND retracted = 0 LIMIT 1",
332
+ (eid, attr, val),
333
+ ).fetchone()
334
+ if ref_rows:
335
+ next_frontier.add(val)
336
+ # Follow incoming refs (backrefs)
337
+ for src_eid, _ in self.backrefs(eid, as_of_tx=as_of_tx):
338
+ next_frontier.add(src_eid)
339
+ frontier = next_frontier - visited
340
+
341
+ return result
342
+
343
+ # ----- Novelty (change feed) -----
344
+
345
+ def novelty(
346
+ self, since_tx: int, until_tx: int | None = None
347
+ ) -> list[dict]:
348
+ """Return triples asserted or retracted since since_tx (exclusive)."""
349
+ if until_tx is not None:
350
+ rows = self._conn.execute(
351
+ "SELECT id, tx_id, entity_id, attribute, value, value_type, retracted, created_at "
352
+ "FROM triples WHERE tx_id > ? AND tx_id <= ? ORDER BY id",
353
+ (since_tx, until_tx),
354
+ ).fetchall()
355
+ else:
356
+ rows = self._conn.execute(
357
+ "SELECT id, tx_id, entity_id, attribute, value, value_type, retracted, created_at "
358
+ "FROM triples WHERE tx_id > ? ORDER BY id",
359
+ (since_tx,),
360
+ ).fetchall()
361
+ return [dict(r) for r in rows]
362
+
363
+ # ----- Stats -----
364
+
365
+ def stats(self) -> dict:
366
+ """Return store statistics."""
367
+ triple_count = self._conn.execute(
368
+ "SELECT COUNT(*) FROM triples WHERE retracted = 0"
369
+ ).fetchone()[0]
370
+ entity_count = self._conn.execute(
371
+ "SELECT COUNT(*) FROM entity_types"
372
+ ).fetchone()[0]
373
+ tx_count = self._conn.execute(
374
+ "SELECT COUNT(*) FROM transactions"
375
+ ).fetchone()[0]
376
+ try:
377
+ db_size = os.path.getsize(self.db_path)
378
+ except OSError:
379
+ db_size = 0
380
+ return {
381
+ "triples": triple_count,
382
+ "entities": entity_count,
383
+ "transactions": tx_count,
384
+ "db_size_bytes": db_size,
385
+ }
386
+
387
+ # ----- Garbage collection -----
388
+
389
+ def gc(self, older_than_days: int = 30) -> int:
390
+ """Physically delete retracted triples older than N days. Returns count."""
391
+ cutoff = (datetime.now(timezone.utc) - timedelta(days=older_than_days)).isoformat()
392
+ cur = self._conn.execute(
393
+ "DELETE FROM triples WHERE retracted = 1 AND created_at < ?",
394
+ (cutoff,),
395
+ )
396
+ self._conn.commit()
397
+ return cur.rowcount
398
+
399
+
400
+ # ---------------------------------------------------------------------------
401
+ # Self-test
402
+ # ---------------------------------------------------------------------------
403
+
404
+ def _self_test() -> None:
405
+ """Run a quick self-test with a temp in-memory database."""
406
+ import tempfile
407
+
408
+ with tempfile.TemporaryDirectory() as tmpdir:
409
+ db_path = os.path.join(tmpdir, "test.db")
410
+ store = TripleStore(db_path)
411
+
412
+ # Transaction
413
+ tx1 = store.begin_tx("self-test", session_key="test-session")
414
+ assert tx1 > 0, "begin_tx should return positive tx_id"
415
+ assert store.latest_tx() == tx1
416
+
417
+ # Assert triples
418
+ store.assert_triple(tx1, "signal:2026-03-01", "description", "OCR stall detected")
419
+ store.assert_triple(tx1, "signal:2026-03-01", "priority", "high")
420
+ store.assert_triple(tx1, "signal:2026-03-01", "related_to", "concept:ocr", value_type="ref")
421
+ store.assert_triple(tx1, "concept:ocr", "name", "OCR")
422
+ store.assert_triple(tx1, "pattern:frame-batching", "text", "Frame batching improves OCR")
423
+ store.assert_triple(tx1, "pattern:frame-batching", "related_to", "concept:ocr", value_type="ref")
424
+
425
+ # EAVT: entity view
426
+ ent = store.entity("signal:2026-03-01")
427
+ assert "description" in ent, f"entity missing 'description': {ent}"
428
+ assert ent["description"] == ["OCR stall detected"]
429
+ assert ent["priority"] == ["high"]
430
+ assert ent["related_to"] == ["concept:ocr"]
431
+ print(" [OK] EAVT: entity view")
432
+
433
+ # AEVT: attribute scan
434
+ with_desc = store.entities_with_attr("description")
435
+ assert ("signal:2026-03-01", "OCR stall detected") in with_desc
436
+ print(" [OK] AEVT: entities_with_attr")
437
+
438
+ # VAET: backrefs
439
+ refs = store.backrefs("concept:ocr")
440
+ entity_ids = [r[0] for r in refs]
441
+ assert "signal:2026-03-01" in entity_ids
442
+ assert "pattern:frame-batching" in entity_ids
443
+ print(" [OK] VAET: backrefs")
444
+
445
+ # AVET: lookup
446
+ found = store.lookup("name", "OCR")
447
+ assert "concept:ocr" in found
448
+ print(" [OK] AVET: lookup")
449
+
450
+ # BFS neighbors
451
+ nbrs = store.neighbors("concept:ocr", depth=1)
452
+ assert "concept:ocr" in nbrs
453
+ assert "signal:2026-03-01" in nbrs or "pattern:frame-batching" in nbrs
454
+ print(" [OK] BFS neighbors")
455
+
456
+ # Novelty
457
+ tx2 = store.begin_tx("self-test-2")
458
+ store.assert_triple(tx2, "concept:test", "name", "Test")
459
+ changes = store.novelty(tx1)
460
+ assert len(changes) >= 1
461
+ assert any(c["entity_id"] == "concept:test" for c in changes)
462
+ print(" [OK] Novelty feed")
463
+
464
+ # Retraction
465
+ count = store.retract_triple(tx2, "signal:2026-03-01", "priority")
466
+ assert count == 1
467
+ ent_after = store.entity("signal:2026-03-01")
468
+ assert "priority" not in ent_after
469
+ print(" [OK] Retraction")
470
+
471
+ # as_of_tx isolation
472
+ ent_before = store.entity("signal:2026-03-01", as_of_tx=tx1)
473
+ assert "priority" in ent_before, "as_of_tx should see pre-retraction state"
474
+ print(" [OK] as_of_tx isolation")
475
+
476
+ # GC (retracted triples are fresh, so gc with 0 days should get them)
477
+ gc_count = store.gc(older_than_days=0)
478
+ assert gc_count >= 1
479
+ print(" [OK] Garbage collection")
480
+
481
+ # Stats
482
+ s = store.stats()
483
+ assert s["triples"] >= 1
484
+ assert s["entities"] >= 1
485
+ assert s["transactions"] >= 2
486
+ assert s["db_size_bytes"] > 0
487
+ print(f" [OK] Stats: {s}")
488
+
489
+ store.close()
490
+ print("\n All self-tests passed!")
491
+
492
+
493
+ if __name__ == "__main__":
494
+ if "--self-test" in sys.argv:
495
+ print("Running triplestore self-test...")
496
+ _self_test()
497
+ else:
498
+ print("Usage: python3 triplestore.py --self-test")