zizi 7f9114eca5 W13 旧用例承接补充:作者层路由与中文方法 Skill 的已跟踪文件改动
作者场景路由与指令、完善故事基础设定/合并故事方案/制定作品规划的技能中文化重构及其脚本、对应旧测试修改、SOT 导读、技能目录与实现回顾。此为 W13 阶段对已跟踪文件的改动,随批次按阶段整理补提交。
2026-09-10 19:39:47 +08:00

142 lines
5.7 KiB
Python

#!/usr/bin/env python3
"""只校验阶段 0 最终交付稿的格式,不参与探索调度或创作判断。"""
from __future__ import annotations
import argparse
from collections import Counter
from dataclasses import asdict, dataclass
import json
from pathlib import Path
import re
from typing import Iterable
TREE_PATH = Path(__file__).with_name("candidate_tree.json")
HEADING_RE = re.compile(r"^(#{1,6})[ \t]+(.+?)[ \t]*$", re.MULTILINE)
SETTING_RE = re.compile(r"^\s*[-*]\s+(?:\*\*)?S(\d{3,})(?=[^\d]|$)", re.MULTILINE)
PLACEHOLDER_RE = re.compile(
r"^\s*(?:[-*>]\s*)?(?:TODO|TBD|PLACEHOLDER|\[待填\])\s*$",
re.MULTILINE | re.IGNORECASE,
)
@dataclass(frozen=True)
class Problem:
code: str
file: str
message: str
@dataclass(frozen=True)
class CandidateReport:
file: str
setting_count: int
def _validate(path: Path, tree: dict) -> tuple[CandidateReport, list[Problem]]:
text = path.read_text(encoding="utf-8")
headings = list(HEADING_RE.finditer(text))
problems: list[Problem] = []
def fail(code: str, message: str) -> None:
problems.append(Problem(code, str(path), message))
if sum(len(h.group(1)) == 1 for h in headings) != 1:
fail("CANDIDATE_STRUCTURE", "交付稿须有且只有一个一级标题")
if headings and len(headings[0].group(1)) != 1:
fail("CANDIDATE_STRUCTURE", "首个标题须为作品一级标题")
if any(len(h.group(1)) >= 4 for h in headings):
fail("CANDIDATE_STRUCTURE", "交付稿不使用四级及更深标题")
if PLACEHOLDER_RE.search(text):
fail("CANDIDATE_PLACEHOLDER", "请把裸占位符改为具体的待定或未探索说明")
chapters = {chapter["heading"]: chapter for chapter in tree["chapters"]}
expected = ["根设定", *chapters, "待决与取舍"]
h2 = [h for h in headings if len(h.group(1)) == 2]
titles = [h.group(2) for h in h2]
if titles and titles[0] == "目录":
titles = titles[1:]
if titles != expected:
fail("TREE_MISMATCH", "二级标题须按顺序包含根设定、九章和待决与取舍;目录仅可置于最前")
if any(
len(h.group(1)) == 3 and (not h2 or h.start() < h2[0].start())
for h in headings
):
fail("CANDIDATE_STRUCTURE", "三级标题须位于对应章节内")
for index, heading in enumerate(h2):
title = heading.group(2)
end = h2[index + 1].start() if index + 1 < len(h2) else len(text)
body = text[heading.end():end]
# 标题和注释不能代替说明;未知内容可以用一句明确说明表达。
prose = HEADING_RE.sub("", body)
prose = re.sub(r"<!--.*?-->", "", prose, flags=re.DOTALL)
if not re.sub(r"[\s>*_\x60-]", "", prose):
fail("SECTION_EMPTY", f"“{title}”为空,请写已有内容或说明尚未探索的部分")
actual = [
h.group(2) for h in headings
if len(h.group(1)) == 3 and heading.end() <= h.start() < end
]
allowed: list[str] = []
if title in chapters:
for section in chapters[title]["sections"]:
allowed.append(section["heading"])
allowed.extend(section["units"])
positions = {name: position for position, name in enumerate(allowed)}
if any(name not in positions for name in actual):
fail("TREE_MISMATCH", f"“{title}”含导航之外的三级标题")
elif any(positions[right] <= positions[left] for left, right in zip(actual, actual[1:])):
fail("TREE_MISMATCH", f"“{title}”的已展开标题重复或顺序错误")
ids = [int(value) for value in SETTING_RE.findall(text)]
duplicates = [value for value, count in Counter(ids).items() if count > 1]
if duplicates:
fail("SETTING_ID_DUPLICATE", "编号重复:" + "、".join(f"S{value:03d}" for value in duplicates))
return CandidateReport(str(path), len(ids)), problems
def validate_candidates(paths: Iterable[Path]) -> tuple[list[CandidateReport], list[Problem]]:
"""各稿独立校验;不要求候选间同构,也不裁决是否已经完成创作。"""
tree = json.loads(TREE_PATH.read_text(encoding="utf-8"))
reports: list[CandidateReport] = []
problems: list[Problem] = []
for item in paths:
path = Path(item)
try:
report, found = _validate(path, tree)
except (OSError, UnicodeError) as exc:
problems.append(Problem("CANDIDATE_FILE_UNREADABLE", str(path), str(exc)))
continue
reports.append(report)
problems.extend(found)
if not reports and not problems:
problems.append(Problem("CANDIDATE_FILE_MISSING", "<input>", "请提供一份最终交付稿"))
return reports, problems
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="校验阶段 0 最终交付稿格式")
parser.add_argument("files", nargs="+", type=Path, help="已整理的作品探索稿")
parser.add_argument("--json", action="store_true", help="输出稳定 JSON")
args = parser.parse_args(argv)
reports, problems = validate_candidates(args.files)
code = "SETTING_INIT_VALIDATION_FAILED" if problems else "SETTING_INIT_OK"
if args.json:
print(json.dumps({
"status": "invalid" if problems else "ok",
"code": code,
"reports": [asdict(report) for report in reports],
"problems": [asdict(problem) for problem in problems],
}, ensure_ascii=False, indent=2))
else:
print(f"{code}: {len(reports)} 份交付稿,{len(problems)} 个格式问题")
for problem in problems:
print(f"[{problem.code}] {problem.file}: {problem.message}")
return 1 if problems else 0
if __name__ == "__main__":
raise SystemExit(main())