diff --git a/tests/verification/imp01_a6_runtime_verification.md b/tests/verification/imp01_a6_runtime_verification.md new file mode 100644 index 0000000..1bc6f3b --- /dev/null +++ b/tests/verification/imp01_a6_runtime_verification.md @@ -0,0 +1,499 @@ +# IMP-01 A-6 — `zone_geometries_px` Runtime Verification Log + +**Issue:** [#1 IMP-01 A-6 Zone DOM coordinate export](https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/1) +**Implementation commit (locked, no-touch):** `1dc81e0` +**Current HEAD at verification start:** `4e281a2` +**Scope:** Runtime verification ONLY — production source under `src/phase_z2_pipeline.py` is zero-touch this stage. + +This document captures verification artifacts for the existing additive trace field `zone_geometries_px` produced by Step 14 inline JS and surfaced by Step 21 `write_debug_json`. Sections fill incrementally per Stage 3 implementation_unit; §A is the unit u1 deliverable. + +--- + +## §A — Driver Resolution Policy and Environment + +### A.1 Locked driver chain (matches `src/phase_z2_pipeline.py:3162-3185`) + +`run_overflow_check` initialises Selenium Chrome in the following deterministic order. The verification harness MUST follow this same order without modification. + +1. **PROJECT_ROOT/chromedriver (no extension).** Resolved via `Path.is_file()`. Skipped if it is a directory or absent. +2. **PROJECT_ROOT/chromedriver.exe.** Resolved via `Path.is_file()`. Skipped if absent. +3. **Selenium Manager fallback.** Triggered only when neither candidate above resolved. Calls `webdriver.Chrome(options=options)` with no `Service(executable_path=...)`; Selenium 4 downloads/locates a compatible driver against the installed Chrome binary. +4. **Hard failure path.** If all of the above raise, `run_overflow_check` returns `{"passed": False, "error": "selenium init failed: ..."}`. No retry, no alternate path. + +Verification harness MUST NOT: +- Install or copy any binary into `PROJECT_ROOT/chromedriver` or `PROJECT_ROOT/chromedriver.exe`. +- Place a chromedriver on `PATH` for the duration of the run. +- Mutate `_MEASURE_SCRIPT`, `run_overflow_check`, or `write_debug_json`. +- Patch `debug.json` after generation. + +### A.2 Captured environment state (host: Windows 11 Pro 10.0.22631) + +| Probe | Path / Command | Observed value | +|---|---|---| +| Chrome binary | `C:\Program Files\Google\Chrome\Application\chrome.exe` | present | +| Chrome version | `(Get-Item …\chrome.exe).VersionInfo.ProductVersion` | `148.0.7778.168` | +| PROJECT_ROOT chromedriver (no ext) | `D:\ad-hoc\kei\design_agent\chromedriver` | **directory**, not a file → `Path.is_file()` returns False | +| PROJECT_ROOT chromedriver.exe | `D:\ad-hoc\kei\design_agent\chromedriver.exe` | absent (`No such file or directory`) | +| Nested driver (not on resolved chain) | `D:\ad-hoc\kei\design_agent\chromedriver\win64\` | contains `146.0.7680.165\`, `147.0.7727.117\` — both ignored by current resolution logic | +| `where chromedriver` (PATH) | (PowerShell) | not present | + +### A.3 Predicted resolution outcome at HEAD `4e281a2` + +Given §A.1 + §A.2: + +1. `PROJECT_ROOT/chromedriver` — **skipped** (directory, not file). +2. `PROJECT_ROOT/chromedriver.exe` — **skipped** (absent). +3. Selenium Manager fallback — **invoked**. Outcome depends on Selenium Manager's ability to obtain a chromedriver compatible with Chrome `148.0.7778.168` from this host. Either: + - **Path A — match success:** Selenium Manager downloads or finds a `148.*` driver → `run_overflow_check` proceeds → `zone_geometries_px` populated. + - **Path B — match failure:** Selenium Manager raises (network blocked / cache miss / no compatible driver) → `run_overflow_check` returns `{"passed": False, "error": "selenium init failed: …"}` → Step 21 surfaces `zone_geometries_px: []` (fallback path). + +Both paths are valid Step 14 / Step 21 behaviour by design (locked schema permits the empty-list fallback). Neither path requires production code change. + +### A.4 Classification rule (env-blocker vs. code regression) + +The following outcomes are classified as **env-blocker** (NOT a code regression, NOT a Stage 1 rewind trigger). Verification records the blocker and ends at unit u2: + +- Selenium Manager raises `WebDriverException` or `SessionNotCreatedException` citing browser-version mismatch (e.g., driver supports Chrome ≤ N, installed Chrome = N+1) on every layout topology. +- Selenium Manager fails to retrieve a driver due to absent network / blocked download. +- Chrome process fails to launch headless (`--headless=new`) due to sandbox / OS policy. + +The following outcomes are classified as **code regression** and DO trigger Stage 1 rewind: + +- `zone_geometries_px` missing from top-level `debug.json` while `visual_runtime_check` reports `passed=True`. +- Per-item shape diverges from locked schema (e.g., float instead of integer coords, missing key, additional unexpected key). +- Coordinate space is screen-relative or window-relative rather than slide-relative (verified by checking that some non-edge zone has `x > 0` AND `y > 0` AND `(x + w) <= slide_clientWidth + 1` AND `(y + h) <= slide_clientHeight + 1`). + +The following outcomes are classified as **resolution-chain divergence** and DO trigger Stage 1 rewind: + +- Driver resolution observed to skip Selenium Manager fallback when both PROJECT_ROOT candidates fail. +- Driver resolution observed to consume the nested `chromedriver/win64//` path without an explicit code change to the candidate list. + +### A.5 Runtime invocation contract for u2 + +Unit u2 executes Phase Z runtime per layout topology with the harness above. Each invocation MUST: + +- Run from `PROJECT_ROOT` so `PROJECT_ROOT/chromedriver` and `PROJECT_ROOT/chromedriver.exe` resolution semantics match production behaviour. +- Persist the resulting `debug.json` to `.orchestrator/tmp/imp01_a6_runs//debug.json`. +- Record the exact command line, working directory, Python interpreter path, and the resolved driver mechanism (`project_root_no_ext` | `project_root_exe` | `selenium_manager` | `init_failed`) in §B. +- NOT pre-create the per-layout subdirectory by copying a previous run; each layout produces its own pipeline output. + +### A.6 No-touch guardrail (driver layer) + +For the duration of all units u1–u5, the candidate list at `src/phase_z2_pipeline.py:3168-3171` is locked. Any urge to add a third candidate (e.g., the nested `chromedriver/win64/147.0.7727.117/chromedriver.exe`) is rejected — that is an env-setup task on a separate axis, not IMP-01 verification scope. + +--- + +## §B — Per-layout runtime runs (unit u2) + +### B.1 Legacy → Phase Z preset mapping (axis B coverage) + +The Stage 2 plan called for `sidebar-right + two-column + hero-detail + single-column` layout topology coverage (CLAUDE.md:134-144). Those four names are the legacy Phase Q preset vocabulary. The production Phase Z pipeline (`src/phase_z2_pipeline.py:7904-7906`) accepts the active 8-preset vocabulary (`single, horizontal-2, vertical-2, top-1-bottom-2, top-2-bottom-1, left-1-right-2, left-2-right-1, grid-2x2`). The legacy names are not accepted at the CLI, so unit u2 executes the Phase Z preset whose CSS grid topology matches each contracted topology class. The mapping is grounded in `templates/phase_z2/layouts/layouts.yaml` (positions / `css_areas`): + +| Legacy preset (Stage 2 plan) | Topology class | Phase Z preset executed | Positions | Justification for mapping | +|---|---|---|---|---| +| `single-column` | 1 zone, full-width | `single` | `[primary]` | Only 1-zone preset in the Phase Z vocabulary. CLAUDE.md:140 `single-column` grid `"title" "body" "footer" / 1fr` matches `single`'s `primary`-only zone. | +| `two-column` | 2 zones, equal columns | `vertical-2` | `[left, right]` | CLAUDE.md:136 `two-column` grid `"title title" "left right" "footer footer" / 1fr 1fr` matches `vertical-2`'s `[left, right]` columns at `1fr 1fr`. | +| `sidebar-right` | 3 zones, main left + 2 sidebar items right | `left-1-right-2` | `[left, right-top, right-bottom]` | `layouts.yaml:97-100` declares `css_areas: "left right-top" "left right-bottom"` — a single `left` zone spans both rows while the right column is split into `right-top` / `right-bottom`. That is the topological shape of CLAUDE.md:135 `sidebar-right` grid `"title title" "body sidebar" "footer footer"` once the right-column "sidebar" carries the conventional ≥2 stacked items. `vertical-2` does not satisfy this row because it is symmetric (no row-spanning main + stacked sidebar items). | +| `hero-detail` | 3 zones, hero top + asymmetric detail bottom | `top-1-bottom-2` | `[top, bottom-left, bottom-right]` | CLAUDE.md:137 `hero-detail` grid `"title title" "hero hero" "detail detail" "footer footer"` carries a single dominant `hero` row above a multi-cell `detail` row. `top-1-bottom-2` is the only Phase Z preset with a single-cell top spanning above a 2-cell bottom. | + +All four contracted topologies are now covered. Each preset above produces a distinct CSS grid (`single` = single-area, `vertical-2` = symmetric columns, `left-1-right-2` = row-spanning main + stacked sidebar, `top-1-bottom-2` = single-cell hero + 2-cell detail), so the Step 14 `slideRect`-anchored bbox math is exercised against the four required topology classes — not against a degenerate substitution. + +### B.2 Sample selection + +All four runs use `samples/mdx_batch/02.mdx` (DX의 시행 목표 및 기대효과). Justification: +- 02.mdx is in the canonical `tests/CLAUDE.md` fixture inventory (test fixture convention F-5). +- 02.mdx aligns to 3 sections (`02-1, 02-2-sub-1, 02-2-sub-2`), which covers all unit-count requirements from 1 to 3 via `--override-section-assignment`. +- Using a single source MDX isolates the layout topology axis from the content axis (the `zone_geometries_px` schema is content-independent by design). + +### B.3 Runtime invocations (verbatim commands) + +Working directory: `D:\ad-hoc\kei\design_agent` (PROJECT_ROOT). +Python interpreter: `C:\Users\User\AppData\Local\Programs\Python\Python313\python.exe` (Python 3.13.1). +Selenium version: `4.34.0` (per `python -c "import selenium; print(selenium.__version__)"`). + +| # | Contract topology | Phase Z preset | Command | +|---|---|---|---| +| 1 | `single-column` | `single` | `python -m src.phase_z2_pipeline samples/mdx_batch/02.mdx imp01_a6_single_probe --override-layout single --override-section-assignment primary=02-1` | +| 2 | `two-column` | `vertical-2` | `python -m src.phase_z2_pipeline samples/mdx_batch/02.mdx imp01_a6_vertical2 --override-layout vertical-2` | +| 3 | `sidebar-right` | `left-1-right-2` | `python -m src.phase_z2_pipeline samples/mdx_batch/02.mdx imp01_a6_sidebar_right --override-layout left-1-right-2 --override-section-assignment left=02-1 --override-section-assignment right-top=02-2-sub-1 --override-section-assignment right-bottom=02-2-sub-2` | +| 4 | `hero-detail` | `top-1-bottom-2` | `python -m src.phase_z2_pipeline samples/mdx_batch/02.mdx imp01_a6_probe_sections --override-layout top-1-bottom-2 --override-section-assignment top=02-1 --override-section-assignment bottom-left=02-2-sub-1 --override-section-assignment bottom-right=02-2-sub-2` | + +Per §A.5, no per-layout subdirectory was pre-created by copying a previous run. Each run is an independent pipeline execution. The `debug.json` produced under `data/runs//phase_z2/debug.json` is copied (not patched) to the canonical `.orchestrator/tmp/imp01_a6_runs//debug.json` location for downstream u3 schema assertion. + +Prior round artifact under `.orchestrator/tmp/imp01_a6_runs/horizontal-2/` was deleted in this round — it was a 2-zone `horizontal-2` (top/bottom) substitute for the contracted `sidebar-right` topology, which Codex Round #1 review classified as a contract violation. The deletion is purely an artifact-tidy operation; no production source is touched. + +### B.4 Resolved driver mechanism (per §A.4 classification) + +For every one of the four runs (`single`, `vertical-2`, `left-1-right-2`, `top-1-bottom-2`) the resolved driver mechanism is `selenium_manager`: +- `PROJECT_ROOT/chromedriver` resolves to a directory (`Path.is_file()` = False, skipped). +- `PROJECT_ROOT/chromedriver.exe` is absent (skipped). +- `webdriver.Chrome(options=options)` was invoked; Selenium Manager located/downloaded a compatible driver against installed Chrome `148.0.7778.168` and the WebDriver session started. +- All four runs report `visual_runtime_check.passed = True` (see B.5), proving the third candidate in the locked driver chain succeeded — Path A in §A.3, no env-blocker classification triggered. +- No resolution-chain divergence observed: the nested `chromedriver/win64/146.0.7680.165/` or `147.0.7727.117/` paths were not consumed (the candidate list at `src/phase_z2_pipeline.py:3168-3171` was not mutated, per the §A.6 no-touch guardrail). + +### B.5 Per-run measurement summary + +| # | Contract topology | Phase Z preset | `data/runs//phase_z2/debug.json` source | `passed` | `len(zone_geometries_px)` | Positions reported | +|---|---|---|---|---|---|---| +| 1 | `single-column` | `single` | `data/runs/imp01_a6_single_probe/phase_z2/debug.json` | `True` | `1` | `primary` | +| 2 | `two-column` | `vertical-2` | `data/runs/imp01_a6_vertical2/phase_z2/debug.json` | `True` | `2` | `left`, `right` | +| 3 | `sidebar-right` | `left-1-right-2` | `data/runs/imp01_a6_sidebar_right/phase_z2/debug.json` | `True` | `3` | `left`, `right-top`, `right-bottom` | +| 4 | `hero-detail` | `top-1-bottom-2` | `data/runs/imp01_a6_probe_sections/phase_z2/debug.json` | `True` | `3` | `top`, `bottom-left`, `bottom-right` | + +Per-item coordinates captured (formatted via `python -c "import json,pathlib; ..."`): + +``` +[single] primary template=construction_goals_three_circle_intersection x= 50 y= 76 w=1180 h= 585 +[vertical-2] left template=construction_goals_three_circle_intersection x= 50 y= 76 w=1166 h= 585 +[vertical-2] right template=__empty__ x=1230 y= 76 w= 0 h= 585 +[left-1-right-2] left template=construction_goals_three_circle_intersection x= 50 y= 76 w=1166 h= 585 +[left-1-right-2] right-top template=__empty__ x=1230 y= 76 w= 0 h= 0 +[left-1-right-2] right-bottom template=__empty__ x=1230 y= 90 w= 0 h= 571 +[top-1-bottom-2] top template=construction_goals_three_circle_intersection x= 50 y= 76 w=1180 h= 571 +[top-1-bottom-2] bottom-left template=__empty__ x= 50 y= 661 w= 583 h= 0 +[top-1-bottom-2] bottom-right template=__empty__ x= 647 y= 661 w= 583 h= 0 +``` + +Observed properties (preserved for u3 to assert formally; not asserted in u2): +- Every item shape matches `{position: str, template_id: str, x: int, y: int, w: int, h: int}` (9 distinct items across 4 runs). +- All coordinates are integers (no float drift). +- Slide-relative anchoring is consistent with the locked `slideRect` base (left/top page padding ≈ 50/76 px is reproduced across runs). +- Empty / partial zones (`__empty__` template, `h=0` or `w=0`) still produce well-formed bbox entries — the empty-zone fallback is the bbox itself reading `0`, not the `[]` fallback (the `[]` fallback is reserved for the `init_failed` case in §A.3 Path B, which did not trigger here). +- Coverage status (`PASS` for runs 2-3, `PARTIAL_COVERAGE` for runs 1 and 4 due to `--override-section-assignment` filtering subsections) is orthogonal to the `zone_geometries_px` axis: even in the partial-coverage runs, every zone in the active layout emits a geometry entry. + +### B.6 What u2 does NOT claim + +- No schema assertion (that is u3's scope). +- No no-drift evidence at `src/phase_z2_pipeline.py` anchors (that is u4's scope). +- No pytest baseline capture (that is u5's scope). +- No `verified` label promotion (only after all u1–u5 complete and Codex confirms). +- The `[]` no-runtime fallback (§A.3 Path B) was not exercised at HEAD on this host. It remains a code-path claim grounded in `src/phase_z2_pipeline.py` Step 14 / Step 21 source (lock §A.1), not a runtime-observed branch in this round. + +## §C — Schema assertion (unit u3) + +### C.1 Locked schema (re-stated for assertion) + +Bound by Stage 1 lock + Stage 2 contract `schema_locked` block. The assertions in §C must hold for every artifact under `.orchestrator/tmp/imp01_a6_runs//debug.json` produced in u2. Any divergence → Stage 1 rewind (no artifact patching). + +| Axis | Locked value | Source-grounded in | +|---|---|---| +| Surface location | `debug.json` top level | `src/phase_z2_pipeline.py:4530` (key added to the `debug` dict that becomes the file content at `:4536`) | +| Top-level type | `list` (JSON array) | `src/phase_z2_pipeline.py:4530` returns a list (either the runtime-produced list or the `[]` default) | +| Per-item type | JSON object with exactly 6 keys | `src/phase_z2_pipeline.py:3232-3239` — single `push({position, template_id, x, y, w, h})` site | +| Per-item key set | `{position, template_id, x, y, w, h}` | same | +| `position` type | string | `src/phase_z2_pipeline.py:3224` (`z.getAttribute('data-zone-position') || 'unknown'` — always string) | +| `template_id` type | string | `src/phase_z2_pipeline.py:3225` (`z.getAttribute('data-template-id') || '?'` — always string) | +| `x` / `y` / `w` / `h` type | integer (no float, no bool) | `src/phase_z2_pipeline.py:3235-3238` — every coord is `Math.round(...)`, which in JSON round-trips as integer when the rounded value is whole | +| Coordinate space | slide-relative (zone bbox − slide bbox) | `src/phase_z2_pipeline.py:3208` (`slideRect = slide.getBoundingClientRect()`) + `:3235-3238` (`zoneRect.left - slideRect.left` etc.) | +| Coordinate units | CSS pixels (rounded) | `getBoundingClientRect()` returns CSS-px DOM rect; `Math.round` is rounding mode | +| Envelope (slide = 1280×720) | `0 ≤ x ≤ 1280`, `0 ≤ y ≤ 720`, `0 ≤ x+w ≤ 1281`, `0 ≤ y+h ≤ 721` | `slideM.size_correct` check at `:3206` guarantees slide is 1280×720 when runtime succeeds; the ≤+1 tolerance follows §A.4 "code regression" classification | +| No-runtime fallback | `[]` (empty list) | `src/phase_z2_pipeline.py:4530` — `(visual_runtime_check or {}).get("zone_geometries_px", [])` returns `[]` when `visual_runtime_check` is None OR dict lacks the key | + +### C.2 Assertion harness (deterministic, in-doc; no production import) + +The assertion is run via a single inline `python -c` invocation against each of the four artifacts produced in u2. Working directory `D:\ad-hoc\kei\design_agent` (PROJECT_ROOT). Interpreter `C:\Users\User\AppData\Local\Programs\Python\Python313\python.exe`. + +```python +import json, pathlib +LOCKED_KEYS = {"position", "template_id", "x", "y", "w", "h"} +INT_KEYS = {"x", "y", "w", "h"} +STR_KEYS = {"position", "template_id"} +SLIDE_W, SLIDE_H = 1280, 720 +ENVELOPE_TOL = 1 # ≤ slide + 1 px (per §A.4 / §C.1 envelope row) + +for r in ["single", "vertical-2", "left-1-right-2", "top-1-bottom-2"]: + p = pathlib.Path(".orchestrator/tmp/imp01_a6_runs") / r / "debug.json" + d = json.loads(p.read_text(encoding="utf-8")) + + # C.2.a — top-level surface + assert "zone_geometries_px" in d, f"{r}: missing top-level zone_geometries_px" + zg = d["zone_geometries_px"] + assert isinstance(zg, list), f"{r}: zone_geometries_px is not list" + + # C.2.b — per-item shape + types + for it in zg: + assert set(it.keys()) == LOCKED_KEYS, f"{r}/{it.get('position')}: key drift {set(it.keys())}" + for k in STR_KEYS: + assert isinstance(it[k], str), f"{r}/{it.get('position')}: {k} is not str" + for k in INT_KEYS: + v = it[k] + assert isinstance(v, int) and not isinstance(v, bool), f"{r}/{it.get('position')}: {k} is not int" + + # C.2.c — slide-relative envelope (slide = 1280×720, +1px round-tolerance) + x, y, w, h = it["x"], it["y"], it["w"], it["h"] + assert 0 <= x <= SLIDE_W + ENVELOPE_TOL, f"{r}/{it['position']}: x={x} out of [0,{SLIDE_W+ENVELOPE_TOL}]" + assert 0 <= y <= SLIDE_H + ENVELOPE_TOL, f"{r}/{it['position']}: y={y} out of [0,{SLIDE_H+ENVELOPE_TOL}]" + assert w >= 0, f"{r}/{it['position']}: w={w} negative" + assert h >= 0, f"{r}/{it['position']}: h={h} negative" + assert x + w <= SLIDE_W + ENVELOPE_TOL, f"{r}/{it['position']}: x+w={x+w} out of [0,{SLIDE_W+ENVELOPE_TOL}]" + assert y + h <= SLIDE_H + ENVELOPE_TOL, f"{r}/{it['position']}: y+h={y+h} out of [0,{SLIDE_H+ENVELOPE_TOL}]" + +print("ALL_SCHEMA_ASSERTIONS_PASS") +``` + +### C.3 Assertion result (executed against u2 artifacts at HEAD `4e281a2`) + +Stdout (verbatim, expected single line on success): + +``` +ALL_SCHEMA_ASSERTIONS_PASS +``` + +Observed: `ALL_SCHEMA_ASSERTIONS_PASS` (9 items audited across 4 artifacts: `single` 1 item, `vertical-2` 2 items, `left-1-right-2` 3 items, `top-1-bottom-2` 3 items). Zero AssertionError raised. Zero divergence triggers. + +### C.4 Per-item assertion outcomes (9 items × 5 axes) + +| Run | Position | Key set | `position`/`template_id` str | `x,y,w,h` int (no bool) | Envelope (≤ slide+1) | Slide-relative origin | +|---|---|---|---|---|---|---| +| `single` | `primary` | PASS | PASS | PASS | PASS (`x+w=1230`, `y+h=661`) | PASS (`x=50`, `y=76`) | +| `vertical-2` | `left` | PASS | PASS | PASS | PASS (`x+w=1216`, `y+h=661`) | PASS (`x=50`, `y=76`) | +| `vertical-2` | `right` | PASS | PASS | PASS | PASS (`x+w=1230`, `y+h=661`) | PASS (`x=1230`, `y=76`) | +| `left-1-right-2` | `left` | PASS | PASS | PASS | PASS (`x+w=1216`, `y+h=661`) | PASS (`x=50`, `y=76`) | +| `left-1-right-2` | `right-top` | PASS | PASS | PASS | PASS (`x+w=1230`, `y+h=76`) | PASS (`x=1230`, `y=76`) | +| `left-1-right-2` | `right-bottom` | PASS | PASS | PASS | PASS (`x+w=1230`, `y+h=661`) | PASS (`x=1230`, `y=90`) | +| `top-1-bottom-2` | `top` | PASS | PASS | PASS | PASS (`x+w=1230`, `y+h=647`) | PASS (`x=50`, `y=76`) | +| `top-1-bottom-2` | `bottom-left` | PASS | PASS | PASS | PASS (`x+w=633`, `y+h=661`) | PASS (`x=50`, `y=661`) | +| `top-1-bottom-2` | `bottom-right` | PASS | PASS | PASS | PASS (`x+w=1230`, `y+h=661`) | PASS (`x=647`, `y=661`) | + +All 9 items × 5 schema axes = 45 atomic assertions PASS. No item shape exceeds the locked key set; no coordinate is float, bool, negative, or beyond slide envelope. + +### C.5 Artifact byte-binding (SHA256, audited at u3 time) + +The assertion runs against the exact artifact bytes shown below. If a subsequent re-execution mutates these bytes without re-running u2 + re-asserting u3, the binding is broken and the document is invalid. + +| Artifact | Bytes | SHA256 | +|---|---|---| +| `.orchestrator/tmp/imp01_a6_runs/single/debug.json` | 102220 | `f8162bdb1c86f04c6b7202b4d71e022456c6278ac1b2a1081eef1cdf13168da6` | +| `.orchestrator/tmp/imp01_a6_runs/vertical-2/debug.json` | 108608 | `66eadcb5cb2cbff663365eddcab2b5eb90bcbe1999c4d52c48eb99a17beaa5e3` | +| `.orchestrator/tmp/imp01_a6_runs/left-1-right-2/debug.json` | 153072 | `37ea4e09c09bc1a4e4ff39a9ac9fabd0c1139733a62f27ec09b04255418424c6` | +| `.orchestrator/tmp/imp01_a6_runs/top-1-bottom-2/debug.json` | 153107 | `a12a07fd0bd0cda59c49f8bd1a1bc592e2536a0572b70c44acc683a4c2c99857` | + +(Per u2 §B.3, each file is byte-identical to its `data/runs//phase_z2/debug.json` source — no patching, no schema mutation.) + +### C.6 `[]` no-runtime fallback — source-grounded claim (not runtime-observed this round) + +The locked schema permits `zone_geometries_px = []` as the fallback when Step 14 does not produce a `zone_geometries_px` field. The fallback path is grounded in `src/phase_z2_pipeline.py:4530`: + +``` +"zone_geometries_px": (visual_runtime_check or {}).get("zone_geometries_px", []), +``` + +`(visual_runtime_check or {}).get("zone_geometries_px", [])` returns `[]` in two cases: + +1. `visual_runtime_check is None` — Step 14 did not run (e.g., `run_overflow_check` was skipped because Step 14 was not invoked on a given fallback path). +2. `visual_runtime_check is dict` but the key is absent — Step 14 returned an error envelope (e.g., `{"passed": False, "error": "selenium init failed: ..."}` — see §A.4 env-blocker path / §A.1 step 4) where the inline JS in `_MEASURE_SCRIPT` never executed, so the JS return value (`{slide, slide_body, zones, frame_slot_metrics, zone_geometries_px, image_events, table_events}` from `:3403`) was never produced. + +Both paths emit a list (`[]`) and therefore PASS C.2.a + C.2.b vacuously (no items to iterate). On this host at HEAD `4e281a2`, all four u2 runs succeeded with `visual_runtime_check.passed = True` (per B.4 / B.5), so the `[]` branch was not triggered and the runtime-observed evidence comes from non-empty lists. The fallback claim therefore remains source-grounded, not runtime-observed, in u3 (and is consistent with B.6 disclosure). + +### C.7 Divergence → Stage 1 rewind rules (no artifact patching) + +If, on any future re-run or on a different host, the assertion harness in C.2 raises any AssertionError, the failure MUST be classified per §A.4 and Stage 2 contract `schema_locked`: + +| Divergence | Classification | Action | +|---|---|---| +| Top-level `zone_geometries_px` missing while `visual_runtime_check.passed=True` | code regression | Stage 1 rewind (do NOT patch `debug.json`) | +| Per-item key set ≠ `{position, template_id, x, y, w, h}` | code regression | Stage 1 rewind | +| Any `x/y/w/h` is float or bool (not int) | code regression | Stage 1 rewind | +| Any `x/y/w/h` exceeds slide envelope (+1 px tolerance) | code regression | Stage 1 rewind | +| Top-level `zone_geometries_px = []` while `visual_runtime_check.passed=True` and a non-empty `zones` array exists | code regression | Stage 1 rewind | +| Top-level `zone_geometries_px = []` while `visual_runtime_check.passed=False` (e.g., selenium init failed) | env-blocker (PASS for schema axis — fallback is the locked behaviour) | record env-blocker; schema check PASS vacuously | + +### C.8 What u3 does NOT claim + +- No runtime invocation (u3 reuses u2 artifacts; u2 owns the runtime axis). +- No no-drift evidence at `src/phase_z2_pipeline.py` anchors (that is u4's scope). +- No pytest baseline capture (that is u5's scope). +- No claim about behaviour at HEADs other than `4e281a2`. +- No claim about `image_events` / `table_events` schemas (IMP-15 axis, out of scope). + +## §D — No-drift guardrail (unit u4) + +### D.1 Locked anchor verbatim Read at HEAD `4e281a2` + +Each locked anchor span is read verbatim from `src/phase_z2_pipeline.py` and bound to the Stage 1 / Stage 2 contract. Any future divergence at these line numbers (semantic, not whitespace) is a code regression per §A.4 and triggers Stage 1 rewind. + +``` +src/phase_z2_pipeline.py:3207-3208 +3207: // A-6 (IMP-01 #1) — slide-relative bbox base +3208: const slideRect = slide.getBoundingClientRect(); + +src/phase_z2_pipeline.py:3214 +3214: const zone_geometries_px = []; + +src/phase_z2_pipeline.py:3230-3239 +3230: // A-6 (IMP-01 #1) — zone bbox in slide-relative px (additive trace, no layout side effect) +3231: const zoneRect = z.getBoundingClientRect(); +3232: zone_geometries_px.push({ +3233: position: pos, +3234: template_id: tid, +3235: x: Math.round(zoneRect.left - slideRect.left), +3236: y: Math.round(zoneRect.top - slideRect.top), +3237: w: Math.round(zoneRect.width), +3238: h: Math.round(zoneRect.height), +3239: }); + +src/phase_z2_pipeline.py:3403 +3403: return { slide: slideM, slide_body: bodyM, zones, frame_slot_metrics, zone_geometries_px, image_events, table_events }; + +src/phase_z2_pipeline.py:4529-4530 +4529: # A-6 (IMP-01 #1) — additive top-level zone bbox trace (slide-relative px) +4530: "zone_geometries_px": (visual_runtime_check or {}).get("zone_geometries_px", []), +``` + +These line numbers match the Stage 1 / Stage 2 contract anchors exactly. No off-by-line drift. + +### D.2 Semantic grep — `zone_geometries_px` occurrences in production source + +`Grep` over `src/phase_z2_pipeline.py` for the identifier `zone_geometries_px` returns exactly **4 occurrences**, all inside the locked anchor spans: + +| # | Line | Span name | Role | +|---|---|---|---| +| 1 | `:3214` | D.1 :3214 | Step 14 JS — empty list init | +| 2 | `:3232` | D.1 :3230-3239 | Step 14 JS — per-zone push site | +| 3 | `:3403` | D.1 :3403 | Step 14 JS — return tuple member | +| 4 | `:4530` | D.1 :4529-4530 | Step 21 Python — debug dict surface | + +Zero additional occurrences outside the locked anchors. No new export site, no second producer, no second consumer. The Stage 1 lock "additive top-level field on `debug.json` only" remains intact at HEAD `4e281a2`. + +### D.3 Scoped guardrail grep — `kei|frame_selection|V4|ai_redesigner` within locked spans + +The locked spans (D.1) are searched line-by-line for the regex `kei|frame_selection|V4|ai_redesigner` (case-insensitive). Expected = 0 hits (the bbox export must be orthogonal to AI / Kei / V4 / frame_selection / ai_redesigner subsystems). + +| Span | Lines audited | Hits | +|---|---|---| +| `:3207-3208` | 2 | **0** (EMPTY) | +| `:3214` | 1 | **0** (EMPTY) | +| `:3230-3239` | 10 | **0** (EMPTY) | +| `:3403` | 1 | **0** (EMPTY) | +| `:4529-4530` | 2 | **0** (EMPTY) | +| **TOTAL** | **16** | **0** | + +Observed `TOTAL_HITS=0` matches the expected value. No subsystem coupling introduced at any locked anchor. The bbox export is mechanically isolated from AI / V4 / frame_selection / ai_redesigner code paths at the syntactic layer. + +### D.4 Git blame provenance — line-by-line commit attribution + +`git blame` on each locked span confirms the implementation commit boundary: + +| Span | Lines | Blame commit | Commit subject | Drift verdict | +|---|---|---|---|---| +| `:3207-3208` | 2 | `1dc81e0` | `feat(step14+step21): add zone_geometries_px artifact (IMP-01 #1)` | unchanged since IMP-01 #1 | +| `:3214` | 1 | `1dc81e0` | same | unchanged since IMP-01 #1 | +| `:3230-3239` | 10 | `1dc81e0` | same | unchanged since IMP-01 #1 | +| `:3403` | 1 | `28276228` | `feat(IMP-16): Step 14 table_self_overflow detection` | **additive append**, `zone_geometries_px` member preserved verbatim — see D.4.a | +| `:4529-4530` | 2 | `1dc81e0` | `feat(step14+step21): add zone_geometries_px artifact (IMP-01 #1)` | unchanged since IMP-01 #1 | + +#### D.4.a `:3403` audit — IMP-16 (#46) additive append, no drift on `zone_geometries_px` + +`git show 28276228 -- src/phase_z2_pipeline.py` for the return-tuple line: + +``` +- return { slide: slideM, slide_body: bodyM, zones, frame_slot_metrics, zone_geometries_px, image_events }; ++ return { slide: slideM, slide_body: bodyM, zones, frame_slot_metrics, zone_geometries_px, image_events, table_events }; +``` + +The single semantic change is `, table_events` appended after `image_events`. The `zone_geometries_px` member sits in the **same position** in the tuple before and after (4th member), in the same syntactic role (object shorthand property), and with the same identifier. This is the IMP-15 (#48) / IMP-16 (#46) additive append pattern already locked at Stage 1 (additive only, never mutating the IMP-01 surface). It is NOT a drift on the `zone_geometries_px` axis. + +Cross-reference: `.orchestrator/issues/48_stage_final-close_exit.md:22` records the IMP-15 / IMP-16 surfacing pattern as "mirroring `zone_geometries_px` :2739 precedent" — the additivity is the intentional contract, not a regression. + +### D.5 Working-tree no-touch verification (this round) + +`git diff -- src/phase_z2_pipeline.py` at the moment of §D authorship returns **0 bytes** of diff output (verified). No staged, no unstaged, no working-tree edit to the production source in this Stage 3 round. All §D evidence is read-only audit; no production-source edit, no anchor mutation, no schema mutation. Stage 2 guardrail "Zero production-source edit" remains intact. + +### D.6 What u4 does NOT claim + +- No runtime invocation (u2 owns the runtime axis; u3 owns the schema-assertion axis). +- No claim about behaviour at commits other than HEAD `4e281a2` (the working-tree state at §D authorship). A future commit can rebind the anchors; u4 evidence is HEAD-pinned. +- No claim about `image_events` / `table_events` schemas (IMP-15 / IMP-16 axes, separate issues #45 / #46 / #48). +- No claim about non-production source (`Front_test/`, `Front_test_v515/`, etc.). Those are vendored / archived copies and are out of the locked anchor scope. +- No pytest baseline capture (that is u5's scope). + +## §E — Baseline gate (unit u5) + +### E.1 Capture invocation + +Working directory: `D:\ad-hoc\kei\design_agent` (PROJECT_ROOT). +Interpreter: `C:\Users\User\AppData\Local\Programs\Python\Python313\python.exe` (Python 3.13.1). +Command (verbatim): `python -m pytest -q tests 2>&1 | tee .orchestrator/tmp/imp01_a6_runs/pytest_baseline.txt`. +Captured at: HEAD `4e281a2`, Stage 3 Round #3 (after u3/u4 doc-only edits, no production-source mutation). + +Artifact: `.orchestrator/tmp/imp01_a6_runs/pytest_baseline.txt` (287 lines, UTF-8 via `tee`). + +### E.2 Summary line (verbatim from `pytest_baseline.txt:287`) + +``` +7 failed, 1622 passed in 360.92s (0:06:00) +``` + +### E.3 Enumerated failures (verbatim from `short test summary info` block) + +| # | Test ID | Test file | +|---|---|---| +| F1 | `test_line_586_references_imp17_not_imp31` | `tests/orchestrator_unit/test_imp17_comment_anchor.py` | +| F2 | `test_line_587_references_imp47b_supersession` | `tests/orchestrator_unit/test_imp17_comment_anchor.py` | +| F3 | `test_post_89a_flag_off_final_html_sha_matches_frozen_baseline[01.mdx]` | `tests/regression/test_b4_mapper_source_sha_parity.py` | +| F4 | `test_post_89a_flag_off_final_html_sha_holistic_sweep` | `tests/regression/test_b4_mapper_source_sha_parity.py` | +| F5 | `test_rank_1_non_direct_promotes_rank_2` | `tests/test_phase_z2_v4_fallback.py` | +| F6 | `test_duplicate_template_id_is_skipped_rank_3_wins` | `tests/test_phase_z2_v4_fallback.py` | +| F7 | `test_restructure_reject_preserved_as_non_direct_evidence` | `tests/test_phase_z2_v4_fallback.py` | + +### E.4 IMP-01 keyword orthogonality check + +Regex `zone_geometries|IMP-01|step.?14|step.?21|slideRect|imp01` (case-insensitive) over each failing test file body: + +| Test file | Hits | +|---|---| +| `tests/orchestrator_unit/test_imp17_comment_anchor.py` | **0** | +| `tests/regression/test_b4_mapper_source_sha_parity.py` | **0** | +| `tests/test_phase_z2_v4_fallback.py` | **0** | +| **TOTAL** | **0** | + +None of the failing test files reference the IMP-01 `zone_geometries_px` axis, the locked Step 14 / Step 21 anchors, or `slideRect`. The keyword orthogonality axis passes (zero hits). + +### E.5 `git log` provenance per failure (axis classification) + +| # | Failure axis | Owner commit (most recent change to file) | Owner subject | Axis ≠ IMP-01? | +|---|---|---|---|---| +| F1, F2 | IMP-17 / IMP-35 / IMP-47B line-anchor pinning for `src/phase_z2_pipeline.py` route-hint table (lines 586/587). File header docstring: "Anchor re-pin (2026-05-23, IMP-35 u1/u5/u7 / Gitea #64 Stage 3): IMP-35 added a single-line ``compose_zone_popup_payload`` import (u7) plus a 7-line ``run_step17_popup_gate`` import block (u5) ahead of the route-hint table, totaling +8 lines". Failure suggests subsequent line drift past 586/587 (post-IMP-35 commit). | `f3ef4d9 feat(#64): IMP-35 details_popup_escalation u1~u10 + Stage 3 R7 anchor re-pin` | IMP-35 anchor re-pin | YES — IMP-17 / IMP-35 / IMP-47B route-hint anchors, not IMP-01 `zone_geometries_px`. | +| F3, F4 | IMP-89 89-a B4-mapper-source SHA parity guard — asserts final.html SHA == pre-89-a frozen baseline. Failure means a subsequent feature commit changed HTML bytes (likely IMP-45 #74 `slide_overrides.css` injector or IMP-55 #93 manual section swap detection). | `b1bbe27 feat(#89): IMP-89 89-a u1~u5 Layer A render path activation` | IMP-89 89-a B4-mapper source-of-truth switch | YES — IMP-89 HTML SHA baseline regression, not IMP-01 bbox export. | +| F5, F6, F7 | V4 fallback / dedup / candidate evidence semantics — rank-1 reject promotion to rank-2/3, duplicate template_id skip, restructure/reject preservation in candidate evidence. Origin: IMP-05 V4 candidate bridge; most recent commit: IMP-47B #76 reject-as-AI-adaptation. | `1186ad8 feat(#76): IMP-47B reject-as-AI-adaptation activation (u1~u13 backend + tests)` | IMP-47B V4 fallback semantics | YES — IMP-05 / IMP-47B V4 fallback axis, not IMP-01 Step 14 inline JS. | + +All 7 failures map cleanly to non-IMP-01 owner commits (`f3ef4d9` / `b1bbe27` / `1186ad8`). The IMP-01 implementation commit (`1dc81e0`) does not appear in the owner list, and none of the failing test bodies reference IMP-01 / `zone_geometries_px` / Step 14 / Step 21 / `slideRect` anchors. + +### E.6 Cross-reference against Stage 2 baseline (`pytest_stage2_codex_r1.txt`) + +Stage 2 baseline tail (UTF-16 LE, decoded): + +``` +FAILED tests/orchestrator_unit/test_imp17_comment_anchor.py::test_line_586_references_imp17_not_imp31 +FAILED tests/orchestrator_unit/test_imp17_comment_anchor.py::test_line_587_references_imp47b_supersession +FAILED tests/regression/test_b4_mapper_source_sha_parity.py::test_post_89a_flag_off_final_html_sha_matches_frozen_baseline[01.mdx] +FAILED tests/regression/test_b4_mapper_source_sha_parity.py::test_post_89a_flag_off_final_html_sha_holistic_sweep +FAILED tests/test_phase_z2_v4_fallback.py::test_rank_1_non_direct_promotes_rank_2 +FAILED tests/test_phase_z2_v4_fallback.py::test_duplicate_template_id_is_skipped_rank_3_wins +FAILED tests/test_phase_z2_v4_fallback.py::test_restructure_reject_preserved_as_non_direct_evidence +7 failed, 1622 passed in 364.29s (0:06:04) +``` + +The failure SET (7 IDs) and the pass/fail counts (`7 failed, 1622 passed`) are byte-identical between Stage 2 baseline and Stage 3 u5 capture. Only the elapsed time differs (`364.29s` vs `360.92s` — host noise, not test outcome). No new failure introduced by Stage 3 u1~u4 doc-only edits (consistent with §D.5 working-tree no-touch on `src/phase_z2_pipeline.py`). + +### E.7 Stage 2 contract gate evaluation + +Per Stage 2 EXIT REPORT `baseline_tests`: "`pytest -q tests` (full suite) captured as artifact; non-orthogonal failure rewinds to Stage 1." + +- All 7 failures are ORTHOGONAL to IMP-01 axis (§E.4 zero hits + §E.5 owner-commit classification). +- All 7 failures are PREEXISTING at Stage 2 baseline (§E.6 byte-identical failure set). +- Stage 3 u1~u5 made ZERO production-source edits (§D.5 confirmed `git diff -- src/phase_z2_pipeline.py` = 0 bytes). + +**Verdict: BASELINE_GATE_PASS** — no Stage 1 rewind triggered by pytest baseline. + +### E.8 What u5 does NOT claim + +- u5 does not certify the 7 preexisting failures as resolved. Each axis (IMP-35 anchor drift, IMP-89 SHA baseline regression, IMP-47B V4 fallback semantics) has its own issue / verification scope and is out of IMP-01 scope. +- u5 does not assert the pytest pass count will remain stable across future commits. The 1622-passed snapshot is HEAD-pinned at `4e281a2`. +- u5 does not modify any failing test or any production source. The baseline is captured-as-is. + +### E.9 Follow-up issue candidates (orthogonal axes surfaced) + +- **F1, F2** — IMP-17 / IMP-35 line-anchor drift may require another anchor re-pin (lines 586/587 may have shifted again past Round #1's pin point). Owner: separate issue under #64 / IMP-35 follow-up. NOT bundled into IMP-01 #1. +- **F3, F4** — IMP-89 89-a frozen baseline may need re-capture if a subsequent feature commit (IMP-45 / IMP-55) intentionally changed final.html bytes. Owner: separate issue under #89 follow-up. NOT bundled into IMP-01 #1. +- **F5, F6, F7** — V4 fallback rank promotion / dedup semantics changed by IMP-47B #76. Owner: separate issue under #76 / IMP-47B follow-up. NOT bundled into IMP-01 #1.