"""IMP-#85 u3b — Audit CLI invariant I4 (slot_payload ↔ builder generated keys). Scope (Stage 2 lock): I4 slot_payload refs — every key generated by the contract's builder must appear as a `slot_payload.` reference in the partial. Direction A only (dead generated key). Skipped when the partial uses dynamic bracket access (`slot_payload[...]`). `visual_pending: true` skipped (data-driven from catalog, matches u2/u3a invariant scope; no hard-coded frame allow-list). Out of scope (별 axis): - V4 runtime VP filter (u4). - Catalog regression coverage suite (u5). - Implementing the 17 missing VP builders. """ from __future__ import annotations import subprocess import sys from pathlib import Path import yaml REPO_ROOT = Path(__file__).resolve().parent.parent SCRIPT_PATH = REPO_ROOT / "scripts" / "audit_frame_invariants.py" def _write_yaml(path: Path, payload: dict) -> Path: path.write_text(yaml.safe_dump(payload, sort_keys=False), encoding="utf-8") return path def _run_cli(catalog: Path, partials: Path) -> subprocess.CompletedProcess: return subprocess.run( [ sys.executable, str(SCRIPT_PATH), "--catalog", str(catalog), "--partials-dir", str(partials), ], cwd=str(REPO_ROOT), capture_output=True, text=True, ) def test_prod_catalog_audit_passes_i4(): """Prod catalog + prod partials dir → no I4 violations on live contracts.""" from scripts.audit_frame_invariants import ( DEFAULT_CATALOG_PATH, DEFAULT_PARTIALS_DIR, check_i4_slot_payload_refs, ) from src.phase_z2_mapper import PAYLOAD_BUILDERS catalog = yaml.safe_load( DEFAULT_CATALOG_PATH.read_text(encoding="utf-8") ) or {} registered = set(PAYLOAD_BUILDERS.keys()) violations = check_i4_slot_payload_refs( catalog, DEFAULT_PARTIALS_DIR, registered ) assert violations == [], ( "Prod live contracts must satisfy I4 (every generated key is " "referenced by the partial, or partial uses dynamic access). " f"Got: {violations}" ) def test_extract_static_slot_refs_finds_dot_access(): from scripts.audit_frame_invariants import extract_static_slot_refs partial = ( "{{ slot_payload.title }}\n" "{% if slot_payload.foo %}{{ slot_payload.foo }}{% endif %}\n" "{% for x in slot_payload.bar %}{{ x }}{% endfor %}\n" ) refs = extract_static_slot_refs(partial) assert refs == {"title", "foo", "bar"} def test_extract_static_slot_refs_ignores_dynamic_bracket(): from scripts.audit_frame_invariants import extract_static_slot_refs partial = "{{ slot_payload['pill_' ~ n ~ '_label'] }}" # Dynamic access does NOT contribute dot-access refs. assert extract_static_slot_refs(partial) == set() def test_partial_uses_dynamic_slot_access_detects_bracket(): from scripts.audit_frame_invariants import partial_uses_dynamic_slot_access dynamic = "{{ slot_payload['pill_' ~ n ~ '_label'] }}" static = "{{ slot_payload.title }} and {{ slot_payload.body }}" assert partial_uses_dynamic_slot_access(dynamic) is True assert partial_uses_dynamic_slot_access(static) is False def test_expected_keys_quadrant_flat_slots_default_pattern(): from scripts.audit_frame_invariants import expected_payload_keys contract = { "payload": { "title": {"source": "section.title"}, "builder": "quadrant_flat_slots", "builder_options": { "item_parser": "quadrant_item", "pad_to": 4, "label_key_pattern": "quadrant_{n}_label", "body_key_pattern": "quadrant_{n}_body", }, } } keys = expected_payload_keys(contract) assert "title" in keys for n in range(1, 5): assert f"quadrant_{n}_label" in keys assert f"quadrant_{n}_body" in keys def test_expected_keys_quadrant_flat_slots_custom_pattern(): from scripts.audit_frame_invariants import expected_payload_keys contract = { "payload": { "title": {"source": "section.title"}, "builder": "quadrant_flat_slots", "builder_options": { "item_parser": "quadrant_item", "pad_to": 3, "label_key_pattern": "category_{n}_label", "body_key_pattern": "category_{n}_body", }, } } keys = expected_payload_keys(contract) assert keys == { "title", "category_1_label", "category_2_label", "category_3_label", "category_1_body", "category_2_body", "category_3_body", } def test_expected_keys_cycle_intersect_3(): from scripts.audit_frame_invariants import expected_payload_keys contract = { "payload": { "title": {"source": "section.title"}, "builder": "cycle_intersect_3", "builder_options": { "item_parser": "quadrant_item", "pad_to": 3, "label_key_pattern": "circle_{n}_label", }, } } keys = expected_payload_keys(contract) assert keys == { "title", "circle_1_label", "circle_2_label", "circle_3_label", "intersection", } def test_expected_keys_compare_table_2col(): from scripts.audit_frame_invariants import expected_payload_keys contract = { "payload": { "title": {"source": "section.title"}, "builder": "compare_table_2col", "builder_options": {"item_parser": "compare_row_2col_item"}, } } keys = expected_payload_keys(contract) assert keys == {"title", "col_a_label", "col_b_label", "rows"} def test_expected_keys_paired_rows_4x2_slots(): from scripts.audit_frame_invariants import expected_payload_keys contract = { "payload": { "title": {"source": "section.title"}, "builder": "paired_rows_4x2_slots", "builder_options": { "item_parser": "quadrant_item", "label_key_pattern": "row_{r}_{side}_label", "body_key_pattern": "row_{r}_{side}_body", "rows": 4, "sides": ["left", "right"], }, } } keys = expected_payload_keys(contract) assert "title" in keys for r in range(1, 5): for side in ("left", "right"): assert f"row_{r}_{side}_label" in keys assert f"row_{r}_{side}_body" in keys def test_expected_keys_process_product_pair(): from scripts.audit_frame_invariants import expected_payload_keys contract = { "payload": { "title": {"source": "section.title"}, "builder": "process_product_pair", "builder_options": { "pad_sections_to": 3, "columns": [ {"title_to": "banner_left", "body_to": "process", "body_parser": "column_with_transform"}, {"title_to": "banner_right", "body_to": "product", "body_parser": "column_with_transform"}, ], }, } } keys = expected_payload_keys(contract) assert keys == {"title", "banner_left", "process", "banner_right", "product"} def test_expected_keys_items_with_role(): from scripts.audit_frame_invariants import expected_payload_keys contract = { "payload": { "title": {"source": "section.title"}, "builder": "items_with_role", "builder_options": { "item_parser": "pillar_item", "array_root": "pillars", }, } } keys = expected_payload_keys(contract) assert keys == {"title", "pillars"} def test_i4_dead_generated_key_flagged(tmp_path): """Builder produces key X, partial doesn't reference it → I4 violation.""" from scripts.audit_frame_invariants import check_i4_slot_payload_refs partials_dir = tmp_path / "families" partials_dir.mkdir() # Partial only references `title` — missing category_2_label / _body etc. (partials_dir / "drift_frame.html").write_text( "
{{ slot_payload.title }}
" "
{{ slot_payload.category_1_label }}
" "
{{ slot_payload.category_1_body }}
", encoding="utf-8", ) catalog = { "drift_frame": { "template_id": "drift_frame", "payload": { "title": {"source": "section.title"}, "builder": "quadrant_flat_slots", "builder_options": { "item_parser": "quadrant_item", "pad_to": 2, "label_key_pattern": "category_{n}_label", "body_key_pattern": "category_{n}_body", }, }, }, } violations = check_i4_slot_payload_refs( catalog, partials_dir, registered_builders={"quadrant_flat_slots"} ) msgs = "\n".join(violations) assert "I4 generated-key-orphan" in msgs assert "drift_frame" in msgs assert "category_2_label" in msgs assert "category_2_body" in msgs # category_1 keys ARE referenced — must NOT be flagged. assert "slot_payload.category_1_label." not in msgs assert "slot_payload.category_1_body." not in msgs def test_i4_skips_partial_with_dynamic_bracket_access(tmp_path): """Dynamic bracket access in partial → I4 skipped (cannot resolve statically).""" from scripts.audit_frame_invariants import check_i4_slot_payload_refs partials_dir = tmp_path / "families" partials_dir.mkdir() (partials_dir / "dynamic_frame.html").write_text( "{{ slot_payload.title }}\n" "{% for n in range(1, 6) %}" "{{ slot_payload['pill_' ~ n ~ '_label'] }}" "{{ slot_payload['pill_' ~ n ~ '_body'] }}" "{% endfor %}", encoding="utf-8", ) catalog = { "dynamic_frame": { "template_id": "dynamic_frame", "payload": { "title": {"source": "section.title"}, "builder": "quadrant_flat_slots", "builder_options": { "item_parser": "quadrant_item", "pad_to": 5, "label_key_pattern": "pill_{n}_label", "body_key_pattern": "pill_{n}_body", }, }, }, } violations = check_i4_slot_payload_refs( catalog, partials_dir, registered_builders={"quadrant_flat_slots"} ) assert violations == [], ( "Dynamic bracket access must suppress I4 (cannot resolve statically); " f"got: {violations}" ) def test_i4_skips_visual_pending(tmp_path): """VP contract with drift → I4 skip (no violation).""" from scripts.audit_frame_invariants import check_i4_slot_payload_refs partials_dir = tmp_path / "families" partials_dir.mkdir() (partials_dir / "vp_frame.html").write_text( "
nothing
", encoding="utf-8" ) catalog = { "vp_frame": { "template_id": "vp_frame", "visual_pending": True, "payload": { "title": {"source": "section.title"}, "builder": "quadrant_flat_slots", "builder_options": { "item_parser": "quadrant_item", "pad_to": 4, }, }, }, } violations = check_i4_slot_payload_refs( catalog, partials_dir, registered_builders={"quadrant_flat_slots"} ) assert violations == [] def test_i4_skips_unregistered_builder(tmp_path): """Unregistered builder (already an I3 hit) → I4 silent on same contract.""" from scripts.audit_frame_invariants import check_i4_slot_payload_refs partials_dir = tmp_path / "families" partials_dir.mkdir() (partials_dir / "ghost_frame.html").write_text( "{{ slot_payload.title }}", encoding="utf-8" ) catalog = { "ghost_frame": { "template_id": "ghost_frame", "payload": { "title": {"source": "section.title"}, "builder": "ghost_builder_not_in_registry", }, }, } violations = check_i4_slot_payload_refs( catalog, partials_dir, registered_builders={"quadrant_flat_slots"} ) assert violations == [], ( "Unregistered builder is already flagged by I3 — I4 must stay silent " f"on the same contract; got: {violations}" ) def test_i4_skips_missing_partial(tmp_path): """Missing partial (already I1 hit) → I4 silent on same contract.""" from scripts.audit_frame_invariants import check_i4_slot_payload_refs partials_dir = tmp_path / "families" partials_dir.mkdir() # No partial file written. catalog = { "missing_partial_frame": { "template_id": "missing_partial_frame", "payload": { "title": {"source": "section.title"}, "builder": "quadrant_flat_slots", "builder_options": { "item_parser": "quadrant_item", "pad_to": 4, }, }, }, } violations = check_i4_slot_payload_refs( catalog, partials_dir, registered_builders={"quadrant_flat_slots"} ) assert violations == [] def test_cli_pass_on_prod_paths(tmp_path): """End-to-end CLI on prod paths reports PASS with I1-I4 wording.""" result = subprocess.run( [sys.executable, str(SCRIPT_PATH)], cwd=str(REPO_ROOT), capture_output=True, text=True, ) assert result.returncode == 0, result.stdout + result.stderr assert "PASS (I1-I4 clean" in result.stdout def test_cli_fail_on_synthetic_i4_drift(tmp_path): """CLI exits 1 + emits I4 violation when a non-VP contract has dead keys.""" partials_dir = tmp_path / "families" partials_dir.mkdir() (partials_dir / "drift_frame.html").write_text( "{{ slot_payload.title }}", encoding="utf-8" ) catalog_path = _write_yaml( tmp_path / "frame_contracts.yaml", { "drift_frame": { "template_id": "drift_frame", "payload": { "title": {"source": "section.title"}, "builder": "quadrant_flat_slots", "builder_options": { "item_parser": "quadrant_item", "pad_to": 2, "label_key_pattern": "category_{n}_label", "body_key_pattern": "category_{n}_body", }, }, }, }, ) result = _run_cli(catalog_path, partials_dir) assert result.returncode == 1, result.stdout + result.stderr assert "I4 generated-key-orphan" in result.stdout assert "category_1_label" in result.stdout