init: technical-visualization-haness 하네스 설계

This commit is contained in:
DongHyeonka
2026-07-24 14:02:50 +09:00
parent 09d7c594da
commit f43e909162
117 changed files with 10150 additions and 1 deletions
+104
View File
@@ -0,0 +1,104 @@
from __future__ import annotations
import json
import tempfile
import unittest
from pathlib import Path
from techviz.cli import main
ROOT = Path(__file__).resolve().parents[1]
class CliTests(unittest.TestCase):
def test_prepare_prompt_lint_render(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
context = root / "context.json"
prompt = root / "prompt.md"
output = root / "out"
self.assertEqual(
main([
"prepare",
str(ROOT / "examples/docs/payment-flow.md"),
"--marker",
"payment-request",
"-o",
str(context),
]),
0,
)
self.assertEqual(main(["prompt", str(context), "-o", str(prompt)]), 0)
self.assertIn("untrusted evidence data", prompt.read_text(encoding="utf-8"))
self.assertEqual(
main([
"lint",
str(ROOT / "examples/work/payment/spec.json"),
"--context",
str(context),
]),
0,
)
self.assertEqual(
main([
"render",
str(ROOT / "examples/work/payment/spec.json"),
"--context",
str(context),
"--formats",
"svg,a11y",
"-o",
str(output),
]),
0,
)
manifest = json.loads((output / "payment-request.manifest.json").read_text(encoding="utf-8"))
self.assertEqual(manifest["spec_id"], "payment-request")
def test_render_blocks_unapproved_assumptions(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
data = json.loads((ROOT / "examples/work/payment/spec.json").read_text(encoding="utf-8"))
data["nodes"][0]["evidence"] = []
data["nodes"][0]["assumption"] = True
spec = root / "assumption.json"
spec.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
blocked_output = root / "blocked"
self.assertEqual(
main([
"render",
str(spec),
"--context",
str(ROOT / "examples/work/payment/context.json"),
"--formats",
"svg",
"-o",
str(blocked_output),
]),
1,
)
self.assertFalse((blocked_output / "payment-request.svg").exists())
approved_output = root / "approved"
self.assertEqual(
main([
"render",
str(spec),
"--context",
str(ROOT / "examples/work/payment/context.json"),
"--formats",
"svg",
"--allow-assumptions",
"-o",
str(approved_output),
]),
0,
)
manifest = json.loads((approved_output / "payment-request.manifest.json").read_text(encoding="utf-8"))
self.assertEqual(manifest["assumption_count"], 1)
self.assertTrue(manifest["assumptions_allowed"])
if __name__ == "__main__":
unittest.main()
+105
View File
@@ -0,0 +1,105 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from techviz.document import build_context, canonicalize_document
class DocumentContextTests(unittest.TestCase):
def test_extracts_current_and_neighbor_sections(self) -> None:
text = """# Title
## Before
before fact
## Target
target fact
<!-- techviz:generate id=target -->
## After
after fact
"""
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "doc.md"
path.write_text(text, encoding="utf-8")
context = build_context(path, marker_id="target")
self.assertEqual(context["current_section"]["heading"]["text"], "Target")
self.assertEqual(context["previous_section"]["heading"]["text"], "Before")
self.assertEqual(context["next_section"]["heading"]["text"], "After")
self.assertIn("target fact", context["numbered_context"])
self.assertEqual(context["line_number_space"], "canonical-source-with-managed-blocks-collapsed")
def test_generated_block_canonicalizes_to_original_marker(self) -> None:
original = """# Title
## Target
fact
<!-- techviz:generate id=target -->
## After
more
"""
rendered = """# Title
## Target
fact
<!-- techviz:begin id=target context-sha256=abc -->
<!-- techviz:generate id=target -->
![alt](target.svg)
<details><summary>Description</summary>Long description</details>
<!-- techviz:end id=target -->
## After
more
"""
self.assertEqual(canonicalize_document(rendered), original)
with tempfile.TemporaryDirectory() as tmp:
original_path = Path(tmp) / "original.md"
rendered_path = Path(tmp) / "rendered.md"
original_path.write_text(original, encoding="utf-8")
rendered_path.write_text(rendered, encoding="utf-8")
original_context = build_context(original_path, marker_id="target")
rendered_context = build_context(rendered_path, marker_id="target")
self.assertEqual(original_context["document_sha256"], rendered_context["document_sha256"])
self.assertEqual(original_context["numbered_context"], rendered_context["numbered_context"])
def test_parent_preamble_is_trimmed_before_nested_target(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
document = Path(tmp) / "nested.md"
document.write_text(
"# System\n\n"
"## Parent\n\n"
"Parent-level context.\n\n"
"### Target\n\n"
"Target details.\n\n"
"<!-- techviz:generate id=nested -->\n\n"
"### Following\n\n"
"Following details.\n",
encoding="utf-8",
)
context = build_context(document, marker_id="nested")
previous = context["previous_section"]
self.assertEqual(previous["heading"]["text"], "Parent")
self.assertLess(previous["end_line"], context["current_section"]["start_line"])
self.assertIn("Parent-level context.", previous["text"])
self.assertNotIn("Target details.", previous["text"])
self.assertEqual(context["next_section"]["heading"]["text"], "Following")
if __name__ == "__main__":
unittest.main()
+40
View File
@@ -0,0 +1,40 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from techviz.insert import build_markdown_block, insert_or_replace
class InsertTests(unittest.TestCase):
def test_insert_is_idempotent(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
document = root / "doc.md"
svg = root / "diagram.svg"
source = root / "diagram.drawio"
spec = root / "spec.json"
document.write_text("# Doc\n\n<!-- techviz:generate id=diagram -->\n", encoding="utf-8")
for path in (svg, source, spec):
path.write_text("x", encoding="utf-8")
block = build_markdown_block(
diagram_id="diagram",
alt="A useful diagram",
long_description="The diagram explains a source-grounded path.",
svg_path=svg,
editable_path=source,
spec_path=spec,
document_path=document,
context_sha256="abc",
)
insert_or_replace(document, "diagram", block)
insert_or_replace(document, "diagram", block)
result = document.read_text(encoding="utf-8")
self.assertEqual(result.count("techviz:begin id=diagram"), 1)
self.assertEqual(result.count("techviz:end id=diagram"), 1)
self.assertEqual(result.count("techviz:generate id=diagram"), 1)
if __name__ == "__main__":
unittest.main()
+41
View File
@@ -0,0 +1,41 @@
from __future__ import annotations
import json
import unittest
from pathlib import Path
from techviz.document import build_context
from techviz.prompt import build_agent_prompt
from techviz.spec import validate_raw_spec
ROOT = Path(__file__).resolve().parents[1]
class PromptTests(unittest.TestCase):
def test_embedded_vizspec_scaffold_is_valid_and_context_bound(self) -> None:
context = build_context(
ROOT / "examples/docs/payment-flow.md",
marker_id="payment-request",
)
prompt = build_agent_prompt(context)
section_start = prompt.index("## VizSpec 1.0 shape")
json_start = prompt.index("{\n", section_start)
json_end = prompt.index("\n\nFor a sequence diagram", json_start)
scaffold = json.loads(prompt[json_start:json_end])
validate_raw_spec(scaffold)
self.assertEqual(
scaffold["source_context"],
{
"document": context["document"],
"document_sha256": context["document_sha256"],
"anchor": context["anchor"],
},
)
self.assertFalse(scaffold["groups"])
self.assertFalse(scaffold["legend"])
if __name__ == "__main__":
unittest.main()
+53
View File
@@ -0,0 +1,53 @@
from __future__ import annotations
import unittest
from techviz.layout import DiagramLayout, EdgePath, NodeBox
from techviz.quality import validate_layout
from techviz.spec import VizSpec
class QualityTests(unittest.TestCase):
def test_detects_edge_passing_through_unrelated_node(self) -> None:
spec = VizSpec.from_dict(
{
"version": "1.0",
"id": "quality-case",
"title": "Quality case",
"question": "Does an edge cross a node?",
"type": "architecture",
"direction": "LR",
"summary": "Synthetic quality test.",
"alt": "Synthetic diagram with three nodes.",
"long_description": "A line from A to C passes through unrelated node B.",
"source_context": {},
"groups": [],
"nodes": [
{"id": "a", "label": "A", "kind": "service", "evidence": [], "assumption": True},
{"id": "b", "label": "B", "kind": "service", "evidence": [], "assumption": True},
{"id": "c", "label": "C", "kind": "service", "evidence": [], "assumption": True}
],
"edges": [
{"id": "a-to-c", "from": "a", "to": "c", "label": "calls", "kind": "request", "evidence": [], "assumption": True}
]
}
)
layout = DiagramLayout(
width=500,
height=240,
nodes={
"a": NodeBox("a", 20, 80, 100, 60, ["A"]),
"b": NodeBox("b", 190, 80, 100, 60, ["B"]),
"c": NodeBox("c", 360, 80, 100, 60, ["C"]),
},
groups={},
edges={
"a-to-c": EdgePath("a-to-c", [(120, 110), (360, 110)], 240, 70)
},
)
issues = validate_layout(spec, layout)
self.assertTrue(any(issue.code == "edge-through-node" for issue in issues))
if __name__ == "__main__":
unittest.main()
+48
View File
@@ -0,0 +1,48 @@
from __future__ import annotations
import json
import tempfile
import unittest
import xml.etree.ElementTree as ET
from pathlib import Path
from techviz.layout import build_layout
from techviz.renderers import render_formats
from techviz.spec import load_spec
ROOT = Path(__file__).resolve().parents[1]
class RendererTests(unittest.TestCase):
def test_all_builtin_outputs_are_parseable(self) -> None:
spec = load_spec(ROOT / "examples/work/payment/spec.json")
layout = build_layout(spec)
with tempfile.TemporaryDirectory() as tmp:
outputs = render_formats(
spec,
layout,
tmp,
["svg", "mermaid", "d2", "dot", "drawio", "excalidraw", "a11y"],
)
by_suffix = {path.suffix: path for path in outputs}
ET.parse(by_suffix[".svg"])
ET.parse(by_suffix[".drawio"])
excalidraw = json.loads(by_suffix[".excalidraw"].read_text(encoding="utf-8"))
self.assertEqual(excalidraw["type"], "excalidraw")
self.assertGreater(len(excalidraw["elements"]), 0)
self.assertIn("flowchart LR", by_suffix[".mmd"].read_text(encoding="utf-8"))
self.assertIn("direction: right", by_suffix[".d2"].read_text(encoding="utf-8"))
self.assertIn("digraph techviz", by_suffix[".dot"].read_text(encoding="utf-8"))
alt_file = next(path for path in outputs if path.name.endswith(".alt.md"))
self.assertIn("## Long description", alt_file.read_text(encoding="utf-8"))
def test_layout_is_deterministic(self) -> None:
spec = load_spec(ROOT / "examples/work/payment/spec.json")
first = build_layout(spec)
second = build_layout(spec)
self.assertEqual(first, second)
if __name__ == "__main__":
unittest.main()
+41
View File
@@ -0,0 +1,41 @@
from __future__ import annotations
import json
import tempfile
import unittest
from pathlib import Path
from techviz.spec import load_spec
ROOT = Path(__file__).resolve().parents[1]
class SpecParsingTests(unittest.TestCase):
def setUp(self) -> None:
self.data = json.loads((ROOT / "examples/work/payment/spec.json").read_text(encoding="utf-8"))
def _write(self, data: dict) -> Path:
self.temp = tempfile.TemporaryDirectory()
path = Path(self.temp.name) / "spec.json"
path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
return path
def tearDown(self) -> None:
temp = getattr(self, "temp", None)
if temp is not None:
temp.cleanup()
def test_boolean_fields_are_not_coerced_from_strings(self) -> None:
self.data["nodes"][0]["assumption"] = "false"
with self.assertRaisesRegex(ValueError, "expected boolean"):
load_spec(self._write(self.data))
def test_unknown_model_output_fields_are_rejected(self) -> None:
self.data["nodes"][0]["visual_magic"] = "glow"
with self.assertRaisesRegex(ValueError, "unknown field"):
load_spec(self._write(self.data))
if __name__ == "__main__":
unittest.main()
+77
View File
@@ -0,0 +1,77 @@
from __future__ import annotations
import json
import unittest
from copy import deepcopy
from pathlib import Path
from techviz.spec import VizSpec
from techviz.validate import has_errors, validate_spec
ROOT = Path(__file__).resolve().parents[1]
class ValidationTests(unittest.TestCase):
def setUp(self) -> None:
self.spec_data = json.loads((ROOT / "examples/work/payment/spec.json").read_text(encoding="utf-8"))
self.context = json.loads((ROOT / "examples/work/payment/context.json").read_text(encoding="utf-8"))
def test_example_is_clean(self) -> None:
issues = validate_spec(VizSpec.from_dict(self.spec_data), self.context)
self.assertFalse(has_errors(issues), issues)
self.assertEqual(issues, [])
def test_ungrounded_node_is_error(self) -> None:
data = deepcopy(self.spec_data)
data["nodes"][0]["evidence"] = []
issues = validate_spec(VizSpec.from_dict(data), self.context)
self.assertTrue(any(issue.code == "ungrounded-element" and issue.path == "nodes[0]" for issue in issues))
def test_boundary_requires_evidence(self) -> None:
data = deepcopy(self.spec_data)
data["groups"] = [
{
"id": "trust-boundary",
"label": "Trust boundary",
"kind": "trust",
"evidence": [],
"assumption": False
}
]
data["nodes"][1]["group"] = "trust-boundary"
issues = validate_spec(VizSpec.from_dict(data), self.context)
self.assertTrue(any(issue.code == "ungrounded-element" and issue.path == "groups[0]" for issue in issues))
def test_explicit_assumption_is_warning_not_grounding_error(self) -> None:
data = deepcopy(self.spec_data)
data["nodes"][0]["evidence"] = []
data["nodes"][0]["assumption"] = True
issues = validate_spec(VizSpec.from_dict(data), self.context)
self.assertFalse(any(issue.code == "ungrounded-element" and issue.path == "nodes[0]" for issue in issues))
self.assertTrue(any(issue.code == "explicit-assumption" for issue in issues))
def test_assumption_cannot_also_claim_evidence(self) -> None:
data = deepcopy(self.spec_data)
data["nodes"][0]["assumption"] = True
issues = validate_spec(VizSpec.from_dict(data), self.context)
self.assertTrue(any(issue.code == "assumption-with-evidence" for issue in issues))
def test_indirect_group_cycle_is_error(self) -> None:
data = deepcopy(self.spec_data)
data["groups"] = [
{
"id": "a", "label": "A", "kind": "system", "parent": "b",
"evidence": [{"start_line": 7, "end_line": 7}], "assumption": False,
},
{
"id": "b", "label": "B", "kind": "system", "parent": "a",
"evidence": [{"start_line": 7, "end_line": 7}], "assumption": False,
},
]
issues = validate_spec(VizSpec.from_dict(data), self.context)
self.assertTrue(any(issue.code == "recursive-group" for issue in issues))
if __name__ == "__main__":
unittest.main()