muse-agent-example/工具/维护索引.py
zizi b9af240b68 R2 收尾:W30 唯一写入观察与最终验收(C08 章后接通 / 构建身份重建 / 最终备份与恢复演练)
- 章后处理:事实提案命令身份加入类型、批内去重改为先校验后去重(REPEAT_OBJECT_TYPE 记跳过、不再静默丢重复);查询章后状态回传事实跳过;可按需发布/核对可登记任务与 config_not_declared 语义收紧
- 模型治理:推理字符数计量(ChatCompletions/Messages/执行合同)、探针在声明关闭推理却仍有推理时拒绝、计价支持缓存协议与未知模型兜底
- 运行态:资源包重建(发布身份 21d850f7…),三处配置同步,post-extract v5 代次 5 启用,服务 PID 96675(health/ready 通过)
- 门禁:make 检查通过;离线 1008 passed;定点数据库 101 passed;最终完整备份 bkp-1f5591d8… 与空库恢复演练 verified(152 表/158 关系/迁移 45,演练库已 DROP)
- 文档与证据:运行手册、功能覆盖、工作包清单 W30 证据、测试用例清单(17 条 execution_evidence 回填 + 历史死指针说明)、目标文件清单
- 旧实现与旧库直连材料退出(含明文凭据文件移除);新配置 配置/本机正式.toml、配置/本机维护.toml、配置/计费/、配置/运行配置/ 入库,均只含受控引用
- R2 执行证据在 .agents.local/改造/R2-20260909/(不入库)
2026-09-16 08:23:39 +08:00

