CLAUDE.md

이 워크스페이스에서 일할 때의 규칙. 구성과 빠른 시작은 README.md.

이 워크스페이스는 cob 커널에서 만들어졌다. 기능은 feature(모듈)로 더하고, 모듈은 slots.yaml 의 자리에만 꽂는다. 커널 저장소(coco-de/kernel) 자체를 고치는 중이라면 KERNEL.md 를 먼저 읽을 것 — 커널에만 있는 규칙(포장 · 도너 흔적 · compose 계약 시험)이 거기 있고, 생성 프로젝트에는 그 파일이 없다.

반드시 지킬 것

1. 정체성은 지정 파일에만 (Identity-Free)

앱 이름 · 번들 ID · 조직 값은 identity.yaml 의 files 밖에 쓰지 않는다. 필요하면 참조한다:

자리 참조
Dart (앱) AppIdentity (shared/env_config)
Dart (서버) ServerIdentity (backend/handalpum_server/lib/src/server_identity.dart)
iOS · macOS $(APP_DISPLAY_NAME) · $(APP_BUNDLE_ID) (AppInfo.xcconfig)
Android identity.properties → applicationId · ${appName}

project_name(서버 · 클라이언트 패키지 이름)과 app_link_domain(유니버설 · 앱 링크)도 정체성 값이다. project_name 만은 import · 경로 · serverpod 생성물에 두루 들어가 지정 파일 제한이 없다(포장이 모든 파일에서 치환한다) — 대신 중립 이름 backend_* 와 카멜 표기를 게이트가 막는다.

Android namespace 와 Kotlin 패키지는 정체성과 무관한 고정값 app.shell 이다. 테스트 타깃 번들 ID 도 app.shell.RunnerTests 로 고정한다. 새 플랫폼 파일에 정체성이 꼭 들어가야 하면 identity.yaml 의 files 에 올린다.

2. 마커는 계약이다

slots.yaml 의 마커 문구는 cob 레지스트라가 문자열 그대로 찾는다. 문구를 바꾸거나 지우면 compose 가 멈춘다. 새 슬롯은 slots.yaml 에 선언하고 파일에 한 번만 둔다(check_slots.py).

3. 생성물은 커밋하고, 손대지 않는다

