Files
SoulStream/handoff/rounds/patch-round28.cjs
T
KeelrLeek 9f0601027c android: the handoff — the pending rounds, the pipeline, and what is unverified
Everything written this session that has not been through the device: rounds 43
to 48 (each patch adversarially verified, 43's transient-notice expectation
fixed), a partial round 49, the pipeline scripts, the Android Auto design and
the icon comparison mock. Also the two harness changes that were never A/B'd —
adaptive waits, which may only shorten a declared wait and never lengthen it,
and a ship that runs the mock walk and the deploy at the same time. The A/B and
its 255-step baseline are in handoff/tools; main stays at what the phone has
actually walked.
2026-09-14 07:57:41 -05:00

394 lines
29 KiB
JavaScript

// Round twenty-eight: what a screen reader says. No new Tauri command and no
// backend call at all, so src/lib/api.js and src/lib/mockLibrary.js are
// deliberately untouched — nothing here can warn about an unmocked command.
// Nothing visible changes either: every edit is an attribute, a role, one
// live region clipped to a pixel, and the probe that holds it all at zero.
const fs = require('fs');
const root = process.env.SS_ROOT || 'F:/Coding/SoulStream/';
const scratch = 'C:/Users/Kyle/AppData/Local/Temp/claude/F--Coding-SoulStream/5488514a-11e8-482a-b0a0-20cb511ee110/scratchpad/';
const ins = (name) => fs.readFileSync(scratch + name, 'utf8').replace(/\s+$/, '');
let p, s;
const load = (file) => { p = file; s = fs.readFileSync(root + p, 'utf8'); };
const save = () => fs.writeFileSync(root + p, s);
const once = (a) => {
const i = s.indexOf(a);
if (i < 0) throw new Error('missing in ' + p + ': ' + a.slice(0, 90));
if (s.indexOf(a, i + 1) >= 0) throw new Error('ambiguous in ' + p + ': ' + a.slice(0, 90));
return i;
};
const rep = (a, b) => { once(a); s = s.replace(a, () => b); };
const NL = String.fromCharCode(10);
/* Two mechanical sweeps below are regex replace-alls (aria-pressed on the
choice chips, aria-checked on the switch rows) and would happily apply
themselves a second time. Refuse a re-run before anything is written:
a11yAnon is created by this round and exists nowhere in the tree today. */
if (fs.readFileSync(root + 'tests/mobile/smoke.mjs', 'utf8').includes('a11yAnon')) {
throw new Error('round 28 is already applied in ' + root + ' — reset the tree rather than re-running');
}
/* the thirteen choice chips: one uniform line, so one regex — with the
count asserted first, so a chip added by a later round is noticed here
rather than quietly skipped */
const CHIP = /<button class="chip" class:on=\{([^}]*)\} class:quiet=\{[^}]*\} onclick=/g;
const chips = (expected) => {
const n = (s.match(CHIP) || []).length;
if (n !== expected) throw new Error(p + ': expected ' + expected + ' choice chips, found ' + n);
s = s.replace(CHIP, (m, on) => m.replace('onclick=', 'aria-pressed={' + on + '} onclick='));
};
// ── the shell: the nav is a tab set, the mini says what is playing, and
// one live region is mounted for the life of the app ────────────────
load('src/mobile/Mobile.svelte');
rep(` const played = $derived(player.duration ? Math.min(1, player.position / player.duration) : 0);`,
` const played = $derived(player.duration ? Math.min(1, player.position / player.duration) : 0);` + NL + ins('ins-r28-mobile-script.txt'));
rep(` <nav class="nav" class:gone={nav.sheet}>`,
` <!-- svelte-ignore a11y_no_noninteractive_element_to_interactive_role -- the
five destinations ARE a tab set, and the desktop's own segmented
control says so too; wrapping them in an inner div to keep <nav>'s
implicit role would change the bar's flex layout -->
<nav class="nav" class:gone={nav.sheet} role="tablist" aria-label="Sections" aria-hidden={nav.sheet ? "true" : undefined}>`);
rep(` <button class:on={nav.tab === id} onclick={() => go(id)}>`,
` <button class:on={nav.tab === id} onclick={() => go(id)} role="tab" aria-selected={nav.tab === id} aria-label={tabName(id, label)}>`);
/* the digits are in the tab's name now, so the dot itself is decoration */
rep(`{#if id === "you" && activeTransfers().length}<span class="nb">{activeTransfers().length}</span>{/if}`,
`{#if id === "you" && activeTransfers().length}<span class="nb" aria-hidden="true">{activeTransfers().length}</span>{/if}`);
rep(`{#if id === "community" && unreadTotal()}<span class="nb">{unreadTotal()}</span>{/if}`,
`{#if id === "community" && unreadTotal()}<span class="nb" aria-hidden="true">{unreadTotal()}</span>{/if}`);
/* the mini bar swallowed the title and the artist that are visibly in it;
and while a sheet is over it, it leaves the tree rather than sitting
off-screen in it. aria-hidden only, never inert — the walk clicks it */
rep(`role="button" tabindex="0" onkeydown={() => {}} aria-label="Now playing"`,
`role="button" tabindex="0" onkeydown={() => {}} aria-hidden={nav.sheet ? "true" : undefined} aria-label={"Now playing: " + (player.track.title || "") + (player.track.artist || player.album?.artist ? " · " + (player.track.artist || player.album?.artist) : "")}`);
rep(` <div class="scrim" class:on={!!nav.half} onclick={closeHalf} role="presentation"></div>`,
` <div class="scrim" class:on={!!nav.half} onclick={closeHalf} aria-hidden="true"></div>`);
rep(` {#if player.error || lib.error}
<div class="toast" onclick={() => { player.error = ""; lib.error = ""; }} role="status">{player.error || lib.error}</div>
{/if}
{#if notice.text}
<div class="toast notice" role="status">
<span class="nt">{notice.text}</span>
{#if notice.action}<button class="na" onclick={() => { const f = notice.action.fn; clearNote(); f?.(); }}>{notice.action.label}</button>{/if}
</div>
{/if}`, ins('ins-r28-toasts.txt'));
save();
// ── the look: the live region, and nothing else ─────────────────────────
load('src/mobile/shell.css');
s = s.replace(/\s*$/, NL) + `
/* The one live region: never drawn, never laid out, always there — so an
announcement is a change of text inside it rather than a node arriving
with its words already in it (PreviewOverlay.svelte's .sr-announce, at
phone scale) */
.sr-live { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
`;
save();
// ── the player sheet: a dialog, with somewhere to land ──────────────────
load('src/mobile/Sheet.svelte');
rep(`<div class="sheet" class:up={shown} class:morphing use:drag={sheetDrag}>`,
`<div class="sheet" class:up={shown} class:morphing use:drag={sheetDrag}
role="dialog" aria-modal="true" aria-label={"Now playing" + (player.track ? ": " + player.track.title : "")}
aria-hidden={nav.sheet ? undefined : "true"} inert={!nav.sheet}>`);
rep(` function flip(a) {`,
` /* Somewhere sensible to land when the sheet opens: its own Close. The
gate is nav.sheet and not "shown" — "shown" is false for a frame at
the start of a morph arrival and the dialog has to be live by then —
and preventScroll because that arrival is measuring rects in the
same frames. */
$effect(() => {
if (!nav.sheet) return;
const t = setTimeout(() => {
const el = document.querySelector(".sheet.up .np-head .iconbtn");
if (el && !el.closest(".sheet")?.contains(document.activeElement)) el.focus({ preventScroll: true });
}, 160);
return () => clearTimeout(t);
});
function flip(a) {`);
/* the artwork opens the viewer, so it is a control and not decoration */
rep(` <div class="np-art" class:small onclick={() => openViewer(player.album?.art_path)} role="presentation">`,
` <div class="np-art" class:small onclick={() => openViewer(player.album?.art_path)}
role="button" tabindex="0" onkeydown={() => {}} aria-label={"View album artwork" + (player.album?.title ? " — " + player.album.title : "")}>`);
rep(` <div class="np-faces">`, ` <div class="np-faces" role="tablist" aria-label="Player faces">`);
rep(` <button class="fbtn" class:on={face === f} onclick={() => (nav.face = f)}><Icon name={ic} size={17} /><span>{lb}</span></button>`,
` <button class="fbtn" class:on={face === f} onclick={() => (nav.face = f)} role="tab" aria-selected={face === f}><Icon name={ic} size={17} /><span>{lb}</span></button>`);
save();
// ── the half sheet: a dialog that names itself, and a grab that closes ──
load('src/mobile/HalfSheet.svelte');
rep(` import Icon from "../components/Icon.svelte";`,
` import { tick } from "svelte";` + NL + ` import Icon from "../components/Icon.svelte";`);
rep(` $effect(() => { if (nav.half) shown = nav.half; });`,
` $effect(() => { if (nav.half) shown = nav.half; });
/* What this sheet is called, for a screen reader: whatever its own
first title says. Every kind writes one — a .sheet-title, or the
head's .t — so read it back out of the DOM rather than keeping a
list of twenty-seven names in step with the markup. */
let hsName = $state("");
let grabEl = $state(null);
$effect(() => {
if (!nav.half) return;
void shown;
tick().then(() => {
const el = document.querySelector(".halfsheet .sheet-title, .halfsheet .sheet-head .t");
hsName = el ? (el.textContent || "").replace(/\\s+/g, " ").trim() : "Options";
});
});
/* the grab handle is where the focus lands — unless the sheet has a
field, which takes it for itself a moment later (focusLater) */
$effect(() => {
if (!nav.half) return;
const t = setTimeout(() => {
if (!grabEl || document.querySelector(".halfsheet .field")) return;
if (grabEl.closest(".halfsheet")?.contains(document.activeElement)) return;
grabEl.focus({ preventScroll: true });
}, 160);
return () => clearTimeout(t);
});`);
/* the scrim was the only tap-dismissal and a screen reader could not reach
it; the grab handle is the first one that can be. The scrim still works */
rep(`<div class="halfsheet" class:up={!!nav.half}>
<div class="grab"></div>`,
`<div class="halfsheet" class:up={!!nav.half} role="dialog" aria-modal="true" aria-label={hsName || "Options"}
aria-hidden={nav.half ? undefined : "true"} inert={!nav.half}>
<button class="grab" bind:this={grabEl} onclick={closeHalf} aria-label="Close this sheet"></button>`);
rep(`{#snippet swrow(icon, t, m, on, fn)}
<button class="row" onclick={fn}>`,
`{#snippet swrow(icon, t, m, on, fn)}
<button class="row" onclick={fn} role="switch" aria-checked={on}>`);
rep(` <button class="row" onclick={() => setMeta(u, { favorite: !m.favorite })}>`,
` <button class="row" onclick={() => setMeta(u, { favorite: !m.favorite })} role="switch" aria-checked={!!m.favorite}>`);
rep(` <button class="row" onclick={() => setMeta(u, { trusted: !m.trusted })}>`,
` <button class="row" onclick={() => setMeta(u, { trusted: !m.trusted })} role="switch" aria-checked={!!m.trusted}>`);
/* a placeholder stops being a name the moment there is text in the field,
so both prompts take the sheet's own title instead */
rep(` <textarea class="field area" bind:value={promptVal} placeholder={a.placeholder || ""} rows="3" use:focusLater></textarea>`,
` <textarea class="field area" bind:value={promptVal} placeholder={a.placeholder || ""} rows="3" aria-label={a.title} use:focusLater></textarea>`);
rep(`inputmode={a.numeric ? "numeric" : undefined} enterkeyhint="done"`,
`inputmode={a.numeric ? "numeric" : undefined} aria-label={a.title} enterkeyhint="done"`);
chips(3);
save();
// ── the viewer: its own dismissal, named (ArtworkViewer.svelte's words) ─
load('src/mobile/Viewer.svelte');
rep(` <div class="viewer" onclick={tap} onpointerdown={down} onpointermove={move} onpointerup={up} onpointercancel={up} role="presentation">`,
` <div class="viewer" onclick={tap} onpointerdown={down} onpointermove={move} onpointerup={up} onpointercancel={up}
role="button" tabindex="0" onkeydown={(e) => { if (e.key === "Enter" || e.key === " ") closeViewer(); }} aria-label="Close artwork viewer">`);
save();
// ── the tap-sync room is a dialog too ───────────────────────────────────
load('src/mobile/SyncRoom.svelte');
rep(` <div class="syncroom">`, ` <div class="syncroom" role="dialog" aria-modal="true" aria-label="Tap in time">`);
save();
// ── the waveform: the app's one position control, so a slider ───────────
load('src/mobile/Lane.svelte');
rep(` function pu() { down = false; }`,
` function pu() { down = false; }
/* a seek from the keyboard, or from TalkBack: two per cent a press —
the fraction the pointer path already speaks */
function key(e) {
if (!onseek) return;
const step = e.key === "ArrowRight" ? 0.02 : e.key === "ArrowLeft" ? -0.02 : 0;
if (!step) return;
e.preventDefault();
onseek(Math.max(0, Math.min(1, played + step)));
}`);
rep(`<span
class="lane {cls}"`,
`<!-- svelte-ignore a11y_no_noninteractive_tabindex -- the role and the
tabindex arrive together: a lane that can seek is a slider, and one
that cannot is out of the tree entirely -->
<span
class="lane {cls}"`);
rep(` onpointercancel={pu}
>`,
` onpointercancel={pu}
onkeydown={key}
role={onseek ? "slider" : undefined}
tabindex={onseek ? 0 : undefined}
aria-hidden={onseek ? undefined : "true"}
aria-label={onseek ? "Seek" : undefined}
aria-valuemin={onseek ? 0 : undefined}
aria-valuemax={onseek ? 100 : undefined}
aria-valuenow={onseek ? Math.round(played * 100) : undefined}
>`);
save();
// ── the release card: the badge belongs in the name, not glued to the
// title ("FLAC 24Discovery2001 · 14 tracks" was the whole name) ───────
load('src/mobile/Release.svelte');
rep(`<button class="release" class:w2 {onclick} oncontextmenu={hold} use:visible={path}>`,
`<button class="release" class:w2 {onclick} oncontextmenu={hold} use:visible={path} aria-label={[title, sub, badge].filter(Boolean).join(", ")}>`);
rep(` {#if badge}<span class="ed-badge">{badge}</span>{/if}`,
` {#if badge}<span class="ed-badge" aria-hidden="true">{badge}</span>{/if}`);
save();
// ── a record: the cover opens the viewer, and each row says which row ───
load('src/mobile/screens/Album.svelte');
rep(` <div class="rec-cover" onclick={() => openViewer(album.art_path)} role="presentation"><Cover path={album.art_path} eager size={48} /></div>`,
` <div class="rec-cover" onclick={() => openViewer(album.art_path)} role="button" tabindex="0" onkeydown={() => {}} aria-label={"View album artwork — " + album.title}><Cover path={album.art_path} eager size={48} /></div>`);
rep(` <button class="trk" class:playing={isPlaying(t)} onclick={() => playAt(t)} oncontextmenu={(e) => { e.preventDefault(); openHalf("track", { album, track: t }); }}>`,
` <button class="trk" class:playing={isPlaying(t)} onclick={() => playAt(t)} oncontextmenu={(e) => { e.preventDefault(); openHalf("track", { album, track: t }); }} aria-label={(t.track_no ?? i + 1) + ". " + t.title + (various && t.artist ? " · " + t.artist : "")}>`);
rep(` <span class="r" role="button" tabindex="0" aria-label="More" onkeydown={() => {}}`,
` <span class="r" role="button" tabindex="0" aria-label={"More for " + t.title} onkeydown={() => {}}`);
save();
// ── the library: three segmented tab groups are tab groups ──────────────
load('src/mobile/screens/Library.svelte');
rep(`<div class="segtabs">
{#each TABS as [k, l] (k)}
<button class:on={tab === k} onclick={() => patch({ tab: k })}>{l}</button>`,
`<div class="segtabs" role="tablist" aria-label="Library sections">
{#each TABS as [k, l] (k)}
<button class:on={tab === k} onclick={() => patch({ tab: k })} role="tab" aria-selected={tab === k}>{l}</button>`);
rep(` <button class="gapline" onclick={() => toggleGaps(ar.name)}>`,
` <button class="gapline" onclick={() => toggleGaps(ar.name)} aria-expanded={!!gapOpen[key]}>`);
chips(1);
save();
load('src/mobile/screens/Community.svelte');
rep(` <div class="segtabs" style="padding-top:12px">`,
` <div class="segtabs" style="padding-top:12px" role="tablist" aria-label="Community sections">`);
rep(` <button class:on={tab === k} onclick={() => patch({ tab: k })}>
{l}{#if k === "chat" && unreadTotal()} <span class="count">{unreadTotal()}</span>{/if}`,
` <button class:on={tab === k} onclick={() => patch({ tab: k })} role="tab" aria-selected={tab === k} aria-label={l + (k === "chat" && unreadTotal() ? ", " + unreadTotal() + " unread" : "")}>
{l}{#if k === "chat" && unreadTotal()} <span class="count" aria-hidden="true">{unreadTotal()}</span>{/if}`);
rep(` <input class="field" bind:value={user} placeholder="Username" autocomplete="username"`,
` <input class="field" bind:value={user} placeholder="Username" aria-label="Soulseek username" autocomplete="username"`);
rep(` <input class="field" type={showPass ? "text" : "password"} bind:value={pass} placeholder="Password" autocomplete="current-password"`,
` <input class="field" type={showPass ? "text" : "password"} bind:value={pass} placeholder="Password" aria-label="Soulseek password" autocomplete="current-password"`);
save();
load('src/mobile/screens/Artist.svelte');
rep(` <div class="segtabs disco-tabs">`, ` <div class="segtabs disco-tabs" role="tablist" aria-label="Discography">`);
rep(` <button class:on={tab === t.key} onclick={() => (tab = t.key)}>{t.label}<span class="count">{t.items.length}</span></button>`,
` <button class:on={tab === t.key} onclick={() => (tab = t.key)} role="tab" aria-selected={tab === t.key} aria-label={t.label + ", " + t.items.length}>{t.label}<span class="count" aria-hidden="true">{t.items.length}</span></button>`);
/* the biography IS the button's name — that is why the probe lets an
expander off its long-name check */
rep(` <p class="bio" class:open={bioOpen} onclick={() => (bioOpen = !bioOpen)} role="button" tabindex="0" onkeydown={() => {}}>{extras.bio}</p>`,
` <p class="bio" class:open={bioOpen} onclick={() => (bioOpen = !bioOpen)} role="button" tabindex="0" aria-expanded={bioOpen} onkeydown={() => {}}>{extras.bio}</p>`);
save();
load('src/mobile/screens/CatalogAlbum.svelte');
rep(` <p class="bio" class:open={descOpen} onclick={() => (descOpen = !descOpen)} role="button" tabindex="0" onkeydown={() => {}}>{desc.rest}</p>`,
` <p class="bio" class:open={descOpen} onclick={() => (descOpen = !descOpen)} role="button" tabindex="0" aria-expanded={descOpen} onkeydown={() => {}}>{desc.rest}</p>`);
save();
// ── the fields that had nothing but a placeholder ───────────────────────
load('src/mobile/screens/Room.svelte');
rep(` <input class="field" bind:this={inputEl} bind:value={text} placeholder="Message #{name}…" autocomplete="off"`,
` <input class="field" bind:this={inputEl} bind:value={text} placeholder="Message #{name}…" aria-label={"Message #" + name} autocomplete="off"`);
/* the roster button's whole name was the digits on it */
rep(` <button class="btn btn-ghost" style="height:32px;padding:0 12px" onclick={() => openHalf("roomusers", { name })}>{room.users.length} <Icon name="users" size={14} /></button>`,
` <button class="btn btn-ghost" style="height:32px;padding:0 12px" onclick={() => openHalf("roomusers", { name })} aria-label={room.users.length + " people in this room"}>{room.users.length} <Icon name="users" size={14} /></button>`);
save();
load('src/mobile/screens/Thread.svelte');
rep(` <input class="field" bind:this={inputEl} bind:value={text} placeholder="Message {display(username)}…" autocomplete="off"`,
` <input class="field" bind:this={inputEl} bind:value={text} placeholder="Message {display(username)}…" aria-label={"Message " + display(username)} autocomplete="off"`);
save();
// ── the per-row ⋯ says which row it belongs to ──────────────────────────
load('src/mobile/screens/Playlist.svelte');
rep(` <span class="r" role="button" tabindex="0" aria-label="More" onkeydown={() => {}} onclick={(e) => { e.stopPropagation(); sheet(item); }}><Icon name="more" size={17} /></span>`,
` <span class="r" role="button" tabindex="0" aria-label={"More for " + item.title} onkeydown={() => {}} onclick={(e) => { e.stopPropagation(); sheet(item); }}><Icon name="more" size={17} /></span>`);
save();
load('src/mobile/screens/Search.svelte');
rep(` <span class="r" role="button" tabindex="0" aria-label="More" onkeydown={() => {}} onclick={(e) => { e.stopPropagation(); moreHit(row); }}><Icon name="more" size={17} /></span>`,
` <span class="r" role="button" tabindex="0" aria-label={"More for " + row.title} onkeydown={() => {}} onclick={(e) => { e.stopPropagation(); moreHit(row); }}><Icon name="more" size={17} /></span>`);
chips(2);
save();
load('src/mobile/screens/Tracks.svelte');
rep(` <span class="r" role="button" tabindex="0" aria-label="More" onkeydown={() => {}} onclick={(e) => { e.stopPropagation(); more(r); }}><Icon name="more" size={17} /></span>`,
` <span class="r" role="button" tabindex="0" aria-label={"More for " + r.title} onkeydown={() => {}} onclick={(e) => { e.stopPropagation(); more(r); }}><Icon name="more" size={17} /></span>`);
save();
// ── the switch rows say on or off ───────────────────────────────────────
load('src/mobile/screens/Import.svelte');
rep(` <button class="row" onclick={() => (importer.includeUnresolved = !importer.includeUnresolved)}>`,
` <button class="row" onclick={() => (importer.includeUnresolved = !importer.includeUnresolved)} role="switch" aria-checked={!!importer.includeUnresolved}>`);
save();
load('src/mobile/screens/You.svelte');
rep(` <button class="row" onclick={() => setSetting(k, !settings[k])}>`,
` <button class="row" onclick={() => setSetting(k, !settings[k])} role="switch" aria-checked={!!settings[k]}>`);
rep(` <button class="row" onclick={toggleNormalization}>`,
` <button class="row" onclick={toggleNormalization} role="switch" aria-checked={normalization}>`);
rep(` <button class="row" onclick={() => setSetting("wifiOnly", !settings.wifiOnly)}>`,
` <button class="row" onclick={() => setSetting("wifiOnly", !settings.wifiOnly)} role="switch" aria-checked={!!settings.wifiOnly}>`);
rep(` <button class="row" onclick={() => setFlag("wordTiming", !cfg?.word_timing)}>`,
` <button class="row" onclick={() => setFlag("wordTiming", !cfg?.word_timing)} role="switch" aria-checked={!!cfg?.word_timing}>`);
rep(` <button class="row" onclick={() => setFlag("writeFolderCover", !(cfg?.write_folder_cover ?? true))}>`,
` <button class="row" onclick={() => setFlag("writeFolderCover", !(cfg?.write_folder_cover ?? true))} role="switch" aria-checked={!!(cfg?.write_folder_cover ?? true)}>`);
chips(7);
save();
// ── the walk: four numbers, held at zero on ten screens ─────────────────
load('tests/mobile/smoke.mjs');
rep(` var e = document.querySelector('.screen');`,
` var e = document.querySelector('.screen');` + NL + ins('ins-r28-probe.txt'));
rep(` contactsLoaded: (function () { try { return __ss.contacts.loaded ? 'yes' : (__ss.contacts.error ? 'err' : 'no'); } catch (e) { return ''; } })(),`,
` contactsLoaded: (function () { try { return __ss.contacts.loaded ? 'yes' : (__ss.contacts.error ? 'err' : 'no'); } catch (e) { return ''; } })(),` + NL + ins('ins-r28-fields.txt'));
// the mock walk
rep(` home: (i) => /^(Good |Still)/.test(i.title),`,
` home: (i) => /^(Good |Still)/.test(i.title) && /^Home\\*\\|Search\\|Library\\|Community(, \\d+ unread)?\\|You(, \\d+ transfers?)?$/.test(i.navTabs) && !i.a11yAnon && !i.a11yGhost && !i.a11yBad && !i.a11yState,`);
rep(` library: (i) => i.title.startsWith("Your library") && i.fmtBadges >= 10 && /hours/.test(i.countline),`,
` library: (i) => i.title.startsWith("Your library") && i.fmtBadges >= 10 && /hours/.test(i.countline) && !i.a11yAnon && !i.a11yGhost && !i.a11yBad && !i.a11yState,`);
rep(` album: (i) => i.screen.includes("tr-push") && !i.mini,`,
` album: (i) => i.screen.includes("tr-push") && !i.mini && !i.a11yAnon && !i.a11yGhost && !i.a11yBad && !i.a11yState,`);
rep(` "track-sheet": (i) => i.half.includes("up"),
sheet: (i) => i.sheet.includes("up"),`,
` "track-sheet": (i) => i.half.includes("up") && /^dialog\\/true\\/.+\\/focus$/.test(i.dlg) && !i.a11yAnon && !i.a11yBad && !i.a11yGhost && !i.a11yState,
sheet: (i) => i.sheet.includes("up") && /^dialog\\/true\\/Now playing/.test(i.dlg) && !i.a11yAnon && !i.a11yBad && !i.a11yGhost && !i.a11yState,`);
rep(` viewer: (i) => i.viewer,
"viewer-closed": (i) => !i.viewer && i.sheet.includes("up"),
"album-playing": (i) => i.mini,`,
` viewer: (i) => i.viewer && /^button\\/-\\/Close artwork viewer/.test(i.dlg) && !i.a11yAnon && !i.a11yBad && !i.a11yGhost,
"viewer-closed": (i) => !i.viewer && i.sheet.includes("up"),
"album-playing": (i) => i.mini,`);
rep(` "cat-album": (i) => i.screen.includes("tr-push") && i.title.startsWith("Discovery") && i.eds >= 2 && i.lede && i.bars >= 10,`,
` "cat-album": (i) => i.screen.includes("tr-push") && i.title.startsWith("Discovery") && i.eds >= 2 && i.lede && i.bars >= 10 && !i.a11yAnon && !i.a11yGhost && !i.a11yBad && !i.a11yState,`);
rep(` "cat-album-save": (i) => /^Saving \\d+ from /.test(i.save) && /^\\d+$/.test(i.navBadges),`,
` "cat-album-save": (i) => /^Saving \\d+ from /.test(i.save) && /^\\d+$/.test(i.navBadges) && /\\|You, \\d+ transfers?$/.test(i.navTabs),`);
rep(` you: (i) => i.title.startsWith("You") && i.norm === "on" && i.folderRow && i.srcMax === "No limit",`,
` you: (i) => i.title.startsWith("You") && i.norm === "on" && i.folderRow && i.srcMax === "No limit" && !i.a11yAnon && !i.a11yGhost && !i.a11yBad && !i.a11yState,`);
rep(` "you-norm-off": (i) => i.norm === "off",`,
` "you-norm-off": (i) => i.norm === "off" && i.a11yState === "",`);
rep(` taste: (i) => i.likes === "Daft Punk|french house|Warp Records" && i.hates === "loudness war" && i.similar === 4,`,
` taste: (i) => i.likes === "Daft Punk|french house|Warp Records" && i.hates === "loudness war" && i.similar === 4 && !i.a11yAnon && !i.a11yBad && !i.a11yGhost && !i.a11yState,`);
rep(` artist: (i) => i.screen.includes("has-hero") && i.amb.includes("on:img") && /played/.test(i.played),`,
` artist: (i) => i.screen.includes("has-hero") && i.amb.includes("on:img") && /played/.test(i.played) && !i.a11yAnon && !i.a11yBad && !i.a11yGhost && !i.a11yState,`);
/* the six fields this round names are a room's message box and the two
shapes of prompt; without these three the count is held at zero on ten
screens that never had one of them. The roster button's whole name was
the digits on it, so the room takes the junk count too. */
rep(` room: (i) => i.screen.includes("tr-push") && i.msgs >= 2,`,
` room: (i) => i.screen.includes("tr-push") && i.msgs >= 2 && !i.a11yAnon && !i.a11yBad,`);
rep(` "playlist-new-prompt": (i) => i.half.includes("up") && /^New Playlist \\d+$/.test(i.promptVal),`,
` "playlist-new-prompt": (i) => i.half.includes("up") && /^New Playlist \\d+$/.test(i.promptVal) && !i.a11yAnon,`);
rep(` "import-paste-prompt": (i) => i.half.includes("up") && /Paste a tracklist/.test(i.halfText) && i.promptVal === "",`,
` "import-paste-prompt": (i) => i.half.includes("up") && /Paste a tracklist/.test(i.halfText) && i.promptVal === "" && !i.a11yAnon,`);
rep(` "playlist-removed": (i) => i.plrows === 5 && /Removed/.test(i.notice),`,
` "playlist-removed": (i) => i.plrows === 5 && /Removed/.test(i.notice) && /Removed/.test(i.announce) && /Undo/.test(i.announce),`);
// the phone walk: the markup is the same, so carry three over conservatively
rep(` album: (i) => i.screen.includes("tr-push") && i.amb.includes("on:img"),`,
` album: (i) => i.screen.includes("tr-push") && i.amb.includes("on:img") && !i.a11yAnon && !i.a11yGhost,`);
rep(` "home-ambience": (i) => /^(Good |Still)/.test(i.title) && i.amb.includes("on:img"),
sheet: (i) => i.sheet.includes("up"),`,
` "home-ambience": (i) => /^(Good |Still)/.test(i.title) && i.amb.includes("on:img"),
sheet: (i) => i.sheet.includes("up") && /^dialog\\/true\\/Now playing/.test(i.dlg) && !i.a11yGhost,`);
rep(` you: (i) => i.title.startsWith("You") && !!i.norm && !!i.srcMax,`,
` you: (i) => i.title.startsWith("You") && !!i.norm && !!i.srcMax && !i.a11yAnon && !i.a11yGhost && !i.a11yState,`);
save();
// ── the doc ─────────────────────────────────────────────────────────────
load('ANDROID.md');
rep(`## Performance`, ins('ins-r28-doc.txt') + NL + NL + `## Performance`);
save();
console.log('round 28 wired');