A Hickey/Lowy review of kolu's mobile support — is mobile an encapsulated change behind a receptacle, or smeared across every consumer measuring the voltage?
An adversarially-verified read of kolu’s mobile support through two lenses: Rich
Hickey’s Simple Made Easy (is mobile complected through the app?) and Juval
Lowy’s volatility-based decomposition (is mobile an encapsulated change, or
smeared across every consumer?).
① The analogy — Lowy’s receptacle
Lowy uses household power to argue you decompose by what changes, not what it
does:
“Power in a house is highly volatile: AC or DC; 110 or 220 volts; 50 or 60 hertz; solar, generator, or grid. All that volatility is encapsulated behind a receptacle. When it is time to consume power, all the user sees is an opaque receptacle.” — Juval Lowy, Righting Software
The toaster never exposes the wires or measures the frequency — it plugs in.
Every appliance carrying its own voltmeter and deciding what to do with the raw
mains is what Lowy calls functional decomposition and Hickey calls
complecting. So the question is precise: does feature code plug into a
stable interface that already resolved “where am I running,” or does each
consumer measure the voltage (isMobile()) and branch itself? The answer for
kolu: two outlets, one good extension cord, and a lot of bare wire.
② The circuit as wired today
One source, two voltages, two clean outlets — then exposed mains:
Encapsulated — appliance plugs in blindBare mains — consumer measures the voltageTwo voltages for one threshold
③ Scorecard
Dimension
Grade
Note
Single source of truth
C
Each concept has one signal — but the breakpoint value was defined twice (JS 639px vs Tailwind sm: 640px); the comment misclaimed they match. fixed · #1088 — the JS query now derives from the --breakpoint-sm token.
Encapsulation
C
Two receptacles done right; ~18 consumers branch on the raw signal with no posture/capability layer.
Concept separation
B
isMobile (size) and isTouch (modality) are orthogonal; useViewPosture refuses to fold mobile in.
CSS vs JS discipline
D
18 JS signal-reads vs 4 Tailwind classes, 0 custom @media. CSS owns almost no structure.
Component duplication
B
Verification refuted the alarms: row/pip/metadata logic is already shared. Only reviewer-approved JSX shells diverge.
Consumer leakage
D
openInCodeTab forks intent inline; tips/commands/canvas each ask “am I mobile?”; the feature layer is functionally decomposed.
④ What’s wired right — the parts that already are electricity
⑤ Where the leads are bare — five leaks that survived verification
⑥ Where the analogy honestly breaks — a phone is not a 110-volt desktop
Some mobile volatility is not a different voltage of the same signal; it is a
categorically different appliance. A soft keyboard is not a 220V keyboard —
it’s a different input device with its own focus model and contenteditable target.
The pan/zoom canvas is genuinely unusable on a phone, so MobileTileView and
the drawers are a second, correctly-built product surface. This is why “just move
it all to CSS” is wrong:
Same appliance, different voltage → CSS. Spacing, inline-label hide/show, max-widths, density. Today kolu does almost none of this in CSS (18 JS reads vs 4 classes) — the recoverable ground.
Different appliance → JS, behind a seam. Soft-keyboard input surface, focus suppression, swipe nav, touch-scroll. Lowy’s demand isn’t “don’t branch” — it’s “branch in one named place, not as raw isTouch() poking xterm internals from a setup loop.”
There are four concepts wearing one word — viewport-size, touch-modality,
layout-posture, feature-availability. The first two are correctly receptacled; the
second two are functionally decomposed. The fix is to add the missing posture and
capability receptacles, not to erase the divergence.
⑦ The receptacle that should exist — keep the wiring, add three faceplates
Keep the wiring.isMobile and isTouch stay as the two axes — but unify the
rating: register the breakpoint once (Tailwind v4 @theme { --breakpoint-sm }
or a shared constant) and derive the JS createMediaQuery from the same number.
The 639/640 desync and the misleading comment both vanish. Then three named seams:
useRightPanel.reveal() — one verb that internally resolves drawer-open (mobile) vs uncollapse (desktop). openInCodeTab and every future producer call it and never read isMobile. ~5 lines; highest payoff-to-effort fix.
A resolved capability object — layout.supportsSpatialCanvas, showsAmbientTips, isCompact — computed once, each from the right axis. Consumers ask about capability, not pixels.
enableSoftKeyboardInput(term) — owns the contenteditable knowledge + xterm poking so Terminal.tsx calls one verb; re-key CodeTab’s touch-scroll off touch capability rather than viewport.
withKeyboardDismiss is the existence proof these work: a drawer plugs in by
passing its setter and never measures the voltage.
⑧ What was done — ranked by payoff ÷ effort
Action
Why
Effort
Payoff
Status
Unify the breakpoint (639px ⟶ one value)
Register --breakpoint-sm once + derive the JS query; fix the contradicting comment. Kills the desync band + orphaned ChromeBarsm:.
small
high
shipped · #1088
Add useRightPanel.reveal()
Move the isMobile fork out of openInCodeTab into one host-owned verb; mirrors withKeyboardDismiss. Near-zero blast radius.
small
high
shipped · #1088
A resolved capability seam
Replace scattered isMobile() feature checks with one resolved object, each keyed off the right axis.
medium
medium
shipped · #1088
enableSoftKeyboardInput(term)
Wrap the contenteditable surgery behind a named seam; re-key CodeTab touch-scroll off touch capability.
medium
medium
shipped · #1088
Do not extract the dock-row / metadata JSX shells
The volatile logic is already shared; only JSX shells diverge, for documented touch-target reasons two reviewers chose. A forced BaseRow would be worse.
small
guard
standing
The one-line answer at review time: mobile is electricity for the macro layout
switch and the keyboard-dismiss wrapper, and bare mains everywhere else. The
wiring discipline was good — two correctly-separated circuits — but neither
terminated in enough receptacles, and the one threshold was rated at two
voltages.
#1088 added the missing receptacles and unified the rating.