92 lines
4.2 KiB
Python
92 lines
4.2 KiB
Python
#!/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
|