makepad-ports/makepad_ports_deep_assessment.md
andodeki bcd362ed0d Deep assessment v2 (brutal, evidence-based) + phased improvement roadmap
- makepad_ports_deep_assessment.md: architecture/design/performance/bugs/security/quality,
  scored 5.4/10 with file:line evidence; key findings: unpinned dev checkout (S1),
  commit-on-down + zero animators (D1/D2), undo deep-copies scene (P1), god-router +
  full chrome resync (A2/A3), copy-coupled UI tests (B3), test-shaped product (A4)
- plans/ROADMAP.md: Phases 0-5 mapping findings to plans 000-005 + engine hardening,
  tranche discipline per phase
2026-09-01 18:59:16 +00:00

193 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Makepad Ports — Deep Assessment (v2, brutal)
**Date**: 2026-09-01 · **Commit**: `3cfeaa2` · **Scope**: `makepad-ports/{rider,koboyo,insurance}` crates
(`app.rs`, `model.rs`, `canvas_view.rs`, tests, `setup_makepad.sh`, patch) · **Method**: full static read
with file:line evidence; coverage measured separately (see `docs/coverage/`). Supersedes the 6.0/10
in `makepad_ports_assessment.md` — that assessment predates the UI-craft audit and missed the
reproducibility hole.
## Verdict
**5.4 / 10.** A well-engineered *engine* wearing an unfinished *product*. The model layers are the
real thing — pure, documented, unit-tested state machines. Everything the user actually touches is
scaffolding: three parallel app monoliths with zero animation infrastructure, commit-on-touch
semantics, no keyboard path, hand-rolled id-string routing, and a build script that silently
tracks a moving upstream branch while claiming a pinned commit. The 140 green tests measure the
engine and the DOM-shape of the chrome; they cannot measure the interaction contract because the
interaction contract was never implemented.
| Axis | Score | One line |
|---|---|---|
| Architecture | 6.0 | One good idea (model/app split) executed consistently; everything above it is triplicated monolith |
| Design / UX craft | 3.0 | Zero animator tracks in 3,903 lines of UI; commit-on-down everywhere; pointer-only chrome |
| Performance | 5.0 | Fine at demo scale; three wrong shapes that cliff at real scale (undo snapshots, chrome resync, per-move relayout) |
| Bugs / correctness | 6.5 | Engine solid; risks concentrated at the app seam (multi-touch, panics-by-invariant, copy-coupled tests) |
| Security / reproducibility | 4.0 | Unpinned `dev` checkout is a build-breaking and supply-chain hole; `curl \| sh`; token hygiene OK-ish |
| Code quality | 6.0 | Excellent comments, honest READMEs; write-only aliases, no CI, no clippy/fmt gate |
| Tests | 6.5 | Real breadth on models; UI layer asserts copy strings, not contracts; product was bent to fit the harness |
---
## 1. Architecture
**The good — and it is genuinely good.** Every crate splits into a Makepad-free `model.rs`
(state machine, physics, registry, camera math) and an `app.rs` shell. Insurance's `SheetDrag`
ports the HTML's drag math verbatim with the constants documented (`model.rs:402406`); koboyo's
`model.rs` header maps every HTML behavior to its Rust owner (`model.rs:316`). This is why 115
of the 140 tests exist at all. Keep this. It is the foundation the rest of this document builds on.
**A1 — Three monoliths, zero shared code.** `app.rs` = 1,592 / 1,291 / 1,020 lines. Three separate
toast implementations (`insurance:1313`, `rider:1004`, `koboyo:589`), three ad-hoc DSL component
vocabularies (`Card`/`RoundBtn`/`IconTile` vs rider's `Mut`/`Wht`/`Deep`), three navigation
handlers. The README's hard-won "DSL lessons" list exists precisely because every crate re-learns
the same lessons with no shared crate to encode them. Cost: every fix lands three times or drifts.
**A2 — God-function routing.** `handle_actions` is 239 lines (koboyo), 164 (rider), 125
(insurance) of sequential `if ui.view(cx, &[id]).finger_down(actions)` chains. This is a
hand-rolled router with no table, no exhaustiveness, no type-safety: adding one button means
editing a 200-line function whose match order is load-bearing (`return`s short-circuit).
The `live_id!` names it dispatches on are duplicated strings with zero compile-time link to the
DSL — the exact bug class that already cost 6 rider tests (README bug 1).
**A3 — Retained-mode framework driven as immediate-mode.** `refresh_chrome` (insurance 89 lines,
rider 74) re-derives *all* chrome — active page, tab tints, dots, sheet visibility, labels — from
`self.flow` on essentially every action. Correct-by-brute-force, no dirty tracking, and it
structurally forecloses animation: any transition started on a widget is stomped by the next
full resync. The zero-animator finding in `skills/makepad-ui-craft/PORTS-AUDIT.md` is not an
omission on top of this architecture; it is a consequence of it.
**A4 — The product was bent to fit the test harness.** Insurance runs a 390×1280 window because
headless `click()` targets widget centers and content past the window edge passes `wait_visible`
but can't be clicked (README bug 8). The fix chosen was *remove scrolling from the product*
rather than *teach the harness to scroll*. That is the tail wagging the dog, and it means the
ports don't reproduce the one thing every 390×824 phone app has: a scroll container.
**A5 — Physics constants are folklore, not spec.** The sheet thresholds (26/12 @ 0.2 px·ms⁻¹,
+36/+18 @ +0.28, 140 clamp, 8 px tap) live as inline literals + comments. Nothing names them as
one `MotionSpec`; plan 003 will add velocity handoff and the constants will now matter in two
places. Name them once.
## 2. Design / UX craft
Fully documented with Before/After tables in `skills/makepad-ui-craft/PORTS-AUDIT.md`; summary of
the brutal part:
- **D1 (HIGH)** — ~40 tappables across all three crates fire actions on `FingerDown`
(`insurance:14271531`, `rider:11081214`, `koboyo:822883`). No tap-cancel, no press feedback,
and rider mixes commit-on-down Views with commit-on-up `Button`s on the same screens — identical
looks, different semantics.
- **D2 (HIGH)** — **Zero `Animator` tracks in 3,903 lines of UI.** Every toast, drawer, popover,
sheet-settle and screen change is a `set_visible`/`refresh_chrome` snap.
- **D3** — Chrome is pointer-only. Plain-View buttons have no focus state, no keyboard activation,
no semantics. Koboyo has a 76-line `handle_keys` for canvas tools while its own chrome can't
take focus. This fails jakubkrehel/better-accessibility wholesale, not in detail.
- **D4** — Emoji as icons (`insurance:139,264`), brand hex repeated per widget
(`#7b5cf6` at `insurance:122,170`…), nested radii unrelated to padding (`insurance:262`:
outer 20 / pad 14 / inner 13), koboyo focuses the search field before the drawer is visible
(`koboyo:608617`).
- **Correct restraint that must be preserved**: koboyo's 1:1 canvas gestures and wheel zoom;
rider's unanimated PageFlip; the sheet's 1:1 drag tracking.
## 3. Performance
Fine at demo scale. Three shapes are wrong and will cliff:
- **P1 — Undo is 60 deep copies of the world.** `koboyo/model.rs:555,564,575`:
`undo_stack.push(self.elements.clone())` on every committed edit, `HISTORY_MAX = 60`. A freehand
stroke with hundreds of points makes every subsequent edit clone it again — O(scene) memory and
time per edit, 60 retained copies. At toy scale invisible; on a real canvas this is the first
thing that dies. Command-pattern deltas or `Arc`-shared structural snapshots are the fixes.
- **P2 — Per-pointer-move relayout.** The sheet drag writes
`script_apply_eval!(…{ margin: Inset{…bottom: #(b)} })` per `FingerMove`
(`insurance:1570`) — a script eval + full relayout per event at input rate, for motion that
should be a shader-instance offset (repaint only). Same pattern class in the other crates
(9 `script_apply_eval` call sites total).
- **P3 — O(everything) chrome resync per action** (A3). Every tab press rewrites all chrome.
- **P4 — Per-keystroke allocation storm, wrong shape.** `koboyo/model.rs:281293`:
`search_tools` builds `format!("{} {} {}")` + `to_lowercase()` for each of 112 tools on every
keystroke, then `rebuild_drawer` re-marshals the whole `PortalList` (`app.rs:806807`).
Precompute a lowercase haystack per tool once; 112 items forgives you today, the shape doesn't scale.
- **P5 — Linear scans on the hot pointer path.** `erase_at`/`select_at`/`lasso_hit`
(`koboyo/model.rs:633,655`) scan all elements per pointer move while erasing/selecting. No
spatial index. Acceptable below ~1k elements — the threshold is not documented anywhere.
- **P6 — Test economics.** Headless UI tests boot the full app per test (~185 s per crate for 89
tests). That is a per-test tax that discourages exactly the interaction tests this codebase
is missing (see §6).
## 4. Bugs & correctness risks
- **B1 — No multi-touch guard on the sheet drag.** `insurance:15591585` matches
`Hit::FingerDown/Move/Up` without checking the digit/device that started the drag. A second
finger mid-drag re-enters `FingerMove` with alien coordinates. The skill's gesture checklist
calls this out explicitly; the HTML original effectively got this free from Pointer Events
capture semantics. *Not verified on hardware — flagged from code shape.*
- **B2 — Panic-by-invariant.** `koboyo/model.rs:852` `.position(...).unwrap()` (tool group must
exist in `groups`); `insurance/model.rs:647` `is_some() && .as_ref().unwrap()` (safe today,
fragile idiom — this is what `if let Some` is for). The invariants hold by construction *now*;
nothing enforces them at the seam where the registry is edited.
- **B3 — Tests coupled to copy.** 166 `text_exact`/`wait_visible`/`click()` locator uses across
the three `ui.rs` suites; README bug 9 records the `"🔋 89% Charged"` breakage. Any copy edit —
including plan 005's emoji removal — breaks UI tests by design. Locators should target ids,
not marketing strings.
- **B4 — Focus before existence.** `koboyo:608617` sets key focus onto a widget inside a
panel that isn't visible yet in the same dispatch. Works by accident of ordering today.
- **B5 — Radius/padding math unverified.** `insurance:262` and siblings (`:273+`) — flagged in
the audit; the side paddings were never measured. Either the math is wrong or it's accidental.
## 5. Security & reproducibility
- **S1 (the real one) — The build is not reproducible and trusts a moving target.**
`setup_makepad.sh` does `git clone --depth 1 --branch dev`**the tip of `dev`, unpinned**
while the README claims `b41e740`. Any upstream push can (a) break the patch, (b) change
behavior under test, (c) inject code you never reviewed into a build you run with local
network access. This contradicts the repo's own "verified from scratch" claim: what was
verified was *that day's* dev tip. Pin the SHA in the script (`git fetch origin b41e740 &&
git checkout b41e740`), fail hard if the patch doesn't apply.
- **S2 — `curl | sh` rustup install, unpinned toolchain** (script installs "stable", today
1.98.0; README recorded 1.97.1 — silent drift already happened). Pin via `rustup-init`
checksum or at minimum `--default-toolchain <version>`.
- **S3 — Vendored skills are agent-executed instructions.** `skills/vendor/**` is third-party
Markdown that agents in this workspace read as doctrine — a prompt-injection surface by
definition (Emil's own `improve-animations` Hard Rule 4 makes the point). Policy: `vendor/`
is *data*; only `skills/makepad-ui-craft/` is executable doctrine; diff vendor updates.
- **S4 — Credential hygiene.** The gitdab token was passed on process command lines (visible in
`ps` for the push duration) and pasted in the conversation; it was never written to disk or
git config. Rotate it; move to a credential helper or env var piped via stdin.
- **S5 — Application surface itself is small** (no network, no file IO beyond resources, zero
`unsafe` in the ports — verified by grep). The risk lives in the build chain, not the apps.
## 6. Code quality & tests
- **Q1** — Comment discipline is genuinely excellent (model headers mapping HTML→Rust, bug
postmortems in the README). This is the codebase's best habit. Keep it.
- **Q2** — Write-only naming: rider's `Mut`, `Wht`, `Deep` label styles; single-letter loop ids;
`fx` effect bags. Fine for the author-of-the-week, hostile to everyone after.
- **Q3** — No CI, no gate. 140 green is enforced by nothing: no workflow, no pre-push hook, no
clippy, no rustfmt config. The setup script is the CI, and it runs on demand only.
- **Q4** — Test pyramid is honest at the bottom (78 unit + 37 integration on pure models), thin
and brittle at the top (25 UI tests asserting string presence and screen reachability). There
are **no interaction-contract tests** — tap-cancel, drag-release-settle, toast-interrupt —
because the contracts don't exist yet (D1/D2). Plans 001003 add both the behavior and its tests.
- **Q5** — Coverage was never measured before this assessment. The instrumented run and its
gaps live in `docs/coverage/COVERAGE.md`; uncovered branches found there get tests in the same
tranche (see roadmap Phase 0).
## 7. Priority matrix
| # | Finding | Sev | Owner plan |
|---|---|---|---|
| S1 | Unpinned upstream checkout | HIGH | Phase 0 |
| D1 | Commit-on-down, no press contract | HIGH | Plan 001 |
| D2 | Zero animation infrastructure | HIGH | Plans 000/002004 |
| A2/A3 | God-router + full chrome resync | HIGH (enabler) | Phase 5 |
| P1 | Undo deep-copies the scene | MED (cliff) | Phase 5 |
| D3 | No keyboard/focus path in chrome | MED | Phase 2 |
| B1 | Multi-touch drag hijack | MED | Plan 003 |
| B3 | Copy-coupled UI tests | MED | Phase 0/5 |
| P2 | Per-move relayout | MED | Plan 003 |
| A4 | No scroll container (test-shaped product) | MED | Phase 5 |
| S2/S4 | Toolchain pin, token rotation | MED | Phase 0 |
| P4/P5 | Search allocs, linear scans | LOW (document threshold) | Phase 5 |
| Q2/B2 | Naming, panic idioms | LOW | Phase 5 |
Execution phases: `plans/ROADMAP.md`.