Skip to content
Classification & Routing

Severity Scoring Algorithms for DVIR Defects

A binary pass/fail flag cannot tell a dispatcher whether a reported brake condition is a scheduled-maintenance item or an out-of-service (OOS) event, yet 49 CFR § 396.11(a)(3) obligates the carrier to correct — and certify corrected — every Driver Vehicle Inspection Report (DVIR) defect that affects safe operation before the vehicle is dispatched again. Deterministic severity scoring is the layer that turns a normalized defect record into a reproducible 0–100 number, so that the same defect always resolves to the same compliance action and never depends on which dispatcher is on shift. This page specifies that scoring engine inside the Defect Classification & Repair Order Routing pipeline: it consumes a validated defect, computes a weighted, auditable score, and maps that score to the same severity bands that Critical vs Non-Critical Routing Logic uses to trigger an OOS hold or defer to a scheduled window.

Deterministic weighted-factor severity scoring pipeline for a DVIR defect A validated DVIR defect record enters a feature extractor that emits four normalized factors, each in the range 0.0 to 1.0: Safety Impact, Regulatory Risk, Operational Downtime, and Historical Recurrence. Each factor is multiplied by a configurable policy weight — 0.45 safety, 0.30 regulatory, 0.15 downtime, 0.10 recurrence, weights that must sum to 1.0. The four weighted terms are summed into a raw score, then scaled to 0-100 and rounded with Decimal ROUND_HALF_UP into a clamped 0-100 integer. That integer passes through a band gate that maps 0 to 34 to the Minor tier and a scheduled preventive-maintenance queue, 35 to 69 to the Major tier and the expedited 24-hour lane, and 70 to 100 to the Critical out-of-service tier that freezes dispatch. The raw factor vector, the applied weight version, and the final score also branch off into an append-only, SHA-256 hash-chained audit ledger for DOT reconstruction. Validated defect frozen record Pydantic contract Feature extractor pure fn FACTOR VECTOR · each Fᵢ ∈ [0.0, 1.0] Safety Impact component criticality × 0.45 Regulatory Risk OOS / § 396.11 tags × 0.30 Op. Downtime labor + parts lead × 0.15 Recurrence 90-day frequency × 0.10 Σ Σwᵢ = 1.0 Scale · round × 100 · Decimal ROUND_HALF_UP → int Band gate 70–100 · CRITICAL / OOS immediate OOS hold freeze dispatch 35–69 · MAJOR expedited 24h lane documented clearance 0–34 · MINOR scheduled PM window standard bay queue Append-only audit ledger factor vector · weight version · SHA-256 chain

Prerequisites and Environment Setup

Anchor link to "Prerequisites and Environment Setup"

The scoring service is a pure, side-effect-free consumer that sits between ingestion and routing. It does not fetch the record and it does not perform the routing itself — it receives a validated defect and returns a score plus its supporting factor vector. Target Python 3.10+ (the code below uses match statements and the union type operator) with:

  • Pydantic 2.x — immutable input models and strict type coercion at the boundary.
  • NumPy 1.26+ / Pandas 2.x — vectorized factor arithmetic for fleet-scale batches.
  • decimal (stdlib) — fixed-point rounding to eliminate floating-point drift before threshold evaluation.
  • PyYAML — version-controlled weight matrices loaded from configuration rather than hard-coded.

Every inbound defect must already conform to the canonical contract defined in the Standardized DVIR JSON Schema Design; a payload that fails that contract must be rejected before it reaches this layer, never defaulted to a low score. The component taxonomy the safety factor keys off is the same one produced by Defect Code Standardization Across Fleets, so a component_code that maps to a steering or brake group carries its OOS weight consistently across the pipeline.

Before scoring can run, heterogeneous DVIR payloads from telematics gateways and mobile inspection apps must be normalized into one defect record with strict type enforcement. The field names and the 0–100 scale are held identical to the routing and threshold pages so a single defect can be reconstructed across the pipeline during a DOT audit.

