Diagram tooling: make targets, CI gate, LLM-rewrite hook, refresh docs

Implements points 3–8 of the post-procedure follow-up.

- Make targets: diagram-lookup, diagram-validate-spec, diagram-spec-audit,
  archcenter-descriptions-refresh, archcenter-smoke. Promotes the new
  tools out of "remember the CLI" into discoverable build-automation.
- .gitea/workflows/diagram-validators.yaml — runs spec validator across
  every absolute_layout spec on push/PR, plus a drawio re-render smoke
  test on the canonical example. Hard fail blocks merges.
- archcenter_pattern_lookup --llm-rewrite — opt-in (ANTHROPIC_API_KEY
  env var, mirroring DRAWIO_EXE pattern). Rewrites natural-language
  queries into canonical OCI terminology before scoring; falls back to
  the original query on any error.
- diagram_spec_validator — documents the policy on legacy archcenter
  reproductions: LABEL_NEAR_PARENT_EDGE stays a warning so verbatim
  reconstructions don't lose pixel-fidelity from cosmetic edits.
- README — documents the asset refresh procedure (downloader +
  description fetcher are idempotent, refresh quarterly).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-04-25 21:37:28 -03:00
parent b30a4f0d32
commit e5a1959585
5 changed files with 193 additions and 1 deletions

View File

@@ -0,0 +1,75 @@
# Diagram-validator gate.
#
# Runs on every push and PR. Two checks:
#
# 1. diagram-spec-audit — every absolute_layout spec under examples/
# passes tools/diagram_spec_validator.py (geometry rules:
# CONTAINER_TOO_THIN, CONTAINER_PADDING_VIOLATION,
# LABEL_OVERFLOW_PARENT). Hard fail surfaces the regressions
# Diego flagged on the MySQL HeatWave HA example.
#
# 2. drawio-validators-smoke — re-renders the canonical example with
# tools/oci_diagram_gen.py so the post-render
# drawio_visual_validator runs (font sizes, dangling edges,
# duplicate ids, off-canvas geometry).
#
# Failures block the merge so a busted geometry never ships.
name: Diagram validators
on:
push:
branches: [main]
paths:
- 'examples/**'
- 'kb/diagram/**'
- 'tools/diagram_spec_validator.py'
- 'tools/drawio_visual_validator.py'
- 'tools/oci_diagram_gen.py'
- 'tools/oci_pptx_diagram_gen.py'
pull_request:
branches: [main]
workflow_dispatch:
jobs:
diagram-validators:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
run: |
apt-get update -qq
apt-get install -y --no-install-recommends python3 python3-pip python3-yaml
python3 --version
- name: Install minimal deps
run: |
python3 -m pip install --quiet --upgrade pip
python3 -m pip install --quiet drawpyo lxml pyyaml
- name: Spec validator audit (every absolute_layout spec)
run: |
set -e
total=0; failed=0
for f in $(find examples -name 'diagram-spec.yaml' -o -name '*-diagram-spec.yaml'); do
total=$((total+1))
if ! python3 tools/diagram_spec_validator.py --spec "$f" --strict; then
failed=$((failed+1))
fi
done
echo "Validated $total spec(s); $failed hard-failed."
test $failed -eq 0
- name: Re-render canonical example (drawio post-render validator)
run: |
if [ -f examples/aws-app-mysql-heatwave-ha/specs/diagram-spec.yaml ]; then
python3 tools/oci_diagram_gen.py \
--spec examples/aws-app-mysql-heatwave-ha/specs/diagram-spec.yaml \
--output /tmp/aws-app-mysql-heatwave-ha.drawio --strict
else
echo "Canonical example missing — skipping drawio smoke test"
fi

View File

