forked from diegoecab/oci-deal-accelerator
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>
160 lines
6.1 KiB
Python
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()
|