Files
oci-deal-accelerator/tools/refresh_arch_catalog.py
root ca93a0aa4e Auto-refresh KB pricing, align skill with Anthropic best practices
Why this set of changes:
- KB pricing was drifting silently — domain files (database.yaml,
  storage.yaml, etc.) had prices 30-800% off the live Oracle API and
  nobody read them. The skill was auditing as stale on every check
  with no path to fix it.
- The skill itself violated Anthropic's spec (`name` field had
  uppercase/spaces) and was over the 500-line guideline (647 lines),
  hurting discovery and load performance.
- Welcome flow occasionally improvised the menu instead of reading
  SKILL.md, missing options.

Pricing — single source of truth, fully automated:
- Extend tools/refresh_sku_catalog.py with --refresh-domain compute,
  pulls shape-level prices from the Oracle public pricing API
  (apexapps.oracle.com), preserves manual fields (notes, GPU specs,
  free-tier annotations, estimation_helpers), recomputes derived
  monthly values, and protects $0 free-tier prices from overwrite.
- Delete 12 redundant pricing/<domain>.yaml files. They duplicated
  oci-sku-catalog.yaml with worse abstractions and were nobody's
  source of truth (no tool consumed them).
- Migrate the genuinely valuable knowledge from those 12 files
  (billing models, BYOL rules, free-tier rules, ECPU vs OCPU,
  X11M elastic model, hyperscaler comparisons, service nuances)
  into kb/field-knowledge/pricing-knowledge.yaml — non-numeric,
  no refresh needed.
- Result: pricing freshness check goes from 13 stale files to 0.

KB freshness automation:
- Add tools/kb_freshness.py — wrapper around kb_linter.check_freshness()
  with --check, --auto-refresh, --json, --quiet modes. Bridges stale
  files to their refresh tools (SKU catalog, compute domain, arch
  center). Wired into the welcome flow as a pre-flight banner that
  asks the user before refreshing.
- Fix pre-existing kb_linter bug: it crashed on the 45 multi-doc
  YAML files (frontmatter + body pattern) because it used safe_load
  instead of safe_load_all. Freshness check was effectively dead.
- Standardize timestamp field: linter now accepts last_verified,
  last_updated, and last_refreshed; refresh_arch_catalog writes
  last_verified instead of last_refreshed.
- Add make freshness / make freshness-refresh targets.

Skill alignment with Anthropic Agent Skills best practices:
- Rename `name: OCI Deal Accelerator` to `oci-deal-accelerator`
  to comply with the [a-z0-9-]{1,64} spec.
