- Drift 21 tables (8 catalog + 11 user + habit_dose_variants + meta_kv) with R1~R10 CHECK constraints and 19 indexes - 8 hand-crafted seed JSON catalogs in app/assets/seed/ (refs 84, protocols 34, methodologies 21, frame_patterns 30, reward_menu_items 30, break_protocols 8, common_frames 5, diet_patterns 5) - 6 domain functions: recommend_variant, compute_streak, validate_frame_level, active_habit_quota, weekly_minimum_ratio, seed_importer (transactional, idempotent) - 4 vertical-slice Riverpod screens: HabitList, HabitCreate, CheckIn, Streak - 31 unit tests passing; flutter analyze clean - OQ-5 streak semantics: missing entry ≠ explicit blank (missing = end of history; only TrackerValue.blank triggers Never-miss-twice) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
164 lines
7.9 KiB
Dart
164 lines
7.9 KiB
Dart
import 'package:drift/drift.dart';
|
|
|
|
// 8 catalog tables, read-only. Source: schema/*.schema.json.
|
|
// Nested objects + arrays stored as JSON TEXT for read-only simplicity.
|
|
|
|
class Protocols extends Table {
|
|
TextColumn get id => text()();
|
|
TextColumn get category => text().check(const CustomExpression<bool>(
|
|
"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_text')();
|
|
TextColumn get dose => text()();
|
|
TextColumn get why => text()();
|
|
TextColumn get howJson => text().named('how_json')();
|
|
TextColumn get checkText => text().named('check_text')();
|
|
TextColumn get caution => text().nullable()();
|
|
TextColumn get defaultAnchorJson => text().named('default_anchor_json').nullable()();
|
|
TextColumn get minDoseForStart => text().named('min_dose_for_start').nullable()();
|
|
TextColumn get referenceIdsJson => text().named('reference_ids_json').nullable()();
|
|
TextColumn get evidenceStrength => text().named('evidence_strength').nullable().check(
|
|
const CustomExpression<bool>(
|
|
"evidence_strength IS NULL OR evidence_strength IN ('strong_rct','meta_analysis','observational','mechanistic','expert_opinion')"))();
|
|
TextColumn get sourceDoc => text().named('source_doc').nullable().check(
|
|
const CustomExpression<bool>(
|
|
"source_doc IS NULL OR source_doc IN ('huberman-protocols.md','diet-protocols.md')"))();
|
|
|
|
@override
|
|
Set<Column> get primaryKey => {id};
|
|
}
|
|
|
|
class BreakProtocols extends Table {
|
|
TextColumn get id => text()();
|
|
TextColumn get category => text().check(const CustomExpression<bool>(
|
|
"category IN ('alcohol','nicotine','porn_masturbation','social_media','sugar','caffeine','cannabis','behavioral')"))();
|
|
TextColumn get title => text()();
|
|
TextColumn get hubermanSummary => text().named('huberman_summary')();
|
|
TextColumn get frameExamplesJson => text().named('frame_examples_json').nullable()();
|
|
TextColumn get phasesJson => text().named('phases_json')();
|
|
TextColumn get defaultCommonFramesJson => text().named('default_common_frames_json')();
|
|
TextColumn get toolsJson => text().named('tools_json').nullable()();
|
|
TextColumn get medicalWarning => text().named('medical_warning').nullable()();
|
|
TextColumn get referenceIdsJson => text().named('reference_ids_json').nullable()();
|
|
|
|
@override
|
|
Set<Column> get primaryKey => {id};
|
|
}
|
|
|
|
class CommonFrames extends Table {
|
|
TextColumn get id => text().check(const CustomExpression<bool>(
|
|
"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().named('how_json').nullable()();
|
|
TextColumn get checkText => text().named('check_text')();
|
|
TextColumn get applicableBreakCategoriesJson =>
|
|
text().named('applicable_break_categories_json').nullable()();
|
|
TextColumn get referenceIdsJson => text().named('reference_ids_json').nullable()();
|
|
|
|
@override
|
|
Set<Column> get primaryKey => {id};
|
|
}
|
|
|
|
class Methodologies extends Table {
|
|
TextColumn get id => text()();
|
|
TextColumn get name => text()();
|
|
TextColumn get originator => text()();
|
|
TextColumn get oneLineDefinition => text().named('one_line_definition')();
|
|
TextColumn get coreUnit => text().named('core_unit')();
|
|
TextColumn get procedureJson => text().named('procedure_json').nullable()();
|
|
TextColumn get toolsJson => text().named('tools_json').nullable()();
|
|
TextColumn get strengthsJson => text().named('strengths_json').nullable()();
|
|
TextColumn get weaknessesJson => text().named('weaknesses_json').nullable()();
|
|
TextColumn get goodFor => text().named('good_for').nullable()();
|
|
IntColumn get hubermanFitScore => integer().named('huberman_fit_score').check(
|
|
const CustomExpression<bool>("huberman_fit_score BETWEEN 1 AND 5"))();
|
|
BoolColumn get isCoreEngine =>
|
|
boolean().named('is_core_engine').withDefault(const Constant(false))();
|
|
TextColumn get referenceIdsJson => text().named('reference_ids_json').nullable()();
|
|
|
|
@override
|
|
Set<Column> get primaryKey => {id};
|
|
}
|
|
|
|
class FramePatterns extends Table {
|
|
TextColumn get id => text()();
|
|
TextColumn get domain => text().nullable().check(const CustomExpression<bool>(
|
|
"domain IS NULL OR domain IN ('food','drink','smoking','screen','porn','sleep','exercise','general')"))();
|
|
TextColumn get avoidanceKeyword => text().named('avoidance_keyword')();
|
|
TextColumn get l0Example => text().named('l0_example')();
|
|
TextColumn get l1SimpleReplace => text().named('l1_simple_replace').nullable()();
|
|
TextColumn get l2Suggestion => text().named('l2_suggestion')();
|
|
TextColumn get l3Identity => text().named('l3_identity').nullable()();
|
|
|
|
@override
|
|
Set<Column> get primaryKey => {id};
|
|
}
|
|
|
|
class RewardMenuItems extends Table {
|
|
TextColumn get id => text()();
|
|
TextColumn get tierRecommended => text().named('tier_recommended').check(
|
|
const CustomExpression<bool>("tier_recommended IN ('T0','T1','T2','T3','T4')"))();
|
|
TextColumn get title => text()();
|
|
TextColumn get description => text().nullable()();
|
|
IntColumn get estimatedCostKrwMin => integer().named('estimated_cost_krw_min').nullable()();
|
|
IntColumn get estimatedCostKrwMax => integer().named('estimated_cost_krw_max').nullable()();
|
|
BoolColumn get isEffortTied => boolean().named('is_effort_tied').nullable()();
|
|
TextColumn get tagsJson => text().named('tags_json').nullable()();
|
|
TextColumn get avoidForBreakHabitsJson =>
|
|
text().named('avoid_for_break_habits_json').nullable()();
|
|
|
|
@override
|
|
Set<Column> get primaryKey => {id};
|
|
}
|
|
|
|
@DataClassName('ReferenceRow')
|
|
class References extends Table {
|
|
TextColumn get id => text()();
|
|
TextColumn get kind => text().check(const CustomExpression<bool>(
|
|
"kind IN ('paper','podcast_episode','book','url','korean_explainer')"))();
|
|
TextColumn get title => text()();
|
|
TextColumn get authorsJson => text().named('authors_json').nullable()();
|
|
IntColumn get year => integer().nullable().check(
|
|
const CustomExpression<bool>("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().named('episode_number').nullable()();
|
|
TextColumn get publisher => text().nullable()();
|
|
TextColumn get evidenceStrength => text().named('evidence_strength').nullable().check(
|
|
const CustomExpression<bool>(
|
|
"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};
|
|
}
|
|
|
|
class DietPatterns extends Table {
|
|
TextColumn get id => text()();
|
|
TextColumn get name => text()();
|
|
TextColumn get core => text()();
|
|
TextColumn get strengthsJson => text().named('strengths_json').nullable()();
|
|
TextColumn get weaknessesJson => text().named('weaknesses_json').nullable()();
|
|
TextColumn get evidenceStrength => text().named('evidence_strength').check(
|
|
const CustomExpression<bool>(
|
|
"evidence_strength IN ('strong','moderate','mixed','weak')"))();
|
|
TextColumn get koreanContextFit => text().named('korean_context_fit').nullable().check(
|
|
const CustomExpression<bool>(
|
|
"korean_context_fit IS NULL OR korean_context_fit IN ('high','medium','low')"))();
|
|
TextColumn get starterLeversJson => text().named('starter_levers_json').nullable()();
|
|
TextColumn get medicalWarning => text().named('medical_warning').nullable()();
|
|
TextColumn get linkedProtocolIdsJson =>
|
|
text().named('linked_protocol_ids_json').nullable()();
|
|
TextColumn get referenceIdsJson => text().named('reference_ids_json').nullable()();
|
|
|
|
@override
|
|
Set<Column> get primaryKey => {id};
|
|
}
|