UNPKG

@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
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) }; };