Files
oci-deal-accelerator/tools/import_oci_library.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

364 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
import_oci_library — bulk-import every shape from the official Oracle
``OCI Library.xml`` (downloadable from the Style Guide for Drawio zip)
into ``kb/diagram/oci-icons.json``.
Why this is a real tool, not a one-off audit: the previous icon set
covered ~60 shapes hand-harvested from a handful of Architecture
Center exports. The official library has 220+ shapes. Without a bulk
importer we silently fail to render whole product families
(Analytics, AI, Applications, Identity & Security, Compute variants,
…), and a developer wouldn't know until the deliverable comes out
without the right icon.
Usage:
python tools/import_oci_library.py
python tools/import_oci_library.py --library /path/to/OCI Library.xml
python tools/import_oci_library.py --dry-run
The library file lives under
``kb/diagram/assets/oci-toolkit-drawio/OCI Library.xml`` after running
``tools/refresh_oci_drawio_toolkit.py``.
Each entry is decoded (zlib + base64 + URL-encoded XML), its cells'
geometry is normalised so the wrapper sits at ``(0, 0, w, h)``, and
the resulting entry is keyed by a canonical slug derived from the
title (e.g. ``Database - Autonomous Database`` →
``database_autonomous_database`` plus ``autonomous_database`` alias).
"""
from __future__ import annotations
import argparse
import base64
import html
import json
import re
import sys
import xml.etree.ElementTree as ET
import zlib
from pathlib import Path
from typing import Iterable
from urllib.parse import unquote
PROJECT_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_LIBRARY = PROJECT_ROOT / "kb" / "diagram" / "assets" / "oci-toolkit-drawio" / "OCI Library.xml"
DEFAULT_DEST = PROJECT_ROOT / "kb" / "diagram" / "oci-icons.json"
def _slug(text: str) -> str:
text = html.unescape(text).replace("&nbsp;", " ")
text = re.sub(r"[^a-zA-Z0-9]+", "_", text.lower()).strip("_")
return text
def _decode_xml(payload: str) -> str:
"""drawio library entries are zlib-deflated + base64 + URL-encoded."""
raw = base64.b64decode(payload)
xml_bytes = zlib.decompress(raw, -15)
return unquote(xml_bytes.decode("utf-8"))
def _normalize_cells(xml_text: str, w: float, h: float) -> tuple[list[str], int]:
"""Return cells serialised with normalised geometry.
Strategy:
1. Parse the XML fragment (drawio wraps cells in <root>…</root>).
2. Find the wrapper cell — the one whose mxGeometry width/height
most closely matches the entry's declared (w, h). That cell
becomes id "2", with geometry forced to (0, 0, w, h).
3. All other cells get sequential ids; their geometry is left
alone (it's relative to their parent). Parent ids are remapped.
"""
# The library blob is just a list of mxCell siblings, not a full doc.
# Wrap so ET can parse.
wrapped = f"<root>{xml_text}</root>"
try:
root = ET.fromstring(wrapped)
except ET.ParseError:
return [], 0
cells = list(root.iter("mxCell"))
if not cells:
return [], 0
# Identify wrapper: cell whose geometry's w/h best matches (w, h).
def _dims(c: ET.Element) -> tuple[float, float]:
g = c.find("mxGeometry")
if g is None:
return 0.0, 0.0
try:
return float(g.get("width") or 0), float(g.get("height") or 0)
except ValueError:
return 0.0, 0.0
wrapper_idx = 0
best_score = float("inf")
for i, c in enumerate(cells):
cw, ch = _dims(c)
score = abs(cw - w) + abs(ch - h)
if score < best_score and cw > 0 and ch > 0:
best_score = score
wrapper_idx = i
# Build id map. Wrapper → "2"; descendants → "3","4",…
id_map: dict[str, str] = {}
wrapper_orig = cells[wrapper_idx].get("id") or f"_w{wrapper_idx}"
id_map[wrapper_orig] = "2"
counter = 3
for i, c in enumerate(cells):
if i == wrapper_idx:
continue
orig = c.get("id") or f"_c{i}"
id_map[orig] = str(counter)
counter += 1
serialized: list[str] = []
for i, c in enumerate(cells):
orig = c.get("id") or (f"_w{i}" if i == wrapper_idx else f"_c{i}")
new_id = id_map[orig]
c_copy = ET.fromstring(ET.tostring(c))
c_copy.set("id", new_id)
old_parent = c_copy.get("parent") or ""
c_copy.set("parent", id_map.get(old_parent, "1"))
if i == wrapper_idx:
geom = c_copy.find("mxGeometry")
if geom is not None:
geom.set("x", "0")
geom.set("y", "0")
geom.set("width", str(w))
geom.set("height", str(h))
serialized.append(ET.tostring(c_copy, encoding="unicode"))
return serialized, len(cells)
# ---------------------------------------------------------------------------
# Canonical-title → short alias map. Keys must be the EXACT library title
# (post HTML-decode, after stripping the category prefix). Substring
# matching was tried first but produced silent collisions — e.g.
# "Burstable Virtual Machine Burstable VM" contains the substring
# "virtual machine", so it claimed the canonical `virtual_machine` alias
# from the actual "Virtual Machine VM" entry. Exact match means each
# alias has exactly one source.
# ---------------------------------------------------------------------------
# Verified against `OCI Library.xml` v24.2 short suffixes 2026-04-25.
# Substring matching produced silent collisions (e.g. "virtual machine" hit
# Burstable VM first), so this table uses EXACT title-suffix matches. Run
# `python tools/import_oci_library.py --dry-run` to verify which entries
# resolve before writing.
TITLE_TO_ALIASES: dict[str, list[str]] = {
# Compute (Compute - …)
"Virtual Machine VM": ["virtual_machine", "vm", "oci_compute", "compute_instance", "compute"],
"Bare Metal Compute": ["bare_metal", "bare_metal_compute"],
"Burstable Virtual Machine Burstable VM": ["burstable_vm", "burstable_virtual_machine"],
"Flex Virtual Machine Flex VM": ["flex_vm"],
"Instance Pools": ["instance_pool", "instance_pools"],
"Functions": ["functions", "oci_functions"],
"Autoscaling": ["autoscaling"],
# Database (Database - …) — verified library suffixes
"Database System": ["database_system", "base_db", "base_database", "dbcs", "db_system"],
"Autonomous DB": ["autonomous_db", "autonomous_database"],
"ADB-D": ["adb_d", "adb"], # ADB on Dedicated infrastructure
"ADW-D": ["adw_d", "adw"],
"ATP-D": ["atp_d", "atp"],
"ADB-S": ["adb_s"],
"Exadata": ["exadata", "exacs", "oracle_exadata_database_service", "exadata_dedicated"],
"Exadata C@C": ["exadata_cc", "exacc"],
"Data Safe": ["data_safe"],
"GoldenGate": ["goldengate"],
"NoSQL": ["nosql"],
"MySQL": ["mysql"],
"APEX": ["apex"],
# Storage (Storage - …)
"Object Storage": ["object_storage", "bucket", "storage", "oci_object_storage"],
"Block Storage": ["block_volume", "block_storage"],
"File Storage": ["file_storage"],
"Buckets": ["bucket"],
"Backup Restore": ["backup_restore"],
# Networking (Networking - …)
"Internet Gateway": ["internet_gateway", "igw"],
"Service Gateway": ["service_gateway", "sgw"],
"NAT Gateway": ["nat_gateway", "natgw"],
"Dynamic Routing Gateway DRG": ["drg", "dynamic_routing_gateway"],
"Load Balancer": ["load_balancer", "lb"],
"Flexible Load Balancer": ["flexible_lb", "nlb", "network_load_balancer"],
# FastConnect icon — Library only ships connector-themed variants;
# use the vertical 71×75 stencil as the canonical FastConnect badge.
"Special Connectors - FastConnect - Vertical": ["fastconnect", "fastconnect_vertical"],
"Special Connectors - FastConnect - Horizontal": ["fastconnect_horizontal"],
"Virtual Cloud Network VCN": ["vcn", "vcn_icon"],
"Route Table": [
# Per OCI Toolkit slide 18: "VCN, routing table, and security list
# icons are used at half size as labels to differentiate the VCN
# and subnet." There is no separate Security List entry, so the
# Route Table stencil serves as the canonical subnet corner marker.
"route_table", "route_table_and_security_list",
"security_list", "security_lists",
],
"DNS": ["dns"],
# Identity & Security (Identity and Security - …)
"Policies": ["policies", "iam_policies"],
"Vault": ["vault"],
"Key Vault": ["key_vault", "vault_secrets"],
"Bastion": ["bastion"],
"Cloud Guard": ["cloud_guard"],
"Firewall": ["network_firewall", "firewall"],
"Web Application Firewall WAF": ["waf", "web_application_firewall"],
# Observability and Management
"Monitoring": ["monitoring"],
"Notifications": ["notifications", "notification"],
"Logging": ["logging"],
"Logging Analytics": ["logging_analytics"],
"Alarms": ["alarms"],
# Integration / Developer Services
"API Gateway": ["api_gateway"],
"Streaming": ["streaming", "oci_streaming"],
"Queue": ["queue"],
"Events Service": ["events_service"],
"API Service": ["api_service"],
# Containers
"Container Engine for Kubernetes Container Engine for Kubernetes Cluster": [
"oke", "kubernetes", "container_engine_for_kubernetes_cluster",
],
# Migration / DR
"Full Stack Disaster Recovery FSDR": ["full_stack_disaster_recovery", "fsdr"],
}
# Some types Oracle's library doesn't ship a stencil for (Data Guard,
# Refreshable Clone). For those we point the alias at an equivalent
# Database icon so renders don't fall back to colored rectangles.
COMPOSITE_FALLBACK_ALIASES: dict[str, str] = {
"data_guard": "Database", # plain "Database" stencil
"data_guard_recovery": "Database",
"refreshable_clone": "ADB-D",
}
def _alias_keys(title: str) -> list[str]:
"""Return [canonical_slug, *short_aliases].
The canonical slug is always the long title slug
(e.g. ``compute_virtual_machine_vm``). Short aliases come from the
explicit TITLE_TO_ALIASES table, keyed by the title *suffix* after
the category prefix (post HTML-decode).
"""
canonical = _slug(title)
aliases: list[str] = [canonical]
decoded = html.unescape(title).replace("&nbsp;", " ").replace("&amp;", "&")
short = decoded.split(" - ", 1)[-1].strip()
if short in TITLE_TO_ALIASES:
aliases.extend(TITLE_TO_ALIASES[short])
seen: set[str] = set()
out: list[str] = []
for a in aliases:
if a and a not in seen:
out.append(a)
seen.add(a)
return out
def _iter_library_entries(library_path: Path) -> Iterable[dict]:
raw = library_path.read_text(encoding="utf-8")
m = re.search(r"<mxlibrary>(.+?)</mxlibrary>", raw, re.DOTALL)
if not m:
raise SystemExit(f"No <mxlibrary> in {library_path}")
entries = json.loads(m.group(1))
yield from entries
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--library", type=Path, default=DEFAULT_LIBRARY)
parser.add_argument("--dest", type=Path, default=DEFAULT_DEST)
parser.add_argument("--dry-run", action="store_true",
help="Print what would be added without writing.")
parser.add_argument("--keep-existing", action="store_true",
help="Preserve hand-curated entries already present "
"(do not overwrite). Off by default — the official "
"library is the source of truth.")
args = parser.parse_args()
icons = json.loads(args.dest.read_text(encoding="utf-8")) if args.dest.exists() else {}
initial_count = len(icons)
added: list[str] = []
aliased: list[str] = []
skipped: list[str] = []
failed: list[str] = []
for entry in _iter_library_entries(args.library):
title = entry.get("title", "")
if not title:
continue
try:
xml_text = _decode_xml(entry["xml"])
except Exception as exc:
failed.append(f"{title}: {exc}")
continue
w = float(entry.get("w", 0) or 0)
h = float(entry.get("h", 0) or 0)
if w <= 0 or h <= 0:
failed.append(f"{title}: missing w/h")
continue
cells, ncells = _normalize_cells(xml_text, w, h)
if not cells:
failed.append(f"{title}: no cells parsed")
continue
keys = _alias_keys(title)
canonical = keys[0]
record = {
"title": title.replace("&amp;nbsp;", " ").replace("&amp;", "&"),
"w": w, "h": h, "cells": cells,
"source": "OCI Library.xml v24.2",
}
if canonical in icons and args.keep_existing:
skipped.append(canonical)
else:
icons[canonical] = record
added.append(canonical)
for alias in keys[1:]:
if alias in icons and args.keep_existing:
continue
icons[alias] = record
aliased.append(alias)
# Composite/derived types Oracle does not ship as their own stencil
# (Data Guard, refreshable clones, …). Resolve via fallback aliases
# so spec authors can keep using the friendly names without falling
# back to a colored rectangle.
by_short_title: dict[str, dict] = {}
for record in icons.values():
title = record.get("title", "")
if " - " in title:
by_short_title.setdefault(title.split(" - ", 1)[-1].strip(), record)
fallback_count = 0
for alias, source_title in COMPOSITE_FALLBACK_ALIASES.items():
if alias in icons:
continue
source = by_short_title.get(source_title)
if source:
icons[alias] = source
fallback_count += 1
print(f"Initial entries: {initial_count}", file=sys.stderr)
print(f"Library entries: {len(added) + len(skipped) + len(failed)}", file=sys.stderr)
if fallback_count:
print(f"Composite fallbacks added: {fallback_count}", file=sys.stderr)
print(f"Added/replaced: {len(added)}", file=sys.stderr)
print(f"Aliased: {len(aliased)}", file=sys.stderr)
print(f"Skipped (kept): {len(skipped)}", file=sys.stderr)
print(f"Failed to parse: {len(failed)}", file=sys.stderr)
for line in failed[:10]:
print(f" - {line}", file=sys.stderr)
print(f"Final entries: {len(icons)}", file=sys.stderr)
if not args.dry_run:
args.dest.write_text(json.dumps(icons, indent=2), encoding="utf-8")
print(f"\nWrote {args.dest}", file=sys.stderr)
if __name__ == "__main__":
main()