diff --git a/tools/pipeline_gate.py b/tools/pipeline_gate.py index 57224f7b..79a97716 100644 --- a/tools/pipeline_gate.py +++ b/tools/pipeline_gate.py @@ -55,6 +55,9 @@ LEDGER_PROOF_MARKS = ("撤销", "证明") BINDING_RULE_DATE = (2026, 10, 28) # req-card §8:换代为判据绑定面列,撤销/变异证明列同日禁列 BANNED_PROOF_MARKS = ("撤销证明", "变异证明") AUDIT_MARK = "验收审计" +CAP_ROW_RULE_DATE = (2026, 10, 29) # req-card §8:每行标能力归属(对齐 test-conventions §8.9 三层模型) +CAP_ROW_RE = re.compile(r"^(既有|新增)\s*[::]\s*(.+)$") +CAP_README = Path("game/tests/README.md") DATE_RE = re.compile(r"\d{4}-\d{2}-\d{2}") VER_RE = re.compile(r"[vV]\d+(?:\.\d+)*") @@ -453,7 +456,30 @@ def card_date(path): return tuple(int(x) for x in found[-1].split("-")) -def check_ledger(text, path, when, issues): +def capability_names(root): + """game/tests/README.md 能力表首列行名集合;文件或表缺失返回空集(调用方据此判红)。""" + p = root / CAP_README + if not p.is_file(): + return set() + names = set() + header = None + for raw in p.read_text(encoding="utf-8").splitlines(): + s = raw.strip() + if not s.startswith("|"): + header = None + continue + cells = row_cells(s) + if header is None: + header = cells + continue + if SEP_ROW_RE.match(s): + continue + if any("代表用例" in c for c in header) and cells: + names.add(cells[0].strip("`").strip()) + return names + + +def check_ledger(text, path, when, issues, root): """C3 改动—验收对照表结构检查(生效 2026-10-21)。 只查:对照表节在不在、表头有没有五个必需列、有没有数据行、必填格空不空。 @@ -517,6 +543,18 @@ def check_ledger(text, path, when, issues): return idx["证明"] = proof_hit + cap_hit = None + cap_names = None + if when >= CAP_ROW_RULE_DATE: + cap_hit = next((k for k, cell in enumerate(header) if "能力行" in cell), None) + if cap_hit is None: + issues.append((str(path), header_line, "验收对照表缺列", + "缺「能力行」列(2026-10-29 起:每行写 既有:<能力名> 或 " + "新增:<能力名>,对齐能力清单)")) + return + idx["能力行"] = cap_hit + cap_names = capability_names(root) + rows = 0 for i in range(header_line, len(lines)): s = lines[i].strip() @@ -536,6 +574,18 @@ def check_ledger(text, path, when, issues): if k >= len(cells) or not cells[k].strip(): issues.append((str(path), i + 1, "验收对照表空格", "「%s」列为空" % name)) + if cap_hit is not None: + cap_cell = cells[cap_hit] if cap_hit < len(cells) else "" + for seg in [x.strip() for x in re.split(r"[;;]", cap_cell) if x.strip()]: + m = CAP_ROW_RE.match(seg) + if not m: + issues.append((str(path), i + 1, "能力行格式", + "「能力行」格须写 既有:<能力名>/新增:<能力名>:%s" + % seg[:24])) + elif m.group(1) == "既有" and m.group(2).strip("`").strip() not in cap_names: + issues.append((str(path), i + 1, "能力行不可核", + "既有能力「%s」不在 game/tests/README.md 能力表" + % m.group(2).strip("`").strip()[:24])) if rows == 0: issues.append((str(path), header_line, "验收对照表零行", "表内没有数据行")) @@ -569,7 +619,7 @@ def check_cards(root, issues): "须含「设计事实源」或「唯一事实源」声明句(req-card §2 三选一," "GDD 节+版本号/样张+art-bible 版本/工程形态证据指认)")) if when >= LEDGER_RULE_DATE: - check_ledger(text, path, when, issues) + check_ledger(text, path, when, issues, root) if AUDIT_MARK not in text: issues.append((str(path), 1, "独立验收审计未声明", "须在卡内写明独立验收审计入口与报告文件(2026-10-21 起,"