--- 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//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을 같이 바꾼다.