initial code
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
# Utah Vehicle Health dashboard
|
||||
|
||||
This directory is a dependency-free static prototype. It uses semantic HTML,
|
||||
CSS, vanilla ES modules, and inline SVG generated from approved aggregate JSON.
|
||||
It has no database client, server credential, analytics SDK, or external CDN.
|
||||
|
||||
## Run locally from the repository root
|
||||
|
||||
```bash
|
||||
node dashboard/server.mjs
|
||||
```
|
||||
|
||||
Open `http://127.0.0.1:4173`. Do not open `index.html` directly with a `file:`
|
||||
URL; browsers will block the JSON module requests. To use another port:
|
||||
|
||||
```bash
|
||||
PORT=8080 node dashboard/server.mjs
|
||||
```
|
||||
|
||||
Run the dependency-free contract checks with:
|
||||
|
||||
```bash
|
||||
node --test dashboard/tests/contract.test.mjs
|
||||
```
|
||||
|
||||
## Public-data boundary
|
||||
|
||||
The browser requires and validates these files beneath `public/data/`:
|
||||
|
||||
- `data_manifest.json`
|
||||
- `overview_period_county.json`
|
||||
- `age_risk_curve.json`
|
||||
- `cohort_scorecard.json`
|
||||
- `model_diagnostics.json`
|
||||
- `coverage_quality.json`
|
||||
- `filter_catalog.json`
|
||||
- `sha256_manifest.json`
|
||||
|
||||
If any file is missing, malformed, has inconsistent publication flags, or
|
||||
contains a denied identifier-like field, the dashboard shows an unavailable
|
||||
state and no estimates. Regenerate assets with the repository's private local
|
||||
pipeline; never hand-edit public JSON to bypass suppression.
|
||||
|
||||
The next-test estimator is intentionally disabled. It must remain disabled
|
||||
until a separately reviewed and suppressed `prediction_lookup` contract is
|
||||
approved. Do not add a connection from this site to `countydata`.
|
||||
|
||||
## Import into Bolt
|
||||
|
||||
The recommended publication boundary is a separate static Bolt project (and,
|
||||
ideally, a separate deployment repository) containing only the contents of
|
||||
`dashboard/`. Upload or copy this directory's contents so `index.html` is at the
|
||||
new project root. There is no install or build step. If Bolt asks for a preview
|
||||
command, use `node server.mjs` with `HOST=0.0.0.0`; Bolt supplies `PORT`.
|
||||
|
||||
Before publishing, confirm that `/public/data/data_manifest.json` resolves,
|
||||
the development-preview banner remains visible, and the contract test passes.
|
||||
The generated JSON bundle must also complete its privacy review.
|
||||
|
||||
Importing the whole source repository is a discouraged fallback because Bolt
|
||||
would receive SQL, database tooling, and other files that are not needed by the
|
||||
public site. If that has already happened, set the project/working root to
|
||||
`dashboard`, never serve the repository root, and create a dashboard-only
|
||||
project before production publication. Do not copy private `data/`,
|
||||
`artifacts/`, `models/`, `.env`, or database tooling into the public project.
|
||||
|
||||
No publishing action is performed by this repository.
|
||||
@@ -0,0 +1,379 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="Utah Vehicle Health — an accessible, aggregate view of emissions-inspection outcomes.">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<title>Overview · Utah Vehicle Health</title>
|
||||
<link rel="stylesheet" href="./styles.css">
|
||||
<script type="module" src="./js/app.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main-content">Skip to dashboard content</a>
|
||||
|
||||
<div id="preview-banner" class="preview-banner" role="status" hidden>
|
||||
<span class="preview-banner__icon" aria-hidden="true">◆</span>
|
||||
<div>
|
||||
<strong id="preview-title">Development preview</strong>
|
||||
<span id="preview-copy">Sample results are not population estimates and must not be used for individual decisions.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<header class="site-header">
|
||||
<div class="site-header__inner">
|
||||
<a class="brand" href="#overview" aria-label="Utah Vehicle Health overview">
|
||||
<svg class="brand__mark" viewBox="0 0 56 56" aria-hidden="true">
|
||||
<path d="M9 43 23 17l7 12 6-9 12 23H9Z" fill="currentColor"></path>
|
||||
<path d="m18 43 9-16 6 10 4-6 6 12H18Z" fill="var(--sand-100)"></path>
|
||||
</svg>
|
||||
<span>
|
||||
<span class="brand__name">Utah Vehicle Health</span>
|
||||
<span class="brand__tagline">Next-episode inspection insights</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<div class="header-status" id="header-status" aria-live="polite">
|
||||
<span class="status-dot" aria-hidden="true"></span>
|
||||
<span>Loading approved aggregates…</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="primary-nav" aria-label="Dashboard views">
|
||||
<div class="primary-nav__inner">
|
||||
<a href="#overview" data-route="overview" aria-current="page">Overview</a>
|
||||
<a href="#reliability" data-route="reliability">Reliability explorer</a>
|
||||
<a href="#estimator" data-route="estimator">Next-test estimator</a>
|
||||
<a href="#methods" data-route="methods">Data & methods</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main id="main-content" tabindex="-1">
|
||||
<section class="view" id="view-overview" data-view="overview" aria-labelledby="overview-heading">
|
||||
<div class="page-shell">
|
||||
<header class="page-heading page-heading--split">
|
||||
<div>
|
||||
<p class="eyebrow">Inspection outcomes over time</p>
|
||||
<h1 id="overview-heading">A clearer view of the next inspection</h1>
|
||||
<p class="lede">Explore aggregate first-attempt outcomes for returning vehicles in participating Utah county feeds. Non-pass combines fail, reject, and abort.</p>
|
||||
</div>
|
||||
<div class="data-stamp" aria-label="Dataset status">
|
||||
<span>Data through</span>
|
||||
<strong id="data-cutoff">Unavailable</strong>
|
||||
<small id="model-version">Waiting for manifest</small>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="unavailable-state" data-unavailable hidden role="status">
|
||||
<div class="unavailable-state__icon" aria-hidden="true">!</div>
|
||||
<div>
|
||||
<h2>Dashboard data is unavailable</h2>
|
||||
<p data-unavailable-message>The approved aggregate files could not be validated. No estimates are being shown.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="kpi-grid" aria-label="Overview statistics" data-requires-data>
|
||||
<article class="kpi-card">
|
||||
<span class="kpi-card__label">Published support</span>
|
||||
<strong id="kpi-eligible">—</strong>
|
||||
<small>Rounded total; suppressed cells omitted</small>
|
||||
</article>
|
||||
<article class="kpi-card kpi-card--pass">
|
||||
<span class="kpi-card__label">Pass rate</span>
|
||||
<strong id="kpi-pass">—</strong>
|
||||
<small>Recognized first-attempt outcomes</small>
|
||||
</article>
|
||||
<article class="kpi-card kpi-card--nonpass">
|
||||
<span class="kpi-card__label">Non-pass rate</span>
|
||||
<strong id="kpi-nonpass">—</strong>
|
||||
<small>Fail + reject + abort</small>
|
||||
</article>
|
||||
<article class="kpi-card">
|
||||
<span class="kpi-card__label">Covered counties</span>
|
||||
<strong id="kpi-counties">—</strong>
|
||||
<small>Not statewide coverage</small>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<div class="dashboard-grid dashboard-grid--wide" data-requires-data>
|
||||
<article class="panel panel--wide">
|
||||
<header class="panel__header">
|
||||
<div>
|
||||
<p class="eyebrow">Quarterly outcomes</p>
|
||||
<h2>First-attempt non-pass trend</h2>
|
||||
</div>
|
||||
<div class="chart-legend" aria-label="Outcome legend">
|
||||
<span><i class="legend-swatch legend-swatch--fail"></i>Non-pass (fail + reject + abort)</span>
|
||||
</div>
|
||||
</header>
|
||||
<div id="outcome-trend-chart" class="chart chart--large" role="img" aria-label="Quarterly inspection outcome rates"></div>
|
||||
<p class="chart-note" id="outcome-trend-note">Partial periods and source changes are called out when present.</p>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<header class="panel__header">
|
||||
<div>
|
||||
<p class="eyebrow">Feed availability</p>
|
||||
<h2>County coverage</h2>
|
||||
</div>
|
||||
</header>
|
||||
<div id="utah-coverage-map" class="utah-map" role="img" aria-label="Utah county-feed coverage"></div>
|
||||
<div id="county-coverage-list" class="county-list" aria-label="County availability list"></div>
|
||||
<p class="chart-note">Teal counties appear in the approved inspection feeds. Gray counties are unavailable, not zero.</p>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<header class="panel__header">
|
||||
<div>
|
||||
<p class="eyebrow">Age pattern</p>
|
||||
<h2>Non-pass risk by vehicle age</h2>
|
||||
</div>
|
||||
</header>
|
||||
<div id="age-risk-chart" class="chart" role="img" aria-label="Published non-pass risk by vehicle-age band"></div>
|
||||
<p class="chart-note">The development bundle does not include confidence intervals; published aggregate rates are shown as points.</p>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<aside class="callout" data-requires-data>
|
||||
<span class="callout__icon" aria-hidden="true">i</span>
|
||||
<p><strong>What this measures:</strong> emissions-inspection outcomes for covered feeds. It is not a diagnosis of mechanical condition, safety, roadworthiness, or legal compliance.</p>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view" id="view-reliability" data-view="reliability" aria-labelledby="reliability-heading" hidden>
|
||||
<div class="page-shell">
|
||||
<header class="page-heading">
|
||||
<p class="eyebrow">Supported aggregate cohorts</p>
|
||||
<h1 id="reliability-heading">Reliability explorer</h1>
|
||||
<p class="lede">Compare published observed next-episode non-pass rates. Every result is a supported aggregate cohort—not an individual prediction.</p>
|
||||
</header>
|
||||
|
||||
<div class="unavailable-state" data-unavailable hidden role="status">
|
||||
<div class="unavailable-state__icon" aria-hidden="true">!</div>
|
||||
<div><h2>Explorer unavailable</h2><p data-unavailable-message>Approved scorecards could not be validated.</p></div>
|
||||
</div>
|
||||
|
||||
<div class="explorer-layout" data-requires-data>
|
||||
<aside class="filter-panel" aria-labelledby="filter-heading">
|
||||
<div class="filter-panel__heading">
|
||||
<h2 id="filter-heading">Refine cohorts</h2>
|
||||
<button class="text-button" id="reset-filters" type="button">Reset</button>
|
||||
</div>
|
||||
<form id="explorer-filters">
|
||||
<label class="field">
|
||||
<span>Make or model</span>
|
||||
<input id="cohort-search" type="search" autocomplete="off" placeholder="Search supported cohorts">
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>County</span>
|
||||
<select id="filter-county"><option value="">All covered counties</option></select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Vehicle age</span>
|
||||
<select id="filter-age"><option value="">All age bands</option></select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Fuel</span>
|
||||
<select id="filter-fuel"><option value="">All supported fuels</option></select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Program</span>
|
||||
<select id="filter-program"><option value="">All approved programs</option></select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Period</span>
|
||||
<select id="filter-period"><option value="">All published periods</option></select>
|
||||
</label>
|
||||
<fieldset class="segmented-control">
|
||||
<legend>Risk view</legend>
|
||||
<label><input type="radio" name="risk-view" value="observed" checked><span>Observed</span></label>
|
||||
<label><input type="radio" name="risk-view" value="adjusted"><span>Model-adjusted</span></label>
|
||||
</fieldset>
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
<div class="explorer-results">
|
||||
<div class="result-toolbar">
|
||||
<p id="result-summary" aria-live="polite">Loading supported cohorts…</p>
|
||||
<label class="field field--inline">
|
||||
<span>Sort</span>
|
||||
<select id="result-sort">
|
||||
<option value="support">Largest support</option>
|
||||
<option value="risk-desc">Highest non-pass risk</option>
|
||||
<option value="risk-asc">Lowest non-pass risk</option>
|
||||
<option value="name">Make and model</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<article class="panel">
|
||||
<header class="panel__header">
|
||||
<div><p class="eyebrow">Published aggregate comparison</p><h2>Supported cohort scorecard</h2></div>
|
||||
</header>
|
||||
<div id="cohort-dot-plot" class="chart chart--scorecard" role="img" aria-label="Ranked published cohort non-pass rates"></div>
|
||||
</article>
|
||||
|
||||
<div id="cohort-cards" class="cohort-cards"></div>
|
||||
<div class="empty-results" id="empty-results" hidden>
|
||||
<h2>No supported cohorts match</h2>
|
||||
<p>Try clearing one or more filters. Suppressed cohorts are never included in the browser bundle.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view" id="view-estimator" data-view="estimator" aria-labelledby="estimator-heading" hidden>
|
||||
<div class="page-shell page-shell--narrow">
|
||||
<header class="page-heading">
|
||||
<p class="eyebrow">Cohort estimate · not an individual diagnosis</p>
|
||||
<h1 id="estimator-heading">Next-test risk estimator</h1>
|
||||
<p class="lede">This planned tool will combine approved, coarsened attributes to return a calibrated next-episode non-pass probability.</p>
|
||||
</header>
|
||||
|
||||
<div class="locked-panel" role="status" aria-labelledby="estimator-status-title">
|
||||
<span class="locked-panel__icon" aria-hidden="true">◇</span>
|
||||
<div>
|
||||
<p class="eyebrow">Intentionally disabled</p>
|
||||
<h2 id="estimator-status-title">An approved prediction lookup is not available</h2>
|
||||
<p>The estimator will remain off until a privacy-reviewed, suppressed <code>prediction_lookup</code> is published. The current model diagnostics are not enough to serve individual or row-level estimates.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="estimator-form" aria-describedby="estimator-disabled-copy">
|
||||
<fieldset disabled>
|
||||
<legend>Coarsened cohort attributes</legend>
|
||||
<div class="form-grid">
|
||||
<label class="field"><span>County</span><select><option>Select a covered county</option></select></label>
|
||||
<label class="field"><span>Supported make and model</span><select><option>Select a cohort</option></select></label>
|
||||
<label class="field"><span>Vehicle-age band</span><select><option>Select an age band</option></select></label>
|
||||
<label class="field"><span>Fuel</span><select><option>Select a supported fuel</option></select></label>
|
||||
<label class="field"><span>Prior episode outcome</span><select><option>Pass / fail / reject / abort</option></select></label>
|
||||
<label class="field"><span>Time since prior episode</span><select><option>Select a coarsened interval</option></select></label>
|
||||
<label class="field"><span>Season</span><select><option>Select a season</option></select></label>
|
||||
<label class="field"><span>Program category</span><select><option>Select an approved program</option></select></label>
|
||||
</div>
|
||||
<button class="button" type="button">Estimate cohort risk</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
<p id="estimator-disabled-copy" class="form-note">No VIN, plate, exact address, station, free text, or current-test diagnostic will ever be requested.</p>
|
||||
|
||||
<section class="method-preview" aria-labelledby="future-output-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Future approved output</p>
|
||||
<h2 id="future-output-heading">Designed for calibrated context</h2>
|
||||
</div>
|
||||
<ul class="check-list">
|
||||
<li>Calibrated non-pass probability and uncertainty</li>
|
||||
<li>Relevant aggregate baseline</li>
|
||||
<li>High-level factor contributions</li>
|
||||
<li>Support and coverage limitations</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view" id="view-methods" data-view="methods" aria-labelledby="methods-heading" hidden>
|
||||
<div class="page-shell">
|
||||
<header class="page-heading">
|
||||
<p class="eyebrow">Transparent by design</p>
|
||||
<h1 id="methods-heading">Data & methods</h1>
|
||||
<p class="lede">How episodes, labels, temporal evaluation, coverage limits, and privacy controls shape every published result.</p>
|
||||
</header>
|
||||
|
||||
<div class="unavailable-state" data-unavailable hidden role="status">
|
||||
<div class="unavailable-state__icon" aria-hidden="true">!</div>
|
||||
<div><h2>Live diagnostics unavailable</h2><p data-unavailable-message>Static methodology remains below; no performance values are being shown.</p></div>
|
||||
</div>
|
||||
|
||||
<section class="method-grid" aria-label="Core definitions">
|
||||
<article class="method-card">
|
||||
<span class="method-card__number">01</span>
|
||||
<h2>Prediction unit</h2>
|
||||
<p>The first attempt of the next inspection episode. Attempts no more than 30 days apart form one episode.</p>
|
||||
</article>
|
||||
<article class="method-card">
|
||||
<span class="method-card__number">02</span>
|
||||
<h2>Binary target</h2>
|
||||
<p>Pass versus non-pass. Non-pass combines fail, reject, and abort; blanks and unrecognized outcomes are not labels.</p>
|
||||
</article>
|
||||
<article class="method-card">
|
||||
<span class="method-card__number">03</span>
|
||||
<h2>Point-in-time features</h2>
|
||||
<p>Every history window ends before the target episode. Current-test results, diagnostics, stations, and later attempts are excluded.</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="panel" data-requires-data>
|
||||
<header class="panel__header"><div><p class="eyebrow">Availability by source era</p><h2>Coverage timeline</h2></div></header>
|
||||
<div id="coverage-heatmap" class="heatmap" role="img" aria-label="Inspection source availability by year"></div>
|
||||
<p class="chart-note">The <code>slc</code> and <code>slco</code> labels are source eras for Salt Lake County, not separate physical counties.</p>
|
||||
</section>
|
||||
|
||||
<section class="split-section" aria-labelledby="split-heading">
|
||||
<div class="split-section__copy">
|
||||
<p class="eyebrow">No random row split</p>
|
||||
<h2 id="split-heading">Chronological evaluation</h2>
|
||||
<p>Historical events establish prior context. The 2025 development-sample holdout was evaluated once after model choices were frozen, so it is no longer a pristine final test. A later complete-data run is confirmatory; the partial 2026 feed is monitoring only.</p>
|
||||
</div>
|
||||
<ol class="timeline">
|
||||
<li><span>2010–15</span><strong>Context</strong><small>History only</small></li>
|
||||
<li><span>2016–22</span><strong>Train</strong><small>Fit preprocessing + model</small></li>
|
||||
<li><span>2023</span><strong>Tune</strong><small>Select regularization</small></li>
|
||||
<li><span>2024</span><strong>Calibrate</strong><small>Scale probabilities</small></li>
|
||||
<li class="timeline__holdout"><span>2025</span><strong>One-time holdout</strong><small>Already evaluated</small></li>
|
||||
<li><span>2026</span><strong>Shadow</strong><small>Partial-period drift</small></li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<div class="dashboard-grid" data-requires-data>
|
||||
<article class="panel">
|
||||
<header class="panel__header"><div><p class="eyebrow" id="diagnostic-scope-label">Model diagnostics</p><h2>Published model diagnostics</h2></div></header>
|
||||
<div class="diagnostic-kpis">
|
||||
<div><span>PR-AUC</span><strong id="diagnostic-ap">—</strong></div>
|
||||
<div><span>Brier score</span><strong id="diagnostic-brier">—</strong></div>
|
||||
<div><span>Support</span><strong id="diagnostic-n">—</strong></div>
|
||||
</div>
|
||||
<div id="calibration-chart" class="chart" role="img" aria-label="Predicted versus observed non-pass risk calibration"></div>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<header class="panel__header"><div><p class="eyebrow">Interpret with care</p><h2>Known limitations</h2></div></header>
|
||||
<ul class="limitation-list" id="manifest-limitations">
|
||||
<li>Participating inspection feeds do not cover all 29 counties.</li>
|
||||
<li>Feed and program changes can resemble real-world trends.</li>
|
||||
<li>DMV history has a 2021 gap and ends in March 2024.</li>
|
||||
<li>Reject and abort can reflect process or readiness issues.</li>
|
||||
<li>Association is not causation or a mechanical diagnosis.</li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<section class="privacy-panel">
|
||||
<div><p class="eyebrow">Publication boundary</p><h2>Only approved aggregates reach this site</h2></div>
|
||||
<div class="privacy-flow" aria-label="Private-to-public data flow">
|
||||
<span class="privacy-flow__private">Private source<br><small>Read-only local pipeline</small></span>
|
||||
<span aria-hidden="true">→</span>
|
||||
<span>Suppression<br><small>Minimum support + complements</small></span>
|
||||
<span aria-hidden="true">→</span>
|
||||
<span class="privacy-flow__public">Public assets<br><small>Aggregate JSON only</small></span>
|
||||
</div>
|
||||
<p>Direct identifiers, private vehicle tokens, plates, ZIPs, stations, raw records, and row-level predictions are outside the public data contract.</p>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="page-shell site-footer__inner">
|
||||
<div><strong>Utah Vehicle Health</strong><p>Aggregate emissions-inspection research.</p></div>
|
||||
<p>Not a diagnosis, certification, safety assessment, or guarantee of an inspection outcome.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<div id="route-announcer" class="sr-only" aria-live="polite"></div>
|
||||
<noscript><div class="noscript">JavaScript is required to validate and display the approved aggregate data files. No estimates are shown without validation.</div></noscript>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,352 @@
|
||||
import { loadDashboardData } from "./data.js";
|
||||
import {
|
||||
aggregateOverview,
|
||||
compactNumber,
|
||||
percent,
|
||||
renderAgeRisk,
|
||||
renderCohortDotPlot,
|
||||
renderCoverageHeatmap,
|
||||
renderNoCalibration,
|
||||
renderOutcomeTrend,
|
||||
renderUtahCoverage,
|
||||
} from "./charts.js";
|
||||
|
||||
const ROUTES = Object.freeze({
|
||||
overview: "Overview",
|
||||
reliability: "Reliability explorer",
|
||||
estimator: "Next-test estimator",
|
||||
methods: "Data & methods",
|
||||
});
|
||||
|
||||
const UTAH_COUNTIES = [
|
||||
"Beaver",
|
||||
"Box Elder",
|
||||
"Cache",
|
||||
"Carbon",
|
||||
"Daggett",
|
||||
"Davis",
|
||||
"Duchesne",
|
||||
"Emery",
|
||||
"Garfield",
|
||||
"Grand",
|
||||
"Iron",
|
||||
"Juab",
|
||||
"Kane",
|
||||
"Millard",
|
||||
"Morgan",
|
||||
"Piute",
|
||||
"Rich",
|
||||
"Salt Lake",
|
||||
"San Juan",
|
||||
"Sanpete",
|
||||
"Sevier",
|
||||
"Summit",
|
||||
"Tooele",
|
||||
"Uintah",
|
||||
"Utah",
|
||||
"Wasatch",
|
||||
"Washington",
|
||||
"Wayne",
|
||||
"Weber",
|
||||
];
|
||||
|
||||
let dashboardData = null;
|
||||
let visibleScorecards = [];
|
||||
|
||||
function byId(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function setText(id, value) {
|
||||
const element = byId(id);
|
||||
if (element) element.textContent = value;
|
||||
}
|
||||
|
||||
function normalizeCounty(value) {
|
||||
return String(value).trim().toLowerCase().replaceAll("_", " ").replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
function displayCategory(value) {
|
||||
return String(value)
|
||||
.replaceAll("_", " ")
|
||||
.toLowerCase()
|
||||
.replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||
}
|
||||
|
||||
function routeFromHash() {
|
||||
const candidate = window.location.hash.replace(/^#/, "").toLowerCase();
|
||||
return Object.hasOwn(ROUTES, candidate) ? candidate : "overview";
|
||||
}
|
||||
|
||||
function showRoute({ announce = true, focus = false } = {}) {
|
||||
const route = routeFromHash();
|
||||
for (const view of document.querySelectorAll("[data-view]")) {
|
||||
view.hidden = view.dataset.view !== route;
|
||||
}
|
||||
for (const link of document.querySelectorAll("[data-route]")) {
|
||||
if (link.dataset.route === route) link.setAttribute("aria-current", "page");
|
||||
else link.removeAttribute("aria-current");
|
||||
}
|
||||
document.title = `${ROUTES[route]} · Utah Vehicle Health`;
|
||||
if (announce) setText("route-announcer", `${ROUTES[route]} view`);
|
||||
if (focus) {
|
||||
const heading = document.querySelector(`[data-view="${route}"] h1`);
|
||||
if (heading) {
|
||||
heading.setAttribute("tabindex", "-1");
|
||||
heading.focus({ preventScroll: true });
|
||||
heading.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initializeRouting() {
|
||||
showRoute({ announce: false, focus: false });
|
||||
window.addEventListener("hashchange", () => showRoute({ focus: true }));
|
||||
}
|
||||
|
||||
function setHeaderStatus(state, message) {
|
||||
const status = byId("header-status");
|
||||
if (!status) return;
|
||||
status.dataset.state = state;
|
||||
const text = status.querySelector("span:last-child");
|
||||
if (text) text.textContent = message;
|
||||
}
|
||||
|
||||
function setPreviewBanner(manifest) {
|
||||
const preview = manifest.development_preview || !manifest.population_estimate_allowed;
|
||||
const banner = byId("preview-banner");
|
||||
banner.hidden = !preview;
|
||||
if (!preview) return;
|
||||
setText("preview-title", "Development preview");
|
||||
setText(
|
||||
"preview-copy",
|
||||
"These suppressed sample aggregates are not population estimates and must not be used for individual decisions.",
|
||||
);
|
||||
}
|
||||
|
||||
function setUnavailable(error) {
|
||||
const safeMessage =
|
||||
error instanceof Error && error.message
|
||||
? `${error.message} No estimates are being shown.`
|
||||
: "The approved aggregate files could not be validated. No estimates are being shown.";
|
||||
for (const state of document.querySelectorAll("[data-unavailable]")) {
|
||||
state.hidden = false;
|
||||
const copy = state.querySelector("[data-unavailable-message]");
|
||||
if (copy) copy.textContent = safeMessage;
|
||||
}
|
||||
for (const region of document.querySelectorAll("[data-requires-data]")) {
|
||||
region.hidden = true;
|
||||
}
|
||||
const banner = byId("preview-banner");
|
||||
banner.hidden = false;
|
||||
setText("preview-title", "Data unavailable");
|
||||
setText("preview-copy", "This static shell could not validate every required public aggregate. No fallback or estimated values are displayed.");
|
||||
setHeaderStatus("error", "Approved data unavailable");
|
||||
}
|
||||
|
||||
function populateSelect(id, values, formatter = displayCategory) {
|
||||
const select = byId(id);
|
||||
if (!select) return;
|
||||
for (const value of values) {
|
||||
const option = document.createElement("option");
|
||||
option.value = String(value);
|
||||
option.textContent = formatter(value);
|
||||
select.append(option);
|
||||
}
|
||||
}
|
||||
|
||||
function disableUnsupportedFilter(id, explanation) {
|
||||
const select = byId(id);
|
||||
if (!select) return;
|
||||
select.disabled = true;
|
||||
select.title = explanation;
|
||||
select.options[0].textContent = explanation;
|
||||
}
|
||||
|
||||
function renderCountyList(coveredCounties) {
|
||||
const container = byId("county-coverage-list");
|
||||
container.replaceChildren();
|
||||
const normalized = new Set(coveredCounties.map(normalizeCounty));
|
||||
for (const county of UTAH_COUNTIES) {
|
||||
const chip = document.createElement("span");
|
||||
chip.className = "county-chip";
|
||||
chip.dataset.covered = String(normalized.has(normalizeCounty(county)));
|
||||
chip.textContent = county;
|
||||
container.append(chip);
|
||||
}
|
||||
}
|
||||
|
||||
function renderOverview(data) {
|
||||
const rows = data.overview.rows;
|
||||
const periods = aggregateOverview(rows);
|
||||
const totalSupport = rows.reduce((sum, row) => sum + row.support_rounded, 0);
|
||||
const weightedNonpass = rows.reduce(
|
||||
(sum, row) => sum + row.support_rounded * row.nonpass_rate,
|
||||
0,
|
||||
);
|
||||
const nonpassRate = totalSupport > 0 ? weightedNonpass / totalSupport : 0;
|
||||
const counties = [...new Set(rows.map((row) => row.public_county))].sort();
|
||||
const latest = periods[periods.length - 1];
|
||||
|
||||
setText("kpi-eligible", `≈${compactNumber(totalSupport)}`);
|
||||
setText("kpi-pass", `≈${percent(1 - nonpassRate)}`);
|
||||
setText("kpi-nonpass", `≈${percent(nonpassRate)}`);
|
||||
setText("kpi-counties", String(counties.length));
|
||||
setText("data-cutoff", latest ? `${latest.year} Q${latest.quarter}` : "Unavailable");
|
||||
setText(
|
||||
"model-version",
|
||||
`Release ${data.manifest.release_id.slice(0, 8)} · ${data.manifest.model_versions.join(" / ")}`,
|
||||
);
|
||||
renderOutcomeTrend(byId("outcome-trend-chart"), rows);
|
||||
setText(
|
||||
"outcome-trend-note",
|
||||
"The current approved bundle publishes binary non-pass rates only. Four-class outcome mix and blank rates are not inferred or displayed.",
|
||||
);
|
||||
renderAgeRisk(byId("age-risk-chart"), data.ageRisk.rows);
|
||||
renderUtahCoverage(byId("utah-coverage-map"), counties);
|
||||
renderCountyList(counties);
|
||||
}
|
||||
|
||||
function scorecardLabel(row) {
|
||||
return `${row.prior_make} ${row.prior_model}`.trim();
|
||||
}
|
||||
|
||||
function sortedScorecards(rows, mode) {
|
||||
const sorted = [...rows];
|
||||
if (mode === "risk-desc") sorted.sort((a, b) => b.nonpass_rate - a.nonpass_rate);
|
||||
else if (mode === "risk-asc") sorted.sort((a, b) => a.nonpass_rate - b.nonpass_rate);
|
||||
else if (mode === "name") sorted.sort((a, b) => scorecardLabel(a).localeCompare(scorecardLabel(b)));
|
||||
else sorted.sort((a, b) => b.support_rounded - a.support_rounded);
|
||||
return sorted;
|
||||
}
|
||||
|
||||
function renderScorecardCards(rows) {
|
||||
const container = byId("cohort-cards");
|
||||
container.replaceChildren();
|
||||
for (const row of rows.slice(0, 18)) {
|
||||
const article = document.createElement("article");
|
||||
article.className = "cohort-card";
|
||||
const heading = document.createElement("h3");
|
||||
heading.textContent = scorecardLabel(row);
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "cohort-card__meta";
|
||||
meta.textContent = `${compactNumber(row.support_rounded)} rounded support`;
|
||||
const bar = document.createElement("div");
|
||||
bar.className = "risk-bar";
|
||||
bar.setAttribute("aria-hidden", "true");
|
||||
const fill = document.createElement("span");
|
||||
fill.style.width = `${Math.min(100, row.nonpass_rate * 100)}%`;
|
||||
bar.append(fill);
|
||||
const value = document.createElement("div");
|
||||
value.className = "cohort-card__value";
|
||||
const strong = document.createElement("strong");
|
||||
strong.textContent = percent(row.nonpass_rate);
|
||||
const small = document.createElement("small");
|
||||
small.textContent = "Observed non-pass";
|
||||
value.append(strong, small);
|
||||
article.append(heading, meta, bar, value);
|
||||
container.append(article);
|
||||
}
|
||||
}
|
||||
|
||||
function updateExplorer() {
|
||||
if (!dashboardData) return;
|
||||
const query = byId("cohort-search").value.trim().toLowerCase();
|
||||
const sortMode = byId("result-sort").value;
|
||||
const matches = dashboardData.scorecards.rows.filter((row) =>
|
||||
scorecardLabel(row).toLowerCase().includes(query),
|
||||
);
|
||||
visibleScorecards = sortedScorecards(matches, sortMode);
|
||||
setText(
|
||||
"result-summary",
|
||||
`${visibleScorecards.length} supported cohort${visibleScorecards.length === 1 ? "" : "s"}; showing up to 18 cards and 12 chart rows.`,
|
||||
);
|
||||
byId("empty-results").hidden = visibleScorecards.length > 0;
|
||||
renderCohortDotPlot(byId("cohort-dot-plot"), visibleScorecards);
|
||||
renderScorecardCards(visibleScorecards);
|
||||
}
|
||||
|
||||
function initializeExplorer(data) {
|
||||
populateSelect("filter-county", data.filters.public_counties);
|
||||
populateSelect("filter-age", data.filters.age_bands, (value) => String(value));
|
||||
populateSelect(
|
||||
"filter-period",
|
||||
data.filters.periods.map((period) => period.year),
|
||||
(value) => String(value),
|
||||
);
|
||||
|
||||
// Current scorecards are make/model aggregates only. These planned controls
|
||||
// remain visible but disabled so the UI never implies unsupported slicing.
|
||||
disableUnsupportedFilter("filter-county", "Unavailable at current scorecard grain");
|
||||
disableUnsupportedFilter("filter-age", "Unavailable at current scorecard grain");
|
||||
disableUnsupportedFilter("filter-fuel", "Fuel not published in this bundle");
|
||||
disableUnsupportedFilter("filter-program", "Program not published in this bundle");
|
||||
disableUnsupportedFilter("filter-period", "Period not published in this scorecard");
|
||||
const adjusted = document.querySelector('input[name="risk-view"][value="adjusted"]');
|
||||
adjusted.disabled = true;
|
||||
adjusted.parentElement.title = "Model-adjusted cohort scorecards are not published.";
|
||||
|
||||
byId("cohort-search").addEventListener("input", updateExplorer);
|
||||
byId("result-sort").addEventListener("change", updateExplorer);
|
||||
byId("reset-filters").addEventListener("click", () => {
|
||||
byId("explorer-filters").reset();
|
||||
byId("cohort-search").value = "";
|
||||
byId("result-sort").value = "support";
|
||||
updateExplorer();
|
||||
});
|
||||
updateExplorer();
|
||||
}
|
||||
|
||||
function chooseDiagnostic(rows) {
|
||||
const partitionRank = { calibrate: 3, tune: 2, train: 1 };
|
||||
return [...rows].sort((left, right) => {
|
||||
const preferredLeft = left.model === "logistic_platt" ? 1 : 0;
|
||||
const preferredRight = right.model === "logistic_platt" ? 1 : 0;
|
||||
return (
|
||||
preferredRight - preferredLeft ||
|
||||
(partitionRank[right.partition] ?? 0) - (partitionRank[left.partition] ?? 0)
|
||||
);
|
||||
})[0];
|
||||
}
|
||||
|
||||
function renderMethods(data) {
|
||||
renderCoverageHeatmap(byId("coverage-heatmap"), data.coverage.rows);
|
||||
const diagnostic = chooseDiagnostic(data.diagnostics.rows);
|
||||
setText(
|
||||
"diagnostic-scope-label",
|
||||
diagnostic
|
||||
? `${diagnostic.partition === "calibrate" ? "Calibration cohort" : "Development diagnostics"} · ${diagnostic.model}`
|
||||
: "Development diagnostics · model unavailable",
|
||||
);
|
||||
setText("diagnostic-ap", diagnostic ? diagnostic.average_precision.toFixed(3) : "—");
|
||||
setText("diagnostic-brier", diagnostic ? diagnostic.brier.toFixed(3) : "—");
|
||||
setText("diagnostic-n", "Not published");
|
||||
renderNoCalibration(byId("calibration-chart"));
|
||||
}
|
||||
|
||||
function renderDashboard(data) {
|
||||
dashboardData = data;
|
||||
setPreviewBanner(data.manifest);
|
||||
renderOverview(data);
|
||||
initializeExplorer(data);
|
||||
renderMethods(data);
|
||||
setHeaderStatus(
|
||||
"ready",
|
||||
data.manifest.development_preview ? "Validated development aggregates" : "Validated public aggregates",
|
||||
);
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
initializeRouting();
|
||||
try {
|
||||
const data = await loadDashboardData();
|
||||
renderDashboard(data);
|
||||
} catch (error) {
|
||||
console.error("Dashboard data contract rejected the public assets.", error);
|
||||
setUnavailable(error);
|
||||
}
|
||||
}
|
||||
|
||||
initialize();
|
||||
|
||||
export { displayCategory, normalizeCounty, routeFromHash };
|
||||
@@ -0,0 +1,281 @@
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
function escapeText(value) {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function percent(value, digits = 1) {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "percent",
|
||||
minimumFractionDigits: digits,
|
||||
maximumFractionDigits: digits,
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
function compactNumber(value) {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
notation: value >= 10_000 ? "compact" : "standard",
|
||||
maximumFractionDigits: 1,
|
||||
}).format(value);
|
||||
}
|
||||
|
||||
function emptyChart(container, message) {
|
||||
container.innerHTML = `<div class="chart-empty"><p>${escapeText(message)}</p></div>`;
|
||||
container.setAttribute("aria-label", message);
|
||||
}
|
||||
|
||||
function aggregateOverview(rows) {
|
||||
const groups = new Map();
|
||||
for (const row of rows) {
|
||||
const key = `${row.year}-Q${row.quarter}`;
|
||||
const current = groups.get(key) ?? {
|
||||
label: `${row.year} Q${row.quarter}`,
|
||||
year: row.year,
|
||||
quarter: row.quarter,
|
||||
support: 0,
|
||||
weightedRisk: 0,
|
||||
};
|
||||
current.support += row.support_rounded;
|
||||
current.weightedRisk += row.support_rounded * row.nonpass_rate;
|
||||
groups.set(key, current);
|
||||
}
|
||||
return [...groups.values()]
|
||||
.map((group) => ({
|
||||
...group,
|
||||
risk: group.support > 0 ? group.weightedRisk / group.support : 0,
|
||||
}))
|
||||
.sort((left, right) => left.year - right.year || left.quarter - right.quarter);
|
||||
}
|
||||
|
||||
export function renderOutcomeTrend(container, rows) {
|
||||
const points = aggregateOverview(rows);
|
||||
if (points.length < 2) {
|
||||
emptyChart(container, "Not enough approved periods to draw a trend.");
|
||||
return;
|
||||
}
|
||||
const width = 920;
|
||||
const height = 330;
|
||||
const margin = { top: 24, right: 22, bottom: 54, left: 58 };
|
||||
const plotWidth = width - margin.left - margin.right;
|
||||
const plotHeight = height - margin.top - margin.bottom;
|
||||
const maxRisk = Math.max(0.05, ...points.map((point) => point.risk));
|
||||
const yMax = Math.min(1, Math.ceil(maxRisk * 20) / 20);
|
||||
const x = (index) => margin.left + (index / (points.length - 1)) * plotWidth;
|
||||
const y = (value) => margin.top + plotHeight - (value / yMax) * plotHeight;
|
||||
const path = points
|
||||
.map((point, index) => `${index === 0 ? "M" : "L"}${x(index).toFixed(1)},${y(point.risk).toFixed(1)}`)
|
||||
.join(" ");
|
||||
|
||||
const yTicks = Array.from({ length: 5 }, (_, index) => (index / 4) * yMax);
|
||||
const tickEvery = Math.max(1, Math.ceil(points.length / 8));
|
||||
const labelled = points.filter(
|
||||
(_point, index) => index % tickEvery === 0 || index === points.length - 1,
|
||||
);
|
||||
const description = points
|
||||
.map((point) => `${point.label}: ${percent(point.risk)}`)
|
||||
.join("; ");
|
||||
|
||||
container.innerHTML = `
|
||||
<svg viewBox="0 0 ${width} ${height}" aria-hidden="true" focusable="false">
|
||||
${yTicks
|
||||
.map(
|
||||
(tick) => `
|
||||
<line class="grid-line" x1="${margin.left}" x2="${width - margin.right}" y1="${y(tick)}" y2="${y(tick)}"></line>
|
||||
<text class="axis-label" x="${margin.left - 10}" y="${y(tick) + 4}" text-anchor="end">${percent(tick, 0)}</text>`,
|
||||
)
|
||||
.join("")}
|
||||
<line class="axis-line" x1="${margin.left}" x2="${width - margin.right}" y1="${height - margin.bottom}" y2="${height - margin.bottom}"></line>
|
||||
<path class="chart-line" d="${path}"></path>
|
||||
${points
|
||||
.map(
|
||||
(point, index) => `
|
||||
<circle class="chart-dot" cx="${x(index)}" cy="${y(point.risk)}" r="4">
|
||||
<title>${escapeText(point.label)}: ${percent(point.risk)} non-pass (${compactNumber(point.support)} rounded support)</title>
|
||||
</circle>`,
|
||||
)
|
||||
.join("")}
|
||||
${labelled
|
||||
.map((point) => {
|
||||
const index = points.indexOf(point);
|
||||
return `<text class="axis-label" x="${x(index)}" y="${height - 24}" text-anchor="middle">${escapeText(point.label.replace(" ", "\u00a0"))}</text>`;
|
||||
})
|
||||
.join("")}
|
||||
<text class="axis-label" transform="translate(16 ${height / 2}) rotate(-90)" text-anchor="middle">Non-pass rate</text>
|
||||
</svg>`;
|
||||
container.setAttribute(
|
||||
"aria-label",
|
||||
`Quarterly aggregate non-pass trend. ${description}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function renderAgeRisk(container, rows) {
|
||||
if (!rows.length) {
|
||||
emptyChart(container, "No supported vehicle-age bands are available.");
|
||||
return;
|
||||
}
|
||||
const points = [...rows];
|
||||
const width = 600;
|
||||
const height = 300;
|
||||
const margin = { top: 24, right: 20, bottom: 58, left: 52 };
|
||||
const plotWidth = width - margin.left - margin.right;
|
||||
const plotHeight = height - margin.top - margin.bottom;
|
||||
const maxRisk = Math.max(0.05, ...points.map((point) => point.nonpass_rate));
|
||||
const yMax = Math.min(1, Math.ceil(maxRisk * 20) / 20);
|
||||
const denominator = Math.max(1, points.length - 1);
|
||||
const x = (index) => margin.left + (index / denominator) * plotWidth;
|
||||
const y = (value) => margin.top + plotHeight - (value / yMax) * plotHeight;
|
||||
const path = points
|
||||
.map((point, index) => `${index === 0 ? "M" : "L"}${x(index)},${y(point.nonpass_rate)}`)
|
||||
.join(" ");
|
||||
|
||||
container.innerHTML = `
|
||||
<svg viewBox="0 0 ${width} ${height}" aria-hidden="true" focusable="false">
|
||||
${[0, 0.25, 0.5, 0.75, 1]
|
||||
.map((ratio) => {
|
||||
const tick = ratio * yMax;
|
||||
return `<line class="grid-line" x1="${margin.left}" x2="${width - margin.right}" y1="${y(tick)}" y2="${y(tick)}"></line>
|
||||
<text class="axis-label" x="${margin.left - 8}" y="${y(tick) + 4}" text-anchor="end">${percent(tick, 0)}</text>`;
|
||||
})
|
||||
.join("")}
|
||||
<path class="chart-line" d="${path}"></path>
|
||||
${points
|
||||
.map(
|
||||
(point, index) => `
|
||||
<circle class="chart-dot" cx="${x(index)}" cy="${y(point.nonpass_rate)}" r="5">
|
||||
<title>${escapeText(point.age_band)}: ${percent(point.nonpass_rate)} (${compactNumber(point.support_rounded)} rounded support)</title>
|
||||
</circle>
|
||||
<text class="axis-label" x="${x(index)}" y="${height - 25}" text-anchor="middle">${escapeText(point.age_band)}</text>`,
|
||||
)
|
||||
.join("")}
|
||||
</svg>`;
|
||||
container.setAttribute(
|
||||
"aria-label",
|
||||
`Non-pass risk by vehicle-age band. ${points
|
||||
.map((point) => `${point.age_band}: ${percent(point.nonpass_rate)}`)
|
||||
.join("; ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function renderCohortDotPlot(container, rows) {
|
||||
const points = rows.slice(0, 12);
|
||||
if (!points.length) {
|
||||
emptyChart(container, "No supported cohorts match the current filters.");
|
||||
return;
|
||||
}
|
||||
const width = 820;
|
||||
const rowHeight = 34;
|
||||
const margin = { top: 24, right: 60, bottom: 42, left: 220 };
|
||||
const height = margin.top + margin.bottom + points.length * rowHeight;
|
||||
const plotWidth = width - margin.left - margin.right;
|
||||
const maxRisk = Math.max(0.05, ...points.map((point) => point.nonpass_rate));
|
||||
const xMax = Math.min(1, Math.ceil(maxRisk * 20) / 20);
|
||||
const x = (value) => margin.left + (value / xMax) * plotWidth;
|
||||
|
||||
container.innerHTML = `
|
||||
<svg viewBox="0 0 ${width} ${height}" aria-hidden="true" focusable="false">
|
||||
${[0, 0.25, 0.5, 0.75, 1]
|
||||
.map((ratio) => {
|
||||
const value = ratio * xMax;
|
||||
return `<line class="grid-line" x1="${x(value)}" x2="${x(value)}" y1="${margin.top - 8}" y2="${height - margin.bottom + 5}"></line>
|
||||
<text class="axis-label" x="${x(value)}" y="${height - 15}" text-anchor="middle">${percent(value, 0)}</text>`;
|
||||
})
|
||||
.join("")}
|
||||
${points
|
||||
.map((point, index) => {
|
||||
const y = margin.top + index * rowHeight + rowHeight / 2;
|
||||
const label = `${point.prior_make} ${point.prior_model}`;
|
||||
return `
|
||||
<text class="axis-label" x="${margin.left - 12}" y="${y + 4}" text-anchor="end">${escapeText(label)}</text>
|
||||
<line x1="${margin.left}" x2="${x(point.nonpass_rate)}" y1="${y}" y2="${y}" stroke="var(--red-100)" stroke-width="8" stroke-linecap="round"></line>
|
||||
<circle class="chart-dot" cx="${x(point.nonpass_rate)}" cy="${y}" r="5"><title>${escapeText(label)}: ${percent(point.nonpass_rate)}, ${compactNumber(point.support_rounded)} rounded support</title></circle>
|
||||
<text class="axis-label" x="${x(point.nonpass_rate) + 10}" y="${y + 4}">${percent(point.nonpass_rate)}</text>`;
|
||||
})
|
||||
.join("")}
|
||||
</svg>`;
|
||||
container.setAttribute(
|
||||
"aria-label",
|
||||
`Ranked supported cohort non-pass risk. ${points
|
||||
.map((point) => `${point.prior_make} ${point.prior_model}: ${percent(point.nonpass_rate)}`)
|
||||
.join("; ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
const COUNTY_POINTS = {
|
||||
cache: [154, 40],
|
||||
weber: [137, 86],
|
||||
davis: [126, 108],
|
||||
salt_lake: [133, 133],
|
||||
"salt lake": [133, 133],
|
||||
utah: [140, 170],
|
||||
};
|
||||
|
||||
export function renderUtahCoverage(container, coveredCounties) {
|
||||
const normalized = new Set(coveredCounties.map((county) => county.toLowerCase()));
|
||||
const points = [...normalized]
|
||||
.map((county) => ({ county, coordinates: COUNTY_POINTS[county] }))
|
||||
.filter((item) => item.coordinates);
|
||||
container.innerHTML = `
|
||||
<svg viewBox="0 0 270 300" aria-hidden="true" focusable="false">
|
||||
<path d="M79 18h101v54l18 18v183H52V116l27-27V18Z" fill="var(--gray-100)" stroke="var(--gray-300)" stroke-width="3"></path>
|
||||
<path d="M80 20h98v53l17 18v179H55V117l25-27V20Z" fill="none" stroke="var(--sand-200)" stroke-width="1.5" stroke-dasharray="4 5"></path>
|
||||
${points
|
||||
.map(
|
||||
({ county, coordinates }) => `
|
||||
<circle cx="${coordinates[0]}" cy="${coordinates[1]}" r="8" fill="var(--teal-700)" stroke="var(--paper)" stroke-width="3"><title>${escapeText(county.replace("_", " "))} feed available</title></circle>`,
|
||||
)
|
||||
.join("")}
|
||||
<text x="135" y="288" text-anchor="middle" class="axis-label">Participating feeds highlighted</text>
|
||||
</svg>`;
|
||||
container.setAttribute(
|
||||
"aria-label",
|
||||
coveredCounties.length
|
||||
? `Utah feed coverage includes ${coveredCounties.join(", ")}. Other counties are unavailable.`
|
||||
: "No county feed coverage is available.",
|
||||
);
|
||||
}
|
||||
|
||||
export function renderCoverageHeatmap(container, rows) {
|
||||
if (!rows.length) {
|
||||
emptyChart(container, "No source-era coverage rows are available.");
|
||||
return;
|
||||
}
|
||||
const years = [...new Set(rows.map((row) => row.year))].sort((a, b) => a - b);
|
||||
const eras = [...new Set(rows.map((row) => row.source_era))].sort();
|
||||
const lookup = new Map(rows.map((row) => [`${row.source_era}-${row.year}`, row]));
|
||||
const maxSupport = Math.max(1, ...rows.map((row) => row.support_rounded));
|
||||
const columns = `90px repeat(${years.length}, minmax(34px, 1fr))`;
|
||||
container.innerHTML = `
|
||||
<div class="heatmap-grid" style="grid-template-columns:${columns}">
|
||||
<span></span>${years.map((year) => `<span class="heatmap-label">${year}</span>`).join("")}
|
||||
${eras
|
||||
.map(
|
||||
(era) => `<span class="heatmap-label">${escapeText(era)}</span>${years
|
||||
.map((year) => {
|
||||
const row = lookup.get(`${era}-${year}`);
|
||||
if (!row) return `<span class="heatmap-cell" title="${escapeText(era)} ${year}: unavailable"></span>`;
|
||||
const alpha = 0.15 + 0.75 * Math.sqrt(row.support_rounded / maxSupport);
|
||||
return `<span class="heatmap-cell" style="background:rgb(25 116 119 / ${alpha.toFixed(2)})" title="${escapeText(era)} ${year}: ${compactNumber(row.support_rounded)} rounded support; ${percent(row.labeled_rate)} labeled"></span>`;
|
||||
})
|
||||
.join("")}`,
|
||||
)
|
||||
.join("")}
|
||||
</div>`;
|
||||
container.setAttribute(
|
||||
"aria-label",
|
||||
`Source-era coverage from ${years[0]} through ${years[years.length - 1]} for ${eras.join(", ")}.`,
|
||||
);
|
||||
}
|
||||
|
||||
export function renderNoCalibration(container) {
|
||||
emptyChart(
|
||||
container,
|
||||
"Calibration-bin data is not published in this development bundle. No calibration curve is shown.",
|
||||
);
|
||||
}
|
||||
|
||||
export { aggregateOverview, compactNumber, percent };
|
||||
@@ -0,0 +1,574 @@
|
||||
const SCHEMA_VERSION = "dashboard_data_v1";
|
||||
const APPROVED_PARTITIONS = Object.freeze(["train", "tune", "calibrate"]);
|
||||
const ENVELOPE_KEYS = Object.freeze([
|
||||
"development_preview",
|
||||
"population_estimate_allowed",
|
||||
"schema_version",
|
||||
]);
|
||||
|
||||
export const REQUIRED_ASSETS = Object.freeze({
|
||||
manifest: "data_manifest.json",
|
||||
overview: "overview_period_county.json",
|
||||
ageRisk: "age_risk_curve.json",
|
||||
scorecards: "cohort_scorecard.json",
|
||||
diagnostics: "model_diagnostics.json",
|
||||
coverage: "coverage_quality.json",
|
||||
filters: "filter_catalog.json",
|
||||
shaManifest: "sha256_manifest.json",
|
||||
});
|
||||
|
||||
const SENSITIVE_KEY_PARTS = new Set([
|
||||
"address",
|
||||
"certificate",
|
||||
"email",
|
||||
"internal",
|
||||
"ip",
|
||||
"owner",
|
||||
"pid",
|
||||
"plate",
|
||||
"raw",
|
||||
"session",
|
||||
"station",
|
||||
"token",
|
||||
"user",
|
||||
"vin",
|
||||
"zip",
|
||||
]);
|
||||
|
||||
const ASSET_VALIDATORS = {
|
||||
overview: (value) => validateRows(value, validateOverviewRow, "overview_period_county"),
|
||||
ageRisk: (value) => validateRows(value, validateAgeRiskRow, "age_risk_curve"),
|
||||
scorecards: (value) => validateRows(value, validateScorecardRow, "cohort_scorecard"),
|
||||
diagnostics: (value) => validateRows(value, validateDiagnosticRow, "model_diagnostics"),
|
||||
coverage: (value) => validateRows(value, validateCoverageRow, "coverage_quality"),
|
||||
filters: validateFilterCatalog,
|
||||
shaManifest: validateShaManifest,
|
||||
};
|
||||
|
||||
export class DataContractError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = "DataContractError";
|
||||
}
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
||||
const prototype = Object.getPrototypeOf(value);
|
||||
return prototype === Object.prototype || prototype === null;
|
||||
}
|
||||
|
||||
function requirePlainObject(value, label) {
|
||||
if (!isPlainObject(value)) {
|
||||
throw new DataContractError(`${label} must be a JSON object.`);
|
||||
}
|
||||
}
|
||||
|
||||
function requireExactKeys(value, expectedKeys, label) {
|
||||
const observed = Object.keys(value).sort();
|
||||
const expected = [...expectedKeys].sort();
|
||||
if (JSON.stringify(observed) !== JSON.stringify(expected)) {
|
||||
throw new DataContractError(`${label} does not match the approved field schema.`);
|
||||
}
|
||||
}
|
||||
|
||||
function requireExactStringSet(value, expectedValues, label) {
|
||||
validateStringArray(value, label);
|
||||
const observed = [...value].sort();
|
||||
const expected = [...expectedValues].sort();
|
||||
if (JSON.stringify(observed) !== JSON.stringify(expected)) {
|
||||
throw new DataContractError(`${label} does not match the approved values.`);
|
||||
}
|
||||
}
|
||||
|
||||
function requireBoolean(value, label) {
|
||||
if (typeof value !== "boolean") {
|
||||
throw new DataContractError(`${label} must be true or false.`);
|
||||
}
|
||||
}
|
||||
|
||||
function requireString(value, label) {
|
||||
if (typeof value !== "string" || value.trim() === "") {
|
||||
throw new DataContractError(`${label} must be a non-empty string.`);
|
||||
}
|
||||
}
|
||||
|
||||
function requireSafeModelVersion(value, label) {
|
||||
requireString(value, label);
|
||||
if (!/^[A-Za-z0-9._-]{1,64}$/.test(value)) {
|
||||
throw new DataContractError(`${label} is not an approved model version.`);
|
||||
}
|
||||
}
|
||||
|
||||
function requireNumber(value, label, { min = -Infinity, max = Infinity } = {}) {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
|
||||
throw new DataContractError(`${label} must be a finite number in the approved range.`);
|
||||
}
|
||||
}
|
||||
|
||||
function requireInteger(value, label, { min = -Infinity, max = Infinity } = {}) {
|
||||
requireNumber(value, label, { min, max });
|
||||
if (!Number.isInteger(value)) {
|
||||
throw new DataContractError(`${label} must be an integer.`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateEnvelope(value, label) {
|
||||
requirePlainObject(value, label);
|
||||
if (value.schema_version !== SCHEMA_VERSION) {
|
||||
throw new DataContractError(`${label} uses an unsupported schema version.`);
|
||||
}
|
||||
requireBoolean(value.development_preview, `${label}.development_preview`);
|
||||
requireBoolean(
|
||||
value.population_estimate_allowed,
|
||||
`${label}.population_estimate_allowed`,
|
||||
);
|
||||
rejectSensitiveKeys(value, label);
|
||||
}
|
||||
|
||||
function validateRows(value, rowValidator, label) {
|
||||
validateEnvelope(value, label);
|
||||
requireExactKeys(value, [...ENVELOPE_KEYS, "rows"], label);
|
||||
if (!Array.isArray(value.rows)) {
|
||||
throw new DataContractError(`${label}.rows must be an array.`);
|
||||
}
|
||||
value.rows.forEach((row, index) => {
|
||||
requirePlainObject(row, `${label}.rows[${index}]`);
|
||||
rowValidator(row, `${label}.rows[${index}]`);
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
function validateOverviewRow(row, label) {
|
||||
requireExactKeys(
|
||||
row,
|
||||
["year", "quarter", "public_county", "support_rounded", "nonpass_rate"],
|
||||
label,
|
||||
);
|
||||
requireInteger(row.year, `${label}.year`, { min: 2010, max: 2100 });
|
||||
requireInteger(row.quarter, `${label}.quarter`, { min: 1, max: 4 });
|
||||
requireString(row.public_county, `${label}.public_county`);
|
||||
requireInteger(row.support_rounded, `${label}.support_rounded`, { min: 0 });
|
||||
requireNumber(row.nonpass_rate, `${label}.nonpass_rate`, { min: 0, max: 1 });
|
||||
}
|
||||
|
||||
function validateAgeRiskRow(row, label) {
|
||||
requireExactKeys(row, ["age_band", "support_rounded", "nonpass_rate"], label);
|
||||
requireString(row.age_band, `${label}.age_band`);
|
||||
requireInteger(row.support_rounded, `${label}.support_rounded`, { min: 0 });
|
||||
requireNumber(row.nonpass_rate, `${label}.nonpass_rate`, { min: 0, max: 1 });
|
||||
}
|
||||
|
||||
function validateScorecardRow(row, label) {
|
||||
requireExactKeys(
|
||||
row,
|
||||
["prior_make", "prior_model", "support_rounded", "nonpass_rate"],
|
||||
label,
|
||||
);
|
||||
requireString(row.prior_make, `${label}.prior_make`);
|
||||
requireString(row.prior_model, `${label}.prior_model`);
|
||||
requireInteger(row.support_rounded, `${label}.support_rounded`, { min: 0 });
|
||||
requireNumber(row.nonpass_rate, `${label}.nonpass_rate`, { min: 0, max: 1 });
|
||||
}
|
||||
|
||||
function validateDiagnosticRow(row, label) {
|
||||
requireExactKeys(
|
||||
row,
|
||||
[
|
||||
"model",
|
||||
"partition",
|
||||
"average_precision",
|
||||
"brier",
|
||||
"log_loss",
|
||||
"roc_auc",
|
||||
"top_10_capture",
|
||||
],
|
||||
label,
|
||||
);
|
||||
requireString(row.model, `${label}.model`);
|
||||
requireString(row.partition, `${label}.partition`);
|
||||
if (!APPROVED_PARTITIONS.includes(row.partition)) {
|
||||
throw new DataContractError(`${label}.partition is not approved for this public schema.`);
|
||||
}
|
||||
for (const metric of [
|
||||
"average_precision",
|
||||
"brier",
|
||||
"log_loss",
|
||||
"roc_auc",
|
||||
"top_10_capture",
|
||||
]) {
|
||||
requireNumber(row[metric], `${label}.${metric}`, { min: 0 });
|
||||
}
|
||||
if (row.average_precision > 1 || row.brier > 1 || row.roc_auc > 1 || row.top_10_capture > 1) {
|
||||
throw new DataContractError(`${label} contains a probability metric above 1.`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateCoverageRow(row, label) {
|
||||
requireExactKeys(
|
||||
row,
|
||||
[
|
||||
"year",
|
||||
"source_era",
|
||||
"support_rounded",
|
||||
"labeled_rate",
|
||||
"overall_result_share",
|
||||
"utah_obd_proxy_share",
|
||||
],
|
||||
label,
|
||||
);
|
||||
requireInteger(row.year, `${label}.year`, { min: 2010, max: 2100 });
|
||||
requireString(row.source_era, `${label}.source_era`);
|
||||
requireInteger(row.support_rounded, `${label}.support_rounded`, { min: 0 });
|
||||
for (const metric of ["labeled_rate", "overall_result_share", "utah_obd_proxy_share"]) {
|
||||
requireNumber(row[metric], `${label}.${metric}`, { min: 0, max: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
function validateStringArray(value, label) {
|
||||
if (!Array.isArray(value)) throw new DataContractError(`${label} must be an array.`);
|
||||
value.forEach((item, index) => requireString(item, `${label}[${index}]`));
|
||||
}
|
||||
|
||||
function validateFilterCatalog(value) {
|
||||
const label = "filter_catalog";
|
||||
validateEnvelope(value, label);
|
||||
requireExactKeys(
|
||||
value,
|
||||
[
|
||||
...ENVELOPE_KEYS,
|
||||
"public_counties",
|
||||
"age_bands",
|
||||
"prior_make_models",
|
||||
"periods",
|
||||
"models",
|
||||
"partitions",
|
||||
],
|
||||
label,
|
||||
);
|
||||
validateStringArray(value.public_counties, `${label}.public_counties`);
|
||||
validateStringArray(value.age_bands, `${label}.age_bands`);
|
||||
validateStringArray(value.models, `${label}.models`);
|
||||
requireExactStringSet(value.partitions, APPROVED_PARTITIONS, `${label}.partitions`);
|
||||
if (!Array.isArray(value.prior_make_models)) {
|
||||
throw new DataContractError(`${label}.prior_make_models must be an array.`);
|
||||
}
|
||||
value.prior_make_models.forEach((row, index) => {
|
||||
requirePlainObject(row, `${label}.prior_make_models[${index}]`);
|
||||
requireExactKeys(
|
||||
row,
|
||||
["prior_make", "prior_model"],
|
||||
`${label}.prior_make_models[${index}]`,
|
||||
);
|
||||
requireString(row.prior_make, `${label}.prior_make_models[${index}].prior_make`);
|
||||
requireString(row.prior_model, `${label}.prior_make_models[${index}].prior_model`);
|
||||
});
|
||||
if (!Array.isArray(value.periods)) {
|
||||
throw new DataContractError(`${label}.periods must be an array.`);
|
||||
}
|
||||
value.periods.forEach((period, index) => {
|
||||
requirePlainObject(period, `${label}.periods[${index}]`);
|
||||
requireExactKeys(period, ["year", "quarters"], `${label}.periods[${index}]`);
|
||||
requireInteger(period.year, `${label}.periods[${index}].year`, {
|
||||
min: 2010,
|
||||
max: 2100,
|
||||
});
|
||||
if (!Array.isArray(period.quarters) || period.quarters.length === 0) {
|
||||
throw new DataContractError(`${label}.periods[${index}].quarters must be non-empty.`);
|
||||
}
|
||||
period.quarters.forEach((quarter, quarterIndex) =>
|
||||
requireInteger(quarter, `${label}.periods[${index}].quarters[${quarterIndex}]`, {
|
||||
min: 1,
|
||||
max: 4,
|
||||
}),
|
||||
);
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
function validateShaManifest(value) {
|
||||
const label = "sha256_manifest";
|
||||
validateEnvelope(value, label);
|
||||
requireExactKeys(value, [...ENVELOPE_KEYS, "files"], label);
|
||||
if (!Array.isArray(value.files)) {
|
||||
throw new DataContractError(`${label}.files must be an array.`);
|
||||
}
|
||||
const expectedNames = Object.entries(REQUIRED_ASSETS)
|
||||
.filter(([name]) => name !== "shaManifest")
|
||||
.map(([, filename]) => filename)
|
||||
.sort();
|
||||
const observedNames = [];
|
||||
for (const [index, file] of value.files.entries()) {
|
||||
requirePlainObject(file, `${label}.files[${index}]`);
|
||||
requireExactKeys(file, ["name", "sha256"], `${label}.files[${index}]`);
|
||||
requireString(file.name, `${label}.files[${index}].name`);
|
||||
requireString(file.sha256, `${label}.files[${index}].sha256`);
|
||||
if (!/^[0-9a-f]{64}$/.test(file.sha256)) {
|
||||
throw new DataContractError(`${label}.files[${index}].sha256 is invalid.`);
|
||||
}
|
||||
observedNames.push(file.name);
|
||||
}
|
||||
observedNames.sort();
|
||||
if (JSON.stringify(observedNames) !== JSON.stringify(expectedNames)) {
|
||||
throw new DataContractError(`${label} does not cover the exact approved asset set.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function rejectSensitiveKeys(value, label, seen = new WeakSet()) {
|
||||
if (value === null || typeof value !== "object") return;
|
||||
if (seen.has(value)) return;
|
||||
seen.add(value);
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item, index) => rejectSensitiveKeys(item, `${label}[${index}]`, seen));
|
||||
return;
|
||||
}
|
||||
for (const [key, child] of Object.entries(value)) {
|
||||
const normalizedParts = key.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
||||
if (normalizedParts.some((part) => SENSITIVE_KEY_PARTS.has(part))) {
|
||||
throw new DataContractError(`${label} contains a field outside the public data contract.`);
|
||||
}
|
||||
rejectSensitiveKeys(child, `${label}.${key}`, seen);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateAssetSet(rawAssets) {
|
||||
requirePlainObject(rawAssets, "asset set");
|
||||
requireExactKeys(rawAssets, Object.keys(REQUIRED_ASSETS), "asset set");
|
||||
const manifest = rawAssets.manifest;
|
||||
validateEnvelope(manifest, "data_manifest");
|
||||
requireExactKeys(
|
||||
manifest,
|
||||
[
|
||||
...ENVELOPE_KEYS,
|
||||
"assets",
|
||||
"data_scope",
|
||||
"definitions",
|
||||
"model_versions",
|
||||
"release_id",
|
||||
],
|
||||
"data_manifest",
|
||||
);
|
||||
if (Object.hasOwn(manifest, "rows")) {
|
||||
throw new DataContractError("data_manifest must not contain row data.");
|
||||
}
|
||||
requirePlainObject(manifest.data_scope, "data_manifest.data_scope");
|
||||
requirePlainObject(manifest.definitions, "data_manifest.definitions");
|
||||
requireString(manifest.release_id, "data_manifest.release_id");
|
||||
if (!/^[0-9a-f]{64}$/.test(manifest.release_id)) {
|
||||
throw new DataContractError("data_manifest.release_id is invalid.");
|
||||
}
|
||||
if (!Array.isArray(manifest.model_versions) || manifest.model_versions.length === 0) {
|
||||
throw new DataContractError("data_manifest.model_versions must be a non-empty array.");
|
||||
}
|
||||
manifest.model_versions.forEach((version, index) =>
|
||||
requireSafeModelVersion(version, `data_manifest.model_versions[${index}]`),
|
||||
);
|
||||
if (
|
||||
JSON.stringify(manifest.model_versions) !==
|
||||
JSON.stringify([...new Set(manifest.model_versions)].sort())
|
||||
) {
|
||||
throw new DataContractError("data_manifest.model_versions must be sorted and unique.");
|
||||
}
|
||||
requireExactKeys(
|
||||
manifest.data_scope,
|
||||
["first_year", "last_year", "model_names", "partitions"],
|
||||
"data_manifest.data_scope",
|
||||
);
|
||||
requireInteger(manifest.data_scope.first_year, "data_manifest.data_scope.first_year", {
|
||||
min: 2010,
|
||||
max: 2100,
|
||||
});
|
||||
requireInteger(manifest.data_scope.last_year, "data_manifest.data_scope.last_year", {
|
||||
min: manifest.data_scope.first_year,
|
||||
max: 2100,
|
||||
});
|
||||
validateStringArray(manifest.data_scope.model_names, "data_manifest.data_scope.model_names");
|
||||
requireExactStringSet(
|
||||
manifest.data_scope.partitions,
|
||||
APPROVED_PARTITIONS,
|
||||
"data_manifest.data_scope.partitions",
|
||||
);
|
||||
requireExactKeys(
|
||||
manifest.definitions,
|
||||
[
|
||||
"episode_gap_days",
|
||||
"locked_test_metrics_published",
|
||||
"support_rounding",
|
||||
"suppression_min_nonpass",
|
||||
"suppression_min_pass",
|
||||
"suppression_min_support",
|
||||
"suppression_min_distinct_vehicles",
|
||||
"suppression_min_distinct_pass_vehicles",
|
||||
"suppression_min_distinct_nonpass_vehicles",
|
||||
"target",
|
||||
],
|
||||
"data_manifest.definitions",
|
||||
);
|
||||
requireInteger(manifest.definitions.episode_gap_days, "data_manifest.definitions.episode_gap_days", {
|
||||
min: 1,
|
||||
});
|
||||
requireBoolean(
|
||||
manifest.definitions.locked_test_metrics_published,
|
||||
"data_manifest.definitions.locked_test_metrics_published",
|
||||
);
|
||||
if (manifest.definitions.locked_test_metrics_published !== false) {
|
||||
throw new DataContractError("Locked-test metrics are not approved for this public preview.");
|
||||
}
|
||||
for (const key of [
|
||||
"support_rounding",
|
||||
"suppression_min_nonpass",
|
||||
"suppression_min_pass",
|
||||
"suppression_min_support",
|
||||
"suppression_min_distinct_vehicles",
|
||||
"suppression_min_distinct_pass_vehicles",
|
||||
"suppression_min_distinct_nonpass_vehicles",
|
||||
]) {
|
||||
requireInteger(manifest.definitions[key], `data_manifest.definitions.${key}`, { min: 1 });
|
||||
}
|
||||
requireString(manifest.definitions.target, "data_manifest.definitions.target");
|
||||
if (!Array.isArray(manifest.assets)) {
|
||||
throw new DataContractError("data_manifest.assets must be an array.");
|
||||
}
|
||||
validateStringArray(manifest.assets, "data_manifest.assets");
|
||||
const expectedManifestAssets = Object.entries(REQUIRED_ASSETS)
|
||||
.filter(([name]) => !["manifest", "shaManifest"].includes(name))
|
||||
.map(([, filename]) => filename)
|
||||
.sort();
|
||||
const declaredAssets = [...manifest.assets].sort();
|
||||
if (JSON.stringify(declaredAssets) !== JSON.stringify(expectedManifestAssets)) {
|
||||
throw new DataContractError("data_manifest.assets does not match the approved asset set.");
|
||||
}
|
||||
|
||||
const validated = { manifest };
|
||||
for (const [name, validator] of Object.entries(ASSET_VALIDATORS)) {
|
||||
if (!Object.hasOwn(rawAssets, name)) {
|
||||
throw new DataContractError(`A required approved aggregate is missing: ${name}.`);
|
||||
}
|
||||
validated[name] = validator(rawAssets[name]);
|
||||
if (
|
||||
validated[name].development_preview !== manifest.development_preview ||
|
||||
validated[name].population_estimate_allowed !== manifest.population_estimate_allowed
|
||||
) {
|
||||
throw new DataContractError(`${name} publication flags disagree with data_manifest.`);
|
||||
}
|
||||
}
|
||||
requireExactStringSet(
|
||||
validated.filters.models,
|
||||
manifest.data_scope.model_names,
|
||||
"filter_catalog.models",
|
||||
);
|
||||
for (const [index, row] of validated.diagnostics.rows.entries()) {
|
||||
if (!validated.filters.models.includes(row.model)) {
|
||||
throw new DataContractError(`model_diagnostics.rows[${index}].model is not cataloged.`);
|
||||
}
|
||||
}
|
||||
const minimumSupport = manifest.definitions.suppression_min_support;
|
||||
const supportRounding = manifest.definitions.support_rounding;
|
||||
for (const name of ["overview", "ageRisk", "scorecards", "coverage"]) {
|
||||
for (const [index, row] of validated[name].rows.entries()) {
|
||||
if (
|
||||
row.support_rounded < minimumSupport ||
|
||||
row.support_rounded % supportRounding !== 0
|
||||
) {
|
||||
throw new DataContractError(
|
||||
`${name}.rows[${index}].support_rounded violates the publication thresholds.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Object.freeze(validated);
|
||||
}
|
||||
|
||||
async function fetchBytes(path, fetchImplementation) {
|
||||
let response;
|
||||
try {
|
||||
response = await fetchImplementation(path, {
|
||||
cache: "no-store",
|
||||
credentials: "same-origin",
|
||||
});
|
||||
} catch {
|
||||
throw new DataContractError("Approved dashboard assets could not be reached.");
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new DataContractError("One or more approved dashboard assets are unavailable.");
|
||||
}
|
||||
try {
|
||||
return await response.arrayBuffer();
|
||||
} catch {
|
||||
throw new DataContractError("An approved dashboard asset could not be read safely.");
|
||||
}
|
||||
}
|
||||
|
||||
function parseJsonBytes(bytes, label) {
|
||||
if (typeof TextDecoder !== "function") {
|
||||
throw new DataContractError("This browser cannot decode dashboard assets safely.");
|
||||
}
|
||||
try {
|
||||
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
||||
} catch {
|
||||
throw new DataContractError(`${label} is not valid UTF-8 JSON.`);
|
||||
}
|
||||
}
|
||||
|
||||
async function sha256Hex(bytes, cryptoImplementation) {
|
||||
if (!cryptoImplementation?.subtle || typeof cryptoImplementation.subtle.digest !== "function") {
|
||||
throw new DataContractError("This browser cannot verify dashboard asset integrity.");
|
||||
}
|
||||
let digest;
|
||||
try {
|
||||
digest = await cryptoImplementation.subtle.digest("SHA-256", bytes);
|
||||
} catch {
|
||||
throw new DataContractError("Dashboard asset integrity verification failed.");
|
||||
}
|
||||
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
export async function loadDashboardData({
|
||||
basePath = "./public/data/",
|
||||
fetchImplementation = globalThis.fetch,
|
||||
cryptoImplementation = globalThis.crypto,
|
||||
} = {}) {
|
||||
if (typeof fetchImplementation !== "function") {
|
||||
throw new DataContractError("This browser cannot load dashboard assets safely.");
|
||||
}
|
||||
const baseUrl = new URL(basePath, globalThis.location?.href ?? "http://local/");
|
||||
const shaBytes = await fetchBytes(
|
||||
new URL(REQUIRED_ASSETS.shaManifest, baseUrl),
|
||||
fetchImplementation,
|
||||
);
|
||||
const shaManifest = validateShaManifest(
|
||||
parseJsonBytes(shaBytes, "The checksum manifest"),
|
||||
);
|
||||
const expectedDigests = new Map(
|
||||
shaManifest.files.map((entry) => [entry.name, entry.sha256]),
|
||||
);
|
||||
const assetEntries = Object.entries(REQUIRED_ASSETS).filter(
|
||||
([name]) => name !== "shaManifest",
|
||||
);
|
||||
const rawEntries = await Promise.all(
|
||||
assetEntries.map(async ([name, filename]) => [
|
||||
name,
|
||||
filename,
|
||||
await fetchBytes(new URL(filename, baseUrl), fetchImplementation),
|
||||
]),
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
rawEntries.map(async ([, filename, bytes]) => {
|
||||
const observed = await sha256Hex(bytes, cryptoImplementation);
|
||||
if (observed !== expectedDigests.get(filename)) {
|
||||
throw new DataContractError(`Integrity verification failed for ${filename}.`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const parsedEntries = rawEntries.map(([name, filename, bytes]) => [
|
||||
name,
|
||||
parseJsonBytes(bytes, filename),
|
||||
]);
|
||||
parsedEntries.push(["shaManifest", shaManifest]);
|
||||
return validateAssetSet(Object.fromEntries(parsedEntries));
|
||||
}
|
||||
|
||||
export { SCHEMA_VERSION };
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "utah-vehicle-health-dashboard",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Dependency-free static dashboard for approved Utah Vehicle Health aggregates.",
|
||||
"scripts": {
|
||||
"start": "node server.mjs",
|
||||
"test": "node --test tests/contract.test.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"development_preview":true,"population_estimate_allowed":false,"rows":[{"age_band":"0-3","nonpass_rate":0.058,"support_rounded":500},{"age_band":"4-7","nonpass_rate":0.06,"support_rounded":5900},{"age_band":"8-11","nonpass_rate":0.077,"support_rounded":9800},{"age_band":"12-15","nonpass_rate":0.109,"support_rounded":9700},{"age_band":"16-20","nonpass_rate":0.157,"support_rounded":8600},{"age_band":"21+","nonpass_rate":0.221,"support_rounded":4700}],"schema_version":"dashboard_data_v1"}
|
||||
@@ -0,0 +1 @@
|
||||
{"development_preview":true,"population_estimate_allowed":false,"rows":[{"nonpass_rate":0.116,"prior_make":"FORD","prior_model":"F150","support_rounded":1100},{"nonpass_rate":0.09,"prior_make":"HONDA","prior_model":"ACCORD","support_rounded":400},{"nonpass_rate":0.073,"prior_make":"HONDA","prior_model":"CIVIC","support_rounded":300},{"nonpass_rate":0.086,"prior_make":"TOYOTA","prior_model":"CAMRY","support_rounded":500}],"schema_version":"dashboard_data_v1"}
|
||||
@@ -0,0 +1 @@
|
||||
{"development_preview":true,"population_estimate_allowed":false,"rows":[{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"slc","support_rounded":3100,"utah_obd_proxy_share":0.0,"year":2016},{"labeled_rate":0.883,"overall_result_share":0.002,"source_era":"utah","support_rounded":600,"utah_obd_proxy_share":0.998,"year":2016},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"weber","support_rounded":700,"utah_obd_proxy_share":0.0,"year":2016},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"slc","support_rounded":3200,"utah_obd_proxy_share":0.0,"year":2017},{"labeled_rate":0.899,"overall_result_share":0.0,"source_era":"utah","support_rounded":900,"utah_obd_proxy_share":1.0,"year":2017},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"weber","support_rounded":700,"utah_obd_proxy_share":0.0,"year":2017},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"slc","support_rounded":3200,"utah_obd_proxy_share":0.0,"year":2018},{"labeled_rate":0.921,"overall_result_share":0.001,"source_era":"utah","support_rounded":1000,"utah_obd_proxy_share":0.999,"year":2018},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"weber","support_rounded":800,"utah_obd_proxy_share":0.0,"year":2018},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"slc","support_rounded":3300,"utah_obd_proxy_share":0.0,"year":2019},{"labeled_rate":0.93,"overall_result_share":0.001,"source_era":"utah","support_rounded":800,"utah_obd_proxy_share":0.999,"year":2019},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"weber","support_rounded":800,"utah_obd_proxy_share":0.0,"year":2019},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"slc","support_rounded":3200,"utah_obd_proxy_share":0.0,"year":2020},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"weber","support_rounded":800,"utah_obd_proxy_share":0.0,"year":2020},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"slc","support_rounded":2200,"utah_obd_proxy_share":0.0,"year":2021},{"labeled_rate":0.931,"overall_result_share":0.0,"source_era":"utah","support_rounded":1100,"utah_obd_proxy_share":1.0,"year":2021},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"weber","support_rounded":800,"utah_obd_proxy_share":0.0,"year":2021},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"slc","support_rounded":3000,"utah_obd_proxy_share":0.0,"year":2022},{"labeled_rate":0.927,"overall_result_share":0.001,"source_era":"utah","support_rounded":1100,"utah_obd_proxy_share":0.999,"year":2022},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"weber","support_rounded":800,"utah_obd_proxy_share":0.0,"year":2022},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"slc","support_rounded":3000,"utah_obd_proxy_share":0.0,"year":2023},{"labeled_rate":0.93,"overall_result_share":0.0,"source_era":"utah","support_rounded":1200,"utah_obd_proxy_share":1.0,"year":2023},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"weber","support_rounded":800,"utah_obd_proxy_share":0.0,"year":2023},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"slc","support_rounded":500,"utah_obd_proxy_share":0.0,"year":2024},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"slco","support_rounded":300,"utah_obd_proxy_share":0.0,"year":2024},{"labeled_rate":0.929,"overall_result_share":0.0,"source_era":"utah","support_rounded":1100,"utah_obd_proxy_share":1.0,"year":2024},{"labeled_rate":1.0,"overall_result_share":1.0,"source_era":"weber","support_rounded":800,"utah_obd_proxy_share":0.0,"year":2024}],"schema_version":"dashboard_data_v1"}
|
||||
@@ -0,0 +1 @@
|
||||
{"assets":["age_risk_curve.json","cohort_scorecard.json","coverage_quality.json","filter_catalog.json","model_diagnostics.json","overview_period_county.json"],"data_scope":{"first_year":2016,"last_year":2024,"model_names":["hist_gradient_boosting_platt","hist_gradient_boosting_raw","logistic_platt","logistic_raw","previous_episode_literal","training_prevalence"],"partitions":["train","tune","calibrate"]},"definitions":{"episode_gap_days":30,"locked_test_metrics_published":false,"support_rounding":100,"suppression_min_distinct_nonpass_vehicles":10,"suppression_min_distinct_pass_vehicles":10,"suppression_min_distinct_vehicles":100,"suppression_min_nonpass":10,"suppression_min_pass":10,"suppression_min_support":100,"target":"first-attempt next-episode binary non-pass rate"},"development_preview":true,"model_versions":["baseline_v1","hist_gradient_boosting_v1"],"population_estimate_allowed":false,"release_id":"7c1af8d22d3841b84d2cf4cd5ba7bce6a43578c78bc3d32a25d200b4b1986704","schema_version":"dashboard_data_v1"}
|
||||
@@ -0,0 +1 @@
|
||||
{"age_bands":["0-3","4-7","8-11","12-15","16-20","21+"],"development_preview":true,"models":["hist_gradient_boosting_platt","hist_gradient_boosting_raw","logistic_platt","logistic_raw","previous_episode_literal","training_prevalence"],"partitions":["train","tune","calibrate"],"periods":[{"quarters":[1,2,3,4],"year":2016},{"quarters":[1,2,3,4],"year":2017},{"quarters":[1,2,3,4],"year":2018},{"quarters":[1,2,3,4],"year":2019},{"quarters":[1,2,3,4],"year":2020},{"quarters":[1,2,3,4],"year":2021},{"quarters":[1,2,3,4],"year":2022},{"quarters":[1,2,3,4],"year":2023},{"quarters":[1,2,3,4],"year":2024}],"population_estimate_allowed":false,"prior_make_models":[{"prior_make":"FORD","prior_model":"F150"},{"prior_make":"HONDA","prior_model":"ACCORD"},{"prior_make":"HONDA","prior_model":"CIVIC"},{"prior_make":"TOYOTA","prior_model":"CAMRY"}],"public_counties":["salt_lake","utah","weber"],"schema_version":"dashboard_data_v1"}
|
||||
@@ -0,0 +1 @@
|
||||
{"development_preview":true,"population_estimate_allowed":false,"rows":[{"average_precision":0.275,"brier":0.0928,"log_loss":0.3218,"model":"hist_gradient_boosting_platt","partition":"calibrate","roc_auc":0.7072,"top_10_capture":0.2744},{"average_precision":0.3966,"brier":0.0877,"log_loss":0.3011,"model":"hist_gradient_boosting_raw","partition":"train","roc_auc":0.7871,"top_10_capture":0.3658},{"average_precision":0.2743,"brier":0.1011,"log_loss":0.3431,"model":"hist_gradient_boosting_raw","partition":"tune","roc_auc":0.7147,"top_10_capture":0.2635},{"average_precision":0.275,"brier":0.0928,"log_loss":0.3218,"model":"hist_gradient_boosting_raw","partition":"calibrate","roc_auc":0.7072,"top_10_capture":0.2744},{"average_precision":0.2573,"brier":0.0936,"log_loss":0.3261,"model":"logistic_platt","partition":"calibrate","roc_auc":0.6905,"top_10_capture":0.2729},{"average_precision":0.2482,"brier":0.0963,"log_loss":0.3327,"model":"logistic_raw","partition":"train","roc_auc":0.6955,"top_10_capture":0.2593},{"average_precision":0.2821,"brier":0.1012,"log_loss":0.3448,"model":"logistic_raw","partition":"tune","roc_auc":0.7072,"top_10_capture":0.2527},{"average_precision":0.2573,"brier":0.0936,"log_loss":0.3261,"model":"logistic_raw","partition":"calibrate","roc_auc":0.6905,"top_10_capture":0.2729},{"average_precision":0.146,"brier":0.1695,"log_loss":5.8429,"model":"previous_episode_literal","partition":"train","roc_auc":0.5687,"top_10_capture":0.2128},{"average_precision":0.1658,"brier":0.1723,"log_loss":5.9436,"model":"previous_episode_literal","partition":"tune","roc_auc":0.5821,"top_10_capture":0.2291},{"average_precision":0.1476,"brier":0.1677,"log_loss":5.7909,"model":"previous_episode_literal","partition":"calibrate","roc_auc":0.5767,"top_10_capture":0.224},{"average_precision":0.116,"brier":0.1025,"log_loss":0.3588,"model":"training_prevalence","partition":"train","roc_auc":0.5,"top_10_capture":0.1},{"average_precision":0.1246,"brier":0.1092,"log_loss":0.3764,"model":"training_prevalence","partition":"tune","roc_auc":0.5,"top_10_capture":0.1},{"average_precision":0.1129,"brier":0.1002,"log_loss":0.3526,"model":"training_prevalence","partition":"calibrate","roc_auc":0.5,"top_10_capture":0.1}],"schema_version":"dashboard_data_v1"}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"development_preview":true,"files":[{"name":"age_risk_curve.json","sha256":"5445d52365ad3494ba05f097e7f4f5e41fffeecfe50fbb2952d72044f496c925"},{"name":"cohort_scorecard.json","sha256":"2ae83d9d55aa1f2ccae6420c66350fb491405eedfa90374aa3df2782a4aa0bf1"},{"name":"coverage_quality.json","sha256":"35b7dbed50e1b8260aac18c269f5045010ea23649a649f984a4693f3f00a1e83"},{"name":"data_manifest.json","sha256":"b7196b04eb223683c928cfb5375ed618e1230fa03382bd8a889de24c589557a0"},{"name":"filter_catalog.json","sha256":"9e45d428cd38853002ebd0bda089eb46d0832cd1b7b6b05ac953bc8472b01eb5"},{"name":"model_diagnostics.json","sha256":"ebf99eb32436a5eabb7d754cd88109f2cc0981575dd28966757031ac7faafa57"},{"name":"overview_period_county.json","sha256":"fe2a4233563b47fa31fc87859b100983d3bfb574c5a7fbc7241b9cfb4661b038"}],"population_estimate_allowed":false,"schema_version":"dashboard_data_v1"}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { createReadStream, realpathSync, statSync } from "node:fs";
|
||||
import { createServer } from "node:http";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const ROOT = realpathSync(path.dirname(fileURLToPath(import.meta.url)));
|
||||
const HOST = process.env.HOST || "127.0.0.1";
|
||||
const PORT = Number.parseInt(process.env.PORT || "4173", 10);
|
||||
const TYPES = new Map([
|
||||
[".css", "text/css; charset=utf-8"],
|
||||
[".html", "text/html; charset=utf-8"],
|
||||
[".js", "text/javascript; charset=utf-8"],
|
||||
[".json", "application/json; charset=utf-8"],
|
||||
[".mjs", "text/javascript; charset=utf-8"],
|
||||
[".svg", "image/svg+xml"],
|
||||
]);
|
||||
const PUBLIC_PATHS = new Set([
|
||||
"/index.html",
|
||||
"/styles.css",
|
||||
"/js/app.js",
|
||||
"/js/charts.js",
|
||||
"/js/data.js",
|
||||
"/public/data/age_risk_curve.json",
|
||||
"/public/data/cohort_scorecard.json",
|
||||
"/public/data/coverage_quality.json",
|
||||
"/public/data/data_manifest.json",
|
||||
"/public/data/filter_catalog.json",
|
||||
"/public/data/model_diagnostics.json",
|
||||
"/public/data/overview_period_county.json",
|
||||
"/public/data/sha256_manifest.json",
|
||||
]);
|
||||
|
||||
function safeFile(requestUrl) {
|
||||
let pathname;
|
||||
try {
|
||||
pathname = decodeURIComponent(new URL(requestUrl, "http://local").pathname);
|
||||
} catch {
|
||||
return { file: null, status: 400 };
|
||||
}
|
||||
if (pathname === "/") pathname = "/index.html";
|
||||
if (!PUBLIC_PATHS.has(pathname)) return { file: null, status: 404 };
|
||||
try {
|
||||
const candidate = realpathSync(path.join(ROOT, pathname.slice(1)));
|
||||
if (!candidate.startsWith(`${ROOT}${path.sep}`) || !statSync(candidate).isFile()) {
|
||||
return { file: null, status: 404 };
|
||||
}
|
||||
return { file: candidate, status: 200 };
|
||||
} catch {
|
||||
return { file: null, status: 404 };
|
||||
}
|
||||
}
|
||||
|
||||
const server = createServer((request, response) => {
|
||||
if (!request.url || !["GET", "HEAD"].includes(request.method || "")) {
|
||||
response.writeHead(405, { Allow: "GET, HEAD" });
|
||||
response.end("Method not allowed");
|
||||
return;
|
||||
}
|
||||
const resolved = safeFile(request.url);
|
||||
if (!resolved.file) {
|
||||
response.writeHead(resolved.status, { "Content-Type": "text/plain; charset=utf-8" });
|
||||
response.end(resolved.status === 400 ? "Bad request" : "Not found");
|
||||
return;
|
||||
}
|
||||
const file = resolved.file;
|
||||
const extension = path.extname(file).toLowerCase();
|
||||
response.writeHead(200, {
|
||||
"Content-Type": TYPES.get(extension) || "application/octet-stream",
|
||||
"Cache-Control": extension === ".json" ? "no-store" : "public, max-age=300",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
"Referrer-Policy": "no-referrer",
|
||||
});
|
||||
if (request.method === "HEAD") {
|
||||
response.end();
|
||||
return;
|
||||
}
|
||||
createReadStream(file).on("error", () => response.destroy()).pipe(response);
|
||||
});
|
||||
|
||||
server.listen(PORT, HOST, () => {
|
||||
process.stdout.write(`Utah Vehicle Health dashboard: http://${HOST}:${PORT}\n`);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,196 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash, webcrypto } from "node:crypto";
|
||||
import { readFileSync, readdirSync, statSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import {
|
||||
DataContractError,
|
||||
REQUIRED_ASSETS,
|
||||
SCHEMA_VERSION,
|
||||
loadDashboardData,
|
||||
validateAssetSet,
|
||||
} from "../js/data.js";
|
||||
|
||||
const DASHBOARD_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const PUBLIC_DATA = path.join(DASHBOARD_ROOT, "public", "data");
|
||||
|
||||
function json(filename) {
|
||||
return JSON.parse(readFileSync(path.join(PUBLIC_DATA, filename), "utf8"));
|
||||
}
|
||||
|
||||
function assetSet() {
|
||||
return Object.fromEntries(
|
||||
Object.entries(REQUIRED_ASSETS).map(([name, filename]) => [name, json(filename)]),
|
||||
);
|
||||
}
|
||||
|
||||
function publicDataFetch(overrides = new Map()) {
|
||||
return async (requestUrl) => {
|
||||
const filename = path.basename(new URL(String(requestUrl)).pathname);
|
||||
const source = path.join(PUBLIC_DATA, filename);
|
||||
let bytes;
|
||||
try {
|
||||
bytes = overrides.has(filename) ? overrides.get(filename) : readFileSync(source);
|
||||
} catch {
|
||||
return { ok: false, async arrayBuffer() { return new ArrayBuffer(0); } };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
async arrayBuffer() {
|
||||
return Uint8Array.from(bytes).buffer;
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function filesRecursively(directory) {
|
||||
return readdirSync(directory).flatMap((name) => {
|
||||
const target = path.join(directory, name);
|
||||
return statSync(target).isDirectory() ? filesRecursively(target) : [target];
|
||||
});
|
||||
}
|
||||
|
||||
test("static shell exposes four semantic navigable views", () => {
|
||||
const html = readFileSync(path.join(DASHBOARD_ROOT, "index.html"), "utf8");
|
||||
assert.match(html, /<header\b/);
|
||||
assert.match(html, /<nav\b[^>]*aria-label="Dashboard views"/);
|
||||
assert.match(html, /<main\b/);
|
||||
assert.match(html, /<footer\b/);
|
||||
for (const route of ["overview", "reliability", "estimator", "methods"]) {
|
||||
assert.match(html, new RegExp(`data-route="${route}"`));
|
||||
assert.match(html, new RegExp(`data-view="${route}"`));
|
||||
}
|
||||
assert.match(html, /2025 development-sample holdout was evaluated once/i);
|
||||
assert.match(html, /<strong>One-time holdout<\/strong><small>Already evaluated<\/small>/);
|
||||
assert.doesNotMatch(html, /<strong>Locked test<\/strong>|<small>Final evaluation<\/small>/);
|
||||
});
|
||||
|
||||
test("estimator remains disabled and contains no identifying input", () => {
|
||||
const html = readFileSync(path.join(DASHBOARD_ROOT, "index.html"), "utf8");
|
||||
const estimator = html.match(/<section class="view" id="view-estimator"[\s\S]*?<\/section>/)?.[0];
|
||||
assert.ok(estimator, "estimator section is present");
|
||||
assert.match(estimator, /<fieldset disabled>/);
|
||||
const controlAttributes = [...estimator.matchAll(/<(?:input|select|textarea)\b([^>]*)>/g)].map(
|
||||
(match) => match[1],
|
||||
);
|
||||
for (const attributes of controlAttributes) {
|
||||
assert.doesNotMatch(
|
||||
attributes,
|
||||
/(?:name|id)\s*=\s*["'][^"']*(?:vin|plate|address|station|free.?text|zip)[^"']*["']/i,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("all required generated assets satisfy the browser contract", () => {
|
||||
const validated = validateAssetSet(assetSet());
|
||||
assert.equal(validated.manifest.schema_version, SCHEMA_VERSION);
|
||||
assert.equal(validated.manifest.development_preview, true);
|
||||
assert.equal(validated.manifest.population_estimate_allowed, false);
|
||||
assert.equal(validated.manifest.definitions.locked_test_metrics_published, false);
|
||||
assert.match(validated.manifest.release_id, /^[0-9a-f]{64}$/);
|
||||
assert.ok(validated.manifest.model_versions.length > 0);
|
||||
assert.deepEqual(
|
||||
validated.filters.partitions,
|
||||
["train", "tune", "calibrate"],
|
||||
);
|
||||
assert.ok(validated.overview.rows.length > 0);
|
||||
});
|
||||
|
||||
test("browser loader verifies every raw asset digest before rendering", async () => {
|
||||
const validated = await loadDashboardData({
|
||||
basePath: "http://dashboard.test/public/data/",
|
||||
fetchImplementation: publicDataFetch(),
|
||||
cryptoImplementation: webcrypto,
|
||||
});
|
||||
assert.equal(validated.manifest.schema_version, SCHEMA_VERSION);
|
||||
|
||||
const changedOverview = Buffer.concat([
|
||||
readFileSync(path.join(PUBLIC_DATA, REQUIRED_ASSETS.overview)),
|
||||
Buffer.from("\n"),
|
||||
]);
|
||||
await assert.rejects(
|
||||
loadDashboardData({
|
||||
basePath: "http://dashboard.test/public/data/",
|
||||
fetchImplementation: publicDataFetch(
|
||||
new Map([[REQUIRED_ASSETS.overview, changedOverview]]),
|
||||
),
|
||||
cryptoImplementation: webcrypto,
|
||||
}),
|
||||
(error) =>
|
||||
error instanceof DataContractError &&
|
||||
/Integrity verification failed for overview_period_county\.json/.test(error.message),
|
||||
);
|
||||
});
|
||||
|
||||
test("sha256 manifest covers and matches every approved data asset", () => {
|
||||
const manifest = json(REQUIRED_ASSETS.shaManifest);
|
||||
const expectedNames = Object.entries(REQUIRED_ASSETS)
|
||||
.filter(([name]) => name !== "shaManifest")
|
||||
.map(([, filename]) => filename)
|
||||
.sort();
|
||||
assert.deepEqual(
|
||||
manifest.files.map((file) => file.name).sort(),
|
||||
expectedNames,
|
||||
);
|
||||
for (const entry of manifest.files) {
|
||||
const digest = createHash("sha256")
|
||||
.update(readFileSync(path.join(PUBLIC_DATA, entry.name)))
|
||||
.digest("hex");
|
||||
assert.equal(digest, entry.sha256, `${entry.name} digest`);
|
||||
}
|
||||
});
|
||||
|
||||
test("contract fails closed for missing, inconsistent, or sensitive data", () => {
|
||||
const missing = assetSet();
|
||||
delete missing.overview;
|
||||
assert.throws(() => validateAssetSet(missing), DataContractError);
|
||||
|
||||
const inconsistent = assetSet();
|
||||
inconsistent.ageRisk.population_estimate_allowed = true;
|
||||
assert.throws(() => validateAssetSet(inconsistent), DataContractError);
|
||||
|
||||
const sensitive = assetSet();
|
||||
sensitive.scorecards.rows[0].vehicle_token = "not-public";
|
||||
assert.throws(() => validateAssetSet(sensitive), DataContractError);
|
||||
|
||||
const extraRowField = assetSet();
|
||||
extraRowField.overview.rows[0].note = "unapproved";
|
||||
assert.throws(() => validateAssetSet(extraRowField), DataContractError);
|
||||
|
||||
const lockedMetrics = assetSet();
|
||||
lockedMetrics.manifest.definitions.locked_test_metrics_published = true;
|
||||
assert.throws(() => validateAssetSet(lockedMetrics), DataContractError);
|
||||
|
||||
const extraPartition = assetSet();
|
||||
extraPartition.filters.partitions.push("locked_test");
|
||||
assert.throws(() => validateAssetSet(extraPartition), DataContractError);
|
||||
|
||||
const unsortedVersions = assetSet();
|
||||
unsortedVersions.manifest.model_versions = ["z_v1", "a_v1"];
|
||||
assert.throws(() => validateAssetSet(unsortedVersions), DataContractError);
|
||||
|
||||
const belowSuppression = assetSet();
|
||||
belowSuppression.overview.rows[0].support_rounded =
|
||||
belowSuppression.manifest.definitions.suppression_min_support - 1;
|
||||
assert.throws(() => validateAssetSet(belowSuppression), DataContractError);
|
||||
|
||||
const incorrectlyRounded = assetSet();
|
||||
incorrectlyRounded.coverage.rows[0].support_rounded += 1;
|
||||
assert.throws(() => validateAssetSet(incorrectlyRounded), DataContractError);
|
||||
});
|
||||
|
||||
test("dashboard source has no external runtime dependency or credential marker", () => {
|
||||
const sourceFiles = filesRecursively(DASHBOARD_ROOT).filter(
|
||||
(filename) =>
|
||||
!filename.includes(`${path.sep}public${path.sep}data${path.sep}`) &&
|
||||
!filename.includes(`${path.sep}tests${path.sep}`) &&
|
||||
/\.(?:html|css|js|mjs)$/.test(filename),
|
||||
);
|
||||
for (const filename of sourceFiles) {
|
||||
const source = readFileSync(filename, "utf8");
|
||||
assert.doesNotMatch(source, /(?:postgres(?:ql)?:\/\/|PGPASSWORD|PGHOST|countydata\.)/i);
|
||||
assert.doesNotMatch(source, /<script[^>]+src=["']https?:|@import\s+url\(["']?https?:/i);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user