| // Evidence β public badge derivation for the strategic storage registry. | |
| // | |
| // Same design as src/shared/pipeline-evidence.ts: we ship the evidence, not | |
| // our opinion. `publicBadge` is a deterministic function of the raw evidence | |
| // bundle, versioned so consumers can pin a reader to a classifier version. | |
| // | |
| // Shared between the server handler (server/worldmonitor/supply-chain/v1/ | |
| // list-storage-facilities.ts β attaches the derived badge to the RPC | |
| // response) AND the client panel's bootstrap path | |
| // (src/components/StorageFacilityMapPanel.ts β runs the same deriver | |
| // client-side on bootstrap-hydrated raw registries that don't carry | |
| // publicBadge). Identical output in both paths is the whole point β | |
| // first-paint badge must match post-RPC badge or the UI flickers. | |
| // | |
| // Duck-typed input: the shared interface here intentionally does NOT | |
| // import from src/generated so it stays dependency-free; server + client | |
| // both assign their proto-typed evidence bundles to it by structural | |
| // subtyping. | |
| // | |
| // See docs/methodology/storage.mdx Β§"How public badges move". | |
| export const STORAGE_BADGE_DERIVER_VERSION = 'storage-badge-deriver-v1'; | |
| export type StoragePublicBadge = 'operational' | 'reduced' | 'offline' | 'disputed'; | |
| export interface StorageEvidenceInput { | |
| physicalState?: string; // 'operational'|'reduced'|'offline'|'under_construction'|'unknown' | |
| physicalStateSource?: string; // 'operator'|'regulator'|'press'|'satellite'|'ais-relay' | |
| operatorStatement?: { text?: string; url?: string; date?: string } | null; | |
| commercialState?: string; // 'under_contract'|'expired'|'suspended'|'unknown' | |
| sanctionRefs?: ReadonlyArray<{ authority?: string; listId?: string; date?: string; url?: string }>; | |
| fillDisclosed?: boolean; | |
| fillSource?: string | null; | |
| lastEvidenceUpdate?: string; | |
| classifierVersion?: string; | |
| classifierConfidence?: number; | |
| } | |
| // Same 14-day staleness window as pipelines β registry fields refreshed | |
| // weekly by seed-storage-facilities.mjs, anything older means the cron is | |
| // broken rather than the asset's state has drifted. | |
| const EVIDENCE_STALENESS_DAYS = 14; | |
| /** | |
| * Derive the public badge for a single storage facility. | |
| * | |
| * Rules (first match wins): | |
| * 1. physical_state = "offline" AND (sanctionRefs.length > 0 OR commercialState β {expired, suspended}) | |
| * β "offline" (high-confidence offline with paperwork) | |
| * 2. physical_state = "offline" AND operatorStatement != null | |
| * β "offline" (operator-disclosed outage) | |
| * 3. physical_state = "offline" AND physicalStateSource β {press, ais-relay, satellite} | |
| * β "disputed" (external-signal offline without operator/sanction confirmation) | |
| * 4. physical_state = "reduced" | |
| * β "reduced" | |
| * 5. physical_state = "operational" | |
| * β "operational" | |
| * 6. physical_state = "under_construction" OR "unknown" OR evidence missing | |
| * β "disputed" | |
| * | |
| * Freshness guard: if lastEvidenceUpdate is older than EVIDENCE_STALENESS_DAYS, | |
| * a non-"operational" badge drops to "disputed". | |
| */ | |
| export function deriveStoragePublicBadge( | |
| evidence: StorageEvidenceInput | null | undefined, | |
| nowMs: number = Date.now(), | |
| ): StoragePublicBadge { | |
| if (!evidence) return 'disputed'; | |
| const stale = isStale(evidence.lastEvidenceUpdate, nowMs); | |
| const physical = evidence.physicalState; | |
| if (physical === 'offline') { | |
| const hasSanctionEvidence = (evidence.sanctionRefs?.length ?? 0) > 0; | |
| const hasCommercialHalt = | |
| evidence.commercialState === 'expired' || evidence.commercialState === 'suspended'; | |
| const hasOperatorStatement = evidence.operatorStatement != null && | |
| ((evidence.operatorStatement.text?.length ?? 0) > 0); | |
| const hasExternalSignal = ['press', 'ais-relay', 'satellite'].includes( | |
| evidence.physicalStateSource ?? '', | |
| ); | |
| if (hasSanctionEvidence || hasCommercialHalt) { | |
| return stale ? 'disputed' : 'offline'; | |
| } | |
| if (hasOperatorStatement) { | |
| return stale ? 'disputed' : 'offline'; | |
| } | |
| if (hasExternalSignal) return 'disputed'; | |
| return 'disputed'; | |
| } | |
| if (physical === 'reduced') { | |
| return stale ? 'disputed' : 'reduced'; | |
| } | |
| if (physical === 'operational') { | |
| return 'operational'; | |
| } | |
| // 'under_construction' | 'unknown' | malformed | |
| return 'disputed'; | |
| } | |
| function isStale(iso: string | undefined, nowMs: number): boolean { | |
| if (!iso) return true; | |
| const t = Date.parse(iso); | |
| if (Number.isNaN(t)) return true; | |
| const ageDays = (nowMs - t) / (1000 * 60 * 60 * 24); | |
| return ageDays > EVIDENCE_STALENESS_DAYS; | |
| } | |