diff --git a/.env.example b/.env.example index 4ccec424..58bc57b8 100644 --- a/.env.example +++ b/.env.example @@ -25,3 +25,9 @@ NEXT_PUBLIC_COOKIE_CONSENT=on # GraphQL indexer URL for org.plresearch.* records # Default: https://api.hi.gainforest.app/graphql # INDEXER_URL=http://localhost:4000/graphql + +# Metaculus API token — enables live community-forecast readings on the /impact +# dashboard (e.g. the whole-brain-emulation question). Without it the question +# is still linked, just without a live number. Create a token at +# https://www.metaculus.com/accounts/settings/ +# METACULUS_API_TOKEN= diff --git a/docs/screenshots/impact-modal-restructured.png b/docs/screenshots/impact-modal-restructured.png new file mode 100644 index 00000000..6671c3f7 Binary files /dev/null and b/docs/screenshots/impact-modal-restructured.png differ diff --git a/docs/screenshots/impact-signals-grid.png b/docs/screenshots/impact-signals-grid.png new file mode 100644 index 00000000..510e6419 Binary files /dev/null and b/docs/screenshots/impact-signals-grid.png differ diff --git a/docs/screenshots/impact-signals-livebar.png b/docs/screenshots/impact-signals-livebar.png new file mode 100644 index 00000000..c938d6af Binary files /dev/null and b/docs/screenshots/impact-signals-livebar.png differ diff --git a/docs/screenshots/impact-signals-modal.png b/docs/screenshots/impact-signals-modal.png new file mode 100644 index 00000000..1f841009 Binary files /dev/null and b/docs/screenshots/impact-signals-modal.png differ diff --git a/docs/screenshots/impact-signals-platforms.png b/docs/screenshots/impact-signals-platforms.png new file mode 100644 index 00000000..0b9e5eb3 Binary files /dev/null and b/docs/screenshots/impact-signals-platforms.png differ diff --git a/src/app/globals.css b/src/app/globals.css index 1548fae9..99c69c6f 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -95,6 +95,15 @@ body { background-color: var(--color-white); } +/* Impact dashboard two-axis accents — theme-aware so the “field” ink and the + progress track stay legible in dark mode (ImpactDashboard / MeasuringQuestions). */ +:root { + --impact-field: #131316; /* the field axis (neutral ink) */ + --impact-field-ink: #ffffff; /* text on a field-accent fill */ + --impact-field-track: #e5e7eb; /* empty progress segment */ + --impact-hand: #1982f4; /* our-hand axis (blue) */ +} + /* ===== Dark mode ===== Rather than annotating every component, we remap the semantic color tokens. Because Tailwind v4 utilities compile to var(--color-*), @@ -120,6 +129,13 @@ html.dark { --color-gray: #9A9EAB; --color-blue: #3B96F6; /* slightly brighter accents on dark */ + + /* Two-axis accents: light ink reads on dark surfaces; the field fill flips + to light so white badge text is swapped for dark ink. */ + --impact-field: #e9e9ee; + --impact-field-ink: #15171c; + --impact-field-track: #3a3f4b; + --impact-hand: #3b96f6; } /* Keep the footer's intentional dark design in both themes by diff --git a/src/app/impact/page.tsx b/src/app/impact/page.tsx new file mode 100644 index 00000000..ec39b16d --- /dev/null +++ b/src/app/impact/page.tsx @@ -0,0 +1,124 @@ +import type { Metadata } from 'next' +import Breadcrumb from '@/components/Breadcrumb' +import ImpactDashboard, { type LiveMetric, type LiveOutputs } from '@/components/ImpactDashboard' +import MeasuringQuestions from '@/components/MeasuringQuestions' +import { fetchSimocracyStats } from '@/lib/simocracy' +import { fetchGainforestStats } from '@/lib/gainforest' +import { fetchGlowStats } from '@/lib/glow' +import { resolveAllSignals } from '@/lib/market-signals' +import { FIELD_COLOR, HAND_COLOR } from '@/lib/inflection-points' + +// Pull live output metrics for the Economies & Governance inflection points from +// the same sources as the FA2 live dashboard. These are Q3 OUTPUTS (the work +// PL-backed teams have produced) — never Q2 progress toward a threshold. +export const revalidate = 60 + +async function fetchLiveOutputs(): Promise { + const compact = (n: number) => + new Intl.NumberFormat('en-US', { notation: 'compact', maximumFractionDigits: 1 }).format(n) + const out: LiveOutputs = {} + + const [sim, gf, glow] = await Promise.allSettled([ + fetchSimocracyStats(), + fetchGainforestStats(), + fetchGlowStats(), + ]) + + // A binding decision at scale — Simocracy (a PL-supported deliberation mechanism). + if (sim.status === 'fulfilled' && !sim.value.degraded) { + const t = sim.value.totals + const metrics: LiveMetric[] = [ + { n: t.uniqueHumans, label: 'participants' }, + { n: t.totalSims, label: 'simulations' }, + { n: t.totalGatherings, label: 'gatherings' }, + ] + .filter((m) => m.n > 0) + .map((m) => ({ value: compact(m.n), label: m.label })) + if (metrics.length) out['A binding decision at scale'] = metrics + } + + // Capital that pays on verified outcomes — GainForest + Glow (PL-backed MRV teams). + const verified: LiveMetric[] = [] + if (gf.status === 'fulfilled' && !gf.value.degraded) { + const g = gf.value + if (g.observations > 0) verified.push({ value: compact(g.observations), label: 'species observations' }) + if (g.certifiedOrgs > 0) verified.push({ value: compact(g.certifiedOrgs), label: 'certified orgs' }) + } + if (glow.status === 'fulfilled' && !glow.value.degraded) { + const gl = glow.value + if (gl.activeFarms > 0) verified.push({ value: compact(gl.activeFarms), label: 'active solar farms' }) + if (gl.carbon > 0) verified.push({ value: compact(gl.carbon), label: 'tCO₂ / wk' }) + } + if (verified.length) out['Capital that pays on verified outcomes'] = verified + + return out +} + +export const metadata: Metadata = { + title: 'Impact', + description: + 'How we measure whether PL R&D’s work matters: the inflection points we have pre-registered across every focus area, and how we will know if they happen.', +} + +export default async function ImpactPage() { + const [liveOutputs, marketSignals] = await Promise.all([ + fetchLiveOutputs(), + resolveAllSignals(), + ]) + return ( +
+ {/* Hero */} +
+ +
+

+ Our impact +

+

+ Across our four focus areas we have named a small set of{' '} + inflection points — specific, + observable, falsifiable shifts we believe would be catalytic, and that have not yet + happened. +

+ + Our methodology + + + + +
+
+ + {/* Inflection points — grey full-bleed section to set it apart from the rest of the site */} +
+
+

+ Inflection points we are tracking +

+

+ Select a focus area. Each card shows the defined threshold (did it happen), the cascade we + expect if it matters, and the PL contribution we would trace. +

+ +
+
+ + {/* Our methodology */} +
+

Our methodology

+

+ For every inflection point we ask three questions — different jobs that should not be + collapsed into a single score. We hold two axes apart:{' '} + our hand (the work + we control) and{' '} + the field (the + change that follows, with or without us). +

+ +
+
+ ) +} diff --git a/src/app/insights/page.tsx b/src/app/insights/page.tsx index 8ee97b26..a1aa6d40 100644 --- a/src/app/insights/page.tsx +++ b/src/app/insights/page.tsx @@ -28,7 +28,12 @@ const FALLBACK_CARDS = { 'card-blog': { title: 'Blog' }, } -export default async function InsightsPage() { +export default async function InsightsPage({ + searchParams, +}: { + searchParams: Promise<{ area?: string }> +}) { + const { area: initialArea } = await searchParams const page = await fetchPage('insights') const hero = getSection(page, 'hero') const heroTitle = hero?.title || FALLBACK_HERO_TITLE @@ -223,7 +228,7 @@ export default async function InsightsPage() { )} - + diff --git a/src/components/ImpactDashboard.tsx b/src/components/ImpactDashboard.tsx new file mode 100644 index 00000000..3c03bff0 --- /dev/null +++ b/src/components/ImpactDashboard.tsx @@ -0,0 +1,699 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import { + ROLE_META, + PL_ROLE_ORDER, + FIELD_COLOR, + FIELD_TRACK, + HAND_COLOR, + LIVE_COLOR, + FIELD_STAGES, + FOCUS_AREAS, + INFLECTION_POINTS, + TEAM_LINKS, + HOW_TO_READ, + FIELD_INK, + stageIndexForStatus, + type FocusAreaKey, + type InflectionPoint, + type PLRole, +} from '@/lib/inflection-points' +import { AreaIcon, type AreaIconType } from '@/components/AreaIcons' +import type { MarketSignal } from '@/lib/market-signals' + +type Filter = FocusAreaKey + +/** Live output metrics for a point, keyed by the point's title. Fetched server-side. */ +export type LiveMetric = { value: string; label: string } +export type LiveOutputs = Record +/** Prediction-market crowd signal per point, keyed by title. Fetched server-side. */ +export type MarketSignals = Record + +const PLATFORM_LABEL: Record<'polymarket' | 'kalshi' | 'metaculus', string> = { + polymarket: 'Polymarket', + kalshi: 'Kalshi', + metaculus: 'Metaculus', +} + +const FA_ICON: Record = { + 'digital-human-rights': 'shield', + 'economies-governance': 'hexagon', + 'ai-robotics': 'neural', + neurotech: 'brain', +} + +export default function ImpactDashboard({ + liveOutputs = {}, + marketSignals = {}, +}: { + liveOutputs?: LiveOutputs + marketSignals?: MarketSignals +}) { + const [filter, setFilter] = useState('digital-human-rights') + const [active, setActive] = useState(null) + const [howToOpen, setHowToOpen] = useState(false) + + const visible = useMemo( + () => INFLECTION_POINTS.filter((p) => p.area === filter), + [filter], + ) + + return ( + <> + {/* “How to read this” — a persistent affordance above the grid (never per-card). */} +
+ +
+
+ {/* Vertical tabs */} +
+ {FOCUS_AREAS.map((fa) => ( + p.area === fa.key).length} + forthcoming={fa.forthcoming} + icon={FA_ICON[fa.key]} + active={filter === fa.key} + onClick={() => setFilter(fa.key)} + /> + ))} +
+ + {/* Content */} +
+ {visible.length > 0 ? ( +
+ {visible.map((p) => ( + setActive(p)} + /> + ))} +
+ ) : ( + + )} +
+ + {active && ( + setActive(null)} + /> + )} +
+ {howToOpen && setHowToOpen(false)} />} + + ) +} + +// Turn any team/venture name present in TEAM_LINKS into a link, wherever it +// appears in contribution copy. Names are matched longest-first so multi-word +// names (e.g. "Funding the Commons") win over any shorter substring. +const TEAM_LINK_PATTERN = new RegExp( + '(' + + Object.keys(TEAM_LINKS) + .sort((a, b) => b.length - a.length) + .map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + .join('|') + + ')', + 'g', +) + +function Linkify({ text }: { text: string }) { + const parts = text.split(TEAM_LINK_PATTERN) + return ( + <> + {parts.map((part, i) => + TEAM_LINKS[part] ? ( + + {part} + + ) : ( + {part} + ), + )} + + ) +} + +function Tab({ + label, + count, + forthcoming = false, + active, + icon, + onClick, +}: { + label: string + count: number + forthcoming?: boolean + active: boolean + icon?: AreaIconType + onClick: () => void +}) { + return ( + + ) +} + +/** Field-progress lifecycle meter — the “field” axis (teal). */ +function FieldMeter({ + status, + compact = false, +}: { + status: InflectionPoint['status'] + compact?: boolean +}) { + const reached = stageIndexForStatus(status) + return ( +
+
+ {FIELD_STAGES.map((_, i) => ( + + ))} +
+ {!compact && ( +
+ {FIELD_STAGES.map((s, i) => ( + + {s} + + ))} +
+ )} +
+ ) +} + +/** One pill per PL role, in canonical order. */ +function RoleChips({ roles, eyebrow = true }: { roles: PLRole[]; eyebrow?: boolean }) { + const ordered = PL_ROLE_ORDER.filter((r) => roles.includes(r)) + return ( + + {eyebrow && ( + PL role + )} + {ordered.map((r) => ( + + + {ROLE_META[r].label} + + + {ROLE_META[r].label} — {ROLE_META[r].description} + + + ))} + + ) +} + +function InflectionCard({ + point, + metrics, + signal, + onOpen, +}: { + point: InflectionPoint + metrics?: LiveMetric[] + signal?: MarketSignal + onOpen: () => void +}) { + const fa = FOCUS_AREAS.find((f) => f.key === point.area)! + const stageLabel = FIELD_STAGES[stageIndexForStatus(point.status)] + // Whether this point has any live signal worth opening the card for. The + // preview only hints that something exists — the detail lives in the modal. + const hasLiveSignal = !!( + point.liveEvidence?.length || + (metrics && metrics.length) || + (signal && signal.match !== 'gap') + ) + + return ( + + ) +} + +/** Compact USD, e.g. $15k, $1.2M. */ +function formatUSD(n: number): string { + if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(n >= 10_000_000 ? 0 : 1)}M` + if (n >= 1_000) return `$${Math.round(n / 1_000)}k` + return `$${Math.round(n)}` +} + +/** Crowd-forecast row for the modal's horizontal Live-signal band (with market link). */ +function CrowdForecast({ signal, divider = false }: { signal: MarketSignal; divider?: boolean }) { + const pct = signal.prob != null ? Math.round(signal.prob * 100) : null + return ( +
+
+ + Crowd forecast + + {signal.platform && ( + + {PLATFORM_LABEL[signal.platform]} + {signal.viaFallback ? ' (fallback)' : ''} + + )} + {signal.volume != null && ( + + {formatUSD(signal.volume)} at stake + + )} + + {signal.readout ?? (pct != null ? `${pct}%` : '—')} + +
+ {signal.url && ( + + {signal.question} + + )} +

+ {signal.note} Independent estimate on the field axis — not PL contribution, not a settled outcome. +

+
+ ) +} + +function InflectionModal({ + point, + metrics, + signal, + onClose, +}: { + point: InflectionPoint + metrics?: LiveMetric[] + signal?: MarketSignal + onClose: () => void +}) { + const fa = FOCUS_AREAS.find((f) => f.key === point.area)! + + // Close on Escape, and lock body scroll while open. + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose() + } + document.addEventListener('keydown', onKey) + const prev = document.body.style.overflow + document.body.style.overflow = 'hidden' + return () => { + document.removeEventListener('keydown', onKey) + document.body.style.overflow = prev + } + }, [onClose]) + + return ( +
+
e.stopPropagation()} + > + {/* Close */} + + +
+ {/* Header */} +
+ + + + {fa.label} + · + {point.opportunitySpace} +
+ {/* THE FIELD — title, outcome, impact, then progress (grey card) */} +
+
+ The field +
+

+ {point.title} +

+
+
+
Outcome
+

{point.signal}

+
+
+
Impact
+

{point.cascade}

+
+
+
+
+ Progress against Inflection Point +
+ +
+
+ + {/* OUR HAND — grey card: PL intervention pills (definition on hover) + real examples */} +
+
+ Our hand +
+
How PL is making a difference
+ +
+
In practice
+

+

+
+
+ + {/* LIVE SIGNAL — grey card: title, PL-backed live outputs (Q3), crowd forecast, latest insights */} +
+
+ + + + + Live signal +
+ {(point.liveEvidence?.length || (metrics && metrics.length) || (signal && signal.match !== 'gap')) && ( +
+ {point.liveEvidence?.map((ev, i) => { + const external = /^https?:\/\//.test(ev.href) + return ( + 0 ? 'mt-3 border-t border-gray-100 pt-3' : ''}`} + > +
+ {ev.label} + + + +
+ {i === 0 && metrics && metrics.length > 0 && ( +
+ {metrics.map((m) => ( + + {m.value} + {m.label} + + ))} +
+ )} +

{ev.note}

+
+ ) + })} + + {!point.liveEvidence?.length && metrics && metrics.length > 0 && ( +
+ {metrics.map((m) => ( + + {m.value} + {m.label} + + ))} +
+ )} + + {signal && signal.match !== 'gap' && ( + + )} +
+ )} + + {signal && signal.match === 'gap' && ( +
+
+ Crowd forecast +
+

{signal.note}

+
+ )} + + + See the latest {fa.label} insights + + + + +
+
+
+
+ ) +} + +function EmptyState({ filter }: { filter: Filter }) { + const fa = FOCUS_AREAS.find((f) => f.key === filter) + return ( +
+ + Coming Soon + +

+ Inflection points for {fa?.label ?? 'this focus area'} are being defined. +

+

+ Not zero — just not yet. This focus area is still finalizing its plan of attack, and its + inflection points will appear here once they are set. +

+
+ ) +} + +/** “How to read this” — the one-line contract plus the two-axis legend. */ +function HowToReadModal({ onClose }: { onClose: () => void }) { + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose() + } + document.addEventListener('keydown', onKey) + const prev = document.body.style.overflow + document.body.style.overflow = 'hidden' + return () => { + document.removeEventListener('keydown', onKey) + document.body.style.overflow = prev + } + }, [onClose]) + + return ( +
+
e.stopPropagation()}> + + +
How to read this
+

{HOW_TO_READ}

+ +
+ + The change in the world — did it happen (outcome) and did it matter (impact). Moves with or without us. + + + Our contribution — the PL instruments on the critical path. The axis we control with our partners. + +
+ + + + + + +

+ Live signal — real-world evidence for and against, refreshed from the field. Never a settled outcome. +

+
+
+
+
+ ) +} + +function LegendRow({ + color, + ink, + label, + children, +}: { + color: string + ink: string + label: string + children: React.ReactNode +}) { + return ( +
+ +

+ {label} — {children} +

+
+ ) +} diff --git a/src/components/InsightsExplorer.tsx b/src/components/InsightsExplorer.tsx index 656eaaf8..3c308067 100644 --- a/src/components/InsightsExplorer.tsx +++ b/src/components/InsightsExplorer.tsx @@ -49,12 +49,16 @@ const DISPLAY_LIMIT = 12 export default function InsightsExplorer({ sections, areas, + initialArea, }: { sections: InsightSection[] areas: AreaDef[] + initialArea?: string }) { const [type, setType] = useState(ALL) - const [area, setArea] = useState(ALL) + const [area, setArea] = useState( + initialArea && areas.some((a) => a.slug === initialArea) ? initialArea : ALL, + ) const matchArea = (tile: InsightTile, a: string) => a === ALL || tile.areas.includes(a) diff --git a/src/components/MeasuringQuestions.tsx b/src/components/MeasuringQuestions.tsx new file mode 100644 index 00000000..eb83d721 --- /dev/null +++ b/src/components/MeasuringQuestions.tsx @@ -0,0 +1,171 @@ +'use client' + +import { useEffect, useState } from 'react' +import { FIELD_COLOR, FIELD_INK, HAND_COLOR, ROLE_META, PL_ROLE_ORDER } from '@/lib/inflection-points' + +function Badge({ letter, color, ink = '#ffffff' }: { letter: string; color: string; ink?: string }) { + return ( + + {letter} + + ) +} + +export default function MeasuringQuestions() { + const [rolesOpen, setRolesOpen] = useState(false) + + return ( + <> +
+ {/* A — our hand (whole card opens the roles modal) */} + + + {/* B — the field */} + + + {/* C — the field */} + +
+ + {rolesOpen && setRolesOpen(false)} />} + + ) +} + +function FieldCard({ + letter, + title, + eyebrow, + body, + footRest, +}: { + letter: string + title: string + eyebrow: string + body: string + footRest: string +}) { + return ( +
+
+ +

{title}

+
+
+ {eyebrow} +
+

{body}

+

+ The field. {footRest} +

+
+ ) +} + +function RolesModal({ onClose }: { onClose: () => void }) { + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose() + } + document.addEventListener('keydown', onKey) + const prev = document.body.style.overflow + document.body.style.overflow = 'hidden' + return () => { + document.removeEventListener('keydown', onKey) + document.body.style.overflow = prev + } + }, [onClose]) + + return ( +
+
e.stopPropagation()}> + + +
+ Question A · Our hand +
+

+ Six roles, matched to the bottleneck each one releases +

+

+ Every card tags the roles PL played. Match the instrument to the bottleneck — a point can + be reached with little PL involvement, which is still a win for the field, and we record + our role honestly. +

+ +
+ {PL_ROLE_ORDER.map((r) => ( +
+
+ + {ROLE_META[r].label} +
+

{ROLE_META[r].description}

+
+ ))} +
+ +

+ Each role answers a different bottleneck. On a card, the tags under “our hand” + tell you which ones PL brought to that specific inflection point. +

