"""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()