Strip the browser's detail routes (ADR-0057) - #167
Merged
Conversation
ADR-0039 point 4 asked for marks that are theme-aware "so the site can have a dark mode without a second set of files", which in practice meant `currentColor` and no hardcoded colour anywhere. **The premise was never true.** `site/assets/site.css` has no `prefers-color-scheme` block and no theme toggle; neither does the page. Familiar is dark everywhere — Mac, phone, web — and nothing planned would change that. The requirement was insuring against a mode nobody intends to build. The premium on that insurance was the whole visual register. A `currentColor` mark is monochrome by construction, which is right for an icon and wrong for the "black cats, crows, witchy-but-playful" the same point asks for. Point 4 was asking for two incompatible things and getting the icon — the reference mark drawn under the constraint proved it: competent, and not charming. It also closed off the practical route to producing them, since generated illustration is raster and raster cannot inherit a colour. So: colour is allowed, no light-mode variants, no second set. What survives is the part that was load-bearing — one named set, drawn once, used consistently — plus a new point that the set must commit to *one* register, because colour illustrations beside monochrome stroked icons is point 4's failure mode arriving by a different route. Recorded rather than quietly changed, per the convention: this reverses a clause an accepted ADR decided, so it supersedes that clause and says which. The tradeoff is recorded honestly too — `currentColor` was never only about themes, and losing it means a mark can no longer sit in a heading and a muted footnote from one file. Status: proposed. The brief and the sprite's comment are updated to match. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
It was a spec — register options, palette table, naming conventions, checks. Useful to me when files come back, useless to paste. Now it is fifteen prompts and nothing else. Each is self-contained and repeats the style block, which reads as redundant and is the point: a generator drifts within a few images once the description leaves the context window, and drift across the set is the one failure ADR-0039 point 4 actually cares about. The register is decided rather than offered, because a document of choices is not a document of prompts. Flat vector art in the site's own palette — violet outlines, near-black fills, pale green for anything that reads as live — which is register B from the old brief, produced by raster generation. It keeps the marks belonging to the page rather than sitting on it. Two instructions carry more weight than the wording and are in all fifteen: transparent background, because the site's panels are #18181b on a #0a0a0a page and a baked background shows its own rectangle on one of them; and legibility at 40 pixels, which is the actual size on a feature card. Ends with the four ways a result drifts and what to add when it does, plus the one tactic that matters: get `cat` right first, then attach it to every later prompt. Matching a real image holds a set together far better than matching a description. Verified by parsing the file rather than reading it: fifteen prompts, every one carrying the transparency instruction, the flat-colour instruction, the 40-pixel legibility floor and the palette. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
Fourteen of the first fifteen illustrations came back sitting on a grey Photoshop checkerboard. The prompts asked for a transparent background, and that request cannot be honoured: Gemini returns JPEG, JPEG has no alpha channel. Rather than refuse, the model drew the checkerboard — it is the visual signifier of transparency, so it renders it as pixels. Asking more firmly cannot fix a format that has nowhere to put the answer. So the prompts now ask for flat cyan. No mark uses cyan, and its nearest neighbour in the palette is the green accent at ~174 in RGB, which leaves ample room to key against. `extract-marks.py` does the cutting, reading the background off the image so the fourteen already generated are rescued rather than regenerated — they are a good set, and a background is no reason to throw them away. Two things it does that a corner flood-fill cannot. It keys on colour, so fully enclosed background survives nothing: the gap between the crow's legs is background, and no fill starting at a corner ever reaches it. And for checkerboards it leans on the fact that only ONE of the two tones is ambiguous — the dark square is a mid-grey and no mark uses mid-grey, so every dark pixel is certainly background, while a light pixel is background only when connected to a dark one. That is what keeps the key's off-white body and the moons, whose own fill is 250 against a checkerboard white of 253; JPEG will not hold a 5-value gap, and colour matching alone ate them. Two measured details drove the rest. Tones are clustered rather than quantised, because quantising splits one tone across two buckets whenever it straddles an edge (239 and 240 are the same grey), which then forces a merge threshold wide enough to swallow the real second tone — the first version read every checkerboard as solid. And the output is quantised to 16 colours: the art is flat, but JPEG mottling inside the fills is what stopped PNG compressing it, at 167 KB for a mark that is really five colours. All fifteen now come to 208 KB rather than 2.4 MB. The source JPEGs move out of site/ and out of git. `cp -R site/. _site/` ships whatever is in that directory, so 28 MB of intermediates would have gone to Cloudflare; the same copy was also publishing the prompts document and now the script, so the assemble step removes them. Verified by compositing all fifteen on the page's own #0a0a0a and at 40px, which is the size a feature card actually uses. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
Two problems with the raster cut, both raised looking at the results. The first: bits of outline were missing from the marks worth keeping. That was the anti-halo erosion — two pixels off every edge, which a 30px outline never notices and a 10px one does not survive. Measured across the set it removed 17% of the constellation's artwork, 6% of the key's and 5% of the tuning fork's. A tracer follows colour regions instead, so a thin stroke becomes a path rather than a casualty. The second: outline weight ranged from 10px to 54px against a median of 30, because the set was generated a prompt at a time. In vector that becomes adjustable — the violet band is grown or shrunk to a single target before tracing, and the set now reads as a set. Getting there took four wrong diagnoses, and the notes in the file are mostly there so the next person skips them. The ragged edges were not JPEG noise (more smoothing changed nothing), not Lanczos ringing, not Floyd-Steinberg (FASTOCTREE ignores the flag), and not path precision — every trace was clean until the background paths came out. Looking at the actual pre-trace image rather than reasoning about it showed the cause in one glance: the outline narrowing hands each vacated pixel the colour of its nearest non-violet neighbour, and quantising had left stray sentinel pixels along interior boundaries. Each one became nearest-neighbour for a wedge of the band and got smeared down it, combing every outline with magenta teeth a dozen pixels long. Excluding the sentinel as a refill source removes them, and the eye went from 105 KB to 28 KB because the teeth *were* most of the path data. Three smaller findings kept as constants rather than comments. The fill black is #0f0e14, not the page's #0a0a0a — unifying to the page colour made the cat, the crow and the two cats read as hollow outlines. The snap radii differ, 70 for violet and 30 for black, because 60 reached far enough to swallow the shaded end of the cat-ear's rose gradient and scatter black speckles through it. And accents are left exactly as drawn: snapping every pixel to the site palette turned the cat's pale green eyes rose, pale green being nearer #c07f8a than #22c55e in RGB. Fifteen scalable marks, 668 KB, outlines within 26-32px of the 30px target. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
The first set is discarded. The problem was not extraction, it was the prompts: every one of the fifteen asked for "thick confident outlines in warm violet", so every image came back as a vector sticker. No amount of post-processing undoes a style instruction. The new register is the Welcome to Night Vale tour poster — hand-inked line of varying weight, shading built from crosshatch and stipple, three inks on a black ground. Chosen over that show's other look, the flat vector novel cover, in the knowledge that hatching turns to mud below about 80px: the page will want a few large illustrations rather than a grid of small icons, and the brief now says so rather than leaving it to be discovered. Twelve prompts instead of fifteen, leading with characters — a cat and a crow that recur — because a small recurring cast carries more identity than a scatter of objects. One of them is a model sheet: the same cat four times in a single image, so consistency across poses is structural instead of hoped for across separate generations. **Black background, and it is not a compromise.** Over a black ground a pixel is its own colour multiplied by its coverage, so coverage is just how bright it got, and dividing that back out recovers the ink exactly. `extract-marks.py` gains that mode and picks it automatically. Verified against a synthetic target: one-pixel strokes survive with correct partial alpha, colours are preserved, and there is no halo on a lighter panel. That also answers the concern that keying would suck the black out of the middle of a drawing. In this register there is no black in the middle to lose — the darkness is the ground, and letting the page show through it is what the style wants. Two smaller things. The colour-key path now mattes its edges instead of cutting hard and eroding two pixels to hide the fringe, which is what destroyed 17% of the constellation. And there is no eye prompt: the eye-and-crescent is Night Vale's actual logo, so the register is borrowed and the mark is not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
The previous commit argued that a model sheet is the only reliable way to keep a recurring character consistent, and then, two sections later, gave the cat three separate pose prompts and the crow two. Jeff caught it. The prompts now do what the reasoning said. Six character prompts become two: one cat sheet and one crow sheet, each four poses in a single generation, because poses drawn together agree by construction and poses drawn separately only agree by luck. `cat-and-crow` stays a single image — it needs both characters in one composition, which no single-character sheet can produce — and it takes an approved pose from each sheet as its reference. The workflow was also unsupported by the tooling, which is the more useful half of the catch: `extract-marks.py` wrote one file per input, so a sheet of four poses came out as one image with four cats in it. It now takes `--split RxC` and carves the sheet at its emptiest rows and columns, ranking runs of empty by length so a real gutter is picked over the incidental gap between a cat's ear and its tail, and ignoring the frame's own margin, which is longer than any gutter. Verified on a synthetic 2x2: four poses out, each cropped to its own content, colours and partial alpha intact. Eight prompts producing eleven marks, against twelve prompts producing twelve. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
Fourteen marks: two model sheets of four poses each, the cat-and-crow hero, and five objects. The register landed — hand-inked line, crosshatch shading, magenta and violet on black, and no outline anywhere. Two things broke on contact with real sheets. The splitter cut at empty rows and columns, and the cat sheet has no empty row: the walking cat's tail and the stretching cat's tail both cross the horizontal middle, so it found one column gutter and wrote two images with two cats each. Nothing is wrong with that drawing — poses simply are not boxed, and the brief asking for "a wide band of clear black space" was asking the artwork to accommodate the tool. It now groups the drawing's connected parts by where their centres of mass fall, which separates poses whose extents overlap, keeps a detached whisker with the pose it belongs to, and masks anything reaching in from a neighbour before cropping. Eight poses out, each tight to its own content. And the output is WebP lossless rather than PNG. The hatching lives in the alpha channel, and 8-bit alpha full of cross-hatch barely compresses in PNG — half a megabyte for one mark. Lossy WebP does not help, because it stores alpha losslessly whatever the quality; only WebP's lossless mode, which has a real alpha codec, does, at about half the size. Flattening the ink to one colour was measured and rejected: hue is stable across the set (sd ~10) but saturation is not (sd ~60), and that variance is the pale-highlight-against -body reading. It would have saved 25% and cost the thing the style is made of. The matte needed no adjustment. Checked against #18181b and #27272a panels: no halo, and a cat's dark interior shows the panel through it, which is what a light-on-dark register should do. Still 2.8 MB for fourteen, so the brief now says to pick the few the page uses rather than ship the set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
The hero takes cat-and-crow, and the cauldron, lantern and curled cat sit beside the headings of Ask, Install and Get started. Decorative, so `aria-hidden` and no alt text — the heading beside each one is the accessible name, and describing it twice only makes a screen reader slower. Hidden below 760px rather than shrunk, since a hatched drawing at thumbnail size is texture, not a picture. The inline symbol sheet goes with them. `mark-cat` was the reference drawn under the constraint ADR-0054 removed, it was used exactly once, and a stroked monochrome icon beside an inked illustration is the mixed set that ADR-0054 point 4 exists to prevent. `marks.svg` and the `.mark` rule follow it out. `#intro` moved under the hero. The three sentences that say what Familiar is were sitting between the FAQ and Install, about 1,400 words down, and in no navigation. **ADR-0055 proposes the restructure Jeff asked for**, because reorganising the site is what ADR-0039 decided and not something to quietly redo. Its useful finding is that 0039 point 2 described its own plan as "taking Fork's architecture: ... a comparison table, FAQ" — and Fork has neither. What Fork does have, and 0039 skipped, is a feature section carrying its own screenshot; Familiar pooled sixteen screenshots into one grid and listed fourteen features as text with no pictures. It took the parts of Fork that are easy to write and left the part that does the work. The claims were audited against the repo rather than trusted, and they mostly hold — listening sessions, music videos, community cache, Mood Grid, 3D Explorer, smart playlists and MCP all exist. I briefly had music videos as removed; that was a bad grep, and the correction is recorded in the ADR so it is not re-derived. What has drifted is the pictures: fifteen of the twenty screenshots predate May, so 0039 point 5's "just re-run the spec" held as policy and failed as practice for four months. The density point is written as a budget — six claims, under 700 words, against fourteen and 1,778 — because "simplify" is not a decision. Every sentence on that page is individually defensible, which is how it reached eleven sections. Also recorded as a follow-up, found while screenshotting and unrelated to any of this: the page overflows horizontally at 420px, cutting off both the nav's Install button and the body copy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
Jeff's call, and it splits what ADR-0055 had treated as one problem. The comparison table and the FAQ are not the same kind of thing: the table is an argument, answering "why not Jellyfin, why not Plex" for a reader who is holding that question right now, so it earns a place in front of them. The eight FAQ entries answer someone who has already decided to care and is checking specifics — that reader follows a link, and the entries were costing a third of the page to serve them. So `site/faq.html`, linked from the nav and both footers. The landing page drops from 1,778 words to 1,314 and from eleven sections to ten. ADR-0055's budget is 700, so this is a quarter of the way, not the job. The cross-page links were the part with teeth. Two answers link to `#install` and `#remote-access`, which are now on a different document, and `privacy.html`'s nav pointed at `./#faq`, an anchor that no longer exists anywhere. All three were dead the moment the section moved, and none of them would have looked broken — a dangling fragment just lands you at the top of the page. Every internal anchor across the three pages is now checked against the ids that actually exist, and they all resolve. The FAQ page's illustration sits beside its title rather than above it. Stacked, the way the landing hero does it, the first question started most of a screen down — fine when the picture is the point, wrong when the reader came with a question. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
Green was the title, every link, the version chip, the requirements rule, the step badges, the disclosure chevrons, the comparison table's affirmatives *and* the install button. When everything is the accent, nothing is, and the one thing worth clicking looked like the rest of the page. So purple takes the voice and green keeps the job. The split is a rule rather than taste: green means "act on this" or "yes" — the install button, the primary buttons, the .yes cells, the live dot — and everything else follows the illustrations. The measurement is what settled it. The marks sit at hue 288deg; `--accent-purple`, already defined in the stylesheet and used for nothing but the arrows in the prompt cards, is at 271deg. The green is at 142deg, which is 146deg from the artwork — close enough to complementary to read as a clash and not close enough to be one. All-purple was mocked up and rejected: the app icon is a green waveform, it ships on the App Store, and with no other green on the page it reads as a foreign object rather than the product's mark. Two things the change turned up. The hero title is a gradient, not a flat colour, so it needed a real edit rather than a variable swap. And the closing call to action had no button styling at all — `.nav-links a.primary` was the only rule, so the repeated "Install the server" that ADR-0039 point 2 added on purpose was rendering as a bare text link. It now shares the primary style, which is the whole point of repeating it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
ADR-0055 points 3 and 4. Fourteen features listed as text and sixteen thumbnails in a wall become six claims, each with a picture of itself, plus a compact list of the other nine. The grid was a gallery rather than an argument: a reader met sixteen pictures at once, in no order, before being told what any of them were for. Now each screenshot appears beside the sentence it supports, which also forces the question of whether a claim deserves a picture at all. **Only screenshots taken this week are on the page.** Fifteen of the twenty in `screenshots/` predate May, and the grid was showing all of them; the four that survive are the Mac ones from today. Where there is no current screenshot the illustration carries the section, which is honest about showing a feeling rather than a feature — and the illustrations are deliberately smaller than the screenshots, because a drawing is atmosphere and giving it the weight of evidence overstates it. Two things found while rewriting. The iOS entry described the app as a "native Capacitor wrapper", which has been wrong since `packages/ios` was deleted on 2026-08-11 — it is native Swift now, and the site has been claiming otherwise for two days. And removing `#screenshots` orphaned the nav links to it on both the landing page and the FAQ page; the anchor check caught the second one, which is exactly the silent-fragment failure that made the check worth writing. Word count is honest: 1,356 against a 700 budget, and points 3 and 4 barely moved it — six paragraphs that say something cost about what fourteen blurbs cost. What they bought is structure: eleven sections to nine, thirty-six screenshot references to eight. Getting to 700 means a section leaving, which is point 2 and a separate call. `site/screenshots` is gitignored. The deploy copies `screenshots/` into `_site` itself, but a local `http.server` in `site/` cannot resolve them without it — which is why the feature screenshots rendered as broken-image icons the first time this was reviewed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
Jeff asked whether the site's purpose was ever decided. It was not: ADR-0039 decided its layout, ADR-0055 decided its structure, and neither said who the site is for or what it may claim. That is the mechanism behind the drift — with no rule for what belongs, every true thing gets added and nothing is removed when it stops being true. So ADR-0055 gains two points, purpose and accuracy, and they go first because everything else is downstream. The audit checked claims against the repository rather than trusting them. Two were wrong. The version chip read v0.1.0-alpha2 · 2026-04-17; CHANGELOG.md says 0.2.0-alpha1, released 2026-08-10. Four months and a minor version. The serious one is the privacy policy, which told readers that "when you use the AI chat feature, your prompt and conversation context are sent to Anthropic". There is no chat feature — `components/Chat/` and `api/chat.ts` are both gone — and under MCP the claim is false in a specific way: Familiar holds no key and calls no model, so nothing goes from Familiar to a provider at all. The host does the calling, on the reader's own account. It now says that, and says what does cross the connection: tool-call answers, never audio. A privacy page is where a stale claim costs the most, because it is read precisely by the person deciding whether to trust the thing. Not everything was wrong, and the ADR records that too so it is not "fixed" later. "36 tools" is right — 34 in MUSIC_TOOLS plus list_players and now_playing. CLAP is 512-dimensional. The demo and App Store links both resolve. And the PWA is still real, against Jeff's recollection: vite-plugin-pwa is configured, public/manifest.json exists, main.tsx registers /sw.js. Recorded as a fact rather than acted on, because if it leaves the page it should leave as a positioning decision now that there are native apps — not as a correction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
The site still sold the browser as a way to listen: an "Offline PWA" feature promising home-screen install and offline caching, a comparison row claiming "Mobile PWA", and a remote-access section justified by the PWA install prompt. ADR-0050 was accepted on 2026-08-11 and says the opposite — the web app is a management surface, the listening path belongs to the native clients, and new listening features do not come back to the browser. This is not a new decision, which is the point. The direction was decided, accepted and shipped two days ago, and the site never caught up. Nothing here needed an ADR; it needed someone to read the one that exists. So the sixth feature block becomes the apps rather than "your library, properly browsable" — listening happens in real apps, and the browser is where you set Familiar up and look after the library. Every PWA reference is gone: zero left across all three pages. One row was removed rather than rewritten. I had replaced "Mobile PWA" with "Native Mac & iOS apps" and marked Jellyfin as third-party, which is wrong — Jellyfin ships official mobile apps — and I cannot verify competitor app support from this repository, which ADR-0055 point 2 now forbids me from pretending otherwise about. A row where every column answers Yes also differentiates nothing, so it earns no space. Deliberately not claimed: that the web app is settings-only today. ADR-0050 point 6 is explicit that "settings only" is not reachable until the Apple clients can edit playlists, edit metadata, trigger a scan and create a profile, and `docs/WEB-PARITY.md` still shows playlist reorder as browser-only. The site now describes the browser's job as setup and library care, which is true today and stays true when the last routes retire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
The first audit failed and Jeff caught it. It passed "Music videos — attach video files to tracks and play them in the full player" on the strength of a grep that returned routes/videos.py. A filename is not a feature. Five failures, all method rather than oversight. It checked that code exists rather than that a capability does. It never opened docs/WEB-PARITY.md, which is a maintained per-surface matrix answering exactly the question being asked, and which ADR-0050 point 6 names as the reference. It audited the wrong surface — the site promises native apps, and `videos` is one of 22 API tags absent from the Swift client, so it is browser-only by construction. It matched the topic rather than the assertion: the real feature is a visualizer that searches YouTube. And it left no artefact, so "broadly sound" was generalised from a handful of positives and nothing could be re-run. The sweep found four more. **Mood Grid and 3D Explorer do not exist anywhere.** No component in the repository; the routes survive only as icon entries in `Sidebar.tsx:42,44`, absent from both `LIBRARY_ITEMS` and `BROWSER_ROUTES`, with no native equivalent. This is the affordance-with-no-destination shape the codebase already has a name for, and the site was selling both. **Semantic search has no UI on any client.** `semantic_search` is an MCP tool with a handler in `llm/handlers/search.py`; there is no REST endpoint. The headline feature block read as an in-app search box. It now names the host, and merges with the MCP block that was making the same point — six feature blocks become five. **Listening sessions is browser-only by decision** (ADR-0037 rejected), and music videos are browser-only by construction. Both removed, Jeff's call: they sat two sections below a block saying the browser is not where you listen. Everything else checked out and is recorded so it does not get "fixed" later — 36 tools, the feature columns, the community cache, Music Map on the Mac, offline downloads. `docs/SITE-CLAIMS.md` is the ledger: every claim, its verdict, its evidence, the date. `site/scripts/check-claims.py` re-runs the part that rots on its own — version against CHANGELOG, links, screenshot age, retired terms, dangling anchors. It caught one thing immediately, and it was its own bug: the demo answers HEAD with 405 while serving fine, so it now falls back to GET rather than crying wolf about its method. 28 competitor assertions in the comparison table are marked unverifiable and left on the page pending Jeff's review, as agreed. The ledger is the worklist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
Jeff caught the demo link and named the real problem: I had been correcting sentences one at a time instead of asking what the site should say. The page still described a product where the browser is the client and an AI chat is the hook, and both stopped being true months ago. **The demo link is the clearest case and it is gone.** ADR-0038's opening line is "Apple App Store review requires a working backend server with test data" — the instance exists so a reviewer has something to type into the setup screen. ADR-0039 point 8 put it on the site as "try the live demo", turning a compliance fixture into a product tour. There is no way to try Familiar without your own server: `SetupView.swift` asks for an address on first launch and the string `familiar-demo` appears nowhere in the Swift. That also makes `faq.html`'s "first launch connects to a public demo backend" false, and it is corrected. **The best thing Familiar does was not on the page at all.** ADR-0048's seeded playlists — build a playlist from any track, album, artist or selection, scored from the library's own analysis, with no English sentence constructed in any client — is a better headline than "connect an MCP host", because it works whether or not a language model exists. It now leads. It is web-only today: `playlists_generate` is tagged `playlists` so it IS in the generated Swift client, and nothing calls it. Jeff's call to state the capability without naming a surface; the ledger records the gap and the native work is the agreed next task. The page is rebuilt around the actual journey — install the server, point it at your files in a browser, listen in the apps, connect an assistant if you want one. Nine sections become four plus the hero; 1,273 words become 758. `#use-case` restated the hero, `#remote-access` was install documentation that had grown into a peer section and is now step 4 of Install, `#feedback` was a GitHub link, and `#ask` survives as two prompt->filter examples inline in the MCP block — which is the proof it was providing, at a tenth of the words. A fourth claim is new and overdue: Familiar resolves canonical artists and albums, fetches artwork, and keeps your fixes across a re-scan (ADR-0051, ADR-0052). That is real work the site had never mentioned. `check-claims.py` earned itself immediately — the rewrite orphaned `./#ask` and `./#remote-access` on two other pages, which a dangling fragment never reports at runtime. 36 orphaned CSS rules went with the sections; no selector is now unused. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
The demo link was removed from the site with two pieces of evidence, and one of them stopped being true the same afternoon. "`familiar-demo` appears nowhere in the Swift" was accurate when checked; `familiar-apple` e78a44b now sets the macOS setup field to `https://familiar-demo.fly.dev`. iOS still starts empty. The removal stands, and on firmer ground than before: that prefill exists so an App Store reviewer has something to connect to, which is exactly what ADR-0038 says the instance is for. It is a filled-in text field, not a connection, and not a product tour. So the ledger now rests the decision on ADR-0038's reasoning rather than on the absence of a string, which is the part that could rot. The FAQ said the app "asks for your server's address the first time you open it". True of the phone, no longer true of the Mac, and a reader who opens the Mac app and finds someone else's server already typed in deserves to have been told why. It now says. Recorded rather than quietly fixed, because a one-day-old citation going stale is precisely the failure this ledger exists to catch — and it caught it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
ADR-0031 point 7 deferred casting on the phone and named the bar for revisiting it: "the ADR that revisits it should measure the drain rather than assume it". Jeff asked for it. This is that ADR, and its first job is to say that **the cost point 7 names is probably not the cost that matters.** Point 7 frames it as a full decode running silently and an audio session held for output nobody hears. Reading the implementation, the decode is the cheap half. `Casting.swift:322` mutes the local engine and leaves it running; `observe(_ player:)` derives every speaker command from the local timeline and nothing polls the device; and the speaker separately fetches its own audio through `device_stream_base_url`. So a casting phone would pull a second complete copy of the audio over the network, decode it, and throw it away — purely to learn when the track ends. On a phone the radio is the expensive part, and it scales with session length. Point 7 does not mention it. ADR-0031's alternatives rejected zones, AirPlay-only, fixing the subsystem first, deferring again, and both platforms at once. **Decoupling the timeline from the decoder is not among them** — it is unexamined rather than rejected. The client needs one fact from that stream: when to advance. Crossfade is already suppressed while casting (point 6), so sample-exact end-of-track is not required, which removes most of the reason to want a decoder at all. Two things the ADR is careful about, because both are places this could quietly overclaim. Nothing has been measured, and point 7 requires it. So the measurement is a decision point with a protocol rather than a footnote, the ADR says it should not be accepted until that has been run, and it names bytes transferred — not CPU — as the number that decides. And `PlayableTrack` carries no duration. I said in conversation that the client already knows how long a track is; that was wrong, and the ADR records it. Duration reaches the row menus and not the player's queue item, so a timer-driven advance needs the type to grow a field — optional, falling back to today's behaviour, so it is not a flag day. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
Point 5 asked for the drain to be measured on a casting phone. A phone cannot cast — that is what the ADR proposes — so the evidence required to accept it could only be gathered after the decision it was meant to inform. Circular, and it would have sat there unactionable. It measures on the Mac instead, which is not a compromise: the Mac casts today, pulls the same discarded stream from the same server, and the quantity in question is bytes over the wire, which does not care which machine asked for them. `nettop` against the app's pid, three readings on one track — playing locally, casting, idle. Stating the falsifier rather than only the expectation: the ADR claims casting costs about what playing locally costs, because it is a second full stream. If it measures near idle, the discarded fetch is not happening the way the ADR reads the code, and it should be rejected rather than reworded. The phone battery figure is still worth having, but it is downstream of the bytes rather than independent evidence, and it cannot be collected until there is something to collect it from. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
Jeff asked why I could not verify them online. There was no good answer. ADR-0055 point 2 said claims must be "checkable against this repository", and I let that framing turn "not in the repo" into "unverifiable" — which does not follow, and I had used web search twice earlier in the same session. Checked against each product's own documentation. Two cells were wrong, both in Familiar's favour, which is the direction that matters: **Plex was written off as "Basic" audio analysis.** It runs a neural network that places every track in an N-dimensional space and uses it for sonic similarity, track radio and generated mixes. That is the nearest competitor on the exact axis Familiar claims as its own, and the table understated it. Now "Sonic analysis (Plex Pass)". **Spotify was shown as unable to play your own files.** Local Files plays MP3/MP4/M4P and syncs to a phone over the same Wi-Fi. No FLAC and not remotely — which is a real distinction and a better argument than a dash. The site's own FAQ already described Local Files while the table contradicted it. Two smaller ones. Jellyfin smart playlists are a maintained community plugin rather than nothing. And "Semantic audio search" became "Search by describing the sound", because Plex's similarity is track-to-track and real; what Familiar does differently is text-to-audio, so the row now says that instead of implying nobody else analyses audio. The table carries the date it was checked and asks to be corrected. That is a promise that can be kept — not "this is current", but "this was true on a stated day". ADR-0055 point 2 now says `unverifiable` means nobody can check it, not that it sits outside this repository, and records why: a comparison table audited only against its own author's repo will drift towards flattering its author, which is worse than being out of date. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
`familiar-apple` #123. The row said `Web ✅ / Mac ✅ / iPhone ❌`, which was true of the whole capability and is now only true of half of it. Browsing and playing is on all three. Editing stays macOS-only — the "Create a smart playlist" and "Delete a smart playlist" rows below keep their ❌ for the phone, because ADR-0013 point 3 assigns that CRUD to the Mac and nothing here changes it. The note says which half moved rather than leaving a ✅ that overstates it. ADR-0050 point 6 makes this file the reference and says it is maintained, so it moves with the change rather than after it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
Jeff asked for this after the file produced a false statement in a source comment two repositories away — I wrote that `playlistsReorderPlaylistTracks` was generated and uncalled, citing this matrix, while `BrowseStores.reorder` has been calling it since #103. Eight rows were wrong, all in the same direction: native had more than this said. Playlist reorder, queue reorder and removal, track metadata editing, library scan, profile creation, playlist creation on the phone, removing a track from a playlist, and smart playlists. Every one shipped in a numbered PR and none of them updated the file. ADR-0050 point 6 calls this document the reference and says it is maintained; it was the reference and it was not maintained. **The consequence is larger than the rows.** Point 6 said "settings only" is not reachable until the Apple clients can edit playlists, edit track metadata, trigger a scan and create a profile. All four are done. The milestone the ADR was waiting for has arrived, and the file that was supposed to announce it said the opposite. Two structural corrections as well. `outputs` sat in the list of tags "not generated, so browser-only by construction" — nine of its operations are generated by name (ADR-0031), and the filter names operations rather than the tag precisely so the nine zone operations stay out. And the file implied the vendored `openapi.json` is filtered; it is not, it is the whole artifact copied verbatim, so counting its tags proves nothing about reach. I ran exactly that check during this pass and it briefly looked like the entire list was wrong. The method is written down now, because three false positives came from grepping for words: `WindowCapture.swift` matches "restore", `BeatGrid.swift` matches "diagnostics", `SmartPlaylistEditor.swift` matches "recentlyPlayed". Grep for the generated operation, then confirm a reachable caller. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
Jeff asked to start stripping the web app and, offered a scope, chose to settle the boundary first. That was the right call: the question turned out not to be which routes to delete. ADR-0050 point 3 recorded the browser's scope as a list of five keeps, and that list has now failed in both directions. `/playlists/:id` was kept "until the Apple clients can edit a playlist" — they can, on both platforms, and nothing happened when it became true, because a condition written into prose has nobody watching it. Meanwhile `/library/artists/:name` and `/library/albums/:artist/:album` are still mounted and appear in point 3 at all. They were not decided against; they were not decided, and a list says nothing about what it omits. The document that would have caught both was itself wrong. The re-verification found nine stale rows, every one understating native, each from a numbered PR that never updated the file — and ADR-0050 point 6's own "settings only" milestone had arrived unnoticed. So this replaces the inventory with the rule it was an instance of: the browser keeps a capability only while it has no native answer, with two stated exceptions — infrastructural work aimed at the server rather than at listening, and listen-together, which is web-only by decision since ADR-0037 was rejected. The contested part is point 3, and it is written as a decision rather than a discovery: the fallback player is a flat searchable track list, and browsing is not part of it. Artist and album drill-down go. The alternative is recorded honestly — from 26,000 tracks, clicking through to an album is a natural way to find something, and this makes the guest-machine case worse. Point 5 is the one that will earn its keep: a capability and its affordances leave in the same commit. Removing `/playlists/:id` alone would strand five "Make a playlist" buttons, a sidebar that lists playlists it cannot open, and mobile rows that do nothing — the defect this codebase has shipped four times. Checked rather than assumed: `embedBridge.ts` posts its `navigate` intent to the native side and names API paths, not web routes, so removing the drill-down does not touch the embedded Discover that both Apple clients render. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
3,702 lines out, 34 in. The first strip under the rule ADR-0057 replaced ADR-0050's list with: the browser keeps a capability only while it has no native answer. Three routes go — `/playlists/:id`, `/library/artists/:name`, `/library/albums/:artist/:album` — with the components behind them and, per point 5, everything that reached them in the same commit. That last part is most of the work and the reason the diff is not three files: five "Make a playlist" affordances, `useGeneratePlaylist`, the sidebar's playlist and smart-playlist sections, the mobile nav's playlist rows, two context menus, an edit modal, a row component, and the queries that fed them. **"Go to artist" survives as a filter rather than dying.** ADR-0057 point 3 makes the fallback player a flat searchable list, and a filtered flat list is still a flat list — `navigateToAlbum` already worked that way, so `navigateToArtist` now does too. Removing the menu item would have been the easy reading of the ADR and a worse one: finding an artist's tracks in a list of 26,000 is exactly what that player is for. Two things fell out that predate this ADR. `LegacyRedirect` — 82 lines migrating a hash and query-param URL scheme — pointed at `/home`, `/favorites`, `/downloads`, `/library/music-map`, `/library/discover` and `/library/proposed-changes`. **Not one of them has been a mounted route.** It was migrating old URLs to newer URLs that no longer exist, and its default landing was `/home`, which falls through to the catch-all. And `SmartPlaylistDetail`, 531 lines, whose route `/smart-playlists/:id` was never mounted either. The sidebar linked to it; nothing served it. Deleted here rather than left, though it was dead before this ADR and is not strictly in its scope. Checked rather than assumed, because ADR-0050 point 4's trap is exactly this: `EmbedDiscover` always supplies `onGoToArtist`, `DiscoverBrowser` is mounted only in the embed, and `embedBridge` posts to the native side naming API paths rather than web routes. Removing these routes cannot reach the embedded Discover both Apple clients render. Typecheck holds at the 14-error baseline, all 63 test files and 952 tests pass, and the web app builds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
This was referenced Aug 16, 2026
# Conflicts: # docs/WEB-PARITY.md
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.
3,702 lines out, 34 in. The first strip under ADR-0057's rule: the browser keeps a capability only while it has no native answer.
What goes
Three routes —
/playlists/:id,/library/artists/:name,/library/albums/:artist/:album— with the components behind them and, per point 5, everything that reached them in the same commit. That last part is most of the diff: five "Make a playlist" affordances,useGeneratePlaylist, the sidebar's playlist and smart-playlist sections, the mobile nav's playlist rows, two context menus, an edit modal, a row component, and the queries that fed them."Go to artist" survives as a filter
Point 3 makes the fallback player a flat searchable list — and a filtered flat list is still a flat list.
navigateToAlbumalready worked that way, sonavigateToArtistnow does too. Deleting the menu item would have been the easy reading of the ADR and a worse one: finding an artist's tracks among 26,000 is exactly what that player is for.Two things that predate this ADR
LegacyRedirect— 82 lines migrating a hash/query-param URL scheme — pointed at/home,/favorites,/downloads,/library/music-map,/library/discoverand/library/proposed-changes. Not one is a mounted route. It migrated old URLs to newer URLs that no longer exist.SmartPlaylistDetail, 531 lines, whose route/smart-playlists/:idwas never mounted. The sidebar linked to it; nothing served it. Deleted here, though it was dead before this ADR and isn't strictly in its scope.The embed check
ADR-0050 point 4's trap is exactly this, so it was checked rather than assumed:
EmbedDiscoveralways suppliesonGoToArtist,DiscoverBrowseris mounted only in the embed, andembedBridgeposts to the native side naming API paths rather than web routes. Removing these routes cannot reach the embedded Discover that both Apple clients render.Verification
packages/webbuildsdocs/WEB-PARITY.mdgains a "Removed from the browser" table recording where each capability went.🤖 Generated with Claude Code
https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW