diff --git a/app/scaling/components/CeilingCards.tsx b/app/scaling/components/CeilingCards.tsx new file mode 100644 index 0000000..8188204 --- /dev/null +++ b/app/scaling/components/CeilingCards.tsx @@ -0,0 +1,46 @@ +import Link from 'next/link'; + +import { Card } from '../../components/ui/Card'; +import { Text } from '../../components/ui/Text'; +import { deriveBinding, type Ceiling, type CeilingInput } from '../snapshot.mjs'; +import { Info } from './Info'; +import { Tag } from './Tag'; + +const AREAS: [Ceiling['area'], string][] = [['da', 'DA'], ['execution', 'Execution'], ['state', 'State'], ['networking', 'Networking']]; + +function input({ name, value, unit, status, asOf }: CeilingInput): string { + const shown = value === null ? status : `${value} ${unit}`.trim(); + return [name, shown, value === null ? null : status, asOf && new Date(asOf).toISOString().slice(0, 10)].filter(Boolean).join(' · '); +} + +export function CeilingCards({ ceilings }: { ceilings: Ceiling[] }) { + const binding = deriveBinding(ceilings); + return ( +
+ Ceilings by bottleneck · Mgas/s +
+ {AREAS.map(([area, name]) => { + const c = ceilings.find((x) => x.area === area)!; + return ( + + + {name} + {c.help} + + + {c.value === null ? '—' : c.value.toLocaleString('en-US', { maximumFractionDigits: 2 })} + + {c === binding ? Binding : null} + + {c.formula} + {c.reason ?

{c.reason}

: null} +
    + {c.inputs.map((i) =>
  • {input(i)}
  • )} +
+
+ ); + })} +
+
+ ); +} diff --git a/app/scaling/components/Guardrails.tsx b/app/scaling/components/Guardrails.tsx new file mode 100644 index 0000000..0a31143 --- /dev/null +++ b/app/scaling/components/Guardrails.tsx @@ -0,0 +1,57 @@ +import { cn } from '../../components/ui/cn'; +import { Card } from '../../components/ui/Card'; +import { Text } from '../../components/ui/Text'; +import type { Guardrail } from '../snapshot.mjs'; +import { Info } from './Info'; +import { Tag } from './Tag'; + +const TH = 'px-4 py-2 text-left text-[12px] font-[500] text-bds-gray-60'; +const TD = 'px-4 py-3 align-top'; +const DOT: Record = { ok: 'bg-bds-green-50', breached: 'bg-bds-red-50', unknown: 'bg-bds-gray-30' }; +const STATUS: Record = { ok: 'OK', breached: 'Breached', unknown: 'Unknown' }; + +const fmt = (n: number, unit: string) => `${n.toLocaleString('en-US', { maximumFractionDigits: 2 })} ${unit}`.trim(); + +export function Guardrails({ guardrails }: { guardrails: Guardrail[] }) { + return ( +
+ Guardrails + + + + + + + + + + + + {guardrails.map((g) => ( + + + + + + + ))} + +
GuardrailValueThresholdStatus
+ {g.label} {g.help} + {g.value === null ? '—' : fmt(g.value, g.unit)} + + {g.threshold === null ? '—' : `${g.comparator === '<=' ? '≤' : '≥'} ${fmt(g.threshold, g.unit)}`} + {g.thresholdTag === 'set' ? null : } + {g.owner ? {g.owner} : null} + + + + +
+
+ Breached SLOs are not attributed to saturation until triaged. +
+ ); +} diff --git a/app/scaling/components/Info.tsx b/app/scaling/components/Info.tsx new file mode 100644 index 0000000..65f6983 --- /dev/null +++ b/app/scaling/components/Info.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { useId, useState, type ReactNode } from 'react'; +import { Popover } from '@base-ui/react/popover'; + +// A popover, not InfoTooltip: it must also open on tap and stay open until Escape or an outside click. +export function Info({ label, children }: { label: string; children: ReactNode }) { + const [open, setOpen] = useState(false); + const popupId = useId(); + return ( + + e.currentTarget.matches(':focus-visible') && setOpen(true)} + onBlur={() => setOpen(false)} + className="inline-flex h-4 w-4 shrink-0 cursor-help items-center justify-center rounded-full text-bds-gray-50 outline-none transition-colors hover:text-base-blue focus-visible:ring-2 focus-visible:ring-base-blue/40 data-[popup-open]:text-base-blue" + > + + + + + + {children} + + + + + ); +} diff --git a/app/scaling/components/Ladder.tsx b/app/scaling/components/Ladder.tsx new file mode 100644 index 0000000..3c0df49 --- /dev/null +++ b/app/scaling/components/Ladder.tsx @@ -0,0 +1,106 @@ +'use client'; + +import { useLayoutEffect, useRef, useState } from 'react'; + +import { cn } from '../../components/ui/cn'; +import { Text } from '../../components/ui/Text'; +import { deriveBinding, type Rung, type Snapshot } from '../snapshot.mjs'; +import { Info } from './Info'; +import { Tag } from './Tag'; +import { layoutRungs, type Anchor } from './ladderLayout'; + +const LABEL_PX = 150; +const ROW = 44; +const AXIS = 12; +const AREA_NAME = { da: 'DA', execution: 'execution', state: 'state', networking: 'networking' }; +const ALIGN: Record = { start: 'items-start text-left', middle: 'items-center text-center', end: 'items-end text-right' }; +const SHIFT: Record = { start: 'none', middle: 'translateX(-50%)', end: 'translateX(-100%)' }; + +type Props = Pick; +type Item = { id: string; label: string; value: number | null; tag: string; help: string; limit?: boolean }; + +function items({ ladder, ceilings, windows }: Props): Item[] { + const rung = (id: string, label: string, r: Rung): Item => + ({ id, label, value: r.value, tag: r.tag, help: r.window ? `${r.help} Window: ${windows[r.window].label}.` : r.help }); + const binding = deriveBinding(ceilings); + const ceiling: Item = binding + ? { id: 'ceiling', label: `Ceiling · ${AREA_NAME[binding.area]}`, value: binding.value, tag: binding.tag, help: binding.help } + : { id: 'ceiling', label: 'Ceiling', value: null, tag: 'not-modeled', help: `No area ceiling is measured or modeled yet: ${ceilings.map((c) => `${AREA_NAME[c.area]} ${c.tag}`).join(', ')}.` }; + return [ + rung('p50', 'Observed p50', ladder.observed.p50), + rung('p99', 'Observed p99', ladder.observed.p99), + rung('operating', 'Operating point', ladder.operatingPoint), + rung('qualified', 'Qualified sustained', ladder.qualified), + ceiling, + { ...rung('limit', 'Configured limit', ladder.configuredLimit), limit: true }, + ]; +} + +function translation({ gasPerPayment, gasPerSwap }: Snapshot['translations']): string { + if (!gasPerPayment && !gasPerSwap) return 'Translation to payments/s and swaps/s: constants pending'; + const per = (c: typeof gasPerPayment, noun: string) => (c ? `${Math.round(1e6 / c.value)} ${noun}/s` : `${noun}/s pending`); + return `1 Mgas/s ≈ ${per(gasPerPayment, 'payments')} ≈ ${per(gasPerSwap, 'swaps')}`; +} + +export function Ladder(props: Props) { + const axisRef = useRef(null); + const [width, setWidth] = useState(960); + useLayoutEffect(() => { + const observer = new ResizeObserver(([entry]) => setWidth(entry.contentRect.width)); + observer.observe(axisRef.current!); + return () => observer.disconnect(); + }, []); + + const all = items(props); + const valued = all.flatMap((i) => (i.value === null ? [] : [{ id: i.id, value: i.value }])); + const max = props.ladder.configuredLimit.value ?? Math.max(0, ...valued.map((v) => v.value)); + const placed = new Map(max > 0 ? layoutRungs(valued, max, LABEL_PX / width).map((p) => [p.id, p]) : []); + const depth = (side: string) => 1 + Math.max(-1, ...[...placed.values()].filter((p) => p.side === side).map((p) => p.tier)); + const above = depth('above'); + const axisY = above * ROW + AXIS / 2; + + return ( +
+ Capacity ladder · Mgas/s +
+
+ {all.map((item) => { + const p = placed.get(item.id); + if (!p) return null; + const top = p.side === 'above' ? (above - 1 - p.tier) * ROW : above * ROW + AXIS + p.tier * ROW; + const left = `${p.x * 100}%`; + return ( +
+
+
+ + {item.label} {item.help} + + + {item.value!.toLocaleString('en-US', { maximumFractionDigits: 2 })} + {item.limit ? null : {item.tag}} + +
+
+ ); + })} +
+
    + {all.filter((item) => !placed.has(item.id)).map((item) => ( +
  • + {item.label} + + {item.help} +
  • + ))} +
