ADR-0023: Neutral-prior fix A(iii) — deduction rule baseline re-anchored to 50
Status: Superseded (2026-05-24 — see ADR-0026) Supersedes: — Superseded by: ADR-0026 (baseline anchor → 100, k=45)
Context
ADR-0012 established the neutral-prior scoring model: every institution starts at 50, bad evidence moves the score down from 50, good evidence moves it up from 50, no evidence leaves it at 50.
ADR-0022 (A(ii), committed 2026-05-23) fixed the positive-rule half: positive-type rules that are scraped but return no positive hits now OMIT from the pillar weighted mean instead of scoring 0. Deduction rules were preserved byte-identically in that commit.
After A(ii) the distribution (run_id=41, 79 institutions) revealed the mirror-image bug on the deduction side: institutions with 1–2 violations scored 55–80, above the 50 neutral point. Specifically: - Invesco Ltd. (1 FOREST500 deforestation-financier hit, all other rules OMIT): ESG = 80.0 - BlackRock (E7 + S6 violations): ESG = 68.6
This inverts the methodology: bad evidence (a documented violation) should move the score below 50, not above it. A violation is not a positive signal.
Root cause
Deduction rules have base_score=5. The existing scoreValue = rawScore * 20 formula maps the
full 0–5 raw-score range to 0–100:
clean (no violations) → rawScore=5 → scoreValue=100 (OMIT'd via Case B → 50 effective)
1 violation → rawScore=4 → scoreValue=80 ← ABOVE neutral — BUG
2 violations → rawScore=3 → scoreValue=60 ← ABOVE neutral — BUG
3 violations → rawScore=2 → scoreValue=40 ← first sub-50 score
4 violations → rawScore=1 → scoreValue=20
5 violations → rawScore=0 → scoreValue=0
The deduction scale was anchored at 100, not 50. Three violations were required before the score fell below neutral. The deduction model was implementing ceiling-and-subtract footing, not neutral-prior.
ADR-0022's Case B (clean deduction → OMIT) masked the worst of this during low-traffic runs, but once A(ii) removed the positive-rule floor effect, the above-neutral deduction scores became the dominant signal for any institution with violations.
Decision
A(iii): Re-anchor deduction rule scoreValue to the 50 neutral baseline.
For deduction rules (rule_type = 'deduction'), replace the scoreValue = rawScore * 20 formula
with:
scoreValue = rule_type === 'deduction'
? Math.max(0, rawScore * 20 - 50)
: rawScore * 20;
The arithmetic: rawScore * 20 - 50 shifts the entire deduction scale down by 50 points, so the
clean-case anchor moves from 100 to 50. The Case B OMIT still fires before this formula is reached
(when pointsSum === 0), so the "clean" path is unchanged. The per-violation step magnitude is
preserved at 20 sub-criterion score points per raw-point deducted — only the reference anchor
changes.
New deduction mapping:
clean (no violations) → Case B OMIT → 50 effective (unchanged)
1 violation → rawScore=4 → scoreValue=30 ← below neutral ✓
2 violations → rawScore=3 → scoreValue=10 ← below neutral ✓
3+ violations → rawScore≤2 → scoreValue=0 ← floor (no further differentiation)
What is NOT changed
- Per-violation raw-point magnitude: each violation still deducts 1 from rawScore; the
pointsvalues in theruletable are untouched. - Per-violation sub-criterion score step: each raw point still equals 20 sub-criterion score units; only the reference point moved from 100 to 50.
- Case B (clean deduction → OMIT): byte-identical; this commit does not touch the
pointsSum=0guard. - Positive-rule path (Case C and normal positive): byte-identical.
- Score floor / ceiling on rawScore: unchanged (
base_score=5,floor=0,ceiling=5). - Per-source/per-violation weighting (FOREST500 vs Google-News vs BankTrack relative severity): out of scope; that is the obs-1 scraper-sufficiency review.
×20 / −50 coupling — load-bearing dependency
The −50 in rawScore * 20 − 50 is an absolute offset on the 0–100 scale and is correct
only because the scale factor is ×20. They are coupled:
rawScore=5(clean) →5 * 20 − 50 = 50← correct neutral anchor- If ×20 were changed to ×10:
5 * 10 − 50 = 0← wrong; clean would floor at 0 - If ×20 were changed to ×15:
5 * 15 − 50 = 25← wrong; clean would anchor at 25, not 50
The correct formula for any scale factor k is rawScore * k − (ceiling * k / 2), where
ceiling = base_score = 5 and the divisor 2 centres the range at 50. At k=20 this is
rawScore * 20 − 50.
Do not change the ×20 scale factor without updating the −50 offset proportionally. A "tidy up the constant" change to ×20 that leaves −50 unchanged will silently break the deduction baseline. This coupling is the only place in the scoring stack where ×20 is not cosmetic — see below.
×20 is otherwise cosmetic
Outside this deduction re-anchor, ×20 is a uniform 0–5 → 0–100 scale conversion that cancels
in all downstream weighted means (score_pillar, score_stage1_esg, score_stage2_composite).
Relative rankings are governed by the real weights (rule.points, rule.rule_weight,
blend_weight). ×20 does not amplify or suppress any institution's advantage over another.
Its only non-cosmetic role is in this deduction re-anchor, where the −50 offset is absolute on
the post-scaled values.
Per-violation magnitude — weights legitimate, calibration open
The magnitude of a single E7 violation (→ composite ~39 in the current distribution) is a
function of the real weights (rule.points, rule.rule_weight, blend_weight). These weights
are legitimate — they come from the v0.4 workbook methodology — but whether their values are
correctly calibrated for the intended violation severity is a separate question. That
calibration review is reserved for the obs-1 per-source-weighting review and is not addressed
here. This ADR only re-anchors the baseline; it does not retune what each violation is worth.
Affected rule IDs
All deduction rules — those where rule_type = 'deduction' in the rule table:
| Scope | Rule IDs |
|---|---|
| Financials (sector 40) | E7, G5, G7, S6 |
| Non-financials | NF-E4, NF-G4, NF-S4 |
E8 is inert (no signals ever written); its path remains Case A (no live signals → OMIT). It is technically a deduction rule and the formula change applies to it, but has no observable effect.
Consequences
Score distribution (run_id=41, 79 institutions)
Re-scored on the same signal data (run_id=41) immediately after A(ii). The "before" column reflects the post-A(ii) state.
Institutions with deduction violations (24 institutions, sorted by before score):
| Institution | Before | After | Δ | Deduction rule(s) |
|---|---|---|---|---|
| Invesco Ltd. | 80.0 | 30.0 | −50.0 | E7 raw4→30 |
| BlackRock, Inc. | 68.6 | 18.6 | −50.0 | E7 raw3→10, S6 raw4→30 |
| DBS Group Holdings | 62.9 | 34.3 | −28.6 | E7 raw4→30 |
| Mitsubishi UFJ FG | 62.9 | 34.3 | −28.6 | E7 raw4→30 |
| Banco Santander | 55.3 | 33.7 | −21.6 | E7 raw3→10, S6 raw4→30 |
| Deutsche Bank | 55.3 | 33.7 | −21.6 | E7 raw3→10, S6 raw4→30 |
| UniCredit | 55.3 | 33.7 | −21.6 | E7 raw3→10, S6 raw4→30 |
| Berkshire Hathaway | 54.3 | 25.7 | −28.6 | E7 raw4→30 |
| Crédit Agricole | 48.6 | 39.0 | −9.6 | E7 raw4→30 |
| ING Groep | 48.6 | 39.0 | −9.6 | E7 raw4→30 |
| BNP Paribas | 44.8 | 35.2 | −9.6 | E7 raw3→10 |
| Citigroup | 44.8 | 35.2 | −9.6 | E7 raw3→10 |
| Nordea Bank | 44.8 | 35.2 | −9.6 | E7 raw3→10 |
| BROADCOM INC. | 44.0 | 24.0 | −20.0 | NF-S4 raw4→30 |
| BANK OF AMERICA | 40.0 | 30.5 | −9.5 | E7 raw4→30 |
| Barclays PLC | 40.0 | 33.3 | −6.7 | E7 raw4→30 |
| Goldman Sachs | 40.0 | 30.5 | −9.5 | E7 raw4→30 |
| Royal Bank of Canada | 40.0 | 30.5 | −9.5 | E7 raw4→30 |
| Lloyds Banking Group | 37.3 | 30.7 | −6.6 | E7 raw3→10 |
| NatWest Group | 37.3 | 30.7 | −6.6 | E7 raw3→10 |
| JPMorgan Chase | 36.2 | 26.7 | −9.5 | E7 raw3→10 |
| Schroders PLC | 34.8 | 29.6 | −5.2 | E7 raw4→30 |
| HSBC Holdings | 31.3 | 24.7 | −6.6 | E7 raw3→10 |
| Amazon.com | 30.3 | 21.7 | −8.6 | NF-S4 raw4→30 |
| Volkswagen AG | 30.3 | 21.7 | −8.6 | NF-S4 raw4→30 |
Institutions with no deduction violations (55 institutions): all unchanged.
The 24-institution M&G cohort (all at 50.0 with confidence=0) remains at 50.0. All institutions
at 40.0 or lower with no deduction violations (ASML, AstraZeneca, Barclays-free cohort, etc.)
remain unchanged. M&G PLC: 50.0 before → 50.0 after ✓.
Both-ends verification
Violations now sub-50: ✓ The highest score for any institution with at least one live deduction violation is 39.0 (Crédit Agricole and ING, each with a single E7 deforestation violation). Every violation-bearing institution now sits below the 50 neutral point.
Clean stays 50: ✓ All 24 institutions in the neutral (conf=0) cohort remain at 50.0. No clean institution moved.
Both directions from 50: ✓ Institutions with positive evidence and no violations (e.g. ASML, AstraZeneca) stay at 40.0 (positive evidence present but below the neutral sub-criterion midpoint due to sparse sub-rule hits). Institutions with only violations (Invesco) land at 30.0. Institutions with both positive evidence and violations (Barclays, E=23.3 with E7=30 pulling below positive peers, G=60.0) produce mixed-pillar composites that reflect both.
Notable mover: BlackRock at 18.6
BlackRock's before score of 68.6 (A(ii) state) drops to 18.6 — a 50-point fall. This is the largest absolute drop and warrants explicit explanation.
BlackRock has only deduction evidence in run_id=41 — no positive sub-rule fired for any E/S/G rule. Its sole non-OMIT sub-criteria are: - E7 (2 violations, raw=3): sub-criterion = 10 → E pillar = 10.0 - S6 (1 violation, raw=4): sub-criterion = 30 → S pillar = 30.0 - G pillar: all rules OMIT'd → null → dropped from composite
Composite with G null: (10 × 0.40 + 30 × 0.30) / (0.40 + 0.30) = 13.0 / 0.70 = 18.6.
This is not implausible — BlackRock has two documented controversy violations (FOREST500 deforestation-financing exposure, NGO campaign target) and zero positive ESG signal captured. The pre-fix 68.6 was an artefact of those violations anchoring above neutral. 18.6 correctly represents: "controversy evidence found, nothing positive."
The 68.6 → 18.6 delta is symmetric with the Invesco case from ADR-0022: when an institution's only evidence is deduction violations, the fix transforms the score from above-neutral to below-neutral by exactly the same mechanism.
Saturation at 3+ violations (observation for Rob, not a bug)
The deduction floor (floor=0 for score_sub_criterion.score_value) is hit at 3 violations
(max(0, 2×20−50) = max(0,−10) = 0). No institution in the current run has 3+ violations on a
single deduction rule, so no clamping occurs. If an institution were to accumulate 3+ violations on
one rule, all scores above 3 would be indistinguishable at 0. Whether the 5-point raw-score range
is granular enough for high-violation institutions is an obs-1 per-source weighting question.
RAG band landing
Current bands: red <15, amber 15–29, green ≥30 (ADR-0004).
After-distribution with current bands: - GREEN (≥30): ~54 institutions (24 clean at 50, 12 at 40, multiple violators at 30–39) - AMBER (15–29): ~25 institutions (Schroders 29.6, Mastercard 28.6, JPMorgan 26.7, Berkshire/Inditex 25.7, HSBC 24.7, Broadcom 24.0, Amazon/VW 21.7, all 20.0 cohort, BlackRock 18.6) - RED (<15): 0 institutions
RAG recalibration is a required follow-up. Bands were calibrated to the pre-A(ii) distribution (max ~26.7). At green ≥30, 24 confidence=0 neutral institutions are GREEN — the band now fails to distinguish "positive ESG behaviour captured" from "no evidence found." Do not recalibrate in this pass; do it after both halves of the scale are settled.
NOT fixed in this commit
- Per-source/per-violation weighting: FOREST500 vs Google-News vs BankTrack relative severity — obs-1 scraper-sufficiency review. This pass only re-anchors the baseline; it does not retune what each violation is worth.
- RAG band recalibration: deferred to after A(iii) is confirmed.
- Lever B (NZBA/PRB applicability for asset managers): domain review pending.
Commit status
Committed 2026-05-23. Rob approved the distribution in #esg-screening. Code change: single
else branch in src/scoring/sub_criterion.js (line 150). DB re-scored on run_id=41 in-place
(INSERT OR REPLACE semantics — DB is not in git, rescore is reversible by re-running the old
code).
Alternatives considered
-
Use
rawScore * 10instead ofrawScore * 20 - 50: Equivalent formula (both give identical results for all rawScore values, sincerawScore * 10 = rawScore * 20 - 50only at rawScore=5 and diverges elsewhere — wait, no:4*10=40vs4*20-50=30; these are different).rawScore * 10gives 40 for 1 violation (further from neutral at −10);rawScore * 20 - 50gives 30 (−20 from neutral). TherawScore * 20 - 50approach preserves the 20-pt/raw-unit step magnitude and anchors clean at 50 — the stated goal.rawScore * 10would halve the per-violation magnitude. Rejected in favour of preserving magnitude. -
Raise the deduction base_score ceiling from 5 to 10: More granularity for high-violation institutions (5 violations needed to floor rather than 3). Requires
ruletable changes and re-evaluation of per-sub-rule points values. Deferred to obs-1. -
Leave as-is: Violations score above neutral. Contradicts ADR-0012. Rejected.
References
- ADR-0012 — Neutral-prior scoring model
- ADR-0022 — A(ii) positive-rule scraped-negative fix (mirror-image issue, positive side)
- ADR-0004 — RAG thresholds (recalibration follow-up)
src/scoring/sub_criterion.js— A(iii) implementation (line ~150)- Diagnostic session 2026-05-23 — deduction-baseline investigation