A school where the teachers grade you in their own voice. Clear daily classes, bank grades, and keep the yearbook.
Ruby High is a standalone Node service and installable SPA. Ruby hosts the school; specialist faculty (Sally Science, Professor Edward) teach their domains; six AI classmates sit beside you. You play a generated character, build school disciplines and virtues, walk between rooms, clear daily classes and practice questions, collect hidden First Bell comic pages, and graduate after Senior year.
For the product story, the mechanics, the cast, and the roadmap, see DESIGN.md. This file is the runbook.
Production is on Fly.io; see infra/fly-deploy.md.
AWS App Runner is legacy / manual-only; see infra/README.md.
npm install
npm run build
npm run dev:serverOpen http://127.0.0.1:3000/api/apps/ruby-high/viewer. Normal play starts with a Ruby High session cookie; OpenRouter sign-in is still available for BYOK AI (PKCE, your own key, no card). If Privy is configured, the Account button signs the player in and can connect or reuse a Solana wallet; this build does not auto-create one on login. Browser-owned OpenRouter keys default to sessionStorage, can opt into localStorage persistence with rh_openrouter_persist=1, and are never held by the server. Game state, auth sessions, and session-scoped packs persist through the configured store; teacher chat transcripts are process-local and reset on server restart/deploy.
The first C implementation lives in ruby2/c. It is a deterministic engine
and native SDL slice, not the final sokol client yet. It proves fixed-size
state, effect payload reduction, the Source/Sense/Sync/Signal disciplines,
RPN branch gates, item validation, empty-start inventory slots, whole-campus
pointcrawl navigation, ranker-curated two-button action trays with four-button
class moments, two-option conversation branches, Yearbook candidate eviction,
archetype resolution, companion locks, gameplay divergence tests, and the first
Captain Null trace resolver. play-llm adds the local Ollama
vertical slice: deterministic approach-choice gameplay with bounded
speech-bubble lines from ruby-high-local and authored fallback copy if the
model is unavailable or leaks analysis instead of JSON.
make -C ruby2/c test
make -C ruby2/c gameplay-test
make -C ruby2/c llm-test
make -C ruby2/c run
make -C ruby2/c play
make -C ruby2/c play-llm
make -C ruby2/c native-smoke
make -C ruby2/c native-runThe standalone viewer is installable as a PWA from /api/apps/ruby-high/viewer. The service worker is scoped to /api/apps/ruby-high/, caches the shell and core assets, and keeps auth, chat, pack management, and session state requests network-only. Full offline gameplay still requires the Ruby High server because the authoritative school state lives there.
Ruby High also has a static SPA build for browser-only offline testing:
npm run build:spa
npm run spa:devOpen http://127.0.0.1:4173. This build packages the same viewer shell with a browser-local offline API shim backed by localStorage and the bundled Ruby/Sally/Edward question banks. Core classroom play, character creation, room switching, Merit Stars, and local persistence work without the hosted server. OpenRouter auth, Hall Pass purchases, portrait/diploma image generation, teacher publishing, and hosted account sync still require the Node service.
For offline text AI, run Ruby High against a local OpenAI-compatible chat-completions server such as Ollama:
ollama create ruby-high-local -f /path/to/Modelfile
ollama serve
RUBY_HIGH_LLM_PROVIDER=local \
RUBY_HIGH_LLM_BASE_URL=http://127.0.0.1:11434/v1 \
RUBY_HIGH_LLM_MODEL=ruby-high-local \
npm run dev:serverLocal mode removes the OpenRouter key requirement for text chat, teacher turns, NPC chimes, character text, opinion grading, and multiple-choice distractors for source cards. Portrait and diploma image generation still use the OpenRouter image endpoint. A browser-owned OpenRouter key stays BYOK/free-to-Ruby-High; the optional server-hosted image path spends Hall Passes.
The offline SPA uses the same default local model and lets you override the local endpoint in DevTools:
localStorage.setItem("ruby-high:local-llm-base", "http://127.0.0.1:11434/v1");
localStorage.setItem("ruby-high:local-llm-model", "ruby-high-local");Native desktop/mobile packaging is intentionally not part of the current retention-truth build. Reintroduce it only when public-web retention data justifies that surface.
No hosted account or OpenRouter key is needed for these:
GET /dev/pick— draw a question for the active faculty.GET /dev/pick?faculty=sally-science&difficulty=hard— filter the draw.GET /dev/faculty— roster + question counts.GET /dev/clear— wipe the board (keeps Merit Stars).GET /dev/reset— wipe the session (Merit Stars + history).
GET /api/apps/ruby-high/adminrenders a browser dashboard for the token-gated usage snapshot, 14-day charts, and an operator overview. Paste the admin token once; the page stores it locally and calls admin endpoints with a Bearer header.POST /api/apps/ruby-high/metrics/eventrecords first-party viewer events. The bundled viewer sends durableapp_openon boot andsession_resumeafter returning from five-plus minutes inactive.GET /api/apps/ruby-high/admin/metricsreturns a compact JSON snapshot for retention tuning: auth identity records/sessions, visitor counts, 14-day auth/play/event series, Ruby High session progression, visitor and character D1 retention, durable v4 metric events, and in-process log counters.auth.usersis identity records, not unique humans. It is disabled untilRUBY_HIGH_ADMIN_TOKENis set and accepts eitherAuthorization: Bearer <token>or the exact token value.GET /api/apps/ruby-high/admin/metrics/schemapublishes the current admin metrics contract (ruby-high-admin-metrics.v4): field semantics, reliability levels, caveats, and the durable event streams for traffic, retention, funnel, commerce, LLM, and errors.GET /api/apps/ruby-high/admin/overviewreturns a token-gated LLM-generated operator overview built only from aggregate metrics. It requires the normal server LLM credential.GET /api/apps/ruby-high/yearbook/:shareId/:graderenders a static public yearbook card for a sealed grade. Sealed year cards expose Open/Copy controls in the viewer.?format=jsonreturns card data and?format=svgreturns the social image.?format=pngis intentionally 501 until server-side raster rendering is configured.
The standalone server starts four services (FacultyService, RubyHighService, AuthService, ChatService) backed by the content-pack registry under src/content/. Ruby High Original is always the base school; public creator packs rotate into one Guest Faculty course automatically each week, or can be set as a user override from the Guest Faculty screen.
| Knob | Default | Notes |
|---|---|---|
PORT |
8080 |
HTTP port. |
HOST |
0.0.0.0 |
Bind address. |
RUBY_HIGH_PUBLIC_BASE |
http://localhost:3000 |
Public URL the app is reachable at. Must be HTTPS in production — OpenRouter rejects HTTP callbacks. |
RUBY_HIGH_PRIVY_APP_ID |
— | Enables Privy account sign-in when set with RUBY_HIGH_PRIVY_CLIENT_ID and one server verifier secret. |
RUBY_HIGH_PRIVY_CLIENT_ID |
— | Public Privy client id embedded in the viewer so the browser SDK can initialize. |
RUBY_HIGH_PRIVY_LOGIN_METHODS |
email,wallet,google,twitter,passkey |
Comma-separated Privy login methods shown in the viewer. Use google for Gmail sign-in. Each method must also be enabled in the Privy dashboard. |
RUBY_HIGH_PRIVY_APP_SECRET |
— | Preferred server-side Privy secret for verifying tokens and fetching linked wallet/user details. Set via secrets only. |
RUBY_HIGH_PRIVY_VERIFICATION_KEY |
— | Optional JWT verification-key fallback for deployments that do not use RUBY_HIGH_PRIVY_APP_SECRET. |
RUBY_HIGH_STORE_BACKEND |
json |
json for local dev (atomic file at ~/.ruby-high/state.json), dynamodb for production. |
RUBY_HIGH_STATE_PATH |
~/.ruby-high/state.json |
JSON-backend file path. |
RUBY_HIGH_DYNAMO_TABLE |
— | Required when backend is dynamodb. |
AWS_REGION |
— | Required when backend is dynamodb. |
RUBY_HIGH_STATE_TTL_SECONDS |
90 days | DynamoDB TTL for idle sessions. |
RUBY_HIGH_ADMIN_TOKEN |
— | Enables /api/apps/ruby-high/admin/metrics. Keep this in secrets only. |
RUBY_HIGH_METRICS_TRUST_START |
— | Optional ISO date/time shown in admin metric quality notes after a metrics reset or schema migration. |
RUBY_HIGH_LLM_PROVIDER |
openrouter |
Set to local to use a local OpenAI-compatible /v1/chat/completions endpoint. Also inferred as local when RUBY_HIGH_LLM_BASE_URL is set. |
RUBY_HIGH_LLM_BASE_URL |
http://127.0.0.1:11434/v1 in local mode |
Local OpenAI-compatible base URL. Values ending in /v1 or /chat/completions are both accepted. |
RUBY_HIGH_LLM_MODEL |
ruby-high-local in local mode |
Model id sent to the local endpoint. Many single-model servers ignore it, but OpenAI-compatible servers require the field. |
RUBY_HIGH_LLM_API_KEY |
local in local mode |
Optional bearer token for local servers configured with an API key. |
RUBY_HIGH_STUDENT_MODEL |
google/gemini-3.5-flash |
Model used for NPC opinion responses. |
RUBY_HIGH_OPENROUTER_API_KEY |
— | Optional server-side OpenRouter key for hosted AI Access and hosted portrait/diploma generation. Server-hosted text AI is available only while the signed-in session has active AI Access. Browser-owned OpenRouter keys remain BYOK and do not spend Hall Passes. |
RUBY_HIGH_OPENROUTER_REFERER |
https://ruby-high.local |
Sent in OpenRouter request headers. |
RUBY_HIGH_OPENROUTER_TITLE |
Ruby High |
Sent in OpenRouter request headers. |
RUBY_HIGH_STRIPE_SECRET_KEY |
— | Enables web Hall Pass purchases via Stripe Checkout. |
RUBY_HIGH_STRIPE_WEBHOOK_SECRET |
— | Required for /api/apps/ruby-high/billing/stripe/webhook to grant Hall Passes after paid Checkout Sessions. |
RUBY_HIGH_STRIPE_CURRENCY |
usd |
Currency for built-in Hall Pass packs. |
RUBY_HIGH_HALL_PASS_5_CENTS |
199 |
Price for 5 Hall Passes. |
RUBY_HIGH_HALL_PASS_20_CENTS |
699 |
Price for 20 Hall Passes. |
RUBY_HIGH_HALL_PASS_50_CENTS |
1499 |
Price for 50 Hall Passes. |
RUBY_HIGH_HALL_PASS_100_CENTS |
2499 |
Price for 100 Hall Passes. |
RUBY_HIGH_SOLANA_RPC_URL |
https://api.mainnet-beta.solana.com |
Solana JSON-RPC endpoint used to verify token-transfer signatures for crypto pack purchases. |
RUBY_HIGH_SOLANA_NFT_RPC_URL |
RUBY_HIGH_SOLANA_RPC_URL |
Optional separate RPC endpoint for NFT minting. |
RUBY_HIGH_SOLANA_NFT_AUTHORITY_SECRET_KEY |
— | Server mint authority secret key for Metaplex Core pack and card NFTs. Also drives creator attribution in served JSON metadata. Set via secrets only. |
RUBY_HIGH_NFT_METADATA_STORAGE |
— | Optional durable metadata JSON upload mode. Set to arweave for direct AR uploads; unset keeps app-hosted metadata JSON. |
RUBY_HIGH_NFT_METADATA_ARWEAVE_JWK |
— | Arweave RSA JWK JSON used when RUBY_HIGH_NFT_METADATA_STORAGE=arweave. RUBY_HIGH_ARWEAVE_JWK, RUBY_HIGH_ARWEAVE_WALLET_JWK, and ARWEAVE_JWK are also accepted. |
RUBY_HIGH_NFT_METADATA_GATEWAY |
https://arweave.net |
Gateway prefix returned for uploaded metadata JSON. |
RUBY_HIGH_PACK_REVEAL_SECRET |
RUBY_HIGH_SOLANA_NFT_AUTHORITY_SECRET_KEY |
Server-only HMAC secret for deterministic pack-to-card mapping. Set a stable production secret so the mapping remains fair and non-public. |
RUBY_HIGH_SOLANA_CORE_COLLECTION_ADDRESS |
— | Metaplex Core collection address for Ruby High pack NFTs. Create once with npm run nft:create-core-collection, then set this value. |
RUBY_HIGH_SOLANA_CORE_CARD_COLLECTION_ADDRESS |
— | Metaplex Core collection address for Ruby High: First Bell card NFTs. Create once with npm run nft:create-card-collection, then set this value. |
RUBY_HIGH_SOLANA_MEMECOIN_MINT |
ABHQGzXNoRbJ1sjUsCJ2TmTAo1uMx4EUpV1qYiSVpump |
SPL-token mint accepted for crypto pack purchases. |
RUBY_HIGH_SOLANA_TREASURY_OWNER |
1cfpmRU4oriteHQ9vPEN1GGuvTGuHiuX7MQCotKnHxY |
Treasury wallet owner that must receive the SPL-token transfer. |
RUBY_HIGH_SOLANA_MEMECOIN_SYMBOL |
RUBY |
Display symbol for the Solana token. |
RUBY_HIGH_SOLANA_MEMECOIN_DECIMALS |
6 |
SPL-token decimal places used when converting quoted token amounts to base units. |
RUBY_HIGH_SOLANA_HALL_PASS_5_TOKENS |
1000000 |
$RUBY price for the 1-pack / 5 Hall Pass tier. |
RUBY_HIGH_SOLANA_HALL_PASS_20_TOKENS |
2800000 |
$RUBY price for the 3-pack / 20 Hall Pass tier (~7% volume discount vs 1-pack). |
RUBY_HIGH_SOLANA_HALL_PASS_50_TOKENS |
4500000 |
$RUBY price for the 5-pack / 50 Hall Pass tier (~10% volume discount vs 1-pack). |
RUBY_HIGH_SOLANA_HALL_PASS_100_TOKENS |
8500000 |
$RUBY price for the 10-pack / 100 Hall Pass tier (~15% volume discount vs 1-pack). |
RUBY_HIGH_HOSTED_AI_HALL_PASS_COST |
1 |
Hall Pass cost to activate server-hosted text AI for one timed window. |
RUBY_HIGH_HOSTED_AI_DURATION_HOURS |
168 |
Hosted AI Access duration. Ignored when RUBY_HIGH_HOSTED_AI_DURATION_MS is set. |
RUBY_HIGH_HOSTED_AI_DURATION_MS |
— | Optional exact hosted AI pass duration override. |
RUBY_HIGH_QUESTION_GENERATION_HALL_PASS_COST |
1 |
Hall Pass cost for server-hosted Generate More Questions when the browser has no OpenRouter key. |
RUBY_HIGH_MORE_QUESTIONS_COUNT |
6 |
Default number of cards requested by Generate More Questions. |
RUBY_HIGH_PORTRAIT_HALL_PASS_COST |
1 |
Hall Pass cost for server-hosted custom portraits. |
RUBY_HIGH_DIPLOMA_HALL_PASS_COST |
3 |
Hall Pass cost for server-hosted diploma images. |
RUBY_HIGH_HOSTED_IMAGE_PENDING_TTL_MS |
900000 |
Timeout before a stuck pending hosted-image charge is failed and refunded. |
RUBY_HIGH_COURSE_SLOT_HALL_PASS_COST |
3 |
Hall Pass cost to reserve/publish one creator course slot. The legacy RUBY_HIGH_COURSE_GENERATION_HALL_PASS_COST is still honored as a fallback. |
RUBY_HIGH_REVENUECAT_WEBHOOK_AUTH |
— | Required Authorization header value for /api/apps/ruby-high/billing/revenuecat/webhook. The route accepts either this exact value or Bearer <value>. |
RUBY_HIGH_REVENUECAT_VIRTUAL_CURRENCY_CODE |
HLP |
RevenueCat Virtual Currency code to credit as Hall Passes when using RevenueCat Virtual Currency events. |
RUBY_HIGH_CREATOR_DEFAULT_MODEL |
google/gemini-3.5-flash |
Default OpenRouter model for local teacher drafts created in Edit Pack. |
RUBY_HIGH_DRAFT_GENERATIONS_PER_DAY |
5 |
Per-teacher daily cap for draft question/course generation. |
RUBY_HIGH_COURSE_GENERATION_QUESTION_COUNT |
18 |
Default number of questions requested by AI course generation, clamped to 4–24. |
RUBY_HIGH_ALLOW_HTTP_MATERIAL_URLS |
— | Set to true only in trusted local/dev environments. Remote course-material imports require HTTPS by default and reject localhost/private/reserved hosts. |
RUBY_HIGH_ALLOWED_MATERIAL_HOSTS |
raw.githubusercontent.com,gist.githubusercontent.com |
Comma-separated list of additional trusted hosts for remote course-material imports. GitHub blob URLs are normalized to raw.githubusercontent.com. |
RUBY_HIGH_EVAL_MODEL |
openai/gpt-4.1-mini |
LLM-judge model for npm run eval:voice when an OpenRouter key is available. |
RUBY_HIGH_EVAL_REQUIRE_API |
— | Set to 1 to make npm run eval:voice fail when no RUBY_HIGH_OPENROUTER_API_KEY is configured. |
The /health route is readiness: it returns 200 only after services have booted, so the platform should not route first-load traffic while Ruby High is hydrating. /livez is a process-liveness probe. The server trusts x-forwarded-* headers from the first hop for proto, host, and client IP. |
No OpenRouter key is required on the server for normal play: each user can authenticate with their own key via PKCE, or use a Privy account for persistent identity/wallet ownership when Privy is configured. RUBY_HIGH_OPENROUTER_API_KEY enables hosted text AI only for sessions that spend a Hall Pass on AI Access, and enables hosted image generation with per-image Hall Pass costs. Edit Pack creates OpenRouter-backed local teacher drafts; Ruby High does not list, import, grant, or call external avatar/agent backends.
Ruby High now has two currencies:
- Merit Stars are earned by play and mirror the visible session-score payout.
- Hall Passes are paid/entitlement currency for hosted AI windows, creator course slots, extra student slots, and hosted image generation.
Web purchases use Stripe Checkout for Hall Passes only:
GET /api/apps/ruby-high/billing/productsreturns Hall Pass top-ups, AI Access cost/duration, hosted image costs, and the separate Solana pack-NFT quote surface.POST /api/apps/ruby-high/billing/ai-passspends Hall Passes to activate server-hosted text AI for the signed-in Ruby High cookie session. A second call while active returns the existing expiry and does not spend again.- Publishing a draft course reserves a creator course slot for 3 Hall Passes. BYOK/local course generation does not spend Hall Passes.
- Generate More Questions is free with browser OpenRouter or local LLM access; when it uses the server-hosted OpenRouter key, it spends 1 Hall Pass per run.
- Unlocking an extra student slot costs 1 Hall Pass and grants a Photo Day credit; hosted character portraits consume that credit before spending a Hall Pass.
POST /api/apps/ruby-high/billing/checkoutcreates a Stripe Checkout Session for Hall Passes for the signed-in Ruby High cookie session.POST /api/apps/ruby-high/billing/stripe/webhookverifies Stripe signatures and grants Hall Passes idempotently from Checkout metadata. Stripe does not sell card packs or NFTs.POST /api/apps/ruby-high/billing/card-burnverifies owner-signed card burns and credits 5 Hall Passes per burned card. Hosted features then spend Hall Passes normally.
Stripe webhook events to send: checkout.session.completed and, if using asynchronous payment methods, checkout.session.async_payment_succeeded.
Solana purchases are separate from Stripe and use the configured SPL token to mint a Metaplex Core pack NFT:
- The default token is
$RUBYmintABHQGzXNoRbJ1sjUsCJ2TmTAo1uMx4EUpV1qYiSVpump. - The default treasury wallet is
1cfpmRU4oriteHQ9vPEN1GGuvTGuHiuX7MQCotKnHxY. - Every built-in pack defaults to
100000$RUBY. - Create the Core collection once with
npm run nft:create-core-collection, then setRUBY_HIGH_SOLANA_CORE_COLLECTION_ADDRESSto the printed address. - Create the Core card collection once with
npm run nft:create-card-collection, then setRUBY_HIGH_SOLANA_CORE_CARD_COLLECTION_ADDRESSto the printed address. - The current First Bell runtime manifest has 24 mintable profiles and a 12-profile alternate-art expansion, for 36 draft profiles total in
src/services/hall-pass-card-catalog.ts. Revealed metadata includesSet,Set Code,Set Number,Card Profile ID,Card Name,Subject, media traits, and creator attribution. Reveal proof data stays underproperties.provenanceinstead of visible marketplace traits. - Wallet-facing card crops are plain images, not cards inside cards: students, teachers, and specials are tall avatar crops; items are square; locations are wide. Regenerate crops with
npm run nft:crop-cardsafter changing source art. Usenode scripts/generate-nft-grok-art.mjs --parallel 3 --ids <ids>to refresh Grok source art through OpenRouter before cropping. - Opening a pack marks the Core pack as opened, switches its metadata to opened artwork, and creates deterministic face-down card slots. Pack/card records and receipts carry
packRevealVersion,catalogHash,commitment,entropySource, and reveal-timerevealSeedprovenance; seedocs/nft/NFT_PROVABLY_FAIR_V1_1.mdfor the published algorithm. - The current v1.1 entropy source is an auditable server-commit bridge, not decentralized randomness. The next hardening step is a Solana pack-opening program that commits the open request and payment/authority lock first, then settles from Switchboard randomness.
- Marketplace submission copy, collection addresses, and Magic Eden verification steps are tracked in
docs/nft/NFT_MARKETPLACE_VERIFICATION.md. - To mint with durable JSON directly on Arweave, fund the Arweave wallet, set
RUBY_HIGH_NFT_METADATA_STORAGE=arweave, add the JWK secret, and verify a fresh pack/card mint returns anhttps://arweave.net/...metadata URI. Leave the flag unset if durable storage funding is not ready. - Wallet-signed pack checkout is atomic: the prepared transaction creates the treasury ATA if needed, transfers the configured SPL token with the Ruby High payment reference, and creates the Metaplex Core pack NFT. The connected wallet is the fee payer, and Ruby High co-signs only the pack authority/asset parts.
- Each face-down card is minted and revealed one at a time by the Ruby High mint authority to the connected wallet. The connected wallet is the fee payer and recipient; Ruby High co-signs only the mint authority and deterministic card mint.
- Owner-signed card burns are prepared one card per wallet prompt and preflighted before signing;
POST /api/apps/ruby-high/billing/card-burnverifies the burn signature and credits 5 Hall Passes per burned card. POST /api/apps/ruby-high/billing/solana/quoteaccepts the connected owner wallet and returns the treasury wallet, mint, per-session payment reference, token amount, payment-only transaction, and Solana Pay URL for a selected pack.POST /api/apps/ruby-high/billing/solana/confirmaccepts the signed payment transaction signature and owner wallet, then verifies the payment reference/token receipt before minting and recording the pack idempotently.
Native billing is not wired in the current public-web build. If iOS or Android comes back, do not use Stripe for digital in-app currency; create matching consumable in-app purchase products in App Store Connect and Google Play Console, validate receipts/purchase tokens server-side, then call the same Hall Pass grant path. RevenueCat can replace most receipt-validation boilerplate; the Ruby High server remains the authority that credits the wallet after validation.
RevenueCat setup:
- Use one Offering for the shop, for example
hall_passes, with consumable packages forhall_pass_5,hall_pass_20,hall_pass_50, andhall_pass_100. Product IDs with app prefixes are okay if they end in those IDs. - Set the RevenueCat app user ID to the Ruby High state key (
rh:user:<userId>). If the app sends just<userId>, the server prefixes it torh:user:<userId>. Anonymous RevenueCat IDs are ignored for wallet fulfillment. - Add a webhook pointing at
/api/apps/ruby-high/billing/revenuecat/webhookand set its Authorization header toBearer <RUBY_HIGH_REVENUECAT_WEBHOOK_AUTH>or exactly the configured value. - Send
NON_RENEWING_PURCHASEevents to grant Hall Passes andCANCELLATIONevents to revoke refunded Hall Passes. Refund events only debit a wallet when they match a previously recorded RevenueCat transaction; refund-first events are marked so a delayed purchase webhook for the same transaction cannot credit a refunded purchase. If using RevenueCat Virtual Currency, sendVIRTUAL_CURRENCY_TRANSACTIONevents and set the currency code toHLPor configureRUBY_HIGH_REVENUECAT_VIRTUAL_CURRENCY_CODE.
npm test
npm run check:full
npm run test:browser
npm run eval:voicecheck:full runs typecheck, the Vitest suite, and the offline SPA build. test:browser is the opt-in Playwright smoke target; it builds and launches the dev server, boots the viewer in Chromium, exercises guest play, account tabs, responsive framing, and the Privy bundle load path. eval:voice builds the package and runs the faculty-voice smoke harness; without an OpenRouter key it still verifies the local reference set and exits successfully unless RUBY_HIGH_EVAL_REQUIRE_API=1.
The suite covers the daily-class progression mechanic, the cohort, mentor mode, advantage roll, the phase machine, opinion grading, the chat layer, both store backends, the rate limiter, source-card distractor generation, pack routes, yearbook/admin routes, and the content-pack registry.
The current production deploy is Fly.io, driven locally by npm run deploy:
npm run deployThe App Runner workflow is retained as a legacy manual fallback only. The container itself is host-agnostic — anywhere that speaks Docker, sets PORT, and populates x-forwarded-* works.
For the IAM trust policies, the DynamoDB bootstrap, and the manual deploy fallback, see infra/README.md.
MIT for the code. The mechanics layer is CC BY 4.0 — see DESIGN.md §6 and §12.