16 typed TypeSafe questions about whatever you type, re-asked on every keystroke.
| Path | Type | Purpose |
|---|---|---|
index.ts | http | Hono app: serves the shell + assets, GET /api/questions, POST /api/judge |
typesafe.ts | script | Minimal TypeSafe (System One) REST client + answer types |
questions.ts | script | The 16 question specs and the 3 display groups — the single source of truth |
rate-limit.ts | script | Best-effort in-memory per-IP + global rate limit (burst control) |
budget.ts | script | SQLite-backed lifetime spend ledger; the hard $2 cap |
tests/budget.ts | script | Checks the budget guard without spending anything |
frontend/root.tsx | script | HTML shell (Hono JSX); stamps immutable asset URLs |
frontend/index.tsx | script | React entrypoint |
frontend/components/App.tsx | script | Layout, text state, status pill, group sections |
frontend/components/Meter.tsx | script | One question row: label, bar, answer |
frontend/components/Notice.tsx | script | The amber box for system states: no key, budget spent |
frontend/lib/display.ts | script | Answer → text, and the "did it meaningfully change" rules |
frontend/lib/useJudge.ts | script | Debounced, abortable POST /api/judge |
frontend/lib/useChanges.ts | script | Lights up rows whose judgment moved |
frontend/lib/types.ts | script | Client-side mirror of the API shapes |
frontend/lib/presets.ts | script | Sample texts |
frontend/favicon.svg | file | Icon |
Meter.tsx owns the only chart in the app: a single Bar (black fill on a
bg-neutral-200 track). fill() maps each answer type to one 0–1 number —
P(yes) for noul, position on the scale for score, winner probability for
choice. Adding a question type means one line there, nothing else.cdn.twind.style in root.tsx. Twind's theme is Tailwind v2: it has no
950 shades and no opacity modifiers, and it silently drops classes it can't
resolve. Use explicit palette colors, and inline styles for computed values
(bar widths, the faded "No" rows).rate-limit.ts is burst control (600/min per IP,
6000/min globally) and is best-effort in memory. budget.ts is the hard cap:
$2.00 lifetime, one row in the val's SQLite database, so it survives isolates
and deploys. Every call reserves an estimate before it goes out and reconciles
with usage.input_tokens after, which is why index.ts never calls ask()
without a reservation. Set TYPESAFE_BUDGET_USD to move the cap. When it's
spent, /api/judge returns 429 with the ledger in the body and the client
stops asking by itself — don't add a retry loop around it./api/questions returns a setup
object (configured, keyVar, envUrl, consoleUrl) built from
isConfigured() in typesafe.ts, so the page can say what's missing before
the first keystroke. App.tsx renders SetupNeeded and passes
enabled: false to useJudge, which then asks nothing at all. envUrl is
derived from parseVal(), so a fork links to its own settings. Nothing about
the key's value crosses the wire — only whether one exists./api/judge. The CDN replaces an origin 502 with
its own HTML error page, so the val's JSON body never arrives and the client
dies on Unexpected token '<'. Unconfigured is 503, an upstream failure is
500; both pass through intact. useJudge's readJson() is the backstop —
it parses the body itself and falls back to HTTP <status> rather than
throwing a SyntaxError at whatever HTML it got.data-q="<question id>" on each row, data-value on the answer cell,
data-type on the row, data-status on the pill (ok, asking, error,
budget-spent, needs-setup), data-budget-spent on the footer's spend
line, data-budget="spent" on the budget notice and data-setup="needed" on
the missing-key notice — for Playwright.TYPESAFE_API_KEY (from https://console.typesafe.ai/settings/keys).
TYPESAFE_MODEL optionally overrides the model without a deploy.GET /api/questions (which also returns the groups, so questions.ts is the
only place group order and copy live). Edit questions.ts and the panel
follows. Adding a question is one entry there; latency barely moves, since
TypeSafe evaluates every question in a request in parallel.