- Refactor SKILL.md from 674 to 445 lines via progressive disclosure:
  extract WA review output format, ECAL readiness format, and output
  conventions into docs/skill/*.md referenced from the main file.
- Add scripts/sync-skill.py + make sync-skill: source of truth is
  root SKILL.md, .agents/skills/oci-deal-accelerator/SKILL.md is
  auto-generated. make lint validates sync.
- Add evaluations/ with 3 manual baseline scenarios (welcome-flow,
  full-proposal, wa-review) per the Anthropic best-practices guidance
  to "build evaluations first."

Welcome flow hardening:
- Tighten CLAUDE.md to MANDATE reading SKILL.md before showing the
  menu (no improvising), and document the freshness pre-flight check
  with the ask-before-refresh user flow.
- Update SKILL.md welcome flow to instruct: parse kb_freshness JSON,
  show banner with stale count + oldest file, prompt user to refresh
  (only when an automated tool exists), fall back silently on errors.

Linter hygiene (zero remaining issues):
- Expand config/kb-tags.yaml taxonomy with features, operations,
  metrics, limitations sections covering 31 previously-unknown tags
  used in field findings (rac, ecpu, refreshable-clone, hnsw, etc.).
- Assign owners for kb/compatibility/, kb/competitive/,
  kb/well-architected/ (Diego Cabrera as default until team grows);
  kb/pricing/ marked as "Auto-refreshed" since it no longer needs
  human ownership.
- kb_linter accepts top-level `date` as fallback for contributor
  block; migrate FF-202603-008 from legacy `reported_by` to
  contributor block.
- Result: linter goes from 45 issues to 0.

Other:
- Recompute estimation_helpers monthly values in compute.yaml after
  the price refresh (they were derived from the old E5/A1 numbers).
- Add kb/README.md — contributor guide (directory map, frontmatter
  spec, refresh tooling, review cadence).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 23:59:32 -03:00

329 lines
13 KiB
Python

#!/usr/bin/env python3
"""
Refresh the Architecture Center catalog from Oracle docs.
Usage:
python tools/refresh_arch_catalog.py --url https://docs.oracle.com/en/solutions/{slug}/index.html
python tools/refresh_arch_catalog.py --whats-new
python tools/refresh_arch_catalog.py --validate
This script:
1. Fetches the What's New pages for 2024 and 2025
2. Extracts all reference architecture links
3. For each link, fetches the detail page
4. Extracts title, summary, services, recommendations
5. Writes/updates kb/architecture-center/catalog.yaml
IMPORTANT: This is a semi-automated tool. It generates DRAFT entries
that should be reviewed by a human before committing. The auto-generated
summaries may miss nuance or include irrelevant detail.
Requirements: pip install requests beautifulsoup4 pyyaml
"""
import argparse
import os
import re
import sys
import time
from datetime import datetime
from urllib.parse import urljoin
import requests
import yaml
from bs4 import BeautifulSoup
CATALOG_PATH = os.path.join(
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"kb", "architecture-center", "catalog.yaml",
)
WHATS_NEW_URLS = [
"https://docs.oracle.com/en/solutions/oracle-architecture-center/whats-new-20251.html",
"https://docs.oracle.com/en/solutions/oracle-architecture-center/whats-new-2024.html",
]
# ---------------------------------------------------------------------------
# Service name detection patterns
# ---------------------------------------------------------------------------
SERVICE_PATTERNS = {
"adb-s": r"Autonomous\s+(Database|Transaction|Data\s+Warehouse)\s+Serverless|ADB-S|ATP\s+Serverless|Autonomous\s+(Database|Transaction|Data\s+Warehouse)(?!.*Dedicated)",
"adb-d": r"Autonomous\s+Database\s+on\s+Dedicated|ADB-D|Dedicated\s+Exadata\s+Infrastructure",
"exacs": r"Exadata\s+(Cloud|Database)\s+Service|ExaCS|ExaDB-D",
"exascale": r"Exascale",
"base-db": r"Base\s+Database|VM\s+DB|DB\s+System|Oracle\s+Database\s+Service",
"oke": r"Kubernetes\s+Engine|OKE|Container\s+Engine\s+for\s+Kubernetes",
"compute": r"Compute\s+(Instance|VM)|Virtual\s+Machine|Bare\s+Metal",
"functions": r"OCI\s+Functions|Functions\s+Service",
"vcn": r"Virtual\s+Cloud\s+Network|VCN",
"load-balancer": r"Load\s+Balanc",
"waf": r"Web\s+Application\s+Firewall|WAF",
"fastconnect": r"FastConnect",
"drg": r"Dynamic\s+Routing\s+Gateway|DRG",
"object-storage": r"Object\s+Storage",
"block-storage": r"Block\s+Volume|Block\s+Storage",
"file-storage": r"File\s+Storage|FSS",
"data-safe": r"Data\s+Safe",
"vault": r"OCI\s+Vault|Key\s+Management|Key\s+Vault",
"cloud-guard": r"Cloud\s+Guard",
"goldengate": r"GoldenGate",
"oic": r"Oracle\s+Integration(?!\s+3)|OIC",
"oic3": r"Oracle\s+Integration\s+3",
"data-integration": r"Data\s+Integration",
"streaming": r"OCI\s+Streaming|Streaming\s+Service",
"monitoring": r"OCI\s+Monitoring|Stack\s+Monitoring",
"logging": r"Logging\s+Analytics",
"bastion": r"Bastion",
"apex": r"Oracle\s+APEX|APEX\s+application",
"mysql": r"MySQL\s+(Database\s+)?Service|MySQL\s+HeatWave",
"nosql": r"NoSQL\s+Database",
"postgresql": r"PostgreSQL",
"opensearch": r"OpenSearch",
"redis": r"OCI\s+Cache|Redis",
"adw": r"Autonomous\s+Data\s+Warehouse|ADW",
"api-gateway": r"API\s+Gateway",
"dns": r"OCI\s+DNS|DNS\s+Service",
"wls": r"WebLogic",
"soa": r"SOA\s+Suite",
"genai": r"Generative\s+AI|GenAI|OCI\s+AI\s+Agent",
"ai-services": r"OCI\s+AI\s+Services|Vision\s+AI|Language\s+AI|Speech\s+AI|Document\s+Understanding",
"data-science": r"Data\s+Science|ML\s+Pipeline",
"data-catalog": r"Data\s+Catalog",
"ops-insights": r"Ops\s+Insights|Operations\s+Insights",
"apm": r"Application\s+Performance\s+Monitoring|APM",
"fsdr": r"Full\s+Stack\s+Disaster\s+Recovery|FSDR",
"ocvs": r"VMware\s+Solution|OCVS",
"secure-desktops": r"Secure\s+Desktops",
"azure": r"Database@Azure|Oracle\s+Database@Azure",
"aws": r"Database@AWS|Oracle\s+Database@AWS",
"google-cloud": r"Database@Google|Oracle\s+Database@Google",
}
TAG_PATTERNS = {
"database": r"database|autonomous|exadata|rac|mysql|nosql|postgresql|base\s+db",
"migration": r"migrat|move\s+to|zero\s+downtime\s+migration|zdm|data\s+pump",
"ha-dr": r"high\s+availability|disaster\s+recovery|data\s+guard|failover|MAA|standby|far\s+sync",
"security": r"secur|encrypt|vault|data\s+safe|cloud\s+guard|compliance|zero.trust|tls|certificate",
"networking": r"network|vcn|subnet|fastconnect|vpn|drg|load\s+balanc|dns|hub.spoke",
"multicloud": r"multicloud|multi-cloud|azure|aws|google\s+cloud|database@",
"integration": r"integrat|oic|streaming|goldengate|data\s+flow|kafka",
"data-platform": r"data\s+lake|data\s+warehouse|analytics|data\s+platform|lakehouse",
"application": r"apex|ords|kubernetes|container|weblogic|tomcat|microservice|e-business|peoplesoft|siebel|jd\s+edwards",
"ai-ml": r"machine\s+learning|ai\s+|generative|vector|select\s+ai|llm|rag|chatbot|agent",
"autonomous": r"autonomous\s+database|adb|auto-scal",
"observability": r"monitor|logging|apm|ops\s+insights|observability|stack\s+monitoring",
"devops": r"ci/cd|devops|github|gitlab|jenkins|pipeline|gitops",
"hpc": r"hpc|gpu|bare\s+metal\s+gpu|high.performance\s+computing",
"iot": r"iot|internet\s+of\s+things",
"healthcare": r"healthcare|dicom|hl7|fhir",
"ebs": r"e-business\s+suite|ebs",
"peoplesoft": r"peoplesoft",
"vmware": r"vmware|ocvs|vsan",
}
def detect_services(text):
"""Auto-detect OCI services mentioned in text."""
found = []
for svc, pattern in SERVICE_PATTERNS.items():
if re.search(pattern, text, re.IGNORECASE):
found.append(svc)
return sorted(set(found))
def detect_tags(text):
"""Auto-detect topic tags from text."""
found = []
for tag, pattern in TAG_PATTERNS.items():
if re.search(pattern, text, re.IGNORECASE):
found.append(tag)
return sorted(set(found))
def fetch_page(url, delay=1.0):
"""Fetch a page with rate limiting."""
time.sleep(delay)
headers = {"User-Agent": "OCI-Deal-Accelerator-CatalogBuilder/1.0"}
resp = requests.get(url, headers=headers, timeout=30)
resp.raise_for_status()
return BeautifulSoup(resp.text, "html.parser")
def extract_entry(url):
"""Extract catalog entry from a reference architecture page."""
try:
soup = fetch_page(url)
title = soup.find("h1")
title_text = title.get_text(strip=True) if title else "Unknown"
# Get all text for service/tag detection
body = soup.get_text(" ", strip=True)
# Extract first 2-3 meaningful sentences as summary
paragraphs = soup.find_all("p")
summary_parts = []
for p in paragraphs[:8]:
text = p.get_text(strip=True)
if len(text) > 50 and "Copyright" not in text and "cookie" not in text.lower():
summary_parts.append(text)
if len(" ".join(summary_parts)) > 300:
break
summary = " ".join(summary_parts)[:500]
# Detect services and tags
services = detect_services(body)
tags = detect_tags(body)
# Detect type
entry_type = "reference-architecture"
if "playbook" in body.lower():
entry_type = "solution-playbook"
elif "built and deployed" in body.lower():
entry_type = "built-deployed"
# Find terraform/github links
terraform = None
for a in soup.find_all("a", href=True):
href = a["href"]
if "github.com" in href.lower() and "oracle" in href.lower():
terraform = href
break
return {
"title": title_text,
"url": url,
"type": entry_type,
"services": services,
"tags": tags,
"summary": summary,
"terraform": terraform,
}
except Exception as e:
print(f" ERROR fetching {url}: {e}", file=sys.stderr)
return None
def extract_whats_new_links(url):
"""Extract all reference architecture links from a What's New page."""
soup = fetch_page(url, delay=0.5)
links = []
for a in soup.find_all("a", href=True):
href = a["href"]
if "/en/solutions/" in href and href not in (
"https://docs.oracle.com/en/solutions/oracle-architecture-center/",
):
# Normalize URL
if href.startswith("/"):
href = urljoin("https://docs.oracle.com", href)
if not href.endswith("/index.html") and not href.endswith("/"):
href = href.rstrip("/") + "/index.html"
title = a.get_text(strip=True)
if title and len(title) > 10:
links.append({"url": href, "title": title})
return links
def load_catalog():
"""Load existing catalog if present."""
if os.path.exists(CATALOG_PATH):
with open(CATALOG_PATH, "r") as f:
data = yaml.safe_load(f)
return data if data else {"entries": []}
return {"entries": []}
def save_catalog(data):
"""Save catalog to YAML."""
os.makedirs(os.path.dirname(CATALOG_PATH), exist_ok=True)
with open(CATALOG_PATH, "w") as f:
f.write("# =============================================================================\n")
f.write("# ORACLE ARCHITECTURE CENTER — REFERENCE CATALOG\n")
f.write("# =============================================================================\n")
f.write("#\n")
f.write("# Index of Oracle Architecture Center content for the Deal Accelerator.\n")
f.write("# Used to match customer architectures with official Oracle reference designs.\n")
f.write("#\n")
f.write(f"# Last refreshed: {datetime.now().strftime('%Y-%m-%d')}\n")
f.write(f"# Entry count: {len(data.get('entries', []))}\n")
f.write("# To refresh: python tools/refresh_arch_catalog.py --whats-new\n")
f.write("# =============================================================================\n\n")
yaml.dump(data, f, default_flow_style=False, allow_unicode=True, sort_keys=False, width=120)
print(f"Saved {len(data.get('entries', []))} entries to {CATALOG_PATH}")
def validate_catalog():
"""Validate catalog entries have required fields."""
data = load_catalog()
entries = data.get("entries", [])
errors = 0
required = ["title", "url", "type", "tags", "summary"]
for i, entry in enumerate(entries):
for field in required:
if not entry.get(field):
print(f" Entry {i}: missing '{field}'{entry.get('title', 'UNKNOWN')}")
errors += 1
if entry.get("type") not in ("reference-architecture", "solution-playbook", "built-deployed"):
print(f" Entry {i}: invalid type '{entry.get('type')}'{entry.get('title')}")
errors += 1
print(f"\nValidated {len(entries)} entries, {errors} errors found.")
return errors == 0
def main():
parser = argparse.ArgumentParser(description="Refresh Architecture Center catalog")
parser.add_argument("--url", help="Fetch a single URL and print YAML entry")
parser.add_argument("--whats-new", action="store_true", help="Crawl What's New pages for new entries")
parser.add_argument("--validate", action="store_true", help="Validate existing catalog")
parser.add_argument("--output", default=CATALOG_PATH, help="Output file path")
args = parser.parse_args()
if args.validate:
ok = validate_catalog()
sys.exit(0 if ok else 1)
if args.url:
entry = extract_entry(args.url)
if entry:
print(yaml.dump([entry], default_flow_style=False, allow_unicode=True))
else:
print("Failed to extract entry.", file=sys.stderr)
sys.exit(1)
elif args.whats_new:
catalog = load_catalog()
existing_urls = {e["url"] for e in catalog.get("entries", [])}
new_count = 0
for wn_url in WHATS_NEW_URLS:
print(f"Fetching: {wn_url}")
links = extract_whats_new_links(wn_url)
print(f" Found {len(links)} links")
for link in links:
url = link["url"]
if url in existing_urls:
continue
print(f" Processing: {link['title'][:60]}...")
entry = extract_entry(url)
if entry:
catalog.setdefault("entries", []).append(entry)
existing_urls.add(url)
new_count += 1
catalog["last_verified"] = datetime.now().strftime("%Y-%m-%d")
catalog["source"] = "https://docs.oracle.com/en/solutions/oracle-architecture-center/"
catalog["entry_count"] = len(catalog.get("entries", []))
save_catalog(catalog)
print(f"Added {new_count} new entries.")
else:
parser.print_help()
if __name__ == "__main__":
main()