LogoSkills

kobic donor 모듈성 강화 로드맵

**빌드 속도는 이 작업의 근거가 아니다.** 이 리포는 이미 한 번 "아키텍처를 고쳐 빌드를

이관 문서 — 원출처: coco-de/unibook 리포 .claude/plans/donor-modularity-roadmap.md · 이관일: 2026-08-31 · 이관 사유: 외주 프로젝트 리포에서 내부 도구 문서 제거 (coco-de/unibook#12615). 본문 내 리포 상대 링크는 coco-de/unibook 리포의 <경로> 텍스트 표기로 바꾼 것 외에 원문 그대로 보존했다.

kobic donor 모듈성 강화 로드맵#

6축 병렬 감사(75 에이전트 / 1,813 툴콜) + 인라인 재검증 결과. 2026-07-29. 선행 문서: coco-de/unibook 리포의 .claude/plans/ancient-bubbling-hamster.md (CI 속도 실측), coco-de/unibook 리포의 .claude/rules/package-layers.md (계층 규약), dependency_layers.yaml (래칫 SSOT).

논지#

빌드 속도는 이 작업의 근거가 아니다. 이 리포는 이미 한 번 "아키텍처를 고쳐 빌드를 빠르게 하자"로 착수했다가 실측에서 임계경로 8%로 판정하고 방향을 틀었다. 이번 감사는 그 판정을 뒤집지 못했고 오히려 강화했다 — SCC 를 전부 절단해도 비싼 케이스(허브 접촉)의 역의존 폐포는 64→62(−6.9%)뿐이고, codegen 위상은 오히려 7티어→13단계로 늘며, 테스트 과소 커버리지는 순환이 아니라 core 팬인 탓이라 SCC 해소로 고쳐지지 않는다.

동기화가 근거이고, 그것만으로 충분하다. kobic 은 co-bricks 의 canonical donor 인데 실질 공급률이 바닥이다. 그리고 병목은 순환이 아니다 — 순환보다 싸고 큰 것이 셋 있다.

병목실측왜 치명적인가
게이팅 무력화 common→application 하드 의존 5건 + console→application 7건 cob 은 feature/application/*{{#has_X}} 로 게이팅한다. 게이트 없는 계층이 게이트 있는 계층을 pubspec 에 하드 선언하면, cob create --features <부분집합> 산출물이 없는 패키지를 가리켜 dart pub get 에서 죽는다
common 수확 경로 부재 monorepo 브릭의 feature/common/splash 하나뿐 . cob 에 feature/common 을 스캔하는 sync 함수도 없다 (application·console 만 존재) 도메인 토큰 밀도가 가장 낮은(=가장 범용인) 계층이 유일하게 수확 경로가 없다. auth · settings · withdraw · sign_in_with_email · life · extra_infoprebuilt 브릭이 존재하는데도 동봉·apply 어느 쪽도 안 된 고아다
공용 계층의 donor 오염 resourcespod.* 64회 , core8회 참조 디자인 시스템 패키지가 donor 백엔드 생성 모델에 묶여 브릭으로 나간다 → 수용 프로젝트에서 컴파일 불가

그리고 순환의 실제 피해는 여기서 나온다:

cc-bricks:feature-* 스킬 11종 중 8종이 10노드 SCC 안에 있다. ai_chat · book_content_reader · book_content_viewer · chat · my_library · my_page · search · store — 정작 브릭으로 뽑겠다고 스킬까지 만들어 둔 대상이 전부 단독 추출 불가다. (SCC 밖: app_router(preserved), book_content_search, publisher_settlement)


실측 기준선#

워크스페이스 패키지          78
max_scc                      10   (ai_chat auth book_content_reader book_content_viewer
                                   chat my_library my_page search sign_in_with_email store)
scc_count                     3   (10 / 콘솔 4 / core↔resources 2)
scc_edges                    32
layer_violations             71
phantom_edges               135
feature→feature pubspec     154   (console→console 53 · console→common 40 ·
                                   application→application 28 · application→common 11 ·
                                   common→common 10 · console→application 7 · common→application 5)

python3 .github/scripts/check_package_layers.py --report 로 재현.


Track A — 부분 조립 가능화 (게이팅 정합) 🔴 최우선#

목표: cob create --features <부분집합> 산출물이 dart pub get 을 통과한다. 가치: 이것이 통과하지 못하면 SCC 를 아무리 정리해도 브릭 조립은 여전히 0이다. 의존: 없음 — 즉시 착수 가능. Track C 와 독립.

#무엇을대상게이트
A1 common→application 하드 의존 5건 제거 auth→chat auth→store settings→my_page settings→store withdraw→store 5개 pubspec 에서 해당 줄 소멸 + melos run analyze
A2 console→application 7건을 enable_admin 게이트와 정합 console_annotation_viewer→book_content_viewer console_book_registration→book_content_reader console_member_list→chat console_router→chat console_sample_book→store publisher_settlement_management→publisher_settlement scm_instructor_management→store enable_admin=false 조합에서 pub get 통과
A3 {{#has_X}} 카탈로그 미등록 2건 등록 (cob 쪽 PR) known_sync_features.dart CatalogValidator 경고 0

A1 의 해소 패턴 (기존 선례 재사용, 새 설계 아님):

  • auth→chat = ContentTypeUtil/FileValidatorUtil 2개 → core 로 이관 (⚠️ 이 둘이 collection/file_picker 를 끌고 간다 — core pubspec 확인 필요)
  • auth→store / settings→store / withdraw→store = StoreRouteName 경로 상수만AuthRoutePaths 선례대로 core 의 경로 상수로 이관
  • settings→my_page = 라우트 객체 참조 → RoutePageRegistry 위임

⚠️ 경로 상수와 라우트 객체를 혼동하지 말 것. 감사에서 "라우트 상수 전용 L0 패키지로 5엣지 일괄 절단" 안이 나왔으나 반증됐다 — 5엣지 중 4개는 GoRouteData 서브클래스를 소비하고 그 buildPage() 가 피처 페이지를 직접 만든다. L0 패키지로 옮길 수 있는 건 path/name String 상수뿐이다.


Track B — 공용 계층 donor 오염 제거 🔴 브릭 컴파일 파손 차단#

목표: core·resources 브릭이 donor 백엔드 없이도 컴파일된다. 가치: 지금 상태로 sync 하면 컴파일이 깨진 브릭이 만들어진다. 조립 이전 문제다. 의존: 없음.

#무엇을대상게이트
B1 resourcespod.* 64회 제거 widgets/category/category_tree_selector.dart ( pod.BookCategory 24) · widgets/dialog/purchase_dialog.dart ( CouponType 15 · OrderType 8 · CouponInfo 6 · Book 1) · widgets/dialog/cash_charge_dialog.dart (4) · widgets/cash_charge/charge_package_list.dart (4) · widgets/sort/* ( BookSortField 2) rg -c "pod\." package/resources/lib → 0
B2 package/resources_console 분리 widgets/console 15파일 3,546 LOC + widgets/table 10파일 2,757 + widgets/navigation 1파일 75 = 6,378 LOC (40.5%) enable_admin=false 산출물이 콘솔 테이블을 안 받는다
B3 coreunibook_client/pod.* 8건 정리 domain/policy/{name,password}_policy.dart · data/api_interceptor.dart · app/desktop_handoff/ · domain/event/book_purchased_event.dart · data/repositories/book_like_repository.dart · data/patcher/book_purchased_patcher.dart core domain 이 생성 클라이언트를 직접 import 하지 않는다

B1 해소 방향: donor 모델을 뷰 모델로 대체한다. category_tree_selector 는 이미 트리 구조만 쓰므로 제네릭 노드 타입으로 치환 가능. purchase_dialogCouponType/ OrderType enum 을 resources 자체 enum 으로 미러링하고 매핑을 호출부에 남긴다.

B2 주의: console_shared 로 흡수 금지 — 그건 L5 라 feature/application(L5)이 의존하면 동일 계층 위반이 된다. 신설 resources_console 은 L3 로 둔다(console L5→L3, core L2 의존 모두 하향이라 위반이 감소한다).

resources 의 콘솔 편중은 계층 배정 문제와 직교한다. resources 가 L1 계약 (core 비의존)을 어기는 이유는 콘솔이 아니라 23파일의 resources→core import 이고, 그건 core↔resources 2노드 SCC 로 이미 기록돼 있다(Track C4).


Track C — 값싼 SCC 절단 (순서가 계산돼 있음) 🟡#

목표: cc-bricks:feature-* 스킬 8종이 실제로 단독 추출된다. 가치: 동기화 전용. 빌드 근거로 쓰지 말 것. 의존: 없음(A·B 와 병렬 가능). 단 내부 순서는 엄격하다.

무거운 엣지(chat→auth 28파일 · book_content_viewer→auth 20 · store→auth 17 · search→store 13)는 한 번도 끊을 필요가 없다. 값싼 엣지만 순서대로 끊는다.

#절단실체예상 지표비고
C1 my_page→my_library 라우트 객체 1 max_scc 10→9 · scc_edges 32→30 단독으로 max_scc 를 내리는 최저비용 컷. my_library 의 SCC 내 in-edge 가 이것뿐
C2 store→search SearchFilterType enum 1개 (import 문 0줄인 순수 enum) — store/lib 3파일이 전부 show SearchFilterType max_scc −1 · scc_edges −2 store 로 옮기는 편이 낫다 — SearchRoute 가 이미 store 소유라 신규 엣지 0 , core 공개 표면 불변
C3 console_publisher_management→console_router lib 3파일 / 심볼 6개 콘솔 SCC 4→소멸 · scc_count 3→2 · scc_edges −6 MFAS=1 유일해
C4 core↔resources 2-cycle resources→core 23파일 — 다수는 CoUI Core 토큰(core 가 재수출만) scc_count −1 장벽은 Log(5줄)와 context.i10n. B2 이후가 유리

⚠️ 순서 함정 (실측 확인됨)#

scc_count 래칫이 "SCC 가 쪼개지는 개선"을 회귀로 오판해 CI 를 빨갛게 만든다 (check_package_layers.py:391-398, baseline scc_count: 3).

  • 함정 발생 조건: store in-edge 6개 중 5개만 끊어 search↔store 2-사이클을 남기는 경우 → scc_count 4 → exit 1. (6개를 다 끊으면 max_scc 10→6 · scc_count 3 유지 · scc_edges 32→19 · layer_violations 71→65 로 통과)
  • 완화: 노드 이탈 단위로 끊는다. in/out 차수 1인 ai_chat·book_content_reader· search·sign_in_with_email 과 in-degree 1인 my_page 는 1컷으로 이탈하며, 검증한 단일컷 9종 모두 scc_count 3을 유지한다.
  • PR 전 필수: python3 .github/scripts/check_package_layers.py --report 로 4개 지표를 선확인한다. scc_count 만 증가하고 나머지가 개선이면 사람이 직접 상향하고 PR 에 근거를 남긴다(package-layers.md:121-125 에 이미 규정됨).

C3 의 숨은 선행 작업#

console_publisher_management→console_router 는 PDF 업로드 UseCase 4심볼만 옮겨서는 끊기지 않는다IPublisherApplicationRepository/PublisherApplicationRepository (console_router 소유)까지 함께 이동해야 한다. 그런데 그 UseCase 가 package:chatContentTypeUtil 을 쓰고 CPM pubspec 에 chat 이 없어, 그대로 옮기면 새 layer_violation 1건이 생겨 −1 이 상쇄된다.

A1 의 ContentTypeUtil → core 이관이 C3 의 선행 조건이다. 두 트랙이 여기서 한 번 만난다.


Track D — 라우터 셸 정화 🟡#

라우트 import allowlist(core·dependencies·flutter·go_router) 위반 39건이 남아 있는데, 전량이 app_router·console_router 셸 2개에 집중돼 있다. 비-라우터 feature 라우트는 kobic#8490 이 실제로 정화를 마쳤다.

이건 import 정화로 풀리지 않는다 — "라우터 셸이 feature 를 알아야 한다"는 구조 문제라 RoutePageRegistry 확장으로만 풀린다. 그리고 app_router 는 cob 의 kPreservedGenericizedBricks 라 donor sync 대상이 아니므로 급하지 않다.

console_router 는 별개 문제를 겸한다 — L6 선언인데 repository·usecase·bloc·page 를 소유한다(C3 이 이 일부를 건드린다).


Track E — feature/common 수확 경로 (cob 쪽) 🟢#

cob 에 feature/common 을 스캔하는 sync 함수가 없다. 있는 것은 _convertApplicationFeaturesToConditionalDirs(application)와 console 스캔뿐이고, feature/commonfeature_differ(diff 전용)·workspace_name_guard 에만 등장한다.

kobic 쪽 준비는 Track A1 이 이미 한다(common 이 application 을 하드 의존하지 않게 됨). cob 쪽 함수 추가는 co-bricks 저장소 PR 이며 이 로드맵 범위 밖이다 — 다만 A1 없이 그 함수만 추가하면 수확된 common 브릭이 조립 시 깨지므로 A1 이 선행이다.


비목표 (하지 않을 것)#

  1. 완전 DAG(S4)를 빌드 근거로 추진하지 않는다. 실측 −6.9%. 필요하면 동기화 근거로만.
  2. core.dart 재수출 해체. 소비 파일 94%가 프레임워크를 core 로만 받으므로 해체하면 수백 파일이 깨진다. 유령 135개 중 재수출 인과는 51%(69개)뿐이고, 전부 지워도 평균 폐포는 21.2→19.5 라 빌드 근거로도 못 쓴다.
  3. 무거운 엣지 절단 (chat→auth 28 · book_content_viewer→auth 20 · store→auth 17 · search→store 13). 값싼 14엣지로 목표가 달성된다.
  4. console_shared 로 콘솔 위젯 흡수 (L5 라 계층 위반을 만든다).
  5. 테스트 1-hop 전파를 SCC 작업에 묶기. 별개 문제이고 별개 PR 이다.

성공 지표#

지표현재목표트랙
cob create --features 부분집합 pub get 실패 통과 A
resourcespod.* 참조640B1
enable_admin=false 산출물의 콘솔 LOC6,3780B2
SCC 안에 갇힌 cc-bricks:feature-* 스킬 8/11 0/11 C
max_scc101C
scc_edges320C
layer_violations71≤57A·B·C
라우트 allowlist 위반390D

검증 이력 · 신뢰도#

주장상태
기준선 6지표 (78/10/3/32/71/135)✅ 직접 실행 재현
common→application 5건 · console→application 7건 ✅ 직접 실행, 목록 확인
resources pod.* 64회 · core 8회✅ 직접 실행, 심볼 분포 확인
cob 에 feature/common sync 함수 부재✅ 직접 grep
SCC 안 cc-bricks:feature-* 8/11✅ 스킬 목록 × SCC 멤버 교집합
store→search = SearchFilterType 하나 ✅ 반증 통과 (검증기가 파일·심볼·그래프·딥링크·래칫 전수 확인)
빌드 8% 판정 유지✅ 반증 통과 (shadow 계측 재현)
콘솔 SCC MFAS=1 유일해✅ 완전 탐색 확인 (수정안은 정정됨 — 위 "C3 숨은 선행 작업")
donor 실질 공급 2.8% (2/71) 반증됨 — 아래 "정정" 절 참조. 측정 대상을 잘못 잡은 값이다
라우트 allowlist 위반 39건 ✅ 직접 실행 재현 — app_router 25 + console_router 14, 비-라우터 feature 라우트 0건 (#8490 정화 확인). 위반 대상 10파일 목록 확보

반증 37건 중 대부분은 "방향은 맞으나 수치·수정안이 어긋남"이다. 검증기가 수치 불일치 = refuted 규칙으로 엄격히 동작했으므로, 위 로드맵은 정정된 서술을 반영했다(특히 C1 의 max_scc 효과, C3 의 선행 작업, 라우트 상수 vs 라우트 객체 구분).


정정 — "donor 실질 공급 2.8%" 는 틀렸다 (2026-07-29 재확인)#

감사가 낸 2/71 = 2.8%공급 경로 하나만 측정한 값이다. donor 공급은 두 경로로 일어나고, 지배적인 쪽은 측정에서 빠져 있었다.

경로공급량게이트
monorepo 브릭 동봉 (SyncMonorepoService) 65 feature — application 29 · common 1 (splash) · console 35 application 은 {{#has_X}} 개별, console 은 {{#enable_admin}} 통째, common 은 없음
② prebuilt feature-* apply (레시피 apply:) 2book_content_reader · book_content_search 레시피가 명시

2/71 은 ②만 센 값이다. 71 의 정체는 bricks/bricks/feature/feature-* 디렉토리 수(73 − 제네릭 featurebackend_feature)로 맞다. 9개 레시피의 apply: 전수는 app_router(9회) · book_content_reader(1) · book_content_search(1) 뿐이고, app_routerkPreservedGenericizedBricks 라 donor 소유가 아니다 — 여기까지는 감사가 정확했다.

틀린 것은 해석이다. 이 값은 "donor 공급률"이 아니라 "레시피가 prebuilt 브릭을 거의 쓰지 않는다" 는 별개의 사실이다. 레시피는 대부분을 atomic 브릭 합성(compose)으로 만든다(auth·post·course 등).

그래서 진짜 문제는 무엇이었나#

①이 65개를 공급하는데 그 게이트가 작동하지 않았다feature/common (게이트 없음)이 feature/application(gated)을 pubspec 으로 하드 의존해 cob create --features <부분집합>dart pub get 에서 죽었다. Track A(#9898)가 고친 것이 정확히 이것이며, 이 정정은 Track A 의 근거를 약화시키지 않고 오히려 명확히 한다.

common 비대칭은 확인됐고 더 심각하다#

kobic commonprebuilt 브릭monorepo 동봉레시피 apply
splash
auth · extra_info · life · settings · sign_in_with_email · withdraw
account_recovery

6개는 브릭이 이미 존재하는데 어느 경로로도 나가지 않는 고아다. 브릭을 더 만드는 게 아니라 기존 브릭에 출구를 열어주는 것이 Track E 의 일이다.