fix(#238): D9 段落块规则补充 —— 标题章节的子锚点行不截断块扫描
Some checks failed
gitleaks / gitleaks (pull_request) Has been cancelled
issue-gate / gate (pull_request) Has been cancelled
backend-test / commit-message-lint (pull_request) Has been cancelled
backend-test / backend-lint-test (pull_request) Has been cancelled
backend-test / frontend-lint-test (pull_request) Has been cancelled
issue-close-on-merge / close-issue (pull_request) Has been cancelled

复测取证(27b t3): `### 趋势对比` 标题锚点后紧跟子锚点行(`**接待量**:每日
趋势如下:`)+表格, 旧规则遇子锚点行即停 → 块=0 行误判趋势缺失。实际子锚点
行属于该章节的展开组织形态。

规则: 锚点本身是标题时, 段落块仅遇下一个标题终止(子 `**标签**:` 行继续扫,
其行体与表格数字归属该章节); 粗体锚点行为不变(仍遇下一锚点即停, 防他维
数据误归属)。8 非空行预算不变。

测试: 新增 3 用例(子锚点续扫取证复刻/子锚点无数字仍缺失/遇下一标题终止),
28/28 全过 + b23 复刻 12/12 + ai_feed 冒烟全过(容器内 runner 实测)。
This commit is contained in:
2026-10-12 12:22:32 +08:00
parent 4876bfa7bc
commit 86261a08cd
2 changed files with 48 additions and 6 deletions

View File

@@ -62,6 +62,8 @@ _EMPTY_TAILS = {"", ":", ":", "—", "-", "无", "无。", "无。"}
_DIGIT_RE = re.compile(r"\d")
# 段落块边界: `#` 标题行 或 下一个锚点行(`**标签**:`) → 块结束, 不再向后归属
_SECTION_STOP_RE = re.compile(r"^\s*(?:#{1,6}\s|\*\*[^*]{1,24}?\*\*[::])")
# 标题锚点的段落块边界: 仅 `#` 标题行(子锚点行属于章节展开, 见 _section_body_has_data)
_SECTION_STOP_HEADING_RE = re.compile(r"^\s*#{1,6}\s")
# 段落块最多向后看的非空行数(防跨段误归属)
_SECTION_MAX_LINES = 8
@@ -112,21 +114,28 @@ def _anchor_verdict(line: str, kw: str) -> bool | None:
return True
def _section_body_has_data(lines: list[str], anchor_idx: int) -> bool:
def _section_body_has_data(lines: list[str], anchor_idx: int,
heading_anchor: bool = False) -> bool:
"""D9(2026-10-12): 锚点行之后的段落块是否含数据。
解决"空锚点行(或首段缺失标记) + 完整数据列表/表格在后续行"被拒的模式
(生产 rolling_7d/30d 断供 5 天归因的主失败形态)。向后看最多 8 个非空行,
跳过空行(markdown 表格/列表与锚点行之间常见空行), 遇 `#` 标题或下一个
锚点行即停 —— 数据必须紧邻锚点行, 防跨段误归属。块内任一行含数字即判
有数据(报告是经营数字文档, 纯缺失陈述行不含数字)。
跳过空行(markdown 表格/列表与锚点行之间常见空行), 遇 `#` 标题即停;
粗体锚点还遇下一个锚点行即停 —— 数据必须紧邻锚点行, 防跨段误归属。
块内任一行含数字即判有数据(报告是经营数字文档, 纯缺失陈述行不含数字)。
heading_anchor(D9 补充, 2026-10-12 复测取证): 锚点本身是标题时, 其章节的
展开常以子锚点行组织(如 `## 趋势对比` → `**接待量**:每日趋势如下:` →
表格) —— 子锚点行不截断块扫描(它们属于该章节), 只有下一个标题终止。
实证: 27b t3 输出 `### 趋势对比` 后紧跟子锚点行, 旧规则块=0 行误判缺失。
"""
stop_re = (_SECTION_STOP_HEADING_RE if heading_anchor else _SECTION_STOP_RE)
scanned = 0
for ln in lines[anchor_idx + 1:]:
s = ln.strip()
if not s:
continue
if _SECTION_STOP_RE.match(ln):
if stop_re.match(ln):
break
scanned += 1
if scanned > _SECTION_MAX_LINES:
@@ -148,7 +157,8 @@ def _entry_covered(md: str, entry: str | list) -> bool:
v = _anchor_verdict(ln, kw_s)
if v is True:
return True
if v is None and _section_body_has_data(lines, i):
if v is None and _section_body_has_data(
lines, i, heading_anchor=bool(_HEADING_RE.match(ln))):
return True
return False

View File

@@ -210,6 +210,38 @@ def test_heading_missing_mark_missing():
assert out["missing"] == ["涉诉"]
def test_heading_block_continues_past_sub_anchors():
"""D9 复测取证(27b t3): 标题章节的展开以子锚点行组织 —— 子锚点行不截断
块扫描, 其后表格数字归属该章节。形态: `### 趋势对比` → `**接待量**:每日
趋势如下:` → 表格行。"""
md = ("### 趋势对比\n"
"**接待量**:每日趋势如下:\n"
"| 日期 | 接待量 |\n| 10-01 | 3,626 |\n| 10-02 | 3,590 |\n"
"**退款金额**:每日趋势如下:\n"
"| 日期 | 金额 |\n| 10-01 | 23,212 元 |\n")
t = _mk_tmpl(["趋势"])
out = check_dimensions(db=None, report=_mk_report(md), template=t)
assert out["complete"] is True
def test_heading_block_sub_anchors_without_data_missing():
"""D9 边界: 标题锚点 + 子锚点行均无数字(纯定性叙述) → 仍缺失。"""
md = "### 趋势对比\n**接待量**:每日趋势如下:\n整体呈波动走势。\n"
t = _mk_tmpl(["趋势"])
out = check_dimensions(db=None, report=_mk_report(md), template=t)
assert out["complete"] is False
assert out["missing"] == ["趋势"]
def test_heading_block_stops_at_next_heading():
"""D9 边界: 标题锚点的段落块遇下一个标题即终止, 后节数据不归属本节。"""
md = "### 趋势对比\n整体走势平稳。\n\n## 投诉分析\n投诉 1,182 件。\n"
t = _mk_tmpl(["趋势"])
out = check_dimensions(db=None, report=_mk_report(md), template=t)
assert out["complete"] is False
assert out["missing"] == ["趋势"]
def test_h1_not_anchor():
"""H1 是文档标题, 不作锚点(防标题党误判)。"""
md = "# 四路分布\n今日天气晴朗。\n"