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

160 lines
6.1 KiB
Python

#!/usr/bin/env python3
"""
harvest_drawio_icon — extract a multi-cell stencil from any Oracle
Architecture Center .drawio and add it to ``kb/diagram/oci-icons.json``
in the schema the rest of the harness expects.
Why this is a real tool, not a one-off script: the drawio renderer in
``oci_diagram_gen.py`` consumes harvested icons by re-parenting them
under a synthetic group at the spec's ``(x, y, w, h)``. If the harvested
cells carry their original whole-page absolute coordinates, the icon
renders at the harvested position on the new page (typically far below
the spec's container) — a bug we've seen and fixed once. Centralising
the extraction here ensures every harvest is normalised the same way.
Usage:
python tools/harvest_drawio_icon.py \
--source kb/diagram/assets/archcenter-refs/.../some.drawio \
--anchor 205 152 64 84 \
--type oracle_exadata_database_service \
--title "Database - Oracle Exadata Database Service"
The ``--anchor`` argument identifies the icon's wrapper cell by its
absolute (x, y, w, h) in the source file. The harvester:
1. Walks both ``mxCell`` and ``UserObject`` so wrapper ids are
addressable (Oracle stores most cells inside ``UserObject``).
2. Collects the wrapper + every descendant in the parent chain.
3. Renumbers ids so they don't collide with other entries.
4. Sets the wrapper's geometry to ``(0, 0, w, h)`` — the renderer
re-positions the group from the spec; carrying absolute coords
here would shift the rendered icon off-canvas.
"""
from __future__ import annotations
import argparse
import json
import xml.etree.ElementTree as ET
from pathlib import Path
def _build_lookup(root: ET.Element) -> dict[str, ET.Element]:
cells_by_id: dict[str, ET.Element] = {}
for el in root.iter():
if el.tag == "mxCell":
cid = el.get("id")
if cid:
cells_by_id[cid] = el
elif el.tag == "UserObject":
cid = el.get("id")
inner = el.find("mxCell")
if cid and inner is not None:
cells_by_id[cid] = inner
return cells_by_id
def _find_anchor_at(cells: dict[str, ET.Element], target: tuple[int, int, int, int]) -> str | None:
tx, ty, tw, th = target
for cid, cell in cells.items():
style = (cell.get("style") or "").lower()
if "fillcolor=none" not in style or "strokecolor=none" not in style:
continue
geom = cell.find("mxGeometry")
if geom is None:
continue
try:
x = float(geom.get("x") or 0)
y = float(geom.get("y") or 0)
w = float(geom.get("width") or 0)
h = float(geom.get("height") or 0)
except ValueError:
continue
if (abs(x - tx) <= 2 and abs(y - ty) <= 2
and abs(w - tw) <= 2 and abs(h - th) <= 2):
return cid
return None
def _descendants(cells: dict[str, ET.Element], root_id: str) -> list[str]:
out = [root_id]
queue = [root_id]
while queue:
cur = queue.pop(0)
for cid, cell in cells.items():
if cell.get("parent") == cur and cid not in out:
out.append(cid)
queue.append(cid)
return out
def harvest(source: Path, anchor: tuple[int, int, int, int], title: str) -> dict:
tree = ET.parse(source)
root = tree.getroot()
cells = _build_lookup(root)
anchor_id = _find_anchor_at(cells, anchor)
if anchor_id is None:
raise SystemExit(f"No anchor found at {anchor} in {source}")
nodes = _descendants(cells, anchor_id)
id_map = {nodes[0]: "2"}
for i, n in enumerate(nodes[1:], start=3):
id_map[n] = str(i)
serialized: list[str] = []
target_w, target_h = anchor[2], anchor[3]
for n in nodes:
cell = cells[n]
copy = ET.fromstring(ET.tostring(cell))
copy.set("id", id_map[n])
old_parent = copy.get("parent") or ""
copy.set("parent", id_map.get(old_parent, "1"))
if n == anchor_id:
geom = copy.find("mxGeometry")
if geom is not None:
# CRITICAL: normalize wrapper to (0,0,w,h). The renderer
# re-positions the group from the spec. See module docstring.
geom.set("x", "0")
geom.set("y", "0")
geom.set("width", str(target_w))
geom.set("height", str(target_h))
serialized.append(ET.tostring(copy, encoding="unicode"))
return {
"title": title,
"w": float(target_w),
"h": float(target_h),
"cells": serialized,
}
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", required=True, type=Path,
help="Path to the Oracle .drawio source file.")
parser.add_argument("--anchor", required=True, nargs=4, type=int, metavar=("X", "Y", "W", "H"),
help="Anchor coordinates of the icon wrapper in the source.")
parser.add_argument("--type", required=True,
help="Icon type key under which to register the entry (e.g. policies).")
parser.add_argument("--title", default="",
help="Human-readable title (defaults to the type key).")
parser.add_argument("--alias", action="append", default=[],
help="Additional keys to alias to the same entry (repeatable).")
parser.add_argument("--icons-json", type=Path,
default=Path(__file__).resolve().parent.parent / "kb" / "diagram" / "oci-icons.json")
args = parser.parse_args()
entry = harvest(args.source, tuple(args.anchor), args.title or args.type.replace("_", " ").title())
icons = json.loads(args.icons_json.read_text(encoding="utf-8")) if args.icons_json.exists() else {}
icons[args.type] = entry
for alias in args.alias:
icons[alias] = entry
args.icons_json.write_text(json.dumps(icons, indent=2), encoding="utf-8")
print(f"wrote {args.type}"
+ (f" + aliases {args.alias}" if args.alias else "")
+ f"{args.icons_json}")
if __name__ == "__main__":
main()