foxclaw 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (514) hide show
  1. package/README.md +54 -29
  2. package/bin/foxclaw.cjs +51 -0
  3. package/index.js +4 -10
  4. package/package.json +46 -38
  5. package/python/.dockerignore +35 -0
  6. package/python/.env.example +173 -0
  7. package/python/.gitattributes +46 -0
  8. package/python/.github/workflows/ci.yml +167 -0
  9. package/python/.github/workflows/release.yml +183 -0
  10. package/python/CHANGELOG.md +57 -0
  11. package/python/CHANGELOG_CN.md +57 -0
  12. package/python/Dockerfile +64 -0
  13. package/python/LICENSE +21 -0
  14. package/python/MANIFEST.in +14 -0
  15. package/python/README.md +201 -0
  16. package/python/README_CN.md +336 -0
  17. package/python/bench/bm25_audit.py +186 -0
  18. package/python/bench/bm25_regression.py +177 -0
  19. package/python/bench/bm25_stress.py +155 -0
  20. package/python/bench/cd_audit.py +215 -0
  21. package/python/bench/chaos.py +140 -0
  22. package/python/bench/concurrency.py +103 -0
  23. package/python/bench/e2e_real_models.py +94 -0
  24. package/python/bench/llm_retry_test.py +241 -0
  25. package/python/bench/load_test.py +193 -0
  26. package/python/bench/vector_ivf_upgrade.py +128 -0
  27. package/python/docs/CHANNELS.md +195 -0
  28. package/python/docs/GOVERNANCE.md +164 -0
  29. package/python/docs/RELEASE.md +110 -0
  30. package/python/docs/RUN.md +75 -0
  31. package/python/docs/RUN_CN.md +121 -0
  32. package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
  33. package/python/evals/__init__.py +0 -0
  34. package/python/evals/governance/README.md +61 -0
  35. package/python/evals/governance/__init__.py +0 -0
  36. package/python/evals/governance/cases.py +115 -0
  37. package/python/evals/governance/compare_embeddings.py +94 -0
  38. package/python/evals/governance/convergence.svg +11 -0
  39. package/python/evals/governance/report.json +371 -0
  40. package/python/evals/governance/run_evals.py +368 -0
  41. package/python/examples/governance_middleware_demo.py +72 -0
  42. package/python/examples/langgraph_governance_demo.py +104 -0
  43. package/python/examples/sample_itinerary.csv +6 -0
  44. package/python/examples/sample_knowledge.csv +9 -0
  45. package/python/examples/sample_transactions.csv +14 -0
  46. package/python/foxclaw/__init__.py +408 -0
  47. package/python/foxclaw/agents/__init__.py +12 -0
  48. package/python/foxclaw/agents/agents.py +35 -0
  49. package/python/foxclaw/agents/base.py +794 -0
  50. package/python/foxclaw/agents/impl/__init__.py +1 -0
  51. package/python/foxclaw/agents/impl/_helpers.py +70 -0
  52. package/python/foxclaw/agents/impl/finance.py +231 -0
  53. package/python/foxclaw/agents/impl/office.py +350 -0
  54. package/python/foxclaw/agents/impl/supervisor.py +592 -0
  55. package/python/foxclaw/agents/mixins/_context.py +128 -0
  56. package/python/foxclaw/agents/mixins/augment.py +143 -0
  57. package/python/foxclaw/agents/mixins/llm.py +67 -0
  58. package/python/foxclaw/agents/mixins/memory.py +41 -0
  59. package/python/foxclaw/agents/mixins/preference.py +131 -0
  60. package/python/foxclaw/agents/mixins/prompt.py +71 -0
  61. package/python/foxclaw/agents/mixins/skillbox.py +58 -0
  62. package/python/foxclaw/agents/mixins/tool_exec.py +417 -0
  63. package/python/foxclaw/api/__init__.py +224 -0
  64. package/python/foxclaw/api/_state.py +43 -0
  65. package/python/foxclaw/api/bootstrap.py +96 -0
  66. package/python/foxclaw/api/middleware.py +294 -0
  67. package/python/foxclaw/api/routes/ahs.py +404 -0
  68. package/python/foxclaw/api/routes/attachments.py +116 -0
  69. package/python/foxclaw/api/routes/browser.py +35 -0
  70. package/python/foxclaw/api/routes/domain.py +306 -0
  71. package/python/foxclaw/api/routes/knowledge.py +145 -0
  72. package/python/foxclaw/api/routes/llm_admin.py +449 -0
  73. package/python/foxclaw/api/routes/memory.py +105 -0
  74. package/python/foxclaw/api/routes/orchestration.py +39 -0
  75. package/python/foxclaw/api/routes/recovery.py +44 -0
  76. package/python/foxclaw/api/routes/review.py +266 -0
  77. package/python/foxclaw/api/routes/search.py +33 -0
  78. package/python/foxclaw/api/routes/system.py +311 -0
  79. package/python/foxclaw/api/routes/task.py +263 -0
  80. package/python/foxclaw/api/routes/ws.py +64 -0
  81. package/python/foxclaw/api/spine.py +52 -0
  82. package/python/foxclaw/browser/__init__.py +13 -0
  83. package/python/foxclaw/browser/action/__init__.py +1 -0
  84. package/python/foxclaw/browser/action/action_executor.py +50 -0
  85. package/python/foxclaw/browser/action/action_types.py +44 -0
  86. package/python/foxclaw/browser/action/action_verifier.py +68 -0
  87. package/python/foxclaw/browser/action/url_policy.py +143 -0
  88. package/python/foxclaw/browser/adapter/__init__.py +1 -0
  89. package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
  90. package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
  91. package/python/foxclaw/browser/driver/__init__.py +1 -0
  92. package/python/foxclaw/browser/driver/base_driver.py +61 -0
  93. package/python/foxclaw/browser/driver/playwright_driver.py +170 -0
  94. package/python/foxclaw/browser/perception/__init__.py +1 -0
  95. package/python/foxclaw/browser/perception/dom_extractor.py +42 -0
  96. package/python/foxclaw/browser/perception/element_indexer.py +56 -0
  97. package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
  98. package/python/foxclaw/browser/perception/snapshot.py +29 -0
  99. package/python/foxclaw/browser/service.py +144 -0
  100. package/python/foxclaw/browser/session/__init__.py +1 -0
  101. package/python/foxclaw/browser/session/session_manager.py +57 -0
  102. package/python/foxclaw/capability/__init__.py +12 -0
  103. package/python/foxclaw/capability/embedding_service.py +65 -0
  104. package/python/foxclaw/channels/__init__.py +138 -0
  105. package/python/foxclaw/channels/discord.py +175 -0
  106. package/python/foxclaw/channels/dispatcher.py +179 -0
  107. package/python/foxclaw/channels/feishu.py +215 -0
  108. package/python/foxclaw/channels/openclaw_bridge.py +151 -0
  109. package/python/foxclaw/channels/signing.py +187 -0
  110. package/python/foxclaw/channels/telegram.py +120 -0
  111. package/python/foxclaw/channels/weixin.py +215 -0
  112. package/python/foxclaw/cli.py +183 -0
  113. package/python/foxclaw/cognitive/__init__.py +4 -0
  114. package/python/foxclaw/cognitive/reasoner.py +81 -0
  115. package/python/foxclaw/cognitive/state.py +141 -0
  116. package/python/foxclaw/core/__init__.py +9 -0
  117. package/python/foxclaw/core/audit_chain.py +131 -0
  118. package/python/foxclaw/core/audit_sink.py +100 -0
  119. package/python/foxclaw/core/compaction.py +265 -0
  120. package/python/foxclaw/core/config.py +65 -0
  121. package/python/foxclaw/core/confirm_nonce.py +151 -0
  122. package/python/foxclaw/core/dispatch.py +27 -0
  123. package/python/foxclaw/core/envfile.py +48 -0
  124. package/python/foxclaw/core/failure_policy.py +142 -0
  125. package/python/foxclaw/core/gateway.py +81 -0
  126. package/python/foxclaw/core/governance_audit.py +171 -0
  127. package/python/foxclaw/core/idempotency.py +282 -0
  128. package/python/foxclaw/core/injection_guard.py +202 -0
  129. package/python/foxclaw/core/memory_adapter.py +489 -0
  130. package/python/foxclaw/core/metrics.py +88 -0
  131. package/python/foxclaw/core/model_router.py +985 -0
  132. package/python/foxclaw/core/paths.py +103 -0
  133. package/python/foxclaw/core/recovery.py +349 -0
  134. package/python/foxclaw/core/scheduler.py +141 -0
  135. package/python/foxclaw/core/secrets.py +131 -0
  136. package/python/foxclaw/core/sessions.py +155 -0
  137. package/python/foxclaw/core/settings.py +414 -0
  138. package/python/foxclaw/core/shared_write.py +186 -0
  139. package/python/foxclaw/core/steering_queue.py +154 -0
  140. package/python/foxclaw/core/system_bus.py +84 -0
  141. package/python/foxclaw/core/thought_chain.py +316 -0
  142. package/python/foxclaw/core/token_meter.py +231 -0
  143. package/python/foxclaw/core/tools.py +164 -0
  144. package/python/foxclaw/core/tracing.py +375 -0
  145. package/python/foxclaw/core/truth_pipeline.py +110 -0
  146. package/python/foxclaw/core/update_check.py +149 -0
  147. package/python/foxclaw/core/vec_recall_adapter.py +477 -0
  148. package/python/foxclaw/core/verify.py +173 -0
  149. package/python/foxclaw/core/web_search.py +218 -0
  150. package/python/foxclaw/finance/__init__.py +15 -0
  151. package/python/foxclaw/finance/evidence.py +149 -0
  152. package/python/foxclaw/finance/fx.py +98 -0
  153. package/python/foxclaw/finance/graph.py +179 -0
  154. package/python/foxclaw/finance/models.py +146 -0
  155. package/python/foxclaw/finance/nlparse.py +515 -0
  156. package/python/foxclaw/finance/policy.py +301 -0
  157. package/python/foxclaw/finance/reasoner.py +121 -0
  158. package/python/foxclaw/finance/reasoning.py +319 -0
  159. package/python/foxclaw/finance/reconcile.py +175 -0
  160. package/python/foxclaw/finance/runtime.py +500 -0
  161. package/python/foxclaw/governance/__init__.py +24 -0
  162. package/python/foxclaw/governance/middleware.py +176 -0
  163. package/python/foxclaw/harness/__init__.py +17 -0
  164. package/python/foxclaw/harness/_base.py +66 -0
  165. package/python/foxclaw/harness/_policy.py +340 -0
  166. package/python/foxclaw/harness/drift.py +77 -0
  167. package/python/foxclaw/harness/event_bus.py +28 -0
  168. package/python/foxclaw/harness/harness.py +358 -0
  169. package/python/foxclaw/harness/loop_detection.py +145 -0
  170. package/python/foxclaw/itinerary/__init__.py +10 -0
  171. package/python/foxclaw/itinerary/amap_client.py +114 -0
  172. package/python/foxclaw/itinerary/models.py +80 -0
  173. package/python/foxclaw/itinerary/reasoner.py +96 -0
  174. package/python/foxclaw/itinerary/reasoning.py +168 -0
  175. package/python/foxclaw/itinerary/runtime.py +144 -0
  176. package/python/foxclaw/kernel/__init__.py +13 -0
  177. package/python/foxclaw/kernel/capabilities.py +72 -0
  178. package/python/foxclaw/kernel/policy_gate.py +108 -0
  179. package/python/foxclaw/kernel/process_manager.py +186 -0
  180. package/python/foxclaw/kernel/scheduler.py +125 -0
  181. package/python/foxclaw/kernel/sentinel.py +443 -0
  182. package/python/foxclaw/kernel/sentinel_client.py +243 -0
  183. package/python/foxclaw/kernel/spine.py +97 -0
  184. package/python/foxclaw/knowledge/__init__.py +360 -0
  185. package/python/foxclaw/knowledge/truth_crystallizer.py +89 -0
  186. package/python/foxclaw/learning/__init__.py +12 -0
  187. package/python/foxclaw/learning/knowledge_graph.py +144 -0
  188. package/python/foxclaw/learning/models.py +93 -0
  189. package/python/foxclaw/learning/reasoner.py +67 -0
  190. package/python/foxclaw/learning/runtime.py +351 -0
  191. package/python/foxclaw/learning/spaced_repetition.py +91 -0
  192. package/python/foxclaw/mcp_server.py +188 -0
  193. package/python/foxclaw/orchestration/__init__.py +167 -0
  194. package/python/foxclaw/pipeline/__init__.py +23 -0
  195. package/python/foxclaw/pipeline/cognitive_pipeline.py +429 -0
  196. package/python/foxclaw/pipeline/stage_meta.py +21 -0
  197. package/python/foxclaw/pipeline/supervisor_stages.py +201 -0
  198. package/python/foxclaw/planning/__init__.py +17 -0
  199. package/python/foxclaw/planning/llm_planner.py +232 -0
  200. package/python/foxclaw/planning/planner.py +94 -0
  201. package/python/foxclaw/search/__init__.py +24 -0
  202. package/python/foxclaw/search/agent_port.py +190 -0
  203. package/python/foxclaw/search/evidence.py +109 -0
  204. package/python/foxclaw/search/external_search.py +50 -0
  205. package/python/foxclaw/search/hkc_read_port.py +60 -0
  206. package/python/foxclaw/search/internal_search.py +71 -0
  207. package/python/foxclaw/search/models.py +127 -0
  208. package/python/foxclaw/search/planner.py +44 -0
  209. package/python/foxclaw/search/retriever.py +39 -0
  210. package/python/foxclaw/search/runtime.py +127 -0
  211. package/python/foxclaw/search/strategy.py +44 -0
  212. package/python/foxclaw/skills/__init__.py +559 -0
  213. package/python/foxclaw/skills/archive.py +111 -0
  214. package/python/foxclaw/skills/fetch.py +226 -0
  215. package/python/foxclaw/skills/hooks_spec.py +69 -0
  216. package/python/foxclaw/skills/install_cmd.py +216 -0
  217. package/python/foxclaw/skills/lifecycle.py +252 -0
  218. package/python/foxclaw/skills/markdown_skill.py +175 -0
  219. package/python/foxclaw/skills/markdown_store.py +39 -0
  220. package/python/foxclaw/skills/mcp_client.py +184 -0
  221. package/python/foxclaw/skills/mcp_store.py +40 -0
  222. package/python/foxclaw/skills/permission.py +145 -0
  223. package/python/foxclaw/skills/pipeline.py +354 -0
  224. package/python/foxclaw/skills/policy.py +74 -0
  225. package/python/foxclaw/skills/review.py +72 -0
  226. package/python/foxclaw/skills/store.py +57 -0
  227. package/python/frontend/assets/core_bg.jpg +0 -0
  228. package/python/frontend/deck/auth.js +25 -0
  229. package/python/frontend/deck/chat_model_picker.js +483 -0
  230. package/python/frontend/deck/domain.js +276 -0
  231. package/python/frontend/deck/main.js +1806 -0
  232. package/python/frontend/deck/markdown.js +44 -0
  233. package/python/frontend/deck/settings.js +307 -0
  234. package/python/frontend/deck/state.js +5 -0
  235. package/python/frontend/deck/util.js +4 -0
  236. package/python/frontend/deck/visuals.js +190 -0
  237. package/python/frontend/deck.css +855 -0
  238. package/python/frontend/deck.html +686 -0
  239. package/python/frontend/deck_v2.html +341 -0
  240. package/python/frontend/planet/index.html +266 -0
  241. package/python/frontend/planet/planet.css +579 -0
  242. package/python/frontend/planet/planet.js +1436 -0
  243. package/python/frontend/planet/vendor/three.core.min.js +6 -0
  244. package/python/frontend/planet/vendor/three.module.min.js +6 -0
  245. package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
  246. package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
  247. package/python/frontend/v3/index.html +15 -0
  248. package/python/frontend/widget_runtime.js +252 -0
  249. package/python/frontend/widgets.js +639 -0
  250. package/python/frontend_src/index.html +14 -0
  251. package/python/frontend_src/package-lock.json +1376 -0
  252. package/python/frontend_src/package.json +19 -0
  253. package/python/frontend_src/src/App.vue +58 -0
  254. package/python/frontend_src/src/api.js +82 -0
  255. package/python/frontend_src/src/components/ChatDock.vue +365 -0
  256. package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
  257. package/python/frontend_src/src/components/DocViewer.vue +162 -0
  258. package/python/frontend_src/src/components/Inspector.vue +235 -0
  259. package/python/frontend_src/src/components/LeftRail.vue +415 -0
  260. package/python/frontend_src/src/components/MenuPopover.vue +128 -0
  261. package/python/frontend_src/src/components/MessageStream.vue +389 -0
  262. package/python/frontend_src/src/components/ModelPicker.vue +350 -0
  263. package/python/frontend_src/src/components/QuickIngest.vue +112 -0
  264. package/python/frontend_src/src/components/Stage.vue +272 -0
  265. package/python/frontend_src/src/components/ToolMenu.vue +141 -0
  266. package/python/frontend_src/src/components/TopBar.vue +118 -0
  267. package/python/frontend_src/src/components/panels/ActivityPanel.vue +82 -0
  268. package/python/frontend_src/src/components/panels/AgentsPanel.vue +83 -0
  269. package/python/frontend_src/src/components/panels/KnowledgePanel.vue +425 -0
  270. package/python/frontend_src/src/components/panels/MemoryPanel.vue +117 -0
  271. package/python/frontend_src/src/components/panels/PanelBox.vue +50 -0
  272. package/python/frontend_src/src/components/panels/ReviewPanel.vue +292 -0
  273. package/python/frontend_src/src/components/panels/SettingsPanel.vue +358 -0
  274. package/python/frontend_src/src/components/panels/TasksPanel.vue +73 -0
  275. package/python/frontend_src/src/components/panels/ToolsPanel.vue +746 -0
  276. package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
  277. package/python/frontend_src/src/lib/fmt.js +44 -0
  278. package/python/frontend_src/src/lib/i18n.js +53 -0
  279. package/python/frontend_src/src/lib/locales/en.js +173 -0
  280. package/python/frontend_src/src/lib/locales/zh.js +174 -0
  281. package/python/frontend_src/src/lib/md.js +190 -0
  282. package/python/frontend_src/src/main.js +9 -0
  283. package/python/frontend_src/src/stores/app.js +91 -0
  284. package/python/frontend_src/src/stores/chat.js +450 -0
  285. package/python/frontend_src/src/stores/model.js +96 -0
  286. package/python/frontend_src/src/stores/panels.js +289 -0
  287. package/python/frontend_src/src/stores/profile.js +31 -0
  288. package/python/frontend_src/src/stores/system.js +25 -0
  289. package/python/frontend_src/src/styles/base.css +138 -0
  290. package/python/frontend_src/src/styles/panel.css +117 -0
  291. package/python/frontend_src/src/styles/tokens.css +78 -0
  292. package/python/frontend_src/vite.config.js +39 -0
  293. package/python/pyproject.toml +268 -0
  294. package/python/requirements.txt +311 -0
  295. package/python/skills/README.md +123 -0
  296. package/python/skills/execution-contract/SKILL.md +30 -0
  297. package/python/skills/general-assistant/SKILL.md +34 -0
  298. package/python/skills/planner/SKILL.md +33 -0
  299. package/python/skills/schedule-itinerary/SKILL.md +35 -0
  300. package/python/skills/skill-composition/SKILL.md +31 -0
  301. package/python/skills/skill-guard/SKILL.md +67 -0
  302. package/python/skills/skill-memory/SKILL.md +50 -0
  303. package/python/skills/skill-router/SKILL.md +35 -0
  304. package/python/tests/conftest.py +54 -0
  305. package/python/tests/run_all.py +200 -0
  306. package/python/tests/test_agent_lifecycle.py +326 -0
  307. package/python/tests/test_ahs_p2.py +393 -0
  308. package/python/tests/test_anthropic_tools.py +192 -0
  309. package/python/tests/test_api.py +97 -0
  310. package/python/tests/test_api_auth.py +79 -0
  311. package/python/tests/test_api_host_guard.py +69 -0
  312. package/python/tests/test_attachments.py +151 -0
  313. package/python/tests/test_audit_chain.py +116 -0
  314. package/python/tests/test_audit_sink.py +130 -0
  315. package/python/tests/test_browser.py +149 -0
  316. package/python/tests/test_browser_request_guard.py +180 -0
  317. package/python/tests/test_capabilities.py +152 -0
  318. package/python/tests/test_channel_signing.py +185 -0
  319. package/python/tests/test_channel_trust.py +109 -0
  320. package/python/tests/test_channels.py +174 -0
  321. package/python/tests/test_cli.py +46 -0
  322. package/python/tests/test_compaction_toolpair.py +119 -0
  323. package/python/tests/test_config_layering.py +61 -0
  324. package/python/tests/test_confirm_nonce.py +186 -0
  325. package/python/tests/test_context_compaction_wiring.py +124 -0
  326. package/python/tests/test_dispatch.py +89 -0
  327. package/python/tests/test_domain_evals.py +334 -0
  328. package/python/tests/test_domain_import.py +72 -0
  329. package/python/tests/test_domain_reasoners.py +134 -0
  330. package/python/tests/test_e2e.py +161 -0
  331. package/python/tests/test_emb_cache.py +97 -0
  332. package/python/tests/test_embedding_singleton.py +87 -0
  333. package/python/tests/test_envfile.py +83 -0
  334. package/python/tests/test_evals_smoke.py +90 -0
  335. package/python/tests/test_event_spine.py +174 -0
  336. package/python/tests/test_evidence_semantic.py +88 -0
  337. package/python/tests/test_evidence_sse.py +94 -0
  338. package/python/tests/test_file_ingest.py +135 -0
  339. package/python/tests/test_finance.py +144 -0
  340. package/python/tests/test_finance_ledger.py +245 -0
  341. package/python/tests/test_finance_persist.py +174 -0
  342. package/python/tests/test_gateway.py +125 -0
  343. package/python/tests/test_governance.py +421 -0
  344. package/python/tests/test_governance_audit.py +324 -0
  345. package/python/tests/test_governance_middleware.py +137 -0
  346. package/python/tests/test_hkc.py +268 -0
  347. package/python/tests/test_hkc_index_build_manager.py +306 -0
  348. package/python/tests/test_hkc_manifest_reuse.py +294 -0
  349. package/python/tests/test_hkc_search_event_sync.py +482 -0
  350. package/python/tests/test_import_path_guard.py +155 -0
  351. package/python/tests/test_injection_guard.py +193 -0
  352. package/python/tests/test_integration.py +117 -0
  353. package/python/tests/test_itinerary.py +100 -0
  354. package/python/tests/test_kee_semantic.py +84 -0
  355. package/python/tests/test_kernel.py +212 -0
  356. package/python/tests/test_learning.py +101 -0
  357. package/python/tests/test_llm_admin.py +145 -0
  358. package/python/tests/test_llm_planner.py +344 -0
  359. package/python/tests/test_markdown_skills.py +62 -0
  360. package/python/tests/test_markdown_skills_openclaw.py +66 -0
  361. package/python/tests/test_mcp_connectors.py +97 -0
  362. package/python/tests/test_mcp_guardian_discovery.py +87 -0
  363. package/python/tests/test_mcp_no_memory.py +43 -0
  364. package/python/tests/test_mcp_policy_gate.py +76 -0
  365. package/python/tests/test_mcp_server.py +92 -0
  366. package/python/tests/test_memory_recall.py +92 -0
  367. package/python/tests/test_metrics.py +58 -0
  368. package/python/tests/test_multi_chain.py +90 -0
  369. package/python/tests/test_multimodal.py +122 -0
  370. package/python/tests/test_negotiation.py +136 -0
  371. package/python/tests/test_orchestration.py +109 -0
  372. package/python/tests/test_orchestration_decoupled.py +103 -0
  373. package/python/tests/test_p3_p5_hardening.py +324 -0
  374. package/python/tests/test_parallel_dispatch.py +207 -0
  375. package/python/tests/test_pipeline.py +492 -0
  376. package/python/tests/test_plan_visible.py +86 -0
  377. package/python/tests/test_planner.py +88 -0
  378. package/python/tests/test_policy_gate.py +108 -0
  379. package/python/tests/test_profile.py +67 -0
  380. package/python/tests/test_readme_claims.py +163 -0
  381. package/python/tests/test_reasoner.py +170 -0
  382. package/python/tests/test_release_check.py +252 -0
  383. package/python/tests/test_resume_checkpoint.py +148 -0
  384. package/python/tests/test_routing.py +85 -0
  385. package/python/tests/test_scheduler.py +138 -0
  386. package/python/tests/test_search.py +194 -0
  387. package/python/tests/test_secrets.py +153 -0
  388. package/python/tests/test_semantic.py +69 -0
  389. package/python/tests/test_sessions.py +90 -0
  390. package/python/tests/test_settings.py +265 -0
  391. package/python/tests/test_shadow_learning.py +290 -0
  392. package/python/tests/test_shared_write.py +200 -0
  393. package/python/tests/test_skill_taint_gate.py +145 -0
  394. package/python/tests/test_skills.py +117 -0
  395. package/python/tests/test_skills_fetch.py +282 -0
  396. package/python/tests/test_stats_and_events.py +162 -0
  397. package/python/tests/test_streaming.py +55 -0
  398. package/python/tests/test_supervisor_pipeline.py +176 -0
  399. package/python/tests/test_supervisor_triage.py +102 -0
  400. package/python/tests/test_taint.py +112 -0
  401. package/python/tests/test_task_stream.py +74 -0
  402. package/python/tests/test_thinking_sse.py +66 -0
  403. package/python/tests/test_thinking_stream.py +76 -0
  404. package/python/tests/test_tool_center.py +113 -0
  405. package/python/tests/test_tool_parallel.py +113 -0
  406. package/python/tests/test_tool_params.py +99 -0
  407. package/python/tests/test_tool_use.py +229 -0
  408. package/python/tests/test_toplevel_concurrency.py +116 -0
  409. package/python/tests/test_tracing.py +256 -0
  410. package/python/tests/test_truth_crystallizer.py +125 -0
  411. package/python/tests/test_truth_flow.py +88 -0
  412. package/python/tests/test_update_check.py +95 -0
  413. package/python/tests/test_url_policy.py +53 -0
  414. package/python/tests/test_verify_evidence.py +212 -0
  415. package/python/tests/test_version_sync.py +100 -0
  416. package/python/tests/test_web_trigger.py +82 -0
  417. package/python/tools/check_faiss_type.py +30 -0
  418. package/python/tools/check_log.ps1 +38 -0
  419. package/python/tools/ivf_status.py +72 -0
  420. package/python/tools/mypy_baseline.txt +13 -0
  421. package/python/tools/mypy_ratchet.py +143 -0
  422. package/python/tools/pack.py +173 -0
  423. package/python/tools/release_check.py +349 -0
  424. package/python/tools/restart_foxclaw.ps1 +71 -0
  425. package/python/tools/shared_write_audit.py +190 -0
  426. package/python/tools/size_baseline.txt +28 -0
  427. package/python/tools/size_check.py +102 -0
  428. package/python/tools/strip_comments.py +228 -0
  429. package/python/tools/verify_env.bat +2 -0
  430. package/python/tools/verify_env.py +25 -0
  431. package/python/vendor/HMR_LICENSE +37 -0
  432. package/python/vendor/HMR_README.md +65 -0
  433. package/python/vendor/hkc/LICENSE +37 -0
  434. package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
  435. package/python/vendor/hkc/hkc_ace/ace.py +457 -0
  436. package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
  437. package/python/vendor/hkc/hkc_api/__init__.py +0 -0
  438. package/python/vendor/hkc/hkc_api/container.py +416 -0
  439. package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
  440. package/python/vendor/hkc/hkc_api/main.py +157 -0
  441. package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
  442. package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
  443. package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
  444. package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
  445. package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
  446. package/python/vendor/hkc/hkc_api/schemas.py +153 -0
  447. package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
  448. package/python/vendor/hkc/hkc_core/__init__.py +0 -0
  449. package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
  450. package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
  451. package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
  452. package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
  453. package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
  454. package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
  455. package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
  456. package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
  457. package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
  458. package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
  459. package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
  460. package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
  461. package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
  462. package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
  463. package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
  464. package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
  465. package/python/vendor/hkc/hkc_kde/kde.py +298 -0
  466. package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
  467. package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
  468. package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
  469. package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
  470. package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
  471. package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
  472. package/python/vendor/hkc/hkc_kde/models.py +74 -0
  473. package/python/vendor/hkc/hkc_kde/packager.py +329 -0
  474. package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
  475. package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
  476. package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
  477. package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
  478. package/python/vendor/hkc/hkc_kee/kee.py +541 -0
  479. package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
  480. package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
  481. package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
  482. package/python/vendor/hkc/hkc_sdk/base.py +151 -0
  483. package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
  484. package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
  485. package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
  486. package/python/vendor/hkc/hkc_sdk/models.py +171 -0
  487. package/python/vendor/hkc/hkc_search/__init__.py +0 -0
  488. package/python/vendor/hkc/hkc_search/bm25.py +325 -0
  489. package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
  490. package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
  491. package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
  492. package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
  493. package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
  494. package/python/vendor/hmr/__init__.py +0 -0
  495. package/python/vendor/hmr/core/__init__.py +0 -0
  496. package/python/vendor/hmr/core/hmr.py +684 -0
  497. package/python/vendor/hmr/core/models.py +310 -0
  498. package/python/vendor/hmr/engines/__init__.py +0 -0
  499. package/python/vendor/hmr/engines/evolution.py +710 -0
  500. package/python/vendor/hmr/engines/jit_compiler.py +472 -0
  501. package/python/vendor/hmr/engines/lifecycle.py +264 -0
  502. package/python/vendor/hmr/engines/policy.py +625 -0
  503. package/python/vendor/hmr/engines/recall.py +260 -0
  504. package/python/vendor/hmr/engines/runtime_state.py +158 -0
  505. package/python/vendor/hmr/engines/scheduler.py +323 -0
  506. package/python/vendor/hmr/engines/semantic.py +150 -0
  507. package/python/vendor/hmr/engines/temporal.py +261 -0
  508. package/python/vendor/hmr/engines/thought_chain.py +635 -0
  509. package/python/vendor/hmr/graph/__init__.py +0 -0
  510. package/python/vendor/hmr/graph/cwg.py +338 -0
  511. package/python/vendor/hmr/graph/memory_graph.py +530 -0
  512. package/python/vendor/hmr/storage/__init__.py +0 -0
  513. package/python/vendor/hmr/storage/memory_fs.py +300 -0
  514. package/python/vendor/hmr/storage/vector_store.py +520 -0
