jevstrudel.git / website / src / jev / README.md
README.mdpreviewREADME.mdsource400 lines · 26.8 KB · raw

website/src/jev

jevstrudel's additions to the website.

  • ReplPage.astro — the REPL page and its link preview. pages/index.astro renders it for the site, and pages/songs/[theme]/[song].astro once per song at /songs/<theme>/<song>/: that page opens on its song, and a link to it previews with the song's own card.

  • songs.mjs — the build-time song index. ReplPage.astro calls loadSongs() in its frontmatter: Vite globs songs/*/*/SPEC.md and song.js, Astro compiles each spec to HTML, and the page embeds the list as JSON (<script id="jev-songs">). The jev panel (repl/components/panel/WelcomeTab.jsx) reads it to list the songs (by theme, most art first), show a spec, and load and play a song. Each song's card shows its title, its duration (read from song.js), its README's first paragraph and its cover.webp.

  • client.mjs — the browser side: reads that JSON back and picks the song the page opens on (the song page's own, or the featured featuredSongId, Lightning in a Bottle, unless the URL carries code). It is selected on the jev panel (a song page opens on the song itself), loaded, and plays on the first click (browsers keep audio off until then): a song page whenever it is opened, the home page's featured song once per tab, and neither on a reload. Picking a song puts its address and title in the address bar and tab.

  • jevCore.mjs — jev({ state, questions }): TypeSafe's API as Strudel patterns, in the shape of its JavaScript SDK. Questions are built with the SDK's choice, score and noul; djJev.every(cycles, { fallback }) asks them all in one request every cycles cycles and returns the answers as patterns (move.choice, energy.score, …) to compose in Strudel; and walk(answer, { max, sections }) is a song's form as rules in code over one choice. A section's times is the most it plays in one performance: a loop is a cycle of sections, so only a total bounds it, and a section that has played its times (or leads to an ending only through one that has) is no longer offered. walk refuses a section, or a way on, that can never play within the cap (a detour the song would claim and Jev never hear of). The form's history rows also carry what each jev() asked after it settled for that section (arrangement: its levels, transitions and energy, less what it forgets), so the form's Jev hears what the arrangement built and released. Its header shows the whole shape. .every also takes after: { section } (asked once another jev on the cadence has answered, told its answer: questions in one request cannot see each other; its requests then also carry where the song is in the other's form, and each history row names the section it was), sample (draw a choice from Jev's probabilities at that temperature), minConfidence (fall back below it; not on a sampled question, since sampling is what a close call is for), forget (questions whose past answers are left out of the history Jev is sent: songs forget into, since Jev copied its own "cut") and allow (narrow a choice per request from what is decided, e.g. which transitions suit the section being entered; the fallback must stay allowed). History rows that played a fallback say "(fallback)". An answer that lands after its section has begun keeps the section and plays the rest of its answers from the next cycle. Before a song starts, askOpening asks each jev() about its opening, so the first section is Jev's call too. setWrittenParts records which part (orbit) plays in which cycles as written, from the song queried ahead: a level question named for a part is not asked about a section where that part does not play (it keeps its fallback, marked absent, and the mixer greys it). A question a song reads ahead (.early(1)) that sounded its fallback before the answer landed keeps it for that section. Each request also carries listenersSoFar, every listener's 🔥/😴 for the sections that matter (the last few played and those that may come next, only names the song's form has; reactions.mjs). setReplay makes the jev()s evaluated after it play a recorded performance instead of asking Jev: no request at all, and a recorded answer the song's rules no longer allow plays the fallback. Decisions can also come from outside: followDecisions() puts the starting song's jev()s in follow mode, where they ask nothing and play what they are handed (receive(segment, decision), one receiver per jev() in declaration order), each value checked against the song like an answer (a section, against the sections its form allows there); a segment with nothing received when it begins plays its fallbacks, and a decision that arrives after that is a late answer. watchDecisions() is the other end, every decision the song settles as shareDecision() shapes it, and setReactions() hands the jev()s reaction totals counted elsewhere (follow.test.mjs). jev.mjs wires it to the relay and is what the REPL's scope sees (jev, choice, score, noul, walk). Calls go through this site's relay (worker/), which adds the site's own TypeSafe key, because TypeSafe's API refuses calls from web pages; visitors need no key. jevCore.test.mjs covers it with a stubbed Jev, and allSongs.test.mjs evaluates every song the way the REPL does and plays it to its end against a stand-in Jev, checking each request against the relay's rules (nix flake check runs both).

  • JevPane.jsx — the pane under the editor: the booth above the mixer, both showing skeletons (8 empty sections, an idle desk) until a song with a jev() plays, and beside them Jev's request log.

  • jevLog.mjs, JevLog.jsx — Jev's request log, in place of popups: every section request (a line when asked, filled in with Jev's answer and its time when it lands) and every ⏭/📻 pick, newest on top, the page's last 200.

  • ConnectAi.jsx — how to connect your own AI to the hosted MCP: the jev panel's mcp tab and /ai/ (pages/ai.astro) both render it.

  • DataTab.jsx — the jev panel's data tab: everything the site stores, for anyone to read (worker/src/data.ts, "What is public" in worker/README.md): every account and its page, listeners' songs whole (held ones as text), covers, comments, pitches, Jev's queue, performances with Jev's decision history, reactions, votes, what is live, and the caches, each section saying what it holds and what it withholds (secrets, counted and dated only). Other people's writing is text here: nothing in it plays or shows an image.

  • profile.mjs, Profile.jsx, NameLink.jsx — profiles. Every display name (a song's author, a comment's or pitch's, a tab in "listening now", an account on the data tab) is a NameLink, which opens that account's profile in the jev panel, with a back link: its name and when it joined, its public songs (played as any listener song, in the sandbox), its public comments and pitches (and how many wait on Jev or were held), what it played lately, and what its tabs play now, from the lobby. It reads the data tab's account page (/jev/data/accounts/<id>).

  • activity.mjs, ActivityTab.jsx — the jev panel's activity tab: what happened, newest first (/jev/data/activity, worker/src/activity.ts): songs published and revised with Jev's verdict and score, comments, pitches, and every take with who played it and "▶ replay", thirty at a time with "older". A listener's song plays from it in the sandbox; a site song as its card.

  • parties.mjs, LiveParties.jsx — the listening tab's live parties (/jev/parties): each one's song, people and how long it has run, and "join" for one whose host is listed, which asks the room for its name (/jev/parties/<id>) and joins it as a guest (joinParty). A party's host is listed when its tab is (the lobby's "show me here", setListedCheck), and unticking or ticking it mid-party takes the party off the list or puts it back at once (listedChanged); worker/README.md, Listening parties, has why the id and not the name.

  • History.jsx — what a listener played (PlayedList): the you tab's "recently played" for the signed-in listener and a profile's "played lately", from the account's page. Its own takes (every performance it played signed in), each with its time, its path and "▶ replay", merged newest first with the radio's plays that have no take (playedLately in profile.mjs); every song plays again as its card plays it.

  • budget.mjs, JevBudget.jsx — today's Jev budget where calls are made: in the booth's title row, and on each ⏭ pick in the request log (JevPicks.jsx). Signed out, calls share the address's allowance.

  • social.test.mjs — the plain logic of the five above.

  • JevBooth.jsx — Jev made visible while a song plays, at the top of the pane under the editor: a cell per section (each answer, and whether Jev answered, is being asked, or could not be reached; click one for every answer's probabilities and what Jev was told) and 🔥/😴 buttons Jev hears; and (JevBoothEffects) each request into the request log and the playing choices lit up in the code. boothHighlight.mjs also colours Jev's API in the editor. All of it reads boothStore.mjs, which jev() writes and each successful evaluation swaps.

  • reactions.mjs — every listener's 🔥/😴: each press in the booth is also posted to the site's /jev/reactions (worker/src/listening.ts), counted per song, section and reaction. The playing song's tally is fetched as each song loads; the song's view on the jev panel shows it per section, and Jev hears it as listenersSoFar.

  • performance.mjs, JevPerformance.jsx — recorded performances. When a song as written ends, or is stopped or replaced after its opening section, the recorder posts what every jev() played, segment by segment (/jev/performances), with the SHA-256 of its code, and the booth offers "share this performance": /songs/<theme>/<song>/?performance=<id>. Opening that link fetches the performance before the song is evaluated and replays it (jevCore's setReplay): the same sections, levels and choices, no Jev calls, and the booth says "replaying a performance" (and that the song has changed since, when its code hash differs). Picking another song ends the replay. The Worker adds who played it (signed in) and when. A listener's song is recorded too, from the sandbox's booth (each jev() apart, rebuilt by sandboxProtocol.mjs), and its link is /?listener=<id>&performance=<id>: the page fetches the performance and hands it to the frame with the play (armSandboxReplay), so the replay runs in the sandbox like the song, never in the page. The stored rows are also Jev's decision history: nix run .#jev-history -- <song> (tools/jev-history/).

  • takes.mjs, JevTakes.jsx — Jev's takes on a song's view (a site song's, beside its 🔥/😴 tally, and a listener song's, in Listeners.jsx): how Jev moves through the song over its recorded performances (a form map in written order: a line for the song as written, an arc for every other way Jev went, thicker the more often, and per section how often it played, Jev's answer times, how often Jev could not be reached, and the sections it picked next with their shares), and every take, newest first, with who played it and when, its path through the form and "▶ replay", the take's replay link (performance.mjs). The written order is the booth's form while the song plays here, else read from the code's walk() (writtenOrder), else unknown (ALL GREEN builds its form in code), when the sections are placed where Jev plays them and nothing is called "as written". Read from worker/src/listening.ts's /jev/performances?song= and /jev/performances/moves?song=; shows nothing where there is no Worker or no take.

  • JevMixer.jsx — the same answers as a mixing desk, under the booth: a fader per part level (a score, or a choice among out/back/full), a selector knob per choice, a knob per noul, the form's section on an LCD, and live level meters per part and for the master. Controls glide to each section's settings as it begins; a dashed ghost shows the next section's once Jev has answered.

  • meter.mjs — the song's loudness as it plays, read off what superdough sends the speakers (after its master limiter, output.speakers) and off each orbit's output: songs put every part Jev levels on an orbit named for its question (.orbit('riff')), so each part is heard alone. Each request tells Jev how the finished sections measured, in total and per part (measuredSound), and the strip's detail view tables it.

  • measured.mjs — a song's measured mix as written, recorded in its SPEC.md as measured: by tools/measure/ (one JSON line, with the hash of the song.js it measured), whole and per section on the song's .every(N) cadence (sectionCycles); pages get each section's total only. The art critic, recorded or asked live on unedited code, reads it as measuredMix while the hash still matches.

  • preload.mjs — a song's samples, loaded before its first bar: the song is queried as written (asWritten in jevCore.mjs, which asks Jev nothing), and the scheduler's start waits for the samples its first cycles play while the rest load behind them. superdough otherwise drops a note whose sample is still loading.

  • ask.mjs — one Jev call through the site's relay; everything below uses it. The relay answers a request it has already answered, byte for byte, within the hour from its cache, and says which in each answer's Jev-Cache header (hit or miss; CACHE_HEADER). Each request also tells the relay, in headers it logs and never forwards, how many seconds before its section begins it went out (from the scheduler, via jev.mjs) and which attempt it is (worker/README.md, Logs). For a signed-in listener, each answer the relay charged carries Jev-Budget: used/limit, and once the day's budget is spent a 429 with Jev-Budget: spent and Retry-After until 00:00 UTC: calls back off until then (songs play their fallbacks) and say the budget is spent.

  • account.mjs, AccountControl.jsx — accounts, signed in with a passkey (worker/README.md, Accounts). The header's "🔑 sign in" opens a panel to sign in with a passkey or make an account (a display name, and a passkey on this device); signed in, it shows the name, today's Jev calls against the daily budget, "add a passkey" (another device) and "sign out". account.mjs asks the Worker who is signed in (/jev/auth/me) and runs the ceremonies with @simplewebauthn/browser; the session is an HttpOnly cookie the page never reads. The panel also lists the AI apps connected through the hosted MCP (listApps, /jev/me/apps), each with a "disconnect". Nothing else needs an account.

  • myTabs.mjs — the hosted MCP's end in the page (worker/README.md, The hosted MCP; /ai/ explains it to visitors): while someone is signed in, the tab joins that account's own hub over /jev/me/tabs, under the same session id as the dev hub's, and answers what the listener's own AI asks: play, stop, the editor's code, the sandbox's logs since the last play, and status. What the AI plays is foreign code ({ ai: app }) and plays only in the sandbox; playForeign answers once that play has started or failed. The jev panel's mcp tab says so, with the tab's id, while it is joined.

  • tabTools.mjs, tabCommands.mjs — the tools both MCPs share (worker/README.md, The tools both MCPs share), answered by the tab: the sounds tab's sounds, Jev's pick of a sound (soundPick.mjs) and of a song (the mood picker), the settings tab's allowed settings (worker/src/mcp-settings.mjs), the console tab's lines, a song played by id as its card plays it, and a 🔥/😴 (press.mjs, which the booth's buttons press too). tabTools.mjs takes the page's modules as deps, so it runs in vitest; tabCommands.mjs supplies the real ones, loaded on the first such command by myTabs.mjs and by the dev hub's tab (repl/useWebSocketMCP.jsx).

  • sounds.mjs — the sounds tab's sounds as data: which sub-tab each is under (inCategory, which the sounds tab filters with), its variants, and how a song plays it (a drum machine's <machine>_<sound> as a bank).

  • soundPick.mjs — Jev choosing a sound for a description: one Choice among up to 255 names; past that, banks of at most 255 (several to a request), then one question among each bank's three likeliest, as Krug's JevLM does. Asked through ask.mjs, so charged as the page's own calls.

  • reference.mjs — the reference tab's functions out of jsdoc's doc.json (referenceFunctions, which the tab renders), and as text for the MCP's api_reference (referenceData, pages/jev/reference.json.js). jev's own entries (jev, choice, score, noul, walk, tagged jev) are the jsdoc in jevCore.mjs, which package.json's jsdoc-json reads beside packages/; reference.test.mjs checks every name in the REPL scope has one and plays each example, and the hosted MCP's primer song, against a stand-in Jev (playThrough.mjs, shared with allSongs.test.mjs).

  • radioHistory.mjs — a signed-in listener's radio history, kept by the Worker (/jev/me/radio): the radio records each song it starts, and picker.mjs's notRecent keeps the most recently played (on any device) out of Jev's choices, leaving at least three. Signed out, the radio remembers only the tab, as before.

  • SongListening.jsx — a song's page, its listening: 👥 listen together, who has the song open now and how far through (WhoIsListening narrowed to the song), and the live parties on it to join (LiveParties narrowed likewise). The page puts comments first, then this, then the song.

  • party.mjs, ListenTogether.jsx — listening parties. "👥 listen together" on a song's view in the jev panel starts one and gives a link, /songs/<theme>/<song>/?party=<room>; whoever opens it hears the same song with the same Jev decisions, in time with the host, joining mid-song at the cycle the host is on. The host's page is the only one that asks Jev: it sends each decision its jev()s settle (watchDecisions) through the party's room, a Durable Object in the Worker (worker/README.md, Listening parties, has the protocol), and a guest's jev()s follow them (followDecisions). Each page estimates the room's clock from a few pings; the host sends when its scheduler started, and a guest's scheduler starts at the host's cycle now (the cyclist's lastEnd). 🔥/😴 from anyone go to the room, which tells everyone the totals, so the host's Jev hears every listener; the booth shows how many are in. A guest follows the host from song to song (the radio's picks included; a guest's own radio stays off) and stops when the host does. The host's tab keeps the party's key in sessionStorage, so a reload rejoins as host.

  • lobby.mjs, Lobby.jsx — who is listening now (worker/README.md, The lobby). Every page joins the site's lobby and says what its tab plays: a site song (and whether edited), a listener's song, or its own code; playing or not, the playhead and tempo, the song's end once known (or the most it can last while Jev walks its form) and the section. The jev tab's "👥 listening now" lists everyone else's tabs grouped by who (an account's display name, or "a listener" when not signed in; the Worker says which, never the page), with each song's progress moving at its tempo, "🎧 listen along" for a site song that is playing, "play it" for one that is not, and "open it" for a listener's song. The header's 👥 is how many pages are open and opens the list. A tab is listed by default (the user asked for a more social site, 2026-09-27); "show me here" hides it, kept per browser. Listening along is a listening party: the tab asked starts one on its song (or passes on the one it is in) and its room reaches the asker only, whose page joins as a guest (joinParty in party.mjs) and plays in sync with the host's Jev.

  • picker.mjs, JevPicks.jsx, nextTrack.mjs — Jev beyond the booth: the jev panel's mood picker ("how do you feel?"); beside play, "⏭ next" (Jev picks another song now) and "autoplay" (when a song ends, Jev picks the next; the jev panel's 📻 toggle is the same setting, kept per browser); and "🎨 is it art?" in the booth's title row, which asks the art critic about whatever is in the editor. Next and autoplay share one path (nextTrack.mjs): Jev hears what the listener played (signed in, the account's radio history, whose most recent songs it is not offered, and each pick is added to it; signed out, this tab's), and when the editor holds no site song as written it picks from that history alone. A party's guest has neither: the host picks.

  • agree.mjs, AgreeWithJev.jsx — the jev panel's "do you agree with Jev?": pick the more-art of two songs, then see the critic's scores and how often you agree with it. Your tally stays in the browser (localStorage), counted against the critic's current scores; each vote is also posted, fire and forget, to the site's /jev/votes (worker/src/votes.ts), which keeps only a count per pair and pick. "Everyone's votes" (VoteResults.jsx, read from /jev/votes/summary once opened) ranks the songs as listeners do, pair by pair (a Bradley–Terry fit, bradleyTerry in agree.mjs, each song starting from one win and one loss against an average song so a few votes move it little), beside Jev's rank by art, with the overall agreement and the pairs where listeners outvote Jev; every figure carries its count, and a song with fewer than 5 votes is faded. The agreement report is agreementReport, which nix run .#agreement (tools/agreement/) prints too.

  • critic.mjs — the art critic's question, and how it reads a song's spec and code: one source for every "N% art", recorded (tools/critic/, the mean of several runs, written into SPEC.md by its setRevisionArt and setCritic) or asked live (one run). The rubric itself lives in worker/src/art-rubric.mjs, re-exported here, because the Worker scores listeners' songs with the same questions.

  • listeners.mjs, Listeners.jsx — listeners' content (worker/src/content.ts; worker/README.md, Listeners' content). The jev tab's "listeners" section lists public listener songs as cards, most art first; a card fetches the song whole and plays it, and its view (jev › listeners › title) shows its cover, author, art, revisions and spec as plain text. Signed in: "publish the editor's code as a song", and on your own song's view a new revision from the editor or a cover. Every song's view (the site's and listeners') has comments, and the section has pitches for Jev songs, most voted first or newest: signed in, ▲ votes for one (once per account, pressed again to take it back; who voted is public, on the data tab), and a pitch that became a song links to it. The song's maker says so, never the pitch's author: a site song with pitch: in its SPEC.md, a listener song from "answers a pitch" on its own view (pitches.mjs). "What you wrote" lists your items as public, held (Jev's verdict, only you see it) or pending (why, and when Jev is asked again), asking again every 20 s while anything waits. A listener song's address is /?listener=<id> (it has no page of its own; client.mjs's showSong writes it and useReplContext loads it, without playing). A listener's song is untrusted code and plays only in the sandbox below, never in this page.

  • sandbox.mjs, sandbox/player.mjs, sandboxProtocol.mjs, sandboxPolicy.mjs, SandboxNotice.jsx — the isolation boundary for untrusted code (worker/README.md, "A listener's song runs in a sandbox"). A listener's song, and code arriving in a link, play in an iframe with an opaque origin (sandbox="allow-scripts", no allow-same-origin) under a strict CSP, so their code has no cookies, no storage of the site's, no way into this page, and its Jev calls reach the relay without the visitor's session. sandbox.mjs is the page's end: it tracks each code's provenance (foreign vs the page's own; guardEditor makes the editor play foreign code only in the frame, and the page's own scheduler refuse to start while it holds foreign code), owns the frame, and forwards the frame's Jev calls to the relay: with credentials: 'omit' for a listener's song or a link's code, and with the listener's own session for code their own AI played (charged to their budget; the frame never holds the session). Plays are numbered (run) so the page knows which play a state is about. sandbox/player.mjs is a Strudel player with nothing of the page's, run inside the frame. sandboxProtocol.mjs rebuilds every postMessage from typed, bounded fields — nothing from the frame is evaluated or rendered as HTML. sandboxPolicy.mjs is the one source of the CSP and CORS, for the meta tag, the deploy's _headers, and the dev server. SandboxNotice.jsx is the line above the editor while its code is someone else's. The site's own songs are first-party and keep running in the page (visuals, parties, performance recording); only untrusted code is sandboxed.

  • releases.mjs, UpdateModal.jsx — release notes from the repo's RELEASES.md, every release a heading that expands: when a new deploy is waiting, a modal shows what changed and an "update now" button reloads into it (src/pwa.ts); after a return visit, what is new since; and the page's version at the jev panel's foot (ReleaseButton) opens them all, the newest expanded.

  • site.mjs — the site's name, tagline, URL, accent colour and description: the tab title, the app manifest, and link previews.

  • og.mjs — the link-preview images, 1200×630, drawn at build time: the site's (/og.png, nine covers beside the name) and each song's (/og/<theme>/<song>.png, its cover beside its title, "N% art" and length).

pages/jev/songs.json.js builds /jev/songs.json, the site's song ids, which the Worker checks each vote against. pages/jev/catalog.json.js and pages/jev/catalog/[theme]/[song].json.js build the site's songs as the hosted MCP lists them and reads each whole (spec and code; worker/src/catalog.ts), and pages/jev/reference.json.js the reference tab's functions for api_reference (worker/src/reference.ts). pages/ai.astro is /ai/, "Make songs with your AI": how to connect an MCP client, and the tools, read from worker/src/hosted-tools.ts.

pages/jev-samples/strudel.json.js beside it builds the sample map songs load (samples('jev-samples/strudel.json')) from the repo's samples/ folder, with sampleMap.mjs, the builder tools/worktree-samples/ also uses to give headless tools a worktree's own samples.