Skip to content

Latest commit

 

History

History
297 lines (250 loc) · 15 KB

File metadata and controls

297 lines (250 loc) · 15 KB

Developer notes

Ramp-up guide for new contributors. The README covers the elevator pitch and basic build; this file covers how the codebase actually fits together, the invariants you must not break, and the gotchas that cost time to rediscover.

Reading order for day one: this file → docs/PLAN.md (architecture rationale) → docs/adr/0002 (core owns the document) → crates/robotstudio-core/src/engine.rsapps/frontend/src/transport/index.ts. That path explains 80% of the design.

The one-sentence architecture

A Rust core owns the entire document and all domain logic; three interchangeable shells (Tauri desktop, wasm-in-browser, test mock) drive it through one JSON command interface; the React frontend is a thin view that never holds document truth.

Everything else follows from that sentence:

  • The core is a pure state machine (ADR 0002). The document, undo/redo, snapping, the checker, licensing, telemetry — all live in robotstudio-core::Engine. The frontend sends commands and renders the DocumentView it gets back. If you find yourself computing document state in TypeScript, stop — that logic belongs in Rust.
  • The core is clock-free, I/O-free, and wasm-clean (ADR 0003). No std::time, no file access, no network in robotstudio-core. Shells supply time (e.g. today_day for license checks) and do all I/O. This is what makes the browser build and deterministic tests possible.
  • One transport seam. apps/frontend/src/transport/index.ts defines CoreTransport with three implementations: Tauri IPC, wasm, and a mock for unit tests. Nothing outside src/transport/ may import @tauri-apps/api or the wasm package. This rule is what keeps the frontend runnable in a plain browser and in jsdom.

Repo map

crates/                     Rust workspace (all logic)
  robotstudio-core/         Engine, document, commands, snap, checker glue,
                            licensing, telemetry, bundle verification, math
  robotstudio-rsp/          .rsp part-package parsing + schema validation
  robotstudio-rules/        Data-driven rule engine (checker rule definitions)
  robotstudio-kin/          Kinematics: FK/IK for the supported arm families
  robotstudio-sim/          Deterministic sim: cycle check, live run, collision
  robotstudio-export/       URDF/USDA/STEP-outline/JSON exporters
  robotstudio-bom/          BOM derivation + CSV
  robotstudio-wasm/         wasm-bindgen bindings over the core (browser shell)
apps/
  desktop/src-tauri/        Tauri 2 shell: IPC commands, dialogs, resources,
                            license persistence, crash log, close-confirm
  frontend/                 React 18 + Vite 6 + TS strict + R3F + Zustand
  e2e/                      Playwright suite (runs against the BUILT frontend)
catalog/                    200 parts / 58 vendors — ALL GENERATED, see below
  tools/build_catalog.py    The generator; catalog/parts is its output
  rules.json                Checker rule data (CHK families)
spec/                       Open .rsp/.rse spec: JSON Schemas + exemplars
templates/                  Starter projects (.robotstudio files)
environments/               .rse environments for Live Run
behaviors/                  Sim behavior definitions (e.g. sortation)
scripts/                    Build/sync/validation tooling (see below)
docs/                       PLAN, ADRs 0001–0016, budgets, telemetry policy,
                            milestone logs (M0–M5), install-test procedure
docs-site/                  Generated spec site (build-spec-site.mjs)
_clickable-prototype/       The original HTML UX reference — the look/feel
                            source of truth; not shipped

PRD.md is scope truth; ROADMAP.md is status truth (legend at the top — note 🟢 means "verified on-device by the maintainer" and is set only by them).

The core, module by module

crates/robotstudio-core/src/:

  • doc.rsDocument: instances, connections, scenario. The serialized form is the file format (cell-project-1, ADR 0009): field order is append-only, every top-level field is required (no serde defaults), and the format is product-name-neutral by design.
  • command.rs / engine.rs — the command pattern. Every mutation is a Command applied through Engine::apply, which returns an outcome + fresh view and pushes undo state. Add features as new commands, not new mutation paths.
  • snap.rs — interface-based snapping (candidates, capture resolution, orientation cycling). The drag UX in the frontend is a thin driver over this.
  • registry.rs — the part registry, populated from .rsp packages at boot.
  • view.rs — everything serialized to the frontend (DocumentView etc.). View payloads are camelCase; command payloads are snake_case (serde defaults). The TS mirror lives in apps/frontend/src/core/types.ts — keep the two in sync by hand.
  • checker.rs + robotstudio-rules — data-driven checks (CHK-* families) defined in catalog/rules.json, evaluated after every commit.
  • license.rs — offline Ed25519 license keys (ADR 0015). See "Licensing".
  • telemetry.rs — event log, drained by shells (ADR 0016). See "Telemetry".
  • bundle.rs — signed catalog-bundle manifest verification (ADR 0014).
  • world.rs, math.rs, system.rs — transforms/placement, math helpers, and the electrical/data system graph.

