feat: add a Bhote Koshi flood event tool

This commit is contained in:
Sameh Khamis committed 2026-10-01 13:44:41 -07:00
1 parent a747b8a4e0
commit 0953ebb026
10 files changed
+243 -5

No files matched your search

+130
View File
@@ -0,0 +1,130 @@
/** The bundled Bhote Koshi 2026 outburst flood event pack. */
import { BHOTE_KOSHI_FLOOD_PATH } from '../../data/bhoteKoshiFloodPath.js';
import { defineTool, ToolError } from '../catalog.js';
import { distanceKm } from '../area.js';
const EVENT_ID = 'bhote-koshi-2026';
const ATTRIBUTION =
'Imagery: Vantor Open Data; reconstruction: GeoPera (CC BY-NC 4.0). ' +
'Linked witness posts and the GeoGeorgeShadrach geolocation map keep ' +
"their owners' terms.";
const round = (value, digits) =>
Number.isFinite(value) ? Number(value.toFixed(digits)) : null;
/** Length of a `[lon, lat]` line in kilometers. */
function pathLengthKm(path) {
let total = 0;
for (let index = 1; index < path.length; index += 1)
total += distanceKm(
{ lat: path[index - 1][1], lon: path[index - 1][0] },
{ lat: path[index][1], lon: path[index][0] },
);
return total;
}
const pathPoint = ([lon, lat]) => ({ lat, lon });
function evidenceRow(record) {
return {
sequence: record.sequence ?? null,
id: record.id,
title: record.title ?? null,
summary: record.summary ?? null,
phase: record.phase ?? null,
lat: round(record.lat, 5),
lon: round(record.lon, 5),
location_confidence: record.confidence?.location ?? null,
time_confidence: record.confidence?.time ?? null,
captured_at: record.timing?.capturedAt ?? null,
in_imagery: record.imageryCoverage === 'within',
source_url: record.media?.sourceUrl ?? null,
platform: record.media?.platform ?? null,
via: record.provenance?.via ?? null,
};
}
export const getBhoteKoshiFlood = defineTool({
name: 'get_bhote_koshi_flood',
title: 'Bhote Koshi flood',
description:
"The 26 August 2026 Bhote Koshi outburst flood in Nepal as God's Eye " +
'View maps it: the evidence trail of geolocated public witness posts ' +
'from the collapse to Devighat in story order, the mapped flood path, ' +
'and the before and after satellite imagery. A visualization, not an ' +
'official hazard model; capture times are unverified.',
inputSchema: {
type: 'object',
properties: {
phase: {
type: 'string',
minLength: 1,
maxLength: 40,
description: 'Only evidence in a phase, such as "downstream passage".',
},
},
additionalProperties: false,
},
requires: ['events'],
async run(args, { services, signal }) {
const event = await services.events.getEvent(EVENT_ID, { signal });
if (!Array.isArray(event?.evidenceSpine))
throw new ToolError('malformed', 'The event pack has no evidence');
const wanted = args.phase?.toLowerCase();
const evidence = event.evidenceSpine
.filter((record) => !wanted || record.phase?.toLowerCase() === wanted)
.sort((a, b) => a.sequence - b.sequence)
.map(evidenceRow);
const phases = [
...new Set(event.evidenceSpine.map((record) => record.phase)),
];
const lengthKm = round(pathLengthKm(BHOTE_KOSHI_FLOOD_PATH), 1);
const { imagery = {} } = event;
return {
summary:
`${event.title}, observed ${event.observedDate}: ${evidence.length} ` +
`evidence record${evidence.length === 1 ? '' : 's'}` +
(args.phase ? ` in the ${args.phase} phase` : '') +
` along a ${lengthKm} km mapped flood path.`,
data: {
id: event.id,
title: event.title,
observed_date: event.observedDate,
caveat: event.reconstruction?.caveat ?? null,
phases,
evidence,
witnesses: (event.witnessAnchors || []).map((anchor) => ({
title: anchor.title ?? null,
lat: round(anchor.lat, 6),
lon: round(anchor.lon, 6),
source_url: anchor.sourceUrl ?? null,
provenance: anchor.provenance ?? null,
})),
flood_path: {
start: pathPoint(BHOTE_KOSHI_FLOOD_PATH[0]),
end: pathPoint(BHOTE_KOSHI_FLOOD_PATH.at(-1)),
length_km: lengthKm,
description:
'Simplified river centerline from the debris-dammed lake to Trishuli Bazaar.',
},
imagery: {
box: imagery.rectangle ?? null,
before: imagery.before
? {
observed_at: imagery.before.observedAt,
source: imagery.before.source,
}
: null,
after: imagery.after
? {
observed_at: imagery.after.observedAt,
source: imagery.after.source,
}
: null,
},
attribution: ATTRIBUTION,
},
};
},
});
+74
View File
@@ -0,0 +1,74 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
import { composeCatalog, coreTools } from '../index.js';
import { createEventPackSource } from '../../sources/eventPacks.js';
const EVENT_URL = new URL(
'../../../public/events/bhote-koshi-2026/event.json',
import.meta.url,
);
const bundled = JSON.parse(await readFile(EVENT_URL, 'utf8'));
test('the event pack source reads each event once and checks the id', async () => {
const requested = [];
const source = createEventPackSource({
fetchImpl: async (url) => {
requested.push(url);
return Response.json(bundled);
},
});
const event = await source.getEvent('bhote-koshi-2026');
assert.deepEqual(event, bundled);
assert.equal(await source.getEvent('bhote-koshi-2026'), event);
assert.deepEqual(requested, ['/events/bhote-koshi-2026/event.json']);
await assert.rejects(source.getEvent('../secret'), /Invalid event id/);
const missing = createEventPackSource({
fetchImpl: async () => new Response(null, { status: 404 }),
});
await assert.rejects(missing.getEvent('other'), /unavailable \(404\)/);
});
test('the flood tool lists the bundled evidence in story order', async () => {
const catalog = composeCatalog({
tools: coreTools,
services: { events: { getEvent: async () => bundled } },
});
const all = await catalog.call('get_bhote_koshi_flood', {});
assert.match(
all.summary,
/^Bhote Koshi Outburst Flood, observed 2026-08-26: 16 evidence records along a \d+(\.\d)? km mapped flood path\.$/,
);
assert.deepEqual(
all.data.evidence.map((row) => row.sequence),
Array.from({ length: 16 }, (_, index) => index + 1),
);
assert.deepEqual(all.data.evidence[3], {
sequence: 4,
id: 'gyirong-border-gate',
title: 'Gyirong border gate',
summary: 'Public footage geolocated at the Nepal-China border crossing.',
phase: 'border arrival',
lat: 28.28083,
lon: 85.37802,
location_confidence: 'geolocated',
time_confidence: 'unverified',
captured_at: null,
in_imagery: true,
source_url: 'https://www.youtube.com/watch?v=qlE51Eu0thk',
platform: 'YouTube',
via: 'GeoGeorgeShadrach public geolocation map; independently consistent with the linked GeoConfirmed witness',
});
assert.deepEqual(all.data.flood_path.start, { lat: 28.33255, lon: 85.48476 });
assert.ok(all.data.flood_path.length_km > 50);
assert.equal(all.data.imagery.after.observed_at, '2026-08-27');
assert.match(all.data.caveat, /not an official hazard model/);
assert.match(all.data.attribution, /CC BY-NC 4\.0/);
const downstream = await catalog.call('get_bhote_koshi_flood', {
phase: 'Downstream passage',
});
assert.ok(
downstream.data.evidence.every((row) => row.phase === 'downstream passage'),
);
assert.match(downstream.summary, /in the Downstream passage phase/);
});