From fb8f9e1c490366a70c2d5327154d2eb1fb878021 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Mon, 14 Sep 2026 12:58:57 -0700 Subject: [PATCH] refactor: expose portable FIRMS CSV helpers --- CHANGELOG.md | 2 ++ docs/CODE-BOUNDARIES.md | 11 +++++-- docs/CURRENT-STATE.md | 4 +++ package.json | 1 + scripts/package-boundaries.json | 5 +++ src/data/firmsCsv.test.mjs | 20 +++++++++++- src/data/fixtures/firms-csv-cases.json | 44 ++++++++++++++++++++++++++ 7 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 src/data/fixtures/firms-csv-cases.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 948b70467..0c84ebe29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,8 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md ## [Unreleased] +- Expose existing FIRMS CSV parsing and UTC time-window helpers through a portable package export, with shared contract fixtures. + - Separate map source factories from switching and resource ownership; retain current source IDs, attribution and fallbacks. - Separate radio directory loading, station selection, globe presentation and playback into composed components with an explicit metadata source. diff --git a/docs/CODE-BOUNDARIES.md b/docs/CODE-BOUNDARIES.md index cf71854d8..b00b6311a 100644 --- a/docs/CODE-BOUNDARIES.md +++ b/docs/CODE-BOUNDARIES.md @@ -144,9 +144,14 @@ reconstruction mechanics with injectable acquisition dependencies. `gods-eye-view/sources/traffic` exports tile math and budget calculations. `gods-eye-view/sources/gbfs` exports host/path acceptance and cache-header rules. These entries import no Node middleware, application configuration or rendering. -Callers retain their request admission and transport policy. FIRMS CSV parsing -remains an owned dependency of the FIRMS Node provider. The boundary gate -checks each entry independently. +Callers retain their request admission and transport policy. + +`gods-eye-view/sources/firms-csv` exports the existing CSV parser, header +recognition, acquisition-time conversion and trailing-day filter independently +of the Node middleware. It imports no Node, DOM, rendering or network code. +The Node provider continues using the same implementation; contract fixtures +cover malformed rows, acquisition times, empty feeds and the inclusive time +window. The boundary gate checks each entry independently. Browser terrain sampling, traffic matching/drawing, fire overlays and bike-share layer lifecycle remain in their current modules. This extraction changes no diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 6bf6ad5ea..ecadda8da 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1,5 +1,9 @@ # God's Eye View Current State +The `sources/firms-csv` package export exposes existing FIRMS CSV parsing and +UTC acquisition-window helpers without Node middleware or rendering dependencies. +The standalone FIRMS provider, source defaults and parsing behavior are unchanged. + CLI tools and development launchers accept `GEV_PROJECT_ROOT` to select the project environment, dependencies and output directory. Their default remains this checkout; bundled tool resources and source checks stay relative to the script installation. The setup doctor also accepts an explicit `rootDir`. Application composition now uses shared scene/control/catalog/tool constructors and allowlisted HTML component templates. The standalone entry supplies settings and the default sources. Terrain, boundaries, weather, regional context and summaries use configurable services; renderer/action owners remain page-scoped. Annotation proximity guards remain on by default and allow distant targets only for explicit navigation. diff --git a/package.json b/package.json index b6aa6c956..3f0d97a7f 100644 --- a/package.json +++ b/package.json @@ -148,6 +148,7 @@ "./aircraft": "./src/layers/aircraft/index.js", "./layers/vessels": "./src/layers/vessels/index.js", "./layers/earthquakes": "./src/layers/earthquakes/index.js", + "./sources/firms-csv": "./src/data/firmsCsv.js", "./layers/firms": "./src/layers/firms/index.js", "./layers/satellites": "./src/layers/satellites/index.js", "./layers/launches": "./src/layers/launches/index.js", diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index fb9469e52..7cadae541 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -115,6 +115,11 @@ "modules": ["src/data/tomtomTiles.js"], "external": [] }, + "firms-csv": { + "exports": ["./sources/firms-csv"], + "modules": ["src/data/firmsCsv.js"], + "external": [] + }, "firms-provider": { "runtime": "node", "exports": ["./server/providers/firms"], diff --git a/src/data/firmsCsv.test.mjs b/src/data/firmsCsv.test.mjs index 438f728cf..798946e25 100644 --- a/src/data/firmsCsv.test.mjs +++ b/src/data/firmsCsv.test.mjs @@ -9,7 +9,7 @@ import { filterTrailing24h, isLikelyCsv, parseFirmsCsv, -} from './firmsCsv.js'; +} from 'gods-eye-view/sources/firms-csv'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const FIXTURE = fs.readFileSync( @@ -144,3 +144,21 @@ test('filterTrailing24h on the fixture keeps everything for a same-night now', ( const kept = filterTrailing24h(records, Date.UTC(2026, 6, 17, 2, 0)); assert.equal(kept.length, records.length); }); + +const contractCases = JSON.parse( + fs.readFileSync( + path.join(__dirname, 'fixtures', 'firms-csv-cases.json'), + 'utf8', + ), +); +for (const fixture of contractCases) { + test(`portable FIRMS CSV contract: ${fixture.name}`, () => { + const records = parseFirmsCsv(fixture.csv); + assert.equal(records === null ? null : records.length, fixture.parsedCount); + const recent = filterTrailing24h(records, Date.parse(fixture.now)); + assert.deepEqual( + recent.map((record) => record.acqTime), + fixture.recentTimes, + ); + }); +} diff --git a/src/data/fixtures/firms-csv-cases.json b/src/data/fixtures/firms-csv-cases.json new file mode 100644 index 000000000..a57541fae --- /dev/null +++ b/src/data/fixtures/firms-csv-cases.json @@ -0,0 +1,44 @@ +[ + { + "name": "categorical confidence and unpadded midnight time", + "csv": "latitude,longitude,acq_date,acq_time,confidence,frp\n30,-97,2026-07-16,45,n,12\n", + "now": "2026-07-16T12:00:00Z", + "parsedCount": 1, + "recentTimes": ["45"] + }, + { + "name": "truncated rows are omitted", + "csv": "latitude,longitude,acq_date,acq_time,confidence,frp\n30,-97,2026-07-16,1200\n", + "now": "2026-07-16T12:00:00Z", + "parsedCount": 0, + "recentTimes": [] + }, + { + "name": "invalid acquisition hour does not roll into tomorrow", + "csv": "latitude,longitude,acq_date,acq_time,confidence,frp\n30,-97,2026-07-15,2400,n,12\n", + "now": "2026-07-16T12:00:00Z", + "parsedCount": 1, + "recentTimes": [] + }, + { + "name": "inclusive trailing day and two-hour forward slack", + "csv": "latitude,longitude,acq_date,acq_time,confidence,frp\n30,-97,2026-07-15,1200,n,12\n30,-97,2026-07-15,1159,n,12\n30,-97,2026-07-16,1400,h,12\n30,-97,2026-07-16,1401,l,12\n", + "now": "2026-07-16T12:00:00Z", + "parsedCount": 4, + "recentTimes": ["1200", "1400"] + }, + { + "name": "upstream error is different from an empty catalog", + "csv": "Invalid MAP_KEY", + "now": "2026-07-16T12:00:00Z", + "parsedCount": null, + "recentTimes": [] + }, + { + "name": "header-only catalog is successful and empty", + "csv": "latitude,longitude,acq_date,acq_time,confidence,frp\n", + "now": "2026-07-16T12:00:00Z", + "parsedCount": 0, + "recentTimes": [] + } +]