TypeScript Boundary Gate
이 문서는 Barbelic React 코드와 Supabase 공용 worker 코드의 TypeScript suppression·명시 any 정책을 고정한다.
핵심 원칙은 검사 대상 전체에서 TypeScript suppression을 금지하고, 활성 앱의 명시적 any 예외를 두지 않는 것이다. A16 최종 상태에서 남은 명시 any 허용은 기존 서버 와드업 입력 경계 2파일뿐이다.
왜 규칙을 없애지 않는가
@ts-nocheck와 무심한 any는 다음 영역에서 실제 장애로 이어지기 쉽다.
- Supabase RPC 응답 shape 변경
- import/stats/sync/auth 같은 비동기 상태 전이
- repository/controller/domain 경계에서 raw row가 화면까지 새는 문제
- 로그인 실패와 데이터 로딩 실패가 섞여 보이는 문제
- 대량 데이터에서 화면별 summary 계약이 깨지는 문제
따라서 boundary gate는 계속 필요하다. 디자인 전달물도 전달 단계의 preview harness에서 타입을 확인한다. 외부 값은 소유 codec/decoder에서 검증하고 실제 DTO·편집 입력·표시 모델로 연결한다. unknown이나 더 넓은 별칭·단언으로 any 사용처를 숨기는 것은 경계 이식으로 인정하지 않는다.
공식 명령
- 공식 명령:
npm run check:ts-boundary-gate - 이전 이름 호환 alias:
npm run check:ts-strict-gate - 기본 검증:
npm run check안에서 boundary gate를 실행한다.
strict gate라는 이전 이름은 "모든 presentation file을 항상 완전 타입화한다"는 오해를 만들 수 있어, 새 문서와 package script에서는 boundary gate를 기준 이름으로 사용한다.
A16 최종 구현과 허용 목록
2026-09-11 앱 c9bed324 기준이다. 실행 스크립트는 src/react와 supabase/functions/_shared 아래 모든 .ts·.tsx를 순회한다.
- TypeScript AST에서
ts.SyntaxKind.AnyKeyword를 검사한다. 타입의any, 배열·generic 인자·함수 타입 안의any도 대상이다.{ any: [...] }같은 실제 데이터 키나 문자열을 명시any로 세지 않는다. - TypeScript가 인식한
@ts-nocheck,@ts-ignore,@ts-expect-error지시문을 검사한다. suppression 허용 목록은 없으며 아래 두 파일에도 적용한다. - 명시
any허용 목록은 파일별 사유를 가진explicitAnyAllowedFiles다. 존재하지 않는 파일을 가리키면 실패한다. 허용된 두 파일 안의AnyKeyword개수를 2개로 제한하는 게이트는 아니다.
| 영역·파일 | 최종 허용 상태·목적 | 제거 조건 |
|---|---|---|
src/react | 명시 any 0개, 허용 0파일. UI·controller·domain·repository와 codec 모두 포함 | 활성 앱 예외를 추가하지 않고 실제 공급자/소비자 타입과 검증 경계를 수정 |
supabase/functions/_shared/wodup-import-worker.ts | 기존 와드업 worker 외부 입력 경계. A16에서 인입 계약을 바꾸지 않음 | 지원 중인 인입 형태를 명시 모델·검증으로 연결해 파일의 명시 any가 없어지고 기존 원문·오류·처리 의미를 확인한 뒤 목록에서 제거 |
supabase/functions/_shared/wodup-normalizer.ts | 기존 와드업 raw JSON normalizer. 구형 인입 의미 보존 | 지원 raw/exporter 형식을 decoder에서 확정해 명시 any가 없어진 뒤 목록에서 제거. 구형 입력 지원을 타입 정리 편의로 폐기하지 않음 |
허용 파일 수는 Phase 1 63 → 최종 2, 활성 앱은 0이다. AST gate는 명시 any와 suppression을 막는 검사다. 암묵적 타입 오류는 별도 check:types가 검사하며, unknown 검증의 충분성·단언의 정당성·실제 import 방향·런타임 입력 보존까지 이 게이트 하나로 증명하지 않는다. 타입·import 경계·행동 검사와 함께 판단한다. 관리자·와드업의 #1478 자동 제외 정책도 이 허용 목록과 별개이며, 제외 검사를 통과로 집계하지 않는다.
항상 type-check 되어야 하는 영역
아래 영역은 // @ts-nocheck, // @ts-ignore, // @ts-expect-error를 허용하지 않는다.
- app/container:
appController,mobileApp,desktopApp, Vite roots - auth/profile/session/pr/admin/import controllers
- domain services,
barbelicApi.ts,domains/servicePorts.ts - 실제 repository factory/조립,
screenRpcRuntime,repositoryTransport, RPC adapters, domain codecs/projections, validators - Supabase Edge Function shared workers
- direct workout writes, draft cache, stats reconciliation, Wodup upload logic
- mobile UI screens and fixtures
- desktop admin, favorite-group editor, Home, journal, log table, shared widgets, training report, PR, import, mapping, onboarding, profile, search, plan editor, workout flow, preview harness, shell-adjacent screens
- shared UI/contracts/utilities
이 영역에서 타입 오류가 생기면 suppression을 붙이지 말고 타입/adapter/contract를 수정한다.
A16에서 책임을 실제 소유자로 이관한 뒤 barbelicRepository.ts, domains/repositoryPorts.ts, barbelicMappers.ts, barbelicViewMappers.ts, services/index.ts를 퇴역했다. 각 domain은 repositoryComposition.ts의 필요한 인스턴스와 해당 codec 함수를 직접 import하고, import/admin은 각자의 composition을 유지한다. feature 표시 투영은 자신의 feature 또는 실제 공용 소유자로 옮겼다. 삭제된 이름으로 검사 범위를 설명하거나 같은 거대 객체의 호환 barrel을 다시 만들지 않는다. 세부 소유자와 테스트 전용 oracle은 A16 재고의 최종 책임을 따른다.
Design-intake 영역
현재 // @ts-nocheck 예외는 0개다. 2026-08-19부터 preview harness·fixture는 Claude Design 로컬 전용으로 이 레포에 존재하지 않아 확인 대상에서 제외됐다 (docs/process/design-codex-transfer-contract.md).
Design surface 화면은 다음 전제를 계속 만족해야 한다.
- 화면은 pure presentation이어야 한다.
- Supabase, repository, controller, domain service, persistent storage를 직접 import하지 않는다.
- 데이터는 app container가 props로 주입한다.
- intent는 callback으로만 밖으로 내보낸다.
- UI import boundary test와 design marker test는 계속 통과해야 한다.
경계를 변경할 때의 규칙
현재 design-intake suppression·활성 앱 명시 any 예외는 모두 0개다. 외부 데이터 경계를 변경하는 PR에는 다음을 남긴다.
- 실제 입력 작성자·지원 버전·소비자와 검증 소유자
- 검증 뒤 사용하는 유한 DTO·callback·표시 모델
- 원문·0·null·ID·순서·오류 동작을 보존하는 행동 검사
- 보존할 호환 경계의 목적과 구체적인 제거 조건
편의를 위해 예외를 재도입하지 않는다. admin ops, workout flow, repository, controller, domain, RPC, import/sync/auth는 design-intake 예외 대상이 아니다. 지원 버전의 기기 사본 decoder나 저장 provenance 읽기는 외부 값 확인 책임으로 유지할 수 있으나 내부 모델 전체를 이중 표기로 읽는 근거가 되지 않는다.
dual-key 래칫과의 관계
npm run check:dual-key는 별도의 scripts/check-dual-key-tolerance.mjs와 dual-key-baseline.json으로 검사한다. A16 최종 값은 8파일/14곳이다. 파일별 증가뿐 아니라 감축 후 기준선 미갱신도 실패한다. 확인된 감축은 node scripts/check-dual-key-tolerance.mjs --update로 반영하며 숫자를 손으로 낮추지 않는다.
이 검사는 camel/snake 형태가 이어지는 표현식의 정규식 매치 수다. 서로 다른 RPC·카탈로그·입력 초안 사이의 fallback, 서버 기준일과 S.dateKey() 호출, JSONL 바깥 행과 original 사본 사이도 잡힌다. 따라서 14곳을 같은 객체의 이중 키 14개 또는 남은 모든 호환 경계의 총수라고 부르지 않는다. 14곳의 실제 공급원·보존 자격·제거 조건을 기준으로 판단한다.
현재 UI의 load 단위 계약은 canonical 필드만 읽는다. bodyweightTraining.ts의 제한된 저장 provenance 읽기는 저장된 raw 세트와 canonical 세트의 단위 계보·사용자 kg 정정 우선순위를 보존한다. 서버·기기 사본의 실제 공급자를 옮기지 않고 이 세 정규식 매치만 지우면 통계 반올림 의미가 달라질 수 있다. 이 경계를 전체 JSON 스키마 검증이나 제품 내부의 범용 별칭 허용으로 해석하지 않는다.
검증
변경 후 최소 검증:
npm.cmd run check:ts-boundary-gate
npm.cmd run check
npm.cmd run build디자인 전달 PR 후속에서는 check:ts-boundary-gate 실패가 "디자인 화면 자체의 타입 오류"인지 "core boundary 침범"인지 먼저 구분한다.
A16 Phase 3 최종 로컬 npm run check는 c9bed324 기준 3,701건 중 3,609 통과·0 실패·92 제외, 94,518ms다. 이 결과는 release 병합·Production 배포·실서비스 입력 검증을 뜻하지 않는다. Phase 1·2 당시 수치와 검사 결과는 A16 타입 경계 재고에 별도 보존한다.