@huicai/cli 2026.814.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 (485) hide show
  1. package/LICENSE +391 -32
  2. package/README.md +137 -8
  3. package/bin/huicai.js +146 -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 -1
  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 -1
  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 -1
  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 -1
  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 -1
  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 -1
  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/visual.js +1 -1
  450. package/src/voice-form.js +1 -0
  451. package/src/voice.js +1 -0
  452. package/src/watch.js +1 -1
  453. package/src/web-audit.js +1 -0
  454. package/src/web-cdp.js +1 -0
  455. package/src/web-inspect.js +1 -0
  456. package/src/websearch.js +1 -0
  457. package/src/weekly.js +1 -0
  458. package/src/workflow.js +1 -0
  459. package/src/worklog.js +1 -0
  460. package/src/xlsx-tools.js +1 -0
  461. package/ui/command-picker.js +79 -0
  462. package/skills/ab-testing/scripts/__pycache__/ab_test.cpython-312.pyc +0 -0
  463. package/skills/accounting-ops/scripts/__pycache__/acct_tools.cpython-312.pyc +0 -0
  464. package/skills/anomaly-detection/scripts/__pycache__/anomaly.cpython-312.pyc +0 -0
  465. package/skills/audit-assist/scripts/__pycache__/audit_sampling.cpython-312.pyc +0 -0
  466. package/skills/banking-credit/scripts/__pycache__/credit_metrics.cpython-312.pyc +0 -0
  467. package/skills/data-report-pro/scripts/__pycache__/bigdata.cpython-312.pyc +0 -0
  468. package/skills/data-wrangling/scripts/__pycache__/join_tools.cpython-312.pyc +0 -0
  469. package/skills/ecommerce-ops/scripts/__pycache__/gmv_decompose.cpython-312.pyc +0 -0
  470. package/skills/funnel-cohort/scripts/__pycache__/funnel.cpython-312.pyc +0 -0
  471. package/skills/management-accounting/scripts/__pycache__/costing.cpython-312.pyc +0 -0
  472. package/skills/manufacturing-quality/scripts/__pycache__/spc.cpython-312.pyc +0 -0
  473. package/skills/marketing-analytics/scripts/__pycache__/rfm.cpython-312.pyc +0 -0
  474. package/skills/optimization-solver/scripts/__pycache__/solver.cpython-312.pyc +0 -0
  475. package/skills/pivot-crosstab/scripts/__pycache__/pivot_tools.cpython-312.pyc +0 -0
  476. package/skills/portfolio-analysis/scripts/__pycache__/portfolio.cpython-312.pyc +0 -0
  477. package/skills/procurement-supply/scripts/__pycache__/supply.cpython-312.pyc +0 -0
  478. package/skills/production-planning/scripts/__pycache__/mrp_capacity.cpython-312.pyc +0 -0
  479. package/skills/project-management/scripts/__pycache__/cpm.cpython-312.pyc +0 -0
  480. package/skills/quant-analysis/scripts/__pycache__/quant.cpython-312.pyc +0 -0
  481. package/skills/real-estate-property/scripts/__pycache__/property_calc.cpython-312.pyc +0 -0
  482. package/skills/regression-forecast/scripts/__pycache__/regress.cpython-312.pyc +0 -0
  483. package/skills/securities-research/scripts/__pycache__/valuation.cpython-312.pyc +0 -0
  484. package/skills/statistical-testing/scripts/__pycache__/stat_tests.cpython-312.pyc +0 -0
  485. package/skills/treasury-mgmt/scripts/__pycache__/cashflow_forecast.cpython-312.pyc +0 -0
