mdx_to_storage_xhtml_verify_cli.py는 현재 pass/fail 이진 판정과 unified diff 텍스트만 제공한다.
xhtml_beautify_diff.py의 beautify_xhtml() + xhtml_diff()를 이미 사용하고 있지만,
정량적 유사도 측정은 없다.
- 21개 testcase가 모두 fail이지만, 각 케이스의 심각도를 구분할 수 없다
- "1줄 차이" vs "50줄 차이"가 동일하게 fail로 분류된다
- 개선 작업의 진척도를 수치로 추적할 수 없다 (pass 수만으로는 불충분)
- 우선순위 판단에 정량적 근거가 부족하다
beautified XHTML의 전체 라인 수 대비 diff에서 차이나는 줄 수를 line-level edit distance로 간주하여, 두 XHTML 간의 차이를 0.0~1.0 similarity 값으로 정량화한다.
reference_lines = len(beautify_xhtml(normalized_page_xhtml).splitlines())
changed_lines = count of (+/-) lines in unified diff (excluding +++/---/@@ headers)
edit_ratio = changed_lines / (2 * reference_lines) # /2: 변경 1건 = -1줄 +1줄
similarity = clamp(1.0 - edit_ratio, 0.0, 1.0)
왜 2 * reference_lines로 나누는가?
- unified diff에서 한 줄이 변경되면
-1줄 ++1줄 = 2줄이 발생한다 - 순수 삭제(- only)나 순수 추가(+ only)도 있으므로, 이 공식은 근사값이다
- 그러나 라인 수 기반 비교에서 실용적으로 충분한 정밀도를 제공한다
edge case 처리:
reference_lines == 0 and changed_lines == 0→ similarity = 1.0reference_lines == 0 and changed_lines > 0→ similarity = 0.0edit_ratio > 1.0(추가가 삭제보다 많은 경우) → clamp to 0.0
@dataclass
class DiffMetrics:
total_lines: int # beautified page.xhtml 전체 라인 수 (기준)
changed_lines: int # unified diff에서 +/- 라인 수 (헤더 제외)
edit_distance: int # = changed_lines (절대 편집 거리)
edit_ratio: float # changed_lines / (2 * total_lines)
similarity: float # clamp(1.0 - edit_ratio, 0.0, 1.0)| 시나리오 | total_lines | changed_lines | edit_ratio | similarity |
|---|---|---|---|---|
| 완전 일치 | 50 | 0 | 0.000 | 1.000 |
| 속성 1개 차이 | 50 | 2 | 0.020 | 0.980 |
| 테이블 구조 불일치 | 50 | 20 | 0.200 | 0.800 |
| 완전 불일치 | 50 | 100 | 1.000 | 0.000 |
| 파일 | 변경 유형 | 설명 |
|---|---|---|
bin/reverse_sync/mdx_to_storage_xhtml_verify.py |
수정 | DiffMetrics 추가, CaseVerification에 metrics 필드 추가, 집계 로직 |
bin/mdx_to_storage_xhtml_verify_cli.py |
수정 | --show-metrics 플래그, 메트릭 출력, 분석 리포트에 포함 |
tests/test_mdx_to_storage_xhtml_verify.py |
수정 | metrics 계산/집계 테스트 추가 |
파일: bin/reverse_sync/mdx_to_storage_xhtml_verify.py
@dataclass
class DiffMetrics:
total_lines: int
changed_lines: int
edit_distance: int
edit_ratio: float
similarity: float
def compute_diff_metrics(
page_norm: str,
generated_norm: str,
diff_lines: list[str],
) -> DiffMetrics:
"""beautified/normalized XHTML 간의 정량적 차이를 계산한다."""
total = len(page_norm.splitlines())
changed = 0
for line in diff_lines:
if line.startswith(("---", "+++", "@@")):
continue
if line.startswith(("+", "-")):
changed += 1
if total == 0:
edit_ratio = 0.0 if changed == 0 else 1.0
else:
edit_ratio = changed / (2 * total)
similarity = max(0.0, min(1.0, 1.0 - edit_ratio))
return DiffMetrics(
total_lines=total,
changed_lines=changed,
edit_distance=changed,
edit_ratio=round(edit_ratio, 4),
similarity=round(similarity, 4),
)@dataclass
class CaseVerification:
case_id: str
passed: bool
generated_xhtml: str
diff_report: str
metrics: DiffMetrics | None = None # NEW — 기존 코드 하위호환현재 시그니처:
def verify_expected_mdx_against_page_xhtml(
expected_mdx, page_xhtml, ...
) -> tuple[bool, str, str]:변경:
def verify_expected_mdx_against_page_xhtml(
expected_mdx, page_xhtml, ...
) -> tuple[bool, str, str, DiffMetrics]:내부에서 compute_diff_metrics(page_norm, generated_norm, diff_lines) 호출하여 반환.
verify_testcase_dir()에서 이를 받아 CaseVerification.metrics에 저장.
@dataclass
class VerificationSummary:
total: int
passed: int
failed: int
failed_case_ids: list[str]
by_priority: dict[str, int]
by_reason: dict[str, int]
analyses: list[FailureAnalysis]
# NEW metrics
avg_similarity: float # 전체 케이스 평균 similarity
median_similarity: float # 중앙값
min_similarity: float # 최소 similarity
max_similarity: float # 최대 similarity
weighted_pass_rate: float # sum(similarity) / total — 가중 합격률summarize_results()에서 모든 케이스의 metrics.similarity를 모아 계산:
similarities = [r.metrics.similarity for r in results if r.metrics]
summary.avg_similarity = statistics.mean(similarities) if similarities else 0.0
summary.median_similarity = statistics.median(similarities) if similarities else 0.0
summary.min_similarity = min(similarities) if similarities else 0.0
summary.max_similarity = max(similarities) if similarities else 0.0
summary.weighted_pass_rate = sum(similarities) / len(similarities) if similarities else 0.0새 플래그: --show-metrics
parser.add_argument(
"--show-metrics",
action="store_true",
help="Show quantitative similarity metrics per case and in aggregate",
)출력 예시:
[mdx->xhtml-verify] total=21 passed=5 failed=16
[metrics] avg_similarity=0.847 median=0.912 min=0.423 max=1.000
[metrics] weighted_pass_rate=0.891
Case similarity ranking (worst first):
544375741 similarity=0.423 changed=58/50 lines
544375859 similarity=0.567 changed=34/40 lines
...
544375123 similarity=1.000 changed=0/30 lines
_format_analysis_report() 확장:
# MDX -> Storage XHTML Batch Verify Analysis
- total: 21
- passed: 5
- failed: 16
## Similarity Metrics
- avg_similarity: 0.847
- median_similarity: 0.912
- min_similarity: 0.423
- max_similarity: 1.000
- weighted_pass_rate: 0.891
## Priority Summary
...
## Failed Cases (by similarity, ascending)
- 544375741: P1 (similarity=0.423, changed=58 lines) — internal_link_unresolved
- 544375859: P2 (similarity=0.567, changed=34 lines) — emoticon_representation_mismatch
...| 테스트 | 설명 |
|---|---|
test_compute_diff_metrics_identical |
동일 XHTML → changed=0, similarity=1.0 |
test_compute_diff_metrics_partial_diff |
일부 차이 → 0 < similarity < 1 |
test_compute_diff_metrics_completely_different |
완전 불일치 → similarity ≈ 0.0 |
test_compute_diff_metrics_empty_reference |
빈 page.xhtml → edge case 처리 |
test_case_verification_includes_metrics |
verify_testcase_dir() 결과에 metrics 포함 확인 |
test_summarize_results_aggregate_metrics |
집계 메트릭 (avg, median, min, max) 정확성 |
verify_expected_mdx_against_page_xhtml() 반환 타입이 3-tuple → 4-tuple로 변경되므로,
기존 호출부 수정 필요:
# 기존
passed, generated, diff_report = verify_expected_mdx_against_page_xhtml(...)
# 변경
passed, generated, diff_report, metrics = verify_expected_mdx_against_page_xhtml(...)- 진척도 추적:
weighted_pass_rate를 매 iteration마다 기록하여 개선 추이 확인 - 우선순위 결정: similarity가 낮은 케이스 = 차이가 큰 케이스 → 구조적 문제 우선 해결
- 회귀 검출: similarity가 떨어지면 새 변경이 기존 변환을 깨뜨린 것
- 완성도 보고: "21개 케이스 중 가중 합격률 89.1%" 같은 정량적 보고 가능
- 단위 테스트:
pytest tests/test_mdx_to_storage_xhtml_verify.py -v - 메트릭 확인:
python bin/mdx_to_storage_xhtml_verify_cli.py \ --pages-yaml var/pages.yaml \ --show-metrics \ --show-analysis
- 리포트 생성:
python bin/mdx_to_storage_xhtml_verify_cli.py \ --pages-yaml var/pages.yaml \ --show-metrics \ --write-analysis-report reports/metrics-baseline.md
- 각 케이스의 similarity 값이 직관적 기대와 일치하는지 수동 검증