Files
History/scripts/normalize-geojson.mjs
kamilkrzywdaandOpenCode e2936e85bb History: AI-driven alternate-history simulator
Pax Historia-style sim where the LLM is the world engine.

- React 19 + Vite 6 + Tailwind v4 + shadcn/ui frontend
- Express single-port backend (/api + /sse + built client + SPA fallback)
- SQLite (better-sqlite3) persistence, one row per game
- MapLibre choropleth map (OpenFreeMap dark + ISO_A3 regions)
- Defensive turn loop: strip fences, balanced-{} extract, parse, clamp 0-100,
  retry-once nudge, generic 'unclear' fallback; never crashes on bad output
- 5 seed presets (wwii-1939, coldwar-1955, modern-2025, no-wwii-1940, aegis-2400)
- LLM client over native fetch w/ injectable transport (mesh-bot pattern);
  LLM_STUB=1 deterministic transport for tests/e2e
- Unit (vitest, 19) + deterministic Playwright e2e
- Multi-stage node:22-slim Docker + Traefik (history.dev.kkhost.pl:4321)

Co-Authored-By: OpenCode <opencode@users.noreply.github.com>
2026-10-10 21:20:23 +00:00

65 lines
2.2 KiB
JavaScript

// One-shot helper (not a runtime dependency): normalize a Natural Earth admin-0
// GeoJSON into public/countries.geo.json with `id` = ISO_A3 and properties.name.
// Usage: node scripts/normalize-geojson.mjs <input.geo.json>
import { readFileSync, writeFileSync } from 'node:fs';
const input = process.argv[2];
if (!input) {
console.error('usage: node scripts/normalize-geojson.mjs <input.geo.json>');
process.exit(1);
}
const geo = JSON.parse(readFileSync(input, 'utf8'));
// ISO_A3 can be missing/empty for a few territories; fall back to a small table
// keyed by the Natural Earth `NAME` value, then to the (ISO_A2) feature id.
const NAME_TO_A3 = {
'South Sudan': 'SSD',
'Somaliland': 'SOL',
'N. Cyprus': 'CNX',
'Antarctica': 'ATA',
'Western Sahara': 'ESH',
'Falkland Is.': 'FLK',
'Falkland Islands': 'FLK',
'Heard & Island McDonald Is.': 'HMD',
'French Southern & Antarctic Lands': 'ATF',
'Saint Helena, Ascension & Tristan da Cunha': 'SHN',
'Bouvet Is.': 'BVT',
'Pitcairn Is.': 'PCN',
'S. Geo. de Las Malvinas (Falkland Is.)': 'FLK',
};
let withA3 = 0;
let fallback = 0;
const fallbackNames = [];
const out = {
type: 'FeatureCollection',
features: geo.features.map((f) => {
const p = f.properties ?? {};
let iso3 =
(p.ADM0_A3 ?? p.ISO_A3 ?? p.ISO_A3_EH ?? p.SOV_A3 ?? '').toString().trim().toUpperCase();
const name = (p.ADMIN ?? p.NAME ?? f.id ?? 'Unknown').toString().trim();
if (!iso3) {
iso3 = NAME_TO_A3[name] ?? (typeof f.id === 'string' && f.id.length === 2 ? f.id.toUpperCase() : 'ZZZ');
fallback++;
fallbackNames.push(name);
} else {
withA3++;
}
// Trim properties to the two fields the client actually uses (keeps the file small).
const feature = {
type: 'Feature',
id: iso3,
properties: { name },
geometry: f.geometry,
};
return feature;
}),
};
writeFileSync(new URL('../public/countries.geo.json', import.meta.url), JSON.stringify(out));
console.log(`features=${out.features.length} withA3=${withA3} fallback=${fallback}`);
if (fallbackNames.length) console.log('fallbacks:', [...new Set(fallbackNames)].join(', '));
const size = JSON.stringify(out).length;
console.log(`output bytes=${size}`);