Skip to content

Adaptive Learning

The scoring model’s weights are not static. Analyst triage feedback is collected and fed into a Bayesian learner that periodically produces new weight sets, stored versioned and served back to the scorer.

  1. Analysts label findings — in the triage queue, each finding (domain or social) is marked confirmed or benign, optionally with a reason.
  2. Rails exposes samplesGET /v1/scoring/feedback_samples returns the most recent labels (max 500), each carrying label, reason, signals, ai_verdict, ai_confidence, adjusted_score, and created_at.
  3. Rails exposes campaignsGET /v1/scoring/campaign_stats returns cross-scan campaign clusters from the last 30 days (max 2000 findings), which give the learner structural context.
  4. The learner fitsscoring/learner.py updates the signal weights based on the labeled samples (and the decay policy).
  5. Weights are stored — the learner writes a new version via PUT /v1/scoring/weights; Rails persists it in scoring_weights with source: "learned" and version = max(latest+1, requested, 1).
  6. The scorer uses the latest — the score service reads the newest weight version through GET /v1/scoring/weights.
Variable Default Purpose
MIN_LABELED_SAMPLES 10 minimum labeled samples before the learner updates weights
WEIGHTS_DECAY_DAYS 90 recency decay applied to samples

The learner only produces a new weight set once there are enough labeled samples; otherwise the latest weights (or defaults) remain in effect.

Every weight set records its source (manual or learned) and sample_size, so analysts can see whether the current weights came from the curated defaults or from accumulated feedback.

Campaign context is fed to the learner so that structurally-related findings (sharing infrastructure) are not scored independently of one another — an entire cluster of lookalikes sharing one host IP is stronger evidence than isolated findings.