Skip to content

Profile Schema Dictionary

Runtime structure of player_decision_profiles — the table that holds everything Sync has learned about a player. This document complements ADR-024 (which is conceptual) and ADR-028 (Signal 13). For canonical type definitions, see src/types/database.ts:834-952.

Live sample: any row in player_decision_profiles. CoachJ (c2b798f1-7311-4020-a0ae-8001c23a1576) is a useful reference — 69 games analyzed, mid-confidence (0.74), most signals populated.


Foundational vocabulary

These appear inside almost every signal. Memorize them once.

8 Primary Drivers (PrimaryDriver)

What the player optimizes for. Defined in src/lib/ai/player-model.ts:21-30.

Key Meaning
team_harmony Group cohesion, relationships
efficiency Speed, resource optimization
principle Values, ethics
pragmatism Practical outcomes over ideals
caution Safety, risk mitigation
growth Expansion, opportunity
transparency Openness, honesty
autonomy Independence, self-direction

6 Reasoning Modes (ReasoningMode)

How they reason — Signal 1. src/types/database.ts:738

Key Meaning
principled_deductive Top-down from rules/values
consequentialist Outcome-focused weighing
analogical Pattern-match to past cases
eliminative Rule out bad options
intuitive Gut, recognition-primed
systems_thinking Effects on the whole system

6 Attention Categories (AttentionCategory)

What scenario details they notice — Signal 4. src/types/database.ts:741

financial, interpersonal, political, temporal, ethical, technical

10 Context Triggers (ContextTrigger)

Conditions that may shift behavior. src/lib/ai/player-model.ts:35-46.

crisis_mentioned, team_conflict, resource_constraint, opportunity, ethical_dilemma, external_pressure, competitive_pressure, innovation_challenge, authority_threat, relational_proximity

Scenario categories

Strings, not enums. Common values: governance, team-dynamics, resource-allocation, values-culture. New categories may appear.

Decision strength

1-5, where 1 = weak conviction, 5 = strong. Self-reported per choice.

Post-reveal reaction

gets_me | off_track | interesting — the quick-tap on the post-reveal screen.


Meta fields (always present)

Field Type Notes
id uuid
user_id uuid FK → profiles.id
games_analyzed int Total games contributing to this profile
model_confidence numeric (0-1) How much to trust the profile. Climbs slowly with games.
last_updated timestamptz
created_at timestamptz
recent_predictions boolean[] Last 10 prediction results (true = AI got it right)

Pre-existing dimensions (drivers)

These three predate ADR-024.

category_patterns (jsonb)

Per-category driver tendencies. Sparse — only categories the player has touched.

{
  [category: string]: {
    primary_driver: PrimaryDriver;        // dominant driver (back-compat)
    driver_distribution: Record<PrimaryDriver, number>;  // probability spread, sums to ~1
    risk_tolerance: number;               // 0-1
    decision_speed: 'deliberate' | 'balanced' | 'quick';
    games_in_category: number;
    recent_trend: string | null;          // e.g. "shifting toward caution"
  }
}

context_rules (jsonb)

Array of conditional behavior shifts. Each rule says "when X trigger fires, behavior tilts toward Y."

{
  trigger: ContextTrigger;
  behavior_shift: PrimaryDriver;          // back-compat
  behavior_distribution: Record<PrimaryDriver, number>;
  confidence: number;                     // 0-1
  evidence_count: number;                 // games supporting this rule
}[]

Often empty array [] until ~10 games played.

option_preferences (jsonb)

Keyword-level lexical preferences.

{
  keyword_attractors: Record<string, number>;  // word → net (chosen - rejected)
  keyword_repellents: Record<string, number>;
}

Signal 1 — reasoning_style_distribution

How they reason, distributionally. Comes from the classify-driver Claude call.

{
  global: Partial<Record<ReasoningMode, number>>;
  by_category: Record<string, Partial<Record<ReasoningMode, number>>>;
}

Sparsity: null until ≥1 reasoning mode classified. by_category only has categories played. Values: probabilities, sum to ~1 within scope. CoachJ example: global = { consequentialist: 0.59, principled_deductive: 0.23, eliminative: 0.18 } — three modes detected, three never observed.


Signal 2 — self_awareness

Gap between self-model and behavioral model.

{
  milestone_checks: {
    game_number: number;
    category: string;
    self_predicted: PrimaryDriver;
    model_says: PrimaryDriver;
    alignment: boolean;
    timestamp: string;
  }[];
  alignment_rate: number;                 // fraction of milestones aligned
  resonance_stats: {
    gets_me: number;                      // count of post-reveal "gets me" taps
    off_track: number;
    interesting: number;
  };
}

Sparsity: resonance_stats populates from game 1 (any reveal screen). milestone_checks only fires at games 10/25/50 in solo. Multiplayer never produces milestone checks. Headline metric: gets_me / (gets_me + off_track + interesting) is the AI-resonance rate.


Signal 3 — behavioral_trajectory (lives on campaigns, not profiles)

⚠️ Not on player_decision_profiles. Stored per-campaign in campaigns.behavioral_trajectory. Captures how the same player drifts across the chapters of one arc. Worth surfacing on the profile page only if you aggregate across campaigns.

