From 09f7e450a5ae8e06836aadaee281285508f106de Mon Sep 17 00:00:00 2001 From: Byron Gamatos Date: Sat, 11 Jul 2026 23:27:17 +0200 Subject: [PATCH] =?UTF-8?q?refactor(app):=20give=20formatTime=20a=20home?= =?UTF-8?q?=20=E2=80=94=20a=20leaf=20format.js,=20one=20fewer=20host=20hoo?= =?UTF-8?q?k=20(R3a)=20(#895)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit static/js/format.js (17). One function. Retires the formatTime hook: 12 -> 11. WHY A MODULE FOR ONE FUNCTION. formatTime was a host hook — loops.js and section-practice.js both reached back through the seam for it. It is ALSO, by pure accident of who calls it, inside the dependency closure of the library carve that comes next. Leaving it there would have made loops.js and section-practice.js import the LIBRARY in order to format a timestamp — nonsense, and a cycle waiting to happen. Same rule as the transport carve: a hook is a cycle you agreed to live with; an import is a dependency you actually have. formatTime has a real owner. It just isn't app.js, and it certainly isn't the library. Give it a home and both consumers import it directly. A leaf on purpose. Anything else that turns out to be a shared pure formatter belongs here too; nothing does yet (I checked — formatBadge, _safeImageUrl and _fetchJsonOrThrow have no callers outside the library), so nothing else is here. node 1040/1040, host contract 2/2, ESLint 0. Co-authored-by: Claude Opus 4.8 (1M context) --- static/app.js | 3 +-- static/js/format.js | 17 +++++++++++++++++ static/js/loops.js | 7 ++++--- static/js/section-practice.js | 3 ++- 4 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 static/js/format.js diff --git a/static/app.js b/static/app.js index bda30e3..6c0ad2f 100644 --- a/static/app.js +++ b/static/app.js @@ -127,6 +127,7 @@ import { toggleSectionPracticePopover, } from './js/section-practice.js'; import { configureHost } from './js/host.js'; +import { formatTime } from './js/format.js'; // The playback transport. These used to BE app.js — they are imported back now, and the // four modules that reached for them through the host seam import them directly instead. import { @@ -4821,7 +4822,6 @@ if (window.feedBack) { } -function formatTime(s) { return `${Math.floor(s/60)}:${String(Math.floor(s%60)).padStart(2,'0')}`; } @@ -6261,7 +6261,6 @@ async function checkScanAndLoad() { // tests/js/host_contract.test.js fails CI if this list and the host.* uses under // static/js/ ever drift apart. configureHost({ - formatTime, handleSliderInput, playSong, // count-in is a module now, so section-practice reaches it through the seam too — diff --git a/static/js/format.js b/static/js/format.js new file mode 100644 index 0000000..1a902a1 --- /dev/null +++ b/static/js/format.js @@ -0,0 +1,17 @@ +// Display formatters. A LEAF module: imports nothing. +// +// WHY THIS EXISTS FOR ONE FUNCTION. formatTime was a HOST HOOK — loops.js and +// section-practice.js both reached back through the seam for it. It was also, by pure +// accident of who calls it, inside the dependency closure of the library carve. Leaving +// it there would have made loops.js and section-practice.js import the LIBRARY to format +// a timestamp, which is nonsense, and a cycle waiting to happen. +// +// A hook is a cycle you agreed to live with. This one has a real owner — it just isn't +// app.js, and it certainly isn't the library. Give it a home of its own and both +// consumers import it directly. +// +// It is a leaf on purpose. Anything else that turns out to be a shared pure formatter +// belongs here too; nothing does yet, so nothing else is here. + +/** Seconds -> `M:SS`. */ +export function formatTime(s) { return `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, '0')}`; } diff --git a/static/js/loops.js b/static/js/loops.js index 312cd09..898f484 100644 --- a/static/js/loops.js +++ b/static/js/loops.js @@ -20,6 +20,7 @@ // fails CI if the hooks used here and the hooks app.js wires ever drift apart. import { esc, uiPrompt } from './dom.js'; import { _audioSeek, _audioTime } from './transport.js'; +import { formatTime } from './format.js'; import { host } from './host.js'; import { _setSectionPracticeMode, @@ -167,11 +168,11 @@ export function updateLoopUI() { const label = document.getElementById('loop-label'); const hasLoop = loopA !== null && loopB !== null; if (hasLoop) { - label.textContent = `${host.formatTime(loopA)} → ${host.formatTime(loopB)}`; + label.textContent = `${formatTime(loopA)} → ${formatTime(loopB)}`; document.getElementById('btn-loop-clear').classList.remove('hidden'); document.getElementById('btn-loop-save').classList.remove('hidden'); } else if (loopA !== null) { - label.textContent = `${host.formatTime(loopA)} → ?`; + label.textContent = `${formatTime(loopA)} → ?`; document.getElementById('btn-loop-clear').classList.add('hidden'); document.getElementById('btn-loop-save').classList.add('hidden'); } else { @@ -190,7 +191,7 @@ export async function loadSavedLoops() { sel.innerHTML = ''; for (const l of loops) { - sel.innerHTML += ``; + sel.innerHTML += ``; } if (loops.length > 0) { sel.classList.remove('hidden'); diff --git a/static/js/section-practice.js b/static/js/section-practice.js index 741ca58..338737b 100644 --- a/static/js/section-practice.js +++ b/static/js/section-practice.js @@ -28,6 +28,7 @@ import { audio } from './audio-el.js'; import { esc } from './dom.js'; import { _audioDuration, _audioTime, audioSeekGen } from './transport.js'; +import { formatTime } from './format.js'; import { host } from './host.js'; export function _sectionPracticeBarContains(el) { @@ -902,7 +903,7 @@ export function renderSectionPracticeBar() { _showSectionPracticeBar(bar); scroll.innerHTML = parents.map((p, i) => { const label = _formatSectionPracticeName(p.name); - const tip = `${label} (${host.formatTime(p.start)}–${host.formatTime(p.end)})`; + const tip = `${label} (${formatTime(p.start)}–${formatTime(p.end)})`; const kindClass = _sectionPracticeChipKindClass(p.name, i); return ``; }).join('');