research

Naran 데이터 모델 (v4)

Naran 데이터 모델 (v4)

상태: 확정 (설계하자에서 검토 완료) zeroclaw 위에 얹는 도메인 레이어. zeroclaw DB는 건드리지 않음.


핵심 확정 사항

Naran 구조:
  별도 MCP 서버로 독립 운영 (zeroclaw 포크 없음)
  zeroclaw upstream 업데이트를 그대로 받아먹음

DB 구조:
  naran.db     → MySQL (어느 프로세스에서든 연결 가능)
  cron/jobs.db → zeroclaw SQLite (sequential 접근, WAL 충돌 없음)

접근 방식:
  Naran MCP Tool → MySQL naran.db 직접 SQL
  Naran MCP Tool → zeroclaw cron/jobs.db 직접 SQL (sequential)
  REST API 레이어 없음

핵심 개념

Task        = 인간의 work 구조체 (상태 없음)
Event       = Task의 상태 (시간 블록 배정)
rules       = rule 정의 저장소 (재사용 or 일회성 무관)
task_rules  = Task ↔ Rule 관계
event_rules = Event ↔ Rule 관계

Event 생성 시점에:
  task_rules + event_rules → zeroclaw cron_jobs 변환 (sequential)

Task 상태 파생

Task 자체는 상태를 들고 있지 않는다. events에서 파생:

events 없음                        → unscheduled
미래 event 있음                    → scheduled
due_date 지났는데 done event 없음   → overdue
done event 있음                    → complete

재사용 vs 일회성 Rule

모든 rule은 rules 테이블을 거친다. 재사용이냐 일회성이냐는 얼마나 많은 곳에서 참조하느냐의 차이:

재사용 rule:
  rules row 1개 ← task_rules에서 여러 Task가 참조
  예: "spotify 아무노래" → 손씻기, 운동, 공부 Task 모두 연결

일회성 rule:
  rules row 1개 ← event_rules에서 딱 1번만 참조
  예: "오늘만 youtube 인기동영상" → 이번 Event에만 연결

전체 흐름 예시

"손씻기 Task 만들고 spotify 규칙 만들어줘"
  → tasks INSERT (MySQL)
  → rules INSERT (MySQL)
  → task_rules INSERT (MySQL)

"손씻기 1분 전 알람도 추가해줘"
  → rules INSERT (MySQL)
  → task_rules INSERT (MySQL, trigger=prepare, offset=1)
  ← spotify rule은 그대로 유지 (append)

"손씻기를 10분 뒤 Event로 등록하고 youtube도 틀어줘"
  → events INSERT (MySQL)
  → rules INSERT (MySQL, youtube)
  → event_rules INSERT (MySQL)
  → task_rules 읽어서 → cron_jobs INSERT (zeroclaw SQLite, sequential)
  → event_rules 읽어서 → cron_jobs INSERT (zeroclaw SQLite, sequential)

Generative UI 연동

DB 스키마 변경 없음. Tool 반환값에 render_hint 포함:

{
  "render_hint": "calendar",
  "data": [{"id": "...", "title": "손씻기", "start": "..."}]
}

| render_hint | 설명 | |------------|------| | "calendar" | CalendarView 컴포넌트 | | "task_list" | TaskListView 컴포넌트 | | "rule_list" | RuleListView 컴포넌트 | | "rule_flow" | RuleFlowChart 컴포넌트 | | 없음 | 일반 chat card |


소유권 분리

MySQL naran.db (Naran MCP 서버 소유):
  tasks
  rules
  task_rules
  event_rules
  events
  event_series
  event_series_exceptions

zeroclaw cron/jobs.db (zeroclaw 소유, 건드리지 않음):
  cron_jobs  ← Event 생성 시점에 변환되어 등록 (sequential)
  cron_runs  ← 실행 이력

스키마

1. tasks

CREATE TABLE tasks (
  id           VARCHAR(36)  PRIMARY KEY,
  title        VARCHAR(255) NOT NULL,
  description  TEXT,
  due_date     DATE,
  priority     TINYINT      DEFAULT 2,
  -- 1(high) 2(normal) 3(low)
  energy_level VARCHAR(20),
  -- 'deep' | 'shallow' | 'admin'
  tags         JSON,
  project_id   VARCHAR(36),
  created_at   DATETIME     NOT NULL,
  updated_at   DATETIME     NOT NULL,
  INDEX idx_tasks_due_date (due_date)
);

