Files
zeekayandHanzo Dev 1cd0cd7644 semantic: the pipeline, end to end
The first commit was the shape. This is the implementation: every stage of the
pipeline, the graph and its measures, the type system over it, entailment,
deduplication and conflict resolution, the decision layer, the stores and the
export formats. Twenty-six thousand lines of Go and sixteen thousand of tests,
against no dependency but the standard library.

Where the Python reaches for torch, spacy or a database driver, the Go states
an interface and reports what is missing rather than pretending. A caller can
tell "this build cannot serve that" from "nobody has heard of it", which is the
difference between a port and a stub. The deterministic paths are complete and
are what the tests exercise; the model-shaped ones are one adapter away.

Porting a bug is not parity, so several places now disagree with the original
on purpose. The JSON repair that closed a truncated reply counted each bracket
kind separately and closed them backwards on the one input it exists for; it
now works off a stack and ignores brackets inside strings. The entity patterns
joined words with \s, so a heading ran into the sentence beneath it and became
one name while the real relation was lost; a name is separated by horizontal
space now. Eigenvector centrality ran plain power iteration, which on a
bipartite graph swings between the two ends of a sign-paired eigenvalue forever
and reported every node in a star as equally central; it iterates with A+I,
which moves no eigenvector and settles. Similarity summed its weighted parts in
map order, and float addition is not associative, so the same comparison scored
differently between runs and duplicate detection returned a different answer
each time. An inferred ontology named object-property endpoints by raw source
type and data-property endpoints by class, so the schema referred to classes it
did not contain. A property with no IRI of its own was minted as a class and
came back Pascal-cased, renaming the term on the way out of the document.

The command built a pipeline with every stage nil and panicked on each
invocation. It is now the composition the package documents, and Run reports a
missing ingester instead of dereferencing it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 12:57:59 -07:00

128 lines
4.2 KiB
Go

// Package dedupe decides which records are the same thing and which
// disagree.
//
// Both questions start from the same comparison, so they live together. When
// two records agree they merge, and the merge keeps the records it was made
// from. When they disagree the disagreement is a Conflict: a value the caller
// can read, rank and settle. Nothing is dropped for being unresolvable — an
// unsettled Conflict is a result, not an error.
//
// Work that is quadratic in the input takes a context and can be cancelled;
// linear work does not, so the common calls stay short.
package dedupe
import (
"errors"
"reflect"
"strings"
"time"
"github.com/hanzoai/semantic"
)
// ErrOption reports an option outside its allowed range.
var ErrOption = errors.New("bad option")
// ErrEmpty reports a merge with nothing to merge.
var ErrEmpty = errors.New("no entities")
// Source is the document a record came from and how far it is to be trusted.
// Every value in this package carries one, because a claim without the
// document that made it cannot be ranked against a claim that disagrees.
type Source struct {
Doc string // the document
Page int // page within it; 0 when unpaged
Span string // section or heading within the page
At time.Time // when the document stated it; zero when unstated
Score float64 // the document's own confidence, 0 to 1; 0 means unstated
}
// sure is the confidence a source states, or half where it states none —
// the benefit of the doubt an unrated document gets.
func sure(s Source) float64 {
if s.Score > 0 {
return s.Score
}
return 0.5
}
// Entity is one record about one thing, as a single source stated it. The
// same thing described by two documents is two Entity values sharing an ID
// and differing somewhere else — which is what a Conflict is made of.
type Entity struct {
ID string
Name string
Kind string // what it is: Person, Company, Place
Props map[string]any
Edges []semantic.Triple
Vector []float32
Score float64 // confidence in this record; 0 means unstated
From Source // the document it came from
Meta map[string]any
}
// Value returns the property named by prop. Props are searched first, then
// the named fields, so "name" and "kind" read as properties even though
// they have their own place on the struct.
func (e Entity) Value(prop string) (any, bool) {
if v, ok := e.Props[prop]; ok {
return v, true
}
switch strings.ToLower(prop) {
case "name":
return e.Name, e.Name != ""
case "kind", "type", "class":
return e.Kind, e.Kind != ""
}
return nil, false
}
// key identifies an entity across calls. IDs are the identity; a record with
// no ID falls back to its name, which is the only other thing it has.
func (e Entity) key() string {
if e.ID != "" {
return e.ID
}
return fold(e.Name)
}
// size counts how much an entity carries, which is how "most complete" is
// decided.
func (e Entity) size() int { return len(e.Props) + len(e.Edges) }
// Canon folds a triple to the form comparisons are made on: the predicate
// mapped through a synonym table, and, when Fold is set, the predicate and
// object reduced to their words — lower case, single spaces, underscores and
// hyphens read as spaces, so works_at and "Works At" are one relation. The
// zero value folds nothing but case in the predicate.
type Canon struct {
Synonyms map[string]string
Fold bool
}
// Key returns the triple as it is compared: subject, canonical predicate,
// canonical object.
func (c Canon) Key(t semantic.Triple) [3]string {
p := strings.ToLower(t.Predicate)
if c.Fold {
p = strings.Join(strings.FieldsFunc(p, split), " ")
}
if s, ok := c.Synonyms[p]; ok {
p = strings.ToLower(s)
}
o := t.Object
if c.Fold {
o = fold(o)
}
return [3]string{t.Subject, p, o}
}
// fold reduces a string to the form names are compared in: lower case, with
// leading, trailing and repeated whitespace gone. Two spellings that fold
// alike are one name.
func fold(s string) string { return strings.ToLower(strings.Join(strings.Fields(s), " ")) }
// same reports whether two property values are equal. It uses reflection so
// that maps and slices compare by content rather than panicking.
func same(a, b any) bool { return reflect.DeepEqual(a, b) }