pretrip_decision_advisor
0.5.0. Contract + working reference advisor + localized reason chips + humidity-aware black-ice (radiative frost) condition.
This package ships an abstract contract, its data shapes, and a working pure-Dart reference advisor (
SnowAwarePretripAdvisor) plus a source-neutral measured-visibility merge.pub add-ing it gives you a usable advisor, not just types. It does not fetch weather or integrate a route engine — a source-specific fetcher (which owns any HTTP dependency) stays outside this package, so the package itself remains pure Dart with a single runtime dependency —navigation_safety_calibration, the family's dependency-free Magnus effective-temperature source of truth (added in 0.5.0 for the humidity-aware black-ice condition; depend-don't-copy) — and no FFI, so the offline core runs on 32-bit ARM (armv7) car-class hardware. The pure-Dart core was run-verified on genuinearmv7(uname armv7l, 32-bit ARM Dart) producing the Akita briefing identical to the documented x86_64 output; see the runnability proof. (The Flutter visual render onarmv7is gated on flutter/flutter#188063 and is not yet proven.)
Aspiration
The pre-trip departure-timing decision is often a larger pain point than in-drive alerts. A driver asking "should I leave now or wait an hour?" has to combine a forecast, a commute shape, and personal context, and the answer changes whether the trip happens at all. Apps focused on alerts during driving address a smaller window than apps that address departure timing. This package defines the shape of an advisor that could help with that question, so other packages and applications can experiment against a common interface.
Cohorts served
The package serves several distinct downstream cohorts:
- Integrator developers building parallel navigation products on top of common interfaces.
- Open-source consumers depending on shared safety-domain vocabulary.
- Configuration consumers inheriting predictable defaults.
- Drivers (indirectly, via integrator products) who benefit from the pre-trip decision layer addressing a prevention scenario before the in-drive compound-failure scenario.
- Parallel-product builders publishing their own concrete advisors against this contract without forking it.
What is in the package
PretripAdvisor— abstract advisor contract. Given a forecast, a commute shape, and a driver profile spec, it returns a recommendation, ornullto mean "no recommendation; the driver should depart on their own judgment."PretripRecommendation— a suggested delay window, a confidence window, a recommendation strength, and a list of human-readable reason chips.RecommendationStrength—advisoryWeak,advisoryStrong, andhonestyMode.honestyModeis used when the commute is required and the advisor explicitly defers to the driver rather than telling someone to risk being late for required obligations.CommuteShapeandCommuteFlexibility— describe the planned trip, including whether the commute is required, discretionary, or unknown.WeatherForecast,HourlyForecast, andRoadConditionEstimate— the forecast inputs the advisor consumes.DriverProfileSpec— a small profile spec, decoupled from any specific full driver-profile package, so consumers can adopt this advisor without taking on a full safety-core dependency.SnowAwarePretripAdvisor— a deterministic, pure-Dart reference implementation of the contract. No LLM, no network, no clock: the same typed inputs always produce the same recommendation, so the worst-case path stays offline. Null forecast fields never fabricate a hazard, and it returnsnullwhen the forecast does not cover the departure window.PretripBriefing,PretripVerdict, andHourHazard— the richer typed verdict the reference advisor exposes viabrief(...), for UIs that want the structured result alongside the contract-shaped recommendation.VisibilityObservationandmergeObservedVisibility— a source-neutral measured-visibility observation and its departure-hour merge (a real sensor value overrides forecast visibility for the departure hour only, and is never projected into later forecast hours).PretripMessages— a hand-rolled, pure-Dart locale table for the advisor's reason chips.PretripMessages.en(default + fallback) andPretripMessages.ja;PretripMessages.forLanguage(code)resolves one and falls back to English (never throws). Measured numbers pass through every locale verbatim. Extend it to localize into a language this package does not yet carry.
What is NOT in the package
- No weather data fetching (no HTTP dependency — a source-specific fetcher produces the typed inputs and stays outside this package).
- No route engine integration.
Quick start
dart pub add pretrip_decision_advisor
End-to-end: measured source → briefing
This package is pure Dart, offline, and deterministic. A pretrip_source_*
provider gives you a measured VisibilityObservation and a WeatherForecast;
you merge the measurement into the departure hour, call brief(...), and
render the typed result in your own UI. The snippet below constructs its inputs
inline so it runs with no network — see "Pair with a measured source" to
fetch real measurements. (This is also the package's example/main.dart.)
// End-to-end pre-trip briefing from this package, offline + deterministic.
//
// A pretrip_source_* provider (JMA / MET Norway / Digitraffic) gives you a
// measured VisibilityObservation and a WeatherForecast; here we construct them
// inline so the example is reproducible with no network. See those packages to
// fetch real measurements.
//
// HONESTY (binding): visibility is NEVER estimated; a warning never produces a
// number; an observation is valid for the DEPARTURE HOUR only; null = the
// driver's own judgment (a real source returns null, never a fabricated value).
import 'package:pretrip_decision_advisor/pretrip_decision_advisor.dart';
void main() {
// 1. A winter-morning forecast — temperature only, NO visibility (exactly the
// shape a compact global forecast gives; this is why a MEASURED visibility
// source is needed to reach the whiteout band).
final forecast = WeatherForecast(
issuedAt: DateTime(2026, 1, 1, 6),
hourly: [
for (var h = 7; h <= 11; h++)
HourlyForecast(hour: DateTime(2026, 1, 1, h), tempCelsius: -6),
],
);
// 2. A MEASURED visibility observation, exactly as a pretrip_source_* provider
// emits it. 80 m is the labelled whiteout case; a real provider returns
// null (never a fabricated number) when no fresh reading is in range.
final observed = VisibilityObservation(
meters: 80,
stationId: 32402,
stationName: 'Akita',
measuredAt: DateTime(2026, 1, 1, 7, 10),
distanceKm: 0.4,
);
final commute = CommuteShape(
plannedDeparture: DateTime(2026, 1, 1, 7, 15),
plannedDuration: const Duration(minutes: 30),
routeIdentifiers: const ['akita-morning-errand'],
flexibility: CommuteFlexibility.discretionary,
);
// 3. Merge the NOW measurement into the DEPARTURE-HOUR slot only (never
// projected forward), then brief.
final merged =
mergeObservedVisibility(forecast, observed, commute.plannedDeparture);
final briefing = const SnowAwarePretripAdvisor().brief(
forecast: merged,
commute: commute,
profile: const DriverProfileSpec(
profileTag: 'akitaRural', reactionTimeSeconds: 1.5),
);
// 4. Read the typed result out — this is what an edge dev renders in their UI.
print('Verdict : ${briefing.verdict.name}');
print('Strength: ${briefing.recommendation?.strength.name ?? "(none)"}');
print('Delay : ${briefing.recommendation?.suggestedDelay ?? Duration.zero}');
print('Chips :');
for (final c in briefing.chips) {
print(' - $c');
}
print('Measured: ${observed.meters} m at ${observed.stationName} '
'(${observed.distanceKm.toStringAsFixed(1)} km away)');
}
Running it (dart run example/main.dart) prints the typed result an edge
developer renders — the measured 80 m lights the whiteout/severe band a
temperature-only forecast alone could never reach:
Verdict : waitAdvised
Strength: advisoryStrong
Delay : 1:00:00.000000
Chips :
- Visibility may drop to ~80 m around 07:00 — whiteout conditions.
- Conditions improve by about 08:15.
Chips (ja):
- 07:00頃、視界が約80mまで低下する可能性があります — ホワイトアウト状態です。
- 08:15頃までに状況は改善します。
Measured: 80.0 m at Akita (0.4 km away)
Pair with a measured source
This package is pure Dart and fetches nothing. Pair it with a source package
that produces the typed inputs — all emit the SAME VisibilityObservation /
WeatherForecast, so you can swap region without changing your UI code:
| Region / network | pub add |
Emits |
|---|---|---|
| Japan — JMA / AMeDAS | pretrip_source_jma |
measured VisibilityObservation (metres) |
| Finland — Fintraffic Digitraffic | pretrip_source_digitraffic |
measured VisibilityObservation (metres) |
| Global — MET Norway locationforecast | pretrip_source_met_norway |
hourly WeatherForecast |
Localized reason chips
The reason chips can speak the driver's language. Pass a PretripMessages
table; English is the default and the fallback for any language not carried, so
existing callers are unchanged:
final advisor = SnowAwarePretripAdvisor(
messages: PretripMessages.forLanguage(locale.languageCode), // 'ja' → Japanese
);
PretripMessages.en (default + fallback) and PretripMessages.ja ship today;
forLanguage degrades to English for any language not carried (it never
throws). Measured numbers — visibility, temperature, minutes, hours — are
identical in every locale; only the surrounding words change, so a translation
can never restate a safety value.
Need a language this package does not carry yet? PretripMessages is an
abstract class — extend it with your own strings (the same pattern as the
built-in PretripMessages.ja) and pass it as messages:. You must implement
every method (the analyzer enforces this — a missing override will not
compile). The deterministic hazard logic is unchanged and every measured
number is handed to your method already computed — pass it through verbatim; a
localization may reorder words but must never restate a measured safety value.
class KoPretripMessages extends PretripMessages {
const KoPretripMessages();
@override
String visibilityWhiteout(int meters, String at) => '...';
// ...implement the rest of the abstract methods...
}
// SnowAwarePretripAdvisor(messages: const KoPretripMessages())
Full reference integration (Flutter)
A standalone, edge-developer-shaped Flutter app that assembles and RENDERS a
pre-trip briefing from pretrip_decision_advisor + pretrip_source_jma — no
SNGNav app widgets — lives at
examples/edge_dev_akita_briefing/.
Start from its 15-minute QUICKSTART (flutter create → offline Akita briefing). To ship that offline core onto 32-bit ARM (armv7) car-class hardware, see the embedded-armv7 on-ramp — the pure-Dart core is armv7-proven; the Flutter render is gated on flutter/flutter#188063.
Honesty
If a commute is marked CommuteFlexibility.required, an advisor
implementing this contract must not return a strong "wait" recommendation;
it should return either null or a honestyMode recommendation. The
advisor cannot tell a driver to risk being late for a required obligation,
because the cost of doing so is borne by the driver, not the advisor.
Status
0.5.0. Contract + working reference advisor, localized reason chips, and the
humidity-aware black-ice (radiative frost) condition bounded to the
calibration's documented envelope.
Interface stability is committed at this version within the bounds described in
KNOWN_LIMITATIONS.md.
Libraries
- pretrip_decision_advisor
- Pre-trip departure-timing decision advisor.