From 8ad2b8728c1b14265094a6da41c8ebeb94ac0623 Mon Sep 17 00:00:00 2001 From: Tyler Pearson Date: Wed, 1 Jul 2026 15:14:43 -0700 Subject: [PATCH] =?UTF-8?q?Open=204WD/2WD=20>50=E2=80=B3=20routes=20to=20t?= =?UTF-8?q?he=20street-legal=204=C3=974=20profile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The suv4x4 profile mapped only to passenger/high_clearance tokens, leaving the 4wd_gt50/2wd_gt50 MVUM classes unmapped by any profile. 735 routes statewide (e.g. Cleveland NF's Bronco Peak Loops A-D) permit ONLY those gt50 classes and therefore rendered as closed for every profile. A street-legal SUV/4x4/truck IS a 4WD/2WD vehicle >50" wide, so add those tokens to suv4x4. utv_wide is deliberately left unchanged pending a domain check against an official MVUM PDF. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HNxgKNjtfeKtxYvN34KbGs --- web/src/config.test.ts | 66 ++++++++++++++++++++++++++++++++++++++++++ web/src/config.ts | 7 ++++- 2 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 web/src/config.test.ts diff --git a/web/src/config.test.ts b/web/src/config.test.ts new file mode 100644 index 0000000..1c4e4a0 --- /dev/null +++ b/web/src/config.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect } from "vitest"; +import { VEHICLE_PROFILES, CLASS_LABELS } from "./config"; + +// Canonical pipeline tokens, hardcoded from pipeline/normalize.py:50-72 +// (CLASS_DATEFIELD keys + EBIKE_FIELDS keys) as the source of truth. +const CANONICAL_TOKENS = new Set([ + "passenger", + "high_clearance", + "truck", + "bus", + "motorhome", + "4wd_gt50", + "2wd_gt50", + "tracked_ohv_gt50", + "other_ohv_gt50", + "atv", + "motorcycle", + "other_wheeled_ohv", + "tracked_ohv_lt50", + "other_ohv_lt50", + "e_bike1", + "e_bike2", + "e_bike3", +]); + +describe("VEHICLE_PROFILES", () => { + it("suv4x4 includes the 4WD/2WD >50″ classes so gt50-only trails show open", () => { + const suv4x4 = VEHICLE_PROFILES.find((p) => p.key === "suv4x4"); + expect(suv4x4).toBeDefined(); + expect(suv4x4!.tokens).toContain("4wd_gt50"); + expect(suv4x4!.tokens).toContain("2wd_gt50"); + }); + + it("every token used by every profile has a label in CLASS_LABELS", () => { + for (const profile of VEHICLE_PROFILES) { + for (const token of profile.tokens) { + expect( + typeof CLASS_LABELS[token] === "string" && CLASS_LABELS[token].length > 0, + `expected CLASS_LABELS["${token}"] (used by profile "${profile.key}") to be a non-empty string`, + ).toBe(true); + } + } + }); + + it("every token used by every profile is a canonical pipeline token", () => { + for (const profile of VEHICLE_PROFILES) { + for (const token of profile.tokens) { + expect( + CANONICAL_TOKENS.has(token), + `token "${token}" (used by profile "${profile.key}") is not in the canonical pipeline token list`, + ).toBe(true); + } + } + }); + + // Deliberate: see plans/003 "decided mapping" — whether a green-sticker + // UTV >50″ counts as a "4WD vehicle >50″" for a given forest's + // designation is a genuine domain ambiguity, so utv_wide is left + // unchanged. Changing this requires the domain check noted in plans/003's + // maintenance notes (checking against an official MVUM PDF). + it("utv_wide tokens are unchanged (deliberate, see plans/003)", () => { + const utvWide = VEHICLE_PROFILES.find((p) => p.key === "utv_wide"); + expect(utvWide).toBeDefined(); + expect(utvWide!.tokens).toEqual(["other_ohv_gt50", "tracked_ohv_gt50"]); + }); +}); diff --git a/web/src/config.ts b/web/src/config.ts index a9a58bc..d230cd3 100644 --- a/web/src/config.ts +++ b/web/src/config.ts @@ -8,6 +8,11 @@ // So each selector option is a *profile* that maps to a set of underlying MVUM // class tokens (emitted in `classes` by pipeline/normalize.py); a route is open // to the profile if it permits ANY of those tokens. +// +// suv4x4 includes the 4WD/2WD >50″ classes: MVUM designates by vehicle +// type/width, and a street-legal 4×4 IS a 4WD >50″ — without these tokens, +// gt50-only jeep trails (e.g. Bronco Peak, Cleveland NF) would never show +// open for anyone. export interface VehicleProfile { key: string; @@ -19,7 +24,7 @@ export interface VehicleProfile { export const VEHICLE_PROFILES: VehicleProfile[] = [ // Street-legal: highway-legal roads are the big set; plated bikes add moto trails. { key: "moto_plated", label: "Dual-sport motorcycle (plated)", group: "Street-legal (plated)", tokens: ["passenger", "high_clearance", "motorcycle"] }, - { key: "suv4x4", label: "SUV / 4×4 / truck (street-legal)", group: "Street-legal (plated)", tokens: ["passenger", "high_clearance"] }, + { key: "suv4x4", label: "SUV / 4×4 / truck (street-legal)", group: "Street-legal (plated)", tokens: ["passenger", "high_clearance", "4wd_gt50", "2wd_gt50"] }, { key: "car", label: "Car / passenger (street-legal)", group: "Street-legal (plated)", tokens: ["passenger"] }, // Off-road only: OHV-designated routes for that class. { key: "dirtbike", label: "Dirt bike (OHV / green sticker)", group: "Off-road only (green / red sticker)", tokens: ["motorcycle"] },