foxclaw 0.1.0 → 0.1.2

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 (495) hide show
  1. package/README.md +54 -29
  2. package/bin/foxclaw.cjs +51 -0
  3. package/index.js +11 -10
  4. package/package.json +46 -38
  5. package/python/.dockerignore +35 -0
  6. package/python/.env.example +165 -0
  7. package/python/.gitattributes +46 -0
  8. package/python/.github/workflows/ci.yml +122 -0
  9. package/python/.github/workflows/release.yml +183 -0
  10. package/python/Dockerfile +64 -0
  11. package/python/LICENSE +21 -0
  12. package/python/MANIFEST.in +14 -0
  13. package/python/README.md +192 -0
  14. package/python/README_CN.md +324 -0
  15. package/python/bench/bm25_audit.py +199 -0
  16. package/python/bench/bm25_regression.py +178 -0
  17. package/python/bench/bm25_stress.py +159 -0
  18. package/python/bench/cd_audit.py +238 -0
  19. package/python/bench/chaos.py +142 -0
  20. package/python/bench/concurrency.py +103 -0
  21. package/python/bench/e2e_real_models.py +95 -0
  22. package/python/bench/llm_retry_test.py +248 -0
  23. package/python/bench/load_test.py +196 -0
  24. package/python/bench/vector_ivf_upgrade.py +134 -0
  25. package/python/docs/CHANNELS.md +195 -0
  26. package/python/docs/GOVERNANCE.md +164 -0
  27. package/python/docs/RELEASE.md +109 -0
  28. package/python/docs/RUN.md +75 -0
  29. package/python/docs/RUN_CN.md +121 -0
  30. package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
  31. package/python/evals/__init__.py +0 -0
  32. package/python/evals/governance/README.md +61 -0
  33. package/python/evals/governance/__init__.py +0 -0
  34. package/python/evals/governance/cases.py +152 -0
  35. package/python/evals/governance/compare_embeddings.py +94 -0
  36. package/python/evals/governance/convergence.svg +11 -0
  37. package/python/evals/governance/report.json +371 -0
  38. package/python/evals/governance/run_evals.py +400 -0
  39. package/python/examples/governance_middleware_demo.py +77 -0
  40. package/python/examples/langgraph_governance_demo.py +108 -0
  41. package/python/examples/sample_itinerary.csv +6 -0
  42. package/python/examples/sample_knowledge.csv +9 -0
  43. package/python/examples/sample_transactions.csv +14 -0
  44. package/python/foxclaw/__init__.py +526 -0
  45. package/python/foxclaw/agents/__init__.py +12 -0
  46. package/python/foxclaw/agents/agents.py +35 -0
  47. package/python/foxclaw/agents/base.py +865 -0
  48. package/python/foxclaw/agents/impl/__init__.py +1 -0
  49. package/python/foxclaw/agents/impl/_helpers.py +78 -0
  50. package/python/foxclaw/agents/impl/finance.py +248 -0
  51. package/python/foxclaw/agents/impl/office.py +367 -0
  52. package/python/foxclaw/agents/impl/supervisor.py +659 -0
  53. package/python/foxclaw/agents/mixins/_context.py +151 -0
  54. package/python/foxclaw/agents/mixins/augment.py +149 -0
  55. package/python/foxclaw/agents/mixins/llm.py +66 -0
  56. package/python/foxclaw/agents/mixins/memory.py +35 -0
  57. package/python/foxclaw/agents/mixins/preference.py +81 -0
  58. package/python/foxclaw/agents/mixins/prompt.py +77 -0
  59. package/python/foxclaw/agents/mixins/skillbox.py +58 -0
  60. package/python/foxclaw/agents/mixins/tool_exec.py +443 -0
  61. package/python/foxclaw/api/__init__.py +283 -0
  62. package/python/foxclaw/api/app.py +123 -0
  63. package/python/foxclaw/api/middleware.py +317 -0
  64. package/python/foxclaw/api/routes/ahs.py +417 -0
  65. package/python/foxclaw/api/routes/attachments.py +117 -0
  66. package/python/foxclaw/api/routes/browser.py +39 -0
  67. package/python/foxclaw/api/routes/domain.py +314 -0
  68. package/python/foxclaw/api/routes/knowledge.py +163 -0
  69. package/python/foxclaw/api/routes/llm_admin.py +461 -0
  70. package/python/foxclaw/api/routes/memory.py +89 -0
  71. package/python/foxclaw/api/routes/orchestration.py +41 -0
  72. package/python/foxclaw/api/routes/recovery.py +43 -0
  73. package/python/foxclaw/api/routes/review.py +275 -0
  74. package/python/foxclaw/api/routes/search.py +37 -0
  75. package/python/foxclaw/api/routes/system.py +327 -0
  76. package/python/foxclaw/api/routes/task.py +281 -0
  77. package/python/foxclaw/api/routes/ws.py +68 -0
  78. package/python/foxclaw/api/spine.py +55 -0
  79. package/python/foxclaw/browser/__init__.py +13 -0
  80. package/python/foxclaw/browser/action/__init__.py +1 -0
  81. package/python/foxclaw/browser/action/action_executor.py +54 -0
  82. package/python/foxclaw/browser/action/action_types.py +45 -0
  83. package/python/foxclaw/browser/action/action_verifier.py +68 -0
  84. package/python/foxclaw/browser/action/url_policy.py +147 -0
  85. package/python/foxclaw/browser/adapter/__init__.py +1 -0
  86. package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
  87. package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
  88. package/python/foxclaw/browser/driver/__init__.py +1 -0
  89. package/python/foxclaw/browser/driver/base_driver.py +61 -0
  90. package/python/foxclaw/browser/driver/playwright_driver.py +166 -0
  91. package/python/foxclaw/browser/perception/__init__.py +1 -0
  92. package/python/foxclaw/browser/perception/dom_extractor.py +43 -0
  93. package/python/foxclaw/browser/perception/element_indexer.py +57 -0
  94. package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
  95. package/python/foxclaw/browser/perception/snapshot.py +29 -0
  96. package/python/foxclaw/browser/service.py +148 -0
  97. package/python/foxclaw/browser/session/__init__.py +1 -0
  98. package/python/foxclaw/browser/session/session_manager.py +57 -0
  99. package/python/foxclaw/capability/__init__.py +12 -0
  100. package/python/foxclaw/capability/embedding_service.py +67 -0
  101. package/python/foxclaw/channels/__init__.py +140 -0
  102. package/python/foxclaw/channels/discord.py +184 -0
  103. package/python/foxclaw/channels/dispatcher.py +204 -0
  104. package/python/foxclaw/channels/feishu.py +225 -0
  105. package/python/foxclaw/channels/openclaw_bridge.py +156 -0
  106. package/python/foxclaw/channels/signing.py +187 -0
  107. package/python/foxclaw/channels/telegram.py +121 -0
  108. package/python/foxclaw/channels/weixin.py +225 -0
  109. package/python/foxclaw/cli.py +154 -0
  110. package/python/foxclaw/cognitive/__init__.py +4 -0
  111. package/python/foxclaw/cognitive/reasoner.py +82 -0
  112. package/python/foxclaw/cognitive/state.py +147 -0
  113. package/python/foxclaw/core/__init__.py +9 -0
  114. package/python/foxclaw/core/audit_chain.py +133 -0
  115. package/python/foxclaw/core/audit_sink.py +100 -0
  116. package/python/foxclaw/core/compaction.py +283 -0
  117. package/python/foxclaw/core/config.py +65 -0
  118. package/python/foxclaw/core/confirm_nonce.py +157 -0
  119. package/python/foxclaw/core/dispatch.py +27 -0
  120. package/python/foxclaw/core/envfile.py +48 -0
  121. package/python/foxclaw/core/failure_policy.py +151 -0
  122. package/python/foxclaw/core/gateway.py +85 -0
  123. package/python/foxclaw/core/governance_audit.py +175 -0
  124. package/python/foxclaw/core/idempotency.py +289 -0
  125. package/python/foxclaw/core/injection_guard.py +221 -0
  126. package/python/foxclaw/core/memory_adapter.py +516 -0
  127. package/python/foxclaw/core/metrics.py +89 -0
  128. package/python/foxclaw/core/model_router.py +1097 -0
  129. package/python/foxclaw/core/paths.py +103 -0
  130. package/python/foxclaw/core/recovery.py +362 -0
  131. package/python/foxclaw/core/scheduler.py +144 -0
  132. package/python/foxclaw/core/secrets.py +134 -0
  133. package/python/foxclaw/core/sessions.py +157 -0
  134. package/python/foxclaw/core/settings.py +430 -0
  135. package/python/foxclaw/core/shared_write.py +198 -0
  136. package/python/foxclaw/core/steering_queue.py +153 -0
  137. package/python/foxclaw/core/system_bus.py +84 -0
  138. package/python/foxclaw/core/thought_chain.py +318 -0
  139. package/python/foxclaw/core/token_meter.py +235 -0
  140. package/python/foxclaw/core/tools.py +172 -0
  141. package/python/foxclaw/core/tracing.py +390 -0
  142. package/python/foxclaw/core/truth_pipeline.py +114 -0
  143. package/python/foxclaw/core/vec_recall_adapter.py +503 -0
  144. package/python/foxclaw/core/verify.py +181 -0
  145. package/python/foxclaw/core/web_search.py +242 -0
  146. package/python/foxclaw/finance/__init__.py +15 -0
  147. package/python/foxclaw/finance/evidence.py +153 -0
  148. package/python/foxclaw/finance/fx.py +100 -0
  149. package/python/foxclaw/finance/graph.py +188 -0
  150. package/python/foxclaw/finance/models.py +150 -0
  151. package/python/foxclaw/finance/nlparse.py +575 -0
  152. package/python/foxclaw/finance/policy.py +329 -0
  153. package/python/foxclaw/finance/reasoner.py +129 -0
  154. package/python/foxclaw/finance/reasoning.py +340 -0
  155. package/python/foxclaw/finance/reconcile.py +180 -0
  156. package/python/foxclaw/finance/runtime.py +527 -0
  157. package/python/foxclaw/governance/__init__.py +24 -0
  158. package/python/foxclaw/governance/middleware.py +184 -0
  159. package/python/foxclaw/harness/__init__.py +17 -0
  160. package/python/foxclaw/harness/_base.py +72 -0
  161. package/python/foxclaw/harness/_policy.py +359 -0
  162. package/python/foxclaw/harness/drift.py +89 -0
  163. package/python/foxclaw/harness/event_bus.py +31 -0
  164. package/python/foxclaw/harness/harness.py +446 -0
  165. package/python/foxclaw/harness/loop_detection.py +152 -0
  166. package/python/foxclaw/itinerary/__init__.py +10 -0
  167. package/python/foxclaw/itinerary/amap_client.py +117 -0
  168. package/python/foxclaw/itinerary/models.py +80 -0
  169. package/python/foxclaw/itinerary/reasoner.py +101 -0
  170. package/python/foxclaw/itinerary/reasoning.py +174 -0
  171. package/python/foxclaw/itinerary/runtime.py +148 -0
  172. package/python/foxclaw/kernel/__init__.py +13 -0
  173. package/python/foxclaw/kernel/capabilities.py +73 -0
  174. package/python/foxclaw/kernel/policy_gate.py +114 -0
  175. package/python/foxclaw/kernel/process_manager.py +205 -0
  176. package/python/foxclaw/kernel/scheduler.py +131 -0
  177. package/python/foxclaw/kernel/sentinel.py +453 -0
  178. package/python/foxclaw/kernel/sentinel_client.py +246 -0
  179. package/python/foxclaw/kernel/spine.py +97 -0
  180. package/python/foxclaw/knowledge/__init__.py +376 -0
  181. package/python/foxclaw/knowledge/truth_crystallizer.py +92 -0
  182. package/python/foxclaw/learning/__init__.py +12 -0
  183. package/python/foxclaw/learning/knowledge_graph.py +149 -0
  184. package/python/foxclaw/learning/models.py +93 -0
  185. package/python/foxclaw/learning/reasoner.py +70 -0
  186. package/python/foxclaw/learning/runtime.py +363 -0
  187. package/python/foxclaw/learning/spaced_repetition.py +96 -0
  188. package/python/foxclaw/mcp_server.py +189 -0
  189. package/python/foxclaw/orchestration/__init__.py +184 -0
  190. package/python/foxclaw/pipeline/__init__.py +23 -0
  191. package/python/foxclaw/pipeline/cognitive_pipeline.py +481 -0
  192. package/python/foxclaw/pipeline/supervisor_stages.py +230 -0
  193. package/python/foxclaw/planning/__init__.py +17 -0
  194. package/python/foxclaw/planning/llm_planner.py +232 -0
  195. package/python/foxclaw/planning/planner.py +99 -0
  196. package/python/foxclaw/search/__init__.py +24 -0
  197. package/python/foxclaw/search/agent_port.py +194 -0
  198. package/python/foxclaw/search/evidence.py +113 -0
  199. package/python/foxclaw/search/external_search.py +50 -0
  200. package/python/foxclaw/search/hkc_read_port.py +60 -0
  201. package/python/foxclaw/search/internal_search.py +73 -0
  202. package/python/foxclaw/search/models.py +129 -0
  203. package/python/foxclaw/search/planner.py +44 -0
  204. package/python/foxclaw/search/retriever.py +39 -0
  205. package/python/foxclaw/search/runtime.py +134 -0
  206. package/python/foxclaw/search/strategy.py +44 -0
  207. package/python/foxclaw/skills/__init__.py +613 -0
  208. package/python/foxclaw/skills/hooks_spec.py +73 -0
  209. package/python/foxclaw/skills/lifecycle.py +278 -0
  210. package/python/foxclaw/skills/markdown_skill.py +174 -0
  211. package/python/foxclaw/skills/markdown_store.py +39 -0
  212. package/python/foxclaw/skills/mcp_client.py +190 -0
  213. package/python/foxclaw/skills/mcp_store.py +43 -0
  214. package/python/foxclaw/skills/permission.py +180 -0
  215. package/python/foxclaw/skills/pipeline.py +386 -0
  216. package/python/foxclaw/skills/policy.py +80 -0
  217. package/python/foxclaw/skills/review.py +72 -0
  218. package/python/foxclaw/skills/store.py +57 -0
  219. package/python/frontend/assets/core_bg.jpg +0 -0
  220. package/python/frontend/deck/auth.js +32 -0
  221. package/python/frontend/deck/chat_model_picker.js +537 -0
  222. package/python/frontend/deck/domain.js +296 -0
  223. package/python/frontend/deck/main.js +1950 -0
  224. package/python/frontend/deck/markdown.js +48 -0
  225. package/python/frontend/deck/settings.js +324 -0
  226. package/python/frontend/deck/state.js +6 -0
  227. package/python/frontend/deck/util.js +6 -0
  228. package/python/frontend/deck/visuals.js +212 -0
  229. package/python/frontend/deck.css +855 -0
  230. package/python/frontend/deck.html +686 -0
  231. package/python/frontend/deck_v2.html +341 -0
  232. package/python/frontend/planet/index.html +266 -0
  233. package/python/frontend/planet/planet.css +579 -0
  234. package/python/frontend/planet/planet.js +1637 -0
  235. package/python/frontend/planet/vendor/three.core.min.js +6 -0
  236. package/python/frontend/planet/vendor/three.module.min.js +6 -0
  237. package/python/frontend/v3/assets/index-BD7VPbMV.js +33 -0
  238. package/python/frontend/v3/assets/index-CAvnYWGH.css +1 -0
  239. package/python/frontend/v3/index.html +15 -0
  240. package/python/frontend/widget_runtime.js +295 -0
  241. package/python/frontend/widgets.js +696 -0
  242. package/python/frontend_src/index.html +14 -0
  243. package/python/frontend_src/package-lock.json +1376 -0
  244. package/python/frontend_src/package.json +19 -0
  245. package/python/frontend_src/src/App.vue +62 -0
  246. package/python/frontend_src/src/api.js +89 -0
  247. package/python/frontend_src/src/components/ChatDock.vue +377 -0
  248. package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
  249. package/python/frontend_src/src/components/DocViewer.vue +171 -0
  250. package/python/frontend_src/src/components/Inspector.vue +241 -0
  251. package/python/frontend_src/src/components/LeftRail.vue +394 -0
  252. package/python/frontend_src/src/components/MenuPopover.vue +130 -0
  253. package/python/frontend_src/src/components/MessageStream.vue +401 -0
  254. package/python/frontend_src/src/components/ModelPicker.vue +355 -0
  255. package/python/frontend_src/src/components/QuickIngest.vue +118 -0
  256. package/python/frontend_src/src/components/Stage.vue +226 -0
  257. package/python/frontend_src/src/components/ToolMenu.vue +144 -0
  258. package/python/frontend_src/src/components/TopBar.vue +90 -0
  259. package/python/frontend_src/src/components/panels/ActivityPanel.vue +84 -0
  260. package/python/frontend_src/src/components/panels/AgentsPanel.vue +86 -0
  261. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +441 -0
  262. package/python/frontend_src/src/components/panels/MemoryPanel.vue +118 -0
  263. package/python/frontend_src/src/components/panels/PanelBox.vue +51 -0
  264. package/python/frontend_src/src/components/panels/ReviewPanel.vue +308 -0
  265. package/python/frontend_src/src/components/panels/SettingsPanel.vue +254 -0
  266. package/python/frontend_src/src/components/panels/TasksPanel.vue +74 -0
  267. package/python/frontend_src/src/components/panels/ToolsPanel.vue +640 -0
  268. package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
  269. package/python/frontend_src/src/lib/fmt.js +48 -0
  270. package/python/frontend_src/src/lib/md.js +204 -0
  271. package/python/frontend_src/src/main.js +7 -0
  272. package/python/frontend_src/src/stores/app.js +106 -0
  273. package/python/frontend_src/src/stores/chat.js +484 -0
  274. package/python/frontend_src/src/stores/model.js +104 -0
  275. package/python/frontend_src/src/stores/panels.js +330 -0
  276. package/python/frontend_src/src/styles/base.css +138 -0
  277. package/python/frontend_src/src/styles/panel.css +117 -0
  278. package/python/frontend_src/src/styles/tokens.css +78 -0
  279. package/python/frontend_src/vite.config.js +46 -0
  280. package/python/pyproject.toml +265 -0
  281. package/python/skills/README.md +123 -0
  282. package/python/skills/execution-contract/SKILL.md +30 -0
  283. package/python/skills/general-assistant/SKILL.md +34 -0
  284. package/python/skills/planner/SKILL.md +33 -0
  285. package/python/skills/schedule-itinerary/SKILL.md +35 -0
  286. package/python/skills/skill-composition/SKILL.md +31 -0
  287. package/python/skills/skill-guard/SKILL.md +67 -0
  288. package/python/skills/skill-memory/SKILL.md +50 -0
  289. package/python/skills/skill-router/SKILL.md +35 -0
  290. package/python/tests/conftest.py +76 -0
  291. package/python/tests/run_all.py +263 -0
  292. package/python/tests/test_agent_lifecycle.py +360 -0
  293. package/python/tests/test_ahs_p2.py +414 -0
  294. package/python/tests/test_anthropic_tools.py +206 -0
  295. package/python/tests/test_api.py +87 -0
  296. package/python/tests/test_api_auth.py +83 -0
  297. package/python/tests/test_api_host_guard.py +69 -0
  298. package/python/tests/test_attachments.py +167 -0
  299. package/python/tests/test_audit_chain.py +121 -0
  300. package/python/tests/test_audit_sink.py +136 -0
  301. package/python/tests/test_browser.py +153 -0
  302. package/python/tests/test_browser_request_guard.py +184 -0
  303. package/python/tests/test_capabilities.py +158 -0
  304. package/python/tests/test_channel_signing.py +192 -0
  305. package/python/tests/test_channel_trust.py +109 -0
  306. package/python/tests/test_channels.py +184 -0
  307. package/python/tests/test_cli.py +46 -0
  308. package/python/tests/test_compaction_toolpair.py +119 -0
  309. package/python/tests/test_config_layering.py +65 -0
  310. package/python/tests/test_confirm_nonce.py +197 -0
  311. package/python/tests/test_context_compaction_wiring.py +130 -0
  312. package/python/tests/test_dispatch.py +95 -0
  313. package/python/tests/test_domain_evals.py +403 -0
  314. package/python/tests/test_domain_import.py +78 -0
  315. package/python/tests/test_domain_reasoners.py +148 -0
  316. package/python/tests/test_e2e.py +178 -0
  317. package/python/tests/test_emb_cache.py +107 -0
  318. package/python/tests/test_embedding_singleton.py +89 -0
  319. package/python/tests/test_envfile.py +83 -0
  320. package/python/tests/test_evals_smoke.py +114 -0
  321. package/python/tests/test_event_spine.py +182 -0
  322. package/python/tests/test_evidence_semantic.py +101 -0
  323. package/python/tests/test_evidence_sse.py +97 -0
  324. package/python/tests/test_file_ingest.py +146 -0
  325. package/python/tests/test_finance.py +153 -0
  326. package/python/tests/test_finance_ledger.py +252 -0
  327. package/python/tests/test_finance_persist.py +178 -0
  328. package/python/tests/test_gateway.py +144 -0
  329. package/python/tests/test_governance.py +468 -0
  330. package/python/tests/test_governance_audit.py +359 -0
  331. package/python/tests/test_governance_middleware.py +143 -0
  332. package/python/tests/test_hkc.py +286 -0
  333. package/python/tests/test_hkc_index_build_manager.py +339 -0
  334. package/python/tests/test_hkc_manifest_reuse.py +317 -0
  335. package/python/tests/test_hkc_search_event_sync.py +533 -0
  336. package/python/tests/test_import_path_guard.py +161 -0
  337. package/python/tests/test_injection_guard.py +211 -0
  338. package/python/tests/test_integration.py +141 -0
  339. package/python/tests/test_itinerary.py +103 -0
  340. package/python/tests/test_kee_semantic.py +89 -0
  341. package/python/tests/test_kernel.py +222 -0
  342. package/python/tests/test_learning.py +110 -0
  343. package/python/tests/test_llm_admin.py +160 -0
  344. package/python/tests/test_llm_planner.py +366 -0
  345. package/python/tests/test_markdown_skills.py +73 -0
  346. package/python/tests/test_markdown_skills_openclaw.py +71 -0
  347. package/python/tests/test_mcp_connectors.py +109 -0
  348. package/python/tests/test_mcp_guardian_discovery.py +91 -0
  349. package/python/tests/test_mcp_no_memory.py +43 -0
  350. package/python/tests/test_mcp_policy_gate.py +77 -0
  351. package/python/tests/test_mcp_server.py +95 -0
  352. package/python/tests/test_memory_recall.py +95 -0
  353. package/python/tests/test_metrics.py +66 -0
  354. package/python/tests/test_multi_chain.py +102 -0
  355. package/python/tests/test_multimodal.py +135 -0
  356. package/python/tests/test_negotiation.py +146 -0
  357. package/python/tests/test_orchestration.py +119 -0
  358. package/python/tests/test_orchestration_decoupled.py +109 -0
  359. package/python/tests/test_p3_p5_hardening.py +338 -0
  360. package/python/tests/test_parallel_dispatch.py +215 -0
  361. package/python/tests/test_pipeline.py +533 -0
  362. package/python/tests/test_plan_visible.py +86 -0
  363. package/python/tests/test_planner.py +95 -0
  364. package/python/tests/test_policy_gate.py +109 -0
  365. package/python/tests/test_reasoner.py +185 -0
  366. package/python/tests/test_release_check.py +263 -0
  367. package/python/tests/test_resume_checkpoint.py +156 -0
  368. package/python/tests/test_routing.py +95 -0
  369. package/python/tests/test_scheduler.py +141 -0
  370. package/python/tests/test_search.py +204 -0
  371. package/python/tests/test_secrets.py +159 -0
  372. package/python/tests/test_semantic.py +78 -0
  373. package/python/tests/test_sessions.py +91 -0
  374. package/python/tests/test_settings.py +281 -0
  375. package/python/tests/test_shadow_learning.py +330 -0
  376. package/python/tests/test_shared_write.py +207 -0
  377. package/python/tests/test_skill_taint_gate.py +154 -0
  378. package/python/tests/test_skills.py +133 -0
  379. package/python/tests/test_stats_and_events.py +165 -0
  380. package/python/tests/test_streaming.py +60 -0
  381. package/python/tests/test_supervisor_pipeline.py +177 -0
  382. package/python/tests/test_supervisor_triage.py +108 -0
  383. package/python/tests/test_taint.py +117 -0
  384. package/python/tests/test_task_stream.py +85 -0
  385. package/python/tests/test_thinking_sse.py +67 -0
  386. package/python/tests/test_thinking_stream.py +76 -0
  387. package/python/tests/test_tool_center.py +129 -0
  388. package/python/tests/test_tool_parallel.py +113 -0
  389. package/python/tests/test_tool_params.py +103 -0
  390. package/python/tests/test_tool_use.py +242 -0
  391. package/python/tests/test_toplevel_concurrency.py +117 -0
  392. package/python/tests/test_tracing.py +290 -0
  393. package/python/tests/test_truth_crystallizer.py +127 -0
  394. package/python/tests/test_truth_flow.py +93 -0
  395. package/python/tests/test_url_policy.py +53 -0
  396. package/python/tests/test_verify_evidence.py +225 -0
  397. package/python/tests/test_version_sync.py +71 -0
  398. package/python/tests/test_web_trigger.py +99 -0
  399. package/python/tools/check_faiss_type.py +31 -0
  400. package/python/tools/check_log.ps1 +46 -0
  401. package/python/tools/ivf_status.py +72 -0
  402. package/python/tools/mypy_baseline.txt +5 -0
  403. package/python/tools/mypy_ratchet.py +97 -0
  404. package/python/tools/pack.py +178 -0
  405. package/python/tools/release_check.py +349 -0
  406. package/python/tools/restart_foxclaw.ps1 +74 -0
  407. package/python/tools/shared_write_audit.py +204 -0
  408. package/python/tools/size_baseline.txt +28 -0
  409. package/python/tools/size_check.py +102 -0
  410. package/python/tools/verify_env.bat +2 -0
  411. package/python/tools/verify_env.py +25 -0
  412. package/python/vendor/HMR_LICENSE +37 -0
  413. package/python/vendor/HMR_README.md +65 -0
  414. package/python/vendor/hkc/LICENSE +37 -0
  415. package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
  416. package/python/vendor/hkc/hkc_ace/ace.py +457 -0
  417. package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
  418. package/python/vendor/hkc/hkc_api/__init__.py +0 -0
  419. package/python/vendor/hkc/hkc_api/container.py +416 -0
  420. package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
  421. package/python/vendor/hkc/hkc_api/main.py +157 -0
  422. package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
  423. package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
  424. package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
  425. package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
  426. package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
  427. package/python/vendor/hkc/hkc_api/schemas.py +153 -0
  428. package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
  429. package/python/vendor/hkc/hkc_core/__init__.py +0 -0
  430. package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
  431. package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
  432. package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
  433. package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
  434. package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
  435. package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
  436. package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
  437. package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
  438. package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
  439. package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
  440. package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
  441. package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
  442. package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
  443. package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
  444. package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
  445. package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
  446. package/python/vendor/hkc/hkc_kde/kde.py +298 -0
  447. package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
  448. package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
  449. package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
  450. package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
  451. package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
  452. package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
  453. package/python/vendor/hkc/hkc_kde/models.py +74 -0
  454. package/python/vendor/hkc/hkc_kde/packager.py +329 -0
  455. package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
  456. package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
  457. package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
  458. package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
  459. package/python/vendor/hkc/hkc_kee/kee.py +541 -0
  460. package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
  461. package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
  462. package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
  463. package/python/vendor/hkc/hkc_sdk/base.py +151 -0
  464. package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
  465. package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
  466. package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
  467. package/python/vendor/hkc/hkc_sdk/models.py +171 -0
  468. package/python/vendor/hkc/hkc_search/__init__.py +0 -0
  469. package/python/vendor/hkc/hkc_search/bm25.py +325 -0
  470. package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
  471. package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
  472. package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
  473. package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
  474. package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
  475. package/python/vendor/hmr/__init__.py +0 -0
  476. package/python/vendor/hmr/core/__init__.py +0 -0
  477. package/python/vendor/hmr/core/hmr.py +684 -0
  478. package/python/vendor/hmr/core/models.py +310 -0
  479. package/python/vendor/hmr/engines/__init__.py +0 -0
  480. package/python/vendor/hmr/engines/evolution.py +710 -0
  481. package/python/vendor/hmr/engines/jit_compiler.py +472 -0
  482. package/python/vendor/hmr/engines/lifecycle.py +264 -0
  483. package/python/vendor/hmr/engines/policy.py +625 -0
  484. package/python/vendor/hmr/engines/recall.py +260 -0
  485. package/python/vendor/hmr/engines/runtime_state.py +158 -0
  486. package/python/vendor/hmr/engines/scheduler.py +323 -0
  487. package/python/vendor/hmr/engines/semantic.py +150 -0
  488. package/python/vendor/hmr/engines/temporal.py +261 -0
  489. package/python/vendor/hmr/engines/thought_chain.py +635 -0
  490. package/python/vendor/hmr/graph/__init__.py +0 -0
  491. package/python/vendor/hmr/graph/cwg.py +338 -0
  492. package/python/vendor/hmr/graph/memory_graph.py +530 -0
  493. package/python/vendor/hmr/storage/__init__.py +0 -0
  494. package/python/vendor/hmr/storage/memory_fs.py +300 -0
  495. package/python/vendor/hmr/storage/vector_store.py +520 -0