2. rules

CREATE TABLE rules (
  id           VARCHAR(36)  PRIMARY KEY,
  name         VARCHAR(255),
  -- 재사용 rule에 유용한 사람이 읽기 좋은 이름
  content_type VARCHAR(20)  NOT NULL,
  -- 'prompt' | 'shell'
  content      JSON         NOT NULL,
  -- prompt: {"text": "spotify에서 아무노래 틀기", "allowed_tools": ["shell"]}
  -- shell:  {"commands": {"macos": "...", "windows": "...", "linux": "..."}}
  created_at   DATETIME     NOT NULL
);

3. task_rules

CREATE TABLE task_rules (
  id             VARCHAR(36) PRIMARY KEY,
  task_id        VARCHAR(36) NOT NULL,
  rule_id        VARCHAR(36) NOT NULL,
  trigger        VARCHAR(10) NOT NULL,
  -- 'prepare' | 'doing' | 'end'
  offset_minutes INT         DEFAULT 0,
  -- trigger='prepare'일 때 몇 분 전
  condition      JSON,
  -- null이면 항상 실행
  -- {"type": "date_range", "from": "2024-05-20", "to": "2024-06-15"}
  -- {"type": "weekday", "days": ["monday", "wednesday"]}
  enabled        TINYINT(1)  DEFAULT 1,
  priority       INT         DEFAULT 0,
  -- 같은 trigger 내 실행 순서
  created_at     DATETIME    NOT NULL,
  FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
  FOREIGN KEY (rule_id) REFERENCES rules(id) ON DELETE CASCADE,
  INDEX idx_task_rules_task_id (task_id),
  INDEX idx_task_rules_rule_id (rule_id)
);

4. event_rules

CREATE TABLE event_rules (
  id             VARCHAR(36) PRIMARY KEY,
  event_id       VARCHAR(36) NOT NULL,
  rule_id        VARCHAR(36) NOT NULL,
  trigger        VARCHAR(10) NOT NULL,
  -- 'prepare' | 'doing' | 'end'
  offset_minutes INT         DEFAULT 0,
  condition      JSON,
  enabled        TINYINT(1)  DEFAULT 1,
  priority       INT         DEFAULT 0,
  created_at     DATETIME    NOT NULL,
  FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE,
  FOREIGN KEY (rule_id)  REFERENCES rules(id)  ON DELETE CASCADE,
  INDEX idx_event_rules_event_id (event_id),
  INDEX idx_event_rules_rule_id  (rule_id)
);

5. event_series

CREATE TABLE event_series (
  id                 VARCHAR(36)  PRIMARY KEY,
  title              VARCHAR(255) NOT NULL,
  rrule              VARCHAR(500) NOT NULL,
  -- "FREQ=WEEKLY;BYDAY=MO;COUNT=15"
  -- "FREQ=WEEKLY;BYDAY=MO;UNTIL=20240630"
  timezone           VARCHAR(50)  DEFAULT 'Asia/Seoul',
  materialized_until DATE,
  -- 여기까지 eager row 생성됨 (1년 기준)
  created_at         DATETIME     NOT NULL
);

6. event_series_exceptions

CREATE TABLE event_series_exceptions (
  id             VARCHAR(36) PRIMARY KEY,
  series_id      VARCHAR(36) NOT NULL,
  original_date  DATE        NOT NULL,
  exception_type VARCHAR(10) NOT NULL,
  -- 'deleted' | 'modified'
  event_id       VARCHAR(36),
  -- modified이면 수정된 events.id 참조
  FOREIGN KEY (series_id) REFERENCES event_series(id) ON DELETE CASCADE,
  FOREIGN KEY (event_id)  REFERENCES events(id) ON DELETE SET NULL,
  INDEX idx_exceptions_series (series_id)
);

7. events

