The prototype audited through Mark Pincus's product framework (Life at the Speed of Play, ch. 4, 2025) — where we've copied faithfully, where we haven't earned the right to differ yet, which "improvements" are really unproven bets, and which one New idea deserves the isolated test. Written 2026-07-16; gaps 8–9 (the long-press action bar · DMs) added the same day; re-grounded for a world audience (not China-first) the same day; gaps 10–12 (the dismissal-grammar sweep · user profiles · the emoji library) added 2026-07-17; gap 13 (the contact layer) + the shipped-card fold added 2026-07-19.
| Rule | What it says | Applied to us |
|---|---|---|
| Platform shift → go heavy on Proven | During a shift (web → mobile → AI, now) users are in discovery mode; Proven + Better alone can win — the platform itself is the New. Zynga on Facebook, Supercell on mobile, Hay Day = FarmVille-for-mobile with zero New. | We are squarely inside the AI shift. The framework says: nail the group-chat Proven, keep the Betters sharp — the persona room already carries all the New the moment needs. |
| Stacked New → the Tribe failure | Pincus's Tribe.net reinvented everything it touched — wide, shallow, confusing. Zynga Poker was the antithesis: one Better (no download), one New (real friends' faces at the table). | Counted honestly, we have ≈7 New ideas live at once (§ New below). Only one of them runs as a disciplined, isolated experiment. |
Our audience's two proven products: the WhatsApp group chat — the world's default group-conversation surface (iMessage and Telegram as siblings; WeChat for the Chinese-speaking slice) — and Character.ai (Talkie · Replika; 星野/猫箱 as the regional siblings) for the persona side: the proof that talking to characters is a mass daily habit, and the reference for how personas are discovered.
Already copied faithfully: bubbles / composer / chat list · @mentions as entities · the typing indicator · streaming replies · PWA with back-gesture + hero collapse · dark mode · persona cards + fuzzy picker search. The UX benchmark already graded us against WhatsApp / WeChat / Telegram feature by feature; this page adds the strategic frame — which borrowings are load-bearing and why now.
The FarmVille empty-neighbor-bar lesson: Skaggs removed the friend-request step as "friction"; Pincus kept it, because the requests were the reminder to come back. A ping is our only reminder — and the manners v2 experiment is judged on return rate. Without a come-back channel, that metric fails for a Proven-gap reason, not because the New failed: false signal on our own chosen metric. The notifications rail ships everything up to the app's edge; Web Push was deferred under the China-first framing (the GFW tax) — for a world audience FCM/APNs just work, so the deferral has lost its reason. This gap is the argument for pulling it forward.
notify() for notices, the message broadcast for chat), never a parallel path. Payload is a bubble exactly as mocked: sender name · text preview (markdown-stripped) · avatar icon · deep-link — tap focuses the app and routes to the room (no reload). Per-room coalescing so a chatty panel bursts as ONE evolving notification (WhatsApp). Suppressed only where it's genuinely redundant — the server skips a member holding that room's live stream, and the SW skips a message for the room you're focused on this second. Everything else always shows, even with the app open: a broader "app is visible → skip" rule broke the userVisibleOnly contract and got the origin flagged by Chrome as possible spam (fixed v508 — see the rail's two device-learned rules). Permission is never prompted on load — a soft-ask card in the notification centre + a one-time nudge after your first DM/invite + a per-device toggle in the Me sheet. iOS shows an Add-to-Home-Screen hint until the PWA is installed (16.4+); mainland devices that can't reach FCM degrade silently — the in-app rail still covers them. Sent/click events are logged so the return-rate loop (did a push bring them back?) is measurable. Out of v1 (New backlog): personas that INITIATE new content for a push — the greenroom she-speaks-first bubble is the embryo, not proactive outreach.
The shipped dictating composer is speech-to-text — arguably a Better (searchable, quotable, no 60-second replay tax). But the proven mechanic — hold, speak, a voice bubble lands; the other side answers in a voice you can play — is what our audience's thumbs already know. Every piece exists (iFlytek STT in, TTS providers out); the mechanic just isn't assembled. Personas with voices is also the natural end of the roadmap's listen-mode thread.
In a room with four speakers, replying to a specific message is how threads stay legible — table stakes in every group chat our users know. Doubly valuable here: a quote is a free targeting signal for the floor producer — "this human is answering Rams's second point" arrives as structure, not inference. The UX benchmark already ranked quote-to-panel near the top of the borrow list.
@{p:slug} mentions — never re-matched text.
❝{q:<lid>} marker line (the stable line id every bubble now carries); tapping the quoted block jumps to the original and flashes it. Bonus wired in: the floor producer reads a quote as a targeting signal — the quoted host is staged like an @-mention, and the v1p brief carries an explicit QUOTE-REPLY line. v436 adds WhatsApp's second door: swipe the bubble right — it rides the finger, a reply arrow fades in, release past the stake and the same quote docks.
The lowest-friction engagement loop in messaging — one tap, social, visible to the room. We already have a 👍/👎 feedback row inside persona bubbles, but that's the analyst's instrument (it feeds the producer's brief and hides from other guests). A reaction is the social-native cousin: it lives on the message, everyone sees it, personas can notice it — and it still feeds the same guest-feedback brief for free.
lid, stored as append-only react events with a current-state projection on every replay (+ a react SSE push, optimistic taps, local-first cache mirror); a redaction takes its reactions with it, delete-for-me hides them per-user. The floor producer reads them as a REACTIONS warmth line in f's brief; the panel transcript deliberately carries none (§8 append-only cache). Rendering is self-hosted Twemoji SVGs (/emoji/, CC-BY 4.0) so chips look identical on every OS. Phase 2, noted not started: personas reacting sparingly (needs a new panel emission type) · a "X reacted" notice on the rail. Closed by gap 12 (v478–v480): the renderRich Twemoji pass for message-text emoji now ships, and this card's + picker — the parked curated ~300 — was retired with it: the composer's emoji panel is the one picker, reactions and text share the one asset pipeline, and a reaction feeds the same Recents the panel reads.
Proven twice over: every chat app sends photos, and since vision landed, every serious AI chat reads them — it's table stakes on both reference products now. And it's the native use of our own advisory seats: the entire 小胖看房 corpus is about apartments people are looking at. "Here's the floor plan" is the message our users are already trying to send.
w×h/750 token formula is gone:
vision is now patch-based, ⌈w/28⌉ × ⌈h/28⌉ visual tokens (28-px patches), with a per-model resolution cap.
| model (role) | vision? | formula & limits | $ / 1–2 MP photo | under our §8 cache (1h) |
|---|---|---|---|---|
| DeepSeek v4 Pro / Flash (panel · the default) |
✗ text-only | no image input on the platform API, no vision model offered (confirmed 2026-07-17) | — | a text shadow (~400 tok/photo) costs ≈$0.0002 once (miss $0.435/M), then ~$0.000002/turn (hit $0.003625/M) — free in practice |
| Claude Sonnet 4.6 (panel) |
✓ standard tier | downscaled to ≤1568 px long edge, cap 1,568 vtok · JPEG/PNG/GIF/WebP · ≤10 MB · ≤100/request | ~1,300–1,570 tok → $0.004–0.005 base ($3/M) | image blocks cache like text: 2× write once ($0.008–0.009 at $6/M), then 0.1× read = +$0.0005/photo/turn ($0.30/M); a cold reopen (>1h) re-writes the lot |
| Gemini 3.1 Flash Lite (sidecar candidate — already in cost.py, GEMINI key already wired) |
✓ | 258 tok per 768×768 tile → 1–2 MP ≈ 1.0–1.5k tok · PNG/JPEG/WebP/HEIC/HEIF | in $0.25/M + desc out $1.50/M → ≈$0.001 per photo call | pay-once at upload; the shadow rides the room prefix as ordinary text |
| Claude Haiku 4.5 (sidecar candidate — already in cost.py) |
✓ standard tier | same patch formula, 1,568-vtok cap | in $1/M + out $5/M → ≈$0.0035 per photo call | pay-once at upload |
| Gemini 3.5 Flash (sidecar candidate) |
✓ | same tiling as Flash Lite | in $1.50/M + out $9/M → ≈$0.006 per photo call | pay-once at upload (overkill for a describe call) |
| architecture | verdict | the numbers |
|---|---|---|
| A · native only | ✗ dead on arrival | The default model can't see — 小胖看房's natural home is a DeepSeek room. And rooms may switch provider mid-conversation (history is stored as plain text), so a text shadow is needed anyway. Native blocks also bloat the prefix: 3 photos on Sonnet = +4.7k tok → +$0.0014 every warm turn (3.5× the whole $0.0004 round) + $0.028 per cold reopen. |
| B · vision sidecar | ✓ recommended | ONE Flash-Lite call at upload ≈ $0.001/photo — 2.5× one round, paid once; per-turn cost unchanged. Works on every provider; the shadow is ordinary transcript text under §8, searchable (gap 7), and ledger-ready (model already priced in cost.py). Debt: the panel can never "look again" — mitigate with a domain-aware describe prompt (floor plan → orientation/light/layout/dimensions; screenshot → transcribe the text). |
| C · hybrid | ◐ later, as upgrade | Native block on the send turn for Anthropic rooms + the sidecar shadow for continuity. Costs one extra native pass ($0.005) and a one-time BP2 rewrite when the block swaps to text next turn; needs a second harness path — all to benefit only the non-default minority of rooms. Document as the per-room upgrade, don't build in v1. |
Our picker is pull: a search box (with suggested terms) that assumes you already know who you want. The proven pattern on every character app is push: a browsable feed of faces with a greeting hook — the character says something to you before you've chosen them. That's the discovery-mode front door for the user who arrives curious but empty-handed, which in a platform shift is almost everyone.
may_discover predicate — this feed is just a second caller of the same filter; private personas never surface.
Advisory rooms accumulate reference material — numbers, names, the thing Feddo said about the rate three weeks ago. Every messenger our users know can find it; we can't. The Notebook covers the deliberate save; search covers the other 95% — what you didn't know you'd need. Also on the benchmark's borrow list; this is the strategic why-now.
v434 shipped the first door: a stationary long-press opens the lean Reply · Save · Copy menu. WhatsApp's full grammar goes further — long-press selects the message (its whole row tints edge-to-edge; the bubble itself is untouched and nothing else dims; the reaction bar of gap 4 rides the same beat), and the title bar transforms into an action bar: reply · star · copy · forward · delete · ⋮ — then tapping other bubbles grows the selection and the verbs go batch. This is the hub where quote-reply (✓), reactions, and the Notebook converge on one gesture; the two verbs we genuinely lack are forward and delete.
The reason WhatsApp opens twenty times a day is people, not content — DMs are the frequency engine behind Nikita Bier's Heat = Frequency × Intensity. Without them we're a destination you visit; with them we're the messenger you live in. And it's barely a new subsystem: the room architecture already implies it — a DM is the zero-persona room, the one cell of the cardinality matrix we never allowed. The twist that makes it ours: invite a persona into the conversation later — which is exactly the core New (humans + personas at one table) arriving through a proven door.
| 0 personas | ≥1 personas | |
|---|---|---|
| 1 human | notes-to-self (freebie — WhatsApp message-yourself · Telegram Saved Messages) | ✓ 1:1 persona chat · the greenroom |
| 2+ humans | the gap — DM & group DM | ✓ multi-human room (roles · private history) |
may_discover (the v399 seating-door rules apply unchanged). The persona's history floor is set at the join moment — replay-level enforcement, the same see_from mechanism, shown honestly in the capsule. the proven pattern elsewhere is stricter still (WhatsApp and WeChat both spawn a new chat with no history when a 1:1 grows), so floor-at-join is the conservative copy; a "share the backstory" option would need both humans' consent — phase 2 at most.
Shipped as specified, and smaller than feared. The inventory (the standing record, one card per surface, every card owner-stamped before a line moved) found 23 of 36 layers already compliant — the newer surfaces were built to this grammar before it was written down, so the drift was concentrated in the older modal family the 2026-07-10 systemic check had standardized onto .dialog-x. Four task dialogs got their commit slot back; eight more took the door their type asks for. Two invariants kept a shared component safe: the sweep is opt-in (the bare .dialog-x default is byte-identical, so nothing unclassified could shift) and phone-only (desktop verified unchanged, gaining only Esc). The gesture half paid the bigger dividend: the iOS "reveal" turned out to be ours — body.nav-back muted two selectors while six translateX layers had shipped since — and one Esc handler now peels the same LIFO top layer Back does across 24 previously-uncovered layers. One finding deferred by decision: builder-ui.html has no back-stack, popstate or Esc at all and shares top-level history, so Back with its "Delete persona" confirm up unwinds the parent while the confirm stays painted — its own change.
Our layers close with an × in the top-right — a web idiom we standardized (the 2026-07-10 systemic check folded ~11 treatments into one scaffold), but it's our grammar, not the platform's. Native muscle memory differentiates by surface: a page goes back top-left; a task dialog cancels top-left and commits top-right; only overlays close top-right; sheets swipe down. Dismissal is a form of going back — that's why it lives on the left — and an × in the top-right steals the commit slot, creating the "did closing save my changes?" ambiguity the Material layout exists to prevent. Users should spend zero neurons learning our way. Dismissal has two halves: the affordance (where the glyph sits) and the gesture (what the thumb does) — this gap owns both.
| Surface type | The norm (muscle memory) | Ours — candidates for the sweep to classify |
|---|---|---|
| Page you navigate into | ← back, top-left; never × | chat info · persona page · Seen read · notification centre |
| Task dialog (has a commit) | × / Cancel top-LEFT · Done/Save top-RIGHT | hand-pick your panel (the new-chat picker) · forward target · TTS settings |
| Overlay / viewer | × top-right (or tap-out) | image viewer · artifact pane cards |
| Sheet / card | grabber + swipe-down; × optional | the Me sheet · reaction details sheet · pickers that are really menus |
registerBackLayers() (that registry IS the checklist) plus every user of the shared scaffold (.dialog-x/.dialog-back in theme.css, systemic-check #21). The scaffold is one component reused by many surfaces — a single flip would move the × on surfaces where ×-right is correct, so no blanket change, ever. (2) Mock first — a review page (docs/dismissal-sweep.html, the console-redesign pattern) with one card per dialog instance: current vs proposed side by side, its four-type classification, decided exceptions marked keep; the owner stamps every card before any code moves. (3) Apply — variant classes on the shared scaffold (task · overlay · sheet), each surface opting into its approved type; per-page tweaks only where a surface genuinely deviates. Then re-base the house "main dialog / sub dialog" grammar on the four types and re-sync the Style Guide.
registerBackLayers() coverage rather than assuming it (a layer shipped since the registry was written is the classic miss; the symptom is an edge swipe that leaves the room and kills the whole view instead of closing the sheet). The gesture and the glyph must always agree: whatever ← or ✕ does, the swipe does.
In every messenger a person is a face + a name + a one-liner, and tapping them anywhere opens the same small dossier. WhatsApp's anatomy, studied: your own profile = photo (tap to change) · display name · About — a ~139-character one-liner, preset or custom ("Hey there! I am using WhatsApp." is the famous default); someone else's = the contact-info page — big avatar (tap → full screen), name, about line, then shared media and shared groups, then the verbs (message · mute · block/report); privacy = photo, about, and last-seen/online each independently set to Everyone / My contacts / Nobody. We have the name half already (username = handle, display_name = shown, the signup preview bubble) — but our humans are still faceless monograms with no page, just as DMs (gap 9 ✓) gave them more surface than ever. The profile page is also the natural home for gap 9's Message privately door.
| WhatsApp element | Ours |
|---|---|
| Profile photo | new — avatar upload, monogram stays the fallback; photos/grey are human, colour stays reserved for persona seats |
| Display name | exists — display_name, the signup identity screen |
| About one-liner | new — ~140 chars, shown on the profile page |
| Phone number (identity anchor) | exists — the username handle |
| Per-field privacy (Everyone / Contacts / Nobody) | v1 default: visible to room-mates (our whole graph IS people you share a room with); the WhatsApp matrix later |
| Online / last seen | RAM presence already exists (never touches state.json) → a simple online dot v1; last-seen deferred |
| Contact-info verbs | Message privately (gap 9's door) + shared rooms list; mute/block are their own later gaps |
WhatsApp's quiet structural choice, studied: on iOS it defers to Apple's set, but on Android, Web and Desktop it ships its own emoji art (designed in-house, 2017) and replaces every emoji codepoint with that art at display time. The result: all non-iOS users see identical emoji regardless of OS vendor or version, and an emoji sent from the newest iPhone never renders as tofu □ on an old Android. The rest of the anatomy: the composer's smiley panel — search on localized CLDR keywords, Recents first (frecency), eight category tabs, long-press skin-tone variants remembered per emoji; and the signature delight — a message that is a single emoji renders large and bubble-less (WhatsApp scales 1–3; we shipped the lone-emoji case only, v482). Gap 4 deliberately fenced this out ("message-text emoji stay native — phase 2"); this gap is that phase 2, grown to its full shape: one emoji subsystem shared by reactions, the composer, and the text renderer.
/emoji, CC-BY attribution line) to the full set: reactions, the picker, and the text renderer share the same assets, the same CLDR keyword search (localized — ties into the i18n layer), the same recents and tone preferences. Don't build a second pipeline.
renderRich() after sanitize — replace each emoji grapheme with <img class="emj" alt="<char>"> so copy/paste and screen readers keep the real characters. Segmentation must be correct: ZWJ sequences (👨👩👧), variation selectors, tone modifiers — one grapheme, one image. It applies to persona bubbles too (models emit Unicode emoji — cross-platform consistency for the cast, free). Final render only; never in the streaming hot path.
j1/j2/j3 classes; the owner scrapped it for one clear rule and two sizes only — 21px inline, 35px solo.)
emojifyHtml() runs LAST in both paths — renderRich() after DOMPurify and fmtUserText() after esc() — so personas get cross-platform art free. It walks a detached DOM, not the string (a regex over HTML can match inside an attribute — that's how a src gets hijacked), builds src from codepoints only behind a hex guard, and skips code/pre and .kx (raw TeX that typesetMath() reads back as textContent, where an <img> would have silently eaten the character). Segmentation = \p{RGI_Emoji} under the v flag (one atom per RGI sequence), with a hand-built u fallback of the same UTS-51 shape for Safari <17; both require VS16 on text-default characters, so ™ and © stay text and only ™️ becomes art — the call the OS itself makes. Verified on 19 cases (ZWJ families, mixed-tone handshakes, flags that must not merge, bare digits that must not become keycaps). Solo emoji (v478, refined v482): the first cut scaled 1–3 emoji on a graded j1/j2/j3; the owner scrapped that for two sizes only — 21px inline, and 35px for a lone emoji, rendered bubble-less (transparent, no chrome — the .bubble element stays for lid/long-press/reactions, only the styling is stripped via !important). Computed at render time so history and replay agree; the chat-list preview stays plain text. The non-obvious half: a rendered bubble became a picture of its text, and an <img> contributes nothing to innerText — an emoji-only bubble read back as the empty string. Quote-reply, the pacing matcher (it compares a rendered bubble to RAW pending text) and note lookup all now read through speechText(); tokenizeSpeech() treats an img.emj as an atomic token that contributes its real characters, which also makes notes saved before the change match a re-rendered bubble.
#ta, never a child — the v477 tap router keys on "target is #ta or inside it", so the door can't be read as a composer tap. It is a keyboard-space occupant, not a new widget: kbSpace.occupy(h) reserves the keyboard's own measured height (mad-kbh, per orientation) and write() subtracts it from fullH, the keyboard-FREE layout height — never the live clientHeight, because Android's layout viewport shrinks with the keyboard and subtracting mid-animation would shrink the shell by ~2× the keyboard and snap back. Open = reserve THEN blur, so focusout's same-task restore (v475) computes the height the keyboard already had and setShell's dedupe makes it a no-op: zero viewport motion. The ⌨ flip goes through the v477 routing (pre-shrink + forced flush + focus({preventScroll})) — a raw .focus() re-opens the shove. The door's pointerdown only preventDefaults and does no layout: trap ② is that relayouting under a live finger makes iOS cancel the tap (exactly how v474 broke). Search runs on localized CLDR keywords (猫 → cats, 哭 → crying), Recents are frecency (count × a 14-day half-life), tones are remembered per emoji, and the grid mounts in 96-emoji chunks — a tab holds up to 552 and the set is 1,897. Data (/emoji/index.json + kw-{en,zh}.json, ~214KB from emojibase-data's CLDR) is vendored and fetched on first open only. Desktop gets a popover anchored to the smiley. Found by testing, not reasoning: when no keyboard ever materialises (a fine pointer at narrow width, a hardware keyboard) nothing re-fitted the shell after the flip — iOS leans on preShrink's write, Android on the keyboard's own resize, and that path had neither; one late guarded re-fit closes it.
window.openEmojiPanelFor(lid), deliberately unwired — twelve-only is gap 4's call) and it lands on the real thing. One prefs layer: recents and tone prefs moved into the shared core, and a reaction now feeds the same frecency store — tap 🎉 on the bar and it surfaces in the composer panel's Recents. The quick twelve stay a fixed curated strip by design.
Both reference apps separate the pool (everyone on the service) from your people — by opposite philosophies, studied. WhatsApp: the pool is every phone number on earth; "your people" is simply your address book projected onto it — no ceremony, no request, not even symmetric (A can hold B while B never held A); anyone who knows your number can message you cold, and safety lives downstream — block/report plus the privacy tiers (Everyone / My contacts / Nobody) that reference the contact list. WeChat: the pool is closed — no DM until a friend request with a note is accepted; the relationship is symmetric (deletion famously unilateral — you learn you were deleted only when a message bounces); every add-path is a permission switch (phone · ID · QR · group · name card), and meeting in a group does not make you contacts — adding from a group member list is still a request. Ours today: no friend layer at all — "shares a room with me" is the accidental proxy gating DMs (gap 9) and profile visibility (gap 11), and the gap 9 duo-exit work already hit the wall it creates: block needs a relationship model we don't have, and the DM gate is only as strong as the room-invite gate. Contacts are the durable answer: my people persist beyond any room, and strangers can't reach me uninvited.
| Dimension | Ours — proposed | ||
|---|---|---|---|
| The pool | every phone number, open | closed; findability per-switch ("Add me via") | invite-gated accounts (keep) |
| Becoming contacts | address-book sync — no ceremony, one-way is fine | request + note → accept; symmetric | request + note → accept, riding the notifications rail |
| Cold DM | allowed; block downstream | forbidden until accepted | today room-mates only; contacts add the durable, room-independent key |
| Add paths | knowing the number IS the path | ID/QR search · group member · name card | a room-mate's profile page · personal QR/link · username search behind a "findable" toggle, default off |
| Group ≠ friends | n/a (groups already have your number) | group chat ≠ contacts; adding from a group = still a request | same — sharing a room never auto-friends |
| Removing | delete from address book, silent | unilateral delete; the famous bounce reveal | unilateral remove, silent (the proven mercy) |
| What it unlocks | — (everything was already open) | DM · Moments · name card | DM beyond rooms · gap 11's "My contacts" privacy tier · block (the gap 9 v515 debt) · who-can-pull-me-into-rooms |
Pincus's bar: Better is objective — the existing audience cheers instantly. Not "our conversations are deeper."
| Current, real | Why it clears the bar |
|---|---|
| Instant web play — no download, no store, free | Pincus's own number: a required download loses 80–90% of the audience. Zynga Poker's Better, verbatim. The PWA is this strategy. |
| Fast on bad networks — anywhere | The local-first layer (persistent cache · send outbox · delta sync) is the WhatsApp bad-network playbook, serving any flaky link in the world. Bonus, not the point: browser-direct STT + the SG paths mean it even works from inside China. |
| ~$0.0004 a round | The economics that make "free" sustainable — the enabler Better, like Flash-for-free enabled Zynga's no-download. |
| Potential next | The 10/10 test |
| First-token latency | ~3s staging cost is the accepted debt; sub-second first paint of the reply is the clearest "instantly better" left on the table. |
| Reply length discipline | The soft-length debt. "It answers like a person, not an essay" is the #1 cheer in every AI-chat review. |
| Zero-friction entry from a shared link | Tap a shared room link → talking, no signup wall. Account creation is the same cliff as a download. (Gate abuse via the inviter's cap, not the guest's patience.) |
| Serve the world from the edge | The single SG box was a China-first choice; US/EU users pay the distance on every round trip (the ~1.4s handshake in Perceived speed). Edge/multi-region is the world-audience Better. |
Live or in flight today: the multi-persona room itself · floor-producer orchestration · real-figure advisory seats · Seen · Vibes · the Studio machine · the manners steerings. That's the Tribe shape — every zone touched at once. Exactly one runs as Pincus prescribes: manners v2 — stamped per room at creation, immutable, console default, judged on a single metric (return rate). That's the template; the others get its discipline or wait their turn.
The backlog — candidates to isolate one at a time, after the core New has its number:
The caveat Pincus himself makes: the framework produces B+ products; magic is what gets you to A. The floor producer and the persona pipeline are our pursuit of magic — the taste layer that makes the table worth sitting at. Keep pursuing it. Just stop calling it Better, and stop letting it queue-jump the mechanics our audience already expects from any chat app they'd return to.