Files
C.E.L_Slide_test2/tests/matching/pipeline_17b_extend_missing_sections.py
T
KyeongminandClaude Opus 4.8 9b79bf7538 feat(#17): generic fallback 탈출 — V4 evidence 확장 + renderable-aware provisional + 게이트 순서 버그 수정
1. V4 evidence 확장 (pipeline_17b_extend_missing_sections.py):
   '결과물이 아니라 프로세스' 원칙 — pipeline_17 과 동일 평가 코드로 누락 3개
   섹션(01-intro/05-1/05-2)만 평가해 v4_full32_result.yaml 병합 (기존 무접촉,
   blind/ANSWER_MAP 불변). 결과: 05-1 → F20 light_edit 0.77 (design-matched!),
   01-intro/05-2 → all-reject (catalog gap 정직 노출 — F19 가 05 주제와 이름까지
   일치하나 partial 없음 → #2 프로모션 최우선 근거)

2. renderable-aware provisional (IMP-30 u1 정밀화):
   rank-1 무조건 승격 → partial 존재 AND (비-reject OR verbatim builder 보유)
   첫 후보 승격. reject+builder 미보유 renderable 의 mapper 네이티브 렌더는
   원문 drop 위험 (F23 1-atom 손실 실측) — 원문 보존 > design 개선 우선순위.

3. 게이트 순서 버그 수정 (_apply_quality_gate_downgrades 추출):
   T28.5d popup 승격 후 재계산 경로에 quality gate 강등 3종(coverage/forbidden/
   consistency) 미적용 → 텍스트 손실이 overall=PASS 로 통과 (mdx05 실측).
   양 경로 공통 헬퍼로 통일 — mdx04 의 #16 시점 PASS 일부가 이 버그 덕이었음을
   정직하게 정정 (현재 PARTIAL + frame mismatch 라벨, 텍스트는 완전).

최종 5-MDX: 전부 missing_atoms=0 (무손실) / 01·03 PASS / 02·04·05 PARTIAL(정직
frame-mismatch 라벨) / mdx01 readiness not_ready→needs_review 개선.
게이트: 1061 passed. SHA baseline 재캡처 (정당 변경).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 13:51:00 +09:00

152 lines
6.3 KiB
Python

"""Pipeline Step 17b — V4 full-32 평가를 누락 섹션에 확장 (GitHub issue #17).
배경: v4_full32_result.yaml (2026-04-29) 은 mdx 01~04 의 10개 섹션만 평가 —
`01-intro` / `05-1` / `05-2` 는 V4 source 자체가 없어 런타임에서 무조건
generic fallback (design_readiness=not_ready) 이 됨 (emergency.md C3).
원칙: "결과물을 고치지 말고 프로세스를 고친다" — yaml 손편집이 아니라
pipeline_17 과 동일한 평가 코드(detect_mdx_analysis + compute_template_fit
+ route)로 누락 섹션만 평가해 병합한다. 기존 섹션 엔트리는 무접촉.
실행: python tests/matching/pipeline_17b_extend_missing_sections.py
출력: v4_full32_result.yaml (기존 10 + 신규 3 = 13 sections)
"""
import datetime
import sys
from pathlib import Path
import yaml
HERE = Path(__file__).parent
sys.path.insert(0, str(HERE))
from phase_common import load_32_frames, load_frame_index
from detect_mdx import detect_mdx_analysis
from template_fit import (
load_templates_v1, collect_anchor_vocab, compute_template_fit, route,
)
from embeddings import embed_texts, cosine
from pipeline_01_extract_nodes import MDX_DIR
OUT_PATH = HERE / 'v4_full32_result.yaml'
# 신규 평가 대상 — 런타임 section_id 와 동일한 키 (lookup_v4 exact match).
# 01-intro 는 heading 없는 pre-intro 블록이라 시작 라인을 본문 첫 그룹으로 지정.
NEW_SECTIONS = {
'01-intro': {'file': '01.mdx', 'start': '* **용어의 혼용**', 'end_prefix': '## 1.',
'title': '건설산업 DX의 올바른 이해 — 도입(용어의 혼용)'},
'05-1': {'file': '05.mdx', 'start': '## 1. 설계의 자동화', 'end_prefix': '## 2.',
'title': None},
'05-2': {'file': '05.mdx', 'start': '## 2. S/W 중심 설계 방식', 'end_prefix': None,
'title': None},
}
def extract_raw(cfg):
lines = (MDX_DIR / cfg['file']).read_text(encoding='utf-8').split('\n')
start = None
for i, ln in enumerate(lines):
if ln.strip() == cfg['start'].strip():
start = i
break
if start is None:
raise RuntimeError(f"start line not found: {cfg['start']!r} in {cfg['file']}")
end = len(lines)
if cfg.get('end_prefix'):
for i in range(start + 1, len(lines)):
if lines[i].strip().startswith(cfg['end_prefix']):
end = i
break
section = lines[start:end]
title = cfg.get('title') or section[0].lstrip('#').strip()
return title, '\n'.join(section)
def main():
existing = yaml.safe_load(OUT_PATH.read_text(encoding='utf-8'))
already = set(existing['mdx_sections'].keys())
todo = {sid: cfg for sid, cfg in NEW_SECTIONS.items() if sid not in already}
if not todo:
print('신규 평가 대상 없음 — 모두 존재.')
return
templates = load_templates_v1()
anchor_vocab = collect_anchor_vocab(templates)
frames = load_32_frames()
idx_data, frame_to_short = load_frame_index()
fids = list(frames.keys())
frame_num_map = {fid: int(frame_to_short[fid]) for fid in fids}
frame_contents = [frames[fid].get('content', '') for fid in fids]
print(f'[17b] 32 frame content 임베딩 중... (신규 섹션: {sorted(todo)})')
frame_vecs = embed_texts(frame_contents)
for sid, cfg in sorted(todo.items()):
title, raw_text = extract_raw(cfg)
mdx_analysis = detect_mdx_analysis(raw_text, title, anchor_vocab=anchor_vocab)
mdx_vec = embed_texts([mdx_analysis['summary']])[0]
judgments = []
for i, fid in enumerate(fids):
if fid not in templates:
continue
template = templates[fid]
content_emb = max(0.0, min(1.0, float(cosine(mdx_vec, frame_vecs[i]))))
fit = compute_template_fit(mdx_analysis, template, content_emb)
label = route(fit['confidence'], fit['axes'], fit['adaptation'], fit['not_suits'])
judgments.append({
'frame_id': fid,
'frame_number': frame_num_map[fid],
'template_id': template.get('template_id'),
'confidence': round(float(fit['confidence']), 4),
'base': round(float(fit['base']), 4),
'penalty': round(float(fit['total_penalty']), 4),
'label': label,
'content_embedding': round(content_emb, 4),
'axes': {
'anchor': round(float(fit['axes']['anchor']['score']), 4),
'cardinality': round(float(fit['axes']['cardinality']), 4),
'relation': round(float(fit['axes']['relation']), 4),
'slot': round(float(fit['axes']['slot']), 4),
'content': round(float(fit['axes']['content']), 4),
},
})
judgments.sort(key=lambda x: -x['confidence'])
for new_rank, item in enumerate(judgments, start=1):
item['v4_full_rank'] = new_rank
existing['mdx_sections'][sid] = {
'mdx_title': title,
'answer_frame_number': None, # blind — ANSWER_MAP 변경 금지 (Holdout 원칙 1)
'is_holdout': True,
'judgments_full32': judgments,
'usable_count': sum(1 for j in judgments if j['label'] != 'reject'),
'reject_count': sum(1 for j in judgments if j['label'] == 'reject'),
}
top = judgments[0]
print(f" {sid}: rank1 = F{top['frame_number']} {top['template_id']} "
f"({top['label']}, {top['confidence']}) usable={existing['mdx_sections'][sid]['usable_count']}")
meta = existing.setdefault('meta', {})
ext = meta.setdefault('extensions', [])
ext.append({
'step': '17b_extend_missing_sections',
'added_sections': sorted(todo),
'reason': 'GitHub issue #17 — generic fallback 탈출 (C3: V4 source 누락 해소)',
'generated_at': datetime.datetime.now().isoformat(timespec='seconds'),
})
holdout = meta.setdefault('holdout_sections', [])
for sid in sorted(todo):
if sid not in holdout:
holdout.append(sid)
OUT_PATH.write_text(
yaml.safe_dump(existing, allow_unicode=True, sort_keys=False, width=1000),
encoding='utf-8',
)
print(f'병합 완료: {OUT_PATH} (총 {len(existing["mdx_sections"])} sections)')
if __name__ == '__main__':
main()