- CLAUDE.md: product principles, crate layering, common commands, commits. - .claude/rules: common, Rust, frontend, Tauri IPC, docs and security rules. - .claude/skills: check, add-tauri-command, project-model-change, change-record, msix-integration-test, backlog-status. Refs #16 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
61 lines
4.8 KiB
Markdown
61 lines
4.8 KiB
Markdown
---
|
|
paths:
|
|
- "src/crates/**/*.rs"
|
|
- "src/apps/cli/**/*.rs"
|
|
- "src/apps/desktop/src-tauri/**/*.rs"
|
|
- "src/Cargo.toml"
|
|
- "src/**/Cargo.toml"
|
|
---
|
|
|
|
# Rust 규칙
|
|
|
|
도구 체인은 `rust-toolchain.toml`의 1.94.0 고정. `cargo fmt` 기본 설정과 `clippy -D warnings`를 통과해야 한다. 별도 `rustfmt.toml`/`clippy.toml`을 추가하지 않는다.
|
|
|
|
## 크레이트 경계
|
|
|
|
| 크레이트 | 허용 | 금지 |
|
|
| --- | --- | --- |
|
|
| `package-domain` | serde DTO, 순수 검증, manifest 문자열 생성 | `std::fs`, `std::process`, `anyhow`, OS API |
|
|
| `package-engine` | 계약 타입, `PackageBackend` 트레이트, 계획 생성, 해시 유틸 | 파일시스템 직접 접근(트레이트로 위임) |
|
|
| `package-infra` | 파일·프로세스·잠금·레지스트리·서명·저장 | Tauri·clap 타입 |
|
|
| `apps/cli`, `src-tauri` | 인자 파싱, 세션 상태, 오류 → 문자열 변환 | 비즈니스 로직, 해시·검증 재구현 |
|
|
|
|
새 외부 crate는 `[workspace.dependencies]`에 먼저 추가하고 멤버에서 `.workspace = true`로 참조한다. Windows 전용 API는 `windows-sys`의 feature를 명시적으로 추가하고 `#[cfg(windows)]`로 감싼다. 비Windows 빌드가 컴파일되게 유지한다.
|
|
|
|
## DTO
|
|
|
|
- 모든 직렬화 타입: `#[derive(Debug, Clone, Serialize, Deserialize)]` + `#[serde(rename_all = "camelCase")]`. 사용자 파일에서 읽는 타입은 `deny_unknown_fields`를 반드시 붙인다. 외부로 내보내기만 하는 타입(`Validation`, `BuildResult`, `JobRecord`)은 생략 가능.
|
|
- `package-domain`의 단순 구조체는 `model!` 매크로를 쓴다. 기본값·enum·문서 주석이 필요하면 매크로 없이 직접 선언한다.
|
|
- 기존 v2 파일을 깨지 않고 필드를 추가할 때만 `#[serde(default)]`를 쓴다. 필드 제거는 자동 변환 없이 알 수 없는 필드 오류로 거부한다(`projectId` 선례).
|
|
- JSON Schema(`schemars::JsonSchema`)는 `Project` 트리에 포함되는 모든 타입에 derive한다. 변경 후 `schemas/package-project.schema.json`을 재생성한다(`project-model-change` 스킬).
|
|
- `u64` 바이트 수를 IPC로 보내면 안 된다. `ResolvedFile.bytes`처럼 `String`으로.
|
|
|
|
## 오류
|
|
|
|
- 라이브러리 크레이트는 `anyhow::Result`. `ensure!`, `bail!`, `.context()`/`.with_context()`로 `ErrorCode: 설명` 형식을 만든다.
|
|
- 경로를 메시지에 넣을 때는 `path.display()`. 민감한 값(지문 전체, 토큰)은 넣지 않는다.
|
|
- `unwrap()`은 테스트와 불변 조건이 보장된 곳(`path.parent()` 직후의 `.unwrap()`처럼 바로 위에서 확인한 경우)에서만. 그 외는 `.context("MissingParent")?`.
|
|
- `let _ = child.kill();`처럼 정리 경로에서 결과를 무시할 때만 `let _ =`를 쓴다.
|
|
|
|
## 파일·프로세스 안전
|
|
|
|
- 쓰기는 항상 `atomic_json`/`atomic_bytes` 경유: 같은 디렉터리 임시 파일 → `sync_all` → `persist`. `fs::write`로 결과 파일을 직접 쓰지 않는다(스테이징 폴더 내부 임시 파일은 예외).
|
|
- 사용자 경로를 열기 전 `reject_links`. 프로젝트 폴더를 바꾸는 작업은 `lock_project`를 먼저 잡고 `_lock` 바인딩을 함수 끝까지 유지한다.
|
|
- 외부 도구 실행은 `process::run`만 사용한다. 허용 도구는 절대 경로의 `makeappx.exe`, `signtool.exe`, PowerShell 서명 스크립트. 인자는 `&[&OsStr]` 배열. 셸 문자열 결합 금지. 로그 파일 경로를 반드시 받는다.
|
|
- 긴 루프(파일 복사, 해시, 역추출 검사)는 반복마다 `cancelled(cancel)?`를 호출한다.
|
|
- 해시 비교는 계획에 고정된 값과 **실행 전 소스**, **복사 중 스트림**, **역추출 결과** 세 지점에서 한다. 하나를 생략하지 않는다.
|
|
|
|
## 테스트
|
|
|
|
- 통합 테스트는 `crates/<name>/tests/*.rs`. `tempfile::tempdir()`로 격리하고 `_temp` 바인딩을 유지한다. `fixture()` 헬퍼를 재사용한다.
|
|
- MakeAppx/SignTool이 필요한 테스트는 `#[ignore]` + `std::env::var("MSIX_TEST_MAKEAPPX")`.
|
|
- 거부 테스트는 오류 문자열의 `ErrorCode`를 `.contains("UnsupportedProjectExtension")`처럼 확인한다. 메시지 전문을 비교하지 않는다.
|
|
- 시험 PC의 실제 레지스트리·인증서 저장소·`%APPDATA%`를 바꾸는 테스트를 만들지 않는다. `RecentStore`처럼 경로를 주입받는 구조로 만든다.
|
|
|
|
## 스타일
|
|
|
|
- `use` 정렬은 rustfmt에 맡긴다. `std::{fs, path::{Path, PathBuf}}` 묶음 형식을 따른다.
|
|
- 작은 private 헬퍼(`error`, `idle`, `document`)는 파일 상단에 모은다.
|
|
- 매직 넘버는 이름을 붙인다: `MAX_RECENT`, 타임아웃 `Duration::from_secs(900)`은 상수로 올릴 때 README의 "15분 제한"과 함께 바꾼다.
|
|
- 문자열 상태(`"ArtifactVerified"`, `"Interrupted"`, `"notRun"`)는 JSON 계약이다. 바꾸면 TS 타입, README, 검증 JSON을 같이 바꾼다.
|