package eeo import ( "context" "github.com/jackc/pgx/v5" ) // AdverseImpactForSelection recomputes the selection stage four fifths analysis for a caller PROVIDED selection // set, instead of the recorded approve decision. It is how the display order re ranker's promotion gate checks // the four fifths ratio UNDER a challenger's ranking: the caller (running as the migrator, with NO demographic // access) computes which candidates a ranking would surface (the top of each slate) and passes ONLY their ids; // this method, through the compliance pool, joins those ids to the consented demographics and computes the // impact ratios. The ranking features never touch the eeo schema and the demographics never touch the ranking // path; only an id keyed selection set crosses. selected maps a candidate id to whether the ranking would // select that candidate. It runs inside a tenant tx so RLS scopes the slate membership to the caller's org and // the demographics stay reachable only through the compliance pool. func (s *Service) AdverseImpactForSelection(ctx context.Context, selected map[string]bool) (AnalysisSection, error) { if !s.compliance.Ready() { return AnalysisSection{}, ErrNoCompliance } var section AnalysisSection err := s.compliance.RunInTenantTx(ctx, func(ctx context.Context, tx pgx.Tx) error { rows, e := s.selectionCandidateRows(ctx, tx, selected) if e != nil { return e } noDemo, e := countQuery(ctx, tx, selectionNoDemoQuery) if e != nil { return e } section = analyze("selection (challenger ranking)", rows, noDemo) return nil }) if err != nil { return AnalysisSection{}, err } return section, nil } // selectionCandidateRows reads the consented demographics of every candidate with a decided slate membership in // the caller's org, keyed by candidate id, and marks each selected per the provided set (selection under the // challenger ranking), never the recorded decision. It is the ranking analogue of stageRows over selectionQuery. func (s *Service) selectionCandidateRows(ctx context.Context, tx pgx.Tx, selected map[string]bool) ([]candRow, error) { rows, err := tx.Query(ctx, selectionCandidatesQuery) if err != nil { return nil, err } defer rows.Close() var out []candRow for rows.Next() { var candidateID string var r candRow if err := rows.Scan(&candidateID, &r.gender, &r.race, &r.genderUnknown, &r.raceUnknown); err != nil { return nil, err } r.selected = selected[candidateID] out = append(out, r) } return out, rows.Err() } // selectionCandidatesQuery returns the latest consented demographics of each candidate with a decided slate // membership in the caller's org, plus the candidate id so the caller's ranking selection can be applied in Go. // It mirrors selectionQuery but leaves the selected flag to the caller (the challenger ranking) instead of the // recorded decision. RLS scopes slate_members to the org; eeo.candidate_demographics is compliance only. const selectionCandidatesQuery = ` WITH latest AS ( SELECT DISTINCT ON (candidate_id) candidate_id, COALESCE(NULLIF(gender, ''), 'unknown') AS gender, COALESCE(NULLIF(race_ethnicity, ''), 'unknown') AS race, (NULLIF(gender, '') IS NULL) AS gender_unknown, (NULLIF(race_ethnicity, '') IS NULL) AS race_unknown FROM eeo.candidate_demographics WHERE consent_given = true ORDER BY candidate_id, collected_at DESC ) SELECT l.candidate_id::text, l.gender, l.race, l.gender_unknown, l.race_unknown FROM latest l JOIN slate_members sm ON sm.candidate_id = l.candidate_id WHERE sm.decision IN ('approved', 'rejected') GROUP BY l.candidate_id, l.gender, l.race, l.gender_unknown, l.race_unknown` // MinImpactRatio returns the smallest four fifths impact ratio across every group in the section (gender, race, // and intersectional), ignoring the not applicable rows, so a caller can compare the fairness of two rankings // with one number. It returns 1.0 when the section has no comparable group (nothing to flag). A returned value // below 0.80 means at least one group is in adverse impact under this selection. func (a AnalysisSection) MinImpactRatio() float64 { min := 1.0 seen := false for _, group := range [][]ImpactRow{a.ByGender, a.ByRace, a.Intersectional} { for _, r := range group { if r.ImpactRatio == "n/a" || r.ImpactRatio == "" { continue } v := decimalOrZero(r.ImpactRatio) f, _ := v.Float64() if !seen || f < min { min = f seen = true } } } if !seen { return 1.0 } return min }