Files
codex-app-server/cloud/skills/software-engineering/tools/check_appendix.py
T
host 4e554b5c07 docs(skill): add appendix provenance sources, checker and CHANGELOG
SKILL.md claimed its appendix was spliced verbatim from
templates/roles/ by a generation script (see CHANGELOG), but the pack
contained none of the three. Add the nine role sources (mechanically
split from the v0.2 appendix; SKILL.md untouched, zero semantic
change), tools/check_appendix.py (verify + --emit, stdlib only, never
edits SKILL.md), CHANGELOG.md, and test_skill_appendix_provenance.py
(5 tests). Seeding unchanged: skill_seed copies SKILL.md only, so
templates/tools/CHANGELOG never reach the tenant home.
2026-11-03 02:48:55 +00:00

234 lines
9.5 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.
#!/usr/bin/env python3
"""Verify the software-engineering skill appendix against its sources.
Provenance (see CHANGELOG.md in the skill directory): SKILL.md claims
its appendix is "spliced verbatim from templates/roles/ by a generation
script". The sources live at templates/roles/<id>.md next to SKILL.md;
this script is the check that makes that claim testable. It never edits
SKILL.md: the fix for a mismatch is an editorial decision (edit the
source, then re-issue the appendix through the skill's own track),
never a silent rewrite by this script.
What it checks, in order:
1. SKILL.md contains the appendix intro and exactly the nine
chapters 00..08, in order, each with a source note naming its own
templates/roles/<id>.md file.
2. templates/roles/ contains exactly those nine source files:
a missing source fails, an unexpected extra file fails.
3. Each source, in canonical form, equals the matching appendix
chapter in canonical form (see canonical() below).
4. CHANGELOG.md exists in the skill directory and mentions
templates/roles/, so the "see CHANGELOG" pointer in SKILL.md
resolves to a provenance record, not a dangling reference.
Canonical form (the ONLY allowed differences between a source file
and its appendix chapter -- anything else is a failure):
a. The chapter heading line itself ("## 附录角色:<id>(来源
templates/roles/<id>.md)") exists only in SKILL.md; it is
derived from the file name, not stored in the source.
b. One inter-chapter separator line "---": in the current SKILL.md
it appears after chapters 01/02/03/05/06 only. Chapters
00/04/07 end with their GitHub subsection and run straight into
the next heading with no separator, and 08 is the end of file.
The check strips at most one trailing separator; it does NOT
normalise SKILL.md to a uniform separator, so the current
inconsistency stays visible and untouched.
c. Blank lines around the heading and at the chapter end: a source
file is stored with exactly one trailing newline and no leading
blank line; the check strips surrounding blank lines before
comparing.
Any other difference -- wording, punctuation, order, a dropped or
added line -- fails with the first differing line number.
Usage:
python3 tools/check_appendix.py [--skill-dir DIR]
python3 tools/check_appendix.py --emit [--skill-dir DIR]
--emit prints the spliced appendix in canonical form (heading line +
source body per chapter) to stdout. It is a drafting aid for an
editor, NOT a byte patch for SKILL.md: because of (b) and (c) above,
its output differs from SKILL.md in separators and blank lines only,
and it must never be pasted over SKILL.md without a review pass.
Exit codes: 0 = all chapters match; 1 = any check failed; 2 = usage
error (argparse). Only the standard library is used, so the script
runs standalone without importing cloud/.
"""
import argparse
import re
import sys
from pathlib import Path
EXPECTED_IDS = ["00-coordinator", "01-product-manager", "02-architect",
"03-designer", "04-client-developer", "05-backend",
"06-qa", "07-acceptance", "08-shared-core"]
CHAPTER_RE = re.compile(
r"^## 附录角色:(\d{2}-[a-z0-9-]+)"
r"(来源 templates/roles/(\d{2}-[a-z0-9-]+\.md))",
flags=re.M)
TITLE_RE = re.compile(r"^# 角色模板:", flags=re.M)
APPENDIX_MARKER = "## 附录:角色模板"
def canonical(segment):
"""Canonical chapter body: strip surrounding blanks and at most
one trailing inter-chapter separator line, end with one newline."""
lines = segment.split("\n")
while lines and lines[0] == "":
lines.pop(0)
while lines and lines[-1] == "":
lines.pop()
if lines and lines[-1] == "---":
lines.pop()
while lines and lines[-1] == "":
lines.pop()
return "\n".join(lines) + "\n"
def first_diff(expected, actual):
"""Return (line_no_1_based, expected_line, actual_line) or None."""
exp_lines = expected.split("\n")
act_lines = actual.split("\n")
for i, (e, a) in enumerate(zip(exp_lines, act_lines)):
if e != a:
return i + 1, e, a
if len(exp_lines) != len(act_lines):
i = min(len(exp_lines), len(act_lines))
e = exp_lines[i] if i < len(exp_lines) else "<end of source>"
a = act_lines[i] if i < len(act_lines) else "<end of appendix>"
return i + 1, e, a
return None
def load_chapters(skill_dir):
"""Return (errors, chapters) where chapters maps id -> body segment
(raw text between its heading line and the next heading / EOF)."""
errors = []
skill_path = skill_dir / "SKILL.md"
if not skill_path.is_file():
return ["SKILL.md 缺失:%s" % skill_path], {}
text = skill_path.read_text(encoding="utf-8")
if APPENDIX_MARKER not in text:
return ["SKILL.md 缺失附录引言节 %r" % APPENDIX_MARKER], {}
appendix = text.split(APPENDIX_MARKER, 1)[1]
matches = list(CHAPTER_RE.finditer(appendix))
found = [m.group(1) for m in matches]
if found != EXPECTED_IDS:
errors.append(
"附录章节不符:应为 %s,实际为 %s" % (EXPECTED_IDS, found))
chapters = {}
for i, m in enumerate(matches):
end = matches[i + 1].start() if i + 1 < len(matches) else len(appendix)
if m.group(2) != m.group(1) + ".md":
errors.append(
"章节 %s 的来源注不符:应为 %s,实际为 %s"
% (m.group(1), m.group(1) + ".md", m.group(2)))
chapters[m.group(1)] = appendix[m.end():end]
return errors, chapters
def check(skill_dir):
"""Return a list of failure strings (empty means pass)."""
errors, chapters = load_chapters(skill_dir)
roles_dir = skill_dir / "templates" / "roles"
if not roles_dir.is_dir():
errors.append("源目录缺失:%s" % roles_dir)
sources = {}
else:
on_disk = sorted(p.name for p in roles_dir.iterdir()
if p.is_file())
expected_files = [cid + ".md" for cid in EXPECTED_IDS]
if on_disk != expected_files:
errors.append(
"源文件集合不符:应为 %s,实际为 %s"
% (expected_files, on_disk))
sources = {}
for name in on_disk:
sources[name[:-3]] = (roles_dir / name).read_text(
encoding="utf-8")
for cid in EXPECTED_IDS:
if cid not in sources:
errors.append("源缺失:templates/roles/%s.md 不存在" % cid)
continue
source = sources[cid]
if not TITLE_RE.match(source):
errors.append(
"源格式不符:%s.md 首行不是“# 角色模板:”" % cid)
if canonical(source) != source:
errors.append(
"源非规范形式:%s.md 含首尾空行或尾部分隔符"
"(源文件须以正文起、以单个换行收)" % cid)
if cid not in chapters:
errors.append("附录缺失章节:%s" % cid)
continue
expected = source
actual = canonical(chapters[cid])
if actual != expected:
diff = first_diff(expected, actual)
if diff is not None:
lineno, exp_line, act_line = diff
errors.append(
"内容不一致:章节 %s 第 %d 行不同\n"
" 源:%s\n 附录:%s"
% (cid, lineno, exp_line[:120], act_line[:120]))
else:
errors.append("内容不一致:章节 %s(未定位首异行)" % cid)
changelog = skill_dir / "CHANGELOG.md"
if not changelog.is_file():
errors.append("CHANGELOG.md 缺失:%s" % changelog)
elif "templates/roles/" not in changelog.read_text(encoding="utf-8"):
errors.append(
"CHANGELOG.md 未提及 templates/roles/,溯源记录不完整")
return errors
def emit(skill_dir):
"""Print the spliced appendix in canonical form to stdout."""
_errors, chapters = load_chapters(skill_dir)
roles_dir = skill_dir / "templates" / "roles"
parts = []
for cid in EXPECTED_IDS:
source = (roles_dir / (cid + ".md")).read_text(encoding="utf-8")
heading = ("## 附录角色:%s(来源 templates/roles/%s.md)"
% (cid, cid))
parts.append(heading + "\n\n" + source.rstrip("\n"))
return "\n\n".join(parts) + "\n"
def main(argv=None):
parser = argparse.ArgumentParser(
description="Verify the software-engineering skill appendix "
"against templates/roles/ sources.")
parser.add_argument(
"--skill-dir", default=None,
help="skill directory (default: parent of this script's "
"tools/ directory)")
parser.add_argument(
"--emit", action="store_true",
help="print the spliced appendix in canonical form instead "
"of verifying (drafting aid only, never a byte patch)")
args = parser.parse_args(argv)
if args.skill_dir:
skill_dir = Path(args.skill_dir)
else:
skill_dir = Path(__file__).resolve().parents[1]
if args.emit:
sys.stdout.write(emit(skill_dir))
return 0
errors = check(skill_dir)
if errors:
for err in errors:
print("FAIL: " + err, file=sys.stderr)
print("appendix provenance FAILED: %d problem(s)" % len(errors),
file=sys.stderr)
return 1
print("appendix provenance OK: %d chapters match templates/roles/"
% len(EXPECTED_IDS))
return 0
if __name__ == "__main__":
raise SystemExit(main())