feat(check): 规则检查(报价形式、形式要件、响应表),表格按单元格分行

- check/rules.py(规则集 2026.11-r1,不调模型):大小写金额、分项合计、超最高限价、
  多个报价、别的项目名称、没填的地方、落款日期、投标有效期、附加条件、响应表里的未响应;
  每条挂投标原文,有依据的挂招标侧出处;缺限价、截止时间、有效期要求时写进覆盖范围
- 文字层的句子带上单元格框,表格按单元格上沿分行
- 按 Fable 审读意见收紧致命级:认不准的降为重要或提示
- 评测:score.py 评规则问题;P07 注入记录改为空白处前后原文,剔除注入到人员表的 D10
- 上传页的检查范围文案同步

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-04 16:44:22 +08:00
parent 47c6a1258e
commit db1b1f6f9f
17 changed files with 1792 additions and 20 deletions

View File

@@ -59,6 +59,7 @@ class Sentence:
fmt_runs: list[tuple[int, int, str]] = field(default_factory=list) # [起, 止, 格式],格式取 b / i / u 的组合
location: str = "body"
open_end: bool = False # 写到页面末尾还没结束(没有句末标点、末行写满),可能接续到下一页
cell: list[float] | None = None # 在有框线的表格里:所在单元格 [x0, top, x1, bottom](查标按单元格分行)
@property
def marked(self) -> str:
@@ -83,6 +84,7 @@ class Sentence:
"format_runs": [list(r) for r in self.fmt_runs],
"location": self.location,
"open_end": self.open_end,
**({"cell": self.cell} if self.cell else {}),
}
@classmethod
@@ -96,6 +98,7 @@ class Sentence:
fmt_runs=[(a, b, f) for a, b, f in d["format_runs"]],
location=d["location"],
open_end=d.get("open_end", False),
cell=d.get("cell"),
)
@@ -150,6 +153,7 @@ Cell = tuple[float, float, float, float] # 单元格 (x0, top, x1, bottom)
class FragInfo:
right: float | None = None # 单元格的右边界:判断段落末行用它,不用页面右边距
cell_end: bool = False # 单元格的最后一个片段:句子在这里结束
cell: Cell | None = None
def _cell_of(c: dict[str, Any], cells: list[Cell]) -> int | None:
@@ -171,13 +175,13 @@ def _fragments(
for i, cell_chars in by_cell.items():
assert i is not None
x0, top, x1, _ = cells[i]
blocks.append(((top, x0), _line_fragments(cell_chars), FragInfo(right=x1)))
blocks.append(((top, x0), _line_fragments(cell_chars), FragInfo(right=x1, cell=cells[i])))
frags: list[list[dict[str, Any]]] = []
infos: list[FragInfo] = []
for _, fs, info in sorted(blocks, key=lambda b: b[0]):
for j, f in enumerate(fs):
frags.append(f)
infos.append(FragInfo(right=info.right, cell_end=j == len(fs) - 1) if info else FragInfo())
infos.append(FragInfo(right=info.right, cell_end=j == len(fs) - 1, cell=info.cell) if info else FragInfo())
return frags, infos
@@ -310,6 +314,7 @@ def _page(doc_id: str, page: Any, frags: list[list[dict[str, Any]]], infos: list
fmt_runs=runs,
location=location,
open_end=seq == tail_seq,
cell=[round(v, 2) for v in cell] if (cell := infos[g[0][0]].cell) and location == "body" else None,
)
)
return Page(