@@ -1,6 +1,6 @@
# OCI Deal Accelerator — Build Automation # OCI Deal Accelerator — Build Automation
.PHONY: help install test validate example diagram deck full clean lint codex-package update-icons freshness freshness-refresh sync-skill sku-discover pptx-icons-refresh archcenter-benchmark-20 .PHONY: help install test validate example diagram deck full clean lint codex-package update-icons freshness freshness-refresh sync-skill sku-discover pptx-icons-refresh archcenter-benchmark-20 diagram-lookup diagram-validate-spec archcenter-descriptions-refresh diagram-spec-audit archcenter-smoke
# Use venv if present, otherwise find best available python3 # Use venv if present, otherwise find best available python3
ifneq (,$(wildcard .venv/bin/python)) ifneq (,$(wildcard .venv/bin/python))
@@ -83,6 +83,35 @@ archcenter-benchmark-20: ## Run 20-case Oracle Architecture Center native benchm
--fidelity-threshold 0.90 \ --fidelity-threshold 0.90 \
--output-root examples/eval-2026-04-25-archcenter-native-20-v8 --output-root examples/eval-2026-04-25-archcenter-native-20-v8
diagram-lookup: ## Find Architecture Center reference patterns. Usage: make diagram-lookup QUERY="mysql heatwave HA"
@if [ -z "$(QUERY)" ]; then echo "Usage: make diagram-lookup QUERY=\"<topology>\""; exit 1; fi
$(PYTHON) tools/archcenter_pattern_lookup.py "$(QUERY)" --top $${TOP:-5}
diagram-validate-spec: ## Validate a diagram spec's geometry. Usage: make diagram-validate-spec SPEC=path/to/spec.yaml
@if [ -z "$(SPEC)" ]; then echo "Usage: make diagram-validate-spec SPEC=<path>"; exit 1; fi
$(PYTHON) tools/diagram_spec_validator.py --spec "$(SPEC)" --strict
diagram-spec-audit: ## Run spec validator across every diagram-spec in examples/, print pass/fail summary
@echo "Auditing diagram specs under examples/..."
@total=0; failed=0; \
for f in $$(find examples -name 'diagram-spec.yaml' -o -name '*-diagram-spec.yaml'); do \
total=$$((total+1)); \
$(PYTHON) tools/diagram_spec_validator.py --spec "$$f" --strict >/dev/null 2>&1 || { \
failed=$$((failed+1)); echo " FAIL $$f"; \
}; \
done; \
echo "Audited $$total spec(s); $$failed failed."
archcenter-descriptions-refresh: ## Re-fetch _description.md for every Architecture Center entry
$(PYTHON) tools/archcenter_description_fetcher.py --limit 200 --sleep 0.5
archcenter-smoke: ## 3-case Architecture Center reconstruction smoke test (CI-friendly)
$(PYTHON) tools/oci_archcenter_batch.py \
--limit 3 \
--threshold 0.78 \
--fidelity-threshold 0.85 \
--output-root tmp/archcenter-smoke
clean: ## Remove generated output files clean: ## Remove generated output files
rm -f examples/sample-output/*.drawio rm -f examples/sample-output/*.drawio
rm -f examples/sample-output/*.pptx rm -f examples/sample-output/*.pptx

View File

@@ -178,8 +178,13 @@ python tools/archcenter_pattern_lookup.py "fastconnect exacs cross region" --top
# Re-fetch description text after a catalog refresh # Re-fetch description text after a catalog refresh
python tools/archcenter_description_fetcher.py --limit 200 python tools/archcenter_description_fetcher.py --limit 200
# Re-download cached drawio/svg/png assets after a catalog refresh
python tools/archcenter_zip_downloader.py # idempotent; skips slugs whose folder already has a .drawio
``` ```
The cached assets under `kb/diagram/assets/archcenter-refs/` (~83MB) are committed so the skill works offline. Refresh quarterly or when `refresh_arch_catalog.py --whats-new` adds entries — both downloader and description fetcher are idempotent and only fetch what's missing.
### KB Health & Freshness ### KB Health & Freshness
The KB is automatically monitored for staleness and broken links: The KB is automatically monitored for staleness and broken links:

View File

@@ -33,7 +33,12 @@ as the basis for your absolute_layout authoring.
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import json
import os
import re import re
import sys
import urllib.error
import urllib.request
from pathlib import Path from pathlib import Path
import yaml import yaml
@@ -100,6 +105,60 @@ def _expand_query_tokens(tokens: set[str]) -> set[str]:
return expanded return expanded
def _llm_rewrite_query(query: str) -> str:
"""Rewrite ``query`` into canonical OCI/Oracle terminology via the
Anthropic API.
Opt-in: requires ``ANTHROPIC_API_KEY`` in the environment. Returns
the original query unchanged on any error or when the key is
missing — never fails the lookup. The synonym table covers the
common abbreviations; this hook is for natural-language queries
where rule-based expansion alone misses the intent.
"""
api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
print("[lookup] --llm-rewrite skipped: ANTHROPIC_API_KEY not set",
file=sys.stderr)
return query
model = os.environ.get("ANTHROPIC_MODEL", "claude-haiku-4-5-20251001")
payload = {
"model": model,
"max_tokens": 120,
"system": (
"Rewrite the user's free-text architecture query into a "
"concise list of canonical OCI and cloud terms. Expand "
"abbreviations (LB → load balancer, ADG → active data "
"guard, AD → availability domain, etc.). Do NOT add "
"topology elements the user did not request. Output ONLY "
"the rewritten query as a single space-separated phrase, "
"no preface, no quotes."
),
"messages": [{"role": "user", "content": query}],
}
req = urllib.request.Request(
"https://api.anthropic.com/v1/messages",
data=json.dumps(payload).encode("utf-8"),
headers={
"content-type": "application/json",
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
},
)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
body = json.loads(resp.read().decode("utf-8"))
except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, ValueError) as exc:
print(f"[lookup] --llm-rewrite failed: {exc}; using original query",
file=sys.stderr)
return query
blocks = body.get("content") or []
text = " ".join(b.get("text", "") for b in blocks if b.get("type") == "text").strip()
if not text:
return query
print(f"[lookup] llm-rewrote: {query!r}{text!r}", file=sys.stderr)
return text
def _description_text(entry: dict) -> str: def _description_text(entry: dict) -> str:
"""Read the cached `_description.md` body if present. """Read the cached `_description.md` body if present.
@@ -251,6 +310,11 @@ def main() -> None:
parser.add_argument("--no-synonyms", action="store_true", parser.add_argument("--no-synonyms", action="store_true",
help="Disable query-token expansion via " help="Disable query-token expansion via "
"kb/architecture-center/synonyms.yaml.") "kb/architecture-center/synonyms.yaml.")
parser.add_argument("--llm-rewrite", action="store_true",
help="Rewrite the query via the Anthropic API "
"before scoring (opt-in; requires "
"ANTHROPIC_API_KEY). Useful for natural-language "
"queries the synonym table doesn't cover.")
args = parser.parse_args() args = parser.parse_args()
queries: list[str] = list(args.queries) queries: list[str] = list(args.queries)
@@ -260,6 +324,8 @@ def main() -> None:
parser.error("Provide a positional query or --queries.") parser.error("Provide a positional query or --queries.")
expand = not args.no_synonyms expand = not args.no_synonyms
if args.llm_rewrite:
queries = [_llm_rewrite_query(q) for q in queries]
if args.format == "yaml": if args.format == "yaml":
bundle = {q: lookup(q, top=args.top, expand_synonyms=expand) for q in queries} bundle = {q: lookup(q, top=args.top, expand_synonyms=expand) for q in queries}
print(yaml.safe_dump(bundle if len(queries) > 1 else bundle[queries[0]], print(yaml.safe_dump(bundle if len(queries) > 1 else bundle[queries[0]],

View File

@@ -29,6 +29,23 @@ Returns a dict with:
- issues: [{"severity", "code", "message", "id"}] - issues: [{"severity", "code", "message", "id"}]
Severity ``error`` → renderers raise. ``warn`` → printed to stderr. Severity ``error`` → renderers raise. ``warn`` → printed to stderr.
Policy on legacy archcenter reproductions
-----------------------------------------
Specs under ``examples/eval-*-archcenter-native-*/`` are auto-generated
reconstructions of Oracle Architecture Center references. Oracle's own
geometry sometimes places labels 2-5px from container edges, and the
reproductions mirror those coordinates verbatim to keep the perceptual
diff against the canonical PNGs above the fidelity threshold. Touching
them to silence ``LABEL_NEAR_PARENT_EDGE`` (severity=warn) would risk
the pixel-fidelity scores tracked by ``archcenter_batch_driver``.
The validator therefore distinguishes:
- ``LABEL_OVERFLOW_PARENT`` (error, blocks render): label bottom is
OUTSIDE the parent — true regression.
- ``LABEL_NEAR_PARENT_EDGE`` (warn, never blocks): label sits within
6px but still inside — common in Oracle's own ref archs.
""" """
from __future__ import annotations from __future__ import annotations