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.
This commit is contained in:
host committed 2026-11-04 20:54:04 +00:00
1 parent 93f2b350ac
commit 99ce8cf72d
12 files changed
+818

No files matched your search

@@ -0,0 +1,117 @@
"""Appendix provenance for the software-engineering skill.
Background (see cloud/skills/software-engineering/CHANGELOG.md):
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: no templates/, no CHANGELOG.md, no
script. The appendix was therefore unverifiable -- a drifted chapter
would pass CI silently.
This file pins the repair, and nothing else:
- tools/check_appendix.py passes on the real pack (all nine chapters
match their templates/roles/ sources, CHANGELOG present and
mentioning templates/roles/);
- the same script FAILS (nonzero exit) on a temp copy with a source
deleted, a source tampered, or an extra source added -- so a future
"source missing" or "appendix drifted" state cannot slip through;
- --emit output is exactly the nine headings plus the nine source
bodies, so the spliced form is itself derived from the sources.
Scope guard: these tests never edit SKILL.md or the sources. A
failure here is an editorial decision for the skill's own track
(edit the source, then re-issue the appendix), not a silent rewrite.
Seeding behaviour is pinned elsewhere (test_skill_seed_pinning.py):
templates/, tools/ and CHANGELOG.md never reach the tenant home,
because skill_seed copies SKILL.md only.
"""
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
SKILL_DIR = (REPO_ROOT / "cloud" / "skills" / "software-engineering")
SCRIPT = SKILL_DIR / "tools" / "check_appendix.py"
EXPECTED_IDS = ["00-coordinator", "01-product-manager", "02-architect",
"03-designer", "04-client-developer", "05-backend",
"06-qa", "07-acceptance", "08-shared-core"]
def _run(skill_dir=None):
cmd = [sys.executable, str(SCRIPT)]
if skill_dir is not None:
cmd += ["--skill-dir", str(skill_dir)]
return subprocess.run(cmd, capture_output=True, text=True)
def _temp_skill_copy(testcase):
tmp = tempfile.TemporaryDirectory()
testcase.addCleanup(tmp.cleanup)
dest = Path(tmp.name) / "software-engineering"
shutil.copytree(SKILL_DIR, dest)
return dest
class AppendixProvenanceTestCase(unittest.TestCase):
def test_check_script_passes_on_real_pack(self):
self.assertTrue(SCRIPT.is_file(), "校验脚本缺失:%s" % SCRIPT)
self.assertTrue(
(SKILL_DIR / "CHANGELOG.md").is_file(),
"CHANGELOG.md 缺失,SKILL.md 的“见 CHANGELOG”仍是悬空引用")
result = _run()
self.assertEqual(
result.returncode, 0,
"校验脚本应在真实包上通过,stderr:%s" % result.stderr)
self.assertIn("9 chapters match", result.stdout)
def test_missing_source_fails(self):
skill = _temp_skill_copy(self)
(skill / "templates" / "roles" / "03-designer.md").unlink()
result = _run(skill)
self.assertNotEqual(
result.returncode, 0, "源缺失时校验必须失败退出非零")
self.assertIn("03-designer", result.stderr)
def test_tampered_source_fails(self):
skill = _temp_skill_copy(self)
target = skill / "templates" / "roles" / "06-qa.md"
text = target.read_text(encoding="utf-8")
target.write_text(text.replace("不把“没测”写成“通过”",
"不把“没测”写成“通过X”"),
encoding="utf-8")
result = _run(skill)
self.assertNotEqual(
result.returncode, 0, "源被改动时校验必须失败退出非零")
self.assertIn("06-qa", result.stderr)
def test_extra_source_fails(self):
skill = _temp_skill_copy(self)
(skill / "templates" / "roles" / "99-extra.md").write_text(
"# 角色模板:多余\n", encoding="utf-8")
result = _run(skill)
self.assertNotEqual(
result.returncode, 0, "多余源文件时校验必须失败退出非零")
self.assertIn("99-extra", result.stderr)
def test_emit_is_headings_plus_sources(self):
result = subprocess.run(
[sys.executable, str(SCRIPT), "--emit"],
capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
expected_parts = []
for cid in EXPECTED_IDS:
heading = ("## 附录角色:%s(来源 templates/roles/%s.md)"
% (cid, cid))
source = (SKILL_DIR / "templates" / "roles"
/ (cid + ".md")).read_text(encoding="utf-8")
expected_parts.append(heading + "\n\n" + source.rstrip("\n"))
self.assertEqual(result.stdout, "\n\n".join(expected_parts) + "\n",
"--emit 输出须恰为 9 章标题 + 9 份源正文")
if __name__ == "__main__":
unittest.main()