makepad-ports/makepad_ports_assessment.md
andodeki 7ddd75fb70 Makepad 2.0 ports: rider, koboyo, insurance (+ HTML sources, assessments)
Three single-file HTML apps ported to Makepad 2.0 (dev b41e740) script_mod! DSL:
- rider (ride.html): 21 unit + 9 integration + 9 headless UI tests
- koboyo (koboyo.html): 29 + 14 + 8
- insurance (insurance.html): 28 + 14 + 8
All 140 tests green. Includes headless-Linux platform patch, full README,
setup script, and three brutal codebase assessments with execution plans.
2026-08-04 05:07:18 +00:00

23 KiB
Raw Permalink Blame History

makepad-ports (rider / koboyo / insurance) — Brutal Professional Assessment

The third assessment in this series, this time of our own work: the three Makepad 2.0 port crates built in this workspace from ride.html, koboyo.html, insurance.html. Same rubric, same severity scale, no home-team discount.

Evidence base: every claim below was re-verified against the files in /home/user/makepad-ports/ today (grep/LOC quoted inline), plus this session's build and test logs.

Metric Value
Total code 8,652 lines across 3 crates (wc -l, 2026-07-26)
Largest files insurance/src/app.rs 1,592 lines, koboyo/src/model.rs 1,357, rider/src/app.rs 1,291
Tests 140 green: rider 21+9+9, koboyo 29+14+8, insurance 28+14+8 (unit+integration+UI)
Test wall time ~7 min for the three UI suites, serial
Upstream patches carried 6 (5 headless-Linux platform fixes + 1 registration) — not upstreamed
Compiler warnings in our crates 2, unfixed (makepad-example-koboyo lib)
Correctness gate none — no CI, rebuilds happen manually via setup_makepad.sh

Scorecard (010, as product code, not as "a good attempt"):

Axis rider koboyo insurance Suite
Architecture 6.0 6.5 5.5 5.0 (no shared kit — see A1)
Code quality 6.5 6.5 6.0 6.3
Bugs / correctness 6.5 5.5 5.0 5.5
Performance 6.0 5.5 6.0 5.8
Design / fidelity 7.0 6.5 5.5 6.3
Security / privacy 6.0 6.5 5.0 6.0 (mostly N/A surface — see §6)
Test strategy 7.5 7.5 7.0 6.5 (quantity masking coverage holes — §7)
Overall 6.5 6.4 5.8 6.0 — a strong prototype suite that is not yet an engineering project

Credit, once: the three-layer test pyramid (pure model → black-box → headless UI) is real and green from a cold rebuild; the model-first split worked exactly as designed (every behavioral bug this session was catchable in unit tests before any app boot); the drag physics of insurance's sheet is a faithful, testable port rather than a vibe; and the README documents its own failure modes, which is rarer than it should be. Now the problems.


1. Architecture

  • A1 (High) — Zero code reuse across three crates that are 70% the same program. The toast system (show_toast + generation + Timer + view toggle) is copy-pasted in all three apps (grep -l "fn show_toast" → rider, koboyo, insurance). Palette primitives (Ink/Mut/Wht labels), Card/RoundBtn shells, tab-bar-with-active-dot pattern, "refresh_chrome re-syncs everything", the NavEffect/Fx side-effect idiom — all reimplemented two to three times each, with three incompatible variations: rider returns a NavEffect struct, insurance uses bare method calls, koboyo uses CanvasAction events. There is no shared ports-ui-kit crate and no ARCHITECTURE.md stating the conventions, so the "rulebook" lives in the author's head. Crate #4 will invent a fourth toast.
  • A2 (High) — The single source of truth is honored in words, not in data. Model constants (e.g. POLICY_CARDS, BUY_CARDS, tab labels) contain the canonical strings, but the DSL markup in app.rs retypes nearly all of them as literals. Nothing ties "Edit Your Policy Details" in the DSL to POLICY_CARDS[0].title; only the subset a UI test happens to assert is pinned. The suite's core claim — "model owns truth, UI is thin" — is actually only true for the dynamic sections (chat list, tool drawer, chip/page/chip styling). Everything static can drift silently.
  • A3 — The app↔model sync is a full O(everything) refresh per event. refresh_chrome restyles tab dots, re-evals chip colors, re-sets heights and margins on every single action, via interpreted script_apply_eval! calls. Correct, but it's the nuclear option that hid a coupling smell: handlers don't know what their action dirtied, so everything must be dirtied. This pattern will not survive a screen with 200 dynamic elements.
  • A4 — Event handling uses the same two-lane mess I criticized upstream. insurance's grab drag uses event.hits_with_capture_overload(cx, area, false) while every other control uses finger_down(actions) — two disjoint input models in one app, chosen because the first attempt (Event::FingerDown) doesn't even compile against makepad 2.0. The port inherited upstream's API fragmentation without adding a local abstraction over it.
  • A5 — Platform/chrome architecture is per-app improvisation. rider: inline overlays for status bar/home indicator/toast. insurance: seven hand-ordered flow: Overlay layers where z-correctness depends on declaration order (the backdrop↔sheet boundary bug bit exactly here). koboyo: rail/flyout/zoom widgets positioned against a hardcoded (500,350) center (VIEW_C, app.rs:30). No shared "chrome scaffold" exists, which is why these bugs keep recurring per-app.
  • A6 — The workspace is not a project. No root Cargo.toml for the ports (they must be injected into the upstream workspace to build), no CI, no rust-toolchain.toml, one setup shell script as the entire build system. "Clone makepad, apply patch, rsync crates in" is a bootstrap hack that has quietly persisted into being the architecture.

