@huicai/cli 2026.804.0 → 2026.1019.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 (486) hide show
  1. package/LICENSE +391 -32
  2. package/README.md +176 -10
  3. package/bin/huicai.js +166 -73
  4. package/package.json +27 -8
  5. package/skills/ab-testing/trigger-cases.json +30 -0
  6. package/skills/academic-writing/trigger-cases.json +30 -0
  7. package/skills/accounting-ops/trigger-cases.json +30 -0
  8. package/skills/ai-agent/trigger-cases.json +30 -0
  9. package/skills/ai-aiops/trigger-cases.json +30 -0
  10. package/skills/ai-coding/trigger-cases.json +30 -0
  11. package/skills/ai-compliance/trigger-cases.json +30 -0
  12. package/skills/ai-eval/trigger-cases.json +30 -0
  13. package/skills/ai-llm-app/trigger-cases.json +30 -0
  14. package/skills/ai-mlops/trigger-cases.json +30 -0
  15. package/skills/ai-multimodal/trigger-cases.json +30 -0
  16. package/skills/ai-prompt/trigger-cases.json +30 -0
  17. package/skills/ai-rag/trigger-cases.json +30 -0
  18. package/skills/anomaly-detection/trigger-cases.json +30 -0
  19. package/skills/api-design/trigger-cases.json +31 -0
  20. package/skills/api-integration/trigger-cases.json +31 -0
  21. package/skills/arch-design/trigger-cases.json +12 -0
  22. package/skills/arch-diagram/SKILL.md +118 -0
  23. package/skills/arch-diagram/trigger-cases.json +30 -0
  24. package/skills/arch-distributed/trigger-cases.json +30 -0
  25. package/skills/arch-multitenant/trigger-cases.json +30 -0
  26. package/skills/audio-brief/SKILL.md +65 -0
  27. package/skills/audio-brief/trigger-cases.json +30 -0
  28. package/skills/audit-assist/trigger-cases.json +30 -0
  29. package/skills/audit-chain/trigger-cases.json +31 -0
  30. package/skills/banking-credit/trigger-cases.json +30 -0
  31. package/skills/brand-design/trigger-cases.json +30 -0
  32. package/skills/business-plan/trigger-cases.json +30 -0
  33. package/skills/capital-ops/trigger-cases.json +30 -0
  34. package/skills/cicd-pipeline/trigger-cases.json +31 -0
  35. package/skills/code-archaeology/SKILL.md +55 -0
  36. package/skills/code-archaeology/trigger-cases.json +9 -0
  37. package/skills/code-quality/SKILL.md +138 -0
  38. package/skills/code-quality/references/languages.md +107 -0
  39. package/skills/code-quality/scripts/__pycache__/code_battery.cpython-312.pyc +0 -0
  40. package/skills/code-quality/scripts/__pycache__/code_check.cpython-312.pyc +0 -0
  41. package/skills/code-quality/scripts/__pycache__/code_check.cpython-313.pyc +0 -0
  42. package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-312.pyc +0 -0
  43. package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-313.pyc +0 -0
  44. package/skills/code-quality/scripts/__pycache__/code_fix.cpython-312.pyc +0 -0
  45. package/skills/code-quality/scripts/__pycache__/code_fix.cpython-313.pyc +0 -0
  46. package/skills/code-quality/scripts/__pycache__/code_java.cpython-312.pyc +0 -0
  47. package/skills/code-quality/scripts/__pycache__/code_java.cpython-313.pyc +0 -0
  48. package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-312.pyc +0 -0
  49. package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-313.pyc +0 -0
  50. package/skills/code-quality/scripts/__pycache__/code_lens.cpython-312.pyc +0 -0
  51. package/skills/code-quality/scripts/__pycache__/code_lens.cpython-313.pyc +0 -0
  52. package/skills/code-quality/scripts/__pycache__/code_python.cpython-312.pyc +0 -0
  53. package/skills/code-quality/scripts/__pycache__/code_python.cpython-313.pyc +0 -0
  54. package/skills/code-quality/scripts/__pycache__/code_testplan.cpython-312.pyc +0 -0
  55. package/skills/code-quality/scripts/code_battery.py +1072 -0
  56. package/skills/code-quality/scripts/code_check.py +1269 -0
  57. package/skills/code-quality/scripts/code_csharp.py +371 -0
  58. package/skills/code-quality/scripts/code_fix.py +201 -0
  59. package/skills/code-quality/scripts/code_java.py +695 -0
  60. package/skills/code-quality/scripts/code_jsts.py +313 -0
  61. package/skills/code-quality/scripts/code_lens.py +1079 -0
  62. package/skills/code-quality/scripts/code_python.py +536 -0
  63. package/skills/code-quality/scripts/code_testplan.py +239 -0
  64. package/skills/code-quality/trigger-cases.json +45 -0
  65. package/skills/code-review/SKILL.md +24 -0
  66. package/skills/code-review/trigger-cases.json +35 -0
  67. package/skills/commit-and-pr/trigger-cases.json +31 -0
  68. package/skills/content-creation/trigger-cases.json +30 -0
  69. package/skills/corporate-governance/trigger-cases.json +30 -0
  70. package/skills/crawl-chain/trigger-cases.json +31 -0
  71. package/skills/customer-service/trigger-cases.json +30 -0
  72. package/skills/data-analysis/trigger-cases.json +30 -0
  73. package/skills/data-chain/SKILL.md +1 -1
  74. package/skills/data-chain/trigger-cases.json +31 -0
  75. package/skills/data-governance/trigger-cases.json +30 -0
  76. package/skills/data-report-pro/trigger-cases.json +30 -0
  77. package/skills/data-streaming/trigger-cases.json +30 -0
  78. package/skills/data-wrangling/SKILL.md +1 -1
  79. package/skills/data-wrangling/trigger-cases.json +30 -0
  80. package/skills/db-es/trigger-cases.json +30 -0
  81. package/skills/db-migration/trigger-cases.json +30 -0
  82. package/skills/db-mysql/trigger-cases.json +30 -0
  83. package/skills/db-postgres/trigger-cases.json +30 -0
  84. package/skills/db-redis/trigger-cases.json +30 -0
  85. package/skills/debug-root-cause/trigger-cases.json +30 -0
  86. package/skills/dependency-audit/SKILL.md +56 -0
  87. package/skills/dependency-audit/trigger-cases.json +7 -0
  88. package/skills/dependency-upgrade/trigger-cases.json +31 -0
  89. package/skills/dev-cpp/trigger-cases.json +30 -0
  90. package/skills/dev-cross-platform/trigger-cases.json +30 -0
  91. package/skills/dev-csharp/SKILL.md +23 -0
  92. package/skills/dev-csharp/trigger-cases.json +30 -0
  93. package/skills/dev-frontend/SKILL.md +19 -0
  94. package/skills/dev-frontend/trigger-cases.json +30 -0
  95. package/skills/dev-go/trigger-cases.json +30 -0
  96. package/skills/dev-harmony/trigger-cases.json +30 -0
  97. package/skills/dev-java/SKILL.md +20 -0
  98. package/skills/dev-java/trigger-cases.json +30 -0
  99. package/skills/dev-mobile/trigger-cases.json +30 -0
  100. package/skills/dev-node/trigger-cases.json +30 -0
  101. package/skills/dev-php/trigger-cases.json +30 -0
  102. package/skills/dev-python/SKILL.md +21 -0
  103. package/skills/dev-python/trigger-cases.json +30 -0
  104. package/skills/dev-rust/trigger-cases.json +30 -0
  105. package/skills/dev-toolchain/trigger-cases.json +31 -0
  106. package/skills/doc-chain/trigger-cases.json +31 -0
  107. package/skills/doc-compare/trigger-cases.json +30 -0
  108. package/skills/docx-author/SKILL.md +127 -25
  109. package/skills/docx-author/scripts/__pycache__/docx_battery.cpython-312.pyc +0 -0
  110. package/skills/docx-author/scripts/__pycache__/docx_build.cpython-312.pyc +0 -0
  111. package/skills/docx-author/scripts/__pycache__/docx_check.cpython-312.pyc +0 -0
  112. package/skills/docx-author/scripts/__pycache__/docx_layout.cpython-312.pyc +0 -0
  113. package/skills/docx-author/scripts/__pycache__/zh_prose.cpython-312.pyc +0 -0
  114. package/skills/docx-author/scripts/docx_battery.py +586 -0
  115. package/skills/docx-author/scripts/docx_build.py +995 -0
  116. package/skills/docx-author/scripts/docx_check.py +1306 -0
  117. package/skills/docx-author/scripts/docx_doc.py +14 -154
  118. package/skills/docx-author/scripts/docx_layout.py +490 -0
  119. package/skills/docx-author/scripts/zh_prose.py +639 -0
  120. package/skills/docx-author/trigger-cases.json +58 -0
  121. package/skills/echo-brief/SKILL.md +84 -0
  122. package/skills/echo-brief/references/echo-format.md +68 -0
  123. package/skills/echo-brief/trigger-cases.json +40 -0
  124. package/skills/ecommerce-ops/trigger-cases.json +30 -0
  125. package/skills/education-training/trigger-cases.json +30 -0
  126. package/skills/ehs-safety/trigger-cases.json +30 -0
  127. package/skills/email-writing/trigger-cases.json +30 -0
  128. package/skills/eng-ai/trigger-cases.json +30 -0
  129. package/skills/eng-architecture/trigger-cases.json +31 -0
  130. package/skills/eng-automation/trigger-cases.json +31 -0
  131. package/skills/eng-collaboration/trigger-cases.json +30 -0
  132. package/skills/eng-complex-env/trigger-cases.json +30 -0
  133. package/skills/eng-data-platform/trigger-cases.json +31 -0
  134. package/skills/eng-database/trigger-cases.json +31 -0
  135. package/skills/eng-develop/trigger-cases.json +30 -0
  136. package/skills/eng-docs/SKILL.md +24 -0
  137. package/skills/eng-docs/trigger-cases.json +30 -0
  138. package/skills/eng-efficiency/trigger-cases.json +30 -0
  139. package/skills/eng-infra/trigger-cases.json +30 -0
  140. package/skills/eng-integration/trigger-cases.json +31 -0
  141. package/skills/eng-legacy/trigger-cases.json +31 -0
  142. package/skills/eng-observability/trigger-cases.json +31 -0
  143. package/skills/eng-ops/trigger-cases.json +31 -0
  144. package/skills/eng-performance/trigger-cases.json +31 -0
  145. package/skills/eng-planning/trigger-cases.json +31 -0
  146. package/skills/eng-quality/SKILL.md +8 -0
  147. package/skills/eng-quality/trigger-cases.json +31 -0
  148. package/skills/eng-release/trigger-cases.json +31 -0
  149. package/skills/eng-reliability/trigger-cases.json +31 -0
  150. package/skills/eng-requirements/trigger-cases.json +12 -0
  151. package/skills/eng-research/trigger-cases.json +30 -0
  152. package/skills/eng-security/trigger-cases.json +31 -0
  153. package/skills/eng-ux/trigger-cases.json +30 -0
  154. package/skills/engineering-chain/trigger-cases.json +30 -0
  155. package/skills/env-offline/trigger-cases.json +31 -0
  156. package/skills/env-private-deploy/trigger-cases.json +31 -0
  157. package/skills/env-xinchuang/trigger-cases.json +31 -0
  158. package/skills/equipment-tpm/trigger-cases.json +30 -0
  159. package/skills/esg-report/trigger-cases.json +30 -0
  160. package/skills/event-management/trigger-cases.json +30 -0
  161. package/skills/excel-analysis-chain/SKILL.md +17 -4
  162. package/skills/excel-analysis-chain/trigger-cases.json +31 -0
  163. package/skills/explain-codebase/trigger-cases.json +11 -0
  164. package/skills/file-ingest/trigger-cases.json +30 -0
  165. package/skills/finance-analysis/trigger-cases.json +31 -0
  166. package/skills/finance-chain/trigger-cases.json +31 -0
  167. package/skills/financial-modeling/trigger-cases.json +31 -0
  168. package/skills/find-simplifications/SKILL.md +43 -0
  169. package/skills/find-simplifications/trigger-cases.json +23 -0
  170. package/skills/foreign-trade/trigger-cases.json +30 -0
  171. package/skills/funnel-cohort/trigger-cases.json +30 -0
  172. package/skills/gov-document/trigger-cases.json +30 -0
  173. package/skills/gui-change-evidence/SKILL.md +47 -0
  174. package/skills/gui-change-evidence/trigger-cases.json +23 -0
  175. package/skills/hr-chain/trigger-cases.json +31 -0
  176. package/skills/hr-operations/trigger-cases.json +30 -0
  177. package/skills/info-disclosure/trigger-cases.json +31 -0
  178. package/skills/infra-gateway/trigger-cases.json +31 -0
  179. package/skills/infra-k8s/trigger-cases.json +31 -0
  180. package/skills/infra-mq/trigger-cases.json +30 -0
  181. package/skills/insurance-advisory/trigger-cases.json +30 -0
  182. package/skills/internal-audit/trigger-cases.json +30 -0
  183. package/skills/ip-management/trigger-cases.json +31 -0
  184. package/skills/ipo-listing/trigger-cases.json +31 -0
  185. package/skills/it-audit/trigger-cases.json +31 -0
  186. package/skills/itsm-ops/trigger-cases.json +31 -0
  187. package/skills/knowledge-sop/trigger-cases.json +31 -0
  188. package/skills/legal-chain/trigger-cases.json +31 -0
  189. package/skills/legal-contract/trigger-cases.json +30 -0
  190. package/skills/litigation-support/trigger-cases.json +31 -0
  191. package/skills/logistics-warehouse/trigger-cases.json +30 -0
  192. package/skills/long-doc-analysis/trigger-cases.json +31 -0
  193. package/skills/management-accounting/trigger-cases.json +30 -0
  194. package/skills/manufacturing-chain/trigger-cases.json +31 -0
  195. package/skills/manufacturing-quality/trigger-cases.json +30 -0
  196. package/skills/marketing-analytics/trigger-cases.json +31 -0
  197. package/skills/marketing-chain/trigger-cases.json +31 -0
  198. package/skills/meeting-minutes/trigger-cases.json +30 -0
  199. package/skills/metrics-system/trigger-cases.json +31 -0
  200. package/skills/negotiation-prep/trigger-cases.json +30 -0
  201. package/skills/obs-prometheus/trigger-cases.json +31 -0
  202. package/skills/office-automation/trigger-cases.json +31 -0
  203. package/skills/okr-review/trigger-cases.json +30 -0
  204. package/skills/ops-fleet/trigger-cases.json +31 -0
  205. package/skills/ops-linux/trigger-cases.json +31 -0
  206. package/skills/optimization-solver/trigger-cases.json +30 -0
  207. package/skills/patent-disclosure/SKILL.md +105 -0
  208. package/skills/patent-disclosure/references/figures.md +110 -0
  209. package/skills/patent-disclosure/references/outline.md +162 -0
  210. package/skills/patent-disclosure/scripts/patent_docx.py +230 -0
  211. package/skills/patent-disclosure/trigger-cases.json +31 -0
  212. package/skills/patent-drafting/SKILL.md +108 -0
  213. package/skills/patent-drafting/trigger-cases.json +31 -0
  214. package/skills/patent-portfolio/SKILL.md +81 -0
  215. package/skills/patent-portfolio/trigger-cases.json +31 -0
  216. package/skills/pdf-toolkit/trigger-cases.json +31 -0
  217. package/skills/perf-optimize/trigger-cases.json +31 -0
  218. package/skills/pivot-crosstab/trigger-cases.json +30 -0
  219. package/skills/policy-analysis/trigger-cases.json +31 -0
  220. package/skills/portfolio-analysis/trigger-cases.json +31 -0
  221. package/skills/ppt-chain/SKILL.md +14 -6
  222. package/skills/ppt-chain/trigger-cases.json +31 -0
  223. package/skills/pptx-author/SKILL.md +86 -33
  224. package/skills/pptx-author/scripts/__pycache__/pptx_check.cpython-312.pyc +0 -0
  225. package/skills/pptx-author/scripts/__pycache__/pptx_deck.cpython-312.pyc +0 -0
  226. package/skills/pptx-author/scripts/__pycache__/pptx_layout.cpython-312.pyc +0 -0
  227. package/skills/pptx-author/scripts/pptx_battery.py +178 -0
  228. package/skills/pptx-author/scripts/pptx_build.py +119 -0
  229. package/skills/pptx-author/scripts/pptx_check.py +1182 -0
  230. package/skills/pptx-author/scripts/pptx_deck.py +736 -145
  231. package/skills/pptx-author/scripts/pptx_layout.py +384 -0
  232. package/skills/pptx-author/trigger-cases.json +15 -0
  233. package/skills/pr-comms/trigger-cases.json +30 -0
  234. package/skills/procurement-supply/trigger-cases.json +31 -0
  235. package/skills/product-design/trigger-cases.json +31 -0
  236. package/skills/production-planning/trigger-cases.json +31 -0
  237. package/skills/project-management/trigger-cases.json +31 -0
  238. package/skills/project-proposal/trigger-cases.json +31 -0
  239. package/skills/prose-standard/SKILL.md +53 -0
  240. package/skills/prose-standard/trigger-cases.json +23 -0
  241. package/skills/qa-api-automation/trigger-cases.json +31 -0
  242. package/skills/qa-mobile-automation/trigger-cases.json +31 -0
  243. package/skills/qa-ui-automation/trigger-cases.json +31 -0
  244. package/skills/quant-analysis/trigger-cases.json +31 -0
  245. package/skills/rag-triage/SKILL.md +64 -0
  246. package/skills/rag-triage/evals.json +57 -0
  247. package/skills/rag-triage/references/triage-playbook.md +75 -0
  248. package/skills/rag-triage/trigger-cases.json +31 -0
  249. package/skills/real-estate-property/trigger-cases.json +30 -0
  250. package/skills/refactor-safely/SKILL.md +1 -1
  251. package/skills/refactor-safely/trigger-cases.json +30 -0
  252. package/skills/regression-forecast/trigger-cases.json +31 -0
  253. package/skills/release-checklist/trigger-cases.json +31 -0
  254. package/skills/report-craft/SKILL.md +76 -0
  255. package/skills/report-craft/references/feasibility.md +118 -0
  256. package/skills/report-craft/references/review-checklist.md +51 -0
  257. package/skills/report-craft/references/visuals.md +114 -0
  258. package/skills/report-craft/references/writing.md +80 -0
  259. package/skills/report-craft/scripts/__pycache__/chart_render.cpython-312.pyc +0 -0
  260. package/skills/report-craft/scripts/__pycache__/fin_model.cpython-312.pyc +0 -0
  261. package/skills/report-craft/scripts/__pycache__/project_facts.cpython-312.pyc +0 -0
  262. package/skills/report-craft/scripts/chart_render.py +1222 -0
  263. package/skills/report-craft/scripts/fin_model.py +602 -0
  264. package/skills/report-craft/scripts/project_facts.py +580 -0
  265. package/skills/report-craft/scripts/report_battery.py +349 -0
  266. package/skills/report-craft/trigger-cases.json +15 -0
  267. package/skills/retail-store-ops/trigger-cases.json +30 -0
  268. package/skills/risk-compliance/trigger-cases.json +31 -0
  269. package/skills/scope-creep/SKILL.md +45 -0
  270. package/skills/scope-creep/trigger-cases.json +7 -0
  271. package/skills/sec-auth/trigger-cases.json +31 -0
  272. package/skills/sec-compliance/trigger-cases.json +31 -0
  273. package/skills/securities-chain/trigger-cases.json +31 -0
  274. package/skills/securities-research/trigger-cases.json +31 -0
  275. package/skills/security-audit/trigger-cases.json +31 -0
  276. package/skills/slides-html/SKILL.md +25 -10
  277. package/skills/slides-html/trigger-cases.json +11 -0
  278. package/skills/sql-analysis/SKILL.md +110 -0
  279. package/skills/sql-analysis/trigger-cases.json +34 -0
  280. package/skills/sql-tuning/trigger-cases.json +31 -0
  281. package/skills/statistical-testing/trigger-cases.json +31 -0
  282. package/skills/strategy-planning/trigger-cases.json +30 -0
  283. package/skills/supply-chain/trigger-cases.json +31 -0
  284. package/skills/tax-compliance/trigger-cases.json +30 -0
  285. package/skills/tech-doc/SKILL.md +262 -0
  286. package/skills/tech-doc/scripts/__pycache__/archive_battery.cpython-312.pyc +0 -0
  287. package/skills/tech-doc/scripts/__pycache__/archive_build.cpython-312.pyc +0 -0
  288. package/skills/tech-doc/scripts/__pycache__/archive_check.cpython-312.pyc +0 -0
  289. package/skills/tech-doc/scripts/__pycache__/archive_spec.cpython-312.pyc +0 -0
  290. package/skills/tech-doc/scripts/__pycache__/doc_templates.cpython-312.pyc +0 -0
  291. package/skills/tech-doc/scripts/archive_battery.py +860 -0
  292. package/skills/tech-doc/scripts/archive_build.py +599 -0
  293. package/skills/tech-doc/scripts/archive_check.py +1307 -0
  294. package/skills/tech-doc/scripts/archive_spec.py +499 -0
  295. package/skills/tech-doc/scripts/doc_templates.py +624 -0
  296. package/skills/tech-doc/trigger-cases.json +57 -0
  297. package/skills/tech-research/trigger-cases.json +31 -0
  298. package/skills/tender-bidding/trigger-cases.json +31 -0
  299. package/skills/translation-pro/trigger-cases.json +30 -0
  300. package/skills/treasury-mgmt/trigger-cases.json +31 -0
  301. package/skills/trim-cot-leakage/SKILL.md +58 -0
  302. package/skills/trim-cot-leakage/trigger-cases.json +23 -0
  303. package/skills/web-craft/SKILL.md +103 -0
  304. package/skills/web-craft/references/craft-checklist.md +75 -0
  305. package/skills/web-craft/references/design-direction.md +122 -0
  306. package/skills/web-craft/references/h5-mobile.md +100 -0
  307. package/skills/web-craft/references/motion.md +136 -0
  308. package/skills/web-craft/references/recipes.md +74 -0
  309. package/skills/web-craft/templates/wedding-invitation.html +1180 -0
  310. package/skills/web-craft/trigger-cases.json +14 -0
  311. package/skills/web-scraping/trigger-cases.json +34 -0
  312. package/skills/web-search/SKILL.md +11 -10
  313. package/skills/web-search/trigger-cases.json +31 -0
  314. package/skills/weekly-report/SKILL.md +134 -0
  315. package/skills/weekly-report/trigger-cases.json +11 -0
  316. package/skills/write-tests/trigger-cases.json +31 -0
  317. package/skills/xlsx-analysis/SKILL.md +143 -77
  318. package/skills/xlsx-analysis/scripts/__pycache__/data_profile.cpython-312.pyc +0 -0
  319. package/skills/xlsx-analysis/scripts/__pycache__/xlsx_build.cpython-312.pyc +0 -0
  320. package/skills/xlsx-analysis/scripts/__pycache__/xlsx_check.cpython-312.pyc +0 -0
  321. package/skills/xlsx-analysis/scripts/__pycache__/xlsx_layout.cpython-312.pyc +0 -0
  322. package/skills/xlsx-analysis/scripts/__pycache__/xlsx_toolkit.cpython-312.pyc +0 -0
  323. package/skills/xlsx-analysis/scripts/data_profile.py +391 -0
  324. package/skills/xlsx-analysis/scripts/xlsx_battery.py +389 -0
  325. package/skills/xlsx-analysis/scripts/xlsx_build.py +522 -0
  326. package/skills/xlsx-analysis/scripts/xlsx_check.py +924 -0
  327. package/skills/xlsx-analysis/scripts/xlsx_layout.py +235 -0
  328. package/skills/xlsx-analysis/scripts/xlsx_style.py +28 -66
  329. package/skills/xlsx-analysis/scripts/xlsx_toolkit.py +61 -5
  330. package/skills/xlsx-analysis/trigger-cases.json +59 -0
  331. package/squads/hc-defense.md +47 -0
  332. package/squads/hc-rdarchive.md +110 -0
  333. package/squads/hc-redteam.md +50 -0
  334. package/squads/hc-weekly.md +125 -0
  335. package/src/account.js +1 -0
  336. package/src/acp.js +1 -0
  337. package/src/advisor.js +1 -1
  338. package/src/agent.js +1 -1
  339. package/src/api.js +1 -1
  340. package/src/archaeology.js +1 -0
  341. package/src/ask-ui.js +1 -0
  342. package/src/assets-import.js +1 -1
  343. package/src/autofix.js +1 -1
  344. package/src/automation-tools.js +1 -0
  345. package/src/automation.js +1 -1
  346. package/src/autorun.js +1 -1
  347. package/src/banner.js +1 -1
  348. package/src/capability-probe.js +1 -0
  349. package/src/checkpoints.js +1 -1
  350. package/src/code-tools.js +1 -0
  351. package/src/codeindex.js +1 -1
  352. package/src/codemode.js +1 -0
  353. package/src/command-bridge.js +1 -0
  354. package/src/command-ui.js +1 -0
  355. package/src/commands.js +1 -1
  356. package/src/compaction.js +1 -1
  357. package/src/computer.js +1 -0
  358. package/src/config.js +1 -1
  359. package/src/consult.js +1 -1
  360. package/src/context-report.js +1 -1
  361. package/src/context-window.js +1 -0
  362. package/src/context.js +1 -1
  363. package/src/crew.js +1 -0
  364. package/src/crush.js +1 -0
  365. package/src/daemon.js +1 -1
  366. package/src/decisions.js +1 -1
  367. package/src/depaudit.js +1 -0
  368. package/src/device.js +1 -1
  369. package/src/diagram-draw.js +1 -0
  370. package/src/diagram-layouts.js +1 -0
  371. package/src/diagram-raster.js +1 -0
  372. package/src/diagram.js +1 -0
  373. package/src/dictation.js +1 -0
  374. package/src/diffview.js +1 -1
  375. package/src/doctor.js +1 -1
  376. package/src/docx-tools.js +1 -0
  377. package/src/errkind.js +1 -0
  378. package/src/evidence.js +1 -0
  379. package/src/executors.js +1 -1
  380. package/src/expert-catalog.js +1 -1
  381. package/src/experts-store.js +1 -1
  382. package/src/experts.js +1 -1
  383. package/src/external-agents.js +1 -0
  384. package/src/forks.js +1 -1
  385. package/src/fs-observe.js +1 -0
  386. package/src/gitflow.js +1 -1
  387. package/src/goalround.js +1 -0
  388. package/src/hooks.js +1 -1
  389. package/src/input.js +1 -1
  390. package/src/insights.js +1 -1
  391. package/src/jobs.js +1 -0
  392. package/src/kbsync.js +1 -1
  393. package/src/lsp.js +1 -0
  394. package/src/mcp-apps.js +1 -0
  395. package/src/mcp.js +1 -1
  396. package/src/memory.js +1 -1
  397. package/src/notify.js +1 -0
  398. package/src/permissions.js +1 -1
  399. package/src/plan-limits.js +1 -0
  400. package/src/policy.js +1 -0
  401. package/src/pptx-tools.js +1 -0
  402. package/src/presence.js +1 -0
  403. package/src/privatedir.js +1 -0
  404. package/src/pyruntime.js +1 -1
  405. package/src/radar.js +1 -0
  406. package/src/render.js +1 -1
  407. package/src/repl.js +1 -1
  408. package/src/report-tools.js +1 -0
  409. package/src/routing.js +1 -0
  410. package/src/sandbox-probe.js +1 -0
  411. package/src/sandbox-win-launcher.mjs +156 -0
  412. package/src/sandbox.js +1 -0
  413. package/src/scan.js +1 -1
  414. package/src/schedule.js +1 -1
  415. package/src/scopecreep.js +1 -0
  416. package/src/sdk.js +1 -0
  417. package/src/selection.js +1 -0
  418. package/src/session-events.js +1 -0
  419. package/src/session.js +1 -1
  420. package/src/sessionindex.js +1 -0
  421. package/src/sessionlog.js +1 -0
  422. package/src/skill-catalog.js +1 -0
  423. package/src/skill-lint.js +1 -0
  424. package/src/skill-optimize.js +1 -0
  425. package/src/skill-scan.js +1 -0
  426. package/src/skills.js +1 -1
  427. package/src/spill.js +1 -0
  428. package/src/squad.js +1 -1
  429. package/src/subagent.js +1 -1
  430. package/src/surface-state.js +1 -0
  431. package/src/tapd.js +1 -0
  432. package/src/terminal-select.js +1 -0
  433. package/src/terminal.js +1 -0
  434. package/src/test-runner.js +1 -0
  435. package/src/todo.js +1 -1
  436. package/src/toolpipe.js +1 -0
  437. package/src/tools.js +1 -1
  438. package/src/toon.js +1 -0
  439. package/src/transcript.js +1 -1
  440. package/src/tui/app.js +1 -1
  441. package/src/tui/command-menu.js +1 -0
  442. package/src/tui/progress.js +1 -0
  443. package/src/tui/scroll.js +1 -0
  444. package/src/tui/welcome.js +1 -0
  445. package/src/tui/workbench.js +1 -0
  446. package/src/ui-spec.js +1 -0
  447. package/src/uitree.js +1 -0
  448. package/src/uninstall.js +1 -1
  449. package/src/vision.js +1 -0
  450. package/src/visual.js +1 -0
  451. package/src/voice-form.js +1 -0
  452. package/src/voice.js +1 -0
  453. package/src/watch.js +1 -1
  454. package/src/web-audit.js +1 -0
  455. package/src/web-cdp.js +1 -0
  456. package/src/web-inspect.js +1 -0
  457. package/src/websearch.js +1 -0
  458. package/src/weekly.js +1 -0
  459. package/src/workflow.js +1 -0
  460. package/src/worklog.js +1 -0
  461. package/src/xlsx-tools.js +1 -0
  462. package/ui/command-picker.js +79 -0
  463. package/skills/ab-testing/scripts/__pycache__/ab_test.cpython-312.pyc +0 -0
  464. package/skills/accounting-ops/scripts/__pycache__/acct_tools.cpython-312.pyc +0 -0
  465. package/skills/anomaly-detection/scripts/__pycache__/anomaly.cpython-312.pyc +0 -0
  466. package/skills/audit-assist/scripts/__pycache__/audit_sampling.cpython-312.pyc +0 -0
  467. package/skills/banking-credit/scripts/__pycache__/credit_metrics.cpython-312.pyc +0 -0
  468. package/skills/data-report-pro/scripts/__pycache__/bigdata.cpython-312.pyc +0 -0
  469. package/skills/data-wrangling/scripts/__pycache__/join_tools.cpython-312.pyc +0 -0
  470. package/skills/ecommerce-ops/scripts/__pycache__/gmv_decompose.cpython-312.pyc +0 -0
  471. package/skills/funnel-cohort/scripts/__pycache__/funnel.cpython-312.pyc +0 -0
  472. package/skills/management-accounting/scripts/__pycache__/costing.cpython-312.pyc +0 -0
  473. package/skills/manufacturing-quality/scripts/__pycache__/spc.cpython-312.pyc +0 -0
  474. package/skills/marketing-analytics/scripts/__pycache__/rfm.cpython-312.pyc +0 -0
  475. package/skills/optimization-solver/scripts/__pycache__/solver.cpython-312.pyc +0 -0
  476. package/skills/pivot-crosstab/scripts/__pycache__/pivot_tools.cpython-312.pyc +0 -0
  477. package/skills/portfolio-analysis/scripts/__pycache__/portfolio.cpython-312.pyc +0 -0
  478. package/skills/procurement-supply/scripts/__pycache__/supply.cpython-312.pyc +0 -0
  479. package/skills/production-planning/scripts/__pycache__/mrp_capacity.cpython-312.pyc +0 -0
  480. package/skills/project-management/scripts/__pycache__/cpm.cpython-312.pyc +0 -0
  481. package/skills/quant-analysis/scripts/__pycache__/quant.cpython-312.pyc +0 -0
  482. package/skills/real-estate-property/scripts/__pycache__/property_calc.cpython-312.pyc +0 -0
  483. package/skills/regression-forecast/scripts/__pycache__/regress.cpython-312.pyc +0 -0
  484. package/skills/securities-research/scripts/__pycache__/valuation.cpython-312.pyc +0 -0
  485. package/skills/statistical-testing/scripts/__pycache__/stat_tests.cpython-312.pyc +0 -0
  486. package/skills/treasury-mgmt/scripts/__pycache__/cashflow_forecast.cpython-312.pyc +0 -0
