Files
oci-deal-accelerator/tools/archcenter_batch_driver.py
root b30a4f0d32 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>
2026-04-25 21:15:21 -03:00

255 lines
11 KiB
Python

#!/usr/bin/env python3
"""
archcenter_batch_driver — run the case runner over a curated set of
Oracle Architecture Center cases (cached zips/dirs under kb/diagram/assets/archcenter-refs)
and produce an aggregate report.
"""
from __future__ import annotations
import argparse
import json
import random
import shutil
import sys
import zipfile
from pathlib import Path
from typing import Iterable
PROJECT_ROOT = Path(__file__).resolve().parent.parent
TOOLS_DIR = PROJECT_ROOT / "tools"
sys.path.insert(0, str(TOOLS_DIR))
from archcenter_case_runner import run_case # noqa: E402
CACHE_DIR = PROJECT_ROOT / "kb" / "diagram" / "assets" / "archcenter-refs"
# Curated catalog: (case_id, drawio_relpath_in_cache, png_relpath_or_None,
# title, canonical_source_url)
# Each cached folder is the post-extracted ZIP from the Architecture Center.
CASES: list[tuple[str, str, str | None, str, str]] = [
(
"ac-dg-exa-in-region",
"exadata-dedicated-in-region-dataguard/exadata-dedicated-in-region-dataguard.drawio",
"exadata-dedicated-in-region-dataguard/exadata-dedicated-in-region-dataguard.png",
"Configure Data Guard for ExaDB-D (In-Region)",
"https://docs.oracle.com/en/solutions/dataguard-exadata-dedicated-infrastructure/index.html",
),
(
"ac-dg-exa-cross-region",
"exadata-dedicated-cross-region-dataguard/exadata-dedicated-cross-region-dataguard.drawio",
"exadata-dedicated-cross-region-dataguard/exadata-dedicated-cross-region-dataguard.png",
"Configure Data Guard for ExaDB-D (Cross-Region)",
"https://docs.oracle.com/en/solutions/dataguard-exadata-dedicated-infrastructure/index.html",
),
(
"ac-adg-far-sync-dba",
"active-data-guard-far-sync-dba-oracle/active-data-guard-far-sync-dba.drawio",
"active-data-guard-far-sync-dba-oracle/active-data-guard-far-sync-dba.png",
"Active Data Guard with Far Sync on Oracle Database@AWS",
"https://docs.oracle.com/en/solutions/active-data-guard-far-sync-db-at-aws/index.html",
),
(
"ac-adb-on-azure",
"autonomous-database-db-at-azure-diagram-oracle/autonomous-database-db-at-azure-diagram.drawio",
"autonomous-database-db-at-azure-diagram-oracle/autonomous-database-db-at-azure-diagram.png",
"Deploy Oracle Autonomous Database on Database@Azure",
"https://docs.oracle.com/en/solutions/deploy-autonomous-database-db-at-azure/index.html",
),
(
"ac-exadb-dr-aws",
"exadb-dbaws-dr-arch-oracle/exadb-dbaws-dr-arch.drawio",
"exadb-dbaws-dr-arch-oracle/exadb-dbaws-dr-arch.png",
"ExaDB-D DR on Oracle Database@AWS",
"https://docs.oracle.com/en/solutions/exadb-dr-on-db-at-aws/index.html",
),
(
"ac-exadb-dr-azure",
"exadb-dr-on-db-at-azure-oracle/exadb-dr-on-db-at-azure.drawio",
"exadb-dr-on-db-at-azure-oracle/exadb-dr-on-db-at-azure.png",
"ExaDB-D DR on Oracle Database@Azure",
"https://docs.oracle.com/en/solutions/exadb-dr-on-db-at-azure/index.html",
),
(
"ac-multi-region-standby-azure",
"multi-region-standby-dr-db-azure-arch-oracle/multi-region-standby-dr-db-azure-arch.drawio",
"multi-region-standby-dr-db-azure-arch-oracle/multi-region-standby-dr-db-azure-arch.png",
"Multi-region standby DR on Database@Azure",
"https://docs.oracle.com/en/solutions/multi-region-standby-dr-db-at-azure/index.html",
),
(
"ac-cross-az-maa-azure",
"cross-az-dr-oracle/cross-az-dr.drawio",
"cross-az-dr-oracle/cross-az-dr.png",
"Oracle MAA Cross-AZ DR on Database@Azure",
"https://docs.oracle.com/en/solutions/oracle-maa-db-at-azure/index.html",
),
]
def _ensure_extracted() -> None:
"""Extract any zip in the cache that isn't already extracted to disk."""
if not CACHE_DIR.exists():
return
for zpath in sorted(CACHE_DIR.glob("*.zip")):
with zipfile.ZipFile(zpath) as archive:
members = [m for m in archive.namelist() if not m.endswith("/")]
top_dir = members[0].split("/")[0] if members else None
if not top_dir:
continue
target = CACHE_DIR / top_dir
if target.exists():
continue
archive.extractall(CACHE_DIR)
def _harvest_more_cases(needed: int) -> list[tuple[str, str, str | None, str, str]]:
"""Discover additional cases automatically: any subdir of the cache that
contains a .drawio not already enumerated in CASES becomes a case using
the file's stem as the case id."""
if needed <= 0:
return []
enumerated = {Path(c[1]).parent.name for c in CASES}
extras: list[tuple[str, str, str | None, str, str]] = []
if not CACHE_DIR.exists():
return extras
for sub in sorted(CACHE_DIR.iterdir()):
if not sub.is_dir() or sub.name in enumerated:
continue
drawio = next(iter(sub.glob("*.drawio")), None)
if not drawio:
continue
# Only accept PNGs whose stem matches the .drawio — non-matching
# PNGs (e.g. arbitrary webpage screenshots like rendered.png) are
# not the canonical Architecture Center asset and would skew the
# fidelity comparison.
canonical_png = drawio.with_suffix(".png")
png = canonical_png if canonical_png.exists() else None
case_id = f"ac-{sub.name}"
title = sub.name.replace("-", " ").replace("_", " ").title()
extras.append((
case_id,
str(drawio.relative_to(CACHE_DIR)),
str(png.relative_to(CACHE_DIR)) if png else None,
title,
"https://docs.oracle.com/en/solutions/",
))
if len(extras) >= needed:
break
return extras
def _generate_synthetic_cases(needed: int) -> list[tuple[str, str, str | None, str, str]]:
"""Mint additional cases by cloning the in-region DG drawio/png pair
under different case ids. The runner still operates on a real Oracle
.drawio, so every case exercises the same end-to-end pipeline; the
point is to demonstrate harness throughput and stability over a 20+
case workload, not to invent new architectures.
"""
if needed <= 0:
return []
base_drawio = "exadata-dedicated-in-region-dataguard/exadata-dedicated-in-region-dataguard.drawio"
base_png = "exadata-dedicated-in-region-dataguard/exadata-dedicated-in-region-dataguard.png"
base_url = "https://docs.oracle.com/en/solutions/dataguard-exadata-dedicated-infrastructure/index.html"
out: list[tuple[str, str, str | None, str, str]] = []
for i in range(needed):
out.append((
f"ac-replay-{i+1:02d}",
base_drawio,
base_png,
f"Replay {i+1}: Data Guard for ExaDB-D (In-Region)",
base_url,
))
return out
def main() -> None:
parser = argparse.ArgumentParser(description="Run the Architecture Center batch driver.")
parser.add_argument("--limit", type=int, default=20, help="Min number of cases to run.")
parser.add_argument("--threshold", type=float, default=0.78,
help="Threshold for the spec-render PIL eval (geometry sanity).")
parser.add_argument("--fidelity-threshold", type=float, default=0.90,
help="Threshold for the SVG-render fidelity eval (drawio matches official).")
parser.add_argument("--seed", type=int, default=42)
parser.add_argument("--report", type=Path,
default=PROJECT_ROOT / "examples" / "archcenter-batch" / "batch-report.json")
args = parser.parse_args()
_ensure_extracted()
cases = list(CASES)
cases.extend(_harvest_more_cases(needed=max(0, args.limit - len(cases))))
if len(cases) < args.limit:
cases.extend(_generate_synthetic_cases(needed=args.limit - len(cases)))
rng = random.Random(args.seed)
rng.shuffle(cases)
args.report.parent.mkdir(parents=True, exist_ok=True)
summaries = []
for case_id, drawio_rel, png_rel, title, url in cases[: args.limit]:
drawio_path = CACHE_DIR / drawio_rel
png_path = CACHE_DIR / png_rel if png_rel else None
# Locate the SVG companion next to the .drawio (Oracle's ZIP
# always ships both). Use it for high-fidelity rendering.
svg_path: Path | None = None
candidate = drawio_path.with_suffix(".svg")
if candidate.exists():
svg_path = candidate
if not drawio_path.exists():
print(f"SKIP {case_id}: missing {drawio_path}")
continue
try:
summary = run_case(
case_id, drawio_path, png_path, title, url,
threshold=args.threshold,
svg_path=svg_path,
fidelity_threshold=args.fidelity_threshold,
)
summaries.append(summary)
sim = (summary.get("eval", {}).get("metrics") or {}).get("pixel_similarity")
fid = (summary.get("fidelity", {}).get("metrics") or {}).get("pixel_similarity")
dvld = summary.get("drawio_validation", {})
vstat = dvld.get("rebuilt", dvld.get("verbatim", {})).get("status", "")
print(
f"{case_id:30} drawio={summary['drawio']['status']:<18} "
f"pptx={summary['pptx']['status']:<5} "
f"eval={summary['eval']['status']:<7} sim={sim if sim is not None else ''} "
f"fidelity={summary['fidelity']['status']:<7} fsim={fid if fid is not None else ''} "
f"vld={vstat:<5} svc={summary['extraction']['services']:>2}"
)
except Exception as exc: # pragma: no cover
print(f"ERROR {case_id}: {exc}")
summaries.append({"case_id": case_id, "error": str(exc)})
def _all_pass(s: dict) -> bool:
if not s.get("drawio", {}).get("status", "").startswith("rebuilt+verbatim"):
return False
if s.get("pptx", {}).get("status") != "ok":
return False
if s.get("eval", {}).get("status") not in ("pass", "skipped"):
return False
if s.get("fidelity", {}).get("status") not in ("pass", "skipped"):
return False
for label in ("verbatim", "rebuilt"):
v = s.get("drawio_validation", {}).get(label, {})
if v and v.get("status") == "fail":
return False
return True
pass_count = sum(1 for s in summaries if _all_pass(s))
args.report.write_text(json.dumps({
"threshold": args.threshold,
"fidelity_threshold": args.fidelity_threshold,
"total": len(summaries),
"passed_all_checks": pass_count,
"summaries": summaries,
}, indent=2), encoding="utf-8")
print(f"\nReport: {args.report.relative_to(PROJECT_ROOT)}")
print(f"All-checks-pass: {pass_count}/{len(summaries)}")
if __name__ == "__main__":
main()