From 31bdecd6d575b199cf13e4662b00862f96630297 Mon Sep 17 00:00:00 2001
From: DKChan <524912269@qq.com>
Date: Sun, 18 Oct 2026 01:42:52 +0800
Subject: [PATCH] =?UTF-8?q?tools(pipeline):=20=E6=B5=81=E6=B0=B4=E7=BA=BF?=
=?UTF-8?q?=E9=97=B8=E5=AF=B9=E9=BD=90=20doc-conventions=20=C2=A712=20?=
=?UTF-8?q?=E7=8A=B6=E6=80=81=E6=AE=B5=E7=B4=AF=E7=A7=AF=E5=88=B6=E2=80=94?=
=?UTF-8?q?=E2=80=94=EF=BC=8B=E5=88=87=E6=AE=B5=E6=95=B0=E4=B8=8D=E9=99=90?=
=?UTF-8?q?=E9=80=90=E6=AE=B5=E5=90=88=E6=B3=95=EF=BC=88=E5=BA=9F=E5=8F=8C?=
=?UTF-8?q?=E8=BD=A8=E6=81=B0=E4=B8=A4=E6=AE=B5=E4=B8=8A=E9=99=90=EF=BC=89?=
=?UTF-8?q?=EF=BC=8C=E6=96=B0=E5=A2=9E=E5=9C=A8=E5=9C=BA=E7=BA=A6=E6=9D=9F?=
=?UTF-8?q?=EF=BC=88=E4=B8=8B=E6=B8=B8=E6=AE=B5=E5=9C=A8=E5=9C=BA=E9=A1=BB?=
=?UTF-8?q?=E4=B8=8A=E6=B8=B8=E9=97=AD=E5=90=88=E6=AE=B5=EF=BC=8C=E5=B8=A6?=
=?UTF-8?q?=E6=97=A5=E6=9C=9F=E7=89=B9=E6=89=B9=E8=B1=81=E5=85=8D=EF=BC=89?=
=?UTF-8?q?=E4=B8=8E=E5=8F=8C=E8=BD=A8=E8=AE=BE=E8=AE=A1=E9=98=B6=E6=AE=B5?=
=?UTF-8?q?=E6=81=B0=E4=B8=A4=E6=AE=B5=E4=B8=94=E4=B8=80=E6=96=87=E6=A1=A3?=
=?UTF-8?q?=E4=B8=80=E7=BE=8E=E6=9C=AF=EF=BC=9Broadmap=20=E5=8D=A1?=
=?UTF-8?q?=E7=89=87=E5=88=97=20
/=E6=8D=A2=E8=A1=8C=E5=A4=9A=E5=8D=A1?=
=?UTF-8?q?=E9=80=90=E5=8D=A1=E6=A0=A1=E9=AA=8C=E5=AD=98=E5=9C=A8=E6=80=A7?=
=?UTF-8?q?=E4=B8=94=E5=85=8D=E6=95=B4=E6=A0=BC=E9=95=BF=E5=BA=A6=E5=8F=99?=
=?UTF-8?q?=E8=BF=B0=E7=BA=A2=EF=BC=88=C2=A712.2=20=E4=B8=80=E6=A0=BC?=
=?UTF-8?q?=E4=B8=80=E8=A1=8C=E4=B8=80=E5=8D=A1=EF=BC=89=EF=BC=8C=E9=A2=84?=
=?UTF-8?q?=E8=AE=A1=E5=91=A8=E6=9C=9F=E8=B6=85=202=20=E5=91=A8=E5=B8=A6?=
=?UTF-8?q?=E6=97=A5=E6=9C=9F=E7=89=B9=E6=89=B9=E6=94=BE=E8=A1=8C=EF=BC=9B?=
=?UTF-8?q?=E4=BA=A4=E5=8F=89=E6=A0=B8=E4=BE=9D=E6=8D=AE=E6=94=B6=E7=AA=84?=
=?UTF-8?q?=E4=B8=BA=E5=90=8C=E7=8A=B6=E6=80=81=E8=A1=8C=E5=B7=B2=E5=86=99?=
=?UTF-8?q?=E5=8D=A1=E6=AE=B5=E6=8B=AC=E5=8F=B7=E8=B7=AF=E5=BE=84=EF=BC=9B?=
=?UTF-8?q?=E5=8F=98=E5=BC=82=E9=AA=8C=E8=AF=81=2034=20=E5=8F=98=E4=BD=93?=
=?UTF-8?q?=EF=BC=886=20=E7=BB=BF=2028=20=E7=BA=A2=EF=BC=89=E5=85=A8?=
=?UTF-8?q?=E8=BF=87=EF=BC=8B=E8=BF=98=E5=8E=9F=E5=9B=9E=E7=BB=BF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
tools/pipeline_gate.py | 76 +++++++++++++++++++++++++++++++++---------
1 file changed, 60 insertions(+), 16 deletions(-)
diff --git a/tools/pipeline_gate.py b/tools/pipeline_gate.py
index 9ad5379f..a4f58868 100644
--- a/tools/pipeline_gate.py
+++ b/tools/pipeline_gate.py
@@ -2,9 +2,9 @@
# -*- coding: utf-8 -*-
r"""pipeline_gate.py — 工作流程流水线闸(06 条目状态机+roadmap 迭代总表+一次交叉核)。
-职责:机检 06-待办清单条目(标题元数据/属性齐备/状态词白名单/条件凭据/双轨/特批/零命中)
- 与 roadmap 迭代总表(固定六列/状态四形态/卡片与报告存在性/周期≤2 周/单元格白名单/零命中);
- 「已编迭代↔迭代行卡片列」交叉核只在此一处实现。
+职责:机检 06 条目(标题元数据/属性齐备/状态词白名单/条件凭据/状态段累积制在场约束/特批/
+ 零命中)与 roadmap 迭代总表(六列/状态四形态/卡片与报告存在性/周期≤2 周可特批豁免/
+ 单元格白名单/零命中);「已编迭代↔已写卡段↔迭代行卡片列」交叉核只此一处。
约束:每红项一行「文件:行号 判据名 一句白话说明」,exit 0=全绿 / 1=有红 / 2=输入件缺失;
卡片与报告存在性相对 --repo-root 解析(缺省=本文件上两级);现行 06 与 roadmap 尚为旧格式,
对其红属预期(第二批数据整改后才注册上闸)。
@@ -27,6 +27,11 @@ STATUS_WORDS = (
)
# 这些状态 = 设计未闭合,条目必须带「**缺口**」属性;其余状态不得带
GAP_REQUIRED = ("待文档设计", "文档设计中", "待美术设计", "美术设计中")
+# 状态段累积制(doc-conventions §12.1):下游段在场时的上游闭合段在场约束
+DOWNSTREAM_WORDS = ("待写卡", "已写卡", "已编迭代")
+PRESENCE_FACES = ("文档轨", "美术轨", "双轨")
+DOC_DESIGN_WORDS = ("待文档设计", "文档设计中", "文档设计已闭合")
+ART_DESIGN_WORDS = ("待美术设计", "美术设计中", "美术设计已闭合")
ROADMAP_HEADER = ("迭代代号", "状态", "需求卡片", "编入时间", "预计周期", "实现报告")
RESERVED_RETIRED = "已退役(git 历史可考)"
@@ -36,6 +41,7 @@ RESEARCH_RE = re.compile(r"design/research/[^\s()(),,]+")
ITER_RE = re.compile(r"迭代\s*(\d+)")
APPROVAL_RE = re.compile(r"〔用户特批([^〕]*)〕")
CRED_RE = re.compile(r"[((]([^()()]*)[))]")
+CARD_SEP_RE = re.compile(r"
|\n|、") # 迭代行卡片列一格多卡的分隔(§12.2 一格一行一卡)
ATTR_LINE_RE = re.compile(r"^\*\*(现状|缺口|归属|状态)\*\*\s*[::]\s*(.*)$")
HEADING_RE = re.compile(r"^(#{1,4})\s+")
SEP_ROW_RE = re.compile(r"^\|[\s\-|:]+$")
@@ -138,13 +144,21 @@ def check_segment(word, creds, line, issues, where, root):
def check_status(raw, line, face, issues, where, root):
- """判据 3+双轨/特批/备选。返回 (状态词列表, 已写卡路径列表, 已编迭代号|None)。"""
+ """判据 3+状态段累积制(§12.1)+特批/备选。返回 (状态词列表, 已写卡路径列表, 已编迭代号|None)。
+
+ 累积制:按+切段后段数不限,逐段各自合法;再查两条结构约束——
+ 下游段(待写卡/已写卡/已编迭代)在场时上游闭合段必须在场(带日期特批可豁免);
+ 双轨设计阶段(无下游段)必须恰两段且一文档一美术。
+ """
content = raw.strip()
m = re.match(r"^\*\*(.+)\*\*$", content)
if m:
content = m.group(1).strip()
+ dated_approval = False
for am in APPROVAL_RE.finditer(content):
- if not DATE_RE.search(am.group(1)):
+ if DATE_RE.search(am.group(1)):
+ dated_approval = True
+ else:
issues.append((where, line, "特批标记", "〔用户特批〕缺日期(带 YYYY-MM-DD 才放行跳轨)"))
content = APPROVAL_RE.sub("", content)
content = content.replace("(备选)", "").replace("(备选)", "").strip()
@@ -152,10 +166,6 @@ def check_status(raw, line, face, issues, where, root):
if not segs:
issues.append((where, line, "状态词非法", "状态行内容为空"))
return [], [], None
- if len(segs) > 2:
- issues.append((where, line, "双轨段数", "状态 %d 段(+连接时恰好两段)" % len(segs)))
- if face == "双轨" and len(segs) != 2:
- issues.append((where, line, "双轨段数", "设计面=双轨的状态须恰好两段(现 %d 段)" % len(segs)))
words, cards, iter_no = [], [], None
for seg in segs:
creds = CRED_RE.findall(seg)
@@ -167,6 +177,24 @@ def check_status(raw, line, face, issues, where, root):
cards.append(rp)
if it is not None:
iter_no = it
+ downstream = [w for w in words if w in DOWNSTREAM_WORDS]
+ if downstream and face in PRESENCE_FACES and not dated_approval:
+ need = []
+ if face in ("文档轨", "双轨") and "文档设计已闭合" not in words:
+ need.append("文档设计已闭合(凭据)")
+ if face in ("美术轨", "双轨") and "美术设计已闭合" not in words:
+ need.append("美术设计已闭合(凭据)")
+ if need:
+ issues.append((where, line, "在场约束",
+ "下游段(%s)在场但缺上游闭合段 %s(带日期特批可豁免)"
+ % ("、".join(downstream), "、".join(need))))
+ if face == "双轨" and not downstream:
+ n_doc = sum(1 for w in words if w in DOC_DESIGN_WORDS)
+ n_art = sum(1 for w in words if w in ART_DESIGN_WORDS)
+ if len(segs) != 2 or n_doc != 1 or n_art != 1:
+ issues.append((where, line, "双轨段数",
+ "设计面=双轨设计阶段须恰两段且一文档一美术(现 %d 段:文档轨 %d、美术轨 %d)"
+ % (len(segs), n_doc, n_art)))
return words, cards, iter_no
@@ -210,12 +238,20 @@ def row_cells(row):
return [c.strip() for c in row.split("|")[1:-1]]
-def cell_shape(cell):
- """判据 7:单元格白名单外的形态说明;合法返回 None。"""
+def cell_shape(cell, card_col=False):
+ """判据 7:单元格白名单外的形态说明;合法返回 None。
+
+ 需求卡片列按多卡分隔符逐段看叙述、总长不设限(§12.2 一格一行一卡是合法形态),
+ 路径存在性由列规则逐卡承担;其余列维持整格叙述检查。
+ """
if cell == "":
return "空白单元格"
if re.fullmatch(r"〔[^〕]*〕", cell):
return None if DATE_RE.search(cell) else "特批标记缺日期"
+ if card_col:
+ if any(mark in part for part in CARD_SEP_RE.split(cell) for mark in ("。", ",")):
+ return "成段叙述(卡片列一格一行一卡,禁叙述文字)"
+ return None
if "。" in cell or "," in cell or len(cell) > 60:
return "成段叙述(只许路径/日期/状态词/保留字/—/≈N 周/特批标记)"
return None
@@ -240,7 +276,7 @@ def check_iter_table(block, issues, where, root):
continue
code, status, cards, date, weeks, report = cells
for ci, cell in enumerate(cells):
- bad = cell_shape(cell)
+ bad = cell_shape(cell, card_col=(ci == 2))
if bad:
issues.append((where, ln, "单元格白名单",
"第 %d 列「%s」:%s" % (ci + 1, cell[:24], bad)))
@@ -252,15 +288,23 @@ def check_iter_table(block, issues, where, root):
issues.append((where, ln, "总表状态列",
"「%s」不在四形态(待开工/进行中/已完成(YYYY-MM-DD)/已终止(YYYY-MM-DD))" % status))
if norm_parens(cards) != norm_parens(RESERVED_RETIRED):
- for part in (p.strip() for p in cards.split("、")):
+ for part in CARD_SEP_RE.split(cards):
+ part = part.strip()
if part and not (root / part).is_file():
issues.append((where, ln, "卡片路径不存在",
"需求卡片列 %s 相对仓库根不存在" % part))
if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", date):
issues.append((where, ln, "编入时间", "「%s」须为 YYYY-MM-DD" % date))
- wm = WEEKS_RE.match(weeks)
- if not wm or float(wm.group(1)) > 2:
- issues.append((where, ln, "预计周期", "「%s」须为「≈N 周」且 N≤2(允许 0.5 步进)" % weeks))
+ # 预计周期:≈N 周(N≤2);超 2 周须格内带日期特批标记(§12.2/§12.4)
+ week_marks = APPROVAL_RE.findall(weeks)
+ week_core = APPROVAL_RE.sub("", weeks).strip()
+ wm = WEEKS_RE.match(week_core)
+ if not wm:
+ issues.append((where, ln, "预计周期",
+ "「%s」须为「≈N 周」形态(N≤2,允许 0.5 步进)" % weeks))
+ elif float(wm.group(1)) > 2 and not any(DATE_RE.search(mk) for mk in week_marks):
+ issues.append((where, ln, "预计周期",
+ "「%s」超 2 周且无带日期特批标记〔用户特批 YYYY-MM-DD …〕" % weeks))
done = status_n.startswith("已完成(")
report_n = norm_parens(report)
if report_n == "—":