Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13a8d752d3 | ||
|
|
24cecf2b72 |
+113
-4
@@ -2936,6 +2936,27 @@ def write_overflow_error(run_dir: Path, overflow: dict) -> Path:
|
||||
|
||||
# ─── Debug.json (single slide + zones[]) ───────────────────────
|
||||
|
||||
|
||||
def _is_empty_shell_unit(u: CompositionUnit) -> bool:
|
||||
"""IMP-87 u1 — true when a CompositionUnit is the IMP-30 u4 empty-shell
|
||||
placeholder (frame_template_id="__empty__" / label="empty_shell").
|
||||
|
||||
Used by compute_slide_status to redefine `full_mdx_coverage` over
|
||||
rendered content units only: an empty-shell unit attaches the aligned
|
||||
section_ids to a `__empty__` frame for layout purposes, but the slide
|
||||
surface carries no MDX content for those sections. Counting it as
|
||||
coverage would violate feedback_artifact_status_naming (overall/coverage
|
||||
must reflect actual content state, not pipeline completion).
|
||||
"""
|
||||
if getattr(u, "frame_template_id", None) == "__empty__":
|
||||
return True
|
||||
if getattr(u, "label", None) == "empty_shell":
|
||||
return True
|
||||
if getattr(u, "merge_type", None) == "empty_shell":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def compute_slide_status(sections: list[MdxSection],
|
||||
units: list[CompositionUnit],
|
||||
comp_debug: dict,
|
||||
@@ -2964,10 +2985,21 @@ def compute_slide_status(sections: list[MdxSection],
|
||||
Stage 1 Q3 + Codex #10 D4 lock.)
|
||||
"""
|
||||
aligned_ids = [s.section_id for s in sections]
|
||||
covered = set()
|
||||
# IMP-87 u1 — split coverage into content vs empty-shell cover.
|
||||
# `covered` (legacy, kept for downstream display) still tracks every
|
||||
# section attached to any selected unit. `content_covered` is the new
|
||||
# honesty axis: only non-empty-shell units count as "rendered with
|
||||
# content". Sections attached solely to an empty-shell placeholder
|
||||
# are routed into `filtered_section_ids` so Step 20 cannot report
|
||||
# full_mdx_coverage=True for an EMPTY-SHELL-only slide (the Case B
|
||||
# honesty defect documented in IMP-87 Stage 1 / Codex #2).
|
||||
covered: set = set()
|
||||
content_covered: set = set()
|
||||
for u in units:
|
||||
covered.update(u.source_section_ids)
|
||||
filtered_ids = sorted(set(aligned_ids) - covered)
|
||||
if not _is_empty_shell_unit(u):
|
||||
content_covered.update(u.source_section_ids)
|
||||
filtered_ids = sorted(set(aligned_ids) - content_covered)
|
||||
full_coverage = len(filtered_ids) == 0
|
||||
visual_passed = bool(overflow.get("passed", False))
|
||||
|
||||
@@ -3057,7 +3089,34 @@ def compute_slide_status(sections: list[MdxSection],
|
||||
"position": source_position,
|
||||
})
|
||||
|
||||
if full_coverage and visual_passed:
|
||||
# IMP-87 u2 — empty-shell vs content unit accounting (additive surface fields).
|
||||
# Splits the selected units into the two classes that u1 already separates
|
||||
# for coverage purposes, then promotes "no content unit at all" to an
|
||||
# explicit overall enum value (EMPTY_SHELL_NO_CONTENT) so an EMPTY-SHELL-only
|
||||
# slide cannot route through any of the four existing ladder outcomes that
|
||||
# implicitly assume there is real content on the page.
|
||||
# - empty_shell_units: units flagged by _is_empty_shell_unit (IMP-87 u1)
|
||||
# - content_units : the remainder (real frame_template_id)
|
||||
# - empty_shell_dominant: True when there is NO content unit and at least
|
||||
# one empty-shell unit (the Case B mdx05 honesty defect surface)
|
||||
empty_shell_units = [u for u in units if _is_empty_shell_unit(u)]
|
||||
content_units = [u for u in units if not _is_empty_shell_unit(u)]
|
||||
empty_shell_section_ids: set = set()
|
||||
for u in empty_shell_units:
|
||||
empty_shell_section_ids.update(getattr(u, "source_section_ids", []) or [])
|
||||
content_rendered_section_ids: set = set()
|
||||
for u in content_units:
|
||||
content_rendered_section_ids.update(getattr(u, "source_section_ids", []) or [])
|
||||
empty_shell_dominant = (len(content_units) == 0) and (len(empty_shell_units) >= 1)
|
||||
|
||||
if empty_shell_dominant:
|
||||
# IMP-87 u2 — EMPTY_SHELL_NO_CONTENT precedes the legacy 4-way ladder.
|
||||
# Even if visual_check passed (placeholder zones don't overflow) and
|
||||
# filtered_section_ids only lists the IMP-87 u1 routed empty-shell
|
||||
# sections, this slide carries zero MDX content and must not look
|
||||
# shippable. Honors feedback_artifact_status_naming.
|
||||
overall = "EMPTY_SHELL_NO_CONTENT"
|
||||
elif full_coverage and visual_passed:
|
||||
overall = "PASS"
|
||||
elif full_coverage and not visual_passed:
|
||||
overall = "RENDERED_WITH_VISUAL_REGRESSION"
|
||||
@@ -3118,6 +3177,15 @@ def compute_slide_status(sections: list[MdxSection],
|
||||
# IMP-30 u6 — additive provisional qualifiers (overall enum unchanged).
|
||||
"provisional_first_render_count": len(provisional_first_render_units),
|
||||
"provisional_first_render_units": provisional_first_render_units,
|
||||
# IMP-87 u2 — empty-shell vs content unit accounting (additive surface).
|
||||
# Names mirror provisional_first_render_*/fallback_selection_count shape
|
||||
# so downstream readers (CLI tail in u3, status board UI, regression
|
||||
# tests in u5) can pick them up without schema reshaping.
|
||||
"empty_shell_unit_count": len(empty_shell_units),
|
||||
"empty_shell_section_ids": sorted(empty_shell_section_ids),
|
||||
"content_unit_count": len(content_units),
|
||||
"content_rendered_section_ids": sorted(content_rendered_section_ids),
|
||||
"empty_shell_dominant": empty_shell_dominant,
|
||||
"overall": overall,
|
||||
"note": (
|
||||
"자동 파이프라인 결과 보고. review/UI 개념 X. final.html 파일명 != PASS 의미. "
|
||||
@@ -3126,6 +3194,8 @@ def compute_slide_status(sections: list[MdxSection],
|
||||
"content_truncated_count > 0 = builder 가 truncate 한 zone 존재 (rendered 됐지만 일부 콘텐츠 손실). "
|
||||
"provisional_first_render_count > 0 = IMP-30 first-render invariant 가 작동한 unit 존재 "
|
||||
"(empty_shell / chain_exhausted_provisional / 등 — needs user/AI adaptation). "
|
||||
"overall == EMPTY_SHELL_NO_CONTENT (IMP-87 u2) = content_unit_count==0 이고 "
|
||||
"empty_shell_unit_count>=1 — 슬라이드에 실 콘텐츠 없음. PASS 보고 금지 (Case B honesty 가드)."
|
||||
),
|
||||
}
|
||||
|
||||
@@ -5929,7 +5999,18 @@ def run_phase_z2_mvp1(
|
||||
)
|
||||
# Step 20 HTML — 최종 판정 시각 보고
|
||||
_overall = slide_status.get("overall", "?")
|
||||
_ov_class = "pass" if "PASS" in _overall else "fail" if "FAIL" in _overall or "REGRESSION" in _overall else "partial"
|
||||
# IMP-87 u3 — EMPTY_SHELL_NO_CONTENT maps to fail styling (not partial).
|
||||
# Placeholder-only slide is unshippable even when visual check passes,
|
||||
# so final_status.html must render red, matching the CLI BLOCKED exit.
|
||||
_ov_class = (
|
||||
"pass" if "PASS" in _overall
|
||||
else "fail" if (
|
||||
"FAIL" in _overall
|
||||
or "REGRESSION" in _overall
|
||||
or _overall == "EMPTY_SHELL_NO_CONTENT"
|
||||
)
|
||||
else "partial"
|
||||
)
|
||||
_vfs = slide_status.get("visual_fail_reasons") or []
|
||||
_vfs_html = (
|
||||
"<ul>" + "".join(f"<li>{v}</li>" for v in _vfs) + "</ul>"
|
||||
@@ -6056,6 +6137,34 @@ def run_phase_z2_mvp1(
|
||||
print(f" error : {err_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# IMP-87 u3 — EMPTY_SHELL_NO_CONTENT BLOCKED branch precedes the
|
||||
# partial-coverage return so empty-shell-only runs cannot exit zero.
|
||||
# Placeholder zones do not overflow, so visual_check_passed is True here;
|
||||
# the diagnostic surfaces empty-shell counters from compute_slide_status.
|
||||
if overall == "EMPTY_SHELL_NO_CONTENT":
|
||||
print(
|
||||
f"\n[Phase Z-2 IMP-87 u3] BLOCKED @ empty_shell_no_content ({overall})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
f" empty_shell_unit_count = {slide_status.get('empty_shell_unit_count', 0)}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
f" empty_shell_section_ids = {slide_status.get('empty_shell_section_ids') or []}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
f" content_unit_count = {slide_status.get('content_unit_count', 0)}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
" reason : every selected unit is empty_shell placeholder — "
|
||||
"final.html 은 placeholder shell, ship 불가",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if not slide_status["full_mdx_coverage"]:
|
||||
print(
|
||||
f"\n[Phase Z-2 MVP-1.5b] PARTIAL — visual check OK 지만 "
|
||||
|
||||
@@ -1012,11 +1012,14 @@ def test_u6_empty_shell_unit_listed_with_empty_identifiers():
|
||||
assert entry["selection_path"] == "empty_shell"
|
||||
assert entry["fallback_reason"] == "no_v4_rank_1_for_any_section"
|
||||
assert entry["v4_rank"] is None
|
||||
# full_mdx_coverage holds because shell.source_section_ids covers every
|
||||
# aligned section id — u4 deliberately sets this so coverage stays True
|
||||
# under the terminal first-render invariant.
|
||||
assert status["full_mdx_coverage"] is True
|
||||
assert status["overall"] == "PASS"
|
||||
# IMP-87 u4 inversion (Stage 1 anchor c53722ad): empty-shell-only runs
|
||||
# MUST NOT report PASS / full_mdx_coverage. u1 redefines coverage over
|
||||
# content units (frame_template_id == "__empty__" excluded) and u2 routes
|
||||
# empty_shell_dominant slides into the EMPTY_SHELL_NO_CONTENT branch
|
||||
# before the legacy 4-way ladder. Aligned section ids covered only by
|
||||
# the shell unit are surfaced in filtered_section_ids instead.
|
||||
assert status["full_mdx_coverage"] is False
|
||||
assert status["overall"] == "EMPTY_SHELL_NO_CONTENT"
|
||||
|
||||
|
||||
# ─── u6 case 4 : mixed selection — provisional + normal units coexist ──
|
||||
@@ -1357,10 +1360,14 @@ def test_u7_e2e_zero_v4_empty_shell_status_surface(u7_patch_selector_deps):
|
||||
assert shell_entry["phase_z_status"] == "empty_shell"
|
||||
assert shell_entry["frame_template_id"] == "__empty__"
|
||||
assert shell_entry["source_section_ids"] == ["S1", "S2"]
|
||||
# Coverage check — both sections counted as covered by the shell unit
|
||||
# (rendered=True path; PASS enum unchanged by provisional qualifier).
|
||||
assert status["full_mdx_coverage"] is True
|
||||
assert status["overall"] == "PASS"
|
||||
# IMP-87 u4 inversion (Stage 1 anchor c53722ad): even at the e2e level
|
||||
# an empty-shell-only run must surface as EMPTY_SHELL_NO_CONTENT, not
|
||||
# PASS. u1 excludes __empty__ frames from content coverage so the two
|
||||
# aligned sections end up in filtered_section_ids; u2's
|
||||
# empty_shell_dominant branch then short-circuits the overall ladder
|
||||
# ahead of any visual_check verdict.
|
||||
assert status["full_mdx_coverage"] is False
|
||||
assert status["overall"] == "EMPTY_SHELL_NO_CONTENT"
|
||||
|
||||
|
||||
# ─── u7 case 3 : e2e normal path unchanged when opt-in flags both on ─────
|
||||
|
||||
Reference in New Issue
Block a user