From ce27f7c5000637f569510fbc0949dce22fe4dcc2 Mon Sep 17 00:00:00 2001
From: th-kim0823
Date: Thu, 14 May 2026 17:38:04 +0900
Subject: [PATCH] =?UTF-8?q?docs(spec):=20v0.4=20=E2=80=94=20Notebooks=20+?=
=?UTF-8?q?=20Lifecycle=20Simplification=20design?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
기존 v0.3.2 Cut G design (사이드바 + notebook 카테고리) 을 v0.4 로 승격하면서
lifecycle 단순화 (archived 제거) + AI × Notebook 통합 (자동 fit 매칭 + tag 기반
promotion 제안) 을 함께 다룸. dogfood 19일 데이터 (archived 0건 / mlx-ops tag
6건이 사실상 컨텍스트 그룹 역할) 가 묶음 변경의 근거.
핵심 결정:
- notebook 모델 = 옵션 B (단일 DB + notebook_id), 다중 profile 옵션 A 는 v0.5+ 보류
- lifecycle 3분기 — active/completed/trashed, archived 제거 (마이그레이션 시 completed 로 합침)
- AI autonomy = 제안 + 1-click 수락 (새 notebook 자동 생성 X, fit 매칭만 자동 배치)
- promotion trigger = tag 3건 이상 default notebook 누적 (v0.4 first release 는 rule only)
- 사이드바 default hidden, Cmd+B / Ctrl+B 토글
선행 Cut G design (`2026-05-09-v032-cut-g-design.md`) 상단에 deprecate 노트 추가.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.../specs/2026-05-09-v032-cut-g-design.md | 2 +
...26-05-14-v04-notebooks-lifecycle-design.md | 327 ++++++++++++++++++
2 files changed, 329 insertions(+)
create mode 100644 docs/superpowers/specs/2026-05-14-v04-notebooks-lifecycle-design.md
diff --git a/docs/superpowers/specs/2026-05-09-v032-cut-g-design.md b/docs/superpowers/specs/2026-05-09-v032-cut-g-design.md
index baa4f43..cebfc33 100644
--- a/docs/superpowers/specs/2026-05-09-v032-cut-g-design.md
+++ b/docs/superpowers/specs/2026-05-09-v032-cut-g-design.md
@@ -1,5 +1,7 @@
# v0.3.2 — Cut G Design (사이드바 + notebook 카테고리)
+> **[Deprecated 2026-05-14]** 본 cut 은 [v0.4 design](2026-05-14-v04-notebooks-lifecycle-design.md) 으로 승격되며 lifecycle 단순화 (archived 제거) 를 함께 다룸. dogfood 데이터 (archived 0건 / 19일) 가 archived 차원의 dead code 임을 입증해 묶음 변경이 정신 부담 측면에서 더 정합. 본 문서는 history 참조용으로 유지.
+
**작성일:** 2026-05-09
**선행 문서:**
diff --git a/docs/superpowers/specs/2026-05-14-v04-notebooks-lifecycle-design.md b/docs/superpowers/specs/2026-05-14-v04-notebooks-lifecycle-design.md
new file mode 100644
index 0000000..5e4b49a
--- /dev/null
+++ b/docs/superpowers/specs/2026-05-14-v04-notebooks-lifecycle-design.md
@@ -0,0 +1,327 @@
+# v0.4 — Notebooks + Lifecycle Simplification Design
+
+**작성일:** 2026-05-14
+**선행 문서:**
+
+- `docs/superpowers/specs/2026-04-25-dogfood-feedback.md` F25 (raw idea)
+- `docs/superpowers/specs/2026-05-09-v032-cut-g-design.md` (v0.3.2 Cut G — **deprecate**, 본 문서로 승격)
+- `docs/superpowers/strategy/v028plus-roadmap.md` Cut G position
+
+---
+
+## 1. 정체성
+
+inbox 분류 layer 도입 + lifecycle 단순화. 단일 cut 으로 묶음 — 두 변경이 같은 UI 영역 (헤더 탭 + status 모델) 을 동시 손대므로 분리 시 중간 상태가 어색.
+
+- **분류 (notebook)**: cross-cutting 컨텍스트. "회사" / "개인" / "학습" 같은 공간 구분. 단일 DB 안 `notebook_id` 컬럼 (옵션 B — Cut G 결정 승계).
+- **lifecycle 단순화**: status 4분기 → 3분기. `archived` 제거.
+
+---
+
+## 2. dogfood 근거 (왜 이 시점에 이 cut)
+
+2026-04-25 ~ 2026-05-14 (19일) telemetry + DB:
+
+| 신호 | 수치 | 의미 |
+|------|------|------|
+| `archived` 노트 | 0건 | 4분기 lifecycle 중 1차원 dead — 사용자가 한 번도 사용 안 함 |
+| `active` 노트 | 10건 | 사용 중 |
+| `completed` 노트 | 8건 | 적극 사용 |
+| `trashed` 노트 | 20건 | 적극 사용 |
+| top tag `mlx-ops` | 6건 | 단일 tag 가 사실상 "MLX팀 메모" 컨텍스트 그룹 역할 — tag 의 분류 욕구 명확 |
+| tag vocab 적중률 | 32.8% (19/58) | 새 tag 빈번 생성 — 분류 욕구 일관성 부족, grouping UI 필요 |
+
+→ **archived 는 제거 안전** (마이그레이션 영향 0건). **분류 욕구는 데이터로 확인**되었으나 tag 만으로 충족 어려움 (적중률 낮음).
+
+사용자 우려 ("분류 × lifecycle 햇갈림") 의 대응: archived 제거로 한 차원 줄임. 결과 layer = (notebook × 3분기 × tag) — 3차원이지만 archived 0건 빼서 정신 부담 줄음.
+
+---
+
+## 3. 범위
+
+| 항목 | 결정 |
+|------|------|
+| **분류 모델** | 옵션 B — `notebook_id` 카테고리, 단일 DB. 옵션 A (다중 profile + 비밀번호 잠금) 는 v0.5+ 후보. |
+| **lifecycle 차원** | 3분기 — `active` (inbox) / `completed` (완료) / `trashed` (휴지통). `archived` 제거. |
+| **default notebook** | 마이그레이션 시 "기본" 1개 자동 생성. 모든 기존 노트 배치. |
+| **사이드바 가시성** | 사용자 토글 + last state (`settings.sidebar_visible`). Cmd+B / Ctrl+B 단축키. |
+| **사이드바 내용** | 상단 notebook 목록 + 하단 메모 list (compact view, current notebook + current view 필터). |
+| **사이드바 폭** | default 240px, `settings.sidebar_width` 조정 가능 (180-400). |
+| **notebook 삭제** | FK RESTRICT — 메모 잔류 시 throw. UI 가 "메모 N건 이동 후 재시도" 안내. |
+| **search scope** | 기본 current notebook, 사용자가 dropdown 으로 "모든 노트북" 전환 가능. |
+| **새 capture 의 notebook** | current `selectedNotebookId`. 사용자가 다른 notebook 에 있으면 거기 저장. |
+
+---
+
+## 4. Schema 마이그레이션 (m007)
+
+```sql
+-- 1. notebooks 테이블 생성
+CREATE TABLE notebooks (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ color TEXT,
+ created_at TEXT NOT NULL,
+ updated_at TEXT NOT NULL
+);
+CREATE UNIQUE INDEX idx_notebooks_name ON notebooks(name);
+
+-- 2. notes.notebook_id 추가 (FK, NOT NULL — default 통해 보장)
+ALTER TABLE notes ADD COLUMN notebook_id TEXT
+ REFERENCES notebooks(id) ON DELETE RESTRICT;
+
+-- 3. default notebook "기본" 생성
+INSERT INTO notebooks (id, name, created_at, updated_at)
+ VALUES ('', '기본', '', '');
+
+-- 4. 모든 기존 노트를 default notebook 에 배치
+UPDATE notes SET notebook_id = '' WHERE notebook_id IS NULL;
+
+-- 5. NOT NULL 제약 추가 (마이그레이션 후)
+-- SQLite ALTER 제약: 새 테이블 만들고 복사 패턴 사용 (better-sqlite3 표준)
+
+-- 6. archived → completed 정리 (실제 0건이라 no-op, 안전 위해 명시)
+UPDATE notes SET status = 'completed' WHERE status = 'archived';
+
+-- 7. status CHECK constraint 갱신: archived 제거
+-- 마찬가지로 새 테이블 만들고 복사 패턴
+```
+
+마이그레이션 안전성: 기존 archived 노트 0건 확인 (telemetry + DB query). default notebook 자동 생성으로 기존 사용자 영향 없음.
+
+---
+
+## 5. NotebookRepository
+
+```ts
+interface Notebook {
+ id: string;
+ name: string;
+ color: string | null;
+ createdAt: string;
+ updatedAt: string;
+ noteCount: number; // active 노트만 (trashed 제외)
+}
+
+class NotebookRepository {
+ list(): Notebook[]; // count 포함, name ASC
+ findById(id: string): Notebook | null;
+ create(input: { name: string; color?: string }): Notebook;
+ rename(id: string, name: string): void; // UNIQUE name violation throw
+ setColor(id: string, color: string | null): void;
+ delete(id: string): { ok: true } | { ok: false; reason: 'has_notes' };
+ // 메모 잔류 시 FK RESTRICT throw → ok:false 변환
+
+ moveNote(noteId: string, notebookId: string): void;
+ countByNotebook(): Map; // 한번 호출로 전체 count
+}
+```
+
+---
+
+## 6. UI — 사이드바
+
+```
+┌───────────────┬─────────────────────────────────────┐
+│ [≡] Inkling │ [Inbox(N) 완료(N) 휴지통(N)] [🔍] [⚙] │
+├───────────────┼─────────────────────────────────────┤
+│ ● 기본 (5) │ │
+│ ● 회사 (12) │ NoteCard list (main pane) │
+│ ● 학습 (3) │ │
+│ + 새 노트북 │ │
+│ ───── │ │
+│ 메모 빠른 list │ │
+│ ・제목1 │ │
+│ ・제목2 #tag │ │
+│ ・제목3 │ │
+└───────────────┴─────────────────────────────────────┘
+```
+
+- 좌측, 폭 240px (사용자 조정 가능)
+- 헤더 좌측 `≡` 클릭 → 토글. 단축키 `Cmd+B` / `Ctrl+B`
+- 상단: notebook 목록 (active 노트 count badge). 클릭 → `selectedNotebookId` 변경
+- "+ 새 노트북" 클릭 → NotebookCreateModal (name + optional color)
+- 노트북 우클릭 / hover icon → rename / color 변경 / delete
+- 하단: 메모 list — `selectedNotebookId` + `selectedView` (status 탭) 필터 결과의 compact view. 클릭 → main pane 의 noteCard 로 scroll.
+
+---
+
+## 7. UI — lifecycle 단순화
+
+| 변경 | 내용 |
+|------|------|
+| 헤더 탭 | "Inbox / 완료 / 보관 / 휴지통" → **"Inbox / 완료 / 휴지통"** (3탭) |
+| MoveStatusModal | "보관" 옵션 제거. 사용자 선택지: "완료" / "휴지통" (또는 "복원" — trash mode 에서) |
+| countsByStatus IPC | `archived` 필드 제거. 기존 호출자 (헤더 badge) 적용 |
+| NoteRepository.setStatus | `archived` 인자 받으면 throw (defensive). 마이그레이션 후 호출자 없음 |
+| 데이터 호환성 | 기존 archived 노트 0건이라 마이그레이션 단계에서 `completed` 로 일괄 이동 |
+
+---
+
+## 8. store / IPC
+
+```ts
+interface InboxStore {
+ notebooks: Notebook[];
+ selectedNotebookId: string | null; // null = "모든 노트북" 검색 결과 등 특수 모드
+ sidebarVisible: boolean;
+ sidebarWidth: number;
+ loadNotebooks: () => Promise;
+ selectNotebook: (id: string) => void;
+ createNotebook: (name: string, color?: string) => Promise<{ ok: boolean; reason?: string }>;
+ renameNotebook: (id: string, name: string) => Promise<{ ok: boolean; reason?: string }>;
+ setNotebookColor: (id: string, color: string | null) => Promise;
+ deleteNotebook: (id: string) => Promise<{ ok: boolean; reason?: string }>;
+ moveNoteToNotebook: (noteId: string, notebookId: string) => Promise;
+ toggleSidebar: () => void;
+}
+```
+
+IPC channels (신설):
+- `notebook:list` / `notebook:create` / `notebook:rename` / `notebook:set-color` / `notebook:delete`
+- `notebook:move-note`
+
+기존 변경:
+- `inbox:list` / `inbox:list-by-status` 에 `notebookId?` 옵션 추가 (없으면 모든 노트북)
+- `inbox:counts-by-status` 도 `notebookId?` 옵션 + `archived` 필드 제거
+
+---
+
+## 9. search 통합
+
+- 기본 scope: `selectedNotebookId` 안 검색
+- search box 옆 dropdown: "이 노트북" / "모든 노트북"
+- `inbox:search` 에 `notebookId?` 옵션 추가
+- search 결과 NoteCard 가 notebook 표시 (모든 노트북 검색 시) — 작은 색 chip + 이름
+
+---
+
+## 10. sync 영향 (Cut E 이후)
+
+- export 시 frontmatter 에 `notebook` 필드 추가 (이름 — 다기기 간 id 충돌 회피)
+- import 시 notebook 이름이 없으면 생성, 있으면 reuse
+- 충돌: 같은 이름 + 다른 id 면 import side 의 id 우선 (deterministic)
+
+---
+
+## 11. AI × Notebook 통합
+
+### 11-1. fit 매칭 (매 capture)
+
+- `buildPrompt` 가 현재 notebooks 목록 (이름) 을 prompt 에 포함
+- AI 응답 JSON 에 `notebook_match` 필드 (기존 notebook 이름 또는 null) 추가
+- schema (Zod) 갱신: `notebook_match: z.string().nullable().optional()`
+- 매치 성공 시 자동 배치 (Zero-Effort 가치 우선). NoteCard 의 notebook chip 클릭으로 1-click 변경 가능
+- 매치 실패 / null → default "기본" notebook
+
+prompt 추가 라인 예시 (한국어 자연어):
+
+```
+사용 가능한 노트북: 기본, 회사, 학습
+이 노트가 위 노트북 중 하나에 명확히 속하면 그 이름을 "notebook_match" 에 반환. 그렇지 않으면 null.
+새 노트북 이름을 만들지 말 것 — 기존 목록 안에서만 선택.
+```
+
+### 11-2. promotion 제안
+
+**trigger** — 같은 tag 가 **active 노트 안 3건 이상** 누적되고 그 노트들이 모두 default "기본" notebook 에 있을 때 (`active` + `notebook_id = default` + 동일 tag).
+
+분석 timing — inbox 열릴 때 lazy. 단순 SQL aggregation 이라 cost 무시 가능.
+
+```sql
+SELECT t.name, COUNT(DISTINCT n.id) AS cnt
+ FROM tags t
+ JOIN note_tags nt ON nt.tag_id = t.id
+ JOIN notes n ON n.id = nt.note_id
+ WHERE n.status = 'active'
+ AND n.notebook_id = ''
+ GROUP BY t.id
+HAVING cnt >= 3
+```
+
+매치 발생 시 InboxStore 에 `promotionCandidate: { tag, noteIds, suggestedName }` 채움. banner 가 표시.
+
+**v0.4 first release 는 rule only** — LLM 의 semantic cluster 분석은 cost 큼. dogfood 결과 보고 v0.5+ 검토.
+
+### 11-3. UI — PromotionBanner
+
+- Inbox 상단, RecallBanner / ExpiryBanner 와 같은 위치
+- 문구 예: "💡 `mlx-ops` 관련 노트 6개가 모였어요. 새 노트북 **MLX Ops** 로 분리할까요?"
+- 액션: `[수락]` `[나중에]` `[숨기기]`
+- 수락 → 작은 modal 띄움 (이름 inline 수정 + color 선택) → notebook 생성 + 해당 noteIds 일괄 이동 + 사이드바 자동 열어 새 notebook 가시화
+- 나중에 → 24h snooze (RecallBanner 패턴)
+- 숨기기 → 해당 tag 영구 dismiss (`settings.promotion_dismissed_tags` array)
+
+### 11-4. notebook 이름 generation
+
+- default: tag 이름을 Title Case 변환 (예: `mlx-ops` → `MLX Ops`, `user-management` → `User Management`)
+- 사용자가 수락 modal 에서 inline 수정 가능
+
+### 11-5. promotion 후 status 보존
+
+- 이동된 노트의 `status` 그대로 유지 (active → active, completed → completed)
+- `notebook_id` 만 변경
+
+### 11-6. 새 노트의 자동 fit 매칭이 promoted notebook 도 인식
+
+- promotion 후 notebooks 목록에 새 notebook 포함 → 다음 capture 의 prompt 에 자동 반영
+- 그 후 같은 주제 노트는 자동으로 새 notebook 에 들어감
+
+---
+
+## 12. 테스트 전략
+
+| 영역 | 케이스 |
+|------|--------|
+| NotebookRepository | CRUD + count + delete RESTRICT |
+| migration m007 | default notebook 생성 + 기존 노트 마이그레이션 + archived → completed |
+| store actions | 토글 + 선택 변경 + create/rename/delete 흐름 |
+| UI 사이드바 | notebook 목록 render + 클릭 selectedNotebookId 변경 + count badge |
+| 메모 list 필터 | notebook + view 필터 combination |
+| search scope | current / all 옵션별 결과 |
+| MoveStatusModal | archived 옵션 부재 |
+| 헤더 탭 | 3탭 + count |
+| AI prompt notebook_match | prompt 에 notebooks 목록 포함 + AI 응답 schema 의 notebook_match 필드 |
+| capture 자동 fit | AI 응답 notebook_match 매치 시 자동 배치, 미매치 시 default |
+| promotion query | tag 3건 이상 + default notebook 클러스터 검출 |
+| PromotionBanner | 수락 → notebook 생성 + 일괄 이동 / 나중에 → 24h snooze / 숨기기 → tag dismiss 영구 저장 |
+
+---
+
+## 13. risks
+
+| risk | 대응 |
+|------|------|
+| migration 의 status check constraint 갱신 실패 | SQLite 새 테이블 복사 패턴 + 검증 query 후 commit |
+| 좁은 화면 (1280×720) 에서 사이드바 부담 | settings.sidebar_visible default false (사용자가 켜야 보임) |
+| notebook 삭제 시 RESTRICT error UX | "메모 N건 이동 후 다시 시도" + 이동 dialog 제공 |
+| sync (Cut E) 와 결합 시 notebook 정합성 | frontmatter notebook 이름 기반 — 다기기 간 ID 충돌 회피 |
+| tag 와 notebook 의미 혼동 | UI text 명시: "노트북 = 컨텍스트 (회사 / 개인)", "태그 = 주제 키워드 (mlx-ops, keycloak)" — 설정 페이지 도움말에 안내 |
+| 사용자가 다중 notebook 실제 안 만들면 default 1개 + 사이드바 = noise | default sidebar hidden + 사용자가 2번째 notebook 만들 때 자동 reveal hint |
+| AI 가 notebook_match 에 새 이름 ad-hoc 생성 시도 | prompt 에 "기존 목록 안에서만 선택, 새 이름 만들지 말 것" 명시 + schema 검증 단계에서 notebooks 목록과 매치 안 되는 값은 null 로 coerce |
+| 같은 tag 의 promotion 이 반복 노출되어 사용자 피로 | "숨기기" → 영구 dismiss (settings 의 array). RecallBanner 의 24h snooze 와 별개 영구 저장소 |
+
+---
+
+## 14. 게이트
+
+- 단위 테스트 — notebook CRUD + migration + UI 사이드바 + 메모 list 필터 + status 3분기 회귀 + AI fit/promotion 분기
+- 마이그레이션 verify — fresh DB + 기존 DB 두 시나리오에서 default notebook 생성 + archived 정리
+- dogfood — 2-3 일 후 (1) 사이드바 열려있는 비율, (2) notebook 갯수 (수동 vs promoted), (3) notebook 간 메모 이동 빈도, (4) promotion 수락률 측정
+
+---
+
+## 15. 작업 분해 (writing-plans 입력)
+
+1. **m007 마이그레이션** — notebooks 테이블 + notes.notebook_id + archived 정리 + status enum
+2. **NotebookRepository** + IPC 핸들러
+3. **NoteRepository** 변경 — notebook_id 필터, list/search/counts 옵션 확장
+4. **store** — notebooks state + actions + promotionCandidate state
+5. **AI prompt 변경** — buildPrompt 에 notebooks 목록 주입, schema 의 notebook_match 필드, AiWorker 가 응답 매치하여 자동 배치 (또는 default 로 coerce)
+6. **promotion 분석** — NoteRepository.findPromotionCandidates 쿼리 + store action (inbox open 시 lazy)
+7. **PromotionBanner UI** — Inbox 상단, 수락 modal, 24h snooze, 영구 dismiss
+8. **사이드바 UI** — Sidebar.tsx + NotebookList + NotebookCreateModal
+9. **헤더 / MoveStatusModal** — archived 제거
+10. **search** — scope 옵션 통합
+11. **sync (옵션 deferred)** — frontmatter notebook 필드. v0.4 본체 머지 후 별도 작업으로 가능
+12. **CHANGELOG + release notes** — dogfood 근거 명시