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:
@@ -16,6 +16,7 @@ Or import and use programmatically:
|
||||
|
||||
import yaml
|
||||
import argparse
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional, List, Dict
|
||||
@@ -25,7 +26,10 @@ from pptx.dml.color import RGBColor
|
||||
from pptx.enum.text import PP_ALIGN
|
||||
from pptx.enum.shapes import MSO_SHAPE
|
||||
|
||||
from oci_pptx_base import Colors, Layouts, OraclePresBase
|
||||
try:
|
||||
from oci_pptx_base import Colors, Layouts, OraclePresBase
|
||||
except ModuleNotFoundError:
|
||||
from tools.oci_pptx_base import Colors, Layouts, OraclePresBase
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -59,6 +63,184 @@ def pick_list(mapping: dict, *keys):
|
||||
return []
|
||||
|
||||
|
||||
def _timeline_to_weeks(text: str) -> int | None:
|
||||
"""Parse a simple duration phrase like '12 weeks' or '6 months'."""
|
||||
if not text:
|
||||
return None
|
||||
value = str(text).lower()
|
||||
match = re.search(r"(\d+)\s*(week|weeks|month|months)", value)
|
||||
if not match:
|
||||
return None
|
||||
amount = int(match.group(1))
|
||||
unit = match.group(2)
|
||||
return amount * 4 if unit.startswith("month") else amount
|
||||
|
||||
|
||||
def _extract_business_case_text(bc: dict) -> str:
|
||||
parts = [
|
||||
pick(bc, "executive_summary", "summary"),
|
||||
pick(bc, "primary_driver", "main_driver"),
|
||||
]
|
||||
drivers = bc.get("drivers", {})
|
||||
if isinstance(drivers, dict):
|
||||
parts.extend([
|
||||
pick(drivers, "primary", "primary_driver", "main_driver"),
|
||||
pick(drivers, "urgency", "why_now"),
|
||||
])
|
||||
coi = drivers.get("cost_of_inaction", {}) or {}
|
||||
if isinstance(coi, dict):
|
||||
parts.extend(coi.values())
|
||||
return " ".join(str(p) for p in parts if p)
|
||||
|
||||
|
||||
def _infer_driver_headline(text: str) -> str:
|
||||
lowered = text.lower()
|
||||
if any(token in lowered for token in ("budget", "cost", "tco", "license", "refresh")):
|
||||
return "Reduce cost and avoid infrastructure refresh"
|
||||
if any(token in lowered for token in ("dr", "disaster recovery", "resilien", "rto", "rpo")):
|
||||
return "Improve resilience and business continuity"
|
||||
if any(token in lowered for token in ("pci", "regulator", "compliance", "data residency", "sovereignty")):
|
||||
return "Address compliance and sovereignty requirements"
|
||||
return "Modernize the platform on OCI"
|
||||
|
||||
|
||||
def _infer_driver_objective(text: str) -> str:
|
||||
"""Return a noun phrase suitable for recommendation prose."""
|
||||
lowered = text.lower()
|
||||
if any(token in lowered for token in ("budget", "cost", "tco", "license", "refresh")):
|
||||
return "the cost-reduction and refresh-avoidance case"
|
||||
if any(token in lowered for token in ("dr", "disaster recovery", "resilien", "rto", "rpo")):
|
||||
return "the resilience and business-continuity posture"
|
||||
if any(token in lowered for token in ("pci", "regulator", "compliance", "data residency", "sovereignty")):
|
||||
return "the compliance and sovereignty position"
|
||||
return "the OCI modernization approach"
|
||||
|
||||
|
||||
def _infer_business_case_drivers(text: str) -> dict:
|
||||
headline = _infer_driver_headline(text)
|
||||
urgency = "Use the current planning window to validate scope, commercials, and delivery readiness."
|
||||
if _timeline_to_weeks(text):
|
||||
urgency = f"Align the case with the stated delivery window ({_timeline_to_weeks(text)} weeks)."
|
||||
return {
|
||||
"primary": headline,
|
||||
"urgency": urgency,
|
||||
"cost_of_inaction": {
|
||||
"financial": "Continuing with the current platform preserves avoidable run costs and refresh exposure.",
|
||||
"operational": "The delivery team keeps spending time on manual operations instead of modernization work.",
|
||||
"strategic": "Delaying the decision extends technical debt and slows business change.",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _infer_business_case_risks(text: str) -> dict:
|
||||
lowered = text.lower()
|
||||
migration = [
|
||||
{
|
||||
"risk": "Migration scope may be larger than the initial discovery captured.",
|
||||
"mitigation": "Baseline applications, data stores, and dependencies before locking the plan.",
|
||||
},
|
||||
{
|
||||
"risk": "Operating model changes may require team enablement before go-live.",
|
||||
"mitigation": "Define the target RACI and training plan during design.",
|
||||
},
|
||||
]
|
||||
if any(token in lowered for token in ("dr", "disaster recovery", "rto", "rpo")):
|
||||
migration.append({
|
||||
"risk": "Resilience objectives may require rehearsal and explicit runbooks.",
|
||||
"mitigation": "Schedule failover validation before production cutover.",
|
||||
})
|
||||
do_nothing = [
|
||||
{
|
||||
"risk": "Technical debt and platform constraints continue to accumulate.",
|
||||
"impact": "Higher delivery risk and slower response to business demand.",
|
||||
},
|
||||
{
|
||||
"risk": "Commercial and capacity assumptions remain unvalidated.",
|
||||
"impact": "Procurement or renewal decisions may be made without a current baseline.",
|
||||
},
|
||||
]
|
||||
return {
|
||||
"migration_risks": migration[:4],
|
||||
"do_nothing_risks": do_nothing[:4],
|
||||
}
|
||||
|
||||
|
||||
def _infer_business_case_roadmap(text: str) -> dict:
|
||||
total_weeks = _timeline_to_weeks(text)
|
||||
if total_weeks and total_weeks >= 6:
|
||||
define = max(1, round(total_weeks * 0.25))
|
||||
design = max(1, round(total_weeks * 0.35))
|
||||
deliver = max(1, total_weeks - define - design)
|
||||
total_duration = f"{total_weeks} weeks"
|
||||
else:
|
||||
define, design, deliver = 2, 4, 6
|
||||
total_duration = "12 weeks (to validate)"
|
||||
return {
|
||||
"total_duration": total_duration,
|
||||
"phases": [
|
||||
{
|
||||
"name": "Define",
|
||||
"duration": f"{define} weeks",
|
||||
"deliverables": ["Scope baseline", "Business case validation"],
|
||||
"milestones": ["Stakeholder alignment"],
|
||||
},
|
||||
{
|
||||
"name": "Design",
|
||||
"duration": f"{design} weeks",
|
||||
"deliverables": ["Target architecture", "Security and operating model"],
|
||||
"milestones": ["Design sign-off"],
|
||||
},
|
||||
{
|
||||
"name": "Deliver",
|
||||
"duration": f"{deliver} weeks",
|
||||
"deliverables": ["Migration execution", "Cutover and stabilization"],
|
||||
"milestones": ["Go-live readiness"],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _infer_business_case_recommendation(text: str) -> dict:
|
||||
objective = _infer_driver_objective(text)
|
||||
return {
|
||||
"summary": f"Proceed to detailed design on OCI to validate {objective} with customer-specific sizing and commercials.",
|
||||
"next_steps": [
|
||||
"Confirm scope, current-state baseline, and success criteria.",
|
||||
"Validate commercials, licensing assumptions, and target delivery model.",
|
||||
"Run a design workshop and prepare the first migration wave.",
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _enrich_sparse_business_case(spec: dict) -> dict:
|
||||
"""Fill safe narrative sections so sparse business cases still render professionally."""
|
||||
if not isinstance(spec, dict):
|
||||
return spec
|
||||
bc = spec.get("business_case", spec)
|
||||
if not isinstance(bc, dict):
|
||||
return spec
|
||||
|
||||
text = _extract_business_case_text(bc)
|
||||
if not text.strip():
|
||||
return spec
|
||||
|
||||
enriched_bc = dict(bc)
|
||||
if "drivers" not in enriched_bc:
|
||||
enriched_bc["drivers"] = _infer_business_case_drivers(text)
|
||||
if "risks" not in enriched_bc:
|
||||
enriched_bc["risks"] = _infer_business_case_risks(text)
|
||||
if "roadmap" not in enriched_bc:
|
||||
enriched_bc["roadmap"] = _infer_business_case_roadmap(text)
|
||||
if "recommendation" not in enriched_bc:
|
||||
enriched_bc["recommendation"] = _infer_business_case_recommendation(text)
|
||||
|
||||
if "business_case" in spec:
|
||||
enriched = dict(spec)
|
||||
enriched["business_case"] = enriched_bc
|
||||
return enriched
|
||||
return enriched_bc
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Business Case Deck Generator
|
||||
# ============================================================
|
||||
@@ -619,6 +801,7 @@ class BusinessCaseDeckGenerator(OraclePresBase):
|
||||
@classmethod
|
||||
def from_spec(cls, spec: dict, template: Optional[str] = None) -> "BusinessCaseDeckGenerator":
|
||||
"""Build a complete business case deck from a YAML specification."""
|
||||
spec = _enrich_sparse_business_case(spec)
|
||||
bc = spec.get("business_case", spec) # Support both wrapped and unwrapped
|
||||
gen = cls(template=template)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user