설계 개편

This commit is contained in:
DongHyeonka
2026-07-24 16:31:12 +09:00
parent f43e909162
commit 8daa568746
70 changed files with 6554 additions and 956 deletions
+43
View File
@@ -0,0 +1,43 @@
from __future__ import annotations
import json
import tempfile
import unittest
from copy import deepcopy
from pathlib import Path
from techviz.batch_audit import audit_batch, discover_specs
ROOT = Path(__file__).resolve().parents[1]
class BatchAuditTests(unittest.TestCase):
def test_runtime_profiles_do_not_collapse(self) -> None:
paths = discover_specs(ROOT / "examples/runtime-profiles")
audits, findings = audit_batch(paths)
self.assertEqual(len(audits), 10)
self.assertFalse(any(item.code == "batch-template-collapse" for item in findings), findings)
def test_repeated_template_is_rejected(self) -> None:
source = json.loads(
(ROOT / "examples/runtime-profiles/10-comparison/spec.json").read_text(encoding="utf-8")
)
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
for index in range(6):
data = deepcopy(source)
data["id"] = f"copy-{index}"
folder = root / f"item-{index}"
folder.mkdir()
(folder / "spec.json").write_text(
json.dumps(data, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
audits, findings = audit_batch(discover_specs(root))
self.assertEqual(len(audits), 6)
self.assertTrue(any(item.code == "batch-template-collapse" and item.severity == "error" for item in findings), findings)
if __name__ == "__main__":
unittest.main()
+4 -1
View File
@@ -30,7 +30,10 @@ class CliTests(unittest.TestCase):
0,
)
self.assertEqual(main(["prompt", str(context), "-o", str(prompt)]), 0)
self.assertIn("untrusted evidence data", prompt.read_text(encoding="utf-8"))
prompt_text = prompt.read_text(encoding="utf-8")
self.assertIn("untrusted evidence data", prompt_text)
self.assertIn("Automatically selected reference cases", prompt_text)
self.assertEqual(main(["references", str(context), "--limit", "2"]), 0)
self.assertEqual(
main([
"lint",
+84
View File
@@ -0,0 +1,84 @@
from __future__ import annotations
import json
import unittest
from copy import deepcopy
from pathlib import Path
from techviz.layout import build_layout
from techviz.renderers.svg import render_svg
from techviz.spec import VizSpec
from techviz.validate import has_errors, validate_spec
ROOT = Path(__file__).resolve().parents[1]
class CompositionTests(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_disconnected_cards_are_rejected(self) -> None:
data = deepcopy(self.spec_data)
data["nodes"] = data["nodes"][:3]
data["edges"] = []
data["composition"] = {
"profile": "component-flow",
"diagram_only": True,
"reference_ids": ["payment-event-flow"],
"rationale": "Synthetic disconnected case.",
}
issues = validate_spec(VizSpec.from_dict(data), self.context)
self.assertTrue(any(issue.code == "missing-central-relation" for issue in issues), issues)
def test_comparison_requires_comparable_details(self) -> None:
data = deepcopy(self.spec_data)
data["type"] = "concept"
data["nodes"] = data["nodes"][:2]
data["edges"] = []
data["composition"] = {
"profile": "comparison",
"diagram_only": True,
"reference_ids": ["contract-comparison"],
"rationale": "Compare two explicit contracts.",
}
for node in data["nodes"]:
node["role"] = "contract"
node["details"] = ["responsibility", "lifecycle"]
issues = validate_spec(VizSpec.from_dict(data))
self.assertFalse(has_errors(issues), issues)
del data["nodes"][0]["details"]
issues = validate_spec(VizSpec.from_dict(data))
self.assertTrue(any(issue.code == "comparison-missing-details" for issue in issues), issues)
def test_svg_is_diagram_only(self) -> None:
spec = VizSpec.from_dict(self.spec_data)
svg = render_svg(spec, build_layout(spec))
self.assertNotIn('class="diagram-title"', svg)
self.assertNotIn('class="diagram-question"', svg)
self.assertNotIn('class="footer"', svg)
self.assertNotIn("Generated from grounded VizSpec", svg)
self.assertNotIn("<filter", svg)
self.assertNotIn("<linearGradient", svg)
self.assertIn("<title id=", svg)
self.assertIn("<desc id=", svg)
def test_reference_profile_mismatch_is_rejected(self) -> None:
data = deepcopy(self.spec_data)
data["composition"]["profile"] = "ports-adapters"
data["composition"]["reference_ids"] = ["payment-event-flow"]
issues = validate_spec(VizSpec.from_dict(data), self.context)
self.assertTrue(any(issue.code == "reference-profile-mismatch" for issue in issues), issues)
def test_context_rejects_unselected_profile(self) -> None:
data = deepcopy(self.spec_data)
data["composition"]["profile"] = "ports-adapters"
data["composition"]["reference_ids"] = ["order-ports-adapters"]
issues = validate_spec(VizSpec.from_dict(data), self.context)
self.assertTrue(any(issue.code == "profile-not-supported-by-context" for issue in issues), issues)
if __name__ == "__main__":
unittest.main()
+16 -2
View File
@@ -6,6 +6,7 @@ from pathlib import Path
from techviz.document import build_context
from techviz.prompt import build_agent_prompt
from techviz.reference_catalog import select_reference_cases
from techviz.spec import validate_raw_spec
@@ -19,12 +20,15 @@ class PromptTests(unittest.TestCase):
marker_id="payment-request",
)
prompt = build_agent_prompt(context)
section_start = prompt.index("## VizSpec 1.0 shape")
section_start = prompt.index("## VizSpec 1.1 shape")
json_start = prompt.index("{\n", section_start)
json_end = prompt.index("\n\nFor a sequence diagram", json_start)
json_end = prompt.index("\n\n## Final self-check", json_start)
scaffold = json.loads(prompt[json_start:json_end])
validate_raw_spec(scaffold)
self.assertEqual(scaffold["version"], "1.1")
self.assertTrue(scaffold["composition"]["diagram_only"])
self.assertTrue(scaffold["composition"]["reference_ids"])
self.assertEqual(
scaffold["source_context"],
{
@@ -35,6 +39,16 @@ class PromptTests(unittest.TestCase):
)
self.assertFalse(scaffold["groups"])
self.assertFalse(scaffold["legend"])
self.assertIn("disconnected rounded cards", prompt)
self.assertIn("diagram-only", prompt)
def test_reference_selection_is_context_sensitive(self) -> None:
context = {
"numbered_context": "1 | Port와 Adapter를 통해 hexagonal core에 의존한다.",
"current_section": {"text": "Inbound adapter와 outbound port를 분리한다."},
}
selected = select_reference_cases(context, limit=2)
self.assertEqual(selected[0].profile, "ports-adapters")
if __name__ == "__main__":
+35
View File
@@ -0,0 +1,35 @@
from __future__ import annotations
import unittest
import xml.etree.ElementTree as ET
from pathlib import Path
from techviz.layout import build_layout
from techviz.renderers.svg import render_svg
from techviz.spec import load_spec
from techviz.validate import has_errors, validate_spec
ROOT = Path(__file__).resolve().parents[1]
class RuntimeProfileTests(unittest.TestCase):
def test_every_profile_fixture_is_executable(self) -> None:
paths = sorted((ROOT / "examples/runtime-profiles").glob("*/spec.json"))
self.assertEqual(len(paths), 10)
seen_profiles: set[str] = set()
for path in paths:
with self.subTest(path=path):
spec = load_spec(path)
issues = validate_spec(spec)
self.assertFalse(has_errors(issues), issues)
svg = render_svg(spec, build_layout(spec))
ET.fromstring(svg)
self.assertNotIn("Generated from grounded VizSpec", svg)
self.assertNotIn('class="diagram-title"', svg)
seen_profiles.add(spec.profile)
self.assertEqual(len(seen_profiles), 10)
if __name__ == "__main__":
unittest.main()