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>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
"""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()
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user