Skip to content

Indra Relationships

Status Ratified 2026-09-01 after team review round 1. Decisions logged as D16-D21; B2 Claim change landed in interfaces.md; invariant 9 and the validity-anchor correction landed in architecture.md; requirement E5 added to the PRD. Frozen 2026-09-03 (D28): edits go to the canonical docs, this file is history
Created 2026-09-01
Updated 2026-09-01 (v2: relations are claims with a target, per-type attribute schemas with validity intervals)
Context PRD, Architecture. Diagrams: Excalidraw board a8d17080-29dd-47c2-98d7-b1fcaee9c0ab, sections "RELATION CLAIMS - WALKTHROUGH", "CORRECTION 2026-09-01", "TEAM REVIEW R1"

Summary

The current design has exactly one edge type, EntityLink (entity to partial entity). Nothing connects two entities, so profiles, intro paths, and matchmaking are unanswerable. The fix adds no new concept at all: a relation is a Claim with a target, restoring the board's original target_partial_entity hook. Relations occupy a quadrant of the existing 2x2 classification (queryable and structured, non-identifying, temporal). Entity-level relationships are never stored; they are a query-time traversal through member links. The belief model, the resolution judge, re-extraction supersession, and provenance are written once, for all claims, instead of twice.

The gap

Today a relational statement ("John, our partner at Acme, wants to invest 50M") becomes a soft claim on one partial: text, not structure. It cannot be queried and cannot be navigated. Turning that sentence into structure requires an LLM; there is no deterministic parse path. That is a recorded design choice: the extractor's LLM pass is what converts relational statements into typed, target-referenced claims. Relation extraction therefore inherits everything LLM stages already have: extractor_version, eval datasets, cost caps (C1/C2), re-runnability.

The model

One claim shape

Claim {
  id, type,
  value?,               # value claims (email, alias, ...)
  target_partial_id?,   # relation claims: the object partial, same event
  snippet,
  attributes?,          # per-type schema from the registry (see below)
}

A relation is a claim whose registry type declares a target. The subject is the partial that carries the claim; the object is target_partial_id, a partial of the same event (true by construction: B2 output is per event, no invariant needed). Endpoints are partial ids, never names, so relationship resolution rides entity resolution: Orbit's raw-name re-matching problem disappears structurally.

Registry additions

For each relation type the registry declares, in addition to the standard dimensions:

  • endpoint_kinds: (subject_kind, object_kind)
  • directional: bool (symmetric types like partnership store subject/object as extracted and normalize at query time)
  • attributes_schema (below)

Relation types classify on the existing dimensions as: queryable (structured), non-identifying, temporal. They behave like nationality everywhere the classification is consulted, including invariant 2 (a relation claim is a hard non-identifying claim, so whether it can satisfy "at least one hard claim" follows from the registry, same as nationality) and matching (below).

