Use contextual descriptors for entity reconciliation

This commit is contained in:
2026-08-08 15:05:35 +00:00
parent 51d62de1f3
commit fc449863f2
18 changed files with 435 additions and 100 deletions

View File

@@ -6,15 +6,16 @@ import (
)
// ProposalResponse is the private structured response exchanged with the
// reconciliation prompt. It identifies candidates only by opaque keys.
// reconciliation prompt. It identifies candidates by contextual selectors.
type ProposalResponse struct {
DuplicateGroups []DuplicateGroup `json:"duplicate_groups"`
}
// DuplicateGroup proposes candidate keys that might denote one entity.
// DuplicateGroup proposes contextual candidate descriptors that might denote
// one entity.
type DuplicateGroup struct {
Members []string `json:"members"`
Canonical string `json:"canonical"`
Members []Selector `json:"members"`
Canonical Selector `json:"canonical"`
}
// Issue identifies one unsafe proposal category without prescribing a warning
@@ -59,16 +60,13 @@ func (a Assessment) DiscardedGroups() int { return a.discardedGroups }
// Issues returns the deterministic rejection categories in proposal order.
func (a Assessment) Issues() []Issue { return append([]Issue(nil), a.issues...) }
// Assess validates a proposal against the opaque keys created by BuildContext.
// Assess resolves contextual descriptors to internal candidate keys, then
// validates the proposal without exposing those keys to the model.
func (m Materials) Assess(response ProposalResponse) Assessment {
all := make(map[string]struct{}, len(m.candidateKeys))
for _, key := range m.candidateKeys {
all[key] = struct{}{}
}
groups := make([]assessedGroup, len(response.DuplicateGroups))
issues := make([]Issue, 0)
for groupIndex, proposal := range response.DuplicateGroups {
groups[groupIndex] = assessGroup(proposal, all, m.eligible)
groups[groupIndex] = m.assessGroup(proposal)
for _, category := range groups[groupIndex].issues {
issues = append(issues, Issue{GroupIndex: groupIndex, Category: category})
}
@@ -115,12 +113,13 @@ type assessedGroup struct {
conflicting bool
}
func assessGroup(proposal DuplicateGroup, all, eligible map[string]struct{}) assessedGroup {
func (m Materials) assessGroup(proposal DuplicateGroup) assessedGroup {
issues := make([]string, 0)
members := make([]string, 0, len(proposal.Members))
seen := make(map[string]struct{}, len(proposal.Members))
for _, key := range proposal.Members {
if category := keyCategory(key, all, eligible); category != "" {
for _, selector := range proposal.Members {
key, category := m.selectorKey(selector)
if category != "" {
issues = append(issues, "member_"+category)
continue
}
@@ -131,31 +130,39 @@ func assessGroup(proposal DuplicateGroup, all, eligible map[string]struct{}) ass
seen[key] = struct{}{}
members = append(members, key)
}
canonicalCategory := keyCategory(proposal.Canonical, all, eligible)
canonical, canonicalCategory := m.selectorKey(proposal.Canonical)
if canonicalCategory != "" {
issues = append(issues, "canonical_"+canonicalCategory)
}
if len(members) < 2 {
issues = append(issues, "fewer_than_two_members")
}
if canonicalCategory == "" && !contains(members, proposal.Canonical) {
if canonicalCategory == "" && !contains(members, canonical) {
issues = append(issues, "canonical_not_member")
}
sort.Strings(members)
return assessedGroup{members: members, canonical: proposal.Canonical, issues: issues, locallyValid: len(issues) == 0}
return assessedGroup{members: members, canonical: canonical, issues: issues, locallyValid: len(issues) == 0}
}
func keyCategory(key string, all, eligible map[string]struct{}) string {
if strings.TrimSpace(key) == "" {
return "blank"
func (m Materials) selectorKey(selector Selector) (string, string) {
if strings.TrimSpace(selector.Name) == "" {
return "", "blank"
}
if _, ok := all[key]; !ok {
return "unknown"
lookupKey, err := selectorLookupKey(selector)
if err != nil {
return "", "unknown"
}
if _, ok := eligible[key]; !ok {
return "ineligible"
if _, collided := m.collidedSelectors[lookupKey]; collided {
return "", "ineligible"
}
return ""
key, ok := m.keyBySelector[lookupKey]
if !ok {
return "", "unknown"
}
if _, eligible := m.eligible[key]; !eligible {
return "", "ineligible"
}
return key, ""
}
func contains(values []string, want string) bool {