Skip to content

feat(migrate): add vercel-to-aws migration assessment skill - #141

Closed
herosjourney wants to merge 12 commits into
awslabs:mainfrom
herosjourney:feat/vercel-to-aws-migration-skill
Closed

feat(migrate): add vercel-to-aws migration assessment skill#141
herosjourney wants to merge 12 commits into
awslabs:mainfrom
herosjourney:feat/vercel-to-aws-migration-skill

Conversation

@herosjourney

@herosjourney herosjourney commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a new migration-to-aws skill: an honest Vercel-to-AWS assessment for Next.js apps (deliberately not a full migration plan — Vercel billing data is too thin for that, see Out of Scope in the spec). Built on the phase DSL, same architecture as heroku-to-aws.

What it does

  • PreScan -> Discover -> Clarify -> Recommend -> Report, plus an optional Scaffold checkpoint
  • Discover computes a Coupling Score and 10 named Pre-Flight Checks unconditionally, before Recommend has run, so a later outcome override doesn't require recomputation
  • Recommend is a fixed-precedence, three-outcome engine — OpenNext/SST, ECS Fargate, or a Vercel+AWS Hybrid (backend moves, Next.js app + PR previews stay on Vercel) — every recommendation traces to exactly one rule, never a judgment call
  • Report renders a validated HTML assessment (scripts/validate-assessment-report.py + pytest suite + reference/stub fixtures), enforcing an outcome-based findings filter, a reader-vocabulary rule (no check IDs/filenames in the executive summary), and a cost-labeling rule
  • Scaffold (optional, opt-in) 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) wherever SST/Terraform expose the option
  • assessment-state.json — a skill-owned resumability ledger, independent of the vendored .phase-status.json, so a founder can supply a log drain export a week later and only the affected findings recompute

Why

Vercel's infrastructure (CloudFront-equivalent behaviors, function tuning, edge routing) isn't directly exportable the way GCP/Heroku Terraform is. This skill derives what it can from build output, source configs (next.config.js, middleware.ts, vercel.json), and the Vercel API, and is honest about confidence — every sub-HIGH finding names the specific input that would upgrade it, and the report tells founders what they'd lose (PR previews first) alongside what they'd gain.

Files changed

  • New skill: migrate/plugins/migration-to-aws/skills/vercel-to-aws/ (SKILL.md, knowledge/, references/phases/{prescan,discover,clarify,recommend,report,scaffold}/, references/shared/, references/state/, references/vendored/)
  • New validator: scripts/validate-assessment-report.py + tests/test_validate_assessment_report.py + fixtures/assessment-report-{reference,stub}.html + 3 supporting reference JSON fixtures
  • Updated: migrate/README.md, migrate/plugins/migration-to-aws/README.md, .claude-plugin/marketplace.json, and the plugin's .claude-plugin/.codex-plugin/.cursor-plugin manifests to register the new skill's triggers/keywords
  • Spec: .kiro/specs/vercel-to-aws/{requirements,design,tasks}.md

Updates since opening (review rounds folded in)

  • Capture/parse split for Discover (mirrors the Heroku/GCP pattern): new discover-capture.md runs in the main window with a GET-only endpoint whitelist (doc-verified paths; the undocumented crons/stores rows resolve via vercel api list under explicit constraints — GET-only, subject path-segment match, resolved path logged to the manifest as an audit trail); the discover fragments are parse-only.
  • Honest token language: Vercel tokens cannot be permission-scoped read-only — the docs-verified guidance is resource scoping, short expiration, and revocation, with the GET whitelist enforcing our side.
  • Manifest classification fix: routes-manifest.json arrays classify path shape, not rendering; prerender-manifest membership decides static/isr/dynamic (a replay had mis-classified /dashboard under the old wording).
  • Fixtures: fixtures/vercel-capture/ — canned captures (including the three .next build manifests a repo-root .gitignore rule had silently swallowed; scoped negation added), mid-pipeline seeds, expected-discovery.json, and a stdlib asserter.
  • Vercel MCP note: optional enrichment tier documented with full-account-access cautions.

Relationship to other PRs

