fix(v0210): importNote 가 capture revision 을 함께 INSERT (final review fix)
final code review 발견: F5 import 후 first user edit 시 import 시점 본문이 note_revisions 에 없어 history 에서 사라지는 회귀. importNote transaction 안 INSERT 추가 (createdAt = edited_at). 부수 작업: ImportNoteInput / importNote 의 "raw_text invariant guard" 주석을 v0.2.10 의 'fork-on-id-collision (sync determinism)' 정확한 의미로 갱신. 테스트 +2 — insert path / fork path 모두 capture revision 검증.
This commit is contained in:
@@ -22,7 +22,10 @@ export interface NewMediaRow {
|
|||||||
|
|
||||||
export interface ImportNoteInput {
|
export interface ImportNoteInput {
|
||||||
/** Proposed id from the export file. May be replaced if it collides with
|
/** Proposed id from the export file. May be replaced if it collides with
|
||||||
* an existing row whose `raw_text` differs (raw_text invariant guard). */
|
* an existing row whose `raw_text` differs — fork-on-conflict so a single
|
||||||
|
* id never resolves to two distinct historical baselines (v0.2.10 Cut C
|
||||||
|
* changed `raw_text 불변` policy → `raw_text 가변` + revision history; the
|
||||||
|
* baseline distinction is now preserved per-id, edit history per-note). */
|
||||||
id: string;
|
id: string;
|
||||||
rawText: string;
|
rawText: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -681,11 +684,17 @@ export class NoteRepository {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Import a note from an external source (F5 export tree).
|
* Import a note from an external source (F5 export tree).
|
||||||
* Conflict policy:
|
* Conflict policy (fork-on-id-collision):
|
||||||
* - id missing in DB → INSERT (status: 'inserted')
|
* - id missing in DB → INSERT (status: 'inserted')
|
||||||
* - id present + raw_text identical → no-op (status: 'skipped')
|
* - id present + raw_text identical → no-op (status: 'skipped')
|
||||||
* - id present + raw_text differs → INSERT under fresh uuidv7
|
* - id present + raw_text differs → INSERT under fresh uuidv7 so the same id
|
||||||
* to preserve the raw_text-immutable invariant (status: 'forked')
|
* never points at two different baselines (status: 'forked'). v0.2.10 Cut C
|
||||||
|
* relaxed the `raw_text 불변` policy → `raw_text 가변 + note_revisions 보존`,
|
||||||
|
* but per-id baseline distinction is still required for sync determinism.
|
||||||
|
*
|
||||||
|
* v0.2.10 Cut C — INSERT/fork 시 동일 transaction 안에서 note_revisions 에
|
||||||
|
* 'capture' 첫 revision INSERT (createdAt = edited_at). 미수행 시 first user
|
||||||
|
* edit 직후 import 시점 본문이 history 에서 사라지는 회귀 (final review 발견).
|
||||||
*
|
*
|
||||||
* deletedAt merge (v0.2.3 #4, spec §8.2): source/dest 중 IS NOT NULL 우선
|
* deletedAt merge (v0.2.3 #4, spec §8.2): source/dest 중 IS NOT NULL 우선
|
||||||
* (삭제 보존). skip 케이스에서 source NN + dest NULL 일 때만 dest 갱신.
|
* (삭제 보존). skip 케이스에서 source NN + dest NULL 일 때만 dest 갱신.
|
||||||
@@ -736,6 +745,12 @@ export class NoteRepository {
|
|||||||
input.createdAt,
|
input.createdAt,
|
||||||
input.updatedAt
|
input.updatedAt
|
||||||
);
|
);
|
||||||
|
this.db
|
||||||
|
.prepare(
|
||||||
|
`INSERT INTO note_revisions (note_id, raw_text, edited_at, edited_by)
|
||||||
|
VALUES (?, ?, ?, 'capture')`
|
||||||
|
)
|
||||||
|
.run(finalId, input.rawText, input.createdAt);
|
||||||
if (input.tags.length > 0) {
|
if (input.tags.length > 0) {
|
||||||
const getOrInsertTag = this.db.prepare(
|
const getOrInsertTag = this.db.prepare(
|
||||||
`INSERT INTO tags(name) VALUES(?) ON CONFLICT(name) DO UPDATE SET name=name RETURNING id`
|
`INSERT INTO tags(name) VALUES(?) ON CONFLICT(name) DO UPDATE SET name=name RETURNING id`
|
||||||
|
|||||||
@@ -107,4 +107,65 @@ describe('NoteRepository — note_revisions', () => {
|
|||||||
expect(note?.rawText).toBe('v2 corrected');
|
expect(note?.rawText).toBe('v2 corrected');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('importNote — capture revision 생성 (final review 보강)', () => {
|
||||||
|
it('insert path: imported note 가 capture revision (createdAt = edited_at) 을 함께 갖는다', () => {
|
||||||
|
const r = repo.importNote({
|
||||||
|
id: '00000000-0000-0000-0000-000000000001',
|
||||||
|
rawText: 'imported text',
|
||||||
|
createdAt: '2026-04-01T00:00:00Z',
|
||||||
|
updatedAt: '2026-04-02T00:00:00Z',
|
||||||
|
aiTitle: 't',
|
||||||
|
aiSummary: 's',
|
||||||
|
titleEditedByUser: false,
|
||||||
|
summaryEditedByUser: false,
|
||||||
|
aiProvider: 'p',
|
||||||
|
aiGeneratedAt: '2026-04-02T00:00:00Z',
|
||||||
|
userIntent: null,
|
||||||
|
intentPromptedAt: null,
|
||||||
|
tags: []
|
||||||
|
});
|
||||||
|
expect(r.status).toBe('inserted');
|
||||||
|
|
||||||
|
const revs = repo.listRevisions(r.id);
|
||||||
|
expect(revs).toHaveLength(1);
|
||||||
|
expect(revs[0]!.rawText).toBe('imported text');
|
||||||
|
expect(revs[0]!.editedBy).toBe('capture');
|
||||||
|
expect(revs[0]!.editedAt).toBe('2026-04-01T00:00:00Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fork path: id 충돌 시 fresh uuidv7 + 새 capture revision (옛 노트 revision 보존)', () => {
|
||||||
|
// 기존 노트 (capture 'v1' revision 자동 생성됨)
|
||||||
|
const existing = repo.create({ rawText: 'v1' });
|
||||||
|
// 동일 id 로 다른 raw_text 를 import → fork
|
||||||
|
const r = repo.importNote({
|
||||||
|
id: existing.id,
|
||||||
|
rawText: 'imported v2',
|
||||||
|
createdAt: '2026-04-01T00:00:00Z',
|
||||||
|
updatedAt: '2026-04-02T00:00:00Z',
|
||||||
|
aiTitle: null,
|
||||||
|
aiSummary: null,
|
||||||
|
titleEditedByUser: false,
|
||||||
|
summaryEditedByUser: false,
|
||||||
|
aiProvider: null,
|
||||||
|
aiGeneratedAt: null,
|
||||||
|
userIntent: null,
|
||||||
|
intentPromptedAt: null,
|
||||||
|
tags: []
|
||||||
|
});
|
||||||
|
expect(r.status).toBe('forked');
|
||||||
|
expect(r.id).not.toBe(existing.id);
|
||||||
|
|
||||||
|
// forked 노트에 capture revision
|
||||||
|
const forkRevs = repo.listRevisions(r.id);
|
||||||
|
expect(forkRevs).toHaveLength(1);
|
||||||
|
expect(forkRevs[0]!.rawText).toBe('imported v2');
|
||||||
|
expect(forkRevs[0]!.editedBy).toBe('capture');
|
||||||
|
|
||||||
|
// 기존 노트의 revision 은 그대로 보존
|
||||||
|
const existingRevs = repo.listRevisions(existing.id);
|
||||||
|
expect(existingRevs).toHaveLength(1);
|
||||||
|
expect(existingRevs[0]!.rawText).toBe('v1');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user