feat(#84): IMP-84 u1~u3 silent automation policy enforcement (FramePanel reject confirm + slide_base provisional badge/outline + IMP-30 visual assertions inverted)
Some checks failed
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 21s

- u1 FramePanel.tsx: extract `applyFrameSelection(candidate, onFrameSelect)`
  pure helper; collapse `handleFrameSelect` to direct onFrameSelect for every
  V4 label; drop `window.confirm` reject popup (IMP-47B u11 regression noise
  per `feedback_auto_pipeline_first`). New vitest pin `imp84_framepanel_reject_silent.test.ts`
  covers helper invocation across all 4 V4 labels + source-presence pins.
- u2 templates/phase_z2/slide_base.html: delete `.zone--provisional` CSS,
  `.zone__needs-adaptation-badge` CSS, the zone--provisional class fragment
  in the zone div, and the badge `<span>` render at the provisional zone.
  Preserve `data-provisional="1"` attribute as silent telemetry. New pytest
  `tests/phase_z2/test_imp84_provisional_silent_render.py` pins the silent
  contract independently of the IMP-30 first-render file.
- u3 tests/test_phase_z2_imp30_first_render.py: invert the three IMP-30 u5
  positive provisional-visual assertions to IMP-84 silent-contract negatives
  (no class, no badge, no CSS selectors); preserve positive `data-provisional`
  telemetry assertions. Docstrings updated to IMP-84 silent contract.

Out of scope (Round #4 + #92 contract): Home.tsx `toast.error(aiReviewMsg)`
call line, designAgentApi.ts `api_error_kinds`/`api_error_kind` schema and
operational-only formatter, FramePanel reject badge/tooltip read-only labels
(L102/L147/L156), and backend `zone.provisional` flag emission.

Stage 4 PASS: u1 vitest 10/10, u2 pytest 5/5, u3 pytest 29/29 (incl. 3
IMP-84 inverted assertions: `test_imp84_provisional_zone_silent_no_class_no_badge`,
`test_imp84_provisional_badge_never_rendered_in_mixed_zones`,
`test_imp84_slide_base_css_strips_provisional_visual_selectors`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 14:15:02 +09:00
parent f0d4494409
commit b9747c2f4a
5 changed files with 449 additions and 89 deletions

View File

@@ -0,0 +1,249 @@
"""IMP-84 u2 — provisional zone silent-render contract.
Pins that `templates/phase_z2/slide_base.html` no longer surfaces the
provisional visual treatment (dashed outline, striped wash, badge span)
while keeping `data-provisional="1"` as silent telemetry on the zone div.
Stage 2 binding contract (IMP-84):
- Remove .zone--provisional class emission on the zone div.
- Remove .zone__needs-adaptation-badge <span> render.
- Remove the .zone--provisional CSS block and the
.zone__needs-adaptation-badge CSS block from the <style> section.
- Preserve data-provisional="1" attribute emission for provisional zones
(downstream telemetry / debug selectors). Out-of-scope: backend
`zone.provisional` flag emission itself.
Helpers below intentionally mirror the IMP-30 first-render test helpers
(_render_slide_base / _all_zone_div_openings / _all_badge_spans /
_zone_div_for_position) so the silent-render contract is enforced by an
independent rendering surface, not by the IMP-30 file (which u3 will
invert separately).
"""
from __future__ import annotations
import re
from pathlib import Path
from jinja2 import Environment, FileSystemLoader, select_autoescape
# ─── helpers (mirrored from IMP-30 u5 to keep this test self-contained) ───
def _render_slide_base(
zones: list[dict],
*,
layout_preset: str = "single",
layout_css: dict | None = None,
) -> str:
"""Render templates/phase_z2/slide_base.html via Jinja2 with a minimal
zones list. Bypasses render_slide() so the template-only silent-render
contract is exercised without the pipeline (no mapper, no contracts,
no token CSS loader). slot_payload / partial_html are stubbed so the
assertions focus on zone div / CSS surface only."""
template_dir = (
Path(__file__).resolve().parents[2] / "templates" / "phase_z2"
)
env = Environment(
loader=FileSystemLoader(str(template_dir)),
autoescape=select_autoescape(["html"]),
)
if layout_css is None:
layout_css = {
"cols": "1fr",
"rows": "1fr",
"areas": '"single"',
}
for z in zones:
z.setdefault("partial_html", "<div class=\"_stub_partial\">stub</div>")
base = env.get_template("slide_base.html")
return base.render(
slide_title="IMP-84 u2 silent-render test",
slide_footer=None,
zones=zones,
layout_preset=layout_preset,
layout_css=layout_css,
gap_px=12,
token_css="",
embedded_mode="standalone",
)
def _zone_div_for_position(html: str, position: str) -> str:
"""Return the opening `<div class="zone..." data-zone-position="X" ...>`
tag for the zone at the given `data-zone-position`. Anchors assertions
on zone-div-level attributes / classes only."""
pattern = re.compile(
r'<div class="zone[^"]*"\s+data-zone-position="'
+ re.escape(position)
+ r'"[^>]*>',
re.DOTALL,
)
match = pattern.search(html)
return match.group(0) if match else ""
def _all_zone_div_openings(html: str) -> list[str]:
"""Every zone-div opening tag in the layout body. Scopes class /
attribute checks away from the <style> block (which may still contain
selector strings if a future change re-introduces them)."""
return re.findall(
r'<div class="zone[^"]*"[^>]*data-zone-position="[^"]*"[^>]*>',
html,
)
def _all_badge_spans(html: str) -> list[str]:
"""Every `.zone__needs-adaptation-badge` <span> element in the rendered
body. Must be empty under the IMP-84 silent-render contract regardless
of zones[i].provisional value."""
return re.findall(
r'<span class="zone__needs-adaptation-badge"[^>]*>[^<]*</span>',
html,
)
# ─── case 1 : non-provisional zone unchanged (regression boundary) ───
def test_imp84_non_provisional_zone_unchanged():
"""zones[i].provisional=False must render the zone div with no
provisional class, no data-provisional attr, no badge — byte-equivalent
to pre-IMP-84 baseline for the non-provisional path."""
zones = [
{
"position": "single",
"template_id": "MOCK_template_direct_a",
"slot_payload": {},
"content_weight": {"score": 1},
"min_height_px": 100,
"provisional": False,
}
]
html = _render_slide_base(zones)
zone_open = _zone_div_for_position(html, "single")
assert zone_open != ""
assert "zone--provisional" not in zone_open
assert "data-provisional" not in zone_open
assert _all_badge_spans(html) == []
# ─── case 2 : provisional zone silent — class / badge / wash removed ───
def test_imp84_provisional_zone_emits_data_attr_only_no_visual():
"""The core IMP-84 silent-render contract.
With zones[i].provisional=True, the rendered HTML MUST:
- NOT contain the `zone--provisional` class on any zone div
- NOT render a `.zone__needs-adaptation-badge` <span>
- NOT contain the human-visible "needs adaptation" label text
- STILL emit `data-provisional="1"` on the provisional zone div
(silent telemetry preserved for downstream selectors)
"""
zones = [
{
"position": "single",
"template_id": "MOCK_template_restructure_a",
"slot_payload": {},
"content_weight": {"score": 1},
"min_height_px": 100,
"provisional": True,
}
]
html = _render_slide_base(zones)
zone_open = _zone_div_for_position(html, "single")
assert zone_open != ""
assert "zone--provisional" not in zone_open
assert 'data-provisional="1"' in zone_open
assert _all_badge_spans(html) == []
assert "needs adaptation" not in html
# ─── case 3 : mixed zones — telemetry isolation preserved ───
def test_imp84_mixed_zones_data_provisional_only_on_provisional_zone():
"""In a mixed-zone slide (one provisional + one normal), the silent
telemetry attribute must appear ONLY on the provisional zone div, and
no visual artifact may surface on either zone."""
zones = [
{
"position": "top",
"template_id": "MOCK_template_direct_a",
"slot_payload": {},
"content_weight": {"score": 1},
"min_height_px": 100,
"provisional": False,
},
{
"position": "bottom",
"template_id": "MOCK_template_restructure_a",
"slot_payload": {},
"content_weight": {"score": 1},
"min_height_px": 100,
"provisional": True,
},
]
layout_css = {
"cols": "1fr",
"rows": "1fr 1fr",
"areas": '"top" "bottom"',
}
html = _render_slide_base(
zones, layout_preset="vertical-2", layout_css=layout_css
)
zone_divs = _all_zone_div_openings(html)
assert len(zone_divs) == 2
top_zone_open = _zone_div_for_position(html, "top")
bottom_zone_open = _zone_div_for_position(html, "bottom")
assert "data-provisional" not in top_zone_open
assert 'data-provisional="1"' in bottom_zone_open
for tag in zone_divs:
assert "zone--provisional" not in tag
assert _all_badge_spans(html) == []
assert "needs adaptation" not in html
# ─── case 4 : <style> block free of provisional visual selectors ───
def test_imp84_style_block_has_no_provisional_visual_selectors():
"""The provisional visual CSS classes are deleted at source. A future
refactor that re-introduces `.zone--provisional` or
`.zone__needs-adaptation-badge` selectors into slide_base.html breaks
this test rather than silently restoring the visual badge."""
zones = [
{
"position": "single",
"template_id": "MOCK_template_restructure_a",
"slot_payload": {},
"content_weight": {"score": 1},
"min_height_px": 100,
"provisional": True,
}
]
html = _render_slide_base(zones)
assert ".zone--provisional" not in html
assert ".zone__needs-adaptation-badge" not in html
assert "zone__needs-adaptation-badge" not in html
# ─── case 5 : provisional defaults to false (template fallback) ───
def test_imp84_provisional_none_falls_back_to_silent_non_provisional():
"""When zones[i].provisional is explicitly None (falsy but not False),
the template's truthy check must NOT emit `data-provisional`. Pins the
template fallback so a refactor cannot silently invert the default."""
zones = [
{
"position": "single",
"template_id": "MOCK_template_direct_a",
"slot_payload": {},
"content_weight": {"score": 1},
"min_height_px": 100,
"provisional": None,
}
]
html = _render_slide_base(zones)
zone_open = _zone_div_for_position(html, "single")
assert zone_open != ""
assert "data-provisional" not in zone_open
assert _all_badge_spans(html) == []