Fairness hard-gate (publish-time enforcement)
POST /api/v1/decision-flows/publish runs the fairness hard-gate as a
pre-publish check when the tenant has opted in. If configured
thresholds breach, the publish is blocked with HTTP 422 and a
structured violation report — the new flow version is not written.
Configuration
Set ontenant.settings.fairnessPolicy:
Behavior
- Not configured / disabled — gate is a no-op, publish proceeds.
- Active override (not expired) — gate is skipped, publish proceeds, audit log records the bypass.
- Insufficient samples — gate is skipped (
enforced: false, reason explainssamples < minSampleSize). - Thresholds breached — publish blocked with 422:
- Infrastructure error — fail-open (publish proceeds, warning logged) so a transient DB blip doesn’t block legitimate compliance work.
Sample-source caveats
The gate derives the protected group fromdecision_trace.requestAttributes[sensitiveAttribute] (the canonical
source), falling back to
qualificationResults[*].context.attributes[sensitiveAttribute] for
traces persisted before the request-attributes snapshot migration. A
trace counts as a positive decision when it selected at least one offer.
If fewer than minSampleSize of the last 7 days of traces carry the
sensitive attribute, the gate skips with an “insufficient samples” reason.
Backed by the platform’s fairness hard-gate enforcement helper.
Tiered fairness evaluation
POST /api/v1/fairness/evaluate?metrics=basic|advanced runs the full
fairness pipeline. The query string controls which metrics tier is
returned.
Basic tier (default)
Existing demographic-parity, four-fifths-rule, equal-opportunity, and equalized-odds gap calculations fromlib/fairness/metrics.ts.
Unchanged behavior — every existing caller is bit-identical.
Advanced tier
Adds intersectional analysis + mitigation recommendations fromlib/fairness/advanced.ts:
- Intersectional cells require per-sample
intersectionalGroups: { axisName: groupValue }. The route runs the intersectional evaluator with a default minimum cell size of 10 samples and surfaces the cells plus the worst disparate-impact ratio. - Mitigation recommendations are derived from the report shape (DI ratio, four-fifths violation, equal-opportunity gap) — no extra inputs needed.
- Gini-by-group (
giniByGroup) — the Gini coefficient of each group’s binary decision distribution (computed for groups with more than one sample). - KS-by-group (
ksByGroup) — a two-sample Kolmogorov–Smirnov test of each non-reference group’s decision distribution against the first group’s. Both are returned automatically in the advanced tier — nomodelKeyrequired.
intersectionalGroups, the response includes
advancedAwaitingConfig: ["intersectional: no per-sample intersectionalGroups supplied"]
so operators know why the analysis is empty. No silent fallback.
Auto-run counterfactual fairness + LIME (needs a modelKey)
When metrics=advanced and the body carries a modelKey, the route
resolves a real scorer via resolveScorerForFairness (V1 supports
gradient_boosted only) and auto-runs two more primitives on top of the
basic tier:
lime— runscomputeLimeagainst the scorer when alimeBaselineattribute set is supplied. Fairness-route LIME defaults to200samples (override vialimeOptions.samples).counterfactualFairness— runsevaluateCounterfactualFairnesswhen the request ismode: "inline"and suppliesmodelGroupKey+counterfactualGroup, with per-sampleattributesto score.
advancedAwaitingConfig and the basic tier still returns. Individual-
fairness (Lipschitz) and the DeLong AUC comparison also live in
lib/fairness/advanced.ts but are not auto-run from this route —
callers invoke them directly.
EU AI Act report
POST /api/v1/fairness/report runs the same fairness pipeline and
returns a formatted report:
Body shape mirrors
/evaluate. Optional title + subtitle
override the defaults (“Fairness Assessment Report” / “EU AI Act
Article 10 § 2(f)”).
Audit trail
Every call to/evaluate and /report writes one audit-log row
(action: fairness_evaluate or fairness_report) so DSAR exports
can cite the exact report contents.