Skip to content

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 points values in the rule table 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=0 guard.
  • 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 * 10 instead of rawScore * 20 - 50: Equivalent formula (both give identical results for all rawScore values, since rawScore * 10 = rawScore * 20 - 50 only at rawScore=5 and diverges elsewhere — wait, no: 4*10=40 vs 4*20-50=30; these are different). rawScore * 10 gives 40 for 1 violation (further from neutral at −10); rawScore * 20 - 50 gives 30 (−20 from neutral). The rawScore * 20 - 50 approach preserves the 20-pt/raw-unit step magnitude and anchors clean at 50 — the stated goal. rawScore * 10 would 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 rule table 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