Skip to content

ADR-033: Team-level Judgment Layer & Team Primitive

Status: Accepted Date: 2026-04-29 Implemented: 2026-04-30 (migration 073) Context: Sync currently treats every player as an individual — the 13 behavioral signals (ADR-024) and the name-bias signal (ADR-028) all aggregate to player_decision_profiles, scoped to one user. As the team conversation has matured (per the April 28 Cici call and ongoing team discussions), a clear product question has surfaced: can the same judgment layer support a "company brain" view — what does this team optimize for, where does it split, who has voice, who reads whom? An audit of the current schema shows we have most of the raw data (delegation_preferences, peer_prediction_edges, post_discussion_actions, campaign_path_votes) but no team-as-entity primitive and no aggregation layer. This ADR establishes the team primitive, identifies what's derivable from existing data versus what needs new collection, and defers the rollups themselves until a concrete team use case requires them.

At a glance

What it decides: Sync now has an explicit team primitive — teams and team_members tables plus team-tagging on campaigns — so the judgment layer can describe a whole team, not just individuals. The team-level aggregation/rollups are deliberately deferred until a concrete team-facing UI needs them.

  • Explicit opt-in teams an owner creates a team and invites members; teams are not inferred from co-play patterns.
  • Session-level guest flag session_participants.is_guest keeps a visiting player's choices out of team aggregates while still feeding their personal profile.
  • One revise-attribution field responding_to_user_ids captures who moved whom, unifying influence-chain and voice-equity signals in a single UI affordance.
  • Rejected: implicit clustering auto-detecting teams from co-play was ruled out — it breaks the personal-twin default frame and creates overlapping-cluster ambiguity.
  • Watch attribution rate after rollout; if under ~30% of revisions get attributed, the optional dropdown isn't enough and sequential-reveal inference is the next move.
flowchart TD
    U["Solo player (personal twin)"] -->|"owner creates, invites"| T["Team (explicit primitive)"]
    T --> M["team_members"]
    T -.->|"tags"| C["campaigns.team_id"]
    C --> S["Session play"]
    S -->|"is_guest = true"| G["Excluded from team aggregate"]
    S -->|"Revise + responding_to_user_ids"| E["Influence / voice-equity edges"]
    M --> AGG["Team judgment layer (rollups DEFERRED)"]
    E --> AGG

Personal play feeds an explicit team primitive; guest sessions are excluded and revise-attribution edges feed the deferred team-level rollups.

Decision

Adopt explicit team primitives with three changes:

  1. New tables: teams and team_members, modeled on standard ownership/membership patterns (Slack workspace, Notion teamspace).
  2. New nullable columns:
  3. campaigns.team_id — null = ad-hoc / personal play, set = team-tagged campaign
  4. session_participants.is_guest — default false; true means "contributes to personal profile but excluded from team aggregations"
  5. New revise-attribution field on post_discussion_actions: a responding_to_user_ids array (multi-select dropdown on the Revise UI, optional). Captures which teammate(s) the player credits as having moved them.

Defer the actual team_decision_profiles aggregation table and the rollup analyses (fault-line detection, voice-equity rankings, group behavioral trajectory) until a concrete team use case is in flight. Build the primitives now so future aggregation work has data to chew on; don't pre-build rollups speculatively.

Rationale

Why explicit teams (not implicit clustering)

The default Sync user is solo — building their personal twin. If the model auto-suggests teams from co-play patterns, two problems arise: (1) overlapping clusters create UX ambiguity (which "team" does this campaign belong to?), and (2) the framing "you've been added to a team" cuts against the personal-twin contract that brings most users in. Explicit opt-in keeps teams as a deliberate container on top of personal play, not the default frame.

This matches how every successful collaboration tool defines workspaces: explicit creation by an owner, explicit invitations, explicit membership. Implicit team detection is a Notion-style suggestion at most ("you've played 6 times with these 4 — make this a team?"), never an auto-assignment.

Why guest flagging belongs at session level, not team level

A team can play a campaign with a guest joining one session (a friend visiting, a candidate being evaluated). The guest's choices contribute to their personal profile, but mixing them into the team's aggregate would distort it. Per-session, per-participant flagging is the correct granularity — the guest can also be a member of a different team without conflict.

Why one revise-attribution field unifies two signals

The original design considered influence-chain provenance (who moved whom) and voice-equity (who has voice in the room) as separate signals. They're really two views of the same edge: when player A revises and credits player B, that's an influence edge B → A. Aggregated across the team, the same edges produce a voice-equity ranking ("Bob gets cited 40% of the time; Alice 5%, by category"). One UI affordance on Revise covers both.

Why defer the aggregation layer

Most of the team-level signals (fault lines, group trajectory, voice equity rankings) are derivable from already-collected data — they don't need new columns, just new SQL/code. Building the aggregation layer speculatively would create maintenance burden without product validation. Build the primitives now (teams, guest flag, revise attribution) so the data is collected; defer the rollups until a concrete UI surface (team profile page, team dashboard) requires them.

