kitt_score 0.1.0

Decision engine at the core of Project KITT — in-memory stateful matching with pluggable scoring backends.
Documentation
# kitt_score

The decision engine at the core of **Project KITT** — a real-time, in-memory
matcher that picks the highest-scoring action at a location, given that
location's accumulated state.

The crate is transport-agnostic: no networking, no persistence, no renderer.
The host service (typically axum + tokio) owns I/O; see
[`examples/axum_host.rs`](examples/axum_host.rs) for the intended integration
pattern.

## Model

- A **Location** is an abstract entity (a screen, a session, a device — your
  choice) carrying reference data loaded at startup and dynamic state updated
  by events.
- Events come in three flavors: `StateUpdate`, `ActionIngest`, `Trigger`.
- An **Action** binds a string-backed `ActionId` + a scoring function + a
  generic payload `T` + a start/end validity window at a location.
- `Engine<T>` receives events and, on a `Trigger`, selects the
  highest-scoring valid action and returns its payload.

Two scoring backends ship today:

- **Predicate DSL** — a bytecode VM over typed slot reads
  (e.g. `$audience.age > 18 AND $audience.country == "CH"`).
- **Linear SIMD vector similarity** — dot or cosine against a per-location
  embedding, with runtime AVX2/AVX-512/NEON dispatch via `pulp`.

An HNSW ANN backend was scoped and deferred; the `Scorer` trait keeps the
door open for re-introduction when per-location action fan-out grows past
the linear-scan sweet spot.

## Quick start

```rust
use kitt_score::{
    ActionId, Engine, Ingested, LocId, SchemaBuilder, ScorerSpec,
};
use kitt_score::event::{AttrSet, KindRef, ActionIngest, StateUpdate, Trigger};
use kitt_score::location::LocationDef;
use kitt_score::schema::attr::{AttrType, Value};
use smallvec::smallvec;

// 1. Declare the schema once; it's immutable after build.
let mut b = SchemaBuilder::new();
let audience = b.kind(
    "audience",
    &[("male_frac", AttrType::F32), ("young_frac", AttrType::F32)],
);
let schema = b.build();
let aid_male = schema.attr("male_frac").unwrap();
let aid_young = schema.attr("young_frac").unwrap();

// 2. Build the engine. Payload type is whatever you want returned on a win.
let engine: Engine<String> = Engine::builder().schema(schema).build().unwrap();

// 3. Upsert a location.
let loc = LocId(42);
engine.upsert_location(&LocationDef {
    id: loc,
    kinds_allowed: vec![audience],
    ref_attrs: vec![],
}).unwrap();

// 4. Register an action — a scorer + payload, valid in [start, end).
engine.ingest_action(ActionIngest {
    location: loc,
    action_id: ActionId::from("https://www.adserver/mycreative.mp4"),
    start: 0,
    end: i64::MAX,
    priority: 0,
    kind: KindRef::Id(audience),
    scorer: ScorerSpec::Predicate("$audience.male_frac * $audience.young_frac"),
    payload: "https://www.adserver/mycreative.mp4".to_owned(),
    post: None,
});

// 5. Push state updates as they arrive from the outside world.
engine.ingest_update(StateUpdate {
    location: loc,
    kind: KindRef::Id(audience),
    attrs: AttrSet { entries: smallvec![
        (aid_male,  Value::F32(0.8)),
        (aid_young, Value::F32(0.9)),
    ]},
});

// 6. Fire a trigger and get the winning payload.
match engine.ingest_trigger(Trigger {
    location: loc,
    kind: KindRef::Id(audience),
    attrs: AttrSet::new(),
}) {
    Ingested::Decided(outcome) => println!("play {}", outcome.payload),
    Ingested::NoWinner => println!("no valid action"),
    _ => unreachable!(),
}
```

## Performance

Design target: **< 1 ms p99** per `ingest_trigger` on 10⁶ locations with
10–100 actions per location. Benchmarks in `benches/` (run with
`cargo bench`) exercise the predicate VM, the linear dot/cosine kernels,
and end-to-end trigger throughput. See the design spec §9 for the full
performance model.

## Observability

`Engine::metrics()` returns a `MetricsSnapshot` with trigger/register/update
counters and an HDR histogram of decide-latency percentiles. Enable the
`serde` feature to serialise it directly as JSON.

## Concurrency

The engine is `Send + Sync`. Per-location updates are serialised through a
short-critical-section `parking_lot::Mutex`; reads across locations are
shard-parallel. Bulk reference-data reloads will use `ArcSwap` for tear-free
publishing (plumbing arriving post-v0.1.0). Core invariants are
model-checked under `loom` — run:

```bash
RUSTFLAGS="--cfg loom" cargo test --test loom_concurrency --release
```

## Documentation

- Design spec: [`docs/superpowers/specs/2026-04-20-kitt-score-design.md`]docs/superpowers/specs/2026-04-20-kitt-score-design.md
- Implementation plan: [`docs/superpowers/plans/2026-04-20-kitt-score-implementation.md`]docs/superpowers/plans/2026-04-20-kitt-score-implementation.md
- Host integration example: [`examples/axum_host.rs`]examples/axum_host.rs

## MSRV

Rust 1.75.

## License

Dual-licensed under MIT or Apache-2.0.