2 Commits

Author SHA1 Message Date
root
c22ba64bdb Brand icons (Slack/Jira) in diagrams + BOM comparison deck
All checks were successful
Deploy Skill to OCI / deploy (push) Successful in 35s
SKILL.md sync / check-sync (push) Successful in 18s
- brand_icon_catalog: shared resolver for non-OCI vendor assets (Slack, Jira, Jira Service Management) under kb/diagram/brand-icons; official > fallback; never hits the network at render time.
- oci_diagram_gen + oci_pptx_diagram_gen: services and external actors accept `brand_icon:`, unlocking real multicloud / SaaS topologies beyond Oracle stencils.
- build_bom_comparison_deck: customer-facing Oracle FY26 deck that compares N BOM specs with native PowerPoint charts and live SKU pricing.
- README + SKILL.md: document the new capabilities and ship copy/paste demo prompts (native .pptx diagram, Slack→OIC→Jira flow, multicloud Postgres↔ADB-S, discount-aware BOM).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:23:50 -03:00
root
7ea17b2f9e Skill: catalog-first pricing, centralised disclaimer, deck verification
Six fixes after a Codex transcript that scraped oracle.com for SKUs already
in kb/pricing/oci-sku-catalog.yaml. New option-4 cost-estimate flow with a
"catalog first, web never" rule, staleness gate, and miss policy; unconditional
Cookbook scope (any pricing or SKU work, not just specific tool calls); hard
no-WebSearch rule under "What You Do NOT Do"; disclaimer field extended with
reference-only / no-client-discount clauses and referenced from options 4/8/13
(single source of truth — oci_bom_gen.py already consumes it); tightened menu
hints separating option 4 (quick comparison while you scope) from option 13
(formal .xlsx artefact you send); structural deck verification step for
Linux/WSL where PowerPoint binary isn't available.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:23:48 -03:00
6 changed files with 807 additions and 18 deletions

View File

@@ -88,7 +88,7 @@ Present these options as a compact numbered list. Each option has a bold title f
1. 📋 Full proposal — *notes → architecture + deck + native PPTX diagram + costs* 1. 📋 Full proposal — *notes → architecture + deck + native PPTX diagram + costs*
2. 📐 Architecture diagram — *description → .drawio or native PPTX* 2. 📐 Architecture diagram — *description → .drawio or native PPTX*
3. 📊 Slide deck — *architecture → .pptx with native OCI diagram* 3. 📊 Slide deck — *architecture → .pptx with native OCI diagram*
4. 💰 Cost estimate — *services + sizing → PAYG vs BYOL* 4. 💰 Cost estimate — *quick PAYG/BYOL comparison while you scope (terminal + optional deck)*
VALIDATE & CHECK VALIDATE & CHECK
───────────────── ─────────────────
@@ -112,13 +112,22 @@ Present these options as a compact numbered list. Each option has a bold title f
SA TOOLS SA TOOLS
───────────────── ─────────────────
13. 📦 BOM generator — *services + quantities → .xlsx Bill of Materials* 13. 📦 BOM generator — *formal .xlsx artefact you send to the customer*
14. 📤 BOM for AppCA — *BOM → .xlsx ready to import into AppCA* 14. 📤 BOM for AppCA — *BOM → .xlsx ready to import into AppCA*
━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━
Pick a number, or just describe what you need. Pick a number, or just describe what you need.
``` ```
### Example Direct Requests
If the user asks for examples, or skips the menu and writes a natural-language request, prompts like these map directly to the capabilities above:
- `Generame un diagrama nativo en .pptx, simple y prolijo, de esta arquitectura: usuarios externos -> load balancer público -> 2 VMs de aplicación en subnet privada -> Autonomous Database Serverless. Quiero que quede presentable para mostrar directo en PowerPoint. No me pidas YAML ni setup adicional.`
- `Generate a simple high-level integration diagram for this flow: Slack -> Oracle Integration Cloud (OIC) -> Jira Service Management -> My Oracle Support. Add clear Client, Oracle, and External layers. Use brand icons for Slack and Jira Service Management, and deliver both .drawio and .pptx.`
- `Generate a high-level architecture diagram for this scenario: 1 PostgreSQL in OCI Ashburn, accessed from GCP Virginia through interconnect. PostgreSQL connects internally to 1 Autonomous Database Serverless, which also has 1 refreshable clone in the same region but in a different AD. Deliver both .drawio and .pptx. Keep it simple, executive-friendly, and technically correct.`
- `Gere um BOM em USD para este cenário: PostgreSQL no OCI com 4 OCPU e 500 GB, ADB-S com 200 ECPU e 1 TB BYOL, 1 refreshable clone com o mesmo sizing e FastConnect de 1 Gbps. Aplique 11% de desconto, 12 meses, 24 horas por dia. Quero o arquivo .xlsx e um resumo claro do custo mensal e anual.`
### Behavior Rules ### Behavior Rules
- If the user picks **1**, ask: "Paste your discovery notes (meeting notes, emails, whatever you have)." - If the user picks **1**, ask: "Paste your discovery notes (meeting notes, emails, whatever you have)."
@@ -150,7 +159,16 @@ Pick a number, or just describe what you need.
Full reference: `docs/skill/output-formats.md` § "Standard diagram-generation procedure (MANDATORY)". Full reference: `docs/skill/output-formats.md` § "Standard diagram-generation procedure (MANDATORY)".
- If the user picks **3**, ask: "Describe the architecture or paste the spec. I'll generate the deck with a native OCI PowerPoint diagram when the architecture is structured enough." - If the user picks **3**, ask: "Describe the architecture or paste the spec. I'll generate the deck with a native OCI PowerPoint diagram when the architecture is structured enough."
- If the user picks **4**, ask: "What services and sizing? (e.g., 'ADB-S 8 OCPU + 2 VMs + FastConnect')" - If the user picks **4**, ask: "What services and sizing? (e.g., 'ADB-S 8 OCPU + 2 VMs + FastConnect'). License model — PAYG, BYOL, or both?"
Then follow the cost-estimate flow:
1. **Catalog first, web never.** `grep` [`kb/pricing/oci-sku-catalog.yaml`](kb/pricing/oci-sku-catalog.yaml) (auto-refreshed, authoritative) for the requested SKUs and consult [`docs/bom-cookbook.md`](docs/bom-cookbook.md) for copy-paste recipes (ExaCS X11M BYOL, ADB-Dedicated, ADB-S + Block, FastConnect, etc.). Use `kb/pricing/compute.yaml` for shape-level lookups. Do NOT reach for WebSearch / WebFetch / `oracle.com` for list prices — the catalog IS the source.
2. **Staleness gate.** Read `last_verified` at the top of the catalog. If >60 days stale, warn the user and offer `python tools/refresh_sku_catalog.py --refresh` before quoting. If <=60 days, proceed without web lookups.
3. **Catalog miss policy.** If a SKU is genuinely absent after `grep` (not "I didn't find it on first try"), state it explicitly, then run `python tools/refresh_sku_catalog.py --refresh` and re-grep. Only if a fresh catalog still doesn't surface it is `oracle.com/cloud/price-list/` an acceptable fallback — and cite the source URL next to the part number.
4. **Source extraction.** When the request references a customer artefact (`.pptx`, BOM, quote), extract the topology / sizing from the artefact first, list each scenario's components, then map to SKUs. Show the mapping (component → SKU) so the SA can audit it.
5. **Quote shape.** Per scenario list: SKU + product name + metric + list price USD + quantity + monthly subtotal. End with total $/month and total $/year. Cite the catalog's `last_verified` date in the output.
6. When both PAYG and BYOL are requested, present them side-by-side (same SKU rows, two price columns). Save the cost estimate to `examples/<customer>-cost-estimate.yaml` and present the formatted table in the terminal.
7. **Disclaimer (mandatory).** Read the `disclaimer:` field from `kb/pricing/oci-sku-catalog.yaml` and append it verbatim — as a footer line under the terminal table, and (if a deck is generated) as a final "Commercial Disclaimer" slide. Do NOT paraphrase or rewrite; the field already carries Oracle's sample-quote boilerplate plus the "reference only / no client-specific discounts" clauses. `tools/oci_bom_gen.py` already reads this field for BOM xlsx output.
8. **Deck verification (if a deck is generated).** Open the produced `.pptx` with `python-pptx` and assert: (a) slide count matches spec, (b) each slide's expected title is present, (c) chart types match what the spec asked for (e.g. `COLUMN_CLUSTERED`, `COLUMN_STACKED`), (d) the disclaimer slide is the last one. Print a one-line `deck OK: <N> slides, titles=[...]` summary so the SA can audit at a glance. Rasterizing to PNG via PowerPoint is not always available on Linux/WSL — structural validation is the floor, not optional.
- If the user picks **5**, ask: "Describe your architecture or paste the spec. I'll run the 5-pillar review." Then follow the WA review flow: - If the user picks **5**, ask: "Describe your architecture or paste the spec. I'll run the 5-pillar review." Then follow the WA review flow:
1. Parse input to build a workload profile YAML (flags) and architecture YAML 1. Parse input to build a workload profile YAML (flags) and architecture YAML
2. If input is a `.pptx` file, extract text content from all slides to infer architecture and workload context 2. If input is a `.pptx` file, extract text content from all slides to infer architecture and workload context
@@ -176,6 +194,7 @@ Pick a number, or just describe what you need.
8. Generate implementation roadmap based on engagement tier 8. Generate implementation roadmap based on engagement tier
9. Produce a 8-10 slide deck using Oracle FY26 template (`config/oracle-pptx-layouts.yaml` → `business_case` type), using a native OCI PowerPoint diagram when the architecture is structured enough 9. Produce a 8-10 slide deck using Oracle FY26 template (`config/oracle-pptx-layouts.yaml` → `business_case` type), using a native OCI PowerPoint diagram when the architecture is structured enough
10. Output: business-case.pptx + business-case.yaml (reusable spec) 10. Output: business-case.pptx + business-case.yaml (reusable spec)
11. **Disclaimer + deck verification.** Append the `disclaimer:` field from `kb/pricing/oci-sku-catalog.yaml` as a final "Commercial Disclaimer" slide (verbatim — see option 4 step 7). Then run the same structural deck verification described in option 4 step 8 (slide count, titles, chart types, disclaimer-last) before reporting completion.
- If the user picks **9**, ask: "What topic? (e.g., 'DEP', 'TAC', 'maintenance window', 'vector search')" - If the user picks **9**, ask: "What topic? (e.g., 'DEP', 'TAC', 'maintenance window', 'vector search')"
- If the user picks **10**, ask: "What kind of architecture? (e.g., 'ADB + APEX', 'cross-region DR', 'data lakehouse')" - If the user picks **10**, ask: "What kind of architecture? (e.g., 'ADB + APEX', 'cross-region DR', 'data lakehouse')"
- If the user picks **11**, switch to finding intake mode: - If the user picks **11**, switch to finding intake mode:
@@ -221,7 +240,7 @@ Pick a number, or just describe what you need.
**BOM Output Rules:** **BOM Output Rules:**
- NEVER include "Confidential: Internal ONLY" or any confidentiality marking - NEVER include "Confidential: Internal ONLY" or any confidentiality marking
- ALWAYS include the Oracle Cost Estimator disclaimer at the bottom - ALWAYS include the disclaimer from `kb/pricing/oci-sku-catalog.yaml` `disclaimer:` field at the bottom — `tools/oci_bom_gen.py` reads it automatically (do not duplicate, paraphrase, or maintain a second copy elsewhere)
- Only include SKUs the customer actually requested — never dump the full catalog - Only include SKUs the customer actually requested — never dump the full catalog
- Show cost proportions so the customer can see where their spend concentrates - Show cost proportions so the customer can see where their spend concentrates
- Use Excel formulas (not static values) so the customer can adjust quantities - Use Excel formulas (not static values) so the customer can adjust quantities
@@ -462,9 +481,9 @@ See [docs/skill/output-formats.md](docs/skill/output-formats.md) for the per-cus
## Cookbook: Building Tool Payloads ## Cookbook: Building Tool Payloads
Before mapping customer requirements to SKUs for `generate_bom`, `generate_bom_appca`, or `generate_cost_estimate`, check [docs/bom-cookbook.md](docs/bom-cookbook.md) — it has copy-paste recipes for common patterns (ExaCS X11M BYOL, ADB-Dedicated, ADB-S + Block + FastConnect) and explicitly names the gotchas that otherwise burn exploration turns (e.g. ADB-Dedicated shares infrastructure SKUs with ExaCS — there are no separate `adb_dedicated` SKUs in the catalog). Any pricing or SKU work — cost estimate (option 4), BOM (option 13), BOM-AppCA (option 14), business-case TCO (option 8), scenario comparison from a customer artefact (`.pptx`, BOM, quote) — starts in the local catalog, not on the web. Check [docs/bom-cookbook.md](docs/bom-cookbook.md) for copy-paste recipes (ExaCS X11M BYOL, ADB-Dedicated, ADB-S + Block + FastConnect) and the gotchas that otherwise burn exploration turns (e.g. ADB-Dedicated shares infrastructure SKUs with ExaCS — there are no separate `adb_dedicated` SKUs in the catalog).
If a requirement does not match a recipe, `grep` [`kb/pricing/oci-sku-catalog.yaml`](kb/pricing/oci-sku-catalog.yaml) and consult `kb/services/` before inventing SKUs. If a requirement does not match a recipe, `grep` [`kb/pricing/oci-sku-catalog.yaml`](kb/pricing/oci-sku-catalog.yaml) and consult `kb/services/` before inventing SKUs. See "What You Do NOT Do" for the web-lookup rule and `kb/pricing/oci-sku-catalog.yaml`'s `last_verified` for staleness handling.
--- ---
@@ -572,6 +591,7 @@ KB lives under `kb/`. See [kb/README.md](kb/README.md) for the directory map, fr
- You do NOT replace the architect's judgment. You accelerate it. - You do NOT replace the architect's judgment. You accelerate it.
- You do NOT generate pixel-perfect diagrams. You generate 80% drafts the architect refines. - You do NOT generate pixel-perfect diagrams. You generate 80% drafts the architect refines.
- You do NOT make up pricing. If you don't have current pricing, estimate ranges. - You do NOT make up pricing. If you don't have current pricing, estimate ranges.
- You do NOT use WebSearch / WebFetch / `oracle.com` for OCI list prices or SKU lookups. `kb/pricing/oci-sku-catalog.yaml` is auto-refreshed via `tools/refresh_sku_catalog.py` (Oracle apexapps pricing API) and is the source of truth — including for ECPU/OCPU rates, BYOL deltas, Exadata X11M part numbers, and ADB-D ECPU SKUs. Web fallback is allowed ONLY when (a) the SKU is genuinely absent after `grep` AND a fresh `--refresh` still does not surface it, or (b) the catalog's `last_verified` is >60 days stale and a refresh is not currently possible. In either case, cite the source URL alongside the part number. Reflexively reaching for WebSearch when the catalog already has the answer is the most common cost-estimate failure mode — don't.
- You do NOT claim features exist if you're unsure. Check the KB first. - You do NOT claim features exist if you're unsure. Check the KB first.
- You do NOT do detailed project management. DELIVER artifacts are lightweight handover aids. - You do NOT do detailed project management. DELIVER artifacts are lightweight handover aids.
- You do NOT add services or components the user did not request. - You do NOT add services or components the user did not request.

