condition_aggregator_jma 0.3.0
condition_aggregator_jma: ^0.3.0 copied to clipboard
JMA (気象庁) adapter for the condition_aggregator interface. Maps the JMA windowless per-prefecture warning JSON to source-neutral Advisory typed events. Pure Dart.
Changelog #
0.3.0 — 2026-06-29 — Conservative prefecture union at borders (over-warn on resolution; partial reads signalled) #
Border-resolution change (minor bump). Prefecture resolution at a border changes from single-first-match to a conservative union: when a lat/lon falls inside more than one catalogued prefecture bounding box, the provider now fetches every containing prefecture's warning JSON and surfaces the merged, deduplicated union of their in-force warnings.
- Why (safety — under-warn fix): the catalogued bounding boxes are crude axis-aligned rectangles over irregular prefectures, so adjacent prefectures' boxes overlap along every shared border. The previous resolver returned only the first matching box, silently dropping the neighbour(s). No single-prefecture tie-break is correct at every border — e.g. a nearest- centroid guess merely trades a northern-border error for a southern coastal one. The result was that a driver near a prefecture border could miss a neighbouring prefecture's warning purely because of a resolution guess.
- Fix: at a border the provider surfaces all candidate prefectures' warnings (deduplicated). A driver near a prefecture border may now see a neighbouring prefecture's warning — the safe, over-warn direction, consistent with this package's conservative-on-uncertain philosophy. An interior point still resolves to exactly one prefecture (single fetch).
- Concurrency + budget (single 30 s per prefecture, everywhere): at a
border (more than one containing prefecture) the prefectures are fetched
concurrently; each per-prefecture fetch carries its OWN per-request
timeout set to the single
kJmaFetchWallClockBudget(30 s) and always resolves to a captured result rather than throwing, so one hung endpoint cannot block, fail, or discard the whole batch. The same 30 s value is used for an interior single fetch and for every border sibling — there is deliberately no shorter border budget. (An earlier draft used a shorter 10 s per-request cap at borders; it was removed because it could time out HER own slow-but-valid warning: if her prefecture answers a real 大雪警報 on a marginal 10–30 s link while the other containing prefecture answers fast-empty, the 10 s cap turned her warning into a captured failure → empty union + a failure → incomplete-read throw → she got nothing. Never drop a slow-but-valid warning.)- Latency bound (honest cost): per-prefecture isolation already prevents a
hung sibling from blocking or discarding a fast sibling's success, so the
only cost of the single budget is latency — a hung border neighbour can
make the union take up to ~30 s before it returns (carrying the in-band
incomplete-read notice). That pre-trip latency is preferred over ever
dropping a slow-but-valid warning. The batch-level timeout is retained
only as the outer backstop for the theoretical case where the
Future.waitcombinator machinery itself stalls.
- Latency bound (honest cost): per-prefecture isolation already prevents a
hung sibling from blocking or discarding a fast sibling's success, so the
only cost of the single budget is latency — a hung border neighbour can
make the union take up to ~30 s before it returns (carrying the in-band
incomplete-read notice). That pre-trip latency is preferred over ever
dropping a slow-but-valid warning. The batch-level timeout is retained
only as the outer backstop for the theoretical case where the
- Robustness hardening (worst-case / degraded-connectivity):
- Hung-sibling isolation. Previously a single hung endpoint blocked the
Future.waitbatch until the batch-level timeout fired and threw — discarding an already-successful sibling's warnings. At an Akita / Yamagata border with Akita succeeding fast and Yamagata hanging, the driver would have got nothing where the single-prefecture path gave Akita's 大雪警報. Each fetch now self-bounds at one per-request budget and is captured as a failure, so the successful sibling's warnings are still returned. - Captured-failure isolation. Each per-prefecture fetch's catch was
broadened from
JmaAdvisoryFetchExceptionto any error, so a rawFormatException(e.g. fromutf8.decodeof malformed bytes) is captured as a failed sibling rather than escaping and discarding a successful one. - Timeout exception shape. The timeout / all-failed exception now
preserves a meaningful source
uri(the per-request path carries it), restoring the field the single-prefecture path had.
- Hung-sibling isolation. Previously a single hung endpoint blocked the
- Partial-failure policy (conservative; a partial read is never presented as
complete): the over-warn invariant is unconditional — whenever a
containing border prefecture was not successfully checked, the caller (and the
aggregator) always learns it, either by a thrown exception or by an
in-band notice in the returned list.
- The union collects every prefecture that fetched successfully; a successfully fetched warning is never withheld because a sibling fetch failed.
- If every prefecture fetch fails, a
JmaAdvisoryFetchExceptionis thrown (the first failure's shape — uri / statusCode / message — is preserved). - If the union is empty AND at least one containing prefecture failed,
a
JmaAdvisoryFetchException(incomplete read) is thrown rather than returning[]. An empty list would be a false 'no warnings' all-clear for a border where the unreachable prefecture could hold an active 大雪警報; the incomplete read tells the integrator 'could not fully determine', not 'all clear'. (A genuine all-clear — every containing prefecture succeeded with no in-force snow warning — still returns[].) The thrown exception preserves the failing sibling'suri+statusCode. - If the union is non-empty AND at least one containing prefecture
failed, the real warnings are returned plus a synthetic,
clearly-marked, LOW-severity incomplete-read notice (
AdvisorywitheventClass == kJmaIncompleteReadEventClass,severityminor) naming the unreachable prefecture(s) in Japanese. Previously the captured failure was silently discarded here, so a partial border read (e.g. a mild reachable 着雪注意報 while an unreachable sibling could hold a 大雪特別警報) landed as a complete, fully-successful result with no staleness signal — a silent under-warn at the exact scenario this border-union exists for. The notice carries the lowest severity + a distinct event identity, so it can never masquerade as — or be deduped against — a real weather warning. - Residual (disclosed, narrowed): a warning from an unreachable prefecture cannot be surfaced (we cannot read what we could not fetch) — but it is no longer silently dropped: a non-empty union with a failed sibling now carries the incomplete-read notice flagging that an unreachable containing prefecture exists. So the union is over-warn on the resolution guess (it never drops a neighbour because the resolver chose one side) and signals a partial read; it does not guarantee a warning held by an unreachable prefecture is shown — only flagged.
- Driver-facing label localized to Japanese (
areaDescription). At a border the prefecture label is the load-bearing way a Japanese-reading driver tells their own prefecture's warning from an over-warned neighbour's; the headline / event name were already verbatim Japanese, so the structuredareaDescriptionlabel is now Japanese too (e.g.秋田県/山形県rather thanAkita/Yamagata). NewkJmaPrefectureNamesJa/jmaPrefectureNameJaexpose the map; the romajikJmaPrefectureNames/jmaPrefectureNameare retained for logs and non-Japanese consumer UIs. - Deduplication: the union is deduplicated on the full advisory identity (source, event class, severity, certainty, urgency, area, effective, expires, headline, description), so two genuinely different warnings (e.g. the same snow class reported by two different prefectures, each carrying its own prefecture label) both survive, while a byte-identical record is never listed twice.
- API additions (non-breaking):
prefectureCodesForPoint({latitude, longitude}) → List<String>— all catalogued prefecture codes containing the point, in deterministic catalog order (empty when outside the catalog).prefectureCodeForPointis retained for back-compat; it now returns the first containing box (equivalent to the first element ofprefectureCodesForPoint). Callers wanting border-correct behaviour should migrate toprefectureCodesForPoint.kJmaPrefectureNamesJa/jmaPrefectureNameJa(code)— the driver-facing Japanese prefecture-name map / lookup used forAdvisory.areaDescription. The existing romajikJmaPrefectureNames/jmaPrefectureNameare unchanged.kJmaIncompleteReadEventClass— theeventClassof the synthetic incomplete-read notice (so consumers can match / filter / render it distinctly from a real warning).buildIncompleteReadNotice(List<String> failedPrefectureCodes)— builds that notice (exposed for integrator-side replay / rendering tests).
- Residual approximation (disclosed): bounding-box granularity remains coarser than actual prefecture geometry. The union is the deliberate over-warn handling of that approximation, not a claim of exact prefecture geofencing.
- Unchanged: the public entrypoint
fetchActiveAdvisoriesAtPoint({latitude, longitude}) → Future<List<Advisory>>, the bounding-box catalog (6 snow-zone prefectures), the snow-class code mapping,init(), the byte cap, and theJmaAdvisoryFetchExceptionshape.
0.2.0 — 2026-06-26 — Windowless per-prefecture warning JSON (fixes scroll-off false-negative) #
Source-architecture change (minor bump). The default data source moves from
the JMA disaster-info atom feed (extra.xml + per-prefecture report XML) to
the windowless per-prefecture warning JSON
(https://www.jma.go.jp/bosai/warning/data/warning/{areacode}.json).
-
Why (safety — false-negative fix): an independent safety audit (AAA) found that both JMA atom feeds (
extra.xmlandextra_l.xml) carry a window / scroll-off false-negative for a snow-WARNING package. The atom feed is a recent-publication window (a stream of the latest reports). A warning that is still in force but was last re-issued before the window opens scrolls off the feed and is silently missed — so an active-but-not-recently-re-issued 大雪警報 could return no advisory, the worst failure for a winter-safety adapter. (0.1.6 had already shown the atom-feed path was fragile for a separate reason —extra_l.xmloutgrew the byte cap.) -
Fix: the per-prefecture warning JSON has no window — it always reflects the prefecture's current in-force warning state. It is also tiny (~7 KB for Akita, verified live 2026-06-26 at 7,424 bytes, HTTP 200) vs ~0.6 MB for the national atom feed, so it is lighter on constrained in-vehicle hardware.
-
Snow-class mapping by code. The bosai warning JSON keys warnings on a numeric
code. The snow codes were verified against the JMA bosai warning code taxonomy (cross-checked across three independent references, 2026-06-26) and are consistent with the live Akita feed (14= 雷注意報, observed in force):06→ 大雪警報 (severe)12→ 大雪注意報 (moderate)02→ 暴風雪警報 (severe)26→ 着雪注意報 (moderate)36→ 大雪特別警報 (extreme — 特別警報 / emergency level)32→ 暴風雪特別警報 (extreme — 特別警報 / emergency level)
NOTE — the bosai warning JSON code system is distinct from the jmaxml report
<Kind><Code>used by the old path (e.g.06=大雪警報 here, vs33in the report XML), so the codes were re-derived, not carried over. -
特別警報 (emergency) snow codes added — worst-case coverage. An independent safety audit (AAA) flagged that the catalog mapped 警報 / 注意報 but not the highest-severity 特別警報 (emergency /
level:50) snow class — strictly more dangerous than 警報, and an unacceptable silent gap for a worst-case winter-safety package. Codes36(大雪特別警報) and32(暴風雪特別警報) were added, each verified directly against the JMA bosai warning frontend's own servedcode2WarningInfolookup (2026-06-26): the page inlines36:{nameParts:e.snow[5],elem:"snow",level:50}withe.snow[5]=["大雪","特別警報"], and32:{nameParts:e.wind_snow[5],elem:"wind_snow",level:50}withe.wind_snow[5]=["暴風雪","特別警報"](level:50= 特別警報). Both map toAdvisorySeverity.extremevia the existing 特別警報-suffix rule (checked before the bare 警報 suffix, so they do not mis-map to severe). -
着雪 has no 特別警報 level — none added. The served
e.snow_accretionarray has only the advisory slot populated ([[],[],["着雪","注意報"],[],[],[]]), so 着雪 has no 警報 or 特別警報 level; there is no 着雪特別警報 to add. -
暴風雪注意報has no bosai-JSON code. JMA's official 注意報 taxonomy has no暴風雪注意報; the advisory-level counterpart of暴風雪警報is風雪注意報(code13), which is outside this version's snow catalog. The name is retained inkJmaSnowAdvisoryEventNamesfor back-compat, but the windowless JSON source never emits it, so no Advisory is produced for it. -
Preserved: the public entrypoint
fetchActiveAdvisoriesAtPoint({latitude, longitude}) → Future<List<Advisory>>, the lat/lon → prefecture bounding-box catalog (6 snow-zone prefectures, unchanged),init()(still validates the User-Agent),source/ attribution, the severity-by-suffix mapping (警報→severe / 注意報→moderate / 特別警報→extreme), and verbatim event-name relay (Article 17 β). -
API changes:
- Added:
kJmaWarningJsonBaseUrl,kJmaWarningJsonMaxBytes,kJmaSnowWarningCodes,parseJmaWarningJson,JmaWarningRecord,mapJmaWarningToAdvisory. - Removed (atom-path-specific):
kDefaultJmaAtomFeedUrl,kJmaPrefectureWarningReportTitles,kJmaAtomFeedMaxBytes,kJmaReportXmlMaxBytes,parseJmaAtomFeed,parseJmaReportXml,JmaAtomEntry,JmaForecastRecord,mapJmaForecastToAdvisory. - Constructor:
atomFeedUrl→warningJsonBaseUrl(defaulthttps://www.jma.go.jp/bosai/warning/data/warning/);userAgentand injectableclientunchanged. - Dropped the
xmldependency — the JSON path parses viadart:convert.
- Added:
-
Result deduplication: the same warning code repeats across every sub-area / municipality in the JSON; results are deduplicated to one Advisory per distinct in-force snow code, with
areaDescriptionset to the prefecture name (e.g.Akita) — the lat/lon resolution is prefecture-level. -
Tests rewritten for the JSON path, including a fixture asserting a
06code maps to a verbatim大雪警報Advisory (severitysevere) and a fixture of the real live Akita 雷注意報-only state asserting the snow filter returns[].
0.1.6 — 2026-06-26 — Fix: live feed exceeded byte cap (returned no advisories) #
- Bug fix (live-feed-breaking): the default atom feed was the long-history
extra_l.xml, which has grown past 5 MB (observed 2026-06-26 at 5,356,952 bytes) and exceeded the 4 MBkJmaAtomFeedMaxBytescap. As a result every livefetchActiveAdvisoriesAtPoint(...)call threwJmaAdvisoryFetchException: response exceeded 4194304-byte capand returned NO advisories — the package was non-functional against the live JMA feed. - Fix: switch the default feed to the regular
extra.xml(https://www.data.jma.go.jp/developer/xml/feed/extra.xml), observed 2026-06-26 at 635,035 bytes. It carries the same prefecture-class 気象警報・注意報 (VPWW54) warning entries the adapter needs (verified live, including Akita050000), in a recent-window (~10 h) form that is what a current-conditions lookup wants — JMA re-issues active warnings at its standard update cycles well inside that window, and the shorter feed surfaces fewer superseded/cancelled reports. It is also lighter to fetch and parse on constrained in-vehicle hardware. The 4 MB cap now sits ~6x above the live default-feed size. - Integrators that explicitly need a longer history can still override
atomFeedUrlat construction time (and raise the cap accordingly). - Added a regression test asserting the cap accommodates a live-national-feed-sized
body and pinning the default-feed choice to
extra.xml. - No change to the advisory mapping, prefecture filter, or per-report parse path.
0.1.5 — 2026-06-24 — Public API: jmaPrefectureName #
- Add public
jmaPrefectureName(String code)(exported via the barrel) — maps a JMA prefecture code (e.g.050000) to its name, so a consumer UI can show an honest place label instead of a raw numeric code. - No behaviour change to the advisory-mapping path.
0.1.4 — 2026-06-14 — Dependency hygiene #
- Track latest
condition_aggregator(^0.0.4→^0.0.5). - No source or behaviour change.
0.1.3 #
- Republish from the embedded-target Dart 3.10.1 SDK (Flutter 3.38.3) to correct a stale
^3.11.0SDK floor in the previously-published artifact. No source or behavior change; the source already declaredsdk: ^3.10.0. Restorespub getfor embedded/automotive Dart consumers on Dart 3.10.x.
0.1.2 — 2026-05-10 — Pana score recovery (Theme α P3) #
- Trim pubspec
descriptionto within the pana 60–180 character target. - Add minimal
example/main.dartfor pana documentation scoring. - Apply
dart formatto clear any formatter findings. - No SDK source changes; metadata + format pass only.
0.1.1 — 2026-05-04 — atom-feed byte cap raised to 4 MB #
The 0.1.0 byte cap on the atom feed (50 KiB) was set conservatively
without a real-world measurement. Observation against the live JMA
extra_l.xml endpoint shows the feed is approximately 1.6 MB during
normal cadence — it lists all recent JMA reports across Japan, not
per-point. A 50 KiB cap aborts the fetch at the size guard before
any parsing can run.
kJmaAtomFeedMaxBytes raised to 4 * 1024 * 1024 (4 MB), which gives
generous headroom for busy weather days while still preventing a
runaway publisher response from exhausting integrator memory. The
per-report cap (kJmaReportXmlMaxBytes, 200 KiB) is unchanged —
prefecture warning XML reports are typically well under that.
No other behavior change. No API change.
0.1.0 — 2026-05-04 — first deploy via direct-Dart-XML-parse path #
Status: DEPLOYED to pub.dev. The provider fetches the JMA
disaster-info atom feed (extra_l.xml), filters to the
prefecture-class warning report families
(気象警報・注意報(H27), 気象特別警報・警報・注意報), parses each
linked per-prefecture report XML directly with the canonical Dart
xml package, and surfaces winter-snow-class advisories
(大雪警報 / 大雪注意報 / 暴風雪警報 / 暴風雪注意報 / 着雪注意報) as
source-neutral Advisory records.
The jmaxml engagement-shape election (alpha/beta/gamma) for an upstream typed binding remains a separate open question (OQ-1) and does not block this version's publish.
Added #
JmaAdvisoryProvidernow performs real HTTP I/O against the JMA atom feed (defaulthttps://www.data.jma.go.jp/developer/xml/feed/extra_l.xml) + per-prefecture report XML; the constructor accepts an injectablehttp.Clientfor testing and a configurableuserAgent(default(sngnav-class app, https://github.com/aki1770-del/sngnav)).JmaAdvisoryFetchExceptionfor transport / non-2xx / oversize-body failures (caps: 50 KB atom feed, 200 KB per report, 30 s wall clock).parseJmaAtomFeed(String)+parseJmaReportXml(String)exposed for direct consumer use against fixture payloads (e.g. integrator-side replay tests).JmaAtomEntryprojection of the atom-feed entry fields the provider keys on (title,updated,author,reportUrl).JmaForecastRecordre-shaped to carry per-<Item>× per-<Area>publisher fields verbatim (eventName,eventCode,headline,areaName,areaCode,reportDateTime,targetDateTime).kJmaSnowAdvisoryEventNamesconst set — 5 event names the adapter surfaces at this version.kJmaPrefectureBoundingBoxes+prefectureCodeForPoint— bounding-box catalog for 6 snow-zone prefectures (Hokkaido / Aomori / Iwate / Akita / Yamagata / Niigata). Points outside the catalog return empty without an HTTP fetch.- Severity mapping: 警報 →
severe; 注意報 →moderate; 特別警報 →extreme. JMA's verbatim event name is preserved inAdvisory.eventClasseither way per Article 17 (β). - ≥6 new tests covering: prefecture-code resolution, atom-feed parse, report-XML parse, snow-class filter, prefecture filter, HTTP 5xx → exception, body-cap → exception, no-fetch on out-of-catalog point.
Changed #
pubspec.yaml: removedpublish_to: none; bumpedversion: 0.0.1→0.1.0; addedhttp: ^1.0.0; bumpedcondition_aggregator: ^0.0.2→^0.0.3(matching pub.dev-latest).- README "Status" replaced "explore-phase scaffold" with the direct-parse deploy posture.
- Library doc + provider doc + mapper doc updated to reflect the
direct-parse path; the placeholder JMA-report-family code
(
VPWW54) language replaced with the publisher's per-event name (大雪警報etc.).
Open questions retained from 0.0.1 #
- OQ-1 jmaxml engagement-shape election (alpha/beta/gamma) — separate from this version's deploy. A future major version may swap the direct-parse path for the elected upstream binding; the public API is shaped so the swap does not produce churn.
- Per-event-class CAP severity / certainty / urgency table validation against multiple JMA sample feeds — 0.1.0 ships a conservative suffix-based severity mapping; a richer table lands at the same time as the engagement-shape election or later if validated against expanded fixtures.
- Prefecture catalogue expansion past 6 (currently Hokkaido / Aomori / Iwate / Akita / Yamagata / Niigata) — adding prefectures is a deliberate version bump.
0.0.1 — 2026-05-06 — Explore-phase scaffold #
Status: superseded by 0.1.0. This version was never published
(publish_to: none); it shipped on disk as the API-shape lock so
consuming packages could wire against the interface during
explore-phase. See 0.1.0 for the first deployed shape.