7.2 KiB
v0.2.10 — Cut C Design (raw_text 수정 + revision history)
작성일: 2026-05-09 선행 문서:
docs/superpowers/specs/2026-04-25-dogfood-feedback.md(F20)docs/superpowers/strategy/v028plus-roadmap.mdCut C
Cut 라벨: v0.2.10 — load-bearing invariant 변경 (raw_text 불변 폐기 + revision history). semver 엄밀히 minor 이지만 v0.2.x 관습.
1. Cut 정체성
메모리 정책 raw_text 불변 invariant 폐기 + 변경 이력 (revision) 보존. 사용자가 raw_text 자유 수정 + 옛 버전 회수 가능.
load-bearing 정책 변경:
- 옛:
raw_text 불변(capture 시점 원본 영구 보존) - 새:
raw_text 가변+note_revisions 테이블(옛 버전 모두 보존, rollback 가능)
이는 F1 / F4 / F17 / F19 의 raw_text 가정에 영향 — 모두 current latest raw_text 기준으로 동작 (시간 경과 시 정정된 값 사용).
2. 범위
| 항목 | 결정 |
|---|---|
| F20 | C 옵션 — raw_text 수정 허용 + note_revisions 테이블 + 옛 버전 회수 UI. AI 재실행 input = current latest raw_text (B 옵션). |
3. Schema 마이그레이션 (m006)
메모: 본 스펙 작성 시점에는 m005 로 예상했으나 Cut B (v0.2.9) 에서 m005 (ai_disabled CHECK relax) 가 선점됨 → 실제 번호는 m006.
CREATE TABLE note_revisions (
rev_id INTEGER PRIMARY KEY AUTOINCREMENT,
note_id TEXT NOT NULL,
raw_text TEXT NOT NULL,
edited_at TEXT NOT NULL,
edited_by TEXT NOT NULL DEFAULT 'user', -- 'user' or 'capture' (최초 캡처)
FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE
);
CREATE INDEX idx_note_revisions_note_id ON note_revisions(note_id, edited_at DESC);
-- 기존 notes 의 모든 raw_text 를 첫 revision 으로 backfill
INSERT INTO note_revisions (note_id, raw_text, edited_at, edited_by)
SELECT id, raw_text, created_at, 'capture' FROM notes;
note_revisions.rev_id = AUTOINCREMENT — chronological 순서 보장. edited_by = 'user' (사용자 정정) 또는 'capture' (최초).
notes.raw_text 컬럼 그대로 — current latest 값. 검색 인덱스 (F19 FTS5) 가 이걸 source 로 사용 → revision 검색 X (latest only). YAGNI.
4. NoteRepository 메서드
class NoteRepository {
// 기존
insert(input: ...): Note; // 내부에서 note_revisions INSERT (edited_by='capture')
// 신규
updateRawText(id: string, newText: string, now: Date): void {
const tx = this.db.transaction(() => {
this.db.prepare(`UPDATE notes SET raw_text=?, updated_at=? WHERE id=?`).run(newText, now.toISOString(), id);
this.db.prepare(`INSERT INTO note_revisions (note_id, raw_text, edited_at, edited_by) VALUES (?, ?, ?, 'user')`).run(id, newText, now.toISOString());
});
tx();
}
listRevisions(id: string): NoteRevision[] {
return this.db.prepare(`SELECT * FROM note_revisions WHERE note_id=? ORDER BY edited_at DESC`).all(id) as NoteRevision[];
}
restoreRevision(id: string, revId: number, now: Date): void {
const rev = this.db.prepare(`SELECT raw_text FROM note_revisions WHERE rev_id=? AND note_id=?`).get(revId, id) as { raw_text: string } | undefined;
if (!rev) throw new Error(`revision ${revId} not found`);
this.updateRawText(id, rev.raw_text, now); // 새 revision 으로 복원 (linear history 유지)
}
}
restoreRevision 은 옛 raw_text 를 새 revision 으로 INSERT — chain 끊지 않고 latest = restored. timestamp/순서 명확.
5. UI — NoteCard 수정 흐름
5-1. raw_text 편집 UI
기존 NoteCard 의 "원문 보기" 펼침 → 추가 "편집" 버튼:
{rawOpen && (
<div>
{editingRaw ? (
<>
<textarea value={draftRaw} onChange={e => setDraftRaw(e.target.value)} />
<button onClick={onSaveRaw}>저장</button>
<button onClick={() => setEditingRaw(false)}>취소</button>
</>
) : (
<>
<pre>{local.rawText}</pre>
<button onClick={() => { setDraftRaw(local.rawText); setEditingRaw(true); }}>편집</button>
<button onClick={() => setShowRevisions(true)}>이력</button>
</>
)}
</div>
)}
5-2. Revision 회수 UI
"이력" 클릭 → modal 또는 확장 panel:
이력 (3 buah)
[2026-05-12 14:30 사용자] 본문... [회수]
[2026-05-10 09:15 사용자] 옛 본문... [회수]
[2026-05-09 11:00 캡처] 최초 캡처 본문... [회수]
회수 클릭 → confirm dialog ("이 버전으로 되돌릴까요? 현재 본문도 이력에 보존됩니다.") → restoreRevision() 호출.
6. AI 재실행 정책
입력 = current notes.raw_text (latest). 옛 revision 은 AI 재실행 input X. 정책 일관 (사용자 정정 의도 반영).
AiWorker 의 input 추출 코드는 변경 없음 — notes.raw_text 그대로 사용.
7. F1 (Due Date) / F4 (Aha Moment) / F17 / F19 영향
| 영역 | 영향 |
|---|---|
| F1 Due Date 파서 | input = current raw_text. 사용자 정정 후 due 갱신 가능 — 정책 충실 (수정 시 의도 반영) |
| F4 Aha Moment | capture 카운트 = notes 갯수. revision 갯수 무관 |
| F17 status | 영향 X (raw_text 수정과 status 분기 독립) |
| F19 search FTS5 | 인덱스 source = notes.raw_text (latest). revision 검색 미지원. 향후 cut 에서 옵션 |
8. IPC + types
// 신규
'inbox:update-raw-text': (id: string, newText: string) => Promise<{ ok: true }>
'inbox:list-revisions': (id: string) => Promise<NoteRevision[]>
'inbox:restore-revision': (id: string, revId: number) => Promise<{ ok: true }>
interface NoteRevision {
revId: number;
noteId: string;
rawText: string;
editedAt: string;
editedBy: 'user' | 'capture';
}
9. 테스트 전략
| 영역 | 단위 |
|---|---|
| m006 마이그레이션 | 기존 notes → revision backfill (edited_by='capture') |
updateRawText |
notes.raw_text 갱신 + 새 revision INSERT atomic |
listRevisions |
DESC 순 + edited_by 정확 |
restoreRevision |
옛 raw_text 가 새 revision 으로 INSERT + notes.raw_text 갱신 |
| 편집 UI | textarea 입력 + 저장 → IPC 호출 + store 갱신 |
| 이력 modal | revision 목록 표시 + 회수 클릭 → confirm + IPC |
| AiWorker input | current notes.raw_text 사용 (revision X) 회귀 |
목표: 단위 548 → 약 567 (+19, m006 5 + create rev 1 + updateRawText 2 + listRevisions 1 + restoreRevision 2 + IPC 4 + NoteCard 편집 1 + RevisionHistoryModal 2 + findById 회귀 1), typecheck 0.
10. Risk
| Risk | 대응 |
|---|---|
| revision 무한 누적 (메모 1개당 100+ revision 시 DB bloat) | 향후 cut 에서 N개 cap 정책 (예: 최근 50개만 보존). 본 cut 은 unlimited |
| 사용자가 실수로 옛 revision 회수 | confirm dialog 강제 |
| F1 Due Date 가 raw_text 변경 시 재추출 안 함 | 별도 cut. 본 cut 은 raw_text 갱신 + 기존 due 잔류 (사용자 의도 보존) |
| 메모리 정책 갱신 필수 | project_inkling_status.md 의 load-bearing invariant 갱신 |
11. 메모리 정책 갱신 (Cut C 머지 후 필수)
raw_text 불변 → raw_text 가변 + revision 보존. 메모 갱신:
- ~~raw_text 불변~~ → raw_text 가변 (사용자 편집 가능, note_revisions 테이블에 변경 이력 보존)
- AI 재실행 input = current latest raw_text (옛 revision X)