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 existingscrape(inst)→scrapeWatchlist(inst); add module-level API response cache +clearCache(); add newscrape(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': bhrrcScrapein SCRAPERS; callbhrrcClearCache()at run start; update watchlist loop to callbhrrcWatchlist(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-screeningthread 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