tools(pipeline): 流水线闸对齐 doc-conventions §12 状态段累积制——+切段数不限逐段合法(废双轨恰两段上限),新增在场约束(下游段在场须上游闭合段,带日期特批豁免)与双轨设计阶段恰两段且一文档一美术;roadmap 卡片列 <br>/换行多卡逐卡校验存在性且免整格长度叙述红(§12.2 一格一行一卡),预计周期超 2 周带日期特批放行;交叉核依据收窄为同状态行已写卡段括号路径;变异验证 34 变体(6 绿 28 红)全过+还原回绿

This commit is contained in:
DKChan committed 2026-10-18 01:42:52 +08:00
1 parent 4460a13eb2
commit 31bdecd6d5
1 file changed
+60 -16
+60 -16
View File
@@ -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"<br\s*/?>|\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 == "—":