EnviPersonal environmental health
00 / Technical overview

How Envi turns environmental data into a personal caution level.

Envi is built around one principle: a machine decides the safety tier with transparent, auditable rules, and language only ever describes that decision. This page documents the data, the math, the guardrails, and the honesty mechanisms behind every reading.

01 / Architecture

A one-way pipeline with a deterministic core

Every assessment flows through five stages. The safety-critical middle (risk scoring) is pure, deterministic TypeScript with no I/O and no model in the loop, which makes it unit-testable and auditable.

  1. 01

    Ingest

    Open-Meteo air, UV, weather; Google pollen fallback

  2. 02

    Normalize

    Canonical readings + confidence

  3. 03

    Risk model

    Per-hazard 0 to 4 tier + reasons

  4. 04

    Guidance

    Grounded actions + guardrails

  5. 05

    Deliver

    JSON API to the web client

Source layout: src/core holds the pure engine (classifiers, risk model, guidance), src/providers wraps external data, src/assess.ts orchestrates, and src/server.ts exposes it over HTTP. The browser client is dependency-free HTML, CSS, and JavaScript.

02 / Data

Data sources and provenance

Open-Meteo remains the key-free backbone for CAMS air quality, UV, European pollen, and weather. When Open-Meteo has no pollen coverage and a server-side key is configured, Envi requests Google's daily Pollen API forecast. Both sources provide modeled forecasts, not observed monitor readings, and the UI preserves the source.

SignalEndpointNotes on provenance
Air qualityair-quality (us_aqi + sub-indices)US EPA AQI computed by the provider from CAMS data. PM sub-indices use a 24h rolling average. We classify, we do not recompute AQI from an instantaneous concentration.
Heatforecast (temperature, humidity)We compute the NWS Heat Index ourselves so the math is testable and anchored to a public standard.
UVair-quality (uv_index)Classified on the WHO Global Solar UV Index scale. Hourly series gives the upcoming peak.
PollenOpen-Meteo CAMS species; Google Pollen fallbackOpen-Meteo supplies grains/m3 in Europe. Elsewhere, Google supplies a daily Universal Pollen Index for tree, grass, and weed where covered. Google responses are not cached or stored; absent coverage remains confidence "none".

03 / Classification

Normalization and classification

Each raw value is mapped to its authoritative environmental category before any personalization. The Heat Index is the one value Envi computes directly, using the National Weather Service Rothfusz regression:

// NWS Heat Index (Rothfusz regression), degrees Fahrenheit
export function heatIndexF(tF, rh) {
  const simple = 0.5 * (tF + 61 + (tF - 68) * 1.2 + rh * 0.094);
  if ((simple + tF) / 2 < 80) return round1((simple + tF) / 2);
  let hi = -42.379 + 2.04901523*tF + 10.14333127*rh
         - 0.22475541*tF*rh - 0.00683783*tF*tF - 0.05481717*rh*rh
         + 0.00122874*tF*tF*rh + 0.00085282*tF*rh*rh - 0.00000199*tF*tF*rh*rh;
  // low- and high-humidity corrections applied per NWS
  return round1(hi);
}

Categories: air maps to the six EPA AQI bands (Good through Hazardous); heat to the five NWS caution bands; UV to the five WHO bands. Pollen uses grains-per-cubic-meter bands for CAMS data or Google's UPI bands for fallback data.

04 / Risk

The Personal Exposure Risk model

Every hazard resolves to one of five tiers. The tier starts from the official environmental category, then moves up based on the user's health profile. Each step records a plain-language reason, so the "why" is always traceable.

0 Minimal 1 Low 2 Moderate 3 High 4 Extreme

Air is representative. The base tier comes from the AQI category, then bumps apply:

// Air: base tier from EPA AQI category, then profile-driven bumps
let tier = base;                          // 0..4 from the AQI category
if (sensitive && base >= 1) tier++;       // asthma, COPD, heart disease, age, pregnancy, diabetes
if (highExertion && base >= 2) tier++;    // strenuous activity or high time outdoors
if (standard === "who" && pm2_5_24h > 15) tier++;  // WHO 2021 24h guideline, real 24h mean only
tier = clamp(tier, 0, 4);
HazardBaseRaises the tier for
AirEPA AQI bandRespiratory, cardiac, diabetes, immune, pregnancy, children, older adults; exertion; WHO mode when the 24h PM2.5 mean exceeds 15 ug/m3.
HeatNWS Heat Index bandOlder adults, children, cardiac, pregnancy, diabetes, respiratory; strenuous or high outdoor exertion.
UVWHO UV band (unchanged)Fair skin (Fitzpatrick I to II), children, photosensitivity. The environmental category never changes with skin type; only the personal tier and advice do.
PollenCAMS species grains or Google UPIPollen allergy and asthma. Unavailable data stays tier 0 with confidence "none".

