Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 113 additions & 0 deletions tests/ui/cohort-floor-demo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// M01 cohort-floor demo widget — verdict-flip computation tests (cp-j0gw.11).

import { describe, expect, it } from 'vitest';
import {
FLOOR_MIN,
FLOOR_MAX,
FLOOR_DEFAULT,
SAMPLE_PROGRAMS,
computeVerdict,
summarizeAt,
detectFlips,
} from '../../web/widgets/cohort-floor-demo.js';

describe('cohort-floor-demo: constants', () => {
it('floor range is 16–30 inclusive', () => {
expect(FLOOR_MIN).toBe(16);
expect(FLOOR_MAX).toBe(30);
expect(FLOOR_DEFAULT).toBe(30);
});

it('illustrative dataset has at least one program at every interesting boundary', () => {
const ns = SAMPLE_PROGRAMS.map((p) => p.n);
expect(Math.max(...ns)).toBeGreaterThanOrEqual(30);
expect(Math.min(...ns)).toBeLessThan(16);
expect(ns.some((n) => n >= 16 && n < 30)).toBe(true);
});
});

describe('cohort-floor-demo: computeVerdict', () => {
it('MEASURED when n >= floor', () => {
expect(computeVerdict({ n: 30, ahead_published: false }, 30)).toBe('MEASURED');
expect(computeVerdict({ n: 100, ahead_published: false }, 30)).toBe('MEASURED');
expect(computeVerdict({ n: 16, ahead_published: false }, 16)).toBe('MEASURED');
});

it('NOT_MEASURED when n < floor and no AHEAD override', () => {
expect(computeVerdict({ n: 22, ahead_published: false }, 30)).toBe('NOT_MEASURED');
expect(computeVerdict({ n: 15, ahead_published: false }, 16)).toBe('NOT_MEASURED');
});

it('MEASURED_OVERRIDE when n in [16, floor) and AHEAD published', () => {
expect(computeVerdict({ n: 22, ahead_published: true }, 30)).toBe('MEASURED_OVERRIDE');
expect(computeVerdict({ n: 16, ahead_published: true }, 30)).toBe('MEASURED_OVERRIDE');
});

it('AHEAD override does not apply below n=16', () => {
expect(computeVerdict({ n: 15, ahead_published: true }, 30)).toBe('NOT_MEASURED');
expect(computeVerdict({ n: 7, ahead_published: true }, 30)).toBe('NOT_MEASURED');
});

it('NOT_MEASURED for invalid n', () => {
expect(computeVerdict({ ahead_published: true }, 30)).toBe('NOT_MEASURED');
expect(computeVerdict({ n: NaN, ahead_published: true }, 30)).toBe('NOT_MEASURED');
});
});

describe('cohort-floor-demo: summarizeAt', () => {
const programs = [
{ id: 'a', n: 38, ahead_published: true },
{ id: 'b', n: 22, ahead_published: true },
{ id: 'c', n: 22, ahead_published: false },
{ id: 'd', n: 7, ahead_published: false },
];

it('partitions programs at floor=30', () => {
const s = summarizeAt(programs, 30);
expect(s.measured.map((p) => p.id)).toEqual(['a']);
expect(s.override.map((p) => p.id)).toEqual(['b']);
expect(s.notMeasured.map((p) => p.id)).toEqual(['c', 'd']);
});

it('partitions programs at floor=16', () => {
const s = summarizeAt(programs, 16);
expect(s.measured.map((p) => p.id)).toEqual(['a', 'b', 'c']);
expect(s.override).toEqual([]);
expect(s.notMeasured.map((p) => p.id)).toEqual(['d']);
});
});

describe('cohort-floor-demo: detectFlips', () => {
it('detects programs that flip MEASURED → NOT_MEASURED as the floor rises', () => {
const programs = [
{ id: 'stable-high', n: 38, ahead_published: false },
{ id: 'flips', n: 22, ahead_published: false },
{ id: 'override-stable', n: 22, ahead_published: true },
{ id: 'stable-low', n: 7, ahead_published: false },
];
const flips = detectFlips(programs, 16, 30);
expect(flips).toHaveLength(2);
const idsByFlip = new Map(flips.map((f) => [f.program.id, f]));
expect(idsByFlip.get('flips')).toMatchObject({ from: 'MEASURED', to: 'NOT_MEASURED' });
expect(idsByFlip.get('override-stable')).toMatchObject({
from: 'MEASURED',
to: 'MEASURED_OVERRIDE',
});
});

it('returns empty when no programs flip', () => {
const programs = [
{ id: 'high', n: 38, ahead_published: false },
{ id: 'low', n: 5, ahead_published: false },
];
expect(detectFlips(programs, 20, 25)).toEqual([]);
});

it('flip set on the SAMPLE_PROGRAMS between floor=16 and floor=30 is non-empty', () => {
const flips = detectFlips(SAMPLE_PROGRAMS, 16, 30);
expect(flips.length).toBeGreaterThan(0);
for (const f of flips) {
expect(f.from).not.toBe(f.to);
}
});
});
11 changes: 11 additions & 0 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ <h3>Check specific programs</h3>
</p>
<a class="cta" href="./persona-a.html">Check specific programs →</a>
</article>

