<!--
SPDX-FileCopyrightText: 2026 Gary Frattarola <garyf@parkviewlab.ai>
SPDX-License-Identifier: CC-BY-4.0
-->

# Rust port — moving PensaGrex off Electron

Status: under study. This is an in-flight idea held for exploration, not a
commitment; `in-flight_ideas.md` carries the index entry. Everything below is
weighed against `northstar.md`. The option write-ups draw on a dedicated research
pass (sources at the end of each section); the crate versions and licenses were
last verified against crates.io on 2026-08-16, and the design reasoning dates to
July 2026 except where a later revision is marked.

## Why consider it

- Electron forces JavaScript and bundles Chromium. The author was never content
  with the JS constraint, and the artifacts are large: v2.1.0 shipped installers
  of 104 to 134 MB, and a running Electron app idles at roughly 200 to 400 MB.
- The org is Python-first (FastAPI services) and comfortable with a federated,
  ROS-like split of languages across a wire. Rust marries with Python at least as
  well as C++ does: in-process through PyO3 with maturin building the wheels, and
  at the service seam over HTTP, gRPC, or MCP.
- The direction the author has settled on: PensaGrex should become a 100% Rust
  app.

## What "100% Rust" actually means (the first fork)

The phrase forks into two very different projects, and the choice sets the scope
of everything else.

- Tauri is not 100% Rust. Tauri gives you a Rust backend, but the interface stays
  HTML, CSS, JavaScript, and (here) hand-drawn SVG, running in the operating
  system's webview. Rust is the backend language only.
- A Rust-native GUI toolkit is 100% Rust: no JavaScript, no webview.

Only the second is literally 100% Rust. It is also the larger rewrite, because it
discards the renderer, not only the `src/main` backend.

## What the current architecture means for any port

- The renderer imports nothing from Node or Electron; it sees disk only through
  the `pensagrex` preload bridge (about fifteen methods). Under Tauri it ports
  mostly as-is (the transport at the bridge seam changes from Electron IPC to
  Tauri `invoke`/`listen` or local HTTP); under a Rust-native GUI it is rewritten.
- The shared model in `src/shared/` is roughly 1,100 lines of non-test code, of
  which `mutations.js` alone is 658 (with a 661-line test file). It is imported in
  three places, not one: the main-process authority (`taskService.js`), the
  renderer's no-Electron fallback (`bridge/api.js`), and the renderer's view spine
  (`app.js`, which calls `buildModel` to derive what it lays out). This shared
  reuse is the crux of the whole port.
- `store.js` (file I/O, JSON5, atomic write-to-temp-then-rename, settings,
  library-root bounds-checking) ports to Rust as routine work, with one caveat
  below (JSON5 round-trip fidelity).
- The MCP server ports to Rust via rmcp, the official Rust MCP SDK (Apache-2.0,
  v3.1.2 on 2026-08-07, tracking MCP spec 2025-11-25). It supports the
  Streamable-HTTP server transport through `StreamableHttpService` on an axum
  router, so the loopback endpoint on `127.0.0.1:35899` can be rebuilt faithfully
  and, being in the same process as the authority, calls the mutation functions
  directly rather than across a language boundary.

### The JSON5 round-trip caveat (binds every Rust option, on axiom 7)

*Narrowed by model v3.* The axiom moved from 6 to 7 when the northstar was amended,
and it now names plain JSON rather than JSON5. A plain-JSON file has no comments to
strip, so what a serde round trip can still lose is key order and hand-formatting,
which is a preference rather than content. The analysis below was written against
JSON5 and is kept because it is what would bind the choice again if the file ever
carried comments.

The axiom then read that the forest file is the user's: plain JSON5, portable,
hand-editable. The popular Rust `json5` crate is unmaintained (RUSTSEC-2025-0120).
The maintained alternatives (`serde_json5`, `jsonc-parser`) are serde-based, and
serde reconstructs its output from the data model, so it does not by default
preserve comments or hand-formatting on write. A naive Rust `store` would therefore
quietly strip a user's comments and layout from `forest.json5` on the next save,
which is a direct violation of it. Any Rust store must use a format-preserving
write path (a concrete-syntax-tree editor rather than a plain serde round-trip) and
this must be verified, not assumed. It is a small, bounded task, but it is a real
one.

## The model re-homing decision (the real center of a hybrid)

Moving the authority off Node forces a decision about the model, and it is where a
"hybrid" hides a real tax. Today the model is imported verbatim by both the
authority and the renderer because both are JavaScript, so the sharing is free. If
the authority becomes Rust while the UI stays web (the Tauri option), the sharing
is no longer free, and three shapes present themselves:

1. Port only the mutation half to Rust for the authority, and keep `buildModel`
   (pure layout derivation) in JavaScript in the webview; after each edit Rust
   returns the updated forest and the webview lays it out. One mutation authority,
   but the model is split along the mutate/derive seam.
2. Move everything, derivation included, into Rust and return the webview a fully
   derived scene to paint. Least JavaScript, but it discards `buildModel` and
   kills the in-browser fallback outright.
3. Keep the full JS model for the fallback and also port it to Rust for the
   authority. The worst case: two implementations of the same mutation semantics
   that must stay bit-identical forever.

The elegant single-source model is a casualty of the language boundary whichever
shape is chosen; the only question is how much duplication to accept and whether
the browser fallback survives. In a 100% Rust GUI this decision disappears by
construction: the model is one Rust crate imported by both the authority and the
GUI, single-process, no bridge, no second copy. That is the strongest architectural
argument for going all the way to Rust rather than stopping at a hybrid. Whatever
the target, the ported model must be re-validated against the existing 661-line
mutation test suite, either by porting the tests or by differential-testing the
JavaScript and Rust implementations against the same fixtures until they agree.

---

# The three designs

## Design A — Tauri hybrid: web UI, Rust core

Keep the UI in web technology (the existing hand-drawn SVG renderer, the note
editor, the dialogs) running in the OS webview, and reimplement the store, the
task authority, and the MCP server in Rust as the Tauri backend.

The renderer carries over almost unchanged: it is already Node-free and speaks
through the preload bridge, which in Tauri becomes a thin JavaScript shim over
`invoke` (request/response) and `listen` (pushed updates) backed by
`#[tauri::command]` functions. There is no Node in the webview at all. The Electron
main process re-homes into the Rust core as the single task authority, and the MCP
server becomes an rmcp `StreamableHttpService` on an axum router at
`127.0.0.1:35899`, in-process with that authority, pushing live updates to the
webview through Tauri events exactly as today. Distribution is solid and matches
the no-cloud posture: Tauri 2 signs on macOS with a Developer ID and notarizes via
the App Store Connect API (the apparatus conception-space already runs), signs on
Windows, and ships a first-party updater that verifies artifacts with minisign
(Ed25519) against a static `latest.json` manifest hostable on GitHub Releases, no
dynamic server. Binaries land around 10 MB with idle memory near 40 MB, against
Electron's 80 to 150 MB and 200 to 400 MB.

