# Phase Z Overlay Schema (Option E first migration) **Status**: Step 0 schema lock — pre-execution spec. **Scope**: F13 / F29 / F16 only (3 active Phase Z frames). **Last updated**: 2026-05-07. --- ## 0. Why this doc exists `templates/phase_z2/catalog/frame_contracts.yaml` is currently a hand-curated island. It must be hand-edited whenever a Phase Z frame's slot/sub_zone/payload structure changes, even though most of its sibling Phase Z artifacts (V4 matching results, `structure_ontology.yaml` `templates_v1` section, `figma_to_html_agent/blocks/{frame_id}/analysis.md`) are auto-generated from upstream sources. Option E first migration converts `frame_contracts.yaml` to a **generated artifact**: ``` runtime_overlay/{template_id}.yaml (per-template Phase Z fields, hand-edited) + templates_v1[frame_id] (cross-validation only in first migration) ↓ build_phase_z2_frame_contracts.py (generator) ↓ templates/phase_z2/catalog/frame_contracts.yaml (committed, generated) ↓ Phase Z runtime (unchanged — reads frame_contracts.yaml the same way) ``` This doc locks the schema/keyspace/trigger/rollback rules **before** any overlay file or generator code is written. --- ## 0-1. Field owner table Each field in current `frame_contracts.yaml` is classified as one of: - **(a) templates_v1-derived** — generator looks up from `tests/matching/structure_ontology.yaml` `templates_v1` section. **Not declared in overlay.** - **(b) overlay-only** — Phase Z 전용 operational config. **Declared in `runtime_overlay/{template_id}.yaml`.** - **(c) validation duplicate** — generator cross-checks overlay value against `templates_v1`. Disagreement → hard error. ### F13 — three_parallel_requirements (frame_id 1171281190) | Field | Current value | Category | Note | |---|---|---|---| | `template_id` | `three_parallel_requirements` | (c) | overlay filename = template_id; cross-check `templates_v1['1171281190'].template_id` | | `frame_id` | `1171281190` | (b) | overlay declares; generator confirms it exists as a `templates_v1` key | | `family` | `three_parallel` | (b) | Phase Z categorization. Not the same as `templates_v1.visual_pattern.family` (= `list`) — different semantic axis. Name overlap is incidental. | | `source_shape` | `top_bullets` | (b) | Phase Z B1 extractor signal. Not in `templates_v1`. | | `cardinality.strict` | `3` | (c) | cross-check against `templates_v1.visual_pattern.cardinality` — must equal `min` (and `max` if min==max). | | `cardinality.overflow_policy` | `abort_or_review` | (b) | Phase Z fallback policy. | | `role_order` | `[tech, people, nature]` | (b) | F13-specific visual role mapping. Not in `templates_v1`. | | `visual_hints.min_height_px` | `230` | (b) | Phase Z layout calculation. Not in `templates_v1`. | | `accepted_content_types` | `[text_block]` | (b) | SPEC v1 §3 Layer A→B input. Not in `templates_v1`. | | `sub_zones` | `[pillar_1, pillar_2, pillar_3]` (with `partial_target_path`) | (b) | Phase Z Frame Slot declaration. Conceptually overlaps with `templates_v1.slots` (`pillar_*_label/body`) but different shape (sub_zones = column units; slots = label+body pairs). | | `payload.*` | `{title, builder, builder_options}` | (b) | Phase Z mapper directives. Not in `templates_v1`. | ### F29 — process_product_two_way (frame_id 1171281210) | Field | Current value | Category | Note | |---|---|---|---| | `template_id` | `process_product_two_way` | (c) | filename = template_id; cross-check | | `frame_id` | `1171281210` | (b) | | | `family` | `two_column_h3` | (b) | `templates_v1.visual_pattern.family` = `compare`; intentionally different | | `source_shape` | `h3_subsections` | (b) | F29-specific B1 path | | `cardinality.strict` | `2` | (c) | cross-check against `templates_v1.visual_pattern.cardinality.{ideal:2, min:2, max:2}` | | `cardinality.overflow_policy` | `abort_or_review` | (b) | | | `visual_hints.min_height_px` | `345` | (b) | | | `accepted_content_types` | `[text_block, transform_table]` | (b) | F29 process column accepts AS-IS/TO-BE table | | `sub_zones` | `[process_column, product_column]` (each `cardinality.strict: 3`) | (b) | 2 column × 3 sections; sub_zone unit = column | | `payload.*` | `{title, builder=process_product_pair, builder_options}` | (b) | | ### F16 — bim_issues_quadrant_four (frame_id 1171281193) | Field | Current value | Category | Note | |---|---|---|---| | `template_id` | `bim_issues_quadrant_four` | (c) | | | `frame_id` | `1171281193` | (b) | | | `family` | `bim_issues_quadrant` | (b) | `templates_v1.visual_pattern.family` = `cards` | | `source_shape` | `top_bullets` | (b) | | | `cardinality.strict` | **(not declared)** | n/a | F16 intentionally omits — uses `pad_to=4` + `truncate>4` policy in `payload.builder_options`. `templates_v1.cardinality` has `{ideal:4, min:4, max:4}` but Phase Z does not enforce strict here. **Generator must NOT auto-derive this from `templates_v1`.** | | `accepted_content_types` | `[text_block]` | (b) | | | `sub_zones` | `[quadrant_1, quadrant_2, quadrant_3, quadrant_4]` (each `cardinality.strict: 1`) | (b) | sub_zone-level cardinality = capacity; not the same as frame-level | | `payload.*` | `{title, builder=quadrant_flat_slots, builder_options.{item_parser, pad_to, truncate_at, label_key_pattern, body_key_pattern, empty_label, empty_body}}` | (b) | | ### Summary - **(a) templates_v1-derived**: **none** in first migration. Overlay declares all operational config. - **(b) overlay-only**: ~all fields. Overlay file is essentially a per-template extract of the current `frame_contracts.yaml` entry. - **(c) validation duplicate**: 2 fields per template — `template_id` (overlay filename matches `templates_v1[frame_id].template_id`) and `cardinality.strict` when present (must match `templates_v1.visual_pattern.cardinality.min`). **Implication**: first migration is mostly a **split-and-concatenate** refactor with light cross-validation. Migration value is (i) per-template editing isolation, (ii) `templates_v1` consistency check at build time. Heavier derivation (e.g., generating `cardinality.strict` from `templates_v1` automatically) is **별 axis** — defer until a concrete need surfaces. --- ## 0-2. Duplicate rule — hard error If an overlay file declares a field that is classified as **(a) templates_v1-derived**, generator **fails immediately** with a clear message. No silent override semantics. Currently no fields are in (a), so this rule is dormant for first migration. It exists to prevent regression: a future overlay author cannot quietly duplicate a `templates_v1`-owned field without removing the (a) classification first. For **(c) validation duplicate** fields, the rule is: - Overlay declares the value. - Generator looks up the corresponding value in `templates_v1`. - If the two **disagree**, hard error with both values printed and a pointer to this doc. This preserves the lock-layer "overlay = single source of truth for declared values" while making `templates_v1` drift visible. --- ## 0-3. Keyspace rule - **Source ref** = `frame_id` (Figma origin). - **Overlay identity** = `template_id` (filename: `runtime_overlay/{template_id}.yaml`). - **First-migration assumption**: `frame_id` ↔ `template_id` is **1:1** for all active Phase Z frames (F13/F29/F16). - **Generator verification**: for each overlay, the generator looks up `templates_v1[overlay.frame_id].template_id` and asserts it equals the overlay filename's `{template_id}`. Mismatch → hard error. - **Multi-variant** (one frame ↔ multiple templates) = **별 axis**. When that case appears, this doc is updated and the keyspace becomes a composite key. Until then, 1:1 is locked. --- ## 0-4. Trigger - **Manual run** for first migration: ``` python scripts/build_phase_z2_frame_contracts.py ``` - **Verification** (semantic-identical) is **required** after each run. Generator should refuse to write output if the result differs semantically from current `frame_contracts.yaml` during first migration. - **CI / pre-commit automation** = **별 axis**. Will be considered after first migration ships and a drift incident actually occurs. Adding it now is over-engineering for 3 templates. --- ## 0-5. Rollback / failure path If `semantic-identical` verification fails (`yaml.safe_load(current) != yaml.safe_load(generated)`): 1. **Generated artifact is NOT adopted.** No write to `frame_contracts.yaml`. 2. Current `frame_contracts.yaml` remains canonical (status quo preserved). 3. Diff source: overlay schema, generator implementation, or `templates_v1` cross-check rule. Identify root cause. 4. Fix overlay/schema/generator. Retry from Step 1. 5. **Migration is incomplete**: 3 active templates partially migrated count as failure — either all 3 ship or none. The migration commit (Step 5) is **only made after** all 3 templates pass both (a) semantic-identical and (b) Phase Z runtime regression (final.html identical). --- ## 0-6. Scope boundary **Option E first migration solves**: - `frame_contracts.yaml` island problem (hand-curated → generated). - Per-template editing isolation. - Light `templates_v1` consistency check at build time. **Option E first migration does NOT solve** (= 별 axis, NOT addressed by this migration): - `analysis.md` ↔ `structure_ontology.yaml` ownership direction conflict. Specifically: `figma_to_html_agent/CLAUDE.md` implies forward direction (agent owns `analysis.md`), while `tests/matching/sync_analysis_from_ontology.py` enforces reverse direction (`structure_ontology.yaml` is source, `analysis.md` is mirror). This is a separate architectural decision and is out of scope here. - `templates/blocks/structures` legacy retirement. - `legacy templates/catalog/blocks.yaml` removal. - `zone_extract` rule formalization (long-term zone_application policy). - 32-frame full audit (only active F13/F29/F16 in first migration). When the user resumes work on the direction inversion axis, this doc is **not invalidated** — overlay schema and generator continue to work because they read `templates_v1` (not `analysis.md`). --- ## Migration steps (post-Step 0) 1. Create `templates/phase_z2/catalog/runtime_overlay/{F13,F29,F16}.yaml` files. Each contains the (b) overlay-only fields + (c) validation duplicate values for one template. 2. Write `scripts/build_phase_z2_frame_contracts.py` generator. Reads overlays + `templates_v1`, applies 0-2 / 0-3 rules, writes generated `frame_contracts.yaml`. 3. Run generator. Verify `yaml.safe_load(current) == yaml.safe_load(generated)`. List order preserved (sub_zones especially). On disagreement → 0-5 rollback. 4. Run Phase Z pipeline once (regression MDX) — confirm `final.html` is byte-identical to current run. 5. Commit Option E migration (overlay files + generator + new generated `frame_contracts.yaml` with header). Separate commit from Step 0 schema doc. --- ## Out of scope for this doc - Generator implementation details (parsing, emit format, error message templates) — captured in the script itself. - Overlay file format details beyond field categorization — captured in the overlay files themselves. - Direction inversion fixes — see future axis docs.