Skip to content

FE-1485: Synthesize ad-hoc scenarios from an inline definition - #9284

Draft
kube wants to merge 7 commits into
mainfrom
cf/fe-1485-ad-hoc-scenario-synthesis-in-petrinaut-core
Draft

FE-1485: Synthesize ad-hoc scenarios from an inline definition#9284
kube wants to merge 7 commits into
mainfrom
cf/fe-1485-ad-hoc-scenario-synthesis-in-petrinaut-core

Conversation

@kube

@kube kube commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

🌟 What is the purpose of this PR?

The foundation of the ad-hoc scenario feature (FE-778): the shapes, the synthesis, and the expression type-checking infrastructure, all in petrinaut-core. Bottom of stack #9286. The design follows the ratified prototype (the "shared column values" variation handed to the designers) and the decisions from the #lib-petrinaut thread of 2026-08-06.

🔗 Related links

🔍 What does this change?

Three shapes, in pipeline order (simulation/authoring/scenario/ad-hoc-scenario.ts):

  1. AdHocScenarioState — the form's editing state: per-place token spreadsheets whose every cell is an expression, rows that are Fixed or Dynamic and mix freely, shared column values, and Variables at two scopes. Rows cycle Fixed → Dynamic → count-Optimized through cycleAdHocRowKind, retaining the count (bounds included) across the cycle.
  2. AdHocSynthesisOutput — what synthesis emits: a generated code-mode Scenario (never persisted; compileScenario is untouched) plus one AdHocOptimizedField per enabled Optimize toggle, carrying the deterministic adhoc.* parameter name, the source's attribution label (Space › item 0 › x), its target path, and its typed domain.
  3. The downstream forms: output.scenario feeds compileScenario directly (plain runs); adHocOptimizationBindings(output.optimizedFields) is the transform to a PetrinautOptimizationManifest's parameterBindings (the manifest schema itself is unchanged).

Expression vocabulary matches the scenario code editor: top-level Variables are written scenario.<name> (they stand in for scenario parameters, exactly as in the Satellites Launcher example), per-place Variables are bare names, net parameters are parameters.<name>, and i/count are per row. Net parameter overrides may read top-level Variables.

Slots (AdHocSlot = target + part, with stable path-safe adHocSlotKey strings) are the join key between synthesis errors, LSP diagnostics, and the form's rendering of both. Every synthesis error carries its slot.

Place totals: resolveAdHocPlaceTotal sums row counts — a number when everything resolves, the unresolved expressions printed otherwise — for the table's bottom line.

