Insight Player — plain-language explainer

Insight Player is the one shape every People Analytics finding takes on its way to a human, and the one engine that decides which finding that human sees next.

A People Analytics Toolbox cross-cutting primitive (PAT-34) — it lives in src/lib/, not in any one spoke, because every spoke is a candidate emitter and every consumer app is a candidate renderer. Built to the portfolio Explainer Standard v1.0. Every claim below is grounded in the real code (src/lib/insight-player/, src/lib/player/, src/lib/signals/, surface src/surfaces/analytics-player/) and the contract versions in the registry (insight-player.contract 0.1.0, player engine 0.1.0, signals 0.1.0); anything not yet built is marked (TBD).

This is a flagship surface — a thing a buyer and a developer both meet directly — so it earns the fuller, multi-movement treatment rather than a single eight-question page. The eight-question spine (what · why-different · how · enables · fits · worked example · packaging · status · vision) is woven through the movements below.


Lead — the job it does

Give a person the right finding, in a shape they can read in seconds, in an order that learns from how they react — and let any app in the portfolio do that with the same contract and the same ranking math.

Two audiences meet it. A buyer / executive meets the rendered Insight Player — a paced, gestural card feed where one analytical finding fills the screen at a time, swipeable, with a tap-to-reveal justification and a one-tap reaction. A developer meets the primitive — a small pair of vendored files (contract.ts + sequence.ts) plus a converged player engine, that lets their app emit findings as Insight Cards and sequence them the same way every other portfolio app does.


Manifesto — why it exists

The pain it removes is the dashboard. A dashboard hands a person every number at once and makes them do the triage: scan twenty tiles, guess which movement matters, remember it next week. Most People Analytics tooling stops there — it computes well and presents badly, so the analysis that cost real money to produce dies on a tab nobody opens.

The worldview behind Insight Player is the opposite: one finding at a time, sequenced, and the sequence learns. A finding is an atom (the Insight Card). The order those atoms arrive in is a decision (the sequencing engine). And how a person reacts to each one — rate it, dwell on it, expand it, skip it, save it — is a signal that should make the next sequence better (the feedback loop). That is the same loop a recommender runs on a content feed, applied to analytical findings instead of videos.

The differentiation beat, stated as a shift:

  • FROM a dashboard of twenty tiles where the human does the triage, the same layout for everyone, and no memory of what they cared about last time.
  • TO a sequenced feed of single findings, ranked by relevance + freshness + diversity, that records how each person reacts and re-ranks the next session toward what works for them — while a deliberate exploration budget keeps surfacing the unfamiliar so the feed never collapses onto one narrow cluster.

How it differs from the obvious substitutes:

  • vs. generic BI (Tableau / Looker / Power BI): those render charts you assemble and navigate. Insight Player renders findings — a card carries a headline claim, the context, the visual, the justification on reveal, and a request for one rating — and it orders them. BI has no notion of "show this person the most relevant unseen finding next."
  • vs. each app rolling its own feed: if Performix, the toolbox's own /feed, and rankandfile each invented their own card shape and ranking, two apps would show different cards in a different order from the same source data. The contract + the one canonical scoring formula exist precisely so they can't drift. The toolbox owns the math; consumers vendor a copy.
  • vs. doing it by hand: a human curating "the three things you should look at this week" doesn't scale across spokes, doesn't learn, and can't run a reproducible, seedable ranking that two surfaces will agree on.

Visual — Tier B (FROM→TO typographic block). The shift above is the visual. dashboard of 20 tiles · human triages · no memorysequenced feed of single findings · engine ranks · loop learns per person.


Walkthrough — how it actually works

There are two journeys through this primitive: the developer's path through the contract + engine, and the person's path through the rendered feed. They meet at the same atoms.

The atom — an Insight Card

