projectops 4.31.5 → 4.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +144 -145
  2. package/package.json +2 -2
  3. package/src/cli/args.js +65 -29
  4. package/src/cli/errors.js +9 -7
  5. package/src/cli/help.js +51 -3
  6. package/src/commands/doctor.js +80 -78
  7. package/src/commands/full.js +17 -3
  8. package/src/commands/interactive.js +78 -50
  9. package/src/commands/issues.js +8 -1
  10. package/src/commands/skills.js +8 -7
  11. package/src/commands/version.js +4 -2
  12. package/src/commands/workflows.js +3 -1
  13. package/src/context.js +4 -0
  14. package/src/core/breaking-check.js +6 -5
  15. package/src/core/copy/simple.js +27 -1
  16. package/src/core/copy/workflows.js +10 -5
  17. package/src/core/exclusions.js +8 -0
  18. package/src/core/ide/adapters/claude.js +18 -17
  19. package/src/core/ide/adapters/codex.js +12 -11
  20. package/src/core/ide/adapters/cursor.js +11 -10
  21. package/src/core/ide/adapters/gemini.js +11 -10
  22. package/src/core/ide/adapters/pi-common.js +2 -1
  23. package/src/core/ide/adapters/pi-harness.js +13 -16
  24. package/src/core/ide/adapters/pi.js +12 -11
  25. package/src/core/label-style.js +78 -0
  26. package/src/core/migration-guide.js +38 -38
  27. package/src/core/migrations/index.js +19 -15
  28. package/src/core/migrations/registry.js +70 -55
  29. package/src/core/options-ask.js +65 -64
  30. package/src/core/orphan-workflows.js +2 -1
  31. package/src/core/paths-resolve.js +31 -30
  32. package/src/core/repo-language.js +57 -0
  33. package/src/core/run-trace.js +10 -0
  34. package/src/core/version-yml.js +68 -36
  35. package/src/i18n/core.en.js +31 -0
  36. package/src/i18n/core.ko.js +31 -0
  37. package/src/i18n/doctor.en.js +89 -0
  38. package/src/i18n/doctor.ko.js +89 -0
  39. package/src/i18n/en.js +142 -0
  40. package/src/i18n/ide.en.js +102 -0
  41. package/src/i18n/ide.ko.js +102 -0
  42. package/src/i18n/index.js +73 -0
  43. package/src/i18n/ko.js +135 -0
  44. package/src/i18n/migrate.en.js +66 -0
  45. package/src/i18n/migrate.ko.js +66 -0
  46. package/src/i18n/options.en.js +104 -0
  47. package/src/i18n/options.ko.js +104 -0
  48. package/src/i18n/summary.en.js +70 -0
  49. package/src/i18n/summary.ko.js +70 -0
  50. package/src/index.js +46 -28
  51. package/src/ui/banner.js +8 -6
  52. package/src/ui/env-plan.js +19 -18
  53. package/src/ui/prompts.js +25 -24
  54. package/src/ui/readline-engine.js +18 -10
  55. package/src/ui/skills-prompts.js +8 -7
  56. package/src/ui/status-cards.js +38 -38
  57. package/src/ui/summary.js +103 -111
@@ -11,34 +11,34 @@ export const GUIDE_FILE = `${MIGRATION_DIR}/PROJECTOPS-MIGRATION-GUIDE.md`;
11
11
  export const GUIDE_SCHEMA = 1;
12
12
 
13
13
  // 고정 헤더 — 문서 목적 + AI Agent 해석 가이드라인. 최초 생성 시 1회만 쓴다 (엔트리와 분리).
14
- const HEADER = `# ProjectOps 마이그레이션 가이드
14
+ const HEADER = `# ProjectOps Migration Guide
15
15
 
