repository is now a presentation-ready development prototype
This commit is contained in:
+44
-22
@@ -1,10 +1,18 @@
|
||||
# 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.
|
||||
This directory is a dependency-free static development prototype. It uses
|
||||
semantic HTML, CSS, vanilla ES modules, and inline SVG generated from approved,
|
||||
suppressed aggregate JSON. It has no database client, server credential,
|
||||
analytics SDK, or external CDN.
|
||||
|
||||
## Run locally from the repository root
|
||||
Every displayed result comes from the private 10,000-vehicle development
|
||||
sample. Results are sample-based, are not population estimates, and must not be
|
||||
used for individual decisions. The final prototype model is calibrated logistic
|
||||
regression; the boosted tree is a benchmark only.
|
||||
|
||||
## Run locally
|
||||
|
||||
From the Utah Vehicle Health source-repository root:
|
||||
|
||||
```bash
|
||||
node dashboard/server.mjs
|
||||
@@ -23,6 +31,14 @@ Run the dependency-free contract checks with:
|
||||
node --test dashboard/tests/contract.test.mjs
|
||||
```
|
||||
|
||||
After the contents of `dashboard/` have been copied into a dashboard-only
|
||||
repository or project root, use the self-contained equivalents:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
npm test
|
||||
```
|
||||
|
||||
## Public-data boundary
|
||||
|
||||
The browser requires and validates these files beneath `public/data/`:
|
||||
@@ -41,27 +57,33 @@ 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`.
|
||||
The site intentionally contains no vehicle lookup, individual estimator,
|
||||
operational connection, or row-level prediction output. VINs, plates, ZIPs,
|
||||
stations, technician identifiers, raw source JSON, credentials, and operational
|
||||
records must never enter this directory.
|
||||
|
||||
## Import into Bolt
|
||||
## Publish through 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`.
|
||||
Use a separate static Bolt project backed by a dashboard-only repository. Do
|
||||
not import this source repository into Bolt.
|
||||
|
||||
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.
|
||||
1. Run `npm test` from this directory and confirm every contract check passes.
|
||||
2. Create a clean dashboard-only repository or Bolt project.
|
||||
3. Copy only the *contents* of `dashboard/` into that project, so `index.html`
|
||||
is at the project root.
|
||||
4. Confirm that private `data/`, `artifacts/`, `models/`, `.env` files, SQL,
|
||||
notebooks, and database tooling are absent.
|
||||
5. Preview the project. There is no install or build step; if Bolt requests a
|
||||
preview command, use `node server.mjs` with `HOST=0.0.0.0` and let Bolt
|
||||
provide `PORT`.
|
||||
6. Verify all four views, the development-sample messaging on every view, and
|
||||
that the in-app status reads **Validated sample aggregates**.
|
||||
7. Use **Publish** only after the aggregate bundle completes privacy review.
|
||||
Bolt hosting is the default; Netlify can instead be selected before the
|
||||
first publish if desired.
|
||||
|
||||
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.
|
||||
See Bolt's official documentation for [Git
|
||||
integration](https://support.bolt.new/integrations/git) and [publishing with
|
||||
Netlify](https://support.bolt.new/integrations/netlify).
|
||||
|
||||
No publishing action is performed by this repository.
|
||||
|
||||
+108
-102
@@ -3,8 +3,9 @@
|
||||
<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="description" content="Sample-based research on the first-attempt non-pass outcome of a returning vehicle's next emissions inspection.">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'">
|
||||
<title>Overview · Utah Vehicle Health</title>
|
||||
<link rel="stylesheet" href="./styles.css">
|
||||
<script type="module" src="./js/app.js"></script>
|
||||
@@ -16,7 +17,7 @@
|
||||
<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>
|
||||
<span id="preview-copy">Private 10,000-vehicle development sample · suppressed aggregates only · not population estimates.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +30,7 @@
|
||||
</svg>
|
||||
<span>
|
||||
<span class="brand__name">Utah Vehicle Health</span>
|
||||
<span class="brand__tagline">Next-episode inspection insights</span>
|
||||
<span class="brand__tagline">Returning vehicles · next first attempt</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -42,8 +43,8 @@
|
||||
<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="#cohorts" data-route="cohorts">Sample cohorts</a>
|
||||
<a href="#model" data-route="model">Model & benchmark</a>
|
||||
<a href="#methods" data-route="methods">Data & methods</a>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -54,9 +55,9 @@
|
||||
<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>
|
||||
<p class="eyebrow">Private 10,000-vehicle development sample</p>
|
||||
<h1 id="overview-heading">The next inspection, in sample context</h1>
|
||||
<p class="lede">The research question is intentionally narrow: predict whether a returning vehicle's next inspection episode has a first-attempt non-pass. Non-pass combines fail, reject, and abort.</p>
|
||||
</div>
|
||||
<div class="data-stamp" aria-label="Dataset status">
|
||||
<span>Data through</span>
|
||||
@@ -65,6 +66,11 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<aside class="scope-strip" aria-label="Result scope">
|
||||
<strong>Sample-based only</strong>
|
||||
<span>Every value on this page describes the private development sample and covered feeds—not Utah's vehicle population.</span>
|
||||
</aside>
|
||||
|
||||
<div class="unavailable-state" data-unavailable hidden role="status">
|
||||
<div class="unavailable-state__icon" aria-hidden="true">!</div>
|
||||
<div>
|
||||
@@ -75,22 +81,22 @@
|
||||
|
||||
<section class="kpi-grid" aria-label="Overview statistics" data-requires-data>
|
||||
<article class="kpi-card">
|
||||
<span class="kpi-card__label">Published support</span>
|
||||
<span class="kpi-card__label">Published episode support</span>
|
||||
<strong id="kpi-eligible">—</strong>
|
||||
<small>Rounded total; suppressed cells omitted</small>
|
||||
<small>Rounded total; sampled vehicles may contribute multiple eligible episodes</small>
|
||||
</article>
|
||||
<article class="kpi-card kpi-card--pass">
|
||||
<span class="kpi-card__label">Pass rate</span>
|
||||
<span class="kpi-card__label">Observed sample 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>
|
||||
<span class="kpi-card__label">Observed sample 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>
|
||||
<span class="kpi-card__label">Covered county feeds</span>
|
||||
<strong id="kpi-counties">—</strong>
|
||||
<small>Not statewide coverage</small>
|
||||
</article>
|
||||
@@ -100,8 +106,8 @@
|
||||
<article class="panel panel--wide">
|
||||
<header class="panel__header">
|
||||
<div>
|
||||
<p class="eyebrow">Quarterly outcomes</p>
|
||||
<h2>First-attempt non-pass trend</h2>
|
||||
<p class="eyebrow">Sample association over time</p>
|
||||
<h2>Observed 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>
|
||||
@@ -120,36 +126,41 @@
|
||||
</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>
|
||||
<p class="chart-note">Availability only—this is not a county outcome ranking. Teal feeds are represented in the sample; 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>
|
||||
<p class="eyebrow">Sample association by age</p>
|
||||
<h2>Observed non-pass rate 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>
|
||||
<p class="chart-note">Sample aggregates only, without confidence intervals. The pattern is descriptive and not a population estimate or causal effect.</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>
|
||||
<p><strong>What this measures:</strong> sampled returning vehicles' next-episode first-attempt emissions-inspection outcomes in covered feeds. It is not a population estimate, mechanical diagnosis, safety assessment, or legal determination.</p>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view" id="view-reliability" data-view="reliability" aria-labelledby="reliability-heading" hidden>
|
||||
<section class="view" id="view-cohorts" data-view="cohorts" aria-labelledby="cohorts-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>
|
||||
<p class="eyebrow">Private 10,000-vehicle development sample</p>
|
||||
<h1 id="cohorts-heading">Sample cohort explorer</h1>
|
||||
<p class="lede">Explore supported prior make-and-model cohorts and their observed next-episode first-attempt non-pass rates. These are sample associations—not reliability grades or individual predictions.</p>
|
||||
</header>
|
||||
|
||||
<aside class="scope-strip" aria-label="Result scope">
|
||||
<strong>Not a ranking of population reliability</strong>
|
||||
<span>Rates use rounded, suppressed episode support from sampled vehicles. Cohorts can differ in age, history, period, and coverage.</span>
|
||||
</aside>
|
||||
|
||||
<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>
|
||||
@@ -158,7 +169,7 @@
|
||||
<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>
|
||||
<h2 id="filter-heading">Find a cohort</h2>
|
||||
<button class="text-button" id="reset-filters" type="button">Reset</button>
|
||||
</div>
|
||||
<form id="explorer-filters">
|
||||
@@ -166,31 +177,7 @@
|
||||
<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>
|
||||
<p class="filter-panel__note"><strong>Published grain:</strong> prior make and model only. County, age, fuel, program, and period slices are not available for these scorecards.</p>
|
||||
</form>
|
||||
</aside>
|
||||
|
||||
@@ -201,8 +188,8 @@
|
||||
<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="risk-desc">Highest observed sample rate</option>
|
||||
<option value="risk-asc">Lowest observed sample rate</option>
|
||||
<option value="name">Make and model</option>
|
||||
</select>
|
||||
</label>
|
||||
@@ -210,9 +197,10 @@
|
||||
|
||||
<article class="panel">
|
||||
<header class="panel__header">
|
||||
<div><p class="eyebrow">Published aggregate comparison</p><h2>Supported cohort scorecard</h2></div>
|
||||
<div><p class="eyebrow">Descriptive sample comparison</p><h2>Observed cohort associations</h2></div>
|
||||
</header>
|
||||
<div id="cohort-dot-plot" class="chart chart--scorecard" role="img" aria-label="Ranked published cohort non-pass rates"></div>
|
||||
<div id="cohort-dot-plot" class="chart chart--scorecard" role="img" aria-label="Observed development-sample cohort non-pass rates"></div>
|
||||
<p class="chart-note">Ordering is a viewing aid within this sample, not a reliability ranking or population comparison.</p>
|
||||
</article>
|
||||
|
||||
<div id="cohort-cards" class="cohort-cards"></div>
|
||||
@@ -225,51 +213,64 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view" id="view-estimator" data-view="estimator" aria-labelledby="estimator-heading" hidden>
|
||||
<section class="view" id="view-model" data-view="model" aria-labelledby="model-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>
|
||||
<p class="eyebrow">Private 10,000-vehicle development sample</p>
|
||||
<h1 id="model-heading">Model & benchmark</h1>
|
||||
<p class="lede">Calibrated logistic regression is the final development-prototype model for the next-episode first-attempt non-pass target. The boosted tree is retained only as a benchmark.</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>
|
||||
<aside class="scope-strip" aria-label="Result scope">
|
||||
<strong>Development evidence only</strong>
|
||||
<span>These 2024 values come from the partition used to fit Platt calibration. They are sample-based calibration checks—not independent final-performance estimates, population estimates, or locked-test results.</span>
|
||||
</aside>
|
||||
|
||||
<div class="unavailable-state" data-unavailable hidden role="status">
|
||||
<div class="unavailable-state__icon" aria-hidden="true">!</div>
|
||||
<div><h2>Model diagnostics unavailable</h2><p data-unavailable-message>The approved diagnostic summary could not be validated.</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="model-comparison" data-requires-data aria-label="Development model comparison">
|
||||
<article class="model-card model-card--final">
|
||||
<div class="model-card__topline"><span class="decision-badge decision-badge--final">Final prototype</span><span>2024 calibration-fit check</span></div>
|
||||
<h2>Calibrated logistic regression</h2>
|
||||
<p>The selected model for the research conclusion and any prototype-level discussion. Platt scaling calibrates its probabilities.</p>
|
||||
<dl class="metric-list">
|
||||
<div><dt>PR-AUC</dt><dd id="final-ap">—</dd></div>
|
||||
<div><dt>ROC-AUC</dt><dd id="final-roc">—</dd></div>
|
||||
<div><dt>Brier ↓</dt><dd id="final-brier">—</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<section class="method-preview" aria-labelledby="future-output-heading">
|
||||
<article class="model-card">
|
||||
<div class="model-card__topline"><span class="decision-badge">Benchmark only</span><span>2024 calibration-fit check</span></div>
|
||||
<h2>Calibrated boosted tree</h2>
|
||||
<p>Retained for development comparison only. It does not replace the logistic model or define dashboard outputs.</p>
|
||||
<dl class="metric-list">
|
||||
<div><dt>PR-AUC</dt><dd id="benchmark-ap">—</dd></div>
|
||||
<div><dt>ROC-AUC</dt><dd id="benchmark-roc">—</dd></div>
|
||||
<div><dt>Brier ↓</dt><dd id="benchmark-brier">—</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<aside class="callout" data-requires-data>
|
||||
<span class="callout__icon" aria-hidden="true">i</span>
|
||||
<p><strong>Calibration diagnostic—not a final performance estimate:</strong> Platt scaling was fit on this same 2024 partition. PR-AUC and ROC-AUC summarize discrimination; lower Brier is better for probability error. Opened 2025 holdout metrics are intentionally not displayed here.</p>
|
||||
</aside>
|
||||
|
||||
<section class="publication-boundary" aria-labelledby="publication-boundary-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Future approved output</p>
|
||||
<h2 id="future-output-heading">Designed for calibrated context</h2>
|
||||
<p class="eyebrow">Finished publication boundary</p>
|
||||
<h2 id="publication-boundary-heading">No vehicle-level prediction service</h2>
|
||||
<p>This static dashboard serves supported aggregate sample rates and diagnostic summaries only. It contains no vehicle lookup, personal inputs, operational connection, or row-level prediction output.</p>
|
||||
</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>
|
||||
<li>Returning-vehicle next-episode target only</li>
|
||||
<li>First attempt; pass versus non-pass</li>
|
||||
<li>Final model: calibrated logistic regression</li>
|
||||
<li>Boosted tree: benchmark only</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
@@ -278,11 +279,16 @@
|
||||
<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>
|
||||
<p class="eyebrow">Private 10,000-vehicle development sample</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>
|
||||
|
||||
<aside class="scope-strip" aria-label="Result scope">
|
||||
<strong>Sample-based, not population-based</strong>
|
||||
<span>All displayed rates and diagnostics come from the development sample. County displays communicate feed coverage only.</span>
|
||||
</aside>
|
||||
|
||||
<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>
|
||||
@@ -330,13 +336,13 @@
|
||||
|
||||
<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>
|
||||
<header class="panel__header"><div><p class="eyebrow">Model governance</p><h2>Evidence status</h2></div></header>
|
||||
<ul class="limitation-list">
|
||||
<li>Calibrated logistic regression is the declared final prototype model.</li>
|
||||
<li>The boosted tree remains a benchmark only.</li>
|
||||
<li>Published diagnostics cover train, tune, and calibration partitions.</li>
|
||||
<li>No locked-test result or population performance claim is published.</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
@@ -344,7 +350,7 @@
|
||||
<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>The 10,000-vehicle draw retained 9,996 vehicles; page sampling can over-represent vehicles with more inspection records.</li>
|
||||
<li>Reject and abort can reflect process or readiness issues.</li>
|
||||
<li>Association is not causation or a mechanical diagnosis.</li>
|
||||
</ul>
|
||||
@@ -358,9 +364,9 @@
|
||||
<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>
|
||||
<span class="privacy-flow__public">Public assets<br><small>Suppressed summaries 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>
|
||||
<p>VINs, plates, ZIPs, stations, technician identifiers, private vehicle tokens, raw source JSON, credentials, operational records, and row-level predictions are outside this site and its public data contract.</p>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
@@ -368,8 +374,8 @@
|
||||
|
||||
<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><strong>Utah Vehicle Health</strong><p>Private 10,000-vehicle development sample.</p></div>
|
||||
<p>Sample-based research only—not a population estimate, diagnosis, certification, safety assessment, or guarantee of an inspection outcome.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
|
||||
+28
-79
@@ -6,15 +6,14 @@ import {
|
||||
renderAgeRisk,
|
||||
renderCohortDotPlot,
|
||||
renderCoverageHeatmap,
|
||||
renderNoCalibration,
|
||||
renderOutcomeTrend,
|
||||
renderUtahCoverage,
|
||||
} from "./charts.js";
|
||||
|
||||
const ROUTES = Object.freeze({
|
||||
overview: "Overview",
|
||||
reliability: "Reliability explorer",
|
||||
estimator: "Next-test estimator",
|
||||
cohorts: "Sample cohorts",
|
||||
model: "Model & benchmark",
|
||||
methods: "Data & methods",
|
||||
});
|
||||
|
||||
@@ -66,13 +65,6 @@ 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";
|
||||
@@ -120,7 +112,7 @@ function setPreviewBanner(manifest) {
|
||||
setText("preview-title", "Development preview");
|
||||
setText(
|
||||
"preview-copy",
|
||||
"These suppressed sample aggregates are not population estimates and must not be used for individual decisions.",
|
||||
"Private 10,000-vehicle development sample · suppressed aggregates only · not population estimates.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -144,25 +136,6 @@ function setUnavailable(error) {
|
||||
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();
|
||||
@@ -195,12 +168,12 @@ function renderOverview(data) {
|
||||
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(" / ")}`,
|
||||
`Final: calibrated logistic · release ${data.manifest.release_id.slice(0, 8)}`,
|
||||
);
|
||||
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.",
|
||||
"Development-sample aggregates only, not a population trend. The bundle publishes binary non-pass rates; four-class outcome mix and blank rates are not inferred.",
|
||||
);
|
||||
renderAgeRisk(byId("age-risk-chart"), data.ageRisk.rows);
|
||||
renderUtahCoverage(byId("utah-coverage-map"), counties);
|
||||
@@ -230,7 +203,7 @@ function renderScorecardCards(rows) {
|
||||
heading.textContent = scorecardLabel(row);
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "cohort-card__meta";
|
||||
meta.textContent = `${compactNumber(row.support_rounded)} rounded support`;
|
||||
meta.textContent = `≈${compactNumber(row.support_rounded)} eligible sample episodes (rounded)`;
|
||||
const bar = document.createElement("div");
|
||||
bar.className = "risk-bar";
|
||||
bar.setAttribute("aria-hidden", "true");
|
||||
@@ -242,7 +215,7 @@ function renderScorecardCards(rows) {
|
||||
const strong = document.createElement("strong");
|
||||
strong.textContent = percent(row.nonpass_rate);
|
||||
const small = document.createElement("small");
|
||||
small.textContent = "Observed non-pass";
|
||||
small.textContent = "Observed sample non-pass";
|
||||
value.append(strong, small);
|
||||
article.append(heading, meta, bar, value);
|
||||
container.append(article);
|
||||
@@ -259,33 +232,14 @@ function updateExplorer() {
|
||||
visibleScorecards = sortedScorecards(matches, sortMode);
|
||||
setText(
|
||||
"result-summary",
|
||||
`${visibleScorecards.length} supported cohort${visibleScorecards.length === 1 ? "" : "s"}; showing up to 18 cards and 12 chart rows.`,
|
||||
`${visibleScorecards.length} supported sample 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.";
|
||||
|
||||
function initializeExplorer() {
|
||||
byId("cohort-search").addEventListener("input", updateExplorer);
|
||||
byId("result-sort").addEventListener("change", updateExplorer);
|
||||
byId("reset-filters").addEventListener("click", () => {
|
||||
@@ -297,42 +251,37 @@ function initializeExplorer(data) {
|
||||
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 findDiagnostic(rows, model) {
|
||||
return rows.find((row) => row.model === model && row.partition === "calibrate");
|
||||
}
|
||||
|
||||
function renderModel(data) {
|
||||
const finalModel = findDiagnostic(data.diagnostics.rows, "logistic_platt");
|
||||
const benchmark = findDiagnostic(data.diagnostics.rows, "hist_gradient_boosting_platt");
|
||||
const metric = (row, key) => (row ? row[key].toFixed(3) : "—");
|
||||
|
||||
setText("final-ap", metric(finalModel, "average_precision"));
|
||||
setText("final-roc", metric(finalModel, "roc_auc"));
|
||||
setText("final-brier", metric(finalModel, "brier"));
|
||||
setText("benchmark-ap", metric(benchmark, "average_precision"));
|
||||
setText("benchmark-roc", metric(benchmark, "roc_auc"));
|
||||
setText("benchmark-brier", metric(benchmark, "brier"));
|
||||
}
|
||||
|
||||
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);
|
||||
initializeExplorer();
|
||||
renderModel(data);
|
||||
renderMethods(data);
|
||||
setHeaderStatus(
|
||||
"ready",
|
||||
data.manifest.development_preview ? "Validated development aggregates" : "Validated public aggregates",
|
||||
data.manifest.development_preview ? "Validated sample aggregates" : "Validated public aggregates",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -349,4 +298,4 @@ async function initialize() {
|
||||
|
||||
initialize();
|
||||
|
||||
export { displayCategory, normalizeCounty, routeFromHash };
|
||||
export { normalizeCounty, routeFromHash };
|
||||
|
||||
+5
-12
@@ -109,7 +109,7 @@ export function renderOutcomeTrend(container, rows) {
|
||||
</svg>`;
|
||||
container.setAttribute(
|
||||
"aria-label",
|
||||
`Quarterly aggregate non-pass trend. ${description}`,
|
||||
`Development-sample quarterly aggregate non-pass rates, not population estimates. ${description}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ export function renderAgeRisk(container, rows) {
|
||||
</svg>`;
|
||||
container.setAttribute(
|
||||
"aria-label",
|
||||
`Non-pass risk by vehicle-age band. ${points
|
||||
`Observed development-sample non-pass rates by vehicle-age band, not population estimates. ${points
|
||||
.map((point) => `${point.age_band}: ${percent(point.nonpass_rate)}`)
|
||||
.join("; ")}`,
|
||||
);
|
||||
@@ -199,7 +199,7 @@ export function renderCohortDotPlot(container, rows) {
|
||||
</svg>`;
|
||||
container.setAttribute(
|
||||
"aria-label",
|
||||
`Ranked supported cohort non-pass risk. ${points
|
||||
`Supported development-sample cohort associations, not reliability rankings or population estimates. ${points
|
||||
.map((point) => `${point.prior_make} ${point.prior_model}: ${percent(point.nonpass_rate)}`)
|
||||
.join("; ")}`,
|
||||
);
|
||||
@@ -234,7 +234,7 @@ export function renderUtahCoverage(container, coveredCounties) {
|
||||
container.setAttribute(
|
||||
"aria-label",
|
||||
coveredCounties.length
|
||||
? `Utah feed coverage includes ${coveredCounties.join(", ")}. Other counties are unavailable.`
|
||||
? `Development-sample feed availability includes ${coveredCounties.join(", ")}. This is not an outcome ranking; other counties are unavailable.`
|
||||
: "No county feed coverage is available.",
|
||||
);
|
||||
}
|
||||
@@ -267,14 +267,7 @@ export function renderCoverageHeatmap(container, rows) {
|
||||
</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.",
|
||||
`Development-sample source-era coverage from ${years[0]} through ${years[years.length - 1]} for ${eras.join(", ")}.`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
const SCHEMA_VERSION = "dashboard_data_v1";
|
||||
const APPROVED_PARTITIONS = Object.freeze(["train", "tune", "calibrate"]);
|
||||
const APPROVED_TARGET = "first-attempt next-episode binary non-pass rate";
|
||||
const REQUIRED_CALIBRATION_MODELS = Object.freeze([
|
||||
"hist_gradient_boosting_platt",
|
||||
"logistic_platt",
|
||||
]);
|
||||
const ENVELOPE_KEYS = Object.freeze([
|
||||
"development_preview",
|
||||
"population_estimate_allowed",
|
||||
@@ -20,15 +25,22 @@ export const REQUIRED_ASSETS = Object.freeze({
|
||||
const SENSITIVE_KEY_PARTS = new Set([
|
||||
"address",
|
||||
"certificate",
|
||||
"credential",
|
||||
"email",
|
||||
"inspector",
|
||||
"internal",
|
||||
"ip",
|
||||
"owner",
|
||||
"password",
|
||||
"pid",
|
||||
"plate",
|
||||
"prediction",
|
||||
"probability",
|
||||
"raw",
|
||||
"secret",
|
||||
"session",
|
||||
"station",
|
||||
"technician",
|
||||
"token",
|
||||
"user",
|
||||
"vin",
|
||||
@@ -337,6 +349,11 @@ export function validateAssetSet(rawAssets) {
|
||||
requireExactKeys(rawAssets, Object.keys(REQUIRED_ASSETS), "asset set");
|
||||
const manifest = rawAssets.manifest;
|
||||
validateEnvelope(manifest, "data_manifest");
|
||||
if (!manifest.development_preview || manifest.population_estimate_allowed) {
|
||||
throw new DataContractError(
|
||||
"The dashboard accepts development-sample, non-population assets only.",
|
||||
);
|
||||
}
|
||||
requireExactKeys(
|
||||
manifest,
|
||||
[
|
||||
@@ -427,6 +444,9 @@ export function validateAssetSet(rawAssets) {
|
||||
requireInteger(manifest.definitions[key], `data_manifest.definitions.${key}`, { min: 1 });
|
||||
}
|
||||
requireString(manifest.definitions.target, "data_manifest.definitions.target");
|
||||
if (manifest.definitions.target !== APPROVED_TARGET) {
|
||||
throw new DataContractError("data_manifest.definitions.target is outside the approved scope.");
|
||||
}
|
||||
if (!Array.isArray(manifest.assets)) {
|
||||
throw new DataContractError("data_manifest.assets must be an array.");
|
||||
}
|
||||
@@ -463,6 +483,16 @@ export function validateAssetSet(rawAssets) {
|
||||
throw new DataContractError(`model_diagnostics.rows[${index}].model is not cataloged.`);
|
||||
}
|
||||
}
|
||||
for (const model of REQUIRED_CALIBRATION_MODELS) {
|
||||
const matches = validated.diagnostics.rows.filter(
|
||||
(row) => row.model === model && row.partition === "calibrate",
|
||||
);
|
||||
if (matches.length !== 1) {
|
||||
throw new DataContractError(
|
||||
`model_diagnostics must contain exactly one approved calibration row for ${model}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
const minimumSupport = manifest.definitions.suppression_min_support;
|
||||
const supportRounding = manifest.definitions.support_rounding;
|
||||
for (const name of ["overview", "ageRisk", "scorecards", "coverage"]) {
|
||||
|
||||
+209
-2
@@ -37,12 +37,16 @@
|
||||
}
|
||||
|
||||
html {
|
||||
max-width: 100%;
|
||||
overflow-x: clip;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
min-width: 320px;
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
overflow-x: clip;
|
||||
color: var(--ink-950);
|
||||
background:
|
||||
radial-gradient(circle at 7% 2%, rgb(231 216 185 / 35%), transparent 30rem),
|
||||
@@ -165,6 +169,11 @@ h2 {
|
||||
margin-right: 0.35rem;
|
||||
}
|
||||
|
||||
.preview-banner > div {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.preview-banner__icon {
|
||||
color: var(--amber-700);
|
||||
}
|
||||
@@ -182,7 +191,8 @@ h2 {
|
||||
.site-header__inner,
|
||||
.primary-nav__inner,
|
||||
.page-shell {
|
||||
width: min(calc(100% - 2rem), var(--content));
|
||||
width: calc(100% - 2rem);
|
||||
max-width: var(--content);
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
@@ -302,10 +312,15 @@ h2 {
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
min-height: 65vh;
|
||||
}
|
||||
|
||||
.view {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
padding: clamp(2.5rem, 6vw, 5rem) 0 5rem;
|
||||
}
|
||||
|
||||
@@ -318,9 +333,17 @@ main {
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
min-width: 0;
|
||||
margin-bottom: clamp(2rem, 5vw, 3.4rem);
|
||||
}
|
||||
|
||||
.page-heading > *,
|
||||
.dashboard-grid > *,
|
||||
.kpi-grid > *,
|
||||
.model-comparison > * {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.page-heading--split {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
@@ -370,6 +393,26 @@ main {
|
||||
font-size: 1.04rem;
|
||||
}
|
||||
|
||||
.scope-strip {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.7rem 1rem;
|
||||
margin: -1.4rem 0 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid #e9c77d;
|
||||
border-radius: var(--radius-sm);
|
||||
color: #503407;
|
||||
background: rgb(249 236 205 / 72%);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.scope-strip strong {
|
||||
flex: 0 0 auto;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.unavailable-state {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -694,6 +737,14 @@ main {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.filter-panel__note {
|
||||
margin: 1rem 0 0;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--sand-200);
|
||||
color: var(--ink-650);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.text-button {
|
||||
padding: 0.2rem;
|
||||
border: 0;
|
||||
@@ -868,6 +919,124 @@ fieldset:disabled .field select {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.model-comparison {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.model-card {
|
||||
position: relative;
|
||||
padding: clamp(1.25rem, 3vw, 1.7rem);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--sand-200);
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgb(255 255 255 / 86%);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.model-card::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 4px;
|
||||
background: var(--sand-400);
|
||||
content: "";
|
||||
}
|
||||
|
||||
.model-card--final {
|
||||
border-color: #9bc9c6;
|
||||
background: linear-gradient(145deg, rgb(220 239 238 / 58%), rgb(255 255 255 / 92%));
|
||||
box-shadow: var(--shadow-md);
|
||||
}
|
||||
|
||||
.model-card--final::before {
|
||||
background: var(--teal-700);
|
||||
}
|
||||
|
||||
.model-card__topline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.2rem;
|
||||
color: var(--ink-500);
|
||||
font-size: 0.68rem;
|
||||
}
|
||||
|
||||
.decision-badge {
|
||||
padding: 0.25rem 0.55rem;
|
||||
border: 1px solid var(--sand-400);
|
||||
border-radius: 999px;
|
||||
color: #6a4c18;
|
||||
background: var(--amber-100);
|
||||
font-size: 0.64rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.decision-badge--final {
|
||||
border-color: #83bbb7;
|
||||
color: var(--teal-800);
|
||||
background: var(--teal-100);
|
||||
}
|
||||
|
||||
.model-card > p {
|
||||
min-height: 5.1em;
|
||||
margin-bottom: 1.2rem;
|
||||
color: var(--ink-650);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.metric-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.metric-list div {
|
||||
padding: 0.72rem;
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgb(237 240 237 / 78%);
|
||||
}
|
||||
|
||||
.metric-list dt {
|
||||
color: var(--ink-500);
|
||||
font-size: 0.62rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.metric-list dd {
|
||||
margin: 0.18rem 0 0;
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
.publication-boundary {
|
||||
display: grid;
|
||||
grid-template-columns: 1.15fr 0.85fr;
|
||||
gap: 2rem;
|
||||
align-items: start;
|
||||
margin-top: 1rem;
|
||||
padding: clamp(1.4rem, 3vw, 1.9rem);
|
||||
border-radius: var(--radius-lg);
|
||||
color: var(--white);
|
||||
background: var(--ink-950);
|
||||
}
|
||||
|
||||
.publication-boundary p:last-child {
|
||||
margin: 0;
|
||||
color: #cad7d5;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.publication-boundary .eyebrow {
|
||||
color: #9bd4d1;
|
||||
}
|
||||
|
||||
.locked-panel {
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
@@ -1242,6 +1411,8 @@ fieldset:disabled .field select {
|
||||
.dashboard-grid--wide,
|
||||
.explorer-layout,
|
||||
.split-section,
|
||||
.model-comparison,
|
||||
.publication-boundary,
|
||||
.privacy-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -1260,10 +1431,26 @@ fieldset:disabled .field select {
|
||||
}
|
||||
|
||||
@media (max-width: 660px) {
|
||||
.preview-banner {
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.preview-banner__icon {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.site-header__inner {
|
||||
min-height: 68px;
|
||||
}
|
||||
|
||||
.page-shell {
|
||||
width: auto;
|
||||
max-width: calc(100% - 2rem);
|
||||
margin-inline: 1rem;
|
||||
}
|
||||
|
||||
.header-status,
|
||||
.brand__tagline {
|
||||
display: none;
|
||||
@@ -1286,6 +1473,21 @@ fieldset:disabled .field select {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.scope-strip {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.lede,
|
||||
.data-stamp,
|
||||
.scope-strip,
|
||||
.kpi-card,
|
||||
.model-card,
|
||||
.panel {
|
||||
max-width: 100%;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.data-stamp {
|
||||
width: 100%;
|
||||
margin-top: 1.3rem;
|
||||
@@ -1296,10 +1498,15 @@ fieldset:disabled .field select {
|
||||
.method-grid,
|
||||
.cohort-cards,
|
||||
.form-grid,
|
||||
.method-preview {
|
||||
.method-preview,
|
||||
.metric-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.model-card > p {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.kpi-card {
|
||||
min-height: 130px;
|
||||
}
|
||||
|
||||
@@ -52,13 +52,25 @@ function filesRecursively(directory) {
|
||||
});
|
||||
}
|
||||
|
||||
function viewSource(html, route) {
|
||||
const marker = `<section class="view" id="view-${route}"`;
|
||||
const start = html.indexOf(marker);
|
||||
if (start < 0) return "";
|
||||
const remaining = html.slice(start + marker.length);
|
||||
const nextView = remaining.indexOf('<section class="view" id="view-');
|
||||
const mainEnd = remaining.indexOf("</main>");
|
||||
const candidates = [nextView, mainEnd].filter((index) => index >= 0);
|
||||
const end = candidates.length ? Math.min(...candidates) : remaining.length;
|
||||
return html.slice(start, start + marker.length + end);
|
||||
}
|
||||
|
||||
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"]) {
|
||||
for (const route of ["overview", "cohorts", "model", "methods"]) {
|
||||
assert.match(html, new RegExp(`data-route="${route}"`));
|
||||
assert.match(html, new RegExp(`data-view="${route}"`));
|
||||
}
|
||||
@@ -67,20 +79,45 @@ test("static shell exposes four semantic navigable views", () => {
|
||||
assert.doesNotMatch(html, /<strong>Locked test<\/strong>|<small>Final evaluation<\/small>/);
|
||||
});
|
||||
|
||||
test("estimator remains disabled and contains no identifying input", () => {
|
||||
test("narrow layouts constrain body content while preserving local nav scrolling", () => {
|
||||
const css = readFileSync(path.join(DASHBOARD_ROOT, "styles.css"), "utf8");
|
||||
assert.match(css, /html\s*{[^}]*max-width:\s*100%;[^}]*overflow-x:\s*clip;/s);
|
||||
assert.match(css, /body\s*{[^}]*max-width:\s*100%;[^}]*overflow-x:\s*clip;/s);
|
||||
assert.match(css, /\.primary-nav__inner\s*{[^}]*overflow-x:\s*auto;/s);
|
||||
const mobile = css.match(/@media \(max-width: 660px\)\s*{[\s\S]*?(?=@media|$)/)?.[0];
|
||||
assert.ok(mobile, "mobile breakpoint is present");
|
||||
assert.match(mobile, /\.page-shell\s*{[^}]*width:\s*auto;[^}]*max-width:\s*calc\(100% - 2rem\);/s);
|
||||
assert.match(mobile, /\.preview-banner\s*{[^}]*justify-content:\s*flex-start;/s);
|
||||
assert.match(mobile, /overflow-wrap:\s*anywhere;/);
|
||||
});
|
||||
|
||||
test("model view declares the final model, benchmark role, and publication boundary", () => {
|
||||
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,
|
||||
);
|
||||
const modelView = viewSource(html, "model");
|
||||
assert.ok(modelView, "model section is present");
|
||||
assert.match(modelView, /Calibrated logistic regression/);
|
||||
assert.match(modelView, /Final prototype/);
|
||||
assert.match(modelView, /Calibrated boosted tree/);
|
||||
assert.match(modelView, /Benchmark only/);
|
||||
assert.match(modelView, /partition used to fit Platt calibration/i);
|
||||
assert.match(modelView, /not independent final-performance estimates/i);
|
||||
assert.match(modelView, /No vehicle-level prediction service/);
|
||||
assert.doesNotMatch(modelView, /<(?:input|select|textarea|form)\b/i);
|
||||
assert.doesNotMatch(modelView, /prediction_lookup|planned tool|future approved output/i);
|
||||
});
|
||||
|
||||
test("every result-facing view visibly labels the sample and non-population scope", () => {
|
||||
const html = readFileSync(path.join(DASHBOARD_ROOT, "index.html"), "utf8");
|
||||
assert.match(html, /Private 10,000-vehicle development sample/);
|
||||
for (const route of ["overview", "cohorts", "model", "methods"]) {
|
||||
const view = viewSource(html, route);
|
||||
assert.ok(view, `${route} view is present`);
|
||||
assert.match(view, /scope-strip/);
|
||||
assert.match(view, /sample/i);
|
||||
assert.match(view, /not[^.]{0,80}population|population estimate/i);
|
||||
}
|
||||
assert.match(html, /not a county outcome ranking/i);
|
||||
assert.match(html, /not a reliability ranking or population comparison/i);
|
||||
});
|
||||
|
||||
test("all required generated assets satisfy the browser contract", () => {
|
||||
@@ -91,6 +128,12 @@ test("all required generated assets satisfy the browser contract", () => {
|
||||
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.ok(validated.diagnostics.rows.some(
|
||||
(row) => row.model === "logistic_platt" && row.partition === "calibrate",
|
||||
));
|
||||
assert.ok(validated.diagnostics.rows.some(
|
||||
(row) => row.model === "hist_gradient_boosting_platt" && row.partition === "calibrate",
|
||||
));
|
||||
assert.deepEqual(
|
||||
validated.filters.partitions,
|
||||
["train", "tune", "calibrate"],
|
||||
@@ -151,10 +194,37 @@ test("contract fails closed for missing, inconsistent, or sensitive data", () =>
|
||||
inconsistent.ageRisk.population_estimate_allowed = true;
|
||||
assert.throws(() => validateAssetSet(inconsistent), DataContractError);
|
||||
|
||||
const populationClaim = assetSet();
|
||||
populationClaim.manifest.population_estimate_allowed = true;
|
||||
assert.throws(() => validateAssetSet(populationClaim), DataContractError);
|
||||
|
||||
const broadenedTarget = assetSet();
|
||||
broadenedTarget.manifest.definitions.target = "any inspection outcome";
|
||||
assert.throws(() => validateAssetSet(broadenedTarget), DataContractError);
|
||||
|
||||
const sensitive = assetSet();
|
||||
sensitive.scorecards.rows[0].vehicle_token = "not-public";
|
||||
assert.throws(() => validateAssetSet(sensitive), DataContractError);
|
||||
|
||||
for (const deniedField of [
|
||||
"vin",
|
||||
"plate",
|
||||
"zip",
|
||||
"station",
|
||||
"technician_id",
|
||||
"inspector_id",
|
||||
"row_prediction",
|
||||
"probability",
|
||||
"raw_json",
|
||||
"credential",
|
||||
"password",
|
||||
"secret",
|
||||
]) {
|
||||
const denied = assetSet();
|
||||
denied.overview.rows[0][deniedField] = "not-public";
|
||||
assert.throws(() => validateAssetSet(denied), DataContractError, deniedField);
|
||||
}
|
||||
|
||||
const extraRowField = assetSet();
|
||||
extraRowField.overview.rows[0].note = "unapproved";
|
||||
assert.throws(() => validateAssetSet(extraRowField), DataContractError);
|
||||
@@ -167,6 +237,20 @@ test("contract fails closed for missing, inconsistent, or sensitive data", () =>
|
||||
extraPartition.filters.partitions.push("locked_test");
|
||||
assert.throws(() => validateAssetSet(extraPartition), DataContractError);
|
||||
|
||||
const missingFinalDiagnostic = assetSet();
|
||||
missingFinalDiagnostic.diagnostics.rows = missingFinalDiagnostic.diagnostics.rows.filter(
|
||||
(row) => !(row.model === "logistic_platt" && row.partition === "calibrate"),
|
||||
);
|
||||
assert.throws(() => validateAssetSet(missingFinalDiagnostic), DataContractError);
|
||||
|
||||
const duplicatedBenchmarkDiagnostic = assetSet();
|
||||
duplicatedBenchmarkDiagnostic.diagnostics.rows.push({
|
||||
...duplicatedBenchmarkDiagnostic.diagnostics.rows.find(
|
||||
(row) => row.model === "hist_gradient_boosting_platt" && row.partition === "calibrate",
|
||||
),
|
||||
});
|
||||
assert.throws(() => validateAssetSet(duplicatedBenchmarkDiagnostic), DataContractError);
|
||||
|
||||
const unsortedVersions = assetSet();
|
||||
unsortedVersions.manifest.model_versions = ["z_v1", "a_v1"];
|
||||
assert.throws(() => validateAssetSet(unsortedVersions), DataContractError);
|
||||
@@ -194,3 +278,15 @@ test("dashboard source has no external runtime dependency or credential marker",
|
||||
assert.doesNotMatch(source, /<script[^>]+src=["']https?:|@import\s+url\(["']?https?:/i);
|
||||
}
|
||||
});
|
||||
|
||||
test("public data directory contains only the approved suppressed aggregate bundle", () => {
|
||||
const expected = Object.values(REQUIRED_ASSETS).sort();
|
||||
const observed = readdirSync(PUBLIC_DATA).sort();
|
||||
assert.deepEqual(observed, expected);
|
||||
|
||||
const serialized = observed
|
||||
.map((filename) => readFileSync(path.join(PUBLIC_DATA, filename), "utf8"))
|
||||
.join("\n");
|
||||
assert.doesNotMatch(serialized, /\b[A-HJ-NPR-Z0-9]{17}\b/);
|
||||
assert.doesNotMatch(serialized, /(?:postgres(?:ql)?:\/\/|PGPASSWORD|BEGIN [A-Z ]*PRIVATE KEY)/i);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user