+ {translation(props.translations)} +
+ ); +} diff --git a/app/scaling/components/Progress.tsx b/app/scaling/components/Progress.tsx new file mode 100644 index 0000000..15451ae --- /dev/null +++ b/app/scaling/components/Progress.tsx @@ -0,0 +1,62 @@ +import { Card } from '../../components/ui/Card'; +import { EmptyState } from '../../components/ui/EmptyState'; +import { Text } from '../../components/ui/Text'; +import type { WorkItem } from '../snapshot.mjs'; +import { Info } from './Info'; +import { Tag } from './Tag'; + +export function Progress({ work }: { work: WorkItem[] }) { + return ( +
+ Progress + {work.length === 0 ? ( + + ) : ( + + {work.map((w) => )} + + )} + Expected effects are not summed. +
+ ); +} + +function Row({ item: w }: { item: WorkItem }) { + const l = w.linear; + return ( +
+ + {w.title} + +

Hypothesis: {w.hypothesis}

+

Change: {w.change}

+

Owner: {w.owner ?? 'none assigned'}

+
+ + +
+ {l ? ( + + + + + {l.scopeDone}/{l.scopeTotal} + + ) : null} +

Expected: {w.expected ?? 'Not estimated'}

+ {w.evidence && w.realized ?

Realized: {w.realized}

: null} +
+ {[w.evidence, l && { label: 'Linear', url: l.url }, ...w.links].map((link) => link ? ( + {link.label} + ) : null)} +
+
+ ); +} diff --git a/app/scaling/components/Tag.tsx b/app/scaling/components/Tag.tsx new file mode 100644 index 0000000..cb83d08 --- /dev/null +++ b/app/scaling/components/Tag.tsx @@ -0,0 +1,12 @@ +const LABEL: Record = { + measured: 'Measured', modeled: 'Modeled', estimated: 'Estimated', + 'not-measured': 'Not measured', 'not-modeled': 'Not modeled', 'not-established': 'Not established', + provisional: 'Provisional', unset: 'Unset', + da: 'DA', execution: 'Execution', state: 'State', networking: 'Networking', +}; + +export function Tag({ tag }: { tag: string }) { + return ( + {LABEL[tag] ?? tag} + ); +} diff --git a/app/scaling/components/firstScreen.test.tsx b/app/scaling/components/firstScreen.test.tsx new file mode 100644 index 0000000..5f95aa2 --- /dev/null +++ b/app/scaling/components/firstScreen.test.tsx @@ -0,0 +1,70 @@ +import { readFileSync } from 'node:fs'; + +import { renderToStaticMarkup } from 'react-dom/server'; +import { describe, expect, it } from 'vitest'; + +import type { Snapshot } from '../snapshot.mjs'; +import { CeilingCards } from './CeilingCards'; +import { Guardrails } from './Guardrails'; +import { Progress } from './Progress'; + +// Synthetic data only: real snapshots are internal and never committed. +function fixture(): Snapshot { + return JSON.parse(readFileSync(new URL('../fixtures/synthetic.v2.json', import.meta.url), 'utf8')); +} + +const cards = (html: string) => html.split(' { + it('renders every area in a fixed order with its tag, and badges the binding card', () => { + const html = cards(renderToStaticMarkup()); + expect(html.map((c) => c.match(/href="\/scaling\/(\w+)"/)![1])).toEqual(['da', 'execution', 'state', 'networking']); + expect(html.map((c) => c.match(/border-dashed[^>]*>([^<]+) c.includes('>Binding<'))).toEqual([true, false, false, false]); + }); + + it('moves the binding badge to a lower measured ceiling, and shows a dash, reason and pending inputs when there is no value', () => { + const { ceilings } = fixture(); + ceilings[1] = { ...ceilings[1], value: 90, tag: 'measured' }; + const html = cards(renderToStaticMarkup()); + expect(html.map((c) => c.includes('>Binding<'))).toEqual([false, true, false, false]); + expect(html[2]).toContain('>—<'); + expect(html[2]).toContain('live-state growth not measured'); + expect(html[2]).toContain('state_bytes_per_gas · pending'); + expect(html[2]).toContain('runway_years · 4 years · provisional · 2026-01-01'); + }); +}); + +describe('Guardrails', () => { + it('shows comparator and unit with the threshold, tags provisional and unset thresholds, and colors only known statuses', () => { + const rows = renderToStaticMarkup().split('Provisional<'); + expect(rows[2]).toContain('Synthetic owner'); + expect(rows[3]).toContain('>Unset<'); + expect(rows[3]).toContain('bg-bds-gray-30'); + expect(rows[3]).not.toMatch(/bg-bds-(green|red)/); + }); +}); + +describe('Progress', () => { + it('shows Linear state and a labelled bar when linked, and proposed with no bar otherwise', () => { + const [linked, proposed] = renderToStaticMarkup().split('px-4 py-3').slice(1); + expect(linked).toContain('>started<'); + expect(linked).toContain('aria-label="4 of 10 issues done"'); + expect(linked).toContain('width:40%'); + expect(linked).toContain('Realized:'); + expect(linked).toContain('href="https://example.com/synthetic/linear/project"'); + expect(proposed).toContain('>proposed<'); + expect(proposed).not.toContain('progressbar'); + expect(proposed).not.toContain('Realized:'); + }); + + it('hides realized without evidence', () => { + const [item] = fixture().work; + expect(renderToStaticMarkup()).not.toContain('Realized:'); + }); +}); diff --git a/app/scaling/components/ladderLayout.test.ts b/app/scaling/components/ladderLayout.test.ts new file mode 100644 index 0000000..9b073c7 --- /dev/null +++ b/app/scaling/components/ladderLayout.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; + +import { layoutRungs } from './ladderLayout'; + +const rungs = (values: number[]) => values.map((value, i) => ({ id: `r${i}`, value })); + +describe('layoutRungs', () => { + it('keeps sparse rungs on the first tier, alternating sides in value order', () => { + const placed = layoutRungs(rungs([40, 200, 12.5, 95]), 200, 0.15); + expect(placed.map((p) => p.id)).toEqual(['r2', 'r0', 'r3', 'r1']); + expect(placed.map((p) => p.side)).toEqual(['above', 'below', 'above', 'below']); + expect(placed.every((p) => p.tier === 0)).toBe(true); + }); + + it('moves a rung to the next free tier on its side when its box would overlap', () => { + const placed = layoutRungs(rungs([10, 12, 14, 16, 18]), 200, 0.2); + expect(placed.map((p) => [p.side, p.tier])).toEqual([ + ['above', 0], ['below', 0], ['above', 1], ['below', 1], ['above', 2], + ]); + }); + + it('returns to the first tier once the previous label on that side ends', () => { + const placed = layoutRungs(rungs([10, 20, 30, 150]), 200, 0.2); + expect(placed.map((p) => p.tier)).toEqual([0, 0, 1, 0]); + }); + + it('anchors labels near the ends so they stay inside the axis, and clamps values above the limit', () => { + const [low, mid, high] = layoutRungs(rungs([5, 100, 250]), 200, 0.2); + expect(low).toMatchObject({ x: 0.025, anchor: 'start' }); + expect(mid).toMatchObject({ x: 0.5, anchor: 'middle' }); + expect(high).toMatchObject({ x: 1, anchor: 'end' }); + }); +}); diff --git a/app/scaling/components/ladderLayout.ts b/app/scaling/components/ladderLayout.ts new file mode 100644 index 0000000..49511e2 --- /dev/null +++ b/app/scaling/components/ladderLayout.ts @@ -0,0 +1,21 @@ +export type Anchor = 'start' | 'middle' | 'end'; +export type Placement = { id: string; x: number; side: 'above' | 'below'; tier: number; anchor: Anchor }; + +// Positions along a 0–1 axis; `labelWidth` shares that unit. Rungs alternate sides in value +// order; a label takes the first tier on its side where it does not overlap the previous label. +export function layoutRungs(rungs: { id: string; value: number }[], max: number, labelWidth: number): Placement[] { + const rightEdges: Record = { above: [], below: [] }; + return [...rungs] + .sort((a, b) => a.value - b.value) + .map((r, i) => { + const x = Math.min(1, r.value / max); + const anchor: Anchor = x < labelWidth / 2 ? 'start' : x > 1 - labelWidth / 2 ? 'end' : 'middle'; + const left = anchor === 'start' ? x : anchor === 'end' ? x - labelWidth : x - labelWidth / 2; + const side = i % 2 === 0 ? 'above' : 'below'; + const edges = rightEdges[side]; + let tier = edges.findIndex((edge) => edge <= left); + if (tier === -1) tier = edges.length; + edges[tier] = left + labelWidth; + return { id: r.id, x, side, tier, anchor }; + }); +} diff --git a/app/scaling/fixtures/synthetic.v2.json b/app/scaling/fixtures/synthetic.v2.json index ab3f615..7cae772 100644 --- a/app/scaling/fixtures/synthetic.v2.json +++ b/app/scaling/fixtures/synthetic.v2.json @@ -47,6 +47,119 @@ "help": "Highest rate a full-block run on mainnet-sized state has sustained. No run exists yet." } }, + "ceilings": [ + { + "area": "da", + "value": 95, + "unit": "Mgas/s", + "tag": "modeled", + "formula": "(blob_target × blob_bytes ÷ 12 s) × 0.8 ÷ da_bytes_per_gas", + "inputs": [ + { "name": "blob_target", "value": 6, "unit": "blobs/block", "asOf": "2026-01-01T00:00:00Z", "status": "configured" }, + { "name": "blob_bytes", "value": 131072, "unit": "bytes", "asOf": "2026-01-01T00:00:00Z", "status": "configured" }, + { + "name": "da_bytes_per_gas", + "value": 0.0055, + "unit": "bytes/gas", + "asOf": "2026-01-02T00:00:00Z", + "source": { "label": "Synthetic query", "url": "https://example.com/synthetic/da-bytes-per-gas" }, + "status": "observed" + } + ], + "help": "Gas per second the data-availability layer absorbs at 80% blob utilization." + }, + { + "area": "execution", + "value": 150, + "unit": "Mgas/s", + "tag": "estimated", + "formula": "sequencer gas/s of the newest passing bench run", + "inputs": [ + { "name": "bench_run", "value": "synthetic-run-1", "unit": "", "asOf": "2025-12-20T00:00:00Z", "status": "observed" }, + { "name": "state_size", "value": null, "unit": "GB", "asOf": null, "status": "pending" } + ], + "reason": "state size not recorded for the run", + "help": "Highest rate at which block building fits the slot and validators keep up, on mature state." + }, + { + "area": "state", + "value": null, + "unit": "Mgas/s", + "tag": "not-modeled", + "formula": "state_growth_budget ÷ state_bytes_per_gas", + "inputs": [ + { "name": "single_node_max", "value": 45, "unit": "TB", "asOf": "2026-01-01T00:00:00Z", "status": "configured" }, + { "name": "runway_years", "value": 4, "unit": "years", "asOf": "2026-01-01T00:00:00Z", "status": "provisional" }, + { "name": "state_bytes_per_gas", "value": null, "unit": "bytes/gas", "asOf": null, "status": "pending" } + ], + "reason": "live-state growth not measured", + "help": "Gas per second that keeps state within what one node can hold over the runway." + }, + { + "area": "networking", + "value": null, + "unit": "Mgas/s", + "tag": "not-modeled", + "formula": "none yet", + "inputs": [], + "reason": "gossip and peer limits unknown", + "help": "Gas per second at which blocks still propagate within the slot." + } + ], + "guardrails": [ + { + "id": "da-utilization", + "label": "DA utilization", + "value": 42, + "unit": "%", + "threshold": 80, + "comparator": "<=", + "thresholdTag": "set", + "owner": null, + "asOf": "2026-01-02T00:00:00Z", + "status": "ok", + "help": "Share of blob capacity used at the current rate." + }, + { + "id": "rpc-availability", + "label": "RPC availability", + "value": 99.5, + "unit": "%", + "threshold": 99.9, + "comparator": ">=", + "thresholdTag": "set", + "owner": null, + "asOf": "2026-01-02T00:00:00Z", + "status": "breached", + "help": "Successful RPC responses over the window." + }, + { + "id": "validator-keep-up", + "label": "Validator keep-up", + "value": 3, + "unit": "blocks", + "threshold": 5, + "comparator": "<=", + "thresholdTag": "provisional", + "owner": "Synthetic owner", + "asOf": "2026-01-01T00:00:00Z", + "status": "ok", + "help": "p90 distance of validators behind the tip. Threshold is a placeholder until the pod sets one." + }, + { + "id": "rpc-p99", + "label": "RPC p99 latency", + "value": 180, + "unit": "ms", + "threshold": null, + "comparator": "<=", + "thresholdTag": "unset", + "owner": null, + "asOf": "2026-01-02T00:00:00Z", + "status": "unknown", + "help": "Slowest 1% of RPC responses. No operational rule yet, so no status." + } + ], "translations": { "gasPerPayment": { "value": 52000, diff --git a/app/scaling/page.test.tsx b/app/scaling/page.test.tsx new file mode 100644 index 0000000..75068a7 --- /dev/null +++ b/app/scaling/page.test.tsx @@ -0,0 +1,14 @@ +import { fileURLToPath } from 'node:url'; + +import { renderToStaticMarkup } from 'react-dom/server'; +import { expect, it, vi } from 'vitest'; + +import ScalingPage from './page'; + +it('collapses each detail section by default with its title and row count', async () => { + vi.stubEnv('SCALING_SNAPSHOT_PATH', fileURLToPath(new URL('./fixtures/synthetic.v2.json', import.meta.url))); + const html = renderToStaticMarkup(await ScalingPage()); + expect(html).toMatch(/
]*>]*>Data availability<\/h2>]*> · 2 rows<\/span>/); + expect(html).not.toContain('
-
- {headline(snapshot.ladder).map((m) => ( - - {m.label} - - - - ))} -
+ + + + + {snapshot.sections.map((section) => ( -
-
- {section.title} - {section.summary} -
- +
+ + {section.title} + · {section.metrics.length} {section.metrics.length === 1 ? 'row' : 'rows'} + {section.summary} + + {section.metrics.map((m) => (
@@ -94,27 +95,16 @@ export default async function ScalingPage() { ))} {section.gaps.length > 0 ? ( -
+
Missing evidence
    {section.gaps.map((gap) =>
  • {gap}
  • )}
) : null} -
+
))} -
- What we're working on - {snapshot.work.length === 0 ? ( - - ) : ( -
- {snapshot.work.map((w) => )} -
- )} -
- Experiment results: Benchmarks ·{' '} Load tests @@ -123,26 +113,11 @@ export default async function ScalingPage() { ); } -type Shown = Omit & { status: string }; - -function headline(ladder: Ladder): Shown[] { - const rungs: [string, Rung][] = [ - ['Observed gas/s (p50)', ladder.observed.p50], - ['Operating point', ladder.operatingPoint], - ['Configured limit', ladder.configuredLimit], - ['Qualified sustained', ladder.qualified], - ]; - return rungs.map(([label, r]) => ({ - label, status: r.value === null ? r.tag : 'observed', value: r.value, unit: r.unit, window: r.window, note: r.help, source: r.source, - })); -} - const STATUS_LABEL: Record = { observed: '', partial: 'Partial', unavailable: 'Unavailable', 'not-measured': 'Not measured', - 'not-modeled': 'Not modeled', 'not-established': 'Not established', }; -function Value({ metric, large = false }: { metric: Shown; large?: boolean }) { +function Value({ metric }: { metric: Metric }) { if (metric.value === null) { return ( @@ -152,7 +127,7 @@ function Value({ metric, large = false }: { metric: Shown; large?: boolean }) { } return ( - + {metric.value.toLocaleString('en-US', { maximumFractionDigits: 2 })} {metric.unit ? {metric.unit} : null} @@ -161,7 +136,7 @@ function Value({ metric, large = false }: { metric: Shown; large?: boolean }) { ); } -function MetricDetails({ metric, snapshot }: { metric: Shown; snapshot: Snapshot }) { +function MetricDetails({ metric, snapshot }: { metric: Metric; snapshot: Snapshot }) { const window = metric.window ? snapshot.windows[metric.window] : null; return (
@@ -178,28 +153,6 @@ function MetricDetails({ metric, snapshot }: { metric: Shown; snapshot: Snapshot ); } -function WorkCard({ item }: { item: WorkItem }) { - const rows = [ - ['Hypothesis', item.hypothesis], - ['Change', item.change], - ['Expected impact', item.expected ?? 'Not estimated'], - ['Realized impact', item.realized ?? 'Not measured'], - ]; - return ( - - {item.title} - {item.status} · {item.owner ?? 'No owner assigned'} - {rows.map(([term, text]) => ( -

{term}: {text}

- ))} -
- {item.evidence ? : null} - {item.links.map((l) => )} -
-
- ); -} - function ExternalLink({ link }: { link: { label: string; url: string } }) { return (
diff --git a/app/scaling/snapshot.mjs b/app/scaling/snapshot.mjs index 0bba404..465e9e3 100644 --- a/app/scaling/snapshot.mjs +++ b/app/scaling/snapshot.mjs @@ -18,6 +18,19 @@ * @typedef {{ id: string, title: string, summary: string, metrics: Metric[], gaps: string[] }} Section * @typedef {{ value: number | null, unit: 'Mgas/s', tag: Tag, window: string | null, source?: Link, help: string }} Rung * @typedef {{ observed: { p50: Rung, p99: Rung }, operatingPoint: Rung, configuredLimit: Rung, qualified: Rung }} Ladder + * @typedef {{ + * name: string, value: number | string | null, unit: string, asOf: string | null, source?: Link, + * status: 'observed' | 'configured' | 'provisional' | 'pending' + * }} CeilingInput + * @typedef {{ + * area: Area, value: number | null, unit: 'Mgas/s', tag: Tag, formula: string, + * inputs: CeilingInput[], reason?: string, help: string + * }} Ceiling + * @typedef {{ + * id: string, label: string, value: number | null, unit: string, threshold: number | null, + * comparator: '<=' | '>=', thresholdTag: 'set' | 'provisional' | 'unset', owner: string | null, + * asOf: string | null, status: 'ok' | 'breached' | 'unknown', help: string + * }} Guardrail * @typedef {{ value: number, unit: 'gas', derivation: string, window: string | null }} Constant * @typedef {{ * projectId: string, issueId?: string, state: string, progress: number, scopeDone: number, @@ -30,7 +43,8 @@ * }} WorkItem * @typedef {{ * schemaVersion: 2, network: string, collectedAt: string, context: string, windows: Record, - * ladder: Ladder, translations: { gasPerPayment: Constant | null, gasPerSwap: Constant | null }, + * ladder: Ladder, ceilings: Ceiling[], guardrails: Guardrail[], + * translations: { gasPerPayment: Constant | null, gasPerSwap: Constant | null }, * sections: Section[], work: WorkItem[] * }} Snapshot */ @@ -39,10 +53,11 @@ const STATUSES = ['observed', 'partial', 'unavailable', 'not-measured']; const TAGS = ['measured', 'modeled', 'estimated', 'not-measured', 'not-modeled', 'not-established']; const VALUED_TAGS = ['measured', 'modeled', 'estimated']; const AREAS = ['da', 'execution', 'state', 'networking']; +const INPUT_STATUSES = ['observed', 'configured', 'provisional', 'pending']; const DAY_MS = 86_400_000; const V1_MESSAGE = - 'schemaVersion: 1 is no longer read; migrate to 2: replace headline with ladder, add translations, ' + - 'and give each work entry area, expected, realized, evidence and linear'; + 'schemaVersion: 1 is no longer read; migrate to 2: replace headline with ladder, add ceilings, guardrails and ' + + 'translations, and give each work entry area, expected, realized, evidence and linear'; /** * Missing values must be explicit nulls with a non-measured status or tag, @@ -63,6 +78,7 @@ export function parseSnapshot(data) { const date = (o, key, path) => { if (typeof o[key] !== 'string' || Number.isNaN(Date.parse(o[key]))) fail(`${path}.${key}`, 'expected ISO timestamp'); }; + const nullableDate = (o, key, path) => o[key] !== null && date(o, key, path); const oneOf = (o, key, path, values) => { if (!values.includes(o[key])) fail(`${path}.${key}`, `expected one of ${values.join(', ')}`); }; @@ -121,6 +137,56 @@ export function parseSnapshot(data) { rung(observed.p99, 'ladder.observed.p99'); for (const key of ['operatingPoint', 'configuredLimit', 'qualified']) rung(ladder[key], `ladder.${key}`); + const areas = new Set(); + list(data, 'ceilings', 'snapshot').forEach((c, i) => { + const path = `ceilings[${i}]`; + if (!isObj(c)) return fail(path, 'expected object'); + oneOf(c, 'area', path, AREAS); + if (areas.has(c.area)) fail(`${path}.area`, 'duplicate'); + areas.add(c.area); + tagged(c, path); + str(c, 'formula', path); + str(c, 'help', path); + if (c.reason !== undefined) str(c, 'reason', path); + list(c, 'inputs', path).forEach((input, j) => { + const p = `${path}.inputs[${j}]`; + if (!isObj(input)) return fail(p, 'expected object'); + str(input, 'name', p); + if (typeof input.unit !== 'string') fail(`${p}.unit`, 'expected string'); + oneOf(input, 'status', p, INPUT_STATUSES); + if ((input.value === null) !== (input.status === 'pending')) fail(`${p}.value`, 'null exactly when status is pending'); + else if (input.value !== null && !['number', 'string'].includes(typeof input.value)) fail(`${p}.value`, 'expected number or string'); + nullableDate(input, 'asOf', p); + if (input.source !== undefined) link(input.source, `${p}.source`); + }); + }); + for (const area of AREAS) if (!areas.has(area)) fail('snapshot.ceilings', `missing ${area}`); + + const guardrailIds = new Set(); + list(data, 'guardrails', 'snapshot').forEach((g, i) => { + const path = `guardrails[${i}]`; + if (!isObj(g)) return fail(path, 'expected object'); + for (const key of ['id', 'label', 'help']) str(g, key, path); + if (guardrailIds.has(g.id)) fail(`${path}.id`, 'duplicate'); + guardrailIds.add(g.id); + if (typeof g.unit !== 'string') fail(`${path}.unit`, 'expected string'); + for (const key of ['value', 'threshold']) { + if (g[key] !== null && !Number.isFinite(g[key])) fail(`${path}.${key}`, 'expected number or null'); + } + oneOf(g, 'comparator', path, ['<=', '>=']); + oneOf(g, 'thresholdTag', path, ['set', 'provisional', 'unset']); + if ((g.threshold === null) !== (g.thresholdTag === 'unset')) fail(`${path}.thresholdTag`, 'unset exactly when threshold is null'); + nullableStr(g, 'owner', path); + nullableDate(g, 'asOf', path); + oneOf(g, 'status', path, ['ok', 'breached', 'unknown']); + if (g.value === null || g.threshold === null) { + if (g.status !== 'unknown') fail(`${path}.status`, 'must be unknown without a value and threshold'); + } else if (Number.isFinite(g.value) && Number.isFinite(g.threshold)) { + const ok = g.comparator === '<=' ? g.value <= g.threshold : g.value >= g.threshold; + if (g.status !== (ok ? 'ok' : 'breached')) fail(`${path}.status`, `expected ${ok ? 'ok' : 'breached'} for ${g.value} ${g.comparator} ${g.threshold}`); + } + }); + const translations = obj(data, 'translations', 'snapshot'); for (const key of ['gasPerPayment', 'gasPerSwap']) { const c = translations[key]; @@ -184,6 +250,20 @@ export function parseSnapshot(data) { return errors.length ? { ok: false, errors } : { ok: true, snapshot: data }; } +/** + * The lowest ceiling with evidence behind it; estimates and unmodeled areas + * never bind. Null when no area is measured or modeled. + * @param {Ceiling[]} ceilings + * @returns {Ceiling | null} + */ +export function deriveBinding(ceilings) { + let binding = null; + for (const c of ceilings) { + if ((c.tag === 'measured' || c.tag === 'modeled') && (binding === null || c.value < binding.value)) binding = c; + } + return binding; +} + /** * @param {string} collectedAt * @param {Date} now diff --git a/app/scaling/snapshot.test.ts b/app/scaling/snapshot.test.ts index f8491c9..86d344e 100644 --- a/app/scaling/snapshot.test.ts +++ b/app/scaling/snapshot.test.ts @@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; -import { isStale, parseSnapshot } from './snapshot.mjs'; +import { deriveBinding, isStale, parseSnapshot } from './snapshot.mjs'; // Synthetic data only: real snapshots are internal and never committed. function fixture() { @@ -40,22 +40,45 @@ describe('parseSnapshot', () => { ]); }); - it('rejects measured entries without a value', () => { + it('rejects measured or modeled entries without a value', () => { const data = fixture(); + data.ceilings[0].value = null; data.ladder.observed.p50.value = null; - expect(errorsOf(data)).toEqual(['ladder.observed.p50.value: required for measured']); + expect(errorsOf(data)).toEqual(['ladder.observed.p50.value: required for measured', 'ceilings[0].value: required for modeled']); }); it('rejects unknown areas and tags', () => { const data = fixture(); + data.ceilings[1].area = 'l1'; data.ladder.operatingPoint.tag = 'guessed'; data.work[0].area = 'ops'; expect(errorsOf(data)).toEqual([ 'ladder.operatingPoint.tag: expected one of measured, modeled, estimated, not-measured, not-modeled, not-established', + 'ceilings[1].area: expected one of da, execution, state, networking', + 'snapshot.ceilings: missing execution', 'work[0].area: expected one of da, execution, state, networking', ]); }); + it('rejects a missing ceiling area and a duplicate guardrail id', () => { + const data = fixture(); + data.ceilings.pop(); + data.guardrails[1].id = data.guardrails[0].id; + expect(errorsOf(data)).toEqual(['snapshot.ceilings: missing networking', 'guardrails[1].id: duplicate']); + }); + + it('rejects a guardrail status that disagrees with its value and threshold', () => { + const data = fixture(); + data.guardrails[0].status = 'breached'; + data.guardrails[1].status = 'unknown'; + data.guardrails[3].status = 'ok'; + expect(errorsOf(data)).toEqual([ + 'guardrails[0].status: expected ok for 42 <= 80', + 'guardrails[1].status: expected breached for 99.5 >= 99.9', + 'guardrails[3].status: must be unknown without a value and threshold', + ]); + }); + it('rejects a realized effect without evidence', () => { const data = fixture(); data.work[0].evidence = null; @@ -75,6 +98,23 @@ describe('parseSnapshot', () => { }); }); +describe('deriveBinding', () => { + it('picks the lowest measured or modeled ceiling and ignores estimates', () => { + const { ceilings } = fixture(); + ceilings[1].value = 60; + expect(deriveBinding(ceilings)?.area).toBe('da'); + ceilings[1].tag = 'measured'; + expect(deriveBinding(ceilings)?.area).toBe('execution'); + }); + + it('returns null when no ceiling is measured or modeled', () => { + const { ceilings } = fixture(); + Object.assign(ceilings[0], { value: null, tag: 'not-modeled' }); + ceilings[1].tag = 'estimated'; + expect(deriveBinding(ceilings)).toBeNull(); + }); +}); + describe('isStale', () => { const collectedAt = fixture().collectedAt; diff --git a/scripts/scaling-snapshot.mjs b/scripts/scaling-snapshot.mjs index e203907..df1fdce 100644 --- a/scripts/scaling-snapshot.mjs +++ b/scripts/scaling-snapshot.mjs @@ -3,7 +3,7 @@ // Usage: npm run scaling:check -- /private/path/mainnet.json import { readFileSync } from 'node:fs'; -import { parseSnapshot } from '../app/scaling/snapshot.mjs'; +import { deriveBinding, parseSnapshot } from '../app/scaling/snapshot.mjs'; const file = process.argv[2]; if (!file) { @@ -20,7 +20,9 @@ if (!result.ok) { const { snapshot } = result; const metrics = snapshot.sections.flatMap((s) => s.metrics); const missing = metrics.filter((m) => m.value === null).length; +const binding = deriveBinding(snapshot.ceilings); console.log( `${file}: valid — ${snapshot.network}, collected ${snapshot.collectedAt}, ` + - `${metrics.length} metrics (${missing} without a value), ${snapshot.work.length} work items`, + `binding ceiling ${binding ? `${binding.area} ${binding.value} ${binding.unit}` : 'none'}, ` + + `${snapshot.guardrails.length} guardrails, ${metrics.length} metrics (${missing} without a value), ${snapshot.work.length} work items`, );