16
- 이 문서는 projectops 마법사(\`npx projectops\`)가 **실행을 마칠 때마다 자동으로 기록**하는
17
- 마이그레이션 이력입니다. 실행 엔트리는 아래에 시간순으로 append되며, 기존 엔트리는 수정되지 않습니다.
16
+ This document is **written automatically every time a run finishes** by the projectops wizard (\`npx projectops\`).
17
+ Run entries are appended below in chronological order, and existing entries are never modified.
18
18
 
19
- ## 사람이 읽는 법
19
+ ## How humans read it
20
20
 
21
- - 각 엔트리의 **확인 체크리스트**가 그 실행 이후 사람이 마무리해야 하는 수동 작업입니다.
22
- - 체크리스트는 실행에서 실제 발생한 것만 나옵니다. 항목이 없으면 할 일이 없다는 뜻입니다.
23
- - 상세 실행 기록(파일별 결정과 치환 전후값)은 엔트리 메타의 \`trace_file\`(JSONL)과 \`log_file\`(터미널 원문)에 있습니다.
21
+ - The **checklist** in each entry lists the manual work a person still has to finish after that run.
22
+ - The checklist only contains things that actually happened in the run. No items means nothing to do.
23
+ - Detailed run records (per-file decisions and before/after values) are in the entry metadata: \`trace_file\` (JSONL) and \`log_file\` (raw terminal output).
24
24
 
25
- ## AI Agent 해석 가이드라인
25
+ ## Guidelines for AI agents
26
26
 
27
- 각 엔트리의 \`\`\`yaml 메타 블록(\`# projectops-migration\`)을 파싱해 아래 규칙으로 판단하고 조치한다.
27
+ Parse the \`\`\`yaml metadata block (\`# projectops-migration\`) of each entry and decide and act with the rules below.
28
28
 
29
- | 필드 | 의미 | 권장 조치 |
30
- |------|------|----------|
31
- | \`workflows.leftover_old_gen\` | 구세대 워크플로우가 현역 가능성으로 보존됨 | 신형과 트리거 중복 여부 검사 → 전환 확인 후 구 파일 삭제 제안 |
32
- | \`workflows.skipped_conflict\` | 사용자 수정본이 유지됨 (신형 미적용) | 사용자본과 신형 템플릿 diff → 병합 제안 |
33
- | \`workflows.replaced_bak\` | .bak 백업 후 신형으로 교체됨 | .bak과 신형 비교 → 사용자 커스텀 유실분 복원 검토 후 .bak 정리 |
34
- | \`env_applied\` | 워크플로우에 적용된 환경값 | 실제 워크플로우 env와 대조 → 드리프트 발견 시 경고 |
35
- | \`breaking_traversed\` | 이 실행이 통과한 호환성 변경 (조치 방법 전문은 사람용 섹션) | \`action_required: true\` 항목의 조치 완료 여부 확인 |
36
- | \`manual_actions_pending\` | 남은 수동 작업 코드 목록 | 비어 있지 않으면 사용자에게 상기 |
37
- | \`trace_file\` | 파일별 결정과 치환 전후값 JSONL (Layer 2) | "왜 이 파일이 이렇게 됐나"는 파일명으로 grep |
38
- | \`log_file\` | 터미널 출력 원문 (Layer 3) | 실행 재현, 포렌식 디버깅용 |
29
+ | Field | Meaning | Recommended action |
30
+ |-------|---------|--------------------|
31
+ | \`workflows.leftover_old_gen\` | Old-generation workflows kept because they may be live | Check for trigger overlap with the new ones, then suggest deleting the old file once the switch is confirmed |
32
+ | \`workflows.skipped_conflict\` | The user's modified file was kept (new template not applied) | Diff the user's file against the new template and suggest a merge |
33
+ | \`workflows.replaced_bak\` | Replaced by the new one after a .bak backup | Compare .bak with the new file, review any lost customizations, then clean up the .bak |
34
+ | \`env_applied\` | Environment values applied to workflows | Compare with the real workflow env and warn on drift |
35
+ | \`breaking_traversed\` | Breaking changes this run passed through (full action steps are in the human section) | Check that items with \`action_required: true\` were handled |
36
+ | \`manual_actions_pending\` | Codes of the remaining manual work | If not empty, remind the user |
37
+ | \`trace_file\` | JSONL of per-file decisions and before/after values (Layer 2) | To learn why a file ended up this way, grep by file name |
38
+ | \`log_file\` | Raw terminal output (Layer 3) | For reproducing the run and forensic debugging |
39
39
 
40
- - 스키마는 \`schema\` 필드로 버저닝된다. 모르는 필드는 무시하고, 아는 필드만 사용한다.
41
- - 여러 엔트리가 있으면 **가장 최근 엔트리**가 현재 상태의 기준이다. 과거 엔트리는 이력 참고용.
40
+ - The schema is versioned by the \`schema\` field. Ignore unknown fields and use only the ones you know.
41
+ - When there are several entries, the **latest entry** is the reference for the current state. Older entries are history.
42
42
  `;
43
43
 
44
44
  // ── yaml 렌더 헬퍼 (외부 의존성 없이 수동 직렬화 — version-yml.js와 동일 원칙) ──
@@ -98,45 +98,45 @@ export function renderGuideEntry(report) {
98
98
  L.push("");
99
99
  L.push(`## ${r.now || ""} - v${from} → v${to} (${r.mode || "full"})`);
100
100
  L.push("");
101
- L.push(`- 타입: ${(r.types ?? []).join(", ") || "-"} / 배포: ${r.options?.deploy ?? "-"} / publish: ${(r.options?.publish ?? []).join(",") || "없음"}`);
102
- L.push(`- 워크플로우: 신규/갱신 ${wf.added.length + wf.replacedBak.length}개, 유지(unchanged/충돌스킵) ${(r.counters?.skipped ?? 0)}개`);
101
+ L.push(`- Types: ${(r.types ?? []).join(", ") || "-"} / deploy: ${r.options?.deploy ?? "-"} / publish: ${(r.options?.publish ?? []).join(",") || "none"}`);
102
+ L.push(`- Workflows: ${wf.added.length + wf.replacedBak.length} new/updated, ${(r.counters?.skipped ?? 0)} kept (unchanged or skipped on conflict)`);
103
103
  L.push("");
104
104
 
105
105
  // ── 확인 체크리스트 (동적 — 실제 발생분만) ──
106
106
  const checklist = [];
107
107
  if (leftoverOldGen.length) {
108
- checklist.push(`- [ ] **구세대 배포 워크플로우 ${leftoverOldGen.length}개 전환 후 삭제** (현역 배포일 수 있어 마법사가 건드리지 않았습니다):`);
109
- for (const o of leftoverOldGen) checklist.push(` - \`${o.file}\`${o.replacement ? ` → 신형 \`${o.replacement}\`` : ""}`);
108
+ checklist.push(`- [ ] **Switch to the new workflow, then delete ${leftoverOldGen.length} old-generation deploy workflow(s)** (they may be live deployments, so the wizard left them alone):`);
109
+ for (const o of leftoverOldGen) checklist.push(` - \`${o.file}\`${o.replacement ? ` → new \`${o.replacement}\`` : ""}`);
110
110
  }
111
111
  if (wf.replacedBak.length || legacyNeutralized.length) {
112
- checklist.push(`- [ ] **.bak 백업 파일 확인 후 정리** (커스텀 유실분이 없는지 신형과 비교하세요):`);
113
- for (const f of wf.replacedBak) checklist.push(` - \`${f}.bak\` (충돌 교체 백업)`);
114
- for (const a of legacyNeutralized) if (a.to && String(a.to).endsWith(".bak")) checklist.push(` - \`${a.to}\` (레거시 무해화)`);
112
+ checklist.push(`- [ ] **Review the .bak backup files, then clean them up** (compare with the new file to be sure no customization was lost):`);
113
+ for (const f of wf.replacedBak) checklist.push(` - \`${f}.bak\` (backup from conflict replacement)`);
114
+ for (const a of legacyNeutralized) if (a.to && String(a.to).endsWith(".bak")) checklist.push(` - \`${a.to}\` (legacy neutralized)`);
115
115
  }
116
116
  if (wf.skippedConflict.length) {
117
- checklist.push(`- [ ] **기존 수정본 유지 ${wf.skippedConflict.length}개, 신형과 병합 검토**: ${wf.skippedConflict.map((f) => `\`${f}\``).join(", ")}`);
117
+ checklist.push(`- [ ] **Kept ${wf.skippedConflict.length} modified file(s), review a merge with the new template**: ${wf.skippedConflict.map((f) => `\`${f}\``).join(", ")}`);
118
118
  // #654 — 새 템플릿 사본이 있으면 비교 방법까지 적는다
119
119
  for (const d of wf.skippedConflictDetails.filter((x) => x.incoming)) {
120
- const cnt = d.added != null && d.removed != null ? ` (추가 ${d.added}줄, 삭제 ${d.removed}줄)` : "";
120
+ const cnt = d.added != null && d.removed != null ? ` (+${d.added} / -${d.removed} lines)` : "";
121
121
  checklist.push(` - \`${d.file}\`${cnt}: \`diff -u .github/workflows/${d.file} ${d.incoming}\``);
