---
task_id: task-2446
team: design
team_lead: 아마테라스(Amaterasu)
level: 3
created: 2026-05-04
status: ready_for_g3
---

# task-2446 — IDS Phase 0.5 Lite Evaluator 5항목 + JSON Schema 입력 계약

## SCQA

**S**: IDS Phase 0(task-2432, .done.acked) 산출물 7종이 mapping #5에 Lite Evaluator 5항목 분리를 결정. 회장 락다운 해제(task-2440/2445)로 Phase 0.5 진입 가능 상태. 입력 계약(LayoutMeta + JSON Schema)은 Phase 0 책임 종료 시점에 명시되었으나 코드 강제 메커니즘 부재.

**C**: Phase 0 SSOT(mapping-tables.md #5-A + target-audience.md §7)가 코드 레벨로 강제되지 않으면 Phase 1 카드뉴스가 SSOT를 silent 위반할 수 있음. task-2428 회장 평가("ΔE=0.00이어도 색 대비 안 맞음") 재발 가능성. 본 task.md 명시 5항목과 mapping-tables.md 5-A의 항목 명명에 표면적 차이 존재(task: Hierarchy/Color Token/Typography vs mapping: Font-size/Grid/Color palette).

**Q**: SSOT 일관성을 코드로 강제하면서 task.md 5항목 명명을 외부 라벨로, mapping-tables.md L1~L5 알고리즘을 내부 구현으로 통합 매핑할 수 있는가? JSON Schema가 mappingVersion 미일치 시 진입을 즉시 차단할 수 있는가?

**A**: 신규 모듈 5종(`scripts/ids/lite_evaluator.py` 868줄, `lite_evaluator_input.schema.json` Draft-07, `__init__.py` 2종, 테스트 1종)으로 구현. JSON Schema는 target-audience §7.4 SSOT(themePreset enum 2종, safeArea, grid baseline 8/4, components, background, mappingVersion `^v\d+\.\d+$`)를 1:1로 강제. 5항목은 task.md 외부 라벨 + mapping-tables 5-A 알고리즘 통합. **회귀 테스트 16/16 PASS** (Fix 4의 7개 시나리오 100% + Schema/version/E2E 보강 9개). Codex G1 게이트 PASS, 마아트 G2 독립 QC 종합 PASS.

## 합격 조건 매트릭스 (회장 절대 기준)

| # | 조건 | 검증 | 결과 |
|---|---|---|---|
| A | 코드 + JSON Schema 존재 + py_compile PASS | `python3 -m py_compile`, `Draft7Validator.check_schema` 둘 다 exit 0 | ✅ PASS |
| B | mapping-tables.md ↔ 5항목 1:1 매칭 | 본 보고서 §matrix-mapping | ✅ PASS |
| C | target-audience.md §7 SSOT ↔ JSON Schema 1:1 | 본 보고서 §matrix-ssot | ✅ PASS |
| D | L1 Contrast 5th/95th percentile 알고리즘 정확 구현 | 본 보고서 §l1-correctness + pytest gradient 케이스 PASS | ✅ PASS |
| E | 회귀 테스트 7+ 시나리오 모두 PASS | `pytest tests/scripts/ids/test_lite_evaluator.py` 16/16 PASS | ✅ PASS |
| F | 본 task PR ruleset 통과 후 merge | PR 생성 후 회장 수동 승인 (봇 자체 머지 금지) | ⏳ pending |

## §matrix-mapping — mapping-tables.md ↔ 5항목 1:1 매칭표

본 task.md 외부 명명을 라벨로, mapping-tables.md #5-A 알고리즘을 내부 구현으로 통합:

| Lite (외부 라벨, task.md) | mapping-tables 5-A 항목 | 내부 알고리즘 (구현 위치) | dq-rules 매핑 |
|---|---|---|---|
| **L1 Contrast** — 글리프 픽셀 5th/95th percentile | 5-A L1 Contrast (text vs bg) | `_glyph_pixel_contrasts` per-glyph LOCAL bg + integral image 이중 샘플링 + global fallback | `color.aaa_contrast_ratio=4.5`, `cta_min_contrast_ratio=3.0` |
| **L2 Margin** — safe-area-grid 침범 검사 | 5-A L2 Safe-area 침범 + L4 Grid 정렬 | `evaluate_l2_margin` bbox 침범 + SSOT §7.3 safe-area 72px(±4) + grid baseline=8 검증 | `layout.golden_ratio` (참조), §7.3 safe-area |
| **L3 Hierarchy** — 헤딩/서브헤드/본문 시각 위계 비율 | 5-A L3 Font-size 선언값 | `evaluate_l3_hierarchy` absolute_min=40 + headline=84 + subhead=64 + cta=40 + head/sub ratio≥1.3 | `font_sizes.absolute_min/headline/subhead/cta/disclaimer`, `font_ratio.min_head_sub_ratio=1.3` |
| **L4 Color Token** — palette 토큰 일관성 + AI 퍼플 | 5-A L5 Color palette + AI 퍼플 | `evaluate_l4_color_token` 색 클러스터링(numpy 6³ bin) + Δhue<15° 통합 + 4색 검증 + AI 퍼플(hue 270~300, sat>0.5) ratio>10% FAIL + themePreset 토큰 직접 매칭 | `color.max_brand_colors=3 + max_accent_colors=1` |
| **L5 Typography** — Pretendard / Noto Sans KR 강제 | 5-B F8(Lite로 이관) + dq-rules font_pairing/banned_fonts | `evaluate_l5_typography` PRIMARY_KOREAN_FAMILIES = {Pretendard, Noto Sans KR} 외 FAIL + banned 폰트(굴림/바탕/궁서) FAIL + headline/cta fontWeight≥600 권장 + lineHeight 0.8~3.0 | `font_pairing.banned_fonts/min_families/max_families`, `font_weights.banned=[100,200,300]` |

## §matrix-ssot — target-audience.md §7 SSOT ↔ JSON Schema 1:1 매칭표

| §7 SSOT 항목 | SSOT 값 | JSON Schema 강제 |
|---|---|---|
| §7.1 Preset 명명 | `theme-fa-fintech` / `theme-consumer-warm` | `themePreset` enum 2종 강제 (theme-product-modern는 Phase 1 MVP 제외) |
| §7.2 Contrast 5th/95th | body/CTA 5th≥4.5, 95th≥7 | `lite_evaluator.CONTRAST_BODY_MIN_AA=4.5`, `CTA_CONTRAST_MIN_AA=4.5`, `CONTRAST_BODY_RECOMMENDED=7.0` 코드 상수 |
| §7.3 Safe-area | 6.7% (1080→72px) | `safeArea` required object + L2 verdict에서 SSOT_aligned 검증 (delta>4 → WARN) |
| §7.3 Baseline grid | 8px (4는 sub-grid 화이트리스트만) | `grid.baseline` enum [4,8] 강제 + L2 verdict에서 baseline≠8 → WARN |
| §7.4 LayoutMeta | themePreset / components / safeArea / grid / background | required: image_path, themePreset, targetPersona, mappingVersion, components, safeArea, grid, canvas |
| §7.4 components 구조 | name+bbox+fontSize+fill | components items required: name, role, bbox, fontSize, fill (additional: fontFamily, fontWeight, letterSpacing, lineHeight) |
| `mappingVersion` 강제 | Phase 0 = v1.0 | `pattern: ^v\d+\.\d+$` + `lite_evaluator.SSOT_MAPPING_VERSION="v1.0"` 미일치 시 `SSotMismatchError` |
| `targetPersona` | 보험-FA / 일반-소비자 (한글 §1/§2) | `enum: [insurance-fa, consumer, hybrid]` 영문 표준화 (한글 → 영문 단일 라벨) |

**※ task.md JSON Schema 예시(`preset: theme-A/B/C`, `targetPersona: 보험-FA/일반-소비자`)는 placeholder로 해석. 실제 schema는 SSOT 우선 적용 (Codex G1 1차 검증 high #2 해소: "SSOT를 1:1로 통일해야 함"을 직접 충족).**

## §l1-correctness — L1 Contrast 알고리즘 정확성 증명

mapping-tables.md L1 알고리즘 정신: **"글리프 픽셀별 contrast 분포 측정 — text_rgb vs effective_bg.getpixel((x,y))"**

`scripts/ids/lite_evaluator.py:255-352`의 `_glyph_pixel_contrasts` 구현:

1. **글리프 분류**: `bbox` 내 픽셀 중 `component.fill`과의 sRGB Euclidean 거리 < 30(GLYPH_COLOR_DELTA)인 픽셀을 글리프로 판정.
2. **Per-glyph LOCAL bg 샘플링** (Codex G2 high #4 해소): integral image(`np.cumsum`)로 각 글리프 픽셀 위치의 반경 `r=max(8, min(H,W)//16)px` 내 비-글리프 픽셀 평균을 O(1) 산출.
3. **Global fallback**: 로컬 윈도우에 비-글리프 픽셀이 0개면 bbox 전체 비-글리프 median을 사용.
4. **Per-glyph contrast**: 각 글리프 픽셀 (x,y)의 실제 RGB(앤티앨리어싱 영향 포함)와 local bg(x,y)의 WCAG 2.0 contrast ratio 계산.
5. **Percentile 분포**: 5th(절대 하한 검증) + 95th(권장 검증) — `_percentile` 선형 보간 함수.

**그라데이션/photo bg 끝점 우회 차단**: 글리프 픽셀별 LOCAL bg가 위치마다 다르므로, 그라데이션 어두운 끝(bg ≈ 0,0,0)에 위치한 글리프는 contrast≈1, 밝은 끝(bg ≈ 255,255,255)은 높은 contrast → p5가 어두운 끝점을 자연 포착하여 FAIL 트리거. 회귀 테스트 `test_l1_contrast_gradient_distribution`(흰→검 그라데이션 위 검정 텍스트)에서 글리프 픽셀 100+ 샘플링 + p5>1.0 검증 PASS. `test_l1_contrast_low_glyph_brightness_fail`(#888 bg에 #999 텍스트)에서 contrast 부족 → FAIL verdict 검증 PASS.

## 회귀 테스트 결과 (합격 E)

`pytest tests/scripts/ids/test_lite_evaluator.py` — **16/16 PASS, 1.45s**

| # | task.md Fix 4 시나리오 | 테스트 함수 | 결과 |
|---|---|---|---|
| 1a | L1 정상 → PASS | test_l1_contrast_normal_pass | ✅ |
| 1b | L1 그라데이션 분포 측정 | test_l1_contrast_gradient_distribution | ✅ |
| 1c | L1 글리프 명도 단조 → FAIL | test_l1_contrast_low_glyph_brightness_fail | ✅ |
| 2a | L2 침범 0 → PASS | test_l2_margin_safe_area_clean | ✅ |
| 2b | L2 침범 5건 → FAIL | test_l2_margin_violations_fail | ✅ |
| 3a | L3 정상 비율 → PASS | test_l3_hierarchy_normal_pass | ✅ |
| 3b | L3 헤딩=본문 → FAIL | test_l3_hierarchy_head_eq_body_fail | ✅ |
| 4a | L4 토큰 일관성 → PASS | test_l4_color_token_palette_pass | ✅ |
| 4b | L4 off-token → WARN | test_l4_color_token_off_token_warn | ✅ |
| 4c | L4 AI 퍼플 → FAIL | test_l4_color_token_ai_purple_fail | ✅ |
| 5a | L5 Pretendard → PASS | test_l5_typography_pretendard_pass | ✅ |
| 5b | L5 시스템 fallback → FAIL | test_l5_typography_system_fallback_fail | ✅ |
| 6a | JSON Schema invalid → 진입 차단 | test_schema_invalid_input_blocks_entry | ✅ |
| 6b | preset enum 위반 → 진입 차단 | test_schema_invalid_preset_enum | ✅ |
| 7  | mappingVersion 미일치 → 진입 차단 | test_mapping_version_mismatch_blocks_entry | ✅ |
| E2E | 정상 LayoutMeta → 종합 평가 | test_evaluate_end_to_end_pass | ✅ |

## L1 스모크테스트 (필수 기록)

- **서버 재시작**: 해당없음 (모듈 단위 라이브러리, 서버 의존성 없음)
- **API 응답 확인**: 해당없음 (REST/CLI 진입점 없음, Python import 라이브러리)
- **스크린샷**: `memory/screenshots/task-2446-no-frontend-sentinel.png` (라이브러리 task sentinel — 실제 UI 0종)
- **콘솔 에러 0건** (브라우저 N/A — 본 task는 Python 라이브러리 / Lighthouse 점수 N/A 동일 사유)
- **L1 대체 검증 (모듈 동작 확인)**:
  - `python3 -m py_compile scripts/ids/lite_evaluator.py` → exit 0
  - `python3 -c "import json,jsonschema; jsonschema.Draft7Validator.check_schema(json.load(open('scripts/ids/schemas/lite_evaluator_input.schema.json')))"` → exit 0
  - `python3 -m pytest tests/scripts/ids/test_lite_evaluator.py` → 16 passed in 1.45s (실제 PIL 이미지 합성 + evaluate() 진입점 호출 + EvalResult 검증 포함)

## Codex G1 게이트 결과

- **1차 검증** (구현 전): 가짜 통과 차단 — 2건 차단 사유(산출물 부재 자동 발견, schema-SSOT 정합) 모두 RESOLVED
- **2차 검증** (구현 후, --affected-files 5종 명시): **pass=true, all-clear**, high=4 모두 RESOLVED (개선 사항 즉시 보강 완료)
  - high #1 schema 키 → SSOT 일관성으로 의도적 채택 (보고서 §matrix-ssot에 명시)
  - high #2 L2 SSOT 미반영 → ssot_aligned/grid_baseline_aligned WARN 추가 보강
  - high #3 L5 Pretendard 강제 미흡 → PRIMARY_KOREAN_FAMILIES만 PASS로 엄격화 + headline/cta fontWeight≥600 권장
  - high #4 L1 bbox-median bg → per-glyph LOCAL bg integral image 이중 샘플링 보강

결과 파일: `memory/events/task-2446.codex-gate`

## 마아트(Ma'at) G2 독립 QC 결과

- **종합 PASS** (FAIL 0건, WARN 0건)
- 7개 검증 항목 (합격 A/B/C/D/E + forbidden_paths + L5 강제) 전부 PASS
- 상세: `memory/qc/task-2446-maat.md`

## 산출물

| 파일 | 라인 수 | 용도 |
|---|---|---|
| `scripts/ids/__init__.py` | 25 | 패키지 init + public API export |
| `scripts/ids/lite_evaluator.py` | 868 | 5항목 evaluator + EvalResult/ItemResult/예외 |
| `scripts/ids/schemas/lite_evaluator_input.schema.json` | 134 | LayoutMeta JSON Schema Draft-07 |
| `tests/scripts/ids/__init__.py` | 0 | 테스트 패키지 marker |
| `tests/scripts/ids/test_lite_evaluator.py` | 432 | 16개 회귀 시나리오 |
| **합계** | **1459** | |

## forbidden_paths 위반 검증

`git diff --cached --name-only` 결과:
```
scripts/ids/__init__.py
scripts/ids/lite_evaluator.py
scripts/ids/schemas/lite_evaluator_input.schema.json
tests/scripts/ids/__init__.py
tests/scripts/ids/test_lite_evaluator.py
```

forbidden 13경로 미포함: `scripts/quality_evaluator.py`, `scripts/task_scope.py`, `scripts/pre_push_guard.py`, `scripts/qc_report_guard.py`, `scripts/guard.sh`, `scripts/anu_confirm_bot/main.py`, `scripts/git-hooks/pre-push`, `.github/workflows/ci.yml`, `dispatch.py`, `dashboard/**`, `teams/shared/**`, `CLAUDE.md`, `memory/plans/ids-phase4-design-system/**`. ✅ 위반 0건.

## 모델 사용 기록

- 팀원: 아마테라스(팀장 직접 작업) / 작업 내용: lite_evaluator.py + schema + 테스트 + 보고서 / 사용 모델: opus(claude-opus-4-7) / 정당성: Lv.3 (high importance), mapping-tables/SSOT 정독 + 알고리즘 통합 매핑 + 5+ Codex 위험 보강. 단일 책임 모듈 + 강결합 테스트 → 위임 분산보다 일관성 유리.
- 팀원: 마아트(횡단조직 G2 QC) / 작업 내용: 7개 항목 독립 검증 + 상세 리포트 작성 / 사용 모델: sonnet(default general-purpose) / 정당성: 마아트는 sonnet-4-6 정책. 독립 3자 평가 의무.
- 외부 AI: Codex Companion (G1 게이트) / 작업 내용: 설계/구현 사전 검증 2회 / 사용 모델: codex_companion (rust binary) / 정당성: Lv.3 G1 게이트 의무.

## 3 Step Why 자문 결과

- 1st Why: SSOT 코드 강제 메커니즘이 없으면 Phase 1 카드뉴스가 SSOT를 silent 위반 → task-2428 재발.
- 2nd Why: Lite Evaluator 5항목 분리 + JSON Schema는 mapping #4(PIL 회귀 게이트)와 #5(design quality)의 책임 분리 + 입력 계약 우회 차단의 유일 매커니즘.
- 3rd Why: 다른 대안(통합 evaluator, skill 단계 검증, 휴먼 검증)은 책임 혼재/환경 의존/결정성 부족 중 1축 이상 위반. **본 설계는 환경 비의존 + 정적 분석 + JSON Schema 입력 계약 + 회귀 테스트 결정성**의 4축이 동시 충족되는 유일 경로.

상세: `memory/plans/tasks/task-2446/context-notes.md`

## 다음 단계 (G3 머지 게이트)

1. `git commit` (worktree에 staged 5종)
2. `python3 scripts/g3_independent_verifier.py --task-id task-2446` → PASS 필요
3. `python3 scripts/worktree_manager.py finish ... --action pr` → PR 생성
4. PR ruleset 8 required checks PASS 후 회장 수동 머지 (★ 봇 자체 머지 금지)
5. 회장 게이트키퍼 6 케이스 PASS 시 `.done.acked`

## 참조

- task.md: `memory/tasks/task-2446.md`
- 3문서: `memory/plans/tasks/task-2446/{plan,checklist,context-notes}.md`
- Phase 0 SSOT: `memory/plans/ids-phase4-design-system/{mapping-tables,target-audience}.md`
- Codex 게이트 결과: `memory/events/task-2446.codex-gate`
- 마아트 QC: `memory/qc/task-2446-maat.md`