The STEP sidecar (ADR 0005) is designed but not built — the only place unsafe would ever be allowed. The main workspace denies unsafe_code, unwrap_used, dbg_macro, and todo at the workspace level; test modules opt out of unwrap locally.

The frontend

apps/frontend/src/:

  • state/ — Zustand stores. doc.ts is the big one (view, checker, BOM, selection, dirty/save/open, toasts); also drag.ts (placement gestures), ui.ts (view/pane/prefs/telemetry consent), live.ts (Live Run), license.ts, theme.ts.
  • components/ — the shell chrome (TitleBar, Rail, CatalogPanel, SidePanel, StatusBar, modals) and the R3F Stage/CenterView.
  • transport/ — the seam (see above). The wasm transport also bootstraps the catalog: it prefers one /catalog/combined.json fetch (200 parts would otherwise be ~1200 requests — this took the E2E suite from timing out to ~20s) with per-part fallback.
  • core/types.ts — hand-maintained TS mirror of the core's JSON surface.
  • TS is strict; any is banned (lint-enforced). Keep it that way.

State rule of thumb: document truth lives in the core; UI-only flags (hidden/locked instances, open panels, drag state) live in stores and are never persisted into the document (ADR 0002).

Data: catalog, templates, environments

  • catalog/parts/ is entirely generated. Never hand-edit a part. python3 catalog/tools/build_catalog.py regenerates all 200 parts from the definitions in that script (rm -rf catalog/parts first is safe). Add or fix parts by editing the generator. Placeholder meshes come from scripts/make-placeholder-glb.py (note: cylinders take "size": [radius, height]).
  • After changing catalog/templates/environments, run node scripts/sync-web-assets.mjs to refresh the frontend's static copies, including combined.json.
  • templates/*.robotstudio are ordinary project files; the frontend fetches them at boot for the template picker.
  • environments/ hold .rse packages (manifest + zones + geometry) consumed by Live Run (M4).
  • spec/ is the published, versioned spec for third parties — schemas are CC-BY-4.0, exemplar parts included. node scripts/build-spec-site.mjs renders it to docs-site/.

Dev environment & everyday commands

Prerequisites: Rust (pinned by rust-toolchain.toml), Node 20+, pnpm 9+, and for the wasm harness wasm-bindgen-cli matching the workspace's wasm-bindgen version.

pnpm install

# Rust: the full local gate
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all --check
cargo check --workspace --lib --target wasm32-unknown-unknown

# Frontend
pnpm --filter frontend test        # vitest (unit, jsdom + mock transport)
pnpm --filter frontend lint
pnpm --filter frontend build       # tsc --noEmit && vite build

# Wasm bindings (needed before browser/E2E work if the core changed)
./scripts/build-wasm.sh

# E2E — see the gotcha below
pnpm --filter frontend build && pnpm --filter e2e test

# Run the desktop app (long-lived — run in your own terminal)
pnpm --filter desktop dev

# Repo invariant checks
./scripts/check-product-name.sh
python3 scripts/validate-exports.py   # exporter output validation

Testing strategy (ADR 0013)

Three layers, each with a distinct job:

  1. Rust unit tests (~90) — domain logic, in-crate. Determinism matters: the sim is tested for identical output across runs/platforms (golden-run legs in CI).
  2. Vitest (apps/frontend/src/*.test.tsx) — chrome behavior against the mock transport (makeMockTransport(overrides) + setTransportForTests). jsdom; the R3F stage no-ops (canvas warnings in output are normal).
  3. Playwright (apps/e2e/tests/, 35 specs) — the real product in a browser via the wasm core, served by vite preview on :4173. helpers.ts is the toolkit: appReady/startEmpty/openTemplate, real-pointer drag helpers, and read-only store bridges (window.__appDebug, __stageDebug). Gestures always go through real pointer/keyboard events; the bridges only observe.

The E2E gotcha (learn this now): Playwright serves the built frontend. After any frontend change, run pnpm --filter frontend build before pnpm --filter e2e test, or you will debug stale code. Similarly, core changes need ./scripts/build-wasm.sh first.

Other test conventions: appReady seeds hints-dismissed-v1 so onboarding hints don't block unrelated specs (onboarding.spec.ts covers them unseeded); perf.spec.ts runs in its own serial project after the parallel suite because frame-time budgets need an uncontended CPU (docs/budgets.md holds the measured numbers).

Invariants — break these and CI (or a reviewer) breaks you

  • Product display name appears in exactly three code locations (ADR 0009, enforced by scripts/check-product-name.sh): core PRODUCT_NAME, vite.config.ts, tauri.conf.json productName. Everywhere else derive it (Rust: format!("{PRODUCT_NAME} …"); TS: __PRODUCT_NAME__). File formats and the spec never contain it — the product may be renamed before launch.
  • No unsafe (workspace deny), no unwrap in library paths, no dbg!/todo!. No TS any.
  • Transport isolation: only src/transport/ touches Tauri or wasm APIs.
  • Document format is append-only and every persisted format is product-neutral.
  • Core stays clock-free/I-O-free/wasm-clean; shells inject time and do I/O.
  • Secrets never enter the repo — the license issuer's private seed lives outside; only public keys are embedded.
  • Telemetry is content-free (ADR 0016, docs/telemetry.md): event names and counts only — never search queries, never file paths, never part freetext beyond catalog IDs. part-search records a result count. Dev builds never persist telemetry (cfg!(debug_assertions) guard).

Subsystem crib notes

Licensing (ADR 0015). Key = base64url(payload-json).base64url(ed25519-sig), verified offline against ISSUER_KEYS in license.rs. Free tier needs no key; is_pro() is true in Valid and Grace (30 days past expiry). Issue dev keys with cargo run -p robotstudio-core --example license_tool -- issue <seed-hex> <licensee> <days> (test seed 0707…07, kid dev-2026). The embedded dev-2026 key is a designed launch blocker — it must be rotated before any public build (procedure in docs/milestones/M5.md).

Telemetry (ADR 0016). Core appends TelemetryEvents to a bounded log; shells drain every 60s gated on user consent (default off unless VITE_TELEMETRY_DEFAULT=on at build time). There is deliberately no uploader and no backend — desktop appends JSONL to a ~1MB-bounded local file, browser keeps a localStorage ring.

Catalog bundles (ADR 0014). Distribution is static hosting: a signed bundle-manifest.json (same Ed25519 trust root as licensing) built by scripts/build-bundle.py, signed with license_tool sign-bundle, checked at boot by sync.ts when VITE_CATALOG_BUNDLE_URL is set; silent no-op offline. Per-file download/install is deferred (helper file_matches exists in core).

Project files. Save/Open lives in the transport seam (saveProjectToFile/openProjectFromFile): native dialogs on desktop (driven from Rust via tauri-plugin-dialog — the webview needs no plugin permissions), download/upload in the browser. Dirty state lives in the doc store and is mirrored to the desktop shell (set_dirty) for the native close-confirm; the browser uses beforeunload.

Desktop resource lookup. The shell finds catalog/, environments/, templates/ by walking up from the exe and cwd, plus the bundled locations (../Resources on macOS, ../lib/robotstudio-desktop on Linux). CATALOG_DIR env overrides. If parts don't load in a packaged build, start here.

CI and releases

.github/workflows/:

  • ci.yml — Rust tests/clippy/fmt, wasm check, frontend unit + E2E, and the cross-OS golden-run determinism leg.
  • exports.yml — exporter output validation against real toolchains.
  • release.yml — on v* tags: tauri-action builds dmg/app/msi/nsis/deb/ appimage on a 3-OS matrix into a draft release; Apple signing activates only when the APPLE_* secrets exist. Tags containing -m are marked prerelease.

Milestone history: v0.0.1-m0v0.4.0-m4, v1.0.0. Each milestone has a log in docs/milestones/ recording judgment calls and deviations — read M5's for the current launch checklist and open decisions.

Gotchas collected the hard way

  • Rebuild before E2E (frontend and wasm) — see Testing above.
  • catalog/parts is generator output; edit build_catalog.py, then rerun it and sync-web-assets.mjs. Part IDs must be regex-safe (no slashes in PN- derived IDs) and slugs unique — the generator's uniqueness check is your friend.
  • The empty/default project JSON must carry all six top-level document fields (schema, name, nextInstance, instances, connections, scenario) — the format has no serde defaults.
  • Playwright strict mode: prefer data-testids; watch for accessible-name collisions ("New" vs "New…").
  • In Tauri commands, blocking dialog APIs must run off the main thread — make those commands async. On close-confirm, call window.destroy(), not close(), or the handler re-enters and re-prompts.
  • The webview's window.confirm is not reliable across wry platforms — desktop confirms go through the confirm_discard command instead.
  • jsdom canvas "Not implemented" warnings in vitest output are expected noise from the R3F stage probe.
  • serde enum tags in this codebase are kebab-case; the TS mirror must match exactly (state: "grace", days_left snake_case inside view payloads that serialize structs directly — check types.ts precedent before assuming).