{
  snapshots: {
    chapter: number;
    phase: 'setup' | 'rising' | 'crisis' | 'climax' | 'resolution';
    driver_distribution: Record<PrimaryDriver, number>;
    reasoning_style: Record<ReasoningMode, number>;
    attention_focus: Record<AttentionCategory, number>;
    decision_strength_avg: number;
    triggers_present: string[];
    timestamp: string;
  }[];
  pressure_profile: {
    crisis_response: 'centralizes' | 'distributes' | 'freezes' | 'mixed';
    values_stability: number;             // 0-1
    shift_triggers: string[];
    shift_magnitude: number;
  } | null;
  arc_summary: string | null;             // prose, generated at arc completion
}

Sparsity: snapshots accumulate one-per-chapter; pressure_profile requires ≥2 snapshots; arc_summary only at full arc completion.


Signal 4 — attention_distribution

Same shape as reasoning style.

{
  global: Partial<Record<AttentionCategory, number>>;
  by_category: Record<string, Partial<Record<AttentionCategory, number>>>;
}

CoachJ example: global = { political: 0.45, ethical: 0.21, temporal: 0.20, financial: 0.14 }. interpersonal and technical never noticed.


Signal 5 — confidence_calibration

Does self-reported certainty correlate with actual predictability?

{
  global: {
    calibration_score: number;            // 0-1, 1 = perfectly calibrated
    overconfidence_rate: number;          // fraction of high-conf choices that were unpredictable
    underconfidence_rate: number;         // fraction of low-conf that were predictable
    samples: number;
  };
  by_category: Record<string, /* same shape */>;
  history: {
    strength: 1|2|3|4|5;
    was_predictable: boolean;
    category: string;
  }[];                                    // capped at last 50
}

Sparsity: needs ≥3 samples per category to be informative. history is the raw stream; the rest are derived rollups. Gotcha: with very few samples, both overconfidence_rate and underconfidence_rate can both equal 1.0 (CoachJ's case). Wait until samples ≥ 10 before showing this in UI.


Signal 6 — timing_signature

{
  global: { mean_ms: number; variance_ms: number; speed_strength_correlation: number };
  by_category: Record<string, /* same */>;
}

Sparsity: frequently {} in early-stage profiles (CoachJ has empty {}). Don't render until at least the global block is populated.


Signal 7 — persuadability

Stand rate after multiplayer post-discussion. Solo never contributes.

{
  global_stand_rate: number;              // fraction of times they kept their original choice
  by_category: Record<string, number | null>;  // can be null per category
  conviction_change_sensitivity: number;  // correlation between low strength and revision
  total_discussions: number;
}

Sparsity: total_discussions < 5 → noisy. null per-category means no discussions in that category yet.


Signal 8 — peer_reading

How well they predict others in multiplayer.

{
  accuracy_when_confident: number;        // accuracy when their predictor_confidence ≥ 4
  accuracy_when_uncertain: number;        // accuracy when ≤ 2
  calibration: number;                    // correlation
}

Sparsity: requires multiplayer participation with confidence selector. Often null for solo-heavy players.


Signal 9 — dissent_profile

Multiplayer-only. Are they a contrarian?

{
  nota_rate: { global: number; by_category: Record<string, number> };
  consensus_deviation_rate: { global: number; by_category: Record<string, number> };
  total_multiplayer_games: number;
}

Read: consensus_deviation_rate = 1.0 means they always disagreed with the group. CoachJ's case (3 multiplayer games, 100% deviation) — high signal but low sample.


Signal 10 — authenticity

Does the rationale they wrote match the driver their actual choice expressed?

{
  alignment_rate: number;                 // 0-1
  by_category: Record<string, number>;
  samples: number;
}

Sparsity: only games where the player wrote a rationale contribute.


Signal 11 — learning_rate

Improvement trajectory in prediction accuracy.

{
  global: number;                         // positive = improving
  by_category: Record<string, number>;
}

Sparsity: needs longitudinal data. CoachJ's global = 0 despite 69 games suggests the rolling-window logic hasn't accumulated enough — treat values near 0 as "not enough data" rather than "flat learner."


Signal 12 — occasion_noise

Within-person inconsistency on repeat/similar scenarios.

{
  global: number;                         // 0-1, fraction of repeats with different choices
  by_category: Record<string, number>;
  repeat_comparisons: number;
}

Read: high noise (>0.4) = "I am consistently inconsistent" (per Jonathan's own self-description). Sparsity: repeat_comparisons < 3 → don't show.


Signal 13 — name_bias_signal

Per ADR-028. Correlation between scenario character demographics and the player's choices.

{
  origin_alignment: Record<NameOrigin, { aligned_rate: number; opposed_rate: number; samples: number }>;
  formality_alignment: Record<'formal'|'informal'|'institutional', /* same */>;
  gender_alignment: Record<'masc'|'fem'|'ambiguous', /* same */>;
  signal_strength: number;                // 0 = noise, higher = systematic deviation
  total_scenarios_with_names: number;
}

NameOrigin: anglo | east_asian | south_asian | latin | west_african | east_african | middle_eastern | slavic | nordic | ambiguous

Sensitivity: this is the most politically loaded signal. Treat carefully in UX — surface as observation, not accusation. Hide entirely if signal_strength near 0.


Sparsity decision tree

When designing, default to hiding signals that aren't ready:

Condition Action
Field is null Hide entirely
samples / total_* < 3 Hide entirely
Field exists but distribution is empty {} Hide
samples 3-9 Show with "still learning" annotation
samples ≥ 10 Full display
model_confidence < 0.3 Show profile with "Early days — keep playing" framing across the board

Where signals get written

For tracing data flow: