Executing unit: u7 (refresh) [Claude #12] Stage 3 — u7 manifest-schema assertions added (rewind fix from Codex #11 rejection) **unit_executed**: u7 — pytest smoke for `scripts/generate_frame_previews.py` **files_changed**: - `tests/test_generate_frame_previews.py` (overwrite; still single-file scope) **diff_summary**: - Added `import json` for manifest JSON parsing. - Extended `_fixture()` to also create an `ORPHAN/` dir with only `preview.png` (no `index.html`), giving non-empty `missing_index_html` + `orphan` buckets without requiring Selenium. - Updated `test_discover_counts` for the new 3-entry fixture (`["FRAME_A", "FRAME_B", "ORPHAN"]`, has_index sum = 1, has_preview sum = 2). - Merged former `test_no_ai_or_pipeline_imports` + `test_no_hardcoded_slide_dimensions` into one `test_source_invariants` to stay within the Stage 2 ≤50-line guardrail (file final = **50 lines exactly**). - Updated `test_dry_run_prints_counts` expected line to match the new fixture: `discovered: total=3 with_index_html=1 with_preview_png=2`. - **NEW** `test_manifest_schema(tmp_path)` — the explicit miss called out in [Codex #11]: - Builds an orphan-only fixture (one dir with `preview.png`, no `index.html`) so `renderable=[]` and `main()` never invokes `_build_driver()` / Selenium. - Calls `gfp.main(["--blocks-dir", ..., "--manifest", ...])` (no `--dry-run`) → forces the manifest writer path in `scripts/generate_frame_previews.py:183`. - Asserts top-level schema keys ⊇ `{"schema", "generated_at", "blocks_dir", "summary", "frames"}` and `data["schema"] == 1`. - Asserts `summary` subkeys ⊇ `{"total", "renderable", "missing_index_html", "orphan", "rendered", "skipped_unchanged", "error"}`, `summary["orphan"] == 1`, and `frames["F"]["status"] == "orphan"`. **evidence**: - `wc -l tests/test_generate_frame_previews.py` → **50** (Stage 2 u7 guardrail: ≤50 lines, ≤3 files; 1 file changed). - `python -m pytest tests/test_generate_frame_previews.py -q` → **5 passed in 0.05s** (was 4 before this refresh; +1 = `test_manifest_schema`). - `git status --short scripts/ tests/test_generate_frame_previews.py` → only IMP-13's own files (`scripts/generate_frame_previews.py`, `tests/test_generate_frame_previews.py`); no edits to `src/slide_measurer.py`, `src/phase_z2_pipeline.py`, `Front/vite.config.ts`, `templates/phase_z2/frames`, or any tracked file. - `_build_driver()` is **not** called in any test path (orphan-only fixture in `test_manifest_schema` guarantees `buckets["renderable"] == []`; pytest run shows 0 chromedriver/Selenium errors). - Source-invariant tokens still absent from script: `anthropic | openai | jinja | phase_z2 | slide_measurer`; literal-px guard still rejects `1280 | 720 | 1400 | 900`. **remaining_units**: [] (u1, u2, u3, u4, u5, u6, u7 all complete per Stage 2 plan). **follow_up_issue_candidates**: - *(soft-link, IMP-04 territory)* HTML generation for the 13 `missing_index_html` frames — out of IMP-13 scope per Stage 1 lock; manifest will keep emitting them as `status: missing_index_html` until IMP-04 lands. - *(observed during u7 work, no action required)* Stage 2 noted `data/figma_previews/index.json` does not parse as JSON; orthogonal axis, not in IMP-13 boundary. **scope_lock recheck**: - Build-time only ✓ (no `phase_z2_pipeline`/`slide_measurer` imports). - Idempotent ✓ (`is_unchanged` covered by `test_idempotency_unchanged`). - Failure-loud ✓ (`test_manifest_schema` proves orphan/missing rows reach the manifest, not silently dropped). - No spacing-shrink, no hardcoding ✓ (`test_source_invariants` enforces). **Next**: STOP per Stage 3 rule. Next turn = Stage 4 (test-verify) by Codex; expecting `FINAL_CONSENSUS: YES` since all 7 units now satisfy the binding contract.