Costs. This is the design in which the model re-homing tax above is unavoidable and
the single-source model is compromised. Fidelity is the other exposure: Tauri uses
three webview engines (WKWebView on macOS, WebView2 on Windows, WebKitGTK on
Linux), and WebKitGTK is the weak one for CSS, SVG, and font rendering, precisely
the workload the bespoke subway scene with heavy pan and zoom stresses, so intent 2
(faithful, legible reproduction) now depends on real cross-platform visual testing
that Electron's uniform Chromium made unnecessary. The MCP server must enable
Host/Origin allow-listing: rmcp had a loopback DNS-rebinding advisory
(GHSA-89vp-x53w-74fx, fixed in 1.4.0), the same class of Host-header bug we already
fixed once in the Node server, so Rust does not make it disappear. The authority is
reachable by two paths (Tauri IPC for the renderer, loopback HTTP for external MCP
clients) that must enforce validation consistently. And the JSON5 round-trip caveat
applies.

Verdict: viable and mature, and the cleanest form of a Tauri adoption, but not 100%
Rust, and it keeps both the webview-fidelity QA and the model-duplication hazard.
Its best role is an interim step toward Design B, not the destination.

Stack (all MIT or Apache-2.0): Tauri 2.x, rmcp, axum/tower/hyper, a maintained
JSON5 crate with format preservation, the Tauri updater plugin with minisign.

## Design B — 100% Rust GUI (egui or iced): no JavaScript, no webview

For PensaGrex specifically, the larger rewrite may be the more coherent end state
rather than the less. The interface is not HTML layout; it is a bespoke subway-map
vector scene with pan, zoom, and custom node glyphs that are already hand-drawn as
SVG. An immediate-mode Rust GUI like egui is built precisely for custom painting
under a camera transform, so re-expressing the forest as Rust drawing calls is a
natural fit rather than a fight, and it dissolves the webview-fidelity problem
entirely: one renderer you own, no WKWebView-versus-WebView2 variance, no bundled
Chromium. The genuinely web-shaped parts (the markdown note editor, the dialogs,
the domain switcher) become native widgets, which is the tedious part rather than
the hard part. The cost is honest and large: the renderer and interaction layer are
rewritten alongside the model and the store, so it is the widest scope of any option
discussed. But it yields the most unified result, which is what the direction is
after: one language, one rendering path, no JavaScript, no webview.

The model re-homing question disappears here. Because egui and iced are
single-process with no webview, the model has exactly one home, a workspace `core`
crate imported by both the store/MCP authority and the GUI layout code; there is no
bridge, no IPC serialization of task operations, and no risk of divergence. View
state (camera, zoom, collapse; axiom 9) stays in the GUI layer and is never written
to the forest file, which egui's `Scene` state or an iced `Canvas` transform keep
client-side naturally. Nothing ports at the code level (JS to Rust is a rewrite);
the largest single piece of work is not the model but reproducing the Googie skin,
glyphs, gradients, and azure/navy ground faithfully enough to satisfy intent 2.

### egui versus iced

Both are permissively licensed and viable; they differ in ways that matter here.

- egui (with eframe; 0.36.1, 2026-08-07; MIT or Apache-2.0). Immediate mode. Pan
  and zoom come turnkey via `egui::Scene` (added ~0.31). `TextEdit` ships built-in
  undo/redo and IME, and `egui_commonmark` gives a turnkey markdown preview.
  AccessKit accessibility is on by default through eframe. Weaknesses: epaint fills
  only convex polygons, so the two silhouettes whose edges bow inward (the project
  hull's top and the marquee's four sides) need lyon tessellation and a `Shape::Mesh`
  rather than a plain fill; `TextEdit` has no viewport culling, so very large notes
  degrade badly (reported near 1 fps at ~1 MB); and text drawn through `Scene`'s
  layer transform can blur at high zoom unless zoom is driven through painter
  coordinate mapping. (Its thin gradient support is not a weakness for this skin, a
  point the "Rendering the diagrams" subsection below settles: the skin carries no
  gradients.)
- iced (0.14.0, 2025-12-07; MIT). Elm-like, retained. Its `Canvas` redraws geometry
  per frame, so glyphs stay crisp at zoom, and its lyon tessellation and gradient
  fills make it the stronger pure vector painter in the abstract. Weaknesses: you
  build pan and zoom yourself; the `text_editor` widget is solid but has no
  built-in undo/redo (the app must implement the undo stack); there is no AccessKit
  integration (issue #552, open since 2020); and there is no drop-in markdown
  viewer, so a preview pane is custom work.

The trade is legible, and it turns on the framework services rather than on the
painter. egui is the better fit for the subway-map-plus-notes shape: pan/zoom,
editor undo, accessibility, and turnkey markdown all come free, whereas iced asks
the app to build each of them. iced's compensating advantage is its painter, first-
class gradient fills and per-frame geometry; but the current skin is flat (see the
next subsection), so gradients buy nothing here, and the one drawing task that is
genuinely harder under egui, tessellating the two concave silhouettes, is one lyon
also serves. The painter advantage the earlier draft weighed for iced therefore
does not apply to this app, and egui is the recommended toolkit on the strength of
its free services alone.

### Rendering the diagrams with `egui::Painter`

The per-mark geometry, complete enough to redraw every silhouette, glyph, track, and
decorator without reading the renderer, is specified separately in
[`mark-geometry.md`](mark-geometry.md) (with a rendered HTML sibling); this subsection
is the argument, that document is the reference.

The map re-expresses in `egui::Painter` more directly than a general "SVG to Rust"
port would suggest, because its drawing inventory is small and entirely flat. A
search of the renderer for `Gradient`, `filter=`, `feGaussian`, `mask=`, and
`radial-gradient` (`src/renderer/src/render/*.js`, `style.css`) returns nothing but
the ground's dot pattern; every node mark in `shapes.js` and `tracks.js` is a solid
fill or a constant-width stroke in one colour. Most of it maps one for one. Tracks
are polylines with a stroke width, which is `Shape::line` with a `Stroke`
(`tracks.js:trackPath`). The variable-weight outline is a fill-only construction, an
outer silhouette and the same path scaled inward and painted over it
(`shapes.js:buildShape`), so none of the offset-curve or stroke-join arithmetic that
usually makes such a port unpleasant is needed. The terminus half-turn
(`shapes.js`, the `flip` transform) becomes an affine transform on the point list;
the flagged-node orbits are rotated ellipses, flattened to polylines and stroked
(`shapes.js:drawOrbits`); theme colours stop being CSS custom properties and become
a palette struct, a simplification; and the ground's dot grid (`style.css`,
`radial-gradient`) becomes a single tiled texture or a loop of small filled circles
over the visible rectangle.

Two things do not transfer directly, and they are the whole of the extra work.

