145 lines
6.0 KiB
Python
145 lines
6.0 KiB
Python
"""accept_pages.py: 渲图验收的 verdict 记录器(与 svg_preview.py / 导出 gate 配套)。
|
|
|
|
流程:svg_preview.py 渲图并登记 `.build/acceptance.json` → 逐页 read/look_at_image
|
|
过目 → 用本脚本把看过且合格的页标 pass(有问题的标 fail 并写原因)→ 全部 pass
|
|
后 svg_to_pptx.py 才肯导出。
|
|
|
|
用法:
|
|
python accept_pages.py <project_dir> --status # 看各页验收状态
|
|
python accept_pages.py <project_dir> --pass P01_cover P02_toc
|
|
python accept_pages.py <project_dir> --pass-all # 全部标 pass
|
|
python accept_pages.py <project_dir> --fail P05_kpi --reason "大字压说明文字"
|
|
|
|
标 pass 的前提(逐页校验,不满足的页拒标并退非零):
|
|
- 该页渲过图(acceptance.json 有记录、PNG 还在);
|
|
- 渲图之后 svg_output 源没再改过(sha1 一致)—— 改了就得重跑 finalize +
|
|
svg_preview 再来,盲改混不过 gate。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import sys
|
|
try: # zcbot: Windows GBK 控制台兼容
|
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
except Exception:
|
|
pass
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
from svg_preview import acceptance_path, load_acceptance, save_acceptance
|
|
|
|
|
|
def _sha1(path: Path) -> str:
|
|
return hashlib.sha1(path.read_bytes()).hexdigest()
|
|
|
|
|
|
def _page_problems(project_root: Path, stem: str, entry: dict) -> list[str]:
|
|
"""标 pass 前的逐页前提校验;返回问题列表(空 = 可标)。"""
|
|
problems = []
|
|
png = Path(entry.get("png") or "")
|
|
if not png.is_absolute():
|
|
png = project_root / png
|
|
if not entry.get("png") or not png.exists():
|
|
problems.append("渲染 PNG 不存在 —— 先跑 svg_preview.py")
|
|
src = project_root / "svg_output" / f"{stem}.svg"
|
|
if not src.exists():
|
|
src = Path(entry.get("source") or "")
|
|
if str(src) and not src.is_absolute():
|
|
src = project_root / src
|
|
if not src.exists():
|
|
problems.append("找不到源 SVG")
|
|
elif _sha1(src) != entry.get("source_sha1"):
|
|
problems.append("渲图后源 SVG 又改过 —— 重跑 finalize_svg + svg_preview 再标")
|
|
return problems
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description="标记渲图验收 verdict(配合导出 gate)")
|
|
ap.add_argument("project_dir", type=Path)
|
|
g = ap.add_mutually_exclusive_group(required=True)
|
|
g.add_argument("--status", action="store_true", help="打印各页验收状态")
|
|
g.add_argument("--pass", dest="mark_pass", nargs="+", metavar="PAGE",
|
|
help="把指定页(文件名去 .svg)标为 pass")
|
|
g.add_argument("--pass-all", action="store_true",
|
|
help="把所有已渲页标为 pass(仍逐页校验 sha/PNG)")
|
|
g.add_argument("--fail", dest="mark_fail", nargs="+", metavar="PAGE",
|
|
help="把指定页标为 fail(配 --reason)")
|
|
ap.add_argument("--reason", default="", help="fail 的原因,写进记录")
|
|
args = ap.parse_args()
|
|
|
|
root = args.project_dir
|
|
if not root.is_dir():
|
|
print(f"[fatal] 不是目录:{root}")
|
|
return 1
|
|
data = load_acceptance(root)
|
|
pages: dict = data["pages"]
|
|
svg_dir = root / "svg_output"
|
|
all_stems = sorted(p.stem for p in svg_dir.glob("*.svg")) if svg_dir.is_dir() else []
|
|
unrendered = [s for s in all_stems if s not in pages]
|
|
|
|
if args.status:
|
|
if not pages and not all_stems:
|
|
print(f"[status] 无验收记录也无 svg_output:{acceptance_path(root)}")
|
|
return 1
|
|
counts = {"pass": 0, "fail": 0, "pending": 0}
|
|
for stem in sorted(pages):
|
|
e = pages[stem]
|
|
v = e.get("verdict", "pending")
|
|
counts[v if v in counts else "pending"] += 1
|
|
extra = ""
|
|
if v == "pass" and _page_problems(root, stem, e):
|
|
extra = " <- 源已改动/PNG 丢失,导出 gate 会拒(需重渲重标)"
|
|
reason = f" ({e.get('reason')})" if e.get("reason") else ""
|
|
print(f" {v:8s} {stem}{reason}{extra}")
|
|
for stem in unrendered:
|
|
print(f" {'-':8s} {stem} <- 从未渲过")
|
|
print(f"[status] pass {counts['pass']} / fail {counts['fail']} / "
|
|
f"pending {counts['pending']} / 未渲 {len(unrendered)}")
|
|
return 0
|
|
|
|
now = datetime.now().isoformat(timespec="seconds")
|
|
|
|
if args.mark_fail:
|
|
missing = [s for s in args.mark_fail if s not in pages]
|
|
if missing:
|
|
print(f"[fatal] 无渲染记录的页:{', '.join(missing)}(先 svg_preview)")
|
|
return 1
|
|
for stem in args.mark_fail:
|
|
pages[stem].update(verdict="fail", verdict_at=now,
|
|
reason=args.reason or "unspecified")
|
|
print(f" [fail] {stem}" + (f":{args.reason}" if args.reason else ""))
|
|
save_acceptance(root, data)
|
|
return 0
|
|
|
|
targets = sorted(pages) if args.pass_all else args.mark_pass
|
|
missing = [s for s in targets if s not in pages]
|
|
if missing:
|
|
print(f"[fatal] 无渲染记录的页:{', '.join(missing)}(先 svg_preview,"
|
|
"页名 = SVG 文件名去 .svg)")
|
|
return 1
|
|
refused = 0
|
|
for stem in targets:
|
|
problems = _page_problems(root, stem, pages[stem])
|
|
if problems:
|
|
refused += 1
|
|
print(f" [refused] {stem}:{'; '.join(problems)}")
|
|
continue
|
|
pages[stem].update(verdict="pass", verdict_at=now)
|
|
pages[stem].pop("reason", None)
|
|
print(f" [pass] {stem}")
|
|
save_acceptance(root, data)
|
|
if unrendered and args.pass_all:
|
|
print(f" [warn] {len(unrendered)} 页从未渲过、不在记录里:"
|
|
f"{', '.join(unrendered[:8])}{' …' if len(unrendered) > 8 else ''}")
|
|
if refused:
|
|
print(f"[accept_pages] {refused} 页被拒 —— 重跑 finalize_svg + svg_preview "
|
|
"后再标。")
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|