The overall caution level is the worst hazard tier. When two specific hazards are both elevated, Envi adds an evidence-backed synergy note (heat plus air pollution, or pollen plus air pollution) and stays silent for pairs with no established interaction.

05 / Guidance

Guidance and guardrails

The guidance engine reads the tier and emits prioritized actions. It never sets a tier. Its guardrails:

  • No dosing. Medication guidance always points to the user's prescribed action plan, never a dose.
  • Hard-coded emergency escalation. Extreme tiers add an emergency action deterministically, not by model choice.
  • Every action is sourced to EPA, CDC, WHO, or AAAAI guidance.
  • No false all-clear. A missing hazard produces an explicit "not an all-clear" notice.
// Hard-coded emergency escalation (never model-decided)
if (heat.tier >= 4) actions.push({
  priority: "emergency",
  text: "Know heat-stroke signs: confusion, hot dry skin, fainting. "
      + "If they appear, call emergency services immediately.",
  source: "CDC heat-safety guidance / NWS Heat Index",
});

06 / Confidence

Confidence and data gaps

Every reading carries a confidence value. Because the WHO guideline is a 24-hour mean, Envi refuses to compare it against an instantaneous value: it only exposes a 24h PM2.5 mean when all 24 hourly samples are present.

// Only expose a 24h mean when all 24 samples exist, so the WHO
// 24h guideline is compared against a real 24h mean.
function trailing24Mean(values, idx) {
  if (idx < 23) return null;
  let sum = 0;
  for (let i = idx - 23; i <= idx; i++) {
    if (values[i] == null) return null;   // require all 24
    sum += values[i];
  }
  return sum / 24;
}

The orchestrator injects an unavailable placeholder for any of the four expected hazards the provider omits, so data completeness reflects the whole set. If nothing at all can be measured, the interface shows a neutral "Unavailable" state and never a green all-clear.

07 / AI

The AI layer, by design

Envi is deliberately not an "ask a model what is safe" app. The safety verdict is deterministic. The intended role of a language model is a constrained one, and the architecture reserves a clean seam for it:

  • Deterministic tiers and retrieved guidance produce the facts, from a curated EPA, CDC, and WHO knowledge base.
  • A language model personalizes phrasing and sequences actions to the user's day, constrained to the retrieved guidance.
  • A hard safety envelope forbids overriding a tier, forbids dosing, and forces emergency escalation.
  • An evaluation harness of profile-by-scenario cases gates any change to guidance correctness.

The current build ships the deterministic core and rule-based, templated guidance, which is exactly the part that must be correct. The model-driven phrasing layer is the next seam to fill, not a rewrite.

08 / Safety

Safety and honest limitations

  • The personal caution level is a conservative prototype heuristic, not a clinically validated algorithm. The interface labels it as a prototype and shows the official category next to it.
  • Envi presents public-health guidance and is positioned as a general-wellness tool, not medical advice or a diagnostic device.
  • It is single-source and modeled today, so readings can disagree with local monitors. The confidence field is the honest signal.
  • The richer CDC and NWS HeatRisk model is not yet integrated; heat currently uses the NWS Heat Index.

09 / Testing

Testing and verification

The safety core is covered by 41 unit tests under strict TypeScript. They defend observable contracts rather than implementation details:

  • AQI, Heat Index, and UV category boundaries, including a known Heat Index value.
  • Sensitivity bumps and the WHO gate, including that it does not fire without a real 24h mean.
  • Synergy notes fire only for evidence-backed hazard pairs.
  • All-clear appears only when every hazard has data and is tier 0; missing data yields a notice.
  • Emergency escalation is present at extreme tiers, no action contains a dose, and every action is sourced.

Verification is bunx tsc --noEmit (clean), bun test (41 passing), plus a live browser smoke test of the full flow.

10 / Stack

Stack and roadmap

Runtime Bun with TypeScript. Server Bun.serve, no framework. Client dependency-free HTML, CSS, and JavaScript, offline-friendly and accessible (semantic markup, keyboard listbox, reduced-motion, dark mode, color paired with icon, label, and number). Dependencies none at runtime.

Next, in priority order:

  • A multi-source fusion layer with reconciled confidence bands to fix single-source disagreement.
  • The CDC and NWS HeatRisk model where available.
  • The constrained language phrasing layer plus its evaluation gate.
  • Wearable signals, proactive pre-exposure alerts, and an SMS fallback for reach.
  • Clinical validation of the personalization rules before any real deployment.