326 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""索引生成、查重及检查。
工程阶段的生成或预检入口:
- 核对 Markdown 索引(目录.md)表内相对链接有效;
- 核对技能目录(.agent/skills/ 下分组)中每个技能有 SKILL.md,名称不重复;
- 核对测试符号与用例清单的 case 身份一致:仓库内每个 test_ 符号必须带
``__<case6>`` 后缀且能在清单定位;清单登记的目标符号必须在树上真实存在;
只有 print 没有 assert/raise 的用例在执行前即被标记(反假绿)。
边界:只报告差异与失败,不改变业务数据、不自行批准发布。
生成物与唯一输入一致;命令可独立重跑。所有检查函数接受显式根目录,
供测试在临时项目上验证同一语义。
"""
from __future__ import annotations
import argparse
import ast
import json
import re
import sys
import unicodedata
from pathlib import Path
from typing import Any
仓库根 = Path(__file__).resolve().parent.parent
_目录表行 = re.compile(r"^\|\s*([^|]+)\|\s*\[([^\]]+)\]\(([^)]+)\)\s*\|")
_用例后缀 = re.compile(r"__([0-9a-f]{6})$")
_忽略目录片段 = frozenset(
{".venv", ".venv-旧", ".git", "node_modules", "__pycache__", ".agents.local", "研究依据"}
)
# 新版用例目录:旧测试树(tests/skills、tests/adapters、tests/architecture 英文目录等)
# 由旧环境运行,不进入新版用例身份检查
_新版用例目录 = ("单元", "契约", "集成", "架构", "迁移", "端到端", "真实调用")
def _在忽略目录(路径: Path, 根: Path) -> bool:
try:
相对 = 路径.relative_to(根)
except ValueError:
return True
return any(片段 in 相对.parts for 片段 in _忽略目录片段)
def 检查目录链接(根: Path) -> list[str]:
"""核对仓库内 目录.md 的表格相对链接;返回问题列表。"""
问题: list[str] = []
for 目录文件 in 根.rglob("目录.md"):
if _在忽略目录(目录文件, 根):
continue
for 行号, 行 in enumerate(目录文件.read_text(encoding="utf-8").splitlines(), 1):
匹配 = _目录表行.match(行)
if not 匹配:
continue
地址 = 匹配.group(3).split("#")[0].strip()
if not 地址 or 地址.startswith(("http://", "https://")):
continue
目标 = (目录文件.parent / 地址).resolve()
if not 目标.exists():
问题.append(f"{目录文件.relative_to(根)}:{行号} 链接不存在:{地址}")
return 问题
def _读frontmatter名(文件: Path) -> str | None:
"""读 SKILL.md frontmatter 的 name 字段;无 frontmatter 返回 None。"""
文本 = 文件.read_text(encoding="utf-8")
if not 文本.startswith("---"):
return None
结束 = 文本.find("\n---", 3)
if 结束 < 0:
return None
for 行 in 文本[3:结束].splitlines():
if 行.startswith("name:"):
return 行[5:].strip()
return None
def _含中文(名称: str) -> bool:
return any("\u4e00" <= ch <= "\u9fff" for ch in 名称)
def 检查技能目录(根: Path) -> list[str]:
"""核对 .agent/skills 分组:每个技能有非空 SKILL.md、名称不重复、
frontmatter name 与目录一致、目录名全中文且规范化(NFC、无空白)。"""
技能根 = 根 / ".agent" / "skills"
if not 技能根.is_dir():
return ["技能目录不存在:.agent/skills(缺失即失败,不静默跳过)"]
问题: list[str] = []
已见: dict[str, str] = {}
for 分组 in sorted(p for p in 技能根.iterdir() if p.is_dir()):
for 技能目录 in sorted(p for p in 分组.iterdir() if p.is_dir()):
名称 = 技能目录.name
相对 = str(技能目录.relative_to(根))
技能文件 = 技能目录 / "SKILL.md"
if not 技能文件.is_file():
问题.append(f"技能缺少 SKILL.md:{相对}")
continue
正文 = 技能文件.read_text(encoding="utf-8").strip()
if not 正文 or 正文 == "---":
问题.append(f"SKILL.md 为空:{相对}")
continue
front名 = _读frontmatter名(技能文件)
if front名 is None:
问题.append(f"SKILL.md 缺少 frontmatter name:{相对}")
elif front名 != 名称:
问题.append(f"frontmatter name 与目录不一致:{相对}(name: {front名})")
if not _含中文(名称):
问题.append(f"技能名称必须全中文(政策 chinese_only):{相对}")
if 名称 != unicodedata.normalize("NFC", 名称):
问题.append(f"技能名称不是 NFC 规范形式:{相对}")
if any(ch.isspace() for ch in 名称):
问题.append(f"技能名称含空白:{相对}")
if 名称 in 已见:
问题.append(f"技能名称重复:{名称}({已见[名称]} 与 {相对})")
else:
已见[名称] = 相对
return 问题
def 解析测试符号(文件: Path) -> list[ast.FunctionDef]:
"""用 AST 提取 test_ 开头的函数与方法;不执行任何代码。"""
树 = ast.parse(文件.read_text(encoding="utf-8"))
return [
节点
for 节点 in ast.walk(树)
if isinstance(节点, ast.FunctionDef) and 节点.name.startswith("test_")
]
def _有判定语句(节点: ast.FunctionDef) -> bool:
"""函数体内是否有 assert、raise 或 pytest.fail/skip/xfail 调用。"""
for 子 in ast.walk(节点):
if isinstance(子, ast.Assert):
return True
if isinstance(子, ast.Raise):
return True
if isinstance(子, ast.Call) and isinstance(子.func, ast.Attribute):
if 子.func.attr in {"fail", "skip", "xfail", "raises"}:
return True
return False
def 检查用例身份(
根: Path,
清单路径: Path | None = None,
*,
核对缺失目标: bool = False,
全树扫描: bool = False,
) -> list[str]:
"""核对测试符号与用例清单双向一致:
1. 新版用例目录内符号按完整文件、函数或前端用例名绑定清单;
2. 用例体必须包含可判定语句(assert/raise/fail/skip/xfail);
3. 核对缺失目标=True 时,清单登记的 (file, symbol) 必须在树上真实存在
已实施条目始终反向核对;过渡期只容许未实施目标暂缺。
"""
if 清单路径 is None:
清单路径 = 根 / "docs" / "系统架构" / "新版设计" / "验证设计" / "测试用例清单.json"
问题: list[str] = []
if not 清单路径.is_file():
return [f"用例清单不存在:{清单路径}"]
清单: dict[str, Any] = json.loads(清单路径.read_text(encoding="utf-8"))
分配路径 = 根 / "docs/系统架构/新版设计/改造计划/工作包清单.json"
if 分配路径.is_file():
目标表 = {
t["case_id"]: t
for t in [t for r in 清单.get("cases", []) for t in r.get("target_cases", [])]
+ 清单.get("new_cases", [])
}
分配表 = json.loads(分配路径.read_text(encoding="utf-8"))
for a in 分配表.get("case_assignments", []):
id_ = a.get("case_id")
if not id_:
continue
target = 目标表.get(id_)
if target is None:
问题.append(f"分配表引用未登记用例:{id_}")
elif any(
a.get(k) != target.get(v, "")
for k, v in (
("target_file", "file"),
("target_symbol", "symbol"),
("environment", "environment"),
)
):
问题.append(f"分配表用例定位与权威清单不符:{id_}")
登记符号: dict[str, str] = {}
已实施目标: set[str] = set()
for 用例 in 清单.get("cases", []):
for 目标 in 用例.get("target_cases") or []:
case_id = str(目标.get("case_id", ""))
文件 = str(目标.get("file", ""))
符号 = str(目标.get("symbol", ""))
if 文件 and 符号:
登记符号[f"{文件}::{符号}"] = case_id
if 目标.get("verification_state") in {"通过", "已实现未执行", "失败"}:
已实施目标.add(f"{文件}::{符号}")
for 新例 in 清单.get("new_cases", []):
case_id = str(新例.get("case_id", ""))
符号 = str(新例.get("symbol", ""))
文件 = str(新例.get("file", ""))
if 文件 and 符号:
登记符号[f"{文件}::{符号}"] = case_id
if 新例.get("verification_state") in {"通过", "已实现未执行", "失败"}:
已实施目标.add(f"{文件}::{符号}")
树上符号: set[str] = set()
候选 = sorted(根.glob("**/test_*.py")) if 全树扫描 else sorted(根.glob("tests/**/test_*.py"))
for 测试文件 in 候选:
if _在忽略目录(测试文件, 根):
continue
if not 全树扫描:
# 只检查新版用例目录;旧测试树由旧环境运行
try:
相对路径 = 测试文件.relative_to(根 / "tests")
except ValueError:
continue
if not 相对路径.parts or 相对路径.parts[0] not in _新版用例目录:
continue
try:
相对 = 测试文件.relative_to(根).as_posix()
except ValueError:
continue
for 节点 in 解析测试符号(测试文件):
树上符号.add(f"{相对}::{节点.name}")
匹配 = _用例后缀.search(节点.name)
if not 匹配:
问题.append(f"测试符号缺少用例后缀:{相对}::{节点.name}")
if f"{相对}::{节点.name}" not in 登记符号:
问题.append(f"测试文件与符号未在用例清单登记:{相对}::{节点.name}")
if not _有判定语句(节点):
问题.append(f"用例无可判定语句(print 不能代替断言):{相对}::{节点.name}")
前端测试 = sorted(
[*(根 / "web/tests").rglob("*.test.ts*"), *(根 / "web/tests").rglob("*.spec.ts*")]
)
if 前端测试:
import subprocess
# TypeScript AST 只解析静态用例声明,不执行测试或加载其运行依赖。
脚本 = """
import ts from 'typescript';
import fs from 'node:fs';
const files = JSON.parse(fs.readFileSync(0, 'utf8'));
const found = [];
for (const file of files) {
const source = ts.createSourceFile(file, fs.readFileSync(file, 'utf8'),
ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
function visit(node) {
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)
&& ['it', 'test'].includes(node.expression.text)
&& node.arguments[0] && ts.isStringLiteral(node.arguments[0]))
found.push([file, node.arguments[0].text]);
ts.forEachChild(node, visit);
}
visit(source);
}
process.stdout.write(JSON.stringify(found));
"""
try:
结果 = subprocess.run(
["node", "--input-type=module", "-e", 脚本],
cwd=根 / "web",
input=json.dumps([str(p) for p in 前端测试]),
capture_output=True,
text=True,
timeout=20,
check=True,
)
for 文件, 符号 in json.loads(结果.stdout):
键 = f"{Path(文件).relative_to(根).as_posix()}::{符号}"
树上符号.add(键)
if 键 not in 登记符号:
问题.append(f"前端用例未在清单登记:{键}")
except (OSError, ValueError, subprocess.SubprocessError):
问题.append("前端用例解析失败;需要已锁定的 Node 与 TypeScript 工具链")
for 键, case_id in sorted(登记符号.items()):
if 核对缺失目标 or 键 in 已实施目标:
if 键 not in 树上符号:
问题.append(f"用例清单登记的目标不存在:{键}({case_id})")
return 问题
def 检查文件定位(根: Path) -> list[str]:
"""设计清单的文件定位必须恰好命中一个定义,不把目录树文字当作锚点。"""
设计根 = 根 / "docs/系统架构/新版设计"
清单 = 设计根 / "目标文件清单.json"
if not 清单.is_file():
return []
问题, 已读 = [], {}
for 项 in json.loads(清单.read_text(encoding="utf-8"))["files"]:
文件名, 分隔, 锚点 = 项.get("design_location", "").partition("#")
文件 = (设计根 / 文件名).resolve()
if not 分隔 or not 文件.is_relative_to(设计根.resolve()) or not 文件.is_file():
问题.append(f"文件设计定位不存在:{项['path']}")
continue
if 文件 not in 已读:
已读[文件] = 文件.read_text(encoding="utf-8")
次数 = len(re.findall(r'<a\s+id=["\x27]' + re.escape(锚点) + r'["\x27]', 已读[文件]))
if 次数 != 1:
问题.append(f"文件设计锚点应恰一,实际{次数}:{项['path']}({锚点})")
return 问题
def 主() -> int:
解析器 = argparse.ArgumentParser(description="索引生成、查重及检查")
解析器.add_argument("--写入", action="store_true", help="执行索引写入(当前为幂等检查)")
解析器.add_argument("--检查", action="store_true", help="只检查并报告差异")
参数 = 解析器.parse_args()
if 参数.写入:
print("索引写入:当前无可写生成物,执行幂等检查")
问题 = 检查目录链接(仓库根) + 检查技能目录(仓库根) + 检查用例身份(仓库根) + 检查文件定位(仓库根)
if 问题:
for 条 in 问题:
print(f"索引问题:{条}", file=sys.stderr)
return 1
print("索引检查通过:目录链接、技能目录、用例身份及文件定位均一致")
return 0
if __name__ == "__main__":
raise SystemExit(主())