From 6bcf4a5d72761d24b53337d44a4623ae851c43aa Mon Sep 17 00:00:00 2001 From: Logan Kleier Date: Tue, 14 Jul 2026 17:20:30 -0700 Subject: [PATCH 01/17] feat(migrate): add vercel-to-aws migration assessment skill Adds a new migration-to-aws skill: an honest Vercel-to-AWS assessment for Next.js apps (not a full migration plan). Built on the phase DSL (same architecture as heroku-to-aws), 5 backbone phases + 1 optional scaffold checkpoint: - PreScan -> Discover -> Clarify -> Recommend -> Report, with an optional Scaffold checkpoint - Discover computes a Coupling Score and 10 named Pre-Flight Checks unconditionally, before Recommend has run - Recommend is a fixed-precedence, three-outcome engine (OpenNext/SST, ECS Fargate, or a Vercel+AWS Hybrid) - every recommendation traces to exactly one rule, never a judgment call - Report renders a validated HTML assessment (new scripts/validate-assessment-report.py + pytest suite + reference/stub fixtures), with outcome-filtered findings, a reader-vocabulary rule, and a cost-labeling rule enforced by the validator - Scaffold (optional) emits a thin IaC skeleton matching the recommended outcome - SST/Terraform for OpenNext, Terraform-only for Fargate or the Hybrid path - and defaults compute to Graviton (ARM64) where SST/Terraform expose the option - assessment-state.json: a skill-owned resumability ledger (independent of the vendored .phase-status.json) supporting incremental, effort-for-confidence input collection across multiple sessions Registers the skill in the plugin's README, the top-level migrate README, and the Claude/Codex/Cursor plugin manifests. Verified: frontmatter validator (0 problems), pytest (39/39), dprint check and markdownlint-cli2 clean, reference fixture passes the validator (REPORT_OK), stub fixture fails with actionable errors (REPORT_FAIL). --- .claude-plugin/marketplace.json | 2 +- .kiro/specs/vercel-to-aws/design.md | 683 ++++++++++++++++++ .kiro/specs/vercel-to-aws/requirements.md | 199 +++++ .kiro/specs/vercel-to-aws/tasks.md | 298 ++++++++ migrate/README.md | 31 + .../.claude-plugin/plugin.json | 5 +- .../.codex-plugin/plugin.json | 8 +- .../.cursor-plugin/plugin.json | 5 +- migrate/plugins/migration-to-aws/README.md | 59 +- .../fixtures/assessment-report-reference.html | 82 +++ .../fixtures/assessment-report-stub.html | 36 + .../preflight-findings-reference.json | 105 +++ .../fixtures/recommendation-reference.json | 15 + .../fixtures/tier1-signals-reference.json | 19 + .../scripts/validate-assessment-report.py | 604 ++++++++++++++++ .../skills/vercel-to-aws/SKILL.md | 260 +++++++ .../knowledge/coupling-weights.json | 59 ++ .../knowledge/peripheral-mappings.json | 35 + .../knowledge/preflight-checks.json | 182 +++++ .../references/phases/clarify/clarify-ask.md | 208 ++++++ .../phases/clarify/clarify-assemble.md | 122 ++++ .../references/phases/clarify/clarify.md | 133 ++++ .../phases/discover/discover-adapter.md | 120 +++ .../phases/discover/discover-api.md | 125 ++++ .../phases/discover/discover-assemble.md | 212 ++++++ .../phases/discover/discover-configs.md | 149 ++++ .../phases/discover/discover-coupling.md | 134 ++++ .../phases/discover/discover-manifests.md | 136 ++++ .../phases/discover/discover-preflight.md | 159 ++++ .../phases/discover/discover-probe.md | 115 +++ .../references/phases/discover/discover.md | 232 ++++++ .../phases/prescan/prescan-assemble.md | 141 ++++ .../phases/prescan/prescan-collect.md | 133 ++++ .../references/phases/prescan/prescan-scan.md | 133 ++++ .../references/phases/prescan/prescan.md | 185 +++++ .../phases/recommend/recommend-assemble.md | 135 ++++ .../phases/recommend/recommend-rules.md | 155 ++++ .../references/phases/recommend/recommend.md | 137 ++++ .../phases/report/report-assemble.md | 179 +++++ .../references/phases/report/report-render.md | 303 ++++++++ .../references/phases/report/report.md | 139 ++++ .../phases/scaffold/scaffold-assemble.md | 110 +++ .../phases/scaffold/scaffold-fargate.md | 139 ++++ .../phases/scaffold/scaffold-opennext.md | 164 +++++ .../phases/scaffold/scaffold-peripherals.md | 157 ++++ .../references/phases/scaffold/scaffold.md | 169 +++++ .../references/shared/graviton.md | 101 +++ .../shared/vercel-recommendation-engine.md | 443 ++++++++++++ .../state/assessment-state.schema.json | 228 ++++++ .../references/vendored/README.md | 31 + .../references/vendored/dsl/INTERPRETER.md | 526 ++++++++++++++ .../vendored/state/phase-status.schema.json | 34 + .../tests/test_validate_assessment_report.py | 372 ++++++++++ 53 files changed, 8630 insertions(+), 16 deletions(-) create mode 100644 .kiro/specs/vercel-to-aws/design.md create mode 100644 .kiro/specs/vercel-to-aws/requirements.md create mode 100644 .kiro/specs/vercel-to-aws/tasks.md create mode 100644 migrate/plugins/migration-to-aws/fixtures/assessment-report-reference.html create mode 100644 migrate/plugins/migration-to-aws/fixtures/assessment-report-stub.html create mode 100644 migrate/plugins/migration-to-aws/fixtures/preflight-findings-reference.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/recommendation-reference.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/tier1-signals-reference.json create mode 100644 migrate/plugins/migration-to-aws/scripts/validate-assessment-report.py create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/SKILL.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/knowledge/coupling-weights.json create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/knowledge/peripheral-mappings.json create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/knowledge/preflight-checks.json create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/clarify/clarify-ask.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/clarify/clarify-assemble.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/clarify/clarify.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/discover/discover-adapter.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/discover/discover-api.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/discover/discover-assemble.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/discover/discover-configs.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/discover/discover-coupling.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/discover/discover-manifests.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/discover/discover-preflight.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/discover/discover-probe.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/discover/discover.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/prescan/prescan-assemble.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/prescan/prescan-collect.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/prescan/prescan-scan.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/prescan/prescan.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/recommend/recommend-assemble.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/recommend/recommend-rules.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/recommend/recommend.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/report/report-assemble.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/report/report-render.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/report/report.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/scaffold/scaffold-assemble.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/scaffold/scaffold-fargate.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/scaffold/scaffold-opennext.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/scaffold/scaffold-peripherals.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/phases/scaffold/scaffold.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/shared/graviton.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/shared/vercel-recommendation-engine.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/state/assessment-state.schema.json create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/vendored/README.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/vendored/dsl/INTERPRETER.md create mode 100644 migrate/plugins/migration-to-aws/skills/vercel-to-aws/references/vendored/state/phase-status.schema.json create mode 100644 migrate/plugins/migration-to-aws/tests/test_validate_assessment_report.py diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 6651cca9..3fde8fb3 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "migration-to-aws", "source": "./migrate/plugins/migration-to-aws", "version": "1.4.0", - "description": "Migrate from GCP to AWS — including your entire AI stack. Moves infrastructure (Cloud Run → Fargate, Cloud SQL → Aurora, GKE → EKS), OpenAI/Gemini workloads to Amazon Bedrock, and agentic systems (LangChain, CrewAI, AutoGen, OpenAI Agents SDK) to AWS-native frameworks. Generates runnable Terraform, migration scripts, provider adapters, and deployment artifacts. Gives honest model-by-model pricing comparisons so you know exactly when Bedrock saves money and when it doesn't. Pair with the ai-to-aws plugin to execute AI/LLM migrations automatically (rewrite, evaluate, deliver a branch)." + "description": "Migrate from GCP to AWS — including your entire AI stack — or get an honest Vercel-to-AWS assessment. Moves infrastructure (Cloud Run → Fargate, Cloud SQL → Aurora, GKE → EKS), OpenAI/Gemini workloads to Amazon Bedrock, and agentic systems (LangChain, CrewAI, AutoGen, OpenAI Agents SDK) to AWS-native frameworks. For Vercel: computes a Coupling Score and 10 Pre-Flight Checks, then recommends OpenNext/SST, ECS Fargate, or a Vercel+AWS Hybrid via a fixed, auditable decision order. Generates runnable Terraform, migration scripts, provider adapters, and deployment artifacts. Gives honest model-by-model pricing comparisons so you know exactly when Bedrock saves money and when it doesn't. Pair with the ai-to-aws plugin to execute AI/LLM migrations automatically (rewrite, evaluate, deliver a branch)." }, { "name": "aws-startup-advisor", diff --git a/.kiro/specs/vercel-to-aws/design.md b/.kiro/specs/vercel-to-aws/design.md new file mode 100644 index 00000000..e68f9ea2 --- /dev/null +++ b/.kiro/specs/vercel-to-aws/design.md @@ -0,0 +1,683 @@ +# Design Document: Vercel-to-AWS Migration Skill + +## Overview + +`vercel-to-aws` is a new skill under `migrate/plugins/migration-to-aws/skills/`, sitting alongside `gcp-to-aws` and `heroku-to-aws`. It is driven by the same plugin-shared DSL interpreter (`skills/shared/dsl/INTERPRETER.md`, vendored byte-identical into `references/vendored/dsl/`) — phase files carry YAML frontmatter (`_phase`, `_fragments`, `_assemble`, `_produces`, `_preconditions`/`_postconditions`, `_knowledge`, `_exec`) and the interpreter loop drives execution exactly as it does for the other two skills. + +This document resolves the four open design items identified during spec review: + +1. **Phase/fragment breakdown** — mapping the spec's conceptual pipeline onto concrete DSL phase files. +2. **Assessment state ledger** — a skill-owned resumability model beyond `.phase-status.json`, supporting partial recompute. +3. **Recommendation engine** — the §8 precedence-rule cascade, designed as a reusable decision-table pattern (same shape as `skills/shared/org-recommendation-engine.md`). +4. **Validator adaptation** — porting `scripts/validate-migration-report.py` to a Vercel-specific sibling script. + +Everything else (knowledge tables for peripheral mappings, scaffold conditional artifacts, checkpoint semantics for Scaffold) follows existing precedent directly and is noted only where it affects the four items above. + +## 1. Phase / Fragment Breakdown + +The spec's conceptual pipeline (`Collect Tier 1 -> Pre-Scan -> Clarify -> Full Discover -> Coupling Score -> Pre-Flight Checks -> Recommendation -> Scaffold`) collapses onto **5 backbone phases + 1 checkpoint phase**. Coupling Score and Pre-Flight Checks are not separate backbone phases — per Requirement 6.2, they must compute unconditionally *before* Recommendation exists, so they are fragments of Discover's assembler, matching how GCP's optional-section pattern (`validate-artifacts.md`) computes-then-conditionally-renders rather than gating computation on a later phase. + +``` +prescan (_init) --> discover --> clarify --> recommend --> report --> [complete] + \ + scaffold (checkpoint, opt-in) +``` + +### 1.1 `prescan` (backbone, entry phase) + +```yaml +_phase: prescan +_title: "Collect Tier 1 Inputs & Pre-Scan" +_init: true +_input: workspace +_fragments: + - _id: tier1-collect + _trigger: { _always: true } + _file: phases/prescan/prescan-collect.md + - _id: build-free-scan + _trigger: { _always: true } + _file: phases/prescan/prescan-scan.md +_assemble: + _file: phases/prescan/prescan-assemble.md +_produces: + - tier1-signals.json + - assessment-state.json +_advances_to: discover +_interactive: false +_exec: + _agent: rw +_preconditions: + - _check_single_active_phase: true + _on_failure: _halt_and_inform + - _assert: "repo access is present AND a Vercel API token is present AND at least one in-scope Vercel project is identified" + _on_failure: _unrecoverable +_postconditions: + - _check_file_exists: [tier1-signals.json, assessment-state.json] + _on_failure: _halt_and_inform + - _validate_json: [tier1-signals.json, assessment-state.json] + _on_failure: _halt_and_inform + - _assert: "tier1-signals.json has next_version, package_manager, has_middleware, has_vercel_json, and project_list populated (or explicitly null with a reason)" + _on_failure: _halt_and_inform + - _assert: "assessment-state.json validates against references/state/assessment-state.schema.json and inputs_received.tier1 reflects what prescan-collect.md actually found" + _on_failure: _halt_and_inform +_forbids_files: + - README.md + - "terraform/**" + - assessment-report.html +``` + +- `prescan-collect.md` — Requirement 1: validates the three Tier 1 preconditions (repo access + `next build` health check, read-only Vercel API token, project scope), requests the token with the least-privilege statement per Requirement 1.7, and performs `_init` state setup (creates `.migration/`, `.phase-status.json`, **and** the skill-owned `assessment-state.json` — see §2). +- `prescan-scan.md` — Requirement 2: the build-free pass (`package.json`, lockfile census, `middleware.ts` existence, `vercel.json` presence, Vercel API project enumeration). Explicitly forbidden from running `next build` — that is Discover's job. +- `prescan-assemble.md` — merges both fragments into `tier1-signals.json`, seeds `assessment-state.json.inputs_received.tier1`, and hands off. + +This mirrors `heroku-to-aws/discover.md`'s role as the `_init: true` entry phase with `_exec: rw` (file-heavy, non-interactive, exactly the profile `_exec` targets). + +### 1.2 `discover` (backbone) + +```yaml +_phase: discover +_title: "Full Discovery, Coupling Score, Pre-Flight Checks" +_requires_phase: prescan +_input: + - tier1-signals.json + - assessment-state.json +_knowledge: + - { file: knowledge/preflight-checks.json, _when: "always — defines the M1/M2/B1-B4/S1/I1/O1/U1 check table" } + - { file: knowledge/coupling-weights.json, _when: "always — defines Coupling_Score item weights and detection methods" } +_fragments: + - _id: adapter-build + _trigger: { _when: "next_version >= 16.2 AND next build runs clean" } + _file: phases/discover/discover-adapter.md + - _id: manifest-fallback + _trigger: { _when: "next_version < 16.2 OR next build does not run clean" } + _file: phases/discover/discover-manifests.md + - _id: source-configs + _trigger: { _always: true } + _file: phases/discover/discover-configs.md + - _id: vercel-api + _trigger: { _always: true } + _file: phases/discover/discover-api.md + - _id: header-probe + _trigger: { _when: "a production URL and test account were provided (Tier 2)" } + _file: phases/discover/discover-probe.md + - _id: coupling-score + _trigger: { _always: true } + _file: phases/discover/discover-coupling.md + - _id: preflight-checks + _trigger: { _always: true } + _file: phases/discover/discover-preflight.md +_assemble: + _file: phases/discover/discover-assemble.md +_produces: + - discovery.json + - coupling-score.json + - preflight-findings.json +_advances_to: clarify +_interactive: false +_exec: + _agent: rw +_re_entry_guard: + _stale_if_completed: clarify + _stale_artifact: clarify-answers.json + _on_reentry: stop_unless_confirmed + _on_confirm: reset_downstream_to_pending +_preconditions: + - _check_phase_completed: prescan + _on_failure: _halt_and_inform + - _check_single_active_phase: true + _on_failure: _halt_and_inform +_postconditions: + - _check_file_exists: [discovery.json, coupling-score.json, preflight-findings.json] + _on_failure: _halt_and_inform + - _validate_json: [discovery.json, coupling-score.json, preflight-findings.json] + _on_failure: _halt_and_inform + - _assert: "every finding in discovery.json and preflight-findings.json carries a confidence field in {LOW, MEDIUM, HIGH} and, when not HIGH, an upgrade_input field naming the specific missing input" + _on_failure: _halt_and_inform + - _assert: "preflight-findings.json contains an entry for all 10 named checks (M1, M2, B1-B4, S1, I1, O1, U1) regardless of which outcome will eventually be recommended — none are gated on a recommendation that doesn't exist yet" + _on_failure: _halt_and_inform + - _assert: "assessment-state.json findings map was updated with this phase's outputs and computed_from_inputs recorded per finding" + _on_failure: _halt_and_inform +_forbids_files: + - README.md + - "terraform/**" + - assessment-report.html +``` + +- **Signal-priority fragments** (`discover-adapter.md` / `discover-manifests.md`) are mutually exclusive alternatives gated by the same `_when` pattern `design.md` uses for its EKS branch — exactly one runs, chosen by Next.js version + build health (Requirement 4.1-4.2). +- `discover-configs.md`, `discover-api.md` always run (source configs and Vercel REST API are always-available signal classes). +- `discover-probe.md` is conditionally triggered only when Tier 2's throwaway test account was supplied — confirmation-only, per Requirement 4.1 (header probing is never primary). +- `discover-coupling.md` and `discover-preflight.md` always run and are unconditional by design (Requirement 6.2) — this is the concrete mechanism for "compute all checks, filter at render time." They are **fragments of Discover**, not a later phase, specifically so nothing about their execution depends on Recommendation's output. +- `discover-assemble.md` merges everything, and is also the point where the assembler writes back into `assessment-state.json` (see §2.3 — recompute-on-new-input hooks into this same assembler logic on a warm re-entry). + +### 1.3 `clarify` (backbone, interactive) + +```yaml +_phase: clarify +_title: "Clarify — Ask What Discovery Can't Answer" +_requires_phase: discover +_input: + - discovery.json + - tier1-signals.json +_interactive: true +_fragments: + - _id: ask + _trigger: { _always: true } + _file: phases/clarify/clarify-ask.md +_assemble: + _file: phases/clarify/clarify-assemble.md +_produces: + - clarify-answers.json +_advances_to: recommend +_re_entry_guard: + _stale_if_completed: recommend + _stale_artifact: recommendation.json + _on_reentry: stop_unless_confirmed + _on_confirm: reset_downstream_to_pending +_preconditions: + - _check_phase_completed: discover + _on_failure: _halt_and_inform +_postconditions: + - _check_file_exists: clarify-answers.json + _on_failure: _halt_and_inform + - _validate_json: clarify-answers.json + _on_failure: _halt_and_inform + - _assert: "every answer entry has prompt, answer, and design_consequence fields populated (design_consequence may state 'not yet determined — feeds recommend phase rule N' when the consequence depends on a rule that hasn't run)" + _on_failure: _halt_and_inform + - _assert: "no question was asked whose answer PreScan or Discover already determined (e.g. no middleware question when tier1-signals.json.has_middleware is false)" + _on_failure: _halt_and_inform + - _assert: "the Next.js-upgrade question, if asked, is not gated as a precondition for any other question or for phase completion" + _on_failure: _halt_and_inform +_forbids_files: + - README.md + - "terraform/**" + - assessment-report.html +``` + +Interactive phases cannot carry `_exec` (the grammar's own rule — a dispatched worker cannot converse), so this runs inline in the main window, same as `heroku-to-aws/clarify.md`. `clarify-ask.md` implements Requirement 3's fixed question set, consulting `tier1-signals.json` + `discovery.json` first to skip anything already answered (Requirement 2.3). Each answer is written with `prompt` + `design_consequence` per Requirement 3.2 — this is the exact shape `assessment-state.json.clarify_answers` needs (§2.2). + +### 1.4 `recommend` (backbone) + +```yaml +_phase: recommend +_title: "Apply Precedence Rules -> Outcome" +_requires_phase: clarify +_input: + - discovery.json + - coupling-score.json + - preflight-findings.json + - clarify-answers.json +_knowledge: + - { file: references/shared/vercel-recommendation-engine.md, _when: "always" } +_fragments: + - _id: apply-rules + _trigger: { _always: true } + _file: phases/recommend/recommend-rules.md +_assemble: + _file: phases/recommend/recommend-assemble.md +_produces: + - recommendation.json +_advances_to: report +_preconditions: + - _check_phase_completed: clarify + _on_failure: _halt_and_inform + - _check_file_exists: [discovery.json, coupling-score.json, preflight-findings.json, clarify-answers.json] + _on_failure: _unrecoverable +_postconditions: + - _check_file_exists: recommendation.json + _on_failure: _halt_and_inform + - _validate_json: recommendation.json + _on_failure: _halt_and_inform + - _assert: "recommendation.outcome is one of {A, B, C, stay}; recommendation.fired_rule names exactly one of the 4 precedence rules; recommendation.tiebreak is true only when rule 4 fired" + _on_failure: _halt_and_inform + - _assert: "if outcome is C, recommendation.separable is true; if separable is false, outcome MUST be 'stay'" + _on_failure: _halt_and_inform + - _assert: "if outcome is C, recommendation.backend_shape is one of {A-shaped, B-shaped, null} and is never used to imply a partial OpenNext/SST scaffold" + _on_failure: _halt_and_inform +_forbids_files: + - README.md + - "terraform/**" + - assessment-report.html +``` + +`recommend-rules.md` is a thin orchestrator that loads and follows `references/shared/vercel-recommendation-engine.md` (§3 below) — the actual decision table lives there, not duplicated inline, matching how `design.md` in Heroku loads `design-mapping.md` rather than inlining the mapping logic. + +### 1.5 `report` (backbone) + +```yaml +_phase: report +_title: "Write & Validate the Assessment Report" +_requires_phase: recommend +_input: + - discovery.json + - coupling-score.json + - preflight-findings.json + - clarify-answers.json + - recommendation.json + - assessment-state.json +_fragments: + - _id: render + _trigger: { _always: true } + _file: phases/report/report-render.md +_assemble: + _file: phases/report/report-assemble.md +_produces: + - assessment-report.html +_advances_to: complete +_preconditions: + - _check_phase_completed: recommend + _on_failure: _halt_and_inform +_postconditions: + - _check_file_exists: assessment-report.html + _on_failure: _halt_and_inform + - _assert: "the report-render.md validator invocation (scripts/validate-assessment-report.py) exited 0 within the 2-retry cap; the shell exit code was branched on, not stdout text pattern-matching" + _on_failure: _halt_and_inform +_forbids_files: + - README.md + - "terraform/**" +``` + +`report-assemble.md` owns the retry-cap loop (Requirement 12.3-12.4) and the exit-code branch (§4 below). This is prose-driven exactly like GCP's `generate.md` Step 4 — no new DSL primitive. + +### 1.6 `scaffold` (checkpoint) + +```yaml +_phase: scaffold +_title: "Optional IaC Scaffold" +_kind: checkpoint +_requires_phase: report +_input: + - recommendation.json +_trigger: { _when: "the founder opts in to a scaffold at the post-report checkpoint" } +_fragments: + - _id: outcome-a + _trigger: { _when: "recommendation.outcome is 'A', or 'C' with backend_shape 'A-shaped'" } + _file: phases/scaffold/scaffold-opennext.md + - _id: outcome-b + _trigger: { _when: "recommendation.outcome is 'B', or 'C' with backend_shape 'B-shaped'" } + _file: phases/scaffold/scaffold-fargate.md + - _id: peripherals + _trigger: { _always: true } + _file: phases/scaffold/scaffold-peripherals.md +_assemble: + _file: phases/scaffold/scaffold-assemble.md +_produces: + - { file: "sst.config.ts", _when: "outcome-a fragment fired" } + - { file: "terraform/", _when: "any fragment fired" } +_preconditions: + - _check_phase_completed: report + _on_failure: _halt_and_inform +_postconditions: + - _check_file_exists: "terraform/README.md" + _on_failure: _warn_and_skip +_forbids_files: + - "README.md" +``` + +This is off-backbone (`_kind: checkpoint`, no `_advances_to`), same shape as `heroku-to-aws/feedback.md`. The outcome-a/outcome-b fragments are mutually exclusive per Requirement 8.2-8.4 (never both SST and a from-scratch Next.js Terraform stack); `scaffold-peripherals.md` always runs and applies the Requirement 8.6 mapping table (Blob->S3, Cron->EventBridge Scheduler, etc.) as a `knowledge/peripheral-mappings.json` lookup, identical in spirit to `fast-path-addons.json`. + +--- + +## 2. Assessment State Ledger + +`.phase-status.json` (schema: `references/vendored/state/phase-status.schema.json`) stays untouched and skill-agnostic — it only ever tracks `pending`/`in_progress`/`completed` per phase. It cannot express "this one finding's confidence changed because a new input arrived," so `vercel-to-aws` owns a second, sibling file: **`$MIGRATION_DIR/assessment-state.json`**, schema at `skills/vercel-to-aws/references/state/assessment-state.schema.json`. + +The two files are read/written independently (Requirement 11.5) — a corrupt `assessment-state.json` never fails the `.phase-status.json` validation path in `INTERPRETER.md` § State-file validation, and vice versa. The `report` phase's `_postconditions` never inspect `assessment-state.json` structure beyond existence; the `discover`/`clarify` assemblers own its content. + +### 2.1 Schema + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "assessment-state.json", + "description": "Skill-owned resumability ledger for vercel-to-aws. Tracks inputs received, per-finding confidence, and clarify answers with their design consequence, so a re-invocation can recompute only what a new input affects. Independent of .phase-status.json.", + "type": "object", + "required": ["schema_version", "migration_id", "last_updated", "inputs_received", "findings", "clarify_answers", "report_history"], + "properties": { + "schema_version": { "const": "1.0" }, + "migration_id": { "type": "string" }, + "last_updated": { "type": "string", "format": "date-time" }, + "inputs_received": { + "type": "object", + "required": ["tier1", "tier2", "tier3"], + "properties": { + "tier1": { "$ref": "#/definitions/tierInputMap" }, + "tier2": { "$ref": "#/definitions/tierInputMap" }, + "tier3": { "$ref": "#/definitions/tierInputMap" } + } + }, + "findings": { + "type": "object", + "description": "Keyed by a stable finding_id (e.g. 'preflight.M1', 'coupling.isr', 'discovery.traffic_shape').", + "additionalProperties": { "$ref": "#/definitions/findingRecord" } + }, + "clarify_answers": { + "type": "object", + "description": "Keyed by question id (e.g. 'Q1_traffic_shape').", + "additionalProperties": { "$ref": "#/definitions/clarifyAnswerRecord" } + }, + "report_history": { + "type": "array", + "items": { "$ref": "#/definitions/reportHistoryEntry" } + } + }, + "definitions": { + "tierInputMap": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": ["received", "received_at"], + "properties": { + "received": { "type": "boolean" }, + "received_at": { "type": ["string", "null"], "format": "date-time" }, + "source": { "type": "string", "description": "e.g. 'log_drain_export.csv', 'vercel_api_token'" } + } + } + }, + "findingRecord": { + "type": "object", + "required": ["value", "confidence", "computed_at", "computed_from_inputs"], + "properties": { + "value": {}, + "confidence": { "enum": ["LOW", "MEDIUM", "HIGH"] }, + "upgrade_input": { "type": ["string", "null"] }, + "computed_at": { "type": "string", "format": "date-time" }, + "computed_from_inputs": { + "type": "array", + "items": { "type": "string" }, + "description": "Input keys (matching inputs_received leaf keys) this finding's value depends on. Drives selective recompute — see 2.3." + } + } + }, + "clarifyAnswerRecord": { + "type": "object", + "required": ["prompt", "answer", "design_consequence", "answered_at"], + "properties": { + "prompt": { "type": "string" }, + "answer": { "type": "string" }, + "design_consequence": { "type": "string" }, + "answered_at": { "type": "string", "format": "date-time" } + } + }, + "reportHistoryEntry": { + "type": "object", + "required": ["generated_at", "recommendation_snapshot"], + "properties": { + "generated_at": { "type": "string", "format": "date-time" }, + "recommendation_snapshot": { "type": "object" }, + "diff_from_previous": { + "type": ["array", "null"], + "items": { + "type": "object", + "properties": { + "finding_id": { "type": "string" }, + "previous_confidence": { "enum": ["LOW", "MEDIUM", "HIGH"] }, + "new_confidence": { "enum": ["LOW", "MEDIUM", "HIGH"] }, + "previous_value": {}, + "new_value": {} + } + } + } + } + } + } +} +``` + +### 2.2 Who writes what, and when + +| Writer | Fields touched | +|---|---| +| `prescan-assemble.md` | `inputs_received.tier1.*`, initializes empty `findings`/`clarify_answers`/`report_history` | +| `discover-assemble.md` | `inputs_received.tier2.*` / `.tier3.*` (whichever were supplied), all `findings` entries with `computed_from_inputs` populated | +| `clarify-assemble.md` | `clarify_answers.*` | +| `recommend-assemble.md` | may add a synthetic finding entry for `recommendation.fired_rule` (so the traceability appendix in Requirement 10 has one place to read it from) | +| `report-assemble.md` | appends one `report_history` entry per successful report write | + +### 2.3 Recompute-on-new-input (Requirement 11.2-11.4) + +On a warm start, `prescan-collect.md` re-reads `assessment-state.json.inputs_received` before re-scanning the workspace/API. For each tier-2/tier-3 input: + +1. If `inputs_received...received` is already `true` and the underlying source hasn't changed, **skip re-collecting it**. +2. If a previously-`false` input is now present (e.g. a log drain export now exists at the expected path), mark it `received: true` and record it in a `newly_received` list passed forward to `discover`. + +`discover-assemble.md` uses `newly_received` to decide what to recompute: it walks `assessment-state.json.findings`, and for any finding whose `computed_from_inputs` intersects `newly_received`, it re-runs only that finding's originating fragment (e.g. a new log drain re-triggers the traffic-shape portion of `discover-coupling.md`'s fragment, not the whole Discover phase). Findings whose `computed_from_inputs` does not intersect `newly_received` are left untouched — their `value`/`confidence`/`computed_at` carry over verbatim from the prior run. This is the mechanism that satisfies Requirement 11.3 without needing `_re_entry_guard`'s blunt "reset everything downstream" behavior; `_re_entry_guard` still applies at the `.phase-status.json` level (Discover as a whole is either `completed` or not), but partial recompute happens *inside* an already-`completed` Discover phase's re-entry, gated by the user confirmation `_re_entry_guard` already requires. + +`report-assemble.md` computes the diff for Requirement 11.4 by comparing the new `findings` snapshot against `report_history[-1].recommendation_snapshot` plus the finding values at that time, emitting `diff_from_previous` as a list of changed `finding_id`s. + +**Design note (Assessment-State validation is `_assert`-only):** No new closed-vocabulary check kind is needed. `_check_file_exists` / `_validate_json` cover presence/JSON-validity of `assessment-state.json`, and the "is X finding's `computed_from_inputs` intersecting the newly-received set" recompute logic is judgment prose the interpreter evaluates at runtime — the same `_assert` escape hatch every skill already uses for non-mechanically-verifiable predicates (e.g. Heroku's Property-16-style total-equals-sum checks). This keeps the resumability model additive to the DSL rather than requiring a grammar change. + +--- + +## 3. Recommendation Engine (`references/shared/vercel-recommendation-engine.md`) + +Modeled directly on `skills/shared/org-recommendation-engine.md`'s shape (signal table -> ordered decision steps -> confidence resolution -> output schema -> worked examples), but a **precedence cascade that stops at first match** (spec §8) rather than a "collect all matching reasons" scorer — Requirement 7.1 requires the engine to evaluate rules in a fixed order and halt at the first that fires, which is a different algorithm shape than the org engine's "gather every matching reason across all rules." This distinction is deliberate and should not be flattened to match the org engine's collect-all-reasons style during implementation. + +### 3.1 Signal Sources + +| Signal | Artifact | Key path | +|---|---|---| +| Preview dependence | `clarify-answers.json` | `Q4_preview_dependence.answer` | +| Separable AWS-bound surface | `discovery.json` | `peripherals[]` non-empty OR `api_routes[]` non-empty OR `backend_service_detected` | +| Lambda-hostile workload | `discovery.json` + `clarify-answers.json` | `route_analysis.long_running`, `route_analysis.websockets`, `Q3_devops_bandwidth.answer` mentioning an existing separate API service | +| Traffic shape | `clarify-answers.json` (fallback) or `discovery.json.log_drain_analysis` (authoritative) | `traffic_shape.peak_to_median_ratio`, `traffic_shape.confidence` | +| Coupling (ISR/edge) | `coupling-score.json` | `items[].weight` for `isr`, `edge_middleware`, `edge_runtime_routes` | +| Team size / debuggability preference | `clarify-answers.json` | `Q3_devops_bandwidth.answer` | + +### 3.2 Decision Steps (evaluated top-to-bottom, first match wins) + +**Step 1 — Preview dependence + separability (Requirement 7.1 rule 1).** + +| Condition | Result | +|---|---| +| `Q4_preview_dependence.answer` indicates previews are load-bearing AND a separable surface exists | `outcome: C`, `separable: true`, `backend_shape` recurses to Step 2/3 evaluated against the *backend* signals only | +| `Q4_preview_dependence.answer` indicates previews are load-bearing AND no separable surface exists | `outcome: stay`, `separable: false` | +| Previews are not load-bearing | fall through to Step 2 | + +If Step 1 resolves to `C`, re-run Steps 2-3 below using only the backend-relevant subset of signals (route analysis, DB/queue peripherals) to set `backend_shape` to `A-shaped` or `B-shaped` — never re-running them against the full Next.js app, since under Outcome C the Next.js app itself never leaves Vercel (Requirement 7.2). + +**Step 2 — Lambda-hostile workload (rule 2).** + +| Condition | Result | +|---|---| +| Websockets, long-running jobs (>15 min), sustained heavy SSR, or an existing separate API service detected | `outcome: B` (or `backend_shape: B-shaped` if reached via the Step-1 recursion) | +| None detected | fall through to Step 3 | + +**Step 3 — Traffic shape + coupling (rule 3).** + +| Condition | Result | +|---|---| +| Spiky traffic AND high ISR/edge coupling AND small team | `outcome: A` | +| Sustained traffic (peak:median < ~3:1) OR team states a debuggability preference | `outcome: B` | +| Neither condition clearly matches | fall through to Step 4 | + +**Step 4 — Tiebreak (rule 4).** + +| Condition | Result | +|---|---| +| Traffic-shape confidence is LOW (no log drain, vague Clarify answer) | `outcome: [A, B]` (both), `tiebreak: true`, `resolving_input: "14 days of log drain data"` | + +### 3.3 Output Schema (`recommendation.json`) + +```json +{ + "outcome": "A" | "B" | "C" | "stay" | ["A", "B"], + "fired_rule": 1 | 2 | 3 | 4, + "tiebreak": false, + "separable": true, + "backend_shape": "A-shaped" | "B-shaped" | null, + "confidence": "high" | "medium" | "low", + "reasons": ["..."], + "resolving_input": null +} +``` + +Constraints (enforced by `recommend`'s `_postconditions`, mirroring the org engine's invariant style): + +- `outcome` is `["A","B"]` **only if** `tiebreak == true`; otherwise it is a single string. +- `backend_shape` is non-null **only if** `outcome == "C"`. +- `separable` is present **only if** `outcome ∈ {"C", "stay"}`. +- `fired_rule == 4` **iff** `tiebreak == true`. +- EKS and Amplify are never valid `outcome` values (Requirement 7.4-7.5) — they are report-prose callouts, not recommendation-engine outputs. + +### 3.4 Fallback Behavior + +Same principle as the org engine: never block on missing signals. + +| Scenario | Behavior | +|---|---| +| `Q4_preview_dependence` unanswered | Treat as "not load-bearing," fall through to Step 2, note in `reasons` that this assumption was made by default | +| No log drain and vague traffic-shape answer | Confidence `low`, proceed to Step 4 tiebreak rather than guessing | +| `discovery.json.peripherals` unreadable | Treat separability as `false` (fail toward the more conservative "stay" outcome, never toward silently assuming a migration path exists) | + +--- + +## 4. Validator Adaptation + +`migrate/plugins/migration-to-aws/scripts/validate-migration-report.py` (currently on branch `pr-78` / `fix/pr78-review-comments-v2`, not yet on `main`) is ported to a sibling script rather than generalized in place — the two report structures (GCP's infra/AI/billing tracks vs. Vercel's outcome-filtered pre-flight findings) have different enough section sets that a shared script would need a config layer neither skill currently has appetite for. This can be revisited post-v1 if a third skill needs the same script. + +### 4.1 New script: `scripts/validate-assessment-report.py` + +Same CLI contract and exit-code semantics as the original: + +```bash +python3 "$PLUGIN_ROOT/scripts/validate-assessment-report.py" \ + "$MIGRATION_DIR/assessment-report.html" \ + --recommendation "$MIGRATION_DIR/recommendation.json" \ + --preflight-findings "$MIGRATION_DIR/preflight-findings.json" \ + --migration-dir "$MIGRATION_DIR" +``` + +| Exit code | Meaning | Action | +|---|---|---| +| `0` | pass | proceed | +| `1` | fail-with-errors | rename to `assessment-report.incomplete.html`, surface failures, retry (cap: 2 additional attempts per Requirement 12.3-12.4) | +| anything else | validator did not run | tell the user, never treat as pass | + +This is the exact table already documented in `validate-migration-report.md` — copied, not reinvented. + +### 4.2 Section IDs (replaces GCP's `REQUIRED_SECTION_IDS`) + +| Section ID | Required? | Maps to | +|---|---|---| +| `exec-verdict` | always | Requirement 9.2 verdict banner | +| `exec-tiebreak` | conditional — `recommendation.tiebreak == true` | Requirement 9.2 side-by-side section | +| `inputs-received` | conditional — any finding below HIGH | Requirement 9.3 | +| `what-you-gain` | always | Requirement 9.1 | +| `what-you-lose` | always | Requirement 9.1 | +| `coupling-score` | always | Requirement 5 | +| `preflight-findings` | always | Requirement 6.3 (filtered/reframed) | +| `appendix-m1` | conditional — `tier1-signals.json.has_middleware == true` | Requirement 9.4 | +| `decision-traceability` | always | Requirement 10.1 | +| `out-of-scope` | conditional — outcome is `C` or `stay` | Requirement 9.5 | +| `next-steps` | always, rendered as `
    ` | Requirement 9.1 | + +### 4.3 New checks beyond the ported set + +The original 16 checks (section IDs exactly once, TOC/anchor integrity, no stubs, no placeholders, readability/reader-vocabulary, fixture-bleed) port over unchanged in mechanism. Two are re-specified for Vercel's vocabulary: + +| Check | PASS when | +|---|---| +| Reader vocabulary (replaces GCP's check 14) | No pre-flight check ID (`M1`, `M2`, `B1`-`B4`, `S1`, `I1`, `O1`, `U1`), no `*.json` filename, no Terraform resource ID (`aws_*.*`), and no literal "route disposition" inside any `exec-*` section | +| Cost-labeling (new) | Every `$`-prefixed or dollar-amount string anywhere in the document body is adjacent to (within the same sentence or table cell as) the phrase "estimated monthly" — enforced even for U1's cost-driver figures per Requirement 9.6 | +| Fixture bleed (re-pointed) | Reference canary is a new constant (e.g. a fixture migration ID distinct from GCP's `0611-0606`) scoped to the Vercel fixture; same mechanism as `_validate_fixture_bleed` | + +### 4.4 Fixtures + +Per Requirement 12.7, mirror the existing pattern: + +- `fixtures/assessment-report-reference.html` — a golden reference report (built from a reference startup) that passes. +- `fixtures/assessment-report-stub.html` — an inverse fixture that deliberately fails with actionable errors (missing sections, a leaked `M1` in the exec flow, an un-labeled dollar figure). +- Both wired into the same CI regression job that already runs `tests/test_validate_migration_report.py`, as a sibling `tests/test_validate_assessment_report.py`. + +### 4.5 What is explicitly not done in v1 + +Per the spec's own out-of-scope note: no new closed-vocabulary `_check_*` kind is added to `INTERPRETER.md`. The validator remains invoked via phase prose in `report-render.md` / `report-assemble.md`, exactly as GCP's `generate.md` Step 4 invokes its validator. Promoting "run script, branch on exit code" into a canonical DSL primitive (available to both skills without prose duplication) is a reasonable v2 cleanup once a third skill needs the same pattern, not a v1 requirement. + +--- + +## File Structure + +``` +skills/vercel-to-aws/ +├── SKILL.md +├── references/ +│ ├── phases/ +│ │ ├── prescan/ +│ │ │ ├── prescan.md +│ │ │ ├── prescan-collect.md +│ │ │ ├── prescan-scan.md +│ │ │ └── prescan-assemble.md +│ │ ├── discover/ +│ │ │ ├── discover.md +│ │ │ ├── discover-adapter.md +│ │ │ ├── discover-manifests.md +│ │ │ ├── discover-configs.md +│ │ │ ├── discover-api.md +│ │ │ ├── discover-probe.md +│ │ │ ├── discover-coupling.md +│ │ │ ├── discover-preflight.md +│ │ │ └── discover-assemble.md +│ │ ├── clarify/ +│ │ │ ├── clarify.md +│ │ │ ├── clarify-ask.md +│ │ │ └── clarify-assemble.md +│ │ ├── recommend/ +│ │ │ ├── recommend.md +│ │ │ ├── recommend-rules.md +│ │ │ └── recommend-assemble.md +│ │ ├── report/ +│ │ │ ├── report.md +│ │ │ ├── report-render.md +│ │ │ └── report-assemble.md +│ │ └── scaffold/ +│ │ ├── scaffold.md +│ │ ├── scaffold-opennext.md +│ │ ├── scaffold-fargate.md +│ │ ├── scaffold-peripherals.md +│ │ └── scaffold-assemble.md +│ ├── shared/ +│ │ └── vercel-recommendation-engine.md +│ ├── state/ +│ │ └── assessment-state.schema.json +│ └── vendored/ # synced from skills/shared/, same as heroku-to-aws +│ ├── dsl/INTERPRETER.md +│ └── state/phase-status.schema.json +├── knowledge/ +│ ├── preflight-checks.json +│ ├── coupling-weights.json +│ └── peripheral-mappings.json +scripts/ +└── validate-assessment-report.py # sibling to validate-migration-report.py +fixtures/ +├── assessment-report-reference.html +└── assessment-report-stub.html +tests/ +└── test_validate_assessment_report.py +``` + +## Resolved Design Decisions + +The three items previously open at this point are resolved as follows. + +### 1. Fragment-level partial recompute granularity — resolved: internal short-circuit, not finer fragments + +A fragment (e.g. `discover-coupling.md`) keeps computing its full finding family in one file, but on a warm re-entry it internally short-circuits per finding rather than being split into one-fragment-per-finding. Concretely, each fragment's prose gets a standard preamble: + +> Before computing any finding this fragment owns, check whether `assessment-state.json.findings..computed_from_inputs` intersects the `newly_received` list passed in from `prescan-assemble.md`. If a given finding's dependency set does NOT intersect `newly_received`, copy its prior `value`/`confidence`/`computed_at` forward unchanged and skip recomputation for that finding only. If it DOES intersect, recompute normally. + +This is a per-fragment prose contract, not a new frontmatter key — `_fragments` stays exactly as declared in §1.2, one entry per signal-class fragment (`discover-adapter`, `discover-coupling`, `discover-preflight`, etc.). The alternative (one fragment per finding) was rejected: Coupling Score alone has 8+ items and Pre-Flight Checks has 10 named checks, so finding-per-fragment would produce ~20 near-empty fragment files with no independent `_trigger` value, which fails the DSL's own "fragments are independent units of work" intent without buying any real isolation. Short-circuiting inside the existing fragment boundary keeps the file count in line with `heroku-to-aws`'s discover fragments (3 files) while still satisfying Requirement 11.3's per-finding recompute granularity. + +### 2. `report_history` growth cap — resolved: cap at 5 entries, FIFO eviction + +`assessment-state.json.report_history` is capped at the 5 most recent entries. `report-assemble.md` appends the new entry then, if length exceeds 5, drops the oldest (index 0) before writing. Enforced as an `_assert` in `report`'s `_postconditions`: + +```yaml +_assert: "assessment-state.json.report_history has at most 5 entries after this write" +_on_failure: _halt_and_inform +``` + +Rationale: Requirement 11.4 only requires a diff against the *immediately prior* report, so nothing downstream reads deeper into history than index `-2`; 5 is generous headroom without letting the ledger grow unbounded across a long-lived repo. + +### 3. `scripts/validate-migration-report.py` merge status — resolved: already on `main` + +Confirmed directly against `origin/main` (commit `f6f23f2`, "Enforce comprehensive migration HTML reports with post-write validation (#78)"): `scripts/validate-migration-report.py`, `skills/gcp-to-aws/references/shared/validate-migration-report.md`, and both fixtures are live on `main` today, at the paths §4 already assumes. No branch dependency remains — `validate-assessment-report.py` can be authored as a proper sibling from day one of implementation. diff --git a/.kiro/specs/vercel-to-aws/requirements.md b/.kiro/specs/vercel-to-aws/requirements.md new file mode 100644 index 00000000..6745f940 --- /dev/null +++ b/.kiro/specs/vercel-to-aws/requirements.md @@ -0,0 +1,199 @@ +# Requirements Document + +## Introduction + +This feature adds a `vercel-to-aws` migration skill to the `migration-to-aws` plugin, alongside the existing `gcp-to-aws` and `heroku-to-aws` skills. Unlike those two sources, Vercel's proprietary infrastructure (CloudFront behaviors, Lambda tuning, edge routing) cannot be exported or read directly from an API — it is derived instead from build output, source configs, and the Vercel REST API. The skill's deliverable is an honest assessment (discovery → coupling score → pre-flight checks → three-outcome recommendation) with an optional, thin Terraform/SST scaffold. Full cost-estimation parity with the GCP skill's Estimate phase is explicitly deferred to v2 — billing signal from Vercel is structurally thinner than GCP's line-item billing export. + +The skill follows the same DSL contract every skill in this plugin follows: phase files carry YAML frontmatter (`_phase`, `_fragments`, `_assemble`, `_produces`, `_preconditions`/`_postconditions`, `_knowledge`, `_exec`) interpreted per `skills/shared/dsl/INTERPRETER.md`, vendored byte-identical into `references/vendored/dsl/` and CI-checked (`mise run shared:check`). This requirements document describes WHAT the skill must do; the DSL phase/fragment breakdown, the resumability state model, the recommendation-engine pattern, and the report validator adaptation are DESIGN decisions captured in `design.md`. + +## Glossary + +- **Discover_Phase**: Phase that reads Tier 1/2/3 inputs (repo access, Vercel API token, project scope, log drain, invoices) and produces a signal inventory, prioritized by authority (Adapter API build output > `.next` manifests > source configs > `vercel.json` > Vercel REST API > header probing) +- **PreScan**: A cheap, build-free pass over Tier 1 inputs only (`package.json`, lockfile census, `middleware.ts` existence, `vercel.json` presence, Vercel API project enumeration) that runs before Clarify so Clarify's questions can be fact-driven +- **Clarify_Phase**: Phase that asks Mom-Test-shaped questions PreScan and Full Discover cannot answer (traffic shape, migration trigger, team bandwidth, preview-dependence, Next.js upgrade willingness) +- **Coupling_Score**: Per-feature inventory of Vercel-proprietary dependence (ISR, edge middleware, edge runtime routes, image optimization, streaming SSR, preview deployments, KV/Postgres/Blob/Edge Config/Cron, Vercel-injected headers) rolled into a single score with per-item detail +- **PreFlight_Check**: A named, severity-tiered check (M1, M2, B1-B4, S1, I1, O1, U1) computed unconditionally during Discover/Coupling Score and filtered/reframed at report-render time by the recommended Outcome +- **Recommendation_Engine**: The component that evaluates the §8 precedence rules (preview-dependence -> separability -> Lambda-hostility -> traffic shape -> tiebreak) in a fixed order to select Outcome A, B, C, or "stay on Vercel" +- **Outcome_A**: OpenNext/SST full migration (serverless; SST + Terraform, an explicit documented exception to Terraform-first) +- **Outcome_B**: ECS Fargate full migration, containerized (`next start` behind ALB + CloudFront; Terraform only) +- **Outcome_C**: Hybrid — backend/peripherals migrate to AWS, Next.js hosting and PR previews stay on Vercel (Terraform only; backend compute shape recurses to A-shaped or B-shaped per rules 2-3, never emits SST) +- **Separability_Check**: The Outcome C precondition — a separable AWS-bound surface (API routes, crons, DB/storage peripherals, or a backend service) must exist, or the recommendation falls back to "stay on Vercel" +- **Confidence_Tier**: Per-finding label (LOW/MEDIUM/HIGH) keyed to which inputs were received, per the Startup Input Manifest; every sub-HIGH finding names the specific input that would upgrade it +- **Assessment_State**: The skill-owned resumability ledger (distinct from `.phase-status.json`) that persists inputs received, per-finding confidence + upgrade path, and Clarify answers with `prompt` + `design_consequence`, enabling "come back in a week" resumption without re-running completed work +- **Scaffold_Phase**: Optional checkpoint phase that emits IaC per the recommended Outcome's dialect split (Outcome A: SST + Terraform; Outcome B and C: Terraform only) +- **Assessment_Report**: The final HTML deliverable, gated by a post-write validator adapted from the GCP skill's `validate-migration-report.py` / `validate-migration-report.md` pattern + +## Requirements + +### Requirement 1: Startup Input Manifest and Tiered Collection + +**User Story:** As a founder migrating off Vercel, I want to provide inputs incrementally and see exactly what each additional input unlocks, so that I control the effort-for-confidence tradeoff instead of being asked for everything up front. + +#### Acceptance Criteria + +1. THE Discover_Phase SHALL require exactly these Tier 1 inputs before any discovery runs: repo access with a locally-runnable `next build`, a read-only team-scoped Vercel API token, and the in-scope Vercel project list +2. IF repo access is present but `next build` does not run clean locally, THEN THE Discover_Phase SHALL record this as a finding (build health) rather than treating it as a missing-input precondition failure +3. THE Discover_Phase SHALL treat each of the following as an optional Tier 2 input that upgrades specific findings when present: a 7-14 day log drain/observability export, the last 3-6 invoices or usage dashboard export, a production URL plus throwaway test account, and a one-sentence answer describing what `middleware.ts` does +4. THE Discover_Phase SHALL treat each of the following as an optional Tier 3 input: infrastructure-pointing env var hostnames (never secret values), the list of Vercel marketplace integrations and third-party webhooks, an analytics export for geo distribution, and any prior migration attempts +5. WHEN a Tier 2 or Tier 3 input is absent, THE Discover_Phase SHALL record in the corresponding finding which missing input would upgrade its Confidence_Tier and the approximate effort required to provide it +6. THE Discover_Phase SHALL NOT request or persist secret values (env var values, API keys beyond the read-only token itself) at any tier; Tier 3 env var collection is scoped to hostnames only +7. THE Discover_Phase SHALL state, when requesting the Vercel API token, that it is read-only, team-scoped, and should be revoked after the assessment completes + +### Requirement 2: Pre-Scan Before Clarify + +**User Story:** As a founder answering Clarify questions, I want the tool to already know facts it can derive cheaply, so that I am not asked questions the tool could have answered itself. + +#### Acceptance Criteria + +1. WHEN Tier 1 inputs are available, THE Discover_Phase SHALL run a PreScan pass before Clarify that reads `package.json` (Next.js version, `packageManager`, `sharp` dependency), performs a lockfile census, checks for `middleware.ts` existence, checks for `vercel.json` presence, and enumerates Vercel projects via the API +2. THE PreScan SHALL NOT run `next build` or any build-requiring step; build-dependent discovery is scoped to Full Discover +3. THE Clarify_Phase SHALL consult PreScan output to determine which questions are askable: THE Clarify_Phase SHALL NOT ask "what does your middleware do" when PreScan found no `middleware.ts`, and SHALL NOT ask project-scoping questions when PreScan found only one in-scope project +4. THE Clarify_Phase SHALL phrase the Next.js-version-dependent question using the PreScan-detected version rather than asking the founder to self-report it + +### Requirement 3: Clarify Phase Questions and Version Rule + +**User Story:** As a founder being asked migration questions, I want the tool to ask only what it genuinely cannot determine on its own, and I want upgrading my Next.js version framed as a choice, not a requirement. + +#### Acceptance Criteria + +1. THE Clarify_Phase SHALL ask, at minimum, the following questions when PreScan/Full Discover cannot answer them: traffic shape (spiky vs. sustained, rough peak:median ratio), what triggered the migration decision, team DevOps bandwidth for production ownership, how load-bearing PR preview deployments are for the team's workflow, and willingness to upgrade Next.js given the PreScan-detected current version +2. THE Clarify_Phase SHALL record each answer with a `prompt` field (the question asked) and a `design_consequence` field (what downstream decision the answer feeds), per the traceability requirement in Requirement 10 +3. THE Clarify_Phase SHALL NOT treat the Next.js-upgrade question as a gate on which migration path is offered; the default recommended path for a cost-driven founder is migrate now on OpenNext v3 regardless of current Next.js version +4. WHEN the detected Next.js version is below 16.2, THE Clarify_Phase SHALL present upgrading as a "confidence upgrade offer" (unlocks the typed Adapter API build output and positions for the future verified AWS adapter) rather than as a migration prerequisite +5. THE Clarify_Phase SHALL NOT block progression to Full Discover, Coupling Score, Pre-Flight Checks, or Recommendation on the Next.js-upgrade answer + +### Requirement 4: Discovery Signal Priority and Confidence Scoring + +**User Story:** As a founder reading the assessment, I want to know how much to trust each finding, so that I can decide whether to invest more effort before acting on a recommendation. + +#### Acceptance Criteria + +1. THE Discover_Phase SHALL prioritize discovery signals in this order when multiple are available for the same finding: Adapter API typed build output (Next.js >= 16.2) as highest authority, then `.next` build manifests (fallback for Next.js < 16.2), then source configs (`next.config.js`, `middleware.ts` + matcher), then `vercel.json`, then the Vercel REST API, then header probing as confirmation-only and never as a primary signal +2. WHEN Next.js >= 16.2 and a clean `next build` is available, THE Discover_Phase SHALL run the Adapter API build and produce a route-disposition comparison (static/ISR/dynamic/edge classification per route) as an informational finding +3. THE Discover_Phase SHALL NOT attempt a full "what Vercel provisions vs. what OpenNext provisions" infrastructure diff in v1; this is out of scope until the verified AWS adapter reaches general availability +4. THE Discover_Phase SHALL assign every finding a Confidence_Tier of LOW, MEDIUM, or HIGH, keyed to which inputs the finding rests on: a finding resting solely on header probes or coarse usage aggregates SHALL be marked LOW; a finding backed by log-drain data or invoice data SHALL be eligible for HIGH +5. WHEN a finding is below HIGH confidence, THE Discover_Phase SHALL name the specific missing input that would upgrade it +6. WHEN header probing is used for confirmation, THE Discover_Phase SHALL record known probe limitations (auth walls, bot protection, geo variance, preview-vs-prod divergence) alongside the finding + +### Requirement 5: Coupling Score + +**User Story:** As a founder deciding whether to migrate, I want a single score summarizing how deeply my app depends on Vercel-proprietary features, with enough per-feature detail to understand what drives the score. + +#### Acceptance Criteria + +1. THE Discover_Phase SHALL compute a Coupling_Score inventorying, at minimum: ISR/on-demand revalidation, edge middleware, edge runtime routes, image optimization, streaming SSR, Server Actions/version-skew exposure, preview deployments, Vercel KV/Postgres/Blob/Edge Config/Cron usage, and Vercel-injected headers (`x-vercel-ip-*` etc.) +2. THE Discover_Phase SHALL record, for each Coupling_Score item, the detection method used and a weight rationale +3. WHEN the Coupling_Score inventory identifies a single high-coupling component alongside otherwise-migratable code, THE Recommendation_Engine SHALL be able to express this as a phased migration proceeding while that component is evaluated on a specialist path in parallel, rather than defaulting to a blanket stay-on-Vercel recommendation + +### Requirement 6: Pre-Flight Checks + +**User Story:** As a founder planning a migration, I want to know which Vercel-specific behaviors will change and how severe each change is, filtered to the outcome that was actually recommended for me. + +#### Acceptance Criteria + +1. THE Discover_Phase SHALL compute all of the following named Pre-Flight_Checks unconditionally, before the Recommendation_Engine runs, each tagged with an `applies_to` outcome set and (where applicable) an `adapter_generation` tag: + - M1 (cached-route x middleware intersection), applies to A, B, C, severity HIGH when middleware does auth gating/A-B bucketing/geo-redirects/per-request rewrites on cacheable routes, LOW for header decoration/logging + - M2 (geo/IP header dependence), applies to A, B, C, severity MEDIUM + - B1 (monorepo lockfile conflicts), applies to A only, severity HIGH + - B2 (Yarn packageManager pin), applies to A only, severity MEDIUM + - B3 (sharp as a direct dependency), applies to A only, severity LOW-MEDIUM, no finding on B + - B4 (bundle contamination), applies to A only, severity LOW + - S1 (streaming routes with potentially-empty bodies), applies to A only, suppressed on B, severity MEDIUM + - I1 (ISR/on-demand revalidation completeness), applies to A and B (reframed differently per outcome), severity HIGH under the conditions specified in the design + - O1 (build environment consistency), applies to A only, advisory severity, generic hygiene note on B + - U1 (uncached high-invocation routes / cost driver flag), applies to A, B, C, informational severity +2. THE Discover_Phase SHALL NOT gate execution of any Pre-Flight_Check on the Recommendation_Engine's output, since the recommendation does not exist yet at Pre-Flight Check computation time +3. THE Assessment_Report SHALL filter and/or reframe each Pre-Flight_Check's presented wording according to the recommended Outcome (or the outcome the founder overrides to); a check not applicable to the recommended outcome SHALL NOT be surfaced in the primary findings section +4. WHEN the founder overrides the recommended outcome, THE Assessment_Report SHALL be able to surface the previously-computed-but-suppressed findings relevant to the overridden outcome without re-running discovery +5. THE Assessment_Report SHALL note, for M1 specifically, that it is generation-independent (reflects CDN-in-front-of-origin architecture, not build-output reverse-engineering) and applies regardless of which AWS outcome is chosen + +### Requirement 7: Three-Outcome Recommendation Engine + +**User Story:** As a founder deciding how to migrate, I want a recommendation that follows a fixed, explainable decision order rather than an opaque model judgment, so that I can see exactly why I got the answer I got. + +#### Acceptance Criteria + +1. THE Recommendation_Engine SHALL evaluate precedence rules in exactly this order and SHALL stop at the first rule that fires: + 1. IF preview deployments are load-bearing per the Clarify answer, THEN check separability (a separable AWS-bound surface: API routes, crons, DB/storage peripherals, or a backend service worth moving); IF separable THEN recommend Outcome_C with the backend compute shape recursing to rules 2-3; IF NOT separable THEN recommend staying on Vercel (or a thin carve-out of whatever peripheral does exist) + 2. IF a Lambda-hostile workload is present (websockets, long-running jobs, sustained heavy SSR, tasks exceeding 15 minutes, or an existing separate API service), THEN recommend Outcome_B + 3. OTHERWISE, decide between Outcome_A and Outcome_B using traffic shape and coupling: spiky traffic + high ISR/edge coupling + small team favors Outcome_A; sustained traffic (peak:median under ~3:1) or a stated preference for debuggability favors Outcome_B + 4. IF traffic-shape confidence is LOW (no log drain, vague Clarify answer), THEN present Outcome_A and Outcome_B side by side naming the specific input (14 days of log drain data) that would resolve the tie, rather than forcing a single pick +2. WHEN rule 1 recommends Outcome_C and the backend compute shape recurses to an "A-shaped" result, THE Recommendation_Engine SHALL emit that as serverless backend compute (API Gateway + Lambda) in Terraform, and SHALL NOT emit a partial OpenNext/SST scaffold for the Next.js app, since the Next.js app remains on Vercel under Outcome_C +3. WHEN a conflicted profile arises (e.g., heavy ISR + sustained traffic + load-bearing previews), THE Recommendation_Engine SHALL resolve it deterministically via the rule order (rule 1 fires first) and THE Assessment_Report SHALL state explicitly that this is a conflicted profile resolved by precedence, not a judgment call +4. THE Recommendation_Engine SHALL classify EKS as never recommended unless the team already operates Kubernetes elsewhere, and SHALL note the existence of funded Vercel-to-EKS marketplace offerings as the anti-pattern the assessment differentiates against, while separately noting that AWS migration funding programs are a legitimate line item regardless of target +5. THE Recommendation_Engine SHALL classify AWS Amplify as not a default path, and the report SHALL cite the rationale (shared CDN owned by the Amplify team, resources outside the founder's account, closed-source) as sourced from the OpenNext team's assessment and flagged for periodic re-check +6. THE Recommendation_Engine SHALL include, for every report it produces, an out-of-scope honesty paragraph: a pre-revenue founder with a single low-traffic app and no AWS credits is told a VPS or Cloudflare is a rational choice and this tooling is not targeted at them; Cloudflare migration paths themselves SHALL NOT be built + +### Requirement 8: Optional Scaffold Layer + +**User Story:** As a founder who has decided to migrate, I want runnable IaC scaffolding that matches my recommended outcome, without receiving a dialect mismatch (SST where Terraform was expected, or vice versa). + +#### Acceptance Criteria + +1. THE Scaffold_Phase SHALL be an optional checkpoint phase entered only when the founder opts in after receiving the Assessment_Report +2. WHEN the recommended/chosen outcome is Outcome_A, THE Scaffold_Phase SHALL emit the Next.js app surface via SST/OpenNext (server functions, CloudFront, ISR tag cache and revalidation queue provisioned together, image optimization) and SHALL emit all peripherals via Terraform; this SST-for-app-surface exception SHALL be documented inline as an explicit, outcome-scoped exception to the plugin's Terraform-first convention +3. WHEN the recommended/chosen outcome is Outcome_B, THE Scaffold_Phase SHALL emit Terraform only (ECS service running the `next start` container, ALB, CloudFront, ECR, task definitions, autoscaling) and SHALL NOT emit any SST or OpenNext artifacts +4. WHEN the recommended/chosen outcome is Outcome_C, THE Scaffold_Phase SHALL emit no Next.js hosting scaffold (Next.js hosting remains on Vercel) and SHALL emit Terraform only for backend compute (per the Requirement 7 recursion) and peripherals (RDS/S3/EventBridge/etc.) +5. THE Scaffold_Phase SHALL wire each applicable Pre-Flight_Check remediation into the emitted scaffold (e.g., I1's tag cache and revalidation queue provisioned together, M2's CloudFront header mappings) as thin working skeletons, not production-hardened stacks +6. THE Scaffold_Phase SHALL map Vercel peripherals to AWS targets at minimum as follows: Blob -> S3, Cron -> EventBridge Scheduler, KV -> ElastiCache (noting Upstash as a keep-alternative), Postgres -> RDS/Aurora (noting Neon as a keep-alternative), Edge Config -> Parameter Store/AppConfig, env vars -> Secrets Manager/SSM +7. THE Scaffold_Phase SHALL generate its backend-compute and peripheral logic behind an interface such that OpenNext v3 output can be replaced by a future verified Adapter-API-based AWS adapter without changing assessment/discovery/recommendation logic + +### Requirement 9: Report Structure and Reader-Vocabulary Rules + +**User Story:** As a founder reading the assessment report, I want the executive summary in plain language I can act on, with internal implementation detail (check IDs, filenames, Terraform resource names) confined to appendices. + +#### Acceptance Criteria + +1. THE Assessment_Report SHALL include, at minimum, these sections: executive summary with recommendation (A/B/C/stay) and confidence level; inputs received by tier with confidence-upgrade offers; what the founder gains; what the founder loses (preview deployments first); Coupling_Score with per-feature detail; Pre-Flight_Check findings filtered/reframed by outcome; a decision traceability appendix; an out-of-scope honesty paragraph where applicable; and an ordered Next Steps list +2. THE Assessment_Report SHALL render a verdict banner whenever a recommendation exists, and SHALL render an "Outcome A and B side by side" section whenever the Requirement 7 rule-4 tiebreak fired +3. THE Assessment_Report SHALL render the confidence-upgrade-offers section whenever any finding is below HIGH confidence +4. THE Assessment_Report SHALL render an M1 section whenever `middleware.ts` was detected during PreScan +5. THE Assessment_Report SHALL render the separability rationale whenever Outcome_C or stay-on-Vercel was the recommendation +6. THE Assessment_Report SHALL phrase every dollar figure anywhere in the report as an "estimated monthly cost/savings" figure, including figures produced by the U1 Pre-Flight_Check, even though full cost estimation is deferred to v2 +7. THE Assessment_Report SHALL NOT contain, within executive-flow sections (verdict, decision summary, gain/lose sections), any Pre-Flight_Check ID (e.g. "M1"), artifact filename, Terraform resource identifier, or the term "route disposition"; such identifiers SHALL appear only in technical appendices +8. THE Assessment_Report SHALL present the Next.js-upgrade offer in the Next Steps section as a confidence-upgrade offer, never as a migration prerequisite + +### Requirement 10: Decision Traceability + +**User Story:** As a founder or a reviewer of the assessment, I want to see exactly which precedence rule fired and which input drove which decision, so that the recommendation is auditable rather than opaque. + +#### Acceptance Criteria + +1. THE Assessment_Report SHALL always render a decision traceability appendix, regardless of which outcome was recommended +2. THE Assessment_Report SHALL derive the traceability appendix from the Assessment_State record of Clarify answers, each carrying its `prompt` and `design_consequence` fields +3. THE Assessment_Report SHALL state which Requirement 7 precedence rule fired and why, mapping at least the preview-dependence answer and the traffic-shape answer (or its absence) to their design consequences +4. WHEN the Requirement 7 rule-4 tiebreak fired, THE Assessment_Report SHALL state which rule would have applied had the missing input (log drain data) been available + +### Requirement 11: Resumable, Idempotent Assessment State + +**User Story:** As a founder who does not have log drain data yet, I want to turn on logging, come back in a week, and have the tool pick up where it left off instead of restarting the whole assessment. + +#### Acceptance Criteria + +1. THE Discover_Phase SHALL persist an Assessment_State record to the repository, separate from `.phase-status.json`, tracking at minimum: inputs received (by tier), findings with their Confidence_Tier and upgrade-input pointer, Clarify answers with `prompt` and `design_consequence`, and timestamps +2. WHEN the skill is re-invoked and Assessment_State already exists, THE Discover_Phase SHALL load previously-collected inputs and answers rather than re-collecting them +3. WHEN a new input is supplied on a re-invocation (e.g., a log drain export that did not exist previously), THE Discover_Phase SHALL recompute only the findings that input affects, and SHALL leave unaffected findings and their Confidence_Tier unchanged +4. WHEN findings are recomputed on a re-invocation, THE Assessment_Report SHALL be able to render a diff against the immediately prior report, showing which findings changed Confidence_Tier or value +5. THE Assessment_State SHALL be readable and writable independently of `.phase-status.json`, and a corrupt or missing Assessment_State SHALL NOT be treated as a corrupt or missing `.phase-status.json` (the two files fail independently) + +### Requirement 12: Report Validation Gate + +**User Story:** As a founder receiving the assessment report, I want assurance the report is structurally complete and does not contain another company's numbers, before I read it as my own. + +#### Acceptance Criteria + +1. THE Discover_Phase's downstream Report phase SHALL run a post-write validator script immediately after writing the Assessment_Report, adapted from the existing `migrate/plugins/migration-to-aws/scripts/validate-migration-report.py` pattern +2. THE Report phase SHALL branch on the validator's shell exit code, not on pattern-matching stdout text alone: exit code 0 SHALL be treated as pass, exit code 1 SHALL be treated as fail-with-errors, and any other exit code SHALL be treated as "the validator did not run" and reported to the user as such — never silently treated as a pass +3. WHEN the validator reports fail-with-errors, THE Report phase SHALL rename the incomplete report to `assessment-report.incomplete.html` (never delete unless the user asks), emit all failure lines to the user, and retry report generation up to a maximum of 2 additional attempts +4. WHEN the retry cap is reached without a passing validation, THE Report phase SHALL surface the incomplete report and its failures to the user and SHALL stop; it SHALL NOT present a stub report as complete, and the underlying assessment SHALL still be considered complete (the report is the deliverable's rendering, not the assessment itself) +5. THE validator SHALL check, at minimum: each required section ID appears exactly once, table-of-contents anchors match section IDs, appendix content is rendered findings rather than JSON stubs or bare links to JSON, the Requirement 9 conditional gates, the Requirement 9 reader-vocabulary rule, and the Requirement 9 cost-labeling rule +6. THE validator SHALL check for fixture bleed: on a real run, distinctive strings from the reference fixture (its startup name, route paths, dollar figures) SHALL NOT appear in the generated report +7. THE plugin SHALL maintain a golden reference report fixture that passes validation and an inverse stub fixture that deliberately fails with actionable errors, both wired into CI regression, mirroring the existing GCP skill's fixture pattern + +## Out of Scope (v1) + +- Full cost estimation / line-item savings parity with the GCP skill's Estimate phase (Vercel billing data is structurally too thin for this; U1's cost-driver flag is the one exception, and even it is labeled as an estimate) +- Full "what Vercel provisions vs. what OpenNext provisions" infrastructure diff (deferred until the verified Adapter-API-based AWS adapter reaches general availability) +- Cloudflare or VPS migration paths (acknowledged in the out-of-scope honesty paragraph, never built) +- Production-hardened scaffolds (v1 scaffolds are deliberately thin skeletons) +- A promoted canonical `_check_*` DSL primitive for script-exit-code branching (the validator runs via phase prose calling the script directly, matching the current GCP skill pattern; promoting this to a closed-vocabulary check kind is a possible v2 cleanup, not a v1 requirement) diff --git a/.kiro/specs/vercel-to-aws/tasks.md b/.kiro/specs/vercel-to-aws/tasks.md new file mode 100644 index 00000000..90d7c5b4 --- /dev/null +++ b/.kiro/specs/vercel-to-aws/tasks.md @@ -0,0 +1,298 @@ +# Implementation Plan: Vercel-to-AWS Migration Skill + +## Overview + +This plan implements the `vercel-to-aws` skill for the `migration-to-aws` plugin: a 5-backbone-phase + 1-checkpoint DSL skill (`prescan` -> `discover` -> `clarify` -> `recommend` -> `report`, with an optional `scaffold` checkpoint) per `design.md`. All "code" is markdown phase/fragment files carrying DSL frontmatter, JSON knowledge tables and schemas, and one new Python validator script with its pytest suite. The skill lives at `migrate/plugins/migration-to-aws/skills/vercel-to-aws/`, with one new sibling script under `migrate/plugins/migration-to-aws/scripts/`. + +This is a new skill, not a modification to `gcp-to-aws` or `heroku-to-aws` — those skills are read-only precedent, not touched by this plan, except for the plugin-level `README.md` and `.claude-plugin`/`.codex-plugin`/`.cursor-plugin` manifests that need to list the new skill. + +**Status: Implementation complete.** All tasks below are checked off. Verified via: the plugin's own `tools/frontmatter-validator` (structural DSL check — 0 problems across all 6 phases), the `test_validate_assessment_report.py` pytest suite (36/36 passing), both fixtures run end-to-end through the real validator script (reference passes, stub fails with the expected actionable errors), and byte-identity diffs on the two vendored files against canonical source. + +## Tasks + +- [x] 1. Scaffold the skill shell and vendor the shared DSL + - [x] 1.1 Create `skills/vercel-to-aws/SKILL.md` + - Frontmatter: `name`, `description` with trigger phrases ("migrate from Vercel", "Vercel to AWS", "move off Vercel", "migrate Next.js off Vercel", "assess my Vercel migration", etc.), modeled on `heroku-to-aws/SKILL.md`'s frontmatter shape + - Philosophy section: derive-don't-discover, assessment-is-the-durable-value, honest-by-construction, generation-aware (OpenNext v3 today, swappable for the verified Adapter API adapter), prose-to-gate parity — per requirements.md Introduction and design.md Overview + - Declare the entry phase (`prescan`) explicitly, per `INTERPRETER.md` § The interpreter loop step 1 (cold start loads the declared entry directly, never scans for it) + - State the "Clarify is mandatory" policy analogous to Heroku's, adapted: clarify cannot be skipped even though its questions are fewer and gated on PreScan/Discover output + - File structure tree (mirrors design.md's File Structure section) + - Context loading budget note (~800 lines per phase, same convention as Heroku) + - _Requirements: Introduction, Requirement 2, Requirement 3_ + + - [x] 1.2 Vendor the shared DSL and state schema into `references/vendored/` + - Copy `skills/shared/dsl/INTERPRETER.md` -> `skills/vercel-to-aws/references/vendored/dsl/INTERPRETER.md` (byte-identical) + - Copy `skills/shared/state/phase-status.schema.json` -> `skills/vercel-to-aws/references/vendored/state/phase-status.schema.json` (byte-identical) + - Add `skills/vercel-to-aws/references/vendored/README.md` documenting the vendored-path -> canonical-source mapping, same shape as the existing `heroku-to-aws/references/vendored/README.md` table + - Register the new vendored paths in `mise run shared:sync` / `mise run shared:check` so CI enforces byte-identity going forward + - _Requirements: Introduction (DSL contract paragraph)_ + + - [x] 1.3 Author `skills/vercel-to-aws/references/state/assessment-state.schema.json` + - Full schema per design.md §2.1 (schema_version, migration_id, last_updated, inputs_received{tier1,tier2,tier3}, findings, clarify_answers, report_history) + - This is skill-owned, NOT vendored — it has no canonical source elsewhere in the plugin + - _Requirements: 11.1, 11.5_ + +- [x] 2. Checkpoint — Skill shell review + - Ensure `SKILL.md` loads cleanly, vendored files are byte-identical to canonical source (`mise run shared:check` passes), ask the user if questions arise. + +- [x] 3. Implement the `prescan` phase (entry phase) + - [x] 3.1 Create `phases/prescan/prescan.md` (orchestrator) + - Frontmatter per design.md §1.1 verbatim (`_init: true`, `_exec: rw`, `_fragments`, `_produces: [tier1-signals.json, assessment-state.json]`, `_preconditions`/`_postconditions`, `_forbids_files`) + - Prose: Step 0 `_init` state setup (creates `.migration/`, `.phase-status.json`, AND `assessment-state.json` — the skill-owned ledger), Step 1 runs fragments, Step 2 assembles, Step 3 completion gate + - _Requirements: 1.1, 1.2, 1.6, 1.7_ + + - [x] 3.2 Create `phases/prescan/prescan-collect.md` (fragment: tier1-collect) + - Validates the three Tier 1 preconditions: repo access + `next build` health check (non-fatal finding if the build isn't clean, per Requirement 1.2), read-only team-scoped Vercel API token with the least-privilege ask statement (Requirement 1.7), in-scope project list + - Fragment frontmatter: `_fragment: tier1-collect`, `_of_phase: prescan`, `_contributes: tier1-signals.json (repo_access, next_build_health, vercel_token_present, project_list sections)` + - _Requirements: 1.1, 1.2, 1.7_ + + - [x] 3.3 Create `phases/prescan/prescan-scan.md` (fragment: build-free-scan) + - Build-free pass: `package.json` (Next.js version, `packageManager`, `sharp` dep), lockfile census, `middleware.ts` existence check, `vercel.json` presence check, Vercel API project enumeration + - Explicit prose guard: "Do NOT run `next build` or any build step in this fragment — that is Discover's job (Requirement 2.2)" + - _Requirements: 2.1, 2.2_ + + - [x] 3.4 Create `phases/prescan/prescan-assemble.md` (assembler) + - Merges both fragments into `tier1-signals.json` per the schema implied by `prescan`'s `_postconditions` `_assert` (next_version, package_manager, has_middleware, has_vercel_json, project_list) + - Seeds `assessment-state.json.inputs_received.tier1.*` and initializes empty `findings`/`clarify_answers`/`report_history` + - Runs the completion gate, emits `HANDOFF_OK | phase=prescan | artifacts=...` + - _Requirements: 1.5, 11.1_ + +- [x] 4. Checkpoint — PreScan phase integration + - Manually verify: a repo with no `middleware.ts` produces `has_middleware: false` in `tier1-signals.json`; a repo with only one Vercel project skips project-scoping metadata. Ensure all tests pass, ask the user if questions arise. + +- [x] 5. Implement the `discover` phase + - [x] 5.1 Create `phases/discover/discover.md` (orchestrator) + - Frontmatter per design.md §1.2 verbatim (7 fragments, `_knowledge` for `preflight-checks.json`/`coupling-weights.json`, `_re_entry_guard` against `clarify`) + - Prose: signal-priority explanation (Requirement 4.1), explicit statement that Coupling Score and Pre-Flight fragments run unconditionally regardless of what Recommend will later decide (Requirement 6.2) + - _Requirements: 4.1, 4.2, 4.3, 6.2_ + + - [x] 5.2 Create `phases/discover/discover-adapter.md` (fragment: adapter-build) + - Triggered only when `next_version >= 16.2 AND next build runs clean` + - Runs the Adapter API build, consumes its typed/versioned output, produces the route-disposition comparison (static/ISR/dynamic/edge per route) as an informational finding (Requirement 4.2) + - Explicit scope boundary: no full "what Vercel provisions vs. OpenNext provisions" infra diff in v1 (Requirement 4.3) + - _Requirements: 4.1, 4.2, 4.3_ + + - [x] 5.3 Create `phases/discover/discover-manifests.md` (fragment: manifest-fallback) + - Triggered when `next_version < 16.2 OR next build does not run clean` + - Reads `.next` routes manifest / prerender manifest as the fallback signal source + - _Requirements: 4.1_ + + - [x] 5.4 Create `phases/discover/discover-configs.md` (fragment: source-configs) + - Always runs: parses `next.config.js` (route segment configs, `revalidate`, `runtime: 'edge'`, image config) and `middleware.ts` + matcher scope + - Also parses `vercel.json` (headers, redirects, rewrites, function `maxDuration`/`memory`, regions, crons) per signal-priority order + - _Requirements: 4.1_ + + - [x] 5.5 Create `phases/discover/discover-api.md` (fragment: vercel-api) + - Always runs: Vercel REST API calls for projects, deployments, env var names (never values), domains, cron jobs, Edge Config, KV/Postgres/Blob store enumeration, coarse usage metrics + - Enforces Requirement 1.6: never persists secret values, Tier 3 env var collection scoped to hostnames only + - _Requirements: 1.6, 4.1_ + + - [x] 5.6 Create `phases/discover/discover-probe.md` (fragment: header-probe) + - Triggered only when Tier 2's production URL + throwaway test account were supplied + - Curl production routes, read `x-vercel-cache`/`cache-control`/`age` headers; explicitly confirmation-only, never primary (Requirement 4.1) + - Records known probe limitations alongside any finding it produces (Requirement 4.6): auth walls, bot protection, geo variance, preview-vs-prod divergence + - _Requirements: 4.1, 4.6_ + + - [x] 5.7 Create `phases/discover/discover-coupling.md` (fragment: coupling-score) + - Always runs: computes the full Coupling_Score inventory (ISR, edge middleware, edge runtime routes, image optimization, streaming SSR, Server Actions/skew, preview deployments, KV/Postgres/Blob/Edge Config/Cron, Vercel-injected headers), each with detection method + weight rationale + - Implements the design.md §"Resolved Design Decisions" item 1 short-circuit preamble: before computing each item, check `assessment-state.json.findings..computed_from_inputs` against `newly_received`; skip recompute for unaffected items on a warm re-entry + - _Requirements: 5.1, 5.2, 11.3_ + + - [x] 5.8 Create `phases/discover/discover-preflight.md` (fragment: preflight-checks) + - Always runs: computes all 10 named checks (M1, M2, B1-B4, S1, I1, O1, U1) per the table in Requirement 6.1, each carrying its `applies_to` outcome set and (where applicable) `adapter_generation` tag + - Explicit note that M1 is generation-independent (Requirement 6.5) and applies regardless of eventual outcome + - Same recompute short-circuit preamble as 5.7 + - Loads `knowledge/preflight-checks.json` (see task 9.1) for the check definition table rather than inlining it + - _Requirements: 6.1, 6.2, 6.5, 11.3_ + + - [x] 5.9 Create `phases/discover/discover-assemble.md` (assembler) + - Merges all 7 fragment contributions into `discovery.json`, `coupling-score.json`, `preflight-findings.json` + - Assigns Confidence_Tier (LOW/MEDIUM/HIGH) to every finding per Requirement 4.4-4.5, naming the specific missing `upgrade_input` when sub-HIGH + - Writes back into `assessment-state.json`: `inputs_received.tier2/tier3.*`, all `findings` entries with `computed_from_inputs` populated (per design.md §2.2) + - Runs the completion gate including the "all 10 pre-flight checks present regardless of eventual recommendation" `_assert` + - _Requirements: 4.4, 4.5, 6.1, 6.2, 11.1_ + +- [x] 6. Checkpoint — Discover phase integration + - Manually verify: a fixture repo on Next.js 15 produces `manifest-fallback` findings only (no adapter-build); a fixture on Next.js 16.2+ with a clean build produces `adapter-build` findings; `preflight-findings.json` always has exactly 10 entries regardless of fixture. Ensure all tests pass, ask the user if questions arise. + +- [x] 7. Implement the `clarify` phase + - [x] 7.1 Create `phases/clarify/clarify.md` (orchestrator) + - Frontmatter per design.md §1.3 verbatim (`_interactive: true`, no `_exec`, `_re_entry_guard` against `recommend`) + - Prose: mandatory-clarify policy (cannot be skipped even if the founder asks), consult `tier1-signals.json` + `discovery.json` first to skip already-answered questions (Requirement 2.3) + - _Requirements: 2.3, 3.1_ + + - [x] 7.2 Create `phases/clarify/clarify-ask.md` (fragment: ask) + - Implements the fixed question set (Requirement 3.1): traffic shape, migration trigger, team DevOps bandwidth, preview-dependence, Next.js-upgrade willingness + - Skip logic: no middleware question when `tier1-signals.json.has_middleware == false`; no project-scoping question when only one in-scope project (Requirement 2.3) + - Version-rule framing (Requirement 3.3-3.4): presents the Next.js-upgrade question as a "confidence upgrade offer," explicitly never as a migration gate; does not block on this answer (Requirement 3.5) + - Every answer recorded with `prompt` + `design_consequence` fields (Requirement 3.2) + - _Requirements: 2.3, 3.1, 3.2, 3.3, 3.4, 3.5_ + + - [x] 7.3 Create `phases/clarify/clarify-assemble.md` (assembler) + - Writes `clarify-answers.json`; writes `assessment-state.json.clarify_answers.*` per design.md §2.2 + - Completion gate `_assert`s per design.md §1.3: every answer has all three fields, no redundant question was asked, upgrade question was never gating + - _Requirements: 3.2, 11.1_ + +- [x] 8. Checkpoint — Clarify phase integration + - Manually verify: re-running Clarify against a repo with no middleware never surfaces the middleware question; the Next.js-upgrade answer never blocks phase completion when declined. Ensure all tests pass, ask the user if questions arise. + +- [x] 9. Author knowledge tables (pure data, referenced by Discover/Recommend/Scaffold `_knowledge`) + - [x] 9.1 Create `knowledge/preflight-checks.json` + - One entry per named check (M1, M2, B1, B2, B3, B4, S1, I1, O1, U1) with: id, title, `applies_to` outcome set, severity rule (including conditional severity, e.g. M1's HIGH-vs-LOW branch), `adapter_generation` tag where applicable, detection method, remediation list + - _Requirements: 6.1_ + + - [x] 9.2 Create `knowledge/coupling-weights.json` + - One entry per Coupling_Score item (ISR, edge middleware, edge runtime routes, image optimization, streaming SSR, Server Actions/skew, preview deployments, KV/Postgres/Blob/Edge Config/Cron, Vercel-injected headers) with: detection method, weight rationale + - _Requirements: 5.1, 5.2_ + + - [x] 9.3 Create `knowledge/peripheral-mappings.json` + - Vercel peripheral -> AWS target table per Requirement 8.6: Blob->S3, Cron->EventBridge Scheduler, KV->ElastiCache (Upstash keep-alt), Postgres->RDS/Aurora (Neon keep-alt), Edge Config->Parameter Store/AppConfig, env vars->Secrets Manager/SSM + - _Requirements: 8.6_ + +- [x] 10. Implement the Recommendation Engine + - [x] 10.1 Create `references/shared/vercel-recommendation-engine.md` + - Full decision-table document per design.md §3: signal sources table, 4 ordered decision steps (first-match-wins, explicitly NOT a collect-all-reasons scorer — call this distinction out in the doc itself so implementers don't flatten it to match the org engine's style), the Step-1-recursion rule for Outcome C's `backend_shape`, output schema (`recommendation.json` shape), constraints, fallback-behavior table + - Explicitly encode Requirement 7.4 (EKS never recommended unless team runs K8s elsewhere) and 7.5 (Amplify not a default path, cited rationale) as report-prose callouts, NOT as engine outputs — the engine's `outcome` enum never contains `EKS` or `Amplify` + - _Requirements: 7.1, 7.2, 7.3, 7.4, 7.5_ + + - [x] 10.2 Create `phases/recommend/recommend.md` (orchestrator) + - Frontmatter per design.md §1.4 verbatim (`_knowledge` loads `vercel-recommendation-engine.md`, single fragment) + - _Requirements: 7.1_ + + - [x] 10.3 Create `phases/recommend/recommend-rules.md` (fragment: apply-rules) + - Thin orchestrator prose: load and follow `vercel-recommendation-engine.md`'s decision steps against `discovery.json`/`coupling-score.json`/`preflight-findings.json`/`clarify-answers.json`; do not duplicate the decision table inline + - _Requirements: 7.1, 7.2, 7.3_ + + - [x] 10.4 Create `phases/recommend/recommend-assemble.md` (assembler) + - Writes `recommendation.json` per the §3.3 output schema; adds the synthetic `fired_rule` finding entry to `assessment-state.json.findings` for the traceability appendix (design.md §2.2) + - Completion gate `_assert`s per design.md §1.4: outcome enum validity, `separable`/`backend_shape` conditional presence rules, tiebreak-iff-rule-4 invariant + - _Requirements: 7.1, 7.2, 7.3, 10.2_ + +- [x] 11. Checkpoint — Recommendation Engine integration + - Manually verify against the worked-example table (build 4-6 signal-combination fixtures mirroring design.md §3's decision steps): load-bearing-previews + separable -> C; load-bearing-previews + not separable -> stay; websockets present -> B; spiky+high-coupling+small-team -> A; vague traffic answer + no log drain -> tiebreak [A,B]. Ensure all tests pass, ask the user if questions arise. + +- [x] 12. Implement the `report` phase and its validator + - [x] 12.1 Create `scripts/validate-assessment-report.py` + - Fork of `scripts/validate-migration-report.py` (confirmed present on `main`, commit `f6f23f2`): same CLI contract shape, same exit-code semantics (0/1/anything-else) + - Re-pointed `REQUIRED_SECTION_IDS` per design.md §4.2 (`exec-verdict`, `exec-tiebreak` conditional, `inputs-received` conditional, `what-you-gain`, `what-you-lose`, `coupling-score`, `preflight-findings`, `appendix-m1` conditional, `decision-traceability`, `out-of-scope` conditional, `next-steps`) + - Reader-vocabulary check re-specified for Vercel's identifier set (check IDs M1/M2/B1-B4/S1/I1/O1/U1, `*.json` filenames, `aws_*.*` Terraform IDs, literal "route disposition") — Requirement 9.7 + - New cost-labeling check: every dollar figure must be adjacent to "estimated monthly" — Requirement 9.6 + - Fixture-bleed check with a new canary ID scoped to the Vercel reference fixture, same mechanism as the ported `_validate_fixture_bleed` + - _Requirements: 9.6, 9.7, 12.1, 12.2, 12.5, 12.6_ + + - [x] 12.2 Create `tests/test_validate_assessment_report.py` + - Mirror `tests/test_validate_migration_report.py`'s structure and coverage (36 tests written); cover each of the 4.3-listed checks plus the ported 16, exit-code branching for all three cases (0/1/other), fixture-bleed both with and without `--migration-dir` + - _Requirements: 12.2, 12.5, 12.6_ + + - [x] 12.3 Create `fixtures/assessment-report-reference.html` and `fixtures/assessment-report-stub.html` + - Reference: a golden report built from a reference startup profile that passes every check + - Stub: deliberately fails multiple checks (missing section, a leaked `M1` in an `exec-*` section, an un-labeled dollar figure) with actionable error text + - _Requirements: 12.7_ + + - [x] 12.4 Create `phases/report/report.md` (orchestrator) + - Frontmatter per design.md §1.5 verbatim + - _Requirements: 12.1_ + + - [x] 12.5 Create `phases/report/report-render.md` (fragment: render) + - Renders `assessment-report.html` from all upstream artifacts, applying the outcome-based filter/reframe rule (Requirement 6.3) so a check not applicable to the recommended outcome is not surfaced in the primary findings section, while remaining available for an override (Requirement 6.4) + - Applies the reader-vocabulary rule at render time (Requirement 9.7) and the cost-labeling rule (Requirement 9.6) as authoring discipline, backed by the validator as the enforcement mechanism + - _Requirements: 6.3, 6.4, 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7, 9.8, 10.1, 10.3, 10.4_ + + - [x] 12.6 Create `phases/report/report-assemble.md` (assembler) + - Owns the retry-cap loop (Requirement 12.3-12.4): invoke `validate-assessment-report.py`, branch on exit code per the table in design.md §4.1, rename to `assessment-report.incomplete.html` on fail-with-errors, retry up to 2 additional times, stop and surface on cap exhaustion without presenting a stub as complete + - Appends the `report_history` entry to `assessment-state.json` with the 5-entry FIFO cap (design.md "Resolved Design Decisions" item 2) and computes `diff_from_previous` (Requirement 11.4) + - _Requirements: 11.4, 12.2, 12.3, 12.4_ + +- [x] 13. Checkpoint — Report phase and validator integration + - Run `tests/test_validate_assessment_report.py`, confirm the stub fixture fails with actionable errors and the reference fixture passes cleanly. Ensure all tests pass, ask the user if questions arise. + +- [x] 14. Implement the `scaffold` checkpoint phase + - [x] 14.1 Create `phases/scaffold/scaffold.md` (orchestrator) + - Frontmatter per design.md §1.6 verbatim (`_kind: checkpoint`, `_trigger` on founder opt-in, no `_advances_to`) + - _Requirements: 8.1_ + + - [x] 14.2 Create `phases/scaffold/scaffold-opennext.md` (fragment: outcome-a) + - Triggered when `recommendation.outcome == 'A'` or `'C'` with `backend_shape == 'A-shaped'` + - Emits the Next.js app surface via SST/OpenNext (server functions, CloudFront, ISR tag cache + revalidation queue provisioned TOGETHER, image optimization) per Requirement 8.2; when reached via the C-recursion, emits ONLY the backend serverless compute (API Gateway + Lambda) in Terraform, never a partial OpenNext/SST scaffold (Requirement 7.2) + - Documents the SST-for-app-surface exception inline as an explicit, outcome-scoped exception to Terraform-first (Requirement 8.2) + - _Requirements: 7.2, 8.2_ + + - [x] 14.3 Create `phases/scaffold/scaffold-fargate.md` (fragment: outcome-b) + - Triggered when `recommendation.outcome == 'B'` or `'C'` with `backend_shape == 'B-shaped'` + - Emits Terraform only (ECS service running `next start` container, ALB, CloudFront, ECR, task defs, autoscaling); never emits SST/OpenNext artifacts (Requirement 8.3) + - _Requirements: 8.3_ + + - [x] 14.4 Create `phases/scaffold/scaffold-peripherals.md` (fragment: peripherals) + - Always runs: applies `knowledge/peripheral-mappings.json` (task 9.3) to whatever peripherals Discover found + - Wires each applicable Pre-Flight_Check remediation into the scaffold as a thin skeleton, not production-hardened (Requirement 8.5) — e.g. I1's tag cache + queue together, M2's CloudFront header mappings + - Structures backend-compute/peripheral logic behind an interface so OpenNext v3 can later be swapped for the verified Adapter-API adapter without touching assessment/discovery/recommendation logic (Requirement 8.7) + - _Requirements: 8.5, 8.6, 8.7_ + + - [x] 14.5 Create `phases/scaffold/scaffold-assemble.md` (assembler) + - Combines fragment outputs; for Outcome C, emits NO Next.js hosting scaffold at all (Requirement 8.4) — only backend compute + peripherals + - Completion gate: `terraform/README.md` exists (warn-and-skip on failure, since scaffold is optional) + - _Requirements: 8.4_ + + - [x] 14.6 Create `references/shared/graviton.md` and default Scaffold compute to ARM64 + - Ported (scoped down, no Clarify question — this skill's compute is homogeneous Node.js, unlike gcp-to-aws's polyglot Q11b case) from `gcp-to-aws`'s Graviton feature + - Wired into `scaffold-opennext.md` (`sst.aws.Nextjs`'s `server.architecture`, and the Outcome-C A-shaped backend's `aws_lambda_function.architectures`), `scaffold-fargate.md` (`aws_ecs_task_definition.runtime_platform.cpu_architecture` in both full app-surface and backend-only mode), and `scaffold-peripherals.md` (the Cron peripheral's EventBridge-invoked `aws_lambda_function.architectures`) + - Confirmed `sharp` (this skill's one detected native dependency, Pre-Flight Check B3) ships prebuilt ARM64 Linux binaries and is not a Graviton blocker + - _Requirements: 8.5_ + +- [x] 15. Checkpoint — Scaffold phase integration + - Manually verify: an Outcome-A recommendation produces both `sst.config.ts` and `terraform/`; an Outcome-B recommendation produces `terraform/` only, zero SST files anywhere; an Outcome-C recommendation with `backend_shape: A-shaped` produces Terraform-only Lambda/API-Gateway resources, never `sst.config.ts`. Ensure all tests pass, ask the user if questions arise. + +- [x] 16. Plugin-level integration + - [x] 16.1 Update `migrate/README.md` + - Add `vercel-to-aws` to the supported migration sources table and the "What This Does" / "How to Use" sections, mirroring how `heroku-to-aws` is listed today + - _Requirements: Introduction_ + + - [x] 16.2 Update `migrate/plugins/migration-to-aws/README.md` and the plugin manifests + - Add the skill's trigger phrases to the plugin-level skill index + - Update `.claude-plugin/marketplace.json`, `.codex-plugin`, `.cursor-plugin` manifests to list `vercel-to-aws` alongside `gcp-to-aws`/`heroku-to-aws` wherever they enumerate skills + - _Requirements: Introduction_ + + - [x] 16.3 Register the new script/tests in CI wiring + - Add `scripts/validate-assessment-report.py` and `tests/test_validate_assessment_report.py` to whatever CI job runs `tests/test_validate_migration_report.py` (bandit scan scoping, pytest collection, etc.) — mirror the existing exclusion pattern for pytest-assert findings in the tests directory + - _Requirements: 12.7_ + +- [x] 17. Final checkpoint — Full integration + - Ran the plugin's `tools/frontmatter-validator` against the skill (structural DSL check independent of hand-verification) — found and fixed 2 real issues: `_knowledge` misplaced on a fragment instead of its phase (`scaffold-peripherals.md`/`scaffold.md`), and a `_postconditions` `_check_file_exists` gating on `terraform/README.md` without it being declared in `_produces` (`scaffold.md`/`scaffold-assemble.md`). Re-ran after fixes: 0 problems, matching `heroku-to-aws`'s clean result on the same tool. Re-ran the full pytest suite (36/36 passing) and both fixtures through the real validator script (reference -> `REPORT_OK` exit 0; stub -> `REPORT_FAIL` exit 1 with all 9 expected errors) after the fixes to confirm no regression. Re-diffed both vendored files against canonical source (still byte-identical). Ran the frontmatter-validator's own test suite (57/57 passing) as a sanity check on the tool itself. + - **Live end-to-end dry-run: done.** Ran a full PreScan->Discover->Clarify->Recommend->Report pass against a real, buildable Next.js 15.3.0 fixture repo (actual `next build`, real manifests, real Vercel-API simulation, simulated founder Clarify answers), plus a simulated warm re-entry with a synthetic log-drain input verifying the `computed_from_inputs` selective-recompute mechanism and the `_re_entry_guard` stale-downstream reset both behave as documented. The dry-run surfaced 5 real gaps/bugs in the phase files (route-disposition coverage for API Route Handlers under the manifest-fallback path, single-enum middleware classification unable to represent mixed-behavior middleware, a confidence-rubric gap for deterministic source-code facts, a lowercase/uppercase confidence-vocabulary mismatch between `recommendation.json` and `assessment-state.json`, and the `_re_entry_guard`'s all-or-nothing downstream reset as an inherited architectural note) — all were fixed in the actual skill files except the last, which is shared vendored-DSL behavior, not a vercel-to-aws-specific defect. Two follow-up rounds of external (Cursor) review on the recommendation engine's edge cases and cross-file terminology surfaced further real issues (a `fired_rule`/`tiebreak` field contradiction for Outcome C's backend recursion, a Step 3 "no legal outcome" hole, an undefined traffic-shape boundary, and several stale terminology references) — all fixed and re-verified against `dprint`, `markdownlint-cli2`, the frontmatter validator, and the pytest suite. + +## Notes + +- Tasks are NOT marked optional in this plan (unlike `org-scp-support/tasks.md`'s `*` convention) — the validator and its fixtures are core to Requirement 12, not a stretch goal, per the spec's own effort estimate framing them as "days, not a week." +- Each task references specific requirements for traceability back to `requirements.md`. +- This is a prompt-based AI agent skill plugin — "implementation" means creating/modifying markdown reference files, JSON knowledge/schema files, and one Python script + its test suite. +- New top-level paths: `skills/vercel-to-aws/` (entire tree per design.md's File Structure section), `scripts/validate-assessment-report.py`, `fixtures/assessment-report-{reference,stub}.html`, `tests/test_validate_assessment_report.py`. +- Modified files: `migrate/README.md`, `migrate/plugins/migration-to-aws/README.md`, plugin manifests (`.claude-plugin/marketplace.json`, `.codex-plugin`, `.cursor-plugin`), `mise.toml` and `.github/workflows/security-scanners.yml` (bandit exclusion for `migrate/plugins/migration-to-aws/tests/`, backfilled to match `origin/main`). +- `gcp-to-aws` and `heroku-to-aws` are NOT modified by this plan. +- Out-of-scope items from `requirements.md` (full cost estimation parity, full Adapter-API infra diff, Cloudflare/VPS paths, production-hardened scaffolds, a promoted canonical `_check_*` DSL kind) are intentionally absent from this task list — do not add tasks for them without a spec update first. + +## Task Dependency Graph + +```json +{ + "waves": [ + { "id": 0, "tasks": ["1.1", "1.2", "1.3"] }, + { "id": 1, "tasks": ["3.1", "3.2", "3.3", "9.1", "9.2", "9.3"] }, + { "id": 2, "tasks": ["3.4"] }, + { "id": 3, "tasks": ["5.1", "5.2", "5.3", "5.4", "5.5", "5.6", "5.7", "5.8"] }, + { "id": 4, "tasks": ["5.9"] }, + { "id": 5, "tasks": ["7.1", "7.2"] }, + { "id": 6, "tasks": ["7.3"] }, + { "id": 7, "tasks": ["10.1"] }, + { "id": 8, "tasks": ["10.2", "10.3"] }, + { "id": 9, "tasks": ["10.4"] }, + { "id": 10, "tasks": ["12.1"] }, + { "id": 11, "tasks": ["12.2", "12.3", "12.4", "12.5"] }, + { "id": 12, "tasks": ["12.6"] }, + { "id": 13, "tasks": ["14.1", "14.2", "14.3", "14.4"] }, + { "id": 14, "tasks": ["14.5"] }, + { "id": 15, "tasks": ["16.1", "16.2", "16.3"] } + ] +} +``` + +Checkpoints (2, 4, 6, 8, 11, 13, 15, 17) are sequencing gates, not independently schedulable tasks — each runs after its preceding wave completes and before the next wave begins. diff --git a/migrate/README.md b/migrate/README.md index 22742119..919a2825 100644 --- a/migrate/README.md +++ b/migrate/README.md @@ -10,6 +10,7 @@ Point this plugin at your Terraform files, application code, or billing data. It - **GCP → AWS** — Cloud Run, Cloud SQL, GKE, Cloud Functions, Pub/Sub, Cloud Storage, VPC, and AI/agentic workloads - **Heroku → AWS** — Dynos, Postgres, Redis, Kafka, Private Spaces, Pipelines, and 13+ common add-ons +- **Vercel → AWS** — an honest assessment (not a full migration plan) for Next.js apps: discovery, a Coupling Score, Pre-Flight Checks, and a three-outcome recommendation (OpenNext/SST, ECS Fargate, or a Vercel+AWS Hybrid), with an optional thin scaffold **For infrastructure migrations:** @@ -26,6 +27,14 @@ Point this plugin at your Terraform files, application code, or billing data. It - **Gives honest pricing comparisons** — finds the best Bedrock option for your workload with current pricing data, including side-by-side estimated monthly cost comparisons against your existing OpenAI/Gemini spend - **Generates runnable AI artifacts** — `harness.json`, provider adapters, deployment scripts, incremental migration scripts — tailored to your specific models, tools, and architecture +**For Vercel assessments:** + +- **Derives what it can't export** — Vercel's infrastructure (CloudFront-equivalent behaviors, function tuning, edge routing) isn't directly readable, so discovery works from your build output, source configs (`next.config.js`, `middleware.ts`, `vercel.json`), and the Vercel API instead +- **Computes a Coupling Score** — ISR, edge middleware, edge runtime routes, image optimization, streaming SSR, preview deployments, and Vercel-managed stores (KV/Postgres/Blob/Edge Config/Cron), each with a detection method and why it matters +- **Runs 10 named Pre-Flight Checks** — including a flagship check for cached routes that intersect with middleware (a behavior change on every AWS target, not just one), computed unconditionally and filtered to whatever outcome fits you +- **Recommends one of three honest outcomes** — OpenNext/SST (serverless), ECS Fargate (containerized), or a Vercel+AWS Hybrid (your backend moves, your Next.js app and PR previews stay on Vercel) — via a fixed, auditable decision order, never a guess +- **Tells you what you'd lose** — PR preview deployments first, always — and says plainly when this tooling isn't a fit for you (a low-traffic app with no AWS credits is often better served by a VPS) + ## Plugins | Plugin | Description | Status | @@ -90,6 +99,14 @@ After installation, just describe what you want to migrate: - "Estimate AWS costs for my Heroku workload" - "Migrate my Heroku Private Space to AWS" +**Vercel assessments:** + +- "Migrate my Next.js app off Vercel" +- "Assess my Vercel migration" +- "Should I migrate off Vercel" +- "Vercel to Fargate" +- "Vercel coupling score" + The skill creates a `.migration//` directory in the current working directory with all artifacts. ## What It Detects @@ -124,6 +141,18 @@ The skill creates a `.migration//` directory in the current working dir | Secrets | Config vars → AWS Secrets Manager or SSM Parameter Store | | Load Balancing | Web dynos → ALB; non-web → no ALB | +### Vercel → AWS + +| Category | Vercel → AWS | +| ------------------- | ----------------------------------------------------------------------------------------------------------------- | +| Compute (Outcome A) | Next.js app → OpenNext/SST (server functions, CloudFront, ISR tag cache + revalidation queue, image optimization) | +| Compute (Outcome B) | Next.js app → ECS Fargate (`next start` behind ALB + CloudFront) | +| Compute (Outcome C) | Backend/API routes → API Gateway + Lambda or Fargate (Terraform); Next.js app + PR previews stay on Vercel | +| Storage | Blob → S3, Postgres → RDS/Aurora (Neon often correct to keep), KV → ElastiCache (Upstash often correct to keep) | +| Config/Secrets | Edge Config → Parameter Store/AppConfig, env vars → Secrets Manager/SSM | +| Scheduling | Cron → EventBridge Scheduler | +| Detect-only | Preview deployments (no AWS equivalent — this drives the Hybrid outcome and is the top "what you lose" item) | + ## What You Get That a Base LLM Can't **Infrastructure:** @@ -154,6 +183,7 @@ The skill creates a `.migration//` directory in the current working dir | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **gcp-to-aws** | "migrate GCP to AWS", "move from GCP", "GCP migration plan", "migrate Cloud SQL to RDS or Aurora", "move Cloud Run to Fargate", "estimate AWS costs for my GCP infrastructure", "migrate my OpenAI app to Bedrock", "migrate my LangChain agents to AWS" | | **heroku-to-aws** | "migrate from Heroku", "Heroku to AWS", "move off Heroku", "migrate Heroku Postgres to RDS", "migrate dynos to Fargate", "migrate Heroku Private Space", "leave Heroku", "estimate AWS costs for my Heroku app" | +| **vercel-to-aws** | "migrate from Vercel", "Vercel to AWS", "move off Vercel", "migrate Next.js off Vercel", "assess my Vercel migration", "leave Vercel", "Vercel to Fargate", "Vercel to OpenNext", "should I migrate off Vercel" | ## MCP Servers @@ -169,6 +199,7 @@ The skill creates a `.migration//` directory in the current working dir - At least one input source: Terraform files, application code, or billing data - **For GCP AI/agentic migration:** Application source code is required (billing/IaC alone cannot detect agent architecture) - **For Heroku migration:** Terraform files with `heroku_*` resources are required (Procfile/app.json supplements but cannot stand alone) +- **For Vercel assessment:** repo access with a locally-runnable `next build`, plus a read-only, team-scoped Vercel API token, are both required — the assessment does not run on partial Tier 1 inputs ## Structure diff --git a/migrate/plugins/migration-to-aws/.claude-plugin/plugin.json b/migrate/plugins/migration-to-aws/.claude-plugin/plugin.json index 5b1aa83a..7175f5b3 100644 --- a/migrate/plugins/migration-to-aws/.claude-plugin/plugin.json +++ b/migrate/plugins/migration-to-aws/.claude-plugin/plugin.json @@ -36,6 +36,9 @@ "llm-migration", "cost-estimation", "cost-comparison", - "pricing-comparison" + "pricing-comparison", + "vercel", + "nextjs", + "opennext" ] } diff --git a/migrate/plugins/migration-to-aws/.codex-plugin/plugin.json b/migrate/plugins/migration-to-aws/.codex-plugin/plugin.json index c9030b68..0a7eb814 100644 --- a/migrate/plugins/migration-to-aws/.codex-plugin/plugin.json +++ b/migrate/plugins/migration-to-aws/.codex-plugin/plugin.json @@ -39,7 +39,10 @@ "llm-migration", "cost-estimation", "cost-comparison", - "pricing-comparison" + "pricing-comparison", + "vercel", + "nextjs", + "opennext" ], "skills": "./skills/", "mcpServers": "./.mcp.json", @@ -56,7 +59,8 @@ "Migrate my OpenAI app to Amazon Bedrock", "Estimate AWS costs for my GCP workload", "Generate Terraform for my GCP to AWS migration", - "Migrate my LangChain app from OpenAI to Bedrock" + "Migrate my LangChain app from OpenAI to Bedrock", + "Assess my Vercel migration" ] } } diff --git a/migrate/plugins/migration-to-aws/.cursor-plugin/plugin.json b/migrate/plugins/migration-to-aws/.cursor-plugin/plugin.json index 349d3907..38be94c1 100644 --- a/migrate/plugins/migration-to-aws/.cursor-plugin/plugin.json +++ b/migrate/plugins/migration-to-aws/.cursor-plugin/plugin.json @@ -37,6 +37,9 @@ "llm-migration", "cost-estimation", "cost-comparison", - "pricing-comparison" + "pricing-comparison", + "vercel", + "nextjs", + "opennext" ] } diff --git a/migrate/plugins/migration-to-aws/README.md b/migrate/plugins/migration-to-aws/README.md index 5cbef0cc..7cc3ee73 100644 --- a/migrate/plugins/migration-to-aws/README.md +++ b/migrate/plugins/migration-to-aws/README.md @@ -10,6 +10,7 @@ Point this plugin at your Terraform files, application code, or billing data. It - **GCP → AWS** — Cloud Run, Cloud SQL, GKE, Cloud Functions, Pub/Sub, Cloud Storage, VPC, and AI/agentic workloads - **Heroku → AWS** — Dynos, Postgres, Redis, Kafka, Private Spaces, Pipelines, and 13+ common add-ons +- **Vercel → AWS** — an honest assessment (discovery, Coupling Score, Pre-Flight Checks, a three-outcome recommendation) for Next.js apps, with an optional thin scaffold **For infrastructure migrations:** @@ -53,10 +54,10 @@ Point this plugin at your Terraform files, application code, or billing data. It ## Plugins -| Plugin | Description | Status | -| -------------------- | ------------------------------------------------------------------------------------------------------------------------ | --------- | -| **migration-to-aws** | Assess & plan: resource discovery, architecture mapping, cost analysis, execution planning (GCP and Heroku) | Available | -| **ai-to-aws** | Execute: rewrite LLM SDK calls to Bedrock, evaluate quality, deliver a ready-to-merge branch (requires migration-to-aws) | Available | +| Plugin | Description | Status | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| **migration-to-aws** | Assess & plan: resource discovery, architecture mapping, cost analysis, execution planning (GCP, Heroku) plus honest Vercel assessment (discovery, coupling, recommendation) | Available | +| **ai-to-aws** | Execute: rewrite LLM SDK calls to Bedrock, evaluate quality, deliver a ready-to-merge branch (requires migration-to-aws) | Available | ## Installation @@ -165,12 +166,22 @@ Pass `--estimation-infra` / `--estimation-ai` only when those files exist. Resol | CI/CD | Pipelines and Review Apps → detect-only (recorded in inventory, no automated migration) | | Secrets | Config vars → AWS Secrets Manager or SSM Parameter Store | +#### Vercel → AWS (assessment only, not a full migration plan) + +| Category | Examples | +| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| Compute | OpenNext/SST (Outcome A), ECS Fargate (Outcome B), or a Vercel+AWS Hybrid where only the backend moves (Outcome C) | +| Coupling | ISR, edge middleware, edge runtime routes, image optimization, streaming SSR, Server Actions/skew, preview deployments, Vercel-managed stores | +| Pre-Flight | 10 named checks (M1/M2/B1-B4/S1/I1/O1/U1), computed unconditionally and filtered by the recommended outcome | +| Peripherals | Blob → S3, Cron → EventBridge Scheduler, KV → ElastiCache, Postgres → RDS/Aurora, Edge Config → Parameter Store/AppConfig | + ### Agent Skill Triggers | Agent Skill | Triggers | | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **gcp-to-aws** | "migrate GCP to AWS", "move from GCP", "GCP migration plan", "migrate Cloud SQL to RDS or Aurora", "move Cloud Run to Fargate", "estimate AWS costs for my GCP infrastructure", "migrate my OpenAI app to Bedrock", "migrate my LangChain agents to AWS" | | **heroku-to-aws** | "migrate from Heroku", "Heroku to AWS", "move off Heroku", "migrate Heroku Postgres to RDS", "migrate dynos to Fargate", "migrate Heroku Private Space", "leave Heroku", "estimate AWS costs for my Heroku app" | +| **vercel-to-aws** | "migrate from Vercel", "Vercel to AWS", "move off Vercel", "migrate Next.js off Vercel", "assess my Vercel migration", "leave Vercel", "Vercel to Fargate", "Vercel to OpenNext", "should I migrate off Vercel" | ### MCP Servers @@ -192,18 +203,24 @@ See the [ai-to-aws README](../ai-to-aws/README.md) for full details on prerequis - At least one input source: Terraform files, application code, or billing data - **For GCP AI/agentic migration:** Application source code is required (billing/IaC alone cannot detect agent architecture) - **For Heroku migration:** Terraform files with `heroku_*` resources are required (Procfile/app.json supplements but cannot stand alone) +- **For Vercel assessment:** repo access with a locally-runnable `next build`, plus a read-only, team-scoped Vercel API token, are both required Tier 1 inputs — the assessment does not run without them - **For AI execution (ai-to-aws):** Python 3.10+, `uv`, and Bedrock model access enabled - **`uvx` required for cost estimation:** The `awspricing` MCP server runs via [`uvx`](https://docs.astral.sh/uv/guides/tools/) (part of the `uv` Python package manager). Install with `pip install uv` or `brew install uv`. Without it, the Estimate phase falls back to cached pricing — migration still works but live pricing lookups are unavailable. ## Architecture & contributing -This plugin ships two migration skills built on **different architectures**, and this -matters if you contribute: - -- **heroku-to-aws** is built on the **phase DSL** — a declarative frontmatter grammar - an LLM interprets at runtime, with a static validator that checks the structure - before anything runs. It is the reference implementation and the **direction for all - new work**. +This plugin ships three migration skills built on **different architectures**, and +this matters if you contribute: + +- **heroku-to-aws** and **vercel-to-aws** are built on the **phase DSL** — a + declarative frontmatter grammar an LLM interprets at runtime, with a static + validator that checks the structure before anything runs. This is the reference + implementation and the **direction for all new work**. `vercel-to-aws` additionally + owns its own resumability ledger (`assessment-state.json`, independent of the + vendored `.phase-status.json`) since its assessment supports incremental, + effort-for-confidence input collection across multiple sessions — see + `skills/vercel-to-aws/references/state/assessment-state.schema.json` if you're + building a skill with similar "come back later with more input" needs. - **gcp-to-aws** predates the DSL and uses the **older prose design**. It is maintained, but a future effort will port it onto the DSL. @@ -244,6 +261,26 @@ python3 scripts/validate-migration-report.py \ See [fixtures/README.md](fixtures/README.md) for what `REPORT_OK` does and does not guarantee. +### Vercel assessment report validator (unit tests) + +When changing anything under `skills/vercel-to-aws/references/phases/report/`, +`scripts/validate-assessment-report.py`, or +`fixtures/assessment-report-reference.html`: + +```bash +cd migrate/plugins/migration-to-aws + +pytest tests/test_validate_assessment_report.py -q + +python3 scripts/validate-assessment-report.py \ + fixtures/assessment-report-reference.html + +# Stub must fail (regression guard) +python3 scripts/validate-assessment-report.py \ + fixtures/assessment-report-stub.html \ + && exit 1 || true +``` + ## Security For security issue notifications, see the repo-root diff --git a/migrate/plugins/migration-to-aws/fixtures/assessment-report-reference.html b/migrate/plugins/migration-to-aws/fixtures/assessment-report-reference.html new file mode 100644 index 00000000..f955fd55 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/assessment-report-reference.html @@ -0,0 +1,82 @@ + + + + +Vercel-to-AWS Assessment - reference-startup + + + + + + +
    +

    Recommendation: migrate to ECS Fargate. Your sustained traffic and small team's preference for predictable, debuggable infrastructure both point the same way.

    +
    + +
    +

    Owning your CDN configuration means you can cache aggressively enough to serve fewer origin requests, not just cheaper ones. Your bill becomes predictable instead of usage-spiky.

    +

    Separately, AWS Activate offers eligible early-stage startups credits (Founders tier: up to $5,000 self-service; Portfolio tier: up to $200,000 for VC/accelerator-backed companies) that apply directly to the AWS services this migration would use. Worth checking eligibility before finalizing a budget.

    +
    + +
    +

    Preview deployments as you know them go away first. Your reviewers will need a different workflow for testing branches before merge. Skew protection (keeping mismatched client/server deploys from breaking users mid-deploy) also needs to be handled explicitly on AWS instead of automatically.

    +
    + +
    +

    Two findings above would upgrade with more input: the ISR completeness finding would firm up if you confirm your autoscaling instance count, and the uncached-route cost estimate would go from a rough guess to a firm number with 7-14 days of log drain data.

    +
    + +
    +

    We detected middleware.ts on your project. Its matcher intersects several cached routes, and it appears to perform an authentication check. On every AWS target, a cached response from the CDN skips middleware entirely - so a visitor could receive a cached page that never had its auth check run.

    +
    + +
    + + + + + + + +
    FeatureDetectedWhy it matters
    ISR / revalidationYesPortable, needs a tag cache and queue together
    Edge middlewareYesBehavioral divergence on every AWS target
    Preview deploymentsYes (load-bearing)No AWS equivalent exists
    +
    + +
    +
    Your middleware runs auth checks on some pages that are also cached by the CDN - those checks get skipped on a cache hit.
    +
    A handful of your API routes fire much more often than they're cached, which is estimated monthly cost of roughly $85 you could trim with better caching.
    +
    + +
    +
    The recommendation rule that fired: your team said production ownership and predictable debugging matter more than minimal ops overhead, and your traffic is steady rather than spiky - that combination pointed to the container-based path over the serverless one.
    +
    + +
    +

    If you were a single low-traffic app with no AWS credits, a small VPS would be the more rational choice and this tooling would not be for you. That is not your situation here, since a separable backend surface exists.

    +
    + +
    +
      +
    1. Review the coupling score and pre-flight findings above with your team.
    2. +
    3. Optionally upgrading to Next.js 16.2+ would unlock higher-confidence discovery for a future re-assessment - entirely your call, not required to proceed.
    4. +
    5. Opt in to the scaffold checkpoint when you are ready for a working Terraform skeleton.
    6. +
    +
    + +
    +

    This is a draft for review. Verify all figures before acting on this report.

    +
    + + + diff --git a/migrate/plugins/migration-to-aws/fixtures/assessment-report-stub.html b/migrate/plugins/migration-to-aws/fixtures/assessment-report-stub.html new file mode 100644 index 00000000..bf07c3d1 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/assessment-report-stub.html @@ -0,0 +1,36 @@ + + + + +Vercel-to-AWS Assessment - stub + + + +
    +

    M1 fired at HIGH severity based on route_disposition analysis. See preflight-findings.json for details.

    +
    + +
    +

    Some cost savings are possible.

    +
    + +
    +

    Preview deployments. TODO: fill in the rest of this section.

    +
    + +
    +

    See coupling-score.json for the full breakdown.

    +
    + +
    +

    An uncached route costs $85 extra per month.

    +
    + +
    +
      +
    • Review findings
    • +
    +
    + + + diff --git a/migrate/plugins/migration-to-aws/fixtures/preflight-findings-reference.json b/migrate/plugins/migration-to-aws/fixtures/preflight-findings-reference.json new file mode 100644 index 00000000..816959a9 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/preflight-findings-reference.json @@ -0,0 +1,105 @@ +{ + "phase": "discover", + "timestamp": "2026-07-22T14:00:00Z", + "checks": [ + { + "id": "M1", + "detected": true, + "severity": "HIGH", + "applies_to": [ + "A", + "B", + "C" + ], + "confidence": "HIGH" + }, + { + "id": "M2", + "detected": false, + "severity": "MEDIUM", + "applies_to": [ + "A", + "B", + "C" + ], + "confidence": "HIGH" + }, + { + "id": "B1", + "detected": false, + "severity": "HIGH", + "applies_to": [ + "A" + ], + "confidence": "HIGH" + }, + { + "id": "B2", + "detected": false, + "severity": "MEDIUM", + "applies_to": [ + "A" + ], + "confidence": "HIGH" + }, + { + "id": "B3", + "detected": false, + "severity": "LOW", + "applies_to": [ + "A" + ], + "confidence": "HIGH" + }, + { + "id": "B4", + "detected": false, + "severity": "LOW", + "applies_to": [ + "A" + ], + "confidence": "HIGH" + }, + { + "id": "S1", + "detected": false, + "severity": "MEDIUM", + "applies_to": [ + "A" + ], + "confidence": "HIGH" + }, + { + "id": "I1", + "detected": true, + "severity": "HIGH", + "applies_to": [ + "A", + "B" + ], + "confidence": "MEDIUM", + "upgrade_input": "confirm autoscaling instance count" + }, + { + "id": "O1", + "detected": false, + "severity": "advisory", + "applies_to": [ + "A" + ], + "confidence": "HIGH" + }, + { + "id": "U1", + "detected": true, + "severity": "informational", + "applies_to": [ + "A", + "B", + "C" + ], + "confidence": "LOW", + "upgrade_input": "7-14 day log drain export" + } + ] +} diff --git a/migrate/plugins/migration-to-aws/fixtures/recommendation-reference.json b/migrate/plugins/migration-to-aws/fixtures/recommendation-reference.json new file mode 100644 index 00000000..52008e1a --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/recommendation-reference.json @@ -0,0 +1,15 @@ +{ + "phase": "recommend", + "timestamp": "2026-07-22T14:00:00Z", + "outcome": "B", + "fired_rule": 3, + "tiebreak": false, + "separable": null, + "backend_shape": null, + "confidence": "high", + "reasons": [ + "Sustained traffic (not spiky)", + "Team stated a debuggability preference over minimal ops overhead" + ], + "resolving_input": null +} diff --git a/migrate/plugins/migration-to-aws/fixtures/tier1-signals-reference.json b/migrate/plugins/migration-to-aws/fixtures/tier1-signals-reference.json new file mode 100644 index 00000000..4437cfea --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/tier1-signals-reference.json @@ -0,0 +1,19 @@ +{ + "phase": "prescan", + "timestamp": "2026-07-22T14:00:00Z", + "repo_access": true, + "next_build_health": "clean", + "vercel_token_present": true, + "project_list": [ + "reference-startup-web" + ], + "project_scoping_needed": false, + "next_version": "15.3.0", + "package_manager": "pnpm@9.0.0", + "has_sharp_dependency": false, + "lockfile_census": [ + "pnpm-lock.yaml" + ], + "has_middleware": true, + "has_vercel_json": true +} diff --git a/migrate/plugins/migration-to-aws/scripts/validate-assessment-report.py b/migrate/plugins/migration-to-aws/scripts/validate-assessment-report.py new file mode 100644 index 00000000..238c758b --- /dev/null +++ b/migrate/plugins/migration-to-aws/scripts/validate-assessment-report.py @@ -0,0 +1,604 @@ +#!/usr/bin/env python3 +"""Validate assessment-report.html completeness after the Report phase. + +Fork of scripts/validate-migration-report.py (GCP skill), adapted for +vercel-to-aws's outcome-filtered pre-flight-finding report structure. Checks +required section IDs, TOC anchor integrity, minimum appendix content, the +reader-vocabulary rule (no Pre-Flight Check IDs / artifact filenames / +Terraform resource IDs / "route disposition" in executive-flow sections), the +cost-labeling rule (every dollar figure phrased as "estimated monthly"), and +fixture-bleed detection. Exit 0 on PASS, 1 on FAIL, anything else means this +script itself did not run (e.g. python3 missing) - the caller must branch on +the shell exit code, never on stdout text alone. + +Usage: + python3 validate-assessment-report.py /path/to/assessment-report.html + python3 validate-assessment-report.py report.html \\ + --recommendation recommendation.json \\ + --preflight-findings preflight-findings.json \\ + --tier1-signals tier1-signals.json \\ + --migration-dir "$MIGRATION_DIR" + +Script location: this file lives at + migrate/plugins/migration-to-aws/scripts/validate-assessment-report.py +Agents should invoke it via Path(__file__) resolution or: + python3 "$(dirname ...)/scripts/validate-assessment-report.py" ... +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + +# Plugin root: migrate/plugins/migration-to-aws/ +PLUGIN_ROOT = Path(__file__).resolve().parent.parent + +# Requirement 9.1 / design.md 4.2 - always-required sections. +REQUIRED_SECTION_IDS = [ + "exec-verdict", + "what-you-gain", + "what-you-lose", + "coupling-score", + "preflight-findings", + "decision-traceability", + "next-steps", +] + +# Conditional sections - required only when their trigger condition holds +# (checked separately in validate_report(), not via REQUIRED_SECTION_IDS). +CONDITIONAL_SECTION_IDS = { + "exec-tiebreak": "recommendation.tiebreak == true", + "inputs-received": "any finding below HIGH confidence", + "appendix-m1": "tier1-signals.has_middleware == true", + "out-of-scope": "recommendation.outcome is 'C' or 'stay'", +} + +OPTIONAL_SECTION_IDS = list(CONDITIONAL_SECTION_IDS.keys()) + +FORBIDDEN_PATTERNS = [ + (r"\[placeholder\]", "placeholder text"), + (r"\bTODO\b", "TODO marker"), +] + +# Customer-facing readability rules (enforced unless --no-readability). +READABILITY_PATTERNS = [ + ( + r"Rubric:", + 'internal scoring trace ("Rubric:") - drop it or gate behind a ' + '
    "Why this mapping?" block', + ), + ( + r"Section\s+0\b", + 'literal "Section 0" heading - drop numeric "Section N" prefixes from ' + "customer-facing headings; let the table of contents carry structure", + ), + ( + r"]*>\s*Section\s+\d+[a-z]?\s*[\u2014-]", + 'numbered "Section N -" heading - drop numeric prefixes from headings; ' + "let the table of contents carry structure", + ), +] + +# Executive-flow sections must speak the founder's language, not the system's. +# Pre-Flight Check IDs, artifact filenames, Terraform resource IDs, and the +# term "route disposition" are internal build vocabulary - they belong in the +# technical appendices, not the executive summary (Requirement 9.7). +EXEC_SECTION_IDS = ( + "exec-verdict", + "exec-tiebreak", + "what-you-gain", + "what-you-lose", +) + +ARTIFACT_FILENAME_RE = re.compile(r"\b[a-z0-9][a-z0-9_-]*\.json\b", re.IGNORECASE) +TERRAFORM_RESOURCE_RE = re.compile(r"\baws_[a-z0-9_]+\.[a-z0-9_]+\b") +# The 10 named Pre-Flight Check IDs: M1, M2, B1-B4, S1, I1, O1, U1. +PREFLIGHT_CHECK_ID_RE = re.compile(r"\b(M1|M2|B[1-4]|S1|I1|O1|U1)\b") +ROUTE_DISPOSITION_RE = re.compile(r"route disposition", re.IGNORECASE) + +APPENDIX_STUB_PATTERNS = [ + re.compile( + r']*id="preflight-findings"[^>]*>.*?Full findings:\s*preflight-findings\.json', + re.DOTALL | re.IGNORECASE, + ), + re.compile( + r']*id="coupling-score"[^>]*>\s*

    \s*See\s*coupling-score\.json', + re.DOTALL | re.IGNORECASE, + ), +] + +MIN_CONTENT_DEPTH = { + "coupling-score": 3, + "preflight-findings": 2, + "decision-traceability": 1, +} + +SECTION_OPEN = re.compile( + r"]*\bid=(['\"])([^'\"]+)\1", + re.IGNORECASE, +) + +# Migration ID baked into the reference fixture. If this appears in a real +# $MIGRATION_DIR run, the agent copied the golden file verbatim (fixture bleed). +# Distinct from the GCP skill's canary (0611-0606) so the two never collide. +FIXTURE_CANARY_ID = "0722-1400" +MIGRATION_ID_RE = re.compile(r"\b(\d{4}-\d{4})\b") + +# Requirement 9.6 - every dollar figure must be phrased as "estimated monthly". +# Matches $123, $1,234.56, $1234, etc. +DOLLAR_AMOUNT_RE = re.compile(r"\$[0-9][0-9,]*(?:\.[0-9]{1,2})?") +ESTIMATED_MONTHLY_RE = re.compile(r"estimated\s+monthly", re.IGNORECASE) + +# AWS Activate credit ceilings (e.g. "up to $5,000 in AWS Activate credits") +# are one-time program limits, not a recurring cost or savings figure - +# forcing "estimated monthly" onto them would misrepresent a credit ceiling as +# a monthly estimate. Exempt a dollar figure from the cost-labeling rule ONLY +# when "activate" appears within the same window already used for the +# "estimated monthly" proximity check. Deliberately narrower than a bare +# "credit(s)" match (which would also match unrelated phrases like "credit +# card" or "store credit" and could let a real, unlabeled cost slip through) - +# "activate" is specific enough to this program that it should not appear +# near a dollar figure for any other reason in this report. +ACTIVATE_CREDIT_CONTEXT_RE = re.compile(r"\bactivate\b", re.IGNORECASE) + +# NOTE: _section_html uses non-greedy match to first . This assumes +# sections are NOT nested. Do not nest

    elements in assessment reports. + + +def plugin_script_path() -> Path: + """Return absolute path to this validator (for agent invocation).""" + return Path(__file__).resolve() + + +def _section_html(html: str, section_id: str) -> str | None: + pattern = re.compile( + rf"]*\bid=\"{re.escape(section_id)}\"[^>]*>(.*?)
    ", + re.DOTALL | re.IGNORECASE, + ) + match = pattern.search(html) + return match.group(1) if match else None + + +def _section_id_counts(html: str) -> dict[str, int]: + counts: dict[str, int] = {} + for match in SECTION_OPEN.finditer(html): + sid = match.group(2) + counts[sid] = counts.get(sid, 0) + 1 + return counts + + +def _validate_required_sections(html: str) -> list[str]: + errors: list[str] = [] + counts = _section_id_counts(html) + for section_id in REQUIRED_SECTION_IDS: + n = counts.get(section_id, 0) + if n == 0: + errors.append(f'missing required
    ') + elif n > 1: + errors.append(f'duplicate
    ({n} occurrences)') + return errors + + +def _validate_conditional_sections( + html: str, + recommendation: dict | None, + preflight_findings: dict | None, + tier1_signals: dict | None, +) -> list[str]: + """Requirement 9.2-9.5 - the four conditional-gate sections.""" + errors: list[str] = [] + counts = _section_id_counts(html) + + if recommendation and recommendation.get("tiebreak") is True: + if counts.get("exec-tiebreak", 0) < 1: + errors.append( + 'recommendation.tiebreak is true but no
    ' + "(the Outcome A/B side-by-side section is required per Requirement 9.2)" + ) + + if preflight_findings: + checks = preflight_findings.get("checks", []) + any_sub_high = any(c.get("confidence", "HIGH") != "HIGH" for c in checks) + if any_sub_high and counts.get("inputs-received", 0) < 1: + errors.append( + 'a finding is below HIGH confidence but no
    ' + "(the confidence-upgrade-offers section is required per Requirement 9.3)" + ) + + if tier1_signals and tier1_signals.get("has_middleware") is True: + if counts.get("appendix-m1", 0) < 1: + errors.append( + 'tier1-signals.has_middleware is true but no
    ' + "(required per Requirement 9.4)" + ) + + if recommendation and recommendation.get("outcome") in ("C", "stay"): + if counts.get("out-of-scope", 0) < 1: + errors.append( + f'recommendation.outcome is "{recommendation.get("outcome")}" but no ' + '
    (the separability rationale is required ' + "per Requirement 9.5)" + ) + + return errors + + +def _toc_hrefs(html: str) -> list[str]: + nav_match = re.search( + r"]*\bclass=[\"'][^\"']*toc[^\"']*[\"'][^>]*>(.*?)", + html, + re.DOTALL | re.IGNORECASE, + ) + if not nav_match: + return [] + return re.findall(r'href="#([^"]+)"', nav_match.group(1), re.IGNORECASE) + + +def _validate_toc(html: str) -> list[str]: + errors: list[str] = [] + hrefs = _toc_hrefs(html) + if not hrefs: + return errors # TOC optional if nav.toc absent; spec requires it in generated reports + + section_ids = set(_section_id_counts(html).keys()) + for href in hrefs: + if href not in section_ids: + errors.append(f'TOC broken link href="#{href}" - no matching
    ') + + for section_id in REQUIRED_SECTION_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}" ' + f'(add )' + ) + return errors + + +def _count_table_rows(section_html: str) -> int: + tbody = re.search(r"(.*?)", section_html, re.DOTALL | re.IGNORECASE) + if not tbody: + return 0 + return len(re.findall(r" int: + rows = _count_table_rows(section_html) + if section_id == "preflight-findings": + cards = len(re.findall(r'class="preflight-check-card"', section_html)) + return max(rows, cards) + if section_id == "decision-traceability": + entries = len(re.findall(r'class="trace-entry"', section_html)) + return max(rows, entries, 1 if re.search(r"fired\b", section_html, re.IGNORECASE) else 0) + return rows + + +def _readability_scope(html: str) -> str: + """Body only, excluding ", "", html, flags=re.DOTALL | re.IGNORECASE) + body = re.search(r"]*>(.*?)", no_style, re.DOTALL | re.IGNORECASE) + return body.group(1) if body else no_style + + +def _validate_readability(html: str) -> list[str]: + errors: list[str] = [] + scope = _readability_scope(html) + for pattern, label in READABILITY_PATTERNS: + if re.search(pattern, scope, re.IGNORECASE): + errors.append(f"readability: {label}") + return errors + + +def _validate_exec_vocabulary(html: str) -> list[str]: + """Requirement 9.7 - executive-flow sections must name what the founder + controls, not internal identifiers. Pre-Flight Check IDs, artifact + filenames, Terraform resource IDs, and "route disposition" belong only in + technical appendices. Appendix sections are exempt by design.""" + errors: list[str] = [] + for sid in EXEC_SECTION_IDS: + section = _section_html(html, sid) + if not section: + continue + filenames = sorted(set(m.lower() for m in ARTIFACT_FILENAME_RE.findall(section))) + resources = sorted(set(TERRAFORM_RESOURCE_RE.findall(section))) + check_ids = sorted(set(PREFLIGHT_CHECK_ID_RE.findall(section))) + has_route_disposition = bool(ROUTE_DISPOSITION_RE.search(section)) + if filenames: + errors.append( + f'exec vocabulary:
    exposes artifact filename(s) ' + f"{filenames} - name what the founder controls in the executive flow; " + "keep artifact filenames in the technical appendices" + ) + if resources: + errors.append( + f'exec vocabulary:
    exposes Terraform resource ID(s) ' + f"{resources} - move resource names to the appendix" + ) + if check_ids: + errors.append( + f'exec vocabulary:
    exposes Pre-Flight Check ID(s) ' + f'{check_ids} (e.g. "M1") - describe the behavior in plain language ' + '("your middleware skips on cached pages"), not the check ID' + ) + if has_route_disposition: + errors.append( + f'exec vocabulary:
    uses the term "route disposition" - ' + "this is internal build vocabulary; describe the behavior in plain language" + ) + return errors + + +def _validate_cost_labeling(html: str) -> list[str]: + """Requirement 9.6 - every dollar figure anywhere in the report body must + be phrased as "estimated monthly cost/savings", including U1's cost-driver + figures, even though full cost estimation is deferred to v2. Scoped to + table cells and sentences (a $ figure and "estimated monthly" must appear + within the same ... or within ~120 characters of each other). + + Exception: an AWS Activate credit ceiling (e.g. "up to $5,000 in AWS + Activate credits") is a one-time program limit, not a recurring cost or + savings estimate - "estimated monthly" would misdescribe it. Exempted only + when "Activate" appears in the same proximity window (deliberately not a + bare "credit(s)" match, which would also match unrelated phrases like + "credit card" and could let a real, unlabeled cost slip through).""" + errors: list[str] = [] + scope = _readability_scope(html) + + # Scan table cells first (most dollar figures live in tables). + for cell_match in re.finditer(r"]*>(.*?)", scope, re.DOTALL | re.IGNORECASE): + cell = cell_match.group(1) + if DOLLAR_AMOUNT_RE.search(cell) and not ESTIMATED_MONTHLY_RE.search(cell): + # allow "estimated monthly" in an adjacent header cell/caption - do a + # widened check against a window around the cell before flagging. + start = max(0, cell_match.start() - 200) + end = min(len(scope), cell_match.end() + 200) + window = scope[start:end] + if ESTIMATED_MONTHLY_RE.search(window): + continue + if ACTIVATE_CREDIT_CONTEXT_RE.search(window): + continue # one-time credit ceiling, not a cost/savings estimate + amount = DOLLAR_AMOUNT_RE.search(cell).group(0) + errors.append( + f'cost-labeling: dollar figure "{amount}" appears without "estimated ' + 'monthly" nearby - every dollar figure must be phrased as "estimated ' + 'monthly cost/savings" (Requirement 9.6, applies even to U1 findings) - ' + 'unless it is an AWS Activate credit ceiling, which reads "Activate" ' + "nearby instead" + ) + + # Then scan prose outside tables for stray dollar figures. + prose = re.sub(r"", "", scope, flags=re.DOTALL | re.IGNORECASE) + for amount_match in DOLLAR_AMOUNT_RE.finditer(prose): + start = max(0, amount_match.start() - 120) + end = min(len(prose), amount_match.end() + 120) + window = prose[start:end] + if ESTIMATED_MONTHLY_RE.search(window): + continue + if ACTIVATE_CREDIT_CONTEXT_RE.search(window): + continue # one-time credit ceiling, not a cost/savings estimate + errors.append( + f'cost-labeling: dollar figure "{amount_match.group(0)}" in prose appears ' + 'without "estimated monthly" nearby (Requirement 9.6) - unless it is an AWS ' + 'Activate credit ceiling, which reads "Activate" nearby instead' + ) + + return errors + + +def _validate_action_lists(html: str) -> list[str]: + """Requirement 9.1 - Next Steps must be an ordered list.""" + errors: list[str] = [] + next_steps = _section_html(html, "next-steps") or "" + if next_steps and not re.search(r" (ordered action items), not a bullet list ' + "or plain paragraphs (Requirement 9.1)" + ) + return errors + + +def _validate_decision_traceability(html: str, recommendation: dict | None) -> list[str]: + """Requirement 10.1-10.4 - the decision-traceability appendix is ALWAYS + required (checked in REQUIRED_SECTION_IDS) and must name the fired rule.""" + errors: list[str] = [] + section = _section_html(html, "decision-traceability") + if section is None: + return errors # already flagged by _validate_required_sections + if not re.search(r"\bfired\b|\brule\b", section, re.IGNORECASE): + errors.append( + "decision-traceability appendix must state which precedence rule fired " + "and why (Requirement 10.1, 10.3)" + ) + if recommendation and recommendation.get("tiebreak") is True: + if not re.search(r"log drain|resolving", section, re.IGNORECASE): + errors.append( + "decision-traceability appendix must state which rule would have applied " + "had the missing input (log drain data) been available, since the " + "tiebreak fired (Requirement 10.4)" + ) + return errors + + +def _validate_verdict(html: str, recommendation: dict | None) -> list[str]: + """Requirement 9.2 - exec-verdict must state a one-sentence verdict, not + only badges.""" + if not recommendation: + return [] + section = _section_html(html, "exec-verdict") or "" + if not section: + return [] # already flagged by _validate_required_sections + if re.search(r'class="[^"]*\bverdict\b[^"]*"', section, re.IGNORECASE): + return [] + if re.search(r"Recommendation:", section): + return [] + return [ + 'exec-verdict section exists but has no verdict banner ' + '(add an element with class="verdict" or a "Recommendation:" sentence)' + ] + + +def _validate_fixture_bleed(html: str, migration_dir: Path | None) -> list[str]: + """Catch agents that copied the reference fixture verbatim into a real run. + + Only active when --migration-dir is passed (i.e. validating a real + $MIGRATION_DIR report, not the fixture itself). Fails if the fixture canary + ID appears, or if the report's stated migration ID does not match the run dir. + """ + if migration_dir is None: + return [] # fixture-self-exemption: no run dir - don't flag the canary + + errors: list[str] = [] + dir_name = migration_dir.name + body = _readability_scope(html) + + if FIXTURE_CANARY_ID in body and dir_name != FIXTURE_CANARY_ID: + errors.append( + f'fixture bleed: reference canary migration ID "{FIXTURE_CANARY_ID}" appears in a ' + f'real run (--migration-dir={dir_name}) - the report was copied from the fixture' + ) + + ids_in_report = {m.group(1) for m in MIGRATION_ID_RE.finditer(body)} + if re.fullmatch(r"\d{4}-\d{4}", dir_name) and ids_in_report and dir_name not in ids_in_report: + errors.append( + f'migration ID mismatch: report references {sorted(ids_in_report)} but ' + f"--migration-dir is {dir_name} - verify the report belongs to this run" + ) + return errors + + +def validate_report( + html: str, + recommendation: dict | None = None, + preflight_findings: dict | None = None, + tier1_signals: dict | None = None, + *, + require_toc: bool = True, + check_readability: bool = True, + migration_dir: Path | None = None, +) -> list[str]: + errors: list[str] = [] + + errors.extend(_validate_required_sections(html)) + errors.extend( + _validate_conditional_sections(html, recommendation, preflight_findings, tier1_signals) + ) + + if require_toc: + if not _toc_hrefs(html): + errors.append('missing