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?
Sparsity: only games where the player wrote a rationale contribute.
Signal 11 — learning_rate¶
Improvement trajectory in prediction accuracy.
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:
- Most signals:
src/lib/ai/player-model.ts→updatePlayerProfile() - Reasoning + attention (Signals 1, 4): extended classify-driver prompt at
src/app/api/ai/classify-driver/route.ts - Self-awareness resonance (Signal 2):
src/app/api/results/[id]/reaction/route.ts - Trajectory (Signal 3):
src/app/api/campaigns/[id]/advance/route.ts - Persuadability + dissent (Signals 7, 9):
src/app/api/sessions/complete/route.ts - Name bias (Signal 13): see ADR-028 key files