Files
life-helper/docs/design/204-flutter-bootstrap/02-drift-schema-catalog.md
joungmin 29befe4d97 [Architect] Refs #204 — apply OQ decisions: diet_pattern (19th), ADR-0002 normalize dose_variants
- OQ-1: dose_variants 정규화 결정을 ADR-0002 로 승격 (ADR-0001 = 왜, ADR-0002 = 어떻게).
- OQ-3: nutrition diet 패턴 5개를 별도 diet_pattern 카탈로그(19번째 SoT)로 분리.
  · 02-catalog §8 신규, 인덱스 IDX_diet_patterns_evidence / IDX_diet_patterns_kfit.
  · 05-seed: diet_patterns.json (5행) 추가, 로딩 순서 끝에 배치.
  · 04-migrations: v1 테이블 합계 = Catalog 8 + User 11 + 부속 1 + meta_kv = 21.
- README §2/§3/§6/§11 갱신: 18→19 SoT, AC-2 에 diet_pattern=5 검증 추가.
- README §12 OQ → Resolved Open Questions 표 (OQ-1~OQ-8 결정 결과).
- habit_dose_variant → habit_dose_variants 표기 통일.
- fn-weekly-minimum-ratio, 03-drift-schema-user 의 ADR-0002 cross-link.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-11 17:13:04 +09:00

11 KiB

