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
+565
View File
@@ -0,0 +1,565 @@
"""명령어 교육성 분석기 회귀 시험.
정확한 명령을 금지하는 검사가 아니라, 사람이 따라 하기 어려운 압축 표현을
후속 planner/editor/reviewer가 볼 수 있게 결정론적으로 표시하는지 본다.
"""
from __future__ import annotations
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__))))
sys.path.insert(0, os.path.join(ROOT, "scripts"))
from command_pedagogy import ( # noqa: E402
analyze_commands,
apply_command_patch_set,
validate_command_patch_set,
validate_command_plan,
)
class CommandPedagogyAnalyzerTest(unittest.TestCase):
def codes(self, text: str) -> set[str]:
result = analyze_commands("record", text)
return {finding["code"] for finding in result["findings"]}
def test_sed_in_place_and_command_substitution_are_signals(self):
text = '''```bash
sed -i \\
-e "s|__LAB_HOST_KEY__|$(cat ~/.ssh/id_ed25519.pub)|" \\
lab.yaml
```
'''
self.assertEqual(
{"in-place-text-rewrite", "command-substitution"},
self.codes(text),
)
def test_sed_print_mode_is_not_in_place_rewrite(self):
text = r'''```bash
sed -n '/^COPY public.databasechangelog /,/^\\\.$/p' dump.sql | wc -l
```
'''
self.assertNotIn("in-place-text-rewrite", self.codes(text))
def test_remote_compound_flow_exposes_ip_and_cleanup_chain(self):
text = '''```bash
ssh donghyeon@192.168.122.11 'cloud-init schema -c ~/kc-lab-2.yaml && rm ~/kc-lab-2.yaml'
```
'''
self.assertEqual(
{
"raw-ssh-ip",
"compound-remote-shell",
"cleanup-chained-with-verification",
"execution-context-implicit",
},
self.codes(text),
)
result = analyze_commands("record", text)
severity = {finding["code"]: finding["severity"] for finding in result["findings"]}
self.assertEqual("minor", severity["compound-remote-shell"])
self.assertEqual("major", severity["cleanup-chained-with-verification"])
def test_reference_mode_keeps_historical_cleanup_as_review_signal(self):
text = '''<!-- command-mode: reference -->
```bash
ssh host 'cloud-init schema -c ~/lab.yaml; rm -f ~/lab.yaml'
```
'''
result = analyze_commands("record", text, mode="operator")
severity = {
finding["code"]: finding["severity"]
for finding in result["findings"]
}
self.assertEqual(
"minor",
severity["cleanup-chained-with-verification"],
)
self.assertEqual("reference", result["blocks"][0]["mode"])
def test_label_mode_marker_can_mark_reference_without_html_marker(self):
text = '''```bash label="[lab host] [reference] historical"
ssh host 'cloud-init schema -c ~/lab.yaml; rm -f ~/lab.yaml'
```
'''
result = analyze_commands("record", text, mode="operator")
self.assertEqual("reference", result["blocks"][0]["mode"])
self.assertTrue(result["blocks"][0]["mode_explicit"])
severity = {
finding["code"]: finding["severity"]
for finding in result["findings"]
}
self.assertEqual(
"minor",
severity["cleanup-chained-with-verification"],
)
def test_printf_generated_file_and_substitution_are_signals(self):
text = '''```bash
printf 'instance-id: kc-lab-1-%s\\nlocal-hostname: kc-lab-1\\n' "$(date +%s)" > meta-kc-lab-1
```
'''
self.assertEqual(
{"opaque-file-generation", "command-substitution"},
self.codes(text),
)
def test_hidden_stderr_and_compressed_pipeline_are_signals(self):
text = '''```bash
openssl s_client -connect example.com:443 2>/dev/null | grep subject | head -1
```
'''
self.assertEqual(
{"hidden-stderr", "compressed-pipeline"},
self.codes(text),
)
def test_simple_operator_commands_are_not_globally_banned(self):
text = '''```bash
kubectl get pods
kubectl describe pod api-0
```
'''
result = analyze_commands("record", text)
self.assertEqual("PASS", result["result"])
self.assertFalse(result["requires_editor"])
self.assertEqual(1, len(result["blocks"]))
self.assertEqual([], result["findings"])
def test_non_shell_fences_are_out_of_scope(self):
result = analyze_commands("record", '''```yaml
apiVersion: v1
kind: Pod
```
''')
self.assertEqual([], result["blocks"])
self.assertEqual([], result["findings"])
def test_block_metadata_is_stable_and_bounded(self):
text = '''앞 문장
```sh
echo hello
```
뒤 문장
'''
result = analyze_commands("record", text)
block = result["blocks"][0]
self.assertEqual("command-001", block["id"])
self.assertEqual("sh", block["language"])
self.assertEqual(text[block["start"]:block["end"]], block["source"])
self.assertEqual(64, len(block["source_sha256"]))
self.assertEqual(64, len(result["source_sha256"]))
def test_labeled_shell_fence_uses_first_info_token_as_language(self):
text = '''```bash label="[lab host] 파드 상태를 본다"
kubectl get pods
```
'''
result = analyze_commands("record", text)
self.assertEqual(1, len(result["blocks"]))
block = result["blocks"][0]
self.assertEqual("bash", block["language"])
self.assertEqual('label="[lab host] 파드 상태를 본다"', block["info_string"])
self.assertEqual("[lab host] 파드 상태를 본다", block["label"])
def test_labeled_sh_fence_is_analyzed(self):
text = '''```sh label="[탐침 파드] 연결을 확인한다"
curl -fsS http://keycloak:8080/health/ready
```
'''
result = analyze_commands("record", text)
self.assertEqual(1, len(result["blocks"]))
self.assertEqual("sh", result["blocks"][0]["language"])
def test_language_must_be_the_complete_first_info_token(self):
result = analyze_commands("record", '''```bashish label="[lab host]"
echo nope
```
''')
self.assertEqual([], result["blocks"])
def test_non_context_label_does_not_hide_remote_context_signal(self):
text = '''```bash label="[결과 확인] 원격 상태를 본다"
ssh test-server
```
'''
result = analyze_commands("record", text)
codes = {finding["code"] for finding in result["findings"]}
self.assertIn("execution-context-implicit", codes)
def test_execution_context_in_label_suppresses_context_signal(self):
text = '''```bash label="[워크스테이션] test-server에 접속한다"
ssh test-server
```
'''
result = analyze_commands("record", text)
codes = {finding["code"] for finding in result["findings"]}
self.assertNotIn("execution-context-implicit", codes)
def test_inline_command_mode_overrides_document_default(self):
text = '''일반 참고 문서다.
<!-- command-mode: operator -->
```bash
echo "$(date +%s)"
```
'''
result = analyze_commands("record", text, mode="reference")
self.assertEqual("operator", result["blocks"][0]["mode"])
self.assertIn(
"command-substitution",
{finding["code"] for finding in result["findings"]},
)
def test_inline_command_mode_applies_only_to_the_next_block(self):
text = '''<!-- command-mode: operator -->
```bash
echo "$(date +%s)"
```
```bash
echo "$(date +%s)"
```
'''
result = analyze_commands("record", text, mode="reference")
self.assertTrue(result["blocks"][0]["mode_explicit"])
self.assertEqual("operator", result["blocks"][0]["mode"])
self.assertFalse(result["blocks"][1]["mode_explicit"])
self.assertEqual("reference", result["blocks"][1]["mode"])
substitution_blocks = {
finding["block_id"]
for finding in result["findings"]
if finding["code"] == "command-substitution"
}
self.assertEqual({"command-001"}, substitution_blocks)
def test_text_fence_with_cli_flow_is_reported_as_extension(self):
text = '''```text
docker build . → docker save app | gzip → scp app.tar.gz test-server:/tmp/
→ kubectl set image deployment/app app=local/app:test
```
'''
result = analyze_commands("record", text, mode="reference")
self.assertEqual([], result["blocks"])
self.assertEqual([], result["findings"])
self.assertEqual(
1,
len(result["extensions"]["command_like_text_blocks"]),
)
def test_explicit_reference_text_flow_is_not_reported_as_misfenced_command(self):
text = '''<!-- command-mode: reference -->
```text
docker build . → docker save app | gzip → scp app.tar.gz test-server:/tmp/
→ kubectl set image deployment/app app=local/app:test
```
'''
result = analyze_commands("record", text, mode="reference")
self.assertEqual([], result["extensions"]["command_like_text_blocks"])
def test_cwd_change_followed_by_new_command_group_is_a_signal(self):
text = '''```bash
cd src
./gradlew build
# 다른 도구
python3 scripts/check.py
```
'''
result = analyze_commands("record", text, mode="operator")
self.assertIn(
"cwd-transition-crosses-command-group",
{finding["code"] for finding in result["findings"]},
)
class CommandPatchBoundaryTest(unittest.TestCase):
def test_patch_can_replace_only_the_named_command_span(self):
text = '''앞 문장은 그대로다.\n\n```bash\nprintf 'x=%s\\n' "$(date +%s)" > x.conf\n```\n\n뒤 문장도 그대로다.\n'''
analysis = analyze_commands("record", text)
block = analysis["blocks"][0]
replacement = '''```bash\nnano x.conf\n```\n```text\nx=20260917\n```'''
repaired = apply_command_patch_set(
text=text,
analysis=analysis,
patch_set={
"schema_version": "1.0",
"source_sha256": analysis["source_sha256"],
"patches": [{
"block_id": block["id"],
"expected_sha256": block["source_sha256"],
"replacement_markdown": replacement,
}],
},
)
self.assertTrue(repaired.startswith("앞 문장은 그대로다.\n\n"))
self.assertTrue(repaired.endswith("\n\n뒤 문장도 그대로다.\n"))
self.assertIn(replacement, repaired)
self.assertNotIn("printf", repaired)
def test_stale_block_hash_is_rejected(self):
text = '''```bash\nsed -i 's/a/b/' x.conf\n```\n'''
analysis = analyze_commands("record", text)
with self.assertRaisesRegex(ValueError, "expected block hash"):
apply_command_patch_set(
text=text,
analysis=analysis,
patch_set={
"schema_version": "1.0",
"source_sha256": analysis["source_sha256"],
"patches": [{
"block_id": "command-001",
"expected_sha256": "0" * 64,
"replacement_markdown": "```bash\\nnano x.conf\\n```",
}],
},
)
def test_patch_cannot_reference_an_unknown_block(self):
text = '''```bash\necho hello\n```\n'''
analysis = analyze_commands("record", text)
with self.assertRaisesRegex(ValueError, "unknown block"):
apply_command_patch_set(
text=text,
analysis=analysis,
patch_set={
"schema_version": "1.0",
"source_sha256": analysis["source_sha256"],
"patches": [{
"block_id": "command-999",
"expected_sha256": "0" * 64,
"replacement_markdown": "```bash\\necho world\\n```",
}],
},
)
class CommandPatchCliTest(unittest.TestCase):
def test_cli_applies_a_frozen_patch_set(self):
script = os.path.join(ROOT, "scripts", "apply-command-pedagogy-patch.py")
text = "\n\n```bash\nprintf 'x\\n' > x.conf\n```\n\n\n"
analysis = analyze_commands("record", text)
block = analysis["blocks"][0]
patch_set = {
"schema_version": "1.0",
"source_sha256": analysis["source_sha256"],
"patches": [{
"block_id": block["id"],
"expected_sha256": block["source_sha256"],
"replacement_markdown": "```bash\nnano x.conf\n```",
}],
}
with tempfile.TemporaryDirectory() as d:
record = os.path.join(d, "record.md")
analysis_path = os.path.join(d, "analysis.json")
patch_path = os.path.join(d, "patch.json")
output = os.path.join(d, "repaired.md")
with open(record, "w", encoding="utf-8") as fh:
fh.write(text)
with open(analysis_path, "w", encoding="utf-8") as fh:
json.dump(analysis, fh, ensure_ascii=False)
with open(patch_path, "w", encoding="utf-8") as fh:
json.dump(patch_set, fh, ensure_ascii=False)
p = subprocess.run(
[sys.executable, script, record, analysis_path, patch_path, "-o", output],
cwd=ROOT,
capture_output=True,
text=True,
)
self.assertEqual(0, p.returncode, p.stdout + p.stderr)
with open(output, encoding="utf-8") as fh:
repaired = fh.read()
self.assertEqual("\n\n```bash\nnano x.conf\n```\n\n\n", repaired)
class CommandPedagogyModeTest(unittest.TestCase):
def test_zsh_fence_is_analyzed(self):
result = analyze_commands("record", "```zsh\nssh user@192.168.1.10\n```\n")
self.assertEqual(1, len(result["blocks"]))
self.assertEqual("zsh", result["blocks"][0]["language"])
def test_nested_command_substitution_is_a_distinct_signal(self):
result = analyze_commands(
"record",
"```bash\necho \"$(printf %s \"$(date +%s)\")\"\n```\n",
)
codes = {finding["code"] for finding in result["findings"]}
self.assertIn("nested-command-substitution", codes)
def test_automation_mode_does_not_blanket_flag_compact_shell(self):
text = '''자동화 스크립트는 입력 파일을 갱신한 뒤 결과를 별도 검증한다.\n\n```bash\nsed -i -e "s|__KEY__|$(cat key.pub)|" config.yaml\ncat config.yaml | grep KEY | head -1\n```\n'''
operator = analyze_commands("record", text, mode="operator")
automation = analyze_commands("record", text, mode="automation")
self.assertTrue(operator["findings"])
self.assertEqual("automation", automation["mode"])
self.assertEqual([], automation["findings"])
def test_unknown_mode_is_rejected(self):
with self.assertRaisesRegex(ValueError, "unsupported command pedagogy mode"):
analyze_commands("record", "```bash\necho ok\n```\n", mode="unknown")
def test_remote_command_without_execution_context_is_a_signal(self):
result = analyze_commands("record", "```bash\nssh test-server\n```\n")
codes = {finding["code"] for finding in result["findings"]}
self.assertIn("execution-context-implicit", codes)
def test_explicit_execution_context_suppresses_context_signal(self):
text = "이 명령은 로컬 워크스테이션에서 실행한다.\n\n```bash\nssh test-server\n```\n"
result = analyze_commands("record", text)
codes = {finding["code"] for finding in result["findings"]}
self.assertNotIn("execution-context-implicit", codes)
def test_repeated_raw_ssh_ip_is_reported(self):
text = '''```bash\nssh user@192.168.122.11\n```\n\n```bash\nscp x user@192.168.122.11:~/x\n```\n'''
result = analyze_commands("record", text)
codes = [finding["code"] for finding in result["findings"]]
self.assertIn("repeated-raw-ssh-host", codes)
class CommandArtifactContractTest(unittest.TestCase):
def setUp(self):
self.text = "```bash\nssh user@192.168.1.10\n```\n"
self.analysis = analyze_commands("section-a", self.text)
self.block = self.analysis["blocks"][0]
def valid_plan(self):
return {
"schema_version": "1.0",
"section_id": "section-a",
"source_sha256": self.analysis["source_sha256"],
"mode": "operator",
"command_groups": [
{
"id": "connect-host",
"block_id": self.block["id"],
"source_sha256": self.block["source_sha256"],
"goal": "대상 호스트 연결을 확인한다.",
"execution_context": {"host": "local", "cwd": "."},
"prerequisites": ["SSH key가 준비되어 있다."],
"steps": [
{
"command": "ssh test-server",
"reason": "별칭으로 연결한다.",
"expected_result": "원격 셸이 열린다.",
}
],
"cleanup": [],
}
],
}
def test_valid_command_plan_passes(self):
validate_command_plan(self.valid_plan(), analysis=self.analysis)
def test_command_plan_requires_supported_mode(self):
plan = self.valid_plan()
plan["mode"] = "clever"
with self.assertRaisesRegex(ValueError, "mode"):
validate_command_plan(plan, analysis=self.analysis)
def test_command_plan_rejects_duplicate_group_ids(self):
plan = self.valid_plan()
plan["command_groups"].append(dict(plan["command_groups"][0]))
with self.assertRaisesRegex(ValueError, "duplicate command group id"):
validate_command_plan(plan, analysis=self.analysis)
def test_command_plan_requires_execution_context(self):
plan = self.valid_plan()
del plan["command_groups"][0]["execution_context"]
with self.assertRaisesRegex(ValueError, "execution_context"):
validate_command_plan(plan, analysis=self.analysis)
def test_patch_set_requires_schema_version_and_known_hashes(self):
patch = {
"schema_version": "1.0",
"source_sha256": self.analysis["source_sha256"],
"patches": [
{
"block_id": self.block["id"],
"expected_sha256": self.block["source_sha256"],
"replacement_markdown": "```bash\nssh test-server\n```",
}
],
}
validate_command_patch_set(patch, analysis=self.analysis)
bad = dict(patch)
bad.pop("schema_version")
with self.assertRaisesRegex(ValueError, "schema_version"):
validate_command_patch_set(bad, analysis=self.analysis)
class CommandArtifactCliContractTest(unittest.TestCase):
def test_analysis_cli_accepts_document_mode(self):
script = os.path.join(ROOT, "scripts", "check-command-pedagogy.py")
with tempfile.TemporaryDirectory() as d:
record = os.path.join(d, "record.md")
output = os.path.join(d, "analysis.json")
with open(record, "w", encoding="utf-8") as fh:
fh.write('```bash\nsed -i "s/a/$(cat value)/" config\n```\n')
p = subprocess.run(
[sys.executable, script, record, "--mode", "automation", "-o", output],
cwd=ROOT,
capture_output=True,
text=True,
)
self.assertEqual(0, p.returncode, p.stdout + p.stderr)
with open(output, encoding="utf-8") as fh:
result = json.load(fh)
self.assertEqual("automation", result["mode"])
self.assertEqual([], result["findings"])
def test_artifact_validator_cli_checks_plan_against_analysis(self):
validator = os.path.join(ROOT, "scripts", "validate-command-pedagogy-artifact.py")
text = "```bash\nssh user@192.168.1.10\n```\n"
analysis = analyze_commands("section-a", text)
block = analysis["blocks"][0]
plan = {
"schema_version": "1.0",
"section_id": "section-a",
"source_sha256": analysis["source_sha256"],
"mode": "operator",
"command_groups": [
{
"id": "connect",
"block_id": block["id"],
"source_sha256": block["source_sha256"],
"goal": "연결한다.",
"execution_context": {"host": "local", "cwd": "."},
"prerequisites": [],
"steps": [{
"command": "ssh test-server",
"reason": "연결 확인",
"expected_result": "원격 셸",
}],
"cleanup": [],
}
],
}
with tempfile.TemporaryDirectory() as d:
analysis_path = os.path.join(d, "analysis.json")
plan_path = os.path.join(d, "plan.json")
with open(analysis_path, "w", encoding="utf-8") as fh:
json.dump(analysis, fh)
with open(plan_path, "w", encoding="utf-8") as fh:
json.dump(plan, fh)
p = subprocess.run(
[sys.executable, validator, "plan", plan_path, "--analysis", analysis_path],
cwd=ROOT,
capture_output=True,
text=True,
)
self.assertEqual(0, p.returncode, p.stdout + p.stderr)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,143 @@
"""command-pedagogy가 실제 파이프라인 문서와 프롬프트에 연결되어 있는지 본다."""
from __future__ import annotations
import json
import os
import unittest
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
PIPELINE = os.path.join(ROOT, ".agents", "skills", "running-tech-log-pipeline")
def read(*parts: str) -> str:
with open(os.path.join(PIPELINE, *parts), encoding="utf-8") as fh:
return fh.read()
class CommandPedagogyPipelineContractTest(unittest.TestCase):
def test_pipeline_skill_routes_command_work_to_separate_agents(self):
text = read("SKILL.md")
for token in (
"check-command-pedagogy.py",
"command-pedagogy-planner",
"command-pedagogy-editor",
"command-pedagogy-reviewer",
"qualityReviews.commandPedagogy",
"qualityReviews.technicalEvidence",
):
self.assertIn(token, text)
self.assertIn("shell/CLI block이 없으면", text)
self.assertIn("finding이 없으면 planner/editor", text)
def test_stage_contract_freezes_bounded_command_repair(self):
text = read("references", "stage-contracts.md")
for token in (
"initialAnalysis",
"finalAnalysis",
"CommandPatchSet",
"apply-command-pedagogy-patch.py",
"majorFindings",
"command-pedagogy-reviewer",
"fact-reviewer",
):
self.assertIn(token, text)
self.assertIn("command block 밖의 산문", text)
def test_subagent_prompts_include_the_command_lane_and_final_reviews(self):
text = read("references", "subagent-prompts.md")
for call in (
'Agent(subagent_type="command-pedagogy-planner"',
'Agent(subagent_type="command-pedagogy-editor"',
'Agent(subagent_type="command-pedagogy-reviewer"',
'Agent(subagent_type="fact-reviewer"',
):
self.assertIn(call, text)
self.assertIn("CommandPatchSet", text)
self.assertIn("initial command analysis", text)
self.assertIn("final command analysis", text)
self.assertIn("validate-command-pedagogy-artifact.py", text)
self.assertIn("sha256", text.lower())
class FirstClassCommandContractTest(unittest.TestCase):
def test_command_plan_and_patch_set_have_repository_schemas(self):
schema_dir = os.path.join(PIPELINE, "schemas")
expectations = {
"command-plan.schema.json": {"section_id", "source_sha256", "mode", "command_groups"},
"command-patch-set.schema.json": {"source_sha256", "patches"},
}
for name, required in expectations.items():
path = os.path.join(schema_dir, name)
self.assertTrue(os.path.isfile(path), path)
with open(path, encoding="utf-8") as fh:
schema = json.load(fh)
self.assertEqual("https://json-schema.org/draft/2020-12/schema", schema["$schema"])
self.assertTrue(required.issubset(set(schema["required"])))
def test_claude_command_agents_are_thin_adapters_to_canonical_contracts(self):
pairs = {
"command-pedagogy-planner.md": "contracts/command-pedagogy-planner.md",
"command-pedagogy-editor.md": "contracts/command-pedagogy-editor.md",
"command-pedagogy-reviewer.md": "contracts/command-pedagogy-reviewer.md",
}
for adapter_name, canonical_rel in pairs.items():
canonical = os.path.join(PIPELINE, canonical_rel)
adapter = os.path.join(ROOT, ".claude", "agents", adapter_name)
self.assertTrue(os.path.isfile(canonical), canonical)
with open(adapter, encoding="utf-8") as fh:
adapter_text = fh.read()
self.assertIn(
f".agents/skills/running-tech-log-pipeline/{canonical_rel}",
adapter_text,
)
substantive = [line for line in adapter_text.splitlines() if line.strip()]
self.assertLessEqual(len(substantive), 14, adapter_name)
def test_command_authoring_policy_declares_modes_without_blanket_bans(self):
policy = os.path.join(PIPELINE, "policies", "command-authoring.yaml")
self.assertTrue(os.path.isfile(policy), policy)
with open(policy, encoding="utf-8") as fh:
content = fh.read()
for mode in ("tutorial", "operator", "diagnostic", "automation", "reference"):
self.assertIn(f" {mode}:", content)
self.assertIn("blanket_ban: false", content)
self.assertIn("validation_cleanup_same_chain", content)
def test_pipeline_structural_gate_requires_command_pedagogy_assets(self):
verifier = os.path.join(ROOT, "scripts", "verify-pipeline.py")
versions = os.path.join(ROOT, "scripts", "skill-versions.py")
with open(verifier, encoding="utf-8") as fh:
verifier_text = fh.read()
with open(versions, encoding="utf-8") as fh:
versions_text = fh.read()
required = (
".agents/skills/writing-practitioner-guides/references/command-pedagogy.md",
".agents/skills/running-tech-log-pipeline/policies/command-authoring.yaml",
".agents/skills/running-tech-log-pipeline/schemas/command-plan.schema.json",
".agents/skills/running-tech-log-pipeline/schemas/command-patch-set.schema.json",
".agents/skills/running-tech-log-pipeline/contracts/command-pedagogy-planner.md",
".agents/skills/running-tech-log-pipeline/contracts/command-pedagogy-editor.md",
".agents/skills/running-tech-log-pipeline/contracts/command-pedagogy-reviewer.md",
".claude/agents/command-pedagogy-planner.md",
".claude/agents/command-pedagogy-editor.md",
".claude/agents/command-pedagogy-reviewer.md",
"scripts/command_pedagogy.py",
"scripts/check-command-pedagogy.py",
"scripts/apply-command-pedagogy-patch.py",
"scripts/validate-command-pedagogy-artifact.py",
)
for rel in required:
self.assertIn(f'"{rel}"', verifier_text, rel)
for rel in (
"scripts/command_pedagogy.py",
"scripts/check-command-pedagogy.py",
"scripts/apply-command-pedagogy-patch.py",
"scripts/validate-command-pedagogy-artifact.py",
):
self.assertIn(f'"{rel}"', versions_text, rel)
if __name__ == "__main__":
unittest.main()
@@ -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()
+73
View File
@@ -3,6 +3,7 @@
이 검사기의 값은 「통과시키는 것」이 아니라 「안 지킨 것을 잡는 것」이라, 시험도 전부
위반을 넣어 걸리는지 보는 모양이다.
"""
import importlib.util
import json
import os
import subprocess
@@ -13,6 +14,9 @@ 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")
LEDGER = os.path.join(ROOT, "runs", "keycloak", "2026-09-07-2215", "run.json")
_spec = importlib.util.spec_from_file_location("verify_pipeline_run", SCRIPT)
vpr = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(vpr)
def run(path, *args):
@@ -125,6 +129,75 @@ class LedgerRules(unittest.TestCase):
self.assertIn("곁증명의 스킬 영수증이 그 스킬의 문장이 아니다", out)
class EvidenceGateV5(unittest.TestCase):
def check(self, gate):
rep = vpr.Report("fixture")
run_ = {"schemaVersion": vpr.EVIDENCE_RECONCILIATION_SCHEMA, "project": "demo"}
found = vpr._evidence_gate_v5(rep, run_, "S3", [gate], "S3 fixture")
return rep, found
def test_repo_flag_is_required(self):
rep, _ = self.check({
"semanticId": "evidence-repo",
"cmd": "node check_evidence.mjs demo",
"exit": 0,
"status": "PASS",
})
self.assertTrue(rep.error_count)
self.assertIn("live source evidence gate에서 --repo가 빠졌다", rep.errors)
def test_unverifiable_preserves_exit_three_and_reason(self):
rep, gate = self.check({
"semanticId": "evidence-repo",
"cmd": "node check_evidence.mjs demo --repo",
"exit": 3,
"status": "UNVERIFIABLE",
"reason": "source repository unavailable on current machine",
"acceptedByProjectReview": True,
})
self.assertEqual(0, rep.error_count, rep.errors)
self.assertIsNotNone(gate)
self.assertEqual(["S3"], rep.facts["live source UNVERIFIABLE"])
def test_unverifiable_cannot_hide_an_arbitrary_exit(self):
rep, _ = self.check({
"semanticId": "evidence-repo",
"cmd": "node check_evidence.mjs demo --repo",
"exit": 1,
"status": "UNVERIFIABLE",
"reason": "something failed",
"acceptedByProjectReview": True,
})
self.assertIn("UNVERIFIABLE evidence gate는 실제 대조 불가 exit 3이어야 한다", rep.errors)
def test_unverifiable_needs_project_review_acceptance(self):
rep, _ = self.check({
"semanticId": "evidence-repo",
"cmd": "node check_evidence.mjs demo --repo",
"exit": 3,
"status": "UNVERIFIABLE",
"reason": "source repository unavailable on current machine",
})
self.assertIn("UNVERIFIABLE evidence gate가 프로젝트 리뷰에서 수용되지 않았다", rep.errors)
def test_pass_requires_real_repo_gate_and_zero_exit(self):
rep, _ = self.check({
"semanticId": "evidence-repo",
"cmd": "node check_evidence.mjs demo --repo",
"exit": 0,
"status": "PASS",
})
self.assertEqual(0, rep.error_count, rep.errors)
def test_semantic_gate_id_is_required(self):
rep = vpr.Report("fixture")
run_ = {"schemaVersion": vpr.EVIDENCE_RECONCILIATION_SCHEMA, "project": "demo"}
vpr._evidence_gate_v5(rep, run_, "S3", [{
"cmd": "node check_evidence.mjs demo --repo", "exit": 0, "status": "PASS"
}], "S3 fixture")
self.assertIn("필수 evidence semantic gate가 정확히 하나가 아니다", rep.errors)
class Init(unittest.TestCase):
def test_틀에서_런을_연다(self):
with tempfile.TemporaryDirectory() as d:
@@ -0,0 +1,145 @@
from __future__ import annotations
import importlib.util
import json
import os
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
SCRIPT = ROOT / "scripts" / "verify-project-layout.py"
spec = importlib.util.spec_from_file_location("verify_project_layout", SCRIPT)
layout = importlib.util.module_from_spec(spec)
spec.loader.exec_module(layout)
def _section(line: int, level: int, title: str, start: int, end: int, text: str) -> dict:
return {
"heading": {"line": line, "level": level, "text": title},
"start_line": start,
"end_line": end,
"text": text,
}
class TechVizContextFallbackTest(unittest.TestCase):
def _check(self, document: str, context: dict) -> bool | None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
doc = root / "document.md"
ctx = root / "context.json"
doc.write_text(document, encoding="utf-8")
ctx.write_text(json.dumps(context, ensure_ascii=False), encoding="utf-8")
return layout._context_snapshot_matches(str(doc), str(ctx))
def test_parent_predecessor_is_only_the_preamble_before_current_child(self):
document = "\n".join([
"# Root",
"root",
"## Parent",
"preamble",
"### Current",
"current",
"### Next",
"next",
"## Tail",
"tail",
])
context = {
"anchor": {"kind": "heading", "value": "Current", "line": 5},
"previous_section": _section(
3, 2, "Parent", 3, 4, "## Parent\npreamble"
),
"current_section": _section(
5, 3, "Current", 5, 6, "### Current\ncurrent"
),
"next_section": _section(
7, 3, "Next", 7, 8, "### Next\nnext"
),
}
self.assertTrue(self._check(document, context))
changed = document.replace("preamble", "changed preamble")
self.assertFalse(self._check(changed, context))
def test_fenced_hash_lines_do_not_end_neighbor_section(self):
document = "\n".join([
"# Root",
"## Current",
"current",
"## Data",
"intro",
"~~~text",
"# not a heading",
"## also not a heading",
"~~~",
"tail",
"## End",
"end",
])
context = {
"anchor": {"kind": "heading", "value": "Current", "line": 2},
"previous_section": _section(
1, 1, "Root", 1, 1, "# Root"
),
"current_section": _section(
2, 2, "Current", 2, 3, "## Current\ncurrent"
),
"next_section": _section(
4, 2, "Data", 4, 10,
"## Data\nintro\n~~~text\n# not a heading\n"
"## also not a heading\n~~~\ntail",
),
}
self.assertTrue(self._check(document, context))
def test_legacy_context_without_anchor_uses_current_heading(self):
document = "\n".join([
"# Root",
"## Previous",
"previous",
"## Current",
"current",
"## Next",
"next",
])
context = {
"previous_section": {
"heading": {"text": "Previous"},
"text": "## Previous\nprevious",
},
"current_section": {
"heading": {"text": "Current"},
"text": "## Current\ncurrent",
},
"next_section": {
"heading": {"text": "Next"},
"text": "## Next\nnext",
},
}
self.assertTrue(self._check(document, context))
def test_keycloak_session_store_seven_fresh_contexts_match_without_techviz(self):
ids = [
"a1-transport-vs-discovery",
"d2-upgrade-direction",
"lab-topology",
"measurement-control",
"observation-points",
"open-questions-answered",
"wrong-predictions",
]
document = ROOT / "docs" / "keycloak-session-store" / "final" / "document.md"
base = ROOT / "docs" / "keycloak-session-store" / "final" / ".techviz"
for diagram_id in ids:
with self.subTest(diagram_id=diagram_id):
context = base / diagram_id / "context.json"
self.assertTrue(
layout._context_snapshot_matches(str(document), str(context))
)
if __name__ == "__main__":
unittest.main()
+36 -2
View File
@@ -85,6 +85,39 @@ class LedgerTest(unittest.TestCase):
p = _cli("gate", self.led, "--stage", "S3", "--cmd", "x")
self.assertNotEqual(0, p.returncode)
def test_unverifiable_gate_keeps_the_real_failure_receipt(self):
p = _cli(
"gate", self.led, "--stage", "S3",
"--cmd", "node check_evidence.mjs demo --repo", "--exit", "3",
"--semantic-id", "evidence-repo", "--status", "UNVERIFIABLE",
"--reason", "source repository unavailable on current machine",
"--accepted-by-project-review",
)
self.assertEqual(0, p.returncode, p.stderr)
gate = self._stage("S3")["gates"][0]
self.assertEqual("evidence-repo", gate["semanticId"])
self.assertEqual("UNVERIFIABLE", gate["status"])
self.assertEqual(3, gate["exit"])
self.assertTrue(gate["acceptedByProjectReview"])
def test_unverifiable_gate_needs_a_reason(self):
p = _cli(
"gate", self.led, "--stage", "S3", "--cmd", "x", "--exit", "3",
"--status", "UNVERIFIABLE",
)
self.assertEqual(2, p.returncode)
self.assertIn("--reason", p.stderr)
def test_gate_status_cannot_lie_about_the_exit_code(self):
self.assertEqual(2, _cli(
"gate", self.led, "--stage", "S3", "--cmd", "x", "--exit", "3",
"--status", "PASS",
).returncode)
self.assertEqual(2, _cli(
"gate", self.led, "--stage", "S3", "--cmd", "x", "--exit", "0",
"--status", "FAIL",
).returncode)
# ── 끊겨도 이어진다 ──────────────────────────────────────────────
def test_the_file_is_never_half_written(self):
"""쓰는 도중에 죽여도 읽는 쪽은 이전 판이나 다음 판 중 하나를 본다."""
@@ -286,7 +319,8 @@ class EchoAgainstSkillHistory(unittest.TestCase):
"""S3 의 영수증만 갈아 끼운, 그 밖에는 흠이 없는 원장."""
run = json.load(open(vpr.TEMPLATE, encoding="utf-8"))
run.update({"runId": "2026-01-01-0000", "project": "demo",
"record": "CLAUDE.md", "startedAt": "2026-01-01T00:00:00+09:00"})
"record": "CLAUDE.md", "startedAt": "2026-01-01T00:00:00+09:00",
"schemaVersion": vpr.AGENT_RUNBY_SCHEMA})
for st in run["stages"]:
spec = vpr.STAGES[st["id"]]
if not carry_field:
@@ -469,7 +503,7 @@ class RunByNamesAManagedAgent(unittest.TestCase):
p = _cli("open", led, "--project", "demo", "--record", "docs/demo/x.md")
self.assertEqual(0, p.returncode, p.stderr)
run = json.load(open(led, encoding="utf-8"))
self.assertEqual(vpr.AGENT_RUNBY_SCHEMA, run["schemaVersion"])
self.assertEqual(vpr.CURRENT_RUN_SCHEMA, run["schemaVersion"])
for st in run["stages"]:
self.assertEqual(vpr.STAGES[st["id"]]["agent"], st["runBy"])
+129
View File
@@ -182,6 +182,49 @@ class ContractTest(unittest.TestCase):
with Fixture(mutate(source=["analysis/05-persistence.md §3.5"])):
self.assertIn("근거가 SSOT 밖에만 있다", verifier.verify("fixture").warns)
def test_numbered_ssot_anchor_is_resolved_against_heading(self):
index = copy.deepcopy(INDEX)
index["candidateScope"]["sections"] = ["§3"]
index["topics"]["session-custody"]["kinds"]["case"][0]["source"] = [
"final/document.md#§3.1"
]
index["topics"]["session-custody"]["kinds"]["concept"][0]["source"] = [
"final/document.md#§3.1"
]
with Fixture(index) as fx:
ssot = os.path.join(fx.base, "final/document.md")
open(ssot, "w", encoding="utf-8").write(
"# fixture\n\n## 3. 세션\n\n### 3.1 교환\n\n내용\n"
)
data = fx.read()
data["ssotSha256"] = hashlib.sha256(open(ssot, "rb").read()).hexdigest()
fx.write(data)
report = verifier.verify("fixture")
self.assertNotIn("앵커가 검사 가능한 절 제목/번호 형식이 아니다", report.warns)
self.assertNotIn("SSOT 에 없는 번호 절을 가리키는 앵커", report.errors)
def test_missing_numbered_ssot_anchor_is_an_error(self):
index = copy.deepcopy(INDEX)
index["candidateScope"]["sections"] = ["§3"]
index["topics"]["session-custody"]["kinds"]["case"][0]["source"] = [
"final/document.md#§3.9"
]
index["topics"]["session-custody"]["kinds"]["concept"][0]["source"] = [
"final/document.md#§3.1"
]
with Fixture(index) as fx:
ssot = os.path.join(fx.base, "final/document.md")
open(ssot, "w", encoding="utf-8").write(
"# fixture\n\n## 3. 세션\n\n### 3.1 교환\n\n내용\n"
)
data = fx.read()
data["ssotSha256"] = hashlib.sha256(open(ssot, "rb").read()).hexdigest()
fx.write(data)
self.assertIn(
"SSOT 에 없는 번호 절을 가리키는 앵커",
verifier.verify("fixture").errors,
)
def test_readiness_is_not_publication(self):
with Fixture(mutate(readiness="NEEDS_EVIDENCE")):
self.assertIn("글을 쓰면 안 되는 readiness 인데 기록이 있다",
@@ -191,6 +234,39 @@ class ContractTest(unittest.TestCase):
with Fixture(mutate(readiness="REJECTED")):
self.assertIn("readiness 값이 계약에 없다", verifier.verify("fixture").errors)
def test_resolved_question_is_a_valid_written_state(self):
index = copy.deepcopy(INDEX)
question = {
"title": "이미 답을 얻은 질문",
"kind": "question",
"slug": "resolved-question",
"readiness": "RESOLVED",
"source": ["final/document.md#fixture"],
"known": "직접 관측으로 답을 얻었다",
"unknown": "후속 조건만 남았다",
"next-verification": "후속 조건을 별도 검증한다",
"decision-criterion": "직접 관측으로 닫혔다",
"relations": ["case:session-split-across-nodes"],
}
index["topics"]["session-custody"]["kinds"]["question"] = [question]
index["candidates"].append({
"id": "F004",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "question:resolved-question",
})
with Fixture(index) as fx:
folder = os.path.join(fx.studio, "session-custody/question")
os.makedirs(folder, exist_ok=True)
open(os.path.join(folder, "question-resolved.md"), "w", encoding="utf-8").write(
"---\nkind: QUESTION\nslug: resolved-question\n"
"title: 이미 답을 얻은 질문\ntopic: session-custody\n"
"project: fixture\nquestionStatus: RESOLVED\n---\n"
)
report = verifier.verify("fixture")
self.assertNotIn("QUESTION readiness 는 OPEN 또는 RESOLVED 다", report.errors)
self.assertNotIn("글을 쓰면 안 되는 readiness 인데 기록이 있다", report.errors)
def test_record_outside_the_contract_is_an_error(self):
with Fixture() as fx:
os.makedirs(os.path.join(fx.studio, "orphan-topic/concept"))
@@ -421,6 +497,44 @@ class LayoutTest(unittest.TestCase):
self.assertIn("기록이 가리키는 그림에 techviz 정본이 없다",
layout.verify("fixture").warns)
def test_context_snapshot_detects_ssot_drift_without_techviz_tool(self):
with Fixture() as fx:
fx.diagram("context-map", cited=True)
document = os.path.join(fx.base, "final/document.md")
open(document, "w", encoding="utf-8").write(
"# fixture\n\n## 1. 이전\n\n그대로\n\n"
"## 2. 대상\n\n그림 근거\n\n"
"## 3. 다음\n\n바뀐 내용\n"
)
source = os.path.join(fx.base, "final/.techviz/context-map")
with open(os.path.join(source, "spec.json"), "w", encoding="utf-8") as fh:
json.dump({"source_context": {
"document": "document.md",
"document_sha256": "1" * 64,
}}, fh)
with open(os.path.join(source, "context.json"), "w", encoding="utf-8") as fh:
json.dump({
"previous_section": {
"heading": {"text": "1. 이전"},
"text": "## 1. 이전\n\n그대로\n",
},
"current_section": {
"heading": {"text": "2. 대상"},
"text": "## 2. 대상\n\n그림 근거\n",
},
"next_section": {
"heading": {"text": "3. 다음"},
"text": "## 3. 다음\n\n예전 내용\n",
},
}, fh, ensure_ascii=False)
original = layout._context_sha
layout._context_sha = lambda _: None
try:
report = layout.verify("fixture")
finally:
layout._context_sha = original
self.assertIn("SSOT 문맥이 바뀐 뒤 그림을 다시 보지 않았다", report.warns)
def test_evidence_folder_outside_the_convention_is_an_error(self):
with Fixture() as fx:
os.makedirs(os.path.join(fx.base, "final/evidence/screenshots"))
@@ -453,6 +567,21 @@ class LayoutTest(unittest.TestCase):
encoding="utf-8").write("원본\n")
self.assertIn("반입 원본이 남아 있다", layout.verify("fixture").warns)
def test_durable_import_snapshot_is_not_transient_source_debt(self):
with Fixture() as fx:
os.makedirs(os.path.join(fx.base, "source/docs"))
open(os.path.join(fx.base, "source/docs/lab.md"), "w",
encoding="utf-8").write("원본\n")
data = fx.read()
data["sourcePolicy"] = {
"mode": "DURABLE_IMPORT_SNAPSHOT",
"reason": "exact commit이 없는 반입 당시 working-tree 바이트를 보존한다",
}
fx.write(data)
report = layout.verify("fixture")
self.assertNotIn("반입 원본이 남아 있다", report.warns)
self.assertIn("durable import snapshot", report.facts.get("source", ""))
if __name__ == "__main__":
unittest.main()