Field Type Enumeration / Range Compliance annotation
dvir_id string (UUID) Immutable identifier for audit-trail reconstruction
vehicle_vin string (17-char) Asset-level tracking and OOS/recall cross-referencing
component_code string SAE J1939 SPN / OEM fault tree Selects the safety criticality weight
defect_description string Sanitized driver input; never an input to a compliance decision
driver_severity_flag enum minor, major, critical § 396.11(a)(3) driver flag; treated as a floor, never a ceiling
timestamp_utc ISO8601 Inspection timestamp for SLA windows and audit keying
odometer_reading integer ≥ 0 Usage context for wear-based scoring adjustments
compliance_tags array[string] e.g. FMCSA_396_11, CVSA_OOS Regulatory markers evaluated by the regulatory-risk factor
severity_score integer 0–100 Output emitted to the routing layer
factor_vector object four floats 0.0–1.0 Per-factor breakdown persisted for audit reconstruction

The normalization step strips unstructured free-text, maps driver-reported symptoms to standardized component_code identifiers, and tags the § 396.11 markers at ingestion so downstream auditability is guaranteed. Records that arrive with malformed VINs or unmappable component codes are rejected — a defect that cannot be typed cannot be scored, and an unscorable safety defect must escalate, not silently default.

Core Algorithm: Weighted Factor Scoring

Anchor link to "Core Algorithm: Weighted Factor Scoring"

A deterministic severity score S is a linear combination of four normalized factors, each in the range 0.0–1.0, multiplied by configurable policy weights that sum to 1.0:

S=(ws×Fs)+(wr×Fr)+(wo×Fo)+(wh×Fh) S = (w_s \times F_s) + (w_r \times F_r) + (w_o \times F_o) + (w_h \times F_h)

  • Safety Impact Factor (FsF_s) — derived from component criticality tables (brakes, steering, suspension, tires, lighting). A steering or service-brake defect keyed to a CVSA OOS component maps to 1.0.
  • Regulatory Risk Factor (FrF_r) — mapped to FMCSA OOS thresholds and any state-specific inspection mandates present in compliance_tags.
  • Operational Downtime Factor (FoF_o) — estimated repair labor hours plus parts-procurement lead time, normalized against a fleet ceiling.
  • Historical Recurrence Factor (FhF_h) — defect frequency for the same component_code on the asset or fleet segment over a rolling 90-day window.

The raw output is clamped and normalized to a 0–100 integer. The steps below are ordered so the computation is reproducible and side-effect-free.

1. Model the input and the weight matrix. Coerce and freeze the record at the boundary so nothing downstream can mutate it.

python
from decimal import Decimal, ROUND_HALF_UP
from enum import StrEnum
from pydantic import BaseModel, Field, field_validator


class DriverFlag(StrEnum):
    MINOR = "minor"
    MAJOR = "major"
    CRITICAL = "critical"


class DefectRecord(BaseModel, frozen=True):
    dvir_id: str
    vehicle_vin: str = Field(min_length=17, max_length=17)
    component_code: str
    driver_severity_flag: DriverFlag
    odometer_reading: int = Field(ge=0)
    compliance_tags: list[str] = Field(default_factory=list)

    @field_validator("component_code")
    @classmethod
    def _known_code(cls, v: str) -> str:
        if v not in COMPONENT_CRITICALITY:
            raise ValueError(f"unmapped component_code: {v!r}")
        return v


class PolicyWeights(BaseModel, frozen=True):
    safety: float = 0.45
    regulatory: float = 0.30
    downtime: float = 0.15
    recurrence: float = 0.10

    @field_validator("safety", "regulatory", "downtime", "recurrence")
    @classmethod
    def _unit_interval(cls, v: float) -> float:
        if not 0.0 <= v <= 1.0:
            raise ValueError("each weight must be in [0.0, 1.0]")
        return v

    def validate_sum(self) -> "PolicyWeights":
        total = self.safety + self.regulatory + self.downtime + self.recurrence
        if abs(total - 1.0) > 1e-9:
            raise ValueError(f"weights must sum to 1.0, got {total}")
        return self

