@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,860 @@
|
|
|
1
|
+
# 归档能力压测台:正向生成 → 档案体检零必修;反向造缺陷 → 断言体检真的会响;反误报。
|
|
2
|
+
#
|
|
3
|
+
# 归档这一层的检查全是「跨文档的事实」,人工复核成本极高(一个项目 50 份文件),
|
|
4
|
+
# 所以它比单文档体检更依赖这层证明:**没有反向用例的档案体检等于没有体检**。
|
|
5
|
+
#
|
|
6
|
+
# 用法:python archive_battery.py [份数] [--keep]
|
|
7
|
+
# 改了 archive_spec / archive_check / archive_build 任何一个,必须重跑。
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import random
|
|
11
|
+
import copy
|
|
12
|
+
import glob
|
|
13
|
+
import re
|
|
14
|
+
import shutil
|
|
15
|
+
import sys
|
|
16
|
+
import tempfile
|
|
17
|
+
|
|
18
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
19
|
+
from docx import Document # noqa: E402
|
|
20
|
+
|
|
21
|
+
import archive_build as B # noqa: E402
|
|
22
|
+
import archive_check as C # noqa: E402
|
|
23
|
+
import archive_spec as S # noqa: E402
|
|
24
|
+
|
|
25
|
+
# 「同一份月报交十二次」的样本必须**够长且多样**:雷同度判据对正文均长不足 300 字的
|
|
26
|
+
# 一律不判(那是还没填的骨架)。样本写短了,测出来的「没命中」不是判据失灵,是样本不够格。
|
|
27
|
+
COPIED_REPORT = (
|
|
28
|
+
'本月项目组围绕总体技术方案继续推进各项研发工作。需求侧完成了与业务部门的三轮口径确认,'
|
|
29
|
+
'澄清了权益核销、跨端同步与异常回滚三类边界场景的处理方式,并同步更新了需求规格说明书,'
|
|
30
|
+
'新增用例十七条,覆盖此前未明确的并发写入与幂等重试两类情形。'
|
|
31
|
+
'开发侧完成数据接入层与规则引擎的联调,打通了从原始事件到决策输出的完整链路,'
|
|
32
|
+
'在测试环境下完成两轮回归,接口平均响应由三百二十毫秒降至二百一十毫秒。'
|
|
33
|
+
'算法侧对特征工程流水线做了重构,将离线与在线特征的口径统一到同一份配置,'
|
|
34
|
+
'消除了训练与推理不一致带来的漂移问题。测试侧补充了高并发与网络抖动的专项用例,'
|
|
35
|
+
'发现并修复三处边界缺陷,其中两处与超时重试的状态机迁移有关。'
|
|
36
|
+
'文档方面完成概要设计说明书的修订与评审,会议纪要与评审记录已归档。'
|
|
37
|
+
'经费方面,本月支出主要为人员工资与测试环境租用,与预算安排基本一致。'
|
|
38
|
+
'风险方面,第三方数据接口的稳定性仍需持续观察,项目组已准备降级方案并纳入下阶段验证计划。')
|
|
39
|
+
|
|
40
|
+
NAMES = ['智能风控', '联邦学习', '知识图谱', '实时数仓', '边缘计算', '数字孪生']
|
|
41
|
+
DEPTS = ['研发一部', '研发二部', '技术中心']
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def random_card(rng, i=0):
|
|
45
|
+
y = rng.choice([2024, 2025, 2026])
|
|
46
|
+
return {
|
|
47
|
+
'项目名称': f'基于{rng.choice(NAMES)}的{rng.choice(["权益","出行","支付"])}系统研发{i or ""}',
|
|
48
|
+
'项目编号': f'ZY{y}{rng.randint(1, 999):05d}', '项目负责人': rng.choice(['张工', '李工', '陈波']),
|
|
49
|
+
'联系电话': f'138{rng.randint(10000000, 99999999)}',
|
|
50
|
+
'建设单位': '示例科技股份有限公司', '部门': rng.choice(DEPTS),
|
|
51
|
+
# 预算总额必须等于科目加总——随机一个总额配固定科目,生成时会(正确地)报「不平」,
|
|
52
|
+
# 而正向跑批把任何提示都当失败。测试数据自己就得先对得平。
|
|
53
|
+
'起止时间': f'{y}年1月1日至{y}年12月31日', '预算': 3000000,
|
|
54
|
+
'考核指标': [[f'指标{k}', f'≥ {rng.randint(80, 99)}%', f'第三方检测{k}']
|
|
55
|
+
for k in range(1, rng.randint(2, 5))],
|
|
56
|
+
'人员': [[f'员工{k}', '研发部', '工程师', f'分工{k}'] for k in range(1, rng.randint(3, 6))],
|
|
57
|
+
'里程碑': [[f'阶段{k}', f'任务{k}', '张工', 80, f'{y}-{k*2:02d}-01', f'{y}-{k*2+1:02d}-28', '交付物']
|
|
58
|
+
for k in range(1, 5)],
|
|
59
|
+
'预算科目': [['其中:人员工资', 2600000], ['服务费', 400000]],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def forward(n, workdir, verbose=True):
|
|
64
|
+
rng = random.Random(20260901)
|
|
65
|
+
bad, worst = 0, []
|
|
66
|
+
for i in range(n):
|
|
67
|
+
card = random_card(rng, i)
|
|
68
|
+
out = os.path.join(workdir, f'f{i}')
|
|
69
|
+
try:
|
|
70
|
+
root, made, warns = B.build(card, out)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
bad += 1
|
|
73
|
+
worst.append((f'f{i}', f'生成异常 {type(e).__name__}: {e}'))
|
|
74
|
+
continue
|
|
75
|
+
if warns:
|
|
76
|
+
bad += 1
|
|
77
|
+
worst.append((f'f{i}', '生成提示:' + ';'.join(warns[:2])))
|
|
78
|
+
continue
|
|
79
|
+
res = C.inspect(root)
|
|
80
|
+
errs = [x for x in res['issues'] if x['level'] == 'error']
|
|
81
|
+
if errs:
|
|
82
|
+
bad += 1
|
|
83
|
+
worst.append((f'f{i}', '; '.join(sorted({x['kind'] + ':' + x['msg'][:44] for x in errs}))))
|
|
84
|
+
shutil.rmtree(out, ignore_errors=True)
|
|
85
|
+
if verbose and (i + 1) % 5 == 0:
|
|
86
|
+
print(f' 正向 {i+1}/{n} …')
|
|
87
|
+
return bad, worst
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ── 反向 ───────────────────────────────────────────────────────────────────
|
|
91
|
+
def _fresh(workdir, name):
|
|
92
|
+
card = random_card(random.Random(7))
|
|
93
|
+
root, _made, _w = B.build(card, os.path.join(workdir, name))
|
|
94
|
+
return root
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _fingerprint(root):
|
|
98
|
+
import hashlib
|
|
99
|
+
h = hashlib.sha256()
|
|
100
|
+
for dp, dn, fn in sorted(os.walk(root)):
|
|
101
|
+
dn.sort()
|
|
102
|
+
for f in sorted(fn):
|
|
103
|
+
p = os.path.join(dp, f)
|
|
104
|
+
h.update(os.path.relpath(p, root).encode())
|
|
105
|
+
try:
|
|
106
|
+
h.update(open(p, 'rb').read())
|
|
107
|
+
except OSError:
|
|
108
|
+
pass
|
|
109
|
+
return h.hexdigest()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _kinds(root):
|
|
113
|
+
return {i['kind'] for i in C.inspect(root)['issues']}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _find(root, frag):
|
|
117
|
+
for dp, _dn, fn in os.walk(root):
|
|
118
|
+
for f in fn:
|
|
119
|
+
if frag in f:
|
|
120
|
+
return os.path.join(dp, f)
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _set_cover(path, key, value):
|
|
125
|
+
"""改封面信息栏里某个字段的值(第二列)。返回是否真的改到了。"""
|
|
126
|
+
d = Document(path)
|
|
127
|
+
for tb in d.tables:
|
|
128
|
+
for row in tb.rows:
|
|
129
|
+
cells = row.cells
|
|
130
|
+
for i in range(len(cells) - 1):
|
|
131
|
+
if S.field_key(cells[i].text) == key:
|
|
132
|
+
cells[i + 1].text = value
|
|
133
|
+
d.save(path)
|
|
134
|
+
return True
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _append_para(path, text, after=None):
|
|
139
|
+
"""追加一段;给了 after 就插到那个标题的**正下方**。
|
|
140
|
+
|
|
141
|
+
字数类用例必须插进指定小节:追加到文末会落到最后一节里,
|
|
142
|
+
「项目立项背景」一个字都没变长,用例看着跑了其实什么都没验到。"""
|
|
143
|
+
d = Document(path)
|
|
144
|
+
if after:
|
|
145
|
+
for para in d.paragraphs:
|
|
146
|
+
if after in para.text:
|
|
147
|
+
new_p = copy.deepcopy(para._p)
|
|
148
|
+
para._p.addnext(new_p)
|
|
149
|
+
from docx.text.paragraph import Paragraph
|
|
150
|
+
np = Paragraph(new_p, para._parent)
|
|
151
|
+
for r in list(np.runs)[1:]:
|
|
152
|
+
r._r.getparent().remove(r._r)
|
|
153
|
+
if np.runs:
|
|
154
|
+
np.runs[0].text = text
|
|
155
|
+
else:
|
|
156
|
+
np.add_run(text)
|
|
157
|
+
np.style = d.styles['Normal']
|
|
158
|
+
d.save(path)
|
|
159
|
+
return
|
|
160
|
+
d.add_paragraph(text)
|
|
161
|
+
d.save(path)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _drop_heading(path, title):
|
|
165
|
+
d = Document(path)
|
|
166
|
+
hit = False
|
|
167
|
+
for p in list(d.paragraphs):
|
|
168
|
+
if title in p.text and str(p.style.name or '').startswith('Heading'):
|
|
169
|
+
p._p.getparent().remove(p._p)
|
|
170
|
+
hit = True
|
|
171
|
+
if hit:
|
|
172
|
+
d.save(path)
|
|
173
|
+
return hit
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def reverse(workdir):
|
|
177
|
+
cases = []
|
|
178
|
+
|
|
179
|
+
def case(name, expect, mutate):
|
|
180
|
+
root = _fresh(workdir, 'r-' + re.sub(r'\W', '', name))
|
|
181
|
+
# 守卫必须按**内容指纹**判,不能只数文件个数:改一份文档里的字段不改变文件数,
|
|
182
|
+
# 数文件会把「用例确实改了」误判成「用例没改到」,反过来掩盖真问题。
|
|
183
|
+
before = _fingerprint(root)
|
|
184
|
+
mutate(root)
|
|
185
|
+
if _fingerprint(root) == before:
|
|
186
|
+
cases.append((name, expect, False, {'<用例没改到文件>'}))
|
|
187
|
+
return
|
|
188
|
+
got = _kinds(root)
|
|
189
|
+
cases.append((name, expect, expect in got, got))
|
|
190
|
+
|
|
191
|
+
case('删掉必备文档', 'missing-doc',
|
|
192
|
+
lambda r: os.remove(_find(r, '研发任务书')))
|
|
193
|
+
case('周期性报告缺期', 'missing-periodic',
|
|
194
|
+
lambda r: [os.remove(_find(r, f'研发进度报告-{n:02d}')) for n in (3, 7)])
|
|
195
|
+
case('评审只有报告没签到表', 'review-unpaired',
|
|
196
|
+
lambda r: os.remove(_find(r, '立项评审会议签到表')))
|
|
197
|
+
case('项目名称被截断', 'field-truncated',
|
|
198
|
+
lambda r: _set_cover(_find(r, '技术总结报告'), '项目名称',
|
|
199
|
+
os.path.basename(r)[:-2]))
|
|
200
|
+
case('项目编号不一致', 'field-mismatch',
|
|
201
|
+
lambda r: _set_cover(_find(r, '项目验收申请表'), '项目编号', 'ZY99999999'))
|
|
202
|
+
case('联系电话栏填了人名', 'field-shape',
|
|
203
|
+
lambda r: _set_cover(_find(r, '立项申请书'), '联系电话', '陈波'))
|
|
204
|
+
case('模板提示块未删', 'hint',
|
|
205
|
+
lambda r: _append_para(_find(r, '项目建议书'), '提示:这里要写清楚产品定义。'))
|
|
206
|
+
case('残留 XX 占位符', 'placeholder',
|
|
207
|
+
lambda r: _append_para(_find(r, '可行性研究报告'), '本项目由技术中心XX部承担。'))
|
|
208
|
+
case('留了待办占位', 'todo',
|
|
209
|
+
lambda r: _append_para(_find(r, '工作总结报告'), '经费明细待补充。'))
|
|
210
|
+
case('缺必备章节', 'missing-section',
|
|
211
|
+
lambda r: _drop_heading(_find(r, '立项申请书'), '项目考核指标'))
|
|
212
|
+
def copy_reports(root):
|
|
213
|
+
"""把 12 期月报换成同一份——真实归档里最常见也最难由份数检查发现的敷衍。"""
|
|
214
|
+
from docx import Document as _D
|
|
215
|
+
d = os.path.join(root, '周期性文档', '研发进度报告')
|
|
216
|
+
for f in sorted(os.listdir(d)):
|
|
217
|
+
p2 = os.path.join(d, f)
|
|
218
|
+
doc = _D(p2)
|
|
219
|
+
for para in list(doc.paragraphs):
|
|
220
|
+
if not str(para.style.name or '').startswith('Heading') and len(para.text.strip()) >= 15:
|
|
221
|
+
para._p.getparent().remove(para._p)
|
|
222
|
+
doc.add_paragraph(COPIED_REPORT)
|
|
223
|
+
doc.save(p2)
|
|
224
|
+
case('12 期月报是同一份', 'periodic-duplicate', copy_reports)
|
|
225
|
+
|
|
226
|
+
def add_para(root, rel, text):
|
|
227
|
+
from docx import Document as _D
|
|
228
|
+
p2 = os.path.join(root, rel)
|
|
229
|
+
d2 = _D(p2); d2.add_paragraph(text); d2.save(p2)
|
|
230
|
+
|
|
231
|
+
case('模板批注未删', 'template-note',
|
|
232
|
+
lambda r: add_para(r, '9-10验收/项目验收报告.docx',
|
|
233
|
+
'注:由项目单位如实填写实际完成情况,下同。'))
|
|
234
|
+
case('选择项一个都没勾', 'unchecked-choice',
|
|
235
|
+
lambda r: add_para(r, '9-10验收/项目验收报告.docx',
|
|
236
|
+
'1.全面完成( ) 2.完成70%及以上( ) 3.完成70%以下( )'))
|
|
237
|
+
|
|
238
|
+
def unfinished(root):
|
|
239
|
+
from docx import Document as _D
|
|
240
|
+
p2 = os.path.join(root, '9-10验收', '项目验收报告.docx')
|
|
241
|
+
d2 = _D(p2)
|
|
242
|
+
for tb in d2.tables:
|
|
243
|
+
head = ' '.join(c.text for c in tb.rows[0].cells)
|
|
244
|
+
if '实际完成' in head or '规定' in head:
|
|
245
|
+
tb.rows[1].cells[1].text = '进行中,指标为预计完成指标'
|
|
246
|
+
break
|
|
247
|
+
d2.save(p2)
|
|
248
|
+
case('验收写「进行中」', 'accepting-unfinished', unfinished)
|
|
249
|
+
|
|
250
|
+
def wrong_month(root):
|
|
251
|
+
from docx import Document as _D
|
|
252
|
+
p2 = os.path.join(root, '周期性文档', '研发进度报告', '研发进度报告-07.docx')
|
|
253
|
+
d2 = _D(p2)
|
|
254
|
+
for tb in d2.tables:
|
|
255
|
+
for row in tb.rows:
|
|
256
|
+
for c in row.cells:
|
|
257
|
+
if re.match(r'^\s*20\d{2}\s*年', c.text.strip()):
|
|
258
|
+
c.text = '2099 年 8 月 31 日'
|
|
259
|
+
d2.save(p2)
|
|
260
|
+
case('期号与落款月份不符', 'period-date-mismatch', wrong_month)
|
|
261
|
+
|
|
262
|
+
def bad_review_order(root):
|
|
263
|
+
from docx import Document as _D
|
|
264
|
+
d = os.path.join(root, '评审文件')
|
|
265
|
+
for f in os.listdir(d):
|
|
266
|
+
if not f.endswith('.docx'):
|
|
267
|
+
continue
|
|
268
|
+
want = None
|
|
269
|
+
if '验收评审' in f:
|
|
270
|
+
want = '2026 年 12 月 20 日'
|
|
271
|
+
elif '结项评审' in f:
|
|
272
|
+
want = '2026 年 1 月 5 日'
|
|
273
|
+
if not want:
|
|
274
|
+
continue
|
|
275
|
+
doc = _D(os.path.join(d, f))
|
|
276
|
+
for tb in doc.tables:
|
|
277
|
+
for row in tb.rows:
|
|
278
|
+
for c in row.cells:
|
|
279
|
+
if re.match(r'^\s*20\d{2}\s*年', c.text.strip()):
|
|
280
|
+
c.text = want
|
|
281
|
+
doc.save(os.path.join(d, f))
|
|
282
|
+
case('结项评审早于验收评审', 'review-out-of-order', bad_review_order)
|
|
283
|
+
|
|
284
|
+
def sheet_edit(root, frag, fn):
|
|
285
|
+
from openpyxl import load_workbook as _lw
|
|
286
|
+
import glob as _g
|
|
287
|
+
p2 = _g.glob(os.path.join(root, '**', f'*{frag}*.xlsx'), recursive=True)[0]
|
|
288
|
+
wb = _lw(p2); fn(wb[wb.sheetnames[0]]); wb.save(p2)
|
|
289
|
+
|
|
290
|
+
def _hdr(ws, key='任务名称'):
|
|
291
|
+
for r in range(1, 8):
|
|
292
|
+
for c in range(1, ws.max_column + 1):
|
|
293
|
+
if str(ws.cell(r, c).value).strip() == key:
|
|
294
|
+
return r
|
|
295
|
+
return 1
|
|
296
|
+
|
|
297
|
+
def _col(ws, r, name):
|
|
298
|
+
for c in range(1, ws.max_column + 1):
|
|
299
|
+
if str(ws.cell(r, c).value).strip() == name:
|
|
300
|
+
return c
|
|
301
|
+
return 1
|
|
302
|
+
|
|
303
|
+
case('进度值越界', 'progress-out-of-range',
|
|
304
|
+
lambda r: sheet_edit(r, '实际-第1季度',
|
|
305
|
+
lambda ws: ws.cell(_hdr(ws) + 1, _col(ws, _hdr(ws), '进度'),
|
|
306
|
+
2.0)))
|
|
307
|
+
def clear_actual_end(ws):
|
|
308
|
+
# 注意:ws.cell(r, c, None) 是空操作(openpyxl 把 None 当「没传值」),
|
|
309
|
+
# 清空必须显式赋 .value = None。
|
|
310
|
+
r = _hdr(ws)
|
|
311
|
+
ws.cell(r + 1, _col(ws, r, '实际结束')).value = None
|
|
312
|
+
case('标记完成却无实际结束', 'done-without-date',
|
|
313
|
+
lambda r: sheet_edit(r, '实际-第4季度', clear_actual_end))
|
|
314
|
+
case('计划表里填了进度', 'plan-has-progress',
|
|
315
|
+
lambda r: sheet_edit(r, '进度表-计划',
|
|
316
|
+
lambda ws: ws.cell(_hdr(ws) + 1, _col(ws, _hdr(ws), '进度'), 1.0)))
|
|
317
|
+
case('实际表任务被改名', 'task-list-drift',
|
|
318
|
+
lambda r: sheet_edit(r, '实际-第2季度',
|
|
319
|
+
lambda ws: ws.cell(_hdr(ws) + 1, _col(ws, _hdr(ws), '任务名称'),
|
|
320
|
+
'偷偷改过的任务')))
|
|
321
|
+
|
|
322
|
+
def clear_review(root):
|
|
323
|
+
from openpyxl import load_workbook as _lw
|
|
324
|
+
import glob as _g
|
|
325
|
+
p2 = _g.glob(os.path.join(root, '评审文件', '*立项评审报告*.xlsx'))[0]
|
|
326
|
+
wb = _lw(p2); ws = wb['评审记录']
|
|
327
|
+
for rr in range(2, ws.max_row + 1):
|
|
328
|
+
for cc in range(1, ws.max_column + 1):
|
|
329
|
+
ws.cell(rr, cc).value = None
|
|
330
|
+
wb.save(p2)
|
|
331
|
+
case('评审记录被清空', 'empty-review-sheet', clear_review)
|
|
332
|
+
|
|
333
|
+
# ── 项目名称一族 ───────────────────────────────────────────────────
|
|
334
|
+
def rename_body(root, old_frag, new_text, doc='技术总结报告'):
|
|
335
|
+
"""把正文里的项目名改掉(只改正文,不动封面字段——封面是判据的基准)。"""
|
|
336
|
+
d = Document(_find(root, doc))
|
|
337
|
+
proj = os.path.basename(root)
|
|
338
|
+
hit = False
|
|
339
|
+
for para in d.paragraphs:
|
|
340
|
+
if proj in para.text:
|
|
341
|
+
for run in para.runs:
|
|
342
|
+
if proj in run.text:
|
|
343
|
+
run.text = run.text.replace(proj, new_text)
|
|
344
|
+
hit = True
|
|
345
|
+
if hit:
|
|
346
|
+
break
|
|
347
|
+
if not hit: # 正文找不到就追加一段,保证用例真的改到了
|
|
348
|
+
d.add_paragraph(new_text + '的研究成果如下。')
|
|
349
|
+
d.save(_find(root, doc))
|
|
350
|
+
|
|
351
|
+
def _proj_name(root):
|
|
352
|
+
"""项目名从档案自身的封面读——**不能用目录名**:
|
|
353
|
+
反向用例的目录叫「r-项目名写错一个字」,拿它当项目名,变异出来的字符串
|
|
354
|
+
跟档案里的任何文字都不沾边,用例看着跑了其实一个字都没改到。"""
|
|
355
|
+
return (C.read_docx(_find(root, '技术总结报告'))['fields']
|
|
356
|
+
.get('项目名称', ''))
|
|
357
|
+
|
|
358
|
+
def typo_body(root):
|
|
359
|
+
proj = _proj_name(root)
|
|
360
|
+
# 改一个**归一化后还在**的字:「的」会被 name_key 删掉,改它等于没改
|
|
361
|
+
# 错字要打在名字**中段**:改「系统/平台」这两个字会让串不再匹配项目名正则,
|
|
362
|
+
# 提取阶段就没了,用例看着改了却一条都不报——那是量具失灵,不是判据对。
|
|
363
|
+
i = max(proj.find('系统'), proj.find('平台'))
|
|
364
|
+
bad = proj[:i - 1] + '盖' + proj[i:] if i > 2 else proj[:-1] + '云'
|
|
365
|
+
rename_body(root, None, bad)
|
|
366
|
+
|
|
367
|
+
case('项目名写错一个字', 'name-typo', typo_body)
|
|
368
|
+
case('签到表抬头是另一个项目', 'name-alien',
|
|
369
|
+
lambda r: _set_cover(_find(r, '立项评审会议签到表'), '项目名称',
|
|
370
|
+
'基于信用模型算法的供应商管理系统研发'))
|
|
371
|
+
case('正文提到另一个项目', 'name-other',
|
|
372
|
+
lambda r: _append_para(_find(r, '工作总结报告'),
|
|
373
|
+
'本项目参考了基于区块链的电子存证平台的研发的经验。'))
|
|
374
|
+
# ── 方框选择题一个都没勾 ─────────────────────────────────────────────
|
|
375
|
+
case('选择框一个都没勾', 'empty-box',
|
|
376
|
+
lambda r: _append_para(_find(r, '项目验收申请表'), '是否通过验收:□是 □否'))
|
|
377
|
+
# ── 模板字数上限 ─────────────────────────────────────────────────────
|
|
378
|
+
case('立项背景严重超字数', 'over-limit',
|
|
379
|
+
lambda r: _append_para(_find(r, '工作总结报告'), '背景补充。' * 400,
|
|
380
|
+
after='项目立项背景'))
|
|
381
|
+
|
|
382
|
+
# ── 模板点名的必备要素 ───────────────────────────────────────────────
|
|
383
|
+
def wipe_basis(root):
|
|
384
|
+
"""把「立项的依据和来源」正文换成一句不含文号/合同号/起止时间的话。"""
|
|
385
|
+
path = _find(root, '工作总结报告')
|
|
386
|
+
d = Document(path)
|
|
387
|
+
hit = False
|
|
388
|
+
for i, para in enumerate(d.paragraphs):
|
|
389
|
+
if para.text.strip() == '项目立项的依据和来源':
|
|
390
|
+
for nxt in d.paragraphs[i + 1:]:
|
|
391
|
+
if nxt.style.name.startswith('Heading'):
|
|
392
|
+
break
|
|
393
|
+
if nxt.text.strip():
|
|
394
|
+
nxt.text = '依据公司研发规划立项。'
|
|
395
|
+
hit = True
|
|
396
|
+
break
|
|
397
|
+
if not hit:
|
|
398
|
+
d.add_paragraph('项目立项的依据和来源')
|
|
399
|
+
d.add_paragraph('依据公司研发规划立项。')
|
|
400
|
+
d.save(path)
|
|
401
|
+
case('立项依据缺文号与起止时间', 'missing-element', wipe_basis)
|
|
402
|
+
|
|
403
|
+
case('缺签批栏', 'missing-approval',
|
|
404
|
+
lambda r: _drop_heading(_find(r, '研发任务书'), '审批记录') and
|
|
405
|
+
_strip_approval(_find(r, '研发任务书')))
|
|
406
|
+
return cases
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _strip_approval(path):
|
|
410
|
+
"""把签批栏那张 kv 表整个删掉(只删标题不够,字段仍在表里)。"""
|
|
411
|
+
d = Document(path)
|
|
412
|
+
hit = False
|
|
413
|
+
for tb in list(d.tables):
|
|
414
|
+
txt = ' '.join(c.text for row in tb.rows for c in row.cells)
|
|
415
|
+
if '技术负责人意见' in txt or '项目负责人意见' in txt:
|
|
416
|
+
tb._tbl.getparent().remove(tb._tbl)
|
|
417
|
+
hit = True
|
|
418
|
+
if hit:
|
|
419
|
+
d.save(path)
|
|
420
|
+
return hit
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
# ── 反误报 ─────────────────────────────────────────────────────────────────
|
|
424
|
+
def negative(workdir):
|
|
425
|
+
cases = []
|
|
426
|
+
|
|
427
|
+
def nocase(name, forbidden, mutate):
|
|
428
|
+
root = _fresh(workdir, 'n-' + re.sub(r'\W', '', name))
|
|
429
|
+
if mutate:
|
|
430
|
+
mutate(root)
|
|
431
|
+
got = _kinds(root)
|
|
432
|
+
bad = [k for k in forbidden if k in got]
|
|
433
|
+
cases.append((name, not bad, bad))
|
|
434
|
+
|
|
435
|
+
# 立项依据写全了文号、合同号、起止时间就不许报
|
|
436
|
+
def full_basis(root):
|
|
437
|
+
_append_para(_find(root, '工作总结报告'),
|
|
438
|
+
'依据〔2025〕12号文件、合同号 HT-2025-007,研究起止时间 2025年1月至2025年12月。',
|
|
439
|
+
after='项目立项的依据和来源')
|
|
440
|
+
nocase('立项依据要素齐全不报', ['missing-element'], full_basis)
|
|
441
|
+
|
|
442
|
+
# 签到表填了与会人就不许报
|
|
443
|
+
def fill_signin(root):
|
|
444
|
+
# **五张全填**:nocase 是对整套归档断言的,只填一张,另外四张照样报,
|
|
445
|
+
# 看着像判据误报,其实是用例只做了五分之一。
|
|
446
|
+
import glob as _g
|
|
447
|
+
for path in _g.glob(os.path.join(root, '**', '*签到表*.docx'), recursive=True):
|
|
448
|
+
d = Document(path)
|
|
449
|
+
for tb in d.tables:
|
|
450
|
+
done = False
|
|
451
|
+
for r, row in enumerate(tb.rows):
|
|
452
|
+
if any('姓名' in c.text for c in row.cells):
|
|
453
|
+
col = next(j for j, c in enumerate(row.cells) if '姓名' in c.text)
|
|
454
|
+
if r + 1 < len(tb.rows):
|
|
455
|
+
tb.rows[r + 1].cells[col].text = '陈波'
|
|
456
|
+
done = True
|
|
457
|
+
break
|
|
458
|
+
if done:
|
|
459
|
+
d.save(path)
|
|
460
|
+
break
|
|
461
|
+
nocase('签到表已填与会人不报', ['signin-empty'], fill_signin)
|
|
462
|
+
|
|
463
|
+
# 简称是正常的:真实归档里「…系统的研发」「…系统研发」「…系统」混用 144 次。
|
|
464
|
+
# 这三种写法**一个都不许报**——报了就会淹掉真正的错字(差一个字的那种)。
|
|
465
|
+
def abbrev(root):
|
|
466
|
+
proj = (C.read_docx(_find(root, '技术总结报告'))['fields']
|
|
467
|
+
.get('项目名称', ''))
|
|
468
|
+
short = re.sub(r'的?研发\d*$', '', proj)
|
|
469
|
+
_append_para(_find(root, '技术总结报告'),
|
|
470
|
+
f'{short}系统已完成;{short}的研发工作已结束;{proj}通过验收。')
|
|
471
|
+
nocase('项目名简称/加减「的」不算异名', ['name-typo', 'name-alien', 'name-other'], abbrev)
|
|
472
|
+
|
|
473
|
+
# 勾了的方框不能再报「一个都没勾」
|
|
474
|
+
nocase('方框已勾选不报', ['empty-box'],
|
|
475
|
+
lambda r: _append_para(_find(r, '项目验收申请表'), '是否通过验收:■是 □否'))
|
|
476
|
+
|
|
477
|
+
# 字数在限内(含 1.5 倍宽限)不报
|
|
478
|
+
nocase('立项背景未超字数不报', ['over-limit'], None)
|
|
479
|
+
|
|
480
|
+
# 文件改名(真实项目会把「6-研发实施计划」写成「6-项目研发实施计划」)不得判成缺失
|
|
481
|
+
def rename(root):
|
|
482
|
+
p = _find(root, '项目研发实施计划')
|
|
483
|
+
os.rename(p, os.path.join(os.path.dirname(p), '6-研发实施计划(V2).docx'))
|
|
484
|
+
nocase('文件改了名', ['missing-doc'], rename)
|
|
485
|
+
|
|
486
|
+
# 章节用了别名(「验收标准」而不是「验收标准和验收工作安排」)不得判成缺章节
|
|
487
|
+
def alias(root):
|
|
488
|
+
p = _find(root, '项目研发实施计划')
|
|
489
|
+
d = Document(p)
|
|
490
|
+
for para in d.paragraphs:
|
|
491
|
+
if '验收标准和验收工作安排' in para.text:
|
|
492
|
+
for r in para.runs:
|
|
493
|
+
r.text = ''
|
|
494
|
+
para.runs[0].text = '七、验收标准' if para.runs else ''
|
|
495
|
+
d.save(p)
|
|
496
|
+
nocase('章节用了别名', ['missing-section'], alias)
|
|
497
|
+
|
|
498
|
+
# 条件性目录(变更/委外/测试)缺席不算缺陷
|
|
499
|
+
nocase('没有变更与委外文档', ['missing-doc'], lambda r: None)
|
|
500
|
+
|
|
501
|
+
# 刚生成的骨架正文还没填,12 期长得像是必然的——不能判成抄
|
|
502
|
+
nocase('空骨架不判雷同', ['periodic-duplicate', 'periodic-similar'], lambda r: None)
|
|
503
|
+
|
|
504
|
+
def vary_reports(root):
|
|
505
|
+
from docx import Document as _D
|
|
506
|
+
uniq = ['本月主攻需求澄清,与业务部门确认了权益核销的三种口径,形成争议纪要一份。',
|
|
507
|
+
'本月完成数据接入层重构,弃用轮询方案,改为基于变更日志的增量同步。',
|
|
508
|
+
'本月算法侧引入对比学习预训练,冷启动召回率由四成七提升到六成一。',
|
|
509
|
+
'本月处理线上事故一起,根因是分布式锁在主从切换时被提前释放,已加租约续期。',
|
|
510
|
+
'本月与法务完成数据合规评审,补充跨境传输三项限制并写入接口契约。',
|
|
511
|
+
'本月压测暴露连接池瓶颈,最大连接数由二百调到八百并按租户隔离。',
|
|
512
|
+
'本月完成灰度发布方案设计,按流量比例与用户标签双维度切分,支持一键回滚。',
|
|
513
|
+
'本月清理十九个已下线接口与四张僵尸表,释放存储三点二 TB。',
|
|
514
|
+
'本月与第三方厂商完成对接联调,就重试语义与幂等键达成一致并签技术附件。',
|
|
515
|
+
'本月组织内部技术分享两场,主题为特征平台设计与在线推理降级策略。',
|
|
516
|
+
'本月开展安全渗透测试,发现越权访问缺陷两处,均已修复并补回归用例。',
|
|
517
|
+
'本月完成验收材料预审,对照任务书逐条核对指标,识别两项证据缺口。']
|
|
518
|
+
d = os.path.join(root, '周期性文档', '研发进度报告')
|
|
519
|
+
for f, u in zip(sorted(os.listdir(d)), uniq):
|
|
520
|
+
p2 = os.path.join(d, f)
|
|
521
|
+
doc = _D(p2)
|
|
522
|
+
doc.add_paragraph(COPIED_REPORT[:300] + u * 5)
|
|
523
|
+
doc.save(p2)
|
|
524
|
+
nocase('各期有实质不同内容', ['periodic-duplicate'], vary_reports)
|
|
525
|
+
|
|
526
|
+
def real_notes(root):
|
|
527
|
+
from docx import Document as _D
|
|
528
|
+
p2 = os.path.join(root, '5-研发任务书.docx')
|
|
529
|
+
d2 = _D(p2)
|
|
530
|
+
for t in ('注:数据口径以合同附件三为准。', '注:金额单位为万元,含税。',
|
|
531
|
+
'注:本表由财务部填报后归档。'):
|
|
532
|
+
d2.add_paragraph(t)
|
|
533
|
+
d2.save(p2)
|
|
534
|
+
nocase('正经注释不判模板批注', ['template-note'], real_notes)
|
|
535
|
+
|
|
536
|
+
def one_ticked(root):
|
|
537
|
+
from docx import Document as _D
|
|
538
|
+
p2 = os.path.join(root, '9-10验收', '项目验收报告.docx')
|
|
539
|
+
d2 = _D(p2)
|
|
540
|
+
d2.add_paragraph('1.全面完成(√) 2.完成70%及以上( ) 3.完成70%以下( )')
|
|
541
|
+
d2.save(p2)
|
|
542
|
+
nocase('勾了一个就不算未勾', ['unchecked-choice'], one_ticked)
|
|
543
|
+
|
|
544
|
+
# 0.03 / 0.38 / 0.75 是正常的完成比例,不许判越界
|
|
545
|
+
def normal_ratio(root):
|
|
546
|
+
from openpyxl import load_workbook as _lw
|
|
547
|
+
import glob as _g
|
|
548
|
+
p2 = _g.glob(os.path.join(root, '7-进度表', '*实际-第2季度*.xlsx'))[0]
|
|
549
|
+
wb = _lw(p2); ws = wb[wb.sheetnames[0]]
|
|
550
|
+
hr = 1
|
|
551
|
+
for rr in range(1, 8):
|
|
552
|
+
if any(str(ws.cell(rr, cc).value).strip() == '进度'
|
|
553
|
+
for cc in range(1, ws.max_column + 1)):
|
|
554
|
+
hr = rr
|
|
555
|
+
break
|
|
556
|
+
pc = next(cc for cc in range(1, ws.max_column + 1)
|
|
557
|
+
if str(ws.cell(hr, cc).value).strip() == '进度')
|
|
558
|
+
for i, v in enumerate((0.03, 0.38, 0.75)):
|
|
559
|
+
ws.cell(hr + 1 + i, pc).value = v
|
|
560
|
+
wb.save(p2)
|
|
561
|
+
nocase('正常完成比例不判越界', ['progress-out-of-range'], normal_ratio)
|
|
562
|
+
return cases
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def invariants(workdir):
|
|
566
|
+
"""不变量:不靠体检报不报,直接断言产物本身对不对。"""
|
|
567
|
+
out = []
|
|
568
|
+
|
|
569
|
+
def chk(name, fn):
|
|
570
|
+
try:
|
|
571
|
+
out.append((name, True, fn() or ''))
|
|
572
|
+
except Exception as e:
|
|
573
|
+
out.append((name, False, f'{type(e).__name__}: {str(e)[:120]}'))
|
|
574
|
+
|
|
575
|
+
def budget_name_mismatch():
|
|
576
|
+
"""卡里写自造科目名 → 必须点名并给出正确科目表。
|
|
577
|
+
|
|
578
|
+
真跑抓到的:卡上预算 280 万、科目加总也 280 万(内部勾稽通过),
|
|
579
|
+
但科目名「人员费/设备费」对不上固定口径,`plan.get(name, 0)` 让它们静默变零,
|
|
580
|
+
生成出来的预算表整张全零而**没有任何提示**——最难查的一类失败。"""
|
|
581
|
+
c = random_card(random.Random(11))
|
|
582
|
+
c['预算科目'] = [['人员费', 1800000], ['设备费', 400000]]
|
|
583
|
+
c['预算'] = 2200000
|
|
584
|
+
root = os.path.join(workdir, 'inv-budgetname')
|
|
585
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
586
|
+
_r, _m, warns = B.build(c, root)
|
|
587
|
+
hit = [w for w in warns if '预算科目' in w and '对不上' in w]
|
|
588
|
+
assert hit, '自造预算科目名没有被点名'
|
|
589
|
+
assert '人员费' in hit[0] and '设备费' in hit[0], '没说清是哪几项对不上'
|
|
590
|
+
assert '其中:人员工资' in hit[0], '没给出正确科目表,调用方无法自纠'
|
|
591
|
+
# 用正确科目名重来,表里必须真有钱
|
|
592
|
+
c['预算科目'] = [['其中:人员工资', 1800000], ['服务费', 400000]]
|
|
593
|
+
root2 = os.path.join(workdir, 'inv-budgetok')
|
|
594
|
+
shutil.rmtree(root2, ignore_errors=True)
|
|
595
|
+
_r2, _m2, w2 = B.build(c, root2)
|
|
596
|
+
assert not [w for w in w2 if '预算科目' in w and '对不上' in w], '正确科目名不该被点名'
|
|
597
|
+
import pandas as _pd
|
|
598
|
+
f = glob.glob(os.path.join(root2, '**', '*预算*'), recursive=True)[0]
|
|
599
|
+
df = _pd.read_excel(f, header=None, dtype=object)
|
|
600
|
+
nz = [v for row in df.itertuples(index=False, name=None) for v in row
|
|
601
|
+
if isinstance(v, (int, float)) and v and v > 1000]
|
|
602
|
+
assert nz and max(nz) == 1800000, f'预算表没落进金额:{nz[:5]}'
|
|
603
|
+
return f'点名 2 项 + 正确名下落进 {len(nz)} 处金额'
|
|
604
|
+
|
|
605
|
+
def unknown_section_lists_valid():
|
|
606
|
+
"""章节名对不上 → 警告里必须列出该类文档的规范章节。
|
|
607
|
+
|
|
608
|
+
只说「写错了」不给对的名字,调用方只能一次次猜;真跑里模型自造的章节名
|
|
609
|
+
几乎没一个对得上,正文全被排到了文末,而表面看一切正常。"""
|
|
610
|
+
c = random_card(random.Random(12))
|
|
611
|
+
c['章节内容'] = {'项目建议书': {'项目建设背景与必要性': '正文一'}}
|
|
612
|
+
root = os.path.join(workdir, 'inv-section')
|
|
613
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
614
|
+
_r, _m, warns = B.build(c, root)
|
|
615
|
+
hit = [w for w in warns if '规范章节' in w]
|
|
616
|
+
assert hit, '未知章节名没有被点名'
|
|
617
|
+
assert '项目建设背景与必要性' in hit[0], '没说是哪个章节名错了'
|
|
618
|
+
assert '规范章节是' in hit[0] and '文档介绍' in hit[0], '没列出规范章节,无法自纠'
|
|
619
|
+
return '点名 + 列出规范章节'
|
|
620
|
+
|
|
621
|
+
def unknown_kind_lists_all():
|
|
622
|
+
"""未收录的文档类型:可用类型要**列全**,不能截断成「…」。"""
|
|
623
|
+
c = random_card(random.Random(13))
|
|
624
|
+
c['章节内容'] = {'项目工作总结报告': {'某节': '正文'}}
|
|
625
|
+
root = os.path.join(workdir, 'inv-kind')
|
|
626
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
627
|
+
_r, _m, warns = B.build(c, root)
|
|
628
|
+
hit = [w for w in warns if '未收录的文档类型' in w]
|
|
629
|
+
assert hit, '未知文档类型没有被点名'
|
|
630
|
+
assert '…' not in hit[0], '可用类型被截断,调用方拿不到正确名字'
|
|
631
|
+
assert '工作总结报告' in hit[0], '正确类型名不在提示里'
|
|
632
|
+
return '未截断'
|
|
633
|
+
|
|
634
|
+
def figure_threshold():
|
|
635
|
+
"""「该有图却没图」的门槛必须落在**空骨架与写实文档之间的空隙**里。
|
|
636
|
+
|
|
637
|
+
量出来的三档:空骨架 260~551 字 / 写实最短 1464 字 / 真实归档 5243 字起。
|
|
638
|
+
门槛原来是 1500,落进了写实区——一份 1464 字、写得很实的技术总结报告
|
|
639
|
+
差 36 字擦边漏过。门槛的作用是「别对空骨架挑刺」,不是「短就不算文档」。
|
|
640
|
+
"""
|
|
641
|
+
c = random_card(random.Random(14))
|
|
642
|
+
root = os.path.join(workdir, 'inv-figskel')
|
|
643
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
644
|
+
B.build(c, root)
|
|
645
|
+
skel = [i for i in C.inspect(root)['issues'] if i['kind'] == 'no-figure']
|
|
646
|
+
assert not skel, f'空骨架不该报缺图:{[i["where"] for i in skel]}'
|
|
647
|
+
# 写实但偏短(~1400 字)的技术总结报告必须报
|
|
648
|
+
c2 = random_card(random.Random(15))
|
|
649
|
+
para = '本系统采用湖仓一体架构,覆盖数据接入、特征计算与决策编排三层。' * 12
|
|
650
|
+
c2['章节内容'] = {'技术总结报告': {'项目简介': para, '主要研究的内容': para}}
|
|
651
|
+
root2 = os.path.join(workdir, 'inv-figreal')
|
|
652
|
+
shutil.rmtree(root2, ignore_errors=True)
|
|
653
|
+
B.build(c2, root2)
|
|
654
|
+
got = [i for i in C.inspect(root2)['issues'] if i['kind'] == 'no-figure']
|
|
655
|
+
assert got, '写实但没图的技术总结报告应当被报出来'
|
|
656
|
+
return '空骨架零误报 + 写实无图必报'
|
|
657
|
+
|
|
658
|
+
chk('缺图门槛落在空骨架与写实文档之间', figure_threshold)
|
|
659
|
+
chk('自造预算科目名被点名且给出正确科目表', budget_name_mismatch)
|
|
660
|
+
chk('未知章节名的警告列出规范章节', unknown_section_lists_valid)
|
|
661
|
+
chk('未知文档类型的可用列表不截断', unknown_kind_lists_all)
|
|
662
|
+
|
|
663
|
+
def long_name():
|
|
664
|
+
c = random_card(random.Random(3))
|
|
665
|
+
c['项目名称'] = '超长项目名称' * 50
|
|
666
|
+
root, made, _w = B.build(c, os.path.join(workdir, 'inv-long'))
|
|
667
|
+
bad = [p for p in made
|
|
668
|
+
for seg in os.path.relpath(p, os.path.dirname(root)).split(os.sep)
|
|
669
|
+
if len(seg.encode()) > 255]
|
|
670
|
+
assert not bad, f'{len(bad)} 个路径段超 255 字节'
|
|
671
|
+
return '路径段均合规'
|
|
672
|
+
chk('超长项目名不撑爆路径', long_name)
|
|
673
|
+
|
|
674
|
+
def inject_unknown():
|
|
675
|
+
c = random_card(random.Random(4))
|
|
676
|
+
c['章节内容'] = {'立项申请书': {'查无此节': '这段内容不该凭空消失。'}}
|
|
677
|
+
root, _m, w = B.build(c, os.path.join(workdir, 'inv-inj'), only=['立项申请书'])
|
|
678
|
+
p = os.path.join(root, '1-立项文档', '3-立项申请书.docx')
|
|
679
|
+
assert '这段内容不该凭空消失' in C.read_docx(p)['text'], '注入内容被静默丢弃'
|
|
680
|
+
assert any('查无此节' in x for x in w), '丢内容却没有任何提示'
|
|
681
|
+
return '内容保留 + 有提示'
|
|
682
|
+
chk('注入到不存在的章节不丢内容', inject_unknown)
|
|
683
|
+
|
|
684
|
+
def budget_sum():
|
|
685
|
+
from openpyxl import load_workbook
|
|
686
|
+
c = random_card(random.Random(5))
|
|
687
|
+
c['预算科目'] = [['其中:人员工资', 2600000], ['服务费', 400001]]
|
|
688
|
+
root, made, _w = B.build(c, os.path.join(workdir, 'inv-bud'))
|
|
689
|
+
p = [x for x in made if '预算表' in x][0]
|
|
690
|
+
ws = load_workbook(p).active
|
|
691
|
+
for row in ws.iter_rows(values_only=True):
|
|
692
|
+
if not row or not row[0] or '小计' in str(row[0]):
|
|
693
|
+
continue
|
|
694
|
+
months = [v for v in row[1:13] if isinstance(v, (int, float))]
|
|
695
|
+
tot = row[13] if len(row) > 13 and isinstance(row[13], (int, float)) else None
|
|
696
|
+
if tot and months:
|
|
697
|
+
assert abs(sum(months) - tot) < 0.005, f'{row[0]}:12 月合计 {sum(months)} ≠ 全年 {tot}'
|
|
698
|
+
return '横向加总处处相等'
|
|
699
|
+
chk('预算 12 个月之和 = 全年金额', budget_sum)
|
|
700
|
+
|
|
701
|
+
def cross_engine():
|
|
702
|
+
"""归档产物逐份过**单文档/单表体检**——三套引擎的接缝就在这儿。
|
|
703
|
+
|
|
704
|
+
这条曾揪出最隐蔽的一个缺陷:没有任何标题的短文档(决议、通知、签到表)里,
|
|
705
|
+
封面大标题与文号被当成正文,19 份产物齐刷刷报「字号不统一/缩进忽有忽无」。
|
|
706
|
+
档案体检看不见它(那是单文档层面的事),单文档 battery 也看不见(它不生成归档件),
|
|
707
|
+
只有把两层接起来跑才现形。
|
|
708
|
+
"""
|
|
709
|
+
import sys as _s, os as _o
|
|
710
|
+
here = _o.path.dirname(_o.path.abspath(__file__))
|
|
711
|
+
for d in ('docx-author', 'xlsx-analysis'):
|
|
712
|
+
q = _o.path.join(_o.path.dirname(_o.path.dirname(here)), 'skills', d, 'scripts')
|
|
713
|
+
q = q if _o.path.isdir(q) else _o.path.join(_o.path.dirname(_o.path.dirname(here)), d, 'scripts')
|
|
714
|
+
if q not in _s.path:
|
|
715
|
+
_s.path.insert(0, q)
|
|
716
|
+
import docx_check as _DC, xlsx_check as _XC
|
|
717
|
+
c = random_card(random.Random(6))
|
|
718
|
+
root, made, _w = B.build(c, os.path.join(workdir, 'inv-cross'))
|
|
719
|
+
bad = []
|
|
720
|
+
for p2 in made:
|
|
721
|
+
try:
|
|
722
|
+
if p2.endswith('.docx'):
|
|
723
|
+
r = _DC.inspect(p2)
|
|
724
|
+
elif p2.endswith('.xlsx'):
|
|
725
|
+
r = _XC.inspect(p2)
|
|
726
|
+
else:
|
|
727
|
+
continue
|
|
728
|
+
except Exception as e:
|
|
729
|
+
bad.append((os.path.basename(p2), f'{type(e).__name__}'))
|
|
730
|
+
continue
|
|
731
|
+
e2 = [i for i in r['issues'] if i['level'] == 'error']
|
|
732
|
+
if e2:
|
|
733
|
+
bad.append((os.path.basename(p2), sorted({i['kind'] for i in e2})))
|
|
734
|
+
assert not bad, f'{len(bad)} 份产物在单文档/单表体检里有必修:{bad[:3]}'
|
|
735
|
+
return f'{len(made)} 份逐份过检全清'
|
|
736
|
+
chk('归档产物逐份过单文档与单表体检', cross_engine)
|
|
737
|
+
|
|
738
|
+
def deliverables():
|
|
739
|
+
"""里程碑的交付物必须落进计划类文档——它是「任务书→实施计划→验收报告」勾稽链的起点,
|
|
740
|
+
只躺在档案卡里等于没有:验收时按什么逐条核对?"""
|
|
741
|
+
c = random_card(random.Random(8))
|
|
742
|
+
c['里程碑'] = [[f'阶段{i}', f'任务{i}', '张工', 40, f'2026-{i*3:02d}-01',
|
|
743
|
+
f'2026-{i*3:02d}-28', f'交付物{i}'] for i in range(1, 5)]
|
|
744
|
+
root, _m, _w = B.build(c, os.path.join(workdir, 'inv-dlv'),
|
|
745
|
+
only=['研发任务书', '项目研发实施计划'])
|
|
746
|
+
t = (C.read_docx(os.path.join(root, '5-研发任务书.docx'))['text']
|
|
747
|
+
+ C.read_docx(os.path.join(root, '6-项目研发实施计划.docx'))['text'])
|
|
748
|
+
miss = [m[6] for m in c['里程碑'] if m[6] not in t]
|
|
749
|
+
assert not miss, f'交付物没进计划类文档:{miss}'
|
|
750
|
+
return f"{len(c['里程碑'])} 项交付物均落地"
|
|
751
|
+
chk('里程碑交付物进入计划类文档', deliverables)
|
|
752
|
+
|
|
753
|
+
def budget_reconcile():
|
|
754
|
+
"""预算科目加总与档案卡总额不平必须当场说——同一个数要在立项书、预算表、
|
|
755
|
+
验收决算里各出现一次,卡上不平,生成出来就是三处互相打架。"""
|
|
756
|
+
c = random_card(random.Random(9))
|
|
757
|
+
c['预算'], c['预算科目'] = 5000000, [['其中:人员工资', 1234567]]
|
|
758
|
+
_r, _m, w = B.build(c, os.path.join(workdir, 'inv-bud2'), only=['年度研发支出预算表'])
|
|
759
|
+
assert any('预算科目加总' in x for x in w), f'不平却没提示:{w}'
|
|
760
|
+
c2 = random_card(random.Random(10))
|
|
761
|
+
c2['预算'], c2['预算科目'] = 3000000, [['其中:人员工资', 2000000], ['服务费', 1000000]]
|
|
762
|
+
_r2, _m2, w2 = B.build(c2, os.path.join(workdir, 'inv-bud3'), only=['年度研发支出预算表'])
|
|
763
|
+
assert not w2, f'对平了却仍在提示:{w2}'
|
|
764
|
+
return '不平会说、平了不吵'
|
|
765
|
+
chk('预算科目与总额勾稽', budget_reconcile)
|
|
766
|
+
|
|
767
|
+
def optional_periodic():
|
|
768
|
+
"""规范里列为「选做」的周期性文档(周报 52 期、月度开发报告)不得默认生成——
|
|
769
|
+
默认全出会凭空多出六十多份没人要的文件;而且它们的规范产物不是 .docx,
|
|
770
|
+
用 docx 建版器写进 .xlsx 文件名会落成一个打不开的废文件。"""
|
|
771
|
+
c = random_card(random.Random(11))
|
|
772
|
+
root, made, w = B.build(c, os.path.join(workdir, 'inv-opt'))
|
|
773
|
+
names = [os.path.basename(p) for p in made]
|
|
774
|
+
assert not any('周报' in n for n in names), '默认生成了周报'
|
|
775
|
+
assert not any('月度开发报告' in n for n in names), '默认生成了月度开发报告'
|
|
776
|
+
from openpyxl import load_workbook as _lw
|
|
777
|
+
for p in made:
|
|
778
|
+
if p.endswith('.xlsx'):
|
|
779
|
+
_lw(p) # 打不开会直接抛
|
|
780
|
+
assert len(made) < 80, f'产物 {len(made)} 份,疑似把选做件也生成了'
|
|
781
|
+
return f'{len(made)} 份(未含选做件)'
|
|
782
|
+
chk('选做周期性文档不默认生成', optional_periodic)
|
|
783
|
+
|
|
784
|
+
def cumulative():
|
|
785
|
+
"""季度实际表必须是**累计**的:完成数逐季不减,且每个标记完成的行都得有实际起止与工时。
|
|
786
|
+
只填本季度会造出「进度标 1、实际日期空着」的自相矛盾——那正是进度表体检要报的必修。"""
|
|
787
|
+
import glob as _g
|
|
788
|
+
c = random_card(random.Random(12))
|
|
789
|
+
root, _m, _w = B.build(c, os.path.join(workdir, 'inv-cum'))
|
|
790
|
+
ns = []
|
|
791
|
+
for q in range(1, 5):
|
|
792
|
+
p2 = _g.glob(os.path.join(root, '7-进度表', f'*实际-第{q}季度*.xlsx'))[0]
|
|
793
|
+
df = C._read_sheet_df(p2)
|
|
794
|
+
done = df[df['进度'] >= 1]
|
|
795
|
+
ns.append(len(done))
|
|
796
|
+
assert done['实际结束'].notna().all(), f'第 {q} 季度有完成项缺实际结束'
|
|
797
|
+
assert (done['实际工时'].fillna(0) > 0).all(), f'第 {q} 季度有完成项工时为 0'
|
|
798
|
+
assert ns == sorted(ns), f'完成数不是逐季不减:{ns}'
|
|
799
|
+
return f'{ns}'
|
|
800
|
+
chk('季度实际表累计且自洽', cumulative)
|
|
801
|
+
return out
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def main(argv):
|
|
805
|
+
n = 8
|
|
806
|
+
for a in argv:
|
|
807
|
+
if a.isdigit():
|
|
808
|
+
n = int(a)
|
|
809
|
+
keep = '--keep' in argv
|
|
810
|
+
workdir = os.path.join(os.getcwd(), '_abattery') if keep else tempfile.mkdtemp(prefix='arcbat-')
|
|
811
|
+
os.makedirs(workdir, exist_ok=True)
|
|
812
|
+
print(f'压测目录:{workdir}')
|
|
813
|
+
fails = 0
|
|
814
|
+
|
|
815
|
+
print(f'\n[1/4] 正向:随机档案卡 × {n} 套,断言生成的整套归档零必修')
|
|
816
|
+
bad, worst = forward(n, workdir)
|
|
817
|
+
if bad:
|
|
818
|
+
fails += 1
|
|
819
|
+
print(f' ✗ {bad}/{n} 套出现必修项:')
|
|
820
|
+
for name, why in worst[:6]:
|
|
821
|
+
print(f' {name} → {why}')
|
|
822
|
+
else:
|
|
823
|
+
print(f' ✓ {n} 套全部零必修(生成即合规,跨文档一致性天然为真)')
|
|
824
|
+
|
|
825
|
+
print('\n[2/4] 反向:每条规则配一个故意做坏的归档,断言体检真的会响')
|
|
826
|
+
cases = reverse(workdir)
|
|
827
|
+
miss = [c for c in cases if not c[2]]
|
|
828
|
+
for name, expect, hit, got in cases:
|
|
829
|
+
print(f" {'✓' if hit else '✗'} {name:<18} 期望 {expect}"
|
|
830
|
+
+ ('' if hit else f" 实际命中:{sorted(got)[:7]}"))
|
|
831
|
+
if miss:
|
|
832
|
+
fails += 1
|
|
833
|
+
print(f' ✗ {len(miss)}/{len(cases)} 条规则是瞎的')
|
|
834
|
+
else:
|
|
835
|
+
print(f' ✓ {len(cases)}/{len(cases)} 条规则全部命中')
|
|
836
|
+
|
|
837
|
+
print('\n[3/4] 不变量:产物本身必须对(不依赖体检报不报)')
|
|
838
|
+
invs = invariants(workdir)
|
|
839
|
+
for name, ok2, why in invs:
|
|
840
|
+
print(f" {'✓' if ok2 else '✗'} {name:<22} {why}")
|
|
841
|
+
if any(not ok2 for _n, ok2, _w in invs):
|
|
842
|
+
fails += 1
|
|
843
|
+
|
|
844
|
+
print('\n[4/4] 反误报:合规做法不得被判成问题')
|
|
845
|
+
negs = negative(workdir)
|
|
846
|
+
for name, ok, bad2 in negs:
|
|
847
|
+
print(f" {'✓' if ok else '✗'} {name:<18}" + ('' if ok else f' 被误报:{bad2}'))
|
|
848
|
+
if any(not ok for _n, ok, _b in negs):
|
|
849
|
+
fails += 1
|
|
850
|
+
else:
|
|
851
|
+
print(f' ✓ {len(negs)}/{len(negs)} 项合规做法零误报')
|
|
852
|
+
|
|
853
|
+
if not keep:
|
|
854
|
+
shutil.rmtree(workdir, ignore_errors=True)
|
|
855
|
+
print('\n' + ('全部通过。' if not fails else f'有 {fails} 个环节未通过。'))
|
|
856
|
+
return 1 if fails else 0
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
if __name__ == '__main__':
|
|
860
|
+
sys.exit(main(sys.argv[1:]))
|