SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
4 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
14.7 KB · 372 lines typescript
Raw Blame History
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