foxclaw 0.1.0 → 0.1.3

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 (514) hide show
  1. package/README.md +54 -29
  2. package/bin/foxclaw.cjs +51 -0
  3. package/index.js +4 -10
  4. package/package.json +46 -38
  5. package/python/.dockerignore +35 -0
  6. package/python/.env.example +173 -0
  7. package/python/.gitattributes +46 -0
  8. package/python/.github/workflows/ci.yml +167 -0
  9. package/python/.github/workflows/release.yml +183 -0
  10. package/python/CHANGELOG.md +57 -0
  11. package/python/CHANGELOG_CN.md +57 -0
  12. package/python/Dockerfile +64 -0
  13. package/python/LICENSE +21 -0
  14. package/python/MANIFEST.in +14 -0
  15. package/python/README.md +201 -0
  16. package/python/README_CN.md +336 -0
  17. package/python/bench/bm25_audit.py +186 -0
  18. package/python/bench/bm25_regression.py +177 -0
  19. package/python/bench/bm25_stress.py +155 -0
  20. package/python/bench/cd_audit.py +215 -0
  21. package/python/bench/chaos.py +140 -0
  22. package/python/bench/concurrency.py +103 -0
  23. package/python/bench/e2e_real_models.py +94 -0
  24. package/python/bench/llm_retry_test.py +241 -0
  25. package/python/bench/load_test.py +193 -0
  26. package/python/bench/vector_ivf_upgrade.py +128 -0
  27. package/python/docs/CHANNELS.md +195 -0
  28. package/python/docs/GOVERNANCE.md +164 -0
  29. package/python/docs/RELEASE.md +110 -0
  30. package/python/docs/RUN.md +75 -0
  31. package/python/docs/RUN_CN.md +121 -0
  32. package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
  33. package/python/evals/__init__.py +0 -0
  34. package/python/evals/governance/README.md +61 -0
  35. package/python/evals/governance/__init__.py +0 -0
  36. package/python/evals/governance/cases.py +115 -0
  37. package/python/evals/governance/compare_embeddings.py +94 -0
  38. package/python/evals/governance/convergence.svg +11 -0
  39. package/python/evals/governance/report.json +371 -0
  40. package/python/evals/governance/run_evals.py +368 -0
  41. package/python/examples/governance_middleware_demo.py +72 -0
  42. package/python/examples/langgraph_governance_demo.py +104 -0
  43. package/python/examples/sample_itinerary.csv +6 -0
  44. package/python/examples/sample_knowledge.csv +9 -0
  45. package/python/examples/sample_transactions.csv +14 -0
  46. package/python/foxclaw/__init__.py +408 -0
  47. package/python/foxclaw/agents/__init__.py +12 -0
  48. package/python/foxclaw/agents/agents.py +35 -0
  49. package/python/foxclaw/agents/base.py +794 -0
  50. package/python/foxclaw/agents/impl/__init__.py +1 -0
  51. package/python/foxclaw/agents/impl/_helpers.py +70 -0
  52. package/python/foxclaw/agents/impl/finance.py +231 -0
  53. package/python/foxclaw/agents/impl/office.py +350 -0
  54. package/python/foxclaw/agents/impl/supervisor.py +592 -0
  55. package/python/foxclaw/agents/mixins/_context.py +128 -0
  56. package/python/foxclaw/agents/mixins/augment.py +143 -0
  57. package/python/foxclaw/agents/mixins/llm.py +67 -0
  58. package/python/foxclaw/agents/mixins/memory.py +41 -0
  59. package/python/foxclaw/agents/mixins/preference.py +131 -0
  60. package/python/foxclaw/agents/mixins/prompt.py +71 -0
  61. package/python/foxclaw/agents/mixins/skillbox.py +58 -0
  62. package/python/foxclaw/agents/mixins/tool_exec.py +417 -0
  63. package/python/foxclaw/api/__init__.py +224 -0
  64. package/python/foxclaw/api/_state.py +43 -0
  65. package/python/foxclaw/api/bootstrap.py +96 -0
  66. package/python/foxclaw/api/middleware.py +294 -0
  67. package/python/foxclaw/api/routes/ahs.py +404 -0
  68. package/python/foxclaw/api/routes/attachments.py +116 -0
  69. package/python/foxclaw/api/routes/browser.py +35 -0
  70. package/python/foxclaw/api/routes/domain.py +306 -0
  71. package/python/foxclaw/api/routes/knowledge.py +145 -0
  72. package/python/foxclaw/api/routes/llm_admin.py +449 -0
  73. package/python/foxclaw/api/routes/memory.py +105 -0
  74. package/python/foxclaw/api/routes/orchestration.py +39 -0
  75. package/python/foxclaw/api/routes/recovery.py +44 -0
  76. package/python/foxclaw/api/routes/review.py +266 -0
  77. package/python/foxclaw/api/routes/search.py +33 -0
  78. package/python/foxclaw/api/routes/system.py +311 -0
  79. package/python/foxclaw/api/routes/task.py +263 -0
  80. package/python/foxclaw/api/routes/ws.py +64 -0
  81. package/python/foxclaw/api/spine.py +52 -0
  82. package/python/foxclaw/browser/__init__.py +13 -0
  83. package/python/foxclaw/browser/action/__init__.py +1 -0
  84. package/python/foxclaw/browser/action/action_executor.py +50 -0
  85. package/python/foxclaw/browser/action/action_types.py +44 -0
  86. package/python/foxclaw/browser/action/action_verifier.py +68 -0
  87. package/python/foxclaw/browser/action/url_policy.py +143 -0
  88. package/python/foxclaw/browser/adapter/__init__.py +1 -0
  89. package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
  90. package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
  91. package/python/foxclaw/browser/driver/__init__.py +1 -0
  92. package/python/foxclaw/browser/driver/base_driver.py +61 -0
  93. package/python/foxclaw/browser/driver/playwright_driver.py +170 -0
  94. package/python/foxclaw/browser/perception/__init__.py +1 -0
  95. package/python/foxclaw/browser/perception/dom_extractor.py +42 -0
  96. package/python/foxclaw/browser/perception/element_indexer.py +56 -0
  97. package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
  98. package/python/foxclaw/browser/perception/snapshot.py +29 -0
  99. package/python/foxclaw/browser/service.py +144 -0
  100. package/python/foxclaw/browser/session/__init__.py +1 -0
  101. package/python/foxclaw/browser/session/session_manager.py +57 -0
  102. package/python/foxclaw/capability/__init__.py +12 -0
  103. package/python/foxclaw/capability/embedding_service.py +65 -0
  104. package/python/foxclaw/channels/__init__.py +138 -0
  105. package/python/foxclaw/channels/discord.py +175 -0
  106. package/python/foxclaw/channels/dispatcher.py +179 -0
  107. package/python/foxclaw/channels/feishu.py +215 -0
  108. package/python/foxclaw/channels/openclaw_bridge.py +151 -0
  109. package/python/foxclaw/channels/signing.py +187 -0
  110. package/python/foxclaw/channels/telegram.py +120 -0
  111. package/python/foxclaw/channels/weixin.py +215 -0
  112. package/python/foxclaw/cli.py +183 -0
  113. package/python/foxclaw/cognitive/__init__.py +4 -0
  114. package/python/foxclaw/cognitive/reasoner.py +81 -0
  115. package/python/foxclaw/cognitive/state.py +141 -0
  116. package/python/foxclaw/core/__init__.py +9 -0
  117. package/python/foxclaw/core/audit_chain.py +131 -0
  118. package/python/foxclaw/core/audit_sink.py +100 -0
  119. package/python/foxclaw/core/compaction.py +265 -0
  120. package/python/foxclaw/core/config.py +65 -0
  121. package/python/foxclaw/core/confirm_nonce.py +151 -0
  122. package/python/foxclaw/core/dispatch.py +27 -0
  123. package/python/foxclaw/core/envfile.py +48 -0
  124. package/python/foxclaw/core/failure_policy.py +142 -0
  125. package/python/foxclaw/core/gateway.py +81 -0
  126. package/python/foxclaw/core/governance_audit.py +171 -0
  127. package/python/foxclaw/core/idempotency.py +282 -0
  128. package/python/foxclaw/core/injection_guard.py +202 -0
  129. package/python/foxclaw/core/memory_adapter.py +489 -0
  130. package/python/foxclaw/core/metrics.py +88 -0
  131. package/python/foxclaw/core/model_router.py +985 -0
  132. package/python/foxclaw/core/paths.py +103 -0
  133. package/python/foxclaw/core/recovery.py +349 -0
  134. package/python/foxclaw/core/scheduler.py +141 -0
  135. package/python/foxclaw/core/secrets.py +131 -0
  136. package/python/foxclaw/core/sessions.py +155 -0
  137. package/python/foxclaw/core/settings.py +414 -0
  138. package/python/foxclaw/core/shared_write.py +186 -0
  139. package/python/foxclaw/core/steering_queue.py +154 -0
  140. package/python/foxclaw/core/system_bus.py +84 -0
  141. package/python/foxclaw/core/thought_chain.py +316 -0
  142. package/python/foxclaw/core/token_meter.py +231 -0
  143. package/python/foxclaw/core/tools.py +164 -0
  144. package/python/foxclaw/core/tracing.py +375 -0
  145. package/python/foxclaw/core/truth_pipeline.py +110 -0
  146. package/python/foxclaw/core/update_check.py +149 -0
  147. package/python/foxclaw/core/vec_recall_adapter.py +477 -0
  148. package/python/foxclaw/core/verify.py +173 -0
  149. package/python/foxclaw/core/web_search.py +218 -0
  150. package/python/foxclaw/finance/__init__.py +15 -0
  151. package/python/foxclaw/finance/evidence.py +149 -0
  152. package/python/foxclaw/finance/fx.py +98 -0
  153. package/python/foxclaw/finance/graph.py +179 -0
  154. package/python/foxclaw/finance/models.py +146 -0
  155. package/python/foxclaw/finance/nlparse.py +515 -0
  156. package/python/foxclaw/finance/policy.py +301 -0
  157. package/python/foxclaw/finance/reasoner.py +121 -0
  158. package/python/foxclaw/finance/reasoning.py +319 -0
  159. package/python/foxclaw/finance/reconcile.py +175 -0
  160. package/python/foxclaw/finance/runtime.py +500 -0
  161. package/python/foxclaw/governance/__init__.py +24 -0
  162. package/python/foxclaw/governance/middleware.py +176 -0
  163. package/python/foxclaw/harness/__init__.py +17 -0
  164. package/python/foxclaw/harness/_base.py +66 -0
  165. package/python/foxclaw/harness/_policy.py +340 -0
  166. package/python/foxclaw/harness/drift.py +77 -0
  167. package/python/foxclaw/harness/event_bus.py +28 -0
  168. package/python/foxclaw/harness/harness.py +358 -0
  169. package/python/foxclaw/harness/loop_detection.py +145 -0
  170. package/python/foxclaw/itinerary/__init__.py +10 -0
  171. package/python/foxclaw/itinerary/amap_client.py +114 -0
  172. package/python/foxclaw/itinerary/models.py +80 -0
  173. package/python/foxclaw/itinerary/reasoner.py +96 -0
  174. package/python/foxclaw/itinerary/reasoning.py +168 -0
  175. package/python/foxclaw/itinerary/runtime.py +144 -0
  176. package/python/foxclaw/kernel/__init__.py +13 -0
  177. package/python/foxclaw/kernel/capabilities.py +72 -0
  178. package/python/foxclaw/kernel/policy_gate.py +108 -0
  179. package/python/foxclaw/kernel/process_manager.py +186 -0
  180. package/python/foxclaw/kernel/scheduler.py +125 -0
  181. package/python/foxclaw/kernel/sentinel.py +443 -0
  182. package/python/foxclaw/kernel/sentinel_client.py +243 -0
  183. package/python/foxclaw/kernel/spine.py +97 -0
  184. package/python/foxclaw/knowledge/__init__.py +360 -0
  185. package/python/foxclaw/knowledge/truth_crystallizer.py +89 -0
  186. package/python/foxclaw/learning/__init__.py +12 -0
  187. package/python/foxclaw/learning/knowledge_graph.py +144 -0
  188. package/python/foxclaw/learning/models.py +93 -0
  189. package/python/foxclaw/learning/reasoner.py +67 -0
  190. package/python/foxclaw/learning/runtime.py +351 -0
  191. package/python/foxclaw/learning/spaced_repetition.py +91 -0
  192. package/python/foxclaw/mcp_server.py +188 -0
  193. package/python/foxclaw/orchestration/__init__.py +167 -0
  194. package/python/foxclaw/pipeline/__init__.py +23 -0
  195. package/python/foxclaw/pipeline/cognitive_pipeline.py +429 -0
  196. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  197. package/python/foxclaw/pipeline/supervisor_stages.py +201 -0
  198. package/python/foxclaw/planning/__init__.py +17 -0
  199. package/python/foxclaw/planning/llm_planner.py +232 -0
  200. package/python/foxclaw/planning/planner.py +94 -0
  201. package/python/foxclaw/search/__init__.py +24 -0
  202. package/python/foxclaw/search/agent_port.py +190 -0
  203. package/python/foxclaw/search/evidence.py +109 -0
  204. package/python/foxclaw/search/external_search.py +50 -0
  205. package/python/foxclaw/search/hkc_read_port.py +60 -0
  206. package/python/foxclaw/search/internal_search.py +71 -0
  207. package/python/foxclaw/search/models.py +127 -0
  208. package/python/foxclaw/search/planner.py +44 -0
  209. package/python/foxclaw/search/retriever.py +39 -0
  210. package/python/foxclaw/search/runtime.py +127 -0
  211. package/python/foxclaw/search/strategy.py +44 -0
  212. package/python/foxclaw/skills/__init__.py +559 -0
  213. package/python/foxclaw/skills/archive.py +111 -0
  214. package/python/foxclaw/skills/fetch.py +226 -0
  215. package/python/foxclaw/skills/hooks_spec.py +69 -0
  216. package/python/foxclaw/skills/install_cmd.py +216 -0
  217. package/python/foxclaw/skills/lifecycle.py +252 -0
  218. package/python/foxclaw/skills/markdown_skill.py +175 -0
  219. package/python/foxclaw/skills/markdown_store.py +39 -0
  220. package/python/foxclaw/skills/mcp_client.py +184 -0
  221. package/python/foxclaw/skills/mcp_store.py +40 -0
  222. package/python/foxclaw/skills/permission.py +145 -0
  223. package/python/foxclaw/skills/pipeline.py +354 -0
  224. package/python/foxclaw/skills/policy.py +74 -0
  225. package/python/foxclaw/skills/review.py +72 -0
  226. package/python/foxclaw/skills/store.py +57 -0
  227. package/python/frontend/assets/core_bg.jpg +0 -0
  228. package/python/frontend/deck/auth.js +25 -0
  229. package/python/frontend/deck/chat_model_picker.js +483 -0
  230. package/python/frontend/deck/domain.js +276 -0
  231. package/python/frontend/deck/main.js +1806 -0
  232. package/python/frontend/deck/markdown.js +44 -0
  233. package/python/frontend/deck/settings.js +307 -0
  234. package/python/frontend/deck/state.js +5 -0
  235. package/python/frontend/deck/util.js +4 -0
  236. package/python/frontend/deck/visuals.js +190 -0
  237. package/python/frontend/deck.css +855 -0
  238. package/python/frontend/deck.html +686 -0
  239. package/python/frontend/deck_v2.html +341 -0
  240. package/python/frontend/planet/index.html +266 -0
  241. package/python/frontend/planet/planet.css +579 -0
  242. package/python/frontend/planet/planet.js +1436 -0
  243. package/python/frontend/planet/vendor/three.core.min.js +6 -0
  244. package/python/frontend/planet/vendor/three.module.min.js +6 -0
  245. package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
  246. package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
  247. package/python/frontend/v3/index.html +15 -0
  248. package/python/frontend/widget_runtime.js +252 -0
  249. package/python/frontend/widgets.js +639 -0
  250. package/python/frontend_src/index.html +14 -0
  251. package/python/frontend_src/package-lock.json +1376 -0
  252. package/python/frontend_src/package.json +19 -0
  253. package/python/frontend_src/src/App.vue +58 -0
  254. package/python/frontend_src/src/api.js +82 -0
  255. package/python/frontend_src/src/components/ChatDock.vue +365 -0
  256. package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
  257. package/python/frontend_src/src/components/DocViewer.vue +162 -0
  258. package/python/frontend_src/src/components/Inspector.vue +235 -0
  259. package/python/frontend_src/src/components/LeftRail.vue +415 -0
  260. package/python/frontend_src/src/components/MenuPopover.vue +128 -0
  261. package/python/frontend_src/src/components/MessageStream.vue +389 -0
  262. package/python/frontend_src/src/components/ModelPicker.vue +350 -0
  263. package/python/frontend_src/src/components/QuickIngest.vue +112 -0
  264. package/python/frontend_src/src/components/Stage.vue +272 -0
  265. package/python/frontend_src/src/components/ToolMenu.vue +141 -0
  266. package/python/frontend_src/src/components/TopBar.vue +118 -0
  267. package/python/frontend_src/src/components/panels/ActivityPanel.vue +82 -0
  268. package/python/frontend_src/src/components/panels/AgentsPanel.vue +83 -0
  269. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +425 -0
  270. package/python/frontend_src/src/components/panels/MemoryPanel.vue +117 -0
  271. package/python/frontend_src/src/components/panels/PanelBox.vue +50 -0
  272. package/python/frontend_src/src/components/panels/ReviewPanel.vue +292 -0
  273. package/python/frontend_src/src/components/panels/SettingsPanel.vue +358 -0
  274. package/python/frontend_src/src/components/panels/TasksPanel.vue +73 -0
  275. package/python/frontend_src/src/components/panels/ToolsPanel.vue +746 -0
  276. package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
  277. package/python/frontend_src/src/lib/fmt.js +44 -0
  278. package/python/frontend_src/src/lib/i18n.js +53 -0
  279. package/python/frontend_src/src/lib/locales/en.js +173 -0
  280. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  281. package/python/frontend_src/src/lib/md.js +190 -0
  282. package/python/frontend_src/src/main.js +9 -0
  283. package/python/frontend_src/src/stores/app.js +91 -0
  284. package/python/frontend_src/src/stores/chat.js +450 -0
  285. package/python/frontend_src/src/stores/model.js +96 -0
  286. package/python/frontend_src/src/stores/panels.js +289 -0
  287. package/python/frontend_src/src/stores/profile.js +31 -0
  288. package/python/frontend_src/src/stores/system.js +25 -0
  289. package/python/frontend_src/src/styles/base.css +138 -0
  290. package/python/frontend_src/src/styles/panel.css +117 -0
  291. package/python/frontend_src/src/styles/tokens.css +78 -0
  292. package/python/frontend_src/vite.config.js +39 -0
  293. package/python/pyproject.toml +268 -0
  294. package/python/requirements.txt +311 -0
  295. package/python/skills/README.md +123 -0
  296. package/python/skills/execution-contract/SKILL.md +30 -0
  297. package/python/skills/general-assistant/SKILL.md +34 -0
  298. package/python/skills/planner/SKILL.md +33 -0
  299. package/python/skills/schedule-itinerary/SKILL.md +35 -0
  300. package/python/skills/skill-composition/SKILL.md +31 -0
  301. package/python/skills/skill-guard/SKILL.md +67 -0
  302. package/python/skills/skill-memory/SKILL.md +50 -0
  303. package/python/skills/skill-router/SKILL.md +35 -0
  304. package/python/tests/conftest.py +54 -0
  305. package/python/tests/run_all.py +200 -0
  306. package/python/tests/test_agent_lifecycle.py +326 -0
  307. package/python/tests/test_ahs_p2.py +393 -0
  308. package/python/tests/test_anthropic_tools.py +192 -0
  309. package/python/tests/test_api.py +97 -0
  310. package/python/tests/test_api_auth.py +79 -0
  311. package/python/tests/test_api_host_guard.py +69 -0
  312. package/python/tests/test_attachments.py +151 -0
  313. package/python/tests/test_audit_chain.py +116 -0
  314. package/python/tests/test_audit_sink.py +130 -0
  315. package/python/tests/test_browser.py +149 -0
  316. package/python/tests/test_browser_request_guard.py +180 -0
  317. package/python/tests/test_capabilities.py +152 -0
  318. package/python/tests/test_channel_signing.py +185 -0
  319. package/python/tests/test_channel_trust.py +109 -0
  320. package/python/tests/test_channels.py +174 -0
  321. package/python/tests/test_cli.py +46 -0
  322. package/python/tests/test_compaction_toolpair.py +119 -0
  323. package/python/tests/test_config_layering.py +61 -0
  324. package/python/tests/test_confirm_nonce.py +186 -0
  325. package/python/tests/test_context_compaction_wiring.py +124 -0
  326. package/python/tests/test_dispatch.py +89 -0
  327. package/python/tests/test_domain_evals.py +334 -0
  328. package/python/tests/test_domain_import.py +72 -0
  329. package/python/tests/test_domain_reasoners.py +134 -0
  330. package/python/tests/test_e2e.py +161 -0
  331. package/python/tests/test_emb_cache.py +97 -0
  332. package/python/tests/test_embedding_singleton.py +87 -0
  333. package/python/tests/test_envfile.py +83 -0
  334. package/python/tests/test_evals_smoke.py +90 -0
  335. package/python/tests/test_event_spine.py +174 -0
  336. package/python/tests/test_evidence_semantic.py +88 -0
  337. package/python/tests/test_evidence_sse.py +94 -0
  338. package/python/tests/test_file_ingest.py +135 -0
  339. package/python/tests/test_finance.py +144 -0
  340. package/python/tests/test_finance_ledger.py +245 -0
  341. package/python/tests/test_finance_persist.py +174 -0
  342. package/python/tests/test_gateway.py +125 -0
  343. package/python/tests/test_governance.py +421 -0
  344. package/python/tests/test_governance_audit.py +324 -0
  345. package/python/tests/test_governance_middleware.py +137 -0
  346. package/python/tests/test_hkc.py +268 -0
  347. package/python/tests/test_hkc_index_build_manager.py +306 -0
  348. package/python/tests/test_hkc_manifest_reuse.py +294 -0
  349. package/python/tests/test_hkc_search_event_sync.py +482 -0
  350. package/python/tests/test_import_path_guard.py +155 -0
  351. package/python/tests/test_injection_guard.py +193 -0
  352. package/python/tests/test_integration.py +117 -0
  353. package/python/tests/test_itinerary.py +100 -0
  354. package/python/tests/test_kee_semantic.py +84 -0
  355. package/python/tests/test_kernel.py +212 -0
  356. package/python/tests/test_learning.py +101 -0
  357. package/python/tests/test_llm_admin.py +145 -0
  358. package/python/tests/test_llm_planner.py +344 -0
  359. package/python/tests/test_markdown_skills.py +62 -0
  360. package/python/tests/test_markdown_skills_openclaw.py +66 -0
  361. package/python/tests/test_mcp_connectors.py +97 -0
  362. package/python/tests/test_mcp_guardian_discovery.py +87 -0
  363. package/python/tests/test_mcp_no_memory.py +43 -0
  364. package/python/tests/test_mcp_policy_gate.py +76 -0
  365. package/python/tests/test_mcp_server.py +92 -0
  366. package/python/tests/test_memory_recall.py +92 -0
  367. package/python/tests/test_metrics.py +58 -0
  368. package/python/tests/test_multi_chain.py +90 -0
  369. package/python/tests/test_multimodal.py +122 -0
  370. package/python/tests/test_negotiation.py +136 -0
  371. package/python/tests/test_orchestration.py +109 -0
  372. package/python/tests/test_orchestration_decoupled.py +103 -0
  373. package/python/tests/test_p3_p5_hardening.py +324 -0
  374. package/python/tests/test_parallel_dispatch.py +207 -0
  375. package/python/tests/test_pipeline.py +492 -0
  376. package/python/tests/test_plan_visible.py +86 -0
  377. package/python/tests/test_planner.py +88 -0
  378. package/python/tests/test_policy_gate.py +108 -0
  379. package/python/tests/test_profile.py +67 -0
  380. package/python/tests/test_readme_claims.py +163 -0
  381. package/python/tests/test_reasoner.py +170 -0
  382. package/python/tests/test_release_check.py +252 -0
  383. package/python/tests/test_resume_checkpoint.py +148 -0
  384. package/python/tests/test_routing.py +85 -0
  385. package/python/tests/test_scheduler.py +138 -0
  386. package/python/tests/test_search.py +194 -0
  387. package/python/tests/test_secrets.py +153 -0
  388. package/python/tests/test_semantic.py +69 -0
  389. package/python/tests/test_sessions.py +90 -0
  390. package/python/tests/test_settings.py +265 -0
  391. package/python/tests/test_shadow_learning.py +290 -0
  392. package/python/tests/test_shared_write.py +200 -0
  393. package/python/tests/test_skill_taint_gate.py +145 -0
  394. package/python/tests/test_skills.py +117 -0
  395. package/python/tests/test_skills_fetch.py +282 -0
  396. package/python/tests/test_stats_and_events.py +162 -0
  397. package/python/tests/test_streaming.py +55 -0
  398. package/python/tests/test_supervisor_pipeline.py +176 -0
  399. package/python/tests/test_supervisor_triage.py +102 -0
  400. package/python/tests/test_taint.py +112 -0
  401. package/python/tests/test_task_stream.py +74 -0
  402. package/python/tests/test_thinking_sse.py +66 -0
  403. package/python/tests/test_thinking_stream.py +76 -0
  404. package/python/tests/test_tool_center.py +113 -0
  405. package/python/tests/test_tool_parallel.py +113 -0
  406. package/python/tests/test_tool_params.py +99 -0
  407. package/python/tests/test_tool_use.py +229 -0
  408. package/python/tests/test_toplevel_concurrency.py +116 -0
  409. package/python/tests/test_tracing.py +256 -0
  410. package/python/tests/test_truth_crystallizer.py +125 -0
  411. package/python/tests/test_truth_flow.py +88 -0
  412. package/python/tests/test_update_check.py +95 -0
  413. package/python/tests/test_url_policy.py +53 -0
  414. package/python/tests/test_verify_evidence.py +212 -0
  415. package/python/tests/test_version_sync.py +100 -0
  416. package/python/tests/test_web_trigger.py +82 -0
  417. package/python/tools/check_faiss_type.py +30 -0
  418. package/python/tools/check_log.ps1 +38 -0
  419. package/python/tools/ivf_status.py +72 -0
  420. package/python/tools/mypy_baseline.txt +13 -0
  421. package/python/tools/mypy_ratchet.py +143 -0
  422. package/python/tools/pack.py +173 -0
  423. package/python/tools/release_check.py +349 -0
  424. package/python/tools/restart_foxclaw.ps1 +71 -0
  425. package/python/tools/shared_write_audit.py +190 -0
  426. package/python/tools/size_baseline.txt +28 -0
  427. package/python/tools/size_check.py +102 -0
  428. package/python/tools/strip_comments.py +228 -0
  429. package/python/tools/verify_env.bat +2 -0
  430. package/python/tools/verify_env.py +25 -0
  431. package/python/vendor/HMR_LICENSE +37 -0
  432. package/python/vendor/HMR_README.md +65 -0
  433. package/python/vendor/hkc/LICENSE +37 -0
  434. package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
  435. package/python/vendor/hkc/hkc_ace/ace.py +457 -0
  436. package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
  437. package/python/vendor/hkc/hkc_api/__init__.py +0 -0
  438. package/python/vendor/hkc/hkc_api/container.py +416 -0
  439. package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
  440. package/python/vendor/hkc/hkc_api/main.py +157 -0
  441. package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
  442. package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
  443. package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
  444. package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
  445. package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
  446. package/python/vendor/hkc/hkc_api/schemas.py +153 -0
  447. package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
  448. package/python/vendor/hkc/hkc_core/__init__.py +0 -0
  449. package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
  450. package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
  451. package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
  452. package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
  453. package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
  454. package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
  455. package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
  456. package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
  457. package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
  458. package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
  459. package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
  460. package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
  461. package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
  462. package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
  463. package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
  464. package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
  465. package/python/vendor/hkc/hkc_kde/kde.py +298 -0
  466. package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
  467. package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
  468. package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
  469. package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
  470. package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
  471. package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
  472. package/python/vendor/hkc/hkc_kde/models.py +74 -0
  473. package/python/vendor/hkc/hkc_kde/packager.py +329 -0
  474. package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
  475. package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
  476. package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
  477. package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
  478. package/python/vendor/hkc/hkc_kee/kee.py +541 -0
  479. package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
  480. package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
  481. package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
  482. package/python/vendor/hkc/hkc_sdk/base.py +151 -0
  483. package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
  484. package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
  485. package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
  486. package/python/vendor/hkc/hkc_sdk/models.py +171 -0
  487. package/python/vendor/hkc/hkc_search/__init__.py +0 -0
  488. package/python/vendor/hkc/hkc_search/bm25.py +325 -0
  489. package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
  490. package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
  491. package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
  492. package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
  493. package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
  494. package/python/vendor/hmr/__init__.py +0 -0
  495. package/python/vendor/hmr/core/__init__.py +0 -0
  496. package/python/vendor/hmr/core/hmr.py +684 -0
  497. package/python/vendor/hmr/core/models.py +310 -0
  498. package/python/vendor/hmr/engines/__init__.py +0 -0
  499. package/python/vendor/hmr/engines/evolution.py +710 -0
  500. package/python/vendor/hmr/engines/jit_compiler.py +472 -0
  501. package/python/vendor/hmr/engines/lifecycle.py +264 -0
  502. package/python/vendor/hmr/engines/policy.py +625 -0
  503. package/python/vendor/hmr/engines/recall.py +260 -0
  504. package/python/vendor/hmr/engines/runtime_state.py +158 -0
  505. package/python/vendor/hmr/engines/scheduler.py +323 -0
  506. package/python/vendor/hmr/engines/semantic.py +150 -0
  507. package/python/vendor/hmr/engines/temporal.py +261 -0
  508. package/python/vendor/hmr/engines/thought_chain.py +635 -0
  509. package/python/vendor/hmr/graph/__init__.py +0 -0
  510. package/python/vendor/hmr/graph/cwg.py +338 -0
  511. package/python/vendor/hmr/graph/memory_graph.py +530 -0
  512. package/python/vendor/hmr/storage/__init__.py +0 -0
  513. package/python/vendor/hmr/storage/memory_fs.py +300 -0
  514. package/python/vendor/hmr/storage/vector_store.py +520 -0