Test plan

  • node tools/frontmatter-validator/validate.ts skills/vercel-to-aws — 0 problems (6 phase files checked)

  • pytest tests/test_validate_assessment_report.py -q — 39/39 passing

  • python3 scripts/validate-assessment-report.py fixtures/assessment-report-reference.htmlREPORT_OK

  • python3 scripts/validate-assessment-report.py fixtures/assessment-report-stub.htmlREPORT_FAIL with actionable errors (regression guard)

  • dprint check and markdownlint-cli2 clean across every touched/new file

  • Vendored files (INTERPRETER.md, phase-status.schema.json) confirmed byte-identical to skills/shared/ canonical source

  • Full live dry-run of the backbone (PreScan through Report) against a real, buildable Next.js 15.3.0 fixture, plus a separate live dry-run of the Scaffold checkpoint across all outcome shapes (A/B/C-A-shaped/C-B-shaped/unresolved-tiebreak) — real next build, real terraform validate/fmt, real node --check on generated SST/CloudFront-Function code

  • 3 rounds of external review on the recommendation engine, with fixes verified after each round

  • Fresh-agent Discover replay from the committed fixture seeds (capture pre-done, zero network/build/token): route dispositions incl. the ISR cross-check and LOW-confidence Route Handlers, 10/10 preflight checks, names-only env capture — python3 fixtures/vercel-capture/check_expected_discovery.py <run-dir> → PASS

  • fixtures:check (from ci(migrate): fixtures integrity check + pricing-cache staleness report #151) green against this branch's fixture set

Out of scope (v1, documented in the spec)

Full cost estimation parity with the GCP skill's Estimate phase, a full "what Vercel provisions vs. what OpenNext provisions" infra diff, and Cloudflare/VPS migration paths (acknowledged in the report's honesty paragraph, never built).

…w 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).
@herosjourney
herosjourney requested review from a team as code owners July 15, 2026 00:23
…hipped) Per the precedent set in 033669c (removed heroku-eks-support, heroku-to-aws-migration, and migration-telemetry-sharing spec dirs from a prior PR), .kiro/specs/ is local Kiro working material, not project documentation this repo ships - no .kiro/specs/ directory exists anywhere on main today. The requirements/design/tasks docs used to drive this skill's build don't belong in the PR.
Logan Kleier added 2 commits July 15, 2026 14:11
…gration-skill

# Conflicts:
#	.claude-plugin/marketplace.json
#	migrate/README.md
#	migrate/plugins/migration-to-aws/.claude-plugin/plugin.json
#	migrate/plugins/migration-to-aws/.codex-plugin/plugin.json
#	migrate/plugins/migration-to-aws/.cursor-plugin/plugin.json
#	migrate/plugins/migration-to-aws/README.md
… the worker contract Three architecture contradictions made the skill unexecutable as written: prescan declared _interactive:false + _exec:rw while its collect fragment converses with the founder (token request), runs next build, and calls the Vercel API; four of discover's dispatched fragments needed shell or network the rw worker (Read/Grep/Glob/Write/Edit) does not have; and the token, collected interactively and never persisted, could not reach a dispatched worker at all. Retrofit (the pattern proven on heroku/gcp live discovery, awslabs#148/awslabs#149): - prescan runs INLINE (_interactive: true, _exec removed) — its work is conversational + shell + network by nature; scope boundary corrected (the Tier 1 build-health attempt belongs to prescan-collect per Requirements 1.1-1.2) - NEW discover-capture.md: main-window pre-work owning ALL shell/network — adapter/manifest build capture, a GET-only 8-endpoint API whitelist (Vercel tokens cannot be scoped read-only, so the whitelist IS the read-only guarantee), env keys projected to names before touching disk, headers-only probe capture, manifest.json index; VERCEL_TOKEN lives in an env var in the main window only - discover-adapter/manifests/api/probe become parse-only fragments reading capture/; triggers keyed to the capture manifest; new postconditions (no env values/token material anywhere; coupling items carry confidence) - prescan-collect token request rewritten per Vercel's token docs: read-only tokens do not exist — ask for project-scoped + shortest expiration, state the GET-whitelist enforcement honestly, give the revoke command - Vercel MCP positioned as optional read-only enrichment (runtime logs, access links) with the purchase-tool caution — not the discovery backbone Validated: fixtures/vercel-capture/ replay set (manifest-fallback path, ISR cross-check, unclassified Route Handlers, skipped crons/usage, names-only env keys) + fresh-agent Discover replay: HANDOFF_OK, zero network/build/token, check_expected_discovery.py PASS. Also: shared:sync refreshed the stale vendored INTERPRETER.md this branch carried.
@herosjourney

Copy link
Copy Markdown
Contributor Author

