chore: add Claude Code guidelines, rules and skills

- 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>
This commit is contained in:
jychoi4andClaude Opus 5.5 committed 2026-11-11 11:09:05 +09:00
1 parent 7f5540f148
commit 905e6d4eaa
13 files changed
+654

No files matched your search

+47
View File
@@ -0,0 +1,47 @@
# 공통 클린코드 규칙
모든 파일에 적용한다. 언어별 규칙은 `rust.md`, `frontend.md`, `tauri-ipc.md`, `docs.md`가 보충한다.
## 범위와 변경 크기
- 요청한 범위만 바꾼다. 지나가는 코드의 스타일 정리·리네임·재배치를 섞지 않는다.
- 새 기능보다 기존 함수 재사용을 먼저 찾는다. `package-infra`에 이미 있는 `hash_file`, `atomic_json`, `reject_links`, `lock_project`, `appended_path` 같은 헬퍼를 다시 만들지 않는다.
- 한 함수는 한 가지 일을 한다. 분기가 3단계를 넘으면 이름 있는 함수로 뺀다.
- 파일 하나가 500줄을 넘기 시작하면 모듈 분리를 제안한다(현재 `main.ts`, `store.rs`가 경계선).
## 이름
- 코드 식별자는 영어. 사용자에게 보이는 문자열(오류 메시지, UI 텍스트, 문서)은 한국어.
- 불리언은 상태를 그대로 읽히게 짓는다: `dirty`, `busy`, `available`, `trusted_people`. `is_`/`has_` 접두사는 Rust 메서드에서만.
- 약어를 새로 만들지 않는다. `doc`(ProjectDocument), `esc`(HTML escape)처럼 이미 쓰는 것만 유지한다.
- JSON 필드는 camelCase, Rust 필드는 snake_case, 변환은 serde `rename_all`에 맡기고 수동 매핑을 하지 않는다.
## 오류 처리
- 오류 메시지 형식: `ErrorCode: 한국어 설명과 조치`. 코드는 PascalCase 한 단어(`RevisionConflict`, `MissingTool`, `InputChanged`). 기존 코드가 있으면 재사용하고, 새 코드는 README 또는 변경 기록에 적는다.
- 오류를 삼키지 않는다. 실패를 기본값으로 대체하는 fallback(미서명으로 자동 전환, 빈 설정으로 조용히 진행 등)을 만들지 않는다. 예외는 명시된 두 곳만: 최근 목록 저장 실패는 경고로 표시하고 프로젝트 열기를 막지 않는다, 이전 v2 프로젝트의 누락 `registry`/`signing` 필드는 기본값으로 연다.
- 조치 방법을 담는다. 사용자가 무엇을 고쳐야 하는지 한 문장으로.
## 결정성
- 파일 목록·맵은 정렬된 자료구조(`BTreeMap`, `BTreeSet`, 정렬된 `Vec`)로 다룬다. 같은 입력은 같은 출력을 내야 한다.
- 시각은 UNIX 초(u64)로 저장한다. 표시 형식 변환은 UI에서만.
- 바이트 수처럼 JS `number` 정밀도를 넘을 수 있는 값은 IPC 경계에서 10진 문자열로 보낸다.
## 주석과 문서화
- 주석은 "왜"만 쓴다. 코드가 말하는 "무엇"을 반복하지 않는다. 주석은 영어 한 문장.
- 공개 함수에 문서 주석을 달 때는 계약(입력 제약, 실패 조건, 부작용 없음)을 쓴다.
- TODO를 남기지 않는다. 미구현은 명시 오류로 거부하고 README의 "다음 작업"이나 백로그에 적는다.
## 테스트
- 동작을 바꾸면 테스트를 함께 바꾼다. 실패 경로(거부, 충돌, 취소, 변조 탐지)를 성공 경로와 같은 비중으로 검사한다.
- 테스트 이름은 검증하는 규칙을 문장으로 쓴다: `json_and_duplicate_extensions_are_rejected_even_with_valid_contents`.
- 실제 SDK·네트워크·레지스트리·인증서 저장소를 건드리는 테스트는 `#[ignore]` 또는 환경 변수 게이트 뒤에 둔다.
- 테스트가 실패하면 그대로 보고한다. 테스트를 약화시켜 통과시키지 않는다.
## 줄 끝과 인코딩
- UTF-8. `.gitattributes`에 따라 `*.md`, `*.json`, `*.html`은 LF. Rust/TS는 `text=auto`를 따른다.
- 파일 끝에 개행 하나. BOM 없음(현재 `style.css`의 BOM은 기존 상태이며 건드리지 않는다).
+47
View File
@@ -0,0 +1,47 @@
---
paths:
- "docs/**/*.md"
- "README.md"
- "src/README.md"
- "src/tests/*.json"
---
# 문서 · 기록 규칙
문서는 한국어, 선언형 종결("~한다", "~이다"). 날짜는 `2026-10-23` 형식. 영어 식별자·명령·파일명은 백틱으로 감싼다.
## 어떤 변경이 어떤 문서를 바꾸는가
| 변경 | 반드시 갱신 |
| --- | --- |
| 사용자 동작·CLI 옵션·종료 코드·오류 코드 | `src/README.md` 해당 절 |
| `.isproj`/local JSON 필드 | `docs/code-design/13-isproj-project-format.md`, `docs/code-design/examples/msix.isproj`, `src/fixtures/InstallerProject.isproj` |
| 기능 추가·정책 결정 | `docs/changes/YYYY-MM-DD-<slug>.md` 신규 작성, 루트 `README.md` 링크 |
| 실제 도구를 돌린 시험 | `src/tests/verification-YYYY-MM-DD-<slug>.json` |
| 백로그 항목 진척 | `docs/backlog/README.md` 상태 열 |
| 범위 변경(1차/후속 이동) | `docs/code-design/12-msix-first-builder.md`, 백로그, `docs/plan/10-development-schedule.md` |
## 변경 기록(`docs/changes`) 구성
1. 제목과 첫 단락: 날짜, 무엇을 왜 바꿨는지, 범위 밖임을 명시할 것.
2. `## 사용` 또는 `## 결정`: 사용자 절차 또는 결정 목록.
3. `## 모델과 실행`: 프로젝트/로컬/계획/결과 필드와 실행 순서.
4. `## 확인 결과`: 실행한 검사와 **실행하지 않은 검사**를 모두 나열. 테스트 개수, 해시, 상태 문자열을 적는다.
5. `## 근거`: Microsoft Learn 등 외부 문서 링크.
## 검증 JSON(`src/tests/verification-*.json`)
- `date`, `checks`(불리언 맵), 실제 결과 객체(`buildId`, `sha256`, `status`, `signingReport` 등), `installationTest`, `notRun` 목록을 포함한다.
- 실행하지 않은 항목은 생략하지 않고 `"notRun"`으로 적는다. GUI 클릭 시험을 하지 않았으면 `"guiClickTest": "notRun"`.
- 로컬 경로가 들어가므로 공유 전에 검토한다. 토큰·개인 키·PFX 비밀번호는 절대 넣지 않는다.
## 금지 표현
- 실행하지 않은 것을 "확인했다"로 쓰지 않는다. 시안·설계 작성은 구현 완료 증빙이 아니다.
- "배포 준비 완료", "설치 검증 완료", "지원 인증"은 Windows VM 설치·업데이트·제거 증빙이 있을 때만.
- 백로그 ID를 완료로 표시하려면 해당 행의 완료 기준 전체가 충족돼야 한다. 일부 구현은 README의 "일부 구현" 문장으로 남긴다.
## 링크
- 저장소 내부 링크는 상대 경로. 깨진 링크를 만들지 않도록 대상 파일 존재를 확인한다.
- 루트 `README.md`는 색인 역할이다. 새 설계·매뉴얼·변경 문서를 만들면 한 줄로 추가한다.
+48
View File
@@ -0,0 +1,48 @@
---
paths:
- "src/apps/desktop/src/**"
- "src/apps/desktop/tests/**"
- "src/apps/desktop/index.html"
- "src/apps/desktop/package.json"
- "src/apps/desktop/tsconfig.json"
---
# 프런트엔드(TypeScript · CSS) 규칙
프레임워크 없는 TypeScript. 화면은 문자열 템플릿으로 만들고 `root.innerHTML`에 넣은 뒤 `data-action`/`data-field` 속성으로 이벤트를 연결한다. `tsc --noEmit` strict와 `node --test`를 통과해야 한다.
## 모듈 구조
- `main.ts`는 상태와 조정(invoke 호출, render, action 분기)만 둔다. 화면 조각과 순수 로직은 별도 모듈로 뺀다: `start-screen.ts`, `registry-editor.ts`, `signing-editor.ts`, `project-transition.ts`, `window-chrome.ts`.
- 새 탭·편집기는 `render<Name>(data, disabled): string`을 export하는 모듈로 만들고, `main.ts`에서는 import와 action 분기만 추가한다.
- 테스트는 `tests/<module>.test.mjs`에서 `.ts`를 직접 import한다. Node의 `--experimental-strip-types`로 실행되므로 **`enum`, `namespace`, 매개변수 프로퍼티, 데코레이터를 쓰지 않는다**. 타입은 `type` 별칭과 `import type`으로.
- 순수 함수(그룹화, 경로 표시, 전환 판단)는 DOM 없이 호출 가능해야 테스트할 수 있다. `document`를 쓰는 코드와 분리한다.
## 보안 · 렌더링
- 템플릿에 들어가는 모든 동적 값은 `esc()`/`escape()`를 거친다. 프로젝트 이름, 경로, 오류 메시지, ID도 예외 없다. 테스트는 `<img src=x onerror=...>` 같은 값이 이스케이프되는지 확인한다.
- `tauri.conf.json`의 CSP는 `'self'`만 허용한다. 인라인 `<script>`, 인라인 `style=`, 외부 폰트·CDN을 추가하지 않는다.
- `innerHTML`을 쓰는 곳은 render 함수 안에서만. 이벤트 핸들러는 `addEventListener`로 붙이고 `onclick=` 속성을 쓰지 않는다.
- `any`를 새로 쓰지 않는다. 경로 기반 필드 갱신이 필요하면 `Record<string, unknown>`과 단계별 검사로.
## 상태와 IPC
- `invoke<T>('command', args)`의 `T`는 Rust 반환 DTO와 필드 단위로 일치하는 `type`이어야 한다. Rust 쪽 `#[serde(default)] Option<T>`는 TS에서 `field?: T | null`.
- `isTauri()`가 false인 미리보기 모드에서는 파일 작업을 시도하지 않고 안내 문구를 보여준다.
- 모든 action은 `busy` 플래그로 중복 실행을 막고, `cancel`만 `busy` 중에 허용한다.
- 저장 안 된 변경(`dirty`)이 있을 때 프로젝트를 바꾸는 모든 경로는 `allowProjectTransition`을 거친다. 저장 실패 시 전환하지 않는다.
- 변경 후에는 `invalidate()`로 `report`/`plan`을 무효화한다. 오래된 검증 결과를 화면에 남기지 않는다.
## 접근성 · 문구
- 버튼은 텍스트 또는 `aria-label`을 가진다. 아이콘 전용 버튼(`×`)에는 `aria-label`과 `title`.
- 상태 메시지 영역은 `role="status"` 또는 `aria-live="polite"`.
- 사용자 문구는 한국어, 조치 중심: "변경 사항을 저장한 뒤 다시 검사하세요." 기술 코드는 문장 뒤에 덧붙인다.
- "배포 가능", "설치 완료", "검증됨" 같은 단어는 실제 상태 필드(`deploymentReady`, `installationTest`)가 그렇게 말할 때만 쓴다.
## CSS
- 색상은 `theme.css`의 `var(--color-*)`만 사용한다. 새 색이 필요하면 토큰을 먼저 추가한다. hex 하드코딩 금지(`.primary`의 `#fff`는 기존 예외).
- 선택자는 클래스 기반. ID 선택자와 `!important` 금지.
- 반응형 분기는 기존 `1200px`, `1050px` 중단점을 재사용한다.
- `style.css`는 한 줄 압축 형식이 섞여 있다. 새 규칙은 파일 끝에 사람이 읽을 수 있는 형식으로 추가하고, 기존 압축 블록을 재포맷하지 않는다.
+60
View File
@@ -0,0 +1,60 @@
---
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을 같이 바꾼다.
+38
View File
@@ -0,0 +1,38 @@
# 보안 · 비밀정보 규칙
모든 파일에 적용한다.
## 커밋 금지 목록
`.gitignore`에 있는 항목을 예외 없이 지킨다: `.mcp.json`(Gitea 토큰 포함), `.env*`, `*.pfx`, `*.p12`, `*.key`, `*.isproj.local.json`, `*.bak`, `output/samples/`, `node_modules/`, `target/`. 이 파일들의 내용을 문서·주석·테스트·로그에 복사하지도 않는다.
`src/.work/`는 로컬 작업 폴더다(미추적). 그 안의 스크립트·토큰·커밋 메타데이터를 소스나 문서에 인용하지 않는다.
## 시스템 변경 금지
제작기와 테스트는 다음을 **절대 자동 수행하지 않는다**. 필요한 경우 사용자가 직접 실행할 스크립트와 안내를 출력한다.
- 빌드 PC의 실제 레지스트리 수정(`offreg.dll`로 오프라인 hive만 생성)
- 인증서 신뢰 저장소 등록(`LocalMachine\TrustedPeople`), 인증서 개인 키 내보내기
- MSIX 설치·제거, Windows 개발자 모드 변경
- Windows SDK·인증서·런타임 자동 설치
- 사용자 프로젝트 파일의 자동 변환·일괄 수정
## 도구 실행
- 허용 도구는 절대 경로의 `makeappx.exe`, `signtool.exe`, 저장소 내 PowerShell 서명 스크립트. 파일 이름을 대소문자 무시로 검사하고 실행 전후 SHA-256을 계획 값과 비교한다.
- 인자는 배열로 전달한다. 셸(`cmd /c`, `powershell -Command "<문자열 결합>"`)로 사용자 입력을 넘기지 않는다.
- 자식 프로세스는 Job Object에 붙여 취소·타임아웃 시 함께 종료한다.
## 경로
- 사용자 입력 경로는 `safe_relative`로 검사한다: `..`, 절대 경로, 예약 장치 이름(`CON`, `COM1`), 제어 문자, `\:<>"|?*` 거부.
- reparse point(심볼릭 링크·junction)는 `reject_links`로 읽기·쓰기 모두 거부한다.
- 소스 폴더가 `.work`, `releases`를 포함하거나 그 아래면 거부한다(출력 재수집 방지).
- 작업 로그와 검증 JSON에는 로컬 절대 경로가 남는다. 외부 공유 전에 사용자에게 알린다.
## 프런트엔드
- CSP `default-src 'self'` 유지. 외부 리소스 로드 금지.
- 모든 동적 HTML은 이스케이프. 이벤트는 `addEventListener`.
- Tauri 명령은 ID·선택지만 받고 경로를 받지 않는다. 새 명령마다 `capabilities/main.json`에 개별 허용을 추가하고 와일드카드 권한을 쓰지 않는다.
+35
View File
@@ -0,0 +1,35 @@
---
paths:
- "src/apps/desktop/src-tauri/**"
- "src/apps/desktop/src/main.ts"
---
# Tauri IPC · 권한 규칙
데스크톱은 셸·임의 경로 파일시스템 플러그인을 쓰지 않는다. 모든 파일 접근은 Rust 명령이 열어 둔 현재 프로젝트 세션과 `rfd` 파일 대화상자를 통해서만 한다.
## 명령 작성
- 시그니처는 `Result<T, String>`. 내부 오류는 `error()` 헬퍼로 문자열화하고, 사용자가 다음에 할 일을 앞에 붙인다: `format!("최근 프로젝트를 열 수 없습니다. 파일 위치와 접근 권한을 확인하세요: {e}")`.
- 세션을 바꾸는 명령(열기·새로 만들기·저장·닫기·소스/도구 선택)은 첫 줄에서 `idle(&session)?`로 빌드 중 변경을 막는다.
- 파일 IO·해시·도구 실행은 `async fn` + `tauri::async_runtime::spawn_blocking`으로 UI 스레드를 막지 않는다. 짧은 메모리 작업만 동기 `fn`.
- `Mutex` 잠금은 함수 범위를 넘기지 않는다. `document()`로 복제해 쓰고 `set_document()`로 돌려놓는다.
- 빌드는 `session.busy`를 잡고 종료 시 반드시 풀어야 한다. 취소는 `session.cancel`을 세팅하고 자식 프로세스 종료를 기다린다.
- 프런트엔드가 보낸 경로를 그대로 믿지 않는다. 명령 인자로는 ID(`plan_id`, `thumbprint`, `target`)나 선택지만 받고, 실제 경로는 세션 문서에서 가져온다.
## 권한 체크리스트 (명령 하나 추가 시)
1. `main.rs`에 `#[tauri::command] fn <snake_name>` 추가.
2. `tauri::generate_handler![...]`에 등록.
3. `cargo build -p msix-studio`가 `permissions/autogenerated/<snake_name>.toml`을 생성한다. 이 파일은 직접 편집하지 않는다.
4. `capabilities/main.json`의 `permissions`에 `"allow-<kebab-name>"` 추가. 이 단계를 빠뜨리면 런타임에 "not allowed" 오류가 난다.
5. `main.ts`에 반환 타입을 선언하고 `invoke<T>('<snake_name>', { ... })` 호출.
6. `src/README.md`의 기능 설명 갱신.
`core:*` 권한은 창 제어(`window-chrome.ts`)에 필요한 항목만 추가한다. `core:default`나 플러그인 전체 허용을 넣지 않는다.
## 설정
- `tauri.conf.json`의 `bundle.active`는 false를 유지한다. 제작기 자체 배포는 범위 밖이다.
- `decorations: false`이므로 창 제어는 `window-chrome.ts`에 모은다. 새 창 권한은 거기서만 쓴다.
- 앱 데이터 경로(`%APPDATA%\dev.networksetup.msixstudio`)에는 최근 목록만 저장한다. 프로젝트 내용·인증서·토큰을 저장하지 않는다.
+46
View File
@@ -0,0 +1,46 @@
---
name: add-tauri-command
description: Installer Studio 데스크톱에 Tauri IPC 명령을 추가하거나 바꾼다. Rust 명령 작성, generate_handler 등록, 자동 생성 권한, capabilities/main.json 허용, TypeScript invoke 타입, README 갱신까지 한 번에 끝낸다. "명령 추가", "IPC", "프런트에서 호출", "capability", "not allowed 오류"에 사용한다.
---
# Tauri 명령 추가
파일 위치: `src/apps/desktop/src-tauri/src/main.rs`, `src/apps/desktop/src-tauri/capabilities/main.json`, `src/apps/desktop/src/main.ts`.
## 설계 먼저 결정
- 비즈니스 로직은 `package-infra`에 함수로 두고 명령은 그 함수를 호출만 한다. CLI(`apps/cli/src/main.rs`)에서도 같은 함수를 쓸 수 있어야 한다.
- 인자로 받을 것은 ID·선택지·사용자 입력값이다. 파일 경로를 프런트엔드에서 받지 않는다. 경로가 필요하면 `rfd` 대화상자 또는 세션 문서에서 얻는다.
- 세션을 바꾸는가? → `idle(&session)?`를 첫 줄에.
- 파일 IO·해시·외부 도구를 실행하는가? → `async fn` + `spawn_blocking`.
- 반환 DTO가 새 타입이면 `#[derive(serde::Serialize)]` + `#[serde(rename_all = "camelCase")]`. 바이트 수는 문자열.
## 절차
1. **Rust 명령**
```rust
#[tauri::command]
async fn example_action(id: String, session: State<'_, Session>) -> Result<ExampleView, String> {
idle(&session)?; // 세션을 바꾸는 경우만
let doc = document(&session)?;
tauri::async_runtime::spawn_blocking(move || infra::example(&doc, &id).map_err(error))
.await
.map_err(error)?
}
```
2. `main.rs` 하단 `tauri::generate_handler![ ... ]` 목록에 `example_action` 추가.
3. `cargo build --locked -p msix-studio --features custom-protocol` 실행. `permissions/autogenerated/example_action.toml`이 생긴다. 이 파일은 수정하지 않는다.
4. `capabilities/main.json`의 `permissions` 배열에 `"allow-example-action"` 추가(snake → kebab). 기존 순서를 따라 명령 그룹 근처에 넣는다.
5. **TypeScript**: `main.ts`(또는 담당 모듈)에 반환 `type` 선언 후 `await invoke<ExampleView>('example_action', { id })`. 호출은 `action()` 분기 안에서 `busy` 보호를 받게 한다.
6. 순수 로직이 생겼으면 별도 모듈로 빼고 `tests/<module>.test.mjs`에 테스트 추가.
7. `src/README.md`에서 해당 화면·CLI 절을 갱신한다. 새 오류 코드가 있으면 적는다.
8. `check` 스킬로 전체 검사.
## 자주 나는 오류
- 런타임 `example_action not allowed by ACL`: 4단계 누락.
- `permissions/autogenerated`에 파일이 없음: 2단계 누락 또는 빌드 미실행.
- TS에서 `undefined` 필드: Rust `snake_case` ↔ TS `camelCase` 불일치. `rename_all` 확인.
- 빌드 중 명령이 실행됨: 1단계 `idle` 누락.
+37
View File
@@ -0,0 +1,37 @@
---
name: backlog-status
description: docs/backlog/README.md의 BL-xx 항목 상태(미착수·진행 중·검증 중·완료·차단)를 운영 규칙에 맞게 갱신하고 변경 기록·검증 증빙과 연결한다. 완료 기준이 모두 충족되지 않은 항목을 완료로 표시하지 않는다. "백로그 갱신", "BL-", "상태 변경", "마일스톤", "완료 처리"에 사용한다.
---
# 백로그 상태 갱신
백로그는 `docs/backlog/README.md` 하나로 관리하며 Gitea 이슈를 만들지 않는다. 53개 ID는 유지하고 번호를 재사용·삭제하지 않는다.
## 상태 규칙 (운영 규칙 절 그대로)
| 상태 | 조건 |
| --- | --- |
| 미착수 | 기본값 |
| 진행 중 | 코드·문서 작업 시작. 동시에 1개 항목만 |
| 검증 중 | 구현은 끝났고 완료 기준의 검사가 남음 |
| 완료 | 해당 행의 **완료 기준 전체**가 충족되고 증빙(변경 기록 + 검증 JSON)이 연결됨 |
| 차단 | 원인·필요 자료·해제 조건을 같은 행에 기록 |
"일부 구현"은 상태가 아니다. 일부만 됐으면 `진행 중` 또는 `검증 중`으로 두고 `src/README.md`의 "현재 구현 범위" 문장에서 어떤 부분이 됐는지 적는다(현재 BL-09/10/11/14/16/29/47/48/49/50이 이 상태).
## 절차
1. 대상 행을 읽고 **완료 기준** 열을 항목별로 나눈다.
2. 각 항목에 대해 증빙을 찾는다: 테스트 이름, 변경 기록 절, 검증 JSON의 check 키. 없는 항목은 미충족이다.
3. 상태를 결정한다. 하나라도 미충족이면 완료가 아니다. Windows VM 설치·업데이트·제거 증빙이 요구되는 항목(BL-06, BL-53, MS6)은 VM 기록 없이는 완료가 될 수 없다.
4. 행을 수정한다. 상태 뒤에 날짜와 링크를 괄호로 붙인다: `검증 중 (2026-10-23, [서명](../changes/2026-10-23-developer-signing.md))`.
5. 선행 조건이 바뀌었으면 의존하는 행의 선행 조건 열도 확인한다. 1차 항목이 후속 항목에 의존하게 만들지 않는다.
6. 마일스톤 영향이 있으면 `docs/plan/10-development-schedule.md`의 해당 MS 줄을 갱신한다. 날짜를 앞당기는 결정은 사용자 확인 사항이다.
7. `src/README.md` 끝의 백로그 요약 문장을 맞춘다.
## 금지
- 시안·설계 문서 작성을 구현 증빙으로 인정.
- 완료 기준 문구를 줄여서 완료로 만들기. 기준 변경은 별도 변경 기록과 사용자 승인이 필요하다.
- P0/P1 우선순위를 상태 갱신과 함께 조용히 바꾸기.
- 범위 이동(1차 ↔ 후속). 이는 `12-msix-first-builder.md`와 일정 문서를 함께 바꾸는 별도 결정이다.
+75
View File
@@ -0,0 +1,75 @@
---
name: change-record
description: 기능·정책 변경을 마친 뒤 docs/changes/YYYY-MM-DD-<slug>.md 변경 기록과 src/tests/verification-YYYY-MM-DD-<slug>.json 검증 기록을 저장소 관례대로 작성하고 README·백로그 링크를 연결한다. "변경 기록", "문서화해", "검증 기록", "changes 작성"에 사용한다.
---
# 변경 기록 작성
기존 예: `docs/changes/2026-10-23-developer-signing.md`, `src/tests/verification-2026-10-23-developer-signing.json`. 먼저 가장 최근 기록 하나를 읽고 같은 어조와 구조를 따른다.
## 수집할 사실
기록을 쓰기 전에 실제로 확인한다. 기억이나 추정으로 채우지 않는다.
- `cargo test --locked --workspace` 통과 개수, `npm.cmd test` 통과 개수.
- 실제 도구를 실행했다면 결과 JSON(`releases/<id>/release.json`, `signing-report.json`)의 `buildId`, `status`, `sha256`, `signing`, `installationTest`, `deploymentReady`.
- 실행하지 않은 것: GUI 클릭 시험, Windows VM 설치, 대용량 샘플 재생성 등. 이것도 기록 대상이다.
- 바뀐 오류 코드·CLI 옵션·종료 코드.
## 변경 기록 (`docs/changes/<date>-<slug>.md`)
```markdown
# <기능 이름>
<YYYY-MM-DD>. <무엇을 어디에 추가/변경했는지 두 문장>. <이 변경이 의미하지 않는 것 한 문장>.
## 사용 ← 사용자 절차가 있으면. 정책 변경이면 "## 결정"
1. ...
## 모델과 실행
- 프로젝트: `field = a | b`; 기존 파일 기본값.
- 로컬: ...
- 계획: 무엇을 고정하고 언제 재검사하는지.
- 빌드: 실행 순서.
- 결과: 상태 문자열, 보고서 파일.
- 범위 밖: 이번에 하지 않은 것.
## 확인 결과
- Rust 테스트 N개 통과. 프런트엔드 테스트 N개 통과; TypeScript/Vite 빌드 통과.
- <실제 도구 시험 결과: 상태, 해시 코드>
- <실행하지 않은 검사>를 수행하지 않았다.
증빙: [시험 결과 JSON](../../src/tests/verification-<date>-<slug>.json)
## 근거
- [Microsoft Learn 등 외부 문서](URL)
```
## 검증 JSON (`src/tests/verification-<date>-<slug>.json`)
```json
{
"date": "YYYY-MM-DD",
"checks": { "<camelCaseCheckName>": true },
"builds": { "<label>": { "buildId": "", "status": "", "artifact": "", "sha256": "", "signing": "", "installationTest": "notRun", "deploymentReady": false } },
"tests": { "cargoTestWorkspace": "passed (N)", "npmTest": "passed (N)", "makeAppxIntegration": "notRun" },
"installationTest": "notRun",
"notRun": ["guiClickTest", "windowsVmInstall"]
}
```
- 실제 도구를 돌리지 않았으면 `builds`를 생략하고 `notRun`에 적는다.
- `checks` 값은 실제 확인한 것만 `true`. 확인하지 않은 항목은 넣지 않거나 `"notRun"`.
- 로컬 절대 경로가 포함되면 사용자에게 공유 전 검토를 안내한다. 토큰·개인 키·PFX 비밀번호 금지.
- LF 줄 끝, 2칸 들여쓰기, 끝 개행.
## 연결
1. 루트 `README.md`의 적절한 목록에 한 줄 추가(설계 문서면 "현재 설계와 시안", 그 외 "문서").
2. `src/README.md`에서 바뀐 사용자 절차·CLI·오류 코드 절을 갱신하고 변경 기록으로 링크.
3. 백로그 항목이 진척됐으면 `backlog-status` 스킬.
4. 관련 설계 문서(`docs/code-design/NN-*.md`)에 결정 날짜와 한 단락을 추가. 설계 문서를 다시 쓰지 않는다.
## 어조
선언형 종결. 수식어 없이 사실만. "성공적으로", "완벽히", "안전하게" 같은 평가 단어 금지. 실행하지 않은 것을 숨기지 않는다.
+56
View File
@@ -0,0 +1,56 @@
---
name: check
description: Installer Studio 저장소의 전체 검사(fmt, clippy, Rust 테스트, 프런트엔드 테스트·빌드, 데스크톱 빌드)를 scripts/Check.ps1과 같은 순서로 실행하고 결과를 보고한다. 커밋 전, PR 전, "검사해", "테스트 돌려", "빌드 확인"에 사용한다.
---
# 전체 검사
`src/scripts/Check.ps1`과 같은 순서를 따른다. 스크립트를 그대로 실행해도 되지만, 어느 단계에서 실패했는지 보고하려면 단계별로 실행한다. 모든 명령은 `D:\networkSetup\src`에서 실행한다.
## 사전 조건
1. 실행 중인 `vite`/`tauri dev`/`msix-studio.exe` 프로세스가 있으면 먼저 종료한다. `npm ci`가 esbuild 파일을 교체하지 못하고, debug 빌드가 실행 파일을 덮어쓰지 못한다.
2. 변경 범위를 보고 빠른 모드와 전체 모드를 정한다.
- Rust만 바뀜: 1~3단계 + 5단계.
- 프런트엔드만 바뀜: 4단계.
- `src-tauri`, 공용 DTO, IPC 계약이 바뀜: 전체.
## 단계
```powershell
# 1. 포맷
cargo fmt --all --check
# 2. 린트 (경고도 실패)
cargo clippy --locked --workspace --all-targets -- -D warnings
# 3. Rust 단위·통합 테스트 (SDK 통합 테스트는 ignored)
cargo test --locked --workspace
# 4. 프런트엔드
cd apps/desktop
npm.cmd ci --no-audit --no-fund
npm.cmd test
npm.cmd run build
cd ../..
# 5. 데스크톱 실행 파일
cargo build --locked -p msix-studio --features custom-protocol
```
MakeAppx 통합 시험까지 요청받았으면 `msix-integration-test` 스킬을 따른다.
## 실패 처리
- `cargo fmt --check` 실패: `cargo fmt --all`을 실행해 고치고 다시 검사한다. 포맷 변경만 커밋 범위에 들어가게 한다.
- clippy 경고: `#[allow]`로 덮지 않는다. 코드를 고친다. 정말 의도된 패턴이면 이유를 주석으로 적고 `allow`를 가장 좁은 범위에 붙인다.
- 테스트 실패: 테스트를 약화시키지 않는다. 실패 출력 전문을 보고에 포함한다.
- `--locked` 실패: `Cargo.lock`/`package-lock.json`이 변경돼야 한다는 뜻이다. 의존성을 의도적으로 바꾼 경우에만 잠금 파일 갱신을 커밋한다.
## 보고 형식
단계별로 통과/실패를 표로 적고, 실행하지 않은 단계는 "실행 안 함"으로 적는다. 실패한 단계는 오류 출력을 코드 블록으로 붙인다. "모두 통과"는 다섯 단계를 실제로 모두 실행했을 때만 쓴다.
| 단계 | 결과 |
| --- | --- |
| fmt | 통과 |
| clippy | 통과 |
| cargo test | 통과 (N개) |
| npm test / build | 통과 (N개) |
| msix-studio build | 실행 안 함 (프런트엔드만 변경) |
@@ -0,0 +1,67 @@
---
name: msix-integration-test
description: Windows SDK MakeAppx.exe를 찾아 MSIX_TEST_MAKEAPPX로 지정하고 package-infra의 ignored 통합 테스트(실제 pack/unpack, 계획 거부, 중복 빌드, 내보내기, 취소)를 실행한 뒤 결과를 해석·기록한다. 선택적으로 FixtureAlpha/Beta 시험 앱을 생성한다. "통합 테스트", "MakeAppx", "실제 빌드 시험", "SDK 테스트", "--ignored"에 사용한다.
---
# MakeAppx 통합 시험
`cargo test --workspace`는 SDK 테스트를 건너뛴다. 실제 MSIX 생성 경로는 이 절차로만 검증된다. 이 시험은 **설치 시험이 아니다**. 결과를 "설치 검증"이라고 쓰지 않는다.
## 1. MakeAppx 찾기
```powershell
Get-ChildItem 'C:\Program Files (x86)\Windows Kits\10\bin\*\x64\makeappx.exe' |
Sort-Object FullName -Descending | Select-Object -First 1 -ExpandProperty FullName
```
- 없으면 중단하고 사용자에게 Windows SDK 설치가 필요하다고 알린다. 자동 설치하지 않는다.
- 여러 버전이 있으면 가장 높은 버전을 쓰되, 사용한 경로와 SHA-256을 기록한다:
```powershell
(Get-FileHash -Algorithm SHA256 '<경로>').Hash.ToLower()
```
- 서명 테스트는 같은 폴더의 `signtool.exe`를 자동으로 쓴다. 없으면 서명 관련 테스트가 실패하므로 결과 해석 시 구분한다.
## 2. 실행
`D:\networkSetup\src`에서. 실행 중인 `msix-studio.exe`/`tauri dev`는 종료한다.
```powershell
$env:MSIX_TEST_MAKEAPPX = '<makeappx.exe 절대 경로>'
cargo test --locked -p package-infra --test workflow -- --ignored --nocapture
Remove-Item Env:MSIX_TEST_MAKEAPPX
```
또는 전체 검사와 함께: `.\scripts\Check.ps1 -MakeAppx '<경로>'`.
시험 앱이 필요하면(GUI에서 열어 볼 fixture):
```powershell
.\scripts\New-Fixtures.ps1 -MakeAppx '<경로>' # 기존 .work/demo 폴더는 덮어쓰지 않음
```
## 3. 결과 해석
통합 테스트가 확인하는 것: 두 identity의 pack/unpack, 원본 변경 후 계획 거부(`InputChanged`), 같은 계획 중복 실행 거부, 내보내기 해시 무결성, 취소 시 자식 프로세스 종료.
| 증상 | 원인 |
| --- | --- |
| `MissingTool` | 경로 오타 또는 환경 변수 미설정 |
| `UnsupportedTool` | 파일 이름이 `makeappx.exe`가 아님 |
| `ToolFailed ... pack.log` | manifest 또는 자산 문제. 로그 파일 경로를 열어 MakeAppx 메시지를 읽는다 |
| `ToolTimeout` | 15분 초과. 안티바이러스 실시간 검사 또는 디스크 문제 |
| `IdentityMismatch`, `ArtifactMismatch` | 역추출 결과가 계획과 다름. 엔진 버그 가능성, 재현 후 보고 |
| 서명 테스트만 실패 | `signtool.exe` 부재 또는 인증서 저장소 상태. 테스트 PC의 저장소를 바꾸지 않는다 |
## 4. 기록
- `change-record` 스킬의 검증 JSON에 `makeAppx: { path, sha256 }`, 통과한 테스트 이름, 실패/미실행 항목을 적는다.
- `installationTest`는 `"notRun"`, `deploymentReady`는 `false`.
- 생성된 샘플이 `output/samples/` 아래면 커밋되지 않는다. 다른 위치에 만들었으면 추적 여부를 확인한다.
## 금지
- `Install-TestCertificate.ps1` 실행, `Add-AppxPackage`, 인증서 저장소 변경.
- 실패한 테스트를 `#[ignore]`로 바꾸거나 단언을 약화.
- 결과를 "두 제품 MSIX 설치/업데이트/제거 검증 완료"(BL-53)로 기록.
@@ -0,0 +1,42 @@
---
name: project-model-change
description: .isproj(JSON v2) 프로젝트 모델이나 local 설정 모델을 바꿀 때 package-domain DTO, 검증, manifest, JSON Schema 파일, TypeScript 타입, fixture·예제 파일, 설계 문서 13번을 빠짐없이 동기화한다. "필드 추가", "프로젝트 형식", "schema", "DTO 변경", "local.json 필드"에 사용한다.
---
# 프로젝트 모델 변경
`Project`(`.isproj`)와 `LocalConfig`(`.isproj.local.json`)는 `src/crates/package-domain/src/lib.rs`에 정의되며 `deny_unknown_fields`로 엄격하게 읽는다. 호환 정책: **필드 추가는 `#[serde(default)]`로 기존 파일을 열 수 있게, 필드 제거는 자동 변환 없이 거부.** `schemaVersion`은 2를 유지한다.
## 먼저 판단할 것
- 이 값이 **공유 가능한 제품 설정**(→ `Project`)인가, **이 PC에만 있는 경로·인증서**(→ `LocalConfig`)인가. 절대 경로, 지문, SDK 위치는 local로.
- 비밀(개인 키, 비밀번호, 토큰)이면 어디에도 저장하지 않는다.
- 1차 MSIX 범위 밖(EXE, Store, bundle)이면 필드를 추가하지 않고 거부 오류를 유지한다.
## 동기화 체크리스트
1. **DTO** — `lib.rs`. 단순 구조체는 `model!`, 기본값이 필요하면 직접 선언 + `#[serde(default)]`. `JsonSchema` derive 유지.
2. **검증** — `package_domain::validate()`에 `check(valid, "Code", "field.path", "메시지")` 추가. 오류 코드는 PascalCase 한 단어. 의미 검증(경로 안전성·버전 형식·대상)도 여기.
3. **manifest** — `package_domain::manifest()`가 영향을 받으면 XML 이스케이프를 거쳐 출력. `inspect_manifest`(infra)에서 역추출 검증도 추가.
4. **계획 고정** — 빌드 결과에 영향을 주는 값이면 `Plan`에 포함되어 자동 고정된다(`project` 전체가 들어감). local 값이면 `Plan`에 명시 필드 추가 + `pack()` 전 재검사.
5. **JSON Schema 파일** — `src/`에서:
```powershell
cargo run -q -p package-builder -- schema | Out-File -Encoding utf8 schemas/package-project.schema.json
```
생성 후 `git diff schemas/`로 의도한 변경만 있는지 확인한다. 줄 끝은 LF.
6. **TypeScript 타입** — `src/apps/desktop/src/main.ts`의 `Project`/`Document` type. 선택 필드는 `field?: T | null`.
7. **UI** — 편집 가능한 값이면 해당 탭의 `field()`/`select()` 추가. JSON 편집기만으로 다루는 값이면 README에 그렇게 적는다.
8. **fixture·예제** — `src/fixtures/InstallerProject.isproj`, `docs/code-design/examples/msix.isproj`, `src/scripts/New-Fixtures.ps1`, `src/crates/package-infra/tests/workflow.rs`의 `fixture()`. `create_project()`의 기본값도 확인.
9. **테스트** — `project_format.rs`에 (a) 필드 없는 기존 파일이 열림, (b) 라운드트립 저장, (c) 잘못된 값 거부 세 가지. 제거한 필드는 "unknown field" 거부 테스트.
10. **문서** — `docs/code-design/13-isproj-project-format.md`(형식), `02-project-model.md`(모델 표), `src/README.md`(사용자 절차). `change-record` 스킬로 변경 기록.
11. `check` 스킬.
## 금지
- `#[serde(alias)]`나 마이그레이션 코드로 옛 이름을 조용히 받아들이기.
- `deny_unknown_fields` 제거.
- 스키마 파일을 손으로 편집하기.
- `schemaVersion`을 올리기(사용자 결정 필요).