Skip to content

FE-588: Add basic Scenario support#8609

Open
kube wants to merge 47 commits intomainfrom
cf/fe-532-implement-basic-experiment
Open

FE-588: Add basic Scenario support#8609
kube wants to merge 47 commits intomainfrom
cf/fe-532-implement-basic-experiment

Conversation

@kube
Copy link
Copy Markdown
Collaborator

@kube kube commented Apr 7, 2026

Kapture.2026-04-15.at.00.36.31.mp4

🌟 What is the purpose of this PR?

Adds basic Scenario support to Petrinaut. A Scenario is a reusable initial configuration (parameter overrides + initial token state) that can be selected before running a simulation.

This is a first pass — future work should harmonize the type system across Color dimensions, Net Parameters, and Scenario Parameters, which all share similar structure but are currently modeled independently.

At a high level, this PR introduces: scenario CRUD (create/edit/save), an expression-based LSP for scenario fields (with type-checking, completions, hover), a scenario compiler that evaluates expressions into concrete simulation state, and the UI to select and configure scenarios before running.

🔗 Related links

🔍 What does this change?

Scenario data model

  • Add Scenario type with scenario parameters, parameter overrides, and initial state (discriminated union: per_place with string | number[][] or code with string)
  • Add ScenarioParameter with types: real, integer, boolean, ratio
  • Zod validation schema for persistence
  • File format schema updated to match

Scenario form & drawers

  • Create/View/Edit scenario via Drawer (TanStack Form for state management)
  • Form-level validation: name required + unique, identifiers snake_case + unique
  • LSP-gated submit: Save/Create disabled when TypeScript errors exist
  • Footer shows error count badge + first error message
  • Inline red outline on inputs with LSP diagnostics (shown on blur only)
  • Form reset after successful create

LSP integration

  • temp/scenario/initialize, didChange, kill protocol messages
  • Virtual TypeScript files for each expression (function-wrapped with correct return types)
  • scenario object for accessing scenario parameters in expressions
  • Suffix support on VirtualFile for expression wrapping
  • Pending message queue for React child-first effect ordering
  • Only lint active initial state mode (per-place vs code)
  • Skip empty expressions

Scenario compiler (compile-scenario.ts)

  • Hardened new Function() evaluation (strict mode, prototype-less frozen objects, shadowed globals)
  • Three-phase: scenario defaults → parameter overrides → initial state
  • Supports per-place expressions (number), colored place data (number[][]), and code-mode (full function body returning object keyed by place name)
  • 22+ tests covering evaluation, error handling, and sandboxing

Simulation integration

  • SimulationContext gains selectedScenarioId, scenarioParameterValues, compiledScenarioResult
  • Compiled scenario overrides parameterValues and initialMarking in context
  • initialize() reads from effective (scenario-overridden) refs
  • Scenario mutations (addScenario, updateScenario, removeScenario) bypass simulate-mode read-only guard

Simulation Settings

  • Scenario picker dropdown with Edit/Create/Manage buttons
  • Boolean params render as Switch, ratio params as Slider + NumberInput
  • Scenario parameter values editable, recompiled on every change
  • Place initial state shown as read-only when scenario active

UI polish

  • Sticky section headers with gradient fade
  • Drawer animation fix (remove forwards to avoid transform stacking context)
  • Single-line CodeEditor: no vertical scroll, Enter works with suggest widget
  • Select dropdown z-index fix, portal default changed to false
  • Wider drawer (640px, max 90vw - 20px)

Examples

  • SIR model: scenarios with population (int) + infected_ratio (ratio) params, expression-derived initial state
  • Satellites: rename earth_radius → planet_radius, add gravity_force/planet_radius scenario params
  • Satellites (non-launcher): "All Around the World" code-mode scenario
  • Production Machines: flag RawMaterial/AvailableMachines as initial state

Other

  • Metrics tab placeholder (disabled) in Simulate view
  • Move simulation error display from Settings to Diagnostics tab
  • Scrollable parameters list in Simulation Settings

🛡 What tests cover this?

  • compile-scenario.test.ts: 25 tests covering expression evaluation, scenario parameters, Math functions, ternaries, rounding, error handling, sandboxing (prototype chain, globals), and colored place data

