From eb88138d152ef3ba80ab53b42d093ccfea62a937 Mon Sep 17 00:00:00 2001 From: karti-ai <176560021+karti-ai@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:09:59 -0700 Subject: [PATCH] Frontend: site chrome, demo shell, pages, and the contract gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five parallel lanes plus an integration pass. The header, gallery, router and sitemap are all generated from the demo registry, so adding src/demos// puts a demo everywhere with zero edits to shared files — which is the whole reason demo nine cannot break demo one. check-demos enforces the twelve contract rules: 142 checks over one live demo. Two worth naming. The shell may not mention a specific slug, because an 'if (slug === wordle)' in src/components/demo/ is a contract bug wearing a patch. And a spec-status demo must ship a real specification — task, actions, grader, counterweight, eval command — since a coming-soon card reads worse than an honest empty gallery. Bundle budget holds: entry 108.79 kB gzipped against a 160 kB ceiling, the demo chunk 21.15 kB against 90 kB. recharts is 108 kB gzipped and lives behind a lazy import so it never touches the entry. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_019mt6sHQHEnEYrJZvoMCJSB --- public/icons/favicon.svg | 9 + public/manifest.webmanifest | 10 + public/robots.txt | 6 + public/traces/wordle/base-on-s0.json | 98 ++++ public/traces/wordle/base-on-s1.json | 98 ++++ public/traces/wordle/base-on-s2.json | 158 ++++++ scripts/check-demos.mjs | 2 +- scripts/check-receipts.mjs | 4 +- scripts/new-demo.mjs | 18 +- scripts/sitemap.mjs | 10 +- src/components/demo/BlindCompare.tsx | 31 +- src/components/demo/CodeReceipt.tsx | 102 ++-- src/components/demo/DemoCard.tsx | 39 +- src/components/demo/DemoErrorBoundary.tsx | 26 +- src/components/demo/DemoShell.tsx | 577 +++++++++------------- src/components/demo/LimitsCallout.tsx | 19 +- src/components/demo/MetricChart.tsx | 5 +- src/components/demo/MetricMover.tsx | 6 +- src/components/demo/ProvenanceCard.tsx | 22 +- src/components/demo/ReasoningDrawer.tsx | 65 +-- src/components/demo/ReasoningPanel.tsx | 5 +- src/components/demo/RewardBreakdown.tsx | 2 +- src/components/demo/RewardEditor.tsx | 307 ++++++------ src/components/demo/SegmentedControl.tsx | 109 ++++ src/components/demo/TracePlayer.tsx | 42 +- src/components/demo/format.tsx | 19 +- src/demos/_template/meta.ts | 44 ++ src/demos/_template/reward.py | 53 ++ src/demos/_template/surface.tsx | 81 +++ src/lib/demo-kit/registry.ts | 20 +- src/pages/DemoPage.tsx | 34 ++ 31 files changed, 1341 insertions(+), 680 deletions(-) create mode 100644 public/icons/favicon.svg create mode 100644 public/manifest.webmanifest create mode 100644 public/robots.txt create mode 100644 public/traces/wordle/base-on-s0.json create mode 100644 public/traces/wordle/base-on-s1.json create mode 100644 public/traces/wordle/base-on-s2.json create mode 100644 src/components/demo/SegmentedControl.tsx create mode 100644 src/demos/_template/meta.ts create mode 100644 src/demos/_template/reward.py create mode 100644 src/demos/_template/surface.tsx create mode 100644 src/pages/DemoPage.tsx diff --git a/public/icons/favicon.svg b/public/icons/favicon.svg new file mode 100644 index 0000000..ca0b292 --- /dev/null +++ b/public/icons/favicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..f1a7769 --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,10 @@ +{ + "name": "PIG Demo", + "short_name": "PIG Demo", + "description": "Interactive demos of reinforcement-learning environments.", + "start_url": "/", + "display": "standalone", + "background_color": "#fafafa", + "theme_color": "#7c3aed", + "icons": [{ "src": "/icons/favicon.svg", "sizes": "any", "type": "image/svg+xml" }] +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..f13b370 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,6 @@ +# The inverse of primeintellectgrowth.com, deliberately: being found is the +# entire point of this site. +User-agent: * +Allow: / + +Sitemap: https://demo.primeintellectgrowth.com/sitemap.xml diff --git a/public/traces/wordle/base-on-s0.json b/public/traces/wordle/base-on-s0.json new file mode 100644 index 0000000..2b23a60 --- /dev/null +++ b/public/traces/wordle/base-on-s0.json @@ -0,0 +1,98 @@ +{ + "runId": "base-on-s0", + "seed": 0, + "model": "brain-qwen38-dspark", + "capturedAt": "2026-08-28", + "rewards": { + "solved": 1.0, + "economy": 1.0, + "consistency": 0.6 + }, + "metrics": { + "guesses_used": 3.0, + "rejected_replies": 2.0, + "reference_depth": 4.0, + "turns_granted": 6.0, + "inconsistent_guesses": 1.0 + }, + "truncated": false, + "outcome": "solved", + "answer": "wants", + "turns": [ + { + "reply": "\n\n[slate]", + "reasoning": "We need answer user: \"Enter your guess to begin.\" We need play word guessing game. Need output only square-bracketed token. Need choose first guess. Need likely 5-letter word. We need maximize information. Common starting word: CRANE, SLATE, ADIEU, AUDIO, RAISE, SLANT, CRATE? Need only bracketed token. No other brackets. Let's think: secret 5-letter word, feedback G/Y/X. Need guess. We can output [crane] maybe. Need ensure lowercase? Example [crane]. Use a common word with distinct letters and common vowels. \"crane\" has c r a n e distinct, good. Alternative \"slate\" s l a t e distinct. \"raise\" has r a i s e. \"adieu\" a d i e u. For first guess, choose high-frequency letters. Common Wordle starting words: CRANE, SLATE, AUDIO, RAISE, LATER, CRATE, SLANT. \"crane\" includes a,e,n,r,c. \"slate\" includes s,l,a,t,e. \"raise\" includes r,a,i,s,e. Which better? Need likely maximize. Maybe \"crane\" is standard. Could choose \"slate\" includes s,l,t. But \"crane\" includes c,r,n. Wordle top letters? e, a, s, r, t, o, i, n, l. \"slate\" has s,l,a,t,e: five top letters. \"crane\" has c,r,a,n,e: c lower. \"raise\" has r,a,i,s,e: top. \"raise\" distinct and top vowels. \"audio\" a,u,d,i,o has multiple vowels, but no consonants top. \"slate\" maybe better. Need one guess. Let's output [slate]? The prompt example [crane], maybe acceptable. We need only bracketed token. I'll choose [slate] maybe. But if they expect valid 5-letter word, slate is valid. Could choose [raise]. Which is most information? Need distinct letters. \"slate\" has s,l,a,t,e; all common. \"raise\" has r,a,i,s,e; all common. \"slate\" includes t/l; \"raise\" includes r/i. Both good. Maybe \"slate\" is often recommended. Use [slate]. Ensure no other brackets. Final only [slate].\n", + "call": { + "promptTokens": 183, + "completionTokens": 516, + "reasoningTokens": null, + "durationMs": 20464, + "finishReason": "stop" + }, + "info": { + "guess": "slate", + "observation": "S L A T E\nY X Y G X\n\nYou have 5 guesses left." + } + }, + { + "reply": "", + "reasoning": "Let's parse the feedback.\n\nGuess: S L A T E\nPositions:\n1 S: Y (in word, wrong position)\n2 L: X (not in word)\n3 A: Y (in word, wrong position)\n4 T: G (correct position)\n5 E: X (not in word)\n\nSo T is position 4. S and A are in word, not at positions 1 and 3 respectively. L and E are out.\n\nNeed 5-letter word with T at pos4, includes S and A (S not pos1, A not pos3). No L, E.\n\nPossible patterns: _ _ A? T _? Wait A not pos3, S not pos1. T pos4.\nLet's think common words: ? ? ? T ?.\nContains S and A.\nMaybe \"CAST?\" no T pos4? C A S T ? => A pos2, S pos3, T pos4. A not pos3 ok, S not pos1 ok. But E? no. Word cast? 5 letters: cast? \"cast\" 4. \"casts\" = C A S T S. Has S pos3 and pos5, T pos4. But S appears twice? We know S in word; if guess S pos1 Y, could be one or more S. \"casts\" possible. But feedback for S only one S guessed, Y means at least one S not pos1. If word has S pos3/5, okay.\nBut we need guess next. Need maximize info.\n\nCandidates with T4, A,S, no L/E:\n- \"casts\": C A S T S. Letters C,A,S,T,S.\n- \"cast?\" no.\n- \"basts\"? B A S T S (bast? 4, basts plural). \n- \"gasts\"? no.\n- \"has?\" H A S T ? \"hast\" 4.\n- \"masts\" M A S T S.\n- \"rasts\"? no.\n- \"tasts\"? no.\n- \"vast?\" V A S T ? \"vasts\" plural.\n- \"past?\" P A S T ? \"pasts\".\n- \"lasts\" L no.\n- \"casts\", \"masts\", \"pasts\", \"vasts\", \"basts\" maybe.\nBut A could be elsewhere, S elsewhere.\nWords: \"atlas\" has L no. \"astir\" no T4? A S T I R T pos3.\n\"status\" 6.\n\"stats\" S T A T S T pos2,4? Contains S,T,A. T pos4? S T A T S: positions S1 T2 A3 T4 S5. T pos4 yes. But A pos3, feedback A at pos3 would be G, but we got Y for A, so not A pos3. So not stats.\n\"stunt\" no A.\n\"staid\" D? T pos3.\n\"stave\" E no.\n\"stare\" E no.\n\"stall\" L no.\n\"stain\" T pos3.\n\"stand\" T pos3.\n\"stamp\" T pos3.\n\"stump\" T pos3.\n\"strut\" T pos3? S T R U T T5.\n\"strap\" T pos3? S T R A P.\n\"strop\" T pos3? S T R O P.\n\"stout\" T pos3? S T O U T.\n\"stole\" E/L no.\n\"stone\" E no.\n\"stern\" E no.\n\"stilt\" L/I? T pos3.\n\"stint\" T pos3? S T I N T.\n\"stint\" T pos5? Actually S T I N T: T pos2 and 5.\n\"stint\" no A.\n\nT pos4 words: \"stint\" no.\nPattern ?? ? T ?.\nCommon 5-letter: \"about\" A B O U T T5.\n\"above\" E.\n\"abort\" A B O R T T5.\n\"abuse\" E.\n\"adapt\" D A P T ? T4? A D A P T: T5.\n\"adobe\" E.\n\"adore\" E.\n\"adult\" L.\n\"after\" E.\n\"again\" G A I N N no T.\n\"agone\" E.\n\"agony\" G A G O N no T.\n\"agape\" E.\n\"ahead\" E.\n\"ahold\" L.\n\"aided\" E.\n\"ailed\" E/L.\n\"aimed\" E.\n\"aired\" E.\n\"aisle\" E/L.\n\"alike\" L/E.\n\"aloud\" L.\n\"amend\" E.\n\"amiss\" A M I S S T no.\n\"among\" M? no T.\n\"anion\" no.\n\"ankle\" L/E.\n\"anted\" E.\n\"antic\" A N T I C T3.\n\"anvil\" L.\n\"apart\" A P A R T T5.\n\"apex\" E.\n\"apnea\" E.\n\"apron\" no T.\n\"aptly\" L.\n\"arbor\" no T? A R B O R.\n\"arena\" E.\n\"argot\" A R G O T T5.\n\"arise\" E.\n\"armed\" E.\n\"armor\" no.\n\"aroma\" no.\n\"arson\" no T.\n\"artsy\" A R T S Y T3.\n\"aside\" E.\n\"askew\" E.\n\"aspen\" E.\n\"assay\" no T.\n\"atlas\" L.\n\"atone\" E.\n\"attic\" A T T I C T2/3.\n\"audio\" no.\n\"audit\" A U D I T T5.\n\"aunty\" no.\n\"avail\" L.\n\"avert\" E.\n\"avoid\" no.\n\"await\" A W A I T T5.\n\"awake\" E.\n\"awful\" L.\n\"axial\" L.\n\"azure\" E.\n\"bacon\" no T.\n\"badge\" E.\n\"bagel\" E/L.\n\"baker\" E.\n\"baler\" L/E.\n\"balm\" L.\n\"banal\" L.\n\"band\" 4.\n\"bang\" 4.\n\"banjo\" no.\n\"barge\" E.\n\"baron\" no.\n\"baser\" E.\n\"basic\" B A S I C no T.\n\"basil\" L.\n\"baste\" B A S T E T4, E no.\n\"batch\" B A T C H T3.\n\"bated\" E.\n\"bates\" B A T E S T3, E.\n\"bathe\" E.\n\"batik\" B A T I K T3.\n\"baton\" B A T O N T3.\n\"bawdy\" no.\n\"beach\" E.\n\"beady\" E.\n\"beast\" B E A S T E, T5.\n\"beaut\" 4.\n\"becue\" E.\n\"beech\" E.\n\"beef\" E.\n\"beige\" E.\n\"being\" E.\n\"belch\" E/L.\n\"belt\" E/L.\n\"bench\" E.\n\"bend\" E.\n\"bened\" E.\n\"bento\" E.\n\"beret\" E.\n\"berry\" E.\n\"beset\" E.\n\"betel\" E/L.\n\"bevel\" E/L.\n\"bible\" E/L.\n\"bicep\" E.\n\"biddy\" no.\n\"bight\" B I G H T T5.\n\"biker\" E.\n\"billy\" L.\n\"binet\" E.\n\"biped\" E.\n\"birch\" no T? B I R C H.\n\"bison\" no.\n\"biter\" E.\n\"blare\" L/E.\n\"blase\" L/E.\n\"blat\" 4.\n\"blaze\" L/E.\n\"bleak\" E/L.\n\"bleat\" E/L.\n\"blent\" E/L.\n\"blimp\" L.\n\"blint\" B L I N T L no, T5.\n\"blitz\" L.\n\"bloat\" L.\n\"block\" L.\n\"bloom\" L.\n\"blot\" 4.\n\"bluer\" L/E.\n\"blunt\" L.\n\"blurt\" L.\n\"boast\" B O A S T T5.\n\"boast\" T5, not pos4.\n\"boar\" 4.\n\"boast\" no.\n\"boast\" not.\n\"boast\" no.\n\"bobby\" no.\n\"bogey\" E.\n\"bogus\" no T.\n\"boil\" L.", + "call": { + "promptTokens": 220, + "completionTokens": 2048, + "reasoningTokens": null, + "durationMs": 78996, + "finishReason": "length" + }, + "info": { + "guess": null, + "observation": "That guess was not accepted: no bracketed guess found.\nIt did not use up a turn. You have 5 guesses left." + } + }, + { + "reply": "\n\n[pasta]", + "reasoning": "I need to provide a bracketed guess. Based on the feedback for \"slate\":\nS Y\nL X\nA Y\nT G\nE X\n\nSo T is in the correct position (4th). S and A are in the word but not in those positions. L and E are not in the word.\n\nLet me think of a 5-letter word with T in position 4, containing S and A somewhere else.\n\nPattern: _ _ _ T _\nContains S (not position 1) and A (not position 3)\n\nPossible words:\n- CASTO? No, A can't be in position 3.\n- WASTE? No E.\n- MASTS? No, S is position 5 and 1, but S can't be position 1.\n- PASTA? No, A is position 5, S is position 4... wait T is position 4. P-A-S-T-A: T is 4th (G), A is 2nd and 5th, S is 3rd. But A can't be 3rd (it's 2nd and 5th, that's fine). S is 3rd (not 1st, good). But we have two A's. Let me check: PASTA - P, A, S, T, A. T is 4th \u2713. S is 3rd (not 1st) \u2713. A is 2nd and 5th (not 3rd) \u2713. This could work!\n\n- RUSTS? R-U-S-T-S: T is 4th \u2713, S is 3rd and 5th (not 1st) \u2713, but no A.\n- WASTE? No E.\n- CASTE? No E.\n- PASTA looks good.\n\nOther options:\n- MASTS: M-A-S-T-S - T is 4th \u2713, S is 3rd and 5th \u2713, A is 2nd \u2713. But two S's.\n- TASTY: T-A-S-T-Y - T is 1st and 4th. But T should only be 4th (if there's only one T). Actually we don't know if there's more than one T. But T at position 1 would be G, not Y. So if the word has T at position 1, it would show G. Since we only got one G for T, there's likely only one T at position 4.\n\nLet me go with PASTA.\n", + "call": { + "promptTokens": 262, + "completionTokens": 555, + "reasoningTokens": null, + "durationMs": 18834, + "finishReason": "stop" + }, + "info": { + "guess": "pasta", + "observation": "P A S T A\nX G Y G X\n\nYou have 4 guesses left." + } + }, + { + "reply": "", + "reasoning": "Let's analyze the clues.\n\nGuess 1: slate\nS L A T E\nY X Y G X\n\nMeaning:\n- S: Y (in word, wrong position)\n- L: X (not in word)\n- A: Y (in word, wrong position)\n- T: G (correct position, position 4)\n- E: X (not in word)\n\nSo word has T at position 4. Contains S and A, but not at positions 1 and 3 respectively. No L, no E.\n\nGuess 2: pasta\nP A S T A\nX G Y G X\n\nMeaning:\n- P: X (not in word)\n- A: G (correct position, position 2)\n- S: Y (in word, wrong position)\n- T: G (correct position, position 4)\n- A: X (not in word? but A at position 2 is G...)\n\nWait, in Wordle-style feedback, duplicate letters are tricky. In \"pasta\", A appears at positions 2 and 5. The feedback shows A at position 2 as G, and A at position 5 as X. That means there's exactly one A in the word, at position 2.\n\nSo far:\n- Position 2: A\n- Position 4: T\n- Contains S (not at position 3, since in \"slate\" S was at position 1 and Y, and in \"pasta\" S at position 3 was Y)\n\nWait, in \"slate\", S was at position 1, feedback Y means S is in word but not at position 1.\nIn \"pasta\", S was at position 3, feedback Y means S is in word but not at position 3.\n\nSo S is in the word, not at position 1, not at position 3. Position 2 is A, position 4 is T. So S must be at position 5? Or... positions available: 1, 3, 5. Not 1, not 3, so S at position 5.\n\nWait, but position 2 is A and position 4 is T, so S can be at 1, 3, or 5. Not 1 (from slate), not 3 (from pasta). So S at position 5.\n\nHmm, but wait. Let me double-check. Actually, let me reconsider whether S could be elsewhere. The word is 5 letters: _ A _ T S? Let me verify.\n\nPosition 1: not S (from slate Y), not P (from pasta X), not L (from slate X)\nPosition 2: A\nPosition 3: not A (from pasta, the second A was X), not S (from pasta Y)\nPosition 4: T\nPosition 5: S (deduced)\n\nSo pattern: _ A _ T S\n\nNot in word: L, E, P\nIn word: S, A, T\n\nRemaining unknowns: position 1 and position 3.\n\nCommon 5-letter words matching _ A _ T S:\n- \"BANTS\" - B A N T S \u2713\n- \"CHANTS\" is 6 letters, no\n- \"DANTS\" no\n- \"FANTS\" no\n- \"HANTS\" no\n- \"JANTS\" no\n- \"MANTS\" - M A N T S \u2713 (mants is a word, plural of mant)\n- \"NANTS\" - N A N T S \u2713 (nants, plural of nant, a river)\n- \"OANTS\" no\n- \"RANTS\" - R A N T S \u2713\n- \"SANTS\" - S A N T S \u2713 (saints is 6... sants? not common)\n- \"TANTS\" - T A N T S \u2713\n- \"UANTS\" no\n- \"WANTS\" - W A N T S \u2713\n- \"YANTS\" - Y A N T S \u2713 (yants, to yank? not common)\n\nAlso \"BARTS\" - B A R T S \u2713\n\"CATS\" is 4 letters. \"CANTS\" - C A N T S \u2713\n\"DARTS\" - D A R T S \u2713\n\"FASTS\" - F A S T S \u2713\n\"GANTS\" - G A N T S \u2713\n\"HANTS\" - H A N T S \u2713\n\"LANTS\" - L is excluded\n\"MARTS\" - M A R T S \u2713\n\"PANTS\" - P is excluded\n\"PARTS\" - P excluded\n\"RATS\" is 4. \"RANTS\" already listed.\n\"SATTS\" no\n\"STATS\" - S T A T S, no, position 1 is S but position 3 is A... wait STATS is S-T-A-T-S, that's position 2 = T, not A. No.\n\"TARTS\" - T A R T S \u2713\n\"VANTS\" no\n\"WARTS\" - W A R T S \u2713\n\"WASTS\" no\n\"YARDS\" no.\n\nSo candidates: BANTS, BARTS, CANTS, DARTS, FASTS, GANTS, HANTS, MANTS, MARTS, NANTS, RANTS, TANTS, TARTS, WANTS, WARTS, YANTS...\n\nAlso \"_A_T S\" where position 3 could be other letters:\n- \"BASTS\"? not a word\n- \"CASTS\" - C A S T S \u2713\n- \"DASTS\" no\n- \"FARTS\" - F A R T S \u2713\n- \"GASTS\" no\n- \"JASTS\" no\n- \"LASTS\" - L excluded\n- \"MASTS\" - M A S T S \u2713\n- \"NASTS\" no\n- \"PASTS\" - P excluded\n- \"RATTS\" no\n- \"SASTS\" no\n- \"TASTS\" - T A S T S? not a word\n- \"VASTS\" - V A S T S \u2713 (vasts, verb)\n- \"WASTS\" no\n- \"YASTS\" no\n\nHmm, lots of candidates. I need a good guess that will discriminate.\n\nI have 4 guesses left. Let me think about what letters to test.\n\nPattern: _ A _ T S\nPosition 1: unknown\nPosition 3: unknown\n\nCommon letters for position 1: R, W, D, B, C, F, M, T, G, H, V, N, Y\nCommon letters for position 3: N, R, S\n\nThe most common pattern would be _AN TS or _AR TS or _AS TS.\n\nLet me think of a good guess. I want to maximize information.\n\nIf I guess a word like \"RANTS\" (R A N T S):\n- If R is G, N is G, then it's RANTS (solved).\n- If R is Y, then R is somewhere else... but R can only be at position 1 or 3. \n- etc.\n\nActually, let me think about what's most likely. Common words: RANTS, WANTS, DARTS, BANTS, TARTS, MARTS, CASTS, MASTS, FARTS, WARTS.\n\nHmm, \"WANTS\" and \"RANTS\" and \"DARTS\" are quite common.\n\nLet me pick a guess that tests multiple possibilities. \n\nA good strategy: guess a word that contains letters likely to be in position 1 and 3.\n\nLet me guess \"DARTS\" - tests D at 1, R at 3.\nOr \"WANTS\" - tests W at 1, N at 3.\nOr \"RANTS\" - tests R at 1, N at 3.\n\nActually, I think I should test the most common letters. Let me consider that position 3 is very likely N, R, or S.\n\nLet me guess \"RANTS\":\n- R at 1, A at 2, N at 3, T at 4, S at 5.\n\nPossible outcomes:\n- If all G: RANTS is the answer.\n- If R is Y: R is in word but not position 1, so R at position 3. Then word is _ A R T S = BARTS, DARTS, MARTS, TARTS, WARTS, FARTS.\n- If N is Y: N is in word but not position 3. Hmm, where would N be? Position 1? Then N A _ T S = NANTS. Or N could be... only positions 1 and 3 are open. So N at 1.\n- If R is X and N is X: neither R nor N is in the word. Then position 1 and 3 are other letters.\n\nThis seems like a decent guess. But maybe I can do better.\n\nActually, let me reconsider. Since I have 4 guesses left and the search space is already quite constrained (_ A _ T S with known exclusions), I should just pick a likely word.\n\nThe most common words fitting _A_T_S:\n- RANTS (common)\n- WANTS (very common)\n- DARTS (very common)\n- BANTS (common)\n- TARTS (common)\n- MART", + "call": { + "promptTokens": 298, + "completionTokens": 2048, + "reasoningTokens": null, + "durationMs": 71636, + "finishReason": "length" + }, + "info": { + "guess": null, + "observation": "That guess was not accepted: no bracketed guess found.\nIt did not use up a turn. You have 4 guesses left." + } + }, + { + "reply": "\n\n[wants]", + "reasoning": "Let me carefully analyze the clues from my two guesses.\n\nGuess 1: S L A T E\nFeedback: Y X Y G X\n\nSo:\n- S: Y (in word, wrong position)\n- L: X (not in word)\n- A: Y (in word, wrong position)\n- T: G (correct position, position 4)\n- E: X (not in word)\n\nSo position 4 is T. S and A are in the word but not in positions 1 and 3 respectively.\n\nGuess 2: P A S T A\nFeedback: X G Y G X\n\nSo:\n- P: X (not in word)\n- A: G (correct position, position 2)\n- S: Y (in word, wrong position)\n- T: G (correct position, position 4)\n- A: X (not in word... wait, but A was G in position 2)\n\nWait, this is interesting. In PASTA, position 2 is A and it's G. Position 5 is A and it's X. But A appears twice in the word PASTA. The feedback says A at position 2 is G (correct) and A at position 5 is X. This means there's only one A in the secret word, and it's at position 2.\n\nNow from guess 1: S L A T E\n- S: Y (S is in word, not at position 1)\n- L: X\n- A: Y (A is in word, not at position 3) - but we now know A is at position 2, so this is consistent\n- T: G (T at position 4)\n- E: X\n\nFrom guess 2: P A S T A\n- P: X\n- A: G (A at position 2)\n- S: Y (S in word, not at position 3)\n- T: G (T at position 4)\n- A: X (second A not in word, confirming only one A)\n\nSo what we know:\n- Position 2: A\n- Position 4: T\n- S is in the word, not at position 1 (from guess 1) and not at position 3 (from guess 2)\n- L, E, P are not in the word\n- Only one A in the word (at position 2)\n\nS can be at position 5 (since not 1, not 3, not 2 which is A, not 4 which is T).\n\nWait, S is not at position 1 (from SLATE where S was at pos 1 and got Y) and not at position 3 (from PASTA where S was at pos 3 and got Y). So S must be at position 5.\n\nSo we have: _ A _ T S\n\nNow I need to figure out positions 1 and 3.\n\nLetters we know are not in the word: L, E, P\nLetters we know are in the word: A (pos 2), T (pos 4), S (pos 5)\n\nI need to guess a word matching _ A _ T S.\n\nCommon 5-letter words with pattern _ A _ T S:\n- BANTS\n- CANTS\n- DANTS\n- FANTS\n- HANTS\n- JANTS\n- KANTS\n- MANTS\n- NANTS\n- OANTS\n- QUANTS\n- RANTS\n- SANTS\n- TANTS\n- UANTS\n- VANTS\n- WANTS\n- YANTS\n\nWait, but S is already at position 5, so SANTS wouldn't work (S at pos 1 and 5).\n\nActually, let me reconsider. The pattern is _ A _ T S.\n\nCommon words:\n- BANTS\n- CANTS\n- DANTS\n- FANTS\n- HANTS\n- KANTS\n- MANTS\n- RANTS\n- WANTS\n\nHmm, but I should also consider that maybe S isn't at position 5. Let me re-check.\n\nFrom SLATE: S at position 1 \u2192 Y (S is in word, wrong position)\nFrom PASTA: S at position 3 \u2192 Y (S is in word, wrong position)\n\nSo S is not at position 1 or 3. Position 2 is A, position 4 is T. So S must be at position 5. Yes, that's correct.\n\nLet me think of common words: _ A _ T S\n- BANTS (not very common)\n- CANTS (not super common)\n- DANTS (not common)\n- FANTS (not common)\n- HANTS (not common)\n- KANTS (not common)\n- MANTS (not common)\n- RANTS (common!)\n- WANTS (very common!)\n\nI'll go with WANTS as it's the most common word.\n\nWait, but I need to make sure W and N are valid. Let me just guess WANTS.\n\nActually, let me think if there are other possibilities. What about words where the pattern isn't _ A _ T S?\n\nNo, I'm confident:\n- Pos 2: A\n- Pos 4: T\n- Pos 5: S\n- Pos 1: ?\n- Pos 3: ?\n\nWANTS seems like the best guess. Let me go with that.\n", + "call": { + "promptTokens": 340, + "completionTokens": 1181, + "reasoningTokens": null, + "durationMs": 36181, + "finishReason": "stop" + }, + "info": { + "guess": "wants", + "observation": "W A N T S\nG G G G G\n\nYou have 3 guesses left." + } + } + ] +} diff --git a/public/traces/wordle/base-on-s1.json b/public/traces/wordle/base-on-s1.json new file mode 100644 index 0000000..6802d59 --- /dev/null +++ b/public/traces/wordle/base-on-s1.json @@ -0,0 +1,98 @@ +{ + "runId": "base-on-s1", + "seed": 1, + "model": "brain-qwen38-dspark", + "capturedAt": "2026-08-28", + "rewards": { + "solved": 1.0, + "economy": 0.75, + "consistency": 0.6 + }, + "metrics": { + "guesses_used": 4.0, + "rejected_replies": 1.0, + "reference_depth": 3.0, + "turns_granted": 6.0, + "inconsistent_guesses": 2.0 + }, + "truncated": false, + "outcome": "solved", + "answer": "amber", + "turns": [ + { + "reply": "\n\n[slane]", + "reasoning": "We need answer user: \"Enter your guess to begin.\" We need play word-guessing game. Need output only square-bracketed token? Prompt says Think it through, then give your guess as the only square-bracketed token in your reply, like [crane]. The game reads first bracketed token and ignores everything else, so do not put any other words in brackets.\nWe need choose first guess. No feedback yet. Need likely pick common 5-letter word maximizing info. Could be [slane] or [crane]? Need only bracketed token. We can think in analysis. Need final only [guess]. Choose strong initial guess. Common Wordle starting words: crane, slane, raise, audio, slant, plant, crane. We need 5-letter. I'd pick [crane] common. But maybe better [slane] has distinct common letters S L A N E. [crane] C R A N E also distinct common. [slane] covers vowels A E, common consonants S L N. [raise] R A I S E. [audio] all vowels. [plant] P L A N T. Since no feedback, any. Maybe [slane] is widely recommended. Need ensure 5 letters. Use [slane]. Final only [slane].\n", + "call": { + "promptTokens": 183, + "completionTokens": 282, + "reasoningTokens": null, + "durationMs": 10711, + "finishReason": "stop" + }, + "info": { + "guess": "slane", + "observation": "S L A N E\nX X Y X Y\n\nYou have 5 guesses left." + } + }, + { + "reply": "\n\n[cared]", + "reasoning": "Let's analyze the feedback for \"slane\":\n\nS: X - not in word\nL: X - not in word\nA: Y - in word, wrong position\nN: X - not in word\nE: Y - in word, wrong position\n\nSo the word contains A and E, but not S, L, N.\nA is not in position 3.\nE is not in position 5.\n\nLet me think of good next guesses. I need to find more letters while placing A and E correctly.\n\nA can be in position 1, 2, 4, or 5\nE can be in position 1, 2, 3, or 4\n\nLet me try a word that tests common letters. I'll try \"crate\" - but wait, C, R, A, T, E.\n- A in position 3 (but A can't be in position 3, it's Y so wrong position)\n- E in position 5 (but E can't be in position 5, it's Y so wrong position)\n\nThat won't work well. Let me think of better options.\n\nHow about \"trade\"? T, R, A, D, E\n- A in position 3 - wrong (A can't be in pos 3)\n- E in position 5 - wrong (E can't be in pos 5)\n\n\"prate\"? P, R, A, T, E - same issues.\n\n\"grape\"? G, R, A, P, E - same issues.\n\nI need A not in position 3, E not in position 5.\n\nLet me try \"cares\"? C, A, R, E, S - S is out.\n\n\"areas\"? A, R, E, A, S - S is out.\n\n\"rates\"? R, A, T, E, S - S is out.\n\n\"trape\"? T, R, A, P, E - A in pos 3 (bad), E in pos 5 (bad)\n\n\"peaks\"? P, E, A, K, S - S is out.\n\n\"creak\"? C, R, E, A, K - E in pos 3, A in pos 4. Let me check: A not in pos 3 \u2713, E not in pos 5 \u2713. This could work!\n\n\"grace\"? G, R, A, C, E - A in pos 3 (bad)\n\n\"pride\"? P, R, I, D, E - E in pos 5 (bad)\n\n\"pried\"? P, R, I, E, D - E in pos 4. This works for E. But no A.\n\nLet me think about what I want to test. I know A and E are in the word. I need to find 3 more letters.\n\nCommon letters I haven't tested: C, D, F, G, H, I, K, M, O, P, R, T, U, V, W, Y\n\nLet me try \"cried\"? C, R, I, E, D - tests C, R, I, D. E in pos 4 (valid position for E). No A though.\n\nHow about \"pride\"? P, R, I, D, E - E in pos 5 (invalid).\n\n\"pried\"? P, R, I, E, D - E in pos 4. Tests P, R, I, D.\n\n\"cries\"? C, R, I, E, S - S is out.\n\n\"grids\"? G, R, I, D, S - S is out.\n\n\"tried\"? T, R, I, E, D - E in pos 4. Tests T, R, I, D.\n\nLet me try \"tried\" - T, R, I, E, D\n- E in position 4 (valid, not position 5)\n- Tests T, R, I, D\n\nActually, let me think more strategically. I want to maximize information.\n\n\"pride\" - E in pos 5 is bad.\n\"pried\" - P, R, I, E, D - E in pos 4 \u2713\n\nLet me go with \"pried\" to test P, R, I, D and place E in position 4.\n\nWait, actually let me reconsider. Maybe I should try to place A as well.\n\n\"cares\" has S. \"cared\" - C, A, R, E, D - A in pos 2, E in pos 4. Both valid positions!\n- A in pos 2 (not pos 3) \u2713\n- E in pos 4 (not pos 5) \u2713\n- Tests C, R, D\n\nThat's good! \"cared\" tests C, R, D and potentially places A in pos 2 and E in pos 4.\n\nLet me go with \"cared\".\n", + "call": { + "promptTokens": 220, + "completionTokens": 1076, + "reasoningTokens": null, + "durationMs": 30001, + "finishReason": "stop" + }, + "info": { + "guess": "cared", + "observation": "C A R E D\nX Y Y G X\n\nYou have 4 guesses left." + } + }, + { + "reply": "", + "reasoning": "We need solve puzzle. Need respond with only square-bracketed token first. Let's analyze carefully.\n\nGame: 5-letter word. Guess 1: slane -> feedback X X Y X Y.\nPositions: 1 s X not in word. 2 l X not in word. 3 a Y letter in word elsewhere. 4 n X not in word. 5 e Y letter in word elsewhere.\nSo word contains A and E, not S,L,N. A not pos3, E not pos5.\n\nGuess2: cared -> C A R E D feedback X Y Y G X.\nPositions: 1 c X not in word. 2 a Y letter in word elsewhere. 3 r Y letter in word elsewhere. 4 e G correct pos4. 5 d X not in word.\nSo E at pos4. A in word not pos2 and not pos3? From guess1 A pos3 Y, guess2 A pos2 Y. So A not pos2, pos3. Also contains R not pos3. No C,D,S,L,N.\nKnown: positions: _ _ _ E _. Contains A,R,E. A not 2,3; R not 3. Need 5-letter word? Need guess.\n\nWe have 4 guesses left. Need choose informative guess. Need maybe solve likely word. Let's enumerate possible words? We don't have dictionary but can reason. Need output a guess. Could be common word. Contains A,R,E, E at 4. A positions possible 1 or 5 (not 2,3). R positions possible 1,2,5 (not 3,4? E at 4, R not 3; could R pos4? no E pos4, so R not 4). So A in {1,5}, R in {1,2,5}. Need two other letters, none of excluded S,L,N,C,D.\n\nPossible patterns:\nA pos1: _? Actually A1, E4. R in 2 or5.\nPattern A R _ E _ or A _ _ E R.\nA pos5: R in 1 or2.\nPattern R _ _ E A or _ R _ E A.\nAlso R and A could same? No distinct letters.\nNeed 5-letter English word. Let's think of words with E fourth, A first/last, R second/fifth.\nA R _ E _: ARE? words: argle? no L excluded. arele? no. arrow? A R R O W? E not pos4. \"arere\" no. \"argue\" A R G U E: E pos5 not 4. \"arete\" A R E T E: E pos3 and5. \"arere\" no. \"argye\"? \"arise\" A R I S E: S excluded E5. \"aroma\" A R O M A: no E. \"arrow\" no. \"arson\" no.\nA _ _ E R: \"abner\"? no N. \"acred\" C/D excluded. \"adzer\" D/Z? \"aglet\" no. \"ahler\"? \"aider\" D. \"amper\" P? \"anthe\"? no. \"apher\"? \"apter\"? \"argen\"? \"asher\" S. \"aster\" S. \"ather\"? \"atone\" no R. \"auter\"? \"aver?\" \"awler\" L. \"ayner\"? N. \"azide\" no.\nR _ _ E A: \"renea\"? \"rhea\"? 5: \"rhea\" 4. \"reata\" (rhea plural) R E A T A: E pos2 not 4. \"renea\" no. \"resea\" S. \"rhea?\" \"rhea\" no. \"rhea?\" \"rhea\" not. \"rhea\" 4.\n_ R _ E A: \"areal\" A? \"areal\" A R E A L: E3, A1/4, L excluded. \"arete\" no. \"areal\" no. \"areta\" no. \"arene\" E? \"arene\" A R E N E N excluded E3/5. \"arete\" no. \"argue\" no. \"aroma\" no. \"array\" A R R A Y: no E. \"arrea\" no. \"arrea\"? \"arrea\" not common. \"arrea\" maybe? \"arrea\" not.\nCould be less common. Need maximize info. We can guess a word that tests candidate positions and likely letters. We know E pos4. Need find A/R positions and other letters. We can guess a word with known letters? We need valid 5-letter word? Probably allowed any 5 letters? Usually word-guessing game likely only valid words. Need choose from dictionary. We can use feedback to narrow. Need maybe use common word containing E4 and A/R. Candidate \"bared\"? B A R E D but D excluded. \"bared\" has D. \"baker\" B A K E R: A2 not allowed? Actually A pos2 would be Y? But we know A not pos2. Baker A2 E4 R5; if secret is baker, guess cared: A pos2 Y, R pos3? Baker R5, so R Y, E G, C/D X. Fits. But baker has A pos2, contradicts A not pos2? Wait feedback from guess2: A at pos2 is Y, means letter A is in word but somewhere else. So if secret baker, A pos2 would be G, not Y. So not baker. \"barer\" B A R E R: A2, no. \"barer\" not.\n\"berth\" no A/R. \"berne\" N. \"berga\"?\n\"cares\" C excluded. \"cared\" no. \"cares\" no.\n\"credo\" C/D no.\n\"dared\" D no.\n\"dater\" D no.\n\"debar\" D no.\n\"debra\" D no.\n\"derby\" D no.\n\"eared\" E1? E pos1 and4? But E pos4 G, E1 would maybe X? If word has E only one? Could have multiple E? Wordle counts. Guess cared E pos4 G, if secret has another E elsewhere, okay. But feedback only says E G at pos4; doesn't rule out E elsewhere. However guess1 E pos5 Y indicates at least one E not pos5. Could be E pos4 only. Need consider duplicate E? Usually 5-letter word can have repeated letters. Feedback algorithm: For each letter occurrence. Guess1 E at pos5 Y: there is E in word not pos5. Guess2 E pos4 G: E at pos4. If only one E, then no other E. If multiple, could be.\nKnown not in word: S,L,N,C,D. But if repeated? X means not in word at all (for that guess occurrence? In Wordle, X means letter not in word, yes). So no S,L,N,C,D.\nNeed choose next guess. We can use candidate set mentally. Let's enumerate likely words with E4 and A/R constraints.\nAll 5-letter words with E at 4, A at 1 or5, R at 2 or5 (if A1) or R at1/2 (if A5), no S,L,N,C,D.\nLet's list by pattern:\n1) A R _ E _ (A1 R2 E4). Need third and fifth letters, not excluded. Common: \"arele\" no, \"arene\" N, \"arete\" E3/5, \"argue\" E5, \"argye\"? \"arere\"? \"arise\" S, \"aroma\" no E, \"arrow\" no E, \"arsen\" S/N, \"arter\" T? \"arter\" A R T E R: E4, A1 R2, T5? Actually A R T E R: positions A1 R2 T3 E4 R5. Fits! Contains no excluded. \"arter\" is a word? Yes, a type of? Arter? Maybe \"arter\" (one who arteries?) not common but valid? \"arter\" maybe not. \"arere\" no. \"argue\" no. \"arere\" no. \"argle\" L. \"argot\" no E. \"arere\"? \"arere\" maybe? \"arete\" no. \"arere\" no. \"arere\". \"arere\".\n\"arere\" not.\n\"arere\".\n\"arere\".\n\"arere\".\nCould be \"arere\"? no.\n\"arere\".\n\"arere\".\nMaybe \"arere\" not.\n\"arere\".\nLet's not dwell.\n2) A _ _ E R (A1 E4 R5). Need pos2,3. Common: \"abler\" L excluded, \"abner\" N, \"acred\" C/D, \"adler\" L, \"adzer\" D/Z, \"aglet\" L, \"ahner\" H/N, \"aider\" D, \"aimer\" I M? \"aimer\" A I M E R: fits! A1 I2 M3 E4 R5. No excluded. Common. \"aimer\" is a word. \"aimer\" likely. \"aimer\" fits all feedback: slane: A pos3? guess1 sl", + "call": { + "promptTokens": 256, + "completionTokens": 2048, + "reasoningTokens": null, + "durationMs": 83968, + "finishReason": "length" + }, + "info": { + "guess": null, + "observation": "That guess was not accepted: no bracketed guess found.\nIt did not use up a turn. You have 4 guesses left." + } + }, + { + "reply": "\n\n[arete]", + "reasoning": "I need to provide a bracketed guess.\n\nKnown:\n- slane: S X, L X, A Y, N X, E Y\n- cared: C X, A Y, R Y, E G, D X\n\nSo E is correct at position 4 (from cared: C A R E D -> E is 4th position G).\nA is in word but not position 2 (from cared A Y at pos2; from slane A Y at pos3).\nR is in word but not position 3 (from cared R Y at pos3).\nE is in word at pos4.\nS,L,N,C,D are out.\n\nNeed 5-letter word with E at pos4, A somewhere not 2 or 3, R somewhere not 3.\nLetters positions: 1 _ , 2 _ , 3 _ , 4 E, 5 _\nA not pos2/3 -> A pos1 or pos5.\nR not pos3 -> R pos1,2,5.\n\nPossible words: ? ? ? E ?\nWith A and R.\nMaybe \"ARETE\"? no A pos1 R pos2 E pos3 E pos4 T pos5 -> but E pos3 would be G? We only know E pos4 G. E pos3 not tested.\nNeed common 5 letter.\n\"ROATE\"? no.\n\"RAVEN\" no N out.\n\"RATED\" D out.\n\"RATE?\" 4.\n\"ARENA\" N out.\n\"RACE?\" C out.\n\"AROSE\" S out.\n\"ARETE\" maybe.\n\"RAISE\" S out.\n\"RAKE?\" 4.\n\"ROATE\" obscure.\n\"RAVEN\" no.\n\"RATED\" no.\n\"RAGOE\"? no.\n\"RAKEE\"?\nMaybe A pos5: _ _ _ E A.\n\"ROBEA\"? no.\n\"RUBEA\"? no.\n\"URETA\"? no.\n\"UREAS\" S out.\n\"UREAL\" L out.\n\"UREIC\" C out.\n\"UREID\" D out.\n\"URENE\" E? maybe.\n\"URENA\" N out.\n\"URETA\" T?\n\"URETE\" T?\n\"UREID\" D out.\n\"UREAL\" L out.\n\"UREAS\" S out.\n\"UREIC\" C out.\n\"UREID\" D out.\n\"URENE\" maybe not common.\n\"URENE\" not.\n\"URETE\" not.\n\"URETA\" not.\nMaybe \"UREAL\" no L.\nA pos1: A _ _ E _.\n\"ARETE\" A R E T E -> E pos3 and pos4. But E pos4 G. E pos3 would also be in word, okay. R pos2 okay. T pos5.\n\"ARETE\" is a word.\n\"ARENE\" A R E N E -> N out.\n\"ARETE\" plausible.\n\"ARPEG\"? no.\n\"AREAS\" S out.\n\"ARETE\" good.\nBut need R not pos3, R pos2 okay.\nA pos1 okay.\nE pos4 okay.\nT pos5.\nLet's guess [arete].\n\nWait, \"ARETE\" has E at pos3 and pos4. If E pos3 is actually not in word? But E is in word at pos4 only? It could be multiple E.\nBut maybe better to test T.\n\"ARETE\" seems reasonable.\n\nAlternative: \"ROATE\" no.\n\"RAVEN\" no.\n\"RATED\" no.\n\"RAGE?\" 4.\n\"RAGOE\" no.\n\"RAKEE\" no.\n\"RALES\" L,S out.\n\"RALES\" no.\n\"RAVEN\" no.\n\"RATES\" S out.\n\"RATE?\" 4.\n\"RATED\" D out.\n\"RATEN\" N out.\n\"RAVEN\" N out.\n\"RAVEN\" no.\n\"RAVEN\" no.\n\"RAVEN\" no.\n\"RAVEN\" no.\n\"RAVEN\" no.\n\"RAVEN\" no.\n\"RAVEN\" no.\n\"RAVEN\" no.\n\nMaybe \"ROATE\" is a word? to roate? not common.\n\"ROATE\" maybe not.\n\"ROATE\" no.\n\n\"ARETE\" is Greek virtue, maybe in word list.\nLet's guess [arete].\n", + "call": { + "promptTokens": 298, + "completionTokens": 1006, + "reasoningTokens": null, + "durationMs": 34384, + "finishReason": "stop" + }, + "info": { + "guess": "arete", + "observation": "A R E T E\nG Y Y X X\n\nYou have 3 guesses left." + } + }, + { + "reply": "\n\n[amber]", + "reasoning": "Let's analyze the clues carefully.\n\nGuess 1: S L A N E\nFeedback: X X Y X Y\n- S: X (not in word)\n- L: X (not in word)\n- A: Y (in word, wrong position)\n- N: X (not in word)\n- E: Y (in word, wrong position)\n\nSo A and E are in the word, but not in positions 3 and 5 respectively.\n\nGuess 2: C A R E D\nFeedback: X Y Y G X\n- C: X (not in word)\n- A: Y (in word, wrong position) \u2014 A is in position 2, but wrong position\n- R: Y (in word, wrong position) \u2014 R is in position 3, but wrong position\n- E: G (correct letter, correct position) \u2014 E is in position 4\n- D: X (not in word)\n\nSo E is confirmed in position 4. A and R are in the word but not in positions 2 and 3 respectively.\n\nGuess 3: A R E T E\nFeedback: G Y Y X X\n- A: G (correct letter, correct position) \u2014 A is in position 1\n- R: Y (in word, wrong position) \u2014 R is in position 2, but wrong position\n- E: Y (in word, wrong position) \u2014 E is in position 3, but wrong position\n- T: X (not in word)\n- E: X (not in word) \u2014 Wait, but we already know E is in position 4. Let me reconsider.\n\nActually, the feedback for duplicate letters can be tricky. Let's think about this more carefully.\n\nFrom guess 2: C A R E D \u2192 X Y Y G X\n- E is in position 4 (G)\n\nFrom guess 3: A R E T E \u2192 G Y Y X X\n- A is in position 1 (G)\n- R is Y (in word, wrong position) \u2014 R was in position 2, so R is not in position 2\n- E is Y (in word, wrong position) \u2014 E was in position 3, so E is not in position 3. But we know E is in position 4 from guess 2.\n- T is X (not in word)\n- E is X \u2014 This is the second E in position 5. Since we only have one E in the word (in position 4), this makes sense.\n\nSo far we know:\n- Position 1: A\n- Position 4: E\n- R is in the word but not in position 2 or 3 (from guess 2, R was in position 3 and got Y; from guess 3, R was in position 2 and got Y)\n- So R must be in position 5 (since positions 1 and 4 are taken by A and E)\n\nWait, let me reconsider. R is in the word. From guess 2, R was in position 3 and got Y, so R is not in position 3. From guess 3, R was in position 2 and got Y, so R is not in position 2. Position 1 is A, position 4 is E. So R must be in position 5.\n\nNow we have:\n- Position 1: A\n- Position 4: E\n- Position 5: R\n\nWe need to fill positions 2 and 3.\n\nLetters we know are NOT in the word: S, L, N, C, D, T\n\nLetters we know ARE in the word: A, E, R\n\nWe need two more letters for positions 2 and 3.\n\nLet me think about common 5-letter words matching A _ _ E R:\n- ACHER? No, that's not common\n- AIDER? No, that's A I D E R, but D is excluded\n- AGGER? No\n- AHER? No, need 5 letters\n- A _ _ E R\n\nWait, let me reconsider. The pattern is A _ _ E R.\n\nCommon words:\n- ACHER (not common)\n- AGGER (not a word)\n- AIDER (D is excluded)\n- AIMER (M, I)\n- APER? No\n- ARER? No\n\nHmm, let me think of words matching A??ER:\n- ACHER\n- AIDER (D excluded)\n- AIMER\n- AMBER (M, B)\n- ANGER (N excluded)\n- APER? No\n- ASHER (S excluded)\n- ATHER (T excluded)\n- AURER? No\n- AVER? No\n\nWait, I should also consider that maybe I'm wrong about R being in position 5. Let me re-examine.\n\nFrom guess 2: C A R E D \u2192 X Y Y G X\n- R is in position 3, feedback Y means R is in the word but not in position 3.\n\nFrom guess 3: A R E T E \u2192 G Y Y X X\n- R is in position 2, feedback Y means R is in the word but not in position 2.\n\nSo R is not in position 2 or 3. Position 1 is A, position 4 is E. So R must be in position 5. That seems correct.\n\nSo the pattern is A _ _ E R.\n\nLet me think of common words:\n- ACHER (not really a common word)\n- AIDER (D excluded)\n- AIMER (I, M)\n- AMBER (M, B)\n- ANGER (N excluded)\n- ASHER (S excluded)\n- ATHER (T excluded)\n- AURER (not a word)\n- AVER? No, that's 4 letters\n\nHmm, what about:\n- ACHER - not common\n- AIDER - D excluded\n- AIMER - possible\n- AMBER - possible\n- ANGER - N excluded\n- ASHER - S excluded\n\nWait, what about words like:\n- ACHER\n- AGGER\n- AIDER\n- AIMER\n- AMBER\n- ANGER\n- APER\n- ASHER\n- ATHER\n- AURER\n\nActually, let me think more broadly. A _ _ E R:\n- ACHER\n- AIDER\n- AIMER\n- AMBER\n- ANGER\n- APER\n- ASHER\n- ATHER\n\nHmm, what about:\n- ACHER (a person who chafes?)\n- AIDER (one who aids) - D is excluded\n- AIMER (one who aims)\n- AMBER (a fossilized resin)\n- ANGER (emotion) - N is excluded\n- ASHER (one who ashes) - S is excluded\n- ATHER (a type of fish?) - T is excluded\n\nWait, I should also think about less common words. But for Wordle, it's usually common words.\n\nLet me reconsider. AMBER seems like a strong candidate. Let me check:\n- A: position 1 \u2713\n- M: position 2\n- B: position 3\n- E: position 4 \u2713\n- R: position 5 \u2713\n\nNone of M or B have been ruled out. This seems like a good guess.\n\nOther options:\n- AIMER: A, I, M, E, R\n- AMBER: A, M, B, E, R\n\nI'll go with AMBER as it's a very common word.\n\nActually, wait. Let me also consider:\n- ACHER\n- AIDER (D excluded)\n- AIMER\n- AMBER\n- ANGER (N excluded)\n- ASHER (S excluded)\n\nWhat about:\n- ACHER - not common\n- AIDER - D excluded\n- AIMER - possible\n- AMBER - possible\n- ANGER - N excluded\n- ASHER - S excluded\n- ATHER - T excluded\n\nI think AMBER is the best guess. It's a very common word and fits all constraints.\n\nLet me go with [amber].\n", + "call": { + "promptTokens": 335, + "completionTokens": 1788, + "reasoningTokens": null, + "durationMs": 45917, + "finishReason": "stop" + }, + "info": { + "guess": "amber", + "observation": "A M B E R\nG G G G G\n\nYou have 2 guesses left." + } + } + ] +} diff --git a/public/traces/wordle/base-on-s2.json b/public/traces/wordle/base-on-s2.json new file mode 100644 index 0000000..df94d2a --- /dev/null +++ b/public/traces/wordle/base-on-s2.json @@ -0,0 +1,158 @@ +{ + "runId": "base-on-s2", + "seed": 2, + "model": "brain-qwen38-dspark", + "capturedAt": "2026-08-28", + "rewards": { + "solved": 0.0, + "economy": 0.0, + "consistency": 0.3333333333333333 + }, + "metrics": { + "guesses_used": 3.0, + "rejected_replies": 6.0, + "reference_depth": 4.0, + "turns_granted": 6.0, + "inconsistent_guesses": 2.0 + }, + "truncated": false, + "outcome": "failed", + "answer": "spume", + "turns": [ + { + "reply": "\n\n[crane]", + "reasoning": "We need respond to user: \"Enter your guess to begin.\" We need provide only square-bracketed token? User says game reads first bracketed token and ignores everything else, do not put any other words in brackets. We need guess 5-letter word. Need choose initial guess likely common word with diverse letters. Need output only [crane] maybe. Need think? We can simply output [crane]. Ensure no other brackets. Final only [crane].\n", + "call": { + "promptTokens": 183, + "completionTokens": 103, + "reasoningTokens": null, + "durationMs": 3113, + "finishReason": "stop" + }, + "info": { + "guess": "crane", + "observation": "C R A N E\nX X X X G\n\nYou have 5 guesses left." + } + }, + { + "reply": "\n\n[those]", + "reasoning": "We need answer user in word-guessing game. Need guess only bracketed token. Need think through. Previous guess crane: C X, R X, A X, N X, E G. So secret 5-letter word ends with E, and does not contain C,R,A,N. Need choose next guess maximizing info. 5 guesses left. Need output [guess]. We need likely use common words with E at end, no C/R/A/N. Need avoid C R A N. Could guess maybe \"STOKE\"? has E end? S T O K E. None banned? C,R,A,N no. But K? unknown. Or \"BLOOM\"? no E end. Need likely choose a good word. Since E is correct position 5. Need find 4 unknown letters from letters excluding C,R,A,N. Common 5-letter words ending E: those letters positions 1-4. Need pick guess with high-frequency letters not banned and ending E. Avoid repeats? Could be \"THERE\" includes R banned, H,E,R,E (E twice) but R banned and two E? We know one E at pos5, could have another E but not necessary. \"THOSE\" includes T,H,O,S,E all not banned, good. \"MOUSE\" M,O,U,S,E. \"HOUSE\" H,O,U,S,E. \"GLOVE\" G,L,O,V,E. \"STONE\" S,T,O,N,E includes N banned. \"SLOPE\" S,L,O,P,E. \"LOOSE\" L,O,O,S,E. \"TOUSE\"? \"SHOES\" S,H,O,E,S. \"CHOSE\" C banned. \"CHOSE\" no. \"THOSE\" is good: T,H,O,S,E. It tests four common letters. But if any Y/G will help. Need only bracketed token. Could guess [those]. But need ensure 5-letter valid? likely yes.\n\nLet's think optimal. Known: _ _ _ _ E, no C R A N. We need guess. Good candidates: \"those\", \"mouse\", \"house\", \"shoes\", \"glove\", \"slope\", \"spoke\", \"stoke\", \"blote\"? Need common. \"those\" tests T,H,O,S. These are very common. If all X, eliminates T,H,O,S. But if one G/Y, helpful. Could there be repeated E? Maybe \"there\" has R banned. \"these\" T,H,E,S,E tests T,H,S and another E at pos3. But if E at pos3 maybe. \"these\" valid? yes. It tests T,H,S,E. But we know E pos5; extra E may be useful. However if E not in other positions, X. If secret has two E, \"these\" gives G/Y. But uses S,T,H. \"those\" uses O instead of E. Which is better? Frequency of O in 5-letter words high. Could choose \"those\". Need maybe avoid words with banned letters? yes.\n\nMaybe use anagram solver? Can't. Need likely answer [those]. Ensure no other bracket tokens. final only [those].\n", + "call": { + "promptTokens": 220, + "completionTokens": 664, + "reasoningTokens": null, + "durationMs": 45137, + "finishReason": "stop" + }, + "info": { + "guess": "those", + "observation": "T H O S E\nX X X Y G\n\nYou have 4 guesses left." + } + }, + { + "reply": "", + "reasoning": "Let's analyze the clues.\n\nSecret word is 5 letters.\n\nGuess 1: crane\nC X\nR X\nA X\nN X\nE G (E at position 5)\n\nSo E is in position 5. C, R, A, N not in word.\n\nGuess 2: those\nT X\nH X\nO X\nS Y\nE G\n\nSo S is in word but not position 4. E at position 5.\n\nNow we have 4 guesses left. Need word with E at position 5, S somewhere not position 4. No C, R, A, N, T, H, O.\n\nLet's think of common 5-letter words ending E with S not in 4th position.\n\nLetters available: B, D, F, G, I, J, K, L, M, P, Q, S, U, V, W, X, Y, Z plus E at 5.\n\nS is in positions 1,2,3 maybe.\n\nWords: _ _ _ _ E with S in 1-3.\n\nPossible: \"globe\" no S. \"spoke\" S P O K E but O excluded. \"swede\" S W E D E has E at 3 and 5, S pos1. But only one E? Actually word could have two E's. Swede: S W E D E. E at 3 and 5. Our clue only says E at 5 G. If there were E at 3, guess \"those\" had no E at 3? Wait those has O at 3, so no info. Crane had A at 3. So E at 3 possible. But \"swede\" maybe less common.\n\n\"slime\" S L I M E. S pos1, E pos5. No excluded letters. Good candidate.\n\n\"smile\" S M I L E. Good.\n\n\"stole\" has O excluded.\n\n\"straw\" no E end.\n\n\"sweat\" no.\n\n\"swipe\" S W I P E. Good.\n\n\"spine\" S P I N E but N excluded.\n\n\"spike\" S P I K E. Good.\n\n\"spoke\" O excluded.\n\n\"spect\" no.\n\n\"stele\" S T E L E but T excluded.\n\n\"stere\" T excluded.\n\n\"stray\" no.\n\n\"study\" no.\n\n\"sword\" no.\n\n\"swore\" O excluded.\n\n\"swine\" N excluded.\n\n\"swipe\" likely.\n\nNeed maximize information. Since we know E at 5, S in 1-3. Let's choose a guess with S at 1 maybe to test position, plus common letters. But we need not guess too many? 4 left.\n\nWe can guess [smile] or [swipe] or [slime]. Which gives most info? If we guess a word with S at 1, we learn if S is pos1. Also test L,M,I etc.\n\nMaybe [smile]: S M I L E. If S G, M Y/X, I Y/X, L Y/X, E G. Good.\n\nIf S Y, then S not pos1, likely pos2 or3.\n\nAlternative [spike]: S P I K E.\n\nCommon letters: I, L, M, P, K, W. Which is best? We want cover likely letters. \"smile\" includes S,M,I,L. \"swipe\" includes S,W,I,P. \"slime\" S,L,I,M. \"spoke\" no. \"stove\" T/O no. \"swede\" S,W,E,D,E.\n\nMaybe guess [swipe] because W/P/I and S pos1. But if S not pos1, still info.\n\nCould guess [lispe]? no.\n\nLet's consider word list: 5-letter ending E, contains S, no C R A N T H O. Common: smile, slime, swipe, spike, spoke(no), swine(no), spine(no), stave(no T), stoke(no T/O), stove(no T/O), style(no T), swine no, swede, swine, swore no, swage? (S W A G E A excluded), swype? no. \"swipe\" very plausible. \"smile\" plausible. \"spike\" plausible. \"slime\" plausible.\n\nWhich guess gives best partition? [smile] has S,M,I,L. If all X except E? Then S not in word? But we know S Y from those, so S must be in word. So at least S will be G or Y. If S Y, we know S not pos1. If M/I/L maybe.\n\n[swipe] S,W,I,P. If S Y, not pos1. W/P/I info.\n\nMaybe choose [swipe] because includes P/W, more distinctive. But smile has L/M. Common ending words? Let's think of possible secret from Wordle frequency. \"smile\" is common. \"swipe\" common. \"spike\" common. \"slime\" common.\n\nWe can also guess a non-word to test letters, but game likely expects valid? It says word-guessing, guess can be any? Better use valid word.\n\nNeed only output bracketed token.\n\nLet's select [swipe]. But maybe [smile] better? Let's evaluate entropy.\n\nKnown: positions: 5=E. S in {1,2,3}. Excluded: C,R,A,N,T,H,O.\nAllowed high freq: S,E,I,L,M,P,K,W,U,B,D,F,G,V,Y.\n\nCandidate words:\n- smile: S M I L E\n- swipe: S W I P E\n- spike: S P I K E\n- slime: S L I M E\n- swine: N no\n- spine: N no\n- spoke: O no\n- stave: T no\n- stoke: T/O no\n- stove: T/O no\n- style: T no\n- swede: S W E D E\n- swage: A no\n- swype? no\n- swish? no E end? no\n- swish not E.\n- swive? no\n- swine no\n- swore no\n- swart no\n- swage no\n- swelt? T no\n- swish no\n- swede less common.\n- swype no.\n- swine no.\n- swine no.\n- swine.\n\nAlso words with S not first: \"besie\"? \"beset\" no E end? \"besie\" not. \"bisque\" no E? \"besom\" O no. \"besot\" O/T no. \"besee\"? no. \"besee\"?\n\"besie\"? \"besie\" maybe not.\n\"besee\" no.\n\"beset\" no.\n\"besom\" no.\n\"besot\" no.\n\"besee\" no.\n\"besee\" no.\n\"besie\" maybe? 5 letters B E S I E? not common.\n\"besee\" no.\n\"besie\" not.\n\"beset\" no.\n\"besom\" O.\n\"besot\" T/O.\n\"besee\" no.\n\"besee\" no.\n\"besie\" maybe B E S I E, S pos3, E pos2/5. But E at pos2 would have been? No previous guesses at pos2 were R and H, both X. So E at pos2 possible. But if secret had E at pos2, our guess? We don't know. Word \"besie\" not common.\n\n\"besee\" no.\n\"besie\" no.\n\"beset\" no.\n\"besom\" no.\n\"besot\" no.\n\"besee\" no.\n\"besie\" no.\n\n\"besee\" no.\n\nWords with S pos2: \"besee\"? \"besie\"? \"beset\" no, \"besom\" no, \"besot\" no, \"besee\" no. \"besie\" maybe. \"besee\" no.\n\"besee\" no.\n\"besie\" no.\n\"beset\" no.\n\"besom\" no.\n\"besot\" no.\n\"besee\" no.\n\"besie\" no.\n\"besee\" no.\n\"besie\" no.\n\"beset\" no.\n\"besom\" no.\n\"besot\" no.\n\"besee\" no.\n\"besie\" no.\n\"besee\" no.\n\nMaybe \"besie\" is not in dictionary. \"besee\" no.\n\"besie\" no.\n\"beset\" no.\n\"besom\" no.\n\"besot\" no.\n\"besee\" no.\n\"besie\" no.\n\"besee\" no.\n\nOther S pos2: \"besee\" no, \"besie\" no, \"beset\" no, \"besom\" no, \"besot\" no, \"besee\" no. \"besie\" no.\n\"besee\" no.\n\"besie\" no.\n\"beset\" no.\n\"besom\" no.\n\"besot\" no.\n\"besee\" no.\n\"besie", + "call": { + "promptTokens": 256, + "completionTokens": 2048, + "reasoningTokens": null, + "durationMs": 66621, + "finishReason": "length" + }, + "info": { + "guess": null, + "observation": "That guess was not accepted: no bracketed guess found.\nIt did not use up a turn. You have 4 guesses left." + } + }, + { + "reply": "", + "reasoning": "Let me analyze the clues so far:\n\n1. [crane] \u2192 X X X X G\n - C: not in word\n - R: not in word\n - A: not in word\n - N: not in word\n - E: correct letter, correct position (position 5)\n\n2. [those] \u2192 X X X Y G\n - T: not in word\n - H: not in word\n - O: not in word\n - S: in word, but not position 4\n - E: correct letter, correct position (position 5)\n\nSo I know:\n- Position 5 is E\n- S is in the word but NOT in position 4\n- C, R, A, N, T, H, O are NOT in the word\n\nLetters available: B, D, F, G, I, J, K, L, M, P, Q, S, U, V, W, X, Y, Z\n\nS is in the word but not position 4. So S is in position 1, 2, 3, or 5. But position 5 is E. So S is in position 1, 2, or 3.\n\nLet me think of common 5-letter words ending in E with S in positions 1-3:\n- S: position 1, 2, or 3\n- Position 5: E\n- No: C, R, A, N, T, H, O\n\nWords:\n- BUSHY? No, doesn't end in E\n- BUSED? B-U-S-E-D? No, ends in D\n- BUSKE? Not a word\n- BUSED? No\n- SUSED? Not a word\n- SUSED? No\n- SUITE? S-U-I-T-E - T is excluded\n- SUSED? No\n- SUSED? No\n- SUSED? No\n\nLet me think more carefully:\n- Position 5: E\n- S in position 1, 2, or 3\n- No C, R, A, N, T, H, O\n\nPosition 1: S\n- S____E\n- SUSED? No\n- SUSED? No\n- SUITE? T excluded\n- SUSED? No\n- SUSED? No\n- SUSED? No\n- SUSED? No\n- SUSED? No\n- SUSED? No\n- SUSED? No\n- SUSED? No\n- SUSED? No\n- SUSED? No\n\nWait, let me think of actual words:\n- SUSED? No\n- SUSED? No\n- SUSED? No\n- SUSED? No\n- SUSED? No\n- SUSED? No\n\nLet me think of 5-letter words ending in E with S in position 1:\n- SUSED? No\n- SUSED? No\n- SUSED? No\n- SUSED? No\n- SUSED? No\n- SUSED? No\n\nHmm, let me think differently. What words end in E and contain S but not C, R, A, N, T, H, O?\n\nAvailable letters: B, D, F, G, I, J, K, L, M, P, Q, S, U, V, W, X, Y, Z\n\n- BUSED? No, ends in D\n- BUSED? No\n- SUSED? No\n- SUSED? No\n- SUSED? No\n- SUSED? No\n- SUSED? No\n\nWait:\n- BUSED? No\n- BUSED? No\n- BUSED? No\n\nLet me think of actual 5-letter words:\n- BUSED? No\n- BUSED? No\n- BUSED? No\n\nHmm, let me think of words ending in E:\n- BUSED? No\n- BUSED? No\n- BUSED? No\n\nOK let me be more systematic. 5-letter words ending in E:\n- BUSED? No\n- BUSED? No\n- BUSED? No\n\nWords ending in E with S:\n- BUSED? No\n- BUSED? No\n- BUSED? No\n\nLet me think...\n- SUSED? No\n- SUSED? No\n- SUSED? No\n\nOK, 5-letter words ending in E:\n- BUSED? No\n- BUSED? No\n- BUSED? No\n\nLet me think of common words:\n- BUSED? No\n- BUSED? No\n- BUSED? No\n\nHmm, let me think of words:\n- BUSED? No\n- BUSED? No\n- BUSED? No\n\nOK let me think of 5-letter words ending in E:\n- BUSED? No\n- BUSED? No\n- BUSED? No\n\nLet me just list 5-letter words ending in E:\n- ABUSE (A excluded)\n- ADIEU (no E at end)\n- AGREE (R excluded)\n- AMEND (no)\n- ANGLE (A, N excluded)\n- ANIME (A, N excluded)\n- ANGLE (A, N excluded)\n- ANTHEM (6 letters)\n- ANVIL (no)\n- APOGE (5 letters? No, APOGEE is 6)\n- APPLE (A excluded)\n- APRON (A, R, O, N excluded)\n- ARISE (A, R, I, S, E) - A, R excluded\n- ARSEN (A, R excluded)\n- ASIDE (A excluded)\n- ASKED (A excluded)\n- ATONE (A, T, O, N excluded)\n- AWAKE (A excluded)\n- BEGOT (no E at end)\n- BEIGE (B, E, I, G, E) - no S\n- BILED (no E at end)\n- BILED? No\n- BISON (no E at end)\n- BUSED (no E at end)\n- BUSKE? No\n- BUTTE (B, U, T, T, E) - T excluded\n- BUTLE? No\n- BUSHY (no E at end)\n- CAUSE (C, A excluded)\n- CHASE (C, H, A excluded)\n- CITED (C excluded)\n- COBRA (C, O, R, A excluded)\n- COMED (C, O excluded)\n- CORAL (C, O, R, A excluded)\n- COURT (C, O, R, T excluded)\n- CREDI (C, R excluded)\n- CRANE (C, R, A, N excluded)\n- CRATE (C, R, A, T excluded)\n- CRAFT (C, R, A, F, T) - C, R, A, T excluded\n- CRAWL (C, R, A excluded)\n- CREAK (C, R, A excluded)\n- CREED (C, R excluded)\n- CROPY (C, R, O excluded)\n- CROWD (C, R, O excluded)\n- CROWN (C, R, O, N excluded)\n- CRYPT (C, R, T excluded)\n- CUBED (no E at end)\n- CULDE? No\n- CURED (C, R excluded)\n- CURIO (C, R, O excluded)\n- CUSP? No\n- CUSPY? No\n- CUSSED? No (6 letters)\n- DABSY? No\n- DAINT (no E at end)\n- DANCE (D, A, N, C, E) - A, N, C excluded\n- DARED (D, A, R, E, D) - A, R excluded\n- DATED (no E at end)\n- DAUNT (no E at end)\n- DAWED (D, A, W, E, D) - A excluded\n- DAWES (D, A, W, E, S) - A excluded\n- DEALT (no E at end)\n- DEBTS (no E at end)\n- DECAL (no E at end)\n- DECAY (no E at end)\n- DECOR (no E at end)\n- DEBIT (no E at end)\n- DECOR (no E at end)\n- DEIFY (no E at end)\n- DEIGN (no E at end)\n- DEITY (no E at end)\n- DELVE (D, E, L, V, E) - no S\n- DENSE (D, E, N, S, E) - N excluded\n- DEPOT (no E at end)\n- DEPOS (no E at end)\n- DERBY (no E at end)\n- DESKS (no E at end)\n- DESKS? No\n- DEUCE (D, E, U, C, E) - C excluded\n- DEWED (no E at end)\n- DIBES (no E at end)\n- DIBES? No\n- DIBES? No\n- DIBES? No\n- DIBES? No\n- DIBES? No\n- DIBES? No\n- DIBES? No\n- DIB", + "call": { + "promptTokens": 298, + "completionTokens": 2048, + "reasoningTokens": null, + "durationMs": 62748, + "finishReason": "length" + }, + "info": { + "guess": null, + "observation": "That guess was not accepted: no bracketed guess found.\nIt did not use up a turn. You have 4 guesses left." + } + }, + { + "reply": "", + "reasoning": "Let me analyze the clues so far:\n\n1. CRANE \u2192 X X X X G\n - C, R, A, N are not in the word\n - E is in the word, position 5\n\n2. THOSE \u2192 X X X Y G\n - T, H, O are not in the word\n - S is in the word, but NOT in position 4\n - E is in the word, position 5\n\nSo I know:\n- E is in position 5\n- S is in the word but NOT in position 4\n- C, R, A, N, T, H, O are NOT in the word\n\nLetters remaining to consider: B, D, F, G, I, J, K, L, M, P, Q, S, U, V, W, X, Y, Z\n\nThe word is: _ _ _ _ E\nS is in positions 1, 2, or 3 (not 4, not 5 since E is there)\n\nLet me think of common 5-letter words ending in E with S in position 1, 2, or 3:\n\n- S in position 1: S____E \u2192 \"sneak\" (N is out), \"stare\" (T is out), \"stone\" (T, O are out), \"swipe\" (W, I, P), \"smile\" (M, I, L), \"snake\" (N, K), \"swole\" (W, O), \"sneer\" (N, R), \"sweat\" (W, T), \"sweep\" (W, P), \"singe\" (N, G), \"slime\" (L, I, M), \"spoke\" (P, O, K), \"spoke\" no... \"spine\" (P, I, N), \"spoke\" no... \"spree\" (P, R), \"sprig\" no E at end... \"spine\" N is out... \"spoke\" O is out... \"swipe\" (W, I, P) - possible! \"swipe\" = S W I P E\n- S in position 2: _S___E \u2192 \"gushes\" no... \"blush\" no E... \"bushel\" 6 letters... \"fushi\" no... \"lushes\" 6... \"sushi\" no... \"shush\" no... \"ushes\" no... \"gushy\" no... \"blush\" no... \"fuses\" 5 letters but F, U, S, E, S - two S's... \"hushes\" 6... \"mushy\" no... \"pushy\" no... \"sushi\" no... \"ushes\" no... \"gushes\" 6... \"bushy\" no... \"fussy\" no... \"lushy\" no... \"mussy\" no... \"pushy\" no... \"shush\" no... \"sushi\" no... \"ushes\" no... \"bushel\" 6... \"fush\" 4... \"gush\" 4... \"hush\" 4... \"lush\" 4... \"mush\" 4... \"push\" 4... \"rush\" 4... \"shush\" 5 but no E... \"sushi\" 5 but no E... \"ushes\" 5 but U, S, H, E, S - H is out... \n\nHmm, S in position 2 is tricky. Let me think...\n- \"blush\" - no E\n- \"bushy\" - no E \n- \"fuses\" - F, U, S, E, S - two S's, possible?\n- \"hushes\" - 6 letters\n- \"lushy\" - no E\n- \"mushy\" - no E\n- \"pushy\" - no E\n- \"rushy\" - no E\n- \"sushi\" - no E\n- \"ushes\" - U, S, H, E, S - H is out\n\nWait, what about \"gushes\"? 6 letters, no.\n\nWhat about \"blush\"? No E.\n\nHmm, S in position 2 ending in E... \n- \"bushel\" - 6 letters\n- \"fushi\" - not a word\n- \"gushy\" - no E\n- \"hushy\" - not a word\n- \"lushy\" - no E\n- \"mushy\" - no E\n- \"pushy\" - no E\n- \"rushy\" - no E\n- \"shush\" - no E\n- \"sushi\" - no E\n\nThis is hard. Maybe S is in position 1 or 3.\n\nS in position 3: __S__E\n- \"bushy\" no... \n- \"fuses\" - F, U, S, E, S - S in position 3, E in position 4, S in position 5... wait that's F-U-S-E-S, S is in position 3, E in position 4, S in position 5. But E should be in position 5. So \"fuses\" doesn't work.\n- \"gushy\" no...\n- \"hushy\" no...\n- \"lushy\" no...\n- \"mushy\" no...\n- \"pushy\" no...\n- \"rushy\" no...\n- \"sushi\" no...\n- \"ushes\" no...\n\nWait, I need to reconsider. The word ends in E (position 5). S is in position 1, 2, or 3.\n\nPosition 1: S _ _ _ E\n- \"swipe\" - S, W, I, P, E \u2713 (none of W, I, P are excluded)\n- \"smile\" - S, M, I, L, E \u2713\n- \"spoke\" - O is out \u2717\n- \"sneak\" - N is out \u2717\n- \"stare\" - T, R are out \u2717\n- \"stone\" - T, O are out \u2717\n- \"swore\" - O, R are out \u2717\n- \"swipe\" \u2713\n- \"smile\" \u2713\n- \"spine\" - N is out \u2717\n- \"spree\" - R is out \u2717\n- \"sprig\" - no E at end, and R is out\n- \"sweep\" - S, W, E, E, P - two E's, and E should only be in position 5... wait, the feedback said E is G at position 5. If there were two E's, one would be G and the other Y. But we only got one E feedback. So probably only one E.\n- \"swipe\" \u2713\n- \"swine\" - N is out \u2717\n- \"swipe\" \u2713\n- \"swore\" \u2717\n- \"swung\" - no E\n- \"swoon\" - no E\n- \"swipe\" seems good\n- \"swipe\" - S, W, I, P, E\n- \"smile\" - S, M, I, L, E\n- \"smile\" \u2713\n- \"sneer\" - N, R out \u2717\n- \"sneak\" - N out \u2717\n- \"snipe\" - N out \u2717\n- \"snipe\" \u2717\n- \"snore\" - N, R, O out \u2717\n- \"snore\" \u2717\n- \"snoop\" - N, O out \u2717\n- \"snoop\" \u2717\n- \"snore\" \u2717\n- \"snipe\" \u2717\n- \"snore\" \u2717\n- \"sneak\" \u2717\n- \"snore\" \u2717\n- \"snipe\" \u2717\n- \"snore\" \u2717\n- \"snore\" \u2717\n- \"snore\" \u2717\n\nOK let me focus on S in position 1:\n- \"swipe\" - S, W, I, P, E - all letters available \u2713\n- \"smile\" - S, M, I, L, E - all letters available \u2713\n- \"swipe\" \u2713\n- \"smile\" \u2713\n- \"swipe\" \u2713\n- \"smile\" \u2713\n- \"swipe\" \u2713\n- \"smile\" \u2713\n- \"swipe\" \u2713\n- \"smile\" \u2713\n\nLet me think of other S___E words:\n- \"swipe\" \u2713\n- \"smile\" \u2713\n- \"swipe\" \u2713\n- \"smile\" \u2713\n- \"swipe\" \u2713\n- \"smile\" \u2713\n\nWhat about:\n- \"swipe\" - S W I P E\n- \"smile\" - S M I L E\n- \"swipe\" - S W I P E\n- \"smile\" - S M I L E\n\nBoth are common words. Let me think of others:\n- \"swipe\" \u2713\n- \"smile\" \u2713\n- \"swipe\" \u2713\n- \"smile\" \u2713\n- \"swipe\" \u2713\n- \"smile\" \u2713\n\nOther options:\n- \"swipe\" \u2713\n- \"smile\" \u2713\n- \"swipe\" \u2713\n", + "call": { + "promptTokens": 340, + "completionTokens": 2048, + "reasoningTokens": null, + "durationMs": 54518, + "finishReason": "length" + }, + "info": { + "guess": null, + "observation": "That guess was not accepted: no bracketed guess found.\nIt did not use up a turn. You have 4 guesses left." + } + }, + { + "reply": "", + "reasoning": "Need answer user's repeated prompt. Need provide bracketed guess. Let's reason carefully.\n\nGame state:\nSecret 5-letter word.\nGuess1 crane -> C R A N E: X X X X G. So E at pos5 correct. C,R,A,N not in word.\nGuess2 those -> T H O S E: X X X Y G. So E pos5 correct; T,H,O not in word; S is in word but not pos4.\nWe need 5-letter word ending E, containing S not at pos4, no C R A N T H O.\nNeed guess. Need likely common word. We need maximize info.\n\nKnown: positions: _ _ _ _ E. S in positions 1,2,3? not 4. No C,R,A,N,T,H,O.\nLetters allowed: B,D,F,G,I,J,K,L,M,P,Q,U,V,W,X,Y,Z plus S,E.\nNeed likely word. Common 5-letter ending E with S not 4, no banned. Possibilities:\n- BUSES? B U S E S: S at 3 and 5? But E at 5, S not pos4. Contains S. B,U,S,E. Valid? Buses = 5 letters, ending e? B U S E S ends S not E. no.\n- BESES? no.\n- BLUSE? bluse? B L U S E: S pos4, not allowed (those feedback S Y at pos4 means S not in pos4; if guess bluse S pos4 would be wrong). So no.\n- BUSES no.\n- BUSES? no.\n- BULGE? B U L G E, no S. But we know S in word, so must include S.\n- BUSED? B U S E D: S pos3, E pos5, D pos? valid, no banned. But BUSED is word.\n- BUSES no.\n- BUSH? 4.\n- BUSKE? no.\n- BUTES? T banned.\n- BOWSE? O banned.\n- BOWES? O.\n- BOWSE no.\n- BOWES no.\n- BOWSE? no.\n- BOWSE no.\n- BOWES no.\n- BOWSE.\n- BOWES.\n- BOWSE.\n- BOWES.\n\nNeed likely guess. Since S can be 1,2,3. Need choose a guess that tests positions and other letters.\n\nPossible words:\nS at 1: S? ? ? E, no banned. Common: SEDGE? S E D G E? But E pos5 and E pos2; if word SEDGE, feedback for crane? E pos5 G; for those S pos1? Those has S pos4, would be Y. Valid. SEDGE uses S,E,D,G,E. No banned. But if secret has E only pos5? Could have multiple E? Wordle usually counts duplicates. We don't know. SEDGE possible. S? ? ? E with S pos1: SEDGE, SEQUE? no. SKEVE? S K E V E? maybe. SLOPE? O banned. S? ? ? E.\nS at 2: ? S ? ? E. Common: BUSED? B U S E D (S pos3 not 2). B?S?E: B?S?E: B?S?E. \"BUSES\" ends S. \"BESNE\"? \"BESOM\" O. \"BESOT\" O,T. \"BESIT\" T. \"BESME\"? \"BESNE\"? \"BESOM\". \"BESOT\". \"BESIT\". \"BESME\" maybe. \"BESNE\" not common. \"BESOM\" no. \"BESOT\" no. \"BESIT\" no. \"BESME\" no.\n\"BLUSE\" S pos4 no. \"BLUES\" ends S. \"BUSES\" ends S. \"BUSKE\" no. \"BUSES\".\n\"BUSKE\" maybe? no.\n\"BUSHE\" H banned. \"BUSHY\" no E.\n\"BUSKY\" no E.\n\"BUSSE\"? B U S S E: S pos3 and4? If secret has S pos3 only, guess busse S pos4 would be X? But S in word, one S at pos3. Wordle with duplicate S: if secret B U S E D and guess B U S S E: letters: B G, U G, S G, S? secret has one S, third S X, E G. Not a valid guess? It's a word maybe. But if we guess with S pos4, it will be Y? Actually if secret has S at pos3, guess S at pos4: one S in word not pos4, so Y. We know S not pos4. So avoid S pos4? We can guess words with S pos4 to learn? It would give Y if S in word elsewhere; already know. Better not waste.\n? S ? ? E: \"S??\" no.\n\"BESNE\"? \"BESOT\" T/O. \"BESOM\" O/M. \"BESIT\" T. \"BESME\" maybe archaic. \"BESNE\" no.\n\"BLUSE\" S pos4.\n\"BLUES\" ends S.\n\"BUSES\" ends S.\n\"BUSHE\" H.\n\"BUSKE\" maybe.\n\"BUSSE\" S pos3/4.\n\"BUSY?\" no E.\n\"BUSKE\" no.\n\"BUSKY\" no.\n\"BUSSE\" maybe.\n\"BUSHE\" no.\n\"BUSKY\".\n\"BUSKY\" no.\n\nS at 3: ? ? S ? E. Common: BUSED, BUSES? B U S E D has S pos3, E pos5, D pos4. BUSED valid. \"BUSES\" no. \"BUSKE\" B U S K E? not common. \"BUSSE\" B U S S E. \"BUSHE\" H. \"BOWSE\" O. \"BOWES\" O. \"BOWSE\" O. \"BOWES\" O. \"BOWSE\".\n\"BOWSE\" O.\n\"BOWES\" O.\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".\n\"BOWSE\".\n\"BOWES\".", + "call": { + "promptTokens": 382, + "completionTokens": 1921, + "reasoningTokens": null, + "durationMs": 68863, + "finishReason": "stop" + }, + "info": { + "guess": null, + "observation": "That guess was not accepted: no bracketed guess found.\nIt did not use up a turn. You have 4 guesses left." + } + }, + { + "reply": "\n\n[swipe]", + "reasoning": "We need respond to user. Need provide guess in brackets only? They said game reads first bracketed token and ignores everything else, do not put other words in brackets. We need continue Wordle.\nState:\nSecret 5 letters.\nGuess1 crane: C R A N E -> X X X X G. So E at pos5. C,R,A,N not in word.\nGuess2 those: T H O S E -> X X X Y G. T,H,O not in word; S in word but not pos4; E pos5.\nKnown: word pattern _ _ _ _ E. Contains S not in pos4. Excluded: C,R,A,N,T,H,O.\nLetters available: B,D,F,G,I,J,K,L,M,P,Q,U,V,W,X,Y,Z,S. Need 5-letter word ending E, contains S not 4th.\nNeed guess. We had failed to output due maybe weird? Need output valid bracket. Need choose informative word using available letters, likely with S and E. Need avoid excluded letters. 5 letters ending E. Contains S somewhere not 4.\nPossible words: BUSED? B U S E D: S pos3, E pos4? Wait ending E? BUSED is B U S E D, no. BUSES ends S not E. BUSH? no.\nNeed ending E. Common 5-letter words ending E with S not 4: BUSED? no ending D. BUSES no. GUST? no.\nLet's list: _ _ _ _ E, S not pos4. S can pos1,2,3.\nCommon: S? ? ? E: S? ? ? E. \"swede\" (S W E D E) but E pos3 and 5, D allowed, W allowed. Contains S pos1. But we know E pos5 only? E can appear multiple? Wordle feedback for E in crane G at pos5. If another E present? Usually if guess has one E and it's G, doesn't tell other Es. Could be. \"swede\" uses two E. But if secret has one E, swede would have E pos3 maybe Y? But we don't know.\n\"swipe\" S W I P E: S pos1, E pos5, all allowed. Good common.\n\"swine\" S W I N E has N excluded. \"swipe\" excellent: tests S,W,I,P. Contains S not pos4.\n\"spine\" S P I N E N excluded. \"spree\" S P R E E R excluded. \"sprue\" S P R U E R excluded. \"spade\" A excluded. \"suede\" S U E D E: S pos1, E pos3/5, D,U. Good.\n\"slime\" S L I M E: good. \"slate\" A excluded. \"sloth\" O,H excluded. \"stole\" T,O excluded. \"stone\" T,O,N excluded. \"stoke\" T,O excluded. \"stove\" T,O,V. \"stare\" A,R excluded. \"stale\" A,T. \"stele\" S T E L E T excluded. \"stere\" T,R. \"straw\" no E.\n\"swipe\" likely good.\nBut maybe we need maximize. Available letters: B D F G I J K L M P Q U V W X Y Z S. Need likely word ending E. We can guess [swipe]. It tests S W I P. If S feedback? We know S is in word not pos4. In swipe S pos1. If G, S pos1; if Y, S pos2/3. W/I/P info.\nCould choose [suede] tests S,U,E,D. But E pos3 may be Y/G, D,U. [swipe] tests more distinct new letters S,W,I,P. Since S already known, one new letter less? Actually swipe has S + W I P = 3 new. Suede has S + U D + E = 2 new (E known pos5, but extra E). Slime S L I M E = 3 new. Which better? We need find common 5-letter. \"swipe\" is common.\nNeed only bracketed token. Final [swipe]. Ensure no other brackets.\n", + "call": { + "promptTokens": 424, + "completionTokens": 931, + "reasoningTokens": null, + "durationMs": 46277, + "finishReason": "stop" + }, + "info": { + "guess": "swipe", + "observation": "S W I P E\nG X X Y G\n\nYou have 3 guesses left." + } + }, + { + "reply": "", + "reasoning": "Let's analyze the clues so far.\n\nGuess 1: crane \u2192 X X X X G\nSo:\n- c not in word\n- r not in word\n- a not in word\n- n not in word\n- e is correct position 5\n\nGuess 2: those \u2192 X X X Y G\n- t not in word\n- h not in word\n- o not in word\n- s is in word but not position 4\n- e correct position 5\n\nSo far:\n- e at pos 5\n- s in word, not pos 4\n- excluded: c,r,a,n,t,h,o\n\nGuess 3: swipe \u2192 G X X Y G\n- s at pos 1 correct\n- w not in word\n- i not in word\n- p in word, not pos 4\n- e at pos 5 correct\n\nSo:\n- s at pos 1\n- e at pos 5\n- p in word, not pos 4\n- excluded: c,r,a,n,t,h,o,w,i\n\nLetters available: b,d,e,f,g,j,k,l,m,p,q,s,u,v,x,y,z\nNeed pos 1=s, pos5=e.\np is in pos 2 or 3.\ns not pos4 already satisfied.\n\nWord pattern: s _ _ _ e, with p at pos2 or pos3.\nCommon words: s p e c e? no c excluded. \ns p i n e? i,n excluded.\ns p o n e? o,n excluded.\ns p a r e? a,r excluded.\ns p e c k? c,k? c excluded.\ns p e l l? p at pos3? s p e l l -> p pos2, e pos3, but e must be pos5. no.\ns p e l m? no.\ns p e c k no.\ns p e c h no.\ns p e c k? no.\n\np at pos3: s _ p _ e\n- s u p e r? r excluded.\n- s o p e l? o excluded.\n- s i p e? no.\n- s c a p e? c,a excluded.\n- s u p p l e? 6 letters.\n- s h o p e? h,o excluded.\n- s h a p e? h,a excluded.\n- s t u p e? t excluded.\n- s m a p e? no.\n- s m o g s? no.\n- s m u g s? no.\n- s m i t h? no.\n- s m i l e? i excluded.\n- s m o g s? no.\n- s m o o t h? no.\n- s m o g s? no.\n- s m o g? no.\n- s m o l t? t excluded.\n- s m o l t? no.\n- s m o l t? no.\n- s m o l t? no.\n- s m o l t? no.\n- s m o l t? no.\n\nLet's think of 5-letter words s _ _ _ e with p at pos2 or pos3.\n\ns p _ _ e:\n- s p e c k? c excluded.\n- s p e c k? no.\n- s p e c k? no.\n- s p e c k? no.\n- s p e c k? no.\n- s p e c k? no.\n- s p e c k? no.\n- s p e c k? no.\n- s p e c k? no.\n- s p e c k? no.\n- s p e c k? no.\n- s p e c k? no.\n- s p e c k? no.\n- s p e c k? no.\n- s p e c k? no.\n- s p e c k? no.\n- s p e c k? no.\n\nMaybe s p e c k not.\n\ns p e c k no.\n\ns p e c k no.\n\nLet's list common 5 letter words ending e:\nable, acid, aged, also, area, army, away, baby, back, ball, band, bank, bare, base, bath, bear, beat, been, beer, bell, belt, best, bike, bill, bird, blow, blue, boat, bone, book, born, boss, both, bowl, bulk, burn, bush, busy, cafe, cake, calm, call, came, camp, card, care, case, cash, cast, cave, cell, cent, chair, chalk, champ, chart, chase, cheap, check, chef, chief, child, chill, chock, chord, chose, chunk, civil, claim, class, clean, clear, click, cliff, climb, clock, close, cloth, coach, coast, count, court, cover, craft, crane, crash, cream, crew, crime, cross, crowd, crown, crude, curve, dance, dark, data, date, deal, debt, deck, deep, deer, desk, dial, diet, dirt, dish, dive, door, dose, down, drag, draw, dress, drip, drop, drug, drum, dual, duck, duel, dug? \nNeed s _ _ _ e.\n\ns _ _ _ e words:\n- sable\n- scald? no e\n- scamp? no\n- scare? a,r excluded\n- scarf? no\n- scale? c,a excluded\n- scalp? c,a excluded\n- scald? no\n- scamp? no\n- scant? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n- scare? no\n-", + "call": { + "promptTokens": 461, + "completionTokens": 1758, + "reasoningTokens": null, + "durationMs": 53790, + "finishReason": "stop" + }, + "info": { + "guess": null, + "observation": "That guess was not accepted: no bracketed guess found.\nIt did not use up a turn. You have 3 guesses left." + } + }, + { + "reply": "", + "reasoning": "Let me analyze the clues so far:\n\n1. crane \u2192 X X X X G (E is correct position at pos 5, C/R/A/N are not in word)\n2. those \u2192 X X X Y G (E is correct position at pos 5, S is in word but wrong position, T/H/O are not in word)\n3. swipe \u2192 G X X Y G (S is correct position at pos 1, P is in word but wrong position, E is correct position at pos 5, W/I are not in word)\n\nSo the word is: S _ _ _ E\n- P is in the word but not at position 4\n- Letters eliminated: C, R, A, N, T, H, O, W, I\n\nRemaining letters to work with: B, D, F, G, J, K, L, M, P, Q, S, U, V, X, Y, Z\n\nThe word is S _ _ _ E, and P is somewhere in positions 2, 3, or 5... wait, position 5 is E. So P is in position 2 or 3.\n\nLet me think of 5-letter words: S _ _ _ E with P in position 2 or 3.\n\nS P _ _ E: SPOKE? SPORE? SPREE? SPICE? SPINE? SPATE?\nWait, I and T are eliminated. SPICE - C is eliminated. SPINE - I is eliminated. SPATE - T is eliminated. SPOKE - O is eliminated. SPREE - R is eliminated.\n\nS _ P _ E: SPOKE (O eliminated), SPORE (O, R eliminated), SPARE (A, R eliminated), SPINE (I eliminated), SPICE (I, C eliminated)\n\nHmm, what about S _ _ P E? Wait, P can't be at position 5 since E is there. P is at position 2 or 3.\n\nWait, let me re-check. The word is S _ _ _ E. P is in the word but not at position 4 (where it was in swipe). So P is at position 2 or 3.\n\nS P _ _ E:\n- SPOKE - O eliminated\n- SPORE - O, R eliminated\n- SPICE - I, C eliminated\n- SPINE - I eliminated\n- SPATE - T eliminated\n- SPREE - R eliminated\n- SPARE - A, R eliminated\n- SPREE - R eliminated\n- SPARE - A, R eliminated\n- SPICE - I, C eliminated\n- SPOKE - O eliminated\n- SPATE - T eliminated\n- SPINE - I eliminated\n- SPARE - A, R eliminated\n- SPREE - R eliminated\n- SPORE - O, R eliminated\n- SPICE - I, C eliminated\n- SPINE - I eliminated\n- SPATE - T eliminated\n- SPARE - A, R eliminated\n- SPREE - R eliminated\n- SPOKE - O eliminated\n- SPICE - I, C eliminated\n- SPINE - I eliminated\n- SPATE - T eliminated\n- SPARE - A, R eliminated\n- SPREE - R eliminated\n- SPORE - O, R eliminated\n- SPICE - I, C eliminated\n- SPINE - I eliminated\n- SPATE - T eliminated\n- SPARE - A, R eliminated\n- SPREE - R eliminated\n- SPOKE - O eliminated\n- SPICE - I, C eliminated\n- SPINE - I eliminated\n- SPATE - T eliminated\n- SPARE - A, R eliminated\n- SPREE - R eliminated\n- SPORE - O, R eliminated\n- SPICE - I, C eliminated\n- SPINE - I eliminated\n- SPATE - T eliminated\n- SPARE - A, R eliminated\n- SPREE - R eliminated\n- SPOKE", + "call": { + "promptTokens": 503, + "completionTokens": 844, + "reasoningTokens": null, + "durationMs": 23187, + "finishReason": "stop" + }, + "info": { + "guess": null, + "observation": "That guess was not accepted: no bracketed guess found.\nIt did not use up a turn. You have 3 guesses left." + } + } + ] +} diff --git a/scripts/check-demos.mjs b/scripts/check-demos.mjs index 03e13d9..a961a92 100644 --- a/scripts/check-demos.mjs +++ b/scripts/check-demos.mjs @@ -329,7 +329,7 @@ for (const slug of slugs) { 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))); + const rendered = walk(abs('src'), (f) => /\.tsx?$/.test(f)).some((f) => /solveRate|solve_rate/.test(read(f))); report.check( typeof rate === 'number' && rendered, rel(abs('public', 'traces', 'manifest.json')), diff --git a/scripts/check-receipts.mjs b/scripts/check-receipts.mjs index ac63eec..07ffe8d 100644 --- a/scripts/check-receipts.mjs +++ b/scripts/check-receipts.mjs @@ -187,8 +187,8 @@ for (const slug of slugs) { 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.', + `marker "${marker}" delimits an empty region between lines ${open + 1} and ${close + 1}. The markers are ` + + 'exclusive, so a pair on adjacent lines quotes nothing.', ); } } diff --git a/scripts/new-demo.mjs b/scripts/new-demo.mjs index 26b7f28..8183c47 100644 --- a/scripts/new-demo.mjs +++ b/scripts/new-demo.mjs @@ -96,6 +96,12 @@ for (const job of jobs) { 'Refusing to overwrite. Pick another slug, or delete the directory yourself if you meant to start over.', ); } + // An empty template directory would copy cleanly, report "Created 0 files" + // and leave behind a demo that the registry quarantines for a reason nobody + // connects back to this command. + if (countFiles(job.from) === 0) { + fail(`${rel(job.from)} is empty.`, 'There is nothing to scaffold from. Fill in the template first.'); + } } /* -------------------------------------------------------------------- copy */ @@ -132,11 +138,21 @@ console.log(dim(`The demo will 404 until meta.ts, demo.tsx and public/og/${slug} /* ----------------------------------------------------------------- helpers */ +/** Files in a tree, recursively. Used only to reject an empty template. */ +function countFiles(dir) { + return fs.readdirSync(dir, { withFileTypes: true }).reduce((total, entry) => { + if (entry.name === '__pycache__' || entry.name === 'node_modules') return total; + if (entry.isDirectory()) return total + countFiles(path.join(dir, entry.name)); + return total + (entry.isFile() ? 1 : 0); + }, 0); +} + function capitalise(word) { return word.charAt(0).toUpperCase() + word.slice(1); } -/** Applies the substitution table, longest token first so prefixes cannot win. */ +/** Applies the substitution table. No token is a prefix of another, so order + * does not matter here — but keep it that way if you add one. */ function substitute(text, bareName) { let out = text; for (const [token, value] of substitutions) out = out.split(token).join(value); diff --git a/scripts/sitemap.mjs b/scripts/sitemap.mjs index 2b6b078..84af03e 100644 --- a/scripts/sitemap.mjs +++ b/scripts/sitemap.mjs @@ -34,12 +34,12 @@ if (errors.length) die(`route enumeration failed:\n - ${errors.join('\n - ')}` /* ------------------------------------------------------------- robots.txt */ +// Kept byte-identical to the committed public/robots.txt so re-running this +// script is a no-op in the diff. If you change the wording, change it here — +// this is the generator, and the committed file is its output. const robots = [ - '# demo.primeintellectgrowth.com', - '#', - '# This site is meant to be found. Every page is public, static and safe to', - '# crawl; the source it documents is public too.', - '', + '# The inverse of primeintellectgrowth.com, deliberately: being found is the', + '# entire point of this site.', 'User-agent: *', 'Allow: /', '', diff --git a/src/components/demo/BlindCompare.tsx b/src/components/demo/BlindCompare.tsx index 21f5b13..561871d 100644 --- a/src/components/demo/BlindCompare.tsx +++ b/src/components/demo/BlindCompare.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import type { ComponentType } from 'react'; import { Eye, Trophy } from 'lucide-react'; import type { DemoStep } from '@/lib/demo-kit/types'; +import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import { formatOrDash } from './format'; @@ -80,7 +81,7 @@ export function BlindCompare({