122
122
  }
123
123
  }
124
124
  if (wf.added.length || wf.replacedBak.length) {
125
- checklist.push(`- [ ] **새/갱신 CICD가 요구하는 GitHub Secrets 등록 확인** (Settings → Secrets → Actions, \`_GITHUB_PAT_TOKEN\` 포함)`);
125
+ checklist.push(`- [ ] **Check that the GitHub Secrets required by the new/updated CICD are registered** (Settings → Secrets → Actions, including \`_GITHUB_PAT_TOKEN\`)`);
126
126
  }
127
127
  if (envByType.size) {
128
- checklist.push(`- [ ] **적용된 배포 환경값 검증** (실제 환경과 다르면 워크플로우 env를 직접 수정):`);
128
+ checklist.push(`- [ ] **Verify the applied deploy environment values** (edit the workflow env directly if they differ from your real environment):`);
129
129
  for (const [t, kv] of envByType) {
130
130
  for (const [k, v] of kv) checklist.push(` - ${t} \`${k}\` = \`${v}\``);
131
131
  }
132
132
  }
133
133
  if (r.branches?.created === true) {
134
- checklist.push(`- [x] 개발(릴리스 소스) 브랜치 \`${r.branches?.deployBranch ?? "develop"}\`: 마법사가 생성 및 확인 완료`);
134
+ checklist.push(`- [x] Development (release source) branch \`${r.branches?.deployBranch ?? "develop"}\`: created and verified by the wizard`);
135
135
  } else if (r.branches?.ready === false) {
136
- checklist.push(`- [ ] 개발(릴리스 소스) 브랜치 \`${r.branches?.deployBranch ?? "develop"}\` 생성 (릴리스 PR이 동작하려면 필요합니다)`);
136
+ checklist.push(`- [ ] Create the development (release source) branch \`${r.branches?.deployBranch ?? "develop"}\` (required for the release PR to work)`);
137
137
  }
