@huicai/cli 2026.814.0 → 2026.1021.0

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 (493) hide show
  1. package/LICENSE +391 -32
  2. package/README.md +139 -8
  3. package/bin/huicai.js +155 -74
  4. package/bin/update.js +97 -0
  5. package/package.json +27 -8
  6. package/skills/ab-testing/trigger-cases.json +30 -0
  7. package/skills/academic-writing/trigger-cases.json +30 -0
  8. package/skills/accounting-ops/trigger-cases.json +30 -0
  9. package/skills/ai-agent/trigger-cases.json +30 -0
  10. package/skills/ai-aiops/trigger-cases.json +30 -0
  11. package/skills/ai-coding/SKILL.md +2 -0
  12. package/skills/ai-coding/trigger-cases.json +30 -0
  13. package/skills/ai-compliance/trigger-cases.json +30 -0
  14. package/skills/ai-eval/trigger-cases.json +30 -0
  15. package/skills/ai-llm-app/trigger-cases.json +30 -0
  16. package/skills/ai-mlops/trigger-cases.json +30 -0
  17. package/skills/ai-multimodal/trigger-cases.json +30 -0
  18. package/skills/ai-prompt/trigger-cases.json +30 -0
  19. package/skills/ai-rag/trigger-cases.json +30 -0
  20. package/skills/anomaly-detection/trigger-cases.json +30 -0
  21. package/skills/api-design/trigger-cases.json +31 -0
  22. package/skills/api-integration/trigger-cases.json +31 -0
  23. package/skills/arch-design/trigger-cases.json +12 -0
  24. package/skills/arch-diagram/SKILL.md +118 -0
  25. package/skills/arch-diagram/trigger-cases.json +30 -0
  26. package/skills/arch-distributed/trigger-cases.json +30 -0
  27. package/skills/arch-multitenant/trigger-cases.json +30 -0
  28. package/skills/audio-brief/SKILL.md +65 -0
  29. package/skills/audio-brief/trigger-cases.json +30 -0
  30. package/skills/audit-assist/trigger-cases.json +30 -0
  31. package/skills/audit-chain/trigger-cases.json +31 -0
  32. package/skills/banking-credit/trigger-cases.json +30 -0
  33. package/skills/brand-design/trigger-cases.json +30 -0
  34. package/skills/business-plan/trigger-cases.json +30 -0
  35. package/skills/capital-ops/trigger-cases.json +30 -0
  36. package/skills/cicd-pipeline/trigger-cases.json +31 -0
  37. package/skills/code-archaeology/SKILL.md +55 -0
  38. package/skills/code-archaeology/trigger-cases.json +9 -0
  39. package/skills/code-quality/SKILL.md +138 -0
  40. package/skills/code-quality/references/languages.md +107 -0
  41. package/skills/code-quality/scripts/__pycache__/code_battery.cpython-312.pyc +0 -0
  42. package/skills/code-quality/scripts/__pycache__/code_check.cpython-312.pyc +0 -0
  43. package/skills/code-quality/scripts/__pycache__/code_check.cpython-313.pyc +0 -0
  44. package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-312.pyc +0 -0
  45. package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-313.pyc +0 -0
  46. package/skills/code-quality/scripts/__pycache__/code_fix.cpython-312.pyc +0 -0
  47. package/skills/code-quality/scripts/__pycache__/code_fix.cpython-313.pyc +0 -0
  48. package/skills/code-quality/scripts/__pycache__/code_java.cpython-312.pyc +0 -0
  49. package/skills/code-quality/scripts/__pycache__/code_java.cpython-313.pyc +0 -0
  50. package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-312.pyc +0 -0
  51. package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-313.pyc +0 -0
  52. package/skills/code-quality/scripts/__pycache__/code_lens.cpython-312.pyc +0 -0
  53. package/skills/code-quality/scripts/__pycache__/code_lens.cpython-313.pyc +0 -0
  54. package/skills/code-quality/scripts/__pycache__/code_python.cpython-312.pyc +0 -0
  55. package/skills/code-quality/scripts/__pycache__/code_python.cpython-313.pyc +0 -0
  56. package/skills/code-quality/scripts/__pycache__/code_testplan.cpython-312.pyc +0 -0
  57. package/skills/code-quality/scripts/code_battery.py +1072 -0
  58. package/skills/code-quality/scripts/code_check.py +1269 -0
  59. package/skills/code-quality/scripts/code_csharp.py +371 -0
  60. package/skills/code-quality/scripts/code_fix.py +201 -0
  61. package/skills/code-quality/scripts/code_java.py +695 -0
  62. package/skills/code-quality/scripts/code_jsts.py +313 -0
  63. package/skills/code-quality/scripts/code_lens.py +1079 -0
  64. package/skills/code-quality/scripts/code_python.py +536 -0
  65. package/skills/code-quality/scripts/code_testplan.py +239 -0
  66. package/skills/code-quality/trigger-cases.json +45 -0
  67. package/skills/code-review/SKILL.md +24 -0
  68. package/skills/code-review/trigger-cases.json +35 -0
  69. package/skills/commit-and-pr/trigger-cases.json +31 -0
  70. package/skills/content-creation/trigger-cases.json +30 -0
  71. package/skills/corporate-governance/trigger-cases.json +30 -0
  72. package/skills/crawl-chain/trigger-cases.json +31 -0
  73. package/skills/customer-service/trigger-cases.json +30 -0
  74. package/skills/data-analysis/trigger-cases.json +30 -0
  75. package/skills/data-chain/SKILL.md +1 -1
  76. package/skills/data-chain/trigger-cases.json +31 -0
  77. package/skills/data-governance/trigger-cases.json +30 -0
  78. package/skills/data-report-pro/trigger-cases.json +30 -0
  79. package/skills/data-streaming/trigger-cases.json +30 -0
  80. package/skills/data-wrangling/SKILL.md +1 -1
  81. package/skills/data-wrangling/trigger-cases.json +30 -0
  82. package/skills/db-es/trigger-cases.json +30 -0
  83. package/skills/db-migration/trigger-cases.json +30 -0
  84. package/skills/db-mysql/trigger-cases.json +30 -0
  85. package/skills/db-postgres/trigger-cases.json +30 -0
  86. package/skills/db-redis/trigger-cases.json +30 -0
  87. package/skills/debug-root-cause/trigger-cases.json +30 -0
  88. package/skills/dependency-audit/SKILL.md +56 -0
  89. package/skills/dependency-audit/trigger-cases.json +7 -0
  90. package/skills/dependency-upgrade/trigger-cases.json +31 -0
  91. package/skills/dev-cpp/trigger-cases.json +30 -0
  92. package/skills/dev-cross-platform/trigger-cases.json +30 -0
  93. package/skills/dev-csharp/SKILL.md +23 -0
  94. package/skills/dev-csharp/trigger-cases.json +30 -0
  95. package/skills/dev-frontend/SKILL.md +19 -0
  96. package/skills/dev-frontend/trigger-cases.json +30 -0
  97. package/skills/dev-go/trigger-cases.json +30 -0
  98. package/skills/dev-harmony/trigger-cases.json +30 -0
  99. package/skills/dev-java/SKILL.md +20 -0
  100. package/skills/dev-java/trigger-cases.json +30 -0
  101. package/skills/dev-mobile/trigger-cases.json +30 -0
  102. package/skills/dev-node/trigger-cases.json +30 -0
  103. package/skills/dev-php/trigger-cases.json +30 -0
  104. package/skills/dev-python/SKILL.md +21 -0
  105. package/skills/dev-python/trigger-cases.json +30 -0
  106. package/skills/dev-rust/trigger-cases.json +30 -0
  107. package/skills/dev-toolchain/trigger-cases.json +31 -0
  108. package/skills/doc-chain/trigger-cases.json +31 -0
  109. package/skills/doc-compare/trigger-cases.json +30 -0
  110. package/skills/docx-author/SKILL.md +127 -25
  111. package/skills/docx-author/scripts/__pycache__/docx_battery.cpython-312.pyc +0 -0
  112. package/skills/docx-author/scripts/__pycache__/docx_build.cpython-312.pyc +0 -0
  113. package/skills/docx-author/scripts/__pycache__/docx_check.cpython-312.pyc +0 -0
  114. package/skills/docx-author/scripts/__pycache__/docx_layout.cpython-312.pyc +0 -0
  115. package/skills/docx-author/scripts/__pycache__/zh_prose.cpython-312.pyc +0 -0
  116. package/skills/docx-author/scripts/docx_battery.py +586 -0
  117. package/skills/docx-author/scripts/docx_build.py +995 -0
  118. package/skills/docx-author/scripts/docx_check.py +1306 -0
  119. package/skills/docx-author/scripts/docx_doc.py +14 -154
  120. package/skills/docx-author/scripts/docx_layout.py +490 -0
  121. package/skills/docx-author/scripts/zh_prose.py +639 -0
  122. package/skills/docx-author/trigger-cases.json +58 -0
  123. package/skills/echo-brief/SKILL.md +84 -0
  124. package/skills/echo-brief/references/echo-format.md +68 -0
  125. package/skills/echo-brief/trigger-cases.json +40 -0
  126. package/skills/ecommerce-ops/trigger-cases.json +30 -0
  127. package/skills/education-training/trigger-cases.json +30 -0
  128. package/skills/ehs-safety/trigger-cases.json +30 -0
  129. package/skills/email-writing/trigger-cases.json +30 -0
  130. package/skills/eng-ai/trigger-cases.json +30 -0
  131. package/skills/eng-architecture/trigger-cases.json +31 -0
  132. package/skills/eng-automation/trigger-cases.json +31 -0
  133. package/skills/eng-collaboration/trigger-cases.json +30 -0
  134. package/skills/eng-complex-env/trigger-cases.json +30 -0
  135. package/skills/eng-data-platform/trigger-cases.json +31 -0
  136. package/skills/eng-database/trigger-cases.json +31 -0
  137. package/skills/eng-develop/SKILL.md +9 -1
  138. package/skills/eng-develop/references/dod-checklist.md +3 -0
  139. package/skills/eng-develop/references/engineering-workflow.md +37 -0
  140. package/skills/eng-develop/trigger-cases.json +30 -0
  141. package/skills/eng-docs/SKILL.md +24 -0
  142. package/skills/eng-docs/trigger-cases.json +30 -0
  143. package/skills/eng-efficiency/trigger-cases.json +30 -0
  144. package/skills/eng-infra/trigger-cases.json +30 -0
  145. package/skills/eng-integration/trigger-cases.json +31 -0
  146. package/skills/eng-legacy/trigger-cases.json +31 -0
  147. package/skills/eng-observability/trigger-cases.json +31 -0
  148. package/skills/eng-ops/trigger-cases.json +31 -0
  149. package/skills/eng-performance/trigger-cases.json +31 -0
  150. package/skills/eng-planning/trigger-cases.json +31 -0
  151. package/skills/eng-quality/SKILL.md +8 -0
  152. package/skills/eng-quality/trigger-cases.json +31 -0
  153. package/skills/eng-release/trigger-cases.json +31 -0
  154. package/skills/eng-reliability/trigger-cases.json +31 -0
  155. package/skills/eng-requirements/trigger-cases.json +12 -0
  156. package/skills/eng-research/trigger-cases.json +30 -0
  157. package/skills/eng-security/trigger-cases.json +31 -0
  158. package/skills/eng-ux/trigger-cases.json +30 -0
  159. package/skills/engineering-chain/trigger-cases.json +30 -0
  160. package/skills/env-offline/trigger-cases.json +31 -0
  161. package/skills/env-private-deploy/trigger-cases.json +31 -0
  162. package/skills/env-xinchuang/trigger-cases.json +31 -0
  163. package/skills/equipment-tpm/trigger-cases.json +30 -0
  164. package/skills/esg-report/trigger-cases.json +30 -0
  165. package/skills/event-management/trigger-cases.json +30 -0
  166. package/skills/excel-analysis-chain/SKILL.md +17 -4
  167. package/skills/excel-analysis-chain/trigger-cases.json +31 -0
  168. package/skills/explain-codebase/trigger-cases.json +11 -0
  169. package/skills/file-ingest/trigger-cases.json +30 -0
  170. package/skills/finance-analysis/trigger-cases.json +31 -0
  171. package/skills/finance-chain/trigger-cases.json +31 -0
  172. package/skills/financial-modeling/trigger-cases.json +31 -0
  173. package/skills/find-simplifications/SKILL.md +43 -0
  174. package/skills/find-simplifications/trigger-cases.json +23 -0
  175. package/skills/foreign-trade/trigger-cases.json +30 -0
  176. package/skills/funnel-cohort/trigger-cases.json +30 -0
  177. package/skills/gov-document/trigger-cases.json +30 -0
  178. package/skills/gui-change-evidence/SKILL.md +47 -0
  179. package/skills/gui-change-evidence/trigger-cases.json +23 -0
  180. package/skills/hr-chain/trigger-cases.json +31 -0
  181. package/skills/hr-operations/trigger-cases.json +30 -0
  182. package/skills/info-disclosure/trigger-cases.json +31 -0
  183. package/skills/infra-gateway/trigger-cases.json +31 -0
  184. package/skills/infra-k8s/trigger-cases.json +31 -0
  185. package/skills/infra-mq/trigger-cases.json +30 -0
  186. package/skills/insurance-advisory/trigger-cases.json +30 -0
  187. package/skills/internal-audit/trigger-cases.json +30 -0
  188. package/skills/ip-management/trigger-cases.json +31 -0
  189. package/skills/ipo-listing/trigger-cases.json +31 -0
  190. package/skills/it-audit/trigger-cases.json +31 -0
  191. package/skills/itsm-ops/trigger-cases.json +31 -0
  192. package/skills/knowledge-sop/trigger-cases.json +31 -0
  193. package/skills/legal-chain/trigger-cases.json +31 -0
  194. package/skills/legal-contract/trigger-cases.json +30 -0
  195. package/skills/litigation-support/trigger-cases.json +31 -0
  196. package/skills/logistics-warehouse/trigger-cases.json +30 -0
  197. package/skills/long-doc-analysis/trigger-cases.json +31 -0
  198. package/skills/management-accounting/trigger-cases.json +30 -0
  199. package/skills/manufacturing-chain/trigger-cases.json +31 -0
  200. package/skills/manufacturing-quality/trigger-cases.json +30 -0
  201. package/skills/marketing-analytics/trigger-cases.json +31 -0
  202. package/skills/marketing-chain/trigger-cases.json +31 -0
  203. package/skills/meeting-minutes/trigger-cases.json +30 -0
  204. package/skills/metrics-system/trigger-cases.json +31 -0
  205. package/skills/negotiation-prep/trigger-cases.json +30 -0
  206. package/skills/obs-prometheus/trigger-cases.json +31 -0
  207. package/skills/office-automation/trigger-cases.json +31 -0
  208. package/skills/okr-review/trigger-cases.json +30 -0
  209. package/skills/ops-fleet/trigger-cases.json +31 -0
  210. package/skills/ops-linux/trigger-cases.json +31 -0
  211. package/skills/optimization-solver/trigger-cases.json +30 -0
  212. package/skills/patent-disclosure/SKILL.md +105 -0
  213. package/skills/patent-disclosure/references/figures.md +110 -0
  214. package/skills/patent-disclosure/references/outline.md +162 -0
  215. package/skills/patent-disclosure/scripts/patent_docx.py +230 -0
  216. package/skills/patent-disclosure/trigger-cases.json +31 -0
  217. package/skills/patent-drafting/SKILL.md +108 -0
  218. package/skills/patent-drafting/trigger-cases.json +31 -0
  219. package/skills/patent-portfolio/SKILL.md +81 -0
  220. package/skills/patent-portfolio/trigger-cases.json +31 -0
  221. package/skills/pdf-toolkit/trigger-cases.json +31 -0
  222. package/skills/perf-optimize/trigger-cases.json +31 -0
  223. package/skills/pivot-crosstab/trigger-cases.json +30 -0
  224. package/skills/policy-analysis/trigger-cases.json +31 -0
  225. package/skills/portfolio-analysis/trigger-cases.json +31 -0
  226. package/skills/ppt-chain/SKILL.md +14 -6
  227. package/skills/ppt-chain/trigger-cases.json +31 -0
  228. package/skills/pptx-author/SKILL.md +86 -33
  229. package/skills/pptx-author/scripts/__pycache__/pptx_check.cpython-312.pyc +0 -0
  230. package/skills/pptx-author/scripts/__pycache__/pptx_deck.cpython-312.pyc +0 -0
  231. package/skills/pptx-author/scripts/__pycache__/pptx_layout.cpython-312.pyc +0 -0
  232. package/skills/pptx-author/scripts/pptx_battery.py +178 -0
  233. package/skills/pptx-author/scripts/pptx_build.py +119 -0
  234. package/skills/pptx-author/scripts/pptx_check.py +1182 -0
  235. package/skills/pptx-author/scripts/pptx_deck.py +736 -145
  236. package/skills/pptx-author/scripts/pptx_layout.py +384 -0
  237. package/skills/pptx-author/trigger-cases.json +15 -0
  238. package/skills/pr-comms/trigger-cases.json +30 -0
  239. package/skills/procurement-supply/trigger-cases.json +31 -0
  240. package/skills/product-design/trigger-cases.json +31 -0
  241. package/skills/production-planning/trigger-cases.json +31 -0
  242. package/skills/project-management/trigger-cases.json +31 -0
  243. package/skills/project-proposal/trigger-cases.json +31 -0
  244. package/skills/prose-standard/SKILL.md +53 -0
  245. package/skills/prose-standard/trigger-cases.json +23 -0
  246. package/skills/qa-api-automation/trigger-cases.json +31 -0
  247. package/skills/qa-mobile-automation/trigger-cases.json +31 -0
  248. package/skills/qa-ui-automation/trigger-cases.json +31 -0
  249. package/skills/quant-analysis/trigger-cases.json +31 -0
  250. package/skills/rag-triage/SKILL.md +64 -0
  251. package/skills/rag-triage/evals.json +57 -0
  252. package/skills/rag-triage/references/triage-playbook.md +75 -0
  253. package/skills/rag-triage/trigger-cases.json +31 -0
  254. package/skills/real-estate-property/trigger-cases.json +30 -0
  255. package/skills/refactor-safely/SKILL.md +1 -1
  256. package/skills/refactor-safely/trigger-cases.json +30 -0
  257. package/skills/regression-forecast/trigger-cases.json +31 -0
  258. package/skills/release-checklist/trigger-cases.json +31 -0
  259. package/skills/report-craft/SKILL.md +76 -0
  260. package/skills/report-craft/references/feasibility.md +118 -0
  261. package/skills/report-craft/references/review-checklist.md +51 -0
  262. package/skills/report-craft/references/visuals.md +114 -0
  263. package/skills/report-craft/references/writing.md +80 -0
  264. package/skills/report-craft/scripts/__pycache__/chart_render.cpython-312.pyc +0 -0
  265. package/skills/report-craft/scripts/__pycache__/fin_model.cpython-312.pyc +0 -0
  266. package/skills/report-craft/scripts/__pycache__/project_facts.cpython-312.pyc +0 -0
  267. package/skills/report-craft/scripts/chart_render.py +1222 -0
  268. package/skills/report-craft/scripts/fin_model.py +602 -0
  269. package/skills/report-craft/scripts/project_facts.py +580 -0
  270. package/skills/report-craft/scripts/report_battery.py +349 -0
  271. package/skills/report-craft/trigger-cases.json +15 -0
  272. package/skills/retail-store-ops/trigger-cases.json +30 -0
  273. package/skills/risk-compliance/trigger-cases.json +31 -0
  274. package/skills/scope-creep/SKILL.md +45 -0
  275. package/skills/scope-creep/trigger-cases.json +7 -0
  276. package/skills/sec-auth/trigger-cases.json +31 -0
  277. package/skills/sec-compliance/trigger-cases.json +31 -0
  278. package/skills/securities-chain/trigger-cases.json +31 -0
  279. package/skills/securities-research/trigger-cases.json +31 -0
  280. package/skills/security-audit/trigger-cases.json +31 -0
  281. package/skills/slides-html/SKILL.md +25 -10
  282. package/skills/slides-html/trigger-cases.json +11 -0
  283. package/skills/sql-analysis/SKILL.md +110 -0
  284. package/skills/sql-analysis/trigger-cases.json +34 -0
  285. package/skills/sql-tuning/trigger-cases.json +31 -0
  286. package/skills/statistical-testing/trigger-cases.json +31 -0
  287. package/skills/strategy-planning/trigger-cases.json +30 -0
  288. package/skills/supply-chain/trigger-cases.json +31 -0
  289. package/skills/tax-compliance/trigger-cases.json +30 -0
  290. package/skills/tech-doc/SKILL.md +262 -0
  291. package/skills/tech-doc/scripts/__pycache__/archive_battery.cpython-312.pyc +0 -0
  292. package/skills/tech-doc/scripts/__pycache__/archive_build.cpython-312.pyc +0 -0
  293. package/skills/tech-doc/scripts/__pycache__/archive_check.cpython-312.pyc +0 -0
  294. package/skills/tech-doc/scripts/__pycache__/archive_spec.cpython-312.pyc +0 -0
  295. package/skills/tech-doc/scripts/__pycache__/doc_templates.cpython-312.pyc +0 -0
  296. package/skills/tech-doc/scripts/archive_battery.py +860 -0
  297. package/skills/tech-doc/scripts/archive_build.py +599 -0
  298. package/skills/tech-doc/scripts/archive_check.py +1307 -0
  299. package/skills/tech-doc/scripts/archive_spec.py +499 -0
  300. package/skills/tech-doc/scripts/doc_templates.py +624 -0
  301. package/skills/tech-doc/trigger-cases.json +57 -0
  302. package/skills/tech-research/trigger-cases.json +31 -0
  303. package/skills/tender-bidding/trigger-cases.json +31 -0
  304. package/skills/translation-pro/trigger-cases.json +30 -0
  305. package/skills/treasury-mgmt/trigger-cases.json +31 -0
  306. package/skills/trim-cot-leakage/SKILL.md +58 -0
  307. package/skills/trim-cot-leakage/trigger-cases.json +23 -0
  308. package/skills/web-craft/SKILL.md +103 -0
  309. package/skills/web-craft/references/craft-checklist.md +75 -0
  310. package/skills/web-craft/references/design-direction.md +122 -0
  311. package/skills/web-craft/references/h5-mobile.md +100 -0
  312. package/skills/web-craft/references/motion.md +136 -0
  313. package/skills/web-craft/references/recipes.md +74 -0
  314. package/skills/web-craft/templates/wedding-invitation.html +1180 -0
  315. package/skills/web-craft/trigger-cases.json +14 -0
  316. package/skills/web-scraping/trigger-cases.json +34 -0
  317. package/skills/web-search/SKILL.md +11 -10
  318. package/skills/web-search/trigger-cases.json +31 -0
  319. package/skills/weekly-report/SKILL.md +134 -0
  320. package/skills/weekly-report/trigger-cases.json +11 -0
  321. package/skills/write-tests/trigger-cases.json +31 -0
  322. package/skills/xlsx-analysis/SKILL.md +143 -77
  323. package/skills/xlsx-analysis/scripts/__pycache__/data_profile.cpython-312.pyc +0 -0
  324. package/skills/xlsx-analysis/scripts/__pycache__/xlsx_build.cpython-312.pyc +0 -0
  325. package/skills/xlsx-analysis/scripts/__pycache__/xlsx_check.cpython-312.pyc +0 -0
  326. package/skills/xlsx-analysis/scripts/__pycache__/xlsx_layout.cpython-312.pyc +0 -0
  327. package/skills/xlsx-analysis/scripts/__pycache__/xlsx_toolkit.cpython-312.pyc +0 -0
  328. package/skills/xlsx-analysis/scripts/data_profile.py +391 -0
  329. package/skills/xlsx-analysis/scripts/xlsx_battery.py +389 -0
  330. package/skills/xlsx-analysis/scripts/xlsx_build.py +522 -0
  331. package/skills/xlsx-analysis/scripts/xlsx_check.py +924 -0
  332. package/skills/xlsx-analysis/scripts/xlsx_layout.py +235 -0
  333. package/skills/xlsx-analysis/scripts/xlsx_style.py +28 -66
  334. package/skills/xlsx-analysis/scripts/xlsx_toolkit.py +61 -5
  335. package/skills/xlsx-analysis/trigger-cases.json +59 -0
  336. package/squads/hc-defense.md +47 -0
  337. package/squads/hc-rdarchive.md +110 -0
  338. package/squads/hc-redteam.md +50 -0
  339. package/squads/hc-weekly.md +125 -0
  340. package/src/account.js +1 -1
  341. package/src/acp.js +1 -0
  342. package/src/advisor.js +1 -1
  343. package/src/agent.js +1 -1
  344. package/src/api.js +1 -1
  345. package/src/archaeology.js +1 -0
  346. package/src/ask-ui.js +1 -0
  347. package/src/assets-import.js +1 -1
  348. package/src/autofix.js +1 -1
  349. package/src/automation-tools.js +1 -1
  350. package/src/automation.js +1 -1
  351. package/src/autorun.js +1 -1
  352. package/src/banner.js +1 -1
  353. package/src/capability-probe.js +1 -0
  354. package/src/checkpoints.js +1 -1
  355. package/src/code-tools.js +1 -0
  356. package/src/codeindex.js +1 -1
  357. package/src/codemode.js +1 -0
  358. package/src/command-bridge.js +1 -0
  359. package/src/command-ui.js +1 -0
  360. package/src/commands.js +1 -1
  361. package/src/compaction.js +1 -1
  362. package/src/computer.js +1 -1
  363. package/src/config.js +1 -1
  364. package/src/consult.js +1 -1
  365. package/src/context-report.js +1 -1
  366. package/src/context-window.js +1 -0
  367. package/src/context.js +1 -1
  368. package/src/crew.js +1 -0
  369. package/src/crush.js +1 -0
  370. package/src/daemon.js +1 -1
  371. package/src/decisions.js +1 -1
  372. package/src/depaudit.js +1 -0
  373. package/src/device.js +1 -1
  374. package/src/diagram-draw.js +1 -0
  375. package/src/diagram-layouts.js +1 -0
  376. package/src/diagram-raster.js +1 -0
  377. package/src/diagram.js +1 -0
  378. package/src/dictation.js +1 -0
  379. package/src/diffview.js +1 -1
  380. package/src/doctor.js +1 -1
  381. package/src/docx-tools.js +1 -0
  382. package/src/engineering.js +1 -0
  383. package/src/errkind.js +1 -1
  384. package/src/evidence.js +1 -0
  385. package/src/executors.js +1 -1
  386. package/src/expert-catalog.js +1 -1
  387. package/src/experts-store.js +1 -1
  388. package/src/experts.js +1 -1
  389. package/src/external-agents.js +1 -0
  390. package/src/forks.js +1 -1
  391. package/src/fs-observe.js +1 -0
  392. package/src/gitflow.js +1 -1
  393. package/src/goalround.js +1 -0
  394. package/src/hooks.js +1 -1
  395. package/src/ide.js +1 -1
  396. package/src/input.js +1 -1
  397. package/src/insights.js +1 -1
  398. package/src/jobs.js +1 -0
  399. package/src/kbsync.js +1 -1
  400. package/src/lsp.js +1 -0
  401. package/src/mcp-apps.js +1 -0
  402. package/src/mcp.js +1 -1
  403. package/src/memory.js +1 -1
  404. package/src/notify.js +1 -1
  405. package/src/permissions.js +1 -1
  406. package/src/plan-limits.js +1 -0
  407. package/src/policy.js +1 -0
  408. package/src/pptx-tools.js +1 -0
  409. package/src/presence.js +1 -1
  410. package/src/privatedir.js +1 -0
  411. package/src/pyruntime.js +1 -1
  412. package/src/radar.js +1 -0
  413. package/src/render.js +1 -1
  414. package/src/repl.js +1 -1
  415. package/src/report-tools.js +1 -0
  416. package/src/routing.js +1 -0
  417. package/src/sandbox-probe.js +1 -0
  418. package/src/sandbox-win-launcher.mjs +156 -0
  419. package/src/sandbox.js +1 -0
  420. package/src/scan.js +1 -1
  421. package/src/schedule.js +1 -1
  422. package/src/scopecreep.js +1 -0
  423. package/src/sdk.js +1 -0
  424. package/src/selection.js +1 -0
  425. package/src/session-events.js +1 -0
  426. package/src/session.js +1 -1
  427. package/src/sessionindex.js +1 -0
  428. package/src/sessionlog.js +1 -0
  429. package/src/skill-catalog.js +1 -0
  430. package/src/skill-lint.js +1 -0
  431. package/src/skill-optimize.js +1 -0
  432. package/src/skill-scan.js +1 -0
  433. package/src/skills.js +1 -1
  434. package/src/spill.js +1 -0
  435. package/src/squad.js +1 -1
  436. package/src/subagent.js +1 -1
  437. package/src/surface-state.js +1 -0
  438. package/src/tapd.js +1 -0
  439. package/src/terminal-select.js +1 -0
  440. package/src/terminal.js +1 -0
  441. package/src/test-runner.js +1 -0
  442. package/src/todo.js +1 -1
  443. package/src/toolpipe.js +1 -0
  444. package/src/tools.js +1 -1
  445. package/src/toon.js +1 -0
  446. package/src/transcript.js +1 -1
  447. package/src/tui/app.js +1 -1
  448. package/src/tui/command-menu.js +1 -0
  449. package/src/tui/progress.js +1 -0
  450. package/src/tui/scroll.js +1 -0
  451. package/src/tui/welcome.js +1 -0
  452. package/src/tui/workbench.js +1 -0
  453. package/src/ui-spec.js +1 -0
  454. package/src/uitree.js +1 -0
  455. package/src/uninstall.js +1 -1
  456. package/src/vision.js +1 -1
  457. package/src/visual.js +1 -1
  458. package/src/voice-form.js +1 -0
  459. package/src/voice.js +1 -0
  460. package/src/watch.js +1 -1
  461. package/src/web-audit.js +1 -0
  462. package/src/web-cdp.js +1 -0
  463. package/src/web-inspect.js +1 -0
  464. package/src/websearch.js +1 -0
  465. package/src/weekly.js +1 -0
  466. package/src/workflow.js +1 -0
  467. package/src/worklog.js +1 -0
  468. package/src/xlsx-tools.js +1 -0
  469. package/ui/command-picker.js +79 -0
  470. package/skills/ab-testing/scripts/__pycache__/ab_test.cpython-312.pyc +0 -0
  471. package/skills/accounting-ops/scripts/__pycache__/acct_tools.cpython-312.pyc +0 -0
  472. package/skills/anomaly-detection/scripts/__pycache__/anomaly.cpython-312.pyc +0 -0
  473. package/skills/audit-assist/scripts/__pycache__/audit_sampling.cpython-312.pyc +0 -0
  474. package/skills/banking-credit/scripts/__pycache__/credit_metrics.cpython-312.pyc +0 -0
  475. package/skills/data-report-pro/scripts/__pycache__/bigdata.cpython-312.pyc +0 -0
  476. package/skills/data-wrangling/scripts/__pycache__/join_tools.cpython-312.pyc +0 -0
  477. package/skills/ecommerce-ops/scripts/__pycache__/gmv_decompose.cpython-312.pyc +0 -0
  478. package/skills/funnel-cohort/scripts/__pycache__/funnel.cpython-312.pyc +0 -0
  479. package/skills/management-accounting/scripts/__pycache__/costing.cpython-312.pyc +0 -0
  480. package/skills/manufacturing-quality/scripts/__pycache__/spc.cpython-312.pyc +0 -0
  481. package/skills/marketing-analytics/scripts/__pycache__/rfm.cpython-312.pyc +0 -0
  482. package/skills/optimization-solver/scripts/__pycache__/solver.cpython-312.pyc +0 -0
  483. package/skills/pivot-crosstab/scripts/__pycache__/pivot_tools.cpython-312.pyc +0 -0
  484. package/skills/portfolio-analysis/scripts/__pycache__/portfolio.cpython-312.pyc +0 -0
  485. package/skills/procurement-supply/scripts/__pycache__/supply.cpython-312.pyc +0 -0
  486. package/skills/production-planning/scripts/__pycache__/mrp_capacity.cpython-312.pyc +0 -0
  487. package/skills/project-management/scripts/__pycache__/cpm.cpython-312.pyc +0 -0
  488. package/skills/quant-analysis/scripts/__pycache__/quant.cpython-312.pyc +0 -0
  489. package/skills/real-estate-property/scripts/__pycache__/property_calc.cpython-312.pyc +0 -0
  490. package/skills/regression-forecast/scripts/__pycache__/regress.cpython-312.pyc +0 -0
  491. package/skills/securities-research/scripts/__pycache__/valuation.cpython-312.pyc +0 -0
  492. package/skills/statistical-testing/scripts/__pycache__/stat_tests.cpython-312.pyc +0 -0
  493. package/skills/treasury-mgmt/scripts/__pycache__/cashflow_forecast.cpython-312.pyc +0 -0
