package main import "time" type SenderMode string const ( SenderHuman SenderMode = "human" SenderTwin SenderMode = "twin" ) type AutonomyLevel string const ( AutonomyL0 AutonomyLevel = "L0" AutonomyL1 AutonomyLevel = "L1" AutonomyL2 AutonomyLevel = "L2" ) // RelationshipTier is the minimum-2-tier persona split roadmap.md §2.7-B / // PRD.md §2.1-②/§3.1 requires: draft tone should read differently for a // close friend than for someone you'd stay formal with. Defaults to the // more conservative "formal" tier for anyone who hasn't set this explicitly // (same fail-safe-leaning default philosophy as AutonomyLevel defaulting // to L0 when missing). type RelationshipTier string const ( RelationshipClose RelationshipTier = "close" RelationshipFormal RelationshipTier = "formal" ) func validRelationshipTier(t RelationshipTier) bool { return t == RelationshipClose || t == RelationshipFormal } type User struct { ID uint `gorm:"primaryKey"` InviteCode string `gorm:"uniqueIndex;not null"` DisplayName string `gorm:"not null"` CreatedAt time.Time } // Session is a bearer token issued at signup/login (roadmap A2). // v1 beta: opaque random token, no refresh rotation yet. type Session struct { ID uint `gorm:"primaryKey"` Token string `gorm:"uniqueIndex;not null"` UserID uint `gorm:"not null;index"` CreatedAt time.Time ExpiresAt time.Time `gorm:"not null;index"` } // InviteCode is a pre-minted, single-use code (roadmap.md Phase 1 §2.6 // "초대 기반 베타 가입 플로우") -- signup validates against this table instead // of just deduping User.InviteCode, so joining actually requires a code // someone handed out, not any never-used string. type InviteCode struct { ID uint `gorm:"primaryKey"` Code string `gorm:"uniqueIndex;not null"` Note string // operator memo (recipient/channel) — roadmap C invite ops CreatedAt time.Time ExpiresAt *time.Time RevokedAt *time.Time UsedAt *time.Time UsedByUserID *uint } type Contact struct { ID uint `gorm:"primaryKey"` OwnerUserID uint `gorm:"not null;index"` ContactUserID *uint DisplayName string `gorm:"not null"` RelationshipNote string // RelationshipTier overrides the owner's global TwinSettings.RelationshipTier // for drafts sent to this specific contact (roadmap.md §2.7-B). Nil means // "use the global default". RelationshipTier *RelationshipTier CreatedAt time.Time } // TwinDisabledByPeer is the veto flag (tech-design.md §4: "대화방 단위 // 플래그(twin_disabled_by_peer)") -- per conversation, not per contact, so // it lives here rather than on Contact. Set by POST /conversations/:id/veto // when the counterpart asks to talk to the human only; checked before any // twin auto-send in that conversation (main.go). // TwinDisabledByFlood is the flood/spam auto-pause flag (roadmap.md §2.7-C, // PRD.md §4 "스팸 감지 임계치 초과 시 응대 중단"). Unlike TwinDisabledByPeer // (a deliberate one-way human/peer choice, no un-veto endpoint by design), // this is a fully automatic system action, so AGENTS.md's "every automatic // action needs post-hoc notification + one-tap undo" applies -- it's // reversible via POST /conversations/:id/flood-reset, unlike peer veto. type Conversation struct { ID uint `gorm:"primaryKey"` IsGroup bool `gorm:"not null;default:false"` TwinDisabledByPeer bool `gorm:"not null;default:false"` TwinDisabledByFlood bool `gorm:"not null;default:false"` CreatedAt time.Time } type ConversationParticipant struct { ID uint `gorm:"primaryKey"` ConversationID uint `gorm:"not null;index"` UserID uint `gorm:"not null;index"` // LastReadMessageID is this participant's read marker (roadmap.md // §2.7-A "단톡 따라잡기") -- nil means nothing read yet. Drives the // unread badge and GET /conversations/:id/summary's "안 본 동안" window. LastReadMessageID *uint } type Message struct { ID uint `gorm:"primaryKey"` ConversationID uint `gorm:"not null;index"` SenderID uint `gorm:"not null"` SenderMode SenderMode `gorm:"not null;default:human"` Text string `gorm:"not null"` // Retracted is the one-tap undo for an L2 auto-send (PRD.md §3.1, // AGENTS.md "every automatic action needs post-hoc notification + // one-tap undo") -- set via POST /messages/:id/retract, twin-authored // messages only. Retracted bool `gorm:"not null;default:false"` CreatedAt time.Time } type TwinSettings struct { ID uint `gorm:"primaryKey"` UserID uint `gorm:"uniqueIndex;not null"` AutonomyLevel AutonomyLevel `gorm:"not null;default:L0"` RelationshipTier RelationshipTier `gorm:"not null;default:formal"` CreatedAt time.Time UpdatedAt time.Time } // WhitelistRule is the L2 auto-send whitelist -- a (contact, topic) pair the // owner has approved for unattended replies. ContactID nil = any counterpart // (PRD.md §3.1). type WhitelistRule struct { ID uint `gorm:"primaryKey"` UserID uint `gorm:"not null;index"` ContactID *uint TopicKeyword string `gorm:"not null"` CreatedAt time.Time } // EscalationLog is one row per escalation_filter trigger -- the post-hoc // notification + undo trail required by AGENTS.md's absolute safety // invariants. type EscalationLog struct { ID uint `gorm:"primaryKey"` UserID uint `gorm:"not null;index"` ConversationID uint `gorm:"not null;index"` Reason string `gorm:"not null"` MessageSnippet string `gorm:"not null"` Resolved bool `gorm:"not null;default:false"` CreatedAt time.Time } // DeviceToken stores an FCM registration token for push (roadmap B). // Sending pushes is wired later; v1 persists tokens per user/device. type DeviceToken struct { ID uint `gorm:"primaryKey"` UserID uint `gorm:"not null;index"` Token string `gorm:"uniqueIndex;not null"` Platform string `gorm:"not null;default:android"` CreatedAt time.Time UpdatedAt time.Time } var allModels = []interface{}{ &User{}, &Session{}, &InviteCode{}, &Contact{}, &Conversation{}, &ConversationParticipant{}, &Message{}, &TwinSettings{}, &WhitelistRule{}, &EscalationLog{}, &DeviceToken{}, }