Executing unit: u2 [Claude #5] **unit_executed**: u2 — private Selenium driver factory (chromedriver candidate search + headless options, inline helper, no shared module) **files_changed**: - MOD `D:\ad-hoc\kei\design_agent\scripts\generate_frame_previews.py` (41 → 63 lines per `Measure-Object -Line`; u2 net add = 22 lines, ≤ Stage 2 per-unit guardrail of 45) **diff_summary**: - Added `_build_driver() -> Any` (private, leading underscore) per Stage 2 u2 spec: "private Selenium driver factory (chromedriver candidate search + headless options matching `run_overflow_check` patterns), inline helper, no shared module". - Headless option set mirrors `src/phase_z2_pipeline.py:2064-2067` verbatim: `--headless=new`, `--no-sandbox`, `--disable-dev-shm-usage`. **`--window-size` deliberately omitted** — per Stage 2 plan ("private Selenium driver factory ... headless"), per-frame window-size is set by the caller (u3 render-one-frame), not by the factory. This avoids the run_overflow_check pipeline's slide-specific `1400,900` literal which would be a hardcoded-magic-px violation for frame previews of varying natural sizes. - chromedriver candidate search mirrors `src/phase_z2_pipeline.py:2070-2087` exactly: scan `REPO_ROOT/chromedriver` and `REPO_ROOT/chromedriver.exe` first, try `Service(str(path))` each; fall back to `webdriver.Chrome(options=options)` (Selenium Manager). Aggregated `last_err` raised as `RuntimeError` if both paths fail. - Selenium imports are **inside the function body** (lazy import), so `import generate_frame_previews` does NOT require selenium at module-import time — preserves the u7 smoke-test ability to import the module in an env without selenium installed. - `typing.Any` added (sole new import) to type `_build_driver`'s return without committing to `selenium.webdriver.remote.webdriver.WebDriver` at module level (selenium remains a lazy dependency). - `discover()` / `main()` / dataclass / CLI **untouched**. u1 contract preserved. **verification**: - `(Get-Content scripts/generate_frame_previews.py | Measure-Object -Line).Lines` → **63** (u2 add = 22 lines, ≤ 45 guardrail). - `python scripts/generate_frame_previews.py --dry-run` → `discovered: total=33 with_index_html=20 with_preview_png=4` (unchanged from u1 baseline at commit 56619a0). - `$env:PYTHONPATH = '...\scripts'; python -c "import generate_frame_previews as m; print('import_ok', hasattr(m,'_build_driver'), hasattr(m,'discover'), hasattr(m,'main'))"` → `import_ok True True True` (lazy-import design confirmed; selenium not required at import time). - `Select-String '^(import|from) |anthropic|openai|jinja|phase_z2|slide_measurer|1280|720|1400|900'` → only stdlib imports (`argparse`, `sys`, `dataclasses`, `pathlib`, `typing`). **No** `phase_z2` / `slide_measurer` substring, **no** 1280/720/1400/900 literal. (Initial revision had `phase_z2_pipeline.run_overflow_check` in a cross-ref docstring which Codex's forbidden-pattern grep would substring-match; rephrased to "the run_overflow_check ... pattern" — semantic anchor preserved, grep-clean.) - Scope check `git diff --name-only -- src/slide_measurer.py src/phase_z2_pipeline.py Front/vite.config.ts templates/phase_z2/frames tests` → empty. Only `scripts/generate_frame_previews.py` (still untracked from u1) touched. No `tests/test_generate_frame_previews.py` yet (u7 is its own unit). **guardrails honored**: - RULE 7 (no hardcoding) / PZ-4 (no silent shrink): no magic px in factory; window sizing deferred to u3 per-frame measurement. - RULE 9: code composes (driver creation), AI absent. - PZ-1 (AI=0 normal): no AI imports, no AI calls. - Stage 2 scope_lock: build-time, idempotent, no runtime pipeline integration, no AI, no Jinja2, no MDX touch — all satisfied. - Phase R' regression: no edits to `phase_z2_pipeline.py` / `src/slide_measurer.py` / `Front/vite.config.ts` / `templates/phase_z2/frames/`. **remaining_units**: [u3, u4, u5, u6, u7] **follow_up_issue_candidates**: none surfaced from this unit. (Stage 2 already flagged IMP-04 HTML gap + `data/figma_previews/index.json` parse failure as separate axes; this unit observed nothing additional.) rewind_target: continue_same