Architecture
ashlar is a workspace of small crates stacked in ranks. An app depends on the umbrella crate
ashlar-rs (imported as ashlar) and turns on the features it needs; each layer below it can also be used on its own.
Crate ranks
A crate depends only on crates of a lower rank (dev-dependencies are exempt). Ranks 0–4 never
depend on a UI crate (egui, eframe, winit, egui-wgpu, egui_kittest), and ranks 0–1 build
for wasm32-unknown-unknown, as do ashlar-ui and ashlar-app (the web build path). cargo xtask layers and cargo xtask wasm enforce this; the table below must match
LAYERS in xtask/src/main.rs (a crate missing from LAYERS fails the gate).
| Rank | Crate | What it is | Headless | wasm32 |
|---|---|---|---|---|
| 0 | ashlar-command | Commands: registry, typed params, one execute chokepoint, discovery | yes | yes |
| 0 | ashlar-fs | Files and settings: app dirs, durable atomic writes, keyed settings, recent files | yes | yes |
| 0 | ashlar-jobs | Background jobs: progress, cancel, panic isolation, apply-on-UI-thread | yes | yes |
| 0 | ashlar-trace-proto | Telemetry wire format: envelopes, items, name rules, limits | yes | yes |
| 0 | ashlar-docs | Documentation library for agents: folders are categories, markdown files are documents, embedded at build time, a run-time folder on top | yes | yes |
| 0 | ashlar-log | Logging and crash safety: directive filters, stderr + rotating file + ring, panic hook, launch guard | yes | yes |
| 0 | ashlar-obfuscate | Compile-time string obfuscation: build-time values kept out of a strings dump of the binary | yes | yes |
| 0 | ashlar-motion | Motion maths: closed-form springs, curves, animated values that keep their velocity on retarget, reduced-motion rules per value class, motion tokens, OKLab/OKLCH colour, gesture helpers | yes | yes |
| 0 | ashlar-syntax | Syntax highlighting: linear-time lexers for 18 languages, line states for incremental views, a Highlighter trait for bigger engines | yes | yes |
| 0 | ashlar-diff | Text diffs: Myers with a cost cap, word-level changes, hunks with collapsible context, a unified-patch parser and writer | yes | yes |
| 0 | ashlar-i18n | Translations: JSON catalogs per locale with fallback chains, {name} templates, CLDR plural categories, number formatting, locale negotiation and catalog checks | yes | yes |
| 1 | ashlar-trace | Telemetry SDK: consent, spool, sending, crash/hang/frame/system signals, event catalog | yes | yes (no-op) |
| 1 | ashlar-store | Documents: Arc snapshots, revisions, undo history, versioned formats, autosave | yes | yes |
| 2 | ashlar-native | Native OS features behind one API: dialogs, menus, clipboard, power, notifications, secrets | yes | – |
| 2 | ashlar-control | Agent control port: hardened JSON-lines server, client, standard method set | yes | – |
| 2 | ashlar-trace-server | Self-hosted telemetry server: ingest, SQLite store, rollups, dashboard | yes | – |
| 2 | ashlar-print | System PDF print spooler (CUPS, Windows) | yes | – |
| 2 | ashlar-update | Update checks: latest release from a releases API, semver comparison, background schedule, cached result, “later” per version | yes | – |
| 2 | ashlar-integrations | Integrations: tool servers installed from a catalog (download with resume and sha256, code-signature check, versions, enable/disable) and run as MCP servers over stdio; their tools for an assistant | yes | – |
| 2 | ashlar-mcp-http | MCP over streamable HTTP on loopback for an open app: bearer token, Host/Origin checks, a gate, sessions | yes | – |
| 2 | ashlar-agents | Connect coding agents to an app: its MCP server in each agent’s config, skills and rules from a docs category, install and uninstall | yes | – |
| 3 | ashlar-telemetry | Always-on crash, performance and usage telemetry over ashlar-trace: client, transport, hooks for commands, logs, frames and dialogs | yes | – |
| 3 | ashlar-mcp | MCP server generated from the command registry (stdio), headless or bridged | yes | – |
| 4 | ashlar-cli | Standard CLI verbs over the command registry | yes | – |
| 4 | ashlar-assistant | In-app AI assistant harness, independent of the model: conversation and tool types, providers (an account backend, Anthropic, OpenAI-compatible), the agent loop with approvals, keys in the OS keychain, an MCP server as the tool host | yes | – |
| 4 | ashlar-debug | ashlar-debug: drive a running app’s UI from the shell (snapshot, refs, click, wait, screenshot, scripted UI tests) | yes | – |
| 5 | ashlar-ui | UI components on egui: theme tokens, controls, layout, docking, icons | no | yes |
| 6 | ashlar-app | Runtime: App builder, frame scheduler, crash guard, logging, telemetry, agent control | no | yes |
| 7 | ashlar-rs | Umbrella crate (imported as ashlar): re-exports the others behind features | no | – |
| 8 | ashlar-example-gallery, ashlar-example-hello, ashlar-example-notes, ashlar-example-trace-demo-cli, ashlar-example-trace-loadgen, ashlar-example-web-hello | Example apps | no | – |
| 9 | xtask | Repo gates and tools (cargo xtask …) | – | – |
Adding a crate: give it the lowest rank its dependencies allow, add it to LAYERS and to this
table, add [lints] workspace = true to its manifest, and add it to [workspace.dependencies]
with both path and version.
Design principles
- Never crash. No
unwrap/expect/panic!/unreachable!/todo!/unimplemented!and nounsafeoutside tests. Every input-derived number is hostile:get()instead of indexing, checked or saturating math, capped allocations, bounded recursion. Lock poisoning is not fatal. Background work runs undercatch_unwind. Enforced by the workspace clippy lints andcargo xtask deny-panics. - No app code, no app names. A crate here serves more than one app and never depends on one.
App-specific widgets (a timeline, a CAD canvas) stay in the apps and are built from ashlar’s
primitives. Docs say “a video editor”, “a DAW”. Enforced by
cargo xtask names. - A headless core. Documents, commands, jobs and files never touch the UI, so they run in tests, in a CLI, behind MCP and in the browser. The UI is openly egui; there is no renderer-agnostic abstraction.
- Everything is a command. Menus, shortcuts, the palette, the CLI and MCP all go through one registry and one execute chokepoint, so anything a user can do an agent can do.
- Performance is a feature. Idle apps do zero work (repaint on demand). Caches are keyed by
revision numbers, not by diffing. Long lists are virtualized. No blocking I/O on the UI thread.
Edits are copy-on-write with structural sharing. Every hot path has a perf test, and
cargo xtask bench --compareguards it (see below). - Motion is a policy. Nothing animates on first show, the user’s own hand is never
animated, reduced motion is applied per value class (
ashlar-motion’sClass), and settled animations request no frames. - Stable contracts. Items marked
CONTRACTinashlar-commandare shared across crates built in parallel and change only additively.
Performance gate
Hot paths have perf tests: #[test] #[ignore] functions whose names start with perf, usually in
tests/perf.rs. They print their numbers as
bench: <metric> <value> <unit> e.g. bench: edit_100k 812 ns
Units ending in /s are higher-is-better; all others lower-is-better. A test that prints no
bench: line is measured by wall time, which is noisy, so prefer explicit metrics.
One step is not a test: perf_hello_size builds examples/hello (an empty document, a heading
and a checkbox) in release, strips a copy and reports hello_stripped_bytes, the size the
framework adds to every app. cargo build --profile release-small (fat LTO, one codegen unit,
opt-level = "s", stripped) is the workspace’s size profile.
cargo xtask bench # writes target/bench/bench.{json,md}
cp target/bench/bench.json baseline.json # before a change
cargo xtask bench --repeat 3 --compare baseline.json # fails on a >5% regression
--repeat N keeps the best value of each metric over N rounds; --threshold PCT changes the 5%;
--filter TEXT runs only matching tests.
Dependencies
cargo xtask ci checks duplicate versions and sources of every dependency when cargo-deny is
installed (deny.toml); cargo xtask advisories also checks the advisory database.