2. Derive the four factors. Each factor is a pure function of the record and reference tables. The driver’s own flag is applied as a floor so the engine never routes below the severity the driver already asserted under § 396.11(a)(3).

python
# component_code -> safety criticality in [0.0, 1.0]
COMPONENT_CRITICALITY: dict[str, float] = {
    "SPN_513_STEERING": 1.00,   # steering — CVSA OOS component
    "SPN_597_SVC_BRAKE": 1.00,  # service brakes — CVSA OOS component
    "SPN_929_SUSPENSION": 0.75,
    "SPN_1213_LIGHTING": 0.40,
    "SPN_2000_WIPER": 0.20,
}

OOS_TAGS = {"CVSA_OOS", "FMCSA_396_11_OOS"}
DRIVER_FLOOR = {DriverFlag.MINOR: 0.0, DriverFlag.MAJOR: 0.55, DriverFlag.CRITICAL: 0.85}


def safety_factor(rec: DefectRecord) -> float:
    base = COMPONENT_CRITICALITY[rec.component_code]
    return max(base, DRIVER_FLOOR[rec.driver_severity_flag])


def regulatory_factor(rec: DefectRecord) -> float:
    return 1.0 if OOS_TAGS & set(rec.compliance_tags) else 0.35


def downtime_factor(labor_hours: float, parts_lead_days: float) -> float:
    # normalize against a 24-labor-hour / 5-day fleet ceiling
    return min(1.0, (labor_hours / 24.0) * 0.6 + (parts_lead_days / 5.0) * 0.4)


def recurrence_factor(occurrences_90d: int) -> float:
    return min(1.0, occurrences_90d / 4.0)  # 4+ recurrences saturates the factor

3. Combine, clamp, and round deterministically. Fixed-point rounding before the final cast removes floating-point drift, so the same input always crosses the same threshold.

python
def severity_score(
    rec: DefectRecord,
    weights: PolicyWeights,
    *,
    labor_hours: float,
    parts_lead_days: float,
    occurrences_90d: int,
) -> tuple[int, dict[str, float]]:
    weights.validate_sum()
    factors = {
        "safety": safety_factor(rec),
        "regulatory": regulatory_factor(rec),
        "downtime": downtime_factor(labor_hours, parts_lead_days),
        "recurrence": recurrence_factor(occurrences_90d),
    }
    raw = (
        weights.safety * factors["safety"]
        + weights.regulatory * factors["regulatory"]
        + weights.downtime * factors["downtime"]
        + weights.recurrence * factors["recurrence"]
    )
    scaled = Decimal(raw * 100).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
    score = max(0, min(100, int(scaled)))
    return score, factors

For fleet-scale batches the same arithmetic runs vectorized over a Pandas frame — the full NumPy/Pandas implementation, including config hot-reload and per-vehicle aggregation, is developed in Building a Weighted Defect Scoring Model in Python.

Compliance Thresholding and Routing

Anchor link to "Compliance Thresholding and Routing"

The 0–100 score maps to exactly one severity band, and each band binds an imperative compliance action. The band boundaries are identical to every other page in this section so a defect keeps the same tier from scoring through routing to certification.

Score range Tier Compliance action Routing behavior
0–34 Minor Log for the next scheduled preventive-maintenance window Queue for standard bay assignment
35–69 Major Record a documented 24-hour pre-trip clearance obligation Route to the expedited lane in Critical vs Non-Critical Routing Logic
70–100 Critical / OOS Trigger an immediate OOS hold and freeze dispatch Immobilize the asset and fan out an emergency alert