138
138
  if (checklist.length) {
139
- L.push("### 확인 체크리스트");
139
+ L.push("### Checklist");
140
140
  L.push("");
141
141
  L.push(...checklist);
142
142
  L.push("");
@@ -144,7 +144,7 @@ export function renderGuideEntry(report) {
144
144
 
145
145
  // ── 버전 점프에서 통과한 호환성 변경 (조치 방법 전문 — 터미널에서 스킵해도 여기 남는다) ──
146
146
  if (breakingAll.length) {
147
- L.push(`### 통과한 호환성 변경 (v${breaking.current} → v${breaking.target})`);
147
+ L.push(`### Breaking changes passed through (v${breaking.current} → v${breaking.target})`);
148
148
  L.push("");
149
149
  for (const it of breakingAll) {
150
150
  const sev = (breaking.critical ?? []).includes(it) ? "CRITICAL" : "WARNING";
@@ -155,7 +155,7 @@ export function renderGuideEntry(report) {
155
155
  }
156
156
 
157
157
  // ── AI 메타데이터 ──
158
- L.push("### AI 메타데이터");
158
+ L.push("### AI metadata");
159
159
  L.push("");
160
160
  L.push("```yaml");
161
161
  L.push("# projectops-migration (machine-readable)");
@@ -164,7 +164,7 @@ export function renderGuideEntry(report) {
164
164
  L.push(`template: { from: ${yq(from)}, to: ${yq(to)} }`);
165
165
  L.push(`mode: ${r.mode || "full"}`);
166
166
  L.push(`types: ${ylist(r.types)}`);
167
- L.push(`options: { deploy: ${yq(r.options?.deploy ?? "")}, publish: ${ylist(r.options?.publish)}, secret_backup: ${r.options?.secretBackup === true}, coderabbit: ${r.options?.coderabbit === true}, changelog_provider: ${yq(r.options?.changelogProvider ?? "")}, intent: ${yq(r.options?.intent ?? "")}, semver_auto: ${r.options?.semverAuto === true}, app_release: ${r.options?.appRelease === true} }`);
167
+ L.push(`options: { deploy: ${yq(r.options?.deploy ?? "")}, publish: ${ylist(r.options?.publish)}, secret_backup: ${r.options?.secretBackup === true}, coderabbit: ${r.options?.coderabbit === true}, changelog_provider: ${yq(r.options?.changelogProvider ?? "")}, intent: ${yq(r.options?.intent ?? "")}, semver_auto: ${r.options?.semverAuto === true}, language: ${yq(r.options?.language ?? "")}, label_style: ${yq(r.options?.labelStyle ?? "")}, close_on_release: ${r.options?.closeOnRelease === true}, app_release: ${r.options?.appRelease === true} }`);
168
168
  L.push(`branches: { default: ${yq(r.branches?.defaultBranch ?? "main")}, deploy: ${yq(r.branches?.deployBranch ?? "develop")}, deploy_branch_created: ${r.branches?.created === true} }`);
169
169
  L.push("workflows:");
170
170
  L.push(` added: ${ylist(wf.added)}`);
@@ -2,10 +2,14 @@
2
2
  // 감지(registry × rules) → 계획 표시 → safe 티어 확인 1회 → 적용 → confirm 티어 안내.
3
3
  // 버전 번호를 믿지 않는다: 절반의 레거시 레포는 version.yml에 template 메타가 없다(실측).
4
4
  import { MIGRATIONS } from "./registry.js";
5
+ import { t } from "../../i18n/index.js";
5
6
  import * as workflowRule from "./rules/obsolete-workflows.js";
6
7
  import * as rootFileRule from "./rules/root-files.js";
7
8
  import * as legacyDirRule from "./rules/legacy-dirs.js";
8
9
 
10
+ // 사유는 화면 언어로 번역(reasonKey), 키가 없으면 원문 그대로
11
+ const reasonOf = (e) => (e.reasonKey ? t(e.reasonKey) : e.reason);
12
+
9
13
  const RULES = {
10
14
  workflow: workflowRule,
11
15
  "root-file": rootFileRule,
@@ -53,60 +57,60 @@ export async function runMigrations({ targetRoot = ".", askYesNo = null, log = c
53
57
 
54
58
  let applied = [];
55
59
  if (safe.length > 0) {
56
- log(`🧹 레거시 템플릿 파일 ${safe.length}개 감지 — 신형으로 대체되어 공존 시 중복 실행 위험:`);
60
+ log(t("migrate.safe.found", { n: safe.length }));
57
61
  for (const e of safe) {
58
62
  const arrow = e.replacedBy ? ` → ${e.replacedBy}` : "";
59
63
  log(` • ${e.file}${arrow}`);
60
- log(` (${e.reason}, ${e.since}부터 폐기)`);
64
+ log(t("migrate.safe.reason", { reason: reasonOf(e), since: e.since }));
61
65
  }
62
66
  const yes = askYesNo
63
- ? await askYesNo(`위 ${safe.length}개를 정리할까요? (워크플로우는 .bak 무해화 — 복원 가능)`, true)
67
+ ? await askYesNo(t("migrate.safe.ask", { n: safe.length }), true)
64
68
  : true;
65
69
  if (yes) {
66
70
  applied = applySafeMigrations(targetRoot, safe);
67
71
  const ok = applied.filter((r) => r.action !== "error");
68
72
  const failed = applied.filter((r) => r.action === "error");
69
- log(`✅ 레거시 정리 완료: ${ok.length}개${failed.length ? ` (실패 ${failed.length}개)` : ""}`);
73
+ log(t("migrate.safe.done", { n: ok.length, failed: failed.length ? t("migrate.safe.failed", { n: failed.length }) : "" }));
70
74
  for (const f of failed) log(` ⚠️ ${f.from}: ${f.error}`);
71
75
  } else {
72
- log("→ 레거시 정리를 건너뜁니다 (다음 업데이트 때 다시 안내)");
76
+ log(t("migrate.safe.skip"));
73
77
  }
74
78
  }
75
79
 
76
80
  // ask 티어 (#476) — 사용자 문서가 담긴 구명칭 폴더: 대화형은 확인 후 이동, 비대화형은 안내만
77
81
  let askPending = [];
78
82
  if (ask.length > 0) {
79
- log(`📁 구명칭 산출물 폴더 ${ask.length}개 감지 — 사용자 문서가 들어 있어 확인 후 이동합니다:`);
83
+ log(t("migrate.dir.found", { n: ask.length }));
80
84
  for (const e of ask) {
81
85
  log(` • ${e.file}/ → ${e.replacedBy}/`);
82
- log(` (${e.reason})`);
86
+ log(` (${reasonOf(e)})`);
83
87
  }
84
88
  if (askYesNo) {
85
- const yes = await askYesNo("위 폴더를 새 이름으로 이동할까요? (파일 손실 없음 — 충돌 시 원본 유지)", true);
89
+ const yes = await askYesNo(t("migrate.dir.ask"), true);
86
90
  if (yes === true) {
87
91
  const results = applySafeMigrations(targetRoot, ask);
88
92
  applied = applied.concat(results);
89
93
  for (const r of results) {
90
94
  if (r.action === "error") log(` ⚠️ ${r.from}: ${r.error}`);
91
- else log(` ✅ ${r.from}/ → ${r.to}/ (이동 ${r.moved ?? 0}개${r.skipped ? `, 충돌 유지 ${r.skipped}개` : ""})`);
95
+ else log(t("migrate.dir.moved", { from: r.from, to: r.to, moved: r.moved ?? 0, kept: r.skipped ? t("migrate.dir.kept", { n: r.skipped }) : "" }));
92
96
  }
93
97
  } else {
94
98
  askPending = ask;
95
- log("→ 폴더 이동을 건너뜁니다 (다음 업데이트 때 다시 안내)");
99
+ log(t("migrate.dir.skip"));
96
100
  }
97
101
  } else {
98
102
  askPending = ask;
99
- log(" 비대화형 실행이라 자동으로 이동하지 않습니다 — 대화형 마법사(npx projectops)에서 확인 후 이동하세요.");
103
+ log(t("migrate.dir.nonInteractive"));
100
104
  }
101
105
  }
102
106
 
103
107
  if (confirm.length > 0) {
104
- log(`⚠️ 구세대 배포 워크플로우 ${confirm.length}개 발견 — 현역 배포일 수 있어 자동으로 건드리지 않습니다:`);
108
+ log(t("migrate.confirm.found", { n: confirm.length }));
105
109
  for (const e of confirm) {
106
- const arrow = e.replacedBy ? ` (신형: ${e.replacedBy})` : "";
107
- log(` • ${e.file}${arrow} — ${e.reason}`);
110
+ const arrow = e.replacedBy ? t("migrate.confirm.newer", { name: e.replacedBy }) : "";
111
+ log(` • ${e.file}${arrow}: ${reasonOf(e)}`);
108
112
  }
109
- log(" 신형 워크플로우로 전환을 마친 뒤 구 파일을 직접 삭제하세요.");
113
+ log(t("migrate.confirm.hint"));
110
114
  }
111
115
 
112
116
  return { applied, confirmPending: confirm, askPending };
@@ -24,183 +24,198 @@
24
24
  // (rules/settings-extractors.js의 EXTRACTORS 키)
25
25
  //
26
26
  // 데이터 출처: git 히스토리 삭제/리네임 전수 + 실제 통합 레포 14개 스캔 (2026-07-11, 설계 문서 참조)
27
+ import { tEn } from "../../i18n/index.js";
28
+
27
29
  export const MIGRATIONS = [
28
30
  // ── workflow / safe — 순수 리네임·대체 (공존 시 중복 실행 실해) ──────────────
29
31
  { id: "wf-version-control", category: "workflow", tier: "safe",
30
32
  file: "PROJECT-VERSION-CONTROL.yaml", replacedBy: "PROJECT-COMMON-VERSION-CONTROL.yaml",
31
- since: "2.x", reason: "1세대 리네임 — 공존 시 버전 이중 증가" },
33
+ since: "2.x", reasonKey: "migrate.reason.gen1VersionControl" },
32
34
  { id: "wf-readme-version-update", category: "workflow", tier: "safe",
33
35
  file: "PROJECT-README-VERSION-UPDATE.yaml", replacedBy: "PROJECT-COMMON-README-VERSION-UPDATE.yaml",
34
- since: "2.x", reason: "1세대 리네임 — 공존 시 README 이중 커밋" },
36
+ since: "2.x", reasonKey: "migrate.reason.gen1Readme" },
35
37
  { id: "wf-sync-issue-labels-v1", category: "workflow", tier: "safe",
36
38
  file: "PROJECT-SYNC-ISSUE-LABELS.yaml", replacedBy: "PROJECT-COMMON-SYNC-ISSUE-LABELS.yaml",
37
- since: "2.x", reason: "1세대 리네임 — 공존 시 라벨 동기화 중복" },
39
+ since: "2.x", reasonKey: "migrate.reason.gen1Labels" },
38
40
  { id: "wf-sync-issue-labels-v0", category: "workflow", tier: "safe",
39
41
  file: "sync-issue-labels.yaml", replacedBy: "PROJECT-COMMON-SYNC-ISSUE-LABELS.yaml",
40
- since: "2.x", reason: "0세대 리네임 — 공존 시 라벨 동기화 중복" },
42
+ since: "2.x", reasonKey: "migrate.reason.gen0Labels" },
41
43
  { id: "wf-issue-comment-v1", category: "workflow", tier: "safe",
42
44
  file: "PROJECT-ISSUE-COMMENT.yaml", replacedBy: "PROJECT-COMMON-SUH-ISSUE-HELPER.yaml",
43
- since: "2.x", reason: "이슈 헬퍼 1세대 — 공존 시 이슈 댓글 중복" },
45
+ since: "2.x", reasonKey: "migrate.reason.helperGen1" },
44
46
  { id: "wf-issue-comment-v2", category: "workflow", tier: "safe",
45
47
  file: "PROJECT-COMMON-ISSUE-COMMENT.yaml", replacedBy: "PROJECT-COMMON-SUH-ISSUE-HELPER.yaml",
46
- since: "2.x", reason: "이슈 헬퍼 2세대 — 공존 시 이슈 댓글 중복" },
48
+ since: "2.x", reasonKey: "migrate.reason.helperGen2" },
47
49
  { id: "wf-issue-helper-api", category: "workflow", tier: "safe",
48
50
  file: "PROJECT-COMMON-SUH-ISSUE-HELPER-API.yaml", replacedBy: "PROJECT-COMMON-SUH-ISSUE-HELPER.yaml",
49
- since: "4.3.0", reason: "이슈 헬퍼 API 버전 폐기 (dispatch 전용 비활성 잔재)" },
51
+ since: "4.3.0", reasonKey: "migrate.reason.helperApi" },
50
52
  { id: "wf-issue-helper-module", category: "workflow", tier: "safe",
51
53
  file: "PROJECT-COMMON-SUH-ISSUE-HELPER-MODULE.yml", replacedBy: "PROJECT-COMMON-SUH-ISSUE-HELPER.yaml",
52
- since: "4.3.0", reason: "외부 액션 내재화(#478) — 공존 시 이슈 댓글 중복",
54
+ since: "4.3.0", reasonKey: "migrate.reason.helperModule",
53
55
  settingsExtractor: "suh-issue-helper-module" }, // 무해화 전 with: 커스텀 값을 version.yml로 이관
54
56
  { id: "wf-auto-changelog-v1", category: "workflow", tier: "safe",
55
57
  file: "PROJECT-AUTO-CHANGELOG-CONTROL.yaml", replacedBy: "PROJECT-COMMON-RELEASE-CHANGELOG.yaml",
56
- since: "2.x", reason: "릴리스 워크플로우 1세대 — 공존 시 릴리스 PR 이중 처리" },
58
+ since: "2.x", reasonKey: "migrate.reason.releaseGen1" },
57
59
  { id: "wf-auto-changelog-v2", category: "workflow", tier: "safe",
58
60
  file: "PROJECT-COMMON-AUTO-CHANGELOG-CONTROL.yaml", replacedBy: "PROJECT-COMMON-RELEASE-CHANGELOG.yaml",
59
- since: "4.3.0", reason: "RELEASE-CHANGELOG로 리네임 — 공존 시 릴리스 PR 이중 처리" },
61
+ since: "4.3.0", reasonKey: "migrate.reason.releaseRename" },
60
62
  { id: "wf-comqa-bot", category: "workflow", tier: "safe",
61
63
  file: "COMQA-ISSUE-CREATION-BOT.yaml", replacedBy: "PROJECT-COMMON-QA-ISSUE-CREATION-BOT.yaml",
62
- since: "2.x", reason: "QA 봇 구명칭 — 공존 시 QA 이슈 중복 생성" },
64
+ since: "2.x", reasonKey: "migrate.reason.qaBot" },
63
65
  { id: "wf-template-util-sync", category: "workflow", tier: "safe",
64
66
  file: "TEMPLATE-UTIL-VERSION-SYNC.yml", replacedBy: "PROJECT-COMMON-TEMPLATE-UTIL-VERSION-SYNC.yml",
65
- since: "2.x", reason: "구명칭 — 공존 시 util 버전 동기화 중복" },
67
+ since: "2.x", reasonKey: "migrate.reason.utilSync" },
66
68
  { id: "wf-backlog-manager", category: "workflow", tier: "safe",
67
69
  file: "PROJECT-COMMON-PROJECT-BACKLOG-MANAGER.yaml", replacedBy: "PROJECT-COMMON-PROJECTS-SYNC-MANAGER.yaml",
68
- since: "3.x", reason: "Projects 동기화로 대체 — 공존 시 보드 상태 중복 갱신" },
70
+ since: "3.x", reasonKey: "migrate.reason.projectsSync" },
69
71
  { id: "wf-suh-lab-build-trigger", category: "workflow", tier: "safe",
70
72
  file: "PROJECT-FLUTTER-SUH-LAB-APP-BUILD-TRIGGER.yaml", replacedBy: "PROJECT-FLUTTER-PROJECTOPS-APP-BUILD-TRIGGER.yaml",
71
- since: "4.3.0", reason: "리브랜딩 리네임 — 공존 시 빌드 트리거 중복" },
73
+ since: "4.3.0", reasonKey: "migrate.reason.buildTrigger" },
72
74
  { id: "wf-next-ci", category: "workflow", tier: "safe",
73
75
  file: "PROJECT-NEXT-CI.yaml", replacedBy: "PROJECT-REACT-CI.yaml",
74
- since: "4.1.0", reason: "next 타입 폐지 (react 흡수)" },
76
+ since: "4.1.0", reasonKey: "migrate.reason.nextType" },
75
77
  { id: "wf-next-cicd", category: "workflow", tier: "safe",
76
78
  file: "PROJECT-NEXT-CICD.yaml", replacedBy: "PROJECT-REACT-CICD.yaml",
77
- since: "4.1.0", reason: "next 타입 폐지 (react 흡수)" },
79
+ since: "4.1.0", reasonKey: "migrate.reason.nextType" },
78
80
  { id: "wf-flutter-ci-yml-ext", category: "workflow", tier: "safe",
79
81
  file: "PROJECT-FLUTTER-CI.yml", replacedBy: "PROJECT-FLUTTER-CI.yaml",
80
- since: "2.x", reason: "구 확장자(.yml) — 현행 .yaml과 공존 시 CI 중복 실행" },
82
+ since: "2.x", reasonKey: "migrate.reason.ymlExt" },
81
83
  { id: "wf-flutter-android-pr-ci", category: "workflow", tier: "safe",
82
84
  file: "PROJECT-FLUTTER-ANDROID-PR-CI.yaml", replacedBy: "PROJECT-FLUTTER-CI.yaml",
83
- since: "3.x", reason: "통합 CI로 흡수 — 공존 시 PR CI 중복 실행" },
85
+ since: "3.x", reasonKey: "migrate.reason.mergedCi" },
84
86
 
