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/sonax_care_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 에 올린다.

CLI 입력 provenance 는 코드/플랫폼 정체성 노출과 구분한다. 게이트가 허용하는 것은 generation-lock.json 의 문자열 필드 delivery.input.expanded_spec · delivery.input.scaffold_spec · delivery.input.options.organization 세 곳뿐이다. 다른 필드 · JSON 키 · 다른 파일은 계속 검사하고, 잘못된 JSON/필드 타입은 거절한다.

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/sonax_care_server/lib/src/generated/** · backend/sonax_care_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 의 포맷 단계 참고).

새 프로젝트의 이름·브랜드·번들 글꼴이 커널과 다르면 제공된 골든 기준 이미지를 그대로 비교할 수 없다. 첫 CI(macOS 러너) 기준선은 아래 「첫 프로젝트 골든 기준선」 절차로 diff를 검토한 뒤 만든다. capture는 compare 성공이 아니다.

번역 접근자는 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/sonax_care_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(7표면 빌드 · 배포 · 공개 스모크). 무엇을 언제 도는지는 각 파일 머리 주석.

러너는 전부 조직 셀프호스트 macOS(#637 S6 · D-11 ①) — runs-on: [self-hosted, macOS, ARM64, flutter], 호스티드 0. 잡마다 checkout 뒤 .github/actions/selfhost-prep(Python ≥ 3.10 · 러너 tool 캐시의 Flutter · 러너별 PUB_CACHE $HOME/actions/_cache637/pub-<러너> + hosted/ 공유 풀 pub-shared · 공유 npm/playwright/gradle 경로), 끝에 if: always() 로 .github/actions/selfhost-cleanup(이 잡의 작업 디렉터리 · 내장 Postgres 만). 한 기계에서 다른 저장소 잡이 함께 돌므로 포트를 고정하지 않는다(tool/ci/free_ports.py → SERVERPOD_DATABASE_PORT), verify 는 max-parallel: 3. 러너 · 라벨 · 그룹 설정과 러너의 _tool/pub-cache-*(다른 프로젝트 것)는 건드리지 않는다. 새 잡도 이 준비 · 뒷정리를 따른다. Docker 가 필요한 잡이 생기면 라벨 docker 를 더하고 컨테이너는 고유 이름 · 빈 포트로 띄워 잡 끝에 지운다.

이벤트 CI Golden Pages
초안 PR 가벼운 층만 — 계약 게이트 · 변경 범위 · 번들 모듈 → 호스트 코드 생성 · 검증 (generated) · 검증 (analyze) — 라벨 pages 일 때만 사이트 빌드
준비 완료 PR 변경 범위로 가른다(문서만이면 계약 게이트 만, Android 는 네이티브 · 의존성 변경 때만) → 전부 성공하면 검증 기록 골든 (compare) → 검증 기록 배포 조립 입력이 바뀌었거나 라벨 pages 일 때 사이트 빌드(배포 전 스모크 포함)
main push 같은 트리의 검증 기록(ci · android)이 있으면 건너뛴다 — 없으면 전부(변경 범위 판정 없음 · Android 포함) 검증 기록 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 을 취소한다).

검증 기록 · 2층 CI

같은 내용을 두 번 검증하지 않는다. 검증이 전부 성공하면 검증 기록 남기기 잡이 그 트리에 refs/ci-receipts/<트리>/<범위>/<날짜>(주석 태그 → 검증한 커밋 · run)를 남기고, 같은 트리를 다시 검증하려는 실행 (머지 뒤 main push)은 검증 기록 조회 잡이 그 기록을 보고 검증 잡을 건너뛴다. 트리 해시는 파일 내용만으로 정해져, PR 이 검증한 머지 미리보기의 트리는 base 가 그대로면 머지 뒤 main 커밋의 트리와 같다. base 가 움직였거나 조회가 실패하면 그대로 검증한다 — 판단이 틀려도 검증을 더 하는 쪽으로만 틀린다. 범위는 ci(Android 를 뺀 검증 전부) · android · golden. 도구는 .github/actions/ci-receipt/(원본 coco-de/skills ci-receipts — 고치지 말고 원본을 판 올림).

# 이 트리를 이미 검증했나(로컬)
GH_TOKEN="$(gh auth token)" python3 .github/actions/ci-receipt/ci_receipt.py --repo <owner>/<저장소> check --scope ci,golden --sha main

무거운 검증은 머지 직전에 한 번. 초안(draft) PR 은 가벼운 층만 돈다(위 표). 그래서 PR 은 초안으로 일찍 열고, 다 됐으면 로컬 점검(tool/gates/check_all.sh · dart analyze --fatal-infos · 바꾼 패키지 테스트 tool/dev/test_all.sh)이 통과한 뒤 gh pr ready 로 준비 완료를 만들어 무거운 층을 한 번 돌린다. 초안은 머지할 수 없으니 무거운 잡이 건너뛴 「성공」이 머지 경로를 열지 않는다.

독립 pub 루트

python3 tool/pub_roots.py list는 실제 pubspec과 workspace 멤버 선언에서 독립 루트를 찾는다. 기본 CI는 analyze·generated 잡에서 prepare로 각각 의존성을 준비하고, analyze 잡은 analyze로 그 루트의 자체 분석까지 실행한다. 별도 사이트가 없어도 기존 workspace만 준비하며 경로 제외는 없다.

첫 프로젝트 골든 기준선

  1. 최초 골든 (compare) 실패의 golden-failures 아티팩트에서 기준·실제·diff PNG를 내려받는다.
  2. 화면별 차이를 검토하고 무엇이 왜 바뀌었는지 기록한다. kernel_status의 프로젝트 제목처럼 정체성 치환으로 달라진 부분과 신규 feature의 기준선 부재를 구분한다. 의도하지 않은 레이아웃·문구·글꼴 문제는 먼저 고친다.
  3. 검토한 브랜치에서 기존 Golden 워크플로를 실행한다: gh workflow run golden.yml --ref <branch> -f update_goldens=true. CI 셀프호스트 macOS 러너가 feature 소유 test/golden/goldens/를 만들고 같은 브랜치에 봇 커밋한다. 로컬 capture를 기준으로 옮기지 않는다(같은 Darwin/arm64 라도 기준은 CI 러너가 만든 것만).
  4. 갱신된 커밋에서 gh workflow run golden.yml --ref <branch> -f evidence=true로 compare를 실행한다. 그 커밋의 골든 (compare) 성공만 증거로 기록한다. update 성공·capture 성공·threshold 변경·skip은 대체 증거가 아니다.

CI 증거 게이트 ↔ 잡 이름

데모 증거 대장(#637)은 게이트마다 (run, 잡 이름)을 적고, main HEAD 와 같은 SHA 의 성공한 run 안에서 그 이름의 잡 하나가 성공했는지, 또는 main HEAD 트리의 검증 기록(ci · golden)이 가리키는 run 안에서 같은 이름의 잡이 성공했는지 본다(트리 · 커밋 · run · 잡을 대조한다 — 다른 트리의 run 은 인정하지 않는다). main push 는 PR 이 같은 트리를 검증했으면 검증 잡을 건너뛰고, 대장의 증거 주소는 실제로 검사가 돈 run(PR run)을 가리킨다. 기록이 없으면 main push 에서 위 표대로 전부 돈다. 같은 SHA 를 다시 돌릴 때는 gh workflow run ci.yml --ref main -f evidence=true(golden.yml · pages.yml 도 같은 입력 — 기록을 보지 않고 다시 돈다). 배포본을 여는 게이트(locale · surfaces · smoke)는 기록으로 대신하지 않는다 — main push 마다 배포 뒤 공개 스모크 가 돈다. 잡 이름은 고정이다 — 정본은 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-modules)) 모든 패키지 테스트(패키지 라운드로빈 샤드) · 기능 서버 단위(TZ=UTC) — run 전체 성공이 다른 샤드를 보증
widget CI 검증 (tests-1) (+ 검증 (tests-2)) 같은 샤드의 위젯 테스트(실제 mount · RTL · 언어 변경 중 상태 보존)
golden Golden 골든 (compare) 셀프호스트 macOS(Darwin/arm64) 렌더러 기준 비교 — 대장은 mode=compare 만 인정(골든 (update) 는 증거가 아니다)
widgetbook CI 검증 (widgetbook) Widgetbook 패키지 테스트(TEST_SUITE=widgetbook) — 웹 산출물은 사이트 빌드 가 만들고, 배포본 /widgetbook/ 은 공개 스모크 가 연다
bdd CI 검증 (bdd) co_bdd widget 시나리오(TEST_SUITE=bdd) — 생성물 최신은 검증 (generated)
patrol CI Patrol (chrome) 실제 app·console 셸 여정(localOnly) · headless · ko-KR · Playwright 1.56.0 · 각 .feature 시나리오 수 = 통과 수(device=chrome), 0 tests·skip·flaky 실패
locale Pages 공개 스모크 배포본(live Serverpod) iframe 실시간 전환 — 다시 불러오지 않고 예약 문구가 바뀐다. 실제 고객 기록은 유지된다
backend CI 검증 (tests-server) 호스트 서버 통합(내장 Postgres · runtime server) + 선언된 hostTests
surfaces Pages 공개 스모크 같은 커밋(pages-commit) 배포본의 7표면(랜딩 · site · app · console · widgetbook · golden · docs)을 실제로 연다 — 표면마다 200 · 그려짐 · 콘솔 오류 0 · 바깥 · 깨진 요청 0 · 새로고침 뒤 200. 사이트 빌드(빌드 + 배포 전 스모크)는 증거가 아니다
smoke Pages 공개 스모크 같은 커밋(pages-commit) 배포본의 7표면(site 포함) · 첫 프레임 전 상태 · iframe 전환(runtime live) · HTTPS API readyz
fr CI FR 대조 (fr) docs/sonax-prd.md의 17 FR · 17 AC에 연결한 실제 테스트 · 공개 SHA는 Pages 증거와 함께 대조

FR 은 PRD 스냅샷(tool/fr/prd_snapshot.json)과 coverage.json 을 채운 생성 데모만 증거 표에 fr → FR 대조 (fr) 를 등록한다. PRD 없는 커널에는 fr 항목이 없고 잡은 skipped 이므로 요구사항 검증 통과로 집계하지 않는다. 입력 둘 중 하나만 있거나 스냅샷 없이 fr 을 등록하면 check_evidence_jobs.py 가 실패한다. 도입 절차 · 계약은 tool/fr/README.md.

Patrol 정본은 app/{app,console}/integration_test/*.feature 이며 co_bdd 는 Patrol 만 생성한다. tool/bdd/generate.sh app/app app/console 로 다시 만들고 생성물은 포맷하지 않는다. 기존 feature-local widget-only BDD 는 별도 bdd 증거다. 실제 셸 bootstrap 을 실행한 뒤 포털과 같은 AppPresentation.requestLocale 로 ko→en→ar 전환을 검사한다(서버 요청 없음).

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

커밋

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