Scoring methodology
This is the full explanation of how a single shift becomes a Fatigue Index, a Relative Risk, and a colour band. It cites the files that own each calculation so a safety reviewer can trace any number on the grid back to source.
- Engine version
- 2026.07.0
- Default profile
- general@1.0
- Engine tests
- 73
Validation: does it behave like the reference FRI?
Two things make these numbers trustworthy without asking you to take them on faith, and neither depends on a proprietary evidence base you cannot see.
Transparency and reproducibility. The method on this page is the whole method: three published component weights, named source files, and no network call, LLM, or randomness in the number. Every stored score carries the engine and profile version that produced it, so any figure on the grid can be replayed and checked months later. A reviewer can trace any score back to source and re-derive it.
It reproduces the relationships the HSE FRI establishes. The table below is produced by the same scoreShift() that scores your rota, run against fixed reference patterns when this page is built. Each row is a property RR446 describes; if a future engine change broke one, this page would show it as check needed rather than hide it.
| Property (from RR446) | Observed in the engine | Holds |
|---|---|---|
A night shift scores higher than the equivalent day shift RR446: night duty falls in the circadian low, so sleepiness rises | Day FI 12 → Night FI 23 | Holds |
Fatigue accumulates across a run of nights (night 2 ≥ night 1) RR446: the cumulative component grows with successive duties and short recovery | Night 1 FI 27 → Night 2 FI 30 | Holds |
Short rest before a shift raises cumulative load vs adequate rest RR446 with Dawson & McCulloch (2005): sleep opportunity drives recovery | Short-rest C 36 vs adequate-rest C 17 | Holds |
Circadian adaptation lowers the timing component by the 5th consecutive night RR446: partial adaptation to night work builds over consecutive nights | Night 1 timing 52 → Night 5 timing 33 | Holds |
A rail safety-critical role weighs heavier than the generic profile RR446 sector coefficients: task load scales the job component | Generic J 22 → Rail J 28 | Holds |
Calibration status. RotaPulse implements the RR446 method faithfully and reproduces its documented relationships, shown above. Exact numeric calibration is an in-progress milestone. RR446 sets out the derivation (the Fatigue Index in Appendix C, the Risk Index in Appendix D); it does not publish a table of worked examples with FI and RI values for named schedules. Exact per-input parity is therefore established against the official HSE Fatigue and Risk Index Calculator, which implements those equations. Until that parity is pinned as a regression test we do not claim it. We would rather state precisely what is proven than overstate it.
1. The inputs we accept
A score is a pure function of a ShiftInput. The public type lives at src/lib/fri-engine/types.ts. Nothing else influences the output. No network call, no LLM, no randomness.
| Input | Type path | Purpose |
|---|---|---|
| start / end | ShiftInput.start, ShiftInput.end | ISO timestamps for the shift. Duration is end minus start. |
| break_minutes | ShiftInput.break_minutes | In-shift rest, subtracted from duration to produce effective hours. Below the profile break threshold on a shift of qualifying length raises a warning. |
| job_type | ShiftInput.job_type | Fatigue class of the role: safety_critical, concentration, physical, or routine. Drives the Job component. Smart Upload maps free-text job titles onto one of these classes. |
| prior_shifts | ShiftInput.prior_shifts | Past shifts for this worker, used to compute rest before start and cumulative workload across a rolling window. |
| travel_minutes_before | ShiftInput.travel_minutes_before | Commute or on-duty travel required before the shift starts, in minutes (0–240). This is deducted from the available rest window before the sleep opportunity estimate and before the daily-rest warning threshold is applied. Based on the door-to-door rest principle documented in HSE RR446 commentary and Dawson & McCulloch (2005). |
| workload | ShiftInput.workload | Optional task intensity (light / normal / heavy). This surfaces the job-workload dimension the HSE FRI itself carries (its Job component takes a workload and an attention rating), scaling the Job component to reflect how demanding the shift is. Omitted or 'normal' leaves the score unchanged, so any shift scored without it reproduces exactly as before; light and heavy are an opt-in refinement. |
2. The three components
The Fatigue Index is a weighted sum of three sub-scores, each on a 0 to 100 scale. Each component lives in its own file so it can be reviewed and unit tested in isolation.
Cumulative
weight 0.4Captures workload over a rolling window of prior shifts: total hours worked, consecutive days, and how much rest the worker actually got before the current start. Heavier when a pattern is both long and dense.
src/lib/fri-engine/cumulative.ts
Job
weight 0.35Captures task load: job_type and the sector profile scale a baseline that represents how demanding the role is. A 12h clinical night weighs heavier than a 12h back-office day.
src/lib/fri-engine/job.ts
Timing
weight 0.25Captures circadian timing: night work, early starts, rotating direction, and shift length. Returns both a number and a tag (the inferred shift tag on the meta block).
src/lib/fri-engine/timing.ts
FI = round( 0.4 x C + 0.35 x J + 0.25 x T ), clamp 0 to 100
3. Relative Risk
Once the Fatigue Index is known, we map it to a Relative Risk (RI) value. RI is anchored so an FI of 30 returns approximately 1.0, the reference. An FI of 55 returns roughly 1.55, and an FI of 80 returns roughly 2.0. The slope is tuned against the HSE FRI reference set and clamped to a sensible range.
RI = clamp( 1.0 + (FI minus 30) / 50, 0.6, 2.6 ), rounded to 2 decimals
RI is useful because it compares schedules directly (a pattern at RI 1.6 carries more risk than a pattern at RI 1.1) without implying a per-incident probability that we have not earned.
Plan vs actual: the rota and the tachograph
Everything above scores a shift from its times, break, job and history. Those facts can come from two places, and RotaPulse can score both: the plan (your rota) and the actual (what the shift turned out to be).
The rota is the plan. Planned start, end, break and job type, imported from a spreadsheet, a connector, or the API. Scoring the plan is where most of the value is: it flags a fatiguing pattern before the week is worked, while you can still move a shift, add rest, or shorten a run of nights.
The tachograph is the record of what actually happened. For drivers, the digital tachograph in the vehicle and the driver’s smart card record driving, other work, availability and rest to the minute. It is a tamper-resistant legal record, not a self-reported timesheet. When you upload a driver-card .ddd download, RotaPulse reads each day into an actual start, end and break and attaches it to that driver’s planned shift for the day. Only the actual columns are written; the plan is left exactly as it was, and a day with no matching planned shift is reported rather than invented. The mapping is by a card-to-worker link you set once in Settings, Connections: the first time a card is seen RotaPulse asks who it belongs to, then remembers it.
Together they give you drift. When a shift carries both a planned and an as-worked version, it gets two scores from the same engine on this page. Where the day ran longer than planned, breaks were shorter, or extra driving crept in, the as-worked Fatigue Index comes out higher than the planned one, and the grid shows the gap. Fatigue is driven by the hours actually worked and the rest actually taken, not the hours intended, so the plan tells you the risk you designed and the tachograph tells you the risk you really ran, backed by an independent record you can put in front of an auditor. The two close the loop: plan, then verify. The matching and write path live in src/lib/ingest/actuals-ingest.ts and src/lib/team/actuals.ts; nothing here changes the FI or RI maths, it only decides whether the inputs are the planned or the worked shift.
Today the driver-card .ddd format is supported. Vehicle-unit downloads and tachograph-analysis-provider feeds are on the roadmap. For non-driving teams the same plan-vs-actual path accepts a CSV or paste of clock-in and clock-out times.
4. Bands on the grid
The grid collapses the FI into three bands so a rota manager can scan a week at a glance. Cutoffs live in src/lib/team/grid.ts as bandFor() and move together with the engine weights when we recalibrate.
- Green FI 0 to 21
Consistent with a reference pattern. No scheduled intervention needed.
- Amber FI 22 to 39
Elevated. Worth a review: rotation length, consecutive nights, or insufficient rest are the usual drivers.
- Red FI 40 and up
High risk. The pattern should change: split the shift, shorten the block, or add rest before the next start.
5. Warnings, in plain English
Alongside the numeric score, the engine emits plain-English warnings. The logic is in buildWarnings() in src/lib/fri-engine/score.ts. Each warning names the compliance profile that triggered it. They cover:
- Shift duration over the profile maximum (12 hours for both built-in profiles).
- In-shift break below the profile minimum: 20 minutes for shifts over 6 hours (Standard WTR) or 30 minutes for shifts over 8 hours (Network Rail NR 003).
- Daily rest before the shift below the profile minimum: 11 hours (Standard WTR) or 12 hours (Network Rail NR 003). When
travel_minutes_beforeis set, the effective rest window (rest minus travel) is checked, not the raw gap. - Consecutive night shifts exceeding the profile cap. Network Rail NR 003 caps at 4 consecutive nights; Standard WTR has no cap.
- Post-night-run rest below the profile threshold. Network Rail NR 003 requires 48 hours of rest after a run of nights ends; Standard WTR uses the standard daily-rest minimum.
- Fatigue Index crossing the amber or red band thresholds.
None of these warnings are triggered by the LLM. They are produced by the same pure function that produces the number.
6. Compliance profiles
A compliance profile is a named set of warning thresholds. Selecting a profile does not change the FI or RI calculation; it only affects which warnings fire and at what levels. The profile is stored per workspace in orgs.compliance_profile_id and can be changed in Settings. Three profiles are built in:
| Profile | Min rest | Break | Night run cap | Post-nights rest |
|---|---|---|---|---|
| standard_wtr | 11 h | 20 min > 6 h shift | none | 11 h |
| network_rail_003 | 12 h | 30 min > 8 h shift | 4 nights | 48 h |
| hgv_wtd | 11 h | 30 min > 6 h shift | none | 11 h |
The hgv_wtd profile adds three additional week-level checks on top of the per-shift warnings above: a 60-hour single-week hard cap, a 90-hour fortnightly cap (14-day rolling window), and a 17-week rolling average check that flags any worker whose average weekly hours exceed 48h over the reference period. These checks are computed in the compliance digest layer, not in the per-shift FRI engine, so they appear as WTD breaches on the Compliance Dashboard rather than as shift warnings.
Sources: UK Working Time Regulations 1998 Regulations 10, 11, and 12 for the Standard WTR profile; Network Rail Fatigue Management Standard NR/L2/OHS/003 clauses 3.2 to 3.5 for the Network Rail NR 003 profile; EU Road Transport Working Time Directive 2002/15/EC as retained in UK law (Road Transport (Working Time) Regulations 2005, Regulations 4, 5, and 7) for the HGV Road Transport WTD profile.
Standards conformance mapping
The standards our customers are audited against, what each expects on fatigue, and how RotaPulse helps you evidence it. RotaPulse is decision-support, not a certification: it does not discharge the operator’s duty, it gives you the assessment to a recognised UK method and the paper trail to show for it.
| Standard | What it expects (fatigue-relevant) | How RotaPulse helps |
|---|---|---|
| UK Working Time Regulations 1998 | 11 hours daily rest, a 20-minute break on shifts over 6 hours, a 48-hour average working week, and night-work limits. | The Standard WTR profile flags rest, break and duration breaches on every shift; the Compliance dashboard tracks the weekly picture and exports a dated PDF. |
| Network Rail NR/L2/OHS/003 | A fatigue risk assessment against Fatigue Index and Relative Risk, a cap on consecutive nights, and rest after a run of nights. | The Network Rail profile scores every duty to FI and RI, caps consecutive nights at 4 and requires 48 hours of post-nights rest, with an ORR-ready PDF report. |
| Road Transport (Working Time) Regulations 2005 (HGV / DVSA) | A 48-hour average week over a 17-week reference, a 60-hour single-week cap, a 90-hour fortnight, and breaks. | The HGV Road Transport WTD profile runs the 60-hour, 90-hour and 17-week rolling-average checks alongside the per-shift break and rest warnings. |
| CQC (healthcare: well-led and safe) | Evidence that staff fatigue is assessed and managed as a risk, not that a specific score is met. | FRI scoring of clinical rotas plus an immutable audit trail and reports you can put in front of an inspector as evidence that fatigue risk is assessed and acted on. |
RotaPulse quantifies and documents fatigue risk to a recognised UK method. It does not certify compliance or replace your safety management system, and passing every threshold check does not by itself discharge a duty of care.
7. Reference material
The engine is built against published UK and international fatigue guidance. The primary references are:
- HSE RR446: The development of a fatigue / risk index for shiftworkers. This report sets out the derivation (the Fatigue Index in Appendix C, the Risk Index in Appendix D); it is a method paper, not a table of worked examples. The relationships it establishes are pinned as regression tests in
src/lib/fri-engine/__tests__/rr446-examples.test.ts, and exact per-input parity is validated against the official HSE FRI Calculator insrc/lib/fri-engine/hse/. - HSE FRI toolkit: the Fatigue and Risk Index tool and supporting guidance documents.
- UK Working Time Regulations 1998: statutory 20 minute break and 11 hour daily rest reference periods used in the warnings block.
- Network Rail NR/L2/OHS/003 Fatigue Management Standard: clauses 3.2 to 3.5 set the minimum rest, break, consecutive-nights, and post-night-run thresholds used in the Network Rail NR 003 compliance profile.
- Dawson D. and McCulloch K. (2005): “Managing fatigue: It’s about sleep”. Occupational and Environmental Medicine 62(4) 231–233. The paper establishes commute time as a direct reduction of the sleep opportunity available between shifts, which is the basis for deducting
travel_minutes_beforefrom the effective rest window rather than treating it as a neutral activity. - EU Road Transport Working Time Directive 2002/15/EC (retained in UK law as the Road Transport (Working Time) Regulations 2005): Regulation 4 (weekly working time 48h average over 17-week reference), Regulation 5 (60h single-week hard cap and 90h fortnightly limit), and Regulation 7 (breaks) form the basis of the HGV Road Transport WTD compliance profile.
- Sector specific guidance: RSSB rail fatigue guidance and CAA CAP 1871 for aviation, used to inform sector coefficient profiles.
8. Versioning and reproducibility
Every stored score carries the fri_version and profile_version that produced it. This means:
- Any number on the grid can be replayed against the same engine version months later.
- Any change to coefficients, weights, or thresholds bumps
FRI_VERSION(currently2026.07.0). - Profiles version independently so a sector tuning does not pretend to be a wholesale engine rewrite.
9. Known limitations
- Rotapulse scores the schedule, not the person. Individual sleep quality, caffeine, medical conditions, and sleep disorders are not captured. travel_minutes_before captures the commute dimension but not sleep quality once home.
- Sector coefficient profiles currently cover generic, driver, and clinical roles. Aviation CAP 1871 and road transport Regulation 561/2006 variants are on the roadmap.
- Relative Risk is a calibrated scalar against a reference pattern, not a causal probability of an incident. Use it to compare schedules, not to predict individual outcomes.
- Compliance profile warnings are threshold checks only. A shift that passes all threshold checks can still carry elevated FI if the pattern is cumulatively demanding.
- LLM narrative text, when enabled, explains a score rather than producing one. The engine re-validates every suggestion before it is shown.