<article class="persona-card">
<h3>See how the 30-completer floor works</h3>
<p>
Drag a slider between 16 and 30 and watch which illustrative
programs flip between MEASURED and NOT MEASURED. Demonstrates
M01 — the cohort-expansion rule — with synthetic completer
counts; no institutional data leaves your browser.
</p>
<a class="cta" href="./widgets/cohort-floor-demo.html">Try the cohort-floor demo →</a>
</article>
</section>

<section class="ground-rules">
Expand Down
161 changes: 161 additions & 0 deletions web/widgets/cohort-floor-demo.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/* M01 cohort-floor demo widget — page-specific styling. */

.public-data-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
background: #e6f4ea;
color: #14532d;
padding: 0.35rem 0.75rem;
border-radius: 999px;
font-size: 0.85rem;
font-weight: 600;
margin-top: 0.75rem;
}

.public-data-badge .badge-dot {
color: #16a34a;
font-size: 0.65rem;
}

.top-disclaimer {
background: #fff8e6;
border-left: 4px solid #c08a00;
padding: var(--space-sm) var(--space-md);
margin: 0 var(--space-md) var(--space-md);
border-radius: 4px;
font-size: 0.95rem;
line-height: 1.45;
max-width: var(--max-content-width);
}

.top-disclaimer p { margin: 0; }
.top-disclaimer strong { font-weight: 700; }

.cf-page {
max-width: var(--max-content-width);
margin: 0 auto;
padding: var(--space-md);
}

.cf-control,
.cf-results,
.cf-caption {
background: var(--color-card);
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: var(--space-md);
margin-bottom: var(--space-md);
}

.cf-control h2 {
margin-top: 0;
}

.cf-slider-label {
display: block;
font-size: 0.9rem;
color: var(--color-text-muted);
margin-bottom: var(--space-sm);
}

.cf-slider {
width: 100%;
height: 2.25rem;
cursor: pointer;
}

.cf-scale {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: var(--color-text-muted);
margin-top: var(--space-xs);
}

.cf-counts {
display: flex;
flex-wrap: wrap;
gap: var(--space-sm);
margin: var(--space-md) 0 0;
font-size: 0.9rem;
}

.cf-count {
padding: 0.15rem 0.5rem;
border-radius: 999px;
font-weight: 600;
}

.cf-count-measured {
background: var(--color-pass-bg);
color: var(--color-pass-band);
}

.cf-count-override {
background: var(--color-noise-band-bg);
color: var(--color-noise-band-band);
}

.cf-count-not-measured {
background: var(--color-not-measured-bg);
color: var(--color-not-measured-band);
}

.cf-list {
list-style: none;
padding: 0;
margin: 0;
}

.cf-row {
display: grid;
grid-template-columns: 1fr auto auto;
gap: var(--space-sm);
align-items: baseline;
padding: var(--space-sm) var(--space-sm);
border-bottom: 1px solid var(--color-border);
font-size: 0.95rem;
}

.cf-row:last-child {
border-bottom: none;
}

.cf-row-label { font-weight: 500; }
.cf-row-n { color: var(--color-text-muted); font-variant-numeric: tabular-nums; }
.cf-row-verdict {
font-weight: 700;
font-size: 0.85rem;
letter-spacing: 0.02em;
padding: 0.1rem 0.5rem;
border-radius: var(--radius);
}

.cf-measured .cf-row-verdict {
background: var(--color-pass-bg);
color: var(--color-pass-band);
}

.cf-measured-override .cf-row-verdict {
background: var(--color-noise-band-bg);
color: var(--color-noise-band-band);
}

.cf-not-measured .cf-row-verdict {
background: var(--color-not-measured-bg);
color: var(--color-not-measured-band);
}

.cf-caption h2 { margin-top: 0; }

.cf-footer {
font-size: 0.9rem;
color: var(--color-text-muted);
margin-top: var(--space-md);
}

@media (prefers-color-scheme: dark) {
.public-data-badge { background: #14321b; color: #b6e8c4; }
.top-disclaimer { background: #2a2410; border-left-color: #d6a400; color: #f4e8c2; }
}
50 changes: 50 additions & 0 deletions web/widgets/cohort-floor-demo.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Type declarations for web/widgets/cohort-floor-demo.js (cp-j0gw.11).

export const FLOOR_MIN: 16;
export const FLOOR_MAX: 30;
export const FLOOR_DEFAULT: 30;

export interface SampleProgram {
id: string;
label: string;
n: number;
ahead_published: boolean;
}

export const SAMPLE_PROGRAMS: ReadonlyArray<SampleProgram>;

export type Verdict = 'MEASURED' | 'MEASURED_OVERRIDE' | 'NOT_MEASURED';

export function computeVerdict(
program: { n?: number; ahead_published?: boolean } | null | undefined,
floor: number,
): Verdict;

export interface FloorSummary<P = SampleProgram> {
floor: number;
measured: P[];
override: P[];
notMeasured: P[];
}

export function summarizeAt<P extends { n: number; ahead_published: boolean }>(
programs: ReadonlyArray<P>,
floor: number,
): FloorSummary<P>;

export interface FlipRecord<P = SampleProgram> {
program: P;
from: Verdict;
to: Verdict;
}

export function detectFlips<P extends { n: number; ahead_published: boolean }>(
programs: ReadonlyArray<P>,
fromFloor: number,
toFloor: number,
): Array<FlipRecord<P>>;

export function initCohortFloorDemo(
rootEl: HTMLElement | null,
programs?: ReadonlyArray<SampleProgram>,
): void;
Loading
Loading