Pushed 8bc4db8 — an architecture retrofit applying the capture/parse pattern proven on the Heroku/GCP live-discovery PRs (#148/#149). Three contradictions made the skill unexecutable as written: 1. prescan declared _interactive: false + _exec: rw, but its collect fragment converses with the founder (token request), runs next build, and calls the Vercel API — none possible in a dispatched worker. It now runs inline (_interactive: true), and its scope boundary no longer forbids the Tier 1 build-health attempt its own fragment performs. 2. discover's dispatched rw worker (Read/Grep/Glob/Write/Edit — deliberately no Bash, no network) hosted four fragments needing builds, curls, or API calls. New discover-capture.md owns ALL shell/network in the main window (GET-only 8-endpoint whitelist, env keys projected to names before touching disk, headers-only probes, manifest index); the fragments are now parse-only with triggers keyed to the capture manifest. 3. The token could never reach the worker (collected interactively, never persisted, workers receive state only via artifacts). It now lives as VERCEL_TOKEN in the main window only — and per Vercel's token docs, the request language is corrected: read-only tokens don't exist (resource scoping only), so we ask for project-scoped + shortest expiration, state the GET-whitelist enforcement honestly, and give the revoke command. Also: Vercel MCP positioned as optional read-only enrichment (runtime logs / access links) rather than the backbone — it lacks env/stores/crons tools and its OAuth grant includes purchase tools; and shared:sync refreshed the stale vendored INTERPRETER.md this branch carried (it was failing mise run build). Validation: fixtures/vercel-capture/ replay set + a fresh-agent Discover run purely from the skill files: HANDOFF_OK, zero network/build/token activity, correct manifest-fallback routing (ISR cross-check, Route Handlers at LOW with the Adapter upgrade offer), check_expected_discovery.py PASS, full mise run build green. Pre-existing observations the replay surfaced (not addressed here, worth follow-ups): no defined severity value for non-fired pre-flight checks (agent used "none"); repo_access: true with a partial source tree has no input-vocabulary term for the upgrade path; suppressed_on present for B1/B3/S1 but not B2/B4 despite similar applies_to; M1's intersection rule is silent on rewrite TARGETS being cacheable routes. Heads-up for #144: this touches prescan/discover files identical on that branch — rebase it on this one after merge.

Logan Kleier added 2 commits July 18, 2026 23:11
…docs - Domains endpoint corrected to the documented /v9 (was /v5) - Env row note strengthened: the documented response schema carries value/ vsmValue/legacyValue fields even with decrypt=false — the key-name pipeline projection is the REAL protection, not the query param - Crons and storage rows reworded honestly: neither path is in the public REST reference; discover the GET endpoints via 'vercel api list' (the CLI's beta OpenAPI-backed api command), else record skipped — vercel.json and env-name/ dependency signals remain the fallbacks - Transport note added: prefer 'vercel api' (session/token auth, --paginate for the 100-per-page list endpoints, OpenAPI endpoint discovery), raw curl with manual pagination.next handling as fallback - Rate limits documented as a non-issue (200-1000 reads/min per endpoint vs this capture's small volume) - Fixture manifest endpoint strings synced
…fied whitelist The prior commit's message claimed this sync but the edit had failed on formatting drift — domains now /v9, crons/stores rows use the OpenAPI-discovered wording.
@herosjourney

Copy link
Copy Markdown
Contributor Author

Doc-verification round (f9d60a0 + 0a1b2c…): checked the capture whitelist against Vercel's REST API reference and limits docs. Four corrections, one improvement: 1. Domains endpoint was wrong — corrected to the documented /v9/projects/{id}/domains (I had guessed /v5). Deployments /v6 and env /v10 verified correct. 2. The env list response is value-bearing even with decrypt=false — the documented schema carries value/vsmValue/legacyValue fields. The key-name pipeline projection was designed as belt-and-suspenders; per the docs it's the ONLY real protection, and the spec now says so explicitly. 3. Crons and storage endpoint paths were guesses — neither is in the public REST reference (store rate limits prove endpoints exist, but paths aren't public). Rows reworded honestly: discover the GET endpoints via vercel api list, else skipped with the existing fallbacks (vercel.json crons; env-name/dependency signals for stores). 4. Rate limits verified a non-issue — documented per-endpoint read limits (200–1000/min) dwarf the capture volume; noted inline. 5. Improvement: the beta vercel api command (session/token auth, --paginate for the 100-per-page list endpoints, OpenAPI-backed endpoint discovery) is now the preferred transport, raw curl with manual pagination.next handling as fallback — this also closed a missing-pagination gap in my original table. Same lesson as the gcloud --region=- catch on #149: endpoint tables written from memory need a docs pass.

Logan Kleier added 2 commits July 18, 2026 23:21
…iew follow-up: rows 6-7 (crons/stores) stay unpinned because Vercel does not document those paths -- pinning guesses would be fake precision that breaks silently. But the discovery step was looser than the pinned rows. New constraints: the resolved operation must be GET in the spec, its path must contain the row's subject segment (crons / stores|storage), the exact resolved path is recorded verbatim in the manifest entry with discovered_via: 'vercel api list' (audit trail), and no match means skipped -- never a close-enough substitute. Fixture manifest updated to model the audit fields.
@herosjourney

Copy link
Copy Markdown
Contributor Author

Review follow-up pushed (1c1ceaa): constrained the OpenAPI-discovered capture rows (6–7, crons/stores). They stay unpinned deliberately — those paths are not in Vercel's public REST reference, and pinning guessed paths would be fake precision that breaks silently when they move. Instead the discovery step is now held to the same narrowness as the pinned rows: (1) the resolved operation must be GET in the OpenAPI spec, (2) its path must contain the row's subject segment (crons for row 6, stores/storage for row 7 — description mentions don't qualify), (3) the exact resolved path is recorded verbatim in the manifest entry with discovered_via: "vercel api list" so the parse fragments and reviewers can audit precisely what was called, and (4) no matching GET endpoint means skipped, never a close-enough substitute. Fixture manifest updated to model the audit fields. Build green. Still looser than Heroku's exact-command whitelist by one degree of freedom — the concrete path — but that degree is exactly what Vercel doesn't document; if they publish these endpoints, the rows should be pinned and the constraints block retired.

Logan Kleier added 4 commits July 19, 2026 10:45
…s The capture manifest's build.files names three .next build manifests (routes/prerender/app-path-routes) that were never committed -- the validated replay ran against scratch copies. Reconstructed exactly per expected-discovery.json and discover-manifests.md: /blog/[slug] in dynamicRoutes with two prerendered entries at initialRevalidateSeconds 3600 (genuinely ISR), / static, /dashboard absent from prerender-manifest (dynamic), /api/checkout only in app-path-routes-manifest (Route Handler, unclassified by page manifests -> LOW-confidence dynamic). Preview keys are labeled fixture placeholders, not secrets.
… repo-root .gitignore's global build/ rule silently dropped the three canned .next manifests from the previous commit. Scoped negation inside the fixture directory re-includes them — they are committed test data mirroring the real capture layout, not build output.
…endering Fresh-agent replay against the restored build-manifest fixtures surfaced the ambiguity: staticRoutes/dynamicRoutes distinguish fixed paths from [param] segments -- a fixed-path page that renders per-request still sits in staticRoutes, and the replay classified /dashboard as static because of it. The rendering disposition comes from prerender-manifest membership: numeric initialRevalidateSeconds = isr, false = static, absent = dynamic. Spelled out the resolution table and extended the never-infer rule to cover static-from-staticRoutes as well as isr-from-revalidate-exports.
…ase Caught by the new fixtures-check tool: the skill declares 6 phases (incl. scaffold) but the replay seed's .phase-status.json listed 5 -- phase-status.schema.json requires one entry per declared phase. (On the stacked generate-phase branch the seed already tracks that branch's estimate/generate phase set.)
@icarthick

Copy link
Copy Markdown
Collaborator

The vercel-to-aws skill this PR introduced is now on main — it landed through the stacked branches built on top of it (#153, merged as 46d2472, carried the full evolved skill via this PR's lineage).

Two things to flag:

  1. The base skill is on main. prescan → discover → clarify → recommend → report plus the report validator, fixtures, and manifests are all present under skills/vercel-to-aws/.

  2. The scaffold checkpoint was intentionally superseded, not dropped. The stack promoted the thin scaffold skeleton into a full estimate + generate phase. Main's SKILL.md carries the explicit breaking-change note:

    Breaking change: Assessments started before this version (with scaffold as a sidebar in .phase-status.json) are NOT compatible with the new backbone. Re-run from prescan to benefit from the estimate and generate phases.

    Merging this PR now would re-introduce the obsolete scaffold/*.md files and regress that breaking change.

Since the substance is merged and the headline scaffold feature was deliberately replaced, closing this as superseded. Thanks for the skill — it's live on main, just via the branches that grew from it. (_kind: checkpoint also became _kind: sidebar plugin-wide in #162, fyi.)

@icarthick icarthick closed this Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants