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:
+121
-52
@@ -664,6 +664,21 @@ def _build_verbatim_compare_table_2col(unit) -> dict:
|
||||
}
|
||||
|
||||
|
||||
# issue #17 — verbatim builder 를 보유한 template 집합 (아래 dispatch 와 동기).
|
||||
# renderable-aware provisional 이 reject 라벨 후보를 승격해도 안전한지(원문
|
||||
# 무손실 코드 렌더 가능한지) 판단하는 데 사용.
|
||||
_VERBATIM_BUILDER_TEMPLATE_IDS: frozenset[str] = frozenset({
|
||||
"three_parallel_requirements",
|
||||
"three_persona_benefits",
|
||||
"construction_goals_three_circle_intersection",
|
||||
"construction_bim_three_usage",
|
||||
"bim_dx_comparison_table",
|
||||
"bim_issues_quadrant_four",
|
||||
"sw_dependency_four_problems",
|
||||
"pre_construction_model_info_stacked",
|
||||
})
|
||||
|
||||
|
||||
def _emergency_p4b_build_verbatim_slot_payload(
|
||||
unit, template_id: str, override_slot_count: Optional[int] = None,
|
||||
) -> Optional[dict]:
|
||||
@@ -2797,16 +2812,41 @@ def lookup_v4_match_with_fallback(
|
||||
# trace entries are left intact (their skip reasons remain accurate).
|
||||
# Default-off keeps IMP-05 behavior byte-identical.
|
||||
if allow_provisional:
|
||||
rank_1_judgment = judgments[0]
|
||||
# issue #17 — renderable-aware provisional. rank 순서는 유지하되
|
||||
# 승격 가능 조건: partial HTML 존재 (IMP-95 u6 partial_exists 원칙)
|
||||
# AND (비-reject 라벨 OR verbatim builder 지원). 근거 실측:
|
||||
# - partial 없는 frame 승격 → mapper FitError → verbatim 복구 →
|
||||
# selected_frame_not_applied 라벨 (mdx05 05-2 rank-1 F6)
|
||||
# - reject 라벨 + verbatim builder 없는 frame 의 mapper 네이티브
|
||||
# 렌더는 원문 drop 위험 (mdx05 05-2 F23: 1 atom 손실 실측 —
|
||||
# 원문 보존이 design 개선보다 우선)
|
||||
# 조건 만족 후보가 없으면 기존대로 rank-1 (동작 보존 — verbatim
|
||||
# 복구가 텍스트 무손실을 보장).
|
||||
provisional_judgment = judgments[0]
|
||||
provisional_rank = 1
|
||||
for _rank, _judgment in enumerate(judgments, start=1):
|
||||
_tid = str(_judgment.get("template_id") or "")
|
||||
if not _b4_partial_exists(_tid):
|
||||
continue
|
||||
_label = str(_judgment.get("label") or "")
|
||||
if _label == "reject" and _tid not in _VERBATIM_BUILDER_TEMPLATE_IDS:
|
||||
continue
|
||||
provisional_judgment, provisional_rank = _judgment, _rank
|
||||
break
|
||||
provisional_match = _v4_match_from_judgment(
|
||||
section_id, rank_1_judgment, rank=1
|
||||
section_id, provisional_judgment, rank=provisional_rank
|
||||
)
|
||||
provisional_match.selection_path = "provisional_rank_1"
|
||||
_prov_path = (
|
||||
"provisional_rank_1"
|
||||
if provisional_rank == 1
|
||||
else f"provisional_renderable_rank_{provisional_rank}"
|
||||
)
|
||||
provisional_match.selection_path = _prov_path
|
||||
provisional_match.fallback_reason = trace["fallback_reason"]
|
||||
provisional_match.provisional = True
|
||||
trace.update({
|
||||
"selection_path": "provisional_rank_1",
|
||||
"selected_rank": 1,
|
||||
"selection_path": _prov_path,
|
||||
"selected_rank": provisional_rank,
|
||||
"selected_template_id": provisional_match.template_id,
|
||||
"selected_frame_id": provisional_match.frame_id,
|
||||
"selected_label": provisional_match.label,
|
||||
@@ -5870,6 +5910,63 @@ def _compute_density_gate(zones_data: list[dict], overflow: dict) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _apply_quality_gate_downgrades(
|
||||
slide_status: dict,
|
||||
*,
|
||||
rendered_text_coverage: dict,
|
||||
forbidden_rendered_syntax: dict,
|
||||
applied_render_consistency: dict,
|
||||
) -> None:
|
||||
"""issue #17 — quality gate 3종의 overall 강등을 단일 헬퍼로.
|
||||
|
||||
두 slide_status 조립 경로(최초 + T28.5d popup 승격 후 재계산)가 동일한
|
||||
강등 규칙을 공유해야 한다 — 재계산 경로에 이 블록이 없어서 텍스트
|
||||
손실(rendered_text_coverage fail)이 overall=PASS 로 통과하는 버그가
|
||||
있었음 (mdx05 05-2 F23 렌더 1-atom 손실 실측).
|
||||
"""
|
||||
if not rendered_text_coverage.get("passed"):
|
||||
slide_status.setdefault("quality_gate_failures", []).append(
|
||||
"rendered_text_coverage_missing"
|
||||
)
|
||||
slide_status.setdefault("visual_fail_reasons", []).append(
|
||||
"rendered_text_coverage_missing: "
|
||||
f"{rendered_text_coverage.get('missing_atoms_count')} source text atom(s) "
|
||||
"not visible in final.html"
|
||||
)
|
||||
if slide_status.get("overall") == "PASS":
|
||||
slide_status["overall"] = "PARTIAL_COVERAGE"
|
||||
|
||||
if not forbidden_rendered_syntax.get("passed"):
|
||||
slide_status.setdefault("quality_gate_failures", []).append(
|
||||
"forbidden_rendered_syntax"
|
||||
)
|
||||
slide_status.setdefault("visual_fail_reasons", []).append(
|
||||
"forbidden_rendered_syntax: "
|
||||
f"{forbidden_rendered_syntax.get('forbidden_syntax_count')} JSX/CSS/code "
|
||||
"fragment(s) visible in final.html"
|
||||
)
|
||||
if slide_status.get("overall") == "PASS":
|
||||
slide_status["overall"] = "PARTIAL_COVERAGE"
|
||||
|
||||
if not applied_render_consistency.get("passed"):
|
||||
for failure in applied_render_consistency.get("failures") or []:
|
||||
slide_status.setdefault("quality_gate_failures", []).append(failure)
|
||||
if applied_render_consistency.get("frame_mismatch_count"):
|
||||
slide_status.setdefault("visual_fail_reasons", []).append(
|
||||
"selected_frame_not_applied: "
|
||||
f"{applied_render_consistency.get('frame_mismatch_count')} zone(s) "
|
||||
"rendered with a different template than Step 6 selected"
|
||||
)
|
||||
if "final_layout_positions_mismatch" in (applied_render_consistency.get("failures") or []):
|
||||
slide_status.setdefault("visual_fail_reasons", []).append(
|
||||
"final_layout_positions_mismatch: "
|
||||
f"expected {applied_render_consistency.get('expected_positions')} but "
|
||||
f"final.html has {applied_render_consistency.get('final_positions')}"
|
||||
)
|
||||
if slide_status.get("overall") == "PASS":
|
||||
slide_status["overall"] = "PARTIAL_COVERAGE"
|
||||
|
||||
|
||||
def _compute_design_readiness(
|
||||
*,
|
||||
units: list[CompositionUnit],
|
||||
@@ -11310,53 +11407,17 @@ def run_phase_z2_mvp1(
|
||||
density_gate=density_gate,
|
||||
)
|
||||
slide_status["design_readiness"] = design_readiness
|
||||
if not rendered_text_coverage.get("passed"):
|
||||
slide_status.setdefault("quality_gate_failures", []).append(
|
||||
"rendered_text_coverage_missing"
|
||||
)
|
||||
slide_status.setdefault("visual_fail_reasons", []).append(
|
||||
"rendered_text_coverage_missing: "
|
||||
f"{rendered_text_coverage.get('missing_atoms_count')} source text atom(s) "
|
||||
"not visible in final.html"
|
||||
)
|
||||
if slide_status.get("overall") == "PASS":
|
||||
slide_status["overall"] = "PARTIAL_COVERAGE"
|
||||
|
||||
# IMP-47B u8 — Surface Step 12 AI repair outcomes through slide_status.
|
||||
# Composes u4 gather errors + u5 apply_status + u7 coverage_invariant
|
||||
# into a single ``ai_repair_status`` axis the frontend (u11) reads to
|
||||
# render human_review notifications. Auto pipeline first
|
||||
# ([[feedback_auto_pipeline_first]]) — no review_queue insertion;
|
||||
# explicit status enum + human_review_required flag.
|
||||
if not forbidden_rendered_syntax.get("passed"):
|
||||
slide_status.setdefault("quality_gate_failures", []).append(
|
||||
"forbidden_rendered_syntax"
|
||||
)
|
||||
slide_status.setdefault("visual_fail_reasons", []).append(
|
||||
"forbidden_rendered_syntax: "
|
||||
f"{forbidden_rendered_syntax.get('forbidden_syntax_count')} JSX/CSS/code "
|
||||
"fragment(s) visible in final.html"
|
||||
)
|
||||
if slide_status.get("overall") == "PASS":
|
||||
slide_status["overall"] = "PARTIAL_COVERAGE"
|
||||
|
||||
if not applied_render_consistency.get("passed"):
|
||||
for failure in applied_render_consistency.get("failures") or []:
|
||||
slide_status.setdefault("quality_gate_failures", []).append(failure)
|
||||
if applied_render_consistency.get("frame_mismatch_count"):
|
||||
slide_status.setdefault("visual_fail_reasons", []).append(
|
||||
"selected_frame_not_applied: "
|
||||
f"{applied_render_consistency.get('frame_mismatch_count')} zone(s) "
|
||||
"rendered with a different template than Step 6 selected"
|
||||
)
|
||||
if "final_layout_positions_mismatch" in (applied_render_consistency.get("failures") or []):
|
||||
slide_status.setdefault("visual_fail_reasons", []).append(
|
||||
"final_layout_positions_mismatch: "
|
||||
f"expected {applied_render_consistency.get('expected_positions')} but "
|
||||
f"final.html has {applied_render_consistency.get('final_positions')}"
|
||||
)
|
||||
if slide_status.get("overall") == "PASS":
|
||||
slide_status["overall"] = "PARTIAL_COVERAGE"
|
||||
# IMP-47B u8 주석 이력 보존 — Surface Step 12 AI repair outcomes through
|
||||
# slide_status (아래 ai_repair_status 블록). quality gate downgrade 3종은
|
||||
# issue #17 에서 _apply_quality_gate_downgrades 로 추출 (popup 승격 후
|
||||
# 재계산 경로에도 동일 적용 — 미적용 시 text 손실이 PASS 로 통과하는
|
||||
# 게이트 순서 버그가 있었음: mdx05 실측).
|
||||
_apply_quality_gate_downgrades(
|
||||
slide_status,
|
||||
rendered_text_coverage=rendered_text_coverage,
|
||||
forbidden_rendered_syntax=forbidden_rendered_syntax,
|
||||
applied_render_consistency=applied_render_consistency,
|
||||
)
|
||||
|
||||
presentation_fit = _build_presentation_fit_report(
|
||||
overflow=overflow,
|
||||
@@ -11448,6 +11509,14 @@ def run_phase_z2_mvp1(
|
||||
slide_status["density_gate"] = density_gate
|
||||
slide_status["design_readiness"] = design_readiness
|
||||
slide_status["presentation_fit"] = presentation_fit
|
||||
# issue #17 — 재계산 경로에도 quality gate 강등 적용 (미적용 시
|
||||
# text 손실이 overall=PASS 로 통과하던 게이트 순서 버그).
|
||||
_apply_quality_gate_downgrades(
|
||||
slide_status,
|
||||
rendered_text_coverage=rendered_text_coverage,
|
||||
forbidden_rendered_syntax=forbidden_rendered_syntax,
|
||||
applied_render_consistency=applied_render_consistency,
|
||||
)
|
||||
slide_status["technical_pass"] = bool(
|
||||
slide_status.get("rendered")
|
||||
and slide_status.get("full_mdx_coverage")
|
||||
|
||||
@@ -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
@@ -2,7 +2,7 @@
|
||||
"schema_version": 2,
|
||||
"axis": "IMP-89 89-a u4 — final.html SHA baseline captured via FULL run_phase_z2_mvp1 pipeline (flag OFF / default)",
|
||||
"description": "Frozen SHA-256 of `final.html` bytes (the artifact written to disk at src/phase_z2_pipeline.py:5994-5996) captured by running the full Phase Z pipeline end-to-end for each mdx 01-05 under PHASE_Z_B4_MAPPER_SOURCE=OFF. Under flag OFF the 89-a selector `_select_mapper_template_id(plan, T)` returns `T` verbatim, so the mapper input is byte-identical to the pre-89-a legacy call shape `map_mdx_to_slots(section, unit.frame_template_id)` — the rendered HTML and therefore the final.html SHA match the pre-89-a baseline. The u4 regression test runs the same pipeline shape under flag OFF and asserts SHA equality. Regenerate only when an upstream mapper/render/template delta is deliberately reviewed and accepted.",
|
||||
"captured_at_utc": "2026-07-07T00:11:02Z",
|
||||
"captured_at_utc": "2026-07-07T04:43:23Z",
|
||||
"renderer": {
|
||||
"entrypoint": "src.phase_z2_pipeline.run_phase_z2_mvp1",
|
||||
"write_site": "src/phase_z2_pipeline.py:5994-5996",
|
||||
@@ -19,8 +19,8 @@
|
||||
"01.mdx": {
|
||||
"mdx_file": "01.mdx",
|
||||
"run_id": "89a_baseline_01",
|
||||
"final_html_size_bytes": 38731,
|
||||
"sha256": "749ebb626e284a62d37b5be32af97c61ae2a5b8d4226d1c3fa90f04c843d9fef",
|
||||
"final_html_size_bytes": 36240,
|
||||
"sha256": "1e398a65298064e0fe653453b81e625bf2eeaec4077a78d85f8e307e58d69c80",
|
||||
"pipeline_exit_code": null
|
||||
},
|
||||
"02.mdx": {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"schema_version": 1,
|
||||
"axis": "IMP-95 u8 — final.html SHA baseline captured via FULL run_phase_z2_mvp1 pipeline under PHASE_Z_B4_V4_EVIDENCE=OFF and PHASE_Z_B4_MAPPER_SOURCE=OFF (defaults)",
|
||||
"description": "Frozen SHA-256 of `final.html` bytes (production write site src/phase_z2_pipeline.py:5994-5996) for mdx 01/02/04/05 under PHASE_Z_B4_V4_EVIDENCE OFF. Under flag OFF, IMP-95 (u1~u7) is strictly no-op for final.html (planner branch falls through to legacy _select_frame at u3; u4/u5/u6 additive telemetry confined to placement_trace per the trace-only docstring at src/phase_z2_pipeline.py:86). The u8 regression test asserts SHA equality with these frozen values, so any future code change that drifts the flag-OFF render output produces a mismatch and breaks the test. mdx 03 is excluded per Stage 2 u8 scope (mdx 03 정비 LOCK). Regenerate only when an upstream delta is reviewed and accepted as the new pre-IMP-95 reference.",
|
||||
"captured_at_utc": "2026-07-07T00:09:32Z",
|
||||
"captured_at_utc": "2026-07-07T04:41:54Z",
|
||||
"renderer": {
|
||||
"entrypoint": "src.phase_z2_pipeline.run_phase_z2_mvp1",
|
||||
"write_site": "src/phase_z2_pipeline.py:5994-5996",
|
||||
@@ -18,8 +18,8 @@
|
||||
"01.mdx": {
|
||||
"mdx_file": "01.mdx",
|
||||
"run_id": "imp95_baseline_01",
|
||||
"final_html_size_bytes": 38731,
|
||||
"sha256": "749ebb626e284a62d37b5be32af97c61ae2a5b8d4226d1c3fa90f04c843d9fef",
|
||||
"final_html_size_bytes": 36240,
|
||||
"sha256": "1e398a65298064e0fe653453b81e625bf2eeaec4077a78d85f8e307e58d69c80",
|
||||
"pipeline_exit_code": null
|
||||
},
|
||||
"02.mdx": {
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""issue #17 — renderable-aware provisional 선택 테스트.
|
||||
|
||||
계약: IMP-30 u1 provisional 합성이 rank-1 을 무조건 승격하던 것을,
|
||||
partial HTML 이 존재하는 첫 judgment(rank 순) 승격으로 변경 (IMP-95 u6
|
||||
partial_exists 원칙). 전부 non-renderable 이면 기존대로 rank-1 (동작 보존).
|
||||
|
||||
실측 근거: mdx05 05-2 rank-1 F6(compensation_complaint_map, contract-only)
|
||||
승격 → mapper FitError → verbatim 복구 → selected_frame_not_applied 라벨
|
||||
→ PARTIAL_COVERAGE. renderable rank-6 F23 선택 시 정상 경로.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import src.phase_z2_pipeline as _pz
|
||||
from src.phase_z2_pipeline import lookup_v4_match_with_fallback
|
||||
|
||||
|
||||
def _j(rank, template_id, frame_id, label, conf=0.5):
|
||||
return {
|
||||
"v4_full_rank": rank,
|
||||
"template_id": template_id,
|
||||
"frame_id": frame_id,
|
||||
"frame_number": rank,
|
||||
"confidence": conf,
|
||||
"label": label,
|
||||
}
|
||||
|
||||
|
||||
def _v4(judgments):
|
||||
return {"mdx_sections": {"S1": {"judgments_full32": judgments}}}
|
||||
|
||||
|
||||
def test_provisional_skips_contract_only_frames(monkeypatch):
|
||||
"""rank1/2 는 partial 없음, rank3 존재(+verbatim builder) → rank3 승격."""
|
||||
monkeypatch.setattr(
|
||||
_pz, "_b4_partial_exists",
|
||||
lambda tid: tid == "T_renderable",
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
_pz, "_VERBATIM_BUILDER_TEMPLATE_IDS", frozenset({"T_renderable"}),
|
||||
)
|
||||
v4 = _v4([
|
||||
_j(1, "T_contract_only_a", "F1", "reject", 0.6),
|
||||
_j(2, "T_contract_only_b", "F2", "reject", 0.5),
|
||||
_j(3, "T_renderable", "F3", "reject", 0.4),
|
||||
])
|
||||
match, trace = lookup_v4_match_with_fallback(
|
||||
v4, "S1", allow_provisional=True,
|
||||
)
|
||||
assert match is not None
|
||||
assert match.template_id == "T_renderable"
|
||||
assert match.provisional is True
|
||||
assert trace["selection_path"] == "provisional_renderable_rank_3"
|
||||
assert trace["selected_rank"] == 3
|
||||
|
||||
|
||||
def test_provisional_rank1_when_renderable(monkeypatch):
|
||||
monkeypatch.setattr(_pz, "_b4_partial_exists", lambda tid: True)
|
||||
monkeypatch.setattr(
|
||||
_pz, "_VERBATIM_BUILDER_TEMPLATE_IDS", frozenset({"T_a", "T_b"}),
|
||||
)
|
||||
v4 = _v4([
|
||||
_j(1, "T_a", "F1", "reject", 0.6),
|
||||
_j(2, "T_b", "F2", "reject", 0.5),
|
||||
])
|
||||
match, trace = lookup_v4_match_with_fallback(
|
||||
v4, "S1", allow_provisional=True,
|
||||
)
|
||||
assert match.template_id == "T_a"
|
||||
assert trace["selection_path"] == "provisional_rank_1"
|
||||
assert trace["selected_rank"] == 1
|
||||
|
||||
|
||||
def test_provisional_all_non_renderable_keeps_rank1(monkeypatch):
|
||||
"""전부 partial 없음 → 기존 동작 보존 (rank-1, 정직 라벨은 하류 게이트)."""
|
||||
monkeypatch.setattr(_pz, "_b4_partial_exists", lambda tid: False)
|
||||
v4 = _v4([
|
||||
_j(1, "T_a", "F1", "reject", 0.6),
|
||||
_j(2, "T_b", "F2", "reject", 0.5),
|
||||
])
|
||||
match, trace = lookup_v4_match_with_fallback(
|
||||
v4, "S1", allow_provisional=True,
|
||||
)
|
||||
assert match.template_id == "T_a"
|
||||
assert trace["selection_path"] == "provisional_rank_1"
|
||||
|
||||
|
||||
def test_provisional_reject_without_verbatim_builder_not_promoted(monkeypatch):
|
||||
"""renderable 이어도 reject + verbatim builder 미보유면 승격 금지 —
|
||||
mapper 네이티브 렌더의 원문 drop 위험 (mdx05 05-2 F23 1-atom 손실 실측).
|
||||
조건 만족 후보 없음 → rank-1 유지 (verbatim 복구가 무손실 보장)."""
|
||||
monkeypatch.setattr(_pz, "_b4_partial_exists", lambda tid: tid == "T_renderable")
|
||||
monkeypatch.setattr(_pz, "_VERBATIM_BUILDER_TEMPLATE_IDS", frozenset())
|
||||
v4 = _v4([
|
||||
_j(1, "T_contract_only", "F1", "reject", 0.6),
|
||||
_j(2, "T_renderable", "F2", "reject", 0.4),
|
||||
])
|
||||
match, trace = lookup_v4_match_with_fallback(
|
||||
v4, "S1", allow_provisional=True,
|
||||
)
|
||||
assert match.template_id == "T_contract_only"
|
||||
assert trace["selection_path"] == "provisional_rank_1"
|
||||
|
||||
|
||||
def test_provisional_non_reject_renderable_promoted_without_builder(monkeypatch):
|
||||
"""light_edit 등 비-reject 라벨은 verbatim builder 없어도 승격 가능
|
||||
(mapper 가 정상 fit 예상 — V4 판정 자체가 사용 가능 평가)."""
|
||||
monkeypatch.setattr(_pz, "_b4_partial_exists", lambda tid: tid == "T_light")
|
||||
monkeypatch.setattr(_pz, "_VERBATIM_BUILDER_TEMPLATE_IDS", frozenset())
|
||||
v4 = _v4([
|
||||
_j(1, "T_contract_only", "F1", "reject", 0.6),
|
||||
_j(2, "T_light", "F2", "light_edit", 0.5),
|
||||
])
|
||||
match, trace = lookup_v4_match_with_fallback(
|
||||
v4, "S1", allow_provisional=True,
|
||||
)
|
||||
assert match.template_id == "T_light"
|
||||
# IMP-39 label-priority sort 로 light_edit 이 정렬 1위 → rank_1 경로.
|
||||
assert trace["selection_path"] == "provisional_rank_1"
|
||||
Reference in New Issue
Block a user