85
87
  // ── workflow / confirm — 배포/업로드 계열 (현역 배포일 수 있어 자동 조치 금지) ──
86
88
  { id: "wf-syn-spring-simple", category: "workflow", tier: "confirm",
87
89
  file: "PROJECT-SPRING-SYNOLOGY-SIMPLE-CICD.yaml", replacedBy: "PROJECT-SPRING-SIMPLE-CICD.yaml",
88
- since: "3.0.137", reason: "SYNOLOGY 폐기 — SSH+Docker 엔진으로 대체됨" },
90
+ since: "3.0.137", reasonKey: "migrate.reason.synSsh" },
89
91
  { id: "wf-syn-spring-nonstop", category: "workflow", tier: "confirm",
90
92
  file: "PROJECT-SPRING-SYNOLOGY-NONSTOP-CICD.yaml", replacedBy: "PROJECT-SPRING-NONSTOP-NGINX-CICD.yaml",
91
- since: "3.0.137", reason: "SYNOLOGY 폐기 — 무중단 배포는 NGINX/TRAEFIK 버전으로 대체됨" },
93
+ since: "3.0.137", reasonKey: "migrate.reason.synNonstop" },
92
94
  { id: "wf-syn-spring-preview", category: "workflow", tier: "confirm",
93
95
  file: "PROJECT-SPRING-SYNOLOGY-PR-PREVIEW.yaml", replacedBy: "PROJECT-SPRING-PR-PREVIEW.yaml",
94
- since: "3.0.137", reason: "SYNOLOGY 폐기" },
96
+ since: "3.0.137", reasonKey: "migrate.reason.syn" },
95
97
  { id: "wf-syn-flutter-android", category: "workflow", tier: "confirm",
96
98
  file: "PROJECT-FLUTTER-ANDROID-SYNOLOGY-CICD.yaml", replacedBy: "PROJECT-FLUTTER-ANDROID-SELFHOSTED-CICD.yaml",
97
- since: "3.0.137", reason: "SYNOLOGY 폐기 — SELFHOSTED(SMB)로 대체됨" },
99
+ since: "3.0.137", reasonKey: "migrate.reason.synSmb" },
98
100
  { id: "wf-syn-python-cicd", category: "workflow", tier: "confirm",
99
101
  file: "PROJECT-PYTHON-SYNOLOGY-CICD.yaml", replacedBy: "PROJECT-PYTHON-SIMPLE-CICD.yaml",
100
- since: "3.0.137", reason: "SYNOLOGY 폐기" },
102
+ since: "3.0.137", reasonKey: "migrate.reason.syn" },
101
103
  { id: "wf-syn-python-preview", category: "workflow", tier: "confirm",
102
104
  file: "PROJECT-PYTHON-SYNOLOGY-PR-PREVIEW.yaml", replacedBy: "PROJECT-PYTHON-PR-PREVIEW.yaml",
103
- since: "3.0.137", reason: "SYNOLOGY 폐기" },
105
+ since: "3.0.137", reasonKey: "migrate.reason.syn" },
104
106
  { id: "wf-syn-secret-upload", category: "workflow", tier: "confirm",
105
107
  file: "PROJECT-COMMON-SYNOLOGY-SECRET-FILE-UPLOAD.yaml", replacedBy: "PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml",
106
- since: "3.0.137", reason: "SYNOLOGY 폐기 — 공통 Secret 업로드로 대체됨" },
108
+ since: "3.0.137", reasonKey: "migrate.reason.synSecret" },
109
+ // #661 — 템플릿에서 사라졌는데 레거시로 잡히지 않던 릴리스 워크플로우. 이 파일의 trunk-based 분기는
110
+ // 지금은 없는 `changelog_manager.py ai-summary` 를 불러 릴리스 때 실패한다. 그 레포의 유일한
111
+ // 릴리스 경로일 수 있어 자동으로 건드리지 않고 안내만 한다. 같은 파일명을 쓰는 사용자 파일을
112
+ // 오탐하지 않도록 내용 마커로 한 번 더 확인한다.
113
+ { id: "wf-release-publish-v1", category: "workflow", tier: "confirm",
114
+ file: "PROJECT-COMMON-RELEASE-PUBLISH.yaml", replacedBy: "PROJECT-COMMON-RELEASE-CHANGELOG.yaml",
115
+ since: "2.x", reasonKey: "migrate.reason.releasePublish1", contentMarker: "ai-summary" },
107
116
  { id: "wf-spring-ci-v1", category: "workflow", tier: "confirm",
108
117
  file: "PROJECT-SPRING-CI.yaml", replacedBy: null,
109
- since: "2.x", reason: "1세대 Spring CI — 현행 대체본 없음(CICD에 통합)" },
118
+ since: "2.x", reasonKey: "migrate.reason.springCi1" },
110
119
  { id: "wf-spring-cicd-v1", category: "workflow", tier: "confirm",
111
120
  file: "PROJECT-SPRING-CICD.yaml", replacedBy: "PROJECT-SPRING-SIMPLE-CICD.yaml",
112
- since: "2.x", reason: "1세대 Spring 배포 — 현역 배포일 수 있음" },
121
+ since: "2.x", reasonKey: "migrate.reason.springDeploy1" },
113
122
  { id: "wf-python-cicd-v1", category: "workflow", tier: "confirm",
114
123
  file: "PROJECT-PYTHON-CICD.yaml", replacedBy: "PROJECT-PYTHON-SIMPLE-CICD.yaml",
115
- since: "3.x", reason: "구 Python 배포 — 현역 배포일 수 있음" },
124
+ since: "3.x", reasonKey: "migrate.reason.pythonDeploy1" },
116
125
  { id: "wf-android-cicd-v1", category: "workflow", tier: "confirm",
117
126
  file: "PROJECT-ANDROID-CICD.yaml", replacedBy: "PROJECT-FLUTTER-ANDROID-PLAYSTORE-CICD.yaml",
118
- since: "2.x", reason: "1세대 Android 배포 — 현역 배포일 수 있음" },
127
+ since: "2.x", reasonKey: "migrate.reason.androidDeploy1" },
119
128
  { id: "wf-ios-testflight-v1", category: "workflow", tier: "confirm",
120
129
  file: "PROJECT-IOS-TESTFLIGHT-CICD.yml", replacedBy: "PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml",
121
- since: "2.x", reason: "1세대 iOS 배포 — 현역 배포일 수 있음" },
130
+ since: "2.x", reasonKey: "migrate.reason.iosDeploy1" },
122
131
  { id: "wf-flutter-ios-cicd", category: "workflow", tier: "confirm",
123
132
  file: "PROJECT-FLUTTER-IOS-CICD.yaml", replacedBy: "PROJECT-FLUTTER-IOS-TESTFLIGHT.yaml",
124
- since: "3.x", reason: "TESTFLIGHT 워크플로우로 대체 — 현역 배포일 수 있음" },
133
+ since: "3.x", reasonKey: "migrate.reason.iosTestflight" },
125
134
  { id: "wf-file-upload-spring", category: "workflow", tier: "confirm",
126
135
  file: "PROJECT-SPRING-AUTO-FILE-UPLOAD.yaml", replacedBy: "PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml",
127
- since: "3.x", reason: "공통 Secret 업로드로 대체 — 운영 업로드 경로일 수 있음" },
136
+ since: "3.x", reasonKey: "migrate.reason.secretUpload" },
128
137
  { id: "wf-file-upload-flutter", category: "workflow", tier: "confirm",
129
138
  file: "PROJECT-FLUTTER-AUTO-FILE-UPLOAD.yaml", replacedBy: "PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml",
130
- since: "3.x", reason: "공통 Secret 업로드로 대체 — 운영 업로드 경로일 수 있음" },
139
+ since: "3.x", reasonKey: "migrate.reason.secretUpload" },
131
140
  { id: "wf-file-upload-python", category: "workflow", tier: "confirm",
132
141
  file: "PROJECT-PYTHON-AUTO-FILE-UPLOAD.yaml", replacedBy: "PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml",
133
- since: "3.x", reason: "공통 Secret 업로드로 대체 — 운영 업로드 경로일 수 있음" },
142
+ since: "3.x", reasonKey: "migrate.reason.secretUpload" },
134
143
  { id: "wf-file-upload-v1", category: "workflow", tier: "confirm",
135
144
  file: "PROJECT-FILE-AUTO-UPLOAD.yaml", replacedBy: "PROJECT-COMMON-SECRET-FILE-UPLOAD.yaml",
136
- since: "2.x", reason: "공통 Secret 업로드로 대체 — 운영 업로드 경로일 수 있음" },
145
+ since: "2.x", reasonKey: "migrate.reason.secretUpload" },
137
146
  { id: "wf-nexus-publish-v1", category: "workflow", tier: "confirm",
138
147
  file: "PROJECT-NEXUS-PUBLISH.yml", replacedBy: "PROJECT-SPRING-NEXUS-PUBLISH.yml",
139
- since: "2.x", reason: "구 Nexus 배포 — 현역 라이브러리 배포일 수 있음" },
148
+ since: "2.x", reasonKey: "migrate.reason.nexusPublish" },
140
149
  { id: "wf-nexus-ci-v1", category: "workflow", tier: "confirm",
141
150
  file: "PROJECT-NEXUS-MODULE-CI-BUILD-CHECK.yml", replacedBy: "PROJECT-SPRING-NEXUS-CI.yml",
142
- since: "2.x", reason: "구 Nexus CI" },
151
+ since: "2.x", reasonKey: "migrate.reason.nexusCi" },
143
152
  { id: "wf-ci-build-check-v0", category: "workflow", tier: "confirm",
144
153
  file: "PROJECT-CI-BUILD-CHECK.yml", replacedBy: "PROJECT-SPRING-NEXUS-CI.yml",
145
- since: "2.x", reason: "0세대 라이브러리 CI" },
154
+ since: "2.x", reasonKey: "migrate.reason.libCi0" },
146
155
  { id: "wf-publish-v0", category: "workflow", tier: "confirm",
147
156
  file: "PROJECT-PUBLISH.yml", replacedBy: "PROJECT-SPRING-NEXUS-PUBLISH.yml",
148
- since: "2.x", reason: "0세대 라이브러리 배포 — 현역 배포일 수 있음" },
157
+ since: "2.x", reasonKey: "migrate.reason.libPublish0" },
149
158
  { id: "wf-deploy-trigger", category: "workflow", tier: "confirm",
150
159
  file: "PROJECT-DEPLOY-TRIGGER.yaml", replacedBy: null,
151
- since: "3.x", reason: "폐기된 배포 트리거" },
160
+ since: "3.x", reasonKey: "migrate.reason.deployTrigger" },
152
161
 
