docs(planning)+ci(boundary): PR 0 — Wave 1.5 governance guardrails

4-team synthesis (rust-maximalist / python-pragmatist / boundary-keeper /
shipping-operator)에서 도출한 Python thinning plan의 첫 슬라이스. 코드 변경
없이 거버넌스 인프라만 활성화 — 후속 PR이 land될 때 mechanical guard로 작용.

- planning/PYTHON_THINNING_PLAN.md: PR 0~16 정식 plan (4축 가중치 + 잔존
  쟁점 8개 결정 표).
- planning/PYTHON_RUST_BOUNDARY.md: amend §A–§M land — 디폴트 거버넌스, 단일
  진실 양방향 보강, parity test 인프라 MUST, thin shim 정량 정의 (≤400 LOC),
  Wave 1.5 + 2.5 신설, Wave 5 일반화, hygiene contract.
- planning/boundary_inventory.yml: Migration inventory 표의 YAML 변환
  (single-source-of-truth, Lint #5 minimal cross-check 데이터).
- scripts/lint_python_thinning.py: ban-list lint #1/#2.5/#4/#6 (PR diff
  기반이라 grandfather 자동 처리).
- scripts/duplication_deadline.py: TEMP_DUPLICATION_UNTIL=vX.Y.Z 마커 만료
  검사 — 만료 시 release 차단.
- .gitea/workflows/boundary-lint.yml: 3 jobs (ban-list / deadline /
  pr-claim) PR + push에서 자동 실행.

uv.lock: pyproject 0.7.25 동기화 (잔재 정리).

Lint 후속 활성화 시점:
- #2 (deque task queue ban) → PR 16 (PR-A 본체) 머지 시
- #3 (python3 -c SSH 폴백 ban) → PR 2 (bootstrap 청산) 머지 시
- #5 (boundary inventory metasync 자동화) → Wave 2.5

Grandfather 위반 2건 (PR diff 기반이라 새 위반만 차단):
- ssh_file_transport.py:1378 _payload_method_label → PR 17+ (디코더 이관)
- commands_python_pipeline.py:639 time.monotonic → Track H2 분리 시

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 19:03:43 +09:00
parent f70999a9d7
commit 86d444885a
7 changed files with 1096 additions and 14 deletions

View File

