Diagram generation: ref-arch-driven procedure + spec validator + KB enrichment

The diagram path now follows a documented standard procedure (lookup
the closest Oracle Architecture Center reference → confirm components
→ author absolute_layout → spec validator → render → visually verify)
and ships persistent guardrails so layout regressions can't recur.

Persistent procedure changes (apply to all users, all sessions):
- tools/diagram_spec_validator.py — geometry checks (CONTAINER_TOO_THIN,
  CONTAINER_PADDING_VIOLATION, LABEL_OVERFLOW_PARENT) run BEFORE either
  renderer (drawio + PPTX). Catches the subnet-collapse / label-overflow
  bugs that the post-render drawio validator missed.
- tools/oci_diagram_gen.py + tools/oci_pptx_diagram_gen.py — call the
  spec validator before emitting any output. Adds mysql / mysql_heatwave
  type aliases.
- tools/archcenter_pattern_lookup.py — scores against cached page
  descriptions (not just the 1-line summary), supports --queries for
  multi-fragment composition, and applies synonym expansion via
  kb/architecture-center/synonyms.yaml so "LB HA cross AD" matches
  "load balancer high availability availability domain".
- kb/architecture-center/synonyms.yaml — canonical synonym table
  (load balancer, autonomous database, data guard, …) used by the
  lookup scorer.

KB enrichment:
- tools/archcenter_description_fetcher.py + 121 cached _description.md
  under kb/diagram/assets/archcenter-refs/<slug>/. Removes the runtime
  dependency on docs.oracle.com when authoring specs and feeds the
  pattern-lookup scorer.
- 110+ cached .drawio / .svg / .png references for offline reuse,
  plus the OCI Toolkit v24.2 import (kb/diagram/assets/oci-toolkit-drawio).

Documentation:
- docs/skill/output-formats.md — new "Standard diagram-generation
  procedure (MANDATORY)" + geometry rules + the new validator entry.
- SKILL.md option 2 — references the mandatory procedure.
- README.md — describes the spec validator, archcenter_pattern_lookup
  and description fetcher, and updates the KB-health table.

Tooling that backs the procedure (cumulative across recent sessions):
tools/archcenter_case_runner.py, archcenter_batch_driver.py,
archcenter_zip_downloader.py, drawio_visual_validator.py,
drawio_fidelity_eval.py, harvest_drawio_icon.py, import_oci_library.py,
oci_pptx_diagram_gen.py, oci_pptx_render.py, refresh_pptx_icon_index.py.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-04-25 21:15:21 -03:00
parent 2491c38d4b
commit b30a4f0d32
635 changed files with 365317 additions and 1014 deletions

169
tools/drawio_to_png.py Normal file
View File

@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""
drawio_to_png — render a .drawio file to PNG using drawio.exe (Windows
under WSL) or a Linux drawio binary if available.
Why this matters: the SVG-based fidelity eval uses Oracle's bundled
SVG companion (same source as the official drawio). It validates that
the *verbatim* drawio is byte-identical to the reference, but it does
NOT validate the *rebuilt* drawio (the spec → drawio path). Only by
exporting our generated drawio to PNG and diffing it against the
canonical PNG do we catch bugs introduced by the rebuild — e.g. a
700pt label that the SVG-based fidelity test never sees.
"""
from __future__ import annotations
import argparse
import os
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
# Order matters: prefer the Windows drawio executable if available
# under WSL; fall back to a Linux binary if installed.
def _candidate_binaries() -> list[Path]:
"""Locate a draw.io binary. The skill is shared, so we *never* probe
machine-specific WSL paths automatically — that would silently
activate a Windows-only path on the original developer's box and
silently disable it everywhere else. Opt-in only via:
- ``DRAWIO_EXE`` env var (full path to the binary), or
- a ``drawio`` / ``draw.io`` binary on PATH.
"""
cands: list[Path] = []
env_path = os.environ.get("DRAWIO_EXE", "")
if env_path:
cands.append(Path(env_path))
for name in ("drawio", "draw.io"):
which = shutil.which(name)
if which:
cands.append(Path(which))
return cands
def find_drawio_binary() -> Path | None:
for p in _candidate_binaries():
if p.exists():
return p
return None
def _to_windows_path(path: Path) -> str:
"""Map a /mnt/c/... path to C:\\... so drawio.exe (running under
Windows from WSL) can read/write it."""
abs_path = str(path.resolve())
if abs_path.startswith("/mnt/"):
# /mnt/c/Users/... → C:\Users\...
m = re.match(r"^/mnt/([a-zA-Z])(/.*)?$", abs_path)
if m:
drive = m.group(1).upper()
rest = (m.group(2) or "").replace("/", "\\")
return f"{drive}:{rest}"
return abs_path
def _read_page_size(drawio_path: Path) -> tuple[int, int] | None:
"""Read pageWidth/pageHeight from the drawio so we can crop the export
to the canvas. drawio.exe expands to fit *all* content by default; if a
cell sits a few pixels outside the page (common for outline edges of
Oracle stencils) the PNG ends up much wider than the diagram, which
destroys raster fidelity comparisons.
"""
try:
text = Path(drawio_path).read_text(encoding="utf-8", errors="ignore")
except OSError:
return None
import re as _re
m = _re.search(r'pageWidth="(\d+)"[^>]*pageHeight="(\d+)"', text)
if not m:
m = _re.search(r'pageHeight="(\d+)"[^>]*pageWidth="(\d+)"', text)
if m:
return int(m.group(2)), int(m.group(1))
return None
return int(m.group(1)), int(m.group(2))
def render(drawio_path: Path, out_png: Path, scale: int = 2,
binary: Path | None = None,
crop_to_page: bool = True) -> None:
binary = binary or find_drawio_binary()
if binary is None:
raise RuntimeError(
"drawio binary not found. Install draw.io for Windows "
"(/mnt/c/Program Files/draw.io/draw.io.exe) or `dnf install drawio`."
)
out_png.parent.mkdir(parents=True, exist_ok=True)
page_size = _read_page_size(drawio_path) if crop_to_page else None
width_arg = ["-s", str(scale)]
# Note: we deliberately use the bare -s scale flag rather than
# --width/--height. With explicit dimensions, drawio.exe's behavior
# diverges across versions (some treat them as DPI multipliers),
# producing renders 3-4x larger than the page. Plain -s consistently
# yields page_w*scale × page_h*scale.
is_windows_binary = ".exe" in str(binary)
if is_windows_binary:
# drawio.exe under WSL needs Windows-visible paths. Stage to a
# /mnt/c/-rooted scratch dir if either input or output sits on a
# WSL-only filesystem.
project_root = Path(__file__).resolve().parent.parent
stage_dir = project_root / "tmp" / "drawio-render-stage"
stage_dir.mkdir(parents=True, exist_ok=True)
def _stage(p: Path, copy_in: bool) -> Path:
if str(p).startswith("/mnt/"):
return p
staged = stage_dir / p.name
if copy_in:
shutil.copy(p, staged)
return staged
staged = _stage(drawio_path, copy_in=True)
staged_out = _stage(out_png, copy_in=False)
# Note: deliberately NOT passing --crop. With --crop drawio.exe
# rasterizes the bounding box of all cells, not the page — if a
# single stencil overflows the canvas by a few pixels (common
# with Oracle's stenciled icons), the export ends up wider than
# the canonical PNG and similarity comparisons get destroyed by
# the aspect-ratio mismatch.
cmd = [
str(binary), "-x", "-f", "png",
*width_arg,
"-o", _to_windows_path(staged_out),
_to_windows_path(staged),
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0 or not staged_out.exists():
raise RuntimeError(
f"drawio.exe failed (rc={result.returncode}): "
f"stdout={result.stdout!r} stderr={result.stderr!r}"
)
if staged_out != out_png:
shutil.move(staged_out, out_png)
else:
cmd = [
str(binary), "-x", "-f", "png",
"-s", str(scale),
"-o", str(out_png),
str(drawio_path),
]
subprocess.run(cmd, check=True, capture_output=True, text=True)
def main() -> None:
parser = argparse.ArgumentParser(description="Render a .drawio file to PNG.")
parser.add_argument("--drawio", required=True, type=Path)
parser.add_argument("--out", required=True, type=Path)
parser.add_argument("--scale", type=int, default=2)
args = parser.parse_args()
render(args.drawio, args.out, scale=args.scale)
print(f"wrote {args.out} ({args.out.stat().st_size} bytes)")
if __name__ == "__main__":
main()