Capture harness, fixture verification, CI, and the public README

The site does no live inference. Rollouts are captured once against spark-1 and
replayed at their recorded wall-clock — a public demo with no auth cannot hold
an API key, and a recorded run can be scrubbed, permalinked, blind-compared and
verified in ways a live one cannot. What stops it being a video is that the
browser re-derives every number from the recorded moves.

verify_fixtures.py is the Python half of that: it replays every committed
fixture through the engine and reproduces its own rewards. All 16 land at
delta 0.0. A fixture that cannot be regenerated is a claim with no receipt.

First real measurement, thinking off, 8 seeds: solved 0/8. The model repeats
guesses it has already played, invents words (trape, slith, postt, boomy),
and contradicts its own feedback — consistency 0.09 to 0.17. That is the
published failure taxonomy showing up in our own data on the first run, and it
is why `consistency` is a reward component rather than a footnote.

A capture failure is recorded as a turn with a null reply, never dropped. A
capture that silently discarded failed turns would be reporting a better model
than the one that ran.

CI gates both halves and four things that fail silently in production: the word
lists must rebuild byte-identically, the prerendered routes must carry their own
baked og tags (crawlers do not run JS, so without them every shared link
previews as the homepage), no blob: URL may reach the bundle (the site's CSP has
no worker-src, so it falls back to default-src 'self' and a blob worker is
blocked with no error), and the conformance digest must match across languages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB
This commit is contained in:
karti-ai
2026-08-28 15:47:31 -07:00
parent 69607fbfe9
commit 408ce4a525
43 changed files with 5279 additions and 136 deletions
+86
View File
@@ -0,0 +1,86 @@
name: ci
on:
push:
branches: [main]
pull_request:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
with: { version: 11.21.0 }
- uses: actions/setup-node@v5
with: { node-version: 22, cache: pnpm }
# Asserts the toolchain matches what package.json pins, rather than
# discovering a mismatch three steps later as an unrelated build error.
- name: preflight
run: |
test "$(pnpm -v)" = "11.21.0" || { echo "pnpm $(pnpm -v) != 11.21.0"; exit 1; }
- run: CI=true pnpm install --frozen-lockfile
- run: pnpm typecheck
- run: pnpm check
- run: pnpm test
- run: pnpm build
- run: node scripts/bundle-budget.mjs
# The prerender pass writes a real HTML file per route. Crawlers do not
# run JavaScript, so without these every shared link previews as the
# homepage — assert the baked tags actually landed.
- name: prerendered head is real
run: |
test -f dist/demos/wordle/index.html || { echo "no prerendered demo route"; exit 1; }
grep -q 'og:title' dist/demos/wordle/index.html || { echo "og tags missing"; exit 1; }
grep -qv 'PIG Demo — RL environments you can play</title>' dist/demos/wordle/index.html \
|| { echo "demo route kept the homepage title"; exit 1; }
test -f dist/404.html || { echo "no 404.html"; exit 1; }
# A blob-backed worker is blocked in production and nowhere else: the
# site's CSP has no worker-src, so it falls back to default-src 'self'.
# The failure is silent — the solver simply never boots.
- name: no inline workers
run: |
! grep -rqE "createObjectURL|blob:" dist/assets/*.js \
|| { echo "a blob: URL reached the bundle; CSP will block it in prod"; exit 1; }
python:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: astral-sh/setup-uv@v6
with: { enable-cache: true }
- run: uv sync --all-packages
# Rebuild the word lists from their committed sources and assert the
# output is byte-identical. If it is not, every downstream number —
# the conformance digest included — is describing a different game.
- name: word lists rebuild identically
run: |
uv run python envs/wordle_five/words/build_words.py
git diff --exit-code envs/wordle_five/words/*.json
- run: uv run pytest envs/wordle_five/tests -q
- run: uv run python envs/probe.py
# The cross-language gate. Both halves score all 21.2M (guess, answer)
# pairs; the digests must match each other and the committed value.
- uses: pnpm/action-setup@v4
with: { version: 11.21.0 }
- uses: actions/setup-node@v5
with: { node-version: 22, cache: pnpm }
- run: CI=true pnpm install --frozen-lockfile
- run: pnpm conformance
# Every committed fixture must replay through the Python engine and
# reproduce its own recorded rewards. A fixture that cannot be
# regenerated is a claim with no receipt behind it.
- run: uv run python envs/verify_fixtures.py
+144
View File
@@ -0,0 +1,144 @@
# PIG-Demo
Interactive demos of reinforcement-learning **environments**, built for people
who sign the budget rather than write the training loop.
**https://demo.primeintellectgrowth.com**
An environment is four things: a task, a set of legal moves, a grader that
cannot be argued with, and a score that moves. Every demo here renders those
four boxes, plays a real recorded rollout against them, and then lets you
change what "good" means and watch the ranking flip.
Sibling project to [PIG](https://primeintellectgrowth.com).
---
## Why a word game first
Because it is **Prime Intellect's own hello-world**, and that is checkable in
three public repositories in about thirty seconds:
- one of five `basic` examples in [prime-rl](https://github.com/PrimeIntellect-ai/prime-rl/tree/main/examples/basic/wordle)
- an environment in [verifiers](https://github.com/PrimeIntellect-ai/verifiers/tree/main/environments/wordle)
- the environment used in the official [lab-cookbook](https://github.com/PrimeIntellect-ai/lab-cookbook) GEPA prompt-optimisation tutorial
We didn't pick a game. We picked theirs.
It also needs zero domain knowledge, which means an executive learns the
*machine* — task, moves, grader, score — before any vertical vocabulary gets in
the way. Every demo after it renders the same four boxes with a different
grader.
## What is actually in here
```
envs/wordle_five/ A real environment: engine, reward, reference solver, tests
envs/probe.py The ladder that proves the reward measures something
src/demos/wordle/ The browser half — a port of the same engine
public/traces/wordle/ Recorded rollouts, committed. The evidence.
```
The site does **no live inference**. Rollouts are captured once against a real
model and replayed at their recorded wall-clock. That is a deliberate choice: a
public demo with no auth cannot hold an API key, a live call is slow and flaky
on conference wifi, and a recorded run can be scrubbed, permalinked,
blind-compared and *verified* in ways a live one cannot.
What keeps it from being a video is that the browser re-derives every number it
shows. It re-runs the recorded moves through its own copy of the engine,
rescores them, and prints the delta against what the environment recorded.
## The three commands that check the claims
```bash
uv sync --all-packages
uv run pytest envs/wordle_five/tests # 25 tests, incl. the duplicate-letter cases
uv run python envs/probe.py # the reward ladder, 7 hard assertions
pnpm install && pnpm check && pnpm build
```
### The cross-language gate
`engine.py` and `engine.ts` must agree on every tile. CI scores **all 4,603² =
21.2 million** (guess, answer) pairs through both and compares a SHA-256:
```
69f4e8dfbdc492176d7b7f04b080d8d3cccb5f18aca2d592e622938a5aeec8e2
```
A hand-picked vector file only ever catches the cases somebody thought of. This
catches all of them, and it is what lets the page claim it *verified* a run.
The subtlety it protects is the duplicate-letter rule. Scoring runs in two
passes — every green in the word is resolved before any yellow is assigned —
because a letter may be marked non-grey at most as many times as it occurs in
the answer, and greens have first claim. `SASSY` against `BASIS` is `YGGXX`:
three S's guessed, two available, so one yellow and one grey. Getting this
wrong is the most common bug in implementations of this game, and it is the bug
that put a correction video on the most-watched explanation of it ever made.
## The reward, and why the third component is the point
| component | weight | role | measures |
|---|---|---|---|
| `solved` | 0.50 | objective | did it win |
| `economy` | 0.30 | objective | turns used, as a ratio against the reference player on the same word |
| `consistency` | 0.20 | **counterweight** | share of turns spent on a move that could still have won |
A counterweight has to be in genuine tension with the objective, or it is a
gate wearing a counterweight's name. This one is: a player maximising
information deliberately guesses words that **cannot** win, because a word
splitting the candidates evenly teaches more than a word that might happen to
be right. That is good play, and it costs consistency.
The probe ladder measures the trade rather than asserting it:
```
policy solved economy consistency TOTAL
inaction 0.0000 0.0000 0.0000 0.0000
crude 0.0000 0.0000 0.0556 0.0111
plausible 0.1250 0.1000 0.1493 0.1224
candidate_only 0.9375 0.7458 1.0000 0.8925
exhaustive 1.0000 0.9375 0.6094 0.9031
oracle 1.0000 1.0000 0.7292 0.9458
```
The two good policies are 0.05 apart and **neither dominates the other**. Which
one wins is a decision about what you actually want — which is the whole
argument this site exists to make, and it is why the reward editor on the page
can flip the ranking with one slider. `probe.py` fails CI if either policy
starts dominating, if any weighted component goes flat, or if doing nothing
scores above zero.
## Honesty
- Every rollout is **recorded, not live**, and labelled as such on the page.
- The reward editor **re-scores recorded attempts**. It does not retrain
anything, and it says so where you use it.
- The word list is [our own construction](envs/wordle_five/words/PROVENANCE.md)
from Wordnik (MIT) and SCOWL. Our answer pool is 4,603 — roughly twice the
original game's — so this is **harder** than the original and our numbers are
not comparable to published figures for it. The famous SALET / 3.4212-guess
optimum belongs to that list, not ours, and is cited as such.
- The verticals listed on the site are **our proposals**. They are not Prime
Intellect's roadmap and not a customer list. No customer logos, ever.
- Not affiliated with The New York Times. Independent implementation, own word
lists, own palette, own reward.
Full accounting: [demo.primeintellectgrowth.com/honesty](https://demo.primeintellectgrowth.com/honesty)
## Adding a demo
```bash
pnpm demo:new <slug>
```
Copies the templates and wires nothing — the registry finds demos by existence,
so the header, the gallery, the router and the sitemap all pick it up with zero
edits to shared files. `pnpm check` enforces the contract; see
[CONTRACT.md](CONTRACT.md).
## Licence
Apache-2.0. Third-party attributions in [NOTICE](NOTICE).
+228
View File
@@ -0,0 +1,228 @@
#!/usr/bin/env python3
"""Capture real rollouts into the fixtures the site replays.
The site does no live inference. That is a deliberate architecture choice, not
a limitation: a public demo with no auth cannot hold an API key, a live call is
slow and flaky on conference wifi, and a recorded run can be scrubbed,
verified, permalinked and blind-compared in ways a live one cannot.
What makes it honest rather than a video is that the browser re-derives every
number it shows from the recorded moves, and says so. See verify.ts.
Usage:
uv run python envs/capture.py --arm base-off --seeds 0-7
uv run python envs/capture.py --arm solver --seeds 0-7 # no model needed
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent / "wordle_five"))
import numpy as np # noqa: E402
from wordle_five import solver as S # noqa: E402
from wordle_five.engine import MAX_GUESSES, Game, answers # noqa: E402
from wordle_five.protocol import ( # noqa: E402
parse_guess,
render_feedback,
render_rejection,
system_prompt,
)
from wordle_five.reward import Episode, metrics, score # noqa: E402
OUT = Path(__file__).parent.parent / "public" / "traces" / "wordle"
ENDPOINT = os.environ.get("PIG_DEMO_INFERENCE", "http://100.127.247.67:8001/v1/chat/completions")
MODEL = os.environ.get("PIG_DEMO_MODEL", "brain-qwen38-dspark")
ARMS = {
"base-off": {"label": "Out of the box", "thinking": False},
"base-on": {"label": "Allowed to think", "thinking": True},
"solver": {"label": "Best-known play", "thinking": None},
}
def call_model(messages: list[dict], thinking: bool) -> dict:
"""One completion. Returns reply, reasoning and the real call metrics.
Never raises on an upstream failure — a dropped call becomes a turn with a
null reply, which the game scores as a rejected guess. A capture that
silently discarded failed turns would be reporting a better model than the
one that ran.
"""
body = {
"model": MODEL,
"messages": messages,
"temperature": 0.7,
"max_tokens": 2048 if thinking else 512,
"chat_template_kwargs": {"enable_thinking": bool(thinking)},
}
request = urllib.request.Request(
ENDPOINT,
data=json.dumps(body).encode(),
headers={"Content-Type": "application/json"},
)
started = time.time()
try:
with urllib.request.urlopen(request, timeout=300) as response:
payload = json.loads(response.read())
except (urllib.error.URLError, TimeoutError, OSError) as exc:
return {
"reply": None,
"reasoning": None,
"call": {
"promptTokens": None,
"completionTokens": None,
"reasoningTokens": None,
"durationMs": round((time.time() - started) * 1000),
"finishReason": f"error: {type(exc).__name__}",
},
}
elapsed = round((time.time() - started) * 1000)
choice = payload["choices"][0]
message = choice.get("message", {})
usage = payload.get("usage", {}) or {}
details = usage.get("completion_tokens_details") or {}
return {
"reply": message.get("content"),
"reasoning": message.get("reasoning_content"),
"call": {
"promptTokens": usage.get("prompt_tokens"),
"completionTokens": usage.get("completion_tokens"),
"reasoningTokens": details.get("reasoning_tokens"),
"durationMs": elapsed,
"finishReason": choice.get("finish_reason"),
},
}
def solver_turn(game: Game) -> dict:
"""The reference player, recorded in the same shape as a model turn.
durationMs is null rather than invented: nothing waited for this, and the
player must not pretend otherwise. The UI renders a null duration as an
instant step and labels the run as generated.
"""
pool = answers()
if not game.history:
guess = pool[S._best_opener()]
else:
alive = S.consistent_candidates(game.history)
guess = (
pool[S.best_guess(np.array([pool.index(w) for w in alive]))] if alive else "tares"
)
return {
"reply": f"[{guess}]",
"reasoning": None,
"call": {
"promptTokens": None,
"completionTokens": None,
"reasoningTokens": None,
"durationMs": None,
"finishReason": "generated",
},
}
def capture(arm: str, seed: int) -> dict:
config = ARMS[arm]
game = Game(seed=seed)
messages = [
{"role": "system", "content": system_prompt()},
{"role": "user", "content": "Enter your guess to begin."},
]
turns: list[dict] = []
while not game.over and len(turns) < MAX_GUESSES * 2:
if arm == "solver":
result = solver_turn(game)
else:
result = call_model(messages, bool(config["thinking"]))
guess = parse_guess(result["reply"])
left = MAX_GUESSES - len(game.history)
if guess is None:
game.rejected += 1
observation = render_rejection("no bracketed guess found", left)
else:
pattern, rejection = game.play(guess)
observation = (
render_rejection(rejection, left)
if rejection is not None
else render_feedback(guess, pattern or "", MAX_GUESSES - len(game.history))
)
turns.append({**result, "info": {"guess": guess, "observation": observation}})
messages.append({"role": "assistant", "content": result["reply"] or ""})
messages.append({"role": "user", "content": observation})
if game.rejected >= MAX_GUESSES:
break
episode = Episode(
answer=game.answer,
guesses=[g for g, _ in game.history],
patterns=[p for _, p in game.history],
rejected=game.rejected,
reference_depth=S.reference_depth(game.answer),
)
# A run that never reached a terminal state is marked truncated, and the
# browser renders its verification as "unverifiable" rather than as a zero.
truncated = not game.over and game.rejected < MAX_GUESSES
return {
"runId": f"{arm}-s{seed}",
"seed": seed,
"model": MODEL if arm != "solver" else "entropy-solver",
"capturedAt": time.strftime("%Y-%m-%d"),
"rewards": score(episode),
"metrics": metrics(episode),
"truncated": truncated,
"outcome": "solved" if game.solved else "failed",
"answer": game.answer,
"turns": turns,
}
def parse_seeds(spec: str) -> list[int]:
if "-" in spec:
lo, hi = spec.split("-")
return list(range(int(lo), int(hi) + 1))
return [int(s) for s in spec.split(",")]
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--arm", required=True, choices=sorted(ARMS))
parser.add_argument("--seeds", default="0-7")
args = parser.parse_args()
OUT.mkdir(parents=True, exist_ok=True)
for seed in parse_seeds(args.seeds):
started = time.time()
episode = capture(args.arm, seed)
path = OUT / f"{episode['runId']}.json"
path.write_text(json.dumps(episode, indent=2) + "\n")
guesses = [t["info"]["guess"] for t in episode["turns"]]
print(
f"{episode['runId']:>16} {episode['answer']} {episode['outcome']:<7}"
f" solved={episode['rewards']['solved']:.0f}"
f" econ={episode['rewards']['economy']:.2f}"
f" cons={episode['rewards']['consistency']:.2f}"
f" {time.time()-started:5.1f}s {guesses}"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
"""Replay every committed fixture and reproduce its recorded rewards.
A fixture is a claim: "this model, on this seed, scored this." The claim is
only worth anything if it can be re-derived from the moves it records. This
does that in Python; `verify.ts` does the same thing in the browser, live, in
front of the visitor.
Exits non-zero if any fixture's rewards cannot be reproduced from its own turns.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent / "wordle_five"))
from wordle_five import solver as S # noqa: E402
from wordle_five.engine import Game # noqa: E402
from wordle_five.protocol import parse_guess # noqa: E402
from wordle_five.reward import Episode, score # noqa: E402
TRACES = Path(__file__).parent.parent / "public" / "traces" / "wordle"
TOLERANCE = 1e-9
def replay(fixture: dict) -> dict[str, float]:
game = Game(seed=fixture["seed"])
if game.answer != fixture["answer"]:
raise AssertionError(
f"seed {fixture['seed']} gives {game.answer!r}, fixture claims {fixture['answer']!r}"
)
for turn in fixture["turns"]:
guess = parse_guess(turn["reply"])
if guess is None:
game.rejected += 1
continue
game.play(guess)
episode = Episode(
answer=game.answer,
guesses=[g for g, _ in game.history],
patterns=[p for _, p in game.history],
rejected=game.rejected,
reference_depth=S.reference_depth(game.answer),
)
return score(episode)
def main() -> int:
fixtures = sorted(TRACES.glob("*.json"))
fixtures = [f for f in fixtures if f.name != "manifest.json"]
if not fixtures:
print("no fixtures found — nothing to verify")
return 0
failures = 0
for path in fixtures:
data = json.loads(path.read_text())
recomputed = replay(data)
recorded = data["rewards"]
deltas = {k: abs(recomputed[k] - recorded[k]) for k in recorded}
worst = max(deltas.values())
status = "ok" if worst <= TOLERANCE else "MISMATCH"
if worst > TOLERANCE:
failures += 1
culprit = max(deltas, key=deltas.get)
print(f"{path.name:>20} {status} worst delta {worst:.3g} on '{culprit}'")
else:
print(f"{path.name:>20} {status} delta {worst:.1e}")
print(f"\n{len(fixtures)} fixtures, {failures} mismatched")
return 1 if failures else 0
if __name__ == "__main__":
raise SystemExit(main())
+143
View File
@@ -0,0 +1,143 @@
{
"runId": "base-off-s0",
"seed": 0,
"model": "brain-qwen38-dspark",
"capturedAt": "2026-08-28",
"rewards": {
"solved": 0.0,
"economy": 0.0,
"consistency": 0.125
},
"metrics": {
"guesses_used": 6.0,
"rejected_replies": 2.0,
"reference_depth": 4.0,
"turns_granted": 6.0,
"inconsistent_guesses": 5.0
},
"truncated": false,
"outcome": "failed",
"answer": "wants",
"turns": [
{
"reply": "[crane]",
"reasoning": null,
"call": {
"promptTokens": 147,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 433,
"finishReason": "stop"
},
"info": {
"guess": "crane",
"observation": "C R A N E\nX X Y Y X\n\nYou have 5 guesses left."
}
},
{
"reply": "[slate]",
"reasoning": null,
"call": {
"promptTokens": 184,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 442,
"finishReason": "stop"
},
"info": {
"guess": "slate",
"observation": "S L A T E\nY X Y G X\n\nYou have 4 guesses left."
}
},
{
"reply": "[stale]",
"reasoning": null,
"call": {
"promptTokens": 221,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 496,
"finishReason": "stop"
},
"info": {
"guess": "stale",
"observation": "S T A L E\nY Y Y X X\n\nYou have 3 guesses left."
}
},
{
"reply": "[prate]",
"reasoning": null,
"call": {
"promptTokens": 258,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 488,
"finishReason": "stop"
},
"info": {
"guess": "prate",
"observation": "P R A T E\nX X Y G X\n\nYou have 2 guesses left."
}
},
{
"reply": "[prate]",
"reasoning": null,
"call": {
"promptTokens": 295,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 443,
"finishReason": "stop"
},
"info": {
"guess": "prate",
"observation": "That guess was not accepted: 'prate' has already been guessed.\nIt did not use up a turn. You have 2 guesses left."
}
},
{
"reply": "[spate]",
"reasoning": null,
"call": {
"promptTokens": 344,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 495,
"finishReason": "stop"
},
"info": {
"guess": "spate",
"observation": "S P A T E\nY X Y G X\n\nYou have 1 guess left."
}
},
{
"reply": "[prate]",
"reasoning": null,
"call": {
"promptTokens": 381,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 445,
"finishReason": "stop"
},
"info": {
"guess": "prate",
"observation": "That guess was not accepted: 'prate' has already been guessed.\nIt did not use up a turn. You have 1 guess left."
}
},
{
"reply": "[stare]",
"reasoning": null,
"call": {
"promptTokens": 430,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 502,
"finishReason": "stop"
},
"info": {
"guess": "stare",
"observation": "S T A R E\nY Y Y X X\n\nYou have 0 guesses left."
}
}
]
}
+113
View File
@@ -0,0 +1,113 @@
{
"runId": "base-off-s1",
"seed": 1,
"model": "brain-qwen38-dspark",
"capturedAt": "2026-08-28",
"rewards": {
"solved": 0.0,
"economy": 0.0,
"consistency": 0.16666666666666666
},
"metrics": {
"guesses_used": 6.0,
"rejected_replies": 0.0,
"reference_depth": 3.0,
"turns_granted": 6.0,
"inconsistent_guesses": 5.0
},
"truncated": false,
"outcome": "failed",
"answer": "amber",
"turns": [
{
"reply": "[crane]",
"reasoning": null,
"call": {
"promptTokens": 147,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 385,
"finishReason": "stop"
},
"info": {
"guess": "crane",
"observation": "C R A N E\nX Y Y X Y\n\nYou have 5 guesses left."
}
},
{
"reply": "[slate]",
"reasoning": null,
"call": {
"promptTokens": 184,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 443,
"finishReason": "stop"
},
"info": {
"guess": "slate",
"observation": "S L A T E\nX X Y X Y\n\nYou have 4 guesses left."
}
},
{
"reply": "[plume]",
"reasoning": null,
"call": {
"promptTokens": 221,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 499,
"finishReason": "stop"
},
"info": {
"guess": "plume",
"observation": "P L U M E\nX X X Y Y\n\nYou have 3 guesses left."
}
},
{
"reply": "[grape]",
"reasoning": null,
"call": {
"promptTokens": 258,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 488,
"finishReason": "stop"
},
"info": {
"guess": "grape",
"observation": "G R A P E\nX Y Y X Y\n\nYou have 2 guesses left."
}
},
{
"reply": "[stare]",
"reasoning": null,
"call": {
"promptTokens": 295,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 439,
"finishReason": "stop"
},
"info": {
"guess": "stare",
"observation": "S T A R E\nX X Y Y Y\n\nYou have 1 guess left."
}
},
{
"reply": "[crate]",
"reasoning": null,
"call": {
"promptTokens": 332,
"completionTokens": 4,
"reasoningTokens": null,
"durationMs": 493,
"finishReason": "stop"
},
"info": {
"guess": "crate",
"observation": "C R A T E\nX Y Y X Y\n\nYou have 0 guesses left."
}
}
]
}
+188
View File
@@ -0,0 +1,188 @@
{
"runId": "base-off-s2",
"seed": 2,
"model": "brain-qwen38-dspark",
"capturedAt": "2026-08-28",
"rewards": {
"solved": 0.0,
"economy": 0.0,
"consistency": 0.09090909090909091
},
"metrics": {
"guesses_used": 5.0,
"rejected_replies": 6.0,
"reference_depth": 4.0,
"turns_granted": 6.0,
"inconsistent_guesses": 5.0
},
"truncated": false,
"outcome": "failed",
"answer": "spume",
"turns": [
{
"reply": "[crane]",
"reasoning": null,
"call": {
"promptTokens": 147,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 301,
"finishReason": "stop"
},
"info": {
"guess": "crane",
"observation": "C R A N E\nX X X X G\n\nYou have 5 guesses left."
}
},
{
"reply": "[shine]",
"reasoning": null,
"call": {
"promptTokens": 184,
"completionTokens": 4,
"reasoningTokens": null,
"durationMs": 444,
"finishReason": "stop"
},
"info": {
"guess": "shine",
"observation": "S H I N E\nG X X X G\n\nYou have 4 guesses left."
}
},
{
"reply": "[shone]",
"reasoning": null,
"call": {
"promptTokens": 220,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 496,
"finishReason": "stop"
},
"info": {
"guess": "shone",
"observation": "S H O N E\nG X X X G\n\nYou have 3 guesses left."
}
},
{
"reply": "[swine]",
"reasoning": null,
"call": {
"promptTokens": 257,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 606,
"finishReason": "stop"
},
"info": {
"guess": "swine",
"observation": "S W I N E\nG X X X G\n\nYou have 2 guesses left."
}
},
{
"reply": "[shine]",
"reasoning": null,
"call": {
"promptTokens": 294,
"completionTokens": 4,
"reasoningTokens": null,
"durationMs": 439,
"finishReason": "stop"
},
"info": {
"guess": "shine",
"observation": "That guess was not accepted: 'shine' has already been guessed.\nIt did not use up a turn. You have 2 guesses left."
}
},
{
"reply": "[spine]",
"reasoning": null,
"call": {
"promptTokens": 341,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 384,
"finishReason": "stop"
},
"info": {
"guess": "spine",
"observation": "S P I N E\nG G X X G\n\nYou have 1 guess left."
}
},
{
"reply": "[spine]",
"reasoning": null,
"call": {
"promptTokens": 378,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 331,
"finishReason": "stop"
},
"info": {
"guess": "spine",
"observation": "That guess was not accepted: 'spine' has already been guessed.\nIt did not use up a turn. You have 1 guess left."
}
},
{
"reply": "[spine]",
"reasoning": null,
"call": {
"promptTokens": 427,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 389,
"finishReason": "stop"
},
"info": {
"guess": "spine",
"observation": "That guess was not accepted: 'spine' has already been guessed.\nIt did not use up a turn. You have 1 guess left."
}
},
{
"reply": "[spine]",
"reasoning": null,
"call": {
"promptTokens": 476,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 386,
"finishReason": "stop"
},
"info": {
"guess": "spine",
"observation": "That guess was not accepted: 'spine' has already been guessed.\nIt did not use up a turn. You have 1 guess left."
}
},
{
"reply": "[spine]",
"reasoning": null,
"call": {
"promptTokens": 525,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 379,
"finishReason": "stop"
},
"info": {
"guess": "spine",
"observation": "That guess was not accepted: 'spine' has already been guessed.\nIt did not use up a turn. You have 1 guess left."
}
},
{
"reply": "[spine]",
"reasoning": null,
"call": {
"promptTokens": 574,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 334,
"finishReason": "stop"
},
"info": {
"guess": "spine",
"observation": "That guess was not accepted: 'spine' has already been guessed.\nIt did not use up a turn. You have 1 guess left."
}
}
]
}
+188
View File
@@ -0,0 +1,188 @@
{
"runId": "base-off-s3",
"seed": 3,
"model": "brain-qwen38-dspark",
"capturedAt": "2026-08-28",
"rewards": {
"solved": 0.0,
"economy": 0.0,
"consistency": 0.09090909090909091
},
"metrics": {
"guesses_used": 5.0,
"rejected_replies": 6.0,
"reference_depth": 3.0,
"turns_granted": 6.0,
"inconsistent_guesses": 5.0
},
"truncated": false,
"outcome": "failed",
"answer": "toady",
"turns": [
{
"reply": "[crane]",
"reasoning": null,
"call": {
"promptTokens": 147,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 393,
"finishReason": "stop"
},
"info": {
"guess": "crane",
"observation": "C R A N E\nX X G X X\n\nYou have 5 guesses left."
}
},
{
"reply": "[slate]",
"reasoning": null,
"call": {
"promptTokens": 184,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 557,
"finishReason": "stop"
},
"info": {
"guess": "slate",
"observation": "S L A T E\nX X G Y X\n\nYou have 4 guesses left."
}
},
{
"reply": "[plate]",
"reasoning": null,
"call": {
"promptTokens": 221,
"completionTokens": 4,
"reasoningTokens": null,
"durationMs": 620,
"finishReason": "stop"
},
"info": {
"guess": "plate",
"observation": "P L A T E\nX X G Y X\n\nYou have 3 guesses left."
}
},
{
"reply": "[crate]",
"reasoning": null,
"call": {
"promptTokens": 257,
"completionTokens": 4,
"reasoningTokens": null,
"durationMs": 9070,
"finishReason": "stop"
},
"info": {
"guess": "crate",
"observation": "C R A T E\nX X G Y X\n\nYou have 2 guesses left."
}
},
{
"reply": "[plate]",
"reasoning": null,
"call": {
"promptTokens": 293,
"completionTokens": 4,
"reasoningTokens": null,
"durationMs": 1037,
"finishReason": "stop"
},
"info": {
"guess": "plate",
"observation": "That guess was not accepted: 'plate' has already been guessed.\nIt did not use up a turn. You have 2 guesses left."
}
},
{
"reply": "[trape]",
"reasoning": null,
"call": {
"promptTokens": 340,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 1620,
"finishReason": "stop"
},
"info": {
"guess": "trape",
"observation": "T R A P E\nG X G X X\n\nYou have 1 guess left."
}
},
{
"reply": "[trape]",
"reasoning": null,
"call": {
"promptTokens": 377,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 985,
"finishReason": "stop"
},
"info": {
"guess": "trape",
"observation": "That guess was not accepted: 'trape' has already been guessed.\nIt did not use up a turn. You have 1 guess left."
}
},
{
"reply": "[trape]",
"reasoning": null,
"call": {
"promptTokens": 426,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 1105,
"finishReason": "stop"
},
"info": {
"guess": "trape",
"observation": "That guess was not accepted: 'trape' has already been guessed.\nIt did not use up a turn. You have 1 guess left."
}
},
{
"reply": "[trape]",
"reasoning": null,
"call": {
"promptTokens": 475,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 1094,
"finishReason": "stop"
},
"info": {
"guess": "trape",
"observation": "That guess was not accepted: 'trape' has already been guessed.\nIt did not use up a turn. You have 1 guess left."
}
},
{
"reply": "[trape]",
"reasoning": null,
"call": {
"promptTokens": 524,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 1349,
"finishReason": "stop"
},
"info": {
"guess": "trape",
"observation": "That guess was not accepted: 'trape' has already been guessed.\nIt did not use up a turn. You have 1 guess left."
}
},
{
"reply": "[trape]",
"reasoning": null,
"call": {
"promptTokens": 573,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 983,
"finishReason": "stop"
},
"info": {
"guess": "trape",
"observation": "That guess was not accepted: 'trape' has already been guessed.\nIt did not use up a turn. You have 1 guess left."
}
}
]
}
+188
View File
@@ -0,0 +1,188 @@
{
"runId": "base-off-s4",
"seed": 4,
"model": "brain-qwen38-dspark",
"capturedAt": "2026-08-28",
"rewards": {
"solved": 0.0,
"economy": 0.0,
"consistency": 0.09090909090909091
},
"metrics": {
"guesses_used": 5.0,
"rejected_replies": 6.0,
"reference_depth": 4.0,
"turns_granted": 6.0,
"inconsistent_guesses": 5.0
},
"truncated": false,
"outcome": "failed",
"answer": "divot",
"turns": [
{
"reply": "[crane]",
"reasoning": null,
"call": {
"promptTokens": 147,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 952,
"finishReason": "stop"
},
"info": {
"guess": "crane",
"observation": "C R A N E\nX X X X X\n\nYou have 5 guesses left."
}
},
{
"reply": "[slith]",
"reasoning": null,
"call": {
"promptTokens": 184,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 1501,
"finishReason": "stop"
},
"info": {
"guess": "slith",
"observation": "That guess was not accepted: 'slith' is not in the word list.\nIt did not use up a turn. You have 5 guesses left."
}
},
{
"reply": "[slant]",
"reasoning": null,
"call": {
"promptTokens": 235,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 1363,
"finishReason": "stop"
},
"info": {
"guess": "slant",
"observation": "S L A N T\nX X X X G\n\nYou have 4 guesses left."
}
},
{
"reply": "[roast]",
"reasoning": null,
"call": {
"promptTokens": 272,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 1609,
"finishReason": "stop"
},
"info": {
"guess": "roast",
"observation": "R O A S T\nX Y X X G\n\nYou have 3 guesses left."
}
},
{
"reply": "[toast]",
"reasoning": null,
"call": {
"promptTokens": 309,
"completionTokens": 4,
"reasoningTokens": null,
"durationMs": 1240,
"finishReason": "stop"
},
"info": {
"guess": "toast",
"observation": "T O A S T\nX Y X X G\n\nYou have 2 guesses left."
}
},
{
"reply": "[plout]",
"reasoning": null,
"call": {
"promptTokens": 345,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 1613,
"finishReason": "stop"
},
"info": {
"guess": "plout",
"observation": "That guess was not accepted: 'plout' is not in the word list.\nIt did not use up a turn. You have 2 guesses left."
}
},
{
"reply": "[postt]",
"reasoning": null,
"call": {
"promptTokens": 396,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 1349,
"finishReason": "stop"
},
"info": {
"guess": "postt",
"observation": "That guess was not accepted: 'postt' is not in the word list.\nIt did not use up a turn. You have 2 guesses left."
}
},
{
"reply": "[roost]",
"reasoning": null,
"call": {
"promptTokens": 447,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 868,
"finishReason": "stop"
},
"info": {
"guess": "roost",
"observation": "R O O S T\nX Y X X G\n\nYou have 1 guess left."
}
},
{
"reply": "[plout]",
"reasoning": null,
"call": {
"promptTokens": 484,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 946,
"finishReason": "stop"
},
"info": {
"guess": "plout",
"observation": "That guess was not accepted: 'plout' is not in the word list.\nIt did not use up a turn. You have 1 guess left."
}
},
{
"reply": "[toast]",
"reasoning": null,
"call": {
"promptTokens": 535,
"completionTokens": 4,
"reasoningTokens": null,
"durationMs": 498,
"finishReason": "stop"
},
"info": {
"guess": "toast",
"observation": "That guess was not accepted: 'toast' has already been guessed.\nIt did not use up a turn. You have 1 guess left."
}
},
{
"reply": "[most]",
"reasoning": null,
"call": {
"promptTokens": 582,
"completionTokens": 4,
"reasoningTokens": null,
"durationMs": 490,
"finishReason": "stop"
},
"info": {
"guess": "most",
"observation": "That guess was not accepted: 'most' is not 5 letters.\nIt did not use up a turn. You have 1 guess left."
}
}
]
}
+143
View File
@@ -0,0 +1,143 @@
{
"runId": "base-off-s5",
"seed": 5,
"model": "brain-qwen38-dspark",
"capturedAt": "2026-08-28",
"rewards": {
"solved": 0.0,
"economy": 0.0,
"consistency": 0.125
},
"metrics": {
"guesses_used": 6.0,
"rejected_replies": 2.0,
"reference_depth": 4.0,
"turns_granted": 6.0,
"inconsistent_guesses": 5.0
},
"truncated": false,
"outcome": "failed",
"answer": "filly",
"turns": [
{
"reply": "[slate]",
"reasoning": null,
"call": {
"promptTokens": 147,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 392,
"finishReason": "stop"
},
"info": {
"guess": "slate",
"observation": "S L A T E\nX Y X X X\n\nYou have 5 guesses left."
}
},
{
"reply": "[crane]",
"reasoning": null,
"call": {
"promptTokens": 184,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 555,
"finishReason": "stop"
},
"info": {
"guess": "crane",
"observation": "C R A N E\nX X X X X\n\nYou have 4 guesses left."
}
},
{
"reply": "[plaid]",
"reasoning": null,
"call": {
"promptTokens": 221,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 496,
"finishReason": "stop"
},
"info": {
"guess": "plaid",
"observation": "P L A I D\nX Y X Y X\n\nYou have 3 guesses left."
}
},
{
"reply": "[blint]",
"reasoning": null,
"call": {
"promptTokens": 258,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 491,
"finishReason": "stop"
},
"info": {
"guess": "blint",
"observation": "That guess was not accepted: 'blint' is not in the word list.\nIt did not use up a turn. You have 3 guesses left."
}
},
{
"reply": "[plaid]",
"reasoning": null,
"call": {
"promptTokens": 309,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 443,
"finishReason": "stop"
},
"info": {
"guess": "plaid",
"observation": "That guess was not accepted: 'plaid' has already been guessed.\nIt did not use up a turn. You have 3 guesses left."
}
},
{
"reply": "[plume]",
"reasoning": null,
"call": {
"promptTokens": 358,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 499,
"finishReason": "stop"
},
"info": {
"guess": "plume",
"observation": "P L U M E\nX Y X X X\n\nYou have 2 guesses left."
}
},
{
"reply": "[climb]",
"reasoning": null,
"call": {
"promptTokens": 395,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 603,
"finishReason": "stop"
},
"info": {
"guess": "climb",
"observation": "C L I M B\nX Y Y X X\n\nYou have 1 guess left."
}
},
{
"reply": "[slimy]",
"reasoning": null,
"call": {
"promptTokens": 432,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 555,
"finishReason": "stop"
},
"info": {
"guess": "slimy",
"observation": "S L I M Y\nX Y Y X G\n\nYou have 0 guesses left."
}
}
]
}
+128
View File
@@ -0,0 +1,128 @@
{
"runId": "base-off-s6",
"seed": 6,
"model": "brain-qwen38-dspark",
"capturedAt": "2026-08-28",
"rewards": {
"solved": 0.0,
"economy": 0.0,
"consistency": 0.14285714285714285
},
"metrics": {
"guesses_used": 6.0,
"rejected_replies": 1.0,
"reference_depth": 4.0,
"turns_granted": 6.0,
"inconsistent_guesses": 5.0
},
"truncated": false,
"outcome": "failed",
"answer": "bobby",
"turns": [
{
"reply": "[crane]",
"reasoning": null,
"call": {
"promptTokens": 147,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 381,
"finishReason": "stop"
},
"info": {
"guess": "crane",
"observation": "C R A N E\nX X X X X\n\nYou have 5 guesses left."
}
},
{
"reply": "[slite]",
"reasoning": null,
"call": {
"promptTokens": 184,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 555,
"finishReason": "stop"
},
"info": {
"guess": "slite",
"observation": "That guess was not accepted: 'slite' is not in the word list.\nIt did not use up a turn. You have 5 guesses left."
}
},
{
"reply": "[slate]",
"reasoning": null,
"call": {
"promptTokens": 235,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 503,
"finishReason": "stop"
},
"info": {
"guess": "slate",
"observation": "S L A T E\nX X X X X\n\nYou have 4 guesses left."
}
},
{
"reply": "[robin]",
"reasoning": null,
"call": {
"promptTokens": 272,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 607,
"finishReason": "stop"
},
"info": {
"guess": "robin",
"observation": "R O B I N\nX G G X X\n\nYou have 3 guesses left."
}
},
{
"reply": "[boomy]",
"reasoning": null,
"call": {
"promptTokens": 309,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 555,
"finishReason": "stop"
},
"info": {
"guess": "boomy",
"observation": "B O O M Y\nG G X X G\n\nYou have 2 guesses left."
}
},
{
"reply": "[booth]",
"reasoning": null,
"call": {
"promptTokens": 346,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 385,
"finishReason": "stop"
},
"info": {
"guess": "booth",
"observation": "B O O T H\nG G X X X\n\nYou have 1 guess left."
}
},
{
"reply": "[booby]",
"reasoning": null,
"call": {
"promptTokens": 383,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 446,
"finishReason": "stop"
},
"info": {
"guess": "booby",
"observation": "B O O B Y\nG G X G G\n\nYou have 0 guesses left."
}
}
]
}
+128
View File
@@ -0,0 +1,128 @@
{
"runId": "base-off-s7",
"seed": 7,
"model": "brain-qwen38-dspark",
"capturedAt": "2026-08-28",
"rewards": {
"solved": 0.0,
"economy": 0.0,
"consistency": 0.14285714285714285
},
"metrics": {
"guesses_used": 6.0,
"rejected_replies": 1.0,
"reference_depth": 3.0,
"turns_granted": 6.0,
"inconsistent_guesses": 5.0
},
"truncated": false,
"outcome": "failed",
"answer": "clews",
"turns": [
{
"reply": "[crane]",
"reasoning": null,
"call": {
"promptTokens": 147,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 254,
"finishReason": "stop"
},
"info": {
"guess": "crane",
"observation": "C R A N E\nG X X X Y\n\nYou have 5 guesses left."
}
},
{
"reply": "[prize]",
"reasoning": null,
"call": {
"promptTokens": 184,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 557,
"finishReason": "stop"
},
"info": {
"guess": "prize",
"observation": "P R I Z E\nX X X X Y\n\nYou have 4 guesses left."
}
},
{
"reply": "[crepe]",
"reasoning": null,
"call": {
"promptTokens": 221,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 607,
"finishReason": "stop"
},
"info": {
"guess": "crepe",
"observation": "C R E P E\nG X G X X\n\nYou have 3 guesses left."
}
},
{
"reply": "[crave]",
"reasoning": null,
"call": {
"promptTokens": 258,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 488,
"finishReason": "stop"
},
"info": {
"guess": "crave",
"observation": "C R A V E\nG X X X Y\n\nYou have 2 guesses left."
}
},
{
"reply": "[credo]",
"reasoning": null,
"call": {
"promptTokens": 295,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 553,
"finishReason": "stop"
},
"info": {
"guess": "credo",
"observation": "C R E D O\nG X G X X\n\nYou have 1 guess left."
}
},
{
"reply": "[crepe]",
"reasoning": null,
"call": {
"promptTokens": 332,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 492,
"finishReason": "stop"
},
"info": {
"guess": "crepe",
"observation": "That guess was not accepted: 'crepe' has already been guessed.\nIt did not use up a turn. You have 1 guess left."
}
},
{
"reply": "[creed]",
"reasoning": null,
"call": {
"promptTokens": 381,
"completionTokens": 5,
"reasoningTokens": null,
"durationMs": 444,
"finishReason": "stop"
},
"info": {
"guess": "creed",
"observation": "C R E E D\nG X G X X\n\nYou have 0 guesses left."
}
}
]
}
+83
View File
@@ -0,0 +1,83 @@
{
"runId": "solver-s0",
"seed": 0,
"model": "entropy-solver",
"capturedAt": "2026-08-28",
"rewards": {
"solved": 1.0,
"economy": 1.0,
"consistency": 0.5
},
"metrics": {
"guesses_used": 4.0,
"rejected_replies": 0.0,
"reference_depth": 4.0,
"turns_granted": 6.0,
"inconsistent_guesses": 2.0
},
"truncated": false,
"outcome": "solved",
"answer": "wants",
"turns": [
{
"reply": "[tares]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "tares",
"observation": "T A R E S\nY G X X G\n\nYou have 5 guesses left."
}
},
{
"reply": "[filch]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "filch",
"observation": "F I L C H\nX X X X X\n\nYou have 4 guesses left."
}
},
{
"reply": "[spams]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "spams",
"observation": "S P A M S\nX X Y X G\n\nYou have 3 guesses left."
}
},
{
"reply": "[wants]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "wants",
"observation": "W A N T S\nG G G G G\n\nYou have 2 guesses left."
}
}
]
}
+68
View File
@@ -0,0 +1,68 @@
{
"runId": "solver-s1",
"seed": 1,
"model": "entropy-solver",
"capturedAt": "2026-08-28",
"rewards": {
"solved": 1.0,
"economy": 1.0,
"consistency": 0.6666666666666666
},
"metrics": {
"guesses_used": 3.0,
"rejected_replies": 0.0,
"reference_depth": 3.0,
"turns_granted": 6.0,
"inconsistent_guesses": 1.0
},
"truncated": false,
"outcome": "solved",
"answer": "amber",
"turns": [
{
"reply": "[tares]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "tares",
"observation": "T A R E S\nX Y Y G X\n\nYou have 5 guesses left."
}
},
{
"reply": "[blend]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "blend",
"observation": "B L E N D\nY X Y X X\n\nYou have 4 guesses left."
}
},
{
"reply": "[amber]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "amber",
"observation": "A M B E R\nG G G G G\n\nYou have 3 guesses left."
}
}
]
}
+83
View File
@@ -0,0 +1,83 @@
{
"runId": "solver-s2",
"seed": 2,
"model": "entropy-solver",
"capturedAt": "2026-08-28",
"rewards": {
"solved": 1.0,
"economy": 1.0,
"consistency": 0.75
},
"metrics": {
"guesses_used": 4.0,
"rejected_replies": 0.0,
"reference_depth": 4.0,
"turns_granted": 6.0,
"inconsistent_guesses": 1.0
},
"truncated": false,
"outcome": "solved",
"answer": "spume",
"turns": [
{
"reply": "[tares]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "tares",
"observation": "T A R E S\nX X X Y Y\n\nYou have 5 guesses left."
}
},
{
"reply": "[spoil]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "spoil",
"observation": "S P O I L\nG G X X X\n\nYou have 4 guesses left."
}
},
{
"reply": "[speck]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "speck",
"observation": "S P E C K\nG G Y X X\n\nYou have 3 guesses left."
}
},
{
"reply": "[spume]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "spume",
"observation": "S P U M E\nG G G G G\n\nYou have 2 guesses left."
}
}
]
}
+68
View File
@@ -0,0 +1,68 @@
{
"runId": "solver-s3",
"seed": 3,
"model": "entropy-solver",
"capturedAt": "2026-08-28",
"rewards": {
"solved": 1.0,
"economy": 1.0,
"consistency": 0.6666666666666666
},
"metrics": {
"guesses_used": 3.0,
"rejected_replies": 0.0,
"reference_depth": 3.0,
"turns_granted": 6.0,
"inconsistent_guesses": 1.0
},
"truncated": false,
"outcome": "solved",
"answer": "toady",
"turns": [
{
"reply": "[tares]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "tares",
"observation": "T A R E S\nG Y X X X\n\nYou have 5 guesses left."
}
},
{
"reply": "[bland]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "bland",
"observation": "B L A N D\nX X G X Y\n\nYou have 4 guesses left."
}
},
{
"reply": "[toady]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "toady",
"observation": "T O A D Y\nG G G G G\n\nYou have 3 guesses left."
}
}
]
}
+83
View File
@@ -0,0 +1,83 @@
{
"runId": "solver-s4",
"seed": 4,
"model": "entropy-solver",
"capturedAt": "2026-08-28",
"rewards": {
"solved": 1.0,
"economy": 1.0,
"consistency": 1.0
},
"metrics": {
"guesses_used": 4.0,
"rejected_replies": 0.0,
"reference_depth": 4.0,
"turns_granted": 6.0,
"inconsistent_guesses": 0.0
},
"truncated": false,
"outcome": "solved",
"answer": "divot",
"turns": [
{
"reply": "[tares]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "tares",
"observation": "T A R E S\nY X X X X\n\nYou have 5 guesses left."
}
},
{
"reply": "[count]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "count",
"observation": "C O U N T\nX Y X X G\n\nYou have 4 guesses left."
}
},
{
"reply": "[pivot]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "pivot",
"observation": "P I V O T\nX G G G G\n\nYou have 3 guesses left."
}
},
{
"reply": "[divot]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "divot",
"observation": "D I V O T\nG G G G G\n\nYou have 2 guesses left."
}
}
]
}
+83
View File
@@ -0,0 +1,83 @@
{
"runId": "solver-s5",
"seed": 5,
"model": "entropy-solver",
"capturedAt": "2026-08-28",
"rewards": {
"solved": 1.0,
"economy": 1.0,
"consistency": 0.75
},
"metrics": {
"guesses_used": 4.0,
"rejected_replies": 0.0,
"reference_depth": 4.0,
"turns_granted": 6.0,
"inconsistent_guesses": 1.0
},
"truncated": false,
"outcome": "solved",
"answer": "filly",
"turns": [
{
"reply": "[tares]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "tares",
"observation": "T A R E S\nX X X X X\n\nYou have 5 guesses left."
}
},
{
"reply": "[doily]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "doily",
"observation": "D O I L Y\nX X Y G G\n\nYou have 4 guesses left."
}
},
{
"reply": "[belch]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "belch",
"observation": "B E L C H\nX X G X X\n\nYou have 3 guesses left."
}
},
{
"reply": "[filly]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "filly",
"observation": "F I L L Y\nG G G G G\n\nYou have 2 guesses left."
}
}
]
}
+83
View File
@@ -0,0 +1,83 @@
{
"runId": "solver-s6",
"seed": 6,
"model": "entropy-solver",
"capturedAt": "2026-08-28",
"rewards": {
"solved": 1.0,
"economy": 1.0,
"consistency": 0.75
},
"metrics": {
"guesses_used": 4.0,
"rejected_replies": 0.0,
"reference_depth": 4.0,
"turns_granted": 6.0,
"inconsistent_guesses": 1.0
},
"truncated": false,
"outcome": "solved",
"answer": "bobby",
"turns": [
{
"reply": "[tares]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "tares",
"observation": "T A R E S\nX X X X X\n\nYou have 5 guesses left."
}
},
{
"reply": "[doily]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "doily",
"observation": "D O I L Y\nX G X X G\n\nYou have 4 guesses left."
}
},
{
"reply": "[knobs]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "knobs",
"observation": "K N O B S\nX X Y G X\n\nYou have 3 guesses left."
}
},
{
"reply": "[bobby]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "bobby",
"observation": "B O B B Y\nG G G G G\n\nYou have 2 guesses left."
}
}
]
}
+68
View File
@@ -0,0 +1,68 @@
{
"runId": "solver-s7",
"seed": 7,
"model": "entropy-solver",
"capturedAt": "2026-08-28",
"rewards": {
"solved": 1.0,
"economy": 1.0,
"consistency": 0.6666666666666666
},
"metrics": {
"guesses_used": 3.0,
"rejected_replies": 0.0,
"reference_depth": 3.0,
"turns_granted": 6.0,
"inconsistent_guesses": 1.0
},
"truncated": false,
"outcome": "solved",
"answer": "clews",
"turns": [
{
"reply": "[tares]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "tares",
"observation": "T A R E S\nX X X Y G\n\nYou have 5 guesses left."
}
},
{
"reply": "[wield]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "wield",
"observation": "W I E L D\nY X G Y X\n\nYou have 4 guesses left."
}
},
{
"reply": "[clews]",
"reasoning": null,
"call": {
"promptTokens": null,
"completionTokens": null,
"reasoningTokens": null,
"durationMs": null,
"finishReason": "generated"
},
"info": {
"guess": "clews",
"observation": "C L E W S\nG G G G G\n\nYou have 3 guesses left."
}
}
]
}
+58 -18
View File
@@ -294,7 +294,7 @@ function codeSpans(src) {
* @param {'{' | '['} open * @param {'{' | '['} open
* @returns {string | null} * @returns {string | null}
*/ */
export function literalAfter(src, anchor, open = '{') { export function literalsAfter(src, anchor, open = '{') {
const flags = anchor.flags.includes('g') ? anchor.flags : `${anchor.flags}g`; const flags = anchor.flags.includes('g') ? anchor.flags : `${anchor.flags}g`;
const re = new RegExp(anchor.source, flags); const re = new RegExp(anchor.source, flags);
const spans = codeSpans(src); const spans = codeSpans(src);
@@ -302,18 +302,29 @@ export function literalAfter(src, anchor, open = '{') {
const hit = spans.find((s) => offset >= s.start && offset < s.end); const hit = spans.find((s) => offset >= s.start && offset < s.end);
return hit ? hit.code : true; return hit ? hit.code : true;
}; };
const found = [];
let match; let match;
while ((match = re.exec(src)) !== null) { while ((match = re.exec(src)) !== null) {
// The anchor may legally match inside a comment or a string a docblock // The anchor may legally match inside a comment or a string \u2014 a docblock
// that quotes `const meta = {`. Only a match in real code counts. // that quotes `const meta = {`. Only a match in real code counts.
if (!isCode(match.index)) continue; if (!isCode(match.index)) continue;
const from = src.indexOf(open, match.index + Math.max(match[0].length - 1, 0)); const from = src.indexOf(open, match.index + Math.max(match[0].length - 1, 0));
if (from === -1) continue; if (from === -1) continue;
const between = src.slice(match.index + match[0].length, from); const between = src.slice(match.index + match[0].length, from);
if (/[;=}]/.test(between)) continue; if (/[;=}]/.test(between)) continue;
return src.slice(from, scanBalanced(src, from)); const end = scanBalanced(src, from);
found.push({ text: src.slice(from, end), start: from, end });
// Skip past this literal so a nested anchor of the same name is not
// reported as a second, overlapping hit.
re.lastIndex = end;
} }
return null; return found;
}
/** First literal following `anchor`, or null. See {@link literalsAfter}. */
export function literalAfter(src, anchor, open = '{') {
const [first] = literalsAfter(src, anchor, open);
return first ? first.text : null;
} }
/** Marker key on the stand-in an unresolvable identifier evaluates to. */ /** Marker key on the stand-in an unresolvable identifier evaluates to. */
@@ -501,7 +512,10 @@ const VERTICAL_ANCHORS = [
const asVertical = (id, v) => ({ const asVertical = (id, v) => ({
id, id,
title: v?.title ?? v?.label ?? v?.name ?? null, title: v?.title ?? v?.label ?? v?.name ?? null,
description: v?.description ?? v?.blurb ?? v?.tagline ?? v?.summary ?? null, // `anxiety` is this repo's field for the one line a vertical page leads
// with, and it is the only prose in a VerticalEntry short enough to be a
// meta description.
description: v?.description ?? v?.blurb ?? v?.tagline ?? v?.summary ?? v?.anxiety ?? null,
}); });
/** /**
@@ -559,8 +573,14 @@ export function loadManifest() {
} }
/** @type {Map<string, {runs: any[], extras: Record<string, any>}>} */ /** @type {Map<string, {runs: any[], extras: Record<string, any>}>} */
const byDemo = new Map(); const byDemo = new Map();
const container = // Unwrap the two envelopes the shell's own reader accepts. `{runs: [...]}`
raw && typeof raw === 'object' && !Array.isArray(raw) && raw.demos && typeof raw.demos === 'object' ? raw.demos : raw; // has to be unwrapped BEFORE the record branch below, or "runs" is read as a
// demo slug and every real demo reports zero runs.
let container = raw;
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
if (raw.demos && typeof raw.demos === 'object') container = raw.demos;
else if (Array.isArray(raw.runs)) container = raw.runs;
}
if (Array.isArray(container)) { if (Array.isArray(container)) {
// A flat array of runs, each carrying its own `demo`/`slug`. // A flat array of runs, each carrying its own `demo`/`slug`.
@@ -706,12 +726,32 @@ export function discoverRoutePatterns() {
return [...patterns]; return [...patterns];
} }
const DEMO_PARAMS = /^(slug|demo|demoSlug)$/; /**
const VERTICAL_PARAMS = /^(vertical|verticalId|sector|category)$/; * Which set fills a pattern's one parameter.
*
* Decided by the STATIC segment in front of it, not by the parameter's name.
* This router calls both of them `:slug` — `demos/:slug` and `verticals/:slug`
* — so a name-based reader confidently prerenders `/verticals/wordle`, twelve
* 404s, and no vertical pages at all.
*/
function fillFor(pattern) {
const head = pattern.replace(/^\/+/, '').split('/')[0]?.toLowerCase() ?? '';
if (/^(demos?|d)$/.test(head)) return 'demo';
if (/^(verticals?|v|sectors?)$/.test(head)) return 'vertical';
const param = pattern.match(/:([A-Za-z0-9_]+)/)?.[1] ?? '';
if (/^(vertical|verticalId|sector|category)$/.test(param)) return 'vertical';
if (/^(slug|demo|demoSlug|id)$/.test(param)) return 'demo';
return null;
}
/** /**
* Expands the router's patterns against the real data into concrete paths. * Expands the router's patterns against the real data into concrete paths.
* *
* Child paths are taken as written. This router nests everything exactly one
* level under `/`, so a child's `path` is already the full path; a second level
* of nesting would need the parent prefix joined on, and this would silently
* emit the wrong routes rather than fail. If you nest deeper, fix this.
*
* @param {{metas: Map<string, any>, verticals: {id: string}[]}} data * @param {{metas: Map<string, any>, verticals: {id: string}[]}} data
* @returns {{routes: {path: string, kind: string, slug?: string, id?: string}[], errors: string[]}} * @returns {{routes: {path: string, kind: string, slug?: string, id?: string}[], errors: string[]}}
*/ */
@@ -749,18 +789,18 @@ export function expandRoutes(data) {
errors.push(`route pattern "${pattern}" has more than one parameter; prerender cannot expand it.`); errors.push(`route pattern "${pattern}" has more than one parameter; prerender cannot expand it.`);
continue; continue;
} }
const param = params[0]; const fill = fillFor(pattern);
const fill = (value, record) => add(pattern.replace(/:[A-Za-z0-9_]+\??/, value), record); const substitute = (value) => pattern.replace(/:[A-Za-z0-9_]+\??/, value);
if (VERTICAL_PARAMS.test(param)) { if (fill === 'vertical') {
if (!verticalIds.length) errors.push(`route "${pattern}" needs verticals, but none were readable from src/content/verticals.ts.`); if (!verticalIds.length) errors.push(`route "${pattern}" needs verticals, but none were readable from ${rel(VERTICALS_FILE)}.`);
for (const id of verticalIds) fill(id, { kind: 'vertical', id }); for (const id of verticalIds) add(substitute(id), { kind: 'vertical', id });
} else if (DEMO_PARAMS.test(param) || param === 'id') { } else if (fill === 'demo') {
if (!demoSlugList.length) errors.push(`route "${pattern}" needs demos, but no demo meta was readable under src/demos/.`); if (!demoSlugList.length) errors.push(`route "${pattern}" needs demos, but no demo meta was readable under src/demos/.`);
for (const slug of demoSlugList) fill(slug, { kind: 'demo', slug }); for (const slug of demoSlugList) add(substitute(slug), { kind: 'demo', slug });
} else { } else {
errors.push( errors.push(
`route pattern "${pattern}" uses parameter ":${param}", which prerender cannot fill. ` + `route pattern "${pattern}" has a parameter prerender cannot fill. Put it under /demos/ or ` +
'Name it :slug (a demo) or :vertical (a vertical), or teach scripts/_lib.mjs about it.', '/verticals/, or teach fillFor() in scripts/_lib.mjs about it.',
); );
} }
} }
+597
View File
@@ -0,0 +1,597 @@
#!/usr/bin/env node
/**
* The demo contract validator.
*
* `src/lib/demo-kit/types.ts` says what a demo is. TypeScript enforces the
* shapes; this enforces everything a type cannot: that the directory name
* matches the slug, that a `spec` demo is a real published specification rather
* than a coming-soon card, that a `live` demo's traces exist on disk, that the
* shared shell has no idea any particular demo exists, and that the reward has
* something pulling against its objective.
*
* Thirteen numbered rules, each reported with the file to open. Run it with
* `pnpm check`.
*
* Most rules are checked by READING the TypeScript, not by running it — the
* demos import React, `?raw` Python and the Vite `@/` alias, none of which
* survive a bare `node` import. Rule 13 is the exception: it tries the adapter
* for real under tsx first, and says so in the output when it had to settle for
* reading the source.
*/
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
import {
DEMOS_DIR,
Report,
abs,
allDemoDirs,
demoFiles,
demoSlugs,
die,
exists,
findInDemo,
isUnresolved,
loadManifest,
loadMeta,
parseStringUnion,
read,
rel,
traceFile,
walk,
} from './_lib.mjs';
const report = new Report('check-demos');
/* ------------------------------------------------ the contract, read once */
const TYPES_FILE = abs('src', 'lib', 'demo-kit', 'types.ts');
if (!exists(TYPES_FILE)) die(`${rel(TYPES_FILE)} is missing. It is the contract; there is nothing to check against.`);
const typesSrc = read(TYPES_FILE);
// Parsed out of types.ts rather than restated here. A second copy of this list
// would drift, and it would drift silently in the direction of passing.
const VERTICALS = parseStringUnion(typesSrc, 'Vertical');
const STATUSES = parseStringUnion(typesSrc, 'DemoStatus');
const ROLES = ['objective', 'counterweight', 'gate'];
if (!VERTICALS) die(`could not parse the \`Vertical\` union out of ${rel(TYPES_FILE)}.`);
if (!STATUSES) die(`could not parse the \`DemoStatus\` union out of ${rel(TYPES_FILE)}.`);
/**
* Every icon name lucide-react actually exports, from its own type
* declarations. "Plausible PascalCase" would accept `Grid3X3`, which compiles
* to `undefined` and renders as a hole in the header.
*/
const LUCIDE_ICONS = (() => {
const dts = abs('node_modules', 'lucide-react', 'dist', 'lucide-react.d.ts');
if (!exists(dts)) return null;
const names = new Set();
for (const m of read(dts).matchAll(/declare const ([A-Za-z][A-Za-z0-9_]*)\s*:/g)) names.add(m[1]);
return names.size > 100 ? names : null;
})();
const nonEmptyString = (v) => typeof v === 'string' && v.trim().length > 0;
/** field -> validator returning `true` or the reason it is wrong. */
const META_FIELDS = {
slug: (v) => (/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(String(v)) ? true : 'must be a non-empty kebab-case string'),
title: (v) => (nonEmptyString(v) ? true : 'must be a non-empty string'),
tagline: (v) => (nonEmptyString(v) ? true : 'must be a non-empty string'),
vertical: (v) => (VERTICALS.includes(v) ? true : `must be one of the Vertical union: ${VERTICALS.join(', ')}`),
status: (v) => (STATUSES.includes(v) ? true : `must be one of the DemoStatus union: ${STATUSES.join(', ')}`),
order: (v) => (typeof v === 'number' && Number.isFinite(v) ? true : 'must be a finite number'),
icon: (v) => {
if (!nonEmptyString(v)) return 'must be a non-empty string';
if (!/^[A-Z][A-Za-z0-9]*$/.test(v)) return `"${v}" is not PascalCase, so it is not a lucide export name`;
if (LUCIDE_ICONS && !LUCIDE_ICONS.has(v)) return `lucide-react does not export "${v}" (check the exact casing at lucide.dev)`;
return true;
},
persona: (v) => (nonEmptyString(v) ? true : 'must be a non-empty string'),
rewardLine: (v) => (nonEmptyString(v) ? true : 'must be a non-empty string'),
ogImage: (v) => (nonEmptyString(v) && v.startsWith('/') ? true : "must be a site-absolute path, e.g. '/og/wordle.png'"),
};
/* --------------------------------------------------------- literal anchors */
const COMPONENT_ANCHORS = [/\bcomponents\s*:\s*/];
const ANATOMY_ANCHORS = [/(?:export\s+)?const\s+anatomy\s*(?::\s*[^=]+)?=\s*/, /\banatomy\s*:\s*/];
const PROVENANCE_ANCHORS = [
/(?:export\s+)?const\s+provenance\s*(?::\s*[^=]+)?=\s*/,
/\bprovenance\s*:\s*/,
];
/* ------------------------------------------------------------- the checks */
const slugs = demoSlugs();
if (slugs.length === 0) die(`no demos found under ${rel(DEMOS_DIR)}. A repo with no demos has nothing to validate.`);
const manifest = loadManifest();
for (const slug of slugs) {
const dir = path.join(DEMOS_DIR, slug);
const found = loadMeta(slug);
if ('error' in found) {
report.fail(found.file, 'rule 2 (DemoMeta present)', found.error);
continue;
}
const { meta, file: metaFile } = found;
/* -- 1. directory name is the slug ------------------------------------- */
report.check(
meta.slug === slug,
metaFile,
'rule 1 (slug is the directory name)',
`meta.slug is ${JSON.stringify(meta.slug)} but the directory is ${JSON.stringify(slug)}. ` +
'The registry, the route and the OG card are all keyed on this; rename one of them.',
);
/* -- 2. every required field, correctly typed ---------------------------- */
for (const [field, validate] of Object.entries(META_FIELDS)) {
if (!(field in meta)) {
report.fail(metaFile, 'rule 2 (DemoMeta complete)', `\`${field}\` is missing.`);
continue;
}
const verdict = validate(meta[field]);
report.check(verdict === true, metaFile, 'rule 2 (DemoMeta complete)', `\`${field}\`: ${verdict}`);
}
if (!LUCIDE_ICONS) {
report.staticOnly(`${slug}: lucide-react is not installed, so \`icon\` was only checked for PascalCase shape.`);
}
/* -- 4. the social card exists ------------------------------------------ */
if (nonEmptyString(meta.ogImage)) {
const card = abs('public', String(meta.ogImage).replace(/^\/+/, ''));
report.check(
exists(card),
metaFile,
'rule 4 (ogImage exists)',
`meta.ogImage points at ${meta.ogImage}, which is ${rel(card)} on disk, and that file does not exist. ` +
'Generate it with `node scripts/og.mjs` (x86 only — it needs Playwright chromium).',
);
}
/* -- 5. demo.tsx default-exports via defineDemo ------------------------- */
const demoFile = path.join(dir, 'demo.tsx');
if (!exists(demoFile)) {
report.fail(rel(demoFile), 'rule 5 (defineDemo default export)', 'demo.tsx does not exist.');
} else {
const src = read(demoFile);
report.check(
/export\s+default\s+defineDemo\s*(?:<[^>]*>)?\s*\(/.test(src),
rel(demoFile),
'rule 5 (defineDemo default export)',
'no `export default defineDemo(...)` found. The registry loads demos through defineDemo; ' +
'a bare object default export skips whatever the kit validates.',
);
report.check(
/\bdefineDemo\b[\s\S]*?from\s*'@\/lib\/demo-kit'/.test(src) || /from\s*'@\/lib\/demo-kit'[\s\S]*?\bdefineDemo\b/.test(src),
rel(demoFile),
'rule 5 (defineDemo default export)',
"defineDemo must be imported from '@/lib/demo-kit'.",
);
}
/* -- 6/10. the specification and the reward ----------------------------- */
const anatomy = findInDemo(slug, ANATOMY_ANCHORS, '{', 'anatomy', { lenient: true });
const provenance = findInDemo(slug, PROVENANCE_ANCHORS, '{', 'provenance', { lenient: true });
const components = findInDemo(slug, COMPONENT_ANCHORS, '[', 'reward.components', { lenient: true });
const specRule = 'rule 6 (complete specification)';
if (anatomy.error) {
report.fail(anatomy.file ?? rel(dir), specRule, anatomy.error);
} else {
for (const field of ['task', 'actions', 'grader', 'score']) {
report.check(
nonEmptyString(anatomy.value?.[field]),
anatomy.file,
specRule,
`anatomy.${field} is empty. A ${meta.status} demo publishes the four boxes in full; ` +
'an empty one reads as a placeholder and undoes the page it sits on.',
);
}
}
if (provenance.error) {
report.fail(provenance.file ?? rel(dir), specRule, provenance.error);
} else {
report.check(
nonEmptyString(provenance.value?.command),
provenance.file,
specRule,
'provenance.command is empty. The eval command is the reader\'s way to disprove the page; ' +
'it is the one field that must never be aspirational.',
);
}
const weightRule = 'rule 10 (reward weights)';
if (components.error) {
report.fail(components.file ?? rel(dir), weightRule, components.error);
} else if (!Array.isArray(components.value) || components.value.length === 0) {
report.fail(components.file, weightRule, 'reward.components is not a non-empty array.');
} else {
const list = components.value;
let sum = 0;
let readable = true;
list.forEach((component, i) => {
const where = `reward.components[${i}]${component?.key ? ` (${component.key})` : ''}`;
for (const field of ['key', 'label', 'description']) {
report.check(
nonEmptyString(component?.[field]),
components.file,
weightRule,
`${where}: \`${field}\` is empty.`,
);
}
if (typeof component?.weight !== 'number' || !Number.isFinite(component.weight)) {
readable = false;
report.fail(
components.file,
weightRule,
`${where}: \`weight\` is ${isUnresolved(component?.weight) ? 'an imported constant' : JSON.stringify(component?.weight)}, ` +
'not a literal number. Weights are read off the page beside the code, so they are written out here.',
);
} else {
sum += component.weight;
}
report.check(
ROLES.includes(component?.role),
components.file,
weightRule,
`${where}: \`role\` must be one of ${ROLES.join(', ')}.`,
);
});
if (readable) {
report.check(
Math.abs(sum - 1) <= 1e-9,
components.file,
weightRule,
`the weights sum to ${sum} and must sum to 1.0 (within 1e-9). ` +
'A reward whose weights do not sum to one is not the reward the page shows you tuning.',
);
}
report.check(
list.some((c) => c?.role === 'counterweight'),
components.file,
'rule 10 (counterweight required)',
'no component has role "counterweight". An objective with nothing pulling against it is a metric to ' +
'game, and the demo exists to show the opposite. If the second term is one every good policy also ' +
'scores 1.0 on, it is a gate — and the demo still needs a real counterweight.',
);
const counterweight = list.find((c) => c?.role === 'counterweight');
if (counterweight) {
report.check(
nonEmptyString(counterweight.description),
components.file,
specRule,
`the counterweight (${counterweight.key}) has no description. It is the half of the reward a reader ` +
'does not expect, so it is the half that must be spelled out.',
);
}
}
/* -- 7/11/12. recorded runs --------------------------------------------- */
if (meta.status === 'live') {
const runRule = 'rule 7 (live demo has runs)';
if (manifest.error) {
report.fail(rel(abs('public', 'traces', 'manifest.json')), runRule, manifest.error);
} else {
const entry = manifest.byDemo.get(slug);
const runs = entry?.runs ?? [];
if (runs.length === 0) {
report.fail(
rel(abs('public', 'traces', 'manifest.json')),
runRule,
`demo "${slug}" is status "live" but has no runs in the manifest. ` +
'A live demo asserts recorded evidence; without a run there is nothing to replay.',
);
} else {
const episodes = [];
for (const run of runs) {
const label = `run ${run?.id ?? '(no id)'}`;
if (!nonEmptyString(run?.path)) {
report.fail(rel(abs('public', 'traces', 'manifest.json')), runRule, `${label}: \`path\` is empty.`);
continue;
}
const file = traceFile(run.path);
if (!report.check(exists(file), rel(file), runRule, `${label} points at ${run.path}, which does not exist on disk.`)) {
continue;
}
try {
const raw = JSON.parse(read(file));
for (const ep of Array.isArray(raw) ? raw : Array.isArray(raw?.episodes) ? raw.episodes : [raw]) {
episodes.push({ run, file, episode: ep });
}
} catch (error) {
report.fail(rel(file), runRule, `not valid JSON: ${error.message}`);
}
/* -- 12. an intervened run must say what was done to it ---------- */
if (run?.kind === 'intervened') {
report.check(
nonEmptyString(run.intervention),
rel(abs('public', 'traces', 'manifest.json')),
'rule 12 (intervened runs declare the intervention)',
`${label} has kind "intervened" but no \`intervention\`. Without it a prompt change reads as a ` +
'training result by omission, which is the single most misleading thing this page could do.',
);
}
}
/* -- 11. a failure, or a solve rate the page renders -------------- */
const failedRun = episodes.find((e) => e.episode?.outcome === 'failed');
if (!failedRun && episodes.length) {
const rateSources = [entry?.extras ?? {}, ...runs];
const rate = rateSources
.map((s) => s?.solveRate ?? s?.solve_rate)
.find((v) => typeof v === 'number' && Number.isFinite(v));
const rendered = walk(abs('src'), (f) => /\.tsx?$/.test(f)).some((f) => /\bsolve[_R]?ate|solveRate|solve_rate/.test(read(f)));
report.check(
typeof rate === 'number' && rendered,
rel(abs('public', 'traces', 'manifest.json')),
'rule 11 (a clean sweep is reported, not hidden)',
`demo "${slug}" ships no run with outcome "failed". That is allowed only if the page states the ` +
'solve rate: add a numeric `solveRate` beside the runs in the manifest AND render it. ' +
`Right now solveRate is ${typeof rate === 'number' ? rate : 'absent'} and a renderer for it was ` +
`${rendered ? 'found' : 'not found'} under src/. Hunting for a losing seed to make the demo look ` +
'honest is the failure mode this rule exists to block.',
);
}
}
}
}
/* -- 13. every step announces itself ------------------------------------ */
checkAnnounce(slug, report, manifest);
}
/* ---------------------------------------- 8. the shell knows no demo names */
const SHELL_DIR = abs('src', 'components', 'demo');
for (const file of walk(SHELL_DIR, (f) => /\.(tsx?|css)$/.test(f))) {
const lines = read(file).split('\n');
for (const slug of allDemoDirs()) {
if (slug.startsWith('_')) continue;
// Word-boundary on both sides so a slug like `wordle` is not found inside
// an unrelated identifier, and so `wordle-five` matches as one token.
const needle = new RegExp(`(?<![\\w-])${slug.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w-])`);
const hit = lines.findIndex((line) => needle.test(line));
if (hit !== -1) {
report.fail(
`${rel(file)}:${hit + 1}`,
'rule 8 (the shell is generic)',
`mentions the demo slug "${slug}". Nothing under src/components/demo/ may know which demo it is ` +
'rendering. If the shell needs to special-case something, that is a missing slot on DemoModule or a ' +
'missing capability in demo-kit — fix it there and every future demo gets it too.',
);
} else {
report.passed += 1;
}
}
}
/* --------------------------------- 9. demos reach only for the kit's barrel */
const FORBIDDEN_IMPORTS = [
{
test: (spec) => /^@\/lib\/demo-kit\/.+/.test(spec),
why: "imports a file INSIDE demo-kit. Import '@/lib/demo-kit' — the barrel is the contract, and anything " +
'you can only get by reaching past it is either private or belongs in the barrel.',
},
{
test: (spec) => /^@\/components\/demo(\/|$)/.test(spec),
why: 'imports the shared shell. The shell renders demos; demos never render the shell. This is the ' +
'dependency that, once it exists, makes the shell impossible to change without opening every demo.',
},
];
for (const slug of allDemoDirs()) {
for (const file of demoFiles(slug)) {
for (const spec of importSpecifiers(read(file))) {
const normalised = normaliseSpecifier(spec, file);
for (const rule of FORBIDDEN_IMPORTS) {
if (rule.test(normalised)) {
report.fail(
rel(file),
'rule 9 (demos import only the demo-kit barrel)',
`\`${spec}\`${normalised === spec ? '' : ` (resolves to ${normalised})`} ${rule.why}`,
);
} else {
report.passed += 1;
}
}
}
}
}
report.finish();
/* ------------------------------------------------------------- helpers */
/** Every module specifier a file imports, static, dynamic or side-effect. */
function importSpecifiers(src) {
const out = new Set();
for (const m of src.matchAll(/\bfrom\s*['"]([^'"]+)['"]/g)) out.add(m[1]);
for (const m of src.matchAll(/\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g)) out.add(m[1]);
for (const m of src.matchAll(/^\s*import\s+['"]([^'"]+)['"]/gm)) out.add(m[1]);
return [...out];
}
/**
* Rewrites a relative specifier into its `@/` form.
*
* Without this, `../../components/demo/Player` walks straight past a lint that
* only looks at the alias — and it is the form an editor's auto-import
* produces, so it is the form the violation actually arrives in.
*/
function normaliseSpecifier(spec, fromFile) {
if (!spec.startsWith('.')) return spec;
const resolved = path.resolve(path.dirname(fromFile), spec);
const inSrc = path.relative(abs('src'), resolved);
if (inSrc.startsWith('..')) return spec;
return `@/${inSrc.split(path.sep).join('/')}`.replace(/\.(tsx?|jsx?)$/, '');
}
/**
* Rule 13: every `DemoStep` an adapter produces sets a non-empty `announce`.
*
* Reduced motion clamps the tile flip to nothing, so for a screen-reader user
* the announcement IS the result. A missing one is not a degraded experience,
* it is a blank page that claims to be showing you something.
*
* Tried dynamically first, under tsx, against the demo's own recorded traces —
* a static check cannot see through a helper that builds the step object. When
* the module will not load in bare Node (a `?raw` import, a Vite-only alias,
* JSX pulling in something browser-shaped) it falls back to reading the source
* and SAYS SO in the report rather than quietly downgrading.
*/
function checkAnnounce(slug, report, manifest) {
const rule = 'rule 13 (every step announces itself)';
const dir = path.join(DEMOS_DIR, slug);
const candidates = ['adapt.ts', 'adapter.ts', 'adapters.ts', 'adapt.tsx', 'demo.tsx']
.map((n) => path.join(dir, n))
.filter((p) => exists(p) && /\badapt\b\s*[:=(]|function\s+adapt\b/.test(read(p)));
if (candidates.length === 0) {
report.fail(
rel(dir),
rule,
'no `adapt` implementation found. DemoModule.adapt is required; it is what turns a recorded episode ' +
'into the steps the player renders.',
);
return;
}
const traces = (manifest.byDemo.get(slug)?.runs ?? [])
.map((run) => (run?.path ? traceFile(run.path) : null))
.filter((p) => p && exists(p));
if (traces.length > 0) {
for (const module of candidates) {
const result = probeAdapter(module, traces);
if (result.loaded) {
if (result.ok) {
report.passed += 1;
} else {
for (const problem of result.problems) report.fail(rel(module), rule, problem);
}
return;
}
}
}
// Static fallback. It cannot prove `announce` is set on every step, only that
// the adapter sets it at all and never sets it to nothing.
const module = candidates[0];
const src = read(module);
const assigns = /\bannounce\s*:/.test(src);
report.check(
assigns,
rel(module),
rule,
'the adapter never assigns `announce`. Every DemoStep must carry one — with motion reduced, it is the ' +
'only thing that reports the result.',
);
if (assigns) {
const empty = /\bannounce\s*:\s*(?:''|""|``|null|undefined)\s*[,}\n]/.test(src);
report.check(!empty, rel(module), rule, 'the adapter assigns an empty `announce` somewhere. An empty announcement is a missing one.');
}
report.staticOnly(
`${slug}: rule 13 was checked by reading ${rel(module)}, not by running the adapter` +
`${traces.length === 0 ? ' (no recorded traces to feed it)' : ' (the module would not load under tsx in bare Node)'}.`,
);
}
/** Runs one adapter over real episodes under tsx. Never throws. */
function probeAdapter(modulePath, traces) {
const tsx = abs('node_modules', '.bin', 'tsx');
if (!exists(tsx)) return { loaded: false, reason: 'tsx is not installed' };
const probe = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'pig-adapt-')), 'probe.mts');
fs.writeFileSync(probe, adapterProbeSource(), 'utf8');
try {
const run = spawnSync(process.execPath, [tsx, probe, modulePath, ...traces], {
cwd: abs('.'),
encoding: 'utf8',
timeout: 30_000,
// tsx resolves the `@/` alias from a tsconfig, and the probe lives in
// a temp dir where it would never find ours.
env: { ...process.env, TSX_TSCONFIG_PATH: abs('tsconfig.json') },
});
if (run.status !== 0 || !run.stdout) return { loaded: false, reason: (run.stderr || '').trim().split('\n').slice(-1)[0] };
const line = run.stdout.trim().split('\n').pop();
const parsed = JSON.parse(line);
return parsed.loaded === false ? { loaded: false, reason: parsed.reason } : { loaded: true, ...parsed };
} catch {
return { loaded: false, reason: 'the probe did not produce parseable output' };
} finally {
fs.rmSync(path.dirname(probe), { recursive: true, force: true });
}
}
/**
* The probe, written to a temp file and executed by tsx.
*
* It lives here as a string rather than as a checked-in `.mts` because it is an
* implementation detail of this script, and a stray TypeScript file in
* `scripts/` would get swept into the typecheck it is deliberately outside of.
*/
function adapterProbeSource() {
return `
import { readFileSync } from 'node:fs';
const [modulePath, ...traces] = process.argv.slice(2);
const say = (o: unknown) => console.log(JSON.stringify(o));
let mod: any;
try {
mod = await import(modulePath);
} catch (error: any) {
say({ loaded: false, reason: String(error?.message ?? error).split('\\n')[0] });
process.exit(0);
}
const adapt =
typeof mod.adapt === 'function' ? mod.adapt :
typeof mod.default?.adapt === 'function' ? mod.default.adapt :
typeof mod.default === 'function' ? mod.default : null;
if (!adapt) {
say({ loaded: false, reason: 'no callable \\\`adapt\\\` export' });
process.exit(0);
}
const problems: string[] = [];
let steps = 0;
for (const trace of traces) {
const raw = JSON.parse(readFileSync(trace, 'utf8'));
const episodes = Array.isArray(raw) ? raw : Array.isArray(raw?.episodes) ? raw.episodes : [raw];
for (const episode of episodes) {
let produced: any;
try {
produced = adapt(episode);
} catch (error: any) {
problems.push(trace + ': adapt() threw ' + String(error?.message ?? error).split('\\n')[0]);
continue;
}
if (!Array.isArray(produced)) {
problems.push(trace + ': adapt() returned ' + typeof produced + ', not an array of DemoStep');
continue;
}
produced.forEach((step: any, i: number) => {
steps += 1;
if (typeof step?.announce !== 'string' || step.announce.trim() === '') {
problems.push(trace + ': step ' + i + ' has an empty \\\`announce\\\`');
}
});
}
}
say({ loaded: true, ok: problems.length === 0, problems: problems.slice(0, 10), steps });
`;
}
+181
View File
@@ -0,0 +1,181 @@
#!/usr/bin/env node
/**
* The attribution checker.
*
* This repo is public and Apache-2.0, and almost none of what makes the demo
* work is ours: the primitives are shadcn/ui over Radix, the answer list is
* Wordnik's filtered through SCOWL, the typeface is Manrope under the SIL Open
* Font License, the icons are lucide, the charts are recharts. Every one of
* those licences is permissive, and every one of them requires attribution.
*
* So `NOTICE` is not paperwork, it is a build artifact with a test. This script
* is that test: it fails if NOTICE is missing any source we actually ship, and
* it fails if a source is named without its licence beside it — because
* "uses Manrope" without "OFL-1.1" is not attribution, it is a mention.
*
* The OFL additionally requires that the licence text travel WITH the font, so
* `public/fonts/OFL.txt` is checked for separately. Shipping the .woff2 out of
* node_modules and leaving the licence behind is the single easiest way to
* violate the one licence on this list that has teeth.
*/
import fs from 'node:fs';
import { Report, abs, exists, read, rel } from './_lib.mjs';
const report = new Report('check-licenses');
const NOTICE = abs('NOTICE');
const OFL = abs('public', 'fonts', 'OFL.txt');
/**
* Every third-party source this site ships, and the licence each one must be
* named with. `used` is a cheap sanity check in the other direction: an entry
* whose artifact has left the repo is stale attribution, and stale attribution
* quietly becomes wrong attribution.
*/
const SOURCES = [
{
label: 'shadcn/ui',
match: /shadcn/i,
licence: /\bMIT\b/,
why: 'the UI primitives in src/components/ui are shadcn/ui components, hand-copied into this repo.',
used: () => exists(abs('components.json')),
},
{
label: 'Radix UI',
match: /radix/i,
licence: /\bMIT\b/,
why: 'every primitive with behaviour — dialog, tabs, tooltip, slider — is Radix underneath.',
used: () => exists(abs('node_modules', '@radix-ui')),
},
{
label: 'the Wordnik word list',
match: /wordnik/i,
licence: /\bMIT\b/,
why: 'the guess list is derived from Wordnik.',
used: () => hasWordFile(/wordnik/i),
},
{
label: 'SCOWL',
match: /\bSCOWL\b/i,
licence: /permissive|attribution|BSD|Kevin\s+Atkinson/i,
why: 'the answer list is filtered through SCOWL.',
used: () => hasWordFile(/scowl/i),
},
{
label: 'Manrope',
match: /manrope/i,
// The OFL is version-specific and the version matters: OFL-1.1 is the one
// Manrope ships under, and it is the one whose terms are quoted in OFL.txt.
licence: /OFL[-\s]?1\.1|SIL\s+Open\s+Font\s+License/i,
why: 'Manrope is the typeface, embedded as a variable woff2.',
used: () => exists(abs('node_modules', '@fontsource-variable', 'manrope')),
},
{
label: 'lucide',
match: /lucide/i,
licence: /\bISC\b/,
why: 'every icon on the site is a lucide icon.',
used: () => exists(abs('node_modules', 'lucide-react')),
},
{
label: 'recharts',
match: /recharts/i,
licence: /\bMIT\b/,
why: 'the reward and metric charts are recharts.',
used: () => exists(abs('node_modules', 'recharts')),
},
{
label: "PIG's own token layer",
match: /\bPIG\b|Prime\s+Intellect\s+Growth/i,
licence: /Apache[-\s]?2\.0/i,
why: 'the colour and motion tokens in src/index.css come from PIG and ship under Apache-2.0.',
used: () => exists(abs('src', 'index.css')),
},
];
/** True when the words directory still carries a file from this source. */
function hasWordFile(pattern) {
const dir = abs('envs', 'wordle_five', 'words');
return exists(dir) && fs.readdirSync(dir).some((name) => pattern.test(name));
}
if (!exists(NOTICE)) {
report.fail(
'NOTICE',
'NOTICE exists',
'there is no NOTICE file at the repo root. Every source below is shipped by this site and each of ' +
`their licences requires attribution: ${SOURCES.map((s) => s.label).join(', ')}.`,
);
report.finish();
}
const notice = read(NOTICE);
const lines = notice.split('\n');
for (const source of SOURCES) {
// Every line that names the source, not just the first. "PIG-Demo" in the
// copyright header matches the PIG entry three dozen lines before its actual
// attribution block, and a first-match-wins reader fails on a NOTICE that is
// completely correct.
const hits = [];
lines.forEach((line, i) => {
if (source.match.test(line)) hits.push(i);
});
if (hits.length === 0) {
report.fail(
'NOTICE',
'attribution complete',
`${source.label} is not named. It is shipped by this site — ${source.why} — and its licence requires ` +
'attribution. Add it with its licence identifier.',
);
continue;
}
report.passed += 1;
// The licence has to sit with the name, not merely somewhere in the file:
// a NOTICE that says "MIT" once at the top and lists nine projects under it
// is attributing all nine to whichever licence happens to be first.
const near = (i) => lines.slice(Math.max(0, i - 2), i + 6).join('\n');
report.check(
hits.some((i) => source.licence.test(near(i))),
`NOTICE:${hits[0] + 1}`,
'attribution names the licence',
`${source.label} is named on line(s) ${hits.map((i) => i + 1).join(', ')} but no matching licence appears ` +
`beside any of them (looking for ${source.licence}). A name without a licence is a mention, not an ` +
'attribution.',
);
if (!source.used()) {
report.warn(
`NOTICE names ${source.label}, but nothing in the repo appears to use it any more. ` +
'Stale attribution is how a NOTICE stops being trustworthy.',
);
}
}
/* -------------------------------------------------------- the OFL's own rule */
report.check(
exists(OFL),
rel(OFL),
'OFL text ships with the font',
'Manrope is under SIL OFL-1.1, which requires the licence text to travel with the font files. ' +
'Copy node_modules/@fontsource-variable/manrope/LICENSE to public/fonts/OFL.txt. ' +
'Shipping the woff2 and leaving the licence behind is the one violation on this list with teeth.',
);
if (exists(OFL)) {
const text = read(OFL);
report.check(
/SIL OPEN FONT LICENSE/i.test(text) && /Version 1\.1/i.test(text),
rel(OFL),
'OFL text ships with the font',
'the file exists but does not look like the SIL Open Font License 1.1. It must be the licence text ' +
'itself, not a pointer to it.',
);
}
report.finish();
+196
View File
@@ -0,0 +1,196 @@
#!/usr/bin/env node
/**
* The receipt checker.
*
* Every demo quotes the code that computes its reward, verbatim, next to the
* number that code produced. That quote is the page's whole claim to being
* evidence rather than marketing, so the thing it quotes has to exist and the
* region it quotes has to resolve. A receipt panel that silently renders empty,
* or renders the wrong forty lines because someone inserted a function above
* the marker, is worse than no receipt at all.
*
* ── MARKER SYNTAX ──────────────────────────────────────────────────────────
*
* `RewardSpec.source` is `{ path, code, marker? }`. Without a `marker` the
* whole file is the receipt. With one, the receipt is the region delimited by
* a matched pair of comment lines in the source file itself:
*
* # region: pig-demo/<marker>
* ... the quoted lines ...
* # endregion: pig-demo/<marker>
*
* Rules, all enforced here:
*
* · The marker lines are EXCLUSIVE — neither appears in the quoted region.
* · EXACTLY ONE pair per file per marker. Zero is a broken receipt; two or
* more is ambiguous, and an ambiguous receipt silently quotes whichever
* region the reader's implementation happened to find first. Both are
* fatal.
* · `region` must come before `endregion`, and the region must be non-empty
* once the marker lines are removed.
* · The comment token is `#` (Python) or `//` (TypeScript), then optional
* whitespace. Everything else on the line is ignored, so
* `# region: pig-demo/consistency (see PR #41)` is legal.
* · The `pig-demo/` prefix is required. It is what makes these greppable and
* stops an editor's own `#region` folding markers from being read as
* receipts.
*
* Marker names are `[a-z0-9][a-z0-9-]*`: they end up in a grep, a CI message
* and a code comment, and mixed case in all three is a bug factory.
*/
import path from 'node:path';
import {
Report,
abs,
allDemoDirs,
demoFiles,
evalLiteral,
exists,
isUnresolved,
literalsAfter,
read,
rel,
} from './_lib.mjs';
const report = new Report('check-receipts');
const RULE_PATH = 'reward.source.path';
const RULE_MARKER = 'reward.source.marker';
const MARKER_NAME = /^[a-z0-9][a-z0-9-]*$/;
const markerLine = (kind, marker) =>
new RegExp(`^[ \\t]*(?:#|//)[ \\t]*${kind}:[ \\t]*pig-demo/${marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![\\w-])`, 'm');
/** Every `source: { ... }` literal in a demo, wherever the demo chose to put it. */
function receipts(slug) {
const out = [];
for (const file of demoFiles(slug)) {
const src = read(file);
for (const literal of literalsAfter(src, /\bsource\s*:\s*/, '{')) {
// `code` is legitimately an identifier here — the Python arrives through
// a Vite `?raw` import, which cannot resolve in plain Node — so the
// literal is read leniently and only `path` and `marker` are trusted.
const evaluated = evalLiteral(literal.text, 'RewardSpec.source', { lenient: true });
if (!evaluated.ok) {
out.push({ file, error: evaluated.error });
continue;
}
const value = evaluated.value;
if (!value || typeof value !== 'object' || !('path' in value)) continue; // some other `source:` key
const line = src.slice(0, literal.start).split('\n').length;
out.push({ file, line, value });
}
}
return out;
}
const slugs = allDemoDirs().filter((s) => !s.startsWith('_'));
if (slugs.length === 0) {
console.log('check-receipts: no demos to check.');
process.exit(0);
}
for (const slug of slugs) {
const found = receipts(slug);
if (found.length === 0) {
report.fail(
rel(path.join(abs('src', 'demos'), slug)),
RULE_PATH,
'no `reward.source` found. Every demo quotes the code that computes its reward; without a source ' +
'the receipt panel has nothing to show and the page is asserting its numbers rather than proving them.',
);
continue;
}
for (const receipt of found) {
const where = `${rel(receipt.file)}${receipt.line ? `:${receipt.line}` : ''}`;
if (receipt.error) {
report.fail(where, RULE_PATH, receipt.error);
continue;
}
const { path: sourcePath, marker } = receipt.value;
if (typeof sourcePath !== 'string' || sourcePath.trim() === '') {
report.fail(
where,
RULE_PATH,
`\`path\` is ${isUnresolved(sourcePath) ? 'an imported constant' : JSON.stringify(sourcePath)}. ` +
'It must be a literal repo-relative path, e.g. "envs/wordle_five/wordle_five/rubric.py", ' +
'so a reader can open the same file this page quotes.',
);
continue;
}
// Repo-relative by contract. A leading slash would mean the site root,
// which is not where the Python lives, and an absolute disk path would
// only work on the machine that wrote it.
const clean = sourcePath.replace(/^\.\//, '');
if (path.isAbsolute(clean)) {
report.fail(where, RULE_PATH, `\`path\` is absolute ("${sourcePath}"). Use a repo-relative path.`);
continue;
}
const onDisk = abs(clean);
if (!report.check(exists(onDisk), where, RULE_PATH, `\`path\` is "${sourcePath}", and ${rel(onDisk)} does not exist.`)) {
continue;
}
if (marker === undefined || marker === null) continue; // whole file is the receipt
if (typeof marker !== 'string' || !MARKER_NAME.test(marker)) {
report.fail(
where,
RULE_MARKER,
`\`marker\` is ${JSON.stringify(marker)}. Markers are lower-kebab-case (${MARKER_NAME}) because they ` +
'appear in a grep, a CI message and a code comment, and mixed case in all three is a bug factory.',
);
continue;
}
const body = read(onDisk);
const lines = body.split('\n');
const opens = [];
const closes = [];
lines.forEach((line, i) => {
if (markerLine('region', marker).test(line)) opens.push(i);
if (markerLine('endregion', marker).test(line)) closes.push(i);
});
const syntax = `Expected exactly one "# region: pig-demo/${marker}" and one "# endregion: pig-demo/${marker}".`;
if (opens.length !== 1 || closes.length !== 1) {
report.fail(
rel(onDisk),
RULE_MARKER,
`marker "${marker}" (referenced from ${where}) resolves to ${opens.length} region marker(s) and ` +
`${closes.length} endregion marker(s). ${syntax} ` +
(opens.length > 1 || closes.length > 1
? 'More than one pair is ambiguous: the receipt would quote whichever region the reader found first.'
: 'A missing marker renders the receipt panel empty, which reads as the code not existing.'),
);
continue;
}
const [open] = opens;
const [close] = closes;
if (close <= open) {
report.fail(
rel(onDisk),
RULE_MARKER,
`marker "${marker}": the endregion is on line ${close + 1}, at or before the region on line ${open + 1}.`,
);
continue;
}
const region = lines.slice(open + 1, close);
report.check(
region.some((line) => line.trim() !== ''),
rel(onDisk),
RULE_MARKER,
`marker "${marker}" delimits an empty region (lines ${open + 2}-${close}). The markers are exclusive, ` +
'so a pair on adjacent lines quotes nothing.',
);
}
}
report.finish();
+192
View File
@@ -0,0 +1,192 @@
import { useState } from 'react';
import type { ComponentType } from 'react';
import { Eye, Trophy } from 'lucide-react';
import type { DemoStep } from '@/lib/demo-kit/types';
import { cn } from '@/lib/utils';
import { formatOrDash } from './format';
export interface BlindCompareRun<T> {
runId: string;
/** The identity, revealed only after the visitor commits. */
label: string;
model: string;
/** Required by the contract on an `intervened` run; shown at reveal. */
intervention?: string;
steps: DemoStep<T>[];
total: number | null;
}
export type BlindVote = 'A' | 'B' | 'tie';
export interface BlindCompareProps<T> {
/** Both runs must be the same seed or the comparison is meaningless. */
seed: number;
a: BlindCompareRun<T>;
b: BlindCompareRun<T>;
Surface: ComponentType<{ state: T; compact?: boolean }>;
question?: string;
onVote?: (vote: BlindVote) => void;
className?: string;
}
/**
* Two runs on the same seed, unlabelled, until you commit.
*
* The point is not the vote. The point is that the visitor forms an opinion
* from the behaviour BEFORE they learn which one had the better prompt, the
* bigger model or the training run — because once they know, they cannot
* unknow it, and every "obviously the trained one looks better" is worthless
* after the fact.
*
* There is a "reveal without voting" escape on purpose: a visitor who does not
* want to play should not be held hostage by a modal-shaped page.
*/
export function BlindCompare<T>({
seed,
a,
b,
Surface,
question = 'Which agent would you rather have running this?',
onVote,
className,
}: BlindCompareProps<T>) {
const [vote, setVote] = useState<BlindVote | null>(null);
const [revealed, setRevealed] = useState(false);
const commit = (choice: BlindVote) => {
setVote(choice);
setRevealed(true);
onVote?.(choice);
};
const winner: 'A' | 'B' | 'tie' =
a.total === null || b.total === null
? 'tie'
: a.total > b.total
? 'A'
: b.total > a.total
? 'B'
: 'tie';
const sides: { id: 'A' | 'B'; run: BlindCompareRun<T> }[] = [
{ id: 'A', run: a },
{ id: 'B', run: b },
];
return (
<section aria-label="Blind comparison" className={cn('space-y-3', className)}>
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<h3 className="text-sm font-semibold">{question}</h3>
<p className="nums text-xs text-muted">Same puzzle, same seed ({seed}).</p>
</div>
<div className="grid gap-3 sm:grid-cols-2">
{sides.map(({ id, run }) => {
const last = run.steps[run.steps.length - 1];
const picked = vote === id;
return (
<div
key={id}
className={cn(
'card flex flex-col gap-3 p-3 transition-colors duration-2 ease-enter',
picked && 'border-brand',
revealed && winner === id && 'border-positive',
)}
>
<div className="flex items-center justify-between gap-2">
<h4 className="text-sm font-semibold">Agent {id}</h4>
<span className="nums text-xs text-muted">{run.steps.length} steps</span>
</div>
<div className="rounded-lg bg-surface-2 p-3">
{last ? (
<Surface state={last.state} />
) : (
<p className="text-sm text-muted">This run recorded no steps.</p>
)}
</div>
{!revealed ? (
<button
type="button"
onClick={() => commit(id)}
className="tap w-full rounded-lg bg-primary px-3 text-sm font-medium text-primary-foreground transition-colors duration-2 ease-enter hover:bg-primary/90"
>
Agent {id} is better
</button>
) : (
<dl className="space-y-1 text-sm">
<div className="flex items-baseline justify-between gap-2">
<dt className="text-muted">Identity</dt>
<dd className="text-right font-medium">{run.label}</dd>
</div>
<div className="flex items-baseline justify-between gap-2">
<dt className="text-muted">Model</dt>
<dd className="nums text-right font-mono text-xs">{run.model}</dd>
</div>
<div className="flex items-baseline justify-between gap-2">
<dt className="text-muted">Intervention</dt>
<dd className="text-right text-xs">
{run.intervention ?? (
<span className="text-muted">none plain rollout</span>
)}
</dd>
</div>
<div className="flex items-baseline justify-between gap-2 border-t border-border pt-1">
<dt className="text-muted">Reward</dt>
<dd className="nums flex items-center gap-1 text-right font-mono font-semibold">
{revealed && winner === id ? (
<Trophy className="h-3.5 w-3.5 text-positive" aria-hidden="true" />
) : null}
{formatOrDash(run.total)}
</dd>
</div>
</dl>
)}
</div>
);
})}
</div>
{!revealed ? (
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={() => commit('tie')}
className="tap rounded-lg border border-border px-3 text-sm font-medium transition-colors duration-2 ease-enter hover:bg-surface-2"
>
Too close to call
</button>
<button
type="button"
onClick={() => setRevealed(true)}
className="tap inline-flex items-center gap-1.5 rounded-lg px-3 text-sm font-medium text-muted transition-colors duration-2 ease-enter hover:text-fg"
>
<Eye className="h-4 w-4" aria-hidden="true" />
Just show me
</button>
</div>
) : (
<p role="status" className="card bg-surface-2 p-3 text-sm leading-relaxed">
{vote === null ? (
<>Revealed without a vote. </>
) : vote === winner ? (
<>
<span className="font-semibold text-positive">You picked the higher-scoring run.</span>{' '}
</>
) : (
<>
<span className="font-semibold text-warning">
You picked the lower-scoring run.
</span>{' '}
</>
)}
The environment scored these two with the same grader, on the same seed. The difference
between them is stated above and if it is a prompt change rather than a training run,
it says so, because a prompt change presented as a training result is the oldest trick
in this business.
</p>
)}
</section>
);
}
+188
View File
@@ -0,0 +1,188 @@
import { useMemo, useState } from 'react';
import { ExternalLink } from 'lucide-react';
import { cn } from '@/lib/utils';
export interface CodeReceiptProps {
/** The file's text, imported with `?raw` so it cannot drift from the source. */
code: string;
/** Repo-relative path, shown as the receipt's header. */
path: string;
/**
* A literal string that appears in the source and marks the interesting part.
* See `resolveMarkedRange` for the two conventions it supports.
*/
marker?: string;
/** Link to the whole file — GitHub, usually. */
href?: string;
/** Lines of context kept around the marked range while collapsed. */
context?: number;
className?: string;
}
export interface MarkedRange {
/** 0-based, inclusive. */
start: number;
/** 0-based, inclusive. */
end: number;
}
/**
* Two marker conventions, because both exist in real repos:
*
* TWICE — the marker brackets a region (`# --8<-- reward` … `# --8<--`).
* The marked range is the lines BETWEEN them; the fences themselves
* are not interesting code.
*
* ONCE — the marker sits on a definition line (`def compute_reward`). The
* marked range is that line plus its indented body, which is what
* you actually meant. Blank lines inside the body are kept; trailing
* blank lines are not, or the highlight runs on past the function.
*
* A marker that matches nothing returns null and the whole file renders. That
* is the right failure: a stale marker must not hide the source.
*/
export function resolveMarkedRange(lines: string[], marker?: string): MarkedRange | null {
if (!marker) return null;
const hits: number[] = [];
lines.forEach((line, index) => {
if (line.includes(marker)) hits.push(index);
});
const first = hits[0];
if (first === undefined) return null;
if (hits.length >= 2) {
const last = hits[hits.length - 1] as number;
return last - first > 1 ? { start: first + 1, end: last - 1 } : { start: first, end: last };
}
const anchor = lines[first] ?? '';
const indent = anchor.length - anchor.trimStart().length;
let end = first;
for (let i = first + 1; i < lines.length; i += 1) {
const line = lines[i] ?? '';
if (line.trim() === '') continue;
const lineIndent = line.length - line.trimStart().length;
if (lineIndent <= indent) break;
end = i;
}
return { start: first, end };
}
/**
* Source, with the part that matters marked.
*
* Syntax highlighting is deliberately absent: it costs a highlighter in the
* bundle and buys nothing an exec can use, while the marked range — the thing
* that says "this, right here, is the grader" — costs nothing and is the whole
* reason the panel exists.
*
* The block scrolls horizontally inside itself. Long Python lines must never
* make the PAGE scroll sideways; on a phone that turns every vertical swipe
* into a fight.
*/
export function CodeReceipt({
code,
path,
marker,
href,
context = 3,
className,
}: CodeReceiptProps) {
const lines = useMemo(() => code.replace(/\n$/, '').split('\n'), [code]);
const range = useMemo(() => resolveMarkedRange(lines, marker), [lines, marker]);
const [expanded, setExpanded] = useState(range === null);
const shown = useMemo(() => {
if (range === null || expanded) return { from: 0, to: lines.length - 1 };
return {
from: Math.max(range.start - context, 0),
to: Math.min(range.end + context, lines.length - 1),
};
}, [range, expanded, context, lines.length]);
const visible = lines.slice(shown.from, shown.to + 1);
const gutterWidth = `${String(lines.length).length + 1}ch`;
return (
<section aria-label={`Source: ${path}`} className={cn('card overflow-hidden', className)}>
<header className="flex flex-wrap items-center gap-x-3 gap-y-1 border-b border-border px-3 py-2">
<h3 className="nums min-w-0 flex-1 truncate font-mono text-xs text-muted" title={path}>
{path}
</h3>
{range && !expanded ? (
<button
type="button"
onClick={() => setExpanded(true)}
className="tap rounded-lg border border-border px-2.5 text-xs font-medium transition-colors duration-2 ease-enter hover:bg-surface-2"
>
Show all {lines.length} lines
</button>
) : null}
{range && expanded ? (
<button
type="button"
onClick={() => setExpanded(false)}
className="tap rounded-lg border border-border px-2.5 text-xs font-medium transition-colors duration-2 ease-enter hover:bg-surface-2"
>
Collapse to the marked part
</button>
) : null}
{href ? (
<a
href={href}
rel="noreferrer"
className="tap inline-flex items-center gap-1 text-xs font-medium text-accent-fg underline-offset-2 hover:underline"
>
Read the whole file
<ExternalLink className="h-3 w-3" aria-hidden="true" />
</a>
) : null}
</header>
<div className="max-h-96 overflow-auto">
<pre className="w-max min-w-full py-2 font-mono text-xs leading-relaxed">
<code>
{shown.from > 0 ? (
<span className="block px-3 text-muted" aria-hidden="true">
</span>
) : null}
{visible.map((line, offset) => {
const number = shown.from + offset;
const marked = range !== null && number >= range.start && number <= range.end;
return (
<span
key={number}
className={cn(
'block border-l-2 pr-4',
marked
? 'border-brand bg-accent-subtle/60 text-fg'
: 'border-transparent text-muted',
)}
>
{/* Sticky so the line numbers survive a horizontal scroll —
without it they slide out of view exactly when a long
line makes you want them. */}
<span
aria-hidden="true"
className="sticky left-0 inline-block select-none bg-surface pr-3 text-right text-muted"
style={{ width: gutterWidth }}
>
{number + 1}
</span>
{line === '' ? ' ' : line}
</span>
);
})}
{shown.to < lines.length - 1 ? (
<span className="block px-3 text-muted" aria-hidden="true">
</span>
) : null}
</code>
</pre>
</div>
</section>
);
}
+100
View File
@@ -0,0 +1,100 @@
import type { ComponentType } from 'react';
import { Link } from 'react-router-dom';
import { ArrowRight } from 'lucide-react';
import type { DemoMeta } from '@/lib/demo-kit/types';
import { cn } from '@/lib/utils';
import { DemoIcon } from './icons';
export interface DemoCardProps<T> {
meta: DemoMeta;
/** Defaults to the canonical demo route. */
href?: string;
/**
* The demo's OWN board, drawn compact, as the thumbnail. A screenshot would
* go stale the first time the board changes and nobody would notice; this
* cannot, because it is the same component the demo page renders.
*/
Surface?: ComponentType<{ state: T; compact?: boolean }>;
/** A representative state for the thumbnail — usually a solved board. */
thumbnailState?: T;
className?: string;
}
export function DemoCard<T>({
meta,
href,
Surface,
thumbnailState,
className,
}: DemoCardProps<T>) {
const to = href ?? `/demo/${meta.slug}`;
const isSpec = meta.status === 'spec';
const showSurface = Surface !== undefined && thumbnailState !== undefined;
return (
<article
className={cn(
'card group relative flex flex-col overflow-hidden transition-colors duration-2 ease-enter hover:border-brand/50',
className,
)}
>
<div className="flex items-start gap-3 p-4 pb-3">
<span className="grid h-10 w-10 shrink-0 place-items-center rounded-lg bg-accent-subtle text-accent-fg">
<DemoIcon name={meta.icon} className="h-5 w-5" />
</span>
<div className="min-w-0 flex-1">
<h3 className="text-base font-semibold leading-tight">
{/* Stretched link: the whole card is the hit target, but there is
still exactly ONE link in the accessibility tree for it. */}
<Link
to={to}
className="after:absolute after:inset-0 after:content-[''] focus-visible:outline-none"
>
{meta.title}
</Link>
</h3>
<p className="mt-0.5 text-sm leading-snug text-muted">{meta.tagline}</p>
</div>
{isSpec ? (
<span className="shrink-0 rounded-md border border-border px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-muted">
Spec
</span>
) : null}
</div>
{showSurface ? (
<div className="mx-4 overflow-hidden rounded-lg bg-surface-2 p-3">
{/* Decorative here: the title and tagline already name the demo, and
a screen reader has no use for a board with no run behind it. */}
<div aria-hidden="true">
<Surface state={thumbnailState as T} compact />
</div>
</div>
) : null}
<dl className="mt-3 flex flex-wrap gap-x-4 gap-y-1 px-4 text-xs">
<div className="flex gap-1">
<dt className="text-muted">For</dt>
<dd className="font-medium">{meta.persona}</dd>
</div>
<div className="flex gap-1">
<dt className="text-muted">Vertical</dt>
<dd className="font-medium capitalize">{meta.vertical.replace(/-/g, ' ')}</dd>
</div>
</dl>
<p className="mt-2 px-4 pb-4 text-xs leading-relaxed text-muted">
<span className="font-medium text-fg">Reward: </span>
{meta.rewardLine}
</p>
<p className="mt-auto flex items-center gap-1 border-t border-border px-4 py-2.5 text-sm font-medium text-accent-fg">
{isSpec ? 'Read the specification' : 'Open the demo'}
<ArrowRight
className="h-4 w-4 transition-transform duration-2 ease-enter group-hover:translate-x-0.5"
aria-hidden="true"
/>
</p>
</article>
);
}
+66
View File
@@ -0,0 +1,66 @@
import { ArrowRight, ShieldQuestion } from 'lucide-react';
import type { Limit } from '@/lib/demo-kit/types';
import { cn } from '@/lib/utils';
export interface LimitsCalloutProps {
limits: Limit[];
/**
* Turns a demo slug into something linkable. The shell has no registry
* dependency of its own, so the page that knows the routes supplies this.
*/
resolveDemo?: (slug: string) => { title: string; href: string } | undefined;
title?: string;
className?: string;
}
function defaultResolve(slug: string) {
return { title: slug, href: `/demo/${slug}` };
}
/**
* What this demo does not teach.
*
* Styled as confidence, not apology. A vendor who tells you the limits of their
* own demo before you find them is a vendor you believe about everything else
* on the page — and every limit here names the demo that closes it, so the list
* reads as a roadmap rather than a disclaimer.
*/
export function LimitsCallout({
limits,
resolveDemo = defaultResolve,
title = 'What this demo does not teach',
className,
}: LimitsCalloutProps) {
if (limits.length === 0) return null;
return (
<section aria-label={title} className={cn('card p-4', className)}>
<div className="flex items-center gap-2">
<ShieldQuestion className="h-4 w-4 text-muted" aria-hidden="true" />
<h3 className="text-sm font-semibold">{title}</h3>
</div>
<ul className="mt-3 space-y-3">
{limits.map((limit) => {
const target = limit.answeredBy ? resolveDemo(limit.answeredBy) : undefined;
return (
<li key={limit.text} className="border-l-2 border-border pl-3">
<p className="text-pretty text-sm leading-relaxed text-fg">{limit.text}</p>
{target ? (
<a
href={target.href}
className="mt-1 inline-flex items-center gap-1 text-xs font-medium text-accent-fg underline-offset-2 hover:underline"
>
Answered by {target.title}
<ArrowRight className="h-3 w-3" aria-hidden="true" />
</a>
) : (
<p className="mt-1 text-xs text-muted">
No demo answers this one yet. It is a real gap, not a rhetorical one.
</p>
)}
</li>
);
})}
</ul>
</section>
);
}
+116
View File
@@ -0,0 +1,116 @@
import {
CartesianGrid,
Line,
LineChart,
ReferenceLine,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import type { TooltipProps } from 'recharts';
import { formatNumber } from './format';
export interface MetricPoint {
/** Whatever the x axis is counting: checkpoint, step, arm name. */
x: string | number;
y: number;
}
export interface MetricChartProps {
data: MetricPoint[];
/** The rule the headline number is being compared against. */
baseline?: { value: number; label: string };
height: number;
/** Off under `prefers-reduced-motion`; recharts animates on mount by default. */
animate: boolean;
digits?: number;
}
/**
* The chart, in its own module so `React.lazy` can hold recharts out of the
* entry chunk. Nothing else in the shell may import this file directly — an
* ordinary import here defeats the whole arrangement and the entry chunk grows
* by ~100 kB without anyone noticing.
*
* Every colour is read from the token layer at paint time rather than passed in
* as a literal, so the chart follows the theme toggle without a re-render.
*/
export default function MetricChart({
data,
baseline,
height,
animate,
digits = 3,
}: MetricChartProps) {
return (
<div style={{ height }} className="w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data} margin={{ top: 8, right: 12, bottom: 4, left: 4 }}>
<CartesianGrid stroke="hsl(var(--border))" strokeDasharray="2 4" vertical={false} />
<XAxis
dataKey="x"
tick={{ fill: 'hsl(var(--muted))', fontSize: 11 }}
tickLine={false}
axisLine={{ stroke: 'hsl(var(--border))' }}
minTickGap={12}
/>
<YAxis
tick={{ fill: 'hsl(var(--muted))', fontSize: 11 }}
tickLine={false}
axisLine={false}
width={40}
tickFormatter={(value: number) => formatNumber(value, digits > 2 ? 2 : digits)}
/>
{baseline ? (
<ReferenceLine
y={baseline.value}
stroke="hsl(var(--muted))"
strokeDasharray="5 4"
label={{
value: baseline.label,
position: 'insideTopLeft',
fill: 'hsl(var(--muted))',
fontSize: 11,
}}
/>
) : null}
<Tooltip
cursor={{ stroke: 'hsl(var(--border))' }}
content={(props: TooltipProps<number, string>) => (
<ChartTooltip {...props} digits={digits} />
)}
/>
<Line
type="monotone"
dataKey="y"
stroke="hsl(var(--accent))"
strokeWidth={2}
dot={{ r: 2.5, fill: 'hsl(var(--accent))', strokeWidth: 0 }}
activeDot={{ r: 4 }}
isAnimationActive={animate}
animationDuration={400}
/>
</LineChart>
</ResponsiveContainer>
</div>
);
}
function ChartTooltip({
active,
payload,
label,
digits,
}: TooltipProps<number, string> & { digits: number }) {
if (!active || !payload || payload.length === 0) return null;
const value = payload[0]?.value;
return (
<div className="card px-2.5 py-1.5 text-xs shadow-md">
<p className="nums text-muted">{String(label)}</p>
<p className="nums font-mono font-semibold">
{typeof value === 'number' ? formatNumber(value, digits) : '—'}
</p>
</div>
);
}
+126
View File
@@ -0,0 +1,126 @@
import { Suspense, lazy, useMemo } from 'react';
import { cn } from '@/lib/utils';
import { formatNumber, formatSigned, usePrefersReducedMotion } from './format';
import type { MetricPoint } from './MetricChart';
import { EditedChip } from './StatStrip';
export type { MetricPoint } from './MetricChart';
// Lazy, and deliberately not a static import: recharts is ~100 kB gzipped and
// exactly one surface on the site uses it. `vite.config.ts` also names it as
// its own manual chunk, so this stays out of the entry bundle in both dev and
// production builds.
const MetricChart = lazy(() => import('./MetricChart'));
const CHART_HEIGHT = 168;
export interface MetricMoverProps {
/** The one number a reader would repeat in a meeting. */
label: string;
value: number;
digits?: number;
unit?: string;
/** The dashed rule: what the number was before, or what counts as par. */
baseline?: { value: number; label: string };
series?: MetricPoint[];
/** One sentence on what moved it. Not a caveat — the mechanism. */
caption?: string;
edited?: boolean;
className?: string;
}
/**
* The headline number, with the line that shows it moving.
*
* The reference rule is not decoration: a number on its own is a claim, and a
* number against the line it used to sit on is evidence. If a demo has no
* baseline to draw, it should not be using this component.
*/
export function MetricMover({
label,
value,
digits = 3,
unit,
baseline,
series,
caption,
edited = false,
className,
}: MetricMoverProps) {
const reducedMotion = usePrefersReducedMotion();
const delta = baseline ? value - baseline.value : null;
const points = useMemo(() => series ?? [], [series]);
return (
<section aria-label={label} className={cn('card p-4', className)}>
<div className="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<h3 className="text-sm font-medium text-muted">{label}</h3>
{edited ? <EditedChip /> : null}
</div>
<p className="mt-1 flex items-baseline gap-2">
<span className="nums text-4xl font-semibold leading-none tracking-tight">
{formatNumber(value, digits)}
</span>
{unit ? <span className="text-sm text-muted">{unit}</span> : null}
{delta !== null ? (
<span
className={cn(
'nums text-sm font-medium',
delta > 0 ? 'text-positive' : delta < 0 ? 'text-danger' : 'text-muted',
)}
>
{formatSigned(delta, digits)} vs {baseline?.label}
</span>
) : null}
</p>
{points.length > 1 ? (
<div className="mt-3">
<Suspense
fallback={
// Reserve the exact chart height. A chart that pops in and pushes
// the caption down is the layout shift this whole page is trying
// not to have.
<div
style={{ height: CHART_HEIGHT }}
className="w-full rounded-lg bg-surface-2"
aria-hidden="true"
/>
}
>
<MetricChart
data={points}
{...(baseline ? { baseline } : {})}
height={CHART_HEIGHT}
animate={!reducedMotion}
digits={digits}
/>
</Suspense>
{/* The chart is a picture of the table; the table is the accessible
version of the picture. Both are the same numbers. */}
<details className="mt-1">
<summary className="tap inline-flex cursor-pointer items-center text-xs text-muted hover:text-fg">
Show these points as a table
</summary>
<table className="nums mt-1.5 w-full border-collapse font-mono text-xs">
<tbody className="divide-y divide-border">
{points.map((point) => (
<tr key={String(point.x)}>
<th scope="row" className="py-1 text-left font-normal text-muted">
{String(point.x)}
</th>
<td className="py-1 text-right">{formatNumber(point.y, digits)}</td>
</tr>
))}
</tbody>
</table>
</details>
</div>
) : null}
{caption ? (
<p className="mt-3 text-sm leading-relaxed text-muted">{caption}</p>
) : null}
</section>
);
}
+128
View File
@@ -0,0 +1,128 @@
import { useState } from 'react';
import { Check, Copy, ExternalLink } from 'lucide-react';
import type { Provenance, RunRef } from '@/lib/demo-kit/types';
import { cn } from '@/lib/utils';
import { formatDate } from './format';
export interface ProvenanceCardProps {
provenance: Provenance;
/** The run currently on screen, if the page is showing one. */
run?: RunRef;
className?: string;
}
type CopyState = 'idle' | 'copied' | 'failed';
/**
* Where these numbers came from, and the command that reproduces them.
*
* The command is the load-bearing element. Everything else on this page is us
* telling you what happened; this is the line you paste into your own terminal
* to find out for yourself, which is why it is verbatim and copy-pasteable
* rather than prettified into something that would not actually run.
*/
export function ProvenanceCard({ provenance, run, className }: ProvenanceCardProps) {
const [copyState, setCopyState] = useState<CopyState>('idle');
const copy = async () => {
try {
// `navigator.clipboard` is undefined on a non-secure origin, which is
// exactly what a colleague testing over a LAN IP will hit. Fail visibly.
await navigator.clipboard.writeText(provenance.command);
setCopyState('copied');
window.setTimeout(() => setCopyState('idle'), 2000);
} catch {
setCopyState('failed');
}
};
const rows: { label: string; value: string; mono?: boolean }[] = [
{ label: 'Environment package', value: provenance.envPackage, mono: true },
{ label: 'Taskset', value: provenance.tasksetId, mono: true },
{ label: 'verifiers version', value: provenance.verifiersVersion, mono: true },
];
if (run) {
rows.push(
{ label: 'Model', value: run.model, mono: true },
{ label: 'Captured', value: formatDate(run.capturedAt) },
{ label: 'Seed', value: String(run.seed), mono: true },
);
if (run.intervention) rows.push({ label: 'Intervention', value: run.intervention });
}
return (
<section aria-label="Provenance" className={cn('card overflow-hidden', className)}>
<header className="border-b border-border px-3 py-2">
<h3 className="text-sm font-semibold">Provenance</h3>
</header>
<dl className="divide-y divide-border">
{rows.map((row) => (
<div key={row.label} className="flex items-baseline gap-3 px-3 py-2">
<dt className="flex-1 text-sm text-muted">{row.label}</dt>
<dd
className={cn(
'min-w-0 break-all text-right text-sm',
row.mono && 'nums font-mono text-xs',
)}
>
{row.value}
</dd>
</div>
))}
</dl>
<div className="border-t border-border p-3">
<div className="flex items-center justify-between gap-2">
<h4 className="text-xs font-semibold uppercase tracking-wide text-muted">
Run it yourself
</h4>
<button
type="button"
onClick={copy}
className="tap inline-flex items-center gap-1.5 rounded-lg border border-border px-2.5 text-xs font-medium transition-colors duration-2 ease-enter hover:bg-surface-2"
>
{copyState === 'copied' ? (
<Check className="h-3.5 w-3.5 text-positive" aria-hidden="true" />
) : (
<Copy className="h-3.5 w-3.5" aria-hidden="true" />
)}
{copyState === 'copied' ? 'Copied' : 'Copy'}
</button>
</div>
<pre className="mt-2 overflow-x-auto rounded-lg bg-surface-2 p-3">
<code className="font-mono text-xs leading-relaxed">{provenance.command}</code>
</pre>
<p role="status" className="mt-1 text-xs text-muted">
{copyState === 'copied'
? 'Command copied to your clipboard.'
: copyState === 'failed'
? 'Your browser blocked clipboard access — select the command above and copy it manually.'
: ''}
</p>
</div>
{provenance.credits.length > 0 ? (
<div className="border-t border-border px-3 py-2.5">
<h4 className="text-xs font-semibold uppercase tracking-wide text-muted">
Built on
</h4>
<ul className="mt-1.5 flex flex-wrap gap-x-4 gap-y-1">
{provenance.credits.map((credit) => (
<li key={credit.href}>
<a
href={credit.href}
className="inline-flex items-center gap-1 text-sm text-accent-fg underline-offset-2 hover:underline"
rel="noreferrer"
>
{credit.label}
<ExternalLink className="h-3 w-3" aria-hidden="true" />
</a>
</li>
))}
</ul>
</div>
) : null}
</section>
);
}
+340
View File
@@ -0,0 +1,340 @@
import { useMemo, useState } from 'react';
import * as Slider from '@radix-ui/react-slider';
import { ArrowDown, ArrowUp, Minus, RotateCcw } from 'lucide-react';
import type { RewardSpec, RewardValues } from '@/lib/demo-kit/types';
import { cn } from '@/lib/utils';
import { formatNumber, formatOrDash } from './format';
import { scoreReward, shippedWeights } from './reward-math';
import { EditedChip } from './StatStrip';
/** One recorded arm — a run, or a group of runs already reduced to one score. */
export interface RewardArm {
id: string;
label: string;
/** The environment's per-component scores. Re-weighted, never re-run. */
values: RewardValues;
note?: string;
}
export interface RewardPreset {
id: string;
label: string;
description?: string;
weights: Record<string, number>;
}
export interface RewardEditorProps {
spec: RewardSpec;
arms: RewardArm[];
/** Two is the right number. More and the visitor reads instead of playing. */
presets?: RewardPreset[];
onWeightsChange?: (weights: Record<string, number>, edited: boolean) => void;
className?: string;
}
const STEP = 0.05;
/**
* Presets built from the spec's own labels, for a demo that does not supply
* its own. Both are stated as the choice a buyer would actually argue for in a
* meeting, not as "preset A" and "preset B".
*/
function derivePresets(spec: RewardSpec): RewardPreset[] {
const shipped = shippedWeights(spec);
const counterweights = spec.components.filter((c) => c.role === 'counterweight');
const objectives = spec.components.filter((c) => c.role === 'objective');
const presets: RewardPreset[] = [
{
id: 'shipped',
label: 'What we ship',
description: 'The weights in the environment as committed.',
weights: shipped,
},
];
const firstObjective = objectives[0];
if (counterweights.length > 0 && firstObjective) {
const onlyObjective = { ...shipped };
for (const component of counterweights) onlyObjective[component.key] = 0;
presets.push({
id: 'objective-only',
label: `${firstObjective.label} at any cost`,
description: `Drops ${counterweights
.map((c) => c.label.toLowerCase())
.join(' and ')} to zero.`,
weights: onlyObjective,
});
const doubled = { ...shipped };
for (const component of counterweights) doubled[component.key] = component.weight * 2;
presets.push({
id: 'counterweight-heavy',
label: `Double ${counterweights[0]?.label.toLowerCase() ?? 'the counterweight'}`,
description: 'What a risk-averse buyer would ask for.',
weights: doubled,
});
}
return presets;
}
function weightsEqual(a: Record<string, number>, b: Record<string, number>): boolean {
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
for (const key of keys) {
if (Math.abs((a[key] ?? 0) - (b[key] ?? 0)) > 1e-9) return false;
}
return true;
}
/**
* Change what "good" means and watch the ranking move.
*
* The honesty problem this component has to solve: re-weighting recorded scores
* is NOT training. It shows you the ranking a different reward would have
* produced over these exact attempts; it cannot show you the different attempts
* a model trained on that reward would have made. That distinction is the
* permanent caption at the bottom, and it is not collapsible.
*/
export function RewardEditor({
spec,
arms,
presets,
onWeightsChange,
className,
}: RewardEditorProps) {
const shipped = useMemo(() => shippedWeights(spec), [spec]);
const [weights, setWeights] = useState<Record<string, number>>(shipped);
const effectivePresets = useMemo(() => presets ?? derivePresets(spec), [presets, spec]);
const bounds = useMemo(() => {
const values = spec.components.map((c) => c.weight);
const max = Math.max(2, ...values.map((v) => Math.ceil(Math.abs(v) * 2)));
const min = Math.min(0, ...values.map((v) => Math.floor(v)));
return { min, max };
}, [spec]);
const edited = !weightsEqual(weights, shipped);
const apply = (next: Record<string, number>) => {
setWeights(next);
onWeightsChange?.(next, !weightsEqual(next, shipped));
};
const ranked = useMemo(() => {
const shippedTotals = new Map(
arms.map((arm) => [arm.id, scoreReward(spec, arm.values).total]),
);
const rows = arms.map((arm) => ({
arm,
total: scoreReward(spec, arm.values, weights).total,
shippedTotal: shippedTotals.get(arm.id) ?? null,
}));
// Nulls sort last: an unscored arm is not a zero-scoring arm.
const byTotal = (a: { total: number | null }, b: { total: number | null }) => {
if (a.total === null && b.total === null) return 0;
if (a.total === null) return 1;
if (b.total === null) return -1;
return b.total - a.total;
};
const shippedOrder = [...rows]
.sort((a, b) => byTotal({ total: a.shippedTotal }, { total: b.shippedTotal }))
.map((row) => row.arm.id);
return [...rows].sort(byTotal).map((row, index) => ({
...row,
rank: index + 1,
shippedRank: shippedOrder.indexOf(row.arm.id) + 1,
}));
}, [arms, spec, weights]);
const span = useMemo(() => {
const totals = ranked.map((row) => row.total).filter((t): t is number => t !== null);
if (totals.length === 0) return { lo: 0, hi: 1 };
const lo = Math.min(0, ...totals);
const hi = Math.max(...totals);
return { lo, hi: hi === lo ? lo + 1 : hi };
}, [ranked]);
const leader = ranked[0];
return (
<div className={cn('grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]', className)}>
<section aria-label="Reward weights" className="card p-3">
<div className="flex flex-wrap items-center gap-2">
<h3 className="text-sm font-semibold">Change what good means</h3>
{edited ? <EditedChip /> : null}
<button
type="button"
onClick={() => apply(shipped)}
disabled={!edited}
className="tap ml-auto inline-flex items-center gap-1.5 rounded-lg border border-border px-2.5 text-xs font-medium transition-colors duration-2 ease-enter hover:bg-surface-2 disabled:opacity-40"
>
<RotateCcw className="h-3.5 w-3.5" aria-hidden="true" />
Reset to shipped
</button>
</div>
<div className="mt-2 flex flex-wrap gap-2">
{effectivePresets.map((preset) => {
const active = weightsEqual(weights, preset.weights);
return (
<button
key={preset.id}
type="button"
onClick={() => apply({ ...preset.weights })}
aria-pressed={active}
title={preset.description ?? preset.label}
className={cn(
'tap rounded-lg border px-3 text-left text-xs font-medium transition-colors duration-2 ease-enter',
active
? 'border-brand bg-accent-subtle text-accent-fg'
: 'border-border hover:bg-surface-2',
)}
>
{preset.label}
</button>
);
})}
</div>
<div className="mt-4 space-y-4">
{spec.components.map((component) => {
const value = weights[component.key] ?? component.weight;
const changed = Math.abs(value - component.weight) > 1e-9;
return (
<div key={component.key}>
<div className="flex items-baseline justify-between gap-2">
{/* A <label htmlFor> would point at Radix's root <span>,
which is not a labelable element — the association would
silently do nothing. The thumb takes its name from this
text via aria-labelledby instead. */}
<span
id={`weight-label-${component.key}`}
className="text-sm font-medium text-fg"
>
{component.label}
</span>
<span
className={cn(
'nums font-mono text-sm',
changed ? 'font-semibold text-accent-fg' : 'text-muted',
)}
>
{formatNumber(value, 2)}
</span>
</div>
<p className="mb-1.5 text-xs leading-snug text-muted">{component.description}</p>
<Slider.Root
className="relative flex h-6 w-full touch-none select-none items-center"
min={bounds.min}
max={bounds.max}
step={STEP}
value={[value]}
onValueChange={(next) =>
apply({ ...weights, [component.key]: next[0] ?? component.weight })
}
>
<Slider.Track className="relative h-1.5 w-full grow rounded-full bg-surface-2">
<Slider.Range className="absolute h-full rounded-full bg-brand" />
</Slider.Track>
{/* 44px of hit area around a 16px dot: the visible thumb is
small enough to read the track under it, and still catches
a thumb on a phone. */}
<Slider.Thumb
aria-labelledby={`weight-label-${component.key}`}
className="block h-6 w-6 rounded-full border-4 border-brand bg-surface shadow-sm"
/>
</Slider.Root>
</div>
);
})}
</div>
</section>
<section aria-label="Ranking under this reward" className="card flex flex-col p-3">
<h3 className="text-sm font-semibold">
Ranking under this reward {edited ? <EditedChip className="ml-1 align-middle" /> : null}
</h3>
<ol className="mt-3 space-y-2">
{ranked.map((row) => {
const moved = row.rank - row.shippedRank;
const width =
row.total === null
? 0
: Math.max(2, ((row.total - span.lo) / (span.hi - span.lo)) * 100);
return (
<li
key={row.arm.id}
className={cn(
'rounded-lg border p-2.5 transition-colors duration-3 ease-enter',
row.rank === 1 ? 'border-brand bg-accent-subtle/50' : 'border-border',
)}
>
<div className="flex items-baseline gap-2">
<span className="nums text-sm font-semibold text-muted">{row.rank}</span>
<span className="min-w-0 flex-1 truncate text-sm font-medium">
{row.arm.label}
</span>
<RankMove moved={moved} />
<span className="nums font-mono text-sm font-semibold">
{formatOrDash(row.total)}
</span>
</div>
<div
aria-hidden="true"
className="mt-1.5 h-1.5 w-full overflow-hidden rounded-full bg-surface-2"
>
<div
className="h-full rounded-full bg-brand transition-[width] duration-3 ease-enter"
style={{ width: `${width}%` }}
/>
</div>
{row.arm.note ? (
<p className="mt-1 text-xs text-muted">{row.arm.note}</p>
) : null}
</li>
);
})}
</ol>
{/* User-initiated, so it is safe to announce here without fighting the
shell's step-change region: the two never fire from one action. */}
<p role="status" className="sr-only">
{leader
? `Leading under this reward: ${leader.arm.label}, ${formatOrDash(leader.total)}.`
: 'No arms to rank.'}
</p>
<p className="mt-auto pt-3 text-xs leading-relaxed text-muted">
We re-scored the same recorded attempts under your reward. Training on it would change
the behaviour, not just the ranking.
</p>
</section>
</div>
);
}
function RankMove({ moved }: { moved: number }) {
if (moved === 0) {
return (
<span className="inline-flex items-center text-muted" title="Same rank as shipped">
<Minus className="h-3.5 w-3.5" aria-hidden="true" />
<span className="sr-only">unchanged</span>
</span>
);
}
const up = moved < 0;
return (
<span
className={cn('nums inline-flex items-center text-xs', up ? 'text-positive' : 'text-danger')}
title={`${Math.abs(moved)} place${Math.abs(moved) === 1 ? '' : 's'} ${up ? 'up' : 'down'} from the shipped reward`}
>
{up ? (
<ArrowUp className="h-3.5 w-3.5" aria-hidden="true" />
) : (
<ArrowDown className="h-3.5 w-3.5" aria-hidden="true" />
)}
{Math.abs(moved)}
<span className="sr-only">{up ? ' places up' : ' places down'}</span>
</span>
);
}
+384
View File
@@ -0,0 +1,384 @@
/**
* A hand-written demo, used to build and exercise the shell without waiting for
* real fixtures.
*
* It is NOT a fixture and it is not shipped as a demo: nothing in
* `src/demos/` imports it, and the shell mounts it only for the reserved slug
* `__mock` in a dev build. It exists so that every surface in this directory
* has something complete to render — including the awkward cases a real trace
* eventually produces: a step with no reasoning, a null model call, a
* not-scored reward component, and a truncated run.
*/
import type {
DemoEpisode,
DemoModule,
DemoStep,
RewardValues,
RunRef,
} from '@/lib/demo-kit/types';
import { cn } from '@/lib/utils';
export type MarkKind = 'exact' | 'present' | 'absent';
export interface MockGuess {
word: string;
marks: MarkKind[];
}
export interface MockState {
answer: string;
guesses: MockGuess[];
solved: boolean;
}
const ANSWER = 'CRANE';
const MAX_GUESSES = 6;
/** Standard Wordle marking, duplicates and all. */
function mark(guess: string, answer: string): MarkKind[] {
const marks: MarkKind[] = Array.from({ length: guess.length }, () => 'absent');
const pool = new Map<string, number>();
for (let i = 0; i < answer.length; i += 1) {
const letter = answer[i] as string;
if (guess[i] === letter) {
marks[i] = 'exact';
} else {
pool.set(letter, (pool.get(letter) ?? 0) + 1);
}
}
for (let i = 0; i < guess.length; i += 1) {
if (marks[i] === 'exact') continue;
const letter = guess[i] as string;
const left = pool.get(letter) ?? 0;
if (left > 0) {
marks[i] = 'present';
pool.set(letter, left - 1);
}
}
return marks;
}
const TILE: Record<MarkKind, string> = {
exact: 'bg-tile-exact text-tile-exact-fg',
present: 'bg-tile-present text-tile-present-fg',
absent: 'bg-tile-absent text-tile-absent-fg',
};
const GLYPH: Record<MarkKind, string> = { exact: '●', present: '◐', absent: '○' };
function MockSurface({ state, compact = false }: { state: MockState; compact?: boolean }) {
const rows = Array.from({ length: MAX_GUESSES }, (_, index) => state.guesses[index]);
return (
<div
className={cn('grid w-fit gap-1', compact && 'gap-0.5')}
role="img"
aria-label={
state.guesses.length === 0
? 'Empty board'
: `Board after ${state.guesses.length} guesses: ${state.guesses
.map((guess) => guess.word)
.join(', ')}`
}
>
{rows.map((guess, rowIndex) => (
<div key={rowIndex} className={cn('flex gap-1', compact && 'gap-0.5')}>
{Array.from({ length: 5 }, (_unused, colIndex) => {
const letter = guess?.word[colIndex];
const kind = guess?.marks[colIndex];
return (
<span
key={colIndex}
className={cn(
'grid place-items-center rounded-md border border-border font-semibold uppercase',
compact ? 'h-3.5 w-3.5 text-[7px]' : 'h-9 w-9 text-base',
kind ? TILE[kind] : 'bg-surface-2 text-muted',
)}
>
{compact ? (kind ? GLYPH[kind] : '') : (letter ?? '')}
</span>
);
})}
</div>
))}
</div>
);
}
const REWARD_SOURCE = `import verifiers as vf
def reward_solved(state) -> float:
"""1.0 if the answer was found, else 0.0."""
return 1.0 if state["solved"] else 0.0
# --8<-- efficiency
def reward_efficiency(state) -> float:
"""Pays for finding it early, and pays nothing for finding it late.
This is the counterweight. Without it the highest-scoring policy is one
that burns every guess it is allowed, because the objective alone cannot
tell a lucky third guess from a grudging sixth.
"""
if not state["solved"]:
return 0.0
used = len(state["guesses"])
return max(0.0, (MAX_GUESSES - used + 1) / MAX_GUESSES)
# --8<--
def reward_legal(state) -> float:
"""Every guess was a real five-letter word in the allowed list."""
return 1.0 if all(g in ALLOWED for g in state["guesses"]) else 0.0
`;
function turnBoard(episode: DemoEpisode, upto: number): MockState {
const guesses: MockGuess[] = [];
for (let i = 0; i <= upto && i < episode.turns.length; i += 1) {
const info = episode.turns[i]?.info;
const word = typeof info?.['guess'] === 'string' ? info['guess'] : null;
if (!word) continue;
guesses.push({ word, marks: mark(word, ANSWER) });
}
const last = guesses[guesses.length - 1];
return { answer: ANSWER, guesses, solved: last?.word === ANSWER };
}
function adapt(episode: DemoEpisode): DemoStep<MockState>[] {
return episode.turns.map((turn, index) => {
const state = turnBoard(episode, index);
const last = state.guesses[state.guesses.length - 1];
const word = last?.word ?? 'no guess';
const exact = last?.marks.filter((m) => m === 'exact').length ?? 0;
return {
index,
state,
reply: turn.reply,
reasoning: turn.reasoning,
call: turn.call,
announce: state.solved
? `Guess ${index + 1}: ${word}. Solved.`
: `Guess ${index + 1}: ${word}. ${exact} letters in the right place.`,
caption: word.toLowerCase(),
};
});
}
function verify(episode: DemoEpisode): RewardValues | null {
if (episode.truncated) return null;
const final = turnBoard(episode, episode.turns.length - 1);
const used = final.guesses.length;
return {
solved: final.solved ? 1 : 0,
efficiency: final.solved ? Math.max(0, (MAX_GUESSES - used + 1) / MAX_GUESSES) : 0,
legal: 1,
};
}
export const mockDemo: DemoModule<MockState> = {
meta: {
slug: '__mock',
title: 'Five-letter word game (mock)',
tagline: 'An agent guesses a hidden five-letter word from letter feedback.',
vertical: 'reference',
status: 'live',
order: 0,
icon: 'Grid3x3',
persona: 'Anyone deciding whether to fund an environment',
rewardLine: 'Pays for solving; takes away for stalling.',
ogImage: '/og/mock.png',
},
narrative: {
thesis:
'An environment is an eval you can take the gradient of. The task, the legal moves and the grader are all code — so you can change what "good" means and watch the number move.',
anxiety: 'Is this a benchmark I read, or a thing I can actually change?',
beats: [
{ id: 'hero', title: 'The run', claim: 'This is a recorded rollout, not a live request.', surface: 'hero' },
{ id: 'anatomy', title: 'The machine', claim: 'Four boxes: task, legal actions, grader, score.', surface: 'anatomy' },
{ id: 'play', title: 'Watch it think', claim: 'Every move has a reason and a cost, both recorded.', surface: 'split-play' },
{ id: 'reward', title: 'Change what good means', claim: 'Move a weight and the ranking moves with it.', surface: 'reward-editor' },
{ id: 'metric', title: 'The number that moves', claim: 'The score is a measurement, not a claim.', surface: 'metric' },
{ id: 'receipt', title: 'The receipts', claim: 'Every number here has a command that reproduces it.', surface: 'receipt' },
{ id: 'limits', title: 'What this does not teach', claim: 'A word game is not your business process.', surface: 'limits' },
],
limits: [
{
text: 'A five-letter word has one right answer. Most business decisions do not, and a grader that pretends otherwise scores confidence rather than correctness.',
answeredBy: 'claims-triage',
},
{
text: 'Nothing here has a cost of being wrong. A real reward has to price the mistake, not just count the win.',
},
],
},
reward: {
components: [
{
key: 'solved',
label: 'Found the word',
description: 'One point if the hidden word was guessed within six tries.',
weight: 1,
role: 'objective',
},
{
key: 'efficiency',
label: 'Found it early',
description: 'Pays more the fewer guesses it took. Zero if it never got there.',
weight: 0.5,
role: 'counterweight',
},
{
key: 'legal',
label: 'Played legal words',
description: 'Every guess was a real five-letter word from the allowed list.',
weight: 0.25,
role: 'gate',
},
],
metrics: [
{ key: 'guesses_used', label: 'Guesses used', description: 'How many of the six were spent.' },
{ key: 'unique_letters', label: 'Unique letters tried', description: 'Breadth of the search.' },
],
source: { path: 'envs/wordle_five/wordle_five/rewards.py', code: REWARD_SOURCE, marker: '--8<-- efficiency' },
},
provenance: {
envPackage: 'wordle_five',
tasksetId: 'wordle-five-v0-mock',
verifiersVersion: '0.1.0',
command: 'uv run vf-eval wordle-five -n 8 -m gpt-4.1-mini --seed 7',
credits: [
{ label: 'verifiers', href: 'https://github.com/PrimeIntellect-ai/verifiers' },
],
},
anatomy: {
task: 'Guess a hidden five-letter word in six tries, using the coloured feedback from each guess.',
actions: 'One legal five-letter word per turn, drawn from the allowed list. Nothing else is a move.',
grader: 'Deterministic Python: it marks the guess against the answer, checks legality, and scores the episode. No model judges it.',
score: 'A weighted sum: one point for finding the word, half a point scaled by how early, a quarter as a legality gate.',
},
adapt,
Surface: MockSurface,
verify,
};
function call(prompt: number, completion: number, reasoning: number | null, ms: number) {
return {
promptTokens: prompt,
completionTokens: completion,
reasoningTokens: reasoning,
durationMs: ms,
finishReason: 'stop',
};
}
export const mockEpisodes: Record<string, DemoEpisode> = {
'mock-base': {
runId: 'mock-base',
seed: 7,
model: 'gpt-4.1-mini',
capturedAt: '2026-08-12',
rewards: { solved: 1, efficiency: 0.5, legal: 1 },
metrics: { guesses_used: 4, unique_letters: 14 },
outcome: 'solved',
turns: [
{
reply: 'SLATE',
reasoning:
'Opening with a word that covers three of the five most common letters and two common consonants. I want information, not a lucky hit.',
call: call(412, 6, 84, 910),
info: { guess: 'SLATE' },
},
{
reply: 'TRACE',
reasoning: null,
call: call(486, 6, null, 640),
info: { guess: 'TRACE' },
},
{
reply: 'BRACE',
reasoning:
'A and E are placed. R is in the word but not where I put it. That leaves the first two positions to resolve.',
call: call(551, 6, 121, 1180),
info: { guess: 'BRACE' },
},
{
reply: 'CRANE',
reasoning: 'Only one word fits every constraint now.',
call: call(618, 6, 96, 720),
info: { guess: 'CRANE' },
},
],
},
'mock-prompted': {
runId: 'mock-prompted',
seed: 7,
model: 'gpt-4.1-mini',
capturedAt: '2026-08-12',
// Written as the expression the environment computes, not as a decimal
// literal: a hand-rounded 0.8333 would fail our own verifier.
rewards: { solved: 1, efficiency: 5 / 6, legal: 1 },
metrics: { guesses_used: 2, unique_letters: 9 },
outcome: 'solved',
turns: [
{
reply: 'TRACE',
reasoning: 'Told to open with maximum letter coverage and to commit early once a word fits.',
call: call(455, 6, 140, 1020),
info: { guess: 'TRACE' },
},
{
reply: 'CRANE',
reasoning: 'Every constraint is satisfied by exactly one candidate.',
call: call(512, 6, 88, 660),
info: { guess: 'CRANE' },
},
],
},
'mock-truncated': {
runId: 'mock-truncated',
seed: 11,
model: 'gpt-4.1-mini',
capturedAt: '2026-08-12',
// A run that hit the turn cap: the environment never reached a terminal
// state, so two of the three components were never scored.
rewards: { solved: null, efficiency: null, legal: 1 },
metrics: { guesses_used: 2, unique_letters: 8 },
truncated: true,
outcome: 'aborted',
turns: [
{ reply: 'AUDIO', reasoning: null, call: call(410, 6, null, 880), info: { guess: 'AUDIO' } },
{ reply: null, reasoning: null, call: null, info: {} },
],
},
};
export const mockRuns: RunRef[] = [
{
id: 'mock-base',
label: 'Out of the box',
path: '/traces/__mock/base.json',
kind: 'recorded',
model: 'gpt-4.1-mini',
capturedAt: '2026-08-12',
seed: 7,
},
{
id: 'mock-prompted',
label: 'With a strategy prompt',
path: '/traces/__mock/prompted.json',
kind: 'intervened',
intervention: 'System prompt rewritten',
model: 'gpt-4.1-mini',
capturedAt: '2026-08-12',
seed: 7,
},
{
id: 'mock-truncated',
label: 'Hit the turn cap',
path: '/traces/__mock/truncated.json',
kind: 'recorded',
model: 'gpt-4.1-mini',
capturedAt: '2026-08-12',
seed: 11,
},
];
+56 -35
View File
@@ -8,9 +8,9 @@ const STORAGE_KEY = 'pig-demo:contrast';
/** /**
* `localStorage` is not always readable. In a cross-origin iframe with third- * `localStorage` is not always readable. In a cross-origin iframe with third-
* party storage blocked, and in Safari private mode, the getter itself THROWS * party storage blocked, and in Safari's private mode, the accessor itself
* rather than returning null — so every access has to be wrapped, not just * THROWS rather than returning null — so every access has to be wrapped, not
* null-checked. Unreadable storage means "off", never a crash. * just null-checked. Unreadable storage means "off", never a crash.
*/ */
function readStored(): boolean { function readStored(): boolean {
try { try {
@@ -24,51 +24,72 @@ function writeStored(high: boolean): void {
try { try {
window.localStorage.setItem(STORAGE_KEY, high ? 'high' : 'normal'); window.localStorage.setItem(STORAGE_KEY, high ? 'high' : 'normal');
} catch { } catch {
/* Preference is session-only here. The toggle still works. */ /* Preference is session-only in this context. The toggle still works. */
} }
} }
/**
* One value shared by every mounted toggle, not one `useState` each.
*
* The header renders this control twice — once in the desktop bar, once inside
* the mobile sheet — and only one of them is ever visible. With local state the
* hidden one keeps a stale `aria-pressed`, so a visitor who toggles on a phone
* and then rotates into the desktop layout is told the setting is off while the
* page is plainly showing it on.
*/
let high = false;
let initialised = false;
const listeners = new Set<() => void>();
function applyToRoot(next: boolean): void {
const root = document.documentElement;
// Removed rather than set to "normal": the CSS keys off
// `:root[data-contrast='high']`, and a leftover attribute makes the DOM lie
// about which palette is actually applied.
if (next) root.setAttribute('data-contrast', 'high');
else root.removeAttribute('data-contrast');
}
function setHigh(next: boolean): void {
high = next;
applyToRoot(next);
writeStored(next);
for (const listener of listeners) listener();
}
function subscribe(listener: () => void): () => void {
if (!initialised) {
initialised = true;
high = readStored();
applyToRoot(high);
}
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}
export function ContrastToggle({ className }: { className?: string }) { export function ContrastToggle({ className }: { className?: string }) {
const [high, setHigh] = React.useState(false); // The server snapshot is `false` so a prerendered page never claims a
const [mounted, setMounted] = React.useState(false); // preference it cannot know; the real value lands on the first subscribe.
const isHigh = React.useSyncExternalStore(
React.useEffect(() => { subscribe,
const stored = readStored(); () => high,
setHigh(stored); () => false,
setMounted(true); );
}, []);
React.useEffect(() => {
if (!mounted) return;
const root = document.documentElement;
// Removing the attribute rather than setting it to "normal": the CSS keys
// off `:root[data-contrast='high']`, and leaving a stale attribute behind
// makes the DOM lie about the palette that is actually applied.
if (high) root.setAttribute('data-contrast', 'high');
else root.removeAttribute('data-contrast');
}, [high, mounted]);
return ( return (
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
size="icon-touch" size="icon-touch"
className={cn('lg:size-9', high && 'bg-accent-subtle text-accent-fg', className)} className={cn('lg:size-9', isHigh && 'bg-accent-subtle text-accent-fg', className)}
aria-pressed={high} aria-pressed={isHigh}
aria-label={ aria-label={isHigh ? 'High contrast tiles on. Turn off.' : 'High contrast tiles off. Turn on.'}
high ? 'High contrast tiles on. Turn off.' : 'High contrast tiles off. Turn on.'
}
title="High contrast tiles" title="High contrast tiles"
onClick={() => { onClick={() => setHigh(!isHigh)}
const next = !high;
setHigh(next);
writeStored(next);
}}
> >
<Contrast aria-hidden="true" /> <Contrast aria-hidden="true" />
<span aria-live="polite" className="sr-only">
{mounted ? (high ? 'High contrast on' : 'High contrast off') : ''}
</span>
</Button> </Button>
); );
} }
+13 -3
View File
@@ -247,7 +247,14 @@ function TasksetSource({ className }: { className?: string }) {
<p className="text-xs font-semibold uppercase tracking-wider text-muted"> <p className="text-xs font-semibold uppercase tracking-wider text-muted">
The actual taskset The actual taskset
</p> </p>
<pre className="overflow-x-auto text-[11px] leading-relaxed text-fg"> {/*
Wrapped, not scrolled. The source is quoted verbatim — reformatting it
to fit would make it stop being a quote — and the widest line is a third
wider than this column at any font size worth reading. In a panel whose
entire job is "this is real code", a visible wrap beats a third of the
line hidden behind a scrollbar nobody in a boardroom will drag.
*/}
<pre className="overflow-x-auto whitespace-pre-wrap break-words text-[11px] leading-relaxed text-fg">
<code className="font-mono">{TASKSET_SOURCE}</code> <code className="font-mono">{TASKSET_SOURCE}</code>
</pre> </pre>
<a <a
@@ -265,8 +272,11 @@ function TasksetSource({ className }: { className?: string }) {
function HowItWorksPanel({ ctaHref }: { ctaHref: string }) { function HowItWorksPanel({ ctaHref }: { ctaHref: string }) {
return ( return (
<div className="w-[min(92vw,760px)] p-4"> // 820px, not more: the panel hangs off the LEFT edge of the nav, which
<div className="grid grid-cols-2 gap-4"> // starts about 100px in, so anything wider than this pushes past the right
// edge of a 1024px laptop and gives the whole page a horizontal scrollbar.
<div className="w-[min(90vw,820px)] p-4">
<div className="grid grid-cols-[minmax(0,0.85fr)_minmax(0,1fr)] gap-4">
<dl className="flex flex-col gap-3"> <dl className="flex flex-col gap-3">
{CONCEPTS.map((concept) => ( {CONCEPTS.map((concept) => (
<div key={concept.term} className="flex flex-col gap-0.5"> <div key={concept.term} className="flex flex-col gap-0.5">
+9 -7
View File
@@ -6,7 +6,7 @@
* or path moves, it moves in one line instead of in four pages. * or path moves, it moves in one line instead of in four pages.
*/ */
import { demos } from '@/lib/demo-kit'; import { demos } from '@/lib/demo-kit/registry';
import type { DemoMeta, Vertical } from '@/lib/demo-kit/types'; import type { DemoMeta, Vertical } from '@/lib/demo-kit/types';
import { VERTICALS, verticalByKey } from '@/content/verticals'; import { VERTICALS, verticalByKey } from '@/content/verticals';
import type { VerticalEntry } from '@/content/verticals'; import type { VerticalEntry } from '@/content/verticals';
@@ -26,13 +26,15 @@ export const routes = {
galleryFiltered: (key: Vertical) => `/gallery?vertical=${key}`, galleryFiltered: (key: Vertical) => `/gallery?vertical=${key}`,
} as const; } as const;
/** The public repository. Every claim on the site is meant to end up here. */ /**
export const REPO_URL = 'https://github.com/karti-ai/PIG-Demo'; * The repository link belongs to the chrome, which already owns it. Re-exported
* rather than repeated so the marketing pages and the footer cannot end up
* pointing at two different repositories.
*/
export { REPO_URL } from '@/components/site/links';
/** Registry order is authorial; this is the order every list renders in. */ /** Already sorted by `order` then slug by the registry. Never sort it again. */
export const allDemos: readonly DemoMeta[] = [...demos].sort( export const allDemos: readonly DemoMeta[] = demos;
(a, b) => a.order - b.order || a.slug.localeCompare(b.slug),
);
export const liveDemos: readonly DemoMeta[] = allDemos.filter((d) => d.status === 'live'); export const liveDemos: readonly DemoMeta[] = allDemos.filter((d) => d.status === 'live');
+63 -16
View File
@@ -1,24 +1,24 @@
/** /**
* The demo-kit public barrel. * The demo-kit barrel.
* *
* This is the ONLY module a demo under `src/demos/` is allowed to import from * Two audiences, and the split between them matters.
* the shared shell, and it deliberately exposes a small surface: the contract
* types, the two `define*` wrappers, and the two pure helpers a demo's own
* surface might need to render a score honestly.
* *
* The player, the registry, the verifier and the reward editor's arithmetic are * A DEMO under `src/demos/` may import from `@/lib/demo-kit` and from nothing
* NOT here. They are shell machinery a demo that reaches for `usePlayer` is a * else in the shell. What it should actually reach for is the first block
* demo that has started rendering its own chrome, and the whole point of the * below: the contract types, the two `define*` wrappers, and the two pure
* contract is that the shell owns chrome so every demo gets the same one. The * helpers it needs to render a score without inventing one. That restriction is
* shell imports those from their own modules: * enforced by `scripts/check-demos.mjs`, not by what this file happens to
* export the shell's own surfaces import through here too, and splitting them
* into a second barrel would only mean the check script had two paths to allow
* instead of one.
* *
* import { listDemos, loadDemoModule } from '@/lib/demo-kit/registry'; * The SHELL may import anything here, or reach into the individual modules
* import { usePlayer } from '@/lib/demo-kit/player'; * (`@/lib/demo-kit/player`, `/registry`, `/episode`, `/reward`, `/verify`) when
* import { loadEpisode, listRuns } from '@/lib/demo-kit/episode'; * it wants one thing without the rest.
* import { decompose, reweight } from '@/lib/demo-kit/reward';
* import { verifyEpisode } from '@/lib/demo-kit/verify';
*/ */
/* -- The contract. Demos live here. --------------------------------------- */
export type { export type {
DemoEpisode, DemoEpisode,
DemoMeta, DemoMeta,
@@ -39,5 +39,52 @@ export type {
export { defineDemo, defineMeta } from './define'; export { defineDemo, defineMeta } from './define';
/** `null` is "not scored", never 0.0. Demos render absences with these two. */ /** `null` is "not scored", never 0.0. Every absence goes through these two. */
export { isNotScored, rewardTotal } from './episode'; export { isNotScored, rewardTotal } from './episode';
/* -- Shell machinery. ------------------------------------------------------ */
export {
demos,
getDemo,
getVertical,
hasDemo,
listDemos,
listVerticals,
loadDemoModule,
VERTICAL_LABELS,
VERTICAL_ORDER,
} from './registry';
export type { AnyDemoModule, VerticalGroup } from './registry';
export {
clearEpisodeCache,
listRuns,
loadEpisode,
loadManifest,
MANIFEST_PATH,
scoredCount,
} from './episode';
export type { RunManifest } from './episode';
export {
FALLBACK_STEP_MS,
isPlaybackSpeed,
PLAYBACK_SPEEDS,
usePlayer,
usePrefersReducedMotion,
} from './player';
export type { PlaybackSpeed, Player, PlayerOptions } from './player';
export {
decompose,
isEdited,
pruneOverrides,
reweight,
WEIGHT_EPSILON,
weightsAreDegenerate,
} from './reward';
export type { RewardBreakdown, RewardRow, WeightOverrides } from './reward';
export { recomputeRewards, verifyEpisode, verifySummary, VERIFY_TOLERANCE } from './verify';
export type { ComponentComparison, VerifyResult, VerifyStatus } from './verify';
+8
View File
@@ -179,6 +179,14 @@ const demosBySlug: ReadonlyMap<string, DemoMeta> = (() => {
const orderedDemos: readonly DemoMeta[] = [...demosBySlug.values()].sort(byOrderThenSlug); const orderedDemos: readonly DemoMeta[] = [...demosBySlug.values()].sort(byOrderThenSlug);
/**
* The same list as `listDemos()`, as a frozen constant.
*
* Handy where a module wants the lineup at import time rather than in a render.
* It is the SAME array every caller sees never sort or splice it in place.
*/
export const demos: readonly DemoMeta[] = orderedDemos;
/** Every demo that survived validation, sorted by `order` then slug. */ /** Every demo that survived validation, sorted by `order` then slug. */
export function listDemos(): DemoMeta[] { export function listDemos(): DemoMeta[] {
return [...orderedDemos]; return [...orderedDemos];
+23
View File
@@ -137,6 +137,29 @@ export function verifyEpisode(module: AnyDemoModule, episode: DemoEpisode): Veri
return result; return result;
} }
/**
* Just the recomputed numbers, with none of the comparison.
*
* `verifyEpisode` is the one to reach for it does the comparing, and the
* comparing is where the honesty rules live. This exists for a surface that
* wants to render the recomputed values itself and only needs the safe call:
* a grader that is missing, throws, or declines returns `null` rather than
* propagating, so no caller can turn a broken verifier into a zero.
*/
export function recomputeRewards(
module: AnyDemoModule,
episode: DemoEpisode,
): RewardValues | null {
if (typeof module.verify !== 'function') return null;
if (episode.truncated === true) return null;
try {
return module.verify(episode);
} catch (error: unknown) {
console.error('[demo-kit] in-browser grader threw', error);
return null;
}
}
/** One line an exec can read, given a result. Keeps the wording in one place. */ /** One line an exec can read, given a result. Keeps the wording in one place. */
export function verifySummary(result: VerifyResult): string { export function verifySummary(result: VerifyResult): string {
switch (result.status) { switch (result.status) {
+23 -11
View File
@@ -3,21 +3,26 @@ import { Link, useSearchParams } from 'react-router-dom';
import { ArrowRight, FileText } from 'lucide-react'; import { ArrowRight, FileText } from 'lucide-react';
import { iconFor } from '@/content/icons'; import { iconFor } from '@/content/icons';
import { allDemos, routes, verticalForDemo, verticalKeysInUse } from '@/content/lineup'; import {
import { PROPOSAL_NOTICE, verticalByKey } from '@/content/verticals'; allDemos,
demosForVertical,
lineup,
routes,
verticalForDemo,
verticalKeysInUse,
} from '@/content/lineup';
import { PROPOSAL_NOTICE } from '@/content/verticals';
// The registry owns the taxonomy's exec-facing names. The lineup's own titles
// are longer marketing headings ("Customer Support Resolution") and would wrap
// two lines inside a filter chip on a phone, so chips use the registry label.
import { VERTICAL_LABELS } from '@/lib/demo-kit/registry';
import type { Vertical } from '@/lib/demo-kit/types'; import type { Vertical } from '@/lib/demo-kit/types';
import * as s from '@/content/styles'; import * as s from '@/content/styles';
const ALL = 'all'; const ALL = 'all';
/**
* A label for a `Vertical` key. Everything except `reference` has a vertical
* entry to borrow the title from; `reference` is the hello-world demo and
* belongs to no industry, so it is named for what it is.
*/
function verticalLabel(key: Vertical): string { function verticalLabel(key: Vertical): string {
if (key === 'reference') return 'Reference'; return VERTICAL_LABELS[key];
return verticalByKey(key)?.title ?? key;
} }
export default function Gallery() { export default function Gallery() {
@@ -44,6 +49,8 @@ export default function Gallery() {
const filters: readonly (Vertical | typeof ALL)[] = [ALL, ...verticalKeysInUse]; const filters: readonly (Vertical | typeof ALL)[] = [ALL, ...verticalKeysInUse];
const unbuilt = lineup.filter((v) => demosForVertical(v.key).length === 0).length;
return ( return (
<main className={`${s.shell} py-10 sm:py-16`}> <main className={`${s.shell} py-10 sm:py-16`}>
<p className={s.eyebrow}>Gallery</p> <p className={s.eyebrow}>Gallery</p>
@@ -114,6 +121,7 @@ export default function Gallery() {
// A spec is dimmed but never disabled: it goes to a real // A spec is dimmed but never disabled: it goes to a real
// page with a real specification on it, which is the only // page with a real specification on it, which is the only
// thing that makes dimming it honest rather than teasing. // thing that makes dimming it honest rather than teasing.
aria-label={`${demo.title}${isSpec ? 'read the specification' : 'play it'}`}
className={`${s.cardLink} h-full ${isSpec ? 'opacity-70 hover:opacity-100' : ''}`} className={`${s.cardLink} h-full ${isSpec ? 'opacity-70 hover:opacity-100' : ''}`}
to={routes.demo(demo.slug)} to={routes.demo(demo.slug)}
> >
@@ -169,8 +177,12 @@ export default function Gallery() {
</ul> </ul>
)} )}
<div className="mt-12 card p-5 sm:p-7"> <div className="card mt-12 p-5 sm:p-7">
<h2 className={s.h2}>The eleven we have not built</h2> {/* Counted, not typed. A hard-coded "eleven" on a site about checkable
numbers goes stale the first time a demo ships. */}
<h2 className={s.h2}>
The {unbuilt} we have not built
</h2>
<p className={`${s.prose} mt-3 max-w-2xl`}> <p className={`${s.prose} mt-3 max-w-2xl`}>
The lineup is a set of proposals, written to the same four-part shape as the live one. The lineup is a set of proposals, written to the same four-part shape as the live one.
Reading one takes a minute and tells you whether the idea survives contact with your own Reading one takes a minute and tells you whether the idea survives contact with your own
+11 -2
View File
@@ -77,6 +77,7 @@ export default function Home() {
{helloWorldCitations.map((c) => ( {helloWorldCitations.map((c) => (
<li key={c.href}> <li key={c.href}>
<a <a
aria-label={c.label}
className={`${s.cardLink} h-full bg-surface-2`} className={`${s.cardLink} h-full bg-surface-2`}
href={c.href} href={c.href}
rel="noreferrer noopener" rel="noreferrer noopener"
@@ -186,7 +187,11 @@ export default function Home() {
watch a recorded model play the same board, read the Python that scored it, and then watch a recorded model play the same board, read the Python that scored it, and then
move the reward weights and watch the ranking of two recorded runs change under you. move the reward weights and watch the ranking of two recorded runs change under you.
</p> </p>
<Link className={`${s.cardLink} mt-6 sm:p-7`} to={routes.demo(Featured.slug)}> <Link
aria-label={`Open the ${Featured.title} demo`}
className={`${s.cardLink} mt-6 sm:p-7`}
to={routes.demo(Featured.slug)}
>
<span className="flex flex-wrap items-center gap-2"> <span className="flex flex-wrap items-center gap-2">
<span className={`${s.pill} border-positive/30 bg-positive/10 text-positive`}> <span className={`${s.pill} border-positive/30 bg-positive/10 text-positive`}>
Live Live
@@ -253,7 +258,11 @@ export default function Home() {
const Icon = iconFor(v.icon); const Icon = iconFor(v.icon);
return ( return (
<li key={v.slug}> <li key={v.slug}>
<Link className={`${s.cardLink} h-full`} to={routes.vertical(v.slug)}> <Link
aria-label={v.title}
className={`${s.cardLink} h-full`}
to={routes.vertical(v.slug)}
>
<span className="flex items-start justify-between gap-3"> <span className="flex items-start justify-between gap-3">
<Icon aria-hidden="true" className="size-5 shrink-0 text-brand" /> <Icon aria-hidden="true" className="size-5 shrink-0 text-brand" />
<span className="nums text-xs font-semibold text-muted"> <span className="nums text-xs font-semibold text-muted">
+16 -3
View File
@@ -113,7 +113,11 @@ export default function VerticalPage() {
<section className="mt-10"> <section className="mt-10">
<h2 className={s.h2}>What exists today</h2> <h2 className={s.h2}>What exists today</h2>
{live ? ( {live ? (
<Link className={`${s.cardLink} mt-4`} to={routes.demo(live.slug)}> <Link
aria-label={`${live.title} — play it`}
className={`${s.cardLink} mt-4`}
to={routes.demo(live.slug)}
>
<span className={`${s.pill} self-start border-positive/30 bg-positive/10 text-positive`}> <span className={`${s.pill} self-start border-positive/30 bg-positive/10 text-positive`}>
Live demo Live demo
</span> </span>
@@ -125,7 +129,11 @@ export default function VerticalPage() {
</span> </span>
</Link> </Link>
) : spec ? ( ) : spec ? (
<Link className={`${s.cardLink} mt-4`} to={routes.demo(spec.slug)}> <Link
aria-label={`${spec.title} — read the specification`}
className={`${s.cardLink} mt-4`}
to={routes.demo(spec.slug)}
>
<span className={`${s.pill} self-start gap-1`}> <span className={`${s.pill} self-start gap-1`}>
<FileText aria-hidden="true" className="size-3.5" /> <FileText aria-hidden="true" className="size-3.5" />
Published specification Published specification
@@ -169,7 +177,11 @@ export default function VerticalPage() {
{/* ── Move along the ranking ─────────────────────────────────────── */} {/* ── Move along the ranking ─────────────────────────────────────── */}
<nav aria-label="Other verticals" className="mt-12 grid gap-3 sm:grid-cols-2"> <nav aria-label="Other verticals" className="mt-12 grid gap-3 sm:grid-cols-2">
{previous ? ( {previous ? (
<Link className={`${s.cardLink} sm:items-start`} to={routes.vertical(previous.slug)}> <Link
aria-label={`Previous: ${previous.title}`}
className={`${s.cardLink} sm:items-start`}
to={routes.vertical(previous.slug)}
>
<span className="inline-flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wider text-muted"> <span className="inline-flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wider text-muted">
<ArrowLeft aria-hidden="true" className="size-3.5" /> <ArrowLeft aria-hidden="true" className="size-3.5" />
Rank {previous.rank} Rank {previous.rank}
@@ -181,6 +193,7 @@ export default function VerticalPage() {
)} )}
{next ? ( {next ? (
<Link <Link
aria-label={`Next: ${next.title}`}
className={`${s.cardLink} sm:col-start-2 sm:items-end sm:text-right`} className={`${s.cardLink} sm:col-start-2 sm:items-end sm:text-right`}
to={routes.vertical(next.slug)} to={routes.vertical(next.slug)}
> >
+10 -41
View File
@@ -15,7 +15,7 @@
* site is arguing. * site is arguing.
*/ */
import { Component, type ReactNode } from 'react'; import type { ReactNode } from 'react';
import { import {
createBrowserRouter, createBrowserRouter,
isRouteErrorResponse, isRouteErrorResponse,
@@ -27,6 +27,7 @@ import {
type RouteObject, type RouteObject,
} from 'react-router-dom'; } from 'react-router-dom';
import { getDemo, loadDemoModule } from '@/lib/demo-kit/registry'; import { getDemo, loadDemoModule } from '@/lib/demo-kit/registry';
import { DemoErrorBoundary } from '@/components/demo/DemoErrorBoundary';
import { SiteFooter } from '@/components/site/SiteFooter'; import { SiteFooter } from '@/components/site/SiteFooter';
import { SiteHeader } from '@/components/site/SiteHeader'; import { SiteHeader } from '@/components/site/SiteHeader';
import { SkipLink } from '@/components/site/SkipLink'; import { SkipLink } from '@/components/site/SkipLink';
@@ -132,7 +133,7 @@ function RootErrorBoundary(): ReactNode {
); );
} }
function DemoErrorBoundary(): ReactNode { function DemoRouteError(): ReactNode {
const error = useRouteError(); const error = useRouteError();
if (isRouteErrorResponse(error) && error.status === 404) { if (isRouteErrorResponse(error) && error.status === 404) {
return ( return (
@@ -153,42 +154,6 @@ function DemoErrorBoundary(): ReactNode {
); );
} }
/**
* Catches errors thrown while a demo's own components RENDER.
*
* The route error boundary above only sees loader and lazy-import failures; a
* demo whose `Surface` throws on a malformed board state would still white-page
* the app without this.
*/
export class DemoRenderBoundary extends Component<
{ children: ReactNode },
{ error: Error | null }
> {
override state: { error: Error | null } = { error: null };
static getDerivedStateFromError(error: unknown): { error: Error } {
return { error: error instanceof Error ? error : new Error(String(error)) };
}
override componentDidCatch(error: unknown): void {
console.error('[demo] render failed', error);
}
override render(): ReactNode {
const { error } = this.state;
if (error) {
return (
<ErrorCard
heading="This demo failed to render"
body="Only this demo is affected. The recorded runs and the environment source in the repository are unaffected by a bug in the viewer."
detail={error.message}
/>
);
}
return this.props.children;
}
}
/** Shown while a lazy route's chunk is in flight. */ /** Shown while a lazy route's chunk is in flight. */
function RouteFallback(): ReactNode { function RouteFallback(): ReactNode {
return ( return (
@@ -244,14 +209,18 @@ export const routes: RouteObject[] = [
{ {
path: 'demos/:slug', path: 'demos/:slug',
loader: demoLoader, loader: demoLoader,
errorElement: <DemoErrorBoundary />, errorElement: <DemoRouteError />,
// Two boundaries, because they catch different things. `errorElement`
// above catches a loader or chunk failure; this one catches a demo whose
// own Surface throws while rendering a board state, which the router
// never sees.
lazy: async () => { lazy: async () => {
const { default: DemoPage } = await import('@/pages/DemoPage'); const { default: DemoPage } = await import('@/pages/DemoPage');
return { return {
Component: () => ( Component: () => (
<DemoRenderBoundary> <DemoErrorBoundary>
<DemoPage /> <DemoPage />
</DemoRenderBoundary> </DemoErrorBoundary>
), ),
}; };
}, },