RFC-037 · Physics Kernel — the technical architecture for the Lab subsystem + MCP server
Status: Draft · 2026-08-08 · Closes into: ADR-TBD (physics-kernel package boundary + FigureSpec + MCP hosting) · Related: PRD-033 (vision), UXS-015 (look & feel), ADR-030 (fly-physics pure-function isolation), RFC-034/#412 (launch-ascent engine), RFC-024 + RFC-035 (containerized VPS stack + nginx/subdomain pattern) · Tracking: #458 (epic) · slices #459–#464
Why this is an RFC. PRD-033 sets the vision (liberate the physics into a Lab + an MCP server) and UXS-015 the look & feel; this RFC is the how it's built, and four load-bearing contracts have to be frozen before any code because every downstream slice and both consumption surfaces depend on them: (1) the kernel package boundary — what is "kernel" vs "app", enforced so the pure core can't regain a framework dependency, imported unchanged by both the SvelteKit build and a standalone Node process; (2) the
FigureSpeccontract — the single declarative figure description that the Lab renderers and the MCP responses both emit, so "draw the result" is defined once; (3) the card / serialization model — the portable atom that lets one card live in Notebook, Focus, and Canvas and persist across sessions; (4) scientific credibility as a public contract — exposing formulas to the world forces choosing one authoritative ephemeris and capping unbounded compute. Getting any of these wrong bakes a shape we can't cheaply walk back into a public API and a persisted document format.
1 · Scope
This RFC owns the technical spine shared by every surface: the kernel, the compute/figure/card contracts, the MCP server, the data strategy, and the slice plan. The product vision lives in PRD-033; the interaction/visual design in UXS-015. Where this RFC says "Notebook / Focus / Canvas," those are the three views defined in UXS-015 over one card model.
2 · Grounding — what already exists (inventory, 2026-08-08)
A two-agent sweep read ~45 modules. Verdict: the physics is overwhelmingly pure already (no three, no Svelte stores, no DOM, deterministic). The kernel is ~80% lift-and-shift.
| Domain | Modules | Representative formulas |
|---|---|---|
| Ascent / launch | orbital/ascent-physics.ts (+constants) | 2-DOF gravity-turn integrator, Tsiolkovsky Δv, pressure-interpolated thrust/Isp, staging, closed-loop insertion |
| Descent / EDL | orbital/descent-physics.ts (+constants, 11 bodies) | 1-DOF entry integrator, drag, dynamic pressure, Mach, per-body atmospheres |
| Transfer / windows | lambert.ts, lambert-grid.ts, orbital.ts, orbital/mission-arc.ts | Lambert (Lagrange-Gauss), porkchop grid, vis-viva, Kepler position, Hohmann + Keplerian transfer ellipse |
| Cislunar | orbital/cislunar/cislunar-geometry.ts | ECI translunar/trans-Earth coasts, lunar orbit/flyby, spiral burns |
| Interplanetary | interplanetary-geometry.ts | Analytic + waypoint-replay heliocentric trajectories |
| Ephemeris / sky | astronomy/{time,planets,moon,horizontal,index}.ts | JD/GMST/LST/obliquity, JPL Standish planet positions (Newton Kepler solve), Schlyter Moon, ecliptic→equatorial→horizontal, alt/az sky-pointing |
| Satellite / TLE | satellite/{tle,propagate,look-angles,index}.ts | TLE parse, Kepler + J2 propagator (own, not satellite.js), look-angles, next-pass prediction |
| Propulsion / engines | orbital/{engine-registry,launcher-engines}.ts | ~22 engines: thrust/Isp/mass/cycle; launcher↔engine cross-refs |
| Utility | parse-delta-v, scale, planet-stats, moon-projection, earth-regimes, orbit-regime-match | Δv parse, escape-velocity/gravity tables, light-time, regime bounds |
This table is the formula-registry seed (§5): each row becomes one or more registered formulas that the palette, the views, and the MCP tools all derive from.
3 · Contract 1 — the kernel boundary (D1: resolved)
Decision: src/lib/physics/ subtree, not a separate workspace package (revisit only if the MCP build forces it). Rationale: single tsconfig, no monorepo wiring, and the pure modules already live under src/lib — this is a move, not a restructure.
- Move the PURE modules from §2 under
src/lib/physics/{ascent,descent,transfer,cislunar,ephemeris,satellite,propulsion,util}/, keeping their sibling.test.ts. - Enforce purity with an ESLint
no-restricted-importsboundary onsrc/lib/physics/**: forbidthree,svelte,$app/*,$lib/*(app internals), DOM globals. This is the guard that keeps the kernel re-couplable-proof — the thing ADR-030 did by convention becomes a lint gate. - Public index
src/lib/physics/index.ts— the only entry both consumers import. Per-export units + citations in doc-comments (public-contract grade). - Impure code stays out:
satellite/tle-source.ts, thefetch-based loaders, the/sciencecross-ref lookups (see §7 decoupling).
4 · Contract 2 — FigureSpec (the draw-once contract)
Every formula result may carry a declarative figure description, not a rendered image. One shape, consumed by the Lab's SVG renderers and returned verbatim by the MCP server so an agent can render it anywhere.
type FigureSpec =
| { kind: 'transfer-ellipse'; frame: 'heliocentric'; bodies: {...}; arc: Vec2[]; marks: Annotation[] }
| { kind: 'porkchop'; depDays: number[]; arrDays: number[]; grid: number[][]; units: 'km/s' }
| { kind: 'dv-waterfall'; segments: {label; dv; kind}[] }
| { kind: 'force-diagram'; vectors: {label; dir; magN}[]; body: 'rocket' }
| { kind: 'curve'; x: {label;units;vals}; y: {label;units;vals}; marks?: Annotation[] }
| { kind: 'orbit' | 'ground-track' | 'sky-chart' | 'entry-corridor' | 'cislunar-eci'; ... };
// every FigureSpec carries: provenance: { source: 'computed'; module: string }, and NEVER 'illustration' from the kernel.- Renderers draw on the
station-blueprint.tsteal-grid substrate (UXS-015 §figure language). The sketchy-but-exact styling lives in the renderer, never in the spec — the spec is pure data. - Provenance is intrinsic. Kernel-emitted specs are always
computed. The generative/illustration layer (PRD-033 T3) is a separate artifact type the kernel never produces — this is the honesty line made structural.
5 · Contract 3 — the formula registry + card model
The single source that the palette, all three views, and the MCP tool generator derive from.
interface FormulaDef<I> {
id: string; // 'tsiolkovsky', 'lambert-transfer', ...
schema: JSONSchema; // typed inputs + UNITS + server-side caps (e.g. steps ≤ N)
compute(inputs: I): FormulaResult;
}
interface FormulaResult {
values: Record<string, { value: number; units: string }>;
figure?: FigureSpec;
status: { ok: true } | { ok: false; reason: string }; // fail-honest: infeasible carries a reason
}The card (the UXS-015 atom) is the serializable instance:
interface Card { id: string; formulaId: string; inputs: Record<string, number|string>;
wires?: { fromCard: string; output: string; toInput: string }[] }- A Notebook =
{ title, cards: Card[] }(order = narrative; a card may reference an upstream card's output). - A Canvas =
{ cards: Card[], positions, edges }— same cards, plus layout + explicit wiring. - Serialization (D-serialize, new): compact form in the URL (shareable, matches IA §state-persistence's URL-encoded exception) for small worksheets;
localStoragefor the working session; a.orrlab.jsonexport/import for durable documents. One codec, all three views. - One registry → MCP tools auto-derived. Each
FormulaDef.schema+computegenerates one MCP tool. The registry is the single place a new formula is added; every surface picks it up.
6 · Contract 4 — MCP server (hosting, transport, auth)
- Subdomain / infra.
mcp.orrerylearn.com, a new container behind the existing nginx on the VPS. Reuses the RFC-024/035 compose + subdomain-vhost pattern; no new infra class. - Transport. MCP Streamable HTTP (current remote-server transport; Claude.ai custom connectors speak it).
- Auth (D3). "Anybody with credentials logs in" → OAuth 2.1 (the spec's authorization framework) for public multi-user; a shared bearer token / nginx-gated credential for the private beta first. Highest-uncertainty area — verify against the current MCP auth spec before the beta slice.
- Statelessness = security asset. Pure compute, no per-user state, no mutation, read-only. Nothing to leak; horizontally scalable.
- Abuse bound.
computePorkchopGridand the integrators are unbounded-cost at largesteps— the schema (§5) caps iteration counts server-side; this cap is part of the tool contract, not an afterthought.
7 · Decoupling items (the non-clean six)
- Static-JSON coupling —
lambert-grid.constants,mission-arc,cislunar-events,satellite/stationsVite-importplanets.json/small-bodies.json/cislunar-phase-science-map.json/station-tles.json. Resolve per §8. - Fetch/loader boundary —
loadLaunchProfile/loadDescentProfilecallfetch; pure helpers move to the kernel, loaders stay app-side adapters. - Genuinely impure — excluded:
satellite/tle-source.ts(localStorage + fetch +Date.now+ memo). - Two ephemerides — §9.
- Non-determinism to scrub —
earth-sidereal.ts(new Date()default);porkchop.tslocale-dependent date/colour helpers move out of the physics namespace (rendering, not physics). - App-domain
/sciencelookups —scienceRefsForet al. stay app-side.
8 · Data strategy (D2)
- D2-a inline (bake ephemeris rows into
.ts) — portable, but duplicates source-of-truth JSON. - D2-b co-bundle (ship the JSON, resolve in both consumers) — single source; build-config cost.
- D2-c inject (caller passes the data) — purest boundary; ripples to call sites.
- Decision: D2-b for ephemeris tables — the JSON stays the single source of truth (PA §"data over code") + a codegen drift-check that fails CI if the baked
.tsdiverges from the JSON; D2-c where the caller already holds the data.
9 · The authoritative ephemeris (public-contract critical)
Two implementations exist: orbital.ts#keplerPos (first-order mean-longitude approximation, /fly's cheap smooth path) and astronomy/planets.ts#heliocentric (JPL Standish + Newton solve, ~arc-minute, 1800–2050). Decision: the Lab and MCP expose the JPL-Standish path as authoritative; the approximation is either unexposed or labelled fast with a documented error bound. /fly internals unchanged. Verified 2026-08-08 by source read: orbital.ts#keplerPos advances mean longitude linearly and uses it directly as the true anomaly (docstring: "no eccentric anomaly solve … accurate to first order"); astronomy/planets.ts#heliocentric uses the JPL Standish ELEMENTS table + an 8-iteration Newton solveKepler (docstring: "~arcminute accuracy, valid 1800–2050"). Authoritative planet positions for the Lab + MCP = astronomy/planets.ts#heliocentric.
10 · Rollout — slices (rollback-safe, Notebook-first per PRD-033)
- S1 — kernel boundary. Move pure modules to
src/lib/physics/, add the ESLint purity gate, resolve JSON coupling (§8). Provenpm run preflightgreen + app behaviour unchanged. No new surface. - S2 — registry + FigureSpec + renderers. The formula registry (§5), the
FigureSpectype (§4), and the SVG renderers on the teal grid. Public-contract tests + citations. - S3 — Lab: Notebook + Focus (v1 home). The
/labroute, card model + serialization codec, Notebook and Focus views over the registry, honesty line + fail-honest states (UXS-015). This is the in-app payoff and it de-risks the kernel in-process. - S4 — MCP server, private beta. Standalone Node process, tools auto-derived from the registry, one domain first (transfer: Lambert + vis-viva + Tsiolkovsky), bearer auth,
mcp.subdomain. - S5 — Canvas + wiring + promote (T2). The graph engine (sockets, edges, cycle-check, recompute), Canvas view, promote-to-Notebook. The expensive/novel slice — deliberately after S3 proves the card model.
- S6 — MCP OAuth + full tool surface, then the generative/illustration layer (T3) and the agentic ask-box (T4) as later, separately-gated work.
Each slice is inert until wired. S1 has no external exposure and can land independently of auth/hosting.
Tracked as: epic #458 · S1 #459 · S2 #460 · S3 #461 · S4 #462 · S5 #463 · S6 #464. Scheduled v0.9 — parked behind v0.8 completion; do not start S1 until v0.8 closes.
11 · Alternatives considered
- A — reimplement formulas in the MCP server. Rejected: duplicates the source of truth, guarantees drift, discards the unit tests. The value is one registry.
- B — expose the whole app over an API. Rejected: drags Three/Svelte/DOM headless, couples the public contract to route internals, can't serve the in-process views.
- C — publish the kernel to npm. Compatible, natural post-S6 follow-on; out of scope (the ask is a hosted MCP surface).
- D — per-view bespoke data models. Rejected: the whole subsystem coherence comes from one card model across three views (UXS-015).
12 · Non-goals
- Changing any physics formula, constant, or its accuracy (extraction is behaviour-preserving; ADR governs constants).
- Real-time / N-body / higher-precision propagation — the kernel exposes today's fidelity, honestly labelled.
- Moving impure data layers (TLE live-fetch, profile fetch) into the kernel.
- The Canvas graph engine in v1 (S5, second workspace).
- npm publication (§11-C); the generative layer (T3) and agentic ask-box (T4).
13 · Decisions
Resolved:
D1 — kernel home →
src/lib/physics/subtree + ESLint purity gate (§3).D5 — route/views →
/lab; Notebook / Focus / Canvas (UXS-015).D2 — JSON-data strategy → D2-b co-bundle, JSON stays the source of truth + a CI drift-check (§8).
D-serialize — card codec → one codec, three outputs: URL-compact (shareable) +
localStorage(session) +.orrlab.json(durable export/import); freeze the URL grammar before S3 (§5).D-verify — ephemeris fidelity → confirmed by source read (§9); authoritative =
astronomy/planets.ts#heliocentric.D3 — MCP auth model → shared bearer token (private beta) → OAuth 2.1 (public multi-user); verify vs the current MCP auth spec before S4. (decided 2026-08-08)
D4 — new dependency
@modelcontextprotocol/sdk(+ an HTTP server dep) → approved, MCP process only, gated to S4; S1–S3 add no new runtime deps (KaTeX already present). (decided 2026-08-08)
All kickoff decisions are resolved; slices S1–S6 are cleared to be tracked as issues.