Skip to content

graflo.architecture.evolution.inverse

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