+
+
+ ) +} diff --git a/src/components/PLRadar.tsx b/src/components/PLRadar.tsx index adf8cf02..1354045a 100644 --- a/src/components/PLRadar.tsx +++ b/src/components/PLRadar.tsx @@ -34,6 +34,9 @@ export type RadarItem = { const STORAGE_KEY = 'plrd-radar-collapsed' +/** How long each story slide dwells before auto-advancing (ms). */ +const SLIDE_MS = 7000 + const AREA_GRADIENT: Record = { 'digital-human-rights': { from: '#0b1f4d', via: '#1e3a8a', to: '#3966FE', dot: '#3966FE' }, 'economies-governance': { from: '#0a3b2e', via: '#0f6b4c', to: '#12bfdf', dot: '#12bfdf' }, @@ -82,6 +85,8 @@ function CoverImage({ src, alt }: { src: string; alt: string }) { export default function PLRadar({ edition, items }: { edition: string; items: RadarItem[] }) { const [open, setOpen] = useState(true) const [i, setI] = useState(0) + const [progress, setProgress] = useState(0) + const [paused, setPaused] = useState(false) const sectionRef = useRef(null) const touchX = useRef(null) @@ -100,10 +105,40 @@ export default function PLRadar({ edition, items }: { edition: string; items: Ra const N = items.length + 1 // + "all caught up" slide const go = useCallback((n: number) => setI((prev) => Math.max(0, Math.min(N - 1, n))), [N]) + const atEnd = i === items.length + + // Story-style auto-advance: fill the current slide's bar over SLIDE_MS, then + // step to the next slide. Pauses while collapsed, held/hovered, or when the + // tab is hidden, and halts on the final "all caught up" slide. + useEffect(() => { + setProgress(0) + if (!open || paused || atEnd) return + let raf = 0 + let start: number | null = null + const tick = (now: number) => { + if (start === null) start = now + const p = Math.min(1, (now - start) / SLIDE_MS) + setProgress(p) + if (p >= 1) go(i + 1) + else raf = requestAnimationFrame(tick) + } + raf = requestAnimationFrame(tick) + return () => cancelAnimationFrame(raf) + }, [i, open, paused, atEnd, go]) + + // Pause auto-advance when the tab loses focus (browsers throttle rAF anyway). + useEffect(() => { + const onVis = () => setPaused(document.hidden) + document.addEventListener('visibilitychange', onVis) + return () => document.removeEventListener('visibilitychange', onVis) + }, []) + const onTouchStart = (e: React.TouchEvent) => { touchX.current = e.touches[0].clientX + setPaused(true) // press-and-hold pauses, like stories } const onTouchEnd = (e: React.TouchEvent) => { + setPaused(false) if (touchX.current === null) return const dx = e.changedTouches[0].clientX - touchX.current if (dx < -40) go(i + 1) @@ -130,8 +165,6 @@ export default function PLRadar({ edition, items }: { edition: string; items: Ra window.open(intent, '_blank', 'noopener,noreferrer') } - const atEnd = i === items.length - return (
{/* progress segments — own band above the content so they never overlap it */}
- {Array.from({ length: N }).map((_, idx) => ( -
-
-
- ))} + {Array.from({ length: N }).map((_, idx) => { + // Past slides are full; the current slide fills as it plays; future slides are empty. + const fill = idx < i ? 1 : idx === i ? (atEnd ? 1 : progress) : 0 + return ( +
+
+
+ ) + })}
setPaused(true)} + onMouseLeave={() => setPaused(false)} > {/* content slides */} {items.map((item, idx) => { diff --git a/src/lib/inflection-points.ts b/src/lib/inflection-points.ts new file mode 100644 index 00000000..0fcbb353 --- /dev/null +++ b/src/lib/inflection-points.ts @@ -0,0 +1,449 @@ +// Cross-focus-area inflection points and how we measure them. +// +// Each focus area names a small set of inflection points: specific, observable, +// falsifiable shifts we believe would be catalytic and that have NOT yet happened +// as of 2026. They are hypotheses, not descriptions of the present. We judge our +// work against three questions, mapped to the fields on every point: +// +// Q1 (cascade) Did it matter? The second-order effects it should unlock. +// Q2 (signal) Did it happen? A pre-registered threshold — yes/no + date. +// Q3 (contribution) Did our work make it happen? PL's inputs -> activities -> outputs on the +// critical path (the planned-work side of the +// logic model), traced honestly. +// +// IMPORTANT — these are distinct jobs and are NOT collapsed into one score. A point +// can be reached (Q2) and matter (Q1) with low PL contribution (Q3); that is still a +// win for the field. So we track field progress (Q1 & Q2) on one axis and PL's +// contribution (Q3) on a separate, independent axis. +// +// Source: "Inflection points across PL R&D, and how we will measure them." + +export type InflectionStatus = 'watching' | 'early-signal' | 'tripped' + +/** PL's pre-registered role(s) on the critical path — claims to be evidenced, not a credit score. */ +export type PLRole = 'infrastructure' | 'legibility' | 'connection' | 'capital' | 'translation' | 'permission' + +/** Canonical display order for role pills, so cards read consistently. */ +export const PL_ROLE_ORDER: PLRole[] = ['infrastructure', 'legibility', 'connection', 'capital', 'translation', 'permission'] + +// Two-axis color system used across the impact dashboard: +// FIELD = the change in the world (outcomes + impact) — moves with or without us (black) +// HAND = our planned work / the PL toolkit — the axis we control with partners (PL blue) +// These resolve to theme-aware CSS variables (see globals.css) so the black +// “field” ink flips to a light tone in dark mode instead of vanishing. +export const FIELD_COLOR = 'var(--impact-field)' +export const HAND_COLOR = 'var(--impact-hand)' +/** Text/ink color to place on a FIELD_COLOR fill (white in light, dark in dark). */ +export const FIELD_INK = 'var(--impact-field-ink)' +/** Empty segment color for the field-progress meter. */ +export const FIELD_TRACK = 'var(--impact-field-track)' +/** Live-signal accent — the pulsing dot on points with live outputs (green). */ +export const LIVE_COLOR = '#22c55e' + +/** + * The one-line contract for reading the whole dashboard. Shared by the + * "How to read this" overlay (near the cards) and the methodology callout so + * the framing lives in exactly one place. + */ +export const HOW_TO_READ = + 'We don’t claim credit for outcomes. We name a falsifiable threshold, state what we contributed, and show live signal for and against. Judge us on whether our thresholds prove right and our contributions prove load-bearing.' + +/** + * Q3 contribution, structured along the planned-work side of the logic model. + * Inputs (resources committed) -> Activities (what we do) -> Outputs (what gets produced). + * The outcomes and impact these aim at are tracked on the field axis (Q2, Q1). + */ +export type Contribution = { + inputs: string + activities: string + outputs: string +} + +/** A pointer to live activity that is contribution evidence (Q3) — NOT a Q2 threshold reading. */ +export type LiveEvidence = { + label: string + href: string + note: string +} + +export type InflectionPoint = { + /** Focus-area slug, matches /areas/. */ + area: FocusAreaKey + /** The opportunity space this bet lives in. */ + opportunitySpace: string + /** The inflection point, stated as a hypothesis. */ + title: string + /** Q2 — observable threshold that says it happened (not yet true). */ + signal: string + /** Q1 — why it matters / the cascade to watch. */ + cascade: string + /** Q3 — PL contribution to trace, as inputs -> activities -> outputs. */ + contribution: Contribution + /** Q3, summarized as the PL role(s) on the critical path — the instruments we bring. */ + roles: PLRole[] + /** Field-progress lifecycle state. All start 'watching' — none reached as of 2026. */ + status: InflectionStatus + /** Optional live activity / real-world signals — strictly Q3 evidence, never Q2 progress. */ + liveEvidence?: LiveEvidence[] +} + +export type FocusAreaKey = + | 'digital-human-rights' + | 'economies-governance' + | 'ai-robotics' + | 'neurotech' + +export type FocusAreaMeta = { + key: FocusAreaKey + label: string + /** Short legacy code used in the strategy docs. */ + code: string + href: string + /** Accent color (hex from the theme) for badges and the field-progress meter. */ + accent: string + /** True when the focus area's inflection points are deliberately not-yet-defined (not empty). */ + forthcoming?: boolean +} + +export const FOCUS_AREAS: FocusAreaMeta[] = [ + { key: 'digital-human-rights', label: 'Digital Human Rights', code: 'FA1', href: '/areas/digital-human-rights/', accent: '#1982F4' }, + { key: 'economies-governance', label: 'Economies & Governance', code: 'FA2', href: '/areas/economies-governance/', accent: '#12bfdf' }, + { key: 'ai-robotics', label: 'AI & Robotics', code: 'FA3', href: '/areas/ai-robotics/', accent: '#3966FE', forthcoming: true }, + { key: 'neurotech', label: 'Neurotech', code: 'FA4', href: '/areas/neurotech/', accent: '#E51A66' }, +] + +// The logic-model chain: planned work (inputs/activities/outputs) → intended +// results (outcomes/impact). Shared by the Measuring-impact section, the three- +// questions explainer, and the per-point detail modal so the vocabulary lives +// in one place. +export const LOGIC_MODEL = [ + { key: 'inputs', label: 'Inputs', body: 'Funding, teams, convenings, standards we commit.' }, + { key: 'activities', label: 'Activities', body: 'Seeding teams, building primitives, setting standards.' }, + { key: 'outputs', label: 'Outputs', body: 'Teams funded, deployments, papers, ventures.' }, + { key: 'outcomes', label: 'Outcomes', body: 'Adoption, capital inflows, new entrants.' }, + { key: 'impact', label: 'Impact', body: 'The lasting shift: an inflection point that holds.' }, +] as const +export type LogicStageKey = (typeof LOGIC_MODEL)[number]['key'] + +// ── Field-progress lifecycle (Q1 & Q2). Deliberately separate from PL contribution. ── +export const FIELD_STAGES = ['Defined', 'Emerging', 'Reached', 'Scaling'] as const +export type FieldStage = (typeof FIELD_STAGES)[number] + +/** How far along the field axis a point sits, given its status. */ +export function stageIndexForStatus(status: InflectionStatus): number { + switch (status) { + case 'watching': + return 0 // threshold defined, waiting at it + case 'early-signal': + return 1 + case 'tripped': + return 2 + } +} + +export const STATUS_META: Record = { + watching: { + label: 'Defined', + description: 'The threshold is defined and pre-registered. No movement toward it yet.', + color: '#6b6d79', + }, + 'early-signal': { + label: 'Emerging', + description: 'Leading indicators are moving toward the threshold.', + color: '#1982F4', + }, + tripped: { + label: 'Reached', + description: 'The defined threshold has been crossed.', + color: '#12bfdf', + }, +} + +/** + * Team / venture display name -> canonical website. Used to linkify the named + * examples that appear in contribution copy (activities / outputs) so each + * mentioned team points to its own site. Keep names exactly as they are written + * in the copy (minus trailing qualifiers like "(World)"). + */ +export const TEAM_LINKS: Record = { + 'Funding the Commons': 'https://fundingthecommons.io/', + 'Tools for Humanity': 'https://www.toolsforhumanity.com/', + 'Impossible Cloud': 'https://www.impossiblecloud.com/', + Hypercerts: 'https://hypercerts.org/', + GainForest: 'https://gainforest.earth/', + WeatherXM: 'https://weatherxm.com/', + Molecule: 'https://www.molecule.xyz/', + 'EQTY Lab': 'https://eqtylab.io/', + SpruceID: 'https://spruceid.com/', + Huddle01: 'https://huddle01.com/', + Spheron: 'https://spheron.network/', + Filecoin: 'https://filecoin.io/', + Fluence: 'https://fluence.network/', + Expanso: 'https://www.expanso.io/', + libp2p: 'https://libp2p.io/', + Privy: 'https://privy.io/', + Fleek: 'https://fleek.co/', + Glow: 'https://glowlabs.org/', + Lava: 'https://www.lavanet.xyz/', + IPFS: 'https://ipfs.io/', +} + +export const ROLE_META: Record = { + infrastructure: { + label: 'Infrastructure', + description: 'The open, neutral rail this runs on did not exist. PL builds and maintains it (libp2p / IPFS / Filecoin lineage).', + }, + legibility: { + label: 'Legibility', + description: 'The field lacked a shared map. PL adds roadmaps, taxonomies, benchmarks, and written artifacts.', + }, + connection: { + label: 'Connection', + description: 'Too few connections blocked progress. PL convenes the people who need to collide: dinners, retreats, residencies, hackathons.', + }, + capital: { + label: 'Funding', + description: 'Pre-commercial work needed patient funding. PL runs grants and prizes and helps peer funders deploy theirs.', + }, + translation: { + label: 'Translation', + description: 'The work was ready to leave the lab. PL helps convert it into ventures, pilots, and deployments.', + }, + permission: { + label: 'Policy', + description: 'The rules did not yet recognize the technology. PL engages standards, policy, and regulatory pathways.', + }, +} + +export const INFLECTION_POINTS: InflectionPoint[] = [ + // ── FA1 · Digital Human Rights ─────────────────────────────────────────── + { + area: 'digital-human-rights', + opportunitySpace: 'Censorship-Resistant Communication', + title: 'Communication that cannot be switched off', + signal: + 'A consumer-scale connectivity provider operates without state licensing or identity gating, and a metadata-resistant messenger crosses tens of millions of users; a population stays connected through a deliberate shutdown.', + cascade: + 'Censoring speech and assembly becomes impractical, not just illegal; organizing survives adversarial conditions.', + contribution: { + inputs: 'The libp2p / IPFS open-source networking stack; funding for resilient-comms and private-messaging teams.', + activities: 'Maintaining the substrate, funding teams, and contributing to interoperability standards.', + outputs: 'libp2p / IPFS deployments and the funded comms / messaging teams building on them, such as Fluence and Huddle01.', + }, + roles: ['infrastructure', 'capital'], + status: 'early-signal', + liveEvidence: [ + { + label: 'Wikipedia kept online via IPFS during Turkey’s block', + href: 'https://observer.com/2017/05/turkey-wikipedia-ipfs/', + note: 'A content-addressed (IPFS) mirror kept Wikipedia reachable under Turkey’s state block — early evidence that information can route around a shutdown, not the population-scale-through-a-shutdown threshold (Q2).', + }, + ], + }, + { + area: 'digital-human-rights', + opportunitySpace: 'Portable Identity, Credentials & Trust', + title: 'Personhood without the state in the loop', + signal: + 'A service at >100M people verifies unique humans for everyday use without a nation-state ID or KYC.', + cascade: + 'Recognition stops being rented from platforms and states; privacy-preserving personhood becomes safe to build on.', + contribution: { + inputs: 'Convening capacity across identity protocols; seed funding for portable-credential initiatives.', + activities: 'Convening identity-protocol teams and seeding / funding portable-credential work.', + outputs: 'The identity and credential initiatives PL has seeded or funded, such as Tools for Humanity (World), SpruceID, and Privy.', + }, + roles: ['connection', 'capital'], + status: 'watching', + }, + { + area: 'digital-human-rights', + opportunitySpace: 'Verifiable Public Knowledge & Provenance', + title: 'Provenance becomes the default for truth', + signal: + 'Two consecutive frontier-model generations ship attested provenance by default, and a major platform or archive adopts content-addressed provenance as default.', + cascade: + 'A public record that can prove its own integrity becomes the precondition for trustworthy information in the AI era.', + contribution: { + inputs: 'Content addressing (IPFS) as the provenance substrate; funding for provenance and verifiable-compute teams.', + activities: 'Providing the content-addressing substrate and backing provenance / verifiable-compute teams.', + outputs: 'Content-addressed provenance tooling and the PL-backed teams building it, such as EQTY Lab.', + }, + roles: ['infrastructure', 'capital'], + status: 'early-signal', + liveEvidence: [ + { + label: 'Starling Lab — content-authenticity displays in newsrooms', + href: 'https://dispatch.starlinglab.org/p/verify-authenticity-displays', + note: 'Reuters, AP, Rolling Stone and others ship C2PA content credentials backed by content-addressed archives (IPFS / Filecoin) — early real-world provenance, not the default-everywhere threshold (Q2).', + }, + ], + }, + { + area: 'digital-human-rights', + opportunitySpace: 'Sovereign Infrastructure for AI & Agents', + title: 'Agents run on open rails', + signal: + 'A frontier-scale model is trained across independent decentralized hardware, or a meaningful share of agent-to-agent economic activity settles on open permissionless compute / storage / identity.', + cascade: + 'The rights architecture of the agent economy is set in the open rather than by whoever owns the cluster.', + contribution: { + inputs: 'The Filecoin / open-compute portfolio; PL capital and coordination across storage, compute, and identity.', + activities: 'Building open compute and storage rails and bridging them with identity for agents.', + outputs: 'Filecoin and the open-compute portfolio — Fluence, Spheron, Expanso, Impossible Cloud, Lava, Fleek — with integrations across storage, compute, and identity.', + }, + roles: ['infrastructure', 'connection', 'capital'], + status: 'watching', + }, + + // ── FA2 · Economies & Governance ───────────────────────────────────────── + { + area: 'economies-governance', + opportunitySpace: 'Sovereign Digital Public Infrastructure', + title: 'Programmable government in production', + signal: + 'At least one sovereign moves >$1B/yr of real public funds through programmable, real-time-auditable rails; 3+ jurisdictions use selective-disclosure credentials for a high-stakes function (election, passport, census).', + cascade: + 'Digital government crosses from digitization to transformation; a reference deployment others can copy.', + contribution: { + inputs: 'Standards and procurement playbooks; convening capacity with sovereigns and builders; funding for DPI primitives.', + activities: 'Writing standards and playbooks, convening sovereigns with builders, and funding DPI primitives.', + outputs: 'Published playbooks, convened sovereign–builder cohorts, and funded DPI primitives.', + }, + roles: ['connection', 'capital', 'permission'], + status: 'watching', + }, + { + area: 'economies-governance', + opportunitySpace: 'Computational Coordination & Governance', + title: 'A binding decision at scale', + signal: + 'A city or government makes a consequential, binding decision through a computational mechanism with tens of thousands participating, beating the legacy process on cost, turnout, or trust.', + cascade: + 'Deliberation tools move from advisory to part of real decision-making infrastructure.', + contribution: { + inputs: 'Support for computational-deliberation mechanisms (e.g. Simocracy, broad-listening tools); convening capacity.', + activities: 'Supporting mechanism teams and convening government teams with tool teams.', + outputs: 'Simocracy and broad-listening tools, and the government–tool convenings around them.', + }, + roles: ['connection', 'capital'], + status: 'watching', + liveEvidence: [ + { + label: 'Simocracy governance simulation — live participation', + href: '/areas/economies-governance/impact/live-dashboard/', + note: 'Live activity from a PL-supported mechanism. This is contribution evidence (Q3) from a simulation — not the binding-decision-in-government threshold (Q2).', + }, + ], + }, + { + area: 'economies-governance', + opportunitySpace: 'Programmable Capital Allocation', + title: 'Public goods become a financeable category', + signal: + '>$1B/yr flows through programmable allocation, a mainstream allocator (DFI, pension, sovereign fund) treats it as infrastructure, and a material real-world outcome is documented.', + cascade: + 'Funding public goods becomes a durable capital market rather than a niche.', + contribution: { + inputs: 'Hypercerts (PL origin) and the Gitcoin / Funding-the-Commons lineage; PL evidence and standards work.', + activities: 'Originating allocation mechanisms, building evidence and standards, and converting work into ventures.', + outputs: 'Hypercerts, Funding the Commons, and ventures spun out of / funded across this lineage such as Molecule.', + }, + roles: ['infrastructure', 'capital', 'translation'], + status: 'early-signal', + }, + { + area: 'economies-governance', + opportunitySpace: 'Verifiable Real-World Infrastructure & Systems', + title: 'Capital that pays on verified outcomes', + signal: + 'A $1B+ climate or public-goods fund disburses against independently verified real-world outcomes, faster and cheaper than a legacy audit.', + cascade: + 'Verification becomes the rail capital runs on; the loop back to better decisions closes.', + contribution: { + inputs: 'Funding for MRV / outcome-verification teams (e.g. GainForest, Glow); benchmark and standards convening.', + activities: 'Backing MRV teams and convening benchmark and standards work for outcome verification.', + outputs: 'GainForest, Glow, WeatherXM, and the verification benchmarks and standards they inform.', + }, + roles: ['legibility', 'connection', 'capital'], + status: 'early-signal', + liveEvidence: [ + { + label: 'GainForest & Glow — live verification activity', + href: '/areas/economies-governance/impact/live-dashboard/', + note: 'Live output from PL-backed MRV teams on this critical path. Contribution evidence (Q3) — not the $1B-disbursed-against-verified-outcomes threshold (Q2).', + }, + ], + }, + + // ── FA4 · Neurotech ────────────────────────────────────────────────────── + { + area: 'neurotech', + opportunitySpace: 'Neural Augmentation (BCI)', + title: 'The BCI app store', + signal: + 'A regulator clears a standardized software / API layer for a commercial BCI (with medical guardrails), and the first third-party app launches and is adopted.', + cascade: + 'BCI value compounds after surgery; a developer ecosystem forms; demand shifts toward elective use.', + contribution: { + inputs: 'PL Neuro standards work for a BCI component ecosystem; convening capacity across regulators, device makers, and developers.', + activities: 'Defining the BCI component-ecosystem standard and convening regulators, makers, and developers.', + outputs: 'A draft BCI component / API standard and the regulator–maker–developer convenings around it.', + }, + roles: ['connection', 'permission'], + status: 'watching', + }, + { + area: 'neurotech', + opportunitySpace: 'Biologically Inspired Intelligence (NeuroAI)', + title: 'Neural distillation', + signal: + 'A major lab matches frontier reasoning by training on human neural data at a fraction of the parameters / energy, or venture funding surges into consumer EEG to harvest cognitive data for AI.', + cascade: + 'High-fidelity neural data becomes as valuable to AI as text; comp-neuro talent flow reverses.', + contribution: { + inputs: 'The PL Neuro talent network bridging AI labs and comp neuro; investment in neural-data infrastructure and norms.', + activities: 'Bridging AI-lab and comp-neuro talent and building neural-data infrastructure and norms.', + outputs: 'The PL Neuro talent network and the neural-data infrastructure and norms it seeds.', + }, + roles: ['infrastructure', 'connection', 'capital'], + status: 'watching', + }, + { + area: 'neurotech', + opportunitySpace: 'Biologically Inspired Intelligence (NeuroAI)', + title: 'The neuromorphic energy pivot', + signal: + 'A brain-inspired model matches state-of-the-art performance at 3+ orders of magnitude better energy efficiency, and frontier labs begin acquiring neuromorphic startups.', + cascade: + 'Neurally derived design becomes foundational to commercial AI; AI is tethered to neuroscience.', + contribution: { + inputs: 'Funding for NeuroAI / neuromorphic research and demos; benchmark design defining the efficiency target.', + activities: 'Funding neuromorphic research and demos and defining the efficiency benchmark.', + outputs: 'PL-funded NeuroAI demos and the energy-efficiency benchmarks that frame the target.', + }, + roles: ['legibility', 'capital'], + status: 'watching', + }, + { + area: 'neurotech', + opportunitySpace: 'Whole Organism Emulation (WBE)', + title: 'Memory retrieval in simulation', + signal: + 'A reconstructed mouse connectome simulated in silico reproduces a specific behavior the biological mouse learned before its connectome was harvested.', + cascade: + 'WBE turns from speculation into a benchmarked engineering discipline; serious policy engagement begins.', + contribution: { + inputs: 'PL Neuro benchmark definition; connectomics workshops and throughput targets; engineering capacity for a demo.', + activities: 'Defining the WBE benchmark, running connectomics workshops, and engineering a reference demo.', + outputs: 'The WBE benchmark, connectomics throughput targets, and a PL-engineered demo.', + }, + roles: ['legibility', 'connection'], + status: 'watching', + }, +] + +export function pointsForArea(area: FocusAreaKey | 'all'): InflectionPoint[] { + if (area === 'all') return INFLECTION_POINTS + return INFLECTION_POINTS.filter((p) => p.area === area) +} diff --git a/src/lib/market-signals.ts b/src/lib/market-signals.ts new file mode 100644 index 00000000..5338f177 --- /dev/null +++ b/src/lib/market-signals.ts @@ -0,0 +1,361 @@ +// Prediction-market "field" signal for each inflection point. +// +// Each inflection point can be paired with the prediction market that most +// closely matches its pre-registered threshold (Q2). The platform is chosen per +// point on match quality, NOT convenience: +// - Polymarket — deepest liquidity for crypto/AI/identity/BCI-valuation themes. +// - Kalshi — regulated venue; better for US-regulatory / adoption-count themes. +// - Metaculus — reputation-weighted community forecasts for long-horizon, +// research-heavy bets no money market prices (e.g. WBE). Its API +// requires a token (METACULUS_API_TOKEN); without one we still +// link the question but show no live number. +// +// A market read is an *external, independent* estimate on the FIELD axis +// ("will it happen?") — it is never PL contribution (Q3) and never a settled Q2. +// Where no market exists, that gap is itself informative: the bet is early or +// contrarian enough that no one is pricing it yet. + +export type Platform = 'polymarket' | 'kalshi' | 'metaculus' +export type SignalMatch = 'direct' | 'proxy' | 'gap' + +type MarketRef = + | { platform: 'polymarket'; slug: string; hint?: string; question: string; url: string } + | { platform: 'kalshi'; ticker: string; question: string; url: string } + | { platform: 'metaculus'; id: number; kind?: 'binary' | 'date'; question: string; url: string } + +export type MarketMapping = { + /** Must match InflectionPoint.title exactly. */ + title: string + match: SignalMatch + /** Best-match market (drives the platform label). Omitted for gaps. */ + primary?: MarketRef + /** Liquidity fallback used only if the primary returns no live price. */ + fallback?: MarketRef + note: string +} + +export type MarketSignal = { + match: SignalMatch + platform: Platform | null + /** 0..1 crowd "yes" probability, or null when unavailable. */ + prob: number | null + /** Total money traded through the market, in USD, or null when unavailable. */ + volume: number | null + question: string | null + url: string | null + /** Pre-formatted headline reading for non-probability markets (e.g. a Metaculus date estimate, “~2068”). */ + readout?: string + /** True when prob came from the fallback rather than the best-match market. */ + viaFallback: boolean + note: string +} + +// ── Mapping: inflection point → closest market ──────────────────────────────── +export const MARKET_MAPPINGS: MarketMapping[] = [ + { + title: 'Communication that cannot be switched off', + match: 'gap', + note: 'No liquid market prices a metadata-resistant messenger at scale or surviving a shutdown — a genuine white space.', + }, + { + title: 'Personhood without the state in the loop', + match: 'proxy', + primary: { + platform: 'polymarket', + slug: 'over-30m-humans-verified-on-world-network-by-december-31', + question: 'Over 30M humans verified on World Network by Dec 31?', + url: 'https://polymarket.com/event/over-30m-humans-verified-on-world-network-by-december-31', + }, + note: 'Proof-of-human adoption at scale is the closest live analogue; Polymarket has the deepest World/identity liquidity.', + }, + { + title: 'Provenance becomes the default for truth', + match: 'proxy', + primary: { + platform: 'kalshi', + ticker: 'KXAILEGISLATION-27-JAN01', + question: 'Will AI regulation become US law by 2027?', + url: 'https://kalshi.com/markets/kxailegislation/ai-regulation', + }, + note: 'Provenance mandates ride on AI regulation; Kalshi (regulated, US-policy focus) is the better-matched venue than any crypto market.', + }, + { + title: 'Agents run on open rails', + match: 'proxy', + primary: { + platform: 'polymarket', + slug: 'us-government-removes-public-access-to-another-major-ai-model-in-2026-20260703202936862', + question: 'US government removes public access to another major AI model in 2026?', + url: 'https://polymarket.com/event/us-government-removes-public-access-to-another-major-ai-model-in-2026-20260703202936862', + }, + note: 'A contrarian, inverse read: if Washington moves to cut off public access to a major (open, largely Chinese) AI model, that is a headwind for open, permissionless rails — high odds here cut against this inflection point, not for it.', + }, + { + title: 'Programmable government in production', + match: 'gap', + note: 'No liquid market prices $1B/yr of public funds moving through programmable, real-time-auditable rails, or selective-disclosure credentials at national scale — a genuine white space.', + }, + { + title: 'A binding decision at scale', + match: 'gap', + note: 'No market prices a binding, at-scale computational decision. We already surface a live Simocracy signal here (Q3 contribution, not Q2).', + }, + { + title: 'Public goods become a financeable category', + match: 'gap', + note: 'No liquid market prices $1B/yr flowing through programmable allocation — long-horizon white space.', + }, + { + title: 'Capital that pays on verified outcomes', + match: 'gap', + note: 'No market prices a $1B+ outcomes-verified climate fund.', + }, + { + title: 'The BCI app store', + match: 'proxy', + primary: { + platform: 'kalshi', + ticker: 'KXNEURALINKCOUNT-26-40', + question: 'Will Neuralink implant ≥40 people in 2026?', + url: 'https://kalshi.com/markets/kxneuralinkcount/neuralink-count', + }, + fallback: { + platform: 'polymarket', + slug: 'will-neuralinks-valuation-hit-by-december-31', + hint: '$75B', + question: "Will Neuralink's valuation hit $75B by Dec 31?", + url: 'https://polymarket.com/event/will-neuralinks-valuation-hit-by-december-31', + }, + note: 'Implant-count (Kalshi) is the best adoption/scaling proxy for a commercial BCI ecosystem; Polymarket valuation is the liquidity fallback.', + }, + { + title: 'Neural distillation', + match: 'proxy', + primary: { + platform: 'metaculus', + id: 372, + question: 'Will brain emulation be the first successful route to human-level AI?', + url: 'https://www.metaculus.com/questions/372/brain-emulation-produces-first-human-ai/', + }, + note: 'The closest live forecast: Metaculus’ long-running question on whether brain emulation is the first route to human-level AI — a reputation-weighted proxy for how seriously the crowd takes neural-data-driven paths to frontier AI. No money at stake, and the crowd prices it in the low single digits.', + }, + { + title: 'The neuromorphic energy pivot', + match: 'gap', + note: 'No market prices a neuromorphic model matching SOTA at 1000× efficiency — pure white space.', + }, + { + title: 'Memory retrieval in simulation', + match: 'proxy', + primary: { + platform: 'metaculus', + id: 2813, + kind: 'date', + question: 'When will the first human whole-brain emulation happen?', + url: 'https://www.metaculus.com/questions/2813/date-of-first-human-whole-brain-emulation/', + }, + note: 'The closest live forecast: Metaculus’ community estimate for the date of the first human whole-brain emulation — the crowd currently places it decades out. A reputation-weighted forecast, no money at stake.', + }, +] + +export function mappingByTitle(): Record { + return Object.fromEntries(MARKET_MAPPINGS.map((m) => [m.title, m])) +} + +// ── Fetchers ───────────────────────────────────────────────────────────────── +const GAMMA = 'https://gamma-api.polymarket.com' +const KALSHI = 'https://api.elections.kalshi.com/trade-api/v2' +const METACULUS = 'https://www.metaculus.com/api' +const METACULUS_TOKEN = process.env.METACULUS_API_TOKEN +const REVALIDATE = 3600 + +function safeParse(s?: string): string[] | null { + if (!s) return null + try { + const v = JSON.parse(s) + return Array.isArray(v) ? v.map(String) : null + } catch { + return null + } +} + +function distinctiveToken(s?: string): string | null { + if (!s) return null + const money = s.match(/\$[0-9]+(?:\.[0-9]+)?[bmk]?/i) + if (money) return money[0].toLowerCase() + const num = s.match(/[0-9]+m\b|[0-9]{2,}/i) + return num ? num[0].toLowerCase() : null +} + +type Quote = { prob: number | null; volume: number | null; readout?: string } + +async function fetchPolymarket(slug: string, hint?: string): Promise { + try { + const res = await fetch(`${GAMMA}/events?slug=${encodeURIComponent(slug)}`, { + next: { revalidate: REVALIDATE }, + }) + if (!res.ok) return null + const events = (await res.json()) as Array<{ + title?: string + markets?: Array<{ question?: string; outcomes?: string; outcomePrices?: string; volumeNum?: number }> + }> + const markets = events?.[0]?.markets + if (!markets?.length) return null + const parsed = markets + .map((m) => { + const outcomes = safeParse(m.outcomes) + const prices = safeParse(m.outcomePrices) + if (!outcomes || !prices) return null + const yesIdx = outcomes.findIndex((o) => o.toLowerCase() === 'yes') + if (yesIdx === -1) return null + const yes = Number(prices[yesIdx]) + if (Number.isNaN(yes)) return null + const volume = typeof m.volumeNum === 'number' && m.volumeNum > 0 ? m.volumeNum : null + return { yes, volume, q: (m.question || '').toLowerCase() } + }) + .filter((x): x is { yes: number; volume: number | null; q: string } => x != null) + if (!parsed.length) return null + const token = distinctiveToken(hint) + const pick = (token && parsed.find((m) => m.q.includes(token))) || parsed[0] + return { prob: pick.yes, volume: pick.volume } + } catch { + return null + } +} + +async function fetchKalshi(ticker: string): Promise { + try { + const res = await fetch(`${KALSHI}/markets/${encodeURIComponent(ticker)}`, { + next: { revalidate: REVALIDATE }, + }) + if (!res.ok) return null + const m = ((await res.json()) as { market?: Record }).market + if (!m) return null + // dollar_volume is in USD; volume is in contracts (each settles $0-$1), so the + // contract count is a reasonable USD upper bound when no dollar figure is given. + const dollarVol = m.dollar_volume + const contractVol = m.volume + const volume = + typeof dollarVol === 'number' && dollarVol > 0 + ? dollarVol + : typeof contractVol === 'number' && contractVol > 0 + ? contractVol + : null + // Prices are in cents (0-100). Prefer last trade, else the bid/ask midpoint. + const last = m.last_price + if (typeof last === 'number' && last > 0) return { prob: last / 100, volume } + const bid = m.yes_bid + const ask = m.yes_ask + if (typeof bid === 'number' && typeof ask === 'number' && (bid > 0 || ask > 0)) { + return { prob: (bid + ask) / 2 / 100, volume } + } + return null + } catch { + return null + } +} + +// Metaculus gates its API behind a token. With METACULUS_API_TOKEN set we read +// the recency-weighted community prediction for a binary question; without it, +// or on any failure, we return null and the caller still links the question +// (prob shows as “—”). Metaculus has no money at stake, so volume is always null. +async function fetchMetaculus(id: number, kind: 'binary' | 'date' = 'binary'): Promise { + if (!METACULUS_TOKEN) return null + try { + const res = await fetch(`${METACULUS}/posts/${id}/`, { + headers: { Authorization: `Token ${METACULUS_TOKEN}` }, + next: { revalidate: REVALIDATE }, + }) + if (!res.ok) return null + const d = (await res.json()) as { + question?: { + scaling?: { range_min?: number; range_max?: number } + aggregations?: { recency_weighted?: { latest?: { centers?: number[]; means?: number[] } } } + } + } + const q = d.question + const latest = q?.aggregations?.recency_weighted?.latest + const center = latest?.centers?.[0] ?? latest?.means?.[0] + if (typeof center !== 'number') return null + if (kind === 'date') { + // Date questions report the community CDF location in 0..1; unscale it + // linearly across the question's range (unix seconds) to a median year. + const min = q?.scaling?.range_min + const max = q?.scaling?.range_max + if (typeof min !== 'number' || typeof max !== 'number') return null + const year = new Date((min + center * (max - min)) * 1000).getFullYear() + if (!Number.isFinite(year)) return null + return { prob: null, volume: null, readout: `~${year}` } + } + return { prob: center, volume: null } + } catch { + return null + } +} + +async function fetchRef(ref: MarketRef): Promise { + switch (ref.platform) { + case 'polymarket': + return fetchPolymarket(ref.slug, ref.hint) + case 'kalshi': + return fetchKalshi(ref.ticker) + case 'metaculus': + return fetchMetaculus(ref.id, ref.kind) + } +} + +/** Resolve one mapping into a display-ready signal (best-match, with fallback). */ +export async function resolveSignal(m: MarketMapping): Promise { + if (m.match === 'gap' || !m.primary) { + return { match: 'gap', platform: null, prob: null, volume: null, question: null, url: null, viaFallback: false, note: m.note } + } + const primary = await fetchRef(m.primary) + if (primary != null) { + return { + match: m.match, + platform: m.primary.platform, + prob: primary.prob, + volume: primary.volume, + readout: primary.readout, + question: m.primary.question, + url: m.primary.url, + viaFallback: false, + note: m.note, + } + } + if (m.fallback) { + const fb = await fetchRef(m.fallback) + if (fb != null) { + return { + match: m.match, + platform: m.fallback.platform, + prob: fb.prob, + volume: fb.volume, + readout: fb.readout, + question: m.fallback.question, + url: m.fallback.url, + viaFallback: true, + note: m.note, + } + } + } + // Market exists but no live price available — still link it. + return { + match: m.match, + platform: m.primary.platform, + prob: null, + volume: null, + question: m.primary.question, + url: m.primary.url, + viaFallback: false, + note: m.note, + } +} + +/** Resolve every mapping, keyed by inflection-point title. */ +export async function resolveAllSignals(): Promise> { + const entries = await Promise.all( + MARKET_MAPPINGS.map(async (m) => [m.title, await resolveSignal(m)] as const), + ) + return Object.fromEntries(entries) +} diff --git a/src/lib/site-config.ts b/src/lib/site-config.ts index 1078cd16..70e11ae0 100644 --- a/src/lib/site-config.ts +++ b/src/lib/site-config.ts @@ -35,6 +35,7 @@ export const mainNav: NavItem[] = [ { name: 'Neurotech', url: '/areas/neurotech/' }, ], }, + { name: 'Impact', url: '/impact/' }, { name: 'Insights', url: '/insights/' }, { name: 'Team', url: '/authors/' }, ]