init: technical-visualization-haness 하네스 설계
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import difflib
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from techviz import __version__
|
||||
from techviz.document import build_context
|
||||
from techviz.layout import build_layout
|
||||
from techviz.prompt import build_agent_prompt
|
||||
from techviz.renderers import render_formats
|
||||
from techviz.spec import load_spec, stable_hash
|
||||
from techviz.validate import has_errors, validate_spec
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DOCUMENT = ROOT / "examples/docs/payment-flow.md"
|
||||
CONTEXT = ROOT / "examples/work/payment/context.json"
|
||||
PROMPT = ROOT / "examples/work/payment/prompt.md"
|
||||
SPEC = ROOT / "examples/work/payment/spec.json"
|
||||
ASSETS = ROOT / "examples/assets"
|
||||
FORMATS = ["svg", "mermaid", "d2", "dot", "drawio", "excalidraw", "a11y"]
|
||||
|
||||
|
||||
def _manifest(spec: object, files: list[Path]) -> str:
|
||||
payload = {
|
||||
"harness_version": __version__,
|
||||
"spec_id": spec.id,
|
||||
"spec_version": spec.version,
|
||||
"spec_sha256": stable_hash([json.dumps(spec.as_dict(), ensure_ascii=False, sort_keys=True)]),
|
||||
"source_context": spec.source_context,
|
||||
"outputs": [path.name for path in files],
|
||||
"lint_issue_count": 0,
|
||||
"assumption_count": 0,
|
||||
"assumptions_allowed": False,
|
||||
}
|
||||
return json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
|
||||
|
||||
|
||||
def _diff(label: str, expected: str, actual: str) -> str:
|
||||
return "".join(
|
||||
difflib.unified_diff(
|
||||
expected.splitlines(keepends=True),
|
||||
actual.splitlines(keepends=True),
|
||||
fromfile=f"committed/{label}",
|
||||
tofile=f"generated/{label}",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Verify committed example artifacts are reproducible.")
|
||||
parser.add_argument("--update", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Keep the path stored in context relative and portable.
|
||||
relative_document = DOCUMENT.relative_to(ROOT)
|
||||
generated_context = build_context(relative_document, marker_id="payment-request")
|
||||
context_text = json.dumps(generated_context, ensure_ascii=False, indent=2) + "\n"
|
||||
|
||||
spec = load_spec(SPEC)
|
||||
issues = validate_spec(spec, generated_context)
|
||||
if has_errors(issues):
|
||||
for issue in issues:
|
||||
print(issue.as_dict())
|
||||
return 1
|
||||
|
||||
prompt_text = build_agent_prompt(generated_context)
|
||||
|
||||
differences: list[str] = []
|
||||
if args.update:
|
||||
CONTEXT.write_text(context_text, encoding="utf-8")
|
||||
PROMPT.write_text(prompt_text, encoding="utf-8")
|
||||
else:
|
||||
committed_context = CONTEXT.read_text(encoding="utf-8")
|
||||
if committed_context != context_text:
|
||||
differences.append(_diff(str(CONTEXT.relative_to(ROOT)), committed_context, context_text))
|
||||
committed_prompt = PROMPT.read_text(encoding="utf-8")
|
||||
if committed_prompt != prompt_text:
|
||||
differences.append(_diff(str(PROMPT.relative_to(ROOT)), committed_prompt, prompt_text))
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
temp_dir = Path(tmp)
|
||||
files = render_formats(spec, build_layout(spec), temp_dir, FORMATS)
|
||||
manifest_path = temp_dir / f"{spec.id}.manifest.json"
|
||||
manifest_path.write_text(_manifest(spec, files), encoding="utf-8")
|
||||
files.append(manifest_path)
|
||||
|
||||
for generated in files:
|
||||
committed = ASSETS / generated.name
|
||||
if args.update:
|
||||
committed.write_bytes(generated.read_bytes())
|
||||
continue
|
||||
expected = committed.read_text(encoding="utf-8")
|
||||
actual = generated.read_text(encoding="utf-8")
|
||||
if expected != actual:
|
||||
differences.append(_diff(str(committed.relative_to(ROOT)), expected, actual))
|
||||
|
||||
if differences:
|
||||
print("\n".join(differences))
|
||||
print("Generated artifacts differ. Run: PYTHONPATH=src python scripts/check_generated.py --update")
|
||||
return 1
|
||||
print("PASS committed context and diagram artifacts are reproducible")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SOURCE = ROOT / "skills" / "technical-visualizer"
|
||||
TARGETS = [
|
||||
ROOT / ".agents" / "skills" / "technical-visualizer",
|
||||
ROOT / ".claude" / "skills" / "technical-visualizer",
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
for target in TARGETS:
|
||||
if target.exists():
|
||||
shutil.rmtree(target)
|
||||
shutil.copytree(SOURCE, target)
|
||||
print(f"SYNCED {target.relative_to(ROOT)}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user