GenerAI / worldmonitor /shared /analysis-adapter-guards.ts
amogaddy's picture
Integra World Monitor (AGPL-3.0, self-hosted) nello Space: pagina, menu, e arricchimento notizie per la AI (part 6)
fa9c65f verified
Raw
History Blame Contribute Delete
2.83 kB
/**
* Shared scalar guards for the analysis adapters (#5696).
*
* Both adapter modules map untrusted seeded-cache JSON onto the analysis
* cores' input types, so both need the same defensive primitives. They were
* written independently and grew two names for each concept
* (`finiteNumber`/`num`, `nonEmptyString`/`str`); one definition removes the
* drift risk, since a divergence in null-handling here silently empties a
* result set rather than raising.
*
* Semantics are deliberately permissive on INPUT (a seeder that stores a
* number as a string still parses) and strict on VALIDATION (a coordinate
* must be real and in range).
*
* Dependency-free: importable from Vite client code, Vercel Edge bundles,
* server handlers, and tsx tests alike.
*/
/** Narrow to a plain object; arrays and null are rejected. */
export function asRecord(value: unknown): Record<string, unknown> | null {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
/** Always yields an iterable — a non-array becomes empty rather than throwing. */
export function asArray(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
/** Finite number, accepting a numeric string; anything else is null. */
export function finiteNumber(value: unknown): number | null {
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
if (typeof value === 'string' && value.trim() !== '') {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
/** Trimmed string, or '' for anything non-string or whitespace-only. */
export function nonEmptyString(value: unknown): string {
return typeof value === 'string' && value.trim() !== '' ? value.trim() : '';
}
/**
* A coordinate pair usable as a real position: both components present, in
* range, and not null island — (0, 0) is overwhelmingly a missing-data
* artifact in these feeds rather than a location in the Gulf of Guinea.
*
* Note the exposure adapters deliberately do NOT apply this: they enrich
* whatever the caller asks about, and rejecting a point there would silently
* drop an event rather than decline to place it on a grid.
*/
export function usableCoord(lat: number | null, lon: number | null): lat is number {
return (
lat !== null && lon !== null &&
lat >= -90 && lat <= 90 &&
lon >= -180 && lon <= 180 &&
!(lat === 0 && lon === 0)
);
}
/** Read a nested `{ location: { latitude, longitude } }` pair. */
export function nestedLocation(record: Record<string, unknown>): { lat: number | null; lon: number | null } {
const location = asRecord(record.location);
return {
lat: finiteNumber(location?.latitude),
lon: finiteNumber(location?.longitude),
};
}