Skip to content

undo가 기존 키를 덮어쓴 add/copy의 값을 복원하지 않고 삭제 (데이터 유실) #200

Description

@developer-1px

요약 (S0 correctness)

기존 object 멤버를 덮어쓰는 add/copy operation의 inverse가 replace(old value)가 아니라 remove로 계산되어, undo 시 원래 값이 조용히 유실됩니다.

재현 (public API)

const doc = createJSONDocument(z.object({ a: z.number(), d: z.number() }), { a: 50, d: 84 }, { history: 100 });
doc.patch({ op: "add", path: "/a", value: 64 }); // → { a: 64, d: 84 }  (forward 정상)
doc.undo();                                       // → { d: 84 }  ❌ a:50 유실 (기대 { a:50, d:84 })

// copy 도 동일
// { a:1, b:2 } 에서 copy /a→/b → { a:1, b:1 } → undo → { a:1 }  ❌ b:2 유실

{ history: N } 로 undo 를 켠 정상 사용 경로에서 발생합니다.

원인

src/foundation/patch/inverse.ts:33-44 inverseOp 의 add/copy 케이스가 항상 { op: "remove", path } 를 반환합니다.

  • 배열 index 삽입은 shift 이므로 remove 가 옳지만,
  • object 멤버 덮어쓰기는 이전 값을 replace 로 복원해야 합니다.

multi-op 루트 변형 computeRootObjectAddInverses (inverse.ts:275) 는 objectHasOwnreplace(old value) 로 이미 올바르게 처리하지만, single-op 경로와 generic fallback loop 는 처리하지 않습니다.

core-standard §5/§11 (undo 는 기록된 document 상태로 복원 MUST) 위반.

수정 방향

inverseOp add/copy 케이스에서 대상 부모가 object 이고 최종 key 가 이미 존재하면 replace(old value), 그 외(배열 삽입/신규 key)는 remove. root path ""replace("", before).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions