@fusedashlabs/ui9000-workspace
Version:
UI9000 workspace MCP. One tool, show_workspace, draws a chart from your table. Set TYPESAFE_API_KEY to let Jev choose the chart.
276 lines (275 loc) • 13.2 kB
JavaScript
import { GEOJSON_KEYS } from "./geojsonKeys.js";
import { createFeaturesIndex } from "./createFeaturesIndex.js";
import { getRegionIdOptimized } from "./getRegionId.js";
import { inferMapTypeFromKeyNames } from "./inferMapTypeFromKeyNames.js";
import { normalizeDataValue } from "./normalizeDataValue.js";
// Headless, framework-free validation that a map widget's geospatial values
// resolve against the boundary set the renderer would actually join them to.
// It reuses the EXACT matching the renderer uses (createFeaturesIndex +
// getRegionIdOptimized + normalizeDataValue) and resolves the map type the same
// way getMapType() does, so "validates" can never drift from "renders". No LLM,
// no network — index lookups against geometry-free region catalogs. Meant to gate
// generated configs before a data-link is signed.
const MAP_TYPES = ["city", "county", "state", "province", "region", "country"];
// Catalogs we can cross-check a stray value against to suggest the right map type.
const SUGGEST_ORDER = ["county", "province", "state", "country", "city"];
const INDEX_CACHE = new WeakMap();
/** Catalog arrays are stable module imports. Build each type's index once. */
function cachedFeaturesIndex(features, mapType) {
let byType = INDEX_CACHE.get(features);
if (!byType) {
byType = new Map();
INDEX_CACHE.set(features, byType);
}
const hit = byType.get(mapType);
if (hit)
return hit;
const index = createFeaturesIndex(features, mapType);
byType.set(mapType, index);
return index;
}
const COUNTRY_EQUIV = [
["United States of America", "United States", "USA", "US"],
["United Kingdom", "UK", "Great Britain"],
];
const normCountry = (c) => normalizeDataValue(String(c ?? ""), "country");
const sameCountry = (a, b) => {
if (normCountry(a) === normCountry(b))
return true;
return COUNTRY_EQUIV.some((group) => {
const n = group.map(normCountry);
return n.includes(normCountry(a)) && n.includes(normCountry(b));
});
};
const featProp = (f, mapType, key) => String(f.properties?.[GEOJSON_KEYS[mapType]?.[key]] ?? "");
// The value to hand back as the suggestion: the canonical "Country,Name". Prefer
// the English name (name_en) when the primary name is non-Latin — Egypt's
// governorates carry name="Al Qahirah" but name_en="Cairo", and the data labels
// are English — so the suggestion reads back what the user actually typed.
const canonicalValue = (f, mapType) => {
if (!f)
return "";
const country = featProp(f, mapType, "country");
const nameEn = String(f.properties?.name_en ?? "");
const name = nameEn || featProp(f, mapType, "name");
return country && name ? `${country},${name}` : name;
};
const dataCountryOf = (value) => value.includes(",") ? value.split(",").slice(0, -1).join(",").trim() : "";
/**
* Resolve the map type exactly like the renderer's getMapType(): an explicit type
* wins; otherwise the dataset field subtype (when the caller knows it), then the
* self-describing geospatial key names. Generated data-links carry neither an
* explicit type nor dataset metadata, so they fall to key-name inference — which
* is precisely how a "state__created" key holding county values ends up resolving
* to "state" and failing to render.
*/
export const resolveMapType = (geospatialData, opts = {}) => {
if (opts.mapType)
return opts.mapType;
if (opts.fieldSubtype && MAP_TYPES.includes(opts.fieldSubtype)) {
return opts.fieldSubtype;
}
return inferMapTypeFromKeyNames(geospatialData) ?? "";
};
/**
* Validate values against pre-loaded boundary features for one resolved map type.
* Pure: pass the same features (geometry-free is fine) the renderer would load.
*/
export const validateRegions = (values, mapType, features) => {
const index = cachedFeaturesIndex(features, mapType);
const matched = [];
const warnings = [];
const unmatched = [];
for (const value of values) {
const regionId = getRegionIdOptimized(value, mapType, index);
if (!regionId) {
unmatched.push({ value, didYouMean: null });
continue;
}
matched.push({ value, regionId });
if (value.includes(",")) {
const f = index.byId.get(regionId);
const actualCountry = f ? featProp(f, mapType, "country") : "";
const dataCountry = dataCountryOf(value);
const regionName = f ? featProp(f, mapType, "name") : "";
if (dataCountry && actualCountry && !sameCountry(dataCountry, actualCountry)) {
warnings.push({
value,
type: "countryMismatch",
dataCountry,
actualCountry,
didYouMean: `${actualCountry},${regionName}`,
});
}
}
}
return {
applicable: true,
mapType,
geospatialKey: "",
mapsCorrectly: unmatched.length === 0,
coverage: values.length ? matched.length / values.length : 1,
matched,
warnings,
unmatched,
};
};
function isMarkerLayer(layer) {
return layer.visualisationType === "markers" || layer.geospatialData?.[0] === "point";
}
/** A region layer the catalog never checked. Coverage 0 so the gate refuses it. */
function uncheckedLayer(mapType, geospatialKey, values) {
const shown = values.length > 0 ? values : ["(empty)"];
return {
applicable: true,
mapType: mapType || "unknown",
geospatialKey,
mapsCorrectly: false,
coverage: 0,
matched: [],
warnings: [],
unmatched: shown.map((value) => ({ value, didYouMean: null })),
};
}
const unwrap = (config) => {
const c = config;
return c?.config ?? c ?? {};
};
export const isMapConfig = (config) => {
const cfg = unwrap(config);
return cfg.chartType === "mapChart" || cfg.type === "mapChart";
};
const layerInputs = (layer, opts) => {
const geospatialKey = layer.geospatialData?.[0] ?? layer.representation?.geospatial_data?.[0] ?? "";
const mapType = resolveMapType([geospatialKey, ...(layer.geospatialData ?? [])], {
mapType: opts.mapType ?? layer.mapType,
fieldSubtype: opts.fieldSubtype ?? layer.fieldSubtype,
});
const values = (layer.data ?? [])
.map((row) => String(row[geospatialKey] ?? "").trim())
.filter((v) => v.length > 0);
return { mapType, geospatialKey, values };
};
// Cross-check a stray value against the OTHER catalogs to name the map type it
// actually belongs to (e.g. a value that fails "state" but is a known county).
//
// Match with the renderer's own machinery (createFeaturesIndex +
// getRegionIdOptimized) on the FULL value, not a bare name-only lookup. That
// buys two things the old buildNameIndex couldn't: the country prefix already
// in the value disambiguates same-named places across countries, and name_en /
// alias indexing finds units whose primary name is non-Latin. Two passes: first
// honour a match whose country agrees with the data's prefix ("Egypt,Alexandria"
// is an Egyptian governorate, not the US "Alexandria" a bare match would hit);
// only if none agrees fall back to any match, so a value carrying a simply-wrong
// country prefix still gets corrected ("United States,Covasna" -> Romanian county).
const suggestAcrossTypes = async (value, resolvedType, loadCatalog, cache) => {
const dataCountry = dataCountryOf(value);
const getIndex = async (candidate) => {
let index = cache.get(candidate);
if (!index) {
try {
index = createFeaturesIndex(await loadCatalog(candidate), candidate);
}
catch {
index = createFeaturesIndex([], candidate);
}
cache.set(candidate, index);
}
return index;
};
let firstAnyMatch = null;
for (const candidate of SUGGEST_ORDER) {
if (candidate === resolvedType)
continue;
const index = await getIndex(candidate);
const regionId = getRegionIdOptimized(value, candidate, index);
if (!regionId)
continue;
const feature = index.byId.get(regionId);
const suggestion = { mapType: candidate, value: canonicalValue(feature, candidate) || value };
const featCountry = feature ? featProp(feature, candidate, "country") : "";
const countryAgrees = !dataCountry || !featCountry || sameCountry(dataCountry, featCountry);
if (countryAgrees)
return suggestion;
if (!firstAnyMatch)
firstAnyMatch = suggestion;
}
return firstAnyMatch;
};
const buildLlmMessage = (layers) => {
const unmatched = layers.flatMap((l) => l.unmatched);
const warnings = layers.flatMap((l) => l.warnings);
if (unmatched.length) {
// When a strict majority of the stray values point at one other map type,
// the diagnosis is a wrong map type, not unknown regions — name it precisely
// and list the exceptions (a different admin level, or a value that matches
// nothing) separately, so a few strays don't mask the dominant signal.
const resolved = new Set(layers.filter((l) => l.unmatched.length).map((l) => l.mapType));
const tally = new Map();
for (const u of unmatched) {
if (u.didYouMean)
tally.set(u.didYouMean.mapType, (tally.get(u.didYouMean.mapType) ?? 0) + 1);
}
const [dominantType, dominantCount] = [...tally.entries()].sort((a, b) => b[1] - a[1])[0] ?? ["", 0];
if (dominantType && resolved.size === 1 && dominantCount > unmatched.length / 2) {
const have = [...resolved][0];
const ex = unmatched.find((u) => u.didYouMean?.mapType === dominantType);
const exceptions = unmatched.filter((u) => u.didYouMean?.mapType !== dominantType);
const tail = exceptions.length
? ` ${exceptions.length} other value(s) are not ${dominantType}s (${exceptions
.slice(0, 10)
.map((u) => `"${u.value}"${u.didYouMean ? ` is a ${u.didYouMean.mapType}` : " matches no boundary"}`)
.join("; ")}); fix or remove them.`
: "";
return `Map will not render: the data resolves to map type "${have}", but ${dominantCount} of ${unmatched.length} value(s) are ${dominantType}s (e.g. "${ex.value}" → use map type "${dominantType}", value "${ex.didYouMean.value}"). Set the map type to "${dominantType}" or fix the geospatial values.${tail}`;
}
const list = unmatched
.slice(0, 10)
.map((u) => `"${u.value}"${u.didYouMean ? ` → ${u.didYouMean.mapType} "${u.didYouMean.value}"` : ""}`)
.join("; ");
return `Map will not fully render: ${unmatched.length} value(s) do not match the boundaries (${list}). Fix or remove them; do not invent locations.`;
}
if (warnings.length) {
const list = warnings
.slice(0, 10)
.map((w) => `"${w.value}" is actually in ${w.actualCountry} (use "${w.didYouMean}")`)
.join("; ");
return `Map renders, but ${warnings.length} value(s) carry the wrong country prefix: ${list}. Set the correct country to avoid ambiguous matches.`;
}
return "All regions match the map boundaries.";
};
/**
* Validate a whole chart config (data-link payload or raw config). `loadCatalog`
* supplies geometry-free boundary features for a map type — the caller owns I/O
* (vendored JSON import, fs read…), keeping this pure. Pass `opts.mapType` or
* `opts.fieldSubtype` when the caller already knows them (the MCP does at
* generation time) for a fully faithful resolution.
*/
export const validateMapData = async (config, loadCatalog, opts = {}) => {
if (!isMapConfig(config)) {
return { applicable: false, mapsCorrectly: true, layers: [], llmMessage: "Not a map chart; nothing to validate." };
}
const rawLayers = unwrap(config).layers ?? [];
const suggestionCache = new Map();
const layers = [];
for (const layer of rawLayers) {
// Marker layers are points, not region joins. A choropleth that cannot be
// checked (no map type, no key, no values) must not look like a pass.
if (isMarkerLayer(layer))
continue;
const { mapType, geospatialKey, values } = layerInputs(layer, opts);
if (!mapType || !geospatialKey || values.length === 0) {
layers.push(uncheckedLayer(mapType, geospatialKey, values));
continue;
}
const features = await loadCatalog(mapType);
const result = validateRegions(values, mapType, features);
result.geospatialKey = geospatialKey;
for (const u of result.unmatched) {
u.didYouMean = await suggestAcrossTypes(u.value, mapType, loadCatalog, suggestionCache);
}
layers.push(result);
}
const mapsCorrectly = layers.every((l) => l.mapsCorrectly);
return { applicable: true, mapsCorrectly, layers, llmMessage: buildLlmMessage(layers) };
};