The band boundaries themselves are not universal — a refrigerated-box fleet raises the downtime weight so a reefer failure crosses the critical floor sooner, while a passenger carrier raises the safety weight for suspension and tire defects. Those fleet-aware cut points are supplied by Dynamic Threshold Tuning for Fleet Types, which the scoring engine reads at classification time rather than hard-coding. Whatever the profile, the mapping is a hard gate: a critical score cannot be overridden into a scheduled queue, because a critical band that reaches the router sets compliance_hold=true and the transition table has no legal edge from there to a deferred lane.

python
def band(score: int) -> DriverFlag:
    match score:
        case s if s >= 70:
            return DriverFlag.CRITICAL
        case s if s >= 35:
            return DriverFlag.MAJOR
        case _:
            return DriverFlag.MINOR

Production Integration and Platform Synchronization

Anchor link to "Production Integration and Platform Synchronization"

A severity score is only useful once it is propagated across the maintenance stack without ambiguity. The scoring engine emits a structured event — the severity_score, the factor_vector, the applied weight version, and a SHA-256 hash of the source record — to the CMMS, telematics, or ELD platform over a webhook or message queue (Kafka, RabbitMQ, AWS SQS). Two invariants keep the integration audit-safe:

  • Idempotent upserts. The event key is a deterministic SHA-256 of vehicle_vin + component_code + timestamp_utc, so a retry or replay resolves to the same record rather than creating a duplicate repair order. The prioritized ticket then flows to Mechanic Assignment & Workload Balancing, which matches it against technician certification and bay capacity.
  • Cryptographic chaining. Each emitted score hash-links to the previous score for the same asset, so a deleted or reordered scoring decision is detectable during a DOT audit.

When mapping to a commercial platform’s native priority field, verify the band-to-priority mapping during onboarding and store the original score alongside it — never overwrite the computed score with the platform’s coarser priority label.

  • Schema validation — coerce and freeze every record with Pydantic at the boundary; reject malformed VINs and unmapped component_code values before they enter the scoring queue.
  • Deterministic execution — no random, no wall-clock reads inside the score function; round with Decimal/ROUND_HALF_UP before the integer cast so the same input never straddles a threshold.
  • Audit logging — persist the raw factor vector, the weight version, and the final score to an append-only ledger so a scoring decision can be reconstructed years later.
  • Configuration management — keep weight matrices in version-controlled YAML/JSON and hot-reload them without a service restart; every score records the config hash that produced it.
Can a driver’s own severity flag lower the computed score?

No. The driver_severity_flag from § 396.11(a)(3) is applied as a floor through DRIVER_FLOOR, never a ceiling. A driver who flags a defect critical guarantees a minimum safety factor of 0.85; the engine can raise the score above the driver’s flag when the component and regulatory context justify it, but it will never route below what the driver already asserted.

Why round with Decimal instead of Python’s built-in round?

Anchor link to "Why round with Decimal instead of Python’s built-in round?"

Binary floating-point cannot represent many decimal fractions exactly, so a raw score that is mathematically 69.5 can land at 69.4999… and fall into the major band instead of critical. Quantizing with Decimal and ROUND_HALF_UP before the integer cast makes the threshold crossing reproducible, which is what an audit requires.

What happens to a defect whose component_code is unmapped?

The Pydantic validator raises before scoring, so the payload is rejected and escalated rather than scored against a default. Silently assigning a low score to a defect the taxonomy does not recognize is exactly the failure mode that puts an unsafe vehicle on the road.

How are the weights allowed to differ between fleets?

The four policy weights are loaded per fleet profile from version-controlled configuration and must still sum to 1.0. A refrigerated fleet raises the downtime weight; a passenger carrier raises the safety weight. The band boundaries stay fixed at 0–34 / 35–69 / 70–100, and the specific cut points come from Dynamic Threshold Tuning for Fleet Types so scores remain comparable across the fleet.

Back to Defect Classification & Repair Order Routing.