aria-core
Generic adaptive sequencing engine. Zero dependencies. Domain-agnostic.
Given a pool of items and a stream of user feedback, aria-core selects the best next item for each user at any point in time — from the very first interaction, with no pre-training required.
Domain is entirely caller-defined. The engine ships no built-in domains. You define items, scoring factors, and optionally state update logic. The same engine works for learning platforms, product recommendation, travel suggestions, content feeds, or any sequencing problem.
Install
[]
= "0.1.0"
Quickstart
use ;
use Item;
use ;
// 1. Build engine
let mut engine = new;
// 2. Register factors — built-ins or your own
engine.add_factor;
engine.add_factor;
engine.add_factor;
// 3. Register items — any domain
engine.add_items.unwrap;
// 4. Suggest
let item = engine.suggest.unwrap;
// 5. Report feedback
engine.feedback.unwrap;
Core Concepts
Items
Items implement the Scoreable trait. Use the built-in Item struct or implement Scoreable on your own type.
score_proxy semantics are caller-defined:
| Domain | score_proxy meaning |
|---|---|
| Learning | Difficulty (0=easy, 1=hard) |
| Ecommerce | Price ratio |
| Travel | Remoteness / adventure level |
| Content | Reading level / complexity |
Factors
Factors implement Factor and return a score in [0.0, 1.0]. The engine multiplies all factor scores — a near-zero from any single factor suppresses the item.
Built-in reference factors:
| Factor | What it does |
|---|---|
ChallengeFactor |
Gaussian centred at skill + optimism_bias. Items too easy or too hard score low. |
SpacingFactor |
Forgetting curve on time since last seen. Recently seen items score low. |
CoverageFactor |
Inverse topic frequency. Under-represented categories score higher. |
Custom factor example:
;
engine.add_factor;
Signal
Feedback reported after each interaction:
new // effort: 0.0=effortless, 1.0=max friction
Semantics are caller-defined. The default StateUpdater derives a performance score:
performance = success × (0.5 + 0.5 × (1 - effort))
skill = skill + alpha × (performance - skill)
ProfileState
User state the engine maintains per user:
Persistence
The engine is in-memory. Persist state with the Serialiser:
use Serialiser;
// Save
let encoded: = encode;
// → store encoded as JSON, in a DB row, cookie, etc.
// Restore on next session
let state = decode.unwrap;
engine.load_state;
Custom StateUpdater
Override the default skill update logic entirely:
use StateUpdater;
;
engine.set_updater;
Scoring Formula
score(item) = Π factors(item, state) × (1 + noise)
challenge = exp(-(score_proxy - (skill + optimism))² / (2 × bandwidth²))
spacing = 1 - exp(-(elapsed / optimal_interval))
coverage = 1 / (1 + category_count[category] / mean_category_count)
noise = xorshift64() × exploration_rate
Multiplicative pipeline: all factors must agree. A near-zero in any factor zeroes the result.
Configuration
EngineConfig
new // default 0.2
new // default 86400 (24h)
Performance
| Item count | Selection strategy |
|---|---|
| ≤ 500 | Linear scan (cache-friendly) |
| > 500 | Max-heap O(log n) |
State updates are O(1). No allocations in the hot path beyond HashMap operations.
Examples
cargo run --example learning_simulation
License
MIT