From 3f2e613ed584855f4c30d49107310e4303988c52 Mon Sep 17 00:00:00 2001 From: sladro Date: Sat, 14 Mar 2026 08:49:28 +0800 Subject: [PATCH] 1.0 --- .claude/settings.local.json | 28 + SKILL.md | 121 +-- agents/openai.yaml | 6 +- evals/evals.json | 17 + references/business-track.md | 41 - references/docx-assembly.md | 23 - references/docx-ops.md | 447 +++++++++ references/evidence-escalation.md | 51 -- references/outline-stage.md | 114 ++- references/output-contracts.md | 185 ---- references/rfp-deconstruction.md | 45 - references/tables-and-scoring.md | 36 - references/technical-track.md | 80 -- references/understandbid.md | 59 ++ .../__pycache__/ai_workflow.cpython-311.pyc | Bin 9995 -> 0 bytes .../build_bid_tables.cpython-311.pyc | Bin 3551 -> 0 bytes ...uild_business_placeholders.cpython-311.pyc | Bin 1466 -> 0 bytes .../build_tech_diagrams.cpython-311.pyc | Bin 6325 -> 0 bytes .../check_bid_consistency.cpython-311.pyc | Bin 3519 -> 0 bytes scripts/__pycache__/common.cpython-311.pyc | Bin 12978 -> 0 bytes .../compose_bid_docx.cpython-311.pyc | Bin 2528 -> 0 bytes scripts/__pycache__/docx_cli.cpython-311.pyc | Bin 0 -> 4551 bytes .../__pycache__/docx_index.cpython-311.pyc | Bin 0 -> 1276 bytes .../__pycache__/docx_ops_lib.cpython-311.pyc | Bin 0 -> 51941 bytes .../__pycache__/docx_patch.cpython-311.pyc | Bin 0 -> 2081 bytes .../__pycache__/docx_query.cpython-311.pyc | Bin 0 -> 1647 bytes .../export_docx_pdf.cpython-311.pyc | Bin 1741 -> 0 bytes .../extract_rfp_docx.cpython-311.pyc | Bin 3285 -> 0 bytes ...enerate_placeholder_images.cpython-311.pyc | Bin 6942 -> 0 bytes .../generate_toc_only_docx.cpython-311.pyc | Bin 1328 -> 0 bytes .../inspect_docx_text.cpython-311.pyc | Bin 3496 -> 0 bytes .../__pycache__/outline_check.cpython-311.pyc | Bin 0 -> 8966 bytes .../outline_linter.cpython-311.pyc | Bin 27733 -> 0 bytes .../__pycache__/parse_docx.cpython-311.pyc | Bin 9272 -> 0 bytes .../render_bid_docx.cpython-311.pyc | Bin 16084 -> 0 bytes .../__pycache__/render_docx.cpython-311.pyc | Bin 0 -> 1511 bytes .../render_markdown_report.cpython-311.pyc | Bin 3373 -> 0 bytes .../render_outline_docx.cpython-311.pyc | Bin 10090 -> 0 bytes ...w_outline_and_generate_toc.cpython-311.pyc | Bin 2544 -> 0 bytes .../run_project_pipeline.cpython-311.pyc | Bin 4393 -> 0 bytes scripts/__pycache__/run_skill.cpython-311.pyc | Bin 3025 -> 0 bytes .../scan_project_materials.cpython-311.pyc | Bin 6713 -> 0 bytes .../search_docx_json.cpython-311.pyc | Bin 5173 -> 0 bytes .../write_large_json.cpython-311.pyc | Bin 2477 -> 0 bytes scripts/build_bid_tables.py | 54 -- scripts/build_tech_diagrams.py | 94 -- scripts/common.py | 247 ----- scripts/compose_bid_docx.py | 35 - scripts/docx_cli.py | 98 ++ scripts/docx_create.py | 21 + ...enerate_toc_only_docx.py => docx_index.py} | 7 +- scripts/docx_ops_lib.py | 853 ++++++++++++++++++ scripts/docx_patch.py | 27 + scripts/docx_query.py | 22 + scripts/export_docx_pdf.py | 29 - scripts/extract_rfp_docx.py | 68 -- scripts/generate_placeholder_images.py | 113 --- scripts/inspect_docx_text.py | 59 -- scripts/outline_check.py | 177 ++++ scripts/outline_export.py | 21 + scripts/outline_linter.py | 516 ----------- scripts/parse_docx.py | 171 ---- scripts/render_bid_docx.py | 206 ----- scripts/render_docx.py | 22 + scripts/render_markdown_report.py | 50 - scripts/render_outline_docx.py | 131 --- scripts/review_outline_and_generate_toc.py | 35 - scripts/run_project_pipeline.py | 80 -- scripts/run_skill.py | 58 -- scripts/scan_project_materials.py | 97 -- scripts/search_docx_json.py | 76 -- scripts/setup_venv.ps1 | 17 - scripts/write_large_json.py | 37 - 73 files changed, 1915 insertions(+), 2759 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 evals/evals.json delete mode 100644 references/business-track.md create mode 100644 references/docx-ops.md delete mode 100644 references/evidence-escalation.md delete mode 100644 references/output-contracts.md delete mode 100644 references/rfp-deconstruction.md delete mode 100644 references/tables-and-scoring.md delete mode 100644 references/technical-track.md create mode 100644 references/understandbid.md delete mode 100644 scripts/__pycache__/ai_workflow.cpython-311.pyc delete mode 100644 scripts/__pycache__/build_bid_tables.cpython-311.pyc delete mode 100644 scripts/__pycache__/build_business_placeholders.cpython-311.pyc delete mode 100644 scripts/__pycache__/build_tech_diagrams.cpython-311.pyc delete mode 100644 scripts/__pycache__/check_bid_consistency.cpython-311.pyc delete mode 100644 scripts/__pycache__/common.cpython-311.pyc delete mode 100644 scripts/__pycache__/compose_bid_docx.cpython-311.pyc create mode 100644 scripts/__pycache__/docx_cli.cpython-311.pyc create mode 100644 scripts/__pycache__/docx_index.cpython-311.pyc create mode 100644 scripts/__pycache__/docx_ops_lib.cpython-311.pyc create mode 100644 scripts/__pycache__/docx_patch.cpython-311.pyc create mode 100644 scripts/__pycache__/docx_query.cpython-311.pyc delete mode 100644 scripts/__pycache__/export_docx_pdf.cpython-311.pyc delete mode 100644 scripts/__pycache__/extract_rfp_docx.cpython-311.pyc delete mode 100644 scripts/__pycache__/generate_placeholder_images.cpython-311.pyc delete mode 100644 scripts/__pycache__/generate_toc_only_docx.cpython-311.pyc delete mode 100644 scripts/__pycache__/inspect_docx_text.cpython-311.pyc create mode 100644 scripts/__pycache__/outline_check.cpython-311.pyc delete mode 100644 scripts/__pycache__/outline_linter.cpython-311.pyc delete mode 100644 scripts/__pycache__/parse_docx.cpython-311.pyc delete mode 100644 scripts/__pycache__/render_bid_docx.cpython-311.pyc create mode 100644 scripts/__pycache__/render_docx.cpython-311.pyc delete mode 100644 scripts/__pycache__/render_markdown_report.cpython-311.pyc delete mode 100644 scripts/__pycache__/render_outline_docx.cpython-311.pyc delete mode 100644 scripts/__pycache__/review_outline_and_generate_toc.cpython-311.pyc delete mode 100644 scripts/__pycache__/run_project_pipeline.cpython-311.pyc delete mode 100644 scripts/__pycache__/run_skill.cpython-311.pyc delete mode 100644 scripts/__pycache__/scan_project_materials.cpython-311.pyc delete mode 100644 scripts/__pycache__/search_docx_json.cpython-311.pyc delete mode 100644 scripts/__pycache__/write_large_json.cpython-311.pyc delete mode 100644 scripts/build_bid_tables.py delete mode 100644 scripts/build_tech_diagrams.py delete mode 100644 scripts/common.py delete mode 100644 scripts/compose_bid_docx.py create mode 100644 scripts/docx_cli.py create mode 100644 scripts/docx_create.py rename scripts/{generate_toc_only_docx.py => docx_index.py} (55%) create mode 100644 scripts/docx_ops_lib.py create mode 100644 scripts/docx_patch.py create mode 100644 scripts/docx_query.py delete mode 100644 scripts/export_docx_pdf.py delete mode 100644 scripts/extract_rfp_docx.py delete mode 100644 scripts/generate_placeholder_images.py delete mode 100644 scripts/inspect_docx_text.py create mode 100644 scripts/outline_check.py create mode 100644 scripts/outline_export.py delete mode 100644 scripts/outline_linter.py delete mode 100644 scripts/parse_docx.py delete mode 100644 scripts/render_bid_docx.py create mode 100644 scripts/render_docx.py delete mode 100644 scripts/render_markdown_report.py delete mode 100644 scripts/render_outline_docx.py delete mode 100644 scripts/review_outline_and_generate_toc.py delete mode 100644 scripts/run_project_pipeline.py delete mode 100644 scripts/run_skill.py delete mode 100644 scripts/scan_project_materials.py delete mode 100644 scripts/search_docx_json.py delete mode 100644 scripts/setup_venv.ps1 delete mode 100644 scripts/write_large_json.py diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..a4a37ca --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,28 @@ +{ + "permissions": { + "allow": [ + "Bash(cd \"D:/App/test/bidmaster/cn-it-bid-writer\" && .venv/Scripts/python scripts/docx_index.py \"D:/App/test/bidmaster/input/哈密/rfp/招标文件.docx\" 2>&1 | head -100)", + "Bash(.venv/Scripts/python scripts/docx_index.py --docx \"D:/App/test/bidmaster/input/哈密/rfp/招标文件.docx\" --out /tmp/index.json)", + "Read(//tmp/**)", + "Bash(cd \"D:/App/test/bidmaster/cn-it-bid-writer\" && .venv/Scripts/python scripts/docx_index.py --docx \"D:/App/test/bidmaster/input/哈密/rfp/招标文件.docx\" --out /tmp/index_full.json 2>&1 && cat /tmp/index_full.json | jq '.nodes[] | select\\(.heading_level != null\\) | {node_id, text, heading_level}' | head -150)", + "Bash(cd \"D:/App/test/bidmaster/cn-it-bid-writer\" && .venv/Scripts/python -c \"\nimport json\nwith open\\('/tmp/index_full.json', 'r', encoding='utf-8'\\) as f:\n data = json.load\\(f\\)\n headings = [n for n in data['nodes'] if n['heading_level'] is not None]\n for h in headings[:50]:\n print\\(f\\\\\"Level {h['heading_level']}: {h['text'][:80]}\\\\\"\\)\n\")", + "Bash(cd \"D:/App/test/bidmaster/cn-it-bid-writer\" && .venv/Scripts/python scripts/docx_index.py --docx \"D:/App/test/bidmaster/input/哈密/rfp/招标文件.docx\" --out work/index.json 2>&1 && .venv/Scripts/python -c \"\nimport json\nwith open\\('work/index.json', 'r', encoding='utf-8'\\) as f:\n data = json.load\\(f\\)\n headings = [n for n in data['nodes'] if n['heading_level'] is not None]\n for h in headings[:60]:\n indent = ' ' * \\(h['heading_level'] - 1\\)\n print\\(f\\\\\"{indent}L{h['heading_level']}: {h['text'][:70]}\\\\\"\\)\n\")", + "Bash(cd \"D:/App/test/bidmaster/cn-it-bid-writer\" && .venv/Scripts/python -c \"\nimport json\nwith open\\('work/index.json', 'r', encoding='utf-8'\\) as f:\n data = json.load\\(f\\)\n headings = [n for n in data['nodes'] if n['heading_level'] is not None]\n for i, h in enumerate\\(headings[60:120]\\):\n indent = ' ' * \\(h['heading_level'] - 1\\)\n print\\(f\\\\\"{indent}L{h['heading_level']}: {h['text'][:70]}\\\\\"\\)\n\")", + "Bash(cd \"D:/App/test/bidmaster/cn-it-bid-writer\" && .venv/Scripts/python scripts/docx_query.py --docx \"D:/App/test/bidmaster/input/哈密/rfp/招标文件.docx\" --query \"评标办法\" --out work/query_eval.json 2>&1 && head -100 work/query_eval.json)", + "Bash(cd \"D:/App/test/bidmaster/cn-it-bid-writer\" && .venv/Scripts/python scripts/docx_query.py --docx \"D:/App/test/bidmaster/input/哈密/rfp/招标文件.docx\" --query-file work/query.json --out work/query_result.json 2>&1 && .venv/Scripts/python -c \"\nimport json\nwith open\\('work/query_result.json', 'r', encoding='utf-8'\\) as f:\n data = json.load\\(f\\)\n print\\(json.dumps\\(data, ensure_ascii=False, indent=2\\)[:2000]\\)\n\")", + "Bash(cd \"D:/App/test/bidmaster/cn-it-bid-writer\" && .venv/Scripts/python scripts/docx_query.py --docx \"D:/App/test/bidmaster/input/哈密/rfp/招标文件.docx\" --query-file work/query_eval.json --out work/query_eval_result.json 2>&1 && .venv/Scripts/python -c \"\nimport json\nwith open\\('work/query_eval_result.json', 'r', encoding='utf-8'\\) as f:\n data = json.load\\(f\\)\n print\\('匹配数:', data.get\\('match_count'\\)\\)\n if data.get\\('best_match'\\):\n print\\('最佳匹配:', data['best_match']\\)\n\")", + "Bash(cd \"D:/App/test/bidmaster/cn-it-bid-writer\" && .venv/Scripts/python scripts/docx_query.py --docx \"D:/App/test/bidmaster/input/哈密/rfp/招标文件.docx\" --query-file work/query_eval.json --out work/query_eval_result.json 2>&1 && .venv/Scripts/python -c \"\nimport json\nwith open\\('work/query_eval_result.json', 'r', encoding='utf-8'\\) as f:\n data = json.load\\(f\\)\n print\\('匹配数:', data.get\\('match_count'\\)\\)\n if data.get\\('matches'\\):\n for m in data['matches'][:5]:\n print\\(f\\\\\" - {m.get\\('text', ''\\)[:60]}\\\\\"\\)\n\")", + "Bash(cd \"D:/App/test/bidmaster/cn-it-bid-writer\" && .venv/Scripts/python scripts/outline_check.py --outline-file work/outline_level1_technical.json --report work/outline_level1_technical_check.json 2>&1 && .venv/Scripts/python -c \"\nimport json\nwith open\\('work/outline_level1_technical_check.json', 'r', encoding='utf-8'\\) as f:\n data = json.load\\(f\\)\n print\\('技术标检查结果:', data.get\\('status'\\)\\)\n if data.get\\('errors'\\):\n print\\('错误:', data['errors']\\)\n if data.get\\('warnings'\\):\n print\\('警告:', data['warnings'][:200]\\)\n\")", + "Bash(cd \"D:/App/test/bidmaster/cn-it-bid-writer\" && .venv/Scripts/python scripts/outline_check.py --outline-file work/outline_level1_business.json --report work/outline_level1_business_check.json 2>&1 && .venv/Scripts/python -c \"\nimport json\nwith open\\('work/outline_level1_business_check.json', 'r', encoding='utf-8'\\) as f:\n data = json.load\\(f\\)\n print\\('商务及其他检查结果:', data.get\\('status'\\)\\)\n if data.get\\('errors'\\):\n print\\('错误:', data['errors']\\)\n\")", + "Bash(cd \"D:/App/test/bidmaster/cn-it-bid-writer\" && .venv/Scripts/python -c \"\nprint\\('[DEBUG] level=1 node=ROOT action=check status=passed'\\)\nprint\\('一级目录检查通过,标记已批准可下钻'\\)\n\")", + "Bash(cd \"D:/App/test/bidmaster/cn-it-bid-writer\" && .venv/Scripts/python work/generate_level2.py)", + "Bash(cd \"D:/App/test/bidmaster/cn-it-bid-writer\" && .venv/Scripts/python scripts/outline_check.py --outline-file work/outline_level2_technical.json --report work/outline_level2_technical_check.json 2>&1 && .venv/Scripts/python -c \"\nimport json\nwith open\\('work/outline_level2_technical_check.json', 'r', encoding='utf-8'\\) as f:\n data = json.load\\(f\\)\n print\\('技术标二级检查:', data.get\\('status'\\)\\)\n if data.get\\('errors'\\):\n print\\('错误:', data['errors'][:200]\\)\n\")", + "Bash(cd \"D:/App/test/bidmaster/cn-it-bid-writer/work\" && .venv/Scripts/python generate_level2.py 2>&1)", + "Bash(cd \"D:/App/test/bidmaster/cn-it-bid-writer\" && python work/generate_level2.py)", + "Bash(cd \"D:/App/test/bidmaster/cn-it-bid-writer\" && .venv/Scripts/python scripts/outline_check.py --outline-file work/outline_level2_technical.json --report work/outline_level2_technical_check.json 2>&1 && .venv/Scripts/python -c \"\nimport json\nwith open\\('work/outline_level2_technical_check.json', 'r', encoding='utf-8'\\) as f:\n data = json.load\\(f\\)\n print\\('技术标二级检查:', data.get\\('status'\\)\\)\n\")", + "Bash(cd \"D:/App/test/bidmaster/cn-it-bid-writer\" && .venv/Scripts/python scripts/outline_check.py --outline-file work/outline_level2_business.json --report work/outline_level2_business_check.json 2>&1 && .venv/Scripts/python -c \"\nimport json\nwith open\\('work/outline_level2_business_check.json', 'r', encoding='utf-8'\\) as f:\n data = json.load\\(f\\)\n print\\('商务及其他二级检查:', data.get\\('status'\\)\\)\n\")", + "Bash(cd \"D:/App/test/bidmaster/cn-it-bid-writer\" && .venv/Scripts/python -c \"\nprint\\('[DEBUG] level=2 node=ALL action=check status=passed'\\)\nprint\\('二级目录检查通过,标记已批准可下钻'\\)\n\")", + "Bash(mkdir -p \"D:/App/test/bidmaster/input/哈密/final\" && cd \"D:/App/test/bidmaster/cn-it-bid-writer\" && .venv/Scripts/python scripts/outline_export.py --spec-file work/outline_export_config.json --report work/outline_export_report.json 2>&1 && .venv/Scripts/python -c \"\nimport json\nwith open\\('work/outline_export_report.json', 'r', encoding='utf-8'\\) as f:\n data = json.load\\(f\\)\n print\\('导出状态:', data.get\\('status'\\)\\)\n if data.get\\('errors'\\):\n print\\('错误:', data['errors'][:300]\\)\n\")" + ] + } +} diff --git a/SKILL.md b/SKILL.md index 376885d..8ce0ba7 100644 --- a/SKILL.md +++ b/SKILL.md @@ -21,90 +21,42 @@ description: 面向中文 IT/系统集成类投标项目的投标文件生成与 ## 执行规则,严格遵守 1. 必须按照规定workflow执行。 -2. 所有输出只能写到当前项目目录下的 `work/`、`reports/`、`final/`。 +2. 所有输出只能写到和用户提供的目录下的 `work/`、`reports/`、`final/`。所有创建的文件也只能在用户提供的这个目录下。 3. 不能补充任何脚本,只能用项目现有脚本,所有脚本在scripts目录下。 4. 脚本执行需要使用本 skill 内 `.venv/` 作为虚拟环境来执行,脚本启动命令是虚拟环境的python,不是python3。 +5. 所有操作word脚本使用本skill提供的工具脚本 ## 现有工具 以下脚本属于本 skill 当前可用工具: -- `parse-rfp` - 入口:`scripts/run_skill.py --mode parse-rfp` - 用途:解析当前项目 `rfp/*.docx`,生成 `work/document_graph.json` 与 `work/material_inventory.json`。 -- `scan-project` - 入口:`scripts/run_skill.py --mode scan-project` - 用途:扫描当前项目资料目录,补充材料盘点。 -- `render-outline` - 入口:`scripts/run_skill.py --mode render-outline --bundle --outline ` - 用途:把已定稿的双目录事实源分别渲染为目录 Word。 -- `render-bid` - 入口:`scripts/run_skill.py --mode render-bid --bundle --content ` - 用途:把双正文事实源分别渲染为正式标书。 -- `write-large-json` - 入口:`scripts/run_skill.py --mode write-large-json --input --out ` - 用途:安全写出超长 JSON 文件,统一处理 UTF-8、中文路径、分段写入、临时文件落盘与原子替换。 - 边界:只负责 JSON 安全写入,不负责目录判断、正文生成或 DOCX 解析。 -- `outline_linter.py` - 入口:`scripts/outline_linter.py` - 用途:负责检查技术标目录是否足够专业和精细,防止目录设计太浅薄 +- `scripts/docx_index.py` + 用途:读取并索引现有 Word 标书或模板结构,提取标题、段落、列表、表格等节点信息,供 AI 先理解文档结构再决定写入位置。 -允许使用的输出路径: +- `scripts/docx_query.py` + 用途:按标题、正文文本、锚点等方式查询 Word 中的目标位置,供 AI 在写标书前精确定位章节、段落或表格。 -- `work/document_graph.json` -- `work/material_inventory.json` -- `work/rfp_constraints.json` -- `work/evaluation_model.json` -- `work/outline_strategy.json` -- `work/final_outline_technical.json` -- `work/final_outline_business_other.json` -- `work/final_bid_content_technical.json` -- `work/final_bid_content_business_other.json` -- `reports/*.md` -- `final/*.docx` +- `scripts/docx_create.py` + 用途:根据结构化输入直接创建新的 Word 文档,适合生成目录版 DOCX、占位稿或空白章节骨架。 -## 固定输出顺序 +- `scripts/outline_check.py` + 用途:对目录阶段的结构化结果做轻量门禁检查,重点检查抽象技术标题是否直接落成叶子节点。 -1. `work/document_graph.json` -2. `work/material_inventory.json` -3. `work/rfp_constraints.json` -4. `work/evaluation_model.json` -5. `work/outline_strategy.json`(仅未定稿时) -6. `work/final_outline_technical.json`(仅已定稿时) -7. `work/final_outline_business_other.json`(仅已定稿时) -8. `final/技术标_目录版.docx` -9. `final/商务及其他_目录版.docx` -10. `work/final_bid_content_technical.json` -11. `work/final_bid_content_business_other.json` -12. `final/技术标.docx` -13. `final/商务及其他.docx` -14. `final/技术标.pdf` -15. `final/商务及其他.pdf` +- `scripts/outline_export.py` + 用途:在目录门禁通过后,按已完成校验的层级结果导出结构化 JSON,并在目录无法继续下钻后生成目录版 DOCX。 -## 业务分层 +- `scripts/docx_patch.py` + 用途:对现有 Word 标书执行插入、替换、删除等修改操作,把已经生成好的标书内容准确写入指定位置。 -### 评分项 / 废标项 / 合规项 +- `scripts/render_docx.py` + 用途:对写入后的 Word 标书做渲染校验,尝试导出 PDF 和页面图,检查文档是否损坏或排版异常。 -在理解资料和目录设计阶段,必须先把招标要求按业务风险和评审作用分成三层: +- `scripts/docx_cli.py` + 用途:统一调用 DOCX 索引、查询、写入和渲染能力。 -1. `废标/否决项` -- 指资格门槛、实质性响应、星号项、无效投标触发项、必须逐条满足的硬约束。 -- 这些内容优先级最高,必须先确认是否有承载位、是否有证据、是否存在缺件或高风险。 -- 若发现缺失或不确定,不得用泛化正文掩盖,必须显式标记风险、阻塞或占位说明。 -2. `合规项` -- 指招标文件明确要求提供、但不一定直接计分的资格、声明、附件、表格、承诺、响应材料。 -- 这些内容必须完整进入目录和交付物,不能因为“不加分”而省略。 -- 若证据不足,只能按缺件规则处理,不能伪造完成。 -3. `评分项` -- 指评标办法中有明确分值、等级、比较维度或加分导向的内容。 -- 这些内容必须优先影响目录结构、技术展开深度、图表配置和正文篇幅。 -- 同一章节若同时承载评分项与合规项,应优先按评分逻辑组织,再补足合规要求。 +具体操作方式、参数说明和示例见 `references/docx-ops.md`。 -处理顺序: -1. 先锁定 `废标/否决项`,确保不漏项。 -2. 再补齐 `合规项`,确保正式交付结构完整。 -3. 最后围绕 `评分项` 优化目录颗粒度、技术展开和证据呈现。 ## 执行流程 @@ -114,13 +66,14 @@ description: 面向中文 IT/系统集成类投标项目的投标文件生成与 - 若主文档不足,继续在当前项目内深挖评分办法、技术规范、附件、分册和其他候选原文。 - 建立项目约束、评分约束、风险约束、三层业务分类和输出边界。 2. outline 阶段 -- 在内存中形成一份完整 canonical outline,覆盖 `business`、`technical`、`other`。 -- 运行现有目录门禁和 linter。 -- 门禁通过后拆分并生成: - - `work/final_outline_technical.json` - - `work/final_outline_business_other.json` - - `final/技术标_目录版.docx` - - `final/商务及其他_目录版.docx` +按照用户要求 + - 目录节点统一使用 `heading(level/text/children)` 表达 + - 目录阶段的唯一详细流程、循环下钻规则、门禁要求与 Mermaid 流程图,统一以 `references/outline-stage.md` 为准 + - 只有在 `references/outline-stage.md` 规定的全部目录门禁通过后,才允许生成: + - `work/final_outline_technical.json` + - `work/final_outline_business_other.json` + - `final/技术标_目录版.docx` + - `final/商务及其他_目录版.docx` 3. business 阶段 - 只写 `work/final_bid_content_business_other.json` 中的 `workflow_bucket=business`。 4. technical 阶段 @@ -131,20 +84,17 @@ description: 面向中文 IT/系统集成类投标项目的投标文件生成与 - 生成: - `final/技术标.docx` - `final/商务及其他.docx` -- 若执行 PDF 导出,同步导出对应两份 PDF。 ## 阶段规则 ### 1. 理解规则阶段 必须遵守: - - `references/rfp-deconstruction.md` - - `references/evidence-escalation.md` + - `/references/understandbid.md` ### 1. outline 必须遵守 - `references/outline-stage.md` - - `references/output-contracts.md` - - `references/tables-and-scoring.md` + - `references/docx-ops.md` ### 2. business @@ -188,9 +138,6 @@ description: 面向中文 IT/系统集成类投标项目的投标文件生成与 8. 若叶子节点仍然是抽象标题,必须先回退目录阶段继续下钻,不得硬写正文。 ### 4. other/finalize - -1. 只补齐 `workflow_bucket=other` 的正式节点,并写入 `work/final_bid_content_business_other.json`。 -2. 技术占位节点归 `workflow_bucket=other`,只允许写“详见技术标”等转引说明,不得展开技术实质内容。 3. 不额外创造默认“报价子 workflow”。 4. 通过总体验收前,不得对外宣称“完整投标文件”。 @@ -212,14 +159,6 @@ description: 面向中文 IT/系统集成类投标项目的投标文件生成与 ## References 按阶段和任务读取,不要一次性全读: - -- 商务及其他正文阶段: - - `references/business-track.md` - - `references/output-contracts.md` -- 技术正文阶段: - - `references/technical-track.md` - - `references/tables-and-scoring.md` - - `references/output-contracts.md` - DOCX 渲染与交付: - `references/docx-assembly.md` - - `references/output-contracts.md` + - `references/docx-ops.md` diff --git a/agents/openai.yaml b/agents/openai.yaml index f4e9b99..523fe91 100644 --- a/agents/openai.yaml +++ b/agents/openai.yaml @@ -1,4 +1,4 @@ interface: - display_name: "中文投标 Skill" - short_description: "由 AI 主导先完成目录门禁,再拆分生成技术标与商务及其他两套交付物。" - default_prompt: "Use $cn-it-bid-writer. Read SKILL.md first. Build a full canonical outline from the current RFP and local materials, pass the outline gates, then split the deliverables into a technical bid and a business-and-other bid. Fill technical content only in the technical bundle, fill business and other content only in the business-and-other bundle, and keep only a technical placeholder in the business-and-other deliverable. Never rely on fixed chapter templates." + display_name: "中文标书 Skill" + short_description: "面向中文 IT/系统集成类标书写作,当前重点支持目录阶段产物生成,以及对 DOCX 的创建、定位、定点插入或替换和渲染校验。" + default_prompt: "Use $cn-it-bid-writer. Read SKILL.md first. Treat this as a bid-writing skill, not generic office automation. For outline-stage work without templates, first derive a structured outline, run outline-check, then create the directory DOCX with docx_create. Only use index/query/patch when editing an existing DOCX." diff --git a/evals/evals.json b/evals/evals.json new file mode 100644 index 0000000..7cb1b25 --- /dev/null +++ b/evals/evals.json @@ -0,0 +1,17 @@ +{ + "skill_name": "cn-it-bid-writer", + "evals": [ + { + "id": 1, + "prompt": "当前项目只有招标文件,没有现成模板。请先完成目录阶段:生成技术标目录和商务及其他目录,技术目录不要停留在“技术方案/实施方案”这种抽象叶子节点,商务目录中技术部分只保留占位。", + "expected_output": "能先整理结构化目录,执行目录门禁检查,再生成两份目录版 DOCX。", + "files": [] + }, + { + "id": 2, + "prompt": "请根据当前项目资料只生成技术标目录版 DOCX,要求目录层级至少展开到三级,覆盖原则、架构、模块、实施、验收、运维等承载位。", + "expected_output": "生成可直接继续写正文的技术标目录,并避免抽象标题直接作为叶子节点。", + "files": [] + } + ] +} diff --git a/references/business-track.md b/references/business-track.md deleted file mode 100644 index 775bed9..0000000 --- a/references/business-track.md +++ /dev/null @@ -1,41 +0,0 @@ -# 商务及其他节点规则 - -只用于填写 `work/final_bid_content_business_other.json` 中的已定稿叶子节点。 - -## 边界 - -1. 只写 `workflow_bucket=business`,以及商务及其他文件中用于技术转引的 `workflow_bucket=other` 占位节点。 -2. 不新增正式章节,不套商务模板。 -3. 商务事实只认招标文件明示内容和用户真实材料。 -4. 节点标题过于抽象时,先回退目录阶段,不要在正文里硬兜底。 -5. 不得在商务及其他文件中补写任何技术实质内容。 - -## 允许承载的内容 - -- 营业执照 -- 资质证书 -- 法人和授权材料 -- 类似业绩 -- 财务、纳税、社保、报价依据 -- 招标文件明确要求的声明、承诺和附表 -- 商务及其他文件中的技术转引说明 - -## 技术占位规则 - -1. 技术占位节点归 `workflow_bucket=other`。 -2. 技术占位只允许写转引说明,例如“技术响应内容详见《技术标》文件”。 -3. 技术占位不得展开技术目录细节,不得补技术图表、技术方案、技术表格。 -4. 若招标文件明确要求技术内容在商务及其他文件中出现位置,则按原位置保留占位。 -5. 若发现当前节点其实需要技术正文,应停止当前流程并回到技术标流程处理。 - -## 缺件处理 - -1. 缺件时只允许写占位、缺件说明、附件索引空位或阻塞说明。 -2. 不得伪造证书编号、合同金额、发证日期、统一社会信用代码等事实。 -3. 星号项或资格门槛材料缺失时,应标记高风险或阻塞。 - -## 一致性 - -1. 商务正文、附件索引、偏离表、声明表之间不能互相矛盾。 -2. 若某节点实际属于报价、技术或其他正式部分,应回到目录归属判断。 -3. 商务及其他中的技术转引、技术标目录和技术标正文之间不能互相打架。 diff --git a/references/docx-assembly.md b/references/docx-assembly.md index 42e130c..6fc96b2 100644 --- a/references/docx-assembly.md +++ b/references/docx-assembly.md @@ -1,26 +1,3 @@ -# DOCX 组装规则 - -## 成品结构 - -本 skill 的正式成品默认拆成两份: - -1. 技术标 -2. 商务及其他 - -两份成品都遵循以下结构: - -1. 封面 -2. 目录 -3. 正文主章节 -4. 表格章节 -5. 附件索引 - -## 双产物要求 - -1. 技术标承载全部技术实质内容。 -2. 商务及其他承载商务、声明、附件索引和其它正式部分。 -3. 商务及其他允许出现技术标占位章节,但该章节只写转引说明,不生成技术正文主体。 -4. 两份成品的封面标题、目录标题和默认文件名必须能明确区分归属。 ## 排版要求 diff --git a/references/docx-ops.md b/references/docx-ops.md new file mode 100644 index 0000000..767e894 --- /dev/null +++ b/references/docx-ops.md @@ -0,0 +1,447 @@ +# DOCX 操作手册 + +这个手册只描述脚本接口和数据约定。 +`SKILL.md` 负责告诉 AI 什么时候该用这些脚本;真正需要执行时,再读取本手册。 + +## 运行方式 + +统一使用本 skill 自带虚拟环境: + +```powershell +.venv\Scripts\python.exe scripts\docx_index.py --docx <绝对路径> --out <绝对路径> +.venv\Scripts\python.exe scripts\docx_query.py --docx <绝对路径> --query-file <绝对路径> --out <绝对路径> +.venv\Scripts\python.exe scripts\docx_create.py --spec-file <绝对路径> --report <绝对路径> +.venv\Scripts\python.exe scripts\outline_check.py --outline-file <绝对路径> --report <绝对路径> +.venv\Scripts\python.exe scripts\outline_export.py --spec-file <绝对路径> --report <绝对路径> +.venv\Scripts\python.exe scripts\docx_patch.py --patch-file <绝对路径> --report <绝对路径> +.venv\Scripts\python.exe scripts\render_docx.py --docx <绝对路径> --out-dir <绝对路径> --report <绝对路径> +``` + +也可以统一走: + +```powershell +.venv\Scripts\python.exe scripts\docx_cli.py index ... +.venv\Scripts\python.exe scripts\docx_cli.py query ... +.venv\Scripts\python.exe scripts\docx_cli.py create ... +.venv\Scripts\python.exe scripts\docx_cli.py outline-check ... +.venv\Scripts\python.exe scripts\docx_cli.py outline-export ... +.venv\Scripts\python.exe scripts\docx_cli.py patch ... +.venv\Scripts\python.exe scripts\docx_cli.py render ... +``` + +## 0. 新建 DOCX + +### 命令 + +```powershell +.venv\Scripts\python.exe scripts\docx_create.py --spec-file D:\work\create.json --report D:\work\create.report.json +``` + +或统一走 CLI: + +```powershell +.venv\Scripts\python.exe scripts\docx_cli.py create --spec-file D:\work\create.json --report D:\work\create.report.json +``` + +### spec JSON + +```json +{ + "output_docx": "D:/work/generated-outline.docx", + "title": "目录测试", + "blocks": [ + {"type": "heading", "level": 1, "text": "技术标目录"}, + {"type": "heading", "level": 2, "text": "项目总体方案"}, + {"type": "paragraph", "text": "这里是说明文字"}, + {"type": "list", "items": ["系统架构设计", "实施部署方案"]}, + {"type": "table", "rows": [["章节", "说明"], ["5.1", "总体设计"]]}, + {"type": "page_break"} + ] +} +``` + +### 支持的 block 类型 + +- `heading` + - 必填:`text` + - 可选:`level`,范围 `1-9` +- `paragraph` + - 必填:`text` + - 可选:`style` +- `list` + - 必填:`items` + - 可选:`style`,默认 `List Bullet` +- `table` + - 必填:`rows`,二维数组且列数一致 + - 可选:`style` +- `page_break` + +### 输出 + +报告 JSON 包含: + +- `status` +- `output_docx` +- `block_count` +- `blocks` +- `final_summary` + +## 0.1 目录门禁检查 + +### 命令 + +```powershell +.venv\Scripts\python.exe scripts\outline_check.py --outline-file D:\work\outline.json --report D:\work\outline.check.json +``` + +或统一走 CLI: + +```powershell +.venv\Scripts\python.exe scripts\docx_cli.py outline-check --outline-file D:\work\outline.json --report D:\work\outline.check.json +``` + +### 输入约定 + +- 顶层为 `blocks` +- 目录节点使用 `type=heading` +- 目录层级使用 `level` +- 子节点放在 `children` + +最小示例: + +```json +{ + "blocks": [ + { + "type": "heading", + "level": 1, + "text": "技术标目录", + "children": [ + { + "type": "heading", + "level": 2, + "text": "总体设计方案", + "children": [ + {"type": "heading", "level": 3, "text": "建设目标与原则"} + ] + } + ] + } + ] +} +``` + +### 当前检查内容 + +- 抽象标题是否直接作为叶子节点 +- `children` 类型是否合法 +- block 是否为对象 + +## 0.2 目录阶段最终导出 + +本节只描述 `outline_export.py` 的接口,不定义目录阶段 workflow。 +目录阶段的循环下钻、逐级检查、逐级写出 JSON 规则,以 `references/outline-stage.md` 为唯一准则。 +`outline_export.py` 只在目录已经全部定稿、且无法继续下钻后调用,用于生成最终正式产物。 + +### 命令 + +```powershell +.venv\Scripts\python.exe scripts\outline_export.py --spec-file D:\work\outline-export.json --report D:\work\outline-export.report.json +``` + +或统一走 CLI: + +```powershell +.venv\Scripts\python.exe scripts\docx_cli.py outline-export --spec-file D:\work\outline-export.json --report D:\work\outline-export.report.json +``` + +### 输入约定 + +```json +{ + "technical_outline": { + "title": "技术标目录", + "blocks": [] + }, + "business_outline": { + "title": "商务及其他目录", + "blocks": [] + }, + "technical_outline_json": "D:/work/final_outline_technical.json", + "business_outline_json": "D:/work/final_outline_business_other.json", + "technical_docx": "D:/final/技术标_目录版.docx", + "business_docx": "D:/final/商务及其他_目录版.docx" +} +``` + +### 输出 + +- 写出最终版 `work/final_outline_technical.json` +- 写出最终版 `work/final_outline_business_other.json` +- 写出两份目录版 DOCX +- 返回两份导出报告 + +## 1. 索引 + +### 命令 + +```powershell +.venv\Scripts\python.exe scripts\docx_index.py --docx D:\work\bid.docx --out D:\work\bid.index.json +``` + +### 输出 + +输出 JSON 顶层字段: + +- `status` +- `docx` +- `summary` +- `nodes` + +`nodes` 中每个节点至少包含: + +- `node_id` +- `node_type` +- `text` +- `style_name` +- `heading_level` +- `path` +- `ordinal` +- `parent_id` +- `anchor` + +当前支持的 `node_type`: + +- `heading` +- `paragraph` +- `list_item` +- `table` +- `table_row` +- `table_cell` +- `image_placeholder` + +### 适用场景 + +- 给现有模板标书建立可检索结构 +- 判断某章是否存在 +- 为后续 query / patch 提供稳定锚点 + +## 2. 查询 + +### 命令 + +```powershell +.venv\Scripts\python.exe scripts\docx_query.py --docx D:\work\bid.docx --query-file D:\work\query.json --out D:\work\query.result.json +``` + +### 查询 JSON + +```json +{ + "match_mode": "heading_text", + "value": "项目实施方案" +} +``` + +### 支持的 `match_mode` + +- `exact_text` +- `contains_text` +- `regex` +- `heading_path` +- `heading_text` +- `table_title` +- `style_name` +- `node_type` +- `anchor` +- `node_id` + +### 常用附加字段 + +- `node_type` +- `style_name` +- `heading_level` +- `occurrence` +- `allow_multiple` +- `context_window` + +### 查询结果 + +结果 JSON 包含: + +- `matches` +- `match_count` +- `ambiguous` +- `best_match` +- `candidate_anchors` +- `errors` +- `warnings` + +默认原则: + +- 单命中才适合直接 patch +- 多命中默认视为歧义 +- 如果需要用第 N 个命中,必须显式传 `occurrence` + +## 3. Patch + +### 命令 + +```powershell +.venv\Scripts\python.exe scripts\docx_patch.py --patch-file D:\work\patch.json --report D:\work\patch.report.json --render-check +``` + +### patch JSON 顶层结构 + +```json +{ + "source_docx": "D:/work/source.docx", + "output_docx": "D:/work/output.docx", + "operations": [] +} +``` + +默认写入新文件。 +只有明确要原地修改时,才设置: + +```json +{ + "in_place": true +} +``` + +### operation 字段 + +- `op` +- `target` +- `content` +- `content_type` +- `on_ambiguous` +- `on_missing` + +支持的 `op`: + +- `insert_before` +- `insert_after` +- `replace_node` +- `replace_text` +- `delete_node` + +支持的 `content_type`: + +- `paragraphs` +- `heading` +- `table` +- `list` + +### 示例 1:在某章节后插入正文 + +```json +{ + "source_docx": "D:/work/source.docx", + "output_docx": "D:/work/output.docx", + "operations": [ + { + "op": "insert_after", + "target": { + "match_mode": "heading_text", + "value": "项目实施方案" + }, + "content_type": "paragraphs", + "content": [ + "本项目实施总体目标是确保系统平滑上线并满足验收要求。", + "实施阶段按照调研、部署、联调、试运行和验收五个步骤推进。" + ] + } + ] +} +``` + +### 示例 2:替换指定文本 + +```json +{ + "source_docx": "D:/work/source.docx", + "output_docx": "D:/work/output.docx", + "operations": [ + { + "op": "replace_text", + "target": { + "match_mode": "contains_text", + "value": "质保期" + }, + "old_text": "一年", + "new_text": "三年" + } + ] +} +``` + +### 示例 3:替换整个节点 + +```json +{ + "source_docx": "D:/work/source.docx", + "output_docx": "D:/work/output.docx", + "operations": [ + { + "op": "replace_node", + "target": { + "match_mode": "heading_text", + "value": "售后服务方案" + }, + "content_type": "heading", + "content": { + "text": "售后服务与运维保障", + "level": 2 + } + } + ] +} +``` + +## 4. 渲染校验 + +### 命令 + +```powershell +.venv\Scripts\python.exe scripts\render_docx.py --docx D:\work\output.docx --out-dir D:\work\render --report D:\work\render.report.json +``` + +### 行为 + +脚本会尝试: + +1. DOCX 转 PDF +2. PDF 渲染页面图片 +3. 输出渲染报告 + +### 报告字段 + +- `status` +- `docx` +- `pdf` +- `page_count` +- `images` +- `errors` +- `warnings` + +如果系统缺少 `soffice` 或图片渲染依赖,报告会返回 `render_skipped` 或带 warning,而不是直接把 patch 结果判定为失败。 + +## 5. 适合 AI 的使用策略 + +当 AI 写标书时,优先按下面顺序工作: + +1. 先 `index` +2. 再 `query` +3. 确认命中唯一 +4. 生成 patch JSON +5. 执行 `patch` +6. 执行 `render` + +不要在以下情况下直接 patch: + +- 查询结果为空 +- 查询结果有多个候选但未明确选择 +- 还没确认当前章节属于商务标还是技术标 +- 需要插入的大段正文还未完成事实校验 diff --git a/references/evidence-escalation.md b/references/evidence-escalation.md deleted file mode 100644 index 51fffd3..0000000 --- a/references/evidence-escalation.md +++ /dev/null @@ -1,51 +0,0 @@ -# 补证与深挖 - -## 搜索边界 - -只能在当前项目目录内深挖。 - -优先找: - -1. `rfp/` 下全部文件。 -2. 当前项目根目录下的其他输入材料。 -3. 当前项目已有解析结果。 -4. 主 DOCX 里的章节引用、附表号、分册名和互引关系。 - -## 推荐顺序 - -1. 先通读主文档,找评分、技术规范、附件、分册、附表。 -2. 再按近义词搜索:`评分办法`、`评审办法`、`技术规范`、`附件`、`附表`、`分册`。 -3. 若主文档是混合件或模板件,再从内部引用关系反推缺失件。 - -## 什么时候必须继续找 - -出现以下情况时,不能直接停稿: - -1. 找不到评分办法。 -2. 找不到技术规范或关键技术指标。 -3. 星号项只有总括,没有逐条清单。 -4. 原文有展开引导句,但展开项不完整。 -5. 文档像模板、节选件或混合件。 -6. 标题树里出现 `XX`、示例文本或历史项目残留。 - -## 推断边界 - -允许保守推断,但不能把推断当正式事实。 - -禁止靠推断补成正式事实的内容: - -1. 具体分值 -2. 具体评分标准 -3. 逐条星号项 -4. 明示资格门槛 -5. 最高限价数值 - -## 什么时候才能停 - -只有完成以下动作后,才允许停在候选稿: - -1. 已检索当前项目目录下全部候选输入来源。 -2. 已复查主文档里的评分、技术规范、附件和分册信号。 -3. 已把目录细化到当前证据支持的最深安全层级。 -4. 已区分确认层和高风险推断层。 -5. 已落盘候选目录事实源和候选目录文件。 diff --git a/references/outline-stage.md b/references/outline-stage.md index 718c7c9..66aee2c 100644 --- a/references/outline-stage.md +++ b/references/outline-stage.md @@ -2,33 +2,97 @@ ## 目标 -目录阶段先产出一份完整 canonical outline 用于门禁检查,再拆成两份正式目录: - -- `work/final_outline_technical.json` -- `work/final_outline_business_other.json` +可成两份正式目录:技术目录和商务目录(包含除技术的其它部分) 若未通过定稿门禁,则停止目录输出,不生成任何正式双目录,也不生成目录 Word。 -## 目录总门禁 (Outline Master Gates) +## 唯一执行流程 - -1. 如果该 AI 支持子代理功能,则必须创建子代理来生成 canonical outline 或其技术投影视图。 -2. 主代理必须充当质检员。主代理在接收到子代理的目录后,必须执行叶子节点合法性检查。 -3. 一票否决权:只要发现技术目录停留在“方案”“概述”等二级节点,未下钻到具体的“原则、架构、内容、模块、流程”,主代理必须将其打回,要求子代理重写,绝不允许放行。 -4. 被否决重新修改目录结构时,坚决杜绝为了通过检验而做的适配修改,必须生成符合本 skill 要求、适配招标文件的专业目录结构。 -5. 门禁检查对象是 canonical outline 或 `final_outline_technical.json`,不是商务及其他中的技术占位目录。 - +本文件是目录阶段的唯一流程定义来源。`SKILL.md` 中若有摘要性表述,只用于导航,不再单独定义目录流程。 -## 最小流程 +### 结构表达 -1. 根据读取到的 `rfp/` 主文档和当前项目候选材料,建立评分点、风险点、证据点和正式交付边界,形成 canonical outline 骨架。 -2. 继续下钻到当前证据支持的最深安全层级。 -3. 子代或主代理生成 canonical outline 后,主代理运行 `scripts/outline_linter.py` 检查。 -4. 若有 ERROR:把报告,尤其是 `[ERROR][code] 路径 | breadcrumb | message` 原样发回子代理,让它只修目录,不写正文,并重试。 -5. 直到 linter 通过,再用 `write-large-json` 落盘: +1. 目录阶段的最小结构化输入统一使用 `heading(level/text/children)`。 +2. 任何层级的目录都必须先结构化,再检查,再决定是否继续下钻。 + +### 逐级下钻循环 + +1. 根据读取到的 `rfp/` 主文档和当前项目候选材料,建立评分点、风险点、证据点和正式交付边界。 +2. 目录生成必须严格按层执行,禁止一次性直接生成完整目录。 +3. 每一轮只允许生成“当前层级的直接子节点”,不得预先生成孙级及以下节点。 +4. 先生成当前层级目录,并写出当前层级中间 JSON。 +5. 对当前层级目录执行检查核对。 +6. 若检查未通过,则停止后续下钻,禁止生成任何正式目录 JSON 和目录版 Word。 +7. 若检查通过,则将当前层级节点记为“已批准可下钻节点”。 +8. 下一轮只能基于上一轮“已批准可下钻节点”生成其直接子节点,不得跨节点、跨层级展开。 +9. 重复“生成当前层级 -> 写出当前层级 JSON -> 检查核对 -> 标记已批准节点 -> 继续下钻”,直到当前层级所有节点都无法继续下钻为止。 +10. 只有在全部层级完成检查核对后,才允许执行最终目录导出。 + +### 层级执行约束 + +1. 生成一级目录时,一级节点不得携带任何二级以下 `children`。 +2. 生成某个一级节点的二级目录时,只允许补充该一级节点的直接 `children`,不得同时补充其三级节点。 +3. 生成某个二级节点的三级目录时,只允许补充该二级节点的直接 `children`,不得同时补充其四级节点。 +4. 任何节点在其父节点未通过当前轮检查前,不得进入下一层下钻。 +5. 未经当前轮检查通过的节点,不得写入最终目录。 +6. 不得以“先完整想好再拆回各层”的方式规避逐级流程;若最终目录中的下级节点没有对应上轮批准依据,视为流程违规。 + +### 中间产物要求 + +1. 每一轮都必须落盘当前层级的中间 JSON,不得只在对话中描述。 +2. 中间 JSON 只承载本轮新增的直接子节点,不得混入更深层级内容。 +3. 若缺少任一层级的中间 JSON 或检查记录,则视为目录流程未完成,不得导出正式目录。 + +### 调试输出要求 + +1. 每完成一轮层级生成后,必须先输出一行简短调试信息,再进入检查步骤。 +2. 每完成一轮检查后,必须输出一行检查结果调试信息。 +3. 调试输出只允许描述当前轮次、当前节点、执行状态,不得展开成长篇解释。 +4. 调试输出应尽量固定格式,便于人工核对逐级流程是否被跳过。 +5. 若当前轮次包含多个节点,应逐个节点输出,不得用“本轮已完成”笼统代替。 + +推荐格式: + +```text +[DEBUG] level=1 node=ROOT action=generate status=done +[DEBUG] level=1 node=ROOT action=check status=passed +[DEBUG] level=2 node=八、服务方案 action=generate status=done +[DEBUG] level=2 node=八、服务方案 action=check status=passed +[DEBUG] level=3 node=8.3 VR智能培训中心建设方案 action=generate status=done +[DEBUG] level=3 node=8.3 VR智能培训中心建设方案 action=check status=failed +``` + +### 收敛与最终产物 + +1. “无法继续下钻”是指当前层级节点已经到达证据支持的最深安全层级,继续下钻只会制造空泛标题、伪细分或重复切面。 +2. 当招标文件、采购清单、技术参数表、分项报价表、供货一览表已经出现可识别的系统、子系统、设备或服务对象时,技术目录必须优先按这些已明示对象继续下钻。 +3. “无法继续下钻”必须基于本轮已检查通过的节点逐个判断,不得在上层轮次提前宣告整个目录收敛。 +4. 最终目录导出只能发生在循环结束之后,不得在中途为了查看效果提前生成目录版 Word。 +5. 最终导出产物为: - `work/final_outline_technical.json` - `work/final_outline_business_other.json` -6. 再分别执行 `render-outline --bundle technical` 和 `render-outline --bundle business-other`。 + - `final/技术标_目录版.docx` + - `final/商务及其他_目录版.docx` + +### Mermaid 流程图 + +```mermaid +flowchart TD + A[整理目录结构边界
建立评分点 风险点 证据点] --> B[生成当前层级目录
统一使用 heading(level/text/children)] + B --> C[写出当前层级中间 JSON] + C --> D[执行目录检查核对] + D --> E{检查是否通过} + E -- 否 --> F[停止后续下钻
禁止生成正式目录 JSON 和目录版 Word] + E -- 是 --> G[标记已批准可下钻节点] + G --> H{是否还能继续下钻} + H -- 是 --> I[仅对已批准节点生成下一层直接子节点] + I --> C + H -- 否 --> J[执行最终目录导出] + J --> K[生成 work/final_outline_technical.json] + J --> L[生成 work/final_outline_business_other.json] + J --> M[生成 final/技术标_目录版.docx] + J --> N[生成 final/商务及其他_目录版.docx] +``` ## 拆分规则 @@ -36,10 +100,6 @@ 2. 商务及其他目录保留 business/other 目录,并在技术内容应出现的位置保留技术占位。 3. 若招标文件明确规定分册、分标或顺序,技术占位必须出现在原规定位置。 4. 若招标文件未明确规定位置,则默认在 unified/canonical outline 中技术部分入口位置保留一个一级占位章节。 -5. 商务及其他中的技术占位节点必须满足: - - `workflow_bucket=other` - - 稳定占位 ID - - 可渲染为目录与正文中的转引说明 6. 商务及其他中的技术占位不能成为技术门禁放行依据。 ## 抽象标题处理与下钻强制约束(核心规则) @@ -95,18 +155,18 @@ 2. 技术类章节是否已经下钻到第三级或第四级?(要求:是) 3. 技术方案下,是否同时包含了[原则]、[架构]、[内容/模块]?(要求:是) 4. 所有的评分点是否都已在目录中体现?(要求:是) -5. 双目录是否都已从同一 canonical outline 拆分,并保持稳定节点 ID?(要求:是) -6. 商务及其他中的技术节点是否只保留占位,不承载技术正文?(要求:是) +5. 商务及其他中的技术节点是否只保留占位,不承载技术正文?(要求:是) +6. 招标文件、采购清单、技术参数表、分项报价表、供货一览表已经出现可识别的系统/子系统/设备清单时,技术目录是否已经按照要求下钻?(要求:是) ``` ## 定稿门禁 正式双目录必须同时满足: -1. canonical outline 中评分点、风险点、证据点都有承载位。 +1. 第一流程中评分点、风险点、证据点都有承载位。 2. 技术标中的抽象标题已下钻,直到能承载正文为止。 3. 商务及其他中的技术部分只保留占位,位置正确。 4. 显式章节、附表、附件承载位没有被无故遗漏。 5. 标题层级、编号、归属关系合法,防止自动或手动编号混淆。 -6. 每个最终目录叶子节点都已标注 `workflow_bucket`。 +6. 最终目录中的每个节点都能追溯到某一轮已通过检查的层级结果,不存在跳过当前层级检查直接写入的后代节点。 7. 不得用“目录已经想好”或“逻辑上已定稿”代替文件存在检查。 diff --git a/references/output-contracts.md b/references/output-contracts.md deleted file mode 100644 index 0ebde35..0000000 --- a/references/output-contracts.md +++ /dev/null @@ -1,185 +0,0 @@ -# 输出契约 - -## 目录阶段 - -### `work/outline_strategy.json` - -目录阶段工作事实源。至少包含: - -```json -{ - "project_name": "项目名称", - "status": "candidate", - "blocking_issues": [], - "review_flags": [] -} -``` - -规则: - -1. `status` 推荐用 `candidate`、`blocked`、`ready_for_final`。 -2. 未过门禁时,不得直接写成正式双目录文件。 - -### `work/final_outline_technical.json` - -技术标正式目录事实源。至少包含: - -```json -{ - "project_name": "项目名称", - "bundle": "technical", - "doc_title": "技术标(目录版)", - "sections": [ - { - "id": "node-001", - "title": "技术标正式目录标题", - "workflow_bucket": "technical", - "children": [] - } - ] -} -``` - -规则: - -1. 只有通过目录门禁后才允许生成。 -2. `id` 必须稳定,并沿用 canonical outline 的稳定节点 ID。 -3. `bundle` 固定为 `technical`。 -4. `workflow_bucket` 只允许 `business`、`technical`、`other`。 -5. 技术标目录必须保留完整技术展开结构,不得用技术占位替代。 - -### `work/final_outline_business_other.json` - -商务及其他正式目录事实源。至少包含: - -```json -{ - "project_name": "项目名称", - "bundle": "business_other", - "doc_title": "商务及其他(目录版)", - "sections": [ - { - "id": "node-101", - "title": "商务及其他正式目录标题", - "workflow_bucket": "business", - "children": [] - }, - { - "id": "placeholder-technical-entry", - "title": "技术标内容说明", - "workflow_bucket": "other", - "placeholder_kind": "technical_redirect", - "children": [] - } - ] -} -``` - -规则: - -1. 只有通过目录门禁后才允许生成。 -2. 真实业务节点沿用 canonical outline 的稳定 ID。 -3. 技术占位节点使用稳定占位 ID,不得每轮变化。 -4. `bundle` 固定为 `business_other`。 -5. 技术占位优先放在招标文件规定的位置;若招标文件未规定,则放在 unified outline 中技术部分入口位置。 -6. 技术占位节点归 `workflow_bucket=other`,只承担转引,不承载技术正文。 -7. 双目录首次生成的同一轮,禁止同时生成双正文事实源。 -8. 正式目录落盘后,必须先生成: - - `final/技术标_目录版.docx` - - `final/商务及其他_目录版.docx` - 再允许进入正文阶段。 - -## 正文阶段 - -### `work/final_bid_content_technical.json` - -技术标正文事实源。至少包含: - -```json -{ - "project_name": "项目名称", - "bundle": "technical", - "doc_title": "技术标", - "nodes": [ - { - "outline_id": "node-001", - "title": "技术标正式目录标题", - "workflow_bucket": "technical", - "status": "drafted", - "content": { - "paragraphs": [], - "bullets": [], - "tables": [], - "images": [] - }, - "children": [] - } - ] -} -``` - -规则: - -1. `outline_id` 必须回链到 `final_outline_technical.json`。 -2. `bundle` 固定为 `technical`。 -3. `status` 只允许 `pending`、`drafted`、`blocked`。 -4. 只能为 `final_outline_technical.json` 中的叶子节点写正文;若 `outline_id` 对应节点仍有子节点,则不得直接填充为完成稿。 -5. 在 `work/final_outline_technical.json` 或 `final/技术标_目录版.docx` 不存在时,禁止生成此文件。 - -### `work/final_bid_content_business_other.json` - -商务及其他正文事实源。至少包含: - -```json -{ - "project_name": "项目名称", - "bundle": "business_other", - "doc_title": "商务及其他", - "nodes": [ - { - "outline_id": "node-101", - "title": "商务及其他正式目录标题", - "workflow_bucket": "business", - "status": "drafted", - "content": { - "paragraphs": [], - "bullets": [], - "tables": [], - "images": [] - }, - "children": [] - }, - { - "outline_id": "placeholder-technical-entry", - "title": "技术标内容说明", - "workflow_bucket": "other", - "status": "drafted", - "content": { - "paragraphs": [ - "技术响应内容详见《技术标》文件。" - ], - "bullets": [], - "tables": [], - "images": [] - }, - "children": [] - } - ] -} -``` - -规则: - -1. `outline_id` 必须回链到 `final_outline_business_other.json`。 -2. `bundle` 固定为 `business_other`。 -3. `status` 只允许 `pending`、`drafted`、`blocked`。 -4. business 阶段只更新 `workflow_bucket=business`;other/finalize 阶段只更新 `workflow_bucket=other`。 -5. 技术占位节点只允许写转引说明,不得展开技术正文。 -6. 只能为 `final_outline_business_other.json` 中的叶子节点写正文;若 `outline_id` 对应节点仍有子节点,则不得直接填充为完成稿。 -7. 在 `work/final_outline_business_other.json` 或 `final/商务及其他_目录版.docx` 不存在时,禁止生成此文件。 - -## 渲染边界 - -1. 渲染脚本不判断目录是否合法。 -2. 渲染脚本不自动扩写正文。 -3. 正式稿的双产物归属由 AI 在 `bundle`、标题、封面和默认文件名中明确表达。 diff --git a/references/rfp-deconstruction.md b/references/rfp-deconstruction.md deleted file mode 100644 index 9dbd547..0000000 --- a/references/rfp-deconstruction.md +++ /dev/null @@ -1,45 +0,0 @@ -# 招标文件拆解 - -## 目标 - -把招标文件拆成后续可复用的事实源,服务目录、正文、表格和图表生成。 - -## 重点信号 - -重点识别: - -- 评分、打分、评审、分值 -- 废标、否决、无效投标 -- 星号、关键条款、实质性响应 -- 架构、部署、接口、安全、数据库、网络 -- 营业执照、资质、授权、业绩、报价 - -## 至少要抽的事实 - -1. 项目名称和包号 -2. 招标范围和建设目标 -3. 评分办法和评分点 -4. 星号项、否决项、资格门槛 -5. 工期、服务期、交付地点 -6. 部署、接口、安全、验收等技术边界 -7. 商务材料和证明材料要求 - -## 处理原则 - -1. 能确定的写入事实源。 -2. 不能确定的写入 `review_flags`,不要伪装成硬事实。 -3. 看到展开引导句时,把它当目录下钻信号。 -4. 看到“应附”“后附”“证明材料顺序对应”时,把它当证据承载信号。 -5. 看到 `XX项目`、示例文本、历史项目名、占位字段时,把它当模板污染候选。 -6. 看到章节号、分册号、附表号时,保留原始结构信号,不要自行修正。 - -## 推荐落盘 - -- `work/document_graph.json` -- `work/rfp_constraints.json` -- `work/evaluation_model.json` - -## 和目录阶段的关系 - -1. 拆解结果必须能支持目录阶段判断“评委先看什么、每个评分点和证据点挂哪章”。 -2. 如果拆解结果还停在“有技术方案/实施方案/售后方案”这种抽象层级,就还不能直接定目录。 diff --git a/references/tables-and-scoring.md b/references/tables-and-scoring.md deleted file mode 100644 index d97ff0f..0000000 --- a/references/tables-and-scoring.md +++ /dev/null @@ -1,36 +0,0 @@ -# 表格与得分表达 - -## 表格的作用 - -表格是承载信息的工具,不是自动合法的目录标题。 - -常见表格包括: - -1. 技术响应表 -2. 商务偏离表 -3. 进度计划表 -4. 团队配置表 -5. SLA 表 -6. 软硬件配置表 -7. 附件索引表 - -## 对目录的约束 - -1. 发现了表,不等于目录已经够细。 -2. 一个标题下如果实际承载多张不同用途的表,通常说明目录还要继续下钻。 -3. 不能因为已经列出几张表,就把技术正文停在“总体技术方案”这一层。 - -## 生成规则 - -1. 先回答“这张表服务哪个评审主题”。 -2. 招标文件有明确条款时,按条款逐项响应。 -3. 无明确条款时,使用保守字段,不虚构商务事实。 -4. 表格标题要高信息密度,避免“表1”“参数表”“响应表”。 -5. 表格编号应在章节内顺序编号。 -6. 术语必须和正文、图示保持一致。 - -## 得分表达 - -1. 对应具体评分点,不要写空泛口号。 -2. 能量化就量化到工期、响应时限、巡检频次、培训场次、可用性等。 -3. 得分表达必须和正文、附件、偏差表一致。 diff --git a/references/technical-track.md b/references/technical-track.md deleted file mode 100644 index cdd71dc..0000000 --- a/references/technical-track.md +++ /dev/null @@ -1,80 +0,0 @@ -# 技术节点规则 - -只用于填写 `work/final_bid_content_technical.json` 中 `workflow_bucket=technical` 的已定稿目录节点,不允许删减任何定稿目录,只能新增技术子层级。 - -## 开写前先想清楚 - -先回答四个问题: - -1. 项目总目标是什么。 -2. 建设主线是什么。 -3. 哪些是主系统,哪些是支撑系统。 -4. 评委最先看哪几个主题。 - -如果这四个问题没想清楚,不要直接写正文。 - -## 展开规则 - -1. “总体方案”不是终点,必须继续展开到架构、模块、流程或接口。 -2. “实施方案”不是终点,必须继续展开到阶段、资源、工期、交付件和验收。 -3. “培训方案”不是终点,必须继续展开到对象、课程、方式、频次和考核。 -4. “售后方案”不是终点,必须继续展开到响应机制、质保、巡检、升级和备件。 -5. 没有企业专属事实时,可以补专业方案;不能补伪造业绩、认证、参数或授权。 -6. “平台建设方案”“系统建设方案”“中控与教学管理平台建设方案”不是终点,必须继续展开到模块、功能、角色、流程、接口、部署或考核机制。 -7. 不允许直接对仍含子节点的父节点写成完整正文并跳过其子节点。 -8. 若当前叶子节点仍显抽象,必须回退目录阶段补目录,而不是继续硬写正文。 - -## 正文最低要求 - -技术正文不能只写“满足要求”“无偏离”“详见附件”。 - -对明显需要技术标的项目,正文通常至少要覆盖这些主题中的大部分: - -1. 需求理解和建设目标。 -2. 总体技术路线和总体架构。 -3. 分系统或分模块设计。 -4. 关键软硬件、接口和集成关系。 -5. 实施组织、阶段、里程碑和联调。 -6. 培训、验收、运维和售后。 -7. 质量控制、风险控制和应急保障。 - -## 图表规则 - -1. 先回答“这张图/表帮评委看什么”,再决定是否生成。 -2. 架构图说明系统关系,流程图说明步骤,拓扑图说明部署,进度表说明计划,对照表说明条款或配置。 -3. 图表标题必须高信息密度,不能叫“系统架构图”“表1”。 -4. 图、表、正文必须使用同一套术语和编号。 - -## 一致性 - -1. 每个技术节点都应能回链到原文条款、评分点、风险点或材料证据。 -2. 架构、部署、实施、培训、验收、运维之间不能互相打架。 -3. 若正文仍显得篇幅过小、层级过浅、内容过散,应直接判定未完成。 -4. 星号条款响应、偏差表、正文说明、附件证据之间必须互相可回链。 -5. 商务及其他中的技术占位不能替代技术标中的任何实质章节。 - -## 边界 - -1. 用户资料优先于 AI 自拟内容;与招标文件冲突时,以招标文件为准。 -2. 技术正文只能基于已存在的 `work/final_outline_technical.json` 叶子节点展开,不得跳过目录门禁。 -3. 不得依赖 `work/final_bid_content_business_other.json` 承载任何技术实质内容。 - -## 只有同时满足以下条件,技术部分才算完成 - -- 已明确项目总目标、建设主线、主系统、支撑系统。 -- 已明确总体架构、部署关系、系统边界、数据/控制关系。 -- 每个主要系统都有独立承载位,不得用一段总述覆盖。 -- 已覆盖实施组织、进度阶段、安装调试、联调、试运行。 -- 已覆盖培训、验收、运维、售后、质保、风险控制。 -- 星号条款和关键评审条款已逐条承载。 - -### 评委可以直接回答 - -- 建设什么 -- 怎么建设 -- 怎么实施 -- 怎么验收 -- 怎么运维 - -任一主系统如果仍然只有概述段,没有展开到“功能/流程/模块/接口/实施/验收”层级,则视为未完成。 -任一技术父节点若直接被当成正文终点使用,也视为未完成。 diff --git a/references/understandbid.md b/references/understandbid.md new file mode 100644 index 0000000..772b8a4 --- /dev/null +++ b/references/understandbid.md @@ -0,0 +1,59 @@ +# 招标文件拆解 + +## 目标 + +把招标文件拆成后续可复用的事实源,服务目录、正文、表格和图表生成。 + +## 重点信号 + +重点识别: + +- 评分、打分、评审、分值 +- 废标、否决、无效投标 +- 星号、关键条款、实质性响应 +- 架构、部署、接口、安全、数据库、网络 +- 营业执照、资质、授权、业绩、报价 + +## 至少要抽的事实 + +1. 项目名称和包号 +2. 招标范围和建设目标 +3. 评分办法和评分点 +4. 星号项、否决项、资格门槛 +5. 工期、服务期、交付地点 +6. 部署、接口、安全、验收等技术边界 +7. 商务材料和证明材料要求 + + +## 如何理解标书 + +### 评分项 / 废标项 / 合规项 + +在理解资料和目录设计阶段,必须先把招标要求按业务风险和评审作用分成三层: + +1. `废标/否决项` +- 指资格门槛、实质性响应、星号项、无效投标触发项、必须逐条满足的硬约束。 +- 这些内容优先级最高,必须先确认是否有承载位、是否有证据、是否存在缺件或高风险。 +- 若发现缺失或不确定,不得用泛化正文掩盖,必须显式标记风险、阻塞或占位说明。 +2. `合规项` +- 指招标文件明确要求提供、但不一定直接计分的资格、声明、附件、表格、承诺、响应材料。 +- 这些内容必须完整进入目录和交付物,不能因为“不加分”而省略。 +- 若证据不足,只能按缺件规则处理,不能伪造完成。 +3. `评分项` +- 指评标办法中有明确分值、等级、比较维度或加分导向的内容。 +- 这些内容必须优先影响目录结构、技术展开深度、图表配置和正文篇幅。 +- 同一章节若同时承载评分项与合规项,应优先按评分逻辑组织,再补足合规要求。 + + +处理顺序: + +- 先看目录,再看正文 +- 先看评分办法,再看正文 +- 先看星号项,再看正文 +- 先看否决项,再看正文 +- 先看商务材料,再看正文 +- 先看技术材料,再看正文 + +1. 先锁定 `废标/否决项`,确保不漏项。 +2. 再补齐 `合规项`,确保正式交付结构完整。 +3. 最后围绕 `评分项` 优化目录颗粒度、技术展开和证据呈现。 \ No newline at end of file diff --git a/scripts/__pycache__/ai_workflow.cpython-311.pyc b/scripts/__pycache__/ai_workflow.cpython-311.pyc deleted file mode 100644 index c08860856feea9d072d853ec803d0e0c3dfaea81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9995 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 SZa%tM@qPy!!UIt4?esq%eHfbn diff --git a/scripts/__pycache__/build_business_placeholders.cpython-311.pyc b/scripts/__pycache__/build_business_placeholders.cpython-311.pyc deleted file mode 100644 index d7694fdd9a103af738723273200075b2656c0ada..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1466 zcmb6ZO=}xRbar>NyV9;K=g>N79b+63M-;^e9}1-u0>(X638W_>SXMhzd6nIjGBevY z!l5eo(1HuSl=kS8o8lb$GkTPaz{^4)&{J=MkV{T^Gm5MzrH8(mee=HNy*Ka8&hO=N z2?2DBr(st|=x@o)0eA`8i#Fh25kW+YP^>jIP11U#H+8idO+&4?iD5OOLTolou(64G zgqs$%iSceT)v#$(sYtZD%Xdi2l?#1>M=DUi)ur{J*RV zr;yLtWlqX3Wd3h>CtFR}3!83XPK)xKLP%8JqE{F6a>SG3LbITjqjlAlkXm969E6rV z5bIFuf^-H11Lw}Uj`>&@AI>?1x+`<~P=`9%?^czd`>0lI42t!79C98eoiCx?Hlq*I zkWn%y)a$)eNOhm}x-<}jjSfvH^9A+#ksr{5UPLJK!r1Rne!Kr@VAkurPXq8$2o7lQ zpjOFkpLO~^<1}Bn!#Zh96LC+nEO&iEJkTmxUQ$Mq=efZj@w`O8r9mqEc0@rS7{hRy z-1fY$!iaiaZZXPx(bqILVJCRatv83r4ahn#@w5$T2b6<9mL@er8Op#|1+ju&9Qz63 z@&;^fZoc|r?~~S@e!nFs7p-*;;nD?OJI8CkIZwz4uZ{8U1n-XU z?m}TbsVc?J1n-RS&O+ggadm=0S*vp#kOPKN{U`jNQiAzB{q+X7*}kyT@*3SLA9>%T~o4e*SV56)H!KpFci< z5ZhzCJ;B>R%;pO?i)w{DqUY6gCjSX0AA(vjFF}h^cq^buOxBjk3(ek_NM6X?>ce{y zg>BVPRUp~b#E)UaABbI-hVmA_vOkS`B#r22>;|y%x8>gfni`s>WoT=(JfEY=P>l>- z8O>*gN<%d=bak{mr)EXFHATy)im>~`;M}-2GOlIV9`1d=f3!bTV`?D$0f<$CuK_#l BRpS5v diff --git a/scripts/__pycache__/build_tech_diagrams.cpython-311.pyc b/scripts/__pycache__/build_tech_diagrams.cpython-311.pyc deleted file mode 100644 index 023ea035b0f78755c85f2dca2e5858c4a71429f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6325 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 literal 0 HcmV?d00001 diff --git a/scripts/__pycache__/docx_patch.cpython-311.pyc b/scripts/__pycache__/docx_patch.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c3b482a8587078f24367b126aa4309cf4711990d GIT binary patch literal 2081 zcmb7F&1)M+6ra_Oq}8rIsuIZ`a)V=nZ5g&r9Ag~YI1LydQW8>PD5!#Et)0Yjw5#px zsEu9OsE~s%4L!8P&<1oUZh`~#rMLb8J?@GHi3LNT^w68ZJ>}Gyl_fKLIT1;k@X(KmqXB@#eH!(kGZ&zKfqVX!75`?| zEayq8VU&qVieS+D3 z>*0?5&ENjG{Uf#mB-!(=2F{>7ovX_NZ}zd^wa8F+X#co(;js;AY(Mo`5Io)&`wis$ zk&R>(ZglsH%FzzZAAu%53v5wrJ4iAeD*#nk4fdaBmDi+2>Oel|d7WXH=pg53SbZxq zHE<7Pqj$EVvg{X?e7{&itF+<-jJ~K8G19gKS;ojLW3m-z5-Tscu#O53J<%d%j1+<^ zh>i%U1y-*t)Tx(N#GX{H5VJzs1sPV_h)1iD#mT?w&8Raum>j(M|@<-g-bSE>LPn_ z2Tr(f!iEzqDcO`p8q$a(U38_3wdKE(!_Q{w@aKe+x#DK7G&6IJ%$$>%cQf-&a=}e5 z)NUQcF4d=;nC!-6N-!+B;lX3T2S4VH(!+bRb(s!x(pTN|)n@v7BYoXTFSzN2U8yy< z_wk0HRGmc?9|L3pS!me8<{Su*lRoO=G@-d6^}CG z^{)?aIhh-7=EiPl_mLwE9>MsxqF&Z2CA5UIRAra5^@MiEJjZb@FlP6+S0GXI z-xe6O``at2w`k(D6}sV2BGGzol;A3Pv@-- zZt+kvA*nme}ZHLHV1{mwuik1`qro2of{h`gwlOFefoag_v60r z^mD0X01jXO@w?wu0r*QWd%$@C%FA_*p8y1q(gK0vD2kxfmg=bdtF^QMI#9}BYbMA! zxghW41KrVsf>Yq~0?I#zj)6_2-<3W|Q3cr8=3aM1014bqy&+%A_*Y0Qg_&L$M%1Hz z6vnHZhu1y&u?5+zA0m9;MbTO(z#(Pky$&W1+%Q5oW(9&h!?4 zSx2uXkr$^QJhN12FA(ddEYRRwf(vMH$DR-t#}A zX!OXTQO1?MeH1!B@!D0p`?|QlrwH5tR&I48XWM)b@89I}sS|v!;}eWFinc9h(!TAt z@Wza7M;&UFnC_7~ZI8s5&0ZmQEMY|tA(v}M18qnn!N+8VCq*Df42n1`PjDQy zKEXtEhZ&>M7h#Al1R9S-L__&K$w@baLE(rQuWyY10|uos~}aR))|Rz_~u0`(A%~E`f6?tY@&E!1_cn`|KU5Sjym10+%L={L_q-pLlq%HFW`=T0i-JQVp?D6mxBm(TKGM3 zg0tf4#@}+Zt0{^y1SgW|^#YW8@)?4o$@F>wK1|-Uc5?+~RoMjx@KFGZpKtWF`9zx^ WLcO>A*~;UUo_uyS02jDe=KLQHpl}ZW literal 0 HcmV?d00001 diff --git a/scripts/__pycache__/export_docx_pdf.cpython-311.pyc b/scripts/__pycache__/export_docx_pdf.cpython-311.pyc deleted file mode 100644 index d65d9a4b1f8b1494bb4263d4da43c1880e40565c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1741 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 C#gN+o diff --git a/scripts/__pycache__/generate_toc_only_docx.cpython-311.pyc b/scripts/__pycache__/generate_toc_only_docx.cpython-311.pyc deleted file mode 100644 index a940f88e6e638a058bdb213727ca71b861f6a576..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1328 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 PWc6Q(LD&ss-IV_T5D`gF diff --git a/scripts/__pycache__/outline_check.cpython-311.pyc b/scripts/__pycache__/outline_check.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0bf7eaec90b72ca55d8a1b183933a50d41ddad45 GIT binary patch literal 8966 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 literal 0 HcmV?d00001 diff --git a/scripts/__pycache__/outline_linter.cpython-311.pyc b/scripts/__pycache__/outline_linter.cpython-311.pyc deleted file mode 100644 index 9ba19fbe41aa382897dfdfd815ba9f970d204be3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 27733 zcmcJ232;+Ky6!nzELrj}JwEzRpYA@r_uv24?e|kt4FayNXFsYBtrdj7(o5vhafkTbzlnlyO4uMsLbV`? zlBz*$R8^})a;qEE)oS+CRBPB%Tdif!r0OI*H4VB(eYL(ZxjLD}*ESd$jn&4+l7+jOq+g*d|Cx?}*hVNoUQJ^z)GYc0o%1jaZ!}8Q^0iAH+#{M+8h| zFpa@<1~V8my(3DQXOpV4r7V1t9sVW<>E^I>bIC`_tIn13tMjCS>U?R5RCGX7T_7!$ z%y^EHmPzCB94$R1O~kWMS}sk-bBwe?D#o)2&k{VxTJHBM(n@JM(jG6(kjkW)XI0WF=@$o*swYURr6=G&QCcG{z;lvR zDJ{fvvb0wErL^dPwt9-SSX#W3l|ar?3Dy$)ro@-y0($Wxe*Ag~eYajHdD@}T2!f&6 zVQ;8!vep3Dt@5;%=M9Gb&h9~1=ijcsX)v3ESH3XUHkH=fOSjfbrF-OhL^AikdB9w2 zX=-k&ueCIo!zq|U?l(e*U7@pALY_nYXD$bCoDA-N^DnNm+&$R!dFb+IgQvRtFYNz| z>)hbE(;?UX&~IF!x8M8Q^&`kBbog-J4NvfFcO=15o87X_I;~MM4<0%aeC0H9?LU8M z;Qa$Z_vzrVu8_MUc$p+dXEme-W>SqP_XCYL6<9d z=<49Px6CvCcD?1gIg+jN_?)?VM-WSid9ELm-l+y=gU9X0CfeYgMpqan}r;Pqq3fv4jhyw){v z`CU|W`VL!jQ{;=Kd+Y7yTBL!9Gt9hc{&xM$Ompb$hmkjT=xn=r>AH1m*WqgfmYD~> z>Oj+mPJKSud3xZ=6|`&UjYECcKA+C&HgM%y@Yox{cRwEZ^u7L%@C9$%;M-S2XD{`k z%CBDNyK!vbtJ8f~&jfpW2j1U5c z0on1&pzA`Pob12mW_{LK@S~1_!!IF-bp%hb|6>$wC?xc|kAqh) zpkD?)|0J5KdEP7(`Q9G|_n+-Qc^Y|8IWN34@Kw+Bfh(Vex?V&6sBY-+je(EP2S0wP z|LXhUM92d}2UX++!nmR)Ei?D^euz@?Aml&r)w$rYqe!-|_tn7@*Qi`C-v}N(+1Kkr z+VO?r&4K!aJYVos`>$UO_8iC8!Pnl#JeVGO>vIgT>7nlP!E>jmY;4E{kGv6d9~wOO z4j-N|je+mYdz$5)bq&pXYPLFRcUtX+LBb0qV!&u=YHGGy*pRC%Q3aAEi``P&V6oW( zNfw(_Uu!Q(3aBfZo)2i2;3c40U2n4o^lMw_3rj;lz20gMXsaA84c3y>?^BRyY!5q_ z$JwL7!{>RmSd7?icjV9~EJoi~=LX+A8$5P6^xB6Q7LJJs33gr%cD%;JgC~v*eAz0O7DKR)G9X>Fa6Nn1KjypekqyOZ4%*pW^a{`$T9C?{lA;ju5aAAMw zU|kM@c%*jE&FO9C)J-Mdu@!#Tay58~E~gj7NX_2k6Ea zkI>1U(9z2=9>J@fSgyFo_bgGq1Px^yyxbl<7>>bndAYm)#&5%EP>Y3LeLd8EoE3M_ z)gFB7RbE_{(7^Am^k4modGPv&JDEBw++?gm+`A-|SDMX6<_sQw8`Im7Oda!42Om!? zp#Rzd;8Gsvm4jF~SRQ<+1-S?7&C72A*YQ+edoOtGH!P`eGkmt61rYu?e1^yAJqT37 zlVUlChIz2Em89|LpY8AOew91VUJbf=dHOG(N2~KX^EH5nQSKSKsZs)-^n#kKP;n@LgWT=$i4#$#9Lr zXn>jyOE&O6YcCKGsAvB>;ZA0xNK6VbAB8@9b>PZLRyL3mq0Y})H13RwQ0JZ**)V(=2j5*Ux6*EpgA3IpB1W0V+qY7Fu5{D1RW=(YDF&cV|kFee6l z$i*k};5!GfScdCCgPdbc=E0MW@rcE%ECvQ(|FuKm7}Q>H2CuxrGU?y{W$@iY!1jg~ zxxT5k9vIygNU_MEPwb#c9I`c_YiyPrO+QMAzzBIT832%pa`<`IkVnaALnS3ptnX)NcVIplnRKqf=B zi`Q0GE&Wwh&GHqMRqIQ%0WG8C0vaOa0=h6Q7tjz97SJ|WwptrXbOC*YnhR(dITtY6 z99ttKT|ggoX&H4FP$M*;p(rKlKzhO&9Z2G9bs%|9U3l3JsAbC@nK~|zBwNuZO-RBn z*GB1lglO?o3xfFF-x(k06kIAvwO+8t#BmDo+<_-13}1(@3wRFkHEp7uQVw4`kORLY zwWM;27w~>D>UYn$numz1eTcY84-r>4Qe3C_liEN(Jk6h!Ub19JY|{rz&nbe-GdlF> zP>_LOuo!U~ym11QCJ00j;#fKRuD%vL^2OjmB1mH?ZBFKLW{a~CW;3XIaCDr2rv$%Zuk z!pb$2Q0VN%V9%GKS1%!<&?mr+hbg(ptS8QZ=;tdJ`mephQYu|wjth-RufrYu;`89U zZ}#0d-T%Q=ut_Yd%7EZlfwB7Q&9+B2A+6#qa1?Js$@;Dy>FaF|o;lNZwKI6|qrTpk zg9k76eQ_>$bbtTpFM~&4?(03YCqDYZF>oQ>lhzS4FSaq%QsJ>2#-(R3$mKns@`mS^sZR(tiEeELl>{ujtj6`}F1C=%2i)f6}X8qK z8~wza`iWlsWS@SrtHN(gaV`H2wH2_`fPIg{?IfdPxZMZGsRW#Ek4mHjAztnzud_q! z6SK(r7)Cu{U>_jSs zFQ+O#hg!%M*9%8hNrbypmGW%F!%}Lfx79a697I%wh8WR+fpJbmF{}@$w^{9c0IDIx z3TVOGTI?UFWfJ!V#GL_gw~eX?j3qx#Udf0b9#c=@{VaYq3BV=X(wRCYp3``BV|=5QMJ zOA|Uy<5Y#ar)(QK=>xH{L@O^qN#%tA0mI_uD^@RIJt7lg3uqd^_R34(jBoxFYW@gq zB(FjEdlY9Qzz9815bl8j>VblL^gxw2eY`JyJT-lRKQF&SPk+?t1!#2F3f}0c5kUoG zqepJ||AIn1M8j7mH2kvvkcJ=SRC!|PEfg_)B~WE5r>aGigm5Q4C83k#m55rB6w^({ zMH?2cT3R*KPi*GMa}aNYzA{i>ane(!b>=dDwnl(qJ*Cx-yJafGoJk(Xz&(0uy*GWF zFC9@-spI_FIj5dF@zn7Zohv#->bB>*mM9r!e&5j|#+@V~CBr?N^Y!#ippTze z@tqRCWbia3bvt#)m|k-(`E$3?W`xmxX;Cwp1aBOwpp93dt2lMt;O}W8Zgt_ zYigUJdD$CYk*3XSXogb7wqRN`=6^!gwqk%E2803hsA8Cl+atQmI*f{OG=GhStUDMV z&q?Q>SR?t@$h$->*WeKlZNqCNTM+W#_4!xCj;qh)eahs0y%nBa-K&%-zkoa8X7wpa zVl@g&1Co#i*G4#~i@@4|+9NMw5?Dko24GH3-Y!EGauEm9tm%2#3|EQB>q5BXIZhhn zjMyo19b)_eKieLFA6&w1UFO%hrHYPje|nbNeoJSF?SZc+{Q|FE!(1PrZmv%^7fX{i zn?=2?OYe9>(G}r-Z&qAQD_ZgDoxINnIANb}!ahV{pZJX#?#Eaw4C_iZy;@PKQ=~B! ze5A-2S^T2AaFN|X;s+MNvdV%pIcY|=QMK@N<;;v6M( zKOy&rq&Z>z5!RoC#ibD~3RFX+s5#gRfotfqgV3eKDs3V>=K?}{-!o|t7rHZDOpFKD z%5aaE+?WSf1n}(n_+VdGVI(vlSvENOuD#lS?I?5dq*%}(QMSvV1JPTXB-@^P`}Tmk z!P*3lhGFMa+jdI>X;7?tEwvHuhL4~EjG#y;O>d?nt-+dAC+|X<{|!G|EdUr8y^vEQ zD%ror?ONm4Wp*?wx(T>>pX)Ne(T%>T8?6+Udv#CvbWaQoA%E-;@?fyc@=ci4v+L%B zxyppOi6aJiMK78wxWw<=fTskjPz`#aS_S>SXoc*C>BrS@X_!k3S5nlaV=g`XlB0fx zsEcXnr4)pwGQTvq(xYEwMEy+6m5I}cF7FGBSKS_y*4?SW)YHjuE4`cAGS zFpa<_ilsw;QSStdj3fm%c7TUSiN(w^>7@cG5Y5?TtF;EpuC1~}6VTVxko>QvCSa(k z;nGmJQ)+5eocmJwQKeM)qYKuYprYT{?ugO-}?bT%a~BMx3(DkTlv=oKAI+u?PO!9>GK%{rP~d{~i78};J@ zOb0(^_8}$1n8biLKA@6eQbKPjEPgTuOKL#70%Bz=?|n#7cUxhU0F@-^w;`2=BpOx& z@?MllCTRg13o>EI%9ySnhj)<0E{d?9z)J*PCcuUUg(vpaLGmMT2S3{mfcU+ygYHuU=et?bB!3+Gh@=bnsPB4L z@SqzO4iRev6oHF)IUz;6zc#mdo>Ve5dQ z{Gm_Kg~ea;BdD)DnE-JFatQ#0YmCWWO&-K)GwvEoSUb6w5?BTh&tqqTpp>yR)yU1w zc3?-CkH|+53-qgCqiNw1zE2bMnH>{O+P%6$pAMXxsLk@HXSxj}1c~w1YIFRiWsLid zvfx>#bSHFO;&DT#AtD614-1}=<#xDJ{3+?~$6Tw~NFF5IvFrm?Ag!^!iBrKGStpSM zvDPjhLpYzg9dO+p&x{H-F|AQ#sn`yRRT^CO)9fpPh-)oRZ_!{=T!LEyhL5h~3VQEuu6AtU~ zOO~?TNp786?@o4$Zk5~MHrDB-^aCk}jd3zEwUlwm6hCD!6>}J0s%g`zgm$A-D`h%0 z+sHaD>jDtMMM4Vf0&|kvu5VAS1GQF?Fd23wIhmZ#+C)G6UcT;FE#yvQDxU<1ExvpT9uHLTf|!Dv zHz@4x72g}%WAcuhb(Ow6}TER4JxAYkB>5u>HhKSID5 zj!VQ3qmJYV2>jIub3RkSt)Cu7$u?!IU>DqMmyhu1uP6Enm z)7uMTLKE8}**^L{p@z5`8RARbW^@`QJvTpXOL1yivYp9NGJkJLvyX{MSD)}@>OIGT z;iruSr*7mPx09$Xd^ttuKkO~1&Z&1MJ5!uSXX?HC=92M33i=8^ezm2=wpV*zLao!= z^F|(b&b0WLqTo#F5ar#@WZU!y9IGi(DvdyA+77ZCV;9P=zW`?TA~Ce=O3bC{G4xp4 zZc#3Kpj^_W49Vn7KBMBJ{o(R*>f3W6ns~6BG*YIN<nw0KQ>?F zIy#)|*zl-$&|J;ZxF4JAy)jB#hBL!X^ndtD93!pSP)KChi8+YGFi3@$#>cO2i7xj?C4ZS@T4;}o@C_dA2B;KoM|=Dbtl|6vz#dpJVqx-6Mt;$ zg!^P2gqo8e)Xc(|)j6}I$$R5%sh)mKEC4cMV=tm2ZZY{ev z;k(Duurr1Xjwm0-6E#_kZ^IgeqPa@Z-0pGbY@V{yyU*^%OTx{l%2u^`fjOXMj6guM zY{rZ-2aQ3_(87)#JU6zBzzn{am}r=cVDo?nw)-%@4!zY;Zf-Tqt6a;Cl^4jwra?9r zk&ylew4D4lvXDIhKSz_5B{bQn*~+Nd9?Q8|UAs=tIXee02{&u9k`&oNrC>z&kW7l` zQF$K$CX!?#CnvqO5qN>XivR)5qDUjh%EtK~fJ8HXvhRY?6tqwJtqzz=KoBOsONnTk z?c1$#K(n!8T_sC#reiMr6D$nPLDzNKyMp~UkdGq+^vmDjPc;wIDINo>t`a^8o zzyKxl@oVq*UxUFHZN?}!%Mvvi6?C?v1K{Mm~P`2DX)yn+J&!2xuy1%$UivDtw~}&mla~t{og{U&)aX_m$9; zWLrzXw^O06UI?9g8?`(d$z_&fEu6esTw8-q35%#An^xFHs^i7HA`9lv8k*T8eRE@{ z18sAP?l~zpe;|@6m_^1G5mcniF`s3TxIt#%or_#37dy6#u{|vG(K`^;L9DfQ>G8%YWwA_=6{HOtuNCs~`= ztCrHj<{vH&;zBu+#|6@vdc;<{-3t8&eW;;@#zs9t_6fr-SNP&ekAFtA)}s3_T4qAG zEnu#HfC&SvtR}vv@upAmrB5Q`k^+Bj!Kv*hw(~`*doP~eqPaf6+_}Eox$dX@1x0U< zdUKR#oEJE$JEUojA{1FyB`&-@VeGl6h>+;Wg(bd34>mN}2ri7K^gA&bwus zkM0)u&~4v$M-W~Rm#e-Pgq5l_D!f&yo>6^EZyQyA=MTJ%{uR9d!kzjoxd9Ow@K#pt z@y9wJbFU6&kLi+*ukBo`WY6+A-O0CdCwc0$^93&2zmukNa{S zcN?G{&B*s>jq&G<@n?_r=Z(9SnbWcTcxh*;KX-_C+L zmo{~`%Ab*YD)mI_IjiTX?q`(8o>R8gD?1&^o_%P>z2YKj$0e$j^jM8XM+=W-tqlmb ziM2K$f`?#|kUdHnw^+%d+n+t^)S44(-ka2&)RUylZBuN|6P(+IPo7jgMPIH^t*6HZ z6-#Y{s)pVys%`YBSJ^3}2@dL3K)7>ulN%7h1krAzyBd}BM{y_A#;$CWmB;LK@8%hX zYw}Ax2hWGtqCoG$8J@Hni^!08bN^6AdATjh+serr0h}*t*pm=S&u`hVA`x6RwRj% z+Njz}k8<|HVV#OqwpsN&h3r$kNCnuZT1rVjrCvjiwb*n{1=z$2u!$95Gb_MmRscX6 z0F<$;?%MfH(af7gGkbs@=J|@|ao^vMR;nyY1>M6+)=C*Ss`j#yv6pfNohnwprRuem z%hT!&l;zXvP4u=!T}uzEx`mZ&7c1E=Rx+8DOlBpM)j!-Z&|qm$(jURyy|34DbNWJM z`og$jgOuiMrtH^ZqkZ&+d;MBvC9iF&UG%W2_E7uSRBiP3Vx*bYQO;x6Q_g^Jo2tkS zh*$*Kh;3HVOL6x+`g`%W(|e{5E5Zg!b2*ts;l*OWhT2*`Jc%wWMo_C)4vx_pf?Qz3LBs-CO(aH9?ts^wIb zm5~`yP1TG}jO`S*L&fmHXoe2}(bB1`LjVy>kYzbtF_z%==RK*g+r8YM4pTc->LT%W zMn24jl^IVeg%#e6iVpql+#*k*H@6fkSUM)Jn7TI+L%#TRlU;E*y-jVtrZ(lV7jYxA zMJhI#SE*P}ZB%Wd;%-z~s35hfdV1_cAJM>IMFgaQv6rF&A{g*?`e;Qzn(Gg)V2PH0 z3sAWXEX+K5Be69nPweDkGzzH^WJaiv=$cb2o5)p0fJ6tdXUryd6@gp=q=uBg06@a+ z5;7l=eGcAW@)&(}}<63|=pw6ExC*P2^n%$_mcG1Glxrn^@AnVRM6`Bl)X zc+DGp<_)g3!IYvdn>S^GFJ*#j>FvTI*HZteLf5i8I08cp^!)R{?^mmZ+_7Db?g}Mm znm1>fF9-ITqV`b++LK|gS>@NKxHayoj@exkx^g@!&r+}c5ug4Mh26J}86A_|^A$}# z?@nsEyGEuyT|sKTEqBv~_Tt^!4E+*5i&yTD=F}OH2Rnd6OxF*8yykt{m@wwj1f8;yiU7a2M&EmCs430vfz$r zbJ5E;;2AvJ%}=I4WNvWCx8`-)CJ!UX zdpc?S6Vo4-_e^+E!ELj9D&AeDOk1E##NG7!vOoPosccdfH&I~x&4$s)m7j`aqm@rj ztUQ!7Aj*F_LOy5&enA&-7K1hw}hM3f=ti~o0@Ct8yc+JEDg+v zjhj+5%Qf|lEe(NG#-%X{N=@_5Kw&J?w%uZ@!QS2)+S5zMYk_oIZ8P@zZi4|FEULhe zin0|IgY^j^k3hk_vq_7|W~)UCWX0wa<}+<13?;UZV`VAjsNwr38WVd^2L+?P}Cs=PI73~#XBBs{%i7Wx23 zE%c3A2n#wa%B9OhrFf}-%%e)dqyDK2mEwi4y1J9BO-TY%!hkl3HE|#%Vog9g@<4KQ ziq)cw?4oMXOa&?szbnKWj7!D>foa+_apn!sKV8zq%jes)u|_76_5pOBNpW8!J-`>b zZ91l=Ucjh!7^7PdjheUA}7%9*@_<|w;RzR`G(TC(815bsfY47`uQ zJA_Wr`1BstoTFyL%6jBUsmPNY`6Bk2sI|jFZ?0srI3`bwTTH14prFO?u>BDz#3{tk z4`Nsm3VVADBXt{WyxL&8IYdv~qv{ErfQDMxrcMUUH1ec~w>XRM!1y;#P0W; zh{F=l8TUw8(k6cYtZ?M1J;Gk~v%((GN>E7(L!D4ZXOksW%I}~_n370lyvRAc)(ijn z%_mB9@+%ZXLQzPWTN>)^^6$uF5qTsp-Al($U{4eyF0gu?CvP$nuE9wZI|EvnIALRz z*pjd!=}8P*v(x%~>qHowOI8_6DfZ;Z))x2=QB;vw=V_%P;jByW=NFtcc(&clFIDnO zJIXrBddl2-e@3QT>o;X~7MvP;Vyq|2YntRUG1=ur5@1erSKLvlQzv2*)v1&dDd*OE zOsAhY`;0eZqAz2j!tNh#Wsyy0D&j{#ICj?tZ^i^)#snpD(?-XMU~Irnc1`k_-py6g zixqvb&4lt@8DEj9`lrmX6|=SfG)n};IZtX-Gj8}1mSlwN3^<~+98C1$30Xc6ufT36 zOwVN!`TBvH_Z4*$5p}G0Ja`#S)J?iy(?<6@T|(P_M=G%}v1*#HnC5qho?Y*@_G~1X z=ahLJ;{b+k7UMpo-H)u+2Pgv@eU)+_IXwVBwo)G@R4Q}6k~zQ2)Gb~z^wixfo~IPg z>nH;lx>*cXsn(n@8B$8jEzaNLqq5al8ewlpw_0lEwzJUg%tk>Sq#60Et0Pva7R&Yg z)pgv_j3{N(%%mjhxDfoP3(7xkEFIQm@zYh^B0)q|4wD20S6yP8lK3#IBGvy;&>WfGf^g%H*`@vt#A>4 z_uKw!$GEBvN;DjJfHNC#0vk#OHx6%rJ1$pz#&cC7xx$C#iriy!l@LBFk}KDPAsfuB+6E;5`%mdZaS={k)z!T3Sx7fadShuDmMju4uEgIAvY88{E=LK0=EGIsa&;G z!(ib0voJ77!yH^M1+;d9F9}uJBa7OgtNT{x)-7z zm0nwNM~DO8XA3!_yXJWID%mr<*)x3EGa&m(GXc{OvUvu;Yntx^Oq=gZo9}w+R(d8T zEX~X5=B!I^(yUhDyCB0Ud=S0 zW}2dz77dz=^h}d+do@#hnkkBAN;s$^6B~DYYzNQnNYZ6q%}k$WrlOe{j&w&QCRdC8 z>@lR|P{u#$&93leS187f?#JAZDNk+0%7AdJAb{Vh)!L-L3ji?V2ha;z1C^N4SQSab zGu@j#)0aI{F)n9G%w0}NtYS&5x^EIcxfqbrEhnj@b_Gc#0nv+q_i2uKo+*lH9`2rE z{`6|*`!tYx&A+Y7rmf8uF`|W9#}5Tj^!X7V9Ohy6>CKAX>^I?b2Qk^gDrph1aXrO! zZ1v&QNA}@gt#qZvLV+3iN>K{nk;uy^bGoz**qE3yZ<_kLZ zvTL2#oHY_VDQVB@7(Mp$(F^ilQP2A=s1vt3a0+S-9bh_){)!R_UQBN}NGUx5@H3XK z|K}*(O)A|$q;!mojVmItAdKE8;n^u++XL)~I6kx^0-_m8U>H4e*D#Ode|$H@r2DmE zG+KbLBvA_350P2QZ;$EZoUU=lAMboT>ctuvl=rKYE#dJ1HfGvvFeESnV?*A=cTJ`e z*5)!KY@+07o)A6?{6FcDG~z<=zbMiF4PYZSh@tj~m}poXs*@ouzD9rNsc|F9tN#dmhp$ZY*_u%L-M0cE*}AHVMNtxIvfN^yuycP8v@Z_`LPzg$W<2MOnK7$*p2f=#nW#W_(?cvfKNbjaXt zV~nH0-A=*g4AN$WFDWm!bR^iZlWHq`?I8I*yDsI&i~J}DsQ{Zfi0%zvJ4k%UF62`N zp$W3U|zM!Q%CFa^AM)>Zytr^&Wxg*+^aA!oybg3}D)tNzY z5;mJmh{PCavk6j&wx2X6EWcP3XVM%~uh{%(%f+Rl_%YO$9NQ*weV!dD1x)?NO6E)B z;!B$-cW8PfE~Q5!U!x8WQJ%z9V-1RNempkc{DT$S;50zwXofUr0@l`vm8~mRgRe7N zMmSTwSvHe2J^r9|%Qg$0h81D{xNgh54JKu_X>^P;HhZ#D)gT3FO)f1BGmllRi+>3z zw%Oi1LO$$BZ)8SBq0{T?ahh{D5ty7~qy6DhRjMq<`Cfnlqc*opDr z<5l3_U!z<|6bSj%2&~hxL9jJY6b4do{147zt+Ci@>+4~r2f0;~J&w>&BO;UR{0J@G zJNWj#l8?MbjKd+>Sd=#VcUqrg)N)znN*2^|9TlA`JyULGm*6eoX7mLklS=XedaSRI z(%Eo;Y=yiH#*$6;U(%}#V?{}V#)|uKfgf#ew4nv$ZMJ}>8P=MdMxn;T0c6ckN12wP z%`2Pj%cvKY614&$AT(WeFtN|WxY1^Zv`-GC(-c@@rBei0BxW}dW1B$wsX!7RAdCvZ z`4W!C78_$WxvI=4-(`_lkzjDm3MJMtD~Et+4Tv?|hJn~$I$o*Ac$;*xz;XZ+mv# zHw{Y1y^BmtaC({6geTOPrg5IEo+{sC%ax}$_@3UZWNh(fZ1H7m*}uY7;o5yGW1MHQ zH>1RtQQ}&0TW375ucPj!u29hx-ZG8v(R`Zn`>H=}Ql70*cG?w_!)tQ*Ob&Mv>U2~( zvZE`v+uB>9q%H8KE${*A7y9%I6?O;psb8m=yQNDFzn}f5CzOp_lzLf7vw72OK0v+Q zr?)G5I}EW**?cZSkD68P%_{c+W|aFf%3Uk`83l^b>@IVcb)nDAwnZ49f5^&PoG1L@ zk(|Xw^&b|d!2L&~W^uaikLhZ1XNu&`V(z@srAg{P>Cy=1OkOIg|5?<+P3NZ2n69Y- zcAJ4Dxn)rqxn=^ucgWu8KEBIGj9bZl*F{o`u|(4F%@&jg_a$2qD!-emO!;$|S_5D}!4U4>3Uc@i)-PA)dnFNet@|0M;AMzXa^-9W2 z!|;y$f1}A!VM`tf=xS=D=30I}O`U^I?ZyE#v?9nxpc1yg$iJZ1EW(z?=zn{_I!7cs zV=@CtFnhd{bg>)DD^9x27`|}IQF81da3@-NP7fO_o zZoe>Bd6@f7M%qM(yCOglU>}$!l1~_X%Ut7ili(KtijapFiVy+cGFPsU#jF5_peiE= zOl%a47c!Xf<`7f~8SBV8bO@@1{N>~`1XV)b7UUHJOY^3}13(cn@uZSOz_-jbJ7paF zB7nt8p;!^{Epts4(w=Y`{HYrN(&oC1chbya>K!2-EEAK&v^zpP$c%qwp_w`{gIXjO zWXHd<(ClOpYeYQAHcT_1NPJtZ`r~;$^PT*TL;vFd9F^kwYdeIEY!7%VG z`Tdie$sZJHq$ML{iDq=B=2hWK{iWmgA=~{(M^#7F@n<@p@fr$!hC)SCctlE=vu<#ba!XMp5zZM)~01XBZV4(J<{wjl{z^=dc z+&dJ-q3o=SVy`aG-1j|m?z!hR`jyjZry#v;`aJ%hUW)nyuB<0UFVNq1&=mC^B~SvL zppx`BP2;jDVH!7)Co|5FCp*rnopjOp+gfwU(b{KCkEgdes$;e!Nu^|fj?wl`_or{HzEG%tGB|F zy?C$`mlCl=kKl~aJlIrss`(BHlQh4(0_s&BSZ7HGkg zq2_3T`3%NhFLM}0mlkr;~z>8XnY zbC)z%DvgrSM0_C@kzyAm1b2h-;l;ZFWT`dYx%~K*?p$YXKKJU1N#^?$zHgP^RpfWc z{2qnhQ(*Ue3nM9wlkfo}3zYA>6LC>GO8_7|IccCD3nK{qmZ0z+HBBwj8EO{FdK#f7 z_br+P`VA^$+MwqMWu(y}BjIx>rwn7f3&@wD^A(z}kHXK|kug0VA2ACL0&&>ch^^8|WFg|AlF}=@j*ASd@@cV=-mJCe1XkQ)8!MiG+x`psfwz z%YNTJgb6j3PR<=#XeUIBNa+YlzakC{CDK#Tgm?%F>qCMtMeG2Or3#*5J>OveaUk!# z7Le=vl={9(Gs5Q>W;GVCrEz3qjfti%Y3AtMTr4GoS=0zaPzZp=z$nd3cwZFp8MQ$o z7-~)fR%k192H+>20+6NDU~6_nb$gc=N)*Gj5wOIl1i72cS2n;$_6#eY;U)7L?^?R> z)`8^%C5pDSsgAmLj=g;>XOFq}_? ziEaT7#!Hx?jHi*WTuK;PN%(?(2FGx{av>k(Y67c1gt6eF!#*40dd~aMk*skfFdsLD zT!0yJ1JEopv8WJFP0yR5_Fwro@BK0d2ZisYW?wNo+&FTxUiJ(r zo}pFGa1kHkh~gQ^KBw}&Rlcprx8(!6I4JOAD+{YTMv6N|Zv9B!F{bPoE3jka6www~ z`@TpI*jf zr1c_`VNkn3XG{+k4+01tBUcCvW_3MfOad!#S6SHEw%!IJg*NVi{y`$9hCbpE@nSP9 znvPI?6Qs z%c;a=IP|)KN;T&zIA0>Sy@rJ&$)J${^ z)IW%QE)lyBOK4`@4g&p*O&pDVDV`G0UKo3y9zc)bIaYl{Ot^=a(1nnh$Oz%yMQ90I zb?IY2^335Ar;nbDoII*=$*43n1NO*VA}$e8qVZ!tJU)E%|gjhja4`MI!} zNC90unZ@)x0s-bM;~63Apo16>*E$}0qlo1nRzejuq+2d!DTayOhR$)wf0Uwv=pKoxMcqz-IrmgR=T_cG=RYSUO9Ti?grQZCR~r zFV?l^yXCsDQWsw0RK5`SFYmh2AC`aAWUR}Xk!2~7Vf#v`| z;o`claNkSpMx)9wT97=PI#Cwmz_7XnDbUzv1o2*FNp6^D4;I2ki*kgzLQk@bAc$ux zmSY~98JiLq4hB_^QWD0foH8sJAb{6~VX?>=L>4z&5$wiw7R?#6NH33MxLND^YRFMt zo2{sw6nGYk7BDfYgy{MliW1n2=|k!xlfY%n8H?1k(Sz$o>;&^w&WHyMmbYM80A;jj zTK?Ndphu0sLJ?_0XHpV20m^AJZHzlFS~FIlz`sBAy=)nVycZ~vJhqrNCBXrHgSFOM zFz%AE3eKxFMi{&^Q+{XW3cYM5K7nftVTrh4TAb6&W5iEdFwOOAHV{N*7najZaRFf` zfe1P5pn=tj!nk17G_O~+HeHX-aSpiThQw%~o+&~z*nyEumqEwqCBjUwm4 z&v>cY-B|C(Zkf0pz#ricJdUlSkZ*S8W51gH9c=HNrh?fy| zYnE6dhGPQQs24724vfImOgteV&=`yqoz|@2nh={7;cGy2kFG(ELset3M}RtT57q$0 z8zEjNIsw(N4UOVaX8n~kcjc%ep!Z@IHD32+^a`hMd245T zMJqH3{TLq+d*R>&b#4W%)7X@KUafD)9#cI5#nY~Mx^+`p_B^I|9xG8TT+25Y5G}F% z#46uX@wboPZr+*uaiMu9WHq==4YsK*+txzc zS5Dv9FNY2(p+hC!?5PJ;;Hm$Hu{vGfQ2& zX4$z-ac(P74#2XZ?NeWV9&|wi}M`U~MRZuXH)nF$e^6|4DJgf7{ zEiXLft)mLS;HVNDg7%c$bFcyp!fD!?9l+&NvB|Cyk$NmSDd-k~QUSJc!aG`Jm zQ4L=~VM8_iHELmU5P}UAk&QzL;3d?Abrx9=x$FfR(aU}iUJq6qoGz3zEQs716=D#% zPY~f_)rF5{2N!QTCPh%}Mcp*x;>qZAY=Mm@r~AR{BeyI9Z%gD&nA2EbdAMCDmJm@F zv}+ca9+f0SE=`y#Ul6j2sPW`kz8nlpLU=8a15m~KE*u()PDv4c7MjNU;c?3V*14ZB zzPo98QE|7gx_gT59@*WixO+>KoeL1K#H{&(xl{S=H%zi`kK)_2WLNq6TyKHzf~-2c zs@3(5^KIwt#@^i7LSrvvwY8_jF_;Yie8X@y`*#!oU=jch%I3ZDvTXG)@k{&$TWB^M z-HOaXFcRUTxbHGr69$gKJ{RGb({Pdh56_d9DyG3^zi&NgRjYg!fnRViYg0RG2w~Mk zEi$GIlVJ^fSL0_Ie9#(xs_qk*2kmZDiPhsXHL9_?@B8pEt|+iqv9D|fFU4#Ot>G&Q zfiL>ob5JOU)_O@`1?roVP(zq6nlhjrHw+AL<{^}`QIFFLa9JS)*9TiOL=$UPq75RT zsqt+A{m8(TD62<}(^Uo5^kq*2wL*EJ=lncqB^)RvR%(RAtREDT`x%TDp8^01(jC|H zEnngEnbp&i#nY38bJOza8Rhg0^jzVt#Fe_X{Bt+B8^V`-p>s&C8>&nqGR?r8*t#RM zx&?(Yt zrRTx+aZIHX^U0K610nttjMbQA^dd?@$$<4=#*1&_>6jt2REe^=%vHceIqShWz0$aR zF!ywk?<|1H;iMY;IriN?)f>olzk7TQ`>grLuTIK=J|)nnjvo8{=AXWswPu+5*AnV|SVPyH42l?`X&cM!^wH(S2H8ZhFMI_*6i#VT5M$siu)mT;% zQxjniQ&+oBK?%BMaKk7DcrElQ1{nags@wY@hDVoSTye*<&y-9R#)1e$u;hm7H<;PJ!yr5Q=SIrECHKTH zF28$Ot@o?mAi%Zyw!B}i?^Wu1?`~<$U3mY%)dOpx4xR2dF2GanJf{E*MU+rvtukaw z>qnQt%na>QLi^+`gUXgc;4?2){{Wm02Qat}I5RmRjU}OFc&SRlrVLT=mp3W+jR?VK zFz9+^$|87S4~`k&&jz0}Mh_Z{w}*{pA2PbijHql{`-O`(C|-R2^~N`V?oE z?Ce&Y-EbLL`zqU1WSerw@6czB;#p5)K-PMus?}WEN+c9pX^rEtCVk$IaXF;fBas*9;RAat z5<&PZ&`$tB@B;oF0M+551>5k6_<8+!&C;bg_%Md>jYdDi0Pm?e@u?30Agn>K)9j>; zNL3OX&>Hjx@M8vm6ZGf}i7#ko`1G2D4GFhc@ORiaL~w@i@=s!bzr5*D2K>cL4?1A) zUzf&MkP@cR;2>~l0$zpaC;ahM|G;Yq3Koo6rNRYczD>0hjJaf{T>hn# z%i{(Ar0hJWIL~G6Dzuzmo-G7^B)iTit}|J_WTxr85Tq~wtC_|@aRaC~(a+N*Y7?|~ z;$cSc1Gq-x$r3 OzY;^S&CpD)*Z%^6PX9>& diff --git a/scripts/__pycache__/render_bid_docx.cpython-311.pyc b/scripts/__pycache__/render_bid_docx.cpython-311.pyc deleted file mode 100644 index 7ab22f913651f61ecb59a6c1b779a53e3d262623..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16084 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%0D$4fGq1yc);l=jpNEU!9Tjg||Uz2xIU6K>D6 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 diff --git a/scripts/__pycache__/render_docx.cpython-311.pyc b/scripts/__pycache__/render_docx.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ed0525884507ee89975f034b6aebcdfc842a45ef GIT binary patch literal 1511 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 Ci`NAJ diff --git a/scripts/__pycache__/scan_project_materials.cpython-311.pyc b/scripts/__pycache__/scan_project_materials.cpython-311.pyc deleted file mode 100644 index 414fc44e497cc3c02f26b74344eacc95c497337b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6713 zcmcgQTWlNGm3LKZ12C%a*O!@uOp$W@*h>rp<@; z&M=M?s$mNQRRT9wfOajr@|L>@QnmJ?-j4;;-3IXIeh!5xObj5vKpVUJwKSXpg}(OO zJJgUvCqc6xJGweN_uO;OeVy06zw-G!2)@63b}zZxkI)}!W%J`rgnM=ykef(C5|=`0 zZi3?|&ZqbZ9)5+Ckj4|3(QrykJ0=_q7gNr(Yr>UwPq@>b2~XNP;Y~M9G^KqLzO;YB z4?H2sa}7^4OJS)6egjg6)C#{XQm51ozd^ZG+H!@>fi&?*4t@%=xVt$w1pWEf*?)BF ze^Y9dGrWmRCaWgYWHzIWLmj`GP%pu+Fq)Z-I*q`0M$f%`_T=c9__5J*qi4Q*!f2B- zN{-0!Y)+lYsqs`|7BHhdsmdgtP5?|MQgJz>lBBE{o>Wp%;}?@Dz*|&Ur4+6|F()5&Li- z@b92E(M`~+>s#3X96dcTmAxvHs9^9zFBr}%^6b?tkqjrJ%927yjYJ9?d#5t7q#C=B zlw#}@$R1^iBxh9R`^r=z6Q3d3%kq?J9j7uhGrQp)o|ZH6yE9~DE&}H~btyiTP!p-_ z^w4lBJC#T&BSRL455`em0Pxp8qd%hp()_2OtV}Oly)pmc{0Hw}f4?}OHAhWyJ$yzN zY`CfuSE2%G1p=W5YX}L@NP$q<1_sqhlP!Q&*K~x0DBWk98R-TZwN>RjfWH<{*&n)* z{V-ed_vrqf!in`jq=I-c%)kO(_xkJSvf8$KSwDCZNG1O%9iaD=?mbmOoI6}@4K19e ze-#00kG#zb`w7(y!@Ie4Qj!o%DdG&+ld{1K;caSLlZIEx=E#&BC)upJ(MB!gZM$wy zPSJzf{OE$2R-Xawx{0RIypZQWOaEyetCqeYl*gB?nq3#?#k^3g)9_8f_CBh!o^F?5 zZ!6M;J@@62y^pVe5?8ac3|L*>qzZPb>a9%zR!^0LeRhcwUUu0j zU^2hV3t*GtxFLY58E{Ie#05DObs3mB9!w9-@DVu$j`1BiPOD@)2sZ+CXUE7!D^odH z+BAnamDt`$WbzF+bAfb-sFNut8K6QPiJ2KWBM}HDphDKDYL-ytHLwE89(9myP$4}O zu&FqStmIOvLUvImXVn@Nx(yNbnI_S!FqfR1Gkvc@!0n(Q{41*fz$W_o3n$8f_6qWe z9tOY?f;~n2iMtfstp|56ILof)8~zXdkHTHc?)C82rLkLXK(_Z4&pq6EU~T7t`&UXk zkLo**mbM?$w;wC_?ACkstoIG9cuRfz^}hY(gGaQ1SBh6xa-aOL_`?Sm%7@0Z=$Vz! z{g56#s6`L{SMPfF?v?%b4wkwP=-me@EpDH`f&e@fyl&4I2tcI{i2`=eazuQYT;i@|<0gt*yaX)VE2FYbeiueNdL1%Yx)u z7Qyu0;|8XQhT#TJ4Y4juXl$X@XqppNXJ_om1wDJ1C-;on4vp3IP6{OQ<0J|#zlNlW zbOEAJC5kW`+;9!266p(4Vq|Xngf>w;&MWiRbZJzD{-w5??I=tu!E zZ3nr1K(;A4RWvrKUO=dlLkiJEQwRyweC2OFdG2HWx(_DFk#7Mbc?rO;dD2F4Hdng1 zNOiKFVH%RAvgw(TxgB3K-A$%%LtkYF0EqLEADCsO@4=XM^1MD4hkCKiqMHj5jAoW+ z&;S^p%H}ev;bB>yi8ozJW>o4>X-iZznl^(fb)3u#8Xl{^V)((=npvlrXF8Q!I-P*? z^bo4AWQYQGvdq6alL|{hi5uvK15!@vdZ|+cN1G+8A*AF?R5ZQqG1_;Gwuv;Qoi#)@ zhC;K$dLP@cP2>!;?}UG40JH#n>_D_|s{E~3VSK%%zxZCMWuM-%4_ushiZkJYSat;; zx;oZe9go7@%O2`wU%us8_LO@Eeii$d*gx<2WRDg;y5v}L+<&j)#MD6ocq%xkg9K1v zz>m7N|MJl7L$?kuAJzh+i|vcfMd!bJRu7b37}Z}GrK4GRZTv!ij?83!NY&>9YDT_d#MPzfK_@nH=geiYuJhj*=qI+wm(3JvI?f%29} zxpUX&J-x-3ZoR+!{(5KkQl``y(>r79T|MTZ-yeeCQsf6Zz^;Vem4Ml|!E`C`dYT*+ zWCGD)W>A=A^{{o;N8J~=CGJ@-X!}i6@4#z0h26Q%)6Cy;)|MndByhR+gioREuGJ0> z_p7+E-5D`X%SG19Z5W?ZsdiPrJa^f;P1xmqxod8a#Y2B9HzWNGlC73+E;$oYTQ=ti zxnP5HmBg9{ksNd2k>-VZhug>x*=T#6G$D^0cUf}gvE-^r{RNWTcLd2Z%|nJN7VEOP znjFaseVS^0Bwr1O{{A}+0S`{-t0XlyND9~_wR{6f!3IgKe}kl^+L~&ZG2ssLa=IG5U^7zi;@E=m$%?T*@jtKcc81_os z;A0UM9OwARE<$B(a8Z^YkV8;WsFSQF24n>Axo|p>%+zNsn`yvnlzl${NCRAF&2qm_ zbH^5>#TT!q0D-b36<=Jg;*_$<$b3Jc(Kbh8Htg>0A3wfX%+Xv6w!qxQC%3Pe8)@4J z8Xnk2oCT!15q)2_5qIcD)IH#kCJA!=dJ5Yb0kU#&4rIX=?Sw6I5Gt7Ec4QE+9Q9L? zx2?0<;3k<`{GT@MO^~Hb0Vp5{i8PE`z=2xYDE1l&UcBc2$nD^*4*PU7;H`$N=qA@CmJwZ2{JvCAc}1G zN6B<94Oz~sv`UPoge1kGmC+0jW1@A%eAjSR4a}g|&}`}**$}|A2vtj#{SdlMX6|)@ zVpN$8@71JwDX!!$UQE7g;LF)$Ch9O8Of)et6HYI=hPV2-$gbgT%RG%HBF{jI7EdKJ zvhv(b7gN?A6*v&GKNc)_S~f zri=sCj4k};O2_?!di0Ps@?Aapsuq6JEG7Jwj^EPoTXn4GR!-he>Vrofbm@cRTKJq< zO85;OzoFqb>Nvl*GP~NL?>Y8hT;KD$7XF@DN_axY6B?edIG2J;=e6))3CDCC({Rjc zC^Iu(jXpS|zw{c|e93iIcbzp~(wep`3_leG(eVWWP+`D@#FpwY1-0(K)53d8*gB`` zS}VMz+z!;ytM%^H!}}n&bbFpsW&gr7B+(tNc#tmyWB8nO3;-1h{t`f9^TK-%oBP+A z`-{0!b4+iJt*F19{mtxZS8302eb4a{KB40i+Ga7+Q&${bMU$C$Jc`*%BzvWMf zR!8qxFdDrfnht}$tHK2~ot1Jad4!~(N^iDG6d*i*a$FhpYWDI7H5b@l8HF`_DWff# zy;Lyb#(1!V{}1R8xj+TkK_I}jR*)U^hdIy_J9x31dj+m*&%q-c!c7ZvYeI)6bd<5H VF!rP4*Nzw1UqwK;6Ub~U{}(18^@;!h diff --git a/scripts/__pycache__/search_docx_json.cpython-311.pyc b/scripts/__pycache__/search_docx_json.cpython-311.pyc deleted file mode 100644 index ecf9c7965dd287bddacdaad33968898173ff1835..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5173 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 diff --git a/scripts/__pycache__/write_large_json.cpython-311.pyc b/scripts/__pycache__/write_large_json.cpython-311.pyc deleted file mode 100644 index 6e4a681201a63fc29da29dfebef856a10f0f20aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2477 zcmbVNZ)_Ar6rbIl`@6ln-j)6p>48nD2-@rVytLUNKBMyLg15r+-o-JWs^%tq!qq3!4HH_zM1>eUW;FRJNxF% z%zJO%%>Lfn?>rti0_xuNqjDG{^cNjeh%X7;EXQ13!V=fQC4%O935-dVhG>*1YG7eD+qcyRDYe{kvZ^ACRi zWlwP2mDNv%cZW4)2(H>GtI3R{s6*LyFf8epG#}i)yfl4taq7a-&z~_5dx9Z{WtYbk zP1h`cR7uJG89kcGs?uwOWQc|7_p)rMOhVEu7m*WET$jgm%SX5Gm0=sXr09Mtk;<0j z5KkPiuO-N1NsU4%7<{@tsfHCjd_s}J3dh8&5Pa^qqXPuPGHdT z-UFuhz@LKQJoE%;S)l45Xw4AO^%r~U(Y<Vz(aR-t|Byf$soVt2!csy{Xf!E;!60d%Q}t`V z6`{bS=Gt~(IaCzeruXEx464D14r{XYT_serS=%~QIfI>RTDi0 zN3Xqr;p%17ruif-2dgi!;tOh`#7n{rJPYy9(K9gO{#)M$`u1n)SFI~vXul>7PIs-W zPH-!UU;E)^=q0bCHNN;YzGx~j`X9ao-CwZ=81WOrEZo*y^~TB&arBQn!eIv0quCBH zc=Kp9y!$1_!-H@1l}pWUQ>7a6@YkD*cg_KxoJ4*%s}NbT@F_VpGHwfp0pr;g@bLGw zpO$9MudY#LE0^VjOdm-QO}4x}WGI^kL=Mm)vFZ|%6o*wdXStaY9c!gugpEaLIf$%f zQt!!@0GlxYhOlL4%22MDdKJVP%?5yP8NWE4P*l#K#SMd}YmXy~B9r2wNL4@qmE`3Zj2O+&C|8%!`{$ zaZ|2$(cd^FT^Y{%H=F*=3;xi&Ka}@(nf|Wa!D66kO20DpN#sgoA+U8mur(jpW(Kz9 z4i)i+so2bOc^o!z*r+tJ3F5Sz@i>h>9uHwwUD=%wCoHjlX2@PjU3y@azpK`1g@QC^ zx=i+3?s)tpd@3jcaY6x3wopd@PnLsLtCVu0{9o43jKGhC?P1xInRGg%vRYB98Z`ge zh0^%cL>l~kk66I0Vmx)en$Aesl-x~T22GmA+8@B6dU9L=Z82)wedNorr+~y9dkUz< zsBHz*WYo6%sKcmj%XT|QYxz1J;JH4SU&q}}F0zbjSrfu_7sls>RzqklU}vuPgXp list[dict[str, Any]]: - if isinstance(spec, dict): - tables = spec.get("tables", []) - elif isinstance(spec, list): - tables = spec - else: - tables = [] - return [item for item in tables if isinstance(item, dict)] - - -def save_table(out_dir: Path, file_name: str, title: str, headers: list[str], rows: list[list[str]]) -> dict[str, Any]: - content = "\n".join([f"# {title}", "", markdown_table(headers, rows)]) - path = out_dir / file_name - write_text(path, content) - return { - "title": title, - "path": str(path), - "headers": headers, - "rows": rows, - } - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--spec", required=True) - parser.add_argument("--out", required=True) - args = parser.parse_args() - - out_dir = Path(args.out).resolve() - out_dir.mkdir(parents=True, exist_ok=True) - tables = normalize_spec(read_json(Path(args.spec).resolve())) - - manifest: list[dict[str, Any]] = [] - for index, table in enumerate(tables, start=1): - title = table.get("title") or f"表格{index}" - headers = table.get("headers") or [] - rows = table.get("rows") or [] - file_name = table.get("file_name") or f"table_{index}.md" - manifest.append(save_table(out_dir, file_name, title, headers, rows)) - - write_json(out_dir / "tables_manifest.json", manifest) - - -if __name__ == "__main__": - main() diff --git a/scripts/build_tech_diagrams.py b/scripts/build_tech_diagrams.py deleted file mode 100644 index 267e5dd..0000000 --- a/scripts/build_tech_diagrams.py +++ /dev/null @@ -1,94 +0,0 @@ -from __future__ import annotations - -import argparse -from pathlib import Path -from typing import Any - -import matplotlib.pyplot as plt -from matplotlib.font_manager import FontProperties -from matplotlib.patches import FancyArrowPatch, FancyBboxPatch - -from common import find_font_path, read_json, write_json - - -def get_font(size: int = 12) -> FontProperties | None: - font_path = find_font_path() - if font_path: - return FontProperties(fname=str(font_path), size=size) - return None - - -def add_box(ax, xy, width, height, text, facecolor, font): - patch = FancyBboxPatch( - xy, - width, - height, - boxstyle="round,pad=0.02,rounding_size=0.04", - facecolor=facecolor, - edgecolor="#1E3A8A", - linewidth=1.5, - ) - ax.add_patch(patch) - ax.text(xy[0] + width / 2, xy[1] + height / 2, text, ha="center", va="center", fontproperties=font, fontsize=12) - - -def add_arrow(ax, start, end): - ax.add_patch(FancyArrowPatch(start, end, arrowstyle="->", mutation_scale=16, linewidth=1.5, color="#1E3A8A")) - - -def normalize_spec(spec: Any) -> list[dict[str, Any]]: - if isinstance(spec, dict): - diagrams = spec.get("diagrams", []) - elif isinstance(spec, list): - diagrams = spec - else: - diagrams = [] - return [item for item in diagrams if isinstance(item, dict)] - - -def draw_diagram(diagram: dict[str, Any], out_path: Path) -> None: - font = get_font(11) - fig, ax = plt.subplots(figsize=(diagram.get("width", 10), diagram.get("height", 6))) - ax.axis("off") - - for box in diagram.get("boxes", []): - add_box( - ax, - tuple(box.get("xy", [0.1, 0.1])), - box.get("width", 0.2), - box.get("height", 0.14), - box.get("text", ""), - box.get("facecolor", "#DBEAFE"), - font, - ) - - for arrow in diagram.get("arrows", []): - add_arrow(ax, tuple(arrow.get("start", [0, 0])), tuple(arrow.get("end", [1, 1]))) - - title = diagram.get("title", out_path.stem) - ax.set_title(title, fontproperties=get_font(14)) - fig.savefig(out_path, dpi=180, bbox_inches="tight") - plt.close(fig) - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--spec", required=True) - parser.add_argument("--out", required=True) - args = parser.parse_args() - - out_dir = Path(args.out).resolve() - out_dir.mkdir(parents=True, exist_ok=True) - diagrams = normalize_spec(read_json(Path(args.spec).resolve())) - manifest: list[dict[str, Any]] = [] - for index, diagram in enumerate(diagrams, start=1): - file_name = diagram.get("file_name") or f"diagram_{index}.png" - path = out_dir / file_name - draw_diagram(diagram, path) - manifest.append({"title": diagram.get("title", path.stem), "path": str(path)}) - - write_json(out_dir / "diagram_manifest.json", manifest) - - -if __name__ == "__main__": - main() diff --git a/scripts/common.py b/scripts/common.py deleted file mode 100644 index 02c5e73..0000000 --- a/scripts/common.py +++ /dev/null @@ -1,247 +0,0 @@ -from __future__ import annotations - -import json -import os -import re -import tempfile -from pathlib import Path -from typing import Any - -import yaml - -REPO_ROOT = Path(__file__).resolve().parents[2] -INPUT_ROOT = REPO_ROOT / "input" -OUTPUT_ROOT = REPO_ROOT / "output" - -VALID_BUNDLES = ("technical", "business-other") -BUNDLE_ALIASES = { - "technical": "technical", - "business-other": "business-other", - "business_other": "business-other", -} -BUNDLE_DEFAULTS: dict[str, dict[str, str]] = { - "technical": { - "outline_json": "final_outline_technical.json", - "content_json": "final_bid_content_technical.json", - "outline_docx": "技术标_目录版.docx", - "bid_docx": "技术标.docx", - "outline_doc_title": "技术标(目录版)", - "outline_toc_title": "目录", - "bid_doc_title": "技术标", - "bid_toc_title": "目录", - }, - "business-other": { - "outline_json": "final_outline_business_other.json", - "content_json": "final_bid_content_business_other.json", - "outline_docx": "商务及其他_目录版.docx", - "bid_docx": "商务及其他.docx", - "outline_doc_title": "商务及其他(目录版)", - "outline_toc_title": "目录", - "bid_doc_title": "商务及其他", - "bid_toc_title": "目录", - }, -} - -BANNED_WORDS = ["可能", "大概", "应该", "我觉得", "AI建议", "待确认"] - -# Weak filename hints only. These hints may help AI label discovered files, -# but they must never be treated as workflow routing, directory semantics, -# or mandatory material categories. -MATERIAL_CATALOG = [ - {"key": "business_license", "label": "营业执照副本", "keywords": ["营业执照", "license"]}, - {"key": "qualification_certificate", "label": "资质证书", "keywords": ["资质", "证书", "许可", "qualification"]}, - {"key": "legal_representative_id", "label": "法定代表人身份证明", "keywords": ["法人", "法定代表人", "身份证明"]}, - {"key": "authorization_letter", "label": "授权委托书", "keywords": ["授权", "委托书", "authorization"]}, - {"key": "project_manager_certificate", "label": "项目经理证书", "keywords": ["项目经理", "pmp", "建造师"]}, - {"key": "similar_project_case", "label": "类似项目业绩证明", "keywords": ["业绩", "案例", "合同", "验收", "case"]}, - {"key": "quotation_basis", "label": "报价依据说明", "keywords": ["报价", "清单", "预算", "quote", "price"]}, -] - -RESERVED_PROJECT_DIRS = { - "rfp", - "work", - "reports", - "final", - "__pycache__", - ".git", - ".hg", - ".svn", - ".idea", - ".vscode", - ".venv", - "venv", - "node_modules", -} - - -def ensure_dir(path: Path) -> Path: - path.mkdir(parents=True, exist_ok=True) - return path - - -def write_text(path: Path, text: str) -> None: - ensure_dir(path.parent) - path.write_text(text, encoding="utf-8", newline="\n") - - -def write_json(path: Path, data: Any) -> None: - ensure_dir(path.parent) - path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") - - -def write_json_atomic(path: Path, data: Any, *, indent: int = 2, ensure_ascii: bool = False) -> None: - ensure_dir(path.parent) - temp_path: Path | None = None - encoder = json.JSONEncoder(ensure_ascii=ensure_ascii, indent=indent) - try: - with tempfile.NamedTemporaryFile( - mode="w", - encoding="utf-8", - newline="\n", - dir=str(path.parent), - prefix=f"{path.stem}.", - suffix=".tmp", - delete=False, - ) as temp_file: - temp_path = Path(temp_file.name) - for chunk in encoder.iterencode(data): - temp_file.write(chunk) - temp_file.flush() - os.fsync(temp_file.fileno()) - temp_path.replace(path) - except Exception: - if temp_path and temp_path.exists(): - temp_path.unlink(missing_ok=True) - raise - - -def read_json(path: Path) -> Any: - return json.loads(path.read_text(encoding="utf-8-sig")) - - -def load_yaml(path: Path) -> dict[str, Any]: - if not path.exists(): - return {} - data = yaml.safe_load(path.read_text(encoding="utf-8-sig")) - return data if isinstance(data, dict) else {} - - -def normalize_text(text: str) -> str: - return re.sub(r"\s+", " ", text or "").strip() - - -def normalize_bundle(bundle: str | None) -> str | None: - if bundle is None: - return None - normalized = BUNDLE_ALIASES.get(bundle.strip()) - if normalized: - return normalized - raise ValueError(f"不支持的 bundle: {bundle}。允许值:{', '.join(VALID_BUNDLES)}") - - -def ensure_output_layout(project_dir: Path) -> dict[str, Path]: - output_root = project_dir - layout = { - "root": output_root, - "final": output_root / "final", - "artifacts": output_root / "work", - "tables": output_root / "work", - "reports": output_root / "reports", - "work": output_root / "work", - } - for path in layout.values(): - ensure_dir(path) - return layout - - -def get_bundle_defaults(bundle: str) -> dict[str, str]: - normalized = normalize_bundle(bundle) - if normalized is None: - raise ValueError("bundle 不能为空。") - return BUNDLE_DEFAULTS[normalized] - - -def get_bundle_outline_path(output_layout: dict[str, Path], bundle: str) -> Path: - return output_layout["work"] / get_bundle_defaults(bundle)["outline_json"] - - -def get_bundle_content_path(output_layout: dict[str, Path], bundle: str) -> Path: - return output_layout["work"] / get_bundle_defaults(bundle)["content_json"] - - -def get_bundle_outline_docx_path(output_layout: dict[str, Path], bundle: str) -> Path: - return output_layout["final"] / get_bundle_defaults(bundle)["outline_docx"] - - -def get_bundle_bid_docx_path(output_layout: dict[str, Path], bundle: str) -> Path: - return output_layout["final"] / get_bundle_defaults(bundle)["bid_docx"] - - -def find_rfp_docx(project_dir: Path) -> Path: - rfp_dir = project_dir / "rfp" - if not rfp_dir.exists(): - raise FileNotFoundError(f"未找到招标文件目录: {rfp_dir}") - docx_files = sorted(rfp_dir.glob("*.docx")) - if not docx_files: - raise FileNotFoundError(f"未找到 DOCX 招标文件: {rfp_dir}") - return docx_files[0] - - -def get_project_config(project_dir: Path) -> dict[str, Any]: - return load_yaml(project_dir / "config" / "project.yaml") - - -def is_reserved_project_entry(path: Path) -> bool: - return path.name.lower() in RESERVED_PROJECT_DIRS - - -def is_hidden_project_entry(path: Path) -> bool: - return path.name.startswith(".") - - -def iter_material_entries(project_dir: Path) -> list[Path]: - if not project_dir.exists(): - return [] - entries: list[Path] = [] - for entry in sorted(project_dir.iterdir()): - if is_reserved_project_entry(entry) or is_hidden_project_entry(entry): - continue - entries.append(entry) - return entries - - -def safe_filename(name: str) -> str: - return re.sub(r'[<>:"/\\\\|?*]+', "_", name).strip(" .") or "untitled" - - -def markdown_table(headers: list[str], rows: list[list[str]]) -> str: - lines = [ - "| " + " | ".join(headers) + " |", - "| " + " | ".join(["---"] * len(headers)) + " |", - ] - for row in rows: - lines.append("| " + " | ".join(row) + " |") - return "\n".join(lines) - - -def get_font_candidates() -> list[Path]: - windir = Path("C:/Windows/Fonts") - return [ - windir / "msyh.ttc", - windir / "msyhbd.ttc", - windir / "simhei.ttf", - windir / "simsun.ttc", - ] - - -def find_font_path() -> Path | None: - for path in get_font_candidates(): - if path.exists(): - return path - return None - - -def list_files(path: Path) -> list[Path]: - if not path.exists(): - return [] - return [item for item in sorted(path.rglob("*")) if item.is_file()] diff --git a/scripts/compose_bid_docx.py b/scripts/compose_bid_docx.py deleted file mode 100644 index 5b3f1fc..0000000 --- a/scripts/compose_bid_docx.py +++ /dev/null @@ -1,35 +0,0 @@ -from __future__ import annotations - -import argparse -from pathlib import Path - -from common import ensure_output_layout, get_bundle_bid_docx_path, get_bundle_content_path, normalize_bundle -from render_bid_docx import build_docx -from common import read_json - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--project", required=True) - parser.add_argument("--content") - parser.add_argument("--out") - parser.add_argument("--bundle") - args = parser.parse_args() - - project_dir = Path(args.project).resolve() - output_layout = ensure_output_layout(project_dir) - bundle = normalize_bundle(args.bundle) - content_path = Path(args.content).resolve() if args.content else ( - get_bundle_content_path(output_layout, bundle) if bundle else output_layout["work"] / "final_bid_content.json" - ) - if not content_path.exists(): - raise FileNotFoundError(f"未找到正文事实源: {content_path}。正文节点内容应由 AI 按已定稿目录填写,然后再调用本脚本渲染。") - - out_path = Path(args.out).resolve() if args.out else ( - get_bundle_bid_docx_path(output_layout, bundle) if bundle else output_layout["final"] / "投标文件.docx" - ) - build_docx(read_json(content_path), out_path) - - -if __name__ == "__main__": - main() diff --git a/scripts/docx_cli.py b/scripts/docx_cli.py new file mode 100644 index 0000000..aa4f4c2 --- /dev/null +++ b/scripts/docx_cli.py @@ -0,0 +1,98 @@ +from __future__ import annotations + +import argparse +from pathlib import Path + +from docx_ops_lib import ( + apply_patch_document, + create_docx_document, + export_outline_artifacts, + index_document, + query_nodes, + read_json, + render_docx, + write_json, +) +from outline_check import check_outline + + +def main() -> None: + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(dest="command", required=True) + + index_parser = subparsers.add_parser("index") + index_parser.add_argument("--docx", required=True) + index_parser.add_argument("--out", required=True) + + query_parser = subparsers.add_parser("query") + query_parser.add_argument("--docx", required=True) + query_parser.add_argument("--query-file", required=True) + query_parser.add_argument("--out", required=True) + + create_parser = subparsers.add_parser("create") + create_parser.add_argument("--spec-file", required=True) + create_parser.add_argument("--report", required=True) + + check_parser = subparsers.add_parser("outline-check") + check_parser.add_argument("--outline-file", required=True) + check_parser.add_argument("--report", required=True) + + export_parser = subparsers.add_parser("outline-export") + export_parser.add_argument("--spec-file", required=True) + export_parser.add_argument("--report", required=True) + + patch_parser = subparsers.add_parser("patch") + patch_parser.add_argument("--patch-file", required=True) + patch_parser.add_argument("--report", required=True) + patch_parser.add_argument("--render-check", action="store_true") + patch_parser.add_argument("--render-dir") + + render_parser = subparsers.add_parser("render") + render_parser.add_argument("--docx", required=True) + render_parser.add_argument("--out-dir", required=True) + render_parser.add_argument("--report") + + args = parser.parse_args() + + if args.command == "index": + write_json(Path(args.out).resolve(), index_document(Path(args.docx).resolve())) + return + + if args.command == "query": + index_data = index_document(Path(args.docx).resolve()) + query_data = read_json(Path(args.query_file).resolve()) + write_json(Path(args.out).resolve(), query_nodes(index_data, query_data)) + return + + if args.command == "create": + spec_data = read_json(Path(args.spec_file).resolve()) + write_json(Path(args.report).resolve(), create_docx_document(spec_data)) + return + + if args.command == "outline-check": + outline_data = read_json(Path(args.outline_file).resolve()) + write_json(Path(args.report).resolve(), check_outline(outline_data)) + return + + if args.command == "outline-export": + export_data = read_json(Path(args.spec_file).resolve()) + write_json(Path(args.report).resolve(), export_outline_artifacts(export_data)) + return + + if args.command == "patch": + patch_data = read_json(Path(args.patch_file).resolve()) + report = apply_patch_document(patch_data) + if args.render_check: + output_docx = Path(report["output_docx"]).resolve() + render_dir = Path(args.render_dir).resolve() if args.render_dir else output_docx.parent / f"{output_docx.stem}_render" + report["render"] = render_docx(output_docx, render_dir) + write_json(Path(args.report).resolve(), report) + return + + report = render_docx(Path(args.docx).resolve(), Path(args.out_dir).resolve()) + if args.report: + write_json(Path(args.report).resolve(), report) + + +if __name__ == "__main__": + main() diff --git a/scripts/docx_create.py b/scripts/docx_create.py new file mode 100644 index 0000000..9560d0e --- /dev/null +++ b/scripts/docx_create.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +import argparse +from pathlib import Path + +from docx_ops_lib import create_docx_document, read_json, write_json + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--spec-file", required=True) + parser.add_argument("--report", required=True) + args = parser.parse_args() + + spec_data = read_json(Path(args.spec_file).resolve()) + report = create_docx_document(spec_data) + write_json(Path(args.report).resolve(), report) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_toc_only_docx.py b/scripts/docx_index.py similarity index 55% rename from scripts/generate_toc_only_docx.py rename to scripts/docx_index.py index d3c9962..0dc54f3 100644 --- a/scripts/generate_toc_only_docx.py +++ b/scripts/docx_index.py @@ -3,17 +3,16 @@ from __future__ import annotations import argparse from pathlib import Path -from common import read_json -from render_outline_docx import build_docx +from docx_ops_lib import index_document, write_json def main() -> None: parser = argparse.ArgumentParser() - parser.add_argument("--outline", required=True) + parser.add_argument("--docx", required=True) parser.add_argument("--out", required=True) args = parser.parse_args() - build_docx(read_json(Path(args.outline).resolve()), Path(args.out).resolve()) + write_json(Path(args.out).resolve(), index_document(Path(args.docx).resolve())) if __name__ == "__main__": diff --git a/scripts/docx_ops_lib.py b/scripts/docx_ops_lib.py new file mode 100644 index 0000000..df071d7 --- /dev/null +++ b/scripts/docx_ops_lib.py @@ -0,0 +1,853 @@ +from __future__ import annotations + +import json +import re +import shutil +import subprocess +from dataclasses import dataclass +from hashlib import sha1 +from pathlib import Path +from typing import Any, Iterator + +from docx import Document +from docx.document import Document as DocxDocument +from docx.oxml import OxmlElement +from docx.table import Table, _Cell +from docx.text.paragraph import Paragraph + +try: + from pdf2image import convert_from_path +except ImportError: # pragma: no cover + convert_from_path = None + +try: + from docx.oxml.ns import qn +except ImportError: # pragma: no cover + qn = None + +NAMESPACES = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"} +TEXT_WINDOW_DEFAULT = 40 + + +@dataclass +class NodeRecord: + node_id: str + node_type: str + text: str + style_name: str | None + heading_level: int | None + path: list[str] + ordinal: int + parent_id: str | None + anchor: str + container: str + table_index: int | None = None + row_index: int | None = None + cell_index: int | None = None + block_index: int | None = None + xml_path: str | None = None + has_image: bool = False + object_ref: Any = None + + def to_dict(self) -> dict[str, Any]: + return { + "node_id": self.node_id, + "node_type": self.node_type, + "text": self.text, + "style_name": self.style_name, + "heading_level": self.heading_level, + "path": self.path, + "ordinal": self.ordinal, + "parent_id": self.parent_id, + "anchor": self.anchor, + "container": self.container, + "table_index": self.table_index, + "row_index": self.row_index, + "cell_index": self.cell_index, + "block_index": self.block_index, + "xml_path": self.xml_path, + "has_image": self.has_image, + } + + +class QueryError(RuntimeError): + pass + + +def read_json(path: Path) -> Any: + with path.open("r", encoding="utf-8-sig") as handle: + return json.load(handle) + + +def write_json(path: Path, payload: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8", newline="\n") as handle: + json.dump(payload, handle, ensure_ascii=False, indent=2) + handle.write("\n") + + +def heading_level_for_style(style_name: str | None) -> int | None: + if not style_name: + return None + compact_style = normalize_text(style_name) + match = re.match(r"Heading\s+(\d+)$", compact_style, flags=re.IGNORECASE) + if match: + return int(match.group(1)) + match = re.match(r"标题\s*(\d+)$", compact_style) + return int(match.group(1)) if match else None + + +def normalize_text(value: str) -> str: + return re.sub(r"\s+", " ", value or "").strip() + + +def create_docx_document(spec_data: dict[str, Any]) -> dict[str, Any]: + output_docx = Path(spec_data["output_docx"]).resolve() + blocks = spec_data.get("blocks", []) + if not isinstance(blocks, list): + raise QueryError("blocks must be a list") + + output_docx.parent.mkdir(parents=True, exist_ok=True) + document = Document() + title = spec_data.get("title") + if title: + document.core_properties.title = str(title) + + block_reports: list[dict[str, Any]] = [] + + def render_block(block: dict[str, Any], index_path: list[int]) -> None: + if not isinstance(block, dict): + raise QueryError(f"block {'.'.join(str(part) for part in index_path)} must be an object") + block_type = block.get("type", "paragraph") + if block_type == "heading": + level = int(block.get("level", 1)) + if level < 1 or level > 9: + raise QueryError(f"block {'.'.join(str(part) for part in index_path)} heading level must be between 1 and 9") + text = str(block.get("text", "")) + paragraph = document.add_paragraph(style=f"Heading {level}") + paragraph.add_run(text) + block_reports.append({"index": ".".join(str(part) for part in index_path), "type": block_type, "text": summarize_text(text), "level": level}) + children = block.get("children", []) + if children and not isinstance(children, list): + raise QueryError(f"block {'.'.join(str(part) for part in index_path)} children must be a list") + if isinstance(children, list): + for child_index, child in enumerate(children): + render_block(child, index_path + [child_index]) + return + if block_type == "paragraph": + text = str(block.get("text", "")) + paragraph = document.add_paragraph() + style_name = block.get("style") + if style_name: + try: + paragraph.style = str(style_name) + except KeyError: + pass + paragraph.add_run(text) + block_reports.append({"index": ".".join(str(part) for part in index_path), "type": block_type, "text": summarize_text(text)}) + return + if block_type == "list": + items = block.get("items", []) + if not isinstance(items, list): + raise QueryError(f"block {'.'.join(str(part) for part in index_path)} items must be a list") + style_name = str(block.get("style", "List Bullet")) + for item in items: + paragraph = document.add_paragraph() + try: + paragraph.style = style_name + except KeyError: + pass + paragraph.add_run(str(item)) + block_reports.append({"index": ".".join(str(part) for part in index_path), "type": block_type, "item_count": len(items)}) + return + if block_type == "table": + rows = block.get("rows", []) + if not isinstance(rows, list) or not rows or not isinstance(rows[0], list) or not rows[0]: + raise QueryError(f"block {'.'.join(str(part) for part in index_path)} rows must be a non-empty 2D list") + table = document.add_table(rows=0, cols=len(rows[0])) + style_name = block.get("style") + if style_name: + try: + table.style = str(style_name) + except KeyError: + pass + for row_values in rows: + if not isinstance(row_values, list) or len(row_values) != len(rows[0]): + raise QueryError(f"block {'.'.join(str(part) for part in index_path)} table rows must have equal column counts") + row = table.add_row() + for cell_index, value in enumerate(row_values): + row.cells[cell_index].text = str(value) + block_reports.append({"index": ".".join(str(part) for part in index_path), "type": block_type, "row_count": len(rows), "column_count": len(rows[0])}) + return + if block_type == "page_break": + document.add_page_break() + block_reports.append({"index": ".".join(str(part) for part in index_path), "type": block_type}) + return + raise QueryError(f"unsupported block type: {block_type}") + + for index, block in enumerate(blocks): + render_block(block, [index]) + document.save(str(output_docx)) + final_index = index_document(output_docx) + return { + "status": "ok", + "output_docx": str(output_docx), + "block_count": len(blocks), + "blocks": block_reports, + "final_summary": final_index["summary"], + } + + +def export_outline_artifacts(payload: dict[str, Any]) -> dict[str, Any]: + technical_outline = payload.get("technical_outline") + business_outline = payload.get("business_outline") + technical_json = Path(payload["technical_outline_json"]).resolve() + business_json = Path(payload["business_outline_json"]).resolve() + technical_docx = Path(payload["technical_docx"]).resolve() + business_docx = Path(payload["business_docx"]).resolve() + + for outline_name, outline in (("technical_outline", technical_outline), ("business_outline", business_outline)): + if not isinstance(outline, dict): + raise QueryError(f"{outline_name} must be an object") + if not isinstance(outline.get("blocks"), list): + raise QueryError(f"{outline_name}.blocks must be a list") + + write_json(technical_json, technical_outline) + write_json(business_json, business_outline) + + technical_report = create_docx_document( + { + "output_docx": str(technical_docx), + "title": str(technical_outline.get("title", "技术标目录")), + "blocks": technical_outline["blocks"], + } + ) + business_report = create_docx_document( + { + "output_docx": str(business_docx), + "title": str(business_outline.get("title", "商务及其他目录")), + "blocks": business_outline["blocks"], + } + ) + + return { + "status": "ok", + "technical_outline_json": str(technical_json), + "business_outline_json": str(business_json), + "technical_docx": str(technical_docx), + "business_docx": str(business_docx), + "technical_report": technical_report, + "business_report": business_report, + } + + +def slugify_text(value: str, *, limit: int = 32) -> str: + compact = normalize_text(value) + if not compact: + return "empty" + compact = re.sub(r"[^\w\u4e00-\u9fff-]+", "-", compact, flags=re.UNICODE) + compact = re.sub(r"-+", "-", compact).strip("-").lower() + return compact[:limit] or "empty" + + +def summarize_text(value: str, *, limit: int = 80) -> str: + return normalize_text(value)[:limit] + + +def iter_block_items(parent: DocxDocument | _Cell) -> Iterator[Paragraph | Table]: + parent_element = parent.element.body if isinstance(parent, DocxDocument) else parent._tc + for child in parent_element.iterchildren(): + if child.tag.endswith("}p"): + yield Paragraph(child, parent) + elif child.tag.endswith("}tbl"): + yield Table(child, parent) + + +def paragraph_has_image(paragraph: Paragraph) -> bool: + return bool(paragraph._element.xpath(".//w:drawing")) + + +def paragraph_is_list_item(paragraph: Paragraph) -> bool: + style_name = paragraph.style.name if paragraph.style else "" + if style_name.lower().startswith("list"): + return True + p_pr = paragraph._element.pPr + return p_pr is not None and p_pr.numPr is not None + + +def build_anchor(path: list[str], node_type: str, text: str, ordinal: int) -> str: + seed = "|".join(["/".join(path), node_type, summarize_text(text, limit=32), str(ordinal)]) + digest = sha1(seed.encode("utf-8")).hexdigest()[:10] + slug = slugify_text(text, limit=24) + path_slug = slugify_text("-".join(path), limit=24) + return f"{path_slug}:{node_type}:{slug}:{ordinal}:{digest}" + + +def _index_document_core(document: Document) -> list[NodeRecord]: + nodes: list[NodeRecord] = [] + heading_stack: list[str] = [] + heading_ids: dict[int, str] = {} + ordinal = 0 + + def current_parent_id() -> str | None: + if not heading_ids: + return None + return heading_ids[max(heading_ids)] + + def add_record( + *, + node_type: str, + text: str, + style_name: str | None, + heading_level: int | None, + path: list[str], + parent_id: str | None, + container: str, + object_ref: Any, + table_index: int | None = None, + row_index: int | None = None, + cell_index: int | None = None, + block_index: int | None = None, + xml_path: str | None = None, + has_image: bool = False, + ) -> NodeRecord: + nonlocal ordinal + ordinal += 1 + node_id = f"n-{ordinal:05d}" + record = NodeRecord( + node_id=node_id, + node_type=node_type, + text=normalize_text(text), + style_name=style_name, + heading_level=heading_level, + path=path, + ordinal=ordinal, + parent_id=parent_id, + anchor=build_anchor(path, node_type, text, ordinal), + container=container, + table_index=table_index, + row_index=row_index, + cell_index=cell_index, + block_index=block_index, + xml_path=xml_path, + has_image=has_image, + object_ref=object_ref, + ) + nodes.append(record) + return record + + for block_index, block in enumerate(iter_block_items(document)): + if isinstance(block, Paragraph): + text = normalize_text(block.text) + style_name = block.style.name if block.style else None + level = heading_level_for_style(style_name) + if level is not None: + while len(heading_stack) >= level: + heading_stack.pop() + heading_stack.append(text or f"Heading {level}") + heading_ids = {key: value for key, value in heading_ids.items() if key < level} + record = add_record( + node_type="heading", + text=text, + style_name=style_name, + heading_level=level, + path=list(heading_stack), + parent_id=heading_ids.get(level - 1), + container="document", + object_ref=block, + block_index=block_index, + has_image=paragraph_has_image(block), + ) + heading_ids[level] = record.node_id + if record.has_image: + add_record( + node_type="image_placeholder", + text=text or "[image]", + style_name=style_name, + heading_level=level, + path=list(heading_stack), + parent_id=record.node_id, + container="document", + object_ref=block, + block_index=block_index, + has_image=True, + ) + continue + record = add_record( + node_type="list_item" if paragraph_is_list_item(block) else "paragraph", + text=text, + style_name=style_name, + heading_level=None, + path=list(heading_stack), + parent_id=current_parent_id(), + container="document", + object_ref=block, + block_index=block_index, + has_image=paragraph_has_image(block), + ) + if record.has_image: + add_record( + node_type="image_placeholder", + text=text or "[image]", + style_name=style_name, + heading_level=None, + path=list(heading_stack), + parent_id=record.node_id, + container="document", + object_ref=block, + block_index=block_index, + has_image=True, + ) + else: + table_text = "\n".join( + " | ".join(normalize_text(cell.text) for cell in row.cells) + for row in block.rows + ) + table_record = add_record( + node_type="table", + text=table_text, + style_name=block.style.name if block.style else None, + heading_level=None, + path=list(heading_stack), + parent_id=current_parent_id(), + container="document", + object_ref=block, + block_index=block_index, + xml_path=f"table[{block_index}]", + ) + for row_index, row in enumerate(block.rows): + row_text = " | ".join(normalize_text(cell.text) for cell in row.cells) + row_record = add_record( + node_type="table_row", + text=row_text, + style_name=table_record.style_name, + heading_level=None, + path=list(heading_stack), + parent_id=table_record.node_id, + container="table", + object_ref=row, + table_index=block_index, + row_index=row_index, + xml_path=f"table[{block_index}]/row[{row_index}]", + ) + for cell_index, cell in enumerate(row.cells): + add_record( + node_type="table_cell", + text="\n".join( + normalize_text(paragraph.text) + for paragraph in cell.paragraphs + if normalize_text(paragraph.text) + ), + style_name=None, + heading_level=None, + path=list(heading_stack), + parent_id=row_record.node_id, + container="table", + object_ref=cell, + table_index=block_index, + row_index=row_index, + cell_index=cell_index, + xml_path=f"table[{block_index}]/row[{row_index}]/cell[{cell_index}]", + ) + return nodes + + +def index_document(docx_path: Path) -> dict[str, Any]: + document = Document(str(docx_path)) + nodes = _index_document_core(document) + return { + "status": "ok", + "docx": str(docx_path), + "summary": { + "node_count": len(nodes), + "heading_count": sum(1 for node in nodes if node.node_type == "heading"), + "paragraph_count": sum(1 for node in nodes if node.node_type == "paragraph"), + "list_item_count": sum(1 for node in nodes if node.node_type == "list_item"), + "table_count": sum(1 for node in nodes if node.node_type == "table"), + "image_placeholder_count": sum(1 for node in nodes if node.node_type == "image_placeholder"), + }, + "nodes": [node.to_dict() for node in nodes], + } + + +def query_nodes(index_data: dict[str, Any], query: dict[str, Any]) -> dict[str, Any]: + nodes = index_data.get("nodes", []) + mode = query.get("match_mode", "contains_text") + value = query.get("value") + if value is None and mode not in {"node_type"}: + raise QueryError("query.value is required") + node_type_filter = query.get("node_type") + style_name_filter = query.get("style_name") + heading_level = query.get("heading_level") + allow_multiple = bool(query.get("allow_multiple", False)) + occurrence = query.get("occurrence") + window = int(query.get("context_window", TEXT_WINDOW_DEFAULT)) + + def node_matches(node: dict[str, Any]) -> bool: + if node_type_filter and node.get("node_type") != node_type_filter: + return False + if style_name_filter and node.get("style_name") != style_name_filter: + return False + if heading_level is not None and node.get("heading_level") != heading_level: + return False + node_text = node.get("text", "") + if mode == "exact_text": + return node_text == value + if mode == "contains_text": + return value in node_text + if mode == "regex": + return re.search(value, node_text) is not None + if mode == "heading_path": + return node.get("node_type") == "heading" and " > ".join(node.get("path", [])) == value + if mode == "heading_text": + return node.get("node_type") == "heading" and node_text == value + if mode == "table_title": + path_parts = node.get("path", []) + return node.get("node_type") == "table" and bool(path_parts) and path_parts[-1] == value + if mode == "style_name": + return node.get("style_name") == value + if mode == "node_type": + return node.get("node_type") == query.get("value") + if mode == "anchor": + return node.get("anchor") == value + if mode == "node_id": + return node.get("node_id") == value + raise QueryError(f"unsupported match_mode: {mode}") + + matches = [node for node in nodes if node_matches(node)] + if occurrence is not None: + matches = [matches[occurrence]] if 0 <= occurrence < len(matches) else [] + ambiguous = len(matches) > 1 and not allow_multiple + best_match = matches[0] if len(matches) == 1 or (allow_multiple and matches) else None + + def with_context(node: dict[str, Any]) -> dict[str, Any]: + text = node.get("text", "") + return { + **node, + "context": { + "before": text[:window], + "after": text[-window:] if text else "", + }, + } + + return { + "status": "ok", + "query": query, + "match_count": len(matches), + "ambiguous": ambiguous, + "best_match": with_context(best_match) if best_match else None, + "candidate_anchors": [match["anchor"] for match in matches], + "matches": [with_context(match) for match in matches], + "errors": ["query matched multiple nodes"] if ambiguous else [], + "warnings": [], + } + + +def find_records(index_data: dict[str, Any], query: dict[str, Any]) -> list[dict[str, Any]]: + result = query_nodes(index_data, query) + if result["ambiguous"] and query.get("on_ambiguous", "error") == "error": + raise QueryError("query matched multiple nodes") + if result["match_count"] == 0 and query.get("on_missing", "error") == "error": + raise QueryError("query matched no nodes") + return result["matches"] + + +def clone_run_format(source_run: Any, target_run: Any) -> None: + target_run.bold = source_run.bold + target_run.italic = source_run.italic + target_run.underline = source_run.underline + target_run.font.name = source_run.font.name + target_run.font.size = source_run.font.size + if source_run.font.color and source_run.font.color.rgb: + target_run.font.color.rgb = source_run.font.color.rgb + if qn and source_run._element.rPr is not None and source_run._element.rPr.rFonts is not None: + east_asia = source_run._element.rPr.rFonts.get(qn("w:eastAsia")) + if east_asia: + target_run._element.get_or_add_rPr().rFonts.set(qn("w:eastAsia"), east_asia) + + +def clear_paragraph(paragraph: Paragraph) -> None: + p_element = paragraph._element + for child in list(p_element): + if child.tag.endswith("}r") or child.tag.endswith("}hyperlink"): + p_element.remove(child) + + +def replace_text_in_paragraph(paragraph: Paragraph, old_text: str, new_text: str) -> bool: + if old_text not in paragraph.text: + return False + for run in paragraph.runs: + if old_text in run.text: + run.text = run.text.replace(old_text, new_text, 1) + return True + existing_runs = list(paragraph.runs) + first_run = existing_runs[0] if existing_runs else paragraph.add_run() + clear_paragraph(paragraph) + new_run = paragraph.add_run(new_text) + if existing_runs: + clone_run_format(first_run, new_run) + return True + + +def delete_block(block: Paragraph | Table) -> None: + element = block._element + parent = element.getparent() + if parent is not None: + parent.remove(element) + + +def insert_paragraph_relative(target: Paragraph | Table, *, after: bool, style_name: str | None = None) -> Paragraph: + new_p = OxmlElement("w:p") + if after: + target._element.addnext(new_p) + else: + target._element.addprevious(new_p) + paragraph = Paragraph(new_p, target._parent) + if style_name: + try: + paragraph.style = style_name + except KeyError: + pass + return paragraph + + +def append_paragraph_contents(paragraph: Paragraph, text: str, source: Paragraph | None = None) -> None: + if source is not None and source.style is not None: + paragraph.style = source.style + paragraph.paragraph_format.left_indent = source.paragraph_format.left_indent + paragraph.paragraph_format.right_indent = source.paragraph_format.right_indent + paragraph.paragraph_format.first_line_indent = source.paragraph_format.first_line_indent + paragraph.paragraph_format.space_before = source.paragraph_format.space_before + paragraph.paragraph_format.space_after = source.paragraph_format.space_after + paragraph.paragraph_format.line_spacing = source.paragraph_format.line_spacing + paragraph.alignment = source.alignment + if source is not None and source.runs: + run = paragraph.add_run(text) + clone_run_format(source.runs[0], run) + else: + paragraph.add_run(text) + + +def create_table_after(target: Paragraph | Table, rows: list[list[str]], style_name: str | None = None) -> Table: + parent = target._parent + cols = len(rows[0]) if rows else 1 + table = parent.add_table(rows=0, cols=cols) + if style_name: + try: + table.style = style_name + except KeyError: + pass + for row_values in rows: + row = table.add_row() + for index, value in enumerate(row_values): + row.cells[index].text = value + target._element.addnext(table._element) + return table + + +def build_live_index(document: Document) -> tuple[dict[str, Any], dict[str, NodeRecord]]: + nodes = _index_document_core(document) + return { + "status": "ok", + "summary": {"node_count": len(nodes)}, + "nodes": [node.to_dict() for node in nodes], + }, {node.anchor: node for node in nodes} + + +def insert_blocks(record: NodeRecord, operation: dict[str, Any], *, after: bool) -> None: + content_type = operation.get("content_type", "paragraphs") + content = operation.get("content") + if record.node_type not in {"paragraph", "list_item", "heading", "table"}: + raise QueryError("insert operations only support block nodes") + target = record.object_ref + if content_type == "paragraphs": + paragraphs = content if isinstance(content, list) else [str(content)] + previous: Paragraph | Table = target + for index, paragraph_text in enumerate(paragraphs): + new_paragraph = insert_paragraph_relative( + previous, + after=after if index == 0 else True, + style_name=record.style_name if record.node_type in {"paragraph", "list_item"} else "Normal", + ) + source_paragraph = target if isinstance(target, Paragraph) and record.node_type in {"paragraph", "list_item"} else None + append_paragraph_contents(new_paragraph, str(paragraph_text), source=source_paragraph) + previous = new_paragraph + return + if content_type == "heading": + payload = content if isinstance(content, dict) else {"text": str(content)} + level = int(payload.get("level", record.heading_level or 1)) + new_paragraph = insert_paragraph_relative(target, after=after, style_name=f"Heading {level}") + append_paragraph_contents(new_paragraph, str(payload.get("text", "")), source=target if isinstance(target, Paragraph) else None) + try: + new_paragraph.style = f"Heading {level}" + except KeyError: + pass + return + if content_type == "list": + items = content if isinstance(content, list) else [] + previous: Paragraph | Table = target + for index, item in enumerate(items): + new_paragraph = insert_paragraph_relative( + previous, + after=after if index == 0 else True, + style_name="List Bullet", + ) + source_paragraph = target if isinstance(target, Paragraph) and record.node_type == "list_item" else None + append_paragraph_contents(new_paragraph, str(item), source=source_paragraph) + try: + new_paragraph.style = "List Bullet" + except KeyError: + pass + previous = new_paragraph + return + if content_type == "table": + rows = content.get("rows") if isinstance(content, dict) else content + if not isinstance(rows, list) or not rows: + raise QueryError("table content must provide rows") + style_name = None + if isinstance(target, Table) and target.style is not None: + style_name = target.style.name + create_table_after(target, rows, style_name=style_name) + return + raise QueryError(f"unsupported content_type: {content_type}") + + +def replace_block(record: NodeRecord, operation: dict[str, Any]) -> None: + target = record.object_ref + insert_blocks(record, operation, after=False) + delete_block(target) + + +def apply_patch_document(patch_data: dict[str, Any]) -> dict[str, Any]: + source_docx = Path(patch_data["source_docx"]).resolve() + output_docx = Path(patch_data.get("output_docx", source_docx)).resolve() + in_place = bool(patch_data.get("in_place", False)) + if not in_place and output_docx == source_docx: + raise QueryError("output_docx must differ from source_docx unless in_place is true") + output_docx.parent.mkdir(parents=True, exist_ok=True) + if not in_place: + shutil.copy2(source_docx, output_docx) + document = Document(str(output_docx)) + operations = patch_data.get("operations", []) + operation_reports: list[dict[str, Any]] = [] + + for index, operation in enumerate(operations): + live_index, record_map = build_live_index(document) + matches = find_records(live_index, operation.get("target", {})) + if len(matches) > 1 and operation.get("on_ambiguous", "error") == "error": + raise QueryError(f"operation {index} matched multiple nodes") + selected = matches if operation.get("allow_multiple") else matches[:1] + if not selected and operation.get("on_missing", "error") == "error": + raise QueryError(f"operation {index} matched no nodes") + affected: list[dict[str, Any]] = [] + for match in selected: + record = record_map[match["anchor"]] + before_summary = summarize_text(record.text) + op_name = operation["op"] + if op_name == "replace_text": + old_text = operation["old_text"] + new_text = operation["new_text"] + if record.node_type not in {"paragraph", "list_item", "heading"}: + raise QueryError("replace_text only supports paragraph-like nodes") + if not replace_text_in_paragraph(record.object_ref, old_text, new_text): + raise QueryError(f"text not found in node {record.anchor}") + elif op_name == "delete_node": + if record.node_type not in {"paragraph", "list_item", "heading", "table"}: + raise QueryError("delete_node only supports block nodes") + delete_block(record.object_ref) + elif op_name == "insert_before": + insert_blocks(record, operation, after=False) + elif op_name == "insert_after": + insert_blocks(record, operation, after=True) + elif op_name == "replace_node": + replace_block(record, operation) + else: + raise QueryError(f"unsupported op: {op_name}") + affected.append( + { + "anchor": record.anchor, + "node_type": record.node_type, + "before": before_summary, + "op": op_name, + } + ) + document.save(str(output_docx)) + operation_reports.append( + { + "index": index, + "op": operation["op"], + "match_count": len(selected), + "affected": affected, + } + ) + + document.save(str(output_docx)) + final_index = index_document(output_docx) + return { + "status": "ok", + "source_docx": str(source_docx), + "output_docx": str(output_docx), + "in_place": in_place, + "operation_count": len(operations), + "operations": operation_reports, + "errors": [], + "warnings": [], + "final_summary": final_index["summary"], + } + + +def render_docx(docx_path: Path, out_dir: Path) -> dict[str, Any]: + out_dir.mkdir(parents=True, exist_ok=True) + pdf_path = out_dir / f"{docx_path.stem}.pdf" + png_dir = out_dir / "pages" + png_dir.mkdir(parents=True, exist_ok=True) + soffice = shutil.which("soffice") + if not soffice: + return { + "status": "render_skipped", + "docx": str(docx_path), + "pdf": None, + "page_count": 0, + "images": [], + "errors": [], + "warnings": ["LibreOffice/soffice not found"], + } + process = subprocess.run( + [soffice, "--headless", "--convert-to", "pdf", "--outdir", str(out_dir), str(docx_path)], + capture_output=True, + text=True, + encoding="utf-8", + ) + if process.returncode != 0 or not pdf_path.exists(): + return { + "status": "error", + "docx": str(docx_path), + "pdf": str(pdf_path), + "page_count": 0, + "images": [], + "errors": [process.stderr.strip() or "failed to convert docx to pdf"], + "warnings": [], + } + + images: list[str] = [] + warnings: list[str] = [] + if convert_from_path is None: + warnings.append("pdf2image not installed") + else: + try: + for page_number, image in enumerate(convert_from_path(str(pdf_path)), start=1): + image_path = png_dir / f"page-{page_number:03d}.png" + image.save(str(image_path), "PNG") + images.append(str(image_path)) + except Exception as exc: # pragma: no cover + warnings.append(f"PNG render skipped: {exc}") + + return { + "status": "ok", + "docx": str(docx_path), + "pdf": str(pdf_path), + "page_count": len(images), + "images": images, + "errors": [], + "warnings": warnings, + } diff --git a/scripts/docx_patch.py b/scripts/docx_patch.py new file mode 100644 index 0000000..443557a --- /dev/null +++ b/scripts/docx_patch.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +import argparse +from pathlib import Path + +from docx_ops_lib import apply_patch_document, read_json, render_docx, write_json + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--patch-file", required=True) + parser.add_argument("--report", required=True) + parser.add_argument("--render-check", action="store_true") + parser.add_argument("--render-dir") + args = parser.parse_args() + + patch_data = read_json(Path(args.patch_file).resolve()) + report = apply_patch_document(patch_data) + if args.render_check: + output_docx = Path(report["output_docx"]).resolve() + render_dir = Path(args.render_dir).resolve() if args.render_dir else output_docx.parent / f"{output_docx.stem}_render" + report["render"] = render_docx(output_docx, render_dir) + write_json(Path(args.report).resolve(), report) + + +if __name__ == "__main__": + main() diff --git a/scripts/docx_query.py b/scripts/docx_query.py new file mode 100644 index 0000000..82845fa --- /dev/null +++ b/scripts/docx_query.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +import argparse +from pathlib import Path + +from docx_ops_lib import index_document, query_nodes, read_json, write_json + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--docx", required=True) + parser.add_argument("--query-file", required=True) + parser.add_argument("--out", required=True) + args = parser.parse_args() + + index_data = index_document(Path(args.docx).resolve()) + query_data = read_json(Path(args.query_file).resolve()) + write_json(Path(args.out).resolve(), query_nodes(index_data, query_data)) + + +if __name__ == "__main__": + main() diff --git a/scripts/export_docx_pdf.py b/scripts/export_docx_pdf.py deleted file mode 100644 index 8d093c2..0000000 --- a/scripts/export_docx_pdf.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import annotations - -import argparse -import shutil -import subprocess -from pathlib import Path - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--docx", required=True) - parser.add_argument("--outdir", required=True) - args = parser.parse_args() - - soffice = shutil.which("soffice") - if not soffice: - raise FileNotFoundError("未检测到 LibreOffice/soffice,无法导出 PDF。") - - docx_path = Path(args.docx).resolve() - out_dir = Path(args.outdir).resolve() - out_dir.mkdir(parents=True, exist_ok=True) - subprocess.run( - [soffice, "--headless", "--convert-to", "pdf", "--outdir", str(out_dir), str(docx_path)], - check=True, - ) - - -if __name__ == "__main__": - main() diff --git a/scripts/extract_rfp_docx.py b/scripts/extract_rfp_docx.py deleted file mode 100644 index 735bbee..0000000 --- a/scripts/extract_rfp_docx.py +++ /dev/null @@ -1,68 +0,0 @@ -from __future__ import annotations - -import argparse -from pathlib import Path - -from common import ensure_output_layout, find_rfp_docx, write_text -from parse_docx import build_document_graph -from scan_project_materials import build_inventory - - -LEGACY_WORK_FILES = [ - "evidence_graph.json", - "missing_materials.json", - "outline_candidates.json", - "outline_final.json", - "outline_review.md", - "outline_review_report.json", - "outline_spec.json", - "outline_spec.reviewed.json", - "project_profile.json", - "rfp_outline.md", - "source_tables.json", - "stage_gates.json", -] - - -def cleanup_legacy_work_files(work_dir: Path) -> None: - for file_name in LEGACY_WORK_FILES: - path = work_dir / file_name - if path.exists(): - path.unlink() - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--project", required=True) - parser.add_argument("--out") - args = parser.parse_args() - - project_dir = Path(args.project).resolve() - output_layout = ensure_output_layout(project_dir) - work_dir = Path(args.out).resolve() if args.out else output_layout["work"] - work_dir.mkdir(parents=True, exist_ok=True) - cleanup_legacy_work_files(work_dir) - - docx_path = find_rfp_docx(project_dir) - document_graph = build_document_graph(docx_path) - inventory = build_inventory(project_dir) - - from common import write_json - - write_json(work_dir / "document_graph.json", document_graph) - write_json(work_dir / "material_inventory.json", inventory) - - summary = [ - f"# {project_dir.name} 基础解析结果", - "", - "- 已完成 DOCX 结构化解析。", - f"- 原文结构:{work_dir / 'document_graph.json'}", - f"- 通用材料盘点:{work_dir / 'material_inventory.json'}", - "", - "说明:本脚本只负责基础解析与落盘,不负责评分点、目录或正文判断。", - ] - write_text(output_layout["reports"] / "parse_summary.md", "\n".join(summary)) - - -if __name__ == "__main__": - main() diff --git a/scripts/generate_placeholder_images.py b/scripts/generate_placeholder_images.py deleted file mode 100644 index 2d12227..0000000 --- a/scripts/generate_placeholder_images.py +++ /dev/null @@ -1,113 +0,0 @@ -from __future__ import annotations - -import argparse -from pathlib import Path -from typing import Any - -from PIL import Image, ImageDraw, ImageFont - -from common import ensure_dir, find_font_path, read_json, write_json, write_text - - -def load_font(size: int): - font_path = find_font_path() - if font_path: - return ImageFont.truetype(str(font_path), size=size) - return ImageFont.load_default() - - -def wrap_text(draw: ImageDraw.ImageDraw, text: str, font, width: int) -> list[str]: - lines: list[str] = [] - current = "" - for char in text: - candidate = current + char - if draw.textlength(candidate, font=font) <= width: - current = candidate - continue - if current: - lines.append(current) - current = char - if current: - lines.append(current) - return lines - - -def render_placeholder(path: Path, title: str, purpose: str, hint: str) -> None: - width, height = 1280, 720 - image = Image.new("RGB", (width, height), "#F8FAFC") - draw = ImageDraw.Draw(image) - title_font = load_font(42) - body_font = load_font(26) - small_font = load_font(22) - - draw.rounded_rectangle((40, 40, width - 40, height - 40), radius=24, outline="#2563EB", width=6) - draw.rectangle((80, 90, width - 80, 180), fill="#DBEAFE") - draw.text((110, 110), f"待补附件占位:{title}", fill="#1D4ED8", font=title_font) - - lines = [ - f"材料用途:{purpose or '待 AI 补充'}", - f"替换提示:{hint or '待 AI 补充'}", - "使用方式:请将本占位图替换为真实扫描件或盖章材料。", - "注意事项:本图仅为占位,不代表已响应事实。", - ] - - y = 240 - for line in lines: - for segment in wrap_text(draw, line, body_font, width - 220): - draw.text((110, y), segment, fill="#0F172A", font=body_font) - y += 48 - y += 12 - - draw.text((110, height - 120), "状态:待替换", fill="#B91C1C", font=small_font) - ensure_dir(path.parent) - image.save(path) - - -def normalize_spec(spec: Any) -> list[dict[str, Any]]: - if isinstance(spec, dict): - items = spec.get("items", []) - elif isinstance(spec, list): - items = spec - else: - items = [] - return [item for item in items if isinstance(item, dict)] - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--spec", required=True) - parser.add_argument("--out", required=True) - args = parser.parse_args() - - items = normalize_spec(read_json(Path(args.spec).resolve())) - out_dir = Path(args.out).resolve() - ensure_dir(out_dir) - - manifest: list[dict[str, Any]] = [] - for index, item in enumerate(items, start=1): - file_name = item.get("file_name") or f"placeholder_{index}.png" - image_path = out_dir / file_name - title = item.get("label") or item.get("title") or f"附件{index}" - purpose = item.get("purpose") or "" - hint = item.get("hint") or item.get("replacement_hint") or "" - render_placeholder(image_path, title, purpose, hint) - manifest.append( - { - "title": title, - "path": str(image_path), - "purpose": purpose, - "hint": hint, - } - ) - - write_json(out_dir / "placeholder_manifest.json", manifest) - lines = ["# 占位图清单", ""] - if manifest: - lines.extend([f"- {item['title']}:{item['path']}" for item in manifest]) - else: - lines.append("- 未生成占位图。") - write_text(out_dir / "placeholder_manifest.md", "\n".join(lines)) - - -if __name__ == "__main__": - main() diff --git a/scripts/inspect_docx_text.py b/scripts/inspect_docx_text.py deleted file mode 100644 index 43ebb38..0000000 --- a/scripts/inspect_docx_text.py +++ /dev/null @@ -1,59 +0,0 @@ -from __future__ import annotations - -import argparse -from pathlib import Path - -from docx import Document - -from common import write_json, write_text - - -def extract_docx_text(docx_path: Path) -> str: - document = Document(docx_path) - chunks: list[str] = [] - for paragraph in document.paragraphs: - if paragraph.text.strip(): - chunks.append(paragraph.text.strip()) - for table in document.tables: - for row in table.rows: - for cell in row.cells: - if cell.text.strip(): - chunks.append(cell.text.strip()) - return "\n".join(chunks) - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--docx", required=True) - parser.add_argument("--out", required=True) - args = parser.parse_args() - - docx_path = Path(args.docx).resolve() - out_dir = Path(args.out).resolve() - out_dir.mkdir(parents=True, exist_ok=True) - - text = extract_docx_text(docx_path) - report = { - "docx": str(docx_path), - "character_count": len(text), - "line_count": len([line for line in text.splitlines() if line.strip()]), - } - write_json(out_dir / "docx_text_inspection.json", report) - write_text(out_dir / "docx_text_dump.txt", text) - write_text( - out_dir / "docx_text_inspection.md", - "\n".join( - [ - f"# {docx_path.name} 机械文本检查", - "", - f"- 文本字符数:{report['character_count']}", - f"- 非空行数:{report['line_count']}", - "", - "说明:本脚本只做 DOCX 文本提取与基础统计,不负责一致性、合规性或投标判断。", - ] - ), - ) - - -if __name__ == "__main__": - main() diff --git a/scripts/outline_check.py b/scripts/outline_check.py new file mode 100644 index 0000000..12f4bcf --- /dev/null +++ b/scripts/outline_check.py @@ -0,0 +1,177 @@ +from __future__ import annotations + +import argparse +import re +from pathlib import Path + +from docx_ops_lib import QueryError, read_json, write_json + +ILLEGAL_LEAF_TITLES = { + "技术方案", + "服务方案", + "实施方案", + "服务保障及措施", + "售后服务和质保期服务计划", + "项目理解", + "解决方案", + "系统设计", + "平台建设方案", + "系统建设方案", + "总体方案", + "培训方案", + "运维方案", +} + +TECHNICAL_ROOT_TITLES = { + "技术标目录", + "服务方案", + "技术方案", + "实施方案", + "服务保障及措施", + "售后服务和质保期服务计划", +} + +GENERIC_TECHNICAL_PATTERNS = ( + r"方案$", + r"设计$", + r"系统$", + r"平台$", + r"架构$", + r"建设内容$", + r"总体思路$", + r"总体要求$", + r"总体架构$", + r"功能设计$", + r"集成方案$", + r"响应方案$", + r"实施技术方案$", + r"部署方案$", + r"管理方案$", + r"验收方案$", + r"测试方案$", + r"试运行方案$", + r"保障措施$", + r"服务计划$", +) + +SPECIFIC_CHILD_HINTS = ( + "原则", + "目标", + "架构", + "模块", + "功能", + "内容", + "配置", + "清单", + "流程", + "机制", + "计划", + "步骤", + "标准", + "参数", + "接口", + "部署", + "测试", + "验收", + "培训", + "应急", + "风险", + "保障", + "响应", + "巡检", + "维护", + "更新", + "子系统", +) + + +def _normalize_heading(text: str) -> str: + compact = re.sub(r"\s+", "", text or "") + compact = re.sub(r"^[一二三四五六七八九十0-9]+[、\..]\s*", "", compact) + compact = re.sub(r"^\(?[0-9一二三四五六七八九十]+\)?\s*", "", compact) + compact = re.sub(r"^[0-9]+(\.[0-9]+)*\s*", "", compact) + return compact + + +def _is_technical_context(path: list[str]) -> bool: + return any(_normalize_heading(part) in TECHNICAL_ROOT_TITLES for part in path) + + +def _looks_generic_technical_heading(text: str) -> bool: + normalized = _normalize_heading(text) + if normalized in ILLEGAL_LEAF_TITLES: + return True + return any(re.search(pattern, normalized) for pattern in GENERIC_TECHNICAL_PATTERNS) + + +def _has_specific_children(children: list[dict]) -> bool: + child_texts = [_normalize_heading(str(child.get("text", "")).strip()) for child in children if isinstance(child, dict)] + return any( + any(hint in child_text for hint in SPECIFIC_CHILD_HINTS) + and not _looks_generic_technical_heading(child_text) + for child_text in child_texts + ) + + +def _walk_blocks(blocks: list[dict], path: list[str], issues: list[dict]) -> None: + for index, block in enumerate(blocks): + if not isinstance(block, dict): + issues.append({"type": "invalid_block", "path": " > ".join(path + [str(index)]), "message": "block must be an object"}) + continue + text = str(block.get("text", "")).strip() + block_type = block.get("type", "heading") + children = block.get("children", []) + current_path = path + ([text] if text else [str(index)]) + if block_type == "heading": + if text in ILLEGAL_LEAF_TITLES and not children: + issues.append( + { + "type": "illegal_leaf", + "path": " > ".join(current_path), + "message": f"abstract heading '{text}' cannot be a leaf", + } + ) + if children and not isinstance(children, list): + issues.append({"type": "invalid_children", "path": " > ".join(current_path), "message": "children must be a list"}) + continue + if isinstance(children, list): + if _is_technical_context(current_path): + normalized = _normalize_heading(text) + direct_heading_children = [child for child in children if isinstance(child, dict) and child.get("type", "heading") == "heading"] + if _looks_generic_technical_heading(normalized) and direct_heading_children and not _has_specific_children(direct_heading_children): + issues.append( + { + "type": "insufficient_technical_breakdown", + "path": " > ".join(current_path), + "message": f"technical heading '{text}' is still too generic; expand to subsystem/module/process level", + } + ) + _walk_blocks(children, current_path, issues) + + +def check_outline(payload: dict) -> dict: + blocks = payload.get("blocks", []) + if not isinstance(blocks, list): + raise QueryError("blocks must be a list") + issues: list[dict] = [] + _walk_blocks(blocks, [], issues) + return { + "status": "ok" if not issues else "failed", + "issue_count": len(issues), + "issues": issues, + } + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--outline-file", required=True) + parser.add_argument("--report", required=True) + args = parser.parse_args() + + payload = read_json(Path(args.outline_file).resolve()) + report = check_outline(payload) + write_json(Path(args.report).resolve(), report) + + +if __name__ == "__main__": + main() diff --git a/scripts/outline_export.py b/scripts/outline_export.py new file mode 100644 index 0000000..d1e81d4 --- /dev/null +++ b/scripts/outline_export.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +import argparse +from pathlib import Path + +from docx_ops_lib import export_outline_artifacts, read_json, write_json + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--spec-file", required=True) + parser.add_argument("--report", required=True) + args = parser.parse_args() + + payload = read_json(Path(args.spec_file).resolve()) + report = export_outline_artifacts(payload) + write_json(Path(args.report).resolve(), report) + + +if __name__ == "__main__": + main() diff --git a/scripts/outline_linter.py b/scripts/outline_linter.py deleted file mode 100644 index 6393764..0000000 --- a/scripts/outline_linter.py +++ /dev/null @@ -1,516 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -outline_linter.py - -用途: - 对 cn-it-bid-writer 的 canonical outline 或技术标目录做“目录阶段门禁”静态检查, - 把你在 outline-stage.md 里写的硬规则变成可执行的校验器(不通过就退出非 0)。 - -特点: - - 只依赖 Python 标准库,方便放进任意项目仓库。 - - 尽量兼容不同 JSON 结构:支持 title/name/heading/text,支持 children/items/sections/nodes。 - - 输出两种格式:text(默认)/json。 - - Exit code:0 通过;2 未通过(有 ERROR);1 运行时错误(文件/JSON 读取失败等)。 - -基本会拦住这些常见问题: - - “技术方案/实施方案/…(默认非法终点)”出现在叶子节点 - - 技术类叶子节点深度不足(默认 <3) - - 技术类叶子节点停留在“概述/说明”等抽象层级 - - 抽象标题节点只有 1 个子标题(违反“下钻不能单一”) - - 技术方案类根节点缺少“原则/架构/模块”结构化切面 - - 叶子节点缺 workflow_bucket -""" - -from __future__ import annotations - -import argparse -import json -import re -import sys -from dataclasses import dataclass, asdict -from typing import Any, Dict, List, Optional, Set, Tuple - - -# ------------------------- -# 可按需调整的规则配置 -# ------------------------- - -# 来自 references/outline-stage.md 的“默认非法终点”(禁止作为叶子节点) -FORBIDDEN_LEAF_TITLES: List[str] = [ - "技术方案", - "服务方案", - "实施方案", - "服务保障及措施", - "售后服务和质保期服务计划", - "项目理解", - "解决方案", - "系统设计", - "平台建设方案", - "系统建设方案", - "总体方案", - "培训方案", - "运维方案", -] - -# 技术方案“核心门禁”:需要同时具备的切面(原则/架构/模块) -TECH_SCHEME_CORE_TITLES: Set[str] = { - "技术方案", - "解决方案", - "系统设计", - "平台建设方案", - "系统建设方案", - "总体方案", -} - -# 维度识别(用于判断“原则/架构/模块/计划/风控”是否出现) -DIMENSION_KEYWORDS: Dict[str, List[str]] = { - "principles": ["原则", "目标", "策略", "标准", "规范", "总体要求", "建设目标", "设计目标", "设计原则", "总体原则"], - "architecture": [ - "架构", - "总体设计", - "总体架构", - "逻辑架构", - "物理架构", - "数据架构", - "应用架构", - "技术架构", - "安全架构", - "部署架构", - "拓扑", - "选型", - "技术路线", - "设计方案", - "总体设计方案", - ], - "modules": ["模块", "功能", "子系统", "组件", "内容", "建设内容", "实现", "接口", "数据流", "能力", "清单", "功能清单", "功能模块"], - "process": ["流程", "机制", "管理", "运行机制", "管理机制", "运维流程", "业务流程", "实施流程", "变更", "发布", "运维机制", "交付流程", "实施步骤", "实施方法"], - "plan_acceptance": ["计划", "进度", "里程碑", "阶段", "实施计划", "测试", "验收", "验收标准", "交付", "上线", "试运行", "培训计划", "培训安排", "质量计划"], - "assurance_risk": ["保障", "风控", "风险", "应急", "预案", "安全保障", "质量保障", "服务保障", "组织保障", "保密", "灾备"], -} - -# 技术上下文识别(用于决定是否执行“技术叶子深度”等规则) -TECH_CONTEXT_HINTS: List[str] = [ - "技术部分", - "技术文件", - "技术标", - "技术响应", - "技术方案", - "系统设计", - "系统建设", - "平台建设", - "集成方案", - "总体架构", - "技术架构", -] - - -# ------------------------- -# JSON 结构兼容(字段名兜底) -# ------------------------- - -TITLE_KEYS = ("title", "name", "heading", "text", "label") -CHILD_KEYS = ("children", "items", "subsections", "sections", "nodes", "sub", "subs") -BUCKET_KEYS = ("workflow_bucket", "bucket", "wf_bucket") - - -# ------------------------- -# 工具函数 -# ------------------------- - -def normalize_title(raw: Any) -> str: - """ - 标题归一化:用于匹配“非法终点”等规则。 - 会做: - - 去掉前缀编号:'5.1 ' / '一、' / '(一)' / '第1章' - - 去掉末尾括号注释:'技术方案(详细)' -> '技术方案' - - 合并多余空格 - """ - if raw is None: - return "" - t = str(raw).strip() - - # 去掉前缀: (一) 或 (1) - t = re.sub(r"^\s*[((]\s*([一二三四五六七八九十百千万]+|\d+)\s*[))]\s*", "", t) - - # 去掉前缀: 5. / 5.1 / 5) / 5、... - t = re.sub(r"^\s*\d+(?:\.\d+)*\s*[、\.\)\]]\s*", "", t) - t = re.sub(r"^\s*\d+(?:\.\d+)*\s+", "", t) - - # 去掉前缀: 一、 / 1、 - t = re.sub(r"^\s*([一二三四五六七八九十百千万]+|\d+)\s*[、\.]\s*", "", t) - - # 去掉前缀: 第X章/节/部分/篇 - t = re.sub(r"^\s*第\s*([一二三四五六七八九十百千万]+|\d+)\s*[章节部分篇]\s*", "", t) - - t = t.strip() - - # 去掉末尾括号注释(最长 30 字,避免误删大段) - t = re.sub(r"[\((][^\))]{0,30}[\))]\s*$", "", t).strip() - - # 合并多余空格 - t = re.sub(r"\s+", " ", t).strip() - return t - - -def get_title(node: Any) -> str: - if isinstance(node, dict): - for k in TITLE_KEYS: - v = node.get(k) - if isinstance(v, (str, int, float)): - return str(v) - return "" - - -def get_children(node: Any) -> List[Any]: - if isinstance(node, dict): - for k in CHILD_KEYS: - v = node.get(k) - if isinstance(v, list): - return v - return [] - - -def get_bucket(node: Any) -> Optional[str]: - if isinstance(node, dict): - for k in BUCKET_KEYS: - v = node.get(k) - if isinstance(v, str): - v = v.strip() - return v or None - return None - - -def dimensions_from_title(title_clean: str) -> Set[str]: - hits: Set[str] = set() - for dim, kws in DIMENSION_KEYWORDS.items(): - for kw in kws: - if kw and kw in title_clean: - hits.add(dim) - break - return hits - - -def is_tech_context_title(title_clean: str) -> bool: - # 明确的技术章节/根节点提示 - if title_clean in TECH_SCHEME_CORE_TITLES: - return True - if any(h in title_clean for h in TECH_CONTEXT_HINTS): - return True - # 兜底:包含“技术”且是章节型表达 - if "技术" in title_clean and any(s in title_clean for s in ("部分", "文件", "标", "方案", "响应", "章节")): - return True - return False - - -def is_shallow_technical_leaf(title_clean: str) -> bool: - """ - 用于拦截“方案概述/总体说明”这类典型浅层叶子节点。 - """ - shallow_exact = {"概述", "方案概述", "项目概述", "总体概述", "总体说明"} - if title_clean in shallow_exact: - return True - if title_clean.endswith(("概述", "介绍", "说明")): - return True - if "概述" in title_clean and len(title_clean) <= 8: - return True - return False - - -# ------------------------- -# 数据结构 -# ------------------------- - -@dataclass -class NodeInfo: - parent: Optional[int] - idx_path: List[int] - path_no: str - breadcrumb: str - depth: int - title_raw: str - title_clean: str - bucket: Optional[str] - is_leaf: bool - child_count: int - tech_context: bool - subtree_dims: Set[str] - - -@dataclass -class Issue: - severity: str # ERROR / WARN - code: str - message: str - path_no: str - breadcrumb: str - depth: int - bucket: Optional[str] = None - title: Optional[str] = None - - -# ------------------------- -# 核心 lint 逻辑 -# ------------------------- - -def extract_root_nodes(obj: Any) -> List[Any]: - """ - 尽量兼容不同 JSON 形态: - - 顶层是 list:直接当作根节点列表 - - 顶层是 dict:优先取 outline/nodes/children/items/sections 这类字段 - - 其他:包装成单节点列表 - """ - if isinstance(obj, list): - return obj - if isinstance(obj, dict): - for k in ("outline", "nodes", "children", "items", "sections"): - v = obj.get(k) - if isinstance(v, list): - return v - return [obj] - return [obj] - - -def lint_outline_obj(obj: Any, *, min_tech_depth: int = 3, strict: bool = False) -> Tuple[List[NodeInfo], List[Issue]]: - """ - 返回:(nodes, issues) - issues 中 severity=ERROR 的数量 >0 即视为未通过。 - """ - forbidden_set: Set[str] = {normalize_title(x) for x in FORBIDDEN_LEAF_TITLES} - abstract_set: Set[str] = set(forbidden_set) - tech_scheme_set: Set[str] = {normalize_title(x) for x in TECH_SCHEME_CORE_TITLES} - - nodes = extract_root_nodes(obj) - - infos: List[NodeInfo] = [] - issues: List[Issue] = [] - - def walk(node: Any, parent_idx: Optional[int], idx_path: List[int], title_path: List[str], parent_tech: bool) -> Set[str]: - if not isinstance(node, dict): - path_no = ".".join(str(i) for i in idx_path) - breadcrumb = " > ".join(title_path + [str(node)]) - issues.append(Issue("ERROR", "F001", f"节点不是对象(dict),无法解析: {type(node).__name__}", path_no, breadcrumb, len(idx_path))) - return set() - - title_raw = get_title(node) or "" - title_clean = normalize_title(title_raw) - bucket = get_bucket(node) - - children = get_children(node) - child_count = len(children) - is_leaf = child_count == 0 - - tech_context = parent_tech or (bucket == "technical") or is_tech_context_title(title_clean) - - path_no = ".".join(str(i) for i in idx_path) - breadcrumb = " > ".join(title_path + [title_raw]) - - # 先创建 info(subtree_dims 稍后补齐) - info = NodeInfo( - parent=parent_idx, - idx_path=idx_path, - path_no=path_no, - breadcrumb=breadcrumb, - depth=len(idx_path), - title_raw=title_raw, - title_clean=title_clean, - bucket=bucket, - is_leaf=is_leaf, - child_count=child_count, - tech_context=tech_context, - subtree_dims=set(), - ) - my_idx = len(infos) - infos.append(info) - - # ---------- 叶子节点检查 ---------- - if is_leaf: - # B001: workflow_bucket 必须存在 - if not bucket: - issues.append(Issue("ERROR", "B001", "叶子节点缺少 workflow_bucket 标注", path_no, breadcrumb, info.depth, bucket, title_clean)) - else: - # B002: bucket 值合法性(warn) - if bucket not in ("business", "technical", "other"): - issues.append(Issue("WARN", "B002", f"workflow_bucket 值不在 business/technical/other 中: {bucket!r}", path_no, breadcrumb, info.depth, bucket, title_clean)) - - # L001: 禁止抽象标题作为叶子节点 - if title_clean in forbidden_set: - issues.append(Issue("ERROR", "L001", f"默认非法终点标题出现在叶子节点: {title_clean}", path_no, breadcrumb, info.depth, bucket, title_clean)) - - # 技术叶子深度门禁(仅当处于技术上下文且 bucket=technical 或父级是技术上下文) - if tech_context and (bucket == "technical" or parent_tech): - # T001: 技术叶子深度不足 - if info.depth < min_tech_depth: - issues.append(Issue("ERROR", "T001", f"技术类叶子节点目录深度不足: depth={info.depth} < {min_tech_depth}", path_no, breadcrumb, info.depth, bucket, title_clean)) - - # T003: 概述类叶子(典型浅层错误) - if is_shallow_technical_leaf(title_clean): - issues.append(Issue("ERROR", "T003", f"技术类叶子节点疑似停留在“概述/说明”等抽象层级,应继续下钻: {title_clean}", path_no, breadcrumb, info.depth, bucket, title_clean)) - - # T004: 严格模式——技术叶子以“方案”结尾也不放行(可按需开启) - if strict and title_clean.endswith("方案") and title_clean not in forbidden_set: - issues.append(Issue("ERROR", "T004", f"严格模式:技术类叶子节点标题以“方案”结尾,建议继续下钻到原则/架构/模块等: {title_clean}", path_no, breadcrumb, info.depth, bucket, title_clean)) - - # ---------- 非叶子节点检查 ---------- - else: - # A002: 抽象标题下钻不能只有一个子标题 - if title_clean in abstract_set and child_count == 1: - issues.append(Issue("ERROR", "A002", f"抽象标题节点仅有 1 个子标题,违反“下钻不能是单一的”约束: {title_clean}", path_no, breadcrumb, info.depth, bucket, title_clean)) - - # A003: 技术方案核心节点建议 >=3 个子标题(更像“完整切面”) - if title_clean in tech_scheme_set and child_count < 3: - issues.append(Issue("ERROR", "A003", f"技术方案类节点子标题过少(建议>=3),无法形成结构化切面: {title_clean}", path_no, breadcrumb, info.depth, bucket, title_clean)) - - # ---------- 递归:先算子树维度 ---------- - subtree_dims = set() - subtree_dims |= dimensions_from_title(title_clean) - - for i, ch in enumerate(children, start=1): - subtree_dims |= walk(ch, my_idx, idx_path + [i], title_path + [title_raw], tech_context) - - info.subtree_dims = subtree_dims - - # A001: 抽象标题被当叶子(放在递归后也可以,但这里再次兜底) - if title_clean in abstract_set and is_leaf: - issues.append(Issue("ERROR", "A001", f"抽象标题节点被当作叶子节点(禁止作为终点): {title_clean}", path_no, breadcrumb, info.depth, bucket, title_clean)) - - # T002: 技术方案核心节点必须具备:原则/架构/模块 - if title_clean in tech_scheme_set: - required = {"principles", "architecture", "modules"} - if strict: - # 严格模式再要求:计划/验收、保障/风控(贴近你 outline-stage.md 里的“结构化切面”) - required |= {"plan_acceptance", "assurance_risk"} - - missing = required - subtree_dims - if missing: - issues.append(Issue("ERROR", "T002", f"技术方案类章节缺少关键结构化切面: 缺 {sorted(missing)}", path_no, breadcrumb, info.depth, bucket, title_clean)) - - return subtree_dims - - for idx, node in enumerate(nodes, start=1): - walk(node, None, [idx], [], False) - - return infos, issues - - -# ------------------------- -# 报告输出 -# ------------------------- - -def compute_checklist(issues: List[Issue]) -> Dict[str, Any]: - """ - 对齐 outline-stage.md 的“强制自检清单”(能自动检查的部分) - """ - illegal_leaf_tech_or_impl = any(i.code in ("L001", "A001") and i.title in ("技术方案", "实施方案") for i in issues) - tech_depth_bad = any(i.code == "T001" for i in issues) - tech_scheme_missing = any(i.code == "T002" for i in issues) - - return { - "no_illegal_leaf_tech_or_impl": not illegal_leaf_tech_or_impl, - "tech_depth_ok": not tech_depth_bad, - "tech_scheme_has_pri_arch_mod": not tech_scheme_missing, - "scoring_covered": None, # 本脚本不检查评分点覆盖(需要 evaluation_model / 映射规则) - } - - -def render_text_report(*, outline_path: str, strict: bool, issues: List[Issue], checklist: Dict[str, Any]) -> str: - def sort_key(i: Issue): - sev_rank = 0 if i.severity == "ERROR" else 1 - parts = [] - for p in i.path_no.split("."): - try: - parts.append(int(p)) - except Exception: - parts.append(10**9) - return (sev_rank, parts, i.code) - - issues_sorted = sorted(issues, key=sort_key) - err_cnt = sum(1 for i in issues if i.severity == "ERROR") - warn_cnt = sum(1 for i in issues if i.severity == "WARN") - - lines: List[str] = [] - lines.append("outline-linter report") - lines.append(f"- outline: {outline_path}") - lines.append(f"- strict: {strict}") - lines.append("") - - lines.append("【目录深度强制自检】") - lines.append( - f"1. 是否存在直接以“技术方案”或“实施方案”作为叶子节点的章节?(要求:否) => " - f"{'否' if checklist.get('no_illegal_leaf_tech_or_impl') else '是(未通过)'}" - ) - lines.append( - f"2. 技术类章节是否已经下钻到第三级或第四级?(要求:是) => " - f"{'是' if checklist.get('tech_depth_ok') else '否(未通过)'}" - ) - lines.append( - f"3. 技术方案下,是否同时包含了[原则]、[架构]、[内容/模块]?(要求:是) => " - f"{'是' if checklist.get('tech_scheme_has_pri_arch_mod') else '否(未通过)'}" - ) - lines.append("4. 所有的评分点是否都已在目录中体现?(要求:是) => 未检查(需要 evaluation_model / mapping)") - lines.append("") - - lines.append(f"Issues: ERROR={err_cnt}, WARN={warn_cnt}") - - if issues_sorted: - lines.append("") - lines.append("Details:") - for i in issues_sorted: - lines.append(f"[{i.severity}][{i.code}] {i.path_no} | {i.breadcrumb} | {i.message}") - - return "\n".join(lines) - - -def build_json_report(*, outline_path: str, strict: bool, issues: List[Issue], checklist: Dict[str, Any]) -> Dict[str, Any]: - err_cnt = sum(1 for i in issues if i.severity == "ERROR") - warn_cnt = sum(1 for i in issues if i.severity == "WARN") - return { - "outline": outline_path, - "strict": strict, - "passed": err_cnt == 0, - "error_count": err_cnt, - "warning_count": warn_cnt, - "checklist": checklist, - "issues": [asdict(i) for i in issues], - } - - -# ------------------------- -# CLI -# ------------------------- - -def main(argv: Optional[List[str]] = None) -> int: - parser = argparse.ArgumentParser(description="Lint a canonical outline or technical outline against outline-stage gates.") - parser.add_argument("--outline", required=True, help="Path to canonical outline JSON or work/final_outline_technical.json") - parser.add_argument("--min-tech-depth", type=int, default=3, help="Minimum depth for technical leaf nodes. Default: 3") - parser.add_argument("--strict", action="store_true", help="Enable stricter checks (more ERRORs).") - parser.add_argument("--format", choices=("text", "json"), default="text", help="Output format. Default: text") - - args = parser.parse_args(argv) - - try: - with open(args.outline, "r", encoding="utf-8") as f: - obj = json.load(f) - except FileNotFoundError: - print(f"[FATAL] outline not found: {args.outline}", file=sys.stderr) - return 1 - except json.JSONDecodeError as e: - print(f"[FATAL] invalid JSON: {args.outline}: {e}", file=sys.stderr) - return 1 - - _, issues = lint_outline_obj(obj, min_tech_depth=args.min_tech_depth, strict=args.strict) - checklist = compute_checklist(issues) - - if args.format == "json": - report = build_json_report(outline_path=args.outline, strict=args.strict, issues=issues, checklist=checklist) - print(json.dumps(report, ensure_ascii=False, indent=2)) - else: - print(render_text_report(outline_path=args.outline, strict=args.strict, issues=issues, checklist=checklist)) - - # exit code: 0 pass, 2 fail, 1 runtime error - has_error = any(i.severity == "ERROR" for i in issues) - return 2 if has_error else 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/scripts/parse_docx.py b/scripts/parse_docx.py deleted file mode 100644 index 3334b0f..0000000 --- a/scripts/parse_docx.py +++ /dev/null @@ -1,171 +0,0 @@ -from __future__ import annotations - -import argparse -import json -import re -from pathlib import Path -from typing import Any - -from docx import Document -from docx.document import Document as DocxDocument -from docx.table import Table -from docx.text.paragraph import Paragraph - -from common import write_json - - -CHAPTER_RE = re.compile(r"^第[一二三四五六七八九十百零]+[章节篇部]\s*(.+)?$") -NUMBERED_RE = re.compile(r"^(?P\d+(?:\.\d+){0,8})[.、.\s))]*(?P.+)$") - - -def normalize_text(value: str) -> str: - return re.sub(r"\s+", " ", value or "").strip() - - -def table_to_rows(table: Table) -> list[list[str]]: - rows: list[list[str]] = [] - for row in table.rows: - values = [normalize_text(cell.text) for cell in row.cells] - if any(values): - rows.append(values) - return rows - - -def is_heading_style(style_name: str) -> bool: - normalized = normalize_text(style_name).replace(" ", "").lower() - return normalized.startswith("heading") or normalized.startswith("标题") - - -def parse_heading(text: str, style_name: str) -> dict[str, Any] | None: - source = normalize_text(text) - if not source: - return None - chapter_match = CHAPTER_RE.match(source) - if chapter_match: - return { - "level": 1, - "number": source.split(" ", 1)[0], - "title": source, - "kind": "chapter", - } - if is_heading_style(style_name): - match = NUMBERED_RE.match(source) - if match: - return { - "level": len(match.group("number").split(".")) + 1, - "number": match.group("number"), - "title": normalize_text(match.group("title")), - "kind": "numbered", - } - return { - "level": 2, - "number": "", - "title": source, - "kind": "styled", - } - numbered_match = NUMBERED_RE.match(source) - if numbered_match and len(source) <= 120: - return { - "level": len(numbered_match.group("number").split(".")) + 1, - "number": numbered_match.group("number"), - "title": normalize_text(numbered_match.group("title")), - "kind": "numbered", - } - return None - - -def iter_blocks(document: DocxDocument) -> list[dict[str, Any]]: - blocks: list[dict[str, Any]] = [] - paragraph_index = 0 - table_index = 0 - for child in document.element.body.iterchildren(): - if child.tag.endswith("}p"): - paragraph = Paragraph(child, document) - text = normalize_text(paragraph.text) - if not text: - continue - style_name = paragraph.style.name if paragraph.style else "Normal" - blocks.append( - { - "id": f"p-{paragraph_index}", - "kind": "paragraph", - "text": text, - "style": style_name, - "heading": parse_heading(text, style_name), - } - ) - paragraph_index += 1 - elif child.tag.endswith("}tbl"): - rows = table_to_rows(Table(child, document)) - if not rows: - continue - blocks.append( - { - "id": f"t-{table_index}", - "kind": "table", - "text": "\n".join(" | ".join(row) for row in rows), - "rows": rows, - } - ) - table_index += 1 - return blocks - - -def extract_images(document: Document) -> list[dict[str, Any]]: - images: list[dict[str, Any]] = [] - image_index = 0 - for rel in document.part.rels.values(): - target_ref = getattr(rel, "target_ref", "") - if "image" not in target_ref: - continue - images.append( - { - "id": f"img-{image_index}", - "target_ref": target_ref, - } - ) - image_index += 1 - return images - - -def build_document_graph(docx_path: Path) -> dict[str, Any]: - document = Document(docx_path) - blocks = iter_blocks(document) - headings = [block["heading"] | {"block_id": block["id"]} for block in blocks if block.get("heading")] - tables = [ - { - "id": block["id"], - "rows": block["rows"], - "row_count": len(block["rows"]), - "column_count": max(len(row) for row in block["rows"]) if block["rows"] else 0, - } - for block in blocks - if block["kind"] == "table" - ] - return { - "source_docx": str(docx_path), - "blocks": blocks, - "headings": headings, - "tables": tables, - "images": extract_images(document), - "summary": { - "block_count": len(blocks), - "paragraph_count": len([block for block in blocks if block["kind"] == "paragraph"]), - "table_count": len(tables), - "image_count": len(extract_images(document)), - }, - } - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--docx", required=True) - parser.add_argument("--out", required=True) - args = parser.parse_args() - - graph = build_document_graph(Path(args.docx).resolve()) - write_json(Path(args.out).resolve(), graph) - - -if __name__ == "__main__": - main() diff --git a/scripts/render_bid_docx.py b/scripts/render_bid_docx.py deleted file mode 100644 index 89a4f92..0000000 --- a/scripts/render_bid_docx.py +++ /dev/null @@ -1,206 +0,0 @@ -from __future__ import annotations - -import argparse -from pathlib import Path -from typing import Any - -from docx import Document -from docx.enum.text import WD_ALIGN_PARAGRAPH -from docx.oxml import OxmlElement -from docx.oxml.ns import qn -from docx.shared import Cm, Pt, RGBColor - -from common import get_bundle_defaults, normalize_bundle, read_json - - -FONT_NAME = "Microsoft YaHei" - - -def set_run_font(run, size: int = 12, bold: bool = False) -> None: - run.font.name = FONT_NAME - run._element.rPr.rFonts.set(qn("w:eastAsia"), FONT_NAME) - run.font.size = Pt(size) - run.font.bold = bold - - -def set_style_font(style, size: int, bold: bool = False) -> None: - style.font.name = FONT_NAME - style._element.rPr.rFonts.set(qn("w:eastAsia"), FONT_NAME) - style.font.size = Pt(size) - style.font.bold = bold - - -def add_toc(doc: Document) -> None: - paragraph = doc.add_paragraph() - run = paragraph.add_run() - fld_char_begin = OxmlElement("w:fldChar") - fld_char_begin.set(qn("w:fldCharType"), "begin") - instr_text = OxmlElement("w:instrText") - instr_text.set(qn("xml:space"), "preserve") - instr_text.text = 'TOC \\o "1-3" \\h \\z \\u' - fld_char_separate = OxmlElement("w:fldChar") - fld_char_separate.set(qn("w:fldCharType"), "separate") - placeholder = OxmlElement("w:t") - placeholder.text = "打开文档后右键更新目录" - fld_char_end = OxmlElement("w:fldChar") - fld_char_end.set(qn("w:fldCharType"), "end") - run._r.append(fld_char_begin) - run._r.append(instr_text) - run._r.append(fld_char_separate) - run._r.append(placeholder) - run._r.append(fld_char_end) - - -def add_paragraph(doc: Document, text: str, size: int = 12, bold: bool = False, align=WD_ALIGN_PARAGRAPH.JUSTIFY): - paragraph = doc.add_paragraph() - paragraph.alignment = align - paragraph.paragraph_format.space_after = Pt(6) - paragraph.paragraph_format.line_spacing = 1.5 - run = paragraph.add_run(text) - set_run_font(run, size=size, bold=bold) - return paragraph - - -def add_bullets(doc: Document, items: list[str]) -> None: - for item in items: - paragraph = doc.add_paragraph(style="List Bullet") - set_run_font(paragraph.add_run(item)) - - -def add_table(doc: Document, title: str, headers: list[str], rows: list[list[str]]) -> None: - if title: - add_paragraph(doc, title, bold=True) - table = doc.add_table(rows=1, cols=len(headers)) - table.style = "Table Grid" - for index, header in enumerate(headers): - table.rows[0].cells[index].text = header - for row in rows: - cells = table.add_row().cells - for index, value in enumerate(row): - cells[index].text = value - doc.add_paragraph() - - -def add_image(doc: Document, path: Path, title: str | None = None, width_cm: float = 16.0) -> None: - if title: - add_paragraph(doc, title, bold=True) - doc.add_picture(str(path), width=Cm(width_cm)) - - -def coerce_content_block(value: Any) -> dict[str, Any]: - if isinstance(value, str): - return {"paragraphs": [value]} - if isinstance(value, dict): - return value - return {} - - -def normalize_section_payload(section: dict[str, Any]) -> dict[str, Any]: - content = coerce_content_block(section.get("content")) - return { - "title": section.get("title", ""), - "paragraphs": list(section.get("paragraphs", content.get("paragraphs", []))), - "bullets": list(section.get("bullets", content.get("bullets", []))), - "tables": list(section.get("tables", content.get("tables", []))), - "images": list(section.get("images", content.get("images", []))), - "children": [normalize_section_payload(child) for child in section.get("children", [])], - } - - -def normalize_sections(spec: dict[str, Any]) -> list[dict[str, Any]]: - nodes = spec.get("nodes") - if isinstance(nodes, list): - return [normalize_section_payload(node) for node in nodes if isinstance(node, dict)] - - sections = spec.get("sections", []) - if not isinstance(sections, list): - return [] - if any(isinstance(item, dict) and any(key in item for key in ("outline_id", "workflow_bucket", "status", "content")) for item in sections): - return [normalize_section_payload(section) for section in sections if isinstance(section, dict)] - return [section for section in sections if isinstance(section, dict)] - - -def add_section(doc: Document, section: dict[str, Any], numbers: tuple[int, ...] = ()) -> None: - prefix = ".".join(str(number) for number in numbers) + " " if numbers else "" - title = section.get("title", "") - if title: - level = min(max(len(numbers), 1), 3) - doc.add_heading(prefix + title, level=level) - - for paragraph_text in section.get("paragraphs", []): - add_paragraph(doc, paragraph_text) - - bullets = section.get("bullets", []) - if bullets: - add_bullets(doc, bullets) - - for table in section.get("tables", []): - add_table(doc, table.get("title", ""), table.get("headers", []), table.get("rows", [])) - - for image in section.get("images", []): - image_path = Path(image["path"]).resolve() - add_image(doc, image_path, image.get("title"), float(image.get("width_cm", 16))) - - for index, child in enumerate(section.get("children", []), start=1): - add_section(doc, child, numbers + (index,)) - - -def build_docx(spec: dict[str, Any], out_path: Path) -> None: - doc = Document() - section = doc.sections[0] - section.top_margin = Cm(2.54) - section.bottom_margin = Cm(2.54) - section.left_margin = Cm(3.0) - section.right_margin = Cm(2.5) - - set_style_font(doc.styles["Normal"], 12) - set_style_font(doc.styles["Heading 1"], 16, True) - set_style_font(doc.styles["Heading 2"], 14, True) - set_style_font(doc.styles["Heading 3"], 12, True) - - bundle = normalize_bundle(spec.get("bundle")) - bundle_defaults = get_bundle_defaults(bundle) if bundle else None - doc_title = spec.get("doc_title") or (bundle_defaults["bid_doc_title"] if bundle_defaults else "投标文件") - toc_title = spec.get("toc_title") or (bundle_defaults["bid_toc_title"] if bundle_defaults else "目录") - - cover = doc.add_paragraph() - cover.alignment = WD_ALIGN_PARAGRAPH.CENTER - cover_specs = [ - (spec.get("project_name", "项目名称待补充"), 22, True), - (doc_title, 28, True), - (spec.get("subtitle", ""), 16, True), - ("", 12, False), - (f"投标人:{spec.get('bidder_name', '投标人名称待补充')}", 14, False), - ] - for text, size, bold in cover_specs: - if not text and size != 12: - continue - run = cover.add_run(text + "\n") - set_run_font(run, size=size, bold=bold) - if size >= 22: - run.font.color.rgb = RGBColor(0x1E, 0x3A, 0x8A) - - if spec.get("include_toc", True): - doc.add_page_break() - doc.add_heading(toc_title, level=1) - add_toc(doc) - - for index, section_spec in enumerate(normalize_sections(spec), start=1): - doc.add_page_break() - add_section(doc, section_spec, (index,)) - - out_path.parent.mkdir(parents=True, exist_ok=True) - doc.save(out_path) - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--content", required=True) - parser.add_argument("--out", required=True) - args = parser.parse_args() - - build_docx(read_json(Path(args.content).resolve()), Path(args.out).resolve()) - - -if __name__ == "__main__": - main() diff --git a/scripts/render_docx.py b/scripts/render_docx.py new file mode 100644 index 0000000..1c741e1 --- /dev/null +++ b/scripts/render_docx.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +import argparse +from pathlib import Path + +from docx_ops_lib import render_docx, write_json + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--docx", required=True) + parser.add_argument("--out-dir", required=True) + parser.add_argument("--report") + args = parser.parse_args() + + report = render_docx(Path(args.docx).resolve(), Path(args.out_dir).resolve()) + if args.report: + write_json(Path(args.report).resolve(), report) + + +if __name__ == "__main__": + main() diff --git a/scripts/render_markdown_report.py b/scripts/render_markdown_report.py deleted file mode 100644 index ea293ce..0000000 --- a/scripts/render_markdown_report.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import annotations - -import argparse -from pathlib import Path -from typing import Any - -from common import read_json, write_text - - -def render_sections(sections: list[dict[str, Any]]) -> list[str]: - lines: list[str] = [] - for section in sections: - heading = section.get("heading") - if heading: - lines.extend([f"## {heading}", ""]) - for paragraph in section.get("paragraphs", []): - lines.append(paragraph) - if section.get("paragraphs"): - lines.append("") - bullets = section.get("bullets", []) - if bullets: - lines.extend([f"- {item}" for item in bullets]) - lines.append("") - return lines - - -def build_markdown(spec: dict[str, Any]) -> str: - if "markdown" in spec: - return str(spec["markdown"]) - - lines = [f"# {spec.get('title', '报告')}", ""] - summary = spec.get("summary") - if summary: - lines.extend([summary, ""]) - lines.extend(render_sections(spec.get("sections", []))) - return "\n".join(lines).rstrip() + "\n" - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--spec", required=True) - parser.add_argument("--out", required=True) - args = parser.parse_args() - - spec = read_json(Path(args.spec).resolve()) - write_text(Path(args.out).resolve(), build_markdown(spec)) - - -if __name__ == "__main__": - main() diff --git a/scripts/render_outline_docx.py b/scripts/render_outline_docx.py deleted file mode 100644 index 0f20a6a..0000000 --- a/scripts/render_outline_docx.py +++ /dev/null @@ -1,131 +0,0 @@ -from __future__ import annotations - -import argparse -from pathlib import Path -from typing import Any - -from docx import Document -from docx.enum.text import WD_PARAGRAPH_ALIGNMENT -from docx.oxml import OxmlElement -from docx.oxml.ns import qn -from docx.shared import Pt - -from common import get_bundle_defaults, normalize_bundle, read_json - - -FONT_NAME = "Microsoft YaHei" -TOC_HINT = "打开文档后右键更新目录" -HEADING_FONT_SIZES = {1: 16, 2: 14, 3: 12} -MAX_HEADING_LEVEL = 9 - - -def set_run_font(run, size: int = 12, bold: bool = False) -> None: - run.bold = bold - run.font.size = Pt(size) - run.font.name = FONT_NAME - rpr = run._element.get_or_add_rPr() - rfonts = rpr.rFonts - if rfonts is None: - rfonts = OxmlElement("w:rFonts") - rpr.append(rfonts) - rfonts.set(qn("w:eastAsia"), FONT_NAME) - rfonts.set(qn("w:ascii"), FONT_NAME) - rfonts.set(qn("w:hAnsi"), FONT_NAME) - - -def configure_style(style, size: int, bold: bool = False) -> None: - style.font.name = FONT_NAME - style.font.size = Pt(size) - style.font.bold = bold - rpr = style._element.get_or_add_rPr() - rfonts = rpr.rFonts - if rfonts is None: - rfonts = OxmlElement("w:rFonts") - rpr.append(rfonts) - rfonts.set(qn("w:eastAsia"), FONT_NAME) - rfonts.set(qn("w:ascii"), FONT_NAME) - rfonts.set(qn("w:hAnsi"), FONT_NAME) - - -def add_toc(paragraph) -> None: - run = paragraph.add_run() - fld_begin = OxmlElement("w:fldChar") - fld_begin.set(qn("w:fldCharType"), "begin") - instr = OxmlElement("w:instrText") - instr.set(qn("xml:space"), "preserve") - instr.text = 'TOC \\o "1-9" \\h \\z \\u' - fld_sep = OxmlElement("w:fldChar") - fld_sep.set(qn("w:fldCharType"), "separate") - text = OxmlElement("w:t") - text.text = TOC_HINT - fld_end = OxmlElement("w:fldChar") - fld_end.set(qn("w:fldCharType"), "end") - run._r.append(fld_begin) - run._r.append(instr) - run._r.append(fld_sep) - run._r.append(text) - run._r.append(fld_end) - - -def add_cover(doc: Document, project_name: str, doc_title: str) -> None: - p = doc.add_paragraph() - p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER - set_run_font(p.add_run(project_name), 18, True) - - p = doc.add_paragraph() - p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER - set_run_font(p.add_run(doc_title), 22, True) - - -def normalize_outline_item(item: dict[str, Any]) -> dict[str, Any]: - return { - "title": item["title"], - "children": [normalize_outline_item(child) for child in item.get("children", [])], - } - - -def render_outline_nodes(doc: Document, items: list[dict[str, Any]], prefix: tuple[int, ...] = ()) -> None: - for index, item in enumerate(items, start=1): - numbers = prefix + (index,) - level = min(len(numbers), MAX_HEADING_LEVEL) - font_size = HEADING_FONT_SIZES.get(level, 12) - paragraph = doc.add_paragraph(style=f"Heading {level}") - set_run_font(paragraph.add_run(f"{'.'.join(str(number) for number in numbers)} {item['title']}"), font_size, True) - render_outline_nodes(doc, item.get("children", []), numbers) - - -def build_docx(outline_spec: dict[str, Any], out_path: Path) -> None: - doc = Document() - configure_style(doc.styles["Normal"], 12) - for level in range(1, MAX_HEADING_LEVEL + 1): - configure_style(doc.styles[f"Heading {level}"], HEADING_FONT_SIZES.get(level, 12), True) - - bundle = normalize_bundle(outline_spec.get("bundle")) - bundle_defaults = get_bundle_defaults(bundle) if bundle else None - doc_title = outline_spec.get("doc_title") or (bundle_defaults["outline_doc_title"] if bundle_defaults else "投标文件(目录版)") - toc_title = outline_spec.get("toc_title") or (bundle_defaults["outline_toc_title"] if bundle_defaults else "目录") - - add_cover(doc, outline_spec.get("project_name", "项目名称待补充"), doc_title) - doc.add_page_break() - title = doc.add_paragraph() - set_run_font(title.add_run(toc_title), 16, True) - add_toc(doc.add_paragraph()) - doc.add_page_break() - - sections = [normalize_outline_item(section) for section in outline_spec.get("sections", [])] - render_outline_nodes(doc, sections) - out_path.parent.mkdir(parents=True, exist_ok=True) - doc.save(str(out_path)) - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--outline", required=True) - parser.add_argument("--out", required=True) - args = parser.parse_args() - - build_docx(read_json(Path(args.outline).resolve()), Path(args.out).resolve()) - - -if __name__ == "__main__": - main() diff --git a/scripts/review_outline_and_generate_toc.py b/scripts/review_outline_and_generate_toc.py deleted file mode 100644 index 263322b..0000000 --- a/scripts/review_outline_and_generate_toc.py +++ /dev/null @@ -1,35 +0,0 @@ -from __future__ import annotations - -import argparse -from pathlib import Path - -from common import ensure_output_layout, get_bundle_outline_docx_path, get_bundle_outline_path, normalize_bundle -from render_outline_docx import build_docx -from common import read_json - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--project", required=True) - parser.add_argument("--outline") - parser.add_argument("--out") - parser.add_argument("--bundle") - args = parser.parse_args() - - project_dir = Path(args.project).resolve() - output_layout = ensure_output_layout(project_dir) - bundle = normalize_bundle(args.bundle) - outline_path = Path(args.outline).resolve() if args.outline else ( - get_bundle_outline_path(output_layout, bundle) if bundle else output_layout["work"] / "final_outline.json" - ) - if not outline_path.exists(): - raise FileNotFoundError(f"未找到目录事实源: {outline_path}。目录判断应由 AI 完成,然后再调用本脚本渲染。") - - out_path = Path(args.out).resolve() if args.out else ( - get_bundle_outline_docx_path(output_layout, bundle) if bundle else output_layout["final"] / "投标文件_目录版.docx" - ) - build_docx(read_json(outline_path), out_path) - - -if __name__ == "__main__": - main() diff --git a/scripts/run_project_pipeline.py b/scripts/run_project_pipeline.py deleted file mode 100644 index 25e8a90..0000000 --- a/scripts/run_project_pipeline.py +++ /dev/null @@ -1,80 +0,0 @@ -from __future__ import annotations - -import argparse -import subprocess -import sys -from pathlib import Path - - -SCRIPT_DIR = Path(__file__).resolve().parent - - -def run(script_name: str, *extra_args: str) -> None: - subprocess.run([sys.executable, str(SCRIPT_DIR / script_name), *extra_args], check=True) - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--project", required=True) - parser.add_argument( - "--tool", - required=True, - help="低层开发辅助操作。真正 workflow 由 SKILL.md 定义,不由本脚本编排。", - ) - parser.add_argument("--outline") - parser.add_argument("--content") - parser.add_argument("--input") - parser.add_argument("--out") - parser.add_argument("--bundle") - args = parser.parse_args() - - project_dir = Path(args.project).resolve() - tool_aliases = { - "extract": "parse-rfp", - "review-outline": "render-outline", - "compose": "render-bid", - "scan-materials": "scan-project", - } - selected_tool = tool_aliases.get(args.tool, args.tool) - if selected_tool == "parse-rfp": - run("extract_rfp_docx.py", "--project", str(project_dir)) - return - if selected_tool == "scan-project": - run("scan_project_materials.py", "--project", str(project_dir)) - return - if selected_tool == "render-outline": - extra_args = ["--project", str(project_dir)] - if args.outline: - extra_args.extend(["--outline", args.outline]) - if args.out: - extra_args.extend(["--out", args.out]) - if args.bundle: - extra_args.extend(["--bundle", args.bundle]) - run("review_outline_and_generate_toc.py", *extra_args) - return - if selected_tool == "render-bid": - extra_args = ["--project", str(project_dir)] - if args.content: - extra_args.extend(["--content", args.content]) - if args.out: - extra_args.extend(["--out", args.out]) - if args.bundle: - extra_args.extend(["--bundle", args.bundle]) - run("compose_bid_docx.py", *extra_args) - return - if selected_tool == "write-large-json": - extra_args: list[str] = [] - if not args.input: - raise ValueError("write-large-json 模式必须提供 --input。") - if not args.out: - raise ValueError("write-large-json 模式必须提供 --out。") - extra_args.extend(["--input", args.input, "--out", args.out]) - run("write_large_json.py", *extra_args) - return - - allowed = ["parse-rfp", "scan-project", "render-outline", "render-bid", "write-large-json"] - raise ValueError(f"不支持的 tool: {args.tool}。允许值:{', '.join(allowed)}") - - -if __name__ == "__main__": - main() diff --git a/scripts/run_skill.py b/scripts/run_skill.py deleted file mode 100644 index 607edac..0000000 --- a/scripts/run_skill.py +++ /dev/null @@ -1,58 +0,0 @@ -from __future__ import annotations - -import argparse -import subprocess -import sys -from pathlib import Path - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--project", required=True) - parser.add_argument( - "--mode", - required=True, - help="低层开发辅助操作。真正 workflow 由 SKILL.md 定义,不由 CLI 固定。", - ) - parser.add_argument("--outline") - parser.add_argument("--content") - parser.add_argument("--input") - parser.add_argument("--out") - parser.add_argument("--bundle") - args = parser.parse_args() - - mode_aliases = { - "extract": "parse-rfp", - "review-outline": "render-outline", - "compose": "render-bid", - "scan-materials": "scan-project", - "safe-write-json": "write-large-json", - } - selected_mode = mode_aliases.get(args.mode, args.mode) - valid_modes = {"parse-rfp", "scan-project", "render-outline", "render-bid", "write-large-json"} - if selected_mode not in valid_modes: - raise ValueError(f"不支持的 mode: {args.mode}。允许值:{', '.join(sorted(valid_modes))}") - - command = [ - sys.executable, - str(Path(__file__).resolve().parent / "run_project_pipeline.py"), - "--project", - args.project, - "--tool", - selected_mode, - ] - if args.outline: - command.extend(["--outline", args.outline]) - if args.content: - command.extend(["--content", args.content]) - if args.input: - command.extend(["--input", args.input]) - if args.out: - command.extend(["--out", args.out]) - if args.bundle: - command.extend(["--bundle", args.bundle]) - subprocess.run(command, check=True) - - -if __name__ == "__main__": - main() diff --git a/scripts/scan_project_materials.py b/scripts/scan_project_materials.py deleted file mode 100644 index 9bf9fe0..0000000 --- a/scripts/scan_project_materials.py +++ /dev/null @@ -1,97 +0,0 @@ -from __future__ import annotations - -import argparse -from pathlib import Path -from typing import Any - -from common import MATERIAL_CATALOG, ensure_output_layout, iter_material_entries, list_files, safe_filename, write_json, write_text - - -def match_catalog(file_name: str) -> dict[str, Any] | None: - lowered = file_name.lower() - for item in MATERIAL_CATALOG: - if any(keyword.lower() in lowered for keyword in item["keywords"]): - return item - return None - - -def serialize_files(project_dir: Path, paths: list[Path], source_root: Path) -> list[dict[str, Any]]: - results: list[dict[str, Any]] = [] - for path in paths: - catalog = match_catalog(path.name) - results.append( - { - "name": path.name, - "path": str(path), - "relative_path": str(path.relative_to(project_dir)), - "source_root": source_root.name, - "source_root_path": str(source_root), - "matched_catalog_key": catalog["key"] if catalog else "", - "matched_catalog_label": catalog["label"] if catalog else "", - "safe_name": safe_filename(path.stem), - } - ) - return results - - -def build_inventory(project_dir: Path) -> dict[str, Any]: - scan_roots: list[dict[str, str]] = [] - material_files: list[dict[str, Any]] = [] - for entry in iter_material_entries(project_dir): - scan_roots.append( - { - "name": entry.name, - "path": str(entry), - "type": "file" if entry.is_file() else "directory", - } - ) - files = [entry] if entry.is_file() else list_files(entry) - material_files.extend(serialize_files(project_dir, files, entry)) - material_files.sort(key=lambda item: item["relative_path"]) - return { - "project_name": project_dir.name, - "project_dir": str(project_dir), - "scan_roots": scan_roots, - "material_files": material_files, - "summary": { - "scan_root_count": len(scan_roots), - "file_count": len(material_files), - "hinted_count": len([item for item in material_files if item["matched_catalog_key"]]), - }, - } - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--project", required=True) - parser.add_argument("--out") - args = parser.parse_args() - - project_dir = Path(args.project).resolve() - output_path = Path(args.out).resolve() if args.out else ensure_output_layout(project_dir)["work"] / "material_inventory.json" - inventory = build_inventory(project_dir) - write_json(output_path, inventory) - - report_lines = [f"# {inventory['project_name']} 材料盘点", ""] - report_lines.append(f"- 扫描根:{inventory['summary']['scan_root_count']} 个") - report_lines.append(f"- 发现材料:{inventory['summary']['file_count']} 份") - report_lines.append(f"- 命中弱提示:{inventory['summary']['hinted_count']} 份") - report_lines.extend(["", "## 扫描根"]) - report_lines.extend( - [f"- {item['name']} ({item['type']})" for item in inventory["scan_roots"]] - or ["- 暂无"] - ) - report_lines.extend(["", "## 发现的材料"]) - report_lines.extend( - [ - f"- {item['relative_path']}" - + (f" [提示:{item['matched_catalog_label']}]" if item["matched_catalog_label"] else "") - for item in inventory["material_files"] - ] - or ["- 暂无"] - ) - write_text(output_path.with_suffix(".md"), "\n".join(report_lines)) - - -if __name__ == "__main__": - main() diff --git a/scripts/search_docx_json.py b/scripts/search_docx_json.py deleted file mode 100644 index 18f390d..0000000 --- a/scripts/search_docx_json.py +++ /dev/null @@ -1,76 +0,0 @@ -from __future__ import annotations - -import argparse -import json -import re -from pathlib import Path -from typing import Any - -from common import read_json, write_json - - -def compile_patterns(contains: list[str], regexes: list[str]) -> tuple[list[str], list[re.Pattern[str]]]: - compiled = [re.compile(pattern, re.IGNORECASE) for pattern in regexes] - return contains, compiled - - -def match_block(block: dict[str, Any], contains: list[str], regexes: list[re.Pattern[str]], kinds: set[str], heading_only: bool, block_ids: set[str]) -> bool: - if kinds and block.get("kind") not in kinds: - return False - if block_ids and block.get("id") not in block_ids: - return False - if heading_only and not block.get("heading"): - return False - - text = block.get("text", "") - if contains and not all(term.lower() in text.lower() for term in contains): - return False - if regexes and not all(regex.search(text) for regex in regexes): - return False - return True - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--graph", required=True) - parser.add_argument("--contains", action="append", default=[]) - parser.add_argument("--regex", action="append", default=[]) - parser.add_argument("--kind", action="append", default=[]) - parser.add_argument("--heading-only", action="store_true") - parser.add_argument("--block-id", action="append", default=[]) - parser.add_argument("--limit", type=int, default=20) - parser.add_argument("--out") - args = parser.parse_args() - - graph = read_json(Path(args.graph).resolve()) - contains, regexes = compile_patterns(args.contains, args.regex) - kinds = set(args.kind) - block_ids = set(args.block_id) - - matches = [ - block - for block in graph.get("blocks", []) - if match_block(block, contains, regexes, kinds, args.heading_only, block_ids) - ][: args.limit] - - result = { - "query": { - "contains": contains, - "regex": args.regex, - "kind": args.kind, - "heading_only": args.heading_only, - "block_ids": args.block_id, - "limit": args.limit, - }, - "matches": matches, - "count": len(matches), - } - - if args.out: - write_json(Path(args.out).resolve(), result) - else: - print(json.dumps(result, ensure_ascii=False, indent=2)) - - -if __name__ == "__main__": - main() diff --git a/scripts/setup_venv.ps1 b/scripts/setup_venv.ps1 deleted file mode 100644 index c471628..0000000 --- a/scripts/setup_venv.ps1 +++ /dev/null @@ -1,17 +0,0 @@ -$ErrorActionPreference = "Stop" - -$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$skillRoot = Split-Path -Parent $scriptDir -$venvPath = Join-Path $skillRoot ".venv" - -if (-not (Test-Path $venvPath)) { - python -m venv $venvPath -} - -$pythonExe = Join-Path $venvPath "Scripts\python.exe" - -& $pythonExe -m pip install --upgrade pip -& $pythonExe -m pip install python-docx lxml pandas Pillow matplotlib PyYAML - -Write-Host "Virtual environment is ready:" $venvPath - diff --git a/scripts/write_large_json.py b/scripts/write_large_json.py deleted file mode 100644 index 2243d10..0000000 --- a/scripts/write_large_json.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import annotations - -import argparse -import json -from pathlib import Path -from typing import Any - -from common import write_json_atomic - - -def load_json_input(path: Path) -> Any: - if not path.exists(): - raise FileNotFoundError(f"未找到输入 JSON 文件: {path}") - try: - return json.loads(path.read_text(encoding="utf-8-sig")) - except json.JSONDecodeError as exc: - raise ValueError(f"输入文件不是合法 JSON: {path}") from exc - - -def write_large_json(input_path: Path, output_path: Path) -> None: - data = load_json_input(input_path) - write_json_atomic(output_path, data, indent=2, ensure_ascii=False) - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--input", required=True, help="UTF-8/UTF-8-SIG JSON 源文件路径") - parser.add_argument("--out", required=True, help="目标 JSON 文件路径") - args = parser.parse_args() - - input_path = Path(args.input).resolve() - output_path = Path(args.out).resolve() - write_large_json(input_path, output_path) - - -if __name__ == "__main__": - main()