Inverses for the subset of contract operations that have one.
Several ops are lossy — merge_vertices discards which source each property
came from, change_field_types discards the previous type when narrowing,
sanitize and project_manifest drop material outright. There is no
information anywhere from which to reconstruct the prior state, so a generic
downgrade() is not achievable and pretending otherwise would produce a
manifest that merely looks restored.
What is achievable is an inverse for the reversible subset, computed against the
pre-state manifest: inverting remove_vertices requires the removed
:class:Vertex models, and they exist only before the op runs.
:func:invert_op returns None for an irreversible op. Callers decide what
that means; :mod:~graflo.architecture.evolution.revision prefers replaying
from a base, which is always correct, and only falls back to inverses when no
base is available.
invert_op(op, *, manifest)
The op undoing op, computed against the pre-state manifest.
Returns None when op is irreversible. manifest must be the manifest
as it was before op was applied — that is where the information an
inverse needs still exists.
Source code in graflo/architecture/evolution/inverse.py
| def invert_op(op: ManifestOp, *, manifest: GraphManifest) -> ManifestOp | None:
"""The op undoing *op*, computed against the **pre-state** *manifest*.
Returns ``None`` when *op* is irreversible. *manifest* must be the manifest
as it was *before* *op* was applied — that is where the information an
inverse needs still exists.
"""
if not is_reversible(op):
return None
handler = _HANDLERS.get(op.op)
if handler is None:
logger.debug("no inverse handler for op %r", op.op)
return None
return handler(op, manifest)
|
invert_ops(ops, *, manifest)
Inverses for ops in reverse order, plus reasons for any that lack one.
Each inverse is computed against the state before its own op, so the ops
are replayed forward to reconstruct those intermediate states.
Source code in graflo/architecture/evolution/inverse.py
| def invert_ops(
ops: list[ManifestOp], *, manifest: GraphManifest
) -> tuple[list[ManifestOp], list[str]]:
"""Inverses for *ops* in reverse order, plus reasons for any that lack one.
Each inverse is computed against the state *before* its own op, so the ops
are replayed forward to reconstruct those intermediate states.
"""
from .apply import apply_evolution
states: list[GraphManifest] = [manifest]
current = manifest
for op in ops:
current = apply_evolution(current, [op], bump_version=False, finish_init=False)
states.append(current)
inverses: list[ManifestOp] = []
blockers: list[str] = []
for index in range(len(ops) - 1, -1, -1):
op = ops[index]
reason = irreversible_reason(op)
if reason is not None:
blockers.append(f"{op.op}: {reason}")
continue
inverse = invert_op(op, manifest=states[index])
if inverse is None:
blockers.append(f"{op.op}: no inverse could be derived")
continue
inverses.append(inverse)
return inverses, blockers
|
irreversible_reason(op)
Why op cannot be inverted, or None when it can.
Source code in graflo/architecture/evolution/inverse.py
| def irreversible_reason(op: ManifestOp) -> str | None:
"""Why *op* cannot be inverted, or ``None`` when it can."""
return IRREVERSIBLE.get(getattr(op, "op", ""))
|
is_reversible(op)
Whether op has a total inverse.
Source code in graflo/architecture/evolution/inverse.py
| def is_reversible(op: ManifestOp) -> bool:
"""Whether *op* has a total inverse."""
return getattr(op, "op", None) not in IRREVERSIBLE
|