- name
- migrate
- scope
- universal
- description
- [UDS] Guide systematic code migration, framework upgrades, and technology modernization.
Use when: planning a framework or major-version upgrade, assessing migration risk, capturing contract-test fixtures before an API migration.
Not for: in-place improvement that keeps the same framework — use /refactor; database schema design — use /database.
Keywords: migration, framework upgrade, modernization, breaking change, dependency upgrade, 遷移, 升級, 技術現代化.
- allowed-tools
- Read, Grep, Glob, Bash(npm:*, git:*)
- argument-hint
- [migration target or framework | 遷移目標或框架]
# Migration Assistant | 遷移助手
Guide systematic code migration, framework upgrades, and technology modernization.
引導系統性程式碼遷移、框架升級與技術現代化。
## Usage | 使用方式
| Command | Purpose | 用途 |
|---------|---------|------|
| `/migrate` | Start interactive migration guide | 啟動互動式遷移引導 |
| `/migrate --assess` | Risk assessment only | 僅風險評估 |
| `/migrate "Vue 2 to 3"` | Guide specific migration | 引導特定遷移 |
| `/migrate --deps` | Dependency upgrade analysis | 依賴升級分析 |
| `/migrate --rollback` | Plan rollback strategy | 規劃回滾策略 |
## Migration Types | 遷移類型
| Type | Examples | Risk | 風險 |
|------|----------|------|------|
| **Framework Upgrade** | React 17→18, Vue 2→3, Angular 15→17 | Medium-High | 中高 |
| **Language Migration** | JS→TS, Python 2→3, Java 8→17 | High | 高 |
| **API Version** | REST v1→v2, GraphQL schema update | Medium | 中 |
| **Database Migration** | MySQL→PostgreSQL, SQL→NoSQL | Very High | 極高 |
| **Build Tool** | Webpack→Vite, Grunt→ESBuild | Low-Medium | 低中 |
| **Package Manager** | npm→pnpm, pip→poetry | Low | 低 |
## Risk Assessment Matrix | 風險評估矩陣
| | Low Impact | Medium Impact | High Impact |
|---|-----------|---------------|-------------|
| **Low Complexity** | Safe (proceed) | Caution | Plan carefully |
| **Medium Complexity** | Caution | Plan + test | Staged rollout |
| **High Complexity** | Plan + test | Staged rollout | Full SDD spec |
| | 低影響 | 中影響 | 高影響 |
|---|--------|--------|--------|
| **低複雜度** | 安全(直接進行) | 謹慎 | 仔細規劃 |
| **中複雜度** | 謹慎 | 規劃 + 測試 | 分階段發布 |
| **高複雜度** | 規劃 + 測試 | 分階段發布 | 完整 SDD 規格 |
## Workflow | 工作流程
1. **ASSESS** - Evaluate current state, identify breaking changes
2. **PLAN** - Create migration checklist with dependencies
3. **PREPARE** - Set up codemods, compatibility layers, feature flags
4. **MIGRATE** - Execute migration in phases with tests
5. **VERIFY** - Run full test suite, check for regressions
6. **CLEANUP** - Remove compatibility shims, old dependencies
---
1. **評估** - 評估現狀、識別破壞性變更
2. **規劃** - 建立含依賴關係的遷移清單
3. **準備** - 設定 codemods、相容層、功能旗標
4. **遷移** - 分階段執行遷移並測試
5. **驗證** - 執行完整測試套件、檢查回歸
6. **清理** - 移除相容層、舊依賴
## API Migration Contract Tests | API 遷移合約測試
When migrating an API endpoint from one tech stack to another (PHP → .NET, Express → Spring, Python → Go), unit tests on the **new** implementation verify against the **new DTO** — they cannot catch fields that the legacy returned but the new implementation forgot. Missing fields, renamed fields, type drift, and top-level vs nested placement drift will silently survive into production and break the existing frontend that still expects the legacy shape.
This is **NOT** caught by unit tests, integration tests, or code review alone. A real 2026-05-24 production incident shipped with 67/67 green tests and was discovered by the customer.
當 API endpoint 從一個技術棧遷至另一個(PHP → .NET、Express → Spring 等),對**新**實作的單元測試只驗證**新 DTO**——無法捕捉「舊版有但新版漏掉」「欄位 rename」「型別漂移」「頂層 vs nested 層級漂移」等問題。這些缺陷會靜默流入生產,導致前端(仍預期舊版回應 shape)失靈。
**僅靠單元測試、整合測試或 code review 無法防止**。2026-05-24 真實 PROD 事故:67/67 測試全綠流入正式環境,由客戶發現缺漏。
### Mandatory rule | 強制規則
Every migrated API endpoint **MUST** ship with at least one contract test that compares the new implementation's response against a fixture captured from the legacy implementation. Structural equivalence (keys, types, placement), not value equality, is verified.
每個被遷移的 API endpoint **必須**至少有一份 contract test,比對新實作的 response 與從 legacy 捕獲的 fixture。驗證的是結構性等價(keys、type、層級位置),而非值等價。
### Fixture capture protocol | Fixture 捕獲協議
**While legacy is still running (typical migration window) | Legacy 仍運行(典型遷移窗口):**
```bash
# 1. Capture ≥3 representative inputs (happy path, edge case, empty result)
curl -X POST $LEGACY_BASE/endpoint -d @input1.json \
> tests/fixtures/migration/endpoint/scenario1.json
curl -X POST $LEGACY_BASE/endpoint -d @input2_empty.json \
> tests/fixtures/migration/endpoint/scenario2_empty.json
curl -X POST $LEGACY_BASE/endpoint -d @input3_edge.json \
> tests/fixtures/migration/endpoint/scenario3_edge.json
# 2. Scrub PII and volatile values (timestamps, generated IDs)
jq 'walk(if type == "string" and test("@") then "redacted@example.com" else . end)' \
tests/fixtures/migration/endpoint/scenario1.json > tmp && mv tmp ...
# 3. Commit fixtures
git add tests/fixtures/migration/endpoint/
```
**When legacy is already retired but source-readable | Legacy 已退役但 source 可讀:**
- Trace through legacy source code, manually construct the expected response shape
- Document each field's origin (SQL column, computed expression, hardcoded) in a sibling `.notes.md` file
- 從 legacy source code 推導預期 response shape;每個欄位來源(SQL 欄位 / 計算式 / hardcoded)記錄於同位置 `.notes.md`
### Contract test template
**C# / xUnit:**
```csharp
[Theory]
[InlineData("scenario1")]
[InlineData("scenario2_empty")]
[InlineData("scenario3_edge")]
public async Task Endpoint_ResponseShape_MatchesLegacyFixture(string scenario)
{
var fixture = LoadFixture($"migration/endpoint/{scenario}");
var response = await CallNewImpl(fixture.Input);
// StructuralEquivalence checks keys + types + placement, ignores values
StructuralEquivalence.Assert(response, fixture.ExpectedShape);
}
```
**TypeScript / Jest:**
```typescript
import { structuralEquivalence } from "./test-utils/structural-equivalence";
describe.each([
["scenario1"],
["scenario2_empty"],
["scenario3_edge"],
])("Endpoint response shape vs legacy fixture (%s)", (scenario) => {
test("matches", async () => {
const fixture = loadFixture(`migration/endpoint/${scenario}.json`);
const response = await callNewImpl(fixture.input);
structuralEquivalence(response, fixture.expectedShape);
});
});
```
`structuralEquivalence` / `StructuralEquivalence.Assert` rule: same set of keys at each level (no missing, no extra unless explicitly opted in), same primitive type per key, same placement (top-level vs nested). Values can differ (timestamps, IDs); types and structure cannot.
### Field-by-field migration audit checklist
Before merging any migrated endpoint:
- [ ] All legacy response fields mapped to new DTO (no silent drops)
- [ ] Naming preserved where possible (avoid `TotalX` rename that drops the "per-member" semantic)
- [ ] Top-level vs nested placement preserved
- [ ] Type compatibility verified (string→int conversions explicit, not coincidental)
- [ ] Error path return codes match legacy (`509` not `506`; `404` not `400`)
- [ ] Contract test fixture committed under `tests/fixtures/migration/`
- [ ] Cross-link to [contract-test-assistant](../contract-test-assistant/SKILL.md) for ongoing consumer-side verification
合併前必確認:
- [ ] 所有 legacy response 欄位 mapping 至新 DTO(無 silent drop)
- [ ] 命名儘量保留
- [ ] 頂層 vs nested 層級保留
- [ ] 型別相容性已明確驗證
- [ ] Error path return code 與 legacy 一致
- [ ] Contract test fixture 已 commit
- [ ] Cross-link 至 contract-test-assistant 做持續消費端驗證
## Post-Cutover Data Reconciliation | Cutover 後生產資料對帳
> **Implements**: XSPEC-284 R2 (axis ③ persisted-data semantics) / closes UDS issue [#134](https://github.com/AsiaOstrich/universal-dev-standards/issues/134).
Contract tests (above) and `behavior-snapshot` catch **interface** divergence but are blind to **persisted-data semantic** divergence. Two blind spots both share:
1. **You can only assert what you thought to enumerate.** The rules that actually ship broken are the *implicit, undocumented* ones — especially the semantics of a persisted field (when is it non-zero? when is it overwritten?). Nobody writes a scenario for a rule they don't know exists.
2. **Per-request parity ≠ data-at-rest parity.** A field written by an **asynchronous** process (delivery-receipt sync, settlement job, status reconciler) against a live external provider is not a deterministic request you can replay. Its correctness only emerges in **aggregate over real production volume**, not in a single happy-path snapshot.
Contract test 與 `behavior-snapshot`只能捕捉**介面**分歧,對**持久化資料語義**分歧視而不見。兩者共有兩個盲區:(1) 你只能驗證你想得到要列舉的規則——真正出包的永遠是沒人寫下來的隱含規則(某欄位何時非零、何時被覆寫);(2) per-request parity ≠ data-at-rest parity——由**非同步**程序(DR sync、結算批次、狀態對帳器)對 live 外部供應商寫入的欄位,不是可重放的確定性請求,其正確性只在**真實生產量的聚合**中浮現。
### Failure fingerprint | 事故指紋(#134)
某企業 SMS 平台 PHP→.NET 重寫:每筆金額由**非同步** DR sync 覆寫(`record.Cost = gatewayDr.Cost`)。legacy 對 carrier-failure 仍計費,rewrite 寫 gateway 回報的 `0` → cutover 邊界兩側同一失敗狀態的金額分歧。**所有既有 gate 全部漏接**(response shape 相同 → contract test 過;無人 curated「失敗仍計費」場景;欄位由背景作業對 live gateway 寫入 → 不可重放)。最後靠 ops 跑生產 `SUM(cost) GROUP BY status, day` 跨邊界彙總才發現。**單日抽樣甚至誤判「失敗不計費=正常」**——只有跨 cutover 邊界的多週聚合才揭露真相。
### Mandatory rule | 強制規則
When a migration loads legacy data into the **same store** as new data, the old/new boundary is a **free differential oracle**. For each business-critical persisted field you **MUST**:
- Define **aggregate reconciliation invariants** comparing the distribution of **legacy-origin vs new-origin** rows along key dimensions (e.g. `SUM(money) / COUNT(*) GROUP BY status, period`).
- Run them **on a schedule against production** and alert when divergence exceeds a declared tolerance.
- Investigate over a **multi-week window spanning the cutover** — never a single-day sample (a sampled check can confirm a *wrong* conclusion).
當 migration 將 legacy 資料載入與 new 相同的**儲存**,新舊邊界即是**免費差分神諭**。對每個 business-critical 持久化欄位**必須**:定義聚合對帳不變量(比對 legacy-origin vs new-origin 列沿關鍵維度的分佈)、對生產**排程執行**並於分歧超過宣告容差時告警、以跨 cutover 的**多週窗口**調查(切忌單日抽樣,抽樣可能坐實錯誤結論)。
### Cutover-boundary reconciliation SQL template | 對帳 SQL 模板
```sql
-- Reconcile a money/state field across the migration cutover boundary.
SELECT status,
SUM(CASE WHEN created < @cutover THEN 1 ELSE 0 END) AS legacy_rows,
SUM(CASE WHEN created < @cutover AND money_field > 0 THEN 1 ELSE 0 END) AS legacy_nonzero,
SUM(CASE WHEN created >= @cutover THEN 1 ELSE 0 END) AS new_rows,
SUM(CASE WHEN created >= @cutover AND money_field > 0 THEN 1 ELSE 0 END) AS new_nonzero
FROM records GROUP BY status;
-- Invariant: nonzero-ratio per status must not differ across the boundary beyond tolerance.
```
### Tolerance & alerting guidance | 容差與告警指引
| Aspect | Guidance | 指引 |
|--------|----------|------|
| **Invariant type** | nonzero-ratio / `SUM` / `COUNT` / `DISTINCT` / checksum per `GROUP BY status, period` | 每維度的非零比率 / 聚合相等 / checksum |
| **Tolerance** | Declare per field; 0% for hard accounting invariants, a small ε only for known legitimate drift (e.g. rounding) | 逐欄宣告;硬會計不變量 0%,僅已知合法漂移容許小 ε |
| **Window** | Multi-week, spanning cutover; bucket by `period` to localise the boundary | 多週、橫跨 cutover;以 `period` 分桶定位邊界 |
| **Schedule** | Standing post-cutover cron (daily) until boundary rows age out of active reporting | post-cutover 排程(每日)直到邊界列退出活躍報表 |
| **Alert** | Page on divergence > tolerance; route via `observability-assistant` alert rules | 分歧超過容差即告警;經 `observability-assistant` 告警規則路由 |
### Gate 0 — Implicit-rule capture for persisted business fields | 隱含規則擷取
Before migrating any feature that writes a persisted business field, **explicitly answer for each such field** three questions, then lock the answer as a snapshot scenario **or** a reconciliation invariant:
> 1. **When is this value SET?** 何時設值?
> 2. **When is it OVERWRITTEN?** 何時被覆寫?(尤其非同步路徑)
> 3. **When is it ZEROED / nulled?** 何時歸零/清空?
**High-risk implicit-rule checklist** — empirically recurring fingerprints | 高風險隱含規則檢查清單(經驗反覆出現的指紋):
- [ ] **Billing / charge semantics** — charged on submit vs on delivery; refund-on-failure? | 計費語義(提交時計費 vs 送達時計費;失敗退費?)
- [ ] **Enum / status-code mapping** — every legacy code mapped; "success set" defined identically | 列舉/狀態碼對映(每個 legacy 碼都對映;「成功集合」定義一致)
- [ ] **Null / empty handling** — empty string vs null vs absent; default-on-missing | 空值處理(空字串 vs null vs 不存在;缺值預設)
- [ ] **Field-name casing / serialization** — snake_case vs camelCase binding | 欄位命名大小寫/序列化(snake_case vs camelCase 綁定)
- [ ] **Timezone** — stored UTC vs local; report boundaries | 時區(存 UTC vs local;報表邊界)
- [ ] **Rounding / type coercion** — `"2.00"` (text) parsed as int → dropped to 0 | 四捨五入/型別強轉(`"2.00"` 文字被當 int 解析 → 掉成 0)
### 3-gate positioning | 3-gate 定位表
Make the boundary between gates explicit so each axis has an owner and nothing falls into the seam:
| Gate | Scope | When | 範圍 / 時機 |
|------|-------|------|------|
| [`behavior-snapshot`](../../core/behavior-snapshot.md) | per-request, **curated** scenarios | pre-UAT CI | per-request、人工 curated 場景;pre-UAT CI |
| Contract tests (above / #112) | response **shape** (keys / types / placement) | unit / integration | response shape(keys/型別/層級);單元/整合 |
| **This section (#134)** | **aggregate, data-at-rest semantics of async-written fields, over real volume** | **post-cutover, scheduled, production** | **非同步寫入欄位的聚合、靜態資料語義,跨真實量;post-cutover 排程生產** |
> Cross-ref: [`behavior-snapshot`](../../core/behavior-snapshot.md) (curated golden masters), [`observability-assistant`](../observability-assistant/SKILL.md) (reconciliation schedule + alert templates).
## Background Job / Side-Effect Completeness | 背景作業/副作用完整性
> **Implements**: XSPEC-284 R3 (axis ⑤).
Migration inventories (`/vo-inventory`, XSPEC-200 `type:background_job`) and Devil's-Advocate grep (XSPEC-201 Step 4) **annotate** cron / queue consumers / scheduled batches / webhooks / mail-send points — but annotation alone proves nothing. A background job can be listed in the manifest, exist in code, and still **never actually run** in the new system.
遷移清單與副作用 grep 只是**標注**背景作業——標注本身不證明任何事。背景作業可能在 manifest 列出、程式碼存在,卻在新系統**從未真正執行**。
### Mandatory rule | 強制規則
For every background side-effect carried over from legacy, verify **two** things — annotation is not enough:
| Check | Pre-flight | Post-cutover |
|-------|-----------|--------------|
| **(a) Exists** — the cron / queue consumer / webhook / mail point is implemented in the new system | source grep + registration check | — |
Auf GitHub ansehen