spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1/**2 * sourceHistory: a merged, dated trail of where an entity's data came from — provenance observations,3 * detected events and page versions of the documents that reference the entity. Shared by facilities,4 * operators and projects.5 */6import type { EventDTO, ProvenanceDTO, SourceHistoryItem, SourceRef } from "@dci/core";7import { pg } from "./sql.js";8import { int, iso, reqStr, type Row } from "./rows.js";9import { asSourceKind, sourceRef } from "./dto.js";1011const ANNOUNCE_EVENTS = new Set(["project_announced", "phase_announced", "expansion_announced", "cloud_region_announced", "investment_announced", "power_agreement", "planning_filed"]);12const CHANGE_EVENTS = new Set(["status_changed", "capacity_changed", "planned_capacity_changed", "opening_date_changed", "operator_changed", "owner_changed", "project_status_changed", "planning_approved", "construction_started", "facility_opened", "acquisition", "closure", "cloud_region_launched", "cloud_region_updated"]);13const DETECT_EVENTS = new Set(["facility_discovered", "page_changed", "incident", "news"]);1415function fmtValue(v: unknown): string {16 if (v == null) return "—";17 if (typeof v === "object") { const s = JSON.stringify(v); return s.length > 80 ? s.slice(0, 77) + "…" : s; }18 const s = String(v);19 return s.length > 80 ? s.slice(0, 77) + "…" : s;20}2122export interface DocVersionRow { fetchedAt: string; url: string; sourceId: string; sourceName: string; sourceKind: string; significance: number; changes: number }2324/** Document versions for documents whose entity_refs contain (type,id). */25export async function documentVersionsFor(entityType: string, entityId: string, limit = 60): Promise<DocVersionRow[]> {26 const sql = pg();27 const refJson = JSON.stringify([{ type: entityType, id: entityId }]);28 const rows = await sql<Row[]>`29 select v.fetched_at, d.url, d.source_id, s.name as source_name, s.kind as source_kind, v.significance, jsonb_array_length(coalesce(v.detected_changes, '[]'::jsonb)) as changes30 from documents d31 join document_versions v on v.document_id = d.id32 left join sources s on s.id = d.source_id33 where d.entity_refs @> ${refJson}::jsonb34 order by v.fetched_at desc35 limit ${limit}`;36 return rows.map((r) => ({ fetchedAt: reqStr(iso(r.fetched_at)), url: reqStr(r.url), sourceId: reqStr(r.source_id), sourceName: reqStr(r.source_name, reqStr(r.source_id)), sourceKind: reqStr(r.source_kind, "secondary"), significance: int(r.significance), changes: int(r.changes) }));37}3839export function buildSourceHistory(provenance: ProvenanceDTO[], events: EventDTO[], versions: DocVersionRow[], max = 150): SourceHistoryItem[] {40 const items: SourceHistoryItem[] = [];41 const seen = new Set<string>();42 const push = (it: SourceHistoryItem) => {43 const k = `${it.date.slice(0, 10)}|${it.sourceId}|${it.kind}|${it.description}`;44 if (seen.has(k)) return;45 seen.add(k);46 items.push(it);47 };48 for (const p of provenance) {49 push({ date: p.firstObserved, sourceId: p.sourceId, sourceName: p.sourceName, sourceKind: p.sourceKind, url: p.url, kind: "observed", description: `${p.field} observed: ${fmtValue(p.value)}${p.isEstimate ? " (estimate)" : ""}` });50 if (p.lastObserved.slice(0, 10) !== p.firstObserved.slice(0, 10)) push({ date: p.lastObserved, sourceId: p.sourceId, sourceName: p.sourceName, sourceKind: p.sourceKind, url: p.url, kind: "verified", description: `${p.field} re-confirmed (${fmtValue(p.value)})` });51 }52 for (const e of events) {53 const kind: SourceHistoryItem["kind"] = ANNOUNCE_EVENTS.has(e.eventType) ? "announced" : CHANGE_EVENTS.has(e.eventType) ? "changed" : DETECT_EVENTS.has(e.eventType) ? "detected" : "updated";54 const desc = e.oldValue != null && e.newValue != null && kind === "changed" ? `${e.title} (${fmtValue(e.oldValue)} → ${fmtValue(e.newValue)})` : e.title;55 push({ date: e.detectedAt, sourceId: e.sourceId, sourceName: e.sourceName, sourceKind: e.sourceKind, url: e.url, kind, description: desc });56 }57 for (const v of versions) {58 push({ date: v.fetchedAt, sourceId: v.sourceId, sourceName: v.sourceName, sourceKind: asSourceKind(v.sourceKind), url: v.url, kind: "changed", description: v.changes > 0 ? `Source page changed — ${v.changes} field change${v.changes > 1 ? "s" : ""} detected (significance ${v.significance})` : `Source page changed (significance ${v.significance})` });59 }60 items.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));61 return items.slice(0, max);62}6364/** SourceRef[] for a set of source ids (envelope `sources`). */65export async function sourceRefsFor(ids: Iterable<string>): Promise<SourceRef[]> {66 const list = [...new Set([...ids].filter(Boolean))];67 if (!list.length) return [];68 const sql = pg();69 const rows = await sql<Row[]>`select id, name, domain, kind, url, license, attribution, redistribution, attribution_required from sources where id = any(${list}) order by name`;70 return rows.map(sourceRef);71}7273export function sourceIdsOf(...lists: Array<Array<{ sourceId: string }>>): Set<string> {74 const s = new Set<string>();75 for (const l of lists) for (const x of l) if (x.sourceId) s.add(x.sourceId);76 return s;77}7879/** Distinct sources behind a set of entities (current provenance rows), capped — for LIST envelopes. */80export async function sourcesForEntities(entityType: string, ids: string[], cap = 30): Promise<SourceRef[]> {81 const list = [...new Set(ids.filter(Boolean))];82 if (!list.length) return [];83 const sql = pg();84 const rows = await sql<Row[]>`85 select s.id, s.name, s.domain, s.kind, s.url, s.license, s.attribution, s.redistribution, s.attribution_required, count(*) as n86 from provenance p join sources s on s.id = p.source_id87 where p.entity_type = ${entityType} and p.entity_id = any(${list}) and p.is_current88 group by s.id order by n desc, s.name limit ${cap}`;89 return rows.map(sourceRef);90}9192/** Distinct sources of a set of source ids (events / map points), capped. */93export async function sourcesForIds(ids: Iterable<string>, cap = 30): Promise<SourceRef[]> {94 const list = [...new Set([...ids].filter(Boolean))].slice(0, 500);95 if (!list.length) return [];96 const sql = pg();97 const rows = await sql<Row[]>`select id, name, domain, kind, url, license, attribution, redistribution, attribution_required from sources where id = any(${list}) order by name limit ${cap}`;98 return rows.map(sourceRef);99}100