The first is concave fills. epaint fills correctly only convex polygons (its
`PathShape` documents "Fill is only supported for convex polygons", and
`fill_closed_path` in `tessellator.rs` assumes a convex area), and exactly the two
shapes that carry the Googie character are concave: the hull's top edge bows inward
and the marquee is bowed inward on all four edges (`shapes.js`, the `hull` and
`marquee` branches of `outerPath`). The remedy is routine and one-time: flatten the
quadratics to points (the paths are already parameterised numerically, so this is a
few lines), tessellate with `lyon`, and hand the triangles to `Shape::Mesh`. Every
silhouette then goes through one function.

The second is the underpass, and it is the sharper constraint. `underpassClip`
(`tracks.js`) draws a crossing by cutting strips out of a stroked path with an
even-odd `clipPath`, and egui offers no arbitrary clipping: `set_clip_rect`
(`tessellator.rs`) takes an axis-aligned rectangle and nothing else. Overdrawing the
gap in the ground colour is unavailable too, since the ground carries the dot
pattern and a solid strip would erase dots. The replacement follows from the comment
already at `tracks.js` (a stroke can only end square to its own direction): stop
stroking the lateral and emit it as an explicit ribbon quad whose end edges are
mitred parallel to the line being passed under, with the angle and offset coming
from `breakSize` unchanged. Since laterals are straight runs of constant width, each
piece is one quadrilateral. It is a re-expression of geometry that already exists,
not a redesign, and it arguably states the intent more directly than the clip does.

Text measurement improves. `measure.js` today mounts every card off-screen, waits on
`document.fonts`, and reads `offsetWidth`; in egui the same measurement is
`Fonts::layout_job` returning a galley with exact dimensions, a pure function of the
loaded atlas, with no DOM, no font-ready race, and no first-paint reflow. The
bundled League Spartan and Boogaloo faces load from bytes through `FontDefinitions`.
One verified caveat: epaint's line breaker (`RowBreakCandidates` in
`text_layout.rs`) breaks on whitespace, CJK, a literal `-`, and ASCII punctuation,
but not on the U+00AD soft hyphen that `hyphenate.js` inserts, so soft-hyphen-aware
wrapping is custom work, either choosing the wrap points explicitly and laying out
line by line, or feeding the `hyphenation` crate's Knuth-Liang points. Separately,
shapes are retessellated every frame and stay crisp at any zoom whereas text is
sampled from an atlas, so zoom should be driven through coordinates and font size
rather than through a `Scene` layer transform, which is the blurring case noted
above.

### Hit-testing, drag-and-drop, and hover-time restyling

The interaction layer maps onto egui's `Response` API closely enough that the port
is a re-hosting rather than a redesign, which the earlier draft did not record.

Hit-testing. The DOM resolves the target under the pointer with
`closest('[data-node-id]')` (`hittest.js`), shape-exact and free because pointer
events respect each element's box. In egui one allocates a `Rect` per node keyed by
a stable `Id` (the node id) and calls `ui.interact(rect, id, Sense::click_and_drag())`
for a per-node `Response`. Cards are already rectangular positioned divs, so
rectangle interaction matches the DOM exactly and loses nothing. The one
non-rectangular target is the junction halo, the 13px transparent circle a
single-branch diamond wears (`tracks.js`, `.jx-hit`); it resolves with a
point-in-circle test against `response.hover_pos()`. Overlap order follows
allocation order together with each region's `Sense`.

Drag-and-drop. The controller in `drag.js` (a 5px threshold before a press becomes a
drag, a floating `.drag-preview` label that follows the cursor, `onProbe` on each
move to resolve the drop and draw the hint, `onDrop` on release, `onCancel` on
abandon) maps almost one for one. `response.drag_started()` replaces the threshold
(egui applies its own); `response.dragged()` with `interact_pointer_pos()` each frame
resolves the target and paints the hint; `response.drag_stopped()` applies the move.
egui's typed drag payload, `dnd_set_drag_payload` / `dnd_hover_payload` /
`dnd_release_payload`, carries the source descriptor that `drag.js` passes as
`source` (`{type:'node',id}` for a card, `{type:'fork'|'merge',footId}` for a
junction handle), and `egui::DragAndDrop` draws the floating payload layer that
replaces `.drag-preview`. The insertion caret and the target ring, which `app.js`
creates and later clears as `.insert-caret` and `.drop-target` DOM nodes
(`renderDropHint` and `clearDropHint`), become `Painter` draw calls with nothing to
create or clear, one fewer moving part than the DOM carries.

Hover and drag-time restyling: the immediate-mode inversion. The DOM changes an
element's look by toggling a CSS class once and leaving it: `.drag-src` dims the
grabbed card to opacity 0.4, `.drop-target` rings the fork target, `.jx:hover .fork`
fills the junction diamond, and `.drag-active` switches the cursor to grabbing
(`style.css`). egui holds no retained styling to toggle; the frame is redrawn every
tick, so changing a look is a conditional in the paint code, which reads the state
(is this node the drag source, the resolved drop target, `response.hovered()`) each
frame and chooses the colour or alpha then, with `ctx.set_cursor_icon(CursorIcon::Grabbing)`
for the cursor. State is read per frame, not mutated once and remembered; that
inversion is how every hover and drag affordance in the app re-expresses, and it is
worth stating plainly because it is the one structural difference the whole
interaction layer inherits.

### The note editor: not CodeMirror, and why

CodeMirror is itself MIT-licensed, so licensing is not what rules it out; it is a
DOM editor, built on the browser's contenteditable, and only exists where there is
a webview. So Design A keeps CodeMirror, Marked, and KaTeX untouched, and Design B
cannot keep any of them. Nothing in the pure-Rust world matches CodeMirror 6
feature-for-feature, but the need is a markdown notes editor, not a code-authoring
surface. In egui that is `TextEdit` plus `egui_commonmark` for preview (with
`ropey` for the buffer and `syntect` or `tree-sitter` for markdown highlighting if
wanted); in iced it is the cosmic-text-based `text_editor` (the closest thing to
CodeMirror-class editing in the permissive Rust ecosystem, though undo is on you).
Markdown rendering moves from Marked to `pulldown-cmark` (MIT), deliberately chosen
over `comrak` (BSD-2-Clause, outside a strict MIT-or-Apache constraint). Avoid
`helix-core` for the editor: capable but MPL-2.0, outside the MIT-or-Apache line
(though AGPL-compatible).

Math (KaTeX today) is a preview-pane concern, not an editor one, and this reframing
makes it tractable. The editor holds plain LaTeX source, so cosmic-text or `TextEdit`
need no math awareness; math renders only in the rendered pane, exactly like
Obsidian's split view. The pipeline is `pulldown-cmark` plus a small extension that
recognises `$...$` and `$$...$$` (optionally `\(...\)` and `\[...\]`), with each math
run rendered to an SVG or cached texture, debounced (100 to 200 ms) and keyed by
source, size, and colour so identical formulas render once. Contrary to an earlier
reading in this notebook, native Rust math renderers do exist: RaTeX (pure Rust, no
JavaScript or WebView, emits a flat display list for SVG/PNG/canvas), ReX (an older
SVG math-typesetting library), and `pulldown-latex` or `katex-rs` (LaTeX to MathML);
`pulldown-cmark-katex` already wires pulldown-cmark math runs to MathML.

RaTeX validated (July 2026). License: MIT, confirmed by the repo LICENSE, the GitHub
sidebar, and the crates.io metadata; it bundles the KaTeX math fonts under OFL-1.1
(`THIRD_PARTY_NOTICES.txt`), the same license class PensaGrex already vendors and the
same fonts the current app ships, so it satisfies the MIT-or-Apache constraint
cleanly. Coverage: the ">99.5% KaTeX syntax coverage" figure is backed by a real
conformance harness (a golden suite comparing rendered output against KaTeX 0.16.45
by ink-coverage IoU, row by row, over math, mhchem chemistry, and physics, plus a
public support table and live demo), so it is evidence-based rather than marketing;
the caveats are that the corpus is the project's own (self-measured, not independently
audited) and the live table renders via JavaScript so the exact number was not read
directly. Maturity is the residual risk, not coverage or license: pre-1.0 (the
typeset crates `ratex-layout` and `ratex-svg` at 0.1.14, 2026-07-28; the umbrella
`ratex` name is a 0.0.1 placeholder, so depend on the workspace crates by name), a
multi-crate workspace on crates.io, ~1.4k stars but apparently a single maintainer,
so expect API churn and weigh bus-factor. The remaining validation, when
the port is real, is to run representative note formulas through its harness. So math
is a bounded implementation task in the preview pane, not a lost capability.

Insulating the dependency. The single-maintainer risk is bounded by the MIT license
itself: worst case, fork the version already held, on identical terms, at any later
date, so RaTeX's adoption does not bet on the maintainer's persistence. Two moves,
different costs. Cheap insurance: vendor the sources into our tree (a `cargo vendor`
dir or a mirror) so the build no longer depends on GitHub or crates.io serving the
code (a pinned git-rev alone does not protect against the repo being deleted); this
addresses "no worries about the future of that repo," stays reversible, and keeps the
option to pull upstream fixes. We would vendor only the string-to-display-list-to-SVG
subset (`ratex-types`, `-lexer`, `-parser`, `-layout`, `-render`, `-svg`, and the
font crates), not `-ffi`/`-wasm`/`-pdf`. Expensive independence: hard-fork and
maintain that subset ourselves, which transfers rather than removes the bus-factor,
since we would then own an intricate TeX layout engine, a specialized long-term
liability. Recommendation: vendor for supply-chain safety and track upstream while it
lives; keep a full fork as a contingency executed only on genuine abandonment. Each
vendored file keeps its MIT header; the bundled math fonts keep OFL-1.1, per the
existing font-vendoring pattern. (Discard the tempting HTML-plus-KaTeX
in a WebView route: it reintroduces the webview Design B exists to remove, and belongs
to Design A.)

Costs. A full rewrite, not a migration: the model, its tests, the store and atomic
writes, the task authority, and the MCP server are all re-authored in Rust, and the
bespoke Googie scene is rebuilt against a new drawing API. Packaging is do-it-
yourself: there is no electron-builder that bundles, signs, and notarizes in one
step, so expect to wire `cargo-packager` (bundles) with `apple-codesign`/`rcodesign`
(pure-Rust macOS sign, notarize, staple, runnable from Linux/Windows CI) and Azure
Trusted Signing (Windows). Both toolkits are pre-1.0 and take breaking changes
across minor versions. The JSON5 round-trip caveat applies here too.

Verdict: the recommended end state if 100% Rust is the goal. It is the widest
rewrite but the only design that is actually 100% Rust, removes the webview-fidelity
problem outright, and collapses the model to a single home. egui is the toolkit to
prototype first, with the subway scene as the acceptance test.

Stack: the linked runtime crates are all MIT or Apache-2.0 (egui/eframe/epaint,
wgpu, winit, AccessKit, pulldown-cmark, egui_commonmark, rmcp, axum), with the one
optional runtime exception being `notify` (CC0-1.0); the build-time signing and
packaging tools add MPL-2.0 (`apple-codesign`) and the vendored math fonts add
OFL-1.1. The full inventory, with versions and licenses, is the next subsection.

### Crate inventory (Design B)

The crates a Design B build would depend on, with the version and license verified
against crates.io on 2026-08-16 and the role each plays. PensaGrex ships under
AGPL-3.0-or-later; every crate below is permissive and compatible with distributing
an AGPL binary (Apache-2.0 is compatible with the GPLv3 family, of which AGPL-3.0 is
a member). Crates marked "if" are conditional on a design choice named in the role.

| Crate | Version | License | Role |
| --- | --- | --- | --- |
| egui | 0.36.1 | MIT OR Apache-2.0 | immediate-mode GUI core |
| eframe | 0.36.1 | MIT OR Apache-2.0 | app shell: window, event loop, AccessKit |
| epaint | 0.36.1 | MIT OR Apache-2.0 | 2D painter, mesh, text layout |
| egui_extras | 0.36.1 | MIT OR Apache-2.0 | static SVG asset loading for icons, if wanted |
| egui-wgpu | 0.36.1 | MIT OR Apache-2.0 | custom paint-callback path, if a bespoke renderer is composited |
| egui_commonmark | 0.25.0 | MIT OR Apache-2.0 | markdown preview pane |
| wgpu | 30.0.0 | MIT OR Apache-2.0 | GPU backend (via eframe) |
| winit | 0.30.13 | Apache-2.0 | windowing (via eframe) |
| accesskit | 0.24.1 | MIT OR Apache-2.0 | accessibility (via eframe) |
| lyon_tessellation | 1.0.20 | MIT OR Apache-2.0 | concave-silhouette fill tessellation |
| i_overlay | 8.1.0 | MIT OR Apache-2.0 | 2D boolean ops, if the underpass ribbon needs polygon difference |
| hyphenation | 0.8.4 | MIT OR Apache-2.0 | Knuth-Liang wrap points, if soft-hyphen wrapping is not hand-rolled |
| pulldown-cmark | 0.13.4 | MIT | markdown parse (under egui_commonmark) |
| ratex-layout | 0.1.14 | MIT | in-note math layout (vendored subset) |
| ratex-svg | 0.1.14 | MIT | math display-list to SVG (vendored subset) |
| pulldown-latex | 0.8.0 | MIT | alternative math path: LaTeX to MathML |
| serde | 1.0.229 | MIT OR Apache-2.0 | model (de)serialization |
| serde_json | 1.0.151 | MIT OR Apache-2.0 | plain-JSON write path (axiom 7) |
| json5 | 1.3.1 | MIT | tolerant read of a legacy `forest.json5` |
| tempfile | 3.27.0 | MIT OR Apache-2.0 | atomic write-to-temp-then-rename |
| directories | 6.0.0 | MIT OR Apache-2.0 | platform library-root path |
| uuid | 1.24.1 | MIT OR Apache-2.0 | node id minting |
| semver | 1.0.28 | MIT OR Apache-2.0 | About-window `-devN` ordering (parity with `version.js`) |
| thiserror | 2.0.20 | MIT OR Apache-2.0 | typed errors in the core crate |
| anyhow | 1.0.104 | MIT OR Apache-2.0 | error plumbing at the app edge |
| rmcp | 3.1.2 | Apache-2.0 | in-app MCP server (official Rust SDK) |
| axum | 0.8.9 | MIT | loopback HTTP for the MCP endpoint |
| tokio | 1.53.1 | MIT | async runtime for rmcp and axum |
| ureq | 3.4.0 | MIT OR Apache-2.0 | About-window GitHub latest-release check (blocking) |
| rfd | 0.17.2 | MIT | native file/folder dialogs: domain switcher, library root |
| open | 5.4.1 | MIT | open the download page and external links |
| arboard | 3.6.1 | MIT OR Apache-2.0 | OS clipboard (egui already uses it) |
| notify | 8.2.0 | CC0-1.0 | optional: reflect external edits to a domain file |
| log | 0.4.33 | MIT OR Apache-2.0 | logging facade |
| env_logger | 0.11.11 | MIT OR Apache-2.0 | log backend |
| egui_kittest | 0.36.1 | MIT OR Apache-2.0 | GUI tests via AccessKit (dev-dependency) |
| insta | 1.48.0 | Apache-2.0 | snapshot tests for layout and geometry (dev-dependency) |
| cargo-packager | 0.11.8 | Apache-2.0 OR MIT | bundle the installers (build-time tool) |
| apple-codesign | 0.29.0 | MPL-2.0 | macOS sign, notarize, staple from any-OS CI (build-time tool) |

License watch. The linked runtime stack is entirely MIT and/or Apache-2.0, with two
qualifications: the optional `notify` is CC0-1.0 (public-domain-equivalent, no
obligation), and the vendored RaTeX subset is MIT code bundling OFL-1.1 math fonts,
the same license class the app already vendors for League Spartan and Boogaloo.
`apple-codesign` (MPL-2.0) and `cargo-packager` are build-time command-line tools,
not linked into the binary, so neither touches the app's own license; this is why
the older "fully MIT or Apache-2.0" phrasing overstated the position, and the Stack
paragraph above now separates the linked stack from the build tools. The MIT-only
crates (rmcp, axum, tokio, pulldown-cmark, json5, rfd, open, the RaTeX crates) and
the Apache-only winit each satisfy the org's MIT-or-Apache library preference; none
imposes a copyleft obligation on the AGPL application.

## Design C — Rust plus Python

A design that keeps Python in the picture, given the org is Python-first and already
ships smalt-mcp. Three shapes are possible: (a) a Rust app embedding Python
in-process via PyO3; (b) a Rust UI with a Python (FastAPI) backend spawned as a
sidecar; (c) the MCP server left in Python while the app core is Rust. The research
is blunt about all three: Python earns a place only across a wire, as a federated
peer service, never inside the app.