Same puzzle, same seed ({seed}).

-
+
{sides.map(({ id, run }) => { const last = run.steps[run.steps.length - 1]; const picked = vote === id; @@ -107,13 +108,9 @@ export function BlindCompare({
{!revealed ? ( - + ) : (
@@ -136,7 +133,7 @@ export function BlindCompare({
Reward
{revealed && winner === id ? ( -
@@ -150,21 +147,13 @@ export function BlindCompare({ {!revealed ? (
- - + +
) : (

diff --git a/src/components/demo/CodeReceipt.tsx b/src/components/demo/CodeReceipt.tsx index d53865d..34db242 100644 --- a/src/components/demo/CodeReceipt.tsx +++ b/src/components/demo/CodeReceipt.tsx @@ -1,5 +1,7 @@ import { useMemo, useState } from 'react'; import { ExternalLink } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import * as st from '@/content/styles'; import { cn } from '@/lib/utils'; export interface CodeReceiptProps { @@ -26,17 +28,33 @@ export interface MarkedRange { end: number; } +/** + * True when a line is nothing but the marker inside a comment — `# --8<-- x`, + * `// region: grader`. Such a line is a POINTER to the code below it, not the + * code itself, and marking only the comment is the most common way this panel + * ends up highlighting nothing worth reading. + */ +function isPointerLine(line: string, marker: string): boolean { + const withoutMarker = line.replace(marker, ''); + return withoutMarker.replace(/[#/*\-\s]/g, '') === ''; +} + /** * Two marker conventions, because both exist in real repos: * - * TWICE — the marker brackets a region (`# --8<-- reward` … `# --8<--`). + * TWICE — the marker brackets a region (`# --8<-- reward` … `# --8<-- reward`). * 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. + * ONCE — the marker names a construct. If it sits ON the construct + * (`def compute_reward`), the range is that line plus its indented + * body. If it sits in a comment ABOVE it, the range starts at the + * next real line instead — a fence comment is a pointer, and + * highlighting the pointer rather than the function is a silent, + * plausible-looking failure. + * + * Blank lines inside a body are kept; trailing blank ones are not, or the + * highlight runs on past the end of 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. @@ -56,17 +74,24 @@ export function resolveMarkedRange(lines: string[], marker?: string): MarkedRang return last - first > 1 ? { start: first + 1, end: last - 1 } : { start: first, end: last }; } - const anchor = lines[first] ?? ''; + let start = first; + if (isPointerLine(lines[first] ?? '', marker)) { + const next = lines.findIndex((line, index) => index > first && line.trim() !== ''); + if (next === -1) return { start: first, end: first }; + start = next; + } + + const anchor = lines[start] ?? ''; const indent = anchor.length - anchor.trimStart().length; - let end = first; - for (let i = first + 1; i < lines.length; i += 1) { + let end = start; + for (let i = start + 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 }; + return { start, end }; } /** @@ -106,38 +131,37 @@ export function CodeReceipt({ return (

-
-

+
+ {/* Full width on a phone: sharing a row with two controls truncates a + path to "envs/wo…", which names nothing. */} +

{path}

- {range && !expanded ? ( - - ) : null} - {range && expanded ? ( - - ) : null} - {href ? ( - - Read the whole file - - ) : null} +
+ {range && !expanded ? ( + + ) : null} + {range && expanded ? ( + + ) : null} + {href ? ( + + Read the whole file + + ) : null} +
diff --git a/src/components/demo/DemoCard.tsx b/src/components/demo/DemoCard.tsx index 3d0f3db..4f2ecc5 100644 --- a/src/components/demo/DemoCard.tsx +++ b/src/components/demo/DemoCard.tsx @@ -2,8 +2,10 @@ 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 { VERTICAL_LABELS } from '@/lib/demo-kit/registry'; +import { Badge } from '@/components/ui/badge'; +import { DemoIcon } from '@/components/site/DemoIcon'; import { cn } from '@/lib/utils'; -import { DemoIcon } from './icons'; export interface DemoCardProps { meta: DemoMeta; @@ -11,7 +13,7 @@ export interface DemoCardProps { 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 + * go stale the first time the board changed and nobody would notice; this * cannot, because it is the same component the demo page renders. */ Surface?: ComponentType<{ state: T; compact?: boolean }>; @@ -20,52 +22,43 @@ export interface DemoCardProps { className?: string; } -export function DemoCard({ - meta, - href, - Surface, - thumbnailState, - className, -}: DemoCardProps) { - const to = href ?? `/demo/${meta.slug}`; +export function DemoCard({ meta, href, Surface, thumbnailState, className }: DemoCardProps) { + const to = href ?? `/demos/${meta.slug}`; const isSpec = meta.status === 'spec'; const showSurface = Surface !== undefined && thumbnailState !== undefined; return (
- - + +

{/* Stretched link: the whole card is the hit target, but there is still exactly ONE link in the accessibility tree for it. */} - + {meta.title}

{meta.tagline}

{isSpec ? ( - + Spec - + ) : null}
{showSurface ? (
- {/* 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. */} + {/* Decorative: the title and tagline already name the demo, and a + board with no run behind it is not information. */} @@ -79,7 +72,7 @@ export function DemoCard({
Vertical
-
{meta.vertical.replace(/-/g, ' ')}
+
{VERTICAL_LABELS[meta.vertical]}

@@ -91,7 +84,7 @@ export function DemoCard({

{isSpec ? 'Read the specification' : 'Open the demo'}

diff --git a/src/components/demo/DemoErrorBoundary.tsx b/src/components/demo/DemoErrorBoundary.tsx index 05c9a37..0dc22da 100644 --- a/src/components/demo/DemoErrorBoundary.tsx +++ b/src/components/demo/DemoErrorBoundary.tsx @@ -1,6 +1,7 @@ import { Component } from 'react'; import type { ErrorInfo, ReactNode } from 'react'; import { AlertTriangle, ExternalLink, RotateCcw } from 'lucide-react'; +import { Button } from '@/components/ui/button'; const REPO_URL = 'https://github.com/karti-ai/PIG-Demo'; @@ -54,7 +55,7 @@ export class DemoErrorBoundary extends Component
-
); diff --git a/src/components/demo/DemoShell.tsx b/src/components/demo/DemoShell.tsx index fee8230..d008fe3 100644 --- a/src/components/demo/DemoShell.tsx +++ b/src/components/demo/DemoShell.tsx @@ -1,14 +1,15 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; +import type { ComponentType, ReactNode } from 'react'; import { useParams } from 'react-router-dom'; -import * as Tabs from '@radix-ui/react-tabs'; -import { useUrlState } from '@/lib/url-state'; -import type { - DemoEpisode, - DemoModule, - DemoStep, - RunRef, - StoryBeat, -} from '@/lib/demo-kit/types'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Skeleton } from '@/components/ui/skeleton'; +import { listRuns, loadEpisode, rewardTotal } from '@/lib/demo-kit/episode'; +import { usePlayer } from '@/lib/demo-kit/player'; +import { loadDemoModule } from '@/lib/demo-kit/registry'; +import type { AnyDemoModule } from '@/lib/demo-kit/registry'; +import type { DemoEpisode, DemoStep, RunRef, StoryBeat } from '@/lib/demo-kit/types'; +import { useRunParam, useSpeedParam, useStepParam, useTabParam } from '@/lib/url-state'; +import * as st from '@/content/styles'; import { cn } from '@/lib/utils'; import { BeatSection } from './BeatSection'; import { BlindCompare } from './BlindCompare'; @@ -24,142 +25,71 @@ import { ReasoningPanel } from './ReasoningPanel'; import { RewardBreakdown } from './RewardBreakdown'; import { RewardEditor } from './RewardEditor'; import type { RewardArm } from './RewardEditor'; +import { SegmentedControl } from './SegmentedControl'; import { SlotRegion } from './SlotRegion'; import { StatStrip } from './StatStrip'; import type { Stat } from './StatStrip'; import { StepTimeline } from './StepTimeline'; -import { RecordedBadge, TracePlayer, useTracePlayback } from './TracePlayer'; +import { RecordedBadge, TracePlayer } from './TracePlayer'; import { VerifyBadge } from './VerifyBadge'; -import { clampIndex, formatOrDash, useIsDesktop } from './format'; -import { scoreReward } from './reward-math'; -import { mockDemo, mockEpisodes, mockRuns } from './mock'; +import { formatOrDash, useIsDesktop } from './format'; const REPO_BLOB = 'https://github.com/karti-ai/PIG-Demo/blob/main/'; -const MANIFEST_URL = '/traces/manifest.json'; -/** - * Every demo module in the repo, as an unresolved import each. - * - * `import.meta.glob` rather than a generated registry import on purpose: this - * file must compile and render before any demo directory exists, and a glob - * that matches nothing is an empty object rather than a build error. - */ -const DEMO_MODULES = import.meta.glob>('/src/demos/*/index.{ts,tsx}'); +/** The tab the step-detail strip opens on. Kept out of the URL when it is this. */ +const DEFAULT_DETAIL_TAB = 'reasoning'; -export interface DemoBundle { - demo: DemoModule; +/** Reserved slug for the shell's own hand-written demo. Dev builds only. */ +const MOCK_SLUG = '__mock'; + +export interface DemoBundle { + demo: AnyDemoModule; runs: RunRef[]; episodes: Record; } -type LoadState = +type LoadState = | { status: 'loading' } - | { status: 'ready'; bundle: DemoBundle } + | { status: 'ready'; bundle: DemoBundle } | { status: 'error'; message: string }; -function isRunRef(value: unknown): value is RunRef { - if (typeof value !== 'object' || value === null) return false; - const run = value as Record; - return ( - typeof run['id'] === 'string' && - typeof run['label'] === 'string' && - typeof run['path'] === 'string' && - typeof run['model'] === 'string' - ); -} - -function isEpisode(value: unknown): value is DemoEpisode { - if (typeof value !== 'object' || value === null) return false; - const episode = value as Record; - return ( - typeof episode['runId'] === 'string' && - Array.isArray(episode['turns']) && - typeof episode['rewards'] === 'object' && - episode['rewards'] !== null - ); -} - /** - * The manifest is data on disk, not a typed import, so it is validated rather - * than trusted — and three plausible shapes are accepted because the file is - * written by a script in another lane and a keyed map, a nested map and a flat - * list are all reasonable things for that script to have produced. + * A demo's module plus every recorded run it has. + * + * `loadDemoModule` and `loadEpisode` both cache their promises, so the route + * loader having already fetched the module makes this resolve without a second + * request. Runs are loaded with `allSettled` on purpose: one unreadable trace + * drops that arm rather than blanking the page. */ -export function extractRuns(json: unknown, slug: string): RunRef[] { - if (typeof json !== 'object' || json === null) return []; - const root = json as Record; - const nested = root['demos']; - const keyed = - (Array.isArray(root[slug]) ? root[slug] : undefined) ?? - (typeof nested === 'object' && nested !== null - ? (nested as Record)[slug] - : undefined); - if (Array.isArray(keyed)) return keyed.filter(isRunRef); - - const flat = Array.isArray(root['runs']) ? root['runs'] : Array.isArray(json) ? json : null; - if (flat) { - return flat.filter(isRunRef).filter((run) => { - const owner = (run as unknown as Record)['demo']; - return owner === undefined || owner === slug; - }); - } - return []; -} - -function pickModule(mod: Record): DemoModule | null { - const candidate = mod['default'] ?? mod['demo']; - if (typeof candidate !== 'object' || candidate === null) return null; - const shape = candidate as Record; - return typeof shape['adapt'] === 'function' && typeof shape['Surface'] === 'function' - ? (candidate as DemoModule) - : null; -} - async function loadBundle(slug: string): Promise { - if (slug === '__mock') { - return { demo: mockDemo as unknown as DemoModule, runs: mockRuns, episodes: mockEpisodes }; + if (slug === MOCK_SLUG) { + // Dynamic, so the mock lands in its own chunk and production never fetches + // it. A static import would ship several hundred lines of fake trace to + // every visitor of every real demo. + const mock = await import('./mock'); + return { demo: mock.mockDemo, runs: mock.mockRuns, episodes: mock.mockEpisodes }; } - const entry = Object.entries(DEMO_MODULES).find(([path]) => - path.startsWith(`/src/demos/${slug}/index.`), - ); - if (!entry) throw new Error(`No demo is registered under the slug "${slug}".`); - const demo = pickModule(await entry[1]()); - if (!demo) { - throw new Error(`The module for "${slug}" does not export a demo that satisfies the contract.`); - } + const demo = await loadDemoModule(slug); + const runs = await listRuns(slug).catch(() => [] as RunRef[]); + const settled = await Promise.allSettled(runs.map((run) => loadEpisode(run))); - const manifest = await fetch(MANIFEST_URL, { cache: 'no-cache' }) - .then((response) => (response.ok ? response.json() : null)) - .catch(() => null); - const runs = extractRuns(manifest, slug); - - // One unreadable trace must not blank the page: fetch them all, keep the - // ones that parse, and let the shell report the shortfall. - const loaded = await Promise.all( - runs.map(async (run) => { - try { - const response = await fetch(run.path, { cache: 'no-cache' }); - if (!response.ok) return null; - const json: unknown = await response.json(); - return isEpisode(json) ? ([run.id, json] as const) : null; - } catch { - return null; - } - }), - ); const episodes: Record = {}; - for (const item of loaded) { - if (item) episodes[item[0]] = item[1]; - } + settled.forEach((outcome, index) => { + const run = runs[index]; + if (!run) return; + if (outcome.status === 'fulfilled') episodes[run.id] = outcome.value; + else console.error(`[pig-demo] dropped run "${run.id}":`, outcome.reason); + }); + return { demo, runs: runs.filter((run) => episodes[run.id] !== undefined), episodes }; } -export interface DemoShellProps { +export interface DemoShellProps { /** Overrides the route param. Useful for previews and tests. */ slug?: string; /** Skips loading entirely when the caller already has the bundle. */ - bundle?: DemoBundle; + bundle?: DemoBundle; } /** @@ -168,13 +98,12 @@ export interface DemoShellProps { * It owns four things and no more: loading, the narrative beats, the URL state, * and the page's single polite live region. Everything visual is delegated to * the surfaces in this directory, and the demo module is never reached into — - * the shell is generic over the demo's board type and only ever calls `adapt` - * and renders `Surface`. + * the shell only ever calls `adapt` and renders `Surface`. */ -export function DemoShell({ slug: slugProp, bundle }: DemoShellProps) { +export function DemoShell({ slug: slugProp, bundle }: DemoShellProps) { const params = useParams(); const slug = slugProp ?? params['slug'] ?? ''; - const [state, setState] = useState>( + const [state, setState] = useState( bundle ? { status: 'ready', bundle } : { status: 'loading' }, ); @@ -187,7 +116,7 @@ export function DemoShell({ slug: slugProp, bundle }: DemoShellProp setState({ status: 'loading' }); loadBundle(slug) .then((loaded) => { - if (live) setState({ status: 'ready', bundle: loaded as DemoBundle }); + if (live) setState({ status: 'ready', bundle: loaded }); }) .catch((error: unknown) => { if (!live) return; @@ -204,36 +133,36 @@ export function DemoShell({ slug: slugProp, bundle }: DemoShellProp if (state.status === 'loading') return ; if (state.status === 'error') { return ( -
-

That demo is not here

-

{state.message}

- - Back to the gallery - -
+
+
+

That demo is not here

+

{state.message}

+ + Back to the demos + +
+
); } return ( + // A second boundary inside the route's own: this one is keyed to the demo + // so a crash names it, and resetting re-renders the surfaces rather than + // re-navigating. ); } -function DemoBody({ bundle }: { bundle: DemoBundle }) { +function DemoBody({ bundle }: { bundle: DemoBundle }) { const { demo, runs, episodes } = bundle; const isDesktop = useIsDesktop(); - // The shell writes `?step=` on every advance, including during playback. It - // is `@/lib/url-state`'s job to REPLACE rather than push for these — pushing - // would turn a six-step run into six back-button presses. - const [runParam, setRunParam] = useUrlState('run', ''); - const [stepParam, setStepParam] = useUrlState('step', '0'); - const [tabParam, setTabParam] = useUrlState('tab', ''); + const [runParam, setRunParam] = useRunParam(); + const [stepParam, setStepParam] = useStepParam(); + const [tabParam, setTabParam] = useTabParam(DEFAULT_DETAIL_TAB); + const [speedParam, setSpeedParam] = useSpeedParam(); const run = useMemo( () => runs.find((candidate) => candidate.id === runParam) ?? runs[0], @@ -241,34 +170,34 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) { ); const episode = run ? episodes[run.id] : undefined; - const steps: DemoStep[] = useMemo( - () => (episode ? (demo.adapt(episode) as DemoStep[]) : []), + const steps = useMemo[]>( + () => (episode ? demo.adapt(episode) : []), [demo, episode], ); - const step = clampIndex(Number(stepParam), steps.length); - const setStep = useCallback( - (next: number) => setStepParam(String(clampIndex(next, steps.length))), - [setStepParam, steps.length], - ); + const player = usePlayer(steps, { + initialIndex: stepParam, + initialSpeed: speedParam, + onIndexChange: setStepParam, + }); - const playback = useTracePlayback({ stepCount: steps.length, step, onStepChange: setStep }); - const current = steps[step]; + // The URL is the other writer of this state — Back, a pasted permalink, the + // run switcher. The player is the source of truth while it is running, so it + // only follows the URL when the two have actually diverged. + const { seek } = player; + useEffect(() => { + if (stepParam !== player.index) seek(stepParam); + // Intentionally keyed on the URL only: including `player.index` here would + // re-run the effect on the player's own advance and fight it. + }, [stepParam, seek]); const hasBeat = (surface: StoryBeat['surface']) => demo.narrative.beats.some((beat) => beat.surface === surface); const timelineInSplit = !hasBeat('scrubber'); const extras = demo.tabs ?? []; - const extrasInCustom = hasBeat('custom'); + const extrasInCustomBeat = hasBeat('custom'); - const tabIds = useMemo(() => { - const ids = isDesktop ? ['reasoning', 'call'] : ['call']; - if (!extrasInCustom) ids.push(...extras.map((tab) => tab.id)); - return ids; - }, [isDesktop, extras, extrasInCustom]); - const activeTab = tabIds.includes(tabParam) ? tabParam : (tabIds[0] ?? 'call'); - - const arms: RewardArm[] = useMemo( + const arms = useMemo( () => runs.map((candidate) => { const armEpisode = episodes[candidate.id]; @@ -283,15 +212,6 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) { [runs, episodes], ); - const totals = useMemo( - () => - arms.map((arm) => ({ - label: arm.label, - total: scoreReward(demo.reward, arm.values).total, - })), - [arms, demo.reward], - ); - const blindPair = useMemo(() => { for (let i = 0; i < runs.length; i += 1) { for (let j = i + 1; j < runs.length; j += 1) { @@ -304,75 +224,113 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) { return { left, right, leftEpisode, rightEpisode }; } } + // Two runs on different seeds are two different puzzles; showing them side + // by side would be a comparison of luck. return null; }, [runs, episodes]); if (!run || !episode || steps.length === 0) { return ( -
-

{demo.meta.title}

-

+

+

{demo.meta.title}

+

No recorded run is available for this demo yet. The environment and its grader are in - the repository; the traces are produced by the eval command on the demo's provenance - card. + the repository; the traces come from the eval command on the provenance card.

-
+ ); } - const Surface = demo.Surface as unknown as React.ComponentType<{ - state: T; - compact?: boolean; - }>; + const Surface = demo.Surface as ComponentType<{ state: unknown; compact?: boolean }>; + const current = steps[player.index]; + const lastStep = steps[steps.length - 1]; const heroStats: Stat[] = [ { label: 'Outcome', value: episode.outcome, tone: episode.outcome === 'solved' ? 'positive' : 'warning', - hint: episode.truncated ? 'Truncated before a terminal state' : undefined, + ...(episode.truncated ? { hint: 'Truncated before a terminal state' } : {}), }, { label: 'Total reward', - value: formatOrDash(scoreReward(demo.reward, episode.rewards).total), + value: formatOrDash(rewardTotal(episode.rewards, demo.reward.components)), tone: 'brand', hint: 'Shipped weights', }, { label: 'Steps', value: steps.length, hint: 'Model calls in this run' }, - { label: 'Seed', value: episode.seed, hint: 'Same seed reproduces this board' }, + { label: 'Seed', value: episode.seed, hint: 'The same seed reproduces this board' }, ]; - const reasoningPanel = ( - - ); - const timeline = ( { - playback.setPlaying(false); - setStep(next); + player.pause(); + player.seek(next); }} Surface={Surface} - onTogglePlay={playback.toggle} + onTogglePlay={player.toggle} /> ); - const renderSurface = (beat: StoryBeat) => { + const detailTabs: { id: string; label: string; content: ReactNode }[] = [ + { + id: 'reasoning', + label: 'Reasoning', + content: isDesktop ? ( + + ) : ( + // Under `lg` there is no column for this, and putting it below the + // board means watching the run with the thinking off-screen. The sheet + // is mounted only here, so vaul never locks body scroll on desktop. + + ), + }, + { + id: 'call', + label: 'Model call', + content: ( +
+ + {current?.reply ? ( +
+

Reply

+

+ {current.reply} +

+
+ ) : null} +
+ ), + }, + ...(extrasInCustomBeat + ? [] + : extras.map((tab) => ({ id: tab.id, label: tab.label, content: }))), + ]; + const activeTab = detailTabs.some((tab) => tab.id === tabParam) ? tabParam : DEFAULT_DETAIL_TAB; + + const renderSurface = (beat: StoryBeat): ReactNode => { switch (beat.surface) { case 'hero': return ( -
+
- ).state} /> + {lastStep ? : null}
@@ -382,9 +340,7 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) { {...(run.intervention ? { intervention: run.intervention } : {})} className="ml-0 w-fit" /> -

- {demo.narrative.thesis} -

+

{demo.narrative.thesis}

); @@ -400,90 +356,61 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) { runs={runs} activeId={run.id} onSelect={(id) => { - playback.setPlaying(false); + player.pause(); + // The run param setter also zeroes `step`: step 6 of a + // nine-turn rollout is not step 6 of a three-turn one. setRunParam(id); - setStepParam('0'); }} /> ) : null} (next ? player.play() : player.pause())} + speed={player.speed} + onSpeedChange={(next) => { + player.setSpeed(next); + setSpeedParam(next); + // `instant` is a destination, not a rate. The player only + // consumes it while running, so choosing it from a paused + // transport has to start the run — otherwise the button + // visibly does nothing, which reads as broken. + if (next === 'instant') player.play(); + }} + onRestart={player.restart} + step={player.index} stepCount={steps.length} onStepChange={(next) => { - playback.setPlaying(false); - setStep(next); + player.pause(); + player.seek(next); }} + progress={player.progress} + timingIsReal={player.timingIsReal} model={run.model} capturedAt={run.capturedAt} {...(run.intervention ? { intervention: run.intervention } : {})} /> -
+
{current ? : null}
-
- {!isDesktop ? ( - - ) : null} - - - - {isDesktop ? Reasoning : null} - Model call - {!extrasInCustom - ? extras.map((tab) => ( - - {tab.label} - - )) - : null} - - - {isDesktop ? ( - - {reasoningPanel} - - ) : null} - - - {current?.reply ? ( -
-

Reply

-

- {current.reply} -

-
- ) : null} -
- {!extrasInCustom - ? extras.map((tab) => ( - - - - )) - : null} -
+
+ + + {detailTabs.map((tab) => ( + + {tab.label} + + ))} + + {detailTabs.map((tab) => ( + + {tab.content} + + ))} +
@@ -496,6 +423,12 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) { return (
{timeline} + {player.timingIsReal ? null : ( +

+ Some steps in this run carried no recorded latency, so their dwell on the timeline + is the player's fallback rather than a measurement. +

+ )}
); @@ -514,23 +447,24 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) { ); case 'metric': { - const currentTotal = scoreReward(demo.reward, episode.rewards).total; - const first = totals[0]; - const series = totals - .filter((entry): entry is { label: string; total: number } => entry.total !== null) - .map((entry) => ({ x: entry.label, y: entry.total })); + const currentTotal = rewardTotal(episode.rewards, demo.reward.components); + const points = arms + .map((arm) => ({ x: arm.label, y: rewardTotal(arm.values, demo.reward.components) })) + .filter((point): point is { x: string; y: number } => point.y !== null); + const baselineArm = arms[0]; + const baselineValue = baselineArm + ? rewardTotal(baselineArm.values, demo.reward.components) + : null; return (
1 + ? { baseline: { value: baselineValue, label: baselineArm.label } } : {})} - series={series} - caption={ - 'Every point is a recorded run scored by the same grader. Nothing here is a projection.' - } + series={points} + caption="Every point is a recorded run scored by the same grader. Nothing here is a projection." /> {blindPair ? ( ({ bundle }: { bundle: DemoBundle }) { ...(blindPair.left.intervention ? { intervention: blindPair.left.intervention } : {}), - steps: demo.adapt(blindPair.leftEpisode) as DemoStep[], - total: scoreReward(demo.reward, blindPair.leftEpisode.rewards).total, + steps: demo.adapt(blindPair.leftEpisode), + total: rewardTotal(blindPair.leftEpisode.rewards, demo.reward.components), }} b={{ runId: blindPair.right.id, @@ -553,8 +487,8 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) { ...(blindPair.right.intervention ? { intervention: blindPair.right.intervention } : {}), - steps: demo.adapt(blindPair.rightEpisode) as DemoStep[], - total: scoreReward(demo.reward, blindPair.rightEpisode.rewards).total, + steps: demo.adapt(blindPair.rightEpisode), + total: rewardTotal(blindPair.rightEpisode.rewards, demo.reward.components), }} /> ) : null} @@ -564,7 +498,7 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) { case 'receipt': return ( -
+
({ bundle }: { bundle: DemoBundle }) { }; return ( -
+
{/* The page's ONE live region. Every step change lands here and nowhere - else: with reduced motion the tile animation is gone, so this sentence + else: with reduced motion the board animation is gone, so this sentence is the only thing that tells a screen-reader user what just happened. */}
@@ -607,13 +541,9 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) {
-

- {demo.meta.vertical.replace(/-/g, ' ')} · for {demo.meta.persona} -

-

- {demo.meta.title} -

-

{demo.meta.tagline}

+

For {demo.meta.persona}

+

{demo.meta.title}

+

{demo.meta.tagline}

{demo.narrative.anxiety}

@@ -626,18 +556,7 @@ function DemoBody({ bundle }: { bundle: DemoBundle }) { ))}
-
- ); -} - -function TabTrigger({ value, children }: { value: string; children: React.ReactNode }) { - return ( - - {children} - + ); } @@ -651,50 +570,38 @@ function RunSwitcher({ onSelect: (id: string) => void; }) { return ( -
- {runs.map((run) => { - const active = run.id === activeId; - return ( - - ); - })} -
+ ({ + value: run.id, + label: run.label, + ...(run.intervention ? { title: run.intervention } : {}), + }))} + value={activeId} + onChange={onSelect} + className="border border-border p-1" + optionClassName="tap px-3 text-sm" + /> ); } /** - * The loading state. Deliberately shaped like the page it becomes, and with no - * spinner: a spinner on this site would imply a live model call, which is the - * one thing the whole page is at pains to say is not happening. + * The loading state. Shaped like the page it becomes, and with no spinner: a + * spinner here would imply a live model call, which is the one thing the whole + * page is at pains to say is not happening. */ function ShellSkeleton() { return ( -
+

Loading the recorded run.

-
-
-
+ + +
{[0, 1, 2, 3].map((index) => ( -
+ ))}
-
+
); } diff --git a/src/components/demo/LimitsCallout.tsx b/src/components/demo/LimitsCallout.tsx index fd471c6..3056f83 100644 --- a/src/components/demo/LimitsCallout.tsx +++ b/src/components/demo/LimitsCallout.tsx @@ -1,5 +1,7 @@ import { ArrowRight, ShieldQuestion } from 'lucide-react'; +import { getDemo } from '@/lib/demo-kit/registry'; import type { Limit } from '@/lib/demo-kit/types'; +import * as st from '@/content/styles'; import { cn } from '@/lib/utils'; export interface LimitsCalloutProps { @@ -13,8 +15,15 @@ export interface LimitsCalloutProps { className?: string; } -function defaultResolve(slug: string) { - return { title: slug, href: `/demo/${slug}` }; +/** + * A limit names the demo that closes it by SLUG. Resolving it through the + * registry means a limit pointing at a demo that does not exist yet renders as + * a stated gap rather than as a link to a 404 — which is the honest outcome, + * since the roadmap is allowed to be ahead of the repository. + */ +function defaultResolve(slug: string): { title: string; href: string } | undefined { + const meta = getDemo(slug); + return meta ? { title: meta.title, href: `/demos/${meta.slug}` } : undefined; } /** @@ -35,7 +44,7 @@ export function LimitsCallout({ return (
-
    @@ -47,10 +56,10 @@ export function LimitsCallout({ {target ? ( Answered by {target.title} - ) : (

    diff --git a/src/components/demo/MetricChart.tsx b/src/components/demo/MetricChart.tsx index 21ff6fe..7426b9b 100644 --- a/src/components/demo/MetricChart.tsx +++ b/src/components/demo/MetricChart.tsx @@ -82,7 +82,10 @@ export default function MetricChart({ )} /> 1e-9 ? rawDelta : null; const points = useMemo(() => series ?? [], [series]); return ( diff --git a/src/components/demo/ProvenanceCard.tsx b/src/components/demo/ProvenanceCard.tsx index 8ee9f04..5758150 100644 --- a/src/components/demo/ProvenanceCard.tsx +++ b/src/components/demo/ProvenanceCard.tsx @@ -1,6 +1,8 @@ import { useState } from 'react'; import { Check, Copy, ExternalLink } from 'lucide-react'; import type { Provenance, RunRef } from '@/lib/demo-kit/types'; +import { Button } from '@/components/ui/button'; +import * as st from '@/content/styles'; import { cn } from '@/lib/utils'; import { formatDate } from './format'; @@ -77,21 +79,17 @@ export function ProvenanceCard({ provenance, run, className }: ProvenanceCardPro

    Run it yourself

    - +
-
-          {provenance.command}
+        
+          {provenance.command}
         

{copyState === 'copied' @@ -112,11 +110,11 @@ export function ProvenanceCard({ provenance, run, className }: ProvenanceCardPro

  • {credit.label} -
  • ))} diff --git a/src/components/demo/ReasoningDrawer.tsx b/src/components/demo/ReasoningDrawer.tsx index 398bd6d..22eefed 100644 --- a/src/components/demo/ReasoningDrawer.tsx +++ b/src/components/demo/ReasoningDrawer.tsx @@ -1,6 +1,14 @@ import { useState } from 'react'; -import { Drawer } from 'vaul'; import { Brain, ChevronUp } from 'lucide-react'; +import { + Drawer, + DrawerBody, + DrawerContent, + DrawerDescription, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from '@/components/ui/drawer'; import { cn } from '@/lib/utils'; import { ReasoningPanel } from './ReasoningPanel'; import type { ReasoningPanelProps } from './ReasoningPanel'; @@ -36,48 +44,41 @@ export function ReasoningDrawer({ const hasReasoning = (panel.reasoning ?? '').length > 0; return ( - - - - - - -