NTSB accidents: read the bulk archive, because there is no working API - #38
Merged
Conversation
The aviation collection shipped without the half that makes it worth keeping.
`faa-nas-status` says the airspace is delayed today and `aviation-metar` says
what the sky is doing over the field, and neither says what happens when it
goes wrong. This adds the 31,000 accidents and incidents the NTSB has
investigated, and the reason it belongs beside the other two rather than in a
collection of its own: the `events` table carries a `metar` column holding the
raw observation at the moment of the accident, in the identical format
`aviation-metar` publishes hourly. The same field, from the same service, thirty
years apart. 252 of 400 accidents in one sampled slice carry one.
WHY A 96 MB DOWNLOAD
The NTSB's query service answers a malformed request with a 400 that names the
problem and a well-formed one with `500 An unknown exception occured` [sic]. It
is not an interface. What the NTSB does publish reliably is `avall.zip`: 96 MB
holding a 558 MB Access database, rebuilt monthly, newest event eight days old.
The monthly delta archives beside it stop in December 2022, so the full file is
the file. `mdbtools` and `unzip` go into the Dockerfile; `mdb-json` writes one
JSON object per line, which is why the narrative table reads as 522,053 lines
and 28,436 rows -- the CSV export does not escape the newlines inside a
narrative and cannot be split on them.
RE-READING IT IS THE POINT, NOT THE COST
An NTSB investigation is published long before it is finished: a preliminary
report within days, a probable cause that can take two years, all under one
`ev_id`. So the adapter re-reads events it already holds and lets the upsert do
the work. An unchanged accident hashes to the row already stored and costs no
write; one the NTSB has since ruled on gains its probable cause in place rather
than arriving as a second row contradicting the first. In the newest slice 6 of
300 are ruled; 8,000 rows deeper it is 307 of 400. Those 294 rows are what a
re-read is for.
A run emits a bounded slice, newest first, and remembers where it stopped,
because `upsertItems` sends every row in one statement and 31,000 accidents
carrying their narratives is 140 MB of text in a single query. The extracts are
cached against the file's publication date, read off the NTSB's own listing
page, so only the first slice pays for the download and a new archive restarts
the walk at the newest accident.
FOUR THINGS THE DATA DECIDED
- The dates are `08/22/26`, and `new Date` reads that as 1926. Every accident in
the file would publish a century early and sort ahead of everything else in
the collection. The database starts in 1982, so 82 and above is last century.
- A regex alternation of `(\d{2}|\d{4})` matches the `20` of `03/04/2015` and
yields 2020 -- a wrong date that parses, sorts, and raises nothing. Four
digits are tried first. A test written for the century boundary caught it.
- The `latitude` column holds packed DMS (`003000N`) beside a `dec_latitude` of
30. Read as a number the packed pair puts a Texas accident three thousand
degrees north, so only the decimal columns are used.
- `damage` is `SUBS`, and printed raw it reads "Aircraft subs." SUBS is the
threshold that makes an event an accident rather than an incident, so it is
the most important word in the row and it is now a word. The operator columns
carry a bare `N` on many rows, which printed straight reads "operated by N".
Also: the three collection tests derived their expected kinds from a hardcoded
list of adapter names, so adding an adapter to a collection made the list wrong
rather than making the test pass. They now derive from the registry. That is
what failed when these feeds were added, which is the test working.
Verified against the real 2026-09-01 archive: 31,124 events, 31,670 aircraft
rows, 28,436 narratives; a 400-row slice at offset 8,000 gave 307 ruled, 60
fatal and 252 with a METAR, with probable-cause text intact. 577 tests pass,
biome clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CjTtJPEpyJbvPeQcVfYPPs
ralyodio
added a commit
that referenced
this pull request
Sep 9, 2026
…er them (#39) Four collections' worth of work since 0.3.1, in two merges. #37 added three collections built so that the feeds inside each explain one another rather than sitting side by side. Aviation: the FAA's traffic management initiatives, written once more when each one ends with how long it ran, which the FAA never publishes; every SIGMET and AIRMET in force; and the decoded observation at the airport underneath. Water: NOAA river gauges at or above their action stage, coastal levels measured against the height at which each station floods, and the weekly Drought Monitor. Consumer finance: the CFPB's complaints, around seven thousand a day, joined by name to the FDIC register of every insured bank and to every merger, failure and branch closing on it. #38 added the NTSB's 31,124 accident and incident investigations, read from the bulk Access archive because the NTSB's query API answers a well-formed request with a 500. It goes in aviation rather than in a collection of its own because its `events` table carries the raw METAR at the moment of each accident, in the identical format the hourly feed publishes today. Twenty-seven collections, seventy-six adapters. The image now carries mdbtools and unzip for the NTSB reader; nothing else in it needs either. Claude-Session: https://claude.ai/code/session_01CjTtJPEpyJbvPeQcVfYPPs Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ralyodio
added a commit
that referenced
this pull request
Sep 9, 2026
Three sources, all keyless, all verified live before they were written.
BUOYS AND SURF, INTO WATER
NDBC publishes the newest observation from every buoy it is listening to in one
hundred-kilobyte file: 853 reports, 184 of them carrying a wave height. Wave
height, dominant period and mean direction are what a surf report is actually
made of, and the period is the number people leave out -- two metres at 18
seconds and two metres at 5 seconds are completely different days in the water.
So a wave row is published as "8.9 ft at 8 s, mixed swell out of the W" as well
as stored as numbers, in feet as well as metres, because every US surf forecast
is in feet and converting at read time is how a nine-foot day becomes a
three-foot day.
Beside it, the National Weather Service Surf Zone Forecast from every coastal
office, in full. A buoy off Oahu reading 2.4 m at 16 seconds is a fact;
"advisory level surf along south facing shores through today" is what it means
to anyone standing on the beach, and no arithmetic over the buoy produces it.
The high surf advisory and the rip current risk are lifted from the
forecaster's own wording into tags so a coast under an advisory can be found
without reading 851 products, and the full text is stored as the authority.
AIRCRAFT, INTO AVIATION
There is a version of this that stores every aeroplane in the sky every few
minutes. It would be tens of thousands of rows a day, each a position that was
true for four seconds, and nobody would read one. A position is not news; an
aircraft squawking 7700 is. So this reads the event surfaces: the three
emergency transponder codes and the military list. At the time of writing the
emergency codes returned zero aircraft between them and the military query
returned 422, which is the shape you want -- the rare thing is rare, so its
appearance means something.
An emergency is an episode, not a ping, and it is held the same way
`faa-nas-status` holds a ground stop: open episodes live in the cursor keyed on
the ICAO hex, the row is keyed on when the aircraft was first seen, and when it
leaves the list it is written once more with a duration. "N123AB squawked 7700
for 22 minutes" is the row. The forty positions in between are not.
FOUR THINGS THE LIVE DATA DECIDED
- NDBC writes a missing reading as `MM`, in every column, in a fixed-width
table. `Number('MM') || 0` turns "this buoy has no anemometer" into a flat
calm and "no wave sensor" into a dead-flat sea, and 669 of 853 stations
report no wave height at all. One guard, and null means the buoy did not say.
- The station register really does publish `name=""` -- 15009 in the Atlantic
array is one of several. An empty string is not a name, and left as one it
beats the fallback and titles the row with nothing at all. That was visible
in the first smoke run as rows titled ": marine observation".
- Some stations report a dominant period of 0 or 1 second. That is a sensor
saying nothing, and classifying it as "windswell" states something about the
water nobody measured. Under two seconds, the period is left out.
- adsb.lol rate limits `/v2/mil` far harder than the squawk lists: it answered
429 while `/sqk/7700` beside it answered 200. A limit is not a failure, so it
returns no items and asks to be called back, which also keeps the cursor
intact -- a run that threw there would leave every open episode untouched and
then report them all as ended on the run after.
ALSO CORRECTED
The comment added in #38 said the NTSB reader slices its output because
`upsertItems` sends every row in one statement. That is wrong: `runSource`
already chunks writes into batches of 200. The slicing is still right, for the
reasons now stated -- 140 MB of narrative text held in memory, and the
four-minute ingest deadline -- but the stated reason was not the real one.
Verified live: 853 buoy observations with 184 reporting waves and one station
over 2.5 m; 40 Pacific and 23 Hawaii surf forecasts with the Honolulu high surf
advisory correctly tagged; all three emergency squawk lists answering with zero
aircraft, which is the correct answer. 596 tests pass, biome clean.
Claude-Session: https://claude.ai/code/session_01CjTtJPEpyJbvPeQcVfYPPs
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The aviation collection shipped without the half that makes it worth keeping.
faa-nas-statussays the airspace is delayed today andaviation-metarsayswhat the sky is doing over the field, and neither says what happens when it
goes wrong. This adds the 31,000 accidents and incidents the NTSB has
investigated, and the reason it belongs beside the other two rather than in a
collection of its own: the
eventstable carries ametarcolumn holding theraw observation at the moment of the accident, in the identical format
aviation-metarpublishes hourly. The same field, from the same service, thirtyyears apart. 252 of 400 accidents in one sampled slice carry one.
WHY A 96 MB DOWNLOAD
The NTSB's query service answers a malformed request with a 400 that names the
problem and a well-formed one with
500 An unknown exception occured[sic]. Itis not an interface. What the NTSB does publish reliably is
avall.zip: 96 MBholding a 558 MB Access database, rebuilt monthly, newest event eight days old.
The monthly delta archives beside it stop in December 2022, so the full file is
the file.
mdbtoolsandunzipgo into the Dockerfile;mdb-jsonwrites oneJSON object per line, which is why the narrative table reads as 522,053 lines
and 28,436 rows -- the CSV export does not escape the newlines inside a
narrative and cannot be split on them.
RE-READING IT IS THE POINT, NOT THE COST
An NTSB investigation is published long before it is finished: a preliminary
report within days, a probable cause that can take two years, all under one
ev_id. So the adapter re-reads events it already holds and lets the upsert dothe work. An unchanged accident hashes to the row already stored and costs no
write; one the NTSB has since ruled on gains its probable cause in place rather
than arriving as a second row contradicting the first. In the newest slice 6 of
300 are ruled; 8,000 rows deeper it is 307 of 400. Those 294 rows are what a
re-read is for.
A run emits a bounded slice, newest first, and remembers where it stopped,
because
upsertItemssends every row in one statement and 31,000 accidentscarrying their narratives is 140 MB of text in a single query. The extracts are
cached against the file's publication date, read off the NTSB's own listing
page, so only the first slice pays for the download and a new archive restarts
the walk at the newest accident.
FOUR THINGS THE DATA DECIDED
08/22/26, andnew Datereads that as 1926. Every accident inthe file would publish a century early and sort ahead of everything else in
the collection. The database starts in 1982, so 82 and above is last century.
(\d{2}|\d{4})matches the20of03/04/2015andyields 2020 -- a wrong date that parses, sorts, and raises nothing. Four
digits are tried first. A test written for the century boundary caught it.
latitudecolumn holds packed DMS (003000N) beside adec_latitudeof30. Read as a number the packed pair puts a Texas accident three thousand
degrees north, so only the decimal columns are used.
damageisSUBS, and printed raw it reads "Aircraft subs." SUBS is thethreshold that makes an event an accident rather than an incident, so it is
the most important word in the row and it is now a word. The operator columns
carry a bare
Non many rows, which printed straight reads "operated by N".Also: the three collection tests derived their expected kinds from a hardcoded
list of adapter names, so adding an adapter to a collection made the list wrong
rather than making the test pass. They now derive from the registry. That is
what failed when these feeds were added, which is the test working.
Verified against the real 2026-09-01 archive: 31,124 events, 31,670 aircraft
rows, 28,436 narratives; a 400-row slice at offset 8,000 gave 307 ruled, 60
fatal and 252 with a METAR, with probable-cause text intact. 577 tests pass,
biome clean.
🤖 Generated with Claude Code
https://claude.ai/code/session_01CjTtJPEpyJbvPeQcVfYPPs