0.1.0-draft

내부 인터페이스 0002 · Rust 트리 코어

상태: 실험 전용 · 인터페이스 버전: 0.1.0-draft · 공개 API: 아님

이 명세는 crates/spinon-core의 플랫폼 비종속 Rust 트리 상태와 변경 묶음 계약을 기록합니다. spinon-ffi, V8, 프레임워크 어댑터, 레이아웃·렌더러와 연결되지 않았습니다. 웹 호환이나 앱 작성자용 HTML API를 약속하지 않습니다.

코드는 id.rs(노드 ID·revision), batch.rs(작업·변경 목록), error.rs(실패 진단), tree.rs(노드 상태·검증·커밋)로 나뉘며 lib.rs는 crate 경계에서 공개할 Rust 타입을 다시 내보냅니다.

자료 모델

  • NodeId는 0이 아닌 u64입니다. 호출자가 할당합니다. 성공적으로 생성된 노드 ID는 삭제되거나 같은 변경 묶음에서 제거되어도 해당 Tree 수명 동안 재사용하지 않습니다. 실패한 묶음의 ID는 공개된 적이 없으므로 다시 사용할 수 있습니다.
  • Revision은 빈 트리에서 0으로 시작합니다. 변경 작업이 하나 이상인 묶음이 성공하면 한 번 증가합니다. 빈 묶음은 revision을 바꾸지 않습니다. 현재 값이 u64::MAX이면 변경 묶음을 거부합니다.
  • 비어 있지 않은 트리는 연결된 root를 정확히 하나 가집니다. 빈 트리도 허용합니다. 성공한 커밋 이후에는 고아 노드가 남지 않아야 합니다.
  • 노드는 호출자가 지정한 불투명 태그 문자열, 선택적 텍스트, 부모 ID와 순서 있는 자식 ID를 소유합니다. 현재 코어는 태그 허용 목록이나 태그별 자식 규칙을 적용하지 않습니다. 태그는 비어 있거나 공백 문자를 포함하거나 NUL 문자를 포함할 수 없습니다.
  • Tree::nodes()의 반복 순서는 보장되지 않습니다. 형제 시각 순서는 각 부모 노드의 children 순서입니다.
  • 속성·CSS·레이아웃·이벤트·접근성 상태는 이 버전에 포함되지 않습니다.

변경 묶음

ChangeBatch는 생성할 때 기준 revision을 고정하고 다음 작업을 순서대로 받습니다.

작업동작거부 조건 예시
Create새 노드와 태그를 만듭니다. 이 단계의 노드는 아직 연결되지 않습니다.ID 중복, 공백 태그, NUL 포함 태그
Insert분리된 노드를 root 또는 부모의 0 기반 자식 위치에 연결합니다. root 위치는 (parent: None, index: 0)입니다.알 수 없는 노드·부모, 이미 연결된 노드, 범위를 벗어난 위치, 두 번째 root
UpdateText노드의 텍스트 값을 설정합니다. 빈 문자열도 유효합니다.알 수 없는 노드
Move연결된 노드와 그 하위 트리를 새 부모와 위치로 옮깁니다. 목적 위치는 원래 부모에서 노드를 분리한 뒤의 자식 목록을 기준으로 합니다.알 수 없는 노드·부모, 연결되지 않은 노드, 순환, 잘못된 위치, 두 번째 root
Remove노드와 하위 트리를 제거합니다. 결과에는 제거 ID가 선행 순회 순서로 포함됩니다.알 수 없는 노드

한 묶음에서 생성한 노드는 같은 묶음 안에서 삽입·수정·이동·삭제할 수 있습니다. 성공 시 CommitReceipt는 이전·새 revision과 작업 순서에 대응하는 변경 목록을 돌려줍니다. 이 목록은 UI 표시나 프레임 완료 신호가 아닙니다.

성공·오류·복구

  1. 호출자는 현재 Tree::revision()과 같은 기준 revision으로 묶음을 만듭니다.
  2. 코어는 트리 후보 복사본에 작업을 순서대로 적용하고, root·부모·자식 연결과 도달 가능성을 검증합니다.
  3. 모든 작업과 최종 검증이 성공하면 변경 묶음 전체와 새 revision을 한 번에 공개합니다.
  4. 기준 revision이 오래됐거나 작업·최종 검증·revision 증가가 실패하면 묶음 전체를 버립니다. 트리 상태, 사용된 ID 집합, revision은 호출 전 값으로 유지됩니다. Rust 패닉이나 프로세스 수준 메모리 할당 실패는 반환 오류 계약에 포함되지 않습니다.
  5. 작업 오류는 0 기반 operation_index를 포함합니다. 기준 revision 불일치와 최종 트리 불변식 오류에는 해당 인덱스가 없습니다.

자동 재시도는 하지 않습니다. 호출자는 오류를 확인하고 원인을 고친 새 묶음을 만들어야 합니다. 실패 묶음의 기준 revision은 여전히 현재 revision과 일치하므로 오류가 일시적이지 않고 입력이 그대로라면 같은 실패가 반복됩니다. &mut Tree를 요구하므로 Rust 호출자는 동시에 커밋할 수 없습니다. FFI 또는 외부 큐가 직렬 실행을 보장하는 방식은 아직 정하지 않았습니다.

최소 예제

use spinon_core::{ChangeBatch, NodeId, Tree};

let root = NodeId::new(1).expect("0은 예약된 ID입니다");
let label = NodeId::new(2).expect("0은 예약된 ID입니다");
let mut tree = Tree::new();
let mut batch = ChangeBatch::new(tree.revision());
batch
    .create(root, "div")
    .insert(root, None, 0)
    .create(label, "span")
    .insert(label, Some(root), 0)
    .update_text(label, "Spinon");

let receipt = tree.commit(batch)?;
assert_eq!(receipt.revision().get(), 1);
assert_eq!(tree.node(label).and_then(|node| node.text()), Some("Spinon"));

같은 코드를 실행하는 예제는 crates/spinon-core/examples/minimal_tree.rs에 있습니다.

구현 경계와 미지원

  • 이 버전은 Rust 라이브러리 호출만 제공합니다. C ABI, V8 호출, JavaScript 이벤트 수명과 플랫폼 호스트는 연결하지 않습니다.
  • 커밋은 동기 함수입니다. Tree에 내부 lock이나 비동기 취소 계약이 없습니다. V8 Isolate·UI 입력·GPU 표면의 thread affinity와 수명은 이 명세에서 정하지 않습니다.
  • 현재 구현은 성공 전에 활성 노드 맵과 노드를 복사해 후보 상태를 만듭니다. 과거 ID 집합은 복사하지 않습니다. 원자성을 확인하기 위한 초기 구현이며, 크기별 시간·메모리 목표나 성능 우위를 뜻하지 않습니다.
  • ID 재사용 방지를 위해 성공적으로 생성된 모든 ID를 Tree 수명 동안 보관합니다. 메모리는 해당 수명에서 만든 총 노드 수에 따라 증가하므로 장시간 반복 생성·삭제하는 앱에 적용하기 전 ID 할당·세대 정책을 다시 검토해야 합니다.
  • Android·iOS 동작 차이는 없습니다. 코어 자체는 표준 라이브러리만 사용하지만 모바일 크로스 컴파일 및 앱 연결은 별도 증거가 필요합니다.
  • spec/0002-ui-tree-events.md의 나머지 웹·모바일 이벤트 의미는 제안 상태입니다. 이 내부 계약은 화면 렌더링, 레이아웃, GPU, 터치, 접근성이나 OTA를 구현 완료로 만들지 않습니다.