❓ How to test this?

  1. Open the SIR or Satellites example
  2. Go to Simulate mode → Scenarios tab → Create a scenario
  3. Add scenario parameters, set parameter overrides with expressions
  4. Verify LSP completions and error highlighting in expression editors
  5. Select the scenario in Simulation Settings → verify parameters update
  6. Press Play → confirm simulation uses scenario's initial state and parameters

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hash Ready Ready Preview, Comment Apr 16, 2026 9:15am
petrinaut Ready Ready Preview Apr 16, 2026 9:15am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
hashdotdesign Ignored Ignored Preview Apr 16, 2026 9:15am
hashdotdesign-tokens Ignored Ignored Preview Apr 16, 2026 9:15am

@github-actions github-actions bot added area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team area/apps > hash.design Affects the `hash.design` design site (app) labels Apr 7, 2026
Copy link
Copy Markdown
Collaborator Author

kube commented Apr 7, 2026

@kube kube changed the title Add vertical orientation and icon-only mode to SegmentGroup Add Experiments Apr 7, 2026
@kube kube changed the title Add Experiments Add Scenarios and Experiments Apr 7, 2026
@kube kube force-pushed the cf/fe-532-implement-basic-experiment branch from 3a765f9 to e8797e1 Compare April 12, 2026 16:44
@github-actions github-actions bot added the area/deps Relates to third-party dependencies (area) label Apr 13, 2026
@kube kube force-pushed the cf/fe-532-implement-basic-experiment branch from bee92fd to 0594dc7 Compare April 14, 2026 08:49
@kube kube force-pushed the cf/fe-532-implement-basic-experiment branch from 0594dc7 to c2d33b0 Compare April 14, 2026 09:02
@kube kube changed the base branch from main to graphite-base/8609 April 14, 2026 12:41
@kube kube force-pushed the cf/fe-532-implement-basic-experiment branch from c77c244 to d3af22f Compare April 14, 2026 12:41
@kube kube changed the base branch from graphite-base/8609 to cf/uplot-simulation-timeline April 14, 2026 12:41
kube and others added 7 commits April 15, 2026 20:18
Renames the "Initial state" place property to "Default starting place"
and swaps the header switch for a ds-components Checkbox rendered to
the left of the section label. Adds a `renderHeaderLeading` prop to
`Section` to support leading header actions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewords the hint to convey that the place should have an initial
marking defined to run the net, and notes it will be pre-selected
in new scenarios.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wires Monaco editor diagnostics and inline identifier/name validation
into the scenario form: single-line CodeEditor gains a `hasError` red
border plus focus/blur callbacks so errors don't flash while typing,
and the form highlights invalid scenario-parameter identifiers.

Refines the Initial State section: places display a color dot next to
their name, the two switches are grouped distinctly, and the section
now shows an empty-state message pointing users to the "Default
starting place" flag when no places are marked and "Show all" is off.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a 480px max-width (and consistent gap) to the parameter row in
simulation-settings so the label and input don't drift far apart when
the bottom panel is wide.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add missing `type: "standard"` on InputArc literals in code-editor
  stories to satisfy the discriminated-union schema.
- Widen scenario code return type to `unknown` so the null/object
  guard in compile-scenario is not flagged as unnecessary.
- Disable `no-unsafe-assignment` where `expect.any(Object)` is used.
- Suppress `no-use-before-define` where `initialize` references
  late-defined refs via a render-time closure.
- Replace the now-unused `react-hooks/exhaustive-deps` disable on
  the simulation-timeline useMemo with a plain explanatory comment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tanstack/react-form → @tanstack/react-store pulls in the pure-CJS
use-sync-external-store, whose `require("react")` calls were surviving
rolldown's lib build (react is external) as a runtime require helper
that throws in the browser. Externalising use-sync-external-store and
its subpaths pushes the CJS→ESM interop to the consumer's bundler,
which handles it via pre-bundling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread libs/@hashintel/petrinaut/src/simulation/compile-scenario.ts
kube and others added 3 commits April 15, 2026 20:46
The per-planet scenarios previously let users tweak
`gravity_force`/`planet_radius`, which are actually planet-defining
constants — not meaningful to adjust at experiment time. They also
hardcoded the launch distance (80) and rate (1) inside the transition
kernel.

