-
Notifications
You must be signed in to change notification settings - Fork 0
overview architecture
Kognitika uses an event-driven architecture (EDA) with a clear separation between UI components, engine hooks, and analytics processing. This document covers the major architectural patterns.
The EventBus class at src/core/events/event-bus.ts is the central communication channel. It decouples trainer engines from UI components, analytics subscribers, and persistence layers. Engines emit events through the bus, and subscribers react to them without direct dependencies.
Key events defined in EventBus.EVENTS:
| Event | Emitter | Subscribers |
|---|---|---|
CELL_CLICK |
Engine | Analytics, Recorder |
TRAINING_COMPLETE |
Engine | DB-writer, Leaderboard |
MISTAKE_MADE |
Engine | Analytics |
STABILITY_UPDATE |
Analytics | UI (HUD widgets) |
DIFFICULTY_SUGGESTION |
Analytics worker | Engine (adaptive mode) |
The EventBus supports middleware chains and Zod schema validation for every event type. Validation errors are caught by a configurable handler instead of crashing the bus.
Every trainer module follows the same hook pattern:
UI Component <--> use{Module}Engine <--> EventBus <--> Analytics / Subscribers
Each use{Module}Engine hook (for example, useSchulteEngine, useNBackEngine, useTypingEngine) owns the game state, exposes actions (click, restart, submit), and emits events to the EventBus. UI components render from the hook's state and call its actions. They never contain game logic directly.
This pattern gives:
- Testability: engines can be tested without DOM rendering.
- Consistency: every trainer shares the same interface for state, actions, and event emission.
- Analytics decoupling: analytics subscribers receive events through the bus without engine imports.
All generators (Schulte grids, N-Back sequences, spatial layouts, mental-math problems) accept a seed parameter. The same seed always produces the same output, which makes tests reproducible:
const grid1 = generateGrid(5, 'classic', 42);
const grid2 = generateGrid(5, 'classic', 42);
expect(grid1).toEqual(grid2); // always trueThis is verified by src/tests/reproducibility.test.ts.
The current product has two related but distinct analytics contracts:
-
src/workers/analytics.worker.tsandsrc/lib/cognitive-metrics.tsuse a lightweight click-oriented JavaScript/TypeScript contract for immediate UI feedback. -
src/core/analyze-session/session-analysis.tsandcrates/kognitika-coreimplement the strict full-sessionAnalyzeSessioncontract. The Rust crate already supports native builds and exportsanalyzeSessionJsonfor WASM.
Rust is therefore not merely a future placeholder, but it is not yet the production authority either. The staged target is:
- Every cognitive module emits a canonical versioned event contract.
- Express validates ownership and creates durable analytics jobs in PostgreSQL.
- TypeScript remains authoritative while a native Rust/Axum analyzer runs in shadow mode.
- Field-level parity, latency, failure, and fallback metrics gate a deterministic canary rollout.
- Rust becomes the primary session analyzer with a temporary TypeScript circuit-breaker fallback.
- Longitudinal, baseline-aware skill dynamics move to Rust after session analysis is stable.
Browser WASM remains a separate frame-budget decision. Existing benchmarks show that worker isolation helps responsiveness, but do not justify replacing the browser TypeScript path solely for throughput. See Rust analytics roadmap.
The server at server.ts starts an Express app with a Socket.io server attached to the same HTTP server. It serves:
- API routes under
/api/*(auth, game, analytics, admin, feedback, ideas, leaderboard, neurotrainer, daily-trajectory, practice-flow, observability). - Static files from the Vite build in production.
- Vite dev middleware in development mode.
Rate limiting applies to all API routes (100 requests per 15 minutes) with a stricter limit for auth endpoints (10 per hour).
The Socket.io server handles real-time duel sessions through src/server/realtime/duels.ts. CORS configuration is shared between Express and Socket.io via src/server/config/cors.ts, which enforces an explicit allowlist in production.
┌──────────────┐ action ┌──────────────────┐
│ UI Widget │◄──────────────│ use{Module}Engine│
│ (React) │ state │ │
└──────┬───────┘ └────────┬─────────┘
│ │ emit events
│ ▼
│ ┌──────────────────┐
│ │ EventBus │
│ │ (type-safe, │
│ │ validated) │
│ └────┬──────┬───────┘
│ │ │
│ ┌─────────────┘ └─────────────┐
│ ▼ ▼
│ ┌──────────────────┐ ┌──────────────────┐
│ │ Analytics │ │ DB / Leaderboard │
│ │ Subscriber │ │ Subscriber │
│ └────────┬─────────┘ └──────────────────┘
│ │
│ ▼
│ ┌──────────────────┐
│ │ Analytics Worker │
│ │ (JS/TS current) │
│ └────────┬─────────┘
│ │ metrics
│ ▼
│ ┌──────────────────┐
└──►│ STABILITY_UPDATE │
│ DIFFICULTY_SUG. │
└──────────────────┘
The diagram shows the current UI flow: a user interacts with a widget, the engine updates state and emits events, and subscribers forward lightweight analytics to the JavaScript/TypeScript worker or persist completed sessions. The planned server-side Rust path is deliberately asynchronous and is not shown as part of the synchronous UI loop: durable job → Axum analyzer → versioned summary → longitudinal projection.