import type { ControlDb } from '../control/db.js'; import { newId } from '../engine/ids.js'; /** * Reading and writing the landing page's evidence. * * Nothing here computes anything. What the reward *is* lives in `reward()` * below and in the window constants beside it, and that is deliberately the * only place it is defined: the moments are replayed from these rows on every * fit rather than accumulated in a column, so changing the definition of * durable use re-scores the whole history instead of corrupting it. */ /** * How long an account is watched before its impression is allowed to vote. * * A fixed window from *signup*, not from today, so an account from January * cannot out-earn one from June simply by having existed longer. It is also * what makes an impression mature: nothing is scored until its window has * closed, because an impression from last Tuesday is not a zero, it is a * question that has not been answered yet, and feeding it in as a zero would * mark down whatever the page is currently showing. */ export const OBSERVATION_WINDOW_DAYS = 28; /** * Active days past this add nothing. * * The cap and the log together are what keep one enthusiast from deciding the * voice of the site. Against a few dozen observations an uncapped reward is a * single-sample experiment wearing a covariance matrix. */ export const ACTIVE_DAY_CAP = 20; /** Durable use, on [0, 1]. */ export function reward(activeDays: number): number { const capped = Math.max(0, Math.min(activeDays, ACTIVE_DAY_CAP)); return Math.log1p(capped) / Math.log1p(ACTIVE_DAY_CAP); } export interface ScoredImpression { id: string; items: string[]; createdAt: string; converted: boolean; activeDays: number; reward: number; } interface ImpressionRow { id: string; items: string; created_at: string; account_id: string | null; active_days: number | null; } /** Serve a page. The row exists from here on; whether it counts is `markEngaged`. */ export function recordImpression(control: ControlDb, items: string[]): string { const id = newId('imp'); control.sqlite .prepare('INSERT INTO voice_impression (id, items) VALUES (?, ?)') .run(id, JSON.stringify(items)); return id; } /** * A person looked at it. * * Idempotent, and it never moves an existing timestamp — a reload posting the * beat twice must not restate when the page was seen. */ export function markEngaged(control: ControlDb, id: string): void { control.sqlite .prepare("UPDATE voice_impression SET engaged_at = datetime('now') WHERE id = ? AND engaged_at IS NULL") .run(id); } /** * Attach a signup to the page that produced it. * * Refuses to overwrite: an impression id is quoted back by the browser, so the * only thing stopping the same id being replayed against a second account is * this clause. Worst case someone donates their own signup to a page they liked * — which is a rounding error, and cheaper than a token exchange for it. */ export function attachAccount(control: ControlDb, id: string, accountId: string): void { control.sqlite .prepare('UPDATE voice_impression SET account_id = ? WHERE id = ? AND account_id IS NULL') .run(accountId, id); } /** This account did something today. Cheap, idempotent, once per account per day. */ export function recordActiveDay(control: ControlDb, accountId: string): void { control.sqlite .prepare("INSERT OR IGNORE INTO account_day (account_id, day) VALUES (?, date('now'))") .run(accountId); } /** * Every impression whose answer is in, oldest first. * * Oldest first because the bleed is applied per observation: replaying in the * order the evidence arrived is what makes the replay identical to an online * accumulator that had been running the whole time. * * `limit` takes the most recent N and still replays them oldest-first, which is * how the report shows a recent window beside the all-time one. A traffic * source changing under you reads as those two diverging. */ export function scoredImpressions(control: ControlDb, limit?: number): ScoredImpression[] { const cutoff = `-${OBSERVATION_WINDOW_DAYS} days`; const rows = control.sqlite .prepare<[string, string, number], ImpressionRow>( `SELECT i.id, i.items, i.created_at, i.account_id, (SELECT COUNT(*) FROM account_day d WHERE d.account_id = i.account_id AND d.day >= date(a.created_at) AND d.day < date(a.created_at, ?)) AS active_days FROM voice_impression i LEFT JOIN account a ON a.id = i.account_id WHERE i.engaged_at IS NOT NULL -- Mature from the account's clock when it converted, from the page's -- when it did not: a signup two days ago has not had its 28 days yet -- even if the page it came from was served a month before that. AND COALESCE(a.created_at, i.created_at) <= datetime('now', ?) ORDER BY i.created_at DESC LIMIT ?`, ) .all(`+${OBSERVATION_WINDOW_DAYS} days`, cutoff, limit && limit > 0 ? limit : -1); return rows .reverse() .map((row) => { const activeDays = row.account_id ? (row.active_days ?? 0) : 0; return { id: row.id, items: parseItems(row.items), createdAt: row.created_at, converted: row.account_id !== null, activeDays, reward: row.account_id ? reward(activeDays) : 0, }; }); } /** Impressions seen by a person but not yet old enough to say anything. */ export function pendingCount(control: ControlDb): number { const row = control.sqlite .prepare<[string], { n: number }>( `SELECT COUNT(*) AS n FROM voice_impression i LEFT JOIN account a ON a.id = i.account_id WHERE i.engaged_at IS NOT NULL AND COALESCE(a.created_at, i.created_at) > datetime('now', ?)`, ) .get(`-${OBSERVATION_WINDOW_DAYS} days`); return row?.n ?? 0; } /** * A stored `items` array that will not parse is a corrupted row, not a reason * to take the landing page down. It drops out of the fit as an empty page. */ function parseItems(json: string): string[] { try { const parsed: unknown = JSON.parse(json); return Array.isArray(parsed) ? parsed.filter((v): v is string => typeof v === 'string') : []; } catch { return []; } }