feat(#90): IMP-56 u1-u19 catch-up before final close (post-u20 push fix)
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 20s
Multi-MDX Regression (IMP-91) / multi-mdx-regression (push) Failing after 20s
u1: text_overrides axis in user_overrides_io u2: structure_overrides axis in user_overrides_io u3: vite allowlist for new endpoints u4: text_override_resolver u5: Step 12 text_overrides apply in phase_z2_pipeline u6: structure_override_resolver u7: text_path_stamper u8: SlideCanvas text-edit capture u9: SlideCanvas structure-edit overlay u10: userOverridesApi service extension u11: designAgent types extension u12: slidePlanUtils restore u13: user_overrides endpoint tests u14: user_overrides restore tests u15: pipeline fallback tests u16: edit-mode state + gating tests u17: slide_base print mode CSS u18: /api/connect endpoint (vite) u19: /api/export endpoint (vite) Recovery scope: 29 files (12 modified + 17 new). u20 already pushed in 9439575; this commit lands u1-u19 that were authored but not committed before #90 was externally closed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
943957562f
commit
4da22adb43
29 files changed
+4937
-78
No files matched your search
+267
-1
@@ -109,6 +109,32 @@ from src.phase_z2_ai_fallback.step17 import run_step17_popup_gate
|
||||
# WRITE the snapshot — restore wiring lands in u4.
|
||||
from src.phase_z2_reuse_snapshot import build_snapshot, SNAPSHOT_FILENAME
|
||||
|
||||
# IMP-56 (#90) u5 — Step 12 text_overrides apply. Pure deterministic; no AI.
|
||||
# u4 resolver imports are aliased with leading-underscore so the helper at
|
||||
# `_apply_text_overrides_to_zones` reads as a pipeline-private wrapper
|
||||
# (mirrors the AI-repair u5 / image-overrides u7 wrapper naming pattern).
|
||||
from src.text_override_resolver import (
|
||||
apply_text_override as _apply_text_override,
|
||||
validate_text_overrides as _validate_text_overrides,
|
||||
InvalidTextOverride as _InvalidTextOverride,
|
||||
)
|
||||
# IMP-56 (#90) u7 — Step 12 structure_overrides apply. Pure deterministic; no AI.
|
||||
# u6 resolver imports are aliased with leading-underscore so the helper at
|
||||
# `_apply_structure_overrides_to_zones` reads as a pipeline-private wrapper
|
||||
# (mirrors the u5 text_overrides wrapper naming pattern). SCOPE LOCKED to
|
||||
# slot_order + hidden_slots — frame swap stays on the existing `frames`
|
||||
# axis to preserve Phase Z's no-AI-HTML-structure invariant.
|
||||
from src.structure_override_resolver import (
|
||||
apply_structure_override as _apply_structure_override,
|
||||
validate_structure_overrides as _validate_structure_overrides,
|
||||
)
|
||||
# IMP-56 (#90) u9 — Step 13 text_path stamping wired into render_slide.
|
||||
# u8 stamper injects ``data-text-path="{slot_key}.{line_index}"`` onto each
|
||||
# ``text-line`` opening tag so the frontend SlideCanvas (u10+) can attribute
|
||||
# per-line edits back to the ``text_overrides`` axis (u1 schema, u4 resolver,
|
||||
# u5 Step-12 apply). Pure deterministic; no AI / HTTP / subprocess.
|
||||
from src.text_path_stamper import stamp_zone_html as _stamp_zone_html
|
||||
|
||||
|
||||
# ─── Constants ──────────────────────────────────────────────────
|
||||
|
||||
@@ -882,6 +908,102 @@ def _apply_ai_repair_proposals_to_zones(
|
||||
record["apply_status"] = "applied:partial_overrides"
|
||||
|
||||
|
||||
def _apply_text_overrides_to_zones(
|
||||
text_overrides: dict,
|
||||
zones_data: list[dict],
|
||||
) -> dict:
|
||||
"""IMP-56 (#90) u5 — Apply persisted ``text_overrides`` to Step 12 zones.
|
||||
|
||||
Captures the audit shape the Step 12 ``text_overrides`` artifact emits
|
||||
so reviewers can see which user text edits re-applied to the next render
|
||||
and which silently skipped (stale ``text_path`` after a frame swap /
|
||||
layout regression). Per-entry tolerant by design — mirrors the
|
||||
``image_overrides`` u7 contract so a single malformed row never blocks
|
||||
a batch (Phase Z PZ-4 "no silent shrink" — surface, don't drop).
|
||||
|
||||
raw_content preservation : the function mutates ``zone["slot_payload"]``
|
||||
in place only — ``debug_zones[i].source_section_ids`` and the
|
||||
``MdxSection`` graph live elsewhere and stay byte-identical. This honours
|
||||
the Stage 1 binding contract ("raw_content preserved at Step 12") and
|
||||
the global no-silent-shrink rule (PZ-4).
|
||||
"""
|
||||
sanitized = _validate_text_overrides(text_overrides or {})
|
||||
applied = 0
|
||||
skipped = 0
|
||||
per_zone: list[dict] = []
|
||||
for zone in zones_data:
|
||||
zone_id = zone.get("position")
|
||||
if not isinstance(zone_id, str) or zone_id not in sanitized:
|
||||
continue
|
||||
slot_payload = zone.get("slot_payload")
|
||||
if not isinstance(slot_payload, dict):
|
||||
continue
|
||||
z_applied = 0
|
||||
z_skipped = 0
|
||||
for text_path, value in sanitized[zone_id].items():
|
||||
try:
|
||||
ok = _apply_text_override(slot_payload, text_path, value)
|
||||
except _InvalidTextOverride:
|
||||
ok = False
|
||||
if ok:
|
||||
z_applied += 1
|
||||
else:
|
||||
z_skipped += 1
|
||||
applied += z_applied
|
||||
skipped += z_skipped
|
||||
per_zone.append({
|
||||
"position": zone_id,
|
||||
"applied": z_applied,
|
||||
"skipped": z_skipped,
|
||||
})
|
||||
return {"applied": applied, "skipped": skipped, "per_zone": per_zone}
|
||||
|
||||
|
||||
def _apply_structure_overrides_to_zones(
|
||||
structure_overrides: dict,
|
||||
zones_data: list[dict],
|
||||
) -> dict:
|
||||
"""IMP-56 (#90) u7 — Apply persisted ``structure_overrides`` at Step 12.
|
||||
|
||||
SCOPE LOCK : only top-level ``slot_payload`` key membership + ordering
|
||||
is mutated (reorder + hide). NO DOM rebuild. NO frame swap — the u6
|
||||
validate gate drops any non-``slot_order`` / non-``hidden_slots`` inner
|
||||
key silently. Frame swap stays on the existing ``frames`` axis.
|
||||
|
||||
Per-zone tolerant — stale slot_keys (frame swap / layout regression
|
||||
between renders) silently no-op via ``apply_structure_override``'s
|
||||
``False`` return (counted as ``skipped_zones``).
|
||||
|
||||
raw_content preservation : per-slot ``list[str]`` line content is
|
||||
NEVER inspected or modified here — the resolver only reorders /
|
||||
removes top-level slot_payload keys. ``debug_zones[i].source_section_ids``
|
||||
+ the ``MdxSection`` graph stay byte-identical (mirrors the u5 wiring
|
||||
invariant + Stage 1 binding contract).
|
||||
"""
|
||||
sanitized = _validate_structure_overrides(structure_overrides or {})
|
||||
applied_zones = 0
|
||||
skipped_zones = 0
|
||||
per_zone: list[dict] = []
|
||||
for zone in zones_data:
|
||||
zone_id = zone.get("position")
|
||||
if not isinstance(zone_id, str) or zone_id not in sanitized:
|
||||
continue
|
||||
slot_payload = zone.get("slot_payload")
|
||||
if not isinstance(slot_payload, dict):
|
||||
continue
|
||||
mutated = _apply_structure_override(slot_payload, sanitized[zone_id])
|
||||
if mutated:
|
||||
applied_zones += 1
|
||||
else:
|
||||
skipped_zones += 1
|
||||
per_zone.append({"position": zone_id, "mutated": mutated})
|
||||
return {
|
||||
"applied_zones": applied_zones,
|
||||
"skipped_zones": skipped_zones,
|
||||
"per_zone": per_zone,
|
||||
}
|
||||
|
||||
|
||||
def _check_post_ai_coverage_invariant(
|
||||
units,
|
||||
ai_repair_records: list[dict],
|
||||
@@ -3131,7 +3253,14 @@ def render_slide(slide_title: str, slide_footer: Optional[str],
|
||||
rendered_partial,
|
||||
f"zones_data[{zone_index}] template_id={template_id!r}",
|
||||
)
|
||||
zone["partial_html"] = rendered_partial
|
||||
# IMP-56 (#90) u9 — Step 13 text_path stamp (u8 stamper, pure
|
||||
# deterministic). Injects ``data-text-path="{slot_key}.{line_index}"``
|
||||
# onto each ``text-line`` opening tag so the frontend SlideCanvas
|
||||
# (u10+) can attribute per-line edits back to the text_overrides
|
||||
# axis. Idempotent + forward-compat: non-list slots are silently
|
||||
# skipped, excess text-lines pass through unstamped, and an
|
||||
# already-stamped element is left unchanged.
|
||||
zone["partial_html"] = _stamp_zone_html(rendered_partial, slot_payload)
|
||||
|
||||
base = env.get_template("slide_base.html")
|
||||
rendered_base = base.render(
|
||||
@@ -4889,6 +5018,8 @@ def run_phase_z2_mvp1(
|
||||
override_section_assignments: Optional[dict[str, list[str]]] = None,
|
||||
override_image_overrides: Optional[dict[str, dict]] = None,
|
||||
override_slide_css: Optional[str] = None,
|
||||
override_text_overrides: Optional[dict[str, dict[str, str]]] = None,
|
||||
override_structure_overrides: Optional[dict[str, dict[str, list[str]]]] = None,
|
||||
reuse_from: Optional[str] = None,
|
||||
) -> Path:
|
||||
"""MVP-1.5b entry — single slide + composition planner v0 + 8 preset vocabulary.
|
||||
@@ -4912,6 +5043,32 @@ def run_phase_z2_mvp1(
|
||||
backend contract (KNOWN_AXES u1 + Vite allowlist u2 + typed
|
||||
client u3 + stamper u4) end-to-end addressable from CLI without
|
||||
diverging the function signature.
|
||||
override_text_overrides : {zone_id: {text_path: value}} — IMP-56 (#90) u5
|
||||
axis. ``text_path`` = ``{slot_key}.{line_index}`` stamped
|
||||
by the u8 ``text_path_stamper`` (pending). Applied at
|
||||
Step 12 AFTER the AI-repair apply (IMP-47B u5) and
|
||||
BEFORE the ``step12_slot_payload.json`` artifact emit so
|
||||
the audit reflects user-edit final state. Per-zone
|
||||
tolerant — stale paths (frame swap / layout regression)
|
||||
skip silently. raw_content preserved (mutates
|
||||
``zone['slot_payload']`` only ; ``debug_zones`` graph
|
||||
untouched). CLI / persistence fallback wiring is u16
|
||||
scope.
|
||||
override_structure_overrides : {zone_id: {slot_order|hidden_slots: [slot_key,...]}}
|
||||
— IMP-56 (#90) u7 axis. SCOPE LOCKED to
|
||||
``slot_order`` (partial reorder) + ``hidden_slots``
|
||||
(hide). Frame swap is rejected at the u6 validate
|
||||
gate (stays on the existing ``frames`` axis) so the
|
||||
Phase Z no-AI-HTML-structure invariant remains
|
||||
intact. Applied at Step 12 AFTER the u5
|
||||
text_overrides apply and BEFORE the
|
||||
``step12_slot_payload.json`` artifact emit. Per-zone
|
||||
tolerant — stale slot_keys silently no-op (counted
|
||||
as ``skipped_zones``). raw_content preserved (only
|
||||
top-level slot_payload key membership + ordering
|
||||
mutated ; per-slot ``list[str]`` line content +
|
||||
``debug_zones`` graph untouched). CLI / persistence
|
||||
fallback wiring is u16 scope.
|
||||
override_slide_css : Optional slide-level CSS string — IMP-45 (#74) u4 axis.
|
||||
Marker-wrapped <style> block injected into ``final.html``
|
||||
at Step 13 via :func:`src.slide_css_injector.inject_slide_css`
|
||||
@@ -6607,6 +6764,59 @@ def run_phase_z2_mvp1(
|
||||
note="IMP-47B u6 — Step 12 AI repair gather + apply records per unit (route, skip_reason, apply_status, proposal). u7 coverage_invariant = pre/post AI source_section_ids set comparison.",
|
||||
)
|
||||
|
||||
# ─── Step 12 IMP-56 #90 u5 — Apply persisted text_overrides ───
|
||||
# User text edits captured by the frontend (u12 capture path, pending)
|
||||
# re-apply here so the next render shows them without re-clicking.
|
||||
# Per-zone tolerant — stale ``text_path`` entries (frame swap / layout
|
||||
# regression) skip silently. raw_content preserved : the helper only
|
||||
# mutates ``zone["slot_payload"]`` ; ``debug_zones[i].source_section_ids``
|
||||
# + ``MdxSection`` graph untouched. Audit artifact emits BEFORE the
|
||||
# slot_payload artifact so reviewers see the post-override state.
|
||||
text_overrides_audit = _apply_text_overrides_to_zones(
|
||||
override_text_overrides or {}, zones_data,
|
||||
)
|
||||
_write_step_artifact(
|
||||
run_dir, 12, "text_overrides",
|
||||
data=text_overrides_audit,
|
||||
step_status="done",
|
||||
pipeline_path_connected=True,
|
||||
inputs=["data/user_overrides/*.json"],
|
||||
outputs=["step12_text_overrides.json"],
|
||||
note=(
|
||||
"IMP-56 #90 u5 — user text edit re-apply to slot_payload "
|
||||
"(per-zone tolerant; raw_content preserved). text_path = "
|
||||
"{slot_key}.{line_index} (u8 stamper contract)."
|
||||
),
|
||||
)
|
||||
|
||||
# ─── Step 12 IMP-56 #90 u7 — Apply persisted structure_overrides ───
|
||||
# User reorder / hide choices captured by the frontend (u13 capture
|
||||
# path, pending) re-apply here. SCOPE LOCKED to slot_order +
|
||||
# hidden_slots — frame swap rejected at u6 validate gate (stays on
|
||||
# the existing `frames` axis) so the Phase Z no-AI-HTML-structure
|
||||
# invariant remains intact. raw_content preserved : helper only
|
||||
# reorders / removes top-level slot_payload keys ; per-slot line
|
||||
# lists + ``debug_zones`` + ``MdxSection`` graph untouched. Audit
|
||||
# artifact emits BEFORE the slot_payload artifact so reviewers see
|
||||
# the post-override state.
|
||||
structure_overrides_audit = _apply_structure_overrides_to_zones(
|
||||
override_structure_overrides or {}, zones_data,
|
||||
)
|
||||
_write_step_artifact(
|
||||
run_dir, 12, "structure_overrides",
|
||||
data=structure_overrides_audit,
|
||||
step_status="done",
|
||||
pipeline_path_connected=True,
|
||||
inputs=["data/user_overrides/*.json"],
|
||||
outputs=["step12_structure_overrides.json"],
|
||||
note=(
|
||||
"IMP-56 #90 u7 — user reorder / hide re-apply to "
|
||||
"slot_payload (SCOPE LOCKED to slot_order + hidden_slots ; "
|
||||
"frame swap stays on the `frames` axis ; raw_content "
|
||||
"preserved)."
|
||||
),
|
||||
)
|
||||
|
||||
# ─── Step 12: Slot Payload (actual values, mapper.py 결과) ───
|
||||
_write_step_artifact(
|
||||
run_dir, 12, "slot_payload",
|
||||
@@ -8203,6 +8413,15 @@ if __name__ == "__main__":
|
||||
)
|
||||
sys.exit(2)
|
||||
|
||||
# IMP-56 (#90) u16 — text_overrides + structure_overrides are file-only
|
||||
# axes (frontend captures via /api/user-overrides PUT; no CLI flag
|
||||
# surface, by design — per-line text edits and per-zone slot reorders /
|
||||
# hides are too granular for argparse). Initialize empty here so the
|
||||
# user_overrides.json fallback below can fill them, mirroring the
|
||||
# ``overrides_images`` (IMP-51 #79 u6) post-CLI / pre-_persisted shape.
|
||||
overrides_text: dict[str, dict[str, str]] = {}
|
||||
overrides_structure: dict[str, dict[str, list[str]]] = {}
|
||||
|
||||
# IMP-52 (#80) u2 — user_overrides.json persistence fallback.
|
||||
# After argparse fully parses CLI flags, fill ONLY the axes the user
|
||||
# did NOT pass on the command line. CLI payload always wins over the
|
||||
@@ -8309,6 +8528,51 @@ if __name__ == "__main__":
|
||||
except (TypeError, ValueError):
|
||||
continue
|
||||
overrides_images = _accepted_img
|
||||
# text_overrides — file-only (no CLI flag) → fill from file as
|
||||
# dict[zone_id, dict[text_path, value]]. IMP-56 (#90) u16. Inline
|
||||
# gate accepts only str-keyed inner dicts; the u4 validator runs
|
||||
# again at Step 12 apply time, where malformed inner entries
|
||||
# surface as silent per-zone skips in the ``step12_text_overrides``
|
||||
# audit (Phase Z PZ-4 "no silent shrink" — count, don't drop).
|
||||
if not overrides_text:
|
||||
_file_text = _persisted.get("text_overrides")
|
||||
if isinstance(_file_text, dict):
|
||||
_accepted_text: dict[str, dict[str, str]] = {}
|
||||
for _zid, _payload in _file_text.items():
|
||||
if isinstance(_zid, str) and isinstance(_payload, dict):
|
||||
_entries = {
|
||||
str(_k): str(_v)
|
||||
for _k, _v in _payload.items()
|
||||
if isinstance(_k, str) and isinstance(_v, str)
|
||||
}
|
||||
if _entries:
|
||||
_accepted_text[_zid] = _entries
|
||||
overrides_text = _accepted_text
|
||||
# structure_overrides — file-only → fill from file as dict[zone_id,
|
||||
# {slot_order|hidden_slots: list[str]}]. IMP-56 (#90) u16. Inner
|
||||
# keys locked to the two allowed names; the u6 validator drops any
|
||||
# other key + frame-swap payloads at apply time so the CLI gate
|
||||
# only does the list[str] structural coercion. Empty inner dicts
|
||||
# are dropped here so ``overrides_structure or None`` collapses to
|
||||
# ``None`` on the call site (matches IMP-51 #79 u6 ``or None`` shape).
|
||||
if not overrides_structure:
|
||||
_file_struct = _persisted.get("structure_overrides")
|
||||
if isinstance(_file_struct, dict):
|
||||
_accepted_struct: dict[str, dict[str, list[str]]] = {}
|
||||
for _zid, _payload in _file_struct.items():
|
||||
if isinstance(_zid, str) and isinstance(_payload, dict):
|
||||
_entries_s: dict[str, list[str]] = {}
|
||||
for _k, _v in _payload.items():
|
||||
if (
|
||||
_k in ("slot_order", "hidden_slots")
|
||||
and isinstance(_v, list)
|
||||
):
|
||||
_entries_s[_k] = [
|
||||
s for s in _v if isinstance(s, str)
|
||||
]
|
||||
if _entries_s:
|
||||
_accepted_struct[_zid] = _entries_s
|
||||
overrides_structure = _accepted_struct
|
||||
|
||||
# IMP-43 (#72) u1 — fail-closed reuse_from precondition guard.
|
||||
# Placed AFTER the user_overrides.json merge so persisted overrides
|
||||
@@ -8350,5 +8614,7 @@ if __name__ == "__main__":
|
||||
override_section_assignments=overrides_section_assignments or None,
|
||||
override_image_overrides=overrides_images or None,
|
||||
override_slide_css=_final_override_slide_css,
|
||||
override_text_overrides=overrides_text or None,
|
||||
override_structure_overrides=overrides_structure or None,
|
||||
reuse_from=args.reuse_from,
|
||||
)
|
||||
Reference in new issue
Block a user