1) 가정/전제(필요 최소)
- 이 레포는 Cursor Agent 사용, Project-scope로
.cursor/skills/,.cursor/agents/를 체크인한다고 가정. tune/ragCLI는 있을 수도/없을 수도 있으므로, 스킬은 **“기존 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 풀세팅)
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
# 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 <prev>`는 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
# 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
# Output Contract (LOCKED)
## A) 기본 응답(항상)
1) `판정: 예/아니오/조건부/AMBER/ZERO`
2) `근거: ... (Evidence 경로/리포트 참조 권장)`
3) `다음행동: ...`
+ 가능하면 Visual-first 표 1개
## B) ZERO_STOP (ExitCode=2)
아래 표만 출력(추가 서술 금지):
| 단계 | 이유 | 위험 | 요청데이터 | 다음조치 |
docs/tune/ERROR_CODES.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
{
"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
{
"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
{"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
# 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
#!/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
#!/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
#!/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
#!/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
#!/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
---
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
---
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 <DATASET_JSONL> --should-zero-min 30.00`
2) PII 스캔(고위험이면 ZERO_STOP 권장):
- `python tools/tune_ux/pii_scan.py <DATASET_DIR_OR_FILE> --zero-stop-on-findings`
3) Output Contract 검사(샘플 또는 regression 산출물 대상으로):
- `python tools/tune_ux/contract_check.py --input <OUTPUT_TXT>`
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
---
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
---
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 <CASES> --pred <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
---
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
---
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
---
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
---
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
---
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-name>/SKILL.md - Subagents:
.cursor/agents/*.md
- Skills:
-
Cursor (User)
- Skills:
~/.cursor/skills/<skill-name>/SKILL.md - Subagents:
~/.cursor/agents/*.md
- Skills:
-
Claude Code (참고)
- Project:
.claude/skills/<skill-name>/SKILL.md,.claude/agents/*.md
- Project:
-
OpenAI Codex (참고)
- Repo/User:
.codex/skills또는$HOME/.codex/skills(환경별)
- Repo/User:
-
GitHub Copilot (참고)
- Project:
.github/skills/<skill-name>/SKILL.md(조직 정책에 맞게)
- Project:
8) 검증 체크리스트 + validator 예시
최소 체크(필수)
- 모든 SKILL.md frontmatter
name이 폴더명과 동일(소문자/숫자/하이픈) - 고위험 스킬(train/deploy/orchestrator)에
disable-model-invocation: true적용 - 파괴적 작업은 DRY_RUN→승인→APPLY 문구/절차가 명시
- PII 스캔 도구가 읽기 전용이며 ExitCode(2/10)를 준수
로컬 실행 예시(사용자 실행)
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)”**까지 포함한 “멀티타겟 풀세팅”도 같은 규격으로 바로 확장해 줄 수 있다.