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
9 changes: 8 additions & 1 deletion src/api/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,14 @@ const $defs = {
fired: { type: 'boolean' },
provenance: {
oneOf: [
{ type: 'string', enum: ['gap_tool_derived', 'earnings_suppressed_ppd_published'] },
{
type: 'string',
enum: [
'gap_tool_derived',
'earnings_suppressed_ppd_published',
'cohort_suppressed_ppd_published',
],
},
{ type: 'null' },
],
},
Expand Down
19 changes: 18 additions & 1 deletion src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,31 @@ function buildNoiseBandAnnotation(
if (surfacedVerdict === 'NOT MEASURED') {
return { fired: false, provenance: null, message: null };
}
if (program.suppression.earn_suppressed || program.median_earn_p4 === null) {
// Privacy-suppressed earnings cell — the federal privacy rule withheld
// the cell-level value. Verdict still surfaces from the published OBBBA
// flag, but the noise-band test cannot run because there is no gap to
// threshold. Distinct from cohort-level suppression below.
if (program.suppression.earn_suppressed) {
return {
fired: true,
provenance: 'earnings_suppressed_ppd_published',
message:
'Verdict is PPD-published; underlying earnings cell suppressed under federal privacy rule. Noise-band test is structurally inapplicable.',
};
}
// Cohort-level suppression — earnings not published at the 4-digit grain
// (typically because the cell's cohort is below the floor or the cell
// was not published at 4-digit despite the verdict being published via
// 2-digit pool flag). Per SPEC-DELTA §2.3 four-layer suppression, this
// is a structurally different cause from the privacy-rule case above.
if (program.median_earn_p4 === null) {
return {
fired: true,
provenance: 'cohort_suppressed_ppd_published',
message:
'Verdict is PPD-published; underlying earnings not published at 4-digit grain (cohort below floor or otherwise unavailable). Noise-band test is structurally inapplicable.',
};
}
if (program.ep_gap_pct === null) {
return { fired: false, provenance: null, message: null };
}
Expand Down
23 changes: 22 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,31 @@ export interface CrossValidation {

export interface NoiseBandAnnotation {
fired: boolean;
/** Provenance string per chair greenlight (SPEC-DELTA §2.2). */
/**
* Provenance string per chair greenlight (SPEC-DELTA §2.2).
*
* - `gap_tool_derived` — verdict's noise-band gap was computed from
* PPD-published earnings + benchmark; the percentage gap itself
* is NOT a published PPD field.
* - `earnings_suppressed_ppd_published` — verdict surfaces from the
* PPD-published OBBBA flag, but the underlying earnings cell is
* suppressed under the federal privacy rule (`earn_suppressed=true`).
* Noise-band test is structurally inapplicable.
* - `cohort_suppressed_ppd_published` — verdict surfaces from the
* PPD-published OBBBA flag, but the underlying earnings cell is
* not published because the cohort is below the floor or otherwise
* unavailable at 4-digit grain (`median_earn_p4 IS NULL` while the
* privacy-suppression flag is NOT the cause). Noise-band test is
* structurally inapplicable for a cohort-level reason rather than a
* privacy-rule reason; the distinction matters for explainability
* per SPEC-DELTA §2.3 four-layer suppression model.
* - `null` — annotation not fired (verdict NOT MEASURED, gap outside
* threshold, or no published gap to evaluate).
*/
provenance:
| 'gap_tool_derived'
| 'earnings_suppressed_ppd_published'
| 'cohort_suppressed_ppd_published'
| null;
message: string | null;
}
Expand Down
81 changes: 81 additions & 0 deletions tests/engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,87 @@ describe('Engine — noise-band suppressed-earnings annotation', () => {
expect(v.noise_band.fired).toBe(true);
expect(v.noise_band.provenance).toBe('earnings_suppressed_ppd_published');
});

// cp-j0gw.14 / R19 fourth annotation case (SPEC-DELTA §2.2).
// When the published OBBBA verdict surfaces but the underlying earnings
// are not published at the 4-digit grain for a cohort-level reason
// (median_earn_p4 IS NULL with earn_suppressed=false — typically cohort
// below floor or pool-inherited verdict), the noise-band test is still
// structurally inapplicable but for a different reason than the
// privacy-rule case above. The annotation must distinguish so the UI
// can explain WHY the noise band did not run, per SPEC-DELTA §2.3
// four-layer suppression model.
describe('fourth annotation case — cohort_suppressed_ppd_published', () => {
it('fires when verdict published but earnings null without privacy-rule suppression', () => {
const inst = makeInstitution([
makeProgram({
median_earn_p4: null,
ep_gap_pct: null,
ppd_fail_obbb: 1,
suppression: { cohort_suppressed: true, earn_suppressed: false },
}),
]);
const r = analyzeInstitution(inst);
const v = r.programs[0]!;
expect(v.verdict).toBe('FAIL');
expect(v.noise_band.fired).toBe(true);
expect(v.noise_band.provenance).toBe('cohort_suppressed_ppd_published');
expect(v.noise_band.message).toMatch(/cohort below floor|not published at 4-digit/i);
});

it('keeps earnings_suppressed_ppd_published when earn_suppressed=true even if median_earn_p4 is null', () => {
// earn_suppressed wins over the cohort branch — the privacy-rule
// explanation is the more specific and user-relevant reason.
const inst = makeInstitution([
makeProgram({
median_earn_p4: null,
ep_gap_pct: null,
ppd_fail_obbb: 1,
suppression: { earn_suppressed: true, cohort_suppressed: true },
}),
]);
const r = analyzeInstitution(inst);
const v = r.programs[0]!;
expect(v.noise_band.provenance).toBe('earnings_suppressed_ppd_published');
});

it('does not fire when surfaced verdict is NOT MEASURED', () => {
// When the cell never surfaces a verdict, no annotation fires —
// regardless of which suppression flag is set.
const inst = makeInstitution([
makeProgram({
median_earn_p4: null,
ep_gap_pct: null,
ppd_fail_obbb: null,
benchmark: null,
suppression: { cohort_suppressed: true },
}),
]);
const r = analyzeInstitution(inst);
const v = r.programs[0]!;
expect(v.verdict).toBe('NOT MEASURED');
expect(v.noise_band.fired).toBe(false);
expect(v.noise_band.provenance).toBeNull();
});

it('does not fire when earnings are present (preserves gap_tool_derived path)', () => {
// Backward-compat sanity: the existing measurable-with-noise-band path
// still produces gap_tool_derived; the new fourth case only branches
// on median_earn_p4 IS NULL.
const inst = makeInstitution([
makeProgram({
median_earn_p4: 38270.0,
ep_gap_pct: 0.0606,
ppd_fail_obbb: 0,
suppression: { cohort_suppressed: false, earn_suppressed: false },
}),
]);
const r = analyzeInstitution(inst);
const v = r.programs[0]!;
expect(v.noise_band.fired).toBe(true);
expect(v.noise_band.provenance).toBe('gap_tool_derived');
});
});
});

// ─────────────────────────────────────────────────────────────────────────────
Expand Down
Loading