Every finding, from every spoke, takes one shape (src/lib/insight-player/contract.ts, InsightCardSchema). The load-bearing fields:

  • id, source_app — globally unique id and the emitting spoke (e.g. "calculus", "anycomp").
  • audience_tags — who should see it (the renderer's visibility filter).
  • topic_tags — open strings (["engagement", "survey", "trend"]) that drive focus-match and diversity scoring.
  • freshness{ computedAt, ttlSeconds? }; past TTL, recency collapses to zero.
  • card_type — a spoke-coined string ("calculus.metric-snapshot") used for redundancy bookkeeping, not a closed taxonomy.
  • headline + body — the one-line claim and its context.
  • visual{ kind, spec } where kind is one of text · metric · trend · chart · comparison · callout and spec is renderer-specific.
  • reveal?{ summary, detail? }, surfaced on tap/long-press (the justification).
  • signals_requested? — what rating the card wants back ({ signalId, prompt }).

A spoke coins its own card_type and topic_tags; the contract doesn't enumerate them. That openness is deliberate — it lets new spokes emit without a contract bump.

The engine — buildPlayerQueue

The ordering decision is a single pure function (src/lib/insight-player/sequence.ts, buildPlayerQueue) — no database, no async, no imports beyond the contract, so it is trivially testable and two consumers running it on the same input get the same queue. The canonical scoring formula (weights named in WEIGHTS):

  • relevance = 0.40 · focusMatch + 0.30 · freshness — focus-match is the fraction of a card's topic_tags that intersect the chosen focus's topic set (a small per-focus map: engagement, performance, compensation, decision-support, default); freshness decays linearly over 7 days, clamped to 0 past ttlSeconds.
  • per-pick: + 0.20 · diversityBonus − 0.15 · redundancyPenalty — diversity is 1 − max(Jaccard overlap) against the already-selected queue; redundancy counts prior cards of the same card_type, soft-capped so a heavy-emitting spoke can't get blacklisted.
  • exploration budget: exploratoryRatio (default 0.25) of the slots are drawn — via a seedable xorshift32 PRNG — from the lower half of the ranking. This is lifted verbatim from Vela's reincarnation "25% exploratory" rule; without it the feed collapses onto a narrow high-confidence cluster and the corpus never gets exposed.

The learning — affinity from signals

buildPlayerQueue takes an optional non-wire hook: opts.affinity(card) → [−1, 1], added to base relevance via WEIGHTS.affinity = 0.5. That number comes from the actor's own history. src/lib/signals/personalize.ts reads an actor's prior signals, builds an affinity profile over topics / sources / card-types (buildAffinityProfile), and scores a new card by feature overlap (affinityScore) — boosting what has worked for this person and damping what hasn't. The exploration budget runs alongside so the feed still surfaces the unknown.

The person's journey through the feed

The rendered surface is analytics-player at the public route /feed (src/app/(surfaces)/feed/page.tsx). On render it:

  1. resolves an actor id (a cookie); anonymous → no affinity → unpersonalized ranking;
  2. if the actor has history, loads up to 500 prior signals and builds affinity (getSignalStore().historyFor(actorId, { limit: 500 })buildAffinity(history));
  3. fetches cards by calling every live spoke's <spoke>.emit-insight-cards tool through the in-process MCP gateway (src/surfaces/analytics-player/lib/fetch-cards.ts) — never importing a spoke's internals, exactly like an external consumer;
  4. runs the concatenated deck through buildPlayerQueue with the actor's affinity;
  5. renders one card at a time. Swipe / drag / arrow-keys advance; tap-to-reveal expands card.reveal; a one-tap reaction posts a rating signal back through a server action (recordSignalAction / recordPlayerSignalsAction), which persists to Postgres (PostgresSignalStore when DATABASE_URL is set, in-memory otherwise).

Optional ?focus=engagement|performance|compensation|decision-support|default reranks the deck. When zero live emitters return cards, the surface falls back to a hand-authored SEED_DECK (flagged isSeed: true) so the page never renders empty.

Visual — Tier B (step flow). spoke emits Insight Card → fetch-cards collects via MCP → buildPlayerQueue ranks (relevance + freshness + diversity − redundancy + learned affinity, 25% exploratory) → /feed renders one card → person reacts → Signal persists → next session's affinity shifts. A rendered capture of the live /feed surface is a follow-up (FU-A — needs the design-system polish pass, currently deferred per the surface README).


What it enables

Concrete uses a practitioner or a builder would recognize:

  • An executive "what should I look at this week" feed — the toolbox /feed sequences findings across every live analytics spoke into one paced, swipeable surface, with a justification one tap away.
  • A shared Insight Player across apps — Performix is the reference renderer; it and the toolbox feed share the same card shape and ranking, so the same source data yields the same cards in the same order. (Performix is the vendored authority for the light/exec renderer — src/lib/insight-player/renderer/ is vendored from it.)
  • A learning content product (rankandfile.com) — the north star: generate role-targeted content as cards, learn from each person's reactions, surface more of what works. The Library view (components/Library.tsx) browses the full card set with filters (source · type · topic · freshness) while the player sequences a slice — the explore-vs-play split rankandfile needs.
  • A per-card rating loop — each card can request one rating (signals_requested); the reaction becomes a portfolio Signal that re-ranks future sessions.
  • A post-session reflection — after a sequence completes, src/lib/signals/sequence-reflection.ts aggregates the session's signals into "what the loop learned" (rated / resonated / passed counts, dwell, top topics + sources) plus two short LLM-authored lines (Haiku via getModel, with deterministic English fallback). Read at GET /api/player/reflection.
  • A converged engine for cards and immersive media — the generalized player engine (src/lib/player/) sequences a content-agnostic PlayerUnit whose renderFamily is card | media | text; an Insight Card maps in 1:1 (fromInsightCard), and Vela's immersive media items are the same atom under a media renderer.

Visual — Tier B (typographic). Consumers that meet it today: toolbox /feed (renderer) · Performix /insights (reference renderer + renderer authority) · rankandfile (planned product) · vela (media-family sibling) — all over the one contract.


How it fits in the toolbox

It is a cross-cutting primitive in src/lib/, not a spoke — it owns no Postgres schema and emits no MetricEnvelope; it is the carrier and sequencer of findings other spokes produce.

What it composes (HTTP + contracts only, never another spoke's internals):

  • Emitters (the producers). Every analytics spoke is a candidate emitter via a <spoke>.emit-insight-cards MCP tool; the surface fetcher calls them through the in-process MCP gateway. Today the fetcher calls reincarnation, data-anonymizer, segmentation-studio unconditionally and preference-modeler / anycomp / forecasting only when a surveyId / modelId / decisionModelId is supplied (calculus needs pre-computed envelopes, so it's skipped from the URL alone). The breadth of which spokes have a live emitter tool is the active edge — see status.
  • The signals loop (src/lib/signals/). The contract (SignalSchema, kinds: explicit rating · thumb · save · dismiss · act · reviewed, implicit view · dwell · expand · complete · share · skip), the durable store (PostgresSignalStore on the signals schema, in-memory fallback), the personalizer (buildAffinity), and the cross-property ingest front door (POST /api/signals/ingest, service-key gated). Signals are also vendored cross-property: any property POSTs Signal[] asserting a stable actorId, so the same human shares affinity across apps.
  • The converged player engine (src/lib/player/). Contract 0.1.0 (PlayerUnit / PlayerSequence, render families card | media | text) + the pure session state machine (session.ts — advance / retreat / rate / reveal, each emitting the right Signal). One engine, two divergent presentation wrappers (card feed vs. immersive media chrome).
  • The renderer (src/lib/insight-player/renderer/). The light/executive card renderer, vendored verbatim from Performix (the renderer authority) — InsightCardRenderer + the PAPER_INK / PERFORMIX_CAMS themes + the toolbox-local fromInsightCard mapper. (Per portfolio convention, the exec Insight Player renders light, never dark.)

Contracts in play and how to consume them:

  1. In-process — toolbox surfaces import @/lib/insight-player/contract + sequence directly.
  2. Vendored — Performix / vela / external apps copy contract.ts + sequence.ts (and signals/contract.ts) with a // vendored from … @ <sha> header.
  3. Discovery — AI agents call the registered MCP tool toolbox.insight-player.contract (src/lib/mcp/discovery-tools.ts) to introspect version + schema names + enum sets without parsing source.

Bump-via-toolbox is the only path to evolve the contract — a consumer that wants a new focus or visual kind files against the toolbox; it never patches its vendored copy. That is what keeps two renderers aligned.

Visual — Tier B (typographic data-flow). { spokes emit cards } → insight-player contract + sequence → { /feed · Performix · rankandfile · vela } → signals contract → store + personalizer → back into sequence. The loop closes.


One load-bearing worked example

A real, end-to-end pass through the engine, grounded in the in-repo SEED_DECK (src/surfaces/analytics-player/lib/fetch-cards.ts) and the canonical scoring formula. This is the deck the surface serves when no live emitter responds — real cards, real ranking, no invented numbers.

The seed deck (five cards, abbreviated to the load-bearing fields):

  • seed-eng-pulse-2026q1preference-modeler, topics engagement · sentiment · survey, "Engagement up 4 pts QoQ (Engineering)".
  • seed-comp-band-l4-2026q1anycomp, topics compensation · band · labor-market, "L4 software-eng band centered at $172k".
  • seed-perf-rating-distribution-2025h2calculus, topics performance · rating · calibration, "Rating distribution skews high (Sales)".
  • seed-decision-attrition-voi-2026q1forecasting, topics decision · voi · attrition-risk, "Attrition-prediction model: EVPI = $84k".
  • seed-text-anonymizer-coveragedata-anonymizer, topics privacy, "PII coverage at 98.7% on Q1 import".

Pass 1 — focus=engagement, no actor history. focusMatch rewards cards whose topic_tags intersect the engagement set (engagement · sentiment · survey · …). The pulse card matches on all three of its topics (focus-match ≈ 1.0); the comp / performance / decision cards match none (focus-match 0); all five carry near-max freshness (computedAt is now). So the engineering pulse card ranks first by the relevance term alone, then diversity + the 25% exploration budget mix in an off-topic card from the lower half so the session isn't single-note. The result is deterministic for a given seed (the surface pins seed: 1).

Pass 2 — same focus, with an actor who has down-rated compensation cards before. The actor's history yields an affinity profile where the compensation / band / labor-market topics and the anycomp source score negative. buildAffinity(history) returns a function that scores seed-comp-band-l4-2026q1 toward −1; via WEIGHTS.affinity = 0.5 that subtracts up to ~0.5 from its base relevance, pushing it down the queue — while the engagement card (positive or neutral affinity) holds the top. The same source data now ranks differently for this person — which is the whole point.

What the person does with it: the engineering-pulse card fills the screen, tap-to-reveal shows the justification (reveal.summary: "n = 142 responses; anonymity threshold met (min N = 12)"), they give it a one-tap rating, that rating Signal persists, and Pass-2-style affinity nudges the next session.

(All values above are the literal contents of the in-repo seed deck and the named weights in sequence.ts. The two-pass ranking is the documented behavior of buildPlayerQueue + buildAffinity, not a measured benchmark — the relative ordering is what the formula guarantees; exact composite scores depend on the live deck and are not asserted here.)

Visual — Tier B (the example IS the visual). A rendered before/after queue-order capture from the live /feed is (TBD — FU-A capture once the design polish pass lands).


Commercialization / packaging

Insight Player is design-system spine, not a line item — it's the surface a buyer meets everywhere an executive output renders, across the portfolio, rather than a product sold on its own. Its commercial role is to make every analytics spoke's output legible and to be the shared rendering + ranking layer the portfolio's apps (and rankandfile, the standalone content product) are built on.

  • Vendor posture: the toolbox is canonical; consumers vendor contract.ts + sequence.ts (+ signals/contract.ts) and bump only via the toolbox. That's the licensing-internal boundary that keeps Performix, the toolbox feed, and future apps aligned.
  • Data-license posture: the primitive itself carries no third-party data — it carries whatever cards the emitting spokes produce, and each card's data-license constraints travel with its source spoke (e.g. a wage card's BLS provenance, a survey card's tenant data).
  • Product-tier placement, pricing, and any packaged "Insight Player" offering are (TBD) — not earned yet, so not stated.

Visual — (TBD — product-family placement diagram showing Insight Player as the shared render/rank layer beneath the consuming apps).


Current status

Grounded in the real code state (contracts: insight-player.contract 0.1.0, player engine 0.1.0, signals 0.1.0):

Shipped:

  • The canonical Insight Card contract + the pure sequencing core (buildPlayerQueue) with the full relevance + freshness + diversity + redundancy + exploration-budget formula (PAT-34).
  • The personalization brain (buildAffinityProfile / affinityScore / buildAffinity) and its wiring into /feed — the rating→sequencing loop is closed: /feed resolves the actor, loads up to 500 prior signals, builds affinity, and feeds it to the ranker.
  • The portfolio Signals contract + durable Postgres store + cross-property ingest (POST /api/signals/ingest) + the in-app server-action capture path.
  • The /feed surface (swipe / drag / arrow-key advance, tap-to-reveal, one-tap rating, focus query param, seed-deck fallback) and the Library browse/filter view.
  • The vendored Performix exec renderer in src/lib/insight-player/renderer/, with a design page at /design/insight-player.
  • The converged player engine (src/lib/player/ — content-agnostic units, card|media|text render families, pure session state machine, signal bridge) and the post-session reflection (sequence-reflection.ts, GET /api/player/reflection).
  • The MCP discovery tool toolbox.insight-player.contract.

In flight / planned:

  • Breadth of live <spoke>.emit-insight-cards emitters — the fetcher is wired for several spokes, but full per-spoke emitter coverage (every analytics spoke producing real, non-seed cards) is the active edge (PAT-36 family).
  • Visual-kind renderers beyond metric / texttrend / chart / comparison / callout currently passthrough on the surface (TBD).
  • Routing a card's signal back to its source spoke for the spoke's own learning (today signals feed only the cross-card affinity loop) (TBD).
  • Cross-property identity linking (merging a pre-login anonymous actor key into a post-login identity) (TBD).
  • The immersive media wrapper (Ken Burns / audio / gestures) over the converged engine — the media render family exists in contract; the chrome is a later step (TBD).
  • Claude Design polish pass on /feed and the Tier-A rendered captures that depend on it (TBD — FU-A).

Visual — Tier B (typographic shipped/edge split). Shipped: contract · sequencer · affinity loop (closed) · signals store · /feed · Library · exec renderer · converged engine · reflection · MCP discovery. Edge: emitter breadth · non-metric visual kinds · signal→spoke routing · cross-property identity · media wrapper · design polish.


The vision

One contract for every finding the portfolio produces, one engine that learns which finding each person wants next, and one loop — fed by every reaction, not just a rating — that makes the content better for that person over time, across every property.

The arc is the universal content feedback loop: many signal kinds (rating is one of twelve — thumbs, save, dismiss, act, view, dwell, expand, complete, share, skip, reviewed) flow into a toolbox-owned loop that re-ranks content per user, content-agnostic and cross-property. rankandfile.com is the first standalone product built on it; Performix and vela are the first cross-portfolio renderers. The two divergent wrappers — the lean-forward analytics card feed and the lean-back immersive media player — converge on this single engine, so a finding and a photograph are the same atom under different chrome, both rated, both feeding the same loop.

The load-bearing next move is breadth and routing: more spokes emitting real cards, and each reaction routed back to the spoke that produced the card so the producer learns too — closing the loop not just for the reader's feed, but for the analysis itself.