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).
- 라우트:
app_routes.dart의 목록 — cobRouteRegistrar가<Name>RouteName.base,를 꽂는다. - DI:
register_app_dependencies.dart— cobDiRegistrar가register<Name>Module();과 import 를 꽂는다. 이 파일에 import 가 하나도 없으면 compose 가 파일 맨 앞에 import 를 넣으므로library지시문을 두지 말 것. - 모듈 계약: README 「모듈 계약」 표가 정본이다. compose 가 만든 feature 는 그 심볼 · 경로를 가정하므로, core · dependencies 의 재수출을 줄이면 feature 가 깨진다.
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. 화면 · 파일 · 라우트 규약 (게이트가 막는다)
- 한 파일 = 공개 타입 하나, 파일 이름 = 타입 이름 snake_case —
check_one_class_per_file.py. 유즈케이스 여러 개를 한 파일에 두지 않는다. 예외(생성물 · part · 테스트 · private 보조 타입 · sealed 가족 · 사유 주석)는 그 게이트 머리 주석이 정본이다. - StatefulWidget 금지 — 화면 · 위젯은
HookWidget+ signals_hooks(useSignal·useComputed·useFutureSignal)로. 불가피하면tool/gates/stateful_widget_allowlist.txt에 사유와 함께 —check_no_stateful_widget.py. - 라우트는
@TypedGoRoute— 문자열 경로로context.go('/x')하지 말고const XRoute().go(context). 모듈 라우트는<Name>RouteName.base로AppRoutes.routes마커에 꽂힌다(계약 유지). 웹은 경로 URL 이라 같은 경로가 유니버설 링크 · 앱 링크로도 열린다(app_link_domain). - 새 feature 는 BDD 시나리오와 골든을 함께 —
test/src/bdd/*.feature(co_bdd) ·test/golden/(co_golden). 폴더 규약은 README 「feature 가 따를 규약」. - 사내 패키지를 먼저 찾는다 — 그리기 · 주석은 co-package
open_board, 뷰어는open_epub등. 직접 CustomPainter 로 다시 짜기 전에gh api repos/coco-de/co-package/contents/packages를 본다.
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 버전 · 프로필
알아 둘 것
-
신규 feature 는 bundle —
.cob/feature-layout.json을 보존하고feature/<domain>/{app,console,server,client,widgetbook}에 둔다.bundle.json은 조립 선언이고, 이름 예외·호스트 포트·식별자/legacy 계약은host-integration.json이 소유한다. 번역은 기능의lib/src/i18n/에 두고featureTranslations로 셸 로케일에 연결한다(README 계약 절). -
계층 래칫은 올리지 않는다 —
dependency_layers.json은 미분류 패키지·런타임 역방향·순환을 막는다.check_package_layers.py --update-baseline은 감소만 허용한다. Widgetbook 은 기능 소유 전시 패키지 +app/app_widgetbooksink 로 분리하고 운영 역할이 전시 코드를 의존하지 않는다. -
검증 스크립트의 종료 코드 77 은 「검증 못 함」이다.
tool/golden/compare.sh는 Darwin/arm64 가 아니면(GOLDEN_LOCAL=1이 아니면),tool/bdd/generate.sh --check는 git 작업 트리가 아니면 비교하지 않고 77 로 끝내며 마지막 줄에 사유를 남긴다.cob verify는 이것을 통과가 아니라⏭ … — skip(사유)로 보고한다. 스크립트를 이어 부를 때 77 을 통과로 삼키지 말 것(0 · 77 밖의 코드는 실패). -
Flutter 3.47 은
pub get때 Flutter 패키지의analysis_options.yaml에build/**(앱은 플랫폼 디렉토리까지) 제외를 스스로 넣는다. 커밋본에 이미 들어 있어야 매번 바뀌지 않는다 — 새 Flutter 패키지를 만들면 기존 패키지의 형태를 따를 것. -
간결 생성자 문법(Dart 3.13)을 쓰지 않는다.
unnecessary_type_name_in_constructor·empty_container_bodies를 루트analysis_options.yaml에서 껐다. 모듈과 코드젠(serverpod_cli · build_runner)이 고전 문법에 머물러 있어서다. 생태계가 옮겨 가면 함께 켠다. -
서버의
Serverpod는 생성된 클래스다.lib/src/generated/serverpod.dart가package:serverpod/serverpod.dart를Serverpod만 숨겨 다시 내보낸다.server.dart에서package:serverpod/serverpod.dart를 함께 import 하면 이름이 겹친다. -
coui 는 비공개 저장소 태그(coui_flutter-v0.159.2 · coui_core-v0.158.1)로 고정한다 — 루트
dependency_overrides. 두 패키지는 반드시 함께 올린다(짝 태그 규칙은 루트 pubspec 주석).tool/gates/check_coui_pin.py가 원격 최신 태그와 비교해 5개 이상 뒤처지면 경고한다(CI 는 경고만,--strict면 실패). coui 0.148 부터 file_picker 12 를 직접 써서 예전의 file_picker · win32 · device_info_plus 덮어쓰기는 지웠다 — 다시 넣지 말 것. coui 를 올리면 웹 빌드뿐 아니라 CI 의 Android 빌드 잡까지 초록인지 볼 것(네이티브 층 깨짐은 웹·테스트로 드러나지 않는다). -
Android 는 Gradle 8.14 · AGP 8.12 에 머문다(
android/settings.gradle.kts). coui 0.131 이 끌던 irondash_engine_context(cargokit) 가 Gradle 9 에서 없어진project.exec를 불러 APK 빌드가 깨졌다(coui 0.151 에서는 그 의존이 빠졌다 — Gradle 9 로 올릴 때 Android 빌드 잡으로 확인할 것). 웹 빌드와 테스트는 네이티브 층을 컴파일하지 않아 이런 깨짐을 못 잡는다 — CI 의 Android 빌드 잡이 그 자리를 지킨다. -
core 를 import 한 파일에는 material 을 함께 import 하지 않는다. coui 가
Scaffold·Theme·ThemeData·Colors를 따로 정의해 겹친다. 커널 화면도 coui 로 쓴다(테스트는buildFeatureTestApp또는CoUIApp아래에서 — coui 위젯은 couiTheme이 없으면 어서션한다). -
한글 글꼴은 번들한다 — coui 본문 글꼴
Pretendard(OFL)를 KS X 1001 2350자 서브셋(굵기 4종 · 각 ~410KB)으로package/resources/lib/fonts/에 두고 앱 pubspec 이 가족 이름Pretendard로 선언한다. 웹 첫 로드에 CDN 글꼴을 기다리며 □ 가 보이거나 라벨이 잘리지 않는다. 목록 밖 글자는 엔진 폴백이 받는다. 브랜드 글꼴로 바꾸면 같은 자리를 바꾼다(골든 하네스도 이 파일을 읽는다). -
flutter build web에는 늘--no-wasm-dry-run— release 웹 빌드의 wasm 시험 컴파일이 호스티드 러너(2vCPU · 8GB)를 메모리로 죽인다(실측, CI · Pages 모두). -
시계는
AppClock.now()—DateTime.now()를 화면에서 직접 읽으면 골든이 날마다 바뀐다. 골든 하네스가goldenNow로 고정한다. 나이 계산처럼 기준 시각이 필요한 곳도 이것을 쓴다. 렌더 결함을 당장 못 고치면kernelGoldenMatrix(knownDefect: '사유')로 표시하고 비교는 계속한다. -
서버 없는 데모(localOnly) —
COB_RUNTIME_MODE=localOnly면 부트스트랩이 원격 연결 대신DemoWorld(core)를 등록하고, 모듈은register_demo_world.dart의AUTO-REGISTER(demo-seed)자리에 시드를 꽂는다. 모듈의 localOnly 저장소는DemoWorld를 읽고 쓴다. 변경 신호는 실제 쓰기에만(읽기에서 신호를 내면 구독자가 다시 읽어 브라우저가 멈춘다), 시드 이력은isSeed항목에만. 데모 전용 앱은--dart-define=COB_DEMO_ONLY=true로 빌드해 localOnly 가 아니면 "서버 없음" 으로 끝낸다(조용한 폴백 금지). -
언어 · 테마는 셸 상태 하나 —
AppPresentation(core) — 앱 · 콘솔 · 위젯북 셸이runApp전에 URL (?lang=·?theme=)을 첫 값으로 싣고, localOnly 는 데모 포털 미리보기 iframe 의sync를DemoEmbedSync(allowLocalhost: kDebugMode)로 받는다. 언어 전환은DemoLocaleSwitcher(마지막 요청 우선)의 확정 한 단계에서 slang ·DisplayProjector·<html lang dir>이 함께 바뀐다. 받은 값은 저장하지 않는다. 화면에서LocaleSettings.setLocale을 직접 부르지 말 것 —sl<AppPresentation>().requestLocale(...). -
웹 첫 프레임 전 배경(pre-boot) —
tool/pages/catalog.tsv4열의 Flutter 호스트에 있는web/index.html의 블록은 생성물이다(원천tool/web/pre_boot.js+AppTheme.scaffoldBackgroundOf). 손으로 고치지 말고app/app에서UPDATE_PRE_BOOT=1 flutter test test/web/pre_boot_test.dart. 판정 규칙은tool/web/pre_boot_cases.json을 Dart · JS 테스트가 함께 본다. -
다국어 폴백 글꼴 — 웹 호스트가
LocaleFallback-*.ttf를Roboto가족으로 선언한다(엔진의 전역 폴백 자리). 이름을 바꾸면 엔진이 아랍 · 한중일 글꼴을 fonts.gstatic.com 에서 받는다(공개 스모크가 잡는다). -
Pages 사이트 —
tool/pages/build.sh가build/pages/(랜딩 +/app/+tool/pages/surfaces/*.sh산출물, 커널은/console/·/widgetbook/·/golden/·/docs/— 7표면)를 만들고pages.yml이 main 에서 배포한다. Pages 는 SPA 폴백이 없어 데모 빌드는WEB_URL_STRATEGY=hash, 실서비스는 경로 URL(기본). 새 산출물은 surface 스크립트 하나 +catalog.tsv한 줄.catalog.tsv는 id · 제목 · 설명 · 서비스 Flutter 호스트 경로(선택) 4열이다. Flutter 표면은 실제 빌드하는app/<host>를 선언하고, docs/golden 같은 정적 표면은 3열만 쓴다. pre-boot 게이트는 이 선언만 검사하며, 선언한 호스트의 index.html/블록 누락은 실패다(폴더 유무로 표면을 추측하지 않는다). -
serverpod 버전은
tool/serverpod/serverpod.yaml한 곳에서 정한다(현재 stable 4.0.3). pubspec 을 손으로 고치지 말고python3 tool/serverpod/profile.py apply stable --version <x> --run—check_serverpod_version.py가 어긋남을 막고 최신보다 뒤처지면 경고한다. CLI 는tool/serverpod/cli.sh …(별도 pub 루트tool/serverpod/cli— serverpod_cli 의 analyzer <11 이 워크스페이스 코드젠을 묶지 않게), 전역 설치를 쓰지 않는다. 오프라인 동기화는apply offline-sync(베타 + co-serverpod 포크). 버전을 바꿀 때는 라이선스를 확인할 것 — 4.x 일부 발행본이 SSPL-1.0 이다. -
서버 · 클라이언트 이름은 identity.yaml
project_name에서 온다(backend/<project_name>_server). 그래서 이 두 패키지와 serverpod_service 는 import · pubspec 정렬 린트를 끈다(이름마다 알파벳 위치가 다르다). -
내장 Postgres 는 첫 실행 때 바이너리를 내려받아 사용자 캐시에 둔다. 데이터는
backend/sonax_care_server/.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 로 준비 완료를 만들어 무거운 층을 한 번 돌린다. 초안은 머지할 수 없으니 무거운 잡이 건너뛴
「성공」이 머지 경로를 열지 않는다.
- CI 를 시험 루프로 쓰지 않는다 — 실패는 로컬에서 재현 · 수정 · 확인한 뒤 push 한다.
- 플레이크는 실패 잡만 다시(
gh run rerun <run-id> --failed). 빈 커밋 push · 전체 재실행을 하지 않는다. - 러너는 셀프호스트 기계를 나눠 쓴다 — CI 가 도는 동안 같은 기계에서 무거운 로컬 실행을 하지 않는다.
독립 pub 루트
python3 tool/pub_roots.py list는 실제 pubspec과 workspace 멤버 선언에서 독립 루트를 찾는다.
기본 CI는 analyze·generated 잡에서 prepare로 각각 의존성을 준비하고, analyze 잡은 analyze로
그 루트의 자체 분석까지 실행한다. 별도 사이트가 없어도 기존 workspace만 준비하며 경로 제외는 없다.
첫 프로젝트 골든 기준선
- 최초
골든 (compare)실패의golden-failures아티팩트에서 기준·실제·diff PNG를 내려받는다. - 화면별 차이를 검토하고 무엇이 왜 바뀌었는지 기록한다.
kernel_status의 프로젝트 제목처럼 정체성 치환으로 달라진 부분과 신규 feature의 기준선 부재를 구분한다. 의도하지 않은 레이아웃·문구·글꼴 문제는 먼저 고친다. - 검토한 브랜치에서 기존 Golden 워크플로를 실행한다:
gh workflow run golden.yml --ref <branch> -f update_goldens=true. CI 셀프호스트 macOS 러너가 feature 소유test/golden/goldens/를 만들고 같은 브랜치에 봇 커밋한다. 로컬 capture를 기준으로 옮기지 않는다(같은 Darwin/arm64 라도 기준은 CI 러너가 만든 것만). - 갱신된 커밋에서
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): 🐛 정체성 게이트가 바이너리를 읽던 문제.