@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.
- package/LICENSE +391 -32
- package/README.md +176 -10
- package/bin/huicai.js +166 -73
- package/package.json +27 -8
- package/skills/ab-testing/trigger-cases.json +30 -0
- package/skills/academic-writing/trigger-cases.json +30 -0
- package/skills/accounting-ops/trigger-cases.json +30 -0
- package/skills/ai-agent/trigger-cases.json +30 -0
- package/skills/ai-aiops/trigger-cases.json +30 -0
- package/skills/ai-coding/trigger-cases.json +30 -0
- package/skills/ai-compliance/trigger-cases.json +30 -0
- package/skills/ai-eval/trigger-cases.json +30 -0
- package/skills/ai-llm-app/trigger-cases.json +30 -0
- package/skills/ai-mlops/trigger-cases.json +30 -0
- package/skills/ai-multimodal/trigger-cases.json +30 -0
- package/skills/ai-prompt/trigger-cases.json +30 -0
- package/skills/ai-rag/trigger-cases.json +30 -0
- package/skills/anomaly-detection/trigger-cases.json +30 -0
- package/skills/api-design/trigger-cases.json +31 -0
- package/skills/api-integration/trigger-cases.json +31 -0
- package/skills/arch-design/trigger-cases.json +12 -0
- package/skills/arch-diagram/SKILL.md +118 -0
- package/skills/arch-diagram/trigger-cases.json +30 -0
- package/skills/arch-distributed/trigger-cases.json +30 -0
- package/skills/arch-multitenant/trigger-cases.json +30 -0
- package/skills/audio-brief/SKILL.md +65 -0
- package/skills/audio-brief/trigger-cases.json +30 -0
- package/skills/audit-assist/trigger-cases.json +30 -0
- package/skills/audit-chain/trigger-cases.json +31 -0
- package/skills/banking-credit/trigger-cases.json +30 -0
- package/skills/brand-design/trigger-cases.json +30 -0
- package/skills/business-plan/trigger-cases.json +30 -0
- package/skills/capital-ops/trigger-cases.json +30 -0
- package/skills/cicd-pipeline/trigger-cases.json +31 -0
- package/skills/code-archaeology/SKILL.md +55 -0
- package/skills/code-archaeology/trigger-cases.json +9 -0
- package/skills/code-quality/SKILL.md +138 -0
- package/skills/code-quality/references/languages.md +107 -0
- package/skills/code-quality/scripts/__pycache__/code_battery.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_check.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_check.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_fix.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_fix.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_java.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_java.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_lens.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_lens.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_python.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_python.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_testplan.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/code_battery.py +1072 -0
- package/skills/code-quality/scripts/code_check.py +1269 -0
- package/skills/code-quality/scripts/code_csharp.py +371 -0
- package/skills/code-quality/scripts/code_fix.py +201 -0
- package/skills/code-quality/scripts/code_java.py +695 -0
- package/skills/code-quality/scripts/code_jsts.py +313 -0
- package/skills/code-quality/scripts/code_lens.py +1079 -0
- package/skills/code-quality/scripts/code_python.py +536 -0
- package/skills/code-quality/scripts/code_testplan.py +239 -0
- package/skills/code-quality/trigger-cases.json +45 -0
- package/skills/code-review/SKILL.md +24 -0
- package/skills/code-review/trigger-cases.json +35 -0
- package/skills/commit-and-pr/trigger-cases.json +31 -0
- package/skills/content-creation/trigger-cases.json +30 -0
- package/skills/corporate-governance/trigger-cases.json +30 -0
- package/skills/crawl-chain/trigger-cases.json +31 -0
- package/skills/customer-service/trigger-cases.json +30 -0
- package/skills/data-analysis/trigger-cases.json +30 -0
- package/skills/data-chain/SKILL.md +1 -1
- package/skills/data-chain/trigger-cases.json +31 -0
- package/skills/data-governance/trigger-cases.json +30 -0
- package/skills/data-report-pro/trigger-cases.json +30 -0
- package/skills/data-streaming/trigger-cases.json +30 -0
- package/skills/data-wrangling/SKILL.md +1 -1
- package/skills/data-wrangling/trigger-cases.json +30 -0
- package/skills/db-es/trigger-cases.json +30 -0
- package/skills/db-migration/trigger-cases.json +30 -0
- package/skills/db-mysql/trigger-cases.json +30 -0
- package/skills/db-postgres/trigger-cases.json +30 -0
- package/skills/db-redis/trigger-cases.json +30 -0
- package/skills/debug-root-cause/trigger-cases.json +30 -0
- package/skills/dependency-audit/SKILL.md +56 -0
- package/skills/dependency-audit/trigger-cases.json +7 -0
- package/skills/dependency-upgrade/trigger-cases.json +31 -0
- package/skills/dev-cpp/trigger-cases.json +30 -0
- package/skills/dev-cross-platform/trigger-cases.json +30 -0
- package/skills/dev-csharp/SKILL.md +23 -0
- package/skills/dev-csharp/trigger-cases.json +30 -0
- package/skills/dev-frontend/SKILL.md +19 -0
- package/skills/dev-frontend/trigger-cases.json +30 -0
- package/skills/dev-go/trigger-cases.json +30 -0
- package/skills/dev-harmony/trigger-cases.json +30 -0
- package/skills/dev-java/SKILL.md +20 -0
- package/skills/dev-java/trigger-cases.json +30 -0
- package/skills/dev-mobile/trigger-cases.json +30 -0
- package/skills/dev-node/trigger-cases.json +30 -0
- package/skills/dev-php/trigger-cases.json +30 -0
- package/skills/dev-python/SKILL.md +21 -0
- package/skills/dev-python/trigger-cases.json +30 -0
- package/skills/dev-rust/trigger-cases.json +30 -0
- package/skills/dev-toolchain/trigger-cases.json +31 -0
- package/skills/doc-chain/trigger-cases.json +31 -0
- package/skills/doc-compare/trigger-cases.json +30 -0
- package/skills/docx-author/SKILL.md +127 -25
- package/skills/docx-author/scripts/__pycache__/docx_battery.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_build.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_check.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_layout.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/zh_prose.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/docx_battery.py +586 -0
- package/skills/docx-author/scripts/docx_build.py +995 -0
- package/skills/docx-author/scripts/docx_check.py +1306 -0
- package/skills/docx-author/scripts/docx_doc.py +14 -154
- package/skills/docx-author/scripts/docx_layout.py +490 -0
- package/skills/docx-author/scripts/zh_prose.py +639 -0
- package/skills/docx-author/trigger-cases.json +58 -0
- package/skills/echo-brief/SKILL.md +84 -0
- package/skills/echo-brief/references/echo-format.md +68 -0
- package/skills/echo-brief/trigger-cases.json +40 -0
- package/skills/ecommerce-ops/trigger-cases.json +30 -0
- package/skills/education-training/trigger-cases.json +30 -0
- package/skills/ehs-safety/trigger-cases.json +30 -0
- package/skills/email-writing/trigger-cases.json +30 -0
- package/skills/eng-ai/trigger-cases.json +30 -0
- package/skills/eng-architecture/trigger-cases.json +31 -0
- package/skills/eng-automation/trigger-cases.json +31 -0
- package/skills/eng-collaboration/trigger-cases.json +30 -0
- package/skills/eng-complex-env/trigger-cases.json +30 -0
- package/skills/eng-data-platform/trigger-cases.json +31 -0
- package/skills/eng-database/trigger-cases.json +31 -0
- package/skills/eng-develop/trigger-cases.json +30 -0
- package/skills/eng-docs/SKILL.md +24 -0
- package/skills/eng-docs/trigger-cases.json +30 -0
- package/skills/eng-efficiency/trigger-cases.json +30 -0
- package/skills/eng-infra/trigger-cases.json +30 -0
- package/skills/eng-integration/trigger-cases.json +31 -0
- package/skills/eng-legacy/trigger-cases.json +31 -0
- package/skills/eng-observability/trigger-cases.json +31 -0
- package/skills/eng-ops/trigger-cases.json +31 -0
- package/skills/eng-performance/trigger-cases.json +31 -0
- package/skills/eng-planning/trigger-cases.json +31 -0
- package/skills/eng-quality/SKILL.md +8 -0
- package/skills/eng-quality/trigger-cases.json +31 -0
- package/skills/eng-release/trigger-cases.json +31 -0
- package/skills/eng-reliability/trigger-cases.json +31 -0
- package/skills/eng-requirements/trigger-cases.json +12 -0
- package/skills/eng-research/trigger-cases.json +30 -0
- package/skills/eng-security/trigger-cases.json +31 -0
- package/skills/eng-ux/trigger-cases.json +30 -0
- package/skills/engineering-chain/trigger-cases.json +30 -0
- package/skills/env-offline/trigger-cases.json +31 -0
- package/skills/env-private-deploy/trigger-cases.json +31 -0
- package/skills/env-xinchuang/trigger-cases.json +31 -0
- package/skills/equipment-tpm/trigger-cases.json +30 -0
- package/skills/esg-report/trigger-cases.json +30 -0
- package/skills/event-management/trigger-cases.json +30 -0
- package/skills/excel-analysis-chain/SKILL.md +17 -4
- package/skills/excel-analysis-chain/trigger-cases.json +31 -0
- package/skills/explain-codebase/trigger-cases.json +11 -0
- package/skills/file-ingest/trigger-cases.json +30 -0
- package/skills/finance-analysis/trigger-cases.json +31 -0
- package/skills/finance-chain/trigger-cases.json +31 -0
- package/skills/financial-modeling/trigger-cases.json +31 -0
- package/skills/find-simplifications/SKILL.md +43 -0
- package/skills/find-simplifications/trigger-cases.json +23 -0
- package/skills/foreign-trade/trigger-cases.json +30 -0
- package/skills/funnel-cohort/trigger-cases.json +30 -0
- package/skills/gov-document/trigger-cases.json +30 -0
- package/skills/gui-change-evidence/SKILL.md +47 -0
- package/skills/gui-change-evidence/trigger-cases.json +23 -0
- package/skills/hr-chain/trigger-cases.json +31 -0
- package/skills/hr-operations/trigger-cases.json +30 -0
- package/skills/info-disclosure/trigger-cases.json +31 -0
- package/skills/infra-gateway/trigger-cases.json +31 -0
- package/skills/infra-k8s/trigger-cases.json +31 -0
- package/skills/infra-mq/trigger-cases.json +30 -0
- package/skills/insurance-advisory/trigger-cases.json +30 -0
- package/skills/internal-audit/trigger-cases.json +30 -0
- package/skills/ip-management/trigger-cases.json +31 -0
- package/skills/ipo-listing/trigger-cases.json +31 -0
- package/skills/it-audit/trigger-cases.json +31 -0
- package/skills/itsm-ops/trigger-cases.json +31 -0
- package/skills/knowledge-sop/trigger-cases.json +31 -0
- package/skills/legal-chain/trigger-cases.json +31 -0
- package/skills/legal-contract/trigger-cases.json +30 -0
- package/skills/litigation-support/trigger-cases.json +31 -0
- package/skills/logistics-warehouse/trigger-cases.json +30 -0
- package/skills/long-doc-analysis/trigger-cases.json +31 -0
- package/skills/management-accounting/trigger-cases.json +30 -0
- package/skills/manufacturing-chain/trigger-cases.json +31 -0
- package/skills/manufacturing-quality/trigger-cases.json +30 -0
- package/skills/marketing-analytics/trigger-cases.json +31 -0
- package/skills/marketing-chain/trigger-cases.json +31 -0
- package/skills/meeting-minutes/trigger-cases.json +30 -0
- package/skills/metrics-system/trigger-cases.json +31 -0
- package/skills/negotiation-prep/trigger-cases.json +30 -0
- package/skills/obs-prometheus/trigger-cases.json +31 -0
- package/skills/office-automation/trigger-cases.json +31 -0
- package/skills/okr-review/trigger-cases.json +30 -0
- package/skills/ops-fleet/trigger-cases.json +31 -0
- package/skills/ops-linux/trigger-cases.json +31 -0
- package/skills/optimization-solver/trigger-cases.json +30 -0
- package/skills/patent-disclosure/SKILL.md +105 -0
- package/skills/patent-disclosure/references/figures.md +110 -0
- package/skills/patent-disclosure/references/outline.md +162 -0
- package/skills/patent-disclosure/scripts/patent_docx.py +230 -0
- package/skills/patent-disclosure/trigger-cases.json +31 -0
- package/skills/patent-drafting/SKILL.md +108 -0
- package/skills/patent-drafting/trigger-cases.json +31 -0
- package/skills/patent-portfolio/SKILL.md +81 -0
- package/skills/patent-portfolio/trigger-cases.json +31 -0
- package/skills/pdf-toolkit/trigger-cases.json +31 -0
- package/skills/perf-optimize/trigger-cases.json +31 -0
- package/skills/pivot-crosstab/trigger-cases.json +30 -0
- package/skills/policy-analysis/trigger-cases.json +31 -0
- package/skills/portfolio-analysis/trigger-cases.json +31 -0
- package/skills/ppt-chain/SKILL.md +14 -6
- package/skills/ppt-chain/trigger-cases.json +31 -0
- package/skills/pptx-author/SKILL.md +86 -33
- package/skills/pptx-author/scripts/__pycache__/pptx_check.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/__pycache__/pptx_deck.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/__pycache__/pptx_layout.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/pptx_battery.py +178 -0
- package/skills/pptx-author/scripts/pptx_build.py +119 -0
- package/skills/pptx-author/scripts/pptx_check.py +1182 -0
- package/skills/pptx-author/scripts/pptx_deck.py +736 -145
- package/skills/pptx-author/scripts/pptx_layout.py +384 -0
- package/skills/pptx-author/trigger-cases.json +15 -0
- package/skills/pr-comms/trigger-cases.json +30 -0
- package/skills/procurement-supply/trigger-cases.json +31 -0
- package/skills/product-design/trigger-cases.json +31 -0
- package/skills/production-planning/trigger-cases.json +31 -0
- package/skills/project-management/trigger-cases.json +31 -0
- package/skills/project-proposal/trigger-cases.json +31 -0
- package/skills/prose-standard/SKILL.md +53 -0
- package/skills/prose-standard/trigger-cases.json +23 -0
- package/skills/qa-api-automation/trigger-cases.json +31 -0
- package/skills/qa-mobile-automation/trigger-cases.json +31 -0
- package/skills/qa-ui-automation/trigger-cases.json +31 -0
- package/skills/quant-analysis/trigger-cases.json +31 -0
- package/skills/rag-triage/SKILL.md +64 -0
- package/skills/rag-triage/evals.json +57 -0
- package/skills/rag-triage/references/triage-playbook.md +75 -0
- package/skills/rag-triage/trigger-cases.json +31 -0
- package/skills/real-estate-property/trigger-cases.json +30 -0
- package/skills/refactor-safely/SKILL.md +1 -1
- package/skills/refactor-safely/trigger-cases.json +30 -0
- package/skills/regression-forecast/trigger-cases.json +31 -0
- package/skills/release-checklist/trigger-cases.json +31 -0
- package/skills/report-craft/SKILL.md +76 -0
- package/skills/report-craft/references/feasibility.md +118 -0
- package/skills/report-craft/references/review-checklist.md +51 -0
- package/skills/report-craft/references/visuals.md +114 -0
- package/skills/report-craft/references/writing.md +80 -0
- package/skills/report-craft/scripts/__pycache__/chart_render.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/__pycache__/fin_model.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/__pycache__/project_facts.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/chart_render.py +1222 -0
- package/skills/report-craft/scripts/fin_model.py +602 -0
- package/skills/report-craft/scripts/project_facts.py +580 -0
- package/skills/report-craft/scripts/report_battery.py +349 -0
- package/skills/report-craft/trigger-cases.json +15 -0
- package/skills/retail-store-ops/trigger-cases.json +30 -0
- package/skills/risk-compliance/trigger-cases.json +31 -0
- package/skills/scope-creep/SKILL.md +45 -0
- package/skills/scope-creep/trigger-cases.json +7 -0
- package/skills/sec-auth/trigger-cases.json +31 -0
- package/skills/sec-compliance/trigger-cases.json +31 -0
- package/skills/securities-chain/trigger-cases.json +31 -0
- package/skills/securities-research/trigger-cases.json +31 -0
- package/skills/security-audit/trigger-cases.json +31 -0
- package/skills/slides-html/SKILL.md +25 -10
- package/skills/slides-html/trigger-cases.json +11 -0
- package/skills/sql-analysis/SKILL.md +110 -0
- package/skills/sql-analysis/trigger-cases.json +34 -0
- package/skills/sql-tuning/trigger-cases.json +31 -0
- package/skills/statistical-testing/trigger-cases.json +31 -0
- package/skills/strategy-planning/trigger-cases.json +30 -0
- package/skills/supply-chain/trigger-cases.json +31 -0
- package/skills/tax-compliance/trigger-cases.json +30 -0
- package/skills/tech-doc/SKILL.md +262 -0
- package/skills/tech-doc/scripts/__pycache__/archive_battery.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_build.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_check.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_spec.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/doc_templates.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/archive_battery.py +860 -0
- package/skills/tech-doc/scripts/archive_build.py +599 -0
- package/skills/tech-doc/scripts/archive_check.py +1307 -0
- package/skills/tech-doc/scripts/archive_spec.py +499 -0
- package/skills/tech-doc/scripts/doc_templates.py +624 -0
- package/skills/tech-doc/trigger-cases.json +57 -0
- package/skills/tech-research/trigger-cases.json +31 -0
- package/skills/tender-bidding/trigger-cases.json +31 -0
- package/skills/translation-pro/trigger-cases.json +30 -0
- package/skills/treasury-mgmt/trigger-cases.json +31 -0
- package/skills/trim-cot-leakage/SKILL.md +58 -0
- package/skills/trim-cot-leakage/trigger-cases.json +23 -0
- package/skills/web-craft/SKILL.md +103 -0
- package/skills/web-craft/references/craft-checklist.md +75 -0
- package/skills/web-craft/references/design-direction.md +122 -0
- package/skills/web-craft/references/h5-mobile.md +100 -0
- package/skills/web-craft/references/motion.md +136 -0
- package/skills/web-craft/references/recipes.md +74 -0
- package/skills/web-craft/templates/wedding-invitation.html +1180 -0
- package/skills/web-craft/trigger-cases.json +14 -0
- package/skills/web-scraping/trigger-cases.json +34 -0
- package/skills/web-search/SKILL.md +11 -10
- package/skills/web-search/trigger-cases.json +31 -0
- package/skills/weekly-report/SKILL.md +134 -0
- package/skills/weekly-report/trigger-cases.json +11 -0
- package/skills/write-tests/trigger-cases.json +31 -0
- package/skills/xlsx-analysis/SKILL.md +143 -77
- package/skills/xlsx-analysis/scripts/__pycache__/data_profile.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_build.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_check.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_layout.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_toolkit.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/data_profile.py +391 -0
- package/skills/xlsx-analysis/scripts/xlsx_battery.py +389 -0
- package/skills/xlsx-analysis/scripts/xlsx_build.py +522 -0
- package/skills/xlsx-analysis/scripts/xlsx_check.py +924 -0
- package/skills/xlsx-analysis/scripts/xlsx_layout.py +235 -0
- package/skills/xlsx-analysis/scripts/xlsx_style.py +28 -66
- package/skills/xlsx-analysis/scripts/xlsx_toolkit.py +61 -5
- package/skills/xlsx-analysis/trigger-cases.json +59 -0
- package/squads/hc-defense.md +47 -0
- package/squads/hc-rdarchive.md +110 -0
- package/squads/hc-redteam.md +50 -0
- package/squads/hc-weekly.md +125 -0
- package/src/account.js +1 -0
- package/src/acp.js +1 -0
- package/src/advisor.js +1 -1
- package/src/agent.js +1 -1
- package/src/api.js +1 -1
- package/src/archaeology.js +1 -0
- package/src/ask-ui.js +1 -0
- package/src/assets-import.js +1 -1
- package/src/autofix.js +1 -1
- package/src/automation-tools.js +1 -0
- package/src/automation.js +1 -1
- package/src/autorun.js +1 -1
- package/src/banner.js +1 -1
- package/src/capability-probe.js +1 -0
- package/src/checkpoints.js +1 -1
- package/src/code-tools.js +1 -0
- package/src/codeindex.js +1 -1
- package/src/codemode.js +1 -0
- package/src/command-bridge.js +1 -0
- package/src/command-ui.js +1 -0
- package/src/commands.js +1 -1
- package/src/compaction.js +1 -1
- package/src/computer.js +1 -0
- package/src/config.js +1 -1
- package/src/consult.js +1 -1
- package/src/context-report.js +1 -1
- package/src/context-window.js +1 -0
- package/src/context.js +1 -1
- package/src/crew.js +1 -0
- package/src/crush.js +1 -0
- package/src/daemon.js +1 -1
- package/src/decisions.js +1 -1
- package/src/depaudit.js +1 -0
- package/src/device.js +1 -1
- package/src/diagram-draw.js +1 -0
- package/src/diagram-layouts.js +1 -0
- package/src/diagram-raster.js +1 -0
- package/src/diagram.js +1 -0
- package/src/dictation.js +1 -0
- package/src/diffview.js +1 -1
- package/src/doctor.js +1 -1
- package/src/docx-tools.js +1 -0
- package/src/errkind.js +1 -0
- package/src/evidence.js +1 -0
- package/src/executors.js +1 -1
- package/src/expert-catalog.js +1 -1
- package/src/experts-store.js +1 -1
- package/src/experts.js +1 -1
- package/src/external-agents.js +1 -0
- package/src/forks.js +1 -1
- package/src/fs-observe.js +1 -0
- package/src/gitflow.js +1 -1
- package/src/goalround.js +1 -0
- package/src/hooks.js +1 -1
- package/src/input.js +1 -1
- package/src/insights.js +1 -1
- package/src/jobs.js +1 -0
- package/src/kbsync.js +1 -1
- package/src/lsp.js +1 -0
- package/src/mcp-apps.js +1 -0
- package/src/mcp.js +1 -1
- package/src/memory.js +1 -1
- package/src/notify.js +1 -0
- package/src/permissions.js +1 -1
- package/src/plan-limits.js +1 -0
- package/src/policy.js +1 -0
- package/src/pptx-tools.js +1 -0
- package/src/presence.js +1 -0
- package/src/privatedir.js +1 -0
- package/src/pyruntime.js +1 -1
- package/src/radar.js +1 -0
- package/src/render.js +1 -1
- package/src/repl.js +1 -1
- package/src/report-tools.js +1 -0
- package/src/routing.js +1 -0
- package/src/sandbox-probe.js +1 -0
- package/src/sandbox-win-launcher.mjs +156 -0
- package/src/sandbox.js +1 -0
- package/src/scan.js +1 -1
- package/src/schedule.js +1 -1
- package/src/scopecreep.js +1 -0
- package/src/sdk.js +1 -0
- package/src/selection.js +1 -0
- package/src/session-events.js +1 -0
- package/src/session.js +1 -1
- package/src/sessionindex.js +1 -0
- package/src/sessionlog.js +1 -0
- package/src/skill-catalog.js +1 -0
- package/src/skill-lint.js +1 -0
- package/src/skill-optimize.js +1 -0
- package/src/skill-scan.js +1 -0
- package/src/skills.js +1 -1
- package/src/spill.js +1 -0
- package/src/squad.js +1 -1
- package/src/subagent.js +1 -1
- package/src/surface-state.js +1 -0
- package/src/tapd.js +1 -0
- package/src/terminal-select.js +1 -0
- package/src/terminal.js +1 -0
- package/src/test-runner.js +1 -0
- package/src/todo.js +1 -1
- package/src/toolpipe.js +1 -0
- package/src/tools.js +1 -1
- package/src/toon.js +1 -0
- package/src/transcript.js +1 -1
- package/src/tui/app.js +1 -1
- package/src/tui/command-menu.js +1 -0
- package/src/tui/progress.js +1 -0
- package/src/tui/scroll.js +1 -0
- package/src/tui/welcome.js +1 -0
- package/src/tui/workbench.js +1 -0
- package/src/ui-spec.js +1 -0
- package/src/uitree.js +1 -0
- package/src/uninstall.js +1 -1
- package/src/vision.js +1 -0
- package/src/visual.js +1 -0
- package/src/voice-form.js +1 -0
- package/src/voice.js +1 -0
- package/src/watch.js +1 -1
- package/src/web-audit.js +1 -0
- package/src/web-cdp.js +1 -0
- package/src/web-inspect.js +1 -0
- package/src/websearch.js +1 -0
- package/src/weekly.js +1 -0
- package/src/workflow.js +1 -0
- package/src/worklog.js +1 -0
- package/src/xlsx-tools.js +1 -0
- package/ui/command-picker.js +79 -0
- package/skills/ab-testing/scripts/__pycache__/ab_test.cpython-312.pyc +0 -0
- package/skills/accounting-ops/scripts/__pycache__/acct_tools.cpython-312.pyc +0 -0
- package/skills/anomaly-detection/scripts/__pycache__/anomaly.cpython-312.pyc +0 -0
- package/skills/audit-assist/scripts/__pycache__/audit_sampling.cpython-312.pyc +0 -0
- package/skills/banking-credit/scripts/__pycache__/credit_metrics.cpython-312.pyc +0 -0
- package/skills/data-report-pro/scripts/__pycache__/bigdata.cpython-312.pyc +0 -0
- package/skills/data-wrangling/scripts/__pycache__/join_tools.cpython-312.pyc +0 -0
- package/skills/ecommerce-ops/scripts/__pycache__/gmv_decompose.cpython-312.pyc +0 -0
- package/skills/funnel-cohort/scripts/__pycache__/funnel.cpython-312.pyc +0 -0
- package/skills/management-accounting/scripts/__pycache__/costing.cpython-312.pyc +0 -0
- package/skills/manufacturing-quality/scripts/__pycache__/spc.cpython-312.pyc +0 -0
- package/skills/marketing-analytics/scripts/__pycache__/rfm.cpython-312.pyc +0 -0
- package/skills/optimization-solver/scripts/__pycache__/solver.cpython-312.pyc +0 -0
- package/skills/pivot-crosstab/scripts/__pycache__/pivot_tools.cpython-312.pyc +0 -0
- package/skills/portfolio-analysis/scripts/__pycache__/portfolio.cpython-312.pyc +0 -0
- package/skills/procurement-supply/scripts/__pycache__/supply.cpython-312.pyc +0 -0
- package/skills/production-planning/scripts/__pycache__/mrp_capacity.cpython-312.pyc +0 -0
- package/skills/project-management/scripts/__pycache__/cpm.cpython-312.pyc +0 -0
- package/skills/quant-analysis/scripts/__pycache__/quant.cpython-312.pyc +0 -0
- package/skills/real-estate-property/scripts/__pycache__/property_calc.cpython-312.pyc +0 -0
- package/skills/regression-forecast/scripts/__pycache__/regress.cpython-312.pyc +0 -0
- package/skills/securities-research/scripts/__pycache__/valuation.cpython-312.pyc +0 -0
- package/skills/statistical-testing/scripts/__pycache__/stat_tests.cpython-312.pyc +0 -0
- package/skills/treasury-mgmt/scripts/__pycache__/cashflow_forecast.cpython-312.pyc +0 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
# Python 专属体检:语言陷阱 + 数据分析(pandas)+ 安全。
|
|
2
|
+
#
|
|
3
|
+
# ── 为什么 Python 这一份能比 JS/Java 那两份更准 ──
|
|
4
|
+
# JS 与 Java 走的是「掩码 + 括号配对」,那是权衡的结果(托管 Python 里没有可靠的
|
|
5
|
+
# JS/Java 解析器,引入外部依赖会让体检在最需要它的时候要先装包)。
|
|
6
|
+
# 但 Python 自己有标准库 `ast`——lens 早就把整棵语法树解析好了,只是一直没被用起来。
|
|
7
|
+
#
|
|
8
|
+
# 用 AST 能做到正则做不到的判断,而这些恰恰是 Python 最难查的坑:
|
|
9
|
+
#
|
|
10
|
+
# class Cfg: ← 类属性是 list,**所有实例共享同一个对象**,
|
|
11
|
+
# items = [] 一个实例 append,别的实例全都看得见
|
|
12
|
+
#
|
|
13
|
+
# fns = [lambda: i for i in range(3)] ← 三个 lambda 全都返回 2:
|
|
14
|
+
# 闭包捕获的是变量本身,不是当时的值
|
|
15
|
+
#
|
|
16
|
+
# try: ... ← ValueError 是 Exception 的子类,
|
|
17
|
+
# except Exception: ... 第二个分支**永远不会执行**
|
|
18
|
+
# except ValueError: ...
|
|
19
|
+
#
|
|
20
|
+
# for x in items: ← 遍历时删元素,会跳过紧随其后的那一个
|
|
21
|
+
# if bad(x): items.remove(x)
|
|
22
|
+
#
|
|
23
|
+
# 这四类正则几乎无从下手(要判断类型层级、作用域、名字绑定),AST 一查就准。
|
|
24
|
+
#
|
|
25
|
+
# ── 分组 ──
|
|
26
|
+
# logic.py-* 语言陷阱(共享可变、闭包、异常、迭代、名字遮蔽)
|
|
27
|
+
# framework.py-* pandas 数据分析(链式赋值、已废弃 API、读文件丢精度)
|
|
28
|
+
# security.py-* 反序列化与子进程
|
|
29
|
+
#
|
|
30
|
+
# 误报纪律与主引擎一致:AST 判定为主,拿不准就不报。
|
|
31
|
+
|
|
32
|
+
import ast
|
|
33
|
+
import os
|
|
34
|
+
import re
|
|
35
|
+
import sys
|
|
36
|
+
|
|
37
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
38
|
+
|
|
39
|
+
# 遮蔽这些内置名会让后面的代码行为诡异,且报错信息完全不指向真正的原因
|
|
40
|
+
_BUILTINS = {
|
|
41
|
+
'list', 'dict', 'set', 'tuple', 'str', 'int', 'float', 'bool', 'bytes',
|
|
42
|
+
'id', 'type', 'input', 'filter', 'map', 'max', 'min', 'sum', 'len', 'range',
|
|
43
|
+
'next', 'iter', 'open', 'file', 'hash', 'format', 'object', 'vars', 'dir',
|
|
44
|
+
'all', 'any', 'zip', 'sorted', 'reversed', 'print', 'compile', 'eval', 'exec',
|
|
45
|
+
}
|
|
46
|
+
# 这几个太常用作参数名,报了只会是噪声
|
|
47
|
+
_BUILTIN_OK = {'id', 'type', 'format', 'input', 'object', 'all', 'any', 'next', 'hash', 'dir', 'vars'}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _unit_of(lens, line):
|
|
51
|
+
u = lens.unit_at(line)
|
|
52
|
+
return u.label if u else None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _name_of(node):
|
|
56
|
+
"""把 ast 节点还原成可读的名字(`pd.read_csv` / `df.append`)。"""
|
|
57
|
+
if isinstance(node, ast.Name):
|
|
58
|
+
return node.id
|
|
59
|
+
if isinstance(node, ast.Attribute):
|
|
60
|
+
base = _name_of(node.value)
|
|
61
|
+
return f'{base}.{node.attr}' if base else node.attr
|
|
62
|
+
return ''
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _is_mutable_literal(node):
|
|
66
|
+
if isinstance(node, (ast.List, ast.Dict, ast.Set)):
|
|
67
|
+
return True
|
|
68
|
+
return isinstance(node, ast.Call) and _name_of(node.func) in ('list', 'dict', 'set', 'collections.OrderedDict')
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _check_late_binding(loop_node, var, lens, ctx, out, add):
|
|
72
|
+
"""循环体里定义的函数有没有捕获循环变量 var。"""
|
|
73
|
+
for sub in ast.walk(loop_node):
|
|
74
|
+
if not isinstance(sub, (ast.Lambda, ast.FunctionDef)):
|
|
75
|
+
continue
|
|
76
|
+
args = sub.args
|
|
77
|
+
bound = {a.arg for a in (args.posonlyargs + args.args + args.kwonlyargs)}
|
|
78
|
+
if var in bound:
|
|
79
|
+
continue # 用默认参数固定住了值,是正确写法
|
|
80
|
+
if any(isinstance(x, ast.Name) and x.id == var for x in ast.walk(sub)):
|
|
81
|
+
add(out, ctx, 'logic.py-late-binding', 'error', lens, sub.lineno,
|
|
82
|
+
f'循环里定义的函数引用了循环变量 `{var}` —— 闭包捕获的是**变量本身**而不是当时的值,'
|
|
83
|
+
f'循环结束后它们全都拿到最后一次的 `{var}`',
|
|
84
|
+
f'用默认参数当场固定:`lambda {var}={var}: ...`,或者用 functools.partial',
|
|
85
|
+
_unit_of(lens, sub.lineno))
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def check_python_traps(lens, ctx, out, add):
|
|
90
|
+
"""Python 语言层面的坑,全部走 AST 判定。"""
|
|
91
|
+
tree = getattr(lens, '_ast', None)
|
|
92
|
+
if tree is None:
|
|
93
|
+
return # 解析失败时不猜,主引擎已如实上报降级
|
|
94
|
+
|
|
95
|
+
# ── 类属性是可变对象:所有实例共享同一个,一个改全都变 ──
|
|
96
|
+
for node in ast.walk(tree):
|
|
97
|
+
if not isinstance(node, ast.ClassDef):
|
|
98
|
+
continue
|
|
99
|
+
is_dataclass = any('dataclass' in _name_of(d) for d in node.decorator_list)
|
|
100
|
+
for stmt in node.body:
|
|
101
|
+
targets, value = [], None
|
|
102
|
+
if isinstance(stmt, ast.Assign):
|
|
103
|
+
targets, value = stmt.targets, stmt.value
|
|
104
|
+
elif isinstance(stmt, ast.AnnAssign) and stmt.value is not None:
|
|
105
|
+
targets, value = [stmt.target], stmt.value
|
|
106
|
+
if not _is_mutable_literal(value):
|
|
107
|
+
continue
|
|
108
|
+
for t in targets:
|
|
109
|
+
if not isinstance(t, ast.Name) or t.id.isupper():
|
|
110
|
+
continue # 全大写按常量看待(约定上不会去改它)
|
|
111
|
+
if is_dataclass:
|
|
112
|
+
add(out, ctx, 'logic.py-dataclass-mutable', 'error', lens, stmt.lineno,
|
|
113
|
+
f'dataclass `{node.name}` 的字段 `{t.id}` 用可变对象做默认值 —— '
|
|
114
|
+
'Python 会直接报 ValueError,或者(旧版本里)所有实例共享同一个对象',
|
|
115
|
+
f'改成 `{t.id}: list = field(default_factory=list)`', node.name)
|
|
116
|
+
else:
|
|
117
|
+
add(out, ctx, 'logic.py-mutable-class-attr', 'error', lens, stmt.lineno,
|
|
118
|
+
f'`{node.name}.{t.id}` 是类属性且是可变对象 —— '
|
|
119
|
+
'**所有实例共享同一个**,任何一个实例 append/update,其它实例全都看得见',
|
|
120
|
+
f'挪进 `__init__`:`self.{t.id} = []`。'
|
|
121
|
+
'这个坑在单实例场景下完全正常,等到第二个实例出现才暴露', node.name)
|
|
122
|
+
|
|
123
|
+
# ── 循环里定义的函数捕获循环变量:延迟绑定,最后全都拿到最后一次的值 ──
|
|
124
|
+
for node in ast.walk(tree):
|
|
125
|
+
if not isinstance(node, (ast.For, ast.AsyncFor)):
|
|
126
|
+
continue
|
|
127
|
+
# 元组解包同样要覆盖:`for idx, row in df.iterrows():` 里的闭包捕获 row,
|
|
128
|
+
# 只认单变量循环的话,pandas 场景下这条规则基本不会响——而那正是它最该响的地方。
|
|
129
|
+
if isinstance(node.target, ast.Name):
|
|
130
|
+
loop_vars = [node.target.id]
|
|
131
|
+
elif isinstance(node.target, (ast.Tuple, ast.List)):
|
|
132
|
+
loop_vars = [e.id for e in node.target.elts if isinstance(e, ast.Name)]
|
|
133
|
+
else:
|
|
134
|
+
continue
|
|
135
|
+
if not loop_vars:
|
|
136
|
+
continue
|
|
137
|
+
for var in loop_vars:
|
|
138
|
+
_check_late_binding(node, var, lens, ctx, out, add)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ── except 顺序:父类写在前面,后面的子类分支永远执行不到 ──
|
|
142
|
+
_HIER = {
|
|
143
|
+
'Exception': {'ValueError', 'TypeError', 'KeyError', 'IndexError', 'AttributeError',
|
|
144
|
+
'OSError', 'IOError', 'RuntimeError', 'ZeroDivisionError', 'StopIteration',
|
|
145
|
+
'ArithmeticError', 'LookupError', 'NameError', 'ImportError', 'FileNotFoundError'},
|
|
146
|
+
'BaseException': {'Exception', 'KeyboardInterrupt', 'SystemExit', 'GeneratorExit'},
|
|
147
|
+
'OSError': {'FileNotFoundError', 'PermissionError', 'FileExistsError', 'TimeoutError',
|
|
148
|
+
'ConnectionError', 'IsADirectoryError', 'NotADirectoryError'},
|
|
149
|
+
'LookupError': {'KeyError', 'IndexError'},
|
|
150
|
+
'ArithmeticError': {'ZeroDivisionError', 'OverflowError', 'FloatingPointError'},
|
|
151
|
+
'ValueError': {'UnicodeDecodeError', 'UnicodeEncodeError'},
|
|
152
|
+
}
|
|
153
|
+
for node in ast.walk(tree):
|
|
154
|
+
if not isinstance(node, ast.Try):
|
|
155
|
+
continue
|
|
156
|
+
seen = []
|
|
157
|
+
for handler in node.handlers:
|
|
158
|
+
names = []
|
|
159
|
+
if handler.type is None:
|
|
160
|
+
names = ['BaseException']
|
|
161
|
+
elif isinstance(handler.type, ast.Tuple):
|
|
162
|
+
names = [_name_of(e).split('.')[-1] for e in handler.type.elts]
|
|
163
|
+
else:
|
|
164
|
+
names = [_name_of(handler.type).split('.')[-1]]
|
|
165
|
+
for nm in names:
|
|
166
|
+
for prev in seen:
|
|
167
|
+
if nm in _HIER.get(prev, ()) or prev == 'BaseException':
|
|
168
|
+
add(out, ctx, 'logic.py-except-order', 'error', lens, handler.lineno,
|
|
169
|
+
f'`except {nm}` 排在 `except {prev}` 后面,而 {nm} 是 {prev} 的子类 —— '
|
|
170
|
+
'**这个分支永远不会执行**,异常会被前面那个捕走',
|
|
171
|
+
f'把 `except {nm}` 挪到 `except {prev}` 前面:'
|
|
172
|
+
'异常分支要从最具体排到最宽泛', _unit_of(lens, handler.lineno))
|
|
173
|
+
break
|
|
174
|
+
seen.extend(names)
|
|
175
|
+
|
|
176
|
+
# ── 遍历时增删同一个容器:会跳过元素,或直接抛 RuntimeError ──
|
|
177
|
+
for node in ast.walk(tree):
|
|
178
|
+
if not isinstance(node, (ast.For, ast.AsyncFor)) or not isinstance(node.iter, ast.Name):
|
|
179
|
+
continue
|
|
180
|
+
coll = node.iter.id
|
|
181
|
+
for sub in ast.walk(node):
|
|
182
|
+
if not isinstance(sub, ast.Call) or not isinstance(sub.func, ast.Attribute):
|
|
183
|
+
continue
|
|
184
|
+
if sub.func.attr not in ('remove', 'pop', 'append', 'insert', 'clear', 'update'):
|
|
185
|
+
continue
|
|
186
|
+
if _name_of(sub.func.value) != coll:
|
|
187
|
+
continue
|
|
188
|
+
add(out, ctx, 'logic.py-mutate-during-iteration', 'error', lens, sub.lineno,
|
|
189
|
+
f'一边遍历 `{coll}` 一边 `{sub.func.attr}()` 它 —— '
|
|
190
|
+
'列表会**跳过**紧随其后的元素(下标已经前移),字典直接抛 RuntimeError',
|
|
191
|
+
f'遍历副本 `for x in list({coll}):`,或用列表推导重建:'
|
|
192
|
+
f'`{coll} = [x for x in {coll} if 条件]`', _unit_of(lens, sub.lineno))
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
# ── is 比较字面量:靠的是对象缓存,小数字/短字符串"碰巧"为真 ──
|
|
196
|
+
for node in ast.walk(tree):
|
|
197
|
+
if not isinstance(node, ast.Compare):
|
|
198
|
+
continue
|
|
199
|
+
for op, cmp in zip(node.ops, node.comparators):
|
|
200
|
+
if not isinstance(op, (ast.Is, ast.IsNot)):
|
|
201
|
+
continue
|
|
202
|
+
if isinstance(cmp, ast.Constant) and cmp.value is not None and not isinstance(cmp.value, bool):
|
|
203
|
+
add(out, ctx, 'logic.py-is-literal', 'error', lens, node.lineno,
|
|
204
|
+
f'用 `is` 比较字面量 `{cmp.value!r}` —— `is` 比的是**同一个对象**,'
|
|
205
|
+
'小整数和短字符串因为有缓存会"碰巧"为真,换个值或换个来源就突然为假',
|
|
206
|
+
'比较值用 `==`;`is` 只留给 None / True / False 这三个单例',
|
|
207
|
+
_unit_of(lens, node.lineno))
|
|
208
|
+
# 反过来:== None / == True 应该用 is
|
|
209
|
+
for op, cmp in zip(node.ops, node.comparators):
|
|
210
|
+
if not isinstance(op, (ast.Eq, ast.NotEq)):
|
|
211
|
+
continue
|
|
212
|
+
if isinstance(cmp, ast.Constant) and (cmp.value is None or isinstance(cmp.value, bool)):
|
|
213
|
+
add(out, ctx, 'logic.py-eq-singleton', 'info', lens, node.lineno,
|
|
214
|
+
f'用 `==` 比较 `{cmp.value!r}` —— 它是单例,应该用 `is`',
|
|
215
|
+
f'写成 `is {cmp.value!r}`;`== True` 还会把 1 也算作真,'
|
|
216
|
+
'而 `is True` 不会', _unit_of(lens, node.lineno))
|
|
217
|
+
|
|
218
|
+
# ── except 里 raise 新异常不带 from:原始堆栈就此丢失 ──
|
|
219
|
+
for node in ast.walk(tree):
|
|
220
|
+
if not isinstance(node, ast.ExceptHandler):
|
|
221
|
+
continue
|
|
222
|
+
for sub in ast.walk(node):
|
|
223
|
+
if not isinstance(sub, ast.Raise) or sub.cause is not None:
|
|
224
|
+
continue
|
|
225
|
+
if sub.exc is None:
|
|
226
|
+
continue # 裸 raise 是重新抛出原异常,正确
|
|
227
|
+
add(out, ctx, 'logic.py-raise-without-from', 'warn', lens, sub.lineno,
|
|
228
|
+
'在 except 里抛出新异常却没有 `from` —— 原始异常的堆栈会被截断,'
|
|
229
|
+
'排查时只看得到"包装后"的那一层',
|
|
230
|
+
'写成 `raise 新异常(...) from e`;确实要隐藏内层细节就写 `from None`,'
|
|
231
|
+
'那也是一个明确的决定而不是遗漏', _unit_of(lens, sub.lineno))
|
|
232
|
+
break
|
|
233
|
+
|
|
234
|
+
# ── 遮蔽内置名:后面用到那个内置函数时会得到诡异的报错 ──
|
|
235
|
+
for node in ast.walk(tree):
|
|
236
|
+
target = None
|
|
237
|
+
if isinstance(node, ast.Assign) and len(node.targets) == 1:
|
|
238
|
+
target = node.targets[0]
|
|
239
|
+
elif isinstance(node, (ast.For, ast.AsyncFor)):
|
|
240
|
+
target = node.target
|
|
241
|
+
if not isinstance(target, ast.Name):
|
|
242
|
+
continue
|
|
243
|
+
if target.id in _BUILTINS and target.id not in _BUILTIN_OK:
|
|
244
|
+
add(out, ctx, 'logic.py-shadow-builtin', 'warn', lens, node.lineno,
|
|
245
|
+
f'变量名 `{target.id}` 遮蔽了同名内置函数 —— '
|
|
246
|
+
f'这一行之后再想调用 `{target.id}()` 就会报"不可调用",而错误信息完全不指向这里',
|
|
247
|
+
f'换个名字,比如 `{target.id}_` 或更具体的 `user_{target.id}`',
|
|
248
|
+
_unit_of(lens, node.lineno))
|
|
249
|
+
|
|
250
|
+
# ── 生产代码里用 assert 做校验:python -O 会把它整行删掉 ──
|
|
251
|
+
if not lens.is_test:
|
|
252
|
+
# `if __name__ == '__main__':` 里的 assert 是脚本自测,不是生产校验。
|
|
253
|
+
# 判定要用 AST 的节点范围——往上找一行的写法只在 assert 紧贴 if 时才对,
|
|
254
|
+
# 而自测块里通常有十几行。
|
|
255
|
+
main_ranges = []
|
|
256
|
+
for top in tree.body:
|
|
257
|
+
if not isinstance(top, ast.If):
|
|
258
|
+
continue
|
|
259
|
+
test = top.test
|
|
260
|
+
if isinstance(test, ast.Compare) and _name_of(test.left) == '__name__':
|
|
261
|
+
last = max((getattr(x, 'end_lineno', x.lineno) or x.lineno) for x in top.body)
|
|
262
|
+
main_ranges.append((top.lineno, last))
|
|
263
|
+
for node in ast.walk(tree):
|
|
264
|
+
if not isinstance(node, ast.Assert):
|
|
265
|
+
continue
|
|
266
|
+
fn = lens.unit_at(node.lineno)
|
|
267
|
+
# 自测函数的命名约定不止 `test_xxx` 一种:本仓库大量用 `selftest` / `self_test` / `_selftest`,
|
|
268
|
+
# 只认「以 test 开头」会把 117 条自测断言全报成生产校验。
|
|
269
|
+
# `(^|_)` 这个前缀不能省——否则 `latest_config` 这类名字也会被当成测试。
|
|
270
|
+
if fn and re.search(r'(^|_)(self_?)?tests?(_|$)', fn.name.lower()):
|
|
271
|
+
continue
|
|
272
|
+
if any(a <= node.lineno <= b for a, b in main_ranges):
|
|
273
|
+
continue
|
|
274
|
+
add(out, ctx, 'logic.py-assert-in-production', 'warn', lens, node.lineno,
|
|
275
|
+
'用 `assert` 做运行时校验 —— 以 `python -O` 启动时**整行会被删掉**,校验静默消失',
|
|
276
|
+
'参数校验改成 `if not 条件: raise ValueError(...)`;'
|
|
277
|
+
'assert 只用来表达"这里不可能发生"的内部不变量', _unit_of(lens, node.lineno))
|
|
278
|
+
|
|
279
|
+
# ── type(x) == T:拿不到继承关系 ──
|
|
280
|
+
for node in ast.walk(tree):
|
|
281
|
+
if not isinstance(node, ast.Compare) or not node.ops:
|
|
282
|
+
continue
|
|
283
|
+
if not isinstance(node.ops[0], (ast.Eq, ast.NotEq)):
|
|
284
|
+
continue
|
|
285
|
+
if isinstance(node.left, ast.Call) and _name_of(node.left.func) == 'type':
|
|
286
|
+
add(out, ctx, 'logic.py-type-compare', 'warn', lens, node.lineno,
|
|
287
|
+
'用 `type(x) == T` 判类型 —— 子类实例会被判为不匹配',
|
|
288
|
+
'改用 `isinstance(x, T)`;确实要排除子类(少见)就把这个意图写进注释',
|
|
289
|
+
_unit_of(lens, node.lineno))
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# ── 数据分析(pandas)、安全、异步 ───────────────────────────────────────────
|
|
293
|
+
# pandas 这一组的价值特别高,因为它的事故**不改变外观、只改变数字**:
|
|
294
|
+
# 读进来的时候前导零没了、大数字变成科学计数法、链式赋值静默不生效——
|
|
295
|
+
# 表格看上去一切正常,只是数对不上,而人盯着屏幕是看不出来的。
|
|
296
|
+
_PANDAS_READERS = ('read_csv', 'read_excel', 'read_table', 'read_json')
|
|
297
|
+
_BLOCKING_IN_ASYNC = {
|
|
298
|
+
'time.sleep': 'asyncio.sleep',
|
|
299
|
+
'requests.get': 'httpx/aiohttp 的异步客户端',
|
|
300
|
+
'requests.post': 'httpx/aiohttp 的异步客户端',
|
|
301
|
+
'requests.put': 'httpx/aiohttp 的异步客户端',
|
|
302
|
+
'requests.delete': 'httpx/aiohttp 的异步客户端',
|
|
303
|
+
'subprocess.run': 'asyncio.create_subprocess_exec',
|
|
304
|
+
'subprocess.call': 'asyncio.create_subprocess_exec',
|
|
305
|
+
'subprocess.check_output': 'asyncio.create_subprocess_exec',
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _kw(call, name):
|
|
310
|
+
"""取调用的某个关键字实参节点;没有返回 None。"""
|
|
311
|
+
for k in call.keywords:
|
|
312
|
+
if k.arg == name:
|
|
313
|
+
return k.value
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def check_python_data_and_security(lens, ctx, out, add):
|
|
318
|
+
"""pandas 用法、反序列化与子进程安全、异步里的阻塞调用。"""
|
|
319
|
+
tree = getattr(lens, '_ast', None)
|
|
320
|
+
if tree is None:
|
|
321
|
+
return
|
|
322
|
+
text = lens.text
|
|
323
|
+
uses_pandas = 'pandas' in text or re.search(r'\bpd\.', text)
|
|
324
|
+
|
|
325
|
+
for node in ast.walk(tree):
|
|
326
|
+
# ── 链式赋值:`df[a][b] = x` 改的是临时副本,原表没变(SettingWithCopyWarning) ──
|
|
327
|
+
if isinstance(node, ast.Assign):
|
|
328
|
+
for t in node.targets:
|
|
329
|
+
if (isinstance(t, ast.Subscript) and isinstance(t.value, ast.Subscript)
|
|
330
|
+
and uses_pandas):
|
|
331
|
+
add(out, ctx, 'framework.py-pandas-chained-assign', 'error', lens, node.lineno,
|
|
332
|
+
'链式下标赋值 `df[...][...] = ...` —— pandas 很可能改的是一份**临时副本**,'
|
|
333
|
+
'原表根本没变,而且只会给个 SettingWithCopyWarning,不报错',
|
|
334
|
+
'用 `.loc` 一次定位:`df.loc[行条件, "列名"] = 值`。'
|
|
335
|
+
'这类问题最坏的形态是"程序跑完了、结果是错的、没有任何提示"',
|
|
336
|
+
_unit_of(lens, node.lineno))
|
|
337
|
+
|
|
338
|
+
if not isinstance(node, ast.Call):
|
|
339
|
+
continue
|
|
340
|
+
fname = _name_of(node.func)
|
|
341
|
+
short = fname.split('.')[-1]
|
|
342
|
+
|
|
343
|
+
# ── 读表不指定 dtype:前导零丢失、长编号变科学计数法 ──
|
|
344
|
+
if short in _PANDAS_READERS and uses_pandas:
|
|
345
|
+
# `**kw` 展开时 dtype 可能就在里面,看不见也不能断言它没传
|
|
346
|
+
splatted = any(k.arg is None for k in node.keywords)
|
|
347
|
+
if not splatted and _kw(node, 'dtype') is None and _kw(node, 'converters') is None:
|
|
348
|
+
add(out, ctx, 'framework.py-read-no-dtype', 'warn', lens, node.lineno,
|
|
349
|
+
f'`{short}()` 没有指定 dtype —— pandas 会自动推断类型,'
|
|
350
|
+
'「007」这类前导零编号会变成数字 7,18 位的单号会变成科学计数法**且精度已经丢了**',
|
|
351
|
+
'编号/证件号/手机号一律 `dtype=str`(或 `dtype={"单号": str}`)。'
|
|
352
|
+
'这一步省下来,后面对账对不上时已经救不回来,只能重新导一次数据',
|
|
353
|
+
_unit_of(lens, node.lineno))
|
|
354
|
+
|
|
355
|
+
# ── DataFrame.append:pandas 2.0 已经删掉了这个方法 ──
|
|
356
|
+
# 判 DataFrame.append 不能靠变量名猜:`tables.append(rows)` 是再普通不过的 list.append,
|
|
357
|
+
# 却会被 `table\w*` 这种模式捞走。可靠信号有两个——
|
|
358
|
+
# ① 传了 `ignore_index=`(list.append 根本不接受关键字参数);
|
|
359
|
+
# ② 变量名严格就是 df / xxx_df / df_xxx。
|
|
360
|
+
base = _name_of(node.func.value) if isinstance(node.func, ast.Attribute) else ''
|
|
361
|
+
looks_df = bool(re.fullmatch(r'df|df_\w+|\w+_df|dataframe', base, re.I))
|
|
362
|
+
if short == 'append' and uses_pandas and (_kw(node, 'ignore_index') is not None or looks_df):
|
|
363
|
+
add(out, ctx, 'framework.py-pandas-append', 'error', lens, node.lineno,
|
|
364
|
+
'`DataFrame.append()` 在 pandas 2.0 已被移除,调用会直接 AttributeError',
|
|
365
|
+
'改用 `pd.concat([df1, df2], ignore_index=True)`;'
|
|
366
|
+
'循环里逐行 append 更要改成先收集成列表、最后一次性 concat(否则是 O(n²))',
|
|
367
|
+
_unit_of(lens, node.lineno))
|
|
368
|
+
|
|
369
|
+
# ── iterrows:每行都构造一个 Series,大表上慢几十倍 ──
|
|
370
|
+
if short == 'iterrows' and uses_pandas:
|
|
371
|
+
add(out, ctx, 'framework.py-pandas-iterrows', 'info', lens, node.lineno,
|
|
372
|
+
'`iterrows()` 逐行遍历:每一行都会构造一个 Series 对象,几万行就能感觉到卡',
|
|
373
|
+
'优先向量化(`df["c"] = df["a"] * df["b"]`);'
|
|
374
|
+
'确实要逐行就用 `itertuples()`(快一个量级),或 `df.apply(..., axis=1)`',
|
|
375
|
+
_unit_of(lens, node.lineno))
|
|
376
|
+
|
|
377
|
+
# ── inplace=True:既不省内存,还让链式写法断掉,pandas 官方已在劝退 ──
|
|
378
|
+
if uses_pandas:
|
|
379
|
+
inplace = _kw(node, 'inplace')
|
|
380
|
+
if isinstance(inplace, ast.Constant) and inplace.value is True:
|
|
381
|
+
add(out, ctx, 'framework.py-pandas-inplace', 'info', lens, node.lineno,
|
|
382
|
+
f'`{short}(..., inplace=True)` —— 它并不省内存(内部照样复制),'
|
|
383
|
+
'却让返回值变成 None,链式写法一接就断,也更容易误改到副本',
|
|
384
|
+
'改成赋值回去:`df = df.xxx(...)`', _unit_of(lens, node.lineno))
|
|
385
|
+
|
|
386
|
+
# ── subprocess shell=True:拼接的命令会被 shell 解释 ──
|
|
387
|
+
if fname.startswith('subprocess.') or short in ('run', 'call', 'Popen', 'check_output'):
|
|
388
|
+
shell = _kw(node, 'shell')
|
|
389
|
+
if isinstance(shell, ast.Constant) and shell.value is True:
|
|
390
|
+
first = node.args[0] if node.args else None
|
|
391
|
+
dynamic = first is not None and not isinstance(first, ast.Constant)
|
|
392
|
+
add(out, ctx, 'security.py-subprocess-shell',
|
|
393
|
+
'error' if dynamic else 'warn', lens, node.lineno,
|
|
394
|
+
'subprocess 用了 `shell=True`'
|
|
395
|
+
+ (',并且命令是拼出来的 —— 参数里的 `;` `|` `$()` 都会被 shell 执行'
|
|
396
|
+
if dynamic else '(本次命令是常量,风险有限)'),
|
|
397
|
+
'传列表而不是字符串:`subprocess.run(["git", "checkout", name])`,'
|
|
398
|
+
'不经 shell 解析就没有注入面', _unit_of(lens, node.lineno))
|
|
399
|
+
|
|
400
|
+
# ── yaml.load 不带 Loader:能构造出任意 Python 对象 ──
|
|
401
|
+
if fname in ('yaml.load', 'load') and 'yaml' in text:
|
|
402
|
+
if _kw(node, 'Loader') is None and len(node.args) < 2:
|
|
403
|
+
add(out, ctx, 'security.py-yaml-load', 'error', lens, node.lineno,
|
|
404
|
+
'`yaml.load()` 没有指定 Loader —— 默认加载器能构造任意 Python 对象,'
|
|
405
|
+
'解析不可信的 YAML 等同于执行它',
|
|
406
|
+
'用 `yaml.safe_load()`;确实需要自定义标签再显式传 Loader',
|
|
407
|
+
_unit_of(lens, node.lineno))
|
|
408
|
+
|
|
409
|
+
# ── pickle 反序列化:等同于执行对方给的代码 ──
|
|
410
|
+
if fname in ('pickle.loads', 'pickle.load', 'cPickle.loads'):
|
|
411
|
+
add(out, ctx, 'security.py-pickle', 'warn', lens, node.lineno,
|
|
412
|
+
f'`{fname}()` 反序列化 —— pickle 会在加载时执行对象里的构造逻辑,'
|
|
413
|
+
'来源不可信时等同于任意代码执行',
|
|
414
|
+
'换成 JSON 等只描述数据的格式;确实要用 pickle 就限定只读自己写出的文件,'
|
|
415
|
+
'并在注释里写明这个前提', _unit_of(lens, node.lineno))
|
|
416
|
+
|
|
417
|
+
# ── async 函数里的阻塞调用:整个事件循环被卡住,所有并发一起停 ──
|
|
418
|
+
for node in ast.walk(tree):
|
|
419
|
+
if not isinstance(node, ast.AsyncFunctionDef):
|
|
420
|
+
continue
|
|
421
|
+
for sub in ast.walk(node):
|
|
422
|
+
if not isinstance(sub, ast.Call):
|
|
423
|
+
continue
|
|
424
|
+
fname = _name_of(sub.func)
|
|
425
|
+
better = _BLOCKING_IN_ASYNC.get(fname)
|
|
426
|
+
if better is None and fname == 'open':
|
|
427
|
+
better = 'aiofiles,或把文件 IO 丢进 run_in_executor'
|
|
428
|
+
if better is None:
|
|
429
|
+
continue
|
|
430
|
+
add(out, ctx, 'logic.py-blocking-in-async', 'error', lens, sub.lineno,
|
|
431
|
+
f'async 函数 `{node.name}` 里调用了阻塞的 `{fname}()` —— '
|
|
432
|
+
'事件循环被整个卡住,**同一个循环上的所有协程一起停**,异步就失去了意义',
|
|
433
|
+
f'改用 {better};实在只有同步库可用就 `await loop.run_in_executor(None, ...)`',
|
|
434
|
+
node.name)
|
|
435
|
+
|
|
436
|
+
# ── open 不用 with:异常路径上句柄不会关 ──
|
|
437
|
+
for node in ast.walk(tree):
|
|
438
|
+
if not isinstance(node, ast.Assign) or not isinstance(node.value, ast.Call):
|
|
439
|
+
continue
|
|
440
|
+
if _name_of(node.value.func) != 'open':
|
|
441
|
+
continue
|
|
442
|
+
add(out, ctx, 'logic.py-open-without-with', 'warn', lens, node.lineno,
|
|
443
|
+
'`open()` 的结果直接赋给变量,没有走 `with` —— 中途抛异常时文件不会被关闭',
|
|
444
|
+
'写成 `with open(...) as f:`;句柄泄漏平时看不出来,'
|
|
445
|
+
'到达上限时整个进程一起打不开文件', _unit_of(lens, node.lineno))
|
|
446
|
+
|
|
447
|
+
# ── logging 用 f-string:无论日志级别开没开,字符串都先拼好了 ──
|
|
448
|
+
for node in ast.walk(tree):
|
|
449
|
+
if not isinstance(node, ast.Call):
|
|
450
|
+
continue
|
|
451
|
+
fname = _name_of(node.func)
|
|
452
|
+
if not re.search(r'(^|\.)(log|logger|logging|LOG|LOGGER)\.(debug|info|warning|error|exception|critical)$',
|
|
453
|
+
fname):
|
|
454
|
+
continue
|
|
455
|
+
if node.args and isinstance(node.args[0], ast.JoinedStr):
|
|
456
|
+
add(out, ctx, 'logic.py-logging-fstring', 'info', lens, node.lineno,
|
|
457
|
+
'logging 的消息用了 f-string —— 参数会**无条件先求值拼接**,'
|
|
458
|
+
'即使这条日志因为级别不够根本不会输出',
|
|
459
|
+
'改成惰性占位:`log.info("处理了 %s 条", n)`。'
|
|
460
|
+
'热路径上的 debug 日志尤其要注意这一条', _unit_of(lens, node.lineno))
|
|
461
|
+
|
|
462
|
+
# ── finally 里 return:会把正在抛的异常吞掉 ──
|
|
463
|
+
for node in ast.walk(tree):
|
|
464
|
+
if not isinstance(node, ast.Try) or not node.finalbody:
|
|
465
|
+
continue
|
|
466
|
+
for sub in node.finalbody:
|
|
467
|
+
for x in ast.walk(sub):
|
|
468
|
+
if isinstance(x, (ast.Return, ast.Break, ast.Continue)):
|
|
469
|
+
add(out, ctx, 'logic.py-return-in-finally', 'error', lens, x.lineno,
|
|
470
|
+
'finally 块里 return/break/continue —— '
|
|
471
|
+
'会把 try 块中正在抛出的异常直接丢弃,故障现场就此消失',
|
|
472
|
+
'finally 只做清理;要改返回值就在 try 里改', _unit_of(lens, x.lineno))
|
|
473
|
+
break
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
# ── 与 dev-python「常见坑 TOP」对齐的几条 ──────────────────────────────────
|
|
477
|
+
# 同 Java 那一轮的做法:把技能文档里「评审自查单」上的条目变成机器每次都查。
|
|
478
|
+
# 这几条的共同点是**在开发机上永远正常**,换个操作系统、换个时区、上了生产才出事。
|
|
479
|
+
def check_python_pitfalls(lens, ctx, out, add):
|
|
480
|
+
"""dev-python 技能里「评审必查」的那几条。"""
|
|
481
|
+
tree = getattr(lens, '_ast', None)
|
|
482
|
+
if tree is None:
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
for node in ast.walk(tree):
|
|
486
|
+
if not isinstance(node, ast.Call):
|
|
487
|
+
continue
|
|
488
|
+
fname = _name_of(node.func)
|
|
489
|
+
|
|
490
|
+
# ── open() 不写 encoding:Windows 默认 GBK,读 UTF-8 文件当场乱码或抛异常 ──
|
|
491
|
+
if fname == 'open':
|
|
492
|
+
mode = node.args[1] if len(node.args) > 1 else _kw(node, 'mode')
|
|
493
|
+
is_binary = isinstance(mode, ast.Constant) and 'b' in str(mode.value)
|
|
494
|
+
if not is_binary and _kw(node, 'encoding') is None:
|
|
495
|
+
add(out, ctx, 'logic.py-open-no-encoding', 'warn', lens, node.lineno,
|
|
496
|
+
'`open()` 没写 encoding —— 它取的是**操作系统默认编码**,'
|
|
497
|
+
'Windows 上是 GBK,读 UTF-8 文件当场乱码或抛 UnicodeDecodeError',
|
|
498
|
+
'一律显式 `encoding="utf-8"`。这个 bug 在开发机(macOS/Linux)上永远复现不了,'
|
|
499
|
+
'只在用户那边炸', _unit_of(lens, node.lineno))
|
|
500
|
+
|
|
501
|
+
# ── datetime.now() 不带时区:naive 时间一旦跨时区或存库就说不清是几点 ──
|
|
502
|
+
if fname in ('datetime.now', 'datetime.datetime.now', 'datetime.utcnow',
|
|
503
|
+
'datetime.datetime.utcnow'):
|
|
504
|
+
if fname.endswith('utcnow'):
|
|
505
|
+
add(out, ctx, 'logic.py-naive-datetime', 'warn', lens, node.lineno,
|
|
506
|
+
'`utcnow()` 返回的是**不带时区**的 datetime(已废弃)—— '
|
|
507
|
+
'它看起来是 UTC,但对象上没有这个信息,一转换就错',
|
|
508
|
+
'改用 `datetime.now(timezone.utc)`', _unit_of(lens, node.lineno))
|
|
509
|
+
elif not node.args and not node.keywords:
|
|
510
|
+
add(out, ctx, 'logic.py-naive-datetime', 'warn', lens, node.lineno,
|
|
511
|
+
'`datetime.now()` 不带时区 —— 得到的是 naive 时间,'
|
|
512
|
+
'存库、跨服务传递、跨时区比较时没人说得清它到底是哪个时区的几点',
|
|
513
|
+
'写成 `datetime.now(timezone.utc)`,展示时再转本地时区',
|
|
514
|
+
_unit_of(lens, node.lineno))
|
|
515
|
+
|
|
516
|
+
# ── Decimal(浮点):和 Java 的 new BigDecimal(double) 是同一个坑 ──
|
|
517
|
+
if fname in ('Decimal', 'decimal.Decimal') and node.args:
|
|
518
|
+
arg = node.args[0]
|
|
519
|
+
if isinstance(arg, ast.Constant) and isinstance(arg.value, float):
|
|
520
|
+
add(out, ctx, 'logic.py-decimal-float', 'error', lens, node.lineno,
|
|
521
|
+
f'`Decimal({arg.value})` 用浮点构造 —— 误差在进入 Decimal 之前就已经产生了,'
|
|
522
|
+
f'结果是 {arg.value} 的二进制近似值而不是你写的那个数',
|
|
523
|
+
'用字符串构造:`Decimal("0.1")`。'
|
|
524
|
+
'钱一旦这样算错,对账时差的那几分钱要查很久', _unit_of(lens, node.lineno))
|
|
525
|
+
|
|
526
|
+
# ── os.path.join 后段是绝对路径时会把前面全部丢掉 ──
|
|
527
|
+
if fname == 'os.path.join' and len(node.args) >= 2:
|
|
528
|
+
for extra in node.args[1:]:
|
|
529
|
+
if isinstance(extra, ast.Constant) and isinstance(extra.value, str) \
|
|
530
|
+
and extra.value.startswith(('/', '\\')):
|
|
531
|
+
add(out, ctx, 'logic.py-join-absolute', 'error', lens, node.lineno,
|
|
532
|
+
f'`os.path.join(..., "{extra.value}")` 的后段以斜杠开头 —— '
|
|
533
|
+
'join 遇到绝对路径会**丢弃前面所有部分**,结果完全不是拼接',
|
|
534
|
+
'把后段开头的斜杠去掉,或改用 `pathlib.Path(a) / b`(同样有这个语义,'
|
|
535
|
+
'但写法上更容易看出来)', _unit_of(lens, node.lineno))
|
|
536
|
+
break
|