+ Drag a slider between 16 and 30 and watch which illustrative
+ programs flip between MEASURED and NOT MEASURED. Demonstrates
+ M01 — the cohort-visibility cascade — with synthetic completer
+ counts; no institutional data leaves your browser.
+
+ Move the slider between 16 and 30 and watch which illustrative
+ programs flip between MEASURED and NOT MEASURED. The 30-completer
+ statutory floor (NPRM § 668.403(d)(1)) and the AHEAD count_wne_p4
+ single-window threshold (16) define the operative gap.
+
+
+ ●
+ Illustrative data only · No institutional data leaves your browser
+
+ AHEAD's count_wne_p4 is single-window — a count of
+ completers in one award year matched to year-4 earnings. OBBBA's
+ statutory 30-floor (HEA § 454(c)(4)(A)) is for the
+ pooled measurement cohort assembled under NPRM § IX.GD
+ (raw.txt 2272–2344), one award year added at a time across years
+ 5–8 prior to the earnings year.
+
+
+ The AHEAD-published-flag override (cp-wssr) honors
+ verdicts in the 16 ≤ n < 30 gap when AHEAD already published a
+ flag for the program. That override is why some programs in this
+ demo stay MEASURED at floor=30 even though their pooled cohort
+ sits below 30.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/widgets/cohort-floor-demo.js b/web/widgets/cohort-floor-demo.js
new file mode 100644
index 0000000..c9e00e8
--- /dev/null
+++ b/web/widgets/cohort-floor-demo.js
@@ -0,0 +1,152 @@
+// M01 cohort-floor demo widget — verdict-flip logic + DOM wiring.
+//
+// Model (cp-j0gw.11 spec):
+// - Slider chooses a cohort floor in the range [16, 30].
+// - 16 = the AHEAD count_wne_p4 single-window publication threshold.
+// - 30 = the OBBBA statutory floor for the pooled measurement cohort
+// (NPRM § 668.403(d)(1); HEA § 454(c)(4)(A)).
+// - For each illustrative program, the verdict at a given floor is:
+// MEASURED if n >= floor
+// MEASURED if n < floor AND ahead_published === true AND n >= 16
+// (the cp-wssr AHEAD-published-flag override:
+// when AHEAD already published a verdict in the
+// 16 <= n < 30 gap, the override honors it)
+// NOT MEASURED otherwise
+//
+// The data set is illustrative and labeled as such on the page — no
+// institution-specific completer counts.
+
+export const FLOOR_MIN = 16;
+export const FLOOR_MAX = 30;
+export const FLOOR_DEFAULT = 30;
+
+/** Illustrative programs (synthetic, labeled "illustrative" on the page). */
+export const SAMPLE_PROGRAMS = Object.freeze([
+ { id: 'p01', label: 'Program A — 4-yr, CIP 50.07', n: 38, ahead_published: true },
+ { id: 'p02', label: 'Program B — 4-yr, CIP 50.05', n: 32, ahead_published: true },
+ { id: 'p03', label: 'Program C — 4-yr, CIP 50.06', n: 28, ahead_published: true },
+ { id: 'p04', label: 'Program D — 4-yr, CIP 50.07', n: 25, ahead_published: true },
+ { id: 'p05', label: 'Program E — 4-yr, CIP 50.09', n: 22, ahead_published: true },
+ { id: 'p06', label: 'Program F — 4-yr, CIP 50.04', n: 20, ahead_published: false },
+ { id: 'p07', label: 'Program G — 4-yr, CIP 50.0102', n: 18, ahead_published: true },
+ { id: 'p08', label: 'Program H — 4-yr, CIP 50.10', n: 17, ahead_published: false },
+ { id: 'p09', label: 'Program I — 4-yr, CIP 50.0901', n: 16, ahead_published: true },
+ { id: 'p10', label: 'Program J — 4-yr, CIP 50.06', n: 14, ahead_published: false },
+ { id: 'p11', label: 'Program K — 4-yr, CIP 50.07', n: 11, ahead_published: false },
+ { id: 'p12', label: 'Program L — 4-yr, CIP 50.05', n: 7, ahead_published: false },
+]);
+
+/**
+ * Compute the verdict for one program at a given cohort floor.
+ *
+ * @param {{n:number, ahead_published:boolean}} program
+ * @param {number} floor integer in [FLOOR_MIN, FLOOR_MAX]
+ * @returns {'MEASURED'|'MEASURED_OVERRIDE'|'NOT_MEASURED'}
+ */
+export function computeVerdict(program, floor) {
+ if (!Number.isFinite(program?.n)) return 'NOT_MEASURED';
+ if (program.n >= floor) return 'MEASURED';
+ if (program.ahead_published === true && program.n >= FLOOR_MIN) {
+ return 'MEASURED_OVERRIDE';
+ }
+ return 'NOT_MEASURED';
+}
+
+/**
+ * Categorize all programs at a given floor into measured / override / not-measured.
+ * @param {ReadonlyArray<{id:string,label:string,n:number,ahead_published:boolean}>} programs
+ * @param {number} floor
+ */
+export function summarizeAt(programs, floor) {
+ const measured = [];
+ const override = [];
+ const notMeasured = [];
+ for (const p of programs) {
+ const v = computeVerdict(p, floor);
+ if (v === 'MEASURED') measured.push(p);
+ else if (v === 'MEASURED_OVERRIDE') override.push(p);
+ else notMeasured.push(p);
+ }
+ return { floor, measured, override, notMeasured };
+}
+
+/**
+ * Detect programs whose verdict differs between two floor settings.
+ * Returns an array of { program, from, to } records, ordered as in the input.
+ */
+export function detectFlips(programs, fromFloor, toFloor) {
+ const flips = [];
+ for (const p of programs) {
+ const from = computeVerdict(p, fromFloor);
+ const to = computeVerdict(p, toFloor);
+ if (from !== to) flips.push({ program: p, from, to });
+ }
+ return flips;
+}
+
+/* ─── DOM wiring ───────────────────────────────────────────────────── */
+
+const VERDICT_LABELS = {
+ MEASURED: 'MEASURED',
+ MEASURED_OVERRIDE: 'MEASURED (AHEAD override)',
+ NOT_MEASURED: 'NOT MEASURED',
+};
+
+function renderProgramRow(program, verdict) {
+ const row = document.createElement('li');
+ row.className = `cf-row cf-${verdict.toLowerCase().replace(/_/g, '-')}`;
+ row.innerHTML = `
+ ${program.label}
+ n=${program.n}
+ ${VERDICT_LABELS[verdict]}
+ `;
+ return row;
+}
+
+function render(rootEl, summary) {
+ const slot = rootEl.querySelector('[data-cf-list]');
+ if (!slot) return;
+ slot.innerHTML = '';
+ for (const p of [...summary.measured, ...summary.override, ...summary.notMeasured]) {
+ const v = computeVerdict(p, summary.floor);
+ slot.appendChild(renderProgramRow(p, v));
+ }
+ const counters = rootEl.querySelector('[data-cf-counts]');
+ if (counters) {
+ counters.innerHTML = `
+ ${summary.measured.length} measured
+ ${summary.override.length} AHEAD override
+ ${summary.notMeasured.length} not measured
+ `;
+ }
+ const floorLabel = rootEl.querySelector('[data-cf-floor]');
+ if (floorLabel) floorLabel.textContent = String(summary.floor);
+}
+
+/**
+ * Initialize the widget against a root element. Idempotent; safe to call once.
+ * @param {HTMLElement} rootEl
+ * @param {ReadonlyArray} programs
+ */
+export function initCohortFloorDemo(rootEl, programs = SAMPLE_PROGRAMS) {
+ if (!rootEl) return;
+ const slider = rootEl.querySelector('[data-cf-slider]');
+ if (!slider) return;
+ const apply = () => {
+ const raw = Number(slider.value);
+ const floor = Number.isFinite(raw)
+ ? Math.min(FLOOR_MAX, Math.max(FLOOR_MIN, Math.round(raw)))
+ : FLOOR_DEFAULT;
+ render(rootEl, summarizeAt(programs, floor));
+ };
+ slider.addEventListener('input', apply);
+ slider.addEventListener('change', apply);
+ apply();
+}
+
+if (typeof window !== 'undefined' && typeof document !== 'undefined') {
+ document.addEventListener('DOMContentLoaded', () => {
+ const rootEl = document.querySelector('[data-cf-root]');
+ if (rootEl) initCohortFloorDemo(rootEl);
+ });
+}
From 38e236cbfd7f458c05a4e25f12104a207ea148bc Mon Sep 17 00:00:00 2001
From: obsidian <150540200+AJBcoding@users.noreply.github.com>
Date: Tue, 5 May 2026 05:30:55 -0700
Subject: [PATCH 2/2] lint: replace cascade literal in widget link text
(cp-j0gw.17)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
design.v6 §13 forbids the literal "cascade" in dean-facing UI surfaces.
Two surfaces in the cp-j0gw.11 widget commit tripped the linter:
- web/widgets/cohort-floor-demo.html footer link text
"M01 — Cohort visibility cascade" → "M01 — The cohort-expansion rule"
(matches the existing M01 panel header in web/learn.html on
cp-0on-ext-senate-plain-lang).
- web/index.html persona-card body
"the cohort-visibility cascade" → "the cohort-expansion rule"
(same substitute).
Verification:
- npx tsx scripts/lint-glossary.ts content web → no violations
- npm test → 209/209 passing on this branch
---
web/index.html | 2 +-
web/widgets/cohort-floor-demo.html | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/web/index.html b/web/index.html
index 94515c6..15b1b6f 100644
--- a/web/index.html
+++ b/web/index.html
@@ -48,7 +48,7 @@
See how the 30-completer floor works
Drag a slider between 16 and 30 and watch which illustrative
programs flip between MEASURED and NOT MEASURED. Demonstrates
- M01 — the cohort-visibility cascade — with synthetic completer
+ M01 — the cohort-expansion rule — with synthetic completer
counts; no institutional data leaves your browser.
Try the cohort-floor demo →
diff --git a/web/widgets/cohort-floor-demo.html b/web/widgets/cohort-floor-demo.html
index 2a2ce8f..7effe5c 100644
--- a/web/widgets/cohort-floor-demo.html
+++ b/web/widgets/cohort-floor-demo.html
@@ -92,7 +92,7 @@