@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,391 @@
|
|
|
1
|
+
# 数据体检:分析动手之前,先把这份数据「哪里不能信」查清楚。
|
|
2
|
+
#
|
|
3
|
+
# 为什么必须有:数据分析出错误答案,绝大多数不是算法错,是**读进来的东西不是你以为的东西**——
|
|
4
|
+
# 金额是文本(SUM 得 0)、订单号被读成数字(前导零没了)、日期是 Excel 序列号、
|
|
5
|
+
# 明细里混着「合计」行(一汇总就翻倍)、两个「北京」差一个空格没合并、主键其实不唯一。
|
|
6
|
+
# 这些在代码里都不报错,只是把数字算错,而算错的数字看起来和算对的一模一样。
|
|
7
|
+
#
|
|
8
|
+
# 与 xlsx_toolkit 的分工:那边管**把脏表读进来**(表头识别、金额救回、日期归一),
|
|
9
|
+
# 这边管**读进来之后告诉你它还有哪里不对**。先 smart_read,再 profile。
|
|
10
|
+
#
|
|
11
|
+
# 用法:
|
|
12
|
+
# import data_profile as P
|
|
13
|
+
# P.report(P.profile(df, '应收明细')) # 单表体检
|
|
14
|
+
# P.report_file('收款单.xlsx') # 整本工作簿逐 Sheet 体检
|
|
15
|
+
# 或 CLI:python data_profile.py 数据.xlsx [--sheet 名] [--json]
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
import warnings
|
|
21
|
+
from collections import Counter
|
|
22
|
+
|
|
23
|
+
warnings.simplefilter('ignore')
|
|
24
|
+
import numpy as np # noqa: E402
|
|
25
|
+
import pandas as pd # noqa: E402
|
|
26
|
+
|
|
27
|
+
ID_HINTS = ('编码', '编号', '代码', '号', 'id', 'code', 'no', '卡号', '工号', '身份证', '邮编')
|
|
28
|
+
AMOUNT_HINTS = ('金额', '贷方', '借方', '余额', '税额', '价税', '收入', '成本', '费用',
|
|
29
|
+
'单价', '工资', '总额', '应收', '应付', '预收', '预付', '销售额', '利润')
|
|
30
|
+
DATE_HINTS = ('日期', '时间', '月份', '期间', '账期', '年月')
|
|
31
|
+
SUBTOTAL_MARKS = ('合计', '小计', '总计', '合 计', '小 计', '总 计', '本页合计', '累计', 'total', 'subtotal')
|
|
32
|
+
INVISIBLE = ' \t\r\n'
|
|
33
|
+
NUM_RE = re.compile(r'^\s*[((]?\s*[-+]?[¥¥$]?\s*[\d,,]+(\.\d+)?\s*[%元]?\s*[))]?\s*$')
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _issue(level, kind, where, msg, detail='', suggest=''):
|
|
37
|
+
return {'level': level, 'kind': kind, 'where': where, 'msg': msg,
|
|
38
|
+
'detail': detail, 'suggest': suggest}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _sample(s, n=3):
|
|
42
|
+
v = s.dropna()
|
|
43
|
+
if v.empty:
|
|
44
|
+
return []
|
|
45
|
+
return [str(x)[:24] for x in v.head(n).tolist()]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _looks_number(x):
|
|
49
|
+
if isinstance(x, (int, float, np.number)) and not isinstance(x, bool):
|
|
50
|
+
return True
|
|
51
|
+
return bool(NUM_RE.match(str(x)))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _looks_date(x):
|
|
55
|
+
s = str(x).strip()
|
|
56
|
+
if re.fullmatch(r'\d{4}[-/.年]\d{1,2}([-/.月]\d{1,2}日?)?', s):
|
|
57
|
+
return True
|
|
58
|
+
return bool(re.fullmatch(r'\d{4}[-/]\d{1,2}[-/]\d{1,2}[ T].*', s))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _norm_cat(x):
|
|
62
|
+
"""分类值归一:去空白与不可见字符、全半角、大小写——用于找「同物异名」。"""
|
|
63
|
+
t = str(x)
|
|
64
|
+
for ch in INVISIBLE:
|
|
65
|
+
t = t.replace(ch, '')
|
|
66
|
+
return t.strip().lower().replace('(', '(').replace(')', ')')
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ── 单列体检 ───────────────────────────────────────────────────────────────
|
|
70
|
+
def profile_column(s, name, n_rows):
|
|
71
|
+
col = {'name': name, 'dtype': str(s.dtype), 'nulls': int(s.isna().sum()),
|
|
72
|
+
'null_rate': round(float(s.isna().mean()), 4) if n_rows else 0.0,
|
|
73
|
+
'unique': int(s.nunique(dropna=True)), 'sample': _sample(s)}
|
|
74
|
+
issues = []
|
|
75
|
+
nn = s.dropna()
|
|
76
|
+
col['inferred'] = 'empty'
|
|
77
|
+
if nn.empty:
|
|
78
|
+
issues.append(_issue('warn', 'empty-col', name, '整列为空',
|
|
79
|
+
'', '确认是否漏读了列,或直接删掉这一列'))
|
|
80
|
+
return col, issues
|
|
81
|
+
|
|
82
|
+
is_obj = s.dtype == object or pd.api.types.is_string_dtype(s)
|
|
83
|
+
if pd.api.types.is_numeric_dtype(s) and not pd.api.types.is_bool_dtype(s):
|
|
84
|
+
col['inferred'] = 'number'
|
|
85
|
+
elif pd.api.types.is_datetime64_any_dtype(s):
|
|
86
|
+
col['inferred'] = 'date'
|
|
87
|
+
elif pd.api.types.is_bool_dtype(s):
|
|
88
|
+
col['inferred'] = 'bool'
|
|
89
|
+
else:
|
|
90
|
+
col['inferred'] = 'text'
|
|
91
|
+
|
|
92
|
+
if is_obj:
|
|
93
|
+
# **按值的真实类型判,不按列 dtype 判**:pandas 用 dtype=object 读进来的列里
|
|
94
|
+
# 装的往往是货真价实的 int/float(smart_read 就是这么读的)。
|
|
95
|
+
# 早先按 dtype 判,这类列一律被判成「数值被存成文本」——在最常用的读表路径上
|
|
96
|
+
# 大面积误报,而误报比漏报更快让人关掉检查。
|
|
97
|
+
head = nn.head(500)
|
|
98
|
+
n = len(head)
|
|
99
|
+
strs = [v for v in head if isinstance(v, str)]
|
|
100
|
+
reals = [v for v in head if isinstance(v, (int, float, np.number)) and not isinstance(v, bool)]
|
|
101
|
+
num_like = sum(1 for v in strs if NUM_RE.match(v)) / max(n, 1)
|
|
102
|
+
date_like = sum(1 for v in strs if _looks_date(v)) / max(n, 1)
|
|
103
|
+
if reals and len(reals) >= n * 0.9:
|
|
104
|
+
col['inferred'] = 'number' # object 列,但装的是真数字
|
|
105
|
+
# ① 数字存成了文本——SUM 会得 0,是「对账对不平」的头号原因
|
|
106
|
+
if num_like >= 0.9 and not any(k in str(name) for k in ID_HINTS):
|
|
107
|
+
col['inferred'] = 'number-as-text'
|
|
108
|
+
issues.append(_issue('error', 'num-as-text', name,
|
|
109
|
+
f'数值被存成文本({num_like*100:.0f}% 的值形如数字)',
|
|
110
|
+
f'示例 {col["sample"]}',
|
|
111
|
+
'用 to_amount() 转成 float 再算;直接 sum 会得 0 或字符串拼接'))
|
|
112
|
+
elif date_like >= 0.9:
|
|
113
|
+
col['inferred'] = 'date-as-text'
|
|
114
|
+
issues.append(_issue('warn', 'date-as-text', name,
|
|
115
|
+
f'日期被存成文本({date_like*100:.0f}% 形如日期)',
|
|
116
|
+
f'示例 {col["sample"]}', '用 to_period()/to_datetime() 转换后再排序或分组'))
|
|
117
|
+
# ② 一列里数字与文本混——排序、分组、比较全都会出鬼
|
|
118
|
+
elif reals and strs and 0.15 < len(strs) / max(n, 1) < 0.85:
|
|
119
|
+
issues.append(_issue('warn', 'mixed-type', name,
|
|
120
|
+
f'同一列里真数字与文本混杂({len(reals)} 数字 / {len(strs)} 文本)',
|
|
121
|
+
f'示例 {col["sample"]}', '先统一类型:要么全转数字,要么全按文本处理'))
|
|
122
|
+
# ③ 首尾空白与不可见字符——「北京 」和「北京」分组会分成两组
|
|
123
|
+
dirty = int(sum(v != v.strip() or any(ch in v for ch in INVISIBLE)
|
|
124
|
+
for v in nn.head(2000) if isinstance(v, str)))
|
|
125
|
+
if dirty:
|
|
126
|
+
issues.append(_issue('warn', 'whitespace', name,
|
|
127
|
+
f'{dirty} 个值带首尾空白或不可见字符(含全角空格/换行)',
|
|
128
|
+
'', '.str.strip() 并清掉 \\u00a0/\\u3000,否则分组会分成两组'))
|
|
129
|
+
# ④ 同物异名:归一化后能合并的组
|
|
130
|
+
if 1 < col['unique'] <= 2000 and col['inferred'] not in ('number', 'number-as-text'):
|
|
131
|
+
groups = Counter(_norm_cat(v) for v in nn.head(2000))
|
|
132
|
+
merged = [k for k, c in groups.items() if c > 0]
|
|
133
|
+
if len(merged) < col['unique']:
|
|
134
|
+
issues.append(_issue('warn', 'alias', name,
|
|
135
|
+
f'归一化后取值从 {col["unique"]} 个降到 {len(merged)} 个,存在同物异名',
|
|
136
|
+
'', '先 normalize_key() 再分组/关联,否则会算成不同的实体'))
|
|
137
|
+
# ⑤ ID 列被读成数字 → 前导零丢了、超 15 位精度丢了
|
|
138
|
+
if col['inferred'] == 'number' and any(k in str(name).lower() for k in ID_HINTS):
|
|
139
|
+
mx = float(pd.to_numeric(nn, errors='coerce').abs().max() or 0)
|
|
140
|
+
issues.append(_issue('warn' if mx < 1e15 else 'error', 'id-as-number', name,
|
|
141
|
+
'编号类列被读成了数字',
|
|
142
|
+
f'最大值 {mx:.0f}' + ('(已超 15 位,末位精度可能已丢)' if mx >= 1e15 else ''),
|
|
143
|
+
'read_excel(dtype={"%s": str}),否则前导零会消失、长号会变科学计数法' % name))
|
|
144
|
+
|
|
145
|
+
if col['inferred'] == 'number':
|
|
146
|
+
v = pd.to_numeric(nn, errors='coerce').dropna()
|
|
147
|
+
if len(v):
|
|
148
|
+
q1, q3 = float(v.quantile(.25)), float(v.quantile(.75))
|
|
149
|
+
iqr = q3 - q1
|
|
150
|
+
out = int(((v < q1 - 3 * iqr) | (v > q3 + 3 * iqr)).sum()) if iqr > 0 else 0
|
|
151
|
+
col['stats'] = {'min': float(v.min()), 'max': float(v.max()),
|
|
152
|
+
'mean': round(float(v.mean()), 4), 'median': float(v.median()),
|
|
153
|
+
'zeros': int((v == 0).sum()), 'negatives': int((v < 0).sum()),
|
|
154
|
+
'outliers': out}
|
|
155
|
+
if out and out / len(v) < 0.05:
|
|
156
|
+
issues.append(_issue('info', 'outlier', name,
|
|
157
|
+
f'{out} 个极端值(超出 3 倍四分位距)',
|
|
158
|
+
f'范围 {v.min():.2f} ~ {v.max():.2f}', '确认是真实极值还是录入错误'))
|
|
159
|
+
if col['inferred'] == 'date':
|
|
160
|
+
v = nn
|
|
161
|
+
try:
|
|
162
|
+
col['stats'] = {'min': str(v.min())[:10], 'max': str(v.max())[:10]}
|
|
163
|
+
future = int((v > pd.Timestamp.now()).sum())
|
|
164
|
+
if future:
|
|
165
|
+
issues.append(_issue('warn', 'future-date', name,
|
|
166
|
+
f'{future} 个未来日期', f'最大 {str(v.max())[:10]}',
|
|
167
|
+
'确认是计划日期还是录入错误'))
|
|
168
|
+
months = pd.Series(v).dt.to_period('M').nunique()
|
|
169
|
+
span = (v.max() - v.min()).days if len(v) > 1 else 0
|
|
170
|
+
if span > 62 and months and months < span / 31 * 0.7:
|
|
171
|
+
issues.append(_issue('info', 'gap-month', name,
|
|
172
|
+
f'时间跨度 {span} 天但只覆盖 {months} 个月,中间有断月',
|
|
173
|
+
'', '按月汇总时缺月会被跳过,画趋势图要先 reindex 补齐'))
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
return col, issues
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ── 整表体检 ───────────────────────────────────────────────────────────────
|
|
180
|
+
def profile(df, name='', key_hint=None):
|
|
181
|
+
"""单表体检。返回 {name, shape, columns[], issues[], keys[]}。"""
|
|
182
|
+
n_rows, n_cols = df.shape
|
|
183
|
+
res = {'name': name, 'rows': int(n_rows), 'cols': int(n_cols),
|
|
184
|
+
'memory_mb': round(float(df.memory_usage(deep=True).sum()) / 1048576, 2),
|
|
185
|
+
'columns': [], 'issues': [], 'keys': []}
|
|
186
|
+
if n_rows == 0:
|
|
187
|
+
res['issues'].append(_issue('error', 'empty', name or '表', '表里没有数据行',
|
|
188
|
+
'', '确认表头行是否识别对了(smart_read 可手动传 header=N)'))
|
|
189
|
+
return res
|
|
190
|
+
# 列名问题
|
|
191
|
+
names = [str(c) for c in df.columns]
|
|
192
|
+
unnamed = [c for c in names if c.lower().startswith('unnamed') or re.fullmatch(r'列\d+', c)]
|
|
193
|
+
if unnamed:
|
|
194
|
+
res['issues'].append(_issue('warn', 'unnamed-col', '列名',
|
|
195
|
+
f'{len(unnamed)} 个列没有名字:{unnamed[:5]}',
|
|
196
|
+
'', '多半是表头行认错了——用 detect_header 复核,或手动指定 header=N'))
|
|
197
|
+
dup = [c for c, k in Counter(names).items() if k > 1]
|
|
198
|
+
if dup:
|
|
199
|
+
res['issues'].append(_issue('error', 'dup-col', '列名', f'重名列:{dup[:5]}',
|
|
200
|
+
'', '重名列取值时会拿到 DataFrame 而不是 Series,后续全错'))
|
|
201
|
+
messy = [c for c in names if c != c.strip() or any(ch in c for ch in INVISIBLE)]
|
|
202
|
+
if messy:
|
|
203
|
+
res['issues'].append(_issue('warn', 'messy-col', '列名',
|
|
204
|
+
f'{len(messy)} 个列名带空白/换行:{messy[:3]}',
|
|
205
|
+
'', 'df.columns = df.columns.str.strip(),否则 df["销售额"] 会 KeyError'))
|
|
206
|
+
|
|
207
|
+
# 重复行
|
|
208
|
+
try:
|
|
209
|
+
dups = int(df.duplicated().sum())
|
|
210
|
+
if dups:
|
|
211
|
+
res['issues'].append(_issue('warn', 'dup-row', '全表',
|
|
212
|
+
f'{dups} 行完全重复(占 {dups/n_rows*100:.1f}%)',
|
|
213
|
+
'', '确认是真实重复业务还是导出重复;汇总前要不要去重'))
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
|
216
|
+
|
|
217
|
+
# 「合计」行混在明细里——一汇总就双计,是最隐蔽的错误来源
|
|
218
|
+
sub = _subtotal_rows(df)
|
|
219
|
+
if sub:
|
|
220
|
+
res['issues'].append(_issue('error', 'subtotal-row', '全表',
|
|
221
|
+
f'{len(sub)} 行疑似「合计/小计」混在明细中(行号 {sub[:6]})',
|
|
222
|
+
'', '分析前剔除,否则求和会翻倍;合计用 pivot_table(margins=True) 自己生成'))
|
|
223
|
+
|
|
224
|
+
# 全空行/列
|
|
225
|
+
blank_rows = int(df.isna().all(axis=1).sum())
|
|
226
|
+
if blank_rows:
|
|
227
|
+
res['issues'].append(_issue('info', 'blank-row', '全表', f'{blank_rows} 行整行为空',
|
|
228
|
+
'', 'dropna(how="all") 清掉'))
|
|
229
|
+
|
|
230
|
+
# 主键候选:先单列,再两列组合(组合只在单列都不唯一时找,且列数不多时才试)
|
|
231
|
+
res['keys'] = _find_keys(df, key_hint)
|
|
232
|
+
if not res['keys']:
|
|
233
|
+
res['issues'].append(_issue('info', 'no-key', '全表',
|
|
234
|
+
'没有找到唯一标识列(单列或两列组合)',
|
|
235
|
+
'', '关联前要确认粒度:主键不唯一时 merge 会产生笛卡尔积把行数放大'))
|
|
236
|
+
|
|
237
|
+
for c in df.columns:
|
|
238
|
+
col, iss = profile_column(df[c], str(c), n_rows)
|
|
239
|
+
res['columns'].append(col)
|
|
240
|
+
res['issues'].extend(iss)
|
|
241
|
+
return res
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _subtotal_rows(df, max_scan=20000):
|
|
245
|
+
hits = []
|
|
246
|
+
for c in df.columns:
|
|
247
|
+
s = df[c]
|
|
248
|
+
if s.dtype != object and not pd.api.types.is_string_dtype(s):
|
|
249
|
+
continue
|
|
250
|
+
# .str 访问器会把 NaN 原样传下去(不是 'nan'),所以必须 fillna 后再判,
|
|
251
|
+
# 否则 lambda 会收到 float nan 直接 TypeError
|
|
252
|
+
v = s.head(max_scan).astype(str).str.strip().str.lower().fillna('')
|
|
253
|
+
m = v.apply(lambda x: bool(x) and len(x) <= 12 and any(k in x for k in SUBTOTAL_MARKS))
|
|
254
|
+
hits.extend(list(df.index[:len(m)][m.fillna(False)]))
|
|
255
|
+
return sorted(set(int(i) for i in hits if isinstance(i, (int, np.integer))))[:50]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _find_keys(df, key_hint=None, max_cols=12):
|
|
259
|
+
n = len(df)
|
|
260
|
+
single = [str(c) for c in df.columns
|
|
261
|
+
if df[c].notna().all() and df[c].nunique(dropna=True) == n]
|
|
262
|
+
if single:
|
|
263
|
+
return single[:3]
|
|
264
|
+
cand = [c for c in df.columns if df[c].notna().all() and 1 < df[c].nunique(dropna=True) < n]
|
|
265
|
+
if key_hint:
|
|
266
|
+
cand = [c for c in cand if str(c) in key_hint] or cand
|
|
267
|
+
cand = cand[:max_cols]
|
|
268
|
+
for i, a in enumerate(cand):
|
|
269
|
+
for b in cand[i + 1:]:
|
|
270
|
+
try:
|
|
271
|
+
if not df.duplicated(subset=[a, b]).any():
|
|
272
|
+
return [f'{a} + {b}']
|
|
273
|
+
except Exception:
|
|
274
|
+
continue
|
|
275
|
+
return []
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def profile_file(path, sheet=None, smart=True):
|
|
279
|
+
"""整本工作簿逐 Sheet 体检。smart=True 时优先用 xlsx_toolkit 的稳读(治表头不在首行)。"""
|
|
280
|
+
reader = None
|
|
281
|
+
if smart:
|
|
282
|
+
try:
|
|
283
|
+
import os
|
|
284
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
285
|
+
import xlsx_toolkit as T
|
|
286
|
+
reader = T
|
|
287
|
+
except Exception:
|
|
288
|
+
reader = None
|
|
289
|
+
out = []
|
|
290
|
+
if str(path).lower().endswith('.csv'):
|
|
291
|
+
df = pd.read_csv(path, dtype=object, encoding='utf-8-sig') # 吃掉 BOM,否则首列名带 \ufeff
|
|
292
|
+
out.append(profile(df, '(csv)'))
|
|
293
|
+
return out
|
|
294
|
+
xf = None
|
|
295
|
+
if reader is not None:
|
|
296
|
+
xf, why = reader.open_workbook(path)
|
|
297
|
+
if xf is None:
|
|
298
|
+
return [{'name': str(path), 'rows': 0, 'cols': 0, 'columns': [], 'keys': [],
|
|
299
|
+
'issues': [_issue('error', 'unreadable', str(path), f'打不开:{why}')]}]
|
|
300
|
+
else:
|
|
301
|
+
xf = pd.ExcelFile(path)
|
|
302
|
+
names = [sheet] if sheet else xf.sheet_names
|
|
303
|
+
for s in names:
|
|
304
|
+
try:
|
|
305
|
+
df = reader.smart_read(path, s) if reader is not None else xf.parse(s)
|
|
306
|
+
except Exception as e:
|
|
307
|
+
out.append({'name': s, 'rows': 0, 'cols': 0, 'columns': [], 'keys': [],
|
|
308
|
+
'issues': [_issue('error', 'unreadable', s, f'读取失败:{type(e).__name__}: {e}')]})
|
|
309
|
+
continue
|
|
310
|
+
out.append(profile(df, str(s)))
|
|
311
|
+
return out
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# ── 报告 ───────────────────────────────────────────────────────────────────
|
|
315
|
+
LEVEL = {'error': '必修', 'warn': '建议', 'info': '提示'}
|
|
316
|
+
ORDER = {'error': 0, 'warn': 1, 'info': 2}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def report(res, max_cols=40):
|
|
320
|
+
if isinstance(res, list):
|
|
321
|
+
return '\n\n'.join(report(r, max_cols) for r in res)
|
|
322
|
+
lines = [f"【数据体检】{res.get('name') or '表'} · {res['rows']} 行 × {res['cols']} 列"
|
|
323
|
+
+ (f" · {res['memory_mb']}MB" if res.get('memory_mb') else '')]
|
|
324
|
+
if res.get('keys'):
|
|
325
|
+
lines.append(f" 唯一标识:{'、'.join(res['keys'])}")
|
|
326
|
+
cnt = Counter(i['level'] for i in res['issues'])
|
|
327
|
+
lines.append(f" 结论:必修 {cnt.get('error',0)} · 建议 {cnt.get('warn',0)} · 提示 {cnt.get('info',0)}")
|
|
328
|
+
for it in sorted(res['issues'], key=lambda x: ORDER[x['level']]):
|
|
329
|
+
tail = f" — {it['detail']}" if it['detail'] else ''
|
|
330
|
+
lines.append(f" [{LEVEL[it['level']]}] {it['where']}:{it['msg']}{tail}")
|
|
331
|
+
if it['suggest']:
|
|
332
|
+
lines.append(f" → {it['suggest']}")
|
|
333
|
+
if res['columns']:
|
|
334
|
+
lines.append(' 列清单(类型 / 缺失 / 唯一值 / 示例):')
|
|
335
|
+
for c in res['columns'][:max_cols]:
|
|
336
|
+
st = c.get('stats') or {}
|
|
337
|
+
extra = ''
|
|
338
|
+
if 'min' in st and 'mean' in st:
|
|
339
|
+
extra = f" | {st['min']:g}~{st['max']:g} 均{st['mean']:g}"
|
|
340
|
+
elif 'min' in st:
|
|
341
|
+
extra = f" | {st['min']}~{st['max']}"
|
|
342
|
+
lines.append(f" · {c['name'][:22]:<22} {c['inferred']:<15} "
|
|
343
|
+
f"缺{c['null_rate']*100:.0f}% 唯{c['unique']}{extra} {c['sample'][:2]}")
|
|
344
|
+
if len(res['columns']) > max_cols:
|
|
345
|
+
lines.append(f" · …另有 {len(res['columns']) - max_cols} 列")
|
|
346
|
+
return '\n'.join(lines)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def self_test():
|
|
350
|
+
df = pd.DataFrame({
|
|
351
|
+
'订单编号': ['001', '002', '003', '004', '合计'],
|
|
352
|
+
'客户': ['北京公司', '北京公司 ', '上海公司', '上海公司', ''],
|
|
353
|
+
'金额': ['1,200.00', '3,400.50', '2,000', '900', '7,500.50'],
|
|
354
|
+
'数量': [1, 2, 3, 4, 10],
|
|
355
|
+
'备注': [None, None, None, None, None],
|
|
356
|
+
})
|
|
357
|
+
r = profile(df, '订单明细')
|
|
358
|
+
ks = {i['kind'] for i in r['issues']}
|
|
359
|
+
assert 'num-as-text' in ks, ks # 金额是带千分位的文本
|
|
360
|
+
assert 'subtotal-row' in ks, ks # 「合计」行混在明细里
|
|
361
|
+
assert 'whitespace' in ks or 'alias' in ks, ks # 「北京公司 」带尾空格
|
|
362
|
+
assert 'empty-col' in ks, ks # 备注整列空
|
|
363
|
+
obj_int = pd.DataFrame({'序列': pd.Series([1, 2, 3, 4], dtype=object),
|
|
364
|
+
'名称': ['甲', '乙', '丙', '丁']})
|
|
365
|
+
ks_obj = {i['kind'] for i in profile(obj_int, 'object列真数字')['issues']}
|
|
366
|
+
assert 'num-as-text' not in ks_obj, ks_obj
|
|
367
|
+
clean = pd.DataFrame({'编号': ['A1', 'A2', 'A3'], '销售额': [1.0, 2.0, 3.0],
|
|
368
|
+
'日期': pd.to_datetime(['2026-01-01', '2026-02-01', '2026-03-01'])})
|
|
369
|
+
r2 = profile(clean, '干净表')
|
|
370
|
+
assert not [i for i in r2['issues'] if i['level'] == 'error'], r2['issues']
|
|
371
|
+
assert r2['keys'], '应识别出「编号」是唯一标识'
|
|
372
|
+
assert [c for c in r2['columns'] if c['name'] == '销售额'][0]['inferred'] == 'number'
|
|
373
|
+
print(f"[data_profile] self-test OK · 脏表命中 {len(ks)} 类、干净表零必修")
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def main(argv):
|
|
377
|
+
args = [a for a in argv if not a.startswith('--')]
|
|
378
|
+
if not args:
|
|
379
|
+
self_test()
|
|
380
|
+
return 0
|
|
381
|
+
sheet = None
|
|
382
|
+
for a in argv:
|
|
383
|
+
if a.startswith('--sheet='):
|
|
384
|
+
sheet = a.split('=', 1)[1]
|
|
385
|
+
res = profile_file(args[0], sheet)
|
|
386
|
+
print(json.dumps(res, ensure_ascii=False, default=str) if '--json' in argv else report(res))
|
|
387
|
+
return 0
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
if __name__ == '__main__':
|
|
391
|
+
sys.exit(main(sys.argv[1:]))
|