Skip to content

graflo.architecture.evolution.equivalence

The resolved shape of a merge's groups.

A :class:Cluster is one group as merge applies it: its members on each side in the manifests' own names, closed over every class a vocabulary merges with one of them, and the one name the group takes. A :class:ClusterIndex holds every group of one merge op. Both are built by :mod:~graflo.architecture.evolution.naming_graph, which also owns every rule about which groups may exist; this module only carries the result.

Nodes are (side, name) pairs, so a class named Org on the left is never confused with Org on the right, and :func:subject spells them as the ids refusals and previews point at.

Attributes

DeclarationT = TypeVar('DeclarationT', VertexEquivalence, RelationEquivalence) module-attribute

Kind = Literal['vertex', 'relation'] module-attribute

RelationCluster = Cluster[RelationEquivalence] module-attribute

Side = Literal['left', 'right'] module-attribute

SubjectScope = Literal['left', 'right', 'merged', 'canonical', 'resource'] module-attribute

Classes

Cluster dataclass

Bases: Generic[DeclarationT]

One group, over vertices or relations, in the sides' own names.

left / right are the closed member sets: every class the group's equivalences name, and every class a vocabulary merges with one of them. declared_left / declared_right are the ones the equivalences name. declaration is one equivalence over the closed sets. declarations are the positions, in the op's equivalence list, of the equivalences folded into it; empty when merge synthesized it.

Source code in graflo/architecture/evolution/equivalence.py
@dataclass(frozen=True)
class Cluster(Generic[DeclarationT]):
    """One group, over vertices or relations, in the sides' own names.

    ``left`` / ``right`` are the closed member sets: every class the group's
    equivalences name, and every class a vocabulary merges with one of them.
    ``declared_left`` / ``declared_right`` are the ones the equivalences
    name. ``declaration`` is one equivalence over the closed sets.
    ``declarations`` are the positions, in the op's equivalence list, of the
    equivalences folded into it; empty when merge synthesized it.
    """

    left: tuple[str, ...]
    right: tuple[str, ...]
    into: str
    declaration: DeclarationT
    aliases: dict[Side, dict[str, str]] = field(default_factory=dict)
    declared_into: str | None = None
    synthesized: bool = False
    declared_left: tuple[str, ...] = ()
    declared_right: tuple[str, ...] = ()
    declarations: tuple[int, ...] = ()

    def declared_members(self, side: Side) -> tuple[str, ...]:
        """The members the equivalences name on *side*, without the vocabulary-joined ones."""
        declared = self.declared_left if side == "left" else self.declared_right
        return declared or self.members(side)

    def members(self, side: Side) -> tuple[str, ...]:
        return self.left if side == "left" else self.right

    def resolved(self, side: Side, declared: str) -> str:
        """The member a spelling names on *side*: its own name, or its canonical one."""
        return self.aliases.get(side, {}).get(declared, declared)

    def property_maps(self, side: Side) -> dict[str, dict[str, str]]:
        """The declaration's per-member attribute maps, keyed by resolved member name."""
        declaration = self.declaration
        if not isinstance(declaration, VertexEquivalence):
            return {}
        return {
            self.resolved(side, member): attrs
            for member, attrs in declaration.property_maps(side).items()
        }

Attributes

aliases = field(default_factory=dict) class-attribute instance-attribute
declaration instance-attribute
declarations = () class-attribute instance-attribute
declared_into = None class-attribute instance-attribute
declared_left = () class-attribute instance-attribute
declared_right = () class-attribute instance-attribute
into instance-attribute
left instance-attribute
right instance-attribute
synthesized = False class-attribute instance-attribute

Methods:

__init__(left, right, into, declaration, aliases=dict(), declared_into=None, synthesized=False, declared_left=(), declared_right=(), declarations=())
declared_members(side)

The members the equivalences name on side, without the vocabulary-joined ones.

Source code in graflo/architecture/evolution/equivalence.py
def declared_members(self, side: Side) -> tuple[str, ...]:
    """The members the equivalences name on *side*, without the vocabulary-joined ones."""
    declared = self.declared_left if side == "left" else self.declared_right
    return declared or self.members(side)
members(side)
Source code in graflo/architecture/evolution/equivalence.py
def members(self, side: Side) -> tuple[str, ...]:
    return self.left if side == "left" else self.right
property_maps(side)

The declaration's per-member attribute maps, keyed by resolved member name.

Source code in graflo/architecture/evolution/equivalence.py
def property_maps(self, side: Side) -> dict[str, dict[str, str]]:
    """The declaration's per-member attribute maps, keyed by resolved member name."""
    declaration = self.declaration
    if not isinstance(declaration, VertexEquivalence):
        return {}
    return {
        self.resolved(side, member): attrs
        for member, attrs in declaration.property_maps(side).items()
    }
resolved(side, declared)

The member a spelling names on side: its own name, or its canonical one.

