From a2e1ab05dd4d45c789eab39ce595634f5d0f03f7 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Sun, 20 Sep 2026 16:52:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(tools/docs):=20=E6=96=87=E6=A1=A3=E4=B8=80?= =?UTF-8?q?=E8=87=B4=E6=80=A7=E4=B8=89=E9=A1=B9=E5=9F=BA=E5=BB=BA=E6=89=B9?= =?UTF-8?q?=EF=BC=882026-09-20=20=E7=94=A8=E6=88=B7=E6=8B=8D=E6=9D=BF?= =?UTF-8?q?=E4=B8=89=E9=80=89=E5=85=A8=E5=81=9A=EF=BC=8C=E4=B8=89=E5=AD=90?= =?UTF-8?q?=20Agent=20=E5=B9=B6=E8=A1=8C+=E4=B8=BB=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E6=94=B6=E5=8F=A3=EF=BC=89=E2=80=94=E2=80=94=E2=91=A0doc=5Flin?= =?UTF-8?q?k=5Faudit.py=20=E9=93=BE=E6=8E=A5=E5=81=A5=E5=BA=B7=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=EF=BC=88226=20=E8=A1=8C=EF=BC=9Amarkdown=20=E7=9B=B8?= =?UTF-8?q?=E5=AF=B9=E9=93=BE=E6=8E=A5=E7=9B=B8=E5=AF=B9=E6=89=80=E5=9C=A8?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E8=A7=A3=E6=9E=90+=E5=8F=8D=E5=BC=95?= =?UTF-8?q?=E5=8F=B7=E8=B7=AF=E5=BE=84=E5=BD=A2=E5=A4=9A=E5=9F=BA=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E3=80=94=E6=89=80=E5=9C=A8=E7=9B=AE=E5=BD=95=E2=86=92?= =?UTF-8?q?=E4=BB=93=E5=BA=93=E6=A0=B9=E2=86=92design=E2=86=92slice?= =?UTF-8?q?=E3=80=95+=E4=B8=AD=E6=96=87=E9=94=9A=20GFM=20=E8=A7=84?= =?UTF-8?q?=E5=88=99=E5=91=BD=E4=B8=AD+--known=20=E7=99=BD=E5=90=8D?= =?UTF-8?q?=E5=8D=95/--strict=20=E9=97=A8=E7=A6=81/--json=EF=BC=9B?= =?UTF-8?q?=E8=87=AA=E6=B5=8B=2016/16=EF=BC=9B=E5=85=A8=E4=BB=93=20771=20?= =?UTF-8?q?=E9=93=BE=E6=8E=A5=E6=89=AB=E5=87=BA=2011=20=E7=9C=9F=E6=AD=BB?= =?UTF-8?q?=E9=93=BE=200=20=E8=AF=AF=E6=8A=A5=EF=BC=89=EF=BC=9B=E2=91=A1gd?= =?UTF-8?q?d=5Flint=20=E6=96=B0=E5=A2=9E=20R10=20=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E7=89=88=E6=9C=AC=E5=8F=B7=E5=94=AF=E4=B8=80?= =?UTF-8?q?=E6=80=A7=EF=BC=88=E7=89=88=E6=9C=AC=E5=8E=86=E5=8F=B2=E8=A1=A8?= =?UTF-8?q?=E9=A6=96=E5=88=97=E6=9F=A5=E9=87=8D=EF=BC=89+R11=20=E8=AE=A1?= =?UTF-8?q?=E6=95=B0=E8=AF=8D=E6=BC=82=E7=A7=BB=E5=AF=B9=E7=85=A7=EF=BC=88?= =?UTF-8?q?=E9=94=9A=E8=A1=A8=E6=8C=87=E8=B7=AF=20registry:359/:314/:327?= =?UTF-8?q?=EF=BC=8C=E7=89=88=E6=9C=AC=E5=8E=86=E5=8F=B2=E8=8A=82+?= =?UTF-8?q?=E8=BD=A8=E8=BF=B9=E8=AF=8D=E8=B1=81=E5=85=8D=EF=BC=8Cyaml=20#?= =?UTF-8?q?=20=E6=B3=A8=E9=87=8A=E5=89=A5=E7=A6=BB=EF=BC=89=E2=80=94?= =?UTF-8?q?=E2=80=94R10=20=E4=B8=8A=E7=BA=BF=E5=8D=B3=E6=8A=93=E5=88=B0?= =?UTF-8?q?=E6=88=98=E6=96=97=E7=BB=93=E7=AE=97=20v0.16=20=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E6=92=9E=E5=8F=B7=EF=BC=88=E5=85=85=E8=83=BD=E9=80=80?= =?UTF-8?q?=E5=BD=B9=E6=89=B9=20vs=20=E7=94=B2=E6=95=88=E7=9F=A9=E9=98=B5?= =?UTF-8?q?=E6=89=B9=EF=BC=89R11=20=E6=8A=93=E5=88=B0=2006=20=E4=B8=83?= =?UTF-8?q?=E5=9E=8B=C3=974/interaction-patterns=2015=20=E7=B1=BB=C3=972?= =?UTF-8?q?=EF=BC=9B=E2=91=A2=E5=AD=98=E9=87=8F=E6=B8=85=E7=90=86=3D06=20?= =?UTF-8?q?=E4=B8=83=E5=9E=8B=C3=974=E2=86=92=E5=85=AB=E5=9E=8B=EF=BC=88?= =?UTF-8?q?=E5=90=AB=E6=9E=9A=E4=B8=BE=E8=A1=A5=E7=AC=AC8=E5=9E=8B+?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=8F=B7=E6=88=90=E5=AF=B9=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=EF=BC=89+interaction-patterns=2015=20=E7=B1=BB=C3=972=E2=86=92?= =?UTF-8?q?9=20=E7=B1=BB+02-=E7=B3=BB=E7=BB=9F=E6=B8=85=E5=8D=95=20archive?= =?UTF-8?q?=20=E6=AD=BB=E9=93=BE=E9=99=8D=E7=BA=AF=E6=96=87=E5=AD=97+?= =?UTF-8?q?=E6=88=98=E6=96=97=E7=BB=93=E7=AE=97=20v0.16=20=E6=92=9E?= =?UTF-8?q?=E5=8F=B7=E9=A1=BA=E5=BB=B6=20v0.22=EF=BC=88=E5=90=8E=E8=90=BD?= =?UTF-8?q?=E7=B1=8D=E8=80=85=E9=81=BF=E8=AE=A9=EF=BC=8C=E7=85=A7=20v0.19?= =?UTF-8?q?=20=E5=85=88=E4=BE=8B=EF=BC=89+=E5=9C=B0=E5=BD=A2=E5=8F=98?= =?UTF-8?q?=E5=BC=82=E6=88=98=E6=96=97=E7=BB=93=E7=AE=97=E9=93=BE=E6=8E=A5?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E7=BA=A0=E6=AD=A3=C3=972+06=20=E5=BE=85?= =?UTF-8?q?=E5=8A=9E=E8=87=AA=E6=8C=87=E9=93=BE=E6=8E=A5=E5=A4=9A=E9=80=80?= =?UTF-8?q?=E4=B8=80=E7=BA=A7=E4=BF=AE=E6=AD=A3+8=20=E5=A4=84=E6=9C=AA?= =?UTF-8?q?=E5=BB=BA=20UX=20=E6=96=87=E6=A1=A3=E5=BC=95=E7=94=A8=E5=85=A5?= =?UTF-8?q?=20known=20=E7=99=BD=E5=90=8D=E5=8D=95=EF=BC=88player-journey/n?= =?UTF-8?q?ode-reward/accessibility-requirements/level-clear=20=E6=AC=A0?= =?UTF-8?q?=E8=B4=A6=EF=BC=89=EF=BC=9B=E9=AA=8C=E8=AF=81=3Ddoc=5Flink=5Fau?= =?UTF-8?q?dit=20--strict=20PASS=EF=BC=88771=20=E9=93=BE=E6=8E=A5=200=20?= =?UTF-8?q?=E6=AD=BB=E9=93=BE=207=20=E5=B7=B2=E7=9F=A5=EF=BC=89+R10/R11=20?= =?UTF-8?q?=E5=85=A8=E4=BB=93=200=20=E5=91=BD=E4=B8=AD+R1-R9=20=E4=B8=8E?= =?UTF-8?q?=20master=20=E5=9F=BA=E7=BA=BF=E4=B8=80=E8=87=B4=EF=BC=888=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=AD=98=E9=87=8F=EF=BC=89+doc=5Fterm=5Faudi?= =?UTF-8?q?t=20=E7=96=91=E4=BC=BC=203=20=E7=95=99=E6=A1=A3=E4=B8=8D?= =?UTF-8?q?=E5=8F=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/gdd/02-系统清单.md | 2 +- design/gdd/06-待办清单.md | 8 +- design/gdd/systems/战斗循环/战斗结算.md | 2 +- design/gdd/systems/排雷循环/地形变异.md | 4 +- design/ux/interaction-patterns.md | 4 +- tools/doc_link_audit.py | 226 ++++++++++++++++++++++++ tools/doc_link_known.txt | 4 + tools/gdd_lint.py | 87 ++++++++- 8 files changed, 319 insertions(+), 18 deletions(-) create mode 100644 tools/doc_link_audit.py create mode 100644 tools/doc_link_known.txt diff --git a/design/gdd/02-系统清单.md b/design/gdd/02-系统清单.md index f1f88e48..20ad7640 100644 --- a/design/gdd/02-系统清单.md +++ b/design/gdd/02-系统清单.md @@ -88,7 +88,7 @@ > 四个循环本身的英文代号为 `MINESWEEPER_CYCLE`(排雷循环)、`COMBAT_CYCLE`(战斗循环)、`ADVENTURE_CYCLE`(冒险循环)、`PRESENTATION_SUPPORT`(表现与支撑)。 -> 旧版参考:`../archive/gdd-2026-09-14/systems-index.md` +> 旧版参考:旧骨架系统索引 systems-index.md(gdd-2026-09-14 快照,已封存;封存归档目录待归位——2026-09-20 链接审计发现 design/archive 目录缺失,不再作路径引用) ## 版本历史 diff --git a/design/gdd/06-待办清单.md b/design/gdd/06-待办清单.md index 4b001864..1ee5ae8d 100644 --- a/design/gdd/06-待办清单.md +++ b/design/gdd/06-待办清单.md @@ -45,7 +45,7 @@ #### 探宝罗盘效果重定——两方案评估(2026-09-19 立项) -**现状**:探宝罗盘现行效果 = 预读 3×3 窗口内未开格的奖励身份;未开格微光方案拍板(2026-09-18,见[待办清单](../../06-待办清单.md)「未开格类型徽章呈现时机」)后,其信息价值被常显色光覆盖,效果需重定。用户 2026-09-19 立两方案评估:**方案一(色光永久化路线)**——周围 8 格类型色光作为基础常显能力人人永久享有,预读即常态,罗盘原效果彻底失义、需另找定位或退役;**方案二(可选天赋路线)**——探宝罗盘这类预测型秘宝改造成可选天赋/增强件(黑帝斯塔罗牌式:拿不拿、如何组合构成构筑面),预读从"道具一次性效果"变"天赋增强效果"。两方案不互斥(色光为基础、天赋在其上叠加更强预读),评估时一并考量。 +**现状**:探宝罗盘现行效果 = 预读 3×3 窗口内未开格的奖励身份;未开格微光方案拍板(2026-09-18,见[待办清单](06-待办清单.md)「未开格类型徽章呈现时机」)后,其信息价值被常显色光覆盖,效果需重定。用户 2026-09-19 立两方案评估:**方案一(色光永久化路线)**——周围 8 格类型色光作为基础常显能力人人永久享有,预读即常态,罗盘原效果彻底失义、需另找定位或退役;**方案二(可选天赋路线)**——探宝罗盘这类预测型秘宝改造成可选天赋/增强件(黑帝斯塔罗牌式:拿不拿、如何组合构成构筑面),预读从"道具一次性效果"变"天赋增强效果"。两方案不互斥(色光为基础、天赋在其上叠加更强预读),评估时一并考量。 **归属**:魔法道具内容重定批(排垂直切片收尾后) @@ -189,7 +189,7 @@ #### 冒险前资源配比预设(构筑提案) -**现状**:2026-09-18 用户提案,方向评估通过,登记在案。核心想法 = 玩家在**开始冒险前**预设地图战斗资源的类型配比——预设的是**分布**(七型在格面上的权重占比),不是落位:装板摇号仍按专属随机流摇出各格类型身份,玩家定分布、摇号定落位,无固定出招剧本(与既定随机性立场相容)。两分法:① **基础资源配比**——玩家对七型格面池(attack 增伤 / defense 护甲 / heal 回血 / blast 群伤 / weaken 削弱 / pierce 穿刺 / magic 魔法)的正向行权重做配比,经覆盖值通道在开局注入,摇号按覆盖后的分布落位;② **关卡特殊资源**——不同关卡(board_biome 图主题)的地图携带配比池之外的特殊资源与差异化分布。设计红利:雷安同池 + 极性矩阵使配比自带代价轴——调高某型权重,安全格正向与雷格负向同步变多,配比 =「赌自己挖掘少失误 + 赌关卡敌人构成怕什么」的双重下注,不是免费午餐,无需另发明平衡机制。架构红利:① 的注入通道 = [元进度](systems/冒险循环/元进度.md) v1.0 刚定稿的「冒险开始参数面 + 覆盖值通道」(追加/替换集合操作已登记),零新机制;② 正是[棋盘生成](systems/排雷循环/棋盘生成.md) board_biome 上下文与[地图战斗资源](systems/战斗循环/地图战斗资源.md)`clear_<图类型>` 差异化插槽、P1 功能道具渠道等预留位的启用场景。 +**现状**:2026-09-18 用户提案,方向评估通过,登记在案。核心想法 = 玩家在**开始冒险前**预设地图战斗资源的类型配比——预设的是**分布**(八型在格面上的权重占比),不是落位:装板摇号仍按专属随机流摇出各格类型身份,玩家定分布、摇号定落位,无固定出招剧本(与既定随机性立场相容)。两分法:① **基础资源配比**——玩家对八型格面池(attack 增伤 / defense 护甲 / heal 回血 / blast 群伤 / weaken 削弱 / pierce 穿刺 / magic 魔法 / action 行动点)的正向行权重做配比,经覆盖值通道在开局注入,摇号按覆盖后的分布落位;② **关卡特殊资源**——不同关卡(board_biome 图主题)的地图携带配比池之外的特殊资源与差异化分布。设计红利:雷安同池 + 极性矩阵使配比自带代价轴——调高某型权重,安全格正向与雷格负向同步变多,配比 =「赌自己挖掘少失误 + 赌关卡敌人构成怕什么」的双重下注,不是免费午餐,无需另发明平衡机制。架构红利:① 的注入通道 = [元进度](systems/冒险循环/元进度.md) v1.0 刚定稿的「冒险开始参数面 + 覆盖值通道」(追加/替换集合操作已登记),零新机制;② 正是[棋盘生成](systems/排雷循环/棋盘生成.md) board_biome 上下文与[地图战斗资源](systems/战斗循环/地图战斗资源.md)`clear_<图类型>` 差异化插槽、P1 功能道具渠道等预留位的启用场景。 **影响面(按循环)**: - 排雷循环:[行动经济](systems/排雷循环/行动经济.md)——经济契约对账以资源行值为原材料,类型频次构成随配比漂移,对账窗口须按配比包络复算;[棋盘生成](systems/排雷循环/棋盘生成.md)——生成管线零改动(资源身份摇号不在其域),board_biome 词表定稿是关卡特殊资源的前置;[排雷玩法](systems/排雷循环/排雷玩法.md)——挖掘/标旗动作决定极性、承载配比的风险轴,机制零改动;[魔法道具](systems/排雷循环/魔法道具.md)——相邻:预读类道具读取身份查询面,配比改变分布先验后「资源预读与推理的边界」读感联动,不做机制耦合。 @@ -197,7 +197,7 @@ - 冒险循环:[元进度](systems/冒险循环/元进度.md)——配比能力的家:可用类型 / 预算点数 / 预设槽位走营地柜台解锁,配比预设 = 覆盖值通道的又一生产者;[冒险结构](systems/冒险循环/冒险结构.md)——冒险开始参数面扩容,配比覆盖域的字段语义待裁;[难度曲线与Boss](systems/冒险循环/难度曲线与Boss.md)——杠杆主权转移:设计者从「定权重」改为「定预算包络、玩家在包络内分配」,通关率验收带随配比漂移,切片与验收期必须锁默认权重;[关卡节点地图](systems/冒险循环/关卡节点地图.md)——关卡特殊资源与节点奖励(功能道具渠道)的分工须一次说清:特殊资源走图内格面池差异化,功能道具走节点奖励,两渠道不混。 - 表现与支撑:[HUD 框架](systems/表现与支撑/HUD框架.md)——新增构筑界面(冒险前的配比设置面)与配比效果的局内可读呈现;[教学引导](systems/表现与支撑/教学引导.md)——新增「读敌配比」教学口径;[数据配置](systems/表现与支撑/数据配置.md)——配比预设存档域与预算参数表建账,registry 新条目;[存档序列化](systems/表现与支撑/存档序列化.md)——预设档契约域随批登记(与元进度存档四件同批)。 -**依赖(本提案依赖谁)**:① 冒险开始参数面 + 覆盖值通道([元进度](systems/冒险循环/元进度.md) v1.0 已定稿口径,现成);② 七型候选集合与权重语义([地图战斗资源](systems/战斗循环/地图战斗资源.md) v0.10 现行结构);③ board_biome 词表定稿([棋盘生成](systems/排雷循环/棋盘生成.md),关卡特殊资源的前置);④ 数据配置表承载与存档域(预设持久化);⑤ 元进度解锁状态(可用配比容量)。 +**依赖(本提案依赖谁)**:① 冒险开始参数面 + 覆盖值通道([元进度](systems/冒险循环/元进度.md) v1.0 已定稿口径,现成);② 八型候选集合与权重语义([地图战斗资源](systems/战斗循环/地图战斗资源.md) v0.14 现行结构);③ board_biome 词表定稿([棋盘生成](systems/排雷循环/棋盘生成.md),关卡特殊资源的前置);④ 数据配置表承载与存档域(预设持久化);⑤ 元进度解锁状态(可用配比容量)。 **被依赖(谁会依赖本提案)**:HUD 构筑界面呈现;教学新口径;难度曲线与Boss 验收判读口径(配比包络下的通关率带复算,换算率表强条目纪律同类);行动经济对账窗口;registry 与架构概念图的结构登记(新节点落地时 archify 图须重交付)。 @@ -256,7 +256,7 @@ **数值语言注记(2026-09-18)**:全项目数值语言已拍板小整数加法核(见[01-核心循环](01-核心循环.md) 数值语言条款)——GP 商店属性提升档位锁定 **flat 整数**(与永久强化三属性同族),开放问题④的档位词汇评估时免议;提案本身维持未评估不设计。 -**开放问题(评估前须过)**:① GP 格面与七型池的结构关系——进不进「雷安同池」同一摇号池、权重占比几何;雷格侧 GP 的极性语义(负向 = 扣 GP / 并入挖雷惩罚 / GP 面中性无极性);② 战斗掉落的量级与记账——按敌种/节点编号定值还是掷定,入账归冒险结构资源账本还是战斗结算兑付;③ 商店房进图版结构——关卡节点地图 v1.0 节点封闭枚举 +1 的修订面:编排规则(出现频率/必现位/深度约束)、图合法性校验连带、确认选路交互、兑付移交语义;④ 商店内容与定价——属性提升的档位词汇(与永久强化三属性同族还是冒险内独立档)、价格曲线、三选一候选池、刷新次数/费用;⑤ 双币分工与呈现——GP 与钥石记账分离(局内清零不入存档)、GP 余额与商店界面的 HUD 归属;⑥ 成长与验收纪律——商店属性提升是通关率带的新玩家侧杠杆(换算率表强条目纪律同类),验收读数须按商店存在与否分层,GP 供给量级与难度曲线加难杠杆族的关系。 +**开放问题(评估前须过)**:① GP 格面与八型池的结构关系——进不进「雷安同池」同一摇号池、权重占比几何;雷格侧 GP 的极性语义(负向 = 扣 GP / 并入挖雷惩罚 / GP 面中性无极性);② 战斗掉落的量级与记账——按敌种/节点编号定值还是掷定,入账归冒险结构资源账本还是战斗结算兑付;③ 商店房进图版结构——关卡节点地图 v1.0 节点封闭枚举 +1 的修订面:编排规则(出现频率/必现位/深度约束)、图合法性校验连带、确认选路交互、兑付移交语义;④ 商店内容与定价——属性提升的档位词汇(与永久强化三属性同族还是冒险内独立档)、价格曲线、三选一候选池、刷新次数/费用;⑤ 双币分工与呈现——GP 与钥石记账分离(局内清零不入存档)、GP 余额与商店界面的 HUD 归属;⑥ 成长与验收纪律——商店属性提升是通关率带的新玩家侧杠杆(换算率表强条目纪律同类),验收读数须按商店存在与否分层,GP 供给量级与难度曲线加难杠杆族的关系。 **归属**:[关卡节点地图](systems/冒险循环/关卡节点地图.md)修订批(商店房节点与图版结构,主责);GP 格面枚举归[地图战斗资源](systems/战斗循环/地图战斗资源.md);战斗掉落兑付归[战斗结算](systems/战斗循环/战斗结算.md)与[冒险结构](systems/冒险循环/冒险结构.md)共裁;成长节奏与验收带联动归[难度曲线与Boss](systems/冒险循环/难度曲线与Boss.md);GP 与钥石分工知会[元进度](systems/冒险循环/元进度.md)。 diff --git a/design/gdd/systems/战斗循环/战斗结算.md b/design/gdd/systems/战斗循环/战斗结算.md index 9415307d..4a616955 100644 --- a/design/gdd/systems/战斗循环/战斗结算.md +++ b/design/gdd/systems/战斗循环/战斗结算.md @@ -350,7 +350,7 @@ EV 参考数不入正文(派生读数,需要时按判定式与现行表值 | v0.12 | 2026-09-17 | **数值纪律收口批:正文与调参表 knob 现行值改指路存根(事实源 `data/tables/*.tres` 与 registry),20 余处失同步值修正**——收益对照表参考敞口 12 / 完美乘数 200 改指路 settlement.tres(equivalence_ref_damage / p_perfect);伏击加层"+2 层"改"+stacks 层(读 reward.tres ambush_double 行)";充能供给表与 charge_grants 行"每格 1 / 上限 8 / 每雷 2"改 per_unit/cap 读表;三乘法链与甲效矩阵 1.5/1.0/0.5、150/100/50 改三档读表;掷点判定 1d20 / nat20 ×2 / 缺口档 ×0.75/×0.5/×0.25 / 边界 [3,7] / 档位 [75,50,25] / damage_floor=1 / AC 10·0 / ac_up +1×2 层全部改指路 settlement.tres·status_effects.tres·hero/enemy 表;EV 参考数(AC10 21.25% 等)与"旧 defense_up 满层 −60%"删除(派生读数 / 退役值,版本历史可查);调参表与验收标准同步 | | v0.14 | 2026-09-18 | **战斗数值结构修复批(2026-09-18 盘问会话用户拍板,与增益状态 / registry / 切片代码同批)**:① 主角侧乘区接线——hero_offense 池入主角攻击乘区:Σ三分量后 ×(100+hero_offense)/100 ×(100+enemy_defense)/100 两池串乘,增伤/乏力覆盖面 = 普通+穿刺+魔法 合计;attack_up 普通分量直乘旁路退役(正文甲效矩阵"普通"行注记、池乘区公式行、v0.11 切片现状注记三处同步销项);② off_guard(失衡)换装 AC 轴——unit percent→ac(幅度 provisional 归状态层重设计批),effect_pool_map 删 off_guard 键,hero_defense 池清空、结构保留,消费走既有掷点判定读法零改动;③ fortify(硬化)符号纠正——旧 +100 入敌方防御池实际 ×2 翻倍与"硬化=减伤"语义相反,改负值(读表)后主角攻击折半;④ weaken(削弱)叠层改续时——同名重复授予只刷新剩余结算数、层数恒 1,×2 叠满池触底 ×0 的免伤轮结构性根治(消饱和);duration 终值、off_guard 幅度、当量锚复算全部归状态层重设计批。切片同批:battle_orchestrator 池接线与直乘删除、status_effects.tres 三行、settlement.tres 删键、冒烟护栏(削弱续时 / 乘区接线 ×1.5·×0.75 / 硬化折半方向三组新断言,旧"削弱×2 池触底"采样通道随批退役) | | v0.15 | 2026-09-18 | **甲效矩阵用户调参批(用户逐格给值,事实源 settlement.tres 五格改值、四格不动,零代码)**:普通行 ward 100→120 / heavy 100→50;穿刺行 light 100→80 / ward 150→120 / heavy 50→80;魔法行与普通-轻甲不动——「克制/中性/被克」三档结构与「普通行恒中性」退役,改五档(克制 / 弱克制 / 中性 / 弱被克 / 被克,档值读表)。矩阵消费面成文 = 主角侧(奖励直伤道具 / 主角攻击三分量 / 充能分量),敌方攻击不查矩阵、主角 armor_type 轴仍休眠(v0.13「敌方攻击恒 normal 档故中性」的归因改述——轴休眠是结构性的非数值性的)。语义列两行同步(穿刺「破不开重甲」重压语义弱化为与轻甲同档折损)。切片同批:settlement.tres 批注、冒烟矩阵三行断言改五档读数 + 乘区接线穿刺分量 3→2(3 点 ×80% 取整)随批改形,冒烟 300 项 0 失败 + 四探针 PASS;registry(enemy/hero armor_type 两行注记)与 敌人系统 / 难度曲线与Boss 三档措辞同步 | -| v0.16 | 2026-09-19 | **充能退役批(用户拍板,与切片代码 / registry / 07 清单 / README 同批)**:主角攻击三分量模型(普通 + 穿刺/魔法充能)整体退役,回归单分量——量 = 攻击基线、类型 = 主角表 attack_type;挖格批/旗判定批的充能计数条款删除、充能供给表与 charge_grants 调参行删除、乘区公式与粒度口径改"攻击合计";退役理由 = 克制策略归冒险前配置(营地换型/未来冒险前参数面),局内不再有挖掘攒弹药的第二伤害供给;保留面 = 攻击类型属性轴与棋盘奖励触发源的抽象分离、奖励直伤道具(穿刺弹/魔法矢)的克制直伤消费不变。切片同批:battle_orchestrator 攻击段单分量化 + _charge_points/_charge_counts/round_facts 充能键删除、DamageMath.charge_component → counter_component 改名(奖励直伤唯一消费方)、settlement.tres charge_grants 键与 schema 校验删除、冒烟充能断言组退役改形(乘区接线单分量基线 100、标中正向兑付行断言、恢复点探针换种子 42)、冒烟 293 项 0 失败 | +| v0.22 | 2026-09-19 | **充能退役批(版本号避让注记:原登 v0.16 与甲效矩阵加法核换装批撞号——2026-09-20 R10 版本号唯一性规则上线发现,后落籍者顺延;用户拍板,与切片代码 / registry / 07 清单 / README 同批)**:主角攻击三分量模型(普通 + 穿刺/魔法充能)整体退役,回归单分量——量 = 攻击基线、类型 = 主角表 attack_type;挖格批/旗判定批的充能计数条款删除、充能供给表与 charge_grants 调参行删除、乘区公式与粒度口径改"攻击合计";退役理由 = 克制策略归冒险前配置(营地换型/未来冒险前参数面),局内不再有挖掘攒弹药的第二伤害供给;保留面 = 攻击类型属性轴与棋盘奖励触发源的抽象分离、奖励直伤道具(穿刺弹/魔法矢)的克制直伤消费不变。切片同批:battle_orchestrator 攻击段单分量化 + _charge_points/_charge_counts/round_facts 充能键删除、DamageMath.charge_component → counter_component 改名(奖励直伤唯一消费方)、settlement.tres charge_grants 键与 schema 校验删除、冒烟充能断言组退役改形(乘区接线单分量基线 100、标中正向兑付行断言、恢复点探针换种子 42)、冒烟 293 项 0 失败 | | v0.16 | 2026-09-18 | **甲效矩阵加法核换装批(用户拍板数值语言——小整数加法核,与 01-核心循环 数值语言条款 / registry / 切片代码同批)**:量纲 percent→整数点数,三档(克制 +1/中性 0/被克 −1)九格改值〔normal 0/+1/−1、pierce −1/+1/0、magic 0/−1/+1〕,pierce×heavy = 0 中性为用户特别拍板;五档词表与百分比乘区退役(v0.15 的五档表随批短命退役,理由=个位数刻度假粒度);分量算术改述=折算取整后加修正点数;缺项回退 100→0;奖励直伤行 payload 键 effectiveness 保留、值语义改点数;池乘区/AC 折扣档/暴击保留百分比并成文粒度口径(数值语言条款:百分比仅此三处既有粗档+开发侧工具);切片同批:settlement.tres/settlement_table.gd/damage_math/battle_orchestrator/smoke/registry/01-核心循环同步 | | v0.17 | 2026-09-19 | **技能池新效应词汇批(用户拍板——设计权威 docs/proposal-status-vocabulary.md v1.0,与 敌人系统 / 增益状态 / registry / 切片代码同批)**:① 三乘法链尾部 +必中附加分量 rider(伤害技 rider_mult 字段:max(伤害下限, floor(攻击基线 × rider_mult))——后置加法、必中全额、不进乘区不出暴击、价位锚 0.75 = AC 折扣档最轻档;"固定值伤害"改形——固定值不随基线缩放与共享技能表前提冲突);② 增益/妨害技 = 同族施放行(0 伤害不掷不折不兑伏击),妨害状态按状态行 target_side 落主角——零新授予通道;③ 必中家族清单 +rider(掷点边界切口);④ 公式行/吃掷点行/验收标准三处同步 + 新效应词汇验收条目(rider 逐拍精确复算 + rider 键封闭 + 妨害落主角端到端);⑤ 实装面 = 仅 L2 Boss 卓尔(毒爆 +rider、新行蛛网束缚妨害→失衡),其余敌种不动;多段命中经用户拍板退役不入词汇表 | | v0.18 | 2026-09-19 | **插旗不可逆批(用户拍板全套落地,2026-09-18 提案转正;与排雷玩法 v0.5 / 行动经济 v0.4 / 地图战斗资源 v0.12 / registry / 切片代码同批)**:① 授予段「抵消过滤」改述「判定基线」——撤旗动作退役,旗判定以轮末在场旗为基线,被挖开的旗不入基线;② 新增插旗不可逆四情况兑付表:①标中照常 / ②插旗雷格直挖只吃挖雷惩罚标中不发 / ③标空照常 / ④插旗安全格直挖零惩罚零奖励(正向作废 + reward_ineffective 归因行);③ 挖格批兑付表补四情况分支;④ 标中资格措辞改「旗无移除手段」(拔旗重插通道消失);⑤ P0 闭环第 2/4 步与验收标准同步(「抵消过滤」条改「挖旗格兑付改判」)。切片同批:minesweeper_rules 挖旗格放行 + flag_pull 出目录 + dug_under_flag 事件标记、battle_orchestrator 情况④零奖励抑制、action_economy 表 flag_pull_ap_cost 删除、冒烟 317→327 项 0 失败(四情况探针 `_smoke_flag_irreversible_quadrants`) | diff --git a/design/gdd/systems/排雷循环/地形变异.md b/design/gdd/systems/排雷循环/地形变异.md index 29271c1d..045db550 100644 --- a/design/gdd/systems/排雷循环/地形变异.md +++ b/design/gdd/systems/排雷循环/地形变异.md @@ -28,7 +28,7 @@ | 不负责 | 归谁 | |---|---| -| 布置的编排时点与算法执行(变异格赋予在棋盘设置顺序第 4 步) | [战斗结算](../冒险循环/战斗结算.md)(编排段执行面);布置参数数值归[难度曲线与Boss](../冒险循环/难度曲线与Boss.md) | +| 布置的编排时点与算法执行(变异格赋予在棋盘设置顺序第 4 步) | [战斗结算](../战斗循环/战斗结算.md)(编排段执行面);布置参数数值归[难度曲线与Boss](../冒险循环/难度曲线与Boss.md) | | 挖格、插旗、连锁展开的规则本体 | [排雷玩法](排雷玩法.md) | | 反制道具的效果、时效与定价 | [魔法道具](魔法道具.md) | | 数值表本体与读取机制 | [数据配置](../表现与支撑/数据配置.md) | @@ -96,7 +96,7 @@ | 方向 | 系统 | 接口要点 | |---|---|---| | 上游 | [数据配置](../表现与支撑/数据配置.md) | 变异类型清单的表结构承载与统一读取;本系统是清单内容的回填责任方 | -| 上游 | [战斗结算](../冒险循环/战斗结算.md) | 布置段的编排执行位(棋盘设置顺序第 4 步)+ 伤害型兑付面(轮末结算逐行兑现) | +| 上游 | [战斗结算](../战斗循环/战斗结算.md) | 布置段的编排执行位(棋盘设置顺序第 4 步)+ 伤害型兑付面(轮末结算逐行兑现) | | 下游 | [排雷玩法](排雷玩法.md) | 零特判义务的承接方:执行逻辑不按变异分支,只按格子数据行动 | | 下游 | [魔法道具](魔法道具.md) | 反制道具配套义务的承接方(五维度框架的"反制"维度;首批兑现排 Alpha) | | 相邻 | [行动经济](行动经济.md) | "地形成本修饰"价目钩子(不同格子不同价)暂缓启用,待变异类型扩展(见该文档 P1/P2 后置) | diff --git a/design/ux/interaction-patterns.md b/design/ux/interaction-patterns.md index e08b57ba..5823ac12 100644 --- a/design/ux/interaction-patterns.md +++ b/design/ux/interaction-patterns.md @@ -84,7 +84,7 @@ MVP 未使用标准控件(不入库):toggle / dropdown / input field / tab **Description**: 轮末结算逐行流水实时呈现 + 可回看——「扫雷之因逐行兑现为战果」(P2 归因)的承载面。挖掘期收起为 1 行上轮摘要;结算期展开 84px 实时流;点击开完整回看 overlay。 **Specification**: -- 数据源:[战斗结算](../gdd/systems/战斗循环/战斗结算.md#明细流水每一行都能指回你刚才的哪一步棋)步骤流(15 类行封闭协议;公共字段 `type/source_class/origin_cell/round_index/display_name/value/highlight?`);**无来源行 = 断言失败** +- 数据源:[战斗结算](../gdd/systems/战斗循环/战斗结算.md#明细流水每一行都能指回你刚才的哪一步棋)步骤流(9 类行封闭协议(现行);公共字段 `type/source_class/origin_cell/round_index/display_name/value/highlight?`);**无来源行 = 断言失败** - 行模板:「{行为} {值} → {目标}(来源)」——来源标注(第 N 轮 · 安全格式);行文案最长 token = 道具名(4 汉字),模板预留 40% 膨胀宽度 - 聚合纪律:未生效类行(奖励未生效——现行 9 类行协议唯一未生效型,2026-09-20 收敛)按聚合计数呈现(防逐行惩罚轰炸);衰减行聚合计数;效果行 value=单次贡献值、禁与 pool_value(池合成值)相加呈现;零值行可见不吞(免伤轮归因不断点) - 高亮:highlight 字段 {flag_hit, clear_reward_h2, perfect_broken} → 金框强调;完美排雷 = 独立高光行 + 全屏喝彩级反馈 @@ -92,7 +92,7 @@ MVP 未使用标准控件(不入库):toggle / dropdown / input field / tab - 击杀标记呈现(H3 数据源 = 战斗结算流水击杀结果字段) **When to Use**: 任何「因果可归因」的逐行流水呈现 -**When NOT to Use**: 自造行类型/字段(15 类封闭枚举——增删 = 接口漂移,须走[《战斗结算》](../gdd/systems/战斗循环/战斗结算.md#明细流水每一行都能指回你刚才的哪一步棋)修订) +**When NOT to Use**: 自造行类型/字段(9 类封闭枚举(现行)——增删 = 接口漂移,须走[《战斗结算》](../gdd/systems/战斗循环/战斗结算.md#明细流水每一行都能指回你刚才的哪一步棋)修订) **Reference**: battle.md §Component Inventory 结算明细带、§AC 信息与归因;归档《战斗结算》(b1-settlement.md)Core Rule 14 / Visual Req diff --git a/tools/doc_link_audit.py b/tools/doc_link_audit.py new file mode 100644 index 00000000..b42589e9 --- /dev/null +++ b/tools/doc_link_audit.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +r"""doc_link_audit.py — design 文档链接健康检查 + +扫 design/gdd/、design/registry/、design/ux/ 下的 .md 与 .yaml,校验两类引用: + (a) markdown 链接 [文字](路径[#锚点]) —— 一律相对"链接所在文件自身目录"解析(标准 + markdown 语义),跨目录用 ../。排除图片 ![...];目标里混入全角标点(偶发把全角括号 + 写进目标)时在首个全角标点处截断再校验。 + (b) 反引号路径引用 `路径.扩展名`(扩展名限 md/yaml/yml/tres/gd/py/json/csv)—— + 只校验"路径形"(含 / 且不含空格、通配符)的引用;裸文件名(如表格里的 + `reward.tres`、文档树里的 `棋盘生成.md`)是简称提及不是链接,跳过不校验。 + 解析顺序:所在文件目录 → 仓库根 → design/ → prototypes/dungeon-sweeper-vertical-slice/ + (data/tables/*.tres 类指路代码侧的惯用写法实际落在 slice 下;带 ./ ../ 前缀的只按 + 所在文件目录解析,不做多基猜测)。 +锚点校验:目标为 .md 时,把其标题行转成 GFM 中文锚(去空格、去冒号等标点、小写英文, +仅保留字母/数字/汉字),链接 #锚点 与之精确匹配即命中。 + +用法: + python tools/doc_link_audit.py # 扫默认范围,报告模式(exit 0) + python tools/doc_link_audit.py --strict # 有非已知死链 exit 1(供门禁/CI) + python tools/doc_link_audit.py --known tools/doc_link_known.txt + # 已知死链清单,每行 <文件相对路径>|<目标>, + # 命中的归"已知"类不算失败 + python tools/doc_link_audit.py ... # 只扫指定文件 + python tools/doc_link_audit.py --json # 机器可读 + +口径:只扫 design/gdd/ design/registry/ design/ux/——design/art、design/research +(历史素材/提案)不在扫描域,但作为链接目标被校验。排除 ``` 围栏代码块与 ; 开头注释行。 +""" +import argparse +import json +import re +import sys +from pathlib import Path + +DEFAULT_DIRS = ["design/gdd", "design/registry", "design/ux"] +PROTO_PREFIX = "prototypes/dungeon-sweeper-vertical-slice" + +ALLOWED_EXTS = ("md", "yaml", "yml", "tres", "gd", "py", "json", "csv") +MD_LINK_RE = re.compile(r"(? set(slug) + + def headings(self, path): + key = path.resolve() + if key not in self._heads: + slugs = set() + try: + text = key.read_text(encoding="utf-8").replace("\r\n", "\n") + except OSError: + text = "" + for _, line in readable_lines(text): + m = HEADING_RE.match(line) + if m: + slugs.add(anchor_slug(m.group(1))) + self._heads[key] = slugs + return self._heads[key] + + def resolve_markdown(self, src, target): + """markdown 链接目标:一律相对所在文件目录,不做多基猜测。""" + return src.parent / target + + def resolve_backtick(self, src, target): + """反引号路径引用:带 ./ ../ 前缀只按所在文件目录;否则按序试多基目录。""" + norm = target.replace("\\", "/") + if norm.startswith(("./", "../")): + bases = [src.parent] + else: + bases = [src.parent, self.root, self.root / "design", self.root / PROTO_PREFIX] + for base in bases: + p = base / norm + if p.exists(): + return p + return None + + +def scan(repo, paths, comment_re=re.compile(r"^\s*;")): + findings = [] # 死链/锚点未命中 + counts = {"markdown": 0, "anchor": 0, "backtick": 0} + for p in paths: + text = p.read_text(encoding="utf-8").replace("\r\n", "\n") + for lineno, line in readable_lines(text): + if comment_re.match(line): + continue + # (a) markdown 链接 + for m in MD_LINK_RE.finditer(line): + raw = m.group(1) + target = DEST_CUT_RE.split(raw, 1)[0] + if not target or SCHEME_RE.match(target): + continue + path, sep, frag = target.partition("#") + if not path: # 同页锚点 + counts["anchor"] += 1 + if frag not in repo.headings(p): + findings.append(_f(p, lineno, "anchor", target, "锚点未命中", line)) + continue + counts["markdown"] += 1 + tp = repo.resolve_markdown(p, path) + if not tp.exists(): + findings.append(_f(p, lineno, "markdown", target, "目标不存在", line)) + elif frag: + counts["anchor"] += 1 + if tp.suffix.lower() == ".md" and frag not in repo.headings(tp): + findings.append(_f(p, lineno, "anchor", target, "锚点未命中", line)) + # (b) 反引号路径引用(只校验路径形) + for m in BACKTICK_RE.finditer(line): + raw = m.group(1).strip() + if "/" not in raw or " " in raw or "*" in raw or "?" in raw: + continue + if not EXT_RE.search(raw) or SCHEME_RE.match(raw): + continue + counts["backtick"] += 1 + if repo.resolve_backtick(p, raw) is None: + findings.append(_f(p, lineno, "backtick", raw, "目标不存在", line)) + findings.sort(key=lambda f: (f["file"], f["line"])) + return findings, counts + + +def _f(p, lineno, kind, target, problem, line): + return { + "file": p.as_posix(), + "line": lineno, + "kind": kind, + "target": target, + "problem": problem, + "text": line.strip()[:160], + } + + +def load_known(path): + """已知死链清单:每行 <文件相对路径>|<目标>;# 与空行忽略。""" + known = set() + if not path: + return known + for line in Path(path).read_text(encoding="utf-8").replace("\r\n", "\n").split("\n"): + line = line.strip() + if not line or line.startswith("#"): + continue + file, sep, target = line.partition("|") + if sep: + known.add((file.strip().replace("\\", "/").lstrip("./"), target.strip())) + return known + + +def main(): + ap = argparse.ArgumentParser(description="design 文档链接健康检查") + ap.add_argument("files", nargs="*", help="指定文件;缺省扫默认目录") + ap.add_argument("--strict", action="store_true", help="有非已知死链时 exit 1") + ap.add_argument("--known", help="已知死链清单(每行 <文件相对路径>|<目标>)") + ap.add_argument("--json", action="store_true") + args = ap.parse_args() + + if args.files: + paths = [Path(f) for f in args.files] + else: + paths = [] + for d in DEFAULT_DIRS: + paths += sorted(Path(d).rglob("*.md")) + paths += sorted(Path(d).rglob("*.yaml")) + paths = [p for p in paths if p.is_file()] + if not paths: + print("扫描域为空(未找到任何 .md/.yaml)", file=sys.stderr) + return 2 + + repo = Repo(Path(".")) + findings, counts = scan(repo, paths) + known = load_known(args.known) + dead = [f for f in findings if (f["file"], f["target"]) not in known] + known_dead = [f for f in findings if (f["file"], f["target"]) in known] + + if args.json: + print(json.dumps({ + "scanned": len(paths), + "counts": {"markdown": counts["markdown"], "backtick": counts["backtick"], + "anchor": counts["anchor"], "total": sum(counts.values())}, + "dead": dead, + "known_dead": known_dead, + "dead_count": len(dead), + "known_count": len(known_dead), + }, ensure_ascii=False, indent=1)) + else: + total = sum(counts.values()) + print(f"扫描 {len(paths)} 个文件 · 链接 {total} 处" + f"(markdown {counts['markdown']} + 反引号路径 {counts['backtick']}" + f" + 锚点 {counts['anchor']})· 死链 {len(dead)} 处 · 已知死链 {len(known_dead)} 处") + for title, items in (("死链", dead), ("已知死链(不算失败)", known_dead)): + if not items: + continue + print(f"\n【{title}】×{len(items)}") + by_file = {} + for f in items: + by_file.setdefault(f["file"], []).append(f) + for file, fs in sorted(by_file.items()): + for f in fs: + print(f" {file}:{f['line']} [{f['kind']}] {f['target']}({f['problem']})") + return 1 if (args.strict and dead) else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/doc_link_known.txt b/tools/doc_link_known.txt new file mode 100644 index 00000000..a03c9d5f --- /dev/null +++ b/tools/doc_link_known.txt @@ -0,0 +1,4 @@ +design/ux/battle.md|design/player-journey.md +design/ux/battle.md|design/ux/node-reward.md +design/ux/battle.md|design/accessibility-requirements.md +design/ux/battle.md|design/ux/level-clear.md diff --git a/tools/gdd_lint.py b/tools/gdd_lint.py index 0c0b3c04..57691f50 100644 --- a/tools/gdd_lint.py +++ b/tools/gdd_lint.py @@ -17,6 +17,11 @@ r"""gdd_lint.py — GDD lean 格式门禁 R8 核心规则表:规则列必须是"谁·何时·做什么"陈述句所在的可读表——说明/要点列 禁止出现编号引用(AC-x/F-x/EC-x/E-x/OQ-x/Rule-x/义务),引用只允许在依据类列 R9 一页纸节点:任一标题节点的直接内容(到下一标题为止)≤ 80 行——装不下就拆子节点 + R10 版本历史版本号唯一:版本历史/修订记录节表格首列提取 v\d+(\.\d+)* 或裸数字编号查重 + (表头/分隔行/空值/日期式首列跳过);无版本历史表的文档不适用——存在性归 R4 + R11 计数词漂移:正文禁用旧计数口径——"10 类行/行型封闭为 10 类/10 类封闭协议/行型 10 类" + →9 类、"七型/五型 × 正负"→八型、"15 类"→9 类、"14 格"→十六格;版本历史/修订记录节、 + 留档/作废/退役/历史/旧/改型/随批/顺延等轨迹措辞行、`;` 注释行豁免(现行口径见 registry) """ import argparse import json @@ -34,8 +39,27 @@ MAX_PAREN_SPAN = 80 DEFAULT_DIR = "design/gdd" DEFAULT_EXCLUDE = {"game-concept.md", "game-pillars.md", "systems-index.md"} VERSION_SECTION = "版本历史" +# 版本历史/修订记录节(R10 查重域与 R11 豁免域共用;battle.md 等用「修订记录」同名节) +HISTORY_SECTION_RE = re.compile(r"(版本历史|修订记录)") FLOW_SECTION_RE = re.compile(r"(流程|生命周期)") +# R11 计数词漂移对照锚表。事实源 = design/registry/entities.yaml:359 行协议 9 类 / :314 八型 / +# :327 权重 74;口径变动时同步此处。 +CURRENT_COUNTS = [ + (re.compile(r"(?:10 类行|行型封闭为 10 类|10 类封闭协议|行型 10 类)"), "9 类"), + (re.compile(r"七型"), "八型"), + (re.compile(r"(?:五型 × 正负|五型×正负)"), "八型"), + (re.compile(r"15 类"), "9 类"), + (re.compile(r"14 格"), "十六格"), +] +# R11 轨迹措辞豁免:留档/作废/退役/历史/旧 + 改型/随批/顺延(registry 历史轨迹句「同批改型」) +R11_TRACE_RE = re.compile(r"(留档|作废|退役|历史|旧|改型|随批|顺延)") +# R10 版本号提取:v 前缀优先,裸数字编号兜底;日期式首列(修订记录表)整体跳过 +VER_TOKEN_RE = re.compile(r"[vV]\d+(?:\.\d+)*") +BARE_VER_RE = re.compile(r"^\d+(?:\.\d+)*$") +DATE_CELL_RE = re.compile(r"\d{4}-\d{2}-\d{2}") +SEPARATOR_ROW_RE = re.compile(r"^\|[\s\-|:]+$") + DATE_KEYWORD_RE = re.compile( r"[((][^()()]*\d{4}-\d{2}-\d{2}[^()()]*" r"(评审|修订|拍板|裁决|核销|门禁|作废|批)[^()()]*[))]" @@ -43,9 +67,12 @@ DATE_KEYWORD_RE = re.compile( def parse(text): - """按行解析:返回代码块行集合、版本历史节行集合、表格块列表、散文段落列表。""" + """按行解析:返回代码块行集合、版本历史节行集合、表格块列表、散文段落列表、 + 版本历史/修订记录节表格列表(R10 查重域)。""" code_lines, version_lines, tables, paras = set(), set(), [], [] + version_tables = [] cur_table, cur_para, cur_section = [], [], "" + cur_table_hist = False in_code = False for raw in text.splitlines(): stripped = raw.strip() @@ -56,8 +83,8 @@ def parse(text): paras.append(" ".join(cur_para)) cur_para = [] if cur_table: - tables.append(cur_table) - cur_table = [] + (version_tables if cur_table_hist else tables).append(cur_table) + cur_table, cur_table_hist = [], False continue if raw.startswith("## "): cur_section = stripped.lstrip("#").strip() @@ -68,10 +95,12 @@ def parse(text): cur_table.append(raw) if cur_section == VERSION_SECTION: version_lines.add(raw) + if HISTORY_SECTION_RE.search(cur_section): + cur_table_hist = True continue if cur_table: - tables.append(cur_table) - cur_table = [] + (version_tables if cur_table_hist else tables).append(cur_table) + cur_table, cur_table_hist = [], False is_para = ( stripped and not in_code @@ -90,8 +119,8 @@ def parse(text): if cur_para: paras.append(" ".join(cur_para)) if cur_table: - tables.append(cur_table) - return code_lines, version_lines, tables, paras + (version_tables if cur_table_hist else tables).append(cur_table) + return code_lines, version_lines, tables, paras, version_tables def unescaped_pipes(line): @@ -118,7 +147,7 @@ MAX_NODE_LINES = 80 def lint_file(path): text = Path(path).read_text(encoding="utf-8") - code_lines, version_lines, tables, paras = parse(text) + code_lines, version_lines, tables, paras, version_tables = parse(text) issues, infos = [], [] # R9 一页纸节点:每个标题的直接内容 ≤ MAX_NODE_LINES 行 @@ -177,6 +206,28 @@ def lint_file(path): elif not any(l.strip().startswith("|") for l in version_lines): issues.append(f"R4 「{VERSION_SECTION}」节无表格") + # R10 版本历史版本号唯一性(实证撞号:v0.19 曾被并行两批同时占用) + ver_seen = {} # key: 归一版本号 → [首次原样 token, 出现次数] + for table in version_tables: + for row in table[1:]: # 首行 = 表头 + if SEPARATOR_ROW_RE.match(row.strip()): + continue + cells = [c.strip() for c in re.split(r"(? 1: + issues.append(f"R10 版本历史版本号重复: {first}({count} 处)") + # R5 流程章节须含代码块图 cur_section, prev_section = "", "" pending_flow, has_code = False, False @@ -226,6 +277,26 @@ def lint_file(path): ) break + # R11 计数词漂移对照(锚表 CURRENT_COUNTS;版本历史/修订记录节与轨迹措辞行豁免) + in_code11, hist11 = False, False + for raw in text.splitlines(): + stripped = raw.strip() + if stripped.startswith("```"): + in_code11 = not in_code11 + continue + if raw.startswith("## "): + hist11 = bool(HISTORY_SECTION_RE.search(stripped.lstrip("#").strip())) + continue + if in_code11 or hist11 or stripped.startswith(";") or R11_TRACE_RE.search(raw): + continue + # yaml/yml 的 # 行内注释是登记轨迹(如 entities.yaml「2026-09-18 扩七型」),不入正文扫描 + line = raw.split("#", 1)[0] if str(path).endswith((".yaml", ".yml")) else raw + for pat, fix in CURRENT_COUNTS: + for m in pat.finditer(line): + issues.append( + f'R11 计数词漂移: "{m.group(0)}"→应为"{fix}"(现行口径见 registry entities.yaml)' + ) + # R7 括号跨度 longest = max((paren_spans(p) for p in paras), key=lambda s: max((len(x) for x in s), default=0), default=[]) longest_span = max((len(s) for s in longest), default=0)