fix(extract): ★ 一律按实质性条款处理(口径 Q2 修订)
文件把 ★ 定义成计分项(S02「加★为核心技术参数」)时不再降为「重要」: 删掉 star_scored / STAR_IMPORTANT 及相关分支,不再生成 meaning = important 的标注规则。 S02 的 11 条 ★ 要求离线验证全部为实质性。决策记入 docs/decisions.md。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -50,7 +50,7 @@ Fable 用样本外的写法逐条验证过,标「已验证」的是复现过
|
|||||||
| `INVALID` 新增「不得参加 / 被拒绝 / 拒绝接受」 | 「评标委员会成员不得参加评审」「不得参加后续培训」「甲方有权拒绝接受不合格货物」(已验证) | 一般要求冒充写明无效;只排除了合同章,而且前提是章识别成功 |
|
| `INVALID` 新增「不得参加 / 被拒绝 / 拒绝接受」 | 「评标委员会成员不得参加评审」「不得参加后续培训」「甲方有权拒绝接受不合格货物」(已验证) | 一般要求冒充写明无效;只排除了合同章,而且前提是章识别成功 |
|
||||||
| 概括句过滤 `BLANKET` / leads | 「上述证书原件须在开标时携带,否则投标无效」本身是一条要求(已验证) | 被当成引导句丢掉,兜底也不补 |
|
| 概括句过滤 `BLANKET` / leads | 「上述证书原件须在开标时携带,否则投标无效」本身是一条要求(已验证) | 被当成引导句丢掉,兜底也不补 |
|
||||||
| ★ 节 `star_sections` | 「★1 核心交换机」被当成节标题(已验证);后面没有同级编号时,范围最多延伸 800 句 | S10 一个标题覆盖 381 句、产出 225 条致命,没有人逐条核对 |
|
| ★ 节 `star_sections` | 「★1 核心交换机」被当成节标题(已验证);后面没有同级编号时,范围最多延伸 800 句 | S10 一个标题覆盖 381 句、产出 225 条致命,没有人逐条核对 |
|
||||||
| ★ 降级 `star_scored`(Q2) | 后果写在不带 ★ 的句子里(「重要技术参数负偏离超过三项的,投标无效」)(已验证) | 真正的废标 ★ 被显示成「重要」 |
|
| ~~★ 降级 `star_scored`(Q2)~~ | 2026-11-05 Q2 修订后 ★ 一律按实质性,降级已删除,这条风险不再存在 | — |
|
||||||
| 评标方法 `EVAL_METHOD` | 招标投标体系的「综合评估法」「经评审的最低投标价法」 | 项目信息答不出 |
|
| 评标方法 `EVAL_METHOD` | 招标投标体系的「综合评估法」「经评审的最低投标价法」 | 项目信息答不出 |
|
||||||
| 阈值 | 表后往回 40 句、行长 ≤ 60 字、★ 节范围 800 句、最后一章不超过一半、92% | 都是看样本定的,没有样本外依据 |
|
| 阈值 | 表后往回 40 句、行长 ≤ 60 字、★ 节范围 800 句、最后一章不超过一半、92% | 都是看样本定的,没有样本外依据 |
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ Fable 用样本外的写法逐条验证过,标「已验证」的是复现过
|
|||||||
- 「拒绝接受」只认主语是采购人或代理机构、对象是投标文件的写法;
|
- 「拒绝接受」只认主语是采购人或代理机构、对象是投标文件的写法;
|
||||||
- 按法律体系补「作废标处理」「予以废标」(招标投标体系里等于否决投标;政府采购里「废标」多指整个采购终止,不能混用)。
|
- 按法律体系补「作废标处理」「予以废标」(招标投标体系里等于否决投标;政府采购里「废标」多指整个采购终止,不能混用)。
|
||||||
4. **概括句过滤**:只在上下确实有编号列表时生效,句子本身写着具体要求的不过滤。
|
4. **概括句过滤**:只在上下确实有编号列表时生效,句子本身写着具体要求的不过滤。
|
||||||
5. **★ 降级**:带后果的句子里没有 ★,但提到了 ★ 的定义词(「核心技术参数」「重要技术参数」),也不降级。
|
5. ~~**★ 降级**~~:不再需要(2026-11-05 Q2 修订:★ 一律按实质性)。
|
||||||
6. **章识别**:
|
6. **章识别**:
|
||||||
- 支持「第1章」「第一部分」、章号和标题拆成两句;
|
- 支持「第1章」「第一部分」、章号和标题拆成两句;
|
||||||
- 认不出时计数 `chapters_unreliable`,写进解析结果;
|
- 认不出时计数 `chapters_unreliable`,写进解析结果;
|
||||||
|
|||||||
@@ -4,6 +4,14 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Q2(修订)· 2026-11-05 · ★ 一律按实质性条款处理
|
||||||
|
|
||||||
|
| # | 决策 | 理由 |
|
||||||
|
|---|---|---|
|
||||||
|
| Q2 | 修订原决策:文件把 ★ 定义成计分项(如 S02「加★为核心技术参数」、不满足只扣分)时,**也不再降为「重要」**,★ 一律按实质性要求;不再生成 meaning = important 的标注规则 | 用户在读标、查标走查后决定。走查时 S02 的 ★ 条款显示「重要」、负偏离只报「重要」,和用户对 ★ 的理解不一致。按实质性处理的代价是计分类 ★ 的负偏离会报成致命,用户可以「确认无误」并写说明(会作为误报反馈);降级的风险是漏掉真正导致无效的条款 |
|
||||||
|
|
||||||
|
- **影响**:`checklist/extract.py` 删掉 `star_scored` 和 `STAR_IMPORTANT`;已解析的项目要重新解析才会变。查标评测开发集里 S02 的级别分布会变(致命级多出 ★ 条款),下次跑评测时注意和旧数字不可直接比。
|
||||||
|
|
||||||
## AD1–AD6 · 2026-11-05 · 补遗追加:全部按建议
|
## AD1–AD6 · 2026-11-05 · 补遗追加:全部按建议
|
||||||
|
|
||||||
依据 `docs/architecture/addendum-plan.md` §7。
|
依据 `docs/architecture/addendum-plan.md` §7。
|
||||||
|
|||||||
@@ -65,9 +65,6 @@ CN_DIGIT = {"零": 0, "〇": 0, "一": 1, "二": 2, "两": 2, "三": 3, "四": 4
|
|||||||
CN_UNIT = {"十": 10, "百": 100, "千": 1000}
|
CN_UNIT = {"十": 10, "百": 100, "千": 1000}
|
||||||
END_PUNCT = "。;;,,、::!!??"
|
END_PUNCT = "。;;,,、::!!??"
|
||||||
OUR_MARKUP = re.compile(r"</?(?:iu|u|i)>|〔(?:续下页|上级:[^〕]*|表后说明:[^〕]*)〕")
|
OUR_MARKUP = re.compile(r"</?(?:iu|u|i)>|〔(?:续下页|上级:[^〕]*|表后说明:[^〕]*)〕")
|
||||||
# ★ 的含义被文件定义成别的(「加★为核心技术参数」),而且没有任何地方说 ★ 是实质性要求或不满足就无效(口径 Q2)
|
|
||||||
STAR_DEFINED = re.compile(r"(?:加|标|打|带)\s*[“\"]?★[”\"]?\s*号?\s*(?:的|为|是|表示)+\s*([^,。;]{0,12})")
|
|
||||||
STAR_IMPORTANT = "star:important" # 放进 Doc.declared:这份文件的 ★ 只是「重要」,不是实质性
|
|
||||||
# 「必须」「应」字样的要求都是实质性要求(S07 第 18 页):只在文件层面提示,不逐句抽(口径 Q3)
|
# 「必须」「应」字样的要求都是实质性要求(S07 第 18 页):只在文件层面提示,不逐句抽(口径 Q3)
|
||||||
KEYWORD_NOTE = re.compile(r"[“\"](?:必须|应|应当|应(应当))[”\"]")
|
KEYWORD_NOTE = re.compile(r"[“\"](?:必须|应|应当|应(应当))[”\"]")
|
||||||
SID = re.compile(r"^p\d+s\d+$")
|
SID = re.compile(r"^p\d+s\d+$")
|
||||||
@@ -192,42 +189,13 @@ def marking_notes(sentences: list[Sentence]) -> list[MarkingNote]:
|
|||||||
markers.add("keyword")
|
markers.add("keyword")
|
||||||
m = DECLARED.search(s.text)
|
m = DECLARED.search(s.text)
|
||||||
notes.append(MarkingNote(markers=markers, sentence=s, declared_count=int(m.group(1)) if m else None))
|
notes.append(MarkingNote(markers=markers, sentence=s, declared_count=int(m.group(1)) if m else None))
|
||||||
notes = notes[:10]
|
return notes[:10]
|
||||||
if not any("star" in n.markers for n in notes) and (d := star_scored(sentences)) is not None:
|
|
||||||
notes.append(MarkingNote(markers={"star"}, sentence=d, declared_count=None, meaning="important"))
|
|
||||||
return notes
|
|
||||||
|
|
||||||
|
|
||||||
STAR_CONSEQUENCE = re.compile(r"废标|负偏离|不得偏离|未响应|不响应|不予通过|实质|否决|拒绝")
|
|
||||||
SCORED = re.compile(r"扣\s*\d|减\s*\d|扣分|得分|加分|计分|评分")
|
|
||||||
|
|
||||||
|
|
||||||
def star_scored(sentences: list[Sentence]) -> Sentence | None:
|
|
||||||
"""★ 只是计分项的定义句(口径 Q2,S02「备注:加★为核心技术参数」),没有就返回 None。
|
|
||||||
|
|
||||||
降级的代价是把致命问题显示成「重要」,所以条件从严(顾问审读第 1 条):
|
|
||||||
1. 定义句说 ★ 是什么(「核心技术参数」),这句本身不带任何后果写法;
|
|
||||||
2. 有计分证据:这个说法出现在写着扣分、得分的句子里(S02 p29「每有一项核心技术参数不满足……减 4 分」);
|
|
||||||
3. 全文没有一句带 ★ 的话写着无效、废标、负偏离、未响应、实质性这类后果。
|
|
||||||
"""
|
|
||||||
if any("★" in s.text and (INVALID.search(s.text) or STAR_CONSEQUENCE.search(s.text)) for s in sentences):
|
|
||||||
return None
|
|
||||||
for s in sentences:
|
|
||||||
m = STAR_DEFINED.search(s.text)
|
|
||||||
if not m or INVALID.search(s.text) or STAR_CONSEQUENCE.search(s.text) or re.search(r"必须|关键", s.text):
|
|
||||||
continue
|
|
||||||
term = re.sub(r"^[的为是]+", "", m.group(1)).strip()
|
|
||||||
if len(term) >= 4 and any(term in x.text and SCORED.search(x.text) for x in sentences if x is not s):
|
|
||||||
return s
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def build_doc(sentences: list[Sentence]) -> Doc:
|
def build_doc(sentences: list[Sentence]) -> Doc:
|
||||||
"""一份文档给各抽取任务共用的结构信息(解析流水线和离线评测都用它)。"""
|
"""一份文档给各抽取任务共用的结构信息(解析流水线和离线评测都用它)。"""
|
||||||
notes = marking_notes(sentences)
|
notes = marking_notes(sentences)
|
||||||
declared = {m for n in notes if n.meaning == "substantive" for m in n.markers}
|
declared = {m for n in notes if n.meaning == "substantive" for m in n.markers}
|
||||||
if any(n.meaning == "important" and "star" in n.markers for n in notes):
|
|
||||||
declared.add(STAR_IMPORTANT)
|
|
||||||
return Doc(
|
return Doc(
|
||||||
sentences=sentences,
|
sentences=sentences,
|
||||||
parents=structure.parents(sentences),
|
parents=structure.parents(sentences),
|
||||||
@@ -236,7 +204,7 @@ def build_doc(sentences: list[Sentence]) -> Doc:
|
|||||||
declared=declared,
|
declared=declared,
|
||||||
chapters=structure.chapters(sentences),
|
chapters=structure.chapters(sentences),
|
||||||
trailing=structure.trailing_notes(sentences),
|
trailing=structure.trailing_notes(sentences),
|
||||||
star_scope=structure.star_sections(sentences) if STAR_IMPORTANT not in declared else {},
|
star_scope=structure.star_sections(sentences),
|
||||||
regime=legal_regime_by_rule(sentences),
|
regime=legal_regime_by_rule(sentences),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -254,7 +222,7 @@ def is_marked(s: Sentence, declared: set[str]) -> str | None:
|
|||||||
if is_marker_talk(s):
|
if is_marker_talk(s):
|
||||||
return None
|
return None
|
||||||
if "★" in s.text:
|
if "★" in s.text:
|
||||||
return None if STAR_IMPORTANT in declared else "star"
|
return "star" # ★ 一律按实质性要求(口径 Q2 修订)
|
||||||
if "asterisk" in declared and ASTERISK.match(s.text.strip()):
|
if "asterisk" in declared and ASTERISK.match(s.text.strip()):
|
||||||
return "asterisk"
|
return "asterisk"
|
||||||
iu = sum(e - b for b, e, f in s.fmt_runs if "i" in f and "u" in f)
|
iu = sum(e - b for b, e, f in s.fmt_runs if "i" in f and "u" in f)
|
||||||
@@ -270,7 +238,7 @@ def inherited_star(s: Sentence, doc: Doc) -> bool:
|
|||||||
|
|
||||||
def importance_of(sents: list[Sentence], doc: Doc) -> tuple[str, str | None]:
|
def importance_of(sents: list[Sentence], doc: Doc) -> tuple[str, str | None]:
|
||||||
"""重要性按原文标注确定,不信模型:★ / 声明过的标注 / ★ 节里的句子 → 实质性;▲ → 重要。
|
"""重要性按原文标注确定,不信模型:★ / 声明过的标注 / ★ 节里的句子 → 实质性;▲ → 重要。
|
||||||
文件把 ★ 定义成计分项时(口径 Q2),★ 和 ▲ 一样是重要。"""
|
文件把 ★ 定义成计分项(「加★为核心技术参数」)也按实质性(口径 Q2 修订,2026-11-05)。"""
|
||||||
if any(is_marked(s, doc.declared) or inherited_star(s, doc) for s in sents):
|
if any(is_marked(s, doc.declared) or inherited_star(s, doc) for s in sents):
|
||||||
return "substantive", "marker"
|
return "substantive", "marker"
|
||||||
if any("▲" in s.text or "★" in s.text for s in sents):
|
if any("▲" in s.text or "★" in s.text for s in sents):
|
||||||
@@ -349,14 +317,7 @@ def _render(chunk: list[Sentence], doc: Doc, marks: bool = False) -> str:
|
|||||||
if s.key in doc.continuations:
|
if s.key in doc.continuations:
|
||||||
line += "〔续下页〕" # 这句在页尾被截断,下一行是它在下一页的接续部分
|
line += "〔续下页〕" # 这句在页尾被截断,下一行是它在下一页的接续部分
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
notes = (
|
notes = "\n".join(f"- {n.sentence.text[:200]}" for n in doc.notes) or "(未找到标注规则说明)"
|
||||||
"\n".join(
|
|
||||||
f"- {n.sentence.text[:200]}"
|
|
||||||
+ ("(本文件的 ★ 只是计分项,不是实质性标注)" if n.meaning == "important" else "")
|
|
||||||
for n in doc.notes
|
|
||||||
)
|
|
||||||
or "(未找到标注规则说明)"
|
|
||||||
)
|
|
||||||
return "## 标注规则说明\n" + notes + "\n\n## 待审分块\n" + "\n".join(lines)
|
return "## 标注规则说明\n" + notes + "\n\n## 待审分块\n" + "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
@@ -565,13 +526,6 @@ def _item(
|
|||||||
if "basis" not in kw and importance == "substantive":
|
if "basis" not in kw and importance == "substantive":
|
||||||
# 按哪种标注认定为实质性:清单项据此挂到对应的标注规则上;★ 节里的句子挂 ★
|
# 按哪种标注认定为实质性:清单项据此挂到对应的标注规则上;★ 节里的句子挂 ★
|
||||||
kw["basis"] = next((m for x in sents if (m := is_marked(x, doc.declared))), "star")
|
kw["basis"] = next((m for x in sents if (m := is_marked(x, doc.declared))), "star")
|
||||||
elif (
|
|
||||||
"basis" not in kw
|
|
||||||
and importance == "important"
|
|
||||||
and STAR_IMPORTANT in doc.declared
|
|
||||||
and any("★" in x.text for x in sents)
|
|
||||||
):
|
|
||||||
kw["basis"] = "star" # ★ 只是「重要」的文件也挂到 ★ 规则上,清单页「已识别 N 条」才数得对
|
|
||||||
if importance == "substantive" and "invalidity" not in categories:
|
if importance == "substantive" and "invalidity" not in categories:
|
||||||
categories = [*categories, "invalidity"] # ★ 条款同时进「无效 / 废标」类(FR-M2-05)
|
categories = [*categories, "invalidity"] # ★ 条款同时进「无效 / 废标」类(FR-M2-05)
|
||||||
return Extracted(
|
return Extracted(
|
||||||
@@ -608,7 +562,6 @@ def _substantive(
|
|||||||
basis=basis,
|
basis=basis,
|
||||||
spec={"invalidity": {"source": "substantive_marker" if basis != "explicit_invalid" else "tender_text"}},
|
spec={"invalidity": {"source": "substantive_marker" if basis != "explicit_invalid" else "tender_text"}},
|
||||||
)
|
)
|
||||||
if not (basis == "star" and STAR_IMPORTANT in doc.declared): # ★ 只是计分项的文件:按 importance_of 的结果
|
|
||||||
e.importance = "substantive"
|
e.importance = "substantive"
|
||||||
e.importance_basis = e.importance_basis or ("tender_text" if basis == "explicit_invalid" else "marker")
|
e.importance_basis = e.importance_basis or ("tender_text" if basis == "explicit_invalid" else "marker")
|
||||||
return e
|
return e
|
||||||
|
|||||||
@@ -469,21 +469,16 @@ def test_star_section_heading() -> None:
|
|||||||
assert importance_of([ss[4]], d) == ("normal", None)
|
assert importance_of([ss[4]], d) == ("normal", None)
|
||||||
|
|
||||||
|
|
||||||
def test_star_defined_as_scored_is_important() -> None:
|
def test_star_defined_as_scored_stays_substantive() -> None:
|
||||||
"""文件把 ★ 定义成「核心技术参数」、不满足只扣分(S02):★ 是「重要」,不是实质性(口径 Q2)。"""
|
"""文件把 ★ 定义成「核心技术参数」、不满足只扣分(S02):★ 仍按实质性要求(口径 Q2 修订)。"""
|
||||||
from bid_doctor.checklist.extract import build_doc, importance_of
|
from bid_doctor.checklist.extract import build_doc, importance_of
|
||||||
|
|
||||||
star, define = sent("★设备采用国产自主可控CPU芯片。", seq=1), sent("备注:加★为核心技术参数;", seq=2)
|
star, define = sent("★设备采用国产自主可控CPU芯片。", seq=1), sent("备注:加★为核心技术参数;", seq=2)
|
||||||
scored = sent("每有一项核心技术参数不满足的减4分。", seq=3)
|
scored = sent("每有一项核心技术参数不满足的减4分。", seq=3)
|
||||||
d = build_doc([star, define, scored])
|
d = build_doc([star, define, scored])
|
||||||
assert is_marked(star, d.declared) is None
|
assert is_marked(star, d.declared) == "star"
|
||||||
assert importance_of([star], d) == ("important", "marker")
|
assert importance_of([star], d) == ("substantive", "marker")
|
||||||
assert [(n.markers, n.meaning) for n in d.notes] == [({"star"}, "important")]
|
assert all(n.meaning != "important" for n in d.notes)
|
||||||
# 没有计分证据:维持实质性(降级的代价是把致命显示成重要)
|
|
||||||
assert is_marked(star, build_doc([star, define]).declared) == "star"
|
|
||||||
# 有一处说 ★ 是实质性要求、或者带 ★ 的句子写着后果,就按实质性(顾问审读第 1 条的反例)
|
|
||||||
for extra in ("本文件中打“★”内容为实质性要求。", "打★号的参数负偏离的,视为未响应。"):
|
|
||||||
assert is_marked(star, build_doc([star, define, scored, sent(extra, seq=4)]).declared) == "star"
|
|
||||||
|
|
||||||
|
|
||||||
def test_heuristics_reject_lookalikes() -> None:
|
def test_heuristics_reject_lookalikes() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user