Spreadsheet-UX op een echte relationele engine
Deze repository wordt als momentopname gepubliceerd: een clone bevat de volledige inhoud, maar geen commitgeschiedenis.
import { VOICE_PER_PAGE, VOICE_POOL } from '@wellformed/shared';
/**
* Choosing which selling points a visitor sees.
*
* A softmax over the pool, drawn without replacement, reading weights that came
* out of the covariance matrix — see `latentWeights`. The sampler itself stays
* the simple thing it should be.
*
* Two properties matter more here than the softmax does.
*
* The temperature is not a schedule, it is a function of how much evidence
* exists. With nothing observed the distribution is exactly uniform; it sharpens
* only as the effective sample size grows. So "random" is not a phase to get
* through before the real policy starts — it is what the policy *is* until the
* data earns something sharper, and there is no moment where anyone has to
* declare the experiment over.
*
* The floor is the other half. Every point keeps a share of the traffic
* forever, so none can be killed by an unlucky first fortnight, and each goes on
* collecting the evidence that could overturn its weight. Without it a bandit at
* this sample size reliably locks onto noise.
*/
/** How sharp the softmax gets once the evidence is in. */
const KAPPA_MAX = 6;
/** Effective observations at which the sampler is halfway to trusting itself. */
const KAPPA_HALF_LIFE = 60;
/** Share of each draw handed back to the uniform distribution, always. */
const EXPLORE_FLOOR = 0.05;
/**
* How far below the best point a score may sit and still be exp()'d.
*
* Applied to the *gap* from the leader, never to the score itself. A softmax is
* shift-invariant, so clamping absolute scores is not a safety net — once every
* point is past the limit they all pin to the same value and the sampler quietly
* collapses back to uniform, at exactly the point it has learned the most.
* Clamping the gap keeps the ordering and only bounds the tail.
*/
const GAP_CLAMP = 8;
export interface VoiceFitWeights {
/** Effective sample size behind the weights, after bleeding. */
weight: number;
/** Bled standard deviation of the reward — the unit the weights are read in. */
sdR: number;
/** Point id → its weight. Missing means zero, i.e. unproven. */
weights: Record<string, number>;
}
export function trustFrom(weight: number): number {
return (KAPPA_MAX * weight) / (weight + KAPPA_HALF_LIFE);
}
/**
* One page's worth of selling points.
*
* `rng` is injectable so the tests can assert the distribution rather than
* assert that something random happened.
*/
export function drawPage(fit: VoiceFitWeights, rng: () => number = Math.random): string[] {
// A reward with no spread yet — everyone scored zero, or nothing has matured —
// makes every weight meaningless rather than large. Fall back to uniform.
const sd = fit.sdR > 1e-9 ? fit.sdR : 0;
const kappa = trustFrom(fit.weight);
const remaining = VOICE_POOL.map((point) => point.id);
const chosen: string[] = [];
const wanted = Math.min(VOICE_PER_PAGE, remaining.length);
for (let pick = 0; pick < wanted; pick++) {
const scores = remaining.map((id) => (sd === 0 ? 0 : (kappa * (fit.weights[id] ?? 0)) / sd));
const highest = Math.max(...scores);
let total = 0;
const weighted = scores.map((score) => {
const value = Math.exp(Math.max(-GAP_CLAMP, score - highest));
total += value;
return value;
});
// Mixed towards uniform per draw rather than once over the page, so the
// floor holds for the fourth point as well as the first.
const uniform = 1 / remaining.length;
let roll = rng();
let index = remaining.length - 1;
for (let i = 0; i < remaining.length; i++) {
roll -= (1 - EXPLORE_FLOOR) * (weighted[i]! / total) + EXPLORE_FLOOR * uniform;
if (roll <= 0) {
index = i;
break;
}
}
chosen.push(remaining[index]!);
remaining.splice(index, 1);
}
return chosen;
}
/** The indicator vector a page becomes once it is an observation. */
export function indicators(items: string[], order: string[]): Float64Array {
const x = new Float64Array(order.length);
const seen = new Set(items);
for (let i = 0; i < order.length; i++) {
if (seen.has(order[i]!)) x[i] = 1;
}
return x;
}