graflo.architecture.evolution.canonicalize¶
Canonical form: the payload a content hash is taken over.
Two manifests that describe the same world model must hash equal. The minimal
canonical dict produced by
:meth:~graflo.architecture.base.ConfigBaseModel.to_minimal_canonical_dict
already normalizes defaults, None, aliases and key order. What it does not
normalize is list order -- and most lists in the contract are declaration
order over a set, so two identical schemas authored in different order, or one
authored and one replayed (apply_add_vertices appends), hash differently.
This module adds exactly that one normalization, driven by :data:LIST_ORDER
below: a total classification of every list-typed field reachable from
GraphManifest as :attr:ListOrder.SORTED or :attr:ListOrder.PRESERVED.
The asymmetry that decides every doubtful case¶
The two mistakes are not equally bad.
- Marking an order-significant list
SORTEDmakes two different world models hash equal. A false positive: lineage merges two things that are not the same, and nothing downstream can detect it. - Marking an order-insignificant list
PRESERVEDmakes two identical world models hash differently. A false negative: a missed dedup, a redundant commit. Visible, harmless, and fixable later by moving the entry.
So when in doubt, preserve. Every SORTED entry is a positive claim that
order carries no meaning in that field, and needs a reason. PRESERVED is the
safe default and is used for anything whose ordering semantics are not settled.
Sorting is by the canonical JSON rendering of each element, not by a per-field
key. That is total over heterogeneous unions (BindingsRegistry.connectors
holds five connector types), needs no tie-break rule, and cannot be
input-order-dependent: elements that compare equal are byte-identical and
therefore interchangeable. Nothing reads this order -- it is hash-side only, and
authored YAML keeps its declaration order.
Nesting is independent. Edge.identities is list[list[str]] marked
SORTED: the outer list of alternative keys is sorted, while each inner
composite key keeps its order, because (a, b) and (b, a) are different
keys.
Attributes¶
CANON_VERSION = 'graflo/canon@3'
module-attribute
¶
CLASSIFIED_MAPPINGS = frozenset({('DatabaseProfile', 'vertex_indexes')})
module-attribute
¶
LIST_ORDER = {('VertexConfig', 'vertices'): SORTED, ('EdgeConfig', 'edges'): SORTED, ('EdgeConfig', 'inverses'): SORTED, ('EdgeConfig', 'symmetric'): SORTED, ('Vertex', 'properties'): SORTED, ('Edge', 'properties'): SORTED, ('Vertex', 'secondary_identities'): SORTED, ('Edge', 'identities'): SORTED, ('Vertex', 'identity'): PRESERVED, ('Vertex', 'hash_identity_properties'): PRESERVED, ('IdentityBranch', 'fields'): PRESERVED, ('IdentityBranch', 'when_all_present'): PRESERVED, ('IdentityFunnel', 'branches'): PRESERVED, ('SecondaryIdentity', 'fields'): PRESERVED, ('Index', 'fields'): PRESERVED, ('DatabaseProfile', 'vertex_indexes'): SORTED, ('DatabaseProfile', 'edge_specs'): SORTED, ('DatabaseProfile', 'native_inverses'): SORTED, ('EdgePhysicalSpec', 'indexes'): SORTED, ('DefaultPropertyValues', 'edges'): SORTED, ('Semantics', 'exact_match'): SORTED, ('Semantics', 'synonyms'): SORTED, ('FieldSemantics', 'exact_match'): SORTED, ('FieldSemantics', 'synonyms'): SORTED, ('IngestionModel', 'resources'): SORTED, ('IngestionModel', 'transforms'): SORTED, ('ResourceConfig', 'pipeline'): PRESERVED, ('ResourceConfig', 'merge_collections'): SORTED, ('ResourceConfig', 'infer_edge_only'): SORTED, ('ResourceConfig', 'infer_edge_except'): SORTED, ('ResourceConfig', 'extra_weights'): SORTED, ('ResourceExtraWeightEntry', 'vertex_weights'): SORTED, ('Weight', 'fields'): PRESERVED, ('ProtoTransform', 'input'): PRESERVED, ('ProtoTransform', 'output'): PRESERVED, ('ProtoTransform', 'input_groups'): PRESERVED, ('ProtoTransform', 'output_groups'): PRESERVED, ('Provenance', 'parents'): PRESERVED, ('KeySelectionConfig', 'names'): SORTED, ('BindingsRegistry', 'connectors'): SORTED, ('BindingsRegistry', 'connector_templates'): SORTED, ('BindingsRegistry', 'connector_connection'): SORTED, ('BindingsRegistry', 'resource_connector'): SORTED, ('BindingsRegistry', 'staging_proxy'): SORTED, ('KafkaConnector', 'topics'): SORTED, ('APIConnector', 'retry_status_forcelist'): SORTED, ('TableConnector', 'joins'): PRESERVED, ('TableConnector', 'select_columns'): PRESERVED, ('JoinClause', 'select_fields'): PRESERVED, ('FilterExpression', 'deps'): PRESERVED, ('FilterExpression', 'value'): PRESERVED, ('TableConnector', 'filters'): PRESERVED, ('Vertex', 'filters'): PRESERVED}
module-attribute
¶
PRESERVED = ListOrder.PRESERVED
module-attribute
¶
SORTED = ListOrder.SORTED
module-attribute
¶
Classes¶
ListOrder
¶
Bases: str, Enum
Whether a list field's order carries meaning.
Source code in graflo/architecture/evolution/canonicalize.py
UnclassifiedListField
¶
Bases: LookupError
A list field reached during canonicalization is absent from LIST_ORDER.
Raised rather than guessed: a default of either kind would silently decide a hash question that this module exists to make explicit.
Source code in graflo/architecture/evolution/canonicalize.py
Attributes¶
field_name = field_name
instance-attribute
¶
model_name = model_name
instance-attribute
¶
Methods:¶
__init__(model_name, field_name)
¶
Source code in graflo/architecture/evolution/canonicalize.py
Functions:¶
canonical_payload(model)
¶
Canonical, hashable payload for any GraFlo config model.
Equivalent to to_minimal_canonical_dict() with every order-insignificant
list sorted. Raises :class:UnclassifiedListField if the model tree reaches
a list field the audit table does not classify.