Rework so each scenario hardcodes the planet-defining constants
(`gravitational_constant`, `planet_radius`) as parameter overrides,
and exposes three user-tweakable scenario parameters that flow
through newly-added SDCPN parameters into the code:

  - `altitude`  → SDCPN `altitude`        → kernel distance
                                           (planet_radius + altitude)
  - `rate`      → SDCPN `launch_rate`     → Lambda rate
  - `velocity`  → SDCPN `initial_velocity`→ kernel satellite velocity

The Lambda now returns `parameters.launch_rate`, and the
TransitionKernel computes the spawn position from
`planet_radius + altitude` and the initial velocity mean from
`initial_velocity` (with a 10% Gaussian spread).

Note: an "altitude deviation" scenario parameter was considered but
skipped — combining two distributions (angle + altitude) into a
single coordinate field isn't expressible with the current
`Distribution.map` API without breaking simulator determinism.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- altitude → satellite_initial_altitude
- rate     → launch_rate
- velocity → satellite_initial_velocity

launch_rate is now listed first in each scenario so it appears at the
top of the scenario-parameters UI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous rename commit only updated Moon Orbit's
parameterOverrides (the replace pattern matched a comment that only
existed there). Earth, Mars, and Solar orbit were still forwarding
from the old scenario.altitude / scenario.rate / scenario.velocity
identifiers, which no longer exist — causing those scenarios to
fail to compile. Point them at the renamed scenario params.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The existing sandbox shadowed `Function` via a local `var`, which only
prevents identifier lookup. An attacker could still reach the real
Function constructor via any literal's `.constructor.constructor`
chain (e.g. `({}).constructor.constructor("return this")()`), because
property access bypasses the shadowed identifier and `createSafeObject`
only hardens the `parameters`/`scenario` arguments — not freshly-made
literals inside the expression body.

Add a `runSandboxed` helper that, for the duration of a synchronous
evaluation, replaces the `.constructor` getter on every built-in
prototype a literal can reach (Object, Array, Function, String,
Number, Boolean) with one that throws. Descriptors are restored in
`finally` so the patch is invisible to surrounding code (JS is
single-threaded — no microtasks run until we've reverted).

Apply the sandbox to both `evaluateExpression` and the code-mode
initial-state block, and cover the bypass with tests that confirm
(a) the literal-constructor walk is blocked and (b) prototype state
is restored after evaluation.

The proper fix remains moving scenario evaluation to a Worker/iframe
realm; this is defense-in-depth for the same-realm case.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a "Default Production" scenario with three user-tweakable params:
  - raw_material (integer)
  - machines_count (integer)
  - initial_machine_damage (ratio)

RawMaterial is uncolored, so its initial count could be set via a
per-place expression — but AvailableMachines is colored (type Machine
with `machine_damage_ratio`) and per-place mode only accepts static
number[][] for colored places. Using code-mode initialState lets the
scenario dynamically generate N machines each with the chosen damage
ratio from a single expression.

The places RawMaterial and AvailableMachines already had
`showAsInitialState: true`, so they're pre-selected in the scenario
form — no further net changes required.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 82406f1. Configure here.

Comment thread libs/@hashintel/petrinaut/src/components/dialog.tsx
The simulation-settings Select for choosing a scenario was the only
control in the panel that stayed interactive during Running/Paused
state — all other inputs (parameter Switch/Slider/NumberInputs, time
step, ODE solver) already took `disabled={isSimulationActive}`. Wire
the same flag into the scenario picker so it can't swap scenarios
mid-run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Users typing "My Variable" or "myVariable" into the scenario parameter
identifier field previously saw a validation error until they manually
fixed the casing. Add a `snakify` helper (camelCase/PascalCase split,
lowercase, non-alphanumerics → single underscore, trim underscores)
and call it from the Input's `onBlur`. Empty/invalid inputs collapse
to `""` so the existing "cannot be empty" validation still fires.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps > hash.design Affects the `hash.design` design site (app) area/deps Relates to third-party dependencies (area) area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

2 participants