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,178 @@
1
+ """
2
+ bench/bm25_regression.py
3
+ 新 BM25(自实现倒排) vs rank_bm25.BM25Okapi top-10 一致率回归测试。
4
+
5
+ 目标:1万 corpus + 100 query,新实现 top-10 一致率 >= 99%。
6
+ 跑法:python bench/bm25_regression.py
7
+ """
8
+ import os
9
+ import random
10
+ import sys
11
+ import time
12
+ from dataclasses import dataclass
13
+
14
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'vendor'))
15
+
16
+ from hkc.hkc_search.bm25 import BM25Index as NewBM25, _tokenize
17
+
18
+
19
+ @dataclass
20
+ class MockKU:
21
+ ku_id: str
22
+ name: str
23
+ summary: str
24
+ tags: list[str]
25
+
26
+
27
+ TOPICS_EN = [
28
+ "machine learning", "neural network", "deep learning", "data science",
29
+ "natural language", "computer vision", "reinforcement learning",
30
+ "transformer model", "graph neural network", "knowledge graph",
31
+ "vector database", "embedding model", "semantic search", "retrieval augmented",
32
+ "transformer architecture", "attention mechanism", "language model",
33
+ "encoder decoder", "self supervised", "contrastive learning",
34
+ ]
35
+ TOPICS_CN = [
36
+ "机器学习", "神经网络", "深度学习", "数据科学",
37
+ "自然语言处理", "计算机视觉", "强化学习",
38
+ "图神经网络", "知识图谱", "向量数据库",
39
+ "嵌入模型", "语义搜索", "检索增强",
40
+ "注意力机制", "语言模型", "自监督学习",
41
+ ]
42
+ ACTIONS = ["tutorial", "review", "introduction", "advanced", "case study", "comparison"]
43
+ LANGS = ["python", "rust", "cpp", "java", "go", "typescript"]
44
+
45
+
46
+ def gen_corpus(n=10000, seed=42):
47
+ rng = random.Random(seed)
48
+ corpus = []
49
+ for i in range(n):
50
+ k = rng.randint(1, 3)
51
+ topics = [rng.choice(TOPICS_EN) for _ in range(k)]
52
+ if rng.random() < 0.5:
53
+ topics.append(rng.choice(TOPICS_CN))
54
+ action = rng.choice(ACTIONS)
55
+ lang = rng.choice(LANGS)
56
+ name = f"{' '.join(topics)} {action} ({lang}) #{i}"
57
+ summary = f"This is a {action} about {' '.join(topics)} in {lang}. " \
58
+ f"Topics: {', '.join(topics)}."
59
+ tags = topics + [action, lang]
60
+ corpus.append(MockKU(
61
+ ku_id=f"ku_{i:06d}", name=name, summary=summary, tags=tags,
62
+ ))
63
+ return corpus
64
+
65
+
66
+ def build_rank_bm25(corpus):
67
+ try:
68
+ from rank_bm25 import BM25Okapi
69
+ except ImportError:
70
+ return None
71
+ tokenized = [_tokenize(f"{ku.name} {ku.summary} {' '.join(ku.tags)}") for ku in corpus]
72
+ return BM25Okapi(tokenized)
73
+
74
+
75
+ def rank_bm25_search(bm25, query, top_k, corpus):
76
+ if bm25 is None:
77
+ return []
78
+ tokens = _tokenize(query)
79
+ if not tokens:
80
+ return []
81
+ scores = bm25.get_scores(tokens)
82
+ indexed = sorted(enumerate(scores), key=lambda x: x[1], reverse=True)[:top_k]
83
+ return [(corpus[i].ku_id, float(s)) for i, s in indexed if s > 0]
84
+
85
+
86
+ def gen_queries(n=100, seed=99):
87
+ rng = random.Random(seed)
88
+ queries = []
89
+ for _ in range(n):
90
+ k = rng.randint(1, 3)
91
+ terms = [rng.choice(TOPICS_EN) for _ in range(k)]
92
+ if rng.random() < 0.3:
93
+ terms.append(rng.choice(TOPICS_CN))
94
+ if rng.random() < 0.4:
95
+ terms.append(rng.choice(ACTIONS))
96
+ queries.append(" ".join(terms))
97
+ return queries
98
+
99
+
100
+ def top10_overlap(rank_results, new_results):
101
+ rank_ids = [ku_id for ku_id, _ in rank_results[:10]]
102
+ new_ids = {ku_id for ku_id, _ in new_results[:10]}
103
+ if not rank_ids:
104
+ return 1.0
105
+ return sum(1 for x in rank_ids if x in new_ids) / len(rank_ids)
106
+
107
+
108
+ def main():
109
+ print("=" * 60)
110
+ print("BM25 回归测试: 自实现倒排 vs rank_bm25.BM25Okapi")
111
+ print("=" * 60)
112
+
113
+ print("\n[1/4] 生成 10000 mock corpus ...")
114
+ corpus = gen_corpus(10000, seed=42)
115
+ print(f" OK, {len(corpus)} docs")
116
+
117
+ print("\n[2/4] 构建索引 ...")
118
+ t0 = time.time()
119
+ new_idx = NewBM25()
120
+ new_idx.build(corpus)
121
+ t_new = time.time() - t0
122
+ print(f" 新实现 build: {t_new*1000:.1f} ms")
123
+
124
+ rank_bm25 = build_rank_bm25(corpus)
125
+ if rank_bm25 is None:
126
+ print(" rank_bm25 未安装,跳过一致率比对(只测性能)")
127
+
128
+ print("\n[3/4] 100 query 跑搜索 ...")
129
+ queries = gen_queries(100, seed=99)
130
+ new_topk = 0
131
+ rank_topk = 0
132
+ overlap_sum = 0.0
133
+ valid = 0
134
+ t0 = time.time()
135
+ for q in queries:
136
+ new_r = [(r.ku_id, r.score) for r in new_idx.search(q, top_k=10)]
137
+ rank_r = rank_bm25_search(rank_bm25, q, 10, corpus)
138
+ new_topk += len(new_r)
139
+ rank_topk += len(rank_r)
140
+ if rank_r and new_r:
141
+ overlap_sum += top10_overlap(rank_r, new_r)
142
+ valid += 1
143
+ t_search = time.time() - t0
144
+ print(f" 耗时: {t_search*1000:.1f} ms ({(t_search/100)*1000:.2f} ms/query)")
145
+ print(f" 新实现召回: {new_topk}, rank_bm25 召回: {rank_topk}")
146
+
147
+ if rank_bm25 is not None and valid > 0:
148
+ avg = overlap_sum / valid
149
+ print(f"\n[4/4] top-10 一致率(对 rank_bm25 ground truth): {avg*100:.1f}%")
150
+ if avg >= 0.99:
151
+ print(" PASS (>= 99%)")
152
+ else:
153
+ print(" FAIL (< 99%)")
154
+ sys.exit(1)
155
+ else:
156
+ print("\n[4/4] 跳过一致率(无 rank_bm25 对照)")
157
+
158
+ # save/load roundtrip
159
+ print("\n[bonus] save/load roundtrip ...")
160
+ tmp = "_tmp_bm25.pkl"
161
+ new_idx.save(tmp)
162
+ new_idx2 = NewBM25()
163
+ assert new_idx2.load(tmp), "load 失败"
164
+ assert new_idx2.size() == new_idx.size()
165
+ for q in queries[:5]:
166
+ r1 = sorted([(r.ku_id, r.score) for r in new_idx.search(q, 10)])
167
+ r2 = sorted([(r.ku_id, r.score) for r in new_idx2.search(q, 10)])
168
+ assert r1 == r2, f"save/load 不一致: {q}"
169
+ os.remove(tmp)
170
+ print(f" OK, roundtrip 一致 ({new_idx.size()} docs)")
171
+
172
+ print("\n" + "=" * 60)
173
+ print("全部通过")
174
+ print("=" * 60)
175
+
176
+
177
+ if __name__ == "__main__":
178
+ main()
@@ -0,0 +1,159 @@
1
+ """
2
+ bench/bm25_stress.py
3
+ 5万/10万 corpus 压测 + remove_ku 增量测试。
4
+ """
5
+ import os, sys, random, time
6
+ from dataclasses import dataclass
7
+
8
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'vendor'))
9
+ from hkc.hkc_search.bm25 import BM25Index as NewBM25, _tokenize
10
+
11
+
12
+ @dataclass
13
+ class MockKU:
14
+ ku_id: str
15
+ name: str
16
+ summary: str
17
+ tags: list[str]
18
+
19
+
20
+ TOPICS_EN = ["machine learning","neural network","deep learning","data science",
21
+ "natural language","computer vision","reinforcement learning",
22
+ "transformer model","graph neural network","knowledge graph",
23
+ "vector database","embedding model","semantic search","retrieval augmented",
24
+ "transformer architecture","attention mechanism","language model",
25
+ "encoder decoder","self supervised","contrastive learning"] * 5
26
+ ACTIONS = ["tutorial","review","introduction","advanced","case study","comparison"] * 10
27
+ LANGS = ["python","rust","cpp","java","go","typescript"] * 10
28
+ TOPICS_CN = ["机器学习","神经网络","深度学习","数据科学","自然语言处理",
29
+ "计算机视觉","强化学习","图神经网络","知识图谱","向量数据库",
30
+ "嵌入模型","语义搜索","检索增强","注意力机制","语言模型","自监督学习"] * 5
31
+
32
+
33
+ def gen_corpus(n, seed=42):
34
+ rng = random.Random(seed)
35
+ corpus = []
36
+ for i in range(n):
37
+ k = rng.randint(1, 3)
38
+ topics = [rng.choice(TOPICS_EN) for _ in range(k)]
39
+ if rng.random() < 0.5:
40
+ topics.append(rng.choice(TOPICS_CN))
41
+ action = rng.choice(ACTIONS)
42
+ lang = rng.choice(LANGS)
43
+ name = f"{' '.join(topics)} {action} ({lang}) #{i}"
44
+ summary = f"This is a {action} about {' '.join(topics)} in {lang}. " \
45
+ f"Topics: {', '.join(topics)}."
46
+ tags = topics + [action, lang]
47
+ corpus.append(MockKU(ku_id=f"ku_{i:08d}", name=name, summary=summary, tags=tags))
48
+ return corpus
49
+
50
+
51
+ def gen_queries(n, seed=99):
52
+ rng = random.Random(seed)
53
+ queries = []
54
+ for _ in range(n):
55
+ k = rng.randint(1, 3)
56
+ terms = [rng.choice(TOPICS_EN) for _ in range(k)]
57
+ if rng.random() < 0.3:
58
+ terms.append(rng.choice(TOPICS_CN))
59
+ if rng.random() < 0.4:
60
+ terms.append(rng.choice(ACTIONS))
61
+ queries.append(" ".join(terms))
62
+ return queries
63
+
64
+
65
+ def bench_build(n):
66
+ print(f"\n[build] {n} corpus ...")
67
+ corpus = gen_corpus(n)
68
+ t0 = time.time()
69
+ idx = NewBM25()
70
+ idx.build(corpus)
71
+ dt = time.time() - t0
72
+ print(f" build: {dt*1000:.0f} ms ({dt*1000/n:.3f} ms/doc)")
73
+ return idx, corpus
74
+
75
+
76
+ def bench_search(idx, queries):
77
+ t0 = time.time()
78
+ total_hits = 0
79
+ for q in queries:
80
+ r = idx.search(q, top_k=10)
81
+ total_hits += len(r)
82
+ dt = time.time() - t0
83
+ print(f" search {len(queries)} query: {dt*1000:.0f} ms ({dt*1000/len(queries):.2f} ms/query, {total_hits} hits)")
84
+ return dt
85
+
86
+
87
+ def bench_remove(idx, n_remove=1000):
88
+ print(f"\n[remove] 删 {n_remove} 条 (random) ...")
89
+ import random
90
+ rng = random.Random(123)
91
+ to_remove = rng.sample(idx._doc_ids, n_remove) # noqa
92
+ t0 = time.time()
93
+ for ku_id in to_remove:
94
+ idx.remove_ku(ku_id)
95
+ dt = time.time() - t0
96
+ print(f" remove: {dt*1000:.0f} ms ({dt*1000/n_remove:.3f} ms/op)")
97
+ print(f" size after: {idx.size()}")
98
+
99
+
100
+ def bench_save_load(idx, path):
101
+ print(f"\n[save/load] {path} ...")
102
+ t0 = time.time()
103
+ idx.save(path)
104
+ t_save = time.time() - t0
105
+ sz = os.path.getsize(path)
106
+ t0 = time.time()
107
+ idx2 = NewBM25()
108
+ ok = idx2.load(path)
109
+ t_load = time.time() - t0
110
+ print(f" save : {t_save*1000:.0f} ms ({sz/1024/1024:.1f} MB)")
111
+ print(f" load : {t_load*1000:.0f} ms (ok={ok}, size={idx2.size()})")
112
+
113
+
114
+ def main():
115
+ print("=" * 60)
116
+ print("BM25 压测: 5万/10万 corpus build + search + remove + save/load")
117
+ print("=" * 60)
118
+
119
+ # 5万
120
+ idx, corpus = bench_build(50_000)
121
+ queries = gen_queries(200)
122
+ bench_search(idx, queries)
123
+ bench_remove(idx, 1000)
124
+ tmp = "_tmp_bm25_50k.pkl"
125
+ bench_save_load(idx, tmp)
126
+ os.remove(tmp)
127
+
128
+ # 10万 (增量:在 5万 上 append 5万,模拟冷构建期分批)
129
+ print(f"\n[build] +5万 增量 (5万 -> 10万) ...")
130
+ extra_corpus = gen_corpus(50_000, seed=43)
131
+ # 取后 5万 (id 范围 0-49999 是 5万 corpus;这里 gen 同一 seed 重叠,改 seed)
132
+ # 实际:重新 gen 5万不同 id
133
+ extra_corpus = []
134
+ rng = random.Random(43)
135
+ for i in range(50_000, 100_000):
136
+ k = rng.randint(1, 3)
137
+ topics = [rng.choice(TOPICS_EN) for _ in range(k)]
138
+ if rng.random() < 0.5:
139
+ topics.append(rng.choice(TOPICS_CN))
140
+ action = rng.choice(ACTIONS)
141
+ lang = rng.choice(LANGS)
142
+ name = f"{' '.join(topics)} {action} ({lang}) #{i}"
143
+ summary = f"This is a {action} about {' '.join(topics)} in {lang}. Topics: {', '.join(topics)}."
144
+ tags = topics + [action, lang]
145
+ extra_corpus.append(MockKU(ku_id=f"ku_{i:08d}", name=name, summary=summary, tags=tags))
146
+ t0 = time.time()
147
+ idx.append_batch(extra_corpus)
148
+ dt = time.time() - t0
149
+ print(f" append 5万 增量: {dt*1000:.0f} ms ({dt*1000/50_000:.3f} ms/doc)")
150
+ print(f" size after: {idx.size()}")
151
+ bench_search(idx, queries)
152
+
153
+ print("\n" + "=" * 60)
154
+ print("压测完成")
155
+ print("=" * 60)
156
+
157
+
158
+ if __name__ == "__main__":
159
+ main()
@@ -0,0 +1,238 @@
1
+ """
2
+ bench/cd_audit.py
3
+ C+D 阶段 5 项 Risk 审核(commit 0bf4349):
4
+ D1. IngestJobRegistry(max_workers=4) 真起 4 thread 并发
5
+ D2. Extractor 16 chunk × 4 worker + 50% 429 重试 (限流雪崩测试)
6
+ D3. progress_cb 线程安全:done 计数无重复无漏
7
+ D4. _retry_llm max_retries 可调 (验证配置生效)
8
+ D5. retry 不破坏 progress_cb 顺序
9
+ """
10
+ import os, sys, time, threading
11
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'vendor'))
12
+
13
+ from hkc.hkc_kde.extractor import Extractor
14
+ from hkc.hkc_api.ingest_jobs import IngestJobRegistry
15
+
16
+
17
+ # ── D1: IngestJobRegistry 4 thread 并发 ────────────────
18
+
19
+ def d1_ingest_4_workers():
20
+ """D1. 4 worker 真并发 peak=4,4 任务 ~0.3s 完成"""
21
+ reg = IngestJobRegistry(max_workers=4)
22
+ running = [0]
23
+ peak = [0]
24
+ lock = threading.Lock()
25
+
26
+ def work(progress):
27
+ with lock:
28
+ running[0] += 1
29
+ peak[0] = max(peak[0], running[0])
30
+ time.sleep(0.3)
31
+ with lock:
32
+ running[0] -= 1
33
+ return []
34
+
35
+ jobs = [reg.create(f"f{i}") for i in range(4)]
36
+ t0 = time.time()
37
+ for j in jobs:
38
+ reg.submit(j, work)
39
+ # wait done
40
+ while True:
41
+ statuses = [reg.get(j.job_id)["status"] for j in jobs]
42
+ if all(s == "done" for s in statuses):
43
+ break
44
+ time.sleep(0.02)
45
+ elapsed = time.time() - t0
46
+ reg.shutdown()
47
+ assert peak[0] == 4, f"expected peak=4, got {peak[0]}"
48
+ assert elapsed < 0.5, f"expected < 0.5s (并行), got {elapsed:.2f}s"
49
+ print(f" D1 PASS: peak={peak[0]}, 4 jobs in {elapsed*1000:.0f}ms (串行需 1.2s)")
50
+
51
+
52
+ # ── D2: Extractor 16 chunk 限流雪崩 ────────────────────
53
+
54
+ def d2_429_snowball():
55
+ """D2. 16 chunk × 4 worker × 100% 429 永不成功,验证不死循环 + 总调用有上界"""
56
+ state = {"calls": 0}
57
+
58
+ def always_429(*args, **kwargs):
59
+ state["calls"] += 1
60
+ err = Exception("rate limited")
61
+ err.status_code = 429
62
+ raise err
63
+
64
+ ext = Extractor(provider="anthropic", model="m")
65
+ # 替换 _extract_chunk:走 _retry_llm 但传极小 base_delay 测试用
66
+ # 同时 monkey-patch time.sleep 避免真睡
67
+ import hkc.hkc_kde.extractor as ex_mod
68
+ orig_sleep = ex_mod.time.sleep
69
+ sleeps = []
70
+ ex_mod.time.sleep = lambda s: sleeps.append(s)
71
+ try:
72
+ def mock_chunk(chunk, timeout=60.0):
73
+ try:
74
+ return ext._retry_llm(always_429, base_delay=0.001, max_delay=0.01)
75
+ except Exception:
76
+ return None
77
+ ext._extract_chunk = mock_chunk
78
+
79
+ class MockChunk:
80
+ def __init__(self, i):
81
+ self.chunk_id = f"c{i}"
82
+ self.doc_id = "d0"
83
+ self.content = "x"
84
+ self.source_hint = ""
85
+
86
+ chunks = [MockChunk(i) for i in range(16)]
87
+ t0 = time.time()
88
+ result = ext.extract(chunks)
89
+ elapsed = time.time() - t0
90
+
91
+ # 期望:16 chunk 全 fail,总调用 = 16 * (1+3 retry) = 64,无死循环
92
+ success = sum(1 for item in result.items if item is not None)
93
+ assert success == 0, f"expected 0 success, got {success}"
94
+ assert state["calls"] == 64, f"snowball: expected 64 calls, got {state['calls']}"
95
+ assert len(sleeps) == 48, f"expected 48 sleeps (16 chunk * 3 retry), got {len(sleeps)}"
96
+ assert elapsed < 3.0, f"deadlock: {elapsed:.1f}s"
97
+ print(f" D2 PASS: 16 chunk x 4 worker x 100% 429 -> {state['calls']} calls, {len(sleeps)} sleeps, {elapsed*1000:.0f}ms (no deadlock)")
98
+ finally:
99
+ ex_mod.time.sleep = orig_sleep
100
+
101
+
102
+ # ── D3: progress_cb 线程安全 ─────────────────────────
103
+
104
+ def d3_progress_cb_thread_safe():
105
+ """D3. progress_cb 在 as_completed 循环调,主线程串行;16 chunk 计数无重复无漏"""
106
+ ext = Extractor(provider="anthropic", model="m")
107
+
108
+ # 替换 _extract_chunk 为 fast mock:50% 概率 fail(模拟 chunk 失败)
109
+ import random
110
+ random.seed(42)
111
+ def mock_extract(chunk, timeout=60.0):
112
+ # 永远返 RawExtraction
113
+ from hkc.hkc_kde.models import RawExtraction
114
+ return RawExtraction()
115
+ ext._extract_chunk = mock_extract
116
+
117
+ class MockChunk:
118
+ def __init__(self, i):
119
+ self.chunk_id = f"c{i}"
120
+ self.doc_id = "d0"
121
+ self.content = "x"
122
+ self.source_hint = ""
123
+
124
+ done_calls = []
125
+ lock = threading.Lock()
126
+
127
+ def cb(processed, total):
128
+ with lock:
129
+ done_calls.append(processed)
130
+
131
+ chunks = [MockChunk(i) for i in range(16)]
132
+ result = ext.extract(chunks, progress_cb=cb)
133
+
134
+ # 验证:done_calls 长度 16(每个 chunk 一次)
135
+ assert len(done_calls) == 16, f"expected 16 callbacks, got {len(done_calls)}"
136
+ # 验证:1..16 各一次(set 大小 = 16,排序后 1..16)
137
+ assert sorted(done_calls) == list(range(1, 17)), \
138
+ f"expected processed 1..16, got {sorted(done_calls)}"
139
+ print(f" D3 PASS: {len(done_calls)} callbacks, all 1..16 unique")
140
+
141
+
142
+ # ── D4: max_retries 可调 ──────────────────────────────
143
+
144
+ def d4_max_retries_configurable():
145
+ """D4. _retry_llm 的 max_retries 真的生效"""
146
+ ext = Extractor(provider="mock", model="m")
147
+
148
+ # 测 max_retries=2(默认 3):4 次后 raise
149
+ call_count = [0]
150
+ def always_fail():
151
+ call_count[0] += 1
152
+ err = Exception("rate")
153
+ err.status_code = 429
154
+ raise err
155
+
156
+ # 用 monkey-patch 把 time.sleep 替换掉,这样不会真睡
157
+ import hkc.hkc_kde.extractor as ex_mod
158
+ orig_sleep = ex_mod.time.sleep
159
+ sleeps = []
160
+ ex_mod.time.sleep = lambda s: sleeps.append(s)
161
+ try:
162
+ try:
163
+ ext._retry_llm(always_fail, max_retries=2, base_delay=0.001)
164
+ assert False, "expected raise"
165
+ except Exception:
166
+ pass
167
+ # max_retries=2 + 1 = 3 次尝试
168
+ assert call_count[0] == 3, f"max_retries=2 expected 3 calls, got {call_count[0]}"
169
+ assert len(sleeps) == 2, f"max_retries=2 expected 2 sleeps, got {len(sleeps)}"
170
+ print(f" D4 PASS: max_retries=2 -> 3 calls, 2 sleeps")
171
+ finally:
172
+ ex_mod.time.sleep = orig_sleep
173
+
174
+
175
+ # ── D5: retry + progress_cb 顺序 ─────────────────────
176
+
177
+ def d5_retry_no_progress_leak():
178
+ """D5. retry 期间 progress_cb 仍只调 1 次/chunk(不管 retry 几次)"""
179
+ import time as time_mod
180
+ orig_sleep = time_mod.sleep
181
+ time_mod.sleep = lambda s: None
182
+
183
+ state = {"calls": 0}
184
+ def always_429(*args, **kwargs):
185
+ state["calls"] += 1
186
+ err = Exception("rate"); err.status_code = 429
187
+ raise err
188
+
189
+ class MockResp:
190
+ def __init__(self, t): self.content = [MockText(t)]
191
+ class MockText:
192
+ def __init__(self, t): self.text = t
193
+
194
+ class MockClient:
195
+ @property
196
+ def messages(self): return self
197
+ def create(self, *args, **kwargs): return always_429(*args, **kwargs)
198
+
199
+ ext = Extractor(provider="anthropic", model="m")
200
+ ext._client = MockClient()
201
+ try:
202
+ class MockChunk:
203
+ def __init__(self, i):
204
+ self.chunk_id=f"c{i}"; self.doc_id="d0"; self.content="x"; self.source_hint=""
205
+ cb_count = [0]
206
+ cb_lock = threading.Lock()
207
+ def cb(p, t):
208
+ with cb_lock:
209
+ cb_count[0] += 1
210
+ chunks = [MockChunk(i) for i in range(8)]
211
+ result = ext.extract(chunks, progress_cb=cb)
212
+ # 不管 retry 几次,每个 chunk 调 1 次 progress_cb
213
+ assert cb_count[0] == 8, f"expected 8 progress, got {cb_count[0]}"
214
+ # 8 chunk 全 fail,总调用 = 8 * 4 = 32
215
+ assert state["calls"] == 32, f"expected 32 calls, got {state['calls']}"
216
+ success = sum(1 for item in result.items if item is not None)
217
+ assert success == 0
218
+ print(f" D5 PASS: 8 chunks x 4 attempts, progress_cb={cb_count[0]} (1 per chunk), calls={state['calls']}")
219
+ finally:
220
+ time_mod.sleep = orig_sleep
221
+
222
+
223
+ def main():
224
+ print("=" * 60)
225
+ print("C+D 阶段 5 项 Risk 审核 (commit 0bf4349)")
226
+ print("=" * 60)
227
+ d1_ingest_4_workers()
228
+ d2_429_snowball()
229
+ d3_progress_cb_thread_safe()
230
+ d4_max_retries_configurable()
231
+ d5_retry_no_progress_leak()
232
+ print("=" * 60)
233
+ print("5/5 PASS")
234
+ print("=" * 60)
235
+
236
+
237
+ if __name__ == "__main__":
238
+ main()