safr.← home

safr Compatibility Score — Public Methodology

Version: 0.1 (v0.1 beta methodology) Last updated: 2026-05-24

This document describes how safr computes the Compatibility Score.

The compatibility score is only shown when two profiles are cross-matched. A single individual on safr has a profile (their lab panel + verification tier + context), not a compatibility score. There is no 0–100 number on your own card. Compatibility is by definition a relationship between two parties.

The reference implementation lives in lib/score/ and is closed-source. This document specifies its behaviour precisely enough that any third party can audit the math.


1. Two distinct concepts

Profile (per person)Compatibility (per pair)
What it isThe factual content of one person's safr — tests done, results, dates, verification, contextThe output of comparing two profiles
Number?No top-line number. Just the facts.Yes — a single 0–100 score
When seenOn your own dashboard, and when you share with a partnerOnly when two profiles are compared
WhyA profile is yours. It isn't a grade.Compatibility is the actual decision-support output

This split matters because earlier framings ("personal compatibility score") were semantically wrong (you can't be compatible with no one) and gave users a misleading sense of being graded as an individual.

2. Compatibility score — ranges and meaning

Compatibility is computed when two cards A and B are combined.

RangeLabelMeaning
80–100ComprehensiveBoth profiles recent, well-covered, well-verified, with risk-reduction context
60–79SolidReasonably aligned, one or two gaps to discuss
40–59PartialMaterial gaps in coverage, recency, or verification on one or both sides
20–39LimitedSignificant gaps — partners should ask follow-up questions before acting
0–19InsufficientNot enough combined information to be useful

Labels are advisory only. The numeric score is the source of truth in the data model.

A score of 100 is not a clearance. A score of 30 is not a warning. Both reflect the quality and completeness of the combined picture.

3. Profile metrics — what each side contributes

When two profiles are cross-matched, each side contributes the same internal metrics. These metrics are inputs to the compatibility math. They are never shown to users as standalone scores.

For each card, four factors are computed, each in [0, 1]:


profileMetrics = { completeness, recency, verification, context }

3.1 Completeness C

Tests are grouped into a standard panel and an extended panel.

Standard panel (weight 0.85):

  • HIV (Ag/Ab)
  • Syphilis (RPR or treponemal)
  • Gonorrhea (at minimum: urethral; bonus for pharyngeal + rectal swabs)
  • Chlamydia (same)
  • Hepatitis B (surface antigen)
  • Hepatitis C (antibody)

Extended panel (weight 0.15):

  • HSV-2 (IgG)
  • HSV-1 (IgG) — note: positive prevalence is high and clinically mild
  • Trichomoniasis
  • Mpox (where regional epidemiology warrants)

Each test contributes weight × presence where presence is:

  • 1.0 if tested and result present
  • 0.6 if marked "withheld" (gives partial credit for having tested)
  • 0.0 if missing entirely

Site-of-collection bonus (gono/chlamydia only):

  • Urethral only: 1.0 base
  • Urethral + one other site: × 1.05
  • Urethral + pharyngeal + rectal: × 1.10

Final C is normalised to [0, 1].

3.2 Recency R


R = 1.0                              if days_since_test ≤ 90
R = max(0, 1 - (days - 90) / 275)    if 90 < days ≤ 365
R = 0                                if days > 365

Linear decay from full credit at 90 days to zero at one year. v1.0 with clinical-advisor input will tune per-test decay rates.

3.3 Verification V

TierMultiplier
Self (grey)0.70
Parsed (blue)0.85
Verified (green)1.00
Clinician (gold)1.00

3.4 Context X


X = 1.0
if HIV negative AND on PrEP (adherent): X += 0.04
if HIV positive AND U=U (undetectable):  X += 0.06
if HPV vaccine series complete:          X += 0.02
if Hep B vaccine series complete:        X += 0.02

X = min(X, 1.10)

3.5 Rolled-up "profile strength" S (internal only)

For pairwise math, the four factors are rolled into a single number:


S = round( 100 × C × R × V × X )

This S is computed per side and used as input to the compatibility calculation in §4. It is not shown to users. It exists only inside lib/score/score.ts to make the pairwise formula simple.

A user can always inspect the components (recency status, verification badge, panel coverage list) on their own profile view — but never the single rolled-up number.


4. Pairwise compatibility score

When two cards are combined, the compatibility score P is computed in two stages.

4.1 Base — geometric mean


P_base = sqrt(S_a × S_b)

Geometric mean (rather than arithmetic) penalises asymmetry: a strong profile paired with a sparse one yields ~mid, not high. Confidence is bounded by the weaker side.

4.2 Adjustments

Concordance bonus: when both partners are positive for the same condition where transmission risk between them is materially lower than to a discordant pair (e.g., HSV-2):


P += 3 per concordant condition

Discordance flag (readout only, no score change):

  • HIV discordant where the negative party is not on PrEP → discuss item
  • HSV-2 discordant where negative party hasn't been counselled on
  • suppressive therapy → discuss item

Mutual gap penalty: if both cards lack a result for a standard-panel test, the pair gets -2 per shared gap. You can't fill a gap with another gap.

Both-on-PrEP bonus: if both are HIV-negative and both adherent on PrEP, +2.


P = round(clamp(P_base + adjustments, 0, 100))

4.3 Readout generation

The pairwise readout is generated deterministically from a small set of templates keyed off the same inputs. It always has three sections:

1. Strengths — what the data shows positively 2. Things to talk about — gaps, asymmetries, discordances worth discussing 3. Closing line — invariant: "safr won't tell you what to do. you have the facts. talk it over."

The readout never uses imperative mood ("do X", "avoid Y") — only declarative.


5. What safr deliberately does NOT do

  • Never display a 0–100 score on a single person's profile. No
  • individual grading. Profile = facts, only.

  • No probability claims. We don't say "92% safe" or "8% transmission
  • risk." Those numbers require population priors safr doesn't have.

  • No medical advice. "You should get HSV-2 tested" is advice; "HSV-2
  • not on file" is information. We do the latter.

  • No partner ranking. Two of your connections both at 87 are not
  • "tied" — the score is a measure of information quality across the pair, not a leaderboard.

  • No score history exposure to others. Trends are visible to the
  • card owner only.

  • No automatic disqualification. A pairwise score below any
  • threshold never triggers a "do not share" warning. We surface; users decide.

6. Versioning and audit

Every card stores the scoring version used to compute it (e.g., score_version: "v0.1.0"). If methodology changes, all prior pairwise scores are re-displayed with their original number plus a "scored under v0.1.0" annotation, until refreshed. We never silently re-score.

When a card is shared, the share payload includes the scoring version so a viewer cannot be misled by a stale algorithm.

7. Roadmap

  • v0.2 methodology (with advisory board, ~v1.0 launch):
  • - Per-test recency decay curves - HSV-1 vs HSV-2 weighting refined with clinical input - Mpox handling refined per region - Verified-tier multiplier may be re-evaluated

  • v0.3 methodology (post-launch, ongoing):
  • - Bayesian update where partial data exists - Optional regional prevalence priors for context (off by default — opt-in)

8. Feedback and responsible disclosure

If you find an issue with this methodology — clinical, ethical, or mathematical — please contact methodology@safr.app. Substantive issues are credited (with consent) in this document's changelog.


safr is decision support between consenting adults. A profile is information. Compatibility is information about a pair. Neither is a verdict.