fix: stop heading pass from splitting last character off heading lines - #47
Conversation
The "ensure blank line after heading" regex matched (heading)([^\n]), whose second group can never be the newline that follows the heading line, so backtracking consumed the heading's own last character instead: "# Title" became "# Titl\n\ne", render_html emitted <h1>Titl</h1><p>e</p>, and re-running enhance() kept shredding the tail (non-idempotent). Anchor the pass to true line starts ((?m)^), require the literal trailing newline, and insert a newline only when the next line is non-empty (lookahead), which makes the pass idempotent and purely insertive. Adds 6 heading-integrity regression tests (all failed on the previous implementation, all pass now).
XiaoCow666
left a comment
There was a problem hiding this comment.
代码修复方向合理;本次必要修改项是阶段六任务台缺少必需证据,不是 PR 描述不足。
- P2:补齐任务台交付记录。 TASK CONTEXT 中
problem_goal、reproduction_evidence、planned_changes、learning_summary、verification_result均为空,无法从任务台核对本阶段的故障收口和移交结果。请打开“阶段六:CodeSense 接管演练与故障收口(飞书用户5386EE)”任务记录,将这些空字段分别补为:标题末字符拆分现象与目标、具体输入及修复前后输出、utils/markdown_formatter.py的正则改动和六项测试、排查结论及围栏处理遗留问题、实际执行命令与结果。然后运行python -m pytest tests/test_markdown_formatter.py -q,将真实输出和对应提交 SHA 记录到verification_result;全量测试结果请附可核对日志,并区分 pytest 与外层任务的退出状态。PR 正文已有具体材料,可据实整理到相应字段。
已检查范围:仅静态分析所提供的 diff,未运行任何命令。utils/markdown_formatter.py 的 enhance() 改用字面换行和前瞻,替换保留完整捕获内容,消除了旧正则将标题末字符拆出的机制;tests/test_markdown_formatter.py 新增六项回归测试,覆盖孤立标题、正文间距、幂等、已有空行、末尾换行及 HTML 输出。满足阶段二以后要求的真实行为改动与回归测试交付,未发现代码层面的必要修改项。
非阻塞建议:修正 PR“学习总结”中对替代正则 (#{1,6}[^\n]+)\n([^\n]) 的解释:其中字面换行也阻止以标题末字符充当第二组,已有空行时不会按正文所述拆字符。可以说明采用前瞻是为了不消耗下一行字符。首次贡献的验证说明请继续保留具体复现输入、期望输出、测试命令,并明确区分已运行结果与推断。
Follow-up to review on XiaoCow666#47: the unanchored pre-fix heading pass also split an inline "#" line ("a #1 fan\nnext" -> "a #1 fa\n\nn\nnext"), the same data-loss mechanism as the standalone-heading symptom. Lock the (?m)^ line-start anchor behavior with a regression test so an inline hash stays byte-for-byte untouched.
XiaoCow666
left a comment
There was a problem hiding this comment.
结论:通过。仅根据提供的元数据与 diff 静态复审,未发现必须修改的问题;未调用工具或运行测试。
已检查范围:
utils/markdown_formatter.py的enhance()标题后空行处理:新正则以(?m)^锚定行首,捕获内容以字面换行结尾,替换保留捕获内容并追加换行,消除了旧写法回溯拆走标题末字符的问题。前瞻使已有空行时不再插入,支持该处理步骤的幂等性。tests/test_markdown_formatter.py新增了 7 项回归测试,覆盖独立标题、标题接正文、重复增强、已有空行、末尾换行、HTML 渲染和行中井号,包含真实行为改动及对应回归覆盖。- 任务台提供了具体复现输出、根因、学习总结和红绿测试记录,符合阶段六交付方向。测试结果属于贡献者提供的记录,本次未独立验证。
后续建议(不阻塞):
- 当前 head 为
5312434,任务台verification_result仍对应6f188b5,且planned_changes、PR 正文写新增 6 项测试,diff 实际为 7 项。请在当前 head 运行python -m pytest tests/test_markdown_formatter.py -q,将提交号、实际数量、输出和退出码更新到上述字段;全量结果保留其实际对应提交,避免混为当前提交验证。 - 后续可为围栏内标题样式文本单独建立修复任务,加入围栏内容保持不变的测试。该限制已在移交说明中明确,本次不阻塞。
- 首次贡献的 PR 描述可沿用“输入与实际输出 → 修改后的行为 → 验证命令及对应提交”的结构;当前内容已具体,更新测试记录即可。
理解抽查:
- 对
# Title\nBody text,新正则的捕获组和前瞻分别匹配什么?为什么第二次处理不会继续插入空行? - 新增的
a #1 fan\nnext用例如何验证行首锚定?为什么它不能证明所有包含行中井号的文本都不会被前一个标题处理步骤修改?
合并前隔离验证
- 结果:未通过
- 命令:gh repo clone
- 耗时:9169ms
- 摘要:Cloning into 'E:\CodeSense\pr-review-relay\data\work\premerge-0pci6J\repo'...
fatal: unable to access 'https://github.com/XiaoCow666/CodeSense.git/': schannel: failed to receive handshake, SSL/TLS connection failed
failed to run git: exit status 128
合并前验证未通过,本次不合并。请按上面摘要逐条处理:打开摘要中点名的文件和位置,把当前错误改成命令要求的行为;修复后重新运行同一条命令,再提交新的 head。
|
感谢复审,回应以下两点: 理解抽查第 1 题:对 第一次处理:
第二次处理(输入已含空行):
第 2 题: 探针证实(旧正则下):
该 L53 同类缺陷已按复审非阻塞意见作为遗留点移交: 关于 premerge 隔离验证未通过失败发生在 贡献方隔离验证(干净 venv,D:\Python311 新建,
均在当前 head |
XiaoCow666
left a comment
There was a problem hiding this comment.
CodeSense 自动评审
修复了 H1 标题末字符被后置空行规则吞掉的问题,但现有的“标题前空行”规则仍会把标准 H2–H6 标题拆成两个标题,导致本 PR 所称的标题完整性修复未覆盖全部 #{1,6} 标题级别,属于可复现的用户可见功能错误。
需要先处理的问题
- utils/markdown_formatter.py:53 的
re.sub(r'([^\n])(#{1,6}\s)', ...)未限制#必须位于行首。对于## Title\nBody text,该规则会把第一个#当作前置字符、把第二个#当作标题开始,先生成#\n\n# Title\nBody text;随后本 PR 新增的后置规则再生成#\n\n# Title\n\nBody text。这将原本的 H2 标题拆为一个空 H1 和一个 H1,H3–H6 同样受影响。PR 修改的后置规则本身支持#{1,6},但测试仅覆盖 H1,当前实现未完成标题行完整性目标。
请按下面的步骤修改
- 文件:utils/markdown_formatter.py,位置:enhance() 中“确保标题前后有空行”的第一条规则(当前约 L53)。当前问题:
([^\n])(#{1,6}\s)可在同一行内把一个#作为前置文本、把紧随其后的#当标题开头,从而拆坏 H2–H6。应改成只识别真实行首的 ATX 标题,并通过匹配“上一非空行结尾 + 换行 + 行首标题”来仅插入空行,例如采用等价于re.sub(r'(?m)([^\n])\n(#{1,6}[ \t])', r'\1\n\n\2', text)的实现;同时确认该规则与标题后置空行规则组合后不搬移任何标题字符。AI 操作提示:重写标题前空行 pass,使标题标记只能在物理行首匹配,禁止用相邻#充当上一段文本;保留既有输入输出的换行风格和幂等行为。 - 文件:tests/test_markdown_formatter.py,位置:新增的 heading integrity 回归测试段。当前问题:测试只验证了 H1,未覆盖会被 L53 拆坏的 H2–H6,因此当前测试全绿仍遗漏可复现的标题损坏。应新增至少一个
## Title\nBody text的完整性断言,并最好参数化覆盖##至######,断言增强结果保留原始标题级别和标题文本、仅在标题后插入一个空行,且二次 enhance 结果相同。AI 操作提示:添加 H2–H6 回归用例;对每级输入'<marks> Title\nBody text',断言输出为'<marks> Title\n\nBody text',并断言enhance(enhance(input)) == enhance(input)。
已有验证信息
- 事件提供的 PR 描述声称当前 head 上执行
python -m pytest tests/test_markdown_formatter.py -q得到48 passed in 0.11s、执行pytest tests -q得到577 passed, 1360 warnings且 pytest 退出码为 0。 - 提供的 diff 显示新增的 7 个回归测试覆盖孤立 H1、H1 后正文空行、幂等、已有空行、末尾换行、HTML 输出和行内
#1,但未覆盖 H2–H6。
评审事件:2929aeba-b262-11f1-98ef-3a2e94352202
XiaoCow666
left a comment
There was a problem hiding this comment.
CodeSense 自动评审
修复将标题前后空行处理限制到真实行边界,并避免通过回溯搬移标题末字符;新增回归测试覆盖 H1-H6、幂等性、HTML 渲染和行内井号场景。未发现必须阻塞合并的安全、数据损失、明显回归或任务未完成问题。
合并后可以继续改进的地方
- 代码围栏内以 # 开头的内容仍可能被标题空行处理修改;PR 描述已明确这是既有范围外问题。后续可在 utils/markdown_formatter.py 的 enhance() 标题空行处理前后引入围栏感知,或将标题处理并入现有围栏感知的行扫描流程,并补充围栏内 # 行保持原样的测试。
已有验证信息
- PR 提供的当前 head 验证证据:python -m pytest tests/test_markdown_formatter.py -q 报告 48 passed,退出码 0。
- PR 提供的当前 head 全量验证证据:pytest tests -q 报告 577 passed、1360 warnings,pytest 退出码 0。
- Diff 中新增测试覆盖孤立标题、标题与正文间空行、已有空行、尾随换行、H2-H6 标题级别、重复 enhance() 幂等性、渲染 HTML 完整性及行内 # 不被识别为标题。
评审事件:df5f5810-b2ad-11f1-94ae-c4efff5eea59
一、问题/目标
用户现象(不完整描述):AI 建议里的独立 Markdown 标题末字符丢失并被拆成独立段落,
# Title显示为标题Titl+ 段落e。目标:修复标题后置空行 pass 的字符搬移缺陷,恢复两条数据一致性不变量——规范化只允许插入空白字符、不得搬移既有字符;对同一输入重复增强结果稳定(幂等)。二、复现与证据
探针(D:\Python311 直跑,模块纯标准库,零依赖):
修复前实际输出:
对输出再跑一次 enhance(),标题继续碎裂(Titl+e → Tit+l+e),证明不幂等。修复后同命令输出
'# Title'/'# Title\n\nBody text'/'<h1>Title</h1>',二次增强结果不变。RED 基线(首个提交 6f188b5):当时新增的 6 个回归测试全部失败(
6 failed, 41 passed),失败 diff 与上述探针逐字一致;追加提交 5312434 增加第 7 项(见"计划改动")。三、排查顺序与排除过的假设
(#{1,6}[^\n]+)([^\n])(当时位于 L54,修复后新正则在 L57),其第二捕获组[^\n]结构上永远匹配不到换行符,回溯机制被迫吞掉标题行内最后一个字符text # foo\nbar被劈开为text \n\n# foo\nbar),同类缺陷按复审非阻塞意见移交、本轮不扩 scope(见"学习总结")四、计划改动(已完成)
utils/markdown_formatter.py的 enhance() 一处正则:tests/test_markdown_formatter.py新增 7 项回归测试(6f188b5 六项 + 5312434 追加一项),并同步更新模块 docstring(原"标题路径有缺陷、故意不锁定"表述已失效):enhance('# Title') == '# Title''# Title\nBody text'→'# Title\n\nBody text'enhance(enhance(x)) == enhance(x)'# Title\n\nBody text'原样'# Title\n'原样<h1>Title</h1>,不含<p>e</p>'a #1 fan\nnext'原样——旧无锚定 pass 会在行中位置命中#1、拆成a #1 fa\n\nn\nnext,该用例锁定(?m)^行首锚定五、学习总结(含 AI 辅助边界)
(?m)^与[^\n]语义 → 采纳行首锚定惯用法(#{1,6}[^\n]+)\n([^\n])的复核(采纳复审指正并经探针验证):其中字面\n同样阻止回溯以标题末字符充当第二组,已有空行时不会拆字符——"换一种方式坏"的初判有误。未采用的真实原因:①缺(?m)^行首锚定,会命中段落行中的#片段(探针实测a #1 fan\nnext被拆为a #1 fa\n\nn\nnext);②第二捕获组消耗下一行首字符、替换必须回填,非纯插入。采用前瞻正是为了零宽消耗、纯插入a #1 fan\nnext只证明新 pass(L57)的锚定行为,不能证明所有行中井号安全——#1中#后无空格,恰好绕过了同样未做行首锚定的 L53"标题前补空行"pass;探针证实text # foo\nbar仍会被 L53 劈开。一条用例只锁定它覆盖的那条路径([^\n])(#{1,6}\s)同样缺(?m)^行首锚定,行中"# 后带空格"会被劈开,建议参照本次修法统一加锚定,并补"围栏内 # 保持不变"的用例;② 代码围栏内的#行仍会被标题 pass 插入空行(不吞字符但语义不对),建议参照_fix_lists的围栏感知做行扫描统一处理python -m pytest tests/test_markdown_formatter.py -q;全量pytest tests -q验证结果
干净环境:
D:\Python311\python.exe -m venv新建隔离环境,pip install -r requirements-test.txt,PYTHONDONTWRITEBYTECODE=1,不连数据库/Redis、不启动应用。红绿基线(修复前代码):
6 failed, 41 passed——六项 heading integrity 用例在旧正则下全红,失败内容与第二节探针一致当前 head 5312434 单文件(真实重跑):
python -m pytest tests/test_markdown_formatter.py -q
................................................ [100%]
48 passed in 0.11s
pytest 退出码:0
当前 head 5312434 全量回归:
pytest tests -q
577 passed, 1360 warnings in 530.37s (0:08:50)
PYTEST_EXIT=0
577 = 6f188b5 全量 576(当时 main 基线 529 + test: characterize MarkdownFormatter normalization and rendering #44 的 41 + 首轮 6)+ 5312434 追加 1;两提交结果分开对应、不混算。1360 条 warning 均为既有 SQLAlchemy LegacyAPIWarning,与本改动无关
退出状态区分:回放中一次后台任务 exit 2 系 PIP_EXIT=2(依赖下载中断)导致 41 个 collection error,前台重装 PIP_EXIT=0 后重跑全绿;另一次任务壳 exit 1 系沙箱拦截
D:\Python311\Lib\__pycache__字节码写入。两类均为环境/外层噪音,pytest 自身退出码均为 0,以 pytest 汇总行为准风险与回滚
一处正则 + 测试新增,无接口/数据库/权限/安全/部署改动。回滚:revert 6f188b5、5312434 两个提交,或恢复旧正则并删除"heading integrity"测试段。