RFC-037 · Physics Kernel — the technical architecture for the Lab subsystem + MCP server
Status: Shipped (updated 2026-10-16: src/lib/physics/ + server/mcp; the closing ADR it names was never written (ADR-087 covers only the systems layer)) · was: 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'; ... };
// NOTE: 'cislunar-eci' is emitted by an APP-SIDE geometric producer (see Amendment 01
// A01.2a), NOT the kernel — it carries fidelity 'geometric', never 'computed'.
// every FigureSpec carries (UPDATED by Amendment 01 D11/D12):
// provenance: { fidelity: 'computed' | 'geometric' | 'replayed-published'; module: string }
// assumptions: string[] // e.g. ['two-body only','no drag','non-rotating frame']
// The kernel emits ONLY fidelity:'computed'; 'geometric'/'replayed-published' come from
// app-side producers and render in a visually distinct register. NEVER 'illustration' here.- 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 and multi-fidelity (Amendment 01 A01.2a/D11). Kernel-emitted specs are always
computed; app-side producers may emitgeometric(analytic shape for continuity) orreplayed-published(interpolated authored data) — the kernel may NOT, and renderers MUST keep the three registers visually distinct (enforced by a register-distinction golden-master in S2.5, Fable-5 round-2 NEW-2). The generative/illustration layer (PRD-033 T3) is a separate artifact type nothing in the kernel 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; reasonKey: string }; // fail-honest: infeasible carries an i18n reason KEY
// UPDATED by Amendment 01 (all user-facing text is i18n-keyed — Fable-5 r5 MAJOR-2):
assumptions: string[]; // D12 — message KEYS naming what the model ignores (teaching-honesty payload)
epochAgeDays?: number; // D13 — TLE/ephemeris-derived results carry data-staleness age (numeric, not text)
}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 Caddy edge on the VPS (ops/deploy_caddy_vhosts.sh— Caddy is the actual edge; nginx is only the inside-container static server). Reuses the RFC-024/035 compose + subdomain-vhost pattern; no new infra class. (Fable-5 r5 MINOR-5: was wrongly "behind the existing nginx".) - 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 extracted physics accuracy (extraction is behaviour-preserving) — but Amendment 01 adds new closed-form mechanics + per-milestone astrodynamics formulas (A01.1/A01.2b). No N-body/CFD/higher-precision numerical propagation.
- Moving impure data layers (TLE live-fetch, profile fetch) into the kernel.
The Canvas graph engine in v1.NOW IN v0.9 (operator "all of it", A01.5/A01.7) — T2 Canvas + wiring, T3 generative layer, T4 ask-box, and MCP OAuth all ship in v0.9 (gated beta).- Still out: npm publication of the kernel (§11-C).
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.
Amendment 01 (2026-08-29) — mechanics domain, the Goal model, full-coverage registry
Status · Shipped v0.9.1 (A01 realized:
src/lib/physics/mechanics/,registry/,systems/,server/mcp; result of the adversarial review not verified) · was: Draft, operator-approved shape · Pending adversarial review (advisor / Fable-5) before re-slicing · Implements the technical spine for PRD-033 Amendment 01 (first-principles / milestone ladder / full coverage) · Companion notes ·docs/wip/_archive/2026-08-29-kernel-scope-expansion-classical-mechanics.md,docs/wip/_archive/2026-08-29-s1-kernel-boundary-manifest.md
PRD-033 A01 widened the product to a first-principles learning environment organized as a ladder of mission-goal milestones exposing the entire built physics library. This amendment adds the technical pieces that requires. Contract 1 (§3) is unchanged; Contracts 2 (§4 FigureSpec) and 3 (§5 FormulaResult) are AMENDED in place by D11/D12/D13 (provenance fidelity axis, assumptions, epochAgeDays) — the §4/§5 code blocks have been updated so an implementer freezing them verbatim gets the honest contract (Fable-5 round-2 NEW-2). S1 (kernel carve) is unaffected. Scope note (operator 2026-08-29): the v0.9 MVP is the ENTIRE goal ladder + kernel + mechanics + Lab + MCP — no subset; M1 is built first, but everything ships.
A01.1 · New kernel subdomain — physics/mechanics/ (greenfield)
A new pure subdomain under the §3 boundary: src/lib/physics/mechanics/{kinematics, dynamics,momentum,energy,gravitation}.ts. Same purity gate, same FormulaDef registration, same citation grade as the extracted modules. Greenfield content (not a move), so it is out of S1 and lands with S2 (registry).
Compose honestly, don't overclaim (Fable-5 MAJOR 4). The reuse story is per-body helpers, not "the same integrator." A drop from rest is the singular case of the EDL integrator — its dγ/dt divides by velocity, and the code clamps Math.max(v, 1) (descent-physics.ts:542), so a drop from rest doesn't crash — it returns silently wrong numbers (a stronger reason for the purpose-built integrator, not a weaker one); and SURFACE_BODY_KINEMATICS (planet-stats.ts) covers only moon/mars/earth/venus (no Titan — Titan's constants live in descent-physics-constants.ts). Correct model: a purpose-built free-fall integrator in mechanics/ that reuses the clean per-body exports bodyGravity / bodyAirDensity (descent-physics.ts:253/260). Fidelity ceiling unchanged (§12 holds for N-body/CFD/numeric propagation); see A01.2b for the sideways per-milestone new-physics budget.
D10 — one authoritative per-body constants home (Fable-5 MAJOR 4 back-pressure). μ_Earth is currently defined in ≥3 kernel-bound places in different units (cislunar-geometry.ts:14, lambert-geocentric-grid.constants.ts:18, ascent-physics-constants.ts:22) plus planet-stats. S1 moves them as-is (fine), but mechanics/ must NOT add a 4th table — S2 designates util/constants (or util/planet-stats) as the single per-body source and mechanics imports it. Decide the home before S2; this is the one real S2→S1 back-pressure.
A01.2 · Contract 5 — the Goal / Milestone model (the load-bearing new contract)
The registry (§5) gains a Goal as a first-class, ordered, serializable entity — the atom of the milestone ladder. This is the "milestones-as-data, not code" bet PRD-033 A01.5 holds the MVP accountable to.
interface Goal {
id: string; // 'launch-a-rocket', 'get-to-the-moon', ...
titleKey: string; // i18n message key, e.g. 'lab.goal.launch-a-rocket.title'
family: 'spaceflight' | 'observe';
tier: number; // difficulty / ladder rank (M1=1 … M6=6, G7…)
prereqs: string[]; // Goal ids that should precede this one
path: GoalStep[]; // the ORDERED critical path — the curriculum
}
interface GoalStep {
formulaId: string; // a FormulaDef in the registry (§5)
narrativeKey: string; // paraglide message KEY, not raw prose (Fable-5 r5 MAJOR-2):
// all user-facing text is i18n-keyed (×14 locales) + lives in
// the message bundle, never baked into the serialized Goal /
// .orrlab.json. Same for Goal.titleKey, assumptions (key[]),
// and FormulaResult.status.reasonKey. FREEZE this at S2a/S2c.
// Fable-5 MAJOR 2: mirror Contract 3's `Card.wires` EXACTLY — a single string
// can't express M1's own payoff rung (a comparison fed by TWO upstream steps:
// Δv-capacity from Tsiolkovsky vs Δv-to-orbit from the integrator). Freeze this
// shape before D7, not after.
wiresFrom?: { fromStep: number; output: string; toInput: string }[];
}- A Goal renders as a seed Notebook (UXS-015 §Notebook):
path→ ordered cards,narrative→ the prose between them,wiresFrom→ the "↳ result feeds below" reference. No new view primitive; the Notebook already is a lesson. FormulaDefgainstier+prereqstoo (per-formula), so the concept graph exists below the goal level. The visual skill-tree (PRD-033 A01.5: deferred) is a later consumer of this graph; v1 renders a Goal as a flat Notebook and ships no tree UI.- Adding a milestone = adding one
Goal+ any missingFormulaDefs — for the registry/formula work. Honest caveat (Fable-5 BLOCKER 2 / additivity verdict): this is NOT free of structural change at every rung. Additivity holds cleanly for M2–M3 once their figures are honest (A01.2a); it first strains at M4, whose porkchop card needs an interactive cell-selection-as-wire-source primitive no current contract expresses (freeze how a selection becomes a card output at S2, even if built at M4); and each milestone may carry a bounded new-physics budget (A01.2b). "M2 costs a fraction of M1" is the claim; since the MVP ships all milestones (A01.5), M2 is built for real right after M1 and is the first additivity checkpoint — a contract change there (vs just newFormulaDefs + figure kinds) means the architecture failed, caught after one milestone.
A01.2a · Provenance is a fidelity axis, not a binary (Fable-5 BLOCKER 1 · operator-approved)
RFC §4's "kernel-emitted specs are always computed" is too coarse and actively dangerous under a teaching mandate. Some kernel-classified geometry produces scene shapes, not integrated truth: cislunar-geometry.ts's translunarCoast/lunarOrbit build a two-body ellipse pinned behind the Moon for visual continuity ("inclination_deg is then ignored" to close the scene gap — no lunar gravity in the coast); interplanetary-geometry's Tier-2 mode replays authored waypoints. Stamping either computed · orrery-kernel tells a schoolkid "this is computed truth" when it is a cartoon — the PRD's exact make-or-break.
Decision (D11): FigureSpec.provenance.fidelity: 'computed' | 'geometric' | 'replayed-published'.computed = closed-form / integrated physics. geometric = analytic shape for continuity (cislunar Tier-1). replayed-published = interpolated authored data (Tier-2). Renderers MUST visually distinguish the three registers; geometric/replayed may never share the computed register. Paired with the reclassification: cislunar Tier-1 shape-generators move app-side (S1 manifest revised); the real patched-conic TLI/LOI compute (lambert-geocentric.ts, ADR-085, unit-tested vs Apollo bands) stays kernel and is what M2's honest figure derives from.
A01.2b · Per-milestone new-physics budget (Fable-5 BLOCKER 2)
The ladder needs bounded new astrodynamics sideways, not only the mechanics floor. Verified absent from the kernel today: SOI/patched-conic radius (M2), ignition-altitude / hover-thrust inverse (M3), gravity-assist v∞-rotation + Oberth (M6 — assists exist only as authored waypoints + labels), eclipse geometry (G8 — only Moon phases exist). Each is a small, named, closed-form formula (not N-body), budgeted per milestone and pulled demand-driven (A01.3). This is the honest scope, and it must read that way in the PRD.
A01.3 · Full-coverage requirement on the registry
Bidirectional coverage — reconciled with "MVP = the whole ladder" (Fable-5 MAJOR 3 + round-2 NEW-1). Two rules, both hard, that together dissolve the round-1 contradiction (export-vs-FormulaDef granularity, a taxonomy shipping formulas no goal uses) without letting demand-driven quietly drop built capabilities:
- Coverage direction (built → goal), the hard MVP requirement. Because the MVP ships every goal (A01.5), every computable kernel capability must be pulled by some goal — including the ones the first draft's ladder missed: gas-giant-probe and micro-g small-body descent (
DescentBodyhasjupiter,comet_67p,ryugu/bennu/eros/itokawa). Full coverage is the definition of done, so where a capability has no goal, we author the goal, not drop the capability. The export→FormulaDef→goal audit is a real S2 deliverable. - Demand direction (goal → formula), the anti-speculation rule. No
FormulaDefexists unless a goal'spathneeds it — nothing speculative. This is what keeps the classical taxonomy bounded: friction, collisions/restitution, rotational, fluids, thermo stay OUT until a goal pulls them. The cut falls wherever no goal reaches — and since coverage is bidirectional, "no goal reaches it" now means "it's genuinely not a built capability." - Ledger granularity =
FormulaDef, not raw export (infra likepegSolve,commandedPitchRad, TLE-parse internals are not formulas). CI gate: every registeredFormulaDefreachable from ≥1 Goal, AND every kernel capability on the S2 coverage manifest has a goal. Both directions enforced; neither perverse, because the MVP genuinely ships all.
The seed Goal set: Family A (M1–M6 spine + M-return + "land on any world" incl. small-body/ gas-giant) · Family B (G7–G10 observe/orbit) · cross-cutting (engines, scales-time). Editorial /science domains are excluded by construction (not computable → no FormulaDef).
A01.4 · FigureSpec additions (§4)
Mechanics needs a few FigureSpec kinds; most already exist or are minor: projectile/ trajectory arc (reuse transfer-ellipse's arc renderer), v–t / a–t curve (curve, already defined), general free-body diagram (force-diagram, already defined — generalize body:'rocket' to any labelled mass), energy bar/waterfall (reuse dv-waterfall styling). Net new renderer work is small.
D12 — assumptions: string[] on FormulaResult + FigureSpec (Fable-5 MAJOR 7). Provenance labels who produced a figure; it does not label what the model ignores — and for a first-principles teaching audience, the model assumptions ARE the honesty payload. A bare "computed" caption on a two-body coast / no-drag projectile / non-rotating frame stamps institutional authority on a simplification without naming it. Every formula declares its assumptions ("two-body only", "no atmospheric drag", "non-rotating frame", "point mass"), rendered as a margin note. One field now vs a public-contract break later — and it converts every simplification into a first-principles teaching moment, which is the brand. (Credit: the kernel is better than assumed — ascent-physics.ts:677 already models Earth's rotation contribution, 465.1·cos(lat) m/s.)
A01.5 · Slice impact — re-slicing pending (do NOT reorder yet)
This is a scope delta to be folded in at the re-slicing step (after adversarial review), not a live reorder. Provisional shape:
- S1 — unchanged (except the
interplanetary-geometry.tssplit at S1.2 — S1 manifest). - S2 — must SPLIT; it is now a mega-slice everything hangs off (Fable-5 round-3 problem 3). Post-amendment S2 = registry + FigureSpec + renderers +
mechanics/domain + Goal model +tier/prereqs+ coverage-ledger CI + D10 constants home + freeze the M4 cell-selection contract + freeze the D13 result shape. Provisional split: S2a contracts + registry + FigureSpec/renderers (incl. the fidelity axis +assumptions); S2bmechanics/domain (demand-driven, M1 rungs first); S2c Goal model +tier/prereqs+ bidirectional coverage CI. The re-slicing step confirms the cut; the point is the six-slice structure cannot hold S2 whole. - S2.5 (Workbench Coverage Suite) = the milestone scenarios: M1 first (build order), then every milestone (M2…M-return, G7–G10) a golden-master scenario (FigureSpec + values snapshot) — all in v0.9, since the MVP is the whole ladder (A01.5). Includes the register-distinction golden-master (NEW-2) proving
computed/geometric/replayedrender in visibly separate registers. - S3 — Notebook renders a Goal; M1 is the first goal built. The additivity checkpoint (Fable-5 MAJOR 1, A01.6/D-gate): M2 is authored + built for real immediately after M1; if it needs a contract change (not just new
FormulaDefs +FigureSpeckinds), the architecture failed and we caught it after one milestone. (A dry-run of the M2 delta while finishing M1 is a cheap early read of the same signal — draft-state goals live in a fixtures dir, not the validated shipping registry, sinceGoalStep.formulaIdmust resolve — NEW/MAJOR-1 gap.) - S4–S6 — MCP + the NL "ask" (T4) resolves a human goal against the Goal registry.TLE freshness (Fable-5 MAJOR 6 / D13): any satellite (G9) result carries
epochAgeDays- a staleness bound in the result contract and the MCP tool description — a months-stale baked
station-tles.jsonyields wrong ISS passes stamped "computed" to a learner told to go look up. The app-side adapter injects fresh TLEs where available; freeze the result shape at S2.
- a staleness bound in the result contract and the MCP tool description — a months-stale baked
Re-slicing note (operator 2026-08-29): the MVP is the entire ladder + MCP, so re-slicing must size ALL of Family A + Family B + cross-cutting, not defer any as roadmap. This is a materially larger v0.9 than the original six-slice RFC scoped; the honest sizing lands at the re-slicing step.
A01.6 · Decisions (this amendment)
- D6 —
physics/mechanics/subdomain → approved; greenfield, S2, purity-gated like the rest. - D7 — Goal/Milestone as a first-class registry entity (Contract 5) → approved;
GoalStep. wiresFromis an array mirroring Contract 3 (Fable-5 MAJOR 2). Freeze the shape before S2. - D8 — full-coverage CI ledger → approved, demand-driven + FormulaDef-granular (Fable-5 MAJOR 3): no formula without a pulling Goal; CI at FormulaDef level; export audit is a one-time S2 task.
- D9 —
FormulaDef.tier/prereqsconcept graph → approved; v1 renders flat Notebooks, the skill-tree UI is a deferred consumer. - D10 — one per-body constants home → approved (A01.1);
mechanics/adds no 4th μ table. - D11 — provenance fidelity axis
computed | geometric | replayed-published→ approved (A01.2a); cislunar Tier-1 shape-gens reclassified app-side; renderers keep the registers distinct. - D12 —
assumptions: string[]on results/figures → approved (A01.4). - D13 — TLE freshness in the result contract → approved (A01.5).
- D-gate — M2 real-build additivity checkpoint at S3 (A01.5); a contract change in the M2 delta fails the MVP. (A dry-run while finishing M1 is a cheap early read.)
- Open (for re-slicing): whether Family B ships before/after Family A's mid-milestones; the exact numeric threshold for "M2 costs a fraction" (delta-ledger size).
A01.7 · Access control + the T4 LLM proxy (operator, 2026-08-29 — "all of it," gated beta)
Scope on the tier axis is now settled: v0.9 ships all tiers T0–T4 + the MCP with OAuth — no deferral (supersedes PRD §Non-goals' "not v1" items + RFC §12's Canvas/T3/T4 exclusions, reconciled below). But both public-facing doors run as a controlled beta gated to one allowlist (~6 accounts: operator + a handful of testers), to learn before opening up.
Two doors, one guest list:
- Door 2 — MCP (
mcp.orrerylearn.com). Full tool surface, so an external agent (Claude.ai) does the NL/interpret/render loop with its own model; Orrery just serves the physics tools. D3 UPDATED: v0.9 auth = OAuth 2.1 with Google/Gmail as the identity provider, gated by an email allowlist — NOT the bearer-token beta the original D3 planned. Bearer degrades to an internal dev convenience. Verify the MCP-OAuth-2.1 + Google-IdP + allowlist pattern against the CURRENT MCP auth spec before S4 — this is still the highest-uncertainty corner (§6). - Door 1 — the in-Lab NL ask-box (T4), beta. Same allowlist, not public. Correction (Fable-5 round 4 B1): the app is
adapter-static(no server runtime), so this CANNOT be a SvelteKit+server.ts. The ask-box's LLM steps (understand → select formulas → narrate; the kernel still computes — honesty line intact) run in a separatelab-apicontainer on the VPS (Caddy vhost, RFC-024/035 pattern; operator-decided 2026-08-29) that also hosts the shared identity/allowlist — see A01.8. From there, calls go through the existing LiteLLM proxy on the prod VPS, with two swappable backends:- LiteLLM → OpenRouter (new dedicated key) — always-available path.
- LiteLLM → a model stood up on the DGX — local path; depends on prod-VPS→DGX reachability (tailnet) + the DGX's shared-GPU coordination (
gpu-mode), so it competes with other vLLM usage. OpenRouter is the fallback when the GPU's busy.
D14 (new) — T4 in-Lab LLM routing → approved: routed via the lab-api container → existing prod-VPS LiteLLM, backends {OpenRouter key | DGX model}. No +server.ts (adapter-static). D15 (new) — access = a single allowlist gating both doors; eventual direction is full public OAuth (Gmail), rethought after the beta.
A01.8 · Identity/auth architecture (Fable-5 round 4 · operator-decided 2026-08-29)
The app is adapter-static (svelte.config.js) — no server runtime, no login anywhere. Both public doors need identity the static app can't provide. Decision: a separate lab-api container on the VPS (Caddy vhost, reusing the RFC-024/035 subdomain-container pattern — same class as the mcp. server, no app-adapter change).
- D16 — one identity layer, two doors. Door-1 sign-in and Door-2 MCP OAuth are the same question ("is this Google account on the allowlist?").
lab-apihosts it once: Google sign-in + session + the shared allowlist + the Door-1 ask-box endpoint; the MCP resource server consumes the same identity/allowlist. Build once, not twice. - D17 — auth spike FIRST. MCP OAuth 2.1 expects dynamic client registration, which Google's IdP does not offer — so an authorization-server / proxy layer federating to Google is likely required (in
lab-api). This is the highest-uncertainty, highest-slip item; a scoped INFRA-auth-spike runs before Phase 1b/4 and its outcome shapes both doors. Verify against the current MCP auth spec revision before building.
Slice-plan consequences (auth infra started early, S3 owns the recompute core, Phase 2 is ~16 enumerated goal slices, S6/T4 depend on Phase-2 completion) live in docs/wip/_archive/2026-08-29-v0.9-flagship-slice-plan.md v2.