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:
67
.gitea/workflows/boundary-lint.yml
Normal file
67
.gitea/workflows/boundary-lint.yml
Normal 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
|
||||
@@ -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 225–333) 삭제. 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 0–12 |
|
||||
| **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 3–7 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 225–333) | 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 3–7 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:1–17` — `#![allow(dead_code)]` + "S2.3–S2.5 not wired" docstring; broker는 production wired 상태이므로 stale. PR 0 또는 가장 빠른 후속 PR에서 청산.
|
||||
|
||||
293
planning/PYTHON_THINNING_PLAN.md
Normal file
293
planning/PYTHON_THINNING_PLAN.md
Normal 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 23–27](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 17–19](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 8–15](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 1–6에서 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 100–112](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 §17–19 위반 청산)가 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 5–6 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:1–17` stale `#![allow(dead_code)]` + "S2.3–S2.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 §17–19 완전 청산.
|
||||
|
||||
#### **PR 3–7 — `_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:225–333` (~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 12–14 영향) ≈ **5500–6000 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 3–7)이 이미 패턴 시범.
|
||||
- `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 1–14 거버넌스 추적 가능. |
|
||||
| 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 1–16 진행에는 영향 없음. |
|
||||
|
||||
## 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개 섹션).
|
||||
183
planning/boundary_inventory.yml
Normal file
183
planning/boundary_inventory.yml
Normal 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 3–7): 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 225–333 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
110
scripts/duplication_deadline.py
Executable 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
355
scripts/lint_python_thinning.py
Executable 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())
|
||||
Reference in New Issue
Block a user