@@ -0,0 +1,695 @@
1
+ # Java 专属体检:语言陷阱 + 框架(Spring / MyBatis / 分片)用法。
2
+ #
3
+ # ── 为什么 Java 要单独一份 ──
4
+ # 通用规则(函数过长、空捕获、注释与实现不符)对所有语言都成立,Java 也不例外。
5
+ # 但 Java 真正会出事的地方,几乎都是**通用规则看不见的**:
6
+ #
7
+ # @Transactional 写在 private 方法上 —— 编译通过、运行不报错、事务根本不生效,
8
+ # 直到某天一半数据写进去一半没写;
9
+ # new BigDecimal(0.1) —— 结果是 0.1000000000000000055511151231257827…,
10
+ # 对账时差几分钱,查三天;
11
+ # 同类内调用 @Transactional 方法 —— 不走代理,注解等于没写;
12
+ # static SimpleDateFormat —— 单线程测试永远正常,压测时日期串成乱码;
13
+ # Integer == Integer —— 128 以内因为缓存是对的,128 以上突然为 false。
14
+ #
15
+ # 这些的共同点是:**编译器不管、单测容易漏、出事时离现场很远**。
16
+ # 正是最该交给机器每次都查一遍的那一类。
17
+ #
18
+ # ── 分组 ──
19
+ # logic.java-* 语言陷阱(拆箱、精度、并发、资源、集合)
20
+ # framework.* 框架用法(Spring 事务/注入/异步、MyBatis、分片表)
21
+ # comment.javadoc-* Javadoc 与签名不符
22
+ # test.junit-* JUnit / Mockito
23
+ #
24
+ # 误报纪律与主引擎一致:所有模式在掩码视图上跑;每条规则都要在真实 Spring 代码上验过。
25
+
26
+ import os
27
+ import re
28
+ import sys
29
+
30
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
31
+
32
+ # ── 语言陷阱 ────────────────────────────────────────────────────────────────
33
+ _RE_PRINT_STACK = re.compile(r'\.printStackTrace\s*\(\s*\)')
34
+ _RE_BROAD_CATCH = re.compile(r'catch\s*\(\s*(?:final\s+)?(Exception|Throwable|RuntimeException)\s+(\w+)\s*\)')
35
+ _RE_BIGDECIMAL_DOUBLE = re.compile(r'new\s+BigDecimal\s*\(\s*(?!"|\')([\w.]*\d[\w.]*|\w+)\s*\)')
36
+ _RE_BIGDECIMAL_EQ = re.compile(r'\b(\w+)\s*\.\s*equals\s*\(\s*(\w+)\s*\)')
37
+ # 需要显式关闭的资源:不在 try-with-resources 里 new 出来就有泄漏风险
38
+ # `(?:[\w.]+\.)?` 这一段不能省:真实代码里写全限定名(`new java.io.FileInputStream(...)`)很常见,
39
+ # 少了它这条规则在一半的场景下静默不响,而报告照样显示「已体检」。
40
+ _RE_CLOSEABLE = re.compile(
41
+ r'\bnew\s+(?:[\w.]+\.)?(FileInputStream|FileOutputStream|FileReader|FileWriter'
42
+ r'|BufferedReader|BufferedWriter|RandomAccessFile|Socket|ServerSocket'
43
+ r'|ZipFile|ZipInputStream|ZipOutputStream|Scanner)\s*\(')
44
+ _RE_ASLIST = re.compile(r'\bArrays\s*\.\s*asList\s*\(')
45
+ _RE_SWITCH = re.compile(r'\bswitch\s*\(')
46
+ _RE_INT_DIV = re.compile(r'\b(?:double|float)\s+\w+\s*=\s*([A-Za-z_$][\w$.]*|\d+)\s*/\s*([A-Za-z_$][\w$.]*|\d+)\s*[;)]')
47
+ _RE_BOXED_EQ = re.compile(
48
+ r'\b([a-z]\w*(?:Id|Num|Count|Size|Code|Type|Status|Amount|Age|Level|No)?)\s*(==|!=)\s*'
49
+ r'([a-z]\w*(?:Id|Num|Count|Size|Code|Type|Status|Amount|Age|Level|No)?)\b')
50
+ _BOXED_TYPES = ('Integer', 'Long', 'Short', 'Byte', 'Character', 'Double', 'Float', 'Boolean')
51
+
52
+
53
+ def _line_of(lens, off):
54
+ return lens.text.count('\n', 0, off) + 1
55
+
56
+
57
+ def _unit_at(lens, line):
58
+ u = lens.unit_at(line)
59
+ return u.label if u else None
60
+
61
+
62
+ def check_java_traps(lens, ctx, out, add):
63
+ """Java 语言层面的坑:编译器不管、单测容易漏、出事时离现场很远的那些。"""
64
+ code = lens.code
65
+
66
+ # ── e.printStackTrace():打到 stderr,日志系统收不到,线上等于没记 ──
67
+ for m in _RE_PRINT_STACK.finditer(code):
68
+ ln = _line_of(lens, m.start())
69
+ add(out, ctx, 'logic.java-print-stack-trace', 'warn', lens, ln,
70
+ '用 printStackTrace() 打异常:它写到 stderr,不进日志系统,线上查不到也报不了警',
71
+ '改成 log.error("做什么失败, 关键参数={}", 参数, e)——异常对象要作为最后一个参数传进去,'
72
+ '否则堆栈会丢', _unit_at(lens, ln))
73
+
74
+ # ── 捕获范围过宽 ──
75
+ # 这条规则最初写成「catch (Exception) 且块里没重抛没记日志就报」,在真实 Spring 代码上
76
+ # 报出 67 条、无一为真:`catch (Exception ignore) { // 非 JSON,退化为分隔拆分 }`
77
+ # 是有意识的降级,不是疏忽。而「捕获了什么都不做」本来就归 logic.empty-catch 管。
78
+ #
79
+ # 所以改成只盯两个**真正的**风险点:
80
+ # ① catch (Throwable):连 OutOfMemoryError / StackOverflowError 一起吞,几乎从不是对的;
81
+ # ② catch (Exception) 罩住一大段代码:业务异常和 NPE、类型转换错误被拉平成一种,
82
+ # 出事时既分不清是哪一类,也定位不到是哪一行。
83
+ for m in _RE_BROAD_CATCH.finditer(code):
84
+ ln = _line_of(lens, m.start())
85
+ kind, var = m.group(1), m.group(2)
86
+ brace = code.find('{', m.end())
87
+ if brace < 0:
88
+ continue
89
+ # try 块有多大:从 catch 往回找配对的 `try {`
90
+ try_head = code.rfind('try', max(0, m.start() - 6000), m.start())
91
+ try_lines = code.count('\n', try_head, m.start()) if try_head >= 0 else 0
92
+ if kind == 'Throwable':
93
+ add(out, ctx, 'logic.java-catch-throwable', 'warn', lens, ln,
94
+ f'`catch (Throwable {var})` 会连 Error 一起捕获'
95
+ '(OutOfMemoryError、StackOverflowError 这些,JVM 已经处于不可靠状态)',
96
+ '改成 catch (Exception);真要兜住 Error 只应发生在最外层线程边界,'
97
+ '且必须记日志后让进程退出,而不是继续跑', _unit_at(lens, ln))
98
+ continue
99
+ if try_lines >= 25:
100
+ add(out, ctx, 'logic.java-broad-catch', 'warn', lens, ln,
101
+ f'`catch ({kind} {var})` 罩住了约 {try_lines} 行代码:'
102
+ '业务异常与 NPE、类型转换错误被拉平成同一种,出事时分不清是哪一类、也定位不到哪一行',
103
+ '把 try 的范围缩到真正会抛的那几行;或按异常类型分别捕获,'
104
+ '让调用方能据此决定重试、降级还是直接失败', _unit_at(lens, ln))
105
+
106
+ # ── new BigDecimal(double):二进制浮点的误差会原样带进金额 ──
107
+ for m in _RE_BIGDECIMAL_DOUBLE.finditer(code):
108
+ arg = m.group(1)
109
+ ln = _line_of(lens, m.start())
110
+ if re.fullmatch(r'\d+', arg) or arg.endswith('L'):
111
+ continue # 整数字面量没有精度问题
112
+ add(out, ctx, 'logic.java-bigdecimal-double', 'error', lens, ln,
113
+ f'`new BigDecimal({arg})` 用 double 构造:0.1 会变成 0.1000000000000000055511151231257827',
114
+ '改用 `BigDecimal.valueOf(x)` 或 `new BigDecimal("0.1")`(字符串构造)——'
115
+ '金额差几分钱就是对不上账', _unit_at(lens, ln))
116
+
117
+ # ── BigDecimal.equals:值相同但标度不同会判不等(2.0 != 2.00) ──
118
+ for m in _RE_BIGDECIMAL_EQ.finditer(code):
119
+ ln = _line_of(lens, m.start())
120
+ line_txt = lens.line(ln)
121
+ names = (m.group(1) + m.group(2)).lower()
122
+ if not re.search(r'(amount|price|money|fee|cost|balance|total|decimal|rate|tax|金额)', names):
123
+ continue
124
+ if 'BigDecimal' not in line_txt and not re.search(r'(amount|price|money|fee|balance)', names):
125
+ continue
126
+ add(out, ctx, 'logic.java-bigdecimal-equals', 'warn', lens, ln,
127
+ 'BigDecimal 用 equals 比较:它同时比较值和标度,`2.0` 与 `2.00` 会判为不等',
128
+ '改用 `a.compareTo(b) == 0`', _unit_at(lens, ln))
129
+
130
+ # ── 装箱类型用 == 比较:128 以内因缓存为真,128 以上突然为假 ──
131
+ boxed_vars = {f['name'] for f in lens.fields if f['type'].split('<')[0] in _BOXED_TYPES}
132
+ for m in _RE_BOXED_EQ.finditer(code):
133
+ left, right = m.group(1), m.group(3)
134
+ if left not in boxed_vars and right not in boxed_vars:
135
+ continue
136
+ if 'null' in (left, right):
137
+ continue
138
+ ln = _line_of(lens, m.start())
139
+ add(out, ctx, 'logic.java-unboxing-eq', 'error', lens, ln,
140
+ f'`{left} {m.group(2)} {right}` 比较的是装箱对象的引用,不是数值'
141
+ '——-128~127 因为有缓存会"碰巧正确",超出这个范围突然为 false',
142
+ '改用 `equals()`,或 `Objects.equals(a, b)`(同时防 NPE)', _unit_at(lens, ln))
143
+
144
+ # ── static SimpleDateFormat:它不是线程安全的,压测时日期会串 ──
145
+ for f in lens.fields:
146
+ if 'static' not in f['modifiers']:
147
+ continue
148
+ base = f['type'].split('<')[0]
149
+ if base in ('SimpleDateFormat', 'DateFormat', 'Calendar'):
150
+ add(out, ctx, 'logic.java-shared-dateformat', 'error', lens, f['line'],
151
+ f'`static {base} {f["name"]}` —— 它**不是线程安全的**,'
152
+ '并发时会解析出错误日期甚至抛异常,而单线程测试永远正常',
153
+ '换成 `DateTimeFormatter`(线程安全,java.time 包),'
154
+ '或者每次用时 new 一个;真要复用就套 ThreadLocal', f['owner'])
155
+
156
+ # ── 需要 close 的资源没走 try-with-resources ──
157
+ for m in _RE_CLOSEABLE.finditer(code):
158
+ ln = _line_of(lens, m.start())
159
+ # 往前找最近的 try:`try (` 开头就是 try-with-resources
160
+ head = code.rfind('try', max(0, m.start() - 300), m.start())
161
+ if head >= 0 and re.match(r'try\s*\(', code[head:head + 8]):
162
+ continue
163
+ if re.search(r'\breturn\s+new\s+', lens.line(ln)):
164
+ continue # 造出来交给调用方关,责任不在这里
165
+ add(out, ctx, 'logic.java-resource-leak', 'warn', lens, ln,
166
+ f'`{m.group(1)}` 在 try-with-resources 之外创建,异常路径上不会被关闭',
167
+ '改成 `try (var in = new ...()) { … }`——句柄泄漏平时看不出来,'
168
+ '到达上限时整个服务一起打不开文件', _unit_at(lens, ln))
169
+
170
+ # ── Arrays.asList(...) 之后又改它:它返回的是定长视图,add/remove 直接抛 ──
171
+ for m in _RE_ASLIST.finditer(code):
172
+ ln = _line_of(lens, m.start())
173
+ var = re.search(r'(\w+)\s*=\s*Arrays\s*\.\s*asList', lens.line(ln))
174
+ if not var:
175
+ continue
176
+ name = var.group(1)
177
+ tail = code[m.end():m.end() + 1200]
178
+ if re.search(rf'\b{re.escape(name)}\s*\.\s*(add|remove|clear|set|sort)\s*\(', tail):
179
+ add(out, ctx, 'logic.java-aslist-modify', 'error', lens, ln,
180
+ f'`{name}` 来自 `Arrays.asList`,它是定长视图,'
181
+ '对它 add/remove 会抛 UnsupportedOperationException',
182
+ '要可变列表就包一层:`new ArrayList<>(Arrays.asList(...))`', _unit_at(lens, ln))
183
+
184
+ # ── for-each 里增删集合:ConcurrentModificationException ──
185
+ for m in re.finditer(r'for\s*\(\s*(?:final\s+)?[\w<>\[\],.\s?]+\s+(\w+)\s*:\s*(\w+)\s*\)', code):
186
+ coll = m.group(2)
187
+ brace = code.find('{', m.end())
188
+ if brace < 0 or brace - m.end() > 4:
189
+ continue
190
+ depth, end = 0, brace
191
+ for i in range(brace, min(len(code), brace + 4000)):
192
+ if code[i] == '{':
193
+ depth += 1
194
+ elif code[i] == '}':
195
+ depth -= 1
196
+ if depth == 0:
197
+ end = i
198
+ break
199
+ body = code[brace:end]
200
+ if re.search(rf'\b{re.escape(coll)}\s*\.\s*(add|remove|clear)\s*\(', body):
201
+ ln = _line_of(lens, m.start())
202
+ add(out, ctx, 'logic.java-concurrent-modify', 'error', lens, ln,
203
+ f'在 for-each 遍历 `{coll}` 的同时增删它,会抛 ConcurrentModificationException',
204
+ '用 `Iterator.remove()`,或 `removeIf(...)`,或先收集再统一处理', _unit_at(lens, ln))
205
+
206
+ # ── 循环里 String += :每次都新建对象,n 大了就是 O(n²) ──
207
+ for m in re.finditer(r'\b(?:for|while)\s*\([^)]*\)\s*\{', code):
208
+ brace = m.end() - 1
209
+ depth, end = 0, brace
210
+ for i in range(brace, min(len(code), brace + 3000)):
211
+ if code[i] == '{':
212
+ depth += 1
213
+ elif code[i] == '}':
214
+ depth -= 1
215
+ if depth == 0:
216
+ end = i
217
+ break
218
+ body = code[brace:end]
219
+ hit = re.search(r'\b(\w+)\s*\+=\s*(?!\d)', body)
220
+ if not hit:
221
+ continue
222
+ name = hit.group(1)
223
+ decl = re.search(rf'\bString\s+{re.escape(name)}\b', code[:m.start()])
224
+ if not decl:
225
+ continue
226
+ ln = _line_of(lens, m.start()) + body[:hit.start()].count('\n')
227
+ add(out, ctx, 'logic.java-concat-in-loop', 'warn', lens, ln,
228
+ f'循环里对 String `{name}` 做 `+=`:每次都新建一个对象并复制全部内容,'
229
+ '循环次数一多就是 O(n²)',
230
+ '循环外建 `StringBuilder`,循环里 `append`,出循环再 `toString()`', _unit_at(lens, ln))
231
+
232
+ # ── 整数相除赋给浮点:先按整数截断,再转成 double,小数部分早没了 ──
233
+ for m in _RE_INT_DIV.finditer(code):
234
+ ln = _line_of(lens, m.start())
235
+ if '.' in m.group(1) or '.' in m.group(2):
236
+ continue
237
+ add(out, ctx, 'logic.java-int-division', 'warn', lens, ln,
238
+ f'`{m.group(1)} / {m.group(2)}` 两边都是整数,会先做整数除法截断,再转成浮点'
239
+ '(3/2 得到的是 1.0 不是 1.5)',
240
+ '给一边加 `* 1.0` 或强转 `(double)`;算百分比/占比时这一条几乎总是 bug',
241
+ _unit_at(lens, ln))
242
+
243
+ # ── switch 没有 default:新增枚举值时静默什么都不做 ──
244
+ for m in _RE_SWITCH.finditer(code):
245
+ brace = code.find('{', m.end())
246
+ if brace < 0:
247
+ continue
248
+ depth, end = 0, brace
249
+ for i in range(brace, min(len(code), brace + 4000)):
250
+ if code[i] == '{':
251
+ depth += 1
252
+ elif code[i] == '}':
253
+ depth -= 1
254
+ if depth == 0:
255
+ end = i
256
+ break
257
+ if 'default' not in code[brace:end]:
258
+ ln = _line_of(lens, m.start())
259
+ add(out, ctx, 'logic.java-switch-no-default', 'warn', lens, ln,
260
+ 'switch 没有 default 分支:将来枚举加了新值,这里会静默地什么都不做',
261
+ '补 default——即使只是抛 IllegalArgumentException 或记一条日志,'
262
+ '也好过让未知取值悄悄走过去', _unit_at(lens, ln))
263
+
264
+ # ── Optional.get() 不先判存在:等同于用一个更麻烦的方式抛 NPE ──
265
+ # 先把本文件里所有声明成 Optional 的名字收齐(方法参数、局部变量、字段都在内)——
266
+ # 只看 `.get()` 所在那一行是找不到类型的,类型几乎总是声明在别处。
267
+ opt_vars = set(re.findall(r'\bOptional\s*<[^>]*>\s+(\w+)', code))
268
+ opt_vars |= {f['name'] for f in lens.fields if f['type'].split('<')[0] == 'Optional'}
269
+ # 返回 Optional 的常见方法,链式调用 `.findById(x).get()` 同样要管
270
+ chained = re.compile(r'\.\s*(findById|findOne|findFirst|findAny|reduce|max|min|'
271
+ r'stream\(\)[^;]*?)\s*\([^;]*\)\s*\.\s*get\s*\(\s*\)')
272
+ for m in list(re.finditer(r'\b(\w+)\s*\.\s*get\s*\(\s*\)', code)) + list(chained.finditer(code)):
273
+ ln = _line_of(lens, m.start())
274
+ line_txt = lens.line(ln)
275
+ name = m.group(1) if m.re.groups and m.lastindex else ''
276
+ is_opt = name in opt_vars or 'Optional' in line_txt or m.re is chained
277
+ if not is_opt:
278
+ continue
279
+ if re.search(r'(isPresent|isEmpty|orElse|ifPresent|orElseThrow)', line_txt):
280
+ continue
281
+ prev = lens.line(ln - 1) + lens.line(ln - 2)
282
+ if 'isPresent' in prev or 'isEmpty' in prev:
283
+ continue
284
+ add(out, ctx, 'logic.java-optional-get', 'warn', lens, ln,
285
+ '`Optional.get()` 没有先判存在,空值时抛 NoSuchElementException',
286
+ '用 `orElse(默认值)` / `orElseThrow(() -> new 业务异常())` / `ifPresent(...)`——'
287
+ '把"取不到怎么办"显式写出来', _unit_at(lens, ln))
288
+
289
+ # ── finally 里 return:会吞掉 try 块里正在抛的异常 ──
290
+ for m in re.finditer(r'\bfinally\s*\{', code):
291
+ brace = m.end() - 1
292
+ depth, end = 0, brace
293
+ for i in range(brace, min(len(code), brace + 2000)):
294
+ if code[i] == '{':
295
+ depth += 1
296
+ elif code[i] == '}':
297
+ depth -= 1
298
+ if depth == 0:
299
+ end = i
300
+ break
301
+ if re.search(r'\breturn\b|\bthrow\b', code[brace:end]):
302
+ ln = _line_of(lens, m.start())
303
+ add(out, ctx, 'logic.java-finally-return', 'error', lens, ln,
304
+ 'finally 块里 return/throw:会把 try 块中正在抛出的异常直接丢弃,故障现场就此消失',
305
+ '把 finally 限制成只做资源清理;要改返回值就在 try 里改', _unit_at(lens, ln))
306
+
307
+ # ── 重写了 equals 却没重写 hashCode:放进 HashMap/HashSet 会找不到 ──
308
+ for cls in lens.classes():
309
+ methods = [u for u in lens.functions() if u.owner == cls.name]
310
+ has_eq = any(u.name == 'equals' for u in methods)
311
+ has_hash = any(u.name == 'hashCode' for u in methods)
312
+ if has_eq and not has_hash:
313
+ add(out, ctx, 'logic.java-equals-no-hashcode', 'error', lens, cls.start,
314
+ f'`{cls.name}` 重写了 equals 但没有重写 hashCode',
315
+ '两者必须成对:只改 equals 的话,两个"相等"的对象散列到不同的桶,'
316
+ 'HashMap/HashSet 里放进去就再也取不出来', cls.name)
317
+ if has_hash and not has_eq:
318
+ add(out, ctx, 'logic.java-equals-no-hashcode', 'warn', lens, cls.start,
319
+ f'`{cls.name}` 重写了 hashCode 但没有重写 equals',
320
+ '两者必须成对,否则散列到同一个桶之后仍然会被判为不同对象', cls.name)
321
+
322
+
323
+ # ── Spring / 持久层 ────────────────────────────────────────────────────────
324
+ # 这一组的共同点:**注解写了、编译过了、运行不报错,但它根本没生效**。
325
+ # 事务不回滚、异步没异步、注入是 null——都要等到线上某次异常路径才暴露,
326
+ # 而那时离写下这行代码已经过去几个月。编译器不管的事,就该机器每次都查。
327
+ _BEAN_ANNOS = ('@Service', '@Component', '@Repository', '@Controller', '@RestController',
328
+ '@Configuration', '@Mapper')
329
+ _RE_VALUE_ANNO = re.compile(r'@Value\s*\(\s*["\']\$\{([^}:]+)(:[^}]*)?\}["\']\s*\)')
330
+ _RE_MAPPING = re.compile(r'@(RequestMapping)\s*\(([^)]*)\)')
331
+ # MyBatis:${} 是字符串替换(注入面),#{} 才是预编译参数
332
+ _RE_MYBATIS_DOLLAR = re.compile(r'\$\{([A-Za-z_][\w.]*)\}')
333
+ _RE_SQL_IN_ANNO = re.compile(r'@(Select|Insert|Update|Delete)\s*\(')
334
+
335
+
336
+ def _class_of(lens, line):
337
+ """这一行落在哪个类里。"""
338
+ best = None
339
+ for c in lens.classes():
340
+ if c.start <= line <= c.end and (best is None or c.start > best.start):
341
+ best = c
342
+ return best
343
+
344
+
345
+ def check_java_framework(lens, ctx, out, add):
346
+ """Spring 事务/注入/异步 与 MyBatis、分片表的用法检查。"""
347
+ code = lens.code
348
+ methods = lens.functions()
349
+ tx_methods = {u.name for u in methods if u.has_annotation('Transactional')}
350
+ async_methods = {u.name for u in methods if u.has_annotation('Async')}
351
+ cached_methods = {u.name for u in methods if u.has_annotation('Cacheable', 'CachePut', 'CacheEvict')}
352
+
353
+ for u in methods:
354
+ anno_tx = u.has_annotation('Transactional')
355
+
356
+ # ── @Transactional 落在 private / final / static 上:AOP 代理根本包不住它 ──
357
+ if anno_tx and ({'private', 'final', 'static'} & set(u.modifiers)):
358
+ bad = sorted({'private', 'final', 'static'} & set(u.modifiers))
359
+ add(out, ctx, 'framework.tx-not-effective', 'error', lens, u.start,
360
+ f'`{u.label}` 是 {"/".join(bad)} 方法,但标了 @Transactional —— '
361
+ 'Spring 靠动态代理织入事务,代理包不住这三种方法,**注解完全不生效**',
362
+ '改成 public 非 final 非 static;真需要私有就把事务边界上移到调用它的 public 方法上。'
363
+ '这类问题编译不报错、平时也看不出来,直到某次异常时数据写了一半', u.label)
364
+
365
+ # ── @Transactional 只声明在类上 / 方法 throws 受检异常却没写 rollbackFor ──
366
+ if anno_tx and u.throws:
367
+ decl = ' '.join(str(a) for a in u.decorators)
368
+ checked = [t for t in u.throws if not re.search(r'Runtime|Unchecked', t)]
369
+ if checked and 'rollbackFor' not in decl:
370
+ add(out, ctx, 'framework.tx-no-rollback-for', 'error', lens, u.start,
371
+ f'`{u.label}` 声明抛出受检异常 {"、".join(checked)},但 @Transactional 没写 rollbackFor —— '
372
+ '**Spring 默认只对 RuntimeException 回滚**,抛受检异常时事务照常提交',
373
+ '写成 `@Transactional(rollbackFor = Exception.class)`;'
374
+ '这是所有事务陷阱里最安静的一个——数据写进去了,异常也抛出来了,两边都"正常"', u.label)
375
+
376
+ # ── @Transactional 方法里把异常吞了:异常不往外抛,事务就不回滚 ──
377
+ if anno_tx:
378
+ for cm in re.finditer(r'\bcatch\s*\(([^)]*)\)\s*\{', u.body):
379
+ brace = cm.end() - 1
380
+ depth, end = 0, brace
381
+ for i in range(brace, len(u.body)):
382
+ if u.body[i] == '{':
383
+ depth += 1
384
+ elif u.body[i] == '}':
385
+ depth -= 1
386
+ if depth == 0:
387
+ end = i
388
+ break
389
+ blk = u.body[brace:end]
390
+ if re.search(r'\bthrow\b|setRollbackOnly', blk):
391
+ continue
392
+ ln = u.line_at(cm.start())
393
+ add(out, ctx, 'framework.tx-swallow', 'error', lens, ln,
394
+ f'`{u.label}` 带 @Transactional,却在方法内 catch 住异常且不再抛出 —— '
395
+ '异常不出方法,事务就**不会回滚**,前面写进去的数据会留下来',
396
+ '要么重新抛出(或包成运行时异常),要么显式 '
397
+ '`TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()`;'
398
+ '想"忽略这个异常并继续提交"就必须在注释里写明是刻意的', u.label)
399
+
400
+ # ── 同类内自调用 @Transactional / @Async / @Cacheable:不过代理,注解等于没写 ──
401
+ for target, kind, why in ((tx_methods, '@Transactional', '事务不会开启'),
402
+ (async_methods, '@Async', '不会异步执行,仍在当前线程同步跑完'),
403
+ (cached_methods, '@Cacheable', '缓存不生效,每次都真的执行')):
404
+ for name in target:
405
+ if name == u.name:
406
+ continue
407
+ if not re.search(rf'(?<![\w.]){re.escape(name)}\s*\(', u.body) and \
408
+ not re.search(rf'\bthis\s*\.\s*{re.escape(name)}\s*\(', u.body):
409
+ continue
410
+ ln = u.start
411
+ add(out, ctx, 'framework.self-invocation', 'error', lens, ln,
412
+ f'`{u.label}` 在**同一个类内**直接调用了带 {kind} 的 `{name}()` —— '
413
+ f'这样走的是 this 引用而不是 Spring 代理,{why}',
414
+ '把被调方法挪到另一个 Bean 再注入进来调用;'
415
+ '或注入自身代理(@Lazy 自注入 / AopContext.currentProxy())。'
416
+ '这个坑的隐蔽之处在于:单独调那个方法时一切正常,只有内部调用这条路径是坏的', u.label)
417
+ break
418
+
419
+ # ── @RequestMapping 不限定 method:一个地址同时接受 GET 与 POST ──
420
+ for am in _RE_MAPPING.finditer(' '.join(str(a) for a in u.decorators)):
421
+ if 'method' not in am.group(2) and '…' not in am.group(2):
422
+ add(out, ctx, 'framework.mapping-no-method', 'warn', lens, u.start,
423
+ f'`{u.label}` 用 @RequestMapping 却没限定 method,该地址会同时接受 GET/POST/PUT/DELETE',
424
+ '改用 @GetMapping / @PostMapping 等语义化注解;'
425
+ '写操作被 GET 打开意味着它能被浏览器预取、被爬虫触发', u.label)
426
+
427
+ # ── 字段注入:拿不到编译期的依赖检查,测试里也没法直接构造 ──
428
+ cls_is_bean = {}
429
+ for c in lens.classes():
430
+ cls_is_bean[c.name] = any(str(a).split('(')[0] in _BEAN_ANNOS for a in c.decorators)
431
+ for f in lens.fields:
432
+ annos = [str(a).split('(')[0] for a in f['annotations']]
433
+ if '@Autowired' in annos or '@Resource' in annos or '@Inject' in annos:
434
+ add(out, ctx, 'framework.field-injection', 'warn', lens, f['line'],
435
+ f'`{f["owner"]}.{f["name"]}` 用字段注入 —— 依赖关系不出现在构造器上,'
436
+ '写单测时没法直接 new 出来,循环依赖也要等到启动才暴露',
437
+ '改成构造器注入(配合 final):依赖一目了然、对象一建好就完整、测试可以直接传替身进去。'
438
+ 'Lombok 项目加 @RequiredArgsConstructor 即可,改动很小', f['owner'])
439
+ # ── @Value 不给默认值:配置漏配时整个应用起不来,且报错很难懂 ──
440
+ for a in f['annotations']:
441
+ vm = _RE_VALUE_ANNO.search(str(a))
442
+ if vm and not vm.group(2):
443
+ add(out, ctx, 'framework.value-no-default', 'info', lens, f['line'],
444
+ f'`@Value("${{{vm.group(1)}}}")` 没有默认值,这一项漏配时应用启动会直接失败',
445
+ f'写成 `${{{vm.group(1)}:合理的默认值}}`;确实必须由部署方提供的,'
446
+ '就在文档与配置样例里标成必填项', f['owner'])
447
+
448
+ # ── 单例 Bean 里的可变实例字段:多线程共享同一份状态 ──
449
+ # 配置类天生就该有可变字段——Spring 启动时按配置往里注值,那正是它的工作方式。
450
+ # 不排掉它们,这条规则在一个真实后端上报出 82 条,其中绝大多数是 @ConfigurationProperties。
451
+ conf_classes = set()
452
+ for c in lens.classes():
453
+ annos = ' '.join(str(a) for a in c.decorators)
454
+ if 'ConfigurationProperties' in annos or 'ConfigMap' in annos:
455
+ conf_classes.add(c.name)
456
+ if re.search(r'(Properties|Config|Configuration|Settings|Options|Props)$', c.name):
457
+ conf_classes.add(c.name)
458
+ for f in lens.fields:
459
+ if not cls_is_bean.get(f['owner']) or f['owner'] in conf_classes:
460
+ continue
461
+ mods = set(f['modifiers'])
462
+ # volatile 与 final/static 一样,是作者**已经意识到**这是共享状态的标记:
463
+ # `private volatile Map<..> snapshot = emptyMap()` 是标准的快照缓存写法,不是疏忽。
464
+ # 报它等于在指责一段深思熟虑的代码,这类误报最伤工具的可信度。
465
+ if mods & {'static', 'final', 'volatile'}:
466
+ continue
467
+ if any('Value' in str(a) or 'ConfigurationProperties' in str(a) for a in f['annotations']):
468
+ continue # 由配置注入的字段,同上
469
+ base = f['type'].split('<')[0]
470
+ # 接口类型必须一起收:字段几乎总是声明成 `List<X>` / `Map<K,V>` 而不是 ArrayList/HashMap,
471
+ # 只列具体类的话,这条规则在最常见的写法上静默不响。
472
+ mutable = base in ('List', 'Map', 'Set', 'Collection', 'Queue', 'Deque', 'Iterable',
473
+ 'ArrayList', 'HashMap', 'HashSet', 'LinkedList', 'StringBuilder',
474
+ 'StringBuffer', 'ArrayDeque', 'LinkedHashMap', 'TreeMap', 'TreeSet')
475
+ counter = base in ('int', 'long', 'Integer', 'Long', 'double', 'Double') and bool(f['init'])
476
+ if not (mutable or counter):
477
+ continue
478
+ add(out, ctx, 'framework.singleton-mutable-state', 'warn', lens, f['line'],
479
+ f'`{f["owner"]}` 是单例 Bean,字段 `{f["name"]}`({base})是可变的实例状态 —— '
480
+ '所有请求线程共用同一份,并发下会互相看见对方的数据',
481
+ '把状态挪进方法局部变量;确实要跨请求共享就用线程安全容器'
482
+ '(ConcurrentHashMap / AtomicLong)并说明为什么需要共享', f['owner'])
483
+
484
+ # ── MyBatis ${} 拼接:它是字符串替换,不是预编译参数 ──
485
+ for m in _RE_MYBATIS_DOLLAR.finditer(lens.text):
486
+ if lens.in_literal(m.start()) is False:
487
+ continue # 只看写在字符串里的 SQL 片段
488
+ ln = _line_of(lens, m.start())
489
+ line_txt = lens.line(ln)
490
+ if not re.search(r'(select|insert|update|delete|from|where|order\s+by|group\s+by)', line_txt, re.I):
491
+ continue # 不是 SQL 上下文(日志模板、路径模板里的 ${} 无害)
492
+ add(out, ctx, 'framework.mybatis-dollar', 'error', lens, ln,
493
+ f'SQL 里用了 `${{{m.group(1)}}}` —— 它是**字符串替换**,参数被原样拼进语句,是注入入口',
494
+ '值一律用 `#{}`(预编译占位符)。表名/列名/排序方向确实不能预编译,'
495
+ '那就用白名单把取值限死,绝不能让调用方直接决定', _unit_at(lens, ln))
496
+
497
+
498
+ # ── Javadoc 与 JUnit ───────────────────────────────────────────────────────
499
+ _RE_DOC_THROWS = re.compile(r'@throws\s+([A-Za-z_$][\w$.]*)')
500
+ _RE_ASSERT_EQ = re.compile(r'\bassert(?:Equals|Same|ArrayEquals)\s*\(')
501
+
502
+
503
+ # 构造「期望值」的常见工厂——它们带括号,但绝不是「被测代码算出来的结果」
504
+ _LITERAL_FACTORY = re.compile(
505
+ r'^(List|Map|Set|Arrays|Collections|Optional|BigDecimal|BigInteger|Integer|Long|Double|'
506
+ r'String|Instant|Duration|LocalDate\w*|LocalDateTime|UUID)\s*\.\s*'
507
+ r'(of|ofEntries|asList|valueOf|empty\w*|singleton\w*|emptyList|emptyMap|parse|ofPattern|ofSeconds)')
508
+
509
+
510
+ def _looks_literal(arg):
511
+ """这个实参像不像「写死的期望值」。"""
512
+ arg = arg.strip()
513
+ if not arg:
514
+ return False
515
+ if re.match(r'^["\']|^-?\d|^(true|false|null)$', arg):
516
+ return True # 字面量
517
+ if _LITERAL_FACTORY.match(arg):
518
+ return True # List.of(...) 这类
519
+ if re.match(r'^new\s+\w', arg):
520
+ return True # new ExpectedDto(...)
521
+ if re.match(r'^[A-Z][A-Z0-9_]*$', arg):
522
+ return True # 常量
523
+ return False
524
+
525
+
526
+ def _first_arg(code, lparen):
527
+ """取调用的第一个实参(按顶层逗号切)。
528
+
529
+ 不能用 `([^,()]+?),` 这种正则:真实的第一个参数几乎总是带括号的调用
530
+ (`assertEquals(service.compute(1), 42)`),正则一遇到括号就匹配不上,
531
+ 规则于是静默不响——而这正是它最该响的场景。
532
+ """
533
+ depth = 0
534
+ for i in range(lparen, len(code)):
535
+ ch = code[i]
536
+ if ch in '([{':
537
+ depth += 1
538
+ elif ch in ')]}':
539
+ depth -= 1
540
+ if depth == 0:
541
+ return code[lparen + 1:i].strip()
542
+ elif ch == ',' and depth == 1:
543
+ return code[lparen + 1:i].strip()
544
+ return ''
545
+
546
+
547
+ def _first_two_args(code, lparen):
548
+ """取前两个实参(按顶层逗号切)。"""
549
+ args, depth, start = [], 0, lparen + 1
550
+ for i in range(lparen, len(code)):
551
+ ch = code[i]
552
+ if ch in '([{':
553
+ depth += 1
554
+ elif ch in ')]}':
555
+ depth -= 1
556
+ if depth == 0:
557
+ args.append(code[start:i].strip())
558
+ break
559
+ elif ch == ',' and depth == 1:
560
+ args.append(code[start:i].strip())
561
+ start = i + 1
562
+ if len(args) >= 2:
563
+ break
564
+ return (args + ['', ''])[:2]
565
+ _RE_TRY_FAIL = re.compile(r'\btry\s*\{[^{}]{0,600}?\bfail\s*\(')
566
+
567
+
568
+ def check_java_doc_and_test(lens, ctx, out, add, doc_text):
569
+ """Javadoc 声明与实现是否一致;JUnit 用例的几个经典写错。
570
+
571
+ doc_text 由主引擎传进来(剥注释记号的那个函数),避免两处各写一份剥法——
572
+ 两份剥法迟早会对同一段注释给出不同结果,而那种分歧极难排查。
573
+ """
574
+ for u in lens.functions():
575
+ doc = doc_text(u.doc)
576
+ if not doc:
577
+ continue
578
+ # ── @throws 写了,方法既不声明也不抛 ──
579
+ declared = _RE_DOC_THROWS.findall(doc)
580
+ if declared:
581
+ thrown_in_body = set(re.findall(r'\bthrow\s+new\s+([A-Za-z_$][\w$.]*)', u.body))
582
+ sig = {t.split('.')[-1] for t in u.throws}
583
+ ghost = [t for t in declared
584
+ if t.split('.')[-1] not in sig and t.split('.')[-1] not in
585
+ {x.split('.')[-1] for x in thrown_in_body}]
586
+ # 运行时异常不必出现在 throws 上,只有确实也没 throw 过才算不符
587
+ ghost = [t for t in ghost if not re.search(r'(Runtime|Illegal|Unsupported|NullPointer)', t)]
588
+ if ghost:
589
+ add(out, ctx, 'comment.javadoc-throws', 'warn', lens, u.start,
590
+ f'`{u.label}` 的 Javadoc 写了 @throws {"、".join(ghost)},'
591
+ '但方法签名不声明、方法体里也没有抛',
592
+ '异常改过之后注释没跟着改——按现在的实现更新 @throws;'
593
+ '调用方是照着这份文档写 catch 的', u.label)
594
+
595
+ if not lens.is_test:
596
+ return
597
+ # ── assertEquals 的参数顺序:第一个是**期望值** ──
598
+ # 写反了不会让用例失败,但失败信息会把期望和实际说反,排查时直接把人带沟里
599
+ # 判据必须**同时看两个参数**:只看第一个带不带括号是不够的——
600
+ # `assertEquals(List.of(1L, 2L), ids(got))` 的第一个参数带括号,但它正是期望值,顺序完全正确。
601
+ # 真正写反的形状是「第一个是算出来的、第二个是写死的」。
602
+ for m in _RE_ASSERT_EQ.finditer(lens.code):
603
+ first, second = _first_two_args(lens.code, m.end() - 1)
604
+ if not first or not second:
605
+ continue
606
+ ln = _line_of(lens, m.start())
607
+ first_is_computed = (not _looks_literal(first)
608
+ and bool(re.search(r'\w\s*\(|^(result|actual|res|out|ret)\b', first)))
609
+ if not (first_is_computed and _looks_literal(second)):
610
+ continue
611
+ add(out, ctx, 'test.junit-assert-order', 'info', lens, ln,
612
+ f'`assertEquals({first[:30]}, …)` 的第一个参数像是**实际值** —— JUnit 的约定是 (期望, 实际)',
613
+ '调换顺序。写反了用例照样通过,但失败信息会把「期望 X 实际 Y」说反,排查时会被带偏',
614
+ _unit_at(lens, ln))
615
+
616
+ # ── 用 try/fail() 断言异常:漏写 fail 或被 catch 吞掉都会让用例变成永远通过 ──
617
+ for m in _RE_TRY_FAIL.finditer(lens.code):
618
+ ln = _line_of(lens, m.start())
619
+ add(out, ctx, 'test.junit-try-fail', 'info', lens, ln,
620
+ '用 try/fail() 断言异常:这种写法一旦 catch 范围写宽了(或忘了 fail),用例会永远通过',
621
+ '改用 `assertThrows(XxxException.class, () -> …)`,顺带还能断言异常消息', _unit_at(lens, ln))
622
+
623
+
624
+ # ── 与 dev-java「事务失效八景 / 常见坑 TOP」对齐的几条 ────────────────────
625
+ # 这一组不是新想出来的,是把团队已经写进技能文档的评审自查项变成机器每次都查一遍。
626
+ # 方法论写在文档里靠人记得,写在这里靠机器不忘——两者要说同一件事,不能各说各的。
627
+ _RE_NEW_THREAD = re.compile(
628
+ r'\bnew\s+Thread\s*\(|\bCompletableFuture\s*\.\s*(?:runAsync|supplyAsync)\s*\('
629
+ r'|\b\w*(?:executor|Executor|pool|Pool)\w*\s*\.\s*(?:submit|execute)\s*\(')
630
+ _RE_EXECUTORS = re.compile(r'\bExecutors\s*\.\s*(newFixedThreadPool|newCachedThreadPool|'
631
+ r'newSingleThreadExecutor|newScheduledThreadPool|newWorkStealingPool)\s*\(')
632
+ _RE_WEEK_YEAR = re.compile(r'["\'][^"\']*\bYYYY\b[^"\']*["\']')
633
+ _RE_TL_SET = re.compile(r'\b(\w+)\s*\.\s*set\s*\(')
634
+
635
+
636
+ def check_java_pitfalls(lens, ctx, out, add):
637
+ """dev-java 技能里「评审必查」的那几条,改由机器每次都查。"""
638
+ code = lens.code
639
+
640
+ for u in lens.functions():
641
+ # ── 事务内起新线程:事务绑定在当前线程上,新线程里的操作不在这个事务里 ──
642
+ if u.has_annotation('Transactional'):
643
+ m = _RE_NEW_THREAD.search(u.body)
644
+ if m:
645
+ ln = u.line_at(m.start())
646
+ add(out, ctx, 'framework.tx-new-thread', 'error', lens, ln,
647
+ f'`{u.label}` 带 @Transactional,方法内却起了新线程/异步任务 —— '
648
+ '事务是绑在**当前线程**上的,新线程里的数据库操作不属于这个事务,'
649
+ '外层回滚时它已经提交了',
650
+ '把异步部分挪到事务提交之后(`TransactionSynchronizationManager` 的 afterCommit 回调),'
651
+ '或者让异步任务自己开事务并自行保证幂等', u.label)
652
+
653
+ # ── ThreadLocal.set 之后没有 remove:线程池复用会把上一个请求的数据带给下一个 ──
654
+ tl_vars = {f['name'] for f in lens.fields if f['type'].split('<')[0].endswith('ThreadLocal')}
655
+ tl_vars |= set(re.findall(r'\bThreadLocal\s*<[^>]*>\s+(\w+)', code))
656
+ for u in lens.functions():
657
+ for m in _RE_TL_SET.finditer(u.body):
658
+ if m.group(1) not in tl_vars:
659
+ continue
660
+ # 在整个文件里找 remove,而不是只看当前方法:
661
+ # `UserContext.set(x)` 与 `UserContext.clear()`(里面统一 remove)是标准写法,
662
+ # 只看同一个方法会把这种正确实现全部报成泄漏。
663
+ if re.search(rf'\b{re.escape(m.group(1))}\s*\.\s*remove\s*\(', code):
664
+ continue
665
+ ln = u.line_at(m.start())
666
+ add(out, ctx, 'logic.java-threadlocal-leak', 'error', lens, ln,
667
+ f'`{m.group(1)}.set(...)` 之后没有对应的 `remove()` —— '
668
+ '线程池会复用线程,残留的值会被下一个请求读到(串数据),同时造成内存泄漏',
669
+ '用 `try { tl.set(x); … } finally { tl.remove(); }`,remove 必须在 finally 里',
670
+ u.label)
671
+ break
672
+
673
+ # ── Executors 快捷工厂:无界队列 / 无限线程,两种都是 OOM ──
674
+ for m in _RE_EXECUTORS.finditer(code):
675
+ ln = _line_of(lens, m.start())
676
+ why = ('队列无界(LinkedBlockingQueue 不限长),任务堆积直到 OOM'
677
+ if m.group(1) in ('newFixedThreadPool', 'newSingleThreadExecutor', 'newScheduledThreadPool')
678
+ else '线程数无上限,峰值时能把机器创建线程创建到崩')
679
+ add(out, ctx, 'logic.java-executors-factory', 'warn', lens, ln,
680
+ f'`Executors.{m.group(1)}()` —— {why}',
681
+ '手动 new ThreadPoolExecutor:核心/最大线程数、**有界**队列、'
682
+ '显式拒绝策略(CallerRuns 做背压 / Abort 快速失败)、带名字的 ThreadFactory 便于排查',
683
+ _unit_at(lens, ln))
684
+
685
+ # ── 日期格式里的 YYYY:它是「周年」不是「年」,跨年那一周会错一整年 ──
686
+ for m in _RE_WEEK_YEAR.finditer(lens.text):
687
+ ln = _line_of(lens, m.start())
688
+ line_txt = lens.line(ln)
689
+ if not re.search(r'(DateFormat|DateTimeFormatter|ofPattern|format|parse|pattern)', line_txt, re.I):
690
+ continue
691
+ add(out, ctx, 'logic.java-week-year', 'error', lens, ln,
692
+ '日期格式里用了 `YYYY`(week-based year,周年),它不是普通的年份 —— '
693
+ '每年跨年那一周会算成下一年,比如 2026-12-28 会格式化成 2027',
694
+ '改成小写 `yyyy`。这个 bug 一年只在跨年那几天出现,测试期几乎必然测不到',
695
+ _unit_at(lens, ln))