Source code for mmm_framework.config.spec_diff
"""Pre-specification lock + diff (P2-3).
Pre-registration is only meaningful if divergences from the locked spec are
*detected*, not just logged. :func:`diff_spec` is the pure, testable core: it
compares two serialized specs (a DAG, an MFFConfig, a ModelConfig -- any nested
dict) and reports what changed. List items are matched by identity (``name`` /
``id`` / ``source→target``) rather than position, so reordering is not a false
divergence and a genuinely added/removed channel or control is caught.
"""
from __future__ import annotations
from collections import Counter
from dataclasses import dataclass
from typing import Any
[docs]
@dataclass(frozen=True)
class SpecChange:
"""A single difference between a frozen spec and the current one."""
path: str
kind: str # "added" | "removed" | "changed"
old: Any
new: Any
[docs]
def describe(self) -> str:
if self.kind == "added":
return f"+ {self.path} = {self.new!r} (added)"
if self.kind == "removed":
return f"- {self.path} = {self.old!r} (removed)"
return f"~ {self.path}: {self.old!r} → {self.new!r}"
def _list_item_key(x: Any) -> str | None:
"""Stable identity key for a list item, or None to fall back to index."""
if isinstance(x, dict):
for k in ("name", "id", "variable_name"):
if x.get(k):
return f"{k}={x[k]}"
if "source" in x and "target" in x:
return f"{x['source']}->{x['target']}"
return None
def _flatten(obj: Any, prefix: str = "") -> dict[str, Any]:
"""Flatten a nested structure to ``dotted.path -> leaf`` entries.
Lists of identifiable dicts are keyed by identity so order does not matter.
"""
out: dict[str, Any] = {}
if isinstance(obj, dict):
if not obj:
# Record an empty *nested* container as a leaf so it can change to/
# from {}, but never emit a bare top-level key.
if prefix:
out[prefix] = {}
return out
for k, v in obj.items():
key = f"{prefix}.{k}" if prefix else str(k)
out.update(_flatten(v, key))
elif isinstance(obj, list):
if not obj:
if prefix:
out[prefix] = []
return out
keys = [_list_item_key(x) for x in obj]
if all(k is not None for k in keys):
# Identity-keyed; duplicate names are disambiguated by occurrence so
# both specs key consistently (a genuinely duplicated channel shows
# as one clean diff, not a re-indexing of the whole list).
counts = Counter(keys)
occ: dict[str, int] = {}
for k, x in zip(keys, obj):
if counts[k] > 1:
i = occ.get(k, 0)
occ[k] = i + 1
item_key = f"{k}#{i}"
else:
item_key = k
out.update(_flatten(x, f"{prefix}[{item_key}]"))
else:
# Some items have no stable identity -> positional keying.
for i, x in enumerate(obj):
out.update(_flatten(x, f"{prefix}[{i}]"))
else:
out[prefix] = obj
return out
[docs]
def diff_spec(frozen: dict[str, Any], current: dict[str, Any]) -> list[SpecChange]:
"""Return the structural differences from ``frozen`` to ``current``.
Each :class:`SpecChange` is an added / removed / changed leaf, identified by
a dotted path. An empty list means the current spec matches what was locked.
"""
f = _flatten(frozen)
c = _flatten(current)
changes: list[SpecChange] = []
for key in sorted(set(f) | set(c)):
if key not in c:
changes.append(SpecChange(key, "removed", f[key], None))
elif key not in f:
changes.append(SpecChange(key, "added", None, c[key]))
elif f[key] != c[key]:
changes.append(SpecChange(key, "changed", f[key], c[key]))
return changes
[docs]
def summarize_spec_diff(changes: list[SpecChange]) -> str:
"""Human-readable summary; empty changes => an explicit 'no divergence'."""
if not changes:
return "No divergence from the pre-registered specification."
lines = [f"{len(changes)} divergence(s) from the pre-registered specification:"]
lines += [f" {ch.describe()}" for ch in changes]
return "\n".join(lines)
__all__ = ["SpecChange", "diff_spec", "summarize_spec_diff"]