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,482 @@
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
+ from hkc_search.hybrid import HybridSearch
35
+ from hkc_search.vector_search import VectorIndex
36
+ from hkc_search.embedding_backends import StubBackend
37
+ from hkc_api.container import HKCContainer, init_container, reset_container
38
+ from hkc_core.graph.sqlite_store import SQLiteGraphStore
39
+ from hkc_core.models.ku import ClaimKU, ConceptKU, EvidenceKU, ConflictCard
40
+ from hkc_kep.event_bus import EventBus, KEPEvents
41
+
42
+ _VENDOR_ROOT = Path(__file__).resolve().parents[1] / "vendor" / "hkc"
43
+
44
+
45
+ def _assert_vendor_path(obj, name: str) -> None:
46
+ mod = obj.__module__
47
+ f = getattr(sys.modules[mod], "__file__", "") or ""
48
+ assert str(Path(f).resolve()).startswith(str(_VENDOR_ROOT.resolve())), (
49
+ f"vendor isolation failed: {name}.__module__={mod!r} __file__={f!r} "
50
+ f"not under vendor/hkc. Test is running against wrong HKC copy."
51
+ )
52
+
53
+
54
+ for _cls_name, _cls in [
55
+ ("HybridSearch", HybridSearch),
56
+ ("VectorIndex", VectorIndex),
57
+ ("StubBackend", StubBackend),
58
+ ("HKCContainer", HKCContainer),
59
+ ("SQLiteGraphStore", SQLiteGraphStore),
60
+ ("ConceptKU", ConceptKU),
61
+ ("ClaimKU", ClaimKU),
62
+ ("EventBus", EventBus),
63
+ ]:
64
+ _assert_vendor_path(_cls, _cls_name)
65
+
66
+
67
+ class TestEventCreatedAppendsToIndex:
68
+ def test_create_single_ku_appends(self, tmp_path):
69
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
70
+ try:
71
+ ku = ConceptKU(ku_id="KU_E1", name="x", summary="x", domain="d")
72
+ c.graph_store.put(ku)
73
+ from hkc_kep.event_bus import KEPEvents
74
+ c.event_bus.publish({
75
+ "event": KEPEvents.KNOWLEDGE_CREATED,
76
+ "source": "test",
77
+ "payload": {"ku_ids": ["KU_E1"]},
78
+ })
79
+ assert c.search.vector.size() == 1
80
+ assert "KU_E1" in c.search.vector._ku_ids
81
+ finally:
82
+ c.close()
83
+ reset_container()
84
+
85
+ def test_create_many_kus_preserves_order(self, tmp_path):
86
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
87
+ try:
88
+ for i in range(5):
89
+ ku = ConceptKU(ku_id=f"KU_E2_{i}", name=f"k{i}", summary="k", domain="d")
90
+ c.graph_store.put(ku)
91
+ from hkc_kep.event_bus import KEPEvents
92
+ c.event_bus.publish({
93
+ "event": KEPEvents.KNOWLEDGE_CREATED,
94
+ "source": "test",
95
+ "payload": {"ku_ids": [f"KU_E2_{i}" for i in range(5)]},
96
+ })
97
+ assert c.search.vector.size() == 5
98
+ assert c.search.vector._ku_ids == [f"KU_E2_{i}" for i in range(5)]
99
+ finally:
100
+ c.close()
101
+ reset_container()
102
+
103
+
104
+ class TestEventUpdatedEncodesOrSkips:
105
+ def test_updated_changes_text_hash_reencodes(self, tmp_path):
106
+ """内容变(text_hash 变)→ 重编码,vector_size 不变。"""
107
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
108
+ try:
109
+ ku = ConceptKU(ku_id="KU_E3", name="x", summary="x", domain="d")
110
+ c.graph_store.put(ku)
111
+ c.event_bus.publish({
112
+ "event": KEPEvents.KNOWLEDGE_CREATED,
113
+ "source": "test",
114
+ "payload": {"ku_ids": ["KU_E3"]},
115
+ })
116
+ assert c.search.vector.size() == 1
117
+ old_hash = list(c.search.vector._text_hashes)
118
+ ku_new = ConceptKU(ku_id="KU_E3", name="x", summary="x 的描述升级了", domain="d")
119
+ c.graph_store.put(ku_new)
120
+ c.event_bus.publish({
121
+ "event": KEPEvents.KNOWLEDGE_UPDATED,
122
+ "source": "test",
123
+ "payload": {"ku_ids": ["KU_E3"]},
124
+ })
125
+ assert c.search.vector.size() == 1
126
+ new_hash = c.search.vector._text_hashes[0]
127
+ assert new_hash != old_hash[0], "应换新 hash"
128
+ finally:
129
+ c.close()
130
+ reset_container()
131
+
132
+ def test_updated_same_text_hash_skips_encode(self, tmp_path):
133
+ """内容同(text_hash 同)→ 跳过 encode,但 vector_size 不变。"""
134
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
135
+ try:
136
+ ku = ConceptKU(ku_id="KU_E4", name="x", summary="x", domain="d")
137
+ c.graph_store.put(ku)
138
+ c.event_bus.publish({
139
+ "event": KEPEvents.KNOWLEDGE_CREATED,
140
+ "source": "test",
141
+ "payload": {"ku_ids": ["KU_E4"]},
142
+ })
143
+ old_hash = list(c.search.vector._text_hashes)
144
+ ku_same = ConceptKU(ku_id="KU_E4", name="x", summary="x", domain="d")
145
+ c.graph_store.put(ku_same)
146
+ c.event_bus.publish({
147
+ "event": KEPEvents.KNOWLEDGE_UPDATED,
148
+ "source": "test",
149
+ "payload": {"ku_ids": ["KU_E4"]},
150
+ })
151
+ assert c.search.vector.size() == 1
152
+ assert c.search.vector._text_hashes[0] == old_hash[0]
153
+ finally:
154
+ c.close()
155
+ reset_container()
156
+
157
+
158
+ class TestEventUpdatedStatusChangeRemoves:
159
+ def test_claim_superseded_removes_from_vector(self, tmp_path):
160
+ """Claim status 改成 superseded → vector 移除(检索不该召回已废弃)。"""
161
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
162
+ try:
163
+ from hkc_core.models.enums import ClaimStatus
164
+ ku = ClaimKU(ku_id="KU_E5", name="claim_x", statement="x is y",
165
+ domain="d", claim_status=ClaimStatus.ACTIVE, confidence=0.8)
166
+ c.graph_store.put(ku)
167
+ c.event_bus.publish({
168
+ "event": KEPEvents.KNOWLEDGE_CREATED,
169
+ "source": "test",
170
+ "payload": {"ku_ids": ["KU_E5"]},
171
+ })
172
+ assert "KU_E5" in c.search.vector._ku_ids
173
+ ku.claim_status = ClaimStatus.SUPERSEDED
174
+ c.graph_store.put(ku)
175
+ c.event_bus.publish({
176
+ "event": KEPEvents.KNOWLEDGE_UPDATED,
177
+ "source": "test",
178
+ "payload": {"ku_ids": ["KU_E5"]},
179
+ })
180
+ assert "KU_E5" not in c.search.vector._ku_ids
181
+ assert c.search.vector.size() == 0
182
+ finally:
183
+ c.close()
184
+ reset_container()
185
+
186
+
187
+ class TestEventDeletedRemoves:
188
+ def test_deleted_event_removes_from_vector(self, tmp_path):
189
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
190
+ try:
191
+ ku = ConceptKU(ku_id="KU_E6", name="x", summary="x", domain="d")
192
+ c.graph_store.put(ku)
193
+ c.event_bus.publish({
194
+ "event": KEPEvents.KNOWLEDGE_CREATED,
195
+ "source": "test",
196
+ "payload": {"ku_ids": ["KU_E6"]},
197
+ })
198
+ assert c.search.vector.size() == 1
199
+ assert c.graph_store.delete("KU_E6") is True
200
+ c.event_bus.publish({
201
+ "event": KEPEvents.KNOWLEDGE_DELETED,
202
+ "source": "test",
203
+ "payload": {"ku_id": "KU_E6"},
204
+ })
205
+ assert c.search.vector.size() == 0
206
+ finally:
207
+ c.close()
208
+ reset_container()
209
+
210
+
211
+ class TestSQLiteStoreDeleteReturnsBool:
212
+ def test_delete_existing_returns_true(self, tmp_path):
213
+ store = SQLiteGraphStore(str(tmp_path / "hkc.db"))
214
+ try:
215
+ ku = ConceptKU(ku_id="KU_DEL_1", name="x", summary="x", domain="d")
216
+ store.put(ku)
217
+ assert store.delete("KU_DEL_1") is True
218
+ finally:
219
+ store.close()
220
+
221
+ def test_delete_missing_returns_false(self, tmp_path):
222
+ store = SQLiteGraphStore(str(tmp_path / "hkc.db"))
223
+ try:
224
+ assert store.delete("KU_DOES_NOT_EXIST") is False
225
+ finally:
226
+ store.close()
227
+
228
+ def test_double_delete_returns_false_second_time(self, tmp_path):
229
+ """幂等:再删同一个 ku_id 必返 False(已 deleted)。"""
230
+ store = SQLiteGraphStore(str(tmp_path / "hkc.db"))
231
+ try:
232
+ ku = ConceptKU(ku_id="KU_DEL_2", name="x", summary="x", domain="d")
233
+ store.put(ku)
234
+ assert store.delete("KU_DEL_2") is True
235
+ assert store.delete("KU_DEL_2") is False
236
+ finally:
237
+ store.close()
238
+
239
+
240
+ class TestKEEPublishesUpdated:
241
+ def test_process_new_claim_status_change_publishes_updated(self, tmp_path):
242
+ """E8:_process_new_claim 状态机变化后 publish UPDATED。"""
243
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
244
+ try:
245
+ from hkc_core.models.enums import ClaimStatus
246
+ ku = ClaimKU(ku_id="KU_E8", name="c", statement="some claim",
247
+ domain="d", claim_status=ClaimStatus.PENDING, confidence=0.1)
248
+ c.graph_store.put(ku)
249
+ captured: list[dict] = []
250
+ c.event_bus.subscribe(KEPEvents.KNOWLEDGE_UPDATED,
251
+ lambda e: captured.append(e))
252
+ c.kee._process_new_claim(ku)
253
+ assert any(
254
+ e["payload"].get("ku_id") == "KU_E8" or
255
+ "KU_E8" in e["payload"].get("ku_ids", [])
256
+ for e in captured
257
+ ), f"未捕获 KU_E8 的 UPDATED 事件: {captured}"
258
+ finally:
259
+ c.close()
260
+ reset_container()
261
+
262
+ def test_auto_resolve_publishes_updated(self, tmp_path):
263
+ """E9:_try_auto_resolve 裁决后 publish UPDATED(loser → superseded 从 vector 移除)。
264
+ 构造:两个 statement 不同的 Claim(confidence gap=0.5 > 0.20 阈值) + OPEN conflict 卡
265
+ → 调 _try_auto_resolve → 必走 put winner + put loser + publish UPDATED。
266
+ 注:用 _try_auto_resolve 直接调而非 _process_new_claim,避免被 dedup 路径干扰
267
+ (两个 statement 不同时,同领域不会 dedup;但 _process_new_claim 还要走 state
268
+ machine,可能不触发 _merge_evidence → 不发 UPDATED)。"""
269
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
270
+ try:
271
+ from hkc_core.models.enums import ClaimStatus, ConflictStatus, ResolutionStrategy
272
+ a = ClaimKU(ku_id="KU_E9A", name="a",
273
+ statement="the sky is blue today always",
274
+ domain="weather", claim_status=ClaimStatus.ACTIVE,
275
+ confidence=0.95)
276
+ b = ClaimKU(ku_id="KU_E9B", name="b",
277
+ statement="the sky is gray overcast every day",
278
+ domain="weather", claim_status=ClaimStatus.ACTIVE,
279
+ confidence=0.45)
280
+ c.graph_store.put(a)
281
+ c.graph_store.put(b)
282
+ card = ConflictCard(
283
+ conflict_id="CONF_E9",
284
+ claim_a_id="KU_E9A",
285
+ claim_b_id="KU_E9B",
286
+ domain="weather",
287
+ status=ConflictStatus.OPEN,
288
+ resolution_strategy=ResolutionStrategy.EVIDENCE_WEIGHT,
289
+ )
290
+ c.graph_store.save_conflict(card)
291
+ captured: list[dict] = []
292
+ c.event_bus.subscribe(KEPEvents.KNOWLEDGE_UPDATED,
293
+ lambda e: captured.append(e))
294
+ c.kee._try_auto_resolve(card, a, b)
295
+ updated_payloads = [
296
+ e["payload"] for e in captured
297
+ if e.get("event") == KEPEvents.KNOWLEDGE_UPDATED
298
+ ]
299
+ assert len(updated_payloads) >= 1, f"应至少 1 条 UPDATED: {captured}"
300
+ assert any(
301
+ set(e.get("ku_ids", [])) >= {"KU_E9A", "KU_E9B"}
302
+ for e in updated_payloads
303
+ ), f"应有 a+b 的 UPDATED: {updated_payloads}"
304
+ finally:
305
+ c.close()
306
+ reset_container()
307
+
308
+ def test_merge_evidence_publishes_updated(self, tmp_path):
309
+ """E10:_merge_evidence 后 publish UPDATED(同 hash,但事件仍发)。"""
310
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
311
+ try:
312
+ from hkc_core.models.enums import ClaimStatus
313
+ a = ClaimKU(ku_id="KU_E10A", name="a", statement="moon is rock",
314
+ domain="astro", claim_status=ClaimStatus.ACTIVE, confidence=0.6)
315
+ b = ClaimKU(ku_id="KU_E10B", name="b", statement="moon is rock",
316
+ domain="astro", claim_status=ClaimStatus.ACTIVE, confidence=0.6)
317
+ a.supports = ["EVD_1"]
318
+ b.supports = ["EVD_2"]
319
+ c.graph_store.put(a)
320
+ c.graph_store.put(b)
321
+ c.event_bus.publish({
322
+ "event": KEPEvents.KNOWLEDGE_CREATED, "source": "test",
323
+ "payload": {"ku_ids": ["KU_E10A", "KU_E10B"]},
324
+ })
325
+ captured: list[dict] = []
326
+ c.event_bus.subscribe(KEPEvents.KNOWLEDGE_UPDATED,
327
+ lambda e: captured.append(e))
328
+ c.kee._merge_evidence(b, a)
329
+ assert any(
330
+ e["payload"].get("ku_id") == "KU_E10A" or
331
+ "KU_E10A" in e["payload"].get("ku_ids", [])
332
+ for e in captured if e.get("event") == KEPEvents.KNOWLEDGE_UPDATED
333
+ ), f"_merge_evidence 应 publish UPDATED: {captured}"
334
+ finally:
335
+ c.close()
336
+ reset_container()
337
+
338
+ def test_manual_resolve_publishes_updated(self, tmp_path):
339
+ """E11:manual_resolve 后 publish UPDATED。"""
340
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
341
+ try:
342
+ from hkc_core.models.enums import ClaimStatus, ConflictStatus, ResolutionStrategy
343
+ a = ClaimKU(ku_id="KU_E11A", name="a", statement="the sea is calm today",
344
+ domain="sea", claim_status=ClaimStatus.ACTIVE, confidence=0.9)
345
+ b = ClaimKU(ku_id="KU_E11B", name="b", statement="the sea is rough today",
346
+ domain="sea", claim_status=ClaimStatus.ACTIVE, confidence=0.5)
347
+ c.graph_store.put(a)
348
+ c.graph_store.put(b)
349
+ card = ConflictCard(
350
+ conflict_id="CONF_E11",
351
+ claim_a_id="KU_E11A",
352
+ claim_b_id="KU_E11B",
353
+ domain="sea",
354
+ status=ConflictStatus.OPEN,
355
+ resolution_strategy=ResolutionStrategy.MANUAL,
356
+ )
357
+ c.graph_store.save_conflict(card)
358
+ captured: list[dict] = []
359
+ c.event_bus.subscribe(KEPEvents.KNOWLEDGE_UPDATED,
360
+ lambda e: captured.append(e))
361
+ ok = c.kee.manual_resolve("CONF_E11", "KU_E11A", note="test")
362
+ assert ok is True
363
+ updated_payloads = [
364
+ e["payload"] for e in captured
365
+ if e.get("event") == KEPEvents.KNOWLEDGE_UPDATED
366
+ ]
367
+ assert any(
368
+ "KU_E11B" in e.get("ku_ids", []) or
369
+ e.get("ku_id") == "KU_E11B"
370
+ for e in updated_payloads
371
+ ), f"manual_resolve 应 publish UPDATED 涉及 KU_E11B: {updated_payloads}"
372
+ finally:
373
+ c.close()
374
+ reset_container()
375
+
376
+
377
+ class TestDeleteSourcePublishesDeleted:
378
+ def test_cascade_delete_publishes_deleted(self, tmp_path):
379
+ """routes.delete_source 级联删应发 DELETED,vector 同步 remove。
380
+ _delete_source_cascade 按 EvidenceKU.source 匹配,所以喂 EvidenceKU。"""
381
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
382
+ try:
383
+ evd = EvidenceKU(ku_id="EVD_E12", name="e", source="book1.md",
384
+ quote="一段引用文本占位")
385
+ c.graph_store.put(evd)
386
+ c.event_bus.publish({
387
+ "event": KEPEvents.KNOWLEDGE_CREATED, "source": "test",
388
+ "payload": {"ku_ids": ["EVD_E12"]},
389
+ })
390
+ assert c.search.vector.size() == 1
391
+ from hkc_api.routes.knowledge import _delete_source_cascade
392
+ _delete_source_cascade(c, "book1.md")
393
+ assert c.search.vector.size() == 0, "vector 应被同步清空"
394
+ finally:
395
+ c.close()
396
+ reset_container()
397
+
398
+
399
+ class TestDedupedKUDoesNotEnterIndex:
400
+ def test_deduped_duplicate_ku_skipped_in_index(self, tmp_path):
401
+ """用户原话:重复 KU 被 KEE 软删后,CREATED 处理器不会重新加入索引。
402
+ 这里用 ConceptKU + 相同 name(主身份键)触发 dedup。"""
403
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
404
+ try:
405
+ a = ConceptKU(ku_id="KU_E13A", name="重复名", summary="第一个",
406
+ domain="d")
407
+ c.graph_store.put(a)
408
+ c.event_bus.publish({
409
+ "event": KEPEvents.KNOWLEDGE_CREATED, "source": "test",
410
+ "payload": {"ku_ids": ["KU_E13A"]},
411
+ })
412
+ assert c.search.vector.size() == 1
413
+ b = ConceptKU(ku_id="KU_E13B", name="重复名", summary="第二个",
414
+ domain="d")
415
+ c.graph_store.put(b)
416
+ c.event_bus.publish({
417
+ "event": KEPEvents.KNOWLEDGE_CREATED, "source": "test",
418
+ "payload": {"ku_ids": ["KU_E13B"]},
419
+ })
420
+ assert "KU_E13B" not in c.search.vector._ku_ids
421
+ assert c.search.vector.size() == 1
422
+ finally:
423
+ c.close()
424
+ reset_container()
425
+
426
+
427
+ class TestSyncKUBranches:
428
+ def test_sync_ku_append_when_not_in_vector(self, tmp_path):
429
+ """E14:五分支 (4) active + 不在 vector → append。"""
430
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
431
+ try:
432
+ ku = ConceptKU(ku_id="KU_E14", name="x", summary="x", domain="d")
433
+ c.graph_store.put(ku)
434
+ c._sync_ku("KU_E14")
435
+ assert c.search.vector.size() == 1
436
+ assert "KU_E14" in c.search.vector._ku_ids
437
+ finally:
438
+ c.close()
439
+ reset_container()
440
+
441
+ def test_sync_ku_remove_when_superseded(self, tmp_path):
442
+ """E15:五分支 (3) status=superseded + 在 vector → remove。"""
443
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
444
+ try:
445
+ from hkc_core.models.enums import ClaimStatus
446
+ ku = ClaimKU(ku_id="KU_E15", name="c", statement="x",
447
+ domain="d", claim_status=ClaimStatus.ACTIVE, confidence=0.8)
448
+ c.graph_store.put(ku)
449
+ c._sync_ku("KU_E15")
450
+ assert c.search.vector.size() == 1
451
+ ku.claim_status = ClaimStatus.SUPERSEDED
452
+ c.graph_store.put(ku)
453
+ c._sync_ku("KU_E15")
454
+ assert c.search.vector.size() == 0
455
+ finally:
456
+ c.close()
457
+ reset_container()
458
+
459
+
460
+ class TestBatchDeletedEvent:
461
+ def test_bulk_delete_event_removes_all(self, tmp_path):
462
+ c = init_container(data_dir=str(tmp_path), embedding_kind="stub")
463
+ try:
464
+ for i in range(5):
465
+ ku = ConceptKU(ku_id=f"KU_E16_{i}", name=f"k{i}",
466
+ summary="k", domain="d")
467
+ c.graph_store.put(ku)
468
+ c.event_bus.publish({
469
+ "event": KEPEvents.KNOWLEDGE_CREATED, "source": "test",
470
+ "payload": {"ku_ids": [f"KU_E16_{i}" for i in range(5)]},
471
+ })
472
+ assert c.search.vector.size() == 5
473
+ for i in range(5):
474
+ assert c.graph_store.delete(f"KU_E16_{i}") is True
475
+ c.event_bus.publish({
476
+ "event": KEPEvents.KNOWLEDGE_DELETED, "source": "test",
477
+ "payload": {"ku_ids": [f"KU_E16_{i}" for i in range(5)]},
478
+ })
479
+ assert c.search.vector.size() == 0
480
+ finally:
481
+ c.close()
482
+ reset_container()
@@ -0,0 +1,155 @@
1
+ """导入路径准入 + 技能副作用默认值的回归。
2
+
3
+ 两条都是「默认配置不安全」类问题,不是功能 bug:
4
+ - ``csv_path`` 原样进 ``open()`` → HTTP 面等于开放「读本机任意文件」;
5
+ - ``SkillSpec.side_effect`` 默认 ``read`` → 漏声明的技能等于放行(与工具侧 fail-closed 相反)。
6
+ """
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ sys.path.insert(0, str(Path(__file__).parent.parent))
13
+
14
+
15
+ def _csv(path: Path, text: str = "amount\n-1\n") -> Path:
16
+ path.write_text(text, encoding="utf-8")
17
+ return path
18
+
19
+
20
+ def test_inside_data_dir_ok(tmp_path, monkeypatch):
21
+ from foxclaw.core.paths import resolve_import_path
22
+
23
+ root = tmp_path / "data"
24
+ root.mkdir()
25
+ f = _csv(root / "a.csv")
26
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(root))
27
+
28
+ assert resolve_import_path(str(f)) == f.resolve()
29
+
30
+
31
+ def test_outside_root_rejected(tmp_path, monkeypatch):
32
+ from foxclaw.core.paths import resolve_import_path
33
+
34
+ root = tmp_path / "data"
35
+ root.mkdir()
36
+ secret = _csv(tmp_path / "secret.csv")
37
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(root))
38
+ monkeypatch.delenv("FOXCLAW_IMPORT_ROOTS", raising=False)
39
+
40
+ with pytest.raises(ValueError, match="不在允许的导入目录内"):
41
+ resolve_import_path(str(secret))
42
+
43
+
44
+ def test_sibling_prefix_not_allowed(tmp_path, monkeypatch):
45
+ """`data2/` 不能被 `data/` 的根放行(前缀比较会误判,必须按路径段比)。"""
46
+ from foxclaw.core.paths import resolve_import_path
47
+
48
+ root = tmp_path / "data"
49
+ root.mkdir()
50
+ sibling = tmp_path / "data2"
51
+ sibling.mkdir()
52
+ f = _csv(sibling / "a.csv")
53
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(root))
54
+
55
+ with pytest.raises(ValueError):
56
+ resolve_import_path(str(f))
57
+
58
+
59
+ def test_import_roots_env_opens_extra_dir(tmp_path, monkeypatch):
60
+ from foxclaw.core.paths import resolve_import_path
61
+
62
+ root = tmp_path / "data"
63
+ root.mkdir()
64
+ downloads = tmp_path / "downloads"
65
+ downloads.mkdir()
66
+ f = _csv(downloads / "bank.csv")
67
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(root))
68
+ monkeypatch.setenv("FOXCLAW_IMPORT_ROOTS", f"{downloads}{__import__('os').pathsep}{tmp_path / 'other'}")
69
+
70
+ assert resolve_import_path(str(f)) == f.resolve()
71
+
72
+
73
+ def test_suffix_whitelist(tmp_path, monkeypatch):
74
+ from foxclaw.core.paths import resolve_import_path
75
+
76
+ root = tmp_path / "data"
77
+ root.mkdir()
78
+ f = root / "env.json"
79
+ f.write_text("{}", encoding="utf-8")
80
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(root))
81
+
82
+ with pytest.raises(ValueError, match="不支持的扩展名"):
83
+ resolve_import_path(str(f))
84
+
85
+
86
+ def test_missing_file_rejected(tmp_path, monkeypatch):
87
+ from foxclaw.core.paths import resolve_import_path
88
+
89
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(tmp_path))
90
+ with pytest.raises(ValueError, match="不存在"):
91
+ resolve_import_path(str(tmp_path / "nope.csv"))
92
+
93
+
94
+ def test_endpoint_rejects_outside_path(tmp_path, monkeypatch):
95
+ monkeypatch.setenv("HKC_EMBEDDING", "stub")
96
+ root = tmp_path / "data"
97
+ root.mkdir()
98
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(root))
99
+ monkeypatch.delenv("FOXCLAW_IMPORT_ROOTS", raising=False)
100
+ secret = _csv(tmp_path / "secret.csv")
101
+
102
+ from fastapi.testclient import TestClient
103
+ from foxclaw.api import app
104
+
105
+ with TestClient(app) as c:
106
+ r = c.post("/api/finance/import", json={"csv_path": str(secret)})
107
+ assert r.status_code == 400, r.text
108
+ assert "csv_path 不允许" in r.text
109
+
110
+
111
+ def test_endpoint_accepts_inside_path(tmp_path, monkeypatch):
112
+ monkeypatch.setenv("HKC_EMBEDDING", "stub")
113
+ root = tmp_path / "data"
114
+ root.mkdir()
115
+ monkeypatch.setenv("FOXCLAW_DATA_DIR", str(root))
116
+ f = _csv(root / "ok.csv", "amount,category\n-12,food\n-3,transport\n")
117
+
118
+ from fastapi.testclient import TestClient
119
+ from foxclaw.api import app
120
+
121
+ with TestClient(app) as c:
122
+ r = c.post("/api/finance/import", json={"csv_path": str(f)})
123
+ assert r.status_code == 200, r.text
124
+ assert r.json()["imported"] == 2
125
+
126
+
127
+ def test_skill_default_side_effect_is_write():
128
+ from foxclaw.skills import SkillSpec
129
+
130
+ spec = SkillSpec(id="undeclared", name="未声明", description="")
131
+ assert spec.side_effect == "write"
132
+
133
+
134
+ def test_undeclared_skill_asks_for_confirmation():
135
+ """鸭子类型对象没有 side_effect 属性时,也必须落到「需确认」而不是只读放行。"""
136
+ from types import SimpleNamespace
137
+
138
+ from foxclaw.skills.permission import evaluate
139
+
140
+ skill = SimpleNamespace(id="duck", kind="local", external=False,
141
+ trust_level=lambda: "high")
142
+ decision = evaluate(skill, {})
143
+ assert decision.decision == "ask", decision
144
+ assert decision.stage == 4
145
+
146
+
147
+ def test_declared_read_skill_still_allowed():
148
+ """显式声明 read 的技能不受影响 —— 默认值只惩罚「漏写」。"""
149
+ from types import SimpleNamespace
150
+
151
+ from foxclaw.skills.permission import evaluate
152
+
153
+ skill = SimpleNamespace(id="reader", kind="local", side_effect="read",
154
+ external=False, trust_level=lambda: "high")
155
+ assert evaluate(skill, {}).decision == "allow"