@spectrabrain/eds 0.3.0 → 0.3.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.
@@ -1,22 +1,81 @@
1
- import { conventionsFileName, edsLucideVersion, installRanges, mockDevDependencies, templateDevDependencies } from "./align-rules.mjs";
1
+ import {
2
+ conventionsFileName,
3
+ edsLucideVersion,
4
+ installRanges,
5
+ maximumSupportedViteMajor,
6
+ minimumPluginReactVersion,
7
+ minimumTailwindViteVersion,
8
+ minimumVersions,
9
+ rolldownBabelPackage,
10
+ stateFileName,
11
+ templateDevDependencies,
12
+ } from "./align-rules.mjs";
2
13
  import { cliCommand, runnerCommand } from "./paths.mjs";
3
14
 
4
15
  const installPrefixes = {
5
- npm: { add: "npm install", dev: "npm install -D", remove: "npm uninstall", dlx: "npx", exec: "npx" },
6
- pnpm: { add: "pnpm add", dev: "pnpm add -D", remove: "pnpm remove", dlx: "pnpm dlx", exec: "pnpm exec" },
7
- yarn: { add: "yarn add", dev: "yarn add -D", remove: "yarn remove", dlx: "yarn dlx", exec: "yarn" },
8
- bun: { add: "bun add", dev: "bun add -d", remove: "bun remove", dlx: "bunx", exec: "bunx" },
16
+ npm: {
17
+ add: "npm install",
18
+ dev: "npm install -D",
19
+ remove: "npm uninstall",
20
+ dlx: "npx",
21
+ },
22
+ pnpm: {
23
+ add: "pnpm add",
24
+ dev: "pnpm add -D",
25
+ remove: "pnpm remove",
26
+ dlx: "pnpm dlx",
27
+ },
28
+ yarn: {
29
+ add: "yarn add",
30
+ dev: "yarn add -D",
31
+ remove: "yarn remove",
32
+ dlx: "yarn dlx",
33
+ },
34
+ bun: { add: "bun add", dev: "bun add -d", remove: "bun remove", dlx: "bunx" },
9
35
  };
10
36
 
11
- // @vitejs/plugin-react 6 requires Vite 8; 4 still covers Vite 4–7 on Node 18.
37
+ // The Babel template uses a plugin-react release whose peer range covers the selected Vite major.
12
38
  export function pluginReactRange(viteMajor) {
13
- return viteMajor !== null && viteMajor >= 8 ? "^6" : "^4";
39
+ if (viteMajor !== null && viteMajor > maximumSupportedViteMajor) {
40
+ throw new RangeError(
41
+ "eds align supports Vite 5.2.0–" + maximumSupportedViteMajor + ".x only."
42
+ );
43
+ }
44
+ return viteMajor === 8
45
+ ? "^" + minimumVersions.pluginReactForVite8
46
+ : "^" + minimumPluginReactVersion(viteMajor);
14
47
  }
15
48
 