@@ -0,0 +1,143 @@
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
+ DETAIL = re.compile(r"^(?P<file>[^:\n]+?\.py):\d+: error: .*\[(?P<code>[a-z-]+)\]$")
31
+ TOP_N = 30
32
+ SAMPLES: dict[str, list[str]] = {}
33
+
34
+ for _s in (sys.stdout, sys.stderr):
35
+ try:
36
+ _s.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
37
+ except Exception:
38
+ pass
39
+
40
+
41
+ def read_baseline() -> int:
42
+ if not BASELINE.exists():
43
+ print(f"✗ 缺少基线文件 {BASELINE.relative_to(ROOT)}(内容应为错误数,如 673)")
44
+ sys.exit(2)
45
+ for line in BASELINE.read_text(encoding="utf-8").splitlines():
46
+ line = line.strip()
47
+ if line and not line.startswith("#"):
48
+ return int(line)
49
+ print(f"✗ 基线文件 {BASELINE.relative_to(ROOT)} 里没有数字")
50
+ sys.exit(2)
51
+
52
+
53
+ def run_mypy() -> tuple[int, int, dict[str, int], dict[str, int]]:
54
+ """返回 (错误数, 文件数, 按文件计数, 按错误码计数)。
55
+
56
+ 配置取自 pyproject [tool.mypy],此处不重复传参。明细统计是为了让 CI 失败时能直接
57
+ 看出「多出来的错误在哪」——此前门禁只打印总数,CI 报 486 而本地 408 时无从下手。
58
+ """
59
+ r = subprocess.run(
60
+ [sys.executable, "-m", "mypy", "foxclaw"],
61
+ cwd=ROOT, capture_output=True, text=True, encoding="utf-8", errors="replace",
62
+ )
63
+ out = (r.stdout or "") + (r.stderr or "")
64
+ by_file: dict[str, int] = {}
65
+ by_code: dict[str, int] = {}
66
+ for line in out.splitlines():
67
+ d = DETAIL.match(line.strip())
68
+ if d:
69
+ f = d.group("file").replace("\\", "/")
70
+ by_file[f] = by_file.get(f, 0) + 1
71
+ by_code[d.group("code")] = by_code.get(d.group("code"), 0) + 1
72
+ SAMPLES.setdefault(f, []).append(line.strip().split("error: ", 1)[-1])
73
+ m = SUMMARY.search(out)
74
+ if m:
75
+ return int(m.group(1)), int(m.group(2)), by_file, by_code
76
+ if "Success: no issues found" in out:
77
+ return 0, 0, {}, {}
78
+ print(out[-4000:])
79
+ print("✗ 无法解析 mypy 输出(格式变了?上面是原始输出)")
80
+ sys.exit(2)
81
+
82
+
83
+ def report(by_file: dict[str, int], by_code: dict[str, int]) -> None:
84
+ """失败时打印分布 + 样本消息,便于对照本地与 CI 的差异。"""
85
+ if by_code:
86
+ print(" 按错误码:")
87
+ for code, n in sorted(by_code.items(), key=lambda kv: -kv[1])[:TOP_N]:
88
+ print(f" {n:4d} {code}")
89
+ if by_file:
90
+ print(f" 按文件(top {TOP_N}):")
91
+ for f, n in sorted(by_file.items(), key=lambda kv: -kv[1])[:TOP_N]:
92
+ print(f" {n:4d} {f}")
93
+ if SAMPLES:
94
+ top_files = sorted(by_file.items(), key=lambda kv: -kv[1])[:5]
95
+ print(" 错误样本(按错误最多的文件取前 4 条):")
96
+ for f, _ in top_files:
97
+ for s in SAMPLES.get(f, [])[:4]:
98
+ print(f" {f}: {s}")
99
+
100
+
101
+ def toolchain() -> str:
102
+ """打出 mypy 与解释器版本:本地/CI 计数不一致时,版本漂移是第一嫌疑。"""
103
+ try:
104
+ from importlib.metadata import version
105
+ mypy_ver = version("mypy")
106
+ except Exception:
107
+ mypy_ver = "?"
108
+ return f"mypy {mypy_ver} on Python {sys.version.split()[0]}"
109
+
110
+
111
+ def main() -> int:
112
+ baseline = read_baseline()
113
+ errors, files, by_file, by_code = run_mypy()
114
+ update = "--update" in sys.argv[1:]
115
+
116
+ print(f"mypy: {errors} errors in {files} files | baseline: {baseline}")
117
+ print(f" toolchain: {toolchain()}")
118
+
119
+ if update:
120
+ if errors > baseline:
121
+ print(f"✗ 拒绝更新基线:{errors} > {baseline}。棘轮只允许下调,"
122
+ f"请先修掉新增的 {errors - baseline} 个错误")
123
+ report(by_file, by_code)
124
+ return 1
125
+ BASELINE.write_text(f"{errors}\n", encoding="utf-8")
126
+ print(f"✓ 基线已刷新为 {errors}")
127
+ return 0
128
+
129
+ if errors > baseline:
130
+ print(f"✗ 类型错误增加 {errors - baseline} 个({baseline} → {errors})。"
131
+ f"新增错误必须修掉,不能靠调高基线通过")
132
+ report(by_file, by_code)
133
+ return 1
134
+ if errors < baseline:
135
+ print(f"✓ 通过,且比基线少 {baseline - errors} 个 —— "
136
+ f"可信后执行 `python tools/mypy_ratchet.py --update` 锁定战果")
137
+ return 0
138
+ print("✓ 通过(与基线持平)")
139
+ return 0
140
+
141
+
142
+ if __name__ == "__main__":
143
+ sys.exit(main())
@@ -0,0 +1,173 @@
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
+ _INTERNAL_DOCS = "docs/internal/"
41
+
42
+
43
+ def read_version() -> str:
44
+ """从 foxclaw/__init__.py 读 __version__(单一来源,避免版本号漂移)。"""
45
+ txt = (ROOT / "foxclaw" / "__init__.py").read_text(encoding="utf-8")
46
+ m = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', txt)
47
+ return m.group(1) if m else "0.0.0"
48
+
49
+
50
+ def _norm(name: str) -> str:
51
+ return name.replace("\\", "/")
52
+
53
+
54
+ def _forbidden(path: str, allow_internal_docs: bool) -> str | None:
55
+ """返回该条目被禁止的原因;允许则返回 None。"""
56
+ p = _norm(path)
57
+ if p.endswith("/"):
58
+ return None
59
+ parts = p.split("/")
60
+ joined = "/".join(parts[1:]) if len(parts) > 1 else p
61
+
62
+ if not allow_internal_docs and _INTERNAL_DOCS in p:
63
+ return "内部机密文档 docs/internal/(对外分发版必须排除)"
64
+ base = parts[-1]
65
+ if base == ".env" or (base.startswith(".env.") and base != ".env.example"):
66
+ return "密钥配置文件"
67
+ if base.startswith(".env.bak"):
68
+ return "密钥备份文件"
69
+ if "foxclaw_data/" in p or joined.startswith("foxclaw_data/"):
70
+ return "运行时数据(含真实会话/记忆)"
71
+ if re.search(r"\.(zip|rar|7z)$", base, re.I):
72
+ return "嵌套归档(打包产物不应入库/入包)"
73
+ if ".venv/" in p or "venv/" in p:
74
+ return "虚拟环境"
75
+ if "__pycache__/" in p or ".pytest_cache/" in p:
76
+ return "缓存目录"
77
+ if "新建文件夹/" in p:
78
+ return "重复副本目录"
79
+ if ".bak" in base:
80
+ return "备份文件"
81
+ return None
82
+
83
+
84
+ def check_archive(zf: Path, allow_internal_docs: bool) -> list[tuple[str, str]]:
85
+ """校验归档内容,返回 [(条目, 原因)] 违规清单。"""
86
+ bad: list[tuple[str, str]] = []
87
+ with zipfile.ZipFile(zf) as z:
88
+ for name in z.namelist():
89
+ why = _forbidden(name, allow_internal_docs)
90
+ if why:
91
+ bad.append((name, why))
92
+ return bad
93
+
94
+
95
+ def git(args: list[str]) -> subprocess.CompletedProcess:
96
+ return subprocess.run(
97
+ ["git", "-C", str(ROOT), *args],
98
+ capture_output=True, text=True, encoding="utf-8", errors="replace",
99
+ )
100
+
101
+
102
+ def ensure_repo() -> None:
103
+ if git(["rev-parse", "--is-inside-work-tree"]).returncode != 0:
104
+ sys.exit(f"[pack] 错误:{ROOT} 不是 git 仓库。本脚本依赖 git archive,请先 git init。")
105
+
106
+
107
+ def warn_if_dirty() -> None:
108
+ r = git(["status", "--porcelain"])
109
+ if r.stdout.strip():
110
+ print("[pack] ⚠ 工作区有未提交改动 —— 只会打包已提交的内容(HEAD)。"
111
+ "如需包含最新改动,请先 commit。")
112
+
113
+
114
+ def build(variant: str, ref: str) -> Path | None:
115
+ version = read_version()
116
+ stamp = _dt.date.today().strftime("%Y%m%d")
117
+ prefix = f"Foxclaw-{variant}/"
118
+ out = DIST / f"Foxclaw-{variant}-{version}-{stamp}.zip"
119
+
120
+ cmd = ["archive", "--format=zip", f"--prefix={prefix}", "-o", str(out), ref]
121
+ if variant == "public":
122
+ cmd += ["--", ".", f":(exclude){_INTERNAL_DOCS.rstrip('/')}"]
123
+
124
+ r = git(cmd)
125
+ if r.returncode != 0:
126
+ print(f"[pack] ✗ git archive 失败:\n{r.stderr.strip()}")
127
+ out.unlink(missing_ok=True)
128
+ return None
129
+
130
+ bad = check_archive(out, allow_internal_docs=(variant == "internal"))
131
+ if bad:
132
+ out.unlink(missing_ok=True)
133
+ print(f"[pack] ✗ {variant} 版校验未通过,已删除归档。违规条目:")
134
+ for name, why in bad[:20]:
135
+ print(f" - {name} ({why})")
136
+ if len(bad) > 20:
137
+ print(f" ... 另有 {len(bad) - 20} 条")
138
+ return None
139
+
140
+ with zipfile.ZipFile(out) as z:
141
+ n = len([x for x in z.namelist() if not x.endswith("/")])
142
+ size = out.stat().st_size / 1024 / 1024
143
+ print(f"[pack] ✓ {variant:<8} {out.relative_to(ROOT)} ({n} 文件 / {size:.2f} MB)")
144
+ return out
145
+
146
+
147
+ def main() -> int:
148
+ ap = argparse.ArgumentParser(description="用 git archive 生成 Foxclaw 交付归档")
149
+ ap.add_argument("--only", choices=["public", "internal"], help="只生成指定版本")
150
+ ap.add_argument("--ref", default="HEAD", help="要打包的 git 引用(默认 HEAD)")
151
+ args = ap.parse_args()
152
+
153
+ ensure_repo()
154
+ DIST.mkdir(exist_ok=True)
155
+ warn_if_dirty()
156
+
157
+ variants = [args.only] if args.only else ["internal", "public"]
158
+ ok = True
159
+ for v in variants:
160
+ if build(v, args.ref) is None:
161
+ ok = False
162
+
163
+ if not ok:
164
+ print("[pack] 有归档未通过校验,请检查上面的违规清单。")
165
+ return 1
166
+ print(f"[pack] 完成。产物目录:{DIST.relative_to(ROOT)}/")
167
+ if "internal" in variants:
168
+ print("[pack] 提醒:internal 版含 docs/internal/,请勿对外分发。")
169
+ return 0
170
+
171
+
172
+ if __name__ == "__main__":
173
+ 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
45
+ code: str
46
+ msg: str
47
+
48
+ def __str__(self) -> str:
49
+ mark = "ERROR" if self.level == "error" else " WARN"
50
+ return f" [{mark}] {self.code}: {self.msg}"
51
+
52
+
53
+ def _dunder_version(root: Path) -> str:
54
+ txt = (root / "foxclaw" / "__init__.py").read_text(encoding="utf-8")
55
+ m = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']', txt, re.M)
56
+ return m.group(1) if m else ""
57
+
58
+
59
+ def _npm_index_version(root: Path) -> str:
60
+ src = (root / "npm" / "index.js").read_text(encoding="utf-8")
61
+ m = re.search(r'export\s+const\s+version\s*=\s*["\']([^"\']+)["\']', src)
62
+ return m.group(1) if m else ""
63
+
64
+
65
+ def _project(root: Path) -> dict:
66
+ with (root / "pyproject.toml").open("rb") as fh:
67
+ return tomllib.load(fh)["project"]
68
+
69
+
70
+ def _npm_package(root: Path) -> dict:
71
+ return json.loads((root / "npm" / "package.json").read_text(encoding="utf-8"))
72
+
73
+
74
+ def _frontend_package(root: Path) -> dict:
75
+ return json.loads((root / "frontend_src" / "package.json").read_text(encoding="utf-8"))
76
+
77
+
78
+ def project_name(root: Path = ROOT) -> str:
79
+ try:
80
+ return _project(root).get("name", "")
81
+ except Exception:
82
+ return ""
83
+
84
+
85
+ def collect_versions(root: Path = ROOT) -> dict:
86
+ """五处版本号;读不到留空串,交给 check_versions 报 unreadable。"""
87
+ def safe(fn, *a):
88
+ try:
89
+ return fn(*a) or ""
90
+ except Exception:
91
+ return ""
92
+
93
+ return {
94
+ "pyproject.toml:project.version": safe(lambda: _project(root)["version"]),
95
+ "foxclaw/__init__.py:__version__": safe(_dunder_version, root),
96
+ "npm/package.json:version": safe(lambda: _npm_package(root)["version"]),
97
+ "npm/index.js:export const version": safe(_npm_index_version, root),
98
+ "frontend_src/package.json:version": safe(lambda: _frontend_package(root)["version"]),
99
+ }
100
+
101
+
102
+ def check_versions(root: Path = ROOT) -> list:
103
+ versions = collect_versions(root)
104
+ issues = [Issue("error", "unreadable", f"{src} 里读不到版本号")
105
+ for src, v in versions.items() if not v]
106
+ vals = {k: v for k, v in versions.items() if v}
107
+ if len(set(vals.values())) > 1:
108
+ ref = vals.get("pyproject.toml:project.version") or next(iter(vals.values()))
109
+ for src, v in vals.items():
110
+ if v != ref:
111
+ issues.append(Issue("error", "version_mismatch",
112
+ f"{src} = {v},与 pyproject = {ref} 不一致(发布后不可改)"))
113
+ return issues
114
+
115
+
116
+ def readme_target(root: Path = ROOT) -> str:
117
+ """pyproject [project].readme 指向的正文文件(相对路径);未声明返回空串。"""
118
+ try:
119
+ field = _project(root).get("readme")
120
+ except Exception: # noqa: BLE001
121
+ return ""
122
+ if isinstance(field, str):
123
+ return field
124
+ if isinstance(field, dict):
125
+ return str(field.get("file", "") or "")
126
+ return ""
127
+
128
+
129
+ def check_metadata(root: Path = ROOT) -> list:
130
+ """项目页正文必须存在。
131
+
132
+ 0.1.1 上传后 PyPI 项目页只有一句「该软件包的作者未提供项目描述」——pyproject
133
+ 没声明 `readme`,元数据里就没有 Description。包能装、能 import,所以这条缺陷
134
+ 在发布前的任何检查里都不会暴露,只能显式断言。
135
+ """
136
+ path = readme_target(root)
137
+ if not path:
138
+ return [Issue("error", "no_readme",
139
+ "pyproject [project] 未声明 readme → PyPI 项目页显示「未提供项目描述」")]
140
+ f = root / path
141
+ if not f.is_file():
142
+ return [Issue("error", "readme_missing", f"readme 指向的 {path} 不存在")]
143
+ body = f.read_text(encoding="utf-8", errors="replace").strip()
144
+ if len(body) < 200:
145
+ return [Issue("error", "readme_too_short",
146
+ f"{path} 只有 {len(body)} 字符,撑不起项目页描述")]
147
+ return []
148
+
149
+
150
+ def check_tag(tag: str, version: str) -> list:
151
+ """tag 必须就是本次要发的版本(v 前缀可有可无)。"""
152
+ if not tag:
153
+ return []
154
+ stripped = tag.strip()
155
+ if stripped[:1] in ("v", "V"):
156
+ stripped = stripped[1:]
157
+ if stripped != version:
158
+ return [Issue("error", "tag_mismatch",
159
+ f"tag {tag!r} 指向 {stripped},但代码里是 {version}")]
160
+ return []
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:]))