@@ -0,0 +1,67 @@
name: boundary-lint
# Wave 1.5 거버넌스 가드 — PR/push에서 boundary lint + duplication-deadline 검사.
#
# 두 검사 모두 PR diff 기반(추가된 라인만)이므로 main의 기존 코드는 grandfather.
# 자세한 룰: scripts/lint_python_thinning.py docstring 참조.
# 거버넌스 normative: planning/PYTHON_RUST_BOUNDARY.md (Wave 1.5 amend).
on:
push:
branches: [main]
pull_request:
jobs:
ban-list:
name: ban-list lint (Lint #1/#2.5/#4)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # diff base 계산 위해 full history 필요
- name: setup python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: run boundary lint
env:
CI: "true"
run: python3 scripts/lint_python_thinning.py --lint 1 --lint 2.5 --lint 4
duplication-deadline:
name: duplication-deadline (Layer 1/2)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: setup python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: check expired TEMP_DUPLICATION_UNTIL markers
run: python3 scripts/duplication_deadline.py
pr-boundary-claim:
name: PR boundary-claim (Lint #6)
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- name: setup python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: write PR body to temp file
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: printf '%s\n' "$PR_BODY" > /tmp/pr_body.md
- name: validate boundary-claim header
run: python3 scripts/lint_python_thinning.py --lint 6 --pr-body /tmp/pr_body.md

View File

@@ -5,6 +5,10 @@
- **Python (Sublime plugin host)**: stay *thin* — command registration, `sublime` API calls, UI (panels, status), loading JSON settings, scheduling work onto the UI thread, and optional glue to native code.
- **Rust**: *heavy* logic — protocol, workspace identity, remote cache algorithms, SSH-side helpers, and anything performance- or correctness-sensitive that should not grow without bound in Python.
### 디폴트 거버넌스 (Wave 1.5 amend)
위 enumerated list("command registration, `sublime` API calls, UI, loading JSON settings, scheduling work onto the UI thread, optional glue to native code")에 *명시되지 않은* 새 도메인 책임은 디폴트로 **Rust home**이다. Python 잔류를 주장하려면 이 enumeration을 *amend*하는 PR이 코드 PR보다 *선행*한다 — 슬로건이나 관행으로 enumeration을 격하시킬 수 없다.
## Reliability invariant (MUST)
- **Helper/worker lifecycle 기본 원칙:** 요청/메시지 단위 오류는 **프로세스 종료 사유가 아니다**.
@@ -14,6 +18,17 @@
- 재시도 가능한 오류(`retryable`)를 우선 반환하고, 상위 레이어가 backoff/retry 정책으로 흡수한다.
- 이 원칙을 깨는 변경은 명시적 설계 근거와 회귀 테스트를 반드시 동반한다.
### Parity test 인프라 (MUST, Wave 1.5 amend)
모든 Rust 이관 슬라이스 PR은 *paired parity test PR*을 *선행*한다. parity test PR은:
- (a) 동일 입력에 대한 Python 본체 결과를 *baseline*으로 핀한다.
- (b) 머지된 시점에 Python 본체가 그 테스트들에 *통과*해야 한다 (baseline drift 방지). 즉 parity test가 "Rust 미래 동작"만 정의하는 것을 금지한다.
- (c) 이관 PR은 동일 시나리오 매트릭스를 충족한 *후*에만 머지된다.
- (d) 이관 PR과 parity test PR은 *별 PR*로 분리된다 — 하나의 PR이 baseline 정의 + 본체 이관을 동시에 하는 것을 금지한다.
적용 슬라이스(예시): `file_state` (parity → 이관), `eager_hydrate` (parity → 이관), PR-A queue/dispatcher (parity → 이관). 자세한 슬롯 매핑은 [`PYTHON_THINNING_PLAN.md`](PYTHON_THINNING_PLAN.md) §5 참조.
### Remote tree / file I/O (MUST)
- **`tree/list`·`file/read`·`file/stat`·`file/write`:** 원격에서 **`python3 -c …` SSH 폴백을 두지 않는다.** 브리지(`local_bridge` + `session_helper`)가 없거나 요청이 실패하면 **구조화된 오류**(`SessionHelperStartError` 또는 `RemoteWriteFileResult`의 전송 오류)로 끝낸다. (예전처럼 원격 임시 Python으로 “우회 성공”시키지 않는다.)
@@ -26,13 +41,32 @@ MVP code may live in Python for velocity; **new non-trivial logic should default
- **필수:** 이관 시 **한 커밋/PR 안에서** Rust로 옮기고 Python 쪽 중복은 **삭제**한다. Python은 `sublime`·설정·`local_bridge`/FFI 호출만 남긴다.
- 테스트도 “Python 레퍼런스 vs Rust” 이중 유지보수를 늘리지 않는다. 동작 검증은 **Rust 단위 테스트**와 필요 시 **얇은 Python 통합**(브리지 호출)으로 충분하다.
### 양방향 보강 (Wave 1.5 amend)
- **Python → Rust 방향**: helper response JSON 파서는 **Rust 단일 권한**이다. Python은 Rust ABI 응답을 *typed wrapper*로만 감싸고, 정규식·조건 분기·필드 fallback을 *직접 수행하지 않는다*. 위반 검출은 ban-list **Lint #1** (parser 시그니처 ban)로 강제.
- **Rust → Python 방향**: Rust ABI는 *식별자 코드*(int, kebab-case identifier)만 반환하며 *영문 자연어 메시지를 만들지 않는다*. 사용자 보이는 문자열 매핑(코드 → 메시지)은 Python에 단일하게 모이고, 새 메시지 카테고리 추가 시 Python amend가 *선행*한다. 위반 검출은 **Lint #4** (Rust ABI 영문 자연어 ban)로 강제.
- **enum 정합**: enum variant는 *Python을 single source of truth*로 두고 Rust ABI 응답이 그 값을 echo한다(역방향 아님). 새 enum variant 추가는 Python *먼저*, Rust 따라가는 PR이 **.
## What stays in Python
- `sublime_plugin` commands, `EventListener`s, and any direct `sublime.*` usage.
- Project/workspace JSON merge for sidebar folders (unless we later move merge rules to Rust with a tiny JSON bridge).
- Project/workspace JSON merge for sidebar folders (조건부 — sidebar merge plan trigger 참조 아래).
- User-visible strings and command palette wiring.
- Optional: thin wrappers that deserialize settings and call Rust.
### Wave 1.5 amend 보강
- **사용자 보이는 모든 문자열은 Python.** Sublime status panel, command palette caption, error message, conflict resolution prompt — 모두 Python 단일. Rust ABI는 식별자 코드만; Python이 코드 → 메시지 매핑을 단일하게 보유.
- **모듈 분리 가드 (Track H2)**: Python 측 서비스 모듈 분리(예: `commands_runtime_queue.py`, `commands_sidebar_mirror.py`, `commands_connect.py`)는 *허용*한다. 단 *retry, timeout, error mapping*은 모듈 분리 후에도 단일 헬퍼(현재 `_rust_ffi`/bridge 호출 표면)로 수렴한다 — 새 서비스 모듈에 *자기 retry 루프* 신설 금지. 위반 검출은 **Lint #2.5** (commands_*.py에서 retry/timeout 패턴 신규 도입 시 fail)로 강제.
### Sidebar merge plan trigger (Wave 1.5 amend, conditional)
위 line "Project/workspace JSON merge for sidebar folders"의 후반부 trigger("unless we later move merge rules to Rust with a tiny JSON bridge")는 다음 조건이 *모두* 충족될 때만 발동된다:
- (a) merge plan 알고리즘이 ABI 라운드트립을 *증가시키지 않음*을 PR 본체에서 측정 증명.
- (b) merge plan *알고리즘*만 이관 (`sidebar_project_folders.py` 같은 Sublime project 형식 결합 모듈은 그대로 Python 유지).
- (c) sidebar merge 이관 PR은 단독 슬라이스가 아니라 sync 오케스트레이션 슬라이스와 *함께* 평가.
## What belongs in Rust
| Area | Crate / binary | Notes |
@@ -43,6 +77,10 @@ MVP code may live in Python for velocity; **new non-trivial logic should default
| Remote helper CLI | `session_helper` | Runs on the Linux remote. |
| Remote tree mirror (BFS, ignore patterns, prune) | `local_bridge::remote_cache_mirror` | Pure algorithm + local FS; crate 병합 후 `local_bridge` 내부 모듈. Python delegates via bridge. |
| **Multiplex stdio, channel supervisor, code-server children** (timeouts, kill, partial reads) | `session_helper`, `local_bridge`, `session_protocol` | Normative model: [`VSCODE_REMOTE_TRANSPORT_MODEL.md`](VSCODE_REMOTE_TRANSPORT_MODEL.md); Python forwards opaque frames only. |
| **Helper response 파싱(ruff/pyright/diagnostic)** (Wave 1.5 amend) | `sessions_native::diagnostics_parser` (기존 `ruff_diagnostics_json` 확장) | Python `diagnostics.py`에서 진짜 파서 ~110 LOC(line 225333) 삭제. panel rendering / inline scope / path remap만 Python 유지. pyright 추가는 Wave 2 후. |
| **Settings 정규화·검증** (Wave 1.5 amend) | `sessions_native::settings_normalize` | `settings_model.py` 정규화부 → Rust. Python은 sublime 설정 로드 + Rust 호출. |
| **Python interpreter probe / cache / 랭킹** (Wave 1.5 amend) | `sessions_native::interpreter_probe` | `python_interpreter_registry.py`의 캐시·랭킹 → Rust. probe 정규식 ~30 LOC는 Python 유지(ROI 낮음, rust-max 양보 영역). |
| **`_rust_ffi.py` 디코더** (Wave 1.5 amend, PR 17+ 슬라이스) | `sessions_native::abi_decoders` | `_parse_open_outcome` / `_parse_request_outcome` / `parse_response_packet` / `extract_handshake` / `payload_method_label` → Rust. Python `_rust_ffi.py`는 thin ctypes wrapper만 (목표 < 400 LOC). |
| Future: SSH transport, conflict rules, agent payload validation | TBD crates | Migrate when Python surface area becomes a liability. |
## Integration options (Python → Rust)
@@ -69,13 +107,34 @@ MVP code may live in Python for velocity; **new non-trivial logic should default
|------|------|------------------------|
| **0** | **Deliverability:** registry publish 녹색, 다운로드 manifest·무결성 검증. | CI/workflows, runtime helper fetch |
| **1** | **Rust authoritative for hot I/O:** file/tree/stat 경로의 **타임아웃·재시도·구조화 오류**를 bridge/helper 단일 권한으로 수렴. **단발 `local_bridge mirror-cache` 프로세스**는 Wave 2 이전까지 **임시**로 유지(별 SSH·별 helper 세션). | [#24](https://git.teahaven.kr/sublime-rs/sessions/issues/24), `local_bridge`, `session_helper` |
| **2** | **Multiplex v0 + 미러 통합:** 한 stdio 세션(`local_bridge --persistent``session_helper`) 위 **`control` / `file`(및 확장 채널)**; **원격 트리 미러(BFS)를 동일 세션으로 편입**한다. 전제: 장시간 미러가 **한 줄 NDJSON만 독점하지 않도록** 슈퍼바이저·**취소·deadline**·(필요 시)청크/스트리밍 하위 프레임. 완료 후 **`mirror-cache` 단발 프로세스 제거**를 목표로 한다. | [#31](https://git.teahaven.kr/sublime-rs/sessions/issues/31), [`VSCODE_REMOTE_TRANSPORT_MODEL.md`](VSCODE_REMOTE_TRANSPORT_MODEL.md) |
| **1.5** | **위생 + thin shim 청산:** boundary 문서 자체의 부분 미명시 영역(`_rust_ffi.py` 1337 LOC, `settings_model` 정규화, `python_interpreter_registry` probe, diagnostics 잔재) 청산. Wave 2 envelope 합의 *전*에 land 가능한 슬라이스만. parity test 인프라 활성화. **PR 0**(amend + Lint 7종 + boundary inventory YAML 초안) **선행** + 슬라이스별 후속 PR. | [`PYTHON_THINNING_PLAN.md`](PYTHON_THINNING_PLAN.md) §5 PR 012 |
| **2** | **Multiplex v0 + 미러 통합:** 한 stdio 세션(`local_bridge --persistent``session_helper`) 위 **`control` / `file`(및 확장 채널)**; **원격 트리 미러(BFS)를 동일 세션으로 편입**한다. 전제: 장시간 미러가 **한 줄 NDJSON만 독점하지 않도록** 슈퍼바이저·**취소·deadline**·(필요 시)청크/스트리밍 하위 프레임. 완료 후 **`mirror-cache` 단발 프로세스 제거**를 목표로 한다. **2단계 분할**: PR 13a(스펙 + ref impl + parity), PR 13b(완전 구현). | [#31](https://git.teahaven.kr/sublime-rs/sessions/issues/31), [`VSCODE_REMOTE_TRANSPORT_MODEL.md`](VSCODE_REMOTE_TRANSPORT_MODEL.md) |
| **2.5** | **lsp_proxy + boundary inventory 자동화:** `lsp_project_wiring.py` deep-merge → `local_bridge::lsp_stdio` 모듈 확장. boundary inventory YAML LOC 임계 자동 측정(Lint #5 자동화). | [`PYTHON_THINNING_PLAN.md`](PYTHON_THINNING_PLAN.md) §6 잔존 #7 |
| **3** | **Sync / cache policy:** authoritative 시점, prune 안전, 멀티 윈도우; 메타데이터 스키마는 Rust·Python이 동일 해석. | [#27](https://git.teahaven.kr/sublime-rs/sessions/issues/27), [#28](https://git.teahaven.kr/sublime-rs/sessions/issues/28) |
| **4** | **Large-file / streaming:** chunked `file/read`, stale cancel, 활성 탭 우선. | [#32](https://git.teahaven.kr/sublime-rs/sessions/issues/32) |
| **5** | **Diff apply / agent apply:** base hash, path confinement, per-hunk — 전송·캐시 계약 위에만 구축. | [#29](https://git.teahaven.kr/sublime-rs/sessions/issues/29) |
| **5** | **Generic agent apply / hunked apply over the cache contract:** base hash, path confinement, per-hunk — 전송·캐시 계약 위에만 구축. (구체 product surface는 회전 가능; chat→tmux pivot 이후 generic 추상 수준 유지.) | (product surface는 별도 결정) |
**PR 규칙:** 새 non-trivial 알고리즘·프로토콜 파싱·동시성은 **기본 Rust**; Python에는 `sublime` API·설정·봉투 전달만.
### "thin shim" 정량 정의 (Wave 1.5 amend)
Python 모듈이 *thin shim*으로 분류되려면 *모두* 만족:
- 모듈 LOC ≤ **400**.
- 모듈 비-주석 라인 중 `sublime.*` API 호출 또는 Rust FFI/브리지 호출에 직접 닿지 않는 라인 ≤ **30%**.
- 도메인 알고리즘(파싱·정규화·BFS·우선순위·재시도) 본체 *부재*.
위 기준 미달 모듈은 thin shim이 아니며, line "Single source of truth" 원칙 위반 표면이다. 현 시점 위반 모듈: `_rust_ffi.py` (1337 LOC, Wave 1.5 청산 대상; PR 37 split).
### Wave 2 게이트 (Wave 1.5 amend)
envelope 스펙(`v`/`channel`/`kind`/`body`)·취소·deadline 합의가 Rust에 land *되기 전에는* 다음 슬라이스의 이관 PR을 머지하지 않는다: worker loop SM, eager_hydrate BFS, connect SM body, hydrate preflight, Track H1(file_open transaction).
Wave 2 게이트는 **2단계 분할**이다:
- **PR 13a**: envelope *스펙* + 최소 reference impl + parity test 1개. spec drift 방지를 위해 reference impl이 컴파일 시점 검증을 강제. PR-A 본체(PR 16)는 PR 13a ** 머지 가능.
- **PR 13b**: envelope 완전 구현(취소·deadline·우선순위·백프레셔 포함). eager_hydrate 이관(PR 14), H1(PR 14.5)은 PR 13b ** 머지 가능.
### Wave 2 — 미러를 persistent 파이프라인에 넣기 (계획 수정, normative)
**목표:** 호스트당 **하나의 장수명** `local_bridge``session_helper` stdio 링크 위에서 `tree/list`·`file/*`와 **동일한 혼잡 제어**로 원격 트리 미러(BFS)를 돌린다. Python 쪽 미러 큐(`sync_yield` 등)는 **Rust 쪽 취소·우선순위·백프레셔**로 대체·축소할 수 있게 한다.
@@ -99,14 +158,29 @@ MVP code may live in Python for velocity; **new non-trivial logic should default
## Migration inventory (snapshot)
| Python surface (`sublime/sessions/`) | Responsibility | Rust home | Notes |
|------------------------------------|----------------|-----------|--------|
| `commands.py` | Sublime commands, UI orchestration | — | Stays Python; may call Rust via FFI/bridge. **Cache-based directory open** 전체 경로(`connect``mirror-cache` → sidebar merge → `tree/list`)가 Rust bridge-only로 전환 완료. |
| ~~`remote_cache_mirror.py`~~ | ~~BFS mirror, ignore patterns, prune~~ | `local_bridge::remote_cache_mirror` (crate 병합 완료) | **삭제 완료.** 알고리즘은 Rust only; Python 타입(`RemoteCacheMirrorOptions` 등)은 `ssh_file_transport.py`로 이동. |
| `workspace_state.py` (identity) | Cache key, paths | `workspace_identity` | `normalize_remote_root` is **Rust-only** via `sessions_native` cdylib; Python `cache_key` hashing remains until a later slice. |
| `ssh_runner.py`, `ssh_file_transport.py` | SSH subprocess, file I/O | `local_bridge`, `session_helper` | Python glue only; **no remote-Python transport fallback** for tree/file (bridge required or structured failure). |
| `file_state.py` | Open/save policy, conflict rules | *future* `sessions_file_policy` or similar | Pure functions → good Rust candidate. |
| `connect_preflight.py` | remote-root validation | `workspace_identity` + `sessions_native` | Uses ``normalize_remote_root`` (Rust); host-alias resolution stays Python (SSH config objects). |
| `settings_model.py` | typed settings | *future* | Optional codegen from JSON schema. |
표는 **single source of truth**이다. 동등한 표현이 [`planning/boundary_inventory.yml`](boundary_inventory.yml)에 YAML 형태로 존재하며, CI가 (a) Lint #1 시그니처 ban-list, (b) 모듈 LOC 임계와 cross-check한다 (LOC 임계 자동 측정은 Wave 2.5).
This table is updated as slices land; issue **#24** tracks the next concrete moves.
| Python surface (`sublime/sessions/`) | Responsibility | Rust home | Wave | Notes |
|------------------------------------|----------------|-----------|------|--------|
| `commands.py` | Sublime commands, UI orchestration | — | (분할: Track H2 병행, Wave 1.5) | Stays Python; may call Rust via FFI/bridge. **Cache-based directory open** 전체 경로 Rust bridge-only 전환 완료. worker loop SM·connect SM token은 PR 16 (Wave 2 후) 이관. |
| ~~`remote_cache_mirror.py`~~ | ~~BFS mirror, ignore patterns, prune~~ | `local_bridge::remote_cache_mirror` | 1 (완료) | **삭제 완료.** Python 타입(`RemoteCacheMirrorOptions` 등)은 `ssh_file_transport.py`로 이동. |
| `workspace_state.py` (identity) | Cache key, paths | `workspace_identity` | 1 (부분) | `normalize_remote_root` is **Rust-only** via `sessions_native` cdylib. Python `cache_key` hashing remains until a later slice. |
| `ssh_runner.py`, `ssh_file_transport.py` | SSH subprocess, file I/O | `local_bridge`, `session_helper` | 1 (부분) — bootstrap 청산 PR 2 | **no remote-Python transport fallback** for tree/file (bridge required or structured failure). |
| `file_state.py` | Open/save policy, conflict rules | `sessions_native::file_policy` (이미 결정 코드 위임) | 1.5 (kind_codes 통합 + decision 매핑 lookup table; PR 10 parity → PR 11 이관) | 사용자 보이는 SaveConflict.message 등은 Python single source 유지. |
| `connect_preflight.py` | remote-root validation | `workspace_identity` + `sessions_native` | 1 (부분) | Host-alias resolution stays Python (SSH config objects). |
| `settings_model.py` | typed settings | `sessions_native::settings_normalize` | 1.5 (PR 1) | Optional codegen from JSON schema. ROI 정직화: LOC 절감 ~80, dry-run 가치 우선. |
| `python_interpreter_registry.py` | interpreter probe, cache, ranking | `sessions_native::interpreter_probe` | 1.5 (PR 8) | `_parse_probe_stdout` 정규식 ~30 LOC는 Python 유지. |
| `diagnostics.py` ruff parser (line 225333) | ruff JSON parsing | `sessions_native::diagnostics_parser` (기존 `ruff_diagnostics_json` 확장) | 1.5 (W1.5.0 청산 PR 5.5) | Panel rendering / inline scope / path remap만 Python 유지 (~497 LOC). |
| `_rust_ffi.py` 1337 LOC (thin shim 위반) | ctypes 바인딩 + 디코더 + broker | `sessions_native::abi_decoders` (디코더만) + 6 모듈 split | 1.5 (PR 37 split, PR 17+ 디코더 이관) | thin shim 정량 정의 통과 목표 (모듈 ≤ 400 LOC). |
| `eager_hydrate.py` BFS scheduler | placeholder BFS, batch 페이싱 | `local_bridge::remote_cache_mirror` 통합 | 2 (PR 12 parity → PR 14 이관) | envelope 후 land. |
| `commands.py` worker loop + connect SM token | queue/dispatcher/lane gating + `_CONNECT_GENERATION` token | `sessions_orchestrator` (신규 모듈) | 2 (PR 15 reconnect + PR 15.5 test → PR 16 본체 ~600 LOC) | 워크플로우 진행 메시지(사용자 보이는)는 Python 유지. Lint #2 PR 16 머지 동시 활성화. |
This table is updated as slices land; issue **#24** tracks the next concrete moves. Migration plan: [`PYTHON_THINNING_PLAN.md`](PYTHON_THINNING_PLAN.md).
## Hygiene contract (Wave 1.5 amend)
Rust 측 stale `#![allow(dead_code)]` 또는 "not yet wired" docstring은 PR 단위로 청산한다. 새 코드 PR이 기존 stale residue를 발견하면 *같은 PR에서* 해당 residue 제거를 강제한다(RTK CLAUDE.md `feedback_clippy_allow_hygiene.md` 정합).
현 시점 청산 대상:
- `rust/crates/sessions_native/src/broker.rs:117``#![allow(dead_code)]` + "S2.3S2.5 not wired" docstring; broker는 production wired 상태이므로 stale. PR 0 또는 가장 빠른 후속 PR에서 청산.

View File

@@ -0,0 +1,293 @@
# Python Thinning Plan — Rust 이관으로 Python 레이어 얇게 유지
> **상태:** Draft v1.1 — 4인 팀(rust-maximalist / python-pragmatist / boundary-keeper / shipping-operator) 3라운드 SYNTHESIS 결과를 리더가 합성한 정식 계획. 4명 모두 거버넌스 라인에 합의 도달.
> **선행 문서:** [`PYTHON_RUST_BOUNDARY.md`](PYTHON_RUST_BOUNDARY.md) (normative). 이 문서는 그 boundary 문서의 *실행 계획*이다.
> **scope:** 계획 + 거버넌스 가드레일. 코드 변경은 PR 단위로 별도.
> **분량 한계:** PR 0~15까지의 슬라이스만 정식. PR 16+(BACKLOG H 트랙)는 Wave 2 envelope land 후 본 문서를 다시 갱신.
---
## 1. 목표 (Goal)
- **사용자 불만 (원문):** "Python 코드는 그 자체로도 너무 복잡하고, 기능 구현을 위한 많은 책임을 가지고 있음."
- **목표:** Python 레이어를 *Sublime API 호출 + 명령/리스너 등록 + 사용자 보이는 문자열* 중심으로 얇게 만든다. 알고리즘·동시성·정책 결정·프로토콜 파싱은 Rust로.
- **비목표:** 단순 LOC 감소 자체. LOC만 줄고 ABI 라운드트립·dataclass 중복·디버깅 단절이 늘면 *가짜 thinning*. 4축 가중치(사용자 영향 / 회귀 위험 / 거버넌스 / 인지 부담)로 매 슬라이스 평가.
## 2. 제약 (Constraints) — 본 plan은 이 라인 안에서만 움직인다
- **MUST §"Single source of truth"** ([boundary line 2327](PYTHON_RUST_BOUNDARY.md)): 동일 알고리즘을 Python·Rust 양쪽에 *상시* 두는 것 금지. 한 PR 안에서 Rust로 옮기고 Python 중복 *삭제*. 본 plan은 *short-lived dual-path*만 허용 — long-lived feature flag 금지.
- **MUST §"Remote tree / file I/O"** ([boundary line 1719](PYTHON_RUST_BOUNDARY.md)): tree/list·file/read·file/stat·file/write에 `python3 -c` SSH 폴백 두지 않는다. 현 시점 위반 잔재 = `ssh_runner.py` + `python_interpreter_browser.py` bootstrap. **PR 7로 청산.**
- **MUST §"Reliability invariant"** ([boundary line 815](PYTHON_RUST_BOUNDARY.md)): 요청 단위 오류는 프로세스 종료 사유가 아니다. 본 plan의 모든 Rust 이관 슬라이스는 `panic = "abort"` + clippy `panic/unwrap_used/expect_used = "deny"` 조합 + `catch_unwind` 격리로 *강화*해야 한다.
- **Wave 게이트:** Wave 2 envelope (`v`/`channel`/`kind`/`body`) 합의 *전*에는 worker loop / mirror BFS body / connect SM body 이관 PR을 머지하지 않는다.
## 3. 4인 팀 입장 요약 (참조용)
| 입장 | 핵심 주장 | 양보한 부분 | 끝까지 지킨 부분 |
|---|---|---|---|
| **rust-maximalist** ([POSITION](../tmp/python-thinning/POSITION_rust_maximalist.md), [RESPONSE](../tmp/python-thinning/RESPONSE_rust_maximalist.md)) | Python = "거의 빈 shell". 측정 없는 FFI 비용 주장 = 전략 결정 근거 부족. 후보 15개 ~6140 LOC, commands.py 2000 LOC 미만 목표. | file_state 단독 슬라이스(낮은 ROI), Part B(BFS body)는 envelope 후, OpenOutcomeKind enum은 Python single source, 사용자 문자열 Python 매핑, probe parser ~30 LOC 단독 거부. | Part A(queue/dispatcher) 이관, connect SM token Rust화, `_parse_*_outcome` 디코더 Rust화, envelope ID 발행 Rust. |
| **python-pragmatist** ([POSITION](../tmp/python-thinning/POSITION_python_pragmatist.md), [RESPONSE](../tmp/python-thinning/RESPONSES_python_pragmatist.md)) | "두꺼움"의 해법은 *Rust 호출 표면 확대*가 아닌 *Python 내부 응집*. ABI 라운드트립·dataclass 중복·디버거 단절·i18n 위험. | perf-cost framing은 측정 부재로 약화(human-cost framing은 유지), settings_model + interpreter probe 워밍업 인정, file_state 이관 반대를 ROI framing으로 약화. | Track H2 Python 내부 응집(8경로 ~1300 LOC), 디코더 Rust 이관 반대, 사용자 보이는 문자열 = Python 영역. |
| **boundary-keeper** ([POSITION](../tmp/python-thinning/POSITION_boundary_keeper.md), [RESPONSE](../tmp/python-thinning/RESPONSE_boundary_keeper.md)) | 11후보 판정 매트릭스. Wave 1.5 amend + thin shim 정량 정의 + ban-list lint 6종 + Wave 2 envelope 게이트가 *기계적* 거버넌스. | sidebar merge 거부 강도 하향(line 32 trigger 인정), diagnostics 거부 사유 정정(별 crate 신설 → 기존 위반 청산), file_state 우선순위 인상(silent corruption), PR-A 본문이 envelope 무관임 인정. | Wave 6/7 통합 신설 거부, Wave 2 envelope 게이트 절대, M1 단일진실 절대 라인, amend 절차로만 boundary 확장. |
| **shipping-operator** ([POSITION](../tmp/python-thinning/POSITION_shipping_operator.md), [RESPONSE](../tmp/python-thinning/RESPONSE_shipping_operator.md), [SYNTHESIS](../tmp/python-thinning/SYNTHESIS_shipping_operator.md)) | risk surface = 영향 × 발견 지연 × 변경 LOC. v0.6.12+1 / v0.7.24 / v0.6.5 측정 증거. 18-PR + 데드라인 메커니즘 3-layer. | rust_ffi split을 첫 PR로(워크플로우 시범), bootstrap 청산을 PR 7로 끌어올림(거버넌스 가중치), ROI 모델 명시화, H1 transaction-level 큰 PR 인정. | 5영역 동시 이관 거부, long-lived feature flag 거부 (Layer 3 auto-revert로 강제 종료), file_state 4번째 슬롯, file_state 패리티 테스트-먼저. |
## 4. 합의된 거버넌스 가드레일 (PR 0에 함께 land)
본 plan의 **모든** 슬라이스는 다음 가드레일을 통과해야 머지된다.
### 4.1 Boundary doc amend (PR 0)
[`PYTHON_RUST_BOUNDARY.md`](PYTHON_RUST_BOUNDARY.md)에 다음 4개 amend 본문을 land:
- **A1** (`§What stays in Python` 보강 #1): 사용자 보이는 모든 문자열은 Python에 둔다. Rust ABI는 *코드/식별자*만 반환. Enum 값은 Python single source of truth, Rust ABI는 *그 값을 echo*. 새 enum variant 추가는 Python *먼저*. 위반은 Lint #4 강제.
- **A2** (`§What stays in Python` 보강 #2): Python 측 서비스 모듈 분리(Track H2)는 허용하되 *retry/timeout/error mapping*은 모듈 분리 후에도 단일 헬퍼(`_rust_ffi`/bridge 호출 표면)로 수렴. 분산 금지. 위반은 Lint #2 강제.
- **A3** (`§Single source of truth` 보강): helper response JSON 파서는 *Rust 단일 권한*. Python은 Rust ABI 반환을 typed wrapper로 감쌀 수만 있고, 정규식·조건 분기·필드 fallback을 직접 수행 금지. 위반은 Lint #1 강제.
- **A4** (`§What belongs in Rust` 표 보강): `diagnostics_parser` (ruff + pyright + 향후 도구) → `sessions_native::diagnostics`. Python은 panel rendering / inline scope / path remap만.
또한 새 슬롯 **Wave 1.5** 추가 — Wave 1 마무리(부트스트랩 청산) + 위생 슬라이스(`_rust_ffi.py` 분할, settings_model 정규화, interpreter probe, diagnostics 청산)를 흡수.
### 4.2 Thin shim 정량 정의 (boundary 문서 amend)
> "Thin shim"의 작업 정의: 단일 모듈 ≤400 LOC + 비-shim 라인(알고리즘/조건분기/상태) ≤30% + 도메인 알고리즘 부재.
이 기준으로 `_rust_ffi.py` 1337 LOC는 *현 시점 위반*. PR 16에서 6 모듈로 split하여 통과시킨다.
### 4.3 Ban-list CI lint 7종
`scripts/lint_python_thinning.py` 신설, `.gitea/workflows/`에 등록. 활성화 시점은 슬라이스마다 다름.
| Lint | 룰 (요약) | 활성화 시점 |
|---|---|---|
| **#1** Helper response parser ban | Python 측에서 `parse_ruff` / `parse_pyright` / `parse_diagnostic` / `parse_open_outcome` / `parse_request_outcome` / `parse_response_packet` / `extract_handshake` / `payload_method_label` 시그니처 신규 금지. `_rust_ffi.py`의 thin ctypes wrapper만 허용 (본체 = `_lib.<함수>(...)` 호출 + dict 변환 1단계). | **PR 0** |
| **#2** Python deque/Event/Lock task queue 신설 ban | `commands.py` / `commands_*.py`에서 `_*_TASK_QUEUE = deque()` / `_*_TASK_EVENT = threading.Event()` 패턴 금지. 새 큐는 Rust supervisor에. | **PR 16** (PR-A 본체 머지 시) |
| **#2.5** Python 측 retry/timeout 분산 ban (Track H2 가드) | `commands_*.py` (Track H2 분리된 서비스 모듈)에서 `time.monotonic()` / `requests.exceptions` / `for _ in range(retries):` / `tenacity` 같은 retry/timeout 원시 직접 사용 금지. retry는 `_rust_ffi`/bridge 호출 표면에 응집. | **PR 0** (Track H2 시작 전 가드) |
| **#3** Python `python3 -c` SSH 폴백 ban | `sublime/sessions/``subprocess.*[ssh].*python3.*-c` 또는 `"python3", "-c"` literal 금지. | **PR 2** (bootstrap 청산 시) |
| **#4** 사용자 문자열 Rust ABI 반환 ban | `rust/crates/sessions_native/src/`에서 영문 자연어 문장(3+ 어휘)을 ABI 반환에 포함 금지. 식별자 코드(int, kebab-case)만 반환. | **PR 0** |
| **#5** Boundary inventory metasync | [boundary line 100112](PYTHON_RUST_BOUNDARY.md) Migration inventory 표를 `planning/boundary_inventory.yml`로 single source 화. CI가 코드 LOC 임계 + 시그니처 ban-list와 cross-check. | **Wave 2.5 슬라이스** ([잔존 쟁점 #1](#6-잔존-쟁점--리더-결정) 결정 결과) |
| **#6** PR `boundary-claim:` 헤더 필수 | 모든 이관 PR description에 `boundary-claim:` 블록(removes / delete-count / ban-list 활성화). CI 훅이 diff 검증. | **PR 0** |
### 4.4 데드라인 메커니즘 3-layer (이중 구현 임시 잔존 강제 만료)
본 plan은 short-lived dual-path만 허용. *임시 병행*이 release 사이를 넘어서 누적되지 않도록:
| Layer | 메커니즘 | 활성화 |
|---|---|---|
| **Layer 1** | PR template 필수 마커: `TEMP_DUPLICATION_UNTIL=v0.X.Y` + `DELETION_PR=#NNN`. `v0.X.Y`는 현재 + 1 minor 이내. | **PR 0** |
| **Layer 2** | `.gitea/workflows/duplication-deadline.yml` — main HEAD에서 마커 grep + 현재 버전 비교. 만료 시 release 차단. | **PR 0** |
| **Layer 3** | Auto-revert: `DELETION_PR=#NNN`이 같은 sprint(2주) 내 머지 안 되면 원 이관 PR 자동 revert. | **Wave 2 envelope (PR 14) land 후** — envelope 슬라이스 자체가 Layer 3로 자동 revert당하면 회귀 폭발. |
## 5. PR 시퀀스 (PR 0 → 16)
> **참조**: 슬라이스 LOC 추정은 1차 인벤토리 + 2/3라운드 검증 결과의 *관용적* 추정치. PR description의 `boundary-claim:` 블록에 정확한 라인 범위와 delete-count를 기록.
>
> **3라운드 SYNTHESIS 갱신점 (vs v1)**:
> - 4명 합의된 PR 순서를 그대로 채택 (boundary-keeper SYNTHESIS §5.3).
> - PR 13(envelope) → **PR 13a(스펙+ref impl+parity test) / PR 13b(완전 구현) 분할** — rust-maximalist 합의, spec drift 방지 가드.
> - PR 16(PR-A 본체) 사이즈 ~860 → **~600 LOC** — connect 진행 메시지(워크플로우 안내)는 Python 유지.
> - PR 7(bootstrap) → **PR 2로 앞당김** — 거버넌스 가중치(MUST §1719 위반 청산)가 silent corruption 영역(file_state)보다 *기계적 청산* 우선.
### Wave 1.5 (위생 + Wave 1 마무리)
#### **PR 0 — 거버넌스 가드레일 활성화** (코드 변경 0)
- [`PYTHON_RUST_BOUNDARY.md`](PYTHON_RUST_BOUNDARY.md)에 amend §A§N (외부 draft `tmp/python-thinning/AMEND_DRAFT_boundary_keeper.md` 본문) land. 핵심:
- §A 디폴트 거버넌스 (line 56 enumerated list 밖은 디폴트 Rust)
- §C Single source of truth 양방향 보강 (parser + Rust ABI 자연어 ban)
- §D Parity test 인프라 (paired parity test PR 선행 필수)
- §E What stays in Python 보강 (사용자 문자열 + 모듈 분리 가드)
- §F What belongs in Rust 표 신설 4행 (diagnostics_parser, settings_normalize, interpreter_probe, abi_decoders)
- §H Wave 1.5 행 신설 + thin shim 정량 정의
- §I Wave 2 게이트 (PR 13a/13b 분할 명시)
- §M 위생 라인 (`rust/crates/sessions_native/src/broker.rs:117` stale `#![allow(dead_code)]` + "S2.3S2.5 not wired" docstring 제거)
- `scripts/lint_python_thinning.py` 신설 — **Lint #1, #2.5, #4, #6 활성화**. (#2, #3은 후속 PR에서, #5는 Wave 2.5에서.)
- `.gitea/workflows/duplication-deadline.yml` 신설 — Layer 1, 2 활성화.
- `boundary_inventory.yml` *초안만* (Wave 2.5에서 자동화).
**AC**: lint가 현재 코드 베이스에서 *새 위반*은 차단, *기존 위반*은 grandfather. CI 그린. 4명 모두 amend 본문 합의 (3라운드 SYNTHESIS 도달).
#### **PR 1 — settings_model 정규화** (Wave 1.5 워밍업, ROI 정직화)
`settings_model.py` 정규화 함수(`normalize_remote_python_tool_pipeline` 등 ~80 LOC) → `sessions_settings` 신규 crate.
**`boundary-claim:` 헤더에 ROI 정직화 명시:** "LOC 절감 ~80, 진짜 가치는 (a) 데드라인 메커니즘 dry-run, (b) Lint #1/#6 시운전, (c) Wave 1.5 워크플로우 검증."
**AC**: `load_sessions_settings_from_sublime`은 Python 유지 (Sublime API 결합). 정규화 단위 테스트 패리티.
#### **PR 2 — Bootstrap tree/list 청산** ([Wave 1 closure](PYTHON_RUST_BOUNDARY.md))
`ssh_runner.py` + `python_interpreter_browser.py``python3 -c` 부트스트랩 디렉토리 리스팅 제거 → `session_helper tree/list` 호출로 일원화 (~180 LOC 감소).
**Lint #3 동시 활성화** — 다시는 Python에 `python3 -c` 폴백이 안 들어가도록 컴파일-게이트.
**AC**: SSH 폴백 0건. boundary doc MUST §1719 완전 청산.
#### **PR 37 — `_rust_ffi.py` 6 모듈 split** (코드 이동만, ROI: thin shim 위반 청산)
`sublime/sessions/_rust_ffi.py` (1337 LOC) → `sublime/sessions/_rust_ffi/` 패키지:
1. `__init__.py` (loader + AbiError + 공통 `call_string_abi`)
2. `_workspace.py` (normalize_remote_root, workspace_cache_key)
3. `_file_policy.py` (open_guard_reason_code, is_likely_binary, reload/save 결정, 경로 매퍼)
4. `_tool_runtime.py` (parse_ruff_diagnostics)
5. `_bridge_parsers.py` (envelope, response packet, handshake, error_code, mirror result)
6. `_broker.py` (open_session, request, reset, shutdown_all, is_active, handshake, stderr_tail + outcome dataclasses)
**제외:** 디코더 본체 Rust 이관(`_parse_*_outcome`)은 PR 17+로 미룸 (rust-max 양보 영역). 이번 PR들은 *코드 이동만*. 각 결과 모듈 ≤400 LOC + 비-shim 라인 ≤30% (thin shim 정량 정의 통과).
**AC** (PR마다): import 경로만 바뀌고 동작 동일. 기존 테스트 그린. `boundary-claim:` 블록에 이동 LOC 명시.
#### **PR 5.5 (W1.5.0) — diagnostics 파싱 중복 청산**
`sublime/sessions/diagnostics.py:225333` (~110 LOC) 삭제 → `_rust_ffi.parse_ruff_diagnostics` 호출로 일원화. 패널/인라인/경로 매핑(~497 LOC)은 Python 유지.
**선행:** parity test PR (동일 ruff JSON 입력 → 두 파서 결과 비트 동일). amend §D 의무 적용.
**AC**: `test_diagnostics_models.py` 30개 테스트가 새 호출 표면에 재배선되어 모두 통과. Python 측 파싱부 0줄. Lint #1이 추가 위반 차단 중.
#### **PR 8 — interpreter probe 캐시/랭킹 이관**
`python_interpreter_registry.py`의 캐시·랭킹 로직 (~100 LOC) → `sessions_python_interp` 신규 crate.
**유지:** `_parse_probe_stdout` 정규식 ~30 LOC는 Python에 유지 (rust-max 양보 영역, ROI 낮음). 상태바 키 바인딩도 Python.
**AC**: 캐시 동작 동일. 회귀 테스트 (`tests/test_python_interpreter_registry.py` 기준).
#### **PR 9 — tree/list 잔여 호출자 정리**
PR 2 청산 후 잔여하는 Python 측 tree/list 호출자(현재 인벤토리 시점에 ssh_runner.py가 일부, python_interpreter_browser.py가 일부)의 helper 채널 호출 일원화.
**AC**: SSH 폴백이 다시 들어올 코드 경로 0개. lint #3 위반 0건.
#### **PR 10 — file_state 패리티 테스트 (테스트-먼저)** [silent corruption 영역]
기존 `evaluate_open_file` / `evaluate_save_file` / `kind_codes` 매핑에 대해 *Python 동작 baseline* 패리티 테스트 추가. 이관 PR 11이 이를 깨지 않음을 보장. amend §D 적용.
**AC**: 테스트가 Python 현 동작 그대로 fixture화. ≥30 시나리오 (open/save/conflict/binary).
#### **PR 11 — file_state 결정 매핑 이관**
`file_state.py``kind_codes` 3중 복제 통합 + Python ↔ Rust 결정 매핑 정리 (~120 LOC 감소). SaveConflict.message 등 사용자 보이는 문자열은 Python single source 유지 (amend §C/§E).
**AC**: PR 10 패리티 테스트 100% 그린. `boundary-claim: removes ~120 LOC`.
#### **PR 12 — eager_hydrate 패리티 테스트 (테스트-먼저)**
amend §D 적용.
### Wave 2 게이트 — PR 13a/13b가 게이트, 이 라인 *후*에만 PR 14+ 진행
#### **PR 13a — Multiplex envelope 스펙 + reference impl + parity test**
`session_protocol``v` / `channel` / `kind` / `body` envelope **스펙 확정** + 최소 reference impl + parity test 1개. 본 PR이 envelope의 *spec freeze*. 이 PR 머지 *후*에만 PR-A 본체(PR 16) 가능 — supervisor API가 envelope 표준에 정합하게 빚어진다는 보장.
**AC**: backward-compat. 기존 NDJSON 메시지 통과. parity test 그린. spec drift 방지 — 본 PR 외부에서 envelope 필드 추가/변경 금지.
#### **PR 13b — Multiplex envelope 완전 구현**
PR 13a 위에 채널 supervisor + per-request timeout + 취소·deadline 의미 land. 13a/13b 분할은 rust-maximalist의 envelope spec drift 가드.
**AC**: 새 멀티플렉스 케이스 unit + integration test. cancel 의미가 helper에 도착(boundary doc gap 1번 부분 해소).
#### **PR 14 — eager_hydrate BFS 이관**
`eager_hydrate.py`의 placeholder BFS + 배치 페이싱 → `local_bridge::remote_cache_mirror` 통합. 결과 보고만 Python 유지 (~180 LOC 감소).
**AC**: PR 12 패리티. 성능 비교 (Python 기준 동등 이상). multiplex envelope 위에서 동작.
#### **PR 14.5 — H1 file_open transaction**
[BACKLOG H1](BACKLOG.md) — file_open을 단일 transaction으로 묶어 silent corruption 차단. transaction-level 큰 PR 인정 (shipping-operator 양보).
**AC**: 기존 silent-corruption 시나리오 회귀 테스트 5종 그린.
#### **PR 15 — H3-reconnect (auto-reconnect thread + connect SM token)**
[BACKLOG H3](BACKLOG.md) first-PR scope. (a) auto-reconnect thread → broker driven, (b) `_CONNECT_GENERATION`/`_CONNECT_INFLIGHT` token 의미만 Rust로. 워크플로우 진행 메시지(사용자 보이는 문자열)는 Python 유지 (amend §C enum 정합 + amend §E 사용자 문자열).
**AC**: BACKLOG H3 first-PR scope 안. PR-A 분리의 전제 충족.
#### **PR 15.5 — PR-A integration tests (테스트-먼저)**
3개 신설 integration test:
- `test_orchestrator_supervisor.py` (≥30 케이스 — Rust supervisor 안 Python callable invariant)
- `test_connect_preempt_property.py` (proptest 5,000회 — connect generation/preempt 의미)
- `test_orchestrator_python_panic.py` (M1 invariant 회귀 — Rust supervisor 안 Python callable raise → trace event + 후속 task 정상 dispatch)
amend §D paired parity test 의무. PR-A 본체 land 전 단독 PR로 머지.
#### **PR 16 — PR-A 본체: queue/dispatcher/lane gating Rust 이관** (~600 LOC)
queue/dispatcher/lane gating + `_CONNECT_GENERATION` token 의미 + `_connect_generation_is_stale``sessions_orchestrator` 신규 crate. Python 측 deque/Lock/Event/dropped 추적/generation token/preempt SM 전부 *삭제*. 사용자 보이는 워크플로우 진행 메시지는 PR 15 양보 영역으로 Python 유지 → 사이즈 ~860 → ~600.
**Lint #2 동시 활성화** — 다시는 Python에 deque 기반 task queue가 안 생김.
**AC**: PR 15.5 테스트 100% 그린. v0.7.24 `disciscard`-class 오타 회귀 시 cargo check가 즉시 차단. M1 invariant `catch_unwind` 격리 검증.
### PR 17+ — 본 plan scope 밖 (별도 갱신)
PR 16(PR-A) land 후 본 plan을 갱신해서:
- **PR-B**: mirror BFS task body, eager_hydrate apply 본체 → orchestrator (PR 13b envelope 위에서)
- **H3-queue**: BACKLOG H3 본 이관 (queue 본체)
- **H2-save / H2-connect**: BACKLOG H2 분할 (Track H2 main track 흡수)
- **`_rust_ffi` 디코더 Rust 이관**: `_parse_*_outcome` Rust ABI typed JSON (Rust schema oracle 도구는 잔존 쟁점 #6 결정 후)
- **데드라인 Layer 3** auto-revert 활성화
이 시점에 commands.py 예상 LOC: 7394 - (worker loop ~550) - (connect SM ~330 부분) - (hydrate preflight ~300, PR 1214 영향) ≈ **55006000 LOC**.
> **rust-maximalist의 "2000 LOC 미만" 목표는 본 plan scope 안에서는 미달성.** 그가 1라운드에서 인정한 도전 질문(Wave 5 후 5000+ LOC 잔존) 그대로다. 본 plan은 *책임 위치* 정상화에 집중하고, *파일 분할*은 Track H2(Python 내부 응집)에서 별도 진행.
### Track H2 (Python 내부 응집) — *병행 트랙*
main track과 *별개로* 진행:
- `commands_runtime_queue.py`, `commands_sidebar_mirror.py`, `commands_connect.py` 등 추출 — **amend §E 모듈 분리 가드 적용** (retry/timeout/error mapping 분산 금지). 위반은 Lint #2.5가 차단.
- `_rust_ffi/` split (PR 37)이 이미 패턴 시범.
- `kind_codes` 3중 복제 통합 (PR 11에 흡수).
main track과 충돌 시 main track 우선. Track H2는 *코드 이동* 위주, 책임 위치는 변하지 않음.
## 6. 잔존 쟁점 — 리더 결정
3라운드 SYNTHESIS까지 도달 후 미합의 7개에 대한 리더 판단. 모두 약한 선호 영역이며 plan 진행을 막지 않음.
| # | 쟁점 | 리더 결정 | 근거 |
|---|---|---|---|
| 1 | **Lint #5 (boundary inventory metasync YAML)** PR 0 vs Wave 2.5 | **PR 0에 *수동* YAML 초안 + 시그니처 cross-check만, *자동 LOC 측정*은 Wave 2.5**. boundary-keeper SYNTHESIS 합의안. | 자동 LOC 게이트는 비용·노이즈 추정 어려움. 수동 YAML + 시그니처 cross-check만으로도 PR 114 거버넌스 추적 가능. |
| 2 | **PR 16 (PR-A 본체) 사이즈** | **~600 LOC (워크플로우 진행 메시지 Python 유지)**, PR 15.5 paired test PR 강제. | rust-maximalist 3라운드 SYNTHESIS 정정 (~860 → ~600). amend §D paired parity 의무. |
| 3 | **Enum 정책** Python single source vs parity test | **Python single source + Rust echo (amend §C)**. parity test는 보조 안전망. | python-pragmatist §5 + boundary-keeper §1.3 + rust-maximalist 양보. 사용자 보이는 문자열 i18n/UX 일관성. |
| 4 | **diagnostics 청산 위치** | **PR 5.5 (rust_ffi split 후속, bootstrap 청산 후)**. boundary-keeper SYNTHESIS 합의. | PR 0 Lint #1이 추가 위반 차단 중. 실제 코드 삭제는 워크플로우 안정 후가 안전. silent corruption 영역인 file_state(PR 10/11)가 가중치 우선. |
| 5 | **Track H2 (Python 내부 응집 ~1300 LOC) 대체 vs 병행** | **병행 (main track과 별개)**. Lint #2.5가 가드. | rust-maximalist는 책임 위치, python-pragmatist는 파일 정리 — 다른 axis. main track 우선. |
| 6 | **Wave 5 재확인 amend 형태** (a 유지 / b 삭제 / c 일반화) | **(c) 일반화** — boundary-keeper 약한 선호 채택. | chat→tmux pivot으로 #29 product 위치 흔들림. plan v2 갱신 시점에 "diff/agent apply 단계는 Wave 2 envelope·취소·캐시 위에서 정의되며, product surface는 후속 결정"으로 일반화. |
| 7 | **lsp_proxy crate 신설 시점** Wave 1.5 vs Wave 2.5 | **Wave 2.5 (envelope·취소·deadline land 후)** — boundary-keeper 약한 선호 채택. | boundary doc line 45가 부분 normative. envelope 합의 *전*에 lsp_proxy를 신설하면 envelope 표준을 lsp_proxy가 *암묵 결정*. `local_bridge::lsp_stdio` 모듈 확장이 신설 crate보다 정합. |
| 8 | **Rust schema 자동화 도구** (`serde + schemars` vs `PyO3 + pythonize`) | **PR 17+ 결정 — 본 plan scope 밖**. `_parse_*_outcome` 디코더 Rust 이관 시점에 별도 ADR. | rust-maximalist 3라운드 잔존 쟁점. PR 116 진행에는 영향 없음. |
## 7. 성공 기준 (Acceptance Criteria — plan 전체)
-`_rust_ffi.py` 1337 LOC → `_rust_ffi/` 패키지 (각 모듈 ≤400 LOC). thin shim 정량 정의 통과.
-`python3 -c` SSH 폴백 0건. Lint #3 그린.
-`commands.py` worker loop + connect SM token + queue → `sessions_orchestrator`. Python 측 deque/Event/Lock 기반 task queue 0건. Lint #2 그린.
- ✅ Helper response 파싱 = Rust 단일 권한. Lint #1 그린.
- ✅ Wave 2 envelope (`v`/`channel`/`kind`/`body`) land. Wave 3+ 후속 가능.
- ✅ 사용자 보이는 모든 문자열은 Python에 응집. Rust ABI는 식별자만. Lint #4 그린.
- ✅ 모든 이관 PR이 `boundary-claim:` 헤더 + Layer 1/2 데드라인 마커 통과.
- ✅ 회귀: 최근 6개월 사례(v0.6.12 #13/#14, v0.7.24 `disciscard`, v0.6.5 palette 누락)와 같은 종류 회귀 0건. Cluster A LSP race가 본 plan으로 *도입되지 않음*.
추정 Python LOC 변화 (PR 0 → PR 15 완료 시점):
- 삭제: bootstrap 180 + diagnostics parser 110 + file_state 매핑 120 + worker queue + connect token ~530 + eager_hydrate 180 ≈ **~1120 LOC**
- 이동 (책임 위치 미변경): `_rust_ffi.py` 1337 → `_rust_ffi/` 6 모듈 (총 LOC 비슷)
- Track H2 추가 정리: 별도 ~1300 LOC 절감 (병행)
- Sublime/sessions 합산 23437 → ~21000 (main track) → ~19700 (Track H2 포함)
LOC 자체는 절대 metric이 아니다. **인지 부담 metric**: 한 화면에 안 들어오는 모듈 개수 / 한 책임당 평균 파일 수 / 한 PR description의 "Python 측 변경" 평균 LOC가 줄어드는 것이 진짜 목표.
## 8. 다음 단계
1. 본 plan을 사용자 검토.
2. PR 0 — 거버넌스 가드레일 PR 작성 (코드 변경 0, boundary doc amend + lint 스크립트 + workflow YAML).
3. PR 1부터 순서대로 진행. 각 PR이 머지될 때 본 문서 §5 표 갱신.
4. PR 14 (Wave 2 envelope) land 직후 본 plan v2 작성 — PR 16+ 슬라이스 정식화.
## 9. 참조 — 팀 산출물
- `tmp/python-thinning/SHARED_CONTEXT.md` — 4명 공통 입력 자료.
- `tmp/python-thinning/POSITION_*.md` — 1라운드 입장 paper (4건).
- `tmp/python-thinning/RESPONSE_*.md` / `RESPONSES_*.md` — 2라운드 도전 답변 (4건).
- `tmp/python-thinning/SYNTHESIS_*.md` — 3라운드 합의 매트릭스 (3건: shipping-operator / boundary-keeper / rust-maximalist).
- `tmp/python-thinning/AMEND_DRAFT_boundary_keeper.md`**PR 0이 그대로 pull 가능한 boundary doc amend 통합 본문** (§A§N 13개 섹션).

View File

@@ -0,0 +1,183 @@
# Boundary Inventory — single-source-of-truth for Python ↔ Rust 책임 위치
#
# normative 출처: planning/PYTHON_RUST_BOUNDARY.md "Migration inventory" 표.
# 본 YAML은 그 표의 *수동 변환*이며, Wave 2.5에서 LOC 임계 자동 측정과 함께
# 자동 동기화로 승격된다. 현 단계(PR 0)에서는 Lint #5 minimal — 시그니처
# cross-check 용도.
#
# Lint #5 (PR 0 minimal): boundary_inventory.yml의 `parsers_banned_in_python`
# 목록과 sublime/sessions/ 코드의 def 시그니처를 cross-check. 위반 시 fail.
#
# 갱신 규칙:
# - 슬라이스가 land될 때마다 본 YAML과 PYTHON_RUST_BOUNDARY.md "Migration
# inventory" 표를 *같은 PR 안에서* 갱신. drift 발생 시 PR 0의 boundary-claim
# 헤더 검증 (Lint #6)이 차단.
version: 1
last_updated: "2026-05-01" # PR 0 land 시점
# ---------------------------------------------------------------------------
# Python 모듈별 책임 분류
# ---------------------------------------------------------------------------
modules:
# === 1. Sublime API에 결합된 모듈 (Python 영역) ===
- path: sublime/sessions/commands.py
role: sublime-orchestration
loc_estimate: 7394
rust_home: null # Stays Python (Sublime command shells + EventListeners)
notes: |
worker loop SM (queue + dispatcher + lane gating + _CONNECT_GENERATION token)은
PR 16에서 sessions_orchestrator로 이관 (~600 LOC). 진행 메시지는 Python 유지.
Track H2 (commands_runtime_queue.py 등 분할)은 main track과 병행.
- path: sublime/sessions/commands_file_actions.py
role: sublime-orchestration
loc_estimate: 769
rust_home: null
- path: sublime/sessions/commands_python_pipeline.py
role: sublime-orchestration
loc_estimate: 1418
rust_home: null
notes: |
Sublime command shells. 그러나 ruff/pyright pipeline 빌더는 Wave 1.5에서
sessions_native::diagnostics_parser로 분리 가능. 평가는 Wave 2 후.
- path: sublime/sessions/connect_progress.py
role: sublime-orchestration
loc_estimate: 316
rust_home: null
- path: sublime/sessions/lsp_project_wiring.py
role: sublime-orchestration
loc_estimate: 640
rust_home: local_bridge::lsp_stdio # Wave 2.5 모듈 확장
notes: deep-merge 로직만 이관, project file 편집은 Python 유지.
- path: sublime/sessions/marimo_hosting.py
role: sublime-orchestration
loc_estimate: 614
rust_home: null
# === 2. 이미 Rust로 부분/전체 이관된 모듈 ===
- path: sublime/sessions/_rust_ffi.py
role: thin-shim-violator # 현재 1337 LOC, thin shim 정량 정의 위반
loc_estimate: 1337
rust_home: sessions_native::abi_decoders # 디코더만, PR 17+
notes: |
Wave 1.5 (PR 37): 6 모듈 split (loader / workspace / file_policy /
tool_runtime / bridge_parsers / broker). 각 ≤ 400 LOC.
디코더 (_parse_*_outcome) Rust 이관은 PR 17+.
- path: sublime/sessions/file_state.py
role: sublime-domain
loc_estimate: 671
rust_home: sessions_native::file_policy # 이미 결정 코드 위임
wave: 1.5
notes: |
PR 10 parity → PR 11 이관. kind_codes 3중 복제 통합 + decision 매핑
lookup table. SaveConflict.message 등은 Python single source.
- path: sublime/sessions/workspace_state.py
role: sublime-domain
loc_estimate: 636
rust_home: workspace_identity
wave: 1
notes: normalize_remote_root는 Rust 전용; cache_key hashing은 Python 잔존.
- path: sublime/sessions/ssh_runner.py
role: glue
loc_estimate: 654
rust_home: local_bridge + session_helper
wave: 1
notes: bootstrap python3 -c 폴백 PR 2에서 청산.
- path: sublime/sessions/python_interpreter_browser.py
role: glue
loc_estimate: 244
rust_home: session_helper::tree_list
wave: 1
notes: PR 2 청산 후 helper tree/list 호출.
- path: sublime/sessions/ssh_file_transport.py
role: glue
loc_estimate: 2240
rust_home: local_bridge + session_helper
wave: 1
notes: bridge session broker. _payload_method_label은 PR 17+ Rust 이관.
- path: sublime/sessions/diagnostics.py
role: split-target
loc_estimate: 607
rust_home: sessions_native::diagnostics_parser # ruff parser ~110 LOC만
wave: 1.5
notes: |
PR 5.5 (W1.5.0): line 225333 ruff 파서 삭제 → _rust_ffi 호출 일원화.
Panel rendering / inline scope / path remap (~497 LOC)는 Python.
- path: sublime/sessions/settings_model.py
role: split-target
loc_estimate: 494
rust_home: sessions_native::settings_normalize
wave: 1.5
notes: |
PR 1: 정규화 함수 ~80 LOC → Rust. load_sessions_settings_from_sublime은
Python (Sublime API 결합).
- path: sublime/sessions/python_interpreter_registry.py
role: split-target
loc_estimate: 455
rust_home: sessions_native::interpreter_probe
wave: 1.5
notes: |
PR 8: 캐시·랭킹 ~100 LOC → Rust. _parse_probe_stdout 정규식 ~30 LOC는
Python 잔존 (rust-max 양보 영역).
- path: sublime/sessions/eager_hydrate.py
role: split-target
loc_estimate: 247
rust_home: local_bridge::remote_cache_mirror
wave: 2
notes: PR 12 parity → PR 14 이관 (Wave 2 envelope 후).
# ---------------------------------------------------------------------------
# Lint #1 cross-check 데이터: Python 측에 신규 정의 금지 시그니처
# ---------------------------------------------------------------------------
parsers_banned_in_python:
- parse_ruff
- parse_pyright
- parse_diagnostic
- parse_open_outcome
- parse_request_outcome
- parse_response_packet
- extract_handshake
- payload_method_label
parsers_exempt_paths:
- sublime/sessions/_rust_ffi.py # 단일 파일 (PR 0~2 동안)
- sublime/sessions/_rust_ffi/ # 6 모듈 split 이후 (PR 3+)
# ---------------------------------------------------------------------------
# 알려진 grandfather 위반 (PR 0 land 시점 기준)
#
# 본 항목은 신규 위반이 *아니*고 PR 0 활성화 시 main에 이미 있던 위반.
# 후속 PR에서 청산 예정. CI는 diff 기반이라 자동으로 grandfather 처리되지만,
# 가시성 위해 명시.
# ---------------------------------------------------------------------------
grandfather_violations:
- path: sublime/sessions/ssh_file_transport.py
line: 1378
pattern: "_payload_method_label"
lint: "#1"
cleanup_pr: "PR 17+ (디코더 Rust 이관)"
- path: sublime/sessions/commands_python_pipeline.py
line: 639
pattern: "time.monotonic"
lint: "#2.5"
cleanup_pr: "Track H2 분리 시 retry/timeout을 _rust_ffi/bridge로 이동"

110
scripts/duplication_deadline.py Executable file
View File

@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""Duplication deadline enforcement (Layer 1/2).
main HEAD에 남은 TEMP_DUPLICATION_UNTIL 마커를 grep하고, 현재 버전과
비교해 만료된 마커가 있으면 fail. release 차단 가드.
마커 형식 (예시; ``vX.Y.Z`` 자리는 실제 버전):
# TEMP_DUPLICATION_UNTIL = vX.Y.Z
# DELETION_PR = #NNN
위치: 주석/docstring/PR description 어디든 가능. 본 스크립트는 *코드 트리*만
검사한다 (planning/, .gitea/, scripts/, sublime/, rust/, tests/).
normative 출처: planning/PYTHON_RUST_BOUNDARY.md "Single source of truth" +
planning/PYTHON_THINNING_PLAN.md §4.4 (3-layer 데드라인).
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
from typing import List, Tuple
import tomllib
REPO_ROOT = Path(__file__).resolve().parent.parent
SCAN_DIRS = ("planning", ".gitea", "scripts", "sublime", "rust", "tests")
SCAN_EXTENSIONS = {".py", ".rs", ".md", ".yml", ".yaml", ".toml"}
MARKER_RE = re.compile(
r"TEMP_DUPLICATION_UNTIL\s*=\s*v?(?P<version>\d+\.\d+\.\d+)",
)
def _current_version() -> Tuple[int, int, int]:
pyproject = REPO_ROOT / "pyproject.toml"
data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
raw = data.get("project", {}).get("version") or data.get("tool", {}).get(
"poetry", {}
).get("version")
if raw is None:
raise SystemExit("pyproject.toml에서 version을 찾지 못함")
parts = raw.lstrip("v").split(".")
if len(parts) != 3 or not all(p.isdigit() for p in parts):
raise SystemExit(f"비표준 버전: {raw!r}")
return (int(parts[0]), int(parts[1]), int(parts[2]))
def _scan() -> List[Tuple[Path, int, str, Tuple[int, int, int]]]:
findings: List[Tuple[Path, int, str, Tuple[int, int, int]]] = []
for top in SCAN_DIRS:
root = REPO_ROOT / top
if not root.exists():
continue
for path in root.rglob("*"):
if not path.is_file():
continue
if path.suffix not in SCAN_EXTENSIONS:
continue
try:
text = path.read_text(encoding="utf-8")
except (UnicodeDecodeError, OSError):
continue
for n, line in enumerate(text.splitlines(), 1):
m = MARKER_RE.search(line)
if not m:
continue
v = m.group("version").split(".")
version = (int(v[0]), int(v[1]), int(v[2]))
findings.append(
(path.relative_to(REPO_ROOT), n, line.strip(), version),
)
return findings
def main() -> int:
current = _current_version()
findings = _scan()
expired: List[Tuple[Path, int, str, Tuple[int, int, int]]] = []
for entry in findings:
deadline = entry[3]
if deadline <= current:
expired.append(entry)
if not findings:
print("duplication-deadline: 마커 없음 — pass")
return 0
cur_str = "{}.{}.{}".format(*current)
print(f"duplication-deadline: 현재 v{cur_str}")
for path, line_no, content, deadline in findings:
deadline_str = "{}.{}.{}".format(*deadline)
status = "EXPIRED" if (path, line_no, content, deadline) in expired else "ok"
print(f" [{status}] {path}:{line_no} TEMP_DUPLICATION_UNTIL=v{deadline_str}")
if expired:
print(
f"\n{len(expired)}건 데드라인 만료. "
f"해당 이중 구현은 v{cur_str} 이전에 삭제됐어야 함. "
"release 차단.",
file=sys.stderr,
)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

355
scripts/lint_python_thinning.py Executable file
View File

@@ -0,0 +1,355 @@
#!/usr/bin/env python3
"""Boundary lint — Python thinning ban-list checker.
Wave 1.5 거버넌스 가드. PR/push diff에서 *추가된 라인*만 검사하므로
기존 코드의 grandfather 처리가 자동으로 된다.
Usage:
scripts/lint_python_thinning.py [--base-ref REF] [--lint LINT [LINT ...]]
scripts/lint_python_thinning.py --pr-body PATH # Lint #6 only
활성 룰 (PR 0):
- #1 helper response parser 시그니처 ban (Python 측)
- #2.5 Track H2 retry/timeout 분산 ban (commands_*.py)
- #4 Rust ABI 영문 자연어 ban (Rust 측)
- #6 PR boundary-claim 헤더 검증
후속 활성화 룰 (env LINT_THINNING_ACTIVATE 로 켬):
- #2 Python deque/Event/Lock task queue 신설 ban (PR 16에서 활성화)
- #3 Python python3 -c SSH 폴백 ban (PR 2에서 활성화)
- #5 boundary inventory metasync (Wave 2.5에서 자동화)
normative 출처: planning/PYTHON_RUST_BOUNDARY.md (Wave 1.5 amend),
planning/PYTHON_THINNING_PLAN.md §4.3.
"""
from __future__ import annotations
import argparse
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import Iterable, List, Optional, Tuple
REPO_ROOT = Path(__file__).resolve().parent.parent
# ---------------------------------------------------------------------------
# 규칙 정의
# ---------------------------------------------------------------------------
# Lint #1 — Helper response parser 시그니처 ban (Python 측 sublime/sessions/)
# `_rust_ffi/`(또는 `_rust_ffi.py`)의 thin ctypes wrapper만 예외.
LINT_1_PARSER_SIGNATURES = re.compile(
r"^\s*def\s+_?(parse_ruff|parse_pyright|parse_diagnostic|"
r"parse_open_outcome|parse_request_outcome|parse_response_packet|"
r"extract_handshake|payload_method_label)\b",
)
LINT_1_PATH_PATTERN = re.compile(r"^sublime/sessions/")
LINT_1_EXEMPT_PATH_PATTERN = re.compile(r"^sublime/sessions/_rust_ffi(/|\.py$)")
# Lint #2.5 — Track H2 retry/timeout 분산 ban
# commands_*.py 분리 모듈에서 retry/timeout 원시 직접 사용 금지.
# (commands.py 본체는 이미 이런 코드를 보유 — diff 기반이라 자동 grandfather.)
LINT_2_5_RETRY_PATTERNS = [
re.compile(r"\btime\.monotonic\s*\("),
re.compile(r"\brequests\.exceptions\b"),
re.compile(r"\btenacity\b"),
re.compile(r"\bfor\s+\w+\s+in\s+range\s*\(\s*\w*retries?\b"),
re.compile(r"\bbackoff\.\w+"),
]
LINT_2_5_PATH_PATTERN = re.compile(r"^sublime/sessions/commands_[^/]+\.py$")
# Lint #4 — Rust ABI 영문 자연어 ban (Rust 측 sessions_native ABI 함수)
# 식별자 코드만 반환해야 함. ABI 응답에 영문 자연어 문장(공백 + 3+ 어휘) 포함 금지.
# 휴리스틱: ABI 함수 본문 string literal "Word word word..." 패턴 grep.
LINT_4_NATURAL_LANGUAGE = re.compile(r'"[A-Z][a-z]+(?:\s+[a-z]+){2,}[\.,!?]?"')
LINT_4_PATH_PATTERN = re.compile(r"^rust/crates/sessions_native/src/")
# Lint #6 — PR boundary-claim 헤더 검증
# PR description에 다음 블록이 있어야 함:
# boundary-claim:
# removes: <list>
# delete-count: <int>
# ban-list: <list>
LINT_6_BOUNDARY_CLAIM = re.compile(
r"^boundary-claim:\s*$\s*"
r"(?:^\s+removes:\s*.*?\s*$\s*)?"
r"(?:^\s+delete-count:\s*\d+\s*$\s*)?"
r"(?:^\s+ban-list:\s*.*?\s*$\s*)?",
re.MULTILINE,
)
# ---------------------------------------------------------------------------
# Diff 추출
# ---------------------------------------------------------------------------
def _git(args: List[str]) -> str:
result = subprocess.run(
["git", *args],
cwd=REPO_ROOT,
capture_output=True,
text=True,
check=False,
)
return result.stdout
def _resolve_base_ref(explicit: Optional[str]) -> Optional[str]:
if explicit:
return explicit
env_base = os.environ.get("LINT_THINNING_BASE_REF")
if env_base:
return env_base
if os.environ.get("CI"):
merge_base = _git(["merge-base", "HEAD", "origin/main"]).strip()
if merge_base:
return merge_base
return None
def _added_lines(base_ref: Optional[str]) -> List[Tuple[Path, int, str]]:
"""Return (path, line_no_in_new_file, content) for every line added vs base.
base_ref None이면 working tree 전체를 검사한다 (PR 0 활성화 시 sanity).
"""
if base_ref is None:
# 전수 검사 — grandfather 없음. PR 0에서는 호출하지 않는 게 정상.
results: List[Tuple[Path, int, str]] = []
for py in sorted(REPO_ROOT.glob("sublime/**/*.py")):
rel = py.relative_to(REPO_ROOT)
for n, line in enumerate(py.read_text(encoding="utf-8").splitlines(), 1):
results.append((rel, n, line))
for rs in sorted(REPO_ROOT.glob("rust/crates/**/*.rs")):
rel = rs.relative_to(REPO_ROOT)
for n, line in enumerate(rs.read_text(encoding="utf-8").splitlines(), 1):
results.append((rel, n, line))
return results
raw = _git(["diff", "--unified=0", base_ref, "--", "sublime/", "rust/crates/"])
added: List[Tuple[Path, int, str]] = []
current_path: Optional[Path] = None
new_line_no = 0
for line in raw.splitlines():
if line.startswith("+++ b/"):
current_path = Path(line[len("+++ b/") :])
continue
if line.startswith("@@"):
m = re.search(r"\+(\d+)", line)
new_line_no = int(m.group(1)) - 1 if m else 0
continue
if line.startswith("+") and not line.startswith("+++") and current_path:
new_line_no += 1
added.append((current_path, new_line_no, line[1:]))
elif not line.startswith("-") and current_path:
new_line_no += 1
return added
# ---------------------------------------------------------------------------
# Lint 실행
# ---------------------------------------------------------------------------
class Violation:
__slots__ = ("lint_id", "path", "line_no", "content", "reason")
def __init__(
self,
lint_id: str,
path: Path,
line_no: int,
content: str,
reason: str,
) -> None:
self.lint_id = lint_id
self.path = path
self.line_no = line_no
self.content = content
self.reason = reason
def __str__(self) -> str:
return (
f"[{self.lint_id}] {self.path}:{self.line_no}: {self.reason}\n"
f" {self.content.strip()}"
)
def _check_lint_1(added: Iterable[Tuple[Path, int, str]]) -> List[Violation]:
violations: List[Violation] = []
for path, line_no, content in added:
rel = str(path).replace("\\", "/")
if not LINT_1_PATH_PATTERN.match(rel):
continue
if LINT_1_EXEMPT_PATH_PATTERN.match(rel):
continue
if LINT_1_PARSER_SIGNATURES.match(content):
violations.append(
Violation(
lint_id="#1",
path=path,
line_no=line_no,
content=content,
reason=(
"helper response parser 시그니처 신규 금지 — "
"Rust ABI 호출 + typed wrapper 1단계만 허용"
),
)
)
return violations
def _check_lint_2_5(added: Iterable[Tuple[Path, int, str]]) -> List[Violation]:
violations: List[Violation] = []
for path, line_no, content in added:
rel = str(path).replace("\\", "/")
if not LINT_2_5_PATH_PATTERN.match(rel):
continue
for pattern in LINT_2_5_RETRY_PATTERNS:
if pattern.search(content):
violations.append(
Violation(
lint_id="#2.5",
path=path,
line_no=line_no,
content=content,
reason=(
"Track H2 분리 모듈에서 retry/timeout 원시 직접 사용 금지 "
"— _rust_ffi/bridge 호출 표면에 응집"
),
)
)
break
return violations
def _check_lint_4(added: Iterable[Tuple[Path, int, str]]) -> List[Violation]:
violations: List[Violation] = []
for path, line_no, content in added:
rel = str(path).replace("\\", "/")
if not LINT_4_PATH_PATTERN.match(rel):
continue
if LINT_4_NATURAL_LANGUAGE.search(content):
violations.append(
Violation(
lint_id="#4",
path=path,
line_no=line_no,
content=content,
reason=(
"Rust ABI에 영문 자연어 문장 금지 — "
"식별자 코드(int, kebab-case)만 반환"
),
)
)
return violations
def _check_lint_6_pr_body(pr_body_path: Path) -> List[Violation]:
if not pr_body_path.exists():
return [
Violation(
lint_id="#6",
path=pr_body_path,
line_no=0,
content="",
reason=f"PR description 파일 없음: {pr_body_path}",
)
]
body = pr_body_path.read_text(encoding="utf-8")
if not LINT_6_BOUNDARY_CLAIM.search(body):
return [
Violation(
lint_id="#6",
path=pr_body_path,
line_no=0,
content="(PR description)",
reason=(
"PR description에 boundary-claim 블록이 필요함:\n"
" boundary-claim:\n"
" removes: <list of file:line ranges>\n"
" delete-count: <int>\n"
" ban-list: <activated lints, optional>\n"
),
)
]
return []
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
ALL_LINTS = ("1", "2.5", "4", "6")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--base-ref",
default=None,
help="diff base; CI에서는 자동으로 origin/main과의 merge-base 사용",
)
parser.add_argument(
"--lint",
action="append",
default=None,
choices=ALL_LINTS,
help="실행할 룰 (반복 가능, 기본은 활성 룰 전체)",
)
parser.add_argument(
"--pr-body",
type=Path,
default=None,
help="Lint #6: PR description 파일 경로",
)
parser.add_argument(
"--all-files",
action="store_true",
help="diff 대신 전체 파일 검사 (PR 0 sanity 용도, grandfather 없음)",
)
args = parser.parse_args()
selected = set(args.lint) if args.lint else set(ALL_LINTS)
violations: List[Violation] = []
if {"1", "2.5", "4"} & selected:
base_ref = None if args.all_files else _resolve_base_ref(args.base_ref)
added = _added_lines(base_ref)
if "1" in selected:
violations.extend(_check_lint_1(added))
if "2.5" in selected:
violations.extend(_check_lint_2_5(added))
if "4" in selected:
violations.extend(_check_lint_4(added))
if "6" in selected:
pr_body = args.pr_body
if pr_body is None:
env_path = os.environ.get("LINT_THINNING_PR_BODY")
if env_path:
pr_body = Path(env_path)
if pr_body is not None:
violations.extend(_check_lint_6_pr_body(pr_body))
if violations:
print("Boundary lint (Wave 1.5) — 위반 발견:", file=sys.stderr)
for v in violations:
print(str(v), file=sys.stderr)
print(
f"\n{len(violations)}건 위반. "
"boundary 문서: planning/PYTHON_RUST_BOUNDARY.md",
file=sys.stderr,
)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

2
uv.lock generated
View File

@@ -854,7 +854,7 @@ wheels = [
[[package]]
name = "sessions-sublime"
version = "0.7.24"
version = "0.7.25"
source = { virtual = "." }
[package.dev-dependencies]