From 22dd52fd7da1ef1bf4c39cc32cc604753cb9a0c7 Mon Sep 17 00:00:00 2001 From: Logan Kleier Date: Thu, 23 Jul 2026 22:41:40 -0700 Subject: [PATCH 01/14] =?UTF-8?q?feat(gcp-to-aws):=20decision=20report=20?= =?UTF-8?q?=E2=80=94=20shared=20exec-summary=20core,=20rendered=20at=20gat?= =?UTF-8?q?e=20A=20(stacked=20on=20decision=20gate)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract the report's executive summary (Sections 0-8: verdict, TCO, cost comparison with baseline-quality/not-comparable rules, timeline, risks, assumptions) into references/shared/report-decision-core.md — single source of truth rendered in two modes: decision (gate A) and full (Generate Step 1 loads it; do-not-restate rule) - estimate.md gate choice A now writes decision-report.html + DECISION.md from the core (no appendices, CTA footer, pre-Generate fallbacks for timeline/diagram), validated before presenting - Replace-never-patch: Execute renders migration-report.html fresh from artifacts; never splices the decision report's HTML - validate-migration-report.py --mode decision: exec required set + decision-cta, appendix-* sections forbidden; full-mode contract and REPORT_OK format byte-identical (4 new tests; 36 pass) - New golden fixture gcp-decision-gate/after-decide-complete locking the decide-complete terminal state (current_phase complete + run_mode decide + generate pending) and validator-passing decision pack (7 asserters pass) --- .../after-decide-complete/.phase-status.json | 15 ++ .../after-decide-complete/DECISION.md | 37 +++ .../decision-report.html | 62 +++++ .../check_expected_decide.py | 80 ++++++ .../scripts/validate-migration-report.py | 59 ++++- .../skills/gcp-to-aws/SKILL.md | 3 +- .../references/phases/estimate/estimate.md | 25 +- .../generate/generate-artifacts-report.md | 209 +--------------- .../references/shared/report-decision-core.md | 234 ++++++++++++++++++ .../shared/validate-migration-report.md | 2 + .../tests/test_validate_migration_report.py | 64 +++++ .../migration-to-aws/tools/run-asserters.py | 1 + 12 files changed, 568 insertions(+), 223 deletions(-) create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-decision-gate/after-decide-complete/.phase-status.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-decision-gate/after-decide-complete/DECISION.md create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-decision-gate/after-decide-complete/decision-report.html create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-decision-gate/check_expected_decide.py create mode 100644 migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/report-decision-core.md diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-decision-gate/after-decide-complete/.phase-status.json b/migrate/plugins/migration-to-aws/fixtures/gcp-decision-gate/after-decide-complete/.phase-status.json new file mode 100644 index 00000000..1fcfbc94 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-decision-gate/after-decide-complete/.phase-status.json @@ -0,0 +1,15 @@ +{ + "migration_id": "0723-1900", + "last_updated": "2026-07-23T19:22:00Z", + "current_phase": "complete", + "run_mode": "decide", + "phases": { + "discover": "completed", + "clarify": "completed", + "design": "completed", + "estimate": "completed", + "workshop": "completed", + "generate": "pending", + "feedback": "completed" + } +} diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-decision-gate/after-decide-complete/DECISION.md b/migrate/plugins/migration-to-aws/fixtures/gcp-decision-gate/after-decide-complete/DECISION.md new file mode 100644 index 00000000..dc6897e6 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-decision-gate/after-decide-complete/DECISION.md @@ -0,0 +1,37 @@ +# Migration Decision — GCP to AWS + +**Verdict: Go, with conditions** (phased migration · moderate complexity · medium confidence) + +Proceed with a phased migration on the Balanced cost scenario once the availability assumption is confirmed. + +## Costs (estimated monthly) + +| Tier | Est. Monthly AWS | +| -------------------------------------------- | ---------------- | +| Premium | $212/mo | +| **Balanced** (compare GCP to this row first) | **$155/mo** | +| Optimized | $118/mo | + +GCP baseline: estimated from resource configs (±20–30%, standing charges only). The stated $1K–$5K/mo spend band measures the whole bill and is not directly comparable. + +## Migrate if / Stay if + +- **Migrate if:** you want managed compute without Kubernetes operations; consolidating AI usage onto Bedrock matters this year. +- **Stay if:** your GCP committed-use discounts run through 2027. + +## Timeline + +~6–12 weeks if you execute (medium complexity band). + +## Top risks + +- Availability posture assumed single-AZ — confirm before cutover sizing. +- BigQuery deferred to specialist engagement; excluded from combined totals. + +## What this rests on + +Region, database size, and model detection extracted from Terraform and code; availability defaulted to single-AZ per dev-tier signals. Cached pricing dated 2026-07 (±5–10% infra). + +--- + +**Ready to execute?** Say "generate the Terraform and migration scripts" for the full execution pack. _Draft for review._ diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-decision-gate/after-decide-complete/decision-report.html b/migrate/plugins/migration-to-aws/fixtures/gcp-decision-gate/after-decide-complete/decision-report.html new file mode 100644 index 00000000..f10bbe15 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-decision-gate/after-decide-complete/decision-report.html @@ -0,0 +1,62 @@ + + + + + +GCP to AWS Migration Assessment — Decision Report + + + +
+ +
+