02 — Catalog 테이블 8 개 Drift 정의 (#204)

부모 설계서: README.md SoT: schema/protocol.schema.json, break_protocol.schema.json, common_frame.schema.json, methodology.schema.json, frame_pattern.schema.json, reward_menu_item.schema.json, reference.schema.json, diet_pattern.schema.json.

공통 규약

  • 모든 카탈로그 테이블 PK = TEXT id (ULID 또는 의미 있는 식별자).
  • 모든 enum 값은 TextColumn().named('xxx').withCheckConstraint("xxx IN ('...', ...)") 로 CHECK 박는다.
  • 다차원 배열·중첩 객체 (procedure[], phases[], frame_examples[], etc.) 는 JSON TEXT 단일 컬럼에 저장. 카탈로그는 read-only 라서 JSON 쿼리 불필요 (단순 read → freezed 모델로 deserialize).
  • reference_ids[] 도 JSON TEXT. M:N 조인 테이블은 본 Phase 에서 만들지 않음 (실용 카운트 작음, Phase 2 에서 join 필요 시 추가).
  • additionalProperties: false 인 schema 의 모든 명시 필드는 컬럼화. 단 nested object 는 JSON.

1. protocols (← protocol.schema.json)

class Protocols extends Table {
  TextColumn get id => text()();
  TextColumn get category => text().withCheckConstraint(
      "category IN ('health','meditation','motivation','habit','learning','diet')")();
  TextColumn get title => text()();
  TextColumn get titleEn => text().nullable()();
  TextColumn get what => text()();
  TextColumn get whenText => text().named('when')();
  TextColumn get dose => text()();
  TextColumn get why => text()();
  TextColumn get howJson => text()();              // List<String> JSON
  TextColumn get check => text()();
  TextColumn get caution => text().nullable()();
  TextColumn get defaultAnchorJson => text().nullable()(); // {when,after_what,where}
  TextColumn get minDoseForStart => text().nullable()();
  TextColumn get referenceIdsJson => text().nullable()();  // List<String> JSON
  TextColumn get evidenceStrength => text().nullable().withCheckConstraint(
      "evidence_strength IS NULL OR evidence_strength IN "
      "('strong_rct','meta_analysis','observational','mechanistic','expert_opinion')")();
  TextColumn get sourceDoc => text().nullable().withCheckConstraint(
      "source_doc IS NULL OR source_doc IN "
      "('huberman-protocols.md','diet-protocols.md')")();
  @override Set<Column> get primaryKey => {id};
}

인덱스:

  • IDX_protocols_category(category) — UI 의 카테고리 탭 필터.

2. break_protocols (← break_protocol.schema.json)

class BreakProtocols extends Table {
  TextColumn get id => text()();
  TextColumn get category => text().withCheckConstraint(
      "category IN ('alcohol','nicotine','porn_masturbation','social_media',"
      "'sugar','caffeine','cannabis','behavioral')")();
  TextColumn get title => text()();
  TextColumn get hubermanSummary => text()();
  TextColumn get frameExamplesJson => text().nullable()();      // [{level,text}]
  TextColumn get phasesJson => text()();                         // [{week,goal,...}]
  TextColumn get defaultCommonFramesJson => text()();            // ['dopamine_reset',...]
  TextColumn get toolsJson => text().nullable()();
  TextColumn get medicalWarning => text().nullable()();
  TextColumn get referenceIdsJson => text().nullable()();
  @override Set<Column> get primaryKey => {id};
}

인덱스: IDX_break_protocols_category(category) UNIQUE — break protocol 은 카테고리당 1 개.

3. common_frames (← common_frame.schema.json)

class CommonFrames extends Table {
  TextColumn get id => text().withCheckConstraint(
      "id IN ('dopamine_reset','urge_surf','environment_design',"
      "'relapse_recovery','recovery_stack')")();
  TextColumn get title => text()();
  TextColumn get what => text()();
  TextColumn get why => text()();
  TextColumn get dose => text().nullable()();
  TextColumn get howJson => text().nullable()();
  TextColumn get check => text()();
  TextColumn get applicableBreakCategoriesJson => text().nullable()();
  TextColumn get referenceIdsJson => text().nullable()();
  @override Set<Column> get primaryKey => {id};
}

행 수 시드 = 5 (CommonFrameId enum 5 값).

4. methodologies (← methodology.schema.json)

class Methodologies extends Table {
  TextColumn get id => text()();
  TextColumn get name => text()();
  TextColumn get originator => text()();
  TextColumn get oneLineDefinition => text()();
  TextColumn get coreUnit => text()();
  TextColumn get procedureJson => text().nullable()();
  TextColumn get toolsJson => text().nullable()();
  TextColumn get strengthsJson => text().nullable()();
  TextColumn get weaknessesJson => text().nullable()();
  TextColumn get goodFor => text().nullable()();
  IntColumn get hubermanFitScore => integer().withCheckConstraint(
      "huberman_fit_score BETWEEN 1 AND 5")();
  BoolColumn get isCoreEngine => boolean().withDefault(const Constant(false))();
  TextColumn get referenceIdsJson => text().nullable()();
  @override Set<Column> get primaryKey => {id};
}

인덱스: IDX_methodologies_core(is_core_engine) partial WHERE true — 핵심 엔진 3 개 (atomic_habits, tiny_habits, implementation_intentions) 빠른 조회.

5. frame_patterns (← frame_pattern.schema.json)

class FramePatterns extends Table {
  TextColumn get id => text()();
  TextColumn get domain => text().nullable().withCheckConstraint(
      "domain IS NULL OR domain IN ('food','drink','smoking','screen','porn',"
      "'sleep','exercise','general')")();
  TextColumn get avoidanceKeyword => text()();
  TextColumn get l0Example => text()();
  TextColumn get l1SimpleReplace => text().nullable()();
  TextColumn get l2Suggestion => text()();
  TextColumn get l3Identity => text().nullable()();
  @override Set<Column> get primaryKey => {id};
}

인덱스: IDX_frame_patterns_keyword(avoidance_keyword)detectAvoidanceKeywords 가 사전 검색.

6. reward_menu_items (← reward_menu_item.schema.json)

class RewardMenuItems extends Table {
  TextColumn get id => text()();
  TextColumn get tierRecommended => text().withCheckConstraint(
      "tier_recommended IN ('T0','T1','T2','T3','T4')")();
  TextColumn get title => text()();
  TextColumn get description => text().nullable()();
  IntColumn get estimatedCostKrwMin => integer().nullable()();
  IntColumn get estimatedCostKrwMax => integer().nullable()();
  BoolColumn get isEffortTied => boolean().nullable()();
  TextColumn get tagsJson => text().nullable()();
  TextColumn get avoidForBreakHabitsJson => text().nullable()();
  @override Set<Column> get primaryKey => {id};
}

인덱스: IDX_reward_menu_tier(tier_recommended).

7. references (← reference.schema.json)

class References extends Table {
  TextColumn get id => text()();
  TextColumn get kind => text().withCheckConstraint(
      "kind IN ('paper','podcast_episode','book','url','korean_explainer')")();
  TextColumn get title => text()();
  TextColumn get authorsJson => text().nullable()();
  IntColumn get year => integer().nullable().withCheckConstraint(
      "year IS NULL OR (year BETWEEN 1900 AND 2100)")();
  TextColumn get journal => text().nullable()();
  TextColumn get doi => text().nullable()();
  TextColumn get url => text().nullable()();
  IntColumn get episodeNumber => integer().nullable()();
  TextColumn get publisher => text().nullable()();
  TextColumn get evidenceStrength => text().nullable().withCheckConstraint(
      "evidence_strength IS NULL OR evidence_strength IN "
      "('strong_rct','meta_analysis','observational','mechanistic','expert_opinion')")();
  BoolColumn get verified => boolean().nullable()();
  TextColumn get note => text().nullable()();
  @override Set<Column> get primaryKey => {id};
}

인덱스: IDX_references_kind(kind), IDX_references_doi(doi) partial WHERE doi IS NOT NULL.

8. diet_patterns (← diet_pattern.schema.json)

OQ-3 결정 (2026-06-11): protocol(category='diet') 와 분리된 별도 카탈로그. 의견 분열 영역의 합리적 선택지 (지중해/케토/TRE/식물성/한식) — 단일 정답 없음, 사용자 체질·문화에 맞춰 조합 대상.

class DietPatterns extends Table {
  TextColumn get id => text()();
  TextColumn get name => text()();
  TextColumn get core => text()();
  TextColumn get strengthsJson => text().nullable()();             // List<String> JSON
  TextColumn get weaknessesJson => text().nullable()();            // List<String> JSON
  TextColumn get evidenceStrength => text().withCheckConstraint(
      "evidence_strength IN ('strong','moderate','mixed','weak')")();
  TextColumn get koreanContextFit => text().nullable().withCheckConstraint(
      "korean_context_fit IS NULL OR korean_context_fit IN ('high','medium','low')")();
  TextColumn get starterLeversJson => text().nullable()();         // List<String> JSON, ≤3 권장
  TextColumn get medicalWarning => text().nullable()();
  TextColumn get linkedProtocolIdsJson => text().nullable()();     // List<String> JSON, protocol(category='diet') id 참조
  TextColumn get referenceIdsJson => text().nullable()();          // List<String> JSON
  @override Set<Column> get primaryKey => {id};
}

인덱스:

  • IDX_diet_patterns_evidence(evidence_strength) — UI 의 "근거 강한 순" 정렬 / 필터.
  • IDX_diet_patterns_kfit(korean_context_fit) partial WHERE korean_context_fit IS NOT NULL — 한국 식문화 친화도 필터.

linked_protocol_ids 는 JSON TEXT 로 유지. M:N 조인 테이블 미생성 (행 수 작음 — diet_pattern = 5, linked protocol ≤ 6). Phase 2 에서 조인 쿼리 빈도 ↑ 시 정규화 검토.

시드 카운트 추산 (05-seed-data.md 와 sync)

테이블 추산 행 수 비고
references ~50 huberman §8 + methodologies §출처 + breaking §5 + diet §7 합계
protocols ~34 huberman 28 (§1.9 stub 제외) + diet 1.1~1.6 (6)
break_protocols 8 알코올/니코틴/포르노/SNS/설탕/카페인/대마/행동
common_frames 5 dopamine_reset/urge_surf/environment_design/relapse_recovery/recovery_stack
methodologies 21 habit-todo-methodologies.md §1~§21
frame_patterns ~30 habit-todo-methodologies.md "흔한 끊기 목표 변환 30선"
reward_menu_items ~30 habit-todo-methodologies.md "권장 리워드 메뉴 30선"
diet_patterns 5 nutrition/diet-protocols.md §2 (mediterranean / low_carb_keto / tre_if / plant_based / k_diet)

OQ-3 결정 (2026-06-11): diet §2 의 식이 패턴 5 개는 별도 diet_patterns 카탈로그로 분리 (위 §8). protocols(category='diet') 는 diet §1 의 원칙 6 항목만 담는다 (두 카탈로그가 linked_protocol_ids 로 약한 연결).

카탈로그는 read-only

  • 모든 카탈로그 테이블에 대한 INSERT 는 SeedImporter 와 마이그레이션만 수행.
  • App 코드 (features/) 에서는 SELECT 만.
  • 사용자 커스텀 protocol 은 v2 의 user_protocol 별도 테이블 (out of scope).