已开启
同名形状被交叉配对:同一个 PPTX 自比较也报告两条不存在的填充色修改 #41
hh11211sa创建于  10 天前
hh11211sa
10 天前 创建

目标

修复形状级比较中同名形状被交叉配对的问题:同一个 PPTX 自比较也会报告两条不存在的填充色修改,消除由此产生的虚假样式差异。

现状

上游 main:db6c75892650854abf663c91b66ead486fde1299。在核验形状级差异时发现一个可独立复现的配对问题,使用的全是合成矩形,没有业务 PPT 或个人资料。

scripts/comparator.py::_diff_shapes 对 old_shapes 和 new_shapes 双重循环,仅以 os_.name == ns_.name 为条件,且匹配后不标记已使用。因此相同名字的两个对象会产生四个配对;old-A/new-B 与 old-B/new-A 造成虚假样式差异。当前解析模型没有保留 shape_id,不能仅在输出端隐藏这两条来解决。

复现步骤

下面代码放在仓库根目录运行,使用项目已有的 python-pptx 依赖即可。重点是最后两次 parse 读取同一个文件,并没有修改后版本。

import sys
from pathlib import Path
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE
from pptx.dml.color import RGBColor
from pptx.util import Inches

sys.path.insert(0, str(Path('scripts').resolve()))
from ppt_parser import parse_pptx
from comparator import compare_pptx, generate_summary

p = Presentation()
s = p.slides.add_slide(p.slide_layouts[6])
for i, color in enumerate(((0, 100, 200), (200, 100, 0))):
    box = s.shapes.add_shape(MSO_SHAPE.RECTANGLE,
                            Inches(1 + 3*i), Inches(1), Inches(2), Inches(1))
    box.name = 'SharedName'
    box.text = 'Unchanged sample'
    box.fill.solid()
    box.fill.fore_color.rgb = RGBColor(*color)
    print(box.shape_id, box.name)
p.save('same.pptx')
result = compare_pptx(parse_pptx('same.pptx'), parse_pptx('same.pptx'))
print(generate_summary(result))
for c in result[0].style_changes:
    print(c.shape_name, c.attribute, c.old_value, c.new_value)

实际两个形状的 shape_id 分别为 2 和 3;显示名都为 SharedName。自比较却得到:

modified=1, style_changes=1  # summary按发生变化的页数计数
SharedName 填充色 #0064c8 -> #c86400
SharedName 填充色 #c86400 -> #0064c8

控制组

同文件自比较场景 modified 原始 style_changes 条目数
两个不同名字、不同填充色 0 0
两个相同名字、相同填充色 0 0
两个相同名字、不同填充色 1 2

三组解析前后的 SlideContent.to_dict() 都完全相等。控制组仍有既有 #16 的 moved 状态问题,本报告没有把它作为新问题;本次新增证据是共享形状比较层生成了不存在的双向颜色差异。也不同于 #10 的对象位置漏检和 #35 的三种输出模式覆盖差异。

期望行为

  • 形状比较先建立一对一配对,保留 shape_id 作为候选依据
  • 处理跨文件重新编号及同名对象的退化情况
  • 无法唯一配对时明确标记歧义,避免名字相同就笛卡尔积比较

验收标准

优先级

P1(重要)


来源:Issue #41 原始报告(hh11211sa 实测诊断与建议,尚未声称已修复)
VOD 审视结构化

likedislike
gaoruiru
gaoruiru成员
5 天前 评论:

@grape_bot 处理issue

likedislike
grape_botgrape_bot成员
5 天前 修改了issue 的描述
grape_botgrape_bot成员
5 天前 修改了issue 的描述
grape_bot
grape_bot成员
5 天前 评论:

@grape_bot 处理issue

[VOD Agent] 已接手 Issue #41 并完成审视。

审视结论:需求清晰,达到可开发标准(无需澄清)。

对照四要素核查:

  • 目标:明确——修复 scripts/comparator.py::_diff_shapes 同名形状交叉配对问题,消除自比较产生的双向虚假填充色差异
  • 现状:明确——上游 main db6c758 可复现,含完整复现脚本与三组对照实验数据
  • 期望行为:明确——先建立一对一配对、保留 shape_id 作为候选依据、处理跨文件重新编号、无法唯一配对时明确标记歧义
  • 验收标准:可验证——三个自比较控制组(同名不同色不得产生双向差异)+ 单对象修改只报告一次真实变化

重复性核查:与 #16(整页 moved 误报)、#10(位置漏检/拒绝未生效)、#35(三种模式判定不一致)均不重叠,本 Issue 聚焦同名形状配对层,为独立新问题。

