commit 803c0e51e9669e98cdf51d2b4340f815dddbf90e Author: wehub-resource-sync Date: Mon Jul 13 12:29:52 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..d82e3d6 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,13 @@ +{ + "name": "software-copyright-materials", + "description": "从真实项目生成中国软件著作权申请资料(Word/TXT)。支持项目分析、代码材料抽取、申请表信息生成和操作手册撰写,输出符合软著申报要求的正式材料。", + "version": "1.0.0", + "author": { + "name": "Fokkyp", + "url": "https://github.com/Fokkyp" + }, + "homepage": "https://github.com/Fokkyp/SoftwareCopyright-Skill", + "repository": "https://github.com/Fokkyp/SoftwareCopyright-Skill", + "license": "MIT", + "keywords": ["software-copyright", "chinese-copyright", "docx", "软著", "著作权"] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0d42bde --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# macOS +.DS_Store +**/.DS_Store + +# Python +__pycache__/ +**/__pycache__/ +*.py[cod] +*.pyo +.Python +.venv/ +venv/ +env/ + +# .NET build outputs +bin/ +obj/ +**/bin/ +**/obj/ + +# Local generated materials +软件著作权申请资料/ +**/软件著作权申请资料/ + +# Temporary and preview outputs +*.tmp +*.temp +*.log +*.pdf +*.png +*.jpg +*.jpeg +*.webp +!docs/ +!docs/screenshots/ +!docs/screenshots/*.png + +# Office lock files +~$*.docx +~$*.xlsx +~$*.pptx + +# Editor / IDE +.idea/ +.vscode/ + +# Environment +.env +.env.* + +# Keep the generated demo in this repository +!生成demo/ +!生成demo/** +生成demo/**/.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6dd27df --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Software Copyright Materials contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2da0d7 --- /dev/null +++ b/README.md @@ -0,0 +1,241 @@ +# Software Copyright Materials Skill + +这是一个用于生成中文软件著作权申请资料的 Skill / 插件仓库,可用于 Codex,也可作为 Claude Code 插件加载。 + +项目地址:https://github.com/Fokkyp/SoftwareCopyright-Skill + +真正的 Skill 位于: + +```text +software-copyright-materials/ +``` + +安装时不要把仓库根目录当作普通单个 skill 复制。Codex 使用 `software-copyright-materials/` 这个实际 skill 目录;Claude Code 使用仓库根目录中的 `.claude-plugin/plugin.json` 和 `skills/` 插件结构。 + +## 功能概览 + +> **本项目完全免费。请不要相信任何使用本项目包装出来的付费服务。** + +软件著作权申请本身不神秘,真正麻烦的是整理材料:申请表字段要写对,操作手册要像样,代码材料要按规则截取,软件名称、版本号、页数还要保持一致。很多开发者最后会把这件事交给付费代办或资料整理服务,花钱买的往往也只是这些文档整理工作。 + +这个 skill 的目标很直接:让开发者不用再为整理软著材料额外付费,也不用把项目代码和产品细节交给外部商家来回沟通。把真实项目交给支持该 skill 的代码助手,它会按流程引导你确认关键信息,并在本地生成一整套可检查、可修改、可提交前再导出的软著申请资料。 + +- **自己生成整套资料**:从项目分析、业务理解、申请表信息、操作手册到代码材料,一套流程跑完,不再依赖外部代办整理文档。 +- **从真实源码抽取代码**:代码材料只来自开发者已有项目,禁止 AI 编造源码,适合对材料真实性敏感的开发者。 +- **自动处理前 30 页 / 后 30 页规则**:源码足够时按常见鉴别材料要求生成前 30 页和后 30 页;不足 60 页时按规则生成全部代码材料。 +- **操作手册不套模板**:先理解项目业务、页面和功能,再写面向审核员的操作说明,避免只有空泛功能列表。 +- **申请表字段集中整理**:软件名称、版本号、著作权人、开发环境、运行环境、源程序量、功能说明等字段统一生成到 `申请表信息.txt`,官网填报时可以对照复制。 +- **关键节点都让你确认**:业务口径、申请表字段、代码选择、截图方式、最终 Markdown 草稿都会停下来让开发者确认,减少材料写偏的风险。 +- **Word/TXT 一键输出**:确认后生成操作手册 DOCX、代码材料 DOCX 和申请表 TXT,文件统一放在 `软件著作权申请资料/正式资料/`。 +- **本地生成,资料可控**:默认在当前项目目录生成材料,代码、文档和草稿都留在本地,方便开发者自行审阅、修改和归档。 +- **提供完整 demo**:仓库内提供 [`生成demo/软件著作权申请资料/`](生成demo/软件著作权申请资料/),可以直接点击查看生成后的草稿、正式资料和填报辅助文件。 + +## 演示截图 + +| 生成流程 | 生成流程 | +|---------|---------| +| ![软著材料生成演示 1](docs/screenshots/demo-1.png) | ![软著材料生成演示 2](docs/screenshots/demo-2.png) | +| ![软著材料生成演示 3](docs/screenshots/demo-3.png) | ![软著材料生成演示 4](docs/screenshots/demo-4.png) | +| ![软著材料生成演示 5](docs/screenshots/demo-5.png) | ![软著材料生成演示 6](docs/screenshots/demo-6.png) | + +## 目录结构 + +```text +. +├── docs/ +│ └── screenshots/ +│ ├── demo-1.png +│ ├── demo-2.png +│ ├── demo-3.png +│ ├── demo-4.png +│ ├── demo-5.png +│ ├── demo-6.png +│ └── 著作权申请表.png +├── software-copyright-materials/ +│ ├── SKILL.md +│ ├── agents/ +│ ├── references/ +│ ├── scripts/ +│ └── vendor/ +└── 生成demo/ + └── 软件著作权申请资料/ + ├── 草稿/ + └── 正式资料/ + ├── 申请表信息.txt + ├── 软件名称_操作手册.docx + ├── 软件名称-代码(前30页).docx + └── 软件名称-代码(后30页).docx +``` + +## 下载并安装 + +先获取本仓库。会用 Git 的用户执行: + +```bash +git clone https://github.com/Fokkyp/SoftwareCopyright-Skill.git +cd SoftwareCopyright-Skill +``` + +不会用 Git 的用户可以在 GitHub 页面点击 `Code` -> `Download ZIP`,解压后进入仓库目录。目录中应能看到: + +```text +software-copyright-materials/ +.claude-plugin/plugin.json +skills/software-copyright-materials +``` + +按使用工具选择一种方式: + +**Codex**:复制实际 skill 目录。 + +```bash +mkdir -p ~/.codex/skills +cp -R software-copyright-materials ~/.codex/skills/ +``` + +**Claude Code**:把本仓库作为插件目录加载。 + +```bash +claude --plugin-dir /path/to/SoftwareCopyright-Skill +``` + +在本仓库目录中可以直接执行: + +```bash +claude --plugin-dir . +``` + +Claude Code 手动调用时使用插件命名空间: + +```text +/software-copyright-materials:software-copyright-materials +``` + +如果只想在某个项目里使用,Codex 可复制到该项目的 `.codex/skills/`: + +```bash +PROJECT_DIR="<你的项目目录>" +mkdir -p "$PROJECT_DIR/.codex/skills" +cp -R software-copyright-materials "$PROJECT_DIR/.codex/skills/" +``` + +Claude Code 可在目标项目目录启动时指定本仓库: + +```bash +cd "<你的项目目录>" +claude --plugin-dir "" +``` + +修改 README 或 skill 后,重启会话,或在 Claude Code 中执行 `/reload-plugins` 重新加载。 + +## 运行要求和环境校验 + +### 必需环境 + +- **支持 Skill / Plugin 的代码助手**:Codex 可按 skill 目录加载;Claude Code 可按插件目录加载。 +- **Python 3.10+ 和 python-docx**:生成流程依赖 `software-copyright-materials/scripts/` 下的 Python 脚本,用于分析项目、生成草稿、抽取真实代码、校验字段和生成正式资料。 +- **可读取的项目源码**:代码材料必须从真实项目中抽取,所以需要在代码助手中打开或指定你的项目目录。 + +安装 Python 依赖: + +```bash +python3 -m pip install python-docx +``` + +### 可选环境 + +- **.NET SDK 8.0+**:用于启用更完整的 DOCX OpenXML 生成和校验能力。没有 .NET SDK 也可以继续使用基础 DOCX 兜底生成。 +- **Chrome DevTools MCP**:只有在你希望自动截取网页截图时才需要。 +- **Codex Computer Use**:仅 Codex 环境需要桌面界面操作和截图时使用。 +- **用户自行截图**:如果没有 MCP 或 Computer Use,也可以手动把截图放到指定目录,或者直接跳过截图。 + +### 使用过程中会自动检查吗? + +会。每次开始生成资料时,skill 会先运行环境检查,并在当前目录生成: + +```text +软件著作权申请资料/环境检查.md +软件著作权申请资料/环境检查.json +``` + +环境检查会告诉你: + +- Markdown 草稿、TXT、基础 DOCX 是否可用。 +- 完整 DOCX OpenXML 环境是否可用。 +- `.NET SDK` 是否缺失。 +- 当前会把材料生成到哪里。 + +如果完整 DOCX 环境缺失,代码助手会停下来让你选择: + +1. 安装完整 DOCX 环境。 +2. 使用基础 DOCX 兜底继续。 + +它不会在你不确认的情况下静默安装依赖。截图也一样,会先让你选择 Chrome DevTools MCP、Codex Computer Use、用户自行截图或跳过截图;如果你跳过截图,操作手册里会保留可见的截图预留位置。 + +## 基本使用 + +安装或加载完成后,在代码助手中打开需要生成软著资料的项目,然后直接说: + +```text +使用 software-copyright-materials 生成当前项目的软件著作权申请资料 +``` + +在 Claude Code 中也可以手动调用: + +```text +/software-copyright-materials:software-copyright-materials 读取当前目录中的项目,生成软件著作权申请资料草稿 +``` + +代码助手会按流程引导填写信息、确认草稿,并在当前项目目录下生成 `软件著作权申请资料/`。 + +## 开源协议 + +本项目采用 [MIT License](LICENSE) 开源。你可以自由使用、复制、修改、分发,也可以基于它继续开发自己的版本。使用者仍需自行核对生成材料是否符合实际项目和官网当前要求。 + +## 代码材料说明 + +依据软件著作权申请材料要求,代码鉴别材料应来自申请软件本身。本 skill 不通过 AI 生成项目代码,也不编造不存在的源码内容。 + +本 skill 的作用是帮助开发者从已有项目中理解业务、选择代码文件、提取前后代码材料,并整理为便于编辑和提交的文档格式。开发者应在提交前自行核对代码材料是否来自真实项目、软件名称和版本号是否与申请表保持一致。 + +## 官网填报和提交 + +官方入口: + +- 中国版权保护中心:https://www.ccopyright.com.cn/ +- 著作权登记系统:https://register.ccopyright.com.cn/login.html +- 法规依据:《计算机软件著作权登记办法》:https://www.gov.cn/zhengce/2002-02/20/content_5724627.htm + +官方页面可能会调整,实际填报时以官网当前页面为准。 + +### 著作权申请表填写示例 + +著作权申请表按照以下图片填写。 + +![著作权申请表填写示例](docs/screenshots/著作权申请表.png) + +### 申请流程 + +1. 打开中国版权保护中心官网,进入著作权登记系统。 +2. 注册或登录账号,并按页面提示完成实名认证。 +3. 进入软件著作权相关业务,选择计算机软件著作权登记申请。 +4. 在线填写申请表。可以打开本工具生成的 `正式资料/申请表信息.txt`,把软件名称、版本号、开发完成日期、开发环境、运行环境、功能说明等内容复制到官网对应字段。 +5. 上传申请材料。根据官网要求上传 PDF 格式文件和其他证明材料。 +6. 核对信息无误后提交申请,并按官网提示查看受理、补正或登记结果。 + +### 生成文件怎么用 + +`申请表信息.txt` 是填报辅助文件,用来帮助开发者在官网填写申请表,不是直接上传的申请材料。 + +`docx` 文件是本地编辑稿,方便开发者在 Word、WPS 或 Pages 中继续修改。提交官网前,请将需要上传的 `docx` 文件导出或另存为 PDF,再按官网要求上传。 + +实际文件名会包含软件名称。通常需要转换为 PDF 的文件包括: + +- `操作手册.docx` +- `代码(前30页).docx` +- `代码(后30页).docx` +- 不足 60 页时生成的全部代码材料 + +申请人身份证明、权属证明、委托材料等其他文件,请按官网页面要求另行准备并上传。 + +

友情连接:Linux Do 社区 · V2EX

diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..d7aa12a --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`Fokkyp/SoftwareCopyright-Skill` +- 原始仓库:https://github.com/Fokkyp/SoftwareCopyright-Skill +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/docs/screenshots/demo-1.png b/docs/screenshots/demo-1.png new file mode 100644 index 0000000..946c5d2 Binary files /dev/null and b/docs/screenshots/demo-1.png differ diff --git a/docs/screenshots/demo-2.png b/docs/screenshots/demo-2.png new file mode 100644 index 0000000..222f7b2 Binary files /dev/null and b/docs/screenshots/demo-2.png differ diff --git a/docs/screenshots/demo-3.png b/docs/screenshots/demo-3.png new file mode 100644 index 0000000..0b57810 Binary files /dev/null and b/docs/screenshots/demo-3.png differ diff --git a/docs/screenshots/demo-4.png b/docs/screenshots/demo-4.png new file mode 100644 index 0000000..ca85328 Binary files /dev/null and b/docs/screenshots/demo-4.png differ diff --git a/docs/screenshots/demo-5.png b/docs/screenshots/demo-5.png new file mode 100644 index 0000000..1d4e174 Binary files /dev/null and b/docs/screenshots/demo-5.png differ diff --git a/docs/screenshots/demo-6.png b/docs/screenshots/demo-6.png new file mode 100644 index 0000000..290fffb Binary files /dev/null and b/docs/screenshots/demo-6.png differ diff --git a/docs/screenshots/著作权申请表.png b/docs/screenshots/著作权申请表.png new file mode 100644 index 0000000..c519ba6 Binary files /dev/null and b/docs/screenshots/著作权申请表.png differ diff --git a/skills/software-copyright-materials b/skills/software-copyright-materials new file mode 120000 index 0000000..b7e06c9 --- /dev/null +++ b/skills/software-copyright-materials @@ -0,0 +1 @@ +../software-copyright-materials \ No newline at end of file diff --git a/software-copyright-materials/SKILL.md b/software-copyright-materials/SKILL.md new file mode 100644 index 0000000..7ed5a3b --- /dev/null +++ b/software-copyright-materials/SKILL.md @@ -0,0 +1,502 @@ +--- +name: software-copyright-materials +description: > + Generate guided Chinese software copyright application materials from a real project. + Use this skill when the user asks for 软件著作权, 软著申请资料, 软著代码材料, + 操作手册, 申请表信息, or wants Word/TXT materials for software copyright registration. + The workflow analyzes the imported project, extracts real source code, creates Markdown + drafts for user confirmation, then uses bundled DOCX tooling to produce final + Word documents and TXT. +user-invocable: true +compatibility: > + Requires Python 3.10+ with python-docx (pip install python-docx). + Optional: .NET SDK 8.0+ for full OpenXML DOCX validation (run vendor/docx-toolkit/scripts/setup.sh). +allowed-tools: > + Bash, Read, Write, Edit, Glob, Grep, WebSearch, WebFetch +metadata: + short-description: 生成软著申请资料 Word/TXT + author: Fokkyp + version: "1.2" + repository: https://github.com/Fokkyp/SoftwareCopyright-Skill +--- + +# 软著申请资料生成 + +这个 skill 生成可审阅、可追溯的软著申请资料。核心原则: + +- 固定输出目录:当前工作目录下的 `软件著作权申请资料/`。不要默认写到 `/tmp`、`/private/tmp` 或其他临时目录。 +- 只有测试 skill 自身时才允许显式指定临时目录;面向用户生成材料时必须写入当前目录。 +- 先生成 Markdown 草稿,用户确认后再生成正式 Word/TXT。 +- 正式 Word/TXT 只能写入 `软件著作权申请资料/正式资料/`,不要散落在输出目录根部。 +- 正式 Word/TXT 的文字一律使用默认黑色字体,不生成蓝色超链接、主题色标题或其他彩色文字;Markdown 链接写入 Word 时必须转成普通文本。 +- 正式资料中的软件名称必须与 `草稿/申请表信息.md` 的“软件全称”字段一致;正式生成时以已确认的申请表软件全称为准。 +- 正式代码 Word 页眉中的版本号必须与 `草稿/申请表信息.md` 的“版本号”字段一致;正式生成时以已确认的申请表版本号为准。 +- 代码材料必须来自真实项目源码,禁止 AI 编造代码。 +- 写申请表和操作手册前,必须先形成模型研判后的 `草稿/业务理解.md/json`,理解软件业务、行业、目标用户、核心价值和操作流程。 +- 脚本只能收集项目证据、校验字段和生成文件;行业判断、功能抽取、代码抽取选择、操作手册结构必须由模型阅读项目后决定,不得依赖脚本关键字表或固定范本。 +- 优先抽取前端代码:入口、路由、页面、核心组件、接口封装、状态管理、工具函数。 +- 生成代码材料前,必须先生成代码文件候选清单;模型理解项目后填写抽取文件和选择理由,再让用户确认或修改。 +- 代码优先抽取模型和用户确认的、最能体现软件真实功能和运行逻辑的源码;不足 60 页时,从其他相关源码文件补充到 60 页;候选源码仍不足 60 页时,才生成全部代码文档。 +- 操作手册成稿应像真实软件随附的操作说明,而不是研发说明、功能清单或 AI 生成的汇总文。 +- 操作手册草稿必须按传统软著操作手册骨架组织:相关文档、说明、功能特点、系统要求、按真实页面/流程逐章操作、常见问题解答、术语表。一级章节标题使用中文大写序号,例如 `一、相关文档`,不得使用 `(1)、相关文档`。相关文档必须用表格指向总体设计、详细设计、测试案例等配套文档。正文尽量使用连续段落,不使用项目符号列表或 `1. 2. 3.` 编号列表。 +- 每个核心页面都要用普通用户视角说明页面用途、进入位置、用户可见内容、用户动作、输入限制或异常提示、结果反馈和截图预留。不得把章节写成“进入方式:/页面内容:/操作步骤:/操作规则:/操作结果与反馈:”这种字段模板;这些信息要自然合并到段落里。避免代码、框架、接口、状态管理、异步任务等技术化表达;撰写过程中由 agent 自行循环检查、扩写和修正,完整草稿完成后只向用户发起一次整体确认。 +- 操作手册必须去除明显“AI 味”:避免空泛赞美、营销口号、万能句式、每章同一结构、头中尾固定结构、过度对称的排比、没有项目细节的正确废话、频繁使用“旨在、赋能、一站式、智能化、高效便捷、显著提升、强大能力、丰富功能”等套话。每段都应能回答“这个项目里这个功能具体做什么、用户看见什么、操作后有什么结果”。 +- 操作手册生成必须同步输出 `草稿/操作手册自检记录.md` 和 `草稿/操作手册自检记录.json`,记录初稿、按项目流程扩写、去制式表达等自检轮次;如果前 3 轮仍发现问题,必须继续补写修正,直到问题清零或记录无法自动修复的原因后再停止。 +- 截图方式必须先让用户选择:Chrome DevTools MCP、Codex Computer Use、用户自行截图。用户选完后,再检查当前 MCP / Computer Use 能力是否可用;如果用户说现在不截图、先跳过截图或截图失败,操作手册仍必须保留清晰可见的截图预留位置,正式 Word 中也要能看到。 +- 申请表信息中的硬件/系统环境必须让用户确认或填写,不能硬编码。 +- Word 生成能力必须使用本 skill 内置的 `vendor/docx-toolkit`;不得引用外部 DOCX 目录。 + +## 强制人工门禁 + +凡是涉及用户选择、确认或补充信息的阶段,必须先停止当前执行,不得继续调用下一步脚本。即使处于自动审核、自动继续或无人值守模式,也必须把 `STOP_FOR_USER` 和 `NEXT_ACTION` 原样告知用户,并等待用户输入后再继续。 + +禁止使用“用户未选择则默认继续”的逻辑。用户回复确认后,先用确认脚本记录对应门禁,再进入下一阶段: + +```bash +python3 ${CLAUDE_SKILL_DIR}/scripts/confirm_stage.py --workdir 软件著作权申请资料 --stage <阶段名> --note "<用户确认内容>" +``` + +必须停住的门禁: + +- `environment`:完整 DOCX 环境缺失时,用户必须选择“安装完整环境”或“使用基础 DOCX 兜底继续”。 +- `project`:存在多个项目候选目录时,用户必须指定项目目录。 +- `business`:`草稿/业务理解.md` 生成后,用户必须确认行业、目标用户、核心功能和申请口径。 +- `application-fields`:`草稿/申请表信息.md` 生成后,用户必须补全并确认硬件、系统环境、著作权人、日期等字段。 +- `code-selection`:`草稿/代码文件选择.json` 生成后,用户必须确认或修改抽取文件。 +- `screenshot-method`:操作手册截图前,用户必须在 Chrome DevTools MCP、Codex Computer Use、用户自行截图三种方式中选择一种;如果用户明确说“现在不截图/先跳过截图”,记录为 `skip`。 +- `markdown`:全部 Markdown 草稿完成后,用户必须确认可以进入 Word/TXT 生成。 + +## 工作流 + +### 1. 启动环境检查 + +一开始先在当前工作目录创建输出目录并检查运行能力: + +```bash +python3 ${CLAUDE_SKILL_DIR}/scripts/check_environment.py \ + --out-dir 软件著作权申请资料 +``` + +输出: + +- `软件著作权申请资料/环境检查.md` +- `软件著作权申请资料/环境检查.json` + +环境检查必须告诉用户: + +- 当前会在“当前目录/软件著作权申请资料”下生成材料。 +- Markdown 草稿、TXT、基础 DOCX 是否可用。 +- 内置 `vendor/docx-toolkit` 的完整 OpenXML 环境是否可用。 +- 如 `.NET SDK` 缺失,询问用户是否安装完整环境。 + +用户选择: + +- 如果用户愿意安装完整环境,按 `${CLAUDE_SKILL_DIR}/vendor/docx-toolkit/scripts/setup.sh` 的要求安装依赖,再继续。完整环境生成和校验更规范。 +- 如果用户不安装,继续使用兜底方案生成 Markdown、TXT 和基础 DOCX。 +- 如果完整 DOCX 环境缺失,必须停止并等待用户选择;不得自动继续。 + +用户回复后记录门禁: + +```bash +python3 ${CLAUDE_SKILL_DIR}/scripts/confirm_stage.py \ + --workdir 软件著作权申请资料 \ + --stage environment \ + --note "<用户选择>" +``` + +不要等到最后验证阶段才发现完整 DOCX 环境不可用;这个信息必须在流程开始时给出。 + +### 2. 定位项目 + +用户通常会把项目放在当前文件夹下。先扫描当前目录,避开本 skill、自身输出目录、`node_modules`、构建产物和隐藏目录,找到最可能的项目根目录。 + +如果有多个候选项目,必须停止并询问用户选择;如果只有一个明显候选项目,可以直接使用。 + +### 3. 分析项目 + +运行: + +```bash +python3 ${CLAUDE_SKILL_DIR}/scripts/analyze_project.py \ + --project <项目目录> \ + --out 软件著作权申请资料/analysis/project.json +``` + +分析内容包括: + +- `package.json`、README、脚本命令、依赖 +- 前端框架和主要编程语言 +- 入口文件、路由、页面、组件、接口、状态管理 +- 源码文件数量和源程序行数 +- 软件名称候选、主要功能候选、运行命令候选 + +### 4. 形成业务理解 + +在写申请表和操作手册前,先让脚本收集项目证据: + +```bash +python3 ${CLAUDE_SKILL_DIR}/scripts/generate_business_context.py \ + --project <项目目录> \ + --analysis 软件著作权申请资料/analysis/project.json \ + --software-name "<软件全称>" \ + --out-dir 软件著作权申请资料/草稿 +``` + +输出: + +- `草稿/业务理解证据.md` +- `草稿/业务理解证据.json` +- `草稿/业务理解模型稿模板.json` + +这一步只收集证据,不决定最终业务口径。接下来必须由模型阅读 `业务理解证据.md/json`、README、PRD/BRD、页面文案、路由、接口、必要源码和用户补充资料,自行判断: + +- 应该重点读取哪些文档和源码 +- 软件属于什么行业 / 领域 +- 目标用户是谁 +- 核心价值是什么 +- 哪些功能应写入软著申请资料 +- 典型操作流程如何组织 +- 操作手册适合采用什么章节结构 +- 申请表建议口径如何表达 + +模型不得用脚本关键字表决定行业、功能和结构;不得把用户给的范本文案、测试项目名称、测试项目流程写成通用规则。 + +模型完成研判后,生成一个业务理解模型稿 JSON,字段至少包含: + +- `product_positioning` +- `industry` +- `target_users` +- `core_value` +- `business_features` +- `business_feature_details` +- `operation_flow` +- `application_purpose` +- `main_functions` +- `technical_characteristics` +- `manual_sections` +- `manual_modules` +- `system_requirements` +- `faq` +- `glossary` + +其中 `manual_modules` 是操作手册的核心输入,必须按真实页面、导航入口或业务流程填写。脚本不得按 `auth/query/form` 等分类模板自动补入口、步骤或反馈;缺少 `manual_modules` 或关键字段时必须停止让模型回到项目证据中补写。每个模块必须包含: + +- `title`:页面或流程名称。 +- `evidence`:对应页面、路由、组件、需求文档或 README 证据。 +- `purpose`:该页面在软件中的用途。 +- `usage` 或 `usage_scenario`:用户在什么业务场景下会使用该页面,正在处理什么具体事务。缺少时不得生成操作手册。 +- `entry`:用户从哪里进入该页面。 +- `visible_elements`:用户实际能看到的输入框、按钮、列表、标签、状态或结果区域。 +- `operation_steps`:按真实页面顺序写用户动作,不能写代码实现。 +- `validation_rules`:必填项、长度限制、权限、额度、状态、异常提示等规则;没有则留空数组。 +- `feedback`:操作完成后用户能看到的结果、提示或状态变化。 +- `screenshot`:截图预留说明。 + +`manual_sections` 只允许补充当前软件本身的用途、业务场景、页面组织或用户流程,不要写“本操作手册用于……”“面向软著审核……”“不描述代码实现……”这类解释文档写作方式的元话语。最终操作手册应像真实软件说明书,而不是生成过程说明。 + +然后运行: + +```bash +python3 ${CLAUDE_SKILL_DIR}/scripts/generate_business_context.py \ + --project <项目目录> \ + --analysis 软件著作权申请资料/analysis/project.json \ + --software-name "<软件全称>" \ + --out-dir 软件著作权申请资料/草稿 \ + --model-context <模型生成的业务理解JSON> +``` + +输出: + +- `草稿/业务理解.md` +- `草稿/业务理解.json` + +最终业务理解必须覆盖: + +- 产品定位 +- 面向领域 / 行业 +- 目标用户 +- 核心价值 +- 主要业务功能 +- 典型操作流程 +- 申请表建议口径 +- 证据来源 +- 操作手册结构建议 + +如果项目材料不足、业务类型较新,或用户明确希望参考竞品,可联网搜索相近产品和行业资料;外部调研只用于理解行业表达,不能编造项目不存在的功能。调研摘要应写入业务理解草稿,并区分“项目证据”和“行业参考”。 + +生成 `业务理解.md/json` 后必须停止,等待用户确认或修改。业务理解确认前,不得生成申请表和操作手册。如果业务理解仍不充分,先请用户补充产品说明。用户确认后运行: + +```bash +python3 ${CLAUDE_SKILL_DIR}/scripts/confirm_stage.py \ + --workdir 软件著作权申请资料 \ + --stage business \ + --note "<用户确认内容>" +``` + +### 5. 引导用户确认字段 + +根据分析结果,向用户确认以下字段(按官网实际表单顺序): + +- 软件全称 +- 软件简称(可选) +- 版本号 +- 软件分类(应用软件/嵌入式软件/中间件/系统软件/其他) +- 开发完成日期(YYYY-MM-DD 格式) +- 开发方式(单独开发/合作开发/委托开发/下达任务开发) +- 软件说明(原创 / 修改(含翻译软件、合成软件)) +- 发表状态(已发表/未发表) +- 首次发表日期(已发表时填写,YYYY-MM-DD 格式;未发表则留空) +- 著作权人(复合字段:国家/省市/类型[自然人/法人]/姓名/证件类型/证件号) +- 权利范围(全部权利/部分权利) +- 权利取得方式(原始取得/继受取得) +- 开发的硬件环境(≤50字符) +- 运行的硬件环境(≤50字符) +- 开发该软件的操作系统(≤50字符) +- 软件开发环境 / 开发工具(≤50字符,格式:开发环境: xxx/开发工具: xxx) +- 该软件的运行平台 / 操作系统(≤50字符) +- 软件运行支撑环境 / 支持软件(≤50字符) +- 编程语言(预设按钮选择 + 自定义输入≤120字符) +- 源程序量(纯数字,单位为行,指全部源程序总行数) +- 开发目的(≤50字符) +- 面向领域 / 行业(≤50字符) +- 软件的主要功能(500~1300字) +- 软件的技术特点(多选标签 + 文本描述≤100字符;标签:APP/游戏软件/教育软件/金融软件/医疗软件/地理信息软件/云计算软件/信息安全软件/大数据软件/人工智能软件/VR软件/5G软件/小程序/物联网软件/智慧城市软件,都不符合时可不选) +- 页数(代码鉴别材料实际页数) + +项目可推断字段可以先给建议值;硬件/系统环境必须允许用户选择建议值或手动填写。字段口径必须区分清楚: + +- 软件全称:必须由用户确认。最终正式资料文件名、代码 Word 页眉、操作手册标题和正文中的软件名称,都必须与 `申请表信息.md` 的"软件全称"字段一致。 +- 软件简称:可选字段,如有常用简称则填写。 +- 版本号:必须由用户确认。优先读取项目配置中的版本号作为证据;如果项目版本号小于 V1.0(例如 V0.1.0、V0.9.0),必须明确询问用户"软著首次提交通常写 V1.0,本次填写 V1.0 还是项目当前版本号"。最终 `申请表信息.md` 的"版本号"字段就是正式资料版本号。 +- 软件分类:应用软件/嵌入式软件/中间件/系统软件/其他,默认为应用软件。 +- 开发完成日期、首次发表日期:必须使用 YYYY-MM-DD 格式。 +- 开发方式:单独开发/合作开发/委托开发/下达任务开发,默认单独开发。 +- 软件说明:原创 / 修改(含翻译软件、合成软件),默认原创。 +- 发表状态:已发表或未发表;已发表需附首次发表日期,未发表则首次发表日期留空。 +- 软件开发环境 / 开发工具:≤50字符,格式为 `开发环境: <操作系统>/开发工具: `,例如 `开发环境: Windows 11/开发工具: Visual Studio Code`;不要把 React、Next.js、Vite、TypeScript 等技术栈写到此字段。 +- 开发该软件的操作系统:≤50字符,填写实际开发电脑的操作系统版本,例如 Windows 10、Windows 11、macOS 14、macOS 15。 +- 该软件的运行平台 / 操作系统:≤50字符,填写软件运行所在的操作系统或浏览器环境。 +- 软件运行支撑环境 / 支持软件:≤50字符,直接列出运行依赖(如 Node.js、npm、浏览器),不加格式前缀。 +- 开发的硬件环境:≤50字符,优先读取当前电脑 CPU、内存、硬盘配置作为建议值;读取不到时让用户填写。 +- 运行的硬件环境:≤50字符,默认可沿用开发硬件环境建议值,也可以按实际部署或运行设备修改。 +- 源程序量:纯数字(不含"行"字),指登记软件全部源程序的总行数。 +- 开发目的:≤50字符,用一句话说明软件开发目的,不能只写软件名称。 +- 面向领域 / 行业:≤50字符。 +- 软件的主要功能:500~1300字,详细描述软件核心功能,至少 500 字。 +- 软件的技术特点:多选标签(APP/游戏软件/教育软件等)+ 文本描述≤100字符,简述技术架构和关键技术;标签都不符合时可不选。 + +此阶段需要先停止等待用户输入;收到用户回复后,可整理为 `answers` JSON 传入申请表草稿生成。申请表字段的最终门禁在 `草稿/申请表信息.md` 生成后记录。 + +### 6. 确认代码文件选择 + +生成代码材料前,先运行候选文件分析: + +```bash +python3 ${CLAUDE_SKILL_DIR}/scripts/propose_code_selection.py \ + --project <项目目录> \ + --analysis 软件著作权申请资料/analysis/project.json \ + --out-dir 软件著作权申请资料/草稿 +``` + +输出: + +- `草稿/代码文件候选清单.md`:给用户看的候选说明。 +- `草稿/代码文件选择.json`:可编辑的选择文件。 + +脚本生成的候选清单只列证据,不默认选择文件。模型必须先阅读业务理解、候选文件、入口文件、页面文件和必要源码,判断哪些源码最能体现软件真实功能和运行逻辑,然后修改 `代码文件选择.json`: + +- `selected: true` 表示抽取该文件。 +- `selected: false` 表示不抽取该文件。 +- `model_reason` 必须说明为什么选择该文件。 + +模型选择通常优先考虑前端入口、页面、核心组件、业务交互、数据请求、状态处理等能给审核员看懂软件功能的代码;如果相关前端代码不足 60 页,再补充后端服务、业务处理等相关源码。补充文件同样必须写入 `代码文件选择.json` 并由用户确认。不要默认抽取全量代码库。代码材料按完整文件抽取并去除纯空行,不支持只抽取某个文件的中间行段。用户确认并记录 `code-selection` 门禁后,代码抽取只读取 `代码文件选择.json` 中选中的完整文件。用户确认后运行: + +```bash +python3 ${CLAUDE_SKILL_DIR}/scripts/confirm_stage.py \ + --workdir 软件著作权申请资料 \ + --stage code-selection \ + --note "<用户确认内容>" +``` + +### 7. 生成 Markdown 草稿 + +运行代码材料抽取: + +```bash +python3 ${CLAUDE_SKILL_DIR}/scripts/extract_code_material.py \ + --project <项目目录> \ + --analysis 软件著作权申请资料/analysis/project.json \ + --selection 软件著作权申请资料/草稿/代码文件选择.json \ + --software-name "<软件全称>" \ + --version "<版本号>" \ + --out-dir 软件著作权申请资料/草稿 +``` + +代码分页规则: + +- 每页默认 50 行,并在 Word 中使用紧凑固定行距,尽量减少长行折行造成的页面溢出。 +- 总页数 `>= 60`:生成 `代码-前30页.md` 和 `代码-后30页.md`。 +- 总页数 `< 60` 且候选源码已用尽:只生成 `代码-全部.md`。 +- 总页数 `< 60` 但候选清单还有可补充源码:停止并要求用户在 `代码文件选择.json` 中继续选择补充文件。 +- 不为大项目生成超大“全量备份 Word”。 +- 同时生成 `代码提取清单.md` 和 `代码提取清单.json`,用于追溯代码来源。 + +生成申请表信息草稿: + +```bash +python3 ${CLAUDE_SKILL_DIR}/scripts/generate_application_info.py \ + --analysis 软件著作权申请资料/analysis/project.json \ + --code-manifest 软件著作权申请资料/草稿/代码提取清单.json \ + --business-context 软件著作权申请资料/草稿/业务理解.json \ + --software-name "<软件全称>" \ + --version "<版本号>" \ + --out-dir 软件著作权申请资料/草稿 +``` + +生成后必须停止,让用户检查并补全 `草稿/申请表信息.md`。字段补全并确认后运行: + +```bash +python3 ${CLAUDE_SKILL_DIR}/scripts/confirm_stage.py \ + --workdir 软件著作权申请资料 \ + --stage application-fields \ + --note "<用户确认内容>" +``` + +生成操作手册草稿: + +```bash +python3 ${CLAUDE_SKILL_DIR}/scripts/generate_manual_draft.py \ + --analysis 软件著作权申请资料/analysis/project.json \ + --business-context 软件著作权申请资料/草稿/业务理解.json \ + --software-name "<软件全称>" \ + --version "<版本号>" \ + --out-dir 软件著作权申请资料/草稿 +``` + +操作手册草稿不得照抄用户提供的范本文案或旧项目内容,但应吸收其结构特点:先写相关文档、说明、功能特点和系统要求,再按真实页面或核心流程逐章说明操作,最后写常见问题解答和术语表。一级章节标题使用中文大写序号;相关文档章节必须是表格;功能特点和页面操作章节必须以段落展开,不用项目符号和编号列表堆信息。必须基于模型写入 `草稿/业务理解.json` 的 `manual_modules` 组织章节;`manual_sections` 只用于补充说明性段落,不应用来反复插入同一批功能模块。各功能章节必须写清页面用途、进入位置、用户看到的控件和数据、实际操作、输入限制或异常提示、操作结果和截图预留。语言要面向普通用户,说明“这个页面是干嘛的、用户怎么进入、用户点什么/填什么、操作后看到什么”,不要写代码实现、框架名称、接口封装、状态管理、异步队列等技术细节。撰写时由 agent 自行检查章节是否完整、内容是否过薄、语言是否过于技术化,并在草稿内部完成必要补写;完整草稿完成后只让用户做一次整体确认,确认前不得进入正式 Word/TXT 生成。 + +生成脚本必须同时写出 `草稿/操作手册自检记录.md` 和 `草稿/操作手册自检记录.json`。自检记录至少包含: + +- 第 1 轮:初稿生成,检查章节完整性、截图预留、模块内容厚度和技术化表达。 +- 第 2 轮:按项目真实运行流程扩写模块说明,补足上下游衔接关系。 +- 第 3 轮:去除制式表达和 AI 味,重点检查重复句式、统一套话、空泛赞美、营销口号、过度整齐的排比和没有项目细节的正确废话。 +- 后续轮次:如果仍有问题,继续补写、去重、改写,不能把未修正的问题直接交给用户。 + +操作手册的模块写作必须从 `草稿/业务理解.json` 的行业、目标用户、核心价值、业务功能、典型操作流程和 `manual_modules` 出发。不同模块要写出各自的业务作用、入口、控件、规则和反馈,不能统一套用“进入页面、填写内容、提交按钮、查看结果”的固定句式,也不能使用“进入方式:/页面内容:/操作步骤:/操作规则:/操作结果与反馈:”这类字段标题;相近模块也要结合项目真实业务区分各自的操作目的和结果。自检时必须检查是否把同一批模块在多个章节中重复展开;如发现重复,改为每个真实页面或流程独立成章。不得把测试项目的功能名称、业务流程或示例文案写成通用规则。 + +### 8. 选择并获取截图 + +操作手册草稿完成后,先停止并让用户选择截图方式,必须给出三种选项: + +1. Chrome DevTools MCP:适合已在浏览器中打开的 Web 项目,优先用于网页全页截图。 +2. Codex Computer Use:适合需要通过桌面应用或浏览器界面点击、切换、查看状态后截图的场景。 +3. 用户自行截图:用户自己把 PNG/JPG/JPEG/WebP 图片放入 `软件著作权申请资料/用户截图/`,agent 只负责整理和引用。 + +如果用户明确说“现在不截图”“先跳过截图”“这次不截图”,也必须记录截图方式门禁,方法填 `skip`。跳过截图不阻塞正式资料生成,但操作手册中每个核心功能模块必须保留可见的截图预留文字,例如:`【截图预留:请在此处插入“项目管理”页面或操作结果截图。】`。不要使用 HTML 注释作为截图占位,因为正式 Word 中看不到。 + +用户选择后,先记录门禁: + +```bash +python3 ${CLAUDE_SKILL_DIR}/scripts/confirm_stage.py \ + --workdir 软件著作权申请资料 \ + --stage screenshot-method \ + --method \ + --note "<用户选择>" +``` + +然后按用户选择检查当前能力并执行: + +- 选择 Chrome DevTools MCP:先用工具发现能力检查当前环境是否有 `mcp__chrome_devtools__` 的 `list_pages`、`take_snapshot`、`take_screenshot`。可用时,先 `list_pages` 确认当前浏览器页面,再按页面/路由截图保存到 `软件著作权申请资料/截图/`;不可用时停止,告知用户需要重新选择截图方式或手动提供截图。 +- 选择 Codex Computer Use:先用工具发现能力检查当前环境是否有 `mcp__computer_use__` 的 `get_app_state`、`click`、`press_key`。可用时,先 `get_app_state` 查看目标应用或浏览器当前状态,再按操作手册需要导航和截图;如果当前 Computer Use 只能返回会话内截图而不能直接保存图片文件,则说明限制,并让用户改选 Chrome DevTools MCP 或把截图放入 `用户截图/`。 +- 选择用户自行截图:创建 `软件著作权申请资料/用户截图/`,提示用户把截图文件放入该目录;用户放入后运行下面的整理命令,把图片复制到 `软件著作权申请资料/截图/` 并生成 `截图清单.json`。 +- 选择跳过截图:不运行截图工具,继续保留操作手册中的可见截图预留文字;在生成报告中说明用户选择暂不截图,正式操作手册已预留截图位置。 + +```bash +python3 ${CLAUDE_SKILL_DIR}/scripts/capture_screenshots.py \ + --manual-dir 软件著作权申请资料/用户截图 \ + --out-dir 软件著作权申请资料/截图 +``` + +截图成功后,把截图引用补入 `草稿/操作手册.md`;截图失败或用户选择暂不提供截图时,继续生成带截图预留位的文字版,并在报告中说明“操作手册截图未生成或未插入,已保留截图预留位置”。 + +### 9. 用户确认 Markdown + +生成 Word 前,必须让用户确认 `软件著作权申请资料/草稿/` 下的 Markdown。 + +重点检查: + +- 软件名称和版本号是否一致 +- 代码材料前30页、后30页页眉软件名称是否与 `申请表信息.md` 的“软件全称”一致 +- 代码材料前30页、后30页页眉版本号是否与 `申请表信息.md` 的“版本号”一致 +- 操作手册 Word 页眉是否与代码材料页眉一致,均使用 `申请表信息.md` 的“软件全称”和“版本号” +- `业务理解.md` 是否准确反映软件真实业务、行业和目标用户 +- `申请表信息.md` 中“待用户确认”的字段是否已确认 +- 代码材料是否只来自用户确认的完整文件 +- 操作手册是否符合审核员阅读场景,普通读者是否能看懂模块用途和操作方式 +- 操作手册每个章节是否有段落内容,核心模块是否写清模块用途、操作过程和结果反馈,是否避免过度技术化语言 +- 截图是否正确;若用户跳过截图,正式操作手册是否保留可见截图预留位置 + +用户确认后,必须记录 `markdown` 门禁;未记录时不得生成正式 Word/TXT。 + +```bash +python3 ${CLAUDE_SKILL_DIR}/scripts/confirm_stage.py \ + --workdir 软件著作权申请资料 \ + --stage markdown \ + --note "<用户确认内容>" +``` + +### 10. 生成正式 Word/TXT + +用户确认后运行: + +```bash +python3 ${CLAUDE_SKILL_DIR}/scripts/build_docx_from_md.py \ + --workdir 软件著作权申请资料 \ + --software-name "<软件全称>" \ + --version "<版本号>" +``` + +正式生成脚本必须重新读取 `草稿/申请表信息.md` 中已确认的“软件全称”和“版本号”,并用它们生成正式资料文件名、代码 Word 页眉和操作手册 Word 页眉。操作手册页眉必须与代码材料页眉格式一致:左侧为“软件全称 版本号”,右侧为“第 <页码> 页”。若命令参数 `--software-name` / `--version` 与申请表字段不同,以申请表字段为准,并在 `正式资料/生成报告.md` 中记录提示。 + +输出: + +- `正式资料/申请表信息.txt` +- 代码达到或超过 60 页: + - `正式资料/<软件全称>-代码(前30页).docx` + - `正式资料/<软件全称>-代码(后30页).docx` +- 代码不足 60 页: + - `正式资料/<软件全称>-代码(全部).docx` +- `正式资料/<软件全称>_操作手册.docx` +- `正式资料/生成报告.md` + +### 11. 三轮验证 + +至少执行三轮验证并修复发现的问题: + +1. 文件完整性:目标 Word/TXT 是否存在且非空。 +2. 代码真实性:抽样检查代码片段能回溯到项目源码。 +3. 业务真实性:申请表和操作手册中的行业、目标用户、主要功能、操作流程能回溯到 `业务理解.md` 和项目文档。 +4. 一致性和格式:软件名称、版本号、页数规则、申请表字段、操作手册标题和截图引用是否一致。 + +可用命令: + +```bash +python3 -m py_compile ${CLAUDE_SKILL_DIR}/scripts/*.py +bash ${CLAUDE_SKILL_DIR}/vendor/docx-toolkit/scripts/docx_preview.sh <生成的docx> +``` + +完整 DOCX 环境检查和安装必须直接恢复/构建 `${CLAUDE_SKILL_DIR}/vendor/docx-toolkit/scripts/dotnet/DocxToolkit.Cli/DocxToolkit.Cli.csproj`,不要对 `vendor/docx-toolkit/scripts/dotnet` 目录或 `.slnx` 文件执行隐式 restore/build。 + +如果 `环境检查.md` 或 `${CLAUDE_SKILL_DIR}/vendor/docx-toolkit/scripts/env_check.sh` 显示 `.NET SDK` 缺失,说明完整 DOCX OpenXML 校验环境未就绪。用户明确选择不安装并记录 `environment` 门禁后,继续生成 Markdown、TXT 和基础 DOCX,并在报告中说明当前使用兜底路径。 + +## 何时询问用户 + +以下场景必须询问并停止,等待用户输入后再继续: + +- 多个项目候选目录需要选择。 +- 启动环境检查发现完整 DOCX 环境缺失时,询问用户是否安装完整环境。 +- 业务理解草稿生成后,请用户确认软件用途、行业、目标用户、核心功能和申请口径。 +- 软件全称、著作权人、日期、硬件/系统环境等登记字段需要确认。 +- 代码文件候选清单生成后,需要用户确认或修改 `代码文件选择.json`。 +- 操作手册截图前,需要用户在 Chrome DevTools MCP、Codex Computer Use、用户自行截图三种方式中选择一种;选择后再检查对应工具是否可用。 +- 用户是否确认 Markdown 草稿并进入 Word 生成。 diff --git a/software-copyright-materials/agents/openai.yaml b/software-copyright-materials/agents/openai.yaml new file mode 100644 index 0000000..93f1d11 --- /dev/null +++ b/software-copyright-materials/agents/openai.yaml @@ -0,0 +1,3 @@ +display_name: 软著申请资料生成 +short_description: 从真实项目生成软著申请 Word 和 TXT 材料 +default_prompt: 读取当前目录中的项目,生成软件著作权申请资料草稿,确认后输出 Word 和 TXT。 diff --git a/software-copyright-materials/references/application_fields.md b/software-copyright-materials/references/application_fields.md new file mode 100644 index 0000000..bcc9c54 --- /dev/null +++ b/software-copyright-materials/references/application_fields.md @@ -0,0 +1,61 @@ +# 申请表信息字段 + +按官网实际表单顺序和字段名: + +1. 软件全称 +2. 软件简称(可选) +3. 版本号 +4. 软件分类(应用软件/嵌入式软件/中间件/系统软件/其他) +5. 开发完成日期(YYYY-MM-DD) +6. 开发方式(单独开发/合作开发/委托开发/下达任务开发) +7. 软件说明(原创 / 修改(含翻译软件、合成软件)) +8. 发表状态(已发表/未发表) +9. 首次发表日期(已发表时填写,YYYY-MM-DD) +10. 著作权人(复合字段:国家/省市/类型[自然人/法人]/姓名/证件类型/证件号) +11. 权利范围(全部权利/部分权利) +12. 权利取得方式(原始取得/继受取得) +13. 开发的硬件环境(≤50字符) +14. 运行的硬件环境(≤50字符) +15. 开发该软件的操作系统(≤50字符) +16. 软件开发环境 / 开发工具(≤50字符,格式:开发环境: xxx/开发工具: xxx) +17. 该软件的运行平台 / 操作系统(≤50字符) +18. 软件运行支撑环境 / 支持软件(≤50字符) +19. 编程语言(预设按钮选择 + 自定义输入≤120字符) +20. 源程序量(纯数字,单位行,指全部源程序总行数) +21. 开发目的(≤50字符,不能只写软件名称) +22. 面向领域 / 行业(≤50字符) +23. 软件的主要功能(500~1300字) +24. 软件的技术特点(多选标签 + 文本描述≤100字符;标签:APP/游戏软件/教育软件/金融软件/医疗软件/地理信息软件/云计算软件/信息安全软件/大数据软件/人工智能软件/VR软件/5G软件/小程序/物联网软件/智慧城市软件,都不符合时可不选) +25. 页数(代码鉴别材料实际页数) + +## 字段来源与填写口径 + +- 软件全称、版本号、著作权人、日期:由用户确认。 +- 软件全称必须显式确认;正式资料文件名、代码 Word 页眉、操作手册标题和正文中的软件名称均以申请表信息中的"软件全称"为准。 +- 软件简称:可选;如有常用简称则填写。 +- 版本号必须显式确认;如果项目配置中的版本号小于 V1.0,需提醒用户软著首次提交通常写 V1.0,并让用户确认填写 V1.0 还是项目当前版本号。 +- 软件分类:默认选"应用软件"。 +- 开发完成日期和首次发表日期:必须使用 YYYY-MM-DD 格式。 +- 开发方式:默认"单独开发",多人合作项目选"合作开发"。 +- 软件说明:默认"原创"。 +- 发表状态:用户确认已发表或未发表;已发表需附首次发表日期。 +- 编程语言、源程序量、功能模块、技术特点:根据项目分析生成。编程语言官网为预设按钮选择 + 自定义输入(≤120字符),预设选项包括 Assembly language、C、C#、C++、Delphi/Object Pascal、Go、HTML、Java、JavaScript、MATLAB、Objective-C、PHP、PL/SQL、Perl、Python、R、Ruby、SQL、Swift、Visual Basic、Visual Basic .Net。 +- 源程序量:只填纯数字(不含"行"字),指登记软件全部源程序的总行数(非仅代码材料抽取行数)。 +- 软件开发环境 / 开发工具:≤50字符,格式 `开发环境: /开发工具: `,例如 `开发环境: Windows 11/开发工具: Visual Studio Code`;不要填写 React、Next.js、Vite、TypeScript 等技术栈。 +- 开发该软件的操作系统:≤50字符,填写实际开发电脑的操作系统版本。 +- 该软件的运行平台 / 操作系统:≤50字符,填写软件运行所在的操作系统或浏览器环境。 +- 软件运行支撑环境 / 支持软件:≤50字符,直接列出运行依赖(如 Node.js、npm、浏览器),不加格式前缀。 +- 开发的硬件环境:≤50字符,优先读取当前电脑 CPU、内存、硬盘配置作为建议值。 +- 运行的硬件环境:≤50字符,默认可沿用开发硬件建议值,也可按实际运行设备填写。 +- 开发目的:≤50字符,用一句话说明软件开发目的,不能只写软件名称。 +- 面向领域 / 行业:≤50字符。 +- 软件的主要功能:500~1300字,详细描述软件核心功能,至少 500 字。 +- 软件的技术特点:多选标签(APP/游戏软件/教育软件等)+ 文本描述≤100字符;标签都不符合时可不选。 + +## 一致性要求 + +- 软件全称和版本号必须与代码材料、操作手册一致。 +- 正式代码 Word 页眉软件名称必须与申请表信息中的“软件全称”一致,生成 Word 时以申请表软件全称为准。 +- 正式代码 Word 页眉版本号必须与申请表信息中的“版本号”一致,生成 Word 时以申请表版本号为准。 +- 主要功能必须来自当前项目,不得沿用范本中的旧项目描述。 +- `待用户确认` 字段在正式输出前应尽量替换为确认值;如仍存在,必须写入生成报告。 diff --git a/software-copyright-materials/references/business_understanding_rules.md b/software-copyright-materials/references/business_understanding_rules.md new file mode 100644 index 0000000..72c64b2 --- /dev/null +++ b/software-copyright-materials/references/business_understanding_rules.md @@ -0,0 +1,49 @@ +# 业务理解规则 + +申请表信息和操作手册不能只根据代码结构泛泛生成,必须先理解软件业务。 + +## 证据收集 + +先用脚本收集证据,输出 `草稿/业务理解证据.md/json` 和 `草稿/业务理解模型稿模板.json`。证据通常包括: + +- `README.md` +- `docs/*PRD*.md` +- `docs/*BRD*.md` +- `docs/*ARCHITECTURE*.md` +- 产品说明、需求文档、设计文档 +- 前端页面标题、按钮文案、路由、核心组件名 +- 后端 API 路由和模型名称 + +这些只是候选证据,不代表最终行业、功能和手册结构。 + +## 输出业务理解草稿 + +模型必须阅读证据和必要源码,自行判断应该抽取哪些业务信息,再生成业务理解模型稿 JSON。不得用关键字表或固定模板决定行业、功能和结构。 + +模型稿经脚本校验后生成 `草稿/业务理解.md` 和 `草稿/业务理解.json`,至少包含: + +- 产品定位 +- 面向领域 / 行业 +- 目标用户 +- 用户痛点和核心价值 +- 主要业务功能 +- 典型操作流程 +- 操作手册结构建议 +- 操作手册页面/流程模块,必须说明每个真实页面或核心流程的使用场景、进入位置、用户可见元素、用户动作、输入/状态规则、结果反馈和截图预留 +- 申请表建议口径 +- 证据来源 +- 待用户确认项 + +## 外部调研 + +如果项目材料不足、业务类型较新,或用户明确希望参考竞品,可联网搜索相近产品和行业资料。 + +外部调研只用于帮助理解行业表达,不能编造项目不存在的功能。需要把调研结论写入业务理解草稿,并区分“项目证据”和“行业参考”。 + +## 生成材料约束 + +- `申请表信息.md/txt` 的开发目的、行业、主要功能、技术特点必须优先来自业务理解。 +- `操作手册.md/docx` 的说明、功能特点、系统要求、核心页面/流程、常见问题、术语表和章节结构必须优先来自模型确认后的业务理解。 +- 操作手册不应只生成抽象“功能列表”。模型应把路由、页面、按钮、输入框、列表、弹窗、状态提示、额度或权限规则等用户可见证据整理到 `manual_modules`,供脚本按通用操作手册骨架排版。最终成稿应是段落化用户手册,不是“进入方式/页面内容/操作步骤/结果反馈”的字段列表。 +- 如果缺少 `manual_modules`、`system_requirements`、`faq` 或 `glossary`,应回到业务理解阶段补充真实内容;脚本不得用分类模板兜底生成。 +- 如果业务理解仍不充分,先提示用户补充产品说明,而不是直接生成泛泛描述。 diff --git a/software-copyright-materials/references/code_selection_rules.md b/software-copyright-materials/references/code_selection_rules.md new file mode 100644 index 0000000..95ddea2 --- /dev/null +++ b/software-copyright-materials/references/code_selection_rules.md @@ -0,0 +1,39 @@ +# 代码抽取规则 + +## 选择方式 + +脚本只生成候选源码清单,不默认决定抽取文件。模型需要先理解项目业务、页面入口和源码职责,再决定抽取哪些文件或行段,并在 `代码文件选择.json` 中填写: + +- `selected` +- `start_line` +- `end_line` +- `model_reason` + +选择时通常优先考虑审核员能看懂软件功能和运行逻辑的源码,例如入口、页面、业务组件、数据交互、状态处理、业务服务等。具体选择由模型根据项目实际判断,不能用固定路径规则直接拍板。 + +## 排除项 + +排除以下内容: + +- `node_modules` +- `dist`、`build`、`.next`、`.nuxt`、`coverage` +- lock 文件 +- 图片、字体、二进制文件 +- sourcemap、minified 文件 +- 自动生成文件 +- 过短且无业务意义的配置文件 + +## 真实性要求 + +- 保留原始代码文本。 +- 可添加文件路径标记用于追溯。 +- 不改写业务逻辑。 +- 不使用 AI 补齐代码。 + +## 用户确认要求 + +- 代码抽取前必须先生成 `代码文件候选清单.md` 和 `代码文件选择.json`。 +- 模型必须先填写抽取选择和 `model_reason`,再让用户确认或手动调整 `selected`。 +- 用户可以通过 `start_line` / `end_line` 只抽取某个文件的指定行段。 +- 抽取脚本必须以 `代码文件选择.json` 为准,不能绕过确认步骤直接抽全量代码库。 +- `代码提取清单.md` 必须记录每个文件的抽取行段,便于回溯。 diff --git a/software-copyright-materials/references/copyright_material_rules.md b/software-copyright-materials/references/copyright_material_rules.md new file mode 100644 index 0000000..40841b0 --- /dev/null +++ b/software-copyright-materials/references/copyright_material_rules.md @@ -0,0 +1,22 @@ +# 软著材料规则 + +## 鉴别材料 + +根据《计算机软件著作权登记办法》第十条,软件鉴别材料包括程序和文档的鉴别材料。 + +执行规则: + +- 源程序和文档一般由前、后各连续 30 页组成。 +- 整个程序或文档不足 60 页时,提交全部。 +- 除特定情况外,程序每页不少于 50 行。 +- 除特定情况外,文档每页不少于 30 行。 + +## 本 skill 的落地规则 + +- 代码分页默认每页 50 行。 +- 总页数 `>= 60` 时,只输出前 30 页和后 30 页代码材料。 +- 总页数 `< 60` 时,只输出全部代码材料。 +- 不为大项目输出全量代码 Word,避免文件过大且不符合常规提交需求。 +- 代码材料必须来自项目源文件,不能由 AI 生成。 +- 文件页眉或页首必须包含软件全称和版本号。 +- 页码必须连续且清晰。 diff --git a/software-copyright-materials/references/manual_structure.md b/software-copyright-materials/references/manual_structure.md new file mode 100644 index 0000000..e764ea7 --- /dev/null +++ b/software-copyright-materials/references/manual_structure.md @@ -0,0 +1,39 @@ +# 操作手册结构 + +操作手册应像真实软件随附的操作说明,目标是让读者知道软件用途、功能和基本操作。 + +推荐采用软著审核友好的通用骨架,类似传统操作手册: + +1. 相关文档:用表格指向总体设计、详细设计、测试案例等配套资料。 +2. 说明:说明软件定位、目标用户、业务场景和整体流程。 +3. 功能特点:按当前项目真实功能概括 4-8 项特点,每项说明业务作用和用户可见结果。 +4. 系统要求:用表格说明最低配置和推荐配置;Web 项目可写浏览器、网络和服务访问要求,桌面项目可写操作系统、处理器、内存、存储和分辨率。 +5. 具体页面 / 功能操作:从第 5 章开始,按真实页面、导航入口或核心流程逐章说明,每章写使用场景、页面用途、进入位置、页面内容、用户动作、输入限制或异常提示、操作结果和截图预留。 +6. 典型使用流程:如项目存在清晰串联流程,可单独写一章串起从进入软件到完成核心任务的过程。 +7. 常见问题解答:写 3-5 个与当前软件真实使用相关的问题和解决方法。 +8. 术语表:解释软件名称、核心业务对象、页面模块和用户可能不熟悉的术语。 + +以上是通用骨架,不是旧项目内容。正式章节标题使用中文大写序号,例如 `一、相关文档`,不要使用 `(1)、相关文档`。生成时必须根据当前项目业务、页面入口、功能关系和用户可见控件填充内容,不要照抄用户提供的范本文案。 +具体页面和流程必须来自 `草稿/业务理解.json` 中模型写入的 `manual_modules`。如果该字段为空,不能根据 `business_features` 生成兜底模块,应停止并要求模型阅读真实页面和项目资料后补全。 + +## 写作口径 + +- 使用通用、客观、简洁的中文。 +- 不写面向终端用户的复杂教程。 +- 每个章节必须有段落化说明,不能只写项目符号列表;正文不要用 `-`、`*`、`1. 2. 3.` 堆信息。 +- 每个核心页面或功能模块必须覆盖“使用场景 + 页面用途 + 进入位置 + 页面内容 + 用户动作 + 输入/状态规则 + 系统反馈 + 截图预留”,但这些信息要合并成自然段落,不能直接输出成字段表单。 +- 优先写用户真实能看到和操作的内容,例如输入框、按钮、下拉框、标签页、列表、卡片、弹窗、错误提示、状态栏、导入导出入口、额度或权限提示。 +- 补充说明段落只能写当前软件的用途、业务场景、页面组织和用户流程,不写“本操作手册用于……”“面向软著审核……”“不描述代码实现……”这类解释文档写作方式的元话语。 +- 禁止在脚本中按 auth、query、form、workflow 等分类自动生成入口、步骤或结果反馈。入口和步骤必须来自模型对当前项目真实页面的阅读。 +- 功能特点不要写成“开头一句总述 + 编号列表 + 结论”的头中尾结构。每个特点用段落展开,说明该功能解决什么业务问题、用户在页面上看到什么、完成操作后得到什么结果。不同功能的说明要有差异,避免每条都使用相同句式。 +- 页面章节不要输出“进入方式:”“页面内容:”“操作步骤:”“操作规则:”“操作结果与反馈:”这类模板小标题。 +- 操作手册语言要让审核员和普通读者能看懂,重点说明模块是做什么的、怎么操作、操作后看到什么。避免代码、框架、接口、状态管理、异步任务等技术化表达。 +- “AI 味”主要表现为空泛、整齐、万能、没有项目现场感:例如每个模块都用同一种句式,反复写“提升效率、优化体验、提供支持”,使用“旨在、赋能、一站式、智能化、高效便捷、显著提升、强大能力、丰富功能”等口号,却没有说明当前项目的真实页面、真实对象、真实动作和真实反馈。发现这类内容时必须改写成朴素、具体、可回溯到项目证据的表达。 +- 截图前必须先让用户在 Chrome DevTools MCP、Codex Computer Use、用户自行截图三种方式中选择;选择后检查对应能力是否可用。用户说现在不截图或先跳过截图时,记录为 `skip`,并在每个需要截图的位置保留正式 Word 中可见的截图预留文字。 +- 不夸大不存在的功能。 +- 功能名称和章节组织由模型根据项目证据判断;路由、页面、README、接口和组件命名只是证据来源,不是固定抽取规则。 +- Markdown 草稿生成前由 agent 自行检查章节完整性、内容厚度、项目流程一致性和语言自然度,发现章节过薄、模块套话、AI 味、技术化表达或相邻模块含义混淆时先循环补写和修正;完整草稿生成后只向用户发起一次整体确认,再进入 Word 生成。 +- 操作手册生成时必须同步输出 `操作手册自检记录.md/json`。记录中至少包含初稿生成、按项目流程扩写、去除制式表达和 AI 味三轮;如果第三轮仍发现问题,要继续自动修正并追加轮次记录。 +- 操作手册必须基于已确认的业务理解写作。相近功能应结合项目真实业务分别说明各自的操作目的、用户动作和结果反馈,不能用同一段话替换不同模块。 +- 自检时必须检查是否生成了“功能操作说明”大章下反复套同一批模块的情况;如果出现同一模块重复多次,必须改为每个真实页面独立成章。 +- 禁止把测试项目中的行业、角色、流程、功能名称或示例文案写成通用规则;范本只能帮助理解软著手册需要“通顺、具体、能给审核员看懂”,不能作为固定内容来源。 diff --git a/software-copyright-materials/scripts/analyze_project.py b/software-copyright-materials/scripts/analyze_project.py new file mode 100644 index 0000000..760be8d --- /dev/null +++ b/software-copyright-materials/scripts/analyze_project.py @@ -0,0 +1,363 @@ +#!/usr/bin/env python3 +"""Analyze a project and produce facts used by the copyright material workflow.""" + +from __future__ import annotations + +import argparse +import re +from collections import Counter +from pathlib import Path +from typing import Any + +from common import COPYRIGHT_CODE_EXTS, FRONTEND_EXTS, count_text_lines, is_known_config_file, iter_project_files, normalize_title, read_json, read_text, rel, write_json + + +DEPENDENCY_FRAMEWORKS = { + "vue": "Vue", + "@vue/runtime-core": "Vue", + "react": "React", + "next": "Next.js", + "nuxt": "Nuxt", + "svelte": "Svelte", + "astro": "Astro", + "@angular/core": "Angular", + "vite": "Vite", + "uni-app": "UniApp", + "@dcloudio/uni-app": "UniApp", + "electron": "Electron", + "@tauri-apps/api": "Tauri", +} + +ENTRY_NAMES = { + "main.ts", + "main.js", + "main.tsx", + "main.jsx", + "index.tsx", + "index.jsx", + "app.vue", + "App.vue", + "app.tsx", +} + + +def load_package(project: Path) -> tuple[dict[str, Any] | None, Path | None]: + candidates = [ + project / "package.json", + project / "frontend/package.json", + project / "client/package.json", + project / "web/package.json", + project / "app/package.json", + ] + for package_path in candidates: + if not package_path.exists(): + continue + try: + return read_json(package_path), package_path + except Exception: + continue + return None, None + + +def detect_frameworks(package: dict[str, Any] | None, files: list[Path], project: Path) -> list[str]: + found: set[str] = set() + deps: dict[str, Any] = {} + if package: + for key in ("dependencies", "devDependencies", "peerDependencies"): + deps.update(package.get(key) or {}) + for dep in deps: + if dep in DEPENDENCY_FRAMEWORKS: + found.add(DEPENDENCY_FRAMEWORKS[dep]) + suffixes = {p.suffix.lower() for p in files} + if ".vue" in suffixes: + found.add("Vue") + if ".tsx" in suffixes or ".jsx" in suffixes: + if "Vue" not in found: + found.add("React") + if (project / "vite.config.ts").exists() or (project / "vite.config.js").exists(): + found.add("Vite") + if (project / "next.config.js").exists() or (project / "next.config.ts").exists() or any(p.name in {"next.config.js", "next.config.ts"} for p in files): + found.add("Next.js") + return sorted(found) + + +def classify(path: Path, project: Path) -> str: + r = rel(path, project).lower() + name = path.name + if name in ENTRY_NAMES or r in {"src/app/page.tsx", "src/app/layout.tsx", "app/page.tsx", "app/layout.tsx"} or r.endswith("/src/app/page.tsx") or r.endswith("/src/app/layout.tsx"): + return "entry" + if path.suffix.lower() in {".css", ".scss", ".sass", ".less"}: + return "style" + if any(part in r for part in ("/router/", "/routes/", "router.", "routes.")): + return "route" + if any(part in r for part in ("/pages/", "/views/", "/app/", "/screens/")): + return "page" + if "/components/" in r: + return "component" + if any(part in r for part in ("/api/", "/apis/", "/services/", "request.", "/request/", "/controllers/", "/handlers/", "/views.py")): + return "api" + if any(part in r for part in ("/models/", "/schemas/", "/entities/", "/repositories/", "/dao/")): + return "model" + if any(part in r for part in ("/store/", "/stores/", "/pinia/", "/redux/", "/zustand/")): + return "state" + if any(part in r for part in ("/utils/", "/lib/", "/hooks/", "/composables/", "/helpers/")): + return "utility" + return "source" + + +def extract_route_paths(path: Path) -> list[str]: + try: + text = read_text(path, limit=200_000) + except Exception: + return [] + patterns = [ + r"path\s*:\s*['\"]([^'\"]+)['\"]", + r"]+path=['\"]([^'\"]+)['\"]", + r"href=['\"](/[^'\"]*)['\"]", + ] + routes: list[str] = [] + for pattern in patterns: + for match in re.findall(pattern, text): + if match.startswith("/") and len(match) < 120 and "*" not in match: + routes.append(match) + return routes + + +def summarize_readme(project: Path) -> str: + for name in ("README.md", "README.zh.md", "readme.md", "Readme.md"): + path = project / name + if path.exists(): + text = read_text(path, limit=4000) + return "\n".join(line.strip() for line in text.splitlines()[:60] if line.strip()) + return "" + + +def analyze(project: Path) -> dict[str, Any]: + project = project.resolve() + package, package_path = load_package(project) + source_files = [p for p in iter_project_files(project, COPYRIGHT_CODE_EXTS) if not is_known_config_file(p)] + frontend_files = [p for p in source_files if p.suffix.lower() in FRONTEND_EXTS] + class_counts: Counter[str] = Counter() + extension_counts: Counter[str] = Counter() + source_lines = 0 + total_source_lines = 0 + categorized: dict[str, list[str]] = { + "entry": [], + "route": [], + "page": [], + "component": [], + "api": [], + "model": [], + "state": [], + "utility": [], + "style": [], + "source": [], + } + route_paths: list[str] = ["/"] + + for path in source_files: + category = classify(path, project) + class_counts[category] += 1 + extension_counts[path.suffix.lower()] += 1 + categorized[category].append(rel(path, project)) + source_lines += count_text_lines(path, skip_blank=False) + if path.suffix.lower() in FRONTEND_EXTS and category in {"route", "page", "entry"}: + route_paths.extend(extract_route_paths(path)) + + total_source_lines = source_lines + + package_name = "" + scripts: dict[str, str] = {} + dependencies: dict[str, str] = {} + if package: + package_name = str(package.get("name") or "") + scripts = {k: str(v) for k, v in (package.get("scripts") or {}).items()} + for key in ("dependencies", "devDependencies"): + dependencies.update({k: str(v) for k, v in (package.get(key) or {}).items()}) + + frameworks = detect_frameworks(package, frontend_files, project) + language = infer_language(extension_counts, frameworks) + route_paths = sorted(set(route_paths), key=lambda x: (x.count("/"), x)) + + return { + "project_root": str(project), + "project_name": project.name, + "software_name_candidate": normalize_title(package_name or project.name), + "package": { + "name": package_name, + "path": rel(package_path, project) if package_path else "", + "version": str(package.get("version") or "V1.0") if package else "V1.0", + "scripts": scripts, + "dependency_names": sorted(dependencies), + }, + "frameworks": frameworks, + "language": language, + "source": { + "file_count": len(source_files), + "line_count": source_lines, + "total_file_count": len(source_files), + "total_line_count": total_source_lines, + "extension_counts": dict(sorted(extension_counts.items())), + "category_counts": dict(sorted(class_counts.items())), + "categorized_files": {k: v[:80] for k, v in categorized.items() if v}, + }, + "routes": route_paths[:80], + "readme_excerpt": summarize_readme(project), + "run_command_candidates": infer_run_commands(scripts), + "feature_candidates": infer_features(categorized, route_paths), + } + + +def infer_workdir(out: Path) -> Path: + if out.parent.name == "analysis": + return out.parent.parent + return out.parent + + +def check_environment_gate(out: Path) -> None: + workdir = infer_workdir(out) + env_path = workdir / "环境检查.json" + if not env_path.exists(): + return + env = read_json(env_path) + if not env.get("requires_user_input"): + return + confirmation_path = workdir / "环境确认.json" + confirmed = False + if confirmation_path.exists(): + confirmed = bool(read_json(confirmation_path).get("environment_confirmed")) + if not confirmed: + raise SystemExit( + "STOP_FOR_USER\n" + "NEXT_ACTION: 完整 DOCX 环境未确认。请先让用户选择安装完整环境或使用基础 DOCX 兜底继续," + "然后运行 `python3 /scripts/confirm_stage.py --workdir 软件著作权申请资料 --stage environment --note \"<用户选择>\"`。" + ) + + +def infer_language(extension_counts: Counter[str], frameworks: list[str]) -> str: + langs: list[str] = [] + if extension_counts.get(".ts") or extension_counts.get(".tsx"): + langs.append("TypeScript") + if extension_counts.get(".js") or extension_counts.get(".jsx"): + langs.append("JavaScript") + language_by_ext = { + ".py": "Python", + ".java": "Java", + ".go": "Go", + ".rs": "Rust", + ".cs": "C#", + ".php": "PHP", + ".rb": "Ruby", + ".kt": "Kotlin", + ".swift": "Swift", + ".sql": "SQL", + ".sh": "Shell", + } + for ext, label in language_by_ext.items(): + if extension_counts.get(ext): + langs.append(label) + if not langs: + langs = [ext.lstrip(".").upper() for ext, _ in extension_counts.most_common(3) if ext] + return "、".join(dict.fromkeys(langs)) or "待用户确认" + + +def infer_run_commands(scripts: dict[str, str]) -> list[str]: + preferred = ["dev", "start", "serve", "preview"] + commands = [] + for name in preferred: + if name in scripts: + commands.append(f"npm run {name}") + return commands + + +def infer_features(categorized: dict[str, list[str]], routes: list[str]) -> list[str]: + stop = { + "index", + "main", + "app", + "layout", + "page", + "globals", + "providers", + "loading", + "error", + "not-found", + "template", + "default", + "button", + "input", + "label", + "avatar", + "card", + "textarea", + "scroll area", + } + names: list[str] = [] + for route in routes: + cleaned = route.strip("/").replace("-", " ").replace("_", " ") + if cleaned and not cleaned.startswith(":") and cleaned.lower() not in stop: + names.append(cleaned) + for file in categorized.get("page", [])[:60]: + route_name = feature_from_page_path(file) + if route_name and route_name.lower() not in stop: + names.append(route_name) + for category in ("api", "component"): + for file in categorized.get(category, [])[:30]: + lowered = file.lower() + if "/ui/" in lowered or "/components/ui/" in lowered: + continue + stem = Path(file).stem + normalized = stem.replace("-", " ").replace("_", " ").strip() + if normalized.lower() not in stop: + names.append(normalized) + unique: list[str] = [] + for name in names: + normalized = re.sub(r"\s+", " ", name).strip() + if normalized and normalized not in unique: + unique.append(normalized) + return unique[:30] + + +def feature_from_page_path(file: str) -> str: + parts = Path(file).parts + useful: list[str] = [] + for part in parts: + if part in {"src", "app", "pages", "views", "screens", "frontend", "client", "web"}: + continue + if part.startswith("(") and part.endswith(")"): + continue + stem = Path(part).stem + if stem in {"page", "layout", "index", "route", "loading", "error", "globals", "providers"}: + continue + if stem.startswith("[") and stem.endswith("]"): + continue + useful.append(stem) + return " ".join(useful[-2:]).replace("-", " ").replace("_", " ").strip() + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--project", required=True, help="Project root directory") + parser.add_argument("--out", default="软件著作权申请资料/analysis/project.json") + args = parser.parse_args() + + project = Path(args.project) + if not project.exists(): + raise SystemExit(f"Project not found: {project}") + + out = Path(args.out) + check_environment_gate(out) + result = analyze(project) + write_json(out, result) + print(f"OK analysis: {out}") + print(f"Project: {result['project_name']}") + print(f"Frameworks: {', '.join(result['frameworks']) or 'unknown'}") + print(f"Language: {result['language']}") + print(f"Source files: {result['source']['file_count']}") + print(f"Source lines: {result['source']['line_count']}") + print(f"Total source files: {result['source']['total_file_count']}") + print(f"Total source lines: {result['source']['total_line_count']}") + + +if __name__ == "__main__": + main() diff --git a/software-copyright-materials/scripts/build_docx_from_md.py b/software-copyright-materials/scripts/build_docx_from_md.py new file mode 100644 index 0000000..c4c734d --- /dev/null +++ b/software-copyright-materials/scripts/build_docx_from_md.py @@ -0,0 +1,861 @@ +#!/usr/bin/env python3 +"""Build final DOCX/TXT files from confirmed Markdown drafts.""" + +from __future__ import annotations + +import argparse +import html +import re +import shutil +import subprocess +import tempfile +import zipfile +from pathlib import Path +from typing import Any + +from common import ensure_dir, read_json, safe_filename + +try: + from docx import Document + from docx.enum.section import WD_SECTION + from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING + from docx.oxml import OxmlElement + from docx.oxml.ns import qn + from docx.shared import Cm, Inches, Pt, RGBColor + + DOCX_AVAILABLE = True +except Exception: + DOCX_AVAILABLE = False + + +BLACK_RGB = "000000" + + +def strip_markdown_links(text: str) -> str: + text = re.sub(r"(?]+)>", r"\1", text) + return text + + +def parse_application_lines(md_path: Path) -> tuple[list[str], list[str]]: + lines = md_path.read_text(encoding="utf-8").splitlines() + fields = [line.strip() for line in lines if line.strip().startswith("➤")] + warnings = [line for line in fields if "待用户确认" in line] + return fields, warnings + + +def parse_application_field(md_path: Path, field_name: str) -> str: + if not md_path.exists(): + return "" + prefix = f"➤{field_name}:" + for line in md_path.read_text(encoding="utf-8").splitlines(): + stripped = line.strip() + if stripped.startswith(prefix): + return stripped[len(prefix) :].strip() + return "" + + +def application_version(draft_dir: Path) -> str: + version = parse_application_field(draft_dir / "申请表信息.md", "版本号") + if "待用户确认" in version: + return "" + return version + + +def application_software_name(draft_dir: Path) -> str: + name = parse_application_field(draft_dir / "申请表信息.md", "软件全称") + if "待用户确认" in name: + return "" + return name + + +def write_application_txt(draft_dir: Path, out_dir: Path) -> tuple[Path | None, list[str]]: + md_path = draft_dir / "申请表信息.md" + if not md_path.exists(): + return None, ["缺少草稿/申请表信息.md"] + fields, warnings = parse_application_lines(md_path) + out_path = out_dir / "申请表信息.txt" + out_path.write_text("\n".join(fields) + "\n", encoding="utf-8") + return out_path, warnings + + +def read_json_if_exists(path: Path) -> dict[str, Any]: + if not path.exists(): + return {} + return read_json(path) + + +def confirmation_issues(workdir: Path) -> list[str]: + draft_dir = workdir / "草稿" + issues: list[str] = [] + + business = read_json_if_exists(draft_dir / "业务理解.json") + if not business or not business.get("user_confirmed"): + issues.append("业务理解尚未确认:请确认 草稿/业务理解.md 后记录 `business` 门禁") + + selection = read_json_if_exists(draft_dir / "代码文件选择.json") + if not selection or not selection.get("user_confirmed"): + issues.append("代码文件选择尚未确认:请确认 草稿/代码文件选择.json 后记录 `code-selection` 门禁") + + screenshot = read_json_if_exists(workdir / "截图方式确认.json") + if not screenshot.get("screenshot_method_confirmed"): + issues.append("截图方式尚未确认:请选择截图方式后记录 `screenshot-method` 门禁") + + app_md = draft_dir / "申请表信息.md" + if app_md.exists(): + _, warnings = parse_application_lines(app_md) + if warnings: + issues.append("申请表信息仍包含“待用户确认”字段") + else: + issues.append("缺少 草稿/申请表信息.md") + + app_confirmation = read_json_if_exists(draft_dir / "申请表字段确认.json") + if not app_confirmation.get("application_fields_confirmed"): + issues.append("申请表字段尚未确认:请补全字段后记录 `application-fields` 门禁") + + markdown_confirmation = read_json_if_exists(draft_dir / "最终生成确认.json") + if not markdown_confirmation.get("markdown_confirmed"): + issues.append("Markdown 草稿尚未最终确认:请确认全部草稿后记录 `markdown` 门禁") + + return issues + + +def parse_code_pages(md_path: Path) -> list[tuple[int, list[str]]]: + pages: list[tuple[int, list[str]]] = [] + current_no: int | None = None + current_lines: list[str] = [] + in_fence = False + + for raw in md_path.read_text(encoding="utf-8").splitlines(): + page_match = re.match(r"^##\s+第\s*(\d+)\s*页", raw.strip()) + if page_match: + if current_no is not None: + pages.append((current_no, current_lines)) + current_no = int(page_match.group(1)) + current_lines = [] + in_fence = False + continue + if raw.strip().startswith("```"): + in_fence = not in_fence + continue + if current_no is not None and in_fence: + current_lines.append(raw) + + if current_no is not None: + pages.append((current_no, current_lines)) + return pages + + +def set_run_font(run: Any, name: str, size_pt: float) -> None: + run.font.name = name + run.font.size = Pt(size_pt) + try: + run.font.color.rgb = RGBColor(0, 0, 0) + except Exception: + pass + try: + run._element.rPr.rFonts.set(qn("w:eastAsia"), name) + except Exception: + pass + + +def set_normal_font(document: Any, name: str = "SimSun", size_pt: float = 10.5) -> None: + style = document.styles["Normal"] + style.font.name = name + style.font.size = Pt(size_pt) + try: + style.font.color.rgb = RGBColor(0, 0, 0) + except Exception: + pass + try: + style._element.rPr.rFonts.set(qn("w:eastAsia"), name) + except Exception: + pass + + +def set_style_black(document: Any) -> None: + for style_name in ("Normal", "Heading 1", "Heading 2", "Heading 3", "List Bullet", "List Number"): + try: + document.styles[style_name].font.color.rgb = RGBColor(0, 0, 0) + except Exception: + pass + + +def force_black_document(document: Any) -> None: + set_style_black(document) + containers = [document] + for section in document.sections: + containers.extend([section.header, section.footer]) + for container in containers: + for paragraph in container.paragraphs: + for run in paragraph.runs: + try: + run.font.color.rgb = RGBColor(0, 0, 0) + except Exception: + pass + for table in container.tables: + for row in table.rows: + for cell in row.cells: + for paragraph in cell.paragraphs: + for run in paragraph.runs: + try: + run.font.color.rgb = RGBColor(0, 0, 0) + except Exception: + pass + + +def configure_a4(document: Any) -> None: + section = document.sections[0] + section.page_width = Cm(21) + section.page_height = Cm(29.7) + section.top_margin = Cm(2.54) + section.bottom_margin = Cm(2.54) + section.left_margin = Cm(3.17) + section.right_margin = Cm(2.54) + + +def configure_code_a4(document: Any) -> None: + section = document.sections[0] + section.page_width = Cm(21) + section.page_height = Cm(29.7) + section.top_margin = Cm(2.0) + section.bottom_margin = Cm(2.0) + section.left_margin = Cm(2.5) + section.right_margin = Cm(2.0) + + +def add_page_field(paragraph: Any) -> None: + begin = OxmlElement("w:fldChar") + begin.set(qn("w:fldCharType"), "begin") + instr = OxmlElement("w:instrText") + instr.set("{http://www.w3.org/XML/1998/namespace}space", "preserve") + instr.text = " PAGE " + separate = OxmlElement("w:fldChar") + separate.set(qn("w:fldCharType"), "separate") + result = OxmlElement("w:t") + result.text = "1" + end = OxmlElement("w:fldChar") + end.set(qn("w:fldCharType"), "end") + + for element in (begin, instr, separate, result, end): + run = paragraph.add_run() + run._r.append(element) + set_run_font(run, "SimSun", 8) + + +def set_code_header(document: Any, software_name: str, version: str) -> None: + section = document.sections[0] + section.header.is_linked_to_previous = False + header = section.header + header.paragraphs[0].text = "" if header.paragraphs else None + + # Build a two-column header: software name on the left, page number on the right. + table = header.add_table(rows=1, cols=2, width=Cm(17.5)) + table.autofit = True + left_cell = table.rows[0].cells[0] + right_cell = table.rows[0].cells[1] + + left_para = left_cell.paragraphs[0] + left_para.alignment = WD_ALIGN_PARAGRAPH.LEFT + left_para.paragraph_format.space_before = Pt(0) + left_para.paragraph_format.space_after = Pt(0) + left_para.paragraph_format.line_spacing_rule = WD_LINE_SPACING.EXACTLY + left_para.paragraph_format.line_spacing = Pt(12) + left_run = left_para.add_run(f"{software_name} {version}") + set_run_font(left_run, "SimSun", 8) + + right_para = right_cell.paragraphs[0] + right_para.alignment = WD_ALIGN_PARAGRAPH.RIGHT + right_para.paragraph_format.space_before = Pt(0) + right_para.paragraph_format.space_after = Pt(0) + right_para.paragraph_format.line_spacing_rule = WD_LINE_SPACING.EXACTLY + right_para.paragraph_format.line_spacing = Pt(12) + prefix = right_para.add_run("第 ") + set_run_font(prefix, "SimSun", 8) + add_page_field(right_para) + suffix = right_para.add_run(" 页") + set_run_font(suffix, "SimSun", 8) + + # Remove borders from the header table + for cell in table.rows[0].cells: + tc_pr = cell._tc.get_or_add_tcPr() + tc_borders = OxmlElement("w:tcBorders") + for border_name in ("top", "left", "bottom", "right"): + border = OxmlElement(f"w:{border_name}") + border.set(qn("w:val"), "nil") + tc_borders.append(border) + tc_pr.append(tc_borders) + + +def build_code_docx_python(md_path: Path, out_path: Path, software_name: str, version: str) -> None: + pages = parse_code_pages(md_path) + if not pages: + raise RuntimeError(f"No code pages parsed from {md_path}") + + document = Document() + configure_code_a4(document) + set_normal_font(document, "Consolas", 7.2) + set_style_black(document) + set_code_header(document, software_name, version) + + # 后30页文档页码从31开始,实现前后文档连续编号1-60 + start_page_no = pages[0][0] if pages else 1 + if start_page_no != 1: + pg_num_type = OxmlElement("w:pgNumType") + pg_num_type.set(qn("w:start"), str(start_page_no)) + document.sections[0]._sectPr.append(pg_num_type) + + for index, (page_no, lines) in enumerate(pages): + for line in lines: + p = document.add_paragraph() + p.paragraph_format.space_before = Pt(0) + p.paragraph_format.space_after = Pt(0) + p.paragraph_format.line_spacing_rule = WD_LINE_SPACING.EXACTLY + p.paragraph_format.line_spacing = Pt(14) + run = p.add_run(line if line else " ") + set_run_font(run, "Consolas", 7.2) + if index != len(pages) - 1: + # 嵌入式分页符:避免 add_page_break() 产生多余空段落导致空白页 + run = p.add_run() + br = OxmlElement('w:br') + br.set(qn('w:type'), 'page') + run._r.append(br) + + force_black_document(document) + document.save(out_path) + + +def paragraph_xml(text: str, font: str = "SimSun", size_half_points: int = 21, align: str | None = None, line_twips: int = 240) -> str: + align_xml = f'' if align else "" + escaped = html.escape(text) + return ( + "" + f"{align_xml}" + "" + f"" + f"" + f"" + f"{escaped}" + "" + "" + ) + + +def page_break_xml() -> str: + return '' + + +def page_field_runs_xml() -> str: + return ( + '' + f'' + '' + '' + f'' + ' PAGE ' + '' + f'' + '' + '' + f'' + '1' + '' + f'' + '' + ) + + +def header_xml(header_text: str) -> str: + """Build a two-column header: software name left, page number right.""" + escaped = html.escape(header_text) + # Use a borderless table for left/right alignment in header + return f""" + + + + + + + + + + + + + {escaped} + + + + + + + {page_field_runs_xml()} + + + + + +""" + + +def minimal_docx(out_path: Path, body_xml: str, header_text: str | None = None, start_page: int = 1) -> None: + content_types = """ + + + + + + +""" + rels = """ + + +""" + header_rel = ( + '' + if header_text + else "" + ) + doc_rels = f""" +{header_rel}""" + styles = f""" + + + + + +""" + document = f""" + + + {body_xml} + + {'' if header_text else ''} + {'' if start_page != 1 else ''} + + + + +""" + with zipfile.ZipFile(out_path, "w", compression=zipfile.ZIP_DEFLATED) as zf: + zf.writestr("[Content_Types].xml", content_types) + zf.writestr("_rels/.rels", rels) + zf.writestr("word/_rels/document.xml.rels", doc_rels) + zf.writestr("word/styles.xml", styles) + zf.writestr("word/document.xml", document) + if header_text: + zf.writestr("word/header1.xml", header_xml(header_text)) + + +def force_black_xml(xml: str) -> str: + xml = re.sub(r"]*>", "", xml) + xml = xml.replace("", "") + xml = re.sub(r"]*/>", f'', xml) + + def ensure_rpr_color(match: re.Match[str]) -> str: + value = match.group(0) + if "", f'') + + xml = re.sub(r"]*>.*?", ensure_rpr_color, xml, flags=re.S) + xml = re.sub(r"(?!)", f'', xml) + return xml + + +def normalize_docx_text_color(docx_path: Path) -> None: + tmp_path = docx_path.with_suffix(docx_path.suffix + ".tmp") + color_xml_parts = ( + "word/document.xml", + "word/styles.xml", + "word/numbering.xml", + "word/header", + "word/footer", + ) + with zipfile.ZipFile(docx_path, "r") as src, zipfile.ZipFile(tmp_path, "w", compression=zipfile.ZIP_DEFLATED) as dst: + for item in src.infolist(): + data = src.read(item.filename) + if item.filename.endswith(".xml") and item.filename.startswith(color_xml_parts): + text = data.decode("utf-8") + data = force_black_xml(text).encode("utf-8") + elif item.filename.endswith(".rels"): + text = data.decode("utf-8", errors="ignore") + if "hyperlink" in text: + text = re.sub(r'\s*]*Type="[^"]*/hyperlink"[^>]*/>', "", text) + data = text.encode("utf-8") + dst.writestr(item, data) + tmp_path.replace(docx_path) + + +def next_header_part(names: set[str]) -> tuple[str, str]: + index = 1 + while f"word/header{index}.xml" in names: + index += 1 + return f"word/header{index}.xml", f"header{index}.xml" + + +def unique_relationship_id(rels_xml: str, base: str = "rIdManualHeader") -> str: + if f'Id="{base}"' not in rels_xml: + return base + index = 2 + while f'Id="{base}{index}"' in rels_xml: + index += 1 + return f"{base}{index}" + + +def add_header_to_existing_docx(docx_path: Path, header_text: str) -> None: + """Add the same two-column header used by code materials to an existing DOCX.""" + tmp_path = docx_path.with_suffix(docx_path.suffix + ".tmp") + with zipfile.ZipFile(docx_path, "r") as src: + names = set(src.namelist()) + header_part, header_target = next_header_part(names) + rels_xml = src.read("word/_rels/document.xml.rels").decode("utf-8") + rel_id = unique_relationship_id(rels_xml) + + with zipfile.ZipFile(tmp_path, "w", compression=zipfile.ZIP_DEFLATED) as dst: + for item in src.infolist(): + data = src.read(item.filename) + if item.filename == "[Content_Types].xml": + text = data.decode("utf-8") + override = ( + f'' + ) + if f'PartName="/{header_part}"' not in text: + text = text.replace("", f"{override}") + data = text.encode("utf-8") + elif item.filename == "word/_rels/document.xml.rels": + text = data.decode("utf-8") + relationship = ( + f'' + ) + text = text.replace("", f"{relationship}") + data = text.encode("utf-8") + elif item.filename == "word/document.xml": + text = data.decode("utf-8") + if "xmlns:r=" not in text: + text = text.replace( + "' + if "]*/>", header_ref, text, count=1) + else: + text = re.sub(r"(]*>)", rf"\1{header_ref}", text, count=1) + data = text.encode("utf-8") + dst.writestr(item, data) + dst.writestr(header_part, header_xml(header_text)) + tmp_path.replace(docx_path) + + +def build_code_docx_ooxml(md_path: Path, out_path: Path, software_name: str, version: str) -> None: + pages = parse_code_pages(md_path) + if not pages: + raise RuntimeError(f"No code pages parsed from {md_path}") + start_page_no = pages[0][0] if pages else 1 + body: list[str] = [] + for index, (page_no, lines) in enumerate(pages): + for line in lines: + body.append(paragraph_xml(line if line else " ", font="Consolas", size_half_points=14, line_twips=280)) + if index != len(pages) - 1: + # 嵌入式分页符:嵌入最后一段的 run 避免多余空段落 + last = body.pop() + last = last.replace('', '') + body.append(last) + minimal_docx(out_path, "\n".join(body), header_text=f"{software_name} {version}", start_page=start_page_no) + + +def add_markdown_table(document: Any, rows: list[list[str]]) -> None: + if not rows: + return + table = document.add_table(rows=1, cols=len(rows[0])) + table.style = "Table Grid" + for idx, text in enumerate(rows[0]): + table.rows[0].cells[idx].text = strip_markdown_links(text) + for row in rows[1:]: + cells = table.add_row().cells + for idx, text in enumerate(row[: len(cells)]): + cells[idx].text = strip_markdown_links(text) + + +def parse_table_line(line: str) -> list[str]: + return [cell.strip() for cell in line.strip().strip("|").split("|")] + + +def add_image(document: Any, image_path: Path) -> None: + if not image_path.exists(): + p = document.add_paragraph() + run = p.add_run(f"[截图缺失:{image_path}]") + set_run_font(run, "SimSun", 10.5) + return + try: + document.add_picture(str(image_path), width=Inches(5.8)) + except Exception: + p = document.add_paragraph() + run = p.add_run(f"[截图无法插入:{image_path}]") + set_run_font(run, "SimSun", 10.5) + + +def build_manual_docx_python(md_path: Path, out_path: Path, base_dir: Path, software_name: str, version: str) -> None: + document = Document() + configure_a4(document) + set_normal_font(document, "SimSun", 10.5) + set_style_black(document) + set_code_header(document, software_name, version) + lines = md_path.read_text(encoding="utf-8").splitlines() + table_buf: list[list[str]] = [] + in_fence = False + + def flush_table() -> None: + nonlocal table_buf + if table_buf: + data = [row for row in table_buf if not all(re.fullmatch(r":?-{3,}:?", cell) for cell in row)] + add_markdown_table(document, data) + table_buf = [] + + for line in lines: + stripped = line.strip() + stripped = strip_markdown_links(stripped) + if stripped.startswith("```"): + flush_table() + in_fence = not in_fence + continue + if in_fence: + continue + if stripped.startswith("", "【截图预留:请在此处插入当前功能页面或操作结果截图。】", text) + text = strip_markdown_links(text) + if code_mode: + text = re.sub(r"(?=^##\s+第\s*\d+\s*页)", r"\n\\newpage\n", text, flags=re.M) + if code_mode or "STOP_FOR_USER" in original_text: + with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8") as tmp: + tmp.write(text) + tmp_name = tmp.name + source = Path(tmp_name) + try: + subprocess.run(["pandoc", "-f", "markdown", "-t", "docx", str(source), "-o", str(out_path)], check=True) + finally: + if tmp_name: + Path(tmp_name).unlink(missing_ok=True) + + +def build_code_docx(md_path: Path, out_path: Path, software_name: str, version: str) -> None: + if DOCX_AVAILABLE: + build_code_docx_python(md_path, out_path, software_name, version) + else: + build_code_docx_ooxml(md_path, out_path, software_name, version) + normalize_docx_text_color(out_path) + + +def build_manual_docx(md_path: Path, out_path: Path, base_dir: Path, software_name: str, version: str) -> None: + if DOCX_AVAILABLE: + build_manual_docx_python(md_path, out_path, base_dir, software_name, version) + else: + build_with_pandoc(md_path, out_path, code_mode=False) + add_header_to_existing_docx(out_path, f"{software_name} {version}") + normalize_docx_text_color(out_path) + + +def run_command(command: list[str], cwd: Path | None = None, timeout: int = 60) -> tuple[int, str]: + try: + completed = subprocess.run(command, cwd=cwd, text=True, capture_output=True, timeout=timeout) + return completed.returncode, (completed.stdout + completed.stderr).strip() + except Exception as exc: + return 99, str(exc) + + +def docx_checks(skill_dir: Path, outputs: list[Path]) -> list[str]: + notes: list[str] = [] + env_script = skill_dir / "vendor/docx-toolkit/scripts/env_check.sh" + preview_script = skill_dir / "vendor/docx-toolkit/scripts/docx_preview.sh" + if env_script.exists(): + code, output = run_command(["bash", str(env_script)], cwd=env_script.parent.parent, timeout=30) + status = "READY" if code == 0 else "NOT READY" + first_lines = "\n".join(output.splitlines()[:12]) + notes.append(f"DOCX env: {status}\n\n```text\n{first_lines}\n```") + else: + notes.append("DOCX env: vendor script missing") + + if preview_script.exists(): + for out in outputs: + code, output = run_command(["bash", str(preview_script), str(out)], timeout=45) + first_lines = "\n".join(output.splitlines()[:8]) + notes.append(f"Preview {out.name}: exit={code}\n\n```text\n{first_lines}\n```") + return notes + + +def build_all(workdir: Path, software_name: str, version: str, skip_preview: bool) -> dict[str, Any]: + workdir = ensure_dir(workdir) + draft_dir = workdir / "草稿" + final_dir = ensure_dir(workdir / "正式资料") + app_name = application_software_name(draft_dir) + app_version = application_version(draft_dir) + final_software_name = app_name or software_name + final_version = app_version or version + safe_name = safe_filename(final_software_name) + outputs: list[Path] = [] + warnings: list[str] = [] + if app_name and app_name != software_name: + warnings.append(f"命令参数软件名称为 {software_name},正式资料已按申请表信息软件名称 {app_name} 生成") + if app_version and app_version != version: + warnings.append(f"命令参数版本号为 {version},正式资料已按申请表信息版本号 {app_version} 生成") + screenshot_confirmation = read_json_if_exists(workdir / "截图方式确认.json") + screenshot_method = screenshot_confirmation.get("screenshot_method") + screenshot_manifest = workdir / "截图/截图清单.json" + if screenshot_method == "skip": + warnings.append("用户选择暂不截图;操作手册已保留截图预留位置") + elif screenshot_method and not screenshot_manifest.exists(): + warnings.append("操作手册截图未生成或未插入;操作手册应保留截图预留位置") + elif screenshot_manifest.exists(): + screenshots = read_json_if_exists(screenshot_manifest).get("screenshots") or [] + if not screenshots: + warnings.append("操作手册截图清单为空;操作手册应保留截图预留位置") + + app_txt, app_warnings = write_application_txt(draft_dir, final_dir) + if app_txt: + outputs.append(app_txt) + warnings.extend(app_warnings) + + code_specs = [ + ("代码-前30页.md", f"{safe_name}-代码(前30页).docx"), + ("代码-后30页.md", f"{safe_name}-代码(后30页).docx"), + ("代码-全部.md", f"{safe_name}-代码(全部).docx"), + ] + for md_name, docx_name in code_specs: + md_path = draft_dir / md_name + if md_path.exists(): + out_path = final_dir / docx_name + build_code_docx(md_path, out_path, final_software_name, final_version) + outputs.append(out_path) + + manual_md = draft_dir / "操作手册.md" + if manual_md.exists(): + manual_out = final_dir / f"{safe_name}_操作手册.docx" + manual_source = manual_md + tmp_manual: Path | None = None + if app_name and app_name != software_name: + text = manual_md.read_text(encoding="utf-8").replace(software_name, app_name) + with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8") as tmp: + tmp.write(text) + tmp_manual = Path(tmp.name) + manual_source = tmp_manual + try: + build_manual_docx(manual_source, manual_out, draft_dir, final_software_name, final_version) + finally: + if tmp_manual: + tmp_manual.unlink(missing_ok=True) + outputs.append(manual_out) + else: + warnings.append("缺少草稿/操作手册.md") + + skill_dir = Path(__file__).resolve().parents[1] + notes = [] if skip_preview else docx_checks(skill_dir, [p for p in outputs if p.suffix.lower() == ".docx"]) + report = write_report(final_dir, outputs, warnings, notes) + return {"outputs": [str(p) for p in outputs], "warnings": warnings, "report": str(report)} + + +def write_report(workdir: Path, outputs: list[Path], warnings: list[str], notes: list[str]) -> Path: + report = workdir / "生成报告.md" + lines = ["# 生成报告", "", "## 输出文件", ""] + for path in outputs: + size = path.stat().st_size if path.exists() else 0 + lines.append(f"- `{path.name}` ({size} bytes)") + lines.extend(["", "## 警告", ""]) + if warnings: + lines.extend(f"- {warning}" for warning in warnings) + else: + lines.append("- 无") + lines.extend(["", "## DOCX 校验", ""]) + if notes: + lines.extend(notes) + else: + lines.append("- 已跳过预览校验") + report.write_text("\n".join(lines) + "\n", encoding="utf-8") + return report + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--workdir", default="软件著作权申请资料") + parser.add_argument("--software-name", required=True) + parser.add_argument("--version", default="V1.0") + parser.add_argument("--skip-preview", action="store_true") + args = parser.parse_args() + + workdir = Path(args.workdir) + issues = confirmation_issues(workdir) + if issues: + print("STOP_FOR_USER") + print("NEXT_ACTION: 正式 Word/TXT 生成前必须完成以下确认:") + for issue in issues: + print(f"- {issue}") + raise SystemExit(2) + + result = build_all(workdir, args.software_name, args.version, args.skip_preview) + print(f"OK final materials: {Path(args.workdir) / '正式资料'}") + for output in result["outputs"]: + print(output) + if result["warnings"]: + print("Warnings:") + for warning in result["warnings"]: + print(f"- {warning}") + print(f"Report: {result['report']}") + + +if __name__ == "__main__": + main() diff --git a/software-copyright-materials/scripts/capture_screenshots.py b/software-copyright-materials/scripts/capture_screenshots.py new file mode 100644 index 0000000..4964e88 --- /dev/null +++ b/software-copyright-materials/scripts/capture_screenshots.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +"""Best-effort screenshot helpers for operation manuals.""" + +from __future__ import annotations + +import argparse +import json +import shutil +import re +from pathlib import Path +from urllib.parse import urljoin + +from common import ensure_dir, read_json, write_json + + +def safe_name(path: str) -> str: + value = path.strip("/") or "home" + value = re.sub(r"[^A-Za-z0-9._-]+", "_", value) + return value[:80] or "page" + + +def collect_manual_screenshots(input_dir: Path, out_dir: Path) -> dict[str, object]: + out_dir = ensure_dir(out_dir) + screenshots = [] + errors = [] + allowed = {".png", ".jpg", ".jpeg", ".webp"} + for index, path in enumerate(sorted(input_dir.iterdir()), start=1): + if path.suffix.lower() not in allowed or not path.is_file(): + continue + target = out_dir / f"{index:02d}-{safe_name(path.stem)}{path.suffix.lower()}" + if path.resolve() != target.resolve(): + shutil.copy2(path, target) + screenshots.append({"route": "", "url": "", "path": str(target), "source": str(path)}) + if not screenshots: + errors.append({"error": f"no screenshot images found in {input_dir}"}) + manifest = { + "status": "ok" if screenshots else "empty", + "method": "user-supplied", + "screenshots": screenshots, + "errors": errors, + } + write_json(out_dir / "截图清单.json", manifest) + return manifest + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--base-url") + parser.add_argument("--analysis") + parser.add_argument("--out-dir", default="软件著作权申请资料/截图") + parser.add_argument("--max-pages", type=int, default=8) + parser.add_argument("--manual-dir", help="Collect user-supplied screenshots from this directory") + args = parser.parse_args() + + if args.manual_dir: + manifest = collect_manual_screenshots(Path(args.manual_dir), Path(args.out_dir)) + print(json.dumps(manifest, ensure_ascii=False, indent=2)) + if not manifest["screenshots"]: + raise SystemExit(3) + return + + if not args.base_url or not args.analysis: + raise SystemExit("Missing --base-url and --analysis unless --manual-dir is provided") + + try: + from playwright.sync_api import sync_playwright + except Exception as exc: + print(json.dumps({"status": "error", "reason": f"playwright unavailable: {exc}"}, ensure_ascii=False)) + raise SystemExit(2) + + analysis = read_json(Path(args.analysis)) + paths = analysis.get("routes") or ["/"] + clean_paths = [] + for path in paths: + if isinstance(path, str) and path.startswith("/") and path not in clean_paths: + clean_paths.append(path) + clean_paths = clean_paths[: args.max_pages] or ["/"] + + out_dir = ensure_dir(Path(args.out_dir)) + screenshots = [] + errors = [] + with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page(viewport={"width": 1440, "height": 1000}) + for route in clean_paths: + url = urljoin(args.base_url.rstrip("/") + "/", route.lstrip("/")) + file_path = out_dir / f"{safe_name(route)}.png" + try: + page.goto(url, wait_until="networkidle", timeout=15_000) + page.screenshot(path=str(file_path), full_page=True) + screenshots.append({"route": route, "url": url, "path": str(file_path)}) + except Exception as exc: + errors.append({"route": route, "url": url, "error": str(exc)}) + browser.close() + + manifest = {"status": "ok" if screenshots else "partial", "screenshots": screenshots, "errors": errors} + write_json(out_dir / "截图清单.json", manifest) + print(json.dumps(manifest, ensure_ascii=False, indent=2)) + if not screenshots: + raise SystemExit(3) + + +if __name__ == "__main__": + main() diff --git a/software-copyright-materials/scripts/check_environment.py b/software-copyright-materials/scripts/check_environment.py new file mode 100644 index 0000000..4a6f8a0 --- /dev/null +++ b/software-copyright-materials/scripts/check_environment.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""Check runtime capabilities at the beginning of the workflow.""" + +from __future__ import annotations + +import argparse +import importlib.util +import shutil +import subprocess +from pathlib import Path +from typing import Any + +from common import ensure_dir, write_json + + +def command_version(command: list[str]) -> tuple[bool, str]: + if not shutil.which(command[0]): + return False, "not found" + try: + completed = subprocess.run(command, text=True, capture_output=True, timeout=20) + output = (completed.stdout or completed.stderr).strip().splitlines() + return completed.returncode == 0, output[0] if output else "available" + except Exception as exc: + return False, str(exc) + + +def run_docx_env(skill_dir: Path) -> tuple[bool, str]: + env_script = skill_dir / "vendor/docx-toolkit/scripts/env_check.sh" + if not env_script.exists(): + return False, "vendor/docx-toolkit/scripts/env_check.sh not found" + try: + completed = subprocess.run(["bash", str(env_script)], text=True, capture_output=True, timeout=40) + return completed.returncode == 0, (completed.stdout + completed.stderr).strip() + except Exception as exc: + return False, str(exc) + + +def module_available(name: str) -> bool: + return importlib.util.find_spec(name) is not None + + +def check_environment(skill_dir: Path) -> dict[str, Any]: + python_docx = module_available("docx") + pandoc_ok, pandoc_version = command_version(["pandoc", "--version"]) + dotnet_ok, dotnet_version = command_version(["dotnet", "--version"]) + docx_ready, docx_output = run_docx_env(skill_dir) + + final_docx_mode = "docx-openxml" if docx_ready else ("python-docx" if python_docx else "basic-ooxml") + requires_user_input = not docx_ready + next_action = ( + "请选择:1) 安装完整 DOCX 环境;2) 使用基础 DOCX 兜底继续。回复选择后再进入项目分析。" + if requires_user_input + else "完整 DOCX 环境可用,可以进入项目分析。" + ) + return { + "output_directory": "当前目录/软件著作权申请资料", + "capabilities": { + "markdown_drafts": True, + "application_txt": True, + "basic_docx": python_docx or True, + "python_docx": python_docx, + "pandoc_preview": pandoc_ok, + "docx_openxml_full": docx_ready, + "dotnet_sdk": dotnet_ok, + }, + "versions": { + "pandoc": pandoc_version, + "dotnet": dotnet_version, + }, + "final_docx_mode": final_docx_mode, + "recommendation": ( + "完整 DOCX OpenXML 环境已就绪,建议使用完整 Word 生成和校验流程。" + if docx_ready + else "完整 DOCX OpenXML 环境未就绪。可以继续使用兜底 DOCX 生成;如需更规范的 Word 结构和校验,请先安装 .NET SDK 并运行 vendor/docx-toolkit/scripts/setup.sh。" + ), + "install_prompt": ( + "是否安装完整 DOCX 环境?安装后文档生成和校验更规范;不安装也可以继续生成 Markdown、TXT 和基础 DOCX。" + if not docx_ready + else "无需安装,完整环境可用。" + ), + "requires_user_input": requires_user_input, + "confirmation_stage": "environment" if requires_user_input else None, + "next_action": next_action, + "docx_env_output": docx_output, + } + + +def write_markdown(path: Path, data: dict[str, Any]) -> None: + caps = data["capabilities"] + lines = [ + "# 软著申请资料生成环境检查", + "", + f"- 输出目录:`{data['output_directory']}`", + f"- 最终 Word 模式:`{data['final_docx_mode']}`", + "", + "## 能力状态", + "", + f"- Markdown 草稿:{'可用' if caps['markdown_drafts'] else '不可用'}", + f"- 申请表 TXT:{'可用' if caps['application_txt'] else '不可用'}", + f"- 基础 DOCX 生成:{'可用' if caps['basic_docx'] else '不可用'}", + f"- python-docx:{'可用' if caps['python_docx'] else '不可用'}", + f"- pandoc 预览:{'可用' if caps['pandoc_preview'] else '不可用'}({data['versions']['pandoc']})", + f"- .NET SDK:{'可用' if caps['dotnet_sdk'] else '不可用'}({data['versions']['dotnet']})", + f"- DOCX OpenXML 完整环境:{'可用' if caps['docx_openxml_full'] else '不可用'}", + "", + "## 建议", + "", + data["recommendation"], + "", + "## 用户选择", + "", + data["install_prompt"], + "", + "如果完整 DOCX 环境不可用,必须先等待用户选择,并记录 `environment` 门禁后再继续。", + "", + "```text" if data.get("requires_user_input") else "", + "STOP_FOR_USER" if data.get("requires_user_input") else "", + f"NEXT_ACTION: {data['next_action']}" if data.get("requires_user_input") else "", + "```" if data.get("requires_user_input") else "", + "", + "## DOCX 环境输出摘要", + "", + "```text", + "\n".join(data["docx_env_output"].splitlines()[:40]), + "```", + "", + ] + path.write_text("\n".join(lines), encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--out-dir", default="软件著作权申请资料") + args = parser.parse_args() + + skill_dir = Path(__file__).resolve().parents[1] + out_dir = ensure_dir(Path(args.out_dir)) + data = check_environment(skill_dir) + write_json(out_dir / "环境检查.json", data) + write_markdown(out_dir / "环境检查.md", data) + print(f"OK environment check: {out_dir}") + print(f"Final DOCX mode: {data['final_docx_mode']}") + print(data["recommendation"]) + if data.get("requires_user_input"): + print("STOP_FOR_USER") + print(f"NEXT_ACTION: {data['next_action']}") + + +if __name__ == "__main__": + main() diff --git a/software-copyright-materials/scripts/common.py b/software-copyright-materials/scripts/common.py new file mode 100644 index 0000000..7c3caa8 --- /dev/null +++ b/software-copyright-materials/scripts/common.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +"""Shared helpers for the software copyright materials skill.""" + +from __future__ import annotations + +import json +import os +import re +from pathlib import Path +from typing import Any, Iterable + + +EXCLUDE_DIRS = { + ".git", + ".hg", + ".svn", + ".idea", + ".vscode", + "__pycache__", + "node_modules", + "dist", + "build", + ".next", + ".nuxt", + ".output", + "coverage", + "target", + "vendor", + "软件著作权申请资料", + "software-copyright-materials", +} + +CODE_EXTS = { + ".vue", + ".ts", + ".tsx", + ".js", + ".jsx", + ".mjs", + ".cjs", + ".css", + ".scss", + ".sass", + ".less", + ".html", + ".svelte", + ".astro", + ".json", + ".md", +} + +KNOWN_CONFIG_FILES = { + ".babelrc", + ".eslintrc", + ".eslintrc.json", + ".eslintrc.yaml", + ".eslintrc.yml", + ".prettierrc", + ".prettierrc.json", + ".prettierrc.yaml", + ".prettierrc.yml", + ".swcrc", + "angular.json", + "app.json", + "astro.config.mjs", + "astro.config.ts", + "babel.config.js", + "babel.config.json", + "Cargo.lock", + "Cargo.toml", + "composer.json", + "docker-compose.yaml", + "docker-compose.yml", + "eslint.config.cjs", + "eslint.config.js", + "eslint.config.mjs", + "go.mod", + "go.sum", + "jsconfig.json", + "lerna.json", + "manifest.json", + "next.config.js", + "next.config.mjs", + "next.config.ts", + "nuxt.config.js", + "nuxt.config.ts", + "nx.json", + "package-lock.json", + "package.json", + "playwright.config.js", + "playwright.config.ts", + "postcss.config.cjs", + "postcss.config.js", + "prettier.config.cjs", + "prettier.config.js", + "prettier.config.mjs", + "project.json", + "pyproject.toml", + "rollup.config.js", + "rollup.config.mjs", + "rollup.config.ts", + "svelte.config.js", + "stylelintrc.json", + "tailwind.config.js", + "tailwind.config.ts", + "tsconfig.app.json", + "tsconfig.json", + "tsconfig.node.json", + "tslint.json", + "turbo.json", + "vite.config.js", + "vite.config.mjs", + "vite.config.ts", + "vitest.config.js", + "vitest.config.ts", + "webpack.config.js", + "webpack.config.ts", + "workspace.json", +} + +FRONTEND_EXTS = { + ".vue", + ".ts", + ".tsx", + ".js", + ".jsx", + ".mjs", + ".css", + ".scss", + ".sass", + ".less", + ".html", + ".svelte", + ".astro", +} + +SUPPLEMENT_CODE_EXTS = { + ".py", + ".java", + ".go", + ".rs", + ".cs", + ".php", + ".rb", + ".kt", + ".swift", + ".sql", + ".sh", + ".json", +} + +COPYRIGHT_CODE_EXTS = FRONTEND_EXTS | SUPPLEMENT_CODE_EXTS + +LOCK_FILES = { + "package-lock.json", + "pnpm-lock.yaml", + "yarn.lock", + "bun.lockb", + "bun.lock", +} + + +def repo_root_from_script() -> Path: + return Path(__file__).resolve().parents[3] + + +def is_excluded(path: Path) -> bool: + parts = set(path.parts) + if parts & EXCLUDE_DIRS: + return True + name = path.name + if name.startswith(".") and name not in {".env.example"}: + return True + if name in LOCK_FILES: + return True + if name.endswith(".map") or name.endswith(".min.js") or name.endswith(".min.css"): + return True + return False + + +def iter_project_files(project: Path, exts: set[str] | None = None) -> Iterable[Path]: + project = project.resolve() + for root, dirs, files in os.walk(project): + root_path = Path(root) + dirs[:] = [d for d in dirs if not is_excluded(root_path / d)] + for filename in files: + path = root_path / filename + if is_excluded(path): + continue + if exts is not None and path.suffix.lower() not in exts: + continue + yield path + + +def rel(path: Path, root: Path) -> str: + return path.resolve().relative_to(root.resolve()).as_posix() + + +def read_text(path: Path, limit: int | None = None) -> str: + data = path.read_bytes() + if limit is not None: + data = data[:limit] + for encoding in ("utf-8", "utf-8-sig", "gb18030", "latin-1"): + try: + return data.decode(encoding) + except UnicodeDecodeError: + continue + return data.decode("utf-8", errors="replace") + + +def read_json(path: Path) -> dict[str, Any]: + return json.loads(read_text(path)) + + +def write_json(path: Path, data: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") + + +def count_text_lines(path: Path, skip_blank: bool = True) -> int: + try: + text = read_text(path) + except Exception: + return 0 + if not text: + return 0 + if skip_blank: + return sum(1 for line in text.splitlines() if line.strip()) + return len(text.splitlines()) + + +def is_known_config_file(path: Path) -> bool: + """Return True for well-known config files that shouldn't count as source code.""" + return path.name in KNOWN_CONFIG_FILES + + +def looks_binary(path: Path) -> bool: + try: + chunk = path.read_bytes()[:4096] + except Exception: + return True + return b"\x00" in chunk + + +def normalize_title(value: str) -> str: + value = re.sub(r"[-_]+", " ", value).strip() + value = re.sub(r"\s+", " ", value) + return value or "待命名软件" + + +def safe_filename(value: str) -> str: + value = re.sub(r'[\\/:*?"<>|]+', "_", value).strip() + return value or "软件" + + +def ensure_dir(path: Path) -> Path: + path.mkdir(parents=True, exist_ok=True) + return path diff --git a/software-copyright-materials/scripts/confirm_stage.py b/software-copyright-materials/scripts/confirm_stage.py new file mode 100644 index 0000000..ce582a5 --- /dev/null +++ b/software-copyright-materials/scripts/confirm_stage.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +"""Record explicit user confirmations for gated workflow stages.""" + +from __future__ import annotations + +import argparse +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +from common import read_json, write_json + + +MAIN_FUNCTION_MIN_CHARS = 500 +MAIN_FUNCTION_MAX_CHARS = 1300 + + +def timestamp() -> str: + return datetime.now(timezone.utc).isoformat(timespec="seconds") + + +def load_json_or_empty(path: Path) -> dict[str, Any]: + if not path.exists(): + return {} + return read_json(path) + + +def write_confirmation(path: Path, data: dict[str, Any], key: str, note: str) -> None: + data[key] = True + data["confirmation_note"] = note + data["confirmed_at"] = timestamp() + write_json(path, data) + + +def pending_application_fields(md_path: Path) -> list[str]: + if not md_path.exists(): + return [f"缺少 {md_path}"] + return [line.strip() for line in md_path.read_text(encoding="utf-8").splitlines() if "待用户确认" in line] + + +def effective_len(value: str) -> int: + return len(str(value or "").replace(" ", "").replace("\n", "")) + + +def application_field_value(md_path: Path, field_name: str) -> str: + prefix = f"➤{field_name}:" + for line in md_path.read_text(encoding="utf-8").splitlines(): + if line.startswith(prefix): + return line[len(prefix) :].strip() + return "" + + +def application_field_issues(md_path: Path) -> list[str]: + issues = pending_application_fields(md_path) + if not md_path.exists(): + return issues + + main_function = application_field_value(md_path, "软件的主要功能") + if not main_function: + issues.append("➤软件的主要功能:缺少填写内容,请填写 500~1300 字。") + elif "待用户确认" not in main_function: + count = effective_len(main_function) + if count < MAIN_FUNCTION_MIN_CHARS: + issues.append( + f"➤软件的主要功能:当前 {count} 字,少于 {MAIN_FUNCTION_MIN_CHARS} 字,请扩写至 500~1300 字。" + ) + elif count > MAIN_FUNCTION_MAX_CHARS: + issues.append( + f"➤软件的主要功能:当前 {count} 字,超过 {MAIN_FUNCTION_MAX_CHARS} 字,请精简至 500~1300 字。" + ) + return issues + + +def confirm_environment(workdir: Path, note: str) -> Path: + out_path = workdir / "环境确认.json" + data = load_json_or_empty(out_path) + write_confirmation(out_path, data, "environment_confirmed", note) + return out_path + + +def confirm_project(workdir: Path, note: str) -> Path: + out_path = workdir / "项目确认.json" + data = load_json_or_empty(out_path) + write_confirmation(out_path, data, "project_confirmed", note) + return out_path + + +def confirm_business(workdir: Path, note: str) -> Path: + path = workdir / "草稿/业务理解.json" + if not path.exists(): + raise SystemExit("Missing 草稿/业务理解.json") + data = read_json(path) + write_confirmation(path, data, "user_confirmed", note) + return path + + +def confirm_code_selection(workdir: Path, note: str) -> Path: + path = workdir / "草稿/代码文件选择.json" + if not path.exists(): + raise SystemExit("Missing 草稿/代码文件选择.json") + data = read_json(path) + files = data.get("files") if isinstance(data, dict) else [] + selected = [item for item in files if isinstance(item, dict) and item.get("selected")] + if not selected: + raise SystemExit( + "STOP_FOR_USER\n" + "NEXT_ACTION: 代码文件选择尚未由模型填写。请先选择至少一个源码文件并填写选择理由,再让用户确认。" + ) + missing_reason = [item.get("path") for item in selected if not str(item.get("model_reason") or "").strip()] + if data.get("model_selection_required") and missing_reason: + raise SystemExit( + "STOP_FOR_USER\n" + "NEXT_ACTION: 已选源码缺少模型选择理由,请补全 `model_reason` 后再确认。\n" + + "\n".join(f"- {item}" for item in missing_reason[:20]) + ) + write_confirmation(path, data, "user_confirmed", note) + return path + + +def parse_screenshot_method(method: str, note: str) -> str: + value = (method or note or "").lower() + if any(key in value for key in ("skip", "no-screenshot", "none", "不截图", "跳过", "暂不", "先不", "不要截图", "无需截图")): + return "skip" + if any(key in value for key in ("chrome", "devtools", "mcp")): + return "chrome-devtools" + if any(key in value for key in ("computer", "use", "电脑", "桌面")): + return "computer-use" + if any(key in value for key in ("user", "manual", "self", "手动", "自己", "用户")): + return "user-supplied" + raise SystemExit( + "STOP_FOR_USER\n" + "NEXT_ACTION: 请明确截图方式:chrome-devtools、computer-use、user-supplied 或 skip。" + ) + + +def confirm_screenshot_method(workdir: Path, note: str, method: str) -> Path: + selected = parse_screenshot_method(method, note) + out_path = workdir / "截图方式确认.json" + data = load_json_or_empty(out_path) + data["screenshot_method"] = selected + write_confirmation(out_path, data, "screenshot_method_confirmed", note) + return out_path + + +def confirm_application_fields(workdir: Path, note: str) -> Path: + issues = application_field_issues(workdir / "草稿/申请表信息.md") + if issues: + raise SystemExit( + "STOP_FOR_USER\n" + "NEXT_ACTION: 申请表信息仍有字段未满足要求。请先补全或修正字段,再重新确认。\n" + + "\n".join(f"- {item}" for item in issues[:20]) + ) + out_path = workdir / "草稿/申请表字段确认.json" + data = load_json_or_empty(out_path) + write_confirmation(out_path, data, "application_fields_confirmed", note) + return out_path + + +def confirm_markdown(workdir: Path, note: str) -> Path: + issues = [] + business = workdir / "草稿/业务理解.json" + selection = workdir / "草稿/代码文件选择.json" + screenshot = workdir / "截图方式确认.json" + fields = workdir / "草稿/申请表字段确认.json" + + if not business.exists() or not read_json(business).get("user_confirmed"): + issues.append("业务理解尚未确认") + if not selection.exists() or not read_json(selection).get("user_confirmed"): + issues.append("代码文件选择尚未确认") + if not screenshot.exists() or not read_json(screenshot).get("screenshot_method_confirmed"): + issues.append("截图方式尚未确认") + if not fields.exists() or not read_json(fields).get("application_fields_confirmed"): + issues.append("申请表字段尚未确认") + field_issues = application_field_issues(workdir / "草稿/申请表信息.md") + if field_issues: + issues.append("申请表信息仍有字段未满足要求") + + if issues: + raise SystemExit( + "STOP_FOR_USER\n" + "NEXT_ACTION: Markdown 草稿确认前需要先处理以下事项:\n" + + "\n".join(f"- {item}" for item in issues) + ) + + out_path = workdir / "草稿/最终生成确认.json" + data = load_json_or_empty(out_path) + write_confirmation(out_path, data, "markdown_confirmed", note) + return out_path + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--workdir", default="软件著作权申请资料") + parser.add_argument( + "--stage", + required=True, + choices=[ + "environment", + "project", + "business", + "code-selection", + "screenshot-method", + "application-fields", + "markdown", + ], + ) + parser.add_argument("--note", default="用户已确认") + parser.add_argument( + "--method", + choices=["chrome-devtools", "computer-use", "user-supplied", "skip"], + help="Screenshot capture method when --stage screenshot-method", + ) + args = parser.parse_args() + + workdir = Path(args.workdir) + if args.stage == "environment": + path = confirm_environment(workdir, args.note) + elif args.stage == "project": + path = confirm_project(workdir, args.note) + elif args.stage == "business": + path = confirm_business(workdir, args.note) + elif args.stage == "code-selection": + path = confirm_code_selection(workdir, args.note) + elif args.stage == "screenshot-method": + path = confirm_screenshot_method(workdir, args.note, args.method or "") + elif args.stage == "application-fields": + path = confirm_application_fields(workdir, args.note) + else: + path = confirm_markdown(workdir, args.note) + + print(f"OK confirmation recorded: {args.stage}") + print(path) + + +if __name__ == "__main__": + main() diff --git a/software-copyright-materials/scripts/extract_code_material.py b/software-copyright-materials/scripts/extract_code_material.py new file mode 100644 index 0000000..09f9b91 --- /dev/null +++ b/software-copyright-materials/scripts/extract_code_material.py @@ -0,0 +1,314 @@ +#!/usr/bin/env python3 +"""Extract real source code and create Markdown draft pages.""" + +from __future__ import annotations + +import argparse +from pathlib import Path +from typing import Any + +from common import COPYRIGHT_CODE_EXTS, FRONTEND_EXTS, ensure_dir, is_known_config_file, iter_project_files, looks_binary, read_json, read_text, rel, safe_filename, write_json + + +LINES_PER_PAGE = 50 +SPLIT_THRESHOLD_PAGES = 60 + + +def category_weight(path: Path, project: Path) -> tuple[int, str]: + r = rel(path, project).lower() + name = path.name.lower() + priority = 80 + if name in {"main.ts", "main.js", "main.tsx", "main.jsx", "app.vue", "app.tsx"} or r in { + "src/app/page.tsx", + "src/app/layout.tsx", + "app/page.tsx", + "app/layout.tsx", + } or r.endswith("/src/app/page.tsx") or r.endswith("/src/app/layout.tsx"): + priority = 0 + elif path.suffix.lower() in {".css", ".scss", ".sass", ".less"}: + priority = 90 + elif "/router/" in r or "/routes/" in r or "router." in r or "routes." in r: + priority = 10 + elif "/pages/" in r or "/views/" in r or "/app/" in r or "/screens/" in r: + priority = 20 + elif "/api/" in r or "/apis/" in r or "/services/" in r or "request." in r: + priority = 30 + elif "/store/" in r or "/stores/" in r or "/pinia/" in r or "/redux/" in r: + priority = 40 + elif "/components/" in r: + priority = 50 + elif "/utils/" in r or "/lib/" in r or "/hooks/" in r or "/composables/" in r: + priority = 60 + elif path.suffix.lower() not in FRONTEND_EXTS: + if any(part in r for part in ("/backend/app/", "/server/", "/api/", "/services/", "/models/", "/schemas/", "/workers/")): + priority = 70 + elif name in {"docker-compose.yml", "docker-compose.yaml", "pyproject.toml"} or path.suffix.lower() in {".toml", ".yml", ".yaml"}: + priority = 95 + else: + priority = 100 + return priority, r + + +def should_skip_file(path: Path) -> bool: + if path.suffix.lower() not in COPYRIGHT_CODE_EXTS: + return True + if is_known_config_file(path): + return True + if looks_binary(path): + return True + try: + size = path.stat().st_size + except OSError: + return True + if size <= 0 or size > 800_000: + return True + try: + sample = read_text(path, limit=20_000) + except Exception: + return True + lines = sample.splitlines() + if any(len(line) > 3000 for line in lines[:80]): + return True + return False + + +def selected_line_estimate(item: dict[str, Any]) -> int: + try: + total = int(item.get("material_line_count") or item.get("line_count") or 0) + except (TypeError, ValueError): + total = 0 + return total + 1 if total > 0 else 0 + + +def available_pages_from_selection(selection_path: Path | None, lines_per_page: int) -> tuple[int, int, int]: + if selection_path is None or not selection_path.exists(): + return 0, 0, 0 + data = read_json(selection_path) + items = data.get("files") if isinstance(data, dict) else [] + if not isinstance(items, list): + return 0, 0, 0 + available_lines = sum(selected_line_estimate(item) for item in items if isinstance(item, dict)) + unselected = sum(1 for item in items if isinstance(item, dict) and not item.get("selected") and selected_line_estimate(item) > 0) + pages = (available_lines + lines_per_page - 1) // lines_per_page if available_lines else 0 + return available_lines, pages, unselected + + +def marker_for(path: Path, project: Path) -> str: + return f"// File: {rel(path, project)}" + + +def material_code_lines(text: str) -> list[str]: + return [line for line in text.splitlines() if line.strip()] + + +def load_selected_files(project: Path, selection_path: Path | None) -> list[dict[str, Any]]: + if selection_path is None: + raise SystemExit( + "STOP_FOR_USER\n" + "NEXT_ACTION: 代码抽取必须先使用 propose_code_selection.py 生成并确认 草稿/代码文件选择.json。" + ) + + data = read_json(selection_path) + if isinstance(data, dict) and data.get("selection_required") and not data.get("user_confirmed"): + raise SystemExit( + "STOP_FOR_USER\n" + "NEXT_ACTION: 代码文件选择尚未确认。请先确认或修改 草稿/代码文件选择.json," + "再运行 `python3 /scripts/confirm_stage.py --workdir 软件著作权申请资料 --stage code-selection --note \"<用户确认内容>\"`。" + ) + items = data.get("files") if isinstance(data, dict) else data + if not isinstance(items, list): + raise SystemExit(f"Invalid selection file: {selection_path}") + + selected = [] + for item in items: + if not isinstance(item, dict) or not item.get("selected"): + continue + path_value = item.get("path") + if not path_value: + continue + selected.append( + { + "path": str(path_value), + "selected": True, + } + ) + return selected + + +def collect_code_lines(project: Path, selection_path: Path | None) -> tuple[list[str], list[dict[str, Any]]]: + selected_items = load_selected_files(project, selection_path) + all_lines: list[str] = [] + manifest_files: list[dict[str, Any]] = [] + + for item in selected_items: + path = (project / item["path"]).resolve() + try: + path.relative_to(project.resolve()) + except ValueError: + raise SystemExit(f"Selected file is outside project: {path}") + if should_skip_file(path): + continue + text = read_text(path) + source_lines = text.splitlines() + selected_lines = material_code_lines(text) + if not selected_lines: + continue + start = len(all_lines) + 1 + marker = marker_for(path, project) + all_lines.append(marker) + all_lines.extend(selected_lines) + end = len(all_lines) + source_end_line = len(source_lines) + manifest_files.append( + { + "path": rel(path, project), + "source_line_count": len(source_lines), + "blank_line_count": len(source_lines) - len(selected_lines), + "selected_line_start": 1, + "selected_line_end": source_end_line, + "selected_line_count": len(selected_lines), + "material_line_start": start, + "material_line_end": end, + } + ) + return all_lines, manifest_files + + +def paginate(lines: list[str], lines_per_page: int) -> list[list[str]]: + return [lines[i : i + lines_per_page] for i in range(0, len(lines), lines_per_page)] + + +def write_pages_md(path: Path, title: str, software_name: str, version: str, pages: list[tuple[int, list[str]]]) -> None: + chunks = [f"# {title}", "", f"软件名称:{software_name}", f"版本号:{version}", ""] + for page_no, page_lines in pages: + chunks.extend([f"## 第 {page_no} 页", "", "```text"]) + chunks.extend(page_lines) + chunks.extend(["```", ""]) + path.write_text("\n".join(chunks), encoding="utf-8") + + +def write_manifest_md(path: Path, manifest: dict[str, Any]) -> None: + lines = [ + "# 代码提取清单", + "", + f"- 软件名称:{manifest['software_name']}", + f"- 版本号:{manifest['version']}", + f"- 项目目录:{manifest['project_root']}", + f"- 源码文件数:{manifest['file_count']}", + f"- 材料代码行数:{manifest['material_line_count']}", + f"- 每页行数:{manifest['lines_per_page']}", + f"- 总页数:{manifest['total_pages']}", + f"- 目标页数:{manifest['target_pages']}", + f"- 候选源码可生成页数:{manifest['available_candidate_pages']}", + f"- 补充状态:{manifest['supplement_status']}", + f"- 输出模式:{manifest['mode']}", + "", + "## 文件来源", + "", + "| 文件 | 源码行数 | 抽取源码范围 | 抽取行数 | 材料行范围 |", + "| --- | ---: | --- | ---: | --- |", + ] + for item in manifest["files"]: + lines.append( + f"| `{item['path']}` | {item['source_line_count']} | " + f"{item['selected_line_start']}-{item['selected_line_end']} | " + f"{item['selected_line_count']} | " + f"{item['material_line_start']}-{item['material_line_end']} |" + ) + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def extract(project: Path, out_dir: Path, software_name: str, version: str, lines_per_page: int, selection_path: Path | None) -> dict[str, Any]: + ensure_dir(out_dir) + code_lines, files = collect_code_lines(project, selection_path) + if not code_lines: + raise SystemExit("No selected frontend source code files found for extraction.") + + pages = paginate(code_lines, lines_per_page) + total_pages = len(pages) + available_lines, available_pages, unselected_count = available_pages_from_selection(selection_path, lines_per_page) + if total_pages < SPLIT_THRESHOLD_PAGES and available_pages >= SPLIT_THRESHOLD_PAGES and unselected_count > 0: + raise SystemExit( + "STOP_FOR_USER\n" + f"NEXT_ACTION: 当前已选代码只有 {total_pages} 页,但候选源码足够补齐到 {SPLIT_THRESHOLD_PAGES} 页。" + "请在 草稿/代码文件选择.json 中继续选择补充文件,重新记录 code-selection 门禁后再抽取。" + ) + outputs: list[str] = [] + + if total_pages >= SPLIT_THRESHOLD_PAGES: + front = list(enumerate(pages[:30], start=1)) + back = [(31 + i, page) for i, page in enumerate(pages[-30:])] + front_path = out_dir / "代码-前30页.md" + back_path = out_dir / "代码-后30页.md" + write_pages_md(front_path, "代码材料(前30页)", software_name, version, front) + write_pages_md(back_path, "代码材料(后30页)", software_name, version, back) + outputs.extend([front_path.name, back_path.name]) + mode = "front30_back30" + else: + all_path = out_dir / "代码-全部.md" + all_pages = list(enumerate(pages, start=1)) + write_pages_md(all_path, "代码材料(全部)", software_name, version, all_pages) + outputs.append(all_path.name) + mode = "all_under_60_pages" + supplement_status = ( + "候选源码可达到前30页/后30页要求" + if available_pages >= SPLIT_THRESHOLD_PAGES + else "候选源码不足60页,按全部代码材料生成" + ) + + manifest = { + "software_name": software_name, + "version": version, + "project_root": str(project.resolve()), + "file_count": len(files), + "material_line_count": len(code_lines), + "source_line_count": sum(item["source_line_count"] for item in files), + "selected_source_line_count": sum(item["selected_line_count"] for item in files), + "lines_per_page": lines_per_page, + "total_pages": total_pages, + "target_pages": SPLIT_THRESHOLD_PAGES, + "available_candidate_line_count": available_lines, + "available_candidate_pages": available_pages, + "supplement_status": supplement_status, + "mode": mode, + "selection_file": str(selection_path) if selection_path else None, + "outputs": outputs, + "files": files, + "safe_software_filename": safe_filename(software_name), + } + write_json(out_dir / "代码提取清单.json", manifest) + write_manifest_md(out_dir / "代码提取清单.md", manifest) + return manifest + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--project", required=True) + parser.add_argument("--analysis", help="Optional project analysis JSON; retained for workflow traceability") + parser.add_argument("--software-name", required=True) + parser.add_argument("--version", default="V1.0") + parser.add_argument("--out-dir", default="软件著作权申请资料/草稿") + parser.add_argument("--lines-per-page", type=int, default=LINES_PER_PAGE) + parser.add_argument("--selection", help="Editable JSON file created by propose_code_selection.py") + args = parser.parse_args() + + project = Path(args.project) + if not project.exists(): + raise SystemExit(f"Project not found: {project}") + if args.analysis and not Path(args.analysis).exists(): + raise SystemExit(f"Analysis JSON not found: {args.analysis}") + + selection = Path(args.selection) if args.selection else None + if selection and not selection.exists(): + raise SystemExit(f"Selection JSON not found: {selection}") + + manifest = extract(project, Path(args.out_dir), args.software_name, args.version, args.lines_per_page, selection) + print(f"OK code drafts: {args.out_dir}") + print(f"Selected files: {manifest['file_count']}") + print(f"Mode: {manifest['mode']}") + print(f"Total pages: {manifest['total_pages']}") + print(f"Outputs: {', '.join(manifest['outputs'])}") + + +if __name__ == "__main__": + main() diff --git a/software-copyright-materials/scripts/generate_application_info.py b/software-copyright-materials/scripts/generate_application_info.py new file mode 100644 index 0000000..2dfee87 --- /dev/null +++ b/software-copyright-materials/scripts/generate_application_info.py @@ -0,0 +1,719 @@ +#!/usr/bin/env python3 +"""Generate the Markdown draft for application form information.""" + +from __future__ import annotations + +import argparse +import os +import platform +import re +import shutil +from pathlib import Path +from typing import Any + +from common import ensure_dir, read_json, read_text + + +MIN_MAIN_FUNCTION_CHARS = 500 +MAX_MAIN_FUNCTION_CHARS = 1300 + + +FIELD_ORDER = [ + "软件全称", + "软件简称", + "版本号", + "软件分类", + "开发完成日期", + "开发方式", + "软件说明", + "发表状态", + "首次发表日期", + "著作权人", + "权利范围", + "权利取得方式", + "开发的硬件环境", + "运行的硬件环境", + "开发该软件的操作系统", + "软件开发环境 / 开发工具", + "该软件的运行平台 / 操作系统", + "软件运行支撑环境 / 支持软件", + "编程语言", + "源程序量", + "开发目的", + "面向领域 / 行业", + "软件的主要功能", + "软件的技术特点", + "页数", +] + + +def effective_len(value: str) -> int: + return len(str(value or "").replace(" ", "").replace("\n", "")) + + +def clean_text(value: Any) -> str: + text = str(value or "").strip() + text = re.sub(r"\s+", " ", text) + text = text.replace("`", "").replace("#", "").strip() + return text + + +def trim_effective(value: str, max_chars: int = MAX_MAIN_FUNCTION_CHARS) -> str: + if effective_len(value) <= max_chars: + return value + result: list[str] = [] + count = 0 + for char in value: + if char not in (" ", "\n"): + count += 1 + if count > max_chars: + break + result.append(char) + return "".join(result).rstrip(",。;、 ") + "。" + + +def business_feature_pairs(business: dict[str, Any] | None) -> list[tuple[str, str]]: + if not business: + return [] + features = business.get("business_features") or [] + details = business.get("business_feature_details") or {} + if not isinstance(features, list): + return [] + if not isinstance(details, dict): + details = {} + pairs: list[tuple[str, str]] = [] + for feature in features: + name = clean_text(feature) + if not name: + continue + detail = clean_text(details.get(name)) + pairs.append((name, detail)) + return pairs + + +def summarize_business_features(software_name: str, business: dict[str, Any] | None) -> str: + pairs = business_feature_pairs(business) + if not business or not pairs: + return "" + + industry = clean_text(business.get("industry")) + target_users = business.get("target_users") or [] + if isinstance(target_users, list): + target_text = "、".join(clean_text(item) for item in target_users if clean_text(item)) + else: + target_text = clean_text(target_users) + core_value = clean_text(business.get("core_value")) + product_positioning = clean_text(business.get("product_positioning")) + operation_flow = business.get("operation_flow") or [] + if isinstance(operation_flow, list): + flow_steps = [clean_text(item).rstrip("。") for item in operation_flow if clean_text(item)] + flow_text = ",再".join(flow_steps) + else: + flow_text = clean_text(operation_flow) + + feature_names = "、".join(name for name, _ in pairs[:8]) + parts: list[str] = [] + if product_positioning: + parts.append(product_positioning.rstrip("。") + "。") + else: + scope = f"面向{industry}" if industry else "面向实际业务场景" + users = f",服务于{target_text}" if target_text else "" + parts.append(f"{software_name}是一套{scope}{users}的应用软件。") + parts.append(f"软件主要提供{feature_names}等功能。") + if core_value: + parts.append(core_value.rstrip("。") + "。") + + for name, detail in pairs[:8]: + if detail: + detail = detail.rstrip("。") + if detail.startswith(("用户", "系统")): + parts.append(f"在{name}功能中,{detail}。") + else: + parts.append(f"{name}功能主要{detail}。") + else: + parts.append(f"{name}功能支持用户完成相关业务操作,并在处理完成后返回对应结果。") + + if flow_text: + parts.append(f"用户通常先{flow_text},系统在关键步骤提供状态反馈和结果展示。") + + result = "".join(parts) + while effective_len(result) < MIN_MAIN_FUNCTION_CHARS: + result += ( + f"围绕{feature_names}等核心功能,软件将用户输入、过程处理、结果查看和资料管理组织在连续的操作流程中," + "用户可以根据页面提示逐步完成业务处理,系统保存必要的数据记录并提供清晰的反馈信息," + "便于用户后续继续查看、复核和调整相关内容。" + ) + return trim_effective(result) + + +def summarize_features(analysis: dict[str, Any], software_name: str, business: dict[str, Any] | None = None) -> str: + """Generate a substantive main-function description for the application form. + + When business context JSON provides main_functions it will be used upstream; this + function is a fallback that assembles the best available evidence into a multi- + paragraph description targeting the 500-1300 Chinese-character window required by the + Chinese copyright office. + """ + business_summary = summarize_business_features(software_name, business) + if business_summary: + return business_summary + + features = analysis.get("feature_candidates") or [] + readme = (analysis.get("readme_excerpt") or "").strip() + routes = analysis.get("routes") or [] + + readable_features = [] + for feature in features: + name = humanize_feature(str(feature)) + if re.search(r"[A-Za-z]", name): + continue + if name and name not in readable_features: + readable_features.append(name) + + parts: list[str] = [] + + # Opening overview paragraph + feature_list = "、".join(readable_features[:12]) if readable_features else "信息展示、业务处理、数据管理和系统交互" + parts.append( + f"{software_name}是一套面向用户业务场景的综合软件系统," + f"主要提供{feature_list}等核心功能模块。" + f"系统通过清晰的操作界面和合理的业务流程设计,帮助用户高效完成日常工作和业务协作。" + ) + + # Module-by-module breakdown + detail_parts: list[str] = [] + for name in readable_features[:8]: + skip = {"软件登录", "用户注册", "用户认证", "首页", "数据看板", "系统设置"} + if name in skip: + continue + detail_parts.append(f"{name}模块支持用户进行相关数据的查看、录入和管理操作,提供完整的业务处理能力和结果反馈。") + + if not detail_parts: + route_display = [r.strip("/") for r in routes[:6] if r != "/" and not r.startswith("/:")] + if route_display: + for route_name in route_display[:6]: + label = route_name.replace("-", " ").replace("_", " ").title() + detail_parts.append(f"{label}模块支持用户进行相关数据的查看、录入和管理操作,提供完整的业务处理能力和结果反馈。") + else: + detail_parts.append("用户可通过系统界面完成数据查询、信息录入、业务处理和结果导出等操作。") + detail_parts.append("系统支持多角色用户的协同工作,不同权限用户可访问相应的功能模块。") + detail_parts.append("系统提供数据持久化存储和历史记录追溯能力,保障业务数据的完整性和可审计性。") + + # Limit detail parts to avoid exceeding max length + combined = "".join(detail_parts) + if len(combined) + len("".join(parts)) > MAX_MAIN_FUNCTION_CHARS: + while detail_parts and len("".join(detail_parts)) + len("".join(parts)) > MAX_MAIN_FUNCTION_CHARS: + detail_parts.pop() + + parts.extend(detail_parts) + + # Closing paragraph + if readme: + first_line = readme.splitlines()[0][:80] + parts.append(f"系统核心业务围绕{first_line}展开,覆盖从信息采集到结果呈现的完整操作链路。") + + result = "".join(parts) + + while effective_len(result) < MIN_MAIN_FUNCTION_CHARS: + padding = ( + "此外,系统还提供了配套的数据管理、用户操作记录、状态跟踪和系统配置等辅助功能模块," + "各个功能模块之间通过统一的界面布局和操作规范协同运行,用户可以在不同模块间灵活切换和处理跨模块的业务流程。" + "系统整体设计注重业务完整性和操作连续性,能够满足用户日常工作中对信息处理和业务管理的核心需求。" + "系统界面设计遵循清晰直观的原则,主要操作入口集中展示,用户无需复杂培训即可上手使用。" + "系统的数据管理能力包括数据的录入、存储、查询、修改和删除等基本操作,同时支持数据批量处理和导入导出功能。" + "在业务处理方面,系统支持多步骤业务流程的串联执行,各环节之间数据自动流转,减少用户重复录入。" + "系统还提供了灵活的配置选项,管理员可以根据实际业务需求调整系统参数和功能开关。" + ) + result += padding + + return trim_effective(result) + + +def humanize_feature(name: str) -> str: + value = re.sub(r"([a-z])([A-Z])", r"\1 \2", name) + value = value.replace("-", " ").replace("_", " ").strip() + key = value.lower().replace(" ", "") + mapping = { + "login": "软件登录", + "register": "用户注册", + "auth": "用户认证", + "home": "首页", + "dashboard": "数据看板", + "project": "项目管理", + "projects": "项目管理", + "projectsettings": "项目设置", + "projectssettings": "项目设置", + "settings": "系统设置", + "asset": "资源管理", + "assets": "资源管理", + "assethub": "资源中心", + "billing": "费用管理", + "agentstatusbar": "智能体状态展示", + "messagebubble": "消息展示", + "chatpanel": "对话面板", + "chatinput": "对话输入", + "assetpanel": "资源面板", + } + return mapping.get(key, value.title() if re.search(r"[A-Za-z]", value) else value) + + +def build_fields( + analysis: dict[str, Any], + manifest: dict[str, Any], + software_name: str, + version: str, + answers: dict[str, str], + business: dict[str, Any] | None = None, +) -> dict[str, str]: + frameworks = analysis.get("frameworks") or [] + framework_text = "、".join(frameworks) if frameworks else "前端工程化框架" + language = analysis.get("language") or "待用户确认" + project = Path(analysis.get("project_root") or ".") + hardware_hint = current_hardware_environment() + dev_os_hint = current_operating_system() + version_hint = version_confirmation_hint(analysis, version) + software_name_hint = f"待用户确认(建议:{software_name};请确认最终软件全称)" + + defaults = { + "软件全称": software_name_hint, + "软件简称": "", + "版本号": version_hint, + "软件分类": (business.get("software_category") or "应用软件") if business else "应用软件", + "开发完成日期": "待用户确认(YYYY-MM-DD)", + "开发方式": (business.get("development_situation") or "单独开发") if business else "单独开发", + "软件说明": "原创", + "发表状态": "待用户确认(已发表/未发表)", + "首次发表日期": "待用户确认(YYYY-MM-DD,未发表则留空)", + "著作权人": "待用户确认", + "权利范围": (business.get("rights_scope") or "全部权利") if business else "全部权利", + "权利取得方式": (business.get("rights_acquisition") or "原始取得") if business else "原始取得", + "开发的硬件环境": hardware_hint, + "运行的硬件环境": hardware_hint, + "开发该软件的操作系统": dev_os_hint, + "软件开发环境 / 开发工具": f"开发环境: {dev_os_hint}/开发工具: {infer_ide_name(project)}", + "该软件的运行平台 / 操作系统": infer_runtime_os(analysis), + "软件运行支撑环境 / 支持软件": infer_runtime_support(analysis, project), + "编程语言": language, + "源程序量": str(manifest.get("source_line_count") or manifest.get("selected_source_line_count") or "待用户确认"), + "开发目的": (business.get("application_purpose") or f"待用户确认(≤50字符,需说明开发目的,不能只写软件名称)") if business else "待用户确认(≤50字符,需说明开发目的,不能只写软件名称)", + "面向领域 / 行业": (business.get("industry") or "待用户确认") if business else "待用户确认", + "软件的主要功能": (business.get("main_functions") or summarize_features(analysis, software_name, business)) if business else summarize_features(analysis, software_name, business), + "软件的技术特点": (business.get("technical_characteristics") or f"系统采用{framework_text}构建前端界面,结合模块化组件、路由组织、接口封装和状态管理实现业务功能") if business else f"系统采用{framework_text}构建前端界面,结合模块化组件、路由组织、接口封装和状态管理实现业务功能", + "页数": str(manifest.get("total_pages") or "待用户确认"), + } + defaults.update({k: v for k, v in answers.items() if v}) + + # 未发表时首次发表日期应为空,不触发待确认门禁 + publish_status = defaults.get("发表状态", "") + if "未发表" in publish_status and "已发表" not in publish_status: + if "待用户确认" in defaults.get("首次发表日期", "") or not defaults.get("首次发表日期", "").strip(): + defaults["首次发表日期"] = "" + + # 软件的主要功能:确保业务理解提供的内容也满足最低字数 + main_func = defaults.get("软件的主要功能", "") + if main_func and "待用户确认" not in main_func and effective_len(main_func) < MIN_MAIN_FUNCTION_CHARS: + defaults["软件的主要功能"] = summarize_features(analysis, software_name, business) + + return defaults + + +def version_numbers(value: str) -> tuple[int, ...]: + raw = str(value or "").strip() + raw = raw.lstrip("vV") + parts = re.findall(r"\d+", raw) + return tuple(int(part) for part in parts[:3]) + + +def version_less_than_1(value: str) -> bool: + numbers = version_numbers(value) + return bool(numbers) and numbers[0] < 1 + + +def normalize_version_label(value: str) -> str: + raw = str(value or "").strip() + if not raw: + return "" + return raw if raw.upper().startswith("V") else f"V{raw}" + + +def project_version_candidate(analysis: dict[str, Any]) -> str: + value = str((analysis.get("package") or {}).get("version") or "").strip() + if value and value.upper() != "V1.0": + return normalize_version_label(value) + return "" + + +def version_confirmation_hint(analysis: dict[str, Any], requested_version: str) -> str: + project_version = project_version_candidate(analysis) + requested = normalize_version_label(requested_version or "V1.0") + if project_version and version_less_than_1(project_version): + return ( + f"待用户确认(项目版本号为 {project_version},软著首次提交通常建议从 V1.0 开始;" + f"请确认填写 V1.0 还是 {project_version})" + ) + if not project_version and version_less_than_1(requested): + return ( + f"待用户确认(当前建议版本号为 {requested},软著首次提交通常建议从 V1.0 开始;" + f"请确认填写 V1.0 还是 {requested})" + ) + if project_version and project_version != requested: + return f"待用户确认(项目版本号为 {project_version},当前建议为 {requested};请确认最终申报版本号)" + return f"待用户确认(建议:{requested};请确认最终版本号)" + + +def format_gb(size: int | None) -> str: + if not size: + return "" + return f"{size / (1024 ** 3):.0f}GB" + + +def total_memory_bytes() -> int | None: + try: + return os.sysconf("SC_PAGE_SIZE") * os.sysconf("SC_PHYS_PAGES") + except (AttributeError, OSError, ValueError): + return None + + +def current_hardware_environment() -> str: + parts: list[str] = [] + cpu_count = os.cpu_count() + machine = platform.machine() + processor = platform.processor() + if processor and processor != machine and processor.lower() != "arm": + parts.append(f"CPU {processor}") + if cpu_count: + parts.append(f"CPU {cpu_count}核") + if machine: + parts.append(f"架构 {machine}") + memory = format_gb(total_memory_bytes()) + if memory: + parts.append(f"内存 {memory}") + try: + disk = shutil.disk_usage(Path.home()) + disk_total = format_gb(disk.total) + if disk_total: + parts.append(f"硬盘 {disk_total}") + except OSError: + pass + if parts: + return "、".join(parts) + return "待用户确认" + + +def current_operating_system() -> str: + system = platform.system() + if system == "Darwin": + version = platform.mac_ver()[0] + label = f"macOS {version}" if version else f"macOS(Darwin {platform.release()})" + elif system == "Windows": + label = f"Windows {platform.release()}" + elif system == "Linux": + label = f"Linux {platform.release()}" + else: + label = f"{system} {platform.release()}".strip() or "待用户确认" + return label + + +def infer_ide_name(project: Path) -> str: + if (project / ".idea").exists(): + return "WebStorm 或 IntelliJ IDEA" + if (project / ".vscode").exists(): + return "Visual Studio Code" + if list(project.glob("*.code-workspace")): + return "Visual Studio Code" + return "Visual Studio Code" + + +def infer_runtime_os(analysis: dict[str, Any]) -> str: + frameworks = set(analysis.get("frameworks") or []) + deps = set((analysis.get("package") or {}).get("dependency_names") or []) + if "Electron" in frameworks or "electron" in deps or "Tauri" in frameworks or "@tauri-apps/api" in deps: + return "Windows 10/11 或 macOS 13及以上版本" + if frameworks & {"Vue", "React", "Vite", "Next.js", "Nuxt", "Svelte", "Astro", "Angular"}: + return "Windows 10/11 或 macOS 13及以上版本" + return "Windows 10/11 或 macOS 13及以上版本" + + +def project_file(project: Path, relative: str) -> Path | None: + if not relative: + return None + path = project / relative + return path if path.exists() else None + + +def load_project_package(project: Path, analysis: dict[str, Any]) -> dict[str, Any]: + package_path = project_file(project, (analysis.get("package") or {}).get("path") or "") + if package_path: + try: + return read_json(package_path) + except Exception: + return {} + return {} + + +def read_readme(project: Path) -> str: + for name in ("README.md", "README.zh.md", "readme.md", "Readme.md"): + path = project / name + if path.exists(): + try: + return read_text(path, limit=12000) + except Exception: + return "" + return "" + + +def extract_requirement_bullets(text: str) -> list[str]: + wanted = ("python", "node", "docker", "compose", "postgres", "redis", "chrome", "edge", "safari") + # Patterns that indicate a feature description rather than a runtime requirement. + _feature_start = re.compile(r"^(?:[一-鿿]|L\d|P\d|[A-Z]\d\s)") + bullets: list[str] = [] + for line in text.splitlines(): + match = re.match(r"\s*[-*]\s+(.+)", line) + if not match: + continue + item = match.group(1).strip() + if any(key in item.lower() for key in wanted) and item not in bullets: + if len(item) > 80: + continue + if _feature_start.match(item) and "(" in item: + continue + bullets.append(item) + return bullets[:8] + + +def detect_package_manager(project: Path, package_path: str) -> str: + base = (project / package_path).parent if package_path else project + checks = [ + ("pnpm-lock.yaml", "pnpm"), + ("yarn.lock", "Yarn"), + ("bun.lock", "Bun"), + ("bun.lockb", "Bun"), + ("package-lock.json", "npm"), + ] + for filename, manager in checks: + if (base / filename).exists() or (project / filename).exists(): + return manager + return "npm" + + +def has_support_term(items: list[str], term: str) -> bool: + return any(term.lower() in item.lower() for item in items) + + +def infer_runtime_support(analysis: dict[str, Any], project: Path) -> str: + """Infer runtime support environment (≤50 chars plain text).""" + package_info = load_project_package(project, analysis) + package_path = (analysis.get("package") or {}).get("path") or "" + deps = set((analysis.get("package") or {}).get("dependency_names") or []) + frameworks = set(analysis.get("frameworks") or []) + + support: list[str] = [] + readme_requirements = extract_requirement_bullets(read_readme(project)) + + if package_info or deps or frameworks & {"Vue", "React", "Vite", "Next.js", "Nuxt", "Svelte", "Astro", "Angular"}: + if not has_support_term(support, "node"): + node_engine = str((package_info.get("engines") or {}).get("node") or "").strip() + support.append(f"Node.js {node_engine}".strip() if node_engine else "Node.js") + support.append(detect_package_manager(project, package_path)) + support.append("现代浏览器") + + if ((project / "pyproject.toml").exists() or any(project.glob("*/pyproject.toml"))) and not has_support_term(support, "python"): + support.append("Python") + if ((project / "requirements.txt").exists() or list(project.glob("*/requirements*.txt"))) and not has_support_term(support, "python"): + support.append("Python") + + if ((project / "docker-compose.yml").exists() or (project / "docker-compose.yaml").exists() or list(project.glob("docker-compose*.yml"))) and not has_support_term(support, "docker"): + support.append("Docker") + + compose_text = "" + for compose in list(project.glob("docker-compose*.yml")) + list(project.glob("docker-compose*.yaml")): + try: + compose_text += "\n" + read_text(compose, limit=20000).lower() + except Exception: + continue + if "postgres" in compose_text: + support.append("PostgreSQL") + if "redis" in compose_text: + support.append("Redis") + + # Also incorporate readme requirements into support software + for req in readme_requirements: + if not has_support_term(support, req.split()[0].lower()): + support.append(req) + + # Deduplicate + unique: list[str] = [] + for item in support: + clean = str(item).strip().rstrip(";;") + if clean and clean not in unique: + unique.append(clean) + + if not unique: + return "待用户确认" + + # Enforce ≤50 char limit: trim items if needed + result = "、".join(unique) + if len(result) > 50: + trimmed: list[str] = [] + for item in unique: + candidate = "、".join(trimmed + [item]) + if len(candidate) <= 50: + trimmed.append(item) + else: + break + result = "、".join(trimmed) if trimmed else unique[0][:50] + return result + + +def write_application_md(path: Path, fields: dict[str, str], analysis: dict[str, Any], manifest: dict[str, Any], business: dict[str, Any] | None = None) -> None: + # 兜底:未发表时首次发表日期不应输出"待用户确认",直接留空 + publish_status = fields.get("发表状态", "") + if "未发表" in publish_status and "已发表" not in publish_status: + if "待用户确认" in (fields.get("首次发表日期") or "") or not (fields.get("首次发表日期") or "").strip(): + fields["首次发表日期"] = "" + + lines = ["# 申请表信息", ""] + for field in FIELD_ORDER: + lines.append(f"➤{field}:{fields.get(field, '待用户确认')}") + pending = [field for field in FIELD_ORDER if "待用户确认" in (fields.get(field) or "")] + + # Build warnings for common issues + warnings: list[str] = [] + soft_name = fields.get("软件全称", "") + clean_name = str(soft_name).strip() + raw_name = clean_name + if "待用户确认" in clean_name and "建议:" in clean_name: + try: + raw_name = clean_name.split("建议:")[1].rstrip(")").split(";")[0].strip() + except (IndexError, ValueError): + raw_name = clean_name + for suffix in ["软件", "平台"]: + if raw_name.endswith(suffix): + warnings.append(f"软件全称以「{suffix}」结尾,存在被驳回风险。建议考虑去掉「{suffix}」后缀或改用其他命名方式。") + + main_func = fields.get("软件的主要功能", "") + if main_func and "待用户确认" not in main_func: + func_len = len(str(main_func).replace(" ", "").replace("\n", "")) + if func_len < MIN_MAIN_FUNCTION_CHARS: + warnings.append(f"软件的主要功能仅有 {func_len} 字,应不少于 {MIN_MAIN_FUNCTION_CHARS} 字。请扩写功能说明。") + elif func_len > MAX_MAIN_FUNCTION_CHARS: + warnings.append(f"软件的主要功能共 {func_len} 字,超过建议上限 {MAX_MAIN_FUNCTION_CHARS} 字。请精简。") + + # Check character limits for fields with ≤50 or ≤100 constraints + char_limit_fields = { + "开发的硬件环境": 50, + "运行的硬件环境": 50, + "开发该软件的操作系统": 50, + "软件开发环境 / 开发工具": 50, + "该软件的运行平台 / 操作系统": 50, + "软件运行支撑环境 / 支持软件": 50, + "开发目的": 50, + "面向领域 / 行业": 50, + "软件的技术特点": 100, + } + for field_name, limit in char_limit_fields.items(): + value = fields.get(field_name, "") + if value and "待用户确认" not in value and len(value) > limit: + warnings.append(f"{field_name}共 {len(value)} 字符,超过限制 {limit} 字符。请精简。") + + lines.extend( + [ + "", + "## 字段填写口径", + "", + "- 软件全称:必须由用户确认;最终正式资料文件名、代码页眉和操作手册中的软件名称均以本字段为准。", + "- 软件简称:可选;如有常用简称则填写。", + "- 版本号:必须由用户确认;如果项目版本小于 V1.0,软著首次提交通常建议使用 V1.0,也可按实际项目版本填写,最终以本字段为准。", + "- 软件分类:应用软件/嵌入式软件/中间件/系统软件/其他。", + "- 开发完成日期、首次发表日期:必须使用 YYYY-MM-DD 格式。", + "- 开发方式:单独开发/合作开发/委托开发/下达任务开发。", + "- 软件说明:原创 / 修改(含翻译软件、合成软件)。", + "- 发表状态:已发表或未发表;已发表需附首次发表日期。", + "- 软件开发环境 / 开发工具:≤50字符,格式 `开发环境: /开发工具: `。", + "- 开发该软件的操作系统:≤50字符,填写实际开发电脑的操作系统版本。", + "- 该软件的运行平台 / 操作系统:≤50字符,填写软件运行所在的操作系统或浏览器环境。", + "- 软件运行支撑环境 / 支持软件:≤50字符,直接列出运行依赖(如 Node.js、npm、浏览器),不加格式前缀。", + "- 开发的硬件环境和运行的硬件环境:≤50字符,可使用检测到的电脑配置作为建议值。", + "- 源程序量:纯数字(不含'行'字),指全部源程序的总行数。", + "- 开发目的:≤50字符,用一句话说明目的,不能只写软件名称。", + "- 面向领域 / 行业:≤50字符。", + "- 软件的主要功能:500~1300字。", + "- 软件的技术特点:多选标签(APP/游戏软件/教育软件/金融软件/医疗软件/地理信息软件/云计算软件/信息安全软件/大数据软件/人工智能软件/VR软件/5G软件/小程序/物联网软件/智慧城市软件)+ 文本描述≤100字符。", + "", + "## 项目分析摘要", + "", + f"- 项目目录:{analysis.get('project_root', '')}", + f"- 框架:{'、'.join(analysis.get('frameworks') or []) or '未识别'}", + f"- 源码文件数:{analysis.get('source', {}).get('total_file_count', analysis.get('source', {}).get('file_count', 0))}", + f"- 源程序量(含空行):{analysis.get('source', {}).get('total_line_count', analysis.get('source', {}).get('line_count', 0))}", + f"- 代码材料页数:{manifest.get('total_pages', 0)}", + f"- 代码输出模式:{manifest.get('mode', '')}", + f"- 业务理解:{'已读取 草稿/业务理解.json' if business else '未提供,使用项目分析兜底'}", + "", + "## 待确认字段", + "", + ] + ) + if warnings: + lines.append("## 字段提醒") + lines.append("") + lines.extend(f"- {w}" for w in warnings) + lines.append("") + if pending: + lines.extend(f"- {field}" for field in pending) + else: + lines.append("- 无") + lines.extend( + [ + "", + "```text", + "STOP_FOR_USER", + "NEXT_ACTION: 请补全并确认申请表字段;确认后运行 confirm_stage.py --stage application-fields。", + "```", + ] + ) + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def require_confirmed_business(business: dict[str, Any] | None) -> None: + if business is None: + raise SystemExit( + "STOP_FOR_USER\n" + "NEXT_ACTION: 申请表信息必须基于已确认的业务理解生成。请先生成并确认 草稿/业务理解.md。" + ) + if business.get("confirmation_required") and not business.get("user_confirmed"): + raise SystemExit( + "STOP_FOR_USER\n" + "NEXT_ACTION: 业务理解尚未确认。请先确认 草稿/业务理解.md," + "再运行 `python3 /scripts/confirm_stage.py --workdir 软件著作权申请资料 --stage business --note \"<用户确认内容>\"`。" + ) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--analysis", required=True) + parser.add_argument("--code-manifest", required=True) + parser.add_argument("--software-name", required=True) + parser.add_argument("--version", default="V1.0") + parser.add_argument("--answers", help="Optional JSON object with confirmed field values") + parser.add_argument("--business-context", help="Business context JSON generated before material drafting") + parser.add_argument("--out-dir", default="软件著作权申请资料/草稿") + args = parser.parse_args() + + analysis = read_json(Path(args.analysis)) + manifest = read_json(Path(args.code_manifest)) + answers = read_json(Path(args.answers)) if args.answers else {} + business = read_json(Path(args.business_context)) if args.business_context else None + require_confirmed_business(business) + out_dir = ensure_dir(Path(args.out_dir)) + + fields = build_fields(analysis, manifest, args.software_name, args.version, answers, business) + out_path = out_dir / "申请表信息.md" + write_application_md(out_path, fields, analysis, manifest, business) + print(f"OK application draft: {out_path}") + print("STOP_FOR_USER") + print("NEXT_ACTION: 请补全并确认申请表字段;确认后运行 confirm_stage.py --stage application-fields。") + + +if __name__ == "__main__": + main() diff --git a/software-copyright-materials/scripts/generate_business_context.py b/software-copyright-materials/scripts/generate_business_context.py new file mode 100644 index 0000000..1bf4efe --- /dev/null +++ b/software-copyright-materials/scripts/generate_business_context.py @@ -0,0 +1,431 @@ +#!/usr/bin/env python3 +"""Collect project evidence and write a model-authored business context.""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path +from typing import Any + +from common import ensure_dir, iter_project_files, read_json, read_text, rel, write_json + + +DOC_EXTS = {".md", ".txt", ".rst", ".adoc"} +MAX_DOC_CHARS = 80_000 +MAX_DOCS = 40 + + +def normalize_space(text: str) -> str: + return re.sub(r"\s+", " ", text).strip() + + +def strip_md(text: str) -> str: + text = re.sub(r"`{3}.*?`{3}", " ", text, flags=re.S) + text = re.sub(r"\[(.*?)\]\(.*?\)", r"\1", text) + text = re.sub(r"[>#*_`|]", " ", text) + return normalize_space(text) + + +def skip_doc(path: Path, project: Path) -> bool: + r = rel(path, project).lower() + skip_parts = ( + "node_modules", + ".git/", + "dist/", + "build/", + ".next/", + "coverage/", + "软件著作权申请资料", + ) + return any(part in r for part in skip_parts) + + +def extract_headings(text: str, limit: int = 24) -> list[str]: + headings: list[str] = [] + for line in text.splitlines(): + clean = line.strip() + if clean.startswith("#"): + title = clean.lstrip("#").strip() + if title and title not in headings: + headings.append(title[:120]) + if len(headings) >= limit: + break + return headings + + +def extract_opening(text: str, limit: int = 900) -> str: + clean = strip_md(text) + return clean[:limit].strip() + + +def collect_documents(project: Path) -> list[dict[str, Any]]: + docs: list[dict[str, Any]] = [] + for path in iter_project_files(project, DOC_EXTS): + if skip_doc(path, project): + continue + try: + text = read_text(path, limit=MAX_DOC_CHARS) + except Exception: + continue + if not text.strip(): + continue + docs.append( + { + "path": rel(path, project), + "size": path.stat().st_size, + "headings": extract_headings(text), + "opening": extract_opening(text), + } + ) + docs.sort(key=lambda item: (item["path"].count("/"), item["path"])) + return docs[:MAX_DOCS] + + +def collect_code_evidence(analysis: dict[str, Any]) -> dict[str, Any]: + source = analysis.get("source") or {} + categorized = source.get("categorized_files") or {} + return { + "project_name": analysis.get("project_name"), + "software_name_candidate": analysis.get("software_name_candidate"), + "frameworks": analysis.get("frameworks") or [], + "language": analysis.get("language"), + "routes": analysis.get("routes") or [], + "feature_name_candidates": analysis.get("feature_candidates") or [], + "entry_files": categorized.get("entry") or [], + "page_files": categorized.get("page") or [], + "component_files": categorized.get("component") or [], + "api_files": categorized.get("api") or [], + "run_command_candidates": analysis.get("run_command_candidates") or [], + "package": analysis.get("package") or {}, + } + + +def build_evidence(project: Path, analysis: dict[str, Any], software_name: str, web_notes: str) -> dict[str, Any]: + return { + "software_name": software_name, + "project_root": str(project.resolve()), + "instruction": ( + "本文件只收集证据,不决定行业、功能或手册结构。" + "请由模型阅读这些证据以及必要的项目源码后,另行编写业务理解模型稿。" + ), + "documents": collect_documents(project), + "code_evidence": collect_code_evidence(analysis), + "external_research_notes": web_notes, + } + + +def write_evidence_md(path: Path, evidence: dict[str, Any]) -> None: + lines = [ + "# 业务理解证据", + "", + f"- 软件名称:{evidence['software_name']}", + f"- 项目目录:`{evidence['project_root']}`", + "", + "本文件只列出可供模型研判的项目证据,不代表最终申报口径。", + "模型需要自行判断应阅读哪些文档、抽取哪些功能、采用什么操作手册结构。", + "", + "## 代码与页面证据", + "", + ] + code = evidence["code_evidence"] + for key in ("frameworks", "language", "routes", "feature_name_candidates", "entry_files", "page_files", "component_files", "api_files"): + value = code.get(key) + if value: + lines.append(f"- {key}:{value}") + lines.extend(["", "## 文档证据", ""]) + for doc in evidence["documents"]: + lines.extend( + [ + f"### {doc['path']}", + "", + f"- 大小:{doc['size']} bytes", + f"- 标题线索:{';'.join(doc['headings']) if doc['headings'] else '无'}", + "", + doc["opening"], + "", + ] + ) + if evidence.get("external_research_notes"): + lines.extend(["## 外部调研摘要", "", evidence["external_research_notes"], ""]) + path.write_text("\n".join(lines), encoding="utf-8") + + +def write_model_template(path: Path, evidence: dict[str, Any]) -> None: + template = { + "software_name": evidence["software_name"], + "product_positioning": "", + "industry": "", + "target_users": [], + "core_value": "", + "business_features": [], + "business_feature_details": {}, + "operation_flow": [], + "application_purpose": "", + "main_functions": "", + "technical_characteristics": "", + "software_technical_option": "应用软件", + "software_category": "应用软件", + "manual_sections": [ + { + "title": "模型自行命名章节", + "intent": "说明该章节为什么适合当前项目。", + "paragraphs": [], + "include_feature_overview": False, + "include_operation_modules": False, + "include_operation_flow": False, + } + ], + "manual_modules": [ + { + "title": "真实页面或核心流程名称", + "evidence": ["页面、路由、组件、README 或需求文档路径"], + "purpose": "该页面或流程在当前软件中的用途。", + "usage": "用户在什么业务场景下会使用该页面,正在处理什么具体事务。", + "entry": "用户从哪里进入该页面或流程。", + "visible_elements": ["用户实际能看到的输入框、按钮、列表、状态或结果区域"], + "operation_steps": ["按真实页面顺序描述用户动作,不写代码实现。"], + "validation_rules": ["输入限制、必填项、额度、权限、异常提示等规则;没有则留空数组。"], + "feedback": ["操作完成后用户能看到的结果、提示或状态变化。"], + "screenshot": "截图预留说明", + } + ], + "system_requirements": [ + {"item": "操作系统", "minimum": "按项目实际填写", "recommended": "按项目实际填写"}, + {"item": "浏览器或客户端", "minimum": "按项目实际填写", "recommended": "按项目实际填写"}, + ], + "faq": [ + {"question": "按当前软件真实使用场景填写常见问题", "answer": "给出面向普通用户的处理方法。"} + ], + "glossary": [ + {"term": "当前软件中的业务术语", "definition": "用普通中文解释含义。"} + ], + "model_review_notes": [ + "操作手册应采用软著审核友好的通用骨架:相关文档、说明、功能特点、系统要求、按真实页面/流程逐章操作、常见问题、术语表。", + "manual_modules 要按当前项目真实页面、导航入口、按钮、输入限制、系统反馈和截图位置编写,不能只写抽象功能名。", + "不要照抄范本文案;范本只说明手册需要具体、可操作、能给审核员看懂。", + "不要用关键词表决定行业和功能;必须能从项目证据或用户补充中解释来源。", + ], + } + write_json(path, template) + + +def load_model_context(path: Path) -> dict[str, Any]: + data = read_json(path) + if not isinstance(data, dict): + raise SystemExit(f"Invalid model context JSON: {path}") + return data + + +def required_list(value: Any, field: str) -> list[str]: + if not isinstance(value, list): + raise SystemExit(f"Model context field must be a list: {field}") + items = [str(item).strip() for item in value if str(item).strip()] + if not items: + raise SystemExit(f"Model context field cannot be empty: {field}") + return items + + +def required_text(data: dict[str, Any], field: str) -> str: + value = str(data.get(field) or "").strip() + if not value: + raise SystemExit(f"Model context field cannot be empty: {field}") + return value + + +def normalize_model_context(model: dict[str, Any], evidence: dict[str, Any], web_notes: str) -> dict[str, Any]: + features = required_list(model.get("business_features"), "business_features") + details = model.get("business_feature_details") or {} + if not isinstance(details, dict): + raise SystemExit("Model context field must be an object: business_feature_details") + missing = [feature for feature in features if not str(details.get(feature) or "").strip()] + if missing: + raise SystemExit("Model context missing feature details: " + "、".join(missing[:12])) + sections = model.get("manual_sections") or [] + if sections and not isinstance(sections, list): + raise SystemExit("Model context field must be a list: manual_sections") + manual_modules = model.get("manual_modules") or [] + if manual_modules and not isinstance(manual_modules, list): + raise SystemExit("Model context field must be a list: manual_modules") + if not manual_modules: + raise SystemExit("Model context field cannot be empty: manual_modules") + for index, module in enumerate(manual_modules, start=1): + if not isinstance(module, dict): + raise SystemExit(f"manual_modules item {index} must be an object") + title = str(module.get("title") or module.get("feature") or "").strip() + for field in ("purpose", "usage", "entry", "operation_steps", "feedback"): + value = module.get(field) + if field == "usage" and not str(value or "").strip(): + value = module.get("usage_scenario") + if isinstance(value, list): + missing_value = not any(str(item).strip() for item in value) + else: + missing_value = not str(value or "").strip() + if missing_value: + raise SystemExit(f"manual_modules item {index} ({title or 'untitled'}) missing field: {field}") + system_requirements = model.get("system_requirements") or [] + if system_requirements and not isinstance(system_requirements, list): + raise SystemExit("Model context field must be a list: system_requirements") + if not system_requirements: + raise SystemExit("Model context field cannot be empty: system_requirements") + faq = model.get("faq") or [] + if faq and not isinstance(faq, list): + raise SystemExit("Model context field must be a list: faq") + if not faq: + raise SystemExit("Model context field cannot be empty: faq") + glossary = model.get("glossary") or [] + if glossary and not isinstance(glossary, list): + raise SystemExit("Model context field must be a list: glossary") + if not glossary: + raise SystemExit("Model context field cannot be empty: glossary") + context = { + "software_name": evidence["software_name"], + "business_understanding_required": True, + "source_documents": [{"path": doc["path"], "size": doc["size"]} for doc in evidence["documents"]], + "project_evidence_file": "业务理解证据.md", + "product_positioning": required_text(model, "product_positioning"), + "industry": required_text(model, "industry"), + "target_users": required_list(model.get("target_users"), "target_users"), + "core_value": required_text(model, "core_value"), + "business_features": features, + "business_feature_details": {feature: str(details.get(feature)).strip() for feature in features}, + "operation_flow": required_list(model.get("operation_flow"), "operation_flow"), + "application_purpose": required_text(model, "application_purpose"), + "main_functions": required_text(model, "main_functions"), + "technical_characteristics": required_text(model, "technical_characteristics"), + "software_technical_option": str(model.get("software_technical_option") or "应用软件"), + "software_category": str(model.get("software_category") or "应用软件"), + "manual_sections": sections, + "manual_modules": manual_modules, + "system_requirements": system_requirements, + "faq": faq, + "glossary": glossary, + "model_authored": True, + "external_research_notes": web_notes, + "confirmation_required": True, + "user_confirmed": False, + "confirmation_stage": "business", + "next_action": "请确认 草稿/业务理解.md 中的软件用途、行业、目标用户、核心功能、手册结构和申请口径;确认后运行 confirm_stage.py --stage business。", + "review_notes": [ + "请确认模型判断的行业领域、目标用户和主要功能是否符合实际申报口径。", + "请确认操作手册结构是否按真实页面和流程展开,而不是套用抽象功能列表。", + ], + } + return context + + +def write_context_md(path: Path, context: dict[str, Any]) -> None: + lines = [ + "# 业务理解", + "", + f"- 软件名称:{context['software_name']}", + f"- 产品定位:{context['product_positioning']}", + f"- 面向领域 / 行业:{context['industry']}", + f"- 核心价值:{context['core_value']}", + f"- 证据文件:`{context['project_evidence_file']}`", + "", + "## 目标用户", + "", + ] + lines.extend(f"- {item}" for item in context["target_users"]) + lines.extend(["", "## 主要业务功能", ""]) + lines.extend(f"- {item}" for item in context["business_features"]) + lines.extend(["", "## 功能说明", ""]) + for item in context["business_features"]: + lines.append(f"- {item}:{context['business_feature_details'].get(item, '')}") + lines.extend(["", "## 典型操作流程", ""]) + lines.extend(f"{i}. {item}" for i, item in enumerate(context["operation_flow"], start=1)) + if context.get("manual_sections"): + lines.extend(["", "## 操作手册结构建议", ""]) + for i, section in enumerate(context["manual_sections"], start=1): + if isinstance(section, dict): + title = section.get("title") or f"章节 {i}" + intent = section.get("intent") or "" + else: + title = str(section) + intent = "" + lines.append(f"{i}. {title}" + (f":{intent}" if intent else "")) + if context.get("manual_modules"): + lines.extend(["", "## 操作手册页面/流程模块", ""]) + for i, module in enumerate(context["manual_modules"], start=1): + if not isinstance(module, dict): + lines.append(f"{i}. {module}") + continue + title = module.get("title") or module.get("feature") or f"模块 {i}" + usage = module.get("usage") or module.get("usage_scenario") or "" + entry = module.get("entry") or "" + steps = module.get("operation_steps") or module.get("steps") or [] + lines.append(f"{i}. {title}" + (f":{entry}" if entry else "")) + if usage: + lines.append(f" - 使用场景:{usage}") + if steps: + lines.append(f" - 操作要点:{';'.join(str(item) for item in steps[:4])}") + lines.extend( + [ + "", + "## 申请表建议口径", + "", + f"- 开发目的:{context['application_purpose']}", + f"- 软件的主要功能:{context['main_functions']}", + f"- 技术特点:{context['technical_characteristics']}", + f"- 软件的技术特点选项:{context['software_technical_option']}", + f"- 软件分类:{context['software_category']}", + "", + "## 证据来源", + "", + ] + ) + lines.extend(f"- `{item['path']}`" for item in context["source_documents"]) + lines.extend(["", "## 待确认", ""]) + lines.extend(f"- {item}" for item in context["review_notes"]) + lines.extend( + [ + "", + "```text", + "STOP_FOR_USER", + f"NEXT_ACTION: {context['next_action']}", + "```", + ] + ) + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--project", required=True) + parser.add_argument("--analysis", required=True) + parser.add_argument("--software-name", required=True) + parser.add_argument("--out-dir", default="软件著作权申请资料/草稿") + parser.add_argument("--web-notes", help="Optional plain-text notes from external/competitor research") + parser.add_argument("--model-context", help="Model-authored business context JSON") + args = parser.parse_args() + + project = Path(args.project) + analysis = read_json(Path(args.analysis)) + web_notes = read_text(Path(args.web_notes)) if args.web_notes else "" + out_dir = ensure_dir(Path(args.out_dir)) + + evidence = build_evidence(project, analysis, args.software_name, web_notes) + write_json(out_dir / "业务理解证据.json", evidence) + write_evidence_md(out_dir / "业务理解证据.md", evidence) + + if not args.model_context: + write_model_template(out_dir / "业务理解模型稿模板.json", evidence) + print(f"OK business evidence: {out_dir / '业务理解证据.md'}") + print(f"OK model template: {out_dir / '业务理解模型稿模板.json'}") + print("NEXT_ACTION: 模型需要阅读业务理解证据和项目源码,自行编写业务理解模型稿 JSON,然后用 --model-context 生成业务理解.md/json。") + return + + model = load_model_context(Path(args.model_context)) + context = normalize_model_context(model, evidence, web_notes) + write_json(out_dir / "业务理解.json", context) + write_context_md(out_dir / "业务理解.md", context) + print(f"OK business context: {out_dir / '业务理解.md'}") + print(f"Features: {len(context['business_features'])}") + print("STOP_FOR_USER") + print(f"NEXT_ACTION: {context['next_action']}") + + +if __name__ == "__main__": + main() diff --git a/software-copyright-materials/scripts/generate_manual_draft.py b/software-copyright-materials/scripts/generate_manual_draft.py new file mode 100644 index 0000000..7983b9e --- /dev/null +++ b/software-copyright-materials/scripts/generate_manual_draft.py @@ -0,0 +1,837 @@ +#!/usr/bin/env python3 +"""Generate a reviewer-oriented operation manual Markdown draft.""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path +from typing import Any + +from common import ensure_dir, read_json + + +def join_items(items: list[str], limit: int = 4) -> str: + values = [str(item) for item in items if str(item).strip()] + if not values: + return "业务用户" + return "、".join(values[:limit]) + + +def feature_summary(feature: str, detail: str, software_name: str) -> str: + clean_detail = normalize_detail(feature, detail) + return clean_detail + + +def plain_manual_text(text: str) -> str: + value = text + replacements = { + "多 Agent": "多智能体", + "多 agent": "多智能体", + "业务逻辑": "使用过程", + "前端页面": "软件页面", + "前端": "界面", + "后端服务": "系统服务", + "后端": "系统服务", + "接口": "数据通道", + "组件": "页面组成部分", + "路由": "页面入口", + "状态管理": "状态记录", + "数据持久化": "数据保存", + "异步任务": "后台处理任务", + "任务队列": "任务处理服务", + "模型": "智能服务", + "调度中心": "协调中心", + "结构化依据": "后续说明", + "高成本生成": "耗时较长的内容生成", + } + for source, target in replacements.items(): + value = value.replace(source, target) + value = re.sub(r"(? str: + value = plain_manual_text(str(name)) + value = value.replace("Chat", "对话") + return value.strip() or "核心功能" + + +def normalize_detail(feature: str, detail: str) -> str: + value = plain_manual_text(detail or "").strip() + value = re.sub(rf"^{re.escape(feature)}(模块|功能)?用于", "", value) + value = re.sub(rf"^{re.escape(feature)}主要用于", "", value) + value = re.sub(r"^主要用于", "", value) + value = re.sub(rf"^{re.escape(feature)}[::,, ]*", "", value) + value = re.sub(rf"^用户使用{re.escape(feature)}时,可以", "", value) + value = re.sub(rf"^进入{re.escape(feature)}后,用户可以", "", value) + value = re.sub(rf"^在{re.escape(feature)}中,用户可以", "", value) + value = re.sub(rf"^用户通过{re.escape(feature)}可以", "", value) + value = re.sub(rf"^在{re.escape(feature)}环节,用户可以", "", value) + value = re.sub(rf"^通过{re.escape(feature)},用户可以", "", value) + value = value.strip("。;; ,,") + if not value or value == feature: + value = "支撑软件中的相关业务处理,帮助用户完成信息查看、内容填写、结果确认或资料维护" + return value + ("。" if not value.endswith("。") else "") + + +TECHNICAL_TERMS = [ + "技术实现", + "代码", + "框架", + "接口封装", + "状态管理", + "异步任务", + "任务队列", + "数据持久化", + "业务逻辑", + "React", + "Next.js", + "FastAPI", + "LangGraph", + "Celery", +] + +TEMPLATE_MARKERS = [ + "重要功能之一", + "通过清晰的页面入口、信息展示和结果反馈", + "对应操作环节", + "审核时可重点查看", + "审核人员可通过", + "按照页面提示填写内容、选择资料、确认方案或点击提交按钮", + "系统处理完成后显示结果或提示信息", + "帮助用户用户", + "帮助用户系统", + "主要用于在", + "项目管理或资产中心项目管理", + "进入方式:", + "页面内容:", + "操作步骤:", + "操作规则:", + "操作结果与反馈:", + "功能特点根据当前项目资料", + "软件围绕", +] + +AI_TONE_MARKERS = [ + "旨在", + "赋能", + "一站式", + "智能化", + "高效便捷", + "显著提升", + "强大能力", + "丰富功能", + "极大地", + "全方位", + "多维度", + "闭环", + "降本增效", + "优化体验", + "提升效率", +] + + +def manual_section_body(text: str, title: str) -> str: + number_pattern = r"(?:\(\d+\)、|[零一二三四五六七八九十百]+、)" + pattern = re.compile(rf"^##\s+{number_pattern}\s*{re.escape(title)}\s*$", flags=re.M) + match = pattern.search(text) + if not match: + return "" + next_match = re.search(rf"^##\s+{number_pattern}", text[match.end() :], flags=re.M) + end = match.end() + next_match.start() if next_match else len(text) + return text[match.end() : end].strip() + + +def manual_quality_issues(text: str, modules: list[dict[str, Any]]) -> list[str]: + issues: list[str] = [] + required_sections = ["相关文档", "说明", "功能特点", "系统要求", "常见问题解答", "术语表"] + for title in required_sections: + if not manual_section_body(text, title): + issues.append(f"缺少通用手册章节:{title}") + if re.search(r"^##\s+\(\d+\)、", text, flags=re.M): + issues.append("章节标题仍使用括号数字,应使用中文大写序号") + related_body = manual_section_body(text, "相关文档") + if related_body and "| 文档名称 |" not in related_body: + issues.append("相关文档章节应使用表格指向配套文档") + for term in TECHNICAL_TERMS: + if term in text: + issues.append(f"存在偏技术表达:{term}") + for marker in TEMPLATE_MARKERS: + if marker in text: + issues.append(f"存在模板化表达:{marker}") + for marker in AI_TONE_MARKERS: + if marker in text: + issues.append(f"存在疑似 AI 味/空泛表达:{marker}") + if text.count("【截图预留:") < len(modules): + issues.append("截图预留数量少于核心模块数量") + list_lines = [ + line.strip() + for line in text.splitlines() + if re.match(r"^(?:[-*+]\s+|\d+\.\s+)", line.strip()) + ] + if list_lines: + issues.append(f"正文仍存在项目符号或编号列表:{list_lines[0][:40]}") + for module in modules: + title = str(module.get("feature") or "").strip() + if not title: + continue + body = manual_section_body(text, title) + if not body: + issues.append(f"缺少核心模块章节:{title}") + continue + if len(body) < 390: + issues.append(f"模块内容偏薄:{title}") + for label in ("进入方式:", "页面内容:", "操作步骤:", "操作规则:", "操作结果与反馈:"): + if label in body: + issues.append(f"模块仍使用制式小标题:{title} / {label}") + return issues + + +def clean_field(value: str, default: str) -> str: + text = plain_manual_text(str(value or "")).strip() + if not text or text == "待用户确认": + return default + return text + ("。" if not text.endswith(("。", "!", "?")) else "") + + +def as_text_list(value: Any) -> list[str]: + if value is None: + return [] + if isinstance(value, list): + return [plain_manual_text(str(item)).strip() for item in value if str(item).strip()] + text = plain_manual_text(str(value)).strip() + if not text: + return [] + return [item.strip() for item in re.split(r"[;;\n]+", text) if item.strip()] + + +def required_module_text(item: dict[str, Any], field: str, title: str) -> str: + value = plain_manual_text(str(item.get(field) or "")).strip() + if not value: + raise SystemExit( + "STOP_FOR_USER\n" + f"NEXT_ACTION: 操作手册页面模块“{title}”缺少 `{field}`。请回到业务理解阶段," + "由模型根据真实页面证据补全 manual_modules 后再生成操作手册。" + ) + return value + + +def required_module_list(item: dict[str, Any], field: str, title: str) -> list[str]: + values = as_text_list(item.get(field)) + if not values: + raise SystemExit( + "STOP_FOR_USER\n" + f"NEXT_ACTION: 操作手册页面模块“{title}”缺少 `{field}`。请回到业务理解阶段," + "由模型根据真实页面证据补全 manual_modules 后再生成操作手册。" + ) + return values + + +def normalize_manual_modules( + business: dict[str, Any] | None, + fallback_modules: list[dict[str, Any]], +) -> list[dict[str, Any]]: + manual_modules = business.get("manual_modules") if business else [] + if not manual_modules: + raise SystemExit( + "STOP_FOR_USER\n" + "NEXT_ACTION: 业务理解缺少 `manual_modules`。不要由脚本按 auth/query/form 等模板猜测操作手册。" + "请模型阅读项目真实页面、路由、按钮、输入项、提示和结果反馈,补全 manual_modules 后再生成操作手册。" + ) + + modules: list[dict[str, Any]] = [] + for index, item in enumerate(manual_modules, start=1): + if not isinstance(item, dict): + raise SystemExit( + "STOP_FOR_USER\n" + f"NEXT_ACTION: manual_modules 第 {index} 项不是对象,无法生成真实操作手册。请补全 title、purpose、entry、operation_steps、feedback 等字段。" + ) + title = plain_feature_name(item.get("title") or item.get("feature") or f"功能模块 {index}") + purpose = required_module_text(item, "purpose", title) + entry = required_module_text(item, "entry", title) + usage = plain_manual_text( + str(item.get("usage") or item.get("usage_scenario") or item.get("description") or "") + ).strip() + if not usage: + raise SystemExit( + "STOP_FOR_USER\n" + f"NEXT_ACTION: 操作手册页面模块“{title}”缺少 `usage` 或 `usage_scenario`。请回到业务理解阶段," + "补充用户在什么场景下会使用该页面、处理什么具体事务,再生成操作手册。" + ) + steps = required_module_list(item, "operation_steps", title) + feedback = required_module_list(item, "feedback", title) + screenshot_note = plain_manual_text(str(item.get("screenshot") or "")).strip() + if not screenshot_note: + screenshot_note = f"请在此处插入“{title}”页面或操作结果截图" + modules.append( + { + "feature": title, + "raw_feature": title, + "purpose": purpose + ("。" if not purpose.endswith(("。", "!", "?")) else ""), + "entry": entry + ("。" if not entry.endswith(("。", "!", "?")) else ""), + "usage": usage, + "visible_elements": as_text_list(item.get("visible_elements") or item.get("page_elements")), + "steps": steps, + "validation_rules": as_text_list(item.get("validation_rules") or item.get("rules") or item.get("limits")), + "feedback": feedback, + "result": ";".join(feedback), + "screenshot": f"【截图预留:{screenshot_note.strip('。')}。】", + } + ) + return modules + + +def normalize_system_requirements(business: dict[str, Any] | None) -> list[dict[str, str]]: + raw_items = business.get("system_requirements") if business else None + rows: list[dict[str, str]] = [] + if isinstance(raw_items, list): + for item in raw_items: + if isinstance(item, dict): + name = str(item.get("item") or item.get("name") or "").strip() + minimum = str(item.get("minimum") or item.get("min") or "").strip() + recommended = str(item.get("recommended") or item.get("recommend") or "").strip() + if name: + rows.append( + { + "item": plain_manual_text(name), + "minimum": plain_manual_text(minimum or "按实际部署环境配置"), + "recommended": plain_manual_text(recommended or minimum or "按实际部署环境配置"), + } + ) + if not rows: + raise SystemExit( + "STOP_FOR_USER\n" + "NEXT_ACTION: 业务理解缺少 `system_requirements`。请根据真实项目运行形态和已确认申请表环境补全后再生成操作手册。" + ) + return rows + + +def normalize_faq(business: dict[str, Any] | None, software_name: str) -> list[dict[str, str]]: + raw_items = business.get("faq") if business else None + items: list[dict[str, str]] = [] + if isinstance(raw_items, list): + for item in raw_items: + if isinstance(item, dict): + question = str(item.get("question") or item.get("q") or "").strip() + answer = str(item.get("answer") or item.get("a") or "").strip() + if question and answer: + items.append({"question": plain_manual_text(question), "answer": plain_manual_text(answer)}) + if not items: + raise SystemExit( + "STOP_FOR_USER\n" + "NEXT_ACTION: 业务理解缺少 `faq`。请根据当前软件真实使用场景补全常见问题后再生成操作手册。" + ) + return items + + +def normalize_glossary(business: dict[str, Any] | None, modules: list[dict[str, Any]], software_name: str) -> list[dict[str, str]]: + raw_items = business.get("glossary") if business else None + items: list[dict[str, str]] = [] + if isinstance(raw_items, list): + for item in raw_items: + if isinstance(item, dict): + term = str(item.get("term") or item.get("name") or "").strip() + definition = str(item.get("definition") or item.get("description") or "").strip() + if term and definition: + items.append({"term": plain_manual_text(term), "definition": plain_manual_text(definition)}) + if items: + return items + raise SystemExit( + "STOP_FOR_USER\n" + "NEXT_ACTION: 业务理解缺少 `glossary`。请根据当前软件真实业务对象和页面术语补全术语表后再生成操作手册。" + ) + + +def feature_phrase(modules: list[dict[str, Any]], limit: int = 5) -> str: + names = [module["feature"] for module in modules if module.get("feature")] + return "、".join(names[:limit]) if names else "主要业务处理" + + +def chinese_number(value: int) -> str: + digits = "零一二三四五六七八九" + if value <= 0: + return str(value) + if value < 10: + return digits[value] + if value == 10: + return "十" + if value < 20: + return "十" + digits[value % 10] + if value < 100: + tens, ones = divmod(value, 10) + return digits[tens] + "十" + (digits[ones] if ones else "") + return str(value) + + +def section_heading(index: int, title: str) -> str: + return f"## {chinese_number(index)}、{title}" + + +def strip_sentence_punctuation(text: str) -> str: + return str(text or "").strip().strip("。;;,, ") + + +def natural_join(items: list[str], limit: int | None = None) -> str: + values = [strip_sentence_punctuation(item) for item in items if strip_sentence_punctuation(item)] + if limit is not None: + values = values[:limit] + if not values: + return "" + if len(values) == 1: + return values[0] + return "、".join(values[:-1]) + "和" + values[-1] + + +def ensure_sentence(text: str) -> str: + value = strip_sentence_punctuation(plain_manual_text(text)) + if not value: + return "" + return value + "。" + + +def remove_opening_definition(text: str, software_name: str) -> str: + value = plain_manual_text(text).strip() + if not value: + return "" + sentences = re.findall(r"[^。!?]+[。!?]?", value) + if sentences and sentences[0].startswith(software_name) and "是一款" in sentences[0]: + sentences = sentences[1:] + return "".join(sentences).strip() + + +def flow_summary(flow: list[str], modules: list[dict[str, Any]]) -> str: + if flow: + pieces = [strip_sentence_punctuation(plain_manual_text(item)) for item in flow[:4]] + pieces = [item for item in pieces if item] + if pieces: + return ";".join(pieces) + "。" + names = [module["feature"] for module in modules[:4]] + if names: + return f"用户可依次使用{natural_join(names)}等页面完成主要工作。" + return "用户可按照页面提示完成主要业务操作。" + + +def describe_related_doc(name: str) -> str: + if "总体" in name: + return "说明软件整体功能、页面组成、运行环境和业务边界。" + if "详细" in name: + return "说明各功能页面、输入输出、状态变化和处理规则。" + if "测试" in name or "案例" in name: + return "记录主要功能的操作场景、预期结果和异常提示。" + return "记录与本软件功能、操作或验证相关的配套说明。" + + +def normalize_related_documents(business: dict[str, Any] | None) -> list[dict[str, str]]: + raw_items = business.get("related_documents") if business else None + rows: list[dict[str, str]] = [] + if isinstance(raw_items, list): + for item in raw_items: + if isinstance(item, dict): + name = str(item.get("name") or item.get("title") or item.get("document") or "").strip() + target = str(item.get("target") or item.get("path") or item.get("file") or "").strip() + description = str(item.get("description") or item.get("purpose") or "").strip() + if name: + rows.append( + { + "name": plain_manual_text(name), + "target": plain_manual_text(target or f"《{name}》"), + "description": plain_manual_text(description or describe_related_doc(name)), + } + ) + elif str(item).strip(): + name = str(item).strip() + rows.append( + { + "name": plain_manual_text(name), + "target": f"《{plain_manual_text(name)}》", + "description": describe_related_doc(name), + } + ) + if not rows: + for name in ("总体设计", "详细设计", "测试案例"): + rows.append({"name": name, "target": f"《{name}》", "description": describe_related_doc(name)}) + return rows + + +def clean_purpose_text(feature: str, purpose: str) -> str: + value = strip_sentence_punctuation(plain_manual_text(purpose)) + value = re.sub(rf"^{re.escape(feature)}(页面|功能|模块|环节)?(主要)?用于", "", value) + value = re.sub(r"^[^,。;;]{1,30}(页面|功能|模块|环节|状态栏|面板)?(主要)?用于", "", value) + value = re.sub(r"^用于", "", value) + value = value.strip("。;;,, ") + return value or "完成本页面相关操作" + + +def page_label(feature: str) -> str: + value = strip_sentence_punctuation(feature) + if value.startswith("用户") and len(value) > 2: + value = value[2:] + return value + + +def purpose_core_sentence(feature: str, purpose: str) -> str: + value = clean_purpose_text(feature, purpose) + label = page_label(feature) + if re.match(r"^(展示|集中展示|承载|提供|处理|保存|记录|辅助)", value): + return f"{label}页面{value}" + if value.startswith("让用户"): + return f"{label}页面{value}" + return f"用户可在{label}页面{value}" + + +def purpose_sentence(feature: str, purpose: str) -> str: + return purpose_core_sentence(feature, purpose) + "。" + + +def entry_sentence(entry: str) -> str: + value = strip_sentence_punctuation(plain_manual_text(entry)) + if not value: + return "" + if value.startswith("用户"): + return value + "。" + if re.match(r"^(登录|创建|进入|打开|点击|完成|选择|提交)", value): + return f"用户{value}。" + if value.startswith("当"): + return value + "。" + return f"用户可以通过{value}。" + + +def visible_elements_sentence(items: list[str], feature: str, index: int) -> str: + value = natural_join(items, limit=8) + if not value: + return "" + variants = [ + f"页面上主要呈现{value}等内容,这些内容用于帮助用户确认当前位置和可执行操作。", + f"用户在{feature}页面会看到{value}等信息,并可依据页面显示继续处理。", + f"该部分提供{value}等页面内容,用户可据此查看状态、填写信息或选择下一步操作。", + ] + return variants[(index - 1) % len(variants)] + + +def steps_sentence(steps: list[str], module_index: int) -> str: + values = [strip_sentence_punctuation(step) for step in steps if strip_sentence_punctuation(step)] + if not values: + return "" + connectors = ["先", "随后", "接着", "之后", "再", "继续"] + parts: list[str] = [] + for step_index, step in enumerate(values): + if step_index == len(values) - 1 and len(values) > 1: + connector = "最后" + else: + connector = connectors[min(step_index, len(connectors) - 1)] + parts.append(f"{connector}{step}") + prefixes = ["实际操作时,用户", "使用该功能时,用户", "在该页面中,用户"] + return prefixes[(module_index - 1) % len(prefixes)] + ",".join(parts) + "。" + + +def rules_feedback_sentence(rules: list[str], feedback: list[str], index: int) -> str: + parts: list[str] = [] + rule_text = natural_join(rules, limit=6) + if rule_text: + rule_templates = [ + f"操作过程中需要注意{rule_text}。", + f"页面会按照{rule_text}等规则限制或提示用户。", + f"如果不满足{rule_text}等要求,用户需要根据页面提示调整后再继续。", + ] + parts.append(rule_templates[(index - 1) % len(rule_templates)]) + feedback_text = natural_join(feedback, limit=6) + if feedback_text: + feedback_templates = [ + f"操作完成后,系统会显示{feedback_text}。", + f"处理结束后,用户可以看到{feedback_text}。", + f"页面反馈通常包括{feedback_text}。", + ] + parts.append(feedback_templates[(index - 1) % len(feedback_templates)]) + return "".join(parts) + + +def feature_paragraph(module: dict[str, Any], index: int) -> str: + feature = module["feature"] + purpose = clean_purpose_text(feature, module.get("purpose") or "") + label = page_label(feature) + core = purpose_core_sentence(feature, module.get("purpose") or "") + elements = natural_join(as_text_list(module.get("visible_elements")), limit=5) + feedback = natural_join(as_text_list(module.get("feedback")), limit=3) + variants = [ + f"{core}。页面上的{elements or '相关业务信息'}会集中呈现当前可操作内容,用户处理完成后可以看到{feedback or '相应的处理结果'}。", + f"在{label}页面中,用户主要处理{purpose}。系统把{elements or '页面显示内容'}放在当前操作区域,处理结束后会反馈{feedback or '处理结果'}。", + f"{label}页面关注的是{purpose}。用户通过{elements or '必要的页面信息'}确认当前状态,并在操作结束后获得{feedback or '当前状态反馈'}。", + ] + return variants[(index - 1) % len(variants)] + + +def tidy_manual_output(text: str) -> str: + replacements = { + "用户主要处理处理": "用户主要处理", + "主要处理承载一次": "主要围绕一次", + "用户可以看到用户可以看到": "用户可以看到", + "处理结束后会反馈空对话": "处理结束后会显示空对话", + "在AI ": "在 AI ", + "把StudioAgent": "把 StudioAgent", + "页面上的StudioAgent": "页面上的 StudioAgent", + "看到StudioAgent": "看到 StudioAgent", + "提供StudioAgent": "提供 StudioAgent", + "进入StudioAgent": "进入 StudioAgent", + "保证StudioAgent": "保证 StudioAgent", + } + value = text + for source, target in replacements.items(): + value = value.replace(source, target) + value = re.sub(r"(?<=[\u4e00-\u9fff])([A-Za-z][A-Za-z0-9.+-]*)(?=[\u4e00-\u9fff])", r" \1 ", value) + value = re.sub(r" {2,}", " ", value) + return value + + +def append_modules_canonical(lines: list[str], modules: list[dict[str, Any]], start_index: int) -> int: + for i, module in enumerate(modules, start=start_index): + visible_elements = as_text_list(module.get("visible_elements")) + validation_rules = as_text_list(module.get("validation_rules")) + feedback = as_text_list(module.get("feedback")) or [module["result"]] + lines.extend( + [ + section_heading(i, module["feature"]), + "", + purpose_sentence(module["feature"], module["purpose"]) + entry_sentence(module["entry"]), + "", + ] + ) + if module.get("usage"): + lines.extend([ensure_sentence(module["usage"]), ""]) + element_text = visible_elements_sentence(visible_elements, module["feature"], i) + if element_text: + lines.extend([element_text, ""]) + step_text = steps_sentence(module["steps"], i) + if step_text: + lines.extend([step_text, ""]) + rule_feedback = rules_feedback_sentence(validation_rules, feedback, i) + if rule_feedback: + lines.extend([rule_feedback, ""]) + lines.extend(["", module["screenshot"], ""]) + return start_index + len(modules) + + +def append_flow_canonical(lines: list[str], software_name: str, flow: list[str], start_index: int) -> int: + lines.extend( + [ + section_heading(start_index, "典型使用流程"), + "", + f"用户完成一次完整业务时,通常先进入{software_name},再选择或创建业务对象,随后按照页面提示处理内容并查看结果。", + "", + flow_summary(flow, []), + "", + ] + ) + return start_index + 1 + + +def render_manual_canonical( + software_name: str, + version: str, + industry: str, + users: list[str], + positioning: str, + core_value: str, + modules: list[dict[str, Any]], + operation_flow: list[str], + manual_sections: list[Any] | None = None, + business: dict[str, Any] | None = None, +) -> str: + industry_text = "相关业务" if not industry or industry == "待用户确认" else industry + user_text = join_items([user for user in users if user != "待用户确认"]) or "实际使用人员" + positioning_text = remove_opening_definition(positioning, software_name) + core_value_text = clean_field(core_value, "软件可以帮助用户统一处理相关业务资料,并减少重复操作。") + flow = operation_flow + related_documents = normalize_related_documents(business) + system_rows = normalize_system_requirements(business) + faq_items = normalize_faq(business, software_name) + glossary_items = normalize_glossary(business, modules, software_name) + overview_paragraphs: list[str] = [] + for section in manual_sections or []: + if isinstance(section, dict) and section.get("paragraphs") and len(overview_paragraphs) < 4: + overview_paragraphs.extend(as_text_list(section.get("paragraphs"))[:2]) + + lines = [f"# {software_name}操作手册", "", section_heading(1, "相关文档"), ""] + lines.extend(["| 文档名称 | 指向资料 | 说明 |", "| --- | --- | --- |"]) + for item in related_documents: + lines.append(f"| {item['name']} | {item['target']} | {item['description']} |") + lines.extend( + [ + "", + section_heading(2, "说明"), + "", + f"{software_name} {version}适用于{industry_text}场景。用户进入系统后,可以围绕实际工作内容完成账号进入、业务创建、过程查看、结果确认和资料管理等操作。", + "", + f"日常使用时,{user_text}可以按照页面提示从入口进入相应页面,查看当前业务状态,并根据页面中的按钮、输入框、列表或弹窗继续处理。{core_value_text}", + "", + ] + ) + if positioning_text: + lines.extend([positioning_text, ""]) + for paragraph in overview_paragraphs: + lines.extend([paragraph, ""]) + lines.extend( + [ + "本手册用于说明软件的用途、功能特点、运行要求和页面操作流程。各功能章节按用户能够看到的页面、入口、按钮、输入项、提示信息和处理结果进行说明。", + "", + section_heading(3, "功能特点"), + "", + ] + ) + for i, module in enumerate(modules[:8], start=1): + lines.extend([feature_paragraph(module, i), ""]) + lines.extend([section_heading(4, "系统要求"), "", "| 系统要求 | 最低配置 | 推荐配置 |", "| --- | --- | --- |"]) + for row in system_rows: + lines.append(f"| {row['item']} | {row['minimum']} | {row['recommended']} |") + lines.extend( + [ + "", + f"请确保实际运行环境满足以上要求,以保证{software_name}能够正常打开页面、提交操作和展示处理结果。若部署方式、客户端形态或服务器环境与本表不同,应以实际确认的申请表环境字段为准。", + "", + ] + ) + next_index = append_modules_canonical(lines, modules, start_index=5) + if flow: + next_index = append_flow_canonical(lines, software_name, flow, start_index=next_index) + lines.extend([section_heading(next_index, "常见问题解答"), ""]) + for item in faq_items: + lines.extend([f"问题:{item['question']}", f"解决方法:{item['answer']}", ""]) + next_index += 1 + lines.extend([section_heading(next_index, "术语表"), "", "| 术语 | 解释 |", "| --- | --- |"]) + for item in glossary_items: + lines.append(f"| {item['term']} | {item['definition']} |") + lines.append("") + append_stop(lines) + return tidy_manual_output("\n".join(lines)) + + +def append_stop(lines: list[str]) -> None: + lines.extend( + [ + "```text", + "STOP_FOR_USER", + "NEXT_ACTION: 请一次性确认完整操作手册草稿是否符合真实业务;必要时先统一修改段落内容,再运行 confirm_stage.py --stage markdown。", + "```", + "", + ] + ) + + +def build_manual_text( + analysis: dict[str, Any], + software_name: str, + version: str, + business: dict[str, Any] | None = None, +) -> tuple[str, list[dict[str, Any]], list[dict[str, Any]]]: + positioning = plain_manual_text(business.get("product_positioning") if business else f"{software_name} {version}是一款基于项目实际功能整理的软件系统。") + core_value = plain_manual_text(business.get("core_value") if business else "系统通过清晰的软件界面为用户提供主要业务入口,支持用户完成信息查看、业务处理、数据维护和结果反馈等操作。") + users = business.get("target_users") if business else ["业务用户"] + operation_flow = business.get("operation_flow") if business else [] + manual_sections = business.get("manual_sections") if business else [] + industry = business.get("industry") if business else "业务应用" + if positioning.rstrip("。") == software_name.rstrip("。"): + positioning = "用户可以根据项目资料中体现的业务场景完成相应操作。" + elif not positioning.endswith("。"): + positioning += "。" + modules = normalize_manual_modules(business, []) + records: list[dict[str, Any]] = [] + + text = render_manual_canonical(software_name, version, industry, users, positioning, core_value, modules, operation_flow, manual_sections, business) + records.append({"round": 1, "action": "初稿生成", "issues": manual_quality_issues(text, modules)}) + + text = render_manual_canonical(software_name, version, industry, users, positioning, core_value, modules, operation_flow, manual_sections, business) + records.append({"round": 2, "action": "真实页面字段复核", "issues": manual_quality_issues(text, modules)}) + + text = render_manual_canonical(software_name, version, industry, users, positioning, core_value, modules, operation_flow, manual_sections, business) + records.append({"round": 3, "action": "制式模板和 AI 味复核", "issues": manual_quality_issues(text, modules)}) + + for round_no in range(4, 7): + issues = records[-1]["issues"] + if not issues: + break + text = render_manual_canonical(software_name, version, industry, users, positioning, core_value, modules, operation_flow, manual_sections, business) + records.append( + { + "round": round_no, + "action": "复核仍需模型回到业务理解补写", + "issues": manual_quality_issues(text, modules), + } + ) + break + return text, records, modules + + +def write_review_records(out_dir: Path, records: list[dict[str, Any]], modules: list[dict[str, Any]]) -> None: + (out_dir / "操作手册自检记录.json").write_text( + json.dumps({"rounds": records, "module_count": len(modules)}, ensure_ascii=False, indent=2), + encoding="utf-8", + ) + lines = ["# 操作手册自检记录", ""] + for record in records: + lines.extend([f"## 第 {record['round']} 轮:{record['action']}", ""]) + if record["issues"]: + lines.extend(f"- {issue}" for issue in record["issues"]) + else: + lines.append("- 未发现需继续修正的问题") + lines.append("") + lines.extend(["## 模块清单", ""]) + lines.extend(f"- {module['feature']}" for module in modules) + (out_dir / "操作手册自检记录.md").write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def write_manual(path: Path, analysis: dict[str, Any], software_name: str, version: str, business: dict[str, Any] | None = None) -> list[dict[str, Any]]: + text, records, modules = build_manual_text(analysis, software_name, version, business) + path.write_text(text, encoding="utf-8") + write_review_records(path.parent, records, modules) + return records + + +def require_confirmed_business(business: dict[str, Any] | None) -> None: + if business is None: + raise SystemExit( + "STOP_FOR_USER\n" + "NEXT_ACTION: 操作手册必须基于已确认的业务理解生成。请先生成并确认 草稿/业务理解.md。" + ) + if business.get("confirmation_required") and not business.get("user_confirmed"): + raise SystemExit( + "STOP_FOR_USER\n" + "NEXT_ACTION: 业务理解尚未确认。请先确认 草稿/业务理解.md," + "再运行 `python3 /scripts/confirm_stage.py --workdir 软件著作权申请资料 --stage business --note \"<用户确认内容>\"`。" + ) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--analysis", required=True) + parser.add_argument("--software-name", required=True) + parser.add_argument("--version", default="V1.0") + parser.add_argument("--business-context", help="Business context JSON generated before manual drafting") + parser.add_argument("--out-dir", default="软件著作权申请资料/草稿") + args = parser.parse_args() + + analysis = read_json(Path(args.analysis)) + business = read_json(Path(args.business_context)) if args.business_context else None + require_confirmed_business(business) + out_dir = ensure_dir(Path(args.out_dir)) + out_path = out_dir / "操作手册.md" + records = write_manual(out_path, analysis, args.software_name, args.version, business) + print(f"OK manual draft: {out_path}") + print(f"OK manual self-review: {out_dir / '操作手册自检记录.md'}") + for record in records: + print(f"Review round {record['round']}: {record['action']} issues={len(record['issues'])}") + if records[-1]["issues"]: + print("STOP_FOR_USER") + print("NEXT_ACTION: 操作手册自检仍有问题。请回到业务理解阶段补全 manual_modules 中的真实页面内容、操作规则和结果反馈后再重新生成。") + raise SystemExit(1) + print("STOP_FOR_USER") + print("NEXT_ACTION: 请一次性确认完整操作手册草稿是否符合真实业务;必要时先统一修改段落内容,再运行 confirm_stage.py --stage markdown。") + + +if __name__ == "__main__": + main() diff --git a/software-copyright-materials/scripts/propose_code_selection.py b/software-copyright-materials/scripts/propose_code_selection.py new file mode 100644 index 0000000..3bd8909 --- /dev/null +++ b/software-copyright-materials/scripts/propose_code_selection.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +"""Create an editable source-file evidence list before code extraction.""" + +from __future__ import annotations + +import argparse +from pathlib import Path +from typing import Any + +from common import COPYRIGHT_CODE_EXTS, FRONTEND_EXTS, ensure_dir, is_known_config_file, iter_project_files, read_json, rel, write_json +from extract_code_material import LINES_PER_PAGE, SPLIT_THRESHOLD_PAGES, category_weight, material_code_lines, should_skip_file + + +DEFAULT_MAX_FILES = 0 + + +def evidence_for(path: Path, project: Path) -> str: + priority, _ = category_weight(path, project) + if priority == 0: + return "入口文件证据" + if priority == 10: + return "路由文件证据" + if priority == 20: + return "页面文件证据" + if priority == 30: + return "数据交互文件证据" + if priority == 40: + return "状态或数据文件证据" + if priority == 50: + return "页面组成文件证据" + if priority == 60: + return "通用能力文件证据" + if priority == 90: + return "样式文件证据" + if path.suffix.lower() not in FRONTEND_EXTS: + return "补充源码证据" + return "普通源码文件" + + +def build_candidates(project: Path) -> list[dict[str, Any]]: + files = [p for p in iter_project_files(project, COPYRIGHT_CODE_EXTS) if not should_skip_file(p) and not is_known_config_file(p)] + files.sort(key=lambda p: category_weight(p, project)) + candidates: list[dict[str, Any]] = [] + for path in files: + try: + text = path.read_text(encoding="utf-8", errors="replace") + line_count = len(text.splitlines()) + material_line_count = len(material_code_lines(text)) + except Exception: + line_count = 0 + material_line_count = 0 + priority, _ = category_weight(path, project) + candidates.append( + { + "path": rel(path, project), + "selected": False, + "line_count": line_count, + "material_line_count": material_line_count, + "priority": priority, + "selection_tier": "frontend" if path.suffix.lower() in FRONTEND_EXTS else "supplement", + "evidence": evidence_for(path, project), + "model_reason": "", + } + ) + return candidates + + +def selected_line_estimate(item: dict[str, Any]) -> int: + total = int(item.get("material_line_count") or item.get("line_count") or 0) + return total + 1 if total > 0 else 0 + + +def selection_stats(candidates: list[dict[str, Any]]) -> dict[str, int]: + selected_items = [item for item in candidates if item.get("selected")] + return { + "selected_count": len(selected_items), + "selected_lines": sum(selected_line_estimate(item) for item in selected_items), + } + + +def all_candidate_lines(candidates: list[dict[str, Any]]) -> int: + return sum(selected_line_estimate(item) for item in candidates) + + +def write_selection_md(path: Path, data: dict[str, Any]) -> None: + lines = [ + "# 代码文件候选清单", + "", + "请先确认要抽取哪些源码文件,再运行代码材料抽取。", + "", + "本清单只列出候选源码证据,不默认决定抽取文件。", + "模型需要先理解项目业务、页面入口和源码职责,再填写 `selected/model_reason`。", + f"当前已选约 {data['estimated_selected_pages']} 页,全部候选源码约 {data['estimated_all_candidate_pages']} 页。", + "", + "```text", + "STOP_FOR_USER", + "NEXT_ACTION: 请由模型先填写 草稿/代码文件选择.json 的抽取选择和选择理由,再让用户确认;确认后运行 confirm_stage.py --stage code-selection。", + "```", + "", + "确认方式:", + "", + "1. 模型根据项目业务和代码入口选择最能体现软件功能的文件。", + "2. 把需要抽取的文件设为 `selected: true`,并填写 `model_reason`。", + "3. 代码材料按完整文件抽取并去除纯空行,不支持只抽取某个文件的中间行段。", + "4. 用户确认模型选择后,再记录 `code-selection` 门禁。", + "", + "## 默认选中文件", + "", + "| 文件 | 行数 | 模型选择理由 |", + "| --- | ---: | --- |", + ] + for item in data["files"]: + if item.get("selected"): + lines.append(f"| `{item['path']}` | {item['line_count']} | {item.get('model_reason') or '待模型填写'} |") + + lines.extend(["", "## 未选候选文件", "", "| 文件 | 行数 | 证据类型 |", "| --- | ---: | --- |"]) + for item in data["files"]: + if not item.get("selected"): + lines.append(f"| `{item['path']}` | {item['line_count']} | {item['evidence']} |") + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--project", required=True) + parser.add_argument("--analysis", help="Optional project analysis JSON; retained for workflow traceability") + parser.add_argument("--out-dir", default="软件著作权申请资料/草稿") + parser.add_argument("--max-files", type=int, default=DEFAULT_MAX_FILES, help="Only limits candidate inventory size; does not auto-select files") + parser.add_argument("--target-pages", type=int, default=SPLIT_THRESHOLD_PAGES) + parser.add_argument("--lines-per-page", type=int, default=LINES_PER_PAGE) + args = parser.parse_args() + + project = Path(args.project) + if not project.exists(): + raise SystemExit(f"Project not found: {project}") + if args.analysis and not Path(args.analysis).exists(): + raise SystemExit(f"Analysis JSON not found: {args.analysis}") + + out_dir = ensure_dir(Path(args.out_dir)) + candidates = build_candidates(project) + target_lines = max(1, args.target_pages) * max(1, args.lines_per_page) + if args.max_files: + candidates = candidates[: args.max_files] + stats = selection_stats(candidates) + candidate_lines = all_candidate_lines(candidates) + selected_pages = (stats["selected_lines"] + args.lines_per_page - 1) // args.lines_per_page if stats["selected_lines"] else 0 + all_pages = (candidate_lines + args.lines_per_page - 1) // args.lines_per_page if candidate_lines else 0 + data = { + "project_root": str(project.resolve()), + "selection_required": True, + "model_selection_required": True, + "confirmation_required": True, + "user_confirmed": False, + "target_pages": args.target_pages, + "lines_per_page": args.lines_per_page, + "target_lines": target_lines, + "estimated_selected_lines": stats["selected_lines"], + "estimated_selected_pages": selected_pages, + "estimated_all_candidate_lines": candidate_lines, + "estimated_all_candidate_pages": all_pages, + "supplement_rule": "模型优先选择能体现软件核心功能和真实运行逻辑的源码;不足60页时再从其他相关源码中补充;候选源码仍不足时才生成全部代码材料。", + "confirmation_stage": "code-selection", + "next_action": "请由模型填写 草稿/代码文件选择.json 的抽取选择和选择理由,再让用户确认;确认后运行 confirm_stage.py --stage code-selection。", + "instructions": "The script only inventories source files. The model must choose selected/model_reason before user confirmation. Selected files are extracted in full with blank-only lines removed.", + "files": candidates, + } + write_json(out_dir / "代码文件选择.json", data) + write_selection_md(out_dir / "代码文件候选清单.md", data) + selected_count = sum(1 for item in candidates if item.get("selected")) + print(f"OK code selection draft: {out_dir}") + print(f"Candidates: {len(candidates)}") + print(f"Model selected: {selected_count}") + print(f"Estimated selected pages: {selected_pages}") + print(f"Estimated all candidate pages: {all_pages}") + print("STOP_FOR_USER") + print(f"NEXT_ACTION: {data['next_action']}") + + +if __name__ == "__main__": + main() diff --git a/software-copyright-materials/vendor/docx-toolkit/.gitignore b/software-copyright-materials/vendor/docx-toolkit/.gitignore new file mode 100644 index 0000000..59072c5 --- /dev/null +++ b/software-copyright-materials/vendor/docx-toolkit/.gitignore @@ -0,0 +1,3 @@ +obj/ +bin/ +*.user diff --git a/software-copyright-materials/vendor/docx-toolkit/LICENSE b/software-copyright-materials/vendor/docx-toolkit/LICENSE new file mode 100644 index 0000000..1019c72 --- /dev/null +++ b/software-copyright-materials/vendor/docx-toolkit/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 DocxToolkit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/software-copyright-materials/vendor/docx-toolkit/SKILL.md b/software-copyright-materials/vendor/docx-toolkit/SKILL.md new file mode 100644 index 0000000..c7090af --- /dev/null +++ b/software-copyright-materials/vendor/docx-toolkit/SKILL.md @@ -0,0 +1,274 @@ +--- +name: docx-toolkit +license: MIT +metadata: + version: "1.0.0" + category: document-processing + author: DocxToolkit + sources: + - "ECMA-376 Office Open XML File Formats" + - "GB/T 9704-2012 Layout Standard for Official Documents" + - "IEEE / ACM / APA / MLA / Chicago / Turabian Style Guides" + - "Springer LNCS / Nature / HBR Document Templates" +description: > + Professional DOCX document creation, editing, and formatting using OpenXML SDK (.NET). + Three pipelines: (A) create new documents from scratch, (B) fill/edit content in existing + documents, (C) apply template formatting with XSD validation gate-check. + MUST use this skill whenever the user wants to produce, modify, or format a Word document — + including when they say "write a report", "draft a proposal", "make a contract", + "fill in this form", "reformat to match this template", or any task whose final output + is a .docx file. Even if the user doesn't mention "docx" explicitly, if the task + implies a printable/formal document, use this skill. +triggers: + - Word + - docx + - document + - 文档 + - Word文档 + - 报告 + - 合同 + - 公文 + - 排版 + - 套模板 +--- + +# docx-toolkit + +Create, edit, and format DOCX documents via CLI tools or direct C# scripts built on OpenXML SDK (.NET). + +## Setup + +**First time:** `bash scripts/setup.sh` (or `powershell scripts/setup.ps1` on Windows, `--minimal` to skip optional deps). + +**First operation in session:** `scripts/env_check.sh` — do not proceed if `NOT READY`. (Skip on subsequent operations within the same session.) + +## Quick Start: Direct C# Path + +When the task requires structural document manipulation (custom styles, complex tables, multi-section layouts, headers/footers, TOC, images), write C# directly instead of wrestling with CLI limitations. Use this scaffold: + +```csharp +// File: scripts/dotnet/task.csx (or a new .cs in a Console project) +// dotnet run --project scripts/dotnet/DocxToolkit.Cli -- run-script task.csx +#r "nuget: DocumentFormat.OpenXml, 3.2.0" + +using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Wordprocessing; + +using var doc = WordprocessingDocument.Create("output.docx", WordprocessingDocumentType.Document); +var mainPart = doc.AddMainDocumentPart(); +mainPart.Document = new Document(new Body()); + +// --- Your logic here --- +// Read the relevant Samples/*.cs file FIRST for tested patterns. +// See Samples/ table in References section below. +``` + +**Before writing any C#, read the relevant `Samples/*.cs` file** — they contain compilable, SDK-version-verified patterns. The Samples table in the References section below maps topics to files. + +## CLI shorthand + +All CLI commands below use `$CLI` as shorthand for: +```bash +dotnet run --project scripts/dotnet/DocxToolkit.Cli -- +``` + +## Pipeline routing + +Route by checking: does the user have an input .docx file? + +``` +User task +├─ No input file → Pipeline A: CREATE +│ signals: "write", "create", "draft", "generate", "new", "make a report/proposal/memo" +│ → Read references/scenario_a_create.md +│ +└─ Has input .docx + ├─ Replace/fill/modify content → Pipeline B: FILL-EDIT + │ signals: "fill in", "replace", "update", "change text", "add section", "edit" + │ → Read references/scenario_b_edit_content.md + │ + └─ Reformat/apply style/template → Pipeline C: FORMAT-APPLY + signals: "reformat", "apply template", "restyle", "match this format", "套模板", "排版" + ├─ Template is pure style (no content) → C-1: OVERLAY (apply styles to source) + └─ Template has structure (cover/TOC/example sections) → C-2: BASE-REPLACE + (use template as base, replace example content with user content) + → Read references/scenario_c_apply_template.md +``` + +If the request spans multiple pipelines, run them sequentially (e.g., Create then Format-Apply). + +## Pre-processing + +Convert `.doc` → `.docx` if needed: `scripts/doc_to_docx.sh input.doc output_dir/` + +Preview before editing (avoids reading raw XML): `scripts/docx_preview.sh document.docx` + +Analyze structure for editing scenarios: `$CLI analyze --input document.docx` + +## Scenario A: Create + +Read `references/scenario_a_create.md`, `references/typography_guide.md`, and `references/design_principles.md` first. Pick an aesthetic recipe from `Samples/AestheticRecipeSamples.cs` that matches the document type — do not invent formatting values. For CJK, also read `references/cjk_typography.md`. + +**Choose your path:** +- **Simple** (plain text, minimal formatting): use CLI — `$CLI create --type report --output out.docx --config content.json` +- **Structural** (custom styles, multi-section, TOC, images, complex tables): write C# directly. Read the relevant `Samples/*.cs` first. + +CLI options: `--type` (report|letter|memo|academic), `--title`, `--author`, `--page-size` (letter|a4|legal|a3), `--margins` (standard|narrow|wide), `--header`, `--footer`, `--page-numbers`, `--toc`, `--content-json`. + +Then run the **validation pipeline** (below). + +## Scenario B: Edit / Fill + +Read `references/scenario_b_edit_content.md` first. Preview → analyze → edit → validate. + +**Choose your path:** +- **Simple** (text replacement, placeholder fill): use CLI subcommands. +- **Structural** (add/reorganize sections, modify styles, manipulate tables, insert images): write C# directly. Read `references/openxml_element_order.md` and the relevant `Samples/*.cs`. + +Available CLI edit subcommands: +- `replace-text --find "X" --replace "Y"` +- `fill-placeholders --data '{"key":"value"}'` +- `fill-table --data table.json` +- `insert-section`, `remove-section`, `update-header-footer` + +```bash +$CLI edit replace-text --input in.docx --output out.docx --find "OLD" --replace "NEW" +$CLI edit fill-placeholders --input in.docx --output out.docx --data '{"name":"John"}' +``` + +Then run the **validation pipeline**. Also run diff to verify minimal changes: +```bash +$CLI diff --before in.docx --after out.docx +``` + +## Scenario C: Apply Template + +Read `references/scenario_c_apply_template.md` first. Preview and analyze both source and template. + +```bash +$CLI apply-template --input source.docx --template template.docx --output out.docx +``` + +For complex template operations (multi-template merge, per-section headers/footers, style merging), write C# directly — see Critical Rules below for required patterns. + +Run the **validation pipeline**, then the **hard gate-check**: +```bash +$CLI validate --input out.docx --gate-check assets/xsd/business-rules.xsd +``` +Gate-check is a **hard requirement**. Do NOT deliver until it passes. If it fails: diagnose, fix, re-run. + +Also diff to verify content preservation: `$CLI diff --before source.docx --after out.docx` + +## Validation pipeline + +Run after every write operation. For Scenario C the full pipeline is **mandatory**; for A/B it is **recommended** (skip only if the operation was trivially simple). + +```bash +$CLI merge-runs --input doc.docx # 1. consolidate runs +$CLI validate --input doc.docx --xsd assets/xsd/wml-subset.xsd # 2. XSD structure +$CLI validate --input doc.docx --business # 3. business rules +``` + +If XSD fails, auto-repair and retry: +```bash +$CLI fix-order --input doc.docx +$CLI validate --input doc.docx --xsd assets/xsd/wml-subset.xsd +``` + +If XSD still fails, fall back to business rules + preview: +```bash +$CLI validate --input doc.docx --business +scripts/docx_preview.sh doc.docx +# Verify: font contamination=0, table count correct, drawing count correct, sectPr count correct +``` + +Final preview: `scripts/docx_preview.sh doc.docx` + +## Critical rules + +These prevent file corruption — OpenXML is strict about element ordering. + +**Element order** (properties always first): + +| Parent | Order | +|--------|-------| +| `w:p` | `pPr` → runs | +| `w:r` | `rPr` → `t`/`br`/`tab` | +| `w:tbl`| `tblPr` → `tblGrid` → `tr` | +| `w:tr` | `trPr` → `tc` | +| `w:tc` | `tcPr` → `p` (min 1 ``) | +| `w:body` | block content → `sectPr` (LAST child) | + +**Direct format contamination:** When copying content from a source document, inline `rPr` (fonts, color) and `pPr` (borders, shading, spacing) override template styles. Always strip direct formatting — keep only `pStyle` reference and `t` text. Clean tables too (including `pPr/rPr` inside cells). + +**Track changes:** `` uses ``, never ``. `` uses ``, never ``. + +**Font size:** `w:sz` = points × 2 (12pt → `sz="24"`). Margins/spacing in DXA (1 inch = 1440, 1cm ≈ 567). + +**Heading styles MUST have OutlineLevel:** When defining heading styles (Heading1, ThesisH1, etc.), always include `new OutlineLevel { Val = N }` in `StyleParagraphProperties` (H1→0, H2→1, H3→2). Without this, Word sees them as plain styled text — TOC and navigation pane won't work. + +**Multi-template merge:** When given multiple template files (font, heading, breaks), read `references/scenario_c_apply_template.md` section "Multi-Template Merge" FIRST. Key rules: +- Merge styles from all templates into one styles.xml. Structure (sections/breaks) comes from the breaks template. +- Each content paragraph must appear exactly ONCE — never duplicate when inserting section breaks. +- NEVER insert empty/blank paragraphs as padding or section separators. Output paragraph count must equal input. Use section break properties (`w:sectPr` inside `w:pPr`) and style spacing (`w:spacing` before/after) for visual separation. +- Insert oddPage section breaks before EVERY chapter heading, not just the first. Even if a chapter has dual-column content, it MUST start with oddPage; use a second continuous break after the heading for column switching. +- Dual-column chapters need THREE section breaks: (1) oddPage in preceding para's pPr, (2) continuous+cols=2 in the chapter HEADING's pPr, (3) continuous+cols=1 in the last body para's pPr to revert. +- Copy `titlePg` settings from the breaks template for EACH section. Abstract and TOC sections typically need `titlePg=true`. + +**Multi-section headers/footers:** Templates with 10+ sections (e.g., Chinese thesis) have DIFFERENT headers/footers per section (Roman vs Arabic page numbers, different header text per zone). Rules: +- Use C-2 Base-Replace: copy the TEMPLATE as output base, then replace body content. This preserves all sections, headers, footers, and titlePg settings automatically. +- NEVER recreate headers/footers from scratch — copy template header/footer XML byte-for-byte. +- NEVER add formatting (borders, alignment, font size) not present in the template header XML. +- Non-cover sections MUST have header/footer XML files (at least empty header + page number footer). +- See `references/scenario_c_apply_template.md` section "Multi-Section Header/Footer Transfer". + +## References + +Load as needed — don't load all at once. Pick the most relevant files for the task. + +**The C# samples and design references below are the project's knowledge base ("encyclopedia").** When writing OpenXML code, ALWAYS read the relevant sample file first — it contains compilable, SDK-version-verified patterns that prevent common errors. When making aesthetic decisions, read the design principles and recipe files — they encode tested, harmonious parameter sets from authoritative sources (IEEE, ACM, APA, Nature, etc.), not guesses. + +### Scenario guides (read first for each pipeline) + +| File | When | +|------|------| +| `references/scenario_a_create.md` | Pipeline A: creating from scratch | +| `references/scenario_b_edit_content.md` | Pipeline B: editing existing content | +| `references/scenario_c_apply_template.md` | Pipeline C: applying template formatting | + +### C# code samples (compilable, heavily commented — read when writing code) + +| File | Topic | +|------|-------| +| `Samples/DocumentCreationSamples.cs` | Document lifecycle: create, open, save, streams, doc defaults, settings, properties, page setup, multi-section | +| `Samples/StyleSystemSamples.cs` | Styles: Normal/Heading chain, character/table/list styles, DocDefaults, latentStyles, CJK 公文, APA 7th, import, resolve inheritance | +| `Samples/CharacterFormattingSamples.cs` | RunProperties: fonts, size, bold/italic, all underlines, color, highlight, strike, sub/super, caps, spacing, shading, border, emphasis marks | +| `Samples/ParagraphFormattingSamples.cs` | ParagraphProperties: justification, indentation, line/paragraph spacing, keep/widow, outline level, borders, tabs, numbering, bidi, frame | +| `Samples/TableSamples.cs` | Tables: borders, grid, cell props, margins, row height, header repeat, merge (H+V), nested, floating, three-line 三线表, zebra striping | +| `Samples/HeaderFooterSamples.cs` | Headers/footers: page numbers, "Page X of Y", first/even/odd, logo image, table layout, 公文 "-X-", per-section | +| `Samples/ImageSamples.cs` | Images: inline, floating, text wrapping, border, alt text, in header/table, replace, SVG fallback, dimension calc | +| `Samples/ListAndNumberingSamples.cs` | Numbering: bullets, multi-level decimal, custom symbols, outline→headings, legal, Chinese 一/(一)/1./(1), restart/continue | +| `Samples/FieldAndTocSamples.cs` | Fields: TOC, SimpleField vs complex field, DATE/PAGE/REF/SEQ/MERGEFIELD/IF/STYLEREF, TOC styles | +| `Samples/FootnoteAndCommentSamples.cs` | Footnotes, endnotes, comments (4-file system), bookmarks, hyperlinks (internal + external) | +| `Samples/TrackChangesSamples.cs` | Revisions: insertions (w:t), deletions (w:delText!), formatting changes, accept/reject all, move tracking | +| `Samples/AestheticRecipeSamples.cs` | 13 aesthetic recipes from authoritative sources: ModernCorporate, AcademicThesis, ExecutiveBrief, ChineseGovernment (GB/T 9704), MinimalModern, IEEE Conference, ACM sigconf, APA 7th, MLA 9th, Chicago/Turabian, Springer LNCS, Nature, HBR — each with exact values from official style guides | + +Note: `Samples/` path is relative to `scripts/dotnet/DocxToolkit.Core/`. + +### Markdown references (read when you need specifications or design rules) + +| File | When | +|------|------| +| `references/openxml_element_order.md` | XML element ordering rules (prevents corruption) | +| `references/openxml_units.md` | Unit conversion: DXA, EMU, half-points, eighth-points | +| `references/openxml_encyclopedia_part1.md` | Detailed C# encyclopedia: document creation, styles, character & paragraph formatting | +| `references/openxml_encyclopedia_part2.md` | Detailed C# encyclopedia: page setup, tables, headers/footers, sections, doc properties | +| `references/openxml_encyclopedia_part3.md` | Detailed C# encyclopedia: TOC, footnotes, fields, track changes, comments, images, math, numbering, protection | +| `references/typography_guide.md` | Font pairing, sizes, spacing, page layout, table design, color schemes | +| `references/cjk_typography.md` | CJK fonts, 字号 sizes, RunFonts mapping, GB/T 9704 公文 standard | +| `references/cjk_university_template_guide.md` | Chinese university thesis templates: numeric styleIds (1/2/3 vs Heading1), document zone structure (cover→abstract→TOC→body→references), font expectations, common mistakes | +| `references/design_principles.md` | **Aesthetic foundations**: 6 design principles (white space, contrast/scale, proximity, alignment, repetition, hierarchy) — teaches WHY, not just WHAT | +| `references/design_good_bad_examples.md` | **Good vs Bad comparisons**: 10 categories of typography mistakes with OpenXML values, ASCII mockups, and fixes | +| `references/track_changes_guide.md` | Revision marks deep dive | +| `references/troubleshooting.md` | **Symptom-driven fixes**: 13 common problems indexed by what you SEE (headings wrong, images missing, TOC broken, etc.) — search by symptom, find the fix | diff --git a/software-copyright-materials/vendor/docx-toolkit/assets/styles/academic_styles.xml b/software-copyright-materials/vendor/docx-toolkit/assets/styles/academic_styles.xml new file mode 100644 index 0000000..85d1d06 --- /dev/null +++ b/software-copyright-materials/vendor/docx-toolkit/assets/styles/academic_styles.xml @@ -0,0 +1,250 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/software-copyright-materials/vendor/docx-toolkit/assets/styles/corporate_styles.xml b/software-copyright-materials/vendor/docx-toolkit/assets/styles/corporate_styles.xml new file mode 100644 index 0000000..5d7e2fa --- /dev/null +++ b/software-copyright-materials/vendor/docx-toolkit/assets/styles/corporate_styles.xml @@ -0,0 +1,284 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/software-copyright-materials/vendor/docx-toolkit/assets/styles/default_styles.xml b/software-copyright-materials/vendor/docx-toolkit/assets/styles/default_styles.xml new file mode 100644 index 0000000..6efe7f8 --- /dev/null +++ b/software-copyright-materials/vendor/docx-toolkit/assets/styles/default_styles.xml @@ -0,0 +1,449 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/software-copyright-materials/vendor/docx-toolkit/assets/xsd/aesthetic-rules.xsd b/software-copyright-materials/vendor/docx-toolkit/assets/xsd/aesthetic-rules.xsd new file mode 100644 index 0000000..69e63f7 --- /dev/null +++ b/software-copyright-materials/vendor/docx-toolkit/assets/xsd/aesthetic-rules.xsd @@ -0,0 +1,470 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Body text font size in half-points. + Acceptable range: 20-28 (10pt-14pt). + - 10pt (20): minimum for comfortable reading + - 11pt (22): modern default (Calibri, Aptos) + - 12pt (24): traditional default (Times New Roman) + - 14pt (28): maximum before body text looks oversized + + + + + + + + + + + + + + + + + + + + + + + Heading font size in half-points. + Acceptable range: 24-52 (12pt-26pt). + - 12pt (24): APA-style (hierarchy via bold/italic, not size) + - 16pt (32): typical H2/H3 + - 20pt (40): typical H1 + - 26pt (52): maximum before headings dominate the page + + + + + + + + + + + + + + + + + + + + + Line spacing value for auto line-spacing rule. + In 240ths of single spacing: 240 = 1.0x, 480 = 2.0x. + Acceptable range: 240-560 (1.0x to 2.33x). + Common values: + - 240: single spacing (dense, technical) + - 259: Word's 1.08x default + - 276: 1.15x (modern corporate default) + - 336: 1.4x (executive/generous) + - 360: 1.5x (generous/minimal) + - 480: 2.0x (academic double spacing) + + + + + + + + + + + + + + + + + + + Fixed line spacing value (lineRule="exact") in DXA. + Acceptable range: 200-720 (10pt-36pt). + - 560: Chinese government standard (28pt, for 16pt body) + - 480: double-space equivalent for 12pt body + + + + + + + + + + + + + + + + + + + + + + Page margin in DXA. Minimum 720 (0.5 inch), maximum 4320 (3 inches). + Common values: + - 720: 0.5in (minimum printable) + - 1440: 1.0in (standard US) + - 1588: 28mm (Chinese government left margin) + - 1800: 1.25in (executive/premium) + - 2160: 1.5in (binding margin or narrow-column design) + + + + + + + + + + + + + + Vertical (top/bottom) page margin in DXA. + Range: 360 to 4320 (0.25in to 3in). + Slightly more permissive than horizontal margins because + header/footer areas may reduce effective vertical margin. + + + + + + + + + + + + + + + + + + + Paragraph spacing (before/after) in DXA. + Range: 0-960 (0pt-48pt). + Common values: + - 0: academic style (uses first-line indent instead) + - 80: 4pt (tight, used after H2/H3) + - 120: 6pt (moderate) + - 160: 8pt (standard modern spacing) + - 200: 10pt (generous/executive) + - 240: 12pt (very generous/minimal) + - 480: 24pt (heading before — creates section break) + + + + + + + + + + + + + + + + + + + + Table cell padding in DXA. Minimum 28 DXA (~1.4pt). + Recommended: 57 DXA (~2.85pt) for comfortable spacing. + Maximum: 288 DXA (~14pt) — beyond this wastes space. + + + + + + + + + + + + + + + + + + + Border width in eighth-points. + Range: 2-24 (0.25pt to 3pt). + Common values: + - 4: 0.5pt (thin, standard) + - 6: 0.75pt (header separator in three-line tables) + - 8: 1.0pt (medium, good for framing borders) + - 12: 1.5pt (heavy, used for top/bottom in three-line tables) + - 24: 3.0pt (maximum before borders dominate) + + + + + + + + + + + + + + + + + + Color value: 6-digit hex (RRGGBB) or "auto". + Examples: "000000", "1F3864", "2C3E50", "auto". + + + + + + + + + + + + + + + + + + + First-line indent in DXA. Range: 0-1440 (0in to 1.0in). + - 0: no indent (modern style with space-after) + - 480: 0.33in (compact) + - 640: ~0.44in (2 Chinese characters at 16pt) + - 720: 0.5in (standard APA/academic) + - 1440: 1.0in (maximum before it looks wrong) + + + + + + + + + + + + + + + + + Aesthetic run properties validator. + Checks font size and color format at the run level. + + + + + + + + + + + + + + + + + + + + + + + + + + + + Aesthetic spacing validator for paragraph spacing properties. + Validates line spacing and before/after spacing are in range. + + + + + + + + + + + + + + + + + + + + + + + Aesthetic page margin validator. + Ensures all margins meet minimum print-safe thresholds. + + + + + + + + + + + + + + + + + + Aesthetic table cell margin validator. + Ensures minimum padding for readability. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/software-copyright-materials/vendor/docx-toolkit/assets/xsd/business-rules.xsd b/software-copyright-materials/vendor/docx-toolkit/assets/xsd/business-rules.xsd new file mode 100644 index 0000000..a89df38 --- /dev/null +++ b/software-copyright-materials/vendor/docx-toolkit/assets/xsd/business-rules.xsd @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/software-copyright-materials/vendor/docx-toolkit/assets/xsd/common-types.xsd b/software-copyright-materials/vendor/docx-toolkit/assets/xsd/common-types.xsd new file mode 100644 index 0000000..3a7878d --- /dev/null +++ b/software-copyright-materials/vendor/docx-toolkit/assets/xsd/common-types.xsd @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/software-copyright-materials/vendor/docx-toolkit/assets/xsd/wml-subset.xsd b/software-copyright-materials/vendor/docx-toolkit/assets/xsd/wml-subset.xsd new file mode 100644 index 0000000..1bedb26 --- /dev/null +++ b/software-copyright-materials/vendor/docx-toolkit/assets/xsd/wml-subset.xsd @@ -0,0 +1,589 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/software-copyright-materials/vendor/docx-toolkit/references/cjk_typography.md b/software-copyright-materials/vendor/docx-toolkit/references/cjk_typography.md new file mode 100644 index 0000000..e468f10 --- /dev/null +++ b/software-copyright-materials/vendor/docx-toolkit/references/cjk_typography.md @@ -0,0 +1,357 @@ +# CJK Typography & Mixed-Script Guide + +Rules for Chinese, Japanese, and Korean text in DOCX documents. + +## Table of Contents + +1. [Font Selection](#font-selection) +2. [Font Size Names (CJK)](#font-size-names) +3. [RunFonts Mapping](#runfonts-mapping) +4. [Punctuation & Line Breaking](#punctuation--line-breaking) +5. [Paragraph Indentation](#paragraph-indentation) +6. [Line Spacing for CJK](#line-spacing) +7. [Chinese Government Standard (GB/T 9704)](#gbt-9704) +8. [Mixed CJK + Latin Best Practices](#mixed-script) +9. [OpenXML Quick Reference](#openxml-quick-reference) + +--- + +## Font Selection + +### Recommended CJK Fonts + +| Language | Serif (正文) | Sans (标题) | Notes | +|----------|-------------|-------------|-------| +| **Simplified Chinese** | 宋体 (SimSun) | 微软雅黑 (Microsoft YaHei) | YaHei for screen, SimSun for print | +| **Simplified Chinese** | 仿宋 (FangSong) | 黑体 (SimHei) | Government documents | +| **Traditional Chinese** | 新細明體 (PMingLiU) | 微軟正黑體 (Microsoft JhengHei) | Taiwan standard | +| **Japanese** | MS 明朝 (MS Mincho) | MS ゴシック (MS Gothic) | Classic pairing | +| **Japanese** | 游明朝 (Yu Mincho) | 游ゴシック (Yu Gothic) | Modern, Windows 10+ | +| **Korean** | 바탕 (Batang) | 맑은 고딕 (Malgun Gothic) | Standard pairing | + +### Government Document Fonts (公文) + +| Element | Font | Size | +|---------|------|------| +| 标题 (title) | 小标宋 (FZXiaoBiaoSong-B05S) | 二号 (22pt) | +| 一级标题 | 黑体 (SimHei) | 三号 (16pt) | +| 二级标题 | 楷体_GB2312 (KaiTi_GB2312) | 三号 (16pt) | +| 三级标题 | 仿宋_GB2312 加粗 | 三号 (16pt) | +| 正文 (body) | 仿宋_GB2312 (FangSong_GB2312) | 三号 (16pt) | +| 附注/页码 | 宋体 (SimSun) | 四号 (14pt) | + +--- + +## Font Size Names + +CJK uses named sizes. Map to points and `w:sz` half-point values: + +| 字号 | Points | `w:sz` | Common Use | +|------|--------|--------|------------| +| 初号 | 42pt | 84 | Display title | +| 小初 | 36pt | 72 | Large title | +| 一号 | 26pt | 52 | Chapter heading | +| 小一 | 24pt | 48 | Major heading | +| 二号 | 22pt | 44 | Document title (公文) | +| 小二 | 18pt | 36 | Western H1 equivalent | +| 三号 | 16pt | 32 | CJK heading / 公文 body | +| 小三 | 15pt | 30 | Sub-heading | +| 四号 | 14pt | 28 | CJK subheading | +| 小四 | 12pt | 24 | Standard body (CJK) | +| 五号 | 10.5pt | 21 | Compact CJK body | +| 小五 | 9pt | 18 | Footnotes | +| 六号 | 7.5pt | 15 | Fine print | + +--- + +## RunFonts Mapping + +OpenXML uses four font slots to handle multilingual text: + +```xml + + w:hAnsi="Calibri" + w:eastAsia="SimSun" + w:cs="Arial" +/> +``` + +**Word's character classification logic:** + +1. Character is in CJK range → uses `w:eastAsia` font +2. Character is in complex script range → uses `w:cs` font +3. Character is basic Latin (ASCII) → uses `w:ascii` font +4. Everything else → uses `w:hAnsi` font + +**Key**: `w:eastAsia` is the **only** way to set CJK fonts. Setting just `w:ascii` will NOT affect CJK characters. Mixed text within a single run auto-switches fonts at the character level — no need for separate runs. + +### Document Defaults + +```xml + + + + + + + + + + +``` + +`w:lang w:eastAsia` helps Word resolve ambiguous characters (e.g., punctuation shared between CJK and Latin). + +--- + +## Punctuation & Line Breaking + +### Full-Width vs Half-Width + +CJK text uses full-width punctuation: + +| Type | CJK | Latin | +|------|-----|-------| +| Period | 。(U+3002) | . | +| Comma | ,(U+FF0C) 、(U+3001) | , | +| Colon | :(U+FF1A) | : | +| Semicolon | ;(U+FF1B) | ; | +| Quotes | 「」『』 or ""'' | "" '' | +| Parentheses | ()(U+FF08/09) | () | + +In mixed text, use the punctuation style of the **surrounding language context**. + +### OpenXML Controls + +```xml + + + + + + +``` + +### Kinsoku Rules (禁則処理) + +Prevents certain characters from appearing at the start or end of a line: +- **Cannot start a line**: `)」』】〉》。、,!?;:` and closing brackets +- **Cannot end a line**: `(「『【〈《` and opening brackets + +Word applies these automatically when `w:kinsoku` is enabled. + +### Line Breaking + +- CJK characters can break between **any two characters** (no word boundaries needed) +- Latin words within CJK text still follow word-boundary breaking +- `w:wordWrap w:val="false"` enables CJK-style breaking (break anywhere) + +--- + +## Paragraph Indentation + +### Chinese Standard: 2-Character Indent + +Chinese body text conventionally uses a 2-character first-line indent: + +```xml + +``` + +Preferred over `w:firstLine` with fixed DXA because `firstLineChars` scales with font size. + +| Indent | Value | +|--------|-------| +| 1 character | `w:firstLineChars="100"` | +| 2 characters | `w:firstLineChars="200"` | +| 3 characters | `w:firstLineChars="300"` | + +--- + +## Line Spacing + +- CJK characters are taller than Latin characters at the same point size +- Default `1.0` line spacing may feel cramped with CJK text +- Recommended: `1.15–1.5` for mixed CJK+Latin, `1.0` with fixed 28pt for 公文 + +### Auto Spacing + +```xml + + + + +``` + +Adds ~¼ em spacing between CJK and non-CJK characters automatically. **Recommended: always enable.** + +--- + +## GB/T 9704 + +Chinese government document standard (党政机关公文格式). These are **strict requirements**, not suggestions. + +### Page Setup + +| Parameter | Value | OpenXML | +|-----------|-------|---------| +| Page size | A4 (210×297mm) | Width=11906, Height=16838 | +| Top margin | 37mm | 2098 DXA | +| Bottom margin | 35mm | 1984 DXA | +| Left margin | 28mm | 1588 DXA | +| Right margin | 26mm | 1474 DXA | +| Characters/line | 28 | | +| Lines/page | 22 | | +| Line spacing | Fixed 28pt | `line="560"` lineRule="exact" | + +### Document Structure + +``` +┌─────────────────────────────────┐ +│ 发文机关标志 (红头) │ ← 小标宋 or 红色大字 +│ ══════════════════ (红线) │ ← Red #FF0000, 2pt +├─────────────────────────────────┤ +│ 发文字号: X机发〔2025〕X号 │ ← 仿宋 三号, centered +│ │ +│ 标题 (Title) │ ← 小标宋 二号, centered +│ │ 可分多行,回行居中 +│ 主送机关: │ ← 仿宋 三号 +│ │ +│ 正文 (Body)... │ ← 仿宋_GB2312 三号 +│ 一、一级标题 │ ← 黑体 三号 +│ (一)二级标题 │ ← 楷体 三号 +│ 1. 三级标题 │ ← 仿宋 三号 加粗 +│ (1) 四级标题 │ ← 仿宋 三号 +│ │ +│ 附件: 1. xxx │ ← 仿宋 三号 +│ │ +│ 发文机关署名 │ ← 仿宋 三号 +│ 成文日期 │ ← 仿宋 三号, 小写中文数字 +├─────────────────────────────────┤ +│ ══════════════════ (版记线) │ +│ 抄送: xxx │ ← 仿宋 四号 +│ 印发机关及日期 │ ← 仿宋 四号 +└─────────────────────────────────┘ +``` + +### Numbering System + +``` +一、 ← 黑体 (SimHei), no indentation +(一) ← 楷体 (KaiTi), indented 2 chars +1. ← 仿宋加粗 (FangSong Bold), indented 2 chars +(1) ← 仿宋 (FangSong), indented 2 chars +``` + +### Colors + +| Element | Color | Requirement | +|---------|-------|-------------| +| All body text | Black #000000 | Mandatory | +| 红头 (agency name) | Red #FF0000 | Mandatory | +| 红线 (separator) | Red #FF0000 | Mandatory | +| 公章 (official seal) | Red | Mandatory | + +### Page Numbers + +- Position: bottom center +- Format: `-X-` (dash-number-dash) +- Font: 宋体 四号 (SimSun 14pt, `sz="28"`) +- No page number on cover page if present + +--- + +## Mixed Script + +### Font Size Harmony + +CJK characters appear larger than Latin characters at the same point size. Compensation: + +- If body is Calibri 11pt, pair with CJK at 11pt (same size — CJK looks slightly larger but acceptable) +- If precise visual match needed, CJK can be set 0.5–1pt smaller +- In practice, same point size is standard — don't over-optimize + +### Bold and Italic + +- **Chinese/Japanese have no true italic.** Word synthesizes a slant which looks poor +- Use **bold** for emphasis in CJK text +- Use 着重号 (emphasis dots) for traditional emphasis: `` on RunProperties + +--- + +## OpenXML Quick Reference + +### Set EastAsia Font (C#) + +```csharp +new Run( + new RunProperties( + new RunFonts { EastAsia = "SimSun", Ascii = "Calibri", HighAnsi = "Calibri" }, + new FontSize { Val = "32" } // 三号 = 16pt = sz 32 + ), + new Text("这是正文内容") +); +``` + +### Document Defaults (C#) + +```csharp +new DocDefaults(new RunPropertiesDefault(new RunPropertiesBaseStyle( + new RunFonts { + Ascii = "Calibri", HighAnsi = "Calibri", + EastAsia = "Microsoft YaHei" + }, + new Languages { Val = "en-US", EastAsia = "zh-CN" } +))); +``` + +### 公文 Style Definitions (C#) + +```csharp +// Title style — 小标宋 二号 centered +new Style( + new StyleName { Val = "GongWen Title" }, + new BasedOn { Val = "Normal" }, + new StyleRunProperties( + new RunFonts { EastAsia = "FZXiaoBiaoSong-B05S" }, + new FontSize { Val = "44" }, // 二号 = 22pt + new Bold() + ), + new StyleParagraphProperties( + new Justification { Val = JustificationValues.Center }, + new SpacingBetweenLines { Line = "560", LineRule = LineSpacingRuleValues.Exact } + ) +) { Type = StyleValues.Paragraph, StyleId = "GongWenTitle" }; + +// Body style — 仿宋_GB2312 三号 +new Style( + new StyleName { Val = "GongWen Body" }, + new StyleRunProperties( + new RunFonts { EastAsia = "FangSong_GB2312", Ascii = "FangSong_GB2312" }, + new FontSize { Val = "32" } // 三号 = 16pt + ), + new StyleParagraphProperties( + new SpacingBetweenLines { Line = "560", LineRule = LineSpacingRuleValues.Exact } + ) +) { Type = StyleValues.Paragraph, StyleId = "GongWenBody" }; +``` + +### Emphasis Dots (着重号) + +```csharp +new RunProperties(new Emphasis { Val = EmphasisMarkValues.Dot }); +``` + +### East Asian Text Layout + +```xml + + + + + + + + +``` diff --git a/software-copyright-materials/vendor/docx-toolkit/references/cjk_university_template_guide.md b/software-copyright-materials/vendor/docx-toolkit/references/cjk_university_template_guide.md new file mode 100644 index 0000000..da4cfb0 --- /dev/null +++ b/software-copyright-materials/vendor/docx-toolkit/references/cjk_university_template_guide.md @@ -0,0 +1,184 @@ +# Chinese University Thesis Template Guide (中国高校论文模板指南) + +## Why This Guide Exists + +Chinese university thesis templates (.docx) have structural patterns that differ significantly +from Western templates. Agents that assume Western conventions (Heading1/Heading2/Normal) will +fail repeatedly. This guide documents the ACTUAL patterns found in Chinese templates. + +## Common StyleId Patterns + +### Pattern A: Numeric IDs (most common in Chinese Word templates) + +| Style Purpose | styleId | w:name | w:basedOn | +|--------------|---------|--------|-----------| +| Normal body | `a` | "Normal" | — | +| Default paragraph font | `a0` | "Default Paragraph Font" | — | +| Heading 1 (章标题) | `1` | "heading 1" | `a` | +| Heading 2 (节标题) | `2` | "heading 2" | `a` | +| Heading 3 (小节标题) | `3` | "heading 3" | `a` | +| TOC 1 | `11` | "toc 1" | `a` | +| TOC 2 | `21` | "toc 2" | `a` | +| TOC 3 | `31` | "toc 3" | `a` | +| Header | `a3` | "header" | `a` | +| Footer | `a4` | "footer" | `a` | +| Table of Contents heading | `10` | "TOC Heading" | `1` | + +### Pattern B: English IDs (less common, usually from international templates) +Standard Heading1/Heading2/Heading3/Normal — these follow the Western pattern. + +### Pattern C: Mixed (some Chinese, some English) +Some templates define custom styles with Chinese names: +| Style Purpose | styleId | w:name | +|--------------|---------|--------| +| 论文标题 | `lunwenbiaoti` | "论文标题" | +| 章标题 | `zhangbiaoti` | "章标题" | +| 正文 | `zhengwen` | "正文" | + +### How to Identify Which Pattern + +```bash +# Extract all styleIds from the template +$CLI analyze --input template.docx --styles-only + +# Or manually: +# unzip template.docx word/styles.xml +# Search for w:styleId= in the extracted file +``` + +Look at the first few styleIds. If you see `1`, `2`, `3`, `a`, `a0` → Pattern A. +If you see `Heading1`, `Normal` → Pattern B. + +## Standard Thesis Structure + +Chinese university theses follow a highly standardized structure: + +``` +┌─────────────────────────────────────┐ +│ 封面 (Cover Page) │ ← Usually 1-2 pages +│ - 校名、校徽 │ +│ - 论文题目 (title) │ +│ - 作者、导师、院系、日期 │ +├─────────────────────────────────────┤ +│ 学术诚信承诺书 / 独创性声明 │ ← 1 page +│ (Academic Integrity Declaration) │ +├─────────────────────────────────────┤ +│ 中文摘要 (Chinese Abstract) │ ← 1-2 pages +│ - "摘 要" heading │ +│ - Abstract body │ +│ - "关键词:" line │ +├─────────────────────────────────────┤ +│ 英文摘要 (English Abstract) │ ← 1-2 pages +│ - "ABSTRACT" heading │ +│ - Abstract body │ +│ - "Keywords:" line │ +├─────────────────────────────────────┤ +│ 目录 (Table of Contents) │ ← 1-3 pages +│ - Often inside SDT block │ +│ - Static example entries │ +│ - TOC field code │ +├─────────────────────────────────────┤ +│ 正文 (Body) │ ← Main content +│ 第1章 绪论 │ +│ 1.1 研究背景 │ +│ 1.2 研究目的和意义 │ +│ 第2章 文献综述 │ +│ ... │ +│ 第N章 结论与展望 │ +├─────────────────────────────────────┤ +│ 参考文献 (References) │ ← Styled differently +├─────────────────────────────────────┤ +│ 致谢 (Acknowledgments) │ ← Optional +├─────────────────────────────────────┤ +│ 附录 (Appendices) │ ← Optional +└─────────────────────────────────────┘ +``` + +## Identifying Zone Boundaries in Templates + +Templates contain EXAMPLE content that must be replaced. Here's how to find the zones: + +### Zone A (Front matter) — KEEP from template +- Starts at: paragraph 0 +- Ends at: the paragraph BEFORE the first chapter heading +- Contains: cover, declaration, abstracts, TOC +- How to detect end: search for first paragraph with style `1` (or Heading1) containing "第1章" or "绪论" + +### Zone B (Body content) — REPLACE with user content +- Starts at: first chapter heading ("第1章...") +- Ends at: "参考文献" heading (inclusive) or last body paragraph before acknowledgments +- How to detect: + ```python + for i, el in enumerate(body_elements): + text = get_text(el) + style = get_style(el) + if style in ('1', 'Heading1') and ('第1章' in text or '绪论' in text): + zone_b_start = i + if '参考文献' in text: + zone_b_end = i + ``` + +### Zone C (Back matter) — KEEP from template (or remove) +- Starts after: 参考文献 +- Contains: 致谢, 附录, final sectPr + +## Font Expectations in Chinese Thesis Templates + +| Element | Font | Size (字号) | Size (pt) | w:sz | +|---------|------|------------|-----------|------| +| 论文标题 | 华文中宋 or 黑体 | 二号 or 小二 | 22pt or 18pt | 44 or 36 | +| 章标题 (H1) | 黑体 | 三号 | 16pt | 32 | +| 节标题 (H2) | 黑体 | 四号 | 14pt | 28 | +| 小节标题 (H3) | 黑体 | 小四 | 12pt | 24 | +| 正文 | 宋体 | 小四 | 12pt | 24 | +| 页眉 | 宋体 | 五号 | 10.5pt | 21 | +| 页脚/页码 | 宋体 | 五号 | 10.5pt | 21 | +| 表格内容 | 宋体 | 五号 | 10.5pt | 21 | +| 参考文献条目 | 宋体 | 五号 | 10.5pt | 21 | + +## RunFonts for CJK Body Text + +```xml + +``` + +For headings: +```xml + +``` + +IMPORTANT: When cleaning direct formatting, ALWAYS preserve w:eastAsia. +Removing it causes Chinese text to fall back to the wrong font. + +## Common Mistakes with Chinese Templates + +1. **Searching for `Heading1`** — Chinese templates use `1`, not `Heading1` +2. **Clearing all rFonts** — Must keep eastAsia font declarations +3. **Assuming "第1章" is the first paragraph** — It's typically paragraph 100+ after cover/abstract/TOC +4. **Ignoring SDT blocks in TOC** — The TOC is wrapped in an SDT, not just field codes +5. **Wrong line spacing** — Chinese theses typically use fixed 20pt (line="400") or 22pt (line="440"), not the 28pt used in government documents +6. **Missing section breaks** — Each zone (abstract, TOC, body) usually has its own sectPr for different headers/footers + +## Style Mapping Quick Reference + +When source document uses Western IDs and template uses Chinese numeric IDs: + +```json +{ + "Heading1": "1", + "Heading2": "2", + "Heading3": "3", + "Heading4": "3", + "Normal": "a", + "BodyText": "a", + "ListParagraph": "a", + "Caption": "a", + "TOC1": "11", + "TOC2": "21", + "TOC3": "31" +} +``` + +When source uses Chinese numeric IDs and template uses Western IDs — reverse the mapping. diff --git a/software-copyright-materials/vendor/docx-toolkit/references/comments_guide.md b/software-copyright-materials/vendor/docx-toolkit/references/comments_guide.md new file mode 100644 index 0000000..fa12493 --- /dev/null +++ b/software-copyright-materials/vendor/docx-toolkit/references/comments_guide.md @@ -0,0 +1,191 @@ +# Comments System Guide (4-File Architecture) + +## Overview + +Word comments require coordination across **four XML files** plus references in `document.xml`, `[Content_Types].xml`, and `document.xml.rels`. + +--- + +## The Four Comment Files + +### 1. `word/comments.xml` — Main Comment Content + +Contains the actual comment text: + +```xml + + + + + + + + + + + This needs clarification. + + + + +``` + +Key attributes: `w:id` (unique integer), `w:author`, `w:date` (ISO 8601), `w:initials`. + +### 2. `word/commentsExtended.xml` — W15 Extensions + +Links comments to paragraphs and tracks resolved status: + +```xml + + + + +``` + +- `w15:paraId` — matches the `w14:paraId` of the comment's paragraph in `comments.xml` +- `w15:done` — `"0"` = open, `"1"` = resolved + +### 3. `word/commentsIds.xml` — Persistent ID Mapping + +Provides durable IDs that survive copy/paste across documents: + +```xml + + + + +``` + +- `w16cid:paraId` — same as `w15:paraId` +- `w16cid:durableId` — globally unique identifier (8-digit hex) + +### 4. `word/commentsExtensible.xml` — W16 Extensions + +Modern comment extensions (used in newer Word versions): + +```xml + + + + +``` + +--- + +## Document.xml References + +Comments are anchored in document content using three elements: + +```xml + + + This text has a comment. + + + + + + +``` + +- `w:commentRangeStart` — marks where the commented text begins +- `w:commentRangeEnd` — marks where the commented text ends +- `w:commentReference` — the visible comment marker (superscript number), placed in a run after the range end + +The `w:id` on all three must match the `w:id` in `comments.xml`. + +--- + +## Content Types Registration + +Add to `[Content_Types].xml`: + +```xml + + + + +``` + +--- + +## Relationship Registration + +Add to `word/_rels/document.xml.rels`: + +```xml + + + + +``` + +--- + +## Step-by-Step: Adding a New Comment + +1. **Choose a unique comment ID** (scan existing `w:id` values, use max + 1) +2. **Generate a paraId** (8-character hex, e.g., `"1A2B3C4D"`) and durableId (8-digit hex) +3. **Add to `comments.xml`**: Create `w:comment` element with content +4. **Add to `commentsExtended.xml`**: Create `w15:commentEx` with `paraId`, `done="0"` +5. **Add to `commentsIds.xml`**: Create `w16cid:commentId` with `paraId` and `durableId` +6. **Add to `commentsExtensible.xml`**: Create `w16cex:commentExtensible` with `durableId` and `dateUtc` +7. **Add to `document.xml`**: Insert `w:commentRangeStart`, `w:commentRangeEnd`, and `w:commentReference` around target text +8. **Verify `[Content_Types].xml`** and `document.xml.rels` have entries for all 4 files + +--- + +## Step-by-Step: Adding a Reply + +Replies are comments whose paragraph's `w14:paraId` links to a parent comment: + +1. Create a new `w:comment` in `comments.xml` with a new `w:id` +2. In `commentsExtended.xml`, add `w15:commentEx` with: + - `w15:paraId` = new paragraph ID + - `w15:paraIdParent` = the `paraId` of the comment being replied to + - `w15:done="0"` +3. Add entries in `commentsIds.xml` and `commentsExtensible.xml` +4. In `document.xml`, the reply does NOT need its own range markers — it shares the parent's range + +```xml + + +``` + +--- + +## Step-by-Step: Resolving a Comment + +Set `w15:done="1"` on the comment's `w15:commentEx` entry: + +```xml + + + + + +``` + +This marks the comment (and all its replies) as resolved. The comment remains visible but appears grayed out in Word. + +--- + +## Minimum Viable Comment + +At minimum, a working comment requires: +1. `comments.xml` with the `w:comment` element +2. `document.xml` with range markers and reference +3. Relationship in `document.xml.rels` +4. Content type in `[Content_Types].xml` + +The extended files (`commentsExtended`, `commentsIds`, `commentsExtensible`) are optional but recommended for full compatibility with modern Word. diff --git a/software-copyright-materials/vendor/docx-toolkit/references/design_good_bad_examples.md b/software-copyright-materials/vendor/docx-toolkit/references/design_good_bad_examples.md new file mode 100644 index 0000000..82b7c50 --- /dev/null +++ b/software-copyright-materials/vendor/docx-toolkit/references/design_good_bad_examples.md @@ -0,0 +1,829 @@ +# GOOD vs BAD Document Design — Concrete OpenXML Examples + +A side-by-side reference showing common design mistakes and their fixes, with exact OpenXML parameter values. Use this to develop an intuitive sense of what makes a document look professional versus amateur. + +Format: Each comparison shows the **BAD** version first (the mistake), then the **GOOD** version (the fix), with OpenXML markup and a short explanation. + +--- + +## 1. Font Size Disasters + +### 1a. No Hierarchy — Everything the Same Size + +**BAD: Body=12pt, H1=12pt bold** +``` +┌──────────────────────────────────┐ +│ INTRODUCTION │ ← 12pt bold... same visual weight +│ This is the body text of the │ ← 12pt regular +│ report. It discusses findings │ +│ from the quarterly review. │ +│ METHODOLOGY │ ← Where does the section start? +│ We collected data from three │ +│ sources across the enterprise. │ +└──────────────────────────────────┘ +``` +```xml + + + + +``` + +**GOOD: Modular scale — body=11pt, H3=13pt, H2=16pt, H1=20pt** +``` +┌──────────────────────────────────┐ +│ │ +│ Introduction │ ← 20pt, clearly a title +│ │ +│ This is the body text of the │ ← 11pt, comfortable reading size +│ report. It discusses findings │ +│ from the quarterly review. │ +│ │ +│ Methodology │ ← 20pt, section break is obvious +│ │ +│ We collected data from three │ +│ sources across the enterprise. │ +└──────────────────────────────────┘ +``` +```xml + + + + + + + + +``` +**Why better:** A clear size progression (ratio ~1.25x per step) lets readers instantly identify structure without reading a word. + +--- + +### 1b. Too Much Contrast — Children's Book Look + +**BAD: H1=28pt with body=10pt (ratio 2.8x)** +``` +┌──────────────────────────────────┐ +│ │ +│ QUARTERLY REPORT │ ← 28pt, dominates the page +│ │ +│ This is body text set very small │ ← 10pt, straining to read +│ and the contrast with the title │ +│ makes it feel like a poster. │ +└──────────────────────────────────┘ +``` +```xml + + +``` + +**GOOD: H1=20pt with body=11pt (ratio ~1.8x)** +```xml + + +``` +**Why better:** A heading-to-body ratio between 1.5x and 2.0x reads as "structured" rather than "shouting." + +--- + +## 2. Spacing Crimes + +### 2a. Wall of Text — No Paragraph or Line Spacing + +**BAD: Single line spacing, 0pt between paragraphs** +``` +┌──────────────────────────────────┐ +│The findings indicate a strong │ +│correlation between training hours│ +│and performance metrics. │ +│Further analysis revealed that │ ← No gap — where does the new +│departments with higher budgets │ paragraph start? +│achieved better outcomes in all │ +│measured categories. │ +└──────────────────────────────────┘ +``` +```xml + + + + +``` + +**GOOD: 1.15x line spacing, 8pt after each paragraph** +``` +┌──────────────────────────────────┐ +│The findings indicate a strong │ +│correlation between training │ ← Slightly more air between lines +│hours and performance metrics. │ +│ │ ← 8pt gap signals new paragraph +│Further analysis revealed that │ +│departments with higher budgets │ +│achieved better outcomes in all │ +│measured categories. │ +└──────────────────────────────────┘ +``` +```xml + + + + +``` +**Why better:** Line spacing gives each line room to breathe; paragraph spacing separates ideas without wasting a full blank line. + +--- + +### 2b. Floating Headings — Same Space Above and Below + +**BAD: 12pt before and 12pt after heading** +``` +┌──────────────────────────────────┐ +│ ...end of previous section. │ +│ │ ← 12pt gap +│ Section Two │ ← Heading floats in the middle +│ │ ← 12pt gap +│ Start of section two content. │ +└──────────────────────────────────┘ +``` +```xml + + + +``` + +**GOOD: 24pt before, 8pt after heading** +``` +┌──────────────────────────────────┐ +│ ...end of previous section. │ +│ │ +│ │ ← 24pt gap — clear section break +│ Section Two │ ← Heading is close to its content +│ │ ← 8pt gap +│ Start of section two content. │ +└──────────────────────────────────┘ +``` +```xml + + + +``` +**Why better:** Proximity principle: a heading belongs to the text that follows it, so more space above and less space below anchors it to its content. + +--- + +### 2c. Wasteful Gaps — Huge Spacing Everywhere + +**BAD: 24pt after every paragraph, including body text** +``` +┌──────────────────────────────────┐ +│ First paragraph of text here. │ +│ │ +│ │ ← 24pt gap after every paragraph +│ │ +│ Second paragraph of text here. │ +│ │ +│ │ +│ │ +│ Third paragraph. │ ← Document looks mostly white space +└──────────────────────────────────┘ +``` +```xml + +``` + +**GOOD: Proportional spacing — body=8pt, H2=6pt after, H1=10pt after** +```xml + + + + + + +``` +**Why better:** Spacing should vary by element role, creating a visual rhythm rather than uniform gaps. + +--- + +## 3. Margin Mistakes + +### 3a. Cramped Margins — Text Running to the Edge + +**BAD: 0.5in margins all around** +``` +┌────────────────────────────────────────────────┐ +│Text starts almost at the paper edge and runs │ +│all the way across making extremely long lines │ +│that are hard to track from end back to start. │ +│The eye loses its place on every line return. │ +└────────────────────────────────────────────────┘ +``` +```xml + + +``` + +**GOOD: 1in margins (standard)** +```xml + + +``` +**Why better:** Optimal line length is 60-75 characters. At 11pt Calibri, 6.5in width achieves roughly 70 characters per line. + +--- + +### 3b. Over-Padded Margins — Looks Like the Content is Hiding + +**BAD: 2in margins on a short document** +```xml + + +``` + +**GOOD: 1in standard, or 1.25in for formal documents** +```xml + + + + + +``` +**Why better:** Margins should frame the content, not overwhelm it. 1-1.25in works for virtually all business and academic documents. + +--- + +## 4. Table Ugliness + +### 4a. Prison Grid — Full Borders on Every Cell + +**BAD: Every cell with 1pt borders on all four sides** +``` +┌───────┬───────┬───────┬───────┐ +│ Name │ Dept │ Score │ Grade │ +├───────┼───────┼───────┼───────┤ +│ Alice │ Eng │ 92 │ A │ +├───────┼───────┼───────┼───────┤ +│ Bob │ Sales │ 85 │ B │ +├───────┼───────┼───────┼───────┤ +│ Carol │ Eng │ 78 │ C+ │ +└───────┴───────┴───────┴───────┘ +``` +```xml + + + + + + +``` + +**GOOD: Three-line table (三线表) — top thick, header-bottom medium, table-bottom thick** +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ← 1.5pt top border + Name Dept Score Grade +────────────────────────────────── ← 0.75pt header separator + Alice Eng 92 A + Bob Sales 85 B + Carol Eng 78 C+ +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ← 1.5pt bottom border +``` +```xml + + + + + + + + + + + +``` +**Why better:** Removing inner borders lets the eye scan data freely. Three lines provide structure without visual clutter. + +--- + +### 4b. Text Touching Borders — No Cell Padding + +**BAD: Zero cell margins** +``` +┌──────────┬──────────┐ +│Name │Department│ ← Text cramped against borders +├──────────┼──────────┤ +│Alice │Engineering│ +└──────────┴──────────┘ +``` +```xml + + + + + + +``` + +**GOOD: 0.08in vertical, 0.12in horizontal padding** +```xml + + + + + + +``` +**Why better:** Padding gives text breathing room inside cells, making every value easier to read. + +--- + +### 4c. Invisible Headers — Header Row Same Style as Data + +**BAD: Header row indistinguishable from data** +```xml + + +``` + +**GOOD: Bold header text, subtle background fill, bottom border** +```xml + + + + + + + + + + + + + +``` +**Why better:** Distinct header styling lets readers instantly locate column meanings, especially in long tables that span pages. The `w:tblHeader` element ensures the header row repeats on every page. + +--- + +## 5. Font Pairing Failures + +### 5a. Visual Chaos — Too Many Fonts + +**BAD: 4+ fonts in one document** +```xml + + + + + + + + +``` + +**GOOD: One font family with weight variation, or two complementary families** +```xml + + + + + + + + +``` +**Why better:** Limiting to one or two font families creates visual coherence. Vary by size and weight, not by font. + +--- + +### 5b. Mismatched Personality — Comic Sans Meets Times New Roman + +**BAD:** +```xml + + +``` + +**GOOD: Fonts with compatible character** +```xml + + +``` +**Why better:** Paired fonts should share a similar level of formality and geometric character. Comic Sans is playful/informal; Times New Roman is formal/traditional. They clash. + +--- + +### 5c. Everything Bold — Nothing Stands Out + +**BAD: Bold on body, headings, captions, everything** +```xml + + + +``` + +**GOOD: Bold reserved for headings and key terms only** +```xml + + + + + +``` +**Why better:** When everything is emphasized, nothing is emphasized. Bold should be a signal, not a default. + +--- + +## 6. Color Abuse + +### 6a. Rainbow Headings + +**BAD: Each heading level a different bright color** +```xml + + + +``` + +**GOOD: Single accent color for headings, black or dark gray for body** +```xml + + + + + + +``` +**Why better:** A single accent color establishes brand consistency. Multiple bright colors compete for attention and look unprofessional. + +--- + +### 6b. Low Contrast — Light Gray on White + +**BAD: #CCCCCC text on white background** +```xml + + +``` + +**GOOD: #333333 text on white** +```xml + + +``` +**Why better:** Sufficient contrast is not just an accessibility requirement; it makes text physically easier to read for everyone, especially in printed documents. + +--- + +### 6c. Bright Body Text + +**BAD: Body text in a saturated color** +```xml + +``` + +**GOOD: Color reserved for headings and inline accents only** +```xml + + + + +``` +**Why better:** Colored body text causes eye fatigue over long reading. Reserve color for elements that need to attract attention (headings, links, warnings). + +--- + +## 7. List Formatting Issues + +### 7a. Bullet at the Margin — No Indent + +**BAD: List items start at the left margin** +``` +┌──────────────────────────────────┐ +│Here is a paragraph of text. │ +│• First item │ ← Bullet at margin, no indent +│• Second item │ +│• Third item │ +│Next paragraph continues here. │ +└──────────────────────────────────┘ +``` +```xml + + + +``` + +**GOOD: 0.25in left indent with hanging indent for the bullet** +``` +┌──────────────────────────────────┐ +│Here is a paragraph of text. │ +│ • First item │ ← Indented, clearly a list +│ • Second item │ +│ • Third item │ +│Next paragraph continues here. │ +└──────────────────────────────────┘ +``` +```xml + + + + + + + +``` +For nested lists, increment by 360 twips per level: +```xml + + + + +``` +**Why better:** Indentation visually separates lists from body text and makes nesting levels clear. + +--- + +### 7b. List Items with Full Paragraph Spacing + +**BAD: List items have the same 8-10pt spacing as body paragraphs** +``` +┌──────────────────────────────────┐ +│ • First item │ +│ │ ← 10pt gap — looks like separate +│ • Second item │ paragraphs, not a list +│ │ +│ • Third item │ +└──────────────────────────────────┘ +``` +```xml + +``` + +**GOOD: Tight spacing between list items (2-4pt)** +``` +┌──────────────────────────────────┐ +│ • First item │ +│ • Second item │ ← 2pt gap — cohesive list +│ • Third item │ +└──────────────────────────────────┘ +``` +```xml + + + +``` +**Why better:** Tight spacing groups list items as a single unit, matching how readers expect a list to behave. + +--- + +## 8. Header/Footer Problems + +### 8a. Header Text Too Large — Competes with Body + +**BAD: Header in 12pt, same as body** +``` +┌──────────────────────────────────┐ +│ Quarterly Report - Q3 2025 │ ← 12pt header, same as body +│──────────────────────────────────│ +│ Introduction │ +│ This is the body text... │ ← 12pt body — header distracts +└──────────────────────────────────┘ +``` +```xml + + +``` + +**GOOD: Header in 9pt, gray color, subtle** +``` +┌──────────────────────────────────┐ +│ Quarterly Report - Q3 2025 │ ← 9pt, gray — present but quiet +│──────────────────────────────────│ +│ Introduction │ +│ This is the body text... │ ← Body stands out as primary +└──────────────────────────────────┘ +``` +```xml + + + + + + + + + + +``` +**Why better:** Headers are reference information, not primary content. They should be legible but visually subordinate. + +--- + +### 8b. No Page Numbers on a Long Document + +**BAD: 20-page document with no page numbers** +```xml + +``` + +**GOOD: Page numbers in footer, right-aligned or centered** +```xml + + + + + + + + + + + + PAGE + + + + + + 1 + + + + + +``` +**Why better:** Page numbers are essential for navigation in any document over ~3 pages. Readers need to reference specific pages, and printed documents need an ordering mechanism. + +--- + +## 9. CJK-Specific Mistakes + +### 9a. Using Italic for Chinese Emphasis + +**BAD: Italic applied to Chinese text** +```xml + + + + + +``` +CJK glyphs have no true italic form. The renderer applies a synthetic slant that looks broken and ugly — characters appear to lean awkwardly. + +**GOOD: Use bold or emphasis dots (着重号) for Chinese emphasis** +```xml + + + + + + + + + + + + + +``` +**Why better:** Chinese typography has its own emphasis traditions. Bold and emphasis dots are native CJK conventions; italic is a Latin-script concept that does not translate. + +--- + +### 9b. Latin Font for Chinese Characters + +**BAD: Only ASCII font set, no EastAsia font specified** +```xml + + + + + +``` + +**GOOD: Explicit EastAsia font alongside ASCII font** +```xml + + + + +``` +For formal/academic Chinese documents: +```xml + + + + +``` +**Why better:** Setting `w:eastAsia` ensures Chinese characters render in a font designed for CJK glyphs, with correct stroke widths, spacing, and metrics. + +--- + +### 9c. English Line Spacing for Dense CJK Text + +**BAD: 1.15x line spacing for Chinese body text** +```xml + +``` +CJK characters are taller and denser than Latin letters. At 1.15x, lines of Chinese text feel cramped and hard to read. + +**GOOD: 1.5x line spacing or fixed 28pt for CJK body at 12pt (小四)** +```xml + + + + + +``` +For 公文 (government documents) at 三号/16pt body: +```xml + +``` +**Why better:** CJK characters occupy a full em square with no ascenders/descenders providing natural gaps. Extra line spacing compensates, improving readability of dense text blocks. + +--- + +## 10. Overall Document Feel + +### Student Homework vs Professional Document + +**BAD: "Student homework" — every setting is Word's default, no intentional choices** +```xml + + + + + + + + +``` + +**GOOD: Intentional design at every level** +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` +**Why better:** Professional documents result from deliberate, consistent choices across all design dimensions. Each element reinforces the same visual language. The reader may not consciously notice good typography, but they feel the difference in credibility and readability. + +--- + +## Quick Reference: Safe Defaults + +A cheat sheet of values that produce a professional result for most Western business documents: + +| Element | Value | OpenXML | +|---------|-------|---------| +| Body font | Calibri 11pt | `w:sz="22"` | +| H1 | Calibri Light 20pt | `w:sz="40"` | +| H2 | Calibri Light 16pt | `w:sz="32"` | +| H3 | Calibri 13pt bold | `w:sz="26"`, `w:b` | +| Body color | #333333 | `w:color="333333"` | +| Heading color | #1F4E79 | `w:color="1F4E79"` | +| Line spacing | 1.15x | `w:line="276" w:lineRule="auto"` | +| Para spacing after | 8pt | `w:after="160"` | +| H1 spacing | 24pt before, 10pt after | `w:before="480" w:after="200"` | +| H2 spacing | 16pt before, 6pt after | `w:before="320" w:after="120"` | +| Margins | 1in all around | `w:pgMar` all `"1440"` | +| Table cell padding | 0.08in / 0.12in | `w:w="115"` / `w:w="173"` | +| Header/footer size | 9pt gray | `w:sz="18" w:color="808080"` | +| List indent | 0.25in per level | `w:left="360" w:hanging="360"` | +| List item spacing | 2pt after | `w:after="40"` | + +For CJK documents, adjust: body font to SimSun/YaHei, line spacing to 1.5x (`w:line="360"`), and set `w:eastAsia` on all `w:rFonts`. diff --git a/software-copyright-materials/vendor/docx-toolkit/references/design_principles.md b/software-copyright-materials/vendor/docx-toolkit/references/design_principles.md new file mode 100644 index 0000000..6d81dd3 --- /dev/null +++ b/software-copyright-materials/vendor/docx-toolkit/references/design_principles.md @@ -0,0 +1,819 @@ +# Design Principles for Document Typography + +WHY certain typographic choices look good -- the perceptual and psychological +reasons behind professional document design. Use this to make judgment calls +when exact specs are not provided. + +## Table of Contents + +1. [White Space & Breathing Room](#1-white-space--breathing-room) +2. [Contrast & Scale](#2-contrast--scale) +3. [Proximity & Grouping](#3-proximity--grouping) +4. [Alignment & Grid](#4-alignment--grid) +5. [Repetition & Consistency](#5-repetition--consistency) +6. [Visual Hierarchy & Flow](#6-visual-hierarchy--flow) + +--- + +## 1. White Space & Breathing Room + +### Why It Works + +The human eye does not read continuously. It jumps in saccades, fixating on +small clusters of words. White space provides landing zones for these fixations +and gives the reader's peripheral vision a "frame" that makes each text block +feel manageable. When a page is packed to the edges, every glance returns more +text than working memory can buffer, triggering fatigue and avoidance. + +Research on content density consistently shows: + +- **60-70% content coverage** feels comfortable and professional. +- **80%+** starts to feel dense and bureaucratic. +- **90%+** feels oppressive -- the reader unconsciously rushes or skips. +- **Below 50%** feels wasteful or pretentious (unless intentional, like poetry). + +Wider margins also carry cultural signals. Academic and luxury documents use +generous margins (1.25-1.5 inches). Internal memos and drafts use narrower +margins (0.75-1.0 inches). The margin width tells the reader how much care +went into the document before they read a single word. + +Line spacing has a direct physiological basis: the eye must track back to the +start of the next line after each line break. If lines are too close, the eye +"slips" to the wrong line. If too far apart, the eye loses its sense of +continuity. The sweet spot is 120-145% of the font size. + +**Rule of thumb: when in doubt, add more space, not less.** + +### Good Example + +``` +Margins: 1 inch (1440 twips) all sides for business documents. +Line spacing: 1.15 (276 twips at 240 twips-per-line = 115%). +Paragraph spacing after: 8pt (160 twips) between body paragraphs. +``` + +```xml + + + + + + + +``` + +This produces a page where content occupies roughly 65% of the area. The +reader sees clear top/bottom breathing room, and paragraphs are distinct +without feeling disconnected. + +``` + Page layout (good): + +----------------------------------+ + | 1" margin | + | +------------------------+ | + | | Heading | | + | | | | + | | Body text here with | | + | | comfortable spacing | | + | | between lines. | | + | | | | <- visible gap between paragraphs + | | Another paragraph of | | + | | body text follows. | | + | | | | + | +------------------------+ | + | 1" margin | + +----------------------------------+ +``` + +### Bad Example + +```xml + + + + + + + +``` + +This fills ~85% of the page. Text runs edge-to-edge with no visual rest stops. +The reader sees a wall of text. + +``` + Page layout (bad): + +----------------------------------+ + | Heading | + | Body text crammed right up to | + | the margins with no spacing | + | between lines or paragraphs. | + | Another paragraph starts here | + | and the reader cannot tell where | + | one idea ends and another begins | + | because everything blurs into a | + | single dense block of text. | + +----------------------------------+ +``` + +### Quick Test + +1. Zoom out to 50% in your document viewer. If you cannot see clear "channels" + of white between text blocks, the spacing is too tight. +2. Print a test page. Hold it at arm's length. The text area should look like + a rectangle floating in white, not filling the page. +3. Check: is the line spacing value at least 264 (`w:line` for 1.1x) for body + text? If it is 240 (single), it is too tight for anything over 10pt. + +--- + +## 2. Contrast & Scale + +### Why It Works + +The brain processes visual hierarchy through relative difference, not absolute +size. A 20pt heading above 11pt body text creates a clear "this is important" +signal. But if every heading is 20pt and every sub-heading is 19pt, the brain +cannot distinguish them -- they merge into the same level. + +The key insight is **modular scale**: font sizes that grow by a consistent +ratio. This mirrors natural proportions and feels harmonious for the same +reason musical intervals do. + +Common scales and their character: + +| Ratio | Name | Character | Example progression (from 11pt) | +|-------|----------------|---------------------------------|---------------------------------| +| 1.200 | Minor third | Subtle, refined | 11 → 13.2 → 15.8 → 19.0 | +| 1.250 | Major third | Balanced, professional | 11 → 13.75 → 17.2 → 21.5 | +| 1.333 | Perfect fourth | Strong, authoritative | 11 → 14.7 → 19.5 → 26.0 | +| 1.414 | Augmented 4th | Dramatic, presentation-style | 11 → 15.6 → 22.0 → 31.1 | + +For most business documents, 1.25 (major third) works best: + +``` +Body = 11pt (w:sz="22") +H3 = 13pt (w:sz="26") -- 11 * 1.25 ≈ 13.75, round to 13 +H2 = 16pt (w:sz="32") -- 13 * 1.25 ≈ 16.25, round to 16 +H1 = 20pt (w:sz="40") -- 16 * 1.25 = 20 +``` + +Beyond size, **weight contrast** creates hierarchy without consuming vertical +space. Regular (400) vs Bold (700) is visible at any size. Semi-bold (600) vs +Regular is subtle and best avoided unless you also vary size or color. + +**Color contrast** adds a third dimension. Dark blue headings (#1F3864) against +softer dark gray body text (#333333) signals "heading" without needing a huge +size jump. Pure black (#000000) body text is harsher than necessary on white +backgrounds -- #333333 or #2D2D2D reduces glare without losing legibility. + +### Good Example + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +``` + Visual hierarchy (good): + + [████████████████████] <- H1: 20pt bold navy (clearly dominant) + (generous space) + [██████████████] <- H2: 16pt bold navy (distinct step down) + (moderate space) + [████████████] <- H3: 13pt bold navy (smaller but still bold) + [░░░░░░░░░░░░░░░░░░░░░░] <- Body: 11pt regular gray + [░░░░░░░░░░░░░░░░░░░░░░] + [░░░░░░░░░░░░░░░░░░░░░░] +``` + +Each level is visually distinct from its neighbors. You can identify the +hierarchy even in peripheral vision. + +### Bad Example + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +Problems: +- H3 (12pt bold) and body (12pt regular) differ only by weight -- too subtle. +- H1 (14pt) to H2 (13pt) is a 1pt step -- invisible at reading distance. +- Everything is pure black so color provides no differentiating signal. +- The ratio between levels is ~1.07, far too flat. + +### Quick Test + +1. **The squint test**: blur your eyes or step back from the screen. Can you + count the number of heading levels? If two levels merge, their contrast + is insufficient. +2. **Ratio check**: divide each heading size by the next smaller size. If any + ratio is below 1.15, the levels will look too similar. +3. **Color check**: do headings look distinct from body text when you glance + at the page? If everything is the same color, you are relying solely on + size/weight, which limits your hierarchy to ~3 effective levels. + +--- + +## 3. Proximity & Grouping + +### Why It Works + +The Gestalt principle of proximity: items that are close together are perceived +as belonging to the same group. In document typography, this means a heading +must be **closer to the content it introduces** than to the content above it. + +If a heading sits equidistant between two paragraphs, it looks orphaned -- the +reader's eye does not know if it belongs to the text above or below. The fix +is asymmetric spacing: **large space before the heading, small space after**. + +The recommended ratio is 2:1 or 3:1 (space-before : space-after). + +This same principle applies to: +- **List items**: spacing between items should be less than spacing between + paragraphs. Items in a list are a group and should visually cluster. +- **Captions**: a figure caption should be close to its figure, not floating + in the middle between the figure and the next paragraph. +- **Table titles**: the title sits close above the table, with more space + separating the title from preceding text. + +### Good Example + +```xml + + + + + + + + + + + + + + + + +``` + +``` + Proximity (good): + + ...end of previous section text. + <- 18pt gap (w:before="360") + ## Section Heading + <- 6pt gap (w:after="120") + First paragraph of new section + continues here with content. + <- 8pt gap (w:after="160") + Second paragraph follows. + + The heading clearly "belongs to" the text below it. +``` + +``` + List grouping (good): + + Consider these factors: + - First item <- 2pt gap between items + - Second item <- items cluster as a group + - Third item + <- 8pt gap after list + The next paragraph starts here. +``` + +### Bad Example + +```xml + + + + + + + + + + + +``` + +``` + Proximity (bad): + + ...end of previous section text. + <- 12pt gap + ## Section Heading + <- 12pt gap (same!) + First paragraph of new section. + + The heading floats between sections. It is unclear what it belongs to. +``` + +``` + List grouping (bad): + + Consider these factors: + <- 10pt gap + - First item + <- 10pt gap (same as paragraphs) + - Second item + <- 10pt gap + - Third item + <- 10pt gap + Next paragraph. + + The list does not feel like a group. Each item looks like a + separate paragraph that happens to have a bullet. +``` + +### Quick Test + +1. **Cover test**: cover the heading text. Looking only at the whitespace, + can you tell which block of text the heading belongs to? If the gaps above + and below are equal, the answer is "no." +2. **Number check**: `w:before` on headings should be at least 2x `w:after`. + Common good values: before=360 / after=120, or before=240 / after=80. +3. **List check**: `w:after` on list items should be less than half of + `w:after` on body paragraphs. If body uses 160, list items should use + 40-60. + +--- + +## 4. Alignment & Grid + +### Why It Works + +Alignment creates invisible lines that the eye follows down the page. When +elements share the same left edge, the reader perceives order and intention. +When elements are slightly misaligned (off by a few twips), the page looks +sloppy even if the reader cannot consciously identify why. + +**Left-align vs Justify:** + +- **Left-aligned** (ragged right) is best for English and other Latin-script + languages. The uneven right edge actually helps reading because each line + has a unique silhouette, making it easier for the eye to find the next line. + Justified text forces uneven word spacing that creates distracting "rivers" + of white running vertically through paragraphs. + +- **Justified** is best for CJK text. Chinese, Japanese, and Korean characters + are monospaced by design -- each occupies the same cell in an invisible grid. + Justification preserves this grid perfectly. Ragged right in CJK text breaks + the grid and looks untidy. + +**Indentation rule:** Use first-line indent OR paragraph spacing to separate +paragraphs -- never both. They serve the same purpose (marking paragraph +boundaries). Using both wastes space and creates visual stutter. + +- Western convention: paragraph spacing (no indent) is more modern. +- CJK convention: first-line indent of 2 characters is standard. +- Academic convention: first-line indent of 0.5 inch is traditional. + +### Good Example + +```xml + + + + + + + + + + + + + + + + + + + + + +``` + +``` + English paragraph separation (good -- spacing, no indent): + + This is the first paragraph with some text + that wraps to a second line naturally. + + This is the second paragraph. The gap above + clearly marks the boundary. + + + CJK paragraph separation (good -- indent, no spacing): + +   第一段正文内容从这里开始,使用两个字符 + 的首行缩进来标记段落边界。 +   第二段紧跟其后,没有段间距,但首行缩进 + 清晰地标识了新段落的开始。 +``` + +### Bad Example + +```xml + + + + + + + + + + + + + +``` + +Problems: +- Justified English text with narrow columns creates uneven word gaps. +- Using both first-line indent AND paragraph spacing is redundant. +- Left-aligned CJK breaks the character grid that CJK readers expect. +- CJK with spacing-based separation looks like translated western layout. + +### Quick Test + +1. **River test**: in justified English text, squint and look for vertical + white streaks running through the paragraph. If you see them, switch to + left-align or increase the column width. +2. **Double signal check**: does the document use BOTH first-line indent AND + paragraph spacing? If yes, remove one. Choose indent for CJK/academic, + spacing for modern western. +3. **Tab alignment**: if you use tabs for columns, do all tab stops across + the document use the same positions? Inconsistent tab stops create jagged + invisible grid lines. + +--- + +## 5. Repetition & Consistency + +### Why It Works + +Consistency is a trust signal. When a reader sees that every H2 looks the same, +every table follows the same pattern, and every page number sits in the same +spot, they unconsciously trust that the document was crafted with care. A single +inconsistency -- one H2 that is 15pt instead of 14pt, one table with different +borders -- breaks that trust and makes the reader question the content. + +Consistency also reduces cognitive load. Once the reader learns "bold dark blue += section heading," they stop spending mental effort on identifying structure +and focus entirely on content. Every inconsistency forces them to re-evaluate: +"Is this a different kind of heading, or did someone just forget to apply the +style?" + +The implementation rule is simple: **use named styles, not direct formatting.** +If you define Heading2 as a style and apply it everywhere, consistency is +automatic. If you manually set font size, bold, and color on each heading +individually, inconsistency is inevitable. + +### Good Example + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + Market Analysis + +``` + +When using a table style, define it once and reference it for every table: + +```xml + + + + + +``` + +### Bad Example + +```xml + + + + + + + + + + + + Market Analysis + + + + + + + + + + + + + + + Financial Overview + + +``` + +Problems: +- No style references -- everything is direct formatting. +- Second H2 has different size (30 vs 32), color, and spacing. +- If there are 20 headings, each could drift slightly differently. +- Changing the design later means editing every heading individually. + +### Quick Test + +1. **Style audit**: does every paragraph reference a `w:pStyle`? If you find + paragraphs with only direct formatting and no style, that is a consistency + risk. +2. **Search for variance**: search the XML for all `w:sz` values used with + `w:b` (bold). If you find three different sizes for what should be the same + heading level, there is an inconsistency. +3. **Table check**: do all tables in the document reference the same + `w:tblStyle`? If some tables have manual border definitions while others + use a style, the document will look patchy. +4. **Page numbers**: check that header/footer content is defined in the + default section properties and inherited by all sections, not redefined + inconsistently in each section. + +--- + +## 6. Visual Hierarchy & Flow + +### Why It Works + +A well-designed document guides the reader's eye in a predictable path: +title at the top, subtitle below it, section headings as signposts, body text +as the main content, footnotes and captions as supporting details. This flow +mirrors reading priority -- the most important information is the most visually +prominent. + +Each level in the hierarchy must be **distinguishable from its adjacent +levels**. It is not enough for H1 to differ from body text; H1 must also +clearly differ from H2, and H2 from H3. If any two adjacent levels are too +similar, the hierarchy collapses at that point. + +Effective hierarchy uses **multiple simultaneous signals**: + +| Level | Size | Weight | Color | Spacing above | +|----------|-------|---------|---------|---------------| +| Title | 26pt | Bold | #1F3864 | 0 (top) | +| Subtitle | 15pt | Regular | #4472C4 | 4pt | +| H1 | 20pt | Bold | #1F3864 | 24pt | +| H2 | 16pt | Bold | #1F3864 | 18pt | +| H3 | 13pt | Bold | #1F3864 | 12pt | +| Body | 11pt | Regular | #333333 | 0pt | +| Caption | 9pt | Italic | #666666 | 4pt | +| Footnote | 9pt | Regular | #666666 | 0pt | + +Notice how each level differs from its neighbors on at least two dimensions +(size + weight, or size + color, or weight + style). Single-dimension +differences are fragile and can be missed. + +**Section breaks** create rhythm in long documents. A page break before each +major section (H1) gives the reader a mental reset. Within sections, consistent +heading + body patterns create a predictable cadence that makes long documents +less intimidating. + +### Good Example + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +``` + Visual flow (good): + + +----------------------------------+ + | | + | ANNUAL REPORT 2025 | <- Title: 26pt bold navy centered + | Acme Corporation | <- Subtitle: 15pt regular blue + | | + | | + +----------------------------------+ + + +----------------------------------+ + | | + | 1. Executive Summary | <- H1: 20pt bold navy (page break) + | | + | Body text introducing the | <- Body: 11pt regular gray + | main findings of the year. | + | | + | 1.1 Revenue Highlights | <- H2: 16pt bold navy + | | + | Revenue grew by 23% year | <- Body + | over year, driven by... | + | | + | Figure 1: Revenue Growth | <- Caption: 9pt italic gray + | | + +----------------------------------+ + + Each level is immediately identifiable. The eye flows naturally + from title -> heading -> body -> caption. +``` + +### Bad Example + +```xml + + + + + + + + + + + + + + + + + + + +``` + +Problems: +- H1 at 14pt is too close to body at 11pt (ratio 1.27 -- acceptable in + isolation but with black color matching body, the hierarchy is weak). +- Caption is indistinguishable from body text. +- No page breaks means major sections bleed into each other with no + visual rhythm. +- Everything is black, so color provides zero hierarchy signal. + +### Quick Test + +1. **The squint test**: blur your eyes while looking at a full page. You + should see 3-4 distinct "weight levels" of gray. If the page looks like + one uniform shade, the hierarchy is too flat. +2. **The scan test**: flip through pages quickly. Can you identify section + boundaries in under one second per page? If yes, the visual hierarchy is + working. If pages blur together, you need stronger differentiation at H1. +3. **Adjacent level test**: for each heading level, check that it differs + from the next level on at least 2 of: size, weight, color, style (italic). + Single-dimension differences get lost. +4. **Rhythm test**: in a document over 10 pages, do major sections (H1) start + on new pages? If not, long documents will feel like an undifferentiated + stream. Add `w:pageBreakBefore` to Heading1. + +--- + +## Summary: Decision Checklist + +When you are unsure about a typographic choice, run through these checks: + +| Principle | Question | If No... | +|-----------|----------|----------| +| White Space | Does the page have at least 30% white space? | Increase margins or spacing | +| Contrast | Can I count heading levels by squinting? | Increase size ratios (target 1.25x) | +| Proximity | Does each heading clearly belong to text below it? | Make space-before > space-after (2:1) | +| Alignment | Is English left-aligned and CJK justified? | Switch alignment mode | +| Repetition | Do all same-level elements use the same style? | Replace direct formatting with styles | +| Hierarchy | Can I see the document structure at arm's length? | Add more differentiation signals | + +**When two principles conflict, prioritize in this order:** + +1. **Readability** (white space, line spacing) -- always wins +2. **Hierarchy** (contrast, scale) -- readers must find what they need +3. **Consistency** (repetition) -- builds trust +4. **Aesthetics** (alignment, grouping) -- the finishing touch diff --git a/software-copyright-materials/vendor/docx-toolkit/references/openxml_element_order.md b/software-copyright-materials/vendor/docx-toolkit/references/openxml_element_order.md new file mode 100644 index 0000000..a84b5a2 --- /dev/null +++ b/software-copyright-materials/vendor/docx-toolkit/references/openxml_element_order.md @@ -0,0 +1,308 @@ +# OpenXML Child Element Ordering Rules + +Element ordering in OpenXML is defined by the XSD schema. Incorrect ordering produces invalid documents that Word may refuse to open or silently repair (potentially losing data). + +> **Key rule**: Properties elements (`*Pr`) must always be the **first child** of their parent. + +--- + +## w:document + +``` +Children in order: +1. w:background [0..1] — page background color/fill +2. w:body [0..1] — document content container +``` + +--- + +## w:body + +``` +Children in order (repeating group): +1. w:p [0..*] — paragraph +2. w:tbl [0..*] — table +3. w:sdt [0..*] — structured document tag (content control) +4. w:sectPr [0..1] — LAST child: final section properties +``` + +Note: `w:p`, `w:tbl`, and `w:sdt` are interleaved in document order. The only strict rule is that `w:sectPr` must be the **last child** of `w:body`. + +--- + +## w:p (Paragraph) + +``` +Children in order: +1. w:pPr [0..1] — paragraph properties (MUST be first) + +Then any mix of (interleaved in document order): +- w:r [0..*] — run +- w:hyperlink [0..*] — hyperlink wrapper +- w:ins [0..*] — tracked insertion +- w:del [0..*] — tracked deletion +- w:bookmarkStart [0..*] — bookmark anchor start +- w:bookmarkEnd [0..*] — bookmark anchor end +- w:commentRangeStart [0..*] — comment range start +- w:commentRangeEnd [0..*] — comment range end +- w:proofErr [0..*] — proofing error marker +- w:fldSimple [0..*] — simple field +- w:sdt [0..*] — inline content control +- w:smartTag [0..*] — smart tag +``` + +**Practical note**: After `w:pPr`, the remaining children appear in document reading order. Runs, hyperlinks, bookmarks, and comment ranges intermix freely based on their position in the text. + +--- + +## w:pPr (Paragraph Properties) + +``` +Children in order: +1. w:pStyle [0..1] — paragraph style reference +2. w:keepNext [0..1] — keep with next paragraph +3. w:keepLines [0..1] — keep lines together +4. w:pageBreakBefore [0..1] — page break before paragraph +5. w:framePr [0..1] — text frame properties +6. w:widowControl [0..1] — widow/orphan control +7. w:numPr [0..1] — numbering properties +8. w:suppressLineNumbers [0..1] +9. w:pBdr [0..1] — paragraph borders +10. w:shd [0..1] — shading +11. w:tabs [0..1] — tab stops +12. w:suppressAutoHyphens [0..1] +13. w:kinsoku [0..1] — CJK kinsoku settings +14. w:wordWrap [0..1] +15. w:overflowPunct [0..1] +16. w:topLinePunct [0..1] +17. w:autoSpaceDE [0..1] +18. w:autoSpaceDN [0..1] +19. w:bidi [0..1] — right-to-left paragraph +20. w:adjustRightInd [0..1] +21. w:snapToGrid [0..1] +22. w:spacing [0..1] — line and paragraph spacing +23. w:ind [0..1] — indentation +24. w:contextualSpacing [0..1] +25. w:mirrorIndents [0..1] +26. w:suppressOverlap [0..1] +27. w:jc [0..1] — justification (left/center/right/both) +28. w:textDirection [0..1] +29. w:textAlignment [0..1] +30. w:outlineLvl [0..1] — outline level +31. w:divId [0..1] +32. w:rPr [0..1] — run properties for paragraph mark +33. w:sectPr [0..1] — section break (section ends at this paragraph) +34. w:pPrChange [0..1] — tracked paragraph property change +``` + +--- + +## w:r (Run) + +``` +Children in order: +1. w:rPr [0..1] — run properties (MUST be first) + +Then any of (one per run, typically): +- w:t [0..*] — text content +- w:br [0..*] — break (line, page, column) +- w:tab [0..*] — tab character +- w:cr [0..*] — carriage return +- w:sym [0..*] — symbol character +- w:drawing [0..*] — DrawingML object (images) +- w:pict [0..*] — VML picture (legacy) +- w:fldChar [0..*] — complex field character +- w:instrText [0..*] — field instruction text +- w:delText [0..*] — deleted text (inside w:del) +- w:footnoteReference [0..*] +- w:endnoteReference [0..*] +- w:commentReference [0..*] +- w:lastRenderedPageBreak [0..*] +``` + +--- + +## w:rPr (Run Properties) + +``` +Children in order: +1. w:rStyle [0..1] — character style reference +2. w:rFonts [0..1] — font specification +3. w:b [0..1] — bold +4. w:bCs [0..1] — complex script bold +5. w:i [0..1] — italic +6. w:iCs [0..1] — complex script italic +7. w:caps [0..1] — all capitals +8. w:smallCaps [0..1] — small capitals +9. w:strike [0..1] — strikethrough +10. w:dstrike [0..1] — double strikethrough +11. w:outline [0..1] +12. w:shadow [0..1] +13. w:emboss [0..1] +14. w:imprint [0..1] +15. w:noProof [0..1] — suppress proofing +16. w:snapToGrid [0..1] +17. w:vanish [0..1] — hidden text +18. w:color [0..1] — text color +19. w:spacing [0..1] — character spacing +20. w:w [0..1] — character width scaling +21. w:kern [0..1] — font kerning +22. w:position [0..1] — vertical position (raise/lower) +23. w:sz [0..1] — font size (half-points) +24. w:szCs [0..1] — complex script font size +25. w:highlight [0..1] — text highlight color +26. w:u [0..1] — underline +27. w:effect [0..1] — text effect (animated) +28. w:bdr [0..1] — run border +29. w:shd [0..1] — run shading +30. w:vertAlign [0..1] — superscript/subscript +31. w:rtl [0..1] — right-to-left +32. w:cs [0..1] — complex script +33. w:lang [0..1] — language +34. w:rPrChange [0..1] — tracked run property change +``` + +--- + +## w:tbl (Table) + +``` +Children in order: +1. w:tblPr [1..1] — table properties (REQUIRED, must be first) +2. w:tblGrid [1..1] — column width definitions (REQUIRED) +3. w:tr [1..*] — table row(s) +``` + +--- + +## w:tblPr (Table Properties) + +``` +Children in order: +1. w:tblStyle [0..1] — table style reference +2. w:tblpPr [0..1] — table positioning +3. w:tblOverlap [0..1] +4. w:bidiVisual [0..1] — right-to-left table +5. w:tblStyleRowBandSize [0..1] +6. w:tblStyleColBandSize [0..1] +7. w:tblW [0..1] — preferred table width +8. w:jc [0..1] — table alignment +9. w:tblCellSpacing [0..1] +10. w:tblInd [0..1] — table indent from margin +11. w:tblBorders [0..1] — table borders +12. w:shd [0..1] — table shading +13. w:tblLayout [0..1] — fixed or autofit +14. w:tblCellMar [0..1] — default cell margins +15. w:tblLook [0..1] — conditional formatting flags +16. w:tblCaption [0..1] — accessibility caption +17. w:tblDescription [0..1] — accessibility description +18. w:tblPrChange [0..1] — tracked table property change +``` + +--- + +## w:tr (Table Row) + +``` +Children in order: +1. w:trPr [0..1] — row properties (must be first) +2. w:tc [1..*] — table cell(s) +``` + +--- + +## w:trPr (Table Row Properties) + +``` +Children in order: +1. w:cnfStyle [0..1] — conditional formatting +2. w:divId [0..1] +3. w:gridBefore [0..1] — grid columns before first cell +4. w:gridAfter [0..1] — grid columns after last cell +5. w:wBefore [0..1] +6. w:wAfter [0..1] +7. w:cantSplit [0..1] — don't split row across pages +8. w:trHeight [0..1] — row height +9. w:tblHeader [0..1] — repeat as header row +10. w:tblCellSpacing [0..1] +11. w:jc [0..1] — row alignment +12. w:hidden [0..1] +13. w:ins [0..1] — tracked row insertion +14. w:del [0..1] — tracked row deletion +15. w:trPrChange [0..1] — tracked row property change +``` + +--- + +## w:tc (Table Cell) + +``` +Children in order: +1. w:tcPr [0..1] — cell properties (must be first) +2. w:p [1..*] — paragraph(s) — at least one required +3. w:tbl [0..*] — nested table(s) +``` + +--- + +## w:tcPr (Table Cell Properties) + +``` +Children in order: +1. w:cnfStyle [0..1] +2. w:tcW [0..1] — cell width +3. w:gridSpan [0..1] — horizontal merge (column span) +4. w:hMerge [0..1] — legacy horizontal merge +5. w:vMerge [0..1] — vertical merge +6. w:tcBorders [0..1] — cell borders +7. w:shd [0..1] — cell shading +8. w:noWrap [0..1] +9. w:tcMar [0..1] — cell margins +10. w:textDirection [0..1] +11. w:tcFitText [0..1] +12. w:vAlign [0..1] — vertical alignment +13. w:hideMark [0..1] +14. w:tcPrChange [0..1] — tracked cell property change +``` + +--- + +## w:sectPr (Section Properties) + +``` +Children in order: +1. w:headerReference [0..*] — header references (type: default/first/even) +2. w:footerReference [0..*] — footer references +3. w:endnotePr [0..1] +4. w:footnotePr [0..1] +5. w:type [0..1] — section break type (nextPage/continuous/evenPage/oddPage) +6. w:pgSz [0..1] — page size +7. w:pgMar [0..1] — page margins +8. w:paperSrc [0..1] +9. w:pgBorders [0..1] — page borders +10. w:lnNumType [0..1] — line numbering +11. w:pgNumType [0..1] — page numbering +12. w:cols [0..1] — column definitions +13. w:formProt [0..1] +14. w:vAlign [0..1] — vertical alignment of page +15. w:noEndnote [0..1] +16. w:titlePg [0..1] — different first page header/footer +17. w:textDirection [0..1] +18. w:bidi [0..1] +19. w:rtlGutter [0..1] +20. w:docGrid [0..1] — document grid +21. w:sectPrChange [0..1] — tracked section property change +``` + +--- + +## w:hdr (Header) / w:ftr (Footer) + +``` +Children (same structure as w:body content): +1. w:p [0..*] — paragraph(s) +2. w:tbl [0..*] — table(s) +3. w:sdt [0..*] — content controls +``` + +Headers and footers are essentially mini-documents. They follow the same content model as `w:body` but without a final `w:sectPr`. diff --git a/software-copyright-materials/vendor/docx-toolkit/references/openxml_encyclopedia_part1.md b/software-copyright-materials/vendor/docx-toolkit/references/openxml_encyclopedia_part1.md new file mode 100644 index 0000000..177182f --- /dev/null +++ b/software-copyright-materials/vendor/docx-toolkit/references/openxml_encyclopedia_part1.md @@ -0,0 +1,4061 @@ +# OpenXML SDK 3.x Complete Reference Encyclopedia + +**Target:** DocumentFormat.OpenXml 3.x / .NET 8+ / C# 12 +**Last Updated:** 2026-03-22 + +This document serves as an exhaustive reference for building DOCX files with the OpenXML SDK. Every code block is ready to copy-paste. + +--- + +## Namespace Aliases Used Throughout + +```csharp +using DocumentFormat.OpenXml; +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Wordprocessing; +``` + +--- + +## Table of Contents + +1. [Document Creation Skeleton](#1-document-creation-skeleton) +2. [Style System Deep Dive](#2-style-system-deep-dive) +3. [Character Formatting (RunProperties)](#3-character-formatting-runproperties--exhaustive) +4. [Paragraph Formatting (ParagraphProperties)](#4-paragraph-formatting-paragraphproperties--exhaustive) + +--- + +## 1. Document Creation Skeleton + +### 1.1 Complete Flow: Create to Save + +```csharp +// ============================================================================= +// DOCUMENT CREATION SKELETON +// ============================================================================= +// This is the minimal complete flow for creating a valid DOCX from scratch. +// Follow these steps in order: Create -> AddParts -> AddContent -> Save. +// +// Key insight: WordprocessingDocument.Create() adds MainDocumentPart automatically, +// but all other parts (Styles, Settings, Numbering, Theme) must be added manually. + +// --- STEP 1: CREATE THE PACKAGE --- +// The file path can be absolute or relative. WordprocessingDocumentType.Document +// is the standard choice for .docx files (vs. Template, MacroEnabled, etc.) +string outputPath = "C:\\Docs\\MyDocument.docx"; + +using var doc = WordprocessingDocument.Create( + outputPath, // File path + WordprocessingDocumentType.Document, // Document type enum + new DocumentOptions // Optional: AutoSave, etc. + { + AutoSave = false // true = flush changes automatically + }); + +// --- STEP 2: GET OR CREATE THE MAIN DOCUMENT PART --- +// When you call Create(), MainDocumentPart is automatically created and linked. +// You access it via .MainDocumentPart (not .AddMainDocumentPart, which would add +// a SECOND main part — illegal). For a fresh document, just use .MainDocumentPart. +var mainPart = doc.MainDocumentPart!; +var body = mainPart.Document.Body!; // Body is created automatically with the part + +// --- STEP 3: ADD ADDITIONAL PARTS --- +// These are OPTIONAL but recommended for a complete document: +// - StyleDefinitionsPart: required for styles +// - NumberingDefinitionsPart: required for bullets/numbers +// - DocumentSettingsPart: zoom, proof state, tab stops, compatibility +// - ThemePart: color/theme information +// Parts are created fresh and linked via relationships. + +// Example: Add styles part (covered in Section 2) +var stylesPart = mainPart.AddNewPart(); +stylesPart.Styles = new Styles(); +stylesPart.Styles.Save(); + +// Example: Add settings part (covered in 1.4) +var settingsPart = mainPart.AddNewPart(); +settingsPart.Settings = new Settings(); +settingsPart.Settings.Save(); + +// --- STEP 4: ADD CONTENT TO BODY --- +// Body accepts: Paragraph (w:p), Table (w:tbl), Structured Document Tag (w:sdt) +// Content is added in document order (no need for explicit index). +// IMPORTANT: SectionProperties (w:sectPr) MUST be the last child of body. +body.Append(new Paragraph( + new Run(new Text("Hello, World!")))); + +// --- STEP 5: SET SECTION PROPERTIES (PAGE LAYOUT) --- +// sectPr defines page size, margins, headers/footers, columns, etc. +// It must be the last child of body. If missing, Word uses defaults (Letter/A4, 1" margins). +var sectPr = new SectionProperties(); + +// Page Size: Width/Height in DXA (1 inch = 1440 DXA) +// Letter: 12240 x 15840 DXA (8.5" x 11") +// A4: 11906 x 16838 DXA (210mm x 297mm) +sectPr.Append(new PageSize +{ + Width = 12240u, // 8.5 inches + Height = 15840u // 11 inches +}); + +// Page Margins: all four margins in DXA +// Note: Top+Bottom margins + HeaderDistance = distance from page edge to text +sectPr.Append(new PageMargin +{ + Top = 1440, // 1 inch + Bottom = 1440, // 1 inch + Left = 1440u, // 1 inch (uint required) + Right = 1440u, // 1 inch + Header = 720u, // 0.5 inch from page edge to header + Footer = 720u // 0.5 inch from page edge to footer +}); + +// Attach sectPr to body (must be last) +body.Append(sectPr); + +// --- STEP 6: SAVE --- +// Because we use `using`, Dispose() is called automatically when the block exits. +// Dispose() saves the file. If you forget `using`, call doc.Save() explicitly. +``` + +### 1.2 Opening an Existing Document + +```csharp +// ============================================================================= +// OPENING EXISTING DOCUMENTS +// ============================================================================= +// Open() has multiple overloads: +// 1. Open(string path, bool isEditable, AutoSave) +// 2. Open(Stream, bool isEditable, AutoSave) +// 3. Open(string path, bool isEditable, OpenSettings) +// +// isEditable=true means open for read/write. false = read-only. +// isEditable=false is faster (shared locks avoided) but throws if file is read-only. + +// --- OPEN FOR EDITING (READ/WRITE) --- +string inputPath = "C:\\Docs\\Existing.docx"; +using var editDoc = WordprocessingDocument.Open( + inputPath, + isEditable: true, // Required for modification + new OpenSettings + { + AutoSave = true // Automatically save on Dispose + }); + +var body = editDoc.MainDocumentPart!.Document.Body!; +// ... make changes ... +// No explicit Save() needed if AutoSave = true + +// --- OPEN AS READ-ONLY (FASTER) --- +using var readOnlyDoc = WordprocessingDocument.Open( + inputPath, + isEditable: false, // Read-only mode + new OpenSettings + { + // MarkupDeclarationProcess options + }); + +// --- OPEN FROM STREAM --- +byte[] fileBytes = File.ReadAllBytes(inputPath); +using var streamDoc = WordprocessingDocument.Open( + new MemoryStream(fileBytes), + isEditable: true, + new OpenSettings { AutoSave = false }); + +// After editing, you MUST copy the stream back to file if AutoSave=false: +// streamDoc.MainDocumentPart.Document.Save(); +// File.WriteAllBytes(outputPath, streamStream.ToArray()); + +// --- OPEN FROM HTTP RESPONSE (WEB SCENARIO) --- +using var httpClient = new HttpClient(); +var response = await httpClient.GetAsync("https://example.com/document.docx"); +using var webStream = await response.Content.ReadAsStreamAsync(); +using var webDoc = WordprocessingDocument.Open(webStream, isEditable: true); +``` + +### 1.3 Stream-Based Creation (MemoryStream for Web) + +```csharp +// ============================================================================= +// STREAM-BASED DOCUMENT CREATION +// ============================================================================= +// Use MemoryStream when you want to: +// 1. Generate a document in memory before sending to a client +// 2. Avoid touching the filesystem (ASP.NET Core scenarios) +// 3. Return a document from an API endpoint +// +// CRITICAL: The stream MUST be seekable when you call .Open(). +// After WordprocessingDocument.Create(), the stream position is at the beginning. +// If you write to the stream BEFORE creating the document, seek to 0 first. + +// --- CREATE IN MEMORY --- +MemoryStream memStream = new MemoryStream(); + +// Create directly on a stream (no file path involved) +using (var doc = WordprocessingDocument.Create( + memStream, + WordprocessingDocumentType.Document, + new DocumentOptions { AutoSave = false })) +{ + var mainPart = doc.MainDocumentPart!; + mainPart.Document = new Document(new Body()); + mainPart.Document.Body!.Append(new Paragraph( + new Run(new Text("Generated in memory")))); + mainPart.Document.Save(); // Save to the underlying stream +} +// At this point, memStream contains the complete DOCX + +// --- SEND TO HTTP RESPONSE (ASP.NET Core) --- +// In an API controller: +[HttpGet("download")] +public async Task DownloadDocument() +{ + var memStream = new MemoryStream(); + + using (var doc = WordprocessingDocument.Create( + memStream, + WordprocessingDocumentType.Document)) + { + var mainPart = doc.MainDocumentPart!; + mainPart.Document = new Document(new Body()); + mainPart.Document.Body!.Append(new Paragraph( + new Run(new Text("Download me!")))); + mainPart.Document.Save(); + } + + memStream.Position = 0; // IMPORTANT: Reset position for reading + return File(memStream, + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "GeneratedDocument.docx"); +} + +// --- CREATE FROM TEMPLATE IN MEMORY --- +// Useful for mail-merge style operations +MemoryStream templateStream = new MemoryStream(); +File.WriteAllBytes("template.docx", templateStream.ToArray()); // Save a template first + +using var templateSource = new MemoryStream(File.ReadAllBytes("template.docx")); +using var mergedDoc = (WordprocessingDocument)templateSource.Clone(); + +// Clone() creates an editable copy. Don't forget to set position: +mergedDoc.MainDocumentPart!.Document.Body!.Append(new Paragraph( + new Run(new Text("Added content")))); +``` + +### 1.4 Adding All Standard Parts + +```csharp +// ============================================================================= +// ADDING ALL STANDARD DOCUMENT PARTS +// ============================================================================= +// A complete document should have: +// 1. MainDocumentPart (auto-created) +// 2. StyleDefinitionsPart +// 3. NumberingDefinitionsPart +// 4. DocumentSettingsPart +// 5. ThemePart (optional) +// 6. Custom parts (headers, footers, comments, etc.) + +// --- COMPLETE SETUP METHOD --- +public static void CreateCompleteDocument(string path) +{ + using var doc = WordprocessingDocument.Create(path, WordprocessingDocumentType.Document); + var mainPart = doc.MainDocumentPart!; + + // Initialize document + mainPart.Document = new Document(new Body()); + var body = mainPart.Document.Body!; + + // Add all parts + AddStylesPart(mainPart); + AddNumberingPart(mainPart); + AddSettingsPart(mainPart); + AddThemePart(mainPart); + AddHeadersAndFooters(mainPart); + + // Add sample content + AddSampleContent(body); + + // Section properties MUST be last + body.Append(CreateSectionProperties()); + + mainPart.Document.Save(); +} + +// --- STYLES PART --- +// See Section 2 for detailed style creation +private static void AddStylesPart(MainDocumentPart mainPart) +{ + var stylesPart = mainPart.AddNewPart(); + var styles = new Styles(); + + // DocDefaults: document-wide defaults for run and paragraph properties + // These apply when no explicit style or direct formatting overrides them + styles.Append(new DocDefaults( + new RunPropertiesDefault( + new RunPropertiesBaseStyle( + new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri" }, + new FontSize { Val = "22" }, // 22 half-points = 11pt + new FontSizeComplexScript { Val = "22" } + ) + ), + new ParagraphPropertiesDefault( + new ParagraphPropertiesBaseStyle( + new SpacingBetweenLines { After = "200", Line = "276", LineRule = LineSpacingRuleValues.Auto } + ) + ) + )); + + // Default Normal style + styles.Append(new Style( + new StyleName { Val = "Normal" }, + new PrimaryStyle() + ) + { Type = StyleValues.Paragraph, StyleId = "Normal", Default = true }); + + stylesPart.Styles = styles; + stylesPart.Styles.Save(); +} + +// --- NUMBERING PART --- +// Required for bulleted and numbered lists +private static void AddNumberingPart(MainDocumentPart mainPart) +{ + var numberingPart = mainPart.AddNewPart(); + var numbering = new Numbering(); + + // AbstractNum defines the list format (bullet, number, multilevel) +// Creates a bullet list definition with 3 levels + var abstractNum = new AbstractNum { AbstractNumberId = 1 }; + + // Level 0: Bullet (dot) + abstractNum.Append(new Level( + new StartNumberingValue { Val = 1 }, + new NumberingFormat { Val = NumberFormatValues.Bullet }, + new LevelText { Val = "•" }, + new LevelJustification { Val = LevelJustificationValues.Left }, + new PreviousParagraphProperties( + new Indentation { Left = "720", Hanging = "360" }) // 720 DXA indent, 360 DXA hanging + ) + { LevelIndex = 0 }); + + // Level 1: Dash + abstractNum.Append(new Level( + new StartNumberingValue { Val = 1 }, + new NumberingFormat { Val = NumberFormatValues.Bullet }, + new LevelText { Val = "–" }, + new LevelJustification { Val = LevelJustificationValues.Left }, + new PreviousParagraphProperties( + new Indentation { Left = "1440", Hanging = "360" }) + ) + { LevelIndex = 1 }); + + // Level 2: Circle + abstractNum.Append(new Level( + new StartNumberingValue { Val = 1 }, + new NumberingFormat { Val = NumberFormatValues.Bullet }, + new LevelText { Val = "◦" }, + new LevelJustification { Val = LevelJustificationValues.Left }, + new PreviousParagraphProperties( + new Indentation { Left = "2160", Hanging = "360" }) + ) + { LevelIndex = 2 }); + + numbering.Append(abstractNum); + + // NumberingInstance links to AbstractNum and assigns a numId + numbering.Append(new NumberingInstance( + new AbstractNumId { Val = 1 } + ) + { NumberID = 1 }); + + numberingPart.Numbering = numbering; + numberingPart.Numbering.Save(); +} + +// --- SETTINGS PART --- +// Contains document-level settings: zoom, proof state, default tab stop, etc. +private static void AddSettingsPart(MainDocumentPart mainPart) +{ + var settingsPart = mainPart.AddNewPart(); + var settings = new Settings(); + + // Zoom: document zoom percentage (default 100%) + // Val is a percentage value (e.g., "100" = 100%) + settings.Append(new Zoom { Val = "100", Percent = true, SnapToGrid = true }); + + // ProofState: spelling/grammar check state + // Val combines bits: 1=grammar, 2=spelling, 3=both + settings.Append(new ProofState { Val = ProofingStateValues.Clean }); + + // Default tab stop interval in DXA + // Word inserts tab stops every 720 DXA (0.5 inch) by default + settings.Append(new DefaultTabStop { Val = 720 }); + + // Character spacing control: automatically adjust character spacing + // to maintain consistent line spacing (similar to InDesign) + settings.Append(new CharacterSpacingControl { Val = CharacterSpacingValues.CompressPunctuation }); + + // Compatibility settings: controls how Word handles certain formatting + // to ensure compatibility with different Word versions + settings.Append(new Compatibility( + new UseFELayout(), // Use formatted East Asian layout + new UseAsianDigraphicLineBreakRules(), // CJK line breaking rules + new AllowSpaceOfSameStyleInTable(), // Table cell spacing + new DoNotUseIndentAsPercentageForTabStops(), // Legacy tab behavior + new ProportionalOtherIndents(), // Proportional indents + new LayoutTableRawTextInTable() // Raw text in layout tables + )); + + // Revision tracking view settings + settings.Append(new RevisionView { DocPart = false, Formatting = true, Ink = true, Markup = true }); + + settingsPart.Settings = settings; + settingsPart.Settings.Save(); +} + +// --- THEME PART --- +// Defines color scheme, font scheme, and format scheme for the document theme +private static void AddThemePart(MainDocumentPart mainPart) +{ + var themePart = mainPart.AddNewPart(); + var theme = new Theme( + new ThemeElements( + // Color scheme: 10 predefined theme colors + new ColorScheme( + new Dark1Color(new Color { Val = "000000" }), + new Light1Color(new Color { Val = "FFFFFF" }), + new Dark2Color(new Color { Val = "1F497D" }), + new Light2Color(new Color { Val = "EEECE1" }), + new Accent1Color(new Color { Val = "4F81BD" }), + new Accent2Color(new Color { Val = "C0504D" }), + new Accent3Color(new Color { Val = "9BBB59" }), + new Accent4Color(new Color { Val = "8064A2" }), + new Accent5Color(new Color { Val = "4BACC6" }), + new Accent6Color(new Color { Val = "F79646" }), + new Hyperlink(new Color { Val = "0000FF" }), + new FollowedHyperlinkColor(new Color { Val = "800080" }) + ), + // Font scheme: major (headings) and minor (body) fonts + new FontScheme( + new MajorFont { Val = "Calibri Light" }, + new MinorFont { Val = "Calibri" } + ), + // Format scheme: default fill and effect styles + new FormatScheme( + new FillStyleList( + new FillStyle { Fill = new PatternFill { PatternType = PatternValues.Solid } } + ), + new LineStyleList( + new LineStyle { Val = LineValues.Single } + ) + ) + ), + new ThemeName { Val = "Office Theme" }, + new ThemeNames( + new LanguageBasedString { Val = "en-US", LanguageId = "x-none" } + ) + ); + + themePart.Theme = theme; + themePart.Theme.Save(); +} + +// --- HEADERS AND FOOTERS --- +private static void AddHeadersAndFooters(MainDocumentPart mainPart) +{ + // Header + var headerPart = mainPart.AddNewPart(); + headerPart.Header = new Header( + new Paragraph( + new ParagraphProperties( + new Justification { Val = JustificationValues.Right }), + new Run( + new RunProperties( + new RunFonts { Ascii = "Calibri Light", HighAnsi = "Calibri Light" }, + new Italic(), + new FontSize { Val = "20" } // 10pt + ), + new Text("Document Header")) + )); + var headerId = mainPart.GetIdOfPart(headerPart); + + // Footer + var footerPart = mainPart.AddNewPart(); + footerPart.Footer = new Footer( + new Paragraph( + new ParagraphProperties( + new Justification { Val = JustificationValues.Center }), + new Run(new Text("Page ") { Space = SpaceProcessingModeValues.Preserve }), + new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }), + new Run(new FieldCode(" PAGE ") { Space = SpaceProcessingModeValues.Preserve }), + new Run(new FieldChar { FieldCharType = FieldCharValues.End }), + new Run(new Text(" of ") { Space = SpaceProcessingModeValues.Preserve }), + new Run(new FieldChar { FieldCharType = FieldCharValues.Begin }), + new Run(new FieldCode(" NUMPAGES ") { Space = SpaceProcessingModeValues.Preserve }), + new Run(new FieldChar { FieldCharType = FieldCharValues.End }) + )); + var footerId = mainPart.GetIdOfPart(footerPart); + + // Reference IDs in section properties + // (added in CreateSectionProperties below) +} + +// --- SECTION PROPERTIES (COMPLETE) --- +private static SectionProperties CreateSectionProperties() +{ + var sectPr = new SectionProperties(); + + // Header/Footer references (must come before page size/margins) + var mainPart = doc.MainDocumentPart; // Note: in real code, pass as parameter + sectPr.Append(new HeaderReference + { + Type = HeaderFooterValues.Default, + Id = mainPart!.GetIdOfPart(mainPart.HeaderParts.First()) + }); + sectPr.Append(new FooterReference + { + Type = HeaderFooterValues.Default, + Id = mainPart.GetIdOfPart(mainPart.FooterParts.First()) + }); + + // Page size + sectPr.Append(new PageSize { Width = 12240u, Height = 15840u }); + + // Page margins + sectPr.Append(new PageMargin + { + Top = 1440, + Bottom = 1440, + Left = 1440u, + Right = 1440u, + Header = 720u, + Footer = 720u + }); + + // Page numbering format + sectPr.Append(new PageNumberType { Start = 1, Format = NumberFormatValues.Decimal }); + + // Column settings (default: 1 column) + sectPr.Append(new Columns { ColumnCount = 1, EqualWidth = true }); + + // Paper source (printer tray) + // sectPr.Append(new PaperSource { Tray = 1, Paper = 7 }); + + return sectPr; +} +``` + +### 1.5 Unit Systems Reference + +```csharp +// ============================================================================= +// UNIT SYSTEMS IN OPENXML +// ============================================================================= +// Understanding units is critical. Wrong unit = wrong formatting. +// +// DXA (Twentieths of a DXA) - "Standard Document Unit" +// 1 DXA = 1/20th of a point +// 1 inch = 1440 DXA +// 1 cm = 567 DXA (approx) +// Used for: margins, indents, spacing, tab stops, column widths +// +// Half-Points (sz) - Font Size +// Value is in half-points (1/2 point increments) +// 24 = 12pt, 28 = 14pt, 36 = 18pt, 48 = 24pt +// Used for: FontSize.Val, FontSizeComplexScript.Val +// +// Points (pt) - Direct Measurements +// Standard typographic point (72 per inch) +// Used for: some line spacing values, border widths +// +// EMU (English Metric Units) - Drawing Objects +// 1 inch = 914400 EMU +// Used for: drawing object sizes, shapes, images +// +// STARS (Special Twips Advanced Right-Left) - CJK Indentation +// Used for: FirstLineChars, HangingChars (special FirstLine/Hanging for CJK) +// Converts character counts to DXA based on font metrics +// +// LINE SPACING SPECIAL VALUES: +// Line = "240" with LineRule = Auto = single spacing (default) +// Line = "480" with LineRule = Auto = double spacing +// Line = "360" with LineRule = Auto = 1.5 spacing +// Line = "240" with LineRule = Exact = exactly 12pt +// Line = "288" with LineRule = AtLeast = at least 14.4pt (grows with content) + +// --- CONVERSION HELPER METHODS --- +public static class OpenXmlUnits +{ + // DXA conversions + public static int InchesToDxa(double inches) => (int)(inches * 1440); + public static int CmToDxa(double cm) => (int)(cm * 567.0); + public static int PtToDxa(double pt) => (int)(pt * 20); + public static double DxaToInches(int dxa) => dxa / 1440.0; + public static double DxaToCm(int dxa) => dxa / 567.0; + public static double DxaToPt(int dxa) => dxa / 20.0; + + // EMU conversions (for drawings) + public static long InchesToEmu(double inches) => (long)(inches * 914400); + public static long CmToEmu(double cm) => (long)(cm * 360000); + public static double EmuToInches(long emu) => emu / 914400.0; + + // Half-point conversions (font sizes) + public static int PtToHalfPt(double pt) => (int)(pt * 2); + public static int FontSizeToSz(double ptSize) => (int)(ptSize * 2); + public static double SzToPt(int sz) => sz / 2.0; + + // Line spacing + public static int SingleSpacing => 240; + public static int DoubleSpacing => 480; + public static int OneAndHalfSpacing => 360; + public static int LineSpacingPt(double pt) => (int)(pt * 20); // Convert to DXA +} + +// Example usage: +var marginInInches = OpenXmlUnits.DxaToInches(1440); // 1.0 +var fontSizeInSz = OpenXmlUnits.FontSizeToSz(12.0); // 24 +var indentInDxa = OpenXmlUnits.InchesToDxa(0.5); // 720 +``` + +--- + +## 2. Style System Deep Dive + +### 2.1 Style Types and Structure + +```csharp +// ============================================================================= +// STYLE TYPES OVERVIEW +// ============================================================================= +// OpenXML defines 4 style types (StyleValues enum): +// 1. Paragraph (w:p) - controls paragraph-level formatting +// 2. Character (w:r) - controls inline/run-level formatting +// 3. Table (w:tbl) - controls table-level formatting +// 4. Numbering (w:num) - NOT a style type, but a separate numbering system +// +// Key insight: A style can be BOTH paragraph and character style (linked style). +// The "linkedStyle" element links a paragraph style to a character style. + +// --- MINIMAL PARAGRAPH STYLE --- +// A paragraph style controls: pPr (paragraph properties) and optionally rPr +Style minimalParaStyle = new Style( + new StyleName { Val = "MyParagraphStyle" }, + new PrimaryStyle() // Primary styles appear in Style gallery +) +{ + Type = StyleValues.Paragraph, + StyleId = "MyParagraphStyle" +}; + +// --- MINIMAL CHARACTER STYLE --- +// A character style controls: rPr only (no pPr) +Style minimalCharStyle = new Style( + new StyleName { Val = "MyCharacterStyle" }, + new PrimaryStyle() +) +{ + Type = StyleValues.Character, + StyleId = "MyCharacterStyle" +}; + +// Character style with run properties (fonts, size, bold, etc.) +Style charStyleWithFormatting = new Style( + new StyleName { Val = "Emphasis" }, + new PrimaryStyle(), + new StyleRunProperties( + new Italic(), + new Color { Val = "C00000" } // Dark red + ) +) +{ + Type = StyleValues.Character, + StyleId = "Emphasis" +}; + +// --- LINKED STYLE (Paragraph + Character) --- +// A linked style combines both: it can be applied to a paragraph OR a run. +// This is how Word's "Heading 1" works — applies to paragraphs, but you can +// also select text within a heading and apply the same style as character formatting. +Style linkedStyle = new Style( + new StyleName { Val = "LinkedStyle" }, + new PrimaryStyle(), + new LinkedStyle { Val = "LinkedStyleChar" }, // Links to character style + new StyleParagraphProperties( + new SpacingBetweenLines { After = "120" } + ), + new StyleRunProperties( + new Bold(), + new FontSize { Val = "24" } + ) +) +{ + Type = StyleValues.Paragraph, + StyleId = "LinkedStyle" +}; + +// Corresponding character style (normally same name + "Char" suffix by convention) +Style linkedStyleChar = new Style( + new StyleName { Val = "LinkedStyle Char" }, // Word convention: adds " Char" + new PrimaryStyle(), + new StyleRunProperties( + new Bold(), + new FontSize { Val = "24" } + ) +) +{ + Type = StyleValues.Character, + StyleId = "LinkedStyleChar" +}; + +// --- TABLE STYLE --- +Style tableStyle = new Style( + new StyleName { Val = "MyTableStyle" }, + new PrimaryStyle(), + new StyleTableProperties( + new TableWidth { Width = "5000", Type = TableWidthUnitValues.Pct }, // 50% width + new TableBorders( + new TopBorder { Val = BorderValues.Single, Size = 4, Color = "000000" }, + new BottomBorder { Val = BorderValues.Single, Size = 4, Color = "000000" }, + new LeftBorder { Val = BorderValues.Single, Size = 4, Color = "000000" }, + new RightBorder { Val = BorderValues.Single, Size = 4, Color = "000000" }, + new InsideHorizontalBorder { Val = BorderValues.Single, Size = 2, Color = "CCCCCC" }, + new InsideVerticalBorder { Val = BorderValues.Single, Size = 2, Color = "CCCCCC" } + ), + new TableCellMarginDefault( + new TopMargin { Width = "0", Type = TableWidthUnitValues.DXA }, + new StartMargin { Width = "108", Type = TableWidthUnitValues.DXA }, + new BottomMargin { Width = "0", Type = TableWidthUnitValues.DXA }, + new EndMargin { Width = "108", Type = TableWidthUnitValues.DXA } + ) + ) +) +{ + Type = StyleValues.Table, + StyleId = "MyTableStyle" +}; +``` + +### 2.2 DocDefaults and Document-Wide Defaults + +```csharp +// ============================================================================= +// DOCDEFAULTS: DOCUMENT-WIDE DEFAULTS +// ============================================================================= +// DocDefaults lives inside Styles and provides fallback values when: +// 1. No explicit style is applied +// 2. No direct formatting is applied +// It contains RunPropertiesDefault and/or ParagraphPropertiesDefault. +// +// CRITICAL: DocDefaults applies to the entire document. Any explicit style +// or direct formatting will override it. + +// --- COMPLETE DOCDEFAULTS SETUP --- +var docDefaults = new DocDefaults( + // Run properties defaults: default font, size, language for all runs + new RunPropertiesDefault( + new RunPropertiesBaseStyle( + // RunFonts: which font to use for each script + // Word will fall back through these: ASCII -> HighAnsi -> EastAsia -> ComplexScript + // Always specify at minimum Ascii and HighAnsi + new RunFonts + { + Ascii = "Calibri", // Western/Latin font (primary) + HighAnsi = "Calibri", // Latin characters (often same as Ascii) + EastAsia = "SimSun", // East Asian font (CJK) + ComplexScript = "Arial", // Complex scripts (Arabic, Hebrew, Thai) + ASCIITheme = ThemeFontValues.Minor, + HighAnsiTheme = ThemeFontValues.Minor, + EastAsiaTheme = ThemeFontValues.Minor, + ComplexScriptTheme = ThemeFontValues.Minor + }, + // FontSize: in HALF-POINTS (24 = 12pt, 22 = 11pt, 20 = 10pt) + new FontSize { Val = "22" }, // 11pt for body + new FontSizeComplexScript { Val = "22" }, + // Languages: required for proper hyphenation and spell checking + new Languages { Val = "en-US" }, // Default language + new Languages { EastAsia = "zh-CN", Val = "en-US" } // Can set multiple + ) + ), + // Paragraph properties defaults: default spacing, etc. + new ParagraphPropertiesDefault( + new ParagraphPropertiesBaseStyle( + // SpacingBetweenLines: default paragraph spacing + // After = "200" = 200 DXA = 10pt after each paragraph + new SpacingBetweenLines + { + After = "200", + Line = "276", + LineRule = LineSpacingRuleValues.Auto // Auto = 1.15x line height + } + ) + ) +); + +// --- LAYOUT LUNCTIONS (LATENT STYLES) --- +// Latent styles are hidden styles that exist in Word but aren't in styles.xml. +// They provide fast-access defaults for formatting (e.g., Normal, Heading 1-6, etc.) +// when the user hasn't explicitly customized them. +// +// DocDefaults can define LatentStyleCountOverride to adjust count, +// but true latent styles are controlled by Normal.dotm (Word's global template). +Styles CreateStylesWithDocDefaults() +{ + var styles = new Styles(); + + // DocDefaults with run and paragraph properties defaults + styles.Append(new DocDefaults( + new RunPropertiesDefault( + new RunPropertiesBaseStyle( + new RunFonts { Ascii = "Calibri", HighAnsi = "Calibri" }, + new FontSize { Val = "22" }, + new Languages { Val = "en-US" } + ) + ), + new ParagraphPropertiesDefault( + new ParagraphPropertiesBaseStyle( + new SpacingBetweenLines { After = "160", Line = "276", LineRule = LineSpacingRuleValues.Auto } + ) + ) + )); + + // LatentStyles: override defaults for built-in latent styles + // These control Word's "fast-styles" like Heading 1-6 before they're customized + styles.Append(new LatentStyles( + new Count { Val = 159 }, // Total latent style count + new FirstLineChars { Val = 352 }, // Default first line char count + new HorizontalOverflow { Val = HorizontalOverflowValues.Overflow }, + new VerticalOverflow { Val = VerticalOverflowValues.Overflow }, + new KoreanSpaceAdjust { Val = true }, + // Each LatentStyleException overrides ONE attribute of ONE latent style + // StyleID = the built-in style name (e.g., "Normal", "heading 1") + // Attribute: what to change (bold, italic, font, color, etc.) + // The defaults for built-in headings: font=Calibri, size=24, bold + new LatentStyleException( + new Primary烙, + new StyleName { Val = "Normal" }, + new UIPriority { Val = 1 }, + new PrimaryZone(), + new QuickStyle() + ), + new LatentStyleException( + new Primary烙, + new StyleName { Val = "heading 1" }, + new UIPriority { Val = 9 }, + new PrimaryZone(), + new QuickStyle(), + new Bold(), + new BoldComplexScript(), + new FontSize { Val = "48" }, // 24pt = 48 half-pts + new FontSizeComplexScript { Val = "48" } + ) + )); + + return styles; +} +``` + +### 2.3 Complete Heading Styles Hierarchy + +```csharp +// ============================================================================= +// HEADING STYLES WITH PROPER INHERITANCE CHAIN +// ============================================================================= +// Word's built-in heading system uses style inheritance: +// Normal (base) -> Heading1 -> Heading2 -> Heading3 -> Heading4 -> Heading5 -> Heading6 +// +// Why this matters: +// - Each heading INHERITS from its parent (basedOn) +// - Define common properties in Normal, override in each heading +// - Change body font once in Normal, all headings inherit it +// - Heading-specific properties override as needed + +// --- HEADING STYLE FACTORY --- +public static Style CreateHeadingStyle(int level, FontConfig fonts) +{ + // Validate level (1-9 are valid, 1-6 are standard) + if (level < 1 || level > 9) + throw new ArgumentOutOfRangeException(nameof(level)); + + double[] headingSizes = [26.0, 20.0, 16.0, 14.0, 12.0, 11.0, 11.0, 11.0, 11.0]; + string[] outlineLevels = ["0", "1", "2", "3", "4", "5", "6", "7", "8"}; + + var style = new Style( + new StyleName { Val = $"heading {level}" }, // Display name + new BasedOn { Val = level == 1 ? "Normal" : $"Heading{level - 1}" }, // Parent style + new NextParagraphStyle { Val = "Normal" }, // After heading -> Normal + new PrimaryStyle(), // Show in Styles gallery + new UIPriority { Val = 9 - level }, // Priority in gallery (H1 = 8, H2 = 7, etc.) + new QuickStyle(), // Appears in Quick Styles gallery + // Paragraph properties: spacing, keep options, outline level + new StyleParagraphProperties( + new KeepNext(), // Keep heading with next paragraph + new KeepLines(), // Keep all lines of heading together + new SpacingBetweenLines // Spacing before/after + { + Before = level == 1 ? "480" : "240", // H1 = 240pt before, others = 120pt + After = "120" + }, + new OutlineLevel { Val = level - 1 } // 0-indexed for H1=0, H2=1, etc. + ), + // Run properties: font, size, bold + new StyleRunProperties( + new RunFonts + { + Ascii = fonts.HeadingFont, + HighAnsi = fonts.HeadingFont, + EastAsia = "SimHei" // Bold heading font for CJK + }, + new FontSize { Val = UnitConverter.FontSizeToSz(headingSizes[level - 1]) }, + new FontSizeComplexScript { Val = UnitConverter.FontSizeToSz(headingSizes[level - 1]) }, + new Bold(), + new BoldComplexScript() + ) + ) + { + Type = StyleValues.Paragraph, + StyleId = $"Heading{level}" + }; + + return style; +} + +// --- ADD ALL HEADING STYLES TO STYLES COLLECTION --- +public static void AddHeadingStyles(Styles styles, FontConfig fonts) +{ + for (int i = 1; i <= 6; i++) + { + styles.Append(CreateHeadingStyle(i, fonts)); + } + + // Also add Heading 7-9 (valid in Word, less commonly used) + for (int i = 7; i <= 9; i++) + { + styles.Append(CreateHeadingStyle(i, fonts)); + } +} + +// --- HEADING STYLES INHERITANCE VISUALIZATION --- +// When you apply "Heading2" (basedOn="Heading1"): +// +// Normal style: +// - Font: Calibri 11pt +// - Spacing: 0 before, 200 after +// - No bold +// +// Heading1 (basedOn="Normal"): +// - Inherits: Calibri 11pt +// - Overrides: Calibri Light 26pt, Bold, Spacing 480 before/120 after +// - Adds: KeepNext, KeepLines, OutlineLevel=0 +// +// Heading2 (basedOn="Heading1"): +// - Inherits: Calibri Light 26pt, Bold, KeepNext, KeepLines +// - Overrides: 20pt +// - Inherits: OutlineLevel=1 +// +// Effective result: Heading2 = Calibri Light 20pt Bold, KeepNext+KeepLines, 480/120 spacing, OL=1 +``` + +### 2.4 Style Inheritance Chain Resolution + +```csharp +// ============================================================================= +// STYLE INHERITANCE RESOLUTION +// ============================================================================= +// OpenXML styles resolve properties through the basedOn chain at RENDER TIME. +// The document.xml stores only the styleId, not the resolved properties. +// Word (or this library) walks the chain at load/display time. +// +// Example: Applying "Heading2" to a paragraph +// +// 1. Start with Heading2 style definition +// 2. Walk basedOn chain: Heading2 -> Heading1 -> Normal -> (null) +// 3. Collect properties in reverse order (most generic first): +// a. Normal: Ascii=Calibri, sz=22, no bold +// b. Heading1: Ascii=Calibri Light, sz=48, bold (override Calibri, sz, bold) +// c. Heading2: sz=40 (override sz only) +// 4. Final resolved style: Ascii=Calibri Light, sz=40, bold (bold from H1) +// +// IMPORTANT: Style override is COMPLETE for each element type: +// - If Normal has rPr with Fonts, and Heading1 has pPr only, +// Heading1 still inherits Normal's rPr fully. +// - StyleRunProperties (rPr) and StyleParagraphProperties (pPr) are separate. + +// --- RESOLVING STYLE PROPERTIES MANUALLY --- +// For debugging or custom rendering, you may need to resolve style chains +public static class StyleResolver +{ + public record ResolvedStyle( + StyleName? Name, + RunProperties? RunProps, + ParagraphProperties? ParaProps, + string? BasedOn, + string Type); + + public static ResolvedStyle Resolve(Styles styles, string styleId) + { + var styleMap = styles.Elements