v1 types: employment, investment, partnership, governance, plus other. social and service are excluded: noisy or not useful (evidence: Orbit's production experience, see Alternatives).

other is bounded, D8 stands. A relational statement fitting none of the four types is emitted as type: other with the raw label. The label is stored and never queried by label: no search adapter, no registry semantics, no traversal by raw label. This keeps D8 (semi-hard claims out of v1) active rather than superseded; the pair structure is preserved for registry v2 re-typing (a version bump, no re-extraction), and M1 measures what the corpus contains.

Attributes: per-type schemas with validity intervals

Attributes are typed per relation type in the registry, not snippet-only and not a free dict. The reason is time: the event's occurred_at is the wrong anchor for relations. "John worked at Acme until 2018", said in a 2026 meeting, has reported_at 2026 (the event, via lineage) and a validity interval ending 2018, and only the validity interval drives belief. Every temporal relation type gets nullable valid_from / valid_to, filled only when the source states them; when absent, reported_at is the only anchor available.

Thin v1 schemas:

employment  { role?, valid_from?, valid_to? }
investment  { amount?, currency?, round?, date? }
governance  { role?, valid_from?, valid_to? }
partnership { valid_from?, valid_to? }

Active vs latent: derived, never stored

A relation claim is active iff its subject partial and its target partial both hold an active member link to a canonical entity, latent otherwise. No status column, no state machine: usability is computed from endpoint link state. The moment an unresolved endpoint is promoted, the relation becomes traversable with zero writes. Same rule as claims generally: non-canonical means the information exists but is not accessible.

This is the one genuinely new invariant (proposed as invariant 9):

Relationship usability is derived: active iff every endpoint partial holds an active member link to a canonical entity. No stored relationship state exists at entity level.

(Earlier proposed invariants 9 and 10 are withdrawn: same-event endpoints hold by construction of B2; the non-identifying classification is a registry attribute, not an invariant.)

Projection

"John Connor works at Acme Capital" is a traversal: canonical -> member link -> relation claim -> member link -> canonical. Computed at query time with org-scoped SQL joins; nothing materialized in v1.

Walkthrough

Event e1 (meeting recording plus its calendar event): "John, our partner at Acme, wants to invest 50M in cybersecurity next year."

  1. Extraction (module 2, one pass) emits pe-1 "John" with claims email(john@acme.com), soft invest intent, and employment { target_partial_id: pe-2, snippet }; and pe-2 "Acme" with domain(acme.com), alias("Acme Capital").
  2. Normalization: the type and attributes validate against the registry (as E2 does today); no relation value enters the hard-claim index.
  3. Matching (module 3): unchanged. Identifying claims route; relation claims are non-identifying and behave exactly like nationality: they narrow candidates, never route a match (settled in team review: this is v1 behavior, not post-M1).
  4. Applier (module 4): resolves pe-1 to canonical John Connor (email hit), mints unresolved "Acme" for pe-2. No applier logic touches the relation claim.
  5. Query time: one endpoint is not canonical, so the employment claim is latent: stored, invisible. When Acme is promoted, the same query returns the relationship with provenance claim -> e1 -> sources.

Hard cases already solved by existing invariants, with zero new machinery:

Scenario Behavior
Calendar reschedule (D11) Re-extraction diff supersedes the stale claim; the projection stops returning it
Un-merge of John Connor Member link superseded, projection recomputes from remaining active links
pe-2 later merges into canonical Acme Capital The relationship re-points automatically; it was always a traversal
Conflicting statement ("John works at BetaCo") Both claims project; see Conflicts

Conflicts and belief

v1 does not rank, count, or supersede relations: every active relation claim projects as-is, contradictions visible with provenance and time anchors. Believing is a consumer concern (profile generation), not a storage concern.

Post-M1, the belief model is heuristics plus LLM judge passes for the abstract cases, applying the 2x2 asymmetric rules already on the board: exclusive temporal types disconfirm only on overlapping anchors, accumulative types never disconfirm. Correction to the board's earlier note: for relations, the anchor in those rules is the validity interval from the attributes, not the event date; occurred_at is reported_at and is the fallback only when no validity is stated. "Works at" is temporal, so two employments for one entity are fine as long as the time slices differ.

Contract impact (requires owner sign-off, per interfaces.md rules)

B2's Claim gains optional fields; no new list, no change to PartialEntity structure:

Claim { id, type, value?, target_partial_id?, snippet, attributes? }

The shared registry gains, per relation type: endpoint_kinds, directional, attributes_schema. B3, B4, B5 do not change.

Decisions settled (design review + team review round 1, 2026-09-01)

  • A relation is a Claim with a target (target_partial_id), not a separate shape or list. Relations are the queryable, non-identifying, temporal quadrant of the existing classification. All claim machinery (supersession, provenance, judge, belief) is written once.
  • Entity-level relationships are query-time traversals through member links, never stored edges. Usability (active/latent) is derived from endpoint canonical-member state: the one new invariant.
  • The LLM is the parser: no deterministic path from text to relation.
  • Registry v1 types: employment, investment, partnership, governance. other is bounded (label stored, never queried by label); D8 stands.
  • Attributes are per-type registry schemas with nullable validity intervals; reported_at (event lineage) and validity are distinct anchors, validity drives belief.
  • Relations act as candidate-narrowing signals in matching from v1, like nationality: never routing a match.
  • One extraction pass; emitted from milestone 3, measured in M1 (yield and precision per type), consumed at milestone 5. Two-pass split is a remedy if M1 shows mutual degradation.
  • Traversal computed at query time in v1; materialization is a later optimization.
  • v1 conflict handling is pure projection with visible contradictions; belief model post-M1 as above.

Open questions (post-M1)

  1. Belief-model parameterization: heuristic thresholds and judge prompts, from measured yield/precision.
  2. Registry v2 types, chosen from measured other labels.
  3. Per-type cardinality declarations (exclusive vs accumulative per relation type), finalized with the belief model.

Alternatives considered

  • Separate relation_claims sibling list in B2 (proposal v1). Superseded by team review: a claim with a target reuses the 2x2, the judge, supersession, and provenance verbatim; a sibling shape would duplicate all four. Symmetric types are handled by a registry directional flag plus query-time normalization, removing the "arbitrary owner" objection.
  • Stored entity-to-entity edges (Orbit's entity_relations + edges). Rejected: a second mutation surface with its own undo, retraction, and re-pointing machinery; every hard case above required an explicit fix in Orbit that the traversal model gets by construction. Orbit lesson kept: derived co-occurrence must never blend with asserted facts.
  • Raw-name relation suggestions with human confirm (Orbit's relation_suggestions). Rejected: extraction before resolution forced name re-matching forever, and per-relation human confirmation is exactly the manual workload this service exists to remove (D10 ladder applies instead).
  • Soft claims only. Rejected: not queryable, not navigable.
  • Snippet-only or free-dict attributes. Rejected by team review: without a validity interval the belief anchor is wrong (the 2018/2026 employment example).
  • Two-pass extraction. Deferred, not rejected.