horon-engine

Semantic Disk: taxonomy-embedded meaning space

Status: shipped. Companions: SEMANTIC_INDEX.md (supplies the metric-pluggable tree and the epoch invalidation model), ../horon/docs/TEMPORAL_EPOCHS.md ( supplies the trajectory record this design turns into symbolic sequences).

Problem

The engine has two query worlds with asymmetric machinery. The structural world (storage hierarchy → Poincaré disk via Sarkar) gets the full stack: Sarkar placement and an exact cell index over the disk. The semantic world (flat coordinate axes, dims 16+) got Euclidean space and, with the semantic index, an O(log n) VP-tree. But semantic axes usually carry a hidden hierarchy too — a course catalog's 17 category dims are not unrelated numbers, they are a concept taxonomy (trauma and EMDR are kin; trauma and werk_coaching are not). Flat Euclidean distance is blind to that kinship.

The semantic disk embeds the domain's concept taxonomy into a second Poincaré disk and gives every data node a position in it, so "near in meaning" becomes hyperbolic proximity in a space whose geometry matches the domain's actual shape — same engine, two disks.

Ratified design decisions

Three forks were resolved in design review (2026-07-11):

  1. Taxonomy source: derived from structure within the data. The concept tree arrives with the data — the category tree already present in the paths (/catalog/course/{category}/…), a directory tree, or any tree the caller names. No new declaration machinery; anchors are few (dozens, not thousands). The caller supplies the concept paths plus the mapping from each concept to the affinity dimension that weights it — this mapping is calibration, fixed for a file's life like the dimensional schema.

  2. Movement is first-class; no residual-offset model. A node's position in the concept disk travels freely as its affinity dims change. The trajectory itself is the information carrier: one moving point encodes what would otherwise be thousands of static observations. Whatever determines concept position must therefore be captured by the epoch record — a second, separately-maintained position history is forbidden.

  3. Coexistence via derivation ("E+D"). Evaluated alternatives: replacement mode (killed: operational dims like occupancy have no home in a taxonomy, and arbitrary-slice queries must survive); stored parallel layer (killed: new WAL op + format version + a divergence bug class — stored position vs. dims disagreeing with no arbiter); reserved-band storage (viable, kept as a documented later extension for hypothetical hand-placement domains — NOT built now); derived positions (E): concept position is a pure deterministic function of the affinity dims — nothing new is stored anywhere; anchor power-diagram (D): the taxonomy itself is the index.

Architecture

SemanticDisk (standalone object; Store is untouched)
 ├─ taxonomy: private Store holding ONLY the concept tree
 │    → Sarkar embedding, power diagram, O(1) grid — all reused verbatim
 ├─ mapping: Vec<(concept_path, affinity_dim)>       // the calibration
 └─ cache: epoch-tagged MetricVpTree<HyperbolicPoint>  // exact NN layer

Derived position (E)

For a node with affinity weights w_i (read from its mapped dims, negatives clamped to zero):

position = WeightedKleinBarycenter( anchor_i, w_i )
         = klein_to_poincare( Σ w_i·γ_i·k_i / Σ w_i·γ_i ),  γ_i = 1/√(1−|k_i|²)

the weighted Einstein midpoint of the anchor sites in Klein coordinates — the standard hyperbolic barycenter, computed in Q64.64 fixed point (deterministic; verified against the gyro-midpoint for the two-point equal-weight case). All-zero weights → the node has no concept position and is excluded from concept queries (same convention as empty semantic coords).

Consequences, by construction:

Anchor power-diagram (D)

The taxonomy is a real tree, so it is embedded by inserting its concept paths into a private Store — Sarkar placement and the Klein conversions come along unchanged. Classification is the exact Nielsen reduction over the anchor sites, argmin_i (1 - <x, k_i>) * gamma_i, evaluated directly; there is no precomputed cell structure. Because the sites are the anchors (dozens), not the data nodes (unbounded):

Exact NN layer

nearest (k nearest data nodes in the concept disk) uses the semantic index's MetricVpTree with the hyperbolic metric (the Metric trait earns its "pluggable" adjective): build over all derived positions, cache tagged with the store's semantic epoch, rebuild lazily on invalidation — the index's model verbatim. O(log n) expected, results identical to a brute-force hyperbolic scan (property-tested).

Honest scope note: v1 ships O(1) classification (the anchor grid) and O(log n) NN (the metric tree). Bucketing data nodes per anchor cell to give NN the O(1) grid entry as well is a benchmark-driven follow-up — it changes constants, not results.

API (v1)

let disk = SemanticDisk::build(&store_dimension_agnostic_spec)?;
//   spec: &[(&str, usize)] — (concept path, affinity dim), e.g.
//   [("/trauma", 16), ("/cgt", 17), … ] — nested paths allowed and
//   embedded with their real tree shape.

disk.position_of(&store, key)?           // derived Poincaré coords
disk.concept_of(&store, key)?            // O(1): which concept, right now
disk.nearest(&store, key, k)?            // k nearest in meaning-space
disk.nearest_to_weights(&store, &w, k)?  // query by explicit weights
disk.classify_trajectory(start_dim, &samples)
//   samples: the (epoch, coords) pairs HttHistory::trajectory returns →
//   Vec<(epoch, concept_path)> — the symbolic trajectory.

SemanticDisk is a standalone object the application owns (built per calibration, cheap: anchors are few). Store gains no new state, no new locks — minimal-diff, and the disk's lifetime maps naturally onto the punctuated calibrate→seal→query model.

Determinism

Anchor insertion order is the sorted concept-path order; the barycenter is fixed-point arithmetic; classification and NN inherit the index's (distance, key) total order. Identical stores + identical spec → byte-identical answers. The (spec, mapping) pair is part of a file's calibration: changing it mid-epoch-series is a schema-epoch boundary, the same discipline as the dimensional schema itself.

Out of scope (documented, not built)

Verification