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,97 @@
1
+ #!/usr/bin/env python3
2
+ """mypy 棘轮门(mypy ratchet):类型错误数只允许下降,不允许新增。
3
+
4
+ 为什么需要
5
+ ----------
6
+ 此前 CI 用 `continue-on-error: true` 跑 mypy,注释写着「存量 425 个类型错误」,
7
+ 而 2026-11 实测已经是 **673** 个。非阻塞 + 没人核对数字 = 类型债务只涨不跌,
8
+ "渐进式收紧"在事实上从未发生。本脚本把那句注释变成可执行的门禁:
9
+
10
+ 当前错误数 > 基线 → 失败(有人新增了类型错误,或 mypy 升级放大了问题)
11
+ 当前错误数 < 基线 → 通过,并提示可以把基线调低(棘轮只能往一个方向转)
12
+
13
+ 用法
14
+ ----
15
+ python tools/mypy_ratchet.py # CI:校验不新增
16
+ python tools/mypy_ratchet.py --update # 本地:错误数下降后刷新基线
17
+
18
+ `--update` 只在错误数 <= 基线时才写文件,防止把新增债务"洗"进基线。
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import re
23
+ import subprocess
24
+ import sys
25
+ from pathlib import Path
26
+
27
+ ROOT = Path(__file__).resolve().parent.parent
28
+ BASELINE = ROOT / "tools" / "mypy_baseline.txt"
29
+ SUMMARY = re.compile(r"Found (\d+) errors? in (\d+) files?")
30
+
31
+ # Windows GBK 控制台遇到 ✓/✗ 会 UnicodeEncodeError,直接把输出钉成 utf-8。
32
+ for _s in (sys.stdout, sys.stderr):
33
+ try:
34
+ _s.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
35
+ except Exception:
36
+ pass
37
+
38
+
39
+ def read_baseline() -> int:
40
+ if not BASELINE.exists():
41
+ print(f"✗ 缺少基线文件 {BASELINE.relative_to(ROOT)}(内容应为错误数,如 673)")
42
+ sys.exit(2)
43
+ for line in BASELINE.read_text(encoding="utf-8").splitlines():
44
+ line = line.strip()
45
+ if line and not line.startswith("#"):
46
+ return int(line)
47
+ print(f"✗ 基线文件 {BASELINE.relative_to(ROOT)} 里没有数字")
48
+ sys.exit(2)
49
+
50
+
51
+ def run_mypy() -> tuple[int, int]:
52
+ """返回 (错误数, 涉及文件数)。配置取自 pyproject [tool.mypy],此处不重复传参。"""
53
+ r = subprocess.run(
54
+ [sys.executable, "-m", "mypy", "foxclaw"],
55
+ cwd=ROOT, capture_output=True, text=True, encoding="utf-8", errors="replace",
56
+ )
57
+ out = (r.stdout or "") + (r.stderr or "")
58
+ m = SUMMARY.search(out)
59
+ if m:
60
+ return int(m.group(1)), int(m.group(2))
61
+ if "Success: no issues found" in out:
62
+ return 0, 0
63
+ print(out[-4000:])
64
+ print("✗ 无法解析 mypy 输出(格式变了?上面是原始输出)")
65
+ sys.exit(2)
66
+
67
+
68
+ def main() -> int:
69
+ baseline = read_baseline()
70
+ errors, files = run_mypy()
71
+ update = "--update" in sys.argv[1:]
72
+
73
+ print(f"mypy: {errors} errors in {files} files | baseline: {baseline}")
74
+
75
+ if update:
76
+ if errors > baseline:
77
+ print(f"✗ 拒绝更新基线:{errors} > {baseline}。棘轮只允许下调,"
78
+ f"请先修掉新增的 {errors - baseline} 个错误")
79
+ return 1
80
+ BASELINE.write_text(f"{errors}\n", encoding="utf-8")
81
+ print(f"✓ 基线已刷新为 {errors}")
82
+ return 0
83
+
84
+ if errors > baseline:
85
+ print(f"✗ 类型错误增加 {errors - baseline} 个({baseline} → {errors})。"
86
+ f"新增错误必须修掉,不能靠调高基线通过")
87
+ return 1
88
+ if errors < baseline:
89
+ print(f"✓ 通过,且比基线少 {baseline - errors} 个 —— "
90
+ f"可信后执行 `python tools/mypy_ratchet.py --update` 锁定战果")
91
+ return 0
92
+ print("✓ 通过(与基线持平)")
93
+ return 0
94
+
95
+
96
+ if __name__ == "__main__":
97
+ sys.exit(main())
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env python3
2
+ """Foxclaw 打包脚本 —— 用 `git archive` 生成交付归档。
3
+
4
+ 为什么必须走 git archive
5
+ ------------------------
6
+ 直接对工作目录 zip 会**绕过 .gitignore**:只要有人手动压包,写在忽略规则里的
7
+ `foxclaw_data/`(真实会话数据)与 `docs/internal/`(内部机密文档)就会被一起打进去。
8
+ 本项目此前正是这样发生的。`git archive` 只导出「已跟踪且未被忽略」的文件,
9
+ 从流程上杜绝这类泄漏,同时保证归档内容与某个确定的 commit 一一对应、可复现。
10
+
11
+ 产出两种归档(默认都生成)
12
+ ------------------------
13
+ dist/Foxclaw-internal-<版本>-<日期>.zip 完整版,含 docs/internal/,仅限内部/私有分发
14
+ dist/Foxclaw-public-<版本>-<日期>.zip 对外分发版,排除 docs/internal/
15
+
16
+ 两种归档生成后都会做一次**内容校验**:若发现密钥/运行数据/归档嵌套等不该出现的内容,
17
+ 立即删除该归档并以非零码退出(宁可不出包,也不出错包)。
18
+
19
+ 用法
20
+ ----
21
+ python tools/pack.py # internal + public 都生成
22
+ python tools/pack.py --only public # 只生成对外版
23
+ python tools/pack.py --only internal # 只生成内部版
24
+ python tools/pack.py --ref HEAD~1 # 打包指定 commit(默认 HEAD)
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import argparse
30
+ import datetime as _dt
31
+ import re
32
+ import subprocess
33
+ import sys
34
+ import zipfile
35
+ from pathlib import Path
36
+
37
+ ROOT = Path(__file__).resolve().parent.parent
38
+ DIST = ROOT / "dist"
39
+
40
+ # docs/internal/ 只在「内部版」允许出现
41
+ _INTERNAL_DOCS = "docs/internal/"
42
+
43
+
44
+ def read_version() -> str:
45
+ """从 foxclaw/__init__.py 读 __version__(单一来源,避免版本号漂移)。"""
46
+ txt = (ROOT / "foxclaw" / "__init__.py").read_text(encoding="utf-8")
47
+ m = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', txt)
48
+ return m.group(1) if m else "0.0.0"
49
+
50
+
51
+ def _norm(name: str) -> str:
52
+ return name.replace("\\", "/")
53
+
54
+
55
+ def _forbidden(path: str, allow_internal_docs: bool) -> str | None:
56
+ """返回该条目被禁止的原因;允许则返回 None。"""
57
+ p = _norm(path)
58
+ if p.endswith("/"):
59
+ return None
60
+ parts = p.split("/")
61
+ # 归档带 --prefix=Foxclaw-xxx/,故按「任意层级」匹配
62
+ joined = "/".join(parts[1:]) if len(parts) > 1 else p
63
+
64
+ if not allow_internal_docs and _INTERNAL_DOCS in p:
65
+ return "内部机密文档 docs/internal/(对外分发版必须排除)"
66
+ # .env / .env.bak-* / .env.local —— 但 .env.example 模板必须保留
67
+ base = parts[-1]
68
+ if base == ".env" or (base.startswith(".env.") and base != ".env.example"):
69
+ return "密钥配置文件"
70
+ if base.startswith(".env.bak"):
71
+ return "密钥备份文件"
72
+ if "foxclaw_data/" in p or joined.startswith("foxclaw_data/"):
73
+ return "运行时数据(含真实会话/记忆)"
74
+ if re.search(r"\.(zip|rar|7z)$", base, re.I):
75
+ return "嵌套归档(打包产物不应入库/入包)"
76
+ if ".venv/" in p or "venv/" in p:
77
+ return "虚拟环境"
78
+ if "__pycache__/" in p or ".pytest_cache/" in p:
79
+ return "缓存目录"
80
+ if "新建文件夹/" in p:
81
+ return "重复副本目录"
82
+ if ".bak" in base:
83
+ return "备份文件"
84
+ return None
85
+
86
+
87
+ def check_archive(zf: Path, allow_internal_docs: bool) -> list[tuple[str, str]]:
88
+ """校验归档内容,返回 [(条目, 原因)] 违规清单。"""
89
+ bad: list[tuple[str, str]] = []
90
+ with zipfile.ZipFile(zf) as z:
91
+ for name in z.namelist():
92
+ why = _forbidden(name, allow_internal_docs)
93
+ if why:
94
+ bad.append((name, why))
95
+ return bad
96
+
97
+
98
+ def git(args: list[str]) -> subprocess.CompletedProcess:
99
+ return subprocess.run(
100
+ ["git", "-C", str(ROOT), *args],
101
+ capture_output=True, text=True, encoding="utf-8", errors="replace",
102
+ )
103
+
104
+
105
+ def ensure_repo() -> None:
106
+ if git(["rev-parse", "--is-inside-work-tree"]).returncode != 0:
107
+ sys.exit(f"[pack] 错误:{ROOT} 不是 git 仓库。本脚本依赖 git archive,请先 git init。")
108
+
109
+
110
+ def warn_if_dirty() -> None:
111
+ r = git(["status", "--porcelain"])
112
+ if r.stdout.strip():
113
+ print("[pack] ⚠ 工作区有未提交改动 —— 只会打包已提交的内容(HEAD)。"
114
+ "如需包含最新改动,请先 commit。")
115
+
116
+
117
+ def build(variant: str, ref: str) -> Path | None:
118
+ version = read_version()
119
+ stamp = _dt.date.today().strftime("%Y%m%d")
120
+ prefix = f"Foxclaw-{variant}/"
121
+ out = DIST / f"Foxclaw-{variant}-{version}-{stamp}.zip"
122
+
123
+ cmd = ["archive", "--format=zip", f"--prefix={prefix}", "-o", str(out), ref]
124
+ if variant == "public":
125
+ # 用 git pathspec magic 排除内部文档目录
126
+ cmd += ["--", ".", f":(exclude){_INTERNAL_DOCS.rstrip('/')}"]
127
+
128
+ r = git(cmd)
129
+ if r.returncode != 0:
130
+ print(f"[pack] ✗ git archive 失败:\n{r.stderr.strip()}")
131
+ out.unlink(missing_ok=True)
132
+ return None
133
+
134
+ # 内容校验 —— 宁可不出包,也不出错包
135
+ bad = check_archive(out, allow_internal_docs=(variant == "internal"))
136
+ if bad:
137
+ out.unlink(missing_ok=True)
138
+ print(f"[pack] ✗ {variant} 版校验未通过,已删除归档。违规条目:")
139
+ for name, why in bad[:20]:
140
+ print(f" - {name} ({why})")
141
+ if len(bad) > 20:
142
+ print(f" ... 另有 {len(bad) - 20} 条")
143
+ return None
144
+
145
+ with zipfile.ZipFile(out) as z:
146
+ n = len([x for x in z.namelist() if not x.endswith("/")])
147
+ size = out.stat().st_size / 1024 / 1024
148
+ print(f"[pack] ✓ {variant:<8} {out.relative_to(ROOT)} ({n} 文件 / {size:.2f} MB)")
149
+ return out
150
+
151
+
152
+ def main() -> int:
153
+ ap = argparse.ArgumentParser(description="用 git archive 生成 Foxclaw 交付归档")
154
+ ap.add_argument("--only", choices=["public", "internal"], help="只生成指定版本")
155
+ ap.add_argument("--ref", default="HEAD", help="要打包的 git 引用(默认 HEAD)")
156
+ args = ap.parse_args()
157
+
158
+ ensure_repo()
159
+ DIST.mkdir(exist_ok=True)
160
+ warn_if_dirty()
161
+
162
+ variants = [args.only] if args.only else ["internal", "public"]
163
+ ok = True
164
+ for v in variants:
165
+ if build(v, args.ref) is None:
166
+ ok = False
167
+
168
+ if not ok:
169
+ print("[pack] 有归档未通过校验,请检查上面的违规清单。")
170
+ return 1
171
+ print(f"[pack] 完成。产物目录:{DIST.relative_to(ROOT)}/")
172
+ if "internal" in variants:
173
+ print("[pack] 提醒:internal 版含 docs/internal/,请勿对外分发。")
174
+ return 0
175
+
176
+
177
+ if __name__ == "__main__":
178
+ raise SystemExit(main())
@@ -0,0 +1,349 @@
1
+ #!/usr/bin/env python3
2
+ """发布前校验 —— 版本号四处一致 + 构建产物结构合规(本地可跑,CI 同一条命令)。
3
+
4
+ 为什么要在按发布按钮**之前**回答
5
+ ------------------------------
6
+ PyPI / npm 的版本号不可撤回:上传后只能 yank(仍占号、仍可被 pin 装到),不能覆盖
7
+ 重发。所以「四处版本号是否一致」「打出来的包里有没有 .env / 内部文档 / 运行期真实
8
+ 数据」必须发布前回答 —— 等 tag 推上去、CI 跑到一半才发现漏改一处版本,npm 上就已经
9
+ 留了个自己都对不上的版本。
10
+
11
+ 两侧产物性质不同(别把"发布成功"当"装完能用")
12
+ ------------------------------------------
13
+ HKC / HMR 内嵌在 vendor/、运行期按 `Path(__file__)/../../vendor` 找;而 setuptools
14
+ 只收 `foxclaw*`,故 **wheel/sdist 不含 vendor/**:装上是能 import、但知识/记忆
15
+ 子系统降级为 None 的**占位包**(与 npm/index.js 占位同一性质)。本脚本对"缺 vendor"
16
+ 只 warn 不 error —— 既定事实不是事故,但发布说明必须写明。
17
+
18
+ 用法
19
+ ----
20
+ python tools/release_check.py # 只查四处版本一致
21
+ python tools/release_check.py --tag v0.1.0 # 另查 tag 与版本一致
22
+ python tools/release_check.py --dist dist --npm-tgz dist/foxclaw-0.1.0.tgz
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import json
29
+ import re
30
+ import sys
31
+ import tarfile
32
+ import tomllib
33
+ import zipfile
34
+ from dataclasses import dataclass
35
+ from pathlib import Path
36
+
37
+ ROOT = Path(__file__).resolve().parent.parent
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class Issue:
42
+ """一条结果。level=error 导致非零退出;warn 只提示。"""
43
+
44
+ level: str # error | warn
45
+ code: str
46
+ msg: str
47
+
48
+ def __str__(self) -> str:
49
+ # 只用 ASCII 记号:Windows 默认控制台是 GBK,✓/✗/⚠ 会抛 UnicodeEncodeError
50
+ mark = "ERROR" if self.level == "error" else " WARN"
51
+ return f" [{mark}] {self.code}: {self.msg}"
52
+
53
+
54
+ # ── 版本来源(与 tests/test_version_sync.py 同一份事实)────────────
55
+
56
+ def _dunder_version(root: Path) -> str:
57
+ txt = (root / "foxclaw" / "__init__.py").read_text(encoding="utf-8")
58
+ m = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', txt, re.M)
59
+ return m.group(1) if m else ""
60
+
61
+
62
+ def _npm_index_version(root: Path) -> str:
63
+ src = (root / "npm" / "index.js").read_text(encoding="utf-8")
64
+ m = re.search(r'export\s+const\s+version\s*=\s*["\']([^"\']+)["\']', src)
65
+ return m.group(1) if m else ""
66
+
67
+
68
+ def _project(root: Path) -> dict:
69
+ with (root / "pyproject.toml").open("rb") as fh:
70
+ return tomllib.load(fh)["project"]
71
+
72
+
73
+ def _npm_package(root: Path) -> dict:
74
+ return json.loads((root / "npm" / "package.json").read_text(encoding="utf-8"))
75
+
76
+
77
+ def project_name(root: Path = ROOT) -> str:
78
+ try:
79
+ return _project(root).get("name", "")
80
+ except Exception:
81
+ return ""
82
+
83
+
84
+ def collect_versions(root: Path = ROOT) -> dict:
85
+ """四处版本号;读不到留空串,交给 check_versions 报 unreadable。"""
86
+ def safe(fn, *a):
87
+ try:
88
+ return fn(*a) or ""
89
+ except Exception:
90
+ return ""
91
+
92
+ return {
93
+ "pyproject.toml:project.version": safe(lambda: _project(root)["version"]),
94
+ "foxclaw/__init__.py:__version__": safe(_dunder_version, root),
95
+ "npm/package.json:version": safe(lambda: _npm_package(root)["version"]),
96
+ "npm/index.js:export const version": safe(_npm_index_version, root),
97
+ }
98
+
99
+
100
+ def check_versions(root: Path = ROOT) -> list:
101
+ versions = collect_versions(root)
102
+ issues = [Issue("error", "unreadable", f"{src} 里读不到版本号")
103
+ for src, v in versions.items() if not v]
104
+ vals = {k: v for k, v in versions.items() if v}
105
+ if len(set(vals.values())) > 1:
106
+ ref = vals.get("pyproject.toml:project.version") or next(iter(vals.values()))
107
+ for src, v in vals.items():
108
+ if v != ref:
109
+ issues.append(Issue("error", "version_mismatch",
110
+ f"{src} = {v},与 pyproject = {ref} 不一致(发布后不可改)"))
111
+ return issues
112
+
113
+
114
+ def readme_target(root: Path = ROOT) -> str:
115
+ """pyproject [project].readme 指向的正文文件(相对路径);未声明返回空串。"""
116
+ try:
117
+ field = _project(root).get("readme")
118
+ except Exception: # noqa: BLE001
119
+ return ""
120
+ if isinstance(field, str):
121
+ return field
122
+ if isinstance(field, dict):
123
+ return str(field.get("file", "") or "")
124
+ return ""
125
+
126
+
127
+ def check_metadata(root: Path = ROOT) -> list:
128
+ """项目页正文必须存在。
129
+
130
+ 0.1.1 上传后 PyPI 项目页只有一句「该软件包的作者未提供项目描述」——pyproject
131
+ 没声明 `readme`,元数据里就没有 Description。包能装、能 import,所以这条缺陷
132
+ 在发布前的任何检查里都不会暴露,只能显式断言。
133
+ """
134
+ path = readme_target(root)
135
+ if not path:
136
+ return [Issue("error", "no_readme",
137
+ "pyproject [project] 未声明 readme → PyPI 项目页显示「未提供项目描述」")]
138
+ f = root / path
139
+ if not f.is_file():
140
+ return [Issue("error", "readme_missing", f"readme 指向的 {path} 不存在")]
141
+ body = f.read_text(encoding="utf-8", errors="replace").strip()
142
+ if len(body) < 200:
143
+ return [Issue("error", "readme_too_short",
144
+ f"{path} 只有 {len(body)} 字符,撑不起项目页描述")]
145
+ return []
146
+
147
+
148
+ def check_tag(tag: str, version: str) -> list:
149
+ """tag 必须就是本次要发的版本(v 前缀可有可无)。"""
150
+ if not tag:
151
+ return []
152
+ stripped = tag.strip()
153
+ if stripped[:1] in ("v", "V"):
154
+ stripped = stripped[1:]
155
+ if stripped != version:
156
+ return [Issue("error", "tag_mismatch",
157
+ f"tag {tag!r} 指向 {stripped},但代码里是 {version}")]
158
+ return []
159
+
160
+
161
+ # ── 归档内容 ──────────────────────────────────────────────
162
+
163
+ def forbidden_entry(entry: str):
164
+ """归档内条目是否绝对不该出现;返回原因,干净则 None。"""
165
+ p = entry.replace("\\", "/")
166
+ if p.endswith("/"):
167
+ return None
168
+ parts = [x for x in p.split("/") if x]
169
+ if not parts:
170
+ return None
171
+ base = parts[-1]
172
+ if base == ".env" or (base.startswith(".env.") and base != ".env.example"):
173
+ return "密钥配置文件(真实 .env)"
174
+ if "docs/internal/" in p:
175
+ return "内部机密文档 docs/internal/"
176
+ if "foxclaw_data/" in p:
177
+ return "运行期真实数据(会话/记忆)"
178
+ if re.search(r"\.(zip|rar|7z)$", base, re.I):
179
+ return "嵌套归档"
180
+ if "__pycache__/" in p or ".pytest_cache/" in p:
181
+ return "缓存目录"
182
+ if "/.venv/" in p or p.startswith(".venv/") or "/venv/" in p:
183
+ return "虚拟环境"
184
+ if base.endswith(".bak"):
185
+ return "备份文件"
186
+ return None
187
+
188
+
189
+ def archive_entries(path: Path) -> list:
190
+ """列归档内条目名(wheel 是 zip;sdist / npm tgz 是 tar)。"""
191
+ try:
192
+ if path.name.endswith((".whl", ".zip")):
193
+ with zipfile.ZipFile(path) as z:
194
+ return z.namelist()
195
+ if path.name.endswith((".tar.gz", ".tgz")):
196
+ with tarfile.open(path) as t:
197
+ return t.getnames()
198
+ except Exception:
199
+ return []
200
+ return []
201
+
202
+
203
+ def _scan_forbidden(path: Path) -> list:
204
+ out = []
205
+ for e in archive_entries(path):
206
+ why = forbidden_entry(e)
207
+ if why:
208
+ out.append(Issue("error", "forbidden_entry", f"{path.name} 含 {e} ({why})"))
209
+ return out[:10]
210
+
211
+
212
+ def _wheel_metadata(path: Path) -> dict:
213
+ try:
214
+ with zipfile.ZipFile(path) as z:
215
+ for n in z.namelist():
216
+ if n.endswith(".dist-info/METADATA"):
217
+ txt = z.read(n).decode("utf-8", "replace")
218
+ return {k.strip(): v.strip() for k, v in
219
+ (ln.split(": ", 1) for ln in txt.splitlines() if ": " in ln)}
220
+ except Exception:
221
+ pass
222
+ return {}
223
+
224
+
225
+ def _wheel_description(path: Path) -> str:
226
+ """wheel METADATA 里的正文(PEP 566:头部空行之后即 long_description)。
227
+
228
+ 换行符必须按 CRLF 也认:Windows 上构建出来的 METADATA 用 \\r\\n,只按 \\n\\n
229
+ 切会把整份头部当正文(或反过来判成没有正文),两种都会把结论判反。
230
+ """
231
+ try:
232
+ with zipfile.ZipFile(path) as z:
233
+ for n in z.namelist():
234
+ if n.endswith(".dist-info/METADATA"):
235
+ txt = z.read(n).decode("utf-8", "replace").replace("\r\n", "\n")
236
+ parts = txt.split("\n\n", 1)
237
+ return parts[1] if len(parts) > 1 else ""
238
+ except Exception: # noqa: BLE001
239
+ return ""
240
+ return ""
241
+
242
+
243
+ def check_dist(dist: Path, name: str, version: str) -> list:
244
+ """dist/ 里恰好一个 wheel + 一个 sdist,名字/元数据/内容都对得上。"""
245
+ if not dist.is_dir():
246
+ return [Issue("error", "no_dist", f"产物目录不存在:{dist}")]
247
+ wheels = sorted(dist.glob("*.whl"))
248
+ sdists = sorted(dist.glob("*.tar.gz"))
249
+ issues: list = []
250
+ if len(wheels) != 1:
251
+ issues.append(Issue("error", "wheel_count",
252
+ f"{dist} 里 wheel 数 {len(wheels)}(应为 1,旧版产物先清掉)"))
253
+ if len(sdists) != 1:
254
+ issues.append(Issue("error", "sdist_count", f"{dist} 里 sdist 数 {len(sdists)}(应为 1)"))
255
+ if not wheels or not sdists:
256
+ return issues
257
+
258
+ whl, sdist = wheels[0], sdists[0]
259
+ if not whl.name.startswith(f"{name}-{version}-"):
260
+ issues.append(Issue("error", "wheel_name", f"{whl.name} 与 {name}-{version} 不符"))
261
+ if sdist.name != f"{name}-{version}.tar.gz":
262
+ issues.append(Issue("error", "sdist_name", f"{sdist.name} 应为 {name}-{version}.tar.gz"))
263
+
264
+ meta = _wheel_metadata(whl)
265
+ if meta.get("Name") != name or meta.get("Version") != version:
266
+ issues.append(Issue("error", "meta_mismatch",
267
+ f"wheel METADATA 是 {meta.get('Name')}@{meta.get('Version')},"
268
+ f"应为 {name}@{version}"))
269
+ if len(_wheel_description(whl).strip()) < 200:
270
+ issues.append(Issue("error", "no_long_description",
271
+ f"{whl.name} 的 METADATA 没有正文 → PyPI 项目页会是空白"))
272
+
273
+ entries = archive_entries(whl)
274
+ for need in ("foxclaw/__init__.py", "foxclaw/cli.py"):
275
+ if need not in entries:
276
+ issues.append(Issue("error", "missing_entry", f"{whl.name} 缺 {need}(入口会装丢)"))
277
+ if not any(e.endswith("foxclaw/cli.py") for e in archive_entries(sdist)):
278
+ issues.append(Issue("error", "missing_entry", f"{sdist.name} 里没有 foxclaw/cli.py"))
279
+
280
+ issues += _scan_forbidden(whl) + _scan_forbidden(sdist)
281
+ if not any("/vendor/" in e for e in entries):
282
+ issues.append(Issue("warn", "no_vendor",
283
+ f"{whl.name} 不含 vendor/(hkc/hmr)→ 装上是占位包,"
284
+ f"知识/记忆子系统降级;发布说明需写明"))
285
+ return issues
286
+
287
+
288
+ def check_npm_tgz(tgz: Path, name: str, version: str) -> list:
289
+ """npm 包:`npm pack` 的文件名与 package/package.json 都得对。"""
290
+ issues: list = []
291
+ if tgz.name != f"{name}-{version}.tgz":
292
+ issues.append(Issue("error", "npm_name", f"{tgz.name} 应为 {name}-{version}.tgz"))
293
+ if "package/package.json" not in archive_entries(tgz):
294
+ issues.append(Issue("error", "npm_no_pkg", f"{tgz.name} 里没有 package/package.json"))
295
+ return issues
296
+ try:
297
+ with tarfile.open(tgz) as t:
298
+ data = json.loads(t.extractfile("package/package.json").read())
299
+ if data.get("name") != name or data.get("version") != version:
300
+ issues.append(Issue("error", "npm_meta",
301
+ f"package.json 是 {data.get('name')}@{data.get('version')},"
302
+ f"应为 {name}@{version}"))
303
+ except Exception as e: # noqa: BLE001
304
+ issues.append(Issue("error", "npm_meta", f"读 {tgz.name} 的 package.json 失败:{e}"))
305
+ return issues + _scan_forbidden(tgz)
306
+
307
+
308
+ def run(root: Path = ROOT, tag: str = "", dist: Path | None = None,
309
+ npm_tgz: Path | None = None) -> list:
310
+ """跑全部适用检查,返回 issues(不打印、不退出,便于测试复用)。"""
311
+ issues = check_versions(root)
312
+ issues += check_metadata(root)
313
+ name, version = project_name(root), collect_versions(root).get(
314
+ "pyproject.toml:project.version", "")
315
+ if tag:
316
+ issues += check_tag(tag, version)
317
+ if dist is not None:
318
+ issues += check_dist(dist, name, version)
319
+ if npm_tgz is not None:
320
+ issues += check_npm_tgz(npm_tgz, name, version)
321
+ return issues
322
+
323
+
324
+ def main(argv=None) -> int:
325
+ ap = argparse.ArgumentParser(description="发布前校验:版本一致 + 产物合规")
326
+ ap.add_argument("--root", default=str(ROOT), help="仓库根目录")
327
+ ap.add_argument("--tag", default="", help="本次要打的 git tag(如 v0.1.0)")
328
+ ap.add_argument("--dist", default=None, help="构建产物目录(校验 wheel/sdist)")
329
+ ap.add_argument("--npm-tgz", default=None, help="npm pack 产出的 .tgz")
330
+ args = ap.parse_args(argv)
331
+
332
+ issues = run(Path(args.root), args.tag,
333
+ Path(args.dist) if args.dist else None,
334
+ Path(args.npm_tgz) if args.npm_tgz else None)
335
+ errors = [i for i in issues if i.level == "error"]
336
+ warns = [i for i in issues if i.level != "error"]
337
+ if issues:
338
+ print("[release_check] 检查结果:")
339
+ for i in issues:
340
+ print(i)
341
+ if errors:
342
+ print(f"[release_check] FAILED: {len(errors)} 个错误 / {len(warns)} 个提示 —— 不许发布")
343
+ return 1
344
+ print(f"[release_check] PASS({len(warns)} 个提示)")
345
+ return 0
346
+
347
+
348
+ if __name__ == "__main__":
349
+ raise SystemExit(main(sys.argv[1:]))