# identity — which of your records name the same thing. # # This module supplies the REASONING. You supply the facts: the records you # hold, written as `identity.Record`, and what your sources say about them — # that two records name the same thing (`identity.Same`), might # (`identity.Possibly`), or do not (`identity.Different`). Import this module # and the standing rules and the queries below apply to whatever you have # loaded. # # import identity # # Nothing here merges one record into another or rewrites either. Every # statement stays a claim with the source that made it, and what the rules # conclude from them withdraws itself when a statement it rests on is forgotten. # Two records being the same entity is a conclusion you can read and ask about, # never a change to the records. class Record: # What the record is called, for reading. The rules below do not need it: # they take every name a statement uses as a record, recorded or not. So a # misspelt name in a statement does not fail; it becomes a record of its # own, and `unrecorded()` below is how you find one. name: string class Same: # A source says records `a` and `b` name the same thing. The statement's own # name is yours to choose; it only keeps two statements apart. a: string b: string class Possibly: # A source says records `a` and `b` might name the same thing. Kept apart # from `Same` on purpose: a possibility is not closed over, so two # possibilities in a row do not make a third. a: string b: string class Different: # A source says records `a` and `b` do NOT name the same thing. a: string b: string # ── Standing rules ─────────────────────────────────────────────────────────── # # A rule holds continuously: its conclusion is in force exactly while its # condition holds, and withdraws itself when the condition stops holding. You # never write these; the engine maintains them. # # `SameEntity` and `PossiblySame` are RELATIONS: `SameEntity.x.y == True` says # records `x` and `y` name the same thing. A relation a module concludes is not # namespaced by the module, so your own rules read it by its bare name, as # `SameEntity.p.q`, whatever kinds `p` and `q` are — which is how a rule of # yours carries a fact from one record to another (the Reference shows how). @rule def same_stated(): for s in Same, x in Record, y in Record: if s.a == x and s.b == y: SameEntity.x.y = True @rule def same_stated_back(): # The same statement read the other way round: `Same` is symmetric, so a # source that names `a` first has not said anything `b` first would not. for s in Same, x in Record, y in Record: if s.a == x and s.b == y: SameEntity.y.x = True @rule def same_further(): # The same thing as the same thing is the same thing. This is what makes # `SameEntity` a closure: a record reaches every record any chain of `Same` # statements connects it to, and itself once it is in any chain. for x in Record, y in Record, z in Record: if SameEntity.x.y == True and SameEntity.y.z == True: SameEntity.x.z = True @rule def possibly_stated(): for p in Possibly, x in Record, y in Record: if p.a == x and p.b == y: PossiblySame.x.y = True @rule def possibly_stated_back(): for p in Possibly, x in Record, y in Record: if p.a == x and p.b == y: PossiblySame.y.x = True @rule def contradicted(): # A `Different` statement about two records that other statements, taken # together, make the same. Neither side is withdrawn: the disagreement is # the finding, and which source to believe is yours to decide. for d in Different, x in Record, y in Record: if d.a == x and d.b == y and SameEntity.x.y == True: d.contradicted = True @rule def contested_a(): for d in Different, x in Record: if d.contradicted == True and d.a == x: x.contested = True @rule def contested_b(): for d in Different, x in Record: if d.contradicted == True and d.b == x: x.contested = True # ── Queries ────────────────────────────────────────────────────────────────── def same_as(r: string) -> list: # Every other record that names the same thing as `r`. out = [] names = list_attributes(SameEntity.r) for n in names: if n != r: out = out + [n] return out def possibly_same_as(r: string) -> list: # Every record a source says might name the same thing as `r`. Only what # was stated, in either direction: a possibility is not closed over. out = [] names = list_attributes(PossiblySame.r) for n in names: if n != r: out = out + [n] return out def contested() -> list: # Every record named by a `Different` statement that other statements # contradict. Starts from the conclusion, so it costs what it returns. out = [] rs = subjects(Record.contested, True) for r in rs: out = out + [r] return out def contradictions() -> list: # Every `Different` statement that other statements contradict, by the # name you gave it. out = [] ds = subjects(Different.contradicted, True) for d in ds: out = out + [d] return out def unrecorded() -> list: # Every name a statement uses that no record carries — usually a # misspelling. It reads every value a statement's halves hold now: a half # two sources disagree on is read for both values, and a half revised with # `update` for the value that replaced the old one. names = [] same_a = list_values(Same.a) for c in same_a: names = names + [str(c.value)] same_b = list_values(Same.b) for c in same_b: names = names + [str(c.value)] possibly_a = list_values(Possibly.a) for c in possibly_a: names = names + [str(c.value)] possibly_b = list_values(Possibly.b) for c in possibly_b: names = names + [str(c.value)] different_a = list_values(Different.a) for c in different_a: names = names + [str(c.value)] different_b = list_values(Different.b) for c in different_b: names = names + [str(c.value)] out = [] for n in names: r = current(Record.n.name) match r: case active_claim: pass case empty: if n not in out: out = out + [n] return sorted(out)