CREATE TABLE events (
  id             VARCHAR(36)  PRIMARY KEY,
  title          VARCHAR(255) NOT NULL,
  description    TEXT,
  start_datetime DATETIME     NOT NULL,
  end_datetime   DATETIME     NOT NULL,
  timezone       VARCHAR(50)  DEFAULT 'Asia/Seoul',
  is_all_day     TINYINT(1)   DEFAULT 0,
  task_id        VARCHAR(36),
  series_id      VARCHAR(36),
  series_index   INT,
  status         VARCHAR(20)  DEFAULT 'scheduled',
  -- 'scheduled' | 'done' | 'cancelled'
  completed_at   DATETIME,
  priority       TINYINT      DEFAULT 2,
  energy_level   VARCHAR(20),
  tags           JSON,
  project_id     VARCHAR(36),
  created_at     DATETIME     NOT NULL,
  updated_at     DATETIME     NOT NULL,
  FOREIGN KEY (task_id)   REFERENCES tasks(id),
  FOREIGN KEY (series_id) REFERENCES event_series(id) ON DELETE CASCADE,
  INDEX idx_events_start     (start_datetime),
  INDEX idx_events_task_id   (task_id),
  INDEX idx_events_series_id (series_id)
);

task_rules + event_rules → zeroclaw cron_jobs 변환

Event 생성 시점에 sequential하게 처리:

name 형식: "naran:rule:{source}:{source_id}:{event_id}:{trigger}"
  task_rules  → "naran:rule:task:task-001:event-001:doing"
  event_rules → "naran:rule:event:event-001:event-001:doing"
{
  "name": "naran:rule:task:task-001:event-001:doing",
  "schedule": {"kind": "at", "at": "2024-04-01T09:00:00Z"},
  "job_type": "agent",
  "prompt": "spotify에서 아무노래 틀기",
  "delete_after_run": true
}

삭제 쿼리:

-- Event 삭제 시 관련 cron_jobs 정리
DELETE FROM cron_jobs WHERE name LIKE 'naran:rule:%:event-001:%'

-- Task 삭제 시 관련 cron_jobs 정리
DELETE FROM cron_jobs WHERE name LIKE 'naran:rule:task:task-001:%'

반복 Event 전략 — Hybrid Materialization

1년 이내 인스턴스   → MySQL row 즉시 생성 (eager)
1년 초과 인스턴스   → 조회 시 rrule 계산 (lazy, DB row 없음)

lazy → eager 전환 트리거:
  1. rule을 붙일 때
  2. Event 시작 30분 전 (자동)
  3. 사용자가 캘린더에서 해당 날짜 탭

시리즈 삭제 옵션:
  "이 일정만"  → event_series_exceptions deleted 기록
  "이후 모두"  → rrule에 UNTIL 추가
  "모든 일정"  → event_series 삭제 (CASCADE)

Naran MCP Tool 목록

| Tool | 동작 | render_hint | |------|------|------------| | task_add | tasks INSERT | - | | task_list | tasks SELECT + 상태 파생 | "task_list" | | task_update | tasks UPDATE | - | | rule_add | rules INSERT | - | | rule_list | rules SELECT | "rule_list" | | task_rule_add | task_rules INSERT | - | | task_rule_list | task_rules + rules JOIN | "rule_flow" | | task_rule_remove | task_rules DELETE | - | | event_add | events INSERT + task_rules/event_rules → cron_jobs (sequential) | - | | event_rule_add | event_rules INSERT + (rules INSERT) → cron_jobs | - | | event_rule_remove | event_rules DELETE + cron_jobs DELETE | - | | event_list | events SELECT + lazy 머지 | "calendar" | | event_update | events UPDATE + exception 처리 | - | | event_delete | exceptions or CASCADE + cron_jobs 정리 | - |


ERD


세부 미결 사항

  • [ ] 반복 시리즈에서 task_rules 변환 시점 — 시리즈 단위 한 번 vs 인스턴스마다
  • [ ] lazy 인스턴스 rrule 계산 위치 (Naran MCP Tool vs Tauri invoke)
  • [ ] event_delete 시 zeroclaw에서 실행 중인 cron_jobs 처리 방식
  • [ ] rules 삭제 정책 — task_rules/event_rules 참조 중인 rule 삭제 가능 여부