docs: design deriving memento-frontend from this template

Records the approved approach for standing up memento-frontend: merge
this template's history into it and keep the template as an `upstream`
remote so later improvements can be merged in.

Two decisions drive the rest of the design. The derived project tracks
`upstream/main`, so this repository's `main` has to be fast-forwarded to
`develop` first. And the derived project keeps the reference feature and
the example routes, so upstream merges stay conflict-free.

The design also covers one change to this repository that is not about
memento: the supply-chain builder and verifier IDs currently hardcode the
repository name in four files, which would make all four permanent
conflict points for every derived project. Deriving them from the
`name` in `package.json` reduces that surface to one line. This
repository's own name does not change, so the emitted ID strings stay
byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-20 13:44:40 +09:00
co-authored by Claude Opus 5
parent ec7f20e2ee
commit 8dd7cbaea3
@@ -0,0 +1,265 @@
# memento-frontend를 이 템플릿에서 파생시키기 — 설계서
- 작성일: 2026-09-20
- 대상 레포 (둘)
- 템플릿: `/home/donghyeon/workspace/desktop-server-git/clean-architecture-frontend-template` (`develop`, 기준 커밋 `ec7f20e`)
- 파생: `/home/donghyeon/workspace/desktop-server-git/memento-frontend` (`main`, 기준 커밋 `b35e17c`)
- 원격: 둘 다 같은 Gitea (`https://git.learn.hyeonworks.com/donghyeon.kang/...`)
- 이 문서는 설계만 한다. 소스 수정·빌드·테스트 실행 없음.
---
## 0. 요약 (먼저 읽을 것)
| 항목 | 판정 |
|---|---|
| 반영 방식 | **upstream 추적 부트스트랩**. 템플릿 히스토리를 memento에 머지하고 `upstream` 원격으로 남겨 계속 받아온다 |
| 추적 브랜치 | `upstream/main`. 그러려면 템플릿의 `main`을 먼저 `develop` 지점까지 올려야 한다 |
| `develop``main` | **fast-forward**. `main``Initial commit` 1개뿐이고 `develop`의 조상이라 머지 커밋이 안 생긴다 |
| memento 초기 상태 | 템플릿 그대로 유지 + 식별자만 리브랜딩. `reference-feature``/examples/*`**남긴다** |
| 템플릿 선수정 | builder/verifier ID를 `package.json``name`에서 파생. 템플릿 자신의 산출물 문자열은 **불변** |
| 템플릿에 memento 흔적 | **없다.** 템플릿에 "memento"라는 문자열은 들어가지 않는다 |
| 최대 위험 | `pnpm install` 후 게이트가 memento에서도 통과하는지. 특히 `verify:release` 계열이 새 `name`으로 재계산된 ID를 일관되게 쓰는지 |
실행 순서는 되돌리기 어려운 것이 뒤에 오도록 잡았다: 템플릿 수정 → 게이트 → push → memento 머지 → 리브랜딩 → 게이트.
---
## 1. 확정된 결정 (사용자 승인 완료)
1. **반영 = upstream 추적 부트스트랩.** 1회성 스냅샷 복사안과 "동기화 스크립트만 만들기"안은 기각.
근거: 템플릿이 계속 개선되는 레포이므로, 파생 시점에 끊기면 개선분을 수동 diff로 옮겨야 한다.
2. **추적 대상은 `upstream/main`.** (`develop`이 아니다.)
따라서 템플릿의 `main``develop` 지점까지 올리는 것이 선행 작업이다.
3. **memento 초기 상태는 "템플릿 그대로 + 리브랜딩만".**
`reference-feature``/examples/*`를 남긴다. 근거: (a) 살아있는 참조 구현이 남아 팀이 패턴을 보고 따라갈 수 있다, (b) 삭제하면 `INVENTORY.md`·테스트 레지스트리·e2e·a11y까지 연쇄로 손봐야 한다, (c) 삭제한 파일은 upstream 머지마다 충돌한다.
4. **builder/verifier ID는 템플릿 쪽에서 `package.json`의 `name` 파생으로 바꾼다.**
memento에서 문자열만 교체하는 안은 기각. 근거: 그 경우 4개 파일이 영구 충돌 후보가 된다.
5. **앱 표시 이름은 `Memento`.** ko/en 카탈로그 모두 동일 표기.
6. **템플릿 `main`의 origin push 승인됨.**
---
## 2. Part A — 템플릿 선수정: 프로젝트 이름 파생
### A.1 문제
레포 이름이 4개 파일에 하드코딩되어 있다.
| 파일 | 줄 | 값 |
|---|---|---|
| `scripts/lib/provider-evidence.ts` | 1213 | `PROMOTION_VERIFIER_ID = "clean-architecture-frontend-template/promotion-verifier"` |
| `scripts/lib/local-release-evidence.ts` | 306307 | `LOCAL_EVIDENCE_VERIFIER_ID = "clean-architecture-frontend-template/local-evidence-verifier"` |
| `scripts/generate-supply-chain.ts` | 273 | `builder: { id: "local:clean-architecture-frontend-template" }` |
| `tests/unit/security-followup-fixture.ts` | 38, 183, 499 | 위 두 ID의 리터럴 복사본 |
파생 프로젝트는 이 4개 파일을 고쳐야 하고, 그 순간 4개 파일 모두가 upstream 머지의 영구 충돌 지점이 된다.
### A.2 해법: `scripts/lib/supply-chain.ts`에 `PROJECT_NAME`을 둔다
새 파일을 만들지 않는 것이 핵심이다. `scripts/lib/supply-chain.ts`
- 세 소비자가 **이미 전부 임포트**하고 있고
(`provider-evidence.ts:5-8`, `local-release-evidence.ts:70`, `generate-supply-chain.ts:26`)
- `LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS`(`scripts/lib/release-candidate.ts:38-58`)에 **이미 등재**되어 있다.
새 파일을 만들면 이렇게 된다:
- `tests/unit/security-followup.test.ts:79-92``local-release-evidence.ts`의 모든 상대 `.ts` import를
뽑아 `LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS`의 부분집합인지 검사한다. 새 파일을 임포트하면 **이 테스트가 깨진다.**
- 목록을 늘리면 `release-candidate.ts:71`을 통해 릴리스 증적의 대상 파일 집합이 함께 바뀐다.
`supply-chain.ts`는 이미 임포트되어 있고 이미 목록에 있으므로 둘 다 발생하지 않는다.
### A.3 변경 내용
`scripts/lib/supply-chain.ts`에 추가:
- 리포지토리 루트의 `package.json`에서 `name`을 읽어 `PROJECT_NAME`으로 노출한다.
- 읽기는 모듈 로드 시점 1회. `node:fs``readFileSync`를 쓴다
(선례: `scripts/check-adapter-inventory.ts`). 기존 `export const` 소비자들이
동기 상수를 기대하므로 비동기 `readFile`로는 시그니처가 바뀐다.
- 루트 해석은 `new URL("../../package.json", import.meta.url)` 로 고정한다. 레포의
기존 관용구가 이것이다(`scripts/run-vitest.ts:26`, `scripts/check-architecture.ts:74`).
`import.meta.dirname`은 이 레포에서 쓰인 적이 없으므로 도입하지 않는다.
`process.cwd()`에 의존하면 다른 디렉터리에서 스크립트를 호출했을 때 값이 달라진다.
- `name`이 없거나 빈 문자열이면 **던진다**. 조용한 폴백은 증적(provenance)에
잘못된 identity를 박아 넣는다.
파생 상수:
| 상수 | 정의 | 템플릿에서의 값 |
|---|---|---|
| `PROMOTION_VERIFIER_ID` | `` `${PROJECT_NAME}/promotion-verifier` `` | `clean-architecture-frontend-template/promotion-verifier` |
| `LOCAL_EVIDENCE_VERIFIER_ID` | `` `${PROJECT_NAME}/local-evidence-verifier` `` | `clean-architecture-frontend-template/local-evidence-verifier` |
| supply-chain builder id | `` `local:${PROJECT_NAME}` `` | `local:clean-architecture-frontend-template` |
**템플릿의 `package.json` name은 안 바꾼다.** 따라서 세 값 모두 현재와 문자 단위로 동일하다.
이 변경은 템플릿의 산출물을 바꾸지 않는다. 바뀌는 것은 값의 출처뿐이다.
### A.4 테스트 픽스처
`tests/unit/security-followup-fixture.ts`의 리터럴 3곳(38, 183, 499)을 위 상수 import로 교체한다.
**조사 결과 부정 케이스는 없다.** 세 곳 모두 `status: "PASS"` 픽스처이고, 의도적으로 틀린
verifier ID를 넣어 거부를 확인하는 테스트는 `tests/` 어디에도 없다. 따라서 세 곳 전부
상수로 바꿔도 테스트가 무력화되지 않는다.
참고로 불일치를 거부하는 코드는 `scripts/lib/exact-promotion-bundle.ts:93`과
`scripts/lib/local-release-evidence.ts:785`다. 나중에 부정 케이스를 추가한다면
그쪽은 의도적으로 **틀린 리터럴**을 써야 한다. 상수를 쓰면 자기 자신과 비교하게 된다.
### A.5 Part A 완료 조건
```bash
corepack pnpm lint
corepack pnpm check:types
corepack pnpm test:all
corepack pnpm build
corepack pnpm verify:release
```
추가로, 이 변경이 산출물을 안 바꿨다는 것을 확인한다: 변경 전후로
`artifacts/release/provenance.json`의 `predicate.runDetails.builder.id`가 동일해야 한다.
---
## 3. Part B — 템플릿 `develop` → `main`
현재 상태:
```
main 91ae42a Initial commit (커밋 1개)
develop ec7f20e refactor: 프론트엔드 리펙토링 (커밋 234개)
```
`main`은 `develop`의 조상이다(`git merge-base --is-ancestor main develop` 통과). 따라서:
```bash
git checkout main
git merge --ff-only develop
git push origin main
```
`--ff-only`를 쓰는 이유: `main`이 고유 커밋을 하나도 갖고 있지 않아 `--no-ff` 머지 커밋은
아무 정보도 담지 못한다. `--ff-only`는 "조상이 아니면 실패"라는 안전장치 역할도 한다.
Part A의 커밋이 `develop`에 먼저 올라가야 하므로 **Part A → Part B 순서를 지킨다.**
(Part A가 `develop`에 커밋되어도 `main`은 여전히 조상이므로 ff는 유지된다.)
---
## 4. Part C — memento-frontend 부트스트랩
현재 상태: `main`에 커밋 2개 (`bce7c4c Initial commit`, `b35e17c README.md 업데이트`), 파일은 `README.md` 하나.
```bash
cd /home/donghyeon/workspace/desktop-server-git/memento-frontend
git remote add upstream https://git.learn.hyeonworks.com/donghyeon.kang/clean-architecture-frontend-template
git fetch upstream
git merge upstream/main --allow-unrelated-histories
```
- `--allow-unrelated-histories`가 필요한 이유: 두 레포는 공통 조상이 없다.
- 예상 충돌: **`README.md` 1건**(양쪽에 존재하는 유일한 경로). 템플릿 쪽을 채택한 뒤 Part D에서 교체한다.
- 결과: memento의 커밋 2개가 first-parent 선상에 보존되고, 템플릿 234개가 머지로 들어온다.
이후 동기화:
```bash
git fetch upstream && git merge upstream/main
```
---
## 5. Part D — 리브랜딩
### D.1 바꾸는 곳
| 파일 | 위치 | 현재 | 변경 후 |
|---|---|---|---|
| `package.json` | `name` | `clean-architecture-frontend-template` | `memento-frontend` |
| `index.html` | 7 | `Clean Architecture Frontend` | `Memento` |
| `src/presentation/i18n/catalog.ts` | 6, 165 (`common.appName`) | `Frontend Skeleton` | `Memento` |
| `src/presentation/i18n/catalog.ts` | 44, 203 (`route.APP_HOME.title`) | `Clean Architecture Frontend` | `Memento` |
| `src/presentation/pages/home-page.tsx` | 43 | `Clean Architecture Frontend` | `Memento` |
| `src/contracts/storage-keys.ts` | 1 (`APP_NAMESPACE`) | `ca-frontend` | `memento` |
| `README.md` | 전체 | 템플릿 설명 | memento 설명 + upstream 동기화 절차 |
`package.json`의 `name` 변경은 Part A 덕분에 자동으로 verifier/builder ID까지 memento 값으로 전파된다.
`APP_NAMESPACE` 변경 시 주의: 이 값은 `storage-keys.ts:140`에서 `${APP_NAMESPACE}:${scope}:v${schemaVersion}:${name}` 형태로 브라우저 저장소 키를 만든다. 템플릿이 이미 쓰던 키가 남아 있는 브라우저에서는 기존 데이터가 고아가 된다. 신규 프로젝트라 실제 영향은 없지만, 키 형식을 검사하는 테스트가 있으면 같이 갱신해야 한다.
### D.2 바꾸지 않는 곳 (의도적)
- **`docs/superpowers/specs/*`, `docs/reviews/*`, `docs/architecture/*` 안의 템플릿 이름**
이들은 템플릿에서 실제로 일어난 작업의 기록이다. 이름을 바꾸면 기록이 거짓이 되고,
upstream 머지 충돌만 늘어난다. (해당 파일 7개)
- **`config/runtime/*.json`의 `API_BASE_URL`**
`memento-backend`가 아직 빈 레포라 넣을 값이 없다. `http://localhost:8080/` 유지.
- **`public/config.json`의 `FEATURE_OVERRIDES["reference-feature"]`**
결정 3에 따라 `reference-feature`를 남기므로 그대로 둔다.
### D.3 memento `README.md`
최소한 다음을 담는다. memento의 도메인이 아직 정해지지 않았으므로 제품 설명은 쓰지 않는다.
1. 이 레포가 `clean-architecture-frontend-template`에서 파생되었다는 사실
2. upstream 동기화 절차 (`git fetch upstream && git merge upstream/main`)
3. 로컬 실행 절차 (템플릿 README에서 승계)
4. 리브랜딩된 지점 목록 — 다음 사람이 upstream 충돌을 만났을 때 볼 곳
---
## 6. 검증
memento-frontend에서, Part D 완료 후:
```bash
corepack pnpm install --frozen-lockfile
corepack pnpm lint
corepack pnpm check:types
corepack pnpm check:architecture
corepack pnpm check:adapter-inventory
corepack pnpm test:all
corepack pnpm build
corepack pnpm verify:release
```
`verify:release`를 포함하는 이유: Part A가 손댄 경로가 정확히 이 게이트의 대상이고,
`package.json`의 `name`이 바뀐 상태에서 ID 파생이 일관되게 동작하는지는 여기서만 드러난다.
`.nvmrc`의 Node 24.14.0과 Corepack이 필요하다.
---
## 7. 비범위 (이번에 하지 않는 것)
- memento의 도메인 기능 설계·구현. 이번 작업의 산출물은 "빌드되고 게이트를 통과하는 빈 스캐폴드"까지다.
- `memento-backend` 연동. 해당 레포도 아직 비어 있다.
- `reference-feature` 제거. 결정 3에 따라 남긴다. 나중에 제거할 때는 별도 작업으로 다룬다.
- `.gitea/workflows/quality-gates.yml`의 CI 활성화 검증. 파일은 그대로 넘어가지만 Gitea Actions 러너 설정은 이 작업 범위 밖이다.
- 템플릿의 `develop` 브랜치 정리나 `refactor/platform-consumer-migration` 브랜치 처리.
---
## 8. 리스크
| 리스크 | 영향 | 완화 |
|---|---|---|
| Part A에서 `readFileSync` 경로가 호출 컨텍스트에 따라 달라짐 | 증적에 잘못된 identity | `new URL(..., import.meta.url)` 기준 고정, `process.cwd()` 사용 금지 |
| Part A가 `local-release-evidence.ts`에 새 import를 추가함 | `security-followup.test.ts:79-92` 실패 | 이미 임포트 중인 `supply-chain.ts`만 쓴다. 새 파일을 만들지 않는다 |
| memento에서 `pnpm install` 후 게이트 실패 | 부트스트랩 미완 | Part B push 전에 템플릿에서 전 게이트를 먼저 통과시킨다 |
| `--allow-unrelated-histories` 머지에서 예상 밖 충돌 | 수작업 증가 | 충돌 파일이 `README.md` 외에 나오면 중단하고 보고 |
| `main` ff 머지 실패 | Part B 중단 | `--ff-only`가 알아서 실패시킨다. 그 경우 `main`에 예상 못한 커밋이 생긴 것이므로 재조사 |
---
## 9. 실행 순서 (되돌리기 어려운 것이 뒤)
1. Part A — 템플릿 `develop`에서 ID 파생으로 변경, 게이트 통과, 커밋
2. Part B — `develop` → `main` ff 머지, `origin/main` push ← 첫 외부 반영
3. Part C — memento에 `upstream` 추가, `upstream/main` 머지, `README.md` 충돌 해소
4. Part D — 리브랜딩
5. 검증 — memento에서 전 게이트 실행
6. memento 커밋 (push 여부는 별도 확인)