graflo.architecture.evolution.canonical¶
Canonical vocabulary maps, and the shared pieces of merge resolution.
A :class:~graflo.architecture.evolution.ops.CanonicalMap translates one side's
vocabulary into canonical names — a partial function on names, identity where
unmapped, and idempotent: a canonical name is a fixed point nothing maps away
from. On its own it lowers to one
:class:~graflo.architecture.evolution.ops.CanonicalizeOp
(:func:canonical_map_to_ops); :func:dangling_entries and
:func:trim_canonical_map check it against one manifest before any merge.
In a merge it is the vocabulary layer: a class's default name in the union.
:mod:~graflo.architecture.evolution.naming_graph resolves it together with
the op's equivalences and renames, in one pass over the sides' own names;
:func:resolve_clusters is that resolution, raising every problem at once.
Vocabulary¶
- declared map — a
CanonicalMapthe author wrote:op.canonical_maps[scope]and thecanonical_maps=pairs handed to merge. Scopedleft/right(that side's own names) orboth(either side's names). Folded per side by :func:compose_canonical_mapsinto a :class:DeclaredMaps. - composite map (:class:
SideMaps, oneCanonicalizeOpper side) — what merge applies to that side before the union by name: every group member onto its merged name, every other class onto its rename or vocabulary name, simultaneously. - satisfied entry — a declared entry whose source is absent from a side and whose target is present: taken as already applied by the caller. A heuristic — it cannot tell that from a target that never had that source — so it is logged.
- dangling entry — a declared entry that matches nothing on any side it
could apply to. A typo, refused, each with a near-miss candidate where
another spelling denotes the same concept.
allow_dangling_entriesdrops them instead, for a shared vocabulary deliberately broader than the manifest it is applied to. - completion (:class:
Completion) — a declaration that would settle a refusal, carried as a paste-ready payload.
Identity is nominal: a class is the same class across two manifests only by name (or by declared equivalence) — nothing structural fingerprints it, and merge never infers a match.
Attributes¶
CompletionKind = Literal['extend_cluster', 'declare_equivalences', 'set_into', 'rename_away', 'add_key_source', 'acknowledge']
module-attribute
¶
PropertyLayer = Literal['equivalence', 'vocabulary', 'rename', 'demotion']
module-attribute
¶
Scope = Literal['left', 'right', 'both']
module-attribute
¶
__all__ = ['CanonicalMap', 'ClusterResolution', 'Completion', 'DanglingEntry', 'DeclaredMaps', 'MergeCanonicalConflictError', 'MergeIncompleteError', 'Scope', 'SideMaps', 'SideNames', 'canonical_map_to_ops', 'canonical_near_collisions', 'canonicalize_ops', 'check_attribute_fixed_points', 'check_property_fields_exist', 'check_property_maps_against_manifest', 'compose_canonical_maps', 'dangling_entries', 'dangling_refusal', 'fold_declared_maps', 'resolve_clusters', 'trim_canonical_map', 'validate_and_complete_canonical_map']
module-attribute
¶
logger = logging.getLogger(__name__)
module-attribute
¶
Classes¶
CanonicalMap
¶
Bases: ConfigBaseModel
Declared translation of a source vocabulary into canonical names.
A partial function on names, identity where unmapped: vertices maps
source class names to canonical class names, relations does the same
for relation names, and properties maps, per source class name,
source attribute names to canonical attribute names — including for
classes whose name does not change. Two sources sharing a target is a
merge and must be acknowledged with allow_merges.
It is a vocabulary, so it is idempotent: a canonical name is a fixed
point that no entry maps away from. A chain ({X: Z, Z: Q}) or a swap
is refused at construction — that shape is a relabel, which
:class:CanonicalizeOp expresses directly. The rule is what lets two
maps, or a map and an equivalence, be checked for agreement without
asking in which order they were written.
Used on its own through :func:~graflo.architecture.evolution.canonical.canonical_map_to_ops,
and on :attr:MergeManifestsOp.canonical_maps where it names the
merged classes and is checked against the declared equivalences.
Source code in graflo/architecture/evolution/ops/canonical.py
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | |
Attributes¶
allow_dangling_entries = PydanticField(default=False, description='Accept entries that name nothing in the manifest the map is applied to, dropping and logging each one. A shared vocabulary map is legitimately broader than any single manifest. Off by default, because a misspelt class has exactly the same shape, and dropping it silently narrows the rename to less than the author asked for.')
class-attribute
instance-attribute
¶
allow_merges = PydanticField(default=False, description='Accept a non-injective ``vertices`` / ``relations`` map. Two sources sharing a canonical target is a *merge*, not a rename; it must be a stated intent because merging fuses entities and can create self-relations.')
class-attribute
instance-attribute
¶
allow_observation_fusion = PydanticField(default=False, description='Accept a merge this map makes whose sources are produced in one accumulator slot, fusing those observations into one node.')
class-attribute
instance-attribute
¶
allow_self_relations = PydanticField(default=False, description='Accept a merge this map makes whose sources are connected by an edge that becomes a self-relation.')
class-attribute
instance-attribute
¶
properties = PydanticField(default_factory=dict, description='Per-source-class attribute rename map: ``{source_class: {source_attr: canonical_attr}}``.')
class-attribute
instance-attribute
¶
relation_targets
property
¶
Canonical relation names this map establishes.
relations = PydanticField(default_factory=dict, description='Relation rename map: ``{source_relation: canonical_relation}``.')
class-attribute
instance-attribute
¶
vertex_targets
property
¶
Canonical class names this map establishes (targets of a real rename).
vertices = PydanticField(default_factory=dict, description='Class rename map: ``{source_class: canonical_class}``.')
class-attribute
instance-attribute
¶
Methods:¶
canonical_class(source_class)
¶
canonical_property_names(canonical_class)
¶
Canonical attribute names the map establishes on canonical_class.
Source code in graflo/architecture/evolution/ops/canonical.py
canonical_relation(source_relation)
¶
Canonical name of source_relation (itself when unmapped).
ClusterResolution
dataclass
¶
What merge applies: the resolved groups and one composite relabel per side.
declared is the folded vocabulary as seen from each side;
side_maps is the composite — every group member onto its merged
name, every other class onto its rename or vocabulary name, every demoted
key field onto <space>__<field> (its local_key tag, else the
side's origin) — one
:class:~graflo.architecture.evolution.ops.CanonicalizeOp per side.
index includes any group merge synthesized; each cluster's members
are the closed group. findings are the notes the resolution made
(defaults it applied), and graph the naming graph it was read from.
Source code in graflo/architecture/evolution/canonical.py
Attributes¶
auto_key_sources = field(default_factory=dict)
class-attribute
instance-attribute
¶
declared
instance-attribute
¶
demoted_keys = field(default_factory=dict)
class-attribute
instance-attribute
¶
findings = ()
class-attribute
instance-attribute
¶
graph = None
class-attribute
instance-attribute
¶
index
instance-attribute
¶
key_spaces = field(default_factory=dict)
class-attribute
instance-attribute
¶
key_tags = field(default_factory=dict)
class-attribute
instance-attribute
¶
origin_tagged = field(default_factory=dict)
class-attribute
instance-attribute
¶
origins = field(default_factory=lambda: {'left': 'left', 'right': 'right'})
class-attribute
instance-attribute
¶
property_layers = field(default_factory=dict)
class-attribute
instance-attribute
¶
side_maps
instance-attribute
¶
Methods:¶
__init__(index, side_maps, declared, findings=(), graph=None, origins=(lambda: {'left': 'left', 'right': 'right'})(), origin_tagged=dict(), demoted_keys=dict(), key_spaces=dict(), key_tags=dict(), property_layers=dict(), auto_key_sources=dict())
¶
Completion
dataclass
¶
A declaration that would settle a merge refusal, ready to paste.
Payloads are VertexEquivalence / RelationEquivalence documents and
a renames fragment ({side: {vertices: {old: new}}}), so a CLI can
print them and an author can paste them. label says in a few words
what applying it does.
Source code in graflo/architecture/evolution/canonical.py
Attributes¶
kind
instance-attribute
¶
label = ''
class-attribute
instance-attribute
¶
relation_equivalences = ()
class-attribute
instance-attribute
¶
renames = field(default_factory=dict)
class-attribute
instance-attribute
¶
replaces = None
class-attribute
instance-attribute
¶
side = None
class-attribute
instance-attribute
¶
vertex_equivalences = ()
class-attribute
instance-attribute
¶
Methods:¶
__init__(kind, side=None, vertex_equivalences=(), relation_equivalences=(), renames=dict(), label='', replaces=None)
¶
to_dict()
¶
The completion as a plain document.
Source code in graflo/architecture/evolution/canonical.py
DanglingEntry
dataclass
¶
A canonical-map entry whose source matches nothing on its side.
The source names no class or relation the side declares, and the entry is
none of the four ways an absent source is still meaningful: a cluster
member, an already-applied rename, a merged name as the author spelled
it, or a both-scoped entry that applies to the other side.
Carried as data rather than refused one at a time, so that authoring a map against a schema of hundreds of classes is not one refusal per mistake.
Source code in graflo/architecture/evolution/canonical.py
Attributes¶
kind
instance-attribute
¶
side
instance-attribute
¶
source
instance-attribute
¶
suggestion = ''
class-attribute
instance-attribute
¶
target = None
class-attribute
instance-attribute
¶
Methods:¶
__init__(side, kind, source, target=None, suggestion='')
¶
describe()
¶
The entry as it reads in a refusal, without naming the side.
The near-miss :attr:suggestion is left to the caller to place: it is
a trailing clause, and only a listing has somewhere to put one.
Source code in graflo/architecture/evolution/canonical.py
DeclaredMaps
dataclass
¶
The declared vocabulary as each side sees it.
left / right are the both-scoped map folded under that side's
own map; both is kept apart because its entries may legitimately apply
to one side only.
Source code in graflo/architecture/evolution/canonical.py
MergeCanonicalConflictError
¶
Bases: Refusal
A merge op's clusters and its declared maps contradict each other.
A contradiction (one name, two targets; a fixed point moved), an ambiguity
(a canonical name denoting two members), or a dangling entry. The
subclass :class:MergeIncompleteError is the one refusal an extension
resolves.
check names the rule that refused — the phrase the message starts
with — and subjects the names it is about, as
:func:~graflo.architecture.evolution.equivalence.subject ids; see
:class:.Refusal.
Source code in graflo/architecture/evolution/canonical.py
MergeIncompleteError
¶
Bases: MergeCanonicalConflictError
The declarations are consistent but do not cover a name; an extension would.
Distinct from a contradiction: nothing has to be retracted, something has
to be added, and :attr:completion says what.
Source code in graflo/architecture/evolution/canonical.py
SideMaps
dataclass
¶
The composite relabel per side: the one op merge applies to each.
Source code in graflo/architecture/evolution/canonical.py
SideNames
dataclass
¶
The class and relation names one side actually declares.
Source code in graflo/architecture/evolution/canonical.py
Attributes¶
relations
instance-attribute
¶
vertices
instance-attribute
¶
Methods:¶
__init__(vertices, relations)
¶
of(manifest)
classmethod
¶
Source code in graflo/architecture/evolution/canonical.py
Functions:¶
canonical_map_to_ops(cm, *, allow_self_relations=False, allow_observation_fusion=False)
¶
Lower a declared map to its single op.
A canonical map is a function on names, and
:class:~graflo.architecture.evolution.ops.CanonicalizeOp applies exactly
that function in one step: attribute renames (keyed by the source class)
first, then classes and relations simultaneously, so no op order can leak
into the result. A map with no effective entry lowers to no op at all. A
group of more than one class or relation is a merge and is refused unless
allow_merges is set.
Source code in graflo/architecture/evolution/canonical.py
canonical_near_collisions(left_names, right_names, *, exempt)
¶
(left, right) pairs that key alike under canonical_slug but differ.
Exact matches are excluded: those are the same-name path, so the two checks can never report one pair twice.
Source code in graflo/architecture/evolution/canonical.py
canonicalize_ops(op)
¶
op as the op list to apply: empty when it has no effective entry.
check_attribute_fixed_points(cluster, *, side, declared)
¶
A canonical attribute the map established on a member may not be renamed by the cluster.
The fixed-point set is keyed by canonical class, not by member:
canonical_property_names folds every source class the map sends to one
canonical name, which is the whole point -- one member's rename establishes
the attribute for every sibling that lands on the same class. Asking it
with a raw member name returns nothing for exactly the members the map
renames, which is to say for exactly the cases this check is here for.
Source code in graflo/architecture/evolution/canonical.py
check_property_fields_exist(manifest, cluster, *, side, declared)
¶
Every field a property equivalence names must exist on its member, as spelled.
Source code in graflo/architecture/evolution/canonical.py
check_property_maps_against_manifest(manifest, relabel, *, side)
¶
Refuse a property rename whose old name is absent or whose new name collides.
A collision would otherwise drop the losing field at apply time; this turns it into a loud merge-time error instead of a quiet data loss.
Source code in graflo/architecture/evolution/canonical.py
compose_canonical_maps(base, extension)
¶
Partial-function union of two declared maps; a target of either is a fixed point.
Every source named by base or extension maps to exactly one target; a
source the two disagree on raises :class:MergeCanonicalConflictError.
A target of either map is a fixed point the other may not move, checked
in both directions so the result does not depend on which map is base.
properties union the same way per source class.
Source code in graflo/architecture/evolution/canonical.py
dangling_entries(cm, manifest, *, side='left')
¶
The map's entries that match nothing in manifest, with near-miss candidates.
A canonical map is authored against one manifest long before it is merged
against another, and this is that check on its own: no equivalences, no
other side, no merge. With no clusters declared there are no merged
names and no both scope, so the classification merge uses collapses
to its two surviving cases — a source the manifest declares is applicable,
a source it does not but whose target it does is already applied — and
everything else dangles.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cm
|
CanonicalMap
|
The declared map. |
required |
manifest
|
GraphManifest
|
The manifest the map is meant to apply to. |
required |
side
|
Side
|
Which side the map is scoped to; names the entries in the result. |
'left'
|
Returns:
| Name | Type | Description |
|---|---|---|
One |
DanglingEntry
|
|
...
|
relations, then attribute maps. Empty means the map applies as written. |
Source code in graflo/architecture/evolution/canonical.py
dangling_refusal(entries, *, names=None)
¶
One refusal naming every dangling entry on a side.
A canonical map is authored as a whole, so it is corrected as a whole: the refusal lists each entry with a near-miss candidate where one exists, rather than surfacing the next mistake only after the previous one is fixed and the merge re-run.
names is what the side declares, used for context. A caller classifying one entry in isolation has no side to describe and omits it.
Source code in graflo/architecture/evolution/canonical.py
fold_declared_maps(op, extra)
¶
Source code in graflo/architecture/evolution/canonical.py
resolve_clusters(op, *, left, right, canonical_maps=())
¶
Resolve op's groups and build the per-side composite relabel.
canonical_maps are extra (side, map) vocabulary pairs folded into
op.canonical_maps. left / right are the manifests about to be
merged, in their own names: the vocabulary, the equivalences and
op.renames are resolved together over those names, in one pass (see
:mod:~graflo.architecture.evolution.naming_graph). Each side's origin is
:func:~graflo.architecture.evolution.naming_graph.merge_origins.
Raises:
| Type | Description |
|---|---|
MergeNamingError
|
Every blocking problem with the declarations, at once
-- a :class: |
Source code in graflo/architecture/evolution/canonical.py
trim_canonical_map(cm, manifest, *, side='left')
¶
cm with its entries for manifest only, and the entries dropped.
Trimming is an authoring step, not something merge does on its own: the
result is a value to inspect and save, so that a map narrowed to a manifest
is a change with a diff rather than a silent omission at merge time. A
map that is deliberately broader than any one manifest is better served by
allow_dangling_entries, which keeps the map whole.
Returns:
| Type | Description |
|---|---|
CanonicalMap
|
The trimmed map and the entries removed, as |
tuple[DanglingEntry, ...]
|
|
Source code in graflo/architecture/evolution/canonical.py
validate_and_complete_canonical_map(op, *, left, right, canonical_maps=())
¶
Validate op against its vocabulary and return the completed per-side relabels.
Every group's merged name is completed — from into, the vocabulary,
or the members' shared spelling — and every member maps onto it; the
remaining vocabulary entries and renames are carried as written.
Apply the result to each side with :func:canonicalize_ops before the
schema/resource union, which is what
:func:~graflo.architecture.evolution.merge.merge_manifests does.
Raises the :class:MergeNamingError of :func:resolve_clusters.