The structural detection gap the red-team enumeration flagged: Kitsune flags hard contradictions (coherence rules) but cannot score "improbable-but-not-impossible" joint field combinations. That is exactly the niespodd / BrowserForge frontier — generators sample a real-traffic joint distribution to produce fingerprints where every field is individually valid and mutually consistent, so no coherence rule fires, yet the combination is one no real user has. A prevalence (likelihood) model is the principled counter: score a fingerprint by how probable its field combination is under the real-traffic prior, and flag the deep-tail improbable ones.
A prototype built a joint prior from 4000 real-distribution fingerprints (conditional frequency tables
for gpu_family | platform, screen | platform, colorDepth | platform, and cores), scored a log-
prevalence per fingerprint, and tested it against scrambled fingerprints — a real fingerprint with its
GPU + screen swapped in from a different one (each field still valid, the joint improbable; the case the
coherence rules miss):
| set | p10 | median | p90 |
|---|---|---|---|
| real (held-out) | −9.4 | −5.6 | −3.7 |
| scrambled (improbable joint) | −19.0 | −10.3 | −4.2 |
At a threshold of the real distribution's 5th percentile (−11.1): 46% of scrambled caught, 5% of real flagged — on just four fields. More fields (timezone, fonts, audio/codec support, UA-CH high-entropy) and proper conditional modeling raise the separation. The class works.
- Coherence rules catch hard contradictions (UA says Windows, TCP says Linux). Binary, high-precision.
- Prevalence model catches soft improbability (a coherent combo that's vanishingly rare). Continuous, corroborating. It is the only thing that scores a statistically-assembled fingerprint with no contradiction.
The prior here is browserforge — a single generated distribution, not measured real traffic. Per the calibration discipline, a single-source prior cannot be a convicting signal: its deep tail may reflect browserforge's own sampling gaps, not true rarity. So the prevalence model ships as a corroborating signal (low weight, amplifies an existing suspicion) until its prior is corroborated against a second source — Tier-3 real-traffic data (a real-device matrix or the hosted-demo opt-in). The mechanism (real vs scrambled separation) is proven; the prior's fidelity is the open item.
The second source must be REAL TRAFFIC, not another generator (evaluated 2026-06-19). The obvious candidates are all the same Bayesian-network generator family and do NOT qualify: apify/fingerprint-suite IS browserforge's upstream (browserforge is "a reimplementation of apify/fingerprint-suite in Python"), and scrapfly/fingerprint-generator is the same daijro model on Scrapfly's data ("Created by daijro, data provided by Scrapfly"). They share the model's conditional-independence structure, so they share its blind spots, and a
fingerprint-injector-class attacker samples this exact network — probable in all of them by construction. Scrapfly's independent data makes it useful only as an overfit diagnostic (diff the two priors; factors where they disagree are browserforge data-artifacts), never as ground truth. Ground truth = fingerprints measured through our own collector (build_prior_from_sessionson a hosted-demo opt-in / real-device matrix). See docs/evasion-catalog.md "Scrapfly fingerprint-generator".
The diagnostic above is now built (kitsune_harness.fpgen_corroborate) and run: it maps fpgen (Scrapfly's
data) through the same features_from_fingerprint the prior is built from and reports the total-variation
distance (TVD) between browserforge's and fpgen's conditional distributions per factor. First run (n≈400–500):
| factor | condition | TVD (0=identical, 1=disjoint) | read |
|---|---|---|
gpu | macOS |
0.06 | robust — agrees across two independent datasets |
gpu | Windows |
0.12 | robust (the _gpu_family extraction survives browserforge's non-ANGLE renderers) |
gpu | Linux |
0.28–0.34 | softest, but low-n (Linux ≈ 32/sample) → noise-dominated |
screen | Windows |
0.11 | robust |
screen | macOS |
0.23 | moderate — the one desktop cell worth scrutiny when real data lands |
screen | Linux |
0.18 | low-n |
cores (marginal) |
0.11 | robust |
Finding: the single-source prior is not wildly data-idiosyncratic — no factor exceeds the 0.35
"high-divergence" flag; the weakest factors I most feared (gpu/cores, which had no independent check) agree
to TVD ≤ 0.12 on the high-n desktop cells. So the corroborating-weight prevalence signal is better-grounded
than pure-single-source would suggest. Honest limits: fpgen is the same model family (independent data,
shared Bayesian structure), so this corroborates the data, not the model's structural assumptions — it
downgrades the single-source risk, it does not retire it. It also cannot compare the Android/mobile
cells (fpgen's natural sample skewed desktop) — those, plus a convicting (not corroborating) promotion,
still require real-traffic-through-our-collector ground truth. The diagnostic is on-demand
(task prevalence-corroborate; fpgen fetches its model over the network, so it is not a CI gate), with its
pure logic unit-tested.
The TVD diagnostic above corroborates the prior's distributions; a separate check on the rule's firing
rate found a real FP. br.fingerprint_improbable fires below the committed threshold, which was browserforge's
self-p1 (−7.73 — the 1% tail of browserforge scored against its own prior). Scoring fpgen (the independent
second source) under that prior, ~5% of fpgen fingerprints fall below −7.73, not 1%: the threshold is
over-fit to browserforge's narrower distribution, so it over-flags any independent (more real-diverse) dataset
~5×. This is the exact single-source-number trap the standing constraint warns about — and although the rule
is corroborating-only (no false bot), it lifts the legit-browser suspicious rate ~5× above the 1% design.
Fix: the threshold is now the conservative cross-source bound min(browserforge-p1, fpgen-p1) = −9.48,
so it flags ≤1% of both distributions (fpgen 5.0% → 0.8%; browserforge drops below the calibration re-tier
cutoff) — strictly FP-safer, never raising the flag rate, and the near-impossible joints still fire (a
one-eps-floor factor like Apple-GPU-on-Windows is ≈ −10, well below −9.48; synthetic + corpus full-vector
improbables unchanged). write_prior now takes a threshold_feats independent calibration set and
build_prior_file calibrates on fpgen by default; the committed threshold is guarded conservative by
test_prevalence.test_committed_threshold_is_cross_source_conservative. The proper end-state remains a
real-traffic prior (build_prior_from_sessions), at which point the threshold is recomputed from real diversity
directly — this fpgen-conservative bound is the FP-safe interim.
A search for public, independent grounding sources for the three factors, and a cross-check against each:
- screen — already grounded. The
screen_bucketsize-class design was cross-checked against the Intoli real-traffic resolution distribution (collapsing a 13–46% exact-resolution miss to ~0%). Not single-source; this is the model for the others. - gpu — no usable public source (the weakest factor stays single-source). The only fetchable independent
GPU distribution is the Steam Hardware Survey, which is NVIDIA-dominant (~70%) vs browserforge's
general-web Windows profile (intel 55% / nvidia 24% / amd 17%). That gap is a population mismatch
(gamers vs general web), not a browserforge error — so Steam is the wrong population to ground a web GPU
prior. A web-representative WebGL-renderer distribution (hosted-demo opt-in) is still required; open
per-sample fingerprint datasets do not exist publicly (confirmed — even anti-detect tooling notes this gap).
Web3DSurvey (web3dsurvey.com) is the right population (a live WebGL survey of general web visitors,
not gamers), but it publishes only WebGL extension/parameter support rates, not a GPU-vendor-share table —
so even it doesn't expose the breakdown. Net: no clean fetchable public web GPU distribution exists; the
gpu factor's grounding is genuinely a hosted-demo capture (which
--build-prior-from-sessionsconsumes, extracting gpu fromwebgl_rendererviafeatures_from_session). - cores — a real-second-source FLAG, not acted on. Mapping the Steam CPU-core survey to our buckets
gives
5-8≈55%,9-16≈11%; browserforge gives5-8=38%,9-16=32%. Steam gamers should skew to more cores yet show fewer9-16than browserforge — i.e. browserforge may over-generate high core counts for general web. Per the over-leverage discipline this is a flag to corroborate against a web-representative source, not grounds to act on one skewed survey (andcoresis corroborating-only, so the FP stakes are low regardless). Logged here for a future Tier-3 confirm.
Net: the clean public grounding sources are exhausted — screen is done (Intoli), gpu/cores need a
web-representative sample that only the hosted-demo opt-in / real-device matrix provides (the turnkey
--build-prior-from-sessions path consumes exactly that). Market-share surveys are independent but
population-skewed, usable only as flags. Sources: StatCounter, Steam Hardware Survey, AmIUnique/ThumbmarkJS
(2025–2026).
Why this matters for power, not just FP-safety (measured): the prior is the SAME generator a
fingerprint-injector-class attacker samples from, so a browserforge-sampled fingerprint is probable in our
prior by construction — same-source-blind. Scoring 800 browserforge fingerprints through the full
browser-layer ruleset gives 80% human, and br.fingerprint_improbable fires on only ~1% (browserforge's
own p1 tail). So an independent prior is needed for detection power against generator-based attacks, not
merely FP-hygiene (see docs/evasion-catalog.md "Prevalence / likelihood model").
Scope limit (grounded via botright, 2026-06-19): an independent real-traffic prior catches improbable
joints (generated or incoherent), but NOT real-value reuse — a tool like botright that drives a fresh
self-scraped REAL fingerprint per session presents joints that are genuinely probable in real traffic, so
no prevalence prior flags them. Prevalence's scope is therefore "improbable joints," not all evasion;
real-value-reuse fleets are the coordination layer's job (fp_collision on reuse, or the leak signals
if each session is a distinct real fp). The prevalence prior and the coordination harness are complementary,
not redundant.
The infrastructure to swap in a real-traffic prior is built and tested — build_prior_from_dir
(harness/browserforge_corpus.py, guarded by tests/test_prior_builder.py). When a Tier-3 capture exists,
one command rebuilds the prior from REAL ground truth and the detector uses it automatically (it loads
data/prevalence_prior.json):
Two builders, by capture shape (both write ../detector/src/kitsune_detector/data/prevalence_prior.json,
which the detector loads automatically):
# PRIMARY real-traffic path — SESSION captures (what the collector+edge produce; a hosted-demo opt-in
# yields these, shape = corpus/sessions/*.json). Uses the detector's own features_from_session, so the
# prior matches what the scorer computes on live sessions:
cd harness && uv run python -m kitsune_harness.browserforge_corpus --build-prior-from-sessions <session-dir>
# ALT path — raw fingerprint dicts (the shape signals_from_fingerprint reads, e.g. a real-device matrix
# exported as fingerprint JSONs):
cd harness && uv run python -m kitsune_harness.browserforge_corpus --build-prior-from-dir <real-fp-dir>Both produce a prior with the identical schema/factors (gpu/screen/cores) the model already loads, so it is a
drop-in replacement — verified (tests/test_prior_builder.py) against the real-engine fingerprints AND real
session captures. The frontier is now data-only-blocked: supply real-traffic captures (a hosted-demo
opt-in is the natural source — its sessions feed --build-prior-from-sessions directly) and the prevalence
model gains power immediately, no code change. Promoting it from corroborating to a higher weight then
becomes defensible — the single-source caveat above is exactly what a real prior removes.
The recurring unlock above ("a prior built from REAL traffic") now has a concrete, published source.
Berke et al., "How Unique is Whose Web Browser?" (PoPETs 2025) released 8,400 consented real browser
fingerprints (Harvard Dataverse, doi.org/10.7910/DVN/0SGZFF) covering Kitsune's exact prevalence
attributes — User agent, Screen resolution, Hardware concurrency, WebGL Unmasked Renderer — i.e. a measured
second source for the gpu/screen/cores factors that until now had none.
harness/berke_corpus.py (build_prior_from_berke) is the consumer of that dataset, not a fetcher:
cd harness && uv run python -m kitsune_harness.berke_corpus survey-and-browser-attributes-data.csvIt maps each CSV row through the detector's own features_from_fingerprint (no parallel bucketing to drift),
then write_priors a drop-in prevalence_prior.json over the same gpu/screen/cores factors. Two design
points matter:
- Aggregate-prior-only, honouring the no-reshare term. The browser-attributes file is research-use only; Berke's terms of use prohibit re-identification and any further sharing of the data. So the module emits only the resulting prior — coarse joint-frequency tables (P(gpu|plat), P(screen|plat), P(cores)) plus the threshold, de-identified aggregate statistics, never the rows. Committing the prior does not redistribute the dataset. An operator who has accepted the terms and downloaded the CSV runs this; the raw rows never leave their machine.
- The threshold becomes the real distribution's self-p1 — a real-traffic prior is the ground truth the browserforge self-p1 (and the fpgen-conservative interim bound) were stand-ins for.
Before swapping the prior, the standing discipline requires checking the current (browserforge) rule against
real traffic — the same second-source methodology as SapiMouse/Intoli/fpgen, here applied to the prevalence
factors. harness/prevalence_real_corroborate.py is that gate:
cd harness && uv run python -m kitsune_harness.prevalence_real_corroborate /path/to/real-fingerprints.csv
# or: task prevalence-real-corroborate -- /path/to/real-fingerprints.csvIt fuzzy-matches the corpus columns to the model roles (prints the mapping to verify before trusting the
result), buckets each row through features_from_fingerprint, and reports three things, all aggregate and
safe to paste back:
- Real-traffic FP rate — what fraction of REAL fingerprints
br.fingerprint_improbablewould flag under the committed (browserforge) prior.~0%⇒ the rule is FP-safe on real traffic ⇒ promotion candidate;promotion_fp_safereports the rate-== 0verdict directly. - Divergence — the top joint cells where browserforge mis-estimates vs real traffic (the single-source bias the rest of this doc has been chasing per-factor).
- Real prior — the bucketed real-traffic prior (an aggregate; publishable — it is not the dataset).
It runs LOCALLY on the operator's copy (raw CSV never leaves the machine; not committed, not a CI gate), so
the promotion decision rests on de-identified aggregates only. This is the concrete answer to "what would let
us raise the rule from corroborating toward convicting": run it on the Berke corpus (or any real-traffic
fingerprint CSV), confirm the real-traffic FP rate is ~0, then rebuild the prior with build_prior_from_berke.
The prototype is now a tested module — harness/prevalence.py (features_from_fingerprint, build_prior,
log_prevalence, pure + unit-tested) — and the prior is a committed, regenerable artifact:
uv run --with browserforge python -m kitsune_harness.browserforge_corpus --build-prior corpus/calibration/prevalence_prior.json --n 5000Validated on three independent inputs: the synthetic improbable joint (Windows UA + Apple GPU + Mac
screen) scores -23.5, far below the real-engine Tier-2 captures (-8.9 … -16.0) — the deep tail is
the improbable combination, exactly as intended.
Wired into the live detector: the collector now emits raw screen_resolution + color_depth (alongside
webgl_renderer, hardware_concurrency, ua_platform); detector/prevalence.py scores the session against
the committed prior (detector/.../data/prevalence_prior.json) and emits browser.prevalence_low below the
prior's p1 threshold; the rule br.fingerprint_improbable (experimental, weight 0.25) fires on it.
task calibrate confirms the legitimate-browser human rate is unchanged at 77% — the rule fires on ~1%
by design at corroborating weight, so it never convicts a clean browser alone. Stays experimental until the
prior is corroborated against Tier-3 real traffic.
Corroborating-only is now enforced structurally, not just by weight (v0.65.0). The rule carries its own
prevalence rule-category, which is deliberately excluded from scoring.CONVICTING_CATEGORIES. After the
convicting-signal gate (v0.64.0), category — not weight — decides whether a tell can unlock a bot label,
and a single-source likelihood signal must not. The latent case this closes, grounded: a real-but-rare
browser (an improbable-but-coherent joint) on a no-webcam desktop — prevalence_low (0.25) ⊕
media_devices_empty (0.55) = 0.66 — crossed the bot threshold with prevalence as the sole convicting
signal. It is now capped at suspicious: the browserforge prior corroborates a suspicion but cannot convict
a legitimate browser on rarity alone until the prior is corroborated against a second source.
The standing rule is "never act on a single-source number." The prevalence rule is calibrated on
browserforge, so calibrating it against browserforge is circular — it cannot reveal a factor that is common
in reality but rare in the generator. Cross-checking the prior's screen | platform factor against the
Intoli real-traffic source (its screen and UA-derived plat fields are faithful — verified in
calibration.md) exposed exactly that:
| platform | real Intoli sessions in a browserforge near-zero (≤eps) exact-WxH screen bucket |
|---|---|
| Windows | 46.2% |
| macOS | 13.2% |
| Linux | 13.5% |
| Android | 1.1% |
A prevalence model with an exact-resolution screen factor would assign deep-tail (log eps) probability to
13–46% of real desktop users — a latent false positive masked entirely by the circular browserforge
calibration. The browserforge generated distribution simply does not cover the long tail of real screen
sizes.
Fix: coarsen the screen feature to (size-class, orientation) — mobile/small/laptop/desktop/large ×
port/land (prevalence.screen_bucket, mirrored in the detector). The same cross-source check on the coarse
buckets drops the real-traffic miss to ~0% (macOS/Android/Linux; Windows ~12.6%, mostly Intoli's own
incoherent UA×screen pairs), while keeping the joint signal — a randomizer pairing a mobile-port screen
with a Windows + nvidia-desktop GPU is still improbable. The exact-WxH FP landmine is removed.
gpu, colour, and cores are low-cardinality and stable, but Intoli does not carry them, so they remain
single-source (browserforge) until a Tier-3 source can corroborate them — which is why the rule stays
experimental / corroborating-only.
A second single-source FP, this one structural rather than per-factor. The committed threshold is the 1st
percentile of full-vector browserforge fingerprints (browserforge_corpus.build_prior_file), so it is
only a meaningful cut-off for a fully-observed feature vector. But is_improbable scored any session with
a platform + GPU, summing log(P + eps) over every factor — and a missing factor contributes
log(eps) ≈ -9.2 nats. A real session that simply didn't emit screen + colour therefore sank ~-18 below
the threshold and tripped on absence, not improbability. On the live corpus this was the dominant cause
of firing: 11 of 14 prevalence_low fires were partial-vector captures (screen=None/colour=None),
already hard-convicted by coherence/automation — the rule was claiming "improbable" on a data gap.
Fix: is_improbable now abstains unless gpu, screen, colour, and cores are all observed —
the registry's "unknown never fires" discipline. This is strictly monotonic (it can only make the rule fire
less), so it cannot raise the legitimate-browser flag rate; the browserforge calibration corpus is
full-vector, so its numbers are unchanged. On the corpus the fires dropped 14 → 3 — the survivors are
the genuine full-vector improbable joints (iframe-spoof, stealth-patched, ios-ua-spoof: a SwiftShader
GPU under a desktop platform with a real screen/colour/cores) — with zero net detection loss (the 11
dropped remain bot). Locked by test_partial_vector_abstains_unknown_never_fires.
The list below was the original roadmap; items 1–3 are now shipped. What is left is genuinely external — an operator accepting a real-traffic dataset's terms and running one local command — not unwritten code.
- Build the prior offline from a real-distribution sample — BUILT. Two consumers exist, both writing the
drop-in
prevalence_prior.jsondata table (rules-as-data stays the coupling):browserforge_corpus.build_prior_from_sessions/_from_dir(collector/edge or real-device-matrix shape) andberke_corpus.build_prior_from_berke(the published PoPETs-2025 8,400-FP CSV). Each emits aggregate joint-frequency tables only — no rows. - Emit the score and fire a conservative-tail rule — BUILT (v0.63.0). The collector emits the raw fields,
detector/prevalence.pyscores against the committed prior, andbr.fingerprint_improbablefires below a cross-source-conservative p1 tail at corroborating weight only (structurally capped atsuspicioussince v0.65.0 — theprevalencerule-category is excluded fromCONVICTING_CATEGORIES). - Gate via calibration against both sources — BUILT.
task calibrateconfirms the legitimate-browser human rate is unchanged;prevalence_real_corroborate(task prevalence-real-corroborate) is the real-traffic promotion gate, measuring the rule's FP rate and per-cell divergence on an operator-supplied real CSV. - The one external item — corroborate the prior against real traffic before raising its weight toward
convicting. This is data-download-blocked, not code-blocked: accept a real-traffic dataset's terms (the
Berke PoPETs-2025 corpus is the ready candidate), run
prevalence_real_corroboratelocally to confirm the real-traffic FP rate is ~0, thenbuild_prior_from_berketo swap in the real prior — at which point promoting the rule from corroborating toward convicting becomes defensible. No code change required.
The screen-factor lesson recurred on colour. color_depth is a display property — 24 (sRGB) or 30
(HDR/wide-gamut) — and is OS-independent: every real browser reports 24 regardless of platform (grounded
on the headful Chromium/Firefox/WebKit captures, all 24; screen.colorDepth does not depend on the OS). But
the browserforge prior generates color_depth=32 for Windows at 93%, so the colour factor charged every
real Windows user (who reports 24) a ~−3 log penalty. Like the exact-WxH screen artifact, it is invisible
to the browserforge calibration — browserforge scores its own generated 32s against a 32-heavy prior, so no
penalty shows; the bias only bites real 24-reporting traffic, which the single source can't represent.
Conditioning a display property on the OS is unsound, and the colour prior is uncorroborable (Intoli
carries no color_depth, and the Tier-2 captures only confirm 24). Per the standing rule — never act on a
single-source number — the factor was removed rather than trusted. The prior and p1 threshold were
regenerated over gpu/screen/cores (threshold −10.75 → −9.04). A real-Windows-like fingerprint's margin
improved (−6.55 → −3.48 vs the threshold) and the corpus label distribution is unchanged (51 bot / 1
suspicious — zero detection loss; the rule is corroborating-only). gpu and cores remain single-source
pending a Tier-3 real-device matrix.
After colour was dropped, cores (hardware_concurrency) was the last exact-value single-source factor —
and it had the same eps-gap FP the exact-WxH screen factor did. browserforge under-samples real cores
diversity and its prior carries gaps + garbage (a 384-cores value), so a real but uncommon-or-high count
took a deep penalty invisible to the browserforge calibration: a real 128-core workstation hit the eps
floor (−9.2) and was flagged improbable on cores alone (below the −9.04 threshold), and a common
6-core CPU took ~−3.7.
Fix: coarsen to size-class buckets — <=2 / 3-4 / 5-8 / 9-16 / 17+ (prevalence._cores_bucket, mirrored
in the harness cores_bucket), so every plausible real count maps to a populated bin and the eps-gap is
gone. After the fix a real Windows + 128-core workstation scores −4.91 (was effectively −9.2 on cores), a
6/8-core −3.32 — both well above the regenerated threshold (−9.04 → −7.73). Corpus label distribution
unchanged (51 bot / 1 suspicious — zero detection loss; the rule is corroborating-only).
Prevalence factor status after this arc: gpu × screen × cores. screen and cores are
coarsened-robust (immune to browserforge's exact-value coverage gaps); colour is dropped (an OS-independent
display property browserforge generated wrong); gpu remains single-source pending a Tier-3 real-device
matrix (it cannot be coarsened — the engine family IS the signal — and Intoli does not carry it).
With screen + cores coarsened and colour dropped, gpu (gpu_family given platform) is the last single-source
factor. It was examined for the same circular-FP class and is mostly sound — unlike cores it has no
eps-gaps for the major pairings: Windows {intel 55%, nvidia 24%, amd 17%}, Linux {intel 41%, amd 31%, nvidia
21%}, Android {mobile 98%} all cover real GPUs well. The one weakness is macOS at 98% apple, which
under-samples Intel Macs (Apple shipped Intel iGPUs on all, plus AMD/NVIDIA discrete GPUs on 15"/16"
MacBook Pro, iMac, Mac Pro): amd|macOS scores −6.35, so a real Intel-Mac-with-Radeon could be pushed to
improbable when combined with another factor.
This is not safely fixable in-sandbox, and the discipline says leave it:
- It cannot be coarsened — the GPU family is the discriminator (apple-on-Windows, swiftshader-on-desktop are the catches); collapsing families destroys the signal.
- It cannot be dropped — it is the prevalence model's primary signal.
- It cannot be corroborated — Intoli carries no GPU, and every in-sandbox real capture is SwiftShader (the container has no real GPU), so the true macOS GPU distribution is unobservable here.
So gpu stays single-source, with the documented Intel-Mac under-sampling, until a Tier-3 real-device
matrix (or hosted-demo opt-in) supplies a measured GPU/platform distribution. This concludes the
prevalence-hardening arc: of the four original factors, screen is cross-checked vs Intoli, cores is
coarsened-robust, colour is dropped, and gpu is the one genuinely-irreducible single-source factor —
which is itself the honest, precise answer to "what does the prevalence model still need from a second
source." (Deployed live at ruleset 0.74.21; IFRAME_SPOOF — swiftshader-on-desktop — still fires
fingerprint_improbable end-to-end, zero detection loss.)
A further GPU FP, of the same class but in the extraction, not the prior. _gpu_family used to map any
unrecognised renderer to a catch-all other family. But real browsers emit renderer strings that classify to
no vendor: software-rendering Firefox generalises its renderer to "llvmpipe, or similar", and
Mullvad/RFP generalises to "Mozilla". browserforge under-generates these, so other|plat sat near the
eps floor — and a real Firefox/Mullvad user took the eps-floor penalty and falsely tripped
br.fingerprint_improbable on an unclassifiable GPU rather than an improbable one. Grounded on the real
Firefox/Mullvad captures (see docs/real-browser-capture-profiles.md).
Fix: _gpu_family now returns None (abstains) for an unclassifiable renderer instead of bucketing
it to other — the registry's "unknown never fires" discipline, the same monotonic abstention that fixed
partial vectors above. With the GPU factor unobserved, the joint abstains rather than charging a penalty it
cannot justify from a single source. The harness features_from_fingerprint and the detector's _gpu_family
are kept byte-for-byte in sync (both: nvidia/apple/intel/amd/mobile/swiftshader, else None), so the prior
built by berke_corpus/browserforge_corpus and the runtime scorer abstain on exactly the same renderers.
The genuine convicting catches (apple-on-Windows, swiftshader-on-desktop) are classified families and still
fire; only the unclassifiable-real tail stops penalising.