@@ -0,0 +1,533 @@
1
+ """
2
+ HKC 事件 → 索引 C/U/D 同步测试(阶段 B 提交)。
3
+
4
+ 覆盖范围:
5
+ E1 CREATED → vector.append_ku,vector_size + 1
6
+ E2 CREATED × N → vector_size + N,顺序保持
7
+ E3 UPDATED 内容变(text_hash 变)→ vector.update_ku 重编,size 不变
8
+ E4 UPDATED 内容同(text_hash 同)→ vector.update_ku 跳过 encode,size 不变
9
+ E5 UPDATED 把 Claim 改成 superseded → vector.remove
10
+ E6 DELETED → vector.remove
11
+ E7 sqlite_store.delete 返 True/False(已删/不存在)
12
+ E8 KEE _process_new_claim 状态变化后 publish UPDATED(vector 同步 remove)
13
+ E9 KEE _try_auto_resolve 后 publish UPDATED(loser 从 vector 移除)
14
+ E10 KEE _merge_evidence 后 publish UPDATED(text_hash 同,跳过 encode 但事件仍发)
15
+ E11 KEE manual_resolve 后 publish UPDATED
16
+ E12 routes.delete_source 级联删 publish DELETED(每个被删 KU 都从 vector 移除)
17
+ E13 重复 KU 被 KEE 软删后 CREATED 处理器不再加入索引(回归用户特别要求)
18
+ E14 _sync_ku 五分支(4)active + 不在 vector → append
19
+ E15 _sync_ku 五分支(3)status=superseded + 在 vector → remove
20
+ E16 批量 DELETED 事件(paylod.ku_ids 列表)→ 全部 remove
21
+
22
+ 设计原则(同 P0/P1):
23
+ - 不 mock:真 SQLite + 真 faiss + StubBackend + 真 HKCContainer + 真 KEE。
24
+ - vendor 隔离硬阻断:模块级解析后立即自检 __file__,不是 vendor 就 RuntimeError。
25
+ - 不依赖网络 / 真实模型;全程 tmp_path。
26
+ - 每个测试独立 tmp_path;HKCContainer init/reset 严格 try/finally。
27
+ """
28
+ from __future__ import annotations
29
+
30
+ import sys
31
+ from pathlib import Path
32
+
33
+
34
+ # ── vendor 隔离硬阻断(等价于 test_vendor_path_isolation.py 闸门 2-4) ────────
35
+ from hkc_search.hybrid import HybridSearch
36
+ from hkc_search.vector_search import VectorIndex
37
+ from hkc_search.embedding_backends import StubBackend
38
+ from hkc_api.container import HKCContainer, init_container, reset_container
39
+ from hkc_core.graph.sqlite_store import SQLiteGraphStore
40
+ from hkc_core.models.ku import ClaimKU, ConceptKU, EvidenceKU, ConflictCard
41
+ from hkc_kep.event_bus import EventBus, KEPEvents
42
+
43
+ _VENDOR_ROOT = Path(__file__).resolve().parents[1] / "vendor" / "hkc"
44
+
45
+
46
+ def _assert_vendor_path(obj, name: str) -> None:
47
+ mod = obj.__module__
48
+ f = getattr(sys.modules[mod], "__file__", "") or ""
49
+ assert str(Path(f).resolve()).startswith(str(_VENDOR_ROOT.resolve())), (
50
+ f"vendor isolation failed: {name}.__module__={mod!r} __file__={f!r} "
51
+ f"not under vendor/hkc. Test is running against wrong HKC copy."
52
+ )
53
+
54
+
55
+ for _cls_name, _cls in [
56
+ ("HybridSearch", HybridSearch),
57
+ ("VectorIndex", VectorIndex),
58
+ ("StubBackend", StubBackend),
59
+ ("HKCContainer", HKCContainer),
60
+ ("SQLiteGraphStore", SQLiteGraphStore),
61
+ ("ConceptKU", ConceptKU),
62
+ ("ClaimKU", ClaimKU),
63
+ ("EventBus", EventBus),
64
+ ]:
65
+ _assert_vendor_path(_cls, _cls_name)
66
+
67
+
68
+ # ── E1~E2: CREATED → append ─────────────────────────────────────
69
+
70
+ class TestEventCreatedAppendsToIndex:
71
+ def test_create_single_ku_appends(self, tmp_path):
72
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
73
+ try:
74
+ ku = ConceptKU(ku_id="KU_E1", name="x", summary="x", domain="d")
75
+ c.graph_store.put(ku)
76
+ from hkc_kep.event_bus import KEPEvents
77
+ c.event_bus.publish({
78
+ "event": KEPEvents.KNOWLEDGE_CREATED,
79
+ "source": "test",
80
+ "payload": {"ku_ids": ["KU_E1"]},
81
+ })
82
+ assert c.search.vector.size() == 1
83
+ assert "KU_E1" in c.search.vector._ku_ids
84
+ finally:
85
+ c.close()
86
+ reset_container()
87
+
88
+ def test_create_many_kus_preserves_order(self, tmp_path):
89
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
90
+ try:
91
+ for i in range(5):
92
+ ku = ConceptKU(ku_id=f"KU_E2_{i}", name=f"k{i}", summary="k", domain="d")
93
+ c.graph_store.put(ku)
94
+ from hkc_kep.event_bus import KEPEvents
95
+ c.event_bus.publish({
96
+ "event": KEPEvents.KNOWLEDGE_CREATED,
97
+ "source": "test",
98
+ "payload": {"ku_ids": [f"KU_E2_{i}" for i in range(5)]},
99
+ })
100
+ assert c.search.vector.size() == 5
101
+ assert c.search.vector._ku_ids == [f"KU_E2_{i}" for i in range(5)]
102
+ finally:
103
+ c.close()
104
+ reset_container()
105
+
106
+
107
+ # ── E3~E4: UPDATED → update_ku(hash 变重编 / hash 同跳过) ─────────
108
+
109
+ class TestEventUpdatedEncodesOrSkips:
110
+ def test_updated_changes_text_hash_reencodes(self, tmp_path):
111
+ """内容变(text_hash 变)→ 重编码,vector_size 不变。"""
112
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
113
+ try:
114
+ ku = ConceptKU(ku_id="KU_E3", name="x", summary="x", domain="d")
115
+ c.graph_store.put(ku)
116
+ c.event_bus.publish({
117
+ "event": KEPEvents.KNOWLEDGE_CREATED,
118
+ "source": "test",
119
+ "payload": {"ku_ids": ["KU_E3"]},
120
+ })
121
+ assert c.search.vector.size() == 1
122
+ # 改 summary → text_hash 变
123
+ old_hash = list(c.search.vector._text_hashes) # 备份
124
+ ku_new = ConceptKU(ku_id="KU_E3", name="x", summary="x 的描述升级了", domain="d")
125
+ c.graph_store.put(ku_new)
126
+ c.event_bus.publish({
127
+ "event": KEPEvents.KNOWLEDGE_UPDATED,
128
+ "source": "test",
129
+ "payload": {"ku_ids": ["KU_E3"]},
130
+ })
131
+ assert c.search.vector.size() == 1
132
+ # 5 个 list 长度一致(_ku_ids 不变,text_hashes 已经替换)
133
+ new_hash = c.search.vector._text_hashes[0]
134
+ assert new_hash != old_hash[0], "应换新 hash"
135
+ finally:
136
+ c.close()
137
+ reset_container()
138
+
139
+ def test_updated_same_text_hash_skips_encode(self, tmp_path):
140
+ """内容同(text_hash 同)→ 跳过 encode,但 vector_size 不变。"""
141
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
142
+ try:
143
+ ku = ConceptKU(ku_id="KU_E4", name="x", summary="x", domain="d")
144
+ c.graph_store.put(ku)
145
+ c.event_bus.publish({
146
+ "event": KEPEvents.KNOWLEDGE_CREATED,
147
+ "source": "test",
148
+ "payload": {"ku_ids": ["KU_E4"]},
149
+ })
150
+ old_hash = list(c.search.vector._text_hashes)
151
+ # 再次 put 完全相同内容的 KU(text_hash 必同)
152
+ ku_same = ConceptKU(ku_id="KU_E4", name="x", summary="x", domain="d")
153
+ c.graph_store.put(ku_same)
154
+ c.event_bus.publish({
155
+ "event": KEPEvents.KNOWLEDGE_UPDATED,
156
+ "source": "test",
157
+ "payload": {"ku_ids": ["KU_E4"]},
158
+ })
159
+ # size 仍 1,hash 未变
160
+ assert c.search.vector.size() == 1
161
+ assert c.search.vector._text_hashes[0] == old_hash[0]
162
+ finally:
163
+ c.close()
164
+ reset_container()
165
+
166
+
167
+ # ── E5: status 变 superseded → remove ──────────────────────────
168
+
169
+ class TestEventUpdatedStatusChangeRemoves:
170
+ def test_claim_superseded_removes_from_vector(self, tmp_path):
171
+ """Claim status 改成 superseded → vector 移除(检索不该召回已废弃)。"""
172
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
173
+ try:
174
+ from hkc_core.models.enums import ClaimStatus
175
+ ku = ClaimKU(ku_id="KU_E5", name="claim_x", statement="x is y",
176
+ domain="d", claim_status=ClaimStatus.ACTIVE, confidence=0.8)
177
+ c.graph_store.put(ku)
178
+ c.event_bus.publish({
179
+ "event": KEPEvents.KNOWLEDGE_CREATED,
180
+ "source": "test",
181
+ "payload": {"ku_ids": ["KU_E5"]},
182
+ })
183
+ assert "KU_E5" in c.search.vector._ku_ids
184
+ # 把 claim_status 改 superseded
185
+ ku.claim_status = ClaimStatus.SUPERSEDED
186
+ c.graph_store.put(ku)
187
+ c.event_bus.publish({
188
+ "event": KEPEvents.KNOWLEDGE_UPDATED,
189
+ "source": "test",
190
+ "payload": {"ku_ids": ["KU_E5"]},
191
+ })
192
+ assert "KU_E5" not in c.search.vector._ku_ids
193
+ assert c.search.vector.size() == 0
194
+ finally:
195
+ c.close()
196
+ reset_container()
197
+
198
+
199
+ # ── E6: DELETED → remove ──────────────────────────────────────
200
+
201
+ class TestEventDeletedRemoves:
202
+ def test_deleted_event_removes_from_vector(self, tmp_path):
203
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
204
+ try:
205
+ ku = ConceptKU(ku_id="KU_E6", name="x", summary="x", domain="d")
206
+ c.graph_store.put(ku)
207
+ c.event_bus.publish({
208
+ "event": KEPEvents.KNOWLEDGE_CREATED,
209
+ "source": "test",
210
+ "payload": {"ku_ids": ["KU_E6"]},
211
+ })
212
+ assert c.search.vector.size() == 1
213
+ # 模拟 routes.delete_source 的真实顺序:先 store.delete(返 bool 不发事件)
214
+ # 再 publish DELETED 事件,让 _sync_ku 走分支 1/2 → remove
215
+ assert c.graph_store.delete("KU_E6") is True
216
+ c.event_bus.publish({
217
+ "event": KEPEvents.KNOWLEDGE_DELETED,
218
+ "source": "test",
219
+ "payload": {"ku_id": "KU_E6"},
220
+ })
221
+ assert c.search.vector.size() == 0
222
+ finally:
223
+ c.close()
224
+ reset_container()
225
+
226
+
227
+ # ── E7: sqlite_store.delete 返 bool ─────────────────────────────
228
+
229
+ class TestSQLiteStoreDeleteReturnsBool:
230
+ def test_delete_existing_returns_true(self, tmp_path):
231
+ store = SQLiteGraphStore(str(tmp_path / "hkc.db"))
232
+ try:
233
+ ku = ConceptKU(ku_id="KU_DEL_1", name="x", summary="x", domain="d")
234
+ store.put(ku)
235
+ assert store.delete("KU_DEL_1") is True
236
+ finally:
237
+ store.close()
238
+
239
+ def test_delete_missing_returns_false(self, tmp_path):
240
+ store = SQLiteGraphStore(str(tmp_path / "hkc.db"))
241
+ try:
242
+ assert store.delete("KU_DOES_NOT_EXIST") is False
243
+ finally:
244
+ store.close()
245
+
246
+ def test_double_delete_returns_false_second_time(self, tmp_path):
247
+ """幂等:再删同一个 ku_id 必返 False(已 deleted)。"""
248
+ store = SQLiteGraphStore(str(tmp_path / "hkc.db"))
249
+ try:
250
+ ku = ConceptKU(ku_id="KU_DEL_2", name="x", summary="x", domain="d")
251
+ store.put(ku)
252
+ assert store.delete("KU_DEL_2") is True
253
+ assert store.delete("KU_DEL_2") is False
254
+ finally:
255
+ store.close()
256
+
257
+
258
+ # ── E8~E11: KEE 4 个 put 点 publish UPDATED ────────────────────
259
+
260
+ class TestKEEPublishesUpdated:
261
+ def test_process_new_claim_status_change_publishes_updated(self, tmp_path):
262
+ """E8:_process_new_claim 状态机变化后 publish UPDATED。"""
263
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
264
+ try:
265
+ from hkc_core.models.enums import ClaimStatus
266
+ # 把 KU 直接放到 disputed 状态(不经过 KEE 完整流程)
267
+ ku = ClaimKU(ku_id="KU_E8", name="c", statement="some claim",
268
+ domain="d", claim_status=ClaimStatus.PENDING, confidence=0.1)
269
+ c.graph_store.put(ku)
270
+ # 订阅 UPDATED
271
+ captured: list[dict] = []
272
+ c.event_bus.subscribe(KEPEvents.KNOWLEDGE_UPDATED,
273
+ lambda e: captured.append(e))
274
+ # 直接调 KEE 的 _process_new_claim(注:对短语句、短领域,pengding→rejected)
275
+ c.kee._process_new_claim(ku)
276
+ # 应至少 publish 一次 UPDATED(因为 status 变了)
277
+ assert any(
278
+ e["payload"].get("ku_id") == "KU_E8" or
279
+ "KU_E8" in e["payload"].get("ku_ids", [])
280
+ for e in captured
281
+ ), f"未捕获 KU_E8 的 UPDATED 事件: {captured}"
282
+ finally:
283
+ c.close()
284
+ reset_container()
285
+
286
+ def test_auto_resolve_publishes_updated(self, tmp_path):
287
+ """E9:_try_auto_resolve 裁决后 publish UPDATED(loser → superseded 从 vector 移除)。
288
+ 构造:两个 statement 不同的 Claim(confidence gap=0.5 > 0.20 阈值) + OPEN conflict 卡
289
+ → 调 _try_auto_resolve → 必走 put winner + put loser + publish UPDATED。
290
+ 注:用 _try_auto_resolve 直接调而非 _process_new_claim,避免被 dedup 路径干扰
291
+ (两个 statement 不同时,同领域不会 dedup;但 _process_new_claim 还要走 state
292
+ machine,可能不触发 _merge_evidence → 不发 UPDATED)。"""
293
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
294
+ try:
295
+ from hkc_core.models.enums import ClaimStatus, ConflictStatus, ResolutionStrategy
296
+ a = ClaimKU(ku_id="KU_E9A", name="a",
297
+ statement="the sky is blue today always",
298
+ domain="weather", claim_status=ClaimStatus.ACTIVE,
299
+ confidence=0.95)
300
+ b = ClaimKU(ku_id="KU_E9B", name="b",
301
+ statement="the sky is gray overcast every day",
302
+ domain="weather", claim_status=ClaimStatus.ACTIVE,
303
+ confidence=0.45)
304
+ c.graph_store.put(a)
305
+ c.graph_store.put(b)
306
+ # 造 OPEN conflict 卡
307
+ card = ConflictCard(
308
+ conflict_id="CONF_E9",
309
+ claim_a_id="KU_E9A",
310
+ claim_b_id="KU_E9B",
311
+ domain="weather",
312
+ status=ConflictStatus.OPEN,
313
+ resolution_strategy=ResolutionStrategy.EVIDENCE_WEIGHT,
314
+ )
315
+ c.graph_store.save_conflict(card)
316
+ # 订阅 UPDATED
317
+ captured: list[dict] = []
318
+ c.event_bus.subscribe(KEPEvents.KNOWLEDGE_UPDATED,
319
+ lambda e: captured.append(e))
320
+ # 直接调 _try_auto_resolve —— confidence gap = 0.5 > 0.20 必裁决
321
+ c.kee._try_auto_resolve(card, a, b)
322
+ updated_payloads = [
323
+ e["payload"] for e in captured
324
+ if e.get("event") == KEPEvents.KNOWLEDGE_UPDATED
325
+ ]
326
+ # 必有 1 条 UPDATED(ku_ids 含 winner+loser)
327
+ assert len(updated_payloads) >= 1, f"应至少 1 条 UPDATED: {captured}"
328
+ # 至少一条要同时涉及 a 和 b(both winner and loser in ku_ids)
329
+ assert any(
330
+ set(e.get("ku_ids", [])) >= {"KU_E9A", "KU_E9B"}
331
+ for e in updated_payloads
332
+ ), f"应有 a+b 的 UPDATED: {updated_payloads}"
333
+ finally:
334
+ c.close()
335
+ reset_container()
336
+
337
+ def test_merge_evidence_publishes_updated(self, tmp_path):
338
+ """E10:_merge_evidence 后 publish UPDATED(同 hash,但事件仍发)。"""
339
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
340
+ try:
341
+ from hkc_core.models.enums import ClaimStatus
342
+ a = ClaimKU(ku_id="KU_E10A", name="a", statement="moon is rock",
343
+ domain="astro", claim_status=ClaimStatus.ACTIVE, confidence=0.6)
344
+ b = ClaimKU(ku_id="KU_E10B", name="b", statement="moon is rock",
345
+ domain="astro", claim_status=ClaimStatus.ACTIVE, confidence=0.6)
346
+ a.supports = ["EVD_1"]
347
+ b.supports = ["EVD_2"] # 不同 EVD
348
+ c.graph_store.put(a)
349
+ c.graph_store.put(b)
350
+ c.event_bus.publish({
351
+ "event": KEPEvents.KNOWLEDGE_CREATED, "source": "test",
352
+ "payload": {"ku_ids": ["KU_E10A", "KU_E10B"]},
353
+ })
354
+ # 订阅 UPDATED
355
+ captured: list[dict] = []
356
+ c.event_bus.subscribe(KEPEvents.KNOWLEDGE_UPDATED,
357
+ lambda e: captured.append(e))
358
+ # _merge_evidence(a, b) → b 的 EVD_2 合并入 a → a store.put → publish UPDATED
359
+ c.kee._merge_evidence(b, a)
360
+ assert any(
361
+ e["payload"].get("ku_id") == "KU_E10A" or
362
+ "KU_E10A" in e["payload"].get("ku_ids", [])
363
+ for e in captured if e.get("event") == KEPEvents.KNOWLEDGE_UPDATED
364
+ ), f"_merge_evidence 应 publish UPDATED: {captured}"
365
+ finally:
366
+ c.close()
367
+ reset_container()
368
+
369
+ def test_manual_resolve_publishes_updated(self, tmp_path):
370
+ """E11:manual_resolve 后 publish UPDATED。"""
371
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
372
+ try:
373
+ from hkc_core.models.enums import ClaimStatus, ConflictStatus, ResolutionStrategy
374
+ # 注:enum 实际值是 MANUAL(不是 HUMAN)
375
+ a = ClaimKU(ku_id="KU_E11A", name="a", statement="the sea is calm today",
376
+ domain="sea", claim_status=ClaimStatus.ACTIVE, confidence=0.9)
377
+ b = ClaimKU(ku_id="KU_E11B", name="b", statement="the sea is rough today",
378
+ domain="sea", claim_status=ClaimStatus.ACTIVE, confidence=0.5)
379
+ c.graph_store.put(a)
380
+ c.graph_store.put(b)
381
+ # 造一张 conflict 卡 —— 必须设 resolution_strategy(否则 save_conflict 报 NoneType)
382
+ card = ConflictCard(
383
+ conflict_id="CONF_E11",
384
+ claim_a_id="KU_E11A",
385
+ claim_b_id="KU_E11B",
386
+ domain="sea",
387
+ status=ConflictStatus.OPEN,
388
+ resolution_strategy=ResolutionStrategy.MANUAL,
389
+ )
390
+ c.graph_store.save_conflict(card)
391
+ # 订阅 UPDATED
392
+ captured: list[dict] = []
393
+ c.event_bus.subscribe(KEPEvents.KNOWLEDGE_UPDATED,
394
+ lambda e: captured.append(e))
395
+ # 人工裁决 a 胜出
396
+ ok = c.kee.manual_resolve("CONF_E11", "KU_E11A", note="test")
397
+ assert ok is True
398
+ updated_payloads = [
399
+ e["payload"] for e in captured
400
+ if e.get("event") == KEPEvents.KNOWLEDGE_UPDATED
401
+ ]
402
+ assert any(
403
+ "KU_E11B" in e.get("ku_ids", []) or
404
+ e.get("ku_id") == "KU_E11B"
405
+ for e in updated_payloads
406
+ ), f"manual_resolve 应 publish UPDATED 涉及 KU_E11B: {updated_payloads}"
407
+ finally:
408
+ c.close()
409
+ reset_container()
410
+
411
+
412
+ # ── E12: routes.delete_source 级联删 publish DELETED ─────────────
413
+
414
+ class TestDeleteSourcePublishesDeleted:
415
+ def test_cascade_delete_publishes_deleted(self, tmp_path):
416
+ """routes.delete_source 级联删应发 DELETED,vector 同步 remove。
417
+ _delete_source_cascade 按 EvidenceKU.source 匹配,所以喂 EvidenceKU。"""
418
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
419
+ try:
420
+ evd = EvidenceKU(ku_id="EVD_E12", name="e", source="book1.md",
421
+ quote="一段引用文本占位")
422
+ c.graph_store.put(evd)
423
+ c.event_bus.publish({
424
+ "event": KEPEvents.KNOWLEDGE_CREATED, "source": "test",
425
+ "payload": {"ku_ids": ["EVD_E12"]},
426
+ })
427
+ assert c.search.vector.size() == 1
428
+ # 调级联删 helper(按 book1.md)
429
+ from hkc_api.routes.knowledge import _delete_source_cascade
430
+ _delete_source_cascade(c, "book1.md")
431
+ assert c.search.vector.size() == 0, "vector 应被同步清空"
432
+ finally:
433
+ c.close()
434
+ reset_container()
435
+
436
+
437
+ # ── E13: 重复 KU 被 KEE 软删后 CREATED 处理器不加入索引 ──────────
438
+
439
+ class TestDedupedKUDoesNotEnterIndex:
440
+ def test_deduped_duplicate_ku_skipped_in_index(self, tmp_path):
441
+ """用户原话:重复 KU 被 KEE 软删后,CREATED 处理器不会重新加入索引。
442
+ 这里用 ConceptKU + 相同 name(主身份键)触发 dedup。"""
443
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
444
+ try:
445
+ # 第一个 KU 触发正常 CREATED
446
+ a = ConceptKU(ku_id="KU_E13A", name="重复名", summary="第一个",
447
+ domain="d")
448
+ c.graph_store.put(a)
449
+ c.event_bus.publish({
450
+ "event": KEPEvents.KNOWLEDGE_CREATED, "source": "test",
451
+ "payload": {"ku_ids": ["KU_E13A"]},
452
+ })
453
+ assert c.search.vector.size() == 1
454
+ # 第二个 KU 同名(主身份键冲突)+ 不同 ID
455
+ b = ConceptKU(ku_id="KU_E13B", name="重复名", summary="第二个",
456
+ domain="d")
457
+ c.graph_store.put(b)
458
+ # publish CREATED → KEE._on_knowledge_created 内部 dedup 删 b → _sync_ku 拿不到
459
+ c.event_bus.publish({
460
+ "event": KEPEvents.KNOWLEDGE_CREATED, "source": "test",
461
+ "payload": {"ku_ids": ["KU_E13B"]},
462
+ })
463
+ # vector 不应含 b(被 dedup 软删,CREATED 处理器 _sync_ku 拿不到 KU)
464
+ assert "KU_E13B" not in c.search.vector._ku_ids
465
+ assert c.search.vector.size() == 1
466
+ finally:
467
+ c.close()
468
+ reset_container()
469
+
470
+
471
+ # ── E14~E15: _sync_ku 五分支显式测 ─────────────────────────────
472
+
473
+ class TestSyncKUBranches:
474
+ def test_sync_ku_append_when_not_in_vector(self, tmp_path):
475
+ """E14:五分支 (4) active + 不在 vector → append。"""
476
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
477
+ try:
478
+ ku = ConceptKU(ku_id="KU_E14", name="x", summary="x", domain="d")
479
+ c.graph_store.put(ku)
480
+ # 不 publish 事件,直接调 _sync_ku
481
+ c._sync_ku("KU_E14")
482
+ assert c.search.vector.size() == 1
483
+ assert "KU_E14" in c.search.vector._ku_ids
484
+ finally:
485
+ c.close()
486
+ reset_container()
487
+
488
+ def test_sync_ku_remove_when_superseded(self, tmp_path):
489
+ """E15:五分支 (3) status=superseded + 在 vector → remove。"""
490
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
491
+ try:
492
+ from hkc_core.models.enums import ClaimStatus
493
+ ku = ClaimKU(ku_id="KU_E15", name="c", statement="x",
494
+ domain="d", claim_status=ClaimStatus.ACTIVE, confidence=0.8)
495
+ c.graph_store.put(ku)
496
+ c._sync_ku("KU_E15") # 先入 vector
497
+ assert c.search.vector.size() == 1
498
+ # 改 status → superseded
499
+ ku.claim_status = ClaimStatus.SUPERSEDED
500
+ c.graph_store.put(ku)
501
+ c._sync_ku("KU_E15")
502
+ assert c.search.vector.size() == 0
503
+ finally:
504
+ c.close()
505
+ reset_container()
506
+
507
+
508
+ # ── E16: 批量 DELETED ──────────────────────────────────────────
509
+
510
+ class TestBatchDeletedEvent:
511
+ def test_bulk_delete_event_removes_all(self, tmp_path):
512
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
513
+ try:
514
+ for i in range(5):
515
+ ku = ConceptKU(ku_id=f"KU_E16_{i}", name=f"k{i}",
516
+ summary="k", domain="d")
517
+ c.graph_store.put(ku)
518
+ c.event_bus.publish({
519
+ "event": KEPEvents.KNOWLEDGE_CREATED, "source": "test",
520
+ "payload": {"ku_ids": [f"KU_E16_{i}" for i in range(5)]},
521
+ })
522
+ assert c.search.vector.size() == 5
523
+ # 必须先 store.delete(返 bool 不发事件)再 publish DELETED(让 _sync_ku 走分支 1/2 → remove)
524
+ for i in range(5):
525
+ assert c.graph_store.delete(f"KU_E16_{i}") is True
526
+ c.event_bus.publish({
527
+ "event": KEPEvents.KNOWLEDGE_DELETED, "source": "test",
528
+ "payload": {"ku_ids": [f"KU_E16_{i}" for i in range(5)]},
529
+ })
530
+ assert c.search.vector.size() == 0
531
+ finally:
532
+ c.close()
533
+ reset_container()