Skip to content

Decision log

Append-only. One entry per decision. Superseding a decision never edits the old entry's body: flip its Status line to superseded by Dn (date: why) and write the new entry saying what changed. Git provides the versioning; this convention provides the audit trail. Referenced from the PRD.

Status: active | Decided: 2026-09-01 | By: Agus

Entities are views over append-only links to partial entities; merging is linking, unmerging is superseding. Data is never destroyed, only composed.

Status: active | Decided: 2026-09-01 | By: Agus

Member and evidence links are first-class rows (EntityLink); queryable identity comes from active member links only.

D3: Names are hard claims

Status: active | Decided: 2026-09-01 | By: Agus

Names are hard claims; every partial entity carries at least one hard claim.

D4: Two-dimensional claim classification

Status: active | Decided: 2026-09-01 | By: Agus

Claims classify on two dimensions: queryability (hard identifying / hard non-identifying / soft) and temporality (constant / temporal, with exclusive or accumulative cardinality). Mismatch handling is time-aware and asymmetric.

D5: Context embedding in v1

Status: active | Decided: 2026-09-01 | By: Agus

Eager, versioned source and event summaries ship in v1 (evidence in R1).

D6: Resolution judge scope and gating

Status: active | Decided: 2026-09-01 | By: Agus

The resolution judge lives in the entity-resolution phase only; verdicts are promote / supersede / inconclusive; shadow mode until it clears an eval dataset.

D7: Sequential applier per organization

Status: active | Decided: 2026-09-01 | By: Agus

The applier is sequential per organization.

D8: Semi-hard claims out of v1

Status: active | Decided: 2026-09-01 | By: Agus

Semi-hard claim types are out of v1; soft claims are stored, not processed.

D9: Stack

Status: active | Decided: 2026-09-01 | By: Agus

Python 3.12+, FastAPI, pydantic v2, Postgres, SQLAlchemy 2 + Alembic; search in-Postgres for v1 (transactional reindex requirement); LLM calls via OpenRouter. Observability tools: PostHog, Langfuse, BetterStack from the start.

D10: The human-capability ceiling and the review-workload metric

Status: active | Decided: 2026-09-01 | By: Agus

Indra auto-resolves only where a human could disambiguate the reference from the same context; the design's job is to capture that context (D5) and put it in front of the model. What no layer (deterministic, heuristic, stochastic) can decide queues for human review. The success metric is total human workload at fixed precision, not raw queue rate: a prepared queued case costs one accept/deny decision, a wrong merge that escapes costs a full investigation, so precision outweighs queue-minimization. The workload target is parameterized from the M1 report, never guessed.

D11: Changed-source re-ingest semantics

Status: active | Decided: 2026-09-01 | By: Agus

A changed source (same external_id/external_system, new content) recomposes into the same event, never a new one, and forces re-extraction. Reconciliation is diff-based: compare new extraction against prior output, keep what is unchanged, supersede the stale claims and links for what changed. Neither blind append nor blind replace. Claims from a superseded source version stop contributing to critical mass. Driving case: calendar events mutate constantly. Mechanics, including the relation to extractor_version re-runs, are owed by the architecture doc.

D12: Action log and undo scope

Status: active | Decided: 2026-09-01 | By: Agus

Every mutating action (merge, link create/supersede, promotion, bootstrap import, review verdict) is recorded in an action log with actor, timestamp, and inputs, command-pattern style; humans and the system write the same log, distinguished by actor. In v1, resolution actions expose one-step undo and an undo is itself a logged action; ingestion and bootstrap import are re-runnable, not undoable. The supersede model (D1) gives data-level reversibility; the action log is the operation-level record and the entry point for undo.

D13: One LLM interface, provider- and mode-agnostic

Status: active | Decided: 2026-09-01 | By: Agus

All LLM processing is called behind one interface that hides the actual call, so provider (OpenRouter or a specific vendor) can be switched once models are settled. The interface is batch-shaped: sequential calls (testing) and batch calls (production data) are a configuration switch, not a code change. Cost visibility (per-stage metrics, caps, alerts) is P0; production batch processing is P1 and gates any full-corpus run.

D14: No erasure in v1: accepted risk

Status: active | Decided: 2026-09-01 | By: Agus

Right-to-erasure is out of scope for v1 and the storage model does no preparatory work for it (no payload/topology separation, no crypto-shredding). This collides with D1 (data is never destroyed) by design and is accepted as a risk: if an erasure obligation lands, the retrofit is expensive and this decision is the first to revisit.

D15: Build order and milestone shape

Status: active | Decided: 2026-09-01 | By: Agus

Milestones are cut on pipeline stages, not on modules. A module is an ownership and contract boundary and carries its own internal milestones; the PRD tracks only the system gates where a stage joins the one before it and something end to end becomes true. The first wave is the ingestion and extraction stages. The entity store, applier, bootstrap, and matching are deferred as one block behind the M1 report: matching against an empty graph is a no-match generator, so entities must exist first, and the block's internal order is store and applier, then bootstrap import, then matching. Team reality drives the shape: v1 is built by two engineers, not by five module owners, so a milestone list with one module per milestone would leave most of it unstaffed. =======

D16 — Relationships are claims with a target

Status: active | Decided: 2026-09-01 | By: Agus, with team review

A relation is a Claim carrying target_partial_id: a directed edge from the subject partial (the claim's carrier) to another partial of the same event. No separate relation shape or list exists. Relations classify on the existing dimensions as queryable (structured), non-identifying, temporal, so all claim machinery (supersession per D11, provenance, judge, belief) is written once. Entity-level relationships are never stored: they are query-time traversals (canonical, member link, claim, member link, canonical). Usability is derived, the one new invariant: a relation claim is active iff every endpoint partial holds an active member link to a canonical entity; latent otherwise, with zero writes on promotion. Full design: design/relationships.md.

D17 — The LLM is the relation parser

Status: active | Decided: 2026-09-01 | By: Agus, with team review

There is no deterministic path from text to a typed relation. The extractor's LLM pass converts relational statements into claims with a target; without it they stay soft claims (not queryable, not navigable). Relation extraction runs in the same single pass as entity extraction and inherits extractor_version, eval datasets, cost caps, and re-runnability. A two-pass split is a remedy if M1 shows mutual degradation, not a default.

D18 — Relation-type registry v1; other is bounded, D8 stands

Status: active | Decided: 2026-09-01 | By: Agus, with team review

v1 relation types: employment, investment, partnership, governance. social and service are excluded (noisy or not useful in Orbit production). Statements fitting no type are emitted as type: other with the raw label, preserving the pair structure. other is bounded: the label is stored and never queried by label (no search adapter, no traversal by raw label), so D8 (semi-hard claims out of v1) stands, not superseded. Registry v2 types are chosen from measured other labels after M1.

D19 — Per-type relation attributes with validity intervals

Status: active | Decided: 2026-09-01 | By: Agus, with team review

Relation attributes are typed per relation type in the registry, not snippet-only and not a free dict. Reason: the event's occurred_at is the wrong belief anchor for relations ("John worked at Acme until 2018", said in a 2026 meeting, has reported_at 2026 and a validity interval ending 2018; only the validity drives belief). Every temporal relation type gets nullable valid_from/valid_to, filled only when stated; reported_at (event lineage) is the fallback. 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? }.

D20 — Relations are candidate-narrowing signals in matching

Status: active | Decided: 2026-09-01 | By: Agus, with team review

Relation claims behave in matching exactly like nationality: contextual, non-identifying signals that narrow candidates and never route a match. This is v1 behavior, not deferred.

D21 — Relationship timing, traversal, and v1 conflict handling

Status: active | Decided: 2026-09-01 | By: Agus, with team review

Relation claims are emitted from milestone 3 and measured in M1 (yield and precision per type); nothing consumes them until milestone 5. Traversal is computed at query time in v1 (org-scoped SQL joins); materialization is a later optimization that changes no stored model. v1 conflict handling is pure projection: every active relation claim projects as-is with provenance and time anchors, contradictions visible. The post-M1 belief model is heuristics plus LLM judge passes applying the time-aware asymmetric rules (D4), anchored on the validity interval (D19), not the event date.

D22 — Partial entities are typed; classification is per entity kind

Status: active | Decided: 2026-09-03 | By: Agus, team approved

PartialEntity carries a kind. v0 kinds: person | organization; more kinds are a registry/ontology version bump, not a migration. Registry classification (identifying class, and search strategy where it differs) is keyed by (claim type, entity kind): domain is non-identifying for a person and identifying for an organization. Without this, relation-type endpoint_kinds (D16) is unenforceable and organizations rarely reach canonical, leaving relation claims latent (invariant 9). Full argument: design/v0-review.md, A1.

D23 — Groundedness gate on identifying claims

Status: active | Decided: 2026-09-03 | By: Agus, team approved

An identifying claim is rejected at the extraction contract boundary unless its value appears literally in the event's source text (modulo the claim type's canonicalization). Non-identifying and soft claims are exempt: they may be legitimately inferred. Rationale: a hallucinated identifying claim is the worst extractor output, a well-formed email that was never in the text routes a silent member merge. PRD requirement E6. design/v0-review.md, A2.

D24 — Relation claims under changed-source reconciliation

Status: active | Decided: 2026-09-03 | By: Agus, team approved

The reconciliation diff (D11) aligns relation claims across runs by (type, aligned-target partial, attributes), since they carry no value. When a target partial is superseded and an aligned successor exists, a relation claim on an unchanged subject partial is re-issued against the successor and the prior claim superseded (source_changed), in the same reconciliation transaction. design/v0-review.md, A3.

D25 — M1 ground truth and stability metrics

Status: active | Decided: 2026-09-03 | By: Agus, team approved

Precision numbers in M1 require a labeled eval set; its plan (size, sampling, who labels) must exist before the measured corpus is extracted (milestone 3 done-when). M1 additionally reports alignment stability under re-extraction (share of partials that re-align with themselves on an unchanged event) and identifying-claim error rate per source type; the latter decides whether low-trust identifying hits need a trust gate (deferred, D27). design/v0-review.md, A4.

D26 — Organizations first within bootstrap

Status: active | Decided: 2026-09-03 | By: Agus, team approved

Within D15's internal order (store and applier, then bootstrap, then matching), bootstrap imports organizations before people. Relations are usable only with canonical endpoints (invariant 9) and the object of most employment, investment, and governance claims is an organization; canonical orgs are what turn latent relation claims into traversable relationships. design/v0-review.md, B1.

D27 — Four bottlenecks deferred by design

Status: active | Decided: 2026-09-03 | By: Agus, team approved

Named, real, and not v0 work: applier FIFO priority discipline (live resolution vs judge verdicts vs bootstrap imports), judge fan-out caps on common aliases, a materialized relationship-traversal projection, and a trust gate on identifying hits from low-trust sources. Each is recognized on arrival and parameterized from measured data (M1, C1 cost caps); none is built speculatively. Table with arrival conditions and the later fix: design/v0-review.md, section C.

D28: Nine doc types, one place per definition

Status: active | Decided: 2026-09-03 | By: Nacho, with Benve's review

The writing-docs layering gains three types: domain (docs/domain.md, concepts and invariants), scenarios (docs/scenarios/, worked inputs with expected outcomes), and proposals (docs/proposals/, RFC-shaped docs frozen on ratification). Concepts and invariants are defined only in domain.md; every other doc links there. Proposals are linked for history only, never for a definition; a deferred item worked later arrives as its own proposal. Invariant 10 ratified with this entry: adding new evidence never requires re-ingesting the original source. Event vs Episode naming is deferred, not a priority (Nacho, 2026-09-03). Proposal: proposals/docs-structure.md.