#!/usr/bin/env python3 # -*- coding: utf-8 -*- r"""anchor_util.py — 内容判据锚(节锚)核验共用件:pipeline_gate(卡片 C5)与 capability_audit(判据出处注记道)共用的核验逻辑单点。 锚形态(2026-11-09 判据锚内容化改版批立,用户裁行号形态废止): `<文档相对路径>「节名」<行关键词>`——节名与行关键词在 design/ 现行文档(或 game/sim/ 测量面实现件) 里定位判据句,文档增删行使行号漂移与此形态无关,只有判据句内容真被改动才会使锚失配。 核验口径: .md 目标——节名首段(「·」前,沿 2026-11-07 架构收窄批惯例)须命中文档某个标题行(行首 # 记号), 行关键词须落在该标题行的节块内(标题行起至下一个标题级别不高于它的标题行止); .gd 目标(测量面,无标题结构)——节名首段与行关键词均须在文件内子串命中。 本件只做纯文本核验、不读盘:目标文件文本由调用方读入传入(编码口径归调用方)。 机检局限如实声明:判据句被改写而行关键词仍为其子串时核验不红(语义漂移),由复核门实读兜。 """ import re # 内容锚整体形态:路径+「节名」+行关键词(关键词止于空白与常见分隔符;反引号/+/§/冒号 # 不入词——卡内锚常套反引号包裹、杂尾修饰会污染关键词) CONTENT_ANCHOR_RE = re.compile( r"([^\s`'\"()(),,、;;::|]+\.md)「([^「」]+)」([^\s「」|,,、;;::()()+§`]+)") # 行号锚(废止形态,2026-11-09 用户裁)——判据锚位置出现即红 LINE_ANCHOR_RE = re.compile(r"([^\s`'\"()(),,、;;::|]+\.md)\s*:\s*(\d+)") def section_block(text, head): """节名首段 head 命中的标题行的节块文本;无命中标题行返回 None。 节块=该标题行起至下一个标题级别不高于它的标题行止(不含该行); 标题行=行首 # 记号的 ATX 标题(本仓库 design/ 文档惯例);围栏代码块(```)内的 # 行不是标题,探测时跳过。节名命中两档:先找标题文本与 head 全等(去除首尾空白)的行, 全等无命中再退子串包含——同名节(如「验收标准」嵌「一屏样张的验收标准」)取精确者。""" lines = text.splitlines() def headings(): fence = False for i, ln in enumerate(lines): s = ln.lstrip() if s.startswith("```"): fence = not fence continue if fence: continue m = re.match(r"^(#{1,6})\s*(.+?)\s*$", s) if m: yield i, len(m.group(1)), m.group(2).strip() start = -1 level = 0 for i, lvl, txt in headings(): if txt == head.strip(): start = i level = lvl break if start < 0: for i, lvl, txt in headings(): if head in txt: start = i level = lvl break if start < 0: return None fence = False for j in range(start + 1, len(lines)): s = lines[j].lstrip() if s.startswith("```"): fence = not fence continue if fence: continue m = re.match(r"^(#{1,6})\s", s) if m and len(m.group(1)) <= level: return "\n".join(lines[start:j]) return "\n".join(lines[start:]) def verify(target_path, target_text, sec, kw): """核验一个内容锚。返回 None=通过;字符串=红因白话。 target_path 仅用于按扩展名分派核验口径(.gd=测量面文件内子串命中,其余=标题+节块口径)。""" if target_path.replace("\\", "/").endswith(".gd"): if sec.split("·")[0] not in target_text: return "节名首段不在文件内" if kw not in target_text: return "行关键词不在文件内" return None block = section_block(target_text, sec.split("·")[0]) if block is None: return "节名首段未命中任何标题行" if kw not in block: return "行关键词不在该节块内" return None