dsh-biocore 0.2.0 → 0.3.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 (245) hide show
  1. package/README.md +11 -0
  2. package/lib/core-worker.js +268 -0
  3. package/lib/index.js +160 -70
  4. package/lib/modules/autonomy.js +125 -0
  5. package/lib/modules/cache-stats.js +48 -0
  6. package/lib/modules/core-turn.js +24 -6
  7. package/lib/modules/expression-inject.js +0 -10
  8. package/lib/modules/expression-refresh.js +0 -9
  9. package/lib/modules/memory-capture.js +2 -1
  10. package/lib/modules/memory-inject.js +50 -5
  11. package/lib/modules/memory-tools.js +7 -4
  12. package/lib/modules/python-env.js +46 -0
  13. package/lib/modules/safety-gate.js +0 -3
  14. package/lib/modules/series.js +54 -0
  15. package/lib/modules/switch.js +16 -0
  16. package/lib/py-bridge.js +81 -15
  17. package/package.json +1 -1
  18. package/python/biocore_memory/core/config.py +138 -127
  19. package/python/biocore_memory/engine.py +2653 -2476
  20. package/python/biocore_memory/recall/embedding.py +129 -0
  21. package/python/biocore_memory/recall/feedback.py +202 -0
  22. package/python/biocore_model/preference_drift.py +255 -0
  23. package/python/biocore_model/tests/__init__.py +1 -0
  24. package/python/biocore_model/tests/core_mode_ui_check.mjs +63 -0
  25. package/python/biocore_model/tests/core_status_check.mjs +29 -0
  26. package/python/biocore_model/tests/dom_safe_check.mjs +22 -0
  27. package/python/biocore_model/tests/leak_samples_run.py +18 -0
  28. package/python/biocore_model/tests/orb_ring_check.mjs +20 -0
  29. package/python/biocore_model/tests/panel_html_check.mjs +63 -0
  30. package/python/biocore_model/tests/persona_flow_check.mjs +17 -0
  31. package/python/biocore_model/tests/render_guard_check.mjs +19 -0
  32. package/python/biocore_model/tests/switch_groups_check.mjs +94 -0
  33. package/python/biocore_model/tests/tabs_state_check.mjs +24 -0
  34. package/python/biocore_model/tests/test_affective_labor.py +102 -0
  35. package/python/biocore_model/tests/test_appraisal.py +137 -0
  36. package/python/biocore_model/tests/test_appraisal_action.py +55 -0
  37. package/python/biocore_model/tests/test_arch_convergence.py +35 -0
  38. package/python/biocore_model/tests/test_attachment_behavior.py +95 -0
  39. package/python/biocore_model/tests/test_attachment_regulation.py +63 -0
  40. package/python/biocore_model/tests/test_attachment_state.py +29 -0
  41. package/python/biocore_model/tests/test_attribution.py +56 -0
  42. package/python/biocore_model/tests/test_attributor.py +108 -0
  43. package/python/biocore_model/tests/test_autonomic.py +69 -0
  44. package/python/biocore_model/tests/test_autonomy_accumulator.py +84 -0
  45. package/python/biocore_model/tests/test_autonomy_affect.py +62 -0
  46. package/python/biocore_model/tests/test_autonomy_blame.py +44 -0
  47. package/python/biocore_model/tests/test_autonomy_caps.py +29 -0
  48. package/python/biocore_model/tests/test_autonomy_check.py +40 -0
  49. package/python/biocore_model/tests/test_autonomy_clingy.py +41 -0
  50. package/python/biocore_model/tests/test_autonomy_cooldown_selfcontact.py +138 -0
  51. package/python/biocore_model/tests/test_autonomy_frustration.py +42 -0
  52. package/python/biocore_model/tests/test_autonomy_hourly_limit.py +46 -0
  53. package/python/biocore_model/tests/test_autonomy_loop.py +64 -0
  54. package/python/biocore_model/tests/test_autonomy_mode.py +37 -0
  55. package/python/biocore_model/tests/test_autonomy_model.py +60 -0
  56. package/python/biocore_model/tests/test_autonomy_model_v2.py +68 -0
  57. package/python/biocore_model/tests/test_autonomy_module_text.py +104 -0
  58. package/python/biocore_model/tests/test_autonomy_params.py +163 -0
  59. package/python/biocore_model/tests/test_autonomy_reason.py +34 -0
  60. package/python/biocore_model/tests/test_autonomy_self.py +153 -0
  61. package/python/biocore_model/tests/test_autonomy_send_gate.py +38 -0
  62. package/python/biocore_model/tests/test_autonomy_silence.py +37 -0
  63. package/python/biocore_model/tests/test_autonomy_status.py +35 -0
  64. package/python/biocore_model/tests/test_autonomy_test_trigger.py +24 -0
  65. package/python/biocore_model/tests/test_autonomy_tick.py +148 -0
  66. package/python/biocore_model/tests/test_autonomy_v4.py +64 -0
  67. package/python/biocore_model/tests/test_autonomy_v5.py +43 -0
  68. package/python/biocore_model/tests/test_autonomy_v7.py +48 -0
  69. package/python/biocore_model/tests/test_autonomy_v_theta.py +85 -0
  70. package/python/biocore_model/tests/test_batch1_wiring.py +231 -0
  71. package/python/biocore_model/tests/test_batch2_wiring.py +231 -0
  72. package/python/biocore_model/tests/test_batch3_wiring.py +223 -0
  73. package/python/biocore_model/tests/test_bio_tick.py +202 -0
  74. package/python/biocore_model/tests/test_boundary.py +183 -0
  75. package/python/biocore_model/tests/test_boundary_gate.py +295 -0
  76. package/python/biocore_model/tests/test_brain_split.py +99 -0
  77. package/python/biocore_model/tests/test_co_recall.py +350 -0
  78. package/python/biocore_model/tests/test_commitment_extractor.py +197 -0
  79. package/python/biocore_model/tests/test_compassion_fatigue.py +84 -0
  80. package/python/biocore_model/tests/test_connection_bids.py +69 -0
  81. package/python/biocore_model/tests/test_core_bridge.py +88 -0
  82. package/python/biocore_model/tests/test_core_bridge_episode.py +14 -0
  83. package/python/biocore_model/tests/test_core_bridge_forget.py +17 -0
  84. package/python/biocore_model/tests/test_core_bridge_identity.py +15 -0
  85. package/python/biocore_model/tests/test_core_bridge_media.py +17 -0
  86. package/python/biocore_model/tests/test_core_bridge_relation.py +17 -0
  87. package/python/biocore_model/tests/test_core_bridge_summary.py +17 -0
  88. package/python/biocore_model/tests/test_core_client.py +156 -0
  89. package/python/biocore_model/tests/test_core_commands.py +146 -0
  90. package/python/biocore_model/tests/test_core_mode.py +161 -0
  91. package/python/biocore_model/tests/test_core_state.py +240 -0
  92. package/python/biocore_model/tests/test_criticality.py +61 -0
  93. package/python/biocore_model/tests/test_cross_session_copy.py +67 -0
  94. package/python/biocore_model/tests/test_debug_values.py +169 -0
  95. package/python/biocore_model/tests/test_defense_mechanisms.py +177 -0
  96. package/python/biocore_model/tests/test_delta_budget.py +157 -0
  97. package/python/biocore_model/tests/test_diag_trace.py +201 -0
  98. package/python/biocore_model/tests/test_directives_compat.py +698 -0
  99. package/python/biocore_model/tests/test_dissonance.py +97 -0
  100. package/python/biocore_model/tests/test_drift_sign.py +87 -0
  101. package/python/biocore_model/tests/test_embedding_fallback.py +277 -0
  102. package/python/biocore_model/tests/test_embedding_local.py +279 -0
  103. package/python/biocore_model/tests/test_embedding_mode.py +175 -0
  104. package/python/biocore_model/tests/test_embedding_modes.py +342 -0
  105. package/python/biocore_model/tests/test_emotion_geometry.py +114 -0
  106. package/python/biocore_model/tests/test_emotion_probe.py +214 -0
  107. package/python/biocore_model/tests/test_emotion_probe_concurrency.py +128 -0
  108. package/python/biocore_model/tests/test_entity_graph.py +198 -0
  109. package/python/biocore_model/tests/test_entity_resolve.py +446 -0
  110. package/python/biocore_model/tests/test_envy.py +173 -0
  111. package/python/biocore_model/tests/test_exit_or_silence.py +209 -0
  112. package/python/biocore_model/tests/test_expr_guard.py +1 -0
  113. package/python/biocore_model/tests/test_expr_intent.py +66 -0
  114. package/python/biocore_model/tests/test_expr_state.py +66 -0
  115. package/python/biocore_model/tests/test_expr_template.py +71 -0
  116. package/python/biocore_model/tests/test_expr_tone.py +41 -0
  117. package/python/biocore_model/tests/test_extreme_scenarios.py +147 -0
  118. package/python/biocore_model/tests/test_feedback_flow.py +185 -0
  119. package/python/biocore_model/tests/test_feedback_guard.py +65 -0
  120. package/python/biocore_model/tests/test_feedback_llm.py +59 -0
  121. package/python/biocore_model/tests/test_feedback_llm_inferred.py +140 -0
  122. package/python/biocore_model/tests/test_feedback_memory.py +63 -0
  123. package/python/biocore_model/tests/test_feedback_user.py +61 -0
  124. package/python/biocore_model/tests/test_frustration_curve.py +66 -0
  125. package/python/biocore_model/tests/test_global_bio_state.py +116 -0
  126. package/python/biocore_model/tests/test_governance.py +181 -0
  127. package/python/biocore_model/tests/test_granularity.py +133 -0
  128. package/python/biocore_model/tests/test_graph_extract.py +224 -0
  129. package/python/biocore_model/tests/test_graph_recall.py +226 -0
  130. package/python/biocore_model/tests/test_graph_sqlite.py +298 -0
  131. package/python/biocore_model/tests/test_growth.py +301 -0
  132. package/python/biocore_model/tests/test_intent_render.py +60 -0
  133. package/python/biocore_model/tests/test_intero_regulation.py +224 -0
  134. package/python/biocore_model/tests/test_intero_style.py +146 -0
  135. package/python/biocore_model/tests/test_item3_numeric.py +105 -0
  136. package/python/biocore_model/tests/test_item6_numeric.py +116 -0
  137. package/python/biocore_model/tests/test_layered_memory.py +174 -0
  138. package/python/biocore_model/tests/test_leak_guard.py +75 -0
  139. package/python/biocore_model/tests/test_learning_probe.py +93 -0
  140. package/python/biocore_model/tests/test_life_loop.py +222 -0
  141. package/python/biocore_model/tests/test_life_loop_10s.py +131 -0
  142. package/python/biocore_model/tests/test_local_embedding_health.py +258 -0
  143. package/python/biocore_model/tests/test_memory_affect_encoding.py +79 -0
  144. package/python/biocore_model/tests/test_memory_backend.py +332 -0
  145. package/python/biocore_model/tests/test_memory_dedup.py +316 -0
  146. package/python/biocore_model/tests/test_memory_dual.py +219 -0
  147. package/python/biocore_model/tests/test_memory_dual_wiring.py +219 -0
  148. package/python/biocore_model/tests/test_memory_feedback.py +241 -0
  149. package/python/biocore_model/tests/test_memory_fixes.py +84 -0
  150. package/python/biocore_model/tests/test_memory_forgetting.py +58 -0
  151. package/python/biocore_model/tests/test_memory_inject.py +211 -0
  152. package/python/biocore_model/tests/test_memory_mood_recall.py +118 -0
  153. package/python/biocore_model/tests/test_memory_store.py +372 -0
  154. package/python/biocore_model/tests/test_metacog.py +103 -0
  155. package/python/biocore_model/tests/test_migrate.py +245 -0
  156. package/python/biocore_model/tests/test_migrate_store.py +194 -0
  157. package/python/biocore_model/tests/test_mind_wander.py +65 -0
  158. package/python/biocore_model/tests/test_mood.py +35 -0
  159. package/python/biocore_model/tests/test_narrative.py +114 -0
  160. package/python/biocore_model/tests/test_needs_esteem.py +51 -0
  161. package/python/biocore_model/tests/test_needs_events.py +205 -0
  162. package/python/biocore_model/tests/test_needs_sleep.py +54 -0
  163. package/python/biocore_model/tests/test_noise_bank.py +54 -0
  164. package/python/biocore_model/tests/test_numeric_schema.py +98 -0
  165. package/python/biocore_model/tests/test_output_protocol.py +65 -0
  166. package/python/biocore_model/tests/test_page_debug.py +147 -0
  167. package/python/biocore_model/tests/test_page_status_autonomy.py +215 -0
  168. package/python/biocore_model/tests/test_pipeline.py +345 -0
  169. package/python/biocore_model/tests/test_pipeline_api.py +192 -0
  170. package/python/biocore_model/tests/test_pipeline_mode.py +222 -0
  171. package/python/biocore_model/tests/test_plugin_config.py +85 -0
  172. package/python/biocore_model/tests/test_policy_learning.py +116 -0
  173. package/python/biocore_model/tests/test_possible_selves.py +88 -0
  174. package/python/biocore_model/tests/test_predictor.py +227 -0
  175. package/python/biocore_model/tests/test_preference_drift.py +142 -0
  176. package/python/biocore_model/tests/test_preset.py +142 -0
  177. package/python/biocore_model/tests/test_proactive_context.py +70 -0
  178. package/python/biocore_model/tests/test_proactive_history.py +90 -0
  179. package/python/biocore_model/tests/test_proactive_memory.py +141 -0
  180. package/python/biocore_model/tests/test_proactive_send_record.py +88 -0
  181. package/python/biocore_model/tests/test_probe_contract.py +71 -0
  182. package/python/biocore_model/tests/test_prospective.py +154 -0
  183. package/python/biocore_model/tests/test_provider_fallback.py +89 -0
  184. package/python/biocore_model/tests/test_reconstruction.py +111 -0
  185. package/python/biocore_model/tests/test_regulation_strategies.py +228 -0
  186. package/python/biocore_model/tests/test_relation_stage.py +114 -0
  187. package/python/biocore_model/tests/test_relationship_stage.py +59 -0
  188. package/python/biocore_model/tests/test_release_140.py +55 -0
  189. package/python/biocore_model/tests/test_repair_after_boundary.py +249 -0
  190. package/python/biocore_model/tests/test_reply_limit.py +86 -0
  191. package/python/biocore_model/tests/test_reply_limit_persist.py +48 -0
  192. package/python/biocore_model/tests/test_reply_split.py +52 -0
  193. package/python/biocore_model/tests/test_reply_split_astrbot.py +66 -0
  194. package/python/biocore_model/tests/test_reply_split_inject.py +53 -0
  195. package/python/biocore_model/tests/test_rerank.py +229 -0
  196. package/python/biocore_model/tests/test_rollback.py +175 -0
  197. package/python/biocore_model/tests/test_runtime.py +175 -0
  198. package/python/biocore_model/tests/test_runtime_active.py +205 -0
  199. package/python/biocore_model/tests/test_runtime_pipeline.py +1583 -0
  200. package/python/biocore_model/tests/test_runtime_proactive.py +231 -0
  201. package/python/biocore_model/tests/test_scheduler.py +79 -0
  202. package/python/biocore_model/tests/test_secrecy.py +105 -0
  203. package/python/biocore_model/tests/test_self_esteem.py +45 -0
  204. package/python/biocore_model/tests/test_self_intent.py +347 -0
  205. package/python/biocore_model/tests/test_self_interest.py +183 -0
  206. package/python/biocore_model/tests/test_self_layer_wiring.py +198 -0
  207. package/python/biocore_model/tests/test_self_verification.py +113 -0
  208. package/python/biocore_model/tests/test_situation.py +142 -0
  209. package/python/biocore_model/tests/test_sleep_circadian.py +41 -0
  210. package/python/biocore_model/tests/test_social_pain.py +49 -0
  211. package/python/biocore_model/tests/test_social_repair.py +67 -0
  212. package/python/biocore_model/tests/test_social_theory.py +78 -0
  213. package/python/biocore_model/tests/test_source_monitoring.py +204 -0
  214. package/python/biocore_model/tests/test_speak_like_group.py +36 -0
  215. package/python/biocore_model/tests/test_stages.py +226 -0
  216. package/python/biocore_model/tests/test_state_tag.py +86 -0
  217. package/python/biocore_model/tests/test_template_expr.py +65 -0
  218. package/python/biocore_model/tests/test_text_inference.py +70 -0
  219. package/python/biocore_model/tests/test_theta_params.py +115 -0
  220. package/python/biocore_model/tests/test_topic_control.py +200 -0
  221. package/python/biocore_model/tests/test_turn_memory.py +105 -0
  222. package/python/biocore_model/tests/test_turn_memory_panel.py +174 -0
  223. package/python/biocore_model/tests/test_ui_settings.py +84 -0
  224. package/python/biocore_model/tests/test_ui_settings_authority.py +179 -0
  225. package/python/biocore_model/tests/test_user_appraisal_async.py +155 -0
  226. package/python/biocore_model/tests/test_user_classify.py +122 -0
  227. package/python/biocore_model/tests/test_value_order.py +117 -0
  228. package/python/biocore_model/tests/test_voice_bank.py +105 -0
  229. package/python/biocore_model/tests/test_waiting_sense.py +216 -0
  230. package/python/biocore_model/tests/test_x_directives.py +101 -0
  231. package/python/biocore_model/tests/ui_compat_check.mjs +22 -0
  232. package/python/biocore_model/tests/view_mode_check.mjs +17 -0
  233. package/python/biocore_runtime/autonomy.py +208 -0
  234. package/python/biocore_runtime/core_state.py +302 -0
  235. package/python/biocore_runtime/governance.py +495 -0
  236. package/python/biocore_runtime/growth.py +528 -0
  237. package/python/biocore_runtime/pipeline.py +850 -23
  238. package/python/biocore_runtime/relationship_state.py +214 -0
  239. package/python/biocore_runtime/self_layer.py +103 -0
  240. package/python/biocore_runtime/service.py +162 -0
  241. package/python/biocore_runtime/standby.py +472 -0
  242. package/ui/panel-layout.js +83 -39
  243. package/ui/panel.css +885 -743
  244. package/ui/panel.html +356 -296
  245. package/ui/panel.js +739 -865
