diff --git a/CHANGELOG.md b/CHANGELOG.md index 9091c9283..f6530661f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,7 +36,7 @@ - Director timeline seeks cross the dateline along the short longitude arc. -- Reject impossible FIRMS acquisition calendar days in the CSV parser before trailing-24-hour filtering. +- Regional articles retain unknown publication times when compact source dates do not identify a real calendar instant. - Treat explicitly current, complete empty vessel coverage as a successful update, clearing obsolete contacts while retaining stale or incomplete snapshots. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index dbf5e9b05..c6c2c4ede 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1177,7 +1177,9 @@ stalled downloads are cancelled. Development and preview use the same handler. Overpass query validation, geometry simplification, disk caching and upstream transport now have separate modules; military-installation search reuses that transport. Regional briefing combines separate place, news and weather sources, -while weather effects uses only the weather source. Existing process-scoped +while weather effects uses only the weather source. +GDELT compact dates that do not name a real calendar instant keep the article +with an unknown publication time. Existing process-scoped caches, rate limits, stale fallbacks and route ordering are preserved. Local voice has separate HUD-summary, debug-log and Realtime-token handlers, diff --git a/src/data/regional-calendar-evidence.test.mjs b/src/data/regional-calendar-evidence.test.mjs new file mode 100644 index 000000000..a442a9feb --- /dev/null +++ b/src/data/regional-calendar-evidence.test.mjs @@ -0,0 +1,43 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { normalizeRegionalArticles } from './regionalModel.js'; +test('compact source dates must denote an actual calendar instant', () => { + for (const date of [ + '20260230T120000Z', + '20260431T120000Z', + '20261301T120000Z', + '20260716T240000Z', + '20260716T126000Z', + '20260716T120060Z', + ]) { + const [article] = normalizeRegionalArticles({ + articles: [ + { + title: 'Fixture', + url: 'https://fixture.example/news', + seendate: date, + }, + ], + }); + assert.equal(article.publishedAt, null, date); + assert.equal(article.title, 'Fixture'); + } + for (const [input, expected] of [ + ['20240229T004500Z', '2024-02-29T00:45:00Z'], + ['20260716T000000Z', '2026-07-16T00:00:00Z'], + ['2026-07-16T12:00:00Z', '2026-07-16T12:00:00.000Z'], + ]) { + assert.equal( + normalizeRegionalArticles({ + articles: [ + { + title: 'Fixture', + url: 'https://fixture.example/news', + seendate: input, + }, + ], + })[0].publishedAt, + expected, + ); + } +}); diff --git a/src/data/regionalModel.js b/src/data/regionalModel.js index 3a6d79f14..fca473994 100644 --- a/src/data/regionalModel.js +++ b/src/data/regionalModel.js @@ -67,11 +67,20 @@ export function normalizeRegionalArticles(payload, limit = MAX_ARTICLES) { seen.add(signature); const rawDate = cleanText(row?.seendate, 32); const compactDate = /^(\d{8})T(\d{6})Z$/.exec(rawDate); - const publishedAt = compactDate + const candidateDate = compactDate ? `${compactDate[1].slice(0, 4)}-${compactDate[1].slice(4, 6)}-${compactDate[1].slice(6, 8)}T${compactDate[2].slice(0, 2)}:${compactDate[2].slice(2, 4)}:${compactDate[2].slice(4, 6)}Z` - : Number.isNaN(Date.parse(rawDate)) - ? null - : new Date(rawDate).toISOString(); + : rawDate; + const parsedMs = Date.parse(candidateDate); + const validDate = + Number.isFinite(parsedMs) && + (!compactDate || + new Date(parsedMs).toISOString().slice(0, 19) === + candidateDate.slice(0, 19)); + const publishedAt = validDate + ? compactDate + ? candidateDate + : new Date(parsedMs).toISOString() + : null; articles.push({ title, url,