@@ -0,0 +1,789 @@
1
+ """
2
+ hkc-core / graph / sqlite_store.py
3
+ SQLite + NetworkX 实现的 GraphStore。
4
+
5
+ SQLite → 持久化存储,全量数据
6
+ NetworkX → 内存图,启动时从 SQLite 重建,用于路径查询
7
+ """
8
+ import json
9
+ import sqlite3
10
+ import threading
11
+ from datetime import datetime, timezone
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ import networkx as nx
16
+
17
+ from .base import GraphStore
18
+ from ..models.enums import KUType, ClaimStatus
19
+ from ..models.ku import (
20
+ KU, Relation, ConflictCard,
21
+ EntityKU, ConceptKU, FactKU, ClaimKU,
22
+ EvidenceKU, AbilityKU,
23
+ )
24
+
25
+
26
+ # ── 序列化 / 反序列化 ────────────────────────────────────────
27
+
28
+ def _loads_dict(raw) -> dict:
29
+ """extra 列容错解析:坏 JSON / 非 dict 一律当空字典,不抛异常打断写入。"""
30
+ try:
31
+ val = json.loads(raw) if raw else {}
32
+ except Exception:
33
+ return {}
34
+ return val if isinstance(val, dict) else {}
35
+
36
+
37
+ def _dump(value):
38
+ """JSON 列(list/dict)写库前序列化,标量原样返回。"""
39
+ return json.dumps(value, ensure_ascii=False) if isinstance(value, (list, dict)) else value
40
+
41
+
42
+ def _ku_to_row(ku: KU) -> tuple:
43
+ d = ku.to_dict()
44
+ # Column order: ku_id, name, ku_type, summary, confidence, status,
45
+ # domain, version, tags, sources, relations, extra, created_at, updated_at
46
+ return (
47
+ d["ku_id"],
48
+ d["name"],
49
+ d["ku_type"],
50
+ d["summary"],
51
+ d["confidence"],
52
+ d["status"],
53
+ d["domain"],
54
+ d["version"],
55
+ json.dumps(d["tags"], ensure_ascii=False),
56
+ json.dumps(d["sources"], ensure_ascii=False),
57
+ json.dumps(d["relations"], ensure_ascii=False),
58
+ json.dumps(d["extra"], ensure_ascii=False),
59
+ d["created_at"],
60
+ d["updated_at"],
61
+ )
62
+
63
+
64
+ def _row_to_ku(row: tuple) -> KU:
65
+ # Schema CREATE TABLE order:
66
+ # ku_id, ku_type, name, summary, confidence, status,
67
+ # domain, version, tags, sources, relations, extra, created_at, updated_at
68
+ (ku_id, ku_type, name, summary, confidence, status,
69
+ domain, version, tags, sources, relations, extra,
70
+ created_at, updated_at) = row
71
+
72
+ tags = json.loads(tags)
73
+ sources = json.loads(sources)
74
+ relations = json.loads(relations)
75
+ extra = json.loads(extra)
76
+
77
+ base = dict(
78
+ ku_id=ku_id, name=name, summary=summary,
79
+ confidence=confidence, status=status, domain=domain,
80
+ version=version, tags=tags, sources=sources,
81
+ relations=relations, extra=extra,
82
+ created_at=created_at, updated_at=updated_at,
83
+ )
84
+
85
+ t = ku_type
86
+ if t == KUType.ENTITY.value:
87
+ from ..models.enums import EntityType
88
+ ku = EntityKU(**base)
89
+ raw_etype = extra.get("entity_type", "Person")
90
+ try:
91
+ ku.entity_type = EntityType(raw_etype)
92
+ except ValueError:
93
+ ku.entity_type = EntityType.PERSON
94
+ ku.aliases = extra.get("aliases", [])
95
+ ku.birth = extra.get("birth", "")
96
+ ku.active = extra.get("active", True)
97
+ ku.source_text = extra.get("source_text", "")
98
+ elif t == KUType.CONCEPT.value:
99
+ ku = ConceptKU(**base)
100
+ ku.aka = extra.get("aka", [])
101
+ ku.definition = extra.get("definition", "")
102
+ ku.source_text = extra.get("source_text", "")
103
+ elif t == KUType.FACT.value:
104
+ ku = FactKU(**base)
105
+ ku.statement = extra.get("statement", "")
106
+ ku.verifiable = extra.get("verifiable", True)
107
+ ku.source_ref = extra.get("source_ref", "")
108
+ ku.source_text = extra.get("source_text", "")
109
+ elif t == KUType.CLAIM.value:
110
+ ku = ClaimKU(**base)
111
+ ku.statement = extra.get("statement", "")
112
+ ku.claim_status = ClaimStatus(extra.get("claim_status", "pending"))
113
+ ku.supports = extra.get("supports", [])
114
+ ku.contradicts = extra.get("contradicts", [])
115
+ ku.conflict_refs = extra.get("conflict_refs", [])
116
+ ku.source_text = extra.get("source_text", "")
117
+ elif t == KUType.EVIDENCE.value:
118
+ from ..models.enums import SourceType
119
+ ku = EvidenceKU(**base)
120
+ ku.source = extra.get("source", "")
121
+ try:
122
+ ku.source_type = SourceType(extra.get("source_type", "Book"))
123
+ except ValueError:
124
+ ku.source_type = SourceType.BOOK
125
+ ku.author = extra.get("author", "")
126
+ ku.year = extra.get("year", 0)
127
+ ku.page = extra.get("page", 0)
128
+ ku.quote = extra.get("quote", "")
129
+ ku.supports = extra.get("supports", [])
130
+ ku.contradicts = extra.get("contradicts", [])
131
+ elif t == KUType.ABILITY.value:
132
+ ku = AbilityKU(**base)
133
+ ku.ability_key = extra.get("ability_key", "")
134
+ ku.skills = extra.get("skills", [])
135
+ ku.workflows = extra.get("workflows", [])
136
+ ku.coverage = extra.get("coverage", {})
137
+ ku.knowledge_refs = extra.get("knowledge_refs", [])
138
+ ku.package_path = extra.get("package_path", "")
139
+ ku.pkg_version = extra.get("pkg_version", "1.0.0")
140
+ else:
141
+ raise ValueError(f"Unknown ku_type: {t}")
142
+
143
+ return ku
144
+
145
+
146
+ def _rel_to_row(rel: Relation) -> tuple:
147
+ return (
148
+ rel.rel_id, rel.from_ku, rel.to_ku,
149
+ rel.rel_type, rel.weight, rel.source, rel.created_at,
150
+ )
151
+
152
+
153
+ def _row_to_rel(row: tuple) -> Relation:
154
+ rel_id, from_ku, to_ku, rel_type, weight, source, created_at = row
155
+ return Relation(
156
+ rel_id=rel_id, from_ku=from_ku, to_ku=to_ku,
157
+ rel_type=rel_type, weight=weight,
158
+ source=source or "", created_at=created_at,
159
+ )
160
+
161
+
162
+ def _card_to_row(card: ConflictCard) -> tuple:
163
+ return (
164
+ card.conflict_id, card.claim_a_id, card.claim_b_id,
165
+ card.domain, card.status.value,
166
+ card.resolution_strategy.value,
167
+ card.resolution_note, card.resolved_by,
168
+ card.resolved_at, card.created_at,
169
+ )
170
+
171
+
172
+ def _row_to_card(row: tuple) -> ConflictCard:
173
+ from ..models.enums import ConflictStatus, ResolutionStrategy
174
+ (conflict_id, claim_a_id, claim_b_id, domain, status,
175
+ strategy, note, resolved_by, resolved_at, created_at) = row
176
+ return ConflictCard(
177
+ conflict_id=conflict_id,
178
+ claim_a_id=claim_a_id,
179
+ claim_b_id=claim_b_id,
180
+ domain=domain or "",
181
+ status=ConflictStatus(status),
182
+ resolution_strategy=ResolutionStrategy(strategy),
183
+ resolution_note=note or "",
184
+ resolved_by=resolved_by,
185
+ resolved_at=resolved_at,
186
+ created_at=created_at,
187
+ )
188
+
189
+
190
+ # ── SQLiteGraphStore ─────────────────────────────────────────
191
+
192
+ class SQLiteGraphStore(GraphStore):
193
+ """
194
+ v1 GraphStore 实现。
195
+ - SQLite 负责持久化
196
+ - NetworkX DiGraph 负责内存图查询(路径、邻居)
197
+ - 所有写操作同时更新两者,保持一致
198
+ """
199
+
200
+ def __init__(self, db_path: str):
201
+ self._lock = threading.RLock()
202
+ db_file = Path(db_path)
203
+ db_file.parent.mkdir(parents=True, exist_ok=True)
204
+ self._db = sqlite3.connect(str(db_file), check_same_thread=False)
205
+ self._db.row_factory = sqlite3.Row
206
+ self._graph: nx.DiGraph = nx.DiGraph()
207
+ self._init_schema()
208
+ self._load_graph()
209
+
210
+ # ── Schema ────────────────────────────────────────────────
211
+
212
+ def _init_schema(self):
213
+ # Set pragmas explicitly (executescript auto-commits, pragma must be separate)
214
+ self._db.execute("PRAGMA journal_mode=WAL")
215
+ self._db.commit()
216
+ self._db.executescript("""
217
+ PRAGMA foreign_keys=ON;
218
+
219
+ CREATE TABLE IF NOT EXISTS knowledge_units (
220
+ ku_id TEXT PRIMARY KEY,
221
+ ku_type TEXT NOT NULL,
222
+ name TEXT NOT NULL,
223
+ summary TEXT DEFAULT '',
224
+ confidence REAL DEFAULT 1.0,
225
+ status TEXT DEFAULT 'active',
226
+ domain TEXT DEFAULT '',
227
+ version INTEGER DEFAULT 1,
228
+ tags TEXT DEFAULT '[]',
229
+ sources TEXT DEFAULT '[]',
230
+ relations TEXT DEFAULT '[]',
231
+ extra TEXT DEFAULT '{}',
232
+ created_at TEXT,
233
+ updated_at TEXT
234
+ );
235
+
236
+ CREATE INDEX IF NOT EXISTS idx_ku_type ON knowledge_units(ku_type);
237
+ CREATE INDEX IF NOT EXISTS idx_ku_domain ON knowledge_units(domain);
238
+ CREATE INDEX IF NOT EXISTS idx_ku_status ON knowledge_units(status);
239
+
240
+ CREATE VIRTUAL TABLE IF NOT EXISTS ku_fts
241
+ USING fts5(ku_id UNINDEXED, name, summary, domain);
242
+
243
+ CREATE TABLE IF NOT EXISTS relations (
244
+ rel_id TEXT PRIMARY KEY,
245
+ from_ku TEXT NOT NULL,
246
+ to_ku TEXT NOT NULL,
247
+ rel_type TEXT NOT NULL,
248
+ weight REAL DEFAULT 1.0,
249
+ source TEXT DEFAULT '',
250
+ created_at TEXT,
251
+ FOREIGN KEY(from_ku) REFERENCES knowledge_units(ku_id),
252
+ FOREIGN KEY(to_ku) REFERENCES knowledge_units(ku_id)
253
+ );
254
+
255
+ CREATE INDEX IF NOT EXISTS idx_rel_from ON relations(from_ku);
256
+ CREATE INDEX IF NOT EXISTS idx_rel_to ON relations(to_ku);
257
+ CREATE INDEX IF NOT EXISTS idx_rel_type ON relations(rel_type);
258
+
259
+ CREATE TABLE IF NOT EXISTS conflict_cards (
260
+ conflict_id TEXT PRIMARY KEY,
261
+ claim_a_id TEXT NOT NULL,
262
+ claim_b_id TEXT NOT NULL,
263
+ domain TEXT DEFAULT '',
264
+ status TEXT DEFAULT 'open',
265
+ strategy TEXT DEFAULT 'evidence_weight',
266
+ note TEXT DEFAULT '',
267
+ resolved_by TEXT,
268
+ resolved_at TEXT,
269
+ created_at TEXT,
270
+ FOREIGN KEY(claim_a_id) REFERENCES knowledge_units(ku_id),
271
+ FOREIGN KEY(claim_b_id) REFERENCES knowledge_units(ku_id)
272
+ );
273
+ """)
274
+ self._db.commit()
275
+
276
+ def _load_graph(self):
277
+ """启动时从 SQLite 重建 NetworkX 图。"""
278
+ rows = self._db.execute(
279
+ "SELECT from_ku, to_ku, rel_type, weight FROM relations"
280
+ ).fetchall()
281
+ for row in rows:
282
+ self._graph.add_edge(
283
+ row["from_ku"], row["to_ku"],
284
+ rel_type=row["rel_type"],
285
+ weight=row["weight"],
286
+ )
287
+
288
+ # ── 单个 KU ──────────────────────────────────────────────
289
+
290
+ # patch_ku 允许直接写的普通列(JSON 列会按 list/dict 自动序列化)。
291
+ # 刻意不含 ku_id / created_at / updated_at:前者是主键,后者由存储层维护。
292
+ _PATCH_COLUMNS = frozenset({
293
+ "name", "summary", "confidence", "status", "domain",
294
+ "version", "tags", "sources", "relations",
295
+ })
296
+
297
+ def get(self, ku_id: str) -> Optional[KU]:
298
+ with self._lock:
299
+ row = self._db.execute(
300
+ "SELECT * FROM knowledge_units WHERE ku_id = ?", (ku_id,)
301
+ ).fetchone()
302
+ return _row_to_ku(tuple(row)) if row else None
303
+
304
+ def put(self, ku: KU) -> None:
305
+ with self._lock:
306
+ ku.updated_at = datetime.now(timezone.utc).isoformat()
307
+ self._db.execute("""
308
+ INSERT INTO knowledge_units
309
+ (ku_id,name,ku_type,summary,confidence,status,domain,
310
+ version,tags,sources,relations,extra,created_at,updated_at)
311
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
312
+ ON CONFLICT(ku_id) DO UPDATE SET
313
+ name=excluded.name, summary=excluded.summary,
314
+ confidence=excluded.confidence, status=excluded.status,
315
+ domain=excluded.domain, version=excluded.version,
316
+ tags=excluded.tags, sources=excluded.sources,
317
+ relations=excluded.relations, extra=excluded.extra,
318
+ updated_at=excluded.updated_at
319
+ """, _ku_to_row(ku))
320
+ self._db.commit()
321
+ # 同步 FTS:content table 不支持 OR REPLACE,用 DELETE + INSERT
322
+ self._db.execute("DELETE FROM ku_fts WHERE ku_id=?", (ku.ku_id,))
323
+ self._db.execute(
324
+ "INSERT INTO ku_fts(ku_id,name,summary,domain) VALUES (?,?,?,?)",
325
+ (ku.ku_id, ku.name, ku.summary, ku.domain)
326
+ )
327
+ self._db.commit()
328
+
329
+ def patch_ku(
330
+ self,
331
+ ku_id: str,
332
+ columns: Optional[dict] = None,
333
+ extra: Optional[dict] = None,
334
+ ) -> int:
335
+ """只写「指定的列」和「extra 里指定的键」,**其余列一个字节都不动**。
336
+
337
+ 为什么需要:``put()`` 是整行 UPSERT。复习评分只改 ``extra["review"]``,走 put()
338
+ 会把整行其它列按「写入前的快照」回写 —— KEE 在同一个窗口里刚合并进去的
339
+ sources / confidence / supports 会被悄悄冲掉;反过来,KEE 的整行写也会把复习
340
+ 进度冲掉。``extra`` 是 review / supports / claim_status / source_text 共用的
341
+ JSON 列,所以「只改其中一两个键」的写入必须走这里,而不是 put()。
342
+
343
+ 返回受影响行数:0 表示该 KU 不存在,调用方应回退到 ``put()``(那是插入语义)。
344
+ """
345
+ cols = {k: v for k, v in (columns or {}).items() if k in self._PATCH_COLUMNS}
346
+ with self._lock:
347
+ if not cols and not extra:
348
+ return 1 if self._db.execute(
349
+ "SELECT 1 FROM knowledge_units WHERE ku_id=?", (ku_id,)
350
+ ).fetchone() else 0
351
+
352
+ sets: list[str] = []
353
+ args: list = []
354
+ for k, v in cols.items():
355
+ sets.append(f"{k}=?")
356
+ args.append(_dump(v))
357
+ if extra:
358
+ row = self._db.execute(
359
+ "SELECT extra FROM knowledge_units WHERE ku_id=?", (ku_id,)
360
+ ).fetchone()
361
+ if row is None:
362
+ return 0
363
+ cur = _loads_dict(row["extra"])
364
+ cur.update(extra)
365
+ sets.append("extra=?")
366
+ args.append(json.dumps(cur, ensure_ascii=False))
367
+ sets.append("updated_at=?")
368
+ args.append(datetime.now(timezone.utc).isoformat())
369
+ args.append(ku_id)
370
+
371
+ cur2 = self._db.execute(
372
+ f"UPDATE knowledge_units SET {', '.join(sets)} WHERE ku_id=?", args
373
+ )
374
+ self._db.commit()
375
+ n = cur2.rowcount
376
+ # 改了 FTS 索引覆盖的三列才需要同步,否则多一次写纯属浪费
377
+ if n and any(k in cols for k in ("name", "summary", "domain")):
378
+ row2 = self._db.execute(
379
+ "SELECT name, summary, domain FROM knowledge_units WHERE ku_id=?", (ku_id,)
380
+ ).fetchone()
381
+ if row2 is not None:
382
+ self._db.execute("DELETE FROM ku_fts WHERE ku_id=?", (ku_id,))
383
+ self._db.execute(
384
+ "INSERT INTO ku_fts(ku_id,name,summary,domain) VALUES (?,?,?,?)",
385
+ (ku_id, row2["name"] or "", row2["summary"] or "", row2["domain"] or ""),
386
+ )
387
+ self._db.commit()
388
+ return n if n > 0 else 0
389
+
390
+ def update_extra(self, ku_id: str, fn) -> Optional[dict]:
391
+ """在锁内「读 extra → 交给 fn 算 → 写回 extra」,整段原子。
392
+
393
+ 给「读-改-写必须无竞态」的场景用(复习评分:读 review 状态 → SM-2 推演 →
394
+ 写回)。``fn(extra)`` 返回新 dict 则落盘,返回 None 表示不改。
395
+ 返回落盘后的 extra;None 表示该 KU 已不存在。
396
+ """
397
+ with self._lock:
398
+ row = self._db.execute(
399
+ "SELECT extra FROM knowledge_units WHERE ku_id=?", (ku_id,)
400
+ ).fetchone()
401
+ if row is None:
402
+ return None
403
+ extra = _loads_dict(row["extra"])
404
+ new_extra = fn(extra)
405
+ if new_extra is None:
406
+ return extra
407
+ if not isinstance(new_extra, dict):
408
+ raise TypeError("update_extra 的回调必须返回 dict 或 None")
409
+ self._db.execute(
410
+ "UPDATE knowledge_units SET extra=?, updated_at=? WHERE ku_id=?",
411
+ (json.dumps(new_extra, ensure_ascii=False),
412
+ datetime.now(timezone.utc).isoformat(), ku_id),
413
+ )
414
+ self._db.commit()
415
+ return new_extra
416
+
417
+ def delete(self, ku_id: str) -> bool:
418
+ """软删除(将状态置为 deleted,保留行供审计)。不级联。
419
+
420
+ 返回:
421
+ True — 此前该 id 存在且未被删,本次成功置为 deleted
422
+ False — id 不存在或已经是 deleted
423
+
424
+ 阶段 B 设计:本方法**不**发事件,事件由调用方按业务语义发。
425
+ - KEE dedup 删重复 KU → 不发(重复 KU 从未进 vector,无外部观测需要)
426
+ - routes.delete_source 级联删 → 调用方负责 publish KNOWLEDGE_DELETED
427
+ 原因:事件是「对知识库的外部观测」,不是「存储层副作用」。
428
+ 若存储层自动发,会与 KEE / 路由层的事件总线污染(KEE 内部 delete 走 store
429
+ 不应该被业务订阅者看到,否则会误触发 indexing 重新处理同一 ID)。
430
+ """
431
+ with self._lock:
432
+ cur = self._db.execute(
433
+ "UPDATE knowledge_units SET status='deleted', "
434
+ "updated_at=? WHERE ku_id=? AND status != 'deleted'",
435
+ (datetime.now(timezone.utc).isoformat(), ku_id)
436
+ )
437
+ self._db.commit()
438
+ return cur.rowcount > 0
439
+
440
+ def clear(self) -> int:
441
+ """清空整个知识库:硬删除所有 KU / 关系 / 冲突卡 / 全文索引 + 重置内存图。
442
+ 返回清空前的 KU 行数。用于"清库重来"(不可逆)。"""
443
+ with self._lock:
444
+ try:
445
+ n = self._db.execute("SELECT COUNT(*) FROM knowledge_units").fetchone()[0]
446
+ except Exception:
447
+ n = 0
448
+ # 先删引用方(relations 外键指向 knowledge_units),knowledge_units 放最后,
449
+ # 否则先删父表会触发 FOREIGN KEY constraint failed。
450
+ for tbl in ("relations", "conflict_cards", "ku_fts", "knowledge_units"):
451
+ try:
452
+ self._db.execute(f"DELETE FROM {tbl}")
453
+ except Exception:
454
+ pass # 个别表(如 ku_fts)在旧库可能不存在
455
+ self._db.commit()
456
+ self._graph.clear()
457
+ return n
458
+
459
+ # ── 批量 ─────────────────────────────────────────────────
460
+
461
+ def batch_write(self, kus: list[KU]) -> None:
462
+ with self._lock:
463
+ now = datetime.now(timezone.utc).isoformat()
464
+ rows = []
465
+ for ku in kus:
466
+ ku.updated_at = now
467
+ rows.append(_ku_to_row(ku))
468
+ self._db.executemany("""
469
+ INSERT INTO knowledge_units
470
+ (ku_id,name,ku_type,summary,confidence,status,domain,
471
+ version,tags,sources,relations,extra,created_at,updated_at)
472
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
473
+ ON CONFLICT(ku_id) DO UPDATE SET
474
+ name=excluded.name, summary=excluded.summary,
475
+ confidence=excluded.confidence, status=excluded.status,
476
+ domain=excluded.domain, version=excluded.version,
477
+ tags=excluded.tags, sources=excluded.sources,
478
+ relations=excluded.relations, extra=excluded.extra,
479
+ updated_at=excluded.updated_at
480
+ """, rows)
481
+ self._db.commit()
482
+ # 同步 FTS
483
+ for ku in kus:
484
+ self._db.execute("DELETE FROM ku_fts WHERE ku_id=?", (ku.ku_id,))
485
+ self._db.execute(
486
+ "INSERT INTO ku_fts(ku_id,name,summary,domain) VALUES (?,?,?,?)",
487
+ (ku.ku_id, ku.name, ku.summary, ku.domain)
488
+ )
489
+ self._db.commit()
490
+
491
+ # ── 查询 ─────────────────────────────────────────────────
492
+
493
+ def query_by_type(
494
+ self,
495
+ ku_type: KUType,
496
+ domain: str = "",
497
+ status: str = "active",
498
+ limit: int = 100,
499
+ ) -> list[KU]:
500
+ with self._lock:
501
+ if domain:
502
+ rows = self._db.execute(
503
+ "SELECT * FROM knowledge_units "
504
+ "WHERE ku_type=? AND domain=? AND status=? LIMIT ?",
505
+ (ku_type.value, domain, status, limit)
506
+ ).fetchall()
507
+ else:
508
+ rows = self._db.execute(
509
+ "SELECT * FROM knowledge_units "
510
+ "WHERE ku_type=? AND status=? LIMIT ?",
511
+ (ku_type.value, status, limit)
512
+ ).fetchall()
513
+ return [_row_to_ku(tuple(r)) for r in rows]
514
+
515
+ def query_by_domain(self, domain: str, limit: int = 200) -> list[KU]:
516
+ with self._lock:
517
+ rows = self._db.execute(
518
+ "SELECT * FROM knowledge_units "
519
+ "WHERE domain=? AND status!='deleted' LIMIT ?",
520
+ (domain, limit)
521
+ ).fetchall()
522
+ return [_row_to_ku(tuple(r)) for r in rows]
523
+
524
+ def search_by_name(self, name: str, fuzzy: bool = True) -> list[KU]:
525
+ with self._lock:
526
+ if fuzzy:
527
+ rows = self._db.execute(
528
+ "SELECT ku.* FROM knowledge_units ku "
529
+ "JOIN ku_fts ON ku.ku_id = ku_fts.ku_id "
530
+ "WHERE ku_fts MATCH ? AND ku.status!='deleted' LIMIT 20",
531
+ (name,)
532
+ ).fetchall()
533
+ else:
534
+ rows = self._db.execute(
535
+ "SELECT * FROM knowledge_units "
536
+ "WHERE name=? AND status!='deleted'",
537
+ (name,)
538
+ ).fetchall()
539
+ return [_row_to_ku(tuple(r)) for r in rows]
540
+
541
+ # ── 关系 ─────────────────────────────────────────────────
542
+
543
+ def add_relation(self, rel: Relation) -> None:
544
+ with self._lock:
545
+ self._db.execute("""
546
+ INSERT OR IGNORE INTO relations
547
+ (rel_id,from_ku,to_ku,rel_type,weight,source,created_at)
548
+ VALUES (?,?,?,?,?,?,?)
549
+ """, _rel_to_row(rel))
550
+ self._db.commit()
551
+ self._graph.add_edge(
552
+ rel.from_ku, rel.to_ku,
553
+ rel_type=rel.rel_type,
554
+ weight=rel.weight,
555
+ )
556
+
557
+ def batch_add_relations(self, rels: "list[Relation]") -> None:
558
+ """批量写关系:一次 executemany + 单次 commit,再同步内存图。
559
+
560
+ 替代逐条 add_relation(每条一次 commit=一次 fsync):大文档关系发现产出
561
+ 上千条关系时,逐条 commit 的 fsync 风暴会让摄入 job 卡数分钟。此处一次落盘。
562
+ """
563
+ if not rels:
564
+ return
565
+ with self._lock:
566
+ self._db.executemany("""
567
+ INSERT OR IGNORE INTO relations
568
+ (rel_id,from_ku,to_ku,rel_type,weight,source,created_at)
569
+ VALUES (?,?,?,?,?,?,?)
570
+ """, [_rel_to_row(r) for r in rels])
571
+ self._db.commit()
572
+ for r in rels:
573
+ self._graph.add_edge(
574
+ r.from_ku, r.to_ku,
575
+ rel_type=r.rel_type,
576
+ weight=r.weight,
577
+ )
578
+
579
+ def delete_relation(self, rel_id: str) -> None:
580
+ with self._lock:
581
+ row = self._db.execute(
582
+ "SELECT from_ku,to_ku,rel_type FROM relations WHERE rel_id=?",
583
+ (rel_id,)
584
+ ).fetchone()
585
+ self._db.execute("DELETE FROM relations WHERE rel_id=?", (rel_id,))
586
+ self._db.commit()
587
+ if row:
588
+ from_ku, to_ku, _ = row[0], row[1], row[2]
589
+ # 同步移除图中的边(若存在)
590
+ if self._graph.has_edge(from_ku, to_ku):
591
+ try:
592
+ self._graph.remove_edge(from_ku, to_ku)
593
+ except Exception:
594
+ pass
595
+
596
+ def redirect_relations(self, from_ku_id: str, to_ku_id: str) -> int:
597
+ """把 from_ku_id 的所有关系重定向到 to_ku_id;丢弃自环;去重。"""
598
+ if from_ku_id == to_ku_id:
599
+ return 0
600
+ with self._lock:
601
+ rows = self._db.execute(
602
+ "SELECT rel_id,from_ku,to_ku,rel_type,weight,source,created_at "
603
+ "FROM relations WHERE from_ku=? OR to_ku=?",
604
+ (from_ku_id, from_ku_id)
605
+ ).fetchall()
606
+
607
+ redirected = 0
608
+ for r in rows:
609
+ rel = _row_to_rel(tuple(r))
610
+ new_from = to_ku_id if rel.from_ku == from_ku_id else rel.from_ku
611
+ new_to = to_ku_id if rel.to_ku == from_ku_id else rel.to_ku
612
+
613
+ # 删除旧关系(表+图)
614
+ self.delete_relation(rel.rel_id)
615
+
616
+ # 自环丢弃(重定向后两端相同)
617
+ if new_from == new_to:
618
+ continue
619
+
620
+ # 检查是否已存在等价关系(避免重复)
621
+ existing = self.get_relations(new_from, rel_type=rel.rel_type, direction="out")
622
+ if any(e.to_ku == new_to for e in existing):
623
+ continue
624
+
625
+ rel.from_ku = new_from
626
+ rel.to_ku = new_to
627
+ self.add_relation(rel)
628
+ redirected += 1
629
+
630
+ return redirected
631
+
632
+ def get_relations(
633
+ self,
634
+ ku_id: str,
635
+ rel_type: str = "",
636
+ direction: str = "both",
637
+ ) -> list[Relation]:
638
+ with self._lock:
639
+ if direction == "out":
640
+ q = "SELECT * FROM relations WHERE from_ku=?"
641
+ params = [ku_id]
642
+ elif direction == "in":
643
+ q = "SELECT * FROM relations WHERE to_ku=?"
644
+ params = [ku_id]
645
+ else:
646
+ q = "SELECT * FROM relations WHERE from_ku=? OR to_ku=?"
647
+ params = [ku_id, ku_id]
648
+ if rel_type:
649
+ q += " AND rel_type=?"
650
+ params.append(rel_type)
651
+ rows = self._db.execute(q, params).fetchall()
652
+ return [_row_to_rel(tuple(r)) for r in rows]
653
+
654
+ def get_all_relations(self, limit: int = 5000) -> list[Relation]:
655
+ with self._lock:
656
+ rows = self._db.execute(
657
+ "SELECT * FROM relations LIMIT ?", (limit,)
658
+ ).fetchall()
659
+ return [_row_to_rel(tuple(r)) for r in rows]
660
+
661
+ def neighbors(
662
+ self,
663
+ ku_id: str,
664
+ rel_type: str = "",
665
+ depth: int = 1,
666
+ ) -> list[KU]:
667
+ with self._lock:
668
+ if depth == 1:
669
+ # 直接邻居,从内存图取 ID,再去 SQLite 取完整数据
670
+ out_nodes = list(self._graph.successors(ku_id))
671
+ in_nodes = list(self._graph.predecessors(ku_id))
672
+ neighbor_ids = set(out_nodes + in_nodes)
673
+ else:
674
+ # BFS 多层
675
+ neighbor_ids = set()
676
+ frontier = {ku_id}
677
+ for _ in range(depth):
678
+ next_frontier = set()
679
+ for node in frontier:
680
+ next_frontier.update(self._graph.successors(node))
681
+ next_frontier.update(self._graph.predecessors(node))
682
+ neighbor_ids.update(next_frontier)
683
+ frontier = next_frontier
684
+ neighbor_ids.discard(ku_id)
685
+
686
+ result = []
687
+ for nid in neighbor_ids:
688
+ ku = self.get(nid)
689
+ if ku:
690
+ result.append(ku)
691
+ return result
692
+
693
+ def shortest_path(self, from_id: str, to_id: str) -> list[str]:
694
+ try:
695
+ return nx.shortest_path(self._graph, from_id, to_id)
696
+ except (nx.NetworkXNoPath, nx.NodeNotFound):
697
+ return []
698
+
699
+ # ── Conflict Cards ────────────────────────────────────────
700
+
701
+ def save_conflict(self, card: ConflictCard) -> None:
702
+ with self._lock:
703
+ self._db.execute("""
704
+ INSERT OR REPLACE INTO conflict_cards
705
+ (conflict_id,claim_a_id,claim_b_id,domain,status,
706
+ strategy,note,resolved_by,resolved_at,created_at)
707
+ VALUES (?,?,?,?,?,?,?,?,?,?)
708
+ """, _card_to_row(card))
709
+ self._db.commit()
710
+
711
+ def get_conflict(self, conflict_id: str) -> Optional[ConflictCard]:
712
+ with self._lock:
713
+ row = self._db.execute(
714
+ "SELECT * FROM conflict_cards WHERE conflict_id=?",
715
+ (conflict_id,)
716
+ ).fetchone()
717
+ return _row_to_card(tuple(row)) if row else None
718
+
719
+ def list_conflicts(
720
+ self,
721
+ status: str = "open",
722
+ domain: str = "",
723
+ ) -> list[ConflictCard]:
724
+ with self._lock:
725
+ if domain:
726
+ rows = self._db.execute(
727
+ "SELECT * FROM conflict_cards WHERE status=? AND domain=?",
728
+ (status, domain)
729
+ ).fetchall()
730
+ else:
731
+ rows = self._db.execute(
732
+ "SELECT * FROM conflict_cards WHERE status=?", (status,)
733
+ ).fetchall()
734
+ return [_row_to_card(tuple(r)) for r in rows]
735
+
736
+ def find_claims_by_domain(
737
+ self,
738
+ domain: str,
739
+ status: ClaimStatus = None,
740
+ ) -> list[ClaimKU]:
741
+ with self._lock:
742
+ if status:
743
+ rows = self._db.execute(
744
+ "SELECT * FROM knowledge_units "
745
+ "WHERE ku_type='Claim' AND domain=? "
746
+ "AND json_extract(extra,'$.claim_status')=?",
747
+ (domain, status.value)
748
+ ).fetchall()
749
+ else:
750
+ rows = self._db.execute(
751
+ "SELECT * FROM knowledge_units "
752
+ "WHERE ku_type='Claim' AND domain=?",
753
+ (domain,)
754
+ ).fetchall()
755
+ return [_row_to_ku(tuple(r)) for r in rows]
756
+
757
+ # ── 统계 ─────────────────────────────────────────────────
758
+
759
+ def stats(self) -> dict:
760
+ with self._lock:
761
+ type_counts = {}
762
+ rows = self._db.execute(
763
+ "SELECT ku_type, COUNT(*) as cnt "
764
+ "FROM knowledge_units WHERE status!='deleted' "
765
+ "GROUP BY ku_type"
766
+ ).fetchall()
767
+ for row in rows:
768
+ type_counts[row["ku_type"]] = row["cnt"]
769
+
770
+ rel_count = self._db.execute(
771
+ "SELECT COUNT(*) FROM relations"
772
+ ).fetchone()[0]
773
+
774
+ conflict_count = self._db.execute(
775
+ "SELECT COUNT(*) FROM conflict_cards WHERE status='open'"
776
+ ).fetchone()[0]
777
+
778
+ return {
779
+ "ku_counts": type_counts,
780
+ "total_kus": sum(type_counts.values()),
781
+ "total_relations": rel_count,
782
+ "open_conflicts": conflict_count,
783
+ "graph_nodes": self._graph.number_of_nodes(),
784
+ "graph_edges": self._graph.number_of_edges(),
785
+ }
786
+
787
+ def close(self) -> None:
788
+ """显式关闭数据库连接。测试和应用退出时调用。"""
789
+ self._db.close()