Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/screenshots/impact-preview-ribbon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions public/robots.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ Disallow: /admin
Disallow: /edit
Disallow: /write
Disallow: /api/
# Unlisted drafts — shared by link only, not for indexing.
Disallow: /impact
Disallow: /impact-preview

Sitemap: https://www.plrd.org/sitemap.xml
2 changes: 0 additions & 2 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ body {
: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) */
}

Expand Down Expand Up @@ -134,7 +133,6 @@ html.dark {
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;
}

Expand Down
36 changes: 36 additions & 0 deletions src/app/impact-preview/[key]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import ImpactExperience from '@/components/ImpactExperience'

// ── Hidden preview of the Impact experience ───────────────────────────────────
// Shared only by link. The path segment IS the secret: `dynamicParams = false`
// plus a single `generateStaticParams` entry means every URL except the exact
// key 404s. The page is marked noindex/nofollow, is not in the nav, and is not
// in the sitemap. Rotate the key below to invalidate an old link.
const PREVIEW_KEY = '024dbf9194f85c80c584f572d22d5aa7'

export const revalidate = 60
export const dynamicParams = false

export function generateStaticParams() {
return [{ key: PREVIEW_KEY }]
}

export const metadata: Metadata = {
title: 'Impact (preview)',
robots: {
index: false,
follow: false,
googleBot: { index: false, follow: false },
},
}

export default async function ImpactPreviewPage({
params,
}: {
params: Promise<{ key: string }>
}) {
const { key } = await params
if (key !== PREVIEW_KEY) notFound()
return <ImpactExperience preview />
}
141 changes: 12 additions & 129 deletions src/app/impact/page.tsx
Original file line number Diff line number Diff line change
@@ -1,144 +1,27 @@
import type { Metadata } from 'next'
import Breadcrumb from '@/components/Breadcrumb'
import ImpactDashboardV2, { type LiveMetric, type LiveOutputs } from '@/components/ImpactDashboardV2'
import MeasuringQuestionsV2 from '@/components/MeasuringQuestionsV2'
import { fetchSimocracyStats } from '@/lib/simocracy'
import { fetchGainforestStats } from '@/lib/gainforest'
import { fetchGlowStats } from '@/lib/glow'
import { resolveAllSignals } from '@/lib/market-signals'
import { FOCUS_AREAS, type FocusAreaKey } from '@/lib/inflection-points'
import { instrumentsForArea, withOpenAlex, type InstrumentRecord } from '@/lib/velocity-instruments'
import { loadAllOpenAlex } from '@/lib/velocity-openalex'
import ImpactExperience from '@/components/ImpactExperience'

// The impact page reads field velocity: the interventions we run, the five
// instruments we read a field's rate of change with, and the inflection points
// we track, each with its live signal.
//
// NOTE: this is an unlisted draft. It is not in the nav and not in the sitemap,
// and it is marked noindex/nofollow so search engines skip it. The shareable
// entry point is the cryptic /impact-preview/<key>/ route; this canonical path
// is kept working but deliberately undiscoverable until the work is signed off.
export const revalidate = 60

async function fetchLiveOutputs(): Promise<LiveOutputs> {
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 judge PL R&D: whether the fields we back are speeding up. We name the interventions we run, then read field velocity through five instruments and the inflection points we track.',
robots: {
index: false,
follow: false,
googleBot: { index: false, follow: false },
},
}

