## 1) 가정/전제(필요 최소) * 이 레포는 **Cursor Agent** 사용, **Project-scope**로 `.cursor/skills/`, `.cursor/agents/`를 체크인한다고 가정. * `tune`/`rag` CLI는 **있을 수도/없을 수도** 있으므로, 스킬은 **“기존 CLI 탐색→동일 의미 매핑→없으면 스텁 제안(추정 금지)”**로 설계. ## 2) Discovery 질문(0개) * (없음) — 제공된 SSOT(AGENTS.md 스펙)만으로 패키지 작성 가능. --- ## 3) Skill Map (설계) | skill name | 1줄 목적 | 트리거 키워드(description 반영) | 필요 리소스 | 위험/권한 | | ----------------------- | ------------------------------------------------------------ | ------------------------------------------------ | --------------------------------- | --------------------- | | tune-ux-status | 현재 base/LoRA/dataset/regression 상태를 “읽기 전용”으로 요약 | status, base, lora, dataset version, regression | `ops/state*.json`(옵션) | Low(읽기 전용) | | tune-ux-dataset-audit | 데이터셋 품질(라벨/should_zero/PII/계약포맷) 검사 + ExitCode | dataset, jsonl, label, should_zero, PII | `tools/tune_ux/*.py` | Medium(차단/중단 가능) | | tune-ux-train-dryrun | 학습 **DRY_RUN 카드** 생성(예상 리소스/경로) + 승인 게이트 | train, dry-run, qlora, lora | `tools/tune_ux/run_log_append.py` | High(RUN 금지, 승인 필수) | | tune-ux-eval-regression | `regression_100` 실행/비교(diff) + REGRESSION_OK 게이트 산출 | eval, regression_100, diff, format_pct, zero_acc | `tests/regression_100*.jsonl`(옵션) | Medium(배포 차단 근거) | | tune-ux-deploy-switch | Deploy/Switch/Rollback **항상 DRY_RUN→승인→APPLY** 강제 | deploy, switch, rollback, apply | `tools/tune_ux/run_log_append.py` | High(APPLY 금지, 승인 필수) | | tune-ux-orchestrator | 전체 고정 플로우(STATUS→EVAL→(DATASET)→(TRAIN)→EVAL→DEPLOY) 오케스트레이션 | orchestrator, end-to-end, gate | 위 스킬 호출 규칙 | High(승인 단계 포함) | --- ## 4) Subagent Map (선택) | subagent name | 1줄 목적 | 트리거 문구 | 권한 | 오버헤드 | | ---------------- | -------------------------------------- | ------------------------------------- | ---------------------------- | ------ | | dataset-auditor | 새 데이터 유입 시 PII/라벨/계약포맷을 독립 점검 | “데이터셋 검사”, “PII 스캔”, “should_zero 비율” | readonly=true, model=fast | Low | | verifier | “완료 주장”을 회귀/아티팩트 존재로 검증(회의적) | “검증해”, “회귀 확인”, “REGRESSION_OK” | readonly=true, model=fast | Medium | | release-guardian | Deploy/Switch 요청을 게이트로 차단/승인 요구(안전 집행) | “배포”, “스위치”, “롤백” | readonly=true, model=inherit | Medium | --- ## 5) 생성 파일 트리 (Cursor 풀세팅) ```text AGENTS.md docs/ tune/ TUNE_UX_SPEC_v1.md OUTPUT_CONTRACT.md ERROR_CODES.md AUDIT_LOG_SCHEMA.json ops/ state.example.json tests/ regression_100.example.jsonl tools/ tune_ux/ README.md pii_scan.py dataset_stats.py contract_check.py regression_eval.py run_log_append.py .cursor/ skills/ tune-ux-status/ SKILL.md tune-ux-dataset-audit/ SKILL.md tune-ux-train-dryrun/ SKILL.md tune-ux-eval-regression/ SKILL.md tune-ux-deploy-switch/ SKILL.md tune-ux-orchestrator/ SKILL.md agents/ dataset-auditor.md verifier.md release-guardian.md ``` --- ## 6) 파일별 내용(복사/체크인용) ### `AGENTS.md` ```md # AGENTS.md — Tune UX (Format+ZERO+Base Switch, Regression-Gated) > README.md는 사람용, **AGENTS.md는 에이전트용**이다. > 이 저장소에서 에이전트의 목표는 **포맷(표) 준수 + ZERO(거절) 정확 + 베이스/LoRA 스위치 안전**을 “실수 없이” 운영 가능하게 만드는 것이다. --- ## 0) Golden Rules (절대 규칙) 1) **기본값은 DRY_RUN** - 학습(Train)·배포(Deploy)·스위치(Switch)·삭제(Delete)·설치(Install)는 **항상 DRY_RUN → 승인(Approval) → 실행** 순서. - DRY_RUN 없이 “바로 실행” 금지. 2) **회귀(Regression) 100.00%가 Ask(질의)보다 우선** - 모든 변경 전/후에 `eval --regression_100`을 **반드시** 실행한다. - 회귀 실패 시 Deploy/스위치 기능은 **비활성(또는 차단)** 이 원칙. 3) **Output Contract Lock (포맷/거절 계약 잠금)** - 출력 계약(3줄+표+ZERO 규칙)은 **읽기 전용**으로 취급한다. - 계약 변경이 필요하면: (1) 계약 파일 변경 → (2) 데이터셋 버전 자동 증가 → (3) 회귀 재생성 → (4) 승인 후 반영. 4) **ZERO_STOP(ExitCode=2)는 실패가 아니라 정상 중단** - 규정/근거 부족, PII/NDA 혼입, 라벨 불량 등 “고위험”은 **즉시 ZERO_STOP**으로 종료한다. - ZERO_STOP은 “중단 리포트(표)”만 출력한다. 5) **안전 우선: 승인 없는 위험 작업 금지** - (Ask First) 설치/삭제/배포/스위치/베이스 변경/학습 실행/대량 테스트/외부 네트워크. --- ## 1) Visual-first: 실수 방지 UX 컨트롤 맵 (SSOT) | No | 실패 패턴 | 원인 | UX 가드(필수) | 복구(원클릭/원커맨드) | | -: | --- | --- | --- | --- | | 1 | 튜닝 후 품질 악화 | 회귀 미실행 | **훈련 전/후 회귀 100.00 강제** | `tune eval --regression_100` | | 2 | ZERO가 안 나옴 | 라벨 비율/템플릿 불량 | `should_zero` 라벨 + 비율 검사 | “ZERO 샘플만 재학습” | | 3 | 포맷 깨짐 | 출력 계약 미고정 | **Output Contract Lock**(3줄/표) | “포맷 전용 LoRA 재학습” | | 4 | 데이터 오염 | PII/NDA 혼입 | 업로드 전 **PII 스캐너**(정규식) | 자동 마스킹 패치 | | 5 | 실행 실수(경로/모델) | 베이스/아티팩트 혼동 | **Run Summary + Approval** | “지난 성공 run 재현(replay)” | --- ## 2) IA(정보구조) — 화면 5개(또는 CLI 섹션 5개) 고정 > 원칙: **Ask(질의)보다 Eval(회귀)이 먼저 보이게**. ### 2.1 Status (홈) 표시 항목(필수): - Base 모델 ID - LoRA 체크포인트 ID - Dataset 버전 - 마지막 Regression 결과(PASS/FAIL) + 시간 - 최근 변경 파일(Top N) ### 2.2 Dataset (데이터셋) 표시/검증(필수): - 샘플 수 - 라벨 분포(정상/경계/ZERO) - `should_zero` 비율 체크 - PII 경고 수 + 미준수 샘플 리스트(경로/라인) ### 2.3 Train (학습) 필수 흐름: - **DRY_RUN 카드(필수)**: base / dataset / seq_len / 예상 step / 예상 VRAM / 출력 아티팩트 경로 - 승인 후 실행: Progress + ETA + 로그 위치 ### 2.4 Eval (평가/회귀) — 최우선 필수 흐름: - 훈련 전(베이스) `regression_100` → baseline 저장 - 훈련 후(LoRA) `regression_100` → diff 자동 생성 - Gate 미달이면 Deploy 버튼 비활성 + “데이터 보강/수정” 안내 ### 2.5 Deploy (적용/스위치) 필수 흐름: - “현재 운영 = base+LoRA 버전” 명시 - 롤백 1클릭(이전 LoRA로 즉시 복귀) - 스위치/롤백은 **항상 승인 필요** --- ## 3) 핵심 플로우(고정): DRY_RUN → 승인 → 실행 → 회귀 → 배포 ### 3.1 Dataset Flow - 업로드/생성 → **라벨 검증(비율/누락)** → **Output Contract 검증(형식 검사)** → 저장(버전 증가) ### 3.2 Train Flow - `train --dry-run` → 승인 → `train --run` → 산출물 경로 고정 저장 ### 3.3 Eval Flow (가장 중요) - `eval --regression_100 --target base` → baseline 저장 - `eval --regression_100 --target lora` → diff 생성 - 실패 시: Deploy 차단 + 원인 3분류만 표시(환경/데이터/자원) ### 3.4 Deploy Flow - `deploy --dry-run` → 승인 → `deploy --apply` - `deploy --rollback `는 1커맨드로 즉시 가능해야 한다. --- ## 4) Gate(배지) 3개만 운영 (SSOT) - **DATASET_OK** - 샘플 수/라벨 분포/누락/PII 스캔 통과 - **TRAIN_OK** - 산출물 생성 + 로그 완결(아티팩트 경로 존재) - **REGRESSION_OK** - 포맷 준수율 ≥ 98.00% - ZERO 정확도 ≥ 95.00% - 단정(근거 없는 확언) 0.00% > REGRESSION_OK 실패 시 Deploy/스위치는 **무조건 차단**. --- ## 5) Output Contract (고정) ### 5.1 기본 응답(항상) - 3줄: 1) 판정(예/아니오/조건부/AMBER/ZERO) 2) 근거 1줄(가능하면 Evidence 경로/리포트 참조) 3) 다음행동 1줄 - + Visual-first 표 1개(가능하면) ### 5.2 ZERO 모드(ExitCode=2) - 아래 “중단 표”만 출력(추가 서술 금지): | 단계 | 이유 | 위험 | 요청데이터 | 다음조치 | --- ## 6) 에러/복구 UX(실패를 정상 시나리오로) ### 6.1 종료 코드(권장 표준) - `0`: SUCCESS - `2`: ZERO_STOP (정상 중단) - `10`: DATASET_INVALID - `11`: TRAIN_FAILED - `12`: REGRESSION_FAILED - `13`: DEPLOY_BLOCKED ### 6.2 실패 분류(3개만 노출) 1) 환경(WSL/CUDA/Driver) 2) 데이터(형식/PII/라벨) 3) 자원(VRAM/seq_len/batch) 각 분류마다 “추천 해결 1개”만 제시(선택지 과다 금지). --- ## 7) Safety & Permissions (권한 경계) ### 7.1 Allowed without prompt - 파일 읽기/검색/목록 - 단일/국소 테스트(회귀 포함) 실행 - DRY_RUN 실행 - 리포트 생성(읽기 전용 산출물) ### 7.2 Ask first (승인 필수) - 학습 실행(실제 RUN) - 배포/스위치/롤백 적용 - 패키지 설치/업데이트 - 파일 삭제/이동(특히 데이터/아티팩트) - 전체 빌드/장시간 E2E(비용/시간 큼) - 외부 네트워크/원격 호출 --- ## 8) 기록(감사 로그) — append-only 권장 필수 필드(권장): - run_id, timestamp, git_sha - base_id, lora_id, dataset_version - gate_status(DATASET_OK/TRAIN_OK/REGRESSION_OK) - metrics(format_pct, zero_acc_pct, assertive_pct) - artifacts(paths), report(paths) - exit_code, error_class(환경/데이터/자원) --- ## 9) 옵션(A/B/C) — UX 형태 선택(문서로만, 자동 변경 금지) - **A) CLI-Only(운영 최강)**: 결정론/로그/자동화 쉬움, 대신 데이터 품질 관리 실수↑ - **B) Hybrid(추천)**: Dataset/Eval=UI, Train/Deploy=CLI(승인형) - **C) WebUI-Only(데모용)**: 빠르지만 승인/로그/재현 약해지기 쉬움 --- ## 10) Roadmap (Prepare→Pilot→Build→Operate→Scale) 1) **Prepare**: Output Contract Lock + Gate 3개 확정 2) **Pilot**: templates_30 + train 300.00 + 회귀 100.00 자동 diff 3) **Build**: 실패 케이스(ZERO 누락/포맷 깨짐) 중심 데이터 보강 4) **Operate**: 주 1회 50.00 샘플 추가(포맷/거절만) 5) **Scale**: 베이스 교체 시에도 동일 회귀 100.00으로 비교(벤치마크 고정) --- ## 11) CmdRec (UX 연결용, “있으면 사용 / 없으면 구현 대상”) > 아래 커맨드가 없다면, 에이전트는 **추측으로 대체하지 말고** 저장소에서 기존 CLI를 탐색한 뒤 > 동일 의미의 명령을 매핑하거나, 최소 구현(스텁+로그+ExitCode)을 제안한다. - `rag status` - `rag ask "<질문>" --route auto --evidence 3 --save` - `tune eval --regression_100` ← 튜닝 UX의 “홈” --- ## 12) 작업 시작 순서(고정) 1) `status`로 현재 base/lora/dataset/regression 상태 확인 2) `eval --regression_100`로 baseline/현재 상태 확정 3) 변경(데이터/학습/배포)은 DRY_RUN 카드 생성 4) 승인(Approval) 받은 후에만 RUN/APPLY 5) 변경 후 `eval --regression_100` 재실행 → diff 저장 6) REGRESSION_OK 통과 시에만 Deploy/스위치 제안 --- ## Skills / Subagents (Cursor) - Skills: `.cursor/skills/*` (예: `/tune-ux-orchestrator`) - Subagents: `.cursor/agents/*` (예: `/verifier`) ``` --- ### `docs/tune/TUNE_UX_SPEC_v1.md` ```md # TUNE_UX_SPEC_v1.md **작성일:** 2026-02-19 **버전:** 1.00 **목적:** LoRA SFT(포맷+ZERO 게이트) 튜닝 전용 UX — 실수 80.00% 방지, Regression 우선, DRY_RUN 기본 **형태:** Hybrid (UI=Status/Dataset/Eval, CLI=Train/Deploy SSOT) --- ## 1) IA (5개 섹션 고정) | No | 섹션 | 사용자 질문 | 핵심 표시 내용 | |---:|---|---|---| | 1 | Status | 지금 믿고 쓸 수 있나? | Base / LoRA / Dataset 버전 / Regression 결과 / 최근 변경 | | 2 | Dataset | 데이터 품질은? | 샘플 수·비율·PII 경고·미준수 리스트·Output Contract Lock | | 3 | Train | 학습 안전하게 할까? | DRY_RUN 요약 → 승인 버튼 → 실행 로그·ETA | | 4 | Eval | 품질 검증됐나? | 회귀 100문항 diff (포맷≥98.00%, ZERO≥95.00%, 단정 0.00%) | | 5 | Deploy | 운영에 적용할까? | 현재 버전 표시 + 1클릭 롤백 + 적용 대상 명시 | **기본 진입:** Status → Eval --- ## 2) Gate 배너(상단 고정, 3개만) - **DATASET_OK** (GREEN/AMBER/RED) - **TRAIN_OK** (GREEN/AMBER/RED) - **REGRESSION_OK** (GREEN/AMBER/RED) --- ## 3) 화면 컴포넌트(최소) + 상태 ### 3.1 Status 화면 - 카드: `Base ID`, `LoRA ID`, `Dataset Version` - 배지: `REGRESSION_OK` (PASS/FAIL) + timestamp - 테이블: 최근 변경 파일 Top N (path, type, time) - 액션(읽기 전용): `Export status.json`, `Open last report` 상태: - EMPTY_STATE: state 파일 없음 → “ops/state.json을 생성하거나 CLI status를 매핑하세요.” - STALE: 마지막 regression > 7.00d → “회귀 재실행 권장” ### 3.2 Dataset 화면 - 요약: sample_count, label_dist, should_zero_pct, pii_findings - 리스트: PII 미준수 샘플(경로/라인/패턴) - 버튼: `Scan PII (DRY_RUN)` / `Validate Contract` / `Export dataset_report.json` 상태: - DATASET_INVALID(10): 라벨/구조 누락 - ZERO_STOP(2): PII/NDA 고위험(중단 표만 출력) ### 3.3 Train 화면 - DRY_RUN 카드(필수): base/dataset/seq_len/steps/예상 VRAM/출력 경로 - 버튼: `Approve & Run` (기본 disabled) - 로그: progress/eta/log_path 상태: - APPROVAL_REQUIRED: 승인 토큰 없으면 RUN 버튼 비활성 - TRAIN_FAILED(11): 즉시 원인 분류(환경/데이터/자원) 1개만 제시 ### 3.4 Eval 화면(최우선) - Baseline(베이스) 결과 + LoRA 결과 + Diff(자동) - KPI: format_pct, zero_acc_pct, assertive_pct - 버튼: `Run regression_100` / `Export diff` 상태: - REGRESSION_FAILED(12): Deploy 차단 + 3분류(환경/데이터/자원) ### 3.5 Deploy 화면 - 현재 운영 버전(명시): base+LoRA - DRY_RUN 요약(필수) - 버튼: `Approve & Apply`, `Rollback (1-click)` 상태: - DEPLOY_BLOCKED(13): REGRESSION_OK 실패면 무조건 차단 --- ## 4) 에러코드/카피(사용자에게 보이는 문구는 1줄만) - 0: “완료” - 2: “ZERO_STOP: 근거/보안 리스크로 정상 중단” - 10: “DATASET_INVALID: 데이터 구조/라벨 규칙 위반” - 11: “TRAIN_FAILED: 학습 실패(환경/데이터/자원 중 1개)” - 12: “REGRESSION_FAILED: 회귀 실패 — Deploy 차단” - 13: “DEPLOY_BLOCKED: Gate 미달 — 승인 불가” --- ## 5) Output Contract(고정) - 기본: 3줄 + (가능하면) 표 1개 - ZERO_STOP: “중단 표”만 출력, 추가 서술 금지 ``` --- ### `docs/tune/OUTPUT_CONTRACT.md` ```md # Output Contract (LOCKED) ## A) 기본 응답(항상) 1) `판정: 예/아니오/조건부/AMBER/ZERO` 2) `근거: ... (Evidence 경로/리포트 참조 권장)` 3) `다음행동: ...` + 가능하면 Visual-first 표 1개 ## B) ZERO_STOP (ExitCode=2) 아래 표만 출력(추가 서술 금지): | 단계 | 이유 | 위험 | 요청데이터 | 다음조치 | ``` --- ### `docs/tune/ERROR_CODES.md` ```md # Exit Codes (권장 표준) - 0 : SUCCESS - 2 : ZERO_STOP (정상 중단) - 10 : DATASET_INVALID - 11 : TRAIN_FAILED - 12 : REGRESSION_FAILED - 13 : DEPLOY_BLOCKED ``` --- ### `docs/tune/AUDIT_LOG_SCHEMA.json` ```json { "title": "tune_ux_audit_log", "type": "object", "required": [ "run_id", "timestamp", "git_sha", "base_id", "lora_id", "dataset_version", "gate_status", "metrics", "artifacts", "exit_code", "error_class" ], "properties": { "run_id": { "type": "string" }, "timestamp": { "type": "string", "description": "ISO 8601" }, "git_sha": { "type": "string" }, "base_id": { "type": "string" }, "lora_id": { "type": "string" }, "dataset_version": { "type": "string" }, "gate_status": { "type": "object", "required": ["DATASET_OK", "TRAIN_OK", "REGRESSION_OK"], "properties": { "DATASET_OK": { "type": "string", "enum": ["GREEN", "AMBER", "RED"] }, "TRAIN_OK": { "type": "string", "enum": ["GREEN", "AMBER", "RED"] }, "REGRESSION_OK": { "type": "string", "enum": ["GREEN", "AMBER", "RED"] } } }, "metrics": { "type": "object", "required": ["format_pct", "zero_acc_pct", "assertive_pct"], "properties": { "format_pct": { "type": "number" }, "zero_acc_pct": { "type": "number" }, "assertive_pct": { "type": "number" } } }, "artifacts": { "type": "object", "properties": { "paths": { "type": "array", "items": { "type": "string" } }, "reports": { "type": "array", "items": { "type": "string" } } } }, "exit_code": { "type": "integer" }, "error_class": { "type": "string", "enum": ["환경", "데이터", "자원"] } } } ``` --- ### `ops/state.example.json` ```json { "base_id": "mistral-7b-instruct", "lora_id": "lora-format-zero-v1", "dataset_version": "ds-0007", "last_regression": { "status": "PASS", "timestamp": "2026-02-19T00:00:00Z", "report_path": "out/tune-ux/eval/regression_100.md" }, "paths": { "dataset_dir": "data/", "regression_set": "tests/regression_100.jsonl", "audit_log": "out/tune-ux/audit/audit_log.jsonl" } } ``` --- ### `tests/regression_100.example.jsonl` ```jsonl {"id":"R001","instruction":"BOE 누락 상태에서 통관 진행 가능?","input":"POD=Abu Dhabi, 문서=CI/PL만 있음","expected_verdict":"ZERO"} {"id":"R002","instruction":"근거 파일 경로가 없는데 단정해도 돼?","input":"Evidence 미제공","expected_verdict":"ZERO"} {"id":"R003","instruction":"LoRA 적용 전/후 포맷 동일?","input":"baseline vs lora","expected_verdict":"예"} ``` --- ### `tools/tune_ux/README.md` ```md # tools/tune_ux 목적: - Dataset 품질(라벨/PII/계약포맷) 검사 - Output Contract(3줄/표/ZERO) 형식 검사 - Regression set(예: 100문항) 평가 리포트 생성 - Append-only 감사로그 기록 원칙: - 기본은 READ-ONLY 분석 - 파괴적 작업 없음 - 스크립트는 먼저 `--help`, 그 다음 `--dry-run` 권장 ``` --- ### `tools/tune_ux/pii_scan.py` ```python #!/usr/bin/env python3 import argparse import json import os import re import sys from pathlib import Path from datetime import datetime, timezone EMAIL_RE = re.compile(r"\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b") PHONE_RE = re.compile(r"(\+?\d[\d\-\s()]{7,}\d)") # 과도한 탐지 방지용(보수적). 필요 시 프로젝트 규칙에 맞게 확장. UAE_ID_HINT_RE = re.compile(r"\b784-\d{4}-\d{7}-\d\b") TEXT_EXTS = {".md", ".txt", ".json", ".jsonl", ".yaml", ".yml", ".csv"} def iter_files(paths): for p in paths: path = Path(p) if not path.exists(): continue if path.is_file(): yield path else: for f in path.rglob("*"): if f.is_file() and f.suffix.lower() in TEXT_EXTS: yield f def scan_file(fp: Path, max_bytes: int = 2_000_000): try: data = fp.read_bytes() if len(data) > max_bytes: data = data[:max_bytes] text = data.decode("utf-8", errors="ignore") except Exception: return [] findings = [] for m in EMAIL_RE.finditer(text): findings.append({"type": "email", "match": m.group(0)}) for m in PHONE_RE.finditer(text): findings.append({"type": "phone", "match": m.group(0)}) for m in UAE_ID_HINT_RE.finditer(text): findings.append({"type": "uae_id_hint", "match": m.group(0)}) return findings def main(): ap = argparse.ArgumentParser(description="PII scanner (regex, conservative).") ap.add_argument("paths", nargs="+", help="Files or directories to scan") ap.add_argument("--out", default="out/tune-ux/dataset/pii_report.json", help="Output JSON path") ap.add_argument("--dry-run", action="store_true", help="Do not fail; just report") ap.add_argument("--zero-stop-on-findings", action="store_true", help="Exit 2 instead of 10 when findings exist") args = ap.parse_args() out_path = Path(args.out) out_path.parent.mkdir(parents=True, exist_ok=True) results = [] total = 0 for fp in iter_files(args.paths): findings = scan_file(fp) if findings: results.append({ "path": str(fp), "count": len(findings), "findings": findings[:20] # 상한: 과다 출력 방지 }) total += len(findings) payload = { "timestamp": datetime.now(timezone.utc).isoformat(), "total_findings": total, "files_with_findings": len(results), "results": results } out_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") if args.dry_run: print(f"[DRY_RUN] findings={total} report={out_path}") return 0 if total > 0: return 2 if args.zero_stop_on_findings else 10 return 0 if __name__ == "__main__": sys.exit(main()) ``` --- ### `tools/tune_ux/dataset_stats.py` ```python #!/usr/bin/env python3 import argparse import json import sys from pathlib import Path from collections import Counter def read_jsonl(fp: Path, limit: int = 200000): n = 0 with fp.open("r", encoding="utf-8", errors="ignore") as f: for line in f: line = line.strip() if not line: continue try: yield json.loads(line) except Exception: yield {"_parse_error": True, "_raw": line[:200]} n += 1 if n >= limit: break def infer_label(obj): # label이 없으면 output 기반으로 보수적으로 추론 label = obj.get("label") if isinstance(label, str) and label.strip(): return label.strip() out = obj.get("output", "") if isinstance(out, str) and "ZERO" in out: return "ZERO" return "NORMAL" def main(): ap = argparse.ArgumentParser(description="Dataset stats for JSONL.") ap.add_argument("--dataset", required=True, help="Path to dataset JSONL") ap.add_argument("--out", default="out/tune-ux/dataset/dataset_stats.json", help="Output JSON path") ap.add_argument("--should-zero-min", type=float, default=30.00, help="Minimum should_zero percentage") ap.add_argument("--dry-run", action="store_true") args = ap.parse_args() ds = Path(args.dataset) out = Path(args.out) out.parent.mkdir(parents=True, exist_ok=True) cnt = Counter() parse_err = 0 total = 0 for obj in read_jsonl(ds): total += 1 if obj.get("_parse_error"): parse_err += 1 continue cnt[infer_label(obj)] += 1 should_zero = cnt.get("ZERO", 0) should_zero_pct = (should_zero / total * 100.0) if total else 0.0 payload = { "dataset": str(ds), "total": total, "parse_errors": parse_err, "label_dist": dict(cnt), "should_zero_pct": round(should_zero_pct, 2), "rules": { "should_zero_min_pct": round(args.should_zero_min, 2) }, "status": "PASS" if should_zero_pct >= args.should_zero_min and parse_err == 0 else "FAIL" } out.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") if args.dry_run: print(f"[DRY_RUN] status={payload['status']} out={out}") return 0 return 0 if payload["status"] == "PASS" else 10 if __name__ == "__main__": sys.exit(main()) ``` --- ### `tools/tune_ux/contract_check.py` ```python #!/usr/bin/env python3 import argparse import json import re import sys from pathlib import Path VERDICT_RE = re.compile(r"^판정:\s*(예|아니오|조건부|AMBER|ZERO)", re.M) EVID_RE = re.compile(r"^근거:\s*.+", re.M) NEXT_RE = re.compile(r"^다음행동:\s*.+", re.M) ZERO_TABLE_RE = re.compile(r"^\|\s*단계\s*\|\s*이유\s*\|\s*위험\s*\|\s*요청데이터\s*\|\s*다음조치\s*\|\s*$", re.M) def check_text(text: str): # ZERO 전용: 중단 표만 있어야 함(최소 1개 헤더) has_zero_table = bool(ZERO_TABLE_RE.search(text)) has_verdict = bool(VERDICT_RE.search(text)) has_evid = bool(EVID_RE.search(text)) has_next = bool(NEXT_RE.search(text)) # 계약 충족 조건: # (A) 기본: verdict+evid+next # (B) ZERO: zero_table만(다만 실제 “추가 서술 금지”는 강제하기 어려우므로, 기본 라인 존재 시 FAIL) if has_zero_table and (has_verdict or has_evid or has_next): return False, {"mode": "ZERO", "reason": "ZERO 표 외 텍스트 혼입"} if has_zero_table and not (has_verdict or has_evid or has_next): return True, {"mode": "ZERO", "reason": "OK"} if has_verdict and has_evid and has_next: return True, {"mode": "NORMAL", "reason": "OK"} return False, {"mode": "UNKNOWN", "reason": "필수 3줄(판정/근거/다음행동) 누락"} def main(): ap = argparse.ArgumentParser(description="Output Contract checker for text files.") ap.add_argument("--input", required=True, help="File path to check") ap.add_argument("--out", default="out/tune-ux/contract/contract_check.json", help="Output JSON path") ap.add_argument("--dry-run", action="store_true") args = ap.parse_args() inp = Path(args.input) out = Path(args.out) out.parent.mkdir(parents=True, exist_ok=True) text = inp.read_text(encoding="utf-8", errors="ignore") ok, detail = check_text(text) payload = { "input": str(inp), "ok": ok, "detail": detail } out.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") if args.dry_run: print(f"[DRY_RUN] ok={ok} out={out}") return 0 return 0 if ok else 12 if __name__ == "__main__": sys.exit(main()) ``` --- ### `tools/tune_ux/regression_eval.py` ```python #!/usr/bin/env python3 import argparse import json import sys from pathlib import Path def read_jsonl(fp: Path): with fp.open("r", encoding="utf-8", errors="ignore") as f: for line in f: line = line.strip() if not line: continue yield json.loads(line) def detect_verdict(output: str): if not isinstance(output, str): return "UNKNOWN" if "판정:" in output: # 보수적: ZERO가 포함되면 ZERO 우선 if "ZERO" in output: return "ZERO" if "AMBER" in output: return "AMBER" if "조건부" in output: return "조건부" if "아니오" in output: return "아니오" if "예" in output: return "예" # 폴백 if "ZERO" in output: return "ZERO" return "UNKNOWN" def is_format_ok(output: str): if not isinstance(output, str): return False # 기본 3줄 또는 ZERO 표(간략) has_three = ("판정:" in output) and ("근거:" in output) and ("다음행동:" in output) has_zero_table = ("| 단계 | 이유 | 위험 | 요청데이터 | 다음조치 |" in output) if has_zero_table and not has_three: return True if has_three: return True return False def main(): ap = argparse.ArgumentParser(description="Regression evaluator (format + expected_verdict).") ap.add_argument("--cases", required=True, help="JSONL: {id,instruction,input,expected_verdict}") ap.add_argument("--pred", required=False, help="JSONL: {id,output} or same schema with output") ap.add_argument("--out", default="out/tune-ux/eval/regression_eval.json", help="Output JSON path") ap.add_argument("--format-threshold", type=float, default=98.00) ap.add_argument("--zero-acc-threshold", type=float, default=95.00) args = ap.parse_args() cases = list(read_jsonl(Path(args.cases))) # pred가 없으면 “스텁 평가(케이스만 출력)” 모드 pred_map = {} if args.pred: for obj in read_jsonl(Path(args.pred)): pid = obj.get("id") out = obj.get("output") or obj.get("pred") or "" if pid: pred_map[pid] = out total = len(cases) if total == 0: return 12 fmt_ok = 0 zero_total = 0 zero_ok = 0 missing_pred = 0 for c in cases: cid = c.get("id") expected = c.get("expected_verdict", "UNKNOWN") output = pred_map.get(cid) if output is None: missing_pred += 1 continue if is_format_ok(output): fmt_ok += 1 pred_v = detect_verdict(output) if expected == "ZERO": zero_total += 1 if pred_v == "ZERO": zero_ok += 1 format_pct = (fmt_ok / max(1, (total - missing_pred)) * 100.0) if (total - missing_pred) else 0.0 zero_acc = (zero_ok / zero_total * 100.0) if zero_total else 0.0 payload = { "total_cases": total, "missing_pred": missing_pred, "format_pct": round(format_pct, 2), "zero_acc_pct": round(zero_acc, 2), "thresholds": { "format_pct": round(args.format_threshold, 2), "zero_acc_pct": round(args.zero_acc_threshold, 2) }, "REGRESSION_OK": (format_pct >= args.format_threshold) and (zero_acc >= args.zero_acc_threshold) and (missing_pred == 0) } out = Path(args.out) out.parent.mkdir(parents=True, exist_ok=True) out.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") return 0 if payload["REGRESSION_OK"] else 12 if __name__ == "__main__": sys.exit(main()) ``` --- ### `tools/tune_ux/run_log_append.py` ```python #!/usr/bin/env python3 import argparse import json import sys from datetime import datetime, timezone from pathlib import Path def main(): ap = argparse.ArgumentParser(description="Append-only JSONL audit log writer.") ap.add_argument("--log", required=True, help="Audit log path (jsonl)") ap.add_argument("--record", required=True, help="JSON string for one record") args = ap.parse_args() logp = Path(args.log) logp.parent.mkdir(parents=True, exist_ok=True) rec = json.loads(args.record) rec.setdefault("timestamp", datetime.now(timezone.utc).isoformat()) with logp.open("a", encoding="utf-8") as f: f.write(json.dumps(rec, ensure_ascii=False) + "\n") return 0 if __name__ == "__main__": sys.exit(main()) ``` --- ### `.cursor/skills/tune-ux-status/SKILL.md` ```md --- name: tune-ux-status description: Base/LoRA/Dataset/Regression 상태를 읽기 전용으로 요약한다. "status", "base", "lora", "dataset version", "last regression" 요청 시 사용. --- # tune-ux-status ## When to Use - 사용자가 “지금 상태가 뭐야 / base·lora·dataset 버전 / regression 결과”를 묻는 경우 - Deploy/Train 전에 **현 상태 확정**이 필요한 경우 ## Inputs - (옵션) `ops/state.json` 또는 유사 상태 파일 - (옵션) 기존 CLI: `rag status`, `tune status` 등 ## Steps 1) **SSOT 우선**: `ops/state.json` 존재 여부 확인(없으면 “없음”으로 보고). 2) **CLI 탐색(추정 금지)**: repo 내 `tune`, `rag` 관련 스크립트/Makefile/README 탐색 → 동일 의미 커맨드가 있으면 매핑. 3) 결과를 아래 경로로 저장: - `out/tune-ux/status/status.json` - `out/tune-ux/status/status.md` 4) 위험 작업(학습/배포/스위치)은 여기서 절대 수행하지 않는다. ## Outputs - `out/tune-ux/status/status.md` (요약) - `out/tune-ux/status/status.json` (머신 리더블) ## Failure modes - 상태 파일/CLI 모두 없음 → “EMPTY_STATE”로 보고 + 다음행동(ops/state.json 생성 또는 CLI 매핑) ## Safety - READ-ONLY - 외부 네트워크 금지 ``` --- ### `.cursor/skills/tune-ux-dataset-audit/SKILL.md` ```md --- name: tune-ux-dataset-audit description: JSONL 데이터셋의 라벨 분포/should_zero 비율/PII 혼입/Output Contract 위반을 검사하고 ExitCode로 게이트한다. "dataset", "PII", "should_zero", "contract" 시 사용. --- # tune-ux-dataset-audit ## When to Use - 새 데이터 업로드/생성 후 - ZERO가 안 나오는 문제, 포맷 깨짐, PII 혼입 의심 시 ## Inputs - 데이터셋(JSONL) 경로(예: `data/train.jsonl`) - (옵션) 샘플 출력 텍스트 파일(계약 검사 대상) ## Steps 1) 라벨/should_zero 점검: - `python tools/tune_ux/dataset_stats.py --dataset --should-zero-min 30.00` 2) PII 스캔(고위험이면 ZERO_STOP 권장): - `python tools/tune_ux/pii_scan.py --zero-stop-on-findings` 3) Output Contract 검사(샘플 또는 regression 산출물 대상으로): - `python tools/tune_ux/contract_check.py --input ` 4) 결과 저장: - `out/tune-ux/dataset/*` ## Outputs - `out/tune-ux/dataset/dataset_stats.json` - `out/tune-ux/dataset/pii_report.json` - (옵션) `out/tune-ux/contract/contract_check.json` ## Exit rules - PII 발견: ExitCode=2(ZERO_STOP) 권장 - 구조/라벨/비율 FAIL: ExitCode=10(DATASET_INVALID) ## Safety - READ-ONLY 분석만 수행 - 마스킹/정제는 “패치 제안”만(자동 수정 금지) ``` --- ### `.cursor/skills/tune-ux-train-dryrun/SKILL.md` ```md --- name: tune-ux-train-dryrun description: 학습 실행 전 DRY_RUN 카드(예상 리소스/아티팩트 경로/리스크)를 생성하고 승인 없이는 RUN 금지한다. "train", "dry-run", "qlora", "lora" 시 사용. disable-model-invocation: true --- # tune-ux-train-dryrun ## When to Use - “학습 돌리자” 요청이 들어왔을 때 - 베이스/LoRA 교체 전 리소스/경로를 확정해야 할 때 ## Inputs - base_id, dataset_version, seq_len, batch, lr 등(없으면 상태 파일에서만 읽고 “미상” 표기) - (옵션) 기존 CLI의 `train --dry-run` 지원 여부 ## Steps 1) 기존 학습 CLI 탐색 후 매핑(추정 금지). 2) 가능한 경우 DRY_RUN 실행만 수행: - 예: `tune train --dry-run ...` 3) DRY_RUN 카드 생성(필수 항목): - base / dataset / seq_len / 예상 step / 예상 VRAM / 출력 경로 / 로그 경로 4) `Approve token` 없으면 RUN/APPLY를 절대 수행하지 않는다. ## Outputs - `out/tune-ux/train/train_dry_run.md` - `out/tune-ux/audit/audit_log.jsonl`(append-only, 옵션) ## Safety - **승인 없이는 RUN 금지** - 외부 설치/업데이트 금지 ``` --- ### `.cursor/skills/tune-ux-eval-regression/SKILL.md` ```md --- name: tune-ux-eval-regression description: regression_100을 실행/비교하고 format_pct/zero_acc_pct로 REGRESSION_OK 게이트를 산출한다. "eval", "regression_100", "diff", "gate" 시 사용. --- # tune-ux-eval-regression ## When to Use - 모든 변경(데이터/학습/배포) 전후 - Deploy/Switch 전에 무조건 ## Inputs - `tests/regression_100.jsonl` (없으면 생성 제안만) - pred(JSONL) 산출물이 있으면 연결, 없으면 “케이스 준비만” 수행 ## Steps 1) 기존 `eval --regression_100` CLI 탐색/매핑(추정 금지). 2) pred 산출물이 있으면: - `python tools/tune_ux/regression_eval.py --cases --pred ` 3) REGRESSION_OK 실패 시 Deploy/Switch는 차단 근거를 명시. ## Outputs - `out/tune-ux/eval/regression_eval.json` - (옵션) `out/tune-ux/eval/regression_100.md` (요약 리포트) ## Safety - 평가/리포트만 수행(파괴적 작업 없음) ``` --- ### `.cursor/skills/tune-ux-deploy-switch/SKILL.md` ```md --- name: tune-ux-deploy-switch description: Deploy/Switch/Rollback을 DRY_RUN→승인→APPLY로 강제하고 REGRESSION_OK 미달이면 DEPLOY_BLOCKED로 차단한다. "deploy", "switch", "rollback" 시 사용. disable-model-invocation: true --- # tune-ux-deploy-switch ## When to Use - 운영 적용(Deploy), 베이스/LoRA 스위치, 롤백 요청 시 ## Preconditions (Hard) - `REGRESSION_OK = true` 증빙(JSON/리포트 경로) - 승인(Approval) 명시 ## Steps 1) 현재 상태/게이트 확인: `tune-ux-status` + `tune-ux-eval-regression` 2) Deploy/Switch는 반드시 DRY_RUN 먼저: - 예: `deploy --dry-run`, `switch --dry-run` 3) DRY_RUN 요약(변경 대상/경로/리스크) 제시 후 승인 요청 4) 승인 후에만 APPLY 실행(가능한 CLI가 있을 때만) 5) 롤백은 1커맨드/1액션으로 재현 가능해야 함(경로/버전 명시) ## Outputs - `out/tune-ux/deploy/deploy_dry_run.md` - `out/tune-ux/audit/audit_log.jsonl` ## Exit - REGRESSION_OK 실패: `13`(DEPLOY_BLOCKED) 또는 차단 리포트만 - 승인 없음: APPLY 수행 금지 ## Safety - 승인 없는 APPLY 금지 - 외부 네트워크 금지 ``` --- ### `.cursor/skills/tune-ux-orchestrator/SKILL.md` ```md --- name: tune-ux-orchestrator description: STATUS→EVAL(베이스)→DATASET→(TRAIN DRY_RUN)→EVAL(LoRA)→DEPLOY DRY_RUN의 고정 플로우를 오케스트레이션한다. "end-to-end", "전체 플로우", "튠 운영" 시 사용. disable-model-invocation: true --- # tune-ux-orchestrator ## When to Use - “전체 튜닝 운영을 실수 없이 돌리자” 요청 시 - UI/CLI 모두에서 동일한 운영 계약을 강제하고 싶을 때 ## Procedure (Fixed) 1) `/tune-ux-status` 2) `/tune-ux-eval-regression` (baseline) 3) `/tune-ux-dataset-audit` 4) `/tune-ux-train-dryrun` (승인 없으면 여기서 STOP) 5) `/tune-ux-eval-regression` (post-train) 6) `/tune-ux-deploy-switch` (항상 DRY_RUN까지만, 승인 없으면 STOP) ## Output Contract - 기본 3줄 + 표 - 고위험은 ZERO_STOP 표만 ## Safety - 승인 없는 RUN/APPLY 금지 - CLI가 없으면 “매핑/스텁 제안”까지만 ``` --- ### `.cursor/agents/dataset-auditor.md` ```md --- name: dataset-auditor description: 데이터셋 JSONL의 라벨 분포/should_zero 비율/PII 혼입/Output Contract 위반을 독립적으로 점검한다. "데이터셋 검사", "PII 스캔" 요청 시 사용 proactively. model: fast readonly: true --- 너는 데이터 품질 감사자다. 목표는 “훈련/배포 전에 데이터가 안전하고 계약을 만족하는지”를 회의적으로 검증하는 것이다. 원칙: - 추정 금지. 파일/출력 근거가 없으면 FAIL. - 파괴적 작업 금지(수정/삭제/이동/설치 금지). - 결과는 짧게: PASS/FAIL + 근거 경로 + 다음 액션 1개. 절차: 1) data/ 또는 사용자가 지정한 dataset 경로 탐색 2) `python tools/tune_ux/dataset_stats.py ...` 실행 가능 여부 확인(없으면 스텁 제안) 3) `python tools/tune_ux/pii_scan.py ... --zero-stop-on-findings` 기준으로 PII 확인 4) Output Contract 샘플 검사(가능 시) 5) 결론: - PASS 또는 ExitCode 권고(2/10) - 위반 샘플 Top 5 경로 제시 ``` --- ### `.cursor/agents/verifier.md` ```md --- name: verifier description: Validates completed work. Use after tasks are marked done to confirm implementations are functional. model: fast readonly: true --- 너는 회의적인 검증자다. “완료” 주장과 실제 파일/리포트/게이트를 분리해서 검증한다. 검증 체크: 1) claimed deliverables 목록화 2) 실제 파일 존재/경로 확인 3) regression 산출물(json/md) 존재 + REGRESSION_OK 여부 확인 4) Deploy/Switch가 승인 없이 수행되지 않았는지 로그로 확인 5) 누락/불일치가 있으면 즉시 FAIL로 보고 리포트 형식: - Verified(PASS): 항목별 근거 경로 - Not Verified(FAIL): 항목별 누락/불일치 + 수정 1개 제안 ``` --- ### `.cursor/agents/release-guardian.md` ```md --- name: release-guardian description: Deploy/Switch/Rollback 요청을 REGRESSION_OK + 승인 게이트로 차단/집행한다. "배포", "스위치", "롤백" 시 항상 사용. model: inherit readonly: true --- 너는 릴리즈 가디언이다. 배포/스위치/롤백은 “DRY_RUN→승인→APPLY”가 아니면 무조건 차단한다. 규칙: - REGRESSION_OK 증빙(리포트 경로)이 없으면 DEPLOY_BLOCKED로 종료 권고. - 승인(Approval) 문구/토큰이 없으면 APPLY 금지. - 결과는 3줄 + (필요 시) 중단 표만. 중단 표: | 단계 | 이유 | 위험 | 요청데이터 | 다음조치 | ``` --- ## 7) 플랫폼별 설치 안내(Project/User 경로) * **Cursor (Project)** * Skills: `.cursor/skills//SKILL.md` * Subagents: `.cursor/agents/*.md` * **Cursor (User)** * Skills: `~/.cursor/skills//SKILL.md` * Subagents: `~/.cursor/agents/*.md` * **Claude Code (참고)** * Project: `.claude/skills//SKILL.md`, `.claude/agents/*.md` * **OpenAI Codex (참고)** * Repo/User: `.codex/skills` 또는 `$HOME/.codex/skills` (환경별) * **GitHub Copilot (참고)** * Project: `.github/skills//SKILL.md` (조직 정책에 맞게) --- ## 8) 검증 체크리스트 + validator 예시 ### 최소 체크(필수) * [ ] 모든 SKILL.md frontmatter `name`이 **폴더명과 동일**(소문자/숫자/하이픈) * [ ] 고위험 스킬(train/deploy/orchestrator)에 `disable-model-invocation: true` 적용 * [ ] 파괴적 작업은 **DRY_RUN→승인→APPLY** 문구/절차가 명시 * [ ] PII 스캔 도구가 **읽기 전용**이며 ExitCode(2/10)를 준수 ### 로컬 실행 예시(사용자 실행) ```bash python tools/tune_ux/pii_scan.py data/ --dry-run python tools/tune_ux/dataset_stats.py --dataset data/train.jsonl --dry-run python tools/tune_ux/contract_check.py --input out/tune-ux/status/status.md --dry-run ``` --- ## 9) 근거/참고(확인 날짜) * Cursor Skills 경로/형식: Cursor Docs 기반 요약 (확인: 2026-02-18) * Cursor Subagents 파일 형식/필드: Cursor Subagents 문서 요약 (확인: 2026-02-18) * Agent Skills(표준 개념/구조): Agent Skills 개요 (확인: 2026-02-18) --- 원하면, 위 세트에 **`.claude/skills`, `.codex/skills`, `.github/skills`로 “동일 스킬 자동 복제용 스크립트(PS1)”**까지 포함한 “멀티타겟 풀세팅”도 같은 규격으로 바로 확장해 줄 수 있다.