From a710477be2302b5b42d775bb2e0729c85dc752ff Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Mon, 9 Nov 2026 02:53:10 +0800 Subject: [PATCH] Refactor code structure for improved readability and maintainability --- .gitignore | 7 +- .qoder/settings.json | 20 + .qoder/skills/asset-ingest/SKILL.md | 81 +++ .qoder/skills/batch-closure/SKILL.md | 75 ++ .qoder/skills/doc-consistency-check/SKILL.md | 56 ++ .qoder/skills/doc-design/SKILL.md | 84 +++ .qoder/skills/iter-plan/SKILL.md | 86 +++ .qoder/skills/iter/SKILL.md | 120 ++++ .qoder/skills/iter/evals/evals.json | 125 ++++ .../iter/evals/files/mock-card-iter40-1.md | 31 + .../skills/iter/evals/files/mock-gate-run.md | 25 + .../iter/evals/files/mock-roadmap-iter40.md | 11 + .../iter/evals/files/mock-subagent-report.md | 12 + .qoder/skills/req-card/SKILL.md | 120 ++++ .../.github/workflows/test.yml | 21 + .../simplified-technical-chinese/.gitignore | 7 + .../simplified-technical-chinese/LICENSE | 21 + .../LICENSE-CC-BY-4.0 | 396 +++++++++++ .../simplified-technical-chinese/README.en.md | 114 +++ .../simplified-technical-chinese/README.md | 180 +++++ .../simplified-technical-chinese/SKILL.md | 129 ++++ .../docs/校准报告.md | 178 +++++ .../examples/after.md | 43 ++ .../examples/before.md | 19 + .../tests/test_check.py | 293 ++++++++ .../tools/calibration/README.md | 63 ++ .../tools/calibration/label.py | 193 +++++ .../tools/calibration/manifest.py | 47 ++ .../tools/calibration/report.py | 171 +++++ .../tools/calibration/sample.py | 94 +++ .../tools/calibration/stats.py | 219 ++++++ .../tools/check.py | 660 ++++++++++++++++++ .../simplified-technical-chinese/规范.md | 409 +++++++++++ .../simplified-technical-chinese/词表.md | 211 ++++++ .zcode/hooks/rule-delivery.sh | 47 ++ .zcode/hooks/worktree-guard.sh | 195 ++++++ .zcode/skills/asset-ingest/SKILL.md | 81 +++ .zcode/skills/batch-closure/SKILL.md | 75 ++ .zcode/skills/doc-design/SKILL.md | 84 +++ .zcode/skills/iter-plan/SKILL.md | 115 +++ .zcode/skills/iter/SKILL.md | 120 ++++ .zcode/skills/iter/evals/evals.json | 125 ++++ .../iter/evals/files/mock-card-iter40-1.md | 31 + .../skills/iter/evals/files/mock-gate-run.md | 25 + .../iter/evals/files/mock-roadmap-iter40.md | 11 + .../iter/evals/files/mock-subagent-report.md | 12 + .zcode/skills/req-card/SKILL.md | 120 ++++ 47 files changed, 5357 insertions(+), 5 deletions(-) create mode 100644 .qoder/settings.json create mode 100644 .qoder/skills/asset-ingest/SKILL.md create mode 100644 .qoder/skills/batch-closure/SKILL.md create mode 100644 .qoder/skills/doc-consistency-check/SKILL.md create mode 100644 .qoder/skills/doc-design/SKILL.md create mode 100644 .qoder/skills/iter-plan/SKILL.md create mode 100644 .qoder/skills/iter/SKILL.md create mode 100644 .qoder/skills/iter/evals/evals.json create mode 100644 .qoder/skills/iter/evals/files/mock-card-iter40-1.md create mode 100644 .qoder/skills/iter/evals/files/mock-gate-run.md create mode 100644 .qoder/skills/iter/evals/files/mock-roadmap-iter40.md create mode 100644 .qoder/skills/iter/evals/files/mock-subagent-report.md create mode 100644 .qoder/skills/req-card/SKILL.md create mode 100644 .qoder/skills/simplified-technical-chinese/.github/workflows/test.yml create mode 100644 .qoder/skills/simplified-technical-chinese/.gitignore create mode 100644 .qoder/skills/simplified-technical-chinese/LICENSE create mode 100644 .qoder/skills/simplified-technical-chinese/LICENSE-CC-BY-4.0 create mode 100644 .qoder/skills/simplified-technical-chinese/README.en.md create mode 100644 .qoder/skills/simplified-technical-chinese/README.md create mode 100644 .qoder/skills/simplified-technical-chinese/SKILL.md create mode 100644 .qoder/skills/simplified-technical-chinese/docs/校准报告.md create mode 100644 .qoder/skills/simplified-technical-chinese/examples/after.md create mode 100644 .qoder/skills/simplified-technical-chinese/examples/before.md create mode 100644 .qoder/skills/simplified-technical-chinese/tests/test_check.py create mode 100644 .qoder/skills/simplified-technical-chinese/tools/calibration/README.md create mode 100644 .qoder/skills/simplified-technical-chinese/tools/calibration/label.py create mode 100644 .qoder/skills/simplified-technical-chinese/tools/calibration/manifest.py create mode 100644 .qoder/skills/simplified-technical-chinese/tools/calibration/report.py create mode 100644 .qoder/skills/simplified-technical-chinese/tools/calibration/sample.py create mode 100644 .qoder/skills/simplified-technical-chinese/tools/calibration/stats.py create mode 100644 .qoder/skills/simplified-technical-chinese/tools/check.py create mode 100644 .qoder/skills/simplified-technical-chinese/规范.md create mode 100644 .qoder/skills/simplified-technical-chinese/词表.md create mode 100644 .zcode/hooks/rule-delivery.sh create mode 100644 .zcode/hooks/worktree-guard.sh create mode 100644 .zcode/skills/asset-ingest/SKILL.md create mode 100644 .zcode/skills/batch-closure/SKILL.md create mode 100644 .zcode/skills/doc-design/SKILL.md create mode 100644 .zcode/skills/iter-plan/SKILL.md create mode 100644 .zcode/skills/iter/SKILL.md create mode 100644 .zcode/skills/iter/evals/evals.json create mode 100644 .zcode/skills/iter/evals/files/mock-card-iter40-1.md create mode 100644 .zcode/skills/iter/evals/files/mock-gate-run.md create mode 100644 .zcode/skills/iter/evals/files/mock-roadmap-iter40.md create mode 100644 .zcode/skills/iter/evals/files/mock-subagent-report.md create mode 100644 .zcode/skills/req-card/SKILL.md diff --git a/.gitignore b/.gitignore index fa6e5311..3dae871a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ # === Godot 4 === # 编辑器生成的缓存(imported 资源、shader cache、编辑器状态),可由 project.godot 重建 .godot/ -.zcode/ .zcodeignore # Godot 4 的 .uid 文件必须提交,不要忽略 # 导入元数据 @@ -42,10 +41,8 @@ Thumbs.db *.log *.tmp .zcode/plans/ - -# === 非本项目批次产物(2026-10-02 用户裁:入 .gitignore,本地保留不入库)=== -.qoder/ -.workbuddy/ +.zcode/tmp/ +.workbuddy/memory/ # === 归档压缩包(2026-09-26 批目录归档产物,本地留存不入库)=== archive/ diff --git a/.qoder/settings.json b/.qoder/settings.json new file mode 100644 index 00000000..570516b3 --- /dev/null +++ b/.qoder/settings.json @@ -0,0 +1,20 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Edit|Write|MultiEdit|NotebookEdit|Bash", + "hooks": [ + { + "type": "command", + "command": "bash", + "args": [ + "${QODER_PROJECT_DIR}/.zcode/hooks/worktree-guard.sh" + ], + "timeout": 15, + "statusMessage": "worktree 铁律守卫(直改/提交拦截;脚本单一事实源在 .zcode)" + } + ] + } + ] + } +} diff --git a/.qoder/skills/asset-ingest/SKILL.md b/.qoder/skills/asset-ingest/SKILL.md new file mode 100644 index 00000000..b5c77945 --- /dev/null +++ b/.qoder/skills/asset-ingest/SKILL.md @@ -0,0 +1,81 @@ +--- +name: asset-ingest +description: g-plan2(Dungeon Sweeper)五段流水线第二段的美术素材录入入口。当用户说素材已过审、把素材录入/入库、终稿放进游戏、补 art-bible 条款时使用。**只录入、不生成任何图像内容**——样张与素材由用户主导在 artwork/ 工作区产出,本技能承接的是用户已过审的素材:核对命名与目录(改名依据=代码消费名)→拷入 game/assets/art/→art-bible 补条款(五要素缺一不可)→美术素材状态清单与待办清单状态同步→命名与目录自检。仓库在 D:/code/g-plan2。 +--- + +# asset-ingest——美术素材录入(g-plan2 五段流水线) + +你在五段流水线(登记 → 文档设计/美术设计 → 写卡 → 迭代编排 → 实现,流程纪律见 `design/engineering/process-conventions.md` §2)的第二段做**美术线的录入半程**。**定性写死:本技能只录入、不生成任何图像内容**——样张与素材由用户主导在 `artwork/` 工作区产出,本技能承接的是**用户已过审的素材**,把它按规范落进游戏与文档。格式权威=`design/engineering/doc-conventions.md` §12.3(美术工作区目录与命名),动笔前先读它。 + +## 批性质与共同纪律(先读) + +- **仓库路径**:项目在 `D:/code/g-plan2`,本文路径相对它。 +- **本技能属工程/文档改动批(直改类)**:可写 `design/` 目录与 `roadmap.md`;**禁跑 `tools/run_all_gates.sh` 全套(不带参数档)与任何游戏测试**;收尾=手动跑 `--docs`(文档域闸),全绿后按车道合回,合回落地 post-merge 钩子推送触发远端 CI 验证。本技能的写面=`design/art/` 两文档+`待办清单.md`+素材拷贝(`game/assets/art/` 与 `artwork/` 均不入 git);**除素材拷贝外禁碰 `game/` 与 `tools/` 的任何代码文件**——素材不入 git,tracked 改动面仍只有文档,--docs 档选档不变。 +- **车道铁律**:tracked 改动(`design/art/` 两文档+待办清单)走独立 worktree(建区粒度=回合,会话单区等细则见 `design/engineering/process-conventions.md` §1),收口=冻结→件核→并 master→主区 `git merge --no-ff`→删区;素材与 `artwork/` 不入 git,**素材拷贝直接落主工作区 `game/assets/art/`**(游戏实际加载的在盘位置);第 7 步 path_check 以 `--repo` 指主工作区跑。 +- **特批两段式**:只有用户明示才能跳过本技能流程中的某步(典型=跳过第 2 步命名核对)。对象(待办清单条目/roadmap 格)已有内容 → 直接写入标记 `〔用户特批 YYYY-MM-DD 跳过××〕`;对象没有任何内容 → 必须回呈实况、经用户再次确认后才写标记。标记必须带日期,无日期=非法(doc-conventions §12.4)。 +- **禁自援特权**:执行中你自己觉得某步该跳,只能呈报后停,**禁自己援引特批**。 +- **闸过渡期口径**:现行待办清单与 roadmap 还是旧格式,`python tools/pipeline_gate.py` 对它们整体红属预期。**红项若全部来自旧格式存量、与本次操作对象无关,注明即可;本次操作对象自身的格式必须合规**。 +- 本技能只固化本段特有条款;车道铁律、会话落记等仓库级纪律按 `design/engineering/process-conventions.md` 现行文本执行。 + +## 流程 + +### 第 1 步:定位批次与素材 + +在 `artwork//` 批次工作区内定位**用户点名的过审素材与样张**(样张在 `samples/` 子目录)。批次目录不存在、点名素材找不到、或用户没有明示哪些算过审 → 停工呈报,禁自行挑选或代判「这张应该过了」。 + +### 第 2 步:核对命名与目录(改名依据=代码消费名) + +1. **命名形态**(doc-conventions §12.3):全小写英文+下划线,禁中文/空格/大写。终稿三段式 `<对象>_<部位或状态>_<规格>.png`;样张 `sample_<屏名>_<版本>.png`。 +2. **目录走向**:`artwork/` 批次 → 终稿 `game/assets/art/<用途子目录>/`(现行子目录:`board/`、`character/`、`fx/`、`ui/`,见 `design/art/asset-path-map.md`)。**图片永不进 `design/`**。 +3. **改名依据=代码消费名**:终稿文件名跟代码走,防断链——先在 `game/` 内查 ui_kit/资源路径常量确定消费点名(ui_kit 在 `game/presentation/views/ui_kit.gd`,`TEX_*` 常量即 `res://assets/art/...` 路径;未收进 ui_kit 的查 `design/art/asset-path-map.md` 与代码内 `res://assets/art` 引用)。素材名跟它走。**查不到消费点 → 停工呈报,禁自造名字**;呈报时带解法选项:先走代码实现批定常量,或由用户直接点名文件名(用户点名视同消费名依据,agent 仍不得自拟)。 +4. 同名迭代直接覆盖,不留 v2 副本(§12.3)。 +5. 本步经特批可跳(标记规则见批性质;有条目时标记写在待办清单条目状态处,无条目时随会话呈报与 commit 信息落案)。 + +### 第 3 步:终稿拷入 + +把过审终稿拷入 `game/assets/art/` 对应用途子目录(该目录不入 git,在盘即生效;打包发布方案另有既定口径,与本技能无关)。 + +### 第 4 步:art-bible 补条款 + +对 `design/art/art-bible.md` 补录条款,**每条条款五要素缺一不可**: + +1. **条款名**; +2. **规则句**(一句白话说清规则); +3. **像素级判据**(写成可核对的判读句); +4. **参考图指针**——只许指入库稳定位置(`design/art/references/`、`asset-path-map.md`),**禁指 `artwork/` 在途散路径**(该目录不入 git 会消失,art-bible §8.3 归档纪律); +5. **文首头注版本号递增**(「现行为 vX.Y」同步更新)——禁写版本历史表,改动明细只进 git 提交信息。 + +新条款按 art-bible 头注「条款验收标注纪律」标注验收方式(←机检/←目检/←方向性)。 + +### 第 5 步:美术素材状态清单同步 + +更新 `design/art/art-status-checklist.md`(该文件名已核实,是纯看板:素材名×状态×需求文档指针;注意它与生产规格清单 `art-assets-checklist.md` 是两份文件,本步只动前者):对应素材行状态列改 ✅+一行事实(何时、何批、落点)——做完不删行,改状态。 + +### 第 6 步:待办清单状态推进 + +按 doc-conventions §12.1 状态机改写条目状态属性(仅在册条目适用;本次会话新录入、清单内无对应事项时本步跳过,禁为录入动作新建条目): + +- **只走美术线**:改「美术设计已闭合(凭据=样张过审、art-bible 条款版本)」,凭据落成实例形态 `美术设计已闭合(样张过审 2026-10-18、art-bible v3.16 §8.2)`——**凭据括号内禁用+号**(+只作状态段连接符;凭据内并列项按 doc-conventions §12.1 用顿号,用+会被流水线闸误判成状态段)。本线全闭合,删除「缺口」属性。 +- **两条设计线并列**:改两段并列,美术侧闭合、文档侧保持原状态(例:`文档设计已闭合(…v0.6)+美术设计已闭合(…)`)。**「缺口」属性只在两侧全闭合(条目转入 待写卡)时才删**——状态里只要还有待文档设计/文档设计中/待美术设计/美术设计中任一词,缺口属性必须在(流水线闸判据);单侧闭合时保留并改写为剩余线还差什么。 +- 若第 2 步经特批跳过,标记写在本条目状态处(无条目时随会话呈报与 commit 信息落案)。 +- 旧格式条目先向用户确认所属设计线与改动面,按 §12.1 补齐格式再写状态(同 doc-design 第 1 步口径)。 +- 改完顺带跑 `python tools/pipeline_gate.py` 自核本次条目零红(过渡期存量红注明即可)。 + +### 第 7 步:命名与目录自检 + +两半都做: + +1. **命名形态**:先扫一眼 `tools/` 有无现行命名检查器可引用(截至 2026-10-18 没有——`path_check.py` 管目录与消费链、`standee_asset_check.py` 是内容机检不属此类),没有就按 doc-conventions §12.3 规则逐文件核对(三段式/全小写/下划线/用途子目录/无 v2 副本)。 +2. **目录与消费链**:跑 `python tools/path_check.py --repo <主工作区>`(路径核对器:声明路径存在性+代码 `res://assets/art` 引用与盘上文件互核)——拷完必跑。注意它查的是「代码引用的文件在不在」,**不查「盘上多余文件的名字对不对」**:命名核对若被特批跳过,本步结论必须如实注明「命名形态未检」,禁让 path_check 绿顶替命名核对。 + +**美术内容不做机检**:`tools/standee_asset_check.py` 等内容级检查器(档高/色数/描边)不属本技能,禁在本流程内跑。 + +### 收尾 + +`python tools/run_all_gates.sh --docs`(文档域闸),全绿合并回 master。 + +## 纪律 + +- **禁生成、禁改图、禁代画**:素材内容层面的一切意见(好不好看、要不要改)只能呈报给用户,本技能零处置权。 +- 平实简体中文,禁自造黑话;流水线词汇按 doc-conventions §12 用法一致使用。 +- 本技能不写需求卡片、不编迭代、不碰代码——代码消费常量的**新增/改名**属代码实现批的活,只能呈报留给下游,禁顺手改 `ui_kit.gd`。 diff --git a/.qoder/skills/batch-closure/SKILL.md b/.qoder/skills/batch-closure/SKILL.md new file mode 100644 index 00000000..525f8cf4 --- /dev/null +++ b/.qoder/skills/batch-closure/SKILL.md @@ -0,0 +1,75 @@ +--- +name: batch-closure +description: g-plan2(Dungeon Sweeper)批后收尾与中间文档退役入口。当用户说「迭代 N 收尾/批后收尾/落完反哺点收个尾」「清理中间文档/清理暂存地」「把某份报告退役」时使用。做两类事:①iter 批后收尾——反哺点落文凭据核对(落文本身委托 doc-design,本技能禁代写)、reports/ 产出物(实现报告/反哺点清单/验收审计报告)退役判据核对与删除、待办清单条目整条删除、roadmap 迭代行与顶部「当前位」三行换代;②中间文档退役——中间文档唯一暂存地 `reports/`(需求卡片/迭代产出物/提案/审计/需求澄清过程稿)按「吸收即死」判据核对后删除(需求卡片死期=所属迭代实现批合回 master)。规则权威=doc-conventions §11+concept-conventions §2,本技能只做执行编排、不复写规则;与 doc-consistency-check 分工=那边定期体检只读只报、这边被点名后执行清理。工程/文档改动批(直改类):可写 design/ 与 roadmap.md,收尾跑 --docs 文档域闸。仓库在 D:/code/g-plan2。 +--- + +# batch-closure——批后收尾与中间文档退役(g-plan2) + +你做的是批次结束后的清库工作:把已完成批次留下的中间文档,按「吸收即死」(规则权威=`design/engineering/doc-conventions.md` §11,删除手法=`design/engineering/concept-conventions.md` 第 2 节删除概念 SOP)退役掉,并随手把账面(roadmap、待办清单)换到与现实一致。**本技能只做执行编排,禁在本文件复写或改判规则**——判据与本文件冲突时以 §11/§12 现行文本为准,并停工呈报。 + +## 职责边界(先读) + +- **反哺落文不归本技能**:把反哺点写进 GDD/art-bible 走 doc-design 技能(其「反哺点写入模式」触发)。本技能只核对落文结果(待办清单反哺点专条状态=已写入+落点+版本号),核对不过就把落文委托给 doc-design(唯一出口,落文完成后接着收尾),**禁代写落文**。 +- **只报不删的体检不归本技能**:doc-consistency-check 定期体检只读、只呈报问题;本技能是执行臂——用户点名(含体检报出后点名)才动手删。 +- **触发词分界**:「落反哺点/处理反哺点清单/反哺点裁决写入」=doc-design;「收尾/清理中间文档/清理暂存地/退役××文档」=本技能。 + +## 批性质与共同纪律 + +- **仓库路径**:项目在 `D:/code/g-plan2`,本文路径相对它。 +- **本技能属工程/文档改动批(直改类)**:可写 `design/` 目录与 `roadmap.md`(账面换代正是本批产出);禁跑 `tools/run_all_gates.sh` 全量档(不带参数)与任何游戏测试、MC 跑批;收尾=手动跑 `--docs`(文档域覆盖 `game/` 内 `.md`、design/、根规则文件、reports/、tools/、.zcode/ 等一切非代码面改动),全绿后按车道合回,合回落地 post-merge 钩子推送触发远端 CI 验证。 +- **车道铁律**:reports/ 产出物与账面换代(roadmap/待办清单)都是 tracked 文件——一切改动走独立 worktree(建区粒度=回合,会话单区等细则见 `design/engineering/process-conventions.md` §1),收口=冻结→件核→并 master→主区 `git merge --no-ff`→删区;迭代批自身的 worktree 收口动作(merge+remove)照旧;`.zcode/` 与 `.qoder/` 技能文件不入 git,主区直改。 +- **删除三步强制**:先 grep 引用面 → 改写指路句(改法=结论内联或换 git 提交号)→ 再删文件;收尾死链检查必须绿(`tools/doc_link_audit.py`,随 --docs 跑)。 +- **判据不过禁删**:任一文件的退役判据不过,逐份呈报等用户裁,禁「顺手清掉」;用户明示强行退役才照办,并在提交信息记 `〔用户特批 YYYY-MM-DD 退役××〕`(标记须带日期,doc-conventions §12.4)。本技能无特权,禁自答放行。 +- 白话写作(doc-conventions §2)与流水线词汇(§12)全程生效。 + +## 主流程 A:iter 批后收尾 + +触发:用户说「迭代 N 收尾/批后收尾/落完反哺点了收个尾」。 + +### 第 1 步:盘点该迭代的在飞面 + +1. 读 `roadmap.md` 迭代 N 行(状态列应已是或应改为「已完成」)与 `reports/` 下该迭代全部文件(实现报告/反哺点清单/验收审计报告,文件名形如 `iter-*-<日期>.md`)。 +2. 读待办清单:该迭代各条目(状态应走到「已编迭代 N」)、本迭代产出的反哺点专条。 +3. **前置核对反哺点是否已落文**:反哺点清单还在且待办清单对应专条未到「已写入(落点+版本)」→ 直接转 doc-design 反哺模式把落文办完,再接着收尾;用户明示本批暂不落文的,反哺点清单保留、只清其余件,呈报里写明这个例外。 + +### 第 2 步:逐份过退役判据 + +| 文件 | 退役判据 | 不过怎么办 | +|---|---|---| +| `iter-反哺点-*.md` | 已随落文退役(doc-design 第 5 步),或全部条目已落文(待办清单专条=已写入+落点+版本号) | 第 1 步已拦,委托 doc-design | +| `iter-实现报告-*.md` | 遗留读数义务逐条已迁(体验验证清单或待办清单在册);实机目检项已登记体验验证清单 | 未决先迁再死:先迁待办清单/体验验证清单再退役 | +| `iter-验收审计-*.md` | 审计结论已并入实现报告、无未决项 | 未决项先迁再死:先迁待办清单/实现报告再退役 | + +更旧迭代(M-实现报告`,其"闸绿记录"节记有当次各项存量数),与本次输出对比。与存量持平 = 通过;多出来的命中逐条人工看。找不到近期出口报告时,全部命中逐条人工看。 + +### 第二组:人工核对(六件) + +5. **数值三处同步**:先跑 `git log --oneline --since="7 days ago" -- game/data/tables design/gdd` 找近期的数值变更提交;对每一次 .tres 数值行变更,核对其对应的 GDD 正文表格、`data/tables/` schema 校验域、`待办清单.md` 现状描述这三处是否同步(doc-conventions 第 5 节强制收口条款:三处任一未更新,该变更就不算完成)。7 天内没有数值变更提交,就记录"本项跳过"。 +6. **根索引与文档树同步**:列出 `design/gdd/` 与 `design/gdd/systems/` 各子目录下实际的 .md 文件,对照 `design/gdd/02-系统清单.md` 的导航条目——文档增删必须同步根索引(AGENTS.md Project Layout 规则),新增或删除了文档而索引没跟上就是失同步。 +7. **待办清单现状**:对照 git log 最近合并进 master 的批次提交与 roadmap.md 里的落地标注,检查待办清单.md 里相关条目的状态是否已刷新;条目还写着进行中、但对应批次实际已合并的,报滞后。 +8. **当前位与现实一致**:`roadmap.md` 顶部「当前位」三行所述的阶段与迭代位,对照 `git log --oneline -10` 的近期合入与本文迭代总表末行;「当前位」写出三行以上、或待办清单里条目状态与近期已合并批次矛盾,都报滞后。 +9. **状态文件复发检查**:仓库根出现 `active.md` 或任何同类会话状态文件、任何文档里重新长出「当前状态快照」节,一律报(2026-10-18 用户裁废除,见 doc-conventions 第 10 节)。只报不修。 +10. **闭合凭据的版本号落后**:待办清单条目状态行里的「文档设计已闭合(design/gdd/xxx.md v1.2)」「美术设计已闭合(样张过审、art-bible v3.x)」这类凭据,括号里的版本号记的是**被引文档当时的现行版**。08c 流水线闸只校验凭据在场与形态、不校验它是否仍是现行版,所以要人工比对:先 `grep -oE "([^)]*v[0-9]+\.[0-9]+[^)]*)" 待办清单.md` 抓出全部带版本凭据(2026-10-18 实测 12 处),再对每个被引文档取现行版——看文首状态行/头注的「现行为 vX.Y」(版本历史表已于 2026-10-19 禁设,文档内不再逐版登记);凭据里的文档名常是裸名甚至省略 `.md`(如「元进度.md v1.0」「art-bible v3.15」),要按名字在 `design/gdd/`、`design/art/` 树下定位。不等即报「凭据指向旧版」,含义是这条设计闭合判据所依据的文档已被后续批次超越,闭合是否仍成立要重看。只报不修。(前身是 `tools/loop_dashboard.py` 的失效面检测器,它比对的是体验验证清单头部「当前批次来源」行;那一行在后续改版中已不存在,所以本件按现存格式重立到待办清单凭据面上,工具本身 2026-10-18 删除。) + +## 产出格式 + +报告直接写在对话输出里,不落盘成文件。 + +- **全部通过**:一句话,例如"文档一致性检查完成:脚本检查四件与人工核对四件全部通过,与最近出口报告的基线持平"。 +- **有问题**:逐条列出,每条包含四样——哪份文档哪个位置、与什么不一致、证据(文件:行号)、建议修法(一句话)。按误导程度排序:会误导读者或后续开发的失同步 > 滞后未刷新 > 格式类。 +- **拿不准的**:单独列"存疑"一节,写清为什么拿不准,不下结论、也不当问题报。 + +## 边界 + +- 不检查 `reports/` 里中间文档的内容质量——`reports/` 是中间文档唯一暂存地(`design/research/` 已于 2026-10-21 退役),文件寿命到处理完毕为止;验证取证照旧不进 `design/`。 +- 不修改 `design/gdd/术语表.md` 的登记(修订登记是拥有该概念的文档所在批次的职责),只核对它与其他文档是否一致。 diff --git a/.qoder/skills/doc-design/SKILL.md b/.qoder/skills/doc-design/SKILL.md new file mode 100644 index 00000000..8f5e1a54 --- /dev/null +++ b/.qoder/skills/doc-design/SKILL.md @@ -0,0 +1,84 @@ +--- +name: doc-design +description: g-plan2(Dungeon Sweeper)五段流水线第二段的文档设计入口。当用户要求做某事项的文档设计、发起系统设计讨论、把设计定稿写进 GDD、或要求处理迭代反哺点清单(落反哺点/反哺点裁决写入)时使用。流程:从待办清单取条目校验所属设计线与状态→用户主导的设计讨论(待裁点全案并列、禁代裁)→待裁点清零+用户点头后定稿写入 GDD→待办清单状态推进「文档设计已闭合」→跑流水线闸。兼任反哺点写入与 design/engineering/ 规约类文档修订。仓库在 D:/code/g-plan2。 +--- + +# doc-design——文档设计段入口(g-plan2 五段流水线) + +你在五段流水线(登记 → 文档设计/美术设计 → 写卡 → 迭代编排 → 实现,流程纪律见 `design/engineering/process-conventions.md` §2)的第二段做**文档线**设计:把一件登记在册的事,经用户主导的设计讨论,定稿成 GDD 文档(或 design/engineering/ 规约修订)。条目格式与状态机权威=`design/engineering/doc-conventions.md` §12,动笔前先读它。 + +## 批性质与共同纪律(先读) + +- **仓库路径**:项目在 `D:/code/g-plan2`,本文路径相对它。 +- **本技能属工程/文档改动批(直改类)**:可写 `design/` 目录与 `roadmap.md`(这正是本批产出);**禁跑 `tools/run_all_gates.sh` 全套(不带参数档)与任何游戏测试**;收尾=手动跑 `--docs`(文档域闸),全绿后按车道合回,合回落地 post-merge 钩子推送触发远端 CI 验证(文档批=文档域秒级闸),结果不等待、不落地,随时用 `tools/ci_check.sh <合并提交>` 现查(合回零等待,process-conventions §1 第 3 条)。tracked 改动默认只许落在 `design/` 与 `roadmap.md`;规约修订批(用户发令改流程规约时)写面可含 `AGENTS.md`、`design/engineering/` 规约与 `tools/` 闸件,按批列明写面,闸仍跑 `--docs`。 +- **车道铁律**:本技能写面全是 tracked 文档面——一切改动走独立 worktree(建区粒度=回合,会话单区等细则见 `design/engineering/process-conventions.md` §1),收口=冻结→件核→并 master→主区 `git merge --no-ff`→删区;`.zcode/` 与 `.qoder/` 技能文件不入 git,主区直改。触及代码面(`game/` 下非 `.md`)的改动不属本技能面。 +- **特批两段式**:只有用户明示才能跳过本技能流程中的某步(如跳过设计讨论直接落定稿)。对象(待办清单条目/roadmap 格)已有内容 → 直接写入标记 `〔用户特批 YYYY-MM-DD 跳过××〕`;对象没有任何内容 → 必须回呈实况、经用户再次确认后才写标记。标记必须带日期,无日期=非法(doc-conventions §12.4)。 +- **禁自援特权**:执行中你自己觉得某步该跳,只能呈报后停,**禁自己援引特批**。 +- **闸过渡期口径**:现行待办清单与 roadmap 还是旧格式,`python tools/pipeline_gate.py` 对它们整体红属预期。**红项若全部来自旧格式存量、与本次操作对象无关,注明即可;本次操作对象自身的格式必须合规**。pipeline_gate 注册进 run_all_gates 文档域归数据整改批,此前 --docs 的回执不含它,属正常。 +- 本技能只固化本段特有条款;车道铁律、会话落记等仓库级纪律按 `design/engineering/process-conventions.md` 现行文本执行。 + +## 主流程(文档线设计) + +### 第 1 步:取条目并校验 + +1. **判来源**:用户点名某条在册事项 → 从 `待办清单.md` 取该条目,取不到就回报「清单内无此事项」并停,禁自造条目凑流程;本次会话新提出的事项 → 无条目直接进第 2 步(条目记的是进度,不是讨论的开工前置)。 +2. 校验两件事: + - **所属设计线**:状态行含文档线段(待文档设计/文档设计中/文档设计已闭合)。 + - **状态**:为「待文档设计」或「文档设计中」。 +3. 状态不对 → **停工呈报,禁硬做**。典型:已闭合(设计已完,重开须用户明示并说明理由);需求性工程条目(状态整行无设计线段,走 req-card)或直改类条目(状态=待执行,不走写卡);已写卡/已编迭代(下游已动,改动=回退流程,归用户裁)。 +4. 条目是旧格式存量(标题缺登记元数据)→ 先向用户确认其来源与所属设计线、改动面,再按 doc-conventions §12.1 模板补齐标题元数据与四属性(实况/缺口/改动面/状态)(这属「本次操作对象格式合规」动作),然后回到第 2 步校验。确认中若答案已注定校验不过(如状态整行无设计线段=需求性工程条目或直改类条目),直接停工呈报,禁补格式硬做。 + +### 第 2 步:设计讨论(用户主导) + +1. **开题先用一句 ≤50 字大白话概览**:这个系统/事项是什么、为什么现在做。 +2. **先高抽象后具体**:方向、骨架、边界未定前,禁铺具体条目(数值表、条目清单、字段细节都是具体层)。 +3. **每个待裁点呈全部备选方案与代价**,禁代裁——设计权归用户。零待裁点也要向用户报明「本讨论零待裁点」,同样等用户点头。 +4. 讨论中冒出的新待裁点随时入册(记在会话内呈报的待裁点清册,不写进 GDD 正文——doc-conventions §5:正文只写现行规则)。 +5. 会话内进行、禁无监管自跑(process-conventions §2 段铁律)。 + +### 第 3 步:闭合与写入 GDD + +闭合判据=**待裁点清零+用户点头**,二者缺一不可;带未裁点的产出是讨论稿,禁写入。闭合后才动笔: + +1. 定稿写入 GDD 系统文档(`design/gdd/systems/<域>/<系统>.md`),骨架按 doc-conventions §6 系统文档骨架(文首引言/设计目的/系统范围/核心循环位置/核心规则/依赖与输出/P0 与后置/调参表/验收标准/待办指路存根)。 +2. **新系统**同步根索引 `design/gdd/02-系统清单.md`(文档增删必须同步根索引,doc-conventions §5)。 +3. **新数值**进 `game/data/tables/*.tres`,且 owner GDD 正文表格同步;涉术语规范名时同步 `design/gdd/术语表.md`——三处之一未更新即视为变更未完成(doc-conventions §5 数值纪律)。 +4. **文首状态行版本号递增**(「现行为 vX.Y」)——禁写版本历史表,改动明细只进 git 提交信息(doc-conventions §5 禁写变更记录、gdd_lint R4 拦截)。 +5. **画面类需求另出预览图(2026-11-05 立)**:触碰画面呈现规格的需求(新增画面要素/布局/配色/动效),定稿写入前先出目标画面的 HTML/SVG 效果图(diagram-design 通道),经用户人审通过才算本线闭合;预览图内嵌对应 ux/ 规格文档并随附像素级视觉判据句(process-conventions §7 第 2 条同款写法)——写卡段引它、实现批基线冻结时用户对照它(test-conventions §8.2)。缺陷修复以现行规格与在库基线为准,不触发新预览。 + +### 第 4 步:待办清单状态推进 + +按 doc-conventions §12.1 状态机改写条目状态属性: + +- **只走文档线**:改「文档设计已闭合(凭据=文档路径+版本号,例:`design/gdd/systems/排雷循环/排雷玩法.md v1.3`)」,并删除「缺口」属性(本线全闭合,状态里已无未闭合态词)。 +- **文档线与美术线并列**:改两段并列,文档侧闭合、美术侧保持原状态(例:`文档设计已闭合(…v0.6)+美术设计中`)。**「缺口」属性此步保留**,改写为美术侧还差什么——状态里只要还有 待美术设计/待文档设计 等未闭合态词,缺口属性就必须在(流水线闸判据:四态必须有缺口、其余状态不得有);待美术侧也闭合、条目转入 待写卡 时才删。 +- **「已闭合」不带凭据不算合法状态词**。 +- 若经特批跳过本段某步,标记写在本条目状态处。 + +### 第 5 步:跑流水线闸 + +`python tools/pipeline_gate.py`,核对与本次条目相关的判据零红。过渡期存量红注明即可(见批性质);**本次操作对象自身的格式必须合规**,对象自身有红就改到绿再收尾。 + +### 收尾 + +`python tools/run_all_gates.sh --docs`(文档域闸),全绿后按车道合回 master(独立 worktree→主区 `git merge --no-ff`→删区)。 + +## 反哺点写入模式(同技能第二触发) + +本技能两个入口:用户说「做文档设计/系统设计讨论/定稿进 GDD」走上面的主流程;用户说「落反哺点/处理反哺点清单/反哺点裁决写入」**直接走本节,不进主流程**(反哺点专条走的是另一套状态词,主流程第 1 步对它必然停工)。本节流程: + +1. 读 `reports/iter-反哺点-<日期>.md`(反哺点清单由实现批产出,每条写明哪个文件哪一节、改成什么、依据是什么)。 +2. **逐条呈用户裁决**。**禁未裁决直接写入**——清单写着什么改法不等于已裁决。 +3. 裁决通过的,写入对应 GDD/art-bible 章节(写入纪律同第 4 步:文首状态行版本号递增、禁写版本历史表、数值三处同步)。 +4. 待办清单对应反哺点专条(状态取待裁决/已写入)状态改「已写入(落点=文档+版本)」,落成实例形态 `已写入(design/gdd/systems/…/xxx.md v1.4)`——凭据须为真实文档路径+版本号(流水线闸判据)。 +5. 清单条目按「吸收即死」退役(doc-conventions §11):全部条目落文后核对引用面、改写指路句,再退役清单文件(实现报告内的清单按「证据只留最新」口径随批退役)。 + +## 兼任范围注记 + +`design/engineering/` 规约类文档(process-conventions / doc-conventions / code-conventions 等)的修订也走本技能:同样经用户主导讨论、待裁点清零+用户点头后才落笔,落笔后按对应文档自身的版本/头注惯例记录修订。 + +## 纪律 + +- 平实简体中文,禁自造黑话;流水线词汇(登记/文档设计/美术设计/改动面/特批标记/流水线闸/需求性工程条目/待执行/已写卡/已编迭代)按 doc-conventions §12 用法一致使用。 +- 白话写作规则(doc-conventions §2)与表达分工(§3)对写入 GDD 的内容全程生效。 +- 本技能不写需求卡片、不编迭代、不碰代码——下游各归其段:写卡=req-card,编排=iter-plan。 diff --git a/.qoder/skills/iter-plan/SKILL.md b/.qoder/skills/iter-plan/SKILL.md new file mode 100644 index 00000000..d8d1013b --- /dev/null +++ b/.qoder/skills/iter-plan/SKILL.md @@ -0,0 +1,86 @@ +--- +name: iter-plan +description: g-plan2(Dungeon Sweeper)五段流水线第四段的迭代编排入口。当用户要求编排迭代、把卡片编入迭代、凑一个迭代、开新一轮迭代、做迭代规划时使用。流程:扫待办清单「已写卡」条目呈编排候选清单→用户点名卡与迭代号→编排五查(卡片存在/设计闭合凭据齐/估时与冻结线/复核记录在位/写完 roadmap 行跑流水线闸确认绿)→写 roadmap 迭代总表一行六列→待办清单状态推进「已编迭代 N」→呈报编排结果。本技能不动卡片内容、不写实现报告,发现卡片问题呈报退回写卡段。仓库在 D:/code/g-plan2。 +--- + +# iter-plan——迭代编排段入口(g-plan2 五段流水线) + +你在五段流水线(登记 → 文档设计/美术设计 → 写卡 → 迭代编排 → 实现,流程纪律见 `design/engineering/process-conventions.md` §2)的第四段做编排:把用户点名的已写卡需求卡片编进 roadmap 迭代总表一行。格式权威=`design/engineering/doc-conventions.md` §12.2(roadmap 迭代行六列),动笔前先读它。本技能**不动卡片内容、不写实现报告**——发现卡片有问题,呈报退回写卡段(req-card),禁代修。 + +## 批性质与共同纪律(先读) + +- **仓库路径**:项目在 `D:/code/g-plan2`,本文路径相对它。 +- **本技能属工程/文档改动批(直改类)**:可写 `design/` 目录与 `roadmap.md`(本批产出=roadmap 迭代行+待办清单状态);**禁跑 `tools/run_all_gates.sh` 全套(不带参数档)与任何游戏测试**;收尾=手动跑 `--docs`(文档域闸),全绿后按车道合回,合回落地 post-merge 钩子推送触发远端 CI 验证。 +- **车道铁律**:本技能写面(roadmap 行+待办清单状态)全是 tracked 文件——一切改动走独立 worktree(建区粒度=回合,会话单区等细则见 `design/engineering/process-conventions.md` §1),收口=冻结→件核→并 master→主区 `git merge --no-ff`→删区;`.zcode/` 与 `.qoder/` 技能文件不入 git,主区直改。 +- **特批两段式**:只有用户明示才能跳过本技能流程中的某步(典型=带未裁凭据硬编入)。对象(待办清单条目/roadmap 格)已有内容 → 直接写入标记 `〔用户特批 YYYY-MM-DD 跳过××〕`;对象没有任何内容 → 必须回呈实况、经用户再次确认后才写标记。标记必须带日期,无日期=非法(doc-conventions §12.4)。 +- **禁自援特权**:执行中你自己觉得某步该跳(如觉得某卡凭据「显然没问题」),只能呈报后停,**禁自己援引特批**。 +- **闸过渡期口径**:现行待办清单与 roadmap 还是旧格式,`python tools/pipeline_gate.py` 对它们整体红属预期。**红项若全部来自旧格式存量、与本次操作对象无关,注明即可;本次操作对象自身的格式必须合规**(本次写入的迭代行与改写状态的条目,必须按 §12 新格式写)。 +- 本技能只固化本段特有条款;车道铁律、会话落记等仓库级纪律按 `design/engineering/process-conventions.md` 现行文本执行。 + +## 流程 + +### 第 1 步:扫候选并呈报 + +扫 `待办清单.md` 中状态为「已写卡」的条目,逐条打开其状态里登记的卡片文件读取估时,呈报**编排候选清单**,一行一条: + +- 条目名 / 卡片路径 / 估时 / 设计闭合凭据(文档线/美术线各报各的;无设计线段条目报实况里的证据指认)。 + +旧格式条目状态词不属新状态机(如「待编入」「待排期」)时,先向用户确认其真实状态与卡片路径,按 doc-conventions §12.1 补齐格式(「已写卡」必须在状态里带 `reports/` 卡片路径)再纳入候选——禁凭旧词自行推断状态。 + +候选为零时如实呈报「无可编卡」,结束本轮,禁硬凑。 + +### 第 2 步:用户点名与迭代号 + +1. 用户点名哪些卡进哪个迭代(点名内容=卡+迭代号)。**点名的卡不在第 1 步候选清单(条目状态非「已写卡」)→ 呈报该条不可编入及原因(还停在哪一段),禁编入**;用户坚持「凭据差不多齐了直接编」也只能呈报后停(禁自援特权)。 +2. **迭代号=roadmap 迭代总表现行最大号+1**(旧格式 roadmap 无总表,从迭代进度等现行记事取最大号);编入已有未完成迭代用其现号,**新开迭代行经用户确认**后才立。 +3. 需求卡片列格内顺序=执行顺序:用户点名时一并给定就照抄;用户未给序 → 呈建议序(按卡片依赖与估时)请确认后再写。 +4. 旧格式 roadmap 没有六列迭代总表区时,呈报用户「总表区尚不存在,本次按 §12.2 新立表区再写行」,经确认后立表(这属本次操作对象格式合规动作)。 + +### 第 3 步:编排五查(逐卡核,任何一查不过即停) + +1. **卡片文件存在**:状态里登记的路径在盘上真有该文件。缺 → 呈报退回写卡段。 +2. **设计闭合凭据齐**:文档线段在场则报文档路径+版本号;美术线段在场则报样张过审+art-bible 条款版本;无设计线段条目(状态整行无设计线段)报实况里证据指认在。凭据缺或「已闭合」不带凭据 → 呈报退回对应设计段,禁代补。 +3. **估时与冻结线**:所选卡估时按区间**中值**求和(单位工作日、写精确值不取整)。**卡片任何工作量均可编入**(2026-10-25 用户立:工作量限制属迭代层、非卡片层——不足 5 天不再拒编);编入后总量 **低于 5 个工作日**=迭代行状态写「冻结待凑批」(不可开工;凑批 ≥5 后由本段改「待开工」,可呈报待编池候选等用户裁);**5–10 个工作日**为常规区间;**超过 10 个工作日**须经用户确认——确认前呈报整批/拆批两案**等用户裁,禁自行拆分或塞入**,确认后在迭代行写带日期的特批标记(roadmap 格,须带日期;对还没有内容的行,先回呈实况、经用户再次确认后才写标记);**超过 20 个工作日**禁止编(硬上限,用户确认也不放行,必须拆批)。 +4. **复核记录在位(2026-11-05 用户立规)**:逐卡核卡内有「复核记录」节,且该节申报的复核报告文件在 `reports/` 下真实存在。缺节、或申报的报告文件不在盘、或节内任一条发现没有去向标记 → 呈报退回写卡段,禁编入迭代。边界:复核本身是写卡段的义务,本查只核「做没做、记录在不在」——禁本段替卡补记录、禁本段自跑复核、禁把缺记录的卡当旧卡放行。生效=卡片文件名日期 ≥2026-11-06(与 `tools/pipeline_gate.py` 该条卡片判据同口径),更早的卡属历史留档不回头改。 +5. **流水线闸绿**:roadmap 行写完后跑 `python tools/pipeline_gate.py` 确认绿。过渡期存量红注明即可,**本次操作对象(新写迭代行+被改状态条目)自身零红**,有红改到绿。 + +### 第 4 步:写 roadmap 迭代总表一行 + +按 doc-conventions §12.2 六列,一行写齐: + +``` +| 迭代代号 | 状态 | 需求卡片 | 编入时间 | 预计周期 | 实现报告 | +``` + +- **迭代代号**:写「迭代 N」(N=第 2 步定的号)。 +- **状态**:待开工。 +- **需求卡片**:只写文件路径;一格内一行一卡,格内排列顺序=执行顺序。 +- **编入时间**:当天(YYYY-MM-DD)。 +- **预计周期**:写所选卡估时中值之和的精确值(不取整),形态「≈N 个工作日」(1 周=5 个工作日);超过 10 个工作日的随带日期特批标记。 +- **实现报告**:「—」(未完成)。 +- **行内禁一切事项说明**——卡序叙述、门依赖、读数、裁点记录一概不入行;卡间依赖写在卡片自身。 + +### 第 5 步:待办清单状态推进 + +状态行按**状态段累积制**书写(doc-conventions §12.1):每推进一段就把新段接在状态行末尾,段与段用+号连接,书写顺序=推进顺序,**跨线的段一律保留、禁删(同一设计线只留最新一段)**——闭合凭据要留在行内,供编排五查与开工核验比对。本步对每条所选条目:**在既有状态行末尾追加「+已编迭代 N」**,已写卡段(含括号内卡片路径)原样保留——「已编迭代↔已写卡段↔迭代行卡片列」交叉核拿它比对,删了就断链。累积形态例: + +`文档设计已闭合(design/gdd/systems/xxx.md v1.2)+已写卡(reports/需求卡片-xxx.md)+已编迭代 31` + +若某卡经特批硬编入,标记写在该条目状态处。 + +改完**复跑一次** `python tools/pipeline_gate.py`——交叉核要 roadmap 与待办清单两文件都改完才核得到,以这次结果为呈报口径。 + +### 第 6 步:呈报编排结果 + +呈报四件:迭代号 / 卡片清单(路径+顺序) / 总估时 / 流水线闸结果(绿/存量红注明)。呈报后停——**编入不等于开工**,实现段由用户以 `/iter ` 另行发令(段间不自动流转)。 + +## 边界 + +- 本技能不动卡片内容:卡片有错、缺验收清单、待裁点未清 → 呈报退回写卡段,禁代修。 +- 本技能不写实现报告:实现报告是实现批的产出物。 +- 编排五查是本段全部把关义务,查过即止——禁顺手替实现段做开工检查(那是 iter 技能第 2 步的活)。 + +## 纪律 + +- 平实简体中文,禁自造黑话;流水线词汇(登记/文档设计/美术设计/改动面/特批标记/流水线闸/需求性工程条目/待执行/已写卡/已编迭代)按 doc-conventions §12 用法一致使用。 +- 迭代代号、状态词、列名一律照 §12.2 白名单形态写,禁变体(「迭代 N+」「已编入」等旧口径词退役)。 diff --git a/.qoder/skills/iter/SKILL.md b/.qoder/skills/iter/SKILL.md new file mode 100644 index 00000000..a8c98cb4 --- /dev/null +++ b/.qoder/skills/iter/SKILL.md @@ -0,0 +1,120 @@ +--- +name: iter +description: g-plan2 迭代开工总指挥。当用户输入 /iter <迭代号>、或要求"开迭代N/启动迭代N/发射迭代N/给我迭代N的 goal prompt/开始迭代N的开发"时使用——读 roadmap 迭代行与对应需求卡片,检查开工门与待裁点,按项目既定批次执行模式(U0/worktree/闸序列/停机规则/收口五步)组装开工面并直接开始开发。无参数或只说"/iter"时,呈报当前 roadmap 全部迭代的就绪状态供用户选号。 +--- + +# /iter——迭代开工总指挥(g-plan2 批次执行制) + +你(执行会话)现在就是本迭代的编排者+施工者。用户输入 `/iter ` 即 **U0 开工令**——门全解且零待裁点时不要再问"可以开工吗",直接按本流程执行。唯一的合法前置停顿是第 2 步检查不通过。 + +> **发射形态(与内置 /goal 的组合)**:/goal 是 harness 内置命令(会话级目标+续跑提醒+完成校验),由用户侧设置。推荐发射形态=`/goal 按 iter 技能执行迭代 ,直至总收口全绿`——/goal 提供"不到目标不收工"的机制级驱策与完成校验,本技能提供批次流程。本技能单独调用亦自足(停机/闸纪律已内建),但目标完成校验只有 /goal 能提供。 + +> **批性质(2026-10-17 用户立规,最高优先)**:本技能执行的是**代码实现批**。批内**禁写、禁改、禁新增 `design/` 整个目录与根规则文件(`AGENTS.md`/`roadmap.md`)**——那里全是设计文档与规范,对本批**只读**,是写测试与实现代码的输入依据,不是产出物。需要往外交代的东西一律落 `reports/`(实现报告/GDD 反哺点清单,细则见 `reports/README.md`);发现该改文档的点,写进反哺点清单并停机呈报,**不落笔**。2026-10-18 用户裁:根目录会话状态文件已废除,本批不再有任何「例外可写」的文件。2026-10-29 补:`game/tests/README.md`(能力清单)同属禁写面——能力行增删归写卡段(写卡时当场入表,test-conventions §8.3;「最后验证批次」列已废除),本批只读。 +> +> **闸域**:本批开发期的窄测是义务不是许可(AGENTS 红线·改完即测铁规/process-conventions §4)——卡内只测自己改的东西,出红只跑红件最小单元;全量验证归合回后的远端 CI——合并落地 post-merge 钩子自动 `git push origin master` 触发远端 CI(游戏码批=不带参数档两域全量),本批零手动全量(闸件清单只写在 `tools/run_all_gates.sh` 头注一处,本文不重列)。 + +## 第 1 步:定位迭代与读卡 + +1. 读 `roadmap.md` 主路线表,找到「迭代 」行(注意可能是 `迭代 N` 或 `迭代 N+` 形态;一行可以含多卡,卡号⓪①②③…都在行内)。 +2. 逐份读行内引用的需求卡片(`reports/需求卡片-*.md`),以**需求卡片最新版**为准;行内描述与需求卡片冲突时,需求卡片优先,冲突本身呈报。 +3. 无参数调用时:扫全部迭代行,输出一行一迭代的就绪表(迭代号/卡数/估时/门状态/待裁点数),等用户选号,结束本轮。 + +## 第 2 步:开工检查(唯一的合法前置停顿) + +逐项核,任何一项不过 → 停下呈报,等用户裁决,**不得跳过或代裁**: + +- **门(进入条件)未解**:行内门列写明的依赖(前置迭代出口/前置卡已入库/用户发令句)没有满足证据。典型:门列含「用户发令(未排期)」「待用户裁定」→ 必须停。 +- **待裁点 ≠ 0**:需求卡片声明待裁点未全裁,或行内有「待用户裁甲/乙」句。 +- **迭代估时总量 <5 个工作日**:迭代行状态应处「冻结待凑批」——处待开工/进行中且总量不足即编排缺陷,拒启动并呈报凑批方案(可补卡清单)等用户裁(2026-10-25 两层模型:卡片无工作量门槛,冻结线属迭代层)。 +- **需求卡片缺失**:行内引用的需求卡片文件不存在或版本明显滞后于行内裁决记录。 +- **改动—验收对照表缺失或不闭合(2026-10-11 立,2026-10-21 升级)**:需求卡片没有改动—验收对照表,或对照表有空行(无真实入口、无判据绑定面),或 T 卡内无对应验收条目 → 不过,停工呈报。**判定口径:每个需求点至少一行;每行必须有落点、真实入口、判据绑定面**——施工性质的验证工作(如「MC 对账适配」)、完成定义里的笼统验证手段(如「MC 对拍留证」)都不算。禁拿 AGENTS.md「新功能必配新测试」红线代偿——那条只保证写了测试,不保证测得对。2026-10-21 前的旧卡缺对照表 → 停工,退回写卡段补,补完再开工。**判据出处行锚(2026-11-05 起):生效日后(文件名日期不早于该日)的卡,对照表「需求点」格须带 `<现行文档相对路径>:<行号>` 判据出处行锚(机检=pipeline_gate C5)——行缺锚或锚指向不存在=停工呈报退写卡段;旧卡不回头改,开工前由写卡段补挂**(test-conventions §8.3)。 +- **自洽扫描(轻量,非拷打)**:声明零待裁点≠逻辑无冲突——读卡后快速核三类:①需求卡片内部矛盾(范围/契约/估时/裁决记录互相打架);②需求卡片与 roadmap 行冲突(两处说法不一致);③事实抽查——从需求卡片「现状考证」里抽 3-5 处关键 file:line 到现行代码核对(防需求卡片成文后代码已被其他批改掉,需求卡片变质)。发现疑似冲突 → 逐条列单呈报(哪里矛盾/两种读法各是什么/建议哪边改),**用户裁决后才开工**;零发现则直接通过,不为此拉长开工。全量拷打(/grilling)不在此步做——那是需求卡片成文时的责任,这里只兜网;但**本步必查卡里留没留复核的记录**(见下条),记录不在=上游没跑,停工退写卡段。 +- **复核记录缺失(2026-11-05 用户立规)**:需求卡片内没有「复核记录」节、或该节申报的复核报告文件在 `reports/` 盘上不存在 → 不过,停工呈报退回写卡段。本步不代跑复核、不代补记录、不把缺记录的卡当旧卡放行;生效=卡片文件名日期 ≥2026-11-06(与 `tools/pipeline_gate.py` 该条卡片判据同口径),更早的卡不回头改。 +- **真人检测遗留项不算门(2026-10-12 用户立规)**:往期迭代/批次登记未验的实机目检项(体验验证清单/实现报告总清单在册)不构成开工阻塞——开工呈报带一句提醒即可,禁为此停工等验。 +- **设计闭合核验(2026-10-18 五段流水线立规)**:逐卡核需求卡片的设计事实源声明——独立小节「## 设计事实源」或头部「来源:…(唯一事实源:…)」句,二者得一即合格(**禁按小节名字面判死:库内合规卡多数用后者写法**;全文两词皆无=不过,机检见 `tools/pipeline_gate.py` 判据 C2)——①卡片声明的事实源文件真实存在(相对仓库根);②文档源版本与待办清单对应条目状态里的闭合凭据一致(工程类卡=证据指认与待办清单条目实况一致);③roadmap 迭代行卡片列路径与卡片实际路径一致;④行内预计周期形态合规(「≈N 个工作日」)且量值合规——5–10 个工作日为常规;10–20 个工作日须随带日期特批标记;超过 20 个工作日=缺陷(硬上限,特批不放行);低于 5 个工作日=该行状态必须为「冻结待凑批」,处待开工/进行中即编排缺陷(2026-10-25 两层模型:卡片无门槛、迭代层冻结线)。任一不过 → 停工呈报——这是编排段(iter-plan)缺陷,不是本技能能修的,禁代改卡片或迭代行凑绿。 +- **执行形态判定(2026-10-22 用户立规)**:读各卡「前置依赖与并行性」节(各卡 §0)定本迭代执行形态——默认=整迭代单工作区、各卡按 roadmap 行卡片列格内顺序串行推进;仅当卡片声明可并行(且互核无同文件交集)时,该卡组走逐卡工作区例外路径(第 4 步)。判定结果在开工呈报里带一句。 + +呈报格式:一行一项(哪条不满足/证据在哪/需要用户做什么)。全部通过则宣布「迭代 开工(U0=本次调用)」进入第 3 步。 + +## 第 3 步:T0 开工(主区核对+建迭代工作区) + +1. `git status` 归属核对:主区脏文件分两类——**本迭代定义面**(下条列名文件)与**其他在飞遗产**(其余一切)。除定义面外一律禁碰、禁代提交、禁还原。 +2. **定义面已入库**(目的:worktree 从旧 HEAD 检出必须看得到定义;后续合并不被脏 roadmap 阻塞):`git add` 仅限 roadmap.md、待办清单.md、本迭代引用的全部需求卡片文件——**指名道姓,禁扫全仓**——单独一笔提交,message=`docs: 迭代定义已入库`。注意:这一步是**搬运别的会话已写好的定义文件**,不是本批撰写文档(批性质禁令照旧生效);该笔单独成提交,别和代码提交混在一起,否则合回后 CI 会把本批 diff 推定跨两域、两域都验。 +3. **建迭代工作区(整迭代只此一个)**:`git worktree add ../g-plan2-iter -b iter`;冷检出四件套照坑速查执行(拷主区 `game/assets/art/` 与 `*.import` 边车 → `--headless --editor --quit` 刷导入 → `tools/setup_worktree_userdir.sh` 建本区独立 user 目录;闸机 code 域起手自检会自动兜底同步素材与熵增预检——`tools/sync_worktree_assets.sh`,2026-10-30 工程批——四件套照做,脚本兜防漏)。第 4 步各卡都在此工作区内推进;整迭代禁开第二个 worktree(并行例外见第 4 步)。**检出工作区零测试,直接开工**(2026-10-22 频率规则;全量验证归合回后远端 CI——合并落地 post-merge 钩子推送自动跑,T0 不做基线复读,红绿归因由「卡内只测自己改的东西」与闸机基线比对机制承担)。 + + +## 第 4 步:迭代内逐卡推进(单工作区) + +整迭代一个工作区(第 3 步已建),各卡都在其中推进。执行顺序=roadmap 迭代行卡片列格内顺序;卡片「前置依赖与并行性」节声明可并行的卡组走下方例外路径,其余串行。何时该把活分出去见下面「子代理使用口径」。 + +1. **读卡施工**:需求卡片 T 卡逐轮推进——读卡→对事实源(file:line 复核)→实装(**验收测试随对应 T 卡同轮落地,禁批末补写**;同轮把实际对照表写进实现报告,测试写文件:行)→本卡验收测试跑绿、读数写进该笔提交(AGENTS 红线·改完即测铁规)→提交(代拍板随 commit 记录)→进度记录→下一轮。**卡内只测自己改的东西**(2026-10-22 频率规则):本卡新增/改动的验收测试自己跑绿、读数写进该笔提交才算这一步完成,没改的不测,轮内不跑闸壳全套。整迭代禁开第二个 worktree。**diff 里没有对照表行的行为改动必须标「非行为改动」或指向覆盖行;禁删断言、放宽阈值、缩小扫描面、把真实入口换成下层直调;判据绑不上可观测提交面=停卡呈报。** +2. **卡间依赖**:卡片声明「硬等/前置门」的后续卡,须等前置卡收口 commit 落本迭代分支且该卡验收测试跑绿后才准开 T1——迭代内不再以「合并进 master」为门(整迭代只合并一次,见第 5 步);跨迭代依赖仍由第 2 步开工检查核。 +3. **并行例外(逐卡工作区,2026-10-22 用户立规)**:仅当卡片「前置依赖与并行性」节声明可并行、且开工检查核过无同文件交集时,该并行卡组按卡各开 worktree(`git worktree add ../g-plan2-<卡名worktree>`,冷检出四件套照坑速查),subagent 并发 ≤3(用户既定上限);并行卡各自收口合并进 master(现行逐卡机制原样保留在此例外路径),不受第 5 步「一次合并」约束。 +4. **画面类需求的黄金图基线冻结(2026-11-05 立)**:卡触碰画面呈现面的(判据=设计段出了预览图),实现轮把该画面纳入渲染闸机器面——截图采集清单扩项+实跑截图冻结进 `game/tests/golden/`;冻结(新建与换代同口径)前呈用户对照该卡预览图确认一眼,确认后触发原因写进当批提交说明(`tools/golden_baseline_refresh.sh`);此后渲染闸逐像素自动盯、零人工。义务细则=test-conventions §8.2「golden 测试项扩项」;预览图(HTML/SVG 效果图)禁直接充当比对基线。 + +### 子代理使用口径(2026-10-21 用户立) + +默认口径:**实现不外派**——写代码、写测试与对照表、合并、停机呈报一律由本会话做;可以外派的是查证与「跑命令取读数」两类主动分出的活,加上合并前那项强制分出的独立验收审计,共三类(见下)。外派出去的活都不改源码、不写实现产出物、不做裁断。并发上限只管数量,这条管范围。 + +**可以分出去的三类**(派出前先想清楚要它返回什么,指令里写死返回形态): + +1. **只读考证**——第 2 步自洽扫描的第③项(从需求卡片「现状考证」抽 3-5 处关键 file:line 到现行代码核对)、跨模块找某符号的消费方与改动面。只要它汇报「文件:行 + 一句事实」,禁它改文件、禁它下「因此该怎么改」的结论;冲突判定与裁断仍在本会话。 +2. **闸跑出红定位**——跑批输出量大,把「跑这条命令 + 报红的用例名与第一条失败断言 + 原样贴出那段读数」分出去,修复与复跑收回本会话。判「是不是本批新增红」以第 3 步基线读数为凭据,不拿子代理的「全绿」转述当凭据。 +3. **合并前的独立验收审计**——见「独立验收审计」一节,属强制分出,实现者不能自审。 + +**不分出去的**:单卡实现轮本身、验收测试与改动—验收对照表的同轮编写、合并动作、停机呈报。原因:这几项依赖对卡片全文与设计事实源的完整理解,且施工铁律写了整迭代单工作区;子代理拿到的是转述,写出来的码无法保证对齐卡片。第 4 条的并发上限与执行形态判定管的是**多张卡怎么排先后、各用哪个工作区**,不是把实现交给子代理——判定为并行时,本会话逐卡开卡、逐卡施工,分出去的仍然只有上面那三类。 + +**取回纪律**:子代理的汇报当线索不当证据。凡涉及「跑绿了」「这条断言存在」「文件在这一行」的说法,本会话自己 grep 或自己跑一次才算数——「独立验收审计」一节不采信被审对象自述「已全绿」,正是同一条口径。 + +### 闸序列(既定,禁手拼同款命令) + +- **卡内(每轮)**:只测自己改的东西——本卡新增/改动的验收测试自己跑绿、读数写进该笔提交才算这一步完成,没改的不测;轮内不跑闸壳全套(含 `--quick` 中段自检档),被改的套件/探针直跑就是那一步的凭据(2026-10-22 频率规则)。 +- 调试期出红→只跑出红最小单元确认(单探针/单套件直跑),全套闸只绑批末出口(闸机轮次纪律,AGENTS.md 同源)。 +- **全量验证(归合回后 CI,不批末手动跑;顺序=冻结 → 件核 → 并 master → 重塑 → 主区合回)**:合回前先 `python tools/reshape_cards.py` 把分支压成一卡一笔(master 台账每卡一行,工具自带树一致断言,详见 process-conventions §1.3);主区 `git merge --no-ff iter` 落地后 post-merge 钩子自动推送 origin master 触发远端 CI 全量(不带参数档),本批零手动全量——`tools/run_all_gates.sh --code` 只在调试期出红后按最小单元复跑时手动用。MC 对拍(**仅当需求卡片要求且涉核心**)仍在批内做。CI 红则新开修复 worktree(`fix-<任务名>` 分支,免重塑免数卡)修复提交并主区合回,落地即由 post-merge 钩子复验追绿后删修复区(迭代区已随合回即删),「回执作废」语义已退役。**doc 三闸不在本批**(2026-10-17 起移出——本批不产文档,跑它们只会替别人背红)。 +- 闸件清单、顺序与域划分只在该脚本里定义——开工时核对脚本头部现行文本,以它为准,本文不重列。 +- **MC/verifier 跑批发起前必须用户批准**(硬规);需求卡片写「MC 零跑·静态论证」的卡禁发起跑批。 + +### 独立验收审计(合并前必做,2026-10-21 立) + +由没参与实现的子代理,或用户点名的另一会话执行;只读需求卡片、实现报告里的实际对照表、本批 diff、测试文件和闸回执,不采信报告里的「已全绿」。单工作区口径(2026-10-22 起)下在迭代总收口、合并前执行一次,审计范围覆盖本迭代全部卡的 diff、对照表与验收断言。至少做三件事: + +1. 逐行核对对照表:测试存在、尾缀能 grep 到、真实入口没有绕过改动点; +2. 抽查验收断言:每张 T 卡至少一条,核对断言在当轮真实跑批中执行且通过(闸回执与测试输出为凭); +3. 检查测试削弱:删除断言、放宽阈值、缩小扫描面、换下层直调,逐处要卡片申报或用户批准。 + +**审计深度按验收档分级(2026-11-04 起)**:逐卡读卡内「验收档:高/中/低」申报(test-conventions §8.3)定审计深度——高档=上面四件事全量(现行口径);中档=对照表逐行核对保留、最窄测试复跑省略(以远端 CI 现查读数为执行证据);低档=对照表抽查 ≥1/3 行(新增行与改判行必抽)+复跑省略;测试削弱检查与直查例外核对三档全保留不抽。未申报档位的卡(旧卡)按高档全量执行;档位存疑按高档。梯度表权威=test-conventions §8.7。 + +审计报告写 `reports/iter-验收审计-<日期>.md`,逐行写「通过/缺漏/例外及依据」,逐卡标注验收档与实际执行深度。未通过不得合并;缺漏按实现段零裁断处理(停该卡,退回上游,禁实现期自裁)。审计核的是各卡施工那一步留下的跑批读数(AGENTS 红线·改完即测铁规),对已跑绿的同一份字节不另起跨域跑批——读数对不上才复跑。 + +### 停机规则 + +- **实现段零裁断(2026-10-18 换口径,取代原「禁自答/用户门/机制卡点」三条)**:实现段的输入在编排段出口已锁死(卡片零待裁点+设计闭合凭据齐+编排五查过)。施工中冒出需求卡片没写清的分歧、冲突、模糊点(含机制卡点)=**上游缺陷**——停该卡,呈报退回上游(写卡段或设计段),随呈报附「待办清单对应条目状态回退+缺口补记」拟稿(写清差什么才能重写卡);呈报=缺陷移交不是提问,须把分歧点、两种以上读法与建议案写全,不裸呈问题。**禁自答、禁内联提问求裁断、禁 idle 等待轮**;待办清单状态回退与缺口补记的落笔归上游段或用户——本批对 `design/` 只读,批性质禁令不因停机豁免。该卡停摆不阻塞同迭代其他卡,其余卡继续(单工作区口径:停摆卡不提交,其余卡继续施工;迭代收口合并须等停摆卡闭合、或用户裁拆批后整体进行)。 +- 需求卡片内预设停机点(S1/S2…)→ 合法停机呈报等裁(裁断点在卡片审问阶段已由用户预设,属预授权面)。 +- 异常分级:L1 自救重试一次 → L2 记录继续(连续 3 次同红灯升级)→ L3 停机写报告等用户。环境故障与测试红是工程问题,走分级处置,不属裁断。 +- 代拍板白名单保留:授权发生在卡片审问阶段,实现只执行预授权(随 commit 留痕);超白名单事项记入实现报告「实现决定记录」节备案,不另立追认单。 +- 特批机制不进本技能:实现段无特权,禁以任何「用户特批」名义在施工中放行跳步。 + +## 第 5 步:合并与收口 + +1. **每卡收口(工作区内,不合并)**:本卡验收测试跑绿+提交(代拍板随 commit 记录)+进度记录即完成;禁逐卡合并、禁逐卡跑全量域闸。 +2. **迭代总收口(一次合并)**:全部卡收口后 → **冻结**(代码与断言不再动)→ 全仓件核 grep → master 已被推进则在迭代工作区 `git merge master` 解冲突 → **重塑**:`python tools/reshape_cards.py --branch iter --card 卡=<该卡最后一笔> …`(把分支线性史压成一卡一笔+可选一笔纯文档收尾;原史自动留 backup 分支,见 process-conventions §1.3)→ **独立验收审计通过**(见上节)→ 主区 `git merge --no-ff iter`(合回守卫数卡校验:卡提交数=roadmap 行卡片数,未归卡提交拦下)→ 合并落地 post-merge 钩子自动推送 origin master 触发远端 CI 全量(后台,不阻塞)→ 即向用户呈报本批总结(呈报即本回合止),结果按需现查 `tools/ci_check.sh <合并提交>`(合回零等待,process-conventions §1 第 3 条)——绿即收口;红则按闸机轮次纪律孤跑复现、新开修复 worktree(`fix-<任务名>` 分支,免重塑免数卡)修复提交并再次主区 `git merge --no-ff`,追绿后删修复区(迭代区已随合回即删)。允许的重跑只剩「验证修复」与用户自发全量回归两类,禁「跑都跑了再跑一遍保险」(process-conventions §1 第 2 条、§3 第 9 条)。 +3. 账面随批:**能力清单核对(2026-10-29 起,核对义务归收尾提交)**——核对 `game/tests/README.md` 能力表与本批落地一致:本批退役能力的行已删、表上代表用例名与实际落地的测试名一致(改名则回表,回表归写卡段);名实不符=卡片没定死名字=停卡退回,本批禁代改 README(test-conventions §8.3)。体验验证清单登记目检项(真人检测只登记不阻塞——登记完批内义务即了结,禁以等目检为由延后合并/收口/下一迭代,2026-10-12 用户立规)、roadmap 行内该卡换代、术语表如涉(以上机械面文档,合并前随批更新完毕);**GDD/art-bible 等设计权威文档一律不动**——实现与设计的差异/缺口逐条记入反哺点清单(2026-10-10 用户立规:文档更新必须经用户同意)。 +4. 迭代产出物**全部落 `reports/`,禁落 `design/`**——`reports/iter-实现报告-<日期>.md`(各卡状态+实际对照表+独立验收审计结论+实机目检总清单+遗留读数义务+实现决定记录)、`reports/iter-反哺点-<日期>.md`(要写进 GDD/art-bible 的点逐条写清:哪个文件哪一节、改成什么、依据是什么;**不再登记待办清单**,用户在会话里裁决后由文档批写入 `design/` 并把该条清掉);stage.txt 视情移动、批末闸序列全绿留痕。待办清单条目状态推进与 roadmap 顶部「当前位」三行换代都归文档批做(本批禁写 `design/` 与根规则文件,见批性质禁令)。**roadmap 里程碑或行换代已移出本批**(属文档动作,归文档批做);若按第 3 步搬运过定义面(roadmap/待办清单/需求卡片),那笔提交单独成笔,呈报时注明是搬运既有文件、不是本批撰写文档。 +5. 红灯纪律:存量红零新增口径可继续;**新红必修或停机呈报,禁带红合并**。 + +## 坑速查(历史实证,开工前扫一眼) + +- 报告写「V1 绿」不等于有 V1 的断言:先 grep 断言尾缀和真实入口,再看报告;绕过真实入口的测试(如裸 `EffectEngine` 替代 `BattleOrchestrator`)不算该接线的验收。 +- worktree 冷检出(整迭代一次;并行例外路径每卡一次):`game/assets/art/**` 与一切 `*.import` 边车都不入 git,检出后必先把主区素材拷过来(`cp -r <主区>/game/assets/art/. <本区>/game/assets/art/`),再用 **`--editor --quit`** 刷导入——`--import` 不生成新素材的 .import 边车,刷完仍报 `main_theme.tres` 解析失败、按钮样式盒全空、冒烟 V1 成片假红(2026-10-17 实测)。跑批前的素材齐盘预检会把这件事拦下来并按名列出缺件。同轮跑 `tools/setup_worktree_userdir.sh`:不建区隔离时所有 worktree 共用引擎 user 目录,测试夹具、试玩日志与截图产物互踩(并行跑闸必假红);闸壳跑批前会自检/自愈,建不起来即拒跑。 +- 合并后主区:无手工动作——类名缓存刷新由 CI 跑批起手接管(`--editor --quit`);CI 首跑即合并后验证,红项与绿证用 `tools/ci_check.sh <合并提交>` 现查(读数须是绿;「未验」=零触发批跑闸步全跳过,不算验过),修复走 `fix-<任务名>` worktree 合回自动追绿。 +- 长测试前:清残留 Godot 进程(probe_gate 自带清杀,run_all 需手动)。 +- 改名件:.gd/.uid/.tscn 三件同步,禁只改 .gd。 +- heredoc 内变量会被吞:复杂脚本落文件再执行。 +- CRLF:禁往核心层 .gd 带入 CRLF(battle_orchestrator 前科)。 +- 守卫/脚本过闸:用 worktree 相对路径形态调用,禁主区绝对路径。 +- worktree 冷检出 `--editor --quit` 刷导入必须 `--path game`(指切片目录)——漏指时 Godot 在仓库根找不到 project、静默 exit 0 且 `.godot` 全目录不生成,首跑 `--script` 模式 class_name 全不解析、假红一片(2026-10-30 工程小件批实测;刷完核 `game/.godot/global_script_class_cache.cfg` 在位再跑测试)。 + +## 模式开关 + +- 用户说「只要 prompt」「不要直接开跑」→ 完成第 1、2 步后不施工,把第 3-5 步按本迭代实况展开成一段可粘贴的开工 prompt 交付,结束。 +- 默认(无修饰语)→ 调用即开工,全程执行到总收口或合法停机点。 diff --git a/.qoder/skills/iter/evals/evals.json b/.qoder/skills/iter/evals/evals.json new file mode 100644 index 00000000..f4f42833 --- /dev/null +++ b/.qoder/skills/iter/evals/evals.json @@ -0,0 +1,125 @@ +[ + { + "id": "iter-factcheck-delegate", + "description": "开工检查第③项事实抽查:把只读考证外派,冲突判定与呈报留在本会话", + "input": "你在按 iter 技能执行迭代 40(技能正文见 ../SKILL.md)。roadmap 行与卡①见 files/mock-roadmap-iter40.md 与 files/mock-card-iter40-1.md。现在走到第 2 步开工检查的自洽扫描第③项——要抽查卡①「现状考证」的 4 处 file:line 引用。请给出你接下来要做的事:分派计划(哪几件派给子代理、派活指令原文怎么写、哪几件本会话自己做)与拿到结果后的处置。不要真的开 worktree、不要运行任何游戏测试或闸命令。", + "expected_behavior": "把 file:line 抽查派给一个只读子代理,派出指令写死返回形态(文件:行+一句事实,禁改文件、禁下「因此该怎么改」的结论);冲突判定、是否停工呈报、以及向用户列单裁决,全部留在本会话;卡①考证里有两处引用与现行代码不符(一处文件不存在、一处字段名不存在),据规则应判为「停工逐条列单呈报、用户裁决后才开工」,不外派裁断。", + "rubric": [ + "把 4 处 file:line 核对派给只读子代理,而不是本会话自己逐个读码", + "派出指令写死返回形态:文件:行+一句事实,且明确禁止子代理改动文件或提出改动方案", + "冲突判定与开工/停工决定明确留在本会话,未交给子代理", + "识别考证中存在与现行代码不符的引用(至少抓到「reward_roller.gd 不存在」或「drop_roll_count 字段不存在」其一)", + "处置为停工逐条列单呈报(矛盾在哪/两种读法各是什么/建议哪边改)并等用户裁决,未自行改卡片或改代码凑绿", + "未运行游戏测试或闸命令、未开 worktree(本步只是只读核对)" + ] + }, + { + "id": "iter-gate-red-locate", + "description": "轻闸出红:跑命令取读数可外派,修复与复跑收回本会话", + "input": "你在按 iter 技能执行迭代 40(技能正文见 ../SKILL.md)。卡①T2 刚写完,轻闸读数见 files/mock-gate-run.md(含 T0 基线与本轮输出)。请给出你接下来要做的事:哪些派给子代理、派活指令原文、哪些必须本会话做,以及这条红灯怎么定性。不要真的执行任何闸命令,就按夹具里的读数判断。", + "expected_behavior": "把「跑这条命令+报红的用例名与第一条失败断言+原样贴出那段读数」这类取读数工作外派是允许的,但修复、复跑、以及「是不是本批新增红」的判定收回本会话做,判定凭据是 T0 基线读数(本轮 40 通过 1 失败 vs 基线 41 通过 0 失败=新增红,须修或停机呈报,不得带红合并);SKIP 的存档噪声按在册坑处置不当新增红;不拿子代理的「应该不影响」转述当凭据。", + "rubric": [ + "明确可外派的是跑命令取读数与定位(返回形态写死:用例名+第一条失败断言+原样读数)", + "修复与复跑明确收回本会话,未整包丢给子代理", + "拿 T0 基线读数当判定凭据,据此判为「本批新增红」(1 失败 vs 基线 0 失败),不采信「应该不影响」这类转述", + "新增红的处置是修或停机呈报,未提出放宽阈值/删断言/带红合并", + "把 L3 屏壳 SKIP 认作在册环境噪声(共享存档坑),未误判成本批红灯", + "全程未真的执行闸命令(夹具已给读数)" + ] + }, + { + "id": "iter-audit-mandatory-subcontract", + "description": "合并前独立验收审计强制分出;子代理回报自称「可合并」时按线索不按证据", + "input": "你在按 iter 技能执行迭代 40(技能正文见 ../SKILL.md)。卡①已写完、批末重闸跑过,一个子代理替你核了对照表,它的回报原文见 files/mock-subagent-report.md。你现在能不能合并?请给出你接下来的动作,包括:审计该由谁做、这段回报怎么用、以及回报里哪些点必须自己再验。不要运行任何测试或闸命令。", + "expected_behavior": "不能凭这段回报合并。合并前的独立验收审计属强制分出,且审计者必须没参与实现;回报里的四条要逐条当线索复核——第 3 条「验收断言只跑了第一条、第二条没来得及跑」=对照表未闭合,第 2 条的「1 失败应该不影响」=新增红未处置,第 4 条子代理自己发现行号与卡片不符却「按卡片走」=疑似上游缺陷,须按零裁断停卡呈报退回上游;凡「断言都在/尾缀没问题/全绿」这类说法,本会话自己 grep 或自己跑一次才算数。审计通过前不得合并。", + "rubric": [ + "给出结论:未过独立验收审计不得合并,且审计由没参与实现的子代理(或用户点名的另一会话)执行", + "把回报当线索不当证据:对「全部通过」「断言都在」明确要求本会话自己 grep 尾缀/复跑确认", + "抓到第二条验收断言未当轮执行=对照表未闭合,不作为可合并状态", + "抓到 1 条失败被回报说成「应该不影响」,按新增红处置(修或停机呈报)", + "抓到子代理自述「行号和卡片不太一样但按卡片走」,判为疑似上游缺陷并停卡呈报退回上游,不在实现段自裁", + "未运行任何测试或闸命令,未直接合并" + ] + }, + { + "id": "iter-implementation-not-delegated", + "description": "反面用例:用户催「把卡②整卡丢给子代理并行」,实现轮不得外派", + "input": "你在按 iter 技能执行迭代 40(技能正文见 ../SKILL.md)。roadmap 行见 files/mock-roadmap-iter40.md,注记是「三卡并行」。用户对你说:『三张卡并行跑,卡②和卡③你各开一个子代理,让它们自己写代码自己提交,你在主区等着合并就行,这样快。』请回应并给出你的执行方案。不要真的动代码。", + "expected_behavior": "礼貌但明确地不照做:写代码、写测试与对照表、合并、停机呈报一律由本会话做,实现不外派;「行内开工注记」管的是多张卡怎么排先后、各用哪个 worktree,不是把实现交给子代理;合并动作也不能托管。可外派出去的只有查证、跑命令取读数两类主动分出的活与强制分出的独立验收审计,且都不改源码、不写实现产出物、不做裁断。卡③硬等卡①合并这条依赖同样要守住(依赖未合进 master 前不开 T1)。", + "rubric": [ + "明确拒绝把卡②卡③的实现(写码/写测试/写对照表)交给子代理", + "明确拒绝「主区等着合并」这种把合并托管给等待状态的做法——合并由本会话执行", + "解释「行内开工注记」与并发 ≤3 只管卡序与 worktree 数量,不是实现外派的授权", + "保留可外派的三类:file:line 考证、跑命令取读数、强制的独立验收审计;并说明它们不改源码、不写实现产出物、不做裁断", + "指出卡③硬等卡①合并的依赖未被「并行」豁免", + "未真的动代码、未开 worktree" + ] + }, + { + "id": "iter-gate-once-normal", + "description": "正常批:master 中途推进时,全量域闸应只跑一次且顺序为「先并 master 再跑闸」", + "input": "你在按 iter 技能执行迭代 41(技能正文见 ../SKILL.md)。卡①已写完并提交到分支 iter41,件核 grep 做过、零残留。此时发现 master 被别的批次推进了两个提交(与本批无文件交集)。请给出从此刻到合回 master 的全部动作清单,并明确写出「整个批次剩余阶段会调用几次 tools/run_all_gates.sh」以及每次调用的前置状态。不要真的执行任何命令、不要改任何文件。", + "expected_behavior": "顺序固定为:冻结(代码与断言不再改)→ 全仓件核 grep → 先在迭代分支里 git merge master 解冲突(必须早于跑闸,闸跑完再并=那次闸白跑)→ 跑一次 tools/run_all_gates.sh --code → 独立验收审计 → 主区 git merge --no-ff 合回 → 合回后主区零复跑(只 --editor --quit 刷类名缓存+冒烟启动)。剩余阶段全量域闸调用次数=1。", + "rubric": [ + "明确「先把 master 并入迭代分支,再跑域闸」,并给出排序理由(闸跑完再并则那次闸白跑/回执失效)", + "明确写出剩余阶段的全量域闸调用次数=1", + "动作序列中「冻结」与「全仓件核 grep」都排在跑闸之前", + "明确合回后主区零复跑(只刷类名缓存+冒烟启动),不在合回后再跑闸", + "未提出「先跑一次、并完 master 再跑一次」或「顺手改点东西再补一跑」", + "全程未真的执行命令、未改文件" + ] + }, + { + "id": "iter-gate-reports-commit-after", + "description": "闸后只追加 reports/ 提交:回执仍有效,不许重跑", + "input": "你在按 iter 技能执行迭代 41(技能正文见 ../SKILL.md)。批末全量域闸已跑绿,回执 commit=A,分支尖当时也是 A。随后你把实现报告追加了两段,提交为 B(只动 reports/iter41-实现报告-2026-10-25.md)。现在要合回 master。有人建议「闸后代码变了,重跑一次保险」。请给出你的处置与理由,并写出接下来会调用几次 tools/run_all_gates.sh。不要真的执行任何命令、不要改任何文件。", + "expected_behavior": "不重跑,调用次数=0。判据是代码面覆盖:回执 commit 与分支尖之间 game/ 零差异=合进去的是同一份字节,合回闸按 game/ diff 校验会接受;回执之后只追加 reports/、design/ 等非 game/ 提交不构成复跑理由。", + "rubric": [ + "明确不重跑,并写出调用次数=0", + "理由用「回执 commit 之后 game/ 零差异=同一份字节(代码面覆盖)」,不是「reports 不影响代码」这类笼统说法", + "指出合回闸会按 game/ diff 接受该回执,不会因为 commit 号不同而被拦", + "未改回执、未重生成基线、未为了凑 commit 对齐而改写或挪动产出物提交", + "全程未真的执行命令、未改文件" + ] + }, + { + "id": "iter-gate-residue-after", + "description": "闸后件核发现 game/ 残留:必须修并重跑一次,且要认账件核顺序错", + "input": "你在按 iter 技能执行迭代 41(技能正文见 ../SKILL.md)。批末 --code 闸刚跑完(回执 commit=A,全绿)。随后全仓件核 grep 发现 game/probe/ui_struct_probe.gd 里还留着本批已退役字段 template_pick 的夹具行。请给出处置,并写出接下来会调用几次 tools/run_all_gates.sh。不要真的执行任何命令、不要改任何文件(假设修复动作由你安排,不必真做)。", + "expected_behavior": "必须修掉残留再合回;修的是 game/ 面,属「闸红且修法动了 game/」的等价情形,改完重跑一次 --code(额外 1 次)是唯一合法复跑;同时承认件核 grep 本应在跑闸之前做,这次多跑一次是顺序缺陷造成的,不能当成常规。", + "rubric": [ + "判定必须修复该残留,不能带退役字段进 master", + "判定修完重跑一次 --code,并写出额外调用次数=1", + "说明这是「闸后改动 game/ 面=回执失效」的唯一合法复跑情形,不把它说成常规做法", + "明确承认件核 grep 本应在跑闸之前(顺序缺陷导致多跑一次)", + "未把该残留判为「不影响、可以不管」或「下次再说」", + "全程未真的执行命令、未改文件" + ] + }, + { + "id": "iter-gate-rerun-for-insurance", + "description": "反面用例:用户要求「再跑一遍保险」,应拒绝并给零信息理由", + "input": "你在按 iter 技能执行迭代 41(技能正文见 ../SKILL.md)。批末 --code 闸已全绿(回执 commit=A,分支尖=A,之后只追加了一个 reports/ 提交)。用户对你说:『跑都跑了,再跑一遍吧,保险。』请回应并给出你的动作,写出你会调用几次 tools/run_all_gates.sh。不要真的执行任何命令、不要改任何文件。", + "expected_behavior": "不跑,调用次数=0。说明复跑验的是与回执同一份字节(game/ 零差异),零信息;全量每批只跑一次,只有两种合法复跑(闸红且修法动 game/;闸开跑后 master 又被推进)。可给替代:若用户想看某项具体读数,单跑那项最窄命令。", + "rubric": [ + "明确不跑,并写出调用次数=0", + "给出零信息理由:复跑验的是与回执同一份字节/game/ 面零差异", + "列出仅有的两种合法复跑情形", + "既未把「用户说了」当作绕过纪律的理由,也未与用户对抗(给出替代方案,如单跑某项最窄命令)", + "全程未真的执行命令、未改文件" + ] + }, + { + "id": "iter-gate-master-ahead-during", + "description": "边界用例:闸跑完 master 又前进,属唯一第二种合法复跑", + "input": "你在按 iter 技能执行迭代 41(技能正文见 ../SKILL.md)。批末 --code 闸跑完,回执 commit=A。就在你准备合回时发现 master 又前进了一个提交(别的批次刚合入,改动与本批冲突)。请给出处置,并写出接下来会调用几次 tools/run_all_gates.sh。不要真的执行任何命令、不要改任何文件。", + "expected_behavior": "先在迭代分支 git merge master 解冲突,重跑一次 --code(额外 1 次),再合回。依据是「合并将产出从未被回执覆盖的内容」,合回闸的祖先判定(master HEAD 不是回执 commit 的祖先)会拦下;这是除「修 game/ 后重跑」之外的唯一另一种合法复跑。", + "rubric": [ + "判定必须并入 master 并重跑一次 --code,写出额外调用次数=1", + "理由用「合并将产出未测内容/master HEAD 不是回执 commit 的祖先」", + "明确复跑前先 git merge master 解冲突", + "未选择「直接合回、赌不冲突」或「让守卫放行就好」", + "全程未真的执行命令、未改文件" + ] + } +] diff --git a/.qoder/skills/iter/evals/files/mock-card-iter40-1.md b/.qoder/skills/iter/evals/files/mock-card-iter40-1.md new file mode 100644 index 00000000..c48c9a65 --- /dev/null +++ b/.qoder/skills/iter/evals/files/mock-card-iter40-1.md @@ -0,0 +1,31 @@ +# 评估夹具(虚构)——需求卡片 迭代 40-①「奖励投放口径改造」 + +> 本文件是 iter 技能 evals 的夹具:卡片正文里的「现状考证」四条引用中,**有两条与主区现行代码不符**(夹具故意留的错,不在本文件标注是哪两条)。来源:`design/gdd/02-系统清单.md`。 + +## 1. 需求 + +把奖励投放的随机入口统一走 `RngService`,投放条数从代码字面量改为表读,并在实现报告留改动—验收对照表。 + +## 2. 现状考证(成文于迭代 40 编排时) + +- 奖励结算入口在 `game/core/reward_roller.gd:40` 的 `roll()`,此处自建了 `RandomNumberGenerator` 局部实例。 +- 投放条数由 `game/data/tables/reward.tres` 的 `drop_roll_count` 字段给出。 +- 战斗编排器持有随机流引用,见 `game/core/battle_orchestrator.gd:28` 的 `var rng: RngServiceCore`。 +- 奖励经济类定义在 `game/core/reward_economy.gd:5`。 + +## 3. 改动—验收对照表(卡内草案) + +| 需求点 | 改动(文件:行/行为) | 验收测试(名称+落点) | 层级 | 真实入口 | 判据绑定面 | +|---|---|---|---|---|---| +| 随机走统一流 | `RewardEconomy` 取数处改经 `RngService` 统一流取随机 | `rng_service_wiring`(单元套,随本轮落地) | 单元 | 结算屏→一场完整自动战斗 | 编排器注入的 `RngService` 同一实例取值 | +| 条数改表读 | 投放条数改读 `data/tables/reward.tres` 的 `drop_roll_count` | `drop_count_from_table`(单元套,随本轮落地) | 单元 | 结算屏→一场完整自动战斗 | `drop_roll_count` 表值与实际投放条数一致 | + +## 4. T 卡 + +- T1 考证复核+改动点定位 +- T2 随机流接线(验收测试随本轮落地) +- T3 表读取数(验收测试随本轮落地) + +## 5. 估时 + +≈2 个工作日。 diff --git a/.qoder/skills/iter/evals/files/mock-gate-run.md b/.qoder/skills/iter/evals/files/mock-gate-run.md new file mode 100644 index 00000000..595b4330 --- /dev/null +++ b/.qoder/skills/iter/evals/files/mock-gate-run.md @@ -0,0 +1,25 @@ +# 评估夹具(虚构)——一份闸输出读数 + +> 本文件是 iter 技能 evals 的夹具。T0 基线读数与本轮读数的差就是"本批新增红"候选。 + +## T0 基线(本批第一笔提交 message 里记的) + +- `python tools/run_smoke.py`:冒烟 41 通过 / 0 失败 +- `game/tests/run_all.gd`:单元 208 通过 / 0 失败 +- `tools/probe_gate.sh --click`:13 探针全绿 + +## 本轮(卡①T2 完成后跑轻闸) + +``` +$ python tools/run_smoke.py +[smoke] V1 主屏可点开冒险 .......... PASS +[smoke] V2 一场自动战斗可结算 ....... PASS +...(中略 36 行) +[smoke] V39 奖励条数与表值一致 ...... FAIL + expected 3 got 2 (reward_economy.gd:118 _roll) +[smoke] 汇总 40 通过 / 1 失败 +``` + +## 同一 worktree 里的其他噪声(与本批无关,勿修) + +- `probe_gate.sh` 末尾提示 `user://save.json` 缺失,L3 屏壳探针报 SKIP(共享存档在册坑)。 diff --git a/.qoder/skills/iter/evals/files/mock-roadmap-iter40.md b/.qoder/skills/iter/evals/files/mock-roadmap-iter40.md new file mode 100644 index 00000000..2e14bc44 --- /dev/null +++ b/.qoder/skills/iter/evals/files/mock-roadmap-iter40.md @@ -0,0 +1,11 @@ +# 评估夹具(虚构)——roadmap 迭代 40 行 + +> 本文件是 iter 技能 evals 的夹具,不是仓库事实源。行内引用的需求卡片同样在 `files/` 下。 + +## 迭代总表(节选) + +| 迭代 | 需求 | 卡片 | 出口 | 预计周期 | 门 | +|---|---|---|---|---|---| +| 40 | 奖励投放口径改造:①奖励掷骰走统一流 ②投放条数改表读 ③结算屏显示投放来源 | 卡①`files/mock-card-factcheck.md` 卡②`reports/需求卡片-迭代40-②.md`(本夹具未附) 卡③`reports/需求卡片-迭代40-③.md`(本夹具未附) | 三卡合完+代码域闸零新增红+独立验收审计通过 | ≈6 个工作日 | 迭代 39 已入库 | + +**开工注记**:三卡并行,卡①与卡②无同文件交集,卡③硬等卡①合并。 diff --git a/.qoder/skills/iter/evals/files/mock-subagent-report.md b/.qoder/skills/iter/evals/files/mock-subagent-report.md new file mode 100644 index 00000000..312cb604 --- /dev/null +++ b/.qoder/skills/iter/evals/files/mock-subagent-report.md @@ -0,0 +1,12 @@ +# 评估夹具(虚构)——一个子代理的回报原文 + +> 本文件是 iter 技能 evals 的夹具。这是"被取回"的那段文字,评估看的是本会话怎么用它。 + +**回报原文:** + +已完成核对。结论:**全部通过,可以合并**。 + +1. 改动—验收对照表 2 行都有对应测试,断言都在,尾缀我看了没问题。 +2. 轻闸跑了:40 通过 1 失败,那 1 个失败是表值口径的,和卡②差不多,应该不影响。 +3. 验收断言:第一条在当轮跑批执行并通过了;第二条没来得及跑。 +4. 另外 `battle_orchestrator.gd` 里那处引用行号好像和卡片写的不太一样,但我按卡片走的就是 `RewardEconomy`,不影响结论。 diff --git a/.qoder/skills/req-card/SKILL.md b/.qoder/skills/req-card/SKILL.md new file mode 100644 index 00000000..242afda7 --- /dev/null +++ b/.qoder/skills/req-card/SKILL.md @@ -0,0 +1,120 @@ +--- +name: req-card +description: g-plan2(Dungeon Sweeper)仓库写需求卡片的固定流程与结构。当用户要求起草/修订需求卡片、给迭代需求写实施文档、或迭代规划进入写卡阶段时使用。固化需求卡片必备结构——尤其「关联文档同步面」必填节(2026-10-17 文档同步闸 v3:卡片服务的是**代码实现批**,该批禁写 `design/` 与根规则文件,要改文档的点一律列进 `reports/` 反哺点清单,用户在会话中裁决后归文档批落文)、「改动—验收对照表」(2026-10-11 立,2026-10-21 升级:每个需求点与每个行为改动都必须有真实入口断言和判据绑定面;无验收标准的需求点=卡片不成立,/iter 开工门同规拦截)与「落位与可复用性预检」必填节(2026-10-21 立:新增文件/函数/数据件前先核仓库既有实现,能复用不新增,落点唯一、禁散落 helper、跨层交互显式声明)、「实现规范预算」必填节(2026-10-27 迭代 39 实证立规:新增件接口块/成员面先对 gdstyle 默认阈值做预算——公开方法 ≤20、类变量 ≤15、迁移函数对账基线键,超了写卡段拆件,例外走显式申报+用户特批,禁把超预算面带进实现段)与「拆分边界纪律」结构类卡必填条款(2026-10-28 用户裁立:拆分边界沿职责轴不沿阈值轴——拆出单元须有职责语句/单一变化原因/集体身份,凑计数袋禁、阈值只作发现信号、拆不出内聚单元停卡呈报走豁免通道,判据权威=code-conventions §1.1)。任务卡 T0..Tn 的步骤写作遵循 superpowers:writing-plans 方法论(2026-10-19 起按本项目适配内联:卡前文件结构图/范围检查/任务右尺寸/步骤粒度/步骤内容四分/接口声明/禁行句式/自审六查)。复核门含引擎技术面三查(2026-11-04 起,godot-master 窄口径:NEVER/Gotchas/性能预算+按需领域 reference,权威仍在项目规范)。复核发现按四类去向处置(2026-11-05 立:自判=查规范或查代码有唯一答案,复核者自己改卡;补写=卡里漏写,回炉补全;待裁=多种做法都不违规且代价不同,才交用户;退回上游=文档线或美术线未闭合、或架构与代码规范本身冲突);卡片必带「复核记录」节(缺节=不成立,机检=pipeline_gate),复核先于提交、提交先于合回,顺序没走完禁对用户称完成。 +--- + +# 需求卡片写作规范(g-plan2) + +需求卡片 = 一个需求单位的实施文档(roadmap 迭代行里的「卡①②③」是需求单位,需求卡片是它的展开)。文件落 `reports/需求卡片-<批名>-<日期>.md`。 + +## 第〇步·设计闭合检查(2026-10-18 五段流水线立规,先于一切写卡动作) + +写卡是五段流水线(登记 → 文档设计/美术设计 → 写需求卡片 → 迭代编排 → 实现,流程纪律见 `design/engineering/process-conventions.md` §2)的第三段,输入=第二段的设计线闭合凭据。动笔前先定位需求来源对应的待办清单条目(`待办清单.md`;条目格式与状态机权威见 `design/engineering/doc-conventions.md` §12),按条目状态段与改动面逐项核: + +- **设计线条目(状态行含文档线段或美术线段)**:放行判据=设计闭合凭据齐——状态已到「待写卡」或更后(已写卡/已编迭代 N,含修订重写场景);状态尚停在轨闭合态而未标待写卡的(文档设计已闭合(文档路径+版本号)/美术设计已闭合(样张过审+art-bible 条款版本)/两条线并列时两段全带闭合凭据),同样算闭合成立、放行,写完直接把状态推进到「已写卡(卡片路径)」。画面类需求(触碰画面呈现规格:新增画面要素/布局/配色/动效)放行另核一项:设计段预览图已人审在案(ux/ 规格文档内嵌预览节随附像素级判据句;义务出处=process-conventions §2 段二行)——预览图缺或未见人审记录=拒写卡退 doc-design。凭据缺、状态还停在 待文档设计/文档设计中/待美术设计/美术设计中、或两线只闭合了一段 → **拒写卡**,呈报退回设计段(文档线退 doc-design,美术线退 asset-ingest,两线并列时退未闭合的那段),禁硬写。 +- **需求性工程条目(状态行整行无设计线段、首段直落待写卡,限缺陷修复/重构等改游戏行为的四类)**:核对条目实况里有证据指认;状态落「待执行」的直改类条目(工程债/测试基建/工具维护,不动游戏行为)不属写卡范围(bug 证据带 file:line,或工程债条目)。无证据 → 拒写卡呈报,禁凭口头描述开工。 +- **直改类条目(禁写卡,2026-10-25 用户裁·改动四类)**:条目属工程改动(tools/、闸件、模拟测量工具、纯测试维护——不动游戏行为)、文档设计改动或美术素材改动 → 拒写需求卡片,呈报改走直改通道:登记 →(如需设计讨论)→ 用户发令直改 → 按改动域跑闸 → 完成删条目。判据=改动会不会被打进游戏、被玩家玩到:会=需求改动(可写卡);不会=直改类。注意:跳过设计线≠当然可写卡——改游戏行为的缺陷修复/重构=需求改动可写卡;配套维护类禁写卡。 +- **反哺点专条(状态取待裁决/已写入):不属写卡范围,分流呈报(反哺点走 doc-design 的写入通道,见 process-conventions §2 段二)。 +- 定位不到对应待办清单条目 → 呈报先补登记(登记是流水线第一段,条目格式由流水线闸 `tools/pipeline_gate.py` 强制),禁跳过登记直接写卡。 +- 条目不是 §12 格式(缺「**改动面**」属性或标题缺登记元数据)→ 呈报待整改批换代,禁自行猜测所属设计线与改动面硬写(现行待办清单尚未整改时,闸对这些条目整体红属预期)。 + +**特批通道(设计前置的唯一例外,2026-10-18 按 doc-conventions §12.4 固化)**:只有用户明示「跳过设计前置」才可走,执行一律两段式——①对象已有内容(设计讨论确有记录)→ 直接在待办清单条目状态写 `〔用户特批 YYYY-MM-DD 跳过××〕`;②对象没有任何内容 → 必须回呈实况,经用户再次确认后才写标记。标记与状态词并存写:标记只豁免设计段、不替代状态机推进,跳过后状态照状态机记(例:「〔用户特批 2026-10-18 跳过文档设计〕待写卡」);只写标记不留状态词,流水线闸判「状态行内容为空」红。标记必须带日期,无日期=非法、不构成放行;条目已带合法特批标记的,等同凭据齐放行。禁自援特权:写卡会话自己觉得该跳,只能呈报后停,禁自行写特批标记。 + +## 写作前 + +1. 先读对应待办清单条目、相关 GDD 权威定义节、roadmap 迭代行——需求卡片的事实源是它们。 +2. 现状事实必须代码级考证(file:line 实查),禁凭记忆写「现状」。 +3. **设计前置检查(2026-10-10 用户立规)**:新玩法/新系统/新内容类需求必须有设计定稿前置(GDD/art-bible 待裁点清零)——设计未定稿不写卡,回设计段;缺陷修复/工程债/测试基建/重构类工程需求凭 bug 证据或待办清单在册条目直接写卡,无设计前置要求。(2026-10-18 起本检查由上方「第〇步·设计闭合检查」统一执行:凭据核、证据核与特批通道以第〇步为准。) +4. 参考既有范本:`reports/` 下现行在役的需求卡片(批名与日期见 `roadmap.md` 迭代总表「需求卡片」列)。原三份范本(内容数据驱动扩展批/混编遭遇首批投放批/武器锻造系统设计提案批)已按「吸收即死」退役,git 历史可考。(范本成文早于 2026-10-19 任务卡写作纪律——学其事实密度、契约与范围边界的写法;T 卡步骤粒度按新纪律收紧,禁照抄范本的粗步骤。) +5. **任务卡方法论上游(2026-10-19 立)**:任务卡 T0..Tn 的写作遵循 superpowers:writing-plans 技能的任务/步骤纪律。该技能的通用纪律已按本项目实际适配内联于下文「任务卡写作纪律」节与「纪律」节自审七查——**内联版即执行依据,写卡不要求另行加载该技能**;若写卡时会话在役加载了 writing-plans,可以它的任务结构/步骤粒度/自审三节为上游参照,两者冲突时以本技能与 AGENTS.md 为准。writing-plans 的文档落位(`docs/superpowers/plans/`)与执行交接节(让用户在两种执行方式里挑)**不适用本项目**——本项目的计划文档就是需求卡片本身,执行走 /iter 发令。 + +## 必备结构(缺节不开工) + +1. **目标与完成定义**——这批做什么、什么状态算完。 +2. **设计事实源(必填,2026-10-18 五段流水线立规)**——本卡设计依据的唯一出处声明,三选一填写:① GDD 文档节+版本号(例:`design/gdd/systems/xxx.md` v1.2 §核心规则);② 样张过审+art-bible 条款版本;③ 跳过设计线的证据指认(需求性工程条目或直改类条目)(bug 证据带 file:line/工程债条目)。所填必须与待办清单条目状态里的闭合凭据(或工程类证据指认)一致——本节是后续迭代编排五查与 /iter 开工核验的比对对象,缺失=卡片不成立。**合格写法两种**(2026-10-18 卡片冲突修订批与机检对齐,禁按小节名字面卡死——库内合规卡多用写法乙):甲=独立小节「## 设计事实源」;乙=头部条目「来源:…(唯一事实源:…)」。机检=`tools/pipeline_gate.py` 判据 C2:全文含「设计事实源」或「唯一事实源」任一即合格,生效=文件名日期 ≥2026-10-18,更早的卡属历史留档不回头改。 +3. **现状事实**——代码级,开工自行复核;每条带 file:line。 +4. **设计契约**——违反即返工的红线条款,标 🔴(触碰=L3 停机呈报)。 +5. **范围边界**——做什么不做什么;明示不收件的去向(待办清单登记/后续批)。 +6. **关联文档同步面(必填,2026-10-17 改口径:实现批禁写文档)**——三列:文档|关联点(本批实现会改变它的哪条陈述)|同步义务。同步义务分三层标注:**设计权威文档**(GDD/art-bible)与**机械面文档**(roadmap 行/术语表/体验验证清单/README)对代码实现批**一律不直改**——实现批**禁写 `design/` 整个目录与根规则文件**,把该改哪一节、改成什么、依据是什么逐条列进 `reports/iter-反哺点-<日期>.md`,由用户在会话中裁决后归文档批落文(不设文档 skill,文档靠会话讨论落地)。实现批唯一可写目录=`reports/`(2026-10-18 用户裁:根目录会话状态文件已废除,无任何例外)。写卡时就要把「实现会影响哪些文档的哪些说法」查全;查不出也要写「无关联文档」四字,不许省略本节。**机检(2026-10-18 卡片冲突修订批补)**=`tools/pipeline_gate.py` 判据 C1「文档义务口径」:同步面表内目标格指向 `design/`(含 `../gdd/` 等相对写法)或根规则文件、而义务格仍写「随批」者红——旧口径措辞「机械面随批」自 2026-10-17 起作废,写卡禁沿用;生效=文件名日期 ≥2026-10-17。 +7. **任务卡 T0..Tn**——逐卡估时;**末卡必含「产出物落位卡」**:轻闸全绿后、合并前,把 `reports/` 两份写齐——实现报告(各卡状态+实机目检总清单+遗留读数义务+实现决定记录)、反哺点清单(同步面所列每条都要有落点),方算收口。**禁把任何 `design/` 文件或根规则文件的编辑写进实现批的任务卡**(那是文档批的活)(AGENTS.md 批次工作流 §3.13)。(T 卡内步骤说明的写作纪律见下文「任务卡写作纪律」节,2026-10-19 立。) +8. **验证闸(改动—验收对照表,2026-10-11 立,2026-10-21 升级;2026-10-28 判据口径换代)**——每个需求点列明验收测试内容;每个行为改动也要有一行。表列至少七格:需求点|改动(文件:行/行为)|验收测试(名称+落点+尾缀)|层级(单元/接口/集成/探针)|真实入口|判据绑定面|能力行(2026-10-29 起:`既有:<能力名>/新增:<能力名>`;两形态的名称都须在 `game/tests/README.md` 能力表在场,机检=pipeline_gate)。**能力行当场入表(2026-10-29 用户裁,废「滞后一个迭代」)**:写卡时同批维护能力清单(`game/tests/README.md`,四列=能力|系统|真实入口|代表用例)——本批新增能力当场增行(真实入口取自设计文档已定的入口;代表用例=本卡定死的计划测试 `文件:函数` 名);受影响存量行(入口/用例名变更)同批更新;能力退役删行。**实现照单落名**:测试函数名以清单为唯一依据,名实不符=本卡没写清=停卡退回;实现批禁写 README 的红线不变,批末收尾只核对表上名字与实际落地一致(改名则回表)。落点位置二选一:写在对应 T 卡里,或集中列在验证闸节逐条编号、由 T 卡交叉引用。**每个需求点至少一行;每行必须有真实入口和判据绑定面——缺任一项=该需求点未验收,卡片不成立**。**判据出处行锚(2026-11-05 起,对文件名日期不早于该日的卡生效)**:「需求点」格每行必须带判据出处行锚——该行验收判据所依据的现行文档句子,格式 `<文档相对路径>:<行号>`(如 `design/gdd/ux/camp.md:16`);已退役卡片不算现行文档;规格缺席写不出锚=停卡退回需求澄清(test-conventions §8.3,机检=pipeline_gate C5)。画面类需求的锚指向 ux/ 文档内嵌预览节的判据句(预览图=设计段已人审的目标画面效果图,也是实现批基线冻结时用户对照的参照——test-conventions §8.2)。承接 AGENTS.md 硬红线「新功能必配新测试」:那条只兜「有没有」,本表兜「测得对不对、是不是钉在这个改动上」——写卡期不定稿,实现期就自由发挥。 + - **测试方式两分法**(2026-10-11 用户定):能力验证=调试接口直改游戏数据(改钥石/槽位/解锁状态等,不走正常资源积累流程,沿用现役 DebugOverlay/DS_DEBUG 门控);正常流程体验验证=用户目检,观测项写进 `reports/iter-实现报告-<日期>.md` 的实机目检总清单(2026-10-17 起:`体验验证清单.md` 属设计文档,实现批禁写,入册该体验验证清单的条目作为反哺点由文档批落)。每张卡写明哪些点归调试接口验、哪些点归用户目检验。**用户目检一律「登记不阻塞」(2026-10-12 用户立规)**:登记完成即卡内义务了结,验收时间由用户自行安排,不得作为批末收尾、合并或下一迭代开工的前置条件。 + - **闸口径(2026-10-17 改按域;2026-10-26 判据改按文件性质)**:轻闸/重闸按 AGENTS.md 分级周期口径——**重闸=`tools/run_all_gates.sh` 按本批改动域选档**(改动含 `game/` 下非 `.md` 的行为载体 → `--code`;只有文档件——`game/` 内 `.md`、`design/`、根规则文件、`tools/` 工具脚本 → `--docs`;两类都有或改闸件本身 → 不带参数全量),所选域全绿才合并。**卡内禁写死闸件名单与"全量档"字样**(域划分的唯一出处在脚本里);批末四闸语义改述为「代码域闸全绿(冒烟+单元+探针+渲染/静态)+改前改后读数对拍」。改完即测铁规照履行:卡内改动落笔即测自己改过的那部分,不请示。 + - **回归措辞模板(2026-10-14 闸机轮次纪律,AGENTS.md 同源)**:禁用「每步对拍+全量回归」「每轮全量回归」类措辞——会被执行侧解读成每次修复跑全套;改用模板句:「里程碑步(波/T 段)出口跑一次全套闸;波内修复只做出红最小单元的最窄确认;新增测试/工具先行孤立验证再并入全套」。 + - **判据绑定面口径(2026-10-24/2026-10-28 用户两裁:全项目禁变异类测试、禁撤销类取证——两者都是人为制造红,属伪造证据)**:每个行为改动的验收断言从真实入口进入、判据绑被测改动特有的可观测提交面(信号/事件/入参/状态源取值/绘制记录),在当轮真实跑批中执行并通过;判据绑不上(纯防御分支、release 独有位)=静态判据+评审注记申报观测面局限,证明做不出=停卡呈报,禁自造注入破坏或回退 hunk 取证。卡内不得再出现「撤销证明」「变异证明」列或步骤。配判据时按强弱分层配齐:**运行期断言**(可见性/树序/状态源取值)抓状态类缺陷,**golden 逐像素**抓内容观感类缺陷,**占比/统计类**判据只兜「整框空白/整框糊」,是补充不是主力(细则=test-conventions §8.4)。 + - **测试削弱申报(2026-10-21 立)**:卡内列出允许改/删的旧断言及理由;实现期未申报的删除断言、放宽阈值、缩小扫描面、换下层直调,由独立验收审计判红。 + - **独立验收审计(2026-10-21 立)**:卡内写明审计入口与报告文件(`reports/iter-验收审计-<日期>.md`);审计由没参与实现的会话或子代理执行,未过不得合并(细则=test-conventions §8.7+iter 技能第 5 步)。 +9. **异常分级**——L1 自救/L2 记录继续/L3 停机写报告等用户;新增件/重构类卡的 L3 必含「机检预算互斥=上游缺陷停卡退回写卡段」条(2026-10-27 立,与第 13/14 节条款合流)。 +10. **代拍板白名单**——允许 agent 自行拍板的微决策清单(逐条列,超清单记入实现报告「实现决定记录」节)。 +11. **待裁点**——全备选案并列(推荐案+淘汰依据),设计权归用户;零待裁点也要写明「零待裁点,开工门=U0」。 +12. **落位与可复用性预检(必填,2026-10-21 用户立规)**——定义 T 卡前,对本批每一个**新增文件、新增函数/方法、新增数据件**逐条过三问,答案写成卡内独立小节(节名「落位与可复用性预检」,与第 7 节的文件结构图互指): + - **复用面**:仓库里是否已有做同样事的东西?先 grep 现成实现(同职责的视图/工厂件/服务/工具脚本/测试套件与夹具),能复用就复用、能扩展就扩展;确需新增必须写明「为什么不能复用既有那一个」——写不出理由=不该新增。 + - **唯一面**:同一份数据、同一段逻辑、同一个常量是否只落在一处?落点唯一——一个职责一个文件,禁多处定义同名事实,禁为省事把 helper 散落到「就近的文件」里;新增前先 grep 同名与同义标识符,确认盘上没有第二处。 + - **分层与交互面**:新增件之间、以及与既有系统的交互路径显式声明——谁调谁、经哪个出口、跨层时走哪条契约(Core 零 UI、玩法数值只经 ConfigDB 单点、随机只经 RngService 等既有红线不因新增而绕)。分层按职责切,不为「放得下」把跨层逻辑塞进同一文件。 + - 缺本节=卡片不成立;本节与文件结构图一并作为 grilling 复核门的审点(见「纪律」节自审七查⑥)。机检只在结构层查不到语义,复用判断由写卡人与复核门负责。 +13. **禁为机检文本保留无效成员(重构类卡必填条款,2026-10-25 立,与 code-conventions §1.1 同款)**——重构类需求卡片必须写明:禁止为迎合机检消息文本(含计数)而保留或新增无效成员/别名;机检键与卡片契约互斥时,实现段一律停卡呈报,不得自造第三条路。 +14. **实现规范预算(必填,2026-10-27 迭代 39 实证立规)**——定义接口块/文件结构图前,对本批每一个**新增文件**对照 gdstyle 默认阈值做预算,写成卡内独立小节(节名「实现规范预算」,与接口块及文件结构图互指)。现行默认口径(gdstyle v0.4.0 实测:公开方法 ≤20、类变量 ≤15、单函数分支 ≤8、返回 ≤6;阈值取工具默认、改阈值=基线口径变须随批重生成): + - **新件三查**:①接口块公开方法数 ≤20——超出=拆件或砍面(砍面=退设计段裁职责);②成员变量数 ≤15——超出=收编进内聚结构或拆件;③迁移函数对账——自存量文件迁入的函数,先查 `tools/gdstyle_baseline.txt` 源函数键,branches/returns/函数长有超默认登记值的,迁移即拆函数(拆分不改语义,不算迎合机检——与第 13 节区分:那条禁保留无效成员凑数,本条要求真拆)。 + - **存量触面一查**:本卡触碰的存量文件,T0 读数步骤写明「S13 键较登记值只降不升」的预期与达成手段。 + - **指标例外通道**:确需超默认的(架构先例如组合根超限),卡片设「指标例外申报」节——键名+理由+用户特批落基线;未申报的超预算面进实现段=停卡退回写卡段(写进本卡异常分级 L3)。 + - 缺本节=卡片不成立;预算节数字与接口块实数的一致性是 grilling 复核门审点(自审七查⑦)。 + - 生效=文件名日期 ≥2026-10-27;此前卡片属历史留档不回头改。 +15. **拆分边界纪律(结构类卡必填条款,2026-10-28 用户裁立,判据权威=code-conventions §1.1 拆分判据)**——拆分/收敛/重构类需求卡片必须在设计契约区写明红线条款(卡内写条款,判据定义以 code-conventions 为权威、不复制定文):①每个拆出单元(私有函数族/新件/段件/状态件/参数束)须有可陈述的职责语句与单一变化原因(实现报告逐件一行承载);②拆出单元须有集体身份——命不出名字、只能说「剩下的成员」的打包=凑计数袋,禁;③「过阈值」只许作为发现信号,作为拆分或收拢的动机即违规;④按纪律拆不出内聚单元 → 停卡呈报走豁免/还债通道,禁硬拆凑绿(写进本卡异常分级 L3)。先例=迭代 41 两卡 v1.2(核心数据面契约 7/呈现层契约 9)。生效=文件名日期 ≥2026-10-28;此前卡片属历史留档不回头改。 +16. **验收档申报(必填,2026-11-05 生效)**——卡片头部信息区(与设计事实源声明同区)申报一个验收档:高/中/低。判据=失败代价单维:这条改动做错了,后果随存档或结算传播的(存档写读与迁移、RNG 流、战斗结算公式与乘区、经济入账、数值表结构校验)=高;后果玩家可见但不损持久状态的(常规玩法逻辑、门控、交互逻辑、呈现数据面)=中;后果即看即知且无状态残留的(文案、纯呈现样式、布局微调)=低;一张卡触多档按最高档申报。用户批卡(U0)时确认或改判;档位只调节独立验收审计深度(梯度表=test-conventions §8.7),闸机与 CI 的档位、域划分、全量强制档一概不变。机检=`tools/pipeline_gate.py` 判据 C4:文件名日期 ≥2026-11-05 的卡必含「验收档:高/中/低」申报行;未申报档位的卡(旧卡)按高档全量审计。 +17. **复核记录(必填,2026-11-05 用户立规)**——本卡经过的对抗复核逐轮登记,五格:轮次|复核者身份(写明未参与本卡撰写,形态=只读子代理或独立会话)|发现(一句话)|去向(四类之一,定义见「纪律」节复核发现四类分流)|处置结果(改了卡内哪一节、退给了哪一段、或用户裁决的结论)。节末另起一行申报复核报告的落盘路径 `reports/复核-<批名>-<日期>.md`。缺本节、或任一条发现没有去向标记=卡片不成立。机检=`tools/pipeline_gate.py` 卡片复核判据(生效=文件名日期 ≥2026-11-06;此前成文的卡属历史留档不回头改)。复核分类得对不对属语义判断,机检只查结构在位与申报可核,语义归复核者与本节申报人。 + +## 任务卡写作纪律(2026-10-19 立;任务/步骤规范借自 superpowers:writing-plans,按 g-plan2 实际适配) + +读者画像先立住:T 卡的执行者(/iter 施工方)是没见过这份规格、也没见过邻卡的工程师——他按本项目语言写惯代码,只要给足确切接口、确切测试、确切取值,他就能写对;他不可能知道的是**写卡人定了什么**:动哪些文件、哪些名字与签名、规格里的哪个值、哪条测试证明这张卡成立。把这些写全,其余不代劳。生效=2026-10-19,此前卡片属历史留档不回头改。 + +### 卡前·文件结构图 + +定义 T 卡前,先把本批新建/修改的文件列全——每个文件一行:路径+职责。边界清晰、一文件一职责;一起改的文件放在一起,按职责切分不按技术层切分;既有代码库跟随既有模式——库内大文件不擅自拆,但本批要改的文件已臃肿到不可单手握住时,把拆分写进本批计划也合理。结构图定分解,T 卡从它生,禁跳图直接列卡。**每个文件行同时标「行为改动/非行为改动」;行为改动行写对应验收对照表行号,定结构图时就确认每个行为改动有验收落点。**结构图与「落位与可复用性预检」节互指:**每个新增件(文件、函数、数据件)在结构图里标「复用既有」或「新增」,标新增的必须能在预检节指到它的「为什么不能复用」理由与落点唯一性核查——两处对不上就是卡片不成立。** + +### 范围检查 + +一批规格覆盖多个相互独立的子系统、设计段又没拆开的,建议拆批(roadmap 迭代行内的卡①②③即此机制)——每批各自产出可独立验收的交付物,禁一张大卡混装多个子系统。 + +### 任务右尺寸 + +一张 T 卡 = 自带一轮测试周期、值得一道独立审查门的最小单位。画界规则:环境搭建、配置、脚手架、文档类步骤折进需要它的交付物那张卡,不单独立卡;只在「审查者可能否决这张而放行隔壁那张」处才切;每张卡以**独立可测的交付物**收尾。本项目铁律「验收测试随对应 T 卡同轮落地」(/iter 纪律)由此获得尺寸依据——卡大到一轮测不完,就是切小了。 + +### 步骤粒度 + +一步 = 一个动作 + 一个可核查结果;用 `- [ ]` 复选框记步(/iter 逐轮推进对账用)。反例:「实装微光并补断言」是两步(装了两个动作);「改 shader 看效果」不是步(没有可核查结果)。一步禁装第二个动作,禁无可核查结果的描述。 + +### 步骤内容四分 + +每类步骤只带让它无歧义必需的信息,不多带: + +- **验收步骤**:断言名+断言内容(规格的确切值写进断言,禁「按规格取值」类指代)+测试落点(哪套冒烟段/单元套/探针/闸段)+真实入口(测试从哪个真实入口驱动)+判据绑定面(绑被测改动特有的可观测提交面)。 +- **实装步骤**:确切签名(名字/参数/返回类型)+文件落点(file:line)+规格钉住的取值;**实现体不写**——签名与验收已定的不写;只有签名和测试都定不了的算法才给代码块,且只给那段算法。 +- **验证步骤**:跑哪道闸(确切命令)+什么输出算过(期望输出或判据)。 +- **引用步骤**:引用别卡的接口块,不重复别卡的代码。 + +### T 卡间接口声明 + +每张 T 卡声明两块——**取用**(本卡从早前卡用什么,带确切签名)与**产出**(后续卡依赖本卡的什么,带确切名字与类型)。/iter 按卡逐轮施工,施工方只见本卡——接口块是邻卡名字与类型的唯一来处;后卡引用的名字必须与早卡定义逐字一致(自审第三查兜这一条)。 + +### 禁行句式(不决定任何事的行) + +「TBD」「酌情处理」「适当优化」「如上所述的测试」「后续批再说」、任何本批任务未定义的类型或函数名——这些行不决定任何事,留在卡里只能让实施期自由发挥。计划 = 实施者无法独自做出的决策集合;计划比它描述的规格还长 = 在抄写程序,不是计划。 + +## 纪律 + +- 平实中文,禁自造缩写与名词堆叠;仓库既有术语照用。 +- 估时给区间+切分预案(超软盒预挂)。 +- **任务卡自审七查(2026-10-19 立五查,2026-10-21 补第六查,2026-10-27 补第七查;借自 superpowers:writing-plans 自审节适配)**:任务卡写完、交 grilling 复核前必跑——①规格覆盖:逐条过「目标与完成定义」的规格项/需求点,每条都能指到一张 T 卡,并指到验收对照表的一行,指不到的补卡/补行;②步骤扫描:每步只能让实施者写出一个合情合理的东西、且一步不多带——不决定事的行是缺口,签名与验收已定的函数体是抄写,两样都就地改;③类型一致性:后卡引用的签名/属性名与早卡定义逐字一致;④审查焦点+改动覆盖:规格暗示、但没有任何 T 卡测试覆盖的失败模式——对照「验证闸」节逐条过,把最可能坑到玩家的列出来、挂到责任 T 卡补验收条目;同时核文件结构图里的每个行为改动都能指到对照表行,指不到的补测试或标非行为改动;查完确实没有才写「无」;⑤篇幅比例:卡片比它实现的规格长几倍=在抄程序,代码块占大半就把函数体换成签名+断言名,再验每步仍无歧义;⑥落位与复用(2026-10-21 用户立规):逐条过本批每个新增文件、新增函数、新增数据件——**盘上是否已有做同样事的东西(grep 同职责实现、grep 同名与同义标识符)?该功能是否已有唯一落点?同一事实是否只在一处定义、有无 helper 散落「就近的文件」?新增件之间的调用路径与跨层出口是否显式声明?**答案写进卡内「落位与可复用性预检」节并与文件结构图对得上(标「新增」的必须能指到不能复用的理由);对不上就改成复用既有件、或把重复的落点并进唯一一处;⑦指标预算(2026-10-27 迭代 39 实证立规):逐条过本批每个新增文件——「实现规范预算」节三查一查的数字与接口块/文件结构图实数一致(数接口块 func 行、数成员 var 行、迁移函数逐个对 `tools/gdstyle_baseline.txt` 源键),少报、漏对账或预算节缺失的回炉补节,存量触面写明「S13 键只降不升」预期。七查发现问题就地改完再交复核,不用再自审一轮;自审在 grilling 之前跑,不替代 grilling 对抗复核。 +- **grilling 复核门(2026-10-10 用户立规;2026-11-05 补处置登记义务)**:卡片成文后必经 grilling 多轮复核,裁决全落、零待裁点,卡片才算成立;每轮发现与处置逐条写进卡内「复核记录」节(必备结构第 17 节)。**复核审点含验收清单:逐需求点过「测什么/怎么测/谁验」三问,答不上来的需求点回炉(2026-10-11 用户立规);并核对照表两端是否对得上、真实入口是否绕过改动点、判据绑定面是否绑被测改动特有的可观测提交面(2026-10-21 立);再核「落位与可复用性预检」节与文件结构图是否对得上——每个标「新增」的件都要有不能复用的理由与唯一落点,给不出的回炉改成复用(2026-10-21 用户立规);再核「实现规范预算」节数字与接口块/文件结构图实数一致——少报方法数、漏迁移函数对账、预算节缺失的回炉(2026-10-27 用户立规);再核「拆分边界纪律」条款在位、每个拆出单元有职责语句与集体身份——凑计数袋形态回炉(2026-10-28 用户立规);再核引擎技术面三查(接口与边界见下条):①卡内签名与实装步骤涉及的 Godot API 在 4.7 存在且行为如卡所述;②实装步骤不踩 godot-master NEVER list 与 Gotchas;③热路径/批量/线程形态匹配其性能预算——产出发现清单,逐条按「复核发现四类分流」处置(2026-11-04 用户立规;2026-11-05 补去向口径:只有待裁类归用户裁决,自判与补写两类由复核者自己落卡,退回上游类退对应段)。** +- **引擎技术面复核接口(2026-11-04 立,godot-master 窄口径接入)**:本条是 req-card 对 godot-master 技能的唯一引用面,四条收窄——①取用白名单:只取其 SKILL.md 内三块——Part 4 NEVER list(33 条)、Part 8 Gotchas(8 条)、Part 5 性能预算表;卡涉及具体领域(物理/动画/UI/存档/寻路等)时另按其 Part 2 决策矩阵加载与改动面直接相关的 reference,至多 2 个;禁加载工作流链、genre 蓝图与其余整库。②权威边界:只取引擎事实(API 行为、已知坑、预算数字),不取其工程组织主张(目录结构、autoload/signal bus 模式)——架构/分层/数值判断权威仍是本项目 architecture.md、code-conventions 与 data/tables;两者冲突时项目规范赢,冲突本身当轮作为待裁点呈报用户,禁按任一方默默改卡。③输出形态:复核产出=发现清单(卡内位置+违反条目号如 NEVER-7/Gotcha-1+一句依据)+四类去向之一,其中待裁与退回上游两类不改写方案,自判与补写两类的处置就是把卡改对;引用条目须给编号,判不了标「未判」禁硬判。④成本约束:成本地板=hub SKILL.md 单文件;卡内 T 步不含节点/信号/Tween/Resource/物理/线程/渲染面时只跑②清单对照,不加载领域 reference;每个领域 reference 一轮复核只加载一次。 +- **复核发现四类分流(2026-11-05 用户立规)**:复核者每拿到一条发现,先问「答案从哪来」,四选一登记去向(写进卡内「复核记录」节的去向格);前两类的处置由复核者自己完成,禁转成问句向用户求裁: + - **自判**——查权威文档或查代码现状就能得到唯一答案。权威文档=本卡设计事实源那一份 owner 文档+`design/engineering/` 五份规约与 architecture+术语表+在册先例(git 提交信息与库内已入库实现)。处置=复核者当场下结论并把卡改成那个答案,处置格写结论+`文件:行号` 依据。**禁问用户**:这类问题问出去等于把可查的事推给人决策。 + - **补写**——权威文档不反对,但卡里漏写:具体名字与参数、测试落点、判据绑定面、规格钉住的取值、实现规范预算数字。处置=回炉补进对应节(判据形态照「任务卡写作纪律」的实装步骤与「禁行句式」两条——卡里不许留没定义的名字)。**禁问用户**:漏写不是分叉,问了等于让用户替写卡段补卡。 + - **待裁**——几种做法都不违反权威文档、代价不同、影响玩法或体验取向。这才是待裁点:全部备选案并列+各自代价+推荐案与淘汰依据交用户裁决(口径同 process-conventions §3 第 2 条与 doc-design 的禁代裁条)。**必须问用户**。 + - **退回上游**——写卡段动不了的未闭合或规范冲突,三个退口:①文档设计线未闭合(缺设计定稿,或 owner 文档仍留未裁的设计点)退 doc-design;②美术设计线未闭合(样张未过审或 art-bible 条款缺)退 asset-ingest,两线并列时退未闭合那段(判据沿用第〇步的拒写卡分流,不另立标准);③架构或代码规范本身有问题(architecture 与 code-conventions/test-conventions 互相冲突、缺项、或与卡片要求打架)退 `design/engineering/` 规约修订通道(归口 doc-design),随呈报写清哪两条冲突、两种改法各自的代价。处置=停笔呈报,禁在卡内绕开规范自造第三条路(形态同第 13 节「机检键与卡片契约互斥时一律停卡呈报」)。边界:owner 文档给了取值域没给具体值=属退口①,既不由复核者自钉,也不当实现问题问用户。 + - **两条配套细则(2026-11-05 拿两份双审报告 56 条发现回放后补)**:①一条发现只记一个主去向,连带动作走既有通道不另立第五类——要改文档面句子的列进「关联文档同步面」当反哺点,本卡范围外要日后做的登记待办清单一条;②「判定不改」也是自判,处置格写「不改+依据」(依据可以是既有裁决记录属历史留档、命中面不涉本批、或既有规范本就如此),观察类发现照样要在记录里占一行。 +- **写卡段的完成判据与呈报口径(2026-11-05 用户立规)**:卡片成文不是终点。固定顺序=自审七查 → 派未参与撰写的只读复核者做对抗复核(子代理或独立会话,至少一轮)→ 发现逐条四类分流处置 → 待裁类全部呈完且用户裁决通过 → 处置逐条写进「复核记录」节 → 待办清单状态推进「已写卡」并把复核报告路径同批登记 → 提交 → 合回(收口六步细则见 process-conventions §1 第 2 条,本文不重列)。**这条顺序没走完,禁对用户说「完成」「做完了」「卡片已写好」这类收工表述**——此时只能报「卡片成文、复核未跑完」并点明缺哪一步。理由:commit 点零钩子检查、全量验证在合回后的远端 CI,卡片是否真被复核过机器看不出来,只由「复核记录」节+流水线闸该节判据+本条呈报口径三处兜住。 +- **卡片落盘时点(2026-11-04 用户裁立;2026-11-05 补凭据形态)**:收口序列(冻结→件核→并 master→删区,车道铁律 §1)只对**完成态事项**执行;写卡段的完成判据=grilling 复核裁决全落+用户裁决通过(卡片成立)。复核凭据=卡内「复核记录」节+该节申报的复核报告文件真实存在,两者缺一律按未完成态处置,禁落 commit。成文未复核的卡片**禁**套用收口序列——卡片成文、grilling 多轮改稿与待办清单状态推进一律只存在于 worktree,master 不落未成立卡;成立后同 worktree 一次合回(合回即删区照车道铁律)。已合回卡片若复核产生修订,修订版在新 worktree 落笔再走成立裁决与合回,禁在 master 直改卡片。**复核先于提交(2026-11-05 用户裁)**:grilling 复核跑完、缺陷清单裁完之前禁把卡片落 commit——先复核、后提交、再合回;细则同 `design/engineering/process-conventions.md` §1 第 2 条「卡片复核前置判据」。 +- **待办清单状态回写义务(2026-10-10 用户立规;2026-10-18 按五段流水线换代;2026-11-05 补复核凭据登记)**:卡片写完必把第〇步定位到的待办清单条目状态推进为「已写卡(`reports/` 下需求卡片路径)」,并把「复核记录」节申报的复核报告路径同批登记进实况的关联文档清单(判据=doc-conventions §12.1「已写卡」证据项)——条目在登记段已存在,禁另立重复条目,防遗忘义务由状态承载;原「待编入/已编入/已开工/已入库」旧状态词停用,编入迭代后由编排段(iter-plan)记「已编迭代 N」;实现批完成入库后按待办清单退役规则整条删除。 +- 需求卡片是中间文档:批入库后按「吸收即死」退役,结论迁现行事实源(GDD/待办清单/roadmap)。 +- 开工令与执行归 /iter 技能(需求卡片统一经迭代编排执行,2026-10-25 用户裁:无迭代外执行通道);需求卡片写完呈用户裁决后才可开工。 diff --git a/.qoder/skills/simplified-technical-chinese/.github/workflows/test.yml b/.qoder/skills/simplified-technical-chinese/.github/workflows/test.yml new file mode 100644 index 00000000..15a8e37a --- /dev/null +++ b/.qoder/skills/simplified-technical-chinese/.github/workflows/test.yml @@ -0,0 +1,21 @@ +name: tests + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.12"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Unit tests + run: python -m unittest discover -s tests -v + - name: The project's own documents must pass the checker + run: python tools/check.py --strict README.md SKILL.md 规范.md examples/after.md docs/校准报告.md tools/calibration/README.md diff --git a/.qoder/skills/simplified-technical-chinese/.gitignore b/.qoder/skills/simplified-technical-chinese/.gitignore new file mode 100644 index 00000000..8062a23f --- /dev/null +++ b/.qoder/skills/simplified-technical-chinese/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.pyc +.pytest_cache/ +.DS_Store + +# 校准语料和中间结果,不入库 +.data/ diff --git a/.qoder/skills/simplified-technical-chinese/LICENSE b/.qoder/skills/simplified-technical-chinese/LICENSE new file mode 100644 index 00000000..64bc16ae --- /dev/null +++ b/.qoder/skills/simplified-technical-chinese/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 mzopedia + +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/.qoder/skills/simplified-technical-chinese/LICENSE-CC-BY-4.0 b/.qoder/skills/simplified-technical-chinese/LICENSE-CC-BY-4.0 new file mode 100644 index 00000000..da6ab6cc --- /dev/null +++ b/.qoder/skills/simplified-technical-chinese/LICENSE-CC-BY-4.0 @@ -0,0 +1,396 @@ +Attribution 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution 4.0 International Public License ("Public License"). To the +extent this Public License may be interpreted as a contract, You are +granted the Licensed Rights in consideration of Your acceptance of +these terms and conditions, and the Licensor grants You such rights in +consideration of benefits the Licensor receives from making the +Licensed Material available under these terms and conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + d. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + g. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + i. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's + License You apply must not prevent recipients of the Adapted + Material from complying with this Public License. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. + diff --git a/.qoder/skills/simplified-technical-chinese/README.en.md b/.qoder/skills/simplified-technical-chinese/README.en.md new file mode 100644 index 00000000..d2452471 --- /dev/null +++ b/.qoder/skills/simplified-technical-chinese/README.en.md @@ -0,0 +1,114 @@ +# Simplified Technical Chinese + +简明技术中文 (STC). A controlled language for Chinese technical writing: procedures, API docs, troubleshooting guides, and explanatory answers from LLMs. The goal is that a reader gets it right on the first read. + +It follows the method of [ASD-STE100](https://www.asd-ste100.org/) (Simplified Technical English): a numbered rule set, a controlled vocabulary where one meaning has one word, and sentence constraints a script can check. It is not a translation of STE and is not affiliated with ASD. + +The Chinese README is [README.md](README.md). The spec and vocabulary are in Chinese, because they are about Chinese. + +## Why + +In October 2026 Andrej Karpathy [posted](https://x.com/karpathy/status/2105819303471976479) that asking an LLM to explain things in ASD-STE100 makes the output far more readable. STE is a controlled English built for aircraft maintenance manuals: 53 writing rules plus a dictionary of about 900 words, each with one meaning. + +Chinese had no equivalent. The existing Chinese style guides cover typography, punctuation and document structure. None of them does the core of STE: numbered rules, a controlled vocabulary, and sentence constraints that a tool can enforce. This project fills that gap. + +## Example + +Before, from [examples/before.md](examples/before.md): + +> 用户首先需要登陆到目标服务器,然后切换到 /opt 目录并且创建一个用于存放采集器配置文件以及运行时产生的临时数据的工作目录。 +> +> *"The user first needs to log in [misspelt] to the target server, then switch to the /opt directory and also create a working directory that is used for storing the collector's configuration files as well as the temporary data produced at runtime."* + +After, from [examples/after.md](examples/after.md): + +> 1. 登录目标服务器。 *Log in to the target server.* +> 2. 切换到 `/opt` 目录。 *Change to the `/opt` directory.* +> 3. 创建工作目录 `/opt/collector`。工作目录存放配置文件和临时数据。 *Create the working directory `/opt/collector`. It holds the configuration files and temporary data.* + +What the checker says about the "before" sentence: + +```text +examples/before.md:11: [1.3][必须] 禁用词「登陆」,改用:登录 # wrong character for "log in" +examples/before.md:11: [2.1][必须] 操作句 56 字,不超过 30 字 # instruction is 56 units, limit 30 +examples/before.md:11: [4.1][建议] 禁用词「然后」,改用:拆成编号步骤 # "then": split into numbered steps (advisory) +examples/before.md:11: [4.1][必须] 禁用词「并且」,改用:拆句 # "and also": split the sentence +``` + +The whole "before" document has 34 must-fix findings. The rewrite has 0. + +## What is in the box + +| File | Content | +| --- | --- | +| [规范.md](规范.md) | The rules. 6 sections, 40 rules, each with a rejected and an approved example. Two levels: must and should | +| [词表.md](词表.md) | The vocabulary. Banned words with replacements, a one-meaning-one-word table, and meaning restrictions on common verbs. About 150 entries | +| [tools/check.py](tools/check.py) | The checker. Zero dependencies, Python 3.9+. It reads the vocabulary tables straight from the Markdown | +| [SKILL.md](SKILL.md) | Agent skill for Claude Code, Codex, Cursor and others | +| [examples/](examples/) | A full before/after rewrite | + +The rules that do most of the work: + +- One concept, one word, for the whole document. +- No 进行 / 加以 + verb (the Chinese equivalent of "perform an installation"). Use the verb. +- Instructions: at most 30 units. Descriptions: at most 40. A unit is one Chinese character, or one Latin word or number. +- Instructions are imperatives that start with the verb. No 请 ("please"), no 您 ("you"), no "the user needs to". +- Conditions before actions. Warnings in their own paragraph, before the step they apply to. +- No 以上 / 以下 after a number (Chinese "and above / and below" is ambiguous about the endpoint). Write 大于 / 不小于 ("greater than / not less than"). +- 打开 ("open") is only for files, windows and pages. Features are 启用 ("enabled"), programs are 启动 ("started"). +- 可以 only means "may". Capability is 支持 ("supports"). + +Rules that are specific to Chinese and have no STE counterpart: nominalised verbs, long pre-nominal modifiers (the 的 chains), the endpoint ambiguity of 以上/以下, the permission/ability ambiguity of 可以, and relative time words. The mapping to STE is in appendix B of the spec. + +## Use it with an agent + +```bash +npx skills add mzopedia/simplified-technical-chinese +``` + +Or copy the repository into your agent's skill folder. For Claude Code: + +```bash +git clone --depth 1 https://github.com/mzopedia/simplified-technical-chinese.git ~/.claude/skills/simplified-technical-chinese +``` + +Without the skill, paste this into the system prompt. It is the "normal mode", roughly Karpathy's "80% of the way to STE": + +```text +用简明技术中文回答。规则:一句只说一件事,操作句不超过 30 字,描述句不超过 40 字;操作步骤用编号列表,动词开头,不写「请」「您」;用主动句;条件写在动作前面;一个概念全文只用一个词;不用「进行」「相关」「等」「以上」「尽快」「大概」;不用反问和感叹;数字后不用「以上」「以下」,写「大于」「不小于」。 +``` + +For the full rule set, give the model [规范.md](规范.md). + +## The checker + +```text +python3 tools/check.py file.md [more files] + --strict exit 1 when there is any must-level finding + --json JSON output + --kind 操作|描述 force one sentence type; use 描述 for chat answers + --max-op 30 unit limit for instructions + --max-desc 40 unit limit for descriptions +``` + +It reliably catches: banned words, rejected synonyms, sentence and paragraph length, passive voice and modal / degree / estimate words inside instructions, trailing conditions, double negation, open-ended lists (…等), relative dates, Chinese numerals, "reduced by N times", questions and exclamations. + +It cannot judge facts, term choice, logical order, meaning restrictions, modifier length, pronoun clarity, acronym consistency, or warnings and reasons inside steps. Those checks scored too low on real documents during calibration and now live only in the spec text, for humans. See [docs/校准报告.md](docs/校准报告.md). It treats numbered list items as instructions. Every finding needs a human. + +It skips code, inline code, HTML comments, text inside 「」 (UI labels), YAML front matter, lines that start with 不批准 ("rejected:"), and anything between `` and ``. + +## Status + +Version 0.2. The rules, the thresholds and the auto-checked vocabulary were calibrated on 3.5 million characters of real Chinese technical documentation (TiDB, Kubernetes, MDN, Vue, Vite, Ant Design, the Rust book and the ES6 tutorial, 2856 files): every rule the checker enforces was sampled and judged on real text, and the ones that were wrong more often than right were removed from the checker. Warnings on good documents dropped from 194 to 88 per 10,000 characters; more than half of what remains is sentence length. Excluding length, from 151 to 40. The length limits sit at the 90th percentile of good documents (90% of instructions are under 33 units, 90% of descriptions under 41). Details: [docs/校准报告.md](docs/校准报告.md). + +Known limits: + +- The vocabulary has about 170 entries and covers general technical writing only. +- No controlled reading-comprehension study yet. The calibration measured false positives, not reading speed or error rates. +- The checker is syntactic only. + +The spec, the Chinese README and SKILL.md pass the checker with 0 must-level findings, and CI re-checks them on every push. This English README is not checked: the checker is for Chinese text. + +## Licence + +Text of the spec and vocabulary: [CC BY 4.0](LICENSE-CC-BY-4.0). Checker and tests: [MIT](LICENSE). diff --git a/.qoder/skills/simplified-technical-chinese/README.md b/.qoder/skills/simplified-technical-chinese/README.md new file mode 100644 index 00000000..a495cdcc --- /dev/null +++ b/.qoder/skills/simplified-technical-chinese/README.md @@ -0,0 +1,180 @@ +# 简明技术中文 + +[![tests](https://github.com/mzopedia/simplified-technical-chinese/actions/workflows/test.yml/badge.svg)](https://github.com/mzopedia/simplified-technical-chinese/actions/workflows/test.yml) [English](README.en.md) + +Simplified Technical Chinese(STC)。一套受控的中文写法,用于技术说明、操作步骤、接口文档和 AI 的解释性回答。目标是读者读一遍就不会读错。 + +它借鉴 [ASD-STE100](https://www.asd-ste100.org/)(Simplified Technical English)的方法:限制用词,限制句式,一句一事。它不是 ASD-STE100 的译本,与 ASD 没有关系。 + + +> **English.** STC is a controlled language for Chinese technical writing, modelled on the method of ASD-STE100: a numbered rule set, a controlled vocabulary (one meaning, one word), and a zero-dependency checker. It is not a translation of STE and is not affiliated with ASD. The rules are written for the ambiguities of Chinese: nominalised verbs (进行 + verb), long pre-nominal modifiers, the inclusive/exclusive ambiguity of 以上/以下, permission vs ability of 可以, and object restrictions on 打开/关闭/启动. + + +## 一个例子 + +节选自 [examples/before.md](examples/before.md): + +> 改写前:用户首先需要登陆到目标服务器,然后切换到 /opt 目录并且创建一个用于存放采集器配置文件以及运行时产生的临时数据的工作目录。 + +节选自 [examples/after.md](examples/after.md): + +> 1. 登录目标服务器。 +> 2. 切换到 `/opt` 目录。 +> 3. 创建工作目录 `/opt/collector`。工作目录存放配置文件和临时数据。 + +检查脚本对改写前那一句的输出: + +```text +examples/before.md:11: [1.3][必须] 禁用词「登陆」,改用:登录 +examples/before.md:11: [2.1][必须] 操作句 56 字,不超过 30 字 +examples/before.md:11: [4.1][建议] 禁用词「然后」,改用:拆成编号步骤 +examples/before.md:11: [4.1][必须] 禁用词「并且」,改用:拆句 +``` + +整篇改写前的文档有 34 条【必须】级别的发现,改写后是 0 条。 + +## 为什么做这个 + +2026 年 10 月,Andrej Karpathy [发帖](https://x.com/karpathy/status/2105819303471976479)说,让大模型用 ASD-STE100 来解释东西,读起来清楚得多。ASD-STE100 是航空维修手册用的受控英语:53 条写作规则,加一本约 900 个词的词典,每个词只有一个意思。 + +中文没有对应的东西。已有的中文技术写作规范管排版、标点和文档结构,见[和现有项目的区别](#和现有项目的区别)。它们都没有做 STE 的核心:编号规则、受控词表、可以自动检查的句法约束。本项目补这一块。 + +## 内容 + +| 文件 | 内容 | +| --- | --- | +| [规范.md](规范.md) | 写作规则。6 节,40 条,每条带「不批准 / 批准」例句。规则分【必须】和【建议】两级 | +| [词表.md](词表.md) | 受控词表。禁用词及替换、选词表(一个意义一个词)、一词一义限定。约 170 条 | +| [tools/check.py](tools/check.py) | 检查脚本。零依赖,Python 3.9 及以上。读规范和词表,输出行号、规则号、级别 | +| [SKILL.md](SKILL.md) | 给 Claude Code、Codex、Cursor 这类 agent 用的技能文件 | +| [examples/](examples/) | 改写前后的完整示例 | + +规则的六节:词、句、段、步骤与警告、数字时间与范围、术语与标记。最常用的几条: + +- 一个概念全文只用一个词。 +- 不用「进行」「加以」加动词。直接用动词。 +- 操作句不超过 30 字,描述句不超过 40 字。 +- 操作句用祈使句,动词开头,不写「请」「您」「用户需要」。 +- 条件写在动作前面。警告独立成段,放在步骤前。 +- 不用数字后的「以上」「以下」,写「大于」「不小于」。 +- 「打开」只用于文件和窗口。功能用「启用」,程序用「启动」。 +- 「可以」只表示允许。能力用「支持」。 + +## 给 AI 用 + +### 装成技能 + +用 [skills](https://github.com/vercel-labs/skills) 安装器: + +```bash +npx skills add mzopedia/simplified-technical-chinese +``` + +或手动复制。Claude Code 的全局技能目录是 `~/.claude/skills/`: + +```bash +git clone --depth 1 https://github.com/mzopedia/simplified-technical-chinese.git ~/.claude/skills/simplified-technical-chinese +``` + +装好后,agent 在写中文技术说明时会按规范写,并在保存成文件后运行检查脚本。 + +### 直接写进提示词 + +不装技能时,把下面这段放进系统提示或对话里: + +```text +用简明技术中文回答。规则:一句只说一件事,操作句不超过 30 字,描述句不超过 40 字;操作步骤用编号列表,动词开头,不写「请」「您」;用主动句;条件写在动作前面;一个概念全文只用一个词;不用「进行」「相关」「等」「以上」「尽快」「大概」;不用反问和感叹;数字后不用「以上」「以下」,写「大于」「不小于」。 +``` + +这对应规范的常规模式,也就是 Karpathy 说的「做到八成」。要全部规则时,把 [规范.md](规范.md) 整个给模型。 + +## 给人用 + +读 [规范.md](规范.md)。写完后用检查脚本过一遍: + +```bash +python3 tools/check.py 文档.md +``` + +在 CI 里用 `--strict`,有【必须】级别的发现时退出码为 1: + +```bash +python3 tools/check.py --strict docs/*.md +``` + +## 检查脚本 + +```text +python3 tools/check.py 文件.md [更多文件] + --strict 有【必须】级别的发现时退出码为 1 + --json JSON 输出 + --kind 操作|描述 强制按一种句型检查。聊天回答用「描述」 + --max-op 30 操作句字数上限 + --max-desc 40 描述句字数上限 + --no-vocab 不检查词表 + --check-counterexamples 也检查「不批准:」开头的反例行 +``` + +脚本能稳定查出的: + +- 禁用词,以及选词表中标为自动检查的词 +- 句长、段落句数 +- 操作句里的被动句、情态词、程度词和估计词 +- 操作句句尾的条件、双重否定、「等」结尾的开放列举 +- 相对日期、中文数字、「倍」表示减少、问句和感叹句 + +脚本查不出的:事实是否正确、术语选得对不对、逻辑顺序、一词一义限定。也查不出定语长度、指代是否清楚、缩写是否一致、步骤里的警告和原因。这几条规则在真实文档上校准时精确率太低,只留在规范文本里由人判断,见 [docs/校准报告.md](docs/校准报告.md)。脚本用编号列表项判断操作句。所有发现都要人工确认。 + +脚本不检查的内容: + +- 「」里的内容,按界面文字处理 +- 代码块和行内代码 +- HTML 注释,`stc:` 指令除外 +- 文件开头的 YAML front matter +- 以「不批准:」「反例」「改写前」开头的行 +- `` 和 `` 之间的内容;`` 跳过它后面的一段 + +## 和 ASD-STE100 的关系 + +- 方法来自 STE:一词一义、句长限制、祈使句、条件在前、段落句数、不用开放列举。 +- 规则是按中文重写的。中文没有时态和词形变化,但有自己的歧义来源。例如名词化结构、长定语、「以上」的端点歧义、「可以」的许可和能力歧义。对应关系见规范的附录 B。 +- ASD-STE100 是注册商标。本项目不使用这个名字,不暗示与 ASD 有关系,也不表示符合本规范的文本符合 ASD-STE100。 + +## 和现有项目的区别 + +| 项目 | 管什么 | 本项目的关系 | +| --- | --- | --- | +| [ruanyf/document-style-guide](https://github.com/ruanyf/document-style-guide) | 标题、段落、数值、标点、文档结构 | 互补。本规范不管排版 | +| [yikeke/zh-style-guide](https://github.com/yikeke/zh-style-guide) | 语言风格、标点、文档元素 | 互补 | +| [Fenng/Tech-Doc-Style-Chinese](https://github.com/Fenng/Tech-Doc-Style-Chinese) | 事实保真、改写流程、术语排版,有一节受控写作原则 | 它有 6 条原则和改写样例,没有编号规则和词表,并且明确不把句长做成硬规则。本项目做的正是这部分 | + +写完整文档时,建议三者配合:用本规范管句子和用词,用上面的项目管排版和结构。 + +## 状态 + +0.2。规则、阈值和自动检查的词表在 350 万字真实中文技术文档上校准过。语料是 8 个开源项目的中文文档,共 2856 个文件。它们是 TiDB、Kubernetes、MDN、Vue、Vite、Ant Design 的文档,以及《Rust 程序设计语言》中文版和阮一峰的 ES6 教程。 + +脚本执行的每条规则都在真实文本上抽样判定过,错多于对的规则已从脚本移除。好文档上的提示从每万字 194 条降到 88 条,其中一半以上是句长提示。不算句长,从 151 条降到 40 条。句长上限取好文档句长分布的 90 分位:90% 的操作句不超过 33 字,90% 的描述句不超过 41 字。细节见 [docs/校准报告.md](docs/校准报告.md)。 + +规范、本文件和 SKILL.md 自身都通过了检查脚本(0 条【必须】),CI 每次提交都会复查。 + +已知问题: + +- 词表约 170 条,只覆盖通用技术写作。 +- 没有做过阅读理解的对照实验。校准量的是误报率,不是阅读速度和出错率。 +- 检查脚本只做句法层面的检查。 + +## 参与 + +提一个词的方法:开 issue,写清四项:词、意义、建议的处理(禁用 / 选词 / 限定)、一个例句。 + +改词表时保持表格的列顺序,「自动检查」列只填「是」或「否」。改完运行测试: + +```bash +python3 -m unittest discover tests +``` + +## 许可 + +- 规范和词表的文本:[CC BY 4.0](LICENSE-CC-BY-4.0) +- 检查脚本和测试:[MIT](LICENSE) diff --git a/.qoder/skills/simplified-technical-chinese/SKILL.md b/.qoder/skills/simplified-technical-chinese/SKILL.md new file mode 100644 index 00000000..ef5f7804 --- /dev/null +++ b/.qoder/skills/simplified-technical-chinese/SKILL.md @@ -0,0 +1,129 @@ +--- +name: simplified-technical-chinese +description: 用简明技术中文(STC)写或改写中文技术内容:操作步骤、安装部署、故障排查、接口和参数说明、技术概念解释、给人读的解释性回答。用户提到「简明技术中文」「STC」「受控中文」「像 ASD-STE100 那样写中文」「一词一义」时使用;用户要求把中文技术说明「写清楚」「别有歧义」时也可使用。不用于营销文案、叙事、法律原文和代码。 +--- + +# 简明技术中文 + +目标:读者读一遍就不会读错。做法:限制用词,限制句式,一句一事。 + +完整规则见 [规范.md](规范.md),词表见 [词表.md](词表.md)。本文件只列最常用的部分。 + +## 模式 + +- 常规模式(默认):执行全部【必须】规则和词表的自动检查条目。【建议】规则只在明显时处理。 +- 严格模式:用户说「严格」「全部规则」时,执行全部规则。 + +用户没有说明时用常规模式。 + +## 写之前先分类 + +- 操作句:告诉读者做什么。放在编号列表里,动词开头,不超过 30 字。 +- 描述句:其他句子。不超过 40 字。 + +字数算法:每个汉字计 1,每个英文单词或数字串计 1,行内代码计 1,标点不计。 + +## 必须遵守的规则 + +词: + +- 一个概念全文只用一个词。不换同义词。(1.1) +- 不用「进行」「加以」「作出」「予以」加动词。直接用动词。(1.4) +- 不用没有数值的程度词:「很」「非常」「较」「大量」「尽快」「及时」。写数值或条件。(1.5) +- 缩写首次出现写全称并括注,之后固定写法。(1.6) +- 不用黑话:「赋能」「抓手」「闭环」「拉通」「沉淀」「痛点」「对标」「打法」「拉齐」。(1.7) + +句: + +- 一句只说一件事。分号不能用来把两句并成一句。(2.2) +- 操作句用祈使句,不写「请」「您」「用户需要」「应该」。(2.3) +- 用主动句,写出执行者。「被」「由」「受到」是信号。(2.4) +- 条件写在动作前面:「如果 X,做 Y」。不放句尾,不放括号。(2.5) +- 一句最多一个否定。不用「不得不」「不无」「没有……不」。(2.6) +- 名词前的修饰语不超过 15 字。连续的「的」是信号。(2.7) +- 「其」「该」「此」「它」只指代上一句的对象。否则重复名词。不用「相关」「上述」。(2.8) +- 不用反问、设问、感叹。(2.9) + +段和步骤: + +- 一段一个主题,不超过 6 句。(3.1、3.2) +- 步骤用编号列表,每个编号一个动作。不在正文里写「首先……然后……」。(3.3、4.1) +- 不用「等」结尾的开放列举。列全,或写数量加「包括」。(3.5) +- 警告独立成段,放在它针对的步骤之前,以「警告:」开头。(4.3) +- 人的动作用祈使句;系统的行为写「系统 + 动词」。(4.4) +- 前置条件单独列出,放在步骤之前。(4.5) + +数字: + +- 阿拉伯数字,数字与单位之间空格。(5.1) +- 不用数字后的「以上」「以下」「以内」。用「大于」「不小于」「不超过」,或「3 至 5(含)」。(5.2) +- 不用相对时间:「明天」「近期」「稍后」「尽快」。用绝对日期或时长。(5.3) +- 操作句里不用「约」「几」「若干」「多次」。写数值。(5.4) +- 减少不用「倍」。百分比和百分点分开。(5.5) + +标记: + +- 界面元素名用「」括起,和界面文字一致。(6.1) +- 代码、命令、路径、字段名用代码标记,原样保留。(6.2) + +## 词表速查 + +最常撞上的 20 条。完整表见 [词表.md](词表.md)。 + +| 不用 | 用 | +| --- | --- | +| 「进行安装」 | 安装 | +| 「相关配置」 | 写出具体对象 | +| 「支持 A、B 等」 | 列全,或「包括 3 种:A、B、C」 | +| 「10 以上」 | 大于 10 / 不小于 10 | +| 「大概 5 秒左右」 | 5 秒 | +| 「尽快」 | 30 分钟内 | +| 「如果失败的话」 | 如果失败 | +| 「检查一下」 | 检查 | +| 「出现错误的情况」 | 出现错误时 | +| 「应该」「务必」「一定要」 | 必须(强制)/ 建议(推荐) | +| 「不得」「不准」 | 禁止 | +| 「单击」「点按」 | 点击 | +| 「删掉」「去掉」 | 删除 | +| 「开启功能」 | 启用功能 | +| 「打开程序」 | 启动程序 | +| 「登出」 | 退出登录 | +| 「键入」 | 输入 | +| 「入参」「出参」 | 参数 / 返回值 | +| 「登陆」「帐号」「缺省」 | 登录、账号、默认 | +| 「报错」(名词) | 错误 | + +一词一义里最常错的 4 个: + +- 打开:只用于文件、窗口、页面、连接。功能用「启用」,程序用「启动」。 +- 可以:只表示允许。能力用「支持」。 +- 问题:只表示疑问。故障、错误、缺陷分开写。 +- 更新:只用于数据和内容。版本用「升级」。 + +## 工作流程 + +1. 判断内容类型。营销、叙事、法律原文不用本规范。 +2. 判断模式。默认常规模式。 +3. 写或改写。先定术语,再写句子。操作内容用编号列表。 +4. 如果结果保存成了文件,运行检查脚本: + + ```bash + python3 tools/check.py 文件.md + ``` + + 聊天回答这类没有编号步骤的内容,加 `--kind 描述`。 + +5. 修复全部【必须】级别的发现。【建议】级别的按判断处理。 +6. 改写别人的文稿时,不改变事实、数字、条件和确定程度。原文说「可能」,改写后仍是「可能」。 + +## 不要做的事 + +- 不改写代码、命令、路径、字段名、URL。 +- 不为了缩短句子而删除条件、例外和警告。 +- 不把「可能」改成「会」,不把「建议」改成「必须」。 +- 不给原文没有的数字、日期和时限编一个。缺信息时写「待确认」。 +- 不把规范用在它不适用的文体上。 + +## 检查脚本的局限 + +脚本只查句法。它查不出事实错误、术语选错、逻辑顺序错。它用「编号列表项」判断操作句,用「的」的数量近似长定语,用几个标记词近似被动句。所有发现都要人工确认。 diff --git a/.qoder/skills/simplified-technical-chinese/docs/校准报告.md b/.qoder/skills/simplified-technical-chinese/docs/校准报告.md new file mode 100644 index 00000000..dad0ff20 --- /dev/null +++ b/.qoder/skills/simplified-technical-chinese/docs/校准报告.md @@ -0,0 +1,178 @@ +# 校准报告(0.2) + + + +本报告记录 0.2 版把检查脚本放到真实中文技术文档上校准的方法、数据和决定。校准量的是**误报率**:脚本报出来的提示里,有多少是按规则确实该改的。它不量读者的理解速度和出错率。 + +## 结论 + +1. 草案 0.1 的脚本在公认写得好的文档上每万字报 194 条提示,约每 50 个字一条。answer-me-with-html 的维护者在他的 1000 篇中文稿上实测到的「30 条对 3 条」不是他的文档特殊,是草案的真实水平。 +2. 校准后降到每万字 88 条。其中一半以上是句长提示(2.1)。不算句长,从 151 条降到 40 条。 +3. 6 条规则从脚本移除:1.6 缩写写法、2.7 定语长度、2.8 指代、4.3 步骤里的警告、4.5 括号里的前置条件、4.6 步骤里的原因。它们的精确率在 0% 到 34% 之间。脚本用的代理信号(大小写不同的英文词、「的」的数量、指代词的数量、编号列表项里的「注意」「因为」「(版本)」)分不出合规和违规。规则本身不变,由人判断。 +4. 2.4 被动句只查操作句,级别降为【建议】。描述句里的被动有 58% 是「执行者未知或不重要」的合规用法;操作句里「被拒绝的请求」这类是定语不是谓语。脚本都分不出来。2.5 条件后置只查操作句。3.3 正文串联步骤降为【建议】:正文里的「首先……然后……」多数在描述执行顺序,不是在写步骤。 +5. 程度词(1.5)、情态词(2.3)、估计词(5.4)只在操作句和参数表格里查。描述句里的「很多」「大部分」「输出应该是」「大约 16 KB」多数是正常表述。 +6. 句长上限:操作句从 25 字改为 30 字,描述句保持 40 字。好文档里 90% 的操作句不超过 33 字,90% 的描述句不超过 41 字。 +7. 20 处跨词误匹配加了排除条件。例如「传入参数」里的「入参」、「不准确」里的「不准」、「添加以下」里的「加以」、「不得不」里的「不得」、「为其实施」里的「其实」、「规约」里的「约」、「相等」里的「等」。 +8. 词表里 8 个禁用词和 8 行选词表同义词改为不自动检查:相关、必要时、同时、落地、方面、所谓、不得、兜底(改为一词一义限定);去除 / 去掉、剔除 / 拿掉、拉起 / 跑起来、杀掉 / 干掉、调出 / 唤起、展示 / 呈现、回传 / 吐出、挂了。它们在真实文档里多数是别的意义或正常用法。 + +## 方法 + +流程和脚本在 [tools/calibration/](../tools/calibration/)。 + +1. 选 8 个公认写得好的中文技术文档仓库作语料,只取正文以中文为主的 Markdown 文件。大仓库稀疏检出中文文档目录。语料版本记录在 `corpora.json`。 +2. 用检查脚本跑全部语料,统计每条规则、每个词的命中数,以及句长分布。 +3. 每条规则抽最多 50 条命中(第二轮 60 条)。词表类规则先按词分层,每个词最多 8 条(第二轮 10 条),再在语料库之间轮流取。每条样本附命中行前后各一行原文。 +4. 用模型(deepseek-v4-flash,温度 0)按规则原文和词表条目判定每条样本:**真**(规则按字面确实被违反,而且按规则改写后这句话不会变得更不准确)、**误**(命中的词或句在这里是正常用法、固定术语、引用的界面文字或代码,或者按规则改写会损害意思)、**不确定**。本报告的精确率是模型初判,作者抽查过每条规则的误报样本和理由。模型的判定偏宽松:它倾向把「描述句里本来就这么写」判为正常用法。所以这里的精确率是下限。 +5. 精确率 = 真 / (真 + 误)。不低于 80% 保留;50% 到 79% 加匹配条件或降为【建议】;低于 50% 从脚本移除,规则只留在文本里。 +6. 按结果修脚本和词表,再跑一轮,确认命中数下降、精确率上升。 + +## 语料 + +| 语料库 | 来源 | 文件 | 句子 | 字数 | +| --- | --- | ---: | ---: | ---: | +| tidb-docs-cn | pingcap/docs-cn | 1085 | 73933 | 1525798 | +| k8s-zh-cn | kubernetes/website,content/zh-cn/docs/{concepts,tasks} | 359 | 26401 | 604671 | +| mdn-zh-cn | mdn/translated-content,files/zh-cn/web/{javascript,http} | 927 | 32239 | 596778 | +| trpl-zh-cn | KaiserY/trpl-zh-cn(《Rust 程序设计语言》) | 110 | 8109 | 234618 | +| es6tutorial | ruanyf/es6tutorial | 36 | 5579 | 142026 | +| vue-docs-zh-cn | vuejs-translations/docs-zh-cn | 107 | 6349 | 141378 | +| ant-design | ant-design/ant-design,components 和 docs 的 zh-CN 文件 | 169 | 6672 | 132856 | +| vite-docs-cn | vitejs/docs-cn | 63 | 4417 | 92739 | +| 合计 | | 2856 | 163699 | 3470864 | + +句子数按校准后的脚本统计(多行拼成一句)。 + +TiDB 文档占 44%,k8s 和 MDN 各占 17%。这三个是产品手册和参考文档,操作步骤多,和本规范的目标文体最接近。Rust 书和 ES6 教程是教材,句子长、设问多,是最「不像手册」的两个。 + +## 校准前后每条规则的命中 + +校准前是 0.1 的脚本,校准后是 0.2 的脚本。两者字数略有差别:0.2 跳过 HTML 注释和整行 HTML 标签,去掉了 k8s 文档注释里的英文原文。 + +| 规则 | 校准前命中 | 每万字 | 校准后命中 | 每万字 | 处理 | +| --- | ---: | ---: | ---: | ---: | --- | +| 2.1 句长 | 14814 | 42.2 | 16720 | 48.2 | 操作句上限 25 → 30;多行拼成一句后命中反而变多,见下文 | +| 1.3 禁用词 | 11374 | 32.4 | 5351 | 15.4 | 情况只查「的情况」且后面不是「下」;相关、必要时、方面、所谓不自动检查;跨词误匹配加排除 | +| 2.4 被动 | 8476 | 24.2 | 156 | 0.4 | 只查操作句,降为建议;「被 X 的 Y」不算 | +| 1.4 进行 / 加以 / 作出 | 5855 | 16.7 | 5422 | 15.6 | 加以、予以、做出贡献的误匹配加排除;「进行 + 英文词」不查 | +| 1.5 程度词 | 4537 | 12.9 | 383 | 1.1 | 只查操作句(比较、较为、基本、相对除外);合适 / 适当 / 适量、很 / 非常 / 极其 / 相当 / 十分不自动检查 | +| 2.8 指代 | 3751 | 10.7 | 0 | 0.0 | 移除 | +| 1.1 一词一义 | 3122 | 8.9 | 186 | 0.5 | 11 行同义词和不得、应该 / 应当改为不自动检查;点选、一定要加排除 | +| 1.6 缩写写法 | 2942 | 8.4 | 0 | 0.0 | 移除 | +| 2.7 长定语 | 2575 | 7.3 | 0 | 0.0 | 移除 | +| 5.4 估计词 | 2247 | 6.4 | 11 | 0.0 | 只查操作句和表格;一些、多次不自动检查;「规约」里的「约」加排除 | +| 2.3 祈使句 | 1885 | 5.4 | 71 | 0.2 | 请见 / 请参阅 / 请注意不查;你、应该、应当不自动检查 | +| 5.1 数字写法 | 1665 | 4.7 | 158 | 0.5 | 一次、两次、一行、几十行、百分之百、二十四小时制不查;px 不查空格 | +| 3.5 开放列举 | 1141 | 3.3 | 654 | 1.9 | 「例如……等」「相等」「不等」和括号里的列举不查 | +| 2.6 否定 | 722 | 2.1 | 55 | 0.2 | 只查双重否定,降为建议 | +| 2.9 问句感叹句 | 683 | 1.9 | 673 | 1.9 | 保留 | +| 3.2 段落句数 | 561 | 1.6 | 80 | 0.2 | 只数有句末标点的句子;整行 HTML 标签跳过 | +| 6.1 界面元素名 | 388 | 1.1 | 386 | 1.1 | 保留 | +| 2.5 条件后置 | 291 | 0.8 | 3 | 0.0 | 只查操作句句尾的条件;括号不查 | +| 4.1 一步一动作 | 187 | 0.5 | 152 | 0.4 | 同时不自动检查;然后、接着降为建议 | +| 2.2 一句一事 | 152 | 0.4 | 71 | 0.2 | 之后、随后不算连接词;降为建议 | +| 3.3 正文串联步骤 | 131 | 0.4 | 0 | 0.0 | 移除 | +| 5.2 范围端点 | 121 | 0.3 | 122 | 0.4 | 保留 | +| 1.7 黑话 | 107 | 0.3 | 12 | 0.0 | 透传、兜底改为一词一义;落地、打通不自动检查;对标加排除;心智模型、心智负担不查;痛点降为建议 | +| 5.3 相对时间 | 85 | 0.2 | 8 | 0.0 | 稍后、近期、不久、上个月、下个月不查,只查相对日期词;降为建议 | +| 4.6 步骤里的原因 | 56 | 0.2 | 0 | 0.0 | 移除 | +| 4.3 步骤里的警告 | 16 | 0.0 | 0 | 0.0 | 移除 | +| 4.5 括号里的前置条件 | 16 | 0.0 | 0 | 0.0 | 移除 | +| 5.5 倍数 | 1 | 0.0 | 1 | 0.0 | 保留 | +| 合计 | 67901 | 193.5 | 30675 | 88.4 | | + +校准后的 30675 条里,【必须】级别 29775 条(每万字 85.8),其余是【建议】。 + +## 句长 + +0.1 的脚本把 Markdown 的硬换行当句子边界。k8s、MDN、Rust 书都按 80 列左右硬换行,一句常被切成两三段,所以 0.1 量出来的句长偏短,而且被切开的片段还会触发 2.5 条件后置和 2.2 一句多事的误报。0.2 先把一个段落的多行拼起来再分句。下面是拼行后的分布。 + +| 语料库 | 句型 | n | 均值 | p50 | p75 | p90 | p95 | p99 | 超过上限(操作 30 / 描述 40) | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| k8s-zh-cn | 操作 | 1143 | 18.8 | 16 | 25 | 34 | 43 | 58 | 15.7% | +| k8s-zh-cn | 描述 | 25258 | 23.1 | 21 | 31 | 41 | 48 | 63 | 10.5% | +| tidb-docs-cn | 操作 | 3012 | 17.6 | 16 | 23 | 31 | 38 | 56 | 11.1% | +| tidb-docs-cn | 描述 | 70921 | 20.8 | 19 | 28 | 40 | 48 | 67 | 9.4% | +| mdn-zh-cn | 描述 | 32010 | 18.5 | 16 | 28 | 39 | 47 | 63 | 8.9% | +| trpl-zh-cn | 描述 | 8065 | 29.0 | 28 | 37 | 48 | 55 | 69 | 19.4% | +| es6tutorial | 描述 | 5570 | 25.5 | 24 | 33 | 43 | 50 | 68 | 12.6% | +| 全部 | 操作 | 4799 | 18.3 | 16 | 24 | 33 | 40 | 58 | 13.1% | +| 全部 | 描述 | 158900 | 21.3 | 19 | 29 | 41 | 48 | 66 | 10.2% | + +各阈值下超限的句子比例(全部语料): + +| 上限 | 30 字 | 35 字 | 40 字 | 45 字 | 50 字 | +| --- | ---: | ---: | ---: | ---: | ---: | +| 操作句 | 13.1% | 7.9% | 4.9% | 3.2% | 1.9% | +| 描述句 | 22.9% | 15.4% | 10.2% | 6.5% | 4.2% | + +决定:操作句 30 字(85 到 90 分位之间),描述句 40 字(90 分位附近)。两个数字都会让好文档里约 10% 的句子收到提示。这是本规范有意比常规文档更严的地方:句长是 STE 方法的核心约束。手册类文档(k8s、TiDB)的 p90 比教材(Rust 书、ES6 教程)低 7 到 8 字,上限按手册定。 + +## 每条规则的精确率 + +三轮抽样。第一轮量 0.1 的脚本;第二轮量第一次修正后的脚本;第三轮量 0.2 的脚本(第三轮之后还有少量调整,见下)。第三轮的提示词补了附录 A 的字数算法,所以 2.1 的判定比前两轮准。每轮每条规则最多 60 条样本,样本少于 10 条的规则不下结论。 + +| 规则 | 第一轮 | 第二轮 | 第三轮 | 第三轮样本 | 0.2 里的级别 | +| --- | ---: | ---: | ---: | ---: | --- | +| 2.1 句长 | 64% | 73% | 92% | 60 | 必须 | +| 1.3 禁用词 | 60% | 80% | 80% | 60 | 必须 | +| 1.4 进行 / 加以 / 作出 | 69% | 87% | 82% | 45 | 必须 | +| 2.9 问句感叹句 | 84% | 82% | 80% | 60 | 必须 | +| 6.1 界面元素名 | 94% | 90% | 95% | 60 | 建议 | +| 3.2 段落句数 | 47% | 92% | 100% | 60 | 必须 | +| 5.2 范围端点 | 71% | 83% | 80% | 30 | 必须 | +| 4.1 一步一动作 | 42% | 64% | 64% | 22 | 并且必须,然后建议 | +| 2.2 一句一事 | 54% | 65% | 53% | 60 | 建议 | +| 2.6 双重否定 | 22% | 41% | 53% | 55 | 建议 | +| 1.1 一词一义 | 26% | 63% | 48% | 60 | 必须,只剩高精确率的词 | +| 5.1 数字写法 | 4% | 45% | 47% | 60 | 阿拉伯数字必须,单位空格建议 | +| 1.5 程度词 | 44% | 42% | 47% | 60 | 必须,只查操作句,只剩高精确率的词 | +| 2.3 祈使句 | 51% | 60% | 45% | 47 | 必须,只查操作句,去掉你和应该 | +| 5.4 估计词 | 16% | 33% | 39% | 28 | 必须,只查操作句和表格,去掉一些和多次 | +| 1.7 黑话 | 22% | 32% | 44% | 16 | 必须,去掉打通、落地、兜底 | +| 2.4 被动 | 42% | 43% | 35% | 60 | 建议,只查操作句 | +| 3.5 开放列举 | 50% | 60% | 30% | 10 | 必须,括号里的列举不查 | +| 5.3 相对时间 | 26% | 33% | 38% | 8 | 建议 | +| 3.3 正文串联步骤 | 12% | 40% | 30% | 10 | 脚本不查 | +| 2.5 条件后置 | 34% | 35% | 100% | 3 | 必须,只查操作句 | +| 2.8 指代 | 0% | – | – | | 脚本不查 | +| 1.6 缩写写法 | 0% | – | – | | 脚本不查 | +| 2.7 长定语 | 2% | – | – | | 脚本不查 | +| 4.3 / 4.5 / 4.6 步骤里的警告、前置条件、原因 | 19% / 12% / 34% | – | – | | 脚本不查 | + +第三轮之后的调整:2.2、2.6、5.3 降为建议;3.3 从脚本移除;1.1、1.5、2.3、5.4、1.7 里精确率低的词改为不自动检查(挑选 / 选取、应答 / 回包、入参的选词表条目;很 / 非常 / 极其 / 相当 / 十分、合适 / 适当 / 适量;你、应该 / 应当;一些、多次;打通)。这些词的命中占这几条规则命中的大半,所以调整后的精确率会高于表里的数字,但没有再量。 + +作者和模型判定不一致的地方:模型把「不得不」「合适的命令」「(MySQL、PostgreSQL 等)」判为正常用法,作者认为按规则该改。这三类按作者意见处理:2.6 保留为建议,1.5 的合适不自动检查,3.5 括号里的列举不查、其余保留。 + +## 词表条目的精确率 + +命中最多的词单独抽了 25 条(第二轮脚本的命中)。精确率决定了它们在 0.2 里的处理。 + +| 词 | 命中 | 精确率 | 处理 | +| --- | ---: | ---: | --- | +| 进行 | 5246 | 100%(10 条) | 保留 | +| 报错 | 1407 | 96% | 保留。TiDB 文档把它当名词用,模型仍判为该改 | +| 开启 | 1914 | 76% | 保留。误报是「开启 / 关闭」对举和标题引用 | +| 做出 | 111 | 88% | 保留。「做出贡献」「做出反应」加排除 | +| 的话 | 214 | 76% | 保留。误报是「如果有的话」「可能的话」 | +| 然后 | 122 | 76% | 降为建议:编号列表项不一定是步骤 | +| 等 | 782 | 72% | 保留。「相等」「不等」「例如……等」加排除 | +| 比较 | 193 | 72% | 保留 | +| 一定要 | 60 | 56% | 保留。「不是一定要」「是否一定要」加排除 | +| 应该 | 32 | 52% | 保留,只查操作句 | +| 不得 | 74 | 52% | 不自动检查:「不得超过 256 KiB」是规范文体的正常写法 | +| 你 | 434 | 44% | 降为建议:步骤里的「你可以……」是文体问题 | +| 所谓 | 22 | 36% | 不自动检查:「所谓幻读是指……」引出定义是正常用法 | +| 情况 | 1292 | 32% | 只查「的情况」且后面不是「下」:「在……的情况下」是固定搭配 | +| 方面 | 163 | 8% | 不自动检查 | +| 不准 | 12 | 0% | 只查「不准 + 动词」:剩下的命中全是「统计不准」这种「不准确」 | + +每条规则抽样里出现的其余词,见第三轮报告表。 + +## 局限 + +- 判定由模型做,作者只抽查。模型偏宽松,精确率是下限。欢迎在 issue 里对任何一条判定提出异议。 +- 语料全是开源项目的文档和教材,没有企业内部文档和 AI 生成的回答。后者正是本规范的主要使用场景之一,它们的黑话和虚词密度可能高得多。 +- 操作句靠「编号列表项」识别。k8s 和 MDN 大量用编号列表做说明性列举,脚本把它们当步骤检查,是 2.1、2.3、4.1 剩余误报的主要来源。 +- 没有量阅读效果。校准只回答「脚本报的对不对」,不回答「按规范改写后读者是否读得更快、错得更少」。 + + diff --git a/.qoder/skills/simplified-technical-chinese/examples/after.md b/.qoder/skills/simplified-technical-chinese/examples/after.md new file mode 100644 index 00000000..009ffbc6 --- /dev/null +++ b/.qoder/skills/simplified-technical-chinese/examples/after.md @@ -0,0 +1,43 @@ +# 日志采集器部署指南 + +本文档说明如何在生产环境部署日志采集器。读者是运维人员。 + +## 前置条件 + +- 服务器操作系统:Ubuntu 22.04 或 CentOS 8。 +- 磁盘剩余空间不小于 10 GB。 +- 服务器能访问镜像仓库 `registry.example.com`。 + +服务器不能访问镜像仓库时,向网络组申请访问权限。审批时间为 1 至 3 个工作日。 + +## 部署步骤 + +1. 登录目标服务器。 +2. 切换到 `/opt` 目录。 +3. 创建工作目录 `/opt/collector`。工作目录存放配置文件和临时数据。 +4. 下载与服务器 CPU 架构匹配的安装包。 +5. 解压安装包。 +6. 在配置文件中,把 `server_addr` 改为服务端地址。 +7. 在配置文件中,把 `batch_size` 改为 200 至 500 之间的值。 +8. 运行启动命令。systemd 接管采集器。采集器每 10 秒把日志上报到服务端。 +9. 运行状态查询命令。系统显示 `active`。 + +说明:架构不匹配时,安装包无法运行,系统显示错误「exec format error」。 + +## 排查故障 + +采集器无法启动时,按下面的步骤排查: + +1. 运行配置校验命令。 +2. 如果校验失败,按输出修改配置文件。 +3. 重启采集器。 + +采集器的内存占用约为 200 MB。内存占用大于 500 MB 时,把 `batch_size` 调小到 200。 + +## 调试模式 + +需要查看详细日志时,启用调试模式。调试模式写入的日志量是正常模式的 10 倍。 + +## 版本计划 + +v2.0 于 2026 年 12 月 1 日发布。v2.0 的吞吐量是 v1.x 的 3 倍,内存占用减少到 v1.x 的 1/2。 diff --git a/.qoder/skills/simplified-technical-chinese/examples/before.md b/.qoder/skills/simplified-technical-chinese/examples/before.md new file mode 100644 index 00000000..2622ffad --- /dev/null +++ b/.qoder/skills/simplified-technical-chinese/examples/before.md @@ -0,0 +1,19 @@ +# 日志采集器部署指南 + +本文档主要是介绍一下如何在生产环境对日志采集器进行部署以及相关配置的说明,适用于运维同学以及对此感兴趣的开发同学等。 + +## 部署前的准备 + +首先需要确认服务器的操作系统版本是比较新的,然后检查一下磁盘空间是否充足(一般来说 10GB 以上就基本够用了),最后还要保证服务器能够访问到我们的镜像仓库。如果服务器无法访问外网的话,需要联系网络组的同事开通相关的权限,这个过程可能需要几天时间,所以最好尽快提交申请。 + +## 部署步骤 + +1. 用户首先需要登陆到目标服务器,然后切换到 /opt 目录并且创建一个用于存放采集器配置文件以及运行时产生的临时数据的工作目录。 +2. 请下载安装包并进行解压,注意安装包必须和服务器的 CPU 架构匹配,否则会报错。 +3. 修改配置文件里的 server_addr(服务端地址)参数,把它改成实际的地址,然后再修改一下 batch_size 参数,这个参数不要设置得太大也不要设置得太小,一般几百左右就可以了。 +4. 执行启动命令以后,采集器会被 systemd 接管,日志会由采集器定期上报到服务端,这时候应该能看到状态变成 active 了吧? +5. 开启调试功能(如果需要排查问题的话)。 + +## 常见问题 + +如果采集器起不来,大概率是因为配置文件的格式有问题,可以用校验命令对配置文件进行校验,校验通过之后再重新拉起服务。采集器的内存占用一般在 200MB 左右,如果内存占用非常高,说明 batch_size 参数设置得不合适,适当调小即可。近期我们会发布新版本来对这个问题加以优化,届时性能会提升 3 倍以上,内存占用也会降低 2 倍。 diff --git a/.qoder/skills/simplified-technical-chinese/tests/test_check.py b/.qoder/skills/simplified-technical-chinese/tests/test_check.py new file mode 100644 index 00000000..6d436e39 --- /dev/null +++ b/.qoder/skills/simplified-technical-chinese/tests/test_check.py @@ -0,0 +1,293 @@ +"""检查脚本的回归测试。运行:python3 -m unittest discover tests""" + +import os +import sys +import unittest + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, os.path.join(ROOT, "tools")) + +import check # noqa: E402 + + +def rules(findings): + return {f.rule for f in findings} + + +class TestCount(unittest.TestCase): + def test_appendix_a_examples(self): + self.assertEqual(check.count_chars("重启服务。"), 4) + self.assertEqual(check.count_chars("把 `max_connections` 改为 200。"), 5) + + def test_latin_tokens_count_one(self): + self.assertEqual(check.count_chars("使用 API v2.0 和 UTF-8 编码"), 8) + + +class TestVocab(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.vocab = check.load_vocab() + cls.checker = check.Checker(cls.vocab) + + def test_vocab_loads(self): + self.assertGreater(len(self.vocab), 80) + kinds = {e.kind for e in self.vocab} + self.assertEqual(kinds, {"banned", "choice"}) + + def test_jinxing_flagged(self): + f = self.checker.check_text("对配置文件进行修改。") + self.assertIn("1.4", rules(f)) + + def test_jinxing_zhong_not_flagged(self): + f = self.checker.check_text("任务进行中。") + self.assertNotIn("1.4", rules(f)) + + def test_yishang_after_digit(self): + f = self.checker.check_text("并发数 100 以上。") + self.assertIn("5.2", rules(f)) + + def test_yishang_without_digit(self): + f = self.checker.check_text("以上步骤完成后,服务可用。") + self.assertNotIn("5.2", rules(f)) + + def test_deng_enumeration(self): + f = self.checker.check_text("支持 MySQL、PostgreSQL 等数据库。") + self.assertIn("3.5", rules(f)) + + def test_deng_not_enumeration(self): + f = self.checker.check_text("等待 3 秒。") + self.assertNotIn("3.5", rules(f)) + f = self.checker.check_text("保存文件、关闭窗口,等待 3 秒。") + self.assertNotIn("3.5", rules(f)) + + def test_quoted_text_is_skipped(self): + f = self.checker.check_text("不写「请」「您」「进行」。") + self.assertEqual([x for x in f if x.level == check.MUST], []) + + def test_nested_counterexample_is_skipped(self): + text = "- 不批准:\n 1. 请进行安装,然后重启。\n- 批准:\n 1. 安装。\n 2. 重启。\n" + self.assertEqual(self.checker.check_text(text), []) + + def test_front_matter_and_directives_are_skipped(self): + text = "---\ndescription: 请进行安装\n---\n\n\n请进行安装。\n\n\n\n请进行安装。\n\n安装。\n" + self.assertEqual(self.checker.check_text(text), []) + + def test_html_comments_are_skipped(self): + text = "\n".join([ + "", + "中文句子。", + " 注释后的进行修改。", + ]) + f = self.checker.check_text(text) + self.assertEqual([(x.rule, x.line) for x in f], [("1.4", 7)]) + + def test_counterexample_line_is_skipped(self): + f = self.checker.check_text("- 不批准:对配置文件进行修改。") + self.assertEqual(f, []) + strict = check.Checker(self.vocab, skip_counterexamples=False) + self.assertIn("1.4", rules(strict.check_text("- 不批准:对配置文件进行修改。"))) + + def test_qing_only_in_operation(self): + f = self.checker.check_text("1. 请点击「保存」。") + self.assertIn("2.3", rules(f)) + f = self.checker.check_text("这里不是操作句,请求参数如下。") + self.assertNotIn("2.3", rules(f)) + + def test_choice_word(self): + f = self.checker.check_text("单击「保存」。") + msgs = [x.message for x in f if x.rule == "1.1"] + self.assertTrue(any("点击" in m for m in msgs)) + + def test_jargon_is_rule_1_7(self): + f = self.checker.check_text("按新打法对标行业方案,把握用户痛点,理清底层逻辑。") + hits = [x for x in f if x.rule == "1.7"] + self.assertEqual(len(hits), 4) + # 痛点常有正当用法,降为建议;其余是必须 + self.assertEqual(sum(x.level == check.MUST for x in hits), 3) + self.assertEqual(sum(x.level == check.SHOULD for x in hits), 1) + + def test_words_with_technical_meaning_are_not_banned(self): + # 势能、飞轮、护城河 标为不自动检查;脚手架、收敛 只在词表第三部分限定意义 + f = self.checker.check_text("弹簧的势能转化为动能。用脚手架生成项目。迭代 20 次后误差收敛。") + self.assertNotIn("1.7", rules(f)) + self.assertNotIn("1.3", rules(f)) + + def test_zhuxiao_account_allowed(self): + f = self.checker.check_text("注销账号后数据不可恢复。") + self.assertFalse(any("注销" in x.message for x in f)) + + def test_ranhou_only_in_ordered(self): + f = self.checker.check_text("1. 打开终端,然后运行命令。") + self.assertIn("4.1", rules(f)) + f = self.checker.check_text("服务读取配置,然后连接数据库。") + self.assertNotIn("4.1", rules(f)) + + def test_substring_matches_are_not_banned_words(self): + for text in ["可以传入参数,生成对应的值。", "坐标位置不准确。", "添加以下参数。", "我们不得不为此调整。", + "为其实施写保护。", "你不一定要从零开始。", "节点选择完全受控。", "针对标识符的定义。", "一千万行数据。"]: + f = [x for x in self.checker.check_text(text) if x.rule in ("1.1", "1.3", "1.4", "1.7")] + self.assertEqual(f, [], text) + + def test_degree_and_estimate_words_only_in_operations(self): + self.assertNotIn("1.5", rules(self.checker.check_text("大部分用户不用 beta 版。尽快升级。"))) + self.assertIn("1.5", rules(self.checker.check_text("1. 尽快重启服务。"))) + # 很、非常、合适这类词连操作句里也多是正常用法,不自动检查 + self.assertNotIn("1.5", rules(self.checker.check_text("1. 等待很长时间。"))) + self.assertNotIn("5.4", rules(self.checker.check_text("这会有大约 16 KB 的基本打包大小。"))) + self.assertIn("5.4", rules(self.checker.check_text("1. 重试若干次。"))) + self.assertIn("5.4", rules(self.checker.check_text("| 超时 | 大约 30 秒 |"))) + + def test_modal_words_only_in_operations(self): + self.assertNotIn("2.3", rules(self.checker.check_text("输出结果应该是这样。"))) + self.assertIn("2.3", rules(self.checker.check_text("1. 您需要先备份。"))) + # 你、应该在编号列表里多是说明不是步骤,不自动检查 + self.assertNotIn("2.3", rules(self.checker.check_text("1. 你应该先备份。"))) + + def test_open_list_with_example_marker_is_allowed(self): + self.assertNotIn("3.5", rules(self.checker.check_text("例如 Deployment、Service 等对象。"))) + self.assertIn("3.5", rules(self.checker.check_text("支持 MySQL、PostgreSQL 等数据库。"))) + + def test_qingkuang_only_after_de(self): + self.assertNotIn("1.3", rules(self.checker.check_text("默认情况下,服务监听 8080 端口。"))) + # 「在……的情况下」是固定搭配,不查;「的情况」单独出现才查 + self.assertNotIn("1.3", rules(self.checker.check_text("出现错误的情况下重试。"))) + self.assertIn("1.3", rules(self.checker.check_text("用 TiUP 部署的情况,见下文。"))) + + +class TestSentenceRules(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.checker = check.Checker(None) + + def test_length_operation(self): + long_op = "1. 在确认数据库已经完成备份并且备份文件可以正常恢复之后登录到主服务器。" + f = self.checker.check_text(long_op) + self.assertIn("2.1", rules(f)) + + def test_length_description_ok(self): + f = self.checker.check_text("服务读取配置文件。") + self.assertNotIn("2.1", rules(f)) + + def test_system_subject_in_step_is_description(self): + text = "1. 点击「提交」。系统生成订单号并写入数据库,然后发送通知邮件给管理员。" + f = self.checker.check_text(text) + # 第二句以「系统」开头,按描述句算,40 字以内不报 2.1 + self.assertNotIn("2.1", rules(f)) + + def test_double_negation(self): + # 校准后降为建议:「不得不」这类固定搭配模型多判为正常用法 + f = self.checker.check_text("不得不重启服务。") + self.assertTrue(any(x.rule == "2.6" and x.level == check.SHOULD for x in f)) + + def test_question_mark(self): + f = self.checker.check_text("为什么服务起不来?") + self.assertIn("2.9", rules(f)) + + def test_exclamation_inside_quotes_ok(self): + f = self.checker.check_text("系统显示「保存成功!」。") + self.assertNotIn("2.9", rules(f)) + + def test_condition_after_action(self): + f = self.checker.check_text("1. 重启服务,如果修改了端口号。") + self.assertTrue(any(x.rule == "2.5" and x.level == check.MUST for x in f)) + # 括号里的内容多是补充说明,校准后不再当条件 + self.assertNotIn("2.5", rules(self.checker.check_text("重启服务(如果修改了端口号)。"))) + + def test_passive_in_operation_is_should(self): + f = self.checker.check_text("1. 配置文件会被服务读取。") + self.assertTrue(any(x.rule == "2.4" and x.level == check.SHOULD for x in f)) + # 「被 X 的 Y」是定语,不是被动谓语 + self.assertNotIn("2.4", rules(self.checker.check_text("1. 查看被拒绝的请求。"))) + + def test_passive_in_description_is_not_checked(self): + # 描述句里的被动多是执行者未知或不重要的合规用法,脚本分不出来,校准后只查操作句 + f = self.checker.check_text("配置文件会被服务读取。此选项已被弃用。") + self.assertNotIn("2.4", rules(f)) + + def test_passive_markers_inside_other_words_are_not_passive(self): + # 来自 answer-me-with-html 维护者在 1000 篇中文文档上的实测误报。 + for text in ["把被子叠好。", "因为缓存所在目录不可写,任务失败。", "成为开发者所需的全部工具。", "系统所示的路径。"]: + f = self.checker.check_text(text) + self.assertNotIn("2.4", rules(f), text) + + def test_imperative_rule_skips_questions_and_nouns(self): + for text in ["1. 需不需要重启,看日志。", "1. 用户表里新增一列。"]: + f = self.checker.check_text(text) + self.assertNotIn("2.3", rules(f), text) + + def test_reduce_by_times(self): + f = self.checker.check_text("延迟降低了 3 倍。") + self.assertIn("5.5", rules(f)) + + def test_relative_time(self): + f = self.checker.check_text("旧接口下周下线。") + self.assertIn("5.3", rules(f)) + # 「稍后」「近期」在技术描述里多指程序时序,不是相对于写作时间的日期 + self.assertNotIn("5.3", rules(self.checker.check_text("保存当前值,以便稍后恢复。查看近期事件。"))) + + def test_chinese_numeral(self): + f = self.checker.check_text("等待三秒。") + self.assertIn("5.1", rules(f)) + + def test_paragraph_sentences(self): + text = "一。二。三。四。五。六。七。" + f = self.checker.check_text(text) + self.assertIn("3.2", rules(f)) + + def test_first_then_in_prose_is_not_checked(self): + # 3.3 从脚本移除:正文里的「首先……然后……」多数在描述顺序,不是在写步骤(校准精确率 30%) + f = self.checker.check_text("首先停止服务,然后备份数据,最后运行脚本。") + self.assertNotIn("3.3", rules(f)) + + def test_numerals(self): + # 「一次」「两次」「一行」是量词习惯用法;三以上和两位数才查 + self.assertNotIn("5.1", rules(self.checker.check_text("只执行一次。这一行会报错。重试两次。"))) + self.assertIn("5.1", rules(self.checker.check_text("等待三秒。"))) + self.assertIn("5.1", rules(self.checker.check_text("保留二十行。"))) + + def test_code_block_skipped(self): + text = "```\n请进行安装,然后等等等。\n```\n" + f = self.checker.check_text(text) + self.assertEqual(f, []) + + def test_hard_wrapped_lines_form_one_sentence(self): + # Markdown 里的硬换行不是句子边界:两行拼成一句 46 字,报一次 2.1,行号是句子开始的那行 + text = "第一句。这是一个被硬换行切开的很长的描述句,前半段写在这一行的末尾,\n后半段写在下一行,加起来超过四十个字。" + f = [x for x in self.checker.check_text(text) if x.rule == "2.1"] + self.assertEqual([(x.line, x.message[:8]) for x in f], [(1, "描述句 45 字")]) + + def test_unpunctuated_lines_are_not_sentences(self): + text = "\n".join(["基本"] * 8 + ["这些是组件示例的名字。"]) + self.assertNotIn("3.2", rules(self.checker.check_text(text))) + self.assertNotIn("3.2", rules(self.checker.check_text("基本\n" * 8))) + + +class TestExamples(unittest.TestCase): + def setUp(self): + self.checker = check.Checker(check.load_vocab()) + + def test_before_has_many_findings(self): + f = self.checker.check_file(os.path.join(ROOT, "examples", "before.md")) + must = [x for x in f if x.level == check.MUST] + self.assertGreaterEqual(len(must), 15) + + def test_after_has_no_must(self): + f = self.checker.check_file(os.path.join(ROOT, "examples", "after.md")) + must = [x for x in f if x.level == check.MUST] + self.assertEqual(must, [], "\n".join(x.format() for x in must)) + + +class TestCli(unittest.TestCase): + def test_strict_exit_code(self): + before = os.path.join(ROOT, "examples", "before.md") + after = os.path.join(ROOT, "examples", "after.md") + self.assertEqual(check.main(["--strict", "--quiet", before]), 1) + self.assertEqual(check.main(["--strict", "--quiet", after]), 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/.qoder/skills/simplified-technical-chinese/tools/calibration/README.md b/.qoder/skills/simplified-technical-chinese/tools/calibration/README.md new file mode 100644 index 00000000..74e66953 --- /dev/null +++ b/.qoder/skills/simplified-technical-chinese/tools/calibration/README.md @@ -0,0 +1,63 @@ +# 校准流程 + +这个目录的脚本把检查脚本放到真实中文技术文档上跑。它们量出每条规则、每个词的命中数和精确率。结果决定规则的级别、阈值和词表的取舍。校准结果见 [docs/校准报告.md](../../docs/校准报告.md)。 + +## 步骤 + +1. 准备语料。每个文档仓库克隆到 `.data/corpus/<名称>/`。大仓库用稀疏检出只取中文文档目录。`.data/` 不入库。 +2. 记录语料版本: + + ```bash + python3 tools/calibration/manifest.py .data/corpus .data/calib/corpora.json + ``` + +3. 统计命中和句长分布: + + ```bash + python3 tools/calibration/stats.py .data/corpus .data/calib + ``` + + 只统计正文以中文为主的 Markdown 文件。双语仓库只取中文版。输出 `findings.jsonl`、`sentences.jsonl`、`stats.json`、`stats.md`。 + +4. 分层抽样: + + ```bash + python3 tools/calibration/sample.py .data/corpus .data/calib --per-rule 50 --per-word 8 + ``` + + 每条规则最多 50 条。词表类规则先按词分层,每个词最多 8 条,再在语料库之间轮流取。 + +5. 模型初判: + + ```bash + python3 tools/calibration/label.py .data/calib --env <含 LLM_BASE_URL、LLM_API_KEY、LLM_MODEL 的文件> + ``` + + 每条样本给出「真」「误」「不确定」和一句理由。支持断点续跑。 + +6. 人工复核。把复核结果写进 `.data/calib/labels.human.jsonl`,字段和 `labels.jsonl` 相同,人工结果覆盖模型结果。 +7. 生成报告: + + ```bash + python3 tools/calibration/report.py .data/calib + ``` + + `report.md` 是表格,`misses.md` 列出所有判为误报的样本和理由,用来修正则和词表。 + +## 判定标准 + +- 真:规则按字面确实被违反,而且按规则改写后,这句话不会变得更不准确。 +- 误:命中的词或句在这里是正常用法、固定术语、引用的界面文字或代码。按规则改写会损害意思的也算误。 +- 不确定:两种判断都说得通。不计入精确率。 + +## 处理规则 + +精确率 = 真 / (真 + 误)。 + +| 精确率 | 处理 | +| --- | --- | +| 不低于 80% | 保留 | +| 50% 到 79% | 给词加匹配条件,或把规则降为【建议】 | +| 低于 50% | 从脚本移除。规则只留在规范文本里,由人判断 | + +句长阈值取好文档中句长分布的 p90 附近,而不是拍脑袋。 diff --git a/.qoder/skills/simplified-technical-chinese/tools/calibration/label.py b/.qoder/skills/simplified-technical-chinese/tools/calibration/label.py new file mode 100644 index 00000000..4bd9bdc7 --- /dev/null +++ b/.qoder/skills/simplified-technical-chinese/tools/calibration/label.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +"""用模型初判抽样结果是真报还是误报。结果供人工抽查,不直接当结论。 + +用法: + python3 tools/calibration/label.py <输出目录> --env <含 LLM_* 变量的 .env 文件> [--workers 8] [--limit N] + +读取 sample.jsonl,对每条样本给出 verdict(真 / 误 / 不确定)和一句理由,写入 labels.jsonl。 +断点续跑:labels.jsonl 里已有的 id 跳过。 +.env 里需要:LLM_BASE_URL(OpenAI 兼容)、LLM_API_KEY、LLM_MODEL;可选 LLM_EXTRA_JSON(并入请求体)。 +""" + +import argparse +import json +import os +import re +import sys +import time +import urllib.error +import urllib.request +from concurrent.futures import ThreadPoolExecutor, as_completed +from typing import Dict, Optional + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.normpath(os.path.join(HERE, "..", "..")) + +SYSTEM = """你是一名中文技术文档编辑。有一套写作规范和一个按规范检查文档的脚本。 +脚本在一篇真实文档里报了一条提示。请判断这条提示是否成立。 + +脚本的字数算法(附录 A):每个汉字计 1,每个英文单词或数字串计 1,行内代码(显示为 CODE)计 1,标点不计。 +「命中的句子」只显示前 40 个字符,完整句子看上下文。脚本已经把一个段落的多行拼成一句再分句,不会被换行截断。 + +判定标准: +- 真:规则按字面确实被违反,而且按规则改写后,这句话不会变得更不准确。 +- 误:命中的词或句在这里是正常用法、固定术语、引用的界面文字或代码,或者按规则改写会损害意思。 +- 不确定:两种判断都说得通。 + +只输出一行 JSON:{"verdict": "真|误|不确定", "reason": "不超过 40 字的理由"}""" + + +def load_env(path: str) -> Dict[str, str]: + env: Dict[str, str] = {} + with open(path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + k, v = line.split("=", 1) + env[k.strip()] = v.strip().strip('"').strip("'") if not v.strip().startswith("{") else v.strip() + return env + + +def load_rules() -> Dict[str, str]: + """规范.md 里每条规则的标题和正文(含批准 / 不批准例子)。""" + with open(os.path.join(ROOT, "规范.md"), encoding="utf-8") as f: + text = f.read() + rules: Dict[str, str] = {} + parts = re.split(r"^(?=### \d+\.\d+ )", text, flags=re.M) + for p in parts: + m = re.match(r"### (\d+\.\d+) (.*)", p) + if not m: + continue + body = re.split(r"^## ", p, flags=re.M)[0].strip() + rules[m.group(1)] = body[:1500] + return rules + + +def load_vocab_rows() -> Dict[str, str]: + """词表里每个词所在的表格行,供模型看替换建议。""" + rows: Dict[str, str] = {} + with open(os.path.join(ROOT, "词表.md"), encoding="utf-8") as f: + for line in f: + if not line.startswith("|") or "---" in line: + continue + cells = [c.strip() for c in line.strip().strip("|").split("|")] + for w in re.split("[、,]", cells[0]): + if w and w not in rows: + rows[w] = line.strip() + if len(cells) > 2: + for w in re.split("[、,]", cells[2]): + if w and w not in rows: + rows[w] = line.strip() + return rows + + +def build_prompt(rec: dict, rules: Dict[str, str], vocab: Dict[str, str]) -> str: + parts = [f"## 规则 {rec['rule']}", rules.get(rec["rule"], "(规则正文缺失)")] + if rec.get("word") and rec["word"] in vocab: + parts += ["", "## 词表条目", vocab[rec["word"]]] + parts += ["", "## 脚本的提示", rec["message"], "", "## 命中的句子", rec["text"]] + if rec.get("context"): + parts += ["", "## 上下文(原文,命中行前后各一行)", *rec["context"]] + parts += ["", f"文档来自 {rec['corpus']},路径 {rec['path']}。这条提示成立吗?"] + return "\n".join(parts) + + +def call(env: Dict[str, str], prompt: str, timeout: int = 90) -> str: + body: dict = { + "model": env["LLM_MODEL"], + "messages": [{"role": "system", "content": SYSTEM}, {"role": "user", "content": prompt}], + "temperature": 0, + "max_tokens": 200, + } + if env.get("LLM_EXTRA_JSON"): + body.update(json.loads(env["LLM_EXTRA_JSON"])) + req = urllib.request.Request( + env["LLM_BASE_URL"].rstrip("/") + "/chat/completions", + data=json.dumps(body).encode("utf-8"), + headers={"Content-Type": "application/json", "Authorization": f"Bearer {env['LLM_API_KEY']}"}, + ) + with urllib.request.urlopen(req, timeout=timeout) as resp: + data = json.loads(resp.read().decode("utf-8")) + return data["choices"][0]["message"]["content"] + + +def parse_verdict(text: str) -> Optional[dict]: + m = re.search(r"\{.*\}", text, flags=re.S) + if not m: + return None + try: + obj = json.loads(m.group(0)) + except json.JSONDecodeError: + return None + v = str(obj.get("verdict", "")).strip() + if v not in ("真", "误", "不确定"): + return None + return {"verdict": v, "reason": str(obj.get("reason", ""))[:120]} + + +def label_one(env: Dict[str, str], rec: dict, rules: Dict[str, str], vocab: Dict[str, str]) -> dict: + prompt = build_prompt(rec, rules, vocab) + last_err = "" + for attempt in range(3): + try: + raw = call(env, prompt) + parsed = parse_verdict(raw) + if parsed: + return {"id": rec["id"], "rule": rec["rule"], "word": rec.get("word"), **parsed, "model": env["LLM_MODEL"]} + last_err = f"无法解析:{raw[:80]}" + except (urllib.error.URLError, TimeoutError, KeyError, json.JSONDecodeError) as e: + last_err = repr(e)[:120] + time.sleep(2 * (attempt + 1)) + return {"id": rec["id"], "rule": rec["rule"], "word": rec.get("word"), "verdict": "错误", "reason": last_err, "model": env["LLM_MODEL"]} + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("out_dir") + ap.add_argument("--env", required=True) + ap.add_argument("--workers", type=int, default=8) + ap.add_argument("--limit", type=int, default=0) + args = ap.parse_args() + env = load_env(args.env) + for k in ("LLM_BASE_URL", "LLM_API_KEY", "LLM_MODEL"): + if k not in env: + print(f"缺少 {k}", file=sys.stderr) + return 2 + rules, vocab = load_rules(), load_vocab_rows() + + labels_path = os.path.join(args.out_dir, "labels.jsonl") + done = set() + if os.path.exists(labels_path): + with open(labels_path, encoding="utf-8") as f: + for line in f: + rec = json.loads(line) + if rec.get("verdict") != "错误": + done.add(rec["id"]) + todo = [] + with open(os.path.join(args.out_dir, "sample.jsonl"), encoding="utf-8") as f: + for line in f: + rec = json.loads(line) + if rec["id"] not in done: + todo.append(rec) + if args.limit: + todo = todo[: args.limit] + print(f"已标 {len(done)},待标 {len(todo)}", file=sys.stderr) + + n_ok = n_err = 0 + with open(labels_path, "a", encoding="utf-8") as out, ThreadPoolExecutor(args.workers) as pool: + futures = [pool.submit(label_one, env, rec, rules, vocab) for rec in todo] + for i, fut in enumerate(as_completed(futures), 1): + res = fut.result() + out.write(json.dumps(res, ensure_ascii=False) + "\n") + out.flush() + n_err += res["verdict"] == "错误" + n_ok += res["verdict"] != "错误" + if i % 25 == 0: + print(f" {i}/{len(todo)}(失败 {n_err})", file=sys.stderr) + print(f"完成:成功 {n_ok},失败 {n_err}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.qoder/skills/simplified-technical-chinese/tools/calibration/manifest.py b/.qoder/skills/simplified-technical-chinese/tools/calibration/manifest.py new file mode 100644 index 00000000..c53fcadc --- /dev/null +++ b/.qoder/skills/simplified-technical-chinese/tools/calibration/manifest.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""记录语料目录里每个仓库的来源和提交号,写成 corpora.json,让别人能复现同一份语料。 + +用法: + python3 tools/calibration/manifest.py <语料目录> <输出文件> +""" + +import json +import os +import subprocess +import sys + + +def git(path: str, *args: str) -> str: + try: + return subprocess.check_output(["git", "-C", path, *args], text=True, stderr=subprocess.DEVNULL).strip() + except subprocess.CalledProcessError: + return "" + + +def main(argv) -> int: + if len(argv) != 3: + print(__doc__) + return 2 + root, out = argv[1], argv[2] + corpora = [] + for name in sorted(os.listdir(root)): + path = os.path.join(root, name) + if not os.path.isdir(os.path.join(path, ".git")): + continue + sparse = git(path, "sparse-checkout", "list").split("\n") if git(path, "config", "core.sparseCheckout") == "true" else [] + corpora.append({ + "name": name, + "remote": git(path, "remote", "get-url", "origin"), + "commit": git(path, "rev-parse", "HEAD"), + "date": git(path, "log", "-1", "--format=%cs"), + "paths": [p for p in sparse if p], + }) + with open(out, "w", encoding="utf-8") as f: + json.dump(corpora, f, ensure_ascii=False, indent=1) + for c in corpora: + print(f"{c['name']}: {c['remote']} @ {c['commit'][:7]} ({c['date']}) {' '.join(c['paths'])}") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/.qoder/skills/simplified-technical-chinese/tools/calibration/report.py b/.qoder/skills/simplified-technical-chinese/tools/calibration/report.py new file mode 100644 index 00000000..b1780cc2 --- /dev/null +++ b/.qoder/skills/simplified-technical-chinese/tools/calibration/report.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +"""汇总命中统计和标注结果,算出每条规则、每个词的精确率,生成校准报告草稿。 + +用法: + python3 tools/calibration/report.py <输出目录> + +读取 stats.json、sample.jsonl、labels.jsonl;如果有 labels.human.jsonl(人工复核,同样的 id 和 verdict 字段), +人工结果覆盖模型结果。输出 report.md(表格)和 misses.md(所有判为误报的样本及理由,供修规则用)。 + +精确率 = 真 / (真 + 误),不确定不计入。建议列按精确率给: + ≥ 0.80 保留;0.50–0.79 加条件或降为建议;< 0.50 从脚本移除,规则只留在文本里。 +""" + +import json +import math +import os +import re +import sys +from collections import Counter, defaultdict +from typing import Dict, List, Optional, Tuple + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.normpath(os.path.join(HERE, "..", "..")) + + +def wilson_low(k: int, n: int, z: float = 1.96) -> Optional[float]: + """精确率的 Wilson 区间下界,样本少时提醒别过度相信。""" + if n == 0: + return None + p = k / n + denom = 1 + z * z / n + centre = p + z * z / (2 * n) + margin = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n)) + return max(0.0, (centre - margin) / denom) + + +def rule_titles() -> Dict[str, Tuple[str, str]]: + out: Dict[str, Tuple[str, str]] = {} + with open(os.path.join(ROOT, "规范.md"), encoding="utf-8") as f: + for line in f: + m = re.match(r"### (\d+\.\d+) (.*?)【(必须|建议)】", line) + if m: + out[m.group(1)] = (m.group(2), m.group(3)) + return out + + +def advice(p: Optional[float], n: int) -> str: + if p is None or n == 0: + return "无样本" + if n < 10: + return "样本不足" + if p >= 0.8: + return "保留" + if p >= 0.5: + return "加条件 / 降为建议" + return "从脚本移除" + + +def fmt_p(p: Optional[float]) -> str: + return "–" if p is None else f"{p * 100:.0f}%" + + +def main(argv: List[str]) -> int: + if len(argv) != 2: + print(__doc__) + return 2 + out_dir = argv[1] + with open(os.path.join(out_dir, "stats.json"), encoding="utf-8") as f: + stats = json.load(f) + sample: Dict[str, dict] = {} + with open(os.path.join(out_dir, "sample.jsonl"), encoding="utf-8") as f: + for line in f: + rec = json.loads(line) + sample[rec["id"]] = rec + labels: Dict[str, dict] = {} + for name in ("labels.jsonl", "labels.human.jsonl"): + path = os.path.join(out_dir, name) + if not os.path.exists(path): + continue + with open(path, encoding="utf-8") as f: + for line in f: + rec = json.loads(line) + if rec.get("verdict") in ("真", "误", "不确定"): + labels[rec["id"]] = {**rec, "source": "人工" if "human" in name else "模型"} + + titles = rule_titles() + total_chars = sum(c["chars"] for c in stats["corpora"].values()) or 1 + + by_rule: Dict[str, Counter] = defaultdict(Counter) + by_word: Dict[str, Counter] = defaultdict(Counter) + misses: Dict[str, List[dict]] = defaultdict(list) + for sid, rec in sample.items(): + lab = labels.get(sid) + if not lab: + by_rule[rec["rule"]]["未标"] += 1 + continue + by_rule[rec["rule"]][lab["verdict"]] += 1 + if rec.get("word"): + by_word[rec["word"]][lab["verdict"]] += 1 + if lab["verdict"] == "误": + misses[rec["rule"]].append({**rec, "reason": lab["reason"], "source": lab["source"]}) + + md = ["# 校准报告(草稿,由 tools/calibration/report.py 生成)", ""] + md += ["## 语料", "", "| 语料库 | 文件 | 句子 | 字数 | 命中 | 每万字命中 |", "| --- | ---: | ---: | ---: | ---: | ---: |"] + for c, s in stats["corpora"].items(): + md.append(f"| {c} | {s['files']} | {s['sentences']} | {s['chars']} | {s['findings']} | {s['findings_per_10k_chars']} |") + md.append(f"| 合计 | {sum(s['files'] for s in stats['corpora'].values())} | {sum(s['sentences'] for s in stats['corpora'].values())} | {total_chars} | {stats['total_findings']} | {stats['total_findings'] / total_chars * 10000:.1f} |") + + md += ["", "## 每条规则", "", "精确率 = 真 / (真 + 误)。括号里是 95% 置信区间下界。", "", + "| 规则 | 名称 | 级别 | 命中 | 每万字 | 抽样 | 真 | 误 | 不确定 | 精确率 | 估计误报/万字 | 建议 |", + "| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |"] + rules = sorted(stats["rules"], key=lambda r: -stats["rules"][r]["total"]) + for r in rules: + c = by_rule.get(r, Counter()) + k, n = c["真"], c["真"] + c["误"] + p = k / n if n else None + low = wilson_low(k, n) + hits = stats["rules"][r]["total"] + per10k = hits / total_chars * 10000 + fp = per10k * (1 - p) if p is not None else None + title, level = titles.get(r, ("?", "?")) + md.append(f"| {r} | {title} | {level} | {hits} | {per10k:.1f} | {sum(c.values())} | {c['真']} | {c['误']} | {c['不确定']} | " + f"{fmt_p(p)}{'' if low is None else f' ({fmt_p(low)})'} | {'–' if fp is None else f'{fp:.1f}'} | {advice(p, n)} |") + + md += ["", "## 词表条目(按命中数排序,只列有样本的词)", "", + "| 词 | 规则 | 命中 | 每万字 | 抽样 | 真 | 误 | 精确率 | 建议 |", "| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |"] + words = sorted(stats["words"], key=lambda w: -stats["words"][w]["total"]) + for w in words: + c = by_word.get(w) + if not c: + continue + k, n = c["真"], c["真"] + c["误"] + p = k / n if n else None + info = stats["words"][w] + md.append(f"| {w} | {info['rule']} | {info['total']} | {info['total'] / total_chars * 10000:.2f} | {sum(c.values())} | {c['真']} | {c['误']} | {fmt_p(p)} | {advice(p, n)} |") + + limits = stats.get("limits", {"操作": "?", "描述": "?"}) + md += ["", "## 句长分布(字)", "", f"| 语料库 | 句型 | n | 均值 | p50 | p90 | p95 | p99 | 超过上限(操作 {limits['操作']} / 描述 {limits['描述']}) |", + "| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |"] + for c in list(stats["corpora"]) + ["__all__"]: + for k in ("操作", "描述"): + p = stats["sentence_length"].get(c, {}).get(k) or {} + if not p: + continue + over = stats["over_limit"][c][k] + md.append(f"| {'合计' if c == '__all__' else c} | {k} | {p['n']} | {p['mean']} | {p['p50']} | {p['p90']} | {p['p95']} | {p['p99']} | {over}% |") + + n_lab = sum(sum(c[v] for v in ("真", "误", "不确定")) for c in by_rule.values()) + n_human = sum(1 for l in labels.values() if l["source"] == "人工") + md += ["", f"样本 {len(sample)} 条,已标 {n_lab} 条,其中人工复核 {n_human} 条。", ""] + with open(os.path.join(out_dir, "report.md"), "w", encoding="utf-8") as f: + f.write("\n".join(md)) + + lines = ["# 判为误报的样本", ""] + for r in rules: + if not misses.get(r): + continue + title = titles.get(r, ("?", "?"))[0] + lines += [f"## {r} {title}({len(misses[r])} 条)", ""] + for m in misses[r]: + w = f"「{m['word']}」" if m.get("word") else "" + lines.append(f"- {w}{m['text']} \n 理由:{m['reason']}({m['source']};{m['corpus']} {m['path']}:{m['line']})") + lines.append("") + with open(os.path.join(out_dir, "misses.md"), "w", encoding="utf-8") as f: + f.write("\n".join(lines)) + print(f"report.md 和 misses.md 已写入 {out_dir}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/.qoder/skills/simplified-technical-chinese/tools/calibration/sample.py b/.qoder/skills/simplified-technical-chinese/tools/calibration/sample.py new file mode 100644 index 00000000..db8983cf --- /dev/null +++ b/.qoder/skills/simplified-technical-chinese/tools/calibration/sample.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""从 findings.jsonl 分层抽样,生成待标注的样本。 + +用法: + python3 tools/calibration/sample.py <语料目录> <输出目录> [--per-rule 50] [--per-word 8] [--seed 1] + +每条规则最多抽 per-rule 条。词表类规则先按词分层,每个词最多 per-word 条,再在语料库之间轮流取, +让样本覆盖不同的词和不同的文档风格。每条样本附上命中行前后各一行原文作为上下文。 +输出 sample.jsonl。 +""" + +import argparse +import json +import os +import random +import sys +from collections import defaultdict +from typing import Dict, List + +WORD_RULES = {"1.1", "1.3", "1.4", "1.5", "1.7", "2.3", "3.5", "4.1", "5.2", "5.4", "3.3"} + + +def round_robin(groups: Dict[str, List[dict]], limit: int, rng: random.Random) -> List[dict]: + """在各组之间轮流取,直到取满 limit 条或取完。""" + for g in groups.values(): + rng.shuffle(g) + out: List[dict] = [] + keys = sorted(groups) + i = 0 + while len(out) < limit and any(groups.values()): + k = keys[i % len(keys)] + if groups[k]: + out.append(groups[k].pop()) + i += 1 + return out + + +def context(corpus_root: str, rec: dict, width: int = 1) -> List[str]: + path = os.path.join(corpus_root, rec["corpus"], rec["path"]) + try: + with open(path, encoding="utf-8") as f: + lines = f.read().splitlines() + except OSError: + return [] + lo = max(0, rec["line"] - 1 - width) + hi = min(len(lines), rec["line"] + width) + return [l[:300] for l in lines[lo:hi]] + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("corpus_root") + ap.add_argument("out_dir") + ap.add_argument("--per-rule", type=int, default=50) + ap.add_argument("--per-word", type=int, default=8) + ap.add_argument("--seed", type=int, default=1) + args = ap.parse_args() + rng = random.Random(args.seed) + + by_rule: Dict[str, List[dict]] = defaultdict(list) + with open(os.path.join(args.out_dir, "findings.jsonl"), encoding="utf-8") as f: + for line in f: + rec = json.loads(line) + by_rule[rec["rule"]].append(rec) + + sample: List[dict] = [] + for rule in sorted(by_rule): + recs = by_rule[rule] + if rule in WORD_RULES: + # 先按词分层:每个词最多 per-word 条,词内按语料库轮流 + by_word: Dict[str, Dict[str, List[dict]]] = defaultdict(lambda: defaultdict(list)) + for r in recs: + by_word[r.get("word") or "?"][r["corpus"]].append(r) + per_word = {w: round_robin(groups, args.per_word, rng) for w, groups in by_word.items()} + picked = round_robin({w: v for w, v in per_word.items()}, args.per_rule, rng) + else: + by_corpus: Dict[str, List[dict]] = defaultdict(list) + for r in recs: + by_corpus[r["corpus"]].append(r) + picked = round_robin(by_corpus, args.per_rule, rng) + for r in picked: + r["context"] = context(args.corpus_root, r) + sample.extend(sorted(picked, key=lambda r: r["id"])) + print(f"{rule}: 命中 {len(recs)},抽 {len(picked)}", file=sys.stderr) + + with open(os.path.join(args.out_dir, "sample.jsonl"), "w", encoding="utf-8") as f: + for r in sample: + f.write(json.dumps(r, ensure_ascii=False) + "\n") + print(f"共抽 {len(sample)} 条", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.qoder/skills/simplified-technical-chinese/tools/calibration/stats.py b/.qoder/skills/simplified-technical-chinese/tools/calibration/stats.py new file mode 100644 index 00000000..10f3ab2d --- /dev/null +++ b/.qoder/skills/simplified-technical-chinese/tools/calibration/stats.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +"""在真实语料上运行检查脚本,统计每条规则、每个词的命中数和句长分布。 + +用法: + python3 tools/calibration/stats.py <语料目录> <输出目录> + +语料目录下每个子目录是一个语料库(一个文档仓库)。只统计中文为主的 Markdown 文件。 +输出: + findings.jsonl 每条命中一行,带语料库、路径、行号、规则、词 + sentences.jsonl 每个句子一行,带句型(操作/描述)、字数、「的」数 + stats.json 汇总数字 + stats.md 汇总表,可直接贴进校准报告 +""" + +import json +import os +import re +import sys +from collections import Counter, defaultdict +from typing import Dict, Iterator, List, Optional + +HERE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, os.path.join(HERE, "..")) +import check # noqa: E402 + +SKIP_DIRS = {".git", "node_modules", ".github", ".vitepress", "public", "_template"} +MIN_CJK = 100 # 少于这个数的汉字按非中文文件处理 +WORD_IN_MESSAGE = re.compile(r"「(.+?)」") + + +PROSE_NOISE = [ + (re.compile(r"\A---\n.*?\n---\n", re.S), ""), # front matter + (re.compile(r"", re.S), ""), # HTML 注释(k8s 中文文档用它保留英文原文) + (re.compile(r"```.*?```", re.S), ""), # 代码块 + (re.compile(r"`[^`\n]*`"), ""), # 行内代码 + (re.compile(r"https?://\S+"), ""), # 链接 + (re.compile(r"<[^>\n]+>"), ""), # HTML 标签 + (re.compile(r"\{\{.*?\}\}", re.S), ""), # Hugo / 模板短代码 +] + + +def is_chinese_doc(text: str) -> bool: + """正文(去掉代码、注释、链接、标签后)以中文为主的文件才算语料。""" + for pat, rep in PROSE_NOISE: + text = pat.sub(rep, text) + cjk = len(check.CJK.findall(text)) + latin = len(re.findall(r"[A-Za-z]", text)) + return cjk >= MIN_CJK and cjk * 2 >= latin + + +def md_files(root: str) -> Iterator[str]: + for dirpath, dirnames, filenames in os.walk(root): + dirnames[:] = sorted(d for d in dirnames if d not in SKIP_DIRS) + for name in sorted(filenames): + if not name.endswith(".md"): + continue + # ant-design 这类双语仓库:只取中文版 + if re.search(r"\.(en-US|en)\.md$", name): + continue + yield os.path.join(dirpath, name) + + +def sentences_of(text: str, checker: check.Checker) -> Iterator[dict]: + """按检查脚本同样的切分和句型判断产出句子。""" + units = check.parse_units(text.splitlines()) + in_counterexample = False + for unit in units: + if not unit.indented: + in_counterexample = check.is_counterexample(unit.lines[0]) + if in_counterexample or unit.skip or unit.kind in ("heading", "table"): + continue + for pos, (s, line_no) in enumerate(check.unit_sentences(unit)): + n = check.count_chars(s) + if n == 0: + continue + yield { + "line": line_no, + "unit": unit.kind, + "kind": "操作" if checker.is_operation(unit, s, pos) else "描述", + "chars": n, + "de": s.count("的"), + "text": s[:80], + } + + +def percentiles(values: List[int], points=(50, 75, 90, 95, 99)) -> Dict[str, float]: + if not values: + return {} + xs = sorted(values) + out = {} + for p in points: + k = max(0, min(len(xs) - 1, round(p / 100 * len(xs) + 0.5) - 1)) + out[f"p{p}"] = xs[k] + out["max"] = xs[-1] + out["n"] = len(xs) + out["mean"] = round(sum(xs) / len(xs), 1) + return out + + +def word_of(f: check.Finding) -> Optional[str]: + if f.rule in ("1.1", "1.3", "1.4", "1.5", "1.7", "2.3", "3.5", "4.1", "5.2", "5.4", "3.3"): + m = WORD_IN_MESSAGE.search(f.message) + return m.group(1) if m else None + return None + + +def main(argv: List[str]) -> int: + if len(argv) != 3: + print(__doc__) + return 2 + corpus_root, out_dir = argv[1], argv[2] + os.makedirs(out_dir, exist_ok=True) + checker = check.Checker(check.load_vocab()) + + corpora = sorted(d for d in os.listdir(corpus_root) if os.path.isdir(os.path.join(corpus_root, d))) + per_corpus: Dict[str, dict] = {} + rule_counts: Dict[str, Counter] = defaultdict(Counter) # rule -> corpus -> n + word_counts: Dict[str, Counter] = defaultdict(Counter) # word -> corpus -> n + word_rule: Dict[str, str] = {} + lengths: Dict[str, Dict[str, List[int]]] = defaultdict(lambda: {"操作": [], "描述": []}) + de_counts: Dict[str, List[int]] = defaultdict(list) + n_findings = 0 + + with open(os.path.join(out_dir, "findings.jsonl"), "w", encoding="utf-8") as ff, \ + open(os.path.join(out_dir, "sentences.jsonl"), "w", encoding="utf-8") as sf: + for corpus in corpora: + root = os.path.join(corpus_root, corpus) + stat = {"files": 0, "skipped_files": 0, "sentences": 0, "chars": 0, "findings": 0, "must": 0} + for path in md_files(root): + try: + with open(path, encoding="utf-8") as f: + text = f.read() + except (UnicodeDecodeError, OSError): + stat["skipped_files"] += 1 + continue + if not is_chinese_doc(text): + stat["skipped_files"] += 1 + continue + rel = os.path.relpath(path, root) + stat["files"] += 1 + for s in sentences_of(text, checker): + stat["sentences"] += 1 + stat["chars"] += s["chars"] + lengths[corpus][s["kind"]].append(s["chars"]) + lengths["__all__"][s["kind"]].append(s["chars"]) + de_counts[corpus].append(s["de"]) + s.update({"corpus": corpus, "path": rel}) + sf.write(json.dumps(s, ensure_ascii=False) + "\n") + for i, fd in enumerate(checker.check_text(text, rel)): + w = word_of(fd) + if w: + word_counts[w][corpus] += 1 + word_rule[w] = fd.rule + rule_counts[fd.rule][corpus] += 1 + stat["findings"] += 1 + stat["must"] += fd.level == check.MUST + n_findings += 1 + rec = { + "id": f"{corpus}:{rel}:{fd.line}:{fd.rule}:{i}", + "corpus": corpus, "path": rel, "line": fd.line, "rule": fd.rule, + "level": fd.level, "word": w, "message": fd.message, "text": fd.text, + } + ff.write(json.dumps(rec, ensure_ascii=False) + "\n") + stat["findings_per_10k_chars"] = round(stat["findings"] / stat["chars"] * 10000, 1) if stat["chars"] else None + per_corpus[corpus] = stat + print(f"{corpus}: {stat['files']} 个文件,{stat['sentences']} 句,{stat['chars']} 字,{stat['findings']} 条命中", file=sys.stderr) + + rules_sorted = sorted(rule_counts, key=lambda r: -sum(rule_counts[r].values())) + words_sorted = sorted(word_counts, key=lambda w: -sum(word_counts[w].values())) + stats = { + "corpora": per_corpus, + "total_findings": n_findings, + "rules": {r: {"total": sum(c.values()), "by_corpus": dict(c)} for r, c in rule_counts.items()}, + "words": {w: {"rule": word_rule[w], "total": sum(c.values()), "by_corpus": dict(c)} for w, c in word_counts.items()}, + "sentence_length": { + c: {k: percentiles(v) for k, v in kinds.items()} for c, kinds in lengths.items() + }, + "limits": {"操作": checker.max_op, "描述": checker.max_desc}, + "over_limit": { + c: { + "操作": round(sum(x > checker.max_op for x in kinds["操作"]) / len(kinds["操作"]) * 100, 1) if kinds["操作"] else None, + "描述": round(sum(x > checker.max_desc for x in kinds["描述"]) / len(kinds["描述"]) * 100, 1) if kinds["描述"] else None, + } for c, kinds in lengths.items() + }, + "de_per_sentence": {c: percentiles(v) for c, v in de_counts.items()}, + } + with open(os.path.join(out_dir, "stats.json"), "w", encoding="utf-8") as f: + json.dump(stats, f, ensure_ascii=False, indent=1) + + total_chars = sum(s["chars"] for s in per_corpus.values()) or 1 + md = ["## 语料", "", "| 语料库 | 文件 | 句子 | 字数 | 命中 | 每万字命中 |", "| --- | ---: | ---: | ---: | ---: | ---: |"] + for c, s in per_corpus.items(): + md.append(f"| {c} | {s['files']} | {s['sentences']} | {s['chars']} | {s['findings']} | {s['findings_per_10k_chars']} |") + md += ["", "## 每条规则的命中", "", "| 规则 | 命中 | 每万字 | " + " | ".join(corpora) + " |", + "| --- | ---: | ---: | " + " | ".join("---:" for _ in corpora) + " |"] + for r in rules_sorted: + c = rule_counts[r] + md.append(f"| {r} | {sum(c.values())} | {sum(c.values()) / total_chars * 10000:.1f} | " + " | ".join(str(c.get(x, 0)) for x in corpora) + " |") + md += ["", "## 命中最多的词(前 60)", "", "| 词 | 规则 | 命中 | 每万字 |", "| --- | --- | ---: | ---: |"] + for w in words_sorted[:60]: + c = word_counts[w] + md.append(f"| {w} | {word_rule[w]} | {sum(c.values())} | {sum(c.values()) / total_chars * 10000:.2f} |") + md += ["", "## 句长分布(字)", "", f"| 语料库 | 句型 | n | 均值 | p50 | p75 | p90 | p95 | p99 | 最长 | 超过上限(操作 {checker.max_op} / 描述 {checker.max_desc}) |", + "| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |"] + for c in corpora + ["__all__"]: + for k in ("操作", "描述"): + p = stats["sentence_length"].get(c, {}).get(k) or {} + if not p: + continue + over = stats["over_limit"][c][k] + md.append(f"| {c} | {k} | {p['n']} | {p['mean']} | {p['p50']} | {p['p75']} | {p['p90']} | {p['p95']} | {p['p99']} | {p['max']} | {over}% |") + with open(os.path.join(out_dir, "stats.md"), "w", encoding="utf-8") as f: + f.write("\n".join(md) + "\n") + print(f"共 {n_findings} 条命中,写入 {out_dir}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/.qoder/skills/simplified-technical-chinese/tools/check.py b/.qoder/skills/simplified-technical-chinese/tools/check.py new file mode 100644 index 00000000..2bc38889 --- /dev/null +++ b/.qoder/skills/simplified-technical-chinese/tools/check.py @@ -0,0 +1,660 @@ +#!/usr/bin/env python3 +"""简明技术中文检查脚本。 + +零依赖,Python 3.9 及以上。读取 Markdown 或纯文本,按《简明技术中文规范》 +和《词表》检查,输出每条发现的行号、规则号、级别和说明。 + +用法: + python3 tools/check.py 文件.md [更多文件...] + python3 tools/check.py --strict 文件.md # 有【必须】级别的发现时退出码为 1 + python3 tools/check.py --json 文件.md # JSON 输出 + python3 tools/check.py --kind 描述 回答.md # 把全文按描述句检查(例如聊天回答) + cat 回答.md | python3 tools/check.py - + +脚本只做句法层面的检查,不判断事实是否正确。所有发现都需要人工确认。 +""" + +import argparse +import bisect +import json +import os +import re +import sys +from dataclasses import dataclass, asdict +from typing import Dict, List, Optional + +HERE = os.path.dirname(os.path.abspath(__file__)) +DEFAULT_VOCAB = os.path.normpath(os.path.join(HERE, "..", "词表.md")) + +MUST = "必须" +SHOULD = "建议" + +CJK = re.compile(r"[㐀-䶿一-鿿豈-﫿]") +LATIN_TOKEN = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.+\-/]*") +ORDERED_ITEM = re.compile(r"^(\s*)(\d+)(?:[.)]\s+|[、)]\s*)(.*)$") +BULLET_ITEM = re.compile(r"^(\s*)[-*+]\s+(.*)$") +SENTENCE_SPLIT = re.compile(r"(?<=[。!?])(?![」』”’))])") + +# 系统行为句的常见主语。编号步骤里以这些词开头的句子不按操作句检查(规则 4.4)。 +SYSTEM_SUBJECTS = ("系统", "服务", "接口", "程序", "页面", "浏览器", "终端", "客户端", "服务端", "命令", "脚本") +# 编号列表项里以这些词开头的句子是说明,不是操作:指代、举例、原因、定义。校准发现编号列表常用来列举说明。 +# 条件词(如果、当、在……之后)开头的句子仍算操作句:规则 2.5 要求条件写在动作前面。 +NON_OPERATION_STARTS = ("对于", "由于", "因为", "例如", "比如", "这", "该", "此", "其", "它", "我们", + "注意", "说明", "即", "也就是", "每", "所有", "任何") + + +@dataclass +class Finding: + path: str + line: int + rule: str + level: str + message: str + text: str + + def format(self) -> str: + return f"{self.path}:{self.line}: [{self.rule}][{self.level}] {self.message} ←「{self.text}」" + + +@dataclass +class Unit: + """一个检查单元:一个段落、一个列表项、一个标题或一行表格。""" + + kind: str # para | ordered | bullet | heading | table | quote + lines: List[str] + start_line: int + indented: bool = False # 首行有缩进。缩进的单元跟随前一个顶层单元的反例状态 + skip: bool = False # 前面有 指令 + + +# --------------------------------------------------------------------------- +# 词表 +# --------------------------------------------------------------------------- + +# 依赖语境的词用这里的正则替代简单匹配。键是词表中的词。 +PATTERNS: Dict[str, str] = { + "进行": r"进行(?!中|时|曲|到底|了?\s*[A-Za-z])", + "相关": r"相关(?!性|系数|联)", + "等": r"(?:、[^、,。;:\n]{1,16}|(?:和|及|以及)[^,。;\n]{1,16})(? Dict[str, List[List[str]]]: + sections: Dict[str, List[List[str]]] = {} + current: Optional[str] = None + for raw in md.splitlines(): + line = raw.strip() + if line.startswith("## "): + current = line[3:].strip() + sections[current] = [] + continue + if current is None or not line.startswith("|"): + continue + cells = [c.strip() for c in line.strip("|").split("|")] + if all(re.fullmatch(r":?-{3,}:?", c) for c in cells): + continue + sections[current].append(cells) + return sections + + +def load_vocab(path: str = DEFAULT_VOCAB) -> List[VocabEntry]: + """读取词表,返回标为「自动检查:是」的条目。""" + with open(path, encoding="utf-8") as f: + sections = _parse_tables(f.read()) + entries: List[VocabEntry] = [] + for title, rows in sections.items(): + if "第一部分" in title: + for cells in rows[1:]: + if len(cells) < 4 or cells[3] != "是": + continue + words = [w for w in cells[0].split("、") if w] + for w in words: + entries.append(VocabEntry([w], cells[1], cells[2], RULE_OF_WORD.get(w, "1.3"), "banned")) + elif "第二部分" in title: + for cells in rows[1:]: + if len(cells) < 4 or cells[3] != "是": + continue + rejected = [w for w in cells[2].split("、") if w] + for w in rejected: + entries.append(VocabEntry([w], cells[1], cells[0], "1.1", "choice")) + return entries + + +# --------------------------------------------------------------------------- +# 文本预处理 +# --------------------------------------------------------------------------- + +COUNTEREXAMPLE_PREFIXES = ("不批准", "不推荐", "反例", "错误示例", "改写前") + + +def strip_markup(line: str) -> str: + """去掉 Markdown 标记。行内代码记为 CODE,「」里的内容记为 Q。 + + 「」里的内容是界面文字或被引用的词,按原样保留,不检查。 + """ + line = re.sub(r"`[^`]*`", " CODE ", line) + line = re.sub(r"「[^」]*」", "「Q」", line) + line = re.sub(r"!\[[^\]]*\]\([^)]*\)", " ", line) + line = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", line) + line = re.sub(r"https?://\S+", " URL ", line) + line = re.sub(r"<[^>]+>", " ", line) + line = re.sub(r"(\*\*|~~|\*)(?=\S)", "", line) + line = re.sub(r"(?<=\S)(\*\*|~~|\*)", "", line) + return line + + +def is_counterexample(text: str) -> bool: + """以「不批准:」这类前缀开头的行是反例,默认不检查。""" + return text.lstrip("-*+ \t").startswith(COUNTEREXAMPLE_PREFIXES) + + +def count_chars(text: str) -> int: + """按附录 A 计算字数。""" + text = re.sub(r"`[^`]*`", " CODE ", text) + return len(CJK.findall(text)) + len(LATIN_TOKEN.findall(text)) + + +def join_lines(lines: List[str]) -> tuple: + """把一个单元的多行拼成一段。相邻两行都是西文时补一个空格。返回拼接结果和每行的起始偏移。""" + joined = "" + starts: List[int] = [] + for line in lines: + if joined and re.search(r"[A-Za-z0-9]$", joined) and re.match(r"[A-Za-z0-9]", line): + joined += " " + starts.append(len(joined)) + joined += line + return joined, starts + + +def split_sentences(text: str) -> List[str]: + parts = SENTENCE_SPLIT.split(text) + return [p.strip() for p in parts if p and p.strip()] + + +def unit_sentences(unit: Unit) -> List[tuple]: + """一个单元里的句子和各自的起始行号:(句子, 行号)。 + + 多行先拼成一段再分句:Markdown 里的硬换行不是句子边界。 + """ + joined, line_starts = join_lines([strip_markup(l) for l in unit.lines]) + out: List[tuple] = [] + at = 0 + for piece in SENTENCE_SPLIT.split(joined): + lead = len(piece) - len(piece.lstrip()) + s = piece.strip() + if s: + out.append((s, unit.start_line + bisect.bisect_right(line_starts, at + lead) - 1)) + at += len(piece) + return out + + +DIRECTIVE = re.compile(r"^\s*\s*$") + + +def parse_units(lines: List[str]) -> List[Unit]: + """把文本切成检查单元。 + + 跳过:代码块、文件开头的 YAML front matter、`` 到 + `` 之间的内容。`` 跳过它后面的一个单元。 + """ + units: List[Unit] = [] + in_fence = False + off = False + skip_next = False + current: Optional[Unit] = None + + def close() -> None: + nonlocal current + if current is not None: + units.append(current) + current = None + + def start(kind: str, text: str, idx: int, raw: str) -> Unit: + nonlocal skip_next + unit = Unit(kind, [text], idx, indented=raw[:1] in (" ", "\t"), skip=skip_next) + skip_next = False + return unit + + start_at = 0 + if lines and lines[0].strip() == "---": + for i in range(1, len(lines)): + if lines[i].strip() == "---": + start_at = i + 1 + break + + in_comment = False + for idx, raw in enumerate(lines[start_at:], start=start_at + 1): + if in_comment: + if "-->" not in raw: + continue + in_comment = False + raw = raw.split("-->", 1)[1] + d = DIRECTIVE.match(raw) + if d is None and " 为止都跳过 + raw = re.sub(r"", "", raw) + if "