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.
212 lines
13 KiB
JavaScript
212 lines
13 KiB
JavaScript
// Round thirty-eight: the whole discography. ANDROID.md listed the desktop's
|
|
// dedicated discography page as still desktop-only, and the phone's artist
|
|
// page already had the machinery for it — disco.js over the shared
|
|
// localReleaseType.js and editions.js. What was missing was the page: All in
|
|
// front of the categories, a sort, a grouping by year, and the editions kept
|
|
// whole rather than flattened away. The desktop's own page is a stack of
|
|
// album-page heroes at --cover-scale 0.45 — a 108px cover each, with the
|
|
// record's whole tracklist under it, fetched as the section scrolls near and
|
|
// its accent sampled off its own cover; on a 411px screen that is one record
|
|
// per screenful and one catalog_album per row over mobile data, so those
|
|
// three stay behind and are named in the doc as departures. The other half
|
|
// of the round is the cap: the artist page rendered the WHOLE active tab,
|
|
// with no slice anywhere in it, so a 140-release artist drew 140 cards — it
|
|
// shows twelve now, with the way down under them, and the page itself
|
|
// renders sixty at a time. No Rust and no Kotlin.
|
|
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;
|
|
/* Nothing reaches the disk until every anchor in the round has been found.
|
|
This round deletes a clause from an ANDROID.md bullet that round 37
|
|
rewraps — both shapes are written out below — and a neighbour rewording
|
|
anything else would make an anchor miss AFTER the screen had been
|
|
created, at which point the guard below refuses the re-run and the tree
|
|
is stuck half-patched. `save` holds the text; `flush` writes it, once,
|
|
at the end. */
|
|
const pending = new Map();
|
|
const load = (file) => { p = file; s = pending.has(file) ? pending.get(file) : fs.readFileSync(root + p, 'utf8'); };
|
|
const save = () => { pending.set(p, s); };
|
|
const flush = () => { for (const [f, t] of pending) fs.writeFileSync(root + f, t); };
|
|
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); };
|
|
/* One anchor in this round has two known shapes: the "Still desktop-only,
|
|
smaller" bullet is round 37's as well, and 37 takes the drag-reorder
|
|
clause out of it and REWRAPS what is left, which puts "the full
|
|
discography page" across two lines. Exactly one of the shapes has to be
|
|
in the file — neither, or both, is a bullet nobody here recognises and
|
|
the round refuses rather than guessing. */
|
|
const repOneOf = (...pairs) => {
|
|
const hit = pairs.filter(([a]) => s.includes(a));
|
|
if (hit.length !== 1) {
|
|
throw new Error('the "Still desktop-only, smaller" bullet in ' + p + ' matches '
|
|
+ hit.length + ' of the ' + pairs.length + ' shapes round 38 knows — re-read it and add this one');
|
|
}
|
|
rep(hit[0][0], hit[0][1]);
|
|
};
|
|
const NL = String.fromCharCode(10);
|
|
|
|
/* Both the disco.js block and the shell.css block are APPENDS with no anchor
|
|
at all, so a second run would write them twice and `once` would only
|
|
notice further down, if at all. Refuse one here, before anything is even
|
|
read: the screen is the file this round creates and nothing else in the
|
|
repo makes it. */
|
|
if (fs.existsSync(root + 'src/mobile/screens/Discography.svelte')) {
|
|
throw new Error('src/mobile/screens/Discography.svelte already exists — round 38 has already run against ' + root);
|
|
}
|
|
|
|
// ── the classifier, seen whole: All, the sort, the year groups ─────────
|
|
// Appended; the guard above is what refuses a second run.
|
|
load('src/mobile/disco.js');
|
|
s = s.replace(/\s*$/, NL) + NL + ins('ins-r38-disco.txt') + NL;
|
|
save();
|
|
|
|
// ── the page ──────────────────────────────────────────────────────────
|
|
pending.set('src/mobile/screens/Discography.svelte', ins('ins-r38-screen.txt') + NL);
|
|
|
|
// ── the artist page: the way down, and the cap that makes it earn its
|
|
// place. The grid had no slice at all before this.
|
|
load('src/mobile/screens/Artist.svelte');
|
|
rep(` <div class="sec"><h2>Discography <em>· {releasedCount} on the catalog</em></h2></div>`,
|
|
` <div class="sec">
|
|
<h2>Discography <em>· {releasedCount} on the catalog</em></h2>
|
|
<button class="all" onclick={() => push("disco", { name }, screenEl)} aria-label="All releases">All<Icon name="chevron-right" size={14} /></button>
|
|
</div>`);
|
|
rep(` const families = $derived(tabs.find((t) => t.key === tab)?.items ?? []);`,
|
|
` /* the grid is a SLICE — the whole tab lives on the discography page,
|
|
which the head's "All" and the link under the grid both open. Before
|
|
this an artist with 140 releases rendered 140 cards here. */
|
|
const GRID_CAP = 12;
|
|
const activeTab = $derived(tabs.find((t) => t.key === tab) ?? null);
|
|
const families = $derived((activeTab?.items ?? []).slice(0, GRID_CAP));
|
|
const tabCount = $derived(activeTab?.items.length ?? 0);`);
|
|
rep(` onmore={() => openHalf("catalogAlbum", { album: { ...(f.editions?.[0] ?? f), artist: name } })}
|
|
/>
|
|
{/each}
|
|
</div>
|
|
{/if}`,
|
|
` onmore={() => openHalf("catalogAlbum", { album: { ...(f.editions?.[0] ?? f), artist: name } })}
|
|
/>
|
|
{/each}
|
|
</div>
|
|
{#if tabCount > GRID_CAP}
|
|
<button class="more-link dg-all" onclick={() => push("disco", { name, tab }, screenEl)}>Show all {tabCount} {activeTab?.label ?? "releases"}</button>
|
|
{/if}
|
|
{/if}`);
|
|
save();
|
|
|
|
// ── the shell: the screen, the glass bar's words, the ground ──────────
|
|
load('src/mobile/Mobile.svelte');
|
|
rep(` import Artist from "./screens/Artist.svelte";`,
|
|
` import Artist from "./screens/Artist.svelte";
|
|
import Discography from "./screens/Discography.svelte";`);
|
|
rep(` {:else if entry.n === "artist"}
|
|
<Artist name={entry.c.name} seed={entry.c.seed ?? null} {screenEl} />`,
|
|
` {:else if entry.n === "artist"}
|
|
<Artist name={entry.c.name} seed={entry.c.seed ?? null} {screenEl} />
|
|
{:else if entry.n === "disco"}
|
|
<Discography {entry} {screenEl} />`);
|
|
rep(` case "artist": {`, ins('ins-r38-mobile-meta.txt') + NL + ` case "artist": {`);
|
|
save();
|
|
|
|
// ── the sort sheet. Both edits are the FIRST clause of their chain, the
|
|
// furthest from anything a neighbour round would append.
|
|
load('src/mobile/HalfSheet.svelte');
|
|
rep(` if (t.n === "album") return "album-more";`,
|
|
` if (t.n === "disco") return "discosort";
|
|
if (t.n === "album") return "album-more";`);
|
|
rep(` {:else if kind === "librarysort"}`,
|
|
ins('ins-r38-halfsheet.txt') + NL + ` {:else if kind === "librarysort"}`);
|
|
save();
|
|
|
|
// ── the look. Appended after round thirty-four's block, the precedent.
|
|
load('src/mobile/shell.css');
|
|
s = s.replace(/\s*$/, NL) + NL + ins('ins-r38-css.txt') + NL;
|
|
save();
|
|
|
|
// ── the mock: an artist prolific enough that the cap is walked ────────
|
|
load('src/lib/mockLibrary.js');
|
|
rep(`/* the heroes chosen this session — pick_image → set_artist_hero */`,
|
|
ins('ins-r38-mock-big.txt') + NL + `/* the heroes chosen this session — pick_image → set_artist_hero */`);
|
|
/* the `const a = …` chain appears three times; only with the line under it
|
|
is either one unique */
|
|
rep(` const a = id === CAT_ARTIST.id ? CAT_ARTIST : id === CAT_SLOW.id ? CAT_SLOW : RELATED.find((r) => r.id === id);
|
|
if (!a) return Promise.reject(new Error("catalog_artist: unknown artist"));`,
|
|
` const a = id === CAT_ARTIST.id ? CAT_ARTIST : id === CAT_SLOW.id ? CAT_SLOW : id === CAT_BIG.id ? CAT_BIG : RELATED.find((r) => r.id === id);
|
|
if (!a) return Promise.reject(new Error("catalog_artist: unknown artist"));`);
|
|
rep(` albums: own ? CAT_DISCO : slow ? SLOW_ALBUMS : [{ id: 900 + id`,
|
|
` albums: own ? CAT_DISCO : slow ? SLOW_ALBUMS : a === CAT_BIG ? BIG_ALBUMS : [{ id: 900 + id`);
|
|
rep(` const a = id === CAT_ARTIST.id ? CAT_ARTIST : id === CAT_SLOW.id ? CAT_SLOW : RELATED.find((r) => r.id === id);
|
|
if (!a) return Promise.reject(new Error("catalog_artist_overview: unknown artist"));`,
|
|
` const a = id === CAT_ARTIST.id ? CAT_ARTIST : id === CAT_SLOW.id ? CAT_SLOW : id === CAT_BIG.id ? CAT_BIG : RELATED.find((r) => r.id === id);
|
|
if (!a) return Promise.reject(new Error("catalog_artist_overview: unknown artist"));`);
|
|
rep(` const albums = id === CAT_SLOW.id ? SLOW_ALBUMS.slice(0, 4) : id === CAT_ARTIST.id ? CAT_DISCO.slice(0, 4) : [];`,
|
|
` const albums = id === CAT_SLOW.id ? SLOW_ALBUMS.slice(0, 4) : id === CAT_ARTIST.id ? CAT_DISCO.slice(0, 4) : id === CAT_BIG.id ? BIG_ALBUMS.slice(0, 4) : [];`);
|
|
save();
|
|
|
|
// ── the walks ─────────────────────────────────────────────────────────
|
|
load('tests/mobile/smoke.mjs');
|
|
rep(` disco: document.querySelectorAll('.grid2 .release').length,`,
|
|
` disco: document.querySelectorAll('.grid2 .release').length,` + NL + ins('ins-r38-probe.txt'));
|
|
/* the mock steps stay in the artist block, where the page they test lives */
|
|
rep(` { do: \`__click('.disco-tabs button', 'Singles')\`, name: "cat-artist-tab", wait: 700 },`,
|
|
` { do: \`__click('.disco-tabs button', 'Singles')\`, name: "cat-artist-tab", wait: 700 },` + NL + ins('ins-r38-mock-steps.txt'));
|
|
rep(` "cat-artist-tab": (i) => i.title.startsWith("Daft Punk") && i.cardImgs === 2 && i.cardPhs === 2,`,
|
|
` "cat-artist-tab": (i) => i.title.startsWith("Daft Punk") && i.cardImgs === 2 && i.cardPhs === 2,` + NL + ins('ins-r38-mock-expect.txt'));
|
|
/* the phone walk's own two anchors are round thirty-four's — the last step
|
|
and the last expectation — so rounds appending here stack in order */
|
|
rep(` { do: \`document.querySelectorAll('.nav button')[0].click()\`, name: "end", wait: 800 },`,
|
|
ins('ins-r38-phone-steps.txt') + NL + ` { do: \`document.querySelectorAll('.nav button')[0].click()\`, name: "end", wait: 800 },`);
|
|
rep(` end: (i) => /^(Good |Still)/.test(i.title),`,
|
|
ins('ins-r38-phone-expect.txt') + NL + ` end: (i) => /^(Good |Still)/.test(i.title),`);
|
|
save();
|
|
|
|
// ── the doc ───────────────────────────────────────────────────────────
|
|
load('ANDROID.md');
|
|
rep(`CatalogAlbum (a catalog record, rows the library has play), Playlist,`,
|
|
`CatalogAlbum (a catalog record, rows the library has play), Discography (every release, on one page), Playlist,`);
|
|
rep(`The **catalog record page** is the desktop's album page: the kind, the`,
|
|
ins('ins-r38-doc.txt') + NL + NL + `The **catalog record page** is the desktop's album page: the kind, the`);
|
|
/* the walk's own order: my steps sit in the catalog-artist block, where the
|
|
page they test is reached from — not beside the Library artist page
|
|
earlier in the same sentence */
|
|
rep(`catalog search → a song through the resolver →`,
|
|
`catalog search → that artist's whole discography on a page of its own (All
|
|
in front of six categories, a year header per year, the editions opened in
|
|
place, a name order dropping the grouping, and a 140-record artist whose
|
|
grid caps at twelve and whose page shows sixty with the rest a button away)
|
|
→ a song through the resolver →`);
|
|
/* and the line this round closes, out of "Not done yet". The bullet is
|
|
rewrapped rather than left with a half-empty line in the middle of it —
|
|
which is why both shapes are spelled out whole: round 37 takes the drag
|
|
reorder out of the same bullet and rewraps it first, and after that "the
|
|
full discography page" is split across two lines. Whichever of the two
|
|
is in the file, what comes out is a bullet that reads. */
|
|
repOneOf(
|
|
[`- **Still desktop-only, smaller.** Drag reorder (Move up / down / to the top
|
|
stand in), catalog track previews, the full discography page, and the
|
|
fused-artist portrait verdict — that last one needs the identity merge the
|
|
phone does not run, so porting it means porting the merge first.`,
|
|
`- **Still desktop-only, smaller.** Drag reorder (Move up / down / to the top
|
|
stand in), catalog track previews and the fused-artist portrait verdict —
|
|
that last one needs the identity merge the phone does not run, so porting
|
|
it means porting the merge first.`],
|
|
[`- **Still desktop-only, smaller.** Catalog track previews, the full
|
|
discography page, and the fused-artist portrait verdict — that last one
|
|
needs the identity merge the phone does not run, so porting it means
|
|
porting the merge first.`,
|
|
`- **Still desktop-only, smaller.** Catalog track previews and the
|
|
fused-artist portrait verdict — that last one needs the identity merge the
|
|
phone does not run, so porting it means porting the merge first.`],
|
|
);
|
|
save();
|
|
|
|
flush();
|
|
console.log('round 38 wired');
|