생성물 다시 만들기
shared/i10n/lib/src/translations/*.g.dart cd shared/i10n && dart run slang
feature/<domain>/{app,console}/lib/src/i18n/*.g.dart tool/feature_modules flutter-codegen (기능 소유 slang)
backend/handalpum_server/lib/src/generated/** · backend/handalpum_client/lib/src/protocol/** · test/integration/test_tools/ tool/serverpod/cli.sh generate
**/*.g.dart(go_router_builder) · test/src/bdd/*.widget_test.dart(co_bdd) tool/bdd/generate.sh
골든 기준 이미지 test/golden/goldens/** CI(조직 셀프호스트 macOS 러너 · Darwin/arm64) — 커밋 메시지 [update-goldens] 로 push

커밋하지 않는 생성물: envied 의 shared/env_config/lib/src/env/*.g.dart(난독화 값) — tool/dev/write_env.sh.

CI 가 생성기를 다시 돌려 작업 트리가 그대로인지 본다. 생성물(*.widget_test.dart 포함)에 dart format 을 돌리지 말 것 — 생성기 출력과 달라져 CI 가 빨개진다. 포맷은 생성물을 뺀 파일에만 한다(CI 의 포맷 단계 참고).

번역 접근자는 JSON 키 그대로 snake_case 다(key_case: snake). 기존 모듈 코드가 그 표기를 쓰므로 바꾸지 않는다. 기본 로케일 밖은 지연 로딩이라 테스트에서는 await locale.build() 로 읽는다(위젯 테스트의 가짜 시계 안에서는 끝나지 않으니 setUpAll(LocaleSettings.instance.loadAllLocales) 뒤 getTranslations).

번역은 11개 로케일 전부(ko en zh-Hans ja de fr es pt it ru ar — 데모 표준 S-18). 키를 더하면 모든 로케일에 더하고 python3 tool/i18n/check_parity.py --strict 로 확인한다(게이트). 새 글자(한자 · 아랍 문자 등)가 들면 tool/gates/check_locale_fallback_font.py 가 알려 준다 — tool/fonts/build_locale_fallback.py 로 글꼴을 다시 만든다.

4. 화면 · 파일 · 라우트 규약 (게이트가 막는다)

5. 비밀은 저장소 밖

앱 환경변수 shared/env_config/.env.<flavor> 는 커밋하지 않는다(.env.example 만) — tool/dev/write_env.sh 가 만든다. 키 목록 · 우선순위는 shared/env_config/KEYS.md. backend/handalpum_server/config/passwords.yaml 도 커밋하지 않는다. 로컬·CI 는 tool/dev/write_passwords.sh 로 만들고, 운영은 같은 키를 SERVERPOD_PASSWORD_<키> 환경변수로 넣는다. 서버가 부팅 때 읽는 키는 그 스크립트 주석에 있다.

명령

flutter pub get
tool/dev/write_env.sh                     # .env.<flavor> + envied 생성(처음 한 번 · 키를 바꾼 뒤)
dart analyze --fatal-infos
tool/gates/check_all.sh
tool/feature_modules check
tool/serverpod/cli.sh generate            # 모듈 → 호스트 DAG
tool/feature_modules flutter-codegen      # slang · 모든 workspace 생성기

# 테스트 — 루트 workspace 의 모든 패키지 (서버는 내장 Postgres — write_passwords.sh 먼저)
tool/dev/test_all.sh          # SKIP_SERVER=1 이면 서버 제외

(cd app/app && flutter build web --no-wasm-dry-run --dart-define=FLAVOR=production)
bash tool/pages/build.sh                  # Pages 사이트 → build/pages/
(cd tool/pages/smoke && npm ci && node smoke.mjs --base https://<Pages 주소>/<저장소>/ --local-root ../../../build/pages)
python3 tool/i18n/check_parity.py --strict  # 11개 로케일 번역 패리티

tool/bdd/generate.sh                      # .feature · @TypedGoRoute 생성물
tool/bdd/generate.sh feature/<d>/app      # 그 패키지만(--check <패키지> 는 그 경로만 판정 · 모르는 옵션 · co_bdd 밖 경로는 exit 2)
tool/golden/capture.sh && tool/golden/gallery.sh   # 골든 캡처 → build/golden-gallery/index.html
python3 tool/serverpod/profile.py show    # serverpod 버전 · 프로필

알아 둘 것

CI

워크플로: ci.yml(게이트 · 생성물 · 분석 · 테스트 · 웹 · Android) · golden.yml(골든) · pages.yml(6표면 빌드 · 배포 · 공개 스모크). 무엇을 언제 도는지는 각 파일 머리 주석.

이벤트 CI Golden Pages
초안 PR 계약 게이트 · 코드 생성 · generated · analyze compare 없음 빌드 없음(pages 라벨로 요청 가능)
시험 코드(*_test.dart, 게이트 시험)만 바뀐 PR 코드 생성 생략, generated · analyze 실행(준비 완료면 나머지 전체 검증도 실행) 초안이면 compare 없음 배포 조립 입력 변경이 없으면 빌드 없음
준비 완료 PR 변경 범위에 따른 전체 검증. 성공하면 ci · android 검증 기록 골든 (compare) 성공 시 golden 검증 기록 배포 조립 입력 변경 또는 pages 라벨 때만 빌드
main push 같은 트리의 검증 기록이 있으면 해당 범위를 생략, 없으면 전체 검증 golden 기록이 있으면 생략, 없으면 compare 사이트 빌드 → 배포 (GitHub Pages) → 공개 스모크
workflow_dispatch evidence=true(기본) 기록을 무시하고 전부(false 면 Android 만 뺀다) 기록을 무시하고 compare 빌드 · 배포 · 공개 스모크(false 면 빌드만)
[update-goldens] 커밋(main 밖 브랜치) · dispatch update_goldens=true — 골든 (update) — 기준 이미지를 다시 만들어 그 브랜치에 커밋 —

main push · dispatch 의 run 은 서로 취소하지 않는다(PR 만 새 push 가 앞선 run 을 취소한다). module-codegen 이 생성과 드리프트 검사를 한 번 수행한다. 산출물은 모두 Git 추적 파일이므로 검증 레그는 같은 커밋을 checkout 해 쓴다. generated 레그는 작업 트리와 포맷을 확인한다. build_runner 캐시는 생산 잡에서만 선택적으로 복원·저장한다. PR 경로 필터는 마지막 커밋이 아니라 PR 전체 변경을 본다. 전체 PR 에 코드 변경이 있더라도 생산 잡의 캐시에 같은 생성 입력의 성공 기록이 있으면 생성기를 생략한다. 입력은 Git 추적 파일의 blob 으로 비교하며 시험·문서만 제외한다. 캐시가 없거나 입력이 달라지면 전체 생성한다. workflow_dispatch 는 기록을 재사용하지 않고 다시 생성한다.

초안 PR 로 열고 변경 패키지를 로컬에서 검사한 뒤 준비 완료로 전환한다. 준비 완료 이벤트의 새 CI run 이 전체 검증을 마칠 때까지 기다린다. 검증 기록 조회 실패나 트리 불일치는 전체 검증으로 돌아간다.

CI 증거 게이트 ↔ 잡 이름

데모 증거 대장(#637)은 게이트마다 (run, 잡 이름)을 적는다. main HEAD 의 성공한 run 이 해당 잡을 건너뛰면, main HEAD 와 같은 트리의 검증 기록이 가리키는 성공한 run 의 같은 이름 잡을 확인한다. 배포본을 여는 locale · surfaces · smoke 는 기록으로 대체하지 않는다. 같은 SHA 를 전부 다시 검증할 때는 gh workflow run ci.yml --ref main -f evidence=true(golden.yml · pages.yml 도 같은 입력). 잡 이름은 고정이다 — 정본은 tool/ci/evidence_jobs.json, 이름 · matrix 레그를 바꾸면 이 표와 함께 바꾼다 (tool/gates/check_evidence_jobs.py 가 어긋남 · 중복 · 식이 남은 이름을 막는다). 배포본을 여는 게이트(locale · surfaces · smoke)는 배포 잡(actions/deploy-pages) 뒤에 실제 주소를 여는 잡에만 묶는다 — 빌드 잡의 성공은 배포본에 표면이 있다는 증거가 아니다(실측: 사이트 빌드 는 성공했는데 공개 widgetbook/ · docs/ 가 404 였다).

게이트 워크플로 잡 이름(문자열 그대로) 무엇 · 대장 조건
unit CI 검증 (tests-1) (+ 검증 (tests-2) · 검증 (tests-3) · 검증 (tests-modules)) 모든 패키지 테스트(패키지 라운드로빈 샤드) · 기능 서버 단위(TZ=UTC) — run 전체 성공이 다른 샤드를 보증
widget CI 검증 (tests-1) (+ 검증 (tests-2) · 검증 (tests-3)) 같은 샤드의 위젯 테스트(실제 mount · RTL · 언어 변경 중 상태 보존)
golden Golden 골든 (compare) 셀프호스트 macOS 러너 렌더러 기준 비교 — 대장은 mode=compare 만 인정(골든 (update) 는 증거가 아니다)
widgetbook CI 검증 (widgetbook) Widgetbook 패키지 테스트(TEST_SUITE=widgetbook) — 웹 산출물은 사이트 빌드 가 만들고, 배포본 /widgetbook/ 은 공개 스모크 가 연다
bdd CI 검증 (bdd) co_bdd widget 시나리오(TEST_SUITE=bdd) — 생성물 최신은 검증 (generated)
patrol — — 커널 CI 에 없다 — 실행 앱 여정이 있는 데모가 기기 잡(Patrol (<기기>) 권장)을 더하고 device 를 적는다
locale Pages 공개 스모크 배포본(runtime localOnly) iframe 실시간 전환 — 다시 불러오지 않고 UI 문구 · 투영 데이터가 바뀐다
backend CI 검증 (tests-server) 호스트 서버 통합(내장 Postgres · runtime server) + 선언된 hostTests
surfaces Pages 공개 스모크 같은 커밋(pages-commit) 배포본의 6표면(랜딩 · app · console · widgetbook · golden · docs)을 실제로 연다 — 표면마다 200 · 그려짐 · 콘솔 오류 0 · 바깥 · 깨진 요청 0 · 새로고침 뒤 200. 사이트 빌드(빌드 + 배포 전 스모크)는 증거가 아니다
smoke Pages 공개 스모크 같은 커밋(pages-commit) 배포본의 6표면 · 첫 프레임 전 상태 · iframe 전환(runtime localOnly)

그 밖의 고정 이름: 변경 범위 · 계약 게이트 · 번들 모듈 → 호스트 코드 생성 · 검증 (generated) · 검증 (analyze) · 검증 (web) · Android 빌드 (debug) · 사이트 빌드 · 배포 (GitHub Pages).

커밋

한국어 Conventional Commits + gitmoji: <type>(<scope>): <gitmoji> <설명>. 예: feat(app): ✨ 부트스트랩 단계 마커 추가, fix(gates): 🐛 정체성 게이트가 바이너리를 읽던 문제.