Skip to content

ADR-0029 — BHRRC→NF-S4 severity-modelled wiring

Date: 2026-05-24 Status: Accepted Decider: Rob


Context

NF-S4 ("Social controversies, 24m") has three scoring sub-rules:

sub_rule_id Source Description
1 BHRRC Deduct 1 per allegation in 24m (binary trigger)
2 GOOGLE-NEWS-RSS Deduct 1 per major labour-related regulatory action in 24m
3 CHRB Deduct 1 if CHRB score < 40

A 2026-05-24 investigation found root cause (b): BHRRC was absent from the SCRAPERS registry in run.js and only ran in the watchlist pass (writing watchlist_entry rows, not signal rows). Because sub_criterion.js reads only from signal, NF-S4.1 (BHRRC) was permanently at Case A (no signals → OMIT) in every run. Confirmed: zero signal rows with rule_id='NF-S4', sub_rule_id=1 in any run; only NF-S4.3 (CHRB) produced signals (41 rows in run_id=43).

Net effect: institutions with real BHRRC social allegations (IKEA, Amazon, VW, etc.) carried no scored social deduction.

Why the naive binary fix was rejected

A simple "any allegation → NF-S4 fires once → score 55" model is non-monotone. For an institution with low E and null S (because NF-S4 was always OMIT), adding a scored S=55 pulls the composite up. Confirmed worked example with run_id=43 data: IKEA (E=30) would go ESG 30 → 41 (Red→Amber) — the flagship bad-actor would look better after fixing the bug.


Decision

Wire BHRRC into NF-S4 using a severity-modelled deduction that is strictly monotone: worse BHRRC record → lower score; no institution improves.

Severity weight

W = (high-severity count × 3) + (medium-severity count × 1), rolling 24-month window (event_date ≥ run_date − 24 months; null-date items treated as in-window). LOW severity excluded (proactive corporate remediation — classifySeverity() taxonomy, calibrated cycle B1).

Three stacking BHRRC sub-rules (each points = −1)

sub_rule_id Trigger Effect
1 (existing) W > 0 First deduction
4 (new) W ≥ 3 Second deduction (elevated severity)
5 (new) W ≥ 7 Third deduction (severe)

NF-S4 score ladder (BHRRC alone)

W Sub-rules firing raw_score score_value
0 none OMIT (Case B) 50 (excluded from pillar mean)
1–2 1 4 55
3–6 1, 4 3 10
≥ 7 1, 4, 5 2 0

CHRB deduction (sub_rule_id=3, −1 when chrb_score < 40) composes additively: - W ≥ 7 + CHRB poor → raw=1 → score_value=0 (floor, no additional effect) - W 3–6 + CHRB poor → raw=2 → score_value=0 - W 1–2 + CHRB poor → raw=3 → score_value=10 - W=0 + CHRB poor → raw=4 → score_value=55 (NF-S4.3 deduction only)

Implementation

  • Migration 059: INSERT rule rows for sub_rule_id=4 and sub_rule_id=5.
  • bhrrc.js: rename existing scrape(inst)scrapeWatchlist(inst); add module-level API response cache + clearCache(); add new scrape(inst, runId, db) emitting three NF-S4 signals per NF institution based on W. FI (sector 40) skipped via sector guard.
  • run.js: register 'BHRRC': bhrrcScrape in SCRAPERS; call bhrrcClearCache() at run start; update watchlist loop to call bhrrcWatchlist(inst) (reads from cache for NF, live-fetch for FI).

Before/after (run_id=43 basis; projected, confirmed post-run)

Institution Sector Before ESG After ESG Band change W CHRB
Alphabet Inc. 50 67.1 ~56 Green→Amber W=1
Caterpillar Inc. 20 66.3 ~55 Green→Amber W=1
Microsoft Corp. 45 65.7 ~55 Green→Amber W=1
Volkswagen AG 25 67.4 ~55 Green→Amber W=1
Amazon.com 25 48.5 ~0–10 Amber→Red W≥7 poor
Booking Holdings 25 47.6 ~40 Amber→Red W=1
Inditex 25 44.0 ~35 Amber→Red W=1–2
Unilever 30 45.9 ~35 Amber→Red W=1–2
Ingka (IKEA) 25 30.0 ~21 Red→Red (deeper) W≥7
NVIDIA 45 71.0 ~65 Green→Green (hold) W=1
Thermo Fisher 35 90.4 ~81 Green→Green (hold) W=1

0 FI institutions affected (sector 40 filtered out). ~30 NF institutions with W=0 unchanged.

Required caveats

(a) Sparse-pillar sensitivity. Where NF-S4 is the only non-OMIT S sub-rule, it carries the full S-pillar weight, so a single allegation (W=1) produces a larger composite swing than the sub-rule's deduction value alone suggests. Thermo Fisher: 100→81, stays Green — direction correct, magnitude is a coverage artefact (same structural family as the BNP dilution / ×20 / k notes in ADR-0026). This is a known property of the coverage-weighted model, not a defect.

(b) Calibration choice. The W≥3/W≥7 thresholds and H×3+M×1 weighting are an explicit calibration choice, not a derived constant. They were selected for plausible monotone ordering and are revisitable when more live run data is available. Record as such so they are not treated as load-bearing methodology.


CHRB composition rationale

BHRRC and CHRB measure different things and must not double-count: - BHRRC (sub_rule_id=1,4,5): recent event-based incidents, 24-month window, news-media sourced. - CHRB (sub_rule_id=3): systemic policy-quality benchmark, biennial assessment of human rights due diligence frameworks across sector cohorts.

Both are deduction-only; they stack correctly. Amazon and VW have both (W≥7 + CHRB poor score), yielding raw=1 → score=0, the floor. Adding CHRB on top of a W≥7 institution produces no additional score movement but correctly records both dimensions of adverse human rights performance.


NF-S4.2 (GOOGLE-NEWS-RSS) — not fixed here

google-news-rss.js implements only G7/NF-G4; it never handles NF-S4.2 or NF-E4. This is a separate correctness gap (dead-wired by a different cause). Logged in backlog; not fixed in this session.


Alternatives considered

  • Binary trigger (any allegation → NF-S4 fires once): Non-monotone. Rejected because IKEA (E=30, S previously OMIT) would go Red→Amber — fixing the bug would make a bad actor score better. See non-monotone analysis above.
  • Cap at sub_rule_id=1 only with W-based rawValue scaling: Considered; rejected because it requires bespoke scoring logic outside the standard deduction formula. Stacking independent sub-rules with W-thresholds is simpler, auditable, and uses the existing scoring infrastructure without modification.
  • Watchlist→scored deduction bridge (option b from obs-1): Broader architectural change; deferred. BHRRC→NF-S4 signal wiring (this ADR) is the correctness-first resolution.

References

  • Root cause investigation: #esg-screening thread 2026-05-24
  • docs/design/bhrrc-severity-mapping.md — severity taxonomy
  • Migration 059 (059_bhrrc_nfs4_severity_tiers.sql)
  • ADR-0026 — baseline-100 deduction re-anchor (score formula used here)
  • ADR-0022 — OMIT sentinel and Case A/B/C treatment