refactor: 문서 개선 중

This commit is contained in:
donghyeon-ka
2026-09-21 14:30:55 +09:00
parent c93cdea150
commit 805a18f486
1497 changed files with 525837 additions and 59152 deletions
@@ -0,0 +1,470 @@
"""schemaVersion 4 런이 command-pedagogy artifact/hash 계약을 빠뜨리지 않는지 본다."""
from __future__ import annotations
import copy
import hashlib
import json
import os
import subprocess
import sys
import tempfile
import unittest
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
SCRIPT = os.path.join(ROOT, "scripts", "verify-pipeline-run.py")
sys.path.insert(0, os.path.join(ROOT, "scripts"))
from command_pedagogy import analyze_commands # noqa: E402
STAGE_SPECS = {
"S1": ("analyzing-codebase-for-tech-log", "ssot-analyst", True),
"S2": ("deriving-tech-log-root-tree", "tree-deriver", True),
"S3": ("writing-tech-log-records", "record-writer", False),
"S4": ("technical-visualizer", "diagram-maker", True),
"S5": ("rewriting-technical-prose-naturally", "prose-rewriter", False),
"S6": ("writing-as-the-person-who-did-it", "voice-writer", False),
"S7": ("publishing-tech-log-to-studio", "studio-validator", True),
}
def verify(path: str):
p = subprocess.run(
[sys.executable, SCRIPT, path], cwd=ROOT, capture_output=True, text=True
)
return p.returncode, p.stdout + p.stderr
def skill_echo(skill: str) -> str:
path = os.path.join(ROOT, ".agents", "skills", skill, "SKILL.md")
with open(path, encoding="utf-8") as fh:
for line in fh:
value = line.strip()
if len(value) >= 30 and not value.startswith(("#", "---", "name:", "description:")):
return value
raise AssertionError(f"usable skill echo not found: {skill}")
def stage(stage_id: str) -> dict:
skill, agent, skippable = STAGE_SPECS[stage_id]
base = {
"id": stage_id,
"name": stage_id,
"skill": skill,
"runBy": agent,
"status": "SKIPPED" if skippable else "DONE",
"skipReason": "synthetic fixture에서 생략" if skippable else "",
"skillEcho": "" if skippable else skill_echo(skill),
"skillRevision": None,
"inputs": [],
"outputs": [],
"gates": [],
"notes": "",
}
if stage_id == "S3":
base["gates"] = [
{"cmd": "node check_body.mjs", "exit": 0},
{"cmd": "node check_prose.mjs", "exit": 0},
{"cmd": "node check_evidence.mjs keycloak --repo", "exit": 0},
]
elif stage_id == "S5":
base["gates"] = [
{"cmd": "node check_prose.mjs", "exit": 0},
{"cmd": "node style_profile.mjs", "exit": 0},
{"cmd": "node check_body.mjs", "exit": 0},
{"cmd": "node check_evidence.mjs keycloak --repo", "exit": 0},
]
elif stage_id == "S6":
base["gates"] = [
{"cmd": "node check_voice.mjs", "exit": 0},
{"cmd": "node check_prose.mjs", "exit": 0},
{"cmd": "node check_body.mjs", "exit": 0},
{"cmd": "node check_evidence.mjs keycloak --repo", "exit": 0},
]
return base
def done_role(agent: str, verdict: str | None = None) -> dict:
role = {
"runBy": agent,
"status": "DONE",
"skipReason": "",
"notes": "",
}
if agent.startswith("command-pedagogy-"):
role.update(
skill="writing-practitioner-guides",
skillEcho=skill_echo("writing-practitioner-guides"),
)
if verdict is not None:
role["verdict"] = verdict
return role
def skipped_role(agent: str, reason: str) -> dict:
role = done_role(agent)
role["status"] = "SKIPPED"
role["skipReason"] = reason
role.pop("verdict", None)
return role
def good_reviews(*, shell_blocks: int = 1, findings: int = 0) -> dict:
needs_edit = findings > 0
return {
"commandPedagogy": {
"initialAnalysis": {
"cmd": "python3 scripts/check-command-pedagogy.py docs/keycloak/final/document.md",
"exit": 0,
"shellBlocks": shell_blocks,
"findings": findings,
"majorFindings": 0,
},
"finalAnalysis": {
"cmd": "python3 scripts/check-command-pedagogy.py docs/keycloak/final/document.md",
"exit": 0,
"shellBlocks": shell_blocks,
"findings": 0,
"majorFindings": 0,
},
"planner": (
done_role("command-pedagogy-planner")
if needs_edit
else skipped_role("command-pedagogy-planner", "deterministic finding 없음")
),
"editor": (
done_role("command-pedagogy-editor")
if needs_edit
else skipped_role("command-pedagogy-editor", "deterministic finding 없음")
),
"reviewer": (
done_role("command-pedagogy-reviewer", "PASS")
if shell_blocks
else skipped_role("command-pedagogy-reviewer", "shell/CLI block 없음")
),
},
"technicalEvidence": done_role("fact-reviewer", "PASS"),
}
def synthetic_run() -> dict:
return {
"schemaVersion": 4,
"runId": "synthetic-command-pedagogy",
"project": "keycloak",
"record": "docs/keycloak/final/document.md",
"startedAt": "2026-09-17T20:00:00+09:00",
"finishedAt": "2026-09-17T20:30:00+09:00",
"stages": [stage(sid) for sid in STAGE_SPECS],
"qualityReviews": good_reviews(),
}
class Schema3ReviewContractTest(unittest.TestCase):
def setUp(self):
runs_dir = os.path.join(ROOT, "runs")
os.makedirs(runs_dir, exist_ok=True)
self.tmp = tempfile.TemporaryDirectory(prefix="test-command-pedagogy-", dir=runs_dir)
self.addCleanup(self.tmp.cleanup)
self.record_path = os.path.join(self.tmp.name, "record.md")
with open(self.record_path, "w", encoding="utf-8") as fh:
fh.write("실행 확인:\n\n```bash\nkubectl get pods\n```\n")
self.record_rel = os.path.relpath(self.record_path, ROOT)
self.base = synthetic_run()
self.base["record"] = self.record_rel
self.base["qualityReviews"] = self.make_reviews()
def artifact(self, name: str, value: dict) -> dict:
path = os.path.join(self.tmp.name, name)
with open(path, "w", encoding="utf-8") as fh:
json.dump(value, fh, ensure_ascii=False, indent=2)
fh.write("\n")
with open(path, "rb") as fh:
digest = hashlib.sha256(fh.read()).hexdigest()
return {"path": os.path.relpath(path, ROOT), "sha256": digest}
def record_sha256(self) -> str:
with open(self.record_path, "rb") as fh:
return hashlib.sha256(fh.read()).hexdigest()
def make_reviews(self, *, findings: int = 0) -> dict:
with open(self.record_path, encoding="utf-8") as fh:
text = fh.read()
final_analysis = analyze_commands("synthetic-command-pedagogy", text, mode="operator")
initial_analysis = copy.deepcopy(final_analysis)
block = initial_analysis["blocks"][0] if initial_analysis["blocks"] else None
if findings:
initial_analysis["result"] = "WARN"
initial_analysis["requires_editor"] = True
initial_analysis["findings"] = [
{
"block_id": block["id"],
"code": f"synthetic-{idx}",
"severity": "minor",
"evidence": "fixture",
"instruction": "fixture",
}
for idx in range(findings)
]
initial_receipt = self.artifact("command-initial.json", initial_analysis)
final_receipt = self.artifact("command-final.json", final_analysis)
source_sha = self.record_sha256()
if findings:
plan = {
"schema_version": "1.0",
"section_id": "synthetic-command-pedagogy",
"source_sha256": initial_analysis["source_sha256"],
"mode": "operator",
"command_groups": [
{
"id": "inspect-pods",
"block_id": block["id"],
"source_sha256": block["source_sha256"],
"goal": "파드 상태를 확인한다.",
"execution_context": {"host": "local", "cwd": "."},
"prerequisites": [],
"steps": [
{
"command": "kubectl get pods",
"reason": "현재 파드 목록을 본다.",
"expected_result": "파드 목록이 출력된다.",
}
],
"cleanup": [],
}
],
}
patch = {
"schema_version": "1.0",
"source_sha256": initial_analysis["source_sha256"],
"patches": [],
}
planner = done_role("command-pedagogy-planner")
planner["artifact"] = self.artifact("command-plan.json", plan)
editor = done_role("command-pedagogy-editor")
editor["artifact"] = self.artifact("command-patch-set.json", patch)
else:
planner = skipped_role("command-pedagogy-planner", "deterministic finding 없음")
planner["artifact"] = None
editor = skipped_role("command-pedagogy-editor", "deterministic finding 없음")
editor["artifact"] = None
if final_analysis["blocks"]:
review = {
"scope": "command-pedagogy",
"reviewer": "command-pedagogy-reviewer",
"verdict": "PASS",
"source_sha256": source_sha,
"findings": [],
"notes": "",
}
reviewer = done_role("command-pedagogy-reviewer", "PASS")
reviewer["sourceSha256"] = source_sha
reviewer["artifact"] = self.artifact("command-review.json", review)
else:
reviewer = skipped_role("command-pedagogy-reviewer", "shell/CLI block 없음")
reviewer["sourceSha256"] = source_sha
reviewer["artifact"] = None
technical = done_role("fact-reviewer", "PASS")
technical["sourceSha256"] = source_sha
return {
"commandPedagogy": {
"initialAnalysis": {
"cmd": f"python3 scripts/check-command-pedagogy.py {self.record_rel} --mode operator",
"exit": 0,
"shellBlocks": len(initial_analysis["blocks"]),
"findings": len(initial_analysis["findings"]),
"majorFindings": sum(1 for f in initial_analysis["findings"] if f["severity"] == "major"),
"artifact": initial_receipt,
},
"finalAnalysis": {
"cmd": f"python3 scripts/check-command-pedagogy.py {self.record_rel} --mode operator",
"exit": 0,
"shellBlocks": len(final_analysis["blocks"]),
"findings": len(final_analysis["findings"]),
"majorFindings": sum(1 for f in final_analysis["findings"] if f["severity"] == "major"),
"artifact": final_receipt,
},
"planner": planner,
"editor": editor,
"reviewer": reviewer,
},
"technicalEvidence": technical,
}
def write(self, mutate=None):
data = copy.deepcopy(self.base)
if mutate:
mutate(data)
path = os.path.join(self.tmp.name, "run.json")
with open(path, "w", encoding="utf-8") as fh:
json.dump(data, fh, ensure_ascii=False, indent=2)
return path
def test_schema3_with_complete_reviews_passes(self):
code, out = verify(self.write())
self.assertEqual(0, code, out)
def test_schema3_legacy_quality_receipts_do_not_require_new_artifact_hash_fields(self):
def mutate(data):
data["schemaVersion"] = 3
command = data["qualityReviews"]["commandPedagogy"]
command["initialAnalysis"].pop("artifact", None)
command["finalAnalysis"].pop("artifact", None)
for name in ("planner", "editor", "reviewer"):
command[name].pop("artifact", None)
command[name].pop("sourceSha256", None)
data["qualityReviews"]["technicalEvidence"].pop("sourceSha256", None)
code, out = verify(self.write(mutate))
self.assertEqual(0, code, out)
def test_schema3_requires_quality_review_receipts(self):
code, out = verify(self.write(lambda d: d.pop("qualityReviews")))
self.assertEqual(1, code)
self.assertIn("품질 검토 원장이 없다", out)
def test_findings_require_planner_and_editor(self):
def mutate(data):
data["qualityReviews"] = self.make_reviews(findings=2)
data["qualityReviews"]["commandPedagogy"]["planner"] = skipped_role(
"command-pedagogy-planner", "임의 생략"
)
code, out = verify(self.write(mutate))
self.assertEqual(1, code)
self.assertIn("명령 finding이 있는데 planner가 끝나지 않았다", out)
def test_shell_blocks_require_independent_reviewer(self):
def mutate(data):
data["qualityReviews"]["commandPedagogy"]["reviewer"] = skipped_role(
"command-pedagogy-reviewer", "임의 생략"
)
code, out = verify(self.write(mutate))
self.assertEqual(1, code)
self.assertIn("shell/CLI가 있는데 command reviewer가 끝나지 않았다", out)
def test_uncertain_command_review_blocks_acceptance(self):
def mutate(data):
data["qualityReviews"]["commandPedagogy"]["reviewer"]["verdict"] = "UNCERTAIN"
code, out = verify(self.write(mutate))
self.assertEqual(1, code)
self.assertIn("command-pedagogy review가 통과하지 못했다", out)
def test_final_major_finding_blocks_acceptance(self):
def mutate(data):
data["qualityReviews"]["commandPedagogy"]["finalAnalysis"]["majorFindings"] = 1
code, out = verify(self.write(mutate))
self.assertEqual(1, code)
self.assertIn("major command finding이 남았다", out)
def test_command_repair_cannot_remove_all_shell_blocks(self):
def mutate(data):
data["qualityReviews"]["commandPedagogy"]["finalAnalysis"]["shellBlocks"] = 0
data["qualityReviews"]["commandPedagogy"]["reviewer"] = skipped_role(
"command-pedagogy-reviewer", "최종 shell block 없음"
)
code, out = verify(self.write(mutate))
self.assertEqual(1, code)
self.assertIn("command repair가 모든 shell block을 없앴다", out)
def test_fact_review_runs_after_repairs_and_must_pass(self):
def mutate(data):
data["qualityReviews"]["technicalEvidence"]["verdict"] = "FAIL"
code, out = verify(self.write(mutate))
self.assertEqual(1, code)
self.assertIn("technical-evidence review가 통과하지 못했다", out)
def test_command_free_record_skips_command_roles_but_keeps_fact_review(self):
with open(self.record_path, "w", encoding="utf-8") as fh:
fh.write("명령어가 없는 설명 문단이다.\n")
self.base["qualityReviews"] = self.make_reviews()
code, out = verify(self.write())
self.assertEqual(0, code, out)
def test_initial_analysis_requires_path_and_sha_evidence(self):
def mutate(data):
data["qualityReviews"]["commandPedagogy"]["initialAnalysis"].pop("artifact")
code, out = verify(self.write(mutate))
self.assertEqual(1, code)
self.assertIn("command artifact 영수증이 없다", out)
def test_artifact_sha_mismatch_blocks_acceptance(self):
def mutate(data):
data["qualityReviews"]["commandPedagogy"]["finalAnalysis"]["artifact"]["sha256"] = "0" * 64
code, out = verify(self.write(mutate))
self.assertEqual(1, code)
self.assertIn("command artifact sha256이 실제 파일과 다르다", out)
def test_plan_and_patch_artifacts_are_required_when_findings_exist(self):
self.base["qualityReviews"] = self.make_reviews(findings=1)
def mutate(data):
data["qualityReviews"]["commandPedagogy"]["planner"].pop("artifact")
data["qualityReviews"]["commandPedagogy"]["editor"].pop("artifact")
code, out = verify(self.write(mutate))
self.assertEqual(1, code)
self.assertIn("command role artifact 영수증이 없다", out)
def test_command_review_is_bound_to_final_publication_hash(self):
def mutate(data):
data["qualityReviews"]["commandPedagogy"]["reviewer"]["sourceSha256"] = "0" * 64
code, out = verify(self.write(mutate))
self.assertEqual(1, code)
self.assertIn("최종 publication hash와 다르다", out)
def test_technical_evidence_review_is_bound_to_final_publication_hash(self):
def mutate(data):
data["qualityReviews"]["technicalEvidence"]["sourceSha256"] = "0" * 64
code, out = verify(self.write(mutate))
self.assertEqual(1, code)
self.assertIn("최종 publication hash와 다르다", out)
def test_command_review_artifact_is_required_for_command_content(self):
def mutate(data):
data["qualityReviews"]["commandPedagogy"]["reviewer"].pop("artifact")
code, out = verify(self.write(mutate))
self.assertEqual(1, code)
self.assertIn("command role artifact 영수증이 없다", out)
class Schema3InitTest(unittest.TestCase):
def test_init_uses_current_schema_and_prepares_review_receipts(self):
with tempfile.TemporaryDirectory() as d:
path = os.path.join(d, "run.json")
p = subprocess.run(
[
sys.executable,
SCRIPT,
"--init",
path,
"--project",
"keycloak",
"--record",
"docs/keycloak/final/document.md",
],
cwd=ROOT,
capture_output=True,
text=True,
)
self.assertEqual(0, p.returncode, p.stdout + p.stderr)
with open(path, encoding="utf-8") as fh:
data = json.load(fh)
self.assertEqual(5, data["schemaVersion"])
self.assertIn("commandPedagogy", data["qualityReviews"])
self.assertIn("technicalEvidence", data["qualityReviews"])
command = data["qualityReviews"]["commandPedagogy"]
self.assertIn("artifact", command["initialAnalysis"])
self.assertIn("artifact", command["finalAnalysis"])
self.assertIn("artifact", command["planner"])
self.assertIn("artifact", command["editor"])
self.assertIn("artifact", command["reviewer"])
self.assertIn("sourceSha256", command["reviewer"])
technical = data["qualityReviews"]["technicalEvidence"]
self.assertIn("sourceSha256", technical)
self.assertIn("liveSourceReconciliation", technical)
self.assertIn("liveSourceReason", technical)
self.assertIn("acceptedByProjectReview", technical)
if __name__ == "__main__":
unittest.main()