diff --git a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/clarify/clarify-assemble.md b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/clarify/clarify-assemble.md index 6c5b7c71..6c188112 100644 --- a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/clarify/clarify-assemble.md +++ b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/clarify/clarify-assemble.md @@ -33,7 +33,9 @@ Write `$MIGRATION_DIR/preferences.json`: "clarify_mode": "full|fast_path", "questions_asked": ["Q1", "Q2", ...], "questions_defaulted": ["Q7", "Q8", ...], - "questions_skipped_not_applicable": ["Q6", "Q8", ...] + "questions_skipped_extracted": ["Q6", "Q12b", ...], + "questions_skipped_not_applicable": ["Q6", "Q8", ...], + "inventory_clarifications": {"database_ha": "plan:premium-0"} }, "global": { "target_region": "", @@ -104,9 +106,9 @@ Do **not** write a `workshop` object from Clarify. The what-if workshop ### Schema Rules -1. The `sources` object records how each question was answered: `"user"` (explicitly answered), `"default"` (system default applied, including skipped questions and "use defaults for the rest"). +1. The `sources` object records how each question was answered: `"user"` (explicitly answered, or corrected on the Assumption Sheet), `"extracted"` (resolved from the inventory — Detected sheet row), `"default"` (system default applied, including skipped questions, sheet-confirmed defaults, and "use defaults for the rest"). 2. `defaults_applied` is the array of question IDs that received default values. -3. `metadata.questions_skipped_not_applicable` records questions skipped because their triggering condition was not met (e.g., Q6 skipped because no Postgres). +3. `metadata.questions_skipped_not_applicable` records questions skipped because their triggering condition was not met (e.g., Q6 skipped because no Postgres). `metadata.questions_skipped_extracted` records questions resolved from the inventory (interview Step 2.5 Extraction Rules); the raw signal goes in `metadata.inventory_clarifications` (e.g. `{"database_ha": "plan:premium-0"}`). 4. Only write keys with non-null values. Omit sections/keys that are entirely null. 5. `global.fir_intent` is `null` when no Fir apps detected (Q11 not fired). 6. `network.existing_vpc_id` and `network.subnet_ids` are `null`/empty when no Private Space peering exists. @@ -165,4 +167,6 @@ private-space conditionals), then emit `GATE_FAIL` (STOP; do not patch artifacts Only after `HANDOFF_OK`, apply the phase-status update protocol (`INTERPRETER.md` § The interpreter loop) — mark `phases.clarify` completed and advance per `_advances_to` — in the **same turn** as the output message below. -Output to user: "Clarification complete. Proceeding to Phase 3: Design AWS Architecture." +Output to user: "Phase 2 of 6 complete (Clarify). Remaining: Design → Estimate → Generate (+ optional Feedback). Next artifact: aws-design.json. Proceeding to Phase 3: Design AWS Architecture." + +_Emit this breadcrumb only after `HANDOFF_OK` — never on a failed gate._ diff --git a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/clarify/clarify-interview.md b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/clarify/clarify-interview.md index 0dc937e8..ebcaa4a5 100644 --- a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/clarify/clarify-interview.md +++ b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/clarify/clarify-interview.md @@ -130,7 +130,56 @@ Before generating questions, scan the inventory to determine which questions app | Q14 — Alerting preference | Always | Never | | Q15 — Cost optimization | Always | Never | -### Batch Planning +### Extraction Rules (answer from the inventory before asking) + +Before planning batches, resolve what `heroku-resource-inventory.json` already answers. Extracted questions are NOT asked — they appear as **Detected** rows on the Assumption Sheet (Step 2.5) and are recorded in `metadata.questions_skipped_extracted`, with the raw signal in `metadata.inventory_clarifications`. + +| Q | Extraction signal | Resolves to | When NOT to extract | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| Q1 — Region | Private Space `region` (e.g. `virginia` → `us-east-1`, `oregon` → `us-west-2`, `dublin` → `eu-west-1`, `frankfurt` → `eu-central-1`); Common Runtime apps: `us` → suggest `us-east-1`, `eu` → suggest `eu-west-1` | `global.target_region` — **Detected** for Private Spaces (explicit region); **Proposed default** for Common Runtime (a suggestion, not a fact) | Mixed regions across apps/spaces — ask Q1 | +| Q6 — Database HA | Heroku Postgres plan tier: `standard-*` → no HA follower (`database_ha: false` proposed); `premium-*` / `private-*` / `shield-*` → HA included (`database_ha: true` detected) | `data.database_ha` | Multiple Postgres add-ons with mixed tiers — ask Q6 with a per-add-on breakdown | +| Q7 — Redis HA | Redis plan tier: `premium-*` and above → HA (`redis_ha: true` detected); `mini`/hobby tiers → no HA (proposed `false`) | `data.redis_ha` | Mixed tiers — ask Q7 | +| Q12b — Containerization | App stack field: `container` stack → `containerization_status: "dockerfile"` detected; buildpack stacks (`heroku-22`, `heroku-24`) → `buildpack_only` detected | `compute.containerization_status` | Mixed stacks across apps — ask Q12b | + +**Tier-derived HA is a strong signal, not a requirement statement:** the plan tier says what the customer HAS, not what they NEED. Present tier-derived rows on the sheet with the source shown ("your `standard-0` plan has no HA follower") so the user can correct if their target posture differs from their current one — this mirrors Q3 (availability posture), which is always asked and never extracted. + +### Step 2.5: Assumption Sheet (Mandatory Gate) + +**HARD GATE — do NOT ask any batch question until the user responds to this sheet.** Skip the sheet only when nothing was extracted AND no documented default applies (rare). + +Present detected values and to-be-assumed defaults as one confirm-or-edit sheet: + +``` +### Migration assumptions — confirm or correct + +**Detected from your Heroku inventory:** + +| Setting | Value | Source | What it decides | +| ------- | ----- | ------ | --------------- | +| Region | us-east-1 (Private Space: virginia) | space config | All AWS resources deploy here | +| Database HA | Included (premium-0 plan) | Postgres plan tier | RDS Multi-AZ topology | +| Containerization | Buildpacks only (heroku-24) | app stack | Fargate via buildpack-to-image path | + +**Assumed (documented defaults — correct anything that's wrong):** + +| Setting | Assumed value | Consequence if left as-is | +| ------- | ------------- | ------------------------- | +| Migration approach | Full cutover | Single cutover event; say "interim/data-first" for phased | +| DB migration method | pg_dump/restore | Fine under ~100GB; larger needs replication tooling | +| Cost optimization | Balanced | No aggressive Spot/reservation assumptions | + +Reply: +1. **Confirm all** (or "looks good") — I'll ask only the [N] remaining questions. +2. **Change a setting** — name it ("database ha: no") or describe it in plain words ("we can't take downtime") — I'll map it or ask the full question. Several fixes in one message is fine. +3. **"ask me about [setting]"** — I'll ask the full question with all options. +4. **"ask me everything"** — discard assumptions, run the full batch flow. +``` + +_Present these as selectable options via the structured question tool (e.g. AskUserQuestion) when the IDE provides one; otherwise the numbered list verbatim. Free-text corrections are always accepted — the menu never replaces them._ + +Questions resolved on the sheet (confirmed or corrected) are excluded from the batches. User corrections move the question ID from `questions_skipped_extracted`/`questions_defaulted` to `questions_asked`. + +--- After determining active questions, organize them into **three progressive batches**: @@ -144,7 +193,7 @@ After determining active questions, organize them into **three progressive batch **Batch 3 is always active** (Q12–Q15 always fire; Q11 fires only if Fir detected). -Record the ordered list of active batches and count questions per batch after filtering. +Record the ordered list of active batches and count questions per batch after filtering. **Exclude questions resolved on the Assumption Sheet** (extracted, defaulted-and-confirmed, or corrected) — batches contain only the questions the user must actually answer. --- @@ -156,7 +205,7 @@ For each active batch, execute steps 3a–3c: #### 3a. Present Batch -Use a conversational tone with brief context explaining why each question matters. Number questions within each batch starting from 1. +Use a conversational tone with brief context explaining why each question matters. Number questions within each batch starting from 1. **Cap each turn at 4 questions** — when a batch has more, split it and open each part with "Batch [i] of [k]". Open the first batch with: "That leaves [N] decisions only you can make — then we're ready to design." _Use the structured question tool (e.g. AskUserQuestion) when available, identical option text otherwise; shorthand answers ("1A 2C 3 skip") are accepted in either mode._ **Batch 1 — Global / Strategic (always first):** diff --git a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/design/design-assemble.md b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/design/design-assemble.md index d46e112f..a9a5befb 100644 --- a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/design/design-assemble.md +++ b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/design/design-assemble.md @@ -68,7 +68,7 @@ Output to user — build message from design contents: - If pipeline warnings: "N pipeline(s) detected (CI/CD requires manual config)." - VPC mode: "VPC design: [existing VPC referenced | new VPC generated with N subnets]." -Format: "Design phase complete. [artifact summaries] Next required step: Phase 4 — Estimate. Load `references/phases/estimate/estimate.md` now." +Format: "Phase 3 of 6 complete (Design). [artifact summaries] Remaining: Estimate → Generate (+ optional Feedback). Next required step: Phase 4 — Estimate. Load `references/phases/estimate/estimate.md` now." --- diff --git a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/estimate/estimate-assemble.md b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/estimate/estimate-assemble.md index feb7cb80..ece4e4cc 100644 --- a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/estimate/estimate-assemble.md +++ b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/estimate/estimate-assemble.md @@ -103,7 +103,7 @@ After writing `estimation-infra.json`, present a concise summary to the user: 5. **Migration complexity**: tier + timeline range 6. **Monthly and annual savings** (or increase) vs Heroku per tier (if a baseline was determined) 7. **Top 2-3 optimization opportunities** with savings potential -8. **Recommendation**: `path_label` with one-line justification +8. **Recommendation**: lead with `outcome_label` (or `path_label` when `outcome` is absent). List `conditions[]` when `conditional_go`. Close with 1–3 `would_flip_if[]` bullets when present. Keep `path_label` as the execution-shape line under the verdict. Keep under 25 lines. The user can ask for details or re-read `estimation-infra.json`. @@ -129,14 +129,19 @@ After outer-run `HANDOFF_OK`, the summary above, and the deferred phase-status update — offer: ``` -Estimate complete. Before Generate, you can run a what-if workshop: -change region, HA, compute target, or CPU architecture (x86 vs Graviton) -and compare priced scenarios without re-discovering inventory. +Phase 4 of 6 complete (Estimate). Remaining: Generate (+ optional Feedback). +Before Generate, want to see how the numbers move if you change something? +I can reprice scenarios side by side in about a minute each, without +re-running discovery — for example: a different AWS region, single-AZ +database for staging, a different compute target, or ARM-based (Graviton) +instances. [A] Enter what-if workshop [B] Proceed toward Generate ``` +**Data-justified scenario hint (add one line when applicable):** if a material assumption was defaulted or tier-derived rather than confirmed — most commonly `database_ha` — append: "Suggestion: we assumed [assumption]; comparing a [alternative] scenario would bound it before you commit." Suggest at most one. + - **A** → Load `references/phases/workshop/workshop.md` (sidebar) and follow it (baseline capture if `scenarios/` missing, then the sheet). Keep `current_phase: estimate`; set `phases.workshop` → `"in_progress"`. diff --git a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/estimate/estimate-cost-engine.md b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/estimate/estimate-cost-engine.md index abff1aaf..ea71be15 100644 --- a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/estimate/estimate-cost-engine.md +++ b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/estimate/estimate-cost-engine.md @@ -540,12 +540,44 @@ Present 3 paths: 1. **Migrate with Optimizations (Best ROI)** — optimized service choices, projected savings 2. **Phased Migration (Lower Risk)** — app-by-app per design order, validate each before proceeding -3. **Stay on Heroku (Lowest Complexity)** — only if AWS is more expensive and costs are the sole metric +3. **Stay on Heroku** — when the evidence favors staying: AWS materially more expensive without offsetting operational benefit, team leverage from Heroku's managed simplicity with no AWS-specific need, or migration cost/risk exceeding the projected benefit for this stack. **Do not gate "stay" on "cost is the sole metric"** — any decisive factor suffices. Include migrate/stay decision factors: - **Migrate if:** infrastructure control matters, AWS-specific services needed, compliance requirements exceed Heroku's offerings, scaling beyond Heroku limits, long-term cost optimization (Savings Plans, Spot) -- **Stay if:** cost is the only metric and AWS is more expensive, team benefits from Heroku's managed simplicity, no need for AWS-specific services, migration risk exceeds benefit +- **Stay if:** AWS is more expensive without an offsetting benefit for this stack, team benefits from Heroku's managed simplicity, no need for AWS-specific services, migration risk exceeds benefit + +### Decision outcome (write alongside `path`) + +`path` says how a migration would run; `outcome` says whether to run it now. **`defer_for_evidence` is expected to be RARE** — AWS almost always has the services and the AWS-side estimate can almost always be produced; when in doubt, prefer `conditional_go` with named conditions. + +**Hard triggers — any one forces `outcome: "defer_for_evidence"`:** + +| # | Trigger | Evidence to name | +| - | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | +| 1 | GovCloud-class compliance ambiguity: compliance unknown AND signals suggest FedRAMP/government requirements — GovCloud vs commercial changes regions, service catalog, and pricing wholesale | Compliance confirmation from the user's legal/compliance owner | +| 2 | The user's **only** stated motivation is cost savings AND no spend signal exists at all (`billing_profile.available == false` AND the user declined to state Heroku spend) | Heroku invoice/billing export, or a stated monthly spend figure | + +**Soft triggers — never force defer; add each to `conditions[]` (outcome becomes `conditional_go` instead of `go`) and to `would_flip_if[]`:** + +| # | Trigger | Condition wording (adapt to stack) | +| - | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | +| 3 | `database_ha` or availability posture resolved by default/plan-tier, never user-confirmed | "Confirm the availability requirement — Multi-AZ roughly doubles the database line" | +| 4 | `migration_approach` defaulted while the stack has Postgres over ~100GB or zero-downtime signals | "Confirm cutover approach — pg_dump default may not fit this database" | +| 5 | Pricing staleness beyond the vendored rate card's accuracy band | "Refresh pricing before treating the dollar delta as decision-grade" | + +**Outcome derivation:** + +``` +IF any hard trigger fired -> outcome: "defer_for_evidence" +ELSE IF path == "stay" -> outcome: "stay" +ELSE IF any soft trigger fired -> outcome: "conditional_go" (conditions[] = fired soft triggers) +ELSE -> outcome: "go" +``` + +Complexity alone selects `path: "migrate_phased"` — it never moves `outcome` away from go/conditional_go. Populate `decision_basis` from provenance: billing/inventory-extracted values → measured; defaulted → assumed; declined/unknown → unknown. Populate `would_flip_if[]` with the 1–3 changes most likely to alter the outcome. + +**Presenting a defer (lead with what IS established):** open with what the assessment did determine — "AWS can host this stack; the AWS-side estimate is $X–$Y/mo" — before naming the one missing piece of evidence and how to obtain it. Never present defer as "no answer." ### Persist recommendation to estimation-infra.json @@ -553,14 +585,25 @@ Include migrate/stay decision factors: "recommendation": { "path": "migrate_optimized|migrate_phased|stay", "path_label": "Migrate with Optimizations|Phased Migration|Stay on Heroku", + "outcome": "go|conditional_go|defer_for_evidence|stay", + "outcome_label": "Go|Go, with conditions|Defer — get evidence|Stay on Heroku", "roi_justification": "", "confidence": "high|medium|low", "migrate_if": [""], "stay_if": [""], + "conditions": [""], + "decision_basis": { + "measured": [""], + "assumed": [""], + "unknown": [""] + }, + "would_flip_if": ["<1-3 changes most likely to alter the outcome, with direction>"], "next_steps": [""] } ``` +Readers of pre-extension artifacts must tolerate absent v2 fields (`outcome`, `conditions`, `decision_basis`, `would_flip_if`) and fall back to `path`. `outcome: "stay"` only with `path: "stay"`. + **Path selection logic:** | Scenario | `path` value | `path_label` | diff --git a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/estimate/estimate.md b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/estimate/estimate.md index f2d90566..b6602cd1 100644 --- a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/estimate/estimate.md +++ b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/estimate/estimate.md @@ -45,6 +45,8 @@ _postconditions: _on_failure: _halt_and_inform - _assert: "recommendation.migrate_if and recommendation.stay_if are non-empty arrays" _on_failure: _halt_and_inform + - _assert: "recommendation.outcome is one of {go, conditional_go, defer_for_evidence, stay}; conditions is a non-empty array when outcome is conditional_go; outcome may be stay only when path is stay" + _on_failure: _halt_and_inform - _assert: "projected_costs.aws_monthly_balanced is a positive number" _on_failure: _halt_and_inform - _assert: "every service in aws-design.json services[] appears in the cost breakdown, or is listed as 'unpriced' in warnings" diff --git a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/generate/generate-assemble.md b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/generate/generate-assemble.md index d7de47f2..b4c2006f 100644 --- a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/generate/generate-assemble.md +++ b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/generate/generate-assemble.md @@ -65,7 +65,7 @@ Only after `HANDOFF_OK`, apply the phase-status update protocol (`INTERPRETER.md Output to user: ``` -Generate phase complete. +Phase 5 of 6 complete (Generate). Optional remaining: Feedback. Artifacts produced: • terraform/ — [N] Terraform files for AWS infrastructure diff --git a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/generate/generate-report.md b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/generate/generate-report.md index cca04ea1..7ed72f95 100644 --- a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/generate/generate-report.md +++ b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/generate/generate-report.md @@ -33,6 +33,10 @@ _contributes: From `estimation-infra.json`: - `recommendation.path_label` (or `financial_summary.recommendation`) +- `recommendation.outcome` / `outcome_label` / `conditions[]` / `decision_basis` / + `would_flip_if[]` when present (v2 decision fields from Estimate Part 8 — tolerate + absence on pre-extension artifacts and fall back to `path_label`) +- `recommendation.confidence` when present - `projected_costs.aws_monthly_premium` / `_balanced` / `_optimized` - `complexity_tier` - `pricing_source.status` (for a one-line pricing confidence note) @@ -49,10 +53,53 @@ Write a **self-contained** HTML file to `$MIGRATION_DIR/migration-report.html` | Section ID | Content | | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `decision-summary` | Verdict / path label, complexity, one-sentence next action | +| `decision-summary` | Typography-first verdict (see below), cost one-liner, next action | | `exec-costs` | Heroku-vs-AWS side-by-side when a Heroku baseline exists (`current_costs.source != "unavailable"`); otherwise the AWS three-tier table with a one-line note that no Heroku baseline was available. Estimated monthly; Balanced primary | | `next-steps` | Ordered list pointing to `MIGRATION_GUIDE.md` phases (not a procedure dump) | +### `decision-summary` content (REQUIRED) + +1. **Verdict (typography-first — the thesis of this section):** When + `recommendation.outcome` exists, render `outcome_label` as the section's + **headline** in large display type (e.g. + `

Go, with conditions

`), then one body-type + metadata line: `Execution shape: [path_label] · Complexity: [complexity_tier]`. + Do **not** render the verdict as a row of colored pill badges — structure + carries the information; meaning must never depend on color alone (a muted + accent on the headline is fine; the words carry the verdict). When `outcome` + is absent (pre-extension artifacts), use `path_label` as the headline the + same way. + - `conditional_go`: render `conditions[]` as a short checklist under the + metadata line. + - `defer_for_evidence`: lead with what IS established ("AWS can host this + stack; AWS-side estimate $X–$Y/mo"), then the named missing evidence and + how to obtain it — never present defer as "no answer," and do not show a + savings headline as if the decision were made. + - `stay`: headline is the stay label; do not imply a migrate path. +2. **Confidence pointer (when `confidence` or `decision_basis` exists):** one + line under the verdict — `Confidence: [confidence] — full basis in + What This Assessment Rests On.` +3. **Cost one-liner** — Balanced AWS monthly vs Heroku baseline when available + (estimated monthly), else AWS Balanced alone. +4. **What would flip this (v2):** from `recommendation.would_flip_if[]` when + present — short unordered list. Skip silently when absent. +5. **One-sentence next action** — the single most useful next step for the + reader (Generate artifacts, confirm a condition, or gather named evidence). + +### `decision-basis` (end of summary, when v2 fields exist) + +When `recommendation.decision_basis` is present, render +`
` **after** `decision-summary` and **before** +`exec-costs` (or immediately before `next-steps` if you keep costs first — +either order is fine as long as the confidence pointer's href resolves): + +- Heading: `What This Assessment Rests On` (plain title — never "Section N"). +- Three compact columns/lists from `decision_basis`: **Measured** / **Assumed** / + **Unknown**. Invent nothing — copy the arrays as written. +- One-line pricing provenance from `pricing_source` + accuracy band. + +Omit the section when `decision_basis` is absent (pre-extension artifacts). + ### Conditional — `what-if-scenarios` When `scenarios/index.json` exists and `scenarios[]` has **≥ 2** entries, @@ -89,9 +136,10 @@ Omit the section when workshop was declined or never entered. ### Minimal CSS Use a short inline stylesheet: readable body font, `.report` max-width ~900px, -tables with borders, `.active-scenario` or bold active row, `.verdict` badge, -`.toc` list. Keep visual noise low — this is a one-pager for stakeholders, not -a design system. +tables with borders, `.active-scenario` or bold active row, +`.verdict-headline` (large display type for the outcome — **not** a colored +pill badge row), `.toc` list. Keep visual noise low — this is a one-pager for +stakeholders, not a design system. ### Skeleton @@ -108,6 +156,7 @@ a design system.
+
@@ -125,7 +174,11 @@ Before returning: 1. File exists and is non-empty. 2. Contains `decision-summary`, `exec-costs`, `next-steps`, and `draft for review`. -3. If `scenarios/index.json` has ≥2 scenarios, contains `what-if-scenarios`. +3. When `recommendation.outcome` exists: `decision-summary` contains the + `outcome_label` text (or a `verdict-headline` element) — not a row of colored + pill badges as the sole verdict carrier. +4. When `recommendation.decision_basis` exists: contains `decision-basis`. +5. If `scenarios/index.json` has ≥2 scenarios, contains `what-if-scenarios`. On failure: fix and rewrite — do **not** leave a stub. Report generation is part of Generate for heroku-to-aws (stakeholder deliverable), but a report