refactor(file_state): PR 11 — kind_codes 3중 복제 통합 + decision 매핑 table
PYTHON_THINNING_PLAN §5 PR 11. Wave 1.5 amend §C single-source-of-truth 양방향 보강 정합. amend A1 (사용자 보이는 문자열 = Python single source) 보존. 변경: - ``_KIND_CODES`` (4 entries) — module-level constant. RemoteFileKind → Rust REMOTE_KIND_* 매핑. 기존 3중 복제 (open guard / reload / save) 제거. - ``_metadata_to_tuple(meta)`` helper — Rust ABI Optional-tuple 인코딩 단일화. - ``_OPEN_GUARD_REASON_MAP`` (4 entries) — reason_code → enum 단일 lookup. - ``_RELOAD_RECOMMENDATION_MAP`` (4 entries) — reload_code → enum 단일 lookup. - ``_SAVE_CONFLICT_SPECS`` (5 entries) — decision_code → (kind, message, reload_hint) tuple. 기존 6단계 if-chain + inline SaveConflict 생성을 단일 dict + 1줄 unpack 으로 축약. (decision_code 0 / OK 는 inline.) - ``evaluate_save_file`` 본체 ~50 LOC → ~15 LOC. - ``open_guard_reason_for_remote_metadata`` 본체 ~12 LOC → ~6 LOC. - ``reload_recommendation`` 본체 ~30 LOC → ~6 LOC. amend A1 사용자 문자열 정책: - ``_SAVE_CONFLICT_SPECS`` 안의 5종 message string 그대로 보존 — Python이 사용자 보이는 문자열의 single source. Rust ABI는 decision_code (int) 만 반환 (Lint #4 정합). 테스트: PR 10 parity 33 + sublime/tests 전체 1294 그린. pyright (file_state.py CLI): 0 errors. boundary-claim: removes: - sublime/sessions/file_state.py:open_guard kind_codes/reason_map (~12 LOC) - sublime/sessions/file_state.py:reload kind_codes/mapping (~30 LOC) - sublime/sessions/file_state.py:save kind_codes + 6 if-branches (~70 LOC) delete-count: ~85 rust-additions: 0 (Python-only — single-source-of-truth 정합) ban-list: 'amend §C 양방향 + amend A1 사용자 문자열 Python 보존' Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,8 +13,8 @@
|
||||
> | PR 5.5 | ✅ | `c29e3f5` | diagnostics 청산은 *이미 일원화됨* — 인벤토리 정정 (no-op) |
|
||||
> | PR 8 | ✅ | `32fc8ef` | `derive_venv_name` heuristic → `sessions_native::interpreter_probe` (~40 LOC) |
|
||||
> | PR 9 | ✅ no-op | `c19aaae` | tree/list 잔여 호출자 0건 확인 — PR 2가 이미 일원화 완료 |
|
||||
> | PR 10 | ✅ | (TBD) | file_state parity tests +25 (총 33 시나리오, amend §D paired) |
|
||||
> | PR 11 | ⏭ pending | — | file_state kind_codes 통합 + decision 매핑 lookup table (~120 LOC) |
|
||||
> | PR 10 | ✅ | `b47f7eb` | file_state parity tests +26 (총 33 시나리오, amend §D paired) |
|
||||
> | PR 11 | ✅ | (TBD) | file_state kind_codes 3중 복제 통합 + decision 매핑 lookup table (~80 LOC 감소) |
|
||||
> | PR 12 | ⏭ pending | — | eager_hydrate parity tests |
|
||||
> | **PR 13a** | ⏭ Wave 2 게이트 | — | envelope 스펙 + ref impl + parity (PR-A 본체 가능) |
|
||||
> | PR 13b | ⏭ Wave 2 | — | envelope 완전 구현 (취소·deadline·우선순위) |
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Optional, Sequence, Tuple
|
||||
from typing import Mapping, Optional, Sequence, Tuple
|
||||
|
||||
from ._rust_ffi import SessionsNativeLibraryError
|
||||
from ._rust_ffi import (
|
||||
@@ -32,6 +32,33 @@ from ._rust_ffi import (
|
||||
)
|
||||
from .remote import RemoteFileKind, RemoteFileMetadata
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Single source of truth for kind_code mapping (Wave 1.5 amend §C / PR 11).
|
||||
# Mirrors ``rust/crates/sessions_native/src/lib.rs`` REMOTE_KIND_* constants.
|
||||
# ``OTHER`` falls through to ``3`` so the Rust ABI receives a known sentinel.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_KIND_CODES: Mapping[RemoteFileKind, int] = {
|
||||
RemoteFileKind.REGULAR_FILE: 0,
|
||||
RemoteFileKind.DIRECTORY: 1,
|
||||
RemoteFileKind.SYMLINK: 2,
|
||||
RemoteFileKind.OTHER: 3,
|
||||
}
|
||||
|
||||
|
||||
def _metadata_to_tuple(
|
||||
meta: Optional[RemoteFileMetadata],
|
||||
) -> Optional[Tuple[int, int, int]]:
|
||||
"""Pack ``(mtime_ns, size_bytes, kind_code)`` for the Rust decision ABIs.
|
||||
|
||||
Returns ``None`` so callers can pass it straight through to
|
||||
``rust_reload_recommendation_code`` / ``rust_save_decision_code`` whose
|
||||
Optional-tuple branch encodes "no metadata available".
|
||||
"""
|
||||
if meta is None:
|
||||
return None
|
||||
return (meta.mtime_ns, meta.size_bytes, _KIND_CODES.get(meta.kind, 3))
|
||||
|
||||
|
||||
class RemotePathMappingError(ValueError):
|
||||
"""Raised when a remote path cannot be mapped safely to the local cache."""
|
||||
@@ -214,6 +241,16 @@ class UnsupportedOpenReason(Enum):
|
||||
ZERO_BYTE_READ_NOT_ALLOWED = "zero_byte_read_not_allowed"
|
||||
|
||||
|
||||
# Single source of truth for open-guard reason codes (Wave 1.5 amend §C).
|
||||
# Mirrors ``rust/crates/sessions_native/src/lib.rs`` OPEN_REASON_* constants.
|
||||
_OPEN_GUARD_REASON_MAP: Mapping[int, Optional[UnsupportedOpenReason]] = {
|
||||
0: None,
|
||||
1: UnsupportedOpenReason.FILE_TOO_LARGE,
|
||||
2: UnsupportedOpenReason.UNSUPPORTED_REMOTE_KIND,
|
||||
3: UnsupportedOpenReason.ZERO_BYTE_READ_NOT_ALLOWED,
|
||||
}
|
||||
|
||||
|
||||
class CacheInvalidationTrigger(Enum):
|
||||
"""Catalog of events that should drop or refresh cached bytes."""
|
||||
|
||||
@@ -256,6 +293,16 @@ class ReloadRecommendation(Enum):
|
||||
REMOTE_MISSING = "remote_missing"
|
||||
|
||||
|
||||
# Single source of truth for reload recommendation codes (Wave 1.5 amend §C).
|
||||
# Mirrors ``rust/crates/sessions_native/src/lib.rs`` RELOAD_* constants.
|
||||
_RELOAD_RECOMMENDATION_MAP: Mapping[int, ReloadRecommendation] = {
|
||||
0: ReloadRecommendation.NO_ACTION_NEEDED,
|
||||
1: ReloadRecommendation.RECOMMEND_RELOAD,
|
||||
2: ReloadRecommendation.RECOMMEND_REVIEW_CONFLICT,
|
||||
3: ReloadRecommendation.REMOTE_MISSING,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FileOpenGuardrails:
|
||||
"""Hard limits for MVP open behavior.
|
||||
@@ -286,25 +333,13 @@ def open_guard_reason_for_remote_metadata(
|
||||
Raises:
|
||||
None.
|
||||
"""
|
||||
kind_codes = {
|
||||
RemoteFileKind.REGULAR_FILE: 0,
|
||||
RemoteFileKind.DIRECTORY: 1,
|
||||
RemoteFileKind.SYMLINK: 2,
|
||||
}
|
||||
reason_map = {
|
||||
0: None,
|
||||
1: UnsupportedOpenReason.FILE_TOO_LARGE,
|
||||
2: UnsupportedOpenReason.UNSUPPORTED_REMOTE_KIND,
|
||||
3: UnsupportedOpenReason.ZERO_BYTE_READ_NOT_ALLOWED,
|
||||
}
|
||||
kind_code = kind_codes.get(meta.kind, 0)
|
||||
reason_code = rust_open_guard_reason_code(
|
||||
remote_kind_code=kind_code,
|
||||
remote_kind_code=_KIND_CODES.get(meta.kind, 0),
|
||||
size_bytes=meta.size_bytes,
|
||||
max_open_bytes=limits.max_open_bytes,
|
||||
allow_empty_files=limits.allow_empty_files,
|
||||
)
|
||||
return reason_map.get(reason_code)
|
||||
return _OPEN_GUARD_REASON_MAP.get(reason_code)
|
||||
|
||||
|
||||
def is_likely_binary_from_head(content_head: bytes) -> bool:
|
||||
@@ -363,42 +398,12 @@ def reload_recommendation(
|
||||
Raises:
|
||||
None.
|
||||
"""
|
||||
kind_codes = {
|
||||
RemoteFileKind.REGULAR_FILE: 0,
|
||||
RemoteFileKind.DIRECTORY: 1,
|
||||
RemoteFileKind.SYMLINK: 2,
|
||||
RemoteFileKind.OTHER: 3,
|
||||
}
|
||||
baseline_tuple = (
|
||||
(
|
||||
baseline.mtime_ns,
|
||||
baseline.size_bytes,
|
||||
kind_codes.get(baseline.kind, 3),
|
||||
)
|
||||
if baseline is not None
|
||||
else None
|
||||
)
|
||||
current_tuple = (
|
||||
(
|
||||
current.mtime_ns,
|
||||
current.size_bytes,
|
||||
kind_codes.get(current.kind, 3),
|
||||
)
|
||||
if current is not None
|
||||
else None
|
||||
)
|
||||
code = rust_reload_recommendation_code(
|
||||
had_metadata_at_open=had_metadata_at_open,
|
||||
baseline=baseline_tuple,
|
||||
current=current_tuple,
|
||||
baseline=_metadata_to_tuple(baseline),
|
||||
current=_metadata_to_tuple(current),
|
||||
)
|
||||
mapping = {
|
||||
0: ReloadRecommendation.NO_ACTION_NEEDED,
|
||||
1: ReloadRecommendation.RECOMMEND_RELOAD,
|
||||
2: ReloadRecommendation.RECOMMEND_REVIEW_CONFLICT,
|
||||
3: ReloadRecommendation.REMOTE_MISSING,
|
||||
}
|
||||
return mapping[code]
|
||||
return _RELOAD_RECOMMENDATION_MAP[code]
|
||||
|
||||
|
||||
def default_source_of_truth_policy() -> SourceOfTruthPolicy:
|
||||
@@ -460,6 +465,39 @@ class SaveConflictKind(Enum):
|
||||
BASELINE_UNKNOWN = "baseline_unknown"
|
||||
|
||||
|
||||
# Single source of truth for save decision codes (Wave 1.5 amend §C / amend A1
|
||||
# user-visible strings = Python single source).
|
||||
# Mirrors ``rust/crates/sessions_native/src/lib.rs`` SAVE_DECISION_* constants.
|
||||
# ``code 0`` (OK) is handled inline in ``evaluate_save_file`` without a spec.
|
||||
_SAVE_CONFLICT_SPECS: Mapping[int, Tuple[SaveConflictKind, str, ReloadChoice]] = {
|
||||
1: (
|
||||
SaveConflictKind.BASELINE_UNKNOWN,
|
||||
"Cannot save safely without metadata captured at open.",
|
||||
ReloadChoice.CANCEL,
|
||||
),
|
||||
2: (
|
||||
SaveConflictKind.REMOTE_FILE_MISSING,
|
||||
"Remote file disappeared before save; choose reload or cancel.",
|
||||
ReloadChoice.CANCEL,
|
||||
),
|
||||
3: (
|
||||
SaveConflictKind.REMOTE_PATH_IS_DIRECTORY,
|
||||
"Remote path is a directory; refusing save.",
|
||||
ReloadChoice.CANCEL,
|
||||
),
|
||||
4: (
|
||||
SaveConflictKind.REMOTE_PATH_IS_SYMLINK,
|
||||
"Remote path is a symlink; refusing blind save.",
|
||||
ReloadChoice.CANCEL,
|
||||
),
|
||||
5: (
|
||||
SaveConflictKind.REMOTE_METADATA_CHANGED,
|
||||
"Remote file changed since local copy; choose overwrite or reload.",
|
||||
ReloadChoice.KEEP_LOCAL_AND_OVERWRITE_REMOTE,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OpenFileRequest:
|
||||
"""Parameters needed to stage a remote file into the local cache.
|
||||
@@ -591,81 +629,19 @@ def evaluate_save_file(request: SaveFileRequest) -> SaveFileResult:
|
||||
Raises:
|
||||
None.
|
||||
"""
|
||||
kind_codes = {
|
||||
RemoteFileKind.REGULAR_FILE: 0,
|
||||
RemoteFileKind.DIRECTORY: 1,
|
||||
RemoteFileKind.SYMLINK: 2,
|
||||
RemoteFileKind.OTHER: 3,
|
||||
}
|
||||
baseline_tuple = (
|
||||
(
|
||||
request.baseline_remote_metadata.mtime_ns,
|
||||
request.baseline_remote_metadata.size_bytes,
|
||||
kind_codes.get(request.baseline_remote_metadata.kind, 3),
|
||||
)
|
||||
if request.baseline_remote_metadata is not None
|
||||
else None
|
||||
)
|
||||
candidate_tuple = (
|
||||
(
|
||||
request.candidate_remote_metadata.mtime_ns,
|
||||
request.candidate_remote_metadata.size_bytes,
|
||||
kind_codes.get(request.candidate_remote_metadata.kind, 3),
|
||||
)
|
||||
if request.candidate_remote_metadata is not None
|
||||
else None
|
||||
)
|
||||
decision_code = rust_save_decision_code(
|
||||
baseline=baseline_tuple,
|
||||
candidate=candidate_tuple,
|
||||
baseline=_metadata_to_tuple(request.baseline_remote_metadata),
|
||||
candidate=_metadata_to_tuple(request.candidate_remote_metadata),
|
||||
)
|
||||
if decision_code == 0:
|
||||
return SaveFileResult(outcome=SaveOutcome.OK)
|
||||
if decision_code == 1:
|
||||
return SaveFileResult(
|
||||
outcome=SaveOutcome.CONFLICT,
|
||||
conflict=SaveConflict(
|
||||
kind=SaveConflictKind.BASELINE_UNKNOWN,
|
||||
message="Cannot save safely without metadata captured at open.",
|
||||
reload_choice_hint=ReloadChoice.CANCEL,
|
||||
),
|
||||
)
|
||||
if decision_code == 2:
|
||||
return SaveFileResult(
|
||||
outcome=SaveOutcome.CONFLICT,
|
||||
conflict=SaveConflict(
|
||||
kind=SaveConflictKind.REMOTE_FILE_MISSING,
|
||||
message="Remote file disappeared before save; choose reload or cancel.",
|
||||
reload_choice_hint=ReloadChoice.CANCEL,
|
||||
),
|
||||
)
|
||||
if decision_code == 3:
|
||||
return SaveFileResult(
|
||||
outcome=SaveOutcome.CONFLICT,
|
||||
conflict=SaveConflict(
|
||||
kind=SaveConflictKind.REMOTE_PATH_IS_DIRECTORY,
|
||||
message="Remote path is a directory; refusing save.",
|
||||
reload_choice_hint=ReloadChoice.CANCEL,
|
||||
),
|
||||
)
|
||||
if decision_code == 4:
|
||||
return SaveFileResult(
|
||||
outcome=SaveOutcome.CONFLICT,
|
||||
conflict=SaveConflict(
|
||||
kind=SaveConflictKind.REMOTE_PATH_IS_SYMLINK,
|
||||
message="Remote path is a symlink; refusing blind save.",
|
||||
reload_choice_hint=ReloadChoice.CANCEL,
|
||||
),
|
||||
)
|
||||
if decision_code == 5:
|
||||
return SaveFileResult(
|
||||
outcome=SaveOutcome.CONFLICT,
|
||||
conflict=SaveConflict(
|
||||
kind=SaveConflictKind.REMOTE_METADATA_CHANGED,
|
||||
message=(
|
||||
"Remote file changed since local copy; choose overwrite or reload."
|
||||
),
|
||||
reload_choice_hint=ReloadChoice.KEEP_LOCAL_AND_OVERWRITE_REMOTE,
|
||||
),
|
||||
)
|
||||
raise ValueError("unexpected save decision code: {}".format(decision_code))
|
||||
spec = _SAVE_CONFLICT_SPECS.get(decision_code)
|
||||
if spec is None:
|
||||
raise ValueError("unexpected save decision code: {}".format(decision_code))
|
||||
kind, message, reload_hint = spec
|
||||
return SaveFileResult(
|
||||
outcome=SaveOutcome.CONFLICT,
|
||||
conflict=SaveConflict(
|
||||
kind=kind, message=message, reload_choice_hint=reload_hint
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user