From 4dff7285aeebaad2a232b97d0fe850022a2af135 Mon Sep 17 00:00:00 2001 From: Logan Kleier Date: Sun, 19 Jul 2026 21:18:10 -0700 Subject: [PATCH] refactor(migrate): canonical workshop invariants + estimate pricing-mode, vendored Merge-last PR: built from #152 + #153 + #154 (plus #148 via #152's lineage). After those merge, three near-identical copies of the workshop contract and the estimate Step 0 pricing-mode block would exist -- the exact drift pattern that produced the cached_stale enum bug and the vendored-edited-instead-of-canonical arm64 rates. - skills/shared/workshop/workshop-invariants.md (canonical): frozen discovery + fingerprint, checkpoint state semantics incl. the warm-start rule, the inner-runs allowed/forbidden contract, scenario store rules (max-5 warn-and-name eviction, working-tree==active, estimation_summary core fields), region honesty, and the calculator link procedure. Vendored into all three skills; gcp gains a references/vendored tree (now 4 skills under shared:check). - skills/shared/estimate/pricing-mode.md (canonical Step 0): cache staleness, MCP retry ladder, display modes, and the 5-rung pricing hierarchy incl. the estimated rung. heroku + vercel cost engines now execute the vendored copy as Step 0 instead of restating it; vercel keeps its skill-specific Step 0a-workshop knobs section. - Skill workshop files keep knobs/artifact names/engine refreshes and defer to the invariants file for every invariant (workshop.md hard rules note the canonical wins on disagreement; refresh inner-runs and 6b calculator sections are pointers with skill-specific lines only). - READMEs unioned across the three branches; canonical estimation-infra schema takes the workshop-metadata side + shared:sync. Validated: shared:check green across 4 vendored trees, fixtures-check green (123 json / 8 asserters / 10 seeds), all three workshop asserters PASS on the merged tree, frontmatter validation green, full mise build green. --- .../phases/workshop/workshop-refresh.md | 50 +++-------- .../references/phases/workshop/workshop.md | 6 ++ .../gcp-to-aws/references/vendored/README.md | 24 +++++ .../vendored/workshop/workshop-invariants.md | 90 +++++++++++++++++++ .../phases/estimate/estimate-cost-engine.md | 66 ++------------ .../phases/workshop/workshop-refresh.md | 52 ++--------- .../references/phases/workshop/workshop.md | 6 ++ .../vendored/estimate/pricing-mode.md | 64 +++++++++++++ .../vendored/workshop/workshop-invariants.md | 90 +++++++++++++++++++ .../skills/shared/estimate/pricing-mode.md | 64 +++++++++++++ .../shared/workshop/workshop-invariants.md | 90 +++++++++++++++++++ .../phases/estimate/estimate-cost-engine.md | 67 ++------------ .../phases/workshop/workshop-refresh.md | 53 +++-------- .../references/phases/workshop/workshop.md | 6 ++ .../vendored/estimate/pricing-mode.md | 64 +++++++++++++ .../vendored/workshop/workshop-invariants.md | 90 +++++++++++++++++++ 16 files changed, 642 insertions(+), 240 deletions(-) create mode 100644 migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/vendored/README.md create mode 100644 migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/vendored/workshop/workshop-invariants.md create mode 100644 migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/vendored/estimate/pricing-mode.md create mode 100644 migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/vendored/workshop/workshop-invariants.md create mode 100644 migrate/plugins/migration-to-aws/skills/shared/estimate/pricing-mode.md create mode 100644 migrate/plugins/migration-to-aws/skills/shared/workshop/workshop-invariants.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/vendored/estimate/pricing-mode.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/vendored/workshop/workshop-invariants.md diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-refresh.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-refresh.md index 40f09d94..b119dc6e 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-refresh.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-refresh.md @@ -2,26 +2,13 @@ ## Inner runs (artifact-only) — mandatory -When re-running Design or Estimate from this file: **rewrite artifacts only**. -Do **not** advance the backbone mid-workshop. - -| Allowed | Forbidden | -| ----------------------------------------------------- | ------------------------------------------------------- | -| Overwrite `aws-design.json` / `estimation-infra.json` | Set design/estimate to `in_progress` | -| Soft-validate estimate invariants before snapshot | Emit `HANDOFF_OK` from Design or Estimate | -| Brief chat note that reprice finished | Touch `current_phase` or advance to `generate` | -| Keep `phases.workshop` as `in_progress` | Run Estimate's post-Estimate workshop offer (recursion) | - -Leave `phases.design` and `phases.estimate` as `"completed"`. Leave -`current_phase` at `"estimate"` until `workshop-assemble.md`. - -Concrete slices: - -1. **Design** — run `design.md` / `design-infra.md` enough to rewrite - `aws-design.json` (and siblings if the run uses them). Skip handoff and - phase-status updates. -2. **Estimate** — run `estimate.md` / `estimate-infra.md` enough to rewrite - `estimation-infra.json`. Skip `HANDOFF_OK`, phase-status, and the workshop offer. +Follow the canonical allowed/forbidden contract in +`references/vendored/workshop/workshop-invariants.md` § 3 for every inner +Design/Estimate run. GCP specifics: Design follows `design.md` § Inner +workshop reprice and Estimate follows `estimate.md` § Inner workshop +reprice (both skip state transitions); `phases.design`/`phases.estimate` +stay `"completed"`; `current_phase` stays `"estimate"` until +`workshop-assemble.md`. ## Baseline capture @@ -86,22 +73,13 @@ workshop reprice. Chat note after Estimate: ### 6b. Shareable calculator link (best-effort, never blocks) -If the `aws-pricing-calculator` MCP server is available (try `get_server_info` -once; do NOT retry on failure): - -1. Prefer the one-shot `build_estimate` (create + add + lint + save): name - `"GCP migration — {scenario label} ({target_region})"`, services from the - scenario's Balanced-tier `estimation-infra.json` breakdown, each with the - scenario's `target_region` — the calculator computes REGIONAL prices - server-side, which the us-east-1 cache cannot. On a structured - needs-field-discovery response, resolve via `get_service_fields` and retry - ONCE; else fall back to `create_estimate` → `add_service` → - `export_estimate`. -2. Store the URL as `estimation_summary.calculator_url` in the manifest. -3. Any failure or unmappable service → `calculator_url: null`, one chat note, - continue. Workshop numbers stay authoritative; the link is a stakeholder - artifact. (This aligns with #49's Estimate-phase calculator integration — - same server, same degradation rules.) +Follow the canonical procedure in +`references/vendored/workshop/workshop-invariants.md` § 6 with +`{SKILL_LABEL}` = "GCP" — probe once, prefer `build_estimate` on the +scenario's Balanced-tier services, store the URL as +`estimation_summary.calculator_url`, null + one chat note on any failure. +(Aligns with #49's Estimate-phase calculator integration — same server, +same degradation rules.) ### 7. Hand back diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop.md index e60cab78..435bfafa 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop.md @@ -47,6 +47,12 @@ | No BigQuery target knobs | Keep deferred specialist rows; do not invent warehouse targets | | No agentic outcome override | Do not mutate `ai_constraints.agentic.*` in v1 | +These rules restate the canonical contract in +`references/vendored/workshop/workshop-invariants.md` (vendored from +`skills/shared/workshop/workshop-invariants.md`, kept byte-identical by +`shared:sync`). When this table and that file disagree, the invariants +file wins — fix this table. + ## Decline without entering When Estimate offer **[B] Proceed toward Generate** is chosen, mark diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/vendored/README.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/vendored/README.md new file mode 100644 index 00000000..abf863ec --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/vendored/README.md @@ -0,0 +1,24 @@ +# Vendored shared files — DO NOT EDIT + +These files are **synced copies** of the plugin-level canonical source under +`migrate/plugins/migration-to-aws/skills/shared/`. They are vendored into this skill +so the skill folder is **self-contained** — it runs standalone (lifted out, zipped, +or used on its own) without reaching outside its own directory. + +**Do not hand-edit anything in this directory.** Edit the canonical source instead, +then re-sync: + +```sh +mise run shared:sync # copy canonical -> every skill's references/vendored/ +``` + +CI enforces that these copies are byte-identical to the canonical source +(`mise run shared:check`, wired into `build`). A stale copy fails the build. + +| Vendored path | Canonical source | +| --------------------------------------- | ----------------------------------------------------- | +| `dsl/INTERPRETER.md` | `skills/shared/dsl/INTERPRETER.md` | +| `state/phase-status.schema.json` | `skills/shared/state/phase-status.schema.json` | +| `estimate/complexity-tiers.json` | `skills/shared/estimate/complexity-tiers.json` | +| `estimate/estimation-infra.schema.json` | `skills/shared/estimate/estimation-infra.schema.json` | +| `pricing/aws-infra-pricing.json` | `skills/shared/pricing/aws-infra-pricing.json` | diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/vendored/workshop/workshop-invariants.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/vendored/workshop/workshop-invariants.md new file mode 100644 index 00000000..32247c07 --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/vendored/workshop/workshop-invariants.md @@ -0,0 +1,90 @@ +# What-If Workshop — Cross-Skill Invariants (canonical) + +> Canonical contract for the post-Estimate what-if workshop, vendored into each +> skill (`references/vendored/workshop/workshop-invariants.md`) and kept +> byte-identical by `shared:sync`. Skill workshop files (`workshop.md`, +> `workshop-sheet.md`, `workshop-refresh.md`, `workshop-compare.md`, +> `workshop-assemble.md`) own the skill-SPECIFIC parts — knobs, artifact names, +> engine refreshes — and defer to THIS file for every invariant below. When a +> skill file and this file disagree on an invariant, this file wins; fix the +> skill file. +> +> Placeholders: `{INVENTORY}` = the skill's frozen discovery artifact +> (`heroku-resource-inventory.json` / `discovery.json` / +> `gcp-resource-inventory.json`); `{SKILL_LABEL}` = "Heroku" / "Vercel" / "GCP". + +## 1. Discovery is frozen + +- The workshop NEVER writes `{INVENTORY}`, capture directories, or any + discovery-derived analysis artifact (coupling, preflight, clusters). +- `scenarios/index.json.inventory_fingerprint` = SHA-256 hex of the raw + `{INVENTORY}` bytes (no JSON re-serialization), recorded at baseline capture. +- Every Apply & reprice MUST recompute the fingerprint first and ABORT with + "Inventory changed since baseline. Re-run Discover before workshop reprice." + on any difference. + +## 2. Checkpoint state semantics + +- The workshop is a checkpoint: it NEVER becomes `current_phase`. Entry sets + `phases.workshop: "in_progress"`; `current_phase` stays at `"estimate"` + until exit/decline. +- Exit to Generate (assembler): `phases.workshop: "completed"`, + `current_phase: "generate"`. Decline at the Estimate offer: same, without + requiring `scenarios/`. +- Warm-start rule: `current_phase == "estimate"` AND + `phases.estimate == "completed"` AND `phases.workshop == "pending"` → + present the workshop offer; NEVER recompute Estimate. +- If Generate (or later) is already `completed`, apply the Estimate re-entry + guard (confirm → reset downstream to pending) before any refresh. + +## 3. Inner runs are artifact-only + +When the refresh re-runs a backbone phase (Design/Recommend/Estimate) to +reprice: + +| Allowed | Forbidden | +| ------------------------------------- | ---------------------------------------------------------------------------------------- | +| Overwrite that phase's artifact(s) | Set the phase to `in_progress`/`completed` (they stay `completed`) | +| Soft-validate before snapshot | Emit `HANDOFF_OK` from the inner phase | +| One brief chat note when done | Touch `current_phase` or advance the backbone | +| Keep `phases.workshop: "in_progress"` | Run the post-Estimate workshop offer (recursion) | +| | Fail on `_check_single_active_phase`-style preconditions because workshop is in progress | + +## 4. Scenario store + +- Max **5** scenarios. Before evicting, WARN and NAME the victim (id + label); + never delete `baseline_scenario_id` unless the user explicitly resets. +- Working tree == active scenario: the skill's preference/design/estimate + artifacts always match `index.active_scenario_id`. +- Each manifest's `estimation_summary` carries at minimum: + `aws_monthly_premium` / `_balanced` / `_optimized`, `complexity_tier`, + `pricing_source`, `region_note` (nullable), `calculator_url` (nullable). +- `preferences_subset` records ONLY the knob paths that differ from baseline. + +## 5. Region honesty + +The sheet always shows: region repricing needs live pricing access (awspricing +MCP) for true regional rates; without it, numbers stay on the us-east-1 cache +basis and every affected estimate carries a `region_note`. Never present +cache-based numbers as regional. + +## 6. Shareable calculator link (best-effort, never blocks) + +After each scenario snapshot, if the `aws-pricing-calculator` MCP server is +available (probe `get_server_info` once; no retry on failure): + +1. Prefer one-shot `build_estimate`: name + `"{SKILL_LABEL} migration — {scenario label} ({target_region})"`, services + from the scenario's Balanced-tier estimate breakdown (the PRIMARY outcome's + set where outcomes exist), each with the scenario's target region — the + calculator computes REGIONAL prices server-side, which the cache cannot. + On a structured needs-field-discovery response, resolve via + `get_service_fields` and retry ONCE; else fall back to + `create_estimate` → `add_service` → `export_estimate`. +2. Store the URL as the manifest's `estimation_summary.calculator_url`. +3. Any failure or unmappable service → `calculator_url: null`, one chat note, + continue. Workshop numbers stay authoritative; the link is a complementary + stakeholder artifact. Unconfigured server → silent null. + +Compare tables and stakeholder reports render one link line per non-null +`calculator_url`. 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 76e74c8b..abff1aaf 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 @@ -20,66 +20,12 @@ _contributes: ## Step 0: Pricing Mode Selection -### Step 0a: Load Pricing Cache - -Read `references/vendored/pricing/aws-infra-pricing.json` (shared AWS infrastructure pricing data). Check the `_meta.last_updated` date: - -- If ≤ 30 days old: **Cached prices are the primary source.** No MCP calls needed for services listed in the file. Set `pricing_source: "cached"`. -- If > 30 days old: Infrastructure prices (Fargate, RDS, S3, etc.) remain reliable. Attempt MCP (Step 0b) for services not in the file; use the cached rates as fallback with `pricing_source: "cached_stale"`. - -**Region honesty:** Read `preferences.json.global.target_region`. The pricing cache -`_meta.region` is `us-east-1`. When `target_region` differs: - -1. Prefer MCP `get_pricing` with that region for services you look up live. -2. When still using cache rates, set - `workshop.region_note` (and surface in the estimate narrative): - `"Rates from us-east-1 cache applied to {target_region} — verify via awspricing MCP for regional deltas."` -3. When regions match, set `workshop.region_note` to `null` / omit. - -**Scenario metadata:** If `preferences.workshop.active_scenario_id` is set, carry -it into the cost-engine contribution as `workshop.scenario_id` for the assembler. - -**Fargate arch rates:** When a Fargate service has `aws_config.cpu_architecture` -of `ARM64` (or preferences `workshop.cpu_architecture` is `arm64`), use -`fargate.per_vcpu_hour_arm64` / `fargate.per_gb_mem_hour_arm64` when present; -else x86 Fargate rates + warning. - -Each service object carries its rates and (where relevant) a `multi_az_handling` key. Look up the rates from there — do not hardcode them. Apply the cost formula from the Per-Service Calculation Formulas table below. - -### Step 0b: MCP Availability Check (only if cache stale or service not listed) - -Attempt to reach awspricing MCP with **up to 2 retries** (3 total attempts, 10-second timeout per attempt): - -1. **Attempt 1**: Call `get_pricing_service_codes()` -2. **If timeout/error after 10s**: Wait 1 second, retry (Attempt 2) -3. **If still fails after 10s**: Wait 2 seconds, retry (Attempt 3) -4. **If all 3 attempts fail**: Use cached prices. Set `pricing_source: "cached_fallback"`. - -### Step 0c: Display Pricing Mode to User - -**Before any calculation**, surface the pricing status: - -- **Cache fresh + all services covered**: "Pricing source: cached (updated [date], ±5-10% accuracy). Live pricing API not required." -- **Cache stale + MCP available**: "Pricing source: live API (awspricing MCP). Cache is stale ([date]) — using real-time pricing." -- **Cache stale + MCP unavailable**: "⚠️ Pricing source: stale cache only (updated [date]). The awspricing MCP server is unreachable. Proceeding with cached pricing; accuracy ±5-10% for infrastructure." -- **Service not in cache + MCP unavailable**: "⚠️ Some services not in pricing cache and MCP unreachable. Those services will show `pricing_source: unavailable` in the estimate." - -### Pricing Hierarchy (per-service lookup order) - -| Priority | Source | Condition | `pricing_source` value | -| -------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------- | -| 1 | `references/vendored/pricing/aws-infra-pricing.json` | Service found in the pricing file | `"cached"` | -| 2 | MCP API (`get_pricing`) | Service NOT in the file, MCP available | `"live"` | -| 3 | Pricing file after MCP failure | MCP attempted but failed, service IS in file | `"cached_fallback"` | -| 4 | Formula constants / well-known published rate | NOT in file, MCP failed, but this file's own formulas carry the rate (state it verbatim) | `"estimated"` | -| 5 | Unavailable | NOT in file, MCP failed, no formula constant either | `"unavailable"` | - -Row 4 is the documented home of the `services_by_source.estimated` bucket the -schema and assembler already carry: a service priced from a rate this file -itself states (never a guessed or remembered number) is `"estimated"`, always -accompanied by a warning naming the rate and its source. Only a service with -no cache entry, no MCP, AND no stated formula rate is `"unavailable"` and -excluded from totals. +Execute `references/vendored/estimate/pricing-mode.md` (the canonical +Step 0, vendored from `skills/shared/estimate/pricing-mode.md` and kept +byte-identical by `shared:sync`) as this step: cache staleness check, MCP +retry ladder, pricing-mode display, and the per-service pricing hierarchy +(including the `estimated` and `unavailable` rungs). Do not restate or +fork that logic here. For typical Heroku migrations (Elastic Beanstalk, Fargate, RDS, Aurora, ElastiCache, ALB, NAT Gateway, S3, CloudWatch, Secrets Manager, EventBridge, SES, OpenSearch, MQ), ALL prices are in `aws-infra-pricing.json`. Zero MCP calls needed. diff --git a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/workshop/workshop-refresh.md b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/workshop/workshop-refresh.md index 28e01ce6..e393b0c4 100644 --- a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/workshop/workshop-refresh.md +++ b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/workshop/workshop-refresh.md @@ -10,30 +10,11 @@ _of_phase: workshop ## Inner runs (artifact-only) — mandatory -When this fragment re-runs Design or Estimate, treat them like an `_exec` worker's -WORK slice (`INTERPRETER.md` § `_exec`): **fragments + assembler artifact write -only**. Do **not** advance the backbone mid-workshop. - -| Allowed | Forbidden | -| ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -| Overwrite `aws-design.json` / `estimation-infra.json` | Set `phases.design` / `phases.estimate` to `in_progress` | -| Soft-validate Property-16 / schema before snapshot | Emit `HANDOFF_OK` from Design or Estimate | -| Brief chat note that reprice finished | Touch `.phase-status.json` `current_phase` or advance to `generate` | -| Keep `phases.workshop` as `in_progress` | Run Estimate's post-Estimate workshop offer (recursion) | -| | Evaluate Design/Estimate `_preconditions` that fail on `_check_single_active_phase` because workshop is already `in_progress` | - -Leave `phases.design` and `phases.estimate` as `"completed"`. Leave -`current_phase` at `"estimate"` until the user exits workshop via -`workshop-assemble.md`. - -Concrete file slices: - -1. **Design** — run `design.md` fragments + the **write** portion of - `design-assemble.md`. Skip Completion Handoff Gate `HANDOFF_OK` and Step 8 - (phase-status / advance). -2. **Estimate** — run `estimate-cost-engine.md` + the **write + Present Summary** - portions of `estimate-assemble.md`. Skip `HANDOFF_OK`, phase-status update, - deferred-advance logic, and the what-if workshop offer. +Follow `references/vendored/workshop/workshop-invariants.md` § 3 (canonical +allowed/forbidden contract) for every inner Design/Estimate run. Heroku +specifics: leave `phases.design` and `phases.estimate` as `"completed"`; +the inner Estimate skips the post-Estimate workshop offer; `current_phase` +stays `"estimate"` until `workshop-assemble.md`. ## Baseline capture (no Design yet) @@ -119,25 +100,10 @@ Execute Estimate per **Inner runs** above. Overwrite ### 6b. Shareable calculator link (best-effort, never blocks) -If the `aws-pricing-calculator` MCP server is available (its tools respond — -try `get_server_info` once; do NOT retry on failure): - -1. Prefer the one-shot `build_estimate` (create + add services + lint + save): - name `"Heroku migration — {scenario label} ({target_region})"`, services - from the scenario's Balanced-tier `estimation-infra.json` breakdown, each - with the scenario's `target_region` — the calculator computes REGIONAL - prices server-side, which is exactly what the us-east-1 cache cannot do. - On a structured needs-field-discovery response, resolve via - `get_service_fields` and retry ONCE; else fall back to - `create_estimate` → `add_service` → `export_estimate`. -2. Store the returned URL as `estimation_summary.calculator_url` in the - scenario manifest. -3. Any tool failure or an unmappable service → set `calculator_url: null`, - note the reason once in chat, continue. The workshop's own numbers remain - authoritative; the link is a complementary stakeholder artifact. - -If the server is not configured: set `calculator_url: null` silently (the -sheet already explains regional-rate limits). +Follow `references/vendored/workshop/workshop-invariants.md` § 6 with +`{SKILL_LABEL}` = "Heroku" — probe once, prefer `build_estimate` on the +scenario's Balanced-tier services, store the URL as +`estimation_summary.calculator_url`, null + one chat note on any failure. ### 7. Hand back diff --git a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/workshop/workshop.md b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/workshop/workshop.md index 31872399..dc4510e8 100644 --- a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/workshop/workshop.md +++ b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/phases/workshop/workshop.md @@ -85,6 +85,12 @@ _postconditions: | Working tree = active | prefs / design / estimation match active scenario | | No Generate in loop | Mark stale via re-entry; user confirms | +These rules restate the canonical contract in +`references/vendored/workshop/workshop-invariants.md` (vendored from +`skills/shared/workshop/workshop-invariants.md`, kept byte-identical by +`shared:sync`). When this table and that file disagree, the invariants +file wins — fix this table. + ## Decline without entering When Estimate offer **[B] Proceed toward Generate** is chosen, do not enter this diff --git a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/vendored/estimate/pricing-mode.md b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/vendored/estimate/pricing-mode.md new file mode 100644 index 00000000..c3c27286 --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/vendored/estimate/pricing-mode.md @@ -0,0 +1,64 @@ +# Estimate — Pricing Mode Selection (canonical Step 0) + +> Canonical pricing-mode procedure for estimate cost engines, vendored into +> each skill (`references/vendored/estimate/pricing-mode.md`) and kept +> byte-identical by `shared:sync`. The `cached_stale` enum bug happened because +> two copies of this logic evolved separately — do not fork this text again. +> Skill cost engines execute this file AS their Step 0, then own everything +> after it (baseline rungs, service formulas, tiers). + +## Step 0a: Load the pricing cache + +Read `references/vendored/pricing/aws-infra-pricing.json`. Check +`_meta.last_updated` against `_meta.staleness_days` (default 30): + +- Within the window: **cached prices are the primary source.** No MCP calls + needed for services in the file. Set `pricing_source: "cached"`. +- Past the window: infrastructure prices remain reliable. Attempt MCP (Step + 0b) for services not in the file; use cached rates as fallback with + `pricing_source: "cached_stale"`. + +Each service object carries its rates and (where relevant) a +`multi_az_handling` key. Look rates up from the file — never hardcode them. + +## Step 0b: MCP availability check (only if cache stale or service not listed) + +Attempt the awspricing MCP with **up to 2 retries** (3 total attempts, +10-second timeout per attempt): + +1. Attempt 1: `get_pricing_service_codes()` +2. Timeout/error → wait 1s, attempt 2 +3. Timeout/error → wait 2s, attempt 3 +4. All 3 fail → cached prices, `pricing_source: "cached_fallback"` + +## Step 0c: Display the pricing mode + +Before any calculation, surface the status: + +- Cache fresh + all services covered: "Pricing source: cached (updated + [date], ±5-10% accuracy). Live pricing API not required." +- Cache stale + MCP available: "Pricing source: live API (awspricing MCP). + Cache is stale ([date]) — using real-time pricing." +- Cache stale + MCP unavailable: "Pricing source: stale cache only (updated + [date]). The awspricing MCP server is unreachable. Proceeding with cached + pricing; accuracy ±5-10% for infrastructure." +- Service not in cache + MCP unavailable: "Some services not in pricing cache + and MCP unreachable. Those services will show `pricing_source: unavailable` + in the estimate." + +## Pricing hierarchy (per-service lookup order) + +| Priority | Source | Condition | `pricing_source` value | +| -------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------- | +| 1 | `references/vendored/pricing/aws-infra-pricing.json` | Service found in the pricing file | `"cached"` | +| 2 | MCP API (`get_pricing`) | Service NOT in the file, MCP available | `"live"` | +| 3 | Pricing file after MCP failure | MCP attempted but failed, service IS in file | `"cached_fallback"` | +| 4 | Formula constants / well-known published rate | NOT in file, MCP failed, but the cost engine's own formulas carry the rate (state it verbatim) | `"estimated"` | +| 5 | Unavailable | NOT in file, MCP failed, no formula constant either | `"unavailable"` | + +Row 4 is the documented home of the `services_by_source.estimated` bucket the +shared schema and assemblers carry: a service priced from a rate the cost +engine itself states (never a guessed or remembered number) is `"estimated"`, +always accompanied by a warning naming the rate and its source. Only a service +with no cache entry, no MCP, AND no stated formula rate is `"unavailable"` and +excluded from totals. diff --git a/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/vendored/workshop/workshop-invariants.md b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/vendored/workshop/workshop-invariants.md new file mode 100644 index 00000000..32247c07 --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/heroku-to-aws/references/vendored/workshop/workshop-invariants.md @@ -0,0 +1,90 @@ +# What-If Workshop — Cross-Skill Invariants (canonical) + +> Canonical contract for the post-Estimate what-if workshop, vendored into each +> skill (`references/vendored/workshop/workshop-invariants.md`) and kept +> byte-identical by `shared:sync`. Skill workshop files (`workshop.md`, +> `workshop-sheet.md`, `workshop-refresh.md`, `workshop-compare.md`, +> `workshop-assemble.md`) own the skill-SPECIFIC parts — knobs, artifact names, +> engine refreshes — and defer to THIS file for every invariant below. When a +> skill file and this file disagree on an invariant, this file wins; fix the +> skill file. +> +> Placeholders: `{INVENTORY}` = the skill's frozen discovery artifact +> (`heroku-resource-inventory.json` / `discovery.json` / +> `gcp-resource-inventory.json`); `{SKILL_LABEL}` = "Heroku" / "Vercel" / "GCP". + +## 1. Discovery is frozen + +- The workshop NEVER writes `{INVENTORY}`, capture directories, or any + discovery-derived analysis artifact (coupling, preflight, clusters). +- `scenarios/index.json.inventory_fingerprint` = SHA-256 hex of the raw + `{INVENTORY}` bytes (no JSON re-serialization), recorded at baseline capture. +- Every Apply & reprice MUST recompute the fingerprint first and ABORT with + "Inventory changed since baseline. Re-run Discover before workshop reprice." + on any difference. + +## 2. Checkpoint state semantics + +- The workshop is a checkpoint: it NEVER becomes `current_phase`. Entry sets + `phases.workshop: "in_progress"`; `current_phase` stays at `"estimate"` + until exit/decline. +- Exit to Generate (assembler): `phases.workshop: "completed"`, + `current_phase: "generate"`. Decline at the Estimate offer: same, without + requiring `scenarios/`. +- Warm-start rule: `current_phase == "estimate"` AND + `phases.estimate == "completed"` AND `phases.workshop == "pending"` → + present the workshop offer; NEVER recompute Estimate. +- If Generate (or later) is already `completed`, apply the Estimate re-entry + guard (confirm → reset downstream to pending) before any refresh. + +## 3. Inner runs are artifact-only + +When the refresh re-runs a backbone phase (Design/Recommend/Estimate) to +reprice: + +| Allowed | Forbidden | +| ------------------------------------- | ---------------------------------------------------------------------------------------- | +| Overwrite that phase's artifact(s) | Set the phase to `in_progress`/`completed` (they stay `completed`) | +| Soft-validate before snapshot | Emit `HANDOFF_OK` from the inner phase | +| One brief chat note when done | Touch `current_phase` or advance the backbone | +| Keep `phases.workshop: "in_progress"` | Run the post-Estimate workshop offer (recursion) | +| | Fail on `_check_single_active_phase`-style preconditions because workshop is in progress | + +## 4. Scenario store + +- Max **5** scenarios. Before evicting, WARN and NAME the victim (id + label); + never delete `baseline_scenario_id` unless the user explicitly resets. +- Working tree == active scenario: the skill's preference/design/estimate + artifacts always match `index.active_scenario_id`. +- Each manifest's `estimation_summary` carries at minimum: + `aws_monthly_premium` / `_balanced` / `_optimized`, `complexity_tier`, + `pricing_source`, `region_note` (nullable), `calculator_url` (nullable). +- `preferences_subset` records ONLY the knob paths that differ from baseline. + +## 5. Region honesty + +The sheet always shows: region repricing needs live pricing access (awspricing +MCP) for true regional rates; without it, numbers stay on the us-east-1 cache +basis and every affected estimate carries a `region_note`. Never present +cache-based numbers as regional. + +## 6. Shareable calculator link (best-effort, never blocks) + +After each scenario snapshot, if the `aws-pricing-calculator` MCP server is +available (probe `get_server_info` once; no retry on failure): + +1. Prefer one-shot `build_estimate`: name + `"{SKILL_LABEL} migration — {scenario label} ({target_region})"`, services + from the scenario's Balanced-tier estimate breakdown (the PRIMARY outcome's + set where outcomes exist), each with the scenario's target region — the + calculator computes REGIONAL prices server-side, which the cache cannot. + On a structured needs-field-discovery response, resolve via + `get_service_fields` and retry ONCE; else fall back to + `create_estimate` → `add_service` → `export_estimate`. +2. Store the URL as the manifest's `estimation_summary.calculator_url`. +3. Any failure or unmappable service → `calculator_url: null`, one chat note, + continue. Workshop numbers stay authoritative; the link is a complementary + stakeholder artifact. Unconfigured server → silent null. + +Compare tables and stakeholder reports render one link line per non-null +`calculator_url`. diff --git a/migrate/plugins/migration-to-aws/skills/shared/estimate/pricing-mode.md b/migrate/plugins/migration-to-aws/skills/shared/estimate/pricing-mode.md new file mode 100644 index 00000000..c3c27286 --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/shared/estimate/pricing-mode.md @@ -0,0 +1,64 @@ +# Estimate — Pricing Mode Selection (canonical Step 0) + +> Canonical pricing-mode procedure for estimate cost engines, vendored into +> each skill (`references/vendored/estimate/pricing-mode.md`) and kept +> byte-identical by `shared:sync`. The `cached_stale` enum bug happened because +> two copies of this logic evolved separately — do not fork this text again. +> Skill cost engines execute this file AS their Step 0, then own everything +> after it (baseline rungs, service formulas, tiers). + +## Step 0a: Load the pricing cache + +Read `references/vendored/pricing/aws-infra-pricing.json`. Check +`_meta.last_updated` against `_meta.staleness_days` (default 30): + +- Within the window: **cached prices are the primary source.** No MCP calls + needed for services in the file. Set `pricing_source: "cached"`. +- Past the window: infrastructure prices remain reliable. Attempt MCP (Step + 0b) for services not in the file; use cached rates as fallback with + `pricing_source: "cached_stale"`. + +Each service object carries its rates and (where relevant) a +`multi_az_handling` key. Look rates up from the file — never hardcode them. + +## Step 0b: MCP availability check (only if cache stale or service not listed) + +Attempt the awspricing MCP with **up to 2 retries** (3 total attempts, +10-second timeout per attempt): + +1. Attempt 1: `get_pricing_service_codes()` +2. Timeout/error → wait 1s, attempt 2 +3. Timeout/error → wait 2s, attempt 3 +4. All 3 fail → cached prices, `pricing_source: "cached_fallback"` + +## Step 0c: Display the pricing mode + +Before any calculation, surface the status: + +- Cache fresh + all services covered: "Pricing source: cached (updated + [date], ±5-10% accuracy). Live pricing API not required." +- Cache stale + MCP available: "Pricing source: live API (awspricing MCP). + Cache is stale ([date]) — using real-time pricing." +- Cache stale + MCP unavailable: "Pricing source: stale cache only (updated + [date]). The awspricing MCP server is unreachable. Proceeding with cached + pricing; accuracy ±5-10% for infrastructure." +- Service not in cache + MCP unavailable: "Some services not in pricing cache + and MCP unreachable. Those services will show `pricing_source: unavailable` + in the estimate." + +## Pricing hierarchy (per-service lookup order) + +| Priority | Source | Condition | `pricing_source` value | +| -------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------- | +| 1 | `references/vendored/pricing/aws-infra-pricing.json` | Service found in the pricing file | `"cached"` | +| 2 | MCP API (`get_pricing`) | Service NOT in the file, MCP available | `"live"` | +| 3 | Pricing file after MCP failure | MCP attempted but failed, service IS in file | `"cached_fallback"` | +| 4 | Formula constants / well-known published rate | NOT in file, MCP failed, but the cost engine's own formulas carry the rate (state it verbatim) | `"estimated"` | +| 5 | Unavailable | NOT in file, MCP failed, no formula constant either | `"unavailable"` | + +Row 4 is the documented home of the `services_by_source.estimated` bucket the +shared schema and assemblers carry: a service priced from a rate the cost +engine itself states (never a guessed or remembered number) is `"estimated"`, +always accompanied by a warning naming the rate and its source. Only a service +with no cache entry, no MCP, AND no stated formula rate is `"unavailable"` and +excluded from totals. diff --git a/migrate/plugins/migration-to-aws/skills/shared/workshop/workshop-invariants.md b/migrate/plugins/migration-to-aws/skills/shared/workshop/workshop-invariants.md new file mode 100644 index 00000000..32247c07 --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/shared/workshop/workshop-invariants.md @@ -0,0 +1,90 @@ +# What-If Workshop — Cross-Skill Invariants (canonical) + +> Canonical contract for the post-Estimate what-if workshop, vendored into each +> skill (`references/vendored/workshop/workshop-invariants.md`) and kept +> byte-identical by `shared:sync`. Skill workshop files (`workshop.md`, +> `workshop-sheet.md`, `workshop-refresh.md`, `workshop-compare.md`, +> `workshop-assemble.md`) own the skill-SPECIFIC parts — knobs, artifact names, +> engine refreshes — and defer to THIS file for every invariant below. When a +> skill file and this file disagree on an invariant, this file wins; fix the +> skill file. +> +> Placeholders: `{INVENTORY}` = the skill's frozen discovery artifact +> (`heroku-resource-inventory.json` / `discovery.json` / +> `gcp-resource-inventory.json`); `{SKILL_LABEL}` = "Heroku" / "Vercel" / "GCP". + +## 1. Discovery is frozen + +- The workshop NEVER writes `{INVENTORY}`, capture directories, or any + discovery-derived analysis artifact (coupling, preflight, clusters). +- `scenarios/index.json.inventory_fingerprint` = SHA-256 hex of the raw + `{INVENTORY}` bytes (no JSON re-serialization), recorded at baseline capture. +- Every Apply & reprice MUST recompute the fingerprint first and ABORT with + "Inventory changed since baseline. Re-run Discover before workshop reprice." + on any difference. + +## 2. Checkpoint state semantics + +- The workshop is a checkpoint: it NEVER becomes `current_phase`. Entry sets + `phases.workshop: "in_progress"`; `current_phase` stays at `"estimate"` + until exit/decline. +- Exit to Generate (assembler): `phases.workshop: "completed"`, + `current_phase: "generate"`. Decline at the Estimate offer: same, without + requiring `scenarios/`. +- Warm-start rule: `current_phase == "estimate"` AND + `phases.estimate == "completed"` AND `phases.workshop == "pending"` → + present the workshop offer; NEVER recompute Estimate. +- If Generate (or later) is already `completed`, apply the Estimate re-entry + guard (confirm → reset downstream to pending) before any refresh. + +## 3. Inner runs are artifact-only + +When the refresh re-runs a backbone phase (Design/Recommend/Estimate) to +reprice: + +| Allowed | Forbidden | +| ------------------------------------- | ---------------------------------------------------------------------------------------- | +| Overwrite that phase's artifact(s) | Set the phase to `in_progress`/`completed` (they stay `completed`) | +| Soft-validate before snapshot | Emit `HANDOFF_OK` from the inner phase | +| One brief chat note when done | Touch `current_phase` or advance the backbone | +| Keep `phases.workshop: "in_progress"` | Run the post-Estimate workshop offer (recursion) | +| | Fail on `_check_single_active_phase`-style preconditions because workshop is in progress | + +## 4. Scenario store + +- Max **5** scenarios. Before evicting, WARN and NAME the victim (id + label); + never delete `baseline_scenario_id` unless the user explicitly resets. +- Working tree == active scenario: the skill's preference/design/estimate + artifacts always match `index.active_scenario_id`. +- Each manifest's `estimation_summary` carries at minimum: + `aws_monthly_premium` / `_balanced` / `_optimized`, `complexity_tier`, + `pricing_source`, `region_note` (nullable), `calculator_url` (nullable). +- `preferences_subset` records ONLY the knob paths that differ from baseline. + +## 5. Region honesty + +The sheet always shows: region repricing needs live pricing access (awspricing +MCP) for true regional rates; without it, numbers stay on the us-east-1 cache +basis and every affected estimate carries a `region_note`. Never present +cache-based numbers as regional. + +## 6. Shareable calculator link (best-effort, never blocks) + +After each scenario snapshot, if the `aws-pricing-calculator` MCP server is +available (probe `get_server_info` once; no retry on failure): + +1. Prefer one-shot `build_estimate`: name + `"{SKILL_LABEL} migration — {scenario label} ({target_region})"`, services + from the scenario's Balanced-tier estimate breakdown (the PRIMARY outcome's + set where outcomes exist), each with the scenario's target region — the + calculator computes REGIONAL prices server-side, which the cache cannot. + On a structured needs-field-discovery response, resolve via + `get_service_fields` and retry ONCE; else fall back to + `create_estimate` → `add_service` → `export_estimate`. +2. Store the URL as the manifest's `estimation_summary.calculator_url`. +3. Any failure or unmappable service → `calculator_url: null`, one chat note, + continue. Workshop numbers stay authoritative; the link is a complementary + stakeholder artifact. Unconfigured server → silent null. + +Compare tables and stakeholder reports render one link line per non-null +`calculator_url`. diff --git a/migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/estimate/estimate-cost-engine.md b/migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/estimate/estimate-cost-engine.md index e66b7a3e..ea35a03f 100644 --- a/migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/estimate/estimate-cost-engine.md +++ b/migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/estimate/estimate-cost-engine.md @@ -19,19 +19,14 @@ _contributes: ## Step 0: Pricing Mode Selection -### Step 0a: Load Pricing Cache +Execute `references/vendored/estimate/pricing-mode.md` (the canonical +Step 0, vendored from `skills/shared/estimate/pricing-mode.md` and kept +byte-identical by `shared:sync`) as this step: cache staleness check, MCP +retry ladder, pricing-mode display, and the per-service pricing hierarchy +(including the `estimated` and `unavailable` rungs). Do not restate or +fork that logic here. -Read `references/vendored/pricing/aws-infra-pricing.json` (shared AWS -infrastructure pricing data). Check the `_meta.last_updated` date: - -- If <= 30 days old: **Cached prices are the primary source.** No MCP calls - needed for services listed in the file. Set `pricing_source: "cached"`. -- If > 30 days old: Infrastructure prices remain reliable. Attempt MCP (Step - 0b) for services not in the file; use the cached rates as fallback with - `pricing_source: "cached_stale"`. - -Each service object carries its rates and (where relevant) a -`multi_az_handling` key. Look up the rates from there — do not hardcode them. +For typical Vercel migrations (Fargate, Lambda, API Gateway, CloudFront, S3, NAT Gateway, ALB, RDS, ElastiCache, EventBridge, Secrets Manager), ALL prices are in `aws-infra-pricing.json`. Zero MCP calls needed in the common case. ### Step 0a-workshop: What-if knobs (when present) @@ -49,54 +44,6 @@ Read optional `clarify-answers.json.workshop` (created by 4. Carry `workshop.active_scenario_id` into the contribution as `workshop.scenario_id` for the assembler. -### Step 0b: MCP Availability Check (only if cache stale or service not listed) - -Attempt to reach awspricing MCP with **up to 2 retries** (3 total attempts, -10-second timeout per attempt): - -1. **Attempt 1**: Call `get_pricing_service_codes()` -2. **If timeout/error after 10s**: Wait 1 second, retry (Attempt 2) -3. **If still fails after 10s**: Wait 2 seconds, retry (Attempt 3) -4. **If all 3 attempts fail**: Use cached prices. Set - `pricing_source: "cached_fallback"`. - -### Step 0c: Display Pricing Mode to User - -**Before any calculation**, surface the pricing status: - -- **Cache fresh + all services covered**: "Pricing source: cached (updated - [date], +/-5-10% accuracy). Live pricing API not required." -- **Cache stale + MCP available**: "Pricing source: live API (awspricing - MCP). Cache is stale ([date]) — using real-time pricing." -- **Cache stale + MCP unavailable**: "Pricing source: stale cache only - (updated [date]). The awspricing MCP server is unreachable. Proceeding with - cached pricing; accuracy +/-5-10% for infrastructure." -- **Service not in cache + MCP unavailable**: "Some services not in pricing - cache and MCP unreachable. Those services will show - `pricing_source: unavailable` in the estimate." - -### Pricing Hierarchy (per-service lookup order) - -| Priority | Source | Condition | `pricing_source` value | -| -------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------- | -| 1 | `references/vendored/pricing/aws-infra-pricing.json` | Service found in the pricing file | `"cached"` | -| 2 | MCP API (`get_pricing`) | Service NOT in the file, MCP available | `"live"` | -| 3 | Pricing file after MCP failure | MCP attempted but failed, service IS in file | `"cached_fallback"` | -| 4 | Formula constants / well-known published rate | NOT in file, MCP failed, but this file's own formulas carry the rate (state it verbatim) | `"estimated"` | -| 5 | Unavailable | NOT in file, MCP failed, no formula constant either | `"unavailable"` | - -Row 4 is the documented home of the `services_by_source.estimated` bucket the -schema and assembler template already carry: a service priced from a rate this -file itself states (the Lambda per-request/GB-second constants in the Outcome A -table are the canonical example — never a guessed or remembered number) is -`"estimated"`, always accompanied by a warning naming the rate and its source. -Only a service with no cache entry, no MCP, AND no stated formula rate is -`"unavailable"` and excluded from totals. - -For typical Vercel migrations (Fargate, Lambda, API Gateway, CloudFront, S3, -NAT Gateway, ALB, RDS, ElastiCache, EventBridge, Secrets Manager), ALL prices -are in `aws-infra-pricing.json`. Zero MCP calls needed in the common case. - --- ## Part 1: Determine Current Vercel Costs diff --git a/migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/workshop/workshop-refresh.md b/migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/workshop/workshop-refresh.md index a4a124bd..7d68ab0b 100644 --- a/migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/workshop/workshop-refresh.md +++ b/migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/workshop/workshop-refresh.md @@ -7,31 +7,13 @@ _of_phase: workshop ## Inner runs (artifact-only) — mandatory -When this fragment re-runs Recommend or Estimate, treat them like an `_exec` -worker's WORK slice (`INTERPRETER.md` § `_exec`): **fragments + assembler -artifact write only**. Do **not** advance the backbone mid-workshop. - -| Allowed | Forbidden | -| --------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| Overwrite `recommendation.json` / `estimation-infra.json` | Set recommend/estimate to `in_progress` | -| Soft-validate before snapshot | Emit `HANDOFF_OK` from Recommend or Estimate | -| Brief chat note that reprice finished | Touch `.phase-status.json` `current_phase` or advance to `generate` | -| Keep `phases.workshop` as `in_progress` | Run Estimate's post-Estimate workshop offer (recursion) | -| | Evaluate Recommend/Estimate `_preconditions` that fail on `_check_single_active_phase` because workshop is already `in_progress` | - -Leave `phases.recommend` and `phases.estimate` as `"completed"`. Leave -`current_phase` at `"estimate"` until the user exits via `workshop-assemble.md`. - -Concrete file slices: - -1. **Recommend** (when not using `outcome_override` patch) — run recommend - fragments + write portion of `recommend-assemble.md`. Skip `HANDOFF_OK` and - phase-status advance. Also refresh the synthetic - `findings.recommend.fired_rule` entry in `assessment-state.json` when that - file exists. -2. **Estimate** — run `estimate-cost-engine.md` + write + Present Summary in - `estimate-assemble.md`. Skip `HANDOFF_OK`, phase-status, deferred-advance, - and the what-if workshop offer. +Follow `references/vendored/workshop/workshop-invariants.md` § 3 (canonical +allowed/forbidden contract) for every inner Recommend/Estimate run. Vercel +specifics: leave `phases.recommend` and `phases.estimate` as `"completed"`; +inner Recommend = fragments + the write portion of `recommend-assemble.md` +only; inner Estimate = `estimate-cost-engine.md` + write + Present Summary, +skipping `HANDOFF_OK`, phase-status, deferred-advance, and the workshop +offer; `current_phase` stays `"estimate"` until `workshop-assemble.md`. ## Baseline capture (no Recommend/Estimate yet) @@ -160,22 +142,11 @@ Cost-engine MUST honor `workshop.target_region`, ### 6b. Shareable calculator link (best-effort, never blocks) -If the `aws-pricing-calculator` MCP server is available (try `get_server_info` -once; do NOT retry on failure): - -1. Prefer the one-shot `build_estimate` (create + add + lint + save): name - `"Vercel migration — {scenario label} ({target_region})"`, services from - the scenario's Balanced-tier `estimation-infra.json` breakdown (the - PRIMARY outcome's set; skip `tiebreak_alternative`), each with the - scenario's `target_region` — the calculator computes REGIONAL prices - server-side, which the us-east-1 cache cannot. On a structured - needs-field-discovery response, resolve via `get_service_fields` and retry - ONCE; else fall back to `create_estimate` → `add_service` → - `export_estimate`. -2. Store the URL as `estimation_summary.calculator_url` in the manifest. -3. Any failure or unmappable service → `calculator_url: null`, one chat note, - continue. Workshop numbers stay authoritative; the link is a stakeholder - artifact. +Follow `references/vendored/workshop/workshop-invariants.md` § 6 with +`{SKILL_LABEL}` = "Vercel" and the PRIMARY outcome's Balanced services +(skip `tiebreak_alternative`) — probe once, prefer `build_estimate`, store +the URL as `estimation_summary.calculator_url`, null + one chat note on +any failure. ### 7. Hand back diff --git a/migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/workshop/workshop.md b/migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/workshop/workshop.md index 6a04624d..14eee351 100644 --- a/migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/workshop/workshop.md +++ b/migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/workshop/workshop.md @@ -87,6 +87,12 @@ _postconditions: | Working tree = active | clarify / recommendation / estimation match active scenario | | No Generate in loop | Mark stale via re-entry; user confirms | +These rules restate the canonical contract in +`references/vendored/workshop/workshop-invariants.md` (vendored from +`skills/shared/workshop/workshop-invariants.md`, kept byte-identical by +`shared:sync`). When this table and that file disagree, the invariants +file wins — fix this table. + ## Decline without entering When Estimate offer **[B] Proceed toward Generate** is chosen, do not enter this diff --git a/migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/vendored/estimate/pricing-mode.md b/migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/vendored/estimate/pricing-mode.md new file mode 100644 index 00000000..c3c27286 --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/vendored/estimate/pricing-mode.md @@ -0,0 +1,64 @@ +# Estimate — Pricing Mode Selection (canonical Step 0) + +> Canonical pricing-mode procedure for estimate cost engines, vendored into +> each skill (`references/vendored/estimate/pricing-mode.md`) and kept +> byte-identical by `shared:sync`. The `cached_stale` enum bug happened because +> two copies of this logic evolved separately — do not fork this text again. +> Skill cost engines execute this file AS their Step 0, then own everything +> after it (baseline rungs, service formulas, tiers). + +## Step 0a: Load the pricing cache + +Read `references/vendored/pricing/aws-infra-pricing.json`. Check +`_meta.last_updated` against `_meta.staleness_days` (default 30): + +- Within the window: **cached prices are the primary source.** No MCP calls + needed for services in the file. Set `pricing_source: "cached"`. +- Past the window: infrastructure prices remain reliable. Attempt MCP (Step + 0b) for services not in the file; use cached rates as fallback with + `pricing_source: "cached_stale"`. + +Each service object carries its rates and (where relevant) a +`multi_az_handling` key. Look rates up from the file — never hardcode them. + +## Step 0b: MCP availability check (only if cache stale or service not listed) + +Attempt the awspricing MCP with **up to 2 retries** (3 total attempts, +10-second timeout per attempt): + +1. Attempt 1: `get_pricing_service_codes()` +2. Timeout/error → wait 1s, attempt 2 +3. Timeout/error → wait 2s, attempt 3 +4. All 3 fail → cached prices, `pricing_source: "cached_fallback"` + +## Step 0c: Display the pricing mode + +Before any calculation, surface the status: + +- Cache fresh + all services covered: "Pricing source: cached (updated + [date], ±5-10% accuracy). Live pricing API not required." +- Cache stale + MCP available: "Pricing source: live API (awspricing MCP). + Cache is stale ([date]) — using real-time pricing." +- Cache stale + MCP unavailable: "Pricing source: stale cache only (updated + [date]). The awspricing MCP server is unreachable. Proceeding with cached + pricing; accuracy ±5-10% for infrastructure." +- Service not in cache + MCP unavailable: "Some services not in pricing cache + and MCP unreachable. Those services will show `pricing_source: unavailable` + in the estimate." + +## Pricing hierarchy (per-service lookup order) + +| Priority | Source | Condition | `pricing_source` value | +| -------- | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------- | +| 1 | `references/vendored/pricing/aws-infra-pricing.json` | Service found in the pricing file | `"cached"` | +| 2 | MCP API (`get_pricing`) | Service NOT in the file, MCP available | `"live"` | +| 3 | Pricing file after MCP failure | MCP attempted but failed, service IS in file | `"cached_fallback"` | +| 4 | Formula constants / well-known published rate | NOT in file, MCP failed, but the cost engine's own formulas carry the rate (state it verbatim) | `"estimated"` | +| 5 | Unavailable | NOT in file, MCP failed, no formula constant either | `"unavailable"` | + +Row 4 is the documented home of the `services_by_source.estimated` bucket the +shared schema and assemblers carry: a service priced from a rate the cost +engine itself states (never a guessed or remembered number) is `"estimated"`, +always accompanied by a warning naming the rate and its source. Only a service +with no cache entry, no MCP, AND no stated formula rate is `"unavailable"` and +excluded from totals. diff --git a/migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/vendored/workshop/workshop-invariants.md b/migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/vendored/workshop/workshop-invariants.md new file mode 100644 index 00000000..32247c07 --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/vendored/workshop/workshop-invariants.md @@ -0,0 +1,90 @@ +# What-If Workshop — Cross-Skill Invariants (canonical) + +> Canonical contract for the post-Estimate what-if workshop, vendored into each +> skill (`references/vendored/workshop/workshop-invariants.md`) and kept +> byte-identical by `shared:sync`. Skill workshop files (`workshop.md`, +> `workshop-sheet.md`, `workshop-refresh.md`, `workshop-compare.md`, +> `workshop-assemble.md`) own the skill-SPECIFIC parts — knobs, artifact names, +> engine refreshes — and defer to THIS file for every invariant below. When a +> skill file and this file disagree on an invariant, this file wins; fix the +> skill file. +> +> Placeholders: `{INVENTORY}` = the skill's frozen discovery artifact +> (`heroku-resource-inventory.json` / `discovery.json` / +> `gcp-resource-inventory.json`); `{SKILL_LABEL}` = "Heroku" / "Vercel" / "GCP". + +## 1. Discovery is frozen + +- The workshop NEVER writes `{INVENTORY}`, capture directories, or any + discovery-derived analysis artifact (coupling, preflight, clusters). +- `scenarios/index.json.inventory_fingerprint` = SHA-256 hex of the raw + `{INVENTORY}` bytes (no JSON re-serialization), recorded at baseline capture. +- Every Apply & reprice MUST recompute the fingerprint first and ABORT with + "Inventory changed since baseline. Re-run Discover before workshop reprice." + on any difference. + +## 2. Checkpoint state semantics + +- The workshop is a checkpoint: it NEVER becomes `current_phase`. Entry sets + `phases.workshop: "in_progress"`; `current_phase` stays at `"estimate"` + until exit/decline. +- Exit to Generate (assembler): `phases.workshop: "completed"`, + `current_phase: "generate"`. Decline at the Estimate offer: same, without + requiring `scenarios/`. +- Warm-start rule: `current_phase == "estimate"` AND + `phases.estimate == "completed"` AND `phases.workshop == "pending"` → + present the workshop offer; NEVER recompute Estimate. +- If Generate (or later) is already `completed`, apply the Estimate re-entry + guard (confirm → reset downstream to pending) before any refresh. + +## 3. Inner runs are artifact-only + +When the refresh re-runs a backbone phase (Design/Recommend/Estimate) to +reprice: + +| Allowed | Forbidden | +| ------------------------------------- | ---------------------------------------------------------------------------------------- | +| Overwrite that phase's artifact(s) | Set the phase to `in_progress`/`completed` (they stay `completed`) | +| Soft-validate before snapshot | Emit `HANDOFF_OK` from the inner phase | +| One brief chat note when done | Touch `current_phase` or advance the backbone | +| Keep `phases.workshop: "in_progress"` | Run the post-Estimate workshop offer (recursion) | +| | Fail on `_check_single_active_phase`-style preconditions because workshop is in progress | + +## 4. Scenario store + +- Max **5** scenarios. Before evicting, WARN and NAME the victim (id + label); + never delete `baseline_scenario_id` unless the user explicitly resets. +- Working tree == active scenario: the skill's preference/design/estimate + artifacts always match `index.active_scenario_id`. +- Each manifest's `estimation_summary` carries at minimum: + `aws_monthly_premium` / `_balanced` / `_optimized`, `complexity_tier`, + `pricing_source`, `region_note` (nullable), `calculator_url` (nullable). +- `preferences_subset` records ONLY the knob paths that differ from baseline. + +## 5. Region honesty + +The sheet always shows: region repricing needs live pricing access (awspricing +MCP) for true regional rates; without it, numbers stay on the us-east-1 cache +basis and every affected estimate carries a `region_note`. Never present +cache-based numbers as regional. + +## 6. Shareable calculator link (best-effort, never blocks) + +After each scenario snapshot, if the `aws-pricing-calculator` MCP server is +available (probe `get_server_info` once; no retry on failure): + +1. Prefer one-shot `build_estimate`: name + `"{SKILL_LABEL} migration — {scenario label} ({target_region})"`, services + from the scenario's Balanced-tier estimate breakdown (the PRIMARY outcome's + set where outcomes exist), each with the scenario's target region — the + calculator computes REGIONAL prices server-side, which the cache cannot. + On a structured needs-field-discovery response, resolve via + `get_service_fields` and retry ONCE; else fall back to + `create_estimate` → `add_service` → `export_estimate`. +2. Store the URL as the manifest's `estimation_summary.calculator_url`. +3. Any failure or unmappable service → `calculator_url: null`, one chat note, + continue. Workshop numbers stay authoritative; the link is a complementary + stakeholder artifact. Unconfigured server → silent null. + +Compare tables and stakeholder reports render one link line per non-null +`calculator_url`.