wip: phase_z2 evidence 파이프라인 + matching 실험(phase2~26) + 프론트 trace 패널 진행분 스냅샷

- src: phase_z2 composition/mapper/pipeline/placement_planner/retry, ai_fallback(prompts/schema/validate), mdx_text_atoms 신규
- Front: PipelineTracePanel 신규, FramePanel/SlideCanvas/Home/designAgentApi 등 갱신 + 테스트 4종 추가
- templates/phase_z2: catalog(component_expansion_registry, node_slot_mapping 신규), frames, families, slide_base 갱신
- tests/matching: phase2~26 매칭 실험 스크립트·리포트·온톨로지 전체 (미커밋 진행분)
- tests: b4_v4 evidence, task5~28.5 시리즈, regression(imp95 baseline) 등 신규 테스트 대량 추가
- docs/reference: MDX 구조 인벤토리, MDX→Frame 구조 계약 문서
- scripts: mdx 계약/parity/coverage/viewport 체크, gitea comment, run sync 유틸
- .gitignore: tmp*.json, chromedriver, .orchestrator, *.pkl, Front_test* 등 임시/스냅샷 제외

미완성 작업의 보존용 스냅샷 커밋 (2026-07-02)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-02 17:03:42 +09:00
co-authored by Claude Opus 4.8
parent 97b7833a1b
commit b836e79ee1
527 changed files with 673036 additions and 717 deletions
@@ -59,6 +59,11 @@ def test_system_prompt_locks_frame_id_swap() -> None:
assert "frame_id" in SYSTEM_PROMPT
def test_system_prompt_prefers_design_adaptation_plan_not_text_slots() -> None:
assert "design_adaptation_plan" in SYSTEM_PROMPT
assert "Do NOT create titles, labels, summaries, body text, or slot content" in SYSTEM_PROMPT
def test_build_prompt_returns_system_and_user() -> None:
prompt = build_ai_fallback_prompt(**_inputs())
assert set(prompt.keys()) == {"system", "user"}
@@ -78,6 +83,10 @@ def test_user_payload_carries_all_inputs_and_marks_mdx_read_only() -> None:
assert "mdx_text_READ_ONLY" in payload
assert payload["mdx_text_READ_ONLY"].startswith("# 대목차")
assert "mdx_text" not in payload # only the READ_ONLY key, not a writable alias
assert payload["task12_output_contract"]["preferred_proposal_kind"] == (
"design_adaptation_plan"
)
assert "slots" in payload["task12_output_contract"]["forbidden_content_fields"]
@pytest.mark.parametrize(
@@ -1,9 +1,9 @@
"""IMP-86 u4 integration: mdx03 reject override reaches Step 12 AI audit
"""IMP-86 u4 ??integration: mdx03 reject override reaches Step 12 AI audit
without heights_px ValueError; default-path regression.
Locks the Stage 2 guardrails from the IMP-86 issue body. Pre-u1, the mapper
``FitError`` handler appended only to ``adapter_needed_units`` and skipped
``zones_data`` / ``debug_zones`` leaving them at ``len=1`` while
``zones_data`` / ``debug_zones`` ??leaving them at ``len=1`` while
``build_layout_css("horizontal-2", ...)`` still returned ``R=2``. The
``_compute_per_zone_geometry`` invariant then raised
``ValueError("heights_px length 1 != grid rows R=2")`` BEFORE Step 12
@@ -14,16 +14,15 @@ was never written. Post u1+u2 :
* u2 pre-build invariant guard surfaces drift before
``_compute_per_zone_geometry`` raises;
* Step 12 AI router is reached and the audit artifact is produced;
* ``debug.json`` carries the adapter_needed marker (existing channel
u5 will add finer per-record telemetry).
* ``debug.json`` carries the adapter_needed marker (existing channel ?? u5 will add finer per-record telemetry).
AI router is patched to a deterministic ``None``-returning stub so the
integration coverage proves the wiring without any network / API / model
dependency (``feedback_ai_isolation_contract`` AI = fallback only).
dependency (``feedback_ai_isolation_contract`` ??AI = fallback only).
The pair ``(ai_called=False, skip_reason='router_short_circuit')`` is
the gather-side surface (``src/phase_z2_ai_fallback/step12.py:210-213``)
for ``route_ai_fallback None`` and is the explicit "router reached and
returned None" signal distinct from earlier-gate skips
for ``route_ai_fallback ??None`` and is the explicit "router reached and
returned None" signal ??distinct from earlier-gate skips
(``not_provisional`` / ``route_not_ai_adaptation:<hint>``) which would
indicate the router was NOT reached.
"""
@@ -49,7 +48,7 @@ def _stub_router_short_circuit() -> MagicMock:
Gather records ``(ai_called=False, skip_reason='router_short_circuit')``
per ``src/phase_z2_ai_fallback/step12.py:210-213`` when the router
returns ``None``. Asserting that pair on the reject record proves the
router was actually invoked vs. the earlier ``not_provisional`` or
router was actually invoked ??vs. the earlier ``not_provisional`` or
``route_not_ai_adaptation:<hint>`` skips which would indicate the
router was NEVER reached (the IMP-86 pre-fix failure mode, which
manifested as a ``ValueError`` crash before Step 12 even fired).
@@ -61,10 +60,11 @@ def _stub_router_short_circuit() -> MagicMock:
def test_integration_reject_override_reaches_step12_without_value_error(
tmp_path, monkeypatch
):
"""mdx03 + ``--override-frame 03-2=bim_dx_comparison_table`` must reach
Step 12 AI router without raising ``heights_px`` ValueError, and the
Step 12 audit artifact must reflect router reach on the provisional
reject unit.
"""mdx03 + frame override must reach Step 12/20 without geometry crash.
The current pipeline renders ``03-2 -> bim_dx_comparison_table`` through
the deterministic verbatim builder. It should not require AI routing or
adapter-needed placeholders.
"""
if not _SAMPLE_MDX_PATH.is_file():
pytest.skip(f"sample MDX not present: {_SAMPLE_MDX_PATH}")
@@ -82,122 +82,71 @@ def test_integration_reject_override_reaches_step12_without_value_error(
run_dir = tmp_path / "runs" / run_id / "phase_z2"
# Guardrail 1 — step12_ai_repair.json present. Pre-IMP-86, the
# pipeline crashed at _compute_per_zone_geometry before Step 12 fired
# so this artifact was MISSING under reject override (issue body
# primary symptom: "AI 호출 0 / step12_ai_repair.json 미생성").
step12_path = run_dir / "steps" / "step12_ai_repair.json"
assert step12_path.is_file(), (
f"step12_ai_repair.json missing at {step12_path} pre-IMP-86 "
"pipeline crashed at _compute_per_zone_geometry ValueError "
"before Step 12 fired."
f"step12_ai_repair.json missing at {step12_path} - pipeline did "
"not reach Step 12."
)
step12_data = json.loads(step12_path.read_text(encoding="utf-8"))
per_unit = step12_data["data"]["per_unit"]
assert len(per_unit) >= 1, "step12 per_unit must contain at least one record"
# Guardrail 2 — provisional reject unit reached the AI router.
# _apply_frame_override_to_unit (src/phase_z2_pipeline.py:1199-1208)
# promotes the unit to provisional + label='reject' when the override
# target matches a V4 reject judgment; the IMP-47B u1 route map then
# sends reject → 'ai_adaptation_required'.
reject_records = [
override_records = [
r for r in per_unit if r.get("source_section_ids") == ["03-2"]
]
assert len(reject_records) == 1, (
f"expected exactly one per_unit record for 03-2; got {reject_records}"
assert len(override_records) == 1, (
f"expected exactly one per_unit record for 03-2; got {override_records}"
)
reject = reject_records[0]
assert reject["provisional"] is True, (
"03-2 unit must be provisional after reject override per "
"_apply_frame_override_to_unit reject-judgment promotion"
override_record = override_records[0]
assert override_record["provisional"] is False, (
"03-2 should be non-provisional after deterministic verbatim builder recovery"
)
assert reject["route_hint"] == "ai_adaptation_required", (
f"03-2 route_hint must be 'ai_adaptation_required' "
f"(IMP-47B u1 reject→AI map); got {reject['route_hint']}"
assert override_record["route_hint"] == "ai_adaptation_required", (
f"03-2 route_hint should preserve the reject route surface; got "
f"{override_record['route_hint']}"
)
# The stub router returned None → gather records router_short_circuit.
# This pair is the explicit "router reached" surface; the alternative
# skips (not_provisional / route_not_ai_adaptation:*) would mean the
# router was NEVER called — the IMP-86 pre-fix failure mode.
assert reject["skip_reason"] == "router_short_circuit", (
f"03-2 must reach the (stubbed) AI router and record "
f"router_short_circuit; got skip_reason={reject['skip_reason']}, "
f"error={reject.get('error')}"
assert override_record["skip_reason"] == "not_provisional", (
f"03-2 should skip AI routing as non-provisional; got "
f"skip_reason={override_record['skip_reason']}, "
f"error={override_record.get('error')}"
)
assert reject["ai_called"] is False
assert reject["error"] is None
router.assert_called()
assert override_record["ai_called"] is False
assert override_record["error"] is None
router.assert_not_called()
# Guardrail 3 — pipeline reached Step 20 (no heights_px ValueError).
# The pre-IMP-86 crash happened BEFORE the Step 7 layout artifact, so
# the Step 20 final-status artifact is the strongest "pipeline did
# not crash mid-flight" signal.
step20_path = run_dir / "steps" / "step20_slide_status.json"
assert step20_path.is_file(), (
f"step20_slide_status.json missing at {step20_path} pipeline "
"did not reach final step (likely crashed mid-pipeline)."
f"step20_slide_status.json missing at {step20_path} - pipeline did "
"not reach final status."
)
# Guardrail 4 — debug.json adapter_needed marker. u1 placeholder
# writes a parallel zones_data + debug_zone record, but
# adapter_needed_units stays the authoritative adapter signal for
# debug.json consumers. u5 adds finer per-placeholder telemetry
# (adapter_needed=True / mapper_fit_error) on the placeholder records
# themselves (asserted in Guardrail 5 below).
# write_debug_json (src/phase_z2_pipeline.py:3254-3292) nests the
# slide_status payload from compute_slide_status (src/phase_z2_pipeline.py:2939-3128)
# under the top-level "slide_status" key, so adapter_needed_count and
# adapter_needed_units are addressed via debug["slide_status"][...]
# — not at the top level.
debug_path = run_dir / "debug.json"
assert debug_path.is_file(), f"debug.json missing at {debug_path}"
debug = json.loads(debug_path.read_text(encoding="utf-8"))
slide_status = debug.get("slide_status") or {}
assert slide_status.get("adapter_needed_count", 0) >= 1, (
f"debug.json slide_status.adapter_needed_count must reflect the "
f"FitError on 03-2; got {slide_status.get('adapter_needed_count')}"
)
adapter_units = slide_status.get("adapter_needed_units") or []
assert any(
"03-2" in (a.get("source_section_ids") or []) for a in adapter_units
), (
f"slide_status.adapter_needed_units must include the 03-2 reject "
f"override unit; got {adapter_units}"
assert slide_status.get("adapter_needed_count", 0) == 0, (
f"adapter_needed_count should stay zero after deterministic render recovery; "
f"got {slide_status.get('adapter_needed_count')}"
)
assert slide_status.get("overall") == "PASS"
# Guardrail 5 (IMP-86 u5) — per-placeholder telemetry on debug.json
# zones[] entry. Source: src/phase_z2_pipeline.py FitError handler
# appends adapter_needed=True + mapper_fit_error=<str(e)> + provisional
# on the placeholder debug_zone record so consumers reading the zones
# array alone (without joining against slide_status) can identify the
# adapter contract surface. adapter_needed_units stays the
# authoritative per-slide list (preserved by Guardrail 4 above).
debug_zones = debug.get("zones") or []
reject_zones = [
override_zones = [
z for z in debug_zones if list(z.get("source_section_ids") or []) == ["03-2"]
]
assert len(reject_zones) == 1, (
f"expected exactly one debug zones[] record for 03-2 (placeholder "
f"from FitError handler); got {reject_zones}"
assert len(override_zones) == 1, (
f"expected exactly one debug zones[] record for 03-2; got {override_zones}"
)
reject_zone = reject_zones[0]
assert reject_zone.get("adapter_needed") is True, (
f"debug zones[] placeholder for 03-2 must set adapter_needed=True; "
f"got {reject_zone.get('adapter_needed')!r}"
override_zone = override_zones[0]
assert override_zone.get("template_id") == "bim_dx_comparison_table"
assert override_zone.get("adapter_needed") is not True, (
f"debug zones[] record for 03-2 should not set adapter_needed=True; "
f"got {override_zone.get('adapter_needed')!r}"
)
fit_err_msg = reject_zone.get("mapper_fit_error")
assert isinstance(fit_err_msg, str) and fit_err_msg, (
f"debug zones[] placeholder for 03-2 must carry a non-empty "
f"mapper_fit_error string (FitError raised by map_mdx_to_slots); "
f"got {fit_err_msg!r}"
assert override_zone.get("slot_payload_keys"), (
f"debug zones[] record for 03-2 should carry rendered slot payload; "
f"got {override_zone}"
)
assert reject_zone.get("provisional") is True, (
f"debug zones[] placeholder for 03-2 must mirror the unit "
f"provisional state (reject override → provisional=True); got "
f"{reject_zone.get('provisional')!r}"
)
@pytest.mark.integration
def test_integration_default_path_no_override_no_regression(
@@ -223,10 +172,10 @@ def test_integration_default_path_no_override_no_regression(
# Pre-Step 12 crash would surface as both artifacts missing; default
# path must produce both.
assert (run_dir / "steps" / "step12_ai_repair.json").is_file(), (
"step12_ai_repair.json missing on default mdx03 path u1/u2 "
"step12_ai_repair.json missing on default mdx03 path ??u1/u2 "
"must be no-ops when mapper succeeds for every unit."
)
assert (run_dir / "steps" / "step20_slide_status.json").is_file(), (
"step20_slide_status.json missing on default mdx03 path "
"step20_slide_status.json missing on default mdx03 path ??"
"pipeline did not complete."
)
@@ -19,6 +19,7 @@ from src.phase_z2_ai_fallback import AiFallbackProposal, ProposalKind
"builder_options_patch",
"partial_overrides",
"slot_mapping_proposal",
"design_adaptation_plan",
],
)
def test_whitelisted_proposal_kinds_accepted(kind_value: str) -> None:
@@ -142,3 +142,56 @@ def test_internal_region_check_skipped_when_no_region_supplied() -> None:
},
)
validate_proposal(proposal, frame_contract=_FRAME_CONTRACT)
def test_design_adaptation_plan_accepts_structure_only_operations() -> None:
proposal = _make(
ProposalKind.DESIGN_ADAPTATION_PLAN,
{
"operations": [
{
"op": "increase_repeat_count",
"target": "pillars",
"count": 6,
},
{
"op": "compact_spacing",
"target": "frame",
"level": "medium",
},
],
"region_id": "zone_top.region_a",
},
)
validate_proposal(
proposal,
frame_contract=_FRAME_CONTRACT,
internal_region=_REGION,
)
def test_design_adaptation_plan_rejects_generated_text_fields() -> None:
proposal = _make(
ProposalKind.DESIGN_ADAPTATION_PLAN,
{
"operations": [
{
"op": "increase_repeat_count",
"target": "pillars",
"count": 4,
"label": "AI가 만든 라벨",
}
],
},
)
with pytest.raises(AiFallbackValidationError, match="content-bearing fields"):
validate_proposal(proposal, frame_contract=_FRAME_CONTRACT)
def test_design_adaptation_plan_requires_operations() -> None:
proposal = _make(ProposalKind.DESIGN_ADAPTATION_PLAN, {"region_id": "zone_top.region_a"})
with pytest.raises(AiFallbackValidationError, match="operations"):
validate_proposal(proposal, frame_contract=_FRAME_CONTRACT)