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.
This commit is contained in:
1 parent
867fd42972
commit
9f0601027c
400 files changed
+26401
-3
No files matched your search
@@ -0,0 +1,140 @@
|
||||
// Round forty-one: the long session. Every walk this project has ever run is
|
||||
// minutes long; a phone runs the app for hours, and nothing in the port had
|
||||
// been looked at for a leak. Driving a real build of this tree for eleven
|
||||
// laps says the listeners and the intervals are dead flat — the naive
|
||||
// add-minus-remove counter climbs twelve a lap and every one of them is a
|
||||
// `.screen` element {#key nav.seq} threw away — and that exactly two things
|
||||
// grow without a limit: the thumbnail map, which kept every cover it had
|
||||
// ever fetched for the WebView's lifetime, and the playlist cover table,
|
||||
// which strands one data URL per edit under a key nothing can read again.
|
||||
// Both are bounded here at the desktop's own numbers, with two extra rules
|
||||
// the desktop does not need because the phone's templates index the map
|
||||
// instead of asking through a getter. Then the instrument: artTally() on the
|
||||
// handle, seventeen probe fields, and seven mock steps that install a live
|
||||
// listener/timer registry in the page, drive two warm laps, a baseline and
|
||||
// nine more, assert the census did not grow, and take the registry back out
|
||||
// again. No new UI, no CSS, no Rust, 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;
|
||||
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);
|
||||
|
||||
/* The tally is APPENDED to art.svelte.js with no anchor of its own, and
|
||||
several inserts below re-emit their own anchor, so a second run would
|
||||
write twice where `once` cannot see it coming and would only throw
|
||||
further down, leaving half a tree. Refuse one here, before a byte is
|
||||
written: `artTally` appears nowhere in the repo today and is the first
|
||||
thing this round writes. */
|
||||
if (fs.readFileSync(root + 'src/mobile/art.svelte.js', 'utf8').includes('artTally')) {
|
||||
throw new Error('round 41 is already applied in ' + root + ' — reset the tree rather than re-running');
|
||||
}
|
||||
/* PROBE is ONE template literal, so a raw backtick anywhere inside it — a
|
||||
comment included — ends it, and what follows becomes a tagged template:
|
||||
the file still parses, `node --check` says nothing at all, and smoke.mjs
|
||||
throws the moment it is imported. `${` is the same trap the other way.
|
||||
Round thirty-nine's check, kept. */
|
||||
for (const bad of ['`', '${']) {
|
||||
if (ins('ins-r41-probe.txt').includes(bad)) {
|
||||
throw new Error('ins-r41-probe.txt carries a ' + bad + ' — the probe block lives inside a template literal');
|
||||
}
|
||||
}
|
||||
|
||||
// ── the thumbnail map: bounded where it grows, and countable ──────────
|
||||
load('src/mobile/art.svelte.js');
|
||||
rep(`const pending = new Map(); // path -> { promise, resolve }`,
|
||||
ins('ins-r41-art-trim.txt') + NL + NL + `const pending = new Map(); // path -> { promise, resolve }`);
|
||||
/* the one call site: the end of pump()'s own .then, AFTER the waiter has
|
||||
been handed the url it asked for, and told which key just landed so that
|
||||
the entry a consumer is about to read can never be the one thrown away */
|
||||
rep(` art[p] = u;
|
||||
const w = pending.get(p);
|
||||
pending.delete(p);
|
||||
if (w) w.resolve(u);`,
|
||||
` art[p] = u;
|
||||
const w = pending.get(p);
|
||||
pending.delete(p);
|
||||
if (w) w.resolve(u);
|
||||
/* bounded here, where it grows, and nowhere else */
|
||||
trimArt(p);`);
|
||||
/* the accents: the same budget, in insertion order, with none of the
|
||||
painting question — nothing renders a promise */
|
||||
rep(` );
|
||||
}
|
||||
return accents.get(path);`,
|
||||
` );
|
||||
while (accents.size > ART_MAX) accents.delete(accents.keys().next().value);
|
||||
}
|
||||
return accents.get(path);`);
|
||||
/* appended, so the guard above is what refuses a second run */
|
||||
s = s.replace(/\s*$/, NL) + NL + ins('ins-r41-art-tally.txt') + NL;
|
||||
save();
|
||||
|
||||
// ── the playlist covers a revision left behind ────────────────────────
|
||||
load('src/mobile/playlists.svelte.js');
|
||||
rep(` .then((u) => { pls.covers[k] = u || null; }, () => { pls.covers[k] = null; })`,
|
||||
` .then((u) => { pls.covers[k] = u || null; }, () => { pls.covers[k] = null; })
|
||||
/* the key is "id:revision" and an edit bumps the revision, so
|
||||
every earlier revision of this playlist is unreachable by
|
||||
construction — nothing looks a playlist's cover up by
|
||||
anything but its current one. Dropped as the new one lands:
|
||||
no cap, no policy, just the keys that are already dead. */
|
||||
.then(() => { const mine = p.id + ":"; for (const old of Object.keys(pls.covers)) if (old !== k && old.startsWith(mine)) delete pls.covers[old]; })`);
|
||||
save();
|
||||
|
||||
// ── the seam: one name on the handle, and nothing else ────────────────
|
||||
load('src/mobile/Mobile.svelte');
|
||||
rep(` import { art, wantArt, accentFor, applyAccent } from "./art.svelte.js";`,
|
||||
` import { art, artTally, wantArt, accentFor, applyAccent } from "./art.svelte.js";`);
|
||||
/* deliberately a THIRD fragment of this literal: round thirty-nine took the
|
||||
head (`art, catalog, slsk, boot,`) and round thirty-four the tail
|
||||
(`wifi: { downloadsAllowed, cellular }, next, prev, toggle`), so a sibling
|
||||
round appending to the handle reaches for one of those, not for this */
|
||||
rep(`xfn: xfernote, pls, notice, importer,`, `xfn: xfernote, pls, notice, importer, artTally,`);
|
||||
save();
|
||||
|
||||
// ── the walk ──────────────────────────────────────────────────────────
|
||||
load('tests/mobile/smoke.mjs');
|
||||
/* the second-to-last line of the probe, NOT the `contactsLoaded:` line
|
||||
(rounds thirty-one and thirty-four) nor the `lyrTools:` line (round
|
||||
thirty-nine), and not the probe's last line either, so a sibling
|
||||
appending at the end of the object does not land on top of this */
|
||||
rep(` miniLeft: Math.round((box('.mini') || { left: -1 }).left),`,
|
||||
` miniLeft: Math.round((box('.mini') || { left: -1 }).left),` + NL + ins('ins-r41-probe.txt'));
|
||||
/* the walk's current last step, NOT `];\nconst MOCK_EXPECT = {`, which round
|
||||
thirty-four used and a sibling is likely to reuse: anchoring on the step
|
||||
means a round appending at the array end lands after my block rather than
|
||||
inside it */
|
||||
rep(` { do: \`document.querySelectorAll('.nav button')[0].click()\`, name: "export-clear", wait: 900 },`,
|
||||
` { do: \`document.querySelectorAll('.nav button')[0].click()\`, name: "export-clear", wait: 900 },`
|
||||
+ NL + ins('ins-r41-steps.txt'));
|
||||
rep(` "export-clear": (i) => /^(Good |Still)/.test(i.title) && !i.toast && !i.notice && !i.half.includes("up"),`,
|
||||
` "export-clear": (i) => /^(Good |Still)/.test(i.title) && !i.toast && !i.notice && !i.half.includes("up"),`
|
||||
+ NL + ins('ins-r41-expect.txt'));
|
||||
save();
|
||||
|
||||
// ── the doc ───────────────────────────────────────────────────────────
|
||||
/* the last paragraph of ## Performance and the heading after it — away from
|
||||
"Not done yet" and from the walk's own step-list sentence, both of which
|
||||
rounds thirty-four and thirty-nine already rewrote and which siblings in
|
||||
this wave are likely to reach for again */
|
||||
load('ANDROID.md');
|
||||
rep(`The audio side had its own lag: \`dumpsys media.audio_flinger\` showed the
|
||||
track on the fast mixer with runs of ~55 ms underruns. The 8192-frame buffer
|
||||
above moved it to the normal mixer; the phone walk samples the underrun
|
||||
counters before and after and fails if they rose.
|
||||
|
||||
## Prerequisites (home machine, done 2026-09-12)`, ins('ins-r41-doc.txt'));
|
||||
save();
|
||||
|
||||
console.log('round 41 wired');
|
||||
Reference in new issue
Block a user