Migration Decision Summary

+

Go, with conditions

+

Execution shape: Phased migration · Complexity: Moderate

+

Confidence: Medium — full basis in What This Assessment Rests On.

+

Recommendation: Proceed with a phased migration on the Balanced cost scenario once the availability assumption is confirmed.

+

Migrate if

+
  • You want managed compute without Kubernetes operations
  • Consolidating AI usage onto Bedrock matters this year
+

Stay if

+
  • Your GCP committed-use discounts run through 2027
+
+
+

Current Stack

+ +
Primary GCP services detected
ServiceType
Cloud Runcompute
Cloud SQL (PostgreSQL 15)database
Cloud Storagestorage
+
+
+

Cost Comparison

+ +
Estimated monthly costs
TierEst. Monthly AWS
PremiumEst. $212/mo
Balanced (compare GCP to this row first)Est. $155/mo
OptimizedEst. $118/mo
+

Your GCP baseline: estimated from resource configs (±20–30%, standing charges only). Your stated spend band ($1K–$5K/mo) measures your whole bill and is not directly comparable to the AWS figure above.

+
+
+

Timeline

+

~6–12 weeks if you execute (medium complexity band).

+
+
+

Top Risks

+
  • Availability posture assumed single-AZ — confirm before cutover sizing
  • BigQuery deferred to specialist engagement; excluded from combined totals
+
+
+

What This Assessment Rests On

+

Region, database size, and model detection extracted from your Terraform and code; availability defaulted to single-AZ per dev-tier signals. Cached pricing dated 2026-07 (±5–10% infra).

+
+
+

Ready to execute?

+

Say "generate the Terraform and migration scripts" and I'll produce the full execution pack (Terraform, migration scripts, rollback runbook, fill-in checklist) from this same analysis.

+

This decision report was generated without execution artifacts; the full migration report replaces it if you proceed.

+
+
+ + + diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-decision-gate/check_expected_decide.py b/migrate/plugins/migration-to-aws/fixtures/gcp-decision-gate/check_expected_decide.py new file mode 100644 index 00000000..a2f9207a --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-decision-gate/check_expected_decide.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python3 +"""Assert a Decision-gate choice-A run landed in the decide-complete state. + +Locks PR #185's terminal semantics (current_phase complete + run_mode decide + +generate pending) and the decision-pack artifacts (decision-report.html passes +the validator in --mode decision; DECISION.md exists; no Generate artifacts). + +Usage: check_expected_decide.py +""" +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +PLUGIN_ROOT = Path(__file__).resolve().parents[2] +VALIDATOR = PLUGIN_ROOT / "scripts" / "validate-migration-report.py" +FAILS: list[str] = [] + + +def check(cond: bool, msg: str) -> None: + if not cond: + FAILS.append(msg) + + +def main() -> int: + if len(sys.argv) != 2: + print(__doc__) + return 2 + run = Path(sys.argv[1]) + + # Terminal state: decide-complete, not failure, not in-flight. + ph_path = run / ".phase-status.json" + check(ph_path.exists(), "missing .phase-status.json") + if not ph_path.exists(): + print("FAIL") + [print(" -", f) for f in FAILS] + return 1 + ph = json.loads(ph_path.read_text()) + phases = ph.get("phases") or {} + check(ph.get("current_phase") == "complete", f"current_phase={ph.get('current_phase')}") + check(ph.get("run_mode") == "decide", f"run_mode={ph.get('run_mode')}") + check(phases.get("generate") == "pending", f"generate={phases.get('generate')}") + check(phases.get("estimate") == "completed", f"estimate={phases.get('estimate')}") + check(phases.get("workshop") == "completed", f"workshop={phases.get('workshop')}") + + # Decision pack exists; execution artifacts do not. + report = run / "decision-report.html" + check(report.exists(), "missing decision-report.html") + check((run / "DECISION.md").exists(), "missing DECISION.md") + check(not (run / "terraform").exists(), "terraform/ must not exist on a decide run") + check( + not any(run.glob("generation-*.json")), + "generation-*.json must not exist on a decide run", + ) + + # The decision report passes the validator in decision mode. + if report.exists(): + result = subprocess.run( + [sys.executable, str(VALIDATOR), str(report), "--mode", "decision", + "--no-require-toc"], + capture_output=True, + text=True, + ) + check( + result.returncode == 0, + f"decision-report.html fails --mode decision:\n{result.stdout}{result.stderr}", + ) + + if FAILS: + print("FAIL") + [print(" -", f) for f in FAILS] + return 1 + print("PASS") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/migrate/plugins/migration-to-aws/scripts/validate-migration-report.py b/migrate/plugins/migration-to-aws/scripts/validate-migration-report.py index fee13278..c649b835 100644 --- a/migrate/plugins/migration-to-aws/scripts/validate-migration-report.py +++ b/migrate/plugins/migration-to-aws/scripts/validate-migration-report.py @@ -41,6 +41,19 @@ "appendix-artifacts", ] +# Decision mode (decision-report.html rendered at the post-Estimate Decision +# gate, choice A): executive sections + CTA only — appendices are forbidden +# because no Generate artifacts exist yet. +DECISION_REQUIRED_SECTION_IDS = [ + "decision-summary", + "exec-assumptions", + "exec-services", + "exec-costs", + "exec-timeline", + "exec-risks", + "decision-cta", +] + OPTIONAL_SECTION_IDS = [ "exec-tco", "exec-architecture", @@ -152,10 +165,12 @@ def _section_id_counts(html: str) -> dict[str, int]: return counts -def _validate_required_sections(html: str) -> list[str]: +def _validate_required_sections( + html: str, required_ids: list[str] | None = None +) -> list[str]: errors: list[str] = [] counts = _section_id_counts(html) - for section_id in REQUIRED_SECTION_IDS: + for section_id in required_ids if required_ids is not None else REQUIRED_SECTION_IDS: n = counts.get(section_id, 0) if n == 0: errors.append(f'missing required
') @@ -175,7 +190,9 @@ def _toc_hrefs(html: str) -> list[str]: return re.findall(r'href="#([^"]+)"', nav_match.group(1), re.IGNORECASE) -def _validate_toc(html: str) -> list[str]: +def _validate_toc(html: str, required_ids: list[str] | None = None) -> list[str]: + if required_ids is None: + required_ids = REQUIRED_SECTION_IDS errors: list[str] = [] hrefs = _toc_hrefs(html) if not hrefs: @@ -187,7 +204,7 @@ def _validate_toc(html: str) -> list[str]: errors.append(f'TOC broken link href="#{href}" — no matching
') # Every required section must be linked from the TOC. - for section_id in REQUIRED_SECTION_IDS: + for section_id in required_ids: if section_id in section_ids and section_id not in hrefs and hrefs: errors.append( f'TOC missing link to required section id="{section_id}" ' @@ -427,15 +444,27 @@ def validate_report( require_toc: bool = True, check_readability: bool = True, migration_dir: Path | None = None, + mode: str = "full", ) -> list[str]: errors: list[str] = [] - errors.extend(_validate_required_sections(html)) + required_ids = DECISION_REQUIRED_SECTION_IDS if mode == "decision" else REQUIRED_SECTION_IDS + errors.extend(_validate_required_sections(html, required_ids)) + + if mode == "decision": + # No Generate artifacts exist at decision time: appendices are forbidden. + counts = _section_id_counts(html) + for sid in counts: + if sid.startswith("appendix-"): + errors.append( + f'decision mode forbids
— the decision report ' + "has no appendices; the full migration report (Generate) carries them" + ) if require_toc: if not _toc_hrefs(html): errors.append('missing