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.

Attributes

IRREVERSIBLE = {'merge_vertices': 'merging discards which source each property and identity came from', 'merge_edges': "merging discards the source relations' individual definitions", 'change_field_types': 'the previous field type is not recoverable once overwritten', 'sanitize': 'renames are flavor-driven and not recorded per element', 'project_manifest': 'projection drops elements outright', 'merge_manifests': 'merge is binary; there is no single prior manifest', 'add_resource_transforms': 'appended pipeline steps are not tracked per-op; there is no remove_resource_transforms op', 'ensure_extracted_fields': 'widened projections are not tracked per-op; there is no narrowing op'} module-attribute

__all__ = ['IRREVERSIBLE', 'invert_op', 'invert_ops', 'irreversible_reason', 'is_reversible'] module-attribute

logger = logging.getLogger(__name__) module-attribute

Classes

Functions:

invert_op(op, *, manifest)

The op undoing op, computed against the pre-state manifest.

Returns None when op is irreversible, and when it is reversible in general but not from this manifest. manifest must be the manifest as it was before op was applied — that is where the information an inverse needs still exists.

An inverse is exact or absent. The handlers derive a candidate from the op's payload, and the payload says what the op named, not what it did: a removal names a property the type never had and is skipped forward, a relation-addressed op meets edges that disagree, a removal cascades over elements no single op restores. So the candidate is replayed, and one that does not land back on manifest is not offered.

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, and when it is reversible in
    general but not from this *manifest*. *manifest* must be the manifest as it
    was *before* *op* was applied — that is where the information an inverse
    needs still exists.

    An inverse is exact or absent. The handlers derive a candidate from the op's
    payload, and the payload says what the op *named*, not what it *did*: a
    removal names a property the type never had and is skipped forward, a
    relation-addressed op meets edges that disagree, a removal cascades over
    elements no single op restores. So the candidate is replayed, and one that
    does not land back on *manifest* is not offered.
    """
    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
    inverse = handler(op, manifest)
    if inverse is None or not _restores(manifest, op, inverse):
        return None
    return inverse

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
    from .hashing import manifest_hash

    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:
            if manifest_hash(states[index]) == manifest_hash(states[index + 1]):
                # The forward op changed nothing (every entry it named was
                # already there), so the identity is its inverse.
                continue
            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.

canonicalize is reversible exactly when it only renames: a group of more than one class or relation is a merge, which discards where each property and identity came from.

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.

    ``canonicalize`` is reversible exactly when it only renames: a group of
    more than one class or relation is a merge, which discards where each
    property and identity came from.
    """
    if isinstance(op, CanonicalizeOp) and op.merges:
        return (
            "canonicalize merges classes or relations, which discards which "
            "source each property and identity came from"
        )
    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 irreversible_reason(op) is None