From a8158d8b2239f1eac15f7033544e7b4aa2649bff Mon Sep 17 00:00:00 2001 From: sladro Date: Sat, 14 Mar 2026 17:00:12 +0800 Subject: [PATCH] format --- SKILL.md | 92 ++- references/docx-assembly.md | 57 +- references/docx-ops.md | 52 +- references/outline-stage.md | 235 ++++---- references/understandbid.md | 21 +- scripts/__pycache__/docx_cli.cpython-311.pyc | Bin 4551 -> 6931 bytes .../__pycache__/docx_create.cpython-311.pyc | Bin 0 -> 1400 bytes .../__pycache__/docx_ops_lib.cpython-311.pyc | Bin 51941 -> 76169 bytes .../__pycache__/docx_patch.cpython-311.pyc | Bin 2081 -> 2386 bytes .../__pycache__/outline_check.cpython-311.pyc | Bin 8966 -> 21284 bytes .../outline_export.cpython-311.pyc | Bin 0 -> 1405 bytes scripts/docx_cli.py | 8 +- scripts/docx_ops_lib.py | 534 +++++++++++++++++- scripts/docx_patch.py | 8 +- scripts/outline_check.py | 413 +++++++++++--- 15 files changed, 1152 insertions(+), 268 deletions(-) create mode 100644 scripts/__pycache__/docx_create.cpython-311.pyc create mode 100644 scripts/__pycache__/outline_export.cpython-311.pyc diff --git a/SKILL.md b/SKILL.md index 8ce0ba7..112092a 100644 --- a/SKILL.md +++ b/SKILL.md @@ -12,18 +12,32 @@ description: 面向中文 IT/系统集成类投标项目的投标文件生成与 ## 目标 - `outline` - 产出技术标书和商务(包含其它部分除技术标)标书目录,可以同时产出,也可以只产出一种 + 产出技术标书和商务(包含其它部分除技术标)标书目录;默认交付顺序是先技术目录,后商务目录 - `outline+business` 通过产出的商务把标书目录,生成完整商务(包含除技术标)之外的标书。 - `outline+technical` 通过产出的技术把标书目录,生成完整技术标书。 +## 成品导向原则 + +门禁、脚本检查和渲染校验都只是基础门槛,不是完成定义。 +本 skill 的最终目标是生成可直接用于投标的中文标书;无论目录、标题、正文、图表、附件还是最终 Word 排版,都必须服务“可直接交付给评委或采购人阅读”的成品标准。 + +默认文档格式基线如下,若招标文件或正式模板另有明确要求,则以后者优先: + +1. 标题编号默认使用 `1 / 1.1 / 1.1.1 / 1.1.1.1`。 +2. 一级标题默认黑体、小三;二级标题黑体、四号;三级标题黑体、小四;四级标题默认楷体或黑体小四,并在同一项目内保持统一。 +3. 正文默认宋体、小四,首行缩进 2 字符,1.5 倍行距。 +4. 表格默认宋体,五号或小五,表头加粗。 +5. 图、表、附件标题统一编号,默认采用 `图3-1 XXX / 表4-2 XXX / 附件5-1 XXX`。 +6. 占位内容必须显式标明“占位/待替换/待补充”,不得伪装成正式材料。 + ## 执行规则,严格遵守 1. 必须按照规定workflow执行。 2. 所有输出只能写到和用户提供的目录下的 `work/`、`reports/`、`final/`。所有创建的文件也只能在用户提供的这个目录下。 3. 不能补充任何脚本,只能用项目现有脚本,所有脚本在scripts目录下。 -4. 脚本执行需要使用本 skill 内 `.venv/` 作为虚拟环境来执行,脚本启动命令是虚拟环境的python,不是python3。 +4. 脚本执行需要使用本 skill 内 `.venv/`作为虚拟环境来执行,脚本启动命令是虚拟环境的python,不是python3。记住是本skill当前文件夹下`.venv/`目录,不是你现在的工作目录,是本skill的目录。 5. 所有操作word脚本使用本skill提供的工具脚本 ## 现有工具 @@ -40,10 +54,10 @@ description: 面向中文 IT/系统集成类投标项目的投标文件生成与 用途:根据结构化输入直接创建新的 Word 文档,适合生成目录版 DOCX、占位稿或空白章节骨架。 - `scripts/outline_check.py` - 用途:对目录阶段的结构化结果做轻量门禁检查,重点检查抽象技术标题是否直接落成叶子节点。 + 用途:对目录阶段的结构化结果做门禁检查,重点检查目录深度、抽象标题下钻、对象化节点、重复切面和商务中的技术占位。 - `scripts/outline_export.py` - 用途:在目录门禁通过后,按已完成校验的层级结果导出结构化 JSON,并在目录无法继续下钻后生成目录版 DOCX。 + 用途:在目录门禁通过后导出结构化 JSON,并生成最终目录版 DOCX。 - `scripts/docx_patch.py` 用途:对现有 Word 标书执行插入、替换、删除等修改操作,把已经生成好的标书内容准确写入指定位置。 @@ -57,46 +71,16 @@ description: 面向中文 IT/系统集成类投标项目的投标文件生成与 具体操作方式、参数说明和示例见 `references/docx-ops.md`。 +## References -## 执行流程 +- 理解招标:`references/understandbid.md` +- 目录阶段:`references/outline-stage.md` +- DOCX 脚本接口:`references/docx-ops.md` +- DOCX 渲染与交付:`references/docx-assembly.md` -1. 理解资料阶段 -- 读 `SKILL.md`。 -- 读当前项目 `rfp/` 原文。 -- 若主文档不足,继续在当前项目内深挖评分办法、技术规范、附件、分册和其他候选原文。 -- 建立项目约束、评分约束、风险约束、三层业务分类和输出边界。 -2. outline 阶段 -按照用户要求 - - 目录节点统一使用 `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 阶段 -- 只写 `work/final_bid_content_technical.json` 中的 `workflow_bucket=technical`。 -5. other 阶段 -- 只补 `workflow_bucket=other`,并并入 `work/final_bid_content_business_other.json`。 -6. final 阶段 -- 生成: - - `final/技术标.docx` - - `final/商务及其他.docx` +## 非 outline 阶段规则 -## 阶段规则 - -### 1. 理解规则阶段 -必须遵守: - - `/references/understandbid.md` - -### 1. outline -必须遵守 - - `references/outline-stage.md` - - `references/docx-ops.md` - -### 2. business +### business 1. 只写商务及其他文件中的 `workflow_bucket=business`,并配合 `workflow_bucket=other` 文件中的技术占位。 2. 商务事实只认招标文件明示内容和用户真实材料。 @@ -126,7 +110,7 @@ description: 面向中文 IT/系统集成类投标项目的投标文件生成与 3. `业绩与团队`、`财务与纳税社保` 不得用通用表述冒充真实材料。 4. `附件与索引` 只负责承载证据入口和材料定位,不替代正文判断。 -### 3. technical +### technical 1. 只写 `work/final_bid_content_technical.json` 中的 `workflow_bucket=technical`。 2. 技术正文开始前,必须先明确总目标、建设主线、分系统主次和评委检索路径。 @@ -137,10 +121,17 @@ description: 面向中文 IT/系统集成类投标项目的投标文件生成与 7. 只能对 `final_outline_technical.json` 中的叶子节点写正文,不允许直接用父节点概述代替其全部子节点。 8. 若叶子节点仍然是抽象标题,必须先回退目录阶段继续下钻,不得硬写正文。 -### 4. other/finalize +### other/finalize 3. 不额外创造默认“报价子 workflow”。 4. 通过总体验收前,不得对外宣称“完整投标文件”。 +### final + +生成: + +- `final/技术标.docx` +- `final/商务及其他.docx` + ## 最终验收 导出前必须同时检查: @@ -154,11 +145,12 @@ description: 面向中文 IT/系统集成类投标项目的投标文件生成与 7. 图表是否真的承载评审主题。 8. 证据链是否完整。 +除上述业务验收外,还必须满足文档成品验收: + +1. 标题命名自然、专业、适合直接作为投标 Word 目录使用,不允许出现只为通过门禁而生的生硬标题。 +2. 标题编号连续,标题层级、字体、字号、段落格式符合默认标书格式或模板格式。 +3. 图表编号、附件编号连续且可追溯。 +4. 目录、正文、图表、附件、占位提示在文档中的位置关系正确,不错位、不跳号、不混册。 +5. 最终 DOCX 渲染校验、格式校验、编号校验、占位扫描应共同通过后,才可视为“可交付”。 + 任一项不过,不得汇报“已完成”,但可以生成完整的占位草稿。占位内容仅限于资质、商务、声明、索引、技术转引等非技术实质内容;技术部分必须完整呈现于技术标,不得用占位代替。 - -## References - -按阶段和任务读取,不要一次性全读: -- DOCX 渲染与交付: - - `references/docx-assembly.md` - - `references/docx-ops.md` diff --git a/references/docx-assembly.md b/references/docx-assembly.md index 6fc96b2..c158efc 100644 --- a/references/docx-assembly.md +++ b/references/docx-assembly.md @@ -1,11 +1,50 @@ - ## 排版要求 -1. 标题层级采用 `1 / 1.1 / 1.1.1`。 -2. 正文默认使用中文常见字体与统一字号。 -3. 表格标题、图片标题、附件标题统一编号。 -4. 附件占位图必须带材料名称和替换提示。 -5. 目录可使用 Word 域代码生成,允许用户打开后更新域。 -6. 图、表标题建议置于图表上方或紧邻图表位置,并与对应正文段落相邻,避免标题与承载内容分离。 -7. 图表标题应采用“图3-1 XXX / 表4-2 XXX / 附件5-1 XXX”格式,章节号应与当前正文主章节一致。 -8. 图表编号、附件编号在全文中必须连续、可追溯,不得出现跳号、重号或标题含义不明。 +1. 默认标题编号采用 `1 / 1.1 / 1.1.1 / 1.1.1.1`。 +2. 一级标题默认黑体、小三;二级标题黑体、四号;三级标题黑体、小四;四级标题默认楷体或黑体小四。 +3. 正文默认宋体、小四,首行缩进 2 字符,1.5 倍行距。 +4. 表格默认宋体,五号或小五,表头加粗。 +5. 表格标题、图片标题、附件标题统一编号。 +6. 附件占位图必须带材料名称和替换提示。 +7. 目录可使用 Word 域代码生成,允许用户打开后更新域;若当前 workflow 生成的是目录版 DOCX,则标题编号本身应足以直接承载目录阅读。 +8. 图、表标题建议置于图表上方或紧邻图表位置,并与对应正文段落相邻,避免标题与承载内容分离。 +9. 图表标题应采用“图3-1 XXX / 表4-2 XXX / 附件5-1 XXX”格式,章节号应与当前正文主章节一致。 +10. 图表编号、附件编号在全文中必须连续、可追溯,不得出现跳号、重号或标题含义不明。 + +## 最终组装流程 + +1. 先确认目录已定稿: +- `work/outline_basis_summary.md` +- `work/final_outline_technical.json` +- 若需要商务及其他,则再确认 `work/final_outline_business_other.json` + +2. 再确认正文来源已就绪: +- 技术正文来自 `work/final_bid_content_technical.json` +- 商务及其他正文来自 `work/final_bid_content_business_other.json` + +3. 组装最终文档时,必须按定稿目录写入,不得临时改目录层级或改章节顺序。 + +4. 技术标和商务及其他应分别输出: +- `final/技术标.docx` +- `final/商务及其他.docx` + +5. 组装后必须执行渲染校验,确认: +- 文档可正常打开 +- 标题层级、编号、图表编号连续 +- 附件占位、技术转引、目录位置没有错位 +- 标题和正文样式符合默认标书格式或正式模板格式 +- 最终 Word 文档达到可直接用于投标的成品标准,而不是仅仅能打开 + +6. 若渲染校验未通过,不得宣称已完成最终交付。 + +## 默认验收输出 + +脚本报告除了基础的 `status`、`warnings`、`errors` 外,应至少补充: + +- `format_profile` +- `numbering_validation` +- `caption_validation` +- `toc_validation` +- `acceptance_checks` + +只有当业务验收、格式验收、编号验收、占位扫描和渲染验收共同满足时,才能宣称“可交付”。 diff --git a/references/docx-ops.md b/references/docx-ops.md index 767e894..ff4fb02 100644 --- a/references/docx-ops.md +++ b/references/docx-ops.md @@ -48,6 +48,9 @@ ```json { "output_docx": "D:/work/generated-outline.docx", + "docx_style_profile": "default_bid", + "numbering_mode": "explicit_text", + "template_docx": null, "title": "目录测试", "blocks": [ {"type": "heading", "level": 1, "text": "技术标目录"}, @@ -85,6 +88,11 @@ - `block_count` - `blocks` - `final_summary` +- `format_profile` +- `numbering_validation` +- `caption_validation` +- `toc_validation` +- `acceptance_checks` ## 0.1 目录门禁检查 @@ -106,11 +114,21 @@ - 目录节点使用 `type=heading` - 目录层级使用 `level` - 子节点放在 `children` +- 顶层可选 `outline_policy` + - 作为默认策略,不建议直接把例外开到整份目录 +- 单个目录节点可选 `policy` + - `allow_service_facets: true|false` + - `respect_fixed_structure: true|false` + - 只对该节点及其子树生效 最小示例: ```json { + "outline_policy": { + "allow_service_facets": false, + "respect_fixed_structure": false + }, "blocks": [ { "type": "heading", @@ -124,6 +142,17 @@ "children": [ {"type": "heading", "level": 3, "text": "建设目标与原则"} ] + }, + { + "type": "heading", + "level": 2, + "text": "运维服务方案", + "policy": { + "allow_service_facets": true + }, + "children": [ + {"type": "heading", "level": 3, "text": "服务组织与分工"} + ] } ] } @@ -133,15 +162,19 @@ ### 当前检查内容 -- 抽象标题是否直接作为叶子节点 +- 目录深度与抽象标题下钻 +- 对象化子节点与重复切面 +- 商务及其他中的技术占位是否被错误展开 +- 标题 `level` 是否逐级合法 +- 服务型项目 / 固定目录例外是否只在指定节点生效 - `children` 类型是否合法 - block 是否为对象 ## 0.2 目录阶段最终导出 -本节只描述 `outline_export.py` 的接口,不定义目录阶段 workflow。 -目录阶段的循环下钻、逐级检查、逐级写出 JSON 规则,以 `references/outline-stage.md` 为唯一准则。 -`outline_export.py` 只在目录已经全部定稿、且无法继续下钻后调用,用于生成最终正式产物。 +本节只描述 `outline_export.py` 的接口,不定义目录阶段 workflow。 +目录阶段规则以 `references/outline-stage.md` 为唯一准则。 +`outline_export.py` 只在目录已经终检通过后调用,用于生成最终正式产物。 ### 命令 @@ -167,6 +200,9 @@ "title": "商务及其他目录", "blocks": [] }, + "docx_style_profile": "default_bid", + "numbering_mode": "explicit_text", + "template_docx": null, "technical_outline_json": "D:/work/final_outline_technical.json", "business_outline_json": "D:/work/final_outline_business_other.json", "technical_docx": "D:/final/技术标_目录版.docx", @@ -297,6 +333,9 @@ { "source_docx": "D:/work/source.docx", "output_docx": "D:/work/output.docx", + "docx_style_profile": "default_bid", + "numbering_mode": "explicit_text", + "template_docx": null, "operations": [] } ``` @@ -425,6 +464,11 @@ - `images` - `errors` - `warnings` +- `format_profile` +- `numbering_validation` +- `caption_validation` +- `toc_validation` +- `acceptance_checks` 如果系统缺少 `soffice` 或图片渲染依赖,报告会返回 `render_skipped` 或带 warning,而不是直接把 patch 结果判定为失败。 diff --git a/references/outline-stage.md b/references/outline-stage.md index 66aee2c..b96ca7f 100644 --- a/references/outline-stage.md +++ b/references/outline-stage.md @@ -2,9 +2,9 @@ ## 目标 -可成两份正式目录:技术目录和商务目录(包含除技术的其它部分) +优先产出可直接写正文的技术目录;商务及其他目录在需要时继续补齐。 -若未通过定稿门禁,则停止目录输出,不生成任何正式双目录,也不生成目录 Word。 +若终检未通过,则停止正式目录输出,不生成正式目录 JSON,也不生成目录版 Word。 ## 唯一执行流程 @@ -13,86 +13,66 @@ ### 结构表达 1. 目录阶段的最小结构化输入统一使用 `heading(level/text/children)`。 -2. 任何层级的目录都必须先结构化,再检查,再决定是否继续下钻。 +2. 目录先成树,再补钻,再终检;不要回到逐层落盘、逐层审批的重流程。 +3. 目录节点只保留可写、可审、可导出的正式标题,不要把分析说明写进目录树。 +4. 目录标题不仅要通过门禁,还要满足正式投标文档可读性:命名自然、专业、稳定,不能出现明显“为过检查器而造”的生硬标题。 -### 逐级下钻循环 +## 三步法 -1. 根据读取到的 `rfp/` 主文档和当前项目候选材料,建立评分点、风险点、证据点和正式交付边界。 -2. 目录生成必须严格按层执行,禁止一次性直接生成完整目录。 -3. 每一轮只允许生成“当前层级的直接子节点”,不得预先生成孙级及以下节点。 -4. 先生成当前层级目录,并写出当前层级中间 JSON。 -5. 对当前层级目录执行检查核对。 -6. 若检查未通过,则停止后续下钻,禁止生成任何正式目录 JSON 和目录版 Word。 -7. 若检查通过,则将当前层级节点记为“已批准可下钻节点”。 -8. 下一轮只能基于上一轮“已批准可下钻节点”生成其直接子节点,不得跨节点、跨层级展开。 -9. 重复“生成当前层级 -> 写出当前层级 JSON -> 检查核对 -> 标记已批准节点 -> 继续下钻”,直到当前层级所有节点都无法继续下钻为止。 -10. 只有在全部层级完成检查核对后,才允许执行最终目录导出。 +### 1. 理解资料 -### 层级执行约束 +1. 读取 `rfp/` 主文档和当前项目候选材料。 +2. 提取并整理: + - `废标/否决项` + - `合规项` + - `评分项` + - 明确系统、子系统、设备、服务对象清单 + - 项目约束、风险约束、证据边界 +3. 形成一份简短的“目录依据摘要”,至少覆盖: + - 主要评分点 + - 主要风险点 + - 明确对象清单 + - 是否存在必须保留的原始目录顺序 +4. 目录依据摘要必须落盘为 `work/outline_basis_summary.md`,供后续目录、正文和终检复核。 -1. 生成一级目录时,一级节点不得携带任何二级以下 `children`。 -2. 生成某个一级节点的二级目录时,只允许补充该一级节点的直接 `children`,不得同时补充其三级节点。 -3. 生成某个二级节点的三级目录时,只允许补充该二级节点的直接 `children`,不得同时补充其四级节点。 -4. 任何节点在其父节点未通过当前轮检查前,不得进入下一层下钻。 -5. 未经当前轮检查通过的节点,不得写入最终目录。 -6. 不得以“先完整想好再拆回各层”的方式规避逐级流程;若最终目录中的下级节点没有对应上轮批准依据,视为流程违规。 +### 2. 生成候选目录 -### 中间产物要求 +1. 一次性生成完整技术目录树。 +2. 默认先把技术目录做对;商务及其他目录作为第二优先级。 +3. 若用户只要求目录,默认先交技术目录;商务目录仅在用户明确需要时再继续补齐。 +4. 招标文件已有明确章节顺序时,优先继承原顺序和原命名;只有在过粗、缺层或缺承载位时才补层。 +5. 招标文件结构不完整时,可按项目类型默认骨架补缺,但只能补必要层级,不要堆砌模板化大纲。 -1. 每一轮都必须落盘当前层级的中间 JSON,不得只在对话中描述。 -2. 中间 JSON 只承载本轮新增的直接子节点,不得混入更深层级内容。 -3. 若缺少任一层级的中间 JSON 或检查记录,则视为目录流程未完成,不得导出正式目录。 +### 3. 终检与补钻 -### 调试输出要求 +1. 候选目录生成后,运行 `scripts/outline_check.py`。 +2. 若检查失败,只修补失败节点,不推倒重来。 +3. 修补后再检查一次。 +4. 默认最多连续修补两轮;若仍未通过,但失败点清晰且可继续收敛,可以继续修补,并必须说明原因。 +5. 检查通过后,再导出正式目录 JSON 和目录版 DOCX。 +6. `outline_export.py` 只用于最终正式导出,不承担目录生成和目录修补职责。 +7. 导出后的目录版 DOCX 还必须满足默认标书格式、标题编号连续、文档可直接作为投标目录使用。 -1. 每完成一轮层级生成后,必须先输出一行简短调试信息,再进入检查步骤。 -2. 每完成一轮检查后,必须输出一行检查结果调试信息。 -3. 调试输出只允许描述当前轮次、当前节点、执行状态,不得展开成长篇解释。 -4. 调试输出应尽量固定格式,便于人工核对逐级流程是否被跳过。 -5. 若当前轮次包含多个节点,应逐个节点输出,不得用“本轮已完成”笼统代替。 +## 默认优先级 -推荐格式: +1. 技术目录优先于商务及其他目录。 +2. 评分点优先于普通合规项进入技术目录主干。 +3. 明确系统/子系统/设备/服务对象优先于抽象管理性标题进入下钻结构。 +4. 招标文件原始章节优先于默认骨架;默认骨架只用于补缺。 -```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` - - `final/技术标_目录版.docx` - - `final/商务及其他_目录版.docx` +1. 招标文件明确固定了章节名称、顺序或颗粒度,且不宜改写。 +2. 项目主类型为运维服务、驻场服务、咨询培训等服务型项目,评分重点明确落在组织、流程、SLA、响应、考核等服务切面。 -### 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] -``` +1. 仍然禁止把 `技术方案`、`实施方案` 等抽象标题直接作为叶子节点。 +2. 可以用组织、流程、SLA、响应、保障、考核等服务切面完成下钻,不强制要求模块、子系统、设备类对象节点。 +3. 例外只对命中的节点或分支生效,不得整份目录一起放宽。 +4. 必须在对应节点上显式标记例外策略,并在 `work/outline_basis_summary.md` 中写明“例外原因”,说明是固定目录还是服务型项目。 ## 拆分规则 @@ -100,73 +80,92 @@ flowchart TD 2. 商务及其他目录保留 business/other 目录,并在技术内容应出现的位置保留技术占位。 3. 若招标文件明确规定分册、分标或顺序,技术占位必须出现在原规定位置。 4. 若招标文件未明确规定位置,则默认在 unified/canonical outline 中技术部分入口位置保留一个一级占位章节。 -6. 商务及其他中的技术占位不能成为技术门禁放行依据。 +5. 商务及其他中的技术占位不能成为技术门禁放行依据。 +6. 商务及其他中的技术占位只能是单节点占位,不得展开成技术正文树。 -## 抽象标题处理与下钻强制约束(核心规则) +## 抽象标题处理与下钻强制约束 以下标题列为默认非法终点,绝对禁止作为技术标叶子节点直接写正文: [技术方案, 服务方案, 实施方案, 服务保障及措施, 售后服务和质保期服务计划, 项目理解, 解决方案, 系统设计, 平台建设方案, 系统建设方案, 总体方案, 培训方案, 运维方案] -当遇到上述标题时,必须强制下钻。下钻不能是单一的,不能只有一个子标题,必须形成完整的结构化切面。 -必须包含但不限于以下维度的同级子标题: +遇到上述标题时,必须继续下钻。下钻遵循以下最小要求: -- 原则与目标 -- 架构与设计 -- 模块与内容 -- 流程与机制 -- 计划与验收 -- 保障与风控 +1. 不能只有一个直接子标题。 +2. 不能全部是“原则、目标、保障、计划”这类管理性切面。 +3. 至少出现一个对象化子标题,例如: + - 模块 + - 子系统 + - 设备 + - 接口 + - 功能单元 + - 服务项 +4. 同一父节点下不要堆出语义重复的小标题,例如: + - 实施方案 + - 实施计划 + - 实施步骤 - -错误示例(严禁此类浅层结构,发现即打回): +当招标文件、采购清单、技术参数表、分项报价表、供货一览表已经出现可识别的系统、子系统、设备或服务对象时,技术目录必须优先按这些已明示对象继续下钻。 -5. 技术方案 - 5.1 方案概述 - 5.2 实施方案 - 5.3 售后服务 +## 收敛规则 -正确示例(技术标必须模仿此颗粒度与深度继续下钻): +“可以停止下钻”必须同时满足: -5. 技术方案 - 5.1 总体设计方案 - 5.1.1 总体设计原则(安全性、扩展性、规范性等) - 5.1.2 总体架构设计(分层架构图解) - 5.1.3 关键技术路线选型 - 5.2 核心系统建设内容(下钻到具体模块) - 5.2.1 数据中心模块功能实现 - 5.2.2 接口与数据流向设计 - 5.3 实施与交付计划 - 5.3.1 实施阶段划分与里程碑 - 5.3.2 资源配置与人员安排 - 5.4 技术难点分析与应对预案 - +1. 当前节点已经能直接承载正文。 +2. 继续下钻只会制造空泛标题、伪细分或重复切面。 +3. 评分点、风险点、证据点已经都有承载位。 +4. 若存在明确对象清单,目录已经按对象展开到安全层级。 -商务及其他中的技术占位不适用上述“继续下钻”要求,但只能是占位,不得承载技术正文。 +## 终检清单 -## 强制自检清单 (Pre-Output Checklist) - -在物理生成双正式目录之前,AI 或主代理必须在对话框中输出以下自我检查单,所有项为“是”才可调用写入工具: +在调用写入工具前,AI 或主代理必须先完成以下自检,全部满足后才允许导出: ```text -【目录深度强制自检】 -1. 是否存在直接以“技术方案”或“实施方案”作为叶子节点的章节?(要求:否) -2. 技术类章节是否已经下钻到第三级或第四级?(要求:是) -3. 技术方案下,是否同时包含了[原则]、[架构]、[内容/模块]?(要求:是) -4. 所有的评分点是否都已在目录中体现?(要求:是) -5. 商务及其他中的技术节点是否只保留占位,不承载技术正文?(要求:是) -6. 招标文件、采购清单、技术参数表、分项报价表、供货一览表已经出现可识别的系统/子系统/设备清单时,技术目录是否已经按照要求下钻?(要求:是) +【目录终检】 +1. 是否不存在直接以“技术方案”“实施方案”等抽象标题作为叶子节点?(要求:是) +2. 技术目录主干是否至少到第三级?(要求:是) +3. 抽象技术标题下是否至少出现一个对象化子节点?(要求:是) +4. 是否不存在“实施方案 / 实施计划 / 实施步骤”这类重复切面堆叠?(要求:是) +5. 所有主要评分点是否都已在目录中有承载位?(要求:是) +6. 商务及其他中的技术节点是否只保留占位,不承载技术正文?(要求:是) +7. 目录标题是否自然、专业,适合直接进入投标 Word 文档?(要求:是) +8. 导出的目录版 DOCX 是否具备统一样式和连续标题编号?(要求:是) ``` -## 定稿门禁 +目录阶段最终回复必须额外附两段简短说明: -正式双目录必须同时满足: +1. `评分点覆盖` + - 列出主要评分点对应到哪个目录章节。 +2. `推断章节` + - 列出哪些章节不是招标文件明示,而是按常规补出的。 +3. `例外原因` + - 若使用了固定目录或服务型项目例外,简短说明原因;未使用则不写。 -1. 第一流程中评分点、风险点、证据点都有承载位。 -2. 技术标中的抽象标题已下钻,直到能承载正文为止。 -3. 商务及其他中的技术部分只保留占位,位置正确。 -4. 显式章节、附表、附件承载位没有被无故遗漏。 -5. 标题层级、编号、归属关系合法,防止自动或手动编号混淆。 -6. 最终目录中的每个节点都能追溯到某一轮已通过检查的层级结果,不存在跳过当前层级检查直接写入的后代节点。 -7. 不得用“目录已经想好”或“逻辑上已定稿”代替文件存在检查。 +## 最终产物 + +技术目录定稿后,允许生成: + +- `work/outline_basis_summary.md` +- `work/final_outline_technical.json` +- `final/技术标_目录版.docx` + +当商务及其他目录也已定稿时,额外生成: + +- `work/final_outline_business_other.json` +- `final/商务及其他_目录版.docx` + +## Mermaid 流程图 + +```mermaid +flowchart TD + A[整理目录依据摘要
评分点 风险点 对象清单] --> B[一次性生成候选技术目录树] + B --> C[执行 outline_check 终检] + C --> D{检查是否通过} + D -- 否 --> E[仅修补失败节点] + E --> F[再次执行 outline_check] + F --> G{第二轮是否通过} + G -- 否 --> H[停止正式导出并报告失败原因] + G -- 是 --> I[导出正式目录 JSON 和目录版 DOCX] + D -- 是 --> I +``` diff --git a/references/understandbid.md b/references/understandbid.md index 772b8a4..aaba2a1 100644 --- a/references/understandbid.md +++ b/references/understandbid.md @@ -56,4 +56,23 @@ 1. 先锁定 `废标/否决项`,确保不漏项。 2. 再补齐 `合规项`,确保正式交付结构完整。 -3. 最后围绕 `评分项` 优化目录颗粒度、技术展开和证据呈现。 \ No newline at end of file +3. 最后围绕 `评分项` 优化目录颗粒度、技术展开和证据呈现。 + +## 项目类型快速归类 + +目录补缺前,先判断项目主类型。只选一个主类型即可,招标文件有明确目录时始终以招标文件为先,以下骨架只用于补缺,不用于覆盖原结构。 + +1. `软件平台类` +- 默认骨架优先包含:项目理解、总体架构、功能模块、接口与数据、实施部署、测试验收、培训运维、安全保障。 +2. `系统集成类` +- 默认骨架优先包含:建设范围、总体集成架构、软硬件配置、子系统建设、安装部署、联调测试、培训交付、运维保障。 +3. `运维服务类` +- 默认骨架优先包含:服务理解、服务组织、服务内容、服务流程、SLA/响应机制、巡检维护、应急保障、考核验收。 +4. `硬件供货/设备建设类` +- 默认骨架优先包含:供货范围、设备选型、技术参数响应、安装实施、联调测试、培训交付、质保售后、安全与风险控制。 + +判断原则: + +- 先看招标范围和评分办法,再定主类型。 +- 若同时包含软件、硬件、服务,以评分权重最高的主线作为主类型。 +- 默认骨架只补一级、二级结构;更深层级仍应由评分点和对象清单驱动。 diff --git a/scripts/__pycache__/docx_cli.cpython-311.pyc b/scripts/__pycache__/docx_cli.cpython-311.pyc index 00580b5f7cd107108c638818de26d8f9414a53dc..3a4645a5563de802e8b0f55a311a08218c04633f 100644 GIT binary patch delta 2457 zcmcIlZ)_7~7{BY?+Pk)Q*RG}A7`?VBY%5xr1Ys-P#=ju2Ap>NIi+Xmw8!PL&@%A>M z=>jptEPmR2zaR=M1_lOVh=e#lm|$X}Uo^dXWc3UQF;Ty$nNh#^$@jgsu8v6pftnskrqta0w|jO*NrOv`DygxW+OPseodQ{desLnJ zq-yc-KRyFR5ab^}aQEV5vju)_X(BoJ1r;TE=%Us^if)44ln={2!Xk=SsK>V^ z98+M-aK9o9lOz2maz%QG?xffP3op_!iapK3yKEf(MmLizTwxM0VdLR6%fr_jY)CTN z5PKpQD7)GMS7<--;!UrFO`sgAP?c4Xu392lIbFdZ*>`Mx zl=F$R8v}aPO*MOhC^##pf?3vwv#CSl>d{a}o+lY`YeA!{K)HVb73x7NDe`O4o>_&) z*P}JBKCr8THb;WaCuQ^1lI#`Kt*da>_KT~)o^Gn?!KbRb#-p;hA&YXbUUw(7*npZCJ(Yz{;!7a5uE4weu6i##A5IWz7j81M6%V18 zEt++lW`zb^DK~tZjUXxRph?%1x&&OglfoaIsc9ltDG)yMd)M(xri8cY(LnoNS6V(@#wDKzZhkM`bC+$g|_35|$Bq1+7{P;wKQ zX$3G?MB^j@o@R#q%#;GoNjOiyVgZZGWA5=F#Ki&l12&x15>!k%B!aIvdcCM~P>53s z3V{qHISry7__5=;ja+pUoB095S(&lb9X>QdhTl8(^;H7ONj3R2hPiW8$F{k#n}wDz z54NC~0JkvMhDgqXNXT5U+d1Gkh_+aid)5jsIlqD5aT`v~+*OOTsL=_XP88`xnRYMJ zVu=rRiwKfdjiXz_L8UlR#5YF>7K3&{bkyNR z3)`>vXyP_q+_o&nOJZCT6S|ntyxqFDdzOXATrGzmcIMS413EKMWCqF% zKWlw#b6hOUoiEv1i?-HB{?HBYf~>dh)WV&5xN|wYyA=y_$cY?%y}-#Bhi1 zb4wYmqfhVX)0lpp=`S+ZrS_bh!#1nM~;_+o37;Siw!AMdzIgb23B)}moeF2fbnFRWhr!qH@crc pze2O5Wrg^EiI*VTi$vS%--`7ZNp`My3G;6?9ksv(cZ9~d{{wd+oG<_Y delta 991 zcmb7DO=uHQ5Z<@hyiGRA=HDbuZ7NY~4xt8YElq8Oic%15>8&73=x%BHle8}zwIM|W z51y>;oTPfR{!~yYf>%M%gGeLdA*Wsi#TG$S5S+K|f<5}!{g`j&o0*-*EOfs~s}FoW zMZk0G#nWQay{yIwTkaSfC4%nO8I8d~d|S_gIFsP$%)&)kzC zO;4dzDw_qfST0pAAeV*<=46I0H)0Scr=dua9v3Pdfga|c9-09QRCw?e8 zGVo6F!;&*eg76C2Gx5NR6el5gE=E_R1mx&R2dz_>rdbD_a>Iy{hfg$4D14(D;zgPJ zy|CjMA~H0T1TxiPF1SWHvvQ&-YXS@Ea^1CtL#~UElMhDe6t!czvN!zCy-ERIX~1o! zZG|Az-D?DM@!dU(N}W!HY{nMMplzd8(ZdM=c4%+=q6cMRJH(FelVOC5&XhO0saB#_-ZKbqFse zj-fpC%06n%5QAT88r}b7M4;?-9S@UMImXVz(&KNaUHp`5; zGBcqga7oN0U?(urqvAI(zHLlGXgbPOvney9Ql7bNFn;4rNiUCIWqdoD%6!#e3mUHm zkAnj-H+RzAqD>i7HND6(KKK*N!B{AtQmpQpUN8&n7;0IJ!x4mv4*|A9!=5fQc%8~u z2js)=VLjZs@gs7VIInz=+cx2KtXFcl&k!K-`QXF+OQn$-{FoZNdG>Zs!`ljnVoBJF zueRZhQo5xNd{C4%tmi6k4 n(a%mH=Knm~^f>9+7WTsgfiEp-iMRU;0Q2vK diff --git a/scripts/__pycache__/docx_create.cpython-311.pyc b/scripts/__pycache__/docx_create.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..36c695441f38c2997ad700000c38687b4f821528 GIT binary patch literal 1400 zcma(QOKTHBcy@Q1Y_i#=K5IkK`aq1B9;y`;6@(BFp$P3M1eR@fs#~)UXD2>Nl?Xlb zB6tw&p+Zkpsi62vvSFcNp->PH-U_{W@|%fiOr_wrv)|13obNRsbUlp#+_T^8Q5m7{ zoah439HiYj!0(WSEUAVZsVqtSEZ5|+47*ZO99+hNhid~)qMUG&<)ovQRp6zp%0ML`C#h*|AK`zb-N?4t`F`n{VQte#U!2_@UknmU^>molJ1R`z8J(bacRHJ|HQEPPX#SBjq(Fi3tFKN_@r8x zvuk7qWy9ycVk_ec+okAo1WG`asOZeNNIiV_KF0($1`;g%YO& z%1cHupg6^EF!&8|l9IryJt3423FXy`VUWZvI8F#_8e?w%$I3F9Ne_cwz+H_2{JK@V zQMu#$6-EM9nYAs)3>cx6s#~yG0qBC5Qd$YB)b?3W5#!t$?Q#Ekj2+W<=`eWX?GC;G zYM?FsSfrl|_4AQ_zENu9^d=r|;o%kiD{kW95Kl&UvWX{qg7n83A()8pL=#W6wL_cQ zXiFOnwUd!{vQhjoH1hu33R`^{=EkGk_-5{ED|a=_U5j$p!lCQY(DlaME&cRbKGZKo z`lUu`3lF|#Z;Eg4h1szvJGPm<(8^v2vlpZ6#gC;BUyktQW_RsE$S76~gJ)tGd6}xf zpgbMuRoXD->hLs)VNgEyG{d!%KD_aoJ=^I>M%2Xf1B0_fFGRa|Lqut~ch_{l$0tNj zaPDzHzto$KXVq)uHa!I-e+I!apdCe$q&7O!>|fg`+YnzHooM#2ZB%R?ymk_*bY%zi hqvHr?mKR#eNK+YUW3^FyRaz=F#J8g$oCCIq^fx@2KlA_q literal 0 HcmV?d00001 diff --git a/scripts/__pycache__/docx_ops_lib.cpython-311.pyc b/scripts/__pycache__/docx_ops_lib.cpython-311.pyc index a86967da8f8b7e58960e34ba237870cd1fca917e..36b1b196d11792d30e31ccca2e53ce28cf9c3d00 100644 GIT binary patch literal 76169 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 delta 14782 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 delta 251 zcmca4v`~O=IWI340}%9WU7xv&4UkzPJ6VxMl97M%ekQBQr7Yr%Jd=A_3OU(|Qu9($i!=o$7qPZZ{==%p zEdx|s#1A4QCmXT_b14FuoIqS0KDm!=4+}G^1LI_0_E?bx)f>DnO50zNw(nr;sJO@> zahHW}5=R81(Bw!C6E%>oTO9H6d5O8H@$oAeij+VmDgcRJ95%W6DWy57c14<#H*=(j U@G+WvU_d35CTnvlvw*Y!0HEDI%>V!Z diff --git a/scripts/__pycache__/outline_check.cpython-311.pyc b/scripts/__pycache__/outline_check.cpython-311.pyc index 0bf7eaec90b72ca55d8a1b183933a50d41ddad45..b4f014e1dbdce36974715d1d1cf4f210f81db396 100644 GIT binary patch literal 21284 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 diff --git a/scripts/__pycache__/outline_export.cpython-311.pyc b/scripts/__pycache__/outline_export.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ce860106f71bc8b2c426c5a7fd77321d48cf7e8 GIT binary patch literal 1405 zcma)5&1(}u6rbIlCYx+Fp^9deQiEtUrRkxz3L;`5L_sKmJ%!-1%}(2``{nFJZK)8! zgBQVrU=I~~s+9_ge@QkhGz=63@!+lC&66`5)0j%Zx05&XKIYB)_~pH-rU9eXOJA&l z2*5Wk^oYrvgrAFyzXc{R1sga*RS@`Dw8g5(cFC3;ScS0;+CxsFnsAcUq$5{lrc0U0 z7qFT_X;Xd}$HN&Bpwu>txXXjb#Qenj@voTv!8JvJDu(NN#2}XE26Ie?_YAUJl&FTD z`W_~_*C4j#BHh5mS~Th;pec+DQ(p-@m!_X!i=gf~fe~q7m%=&EMfL83T|UG%iPyI& z@cIbjJz6#vQ!zOeDj7-*xebRU#+*)+0H@h~n14JO7iz`ac~s!F^C1 znp;y!rNBq^(xPRfd4>lWqel%3BXdnIl`!I%C@NGo@RD!f0MYC$UTQeVCHJ_9X~r;3 z29~alreh~A2bi}EL&j)|zo7FMXcD8qvmYbOL4tYrC<9Xb3Z#knHl~o@r%B&fwLQbc zCzw~j0~Hwkx>dejoAvz~K>?{PSf*nH1mRlUEm@?* z$FN+SX94-VgHMb#!M=JbQZIz+Sfq|MD;=2LgZVbhZ>XPP3+6*O6Tz7loar0VJM*z& zDuPojIMq>x_mq*gG7>7IkuusWf6wOM!HxON2cb3*X%lHP~N(8U8dg~`k8Bkf* zc_q4D6tT<{n3uzbmDcse2K#R!UB?{vIK#bJJIS(bEp$hcF}?az-SY#TtKwnA^WqbU zTf?KfhQnff09fPN(~SL+Z#bUWu+dF?j*0w#5WHmUKoSI@1J1Vw*H@5h#$N|$t- None: 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) + report["render"] = render_docx( + output_docx, + render_dir, + docx_style_profile=report.get("docx_style_profile", "default_bid"), + numbering_mode=report.get("numbering_mode", "explicit_text"), + document_kind=patch_data.get("document_kind", "assembled"), + ) write_json(Path(args.report).resolve(), report) return diff --git a/scripts/docx_ops_lib.py b/scripts/docx_ops_lib.py index df071d7..c0495ea 100644 --- a/scripts/docx_ops_lib.py +++ b/scripts/docx_ops_lib.py @@ -4,6 +4,7 @@ import json import re import shutil import subprocess +from collections import defaultdict from dataclasses import dataclass from hashlib import sha1 from pathlib import Path @@ -11,7 +12,9 @@ from typing import Any, Iterator from docx import Document from docx.document import Document as DocxDocument +from docx.enum.text import WD_PARAGRAPH_ALIGNMENT from docx.oxml import OxmlElement +from docx.shared import Pt from docx.table import Table, _Cell from docx.text.paragraph import Paragraph @@ -27,6 +30,60 @@ except ImportError: # pragma: no cover NAMESPACES = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"} TEXT_WINDOW_DEFAULT = 40 +DEFAULT_DOCX_STYLE_PROFILE = "default_bid" +DEFAULT_NUMBERING_MODE = "explicit_text" +DEFAULT_DOCUMENT_KIND = "generic" +HEADING_NUMBER_PATTERN = re.compile(r"^(?P\d+(?:\.\d+)*)\s+(?P.+)$") +LEGACY_HEADING_PREFIX_PATTERN = re.compile(r"^(?:\d+(?:\.\d+)*|[一二三四五六七八九十]+)[、\..]?\s*") +CAPTION_PATTERN = re.compile(r"^(图|表|附件)\s*(\d+)-(\d+)\s+(.+)$") +PLACEHOLDER_PATTERN = re.compile(r"(占位|待补充|待提供|待替换|替换提示|TODO|技术转引)") + +DEFAULT_BID_STYLE_SPEC: dict[str, Any] = { + "normal": { + "font_name": "宋体", + "font_size": 12, + "bold": False, + "first_line_indent": 24, + "line_spacing": 1.5, + "space_before": 0, + "space_after": 0, + }, + "headings": { + 1: { + "font_name": "黑体", + "font_size": 15, + "bold": True, + "space_before": 18, + "space_after": 12, + }, + 2: { + "font_name": "黑体", + "font_size": 14, + "bold": True, + "space_before": 12, + "space_after": 6, + }, + 3: { + "font_name": "黑体", + "font_size": 12, + "bold": True, + "space_before": 6, + "space_after": 6, + }, + 4: { + "font_name": "楷体", + "font_size": 12, + "bold": False, + "space_before": 6, + "space_after": 3, + }, + }, + "table": { + "font_name": "宋体", + "font_size": 10.5, + "header_bold": True, + }, +} @dataclass @@ -101,17 +158,392 @@ def normalize_text(value: str) -> str: return re.sub(r"\s+", " ", value or "").strip() +def get_style_spec(docx_style_profile: str) -> dict[str, Any]: + if docx_style_profile != DEFAULT_DOCX_STYLE_PROFILE: + raise QueryError(f"unsupported docx_style_profile: {docx_style_profile}") + return DEFAULT_BID_STYLE_SPEC + + +def resolve_generation_options(payload: dict[str, Any]) -> dict[str, Any]: + return { + "docx_style_profile": str(payload.get("docx_style_profile", DEFAULT_DOCX_STYLE_PROFILE)), + "numbering_mode": str(payload.get("numbering_mode", DEFAULT_NUMBERING_MODE)), + "template_docx": payload.get("template_docx"), + "document_kind": str(payload.get("document_kind", DEFAULT_DOCUMENT_KIND)), + } + + +def _get_xml_element(target: Any) -> Any | None: + return getattr(target, "_element", None) or getattr(target, "element", None) + + +def _set_font_family(target: Any, font_name: str) -> None: + target.font.name = font_name + if not qn: + return + element = _get_xml_element(target) + if element is None: + return + r_pr = getattr(element, "rPr", None) + if r_pr is None: + r_pr = OxmlElement("w:rPr") + element.insert(0, r_pr) + r_fonts = getattr(r_pr, "rFonts", None) + if r_fonts is None: + r_fonts = OxmlElement("w:rFonts") + r_pr.insert(0, r_fonts) + for attr in ("w:ascii", "w:hAnsi", "w:eastAsia"): + r_fonts.set(qn(attr), font_name) + + +def apply_run_font(target_run: Any, *, font_name: str, font_size: float, bold: bool | None = None) -> None: + _set_font_family(target_run, font_name) + target_run.font.size = Pt(font_size) + if bold is not None: + target_run.bold = bold + + +def configure_style(style: Any, *, font_name: str, font_size: float, bold: bool, space_before: float = 0, space_after: float = 0, first_line_indent: float | None = None, line_spacing: float | None = None) -> None: + _set_font_family(style, font_name) + style.font.size = Pt(font_size) + style.font.bold = bold + paragraph_format = style.paragraph_format + paragraph_format.space_before = Pt(space_before) + paragraph_format.space_after = Pt(space_after) + if first_line_indent is not None: + paragraph_format.first_line_indent = Pt(first_line_indent) + if line_spacing is not None: + paragraph_format.line_spacing = line_spacing + + +def initialize_default_bid_styles(document: Document, docx_style_profile: str) -> dict[str, Any]: + spec = get_style_spec(docx_style_profile) + styles = document.styles + configure_style(styles["Normal"], **spec["normal"]) + try: + configure_style(styles["List Bullet"], **spec["normal"]) + except KeyError: + pass + for level, heading_spec in spec["headings"].items(): + configure_style(styles[f"Heading {level}"], **heading_spec) + return { + "status": "pass", + "profile": docx_style_profile, + "summary": { + "heading_numbering": "1 / 1.1 / 1.1.1 / 1.1.1.1", + "normal_font": spec["normal"]["font_name"], + "normal_font_size": spec["normal"]["font_size"], + }, + "issues": [], + } + + +def strip_heading_prefix(text: str) -> str: + normalized = normalize_text(text) + numbered = HEADING_NUMBER_PATTERN.match(normalized) + if numbered: + return numbered.group("title") + return LEGACY_HEADING_PREFIX_PATTERN.sub("", normalized, count=1).strip() + + +def replace_paragraph_text(paragraph: Paragraph, text: str) -> None: + existing_runs = list(paragraph.runs) + source_run = existing_runs[0] if existing_runs else None + clear_paragraph(paragraph) + new_run = paragraph.add_run(text) + if source_run is not None: + clone_run_format(source_run, new_run) + + +def apply_heading_numbering(document: Document, numbering_mode: str) -> None: + if numbering_mode != DEFAULT_NUMBERING_MODE: + raise QueryError(f"unsupported numbering_mode: {numbering_mode}") + counters = [0] * 9 + for paragraph in document.paragraphs: + style_name = paragraph.style.name if paragraph.style else None + level = heading_level_for_style(style_name) + if not level: + continue + counters[level - 1] += 1 + for index in range(level, len(counters)): + counters[index] = 0 + prefix = ".".join(str(value) for value in counters[:level] if value) + base_text = strip_heading_prefix(paragraph.text) + replace_paragraph_text(paragraph, f"{prefix} {base_text}".strip()) + + +def apply_paragraph_profile(paragraph: Paragraph, *, font_name: str, font_size: float, bold: bool, first_line_indent: float | None = None, line_spacing: float | None = None, space_before: float | None = None, space_after: float | None = None) -> None: + if first_line_indent is not None: + paragraph.paragraph_format.first_line_indent = Pt(first_line_indent) + if line_spacing is not None: + paragraph.paragraph_format.line_spacing = line_spacing + if space_before is not None: + paragraph.paragraph_format.space_before = Pt(space_before) + if space_after is not None: + paragraph.paragraph_format.space_after = Pt(space_after) + for run in paragraph.runs: + apply_run_font(run, font_name=font_name, font_size=font_size, bold=bold) + + +def apply_table_profile(table: Table, docx_style_profile: str) -> None: + table_spec = get_style_spec(docx_style_profile)["table"] + try: + table.style = "Table Grid" + except KeyError: + pass + for row_index, row in enumerate(table.rows): + for cell in row.cells: + for paragraph in cell.paragraphs: + apply_paragraph_profile( + paragraph, + font_name=table_spec["font_name"], + font_size=table_spec["font_size"], + bold=bool(row_index == 0 and table_spec["header_bold"]), + first_line_indent=0, + line_spacing=1.0, + space_before=0, + space_after=0, + ) + + +def apply_document_profile(document: Document, docx_style_profile: str) -> None: + spec = get_style_spec(docx_style_profile) + for paragraph in document.paragraphs: + style_name = paragraph.style.name if paragraph.style else None + level = heading_level_for_style(style_name) + if level: + heading_spec = spec["headings"].get(level, spec["headings"][4]) + paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.LEFT + paragraph.paragraph_format.first_line_indent = Pt(0) + paragraph.paragraph_format.space_before = Pt(heading_spec["space_before"]) + paragraph.paragraph_format.space_after = Pt(heading_spec["space_after"]) + paragraph.paragraph_format.keep_with_next = True + apply_paragraph_profile( + paragraph, + font_name=heading_spec["font_name"], + font_size=heading_spec["font_size"], + bold=heading_spec["bold"], + first_line_indent=0, + line_spacing=1.0, + space_before=heading_spec["space_before"], + space_after=heading_spec["space_after"], + ) + continue + apply_paragraph_profile( + paragraph, + font_name=spec["normal"]["font_name"], + font_size=spec["normal"]["font_size"], + bold=spec["normal"]["bold"], + first_line_indent=spec["normal"]["first_line_indent"], + line_spacing=spec["normal"]["line_spacing"], + space_before=spec["normal"]["space_before"], + space_after=spec["normal"]["space_after"], + ) + for table in document.tables: + apply_table_profile(table, docx_style_profile) + + +def remove_initial_blank_paragraph(document: Document) -> None: + if len(document.paragraphs) != 1: + return + paragraph = document.paragraphs[0] + if normalize_text(paragraph.text): + return + delete_block(paragraph) + + +def validate_format_profile(document: Document, docx_style_profile: str) -> dict[str, Any]: + spec = get_style_spec(docx_style_profile) + issues: list[str] = [] + styles = document.styles + for style_name, expected in ( + ("Normal", spec["normal"]), + ("Heading 1", spec["headings"][1]), + ("Heading 2", spec["headings"][2]), + ("Heading 3", spec["headings"][3]), + ("Heading 4", spec["headings"][4]), + ): + style = styles[style_name] + actual_size = style.font.size.pt if style.font.size is not None else None + if style.font.name != expected["font_name"]: + issues.append(f"{style_name} font should be {expected['font_name']}, got {style.font.name!r}") + if actual_size is None or abs(actual_size - expected["font_size"]) > 0.2: + issues.append(f"{style_name} size should be {expected['font_size']}, got {actual_size!r}") + return { + "status": "pass" if not issues else "fail", + "profile": docx_style_profile, + "issues": issues, + } + + +def validate_heading_numbering(document: Document, numbering_mode: str) -> dict[str, Any]: + if numbering_mode != DEFAULT_NUMBERING_MODE: + return { + "status": "fail", + "mode": numbering_mode, + "checked_headings": 0, + "issues": [f"unsupported numbering_mode: {numbering_mode}"], + } + counters = [0] * 9 + issues: list[str] = [] + checked = 0 + for paragraph in document.paragraphs: + style_name = paragraph.style.name if paragraph.style else None + level = heading_level_for_style(style_name) + if not level: + continue + checked += 1 + counters[level - 1] += 1 + for index in range(level, len(counters)): + counters[index] = 0 + expected = ".".join(str(value) for value in counters[:level] if value) + match = HEADING_NUMBER_PATTERN.match(normalize_text(paragraph.text)) + actual = match.group("number") if match else None + if actual != expected: + issues.append(f"{paragraph.text!r} should use heading number {expected}") + return { + "status": "pass" if not issues else "fail", + "mode": numbering_mode, + "checked_headings": checked, + "issues": issues, + } + + +def validate_caption_numbering(document: Document) -> dict[str, Any]: + counters: dict[tuple[str, int], int] = defaultdict(int) + issues: list[str] = [] + caption_count = 0 + for paragraph in document.paragraphs: + match = CAPTION_PATTERN.match(normalize_text(paragraph.text)) + if not match: + continue + caption_count += 1 + kind, chapter_text, index_text, _ = match.groups() + chapter = int(chapter_text) + index = int(index_text) + counters[(kind, chapter)] += 1 + if counters[(kind, chapter)] != index: + issues.append(f"{paragraph.text!r} caption index should be {counters[(kind, chapter)]}") + return { + "status": "pass" if not issues else "fail", + "caption_count": caption_count, + "issues": issues, + } + + +def document_has_toc(document: Document) -> bool: + body = document.element.body + for element in body.iter(): + if element.tag.endswith("}instrText") and "TOC" in "".join(element.itertext()): + return True + return False + + +def validate_toc(document: Document, document_kind: str) -> dict[str, Any]: + has_toc = document_has_toc(document) + if has_toc: + return {"status": "pass", "has_toc": True, "issues": []} + if document_kind == "outline": + return { + "status": "pass", + "has_toc": False, + "issues": ["outline documents can use heading numbering directly as visible目录内容"], + } + return { + "status": "pass", + "has_toc": False, + "issues": ["TOC field not found; current workflow allows user to insert or update TOC in Word"], + } + + +def collect_placeholder_hits(document: Document) -> list[str]: + hits: list[str] = [] + for paragraph in document.paragraphs: + text = normalize_text(paragraph.text) + if text and PLACEHOLDER_PATTERN.search(text): + hits.append(text) + for table in document.tables: + for row in table.rows: + for cell in row.cells: + text = normalize_text(cell.text) + if text and PLACEHOLDER_PATTERN.search(text): + hits.append(text) + return hits + + +def validate_placeholders(document: Document, document_kind: str) -> dict[str, Any]: + hits = collect_placeholder_hits(document) + allow_hits = document_kind == "outline" + status = "pass" if allow_hits or not hits else "fail" + return { + "status": status, + "placeholder_count": len(hits), + "issues": hits[:20], + } + + +def build_acceptance_checks(*, format_profile: dict[str, Any], numbering_validation: dict[str, Any], caption_validation: dict[str, Any], toc_validation: dict[str, Any], placeholder_validation: dict[str, Any], render_status: str | None = None) -> dict[str, Any]: + checks = [ + {"name": "format_profile", "status": format_profile["status"]}, + {"name": "numbering_validation", "status": numbering_validation["status"]}, + {"name": "caption_validation", "status": caption_validation["status"]}, + {"name": "toc_validation", "status": toc_validation["status"]}, + {"name": "placeholder_validation", "status": placeholder_validation["status"]}, + ] + if render_status is not None: + checks.append( + { + "name": "render_validation", + "status": "pass" if render_status == "ok" else ("warn" if render_status == "render_skipped" else "fail"), + } + ) + overall_status = "fail" if any(item["status"] == "fail" for item in checks) else "pass" + return { + "status": overall_status, + "checks": checks, + } + + +def inspect_document_quality(docx_path: Path, *, docx_style_profile: str, numbering_mode: str, document_kind: str, render_status: str | None = None) -> dict[str, Any]: + document = Document(str(docx_path)) + format_profile = validate_format_profile(document, docx_style_profile) + numbering_validation = validate_heading_numbering(document, numbering_mode) + caption_validation = validate_caption_numbering(document) + toc_validation = validate_toc(document, document_kind) + placeholder_validation = validate_placeholders(document, document_kind) + acceptance_checks = build_acceptance_checks( + format_profile=format_profile, + numbering_validation=numbering_validation, + caption_validation=caption_validation, + toc_validation=toc_validation, + placeholder_validation=placeholder_validation, + render_status=render_status, + ) + return { + "format_profile": format_profile, + "numbering_validation": numbering_validation, + "caption_validation": caption_validation, + "toc_validation": toc_validation, + "placeholder_validation": placeholder_validation, + "acceptance_checks": acceptance_checks, + } + + 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") + options = resolve_generation_options(spec_data) + template_docx = options["template_docx"] output_docx.parent.mkdir(parents=True, exist_ok=True) - document = Document() + document = Document(str(Path(template_docx).resolve())) if template_docx else Document() title = spec_data.get("title") if title: document.core_properties.title = str(title) + initialize_default_bid_styles(document, options["docx_style_profile"]) + remove_initial_blank_paragraph(document) block_reports: list[dict[str, Any]] = [] @@ -125,7 +557,9 @@ def create_docx_document(spec_data: dict[str, Any]) -> dict[str, Any]: 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) + run = paragraph.add_run(text) + heading_spec = get_style_spec(options["docx_style_profile"])["headings"].get(level, get_style_spec(options["docx_style_profile"])["headings"][4]) + apply_run_font(run, font_name=heading_spec["font_name"], font_size=heading_spec["font_size"], bold=heading_spec["bold"]) 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): @@ -143,7 +577,9 @@ def create_docx_document(spec_data: dict[str, Any]) -> dict[str, Any]: paragraph.style = str(style_name) except KeyError: pass - paragraph.add_run(text) + run = paragraph.add_run(text) + normal_spec = get_style_spec(options["docx_style_profile"])["normal"] + apply_run_font(run, font_name=normal_spec["font_name"], font_size=normal_spec["font_size"], bold=normal_spec["bold"]) block_reports.append({"index": ".".join(str(part) for part in index_path), "type": block_type, "text": summarize_text(text)}) return if block_type == "list": @@ -157,7 +593,9 @@ def create_docx_document(spec_data: dict[str, Any]) -> dict[str, Any]: paragraph.style = style_name except KeyError: pass - paragraph.add_run(str(item)) + run = paragraph.add_run(str(item)) + normal_spec = get_style_spec(options["docx_style_profile"])["normal"] + apply_run_font(run, font_name=normal_spec["font_name"], font_size=normal_spec["font_size"], bold=normal_spec["bold"]) block_reports.append({"index": ".".join(str(part) for part in index_path), "type": block_type, "item_count": len(items)}) return if block_type == "table": @@ -177,6 +615,7 @@ def create_docx_document(spec_data: dict[str, Any]) -> dict[str, Any]: row = table.add_row() for cell_index, value in enumerate(row_values): row.cells[cell_index].text = str(value) + apply_table_profile(table, options["docx_style_profile"]) 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": @@ -187,14 +626,27 @@ def create_docx_document(spec_data: dict[str, Any]) -> dict[str, Any]: for index, block in enumerate(blocks): render_block(block, [index]) + apply_heading_numbering(document, options["numbering_mode"]) + apply_document_profile(document, options["docx_style_profile"]) document.save(str(output_docx)) final_index = index_document(output_docx) + quality = inspect_document_quality( + output_docx, + docx_style_profile=options["docx_style_profile"], + numbering_mode=options["numbering_mode"], + document_kind=options["document_kind"], + ) return { "status": "ok", "output_docx": str(output_docx), "block_count": len(blocks), "blocks": block_reports, "final_summary": final_index["summary"], + "docx_style_profile": options["docx_style_profile"], + "numbering_mode": options["numbering_mode"], + "document_kind": options["document_kind"], + "template_docx": str(Path(template_docx).resolve()) if template_docx else None, + **quality, } @@ -205,6 +657,7 @@ def export_outline_artifacts(payload: dict[str, Any]) -> dict[str, Any]: business_json = Path(payload["business_outline_json"]).resolve() technical_docx = Path(payload["technical_docx"]).resolve() business_docx = Path(payload["business_docx"]).resolve() + options = resolve_generation_options(payload) for outline_name, outline in (("technical_outline", technical_outline), ("business_outline", business_outline)): if not isinstance(outline, dict): @@ -220,6 +673,10 @@ def export_outline_artifacts(payload: dict[str, Any]) -> dict[str, Any]: "output_docx": str(technical_docx), "title": str(technical_outline.get("title", "技术标目录")), "blocks": technical_outline["blocks"], + "docx_style_profile": options["docx_style_profile"], + "numbering_mode": options["numbering_mode"], + "template_docx": options["template_docx"], + "document_kind": "outline", } ) business_report = create_docx_document( @@ -227,11 +684,19 @@ def export_outline_artifacts(payload: dict[str, Any]) -> dict[str, Any]: "output_docx": str(business_docx), "title": str(business_outline.get("title", "商务及其他目录")), "blocks": business_outline["blocks"], + "docx_style_profile": options["docx_style_profile"], + "numbering_mode": options["numbering_mode"], + "template_docx": options["template_docx"], + "document_kind": "outline", } ) return { "status": "ok", + "docx_style_profile": options["docx_style_profile"], + "numbering_mode": options["numbering_mode"], + "document_kind": "outline", + "template_docx": str(Path(options["template_docx"]).resolve()) if options["template_docx"] else None, "technical_outline_json": str(technical_json), "business_outline_json": str(business_json), "technical_docx": str(technical_docx), @@ -722,12 +1187,14 @@ 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)) + options = resolve_generation_options(patch_data) 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)) + initialize_default_bid_styles(document, options["docx_style_profile"]) operations = patch_data.get("operations", []) operation_reports: list[dict[str, Any]] = [] @@ -781,37 +1248,62 @@ def apply_patch_document(patch_data: dict[str, Any]) -> dict[str, Any]: } ) + apply_heading_numbering(document, options["numbering_mode"]) + apply_document_profile(document, options["docx_style_profile"]) document.save(str(output_docx)) final_index = index_document(output_docx) + quality = inspect_document_quality( + output_docx, + docx_style_profile=options["docx_style_profile"], + numbering_mode=options["numbering_mode"], + document_kind=options["document_kind"], + ) return { "status": "ok", "source_docx": str(source_docx), "output_docx": str(output_docx), "in_place": in_place, + "docx_style_profile": options["docx_style_profile"], + "numbering_mode": options["numbering_mode"], + "template_docx": str(Path(options["template_docx"]).resolve()) if options["template_docx"] else None, "operation_count": len(operations), "operations": operation_reports, "errors": [], "warnings": [], "final_summary": final_index["summary"], + **quality, } -def render_docx(docx_path: Path, out_dir: Path) -> dict[str, Any]: +def render_docx(docx_path: Path, out_dir: Path, *, docx_style_profile: str = DEFAULT_DOCX_STYLE_PROFILE, numbering_mode: str = DEFAULT_NUMBERING_MODE, document_kind: str = DEFAULT_DOCUMENT_KIND) -> 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 { + report = { "status": "render_skipped", "docx": str(docx_path), + "docx_style_profile": docx_style_profile, + "numbering_mode": numbering_mode, + "document_kind": document_kind, "pdf": None, "page_count": 0, "images": [], "errors": [], "warnings": ["LibreOffice/soffice not found"], } + report.update( + inspect_document_quality( + docx_path, + docx_style_profile=docx_style_profile, + numbering_mode=numbering_mode, + document_kind=document_kind, + render_status=report["status"], + ) + ) + return report process = subprocess.run( [soffice, "--headless", "--convert-to", "pdf", "--outdir", str(out_dir), str(docx_path)], capture_output=True, @@ -819,15 +1311,28 @@ def render_docx(docx_path: Path, out_dir: Path) -> dict[str, Any]: encoding="utf-8", ) if process.returncode != 0 or not pdf_path.exists(): - return { + report = { "status": "error", "docx": str(docx_path), + "docx_style_profile": docx_style_profile, + "numbering_mode": numbering_mode, + "document_kind": document_kind, "pdf": str(pdf_path), "page_count": 0, "images": [], "errors": [process.stderr.strip() or "failed to convert docx to pdf"], "warnings": [], } + report.update( + inspect_document_quality( + docx_path, + docx_style_profile=docx_style_profile, + numbering_mode=numbering_mode, + document_kind=document_kind, + render_status=report["status"], + ) + ) + return report images: list[str] = [] warnings: list[str] = [] @@ -842,12 +1347,25 @@ def render_docx(docx_path: Path, out_dir: Path) -> dict[str, Any]: except Exception as exc: # pragma: no cover warnings.append(f"PNG render skipped: {exc}") - return { + report = { "status": "ok", "docx": str(docx_path), + "docx_style_profile": docx_style_profile, + "numbering_mode": numbering_mode, + "document_kind": document_kind, "pdf": str(pdf_path), "page_count": len(images), "images": images, "errors": [], "warnings": warnings, } + report.update( + inspect_document_quality( + docx_path, + docx_style_profile=docx_style_profile, + numbering_mode=numbering_mode, + document_kind=document_kind, + render_status=report["status"], + ) + ) + return report diff --git a/scripts/docx_patch.py b/scripts/docx_patch.py index 443557a..7f23819 100644 --- a/scripts/docx_patch.py +++ b/scripts/docx_patch.py @@ -19,7 +19,13 @@ def main() -> None: 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) + report["render"] = render_docx( + output_docx, + render_dir, + docx_style_profile=report.get("docx_style_profile", "default_bid"), + numbering_mode=report.get("numbering_mode", "explicit_text"), + document_kind=patch_data.get("document_kind", "assembled"), + ) write_json(Path(args.report).resolve(), report) diff --git a/scripts/outline_check.py b/scripts/outline_check.py index 12f4bcf..5d1fa11 100644 --- a/scripts/outline_check.py +++ b/scripts/outline_check.py @@ -2,7 +2,9 @@ from __future__ import annotations import argparse import re +from collections import Counter from pathlib import Path +from typing import Any from docx_ops_lib import QueryError, read_json, write_json @@ -24,66 +26,89 @@ ILLEGAL_LEAF_TITLES = { TECHNICAL_ROOT_TITLES = { "技术标目录", - "服务方案", + "技术目录", + "技术部分目录", "技术方案", + "服务方案", "实施方案", "服务保障及措施", "售后服务和质保期服务计划", } +BUSINESS_ROOT_TITLES = { + "商务及其他目录", + "商务目录", + "商务部分目录", + "商务及其他部分目录", +} + +TECHNICAL_PLACEHOLDER_TITLES = { + "技术标内容详见技术标目录版", + "技术部分详见技术标", + "技术部分", + "技术标", + "技术方案", + "服务方案", + "实施方案", +} + GENERIC_TECHNICAL_PATTERNS = ( - r"方案$", - r"设计$", - r"系统$", - r"平台$", - r"架构$", - r"建设内容$", - r"总体思路$", - r"总体要求$", - r"总体架构$", - r"功能设计$", - r"集成方案$", - r"响应方案$", - r"实施技术方案$", - r"部署方案$", - r"管理方案$", - r"验收方案$", - r"测试方案$", - r"试运行方案$", - r"保障措施$", - r"服务计划$", + r"^(技术|总体技术|总体|项目|整体)?方案$", + r"^(服务|运维|培训|实施|部署|测试|验收|应急|保障)(方案|计划|措施)?$", + r"^(系统|平台|架构|设计)(方案|设计|建设方案)?$", + r"^(项目理解|解决方案|系统设计|总体架构|建设内容|功能设计|集成方案|响应方案|管理方案)$", + r"^(总体设计方案|总体实施方案|总体服务方案)$", ) -SPECIFIC_CHILD_HINTS = ( +OBJECT_HINTS = ( + "子系统", + "模块", + "设备", + "接口", + "功能", + "单元", + "终端", + "节点", + "链路", + "数据库", + "中间件", + "服务器", + "存储", + "网络", + "点位", + "机房", + "服务项", + "清单", +) + +MANAGEMENT_HINTS = ( "原则", "目标", - "架构", - "模块", - "功能", - "内容", - "配置", - "清单", + "思路", + "策略", + "组织", + "保障", + "计划", "流程", "机制", - "计划", - "步骤", - "标准", - "参数", - "接口", - "部署", - "测试", - "验收", + "措施", "培训", + "验收", + "测试", "应急", - "风险", - "保障", + "运维", + "服务", + "售后", "响应", "巡检", "维护", - "更新", - "子系统", + "风险", ) +STEM_SUFFIX_PATTERN = re.compile( + r"(总体|项目|技术|系统|平台|服务|实施|运维|售后|培训|测试|验收|保障|管理|响应|交付|部署)?" + r"(方案|计划|步骤|措施|机制|说明|内容|设计|建设|保障)?$" +) def _normalize_heading(text: str) -> str: compact = re.sub(r"\s+", "", text or "") @@ -93,68 +118,304 @@ def _normalize_heading(text: str) -> str: return compact +def _issue(issues: list[dict[str, Any]], issue_type: str, path: list[str], message: str) -> None: + issues.append({"type": issue_type, "path": " > ".join(path), "message": message}) + + +def _is_heading(block: dict[str, Any]) -> bool: + return block.get("type", "heading") == "heading" + + +def _heading_children(children: list[Any]) -> list[dict[str, Any]]: + return [child for child in children if isinstance(child, dict) and _is_heading(child)] + + def _is_technical_context(path: list[str]) -> bool: return any(_normalize_heading(part) in TECHNICAL_ROOT_TITLES for part in path) +def _is_business_context(path: list[str]) -> bool: + return any(_normalize_heading(part) in BUSINESS_ROOT_TITLES for part in path) + + +def _technical_depth(path: list[str]) -> int: + for index, part in enumerate(path): + if _normalize_heading(part) in TECHNICAL_ROOT_TITLES: + return len(path) - index + return 0 + + +def _contains_object_hint(text: str) -> bool: + normalized = _normalize_heading(text) + if "系统" in normalized and len(normalized) > 4 and normalized not in ILLEGAL_LEAF_TITLES: + return True + return any(hint in normalized for hint in OBJECT_HINTS) + + +def _looks_management_focused(text: str) -> bool: + normalized = _normalize_heading(text) + return not _contains_object_hint(normalized) and any(hint in normalized for hint in MANAGEMENT_HINTS) + + def _looks_generic_technical_heading(text: str) -> bool: normalized = _normalize_heading(text) if normalized in ILLEGAL_LEAF_TITLES: return True + if _contains_object_hint(normalized): + return False 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 _has_object_child(children: list[dict[str, Any]]) -> bool: + return any(_contains_object_hint(str(child.get("text", "")).strip()) for child in children) -def _walk_blocks(blocks: list[dict], path: list[str], issues: list[dict]) -> None: +def _max_heading_depth(block: dict[str, Any]) -> int: + children = block.get("children", []) + if not isinstance(children, list): + return 1 + heading_children = _heading_children(children) + if not heading_children: + return 1 + return 1 + max(_max_heading_depth(child) for child in heading_children) + + +def _semantic_stem(text: str) -> str: + normalized = _normalize_heading(text) + normalized = STEM_SUFFIX_PATTERN.sub("", normalized) + normalized = normalized.strip("-_()()") + return normalized or _normalize_heading(text) + + +def _duplicate_generic_stems(children: list[dict[str, Any]]) -> list[str]: + stems = [ + _semantic_stem(str(child.get("text", "")).strip()) + for child in children + if _looks_generic_technical_heading(str(child.get("text", "")).strip()) + ] + counts = Counter(stem for stem in stems if len(stem) >= 2) + return sorted(stem for stem, count in counts.items() if count >= 2) + + +def _normalize_policy(payload: dict[str, Any]) -> dict[str, bool]: + raw_policy = payload.get("outline_policy", {}) + if raw_policy is None: + raw_policy = {} + if not isinstance(raw_policy, dict): + raise QueryError("outline_policy must be an object when provided") + return { + "allow_service_facets": bool(raw_policy.get("allow_service_facets", False)), + "respect_fixed_structure": bool(raw_policy.get("respect_fixed_structure", False)), + } + + +def _merge_policy(raw_policy: Any, inherited_policy: dict[str, bool]) -> dict[str, bool]: + if raw_policy is None: + return dict(inherited_policy) + if not isinstance(raw_policy, dict): + raise QueryError("policy must be an object when provided on a heading block") + return { + "allow_service_facets": bool(raw_policy.get("allow_service_facets", inherited_policy["allow_service_facets"])), + "respect_fixed_structure": bool(raw_policy.get("respect_fixed_structure", inherited_policy["respect_fixed_structure"])), + } + + +def _parse_heading_level( + block: dict[str, Any], + path: list[str], + issues: list[dict[str, Any]], + *, + parent_level: int | None, +) -> int | None: + raw_level = block.get("level") + if not isinstance(raw_level, int): + _issue(issues, "invalid_heading_level", path, "heading level must be an integer between 1 and 9") + return None + if raw_level < 1 or raw_level > 9: + _issue(issues, "invalid_heading_level", path, "heading level must be between 1 and 9") + return None + if parent_level is None: + if raw_level != 1: + _issue(issues, "invalid_root_heading_level", path, "top-level heading must use level 1") + elif raw_level != parent_level + 1: + _issue( + issues, + "invalid_heading_hierarchy", + path, + f"child heading level must be parent level + 1; expected {parent_level + 1}, got {raw_level}", + ) + return raw_level + + +def _check_technical_depth(blocks: list[dict[str, Any]], issues: list[dict[str, Any]], policy: dict[str, bool]) -> None: + for block in blocks: + if not isinstance(block, dict) or not _is_heading(block): + continue + root_text = str(block.get("text", "")).strip() + if _normalize_heading(root_text) not in TECHNICAL_ROOT_TITLES: + continue + root_children = block.get("children", []) + if not isinstance(root_children, list): + continue + branch_children = _heading_children(root_children) + if not branch_children: + _issue( + issues, + "technical_outline_too_shallow", + [root_text], + "technical outline must include at least one level-2 branch under the root", + ) + continue + for child in branch_children: + child_text = str(child.get("text", "")).strip() + branch_path = [root_text, child_text] + branch_depth = _max_heading_depth(child) + branch_policy = _merge_policy(child.get("policy"), policy) + if branch_policy["respect_fixed_structure"] and branch_depth < 2: + continue + if branch_depth < 2: + _issue( + issues, + "technical_branch_too_shallow", + branch_path, + f"technical branch '{child_text}' must reach at least level 3", + ) + + +def _walk_blocks( + blocks: list[dict[str, Any]], + path: list[str], + issues: list[dict[str, Any]], + policy: dict[str, bool], + parent_level: int | None = None, +) -> 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"}) + _issue(issues, "invalid_block", path + [str(index)], "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 block_type != "heading": + continue + + current_policy = _merge_policy(block.get("policy"), policy) + current_level = _parse_heading_level(block, current_path, issues, parent_level=parent_level) + + if text in ILLEGAL_LEAF_TITLES and not children: + _issue( + issues, + "illegal_leaf", + current_path, + f"abstract heading '{text}' cannot be a leaf", + ) + + if children and not isinstance(children, list): + _issue(issues, "invalid_children", current_path, "children must be a list") + continue + + if not isinstance(children, list): + continue + + direct_heading_children = _heading_children(children) + normalized = _normalize_heading(text) + in_technical_context = _is_technical_context(current_path) + in_business_context = _is_business_context(current_path) + + if in_business_context and normalized in TECHNICAL_PLACEHOLDER_TITLES and direct_heading_children: + _issue( + issues, + "business_technical_placeholder_expanded", + current_path, + f"business outline technical placeholder '{text}' must remain a single placeholder node", + ) + + if in_technical_context: + technical_depth = _technical_depth(current_path) + is_generic_heading = _looks_generic_technical_heading(text) + allow_service_facets = current_policy["allow_service_facets"] + allow_fixed_structure = current_policy["respect_fixed_structure"] + + if is_generic_heading and normalized not in ILLEGAL_LEAF_TITLES and not direct_heading_children: + _issue( + issues, + "generic_technical_leaf", + current_path, + f"technical heading '{text}' is still too generic to write from directly", ) - 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) + + if is_generic_heading and len(direct_heading_children) == 1: + _issue( + issues, + "single_child_breakdown", + current_path, + f"technical heading '{text}' cannot be expanded with only one direct child", + ) + + if ( + is_generic_heading + and direct_heading_children + and not allow_service_facets + and not allow_fixed_structure + and not _has_object_child(direct_heading_children) + ): + _issue( + issues, + "missing_object_breakdown", + current_path, + f"technical heading '{text}' must include at least one object/module/subsystem oriented child", + ) + + duplicate_stems = _duplicate_generic_stems(direct_heading_children) + if duplicate_stems: + joined = ", ".join(duplicate_stems) + _issue( + issues, + "duplicate_technical_facets", + current_path, + f"technical heading '{text}' has repeated generic child facets: {joined}", + ) + + if ( + technical_depth >= 3 + and not direct_heading_children + and not allow_service_facets + and _looks_management_focused(text) + ): + _issue( + issues, + "management_leaf_too_generic", + current_path, + f"management-style leaf '{text}' is too generic; refine it to an object or concrete deliverable", + ) + + if technical_depth == 2 and direct_heading_children: + if ( + not allow_service_facets + and not allow_fixed_structure + and all(_looks_management_focused(str(child.get("text", "")).strip()) for child in direct_heading_children) + ): + _issue( + issues, + "top_branch_missing_object_nodes", + current_path, + f"technical branch '{text}' is expanded only by management facets; add module/subsystem/device oriented nodes", + ) + + _walk_blocks(direct_heading_children, current_path, issues, current_policy, current_level) -def check_outline(payload: dict) -> dict: +def check_outline(payload: dict[str, Any]) -> dict[str, Any]: blocks = payload.get("blocks", []) if not isinstance(blocks, list): raise QueryError("blocks must be a list") - issues: list[dict] = [] - _walk_blocks(blocks, [], issues) + policy = _normalize_policy(payload) + issues: list[dict[str, Any]] = [] + _walk_blocks(blocks, [], issues, policy) + _check_technical_depth(blocks, issues, policy) return { "status": "ok" if not issues else "failed", "issue_count": len(issues),