View File

@@ -195,7 +195,21 @@
"Bash(DRAWIO_EXE=\"/mnt/c/Program Files/draw.io/draw.io.exe\" .venv/bin/python tools/drawio_to_png.py --drawio examples/asis-tobe-adb-modernization/out/tobe-adb-d-active-dataguard.drawio --out tmp/drawio-renders/tobe-final.png)", "Bash(DRAWIO_EXE=\"/mnt/c/Program Files/draw.io/draw.io.exe\" .venv/bin/python tools/drawio_to_png.py --drawio examples/asis-tobe-adb-modernization/out/tobe-adb-d-active-dataguard.drawio --out tmp/drawio-renders/tobe-final.png)",
"Bash(DRAWIO_EXE=\"/mnt/c/Program Files/draw.io/draw.io.exe\" .venv/bin/python tools/drawio_to_png.py --drawio examples/asis-tobe-adb-modernization/out/asis-adb-s-local-dr.drawio --out tmp/drawio-renders/asis-final2.png)", "Bash(DRAWIO_EXE=\"/mnt/c/Program Files/draw.io/draw.io.exe\" .venv/bin/python tools/drawio_to_png.py --drawio examples/asis-tobe-adb-modernization/out/asis-adb-s-local-dr.drawio --out tmp/drawio-renders/asis-final2.png)",
"Bash(DRAWIO_EXE=\"/mnt/c/Program Files/draw.io/draw.io.exe\" .venv/bin/python tools/drawio_to_png.py --drawio examples/asis-tobe-adb-modernization/out/tobe-adb-d-active-dataguard.drawio --out tmp/drawio-renders/tobe-final2.png)", "Bash(DRAWIO_EXE=\"/mnt/c/Program Files/draw.io/draw.io.exe\" .venv/bin/python tools/drawio_to_png.py --drawio examples/asis-tobe-adb-modernization/out/tobe-adb-d-active-dataguard.drawio --out tmp/drawio-renders/tobe-final2.png)",
"Bash(.githooks/pre-commit)" "Bash(.githooks/pre-commit)",
"Bash(unzip -l awr_Transactional.zip)",
"Bash(mkdir -p /tmp/awr_meli)",
"Bash(unzip -o \"/mnt/c/Users/Diego/Documents/Meli/ABD-M/April 2026 - Fraude ADB-S to ADB-D/awr_Transactional.zip\")",
"Bash(cp \"/mnt/c/Users/Diego/Documents/Meli/ABD-M/April 2026 - Fraude ADB-S to ADB-D/meli-fraud-adbd-arch.pptx\" /tmp/meli-fraud-adbd-arch.pptx)",
"Bash(unzip -o meli-fraud-adbd-arch.pptx -d meli-pptx-extracted/)",
"Bash(cp -f \"/mnt/c/Users/Diego/Documents/Meli/ABD-M/April 2026 - Fraude ADB-S to ADB-D/meli-fraud-adbd-arch.pptx\" /tmp/meli-fraud-adbd-arch.pptx)",
"Bash(rm -rf /tmp/meli-pptx-extracted)",
"Bash(unzip -oq /tmp/meli-fraud-adbd-arch.pptx -d /tmp/meli-pptx-extracted/)",
"Bash(cp -f \"/mnt/c/Users/Diego/Documents/Meli/ABD-M/April 2026 - Fraude ADB-S to ADB-D/meli-fraud-adbd-arch.pptx\" meli-fraud-adbd-arch.pptx)",
"Bash(rm -rf meli-pptx-extracted)",
"Bash(unzip -oq meli-fraud-adbd-arch.pptx -d meli-pptx-extracted/)",
"Bash(yes)",
"WebFetch(domain:blog.shadabmohammad.com)",
"Bash(\"/mnt/c/Program Files/draw.io/draw.io.exe\" -x -f png --scale 1 -o \"$\\(pwd\\)/examples/output-aws-mysql-to-oci-heatwave-fsdr/aws-mysql-to-heatwave-fsdr.png\" \"$\\(pwd\\)/examples/output-aws-mysql-to-oci-heatwave-fsdr/aws-mysql-to-heatwave-fsdr.drawio\")"
] ]
} }
} }