@@ -0,0 +1,1079 @@
1
+ # 代码体检的「一把尺」:把任意语言的源码量成同一套结构化事实,供 code_check / code_fix 共用。
2
+ #
3
+ # 与 pptx_layout / docx_layout / xlsx_layout 同构——建版与体检必须共用一把尺,
4
+ # 否则「检查器说的」和「生成器做的」是两回事,体检通过不代表东西是对的。
5
+ #
6
+ # ── 为什么第一件事是掩码,而不是解析 ──
7
+ # 体检规则几乎全是在代码文本上找模式:SQL 拼接、硬编码密钥、== 误用、空 catch。
8
+ # 如果直接在原文上跑正则,第一个撞上的就是**字符串和注释**:
9
+ #
10
+ # log.info("用户表 SELECT * FROM users WHERE id = " + id) ← 日志文案里的 SQL,不是拼接漏洞
11
+ # // 这里以前用 == 比较,改成 === 了 ← 注释里的 ==,不是误用
12
+ # msg = "请勿把 password = 'xxx' 写进代码" ← 安全提示文案,不是硬编码密钥
13
+ #
14
+ # 一个会在这三处报警的体检器,比没有体检器更糟:人会学会无视它。
15
+ # 所以 mask_code() 先把所有字符串字面量与注释替换成等长空格(保留换行与列位置),
16
+ # 之后**所有**规则只在掩码后的「纯代码视图」上跑,需要原文时再按行列回查。
17
+ #
18
+ # 一把尺量出来的东西(Lens):
19
+ # lang / lines / code(掩码后全文) / units(函数·方法·类) / comments / imports /
20
+ # strings(字符串字面量,安全类规则要看它) / parse_ok(解析是否成功)
21
+ #
22
+ # parse_ok=False 时**必须如实上报**「这个文件没能解析,只做了降级检查」——
23
+ # 静默降级会让人以为查过了,这是比漏检更严重的欺骗。
24
+
25
+ import ast
26
+ import io
27
+ import os
28
+ import re
29
+ import tokenize
30
+
31
+ # ── 语言识别 ────────────────────────────────────────────────────────────────
32
+ EXT_LANG = {
33
+ '.js': 'js', '.mjs': 'js', '.cjs': 'js', '.jsx': 'js',
34
+ '.ts': 'ts', '.tsx': 'ts', '.mts': 'ts', '.cts': 'ts',
35
+ '.py': 'py', '.pyi': 'py',
36
+ '.java': 'java',
37
+ '.vue': 'vue',
38
+ '.go': 'go', '.rs': 'rust', '.cs': 'csharp', '.php': 'php',
39
+ '.c': 'c', '.h': 'c', '.cc': 'cpp', '.cpp': 'cpp', '.hpp': 'cpp',
40
+ '.rb': 'ruby', '.kt': 'kotlin', '.swift': 'swift', '.scala': 'scala',
41
+ '.sh': 'shell', '.bash': 'shell', '.zsh': 'shell',
42
+ '.sql': 'sql',
43
+ }
44
+
45
+ # 大括号系语言:共用一套括号配对解析(JS/TS/Java 深覆盖,其余按通用规则降级)
46
+ BRACE_LANGS = {'js', 'ts', 'java', 'go', 'rust', 'csharp', 'php', 'c', 'cpp', 'kotlin', 'swift', 'scala'}
47
+ DEEP_LANGS = {'js', 'ts', 'py', 'java', 'vue'} # 有专门解析器、规则集最全的语言
48
+
49
+ # 不体检的目录(依赖、产物、缓存)——扫描整仓时靠它避免几十万行噪声
50
+ SKIP_DIRS = {
51
+ 'node_modules', '.git', 'dist', 'build', 'out', 'target', '__pycache__',
52
+ '.venv', 'venv', 'vendor', '.idea', '.gradle', 'coverage', '.next', '.nuxt',
53
+ 'release', '.cache', 'site-packages', '.pytest_cache', '.mypy_cache',
54
+ }
55
+ # 产物/压缩文件:体检它们没有意义,且会拖垮扫描
56
+ SKIP_FILE_RE = re.compile(r'(\.min\.(js|css)|\.bundle\.js|-lock\.json|\.d\.ts)$', re.I)
57
+
58
+
59
+ def lang_of(path):
60
+ """按扩展名判语言;不认识的返回 'generic'(仍能跑语言无关规则)。"""
61
+ return EXT_LANG.get(os.path.splitext(str(path))[1].lower(), 'generic')
62
+
63
+
64
+ def is_test_path(path):
65
+ """是否测试文件。体检对测试代码要放宽(长函数、魔法数字在测试里是常态),
66
+ 但测试自身的质量另有一组规则(断言强度、自证循环)。"""
67
+ p = str(path).replace('\\', '/').lower()
68
+ base = os.path.basename(p)
69
+ return bool(
70
+ re.search(r'(^|/)(tests?|__tests__|spec|specs)(/|$)', p)
71
+ or re.match(r'^test_.*\.(py|js|ts)$', base)
72
+ or re.search(r'\.(test|spec)\.[cm]?[jt]sx?$', base)
73
+ or re.search(r'_test\.(py|go|java)$', base)
74
+ or re.search(r'Tests?\.(java|cs)$', os.path.basename(str(path)))
75
+ # C# 的测试约定:xxxTests.cs / xxxTest.cs、以及 Foo.Tests / Foo.UnitTests 项目目录。
76
+ # 不认它的话,C# 项目的测试代码会被按生产标准检查(长度、缺文档、覆盖率),
77
+ # 而测试专用规则(async void 测试)反而不生效。
78
+ or re.search(r'(^|/)[\w.]+\.(Tests?|UnitTests|IntegrationTests)/', p, re.I)
79
+ # 压测台与夹具文件里**故意**放着坏代码样本(SQL 拼接、空捕获、硬编码密钥)。
80
+ # 不把它们算作测试代码,体检就会把这些样本当成真缺陷报出来,
81
+ # 而人每次跑体检都要重新判断一遍「哦这是样本」——噪声会盖掉真问题。
82
+ or re.search(r'(_battery|_fixtures?|conftest|fixtures?)\.(py|js|ts|mjs|cjs)$', base)
83
+ or re.search(r'(^|/)fixtures?/', p)
84
+ )
85
+
86
+
87
+ # ── 掩码:把字符串与注释抹成空格 ────────────────────────────────────────────
88
+ class _Masker:
89
+ """逐字符扫描,维护「当前在代码 / 在字符串 / 在注释」状态机。
90
+
91
+ 比正则可靠:正则处理不了嵌套引号、转义、模板串里的 ${} 插值、以及
92
+ 「注释符号出现在字符串里」这种互相包含的情况——而这些在真实代码里遍地都是。
93
+ """
94
+
95
+ def __init__(self, text, lang):
96
+ self.src = text
97
+ self.lang = lang
98
+ self.out = [] # 掩码后的字符(与原文等长)
99
+ self.strings = [] # [(start_off, end_off, quote, raw)]
100
+ self.comments = [] # [(start_off, end_off, kind)] kind: line|block|doc
101
+
102
+ def run(self):
103
+ s = self.src
104
+ n = len(s)
105
+ i = 0
106
+ lang = self.lang
107
+ # 行注释与块注释的记号按语言不同
108
+ if lang == 'py':
109
+ line_marks, block_pairs = ['#'], []
110
+ elif lang == 'shell':
111
+ line_marks, block_pairs = ['#'], []
112
+ elif lang == 'sql':
113
+ line_marks, block_pairs = ['--'], [('/*', '*/')]
114
+ elif lang == 'vue':
115
+ line_marks, block_pairs = ['//'], [('/*', '*/'), ('<!--', '-->')]
116
+ else:
117
+ line_marks, block_pairs = ['//'], [('/*', '*/')]
118
+ out = self.out
119
+ while i < n:
120
+ ch = s[i]
121
+ # 1) 块注释
122
+ hit = None
123
+ for op, cl in block_pairs:
124
+ if s.startswith(op, i):
125
+ hit = (op, cl)
126
+ break
127
+ if hit:
128
+ op, cl = hit
129
+ j = s.find(cl, i + len(op))
130
+ j = n if j < 0 else j + len(cl)
131
+ kind = 'doc' if s.startswith('/**', i) else 'block'
132
+ self.comments.append((i, j, kind))
133
+ out.append(self._blank(s[i:j]))
134
+ i = j
135
+ continue
136
+ # 2) 行注释
137
+ mark = next((m for m in line_marks if s.startswith(m, i)), None)
138
+ if mark:
139
+ j = s.find('\n', i)
140
+ j = n if j < 0 else j
141
+ self.comments.append((i, j, 'line'))
142
+ out.append(self._blank(s[i:j]))
143
+ i = j
144
+ continue
145
+ # 3) 字符串 / 模板串
146
+ if ch in '"\'`':
147
+ j, quote = self._scan_string(s, i, n)
148
+ self.strings.append((i, j, quote, s[i:j]))
149
+ out.append(self._blank(s[i:j]))
150
+ i = j
151
+ continue
152
+ # 4) 正则字面量(JS):/pattern/flags。必须与除号区分,否则 a / b / c 会被吞掉
153
+ if ch == '/' and self.lang in ('js', 'ts', 'vue') and self._regex_allowed(out):
154
+ j = self._scan_regex(s, i, n)
155
+ if j > i + 1:
156
+ out.append(self._blank(s[i:j]))
157
+ i = j
158
+ continue
159
+ out.append(ch)
160
+ i += 1
161
+ return ''.join(out)
162
+
163
+ @staticmethod
164
+ def _blank(chunk):
165
+ """抹成等长空白,但**保留换行**——行号与列号必须与原文严格对齐,
166
+ 否则体检报出来的位置会指到别处,人一核对就不信这个工具了。"""
167
+ return ''.join('\n' if c == '\n' else ' ' for c in chunk)
168
+
169
+ def _scan_string(self, s, i, n):
170
+ q = s[i]
171
+ # Python 三引号
172
+ if self.lang == 'py' and s.startswith(q * 3, i):
173
+ j = s.find(q * 3, i + 3)
174
+ return (n if j < 0 else j + 3), q * 3
175
+ j = i + 1
176
+ while j < n:
177
+ c = s[j]
178
+ if c == '\\':
179
+ j += 2
180
+ continue
181
+ if c == q:
182
+ return j + 1, q
183
+ # 普通字符串不跨行(模板串可以):遇换行即收,避免一个漏引号吃掉整个文件
184
+ if c == '\n' and q != '`':
185
+ return j, q
186
+ j += 1
187
+ return n, q
188
+
189
+ @staticmethod
190
+ def _regex_allowed(out):
191
+ """看前一个有效字符判断 / 是正则开头还是除号。
192
+ 正则只能出现在「运算符/开括号/关键字」之后,不能跟在标识符或 ) ] 之后。"""
193
+ k = len(out) - 1
194
+ while k >= 0 and out[k] in ' \t\n':
195
+ k -= 1
196
+ if k < 0:
197
+ return True
198
+ prev = out[k]
199
+ if prev in '=(,:[!&|?{};+-*%<>~^':
200
+ return True
201
+ # return / typeof / case 等关键字之后也允许
202
+ tail = ''.join(out[max(0, k - 9):k + 1])
203
+ return bool(re.search(r'\b(return|typeof|case|in|of|delete|void|instanceof|new|do|else|yield|await)$', tail))
204
+
205
+ @staticmethod
206
+ def _scan_regex(s, i, n):
207
+ j = i + 1
208
+ in_class = False
209
+ while j < n:
210
+ c = s[j]
211
+ if c == '\\':
212
+ j += 2
213
+ continue
214
+ if c == '\n':
215
+ return i + 1 # 正则不跨行:判定失败,当普通除号
216
+ if c == '[':
217
+ in_class = True
218
+ elif c == ']':
219
+ in_class = False
220
+ elif c == '/' and not in_class:
221
+ j += 1
222
+ while j < n and s[j].isalpha():
223
+ j += 1
224
+ return j
225
+ j += 1
226
+ return i + 1
227
+
228
+
229
+ def mask_code(text, lang):
230
+ """返回 (code, strings, comments):code 是字符串/注释被抹空的「纯代码视图」。
231
+
232
+ 所有模式类规则都必须在 code 上跑;要看字面量内容时用 strings(已带原文)。
233
+ """
234
+ m = _Masker(text, lang)
235
+ code = m.run()
236
+ return code, m.strings, m.comments
237
+
238
+
239
+ # ── 统一的量度单位 ──────────────────────────────────────────────────────────
240
+ class Unit:
241
+ """一个可体检的代码单元:函数 / 方法 / 类。
242
+
243
+ 三种语言解析出来的东西必须落到同一个形状上,规则层才不用写三遍。
244
+ """
245
+
246
+ __slots__ = ('kind', 'name', 'start', 'end', 'header_line', 'params', 'doc',
247
+ 'body', 'depth', 'complexity', 'returns', 'is_public', 'is_async',
248
+ 'owner', 'lang', 'decorators', 'destructured', 'ret_type', 'throws', 'modifiers',
249
+ 'body_line')
250
+
251
+ def __init__(self, kind, name, start, end, **kw):
252
+ self.kind = kind # function | method | class
253
+ self.name = name
254
+ self.start = start # 1 基:声明行
255
+ self.end = end # 1 基:结束行(含)
256
+ self.header_line = kw.get('header_line', start)
257
+ self.params = kw.get('params', [])
258
+ self.doc = kw.get('doc') # 紧邻上方的文档注释原文
259
+ self.body = kw.get('body', '') # 掩码后的函数体(规则在它上面跑)
260
+ self.depth = kw.get('depth', 0) # 最大嵌套深度
261
+ self.complexity = kw.get('complexity', 1)
262
+ self.returns = kw.get('returns', [])
263
+ self.is_public = kw.get('is_public', True)
264
+ self.is_async = kw.get('is_async', False)
265
+ self.owner = kw.get('owner') # 所属类名(方法才有)
266
+ self.lang = kw.get('lang', 'generic')
267
+ self.decorators = kw.get('decorators', [])
268
+ # 解构参数 `function f({ a, b })` 里的字段名。
269
+ # 它们不是形参,但 JSDoc 惯例正是用 `@param {object} p` + `@param p.a` 来记录它们;
270
+ # 不把这些字段收进来,每一份写得规范的解构文档都会被判成「写了不存在的形参」。
271
+ self.destructured = kw.get('destructured', [])
272
+ # 下面三个是 Java 规则的前提:
273
+ # ret_type —— 判「声明了返回值却只 return null」「整数除法赋给 double」
274
+ # throws —— 判「@throws 写了但方法根本不声明抛」
275
+ # modifiers —— 判 @Transactional 落在 private/final 上(那样 AOP 代理根本不生效)
276
+ self.ret_type = kw.get('ret_type', '')
277
+ self.throws = kw.get('throws', [])
278
+ self.modifiers = kw.get('modifiers', [])
279
+ # 函数体(`{`)所在的行号。规则拿 body 里的偏移换算绝对行号时必须用它,
280
+ # 不能用 start——C# 的 Allman 风格把 `{` 放在下一行,用 start 会整体差 1。
281
+ self.body_line = kw.get('body_line', kw.get('start', start))
282
+
283
+ def has_annotation(self, *names):
284
+ """带没带某个注解(不区分是否写了参数:@Transactional 与 @Transactional(rollbackFor=…) 同等)。"""
285
+ for ann in self.decorators:
286
+ head = re.split(r'[(\s]', str(ann).lstrip('@'), 1)[0]
287
+ if head in names or str(ann).lstrip('@') in names:
288
+ return True
289
+ return False
290
+
291
+ @property
292
+ def lines(self):
293
+ return max(1, self.end - self.start + 1)
294
+
295
+ def line_at(self, body_offset):
296
+ """把「函数体内的字符偏移」换算成文件里的绝对行号。
297
+
298
+ 规则里到处都要做这件事,各写各的就会各错各的——
299
+ 尤其是 C# 这类把 `{` 放在下一行的风格,用 start 推算会整体差 1。
300
+ """
301
+ return self.body_line + str(self.body)[:max(0, body_offset)].count('\n')
302
+
303
+ @property
304
+ def label(self):
305
+ return f'{self.owner}.{self.name}' if self.owner else self.name
306
+
307
+ def __repr__(self):
308
+ return f'<Unit {self.kind} {self.label} L{self.start}-{self.end}>'
309
+
310
+
311
+ class Lens:
312
+ """一个文件量完之后的全部事实。规则层只认它,不再碰原始文本。"""
313
+
314
+ def __init__(self, path, text, lang):
315
+ self.path = path
316
+ self.text = text
317
+ self.lang = lang
318
+ self.lines = text.split('\n')
319
+ self.code, self.strings, self.comments = mask_code(text, lang)
320
+ self.code_lines = self.code.split('\n')
321
+ self.units = []
322
+ self.imports = []
323
+ # 类字段:Java 侧「@Autowired 字段注入」「static SimpleDateFormat」
324
+ # 「单例 bean 里的可变实例字段」三条规则都建立在它上面。
325
+ self.fields = []
326
+ # Vue 单文件组件的 <template> 段(原文 + 起始行号)。
327
+ # script 已经被转成 js/ts 走正常解析,template 是纯标记,只能按文本查。
328
+ self.vue_template = None
329
+ self.vue_template_line = 0
330
+ # .vue 的 script 段会被当成 js/ts 解析,lang 因此不再是 'vue'。
331
+ # 规则层要判「这是不是 Vue 组件」就得靠这个标记——
332
+ # 用 lang 判会把所有 .vue 漏掉,用 vue_template 判会漏掉只有 script 的那些。
333
+ self.is_vue = lang_of(path) == 'vue'
334
+ self.parse_ok = True
335
+ self.parse_error = None
336
+ self.is_test = is_test_path(path)
337
+
338
+ # 行号取文本:1 基,越界返回空串(规则层不必到处判边界)
339
+ def line(self, no):
340
+ return self.lines[no - 1] if 1 <= no <= len(self.lines) else ''
341
+
342
+ def code_line(self, no):
343
+ return self.code_lines[no - 1] if 1 <= no <= len(self.code_lines) else ''
344
+
345
+ @property
346
+ def loc(self):
347
+ """有效代码行数(非空、非纯注释)。"""
348
+ return sum(1 for ln in self.code_lines if ln.strip())
349
+
350
+ def comment_lines(self):
351
+ """注释占用的行号集合。"""
352
+ s = set()
353
+ for a, b, _ in self.comments:
354
+ s.update(range(self._line_of(a), self._line_of(b) + 1))
355
+ return s
356
+
357
+ def _line_of(self, off):
358
+ return self.text.count('\n', 0, off) + 1
359
+
360
+ def functions(self):
361
+ return [u for u in self.units if u.kind in ('function', 'method')]
362
+
363
+ def classes(self):
364
+ return [u for u in self.units if u.kind == 'class']
365
+
366
+ def in_literal(self, off):
367
+ """这个偏移量是否落在某个字符串字面量内部。
368
+
369
+ 凡是「必须在原文上定位」的规则(模式里带引号,掩码视图已把引号抹掉)
370
+ 都要再用它过一道:否则代码生成器、文档示例、压测样本里写在字符串中的代码
371
+ 会被当成真代码检查——报出来的每一条都是噪声。
372
+ """
373
+ for a, b, _q, _raw in self.strings:
374
+ if a <= off < b:
375
+ return True
376
+ if a > off:
377
+ break # strings 按偏移递增,越过就不必再找
378
+ return False
379
+
380
+ def unit_at(self, lineno):
381
+ """某一行落在哪个函数里(取最内层)。诊断要指出「在哪个函数」才有用。"""
382
+ best = None
383
+ for u in self.functions():
384
+ if u.start <= lineno <= u.end and (best is None or u.start > best.start):
385
+ best = u
386
+ return best
387
+
388
+
389
+ def _doc_above(lines, start_line, lang):
390
+ """取声明行正上方紧邻的注释块原文(中间只许有空行)。
391
+
392
+ 这是「注释优化」类规则的输入:有没有文档、文档说的和代码做的是否一致。
393
+ """
394
+ i = start_line - 2 # 0 基,声明行的上一行
395
+ while i >= 0 and not lines[i].strip():
396
+ i -= 1
397
+ # C# 把文档注释写在 `[Attribute]` 上方,跳过注解行才找得到它;
398
+ # 不跳的话,凡是带注解的成员都会被判成「没有文档」。
399
+ while i >= 0 and _RE_CS_ATTRIBUTE.match(lines[i]):
400
+ i -= 1
401
+ while i >= 0 and not lines[i].strip():
402
+ i -= 1
403
+ if i < 0:
404
+ return None
405
+ buf = []
406
+ if lang == 'py':
407
+ return None # Python 的 docstring 在函数体内,由 ast 直接取
408
+ tail = lines[i].strip()
409
+ if tail.endswith('*/'):
410
+ j = i
411
+ while j >= 0 and '/*' not in lines[j]:
412
+ buf.append(lines[j])
413
+ j -= 1
414
+ if j >= 0:
415
+ buf.append(lines[j])
416
+ return '\n'.join(reversed(buf)).strip() or None
417
+ if tail.startswith('//'):
418
+ j = i
419
+ while j >= 0 and lines[j].strip().startswith('//'):
420
+ buf.append(lines[j].strip())
421
+ j -= 1
422
+ return '\n'.join(reversed(buf)).strip() or None
423
+ return None
424
+
425
+
426
+ # ── Python:走标准库 ast,事实最准 ─────────────────────────────────────────
427
+ # 判定分支的节点类型。圈复杂度 = 1 + 分支数,衡量「这个函数有多少条路要测」。
428
+ _PY_BRANCH = (ast.If, ast.For, ast.AsyncFor, ast.While, ast.ExceptHandler,
429
+ ast.With, ast.AsyncWith, ast.Assert, ast.IfExp, ast.comprehension)
430
+ _PY_NEST = (ast.If, ast.For, ast.AsyncFor, ast.While, ast.With, ast.AsyncWith, ast.Try)
431
+
432
+
433
+ def _py_complexity(node):
434
+ n = 1
435
+ for sub in ast.walk(node):
436
+ if isinstance(sub, _PY_BRANCH):
437
+ n += 1
438
+ elif isinstance(sub, ast.BoolOp):
439
+ n += len(sub.values) - 1 # a and b and c = 两个额外分支
440
+ return n
441
+
442
+
443
+ def _py_depth(node, cur=0):
444
+ best = cur
445
+ for child in ast.iter_child_nodes(node):
446
+ step = 1 if isinstance(child, _PY_NEST) else 0
447
+ best = max(best, _py_depth(child, cur + step))
448
+ return best
449
+
450
+
451
+ def parse_python(lens):
452
+ try:
453
+ tree = ast.parse(lens.text)
454
+ except SyntaxError as e:
455
+ lens.parse_ok = False
456
+ lens.parse_error = f'第 {e.lineno} 行语法错误:{e.msg}'
457
+ return
458
+ stack = [] # 当前所处的类名栈
459
+
460
+ def end_of(node):
461
+ return getattr(node, 'end_lineno', None) or node.lineno
462
+
463
+ def visit(node, owner=None, in_func=False):
464
+ """递归登记类与函数。
465
+
466
+ in_func 标记「当前是否已经在某个函数体内」——嵌套函数(闭包、局部 helper)
467
+ 外部根本拿不到,把它们判成「对外可见」会凭空要求一堆文档注释,
468
+ 而误报的文档要求最终会让人把整条 missing-doc 规则关掉。
469
+ """
470
+ for child in ast.iter_child_nodes(node):
471
+ if isinstance(child, ast.ClassDef):
472
+ lens.units.append(Unit(
473
+ 'class', child.name, child.lineno, end_of(child),
474
+ doc=ast.get_docstring(child), lang='py', owner=owner,
475
+ is_public=not child.name.startswith('_') and not in_func,
476
+ decorators=[ast.unparse(d) for d in child.decorator_list],
477
+ body=_slice(lens.code, child.lineno, end_of(child)),
478
+ ))
479
+ stack.append(child.name)
480
+ visit(child, child.name, in_func)
481
+ stack.pop()
482
+ elif isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
483
+ args = child.args
484
+ params = [a.arg for a in (args.posonlyargs + args.args + args.kwonlyargs)]
485
+ if args.vararg:
486
+ params.append('*' + args.vararg.arg)
487
+ if args.kwarg:
488
+ params.append('**' + args.kwarg.arg)
489
+ rets = [s.lineno for s in ast.walk(child) if isinstance(s, ast.Return)]
490
+ lens.units.append(Unit(
491
+ 'method' if owner else 'function', child.name, child.lineno, end_of(child),
492
+ params=params, doc=ast.get_docstring(child), lang='py', owner=owner,
493
+ complexity=_py_complexity(child), depth=_py_depth(child),
494
+ returns=rets, is_async=isinstance(child, ast.AsyncFunctionDef),
495
+ # 私有类的方法、以及嵌套在函数里的局部函数,外部都够不着
496
+ is_public=(not child.name.startswith('_') and not in_func
497
+ and not (owner or '').startswith('_')),
498
+ decorators=[ast.unparse(d) for d in child.decorator_list],
499
+ body=_slice(lens.code, child.lineno, end_of(child)),
500
+ header_line=child.lineno, body_line=child.lineno,
501
+ ))
502
+ visit(child, owner, True) # 嵌套函数也要量,但它们不对外
503
+ elif isinstance(child, (ast.Import, ast.ImportFrom)):
504
+ mod = getattr(child, 'module', None) or ''
505
+ for a in child.names:
506
+ lens.imports.append({'name': a.name, 'module': mod, 'line': child.lineno,
507
+ 'alias': a.asname})
508
+ else:
509
+ visit(child, owner, in_func)
510
+
511
+ visit(tree)
512
+ lens.units.sort(key=lambda u: (u.start, u.end))
513
+ lens._ast = tree # 深度规则(可变默认参、空 except)直接复用
514
+
515
+
516
+ def _slice(code, start, end):
517
+ """按 1 基行号取掩码视图的片段。"""
518
+ return '\n'.join(code.split('\n')[start - 1:end])
519
+
520
+
521
+ # ── 大括号语言:JS / TS / Java 共用一套括号配对 ────────────────────────────
522
+ # 声明式函数:function f() / async function f() / export default function f()
523
+ _RE_JS_FUNC = re.compile(
524
+ r'(?:^|[\s;{}()])(?:export\s+)?(?:default\s+)?(async\s+)?function\s*(\*\s*)?([A-Za-z_$][\w$]*)?\s*\(')
525
+ # 赋值式函数:const f = (...) => / let f = async function(...)
526
+ _RE_JS_ASSIGN = re.compile(
527
+ r'(?:^|[\s;{}])(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*'
528
+ r'(async\s+)?(?:function\s*(?:\*\s*)?[\w$]*\s*)?\(')
529
+ # 类方法 / 对象方法: name(a, b) { | async name() { | get name() {
530
+ _RE_JS_METHOD = re.compile(
531
+ r'^[ \t]*(?:(?:public|private|protected|readonly|static|abstract|override)\s+)*'
532
+ r'(async\s+)?(?:(get|set)\s+)?(?:\*\s*)?([A-Za-z_$#][\w$]*)\s*(?:<[^>(]*>)?\s*\(', re.M)
533
+ _RE_JS_CLASS = re.compile(r'(?:^|\s)(?:export\s+)?(?:default\s+)?(?:abstract\s+)?(class|interface)\s+([A-Za-z_$][\w$]*)')
534
+ # Java 方法:修饰符 + 返回类型 + 名字( —— 必须排掉 if/for/while/catch/switch 这些「长得像调用」的关键字
535
+ _RE_JAVA_METHOD = re.compile(
536
+ r'^[ \t]*((?:(?:public|private|protected|static|final|synchronized|abstract|native|default|strictfp)\s+)*)'
537
+ r'(?:<[^>]+>\s*)?'
538
+ r'([A-Za-z_$][\w$<>\[\],.\s?]*?)\s+'
539
+ r'([A-Za-z_$][\w$]*)\s*\(', re.M)
540
+ _RE_JAVA_TYPE = re.compile(
541
+ r'(?:^|\s)((?:public|private|protected|static|final|abstract|sealed)\s+)*'
542
+ r'(class|interface|enum|record)\s+([A-Za-z_$][\w$]*)')
543
+ # C# 与 Java 形态相近,但修饰符更多、类型种类更多(record/struct),
544
+ # 且注解写成 `[Attribute]` 而不是 `@Attribute`。
545
+ # 改造前 C# 落在 parse_braces 的 js/java 两个分支之间的空隙里——
546
+ # 只认得出 class,**方法一个都解析不到**,而 parse_ok 仍然是 True。
547
+ # 于是所有基于函数的规则(长度、复杂度、缺文档、无测试覆盖)在 C# 上静默失效,
548
+ # 报告却照样说「未发现问题」——规则不响和代码干净,看起来一模一样。
549
+ _CS_MODIFIERS = (r'public|private|protected|internal|static|async|override|virtual|abstract'
550
+ r'|sealed|partial|extern|unsafe|new|readonly|required|volatile|const|file')
551
+ _RE_CS_METHOD = re.compile(
552
+ r'^[ \t]*((?:(?:' + _CS_MODIFIERS + r')\s+)*)'
553
+ r'(?:<[^>]+>\s*)?'
554
+ r'([A-Za-z_@][\w<>\[\],.\s?]*?)\s+'
555
+ r'([A-Za-z_@][\w]*)\s*(?:<[^>(]*>)?\s*\(', re.M)
556
+ _RE_CS_TYPE = re.compile(
557
+ r'(?:^|\s)(?:(?:' + _CS_MODIFIERS + r')\s+)*'
558
+ r'(class|interface|struct|record|enum)\s+([A-Za-z_@][\w]*)')
559
+ # `[Serializable]` / `[HttpGet("/x")]` / `[Obsolete("理由")]`
560
+ _RE_CS_ATTRIBUTE = re.compile(r'^\s*\[([A-Za-z_][\w.]*)\s*(\([^\n]*\))?\s*\]\s*$')
561
+
562
+ _KEYWORD_CALL = {'if', 'for', 'while', 'switch', 'catch', 'return', 'do', 'else', 'try',
563
+ 'synchronized', 'new', 'super', 'this', 'throw', 'case', 'with', 'yield', 'await'}
564
+ # 分支关键字:圈复杂度按它数(&&/||/?: 也算——每一个都是一条独立的路)
565
+ _RE_BRANCH = re.compile(r'\b(if|for|while|case|catch|elif|else\s+if)\b|\?\.?\s*[^.:]|&&|\|\||\?\?')
566
+
567
+
568
+ def _match_brace(code, open_idx):
569
+ """从 open_idx 处的 '{' 找到配对 '}',返回其下标;找不到返回 -1。
570
+
571
+ 在**掩码后**的视图上配对——原文里字符串中的 { } 会把配对带偏,
572
+ 而那正是「函数体范围算错 → 整份体检指错位置」的经典来源。
573
+ """
574
+ depth = 0
575
+ for i in range(open_idx, len(code)):
576
+ c = code[i]
577
+ if c == '{':
578
+ depth += 1
579
+ elif c == '}':
580
+ depth -= 1
581
+ if depth == 0:
582
+ return i
583
+ return -1
584
+
585
+
586
+ def _line_at(code, off):
587
+ return code.count('\n', 0, off) + 1
588
+
589
+
590
+ def _skip_lead(code, off):
591
+ """跳过声明正则吃进来的前导分隔符。
592
+
593
+ `(?:^|[\\s;{}()])function` 这类写法会把上一行的换行符算进匹配,
594
+ 起始行就整体偏到上一行——报出来的位置差一行,人一核对就不信这份报告了。
595
+ """
596
+ while off < len(code) and code[off] in ' \t\n;{}()':
597
+ off += 1
598
+ return off
599
+
600
+
601
+ def _params_of(code, lparen):
602
+ """从 '(' 处取形参,返回 (形参名列表, ')' 下标, 解构字段名列表)。
603
+
604
+ 解构参数按**一个**形参计(`f({a,b,c,d,e,f,g})` 是「一个选项对象」这种好写法,
605
+ 不该被判成「七个参数太多」),但里面的字段名要单独收着——JSDoc 用 `p.a` 记录它们。
606
+ """
607
+ depth = 0
608
+ for i in range(lparen, len(code)):
609
+ if code[i] == '(':
610
+ depth += 1
611
+ elif code[i] == ')':
612
+ depth -= 1
613
+ if depth == 0:
614
+ raw = code[lparen + 1:i]
615
+ out, fields = [], []
616
+ for seg in _split_top(raw, ','):
617
+ seg = seg.strip()
618
+ if not seg:
619
+ continue
620
+ if seg.startswith('{') or seg.startswith('['):
621
+ inner = seg[1:seg.rfind('}')] if '}' in seg else seg[1:]
622
+ for f in _split_top(inner, ','):
623
+ mm = re.match(r'\s*(?:\.\.\.)?([A-Za-z_$][\w$]*)', f)
624
+ if mm:
625
+ fields.append(mm.group(1))
626
+ out.append('{}')
627
+ continue
628
+ seg = re.sub(r'=.*$', '', seg).strip() # 默认值
629
+ seg = re.sub(r':\s*[\w<>\[\]|&.,\s?]+$', '', seg).strip() # TS 类型标注
630
+ m = re.search(r'([A-Za-z_$][\w$]*)\s*$', seg)
631
+ out.append(m.group(1) if m else seg[:24])
632
+ return out, i, fields
633
+ return [], lparen, []
634
+
635
+
636
+ def _split_top(s, sep):
637
+ """只在顶层切分(跳过 () [] {} <> 内的分隔符)。"""
638
+ out, buf, depth = [], [], 0
639
+ for ch in s:
640
+ if ch in '([{<':
641
+ depth += 1
642
+ elif ch in ')]}>':
643
+ depth -= 1
644
+ if ch == sep and depth <= 0:
645
+ out.append(''.join(buf))
646
+ buf = []
647
+ else:
648
+ buf.append(ch)
649
+ out.append(''.join(buf))
650
+ return out
651
+
652
+
653
+ def _depth_of(body):
654
+ """函数体的最大花括号嵌套深度(body 已是掩码视图,字符串里的括号不会干扰)。"""
655
+ depth = best = 0
656
+ for c in body:
657
+ if c == '{':
658
+ depth += 1
659
+ best = max(best, depth)
660
+ elif c == '}':
661
+ depth -= 1
662
+ return max(0, best - 1) # 减掉函数体自身那一层
663
+
664
+
665
+ def _complexity_of(body):
666
+ return 1 + len(_RE_BRANCH.findall(body))
667
+
668
+
669
+ def parse_braces(lens):
670
+ """JS / TS / Java 的统一解析:扫掩码视图找声明,再靠括号配对定边界。
671
+
672
+ 没有用真 AST,是权衡后的选择:托管 Python 里没有可靠的 JS/Java 解析器,
673
+ 引入外部依赖会让「首次体检要装包」——那等于让体检在最需要它的时候不可用。
674
+ 括号配对 + 掩码视图对「函数从哪到哪、有多深、多复杂」这几件事足够准,
675
+ 而这正是体检要量的东西。真正需要类型信息的规则一律不做,不猜。
676
+ """
677
+ code = lens.code
678
+ lang = lens.lang
679
+ seen = set() # 同一个 '(' 只认一次,避免多条正则重复命中
680
+
681
+ def add(kind, name, hdr_off, lparen, is_async=False, owner=None, strict=False, extra=None):
682
+ hdr_off = _skip_lead(code, hdr_off)
683
+ params, rparen, fields = _params_of(code, lparen)
684
+ brace = code.find('{', rparen)
685
+ arrow = code.find('=>', rparen)
686
+ # 箭头函数可能没有花括号体:const f = (a) => a + 1
687
+ if 0 <= arrow < (brace if brace >= 0 else len(code)) and lang in ('js', 'ts', 'vue'):
688
+ nxt = code.find('{', arrow)
689
+ semi = code.find('\n', arrow)
690
+ if nxt < 0 or (0 <= semi < nxt and code[arrow + 2:nxt].strip()):
691
+ start = _line_at(code, hdr_off)
692
+ end = _line_at(code, semi if semi > 0 else len(code) - 1)
693
+ body = _slice(code, start, end)
694
+ lens.units.append(Unit(
695
+ kind, name, start, end, params=params, lang=lang, owner=owner,
696
+ destructured=fields,
697
+ is_async=is_async, body=body, depth=0, complexity=_complexity_of(body),
698
+ doc=_doc_above(lens.lines, start, lang),
699
+ is_public=_is_public(lens.lines[start - 1] if start <= len(lens.lines) else '', name, lang, owner),
700
+ ))
701
+ return end
702
+ if brace < 0:
703
+ return None
704
+ # `)` 与 `{` 之间只允许返回类型标注(TS)/throws(Java)/修饰符。
705
+ # 这一关是防「行首的普通函数调用被当成函数声明」的闸:
706
+ # doSomething(arg);\n if (x) { ← 中间夹着 ; 就不是函数头
707
+ between = code[rparen + 1:brace]
708
+ if strict or lang == 'java':
709
+ if ';' in between or '=' in between.replace('=>', '') or len(between) > 160:
710
+ return None
711
+ elif code.count('\n', rparen, brace) > 3:
712
+ return None
713
+ close = _match_brace(code, brace)
714
+ if close < 0:
715
+ return None
716
+ start = _line_at(code, hdr_off)
717
+ end = _line_at(code, close)
718
+ body = code[brace:close + 1]
719
+ header = lens.lines[start - 1] if start <= len(lens.lines) else ''
720
+ java_extra = extra or {}
721
+ if lang in ('java', 'csharp'):
722
+ # throws 只可能出现在 `)` 与 `{` 之间;注解只可能在声明行上方
723
+ thrown = re.search(r'\bthrows\s+([\w$.,<>\s]+)$', between.strip())
724
+ java_extra.setdefault('throws',
725
+ [x.strip() for x in thrown.group(1).split(',')] if thrown else [])
726
+ # C# 的注解已在调用处取好(写法是 `[Attr]` 而不是 `@Attr`),这里只给 Java 兜底
727
+ if lang == 'java':
728
+ java_extra.setdefault('decorators', _annotations_above(lens.lines, start))
729
+ lens.units.append(Unit(
730
+ kind, name, start, end, params=params, lang=lang, owner=owner,
731
+ destructured=fields, body_line=_line_at(code, brace),
732
+ ret_type=java_extra.get('ret_type', ''), throws=java_extra.get('throws', []),
733
+ modifiers=java_extra.get('modifiers', []),
734
+ decorators=java_extra.get('decorators', []),
735
+ is_async=is_async, body=body, depth=_depth_of(body),
736
+ complexity=_complexity_of(body),
737
+ returns=[start + i for i, ln in enumerate(body.split('\n')) if re.search(r'\breturn\b', ln)],
738
+ doc=_doc_above(lens.lines, start, lang),
739
+ is_public=_is_public(header, name, lang, owner),
740
+ header_line=start,
741
+ ))
742
+ return end
743
+
744
+ # 1) 类型声明(class / interface / enum / record)——先登记,方法才知道自己的 owner
745
+ class_ranges = []
746
+ pat = {'java': _RE_JAVA_TYPE, 'csharp': _RE_CS_TYPE}.get(lang, _RE_JS_CLASS)
747
+ for m in pat.finditer(code):
748
+ name = m.group(3) if lang == 'java' else m.group(2)
749
+ kind_word = m.group(2) if lang == 'java' else m.group(1)
750
+ if lang == 'csharp':
751
+ kind_word, name = m.group(1), m.group(2)
752
+ brace = code.find('{', m.end())
753
+ semi = code.find(';', m.end())
754
+ # positional record:`public record Order(long Id, decimal Amount);` 没有花括号体。
755
+ # 不认它的话,这一整类声明在 C# 项目里都不会被登记成类型。
756
+ if lang == 'csharp' and 0 <= semi < (brace if brace >= 0 else len(code)):
757
+ start = _line_at(code, _skip_lead(code, m.start()))
758
+ lens.units.append(Unit('class', name, start, start, lang=lang,
759
+ doc=_doc_above(lens.lines, start, lang),
760
+ body='', decorators=[kind_word]))
761
+ continue
762
+ if brace < 0:
763
+ continue
764
+ close = _match_brace(code, brace)
765
+ if close < 0:
766
+ continue
767
+ start, end = _line_at(code, _skip_lead(code, m.start())), _line_at(code, close)
768
+ lens.units.append(Unit('class', name, start, end, lang=lang,
769
+ doc=_doc_above(lens.lines, start, lang),
770
+ body=code[brace:close + 1],
771
+ decorators=([kind_word] + _annotations_above(lens.lines, start)
772
+ if lang == 'java' else [kind_word])))
773
+ class_ranges.append((brace, close, name))
774
+
775
+ def owner_of(off):
776
+ best = None
777
+ for a, b, nm in class_ranges:
778
+ if a <= off <= b and (best is None or a > best[0]):
779
+ best = (a, nm)
780
+ return best[1] if best else None
781
+
782
+ # 2) 函数与方法
783
+ if lang in ('js', 'ts', 'vue'):
784
+ for m in _RE_JS_FUNC.finditer(code):
785
+ lp = code.index('(', m.end() - 1)
786
+ if lp in seen:
787
+ continue
788
+ seen.add(lp)
789
+ add('function' if not owner_of(m.start()) else 'method',
790
+ m.group(3) or '(匿名)', m.start(), lp, bool(m.group(1)), owner_of(m.start()))
791
+ for m in _RE_JS_ASSIGN.finditer(code):
792
+ lp = code.index('(', m.end() - 1)
793
+ if lp in seen:
794
+ continue
795
+ seen.add(lp)
796
+ add('function', m.group(1), m.start(), lp, bool(m.group(2)), owner_of(m.start()))
797
+ for m in _RE_JS_METHOD.finditer(code):
798
+ name = m.group(3)
799
+ if name in _KEYWORD_CALL:
800
+ continue
801
+ lp = m.end() - 1
802
+ if lp in seen:
803
+ continue
804
+ own = owner_of(m.start())
805
+ seen.add(lp)
806
+ add('method' if own else 'function', name, m.start(), lp, bool(m.group(1)), own, strict=True)
807
+ elif lang == 'csharp':
808
+ # record 的主构造函数 `public record Order(long Id);` 长得像方法,先把类型所在行排除掉
809
+ type_starts = {_line_at(code, a) for a, _b, _n in class_ranges}
810
+ for m in _RE_CS_METHOD.finditer(code):
811
+ name, rtype = m.group(3), (m.group(2) or '').strip()
812
+ if name in _KEYWORD_CALL or rtype in ('return', 'throw', 'else', 'new', 'await', 'yield'):
813
+ continue
814
+ if _line_at(code, m.start()) in type_starts:
815
+ continue # 这一行是 record/class 声明,不是方法
816
+ lp = m.end() - 1
817
+ if lp in seen:
818
+ continue
819
+ seen.add(lp)
820
+ # 构造函数没有返回类型,正则会把修饰符当成返回类型(`public Foo(` → rtype='public')
821
+ is_ctor = rtype in _CS_MODIFIERS.split('|')
822
+ add('method', name, m.start(), lp, 'async' in (m.group(1) or ''),
823
+ owner_of(m.start()), strict=True,
824
+ extra={'ret_type': '' if is_ctor else rtype,
825
+ 'modifiers': (m.group(1) or '').split(),
826
+ 'decorators': _cs_attributes_above(lens.lines, _line_at(code, m.start()))})
827
+ elif lang == 'java':
828
+ for m in _RE_JAVA_METHOD.finditer(code):
829
+ name, rtype = m.group(3), (m.group(2) or '').strip()
830
+ if name in _KEYWORD_CALL or rtype.split()[-1:] == ['new']:
831
+ continue
832
+ if rtype in ('return', 'throw', 'else', 'new'):
833
+ continue
834
+ lp = m.end() - 1
835
+ if lp in seen:
836
+ continue
837
+ seen.add(lp)
838
+ add('method', name, m.start(), lp, False, owner_of(m.start()), strict=True,
839
+ extra={'ret_type': rtype, 'modifiers': (m.group(1) or '').split()})
840
+ # 3) import
841
+ imp = (re.compile(r'^\s*import\s+(?:type\s+)?(?:(.+?)\s+from\s+)?[\'"]([^\'"]+)[\'"]', re.M)
842
+ if lang != 'java' else re.compile(r'^\s*import\s+(?:static\s+)?([\w.$*]+)\s*;', re.M))
843
+ for m in imp.finditer(lens.text):
844
+ lens.imports.append({'name': (m.group(1) or '').strip(), 'module': m.group(len(m.groups())),
845
+ 'line': lens.text.count('\n', 0, m.start()) + 1, 'alias': None})
846
+ if lang in ('java', 'csharp'):
847
+ # 字段要在方法登记完之后收——它靠「不在任何方法范围内」把字段和局部变量分开。
848
+ # C# 的字段声明形态与 Java 足够接近(修饰符 类型 名字 [= 初值];),共用同一套。
849
+ _collect_java_fields(lens, class_ranges)
850
+ # 去重(同一 '(' 被两条正则捞到时留范围大的那条)与排序
851
+ uniq = {}
852
+ for u in lens.units:
853
+ key = (u.kind, u.name, u.start)
854
+ if key not in uniq or u.end > uniq[key].end:
855
+ uniq[key] = u
856
+ lens.units = sorted(uniq.values(), key=lambda u: (u.start, u.end))
857
+
858
+
859
+ # 注解可以连着写好几行,也可以带很长的参数:@RequestMapping(value = "/x", method = POST)
860
+ _RE_ANNOTATION_LINE = re.compile(r'^\s*@([A-Za-z_$][\w$.]*)\s*(\([^\n]*\))?\s*$')
861
+
862
+
863
+ def _cs_attributes_above(lines, start_line):
864
+ """取声明行上方连续的 `[Attribute]`(C# 的注解写法)。
865
+
866
+ 与 Java 的 `@Xxx` 是同一个角色:`[HttpGet]`、`[Obsolete]`、`[Fact]` 决定了
867
+ 这个成员在框架里怎么被使用,几条 C# 规则都要看它。
868
+ """
869
+ out = []
870
+ i = start_line - 2
871
+ while i >= 0:
872
+ raw = lines[i].strip()
873
+ if not raw:
874
+ i -= 1
875
+ continue
876
+ m = _RE_CS_ATTRIBUTE.match(lines[i])
877
+ if not m:
878
+ break
879
+ out.append('[' + m.group(1) + (m.group(2) or '') + ']')
880
+ i -= 1
881
+ return out
882
+
883
+
884
+ def _annotations_above(lines, start_line):
885
+ """取声明行正上方连续的注解(允许夹空行与注解自身的换行参数)。
886
+
887
+ 注解是 Java 规则的主要抓手:@Transactional 落在哪、@Test 有没有断言、
888
+ @Autowired 是不是字段注入——全靠它。往上扫而不是正则整块匹配,
889
+ 是因为注解参数里可以带花括号与逗号,正则一旦贪心就会吃掉上一个方法。
890
+ """
891
+ out = []
892
+ i = start_line - 2 # 0 基,声明行的上一行
893
+ while i >= 0:
894
+ raw = lines[i].strip()
895
+ if not raw:
896
+ i -= 1
897
+ continue
898
+ m = _RE_ANNOTATION_LINE.match(lines[i])
899
+ if m:
900
+ out.append('@' + m.group(1) + (m.group(2) or ''))
901
+ i -= 1
902
+ continue
903
+ # 注解参数换行的情形:`)` 收尾,往上找到 `@Xxx(` 那一行
904
+ if raw.endswith(')') and not raw.endswith(';'):
905
+ j = i
906
+ buf = []
907
+ while j >= 0 and not re.match(r'^\s*@[A-Za-z_$]', lines[j]):
908
+ buf.append(lines[j].strip())
909
+ j -= 1
910
+ if start_line - j > 12: # 别无限往上找
911
+ return out
912
+ if j >= 0:
913
+ head = re.match(r'^\s*@([A-Za-z_$][\w$.]*)', lines[j])
914
+ if head:
915
+ out.append('@' + head.group(1) + '(…)')
916
+ i = j - 1
917
+ continue
918
+ break
919
+ return out
920
+
921
+
922
+ # 字段声明:`private final OrderRepo repo;` / `private static SimpleDateFormat FMT = new …;`
923
+ _RE_JAVA_FIELD = re.compile(
924
+ r'^[ \t]*((?:(?:public|private|protected|internal|static|final|transient|volatile'
925
+ r'|readonly|const|required|new|abstract|override|sealed)\s+)*)'
926
+ r'([A-Za-z_$][\w$<>\[\],.\s?]*?)\s+'
927
+ r'([A-Za-z_$][\w$]*)\s*(=[^;]*)?;\s*$', re.M)
928
+
929
+
930
+ def _collect_java_fields(lens, class_ranges):
931
+ """收集类字段。只认「类体内、方法体外」的声明——方法里的局部变量长得一模一样。"""
932
+ code = lens.code
933
+ method_spans = [(u.start, u.end) for u in lens.units if u.kind == 'method']
934
+ for m in _RE_JAVA_FIELD.finditer(code):
935
+ ln = _line_at(code, m.start())
936
+ if any(a <= ln <= b for a, b in method_spans):
937
+ continue # 方法体内 = 局部变量,不是字段
938
+ owner = None
939
+ for a, b, nm in class_ranges:
940
+ if _line_at(code, a) <= ln <= _line_at(code, b):
941
+ owner = nm
942
+ if owner is None:
943
+ continue
944
+ mods = (m.group(1) or '').split()
945
+ lens.fields.append({
946
+ 'name': m.group(3), 'type': (m.group(2) or '').strip(), 'line': ln,
947
+ 'modifiers': mods, 'owner': owner, 'init': (m.group(4) or '').strip(),
948
+ 'annotations': _annotations_above(lens.lines, ln),
949
+ })
950
+
951
+
952
+ def _is_public(header, name, lang, owner=None):
953
+ if lang == 'java':
954
+ return 'public' in header or not re.search(r'\b(private|protected)\b', header)
955
+ if name.startswith('_') or name.startswith('#'):
956
+ return False
957
+ if re.search(r'\b(private|protected)\b', header):
958
+ return False
959
+ # 类方法默认对外可见——它不写 export,但外部拿到实例就能调
960
+ return 'export' in header or owner is not None
961
+
962
+
963
+ def parse_generic(lens):
964
+ """降级解析:靠缩进与「看起来像函数头」的行做粗切分。
965
+
966
+ 覆盖不到的语言仍能跑语言无关规则(函数过长、注释密度、TODO、行宽、硬编码密钥),
967
+ 但**不会**去猜它做不到的事——报告里会写明这是降级模式。
968
+ """
969
+ pat = re.compile(
970
+ r'^[ \t]*(?:(?:pub|public|private|protected|static|final|func|fn|def|sub|function|proc)\s+)+'
971
+ r'[\w<>\[\]:*&,\s]*?([A-Za-z_][\w]*)\s*\(')
972
+ hits = [(m.start(), m.group(1)) for m in pat.finditer(lens.code)]
973
+ for i, (off, name) in enumerate(hits):
974
+ start = _line_at(lens.code, off)
975
+ end = (_line_at(lens.code, hits[i + 1][0]) - 1) if i + 1 < len(hits) else len(lens.lines)
976
+ body = _slice(lens.code, start, end)
977
+ lens.units.append(Unit('function', name, start, max(start, end), lang=lens.lang,
978
+ body=body, complexity=_complexity_of(body), depth=_depth_of(body),
979
+ doc=_doc_above(lens.lines, start, lens.lang)))
980
+
981
+
982
+ def _vue_script(text):
983
+ """从 .vue 单文件组件里抠出 <script> 段,连同它的起始行偏移。"""
984
+ m = re.search(r'<script[^>]*>(.*?)</script>', text, re.S | re.I)
985
+ if not m:
986
+ return None, 0
987
+ return m.group(1), text.count('\n', 0, m.start(1))
988
+
989
+
990
+ def _vue_template(text):
991
+ """抠出顶层 <template> 段,返回 (原文, 起始行号);没有则 (None, 0)。
992
+
993
+ Vue 的一半问题在 template 里——`v-for` 少了 `:key`、`v-if` 与 `v-for` 写在同一个元素上、
994
+ `v-html` 塞用户输入。改造前这一段被整个丢弃(只抠 script),
995
+ 于是这些最常见的问题一条也查不到,而报告照样显示「已体检」。
996
+
997
+ 只认**行首**的 `<template`:组件内部的具名插槽 `<template #header>` 是缩进的,
998
+ 不该被当成顶层模板;结束位置取最后一个 `</template>`,因为插槽会造成嵌套。
999
+ """
1000
+ m = re.search(r'^[ \t]*<template[^>]*>', text, re.M | re.I)
1001
+ if not m:
1002
+ return None, 0
1003
+ end = text.rfind('</template>')
1004
+ if end <= m.end():
1005
+ return None, 0
1006
+ return text[m.end():end], text.count('\n', 0, m.end()) + 1
1007
+
1008
+
1009
+ def read_lens(path, text=None, lang=None):
1010
+ """量一个文件。text 可直接传入(压测与内存中的内容用),否则从磁盘读。"""
1011
+ if text is None:
1012
+ with open(path, 'r', encoding='utf-8', errors='replace') as f:
1013
+ text = f.read()
1014
+ lang = lang or lang_of(path)
1015
+ offset = 0
1016
+ vue_template, vue_template_line = None, 0
1017
+ raw_text = None
1018
+ if lang == 'vue':
1019
+ vue_template, vue_template_line = _vue_template(text)
1020
+ raw_text = text
1021
+ script, offset = _vue_script(text)
1022
+ if script is None:
1023
+ lang = 'generic'
1024
+ else:
1025
+ text = '\n' * offset + script # 补回行偏移,行号仍对得上原文件
1026
+ lang = 'ts' if re.search(r'<script[^>]+lang=["\']ts', text) else 'js'
1027
+ lens = Lens(path, text, lang)
1028
+ if vue_template is not None:
1029
+ lens.vue_template = vue_template
1030
+ lens.vue_template_line = vue_template_line
1031
+ if raw_text is not None:
1032
+ # .vue 的解析用的是「补齐换行的 script 段」,行号与原文对齐但内容不是原文:
1033
+ # template 所在的那些行在解析文本里是空的。
1034
+ # 而 lens.line() 是**呈现与豁免**的入口(报告里的源码摘录、就地 ignore 注释都走它),
1035
+ # 所以它必须看原文——否则 template 里的问题报出来只有行号、没有内容,
1036
+ # 写在 template 上的豁免注释也永远读不到。
1037
+ # code_lines(掩码视图)仍是 script,规则照旧只在真代码上跑。
1038
+ lens.lines = raw_text.split('\n')
1039
+ try:
1040
+ if lang == 'py':
1041
+ parse_python(lens)
1042
+ elif lang in BRACE_LANGS:
1043
+ parse_braces(lens)
1044
+ else:
1045
+ parse_generic(lens)
1046
+ except Exception as e: # 解析器自身出错:降级但**如实记录**
1047
+ lens.parse_ok = False
1048
+ lens.parse_error = f'{type(e).__name__}: {e}'
1049
+ lens.units = []
1050
+ try:
1051
+ parse_generic(lens)
1052
+ except Exception as e2:
1053
+ # 连降级解析都失败:把两次失败都记进 parse_error。
1054
+ # 这里若 pass 掉,报告只会说「解析失败」而说不出降级也没成,
1055
+ # 人就无从判断这个文件到底被检查了多少——静默降级比漏检更糟。
1056
+ lens.parse_error += f';降级解析同样失败:{type(e2).__name__}: {e2}'
1057
+ return lens
1058
+
1059
+
1060
+ def walk_files(root, exts=None, limit=4000):
1061
+ """遍历待体检的源文件(跳过依赖/产物/压缩文件)。"""
1062
+ out = []
1063
+ root = os.path.abspath(root)
1064
+ if os.path.isfile(root):
1065
+ return [root]
1066
+ for dirpath, dirnames, filenames in os.walk(root):
1067
+ dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS and not d.startswith('.')]
1068
+ for fn in filenames:
1069
+ ext = os.path.splitext(fn)[1].lower()
1070
+ if ext not in EXT_LANG:
1071
+ continue
1072
+ if SKIP_FILE_RE.search(fn):
1073
+ continue
1074
+ if exts and ext not in exts:
1075
+ continue
1076
+ out.append(os.path.join(dirpath, fn))
1077
+ if len(out) >= limit:
1078
+ return out
1079
+ return out