@@ -0,0 +1,372 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-146: SQLite memory store + upgrade/degrade compatibility scenarios."""
3
+ import json
4
+ import sqlite3
5
+ import tempfile
6
+ import unittest
7
+ from pathlib import Path
8
+
9
+ from biocore_model import memory_store as ms
10
+
11
+ VECTOR = [round((i - 10) / 100.0, 4) for i in range(1024)]
12
+
13
+
14
+ def _doc(name="friend", count=3):
15
+ entries = []
16
+ for index in range(count):
17
+ entries.append({
18
+ "id": "%s%02d" % (name, index),
19
+ "text": "%s 的第 %d 条记忆" % (name, index),
20
+ "role": "user" if index % 2 == 0 else "assistant",
21
+ "ts": 1000.0 + index,
22
+ "importance": 0.5 + index * 0.1,
23
+ "vector": VECTOR,
24
+ "storage_s": 0.45, "retrieval_s": 0.5, "access_count": 2,
25
+ "hits": index, "inhibition": 0.0, "suppression_count": 0,
26
+ "memory_tier": "hot" if index == 0 else "warm",
27
+ "memory_source": "direct", "memory_confidence": 0.7,
28
+ "arousal_at_encoding": 0.4, "hidden": False,
29
+ "tags": ["t%d" % index],
30
+ "unknown_future_field": {"keep": index},
31
+ })
32
+ return {"schema_version": 3, "entries": entries,
33
+ "tombstones": [{"id": "gone", "ts": 1.0, "status": "deleted"}],
34
+ "working_set": ["friend00"], "write_log": [{"op": "ADD"}]}
35
+
36
+
37
+ def _write_json(memory_dir, name, doc):
38
+ path = Path(memory_dir) / (name + ".json")
39
+ path.write_text(json.dumps(doc, ensure_ascii=False), encoding="utf-8")
40
+ return path
41
+
42
+
43
+ class SchemaTest(unittest.TestCase):
44
+ def test_tables_indexes_and_pragmas(self):
45
+ with tempfile.TemporaryDirectory() as tmp:
46
+ store = ms.open_store(tmp)
47
+ try:
48
+ tables = {row[0] for row in store._conn.execute(
49
+ "SELECT name FROM sqlite_master WHERE type='table'")}
50
+ indexes = {row[0] for row in store._conn.execute(
51
+ "SELECT name FROM sqlite_master WHERE type='index'")}
52
+ for name in ("entries", "session_docs", "meta"):
53
+ self.assertIn(name, tables)
54
+ for name in ("idx_entries_ts", "idx_entries_importance",
55
+ "idx_entries_hidden", "idx_entries_tier",
56
+ "idx_entries_session"):
57
+ self.assertIn(name, indexes)
58
+ self.assertEqual(store._conn.execute(
59
+ "PRAGMA journal_mode").fetchone()[0].lower(), "wal")
60
+ self.assertEqual(store._conn.execute(
61
+ "PRAGMA busy_timeout").fetchone()[0], 5000)
62
+ self.assertEqual(store.schema_version(), ms.SCHEMA_VERSION)
63
+ self.assertIn(ms.TEXT_HASH_INDEX, indexes)
64
+ self.assertTrue(Path(tmp, ms.SQLITE_FILE).is_file())
65
+ finally:
66
+ store.close()
67
+
68
+
69
+ class CrudTest(unittest.TestCase):
70
+ def test_save_load_get_delete(self):
71
+ with tempfile.TemporaryDirectory() as tmp:
72
+ store = ms.open_store(tmp)
73
+ try:
74
+ result = store.save_doc("friend", _doc(count=3))
75
+ self.assertEqual(result["entries"], 3)
76
+ self.assertTrue(result["json_mirror"])
77
+ doc = store.load_doc("friend")
78
+ self.assertEqual(len(doc["entries"]), 3)
79
+ self.assertEqual(doc["entries"][0]["text"], "friend 的第 0 条记忆")
80
+ self.assertEqual(store.get_entry("friend", "friend00")["role"],
81
+ "user")
82
+ self.assertTrue(store.delete_entry("friend", "friend00"))
83
+ self.assertIsNone(store.get_entry("friend", "friend00"))
84
+ self.assertEqual(store.stats()["entries"], 2)
85
+ finally:
86
+ store.close()
87
+
88
+ def test_vector_blob_roundtrip(self):
89
+ with tempfile.TemporaryDirectory() as tmp:
90
+ store = ms.open_store(tmp)
91
+ try:
92
+ store.save_doc("friend", _doc(count=1))
93
+ entry = store.load_doc("friend")["entries"][0]
94
+ self.assertEqual(len(entry["vector"]), 1024)
95
+ self.assertAlmostEqual(entry["vector"][0], VECTOR[0], places=9)
96
+ self.assertAlmostEqual(entry["vector"][-1], VECTOR[-1], places=9)
97
+ blob = store._conn.execute(
98
+ "SELECT vector FROM entries").fetchone()[0]
99
+ self.assertIsInstance(blob, bytes)
100
+ self.assertEqual(len(blob), 1024 * 8)
101
+ finally:
102
+ store.close()
103
+
104
+ def test_unknown_fields_and_extras_roundtrip(self):
105
+ with tempfile.TemporaryDirectory() as tmp:
106
+ store = ms.open_store(tmp)
107
+ try:
108
+ store.save_doc("friend", _doc(count=1))
109
+ doc = store.load_doc("friend")
110
+ entry = doc["entries"][0]
111
+ self.assertEqual(entry["unknown_future_field"], {"keep": 0})
112
+ self.assertEqual(entry["tags"], ["t0"])
113
+ self.assertEqual(len(doc["tombstones"]), 1)
114
+ self.assertEqual(doc["working_set"], ["friend00"])
115
+ self.assertEqual(doc["write_log"], [{"op": "ADD"}])
116
+ finally:
117
+ store.close()
118
+
119
+ def test_identical_text_is_deduped_per_session(self):
120
+ # task-147: same normalized text inside one session is one row; the
121
+ # same text in another session is a separate row (unique per session).
122
+ with tempfile.TemporaryDirectory() as tmp:
123
+ store = ms.open_store(tmp)
124
+ try:
125
+ doc = {"entries": [{"text": "同样的话", "ts": 1.0},
126
+ {"text": "同样的话", "ts": 2.0}]}
127
+ store.save_doc("s1", doc)
128
+ store.save_doc("s2", {"entries": [{"text": "同样的话", "ts": 3.0}]})
129
+ total = store.stats()["entries"]
130
+ self.assertEqual(total, 2)
131
+ ids = {row[0] for row in store._conn.execute(
132
+ "SELECT id FROM entries")}
133
+ self.assertEqual(len(ids), 2)
134
+ per_session = {row[0]: row[1] for row in store._conn.execute(
135
+ "SELECT session, COUNT(*) FROM entries GROUP BY session")}
136
+ self.assertEqual(per_session, {"s1": 1, "s2": 1})
137
+ finally:
138
+ store.close()
139
+
140
+
141
+ class MirrorTest(unittest.TestCase):
142
+ def test_write_updates_json_mirror(self):
143
+ with tempfile.TemporaryDirectory() as tmp:
144
+ store = ms.open_store(tmp)
145
+ try:
146
+ store.save_doc("friend", _doc(count=2))
147
+ mirror = Path(tmp) / "friend.json"
148
+ self.assertTrue(mirror.is_file())
149
+ mirrored = json.loads(mirror.read_text(encoding="utf-8"))
150
+ self.assertEqual(len(mirrored["entries"]), 2)
151
+ self.assertTrue(mirrored["entries"][0]["id"])
152
+ store.save_doc("friend", {"entries": []})
153
+ mirrored = json.loads(mirror.read_text(encoding="utf-8"))
154
+ self.assertEqual(mirrored["entries"], [])
155
+ finally:
156
+ store.close()
157
+
158
+ def test_hidden_and_tier_columns(self):
159
+ with tempfile.TemporaryDirectory() as tmp:
160
+ store = ms.open_store(tmp)
161
+ try:
162
+ store.save_doc("friend", _doc(count=3))
163
+ rows = store._conn.execute(
164
+ "SELECT memory_tier, hidden FROM entries ORDER BY seq").fetchall()
165
+ self.assertEqual(rows[0][0], "hot")
166
+ self.assertEqual(rows[1][0], "warm")
167
+ self.assertEqual(rows[0][1], 0)
168
+ finally:
169
+ store.close()
170
+
171
+
172
+ class CorruptionTest(unittest.TestCase):
173
+ def test_corrupt_db_recovers_with_backup(self):
174
+ with tempfile.TemporaryDirectory() as tmp:
175
+ (Path(tmp) / ms.SQLITE_FILE).write_bytes(b"not a sqlite database")
176
+ report = ms.ensure_store(tmp)
177
+ self.assertTrue(report["ok"])
178
+ self.assertTrue(report["recovered"])
179
+ self.assertTrue(Path(tmp, ms.SQLITE_FILE + ".corrupt.bak").is_file())
180
+ store = ms.open_store(tmp)
181
+ try:
182
+ self.assertEqual(store.stats()["entries"], 0)
183
+ store.save_doc("s", {"entries": [{"text": "ok"}]})
184
+ self.assertEqual(store.stats()["entries"], 1)
185
+ finally:
186
+ store.close()
187
+
188
+ def test_corrupt_json_is_skipped(self):
189
+ with tempfile.TemporaryDirectory() as tmp:
190
+ _write_json(tmp, "friend", _doc(count=2))
191
+ (Path(tmp) / "broken.json").write_text("{not json", encoding="utf-8")
192
+ report = ms.ensure_store(tmp)
193
+ self.assertTrue(report["ok"])
194
+ self.assertEqual(report["entries"], 2)
195
+ self.assertIn("broken.json", report["skipped"])
196
+
197
+
198
+ class MigrationScenarioTest(unittest.TestCase):
199
+ def test_empty_db_with_json_imports(self):
200
+ """场景1:空库 + 有 JSON -> 自动导入,不报错。"""
201
+ with tempfile.TemporaryDirectory() as tmp:
202
+ _write_json(tmp, "friend", _doc("friend", 3))
203
+ _write_json(tmp, "group", _doc("group", 2))
204
+ report = ms.ensure_store(tmp)
205
+ self.assertTrue(report["ok"], report)
206
+ self.assertEqual(report["state"], "imported")
207
+ self.assertEqual(report["sessions"], 2)
208
+ self.assertEqual(report["entries"], 5)
209
+ self.assertTrue(report["backup"]["dir"])
210
+ backup = Path(report["backup"]["dir"])
211
+ self.assertTrue((backup / "friend.json").is_file())
212
+ store = ms.open_store(tmp)
213
+ try:
214
+ self.assertEqual(len(store.load_doc("friend")["entries"]), 3)
215
+ self.assertEqual(len(store.load_doc("group")["entries"]), 2)
216
+ finally:
217
+ store.close()
218
+
219
+ def test_db_and_json_both_present_config_wins(self):
220
+ """场景2:有库 + 有 JSON -> 以库为准,不做隐式合并/去重。"""
221
+ with tempfile.TemporaryDirectory() as tmp:
222
+ _write_json(tmp, "friend", _doc("friend", 2))
223
+ first = ms.ensure_store(tmp)
224
+ self.assertEqual(first["entries"], 2)
225
+ # now the JSON gains one extra entry (simulating a stale/other copy)
226
+ _write_json(tmp, "friend", _doc("friend", 5))
227
+ second = ms.ensure_store(tmp)
228
+ self.assertTrue(second["ok"])
229
+ self.assertEqual(second["entries"], 2) # DB authoritative
230
+ store = ms.open_store(tmp)
231
+ try:
232
+ self.assertEqual(len(store.load_doc("friend")["entries"]), 2)
233
+ finally:
234
+ store.close()
235
+
236
+ def test_corrupt_db_with_json_rebuilds(self):
237
+ """场景3:库损坏 + JSON 有数据 -> 备份损坏库并从 JSON 重建。"""
238
+ with tempfile.TemporaryDirectory() as tmp:
239
+ _write_json(tmp, "friend", _doc("friend", 3))
240
+ (Path(tmp) / ms.SQLITE_FILE).write_bytes(b"garbage")
241
+ report = ms.ensure_store(tmp)
242
+ self.assertTrue(report["ok"])
243
+ self.assertTrue(report["recovered"])
244
+ self.assertEqual(report["entries"], 3)
245
+ self.assertTrue(Path(tmp, ms.SQLITE_FILE + ".corrupt.bak").is_file())
246
+
247
+ def test_corrupt_json_with_healthy_db_still_reads(self):
248
+ """场景4:JSON 损坏 + 库健康 -> 库可读,损坏 JSON 跳过。"""
249
+ with tempfile.TemporaryDirectory() as tmp:
250
+ _write_json(tmp, "friend", _doc("friend", 2))
251
+ ms.ensure_store(tmp)
252
+ (Path(tmp) / "friend.json").write_text("{broken", encoding="utf-8")
253
+ report = ms.ensure_store(tmp)
254
+ self.assertTrue(report["ok"])
255
+ self.assertEqual(report["entries"], 2)
256
+ store = ms.open_store(tmp)
257
+ try:
258
+ self.assertEqual(len(store.load_doc("friend")["entries"]), 2)
259
+ finally:
260
+ store.close()
261
+
262
+ def test_json_only_downgrade_startup(self):
263
+ """场景5:降级兼容(只有 JSON 文件也能启动)。"""
264
+ with tempfile.TemporaryDirectory() as tmp:
265
+ _write_json(tmp, "friend", _doc("friend", 3))
266
+ backend = ms.resolve_backend("auto", tmp)
267
+ self.assertEqual(backend, "json")
268
+ counts = ms.json_counts(tmp)
269
+ self.assertEqual(counts["entries"], 3)
270
+ self.assertFalse(ms.db_present_healthy(tmp))
271
+
272
+ def test_migration_idempotent(self):
273
+ with tempfile.TemporaryDirectory() as tmp:
274
+ _write_json(tmp, "friend", _doc("friend", 4))
275
+ first = ms.ensure_store(tmp)
276
+ second = ms.ensure_store(tmp)
277
+ self.assertEqual(first["entries"], 4)
278
+ self.assertEqual(second["entries"], 4)
279
+ self.assertEqual(second["state"], "ok")
280
+
281
+
282
+ class PerfAndPathTest(unittest.TestCase):
283
+ def test_raw_column_does_not_duplicate_vector(self):
284
+ with tempfile.TemporaryDirectory() as tmp:
285
+ store = ms.open_store(tmp)
286
+ try:
287
+ store.save_doc("friend", _doc(count=2))
288
+ raws = [row[0] for row in store._conn.execute(
289
+ "SELECT raw FROM entries")]
290
+ for raw in raws:
291
+ self.assertNotIn('"vector"', raw)
292
+ entry = store.load_doc("friend")["entries"][0]
293
+ self.assertEqual(len(entry["vector"]), 1024)
294
+ raw_bytes = sum(len(raw) for raw in raws)
295
+ blob_bytes = sum(int(row[0]) for row in store._conn.execute(
296
+ "SELECT LENGTH(vector) FROM entries"))
297
+ # raw must no longer carry a full copy of the vector data
298
+ self.assertLess(raw_bytes, blob_bytes)
299
+ finally:
300
+ store.close()
301
+
302
+ def test_compact_raw_drops_legacy_vector_duplication(self):
303
+ with tempfile.TemporaryDirectory() as tmp:
304
+ store = ms.open_store(tmp)
305
+ try:
306
+ store.save_doc("friend", _doc(count=1))
307
+ entry = store.load_doc("friend")["entries"][0]
308
+ store._conn.execute(
309
+ "UPDATE entries SET raw=? WHERE id=?",
310
+ (json.dumps(entry), entry["id"]))
311
+ store._conn.commit()
312
+ self.assertEqual(store.compact_raw(), 1)
313
+ raw = store._conn.execute(
314
+ "SELECT raw FROM entries").fetchone()[0]
315
+ self.assertNotIn('"vector"', raw)
316
+ loaded = store.load_doc("friend")["entries"][0]
317
+ self.assertEqual(len(loaded["vector"]), 1024)
318
+ self.assertEqual(store.compact_raw(), 0) # idempotent
319
+ finally:
320
+ store.close()
321
+
322
+ def test_export_default_path_outside_legacy_glob(self):
323
+ with tempfile.TemporaryDirectory() as tmp:
324
+ store = ms.open_store(tmp)
325
+ try:
326
+ store.save_doc("friend", _doc(count=2))
327
+ exported = store.export_json()
328
+ self.assertTrue(Path(exported).is_file())
329
+ self.assertEqual(Path(exported).parent.name, "exports")
330
+ # legacy scanners glob("*.json") on the memory dir itself and
331
+ # must not see the export as a fake session
332
+ counts = ms.json_counts(tmp)
333
+ self.assertEqual(counts, {"sessions": 1, "entries": 2})
334
+ finally:
335
+ store.close()
336
+
337
+
338
+ class ExportImportTest(unittest.TestCase):
339
+ def test_export_import_roundtrip(self):
340
+ with tempfile.TemporaryDirectory() as tmp, tempfile.TemporaryDirectory() as other:
341
+ store = ms.open_store(tmp)
342
+ try:
343
+ store.save_doc("friend", _doc(count=3))
344
+ exported = store.export_json()
345
+ self.assertTrue(Path(exported).is_file())
346
+ finally:
347
+ store.close()
348
+ store2 = ms.open_store(other)
349
+ try:
350
+ report = store2.import_json(exported)
351
+ self.assertEqual(report["entries"], 3)
352
+ self.assertEqual(len(store2.load_doc("friend")["entries"]), 3)
353
+ self.assertTrue((Path(other) / "friend.json").is_file())
354
+ finally:
355
+ store2.close()
356
+
357
+ def test_restore_from_json_backup(self):
358
+ with tempfile.TemporaryDirectory() as tmp, tempfile.TemporaryDirectory() as backup:
359
+ _write_json(backup, "friend", _doc("friend", 2))
360
+ report = ms.restore(backup, tmp)
361
+ self.assertTrue(report["ok"], report)
362
+ self.assertEqual(report["restored_files"], 1)
363
+ store = ms.open_store(tmp)
364
+ try:
365
+ self.assertEqual(store.stats()["entries"], 2)
366
+ self.assertEqual(len(store.load_doc("friend")["entries"]), 2)
367
+ finally:
368
+ store.close()
369
+
370
+
371
+ if __name__ == "__main__":
372
+ unittest.main()
@@ -0,0 +1,103 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Metacognition: confidence bands, stance conflict, convergence."""
3
+ import unittest
4
+
5
+ from biocore_model import metacog as mc
6
+
7
+ class BandTest(unittest.TestCase):
8
+ def test_bands(self):
9
+ self.assertEqual(mc.band(0.2), "low")
10
+ self.assertEqual(mc.band(0.6), "mid")
11
+ self.assertEqual(mc.band(0.9), "high")
12
+
13
+ def test_bad_input_is_low(self):
14
+ self.assertEqual(mc.band("x"), "low")
15
+ self.assertEqual(mc.band(None), "low")
16
+
17
+ class GuardTest(unittest.TestCase):
18
+ def test_hits_lower_confidence(self):
19
+ out = mc.evaluate(confidence=0.8, guard_hits=1)
20
+ self.assertAlmostEqual(out["confidence"], 0.7)
21
+ self.assertTrue(out["uncertainty"])
22
+
23
+ def test_penalty_capped(self):
24
+ out = mc.evaluate(confidence=0.9, guard_hits=5)
25
+ self.assertAlmostEqual(out["confidence"], 0.6)
26
+
27
+ def test_accepts_list_and_dict(self):
28
+ a = mc.evaluate(confidence=0.8, guard_hits=["len", "tone"])
29
+ b = mc.evaluate(confidence=0.8, guard_hits={"len": 1, "tone": 1})
30
+ self.assertEqual(a["confidence"], b["confidence"])
31
+
32
+ class ConflictTest(unittest.TestCase):
33
+ def test_conflict_allows_correction(self):
34
+ out = mc.evaluate(confidence=0.7, conflict=True)
35
+ self.assertTrue(out["allow_self_correction"])
36
+
37
+ def test_low_confidence_no_correction(self):
38
+ out = mc.evaluate(confidence=0.2, conflict=True)
39
+ self.assertFalse(out["allow_self_correction"])
40
+
41
+ def test_conflict_dict_form(self):
42
+ out = mc.evaluate(confidence=0.7, conflict={"conflicts_with_stance": True})
43
+ self.assertTrue(out["allow_self_correction"])
44
+
45
+ class StreakTest(unittest.TestCase):
46
+ def test_three_lows_converge(self):
47
+ out = mc.evaluate(confidence=0.3, history={"low_flags": [True, True]})
48
+ self.assertTrue(out["converge"])
49
+ self.assertEqual(out["low_streak"], 3)
50
+
51
+ def test_two_lows_no_converge(self):
52
+ out = mc.evaluate(confidence=0.3, history=[True])
53
+ self.assertFalse(out["converge"])
54
+
55
+ def test_high_breaks_streak(self):
56
+ out = mc.evaluate(confidence=0.9, history=[True, True, False])
57
+ self.assertFalse(out["converge"])
58
+
59
+ def test_remember_trims(self):
60
+ history = {"low_flags": [True] * 8}
61
+ out = mc.remember(history, {"band": "low"})
62
+ self.assertEqual(len(out["low_flags"]), 8)
63
+ self.assertTrue(out["low_flags"][-1])
64
+
65
+ class DisabledTest(unittest.TestCase):
66
+ def test_off_is_neutral(self):
67
+ out = mc.evaluate(confidence=0.2, guard_hits=3, conflict=True,
68
+ flags={"enabled": False})
69
+ self.assertFalse(out["enabled"])
70
+ self.assertFalse(out["uncertainty"])
71
+ self.assertFalse(out["allow_self_correction"])
72
+ self.assertFalse(out["converge"])
73
+ self.assertEqual(out["confidence"], 0.2)
74
+
75
+ class MalformedTest(unittest.TestCase):
76
+ def test_garbage_inputs(self):
77
+ out = mc.evaluate(intent=None, confidence="x", guard_hits="?",
78
+ conflict=None, history="nope")
79
+ self.assertIn("reason", out)
80
+ self.assertGreaterEqual(out["confidence"], 0.0)
81
+ self.assertLessEqual(out["confidence"], 1.0)
82
+
83
+ def test_nan_confidence(self):
84
+ out = mc.evaluate(confidence=float("nan"))
85
+ self.assertEqual(out["band"], "low")
86
+
87
+ class DirectiveTest(unittest.TestCase):
88
+ def test_low_confidence_shortens(self):
89
+ out = mc.directives(mc.evaluate(confidence=0.2))
90
+ self.assertLess(out["length_scale"], 1.0)
91
+ self.assertIn("别承诺", out["tone"])
92
+
93
+ def test_converge_shortens_more(self):
94
+ result = mc.evaluate(confidence=0.2, history=[True, True])
95
+ out = mc.directives(result)
96
+ self.assertAlmostEqual(out["length_scale"], 0.7)
97
+
98
+ def test_high_confidence_keeps_length(self):
99
+ out = mc.directives(mc.evaluate(confidence=0.95))
100
+ self.assertEqual(out["length_scale"], 1.0)
101
+
102
+ if __name__ == "__main__":
103
+ unittest.main()