Files
inkling/docs/superpowers/specs/2026-05-09-v0210-cut-c-design.md

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.md Cut 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)