# 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.