@@ -0,0 +1,639 @@
1
+ # 中文技术文档的文字质量体检:错别字、标点、术语一致、表述质量、**AI 味**。
2
+ #
3
+ # 与排版体检(docx_check 的几何部分)的分工:那边管「看得见的事故」(溢出、超宽、变形),
4
+ # 这边管「读得出的毛病」(写错字、标点混用、满篇套话)。两边都在 docx_check 里汇总成一份报告。
5
+ #
6
+ # 本模块**只吃纯文本**,不依赖 python-docx——所以 Markdown 草稿、纯文本、
7
+ # docx 抽出来的段落都能用同一套判据。`python zh_prose.py 草稿.md` 可直接跑。
8
+ #
9
+ # ── 一条纪律:误报比漏报更伤 ──────────────────────────────────────────────
10
+ # 一个天天喊狼来了的检查器,用两次就没人看了。所以:
11
+ # · 黑白分明的(「己经」「按装」)才报 error;
12
+ # · 一词两用、要看语境的(「制定/制订」「截止/截至」)只报 warn 并写明区别,由人定夺;
13
+ # · AI 味**一律按密度报,不按单次出现报**——「综上所述」在结论里出现一次是正常写作,
14
+ # 出现七次才是模板。单次命中就报,等于逼人把正常的话也删掉。
15
+
16
+ import math
17
+ import re
18
+ import sys
19
+ from collections import Counter, defaultdict
20
+
21
+ # ── 一、错别字与易混词 ─────────────────────────────────────────────────────
22
+ # 明确写错的(error):右边是唯一正确写法,替换不需要看语境。
23
+ TYPOS = {
24
+ '己经': '已经', '既使': '即使', '按装': '安装', '安照': '按照', '布署': '部署',
25
+ '峻工': '竣工', '蓝领': None, '篮图': '蓝图', '题纲': '提纲', '编致': '编制',
26
+ '曼延': '蔓延', '讫今': '迄今', '座落': '坐落', '渡过难关': None, '决对': '绝对',
27
+ '既然如此': None, '至至': None, '在所不昔': '在所不惜', '迫不急待': '迫不及待',
28
+ '有条不絮': '有条不紊', '一如继往': '一如既往', '一诺千斤': '一诺千金',
29
+ '按步就班': '按部就班', '不落于俗套': None, '弱不经风': '弱不禁风',
30
+ '穿流不息': '川流不息', '重蹈复辙': '重蹈覆辙', '一愁莫展': '一筹莫展',
31
+ '各行其事': '各行其是', '再接再励': '再接再厉', '悬梁刺骨': '悬梁刺股',
32
+ '甘败下风': '甘拜下风', '好高鹜远': '好高骛远', '如愿以尝': '如愿以偿',
33
+ '和霭': '和蔼', '缘故': None, '寒喧': '寒暄', '慧眼识金': None,
34
+ '通宵达但': '通宵达旦', '相辅相承': '相辅相成', '迫在没睫': '迫在眉睫',
35
+ '不径而走': '不胫而走', '声名雀起': '声名鹊起', '沤心沥血': '呕心沥血',
36
+ '走头无路': '走投无路', '不加思索': '不假思索', '融汇贯通': '融会贯通',
37
+ '严具以待': '严阵以待', '摩拳擦掌': None, '肆无忌单': '肆无忌惮',
38
+ '开门缉盗': '开门揖盗', '功亏一匮': '功亏一篑', '侯选': '候选', '气慨': '气概',
39
+ '爆发户': '暴发户', '奇葩异草': '奇花异草', '直接了当': '直截了当',
40
+ '毛骨耸然': '毛骨悚然', '不落窠桕': '不落窠臼', '循私舞弊': '徇私舞弊',
41
+ '连锁反映': '连锁反应', '暗然失色': '黯然失色', '记忆尤新': '记忆犹新',
42
+ }
43
+ TYPOS = {k: v for k, v in TYPOS.items() if v}
44
+
45
+ # 需要看语境的(warn):写法都存在,但含义不同,用错了读者会误解。
46
+ CONFUSABLES = [
47
+ ('截止目前', '截至目前', '「截止」是停止(截止日期),「截至」是到某时间为止'),
48
+ ('截止到', '截至', '同上:表示「到……为止」用「截至」'),
49
+ ('制定计划', None, '「制定」多用于方针政策等宏观件,「制订」多用于具体方案,可推敲'),
50
+ ('起动', '启动', '机械装置用「起动」,系统/程序/项目一律用「启动」'),
51
+ ('帐号', '账号', '与钱、账目相关一律用「账」,「帐」只用于布幔'),
52
+ ('帐户', '账户', '同上'),
53
+ ('帐单', '账单', '同上'),
54
+ ('其它', '其他', '现代汉语规范推荐「其他」,指物指人皆可'),
55
+ ('部份', '部分', '「部份」为港台写法,大陆规范为「部分」'),
56
+ ('作出贡献', None, '「作出」与「做出」皆通行,全文统一即可'),
57
+ ('反应情况', '反映情况', '「反映」是如实告知,「反应」是物理化学或应激'),
58
+ ('权利机关', '权力机关', '「权力」是支配力,「权利」是法律赋予的资格'),
59
+ ('必需品', None, '「必需」作定语指必不可少之物,「必须」作状语修饰动词,勿互换'),
60
+ ('以致于', '以至于', '「以致」接不好的结果,「以至」表程度延伸'),
61
+ ('进行了优化', None, '「进行+双音节动词」是典型冗余,可直接说「优化了」'),
62
+ ]
63
+
64
+ # ── 重复(打字/粘贴事故)─────────────────────────────────────────────────
65
+ # **不能按「虚词叠字」判**。中文里这些叠字几乎全有合法用法:
66
+ # 目的的重要性 / 为了了解 / 和和平发展 / 在在线账户 / 的的确确 / 把把手 / 对对方 / 从从容容
67
+ # 早先那条 `ch*2 in s` 的规则在真实语料上命中 20 处,**全是误报**,还是必修级——
68
+ # 这种检查比没有更糟。改成两条黑白分明的:
69
+ # ① 同一字符连续出现三次以上(的的的、我我我)——正常中文不会这样;
70
+ # ② 三字以上的词组紧邻重复(本项目本项目)——这是复制粘贴事故的真实指纹。
71
+ # 限定三字起:两字重叠是合法构词(看看、渐渐、一步一步、认认真真)。
72
+ DUP_RUN_RE = re.compile(r'([一-鿿])\1{2,}')
73
+ DUP_PHRASE_RE = re.compile(r'([一-鿿]{3,8})\1')
74
+
75
+ # ── 二、标点 ───────────────────────────────────────────────────────────────
76
+ CJK = r'一-鿿㐀-䶿'
77
+ PUNCT_RULES = [
78
+ ('half-comma', re.compile(rf'[{CJK}],(?![\d])'), 'error', '中文语境用了半角逗号 ","', ','),
79
+ ('half-period', re.compile(rf'[{CJK}]\.(?![\d\w])'), 'error', '中文语境用了半角句号 "."', '。'),
80
+ ('half-semi', re.compile(rf'[{CJK}];'), 'error', '中文语境用了半角分号 ";"', ';'),
81
+ ('half-colon', re.compile(rf'[{CJK}]:(?![\d/\\])'), 'error', '中文语境用了半角冒号 ":"', ':'),
82
+ ('half-question', re.compile(rf'[{CJK}]\?'), 'error', '中文语境用了半角问号 "?"', '?'),
83
+ ('half-paren', re.compile(rf'\([^)]*[{CJK}][^)]*\)'), 'warn', '中文内容用了半角括号 "()"', '()'),
84
+ ('bad-ellipsis', re.compile(r'\.{3,}|。{3,}|·{3,}'), 'warn', '省略号写法不规范', '……'),
85
+ # 破折号只在「前后都贴着字」时才算破折号用法。
86
+ # 单独一个「—」占满整格,是中文表格里「不适用/无」的标准占位写法——
87
+ # 把它判成破折号错误,等于逼人把规范写法改坏。
88
+ ('bad-dash', re.compile(rf'(?<![-—])--(?!-)|(?<=[{CJK}\w])—(?=[{CJK}\w])'), 'warn',
89
+ '破折号应为两个全角「——」', '——'),
90
+ ('dup-punct', re.compile(r'([,。;:!?、])\1+'), 'error', '连续重复标点', None),
91
+ ('space-before-punct', re.compile(r'\s+[,。;:!?、)】」』]'), 'warn', '标点前多了空格', None),
92
+ ('num-range-hyphen', re.compile(r'\d+\s*-\s*\d+\s*(?:年|月|日|万元|元|人|个|台|套|%)'), 'info',
93
+ '数值区间建议用「~」或「—」而非半角连字符', None),
94
+ ]
95
+ PAIRS = [('(', ')'), ('「', '」'), ('『', '』'), ('【', '】'), ('《', '》'), ('“', '”'), ('‘', '’')]
96
+
97
+ # ── 三、AI 味 ──────────────────────────────────────────────────────────────
98
+ # 词表分三类,各有各的阈值。**全部按密度判定**,单次出现一律不报。
99
+ AI_CLICHE = [ # 套话骨架:出现三次以上就是模板在说话
100
+ '值得注意的是', '需要指出的是', '需要强调的是', '综上所述', '总而言之', '总的来说',
101
+ '总体而言', '不难看出', '由此可见', '换句话说', '毫无疑问', '显而易见',
102
+ '在当今', '随着.{0,8}的不断发展', '随着.{0,8}的日益', '显得尤为重要',
103
+ '具有重要的现实意义', '具有重要意义', '奠定了坚实的基础', '提供了有力支撑',
104
+ '发挥了重要作用', '取得了显著成效', '积累了宝贵经验', '为.{0,10}提供了新思路',
105
+ ]
106
+ AI_BUZZ = [ # 互联网黑话/空转动词:技术文档里几乎总能换成具体动作
107
+ '赋能', '抓手', '闭环', '打法', '心智', '颗粒度', '生态位', '组合拳', '护城河',
108
+ '对齐颗粒度', '拉通', '沉淀', '复用性', '强化认知', '深度融合', '全方位',
109
+ '多维度', '立体化', '一体化推进', '持续深化', '不断优化', '有效提升', '显著改善',
110
+ '大力推进', '积极探索', '切实加强', '进一步完善', '深入推进', '扎实开展',
111
+ '助力', '致力于', '旨在打造', '构建了一套', '形成了.{0,6}体系',
112
+ ]
113
+ AI_EMPTY = ['进一步', '有效', '积极', '大力', '不断', '切实', '充分', '全面', '深入', '持续', '高度']
114
+ AI_CONNECT = ['首先', '其次', '再次', '最后', '另一方面', '与此同时', '在此基础上']
115
+
116
+ SPECIFIC_RE = re.compile(r'\d') # 具体性锚点:数字、日期、比例、金额
117
+ DATE_RE = re.compile(r'\d{4}\s*年|\d{1,2}\s*月|\bQ[1-4]\b')
118
+
119
+ # ── 四、表述质量阈值 ───────────────────────────────────────────────────────
120
+ LONG_RUN = 45 # 连续 >45 字不出现停顿标点:读者要憋着读完,必须断开
121
+ LONG_SENT = 110 # 整句 >110 字:即便有逗号也太长,该拆成两句
122
+ LONG_PARA = 600 # 一段 >600 字:该拆
123
+ DE_PER_SENT = 4 # 一句里 4 个以上「的」:定语套娃
124
+
125
+ SENT_SPLIT = re.compile(r'(?<=[。!?;\n])')
126
+
127
+
128
+ def _issue(level, group, kind, loc, msg, quote='', suggest=''):
129
+ return {'level': level, 'group': group, 'kind': kind, 'loc': loc,
130
+ 'msg': msg, 'quote': quote[:60], 'suggest': suggest}
131
+
132
+
133
+ # 「读起来有多长」:书名号、引号里的名称、括注,读者当一个整体读,不会在中间换气;
134
+ # 一个英文词、一个数字(含版本号、百分比)也是一个单位;「[待补:…]」占位不算字数。
135
+ # 按字符硬数会把「艾瑞咨询《……洞察报告》(2026 年 8 月发布)指出」「Spring Cloud 2023.0.1 与 …」报成不喘气——
136
+ # 可研、方案这类报告通篇是引用、文号、版本、公式,这种误报会逼着把写对的东西改坏。
137
+ _QUOTED = re.compile(r'《[^》]{0,80}》|「[^」]{0,80}」|“[^”]{0,80}”|([^)]{0,80})|\([^)]{0,80}\)|〔[^〕]{0,20}〕')
138
+ _PLACEHOLDER = re.compile(r'[[^]]{0,60}]')
139
+ _LATIN_WORD = re.compile(r'[A-Za-z0-9][A-Za-z0-9.\-_/%]*')
140
+ # 除逗号顿号分号冒号外,箭头与公式运算符也是停顿(「A → B → C」「净现金流量 = 收入 + 节约 − 成本」)
141
+ RUN_SPLIT = re.compile(r'[,、;:,→←⇒⇐==++−×÷||]')
142
+
143
+
144
+ def _reading_len(text):
145
+ t = _PLACEHOLDER.sub('', str(text or ''))
146
+ t = _QUOTED.sub('□', t)
147
+ t = _LATIN_WORD.sub('x', t)
148
+ return len(re.sub(r'\s+', '', t))
149
+
150
+
151
+ def _sentences(text):
152
+ return [s.strip() for s in SENT_SPLIT.split(str(text or '')) if s.strip()]
153
+
154
+
155
+ def _cjk_len(s):
156
+ return sum(1 for ch in s if '一' <= ch <= '鿿')
157
+
158
+
159
+ # ── 段级检查 ───────────────────────────────────────────────────────────────
160
+ def check_paragraph(text, loc='', kind='body', strict=False):
161
+ """单段文字体检。kind: body 正文 / heading 标题 / caption 题注 / cell 单元格。"""
162
+ out = []
163
+ s = str(text or '')
164
+ if not s.strip():
165
+ return out
166
+
167
+ for wrong, right in TYPOS.items():
168
+ if wrong in s:
169
+ out.append(_issue('error', 'text', 'typo', loc, f'错别字「{wrong}」应为「{right}」',
170
+ _around(s, wrong), right))
171
+ for wrong, right, note in CONFUSABLES:
172
+ if wrong in s:
173
+ sug = f'建议改为「{right}」' if right else '请人工确认'
174
+ out.append(_issue('warn', 'text', 'confusable', loc,
175
+ f'易混写法「{wrong}」:{note}', _around(s, wrong), sug))
176
+ m = DUP_RUN_RE.search(s)
177
+ if m:
178
+ out.append(_issue('warn', 'text', 'dup-char', loc,
179
+ f'同一字符连续出现 {len(m.group(0))} 次:「{m.group(0)}」',
180
+ _around(s, m.group(0)), m.group(1)))
181
+ m = DUP_PHRASE_RE.search(s)
182
+ if m:
183
+ out.append(_issue('warn', 'text', 'dup-phrase', loc,
184
+ f'词组紧邻重复:「{m.group(1)}」出现了两遍',
185
+ _around(s, m.group(0)), m.group(1)))
186
+
187
+ for name, pat, level, msg, sug in PUNCT_RULES:
188
+ if name == 'num-range-hyphen' and not strict:
189
+ level = 'info'
190
+ m = pat.search(s)
191
+ if m:
192
+ n = len(pat.findall(s))
193
+ out.append(_issue(level, 'punct', name, loc,
194
+ msg + (f'(本段 {n} 处)' if n > 1 else ''),
195
+ _around(s, m.group(0)), sug or ''))
196
+ for lft, rgt in PAIRS:
197
+ if lft == rgt:
198
+ continue
199
+ if s.count(lft) != s.count(rgt):
200
+ out.append(_issue('error', 'punct', 'unpaired', loc,
201
+ f'{lft}{rgt} 不配对({s.count(lft)} 开 / {s.count(rgt)} 闭)', s[:60], ''))
202
+ if s.count('"') % 2 or s.count("'") % 2:
203
+ out.append(_issue('warn', 'punct', 'straight-quote', loc,
204
+ '出现半角直引号且数量为奇数,中文正文应使用“”', s[:60], '“”'))
205
+
206
+ if kind == 'heading':
207
+ if s.rstrip().endswith(('。', '.', '.')):
208
+ out.append(_issue('warn', 'punct', 'heading-period', loc, '标题末尾不应加句号', s[:40], ''))
209
+ if len(s) > 40:
210
+ out.append(_issue('warn', 'text', 'heading-long', loc,
211
+ f'标题过长({len(s)} 字),标题应是名词短语不是句子', s[:40], ''))
212
+ return out
213
+
214
+ if kind == 'body':
215
+ n = len(s)
216
+ if n > LONG_PARA:
217
+ out.append(_issue('warn', 'text', 'para-long', loc,
218
+ f'段落过长({n} 字),建议拆成 2–3 段或改列项', s[:40], ''))
219
+ for sent in _sentences(s):
220
+ # 「长」有两种:一种是整句太长,一种是句子里某一截不喘气。
221
+ # 早先只按「去掉逗号后的字数」判,会把带三个逗号的句子说成「无停顿」——
222
+ # 判据与文案不符的检查最伤信任,所以两件事分开量、分开说。
223
+ chunks = RUN_SPLIT.split(sent)
224
+ longest = max((_reading_len(c) for c in chunks), default=0)
225
+ sent_len = len(_PLACEHOLDER.sub('', sent))
226
+ if longest > LONG_RUN:
227
+ out.append(_issue('warn', 'text', 'sent-run', loc,
228
+ f'连续 {longest} 字没有停顿标点,读者要憋着读完',
229
+ max(chunks, key=_reading_len)[:50], '在语义停顿处补逗号或断句'))
230
+ elif sent_len > LONG_SENT:
231
+ out.append(_issue('warn', 'text', 'sent-long', loc,
232
+ f'整句 {sent_len} 字过长,建议拆成两句', sent[:50], ''))
233
+ if sent.count('的') >= DE_PER_SENT:
234
+ out.append(_issue('info', 'text', 'de-stack', loc,
235
+ f'一句里 {sent.count("的")} 个「的」,定语套娃', sent[:50], ''))
236
+ if s.strip() and s.strip()[-1] not in '。!?;:”)】》…—':
237
+ out.append(_issue('warn', 'punct', 'no-end-punct', loc, '段落结尾缺标点', s[-30:], '。'))
238
+ return out
239
+
240
+
241
+ def _around(s, needle, span=12):
242
+ i = s.find(needle)
243
+ if i < 0:
244
+ return s[:40]
245
+ return s[max(0, i - span):i + len(needle) + span]
246
+
247
+
248
+ # ── 文档级:术语一致 ───────────────────────────────────────────────────────
249
+ # 术语一致性分两路,都刻意做得「无争议」——宁可少报,不报有争议的:
250
+ # ① 拉丁词大小写不一致(API/Api/api):同一个词的不同拼法,客观是错,靠通用扫描发现,
251
+ # 不需要维护词表,也不会误伤(大小写不同就是不一致,没有第二种解释);
252
+ # ② 中文角色词的多种叫法(甲方/建设单位/委托方):同一份文件里混着叫,读者要自己对应。
253
+ # 早先把「用户/客户」「系统/平台」也列进来是错的——那是两个不同概念,不是两种写法。
254
+ # 连字符、下划线、点号、斜杠、@ 连着的是标识符或专名的一部分(one-api、Vue.js、/v1/api、user@host),不是独立术语:
255
+ # 「one-api」里的 api 和正文里的 API 不是同一个词的两种拼法(实测可研里被报成「api×2、API×2」)
256
+ LATIN_TOKEN = re.compile(r'(?<![A-Za-z0-9\-_./@])([A-Za-z][A-Za-z0-9]{1,15})(?![A-Za-z0-9\-_./@])')
257
+ CASE_WHITELIST = {'a', 'i', 'the', 'of', 'and', 'to', 'in', 'is', 'v', 'vs'}
258
+ ROLE_SYNONYMS = [
259
+ (['甲方', '建设单位', '委托方', '需求方'], '委托方'),
260
+ (['乙方', '承建单位', '承研单位', '受托方', '开发方'], '承担方'),
261
+ (['本系统', '本平台', '该系统', '该平台'], '本系统'),
262
+ (['项目组', '课题组', '研制组', '开发组'], '项目组'),
263
+ (['验收组', '专家组', '评审组'], '专家组'),
264
+ ]
265
+
266
+ NUM_UNIT_RE = re.compile(r'(\d[\d,.]*)\s*(万元|亿元|元|万|人|人月|个月|天|台|套|%|%)')
267
+
268
+ # 数量口径:「从七个维度识别」与「按五个维度识别」出现在同一份文件里,是评审最容易抓的硬伤,
269
+ # 而人工通读几乎发现不了(两处隔了几页,各自读都通顺)。只查「结构性名词」的数量(模块/维度/阶段…),
270
+ # 且只在**同一名词 + 同一后续两字**(如「维度识别」「微服务组成」)或两处都带「共/总计」这类总括词时才比——
271
+ # 「网关包含五个模块」与「平台共十二个模块」是局部与整体,不是矛盾,不能报。
272
+ _CN_DIGIT = {'一': 1, '二': 2, '两': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9}
273
+ COUNT_NOUNS = ('维度|子系统|微服务|模块|服务|层次|阶段|环节|步骤|方面|能力|场景|板块|中心|引擎|组件|大类|类别|'
274
+ '原则|目标|子课题|课题|任务|里程碑|措施|指标|创新点|关键技术|技术难点|痛点|方向|领域|终端|入口|角色|'
275
+ '智能体|专家团|工作流|数据源')
276
+ COUNT_RE = re.compile(
277
+ r'(?<![\d一二两三四五六七八九十百千第前后每近约逾超几数多余若至到~~\--..])'
278
+ r'(?P<n>\d{1,3}|[一二两三四五六七八九十]{1,3})\s*(?P<u>个|项|类|大|层|种|条|套|款|支|组)?\s*'
279
+ r'(?P<noun>' + COUNT_NOUNS + r')(?P<tail>[\u4e00-\u9fff]{0,2})')
280
+ COUNT_TOTAL = re.compile(r'(共|总计|合计|一共|总共|全部|整体|总体上|共计|累计)\s*$')
281
+
282
+
283
+ def _cn_int(s):
284
+ if s.isdigit():
285
+ return int(s)
286
+ if s == '十':
287
+ return 10
288
+ if s.startswith('十'):
289
+ return 10 + _CN_DIGIT.get(s[1:], 0)
290
+ if '十' in s:
291
+ a, _, b = s.partition('十')
292
+ return _CN_DIGIT.get(a, 0) * 10 + (_CN_DIGIT.get(b, 0) if b else 0)
293
+ return _CN_DIGIT.get(s, 0)
294
+
295
+
296
+ def count_conflicts(paras):
297
+ """同一对象前后数量不一致。返回 issue 列表(建议级:数量可能各有语境,交人核对)。"""
298
+ strong, total = defaultdict(list), defaultdict(list)
299
+ for p in paras:
300
+ t = str(p.get('text', ''))
301
+ for m in COUNT_RE.finditer(t):
302
+ n = _cn_int(m.group('n'))
303
+ if n < 2: # 「一个平台」是冠词用法,不是数量主张
304
+ continue
305
+ after = t[m.end():m.end() + 3]
306
+ if re.match(r'\s*(以上|以下|左右|多|余|来)', after):
307
+ continue
308
+ noun, tail = m.group('noun'), m.group('tail')
309
+ snippet = t[max(0, m.start() - 8):m.end() + 4].replace('\n', ' ')
310
+ rec = (n, m.group(0), snippet, p.get('loc', ''))
311
+ if len(tail) == 2:
312
+ strong[(noun, tail)].append(rec)
313
+ if COUNT_TOTAL.search(t[max(0, m.start() - 6):m.start()]):
314
+ total[noun].append(rec)
315
+ out, seen = [], set()
316
+ for key, recs in list(strong.items()) + [((k, '*'), v) for k, v in total.items()]:
317
+ vals = sorted({r[0] for r in recs})
318
+ if len(vals) < 2:
319
+ continue
320
+ sig = (key[0], tuple(vals))
321
+ if sig in seen:
322
+ continue
323
+ seen.add(sig)
324
+ forms = Counter(r[1] for r in recs)
325
+ detail = '、'.join(f'「{f}」×{c}' for f, c in forms.most_common(4))
326
+ where = ';'.join(f'{r[3]}:…{r[2]}…' for r in recs[:3] if r[3])
327
+ out.append(_issue('warn', 'consistency', 'count-conflict', '全文',
328
+ f'同一对象的数量前后不一致:{detail}', where,
329
+ '核对后全文统一(数量写错是评审最容易抓的硬伤;若确是不同对象,改写成能区分的叫法)'))
330
+ return out
331
+
332
+
333
+ def check_document(paras, strict=False):
334
+ """文档级检查:术语一致、数字矛盾、AI 味、结构均匀度。paras = [{'text','kind','loc'}]。"""
335
+ out = []
336
+ bodies = [p for p in paras if p.get('kind') in (None, 'body')]
337
+ heads = [p for p in paras if p.get('kind') == 'heading']
338
+ full = '\n'.join(str(p.get('text', '')) for p in paras)
339
+ if not full.strip():
340
+ return out
341
+
342
+ # ① 拉丁词大小写不一致
343
+ byline = defaultdict(Counter)
344
+ for m in LATIN_TOKEN.finditer(full):
345
+ tok = m.group(1)
346
+ if tok.lower() in CASE_WHITELIST:
347
+ continue
348
+ byline[tok.lower()][tok] += 1
349
+ for low, forms in byline.items():
350
+ if len(forms) < 2 or sum(forms.values()) < 3:
351
+ continue
352
+ # 每种拼法都出现两次以上 = 确实在混用(建议级);只出现一次的少数拼法更像手误(提示级)。
353
+ # 不按「每种都 ≥2」一刀切:真实文档里最常见的正是「用了三次 API、漏打一次 api」。
354
+ lvl = 'warn' if all(v >= 2 for v in forms.values()) else 'info'
355
+ detail = '、'.join(f'{k}×{v}' for k, v in sorted(forms.items(), key=lambda x: -x[1]))
356
+ out.append(_issue(lvl, 'consistency', 'term-variant', '全文',
357
+ f'同一术语多种拼法:{detail}', '',
358
+ f'择一并全文统一(通常取出现最多的「{max(forms, key=forms.get)}」)'))
359
+ # ② 中文角色词多种叫法
360
+ for group, prefer in ROLE_SYNONYMS:
361
+ forms = {w: full.count(w) for w in group}
362
+ forms = {k: v for k, v in forms.items() if v >= 2}
363
+ if len(forms) >= 2:
364
+ detail = '、'.join(f'{k}×{v}' for k, v in sorted(forms.items(), key=lambda x: -x[1]))
365
+ out.append(_issue('info', 'consistency', 'term-role', '全文',
366
+ f'同一角色多种叫法:{detail}', '',
367
+ f'全文统一(合同类文件通常用「{prefer}」或首次出现处的写法)'))
368
+
369
+ # 数字一致:同一「单位+量级」的关键数字在全文出现多个不同值时提请核对
370
+ by_unit = defaultdict(Counter)
371
+ for m in NUM_UNIT_RE.finditer(full):
372
+ by_unit[m.group(2)][m.group(1)] += 1
373
+ for unit in ('万元', '亿元', '人月'):
374
+ vals = by_unit.get(unit)
375
+ if vals and len(vals) >= 3:
376
+ top = '、'.join(f'{v}{unit}' for v, _ in vals.most_common(5))
377
+ out.append(_issue('info', 'consistency', 'number-scan', '全文',
378
+ f'全文出现 {len(vals)} 个不同的「{unit}」数值:{top} …'
379
+ f'——总投资/总工期这类数字必须处处相同,请专项核对', '', ''))
380
+
381
+ out.extend(count_conflicts(paras))
382
+
383
+ # 标题重复
384
+ dup = [t for t, n in Counter(str(p.get('text', '')).strip() for p in heads).items() if t and n > 1]
385
+ for t in dup[:5]:
386
+ out.append(_issue('warn', 'structure', 'dup-heading', '全文', f'标题重复出现:{t}', '', ''))
387
+
388
+ out.extend(ai_flavor(bodies, heads, strict=strict))
389
+ return out
390
+
391
+
392
+ # ── 文档级:AI 味 ──────────────────────────────────────────────────────────
393
+ def ai_flavor(bodies, heads=(), strict=False):
394
+ """AI 味体检。三层:词汇层(套话/黑话密度)、句式层(连接词/破折号/排比)、结构层(均匀度/具体性)。
395
+
396
+ 结构层才是真指纹:人写的东西是不均匀的——有的节写得长,有的节两句话带过;
397
+ 模型写的每节都恰好三段、每段都恰好四句、每条列项都恰好一行。
398
+ """
399
+ out = []
400
+ texts = [str(p.get('text', '')) for p in bodies]
401
+ full = '\n'.join(texts)
402
+ total = _cjk_len(full)
403
+ if total < 400: # 太短的文本统计量没有意义,不硬判
404
+ return out
405
+ k = total / 1000.0 # 按每千字密度衡量
406
+
407
+ def _label(w):
408
+ """词表里的条目是正则,报告要给人看:把 `.{0,8}` 这类量词还原成「…」,
409
+ 否则会打出「随着0,8的不断发展」这种没人看得懂的东西。"""
410
+ return re.sub(r'\.\{\d+,\d+\}', '…', re.sub(r'[\\[\]()*+?|]', '', w))
411
+
412
+ def hits(words):
413
+ c = Counter()
414
+ for w in words:
415
+ n = len(re.findall(w, full))
416
+ if n:
417
+ c[_label(w)] = n
418
+ return c
419
+
420
+ cl = hits(AI_CLICHE)
421
+ if sum(cl.values()) / k >= 2.0 and sum(cl.values()) >= 3:
422
+ top = '、'.join(f'{w}×{n}' for w, n in cl.most_common(6))
423
+ out.append(_issue('warn', 'ai', 'cliche', '全文',
424
+ f'套话密度偏高(每千字 {sum(cl.values())/k:.1f} 处):{top}',
425
+ '', '删掉承接性套话,直接说结论;「综上所述」全文留一处足矣'))
426
+ bz = hits(AI_BUZZ)
427
+ if sum(bz.values()) / k >= 2.0 and sum(bz.values()) >= 3:
428
+ top = '、'.join(f'{w}×{n}' for w, n in bz.most_common(6))
429
+ out.append(_issue('warn', 'ai', 'buzzword', '全文',
430
+ f'空转动词/黑话密度偏高(每千字 {sum(bz.values())/k:.1f} 处):{top}',
431
+ '', '换成具体动作:「赋能业务」→「把审批从 3 天缩到 4 小时」'))
432
+ em = hits(AI_EMPTY)
433
+ if sum(em.values()) / k >= 6.0:
434
+ top = '、'.join(f'{w}×{n}' for w, n in em.most_common(6))
435
+ out.append(_issue('info', 'ai', 'empty-adv', '全文',
436
+ f'空洞副词密度高(每千字 {sum(em.values())/k:.1f} 处):{top}',
437
+ '', '「有效提升」→给提升了多少;给不出数字就删掉副词'))
438
+
439
+ cn = hits(AI_CONNECT)
440
+ if sum(cn.values()) >= 8 and sum(cn.values()) / k >= 3.0:
441
+ out.append(_issue('info', 'ai', 'connector', '全文',
442
+ f'「首先/其次/再次」类连接词 {sum(cn.values())} 处,通篇同一套承接骨架',
443
+ '', '改用小标题或直接分条,别用连接词假装有逻辑'))
444
+
445
+ dash = full.count('——')
446
+ if dash / k >= 3.0 and dash >= 5:
447
+ out.append(_issue('info', 'ai', 'dash-overuse', '全文',
448
+ f'破折号 {dash} 处(每千字 {dash/k:.1f}),是模型口癖的显著特征',
449
+ '', '大部分破折号可换成句号另起一句,或换成冒号'))
450
+
451
+ # 句式层:段首雷同
452
+ starts = Counter(t.strip()[:4] for t in texts if len(t.strip()) >= 8)
453
+ for pre, n in starts.most_common(3):
454
+ if n >= 4 and n / max(len(texts), 1) >= 0.25:
455
+ out.append(_issue('warn', 'ai', 'same-opening', '全文',
456
+ f'{n} 个段落以「{pre}…」同样的方式开头(占 {n/len(texts)*100:.0f}%)',
457
+ '', '把其中一半改成直接给结论或直接给数字'))
458
+ break
459
+
460
+ # 结构层一:段落长度过于均匀(人写的东西是参差的)
461
+ lens = [_cjk_len(t) for t in texts if _cjk_len(t) >= 20]
462
+ if len(lens) >= 6:
463
+ mu = sum(lens) / len(lens)
464
+ sd = math.sqrt(sum((x - mu) ** 2 for x in lens) / len(lens))
465
+ cv = sd / mu if mu else 1.0
466
+ if cv < 0.22:
467
+ out.append(_issue('warn', 'ai', 'uniform-para', '全文',
468
+ f'{len(lens)} 个段落长度异常均匀(均值 {mu:.0f} 字,离散度 {cv:.2f},'
469
+ f'人写作通常 >0.35)——这是模板化生成最明显的指纹',
470
+ '', '该长的写长、该一句带过的就一句:把最重要的一节扩到两倍,删掉凑数的段'))
471
+
472
+ # 结构层二:具体性锚点缺失
473
+ with_num = sum(1 for t in texts if SPECIFIC_RE.search(t))
474
+ if texts and with_num / len(texts) < 0.25 and len(texts) >= 8:
475
+ out.append(_issue('warn', 'ai', 'no-anchor', '全文',
476
+ f'仅 {with_num}/{len(texts)} 个段落含数字(<25%),通篇在讲原则不讲事实',
477
+ '', '每段至少落一个可核对的锚点:数量、日期、金额、比例、责任人'))
478
+ if not DATE_RE.search(full):
479
+ out.append(_issue('info', 'ai', 'no-date', '全文',
480
+ '全文没有出现任何具体年月,进度与里程碑无法核对', '', '补上时间点'))
481
+
482
+ # 结构层三:每节字数过于均匀(比段落更强的指纹)
483
+ if heads:
484
+ out.extend(_section_evenness(heads, bodies))
485
+ return out
486
+
487
+
488
+ def _section_evenness(heads, bodies):
489
+ """按标题切节,看各节篇幅是否被均匀摊平。切不出节就静默返回——宁可不报也不瞎报。"""
490
+ order = sorted(bodies + list(heads), key=lambda p: p.get('index', 0))
491
+ secs, cur = [], None
492
+ for p in order:
493
+ if p.get('kind') == 'heading':
494
+ if cur:
495
+ secs.append(cur)
496
+ cur = 0
497
+ elif cur is not None:
498
+ cur += _cjk_len(str(p.get('text', '')))
499
+ if cur is not None:
500
+ secs.append(cur)
501
+ secs = [s for s in secs if s >= 50]
502
+ if len(secs) < 5:
503
+ return []
504
+ mu = sum(secs) / len(secs)
505
+ sd = math.sqrt(sum((x - mu) ** 2 for x in secs) / len(secs))
506
+ cv = sd / mu if mu else 1.0
507
+ if cv < 0.25:
508
+ return [_issue('warn', 'ai', 'uniform-section', '全文',
509
+ f'{len(secs)} 个章节篇幅异常均匀(均值 {mu:.0f} 字,离散度 {cv:.2f})——'
510
+ f'每节都写得一样长,说明是在填格子而不是在说事',
511
+ '', '按重要性重新分配篇幅:核心章节写透,次要章节敢于只写三行')]
512
+ return []
513
+
514
+
515
+ # ── 汇总入口 ───────────────────────────────────────────────────────────────
516
+ def check(paras, strict=False):
517
+ """整篇体检:逐段 + 文档级。paras = [{'text','kind','loc','index'}]。"""
518
+ out = []
519
+ for p in paras:
520
+ out.extend(check_paragraph(p.get('text', ''), p.get('loc', ''), p.get('kind', 'body'), strict))
521
+ out.extend(check_document(paras, strict))
522
+ return out
523
+
524
+
525
+ def from_markdown(path_or_text, is_path=True):
526
+ """Markdown/纯文本 → paras(供 CLI 与草稿预检用;表格行按 cell 处理)。"""
527
+ text = (open(path_or_text, encoding='utf-8-sig').read() if is_path
528
+ else str(path_or_text)).lstrip('\ufeff')
529
+ paras, buf = [], []
530
+ for i, raw in enumerate(text.splitlines() + ['']):
531
+ line = raw.rstrip()
532
+ m = re.match(r'^(#{1,6})\s+(.*)', line)
533
+ if m or not line.strip() or line.lstrip().startswith('|'):
534
+ if buf:
535
+ paras.append({'text': ' '.join(buf), 'kind': 'body',
536
+ 'loc': f'第 {i - len(buf) + 1} 行', 'index': len(paras)})
537
+ buf = []
538
+ if m:
539
+ paras.append({'text': m.group(2).strip(), 'kind': 'heading',
540
+ 'loc': f'第 {i+1} 行', 'index': len(paras)})
541
+ elif line.lstrip().startswith('|'):
542
+ paras.append({'text': line.strip().strip('|'), 'kind': 'cell',
543
+ 'loc': f'第 {i+1} 行', 'index': len(paras)})
544
+ elif line.strip():
545
+ buf.append(re.sub(r'^\s*(?:[-*+]|\d+[.、])\s*', '', line.strip()))
546
+ return paras
547
+
548
+
549
+ LEVEL_ORDER = {'error': 0, 'warn': 1, 'info': 2}
550
+ LEVEL_LABEL = {'error': '必修', 'warn': '建议', 'info': '提示'}
551
+ GROUP_LABEL = {'text': '文字', 'punct': '标点', 'ai': 'AI 味',
552
+ 'consistency': '一致性', 'structure': '结构'}
553
+
554
+
555
+ def report(issues, limit_per_kind=4):
556
+ """把 issues 排成人读的报告(同类问题折叠,只列前几条 + 计数)。"""
557
+ if not issues:
558
+ return '文字体检:未发现问题。'
559
+ lines, counts = [], Counter(i['level'] for i in issues)
560
+ lines.append(f"文字体检:必修 {counts.get('error',0)} · 建议 {counts.get('warn',0)} · 提示 {counts.get('info',0)}")
561
+ grouped = defaultdict(list)
562
+ for it in issues:
563
+ grouped[(it['level'], it['group'], it['kind'])].append(it)
564
+ for key in sorted(grouped, key=lambda k: (LEVEL_ORDER[k[0]], k[1], k[2])):
565
+ lvl, grp, _ = key
566
+ items = grouped[key]
567
+ head = f" [{LEVEL_LABEL[lvl]}·{GROUP_LABEL.get(grp, grp)}] {items[0]['msg']}"
568
+ if len(items) > 1:
569
+ head += f'(共 {len(items)} 处)'
570
+ lines.append(head)
571
+ for it in items[:limit_per_kind]:
572
+ where = f"{it['loc']} " if it['loc'] and it['loc'] != '全文' else ''
573
+ q = f"「{it['quote']}」" if it['quote'] else ''
574
+ sug = f" → {it['suggest']}" if it['suggest'] else ''
575
+ if where or q or sug:
576
+ lines.append(f' · {where}{q}{sug}')
577
+ if len(items) > limit_per_kind:
578
+ lines.append(f' · …另有 {len(items)-limit_per_kind} 处')
579
+ return '\n'.join(lines)
580
+
581
+
582
+ def self_test():
583
+ bad = '本项目己经按装完成,采用了先进的技术,并且部署了系统;取得了显著成效。'
584
+ # 判据自检:带逗号的长句只该报 sent-run(且报的是最长那一截),不该报「整句过长」
585
+ _run = check_paragraph('研制一套面向 5000 节点规模的智能运维平台系统软件与配套工具链并完成部署,' * 1
586
+ + '把时间压到 8 分钟。', 'R')
587
+ assert not any(i['kind'] == 'sent-long' for i in _run), _run
588
+ iss = check_paragraph(bad, 'T1')
589
+ kinds = {i['kind'] for i in iss}
590
+ assert 'typo' in kinds, kinds
591
+ # 这些是**合法中文**,一条都不许报重复
592
+ for ok_txt in ('明确项目目的的重要性。', '这和和平发展有关。', '为了了解情况。',
593
+ '在在线账户权益领域,风险突出。', '项目的的确确完成了。',
594
+ '把把手换掉。', '从从容容推进。', '认认真真核对一遍。'):
595
+ bad = [i for i in check_paragraph(ok_txt, 'F') if i['kind'] in ('dup-char', 'dup-phrase')]
596
+ assert not bad, (ok_txt, bad)
597
+ # 这些是真事故,必须报
598
+ assert any(i['kind'] == 'dup-char' for i in check_paragraph('这的的的确不对。', 'D'))
599
+ assert any(i['kind'] == 'dup-phrase' for i in check_paragraph('本项目本项目已完成。', 'P'))
600
+ assert {'half-comma', 'half-semi'} & kinds, kinds
601
+ assert check_paragraph('一、项目背景。', 'H', 'heading')[0]['kind'] == 'heading-period'
602
+ assert not check_paragraph('本季度交付 12 个模块,缺陷密度 0.8 个/千行。', 'OK')
603
+ assert not check_paragraph('—', 'CELL', 'cell'), '表格里当「无」用的占位符不该判成破折号错误'
604
+ assert any(i['kind'] == 'bad-dash' for i in check_paragraph('这里—用错了破折号。', 'D')), '真破折号误用要报'
605
+
606
+ # AI 味:同一批话堆到 1000 字以上才该报
607
+ ai = ('值得注意的是,本系统通过深度赋能业务闭环,有效提升了整体效能。'
608
+ '综上所述,该方案具有重要的现实意义,为后续工作奠定了坚实的基础。') * 20
609
+ paras = [{'text': ai[i:i+120], 'kind': 'body', 'index': i} for i in range(0, len(ai), 120)]
610
+ ks = {i['kind'] for i in check_document(paras)}
611
+ assert 'cliche' in ks and 'buzzword' in ks, ks
612
+ assert 'uniform-para' in ks or 'no-anchor' in ks, ks
613
+ # 反向:真实写作不应被判成 AI(长短参差 + 处处有数字)
614
+ real = [
615
+ {'text': '一期上线 3 个模块,比计划晚了 11 天,原因是甲方 4 月才给出接口口径。', 'kind': 'body', 'index': 0},
616
+ {'text': '延期没有影响验收:合同里的里程碑是 6 月 30 日,我们 6 月 18 日封版。', 'kind': 'body', 'index': 1},
617
+ {'text': '缺陷密度 0.62 个/千行。', 'kind': 'body', 'index': 2},
618
+ {'text': '压测在 800 并发下 P95 响应 340ms,未达 300ms 目标,二期做连接池改造。'
619
+ '这是本期唯一没有兑现的指标,已写进遗留问题清单,责任人为张工,计划 9 月 15 日前关闭。', 'kind': 'body', 'index': 3},
620
+ {'text': '成本超了 8.4 万元,主要是压测环境多租了两个月机器。', 'kind': 'body', 'index': 4},
621
+ {'text': '2026 年 7 月 3 日通过内部评审。', 'kind': 'body', 'index': 5},
622
+ ] * 3
623
+ ks2 = {i['kind'] for i in check_document(real)}
624
+ assert 'cliche' not in ks2 and 'buzzword' not in ks2, ks2
625
+ assert 'no-anchor' not in ks2, ks2
626
+ assert 'term-variant' not in ks2, ks2 # 正常文字不该被判术语不一致
627
+ tv = [{'text': '本系统提供 API 接口。Api 需鉴权,api 限流 100 QPS,API 版本 v2。',
628
+ 'kind': 'body', 'index': 0}]
629
+ assert any(i['kind'] == 'term-variant' for i in check_document(tv * 2)), '大小写不一致漏报'
630
+ print('[zh_prose] self-test OK ·', f'规则 {len(TYPOS)} 错别字 / {len(CONFUSABLES)} 易混 / '
631
+ f'{len(PUNCT_RULES)} 标点 / {len(AI_CLICHE)+len(AI_BUZZ)} AI 词条')
632
+
633
+
634
+ if __name__ == '__main__':
635
+ args = [a for a in sys.argv[1:] if not a.startswith('-')]
636
+ if not args:
637
+ self_test()
638
+ else:
639
+ print(report(check(from_markdown(args[0]), strict='--strict' in sys.argv)))