Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 14 additions & 33 deletions .vibeready.example.json
Original file line number Diff line number Diff line change
@@ -1,40 +1,21 @@
{
"_comment": "vibe-ready 평가 설정 예시. 기본 7개 카테고리의 기본값을 그대로 담았습니다. 필요한 항목만 수정/제거하거나, optional 항목을 추가하세요.",
"_tier": "tier 값: 'must'=필수(F 등급이면 종합 등급이 C로 제한), 'nice'=권장(가중평균에 반영), 'optional'=선택(등급에 영향 없음, 점수에 비례해 bonusCap 한도 내 가산점만 부여).",
"_adopt": "항목 채용=categories에 포함. 미채용=배열에서 제거(분석·채점 제외). 선택항목=tier를 'optional'로 지정하고 bonusCap 설정.",
"_weight": "must/nice의 weight는 합이 1.0이 아니어도 자동 정규화됩니다. optional은 weight 대신 bonusCap(최대 가산점)을 사용합니다.",
"categories": [
{
"name": "테스트 커버리지",
"tier": "must",
"weight": 0.20
},
{
"name": "CI/CD",
"tier": "must",
"weight": 0.20
},
{
"name": "훅 기반 검증",
"tier": "must",
"weight": 0.20
},
{
"name": "리포지토리 구조",
"tier": "nice",
"weight": 0.13
},
{
"name": "문서화 수준",
"tier": "nice",
"weight": 0.13
},
{
"name": "하네스 엔지니어링",
"tier": "nice",
"weight": 0.14
},
{ "name": "테스트 커버리지", "tier": "must", "weight": 0.20 },
{ "name": "CI/CD", "tier": "must", "weight": 0.20 },
{ "name": "훅 기반 검증", "tier": "must", "weight": 0.20 },
{ "name": "리포지토리 구조", "tier": "nice", "weight": 0.10 },
{ "name": "문서화 수준", "tier": "nice", "weight": 0.10 },
{ "name": "하네스 엔지니어링", "tier": "nice", "weight": 0.10 },
{ "name": "이슈 트래킹 연동", "tier": "nice", "weight": 0.10 },
{
"name": "보안 설정",
"tier": "must",
"weight": 0.15,
"description": "리포지토리의 보안 관련 설정을 평가합니다",
"tier": "optional",
"bonusCap": 5,
"description": "선택항목 예시: 없어도 등급에 영향을 주지 않지만, 갖추면 최대 5점까지 가산됩니다.",
"checkpoints": [
".env 파일이 .gitignore에 포함되어 있는가",
"시크릿/API 키가 소스코드에 하드코딩되어 있지 않은가",
Expand Down
10 changes: 8 additions & 2 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,11 @@ npm test
]
},
{ "name": "문서화 수준", "tier": "nice", "weight": 0.15 },
{ "name": "하네스 엔지니어링", "tier": "nice", "weight": 0.15 }
{ "name": "하네스 엔지니어링", "tier": "nice", "weight": 0.15 },
{ "name": "보안 강화", "tier": "optional", "bonusCap": 5,
"description": "선택항목: 등급에 영향을 주지 않고, 점수에 비례해 최대 5점 가산",
"checkpoints": [".env가 .gitignore에 포함", "의존성 취약점 스캔 설정 여부"]
}
],
"penaltyRule": {
"enabled": true,
Expand All @@ -124,7 +128,9 @@ npm test

- 기본 카테고리의 가중치/tier 변경 가능
- `description` + `checkpoints`로 커스텀 카테고리 추가 가능
- 가중치 합계가 1.0이 아니면 자동 정규화
- **tier 구분**: `must`(F 등급이면 종합 등급이 C로 제한), `nice`(가중평균에 반영), `optional`(등급을 낮추지 않음 — 점수에 비례해 `bonusCap` 한도 내 가산점만 부여하므로 총점·등급이 올라갈 수 있음)
- **채용 / 미채용 / 선택항목**: 항목을 `categories`에 두면 채용, 배열에서 빼면 미채용(분석·채점 제외), `tier: "optional"` + `bonusCap`을 지정하면 가산점 전용 선택항목
- 가중치 합계가 1.0이 아니면 자동 정규화 (`optional`은 정규화 대상에서 제외되고 `weight` 대신 `bonusCap` 사용)
- 지원 파일명: `.vibeready.json`, `.vibeready.config.json`, `vibeready.config.json`
- 전체 예시: [.vibeready.example.json](.vibeready.example.json)

Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,11 @@ Create a `.vibeready.json` in your repo root to customize evaluation:
]
},
{ "name": "Documentation", "tier": "nice", "weight": 0.15 },
{ "name": "Harness Engineering", "tier": "nice", "weight": 0.15 }
{ "name": "Harness Engineering", "tier": "nice", "weight": 0.15 },
{ "name": "Security", "tier": "optional", "bonusCap": 5,
"description": "Optional item: never lowers the grade; adds up to 5 bonus points if present",
"checkpoints": [".env is in .gitignore", "Dependency vulnerability scanning configured"]
}
],
"penaltyRule": {
"enabled": true,
Expand All @@ -126,7 +130,9 @@ Create a `.vibeready.json` in your repo root to customize evaluation:

- Override default category weights and tiers
- Add custom categories with `description` and `checkpoints`
- Weights are auto-normalized if they don't sum to 1.0
- **Tiers**: `must` (F caps the overall grade at C), `nice` (counts toward the weighted average), `optional` (never lowers the grade — adds bonus points proportional to its score, up to `bonusCap`, which can raise the total)
- **Adopt / skip / optional**: include a category to adopt it; remove it from `categories` to skip it (excluded from analysis); set `tier: "optional"` with a `bonusCap` to make it a bonus-only item
- Weights are auto-normalized if they don't sum to 1.0 (`optional` categories are excluded from normalization and use `bonusCap` instead of `weight`)
- `"agent"` pins Harness Engineering to one coding agent (`claude` / `codex` / `cursor` / `copilot`). Omit it for auto-detection. The `--agent` CLI flag overrides this field.
- Supported filenames: `.vibeready.json`, `.vibeready.config.json`, `vibeready.config.json`
- See [.vibeready.example.json](.vibeready.example.json) for a full example
Expand Down
39 changes: 30 additions & 9 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import { normalizeAgent, SUPPORTED_AGENTS } from "./agents.js";
export interface CategoryConfig {
name: string;
tier: CategoryTier;
/** 가중평균에 반영되는 비중(must/nice). optional 카테고리에서는 무시되며 0으로 취급. */
weight: number;
/** optional 카테고리 전용: 점수 비례로 총점에 더해지는 최대 가산점. */
bonusCap?: number;
description?: string;
checkpoints?: string[];
}
Expand Down Expand Up @@ -51,12 +54,12 @@ export function getEffectiveCategories(config: VibeReadyConfig | null): Category
}));
}