export default async function ImpactPage() {
const [liveOutputs, marketSignals] = await Promise.all([fetchLiveOutputs(), resolveAllSignals()])

// Merge any OpenAlex CSV readings (idea vintage + talent entry) into the static
// instrument records, per focus area. Parsed at build time; absent CSVs are a
// no-op, leaving the documented `unwired` records in place.
const openAlex = loadAllOpenAlex()
const recordsByArea = Object.fromEntries(
FOCUS_AREAS.map((fa) => [fa.key, withOpenAlex(instrumentsForArea(fa.key), openAlex[fa.key])]),
) as Partial<Record<FocusAreaKey, InstrumentRecord[]>>

// Example idea-vintage series per field, for the methodology modal that explains
// the instrument (shown as small multiples).
const ideaVintageExamples = FOCUS_AREAS.map((fa) => {
const rec = (recordsByArea[fa.key] ?? []).find(
(r) => r.instrument === 'idea_vintage' && r.state === 'reading' && r.series && r.series.length > 1,
)
return rec ? { label: fa.label, series: rec.series!, scale: rec.seriesScale ?? 'linear' } : null
}).filter((x): x is { label: string; series: NonNullable<InstrumentRecord['series']>; scale: 'linear' | 'log' } => !!x)
return (
<div>
{/* Hero */}
<div className="max-w-6xl mx-auto px-6 pt-8">
<Breadcrumb items={[{ label: 'Impact' }]} />
<div className="pt-8 pb-10">
<h1 className="text-2xl lg:text-[44px] font-semibold leading-[1.1] tracking-tight mb-5 max-w-3xl">
PL R&amp;D&rsquo;s impact on field velocity
</h1>
<p className="text-lg text-gray-600 leading-relaxed max-w-none">
We back fields we think are ready to move, then check whether they do.{' '}
<strong className="font-semibold text-black">Field velocity</strong> is that rate of change:
how fast talent enters, capital forms, tool costs fall, and output ships.{' '}
<strong className="font-semibold text-black">Inflection points</strong> are one of the markers
we read &mdash; dated, falsifiable shifts an accelerating field should produce.
</p>
<p className="mt-4 text-lg text-gray-600 leading-relaxed max-w-none">
We measure velocity the same way we do the work: as a research program. Whether field
acceleration works is itself the open question, tested across all four focus areas.
</p>
<a
href="#methodology"
className="mt-6 inline-flex items-center gap-2 text-gray-600 hover:text-gray-900 transition-colors font-medium text-[15px]"
>
Learn more about our methodology
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</a>
</div>
</div>

{/* Field velocity — grey full-bleed section to set it apart from the rest of the site */}
<section className="border-y border-gray-200 bg-gray-100">
<div className="max-w-6xl mx-auto px-6 py-14 lg:py-16">
<h2 className="text-xl lg:text-2xl font-semibold tracking-tight mb-2">Field velocity</h2>
<p className="text-base text-gray-600 leading-relaxed max-w-3xl mb-8">
Pick a focus area. The summary above reads that field&rsquo;s velocity across the instruments
that apply to it; the inflection points below are the specific markers we track, each with its
live signal.
</p>
<ImpactDashboardV2
liveOutputs={liveOutputs}
marketSignals={marketSignals}
recordsByArea={recordsByArea}
/>
</div>
</section>

{/* Methodology */}
<div id="methodology" className="max-w-6xl mx-auto px-6 py-14 lg:py-16 scroll-mt-24">
<h2 className="text-xl lg:text-2xl font-semibold tracking-tight mb-2">Our methodology</h2>
<p className="text-base text-gray-600 leading-relaxed max-w-3xl mb-10">
The method is the meta-research design of how we do field acceleration. We name the
interventions we run, then read field velocity as the result. Same design, every focus area.
</p>
<MeasuringQuestionsV2 ideaVintageExamples={ideaVintageExamples} />
</div>
</div>
)
return <ImpactExperience />
}
55 changes: 8 additions & 47 deletions src/components/ImpactDashboardV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ import { useEffect, useMemo, useState } from 'react'
import {
ROLE_META,
PL_ROLE_ORDER,
FIELD_STAGES,
HOW_TO_READ,
TEAM_LINKS,
stageIndexForStatus,
resolutionFor,
inflectionLabel,
isConcerningMarker,
Expand All @@ -25,7 +23,6 @@ import {
INFLECTION_POINTS,
FIELD_COLOR,
FIELD_INK,
FIELD_TRACK,
HAND_COLOR,
LIVE_COLOR,
type FocusAreaKey,
Expand Down Expand Up @@ -251,6 +248,11 @@ function shortDate(s?: string): string | undefined {
return s ? s.slice(0, 10) : s
}

/** Review status line. Reads "not yet reviewed" until a review date lands. */
function reviewedLabel(asOf?: string): string {
return asOf ? `reviewed ${asOf}` : 'not yet reviewed'
}

function marketReadout(s: MarketSignal): string {
return s.readout ?? (s.prob != null ? `${Math.round(s.prob * 100)}%` : '—')
}
Expand Down Expand Up @@ -665,30 +667,6 @@ function MissesLedger({ points }: { points: InflectionPoint[] }) {
)
}

function FieldMeter({ status }: { status: InflectionPoint['status'] }) {
const reached = stageIndexForStatus(status)
return (
<div>
<div className="flex gap-1">
{FIELD_STAGES.map((_, i) => (
<span
key={i}
className="h-1.5 flex-1 rounded-full"
style={{ backgroundColor: i <= reached ? FIELD_COLOR : FIELD_TRACK }}
/>
))}
</div>
<div className="mt-1.5 flex justify-between text-[11px] text-gray-400">
{FIELD_STAGES.map((s, i) => (
<span key={s} className={i === reached ? 'font-medium text-gray-600' : ''}>
{s}
</span>
))}
</div>
</div>
)
}

function RoleChips({ roles }: { roles: PLRole[] }) {
const ordered = PL_ROLE_ORDER.filter((r) => roles.includes(r))
return (
Expand Down Expand Up @@ -729,7 +707,6 @@ function InflectionCard({
onOpen: () => void
}) {
const fa = FOCUS_AREAS.find((f) => f.key === point.area)!
const stageLabel = FIELD_STAGES[stageIndexForStatus(point.status)]
const hasLiveSignal = !!(
point.liveEvidence?.length ||
(metrics && metrics.length) ||
Expand Down Expand Up @@ -767,21 +744,12 @@ function InflectionCard({
<div className="mb-5 flex flex-col gap-1.5">
<div className="flex flex-wrap items-center gap-2">
<ResolutionChip point={point} />
<span className="text-[11px] text-gray-400">reviewed {resolution.asOf ?? 'not yet'}</span>
<span className="text-[11px] text-gray-400">{reviewedLabel(resolution.asOf)}</span>
</div>
<ResolutionMeta point={point} />
</div>

<div className="mt-auto border-t border-gray-100 pt-4">
<div className="flex items-center gap-2">
<span className="text-[11px] font-semibold uppercase tracking-wide" style={{ color: FIELD_COLOR }}>
The field
</span>
<span className="ml-auto text-[11px] font-medium" style={{ color: FIELD_COLOR }}>{stageLabel}</span>
</div>
</div>

<div className="mt-4 border-t border-gray-100 pt-4">
<div className="mb-2 flex items-center gap-2">
<span className="text-[11px] font-semibold uppercase tracking-wide" style={{ color: HAND_COLOR }}>
Our hand
Expand Down Expand Up @@ -917,18 +885,11 @@ function InflectionModal({
<p className="text-sm leading-relaxed text-gray-600">{point.cascade}</p>
</div>
</div>
<div className="rounded-xl border border-gray-200 bg-white p-5">
<div className="mb-2 text-xs font-semibold uppercase tracking-wide" style={{ color: FIELD_COLOR }}>
Progress against inflection point
</div>
<FieldMeter status={point.status} />
</div>

{/* Resolution: outcome × mattered (never inferred from one another). */}
<div className="mt-4 rounded-xl border border-gray-200 bg-white p-5">
<div className="rounded-xl border border-gray-200 bg-white p-5">
<div className="mb-3 flex flex-wrap items-center gap-2">
<ResolutionChip point={point} />
<span className="text-[11px] text-gray-400">reviewed {resolutionFor(point).asOf ?? 'not yet'}</span>
<span className="text-[11px] text-gray-400">{reviewedLabel(resolutionFor(point).asOf)}</span>
</div>
<ResolutionMeta point={point} stacked />
{resolutionFor(point).matteredEvidence && (
Expand Down
Loading