feat(闸): pipeline_gate 补两条卡片机检 C1/C2——卡片冲突修订批的防回归兜网

- C1 文档义务口径(生效=文件名日期 ≥2026-10-17):关联文档同步面表内目标格指向 design/(含 ../gdd/ 等相对写法)或根规则文件、义务格仍写「随批」者红——req-card §6 改口径后旧措辞「机械面随批」不得再入卡
- C2 设计事实源声明(生效 ≥2026-10-18):全文既无「设计事实源」也无「唯一事实源」者红;两种合格写法都认(独立小节/头部「来源:…(唯一事实源:…)」),避免按小节名字面判死全库卡
- 只追新规生效日之后的卡,更早的卡属已实现批次历史留档,不回头改(同「换代清零+定期跑批」先例,不设豁免清单、不加编辑期钩子)
- 负向验证已做:临时卡注入两处违例→两红(design 目标红、game/README 行不误红)→删除复跑绿;文档域闸全绿无新增红(回执 commit=ad48d2a2)
- 同批另修两份技能文件的「设计事实源」字面对齐(.zcode/skills 与 .qoder/skills 双写逐字一致)——两目录均被 gitignore,该改动只在本机盘上、不入 git
This commit is contained in:
DKChan committed 2026-10-18 19:56:11 +08:00
1 parent ad48d2a2d2
commit 76868d7228
1 file changed
+71 -4
+71 -4
View File
@@ -1,11 +1,13 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r"""pipeline_gate.py — 工作流程流水线闸(06 条目状态机+roadmap 迭代总表+一次交叉核)。
r"""pipeline_gate.py — 工作流程流水线闸(06 条目状态机+roadmap 迭代总表+需求卡片口径+一次交叉核)。
职责:机检 06 条目(标题元数据/属性齐备/状态词白名单/条件凭据/状态段累积制在场约束/特批/
零命中)与 roadmap 迭代总表(六列/状态四形态/卡片与报告存在性/编入时间与预计周期的
历史行「—」豁免/周期≤2 周可特批豁免/单元格白名单/零命中);「已编迭代↔已写卡段↔迭代行
卡片列」交叉核只此一处。
卡片列」交叉核只此一处。2026-10-18 卡片冲突修订批补两条卡片机检:C1 关联文档同步面的
文档义务口径(生效 2026-10-17)、C2 设计事实源声明(生效 2026-10-18)——两条都按文件名
日期只追新规生效日之后的卡,旧卡属 git 历史留档不回头改。
约束:每红项一行「文件:行号 判据名 一句白话说明」,exit 0=全绿 / 1=有红 / 2=输入件缺失;
卡片与报告存在性相对 --repo-root 解析(缺省=本文件上两级);06 与 roadmap 均已为新格式
(2026-10-18 数据整改批),本闸已注册进 tools/run_all_gates.sh 文档域(08c 流水线闸)。
@@ -36,6 +38,16 @@ ART_DESIGN_WORDS = ("待美术设计", "美术设计中", "美术设计已闭合
ROADMAP_HEADER = ("迭代代号", "状态", "需求卡片", "编入时间", "预计周期", "实现报告")
RESERVED_RETIRED = "已退役(git 历史可考)"
# ---- 卡片机检(2026-10-18 卡片冲突修订批补)----
CARD_GLOB = "需求卡片-*.md"
CARD_DIR = "design/research"
SYNC_RULE_DATE = (2026, 10, 17) # req-card §6:实现批禁写 design/ 与根规则文件,义务一律列反哺点
SOURCE_RULE_DATE = (2026, 10, 18) # req-card §2:设计事实源必填
SOURCE_MARKS = ("设计事实源", "唯一事实源") # 两种合格写法,与库内既有卡片实际形态对齐
SYNC_HEADING_RE = re.compile(r"^#{2,3}\s*\d*\.?\s*关联文档同步面")
BANNED_DOC_MARKS = ("design/", "../gdd/", "../registry/", "../research/", "../qa/",
"../dev/", "../ux/", "../art/", "roadmap.md", "CLAUDE.md")
DATE_RE = re.compile(r"\d{4}-\d{2}-\d{2}")
VER_RE = re.compile(r"[vV]\d+(?:\.\d+)*")
RESEARCH_RE = re.compile(r"design/research/[^\s()(),,]+")
@@ -361,6 +373,60 @@ def cross_check(links, index, issues, where):
"迭代 %d 行卡片列不含本条目已写卡路径 %s" % (no, card)))
def card_date(path):
"""文件名尾日期 → (y, m, d) 元组;取不到返回 None(该卡跳过两条新规机检)。"""
found = DATE_RE.findall(path.stem)
if not found:
return None
return tuple(int(x) for x in found[-1].split("-"))
def check_cards(root, issues):
"""C1 文档义务口径(生效 2026-10-17)+C2 设计事实源声明(生效 2026-10-18)。
只检文件名日期达生效日的卡——更早的卡属已实现批次的历史留档,不回头改。
C1 判据:「关联文档同步面」表内,目标格指向 design/ 或根规则文件、而义务格仍写
「随批」者红(2026-10-17 req-card §6 改口径后,这类点只能列 reports/ 反哺点清单)。
C2 判据:全文既无「设计事实源」也无「唯一事实源」者红(两种写法都与库内实态对齐)。
"""
dirn = root / CARD_DIR
if not dirn.is_dir():
issues.append((str(dirn), 1, "零命中自检", "卡片目录不存在,C1/C2 无卡可检"))
return 0
n = 0
for path in sorted(dirn.glob(CARD_GLOB)):
when = card_date(path)
if when is None:
continue
n += 1
text = path.read_text(encoding="utf-8-sig")
if when >= SOURCE_RULE_DATE and not any(mark in text for mark in SOURCE_MARKS):
issues.append((str(path), 1, "设计事实源缺失",
"须含「设计事实源」或「唯一事实源」声明句(req-card §2 三选一,"
"GDD 节+版本号/样张+art-bible 版本/工程直入证据指认)"))
if when < SYNC_RULE_DATE:
continue
in_sec = False
for lineno, raw in enumerate(text.splitlines(), start=1):
stripped = raw.strip()
if SYNC_HEADING_RE.match(stripped):
in_sec = True
continue
if in_sec and stripped.startswith("#"):
in_sec = False
continue
if not in_sec or not stripped.startswith("|") or SEP_ROW_RE.match(stripped):
continue
cells = row_cells(stripped)
if len(cells) < 3 or cells[0] in ("文档", ""):
continue
if any(mark in cells[0] for mark in BANNED_DOC_MARKS) and "随批" in cells[2]:
issues.append((str(path), lineno, "文档义务口径",
"目标「%s」属实现批禁写面,义务却写「随批」——改标反哺点并列进 "
"reports/ 清单(req-card §6,2026-10-17 起)" % cells[0][:36]))
return n
def main():
ap = argparse.ArgumentParser(
description="工作流程流水线闸:06 条目状态机+roadmap 迭代总表+一次交叉核")
@@ -382,13 +448,14 @@ def main():
links = check_todo(todo_text, issues, str(todo_path), root)
index = check_roadmap(roadmap_text, issues, str(roadmap_path), root)
cross_check(links, index, issues, str(todo_path))
n_cards = check_cards(root, issues)
for where, line, name, msg in issues:
print("%s:%d %s %s" % (display(where, root), line, name, msg))
if issues:
print("流水线闸 FAIL:红项 %d" % len(issues))
return 1
print("流水线闸 PASS:06 条目 %d · 总表数据行 %d · 交叉核 %d 链全过"
% (len(parse_todo(todo_text)), len(index), len(links)))
print("流水线闸 PASS:06 条目 %d · 总表数据行 %d · 交叉核 %d 链 · 卡片口径 %d 张全过"
% (len(parse_todo(todo_text)), len(index), len(links), n_cards))
return 0