16
- export function packageManagerCommands(packageManager, { viteMajor = null } = {}) {
49
+ export function packageManagerCommands(
50
+ packageManager,
51
+ { viteMajor = null } = {}
52
+ ) {
17
53
  const prefixes = installPrefixes[packageManager?.name] ?? installPrefixes.npm;
18
- const dlx = packageManager?.name === "yarn" && !packageManager.berry ? "npx" : prefixes.dlx;
19
- const ranges = { ...installRanges, "@vitejs/plugin-react": pluginReactRange(viteMajor), ...(edsLucideVersion ? { "lucide-react": edsLucideVersion } : {}) };
54
+ const dlx =
55
+ packageManager?.name === "yarn" && !packageManager.berry
56
+ ? "npx"
57
+ : prefixes.dlx;
58
+ const exec =
59
+ packageManager?.name === "pnpm"
60
+ ? "pnpm exec"
61
+ : packageManager?.name === "yarn"
62
+ ? "yarn"
63
+ : packageManager?.name === "bun"
64
+ ? "bunx"
65
+ : "npx";
66
+ const ranges = {
67
+ ...installRanges,
68
+ vite:
69
+ viteMajor === 8
70
+ ? "^8"
71
+ : installRanges.vite,
72
+ "@vitejs/plugin-react": pluginReactRange(viteMajor),
73
+ "@tailwindcss/vite":
74
+ minimumTailwindViteVersion(viteMajor) !== null
75
+ ? "^" + minimumTailwindViteVersion(viteMajor)
76
+ : installRanges["@tailwindcss/vite"],
77
+ ...(edsLucideVersion ? { "lucide-react": edsLucideVersion } : {}),
78
+ };
20
79
  const withRange = (name) => (ranges[name] ? name + "@" + ranges[name] : name);
21
80
  return {
22
81
  run: (script) => runnerCommand(packageManager, script),
@@ -25,12 +84,25 @@ export function packageManagerCommands(packageManager, { viteMajor = null } = {}
25
84
  remove: (names) => prefixes.remove + " " + names.join(" "),
26
85
  eds: (args) => cliCommand(packageManager, args),
27
86
  dlx: (command) => dlx + " " + command,
28
- // A binary of an installed package (for example msw init).
29
- exec: (command) => prefixes.exec + " " + command,
87
+ exec: (command) => exec + " " + command,
30
88
  };
31
89
  }
32
90
 
33
- const kindOrder = ["protected", "outside", "backend-script", "backend-dependency", "structure", "language", "import", "dependency", "config", "script", "pattern", "agents"];
91
+ const kindOrder = [
92
+ "protected",
93
+ "preserved",
94
+ "outside",
95
+ "backend-script",
96
+ "backend-dependency",
97
+ "structure",
98
+ "language",
99
+ "import",
100
+ "dependency",
101
+ "config",
102
+ "script",
103
+ "pattern",
104
+ "agents",
105
+ ];
34
106
 
35
107
  export function countFindings(findings) {
36
108
  const errors = new Map();
@@ -39,9 +111,19 @@ export function countFindings(findings) {
39
111
  const counts = finding.level === "error" ? errors : warnings;
40
112
  counts.set(finding.kind, (counts.get(finding.kind) ?? 0) + 1);
41
113
  }
42
- const format = (counts) => kindOrder.filter((kind) => counts.has(kind)).map((kind) => kind + " " + counts.get(kind)).join(", ");
43
- const total = (counts) => [...counts.values()].reduce((sum, count) => sum + count, 0);
44
- return { errors: total(errors), warnings: total(warnings), errorSummary: format(errors), warningSummary: format(warnings) };
114
+ const format = (counts) =>
115
+ kindOrder
116
+ .filter((kind) => counts.has(kind))
117
+ .map((kind) => kind + " " + counts.get(kind))
118
+ .join(", ");
119
+ const total = (counts) =>
120
+ [...counts.values()].reduce((sum, count) => sum + count, 0);
121
+ return {
122
+ errors: total(errors),
123
+ warnings: total(warnings),
124
+ errorSummary: format(errors),
125
+ warningSummary: format(warnings),
126
+ };
45
127
  }
46
128
 
47
129
  function code(text) {
@@ -76,7 +158,9 @@ export function renderNotes() {
76
158
  "## 진행",
77
159
  "",
78
160
  "- commit: (1단계에서 `허용` 또는 `안 함`으로 적습니다)",
79
- "- 완료한 단계: 0 / " + stepCount + " (단계의 완료 기준을 모두 만족한 뒤에만 번호를 올립니다)",
161
+ "- 완료한 단계: 0 / " +
162
+ stepCount +
163
+ " (단계의 완료 기준을 모두 만족한 뒤에만 번호를 올립니다)",
80
164
  "",
81
165
  "## 기준선",
82
166
  "",
@@ -84,6 +168,8 @@ export function renderNotes() {
84
168
  "",
85
169
  "## 보호 경로 확인",
86
170
  "",
171
+ "## 기존 리소스 확인",
172
+ "",
87
173
  "## 단계별 기록",
88
174
  "",
89
175
  "## 보류 사항",
@@ -96,105 +182,283 @@ function numbered(items) {
96
182
  }
97
183
 
98
184
  // PLAN.md: the facts of this repository and the ordered steps. The rules themselves live in FRONTEND_CONVENTIONS.md.
99
- export function renderPlan({ version, createdAt, base, packageManager, frontend, analysis, templates, alignLabel, conventionsPath = null }) {
185
+ export function renderPlan({
186
+ version,
187
+ createdAt,
188
+ base,
189
+ packageManager,
190
+ frontend,
191
+ analysis,
192
+ templates,
193
+ alignLabel,
194
+ conventionsPath = null,
195
+ }) {
100
196
  const { app, dependencies, protectedInfo, majors } = analysis;
101
- const commands = packageManagerCommands(packageManager, { viteMajor: majors.vite });
197
+ const commands = packageManagerCommands(packageManager, {
198
+ viteMajor: majors.vite,
199
+ });
102
200
  const alignCheck = commands.eds("align --check");
103
201
  const counts = countFindings(analysis.findings);
104
- const packageLabel = frontend.packageRelative === "" ? "repository root" : code(frontend.packageRelative);
105
- const conventions = conventionsPath === null ? "EDS package root의 " + code(conventionsFileName) : code(conventionsPath);
106
- const findingsOf = (kinds) => analysis.findings.filter((finding) => kinds.includes(finding.kind));
202
+ const packageLabel =
203
+ frontend.packageRelative === ""
204
+ ? "repository root"
205
+ : code(frontend.packageRelative);
206
+ const conventions =
207
+ conventionsPath === null
208
+ ? "EDS package root의 " + code(conventionsFileName)
209
+ : code(conventionsPath);
210
+ const findingsOf = (kinds) =>
211
+ analysis.findings.filter((finding) => kinds.includes(finding.kind));
107
212
  const scripts = analysis.packageJson?.scripts ?? {};
108
213
 
109
214
  const protectedRows = protectedInfo.inside.map((entry) => [
110
215
  code(entry),
111
- entry === "shared" ? "backend와 프론트엔드가 함께 쓰는 코드(import 허용, 수정 금지)" : "backend·배포 코드 또는 설정",
216
+ entry === "shared"
217
+ ? "backend와 프론트엔드가 함께 쓰는 코드(import 허용, 수정 금지)"
218
+ : "backend·배포 코드 또는 설정",
112
219
  ]);
113
220
  if (frontend.packageRelative !== "") {
114
- protectedRows.push(["프론트엔드 package 밖 전체", "다른 package와 backend" + (protectedInfo.repositoryOnly.length > 0 ? "(" + protectedInfo.repositoryOnly.map(code).join(", ") + " 포함)" : "")]);
221
+ protectedRows.push([
222
+ "프론트엔드 package 밖 전체(저장소 루트 lockfile 제외)",
223
+ "다른 package와 backend" +
224
+ (protectedInfo.repositoryOnly.length > 0
225
+ ? "(" + protectedInfo.repositoryOnly.map(code).join(", ") + " 포함)"
226
+ : ""),
227
+ ]);
115
228
  }
116
- const backendScriptEntries = Object.entries(protectedInfo.backendScripts ?? {});
117
- const backendScripts = backendScriptEntries.map(([name, command]) => code(name) + ": " + code(command));
229
+ const backendScriptEntries = Object.entries(
230
+ protectedInfo.backendScripts ?? {}
231
+ );
232
+ const backendScripts = backendScriptEntries.map(
233
+ ([name, command]) => code(name) + ": " + code(command)
234
+ );
118
235
 
119
236
  const structureRows = analysis.structure.map((entry) => [
120
237
  code(entry.name + (entry.type === "directory" ? "/" : "")),
121
238
  String(entry.files),
122
- entry.hint ?? (entry.fsd ? "FSD 위치입니다. 하위 규칙은 check 경고를 따릅니다." : "역할을 보고 app·pages·widgets·features·entities·shared 중 한 곳으로 옮깁니다."),
239
+ entry.hint ??
240
+ (entry.fsd
241
+ ? "FSD 위치입니다. 하위 규칙은 check 경고를 따릅니다."
242
+ : "역할을 보고 app·pages·widgets·features·entities·shared 중 한 곳으로 옮깁니다."),
123
243
  ]);
124
244
 
125
- const replacementRows = dependencies.replacements.filter((entry) => !entry.keptForBackend).map((entry) => [
126
- entry.category,
127
- code(entry.name),
128
- entry.target === "lucide-react" ? entry.label + "(" + code(commands.add(["lucide-react"])) + ")" : entry.label,
245
+ const replacementRows = dependencies.replacements
246
+ .filter((entry) => !entry.keptForBackend)
247
+ .map((entry) => [
248
+ entry.category,
249
+ code(entry.name),
250
+ entry.target === "lucide-react"
251
+ ? entry.label + "(" + code(commands.add(["lucide-react"])) + ")"
252
+ : entry.label,
253
+ ]);
254
+ const keptForBackend = dependencies.replacements
255
+ .filter((entry) => entry.keptForBackend)
256
+ .map((entry) => code(entry.name));
257
+ const replacedCategories = new Set(
258
+ replacementRows.map(([category]) => category)
259
+ );
260
+ const viteCompilerPeerPackages = new Set([
261
+ rolldownBabelPackage,
262
+ "babel-plugin-react-compiler",
263
+ "@babel/core",
129
264
  ]);
130
- const keptForBackend = dependencies.replacements.filter((entry) => entry.keptForBackend).map((entry) => code(entry.name));
131
- const replacedCategories = new Set(replacementRows.map(([category]) => category));
132
- const reactOutdated = dependencies.outdated.find((entry) => entry.name === "react") ?? null;
133
- const laterUpgrades = dependencies.outdated.filter((entry) => entry.name !== "react" && entry.name !== "tailwindcss");
265
+ const reactOutdated =
266
+ dependencies.outdated.find((entry) => entry.name === "react") ?? null;
267
+ const pluginReactOutdated =
268
+ dependencies.outdated.find(
269
+ (entry) => entry.name === "@vitejs/plugin-react"
270
+ ) ?? null;
271
+ const viteOutdated =
272
+ dependencies.outdated.find((entry) => entry.name === "vite") ?? null;
273
+ const tailwindViteOutdated =
274
+ dependencies.outdated.find((entry) => entry.name === "@tailwindcss/vite") ??
275
+ null;
276
+ const viteMissing = dependencies.missing.some((entry) => entry.name === "vite");
277
+ const reactMigrationTargets = analysis.reactMigrationTargets ?? [];
278
+ const laterUpgrades = dependencies.outdated.filter(
279
+ (entry) =>
280
+ entry.name !== "react" &&
281
+ entry.name !== "tailwindcss" &&
282
+ entry.name !== "@vitejs/plugin-react" &&
283
+ entry.name !== "vite" &&
284
+ entry.name !== "@tailwindcss/vite" &&
285
+ !viteCompilerPeerPackages.has(entry.name)
286
+ );
134
287
  const upgrades = dependencies.outdated.map((entry) => {
135
- const step = entry.name === "react" || entry.name === "tailwindcss" ? "2단계" : "4단계";
136
- return "- " + code(entry.name) + " " + entry.major + " → " + entry.minimum + " (" + step + ")";
288
+ const step =
289
+ viteCompilerPeerPackages.has(entry.name)
290
+ ? "3단계"
291
+ : entry.name === "react" ||
292
+ entry.name === "tailwindcss" ||
293
+ entry.name === "@vitejs/plugin-react" ||
294
+ entry.name === "vite" ||
295
+ entry.name === "@tailwindcss/vite"
296
+ ? "2단계"
297
+ : "4단계";
298
+ return (
299
+ "- " +
300
+ code(entry.name) +
301
+ " " +
302
+ (entry.version ?? entry.major) +
303
+ " → " +
304
+ (entry.allowedRange ?? entry.minimum) +
305
+ " (" +
306
+ step +
307
+ ")"
308
+ );
137
309
  });
138
- const missingRuntime = dependencies.missing.filter((entry) => !entry.dev && entry.name !== "@spectrabrain/eds").map((entry) => entry.name);
310
+ const missingRuntime = dependencies.missing
311
+ .filter((entry) => !entry.dev && entry.name !== "@spectrabrain/eds")
312
+ .map((entry) => entry.name);
139
313
  // Step 2 already installs Tailwind and, when React is upgraded, the React 19 types.
140
- const installedInStep2 = new Set(["tailwindcss", "@tailwindcss/vite", ...(dependencies.outdated.some((entry) => entry.name === "react") ? ["@types/react", "@types/react-dom"] : [])]);
314
+ const installedInStep2 = new Set([
315
+ "vite",
316
+ "tailwindcss",
317
+ "@tailwindcss/vite",
318
+ ...(dependencies.outdated.some((entry) => entry.name === "react")
319
+ ? ["@types/react", "@types/react-dom"]
320
+ : []),
321
+ ]);
141
322
  const missingDev = [
142
- ...dependencies.missing.filter((entry) => entry.dev && !installedInStep2.has(entry.name)).map((entry) => entry.name),
143
- ...templateDevDependencies.filter((name) => !analysis.declared.has(name) && !(dependencies.sdkOnly && name === "msw")),
323
+ ...dependencies.missing
324
+ .filter((entry) => entry.dev && !installedInStep2.has(entry.name))
325
+ .map((entry) => entry.name),
326
+ ...templateDevDependencies.filter((name) => !analysis.declared.has(name)),
144
327
  ...(dependencies.outdatedTools ?? []).map((entry) => entry.name),
328
+ ...dependencies.outdated
329
+ .filter((entry) => entry.compilerPeer)
330
+ .map((entry) => entry.name),
145
331
  ];
146
332
  const edsDeclared = analysis.declared.has("@spectrabrain/eds");
147
333
  const tailwindMajor = majors.tailwindcss;
148
334
  const viteLabel = majors.vite === null ? "Vite" : "Vite " + majors.vite;
335
+ const tailwindViteMinimum = minimumTailwindViteVersion(majors.vite);
149
336
  const reactLabel = majors.react === null ? "React" : "React " + majors.react;
150
- const configFindings = findingsOf(["config", "script"]).map((finding) => "- " + finding.message);
337
+ const configFindings = findingsOf(["config", "script"]).map(
338
+ (finding) => "- " + finding.message
339
+ );
151
340
  const importedFrontend = protectedInfo.importedFrontendFiles.map(code);
152
- const backendImportsViteConfig = protectedInfo.importedFrontendFiles.some((file) => /^vite\.config\.[cm]?[jt]s$/.test(file));
153
- const backendStartScript = backendScriptEntries.find(([name]) => name === "start")?.[0] ?? backendScriptEntries[0]?.[0] ?? null;
341
+ const backendImportsViteConfig = protectedInfo.importedFrontendFiles.some(
342
+ (file) => /^vite\.config\.[cm]?[jt]s$/.test(file)
343
+ );
344
+ const backendStartScript =
345
+ backendScriptEntries.find(([name]) => name === "start")?.[0] ??
346
+ backendScriptEntries[0]?.[0] ??
347
+ null;
154
348
  const appPrefix = app.appDir === "." ? "" : app.appDir + "/";
155
- const entryLabel = app.entry === null ? appPrefix + "index.html" : appPrefix + app.entry.replace(/^\//, "");
156
- const templatesOf = (relativePath) => code(alignLabel + "/templates/" + relativePath);
349
+ const entryLabel =
350
+ app.entry === null
351
+ ? appPrefix + "index.html"
352
+ : appPrefix + app.entry.replace(/^\//, "");
353
+ const templatesOf = (relativePath) =>
354
+ code(alignLabel + "/templates/" + relativePath);
157
355
  // Config templates are the files outside the source and public trees; package.scripts.json is merged, not copied.
158
- const configTemplates = templates.filter((target) => !(dependencies.sdkOnly && target.endsWith(".env.mock")) &&
159
- target !== "package.scripts.json" && !target.startsWith(app.srcDir + "/") && !target.startsWith(appPrefix + "public/") &&
160
- !target.startsWith("public/")).map(code);
356
+ const configTemplates = templates
357
+ .filter(
358
+ (target) =>
359
+ target !== "package.scripts.json" &&
360
+ !target.startsWith(app.srcDir + "/") &&
361
+ !target.startsWith(appPrefix + "public/") &&
362
+ !target.startsWith("public/")
363
+ )
364
+ .map(code);
161
365
  const srcPath = (relativePath) => app.srcDir + "/" + relativePath;
162
366
  const sdkClients = dependencies.dataClients ?? [];
163
367
  const uiDependents = dependencies.uiDependents ?? [];
164
- const inProgressAgentsLine = "- " + alignLabel + "/PLAN.md가 있으면 작업 전에 PLAN.md와 NOTES.md를 읽고 「진행」의 다음 단계부터 이어갑니다.";
368
+ const preservedDependents = dependencies.preservedDependents ?? [];
369
+ const inProgressAgentsLine =
370
+ "- " +
371
+ alignLabel +
372
+ "/PLAN.md가 있으면 작업 전에 PLAN.md와 NOTES.md를 읽고 「진행」의 다음 단계부터 이어갑니다.";
165
373
 
166
374
  return [
167
375
  "# EDS align 계획",
168
376
  "",
169
- "<!-- @spectrabrain/eds@" + version + " eds align이 생성했습니다. 다시 실행하면 이 파일과 templates/를 새로 씁니다. -->",
170
- "",
171
- "이 문서는 `eds align`이 이 저장소를 분석해 만든 일회성 작업 계획입니다. AI 에이전트는 이 계획의 단계를 순서대로 진행해 프론트엔드를 EVA 프론트엔드 컨벤션에 맞춥니다. 규칙의 원천은 " + conventions + "이며, 이 문서는 이 저장소에 해당하는 사실과 순서만 담습니다.",
172
- "",
173
- "- 기준 commit: " + code(base.slice(0, 12)) + " (" + createdAt.slice(0, 10) + " 생성)",
174
- "- 프론트엔드 package: " + packageLabel + " · " + viteLabel + " · " + reactLabel + " · 패키지 매니저 " + packageManager.name,
175
- "- 작업 디렉터리: " + code(frontend.packageRelative || ".") + "(git root 기준). 이 문서의 모든 명령은 이 디렉터리에서 실행합니다.",
176
- "- 소스 디렉터리: " + code(app.srcDir) + " · Vite root(`index.html`): " + code(app.appDir),
177
- "- 검증: " + code(alignCheck) + "(이하 check)는 기준 commit 이후의 보호 경로 변경과 구조·import·의존성·설정·script를 점검합니다. 1~5단계에서는 각 단계 완료 기준에 적힌 종류의 줄만 사라지면 되고, exit 0은 6단계부터 요구합니다.",
178
- "- 작업 기록: " + code(alignLabel + "/NOTES.md") + "에 진행, 기준선, domain 목록, 결정, 보류 사항을 적습니다.",
179
- "- 템플릿: " + code(alignLabel + "/templates/") + "에 이 저장소의 경로로 채운 설정·소스 파일 " + templates.length + "개가 대상 경로 그대로 있습니다.",
180
- "- 테스트: 테스트 파일·테스트 디렉터리·테스트 도구·설정은 제자리에 둡니다. 소스를 옮겨 깨진 테스트의 import 경로와 `jest.mock`·`vi.mock` 경로 문자열은 고치고, 그 밖의 원인으로 테스트 파일에서 나는 lint·typecheck 오류는 고치지 않고 NOTES.md에 적은 뒤 사용자에게 확인합니다. 옛 디렉터리에 테스트만 남아도 되며 check는 테스트 파일을 점검하지 않습니다.",
377
+ "<!-- @spectrabrain/eds@" +
378
+ version +
379
+ " eds align이 생성했습니다. 다시 실행하면 이 파일과 templates/를 새로 씁니다. -->",
380
+ "",
381
+ "이 문서는 `eds align`이 이 저장소를 분석해 만든 일회성 작업 계획입니다. AI 에이전트는 이 계획의 단계를 순서대로 진행해 프론트엔드를 EVA 프론트엔드 컨벤션에 맞춥니다. 규칙의 원천은 " +
382
+ conventions +
383
+ "이며, 이 문서는 이 저장소에 해당하는 사실과 순서만 담습니다.",
384
+ "",
385
+ "- 기준 commit: " +
386
+ code(base.slice(0, 12)) +
387
+ " (" +
388
+ createdAt.slice(0, 10) +
389
+ " 생성)",
390
+ "- 프론트엔드 package: " +
391
+ packageLabel +
392
+ " · " +
393
+ viteLabel +
394
+ " · " +
395
+ reactLabel +
396
+ " · 패키지 매니저 " +
397
+ packageManager.name,
398
+ "- 작업 디렉터리: " +
399
+ code(frontend.packageRelative || ".") +
400
+ "(git root 기준). 이 문서의 모든 명령은 이 디렉터리에서 실행합니다.",
401
+ "- 소스 디렉터리: " +
402
+ code(app.srcDir) +
403
+ " · Vite root(`index.html`): " +
404
+ code(app.appDir),
405
+ "- 검증: " +
406
+ code(alignCheck) +
407
+ "(이하 check)는 보호 경로, frontend package 밖의 변경(저장소 루트 lockfile 제외), 기준선에 있던 보존 파일의 수정·삭제, tsconfig 파일 삭제와 기준 항목 누락, 구조·import·의존성·설정·script를 점검합니다. 새 리소스 파일 추가는 오류가 아닙니다. 1~5단계에서는 각 단계 완료 기준에 적힌 종류의 줄만 사라지면 되고, exit 0은 6단계부터 요구합니다.",
408
+ "- 작업 기록: " +
409
+ code(alignLabel + "/NOTES.md") +
410
+ "에 진행, 기준선, domain 목록, 결정, 보류 사항을 적습니다.",
411
+ "- 템플릿: " +
412
+ code(alignLabel + "/templates/") +
413
+ "에 이 저장소의 경로로 채운 설정·소스 파일 " +
414
+ templates.length +
415
+ "개가 대상 경로 그대로 있습니다.",
416
+ "- 기존 테스트·mock·다국어 파일과 설정은 원래 위치와 내용 그대로 둡니다. 자동 감지되지 않는 경로는 `state.json`의 `preservedPaths`에 git 저장소 루트 기준으로 추가한 뒤 `eds align`을 다시 실행합니다. 계획 재생성은 기존 기준 commit과 NOTES.md를 유지하고, 새로 보존하는 파일을 기준선에 기록합니다. `check`는 기준선에 있던 보존 파일의 수정·삭제를 `preserved:` 오류로 보고하며 새 파일 추가는 허용합니다.",
417
+ "",
418
+ "## API 계약",
419
+ "",
420
+ "요청 URL·method·header·payload, 응답 해석, 인증 흐름, backend와 배포가 읽는 환경 변수 이름은 그대로 둡니다. check는 frontend service 코드의 요청·응답 의미를 비교하지 않으므로 수동으로 확인합니다.",
181
421
  "",
182
422
  "## 보호 경로",
183
423
  "",
184
424
  "아래 경로는 읽기만 합니다. check가 기준 commit 이후의 변경을 `protected:`·`outside:`로 보고합니다.",
185
425
  "",
186
426
  ...(protectedRows.length === 0
187
- ? ["- 감지된 backend 경로가 없습니다. backend가 다른 저장소에 있거나 이 package가 프론트엔드 전용인지 1단계에서 확인합니다.", ""]
427
+ ? [
428
+ "- 감지된 backend 경로가 없습니다. backend가 다른 저장소에 있거나 이 package가 프론트엔드 전용인지 1단계에서 확인합니다.",
429
+ "",
430
+ ]
188
431
  : table(["경로", "이유"], protectedRows)),
189
- "- API 계약을 유지합니다. 요청 URL·method·header·payload, 응답 해석, 인증 흐름, backend와 배포가 읽는 환경 변수 이름은 그대로 두고, 코드를 옮겨도 같은 요청을 보냅니다.",
190
- ...(backendScripts.length > 0 ? ["- backend를 실행하는 script는 그대로 둡니다: " + backendScripts.join(", ")] : []),
191
- ...(protectedInfo.backendUsed.length > 0 ? ["- backend 코드가 쓰는 의존성은 package.json에서 지우지 않습니다: " + protectedInfo.backendUsed.map(code).join(", ")] : []),
192
- ...(importedFrontend.length > 0 ? ["- backend 코드가 import하는 프론트엔드 파일은 export 형태를 유지합니다: " + importedFrontend.join(", ")] : []),
432
+ ...(backendScripts.length > 0
433
+ ? [
434
+ "- backend를 실행하는 script는 그대로 둡니다: " +
435
+ backendScripts.join(", "),
436
+ ]
437
+ : []),
438
+ ...(protectedInfo.backendUsed.length > 0
439
+ ? [
440
+ "- backend 코드가 쓰는 의존성은 package.json에서 지우지 않습니다: " +
441
+ protectedInfo.backendUsed.map(code).join(", "),
442
+ ]
443
+ : []),
444
+ ...(importedFrontend.length > 0
445
+ ? [
446
+ "- backend 코드가 import하는 프론트엔드 파일은 export 형태를 유지합니다: " +
447
+ importedFrontend.join(", "),
448
+ ]
449
+ : []),
193
450
  "",
194
451
  "## 현재 상태",
195
452
  "",
196
- "check 기준 오류 " + counts.errors + "개" + (counts.errorSummary ? "(" + counts.errorSummary + ")" : "") +
197
- ", 경고 " + counts.warnings + "개" + (counts.warningSummary ? "(" + counts.warningSummary + ")" : "") + "입니다.",
453
+ "check 기준 오류 " +
454
+ counts.errors +
455
+ "개" +
456
+ (counts.errorSummary ? "(" + counts.errorSummary + ")" : "") +
457
+ ", 경고 " +
458
+ counts.warnings +
459
+ "개" +
460
+ (counts.warningSummary ? "(" + counts.warningSummary + ")" : "") +
461
+ "입니다.",
198
462
  "",
199
463
  "### 구조 (" + code(app.srcDir) + ")",
200
464
  "",
@@ -202,16 +466,50 @@ export function renderPlan({ version, createdAt, base, packageManager, frontend,
202
466
  "### 의존성",
203
467
  "",
204
468
  ...(replacementRows.length > 0
205
- ? ["교체할 라이브러리입니다(4단계).", "", ...table(["범주", "패키지", "대체"], replacementRows)]
206
- : keptForBackend.length > 0 ? [] : ["교체할 라이브러리가 없습니다.", ""]),
469
+ ? [
470
+ "교체할 라이브러리입니다(4단계).",
471
+ "",
472
+ ...table(["범주", "패키지", "대체"], replacementRows),
473
+ ]
474
+ : keptForBackend.length > 0
475
+ ? []
476
+ : ["교체할 라이브러리가 없습니다.", ""]),
207
477
  ...(keptForBackend.length > 0
208
- ? ["backend 코드도 쓰는 교체 대상입니다. 패키지는 package.json에 남기고 프론트엔드 코드의 사용만 대체 라이브러리로 옮기며, backend가 import하는 파일은 그대로 둡니다(4단계): " + keptForBackend.join(", "), ""]
478
+ ? [
479
+ "backend 코드도 쓰는 교체 대상입니다. 패키지는 package.json에 남기고 프론트엔드 코드의 사용만 대체 라이브러리로 옮기며, backend가 import하는 파일은 그대로 둡니다(4단계): " +
480
+ keptForBackend.join(", "),
481
+ "",
482
+ ]
483
+ : []),
484
+ ...(upgrades.length > 0
485
+ ? ["업그레이드할 라이브러리입니다.", "", ...upgrades, ""]
209
486
  : []),
210
- ...(upgrades.length > 0 ? ["업그레이드할 라이브러리입니다.", "", ...upgrades, ""] : []),
211
487
  ...(dependencies.backendClients.length > 0
212
- ? ["backend client SDK는 교체하지 않습니다(6단계 service): " + dependencies.backendClients.map(code).join(", "), ""]
488
+ ? [
489
+ "backend client SDK는 교체하지 않습니다(6단계 service): " +
490
+ dependencies.backendClients.map(code).join(", "),
491
+ "",
492
+ ]
493
+ : []),
494
+ ...(dependencies.others.length > 0
495
+ ? [
496
+ "표준 스택 밖의 의존성입니다(4단계): " +
497
+ dependencies.others.map(code).join(", "),
498
+ "",
499
+ ]
213
500
  : []),
214
- ...(dependencies.others.length > 0 ? ["표준 스택 밖의 의존성입니다(4단계): " + dependencies.others.map(code).join(", "), ""] : []),
501
+ ...(reactOutdated === null
502
+ ? []
503
+ : [
504
+ "### React 19 codemod 대상",
505
+ "",
506
+ ...(reactMigrationTargets.length > 0
507
+ ? reactMigrationTargets.map((file) => "- " + code(file))
508
+ : [
509
+ "- 보존 대상이 아닌 기존 JavaScript·TypeScript 파일이 없습니다.",
510
+ ]),
511
+ "",
512
+ ]),
215
513
  "### 설정과 script",
216
514
  "",
217
515
  ...list(configFindings, "- 설정과 script 오류가 없습니다."),
@@ -223,8 +521,22 @@ export function renderPlan({ version, createdAt, base, packageManager, frontend,
223
521
  "- 보호 경로, API 계약, backend script·의존성을 바꿔야만 진행할 수 있을 때",
224
522
  "- 교체할 라이브러리의 기능을 표준 스택으로 표현할 수 없을 때(예: 표준에 없는 UI 컴포넌트), 또는 교체하면 검증 규칙·오류 메시지·제출 동작 중 하나가 달라질 때",
225
523
  ...(uiDependents.length > 0
226
- ? ["- 교체할 패키지를 shadcn ui 파일이 import할 때: " + uiDependents.map((entry) => code(entry.file) + "(" + code(entry.name) + ")").join(", ")]
524
+ ? [
525
+ "- 교체할 패키지를 shadcn ui 파일이 import할 때: " +
526
+ uiDependents
527
+ .map((entry) => code(entry.file) + "(" + code(entry.name) + ")")
528
+ .join(", "),
529
+ ]
227
530
  : []),
531
+ ...(preservedDependents.length > 0
532
+ ? [
533
+ "- 교체 대상 패키지를 보존 대상 파일이 import할 때: " +
534
+ preservedDependents
535
+ .map((entry) => code(entry.file) + "(" + code(entry.name) + ")")
536
+ .join(", "),
537
+ ]
538
+ : []),
539
+ "- 기준선에 있던 테스트·mock·다국어 파일이나 설정을 수정·삭제해야 해당 단계의 검증을 통과할 수 있을 때",
228
540
  "- 업그레이드가 다른 의존성 때문에 막힐 때",
229
541
  "- 파일이 프론트엔드인지 backend인지 판단할 수 없을 때",
230
542
  "- 검증 실패의 원인이 프론트엔드 밖(backend·배포 설정)에 있을 때",
@@ -234,7 +546,9 @@ export function renderPlan({ version, createdAt, base, packageManager, frontend,
234
546
  "",
235
547
  "새 세션을 시작했거나 이전 대화가 요약되었으면 NOTES.md의 「진행」을 읽고 그다음 단계부터 이어갑니다. 각 단계는 완료 기준을 모두 만족해야 끝납니다. 단계를 마치면 check를 실행하고, 「진행」의 완료한 단계 번호를 올린 뒤 결과와 결정을 「단계별 기록」에 적습니다. 「진행」이 `commit: 허용`이면 단계를 마칠 때마다 commit하고, `commit: 안 함`이면 `git status --short` 출력을 「단계별 기록」에 붙입니다.",
236
548
  "",
237
- "`protected:`·`outside:`·`backend-script:`·`backend-dependency:` 오류는 어느 단계에서 나오든 그 단계를 끝내지 못하게 하므로 해당 변경을 되돌립니다. 검증을 통과시키려고 lint 규칙을 끄거나 `eslint-disable`·`@ts-ignore`·`@ts-expect-error`·`any` 캐스팅·ignore 경로를 새로 넣지 않습니다. 명령은 이 저장소의 패키지 매니저(" + packageManager.name + ") 기준입니다.",
549
+ "`protected:`·`outside:`·`backend-script:`·`backend-dependency:` 오류는 어느 단계에서 나오든 그 단계를 끝내지 못하게 하므로 해당 변경을 되돌립니다. 검증을 통과시키려고 lint 규칙을 끄거나 `eslint-disable`·`@ts-ignore`·`@ts-expect-error`·`any` 캐스팅·ignore 경로를 새로 넣지 않습니다. 명령은 이 저장소의 패키지 매니저(" +
550
+ packageManager.name +
551
+ ") 기준입니다.",
238
552
  "",
239
553
  "### 1. 준비",
240
554
  "",
@@ -242,127 +556,321 @@ export function renderPlan({ version, createdAt, base, packageManager, frontend,
242
556
  conventions + "를 끝까지 읽습니다.",
243
557
  "「보호 경로」가 실제 backend·배포 코드와 맞는지 확인합니다. 프론트엔드가 보호 경로(예: backend와 함께 쓰는 package root `shared/`의 schema·type)를 import하는 것은 정상이므로 import와 alias를 그대로 둡니다. 보호 경로 안에 프론트엔드에서만 쓰는 파일이 있거나 backend·배포 경로가 목록에 없으면 멈추고 사용자에게 확인합니다. 답은 NOTES.md 「보호 경로 확인」에 적고, 목록에 없던 backend 경로는 끝까지 수정하지 않습니다(check는 그 경로를 점검하지 않습니다). 보호 경로 안의 프론트엔드 전용 파일은 사용자가 옮기라고 답해도 옮기지 않고 「보류 사항」에 남깁니다(check가 보호 경로의 변경을 오류로 보고합니다).",
244
558
  "작업 branch를 만들고(예: `git switch -c eds-align`), 단계마다 commit해도 되는지 사용자에게 한 번 묻고 답을 NOTES.md 「진행」의 `commit:` 줄에 `허용` 또는 `안 함`으로 적습니다.",
245
- "`AGENTS.md`의 EDS managed block 밖에 다음 줄을 추가합니다: `" + inProgressAgentsLine + "`",
246
- "프론트엔드의 API 호출을 모두 찾아 domain 목록을 정하고 NOTES.md 「domain 목록」에 적습니다. domain 이름은 backend resource를 소문자 복수형으로 씁니다(예: `/api/users` → `users`, `supabase.from(\"profiles\")` → `profiles`).",
247
- scripts.build === undefined ? null : code(commands.run("build")) + "의 exit code와 오류 수를 NOTES.md 「기준선」에 적습니다.",
559
+ "`AGENTS.md`의 EDS managed block 밖에 다음 줄을 추가합니다: `" +
560
+ inProgressAgentsLine +
561
+ "`",
562
+ '프론트엔드의 API 호출을 모두 찾아 domain 목록을 정하고 NOTES.md 「domain 목록」에 적습니다. domain 이름은 backend resource를 소문자 복수형으로 씁니다(예: `/api/users` → `users`, `supabase.from("profiles")` → `profiles`).',
563
+ "기존 test·mock·다국어 파일과 설정을 파일명, import, package.json, tsconfig, Vite 설정에서 찾습니다. 자동 감지되지 않은 경로는 " +
564
+ code(alignLabel + "/" + stateFileName) +
565
+ "의 `preservedPaths`에 git 저장소 루트 기준으로 추가한 뒤 `eds align`을 다시 실행합니다. 검색 범위와 경로를 NOTES.md 「기존 리소스 확인」에 적습니다.",
566
+ scripts.build === undefined
567
+ ? null
568
+ : code(commands.run("build")) +
569
+ "의 exit code와 오류 수를 NOTES.md 「기준선」에 적습니다.",
248
570
  ]),
249
571
  "",
250
- "완료 기준: 「진행」의 `commit:` 줄에 답이 있고, `AGENTS.md`에 4의 줄이 있고, 「domain 목록」·「보호 경로 확인」" + (scripts.build === undefined ? "" : "·「기준선」") + "이 채워져 있습니다.",
572
+ "완료 기준: 「진행」의 `commit:` 줄에 답이 있고, `AGENTS.md`에 4의 줄이 있고, 「domain 목록」·「보호 경로 확인」·「기존 리소스 확인」" +
573
+ (scripts.build === undefined ? "" : "·「기준선」") +
574
+ "이 채워져 있습니다.",
251
575
  "",
252
576
  "### 2. 스타일 기반과 EDS",
253
577
  "",
254
578
  ...numbered([
255
579
  reactOutdated === null
256
580
  ? null
257
- : "`react` " + reactOutdated.major + " → 19: " + code(commands.add(["react", "react-dom"])) + "와 " + code(commands.addDev(["@types/react", "@types/react-dom"])) +
258
- "를 실행한 뒤 " + code(commands.dlx("codemod@latest react/19/migration-recipe")) + "와 " + code(commands.dlx("types-react-codemod@latest preset-19 ./" + app.srcDir)) + "로 코드를 옮깁니다.",
259
- (tailwindMajor === null
260
- ? "Tailwind CSS v4를 설치하고(" + code(commands.addDev(["tailwindcss", "@tailwindcss/vite"])) + ") `vite.config.ts`의 plugins에 `tailwindcss()`를 추가합니다(템플릿 참고)."
581
+ : "`react` " +
582
+ reactOutdated.major +
583
+ " → 19: " +
584
+ code(commands.add(["react", "react-dom"])) +
585
+ "와 " +
586
+ code(commands.addDev(["@types/react", "@types/react-dom"])) +
587
+ "를 실행합니다." +
588
+ (reactMigrationTargets.length > 0
589
+ ? " 이어서 " +
590
+ code(
591
+ commands.dlx("types-react-codemod@latest preset-19 <path...>")
592
+ ) +
593
+ "를 「React 19 codemod 대상」에 적힌 파일만 인자로 전달해 실행합니다."
594
+ : " 보존 대상이 아닌 JavaScript·TypeScript 파일이 없어 codemod는 생략합니다."),
595
+ viteMissing || viteOutdated !== null
596
+ ? (viteOutdated === null
597
+ ? "`vite`가 선언되어 있지 않아 "
598
+ : "Vite " + viteOutdated.version + "이 최소 버전보다 낮아 ") +
599
+ code(commands.addDev(["vite"])) +
600
+ "로 Vite " +
601
+ minimumVersions.vite +
602
+ " 이상으로 맞춥니다. Tailwind CSS v4 plugin을 설치하기 전에 실행합니다."
603
+ : null,
604
+ pluginReactOutdated !== null
605
+ ? "`@vitejs/plugin-react`가 현재 Vite와 호환되지 않아 " +
606
+ code(commands.addDev(["@vitejs/plugin-react"])) +
607
+ "를 실행합니다."
608
+ : null,
609
+ tailwindMajor === null
610
+ ? "Tailwind CSS v4를 설치하고(" +
611
+ code(commands.addDev(["tailwindcss", "@tailwindcss/vite"])) +
612
+ ") `vite.config.ts`의 plugins에 `tailwindcss()`를 추가합니다(템플릿 참고)."
261
613
  : (tailwindMajor < 4
262
- ? "Tailwind CSS를 v4로 올리고(" + code(commands.dlx("@tailwindcss/upgrade")) + "), Tailwind를 `@tailwindcss/vite` plugin으로 실행합니다(" + code(commands.addDev(["@tailwindcss/vite"])) + ")."
263
- : "Tailwind CSS v4를 `@tailwindcss/vite` plugin으로 실행합니다" + (analysis.declared.has("@tailwindcss/vite") ? "." : "(" + code(commands.addDev(["@tailwindcss/vite"])) + ").")) +
264
- " upgrade 도구가 추가했거나 기존에 쓰던 `@tailwindcss/postcss`와 `postcss.config`의 Tailwind 항목은 지웁니다(PostCSS 경로는 EDS font 파일을 번들에 넣지 않습니다)."),
265
- "전역 CSS를 " + code(srcPath("app/styles/index.css")) + " 하나로 모으고(템플릿 같은 경로), 현재 entry " + code(entryLabel) + "가 이 파일을 import하게 합니다. 기존 `@import`와 EDS가 넣은 줄은 유지합니다. 컴포넌트별 CSS 규칙은 6단계에서 className으로 옮깁니다.",
266
- (edsDeclared ? "" : code(commands.add(["@spectrabrain/eds"])) + "로 EDS를 dependencies에 선언하고, ") + code(commands.eds("init")) + "로 EDS script 등록과 첫 sync를 실행합니다. Tailwind root 후보가 여러 개라서 exit 2이면, 나머지 후보가 3번에서 합친 옛 전역 CSS일 때만 " + code("EDS_TAILWIND_ROOT=" + srcPath("app/styles/index.css") + " " + commands.run("sync:eds")) + "를 실행하고, 그렇지 않으면 사용자에게 root를 확인합니다.",
614
+ ? "Tailwind CSS를 v4로 올리고(" +
615
+ code(commands.dlx("@tailwindcss/upgrade")) +
616
+ "), Tailwind를 `@tailwindcss/vite` plugin으로 실행합니다(" +
617
+ code(commands.addDev(["@tailwindcss/vite"])) +
618
+ ")."
619
+ : "Tailwind CSS v4를 `@tailwindcss/vite` plugin으로 실행합니다" +
620
+ (analysis.declared.has("@tailwindcss/vite") &&
621
+ tailwindViteOutdated === null
622
+ ? "."
623
+ : "(" + code(commands.addDev(["@tailwindcss/vite"])) + ").")) +
624
+ " upgrade 도구가 추가했거나 기존에 쓰던 `@tailwindcss/postcss`와 `postcss.config`의 Tailwind 항목은 지웁니다(PostCSS 경로는 EDS font 파일을 번들에 넣지 않습니다).",
625
+ "전역 CSS를 " +
626
+ code(srcPath("app/styles/index.css")) +
627
+ " 하나로 모으고(템플릿 같은 경로), 현재 entry " +
628
+ code(entryLabel) +
629
+ "가 이 파일을 import하게 합니다. 기존 `@import`와 EDS가 넣은 줄은 유지합니다. 컴포넌트별 CSS 규칙은 6단계에서 className으로 옮깁니다.",
630
+ (edsDeclared
631
+ ? ""
632
+ : code(commands.add(["@spectrabrain/eds"])) +
633
+ "로 EDS를 dependencies에 선언하고, ") +
634
+ code(commands.eds("init")) +
635
+ "로 EDS script 등록과 첫 sync를 실행합니다. Tailwind root 후보가 여러 개라서 exit 2이면, 나머지 후보가 3번에서 합친 옛 전역 CSS일 때만 " +
636
+ code(
637
+ "EDS_TAILWIND_ROOT=" +
638
+ srcPath("app/styles/index.css") +
639
+ " " +
640
+ commands.run("sync:eds")
641
+ ) +
642
+ "를 실행하고, 그렇지 않으면 사용자에게 root를 확인합니다.",
267
643
  ]),
268
644
  "",
269
- "완료 기준: " + code(commands.run("eds:doctor")) + "가 `[EDS] synced with` 줄을 출력하고 " + code(commands.run("build")) + "가 exit 0이며, check 출력에 `@tailwindcss/vite`·`@tailwindcss/postcss` 줄이 없습니다." +
270
- (reactOutdated === null ? "" : " package.json의 `react` major가 19입니다."),
645
+ "완료 기준: " +
646
+ code(commands.run("eds:doctor")) +
647
+ "가 `[EDS] synced with` 줄을 출력하고 " +
648
+ code(commands.run("build")) +
649
+ "가 exit 0이며, Vite가 " +
650
+ minimumVersions.vite +
651
+ " 이상이며" +
652
+ (tailwindViteMinimum !== null
653
+ ? " `@tailwindcss/vite`가 " +
654
+ tailwindViteMinimum +
655
+ " 이상이고"
656
+ : "") +
657
+ " check 출력에 `@tailwindcss/vite`·`@tailwindcss/postcss` 줄이 없습니다." +
658
+ (reactOutdated === null
659
+ ? ""
660
+ : " package.json의 `react` major가 19입니다."),
271
661
  "",
272
662
  "### 3. 개발 도구와 설정",
273
663
  "",
274
664
  ...numbered([
275
665
  missingDev.length > 0
276
- ? "개발 도구를 설치합니다(템플릿보다 오래된 도구는 다시 설치합니다): " + code(commands.addDev(missingDev))
666
+ ? "개발 도구를 설치합니다(템플릿보다 오래된 도구는 다시 설치합니다): " +
667
+ code(commands.addDev(missingDev))
277
668
  : "필요한 개발 도구가 모두 선언되어 있습니다.",
278
- "`templates/`의 설정 파일(" + configTemplates.join(", ") + ")을 같은 경로로 복사합니다. 설정 값의 기준은 " + conventions + " 「개발 규칙」입니다. 이미 있는 파일은 다음처럼 합칩니다.",
669
+ majors.vite === 8
670
+ ? "Vite 8 React Compiler 설정에는 Node 22.12.0 이상이 필요합니다. frontend package의 `package.json`에서 `engines.node`를 `>=22.12.0`으로 설정합니다. CI Node 설정은 이 계획에서 바꾸지 않으므로 별도로 확인·갱신합니다. CI 파일이 frontend package 밖에 있으면 align 완료 후 처리합니다."
671
+ : null,
672
+ "`templates/`의 설정 파일(" +
673
+ configTemplates.join(", ") +
674
+ ")을 같은 경로로 복사합니다. 설정 값의 기준은 " +
675
+ conventions +
676
+ " 「개발 규칙」입니다. 이미 있는 파일은 다음처럼 합칩니다.",
279
677
  ]),
280
- " - `vite.config.ts`: 기존 설정의 키와 값은 모두 유지합니다(`root`·`envDir`·`base`·`server`·`build.outDir`·기존 alias·`test` 등). 템플릿에서 없는 것만 추가합니다: `@tailwindcss/vite`·svgr plugin" + (majors.vite !== null && majors.vite >= 8 ? "·`@rolldown/plugin-babel`의 React Compiler plugin" : "") + ", `resolve.alias[\"@\"]`, `resolve.dedupe`(기존 배열이 있으면 합침), `build.minify`·`terserOptions`. " +
281
- (majors.vite !== null && majors.vite >= 8 ? "" : "기존 `react()` 호출에는 React Compiler의 `babel` 옵션을 넣습니다. ") + "같은 키에 다른 값이 이미 있으면 기존 값을 두고 NOTES.md에 적습니다." +
282
- (replacedCategories.has("build-plugin") ? " `@vitejs/plugin-react-swc`·`@vitejs/plugin-react-oxc`를 쓰고 있으면 React Compiler를 위해 `@vitejs/plugin-react`로 바꿉니다(패키지 제거는 4단계)." : ""),
283
- " - `tsconfig.json`: `include`에 「보호 경로」의 경로가 있으면 compilerOptions는 그대로 두고 `paths[\"@/*\"]`만 템플릿처럼 맞춥니다. 보호 경로가 없으면 템플릿 기준으로 바꾸고, 테스트 도구가 쓰는 설정(`types`의 `vitest/globals` 등)만 유지합니다. `tsconfig.app.json`·`tsconfig.node.json`의 프론트엔드 설정은 합친 뒤 지웁니다.",
678
+ " - `vite.config.ts`: 기존 설정의 키와 값은 모두 유지합니다(`root`·`envDir`·`base`·`server`·`build.outDir`·alias 등). 템플릿에서 없는 것만 추가합니다: `@tailwindcss/vite`·svgr plugin" +
679
+ (majors.vite === 8
680
+ ? "·`@rolldown/plugin-babel`의 React Compiler plugin"
681
+ : "") +
682
+ ', `resolve.alias["@"]`, `resolve.dedupe`(기존 배열이 있으면 합침), `build.minify`·`terserOptions`. ' +
683
+ (majors.vite === 8
684
+ ? ""
685
+ : "기존 `react()` 호출에는 React Compiler의 `babel` 옵션을 넣습니다. ") +
686
+ "같은 키에 다른 값이 이미 있으면 기존 값을 두고 NOTES.md에 적습니다." +
687
+ (replacedCategories.has("build-plugin")
688
+ ? " `@vitejs/plugin-react-swc`·`@vitejs/plugin-react-oxc`를 쓰고 있으면 React Compiler를 위해 `@vitejs/plugin-react`로 바꿉니다(패키지 제거는 4단계)."
689
+ : ""),
690
+ ' - `tsconfig*.json`: 각 파일의 `include`·`exclude`·`compilerOptions.types`·project references를 그대로 둡니다. `paths["@/*"]`만 앱의 새 `src` 경로에 맞춥니다. 기존 tsconfig 파일과 설정을 다른 파일로 옮기거나 삭제하지 않습니다. `check`는 위 설정이 각 원래 파일에서 빠지면 `preserved:`로 보고합니다.',
691
+ " - `.prettierignore`: 기존 항목을 모두 유지하고 생성 CSS·보호 경로에 필요한 항목만 추가합니다.",
284
692
  " - `components.json`: `aliases`와 `tailwind.css`만 템플릿 값으로 바꾸고 `style`·`baseColor`는 유지합니다.",
285
- " - `package.json` scripts: `package.scripts.json`에서 없는 script만 추가하고, 추가한 뒤 " + code(commands.eds("init --no-sync")) + "로 `pretypecheck`·`prebuild` hook을 등록합니다. backend를 실행하는 script는 바꾸지 않습니다.",
286
- " - 이전 lint·format 설정(`.eslintrc*`, `biome.json`, 중복 Prettier 설정)은 규칙을 옮긴 뒤 지웁니다" + (replacedCategories.has("lint-format") ? "(패키지 제거는 4단계)." : "."),
693
+ " - `package.json` scripts: `package.scripts.json`에서 없는 script만 추가하고, 추가한 뒤 " +
694
+ code(commands.eds("init --no-sync")) +
695
+ "로 `pretypecheck`·`prebuild` hook을 등록합니다. backend를 실행하는 script는 바꾸지 않습니다.",
696
+ " - 이전 lint·format 설정(`.eslintrc*`, `biome.json`, 중복 Prettier 설정)은 규칙을 옮긴 뒤 정리합니다" +
697
+ (replacedCategories.has("lint-format") ? "(패키지 제거는 4단계)." : ".") +
698
+ " 테스트·mock·다국어 파일과 설정은 그대로 둡니다. 기준선에 있던 항목을 바꾸지 않고 적용할 수 없으면 멈추고 사용자에게 확인합니다.",
287
699
  // Items 1 and 2 above always exist, so the numbering continues at 3.
288
- "3. " + code(commands.run("format")) + "로 전체를 한 번 포맷합니다. 이 변경은 다른 변경과 섞지 않고 따로 commit하거나(`commit: 허용`) 따로 기록합니다(`commit: 안 함`).",
289
- "4. " + code(commands.run("typecheck")) + "·" + code(commands.run("lint")) + "를 처음 실행한 결과(오류 수)를 NOTES.md 「기준선」에 적습니다. lint 오류는 4~6단계에서 파일을 옮기거나 코드를 고치면서 함께 줄입니다.",
290
- "",
291
- "완료 기준: check에 `config:`·`script:` 오류가 없고, " + [commands.run("format:check"), commands.run("typecheck"), commands.run("build")].map(code).join("·") + "가 exit 0입니다. 기존 `build.outDir`·`base`·`server.proxy`가 병합 전과 같고, " + (packageManager.berry ? "`typecheck` script가 `eds doctor && `로 시작하며" : "`package.json`의 `pretypecheck`가 `eds doctor`를 실행하며") + ", 「기준선」에 typecheck·lint 결과가 있습니다.",
700
+ "3. " +
701
+ code(commands.exec("prettier --write <변경 파일>")) +
702
+ "로 정렬 중 수정한 파일만 포맷합니다.",
703
+ "4. " +
704
+ code(commands.run("typecheck")) +
705
+ "·" +
706
+ code(commands.run("lint")) +
707
+ "를 처음 실행한 결과(오류 수)를 NOTES.md 「기준선」에 적습니다. lint 오류는 4~6단계에서 파일을 옮기거나 코드를 고치면서 함께 줄입니다.",
708
+ "",
709
+ "완료 기준: check에 `config:`·`script:` 오류가 없고, " +
710
+ [
711
+ commands.run("format:check"),
712
+ commands.run("typecheck"),
713
+ commands.run("build"),
714
+ ]
715
+ .map(code)
716
+ .join("·") +
717
+ "가 exit 0입니다. 기존 `build.outDir`·`base`·`server.proxy`가 병합 전과 같고, " +
718
+ (packageManager.berry
719
+ ? "`typecheck` script가 `eds doctor && `로 시작하며"
720
+ : "`package.json`의 `pretypecheck`가 `eds doctor`를 실행하며") +
721
+ ", 「기준선」에 typecheck·lint 결과가 있습니다.",
292
722
  "",
293
723
  "### 4. 라이브러리 정렬",
294
724
  "",
295
725
  ...numbered([
296
- missingRuntime.length > 0 ? "표준 라이브러리를 추가합니다: " + code(commands.add(missingRuntime)) + (dependencies.sdkOnly ? ". backend는 SDK로만 호출하므로 axios는 설치하지 않습니다." : "") : "표준 라이브러리가 모두 선언되어 있습니다.",
726
+ missingRuntime.length > 0
727
+ ? "표준 라이브러리를 추가합니다: " +
728
+ code(commands.add(missingRuntime)) +
729
+ (dependencies.sdkOnly
730
+ ? ". backend는 SDK로만 호출하므로 axios는 설치하지 않습니다."
731
+ : "")
732
+ : "표준 라이브러리가 모두 선언되어 있습니다.",
297
733
  replacementRows.length > 0 || keptForBackend.length > 0
298
- ? "「의존성」의 교체 대상을 범주 하나씩 처리합니다. 코드를 대체 라이브러리로 옮기고, 표에 있는 원래 패키지를 제거하고(" + code(commands.remove(["<패키지>"])) + "), typecheck·build를 통과시킨 뒤 서두의 commit 규칙을 적용합니다. 범주별 방법은 " + conventions + " 「라이브러리 스택」을 따릅니다. routing·provider처럼 `app/main.tsx`·`app/router/`·`app/providers/`에 둘 코드는 현재 entry에서 동작만 옮기고, 파일 위치와 템플릿 적용은 5단계에서 합니다."
734
+ ? "「의존성」의 교체 대상을 범주 하나씩 처리합니다. 코드를 대체 라이브러리로 옮기고, 표에 있는 원래 패키지를 제거하고(" +
735
+ code(commands.remove(["<패키지>"])) +
736
+ "), typecheck·build를 통과시킨 뒤 서두의 commit 규칙을 적용합니다. 범주별 방법은 " +
737
+ conventions +
738
+ " 「라이브러리 스택」을 따릅니다. routing·provider처럼 `app/main.tsx`·`app/router/`·`app/providers/`에 둘 코드는 현재 entry에서 동작만 옮기고, 파일 위치와 템플릿 적용은 5단계에서 합니다."
739
+ : null,
740
+ laterUpgrades.length > 0
741
+ ? "「의존성」에서 4단계로 표시한 업그레이드를 하나씩 올리고 검증합니다."
742
+ : null,
743
+ dependencies.others.length > 0
744
+ ? "「의존성」의 표준 스택 밖 의존성마다 쓰임을 확인해, 쓰이지 않으면 제거하고 유지·제거 결정을 NOTES.md에 적습니다."
299
745
  : null,
300
- laterUpgrades.length > 0 ? "「의존성」에서 4단계로 표시한 업그레이드를 하나씩 올리고 검증합니다." : null,
301
- dependencies.others.length > 0 ? "「의존성」의 표준 스택 밖 의존성마다 쓰임을 확인해, 쓰이지 않으면 제거하고 유지·제거 결정을 NOTES.md에 적습니다." : null,
302
746
  ]),
303
747
  "",
304
- "완료 기준: check에 `dependency:` 오류가 없고, " + [commands.run("typecheck"), commands.run("build")].map(code).join("·") + "가 exit 0입니다." +
305
- (dependencies.others.length > 0 ? " 표준 스택 밖 의존성마다 NOTES.md에 결정이 있습니다." : ""),
748
+ "완료 기준: check에 `dependency:` 오류가 없고, " +
749
+ [commands.run("typecheck"), commands.run("build")].map(code).join("·") +
750
+ "가 exit 0입니다." +
751
+ (dependencies.others.length > 0
752
+ ? " 표준 스택 밖 의존성마다 NOTES.md에 결정이 있습니다."
753
+ : ""),
306
754
  "",
307
755
  "### 5. FSD 구조",
308
756
  "",
309
757
  ...numbered([
310
- "「구조」 표를 따라 파일을 `git mv`로 옮겨 이력을 보존합니다. 컴포넌트를 역할별로 나누는 기준은 " + conventions + " 「FSD 계층」입니다.",
311
- "기존 route 경로와 provider 목록을 NOTES.md에 적습니다. entry를 " + code(srcPath("app/main.tsx")) + "로 옮기고 " + code(appPrefix + "index.html") + "의 module script를 `/src/app/main.tsx`로 바꿉니다.",
312
- "기존 entry의 내용을 템플릿 `main.tsx`·`App.tsx` 구조로 합칩니다. 기존 provider는 모두 유지하고, 템플릿과 같은 역할(Router, QueryClient, 오류 처리)의 provider만 템플릿 것으로 바꿉니다. 기존 route는 모두 `router/index.tsx`로 옮기고 예시 `HomePage` route는 지웁니다. 기존 오류 처리 provider가 i18n 호출로 문구를 표시하고 있으면 그 호출은 유지합니다." +
313
- (dependencies.sdkOnly ? " 이 저장소는 MSW를 쓰지 않으므로 템플릿 `main.tsx`의 `VITE_API_MOCKING` 블록은 옮기지 않습니다." : ""),
314
- [srcPath("app/"), srcPath("shared/"), srcPath("vite-env.d.ts"), ...(dependencies.sdkOnly ? [] : [srcPath("mocks/")])].map(templatesOf).join("·") + "의 파일은 같은 경로에 같은 이름의 파일이 없을 때만 복사하고, 있으면 합칩니다. " +
758
+ "「구조」 표를 따라 파일을 `git mv`로 옮겨 이력을 보존합니다. 컴포넌트를 역할별로 나누는 기준은 " +
759
+ conventions +
760
+ " 「FSD 계층」입니다.",
761
+ "기존 route 경로와 provider 목록을 NOTES.md에 적습니다. entry를 " +
762
+ code(srcPath("app/main.tsx")) +
763
+ "로 옮기고 " +
764
+ code(appPrefix + "index.html") +
765
+ "의 module script를 `/src/app/main.tsx`로 바꿉니다.",
766
+ "기존 entry의 모든 초기화 import·호출과 provider를 유지하면서 템플릿 `main.tsx`·`App.tsx` 구조로 합칩니다. 템플릿과 같은 역할(Router, QueryClient, 오류 처리)의 provider만 템플릿 것으로 바꿉니다. 기존 route는 모두 `router/index.tsx`로 옮기고 예시 `HomePage` route는 지웁니다.",
767
+ [srcPath("app/"), srcPath("shared/"), srcPath("vite-env.d.ts")]
768
+ .map(templatesOf)
769
+ .join("·") +
770
+ "의 파일은 같은 경로에 같은 이름의 파일이 없을 때만 복사하고, 있으면 합칩니다. " +
315
771
  (dependencies.sdkOnly
316
- ? code(srcPath("shared/api/index.ts")) + "는 템플릿 대신 기존 SDK client(" + sdkClients.map(code).join(", ") + ")를 한 번 만들어 export하고, `queryClient.ts`·`ErrorHandler.tsx`의 `isAxiosError`·`error.response?.status` 판별은 SDK의 오류 타입 판별로 바꿉니다."
317
- : code(srcPath("shared/api/index.ts")) + "에는 기존 요청의 base URL·header·credentials를 옮겨 채웁니다."),
318
- "import 경로와 barrel(`index.ts`)은 " + conventions + " 「FSD 계층」의 import·이름 규칙을 따릅니다.",
319
- code(app.srcDir) + " 안의 JavaScript 파일(`.js`·`.jsx`)은 TypeScript로 바꿉니다. 테스트 파일은 그대로 둡니다.",
772
+ ? code(srcPath("shared/api/index.ts")) +
773
+ "는 템플릿 대신 기존 SDK client(" +
774
+ sdkClients.map(code).join(", ") +
775
+ ")를 한 번 만들어 export하고, `queryClient.ts`·`ErrorHandler.tsx`의 `isAxiosError`·`error.response?.status` 판별은 SDK의 오류 타입 판별로 바꿉니다."
776
+ : code(srcPath("shared/api/index.ts")) +
777
+ "에는 기존 요청의 base URL·header·credentials를 옮겨 채웁니다."),
778
+ "import 경로와 barrel(`index.ts`)은 " +
779
+ conventions +
780
+ " 「FSD 계층」의 import·이름 규칙을 따릅니다.",
781
+ code(app.srcDir) +
782
+ " 안의 정렬 대상 JavaScript 파일(`.js`·`.jsx`)은 TypeScript로 바꿉니다. 보존 대상 파일은 제외합니다.",
320
783
  ]),
321
784
  "",
322
- "완료 기준: check에 `structure:`·`import:`·`language:` 오류가 없고, " + [commands.run("typecheck"), commands.run("build")].map(code).join("·") + "가 exit 0이며, NOTES.md에 적은 route와 provider가 모두 옮겨져 있습니다.",
785
+ "완료 기준: check에 `structure:`·`import:`·`language:` 오류가 없고, " +
786
+ [commands.run("typecheck"), commands.run("build")].map(code).join("·") +
787
+ "가 exit 0이며, NOTES.md에 적은 route와 provider가 모두 옮겨져 있습니다.",
323
788
  "",
324
789
  "### 6. 코드 패턴",
325
790
  "",
326
791
  "방법은 " + conventions + " 「코드 패턴」의 해당 절을 따릅니다.",
327
792
  "",
328
793
  ...numbered([
329
- "앱 전역을 먼저 한 번 설정합니다: " + code(srcPath("shared/api")) + (dependencies.sdkOnly ? "의 SDK client" : " instance") + ", 오류 처리(ErrorBoundary·ErrorHandler·sonner·eventBus), " + code(srcPath("app/router/index.tsx")) + "의 lazy page" +
330
- (dependencies.sdkOnly
331
- ? "."
332
- : ". 「domain 목록」에 REST API domain이 있으면 " + code(commands.addDev(mockDevDependencies)) + "를 설치하고 `dev:mock` script를 확인한 뒤 " +
333
- code(commands.exec("msw init " + appPrefix + "public --save")) + "로 worker를 Vite root의 `public/`에 만듭니다."),
334
- "NOTES.md 「domain 목록」의 domain마다 `types/api.ts`·`types/enums.ts` → `<domain>Service.ts` → query·mutation hook → (필요하면) store" + (dependencies.sdkOnly ? "" : " → (REST API면) " + code(srcPath("mocks/<domain>")) + " handler") + " → form·component·page 순으로 옮깁니다. UI 문구는 " + conventions + " 「UI 문구」를 따릅니다." +
335
- (dependencies.backendClients.length > 0 ? " backend client SDK(" + dependencies.backendClients.map(code).join(", ") + ")는 교체하지 않으며, SDK client는 " + code(srcPath("shared/api")) + "에서 한 번만 만들고 entity service만 그 client를 import합니다." : "") +
336
- " domain 하나를 마칠 때마다 typecheck·build를 실행하고 「단계별 기록」에 `<domain>: service·hook·store·mock 경로, 완료`를 적습니다.",
794
+ "앱 전역을 한 번 설정합니다: " +
795
+ code(srcPath("shared/api")) +
796
+ (dependencies.sdkOnly ? "의 SDK client" : " instance") +
797
+ ", 오류 처리(ErrorBoundary·ErrorHandler·sonner·eventBus), " +
798
+ code(srcPath("app/router/index.tsx")) +
799
+ "의 lazy page를 둡니다.",
800
+ "NOTES.md 「domain 목록」의 domain마다 `types/api.ts`·`types/enums.ts` → `<domain>Service.ts` → query·mutation hook → (필요하면) store → form·component·page 순으로 옮깁니다." +
801
+ (dependencies.backendClients.length > 0
802
+ ? " backend client SDK(" +
803
+ dependencies.backendClients.map(code).join(", ") +
804
+ ")는 교체하지 않으며, SDK client는 " +
805
+ code(srcPath("shared/api")) +
806
+ "에서 한 번만 만들고 entity service만 그 client를 import합니다."
807
+ : "") +
808
+ " domain 하나를 마칠 때마다 typecheck·build를 실행하고 「단계별 기록」에 `<domain>: service·hook·store 경로, 완료`를 적습니다.",
337
809
  "남은 " + code(commands.run("lint")) + " 오류를 고칩니다.",
338
810
  ]),
339
811
  "",
340
- "완료 기준: 「domain 목록」의 모든 domain에 service·hook 경로와 완료 표시가 있고, 이 저장소 코드의 fetch()·axios 호출은 " + code(srcPath("shared/api")) + "와 entity service에만 있습니다. check가 exit 0이고, 8단계에서 해소하는 `warning agents:`를 뺀 나머지 경고는 모두 고치거나 이유를 NOTES.md에 적었습니다. " +
341
- [commands.run("lint"), commands.run("typecheck"), commands.run("build")].map(code).join("·") + "가 exit 0입니다.",
812
+ "완료 기준: 「domain 목록」의 모든 domain에 service·hook 경로와 완료 표시가 있고, 이 저장소 코드의 fetch()·axios 호출은 " +
813
+ code(srcPath("shared/api")) +
814
+ "와 entity service에만 있습니다. check가 exit 0이고, 8단계에서 해소하는 `warning agents:`를 뺀 나머지 경고는 모두 고치거나 이유를 NOTES.md에 적었습니다. " +
815
+ [commands.run("lint"), commands.run("typecheck"), commands.run("build")]
816
+ .map(code)
817
+ .join("·") +
818
+ "가 exit 0입니다.",
342
819
  "",
343
820
  "### 7. EDS 컴포넌트와 token",
344
821
  "",
345
822
  ...numbered([
346
823
  "`QUICKSTART.md` 규칙대로 버튼을 EDS `Button`으로, 색상·typography를 EDS token class로 바꿉니다. shadcn `button.tsx`는 다른 shadcn 컴포넌트가 쓸 때만 남깁니다. 기존 로컬 Button wrapper를 남길지는 `dist/components/Button/README.md` 「문서와 검증」을 따릅니다.",
347
- code(commands.run("eds:doctor") + " -- --scan") + "을 실행해 보고된 raw 색상 class를 공개 token class로 바꿉니다. 바꿀 수 없는 항목은 파일·class·이유를 NOTES.md에 적습니다.",
824
+ code(commands.run("eds:doctor") + " -- --scan") +
825
+ "을 실행해 보고된 raw 색상 class를 공개 token class로 바꿉니다. 바꿀 수 없는 항목은 파일·class·이유를 NOTES.md에 적습니다.",
348
826
  ]),
349
827
  "",
350
- "완료 기준: " + code(commands.run("eds:doctor")) + "가 `[EDS] synced with` 줄을 출력하고, `-- --scan` 출력이 `[EDS] scan: no raw color classes found`이거나 남은 class마다 NOTES.md에 이유가 있습니다. " +
351
- code("grep -rn \"<button\" " + app.srcDir) + "의 결과가 " + code(srcPath("shared/ui/")) + " 안의 shadcn 컴포넌트뿐입니다.",
828
+ "완료 기준: " +
829
+ code(commands.run("eds:doctor")) +
830
+ "가 `[EDS] synced with` 줄을 출력하고, `-- --scan` 출력이 `[EDS] scan: no raw color classes found`이거나 남은 class마다 NOTES.md에 이유가 있습니다. " +
831
+ code('grep -rn "<button" ' + app.srcDir) +
832
+ "의 결과가 " +
833
+ code(srcPath("shared/ui/")) +
834
+ " 안의 shadcn 컴포넌트뿐입니다.",
352
835
  "",
353
836
  "### 8. 최종 검증과 마무리",
354
837
  "",
355
838
  ...numbered([
356
- "`AGENTS.md`에서 1단계에 넣은 줄을 다음 줄로 바꿉니다: `- 파일을 만들거나 옮길 때, 의존성을 추가하거나 바꿀 때, service·hook·store·component·page와 화면 문구를 쓸 때 EDS package root의 " + conventionsFileName +
357
- " 해당 절을 읽고, 완료 전에 " + alignCheck + "를 실행합니다.`",
358
- [commands.run("format:check"), commands.run("lint"), commands.run("typecheck"), commands.run("build"), commands.run("eds:doctor"), alignCheck].map(code).join(", ") + "을 실행해 모두 exit 0이고 " + code(commands.run("eds:doctor")) + "가 `[EDS] synced with` 줄을 출력하는지 확인합니다.",
839
+ "`AGENTS.md`에서 1단계에 넣은 줄을 다음 줄로 바꿉니다: `- 파일을 만들거나 옮길 때, 의존성을 추가하거나 바꿀 때, service·hook·store·component·page를 작성하거나 수정할 때 EDS package root의 " +
840
+ conventionsFileName +
841
+ " 해당 절을 읽고, 완료 전에 " +
842
+ alignCheck +
843
+ "를 실행합니다.`",
844
+ [
845
+ commands.run("format:check"),
846
+ commands.run("lint"),
847
+ commands.run("typecheck"),
848
+ commands.run("build"),
849
+ commands.run("eds:doctor"),
850
+ alignCheck,
851
+ ]
852
+ .map(code)
853
+ .join(", ") +
854
+ "을 실행해 모두 exit 0이고 " +
855
+ code(commands.run("eds:doctor")) +
856
+ "가 `[EDS] synced with` 줄을 출력하는지 확인합니다.",
359
857
  backendImportsViteConfig && backendStartScript !== null
360
- ? "backend가 `vite.config.ts`를 import하므로 build 뒤 " + code(commands.run(backendStartScript)) + "를 실행해 설정을 불러오는 단계에서 오류 없이 기동되는지 확인합니다."
858
+ ? "backend가 `vite.config.ts`를 import하므로 build 뒤 " +
859
+ code(commands.run(backendStartScript)) +
860
+ "를 실행해 설정을 불러오는 단계에서 오류 없이 기동되는지 확인합니다."
361
861
  : null,
362
- "아래 「완료 보고」를 사용자에게 보내는 최종 응답으로 작성합니다. NOTES.md의 결정·보류 사항과 기록한 수치를 보고에 옮긴 뒤 " + code(alignLabel + "/") + "를 지웁니다.",
862
+ "아래 「완료 보고」를 사용자에게 보내는 최종 응답으로 작성합니다. NOTES.md의 결정·보류 사항과 기록한 수치를 보고에 옮긴 뒤 " +
863
+ code(alignLabel + "/") +
864
+ "를 지웁니다.",
363
865
  ]),
364
866
  "",
365
- "완료 기준: 2의 명령이 모두 exit 0이고 doctor가 `[EDS] synced with` 줄을 출력하고" + (backendImportsViteConfig && backendStartScript !== null ? ", backend가 기동되고" : "") + ", `AGENTS.md`에 1의 줄이 있고 1단계에 넣은 줄은 없으며, " + code(alignLabel + "/") + "가 없습니다.",
867
+ "완료 기준: 2의 명령이 모두 exit 0이고 doctor가 `[EDS] synced with` 줄을 출력하고" +
868
+ (backendImportsViteConfig && backendStartScript !== null
869
+ ? ", backend가 기동되고"
870
+ : "") +
871
+ ", `AGENTS.md`에 1의 줄이 있고 1단계에 넣은 줄은 없으며, " +
872
+ code(alignLabel + "/") +
873
+ "가 없습니다.",
366
874
  "",
367
875
  "## 완료 보고",
368
876
  "",