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 == "—":