The reasons are concrete. Packaging a Python interpreter into a signable,
notarizable desktop app is the dominant cost and it is worse than Electron's, not
better: a frozen Python tree (PyInstaller) crashes under macOS hardened runtime
unless you add entitlements that weaken security and individually sign every bundled
`.so`/`.dylib`, and Tauri has a specific known failure where an app notarizes with
the sidecar removed and fails with it present (tauri #11992). This trades away the
single biggest win of going Rust, one static binary that signs like any other.
Shapes (b) and (c) reintroduce exactly the "two runtimes glued by IPC" arrangement
this notebook already rejects, adding 15 to 40 MB of frozen interpreter and a
visible cold-start spawn. Shape (a) is on PyO3's weaker footing: embedding a Python
interpreter in a Rust binary has no first-class static support (PyO3 issue #416),
dynamic embedding forces shipping libpython plus the standard library, and the GIL
contends with the render thread. And there is a model-duplication trap in shape (c):
a Python MCP server that implements task mutations duplicates the authority in a
second language, which the single-source-of-truth discipline forbids; the only safe
form is a thin proxy in front of what rmcp already provides in-process for free.

There is also a clean licensing reason to keep Python over a wire. AGPL copyleft,
including the section-13 network clause, reaches whatever is combined into one
program. Two separate processes talking over a documented wire (loopback HTTP or
MCP) are separate works, so federating with the org's FastAPI services and smalt-mcp
does not pull those services under AGPL. In-process PyO3 embedding erases that
boundary and links the embedded Python into the AGPL work. The wire is the license
boundary, and it is worth keeping.

Verdict: do not put Python inside PensaGrex. The Rust core is the single authority
in every shape, and Python's right place is the org's existing FastAPI and smalt-mcp
estate, reached over MCP or HTTP, carrying integration rather than any forest
semantics. This is the same picture as ROS federating languages over a wire, and it
is fully compatible with Design B: the app is 100% Rust, and Python stays first-class
for services at the boundary. PyO3 remains available for a future scripting or plugin
surface that calls into the Rust authority (accepting that such embedded Python would
fall under AGPL).

Precedent worth noting: Dora-rs, a Rust-native robotics dataflow framework
(Apache-2.0), is described as a "100% Rust framework" that federates Rust, Python,
C, and C++ nodes over a zero-copy Arrow plus Zenoh wire, positioned against ROS 2 and
reported far faster than ROS 2's Python path. It is the clean instance of "Rust core,
Python as a wire-federated node, no in-process embedding."

---

# Other options considered

Recorded so the option space is explicit; none removes the central cost, which is
reproducing the bespoke subway renderer and rehousing the loopback MCP server.

- Dioxus, desktop/Wry mode (MIT or Apache-2.0; mature, 0.7.x). RSX components in
  Rust, but desktop mode still embeds the OS webview, so it inherits the same
  cross-webview inconsistencies as Tauri and is a full JS-to-Rust rewrite for no
  rendering gain over Tauri. A lateral move.
- Dioxus Native / Blitz plus Vello/WGPU (MIT or Apache-2.0; `stylo_taffy` adds
  file-level MPL-2.0). Webview-free native drawing on a declarative DOM-like model,
  a promising middle path, but alpha (blitz 0.3.0-alpha) and betting the subway map
  on a moving target. Premature for a shipping app; worth watching.
- Flutter plus flutter_rust_bridge (Flutter BSD-3-Clause, bridge MIT; mature).
  Flutter's own canvas suits a bespoke vector scene and a Rust core can hold the
  model and MCP server, but it adds Dart as a third language and rewrites the whole
  UI in Dart. The largest language and culture mismatch for this org.
- Pure local-first web app / installable PWA (permissive; lightest shell). The SVG
  renderer and model port almost verbatim, but it collides with the northstar:
  browser file access is either the File System Access API (Chromium-desktop only)
  or OPFS (a sandboxed virtual store, not user-visible JSON5 and markdown files),
  and a browser tab cannot host the loopback MCP server, so it breaks axiom 7 and
  the live-AI surface. Rejected.
- Trim Electron (MIT; zero rewrite). Harden and prune the current app: context
  isolation, CSP, dependency pruning, asar, v8 snapshots. Real and cheap, but it
  cannot shed bundled Chromium (150 to 200 MB binary, 200 to 400 MB idle), so it is
  optimization, not the redesign the direction calls for. The sensible hold-position
  if the port is deferred.
- gpui, Zed's UI framework. Excluded on licensing despite the Apache-2.0 label: a
  default release build statically links GPL-3.0-or-later object code through its
  dependency chain (gpui to sum_tree to ztracing to zlog), so it fails a permissive
  constraint in practice, and it is not a supported standalone crate (its API tracks
  Zed's main branch). Do not adopt.
- Freya (MIT over BSD-licensed Skia; young). Webview-free native Rust rendering,
  the most fitting emerging permissive option after egui/iced, but 0.4.0
  (2026-07-16) just rewrote its reactive core, so it is a schedule risk for a
  bespoke renderer. Watch, do not bet on yet.
- Slint. Excluded by construction: available only under GPLv3, a
  royalty-free-with-attribution license, or a paid commercial license, never MIT or
  Apache. Mature; the issue is licensing, not maturity.

A note on the permissive constraint and AGPL: because PensaGrex is itself
AGPL-3.0-or-later, GPL contamination from gpui or a GPL Slint would not break the
app's own license compliance. The reason to keep the MIT-or-Apache preference is
that it preserves the freedom to relicense more permissively later and keeps the
dependency tree clean; it is a forward-looking discipline, not a present legal bar.

---

# Sync server (an orthogonal capability)

A recurring idea, distinct from the UI and language choice: an optional,
self-hostable server that syncs a user's forests and notes across devices, modeled
on the Joplin notes server. It could pair with any of the designs above; a Rust
rewrite is merely a natural moment to consider it. The org already ships jonobones,
a Joplin-sync daemon, as prior art.

The northstar tension is the first thing to settle, and it resolves favourably if
the design is disciplined. Axiom 7 says local, no account, no cloud, no lock-in. An
optional, off-by-default, additive sync layer honours that as long as the local
files stay the complete and authoritative source of truth on every device and the
server holds only opaque replicas. Two tempting variants must be rejected because
they invert axiom 7: making a "LAN box the authority" (it forfeits offline use and
the no-cloud property), and storing CRDT causal metadata either inside
`forest.json5` (which destroys its plain, grep-able character) or in a sidecar that
demotes the JSON5 to a derived projection (which inverts "the file is the source of
truth"). On-by-default or required sync would cross the line into "PensaGrex Cloud"
and is out.

Encouragingly, the sync boundary is already implemented in `store.js`:
`bookmarks.json` is deliberately shared with the data, while view state (camera,
zoom, collapse) sits in a `userData` sidecar and is excluded per axiom 9. So "what
to sync versus what to keep local" is a solved question. The on-disk format is
already the sync unit (an id-keyed `forest.json5` per domain, per-task markdown
notes, `bookmarks.json`), the atomic write path and single authority are exactly
what a sync layer needs to serialize its applies, and `validateRecord` becomes the
safety gate that any incoming or merged forest must pass before it is written.

Recommended shape. Do not build a bespoke server first. Implement a
backend-agnostic sync-target driver (list/get/put/delete, plus an optional delta
cursor) in a module beside `store.js`, and point it at infrastructure the user
already self-hosts: WebDAV, S3/Nextcloud, git (the most northstar-aligned: a remote
the user owns, versioned, portable, invoked as a subprocess), or Syncthing. Keep a
local sync-state sidecar (last-synced hash per item, per target) out of
`forest.json5`, mirroring Joplin's `sync_items`. Handle conflict the Joplin way:
last-writer-wins with the losing version preserved as a sibling copy and surfaced,
never a silent overwrite. Add an optional end-to-end-encryption layer at the
serialization boundary only for untrusted (VPS) hosting; a LAN box the user owns may
not need it, and E2EE carries a hard failure mode (a lost master key renders a
zero-knowledge remote copy unrecoverable).

The genuinely hard part is not transport but merge. `forest.json5` holds a whole
domain in one file, so any whole-file syncer conflicts on non-overlapping task edits
from different devices (or from a device and the in-app MCP agent). Scalar per-task
fields merge easily, but the structural moves (`move_subtree`, `move_into_line`,
`detach_to_project`) are arbitrary re-parents, and splice-delete reconnects
children, so two devices can create a cycle or orphan a subtree; those collisions
must be detected and routed to a conflict copy (or, someday, CRDT-resolved). Note
also the note/forest coupling: a task's note filename lives inside `forest.json5`
while the note is a separate file, so a conflict copy of the forest must keep the
note references and note files consistent.

If a bespoke delta-sync server is ever built (justified only at a scale a
single-user tool will not reach), it should be a separate program in its own repo,
model-agnostic, never the authority, and FastAPI is the right stack (org convention,
the smalt-mcp precedent). It should be AGPL, like pensa-grex and jonobones; AGPL is
the correct license for networked server software, and section 13 is trivial for a
self-hosted single-user server. Reuse candidates for merge, if the CRDT path is ever
taken, are all MIT: Loro (a movable-tree CRDT, the best structural fit for
tasks-with-moves), Automerge, or Yjs. One dead end to record: Joplin Server the
software is under a noncommercial Personal Use License and is not open source, so it
cannot be forked, shipped, or offered as a service; only the protocol shape and
Joplin's AGPL client/lib driver code are reusable.

This capability is orthogonal enough that it may deserve its own `sync_ideas.md`
notebook if it firms up; for now it lives here.

---

# Comparison

| Design | 100% Rust | Renderer | Model home | MCP server | Webview fidelity risk | Binary / idle | Permissive-clean | Rewrite scope |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| A. Tauri hybrid | No | Kept (web) | Split (Rust authority + JS derive), duplication hazard | Rust (rmcp) | Yes (3 engines, WebKitGTK weak) | ~10 MB / ~40 MB | Yes | Backend + model; renderer kept |
| B. 100% Rust GUI (egui) | Yes | Rewritten in Rust | One Rust crate, no duplication | Rust (rmcp) | None (own renderer) | Small native binary | Yes | Widest: renderer + interaction + model + store + MCP |
| C. Rust + Python | Core yes; Python at the wire only | As A or B | One Rust crate; Python holds none | Rust in-process (Python only as a peer) | As A or B | Worse if Python bundled | Yes | As A/B, plus Python packaging pain if embedded (rejected) |
| Trim Electron | No | Kept | Unchanged (JS, single source) | Node (kept) | None (bundled Chromium) | 150-200 MB / 200-400 MB | Yes (MIT) | None |

Sync server is orthogonal to every row and can be added to any of them.

---

# Northstar fit

- Axiom 6 (the file is the source of truth, plain JSON5 and markdown on disk,
  portable) is the reason the port is tractable at all: storage is
  framework-agnostic and survives unchanged. The one place it can be violated
  accidentally is the JSON5 write path (see the round-trip caveat); guard it.
- Axiom 8 (view is not data) is preserved in every design: camera, zoom, and
  collapse stay client state. The existing `store.js` split of `bookmarks.json`
  (shared) versus view state (local) already encodes the rule.
- Intent 2 (structure legible at a glance: the subway map and Googie skin) is the
  fidelity requirement. Design B must reproduce the grammar and skin against a new
  drawing API; Design A must survive WebKitGTK. This is the largest single risk and
  the thing to prototype first.
- Nothing in the northstar mandates Electron. This is purely a substrate choice.

# Costs and open questions

- Scope. Design B rewrites the renderer, interaction, model, store, and MCP server;
  it is the widest path. Design A is narrower but keeps the fidelity QA and the
  duplication hazard.
- Org tooling. There is no handbook Rust-desktop profile. A Rust signing and
  notarization CI profile (cargo-packager plus apple-codesign plus Azure Trusted
  Signing, or Tauri's bundler) replaces the electron-builder pipeline set up across
  the v0.8.x and v2.x releases. This is new org work either way and belongs in the
  handbook once chosen.
- Language fluency. Rust is a maintenance shift for a deliberately-plain-JS app,
  even though it is org-aligned; Python stays first-class for services.
- Migration strategy (open). Staged (a Tauri interim, then the webview swapped for
  egui) de-risks the jump but risks paying the model re-homing cost twice unless
  sequenced deliberately; a big-bang rewrite to Design B is cleaner but riskier.
- Editor and math. The note editor has good permissive Rust answers, and math is a
  preview-pane task, not an editor one: native Rust math renderers exist (RaTeX, ReX,
  `pulldown-latex`/`katex-rs`), so KaTeX narrows from a lost capability to a bounded
  implementation task, with library maturity and licensing to validate.
- The data is safe under every option: JSON5 forests and markdown notes on disk are
  unchanged.

# Precedent: is robotics moving from C++/Python toward Rust/Python?

The analogy the direction rests on holds up, and its precise shape is worth
borrowing. A dedicated research pass on the ROS community (July 2026) finds the move
toward Rust serious and sustained rather than fringe, but framed as "add Rust
alongside C++ and Python," not "rewrite ROS in Rust," and not yet a formally
sanctioned peer status.

Two tracks are visible. On the client-library track, ros2-rust/rclrs has reached
near feature-parity with the C++ and Python clients (publishers, subscriptions,
services, actions, timers, parameters, zero-copy loaned messages; v0.7.0 on
2026-01-18), and its message generator entered the ROS 2 Rolling core generator set
in October 2025 alongside the C, C++, and Python generators; a core ROS 2 author
presented it at FOSDEM 2026 as "the official ROS 2 client library for Rust." Yet
rclrs still disclaims API stability, lives under the community org rather than the
official one, and ROS 2's own documentation continues to list only C++ and Python as
officially maintained, with Rust at "various levels of community support." So Rust is
first-class in practice and momentum, not yet by governance.

On the middleware track, Rust entered the core stack by dependency rather than by
rewrite: Zenoh, which is written in Rust, was selected in the 2023 RMW evaluation as
ROS 2's alternative middleware, and rmw_zenoh reached Tier-1 support in Kilted (May
2025). But the ROS team reached Zenoh deliberately through its C and C++ bindings, so
the Rust sits behind the RMW abstraction rather than being exposed in the core. And
the Rust-native alternative, Dora (dora-rs), is a self-described "100% Rust framework"
that positions itself against ROS 2 on performance (claiming a large speedup over ROS
2's Python path via zero-copy shared memory), but as a separate dataflow framework,
not a ROS rewrite.

The bearing on PensaGrex is direct, and it validates the recommended shape rather
than a maximal one. Robotics' actual trajectory is Rust as a first-class citizen
alongside Python, federated over a wire, plus Rust-native alternatives that keep
Python as a node, rather than a wholesale rewrite of a mature C++ core. That is
exactly Design B combined with Design C's discipline: a 100% Rust app, with Python
kept first-class at the service seam over a wire, Dora-rs being the clean instance of
a Rust core with Python nodes. It does not argue for embedding Python in the app, and
it does not treat "rewrite everything in Rust" as the norm; it treats Rust as the
language one adopts for new first-class surfaces while Python stays over the wire.

Sources: rclrs 0.5.0 and 0.6.0 release announcements (ROS Discourse, 2025); the
FOSDEM 2026 rclrs talk (Esteve Fernandez); the ROS 2 alternative-middleware report
(2023) and rmw_zenoh (ros2 org, Tier-1 in Kilted, 2025); dora-rs.ai.

# Migration and repo strategy

How to carry a full rewrite in the repo while `main` keeps shipping the Electron app
until parity. This is a recommended execution shape, not an executed decision; it
presumes the rewrite has been green-lit.

The org already has the on-demand dev-build pattern to model on. `dev-release.yml`
is a `workflow_dispatch` that runs only from `develop`, refuses to build unless the
version source-of-truth carries a `-dev` marker, builds the three-OS matrix, and uploads
installers as artifacts with no `v*` tag and no GitHub Release, so `release.yml`'s
gate is never touched. `git dev-release --open` opens a dev cycle and `git dev-release`
cuts the builds. That workflow is the template for the Rust app's dev builds.

Branch model: a long-lived `rust` integration branch off `develop`. Keep it single-stack.
On `rust`, remove the Electron tree entirely and make it Rust-native from the first commit,
with `Cargo.toml` as the one version source-of-truth. `develop` and `main` go on shipping
the Electron app untouched, and each branch then has exactly one version source, which the
org's `git bump`/`git release`/`git dev-release` require, since they auto-detect a single
source and would be ambiguous with two in one tree. Work lands on `rust` through PRs based
on `rust`, merged with a merge commit as elsewhere; in the contained worktree layout, add a
`pensa-grex-rust` worktree beside `-main` and `-develop`. A long-lived branch is usually a
hazard, but it is safe here because this is a replacement, not two live codebases to
reconcile.

Cutover at parity: `rust` becomes `develop` (a wholesale replace, since the Rust app
supersedes the JS app), then promote `develop` to `main`, `git bump` to `3.0.0`, and retire
the Electron tree. Because the Rust app is never tagged before that moment,
`release.yml`'s gate (the tag must match `package.json` and be reachable from
`main`) stays inert throughout, which is what keeps `main` genuinely untouched until parity.

CI on the `rust` branch, four workflows, each modeled on an existing one:

- `checks-rust.yml` (push and PRs to `rust`): `cargo fmt --check`, `cargo clippy -D warnings`,
  `cargo build`, `cargo test`; the analogue of `test-electron.yml`.
- `dev-release-rust.yml` (`workflow_dispatch` from `rust`): gate on a `-dev` marker in
  `Cargo.toml`, then a macOS/Windows/Linux `cargo` build packaged by `cargo-packager` and
  signed by `apple-codesign`/`rcodesign`, published as artifacts or a GitHub pre-release,
  no `v*` tag. The on-demand parity build; a direct clone of `dev-release.yml`.
- `reuse.yml`: carried over unchanged.
- a Rust `version-guard.yml` for PRs into `rust` that checks `Cargo.toml` (the existing
  guard is keyed to `develop` and `package.json`, so it does not cover `rust`).

Two enabling gaps to close first, both small. The dev-tools SoT helper (`_sot.sh`) detects
`pyproject.toml`, `package.json`, and `VERSION.txt` but not `Cargo.toml`, so
`git bump`/`git release`/`git dev-release` need a Cargo kind added before they work on
`rust` (read and write `[package].version`, for instance via `cargo set-version`). And the
handbook has no rust-desktop profile yet, which both this notebook and the Conception-Space
study flag as needed; the `*-rust.yml` workflows and the signing recipe become that profile
once proven here.

Versioning during the rewrite: put `3.0.0-dev0` in the branch's `Cargo.toml`, since the
parity release is a major bump from v2.x; dev builds increment the suffix, and the cutover
is `git bump release` to `3.0.0` (the dev suffix sorts below it, per the handbook's
development-versioning rule). Keep the branch fresh by merging only `develop`'s shared
surface into it (the `docs/`, the northstar, and any model or JSON5-schema decisions); the
JS implementation never merges into a Rust tree, so the sync burden is light.

Alternatives, and why not lead with them. In-tree coexistence (both apps on `develop`
behind path-gated CI) integrates more continuously and makes the cutover a one-line flip of
which app the release builds, but it puts two version sources in one tree, which fights the
single-source release tooling, and it changes what `develop` builds; defensible for a
trunk-based team, more friction against current conventions. A separate repo is right for a
genuinely separate app (the visionOS enactive mode is its own RealityKit repo for exactly
this reason) but wrong for a replacement, because it forces a repo rename, a split release
history, and a harder cutover than swapping a branch.

# Decisions log

- 2026-07-21 — Direction set (author): PensaGrex should become a 100% Rust app.
  Working recommendation from the session discussion and the research pass: the end
  state is a Rust-native GUI (egui preferred over iced for the subway-map-plus-notes
  shape), not Tauri, because PensaGrex is a bespoke-renderer app for which a webview
  buys little and a Rust-native GUI removes the webview-fidelity problem outright.
  Tauri is a possible interim, not the target. Python stays at the service wire, not
  inside the app (Design C's in-app shapes rejected on packaging and licensing
  grounds). The model must be re-homed to a single Rust crate and its correctness
  preserved against the existing test suite; the write path must preserve the file
  as the user's, to honour what is now axiom 7. A Joplin-style sync server is a
  separate, optional, off-by-default capability that can pair with any design.
  Captured as an in-flight idea for exploration, not yet a plan.
- 2026-07-21 — RaTeX dependency stance (agreed): if Design B proceeds and RaTeX is
  used for in-note math, vendor the typeset-to-SVG subset (`ratex-types`, `-lexer`,
  `-parser`, `-layout`, `-render`, `-svg`, and the font crates) into our tree for
  supply-chain safety, track upstream while it stays active, and reserve a full fork
  for the day upstream is abandoned. The MIT license makes the fork always available,
  so adoption does not bet on the single maintainer. Vendored files keep their MIT
  headers; the bundled math fonts keep OFL-1.1, per the existing font-vendoring
  pattern. A Design B decision; no action until that path is taken.
- 2026-07-21 — Repo strategy (recommended, from the "Migration and repo strategy"
  section): carry the rewrite on a long-lived single-stack `rust` branch off `develop`
  (Cargo.toml the one SoT, Electron tree removed there), keep `main`/`develop` shipping
  Electron until a parity cutover that replaces `develop` with `rust` and releases 3.0.0.
  Cut on-demand three-OS dev builds via a `dev-release-rust.yml` cloned from
  `dev-release-electron.yml` (since replaced by `dev-release.yml`). Two prerequisites: a
  `Cargo.toml` kind in dev-tools `_sot.sh`, and a handbook rust-desktop profile.
  Recommended, not executed; gated on the rewrite being green-lit.
- 2026-07-26 — Model v3 lands in the northstar, and three things here move with it.
  The axioms were renumbered (6 becomes 7, 8 becomes 9) and axiom 7 now names plain
  JSON, which narrows the round-trip caveat above from comments to formatting. The
  model this study proposes re-homing is no longer a strict tree: schema 3 adds a
  terminus closing every scope and a merge returning every branch, so the Rust `core`
  crate would own the three merge clauses, the bracket-matching enclosing-scope walk,
  and a longest-path row layering in place of today's depth-first walk, which is more
  model to port and a harder layout engine than the estimate above assumed. And the
  repo-strategy section reserves 3.0.0 for the Rust cutover, which model v3 has now
  taken; a cutover release would be 4.0.0. None of this changes the recommendation,
  only its size.
- 2026-08-16 — The renderer and interaction layers worked through against the code,
  a premise corrected, and a crate inventory added (this revision). Reading the
  renderer settled that the Googie skin is flat: a search for `Gradient`, `filter=`,
  `feGaussian`, `mask=`, and `radial-gradient` finds only the ground's dot pattern,
  so the earlier claim that the skin needs hand-built gradient meshes, and the
  egui-versus-iced weighing that leaned on iced's gradient support to compensate, do
  not hold; egui's recommendation now rests on its free framework services (pan/zoom,
  editor undo, accessibility, markdown), not on a painter trade it was losing. The
  two genuine porting costs are named instead: tessellating the two concave
  silhouettes (the hull top and the marquee) with lyon into a `Shape::Mesh`, and
  replacing the even-odd `clipPath` underpass with an explicit mitred ribbon quad,
  since egui clips only to an axis-aligned rectangle. The interaction layer is a
  re-hosting, not a redesign: egui's `Response` (`hovered`, `contains_pointer`,
  `drag_started`/`dragged`/`interact_pointer_pos`, `drag_stopped`) plus the typed
  `dnd_set_drag_payload`/`dnd_hover_payload`/`dnd_release_payload` trio maps the
  `drag.js` controller and its `source` descriptor almost one for one, with the one
  structural difference that hover and drag looks are chosen per frame from read
  state rather than by toggling a CSS class. The crate inventory (versions and
  licenses as of this date) narrows the old "fully MIT or Apache-2.0" stack claim:
  the linked runtime is MIT/Apache, but `notify` is CC0-1.0, the build-time
  `apple-codesign` is MPL-2.0, and the vendored math fonts are OFL-1.1. None of this
  changes the Design B recommendation.
