Files
DumiFlex ef2535548f feat(constraint): instance-only matrix cells — empty-cell overrides become new entries at resolve
The previous commit let users cycle empty matrix cells in the modal but
the engine only iterated `payload.matrix` — overrides for keys absent
from the library were silently ignored. Cycling produced dead UI: state
saved, engine ignored, runtime identical.

Engine extension: after the library-merge pass, do a second pass over
`cell_mode_overrides` ∪ `cell_factor_overrides` minus `covered_keys`.
Each leftover key becomes a new matrix entry with implicit baseline
mode "allow" + factor 1.0; whichever side(s) the user set populate the
rest. Disabled set still wins — entries in `disabled_matrix_cells` skip
both passes.

Added `decode_key` helper alongside `encode_key` for the second-pass
src/tgt extraction. Round-trip + malformed-input rejection covered by
new key tests. Malformed override keys in the resolve path are skipped
silently rather than crashing the run (defensive — instance JSON could
have rotted across schema migrations).

Updated the existing "unknown key silently ignored" test to assert the
new behavior (entry created from override). Added focused tests for:
factor-only override defaults to allow mode, disabled set still skips
empty-cell overrides, malformed key in override map doesn't crash.
2026-05-10 14:50:18 +03:00

27 lines
952 B
Python

"""Composite-key encoder. Mirrors JS `encodeKey` byte-for-byte.
Spec invariant: composite keys are JSON-encoded string arrays, no
whitespace, unicode literal (not escaped), array order preserved.
Cross-language fitness test in `tests/test_engine_keys.py` enforces
byte-exact match.
See: docs/superpowers/specs/2026-05-07-instance-overrides-modal-design.md §4.5
"""
import json
def encode_key(parts: list[str]) -> str:
return json.dumps(parts, separators=(",", ":"), ensure_ascii=False)
def decode_key(key: str) -> list[str]:
"""Inverse of encode_key. Round-trip safe by spec invariant.
Raises ValueError on malformed input — caller should treat unknown
keys as "skip silently" rather than crash the resolve.
"""
parts = json.loads(key)
if not isinstance(parts, list) or not all(isinstance(p, str) for p in parts):
raise ValueError(f"composite key must decode to list[str], got {parts!r}")
return parts