View File

@@ -11,14 +11,17 @@ What normally takes an SA days of work (structuring notes, designing architectur
- **ECAL 3.1 native** — engagement RACI, artefact catalog, readiness scoring, and lessons learned per step baked into the workflow - **ECAL 3.1 native** — engagement RACI, artefact catalog, readiness scoring, and lessons learned per step baked into the workflow
- **Field knowledge, beyond the docs** — built-in KB with real gotchas, workarounds, and sizing lessons from production OCI deployments - **Field knowledge, beyond the docs** — built-in KB with real gotchas, workarounds, and sizing lessons from production OCI deployments
- **Honest about trade-offs** — flags OCI limitations and competitive gaps instead of overselling - **Honest about trade-offs** — flags OCI limitations and competitive gaps instead of overselling
- **Multi-cloud aware** — supports hybrid/multi-cloud diagrams (AWS, Azure, GCP icons) and considers options like ADB Multicloud before recommending full migration - **Multi-cloud aware** — supports hybrid/multi-cloud diagrams (AWS, Azure, GCP, plus external brand icons such as Slack and Jira) and considers options like ADB Multicloud before recommending full migration
- **MCP-tolerant inputs** — deck, diagram, BOM, and business-case generators accept flat MCP payloads and common field aliases instead of requiring one rigid schema
- **End-to-end coverage** — from discovery notes to go-live checklist, not just the architecture slide - **End-to-end coverage** — from discovery notes to go-live checklist, not just the architecture slide
## New user? Start here ## New user? Start here
The skill runs as a hosted MCP server. Connect once from your LLM client, sign in with Oracle SSO, and the tools are ready. The skill runs as a hosted MCP server. Connect once from your LLM client, sign in with Oracle SSO, and the tools are ready.
**1. Add the MCP server to your client:** Fastest path: open AI Factory Hub at `https://ai-lad.com/`, search for `OCI Deal Accelerator`, and install it from there. If you prefer manual setup, use the MCP client instructions below.
**1. Add the MCP server to your client (manual setup):**
| Client | How | | Client | How |
|---|---| |---|---|
@@ -45,6 +48,7 @@ The skill runs as a hosted MCP server. Connect once from your LLM client, sign i
### Connection URLs ### Connection URLs
- **AI Factory Hub:** `https://ai-lad.com/`
- **MCP (HTTP streamable):** `https://mcp.tech-lad.com/deal-accelerator/mcp/` - **MCP (HTTP streamable):** `https://mcp.tech-lad.com/deal-accelerator/mcp/`
- **SSE (Windsurf only):** `https://mcp.tech-lad.com/deal-accelerator/sse/` - **SSE (Windsurf only):** `https://mcp.tech-lad.com/deal-accelerator/sse/`
@@ -61,10 +65,12 @@ From unstructured input (meeting notes, emails, Slack threads), the skill genera
| **Business Case** — TCO, ROI, value drivers, risk assessment | .pptx | Define | Ideate | | **Business Case** — TCO, ROI, value drivers, risk assessment | .pptx | Define | Ideate |
| **Joint Engagement Plan** — scope, resources, timeline | YAML | Define | Plan | | **Joint Engagement Plan** — scope, resources, timeline | YAML | Define | Plan |
| **Discovery Questionnaire** — structured IT landscape collection | YAML | Design | Current | | **Discovery Questionnaire** — structured IT landscape collection | YAML | Design | Current |
| **Architecture Diagram**official Oracle visual style, multi-cloud support | .drawio | Design | Future | | **Architecture Diagram**Oracle reference-architecture geometry, multi-cloud + external brand support | .drawio / .pptx | Design | Future |
| **Slide Deck** — 6-15 slides scaled to engagement complexity | .pptx | Design | Confirm | | **Slide Deck** — 6-15 slides scaled to engagement complexity | .pptx | Design | Confirm |
| **Customer PDF** — branded, no internal KB references | .pdf | Design | Confirm | | **Customer PDF** — branded, no internal KB references | .pdf | Design | Confirm |
| **Cost Estimate** — BYOL vs PAYG breakdown with assumptions | YAML | Design | Future | | **Cost Estimate** — BYOL vs PAYG breakdown with assumptions | YAML | Design | Future |
| **Bill of Materials (BOM)** — requested SKUs only, formula-driven totals, discount-aware workbook | .xlsx | Design | Future |
| **AppCA-ready BOM** — AppCA import sheet plus full BOM workbook detail | .xlsx | Design | Future |
| **Well-Architected Scorecard** — 5-pillar automated validation | YAML | Design | Future | | **Well-Architected Scorecard** — 5-pillar automated validation | YAML | Design | Future |
| **Operations Model** — day-2 monitoring, patching, incident response | YAML | Design | Future | | **Operations Model** — day-2 monitoring, patching, incident response | YAML | Design | Future |
| **ECAL Readiness Scorecard** — 60-artefact gap analysis per phase | Text | All | All | | **ECAL Readiness Scorecard** — 60-artefact gap analysis per phase | Text | All | All |
@@ -100,9 +106,40 @@ deck + pdf ← + customer-facing PDF (branded, no internal refs)
pdf ← customer PDF only pdf ← customer PDF only
bizcase ← business case deck for customer approval bizcase ← business case deck for customer approval
full ← everything (pptx + drawio + docx + xlsx + pdf) full ← everything (pptx + drawio + docx + xlsx + pdf)
bom ← bill of materials (.xlsx)
deliver ← handover + go-live checklist + success criteria deliver ← handover + go-live checklist + success criteria
``` ```
Standalone architecture diagrams can be generated as editable `.drawio` files or as native Oracle-style `.pptx` diagrams/slides, depending on the workflow you trigger.
## Example Prompts
These copy/paste prompts are good demo asks for the newest user-visible features, especially when using Codex or another MCP client.
### Native PowerPoint diagram (.pptx)
```text
Generame un diagrama nativo en .pptx, simple y prolijo, de esta arquitectura: usuarios externos -> load balancer público -> 2 VMs de aplicación en subnet privada -> Autonomous Database Serverless. Quiero que quede presentable para mostrar directo en PowerPoint. No me pidas YAML ni setup adicional.
```
### Brand icons + OIC + dual output
```text
Generate a simple high-level integration diagram for this flow: Slack -> Oracle Integration Cloud (OIC) -> Jira Service Management -> My Oracle Support. Add clear Client, Oracle, and External layers. Use brand icons for Slack and Jira Service Management, and deliver both .drawio and .pptx.
```
### Real multicloud topology
```text
Generate a high-level architecture diagram for this scenario: 1 PostgreSQL in OCI Ashburn, accessed from GCP Virginia through interconnect. PostgreSQL connects internally to 1 Autonomous Database Serverless, which also has 1 refreshable clone in the same region but in a different AD. Deliver both .drawio and .pptx. Keep it simple, executive-friendly, and technically correct.
```
### Discount-aware BOM
```text
Gere um BOM em USD para este cenário: PostgreSQL no OCI com 4 OCPU e 500 GB, ADB-S com 200 ECPU e 1 TB BYOL, 1 refreshable clone com o mesmo sizing e FastConnect de 1 Gbps. Aplique 11% de desconto, 12 meses, 24 horas por dia. Quero o arquivo .xlsx e um resumo claro do custo mensal e anual.
```
## Knowledge Base ## Knowledge Base
The KB is the moat — field experience, not documentation regurgitation. The KB is the moat — field experience, not documentation regurgitation.
@@ -159,6 +196,7 @@ kb/services/dbexpert-api-reference.yaml # API endpoints and refresh procedure
Cached locally under `kb/diagram/assets/archcenter-refs/<slug>/`: Cached locally under `kb/diagram/assets/archcenter-refs/<slug>/`:
- `_description.md` — Oracle's architecture rationale (used by both the lookup tool and SKILL.md option 10 "Reference architecture lookup") - `_description.md` — Oracle's architecture rationale (used by both the lookup tool and SKILL.md option 10 "Reference architecture lookup")
- `*.drawio` — official editable source for ~110 references (where Oracle ships a zip) - `*.drawio` — official editable source for ~110 references (where Oracle ships a zip)
- `_template.yaml` — extracted `absolute_layout` scaffold so new diagrams can start from Oracle geometry instead of from scratch
- `*.svg` / `*.png` — raster fallback for the rest - `*.svg` / `*.png` — raster fallback for the rest
During **Phase 2 (DESIGN)**, the skill automatically matches the proposed architecture against the catalog: During **Phase 2 (DESIGN)**, the skill automatically matches the proposed architecture against the catalog:
@@ -183,7 +221,7 @@ python tools/archcenter_description_fetcher.py --limit 200
python tools/archcenter_zip_downloader.py # idempotent; skips slugs whose folder already has a .drawio 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. The cached assets under `kb/diagram/assets/archcenter-refs/` (~83MB) are committed so the skill works offline. Lookup uses a local index for near-instant matches, and can be rebuilt manually when needed. 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
@@ -362,6 +400,8 @@ If you paste discovery notes directly, the skill skips the menu and goes straigh
## Tools ## Tools
All major generators accept either the canonical nested YAML specs or the flatter MCP payload aliases used by hosted clients.
```bash ```bash
# Slide deck generation (technical proposal) # Slide deck generation (technical proposal)
python tools/oci_deck_gen.py --spec examples/proposal-spec.yaml --output proposal.pptx python tools/oci_deck_gen.py --spec examples/proposal-spec.yaml --output proposal.pptx
@@ -377,6 +417,9 @@ python tools/oci_bizcase_gen.py --spec business-case.yaml --output business-case
# § "Standard diagram-generation procedure (MANDATORY)" for the full # § "Standard diagram-generation procedure (MANDATORY)" for the full
# workflow: ref-arch lookup → pre-generation review → spec authoring # workflow: ref-arch lookup → pre-generation review → spec authoring
# → automatic spec validator → render → visual verification. # → automatic spec validator → render → visual verification.
# Supports OCI fallback aliases for services whose official toolkit icons
# are still missing (for example PostgreSQL/Redis) plus `brand_icon` for
# third-party systems such as Slack and Jira in both drawio and PPTX.
python tools/archcenter_pattern_lookup.py "<topology keywords>" # 1. find canonical Oracle reference python tools/archcenter_pattern_lookup.py "<topology keywords>" # 1. find canonical Oracle reference
python tools/oci_diagram_gen.py --spec examples/diagram-spec.yaml --output arch.drawio # 2. render (validators run automatically) python tools/oci_diagram_gen.py --spec examples/diagram-spec.yaml --output arch.drawio # 2. render (validators run automatically)
python tools/oci_pptx_render.py --pptx arch.pptx --output arch.png --width 1600 # 3. rasterize PPTX for visual review python tools/oci_pptx_render.py --pptx arch.pptx --output arch.png --width 1600 # 3. rasterize PPTX for visual review
@@ -389,11 +432,17 @@ python tools/drawio_visual_validator.py arch.drawio
# Output orchestrator (multiple formats at once) # Output orchestrator (multiple formats at once)
python tools/oci_output.py --spec examples/proposal-spec.yaml --format full --output-dir output/ python tools/oci_output.py --spec examples/proposal-spec.yaml --format full --output-dir output/
python tools/oci_output.py --spec examples/exacs-bom-spec.yaml --format bom --output-dir output/
# BOM / AppCA workbook
python tools/oci_bom_gen.py --spec examples/exacs-bom-spec.yaml --output customer-bom.xlsx
python tools/oci_bom_gen.py --spec examples/exacs-bom-spec.yaml --output customer-appca.xlsx --appca
# Architecture Center catalog # Architecture Center catalog
python tools/refresh_arch_catalog.py --validate python tools/refresh_arch_catalog.py --validate
python tools/refresh_arch_catalog.py --whats-new python tools/refresh_arch_catalog.py --whats-new
python tools/refresh_arch_catalog.py --check-links python tools/refresh_arch_catalog.py --check-links
python tools/archcenter_pattern_lookup.py "adb gcp fastconnect" --rebuild-index
# Feature compatibility # Feature compatibility
python tools/feature_matrix_cli.py check "Auto Scaling" adb_s 23ai python tools/feature_matrix_cli.py check "Auto Scaling" adb_s 23ai

View File

@@ -86,7 +86,7 @@ Present these options as a compact numbered list. Each option has a bold title f
1. 📋 Full proposal — *notes → architecture + deck + native PPTX diagram + costs* 1. 📋 Full proposal — *notes → architecture + deck + native PPTX diagram + costs*
2. 📐 Architecture diagram — *description → .drawio or native PPTX* 2. 📐 Architecture diagram — *description → .drawio or native PPTX*
3. 📊 Slide deck — *architecture → .pptx with native OCI diagram* 3. 📊 Slide deck — *architecture → .pptx with native OCI diagram*
4. 💰 Cost estimate — *services + sizing → PAYG vs BYOL* 4. 💰 Cost estimate — *quick PAYG/BYOL comparison while you scope (terminal + optional deck)*
VALIDATE & CHECK VALIDATE & CHECK
───────────────── ─────────────────
@@ -110,13 +110,22 @@ Present these options as a compact numbered list. Each option has a bold title f
SA TOOLS SA TOOLS
───────────────── ─────────────────
13. 📦 BOM generator — *services + quantities → .xlsx Bill of Materials* 13. 📦 BOM generator — *formal .xlsx artefact you send to the customer*
14. 📤 BOM for AppCA — *BOM → .xlsx ready to import into AppCA* 14. 📤 BOM for AppCA — *BOM → .xlsx ready to import into AppCA*
━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━
Pick a number, or just describe what you need. Pick a number, or just describe what you need.
``` ```
### Example Direct Requests
If the user asks for examples, or skips the menu and writes a natural-language request, prompts like these map directly to the capabilities above:
- `Generame un diagrama nativo en .pptx, simple y prolijo, de esta arquitectura: usuarios externos -> load balancer público -> 2 VMs de aplicación en subnet privada -> Autonomous Database Serverless. Quiero que quede presentable para mostrar directo en PowerPoint. No me pidas YAML ni setup adicional.`
- `Generate a simple high-level integration diagram for this flow: Slack -> Oracle Integration Cloud (OIC) -> Jira Service Management -> My Oracle Support. Add clear Client, Oracle, and External layers. Use brand icons for Slack and Jira Service Management, and deliver both .drawio and .pptx.`
- `Generate a high-level architecture diagram for this scenario: 1 PostgreSQL in OCI Ashburn, accessed from GCP Virginia through interconnect. PostgreSQL connects internally to 1 Autonomous Database Serverless, which also has 1 refreshable clone in the same region but in a different AD. Deliver both .drawio and .pptx. Keep it simple, executive-friendly, and technically correct.`
- `Gere um BOM em USD para este cenário: PostgreSQL no OCI com 4 OCPU e 500 GB, ADB-S com 200 ECPU e 1 TB BYOL, 1 refreshable clone com o mesmo sizing e FastConnect de 1 Gbps. Aplique 11% de desconto, 12 meses, 24 horas por dia. Quero o arquivo .xlsx e um resumo claro do custo mensal e anual.`
### Behavior Rules ### Behavior Rules
- If the user picks **1**, ask: "Paste your discovery notes (meeting notes, emails, whatever you have)." - If the user picks **1**, ask: "Paste your discovery notes (meeting notes, emails, whatever you have)."
@@ -148,7 +157,16 @@ Pick a number, or just describe what you need.
Full reference: `docs/skill/output-formats.md` § "Standard diagram-generation procedure (MANDATORY)". Full reference: `docs/skill/output-formats.md` § "Standard diagram-generation procedure (MANDATORY)".
- If the user picks **3**, ask: "Describe the architecture or paste the spec. I'll generate the deck with a native OCI PowerPoint diagram when the architecture is structured enough." - If the user picks **3**, ask: "Describe the architecture or paste the spec. I'll generate the deck with a native OCI PowerPoint diagram when the architecture is structured enough."
- If the user picks **4**, ask: "What services and sizing? (e.g., 'ADB-S 8 OCPU + 2 VMs + FastConnect')" - If the user picks **4**, ask: "What services and sizing? (e.g., 'ADB-S 8 OCPU + 2 VMs + FastConnect'). License model — PAYG, BYOL, or both?"
Then follow the cost-estimate flow:
1. **Catalog first, web never.** `grep` [`kb/pricing/oci-sku-catalog.yaml`](kb/pricing/oci-sku-catalog.yaml) (auto-refreshed, authoritative) for the requested SKUs and consult [`docs/bom-cookbook.md`](docs/bom-cookbook.md) for copy-paste recipes (ExaCS X11M BYOL, ADB-Dedicated, ADB-S + Block, FastConnect, etc.). Use `kb/pricing/compute.yaml` for shape-level lookups. Do NOT reach for WebSearch / WebFetch / `oracle.com` for list prices — the catalog IS the source.
2. **Staleness gate.** Read `last_verified` at the top of the catalog. If >60 days stale, warn the user and offer `python tools/refresh_sku_catalog.py --refresh` before quoting. If <=60 days, proceed without web lookups.
3. **Catalog miss policy.** If a SKU is genuinely absent after `grep` (not "I didn't find it on first try"), state it explicitly, then run `python tools/refresh_sku_catalog.py --refresh` and re-grep. Only if a fresh catalog still doesn't surface it is `oracle.com/cloud/price-list/` an acceptable fallback — and cite the source URL next to the part number.
4. **Source extraction.** When the request references a customer artefact (`.pptx`, BOM, quote), extract the topology / sizing from the artefact first, list each scenario's components, then map to SKUs. Show the mapping (component → SKU) so the SA can audit it.
5. **Quote shape.** Per scenario list: SKU + product name + metric + list price USD + quantity + monthly subtotal. End with total $/month and total $/year. Cite the catalog's `last_verified` date in the output.
6. When both PAYG and BYOL are requested, present them side-by-side (same SKU rows, two price columns). Save the cost estimate to `examples/<customer>-cost-estimate.yaml` and present the formatted table in the terminal.
7. **Disclaimer (mandatory).** Read the `disclaimer:` field from `kb/pricing/oci-sku-catalog.yaml` and append it verbatim — as a footer line under the terminal table, and (if a deck is generated) as a final "Commercial Disclaimer" slide. Do NOT paraphrase or rewrite; the field already carries Oracle's sample-quote boilerplate plus the "reference only / no client-specific discounts" clauses. `tools/oci_bom_gen.py` already reads this field for BOM xlsx output.
8. **Deck verification (if a deck is generated).** Open the produced `.pptx` with `python-pptx` and assert: (a) slide count matches spec, (b) each slide's expected title is present, (c) chart types match what the spec asked for (e.g. `COLUMN_CLUSTERED`, `COLUMN_STACKED`), (d) the disclaimer slide is the last one. Print a one-line `deck OK: <N> slides, titles=[...]` summary so the SA can audit at a glance. Rasterizing to PNG via PowerPoint is not always available on Linux/WSL — structural validation is the floor, not optional.
- If the user picks **5**, ask: "Describe your architecture or paste the spec. I'll run the 5-pillar review." Then follow the WA review flow: - If the user picks **5**, ask: "Describe your architecture or paste the spec. I'll run the 5-pillar review." Then follow the WA review flow:
1. Parse input to build a workload profile YAML (flags) and architecture YAML 1. Parse input to build a workload profile YAML (flags) and architecture YAML
2. If input is a `.pptx` file, extract text content from all slides to infer architecture and workload context 2. If input is a `.pptx` file, extract text content from all slides to infer architecture and workload context
@@ -174,6 +192,7 @@ Pick a number, or just describe what you need.
8. Generate implementation roadmap based on engagement tier 8. Generate implementation roadmap based on engagement tier
9. Produce a 8-10 slide deck using Oracle FY26 template (`config/oracle-pptx-layouts.yaml` → `business_case` type), using a native OCI PowerPoint diagram when the architecture is structured enough 9. Produce a 8-10 slide deck using Oracle FY26 template (`config/oracle-pptx-layouts.yaml` → `business_case` type), using a native OCI PowerPoint diagram when the architecture is structured enough
10. Output: business-case.pptx + business-case.yaml (reusable spec) 10. Output: business-case.pptx + business-case.yaml (reusable spec)
11. **Disclaimer + deck verification.** Append the `disclaimer:` field from `kb/pricing/oci-sku-catalog.yaml` as a final "Commercial Disclaimer" slide (verbatim — see option 4 step 7). Then run the same structural deck verification described in option 4 step 8 (slide count, titles, chart types, disclaimer-last) before reporting completion.
- If the user picks **9**, ask: "What topic? (e.g., 'DEP', 'TAC', 'maintenance window', 'vector search')" - If the user picks **9**, ask: "What topic? (e.g., 'DEP', 'TAC', 'maintenance window', 'vector search')"
- If the user picks **10**, ask: "What kind of architecture? (e.g., 'ADB + APEX', 'cross-region DR', 'data lakehouse')" - If the user picks **10**, ask: "What kind of architecture? (e.g., 'ADB + APEX', 'cross-region DR', 'data lakehouse')"
- If the user picks **11**, switch to finding intake mode: - If the user picks **11**, switch to finding intake mode:
@@ -219,7 +238,7 @@ Pick a number, or just describe what you need.
**BOM Output Rules:** **BOM Output Rules:**
- NEVER include "Confidential: Internal ONLY" or any confidentiality marking - NEVER include "Confidential: Internal ONLY" or any confidentiality marking
- ALWAYS include the Oracle Cost Estimator disclaimer at the bottom - ALWAYS include the disclaimer from `kb/pricing/oci-sku-catalog.yaml` `disclaimer:` field at the bottom — `tools/oci_bom_gen.py` reads it automatically (do not duplicate, paraphrase, or maintain a second copy elsewhere)
- Only include SKUs the customer actually requested — never dump the full catalog - Only include SKUs the customer actually requested — never dump the full catalog
- Show cost proportions so the customer can see where their spend concentrates - Show cost proportions so the customer can see where their spend concentrates
- Use Excel formulas (not static values) so the customer can adjust quantities - Use Excel formulas (not static values) so the customer can adjust quantities
@@ -460,9 +479,9 @@ See [docs/skill/output-formats.md](docs/skill/output-formats.md) for the per-cus
## Cookbook: Building Tool Payloads ## Cookbook: Building Tool Payloads
Before mapping customer requirements to SKUs for `generate_bom`, `generate_bom_appca`, or `generate_cost_estimate`, check [docs/bom-cookbook.md](docs/bom-cookbook.md) — it has copy-paste recipes for common patterns (ExaCS X11M BYOL, ADB-Dedicated, ADB-S + Block + FastConnect) and explicitly names the gotchas that otherwise burn exploration turns (e.g. ADB-Dedicated shares infrastructure SKUs with ExaCS — there are no separate `adb_dedicated` SKUs in the catalog). Any pricing or SKU work — cost estimate (option 4), BOM (option 13), BOM-AppCA (option 14), business-case TCO (option 8), scenario comparison from a customer artefact (`.pptx`, BOM, quote) — starts in the local catalog, not on the web. Check [docs/bom-cookbook.md](docs/bom-cookbook.md) for copy-paste recipes (ExaCS X11M BYOL, ADB-Dedicated, ADB-S + Block + FastConnect) and the gotchas that otherwise burn exploration turns (e.g. ADB-Dedicated shares infrastructure SKUs with ExaCS — there are no separate `adb_dedicated` SKUs in the catalog).
If a requirement does not match a recipe, `grep` [`kb/pricing/oci-sku-catalog.yaml`](kb/pricing/oci-sku-catalog.yaml) and consult `kb/services/` before inventing SKUs. If a requirement does not match a recipe, `grep` [`kb/pricing/oci-sku-catalog.yaml`](kb/pricing/oci-sku-catalog.yaml) and consult `kb/services/` before inventing SKUs. See "What You Do NOT Do" for the web-lookup rule and `kb/pricing/oci-sku-catalog.yaml`'s `last_verified` for staleness handling.
--- ---
@@ -570,6 +589,7 @@ KB lives under `kb/`. See [kb/README.md](kb/README.md) for the directory map, fr
- You do NOT replace the architect's judgment. You accelerate it. - You do NOT replace the architect's judgment. You accelerate it.
- You do NOT generate pixel-perfect diagrams. You generate 80% drafts the architect refines. - You do NOT generate pixel-perfect diagrams. You generate 80% drafts the architect refines.
- You do NOT make up pricing. If you don't have current pricing, estimate ranges. - You do NOT make up pricing. If you don't have current pricing, estimate ranges.
- You do NOT use WebSearch / WebFetch / `oracle.com` for OCI list prices or SKU lookups. `kb/pricing/oci-sku-catalog.yaml` is auto-refreshed via `tools/refresh_sku_catalog.py` (Oracle apexapps pricing API) and is the source of truth — including for ECPU/OCPU rates, BYOL deltas, Exadata X11M part numbers, and ADB-D ECPU SKUs. Web fallback is allowed ONLY when (a) the SKU is genuinely absent after `grep` AND a fresh `--refresh` still does not surface it, or (b) the catalog's `last_verified` is >60 days stale and a refresh is not currently possible. In either case, cite the source URL alongside the part number. Reflexively reaching for WebSearch when the catalog already has the answer is the most common cost-estimate failure mode — don't.
- You do NOT claim features exist if you're unsure. Check the KB first. - You do NOT claim features exist if you're unsure. Check the KB first.
- You do NOT do detailed project management. DELIVER artifacts are lightweight handover aids. - You do NOT do detailed project management. DELIVER artifacts are lightweight handover aids.
- You do NOT add services or components the user did not request. - You do NOT add services or components the user did not request.

View File

@@ -10,7 +10,8 @@ disclaimer: 'This sample quote is provided solely for evaluation purposes and is
for the services specified. If you would like to purchase the services specified in this sample quote, please request that for the services specified. If you would like to purchase the services specified in this sample quote, please request that
Oracle issue you a formal quote (which may include an OMA or a CSA if you do not already have an appropriate agreement in Oracle issue you a formal quote (which may include an OMA or a CSA if you do not already have an appropriate agreement in
place with Oracle) for your acceptance and execution. Your formal quote will be effective only upon Oracle''s acceptance place with Oracle) for your acceptance and execution. Your formal quote will be effective only upon Oracle''s acceptance
of the formal quote (and the OMA or CSA, if required). of the formal quote (and the OMA or CSA, if required). This is reference material, not a commercial commitment, and any
client-specific discounts negotiated with Oracle are not reflected in the prices shown.
' '
categories: categories:

View File

@@ -0,0 +1,685 @@
#!/usr/bin/env python3
"""
Build a short PowerPoint deck comparing multiple OCI BOM scenarios.
The input spec references one or more BOM YAML specs. The script resolves
monthly and 12-month totals from the live SKU catalog and produces a
customer-ready Oracle FY26 deck with native PowerPoint charts.
"""
import argparse
import sys
from dataclasses import dataclass
from decimal import Decimal, ROUND_HALF_UP
from pathlib import Path
import yaml
from pptx.chart.data import CategoryChartData
from pptx.dml.color import RGBColor
from pptx.enum.chart import XL_CHART_TYPE, XL_LABEL_POSITION, XL_LEGEND_POSITION
from pptx.enum.shapes import MSO_SHAPE
from pptx.enum.text import MSO_ANCHOR, PP_ALIGN
from pptx.util import Inches, Pt
sys.path.insert(0, str(Path(__file__).resolve().parent))
try:
from oci_pptx_base import Colors, Layouts, OraclePresBase
except ModuleNotFoundError:
from tools.oci_pptx_base import Colors, Layouts, OraclePresBase
PROJECT_ROOT = Path(__file__).resolve().parent.parent
CATALOG_PATH = PROJECT_ROOT / "kb" / "pricing" / "oci-sku-catalog.yaml"
DB_SERVER_SKU = "B110627"
STORAGE_SERVER_SKU = "B110629"
def _round_money(value: Decimal) -> Decimal:
return value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
def _money_text(value: Decimal) -> str:
return f"USD {value:,.2f}"
def _pct_text(value: Decimal) -> str:
sign = "+" if value > 0 else ""
return f"{sign}{value:.1f}%"
def _pct_abs_text(value: Decimal) -> str:
return f"{abs(value):.1f}%"
def _resolve_path(path_str: str) -> Path:
path = Path(path_str)
if path.is_absolute():
return path
return PROJECT_ROOT / path
def _load_catalog(path: Path) -> dict[str, dict]:
raw = yaml.safe_load(path.read_text(encoding="utf-8"))
catalog: dict[str, dict] = {}
for category in raw.get("categories", {}).values():
for sku in category.get("skus", []):
catalog[str(sku["sku"])] = sku
return catalog
@dataclass
class ScenarioMetrics:
scenario_id: str
short_label: str
title: str
topology: str
tradeoff: str
accent: str
term_months: int
db_servers: int
storage_servers: int
ecpus: int
monthly_total: Decimal
term_total: Decimal
infra_monthly: Decimal
compute_monthly: Decimal
delta_vs_baseline: Decimal = Decimal("0")
delta_pct_vs_baseline: Decimal = Decimal("0")
def _scenario_color(accent: str) -> RGBColor:
return {
"recommended": Colors.COPPER,
"balanced": Colors.TEAL,
"savings": Colors.FOREST,
}.get(accent, Colors.TEAL)
def _load_bom_metrics(spec_path: Path, meta: dict, catalog: dict[str, dict]) -> ScenarioMetrics:
raw = yaml.safe_load(spec_path.read_text(encoding="utf-8"))
bom = raw.get("bom", raw)
items = bom.get("line_items", [])
monthly_total = Decimal("0")
term_total = Decimal("0")
infra_monthly = Decimal("0")
compute_monthly = Decimal("0")
db_servers = Decimal("0")
storage_servers = Decimal("0")
ecpus = Decimal("0")
months_seen: set[int] = set()
for item in items:
sku = str(item["sku"])
entry = catalog.get(sku)
if not entry:
raise KeyError(f"SKU {sku} not found in catalog while processing {spec_path}")
price = Decimal(str(entry.get("list_price_usd", 0)))
qty = Decimal(str(item.get("qty", 0)))
hours_units = Decimal(str(item.get("hours_units", entry.get("default_hours_units", 730))))
months = int(item.get("months", 12))
discount = Decimal(str(item.get("discount", 0)))
line_monthly = price * qty * hours_units * (Decimal("1") - discount)
line_term = line_monthly * Decimal(str(months))
monthly_total += line_monthly
term_total += line_term
months_seen.add(months)
if sku == DB_SERVER_SKU:
db_servers += qty
infra_monthly += line_monthly
elif sku == STORAGE_SERVER_SKU:
storage_servers += qty
infra_monthly += line_monthly
elif "ECPU" in str(entry.get("metric", "")).upper():
ecpus += qty
compute_monthly += line_monthly
else:
compute_monthly += line_monthly
term_months = max(months_seen) if months_seen else int(meta.get("term_months", 12))
return ScenarioMetrics(
scenario_id=meta["id"],
short_label=meta.get("short_label", meta["id"]),
title=meta["title"],
topology=meta["topology"],
tradeoff=meta["tradeoff"],
accent=meta.get("accent", "balanced"),
term_months=term_months,
db_servers=int(db_servers),
storage_servers=int(storage_servers),
ecpus=int(ecpus),
monthly_total=_round_money(monthly_total),
term_total=_round_money(term_total),
infra_monthly=_round_money(infra_monthly),
compute_monthly=_round_money(compute_monthly),
)
class BOMComparisonDeck(OraclePresBase):
TITLE_ACCENT_COLOR = Colors.TEAL
MARGIN = Inches(0.55)
def __init__(self, spec: dict, scenarios: list[ScenarioMetrics]):
super().__init__()
self.spec = spec
self.deck_meta = spec["deck"]
self.scenarios = scenarios
self.term_months = int(self.deck_meta.get("term_months", 12))
@property
def _compute_is_constant(self) -> bool:
return len({scenario.compute_monthly for scenario in self.scenarios}) == 1
def _add_footer_note(self, slide, text: str) -> None:
box = self._add_textbox(
slide,
self.MARGIN,
Inches(7.0),
Inches(12.0),
Inches(0.25),
text=text,
font_size=9,
italic=True,
color=Colors.SECONDARY_TEXT,
)
box.text_frame.word_wrap = True
def _add_pill(self, slide, left, top, width, text, fill_color: RGBColor) -> None:
shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, Inches(0.3))
shape.fill.solid()
shape.fill.fore_color.rgb = fill_color
shape.line.fill.background()
tf = shape.text_frame
tf.clear()
p = tf.paragraphs[0]
p.text = text
p.alignment = PP_ALIGN.CENTER
p.font.name = self.FONT
p.font.size = Pt(10)
p.font.bold = True
p.font.color.rgb = Colors.WHITE
shape.text_frame.vertical_anchor = MSO_ANCHOR.MIDDLE
def add_cover(self) -> None:
slide = self._add_layout_slide(Layouts.COVER_DARK)
self._set_placeholder(slide, 0, self.deck_meta["title"])
self._set_placeholder(slide, 1, self.deck_meta["subtitle"])
disclaimer = self.deck_meta.get("commercial_disclaimer", {})
cover_tag = disclaimer.get("cover_tag")
if cover_tag:
self._add_pill(slide, Inches(8.7), Inches(0.45), Inches(3.9), cover_tag, Colors.COPPER)
def add_summary_slide(self) -> None:
slide = self._add_blank_slide()
self._add_title_bar(slide, "Scenario Summary", margin=self.MARGIN)
if self._compute_is_constant:
headline = (
"All three scenarios carry the same 830 ECPU allocation. "
"The cost delta comes from infrastructure footprint only."
)
else:
headline = (
"Recommended setup remains the architecture-led option. "
"Combined 6x4 trims infrastructure. Combined 5x3 is the strongest cost-down option."
)
self._add_textbox(
slide,
self.MARGIN,
Inches(1.2),
Inches(12.0),
Inches(0.5),
text=headline,
font_size=15,
bold=True,
color=Colors.PRIMARY_TEXT,
)
card_lefts = [self.MARGIN, Inches(4.45), Inches(8.35)]
card_width = Inches(3.85)
card_top = Inches(1.9)
card_height = Inches(4.55)
baseline = next(s for s in self.scenarios if s.scenario_id == self.deck_meta["baseline_scenario_id"])
for left, scenario in zip(card_lefts, self.scenarios):
card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, card_top, card_width, card_height)
card.fill.solid()
card.fill.fore_color.rgb = Colors.WARM_BG
card.line.color.rgb = _scenario_color(scenario.accent)
card.line.width = Pt(1.5)
accent = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, card_top, card_width, Inches(0.18))
accent.fill.solid()
accent.fill.fore_color.rgb = _scenario_color(scenario.accent)
accent.line.fill.background()
self._add_textbox(
slide, left + Inches(0.18), card_top + Inches(0.28), card_width - Inches(0.36), Inches(0.55),
text=scenario.title, font_size=18, bold=True, color=Colors.PRIMARY_TEXT, font_name=self.FONT_HEADING,
)
self._add_textbox(
slide, left + Inches(0.18), card_top + Inches(0.82), card_width - Inches(0.36), Inches(0.7),
text=scenario.topology, font_size=11, color=Colors.SECONDARY_TEXT,
)
tag_text = "Recommended" if scenario.scenario_id == baseline.scenario_id else (
"Lowest cost" if scenario.monthly_total == min(s.monthly_total for s in self.scenarios) else "Alternative"
)
self._add_pill(slide, left + Inches(0.18), card_top + Inches(1.45), Inches(1.35), tag_text, _scenario_color(scenario.accent))
self._add_textbox(
slide, left + Inches(0.18), card_top + Inches(1.92), card_width - Inches(0.36), Inches(0.32),
text="Monthly", font_size=10, bold=True, color=Colors.SECONDARY_TEXT,
)
self._add_textbox(
slide, left + Inches(0.18), card_top + Inches(2.18), card_width - Inches(0.36), Inches(0.42),
text=_money_text(scenario.monthly_total), font_size=20, bold=True, color=_scenario_color(scenario.accent),
)
self._add_textbox(
slide, left + Inches(0.18), card_top + Inches(2.65), card_width - Inches(0.36), Inches(0.3),
text=f"{self.term_months}M total: {_money_text(scenario.term_total)}", font_size=10, color=Colors.SECONDARY_TEXT,
)
if scenario.scenario_id == baseline.scenario_id:
delta_text = "Baseline"
delta_color = Colors.PRIMARY_TEXT
elif scenario.delta_vs_baseline < 0:
delta_text = (
f"Savings vs {baseline.short_label}: "
f"{_money_text(-scenario.delta_vs_baseline)} / mo "
f"({_pct_abs_text(scenario.delta_pct_vs_baseline)} lower)"
)
delta_color = Colors.FOREST
else:
delta_text = (
f"Premium vs {baseline.short_label}: "
f"{_money_text(scenario.delta_vs_baseline)} / mo "
f"({_pct_text(scenario.delta_pct_vs_baseline)})"
)
delta_color = Colors.PRIMARY_TEXT
self._add_textbox(
slide, left + Inches(0.18), card_top + Inches(3.05), card_width - Inches(0.36), Inches(0.45),
text=delta_text, font_size=10, bold=True,
color=delta_color,
)
footprint = (
f"DB servers: {scenario.db_servers}\n"
f"Storage servers: {scenario.storage_servers}\n"
f"Total ECPU: {scenario.ecpus:,}"
)
tf_box = self._add_textbox(
slide, left + Inches(0.18), card_top + Inches(3.55), card_width - Inches(0.36), Inches(0.72),
text=footprint, font_size=10, color=Colors.PRIMARY_TEXT,
)
tf_box.text_frame.word_wrap = True
self._add_textbox(
slide, left + Inches(0.18), card_top + Inches(4.15), card_width - Inches(0.36), Inches(0.32),
text=scenario.tradeoff, font_size=10, italic=True, color=Colors.SECONDARY_TEXT,
)
self._add_footer_note(slide, "Source footprint: recommended and combined options from the reference deck. Pricing based on OCI list price, 730 hours/month, no commercial discount.")
def add_monthly_chart_slide(self) -> None:
slide = self._add_blank_slide()
self._add_title_bar(slide, "Monthly Cost Comparison", margin=self.MARGIN)
data = CategoryChartData()
data.categories = [s.short_label for s in self.scenarios]
data.add_series("Monthly USD", [float(s.monthly_total) for s in self.scenarios])
frame = slide.shapes.add_chart(
XL_CHART_TYPE.COLUMN_CLUSTERED,
self.MARGIN,
Inches(1.35),
Inches(7.55),
Inches(4.9),
data,
)
chart = frame.chart
chart.has_legend = False
chart.value_axis.has_major_gridlines = True
chart.value_axis.tick_labels.number_format = '$0,"K"'
chart.value_axis.format.line.color.rgb = Colors.SECONDARY_TEXT
chart.category_axis.format.line.color.rgb = Colors.SECONDARY_TEXT
plot = chart.plots[0]
plot.vary_by_categories = True
plot.has_data_labels = True
plot.data_labels.number_format = '$#,##0'
plot.data_labels.position = XL_LABEL_POSITION.OUTSIDE_END
series = chart.series[0]
for point, scenario in zip(series.points, self.scenarios):
point.format.fill.solid()
point.format.fill.fore_color.rgb = _scenario_color(scenario.accent)
point.format.line.color.rgb = _scenario_color(scenario.accent)
callout = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, Inches(8.2), Inches(1.45), Inches(4.5), Inches(4.6))
callout.fill.solid()
callout.fill.fore_color.rgb = Colors.TABLE_ALT_ROW
callout.line.color.rgb = Colors.TEAL
callout.line.width = Pt(1.2)
baseline = next(s for s in self.scenarios if s.scenario_id == self.deck_meta["baseline_scenario_id"])
cheapest = min(self.scenarios, key=lambda s: s.monthly_total)
bullets = [
f"{baseline.short_label} is still the architecture-led recommendation.",
f"{self.scenarios[1].short_label} saves {_money_text(-self.scenarios[1].delta_vs_baseline)} per month by reducing storage footprint only.",
f"{cheapest.short_label} saves {_money_text(-cheapest.delta_vs_baseline)} per month and is {_pct_abs_text(cheapest.delta_pct_vs_baseline)} below the baseline.",
(
f"{cheapest.short_label} is cheaper because it cuts both infrastructure and compute footprint."
if not self._compute_is_constant
else f"{cheapest.short_label} is cheaper exclusively because it uses the smallest infrastructure footprint."
),
]
self._add_textbox(
slide, Inches(8.42), Inches(1.7), Inches(3.95), Inches(0.35),
text="Key takeaways", font_size=16, bold=True, color=Colors.PRIMARY_TEXT, font_name=self.FONT_HEADING,
)
notes_box = self._add_textbox(slide, Inches(8.35), Inches(2.15), Inches(3.95), Inches(3.4), text="", font_size=11)
tf = notes_box.text_frame
tf.clear()
for idx, bullet in enumerate(bullets):
p = tf.paragraphs[0] if idx == 0 else tf.add_paragraph()
p.text = f"{bullet}"
p.font.name = self.FONT
p.font.size = Pt(11)
p.font.color.rgb = Colors.PRIMARY_TEXT
p.space_after = Pt(8)
self._add_footer_note(slide, "This chart shows monthly run-rate only. The same ranking holds for the 12-month view because all scenarios use the same 730-hour monthly convention and 12-month term.")
def add_composition_slide(self) -> None:
slide = self._add_blank_slide()
self._add_title_bar(slide, "Cost Composition: Infrastructure vs ECPU", margin=self.MARGIN)
data = CategoryChartData()
data.categories = [s.short_label for s in self.scenarios]
data.add_series("Infrastructure", [float(s.infra_monthly) for s in self.scenarios])
data.add_series("ADB Dedicated ECPU BYOL", [float(s.compute_monthly) for s in self.scenarios])
frame = slide.shapes.add_chart(
XL_CHART_TYPE.COLUMN_STACKED,
self.MARGIN,
Inches(1.35),
Inches(7.55),
Inches(4.8),
data,
)
chart = frame.chart
chart.has_legend = True
chart.legend.position = XL_LEGEND_POSITION.BOTTOM
chart.legend.include_in_layout = False
chart.value_axis.has_major_gridlines = True
chart.value_axis.tick_labels.number_format = '$0,"K"'
series_infra = chart.series[0]
series_infra.format.fill.solid()
series_infra.format.fill.fore_color.rgb = Colors.MUTED_TEAL
series_infra.format.line.color.rgb = Colors.MUTED_TEAL
series_compute = chart.series[1]
series_compute.format.fill.solid()
series_compute.format.fill.fore_color.rgb = Colors.COPPER
series_compute.format.line.color.rgb = Colors.COPPER
table = self._add_table(
slide,
len(self.scenarios) + 1,
4,
Inches(8.2),
Inches(1.6),
Inches(4.4),
Inches(2.4),
)
headers = ["Scenario", "Infra / mo", "ECPU / mo", "Infra share"]
widths = [1.2, 1.05, 1.1, 1.0]
for idx, width in enumerate(widths):
table.columns[idx].width = Inches(width)
for col, header in enumerate(headers):
self._style_table_cell(
table.cell(0, col), header,
font_size=10, bold=True, color=Colors.WHITE, bg_color=Colors.TEAL,
alignment=PP_ALIGN.CENTER if col > 0 else PP_ALIGN.LEFT,
)
for row_idx, scenario in enumerate(self.scenarios, start=1):
bg = Colors.TABLE_ALT_ROW if row_idx % 2 == 0 else None
infra_share = (scenario.infra_monthly / scenario.monthly_total * Decimal("100")) if scenario.monthly_total else Decimal("0")
self._style_table_cell(table.cell(row_idx, 0), scenario.short_label, font_size=10, bold=True, bg_color=bg)
self._style_table_cell(table.cell(row_idx, 1), _money_text(scenario.infra_monthly), font_size=9, bg_color=bg, alignment=PP_ALIGN.RIGHT)
self._style_table_cell(table.cell(row_idx, 2), _money_text(scenario.compute_monthly), font_size=9, bg_color=bg, alignment=PP_ALIGN.RIGHT)
self._style_table_cell(table.cell(row_idx, 3), f"{infra_share:.1f}%", font_size=9, bg_color=bg, alignment=PP_ALIGN.RIGHT)
insight = (
"The ECPU line is flat across all three scenarios: 830 total ECPU.\n"
"Only the DB server and storage server counts move the price."
if self._compute_is_constant
else
"Combined 6x4 changes only the infrastructure layer.\n"
"Combined 5x3 is materially cheaper because it also reduces the ECPU footprint."
)
callout = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, Inches(8.2), Inches(4.35), Inches(4.4), Inches(1.15))
callout.fill.solid()
callout.fill.fore_color.rgb = Colors.WARM_BG
callout.line.color.rgb = Colors.COPPER
self._add_textbox(
slide, Inches(8.42), Inches(4.58), Inches(4.0), Inches(0.7),
text=insight, font_size=11, bold=True, color=Colors.PRIMARY_TEXT,
)
self._add_footer_note(slide, "Infrastructure is the sum of X11M DB servers plus X11M storage servers. Compute is the ADB Dedicated BYOL ECPU line.")
def add_detail_table_slide(self) -> None:
slide = self._add_blank_slide()
self._add_title_bar(slide, "Detailed BOM Comparison", margin=self.MARGIN)
table = self._add_table(
slide,
len(self.scenarios) + 1,
8,
self.MARGIN,
Inches(1.4),
Inches(12.2),
Inches(3.1),
)
widths = [1.5, 3.0, 0.9, 1.1, 1.1, 1.35, 1.35, 1.9]
for idx, width in enumerate(widths):
table.columns[idx].width = Inches(width)
headers = ["Scenario", "Topology", "DB", "Storage", "ECPU", "Monthly", f"{self.term_months}M total", "Trade-off"]
for col, header in enumerate(headers):
self._style_table_cell(
table.cell(0, col), header,
font_size=10, bold=True, color=Colors.WHITE, bg_color=Colors.TEAL,
alignment=PP_ALIGN.CENTER if col not in (0, 1, 7) else PP_ALIGN.LEFT,
)
for row_idx, scenario in enumerate(self.scenarios, start=1):
bg = Colors.TABLE_ALT_ROW if row_idx % 2 == 0 else None
if scenario.scenario_id == self.deck_meta["baseline_scenario_id"]:
bg = Colors.TEAL
txt = Colors.WHITE
else:
txt = Colors.PRIMARY_TEXT
values = [
scenario.short_label,
scenario.topology,
str(scenario.db_servers),
str(scenario.storage_servers),
f"{scenario.ecpus:,}",
_money_text(scenario.monthly_total),
_money_text(scenario.term_total),
scenario.tradeoff,
]
aligns = [PP_ALIGN.LEFT, PP_ALIGN.LEFT, PP_ALIGN.CENTER, PP_ALIGN.CENTER, PP_ALIGN.RIGHT, PP_ALIGN.RIGHT, PP_ALIGN.RIGHT, PP_ALIGN.LEFT]
for col_idx, (value, align) in enumerate(zip(values, aligns)):
self._style_table_cell(
table.cell(row_idx, col_idx), value,
font_size=9, bold=(scenario.scenario_id == self.deck_meta["baseline_scenario_id"]),
color=txt, bg_color=bg, alignment=align,
)
deltas_box = self._add_textbox(slide, self.MARGIN, Inches(5.0), Inches(12.0), Inches(1.0), text="", font_size=10)
tf = deltas_box.text_frame
tf.clear()
baseline = next(s for s in self.scenarios if s.scenario_id == self.deck_meta["baseline_scenario_id"])
summary_lines = [
f"Baseline: {baseline.short_label} at {_money_text(baseline.monthly_total)} per month.",
f"{self.scenarios[1].short_label}: {_money_text(-self.scenarios[1].delta_vs_baseline)} per month lower than baseline.",
f"{self.scenarios[2].short_label}: {_money_text(-self.scenarios[2].delta_vs_baseline)} per month lower than baseline.",
]
for idx, line in enumerate(summary_lines):
p = tf.paragraphs[0] if idx == 0 else tf.add_paragraph()
p.text = line
p.font.name = self.FONT
p.font.size = Pt(10)
p.font.color.rgb = Colors.SECONDARY_TEXT
p.space_after = Pt(5)
self._add_footer_note(slide, "BOM workbook references are saved alongside this deck for traceability and recalculation.")
def add_disclaimer_slide(self) -> None:
disclaimer = self.deck_meta.get("commercial_disclaimer", {})
if not disclaimer:
return
slide = self._add_blank_slide()
self._add_title_bar(slide, disclaimer.get("title", "Commercial Disclaimer"), margin=self.MARGIN)
intro_box = slide.shapes.add_shape(
MSO_SHAPE.ROUNDED_RECTANGLE,
self.MARGIN,
Inches(1.35),
Inches(12.1),
Inches(2.15),
)
intro_box.fill.solid()
intro_box.fill.fore_color.rgb = Colors.WARM_BG
intro_box.line.color.rgb = Colors.COPPER
intro_box.line.width = Pt(1.2)
formal = disclaimer.get("formal_text", "")
formal_tb = self._add_textbox(
slide,
Inches(0.8),
Inches(1.58),
Inches(11.55),
Inches(1.7),
text="",
font_size=11,
color=Colors.PRIMARY_TEXT,
)
formal_tf = formal_tb.text_frame
formal_tf.clear()
p = formal_tf.paragraphs[0]
p.text = formal
p.font.name = self.FONT
p.font.size = Pt(11)
p.font.color.rgb = Colors.PRIMARY_TEXT
p.alignment = PP_ALIGN.LEFT
p.space_after = Pt(0)
formal_tf.word_wrap = True
note_box = slide.shapes.add_shape(
MSO_SHAPE.ROUNDED_RECTANGLE,
self.MARGIN,
Inches(3.95),
Inches(12.1),
Inches(2.15),
)
note_box.fill.solid()
note_box.fill.fore_color.rgb = Colors.TABLE_ALT_ROW
note_box.line.color.rgb = Colors.TEAL
note_box.line.width = Pt(1.2)
self._add_textbox(
slide,
Inches(0.8),
Inches(4.18),
Inches(3.2),
Inches(0.35),
text="Practical notes",
font_size=16,
bold=True,
color=Colors.PRIMARY_TEXT,
font_name=self.FONT_HEADING,
)
bullets_tb = self._add_textbox(
slide,
Inches(0.8),
Inches(4.55),
Inches(11.2),
Inches(1.25),
text="",
font_size=11,
color=Colors.PRIMARY_TEXT,
)
bullets_tf = bullets_tb.text_frame
bullets_tf.clear()
for idx, bullet in enumerate(disclaimer.get("bullets", [])):
p = bullets_tf.paragraphs[0] if idx == 0 else bullets_tf.add_paragraph()
p.text = f"{bullet}"
p.font.name = self.FONT
p.font.size = Pt(11)
p.font.color.rgb = Colors.PRIMARY_TEXT
p.space_after = Pt(10)
self._add_footer_note(
slide,
"Reference pricing only. Formal commercials must move through the Oracle quoting process and the applicable agreement path.",
)
def build(self) -> None:
self.add_cover()
self.add_summary_slide()
self.add_monthly_chart_slide()
self.add_composition_slide()
self.add_detail_table_slide()
self.add_disclaimer_slide()
def load_deck_inputs(spec_path: Path) -> tuple[dict, list[ScenarioMetrics]]:
spec = yaml.safe_load(spec_path.read_text(encoding="utf-8"))
deck = spec["deck"]
catalog = _load_catalog(CATALOG_PATH)
scenarios = []
for scenario_meta in deck.get("scenarios", []):
bom_spec = _resolve_path(scenario_meta["bom_spec"])
scenarios.append(_load_bom_metrics(bom_spec, scenario_meta, catalog))
baseline_id = deck["baseline_scenario_id"]
baseline = next(s for s in scenarios if s.scenario_id == baseline_id)
for scenario in scenarios:
scenario.delta_vs_baseline = _round_money(scenario.monthly_total - baseline.monthly_total)
if baseline.monthly_total:
pct = (scenario.monthly_total - baseline.monthly_total) / baseline.monthly_total * Decimal("100")
scenario.delta_pct_vs_baseline = Decimal(str(pct)).quantize(Decimal("0.1"), rounding=ROUND_HALF_UP)
return spec, scenarios
def main() -> int:
parser = argparse.ArgumentParser(description="Build a comparison deck from multiple BOM specs.")
parser.add_argument("--spec", required=True, help="YAML deck spec path")
parser.add_argument("--output", required=True, help="Output .pptx path")
args = parser.parse_args()
spec_path = _resolve_path(args.spec)
output_path = _resolve_path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
spec, scenarios = load_deck_inputs(spec_path)
deck = BOMComparisonDeck(spec, scenarios)
deck.build()
deck.save(str(output_path))
print(f"Deck saved: {output_path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())