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
@@ -0,0 +1,189 @@
|
||||
"""IMP-56 (#90) u6 — structure_override resolver (validator + apply).
|
||||
|
||||
Step-22 user structure-edit persist axis. Consumed by Step 12 (u7 wiring)
|
||||
so a prior render's reorder / hide choices re-apply to the next render
|
||||
without re-clicking.
|
||||
|
||||
Schema (defined verbatim in ``src/user_overrides_io.py:30`` u2) ::
|
||||
|
||||
structure_overrides = {
|
||||
<zone_id>: {
|
||||
"slot_order": [<slot_key>, ...], # optional, partial reorder
|
||||
"hidden_slots": [<slot_key>, ...], # optional, hide these slot_keys
|
||||
},
|
||||
...
|
||||
}
|
||||
|
||||
SCOPE LOCK (Stage 2 u6 contract, IMP-56 #90 u2 docstring) :
|
||||
|
||||
The only allowed inner keys are ``slot_order`` and ``hidden_slots``.
|
||||
Any other key (e.g., ``frame_id``, ``template_id``, ``unit_id``,
|
||||
``slot_payload``) is treated as a frame-swap / DOM-rebuild attempt and
|
||||
is DROPPED at validate time. Frame swap stays on the existing
|
||||
``frames`` axis so the Phase Z no-AI-HTML-structure invariant remains
|
||||
intact. There is intentionally NO escape hatch through this axis.
|
||||
|
||||
API (deterministic, no AI) :
|
||||
|
||||
- ``validate_structure_overrides(overrides)`` → sanitized copy. Per-entry
|
||||
tolerant (drops malformed rows; never rejects the whole batch — mirrors
|
||||
``src.text_override_resolver.validate_text_overrides`` u4 contract).
|
||||
- ``apply_structure_override(zone, override)`` → ``True`` if the slot-payload
|
||||
mapping was mutated (any hide or any reorder), ``False`` otherwise. The
|
||||
``zone`` argument is the slot-payload mapping at Step 12 (a mutable
|
||||
mapping whose keys are slot_keys and whose values are typically
|
||||
``list[str]`` of lines). Identity-preserving: mutates in-place via
|
||||
``clear`` + ``update`` so caller references remain valid.
|
||||
|
||||
Guardrails (refs : Stage 1 binding contract, Stage 2 unit u6) :
|
||||
|
||||
- raw_content preservation is a wiring-layer (u7) responsibility — the
|
||||
resolver only ever reorders / removes top-level slot_payload entries.
|
||||
Per-slot ``list[str]`` line content is never inspected or mutated here.
|
||||
- AI-isolation : pure deterministic Python; no LLM calls.
|
||||
- Carve-out (IMP-46 #62) : brand-new module, does not touch the #76
|
||||
commit ``1186ad8`` cache region.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Mapping, MutableMapping
|
||||
|
||||
|
||||
class InvalidStructureOverride(ValueError):
|
||||
"""Reserved for future strict-mode parse errors.
|
||||
|
||||
Currently unused — the resolver follows the u4 per-entry-tolerant
|
||||
contract and silently drops malformed rows at validate time rather
|
||||
than raising. Kept as a public surface so u7 wiring (and future
|
||||
strict-mode callers) can distinguish source-malformation from
|
||||
stale-DOM misses without an API rev.
|
||||
"""
|
||||
|
||||
|
||||
_ALLOWED_INNER_KEYS: frozenset[str] = frozenset({"slot_order", "hidden_slots"})
|
||||
|
||||
|
||||
def _sanitize_slot_list(raw: Any) -> list[str]:
|
||||
"""Return a fresh list of non-empty string slot_keys (drop the rest)."""
|
||||
if not isinstance(raw, list):
|
||||
return []
|
||||
out: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for slot in raw:
|
||||
if not isinstance(slot, str) or not slot:
|
||||
continue
|
||||
if slot in seen:
|
||||
# De-dup defensively — a duplicate slot_key in slot_order would
|
||||
# be meaningless (dicts can hold each key once); duplicate in
|
||||
# hidden_slots is redundant. Drop subsequent occurrences.
|
||||
continue
|
||||
seen.add(slot)
|
||||
out.append(slot)
|
||||
return out
|
||||
|
||||
|
||||
def validate_structure_overrides(
|
||||
overrides: Any,
|
||||
) -> dict[str, dict[str, list[str]]]:
|
||||
"""Return a sanitized copy of ``overrides`` (per-entry tolerant).
|
||||
|
||||
Drops:
|
||||
- non-string or empty zone_ids,
|
||||
- non-mapping per-zone payloads,
|
||||
- per-zone inner keys other than ``slot_order`` / ``hidden_slots``
|
||||
(frame-swap attempts are dropped at this gate — see SCOPE LOCK),
|
||||
- non-list ``slot_order`` / ``hidden_slots`` values,
|
||||
- non-string or empty slot_key entries within those lists,
|
||||
- per-zone payloads that contain neither a non-empty ``slot_order``
|
||||
nor a non-empty ``hidden_slots`` after sanitization (empty intent
|
||||
carries no signal).
|
||||
|
||||
Returns a fresh ``dict`` AND fresh nested dicts / lists so callers can
|
||||
use the result as a working buffer without aliasing the persisted
|
||||
payload from ``user_overrides_io.load``.
|
||||
"""
|
||||
if not isinstance(overrides, Mapping):
|
||||
return {}
|
||||
out: dict[str, dict[str, list[str]]] = {}
|
||||
for zone_id, mapping in overrides.items():
|
||||
if not isinstance(zone_id, str) or not zone_id:
|
||||
continue
|
||||
if not isinstance(mapping, Mapping):
|
||||
continue
|
||||
zone_out: dict[str, list[str]] = {}
|
||||
for inner_key, inner_value in mapping.items():
|
||||
if inner_key not in _ALLOWED_INNER_KEYS:
|
||||
# Frame-swap attempt or unknown key — drop silently per
|
||||
# SCOPE LOCK. No mechanism through this axis.
|
||||
continue
|
||||
sanitized = _sanitize_slot_list(inner_value)
|
||||
if sanitized:
|
||||
zone_out[inner_key] = sanitized
|
||||
if zone_out:
|
||||
out[zone_id] = zone_out
|
||||
return out
|
||||
|
||||
|
||||
def apply_structure_override(
|
||||
zone: MutableMapping[str, Any],
|
||||
override: Mapping[str, Any],
|
||||
) -> bool:
|
||||
"""Apply ONE structure override to ``zone`` in-place.
|
||||
|
||||
``zone`` is the slot-payload mapping at Step 12 — i.e. a mutable
|
||||
mapping whose keys are slot_keys and whose values are the per-slot
|
||||
line lists (or other content payload). Mutation is restricted to
|
||||
top-level key membership + ordering; per-slot values are NEVER
|
||||
inspected or modified here.
|
||||
|
||||
``override`` is the per-zone payload after :func:`validate_structure_overrides`
|
||||
sanitization — i.e. a mapping with only ``slot_order`` and / or
|
||||
``hidden_slots`` keys, each holding a list of non-empty str slot_keys.
|
||||
This function is also defensive: if non-list values leak through, they
|
||||
are treated as empty (no raise).
|
||||
|
||||
Semantics :
|
||||
1. ``hidden_slots`` are popped first. Entries absent from ``zone``
|
||||
are silently skipped (stale slot_keys from a prior frame).
|
||||
2. ``slot_order`` partially reorders the surviving slot_keys:
|
||||
listed keys (that are present in ``zone``) move to the front in
|
||||
the given order; remaining keys keep their original relative
|
||||
order at the tail. Unknown slot_keys are silently skipped.
|
||||
|
||||
Returns ``True`` if the zone's slot-payload mapping was mutated (any
|
||||
hide that removed a key OR any reorder that changed key order),
|
||||
``False`` otherwise. Identity-preserving: rebuilds via
|
||||
``clear`` + ``update`` so the caller's reference to ``zone`` remains
|
||||
valid.
|
||||
"""
|
||||
mutated = False
|
||||
|
||||
raw_hidden = override.get("hidden_slots") if isinstance(override, Mapping) else None
|
||||
hidden = _sanitize_slot_list(raw_hidden)
|
||||
for slot in hidden:
|
||||
if slot in zone:
|
||||
del zone[slot]
|
||||
mutated = True
|
||||
|
||||
raw_order = override.get("slot_order") if isinstance(override, Mapping) else None
|
||||
desired_order_seed = _sanitize_slot_list(raw_order)
|
||||
|
||||
current_order = list(zone.keys())
|
||||
desired_order: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for slot in desired_order_seed:
|
||||
if slot in zone and slot not in seen:
|
||||
desired_order.append(slot)
|
||||
seen.add(slot)
|
||||
for slot in current_order:
|
||||
if slot not in seen:
|
||||
desired_order.append(slot)
|
||||
seen.add(slot)
|
||||
|
||||
if desired_order != current_order:
|
||||
snapshot = {k: zone[k] for k in desired_order}
|
||||
zone.clear()
|
||||
zone.update(snapshot)
|
||||
mutated = True
|
||||
|
||||
return mutated
|
||||
Reference in new issue
Block a user