Snapshot-local equality and functional identity
gestaltdb.reasoning.equality.canonicalize rebuilds union-find state from
scratch on every call over one explicit fact set: sameAs/differentFrom links
plus conflicts generated from functional properties. Representatives are the
lexicographically smallest members, aliases are preserved, and every merge
carries its exact link evidence. Nothing is merged physically, no unique-name
assumption is made, and literals never merge.
from gestaltdb.reasoning.equality import (
DifferentFrom, EqualityScope, FunctionalProperty, SameAs, canonicalize,
)
from gestaltdb.reasoning.typed import TypedFact
mapping = canonicalize(
[TypedFact("mbox", ["n", "a@x"]), TypedFact("mbox", ["n", "b@x"])],
[FunctionalProperty("mbox"), DifferentFrom("a@x", "b@x")],
scope=EqualityScope("snapshot-1"),
)
assert mapping.representative("b@x") == "a@x"
assert mapping.members("a@x") == ("a@x", "b@x")
assert mapping.justification("a@x") == (SameAs("a@x", "b@x"),)
assert len(mapping.conflicts) == 1 # the violated inequality, with evidence
Canonicalization, not merging
The input facts are never rewritten in place. rewrite returns a new tuple
with entity values replaced by representatives (literals untouched), for
callers that explicitly opt into canonical reading. Member aliases stay
available through members; the mapping digest binds scope, classes,
justifications and conflicts, so equal inputs under equal scopes agree.
Conflicts are first-class results, not failures: a differentFrom pair
landing in one class yields an InequalityConflict with the merge path and
the violated link. Functional conflicts likewise report the two values and
state that equality is not inferred — generation beyond explicit links is
out of scope, and key/cardinality checks beyond single-valued functionality
belong to RE-23.
Removal, scope and time
Because no state persists between calls, removing support can only split
classes on the next rebuild; insertion-only union-find reuse is explicitly
not implemented. Each mapping is valid for exactly the evaluated snapshot:
callers pass an opaque EqualityScope (a pinned-context identity) and must
rebuild after any change; temporal scoping of representatives is revisited in
RE-20, which may rebuild at change points instead of sharing representatives
globally. Recursive equality interaction with other strata is evaluated
against the same input horizon, never against a cached older mapping.
Measured cost
Canonicalizing 100,000 entities chained by 99,999 links completes in about
0.6s single-threaded with bounded memory (parent/size/evidence tables sized by
distinct entities); see the roadmap register for the frozen reference. The
bound max_entities fails closed instead of degrading, and cooperative
cancel is honored between union batches.