/** Row → DTO mappers (api-types contract). Inputs are raw rows produced by the fragments in sql.ts. */ import 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"; import { 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"; import { bool, int, iso, json, num, ref, reqIso, reqStr, str, type Row } from "./rows.js"; export function asStatus(v: unknown): FacilityStatus { const s = str(v); return s && (FACILITY_STATUSES as readonly string[]).includes(s) ? (s as FacilityStatus) : "unknown"; } export function asType(v: unknown): FacilityType { const s = str(v); return s && (FACILITY_TYPES as readonly string[]).includes(s) ? (s as FacilityType) : "unknown"; } export function asPrecision(v: unknown): GeoPrecision { const s = str(v); return s && (GEO_PRECISIONS as readonly string[]).includes(s) ? (s as GeoPrecision) : "unknown"; } export function asConfidence(v: unknown): ConfidenceLevel { const s = str(v); return s && (CONFIDENCE_LEVELS as readonly string[]).includes(s) ? (s as ConfidenceLevel) : "unverified"; } export function asSourceKind(v: unknown): SourceKind { const s = str(v); return s && (SOURCE_KINDS as readonly string[]).includes(s) ? (s as SourceKind) : "secondary"; } export function asAiEvidence(v: unknown): AiEvidence { const s = str(v); return s && (AI_EVIDENCE_LEVELS as readonly string[]).includes(s) ? (s as AiEvidence) : "unknown"; } export function asScope(v: unknown): ClaimScope | null { const s = str(v); return s && (CLAIM_SCOPES as readonly string[]).includes(s) ? (s as ClaimScope) : null; } export function asClaimStatus(v: unknown): ClaimStatus { const s = str(v); return s && (CLAIM_STATUSES as readonly string[]).includes(s) ? (s as ClaimStatus) : "current"; } export function asProjectClass(v: unknown): ProjectClass | null { const s = str(v); return s && (PROJECT_CLASSES as readonly string[]).includes(s) ? (s as ProjectClass) : null; } export function asRecordScope(v: unknown): "building" | "facility" | "campus" { const s = str(v); return s === "building" || s === "campus" ? s : "facility"; } export function asRedistribution(v: unknown): SourceRef["redistribution"] { const s = str(v); return s === "allowed" || s === "attribution" || s === "restricted" || s === "unknown" ? s : null; } export function asSeverity(v: unknown): "info" | "warn" | "critical" { const s = str(v); return s === "info" || s === "critical" ? s : "warn"; } /** Human label of a claim predicate (capacity / investment vocabularies, else the predicate itself). */ export function predicateLabel(p: string): string { return (CAPACITY_PREDICATE_LABEL as Record)[p] ?? (INVESTMENT_PREDICATE_LABEL as Record)[p] ?? p.replace(/_/g, " "); } export function facilitySummary(r: Row): FacilitySummary { const out: FacilitySummary = { id: reqStr(r.id), slug: reqStr(r.slug), name: reqStr(r.name), operator: ref(r, "op"), city: str(r.city), regionName: str(r.region_name), countryIso2: str(r.country_iso2), countryName: str(r.country_name), metro: ref(r, "met"), lat: num(r.lat), lng: num(r.lng), geoPrecision: asPrecision(r.geo_precision), status: asStatus(r.status), facilityType: asType(r.facility_type), itCapacityMw: num(r.it_capacity_mw), totalPowerMw: num(r.total_power_mw), plannedPowerMw: num(r.planned_power_mw), mwIsEstimate: bool(r.mw_is_estimate), isAi: bool(r.is_ai), isHyperscale: bool(r.is_hyperscale), confidence: asConfidence(r.confidence), completeness: int(r.completeness), openedOn: str(r.opened_on), lastVerified: iso(r.last_verified), carriersCount: num(r.carriers_count), ixpCount: num(r.ixp_count), networksCount: num(r.networks_count), recordScope: asRecordScope(r.record_scope), parentFacility: ref(r, "parent"), aiEvidence: asAiEvidence(r.ai_evidence), capacityScope: asScope(r.capacity_scope), capacitySemantics: str(r.capacity_semantics), utilityCapacityMw: num(r.utility_capacity_mw), gridConnectionMw: num(r.grid_connection_mw), ultimateCampusMw: num(r.ultimate_campus_mw), sourceCount: int(r.source_count), }; return out; } export function projectSummary(r: Row): ProjectSummary { const ai = asAiEvidence(r.ai_evidence); return { id: reqStr(r.id), slug: reqStr(r.slug), name: reqStr(r.name), operator: ref(r, "op"), facility: ref(r, "fac"), city: str(r.city), regionName: str(r.region_name), countryIso2: str(r.country_iso2), metro: ref(r, "met"), lat: num(r.lat), lng: num(r.lng), status: asStatus(r.status), announcedOn: str(r.announced_on), expectedOpening: str(r.expected_opening), plannedMw: num(r.planned_mw), investmentUsd: num(r.investment_usd), acreage: num(r.acreage), phaseCount: num(r.phase_count), confidence: asConfidence(r.confidence), lastUpdate: reqIso(r.last_update ?? r.updated_at), geoPrecision: asPrecision(r.geo_precision), projectClass: asProjectClass(r.project_class), evidenceLevel: (() => { const s = str(r.evidence_level); return s === "strong" || s === "weak" || s === "none" ? s : null; })(), isAi: bool(r.is_ai) || ai === "confirmed" || ai === "likely", aiEvidence: ai, capacityScope: asScope(r.capacity_scope), capacitySemantics: str(r.capacity_semantics), investmentScope: asScope(r.investment_scope), investmentSemantics: str(r.investment_semantics), investmentCurrency: str(r.investment_currency), investmentOriginal: num(r.investment_original), developer: ref(r, "dev"), tenant: ref(r, "ten"), constructionStartedOn: str(r.construction_started_on), approvedOn: str(r.approved_on), permitFiledOn: str(r.permit_filed_on), openedOn: str(r.opened_on), }; } export function cloudRegionSummary(r: Row): CloudRegionSummary { const st = str(r.status); return { id: reqStr(r.id), slug: reqStr(r.slug), provider: ref(r, "pr") ?? { id: "", slug: "", name: "" }, code: reqStr(r.code), name: reqStr(r.name), city: str(r.city), countryIso2: str(r.country_iso2), lat: num(r.lat), lng: num(r.lng), geoPrecision: asPrecision(r.geo_precision ?? "city"), availabilityZones: num(r.availability_zones), launchedOn: str(r.launched_on), status: st === "announced" || st === "restricted" || st === "retired" ? st : "operational", isSovereign: bool(r.is_sovereign), sourceUrl: str(r.source_url), }; } export function ixpSummary(r: Row): IxpSummary { const out: IxpSummary = { id: reqStr(r.id), slug: reqStr(r.slug), name: reqStr(r.name), nameLong: str(r.name_long), city: str(r.city), countryIso2: str(r.country_iso2), website: str(r.website), networkCount: num(r.network_count), facilityCount: int(r.facility_count), }; if (r.met_id !== undefined) out.metro = ref(r, "met"); if (r.lat !== undefined) out.lat = num(r.lat); if (r.lng !== undefined) out.lng = num(r.lng); return out; } export function eventDto(r: Row, entity?: { slug: string; name: string } | null): EventDTO { const sig = int(r.significance, 30); const out: EventDTO = { id: reqStr(r.id), entityType: reqStr(r.entity_type, "facility") as EntityType, entityId: str(r.entity_id), entity: entity ?? null, eventType: reqStr(r.event_type, "news") as EventType, detectedAt: reqIso(r.detected_at), effectiveDate: str(r.effective_date), oldValue: r.old_value ?? null, newValue: r.new_value ?? null, sourceId: reqStr(r.source_id), sourceName: reqStr(r.source_name, reqStr(r.source_id)), sourceKind: asSourceKind(r.source_kind), url: reqStr(r.url), title: reqStr(r.title), summary: str(r.summary), significance: sig, confidence: asConfidence(r.confidence), reviewStatus: (str(r.review_status) ?? "auto") as ReviewStatus, countryIso2: str(r.country_iso2), operator: ref(r, "op"), metro: ref(r, "met"), project: ref(r, "prj"), isAi: bool(r.is_ai), significanceBand: significanceBand(sig), evidenceCount: Math.max(1, int(r.evidence_count, 1)), clusterId: str(r.cluster_id), documentId: str(r.document_id), }; return out; } export function provenanceDto(r: Row): ProvenanceDTO { return { field: reqStr(r.field), value: r.value ?? null, sourceId: reqStr(r.source_id), sourceName: reqStr(r.source_name, reqStr(r.source_id)), sourceKind: asSourceKind(r.source_kind), url: reqStr(r.url), firstObserved: reqIso(r.first_observed), lastObserved: reqIso(r.last_observed), retrievedAt: reqIso(r.retrieved_at), confidence: asConfidence(r.confidence), isEstimate: bool(r.is_estimate), method: str(r.method), isWinner: bool(r.is_winner), scope: str(r.scope), runId: str(r.run_id), documentId: str(r.document_id), }; } export function claimDto(r: Row, isWinner = false): ClaimDTO { const tier = str(r.authority_tier); return { id: reqStr(r.id), predicate: reqStr(r.predicate), label: predicateLabel(reqStr(r.predicate)), value: num(r.value), valueText: str(r.value_text), unit: str(r.unit), scope: asScope(r.scope) ?? "unknown", scopeReason: str(r.scope_reason), sourceId: reqStr(r.source_id), sourceName: reqStr(r.source_name, reqStr(r.source_id)), sourceKind: asSourceKind(r.source_kind), url: reqStr(r.url), documentId: str(r.document_id), publishedAt: str(r.published_at), retrievedAt: reqIso(r.retrieved_at), confidence: asConfidence(r.confidence), isEstimate: bool(r.is_estimate), authorityTier: (tier && /^[A-E]$/.test(tier) ? tier : "D") as AuthorityTier, evidenceText: str(r.evidence_text), parserName: str(r.parser_name), parserVersion: str(r.parser_version), status: asClaimStatus(r.status), rejectionReason: str(r.rejection_reason), isWinner, firstObserved: reqIso(r.first_observed), lastObserved: reqIso(r.last_observed), }; } export function gridConstraintDto(r: Row): GridConstraintDTO { return { id: reqStr(r.id), kind: reqStr(r.kind, "regulation"), title: reqStr(r.title), summary: str(r.summary), effectiveDate: str(r.effective_date), url: reqStr(r.url), sourceName: str(r.source_name), sourceKind: r.source_kind == null ? null : asSourceKind(r.source_kind), metro: ref(r, "met"), countryIso2: str(r.country_iso2), confidence: asConfidence(r.confidence), eventId: str(r.event_id), }; } const GRID_KIND_RULES: Array<[RegExp, string]> = [ [/\b(moratorium|pause on|halt(ed|s)?|ban on)\b/i, "moratorium"], [/\b(load cap|cap on|capped)\b/i, "load_cap"], [/\b(large[- ]load (queue|tariff|request)|interconnection queue|queue)\b/i, "large_load_queue"], [/\b(delay(ed|s)?|wait(ing)? (time|list)|backlog|until 20\d\d)\b/i, "grid_delay"], [/\b(new substation|substation)\b/i, "new_substation"], [/\b(transmission (line|project|upgrade)|new transmission)\b/i, "new_transmission"], [/\b(restrict(ion|ed)|curtail|limit(ed|s)?)\b/i, "capacity_restriction"], ]; /** A grid / utility / power event rendered as a GridConstraintDTO (kind inferred from the title, never invented beyond it). */ export function gridConstraintFromEvent(e: EventDTO): GridConstraintDTO { const text = `${e.title} ${e.summary ?? ""}`; const kind = e.eventType === "power_agreement" ? "power_agreement" : GRID_KIND_RULES.find(([re]) => re.test(text))?.[1] ?? (e.eventType === "grid_connection" ? "new_substation" : "regulation"); 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 }; } export function qualityFlagDto(r: Row, entity: { slug: string; name: string } | null = null): QualityFlagDTO { const st = str(r.status); return { id: reqStr(r.id), entityType: reqStr(r.entity_type), entityId: reqStr(r.entity_id), entity, claimId: str(r.claim_id), code: reqStr(r.code), severity: asSeverity(r.severity), field: str(r.field), message: reqStr(r.message), details: json | null>(r.details, null), priority: int(r.priority), status: st === "resolved" || st === "dismissed" ? st : "open", resolution: str(r.resolution), resolvedBy: str(r.resolved_by), resolvedAt: iso(r.resolved_at), runId: str(r.run_id), createdAt: reqIso(r.created_at), updatedAt: reqIso(r.updated_at), }; } export function sourceRef(r: Row): SourceRef { 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) }; } export function statusBreakdown(rows: Row[], statusKey = "status", countKey = "n"): Partial> { const out: Partial> = {}; for (const r of rows) { const s = asStatus(r[statusKey]); out[s] = (out[s] ?? 0) + int(r[countKey]); } return out; } export function typeBreakdown(rows: Row[], typeKey = "facility_type", countKey = "n"): Partial> { const out: Partial> = {}; for (const r of rows) { const t = asType(r[typeKey]); out[t] = (out[t] ?? 0) + int(r[countKey]); } return out; } /** Cumulative growth series from (year, facilities, mw) rows. Years with data only. */ export function growthSeries(rows: Row[]): Array<{ year: number; facilities: number; knownMw: number | null }> { let facilities = 0; let mw = 0; let sawMw = false; const out: Array<{ year: number; facilities: number; knownMw: number | null }> = []; for (const r of rows.sort((a, b) => int(a.year) - int(b.year))) { const y = int(r.year); if (!y) continue; facilities += int(r.n); const m = num(r.mw); if (m != null) { mw += m; sawMw = true; } out.push({ year: y, facilities, knownMw: sawMw ? Math.round(mw * 100) / 100 : null }); } return out; } export function share(part: number, total: number, digits = 3): number { if (!total) return 0; const f = 10 ** digits; return Math.round((part / total) * f) / f; } export function round2(v: number | null | undefined): number | null { if (v == null || !Number.isFinite(v)) return null; return Math.round(v * 100) / 100; }