Propose ADR-0087: a visualizer is a document, not a component - #191
Open
jeffcrouse wants to merge 7 commits into
Open
Propose ADR-0087: a visualizer is a document, not a component#191jeffcrouse wants to merge 7 commits into
jeffcrouse wants to merge 7 commits into
Conversation
The plugin contract today is "a React component, and here are our four library versions" — React, THREE, @react-three/fiber and Drei on window.Familiar, with the bundle evaluated by new Function inside the host page. ADR-0063 is about to publish that contract to outside authors, after which it is very hard to retract. ADR-0034 point 1 already records that the shape was inherited rather than chosen: "adopted, not designed ... because two working samples already build to it, not because there is an ecosystem that a different choice would strand. There is not." The prompt was ADR-0065's seeded example crashing the first time it ever ran. lyric-pulse ends `})(window.Familiar.React)` and renders with jsxRuntime.jsxs, which React does not export — the automatic runtime is a different module. It draws divs with a glow. It is not 3D and needs no renderer, and it was broken by plumbing the contract obliged it to use. The objection that would sink an event model does not hold: audio already crosses a process boundary at 10 Hz and the page reconstructs 60 Hz from it (nativeAnalysisBuffers.ts:20). A postMessage hop into a child document is noise against that. So: a folder with an index.html, loaded in its own context, sent events. No shared libraries, no framework contract, crash isolation by construction rather than by error boundary. Three of five built-ins genuinely earn r3f, so point 7 lets a plugin opt into a vendored copy by URL — the plugin's choice, visible in its source, rather than a global the host injects. Recorded honestly: ADR-0067 and ADR-0068 were aimed at the wrong contract. Both are proposed, so nothing shipped unwinds, but the effort was spent. Point 6 also names the one thing this makes worse — serving a folder is a traversal surface that serving one known filename was not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two changes, both from Jeff's steer that a solid, simple contract matters more than saving any particular visualizer. Point 7 had let a plugin load a host-vendored THREE by URL. That hatch existed for exactly one reason — sparing the three r3f built-ins from each bundling 2.4 MB — and with those not treated as a constraint it has no other justification. A sharing mechanism is a version contract wearing a different hat. It moves to Alternatives as rejected, and point 7 now says there is no sharing of any kind. New point 8 answers the bundle-size worry the honest way: ship fewer visualizers. Point 2 was vague where it most needed not to be — "events on the plugin's own window" is not a contract anyone can build against. It now names all four: familiar:track, familiar:audio, familiar:state inbound, familiar:ready outbound. And states the property that makes it simple — a plugin is obliged to receive, never to implement, so a document that draws nothing is still valid. Recorded in Consequences that losing the current visualizers was chosen rather than overlooked, so nobody later reads it as a regression to be fixed by reintroducing shared libraries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Approved by Jeff. The plugin contract becomes a folder with an index.html, loaded in its own browsing context and sent four events. No React, no THREE, no @react-three/fiber, no Drei, and no sharing mechanism of any kind. ADR-0034's Status records the partial supersession the way ADR-0013 does for ADR-0050: points 1 and 3 only. Its point 2 — interpreted code evaluated in WebKit and never in the app's process — is untouched and better served by a document than by new Function. Points 4, 5, 7 and 8 stand. Two proposed ADRs are now aimed at the wrong contract and need revisiting before any more work goes into them: ADR-0067 shrinks to almost nothing once there are no libraries to expose, and ADR-0068's conversion of the built-ins to IIFE bundles is redirected. Neither has shipped. The immediate consequence is that ADR-0063 can publish a contract worth publishing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nt 1 An accepted ADR is a living record, so this adds the Implementation block: what landed for point 6, and the measurement that decided how point 1's "own browsing context" is built. The open question was whether an iframe could be sandbox="allow-scripts" — opaque origin, no ambient authority — while still loading its own CSS and JS over a custom scheme. A custom scheme plus an opaque origin is exactly where a surprise hides, and building the event bridge on an assumption would have been finding out late. A standalone WKWebView harness answered it: all subresources fetched, script ran, stylesheet applied, origin null, host DOM unreachable, postMessage working both ways. Two findings worth keeping. The control run could not reach the host DOM either, because `//host` and `//plugin` are already different origins under one scheme — so origin separation and the sandbox attribute stack rather than one carrying the load. And the residual: sandbox denies cookies and storage but not network, so a plugin can still fetch anywhere. The handler serves the document and can attach a CSP; that is named as the next decision rather than quietly assumed solved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ADR-0087 point 8 said the shipped set should be deliberately small — "one or two cheap ones" — because each self-contained document carries its own libraries. Asked which ones should ship, Jeff's answer was all five. That is the opposite of point 8, so it supersedes rather than quietly diverging; the rule against editing an accepted Decision exists for exactly this. Point 8 optimised the wrong thing. It traded what a fresh install can do against megabytes, in an app that already ships a 3.2 MB visualizer document and whose library is measured in tens of gigabytes of audio. It also left two categories — shipped, and "demoted to a drop-in" — which is the distinction ADR-0068 was written to remove and which point 7's "one shape, not two" is otherwise careful about. So: all five convert, all five ship, and the duplicated THREE is paid deliberately rather than engineered around. Paying it in disk is the point of having refused a sharing mechanism — a shared library is a version contract, and this is only bytes. Point 4 notes that per-plugin builds may tree-shake better than one bundle sized for all of them, with a follow-up to strike that optimism if measuring shows it does not hold. This also closes the question point 7 deliberately left open, so the conversion work is now fully specified: five folders, then the registry can go. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"All five existing visualizers" reads as including music video, which is wrong and is a mistake worth making impossible rather than correcting again. ADR-0066 point 2 already removes music-video from the registry — "Four built-ins remain: reactive-terrain, beat-tiles, lyrics and lyric-storm" — and ADR-0085 makes it a native Mac player mode. So the conversion set is those four plus spectrum. The reason it is spelled out rather than left to inference: the registry in the code still lists music-video, because the ADRs removing it have not landed. Anything counting the current state gets five and includes the one that is leaving. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both were proposed against the component contract ADR-0087 deletes, so neither can be accepted as written and leaving them open invites work against a surface that no longer exists. ADR-0067 decided what to expose on window.Familiar. There is no window.Familiar: a visualizer brings its own libraries, so there is nothing left to choose the contents of. ADR-0068 would have built the four built-ins as IIFE bundles against those globals. ADR-0088 reaches its destination — one shape for shipped and drop-in alike — by making both a folder with an index.html. Rejected rather than superseded-as-accepted, since neither was ever accepted, and each records what survived it. ADR-0067's observation that Music Video was the only built-in needing playerStore and an API client was evidence the component contract was wrong, and ADR-0087 cites it. ADR-0068's point 3 — two builds of one visualizer drift, and nothing in CI can see it — is the argument ADR-0088 used to refuse a small shipped set with the rest demoted, because two categories drift the same way. Its point 2, letting a local plugin shadow a built-in, is inherited for free: with one shape there is nothing special about a shipped id. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
One ADR,
proposed. No code.The plugin contract today is "a visualizer is a React component, and here are our four library versions" —
React,THREE,@react-three/fiberandDreionwindow.Familiar, with the bundle evaluated bynew Functioninside the host page. ADR-0063 is about to publish that outward, after which it is very hard to retract. That timing is why this is worth deciding now.The shape was inherited, not chosen
ADR-0034 point 1 says so itself:
What prompted it
ADR-0065's seeded example
lyric-pulsecrashed the first time it ever ran, on a freshly built app. Its bundle ends})(window.Familiar.React)and renders withjsxRuntime.jsxs(...)— but React does not exportjsxs; the automatic runtime is a different module. Verified against the installed React 19.2.4:It draws divs with a glow. Not 3D, no THREE, no renderer needed. It was broken by plumbing the contract obliged it to use. That is the shape of the problem, not an unlucky bug.
The decision
A visualizer is a folder with an
index.html, loaded in its own browsing context, sent events. The whole API is four names:familiar:track,familiar:audio,familiar:stateinbound,familiar:readyoutbound. No shared libraries, no framework, nowindow.Familiar.The objection that would sink an event model does not hold. Audio already crosses a process boundary at 10 Hz and the page reconstructs 60 Hz from it (
nativeAnalysisBuffers.ts:20). ApostMessagehop into a child document is noise against that.What this costs, stated rather than buried
proposed, so nothing shipped unwinds — but the effort was spent, and 0068's conversion work is redirected rather than executed.If this is rejected
The two-line fix — add a JSX runtime to the globals — should be applied anyway. A seeded example that crashes on first run is worse than shipping no examples.
🤖 Generated with Claude Code