已将 Issue body 收敛为结构化模板(目标/现状/期望行为/验收标准/优先级 P1/来源),标签与分配交由 Issue Agent 处理,并已 A2A 交办进入开发流程。

likedislike
grape_bot
grape_bot成员
5 天前 评论:

[issue agent] ✅ 该 Issue 已通过深度可开发性分析,评级:ready

分析结论:

  • 需求完整性:complete — Bug 类,复现脚本(同文件自比较)与实际输出齐全,含预期 vs 实际对照与三组控制组数据,验收标准可验证
  • 技术可行性:feasible — 已核对目标基准 main(db6c758)上 scripts/comparator.py::_diff_shapes(161-170 行)仍为同名双重循环且无已用标记,缺陷在位、未实现;涉及模块:comparator.py、ppt_parser.py
  • 依赖就绪性:ready — 仅依赖项目既有 python-pptx 与标准库,无新增外部依赖
  • 范围确定性:clear — 预估改动:comparator.py(一对一配对逻辑)+ ppt_parser.py(ShapeInfo 保留 shape_id)+ 新增回归测试,复杂度 medium,无架构变更

关联说明:与 #72(同名形状 font_changes 翻倍)同属 _diff_shapes 配对层根因,但本 Issue 范围更完整(自比较双向虚假填充色差异 + shape_id 候选依据 + 歧义标记),建议在同一轮配对层改造中统一处理。与 #16/#10/#35 已确认不重叠。

已生成 Spec 文档并发布在下方,负责人已分配给机器人,将自动启动开发实现(当前 Issue 未绑定专属场景,按默认路由交办 coding agent)。

likedislike
grape_bot
grape_bot成员
5 天前 评论:

[issue agent] 📋 Spec Document

Spec: Issue #41 — 同名形状被交叉配对:同一个 PPTX 自比较也报告两条不存在的填充色修改

Generated by Issue Agent | Developability: ready | 2026-10-20 | 目标分支:main(基准 db6c758)

Objective

  • 要构建什么:修复 scripts/comparator.py::_diff_shapes 的同名形状交叉配对缺陷——同一 PPTX 自比较时不再产生两条互为反向的虚假填充色差异;同时为形状配对建立一对一机制,处理跨文件 shape_id 重新编号与同名退化情况。
  • 为什么:误报直接污染三模式(A/B/C)的差异报告与审阅流程,用户会被不存在的"双向颜色修改"误导去核对;自比较(同文件)本应是 0 差异的兜底场景却报 modified=1。
  • 用户是谁:使用 PPT 修改前后对比工具(三模式)的所有用户。
  • 成功标准:
    1. 三个自比较控制组结果符合验收标准(同名不同色不再产生双向差异)
    2. 单对象真实修改只报告一次
    3. 全部现有测试通过,新增回归测试覆盖控制组

Scope

In Scope

  • comparator.py::_diff_shapes:从名字笛卡尔积改为一对一配对(候选优先级:shape_id → name → 内容相似度),配对后标记已用;无法唯一配对时降级为有限候选集比较并标记歧义,而非全量交叉
  • ppt_parser.py::ShapeInfo:新增 shape_id 字段并纳入 to_dict(),作为配对候选依据(解析层保留,不改变现有字段语义)
  • 回归测试:三个自比较控制组 + 单对象修改只报一次
  • 与 #72 的协调:#72(同名 font_changes 翻倍)与本 Issue 同根因同函数,本 Spec 的一对一配对改造应同时消除 #72 的重复 FontChange;实现时以本 Spec 为准做配对层统一修复

Out of Scope

  • #16 的整页 moved 状态误报(独立缺陷,另案处理)
  • #10 的对象位置漏检/Reject 未生效、#35 的三模式覆盖差异
  • #82 的主题色(schemeClr)解析缺失——本 Spec 不改颜色解析层,仅改配对层
  • 三模式(A/B/C)输出层展示逻辑调整

Technical Approach

方案概述

在 _diff_shapes 中以"候选键 → 一对一贪心/最优配对 → 已用标记"替换现有双重循环,候选键优先级为 shape_id 精确匹配、name 匹配(同名多个时进入歧义处理)、位置+几何相似度兜底。

