Generate public/region-adjacency.json: regionKey -> neighbourKeys, built from regions.geo.json (bbox proximity reject + @turf/boolean-intersects arbitration; symmetric, deduped, no self-refs). Deterministic + idempotent. This is the data backing the LLM's getNeighbours tool (Stage 2) which lets it move contiguous frontier regions instead of whole countries.
88 lines
3.8 KiB
TypeScript
88 lines
3.8 KiB
TypeScript
/**
|
|
* scripts/region-adjacency.test.ts — validation for the committed
|
|
* `public/region-adjacency.json` (built by `scripts/build-region-adjacency.mjs`).
|
|
*
|
|
* The manifest maps each region key to the list of region keys it **borders**
|
|
* (shares an edge/point with). It is the data behind the model's `getNeighbours`
|
|
* tool (v0.21, #16 P2), so it is a load-bearing data artifact — this test pins
|
|
* its contract.
|
|
*
|
|
* Deliberately dependency-free: it reads the JSON with plain `fs`/`JSON.parse`
|
|
* and never imports `@turf`/geojson types (importing one pulls `@types/geojson`
|
|
* into the tsc program and breaks `WorldMap.tsx`; see the v0.17 changelog note).
|
|
* Mirrors the style of `scripts/region-keys.test.ts`.
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const root = path.resolve(here, '..');
|
|
|
|
function readJson(rel: string): Record<string, unknown> {
|
|
return JSON.parse(readFileSync(path.join(root, rel), 'utf8'));
|
|
}
|
|
|
|
describe('region-adjacency.json', () => {
|
|
const adjacency = readJson('public/region-adjacency.json') as Record<string, string[]>;
|
|
const manifest = readJson('public/region-keys.json') as Record<string, string>;
|
|
const keys = new Set(Object.values(manifest));
|
|
|
|
it('has exactly one entry per region key (same key set as region-keys.json)', () => {
|
|
const manifestKeys = new Set(Object.values(manifest));
|
|
const adjacencyKeys = new Set(Object.keys(adjacency));
|
|
expect(adjacencyKeys).toEqual(manifestKeys);
|
|
expect(Object.keys(adjacency)).toHaveLength(keys.size);
|
|
expect(adjacencyKeys.size).toBe(1796);
|
|
});
|
|
|
|
it('every neighbour is itself a valid region key (referential integrity)', () => {
|
|
for (const [key, neighbours] of Object.entries(adjacency)) {
|
|
for (const n of neighbours) {
|
|
expect(keys.has(n), `neighbour ${n} of ${key} is not a valid region key`).toBe(true);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('no region lists itself as a neighbour', () => {
|
|
for (const [key, neighbours] of Object.entries(adjacency)) {
|
|
expect(neighbours.includes(key), `${key} lists itself`).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('adjacency is symmetric (A ∈ B ⟺ B ∈ A)', () => {
|
|
let checked = 0;
|
|
for (const [a, aList] of Object.entries(adjacency)) {
|
|
const aSet = new Set(aList);
|
|
for (const b of aList) {
|
|
const bList = adjacency[b];
|
|
expect(Array.isArray(bList), `${b} has no adjacency entry`).toBe(true);
|
|
// A lists B, so B must list A.
|
|
expect(bList!.includes(a), `${a} → ${b} but ${b} does not list ${a}`).toBe(true);
|
|
checked++;
|
|
}
|
|
// And the converse: every neighbour B of A must be a key (implied above) —
|
|
// also assert no *missing* reverse entry by checking the full set equality.
|
|
const bSet = new Set(aSet);
|
|
for (const b of aList) bSet.add(b);
|
|
// Symmetry means a's set equals the set of {b : a ∈ b}.
|
|
const reverse = Object.entries(adjacency)
|
|
.filter(([, list]) => list.includes(a))
|
|
.map(([k]) => k);
|
|
expect(new Set(reverse).size, `asymmetric degree for ${a}`).toBe(aList.length);
|
|
expect(new Set(aList).size).toBe(aSet.size); // no duplicate neighbours
|
|
}
|
|
expect(checked).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('spot-checks known real bordering pairs', () => {
|
|
// Brazil ↔ Peru (Acre borders Loreto) — a real, unambiguous land border.
|
|
expect(adjacency['acre-ekzw']?.includes('loreto-ipkx')).toBe(true);
|
|
expect(adjacency['loreto-ipkx']?.includes('acre-ekzw')).toBe(true);
|
|
// France ↔ Germany (Moselle borders Baden-Württemberg) — another real border.
|
|
expect(adjacency['moselle-rygy']?.includes('baden-wurttemberg-oyje')).toBe(true);
|
|
expect(adjacency['baden-wurttemberg-oyje']?.includes('moselle-rygy')).toBe(true);
|
|
});
|
|
});
|