Actions and the pure reducer (ad-hoc-actions.ts): the form's editing model as serializable actions, mirroring the net editor's mutation actions — a zod input schema per action (adHocActionInputSchemas), the AdHocAction union, and applyAdHocAction(state, context, action), a pure reducer that returns the same reference when an action changes nothing. The vocabulary covers value edits (setExpression, setDomainField, toggleSelection — count targets pick the count defaults), Variables (add with generated unique names, rename with an optional reference rewrite — scenario.<name> across the whole state for top-level Variables, bare identifiers within the owning place for per-place ones, bounds included — retype, delete, duplicate with name dedup), token rows (add with type-appropriate default cells, delete, duplicate, set-kind through the retention transitions), and shared columns. adHocActionCoalescingKey names the actions that collapse into one undo entry when dispatched consecutively (typing bursts, slot-keyed via adHocSlotKey). The form (#9285) dispatches these exclusively and builds its redux-like undo on top.

LSP: a new ad-hoc session kind mirrors scenario sessions end to end (protocol notifications temp/adhoc/*, virtual-file generation per non-empty value slot with scope-correct ambient declarations, service sync, worker handling, client methods). Every cell, count, Variable, net parameter override, and optimize bound gets a virtual TypeScript document type-checked as the slot's type; bounds see only the constant scope, so i in a bound is a type error.

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • modifies an npm-publishable library and I have added a changeset file(s) (patch).

📜 Does this require a change to the docs?

The changes in this PR:

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph.

🛡 What tests cover this?

24 synthesis tests — every fabricated scenario round-trips through the real compileScenario, including the scenario.<name> vocabulary, optimize suggestions applied to every generated parameter kind, shared-column supersession, the gutter cycle's retention, place totals, slot keys, and attribution labels. 5 LSP-session tests pin scope-correct diagnostics (valid expressions accept parameters.*/scenario.*/i/count/place variables; type mismatches and unknown identifiers report; i is out of scope in bounds; empty expressions produce no document; kill removes the session's files).

❓ How to test this?

turbo run test:unit --filter @hashintel/petrinaut-core

🐾 Next steps

FE-1486 (#9285) builds the form component on these shapes.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Aug 22, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
hash Ready Ready Preview Aug 25, 2026 6:08pm
petrinaut Ready Ready Preview Aug 25, 2026 6:08pm
petrinaut-docs Ready Ready Preview Aug 25, 2026 6:08pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hashdotdesign-tokens Ignored Ignored Preview Aug 25, 2026 6:08pm

Request Review

@github-actions github-actions Bot added 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 labels Aug 22, 2026
@kube
kube force-pushed the cf/fe-1485-ad-hoc-scenario-synthesis-in-petrinaut-core branch 2 times, most recently from 21c1cd9 to a25badb Compare August 22, 2026 16:50
@kube
kube force-pushed the cf/fe-1485-ad-hoc-scenario-synthesis-in-petrinaut-core branch from a25badb to c83cb44 Compare August 22, 2026 16:53
@kube
kube force-pushed the cf/fe-1485-ad-hoc-scenario-synthesis-in-petrinaut-core branch from c018875 to e43bd4a Compare August 24, 2026 22:13
@semgrep-code-hashintel

Copy link
Copy Markdown

Semgrep found 8 detect-non-literal-regexp findings:

RegExp() called with a state function argument, this might allow an attacker to cause a Regular Expression Denial-of-Service (ReDoS) within your application as RegExP blocks the main thread. For this reason, it is recommended to use hardcoded regexes instead. If your regex is run on user-controlled input, consider performing input validation or use a regex checking/sanitization library such as https://www.npmjs.com/package/recheck to verify that the regex does not appear vulnerable to ReDoS.

View Dataflow Graph
flowchart LR
    classDef invis fill:white, stroke: none
    classDef default fill:#e7f5ff, color:#1c7fd6, stroke: none

    subgraph File0["<b>libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc-actions.ts</b>"]
        direction LR
        %% Source

        subgraph Source
            direction LR

            v0["<a href=https://github.com/hashintel/hash/blob/e43bd4aac3d83ed36b1e9600bb0dd907c5d111fe/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc-actions.ts#L597 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 597] state</a>"]
        end
        %% Intermediate

        subgraph Traces0[Traces]
            direction TB

            v2["<a href=https://github.com/hashintel/hash/blob/e43bd4aac3d83ed36b1e9600bb0dd907c5d111fe/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc-actions.ts#L597 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 597] state</a>"]

            v3["<a href=https://github.com/hashintel/hash/blob/e43bd4aac3d83ed36b1e9600bb0dd907c5d111fe/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc-actions.ts#L642 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 642] list</a>"]

            v4["<a href=https://github.com/hashintel/hash/blob/e43bd4aac3d83ed36b1e9600bb0dd907c5d111fe/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc-actions.ts#L649 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 649] ?.</a>"]

            v5["<a href=https://github.com/hashintel/hash/blob/e43bd4aac3d83ed36b1e9600bb0dd907c5d111fe/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc-actions.ts#L649 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 649] oldName</a>"]

            v6["<a href=https://github.com/hashintel/hash/blob/e43bd4aac3d83ed36b1e9600bb0dd907c5d111fe/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc-actions.ts#L671 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 671] rewriteReferencesForRename</a>"]

            v7["<a href=https://github.com/hashintel/hash/blob/e43bd4aac3d83ed36b1e9600bb0dd907c5d111fe/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc-actions.ts#L544 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 544] oldName</a>"]

            v8["<a href=https://github.com/hashintel/hash/blob/e43bd4aac3d83ed36b1e9600bb0dd907c5d111fe/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc-actions.ts#L549 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 549] rewriteAdHocReference</a>"]

            v9["<a href=https://github.com/hashintel/hash/blob/e43bd4aac3d83ed36b1e9600bb0dd907c5d111fe/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc-actions.ts#L467 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 467] oldName</a>"]

            v10["<a href=https://github.com/hashintel/hash/blob/e43bd4aac3d83ed36b1e9600bb0dd907c5d111fe/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc-actions.ts#L470 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 470] escapeForPattern</a>"]

            v11["<a href=https://github.com/hashintel/hash/blob/e43bd4aac3d83ed36b1e9600bb0dd907c5d111fe/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc-actions.ts#L456 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 456] name</a>"]

            v12["<a href=https://github.com/hashintel/hash/blob/e43bd4aac3d83ed36b1e9600bb0dd907c5d111fe/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc-actions.ts#L470 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 470] escaped</a>"]

            v13["<a href=https://github.com/hashintel/hash/blob/e43bd4aac3d83ed36b1e9600bb0dd907c5d111fe/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc-actions.ts#L481 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 481] `</a>"]
        end
            v2 --> v3
            v3 --> v4
            v4 --> v5
            v5 --> v6
            v6 --> v7
            v7 --> v8
            v8 --> v9
            v9 --> v10
            v10 --> v11
            v11 --> v12
            v12 --> v13
        %% Sink

        subgraph Sink
            direction LR

            v1["<a href=https://github.com/hashintel/hash/blob/e43bd4aac3d83ed36b1e9600bb0dd907c5d111fe/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc-actions.ts#L481 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 481] new RegExp(String.raw`(?&lt;![.\w$])${escaped}(?![A-Za-z0-9_$])`, &quot;g&quot;)</a>"]
        end
    end
    %% Class Assignment
    Source:::invis
    Sink:::invis

    Traces0:::invis
    File0:::invis

    %% Connections

    Source --> Traces0
    Traces0 --> Sink

Loading

kube added 7 commits August 25, 2026 19:56
An ad-hoc scenario is an initial-state + parameters definition made inline
— per-place token spreadsheets whose cells are expressions, a Static or
Dynamic count per place, and named Variables at two scopes — compiled into
a Scenario value at run time and never persisted into the net file.
Synthesis emits a code-mode scenario, the one initial-state mode whose
expressions the existing compileScenario already evaluates in its hardened
sandbox, so the compiler is untouched.

synthesizeAdHocScenario ignores Optimize toggles and serves quick
simulation and plain experiments. synthesizeAdHocOptimization turns every
Optimize selection (cell, Variable, dynamic count, net parameter) into a
generated scenario parameter with a deterministic source-derived name —
the join key from optimization results back to what the user selected —
and returns the manifest parameterBindings for them. Bounds are
expressions that must resolve to constants at study creation; an optimized
bound may not reference an optimized entity; per-place Variables evaluate
per row, so they may read i.

The tests round-trip every fabricated scenario through the real
compileScenario.
Top-level Variables are referenced as scenario.<name>, matching the scenario code editor. Synthesis emits AdHocSynthesisOutput (scenario + optimized fields with targets, labels, and domains) with adHocOptimizationBindings as the manifest transform. Adds slot keys and target labels for error and diagnostic attribution, the row-kind gutter cycle, place totals, and an ad-hoc LSP session that type-checks every value slot.
The override wrapper captures the compiler's scenario object before the Variables shadow it, so an override reading an optimized Variable no longer evaluates to undefined. Synthesis rejects empty required expressions at their slot, two participating places sharing a name (the generated initial state is keyed by name), integer steps that cannot reach the maximum, and logarithmic integer steps other than 1; integer preview defaults round; a share whose element no longer exists is ignored rather than fatal. Slot keys escape ids without percent signs or underscores so Monaco's URI normalization cannot break the round-trip, and a place identified "net" cannot collide with the top-level scope. The LSP scope of a place variable's documents covers only earlier siblings, mirroring the generated declaration order, and reserved names are kept out of ambient declarations. The gutter cycle can skip its count-Optimized stage for consumers without Optimize.
@kube
kube force-pushed the cf/fe-1485-ad-hoc-scenario-synthesis-in-petrinaut-core branch from 152c069 to f6d051e Compare August 25, 2026 17:58
@semgrep-code-hashintel

Copy link
Copy Markdown

Semgrep found 8 detect-non-literal-regexp findings:

RegExp() called with a state function argument, this might allow an attacker to cause a Regular Expression Denial-of-Service (ReDoS) within your application as RegExP blocks the main thread. For this reason, it is recommended to use hardcoded regexes instead. If your regex is run on user-controlled input, consider performing input validation or use a regex checking/sanitization library such as https://www.npmjs.com/package/recheck to verify that the regex does not appear vulnerable to ReDoS.

View Dataflow Graph
flowchart LR
    classDef invis fill:white, stroke: none
    classDef default fill:#e7f5ff, color:#1c7fd6, stroke: none

    subgraph File0["<b>libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc/ad-hoc-actions.ts</b>"]
        direction LR
        %% Source

        subgraph Source
            direction LR

            v0["<a href=https://github.com/hashintel/hash/blob/f6d051e2fc8d626103b1bf089cc975472ba42e1d/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc/ad-hoc-actions.ts#L699 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 699] state</a>"]
        end
        %% Intermediate

        subgraph Traces0[Traces]
            direction TB

            v2["<a href=https://github.com/hashintel/hash/blob/f6d051e2fc8d626103b1bf089cc975472ba42e1d/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc/ad-hoc-actions.ts#L699 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 699] state</a>"]

            v3["<a href=https://github.com/hashintel/hash/blob/f6d051e2fc8d626103b1bf089cc975472ba42e1d/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc/ad-hoc-actions.ts#L749 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 749] list</a>"]

            v4["<a href=https://github.com/hashintel/hash/blob/f6d051e2fc8d626103b1bf089cc975472ba42e1d/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc/ad-hoc-actions.ts#L756 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 756] ?.</a>"]

            v5["<a href=https://github.com/hashintel/hash/blob/f6d051e2fc8d626103b1bf089cc975472ba42e1d/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc/ad-hoc-actions.ts#L756 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 756] oldName</a>"]

            v6["<a href=https://github.com/hashintel/hash/blob/f6d051e2fc8d626103b1bf089cc975472ba42e1d/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc/ad-hoc-actions.ts#L771 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 771] rewriteReferencesForRename</a>"]

            v7["<a href=https://github.com/hashintel/hash/blob/f6d051e2fc8d626103b1bf089cc975472ba42e1d/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc/ad-hoc-actions.ts#L643 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 643] oldName</a>"]

            v8["<a href=https://github.com/hashintel/hash/blob/f6d051e2fc8d626103b1bf089cc975472ba42e1d/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc/ad-hoc-actions.ts#L648 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 648] rewriteAdHocReference</a>"]

            v9["<a href=https://github.com/hashintel/hash/blob/f6d051e2fc8d626103b1bf089cc975472ba42e1d/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc/ad-hoc-actions.ts#L542 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 542] oldName</a>"]

            v10["<a href=https://github.com/hashintel/hash/blob/f6d051e2fc8d626103b1bf089cc975472ba42e1d/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc/ad-hoc-actions.ts#L545 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 545] escapeForPattern</a>"]

            v11["<a href=https://github.com/hashintel/hash/blob/f6d051e2fc8d626103b1bf089cc975472ba42e1d/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc/ad-hoc-actions.ts#L531 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 531] name</a>"]

            v12["<a href=https://github.com/hashintel/hash/blob/f6d051e2fc8d626103b1bf089cc975472ba42e1d/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc/ad-hoc-actions.ts#L545 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 545] escaped</a>"]

            v13["<a href=https://github.com/hashintel/hash/blob/f6d051e2fc8d626103b1bf089cc975472ba42e1d/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc/ad-hoc-actions.ts#L561 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 561] `</a>"]
        end
            v2 --> v3
            v3 --> v4
            v4 --> v5
            v5 --> v6
            v6 --> v7
            v7 --> v8
            v8 --> v9
            v9 --> v10
            v10 --> v11
            v11 --> v12
            v12 --> v13
        %% Sink

        subgraph Sink
            direction LR

            v1["<a href=https://github.com/hashintel/hash/blob/f6d051e2fc8d626103b1bf089cc975472ba42e1d/libs/@hashintel/petrinaut-core/src/simulation/authoring/scenario/ad-hoc/ad-hoc-actions.ts#L561 target=_blank style='text-decoration:none; color:#1c7fd6'>[Line: 561] new RegExp(String.raw`(?&lt;![.\w$])${escaped}(?![A-Za-z0-9_$])`, &quot;g&quot;)</a>"]
        end
    end
    %% Class Assignment
    Source:::invis
    Sink:::invis

    Traces0:::invis
    File0:::invis

    %% Connections

    Source --> Traces0
    Traces0 --> Sink

Loading

Comment on lines +553 to +556
new RegExp(
String.raw`(?<![.\w$])(scenario\s*\.\s*)${escaped}(?![A-Za-z0-9_$])`,
"g",
),
Comment on lines +553 to +556
new RegExp(
String.raw`(?<![.\w$])(scenario\s*\.\s*)${escaped}(?![A-Za-z0-9_$])`,
"g",
),
Comment on lines +553 to +556
new RegExp(
String.raw`(?<![.\w$])(scenario\s*\.\s*)${escaped}(?![A-Za-z0-9_$])`,
"g",
),
Comment on lines +553 to +556
new RegExp(
String.raw`(?<![.\w$])(scenario\s*\.\s*)${escaped}(?![A-Za-z0-9_$])`,
"g",
),
);
}
return expression.replace(
new RegExp(String.raw`(?<![.\w$])${escaped}(?![A-Za-z0-9_$])`, "g"),
);
}
return expression.replace(
new RegExp(String.raw`(?<![.\w$])${escaped}(?![A-Za-z0-9_$])`, "g"),
);
}
return expression.replace(
new RegExp(String.raw`(?<![.\w$])${escaped}(?![A-Za-z0-9_$])`, "g"),
);
}
return expression.replace(
new RegExp(String.raw`(?<![.\w$])${escaped}(?![A-Za-z0-9_$])`, "g"),
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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