实现路径

  1. ppt_parser.py:ShapeInfo 增加 shape_id 字段(解析 shape.shape_id),to_dict() 一并输出;确认 SlideContent.to_dict() 等价性结论不被破坏(控制组已验证解析前后 to_dict 完全相等,新增字段后需保持同名同色控制组 modified=0)
  2. comparator.py:重写 _diff_shapes——构建候选集(shape_id → name → 相似度),执行一对一配对(每个 old shape 至多配一个 new shape),已配对标记防重;唯一配对走现有 _diff_font_runs/_diff_shape_styles
  3. 歧义处理:同名候选多于一时,按几何/内容确定最优配对;无法消歧时将 shape_name 标注歧义(如 SharedName(歧义)),仅对唯一配对报告样式差异
  4. 新增回归测试 test_issue_41.py(或并入现有测试目录):覆盖三个控制组与单对象修改场景

关键设计决策

决策 选项 选择 理由
配对键优先级 A. 仅 name;B. shape_id→name→相似度 B shape_id 是 OOXML 稳定标识;name 常重复(如 SharedName);跨文件重编号需相似度兜底
自比较场景处理 A. 文件级哈希短路;B. 配对层修复 B 根因在配对层,文件级短路治标且不解决同名交叉的普遍场景
无法唯一配对 A. 静默跳过;B. 标记歧义 B Issue 明确要求"无法唯一配对时明确标记歧义",静默会引入新漏检
#72 联动 A. 仅修 #41 场景;B. 配对层统一修复 B 同根因同函数,统一修复避免二次返工

Affected Modules

模块 改动类型 说明
scripts/comparator.py 修改 _diff_shapes 一对一配对重写 + 歧义标记
scripts/ppt_parser.py 修改 ShapeInfo 新增 shape_id 字段
测试(新增 tests/test_issue_41.py 或对齐现有测试布局) 新增 三控制组 + 单对象修改回归

依赖关系

  • ppt_parser.parse_pptx → comparator.compare_pptx(现有调用链不变);ShapeInfo 结构变更向下兼容(新增可选字段)
  • 三模式(pptx_annotator.py / ppt_review.py / main.py)经 compare_pptx 消费配对结果,自动受益,无需改动

Acceptance Criteria

Risks & Mitigations

Risk Impact Mitigation
相似度兜底配对引入新误配(跨文件重编号场景) Med 相似度阈值保守设定;无法唯一配对必须落歧义标记而非猜测
ShapeInfo 字段变更影响依赖 to_dict() 的既有比较逻辑 Med 新增可选字段向后兼容;回归测试覆盖三控制组 to_dict 等价性
#72 并行开发冲突(同函数改动) Med 以本 Spec 配对层统一修复为准;#72 验收由本 Spec 一并覆盖
配对层重写影响三模式输出一致性 Low 三模式共用 compare_pptx,无模式侧改动;现有测试 + 新增回归兜底

Open Questions

  • 无
likedislike
grape_botgrape_bot成员
5 天前 将 grape_bot 设为负责人
grape_botgrape_bot成员
5 天前 添加了label:bugP1spec-readytriaged
grape_bot
grape_bot成员
5 天前 评论:

[Coding Agent] 已完成需求文档与方案设计,进入编码(TDD:先写失败回归测试再实现):

  • 需求文档:docs/issue-41/spec.md(问题/根因/验收标准/非目标)
  • 方案设计:docs/issue-41/plan.md(配对算法:shape_id → name → 几何相似度,一对一贪心配对 + 已用标记;无法消歧落歧义标记;#72 同根因一并修复)
  • 任务拆解:docs/issue-41/task.md

按 Issue Agent 已评审的 Spec 执行,复杂度中等,无新增外部依赖。

likedislike
grape_botgrape_bot成员
5 天前 关联了pull request:fix: #41 同名形状一对一配对,消除自比较双向虚假填充色差异
grape_bot
grape_bot成员
5 天前 评论:

[Coding Agent] ✅ 已完成实现,请审查 PR #27(base=main,head=feat/issue-41,2 个原子提交:881d4b4 解析层 shape_id / 22d4aea 一对一配对重写)。

修复内容:

  • ppt_parser.py:ShapeInfo 新增 shape_id 并纳入 to_dict()
  • comparator.py::_diff_shapes:一对一配对((name, shape_id) 精确匹配 → 唯一名直配 → 重名几何相似度贪心 + 并列即歧义),无法唯一配对落 (歧义) 标记,禁止笛卡尔积;同根因一并修复 #72 的 font_changes 翻倍

验证结果:

  • 新增 tests/test_issue_41.py 12 用例全部通过(三控制组 / 单对象只报一次 / #72 / 重编号 / 歧义 / to_dict 等价性)
  • Issue 原始复现脚本核验:自比较 modified=0、style_changes=0(修复前 modified=1 + 2 条反向填充色差异)
  • 三模式经 compare_pptx 自动受益,模式 C(HTML 报告)冒烟通过

请审查后合入;后续将 A2A 交 QA 审查。

likedislike