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.
229 lines
11 KiB
JavaScript
229 lines
11 KiB
JavaScript
// Round thirty-three: the sweep. Thirty rounds by several hands left drift
|
|
// in src/mobile — nine imports nothing in their file ever named again, two
|
|
// helpers written twice word for word, two functions and two CSS rules
|
|
// nothing has ever reached, and one raw NUL byte inside a string literal in
|
|
// player.svelte.js that has made `grep` call that file binary since the day
|
|
// it landed. Nothing here changes what the phone does: every deletion is
|
|
// something with no reference anywhere in src/, tests/, crates/ or
|
|
// src-tauri/, and every unification moves a body that was already identical.
|
|
// One line is ADDED — a comment over the one rule that looks like a stray
|
|
// and is not, so the next sweep does not take it. The walk is untouched on
|
|
// purpose: it is the guard, not the subject.
|
|
//
|
|
// Rounds thirty-one and thirty-two land first and this round reads import
|
|
// lines they may also touch, so every anchor into one is scoped to
|
|
// the NAME being taken out or put in rather than to the whole line — a
|
|
// round that adds a name beside it does not collide. Where a whole import
|
|
// statement goes the line is the anchor on purpose: if something else has
|
|
// joined it, the statement is no longer dead and `once` should say so.
|
|
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);
|
|
const NUL = String.fromCharCode(0);
|
|
|
|
/* A sweep is all deletions and moves, so a second run would hit missing
|
|
anchors halfway through and leave the tree half-swept. `stamp` in util.js
|
|
is what this round creates and nothing else has ever had — refuse before
|
|
a byte is written. */
|
|
if (fs.readFileSync(root + 'src/mobile/util.js', 'utf8').includes('export function stamp(ms)')) {
|
|
throw new Error('src/mobile/util.js already has stamp() — round 33 has already run against ' + root);
|
|
}
|
|
|
|
/* ── util.js: the two helpers that were written twice, once ─────────────
|
|
Both bodies are moved here WORD FOR WORD from the screens below, so the
|
|
diff on each screen is a deletion and an added name on an import line it
|
|
already had. */
|
|
load('src/mobile/util.js');
|
|
rep(`export function longDur(secs) {
|
|
secs = Math.max(0, Math.round(secs || 0));
|
|
const h = Math.floor(secs / 3600);
|
|
const m = Math.round((secs % 3600) / 60);
|
|
if (h) return h + " hr " + m + " min";
|
|
return m + " min";
|
|
}`,
|
|
`export function longDur(secs) {
|
|
secs = Math.max(0, Math.round(secs || 0));
|
|
const h = Math.floor(secs / 3600);
|
|
const m = Math.round((secs % 3600) / 60);
|
|
if (h) return h + " hr " + m + " min";
|
|
return m + " min";
|
|
}
|
|
` + ins('ins-r33-stamp.txt'));
|
|
rep(`export function fmtBytes(n) {
|
|
if (!n) return "";
|
|
if (n >= 1073741824) return (n / 1073741824).toFixed(1) + " GB";
|
|
if (n >= 1048576) return (n / 1048576).toFixed(1) + " MB";
|
|
return Math.round(n / 1024) + " kB";
|
|
}`,
|
|
`export function fmtBytes(n) {
|
|
if (!n) return "";
|
|
if (n >= 1073741824) return (n / 1073741824).toFixed(1) + " GB";
|
|
if (n >= 1048576) return (n / 1048576).toFixed(1) + " MB";
|
|
return Math.round(n / 1024) + " kB";
|
|
}
|
|
` + ins('ins-r33-fmtcount.txt'));
|
|
/* isLossless: written for the source rules, never called once. The desktop
|
|
has its own; this copy has no reference in src/, tests/, crates/ or
|
|
src-tauri/. The LOSSLESS set beside it stays — fmtOf reads it. */
|
|
rep(`export function isLossless(fmt) {
|
|
return /^(FLAC|WAV|AIFF?|APE|WV|ALAC|DSF|DFF)\\b/.test(fmt || "");
|
|
}
|
|
`, ``);
|
|
save();
|
|
|
|
/* ── the four screens the two helpers came from ───────────────────────
|
|
The name goes in at the closing brace of the util.js import, so a round
|
|
that has added its own name to that list still takes this cleanly. */
|
|
load('src/mobile/screens/Import.svelte');
|
|
rep(` } from "../util.js";`, `, stamp } from "../util.js";`);
|
|
rep(` function stamp(ms) {
|
|
const s = Math.floor((ms || 0) / 1000);
|
|
const h = Math.floor(s / 3600);
|
|
const m = Math.floor((s % 3600) / 60);
|
|
const sec = s % 60;
|
|
return (h ? h + ":" + String(m).padStart(2, "0") : String(m)) + ":" + String(sec).padStart(2, "0");
|
|
}
|
|
`, ``);
|
|
save();
|
|
|
|
load('src/mobile/screens/Playlist.svelte');
|
|
rep(` } from "../util.js";`, `, stamp } from "../util.js";`);
|
|
rep(`
|
|
function stamp(ms) {
|
|
const s = Math.floor((ms || 0) / 1000);
|
|
const h = Math.floor(s / 3600);
|
|
const m = Math.floor((s % 3600) / 60);
|
|
const sec = s % 60;
|
|
return (h ? h + ":" + String(m).padStart(2, "0") : String(m)) + ":" + String(sec).padStart(2, "0");
|
|
}
|
|
`, NL);
|
|
save();
|
|
|
|
load('src/mobile/screens/Artist.svelte');
|
|
rep(` } from "../util.js";`, `, fmtCount } from "../util.js";`);
|
|
rep(` const fmtCount = (n) => (!n ? "" : n >= 1e6 ? (n / 1e6).toFixed(1).replace(/\\.0$/, "") + "M" : n >= 1e3 ? Math.round(n / 1e3) + "k" : String(n));
|
|
`, ``);
|
|
save();
|
|
|
|
load('src/mobile/screens/CatalogAlbum.svelte');
|
|
rep(` } from "../util.js";`, `, fmtCount } from "../util.js";`);
|
|
rep(` const fmtCount = (n) => (!n ? "" : n >= 1e6 ? (n / 1e6).toFixed(1).replace(/\\.0$/, "") + "M" : n >= 1e3 ? Math.round(n / 1e3) + "k" : String(n));
|
|
`, ``);
|
|
save();
|
|
|
|
/* ── slsk: a sign-out that forgets the account, which nothing offers ────
|
|
The sheet's Sign out calls disconnect(); nothing in the tree has ever
|
|
called this. The desktop's Settings keeps api.forgetCredentials (and so
|
|
the mock's forget_credentials branch) alive, so only this copy goes. */
|
|
load('src/mobile/slsk.svelte.js');
|
|
rep(`
|
|
export async function forgetAccount() {
|
|
try { await api.forgetCredentials(); } catch { /* fine */ }
|
|
slsk.savedUser = "";
|
|
slsk.hasPassword = false;
|
|
}
|
|
`, ``);
|
|
save();
|
|
|
|
/* ── nine imports no file ever names again ─────────────────────────────
|
|
Each of these appears exactly once in its own file: on the import line.
|
|
Where a whole statement goes, the module is still loaded through another
|
|
import in the same graph (lib.svelte.js comes in through Mobile.svelte
|
|
and player.svelte.js; Library.svelte imports util.js four lines higher),
|
|
and neither module has a top-level statement other than a declaration, so
|
|
nothing moves but the order they are evaluated in. Every export named
|
|
here keeps other callers — only these imports go.
|
|
The tenth was Sheet.svelte's `import * as api`, and it is round
|
|
thirty-two's: a namespace import is the one shape Rollup cannot shake,
|
|
so its reason was the bundle rather than drift, and the round that weighs
|
|
the launch takes it. */
|
|
load('src/mobile/HalfSheet.svelte');
|
|
rep(`, albumsBy`, ``);
|
|
rep(`, refreshRecents`, ``);
|
|
save();
|
|
|
|
load('src/mobile/Sheet.svelte');
|
|
rep(` import { albumsBy } from "./lib.svelte.js";
|
|
`, ``);
|
|
save();
|
|
|
|
load('src/mobile/screens/Library.svelte');
|
|
/* the second import OF THE SAME MODULE, four lines below the first, and
|
|
the name it brings is unused — the line goes whole */
|
|
rep(` import { longDur } from "../util.js";
|
|
`, ``);
|
|
save();
|
|
|
|
load('src/mobile/screens/Room.svelte');
|
|
rep(`, leaveRoom`, ``);
|
|
rep(`, addBuddy`, ``);
|
|
/* `back` is a word as well as a name — anchor it between its neighbours
|
|
so a line of prose cannot make this ambiguous */
|
|
rep(`push, back,`, `push,`);
|
|
save();
|
|
|
|
load('src/mobile/screens/Search.svelte');
|
|
rep(`, folderMeta`, ``);
|
|
save();
|
|
|
|
load('src/mobile/screens/Transfers.svelte');
|
|
rep(`import { transfers, `, `import { `);
|
|
save();
|
|
|
|
/* ── two CSS rules no markup can reach ──────────────────────────────────
|
|
src/mobile has no classList call at all and exactly three class names
|
|
built from an expression — `lane {cls}`, `screen tr-{nav.tr}` and
|
|
`tag {cls}` — none of which can spell either of these. Loading the
|
|
stylesheet before and after and diffing the CSSOM says the same: two
|
|
rules fewer, these two, and no other rule's text changed. */
|
|
load('src/mobile/shell.css');
|
|
rep(`.iconbtn.liked { color: var(--accent); }
|
|
`, ``);
|
|
rep(`.cardpad { padding: 14px; }
|
|
`, ``);
|
|
/* and the opposite case, in the same file: a one-property rule 588 lines
|
|
from the one it exists to defeat, which reads exactly like the strays
|
|
above. Measured before writing this down — it is load-bearing. A comment
|
|
is the whole change; the declaration is untouched. */
|
|
rep(`.screen.has-hero { padding-top: 0; }
|
|
.mini .mini-lane { width: auto; }`,
|
|
`.screen.has-hero { padding-top: 0; }
|
|
/* not a stray: \`.lane\` is width: 100%, and the mini bar's lane is ALSO
|
|
positioned with both left: 16px and right: 16px — an over-constrained
|
|
absolute box drops \`right\`, so without this it comes out 32px wider than
|
|
the strip it was cut for (395 where 363 was asked for, at 411), the bars
|
|
sit off-centre and the tail runs under the bar's own overflow: hidden. */
|
|
.mini .mini-lane { width: auto; }`);
|
|
save();
|
|
|
|
/* ── one byte ───────────────────────────────────────────────────────────
|
|
The play-history key joins artist and title on a NUL, and the NUL was
|
|
written into the source as the character itself. That makes the file
|
|
non-text: every grep of player.svelte.js since has answered "Binary file
|
|
matches" and shown nothing. The escape is the same string to the engine,
|
|
and it is what catalog.svelte.js's infoKey already writes. */
|
|
load('src/mobile/player.svelte.js');
|
|
rep(` const key = artist + "` + NUL + `" + title;`,
|
|
` const key = artist + "\\u0000" + title;`);
|
|
save();
|
|
|
|
/* ── the doc: where the shared helpers live ─────────────────────────── */
|
|
load('ANDROID.md');
|
|
rep(`| \`src/mobile/prefs.svelte.js\` | Appearance, kept in localStorage: ambience, accent from artwork, motion |`,
|
|
`| \`src/mobile/prefs.svelte.js\` | Appearance, kept in localStorage: ambience, accent from artwork, motion |
|
|
| \`src/mobile/util.js\` | what every screen shares and no screen should write twice: the formatters (\`mmss\` / \`longDur\` / \`stamp\` for a length in seconds, in words and in milliseconds; \`ago\` / \`clock\`; \`fmtBytes\` / \`fmtCount\`; \`fmtOf\` / \`albumFmt\`), the name and folder readers a peer's file needs, the \`drag\` action, and \`devBuild\` |`);
|
|
save();
|
|
console.log('round 33 wired');
|