153
162
  // ── root-file / safe — 구 설치 가이드 (사용자가 매번 수동 삭제하던 것) ─────────
154
163
  { id: "root-setup-guide-v2", category: "root-file", tier: "safe",
155
164
  file: "SUH-DEVOPS-TEMPLATE-SETUP-GUIDE.md", replacedBy: "PROJECTOPS-SETUP-GUIDE.md",
156
- since: "4.3.0", reason: "리브랜딩으로 구 가이드 대체 — 잔재 문서" },
165
+ since: "4.3.0", reasonKey: "migrate.reason.guideRebrand" },
157
166
  { id: "root-setup-guide-v1", category: "root-file", tier: "safe",
158
167
  file: "SETUP-GUIDE.md", replacedBy: "PROJECTOPS-SETUP-GUIDE.md",
159
- since: "2.x", reason: "구 가이드 문서 — 잔재",
168
+ since: "2.x", reasonKey: "migrate.reason.guideOld",
160
169
  contentMarker: "SUH" }, // 범용 파일명이라 내용에 템플릿 마커가 있을 때만 소유 판정
161
170
 
162
171
  // ── legacy-dir / ask — 구명칭 산출물 폴더 (사용자 문서 보존 이동, #476) ────────
163
172
  { id: "dir-docs-suh-template", category: "legacy-dir", tier: "ask",
164
173
  file: "docs/suh-template", replacedBy: "docs/projectops",
165
- since: "4.2.9", reason: "리브랜딩 — 스킬 산출물(이슈·보고서) 폴더 구명칭. 신 스킬은 docs/projectops/에 저장" },
174
+ since: "4.2.9", reasonKey: "migrate.reason.docsDir" },
166
175
 
167
176
  // ── util-file / safe — Flutter 마법사 스크립트 Python 단일화 (#500) ──────────
168
177
  // 로컬 실행 도구라 CI 중복 실행 위험은 없지만, 신형 py와 공존 시 사용자가 구 스크립트를
169
178
  // 실행해 갈라진 동작을 볼 수 있다. 마법사 폴더에 사용자 상태가 없어 삭제가 기대 동작.
170
179
  { id: "util-playstore-setup-sh", category: "util-file", tier: "safe",
171
180
  file: ".github/util/flutter/playstore-wizard/playstore-wizard-setup.sh", replacedBy: "playstore-wizard.py",
172
- since: "4.3.0", reason: "마법사 스크립트 Python 단일화 — setup 서브커맨드로 대체" },
181
+ since: "4.3.0", reasonKey: "migrate.reason.wizSetup" },
173
182
  { id: "util-playstore-setup-ps1", category: "util-file", tier: "safe",
174
183
  file: ".github/util/flutter/playstore-wizard/playstore-wizard-setup.ps1", replacedBy: "playstore-wizard.py",
175
- since: "4.3.0", reason: "마법사 스크립트 Python 단일화 — setup 서브커맨드로 대체" },
184
+ since: "4.3.0", reasonKey: "migrate.reason.wizSetup" },
176
185
  { id: "util-playstore-apply-sh", category: "util-file", tier: "safe",
177
186
  file: ".github/util/flutter/playstore-wizard/playstore-wizard-apply.sh", replacedBy: "playstore-wizard.py",
178
- since: "4.3.0", reason: "마법사 스크립트 Python 단일화 — apply 서브커맨드로 대체" },
187
+ since: "4.3.0", reasonKey: "migrate.reason.wizApply" },
179
188
  { id: "util-playstore-apply-ps1", category: "util-file", tier: "safe",
180
189
  file: ".github/util/flutter/playstore-wizard/playstore-wizard-apply.ps1", replacedBy: "playstore-wizard.py",
181
- since: "4.3.0", reason: "마법사 스크립트 Python 단일화 — apply 서브커맨드로 대체" },
190
+ since: "4.3.0", reasonKey: "migrate.reason.wizApply" },
182
191
  { id: "util-playstore-detect-sh", category: "util-file", tier: "safe",
183
192
  file: ".github/util/flutter/playstore-wizard/detect-application-id.sh", replacedBy: "playstore-wizard.py",
184
- since: "4.3.0", reason: "마법사 스크립트 Python 단일화 — detect-app-id 서브커맨드로 대체" },
193
+ since: "4.3.0", reasonKey: "migrate.reason.wizDetect" },
185
194
  { id: "util-playstore-detect-ps1", category: "util-file", tier: "safe",
186
195
  file: ".github/util/flutter/playstore-wizard/detect-application-id.ps1", replacedBy: "playstore-wizard.py",
187
- since: "4.3.0", reason: "마법사 스크립트 Python 단일화 — detect-app-id 서브커맨드로 대체" },
196
+ since: "4.3.0", reasonKey: "migrate.reason.wizDetect" },
188
197
  { id: "util-playstore-patch-py", category: "util-file", tier: "safe",
189
198
  file: ".github/util/flutter/playstore-wizard/patch-build-gradle.py", replacedBy: "playstore-wizard.py",
190
- since: "4.3.0", reason: "마법사 스크립트 Python 단일화 — 내부 함수로 흡수" },
199
+ since: "4.3.0", reasonKey: "migrate.reason.wizPatch" },
191
200
  { id: "util-firebase-setup-sh", category: "util-file", tier: "safe",
192
201
  file: ".github/util/flutter/firebase-wizard/firebase-wizard-setup.sh", replacedBy: "firebase-wizard.py",
193
- since: "4.3.0", reason: "마법사 스크립트 Python 단일화 — setup 서브커맨드로 대체" },
202
+ since: "4.3.0", reasonKey: "migrate.reason.wizSetup" },
194
203
  { id: "util-firebase-setup-ps1", category: "util-file", tier: "safe",
195
204
  file: ".github/util/flutter/firebase-wizard/firebase-wizard-setup.ps1", replacedBy: "firebase-wizard.py",
196
- since: "4.3.0", reason: "마법사 스크립트 Python 단일화 — setup 서브커맨드로 대체" },
205
+ since: "4.3.0", reasonKey: "migrate.reason.wizSetup" },
197
206
  { id: "util-firebase-test-sh", category: "util-file", tier: "safe",
198
207
  file: ".github/util/flutter/firebase-wizard/test/setup-script-test.sh", replacedBy: "test/setup-script-test.py",
199
- since: "4.3.0", reason: "마법사 테스트 Python 단일화" },
208
+ since: "4.3.0", reasonKey: "migrate.reason.wizTest" },
200
209
  { id: "util-firebase-test-ps1", category: "util-file", tier: "safe",
201
210
  file: ".github/util/flutter/firebase-wizard/test/setup-script-test.ps1", replacedBy: "test/setup-script-test.py",
202
- since: "4.3.0", reason: "마법사 테스트 Python 단일화" },
211
+ since: "4.3.0", reasonKey: "migrate.reason.wizTest" },
203
212
  { id: "util-testflight-setup-sh", category: "util-file", tier: "safe",
204
213
  file: ".github/util/flutter/testflight-wizard/testflight-wizard-setup.sh", replacedBy: "testflight-wizard.py",
205
- since: "4.3.0", reason: "마법사 스크립트 Python 단일화 — setup 서브커맨드로 대체" },
214
+ since: "4.3.0", reasonKey: "migrate.reason.wizSetup" },
206
215
  ];
216
+
217
+ // 사유는 영문 정본(tEn)으로 노출해 실행 로그·가이드에 그대로 쓰고,
218
+ // 화면 표시는 reasonKey를 t()로 번역해 쓴다.
219
+ for (const m of MIGRATIONS) {
220
+ Object.defineProperty(m, "reason", { enumerable: true, get: () => tEn(m.reasonKey) });
221
+ }