init: llm-wiki-haness 하네스 설계

This commit is contained in:
DongHyeonka
2026-07-24 14:21:35 +09:00
parent 42bf3db4fd
commit 6c53ded9cb
2436 changed files with 194486 additions and 1 deletions
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""Recoverable multi-file replacement using same-filesystem ``os.replace``."""
from __future__ import annotations
import os
import shutil
from dataclasses import dataclass
from pathlib import Path
import tempfile
from typing import Mapping
class TransactionError(OSError):
"""A commit failed; rollback details are included in the message."""
@dataclass(frozen=True)
class SymlinkValue:
"""A lexical symlink target to install with the surrounding transaction."""
target: str
ReplacementValue = bytes | SymlinkValue
OriginalValue = tuple[str, bytes | str, int | None]
def _lexical_absolute(path: Path) -> Path:
"""Make a path absolute without following an existing symlink."""
return Path(os.path.abspath(path))
def _temporary_bytes(target: Path, data: bytes, mode: int | None = None) -> Path:
target.parent.mkdir(parents=True, exist_ok=True)
descriptor, temporary_name = tempfile.mkstemp(prefix=f".{target.name}.", suffix=".tmp", dir=target.parent)
temporary = Path(temporary_name)
try:
with os.fdopen(descriptor, "wb") as stream:
stream.write(data)
stream.flush()
os.fsync(stream.fileno())
if mode is not None:
os.chmod(temporary, mode)
return temporary
except Exception:
temporary.unlink(missing_ok=True)
raise
def _temporary_symlink(target: Path, link_target: str) -> Path:
target.parent.mkdir(parents=True, exist_ok=True)
descriptor, temporary_name = tempfile.mkstemp(prefix=f".{target.name}.", suffix=".tmp", dir=target.parent)
os.close(descriptor)
temporary = Path(temporary_name)
temporary.unlink()
try:
os.symlink(link_target, temporary)
return temporary
except Exception:
temporary.unlink(missing_ok=True)
raise
def _temporary_value(target: Path, value: ReplacementValue, mode: int | None = None) -> Path:
if isinstance(value, SymlinkValue):
return _temporary_symlink(target, value.target)
return _temporary_bytes(target, value, mode)
def _capture_original(target: Path) -> OriginalValue | None:
if target.is_symlink():
return ("symlink", os.readlink(target), None)
if target.exists():
return ("bytes", target.read_bytes(), target.stat().st_mode & 0o777)
return None
def _write_target(path: Path, value: ReplacementValue, follow_symlinks: bool) -> Path:
"""Pick the entry this value should land on.
vault cutover 이후 ``raw/**``·``wiki/**`` 의 문서는 ``vault/**`` 정본을 가리키는
심링크다. ``os.replace`` 는 심링크를 *따라가지 않고* 그 자리를 실파일로 갈아치우므로,
호출자가 심링크 경로를 그대로 넘기면 (1) 호환 계층이 끊기고 (2) 정본은 낡은 채로
남는 split-brain 이 된다 — 그리고 그 사실이 조용하다. 그래서 bytes 쓰기는 기본적으로
심링크를 따라 정본에 쓴다.
``SymlinkValue`` 는 링크 *자체* 를 설치하는 것이므로 언제나 lexical 경로에 쓴다.
``follow_symlinks=False`` 는 cutover/rollback 처럼 심링크를 실파일로 되돌리는 것이
목적인 호출자(``vault_migrate``)를 위한 예외다.
"""
if isinstance(value, SymlinkValue) or not follow_symlinks:
return _lexical_absolute(path)
if path.is_symlink():
return _lexical_absolute(path.resolve())
return _lexical_absolute(path)
_STAGE_IGNORE = shutil.ignore_patterns(".git", "__pycache__", "*.pyc", ".DS_Store")
def stage_repository(root: Path, destination: Path) -> None:
"""Copy the working tree into ``destination`` *preserving symlinks*.
canonical 모드에서 ``raw/``·``wiki/`` 는 ``vault/`` 정본을 가리키는 상대 심링크
디렉토리다. 기본 ``copytree``(``symlinks=False``)는 각 링크를 따라가 실파일로 복제하므로
스테이지가 split-brain(정본·링크가 독립된 실파일 2개)이 되고, candidate 가 legacy 경로로
정본을 우회 편집해도 투영/레이아웃 검증이 그 사실을 못 잡는다. ``symlinks=True`` 로
링크를 링크 그대로 복제해 실제 저장소 구조를 재현한다.
(검증 스테이징의 단일 구현 — 예전엔 document_commit / branch_contract_check /
migrate_graph_contracts 에 같은 함수가 세 벌 복사돼 있었고, 그 중 하나만 고치면
나머지가 조용히 어긋났다.)
"""
for child in root.iterdir():
if child.name == ".git":
continue
target = destination / child.name
if child.is_dir():
shutil.copytree(child, target, ignore=_STAGE_IGNORE, symlinks=True)
elif child.is_file():
shutil.copy2(child, target, follow_symlinks=False)
def replace_many(
changes: Mapping[Path, ReplacementValue],
*,
must_not_exist: set[Path] | None = None,
follow_symlinks: bool = True,
) -> None:
"""Replace bytes/symlinks atomically and restore the original entry kind on failure."""
forbidden = {_lexical_absolute(path) for path in (must_not_exist or set())}
normalized = {
_write_target(path, data, follow_symlinks): data for path, data in changes.items()
}
for target in forbidden:
if target.exists() or target.is_symlink():
raise FileExistsError(f"target already exists: {target}")
originals: dict[Path, OriginalValue | None] = {}
pending: dict[Path, Path] = {}
committed: list[Path] = []
try:
for target, value in normalized.items():
originals[target] = _capture_original(target)
original_mode = originals[target][2] if originals[target] is not None else None
pending[target] = _temporary_value(target, value, original_mode)
except Exception:
for temporary in pending.values():
temporary.unlink(missing_ok=True)
raise
try:
for target in sorted(pending, key=lambda item: item.as_posix()):
os.replace(pending[target], target)
committed.append(target)
except Exception as exc:
rollback_errors: list[str] = []
for target in reversed(committed):
original = originals[target]
try:
if original is None:
target.unlink(missing_ok=True)
else:
kind, payload, mode = original
restore = (
_temporary_symlink(target, str(payload))
if kind == "symlink"
else _temporary_bytes(target, bytes(payload), mode)
)
os.replace(restore, target)
except Exception as rollback_exc: # pragma: no cover - catastrophic filesystem failure
rollback_errors.append(f"{target}: {rollback_exc}")
detail = f"; rollback failures: {rollback_errors}" if rollback_errors else "; rollback completed"
raise TransactionError(f"multi-file commit failed: {exc}{detail}") from exc
finally:
for temporary in pending.values():
temporary.unlink(missing_ok=True)