How this is built
CQRS / Event Sourcing on the Ash framework (Elixir / Phoenix LiveView), following the same architecture as the production Dangote portal.
The flow
Command → Event → Projection.
Commands are Ash resources with no data layer; on success an
after_action
change dispatches them through Commanded. The handler builds an
immutable, versioned event (*V1) which is the only source
of truth. Read models — the discovery board, scouting reports, the
event feed — are Ash projections rebuilt by subscribing to that log.
The AI layer
A scout's request emits ScoutingReportRequestedV1. An
event-driven automation calls an LLM through an Ash action
(ash_ai, structured output). The result passes a
grounding guardrail: every number the model cites
must trace to a recorded stat, or the report is rejected. The outcome
is captured as ScoutingReportGeneratedV1 /
…RejectedV1 /
…FailedV1 — so every AI decision is replayable and auditable.
Read discipline
No read pipes into Ash.read. Every query is a named read
action with its filter logic embedded in the resource and exposed via
a code interface. LiveViews call Projection.search!/1 —
never a query pipeline.
Why it mirrors Tonsser
- Consumer-scale discovery domain with mobile-shaped reads.
- Multi-sport from day one (football + a basketball slice) — a nod to the NBA R&D.
- AI integrated as an evaluated product feature, not a demo.
Deliberately out of scope
No auth/login wall (reviewer-first), no video pipeline (highlights are metadata), no real PII. These are scope decisions, not omissions — stated plainly because calibrated honesty is the point.
Full ADRs, the legacy-evolution essay and the AI cost/eval notes ship
in the repository's /docs folder.