Source code in graflo/architecture/evolution/equivalence.py
def resolved(self, side: Side, declared: str) -> str:
    """The member a spelling names on *side*: its own name, or its canonical one."""
    return self.aliases.get(side, {}).get(declared, declared)

ClusterIndex dataclass

Every group of one merge op, as merge applies them.

Source code in graflo/architecture/evolution/equivalence.py
@dataclass(frozen=True)
class ClusterIndex:
    """Every group of one merge op, as merge applies them."""

    vertices: tuple[Cluster[VertexEquivalence], ...]
    relations: tuple[Cluster[RelationEquivalence], ...]

    @property
    def labels(self) -> frozenset[str]:
        """The merged names of every vertex cluster."""
        return frozenset(c.into for c in self.vertices)

    @property
    def relation_labels(self) -> frozenset[str]:
        """The merged names of every relation cluster."""
        return frozenset(c.into for c in self.relations)

    @property
    def declared_intos(self) -> frozenset[str]:
        """Every ``into`` the equivalences of the groups spell."""
        return frozenset(
            c.declared_into
            for c in (*self.vertices, *self.relations)
            if c.declared_into is not None
        )

    def vertex_members(self, side: Side) -> frozenset[str]:
        out: set[str] = set()
        for c in self.vertices:
            out.update(c.members(side))
        return frozenset(out)

    def relation_members(self, side: Side) -> frozenset[str]:
        out: set[str] = set()
        for c in self.relations:
            out.update(c.members(side))
        return frozenset(out)

    def cluster_for_label(self, into: str) -> Cluster[VertexEquivalence] | None:
        """The vertex cluster collapsing onto *into*, or ``None``."""
        return next((c for c in self.vertices if c.into == into), None)

Attributes

declared_intos property

Every into the equivalences of the groups spell.

labels property

The merged names of every vertex cluster.

relation_labels property

The merged names of every relation cluster.

relations instance-attribute
vertices instance-attribute

Methods:

__init__(vertices, relations)
cluster_for_label(into)

The vertex cluster collapsing onto into, or None.

Source code in graflo/architecture/evolution/equivalence.py
def cluster_for_label(self, into: str) -> Cluster[VertexEquivalence] | None:
    """The vertex cluster collapsing onto *into*, or ``None``."""
    return next((c for c in self.vertices if c.into == into), None)
relation_members(side)
Source code in graflo/architecture/evolution/equivalence.py
def relation_members(self, side: Side) -> frozenset[str]:
    out: set[str] = set()
    for c in self.relations:
        out.update(c.members(side))
    return frozenset(out)
vertex_members(side)
Source code in graflo/architecture/evolution/equivalence.py
def vertex_members(self, side: Side) -> frozenset[str]:
    out: set[str] = set()
    for c in self.vertices:
        out.update(c.members(side))
    return frozenset(out)

Functions:

did_you_mean(name, candidates)

A suffix naming a candidate that denotes the same concept, if any.

Authoring an equivalence in the wrong convention is the likeliest mistake at this boundary, and "not in left manifest" alone is a dead end when the vertex is right there under another spelling.

Source code in graflo/architecture/evolution/equivalence.py
def did_you_mean(name: str, candidates: Iterable[str]) -> str:
    """A suffix naming a candidate that denotes the same concept, if any.

    Authoring an equivalence in the wrong convention is the likeliest mistake
    at this boundary, and "not in left manifest" alone is a dead end when the
    vertex is right there under another spelling.
    """
    key = canonical_slug(name)
    near = sorted(c for c in candidates if c != name and canonical_slug(c) == key)
    if not near:
        return ""
    return (
        f"; it has {near[0]!r}, which denotes the same concept — author the "
        "equivalence in the manifest's own spelling"
    )

subject(scope, name, attr=None)

A stable id for the class or attribute a refusal is about.

Every refusal at this boundary names the declarations it refuses, and a caller that wants to point at them — a preview, a diagram, an editor — needs those names as data rather than parsed back out of prose.

Parameters:

Name Type Description Default
scope SubjectScope

Which vocabulary the name lives in.

required
name str

The class or relation name.

required
attr str | None

An attribute of it, when the subject is narrower than a class.

None

Returns:

Type Description
str

"left:Firm" for a class, "left:Firm.firm_id" for an attribute.

Source code in graflo/architecture/evolution/equivalence.py
def subject(scope: SubjectScope, name: str, attr: str | None = None) -> str:
    """A stable id for the class or attribute a refusal is about.

    Every refusal at this boundary names the declarations it refuses, and a
    caller that wants to *point* at them — a preview, a diagram, an editor —
    needs those names as data rather than parsed back out of prose.

    Args:
        scope: Which vocabulary the name lives in.
        name: The class or relation name.
        attr: An attribute of it, when the subject is narrower than a class.

    Returns:
        ``"left:Firm"`` for a class, ``"left:Firm.firm_id"`` for an attribute.
    """
    return f"{scope}:{name}" if attr is None else f"{scope}:{name}.{attr}"