이관 문서 — 원출처: 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_info
는
prebuilt 브릭이 존재하는데도
동봉·apply 어느 쪽도 안 된 고아다
|
| 공용 계층의 donor 오염 |
resources
가
pod.*
64회
,
core
가
8회
참조
|
디자인 시스템 패키지가 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/FileValidatorUtil2개 →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/nameString 상수뿐이다.
Track B — 공용 계층 donor 오염 제거 🔴 브릭 컴파일 파손 차단#
목표: core·resources 브릭이 donor 백엔드 없이도 컴파일된다.
가치: 지금 상태로 sync 하면 컴파일이 깨진 브릭이 만들어진다. 조립 이전 문제다.
의존: 없음.
| # | 무엇을 | 대상 | 게이트 |
|---|---|---|---|
| B1 | resources 의 pod.* 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 | core 의 unibook_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_dialog 는 CouponType/
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→coreimport 이고, 그건core↔resources2노드 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).
-
함정 발생 조건:
storein-edge 6개 중 5개만 끊어search↔store2-사이클을 남기는 경우 →scc_count4 → 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_count3을 유지한다. -
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:chat 의 ContentTypeUtil 을 쓰고 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/common 은 feature_differ(diff 전용)·workspace_name_guard
에만 등장한다.
kobic 쪽 준비는 Track A1 이 이미 한다(common 이 application 을 하드 의존하지 않게 됨). cob 쪽 함수 추가는 co-bricks 저장소 PR 이며 이 로드맵 범위 밖이다 — 다만 A1 없이 그 함수만 추가하면 수확된 common 브릭이 조립 시 깨지므로 A1 이 선행이다.
비목표 (하지 않을 것)#
- 완전 DAG(S4)를 빌드 근거로 추진하지 않는다. 실측 −6.9%. 필요하면 동기화 근거로만.
-
core.dart재수출 해체. 소비 파일 94%가 프레임워크를 core 로만 받으므로 해체하면 수백 파일이 깨진다. 유령 135개 중 재수출 인과는 51%(69개)뿐이고, 전부 지워도 평균 폐포는 21.2→19.5 라 빌드 근거로도 못 쓴다. -
무거운 엣지 절단 (
chat→auth28 ·book_content_viewer→auth20 ·store→auth17 ·search→store13). 값싼 14엣지로 목표가 달성된다. console_shared로 콘솔 위젯 흡수 (L5 라 계층 위반을 만든다).- 테스트 1-hop 전파를 SCC 작업에 묶기. 별개 문제이고 별개 PR 이다.
성공 지표#
| 지표 | 현재 | 목표 | 트랙 |
|---|---|---|---|
cob create --features 부분집합 pub get |
실패 | 통과 | A |
resources 의 pod.* 참조 | 64 | 0 | B1 |
enable_admin=false 산출물의 콘솔 LOC | 6,378 | 0 | B2 |
SCC 안에 갇힌 cc-bricks:feature-* 스킬 |
8/11 | 0/11 | C |
| max_scc | 10 | 1 | C |
| scc_edges | 32 | 0 | C |
| layer_violations | 71 | ≤57 | A·B·C |
| 라우트 allowlist 위반 | 39 | 0 | D |
검증 이력 · 신뢰도#
| 주장 | 상태 |
|---|---|
| 기준선 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 숨은 선행 작업") |
| ❌ 반증됨 — 아래 "정정" 절 참조. 측정 대상을 잘못 잡은 값이다 | |
| 라우트 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:) |
2 — book_content_reader · book_content_search |
레시피가 명시 |
2/71 은 ②만 센 값이다. 71 의 정체는 bricks/bricks/feature/ 의 feature-*
디렉토리 수(73 − 제네릭 feature − backend_feature)로 맞다. 9개 레시피의
apply: 전수는 app_router(9회) · book_content_reader(1) ·
book_content_search(1) 뿐이고, app_router 는 kPreservedGenericizedBricks
라 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 common | prebuilt 브릭 | monorepo 동봉 | 레시피 apply |
|---|---|---|---|
splash | ✅ | ✅ | — |
auth
·
extra_info
·
life
·
settings
·
sign_in_with_email
·
withdraw
|
✅ | ❌ | ❌ |
account_recovery | ❌ | ❌ | ❌ |
6개는 브릭이 이미 존재하는데 어느 경로로도 나가지 않는 고아다. 브릭을 더 만드는 게 아니라 기존 브릭에 출구를 열어주는 것이 Track E 의 일이다.