Alternatives Considered

  • Implicit team clustering from co-play patterns: derive teams from "5 people who played 6+ campaigns together." Rejected — UX ambiguity with overlapping clusters, contradicts the personal-twin default frame, fuzzy boundaries make team profile aggregation undefined.

  • Campaign-cohort = team (no separate primitive): every multi-session campaign is the team. Rejected — doesn't match how teams actually work. A real work team plays many campaigns/worlds; collapsing them into one entity loses cross-campaign continuity, and treating each campaign as its own team produces N disconnected mini-profiles.

  • Build full team_decision_profiles aggregation now: ship the rollup table alongside the primitives. Rejected on YAGNI grounds — most signals are derivable on-demand from existing data; building the aggregation layer before a team-facing UI exists creates schema we don't yet know how to populate or query optimally.

  • Capture revision influence by inferring from timing/sequence: instead of an explicit dropdown, infer "Bob lowered conviction at T0+30s after Alice published her rationale at T0." Rejected — the current discussion phase is batch-published (everyone sees rationales simultaneously), so there's no sequence to infer from. Would require a mechanic change (sequential rationale reveal) that's a much bigger product call.

  • Mechanic change to sequential discussion phase: introduce a one-at-a-time speaking order where rationales unfold sequentially and others react. Rejected as out of scope for this ADR. The cheap revise-attribution dropdown captures the same signal without changing how discussion works. Revisit if the explicit dropdown proves insufficient.

Discussion

This ADR was triggered by a team conversation about whether the schema captures everything needed for a "company brain" — a team-level judgment layer that mirrors what Sync does for individuals. An audit confirmed strong foundations (delegation_preferences, peer_prediction_edges, peer_readability_scores, post_discussion_actions, campaign_path_votes, party_cohesion_score) and identified eight gaps:

  1. No team_decision_profiles table — all 13 signals are individual-only
  2. No persistent team identity — campaigns are episodic
  3. No team-level behavioral_trajectory — only individual drift under pressure is tracked
  4. No revision influence chain — we know A revised but not who moved them
  5. No fault-line rollups
  6. No voice equity measure
  7. No domain-of-authority view (team trusts X for governance, Y for resources)
  8. No shared mental model overlap analysis

The conversation collapsed these into three categories:

  • Primitive missing: items 1-2, blockers for everything else. Build now.
  • Single column missing: item 4. The cheap revise-attribution dropdown unifies items 4 and 6 into one signal collection. Build now.
  • Pure aggregation: items 3, 5, 7, 8. Derivable from existing or about-to-be-collected data. Defer until a team-facing UI requires them.

Two side-points worth preserving:

The team-definition discussion ruled out implicit clustering early. The reasoning: the product's headline frame is "build your personal twin," and teams should be a deliberate container on top of that, not an automatic grouping that makes solo players feel pre-categorized. Hundreds or thousands of solo users is the expected steady state; explicit team formation is the right opt-in surface for the smaller subset who want shared profiles.

The voice-equity discussion initially proposed three proxies (conviction-shift correlation, delegation-received rate, rationale text echo). The first was dropped after the realization that the current discussion phase publishes rationales simultaneously, leaving no sequence to correlate against. Delegation-received rate (proxy 2) is already in the data and gives a defensible voice metric for a team profile page even if no other voice signal is added. Rationale echo (proxy 3) requires NLP similarity work and was deemed out of scope until V2.

The decision to defer the aggregation table over building it now reflects a YAGNI bet: speculative aggregation tables tend to drift out of sync with their consumers. Wait for a concrete team UI surface (likely a "team portrait" page mirroring the personal profile work happening with the designer handoff), then build the aggregation against the actual query patterns it needs to serve.

Consequences

  • New tables: teams (id, name, owner_id, created_at) and team_members (team_id, user_id, role, joined_at). Standard ownership semantics.
  • New nullable columns: campaigns.team_id, session_participants.is_guest. Both default-safe — existing rows continue to work as ad-hoc/personal play.
  • post_discussion_actions.responding_to_user_ids: text[] (uuid array) — captures the influence edge when a player revises. Multi-select to handle "multiple teammates moved me." Optional in UI; null/empty array means no attribution given.
  • Revise UI gains a small dropdown: "Responding to: [teammate names]" — appears only in multiplayer post-discussion, only on the Revise action path. Low-friction, opt-in.
  • Team profile page: deferred. When built, it will need to (a) compute aggregations from existing data, (b) define guest-exclusion semantics in queries, (c) handle the empty-team case gracefully.
  • Migration: forward-compatible. Solo and ad-hoc multiplayer continue working unchanged. Existing campaigns have null team_id. Existing post-discussion actions have null responding_to_user_ids. No backfill required.
  • What to revisit: if the explicit revise-attribution dropdown produces too-sparse data (players don't bother selecting), the inference-from-sequence alternative (with a mechanic change to sequential rationale reveal) becomes the next move. Watch attribution rate after rollout; if <30% of revisions get attributed, the optional dropdown isn't enough.
  • Open question for future ADR: when a team plays a campaign together but with rotating subsets per session, does the team profile aggregate from all session-participations (with guest-flag exclusions) or only sessions where ≥N team members were present? Defer until team UI lands.

Key files: - New migration — teams, team_members tables; team_id on campaigns; is_guest on session_participants; responding_to_user_ids on post_discussion_actions - src/types/database.ts — Team and TeamMember interfaces; updated CampaignRow and SessionParticipantRow; PostDiscussionAction extension - src/app/play/[sessionId]/page.tsx — Revise UI gains the responding-to dropdown - src/app/api/sessions/[id]/post-discussion/route.ts (or equivalent) — accept and persist responding_to_user_ids - Future: docs/handoff-team-profile.md — designer-facing equivalent of the individual profile handoff once team UI is in scope