ADR 0016: Entity-type state-space taxonomy¶
Status: Accepted
Date: 2026-06-26
Deciders: Scott, Development Team
Backlog: "[ARCH] Generalize the EntityEvent State Space Taxonomy" (Epic 1)
Context¶
The correlator emits EntityEvents identified only by a flat species_code
string. Epic 1 (the cognitive holarchy) needs a richer, semantic state space —
Animal → (Bird, Critter), Human → (Known, Unknown), Plant — that most
downstream work depends on. The ASR asks for an entity_type that replaces
the flat string, a Pydantic EntityEvent model, a data-driven taxonomy file,
backward-compatible MQTT, and a migration.
Two repo realities shape the design:
- The Reversibility Contract (the studio stacks many commits and rolls the
whole branch back to bare
main): schema must be additive, config defaulted, new behavior off-by-default, MQTT additive; the previous binary must read the new DB untouched. - There is already a cross-classifier identity system —
TaxonomyRef(ioc / audioset / ebird), thespecies.pypredicates (is_corvidae), andtaxonomy_bridge._CLADE_BRIDGES(AudioSet mid → clade predicate). A second, parallel identity authority would be a serious smell.
Decision¶
entity_type is an additive, derived, nullable projection of the existing
identity — not a replacement, and not a new authority.
- Registry, not enum. The canonical
entity_typeis a dotted string ("Animal.Bird.Crow") validated against a tree loaded fromdetection/data/entity_taxonomy.yaml(entity_taxonomy.EntityTaxonomy). Data-driven so the taxonomy extends by editing data, not code — and so it survives Python 3.9 (nomatch). Mirrors theKNOWN_NAMESPACESfrozenset-registry precedent. Internal nodes are valid types too: a detection may resolve only toAnimal.Bird(coarse) — there is no_selfsentinel, sotopic_forstays injective. - Derivation reuses the existing seams.
derive_entity_typeresolves through a singlepredicate → leaftable: the IOC path runs the predicate on the scientific name; the AudioSet path maps a mid → its predicate via_CLADE_BRIDGES(the single source of the mid list) → the same leaf. Then a specificlegacy_species_codesmap, then a coarsedetection_typedefault. No mid list is copied into the YAML. The per-evidenceTaxonomyRefremains the source of truth;entity_typeis documented "not authoritative." - Entity-level rule: the display pick. An entity's
entity_typederives from the same highest-confidence "display" observation that setsspecies_code, so the two are always consistent (rather than a "most-specific wins" rule that could contradict the displayed species). EntityEventmodel (events.py) is a standaloneBaseModelmirroring the correlator's emit dict (NOT anOrpheusBaseEventsubclass, whoseevent_id/event_timestamp/model-contextwould collide). The correlator builds through it, so the model is the single source of the wire shape (the emit dict is its additive superset), not a dead parallel type.- Additive persistence.
entities.entity_typeis a recipe-31 additive nullable column;species TEXT NOT NULLis untouched. Old binary ignores the column; new binary migrates a legacy DB and reads pre-migration rows asNone. - Backward-compatible topics.
orpheus/entities/animalis always published. Behindcorrelation.publish_entity_type_topics(defaultfalse) the correlator also publishes on theentity_type-routed topic (orpheus/entities/animal/bird/crow). The legacy topic is never removed, so the compat requirement holds unconditionally. - Migration.
backfill_entity_types(+make backfill-entity-types) populatesentity_typewhere NULL using the samederive_entity_typeas live emission, idempotent and streamed (mirrors theroot_event_idbackfill).
Consequences¶
- Adding a type is a YAML edit; adding a clade is one predicate in
species.py+ two YAML lines (AudioSet mids stay single-sourced).Human.*,Plant,Animal.Critterare declared but unreachable until a detector emits them — intentional (the state space is the point). entity_typeis coarse and lossy for multi-species clusters (it mirrors the existingspecies_codedisplay pick). The full picture stays in per-evidence taxonomy. If a consumer later needs all of a cluster's types, add an additiveentity_types: list— don't pre-build it.- Whole change is additive + off-by-default; reverting any commit leaves a green
tree. The deferred
species-flat-string removal the ASR imagined is explicitly NOT done (it would break reversibility). - Replay topic-routing is a follow-up (replay keeps the legacy topic, which now
carries
entity_type).