2. Code quality

  • Q1 — DSL files are walls. 1,291/1,020/1,592 lines of script_mod! markup each, single startup() bodies, content inlined. Components were extracted only where forced by mechanics (PortalList rows, custom widgets); cards that repeat 4× (insurance category tiles, sheet action tiles, agent rows) are copy-paste twins with one differing literal each. insurance's app.rs at 1,592 lines is the worst file in the suite and it grew that way in one sitting.
  • Q2 — Test files have the same duplication. UI tests repeat the boot-one-app-per-test pattern with hand-copy-pasted journey prefixes (in insurance, reaching the sheet's Agents tab requires the same 3-step preamble as 2 other tests); there are no shared journey helpers.
  • Q3 — Two unfixed warnings in koboyo's lib (observed in the green build log: "makepad-example-koboyo (lib) generated 2 warnings"). The suite ships with warnings in a repo whose README demands upstream warning hygiene. Physician, heal thyself.
  • Q4 — Magic geometry without provenance. VIEW_C=(500,350) is a hardcoded half of a 1000×700 assumption; insurance's FAB at margin{bottom: 47} is 14+74-62/2+… computed nowhere; rider status-bar offsets are tuned literals. These are the numbers you change when they break, not because you understand them.
  • Q5 — Naming/doc drift between crates. NavEffect vs Fx vs none; refresh_chrome in two apps vs ad-hoc refreshes in koboyo; IDs (pg_home vs page_home vs views without page prefix). No lint config, no fmt check, no CONTRIBUTING.
  • Q6 — Comments document the what, rarely the which-approximation. Reader cannot tell from the code that #6a48f0 is standing in for a 3-stop radial gradient, that the QR view is fake, or that the backdrop is opaque where the HTML is 36% translucent… without reading the README. The approximation ledger belongs in the code at the point of deviation.

3. Bugs (verified, severity-ranked)

  • B1 (Critical, build): setup_makepad.sh clones a floating HEAD and patches it with a diff cut against a pinned commit. Line 27: git clone --depth 1 --branch dev …, then applies a patch whose context is b41e740. The next upstream commit can silently break either the patch or, worse, apply while shifting behavior. It worked three times in a row this week by luck of upstream not moving. Fix is mechanical and currently absent: git fetch --depth 1 origin b41e740… && git checkout FETCH_HEAD.
  • B2 (High, koboyo): phone-mode is implemented, tested… and dead. The model's breakpoint logic has unit coverage (breakpoint_matches_media_query, journey_breakpoint_matches_html) but app.rs wires only the desktop chrome; there is no phone bottom-bar in the UI (grep-verified: no phone bar ids in app.rs, no resize handler). A user-facing feature that exists only as passing tests.
  • B3 (High, insurance — and latent in rider): the app does not fit its own specified form factor. The HTML is a 390×824 phone; the port runs 390×1280 with no scrolling at all, because that's what made the center-only click tests deterministic. On the actual 824 px viewport, Home/Buy content is unreachable below the fold — and there is not even a failing test to tell you, because every UI test runs at 1280 px. We diagnosed source content overflowing at y=1054 (old run) and then changed the window instead of the app.
  • B4 (Medium, insurance): shipped the exact dead-affordance problem we condemned in the HTML review, with extra steps. search_input is a live TextInput users can type into that filters nothing; filter_btn is an inert label that looks like a control; bell_* exist ×4 with ids but zero handlers; hero_arrow is an id'ed RoundedView with no action; the policies/buy "View All" sec-links are static text styled like links. In the insurance.html assessment we called inert controls "a mock wearing a product's clothes" — the port preserves all of it and calls it fidelity.
  • B5 (Medium, insurance): the benefits-chip UI test is vacuous. It clicks Motor, clicks Featured, and asserts… that "Driving Score" is still visible — i.e., that clicking a chip didn't delete the screen. Selection state is visually inexpressible in the locator API, so the only end-to-end verification of the most interactive widget on the screen is the color of a rounded rect nobody reads. Model tests cover logic; the rendering of selection has no effective verification.
  • B6 (Medium, suite): zero handling of window resize in all three apps. No WindowResize handling (grep-verified); rider/insurance fixed 390-ish windows, koboyo's zoom centered on a hardcoded point.
  • B7 (Medium, insurance): backdrop opaqueness vs source. Set to solid #10101a to dodge a hit-test boundary problem discovered late (sheet top edge == backdrop center at peek); source is 36% translucent. A design regression introduced by a testability workaround, currently undocumented in the code.
  • B8 (Low, rider): the "map" is a procedural doodle. Fixed streets, fixed park, fixed route polyline hardcoded in a pixel shader (MapView draw_bg). It looks like San Diego only from a distance. Fine for a demo; catastrophic if anyone mistakes it for a functional map surface.
  • B9 (Low): emoji-as-iconography throughout insurance/koboyo DSL (🔔🛡💬📍👨🏾…). Headless and many Linux font stacks render missing-glyph boxes; no fallback configuration anywhere; UI tests can't see it (they assert text presence, and a tofu box "renders" fine).
  • B10 (Low): data provenance. Mock "agents" carry real-world Kenyan given names + surnames and an ID number/plate lifted from the HTML with zero synthetic-data marking — same PII-adjacency flagged in the insurance.html assessment.
  • B11 (Low): patch rot. The 5 platform patches have no upstream PR tracked, no expiry condition, and live as an unversioned .patch file; Phase 0.5 of the upstream plan literally lists upstreaming — it hasn't happened.

4. Performance

  • P1 — Every click runs interpreted script. Dynamic styling goes through script_apply_eval!, i.e. the makepad script VM, per state change — including per pointermove during sheet drags (insurance margin updates). It works (and shows the VM's point), but the drag hot-path should mutate the walk directly (widget.apply_over(...)-style) or pre-computed animator keys; interpreted eval at 120 events/s is the opposite of free.
  • P2 — koboyo canvas redraws the world per repaint. No viewport culling or spatial index is visible in canvas_view.rs's draw loop; undo history is capped at 60 (good) but element count is uncapped and untested — the suite's "infinite canvas" has never met 10,000 elements. No benchmark exists anywhere in the ports.
  • P3 — Full-tree refresh per action (A3): each go() also re-points PageFlip, rewrites tab colors and re-evals option cards — trivial at this scale, pattern-costly forever.
  • P4 — Test suites don't share boots. 25 UI tests → ~25 child app boots: ~7 minutes wall, serially. As a regression gate it's already past the "I'll just skip it" threshold.
  • P5 — PageFlip instantiates all pages eagerly (9 pages in rider) — acceptable at demo size, worth noting as the non-scaling default.

5. Security & privacy

Limited surface — no network, no eval, no persistence, no secrets. Honest findings are few but real:

  • S1 — The studio-protocol test channel boots app binaries over stdin/stdout with no isolation; dev-only, but keep it there: none of that protocol should ever ship in a release profile (same warning given upstream).
  • S2 — Synthetic-PII hygiene (B10): names/IDs must read as fixtures.
  • S3 — Text inputs (msg_input, search_input, drawer search) are rendered-only today; the pattern for wiring them later must be set before someone interpolates them anywhere capable — currently no such rule exists in the repo.
  • S4 (positive) — The ports add no new unsafe code beyond what custom draw shaders require via the framework's own derives; the only memory unsafe reachable is upstream's S1 convention, which we obey (and tested around) rather than fixed.

6. Design & fidelity

  • D1 — "Faithful port" is an overclaim governed by an unwritten contract. The README lists the big approximations, but there is no per-element fidelity ledger: solid fills stand in for 3-stop gradients (hero/drive/ID/avatars), backdrop opacity changed, QR is fake, the car is 6 rounded rects, portrait/avatars/QR are decorational, map is fictional, emoji ≈ SVG icons, status-bar clock is static, bell dots have no unread backing, search is fake. indistinguishable-from-source screenshots would surprise nobody reviewing this for the first time.
  • D2 — Inconsistency between the three ports is jarring. rider: dark theme, custom map shader, chat widget. koboyo: dual theme + fully custom shader widgets + gesture engine. insurance: light theme, plain views, emoji. Same author, same week, three visual/technical dialects — because A1 means every crate reinvents its kit. A casual reviewer should be able to tell these came from one framework; today they can't.
  • D3 — Design-fidelity choices were made for testability, not UX, and are presented as product decisions (1280-tall window; opaque backdrop). The tradeoffs were correct for the goal of green tests — they are mislabeled as the app design.
  • D4 — The ports inherit, verbatim, the sources' UX sins (three conflicting "Quick Actions", insurance's identity-confused IA, chips that filter nothing) with no "port vs fix" policy documented. We audited insurance.html for these on Monday and shipped them on Tuesday.
  • D5 — No runtime theming token layer: colors are hundreds of inline literals; a brand change is a regex across 3,900 DSL lines (koboyo's model-driven dark mode proves we knew better and didn't generalize it).

7. Test strategy — the suite's pride, honestly examined

  • T1 — 140 tests, but the distribution flatters. A large block of unit/integration assertions are "literal" == other_literal parity mints — valuable as drift guards for a port, but they test the keyboard more than the software. The count overstates behavioral coverage.
  • T2 — "Integration" tests integrate nothing. Every tests/integration.rs imports only model::*: they're a second unit tier with journeys. The actual integration seam — app.rs ↔ model — is covered only by the 89 UI tests per crate, i.e. ~5% of the suite guards the largest risk surface.
  • T3 — The physics port is verified against itself. SheetDrag tests assert thresholds we transcribed; there is no golden trace comparing against the HTML's actual JS behavior across a randomized gesture corpus (a transcription typo at 26px would produce a green suite of a wrong port).
  • T4 — No visual/golden testing. Gradients→solids regressions, tofu emoji, misalignment: invisible to the suite. makepad_test screenshots exist only on failure and are never diffed against goldens.
  • T5 — No negative/stress paths in UI tests: double-tap storms, mid-drag cancels, timer races (toast during sheet drag), text-input edge cases. Happy paths only.
  • T6 — The form-factor lie (B3) is a test-architecture failure first: there is no viewport-fit assertion ("no interactive widget rect outside 0..824") that would have failed loudly instead of quietly resizing the window.
  • T7 — No CI. All 140 greens exist because a human ran the script and reported back. Nothing stops tomorrow's edit from breaking them.

8. Execution plan — turn the demo suite into a project

Phase 0 — Repo → project (12 days)

# Task Acceptance
0.1 Pin the clone (B1): fetch b41e740 by sha, checkout FETCH_HEAD; add rust-toolchain.toml; root workspace that drives the upstream injection instead of a script that does it blind Fresh run byte-deterministic; upstream HEAD bump is a choice, not an accident
0.2 Wire the 2 koboyo warnings to zero + RUSTFLAGS=-Dwarnings equivalent in the script Suite compiles warning-free
0.3 One-command gate: make check = fmt + clippy + all 140 tests; document the ~7 min budget A stranger can verify everything in one step

Phase 1 — De-duplicate into a kit (23 days)

# Task Acceptance
1.1 Extract ports-ui-kit: palette primitives, Card/RoundBtn/TopBar/TabBar+dot, ToastHost (show/gen/timer/view), chrome overlay scaffold, NavFx idiom, UI-test journey helpers Each app.rs shrinks ≥30%; toast exists exactly once (A1/Q2)
1.2 Single source for static content: boot-time refresh writes model constants into DSL labels (extend the existing dynamic path), OR generate the repetitive tiles from data Model const ↔ visible text parity asserted suite-wide (A2); deleting a const deletes it from screen
1.3 Kill the interpret-per-event paths: drag translates via direct walk mutation; refresh_chrome gets a dirty-flag map per handler (A3/P1) Sheet drag does zero script_apply_eval! calls per move

Phase 2 — Fidelity & correctness fixes (35 days)

# Task Acceptance
2.1 Fidelity ledger per crate (FIDELITY.md): element → faithful/approximated/inert + reason; move to-source deviations into code comments at the site (Q6/D1) A reviewer can audit fidelity without tribal knowledge
2.2 Dead affordances policy (B4/D4): wire or remove — bells→toast or deleted, filter deleted, search filters categories or goes, inert "View All"s removed Interactive inventory = 0 inert elements; mirrors the rule we set for the HTML
2.3 Viewport truth (B3/B6): real scroll containers for insurance pages; form-factor UI test at 390×824 asserting every interactive widget's rect is inside the viewport; resize handling for all three apps; koboyo VIEW_C → measured center on resize (B8) insurance runs at 824 px fully reachable; resize storm test green
2.4 Wire koboyo phone chrome (B2) or delete the model feature + its tests (both acceptable — pick one and say which) No tested-but-dead features
2.5 Backdrop translucency restored without the hit bug (B7): shrink backdrop geometry to exclude the sheet slab Matches source visuals; sheet drag + backdrop click tests both green
2.6 Emoji → drawn vector icons at least for chrome (B9: tab bar, bell, shield, lock) — rider already proves drawn shapes survive headless Zero emoji in chrome paths; tofu risk eliminated

Phase 3 — Test architecture (24 days, overlapping)

# Task Acceptance
3.1 Golden traces for gesture physics (T3): scripted corpus of drags/flings/taps with expected transitions captured from the HTML's JS (run the original in a headless browser once, freeze expectations) SheetDrag verified against source behavior, not our transcription
3.2 Golden screenshots per page & sheet state (T4): hash or SSIM-threshold against checked-in references; document update flow Visual regressions fail loudly
3.3 Model↔DSL parity test (A2): UI test asserting every string in the model's public tables appears in the widget tree exactly where expected Static-drift class of bug erased
3.4 Negative + stress journeys (T5): double-tabs, drag-cancel, toast-during-drag, resize mid-sheet, input fuzz (emoji/RTL/long strings) Suite grows <20% and targets seams, not literals
3.5 Chip selection made end-to-end observable (B5): overlay-free state presentation — e.g. drive-card title/battery sourced from per-chip data — then assert that Feature verified through pixels, not faith

Phase 4 — Upstream & product decisions (weeks)

  1. Upstream the 6 patches with the headless CI leg (B11); delete them locally on merge; until then the patch file gets a header: purpose, expiry, owner.
  2. Product question, answered in writing: are these ports throwaway fidelity demos (then stop polishing and archive), or apps (then D2/D4 — one design language, the IA sins fixed, and real scroll + resize land first)?
  3. Performance floor (P2/P4): koboyo canvas culling + 10k-element benchmark; UI-suite boot sharing or parallelism to <90 s/crate; a criterion bench for SheetDrag/zoom math because there is none today.
  4. Privacy pass (S2): all people-shapes renamed to EXAMPLE_* fixtures; a SYNTHETIC_DATA.md stating no real persons are referenced.

Definition of done

  1. Stranger runs one command and gets 140+ green, deterministic, under 5 minutes, zero warnings.
  2. Deleting a model constant visibly deletes UI (single source reality).
  3. insurance at 390×824, koboyo at 375×667, rider at 390×700 — all fully operable, with a test that proves it.
  4. Fidelity ledger says exactly what is not the source and why, element by element.
  5. No inert interactive-looking element in any of the three apps.

Bottom line

As proof that Makepad 2.0 can host real apps, this suite succeeds — three different app shapes, one DSL, all behavior captured in testable models, 140 green tests from a cold machine. As engineering, it is three well-tested prototypes sharing neither kit, conventions, CI, nor a reproducible build, with its two most user-visible cheats (tall window, dead affordances) introduced by the test strategy itself. The single most important next commits are embarrassingly small: pin the clone, fix two warnings, extract the toast. Everything after that is deciding — in writing — whether these are demos or products, because the current repo insists on being graded as the latter while being built as the former.