export function getEffectiveWeights(config: VibeReadyConfig | null): Record<string, { tier: CategoryTier; weight: number }> {
export function getEffectiveWeights(config: VibeReadyConfig | null): Record<string, { tier: CategoryTier; weight: number; bonusCap?: number }> {
if (!config) return CATEGORY_WEIGHTS;

const weights: Record<string, { tier: CategoryTier; weight: number }> = {};
const weights: Record<string, { tier: CategoryTier; weight: number; bonusCap?: number }> = {};
for (const cat of config.categories) {
weights[cat.name] = { tier: cat.tier, weight: cat.weight };
weights[cat.name] = { tier: cat.tier, weight: cat.weight, bonusCap: cat.bonusCap };
}
return weights;
}
Expand All @@ -78,23 +81,41 @@ function validateConfig(raw: unknown): VibeReadyConfig {
const c = cat as Record<string, unknown>;

if (typeof c.name !== "string") throw new Error("카테고리 name이 필요합니다");
if (c.tier !== "must" && c.tier !== "nice") throw new Error(`카테고리 tier는 "must" 또는 "nice"여야 합니다: ${c.name}`);
if (typeof c.weight !== "number" || c.weight <= 0) throw new Error(`카테고리 weight는 양수여야 합니다: ${c.name}`);
if (c.tier !== "must" && c.tier !== "nice" && c.tier !== "optional") {
throw new Error(`카테고리 tier는 "must", "nice", "optional" 중 하나여야 합니다: ${c.name}`);
}

const isOptional = c.tier === "optional";
let bonusCap: number | undefined;
if (isOptional) {
// optional: weight 대신 bonusCap(가산점 상한)을 사용. weight는 무시(0).
if (typeof c.bonusCap !== "number" || c.bonusCap <= 0) {
throw new Error(`optional 카테고리는 양수 bonusCap이 필요합니다: ${c.name}`);
}
bonusCap = c.bonusCap;
} else if (typeof c.weight !== "number" || c.weight <= 0) {
throw new Error(`카테고리 weight는 양수여야 합니다: ${c.name}`);
}

categories.push({
name: c.name,
tier: c.tier as CategoryTier,
weight: c.weight,
weight: isOptional ? 0 : (c.weight as number),
bonusCap,
description: typeof c.description === "string" ? c.description : undefined,
checkpoints: Array.isArray(c.checkpoints) ? c.checkpoints.map(String) : undefined,
});
}

// 가중치 합계 정규화
const totalWeight = categories.reduce((sum, c) => sum + c.weight, 0);
// 가중치 합계 정규화 (optional은 가중평균에 들어가지 않으므로 제외)
const scorable = categories.filter((c) => c.tier !== "optional");
if (scorable.length === 0) {
throw new Error("must/nice 카테고리가 최소 1개 필요합니다 (optional만으로는 구성할 수 없음)");
}
const totalWeight = scorable.reduce((sum, c) => sum + c.weight, 0);
if (Math.abs(totalWeight - 1.0) > 0.01) {
const scale = 1.0 / totalWeight;
for (const c of categories) {
for (const c of scorable) {
c.weight = Math.round(c.weight * scale * 1000) / 1000;
}
}
Expand Down
Loading
Loading