The adoption plan, both waves: close the loop, and mark the doors - #31
Merged
Merged
Conversation
The last unlinked step in the loop. Someone scans a QR, views a stranger's profile, hatches, answers, compares — and the person who shared the phrase still has nothing. No phrase, no creature, nothing their menagerie can refresh. The mechanism to fix that already existed and was already the app's own designed escalation: a boop carrying a view phrase is exactly "here is my creature back". What was missing was the offer, at the one moment the product has just proved itself to the person being asked. So this is a panel, not a feature. It hosts the existing composer and adds framing; the de-anonymization ladder is untouched, and attaching the phrase stays a tick the person makes. The copy is conditional on purpose. This app cannot know whether the other person already holds your phrase — they may have been handed it in the same conversation — so it must not diagnose their state and must never imply they are waiting or watching. It says how comparisons work and offers the means; a spec bans the vocabulary of surveillance from it. Six gates, each one a dead end somebody would otherwise hit: a three-way comparison has no single "them"; a group snapshot carries no boop reachability by design; a profile older than boops has none either; a comparison you are not part of is not yours to answer; logged out there is no creature to send; and a creature you already booped is not re-offered. The demo is excluded structurally rather than by a flag — it renders the panels component directly, and this lives one level up in the compare page. Two things the plan did not predict: - It wanted a `suggestAttachView` input on the composer. An input that pre-ticks would break the rule the item itself states, and one that merely draws attention is mechanism for nothing. The panel's copy names the tick instead: guidance with no code. - The "already sent" gate cannot read the ledger live. `prepareBoop` records the sent boop when the composer OPENS, not when it sends, so the obvious implementation made the panel delete itself the instant anyone used it — taking their half-written boop and their "Booped!" confirmation with it. The e2e found it; no unit test would have, because it needs a real click. It is a linkedSignal keyed on the pair now, and the `untracked` read inside is load-bearing: a linkedSignal computation tracks everything it reads, so a plain read puts the bug straight back. Also fixes an i18n miss from the template sweep: the composer's opening blurb was a two-branch ternary inside an interpolation spanning lines, which the sweep's text-node pass could not see. It is @if/@else with marked spans now, which brings the template catalogue to 356. Ladder green including e2e, which walks the whole path: A compares against B, is offered the panel, ticks the attachment itself, sends, sees the offer stand down, and B's inbox gains a boop carrying a link to A's profile.
…pty states Four small front-of-funnel items, all copy except the card. E2, the link preview. For most second people the first sight of Menagerie is an unfurl in a chat app, and it was a bare URL. The card is deliberately generic — it names the product, never the sender — because a #/view/<phrase> link and a bare link must be indistinguishable in a preview that a whole group chat can read. Hash routing makes that the only possible answer anyway: the fragment never reaches a server, so no crawler can ever see whose profile a link is for. `npm run social-card` renders it through the pinned Chromium the e2e already uses, from the real sprites, so it cannot drift into showing creatures the app does not have. One thing the plan missed: og:image must be absolute or most unfurlers ignore it, and the source cannot hardcode a host because anyone may self-host this bundle. So the deploy stamps it from a repo variable, exactly as it already does for the profile-server URL, and an unset variable degrades the preview rather than failing the build. E3, the cost. The core set is about five minutes and nothing said so; people commit to bounded tasks. Stated on the two surfaces where someone decides — the demo's hatch CTA and the core marker — as a fact, with no timer and no percentage. E4, the compare page's empty state. Compare-with-nothing is where a curious person lands after hatching alone, and it was an input with a placeholder. It now teaches the two moves that exist from exactly there: logged out, see the worked example; logged in, share your phrase and paste theirs when they're done. E5, the install. The PWA has worked since wave 5 and nothing mentioned it. One line in Settings, where people who already trust the app are, and it says what the cache actually holds rather than promising offline access to profiles it can never have. Two things fell out: - Another i18n miss, same shape as the last one: the core marker was a multi-line ternary inside an interpolation, invisible to the D3 sweep's text-node pass. Two marked branches now — 361 template messages. - The unfurl e2e has to fetch the card with a plain GET rather than navigating to it. A service worker answers every navigation with the app shell, which is correct under hash routing and exactly what a crawler will never do. Modelling the real client is what makes the check mean anything. Ladder green including e2e, which now guards the tags, the sender-anonymity of their text, and that the image is a real PNG in the build.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
All five items of
docs/adoption-plan.md. No new cryptography, no server changes, no schema changes.E1 — send your creature back
The last unlinked step in the loop. Someone scans a QR, views a stranger's profile, hatches, answers, compares — and the person who shared the phrase still has nothing. No phrase, no creature, nothing their menagerie can refresh.
The mechanism already existed and was already the app's own designed escalation: a boop carrying a view phrase is exactly "here is my creature back." What was missing was the offer, at the one moment the product has just proved itself to the person being asked. So this is a panel hosting the existing composer, not a feature — the de-anonymization ladder is untouched and attaching the phrase stays a tick the person makes.
The copy is conditional on purpose. This app cannot know whether the other person already holds your phrase, so it must not diagnose their state and must never imply they are waiting or watching. A spec bans the vocabulary of surveillance from it.
Six gates, each a dead end somebody would otherwise hit: a three-way comparison has no single "them"; a group snapshot carries no boop reachability by design; a profile older than boops has none either; a comparison you are not part of is not yours to answer; logged out there is no creature to send; and a creature you already booped is not re-offered. The demo is excluded structurally rather than by a flag.
Two deviations the code argued for:
suggestAttachViewinput. One that pre-ticks would break the rule the item itself states; one that merely draws attention is mechanism for nothing. The panel's copy names the tick instead.prepareBooprecords the sent boop when the composer opens, not when it sends — so the obvious implementation made the panel delete itself the instant anyone used it, taking their half-written boop and their "Booped!" confirmation. The e2e found this; no unit test would have, because it needs a real click. It is alinkedSignalkeyed on the pair now, and theuntrackedread inside is load-bearing: alinkedSignalcomputation tracks everything it reads, so a plain read puts the bug straight back.E2–E5 — the door
#/view/<phrase>link and a bare link must be indistinguishable in a preview a whole group chat can read.npm run social-cardrenders it through the pinned Chromium from the real sprites.og:imagemust be absolute or unfurlers ignore it, and the source can't hardcode a host, so the deploy stamps it from a repo variable exactly as it does the server URL — unset degrades the preview rather than failing the build.Also
Two more i18n misses of the same shape — multi-line ternaries inside interpolations, invisible to the D3 sweep's text-node pass. The template catalogue is 361 messages.
Verification
Full ladder green — format, both typechecks, all four unit suites, production build, e2e. New coverage: nine unit tests on the share-back gates including the ledger latch, an e2e walking the whole path (A compares, is offered, ticks the attachment itself, sends, sees the offer stand down, B's inbox gains a link to A's profile), and an e2e guarding the preview tags, their sender-anonymity, and that the image is a real PNG in the build.
The one thing neither wave could do for itself is look at the card — it's at
public/social-card.png.🤖 Generated with Claude Code
https://claude.ai/code/session_01UaDanzXTm6kVSgbNh1eNrs
Generated by Claude Code