spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1/** Row → DTO mappers (api-types contract). Inputs are raw rows produced by the fragments in sql.ts. */2import type { AiEvidence, AuthorityTier, ClaimDTO, ClaimScope, ClaimStatus, CloudRegionSummary, ConfidenceLevel, EventDTO, EventType, EntityType, FacilityStatus, FacilitySummary, FacilityType, GeoPrecision, GridConstraintDTO, IxpSummary, ProjectClass, ProjectSummary, ProvenanceDTO, QualityFlagDTO, ReviewStatus, SourceKind, SourceRef } from "@dci/core";3import { AI_EVIDENCE_LEVELS, CLAIM_SCOPES, CLAIM_STATUSES, CONFIDENCE_LEVELS, FACILITY_STATUSES, FACILITY_TYPES, GEO_PRECISIONS, PROJECT_CLASSES, SOURCE_KINDS, CAPACITY_PREDICATE_LABEL, INVESTMENT_PREDICATE_LABEL, significanceBand } from "@dci/core";4import { bool, int, iso, json, num, ref, reqIso, reqStr, str, type Row } from "./rows.js";56export function asStatus(v: unknown): FacilityStatus {7 const s = str(v);8 return s && (FACILITY_STATUSES as readonly string[]).includes(s) ? (s as FacilityStatus) : "unknown";9}10export function asType(v: unknown): FacilityType {11 const s = str(v);12 return s && (FACILITY_TYPES as readonly string[]).includes(s) ? (s as FacilityType) : "unknown";13}14export function asPrecision(v: unknown): GeoPrecision {15 const s = str(v);16 return s && (GEO_PRECISIONS as readonly string[]).includes(s) ? (s as GeoPrecision) : "unknown";17}18export function asConfidence(v: unknown): ConfidenceLevel {19 const s = str(v);20 return s && (CONFIDENCE_LEVELS as readonly string[]).includes(s) ? (s as ConfidenceLevel) : "unverified";21}22export function asSourceKind(v: unknown): SourceKind {23 const s = str(v);24 return s && (SOURCE_KINDS as readonly string[]).includes(s) ? (s as SourceKind) : "secondary";25}26export function asAiEvidence(v: unknown): AiEvidence {27 const s = str(v);28 return s && (AI_EVIDENCE_LEVELS as readonly string[]).includes(s) ? (s as AiEvidence) : "unknown";29}30export function asScope(v: unknown): ClaimScope | null {31 const s = str(v);32 return s && (CLAIM_SCOPES as readonly string[]).includes(s) ? (s as ClaimScope) : null;33}34export function asClaimStatus(v: unknown): ClaimStatus {35 const s = str(v);36 return s && (CLAIM_STATUSES as readonly string[]).includes(s) ? (s as ClaimStatus) : "current";37}38export function asProjectClass(v: unknown): ProjectClass | null {39 const s = str(v);40 return s && (PROJECT_CLASSES as readonly string[]).includes(s) ? (s as ProjectClass) : null;41}42export function asRecordScope(v: unknown): "building" | "facility" | "campus" {43 const s = str(v);44 return s === "building" || s === "campus" ? s : "facility";45}46export function asRedistribution(v: unknown): SourceRef["redistribution"] {47 const s = str(v);48 return s === "allowed" || s === "attribution" || s === "restricted" || s === "unknown" ? s : null;49}50export function asSeverity(v: unknown): "info" | "warn" | "critical" {51 const s = str(v);52 return s === "info" || s === "critical" ? s : "warn";53}5455/** Human label of a claim predicate (capacity / investment vocabularies, else the predicate itself). */56export function predicateLabel(p: string): string {57 return (CAPACITY_PREDICATE_LABEL as Record<string, string>)[p] ?? (INVESTMENT_PREDICATE_LABEL as Record<string, string>)[p] ?? p.replace(/_/g, " ");58}5960export function facilitySummary(r: Row): FacilitySummary {61 const out: FacilitySummary = {62 id: reqStr(r.id),63 slug: reqStr(r.slug),64 name: reqStr(r.name),65 operator: ref(r, "op"),66 city: str(r.city),67 regionName: str(r.region_name),68 countryIso2: str(r.country_iso2),69 countryName: str(r.country_name),70 metro: ref(r, "met"),71 lat: num(r.lat),72 lng: num(r.lng),73 geoPrecision: asPrecision(r.geo_precision),74 status: asStatus(r.status),75 facilityType: asType(r.facility_type),76 itCapacityMw: num(r.it_capacity_mw),77 totalPowerMw: num(r.total_power_mw),78 plannedPowerMw: num(r.planned_power_mw),79 mwIsEstimate: bool(r.mw_is_estimate),80 isAi: bool(r.is_ai),81 isHyperscale: bool(r.is_hyperscale),82 confidence: asConfidence(r.confidence),83 completeness: int(r.completeness),84 openedOn: str(r.opened_on),85 lastVerified: iso(r.last_verified),86 carriersCount: num(r.carriers_count),87 ixpCount: num(r.ixp_count),88 networksCount: num(r.networks_count),89 recordScope: asRecordScope(r.record_scope),90 parentFacility: ref(r, "parent"),91 aiEvidence: asAiEvidence(r.ai_evidence),92 capacityScope: asScope(r.capacity_scope),93 capacitySemantics: str(r.capacity_semantics),94 utilityCapacityMw: num(r.utility_capacity_mw),95 gridConnectionMw: num(r.grid_connection_mw),96 ultimateCampusMw: num(r.ultimate_campus_mw),97 sourceCount: int(r.source_count),98 };99 return out;100}101102export function projectSummary(r: Row): ProjectSummary {103 const ai = asAiEvidence(r.ai_evidence);104 return {105 id: reqStr(r.id),106 slug: reqStr(r.slug),107 name: reqStr(r.name),108 operator: ref(r, "op"),109 facility: ref(r, "fac"),110 city: str(r.city),111 regionName: str(r.region_name),112 countryIso2: str(r.country_iso2),113 metro: ref(r, "met"),114 lat: num(r.lat),115 lng: num(r.lng),116 status: asStatus(r.status),117 announcedOn: str(r.announced_on),118 expectedOpening: str(r.expected_opening),119 plannedMw: num(r.planned_mw),120 investmentUsd: num(r.investment_usd),121 acreage: num(r.acreage),122 phaseCount: num(r.phase_count),123 confidence: asConfidence(r.confidence),124 lastUpdate: reqIso(r.last_update ?? r.updated_at),125 geoPrecision: asPrecision(r.geo_precision),126 projectClass: asProjectClass(r.project_class),127 evidenceLevel: (() => { const s = str(r.evidence_level); return s === "strong" || s === "weak" || s === "none" ? s : null; })(),128 isAi: bool(r.is_ai) || ai === "confirmed" || ai === "likely",129 aiEvidence: ai,130 capacityScope: asScope(r.capacity_scope),131 capacitySemantics: str(r.capacity_semantics),132 investmentScope: asScope(r.investment_scope),133 investmentSemantics: str(r.investment_semantics),134 investmentCurrency: str(r.investment_currency),135 investmentOriginal: num(r.investment_original),136 developer: ref(r, "dev"),137 tenant: ref(r, "ten"),138 constructionStartedOn: str(r.construction_started_on),139 approvedOn: str(r.approved_on),140 permitFiledOn: str(r.permit_filed_on),141 openedOn: str(r.opened_on),142 };143}144145export function cloudRegionSummary(r: Row): CloudRegionSummary {146 const st = str(r.status);147 return {148 id: reqStr(r.id),149 slug: reqStr(r.slug),150 provider: ref(r, "pr") ?? { id: "", slug: "", name: "" },151 code: reqStr(r.code),152 name: reqStr(r.name),153 city: str(r.city),154 countryIso2: str(r.country_iso2),155 lat: num(r.lat),156 lng: num(r.lng),157 geoPrecision: asPrecision(r.geo_precision ?? "city"),158 availabilityZones: num(r.availability_zones),159 launchedOn: str(r.launched_on),160 status: st === "announced" || st === "restricted" || st === "retired" ? st : "operational",161 isSovereign: bool(r.is_sovereign),162 sourceUrl: str(r.source_url),163 };164}165166export function ixpSummary(r: Row): IxpSummary {167 const out: IxpSummary = {168 id: reqStr(r.id),169 slug: reqStr(r.slug),170 name: reqStr(r.name),171 nameLong: str(r.name_long),172 city: str(r.city),173 countryIso2: str(r.country_iso2),174 website: str(r.website),175 networkCount: num(r.network_count),176 facilityCount: int(r.facility_count),177 };178 if (r.met_id !== undefined) out.metro = ref(r, "met");179 if (r.lat !== undefined) out.lat = num(r.lat);180 if (r.lng !== undefined) out.lng = num(r.lng);181 return out;182}183184export function eventDto(r: Row, entity?: { slug: string; name: string } | null): EventDTO {185 const sig = int(r.significance, 30);186 const out: EventDTO = {187 id: reqStr(r.id),188 entityType: reqStr(r.entity_type, "facility") as EntityType,189 entityId: str(r.entity_id),190 entity: entity ?? null,191 eventType: reqStr(r.event_type, "news") as EventType,192 detectedAt: reqIso(r.detected_at),193 effectiveDate: str(r.effective_date),194 oldValue: r.old_value ?? null,195 newValue: r.new_value ?? null,196 sourceId: reqStr(r.source_id),197 sourceName: reqStr(r.source_name, reqStr(r.source_id)),198 sourceKind: asSourceKind(r.source_kind),199 url: reqStr(r.url),200 title: reqStr(r.title),201 summary: str(r.summary),202 significance: sig,203 confidence: asConfidence(r.confidence),204 reviewStatus: (str(r.review_status) ?? "auto") as ReviewStatus,205 countryIso2: str(r.country_iso2),206 operator: ref(r, "op"),207 metro: ref(r, "met"),208 project: ref(r, "prj"),209 isAi: bool(r.is_ai),210 significanceBand: significanceBand(sig),211 evidenceCount: Math.max(1, int(r.evidence_count, 1)),212 clusterId: str(r.cluster_id),213 documentId: str(r.document_id),214 };215 return out;216}217218export function provenanceDto(r: Row): ProvenanceDTO {219 return {220 field: reqStr(r.field),221 value: r.value ?? null,222 sourceId: reqStr(r.source_id),223 sourceName: reqStr(r.source_name, reqStr(r.source_id)),224 sourceKind: asSourceKind(r.source_kind),225 url: reqStr(r.url),226 firstObserved: reqIso(r.first_observed),227 lastObserved: reqIso(r.last_observed),228 retrievedAt: reqIso(r.retrieved_at),229 confidence: asConfidence(r.confidence),230 isEstimate: bool(r.is_estimate),231 method: str(r.method),232 isWinner: bool(r.is_winner),233 scope: str(r.scope),234 runId: str(r.run_id),235 documentId: str(r.document_id),236 };237}238239export function claimDto(r: Row, isWinner = false): ClaimDTO {240 const tier = str(r.authority_tier);241 return {242 id: reqStr(r.id),243 predicate: reqStr(r.predicate),244 label: predicateLabel(reqStr(r.predicate)),245 value: num(r.value),246 valueText: str(r.value_text),247 unit: str(r.unit),248 scope: asScope(r.scope) ?? "unknown",249 scopeReason: str(r.scope_reason),250 sourceId: reqStr(r.source_id),251 sourceName: reqStr(r.source_name, reqStr(r.source_id)),252 sourceKind: asSourceKind(r.source_kind),253 url: reqStr(r.url),254 documentId: str(r.document_id),255 publishedAt: str(r.published_at),256 retrievedAt: reqIso(r.retrieved_at),257 confidence: asConfidence(r.confidence),258 isEstimate: bool(r.is_estimate),259 authorityTier: (tier && /^[A-E]$/.test(tier) ? tier : "D") as AuthorityTier,260 evidenceText: str(r.evidence_text),261 parserName: str(r.parser_name),262 parserVersion: str(r.parser_version),263 status: asClaimStatus(r.status),264 rejectionReason: str(r.rejection_reason),265 isWinner,266 firstObserved: reqIso(r.first_observed),267 lastObserved: reqIso(r.last_observed),268 };269}270271export function gridConstraintDto(r: Row): GridConstraintDTO {272 return {273 id: reqStr(r.id),274 kind: reqStr(r.kind, "regulation"),275 title: reqStr(r.title),276 summary: str(r.summary),277 effectiveDate: str(r.effective_date),278 url: reqStr(r.url),279 sourceName: str(r.source_name),280 sourceKind: r.source_kind == null ? null : asSourceKind(r.source_kind),281 metro: ref(r, "met"),282 countryIso2: str(r.country_iso2),283 confidence: asConfidence(r.confidence),284 eventId: str(r.event_id),285 };286}287288const GRID_KIND_RULES: Array<[RegExp, string]> = [289 [/\b(moratorium|pause on|halt(ed|s)?|ban on)\b/i, "moratorium"],290 [/\b(load cap|cap on|capped)\b/i, "load_cap"],291 [/\b(large[- ]load (queue|tariff|request)|interconnection queue|queue)\b/i, "large_load_queue"],292 [/\b(delay(ed|s)?|wait(ing)? (time|list)|backlog|until 20\d\d)\b/i, "grid_delay"],293 [/\b(new substation|substation)\b/i, "new_substation"],294 [/\b(transmission (line|project|upgrade)|new transmission)\b/i, "new_transmission"],295 [/\b(restrict(ion|ed)|curtail|limit(ed|s)?)\b/i, "capacity_restriction"],296];297298/** A grid / utility / power event rendered as a GridConstraintDTO (kind inferred from the title, never invented beyond it). */299export function gridConstraintFromEvent(e: EventDTO): GridConstraintDTO {300 const text = `${e.title} ${e.summary ?? ""}`;301 const kind = e.eventType === "power_agreement" ? "power_agreement" : GRID_KIND_RULES.find(([re]) => re.test(text))?.[1] ?? (e.eventType === "grid_connection" ? "new_substation" : "regulation");302 return { id: e.id, kind, title: e.title, summary: e.summary, effectiveDate: e.effectiveDate, url: e.url, sourceName: e.sourceName, sourceKind: e.sourceKind, metro: e.metro ?? null, countryIso2: e.countryIso2, confidence: e.confidence, eventId: e.id };303}304305export function qualityFlagDto(r: Row, entity: { slug: string; name: string } | null = null): QualityFlagDTO {306 const st = str(r.status);307 return {308 id: reqStr(r.id),309 entityType: reqStr(r.entity_type),310 entityId: reqStr(r.entity_id),311 entity,312 claimId: str(r.claim_id),313 code: reqStr(r.code),314 severity: asSeverity(r.severity),315 field: str(r.field),316 message: reqStr(r.message),317 details: json<Record<string, unknown> | null>(r.details, null),318 priority: int(r.priority),319 status: st === "resolved" || st === "dismissed" ? st : "open",320 resolution: str(r.resolution),321 resolvedBy: str(r.resolved_by),322 resolvedAt: iso(r.resolved_at),323 runId: str(r.run_id),324 createdAt: reqIso(r.created_at),325 updatedAt: reqIso(r.updated_at),326 };327}328329export function sourceRef(r: Row): SourceRef {330 return { id: reqStr(r.id), name: reqStr(r.name), domain: reqStr(r.domain), kind: asSourceKind(r.kind), url: str(r.url), license: str(r.license), attribution: str(r.attribution), redistribution: asRedistribution(r.redistribution), attributionRequired: r.attribution_required == null ? null : bool(r.attribution_required) };331}332333export function statusBreakdown(rows: Row[], statusKey = "status", countKey = "n"): Partial<Record<FacilityStatus, number>> {334 const out: Partial<Record<FacilityStatus, number>> = {};335 for (const r of rows) { const s = asStatus(r[statusKey]); out[s] = (out[s] ?? 0) + int(r[countKey]); }336 return out;337}338339export function typeBreakdown(rows: Row[], typeKey = "facility_type", countKey = "n"): Partial<Record<FacilityType, number>> {340 const out: Partial<Record<FacilityType, number>> = {};341 for (const r of rows) { const t = asType(r[typeKey]); out[t] = (out[t] ?? 0) + int(r[countKey]); }342 return out;343}344345/** Cumulative growth series from (year, facilities, mw) rows. Years with data only. */346export function growthSeries(rows: Row[]): Array<{ year: number; facilities: number; knownMw: number | null }> {347 let facilities = 0;348 let mw = 0;349 let sawMw = false;350 const out: Array<{ year: number; facilities: number; knownMw: number | null }> = [];351 for (const r of rows.sort((a, b) => int(a.year) - int(b.year))) {352 const y = int(r.year);353 if (!y) continue;354 facilities += int(r.n);355 const m = num(r.mw);356 if (m != null) { mw += m; sawMw = true; }357 out.push({ year: y, facilities, knownMw: sawMw ? Math.round(mw * 100) / 100 : null });358 }359 return out;360}361362export function share(part: number, total: number, digits = 3): number {363 if (!total) return 0;364 const f = 10 ** digits;365 return Math.round((part / total) * f) / f;366}367368export function round2(v: number | null | undefined): number | null {369 if (v == null || !Number.isFinite(v)) return null;370 return Math.round(v * 100) / 100;371}372