From 81e9c30b9894f7c91098681b9e801b082649d1f5 Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Wed, 12 Aug 2026 02:42:01 +0200 Subject: [PATCH 1/3] Migrate the architecture prose into the docs bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `petrinaut-core/docs/architecture/` was 3,100 lines of hand-written HTML plus a stylesheet, served by nothing and reachable only by opening files off disk. Its content is the half a generated page cannot produce: why a boundary sits where it does, what the alternatives were, which mistakes are easy to make. It moves to `content/` as MDX, where it merges into the generated tree instead of sitting beside it. A page names the layer it explains with `attachTo: core.simulation`, and lands under that layer's page with the generated reference for the same code. Links are written by name — `layer:core.simulation.engine`, `doc:simulation/memory-model` — and resolved at emit time, because a page's depth is only known once `attachTo` is applied. Unresolved targets fail the build. Five simulation deep-dives carry the detail the generated pages do not: the memory model and frame format, the fixed stage order of a step, the HIR pipeline and what the sandbox does not protect against, the worker protocol and its ack contract, and why only aggregates reach the UI from a Monte Carlo run. Four diagram components render the parts that are genuinely visual — byte layouts, lane diagrams, pipelines and message sequences. Migrating surfaced two claims that had gone stale: UUID support described as unimplemented, which is fully implemented, and a sandbox description thinner than what the code now does. These pages are hand-written and can go stale, which is why they avoid restating facts the generated pages own. The generated half is checked against the code; this half is not, and the docs say so. --- AGENTS.md | 4 + .../docs/architecture/architecture.css | 478 ----------- .../docs/architecture/authoring.html | 288 ------- .../docs/architecture/engine.html | 770 ------------------ .../docs/architecture/index.html | 439 ---------- .../docs/architecture/monte-carlo.html | 376 --------- .../docs/architecture/worker.html | 752 ----------------- .../content/components/byte-map.tsx | 88 ++ .../content/components/diagram.css | 257 ++++++ .../content/components/inline.tsx | 27 + .../content/components/lanes.tsx | 92 +++ .../content/components/pipeline.tsx | 67 ++ .../content/components/sequence.tsx | 94 +++ .../petrinaut-arch-docs/content/index.mdx | 78 ++ .../content/simulation/experiments.mdx | 126 +++ .../content/simulation/memory-model.mdx | 191 +++++ .../content/simulation/protocol.mdx | 168 ++++ .../content/simulation/stepping.mdx | 76 ++ .../content/simulation/user-code.mdx | 106 +++ .../content/two-execution-paths.mdx | 52 ++ .../content/working-on-the-architecture.mdx | 120 +++ 21 files changed, 1546 insertions(+), 3103 deletions(-) delete mode 100644 libs/@hashintel/petrinaut-core/docs/architecture/architecture.css delete mode 100644 libs/@hashintel/petrinaut-core/docs/architecture/authoring.html delete mode 100644 libs/@hashintel/petrinaut-core/docs/architecture/engine.html delete mode 100644 libs/@hashintel/petrinaut-core/docs/architecture/index.html delete mode 100644 libs/@hashintel/petrinaut-core/docs/architecture/monte-carlo.html delete mode 100644 libs/@hashintel/petrinaut-core/docs/architecture/worker.html create mode 100644 libs/@local/petrinaut-arch-docs/content/components/byte-map.tsx create mode 100644 libs/@local/petrinaut-arch-docs/content/components/diagram.css create mode 100644 libs/@local/petrinaut-arch-docs/content/components/inline.tsx create mode 100644 libs/@local/petrinaut-arch-docs/content/components/lanes.tsx create mode 100644 libs/@local/petrinaut-arch-docs/content/components/pipeline.tsx create mode 100644 libs/@local/petrinaut-arch-docs/content/components/sequence.tsx create mode 100644 libs/@local/petrinaut-arch-docs/content/index.mdx create mode 100644 libs/@local/petrinaut-arch-docs/content/simulation/experiments.mdx create mode 100644 libs/@local/petrinaut-arch-docs/content/simulation/memory-model.mdx create mode 100644 libs/@local/petrinaut-arch-docs/content/simulation/protocol.mdx create mode 100644 libs/@local/petrinaut-arch-docs/content/simulation/stepping.mdx create mode 100644 libs/@local/petrinaut-arch-docs/content/simulation/user-code.mdx create mode 100644 libs/@local/petrinaut-arch-docs/content/two-execution-paths.mdx create mode 100644 libs/@local/petrinaut-arch-docs/content/working-on-the-architecture.mdx diff --git a/AGENTS.md b/AGENTS.md index 900a948f282..a8221a79432 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -127,6 +127,10 @@ Verify with `yarn workspace @local/petrinaut-arch-docs lint:arch-docs`, which fa Full reference: `libs/@local/petrinaut-arch-docs/README.md`. +Hand-written MDX in `libs/@local/petrinaut-arch-docs/content/` carries the reasoning an import graph cannot express — why a boundary sits where it does, what the alternatives were. It is optional; the system works with that directory absent. + +Give such a page `attachTo: ` in its frontmatter and it nests inside the generated tree beneath that layer, rather than sitting in a separate section — that is how the simulation deep-dives are wired. Link between pages with `[text](layer:core.simulation.engine)` or `[text](doc:simulation/memory-model)`; relative paths break when a page's `attachTo` changes, and unresolved targets fail CI. + ## Contextual Rules CRITICAL: For the files referenced below, use your Read tool to load it on a need-to-know basis, ONLY when relevant to the SPECIFIC task at hand: diff --git a/libs/@hashintel/petrinaut-core/docs/architecture/architecture.css b/libs/@hashintel/petrinaut-core/docs/architecture/architecture.css deleted file mode 100644 index c9b6ef590c5..00000000000 --- a/libs/@hashintel/petrinaut-core/docs/architecture/architecture.css +++ /dev/null @@ -1,478 +0,0 @@ -/* Shared styles for the simulation architecture docs (architecture*.html). */ - -:root { - /* One colour per module, used consistently across all pages. */ - --engine: #2563eb; /* blue — engine (stepping, frames) */ - --engine-bg: #eff6ff; - --authoring: #9333ea; /* purple — compilation of user code */ - --authoring-bg: #faf5ff; - --worker: #059669; /* green — worker + transport protocol */ - --worker-bg: #ecfdf5; - --mc: #ea580c; /* orange — monte carlo / experiments */ - --mc-bg: #fff7ed; - --memory: #dc2626; /* red — buffers, where bytes live */ - --memory-bg: #fef2f2; - --ui: #0891b2; /* cyan — React / UI consumers */ - --ui-bg: #ecfeff; - --ink: #111111; - --muted: #555555; - --line: #d9d9d9; -} - -* { - box-sizing: border-box; -} - -body { - margin: 0 auto; - padding: 2rem 1.5rem 5rem; - max-width: 1000px; - background: #ffffff; - color: var(--ink); - font: - 15px/1.55 -apple-system, - BlinkMacSystemFont, - "Segoe UI", - Roboto, - Helvetica, - Arial, - sans-serif; -} - -h1 { - font-size: 1.7rem; - margin: 0.5rem 0 0.25rem; -} - -h2 { - font-size: 1.25rem; - margin: 2.5rem 0 0.75rem; - padding-bottom: 0.3rem; - border-bottom: 1px solid var(--line); -} - -h3 { - font-size: 1.02rem; - margin: 1.75rem 0 0.5rem; -} - -p { - max-width: 76ch; -} - -.subtitle { - color: var(--muted); - margin-top: 0; -} - -code, -pre { - font-family: - ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace; - font-size: 0.86em; - background: #f5f5f5; - border-radius: 4px; -} - -code { - padding: 0.1em 0.35em; -} - -pre { - padding: 0.8rem 1rem; - overflow-x: auto; - border: 1px solid #ececec; -} - -pre code { - padding: 0; - background: none; -} - -a { - color: var(--engine); -} - -/* ---- Top navigation ------------------------------------------------- */ - -nav.docs { - display: flex; - flex-wrap: wrap; - gap: 0.4rem; - padding: 0.6rem 0; - margin-bottom: 1rem; - border-bottom: 2px solid var(--ink); - font-size: 0.9rem; -} - -nav.docs a { - text-decoration: none; - color: var(--ink); - padding: 0.25rem 0.7rem; - border: 1px solid var(--line); - border-radius: 999px; -} - -nav.docs a.current { - color: #fff; - background: var(--ink); - border-color: var(--ink); -} - -nav.docs a.tag-engine.current { - background: var(--engine); - border-color: var(--engine); -} -nav.docs a.tag-authoring.current { - background: var(--authoring); - border-color: var(--authoring); -} -nav.docs a.tag-worker.current { - background: var(--worker); - border-color: var(--worker); -} -nav.docs a.tag-mc.current { - background: var(--mc); - border-color: var(--mc); -} - -/* ---- Legend chips ---------------------------------------------------- */ - -.legend { - display: flex; - flex-wrap: wrap; - gap: 0.5rem 1rem; - margin: 0.75rem 0 1.25rem; - font-size: 0.82rem; -} - -.legend span::before { - content: ""; - display: inline-block; - width: 0.75em; - height: 0.75em; - margin-right: 0.4em; - border-radius: 3px; - vertical-align: -0.05em; -} - -.legend .l-engine::before { - background: var(--engine); -} -.legend .l-authoring::before { - background: var(--authoring); -} -.legend .l-worker::before { - background: var(--worker); -} -.legend .l-mc::before { - background: var(--mc); -} -.legend .l-memory::before { - background: var(--memory); -} -.legend .l-ui::before { - background: var(--ui); -} - -/* ---- Tables ----------------------------------------------------------- */ - -table { - border-collapse: collapse; - width: 100%; - margin: 0.75rem 0 1.25rem; - font-size: 0.88rem; -} - -th, -td { - border: 1px solid var(--line); - padding: 0.4rem 0.6rem; - text-align: left; - vertical-align: top; -} - -th { - background: #f6f6f6; - font-weight: 600; -} - -/* ---- Generic diagram primitives -------------------------------------- */ - -.box { - border: 1.5px solid var(--ink); - border-radius: 8px; - padding: 0.55rem 0.8rem; - background: #fff; -} - -.box strong { - display: block; - font-size: 0.9rem; -} - -.box small { - color: var(--muted); - font-size: 0.78rem; - line-height: 1.35; - display: block; -} - -.box.b-engine { - border-color: var(--engine); - background: var(--engine-bg); -} -.box.b-engine strong { - color: var(--engine); -} -.box.b-authoring { - border-color: var(--authoring); - background: var(--authoring-bg); -} -.box.b-authoring strong { - color: var(--authoring); -} -.box.b-worker { - border-color: var(--worker); - background: var(--worker-bg); -} -.box.b-worker strong { - color: var(--worker); -} -.box.b-mc { - border-color: var(--mc); - background: var(--mc-bg); -} -.box.b-mc strong { - color: var(--mc); -} -.box.b-memory { - border-color: var(--memory); - background: var(--memory-bg); -} -.box.b-memory strong { - color: var(--memory); -} -.box.b-ui { - border-color: var(--ui); - background: var(--ui-bg); -} -.box.b-ui strong { - color: var(--ui); -} - -/* Vertical pipeline: boxes separated by a downward arrow. */ -.pipeline { - display: flex; - flex-direction: column; - gap: 0; - margin: 1rem 0 1.5rem; - max-width: 620px; -} - -.pipeline > .step { - position: relative; -} - -.pipeline > .step + .step { - margin-top: 1.6rem; -} - -.pipeline > .step + .step::before { - content: "▼"; - position: absolute; - top: -1.45rem; - left: 2rem; - font-size: 0.8rem; - color: var(--muted); -} - -.pipeline > .step + .step[data-arrow]::after { - content: attr(data-arrow); - position: absolute; - top: -1.5rem; - left: 3.4rem; - font-size: 0.75rem; - color: var(--muted); - font-family: ui-monospace, Menlo, monospace; -} - -/* Horizontal flow: boxes with a → between them. */ -.hflow { - display: flex; - flex-wrap: wrap; - align-items: stretch; - gap: 0.35rem; - margin: 1rem 0 1.5rem; -} - -.hflow > .box { - flex: 1 1 0; - min-width: 118px; -} - -.hflow > .arr { - align-self: center; - color: var(--muted); - font-size: 1rem; - padding: 0 0.1rem; -} - -/* Two/three column thread lanes. */ -.lanes { - display: grid; - gap: 1rem; - margin: 1rem 0 1.5rem; -} - -.lanes.two { - grid-template-columns: 1fr 1fr; -} -.lanes.three { - grid-template-columns: 1fr 1fr 1fr; -} - -.lane { - border: 1px dashed #bbb; - border-radius: 10px; - padding: 0.75rem; - background: #fcfcfc; -} - -.lane > h4 { - margin: 0 0 0.6rem; - font-size: 0.8rem; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--muted); -} - -.lane .box { - margin-bottom: 0.6rem; -} - -.lane .box:last-child { - margin-bottom: 0; -} - -/* ---- Byte-layout bars -------------------------------------------------- */ - -.bytes { - display: flex; - width: 100%; - margin: 0.75rem 0 0.25rem; - border: 1.5px solid var(--ink); - border-radius: 6px; - overflow: hidden; - font-family: ui-monospace, Menlo, monospace; - font-size: 0.72rem; - text-align: center; -} - -.bytes .seg { - padding: 0.45rem 0.2rem; - border-right: 1.5px solid var(--ink); - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -.bytes .seg:last-child { - border-right: none; -} - -.bytes .seg em { - display: block; - font-style: normal; - color: var(--muted); - font-size: 0.66rem; -} - -.seg.s-header { - background: #ededed; -} -.seg.s-u32 { - background: #d3e5ff; -} -.seg.s-f64 { - background: #fecaca; -} -.seg.s-u8 { - background: #d1fae5; -} -.seg.s-tokens { - background: #fee2b8; -} - -.bytes-caption { - font-size: 0.78rem; - color: var(--muted); - margin: 0.15rem 0 1.25rem; -} - -/* ---- Callouts ----------------------------------------------------------- */ - -.note { - border-left: 4px solid #eab308; - background: #fefce8; - padding: 0.6rem 0.9rem; - margin: 1rem 0; - border-radius: 0 6px 6px 0; - font-size: 0.9rem; -} - -.note.refactor { - border-left-color: var(--memory); - background: var(--memory-bg); -} - -.note strong { - display: inline; -} - -/* ---- At-a-glance card ---------------------------------------------------- */ - -.glance { - display: grid; - grid-template-columns: max-content 1fr; - gap: 0.3rem 1.2rem; - border: 1px solid var(--line); - border-radius: 8px; - padding: 0.85rem 1rem; - margin: 1rem 0 1.5rem; - font-size: 0.88rem; - background: #fafafa; -} - -.glance dt { - font-weight: 600; - color: var(--muted); -} - -.glance dd { - margin: 0; -} - -/* ---- SVG diagrams --------------------------------------------------------- */ - -figure { - margin: 1rem 0 1.5rem; -} - -figure svg { - max-width: 100%; - height: auto; -} - -figcaption { - font-size: 0.78rem; - color: var(--muted); - margin-top: 0.25rem; -} - -@media (max-width: 720px) { - .lanes.two, - .lanes.three { - grid-template-columns: 1fr; - } -} diff --git a/libs/@hashintel/petrinaut-core/docs/architecture/authoring.html b/libs/@hashintel/petrinaut-core/docs/architecture/authoring.html deleted file mode 100644 index ae048fa3b0a..00000000000 --- a/libs/@hashintel/petrinaut-core/docs/architecture/authoring.html +++ /dev/null @@ -1,288 +0,0 @@ - - - - - - Simulation Architecture — Compilation (Authoring) - - - - - -

Compilation (Authoring)

-

- The HIR pipeline turns user-authored TypeScript into checked, - source-spanned buffer programs. Scenario code retains a separate - same-realm compiler hardened against accidents, not attackers. -

- -
-
Inputs
-
- Code strings stored on the SDCPN document (lambda, kernel, dynamics, - metric, scenario) -
-
Outputs
-
- Versioned HIR artifacts for runtime code; InitialMarking - for scenarios -
-
Errors
-
- Thrown at compile time with the offending item’s ID - (SDCPNItemError) → surfaced as protocol - error messages / result objects -
-
Runs on
-
- LSP worker (HIR compilation) · simulation workers (buffer programs) · - main thread (scenarios and timeline metric evaluation) -
-
- -

The compilation pipeline (module-style code)

-

- Dynamics, lambdas, kernels, and metrics all compile through the HIR. The - first three are authored as export default Wrapper(fn) - modules; metrics are authored as function bodies. -

- -
-
- User source - export default Lambda((input, parameters) => …) - (TypeScript allowed) -
- -
- Lower to HIR - Parse the supported TypeScript subset with exact source spans -
- -
- Check + analyze - Validate against places, token layouts, parameters, and output - shapes -
- -
- Emit artifact - Generate buffer-native JS with offsets and strides baked in -
- -
- Instantiate in runtime - Validate v4 fingerprint, then bind parameters, pool, and ABI - views -
-
- -
- The - LSP virtual files - (lsp/generate-virtual-files.ts) are the type-level twin of - this pipeline: they generate the Color_*, - Parameters, Dynamics, - TransitionKernel… declarations that Monaco checks against, so - editor diagnostics and runtime artifacts are produced from the same HIR - checks and emitters. -
- -

What gets compiled, where it runs

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SurfaceAuthored asCompiled byExecuted inSignature at runtime
Transition lambdamodule, Lambda(fn)compileHirArtifacts in the LSP workerWorker, per enablement check - (f64, u64, u8, placeBases, indices) → boolean | rate -
Transition kernelmodule, TransitionKernel(fn)compileHirArtifacts in the LSP workerWorker, per firing - (views, placeBases, indices, outputViews, sink) → void -
Dynamicsmodule, Dynamics(fn) - compileHirArtifacts (colours with a real element) - Worker, per step per place - (placeBytes, tokenCount) → Float64Array -
Metricplain function body (no module)compileHirArtifacts in the LSP workerMain thread (timeline) · MC worker (experiments) - (f64, u64, u8, placeCounts, placeOffsets) → finite number - — NaN/∞ throw -
Scenarioper-place expressions or one code-mode bodyauthoring/scenario/compile-scenario.tsMain thread, before createSimulationInitialMarking (+ scenario parameter values)
Place visualizermodule, Visualization(fn) returning JSX - Host UI package (@hashintel/petrinaut, - ui/lib/compile-visualizer.ts — the one inherently - React-based surface, since visualizers author JSX) - Main thread render({tokens, parameters}) → ReactElement
- -

Scenario compilation → InitialMarking

- -
-
-

per_place mode

-
- Uncoloured place → expression string - Evaluated with parameters and scenario in - scope; result rounded and clamped >= 0 (a token count). -
-
- Coloured place → row arrays - Rows in colour element order; each row coerced through - coerceTokenRecord (typed defaults for missing columns, - extra columns throw). -
-
-
-

code mode

-
- One function body - Returns { PlaceName: count | TokenRecord[] } keyed by - place name (per_place uses place IDs — a known - asymmetry). Unknown names are silently dropped. -
-
- Result: InitialMarking (JSON) - Keyed by place ID. Fed to createSimulation, packed to - binary in buildSimulation. -
-
-
- -

Metric compilation and evaluation

-
-
- compileHirArtifacts - Lower, typecheck, and emit a buffer-native metric program. -
- -
- createHirMetricEvaluator - Bind referenced place ordinals and the per-run string pool. -
- -
- Per-frame evaluation - Quick sim: evaluated on the main thread against stored frames. - Experiments: evaluated inside the MC worker per run per frame - (metrics/specs.ts). -
-
- -

Scenario sandboxing (authoring/sandbox.ts)

- - -

Distributions (deferred sampling)

-

- HIR-emitted kernels construct small runtime values for - Distribution.Gaussian / Uniform / Lognormal. Kernels defer - sampling through the ABI sink; the engine samples each value once per - token with the seeded RNG (sample-distribution.ts), so - chained .map(fn) transforms share a single draw and runs stay - reproducible. When stochasticity is disabled, the runtime isn’t injected - and plain values are required. -

- -
- Compatibility seam. HIR artifacts contain a version and - fingerprint of their sanitized compilation input. Any schema, code, or - extension change requires recompilation before the engine will run them. -
- - diff --git a/libs/@hashintel/petrinaut-core/docs/architecture/engine.html b/libs/@hashintel/petrinaut-core/docs/architecture/engine.html deleted file mode 100644 index d135b93ba2c..00000000000 --- a/libs/@hashintel/petrinaut-core/docs/architecture/engine.html +++ /dev/null @@ -1,770 +0,0 @@ - - - - - - Simulation Architecture — Engine - - - - - -

Engine

-

- engine/ + frames/ — builds an SDCPN into a - runnable SimulationInstance and advances it one immutable - binary frame at a time. -

- -
-
Entry points
-
- buildSimulation(input) · - computeNextFrame(simulation) -
-
Inputs
-
- SDCPN snapshot, InitialMarking, parameter values, seed, dt, - maxTime (number | null; null = no limit), extensions -
-
Outputs
-
- New EngineFrame per step + - completionReason: "maxTime" | "deadlock" | null -
-
Events
-
- None — pure return values; the worker turns - them into protocol messages -
-
Runs on
-
- Worker thread (quick sim). The same engine functions are reused by - Monte Carlo with different frame - storage -
-
- -

Lifecycle

- -
-
- buildSimulation(input) - Flattens component instances, compiles user code (authoring): lambdas only where available, kernels only for coloured outputs, - dynamics only for colours with at least one real element. - Packs the initial marking into frame 0 (coercing token values by - element type). -
-
- SimulationInstance - frames: EngineFrame[] (history, frame 0 included) · - frameLayout · compiledTransitions · - differentialEquationFns · parameterValues · - dt · maxTime · currentTime · currentFrameNumber · rngState -
-
- computeNextFrame(simulation) - Returns a new instance value with one more frame appended — - the previous frames are never mutated. -
-
- -

One step, in order

- -
-
- 1 · maxTime? - If maxTime !== null and - currentTime >= maxTime, return - "maxTime" without computing. -
- -
- 2 · Dynamics - Per place with a differential equation: slice its token region, - decode tokens, call the user Dynamics fn, apply Euler - x += dx·dt to real slots (discrete derivatives - are forced to 0). Builds an intermediate frame. -
- -
- 3 · Transitions - executeTransitions: for each transition — enablement - (arc weights, inhibitor/read arcs), token-combination enumeration, - lambda (predicate / stochastic rate vs seeded RNG), kernel for - coloured outputs. Removals compact the buffer; additions - append. -
- -
- 4 · Timers - If nothing fired, advance every transition’s - timeSinceLastFiringMs by dt. -
- -
- 5 · Complete? - maxTime reached, or deadlock (nothing fired and no - transition is enabled). -
-
- -
- Every stage that changes state allocates a new - ArrayBuffer via createEngineFrame() — dynamics, - token removal, and token insertion each rebuild the frame. Immutability - makes history trivially correct at the cost of allocation churn per step. -
- -

EngineFrame — the binary format

-

- One ArrayBuffer, read through section-typed views. The frame - stores no IDs and no time — decoding requires the - EngineFrameLayout (place/transition order, per-place - strideBytes and TokenSlotLayout) derived from - the SDCPN, and time travels as payload metadata. -

-

- Source of truth: - libs/@hashintel/petrinaut-core/src/simulation/frames/internal-frame.ts - — createEngineFrame() computes every offset below and is the - only frame constructor; readEngineFrame() recreates the same - views for reading; the header constants live in the - HeaderOffset enum. -

- -
- - - - - - - - - - Memory map — example instance - - - byte offsets on the left · one typed-array view per section on the - right - - - - - - header — 64 B fixed - - magic "PFRM" · version · P · T · - - - section offsets · byteLength - - DataView, little-endian - - - - place token counts - u32 × P = 8 B - Uint32Array(buf, 64, P) - - tokens currently in each place - - - - - place value offsets - u32 × P = 8 B - Uint32Array(buf, 72, P) - - each place’s token run, relative to the token region start - - - - - transition elapsed - f64 × T = 16 B - Float64Array(buf, 80, T) - - ms since each transition last fired - - - - - transition firing counts - u32 × T = 8 B - Uint32Array(buf, 96, T) - - cumulative firings since frame 0 - - - - - - fired flags - - — u8 × T = 2 B - - - Uint8Array(buf, 104, T) - - - - padding — 6 B - - alignTo(…, 8) so f64 views stay aligned - - - - - - tokenBytes / tokenF64 views (b112…) - - - packed token structs — place-major, token-major; - - - this all-real example is pure f64 (booleans = u8) - - - - - - slot 0 · b112 · p1 · tok0 · x - - slot 1 · b120 · p1 · tok0 · y - - slot 2 · b128 · p1 · tok0 · z - - slot 3 · b136 · p1 · tok1 · x - - slot 4 · b144 · p1 · tok1 · y - - slot 5 · b152 · p1 · tok1 · z - - slot 6 · b160 · p2 · tok0 · a - - slot 7 · b168 · p2 · tok0 · b - - - - - p1 — byteOffset 0 - - 2 tokens × stride 24 B (x,y,z — all real) - - - p2 — byteOffset 48 - - 1 token × stride 16 B (a,b) → byte 112 + 48 - - - - - 0 - 64 - 72 - 80 - 96 - 104 - 106 - 112 - 176 - - - - byteLength = 176 (recorded in the header, checked on read) - - -
- Exact offsets for a concrete instance: P = 2 places (p1 - = 2 tokens × 24-byte stride, p2 = 1 token × 16-byte stride — all - dimensions real here, so every field is one f64; a boolean - dimension would appear as a single u8 byte inside its token’s stride), - T = 2 transitions → 176-byte frame. Sections are laid - out by createEngineFrame(); sections and strides are padded - to 8-byte boundaries by alignTo(). Block heights are not to - scale. -
-
- -

Header (offsets in bytes, little-endian, via DataView)

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
OffsetFieldTypeValue / meaning
0magicu320x5046524d (“PFRM”) — corrupt-frame guard
4versionu162 — the current (only) format; readers assert this on decode
6headerBytesu1664
8 / 12placeCount / transitionCountu32must match the layout (checked on read)
16tokenByteLengthu32token region length in bytes
20–40section offsetsu32 × 6byte offsets of each section above
44byteLengthu32whole-frame length (checked on read)
- -

Token region

-

- Each coloured place owns a contiguous run of - count × strideBytes bytes starting at its byte offset - (uncoloured places have stride 0 and only a count), giving O(1) access to - any place via the layout. Within a token, each element sits at its - layout-computed byte offset: real/integer as - f64, boolean as one u8 byte. Value coercion (integers - rounded, booleans 0/1) lives in engine/token-values.ts; byte - placement in engine/token-layout.ts. -

- -

Who reads and writes the buffer

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
PathDirectionView usedNotes
createEngineFrame(layout, snapshot)write (allocate) - DataView header + section views, bulk - Uint8Array.set - - Only constructor of frames; recomputes byte offsets from counts × - strides. -
- readEngineFrame(layout, frame) → - EngineFrameView - read - Uint32Array sections + tokenBytes (u8) / - tokenF64 views - - Zero-copy; toSnapshot() copies the token region into a - fresh Uint8Array. -
Dynamics (computePlaceNextState)read + writef64 view over a fresh byte copy - Euler touches only realFieldF64Offsets; discrete bytes - copied through untouched. -
- Transition firing (compute-possible-transition, - execute-transitions, remove-tokens…) - read + write - readTokenRecord / - encodeTokenValuesToBytes + byte-range copies - - Input tokens decoded to TokenRecords for user - lambdas/kernels; kernel outputs packed into per-token - Uint8Array blocks. -
SimulationFrameReader (main thread)readSame section views over the cloned buffer - getPlaceTokens() = decoded records via the place’s - TokenSlotLayout; getTransitionState() = - timers/flags. -
- -

Token attribute typing (discrete types)

- - -

Token memory layout — packed structs

-

- Since FRAME_VERSION = 2, tokens are stored as schema-driven - packed structs (array-of-structs; column layout was - considered and rejected because the workload — kernels, copies, UI reads — - is token-oriented). - engine/token-layout.ts (computeTokenSlotLayout) - is the single source of truth: -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Logical typePhysical typeNotes
realf64 — 8 Bunchanged
integerf64 — 8 B, rounded on read/write - Exact only within ±2^53 (documented in the schema). - i32 rejected (silent wraparound at ±2^31); i64 rejected (bigint is - contagious into user code: fortune * 1.05 would throw). - If >2^53 is ever needed, add a separate opt-in - int64 element type instead of changing - integer. -
booleanu8 — 1 B - Not bit-packed (would break byte-granular copies for marginal - savings). -
uuid (128-bit, FE-1121)u64 × 2 — 16 B - Two little-endian lanes read via the shared - BigUint64Array view, combined to one - bigint at the boundary (~28 ns; lane-compare without - combining for equality, ~4× faster). Never routed through - number (NaN-payload hazard). Kernel outputs are - optional — omitted values auto-generate from the seeded RNG. -
string (FE-769)u64 pool reference — 8 B - The frame stores an ID into an append-only per-run string intern - pool (engine/string-pool.ts); the pool lives on - SimulationInstance, not on the frame, so frames stay - fixed-stride and byte-copyable. Equal strings share one ID; id 0 is - the pre-seeded "", so zeroed buffers decode cleanly. -
- - -

Determinism

-

- seeded-rng.ts provides a pure - nextRandom(state) → [value, nextState]. The RNG state lives - on SimulationInstance.rngState and is threaded through - stochastic lambda sampling and distribution draws, so a given - (SDCPN, marking, parameters, seed, dt) always reproduces the - same frame sequence. -

- -
- Refactoring seams. - (1) Per-step allocation: each stage rebuilds the whole buffer — a - double-buffer strategy (as Monte Carlo already does) would remove most - churn. (2) 128-bit types (UUID, FE-1121) slot into the layout as - u64×2 fields (align 8, read via BigUint64Array) - — the layout machinery is ready; the value plumbing (bigint - boundary, v5 coercion, seeded generation) is not. (3) The worker keeps the - full frames[] history although only the latest frame is - needed to advance — history retention belongs to the main-thread store. -
- - diff --git a/libs/@hashintel/petrinaut-core/docs/architecture/index.html b/libs/@hashintel/petrinaut-core/docs/architecture/index.html deleted file mode 100644 index 0df5a6d6e35..00000000000 --- a/libs/@hashintel/petrinaut-core/docs/architecture/index.html +++ /dev/null @@ -1,439 +0,0 @@ - - - - - - Petrinaut Simulation — Architecture Overview - - - - - -

Simulation Architecture — Overview

-

- How Petrinaut runs SDCPN simulations: who owns the memory, what crosses - thread boundaries, and how results are read back. Code lives in - petrinaut-core/src/simulation/ (engine, authoring, worker, - runtime, monte-carlo). These pages describe the headless core only — - consumers appear as "the host application" using the public API; the React - integration is documented separately in - @hashintel/petrinaut/ARCHITECTURE.md. Generated import maps spanning core, React, and UI modules are in the - dependency diagrams. -

- -
- Engine (stepping & frames) - Compilation (user code) - Worker & protocol - Monte Carlo - Memory / buffers - Host application -
- -

The two execution paths

-

- There are two independent ways to execute a net. - Quick Simulation runs one interactive simulation and - keeps every frame so the host can scrub. - Experiments run many Monte Carlo simulations with - bounded memory (two reusable buffers per run) and only ship - metric aggregates to the host — frame buffers never leave the worker. -

- -
-
-

Main thread — host application

-
- Run configuration - owns InitialMarking, parameter values, seed/dt/maxTime - (null = unbounded); calls createSimulation() -
-
- Playback driver - core playback module — picks the viewed frame - (getFrame(i)), drives ack/backpressure per play - mode -
-
- Experiment consumer - one MonteCarloExperiment handle per experiment, - subscribed to its stores -
-
- Rendering & metrics - read via SimulationFrameReader / metric frames -
-
-
-

Main thread — core runtime

-
- createSimulation() - runtime/simulation.ts — sanitizes SDCPN for extensions, flattens - component instances, owns lifecycle stores + events -
-
- SimulationFrameStore - runtime/frame-store.ts — retains every - SimulationFramePayload (ArrayBuffer + time) -
-
- SimulationFrameReader - frames/frame-reader.ts — typed-array views over a stored frame, - zero-copy reads -
-
- createMonteCarloExperiment() - monte-carlo/runtime/experiment.ts — status / progress / metrics - stores -
-
-
-

Worker threads

-
- simulation.worker - init → buildSimulation(); loop → - computeNextFrame(); streams frames under ack - backpressure -
-
- SimulationInstance.frames[] - full EngineFrame history — the compute source of truth -
-
- monte-carlo.worker - MonteCarloSimulator — round-robin - advanceAll(), metrics observed in-worker -
-
- 2 × ArrayBuffer per run - current / next, swapped each step; no history -
-
-
- -

Where the memory actually lives

-

- Everything the simulation computes is stored in raw - ArrayBuffers read through typed-array views. There is - no object graph of tokens — tokens are packed structs - (real/integer as f64 fields, - boolean as one u8 byte, stride rounded to 8 B), decoded to JS - objects only at the read boundary. -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
MemoryStructureThreadLifetime / role
SDCPN documentPlain JS objects (editor state)Main - Snapshot is structured-cloned into the worker at init; - later edits don’t affect a running simulation. -
Initial marking - JSON (InitialMarking: count or - TokenRecord[] per place) - Main (host state) - Packed into frame 0 by - buildSimulation → packInitialPlaceMarking. -
EngineFrame (quick sim) - ArrayBuffer: 64-byte header + - Uint32/Float64/Uint8 sections - + packed token structs (byte-addressed places) - Worker - Immutable snapshot per step. The worker appends every frame to - SimulationInstance.frames[] — compute source of truth. -
SimulationFramePayload{ time, frame: ArrayBuffer }Worker → Main - Structured-clone copy per frame (no transfer list). - Retained forever by the in-memory frame store for scrubbing. -
SimulationFrameReader - Uint32Array/Float64Array views over the - stored buffer - Main - Zero-copy views. getPlaceTokens() materializes typed - TokenRecord objects via the place’s - TokenSlotLayout. -
Monte Carlo run buffers - 2 × ArrayBuffer per run (current / next), token-value - region has capacity + growth policy - MC worker - Swapped each step. Never sent to the main thread — only progress - counters and metric frames (small JSON) cross the boundary. -
Compiled user codeVersioned HIR buffer programs; scenarios remain plain JSWorkers (HIR programs) · Main (scenarios, timeline metrics) - Produced by authoring at init time. -
- -
- Refactoring seam — triple retention. Each quick-sim frame - currently exists three times: in the worker history - (SimulationInstance.frames[]), as a structured-clone copy in - the main-thread frame store, and transiently in the - postMessage queue. Frames are never transferred - (Transferable) and the worker history is only read by the - stepping loop’s latest entry. Retention policy is deliberately isolated - behind runtime/frame-store.ts so this can change without - touching consumers. -
- -

Quick Simulation — end-to-end data flow

- -
-
- SDCPN snapshot + InitialMarking + parameters + - seed/dt/maxTime - Assembled by the host (main thread). Scenario compilation may have - produced the marking and parameter overrides first. -
-
- simulation.worker — buildSimulation() - Sanitize by extensions → compile lambdas / kernels / dynamics → pack - frame 0. Replies frame(0) + ready. -
-
- computeNextFrame() × batch - Dynamics (Euler) → transitions (enablement, lambda, kernel) → new - immutable EngineFrame appended; time += dt. -
-
- SimulationFrameStore (main) - Appends every payload; publishes {count, latest} through - the frames store. -
-
- SimulationFrameReader - compileSimulationFrameReader(sdcpn) specialises the - layout once; readers are created per frame on demand (latest() - / getFrame(i)). -
-
- Host reads - The playback driver picks frameIndex; the host renders - from getTransitionState(), getPlaceTokens(), - and getPlaceTokenCount(); metric code reads the raw - packed frame through a HIR evaluator. -
-
- -

Protocols at a glance

-

- Both workers speak a small typed message protocol over - postMessage, wrapped in a - SimulationTransport (queues messages until the worker boots). - Full payloads and sequence diagrams are on the - Worker and - Monte Carlo pages. -

- - - - - - - - - - - - - - - - - - - - - - - - -
Host → WorkerWorker → HostFlow control
- Quick sim
worker/messages.ts -
- init · start · pause · stop · setBackpressure · ack - - ready · frame · frames · paused · complete · error - - Ack-based backpressure: worker computes at most - maxFramesAhead past the last acked frame. -
- Monte Carlo
monte-carlo/worker/messages.ts -
init · start · cancel - ready · progress · metricFrames · complete · cancelled · - error - - Fire-and-forget batches (advanceAll() × batchSize per - loop tick); cancellation checked between batches. -
- -

Module map

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
AreaPathOwnsDetails
Engineengine/, frames/Build + stepping, EngineFrame binary format, frame readersengine.html
Compilationauthoring/ - User code → JS functions (lambda, kernel, dynamics, metric, - scenario), sandbox hardening - authoring.html
- Worker + runtime - worker/, runtime/ - Transport protocol, backpressure, lifecycle stores, frame retention - worker.html
Monte Carlomonte-carlo/ - Batch runs, bounded buffers, metric pipeline, experiment handle - monte-carlo.html
- -

Design invariants worth keeping

- - - diff --git a/libs/@hashintel/petrinaut-core/docs/architecture/monte-carlo.html b/libs/@hashintel/petrinaut-core/docs/architecture/monte-carlo.html deleted file mode 100644 index 31a10a8eeba..00000000000 --- a/libs/@hashintel/petrinaut-core/docs/architecture/monte-carlo.html +++ /dev/null @@ -1,376 +0,0 @@ - - - - - - Simulation Architecture — Monte Carlo (Experiments) - - - - - -

Monte Carlo (Experiments)

-

- monte-carlo/ — runs many independent simulations of the same - net with bounded memory, aggregates metrics inside the worker, - and ships only small JSON metric frames to the UI. Frame buffers never - cross the thread boundary. -

- -
-
Core
-
- createMonteCarloSimulator(config) → - MonteCarloSimulator (synchronous, thread-agnostic) -
-
Worker
-
- monte-carlo.worker.ts + worker/messages.ts -
-
Host handle
-
- createMonteCarloExperiment() → stores: - status · progress · metrics + events; - start/cancel/dispose -
-
Inputs
-
- SDCPN, marking, parameters, seed, dt, - maxTime (required), runCount, metric specs -
-
Outputs
-
- MonteCarloWorkerProgress (run counters) + - MonteCarloUserDefinedMetricFrame[] -
-
- -

Layers

- -
-
- Host application - One experiment record per run of - createMonteCarloExperiment(); subscribes to the handle’s - status/progress/metrics stores and renders the metric frames. -
-
- Experiment handle (main) - runtime/experiment.ts — worker mode - (createWorker/transport) or - local mode (runs the simulator on the calling thread, used by - tests/embedding). -
-
- monte-carlo.worker - Builds the simulator + compiles metric specs, then loops: - advanceAll() × batchSize (default 4), post - progress + pending metricFrames, yield, - repeat. cancel is honoured between batches. -
-
- MonteCarloSimulator - Owns N × MonteCarloRun; deterministic round-robin - advanceAll() advances every active run one frame per - call, so long runs don’t starve short ones. -
-
- Engine functions, reused - Same enablement/lambda/kernel/dynamics code as quick sim (transition-effect.ts - adapts them to the MC buffers). -
-
- -

Run model

- - - - - - - - - - - - - - - - - - - - - - - - - -
Per runMeaning
- seed, parameterValues, - initialMarking - - Defaults derived from the experiment config; overridable per run - (runs[]), e.g. seed = base seed + index. -
status - ready → running → complete | error — errors are per - run, other runs continue. -
- frameNumber · currentTime · rngState · completionReason - - Progress; a run completes on its own deadlock or the shared - maxTime. -
- tokenByteCount · tokenByteCapacity · reallocations - - Buffer telemetry, exposed in MonteCarloRunSummary. -
- -

Memory — two buffers per run, swapped every step

- -
-
-

step k

-
- currentFrame (read) state at frame k -
-
- nextFrame (write) - dynamics + transitions write frame k+1 here -
-
-
-

after the step

-
- swap pointers - current ⇄ next — no allocation, no history. If the - next token count doesn’t fit, the target buffer alone reallocates: - nextCapacityBytes = max(requiredBytes, capacity × 2, 64). -
-
- metrics observe frame k+1 - then the data may be overwritten — readers are only valid during - observeFrame. -
-
-
- -

- The buffer layout (frame-buffer.ts) is a leaner sibling of - the quick-sim EngineFrame: same sections, - no 64-byte header, one extra - transitionElapsedFrames section, and a token region with - spare capacity: -

- -
-
- place countsu32 × P -
-
- place offsetsu32 × P -
-
- transition elapsedf64 × T -
-
- elapsed framesf64 × T -
-
- firing countsu32 × T -
-
- fired flagsu8 × T -
-
- token structsbytes, used ≤ capacity -
-
- sparecapacity -
-
-

- All views - (Uint32Array/Float64Array/Uint8Array) - are created once per buffer over a single ArrayBuffer; IDs - resolve to dense indices through the shared EngineFrameLayout - (layout.ts). -

- -

Metrics pipeline — computed where the data is

- -
-
- Specs - MonteCarloMetricSpec: expression (metric - code body) · placeTokenCountMean · - transitionFiringCount — serializable, sent in - init. -
- -
- Compile before worker start - Expression metrics carry HIR artifacts; built-in specs create a - measure(run) function directly. -
- -
- Sample per frame - observeFrame(ctx) visits every run’s current frame as a - SimulationFrameReader (forEachRunFrame); - sampleRuns filters active/completed/all. -
- -
- Aggregate - Across runs: mean·sum·min·max·last → scalar, or keep the - run axis and bin it → distribution (exact or bin - width). Optionally aggregate over time. -
- -
- Metric frames - MonteCarloUserDefinedMetricFrame — scalar - (value/frameValue/timeValue) or distribution (bins: [value, frequency][]) per frame. Small JSON. -
-
- -

Protocol (monte-carlo/worker/messages.ts)

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
DirectionTypePayload
Host → Workerinit - sdcpn, extensions?, initialMarking, parameterValues, seed, dt, - maxTime, runCount, batchSize?, metricSpecs? -
start
cancel— (checked between batches)
Worker → Hostready
progress - MonteCarloWorkerProgress = advance counters (advancedRuns, completedRuns, erroredRuns, activeRuns, - allFinished) + frameNumber, time, runCount -
metricFramesPending MonteCarloUserDefinedMetricFrame[] batch
complete / cancelledFinal (or last-known) progress
errormessage, itemId
- -
- Contrast with quick sim: no ack/backpressure — the worker - free-runs to completion in small batches; the only upstream control is - cancel. And the payloads are aggregates, not frames: the UI - never holds Monte Carlo simulation state. -
- -

What the host receives

- - -
- Refactoring seams. - (1) Runs are round-robin on one worker thread; the run-state model was - designed so runs can shard across multiple workers later. (2) User code - still receives decoded TokenRecord objects per firing — the - README’s planned “IR compilation” would let lambdas/kernels operate - directly on the numeric buffers. (3) Buffer growth is a naive ×2 doubling; - arc-weight static analysis could size buffers up front and eliminate - reallocations. -
- - diff --git a/libs/@hashintel/petrinaut-core/docs/architecture/worker.html b/libs/@hashintel/petrinaut-core/docs/architecture/worker.html deleted file mode 100644 index 0a898518bc3..00000000000 --- a/libs/@hashintel/petrinaut-core/docs/architecture/worker.html +++ /dev/null @@ -1,752 +0,0 @@ - - - - - - Simulation Architecture — Worker & Protocol - - - - - -

Worker & Protocol

-

- worker/ (protocol + worker entrypoint) and - runtime/ - (main-thread lifecycle, transport, frame retention). Everything between - the host application and the engine. -

- -
-
Protocol
-
- worker/messages.ts — discriminated unions over - postMessage -
-
Transport
-
- runtime/transport.ts — wraps a Worker factory; - queues messages until the worker boots -
-
Host handle
-
- createSimulation()Simulation (stores: - status, frames; events; - run/pause/reset/ack/getFrame/dispose) -
-
Flow control
-
- Ack-based backpressure — the consumer’s pace bounds worker - memory/compute -
-
- -

Message protocol

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Host → Worker (ToWorkerMessage) -
TypePayloadEffect in the worker
init - sdcpn, extensions?, initialMarking, parameterValues, seed, dt, - maxTime (number | null), maxFramesAhead?, batchSize? - - Runs buildSimulation() (compiles user code, packs frame - 0), replies frame(0) then ready. Resets - lastAckedFrame = -1. -
start - Starts the async compute loop (no-op if running; refuses from - complete/error). -
pauseStops the loop, keeps all state, replies paused.
stopDiscards the SimulationInstance entirely.
setBackpressuremaxFramesAhead?, batchSize?Live-updates loop tuning.
ackframeNumber - lastAckedFrame = max(lastAckedFrame, n) — unblocks - computation. -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Worker → Host (ToMainMessage) -
TypePayloadEffect on the main thread
readyinitialFrameCount - Resolves the createSimulation promise; status → - Ready. -
frame / frames - SimulationFramePayload = - { time, frame: ArrayBuffer } (single / batch) - - Appended to the frame store; frames store publishes - {count, latest: reader}. -
pausedframeNumberStatus → Paused.
completereason: "deadlock" | "maxTime", frameNumberStatus → Complete; emitted on the event stream.
errormessage, itemId (offending SDCPN item if known) - Status → Error; rejects init if still initializing. -
- -

Sequence — one quick simulation

- -
- - - - - - - - - - - Host (playback driver) - - - - createSimulation (main) - - - - simulation.worker - - - - - - - - - - createSimulation(config) - - - - init {sdcpn, initialMarking, seed, dt…} - - - buildSimulation() - - - compile + frame 0 - - - - - frame {t:0, ArrayBuffer²} - - ready {initialFrameCount:1} - - Promise resolves · status "Ready" - - - - simulation.run() - - start - - - - - loop blocked: lastAckedFrame = -1 - - - → nothing computes until first ack - - - - - - - loop — while frames remain to compute - - - - - - ack(frameNumber) — playback mode decides when - - - - - - computeNextFrame() - - - × batchSize while - - - n - acked < ahead - - - - - frames [{time, ArrayBuffer²}, …] - - frames store → {count, latest reader} - - - - complete {reason: deadlock | maxTime} - - status "Complete" + event emitted - -
- ² = the frame ArrayBuffer is - structured-clone copied - across the boundary (no transfer list); the worker keeps its own copy in - SimulationInstance.frames[]. -
-
- -

Backpressure — the ack contract

- -
// worker compute loop (simplified from simulation.worker.ts)
-while (isRunning) {
-  if (lastAckedFrame < 0 || currentFrame - lastAckedFrame >= maxFramesAhead) {
-    await delay(10); continue;          // wait for the consumer
-  }
-  batch = compute up to batchSize frames (stop on complete/error)
-  post "frame" | "frames"
-  await delay(0);                        // let pause/stop/ack messages in
-}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Play mode (core playback module)maxFramesAheadbatchSizeAck behaviour
viewOnlyn/a - No core backpressure profile — the React playback provider pauses - the worker and never acks, so nothing new is computed. -
computeBuffer4010 - Acks when playback is within ~0.5 s of the last computed frame — - keeps a small rolling buffer ahead of the playhead. -
computeMax10000500Acks every arrival → compute as fast as possible.
Worker defaults10001000Used when init omits the settings.
- -

Lifecycle

- -
- - - - - - - - - Initializing - - Ready - - Running - - Paused - - Complete - - Error - - - ready msg - - run() - - pause() - - run() - - deadlock / maxTime - - error msg - - - reset(): stop + clear store → Ready (from any state) - - -
- Main-thread SimulationState. The worker mirrors a smaller - internal status (ready / running / complete / error); - complete and error are terminal for the worker — - reset() sends stop and requires a fresh - init to run again. -
-
- -

- Main-thread runtime responsibilities (createSimulation) -

- - -
- Refactoring seams. - (1) Frames could be posted with a transfer list to halve copies — - but not as-is: computeNextFrame() re-reads the latest stored - frame to compute the next one, so transferring (detaching) the posted - buffer first requires stepping from a retained working copy (e.g. - latest-only retention or a double buffer). (2) reset() keeps - the main-thread status Ready but the worker has discarded its - simulation — a subsequent run() sends start to a - worker with nothing to run; a full re-init is what actually happens in the - UI flow. (3) The 10 ms poll while waiting for acks is a busy-wait; a - promise-per-ack would be exact. -
- - diff --git a/libs/@local/petrinaut-arch-docs/content/components/byte-map.tsx b/libs/@local/petrinaut-arch-docs/content/components/byte-map.tsx new file mode 100644 index 00000000000..b8541da8f06 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/components/byte-map.tsx @@ -0,0 +1,88 @@ +/** + * A byte-addressed memory map: offset gutter on the left, one row per section. + * + * This is the diagram the hand-written HTML pages did best — reading the frame + * format as a table loses the sense of a single contiguous buffer, which is the + * whole point of the format. Offsets are supplied rather than computed, because + * the map illustrates one worked example rather than a live layout. + */ + +import "./diagram.css"; +import { Inline } from "./inline"; + +export interface SegmentSpec { + /** Byte offset where this section starts. */ + offset: number; + name: string; + /** Element type and count, e.g. `u32 × P`. */ + type?: string; + /** The typed-array view used to read it. */ + view?: string; + accent?: "core" | "worker" | "none"; +} + +export interface ByteMapProps { + title?: string; + segments: SegmentSpec[]; + /** Total length, rendered as the closing offset. */ + byteLength?: number; + caption?: string; +} + +const accentVariable = (accent: SegmentSpec["accent"]): string | undefined => + accent === undefined || accent === "none" + ? undefined + : `var(--pnd-${accent})`; + +export const ByteMap = ({ + title, + segments, + byteLength, + caption, +}: ByteMapProps) => ( +
+ {title === undefined ? null : ( +
{title}
+ )} +
+ {segments.map((segment) => ( + + ))} + {byteLength === undefined ? null : ( + <> +
{byteLength}
+
+ + )} +
+ {caption === undefined ? null : ( +
+ +
+ )} +
+); + +const Segment = ({ segment }: { segment: SegmentSpec }) => ( + <> +
{segment.offset}
+
+ + + + {segment.type === undefined ? null : ( + + + + )} + {segment.view === undefined ? null : ( + + + + )} +
+ +); diff --git a/libs/@local/petrinaut-arch-docs/content/components/diagram.css b/libs/@local/petrinaut-arch-docs/content/components/diagram.css new file mode 100644 index 00000000000..2f2eabf60f0 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/components/diagram.css @@ -0,0 +1,257 @@ +/* + * Shared styling for the architecture diagram components. + * + * Deliberately self-contained: plain CSS with no preprocessor, no design-system + * import, and no framework assumptions, so the same file works in the Starlight + * site and in any host that renders the bundle. Colours derive from the host's + * text colour where possible, and everything falls back sensibly if the host + * defines none of the custom properties. + */ + +.pnd { + --pnd-line: color-mix(in srgb, currentColor 25%, transparent); + --pnd-muted: color-mix(in srgb, currentColor 65%, transparent); + --pnd-surface: color-mix(in srgb, currentColor 5%, transparent); + + --pnd-core: #3676b8; + --pnd-react: #7051b5; + --pnd-ui: #3d8055; + --pnd-worker: #b5762f; + + margin: 1.5rem 0; + font-size: 0.85rem; + line-height: 1.45; +} + +.pnd-title { + margin-bottom: 0.5rem; + font-size: 0.8rem; + font-weight: 600; + color: var(--pnd-muted); +} + +.pnd-lanes { + display: grid; + gap: 0.75rem; + grid-template-columns: repeat(var(--pnd-columns, 1), minmax(0, 1fr)); +} + +.pnd-lane { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.75rem; + border: 1px solid var(--pnd-line); + border-radius: 0.5rem; + background: var(--pnd-surface); +} + +.pnd-lane-title { + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.02em; + text-transform: uppercase; + color: var(--pnd-muted); +} + +.pnd-box { + padding: 0.5rem 0.6rem; + border: 1px solid var(--pnd-line); + border-left: 3px solid var(--pnd-accent, var(--pnd-line)); + border-radius: 0.35rem; + background: var(--pnd-surface); +} + +.pnd-box-label { + font-weight: 600; +} + +.pnd-box-note { + margin-top: 0.15rem; + color: var(--pnd-muted); +} + +.pnd-box code, +.pnd-lane code, +.pnd-step code { + font-size: 0.9em; +} + +/* Pipeline ------------------------------------------------------------- */ + +.pnd-pipeline { + display: flex; + flex-wrap: wrap; + align-items: stretch; + gap: 0.4rem; +} + +/* + * One flex item per step, arrow included, so a wrap never separates an arrow + * from the box it points at. + */ +.pnd-step-group { + display: flex; + align-items: stretch; + gap: 0.4rem; + flex: 1 1 8rem; + min-width: 8rem; +} + +.pnd-step { + flex: 1 1 auto; + padding: 0.5rem 0.6rem; + border: 1px solid var(--pnd-line); + border-radius: 0.35rem; + background: var(--pnd-surface); +} + +.pnd-step-index { + font-size: 0.7rem; + font-weight: 700; + color: var(--pnd-muted); +} + +.pnd-arrow { + align-self: center; + color: var(--pnd-muted); +} + +/* Visible to assistive technology, not on screen. */ +.pnd-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + border: 0; +} + +/* Byte map ------------------------------------------------------------- */ + +.pnd-bytes { + display: grid; + grid-template-columns: auto 1fr; + gap: 0 0.6rem; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; +} + +.pnd-offset { + padding-top: 0.45rem; + text-align: right; + color: var(--pnd-muted); + font-size: 0.75rem; + white-space: nowrap; +} + +.pnd-seg { + display: flex; + flex-wrap: wrap; + gap: 0.25rem 0.6rem; + align-items: baseline; + padding: 0.4rem 0.6rem; + margin-bottom: 0.25rem; + border: 1px solid var(--pnd-line); + border-left: 3px solid var(--pnd-accent, var(--pnd-line)); + border-radius: 0.3rem; + background: var(--pnd-surface); +} + +.pnd-seg-name { + font-weight: 600; +} + +.pnd-seg-type, +.pnd-seg-view { + color: var(--pnd-muted); + font-size: 0.78rem; +} + +.pnd-seg-view { + margin-left: auto; +} + +/* Sequence ------------------------------------------------------------- */ + +.pnd-sequence { + display: grid; + gap: 0.35rem; +} + +.pnd-msg { + display: grid; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + gap: 0.5rem; + align-items: center; +} + +.pnd-msg-label { + padding: 0.35rem 0.55rem; + border: 1px solid var(--pnd-line); + border-radius: 0.3rem; + background: var(--pnd-surface); +} + +.pnd-msg-dir { + color: var(--pnd-muted); + font-size: 1rem; +} + +.pnd-msg-note { + color: var(--pnd-muted); +} + +.pnd-msg-spacer { + min-width: 0; +} + +.pnd-actors { + display: grid; + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + gap: 0.5rem; + margin-bottom: 0.5rem; + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.02em; + text-transform: uppercase; + color: var(--pnd-muted); +} + +.pnd-actors span:last-child { + text-align: right; +} + +/* Legend --------------------------------------------------------------- */ + +.pnd-legend { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 0.6rem; + font-size: 0.75rem; + color: var(--pnd-muted); +} + +.pnd-legend span::before { + content: ""; + display: inline-block; + width: 0.6rem; + height: 0.6rem; + margin-right: 0.3rem; + border-radius: 0.15rem; + background: var(--pnd-accent, currentColor); +} + +@media (max-width: 640px) { + .pnd-lanes { + grid-template-columns: minmax(0, 1fr); + } + + .pnd-msg, + .pnd-actors { + grid-template-columns: minmax(0, 1fr); + } +} diff --git a/libs/@local/petrinaut-arch-docs/content/components/inline.tsx b/libs/@local/petrinaut-arch-docs/content/components/inline.tsx new file mode 100644 index 00000000000..0777ef723e4 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/components/inline.tsx @@ -0,0 +1,27 @@ +/** + * Renders a plain string, turning `backticked` spans into ``. + * + * Diagram components take strings rather than nodes on purpose. JSX written + * inside MDX is compiled by the host's MDX renderer — Astro's, here — and + * handing that to a React component fails at render time with "Objects are not + * valid as a React child". Strings avoid the interop entirely, which also means + * a host embedding the bundle needs no special MDX/JSX configuration. + * + * Backticks are the one piece of formatting these diagrams actually need, so + * they are supported directly rather than by accepting arbitrary markup. + */ + +import type { ReactElement } from "react"; + +export const Inline = ({ text }: { text: string }): ReactElement => ( + <> + {text.split("`").map((part, index) => + // Odd indices are the spans that sat between a pair of backticks. + index % 2 === 1 ? ( + {part} + ) : ( + {part} + ), + )} + +); diff --git a/libs/@local/petrinaut-arch-docs/content/components/lanes.tsx b/libs/@local/petrinaut-arch-docs/content/components/lanes.tsx new file mode 100644 index 00000000000..c447ddb95b0 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/components/lanes.tsx @@ -0,0 +1,92 @@ +/** + * Lane-and-box diagrams: parallel columns of boxes, one column per thread or + * mode. + * + * Data-driven rather than hand-marked-up, so a page describes the content and + * the component owns the presentation. + */ + +import "./diagram.css"; +import { Inline } from "./inline"; + +/** Accent colours, keyed to the layer families the diagrams talk about. */ +export type Accent = "core" | "react" | "ui" | "worker" | "none"; + +const accentVariable = (accent: Accent | undefined): string | undefined => + accent === undefined || accent === "none" + ? undefined + : `var(--pnd-${accent})`; + +export interface BoxSpec { + label: string; + note?: string; + accent?: Accent; +} + +export interface LaneSpec { + title: string; + accent?: Accent; + boxes: BoxSpec[]; +} + +export const Box = ({ label, note, accent }: BoxSpec) => ( +
+
+ +
+ {note === undefined ? null : ( +
+ +
+ )} +
+); + +export interface LanesProps { + title?: string; + lanes: LaneSpec[]; + /** Colour key rendered beneath the lanes. */ + legend?: { label: string; accent: Accent }[]; +} + +export const Lanes = ({ title, lanes, legend }: LanesProps) => ( +
+ {title === undefined ? null : ( +
{title}
+ )} +
+ {lanes.map((lane) => ( +
+
{lane.title}
+ {lane.boxes.map((box, index) => ( + + ))} +
+ ))} +
+ {legend === undefined ? null : ( +
+ {legend.map((entry) => ( + + {entry.label} + + ))} +
+ )} +
+); diff --git a/libs/@local/petrinaut-arch-docs/content/components/pipeline.tsx b/libs/@local/petrinaut-arch-docs/content/components/pipeline.tsx new file mode 100644 index 00000000000..575999809cf --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/components/pipeline.tsx @@ -0,0 +1,67 @@ +/** + * A numbered left-to-right chain of steps, wrapping on narrow screens. + * + * Used where the old HTML pages drew an arrow chain: the compilation pipeline, + * the fixed order of a simulation step, the metrics pipeline. + */ + +import "./diagram.css"; +import { Inline } from "./inline"; + +export interface StepSpec { + label: string; + note?: string; +} + +export interface PipelineProps { + title?: string; + steps: StepSpec[]; + /** Numbers the steps. Off for pipelines where order is not a sequence. */ + numbered?: boolean; +} + +export const Pipeline = ({ title, steps, numbered = false }: PipelineProps) => ( +
+ {title === undefined ? null : ( +
{title}
+ )} +
+ {steps.map((step, index) => ( + // A fragment per step so the separator sits between, not inside, boxes. + + ))} +
+
+); + +const Step = ({ + index, + numbered, + step, +}: { + index: number; + numbered: boolean; + step: StepSpec; +}) => ( + // The arrow is grouped with the step that follows it rather than emitted as a + // sibling. As separate flex items they wrapped independently, which left an + // arrow stranded at the end of a row pointing at a box on the next one. +
+ {index === 0 ? null : ( + + )} +
+ {numbered ?
{index + 1}
: null} +
+ +
+ {step.note === undefined ? null : ( +
+ +
+ )} +
+
+); diff --git a/libs/@local/petrinaut-arch-docs/content/components/sequence.tsx b/libs/@local/petrinaut-arch-docs/content/components/sequence.tsx new file mode 100644 index 00000000000..6d761804877 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/components/sequence.tsx @@ -0,0 +1,94 @@ +/** + * A two-actor message sequence: who sends what, in order, with side notes. + * + * Kept to two actors deliberately. The hand-written HTML had three columns + * (host, main-thread runtime, worker), which read as three lifelines but made + * every message ambiguous about which hop it described. Two actors and an + * explicit direction says the same thing without the ambiguity. + */ + +import "./diagram.css"; +import { Inline } from "./inline"; + +export interface MessageSpec { + /** `right` is left-actor → right-actor. */ + direction: "right" | "left"; + label: string; + note?: string; +} + +/** A phase separator, rendered as a full-width note rather than a message. */ +export interface PhaseSpec { + phase: string; +} + +export type SequenceRow = MessageSpec | PhaseSpec; + +export interface SequenceProps { + title?: string; + actors: [string, string]; + rows: SequenceRow[]; +} + +const isPhase = (row: SequenceRow): row is PhaseSpec => "phase" in row; + +export const Sequence = ({ title, actors, rows }: SequenceProps) => ( +
+ {title === undefined ? null : ( +
{title}
+ )} +
+ {actors[0]} +
+
+ {rows.map((row, index) => + isPhase(row) ? ( +
+ +
+ ) : ( + /* + * The direction arrow is decorative, so a screen reader would + * otherwise get the label with no indication of who sent it. The + * visually-hidden sentence carries that, and is the only place + * direction is stated in text. + */ +
+ + {row.direction === "right" + ? `${actors[0]} to ${actors[1]}: ` + : `${actors[1]} to ${actors[0]}: `} + + {row.direction === "right" ? ( + <> +
+ +
+ +
+ {row.note === undefined ? null : } +
+ + ) : ( + <> +
+ {row.note === undefined ? null : } +
+ +
+ +
+ + )} +
+ ), + )} +
+
+); diff --git a/libs/@local/petrinaut-arch-docs/content/index.mdx b/libs/@local/petrinaut-arch-docs/content/index.mdx new file mode 100644 index 00000000000..490a2666763 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/index.mdx @@ -0,0 +1,78 @@ +--- +title: Petrinaut architecture +description: How the Petrinaut packages fit together, and where to start reading. +sidebar_order: 0 +--- + +Petrinaut is a visual editor and simulation runtime for Stochastic Dynamic +Coloured Petri Nets (SDCPN). This documentation is in two halves that are +maintained differently, and it is worth knowing which you are reading. + +## The diagrams + +Every box is a layer and every arrow is real imports, counted from the +dependency graph rather than drawn by hand. There are three kinds, each showing +a different slice: + +- The [Architecture](architecture) page opens with the **top-level layers** — + the whole system at a glance, with everything below the first level folded + into its parent. +- Every layer page opens with its **neighbourhood**: what that layer depends on, + and what depends on it. This is usually the one you want. +- A layer that has sub-layers also gets a **drill-down** of its direct children. + +Where a layer has more neighbours than fit legibly, the remainder is drawn as a +single "further layers" node rather than omitted, and the full list is always in +the tables on the page. + +**Generated pages** — everything under [Architecture](architecture) — are +extracted from annotations in the source on every build, and CI fails if they +drift. Layer sizes and dependency counts all come from the code. If a generated page disagrees with the code, that is a bug in the +annotations, not in the page. + +**Authored pages** — this one, and its siblings — carry the reasoning that no +import graph can supply: why a boundary sits where it does, what the alternatives +were, which mistakes are easy to make. They are hand-written and can go stale, so +they avoid restating facts the generated pages already own. + +## Where to start + +- [Two execution paths](two-execution-paths) — the single most important thing to + understand before changing anything in the simulation layers. +- [Architecture](architecture) — the generated map: layers, dependencies, + boundaries. +- [Working on the architecture](working-on-the-architecture) — how to declare a + layer, and what CI will hold you to. + +## Simulation internals + +Detail that the generated pages do not carry — binary formats, message +protocols, and the costs behind each design choice. + +- [Memory model](doc:simulation/memory-model) — which buffer lives on which thread, + and the frame format. +- [How a step is computed](doc:simulation/stepping) — the fixed stage order, and + determinism. +- [Compiling user code](doc:simulation/user-code) — the HIR pipeline, scenarios, and + what the sandbox does not protect against. +- [Worker protocol and backpressure](doc:simulation/protocol) — messages, and + the ack contract. +- [Monte Carlo experiments](doc:simulation/experiments) — bounded memory, and why + only aggregates reach the UI. + +## The packages at a glance + +`@hashintel/petrinaut-core` is the headless half: the document model, the HIR +compiler for user-authored code, the simulation runtimes, and the language +server. It has no React, no DOM and no Monaco, which is what lets it run +unchanged in Node and inside workers. + +`@hashintel/petrinaut` is the editor built on top. It splits into a React layer +that owns state and a UI layer that renders it — and the dependency only goes one +way, which is enforced rather than merely intended. + +Three further packages are not yet covered by the generated model: +`@hashintel/petrinaut-cli`, `@apps/petrinaut-website`, and `apps/petrinaut-opt` +(the Python Optuna service). +Adding them is a matter of declaring layers in their source; see +[Working on the architecture](working-on-the-architecture). diff --git a/libs/@local/petrinaut-arch-docs/content/simulation/experiments.mdx b/libs/@local/petrinaut-arch-docs/content/simulation/experiments.mdx new file mode 100644 index 00000000000..deb62aa9622 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/simulation/experiments.mdx @@ -0,0 +1,126 @@ +--- +title: Monte Carlo experiments +description: How many runs execute in bounded memory, and why only aggregates reach the UI. +sidebar_order: 10 +attachTo: core.simulation.monte-carlo +--- + +import { Lanes } from "@diagrams/lanes"; + +An experiment runs many independent simulations of the same net with bounded +memory, aggregates metrics inside the worker, and ships only small JSON metric +frames to the host. **Frame buffers never cross the thread boundary.** + +The engine itself is shared with the interactive path — the same enablement, +lambda, kernel and dynamics code — adapted to different frame storage. + +## The layers + +The host holds one experiment handle per experiment, subscribed to its status, +progress and metric stores. The handle runs either in worker mode or in local +mode, where the simulator runs on the calling thread; local mode is what tests +and embedders use. + +Inside the worker, the loop is: advance every run one frame, post progress and +any pending metric frames, yield, repeat. Cancellation is checked between +batches, and the default batch is 4. + +The simulator owns N runs and advances them **round-robin**, one frame each per +call. That is deliberate: it stops long runs from starving short ones, so +progress reporting stays meaningful and a single pathological run cannot stall +the batch. + +## Per run + +Each run carries its own seed, parameter values and initial marking — defaults +come from the experiment config and are overridable per run, typically as +`base seed + index`. + +Stepping errors are **per run**: once the experiment is running, one run throwing +does not stop the others, which is what makes a large sweep survive a parameter +combination that fails mid-flight. A run completes on its own deadlock or on the +shared max time. + +Construction is not isolated. Every run state is built up front in the simulator +constructor, so an invalid marking or parameter set in any single run throws +before the experiment starts and none of the runs execute. + +Each run also reports buffer telemetry — bytes used, capacity, and reallocation +count — which is the first thing to look at when an experiment is slower than +expected. + +## Two buffers, swapped every step + + + +The consequence to internalise: **frame readers handed to metric code are only +valid during observation.** They are views over a buffer that the next step will +overwrite. Anything a metric needs later must be extracted into the value it +returns. + +If the next token count does not fit, only the target buffer reallocates, taking +the larger of what is required and double the current capacity. + +The Monte Carlo buffer layout is a leaner sibling of the interactive frame: the +same sections, **no 64-byte header**, one extra section for elapsed frames, and a +token region with spare capacity beyond what is used. Views are created once per +buffer rather than per read. + +## Metrics are computed where the data is + +```text +specs → compile before start → sample per frame → aggregate → metric frames +``` + +Metric specs are serializable and sent at init: either an expression (carrying +HIR artifacts) or a built-in such as mean place-token count or transition firing +count. + +Sampling visits every run's current frame through the same reader interface the +interactive path uses. Aggregation happens across runs — mean, sum, min, max, +last for a scalar — or keeps the run axis and bins it into a distribution. + +What reaches the host is therefore small JSON: a scalar frame carries a value +per frame and per time, and a distribution frame carries bins as value/frequency +pairs. A host can paint a bins-by-frames heatmap, or derive medians and +percentiles from the bins, **without ever holding simulation state**. + +## Contrast with the interactive path + +No acks, no backpressure: the worker free-runs to completion in small batches, +and the only upstream control is cancel. That is affordable precisely because the +payloads are aggregates rather than frames — there is no consumer pace to respect +because there is no unbounded data to hold back. + +## Known costs + +Runs are round-robin on one worker thread, though the run-state model was +designed so runs can shard across workers later. Buffer growth is naive doubling; +static analysis of arc weights could size buffers up front and remove +reallocations entirely. diff --git a/libs/@local/petrinaut-arch-docs/content/simulation/memory-model.mdx b/libs/@local/petrinaut-arch-docs/content/simulation/memory-model.mdx new file mode 100644 index 00000000000..c236ed3e511 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/simulation/memory-model.mdx @@ -0,0 +1,191 @@ +--- +title: Memory model +description: Where simulation state actually lives, and the binary frame format it lives in. +sidebar_order: 10 +attachTo: core.simulation +--- + +import { ByteMap } from "@diagrams/byte-map"; +import { Lanes } from "@diagrams/lanes"; + +Everything the simulation computes is stored in raw `ArrayBuffer`s read through +typed-array views. There is no object graph of tokens: tokens are packed structs, +decoded to JavaScript objects only at the read boundary. + +Knowing which buffer lives on which thread is the thing that makes the rest of +the simulation layers make sense. + +## Where each piece lives + + + +| Memory | Structure | Thread | Lifetime | +| ------------------- | -------------------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------ | +| SDCPN document | Plain JS objects (editor state) | Main | Structured-cloned into the worker at init. Later edits do not affect a running simulation. | +| Initial marking | JSON — count or `TokenRecord[]` per place | Main | Packed into frame 0 by `buildSimulation`. | +| `EngineFrame` | `ArrayBuffer` — 64-byte header plus typed sections and packed tokens | Worker | Immutable snapshot per step, appended to the instance's frame history. | +| Frame payload | `{ time, frame: ArrayBuffer }` | Worker → Main | Structured-clone copy per frame. Retained by the frame store for scrubbing. | +| Frame reader | Typed-array views over the stored buffer | Main | Zero-copy. Materialises `TokenRecord`s only when asked. | +| Monte Carlo buffers | 2 × `ArrayBuffer` per run | MC worker | Swapped each step. Never cross to the main thread. | +| Compiled user code | Versioned HIR buffer programs | Workers | Produced once at init. | + +The asymmetry between rows 4 and 6 is the whole difference between the two +execution paths — see [Two execution paths](doc:two-execution-paths). + +## The frame format + +One `ArrayBuffer` per frame, read through section-typed views. The frame stores +**no IDs and no time**: decoding requires the layout derived from the SDCPN +(place and transition order, per-place stride and slot layout), and time travels +alongside as payload metadata. + +`createEngineFrame()` computes every offset and is the only frame constructor; +`readEngineFrame()` recreates the same views for reading. + + + +Header fields worth knowing: `magic` (`0x5046524d`, a corrupt-frame guard), +`version` (currently `2`, asserted on decode), `placeCount`/`transitionCount` +(checked against the layout on read), and `byteLength` (checked on read). + +### Token packing + +Each coloured place owns a contiguous run of `count × strideBytes` starting at +its byte offset, giving O(1) access to any place. Uncoloured places have stride +0 and only a count. + +| Element type | Physical | Notes | +| ------------ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `real` | `f64` — 8 B | | +| `integer` | `f64` — 8 B, rounded | Exact within ±2^53. `i32` was rejected for silent wraparound; `i64` because bigint is contagious into user code. | +| `boolean` | `u8` — 1 B | Not bit-packed: that would break byte-granular copies for marginal savings. | +| `uuid` | `u64 × 2` — 16 B | Two little-endian lanes via a shared `BigUint64Array`, combined to one `bigint` at the boundary. Never routed through `number`. | +| `string` | `u64` pool reference — 8 B | The frame stores an ID into an append-only per-run intern pool. The pool lives on the instance, not the frame, so frames stay fixed-stride and byte-copyable. | + +Fields are ordered per colour by decreasing alignment, and the stride is rounded +up to 8 bytes. One module owns all token-byte indexing; every whole-token move is +a byte-range copy. + +Because the string pool never crosses the worker boundary with the frames, each +frame payload ships an append-only delta of new strings that the main-thread +store accumulates and hands to the reader for decoding. + +## Known cost: triple retention + +Each quick-simulation frame currently exists three times — in the worker's frame +history, as a structured-clone copy in the main-thread store, and transiently in +the `postMessage` queue. Frames are never transferred, and the worker's history +is only ever read at its latest entry. + +Retention is deliberately isolated behind the frame store, so this can change +without touching consumers. Transferring buffers instead of copying is not a drop-in +fix: computing the next frame re-reads the latest stored frame, so detaching the +posted buffer first requires stepping from a retained working copy. diff --git a/libs/@local/petrinaut-arch-docs/content/simulation/protocol.mdx b/libs/@local/petrinaut-arch-docs/content/simulation/protocol.mdx new file mode 100644 index 00000000000..56a342e4ff7 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/simulation/protocol.mdx @@ -0,0 +1,168 @@ +--- +title: Worker protocol and backpressure +description: The messages between host and simulation worker, and the ack contract that bounds memory. +sidebar_order: 10 +attachTo: core.simulation.worker +--- + +import { Sequence } from "@diagrams/sequence"; + +Both simulation workers speak a small typed message protocol over `postMessage`, +wrapped in a transport that queues messages until the worker boots. This page +covers the quick-simulation worker; the experiment worker is on +[Monte Carlo](doc:simulation/experiments). + +## Messages + +**Host → worker** + +| Type | Payload | Effect | +| ----------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | +| `init` | net, marking, parameters, seed, `dt`, `maxTime`, backpressure settings | Builds the simulation, packs frame 0, replies `frame(0)` then `ready`. Resets the ack cursor. | +| `start` | — | Starts the compute loop. No-op if running; refused after completion or error. | +| `pause` | — | Stops the loop, keeps all state, replies `paused`. | +| `stop` | — | Discards the simulation entirely. | +| `setBackpressure` | `maxFramesAhead`, `batchSize` | Live-updates loop tuning. | +| `ack` | `frameNumber` | Raises the ack cursor, unblocking computation. | + +**Worker → host** + +| Type | Payload | Effect | +| ------------------ | ----------------------------------------- | ---------------------------------------------------------------- | +| `ready` | initial frame count | Resolves the creation promise; status becomes Ready. | +| `frame` / `frames` | `{ time, frame }`, single or batch | Appended to the frame store, which republishes count and latest. | +| `paused` | frame number | Status becomes Paused. | +| `complete` | `"deadlock"` or `"maxTime"`, frame number | Status becomes Complete, emitted on the event stream. | +| `error` | message, offending item id if known | Status becomes Error; rejects creation if still initialising. | + +Errors carry the SDCPN item id where known, which is what lets the editor +highlight the transition whose lambda threw rather than showing a bare message. + +## One run, end to end + + + +## The ack contract + +**Nothing is computed until the consumer asks for it.** The worker's loop blocks +while the ack cursor is behind, so the consumer's pace bounds the worker's memory +and CPU: + +```js +while (isRunning) { + if (lastAckedFrame < 0 || currentFrame - lastAckedFrame >= maxFramesAhead) { + await delay(10); // wait for the consumer + continue; + } + // compute up to batchSize frames, stopping on complete or error + post("frames", batch); + await delay(0); // let pause / stop / ack messages in +} +``` + +Note the first condition: after `init`, the cursor starts below zero, so a fresh +worker computes **nothing** until the first ack. Starting a run is therefore two +steps, `start` then acks, and a host that starts but never acks looks hung while +behaving exactly as designed. + +Two details the sketch above smooths over, both worth knowing before relying on +the ordering: + +- The ahead limit is checked once per batch, not once per frame, so a batch can + run up to `batchSize - 1` frames past `maxFramesAhead`. It bounds memory in + aggregate rather than capping the cursor exactly. +- On the final iteration the worker posts `complete` before the batch holding the + last frames. A host that stores frames on `frames` and tears down on `complete` + will drop that final batch unless it drains after completing. + +The per-mode profiles the core exposes: + +| Play mode | `maxFramesAhead` | `batchSize` | Behaviour | +| --------------- | ---------------- | ----------- | ----------------------------------------------------------------------------------------- | +| view only | — | — | No profile: the React layer pauses the worker and never acks, so nothing new is computed. | +| compute buffer | 40 | 10 | Acks when playback is within ~0.5 s of the last computed frame. | +| compute max | 10000 | 500 | Acks on every arrival — compute as fast as possible. | +| worker defaults | 1000 | 1000 | Used when `init` omits the settings. | + +## Lifecycle + +```text +Initializing ──ready──▶ Ready ──run()──▶ Running ──pause()──▶ Paused + │ │ + └──── run() ◀────────┘ + Running ──deadlock | maxTime──▶ Complete + any ──error msg───────────▶ Error +``` + +That is the **main-thread** state. The worker mirrors a smaller internal status, +in which complete and error are terminal. + +A sharp edge: `reset()` returns the main-thread status to Ready, but the worker +has discarded its simulation, so a subsequent `run()` sends `start` to a worker +with nothing to run. What actually happens in the UI flow is a full re-init. +Treat reset as "throw this away", not "rewind". + +## Main-thread responsibilities + +The host-side runtime does four things worth knowing about: + +- **Snapshot preparation** — sanitizes the net for enabled extensions and + flattens component instances, so the main-thread layout matches what the worker + produces. A place-count mismatch throws on read rather than decoding garbage. +- **Transport** — queues outbound messages until the worker resolves; tests can + inject a pre-built transport instead of a real worker. +- **Retention** — the in-memory store keeps every frame so playback can scrub. + Alternative stores (latest-only, sliding window, persisted) can be swapped in + without changing consumers. +- **Publication** — status and frames as readable stores, plus an event stream. + +## Known costs + +The 10 ms poll while waiting for acks is a busy-wait; a promise per ack would be +exact. And frames could be posted with a transfer list to halve copying, but only +once stepping no longer re-reads the latest stored frame — see +[Memory model](doc:simulation/memory-model). diff --git a/libs/@local/petrinaut-arch-docs/content/simulation/stepping.mdx b/libs/@local/petrinaut-arch-docs/content/simulation/stepping.mdx new file mode 100644 index 00000000000..39debb4ecd7 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/simulation/stepping.mdx @@ -0,0 +1,76 @@ +--- +title: How a step is computed +description: The fixed order of one simulation step, and what makes a run reproducible. +sidebar_order: 10 +attachTo: core.simulation.engine +--- + +import { Pipeline } from "@diagrams/pipeline"; + +`buildSimulation(input)` turns an SDCPN into a runnable instance; +`computeNextFrame(simulation)` advances it by one frame. Each frame it returns is +immutable, and neither function emits events: the worker is what turns their +return values into protocol messages. + +Neither is pure. Stepping mutates the instance it is given, and the transition +path reuses kernel staging buffers between calls, so a `SimulationInstance` must +not be stepped concurrently or reused across runs. + +Building an instance flattens component instances, compiles user code, and packs +the initial marking into frame 0. Compilation is selective: lambdas only where +available, kernels only for coloured outputs, dynamics only for colours with at +least one real element. + +## One step, in order + +The order is fixed, and several surprising behaviours follow from it. + + + +Discrete derivatives are forced to zero in step 2; removals compact the buffer +and additions append in step 3. + +Dynamics run before transitions, so a transition sees token values already +advanced by this step's integration. Timers only advance on a step where nothing +fired, so they measure idle time rather than wall time. + +## Immutability, and what it costs + +Every stage that changes state allocates a new buffer: dynamics, token removal, +and token insertion each rebuild the frame. That makes history trivially correct +— previous frames are never mutated — at the cost of allocation churn per step. + +This is the single clearest optimisation opportunity in the engine. Monte Carlo already +demonstrates the alternative: two buffers swapped per step, no history. Bringing +that to the interactive path means moving history retention wholly onto the main +thread, which the frame store already isolates. + +## Determinism + +One seeded RNG is threaded through stochastic lambda sampling and distribution +draws, as a pure `nextRandom(state) → [value, nextState]` whose state lives on +the instance. + +The guarantee: the same `(SDCPN, marking, parameters, seed, dt)` always +reproduces the same frame sequence. This is what makes experiments comparable +across runs, and it is why the RNG state is threaded rather than global — a +global would couple runs sharing a worker. diff --git a/libs/@local/petrinaut-arch-docs/content/simulation/user-code.mdx b/libs/@local/petrinaut-arch-docs/content/simulation/user-code.mdx new file mode 100644 index 00000000000..284bea7fd1e --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/simulation/user-code.mdx @@ -0,0 +1,106 @@ +--- +title: Compiling user code +description: How authored TypeScript becomes buffer programs, and what the sandbox does and does not protect. +sidebar_order: 10 +attachTo: core.simulation.authoring +--- + +import { Pipeline } from "@diagrams/pipeline"; + +Dynamics, lambdas, kernels and metrics all compile through the HIR: user +TypeScript is lowered to a source-spanned IR, checked against the net, and +emitted as buffer-native JavaScript with offsets and strides baked in. Scenario +code takes a separate, simpler path. + + + +The editor's type checking is the same pipeline seen from the other end: the LSP +generates the `Color_*`, `Parameters`, `Dynamics` and kernel declarations Monaco +checks against, so editor diagnostics and runtime artifacts come from the same +HIR checks and emitters. + +## What compiles where + +| Surface | Authored as | Executed in | +| ----------------- | -------------------------------------------- | -------------------------------------------------- | +| Transition lambda | module, `Lambda(fn)` | Worker, per enablement check | +| Transition kernel | module, `TransitionKernel(fn)` | Worker, per firing | +| Dynamics | module, `Dynamics(fn)` | Worker, per step per place | +| Metric | plain function body | Main thread (timeline) and MC worker (experiments) | +| Scenario | per-place expressions, or one code-mode body | Main thread, before the run starts | +| Place visualizer | module, `Visualization(fn)` returning JSX | Main thread, at render | + +The visualizer is the one inherently React-based surface — it authors JSX — which +is why it compiles in the UI package rather than in the headless core. + +Metrics must return a finite number; NaN and infinity throw rather than +propagating into a chart. + +## Scenarios produce a marking, not a run + +Scenario compilation is separate from the HIR pipeline and produces an initial +marking (plus parameter overrides) before the simulation is created. + +Two modes, with one asymmetry worth remembering: **per-place mode is keyed by +place ID, code mode is keyed by place name.** In code mode, unknown names are +silently dropped. + +In per-place mode an uncoloured place takes an expression evaluated with +parameters in scope, rounded and clamped at zero; a coloured place takes row +arrays in colour-element order, each coerced with typed defaults for missing +columns, and extra columns throw. + +## What the sandbox is for + +Scenario expressions run with a list of globals shadowed — `window`, `document`, +`globalThis`, `fetch`, `importScripts`, `Function`, timers and similar — so those +name lookups resolve to `undefined`. Because shadowing only stops identifier +lookup, the sandbox additionally blocks the constructor-chain escape +(`({}).constructor.constructor` reaching `Function`) for the duration of the +call, by swapping the `.constructor` descriptors on the built-in prototypes and +restoring them afterwards. + +**Treat this as robustness hardening, not isolation.** User code executes with +`new Function` in the same realm as the host. It protects against typos and +accidental API use, not against an attacker. + +## Distributions are sampled once + +Kernels can construct distribution values rather than numbers. They defer +sampling through the ABI sink, and the engine samples each value once per token +with the seeded RNG. That is what lets chained `.map(fn)` transforms share a +single draw and keeps runs reproducible. + +When stochasticity is disabled the runtime is not injected at all, and plain +values are required. + +## Compatibility + +HIR artifacts carry a version and a fingerprint of their sanitized compilation +input. Any schema, code, or extension change requires recompilation before the +engine will run them — a stale artifact is refused rather than silently +mismatched against a changed net. diff --git a/libs/@local/petrinaut-arch-docs/content/two-execution-paths.mdx b/libs/@local/petrinaut-arch-docs/content/two-execution-paths.mdx new file mode 100644 index 00000000000..455b491dd71 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/two-execution-paths.mdx @@ -0,0 +1,52 @@ +--- +title: Two execution paths +description: Why Quick Simulation and Experiments share an engine but not a memory model. +sidebar_order: 1 +--- + +There are two independent ways to execute a net, and almost every surprising +thing about the simulation layers follows from the difference between them. + +**Quick Simulation** runs one interactive simulation and keeps _every frame_, so +the user can scrub backwards and forwards through the run. Frames accumulate in +a store on the main thread. + +**Experiments** run many Monte Carlo simulations with _bounded memory_ — two +reusable buffers per run — and ship only metric aggregates back to the host. +Frame buffers never leave the worker. + +## Why not one path + +The obvious simplification is to make experiments reuse the interactive +simulator and throw away frames afterwards. It does not work: an experiment is +hundreds or thousands of runs, and retaining frames even transiently makes peak +memory scale with run length times concurrency. Bounding memory is not an +optimisation on the experiment path, it is the reason that path exists. + +The consequence to keep in mind: **anything you add to the frame format costs +memory on the interactive path and nothing on the experiment path, while +anything you add to metric aggregation costs on both.** A per-frame field is +cheap to add and expensive to keep; an aggregate is the reverse. + +## What this means when changing code + +The engine is shared, so a change to stepping or to the frame layout affects both +paths. The _controllers_ are not shared — the interactive path goes through the +simulation controller with its frame store, the experiment path through the Monte +Carlo runtime — so a change to run lifecycle usually needs doing twice, and +forgetting one is the classic bug here. + +If you are touching either, read the generated pages for +[the engine](layer:core.simulation.engine), +[the simulation controller](layer:core.simulation.runtime) and +[the Monte Carlo runtime](layer:core.simulation.monte-carlo) — their +declared invariants say which properties you must not break. + +## Actual mode is a third source, but not a third path + +Actual mode renders an execution that came from somewhere else entirely — a live +external system rather than a simulation. It does not run the engine at all. It +reaches the same canvas and timeline because both read frames through the +[execution-frame interface](layer:react.execution-frame), which abstracts +over "where frames come from". That indirection is why adding Actual mode did not require +touching the canvas. diff --git a/libs/@local/petrinaut-arch-docs/content/working-on-the-architecture.mdx b/libs/@local/petrinaut-arch-docs/content/working-on-the-architecture.mdx new file mode 100644 index 00000000000..ef295fa9507 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/working-on-the-architecture.mdx @@ -0,0 +1,120 @@ +--- +title: Working on the architecture +description: How to declare a layer, refine one, and what CI holds you to. +sidebar_order: 2 +--- + +The generated pages come from annotations in the Petrinaut source. This page is +the short version of how to change them; the full reference lives in +[`libs/@local/petrinaut-arch-docs/README.md`](https://github.com/hashintel/hash/blob/main/libs/@local/petrinaut-arch-docs/README.md). + +## Declaring a layer + +Add frontmatter to the folder's `README.md`: + +```yaml +--- +layer: core.simulation.monte-carlo +role: Runs many simulations with bounded frame memory +--- +``` + +The prose below the frontmatter becomes the layer's page body, so an existing +folder README needs only the frontmatter added. + +For a folder with a barrel entry file and no README, annotate the entry file +instead: + +```ts +/** + * @layerRoot core.simulation.monte-carlo + * @role Runs many simulations with bounded frame memory + */ +``` + +Use one mechanism or the other, never both on the same folder. `layer` and +`role` are the only frontmatter keys, and `@layerRoot` and `@role` the only +tags; anything else is either an error or ignored. + +## You do not need to annotate every file + +A file with no annotation belongs to the nearest ancestor folder that declares a +layer. Thirty-seven declarations cover four hundred and thirteen files. Declare a layer where +the architecture genuinely changes — a new boundary, a different responsibility — +not on every directory that exists. + +## Moving or renaming a folder + +Nothing to update, provided the folder keeps its declaration with it. That is the +main practical gain over the old path-matching script, where a move silently +re-bucketed files into a fallback group. + +Renaming a _layer id_ is a real change: it moves the layer's page and invalidates +inbound links, and every descendant id has to move with it. + +## Adding a new package + +Add it to `packages` in +[`architecture.config.ts`](https://github.com/hashintel/hash/blob/main/libs/@local/petrinaut-arch-docs/architecture.config.ts), +declare a root layer in its source, and rebuild. Build configuration outside the +package's source directory is deliberately excluded — it configures the build, it +is not part of the design. + +## What CI will hold you to + +Run the check before pushing: + +```bash +yarn workspace @local/petrinaut-arch-docs lint:arch-docs +``` + +It fails when a source file no layer covers appears, when a layer id implies an +ancestor nothing declares, when a dependency violates a declared rule, and when +the import graph reaches a source file no layer claims. + +The bundle itself is build output and is git-ignored, so there is nothing to +regenerate before pushing. Only the annotations are versioned. To read the docs +locally: + +```bash +yarn workspace @local/petrinaut-arch-docs doc:architecture +``` + +## Adding a hand-written guide + +Authored pages live in `libs/@local/petrinaut-arch-docs/content/` and are +optional. To have one sit _inside_ the architecture tree rather than beside it, +name the layer it explains: + +```yaml +--- +title: Memory model +attachTo: core.simulation +sidebar_order: 10 +--- +``` + +The page then appears beneath that layer, and the layer's page links to it under +"Guides". + +Because `attachTo` decides where the page ends up, link to other pages by name +rather than by path — `[text](layer:core.simulation.engine)` for a generated +page, `[text](doc:simulation/memory-model)` for another authored one. A target +that does not resolve fails the check. + +## Declaring a rule + +Rules live in `architecture.config.ts` and are checked against the real import +graph: + +```ts +{ + from: "react", + to: "ui", + reason: "state providers must not depend on the components that render them", +} +``` + +Add one only when the code already satisfies it — a rule is a statement of fact +that CI keeps true, not an aspiration. Check first with a build; if there are +existing violations, either fix them in the same change or leave the rule out. From 0fb8433fa74eb9858db23fcebb59c26d45638e7a Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Wed, 12 Aug 2026 02:44:50 +0200 Subject: [PATCH 2/3] Add a Starlight site that renders the docs bundle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bundle is meant to be portable — the same artefact rendering locally, in hash.dev, or read as plain text by an agent. Nothing proved that. A generator with exactly one consumer, and no consumer at all in this case, drifts toward whatever that consumer happens to need. This is that consumer, and it deliberately owns no content. Everything it serves is synced from `bundle/`, and `src/content/` is git-ignored, so anything it renders had to survive the trip through a bundle a different host could equally well read. When something cannot be expressed portably, the site is where that shows up as a failure rather than as a quiet coupling. `sync:bundle` only copies. It used to spawn the generator itself with `spawnSync`, which put a cross-package build step inside a shell call Turborepo could not see, order or report on. The whole chain now resolves in the task graph: build / lint:tsc / dev → sync:bundle → @local/petrinaut-arch-docs#doc:architecture Running the sync without a bundle exits 1 naming the Turborepo command to use, rather than failing partway through a copy. `trailingSlash: "never"` is required, not stylistic: links between generated pages are relative and assume slugs map to URLs without a trailing slash. A host serving `/architecture/core/simulation/` has to rewrite them, and `manifest.json` carries every slug for exactly that. This is the largest part of the change by line count and almost all of it is `yarn.lock` — Astro, Starlight and sharp bring 311 packages. It is separated from the generator for that reason: none of it needs reviewing to judge whether the architecture model is right, and none of it ships to a URL yet. Where the site is deployed is still open, and worth settling against FE-1157 so there is one canonical location. --- AGENTS.md | 2 +- apps/petrinaut-docs/.gitignore | 11 + apps/petrinaut-docs/LICENSE-APACHE.md | 189 + apps/petrinaut-docs/LICENSE-MIT.md | 21 + apps/petrinaut-docs/LICENSE.md | 3 + apps/petrinaut-docs/README.md | 66 + apps/petrinaut-docs/astro.config.mjs | 165 + apps/petrinaut-docs/package.json | 34 + apps/petrinaut-docs/public/favicon.ico | Bin 0 -> 11553 bytes apps/petrinaut-docs/scripts/sync-bundle.mjs | 65 + .../src/assets/petrinaut-helmet.png | Bin 0 -> 7064 bytes apps/petrinaut-docs/src/content.config.mjs | 18 + apps/petrinaut-docs/tsconfig.json | 8 + apps/petrinaut-docs/turbo.json | 30 + yarn.config.cjs | 3 + yarn.lock | 3031 ++++++++++++++++- 16 files changed, 3470 insertions(+), 176 deletions(-) create mode 100644 apps/petrinaut-docs/.gitignore create mode 100644 apps/petrinaut-docs/LICENSE-APACHE.md create mode 100644 apps/petrinaut-docs/LICENSE-MIT.md create mode 100644 apps/petrinaut-docs/LICENSE.md create mode 100644 apps/petrinaut-docs/README.md create mode 100644 apps/petrinaut-docs/astro.config.mjs create mode 100644 apps/petrinaut-docs/package.json create mode 100644 apps/petrinaut-docs/public/favicon.ico create mode 100644 apps/petrinaut-docs/scripts/sync-bundle.mjs create mode 100644 apps/petrinaut-docs/src/assets/petrinaut-helmet.png create mode 100644 apps/petrinaut-docs/src/content.config.mjs create mode 100644 apps/petrinaut-docs/tsconfig.json create mode 100644 apps/petrinaut-docs/turbo.json diff --git a/AGENTS.md b/AGENTS.md index a8221a79432..f29330a7e70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -125,7 +125,7 @@ When you change structure in `libs/@hashintel/petrinaut-core` or `libs/@hashinte Verify with `yarn workspace @local/petrinaut-arch-docs lint:arch-docs`, which fails on unannotated files, undeclared ancestors and rule violations. To read the docs, `turbo run doc:architecture --filter @local/petrinaut-arch-docs` writes the bundle to `libs/@local/petrinaut-arch-docs/bundle/` (git-ignored); open `bundle/architecture.md` for the entire model in one file. -Full reference: `libs/@local/petrinaut-arch-docs/README.md`. +Full reference: `libs/@local/petrinaut-arch-docs/README.md`. Browse the docs with `turbo run dev --filter @apps/petrinaut-docs`, which regenerates the bundle first — run it through Turborepo, since the package script alone skips that. Hand-written MDX in `libs/@local/petrinaut-arch-docs/content/` carries the reasoning an import graph cannot express — why a boundary sits where it does, what the alternatives were. It is optional; the system works with that directory absent. diff --git a/apps/petrinaut-docs/.gitignore b/apps/petrinaut-docs/.gitignore new file mode 100644 index 00000000000..3f53ae7899a --- /dev/null +++ b/apps/petrinaut-docs/.gitignore @@ -0,0 +1,11 @@ +# Copied from the architecture bundle by scripts/sync-bundle.mjs. The bundle in +# libs/@local/petrinaut-arch-docs/bundle is the committed artefact; this is a +# build input derived from it. +src/content/docs/ +src/content/diagrams/ +src/content/components/ +public/architecture.md +public/architecture.json + +dist/ +.astro/ diff --git a/apps/petrinaut-docs/LICENSE-APACHE.md b/apps/petrinaut-docs/LICENSE-APACHE.md new file mode 100644 index 00000000000..4b43328a923 --- /dev/null +++ b/apps/petrinaut-docs/LICENSE-APACHE.md @@ -0,0 +1,189 @@ +# Apache License + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +- **(a)** You must give any other recipients of the Work or Derivative Works a copy of + this License; and +- **(b)** You must cause any modified files to carry prominent notices stating that You + changed the files; and +- **(c)** You must retain, in the Source form of any Derivative Works that You distribute, + all copyright, patent, trademark, and attribution notices from the Source form + of the Work, excluding those notices that do not pertain to any part of the + Derivative Works; and +- **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any + Derivative Works that You distribute must include a readable copy of the + attribution notices contained within such NOTICE file, excluding those notices + that do not pertain to any part of the Derivative Works, in at least one of the + following places: within a NOTICE text file distributed as part of the + Derivative Works; within the Source form or documentation, if provided along + with the Derivative Works; or, within a display generated by the Derivative + Works, if and wherever such third-party notices normally appear. The contents of + the NOTICE file are for informational purposes only and do not modify the + License. You may add Your own attribution notices within Derivative Works that + You distribute, alongside or as an addendum to the NOTICE text from the Work, + provided that such additional attribution notices cannot be construed as + modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ + +### APPENDIX: Apply the Apache License to a specific file + +To apply the Apache License to an individual file, attach the following notice. +The text should be enclosed in the appropriate comment syntax for the file +format. + + Copyright © 2025–, HASH + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/apps/petrinaut-docs/LICENSE-MIT.md b/apps/petrinaut-docs/LICENSE-MIT.md new file mode 100644 index 00000000000..d85585ee20d --- /dev/null +++ b/apps/petrinaut-docs/LICENSE-MIT.md @@ -0,0 +1,21 @@ +# MIT License + +Copyright © 2025–, HASH + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/petrinaut-docs/LICENSE.md b/apps/petrinaut-docs/LICENSE.md new file mode 100644 index 00000000000..6dad94c0e5a --- /dev/null +++ b/apps/petrinaut-docs/LICENSE.md @@ -0,0 +1,3 @@ +# License + +Licensed under either of the [Apache License, Version 2.0](LICENSE-APACHE.md) or [MIT license](LICENSE-MIT.md) at your option. diff --git a/apps/petrinaut-docs/README.md b/apps/petrinaut-docs/README.md new file mode 100644 index 00000000000..be89710cf02 --- /dev/null +++ b/apps/petrinaut-docs/README.md @@ -0,0 +1,66 @@ +# `@apps/petrinaut-docs` + +Renders the Petrinaut architecture bundle as a browsable [Starlight](https://starlight.astro.build) +site. + +**Run it through Turborepo**, which is what knows the site needs a freshly +generated bundle: + +```sh +turbo run dev --filter @apps/petrinaut-docs # browse at http://localhost:4321 +turbo run build --filter @apps/petrinaut-docs # static output in dist/ +``` + +`yarn workspace @apps/petrinaut-docs dev` runs the package script directly and +skips the task graph, so it renders whatever was last synced. + +## This app owns no content + +Every page comes from the bundle generated by `@local/petrinaut-arch-docs`. +`scripts/sync-bundle.mjs` copies that bundle into `src/content/`; generating it +is the generator's own `doc:architecture` task, which `sync:bundle` declares as a +dependency. The bundle is build output and is not committed. + +`build`, `lint:tsc` and `dev` all depend on one shared `sync:bundle` rather than +each running it — they used to call it themselves, which raced when Turborepo ran +them concurrently on the same directories. + +That separation is deliberate. The bundle has to render in a host that did not +generate it — hash.dev is the other one — so this site is a test of the bundle's +portability as much as a way to read it. Anything that only works here is a bug +in the bundle. + +To change what the docs say, change the annotations in the Petrinaut source or +the authored pages in `libs/@local/petrinaut-arch-docs/content/`. Anything run +through Turborepo picks the change up; to regenerate the bundle on its own: + +```sh +turbo run doc:architecture --filter @local/petrinaut-arch-docs +``` + +## Why the bundle is copied rather than read in place + +Pointing Astro's `glob` loader at the bundle directory works for `astro build`, +but not for `astro dev`: in dev, Astro resolves an MDX page's relative image +paths against the project root, so `../diagrams/core.svg` cannot be found. +Copying sidesteps that, and matches what an embedding host does anyway. + +The copied directories (`src/content/docs`, `src/content/diagrams`) are +gitignored, as is the bundle they come from. Nothing generated is versioned. + +## Notes on configuration + +Authored pages are optional. When `content/` in the generator package is empty +or absent, there is no authored home page, so `/` redirects to the generated +architecture overview instead. + +`trailingSlash: "never"` and `build.format: "file"` are load-bearing, not +stylistic. The bundle's inter-page links are relative and assume a page's slug +maps to a URL with no trailing slash; serving `/architecture/core/` instead would +resolve those links one level too deep. + +`installConfig.hoistingLimits` nests this app's dependencies rather than hoisting +them. Astro's generated prerender entry resolves `cookie` from this app's build +output, which would otherwise reach the root-hoisted `cookie@0.7.2` that +`express` pins and fail on a missing `parseCookie` export. Nesting keeps Astro on +its own `cookie@2.x` without changing hoisting for the rest of the monorepo. diff --git a/apps/petrinaut-docs/astro.config.mjs b/apps/petrinaut-docs/astro.config.mjs new file mode 100644 index 00000000000..b7146795c99 --- /dev/null +++ b/apps/petrinaut-docs/astro.config.mjs @@ -0,0 +1,165 @@ +// @ts-check +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +import react from "@astrojs/react"; +import starlight from "@astrojs/starlight"; +import { defineConfig } from "astro/config"; + +/** + * Renders the architecture bundle produced by `@local/petrinaut-arch-docs`. + * + * This site owns no content. That is the point: the bundle has to render in a + * host that did not generate it, and this site is the first of two such hosts + * (hash.dev being the other). Anything that only works here is a bug in the + * bundle's portability, which is why the sidebar below is built from + * `manifest.json` rather than from Starlight-specific frontmatter. + */ + +const manifestPath = fileURLToPath( + new URL( + "../../libs/@local/petrinaut-arch-docs/bundle/manifest.json", + import.meta.url, + ), +); + +/** @type {import("@local/petrinaut-arch-docs").BundleManifest} */ +const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + +/** + * @typedef {{ label: string, link: string }} SidebarLink + * @typedef {{ label: string, collapsed?: boolean, items: SidebarItem[] }} SidebarGroup + * @typedef {SidebarLink | SidebarGroup} SidebarItem + */ + +/** + * Builds Starlight's nested sidebar from the manifest. + * + * Nesting follows the *slug*, not whether a page was generated. An authored + * guide that attached itself to a layer has a slug beneath that layer, so it + * nests with the generated reference for the same code — which is the whole + * point of attaching it. Ordering within a level uses the manifest's `order`, + * so guides (low numbers) sort ahead of sub-layers (1000+). + */ +const buildSidebar = () => { + const pages = [...manifest.pages].sort( + (left, right) => + left.order - right.order || left.slug.localeCompare(right.slug), + ); + + /** + * @param {string} slug + * @returns {string} + */ + const parentOf = (slug) => + slug.includes("/") ? slug.slice(0, slug.lastIndexOf("/")) : ""; + + const hasChildren = new Set( + pages.map((page) => parentOf(page.slug)).filter((slug) => slug !== ""), + ); + + /** + * Recursive, so the return type is annotated explicitly — TypeScript cannot + * infer it from a function that references itself. + * + * @param {string} parentSlug + * @returns {SidebarItem[]} + */ + const itemsUnder = (parentSlug) => + pages + .filter((page) => parentOf(page.slug) === parentSlug) + .map((page) => + hasChildren.has(page.slug) + ? { + label: page.title, + collapsed: true, + items: [ + { label: `${page.title} overview`, link: `/${page.slug}` }, + ...itemsUnder(page.slug), + ], + } + : { label: page.title, link: `/${page.slug}` }, + ); + + const architectureRoot = pages.find((page) => page.slug === "architecture"); + const narrative = pages.filter( + (page) => !page.slug.startsWith("architecture") && !page.slug.includes("/"), + ); + + return [ + ...narrative.map((page) => ({ + label: page.title, + link: page.slug === "index" ? "/" : `/${page.slug}`, + })), + ...(architectureRoot + ? [ + { + label: "Architecture", + collapsed: false, + items: [ + { label: "Overview", link: `/${architectureRoot.slug}` }, + ...itemsUnder(architectureRoot.slug), + ], + }, + ] + : []), + ]; +}; + +/** + * Authored pages are optional, so the site must have a root page without them. + * + * An authored `index` page becomes `/`. With no authored content at all, `/` + * redirects to the generated overview instead, which keeps `content/` a genuine + * opt-in rather than something the site quietly depends on. + */ +const hasAuthoredIndex = manifest.pages.some( + (page) => page.kind === "authored" && page.slug === "index", +); + +export default defineConfig({ + site: "https://petrinaut-docs.hash.dev", + + ...(hasAuthoredIndex ? {} : { redirects: { "/": "/architecture" } }), + + // The bundle's inter-page links are relative and assume slugs map to URLs + // without a trailing slash. `format: "file"` writes `views.html` rather than + // `views/index.html`, so there is no trailing-slash form of a URL for a reader + // to land on and resolve those links one level too deep. See the generator's + // README for the contract. + trailingSlash: "never", + build: { format: "file" }, + + integrations: [ + // Authored pages may import diagram components from the bundle; generated + // pages stay plain Markdown and never need this. + react(), + starlight({ + title: "Architecture Docs", + description: + "How the Petrinaut packages fit together — generated from annotations in the source.", + // The helmet carries the Petrinaut identity, so the title beside it names + // only what this site is. `replacesTitle: false` keeps both. + logo: { + src: "./src/assets/petrinaut-helmet.png", + alt: "Petrinaut", + replacesTitle: false, + }, + favicon: "/favicon.ico", + social: [ + { + icon: "github", + label: "GitHub", + href: "https://github.com/hashintel/hash/tree/main/libs/@hashintel/petrinaut", + }, + ], + sidebar: buildSidebar(), + // No llms.txt plugin: the bundle emits its own `architecture.md` and + // `architecture.json`, which `scripts/sync-bundle.mjs` copies into + // `public/`. Serving those keeps the site's machine-readable surface + // identical to what any other host of the bundle would serve, rather than + // a second, site-shaped copy that could disagree with it. + pagination: false, + }), + ], +}); diff --git a/apps/petrinaut-docs/package.json b/apps/petrinaut-docs/package.json new file mode 100644 index 00000000000..e4318ab78a5 --- /dev/null +++ b/apps/petrinaut-docs/package.json @@ -0,0 +1,34 @@ +{ + "name": "@apps/petrinaut-docs", + "version": "0.0.0-private", + "private": true, + "description": "Renders the Petrinaut architecture bundle as a browsable docs site", + "license": "(MIT OR Apache-2.0)", + "type": "module", + "scripts": { + "build": "astro build", + "dev": "astro dev", + "lint:tsc": "astro check --minimumSeverity error", + "preview": "astro preview", + "sync:bundle": "node scripts/sync-bundle.mjs" + }, + "dependencies": { + "@astrojs/react": "6.0.2", + "@astrojs/starlight": "0.41.5", + "astro": "7.1.6", + "react": "19.2.6", + "react-dom": "19.2.6", + "sharp": "0.35.3" + }, + "devDependencies": { + "@astrojs/check": "0.9.10", + "@local/petrinaut-arch-docs": "workspace:*", + "@types/node": "22.18.13", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "typescript": "5.9.3" + }, + "installConfig": { + "hoistingLimits": "dependencies" + } +} diff --git a/apps/petrinaut-docs/public/favicon.ico b/apps/petrinaut-docs/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..93949fb57615be8d20b84edd51b9fe814b96ef8c GIT binary patch literal 11553 zcmaKxQ+Ot@7OrdCw)xk#?J56G?WzA7Q@fqIo!Zva=G3-r+c|Z1UgMk7A;|2o*BP08-W<~}B6Z|&^0Q^_C5Q2eOql1A(s4B}K zBM>0`OGTEGl~n&1|Lp*V1_$%6C%yYJ{ukgKWp!P^zz{M2ZvzMWorwnq2H7GfDW>VM z3QX`-oCgsNb$4%p-x{I051XQv2y)6YrI^^URozc31(0D%vxlbXdcL4(XVEiSeFNmi zf=Lw^t$>F`#_oxu!4CC;JG=fgB+A4m0sRtO2henfxx*IQ`aZ4*fB9_Ibc*D3!oM`z z*xEuXXp|6Q{2r{--NV%2;$GYMTuJBKd+&QMlB!h)3s4Xk9?27fllo+}=ZNmG#%neG z81o}#f+dN~&PgBK=wkDeUx>S5xVSg-Z6_*u%_NO*0rSG2b^ci9+WUbg^R-=KaelsE z7^8;W=uFLKz79RbidUZ-48$tuGs8C)@H`p+gSj1mSX9i@Y52y-55siMd`7jD_W@nD zrnP)rEt*-6b>fPEAjc3of{s$n05c?G^ifT}6W|-8Tv~coo2m6wuwb-<#KYu4Z5Xrt zlO;H^OZwNCaKk>b>w)U8}k3OQpkh`H9!2%?AZSw0DRW zV(t*OF9yyWY1XzS!pM^JVt6q^9kL2uPL_kV%h#@ydPv0n-1)!me6S`T;}KaVO~6PV zHU$OSl0{tn<*>aao{9|?I6(cr8w7&zKQ9-GE=I`B{9&|424(C?W~i164BFl~4H89lsKkYj$K9FQ;$s>RKT=rF;W1-PXd+)5 z#nL^wE*___o9Y}bNK5_xwl3pJ7YHs%n;h=rGQBLD6;+>nH z$8`7@uD>Gr7yam(!1YLsz#}znm-es3ht@yx-#+h>#kJh0LLT!1K8+P>Jnx?vX%b)T za=JA1+^erw!bbhU9PLT3zaea9{5#lSa#G5Y)#65h|HB2y{wQ6+Q z@0(*|V+X(WRxP{je{6jmD!mmoHO0~e9<>pR6>$1Vrhx=}#>t4#Q zu^w8-$)Tg!)s&iYg3afJP8(Y+S65RM*!c>GtoYr4xaCK`dtpk zN}HPs%q_2yV|APv^S67gR9_0a85xD!fVo-KqH+}#ohR1va_>pPWpuq@WL&taJ&&)b z%0&8UmWrx)Uo(=USFXtdSxu>-#?&>@v=1UV|wtO@XCSvSXFK03rn4&4Nol%8vS zq*4)y1Pv8q6RQkYNsGIsUmF{?(v`IQW-hboV-&+(Qu?V zVit8BL{yXaV=gEBJu|-^+Ll7nfUvqu30qzJJtl4-(`hBG94B zp)SEP>|%h#a)f;VlU6g~;`q=@4P!}nCYT9*_iYCM_eXS}FTK*_6;$69?j}Nqet7LZ z#Lk6Gk*t@Vgpj||1+rnYd+!p@Z!Y$Tq5T^vz>Lz*yj^U(VWQCZLJmdhb0hLc;!2PD z+He6v!V!ZOOu_+l*!cwH;{u!T0mz@&FBCJF;J4x?I3M2+G=Q|Lt&TExOfgpaSkQZx*sSFX3F9cKFkdscX>k@D45mI z#ZI)Q;6oUM!>*YWTie2QQbB2$p^Ad!f8dPKR)VV&U_MhjEs}Z`mMf!5B<36kSk*j^ zoj7Z56xeTq~^6Px6dx{ny zUK+q5o+*Q+pdFvZvSe{jDhFn|W_+ygCK}6~`Asuc^gLqQ*yTDQ4+0AsoSA>?OYN%A zHAx`GofO+i5ijWnR>0F(yuo_3tud-@#KQ=h{%=^yI}9kVm}zF%^NlD{fSdq)tDa;N_RW~15e;E> zh4 zd%V#j)%VEM=<1fo;=@>EUAe|A3>J2!c3dxbi*aBai2e)N9+vGuLjHvtOVc#Z6b;pS zWe*Lr88US+Lk5Xlx!1hqRF!IUx{y1zAoApXLrf9&ZD5c>c@zm0`tA68y?F4vD$-&S zP7DUG5BccckcROzXBOjp7B4Ev5@|s}eX0_RP45Ar_4H;oT=f(qS{0cib|*QF3lfI2 zKrePZWEcl>FrM06K2=f~2Es2&XH96&V@xR>$K*Ou1L8F*kfX*X8ulR0v92wpcqVNq z1N(0WSeWv7Uy+TDjw1u8@k0lk^53j|uZiRyhI6swxK-CEzHr&dq-LKQXjzP8@-m`@ zJb|iH@D4C=N=RGPI~(vak%ULMvKkt~*sIBaVG z9h@~_T2i6PqxaVB8~r_cWeO>djc#>t=NdJYh<{9&RE-n=KKgJV)UUD^GA}=}@;6a4 zZ4C$)(H09V4i-$S+QL(}jFwR_`^$QePX{B1Z2 zwnPRaMeOI}P7E$Sz3JIXDCpD!*;zE%Kd+i0BZ9h2ztsC!q_`xHZSrPqnA#rG>Sci; zz0TcDid}IyE9dCv7z8;{zDM>E^Grl_t3nX*ake}7z{t5BzG+=jWRoSZSX~UQUPQ@@ z8Z!3!GK4){agGm4a1?4XY4D=bBZQ=2{w!p=XKf6G+ElGRCuHP8O4-Hgq-XJdNFet_fLjHICgC=Xx+XfAf#I5MT5{8lxS{1Z zcWNuVcWmL9NhJQU=pkYGYK+yX{a1LFAF5PXV0O;}555b^fH7`2%G#tlbdw;gyDsEJ z2rL<3_$*b@G`O|_a>DNDFI??Ff(JbnJ#@^2WPpkm^kkb&i$z|@AM;f! zk$(>uSqT{AGjcjNfsZ#QAO7R&lC=;jJ3hjGUJ|&TXf8`*7b^6}Pu6YXbkt;}9Ww9D zKvNAQ_C%V4U`$R@J25O~YjuJkT8_|5L_O7{x!~ds2BA_-hdS?TEQO zHepuMQ8y>Q{R-j4s;3kkY@O~5_!jh=dLT)OtU?fnp}cKcYbh|IiN+#!p+_Cz@}-f+ zIg@MVy`H`8C6b~eEp~HMtLSjRU{{0ozLjm+pZt-w#FWRV^MT?HxA{!Rl?nWDGw$Z& zueb;GH`CErLbB|K?F(xt-Ao0!cX>G`=$RZuHlV`rNt!5d2@J;cfo((w_im!G*18SG zo@q`rxt)D;GfCXPi8?5S+G@CN<%9k`wj-{);9|z1IWaja4my=}6BBMNODsPmEZ_Jl zJu*bZ($q;5UwO8CdJ7wRR|9dATo1c~L)3y8n#~=|y|4KfPdda84UIHKf-roIhg!1~ zZb9PtoBI|Dy=sL#4lUha8W~qC-BeyyvOhU`9DOemBFCrLZJ?LUirSYQ4AV25&=8qp z{GA81CSg7u67_(7^df*uJG?U)a;E9QcFRDJZD-K8VR1S?A;eb>W}}-J`a@m99#AXF zt_oegumMq4dgOT+f<_bN7bDg8tfqSnj@{=e#Ie-s<6ITV{axa2qru36@yzbm9t&+c zWGTTE56iwwaguh<7Kpb!o7!DPB+X_Z^orz|^9DTu25srGZ(PJ!RjpCK+4+WLaAAd> zm145=kcM2xMCwqK4n55Q5A1qWm)|w&#`F=IV-pGQkKh{%=rsGelKtR_fcbgK_p92)FjkYkLtlW4b8zyVVC9;51mfnP zECPE^;&8-pR7Mo-jP1&h)8_bCpsSf+wDl4K^7x*gyBmM}gII7w4{@aJi6 zHa>OnLlnSS`ijJ-yJJrJll0 zJB!G(wKQol+_{Cdse4o;cF3Th@!>b}6m%3*vzF8gD_il$J#Qi7B_H?CiZS?@5gmGd zTgFl=)-iZ*x^|sM}v){E&2?H{7wx|M=(s2TT8oDqw*Bz>+q^OV~g0zhH^b=D)Di zsP_++R{jWW?9&{Tl7&M-GQ>s(umu@?%Vaysbdkh@4Cjl&0~kq55`%+7#~0>O$x*1~ zn<2@%FMYxi9Zht69o9T)v`2Pr8X7!Sm;07amRX$pKVpFW+M#*An;0Kgya)_L-B6Gp zrAbcGtfXhITG!Rn>uT?4@X?U6{u?;Et6q|yPjMF=jr1b&Wq4LEC@2_w{`Ce8=)G9` z0-M|lxr$tbctGEh~=S9{O&KqJtErso^)YkI()ghXv$` zQrmZVE>O&~4p0UJdPsPEg>kCj;^u7PZ0K6zk|nS`D^lWmn4i7JeqI-!ScoYdVPz~G zVOi701upMAbyHeV-m1)eeDAcsgZ1-&{*lhpS~QpNC(8L&{!C4CMfQbMSY1vkrsdW& ztK~OjWragwXsEP}on3x$F^!MSYp_a8%=5sN@8Q#L5#QpJnh6Z2JD$3RRxU}hPt*yn zoDAXQ#jG-=NuG}zo*nR=FK~~B02Nlc+6X1zFHWv*gp-%#q?<0qOl@^)7KVzvKjr+d z!$SINFkKM@8NyE2>%7}jRX@8M(F%zyqJq2TIj4u*r8I9DvNok?R1G~k_9fC6BAO7ge1VkzA~z@qyMfBz!zQM9?7w{+O#8L&*IATLnOe67|@0P2i%evd&-ZY+J) zi92nBDR;0EH{C1YzG{X@m8Tl;&uwc7#vl>STg#>$_FqJ46Eq=)@M0N)Y;T0SXd)qR zsyLR5!=BYSn%K=m#w~=K3VK#D7i8$xd!HWs;{B1oxo;n-NG~aQ>uqi46~KD{#P6J_ z$z9KvXV~6kttu}WJq=xaFKK(FvDt>|a*Dg-%=*fwFR+2Kt+m9c zIsHIlV@I-Av7qq_#5BzuLFJoR>f{I&3G*s9*Y03nmgbA{-J!(qIpu4lI%dG1Hp}nl z7xUx9cr6#rK%o zdgq%qV#SjUY^m~1!iaNLV4GQ&Og1hXk}jpHbjUg~n}%|V``AxW=H*(0W& zDvV`~$Z|fOzkfU3^PFU4Ns@n!pay)cZC^>z{D6xd8HfqTa=MN}vVN{0>X7wSnUT}Q zN=}oOJGm-t$&Vcs=0;N?&lb)pD=rTWgb1|Wltuk*4#FVG3aZ<>WqCZ#Di)IW-l1Gq zwp_Moo8+bl{vk$jmrHagfqjnTYYa|hj!LE#4Rc-(ca(9ww&%`F8b&H}u#fn(26z2> z=MaDzr;z1E%RsE%mq6{FU8E)}j*_o_8=s~te)6J~q;ZciZ^IQ@Wut4N_}#JR%FR`%{elKRr3TD6CfDhV+_@rK;W<-JLsHe2^hQAXW!p}PeM z`@ZXlX(eMD3{5fud$+i;kdipVDlr&oYg7n;q;W2oF?54tGJA`U8CRJH zTE+RTOT&5f#ys>=ETRgeQL@w;hnp9~4;82}8-!Zyp!7UL01n}CK`6dTj1q-QxbSIWX}*&_q}72VcRzBhm^p*t(nAq2(J`%Nl)qpUXN!O z(6M(GfAmPGSSoJq>3t+oo8`=LPW{XR6@-PJpBdRIgivdGA>*0&D?6ky(>XLp9x*?B zdu~Q8#{;%&-cF!>+JM(I#UrMtGX>AGk-S;;eYR3@D7gUnWi2Y6V@xfxbaZnJUDR01 zbwSXF!etJs7y6|iH&g}cdVHlYmaM+AP_aa_$;gu$J1}NEw`ZBi>(_I(3A_A>=eg~v z@H5@XM*>Rnrjymu3HIwF-OrlqKxi!)iHYBab|v z?=L0HxBIF>{mT%i3@zMNu1FU ziPb$`D-Ev{oD0OsL})`e+q*?k!3QJmiSMRIrb+qB6>?dQptOt~ zN)g*cqjVFpb0KMs2j$Y7Fb*Bjbbi(6Vk(()H>802ok+@57je^t{QjoCHQy9H5g46X zPxnj03-j!4IK*>&VT3To?Jg3bctl>%22l%q^c}mI5hTrd|ZXzzO6G#6LvvjrMyE9h-Fu*1vl-Wjt1QGO4ZJ$7_Nm=qq7 zl=R~B^!!d!G_xeju}zs~3cyk#l^A-5KKI%;{m57Ga8!>jlaMzu#zO)X#i%9_Lwkxj zBv^8*njIF7E6T>;i)Nhk|H{DcZo89P=u}AJ^@9q+}dnlo;agunhOz3 zV5W%ef@CAztxG3Hy`JjJto55m85kM0+_DkIftMgmTNG&d=4|+)AATxM6o9(MdPzA? zWaW>PNQt;j3hw%71DbQP4`TZ1HZ5oie^h7br(Bz7xL0Yl+LPabc|jaTg@kaRMq*V} z#~>oy8m6<+D$0Ew0XjBFb>p^aBmU_#uI<^#7ACf z2ly?51{oy>l(-mb*CuFpF*|WOEKn4e0v}k)j+`w|05JLuz_g055+&?3zu%^^?Wt&F z;~q($HTP5)um(xx#Xu6s#EFkp7v-Xfz(;b#V-WDy#r%A`$@o|zZV5T5@EPt{vKlUP zF@7KotNLU`OL&fT5dTe2(F*oPdO$|E56$^{PF@9|PaDz)^p6?w(~=c5kviuCRaG}p zq#n4?MwNqq23bOQ8$Jj)*-oV*LMn@ABu;Oa6~bT_j~3)20R1*%%UqbPdJIcLdjGVg zQPsq8A<$f7r<2Ps5nS1M=iAStab9IA&%zoq)2T=q8VsCrL!9|m$9-@A;xDe^r)oU? zV#IeRg|3Q)1h_Am8z4=IBX5cQEymfgTJ|<*BSx`kCKZ3es77Iq>&4@9Upoe#{V(Gtcc$D7;`V^0RZJ72DUFgc5`p77fV z^CP7pGoPX3HHJ5L_d3L9eGf>*|I3S>64rl2igxBr5SWK=ltmiYcn3MR0a@IIqsU39 z=qH%AUVF72lEr7mVITTq8y|LxR~8>dFPq+pna&A9-e6ol^Jh4w>JHRAFF10v#`K`G zxo_q)K)j_6|4+MqftLxZ&&pnON)NP2QdmCSXKw^I1mhDhz5YRmOi&q0;WZi<6&s6~ zYkC%$Q{pgG-EJMOz$z`S#}*s1Z_@1mU>ib2i}r!Fz(lYZg+IQ6n$rO<7Q|LGrb@FD z$>s<0>LA(=uDgDMP4Rl*qv*uaJ3~Yoz#?S+q=3ItlWsMbKwh`T`X{S{G4<-X$dhr=Cz_nYHQE? zO1fBfC)G=L1vHd@X6xrzlSLKlSSY4wOBi8*g~=z$SYGNbg^>hlVqw3pW!T~&D zu|a(0mTrSbK_Ax_y{smhFu8O$vjN-?6&>%y+E=JO4)G3F{hJDALMF)gD=Hy-iYj(( zpD=k5o-I?1*KKDfy#9;`c0J@KbE7?81A)35)Qgj_iVnzZ6V`$;p)L>xE$o6%&=f75=>+{f&1_KCyohRlHic|86z+QyYw95R57`?U@E32PJ+=Ct> z14vv7l{YV%-+d54dr&N7tgofsgy1!EapSm`ZTHMnk;UHv63qOo;55(^=hu3s1*POdh`<3A%4Ts&A zs1QW&PxtXhB-;DAS}oSjb4Onetg3;@(QuvnngD~s*ppMe>-nW2po>ID1CR8~@?+_m zRm4=Pmxcl-t*RFU-^^x|-&L`Kl)|II1poFIl#D~yuhrJdK(HIR`2tVpB5UiWXa(%<7@479ia+WLetYiE>|73k{3F{=C%P{`in zHwPB1uQ(#(Q-C0u0yK;oh5SN|1Yv4YkXnuw7XmulHQymw1$#)d2B5 zCk`L@CR+Kd(82SIoGH>YqgU8~0&Vf5NEdCfQ2;jPVTp#ecYM=FlC7EuU+etgZ zR*bzl``s|>mf4npsEhky`o<96J}$UsSe8? zNNP>Pp703Zpwfkgq@z0((-n*2@#LRYoBf$58#J!TWn7RKvAL?}$Ufi8sUfs#UnJ8D zi5$Oc@iPI`rs=H-tJJJ?Vyj5XHj0z`eDH26GHLT-J@8bu@~O}b1cIpW5IZmBao*O0 z*4j9pt#`TaGi#$le;^H(ez4&k6UR|bRamtO)b!Y=lGiuG+o|I$!?7HHq8kZK-M#jV z;Kv*)*!pKHZ}e(blBseBW&UBiijU6CHvv=9@;xp)>y-o&;^^@omsiC25xvuC5RW&` z7eNX=pz)1@=|;J#b*W&2a2sz%$Reje;Za1qrH{HLw_a~akZ8e>UD#BDuX?~8hRcykz28aLK8Yz3t*GB)R4HZ zn+{8C6t-ACG0_$g63niQi+)&rcGSdf1`F% z4qX{vQ5*979;)V-QjUt7yQa3>X*gwHtB{E z9jrmO^CekEst&txGU~T?GOpQ;$3t8+#)Ga+m z4t~#)hU2~=v3|4#G#?q@9`V8(uGi*WM8zm8!p>WZ2~&+xxF@0f4cu%^BUd>XR#?uQ|^6gaZoDJO> z(ya8nPSD15e&)H2vhZc2=&Ikl&po4ljg)a}3+ZN6r%@SsiL@jBN^lFYbEqm2CGBv9 z!9uwYwO{*t$lRf5%Q-?wg*(2a>NGh3{}7DuDEpYB4Kb4j)K|Ge>}ZyOoM6mi1#N= z#^eIjVvjm_yrBYEr6t%?rZPECU)n#-h&<>}e-mUbrhYdoxVtB&I%|tu_At!y2co3Y z*XQzpAnYu8je7zE(i)m@xvwsw32~W@A0$i}ys8zj;rZ!QrX>x75q7IbcT!3SARhVU zOi91@)DLM#RRvCXQ1d##4=nrcq&f29Bu-KppZ?`kyg34%JrLQ&(Ut76neT&m;&Ebw31;w3iIBj)#0KS)!a=YCK>cC?N=GRx%?o0~rr4N+9@eUw8j*)}uI#(3XpuKTsu zHfo5~s~ z;1BF%|6v(Fd;X%Xh_4j9FRZ*go}-2=Dq(M$w<#z|c|E-{&AVAJNH3K0Y(m;+R8O> z0f@7E)2~07F-s_2PNaRn`_x5LP3)n~Q?GZ(NR<}2t|KfoFc%gV)RdICxaPGre1Xsl z-%@9XB2BE#b@N)TeN%X8&yXql0t-t}>_8uPfWxVp#b2d#^z`^6^o;I%OMCYMu0nIZ z!wp26-(;m(MFK`LSWO9@J{6|~IG5h?%yxDFBe!#E382*7A_0dI+0LsHOp*8edUQV5 z3n^jyfi4+U6XB2!7I954cteX+iYyg(XN0k`Z3$>!x|+hsQHJt;$i&rE0&59iOkr7_ z@_SiXw#t)jVGQy@X$5`B`Z<yWS6i|ovjdqE$94h zX5r0lKw4%VX7d(RsKMb*9XB-x>GOjAKxDXOWO;3E?B?v7Ep|&pWr7W9KZ@HNzd-cH z3Bc`U0lnT`GDhE%*Y*|VtDG7(sLPI*P&#{Q&+%D|;hn$lt+S zEpr*Bw~wt&UULn}UcaR#Z{y|S<71-bp`)X>ouw~!Vn?_OAABwWhyuE)s_x^Ryz2`pXx#v9R-k~@g;^&p(1px5dSX&(Xmude88vZY? zvQstyfCGxXqow_M|DDNM0}Z9gu@M#GF3+%s@r7?)T%6T4)C2F9>L0!&EhY1=g4NSL zFkI63yqu||rn6ku#r`r~kR-?cG~M_jQ+}V>&WY0LFW*I_m0}WdGM>G@b>;Hytg=`2 z%@OAf8jIY0gW_LBT)h(!-da@9V;cM+;u!)d3 z!TCRbUl{%Ja}Pog4%g@zpEf`V*L8hdoSn5x?M0wW1bM_BzZrDk6EpM8Y#y4l^Sx^q z`Nlq>^_X9}mH*@OQBUBQBi1*|y_4&Z{AY8ky5O=I=G2(b%o@0aE?hkFc1a&3=?|AT zPpJEx+suZn24Ap-WgtRGK__QtXQ@?ONKAHZSj{u$B@d}x-oQGZI=tjY$ zPgJN#vxCH>6%}CGPKNS$AEFri3fow_XB$?arXydD&}H$M5d`Af_Y!E z4oRq@%wVn^lE@P>8g_N%Z@4NwT-z}oOYbjEQXdWFUk}e6QSZ%qTp38l6`Q`K`Zk;4`%{Be8qgzqh|+ZDiXeoD<+ow{2u}THqqvST&o& z^<$O78|{8j>I8@!t_c>zctozNSEdQv<%Y41?)G^dURZNMdHH$B#-8o7Hfy7w!;XtZz8J~dt$%h~sE*7x8MtmYw&QC%d;GubBrF!~Y^+SpSFkwWY+}PtvtiAv_};i$i-q$?!ZlU~#zDe$mu>EaK$BIrr<_ z;H9v>B|X8)F}L~uD$Mg_UoI4AzHAN$nqA{#!@gP|_Zte+aoxwSv>rE{TN zn375Gnmk*(rVCyAt z9Eo{^{@tAcemR#t-q)!U^i&lF7@4P_P$nvd;U6K(YN)c3RvXR)rOtv{4wB0#zsdd) zhRgnBK_$4_T7!CZeO+dr`wNAr$VhWjNdDS&u1N3LTwS9Ygvh^SICi~s2@KlE2E`68 zCQY+Gm`h_f;}Gjz!uiR60~eTuU){uaHiK|@UKvg-=E^{5ZWtT^J64ntJQMJ@E#l;n zhekIzzj43iUM)3zS|TAduLW{;{-BiSrjW&A=3wm5&u5GLVH}-Xf2MxcHvOHTN22;Y zWLyKOeR1`HwSO(69RzYCe55|tytYtm(z&7 zWJc(Tm}uz(2euAwPD>1MB)<_`t5A7SAtx;A!a1LF#8ib?i^0`j^T+1t(CB4HfTL|F zHf;?FHj%ftfB#(my}{}iti<7QlLm6DJ7X)TGI$cTu&emkGbv5s8co4YD4EG;`||no zxi$kOZp+VKL|W#Vn;AMpQ@1Z0de#GrthpmTx--uJRb~K+{4kaYU+3MdCw{&4rO(cU z8micOYpDSRPy#aLX2pSZ6G78mFWL#`jQkF5oyNczC(8!45S`GztAQudfA$4sHp*|< z5jrtM0Tmp{l-{y&fIP4QeS9ea6^mRLZTu09OiW6K@4MDC+4fq*-NW_u%{%QG>MnoG z&;`Lk69JyVzwq1{a~8^Hj%!I*jTMf2amXDl~LPStb7w=a2MVa@mT zcwzYD`OOy#a~zlN`)22FAX_~;#%iC_ZJ93c-e9)~Mz?#zHP~4~YetyMIW!92Y=?y~ zFC;`qk!QT^;a(A6HcNuDwlPP;^IFSKnK_xGbMmjan&z5+ZRCz# zK#fRZku=U9bXKfDwu#h4`mK+%!U0j@h?+gWm zY4Nb)=ylMl8{Atx5^S8Vaws+M|5h(zsP$(tx_>ULdeUB zJ12RGgH%x&s<`j=_J}1RE4xTdAtw(1ILm|VQ&U0 z`c>tD!lMEJxz4(7=ZekhZHQOMHiAl>c{Z`s{lh56mlBMJp}Z5WT&p7gtH||R`K%7K z1xiMRf{HNX8W z=r|j$L_-x|RHsgpf?ue@#;ciSF)6ds+f;pVH8#T8n&$JNbnCCp!yOzwpHTjyE^D>O zzLS-8SScB$>LnWr2R)2e`ux`I1LqU0ydf0RGW(-zy1^zVw@BH+lZApQ#ws#A6;(9E zg%Gf6^7F{d#@Q}4zmR4tp5g+zp%dN>aerH}xn%am_u2BwbdH5a#LqtMlSnL)Nb8h1 z@~GUh+^0Xio66b`{A2!jb$8w_Ilp1yGw`G>xamip%H1XLMo#82Mpg%?!K3oNSTUH}Up{5bkLppOqh~ z%~QoVZ-+o(miPZ(vG9%3wDuvgi*o?;!|ozvfSC21hHYwG?5-$#y~Hb zs{CnEDW!dlVs9SpD^cW|L&qWu`Eaemrbr=8xWkx+#f}#h%q5w_DrxZi?bNf=vQyYz z$u>*}Bl!A(97R57XM@DZ-wHPbWOeH5Xw_ZCk-Kgn2U^?W#1R*Jk9vui7`xFy_kXSy z+*zYQ!hBzKAAf}$xjlRC<36yt%=t%UOwIPMB|LAS84dWrRiHdk5uW~9YG@A!85b!Ij6#Y42&>?{a zJZt=T7Yh?D`LrWzUUzBnLHUy4NAH97IqFbuJW1}Q)UNa0|2I6aav};HEyr^8h8!Uf z`XS)gR0Tt}S-(O-iiXe?d0eF2jLE7^dWf#RADuN6w`2({vIIc4cu&<3L6b_$0U-Jw zPs_Ojr{Yj-L~18Y6`&sR5(=@m-<%d$6OJKLV0fiza;X`DwCBX+Sq+CthkWC+#|?X52wYsEuJ^2y_~F9KPK3xqjcf=4aq=+Qw}!>9P{<0W9do~yFGdU2Z@2>pIC29bpO;UI zZUkCSJXPN9bz#M+FIoIZgJ0XQh=j%zu+Q5d;Qo0a?qwME1c?b9M9@;@$sv))@z)|p znn$|oLNgyfcG~q{`x+?!P=kfU)yk20&q5$eXoKbbMMAiWC%l8Jq3X&Gsf*m-TYnUgT5 zOIzRikEJ*#798k15|o5bPMTpK+_2p!V+f3j_{XYqkT`M3KK~R z)fU6PQb#KsJ0Q49YA@?L7F~UvEhY`zYsC&V?}>%|ifE~7mrQnTy_QkFBcy3nfvp@vEd(51rNmc9V7EH+2C z9$9$jwW${Zn$Z>>GT#vi)%o5w;_O5_VGT~7EXpY`*#e(vPIb$= zkK99<)mhE1P{{k`^M##3g`9zU+01k5-PNIlKtYx+LbmO+oj6&Hh(k#8kvr`7$43sFt1)!($KQxQ*|02~TvJ?=+m z!8KykiaZ6nx>>ZbdXBU`0eU6J-0EuiCi|I@mNuh#+%fH+#DpoF*XNk=4CpOFt#O~GHlMt!nXA8try0nAdz zqAy&?;<05+onvI#BBo~mOcQRgv-15JvaC@z;CMYLD#{|835m>!fLdU{*z@9tt|DDT z%~i#TD6HtSyDF=TAGexWI*nX7v=hNN2(z!^{J?oXzC?_LZ4?uEXxymYM3>2GTq;pn zMFf*<>QEkg3P_fpyuw_+jL64FA^51r1k7P0%0Ldqk+2QjAJcqVNC=qmz()M|vQSzD z+ww4M4$eQQLmr8k zk9C8O52<0%3`TRb%W2@nZmJVSKQHk@#LFQV+m=RT0#wHt)G2Ptn>|AMBR+Fv=KPYt z9Xk@7h*Cqp+Eg^!E#ycvO|(cRr0&yJYag%pzP9V1E%W)K18*yYoahY;6Hn)6grD}% zc~~9%7T|Po*>1)VzdUqX(Om#M=JE{tED}thk0u@;pXvWuS}|%Ik95 zf$2dZvYG^TyaN6yB5joiRDTVJIUVu4cba-M#u~?qs2MGS9*N_UX*c|0yMM<2DsIY{ z&GY;<&f_5(Qs-6@+=*tsR*p>%dE{q4TQfYG!*)L0i&P|Ci≈khwbV!-tAO>JHCm z&Nf?yT@?JKF{P*fBiBm_$Evxg=aa(5S#Tb02mk)+CO~lHo-50buzkcm-T(gTa8NMj z(ICmzl<1UU>SN#N?AMUApEmlkDrAra;B3eu%9Q%1y& z?)a;pT?52zueN-+*xTC!Qw+&Omv4Nv-7MI=y|UFWV|;V#N?7K}7r!T!gpe>-JVwb$ zn4C71uBdyZ&m3_s*@(TVeZwYVdr$Jex#G`eYFrOwC+KLCKm!nRy(}~(1esg&egx*Y zDH!PENHbQOPUz?9aYlUX8z(AeucR)jruX4V(oqAx*mxZ?96C*$qNuB{WRaCD#~paY z&-Iq;NHrMt{zeINlL?%EbM5@|B@dS-w>Q@>PqyO5EIL4;mUdP_8sdg9*~Vtun`+Ic z{Y2v_JxiO336b%Fo{UY+{QNnN;KGp7mgHZTKb4DNR~eVy zS%c`_&D90mDbj4wthEf-HB% zFE@m1)8VZN1dfN0P=1M|70WE!TqZ80RiEy*J5ItKNugBNA6<+OZj9Q0bpgg9g<@d+ zSC+pV)-Z0Wz%W-?v_DDOaBIRmQO(eBH>|NgI`s`DpBaZhS5iwJ=Oxo>568vn57+Du z=YEMPoH|Ct8FM{AU!OiN4(rQJI#i&DvL?XvdzLMo)k2f*Wr(NTJKUm!r=wR~>cfRP zu8I>BEB;a$ukxso=;hmofdgC0GBpK3cJU_F+cuXNk5d&VeGaT2GV36Rm1TTwbS%l9 zy<5L38fhG^00ypJn45T&``i3FMs^y_3%hrKWP`Fv~E zu@Inq9Vt3&=;QOC5Trx$qW8_rE)O>8S(i5D;+|4AGhU~O!gF@za{nhg_ zQ|KO^YWgWTI|dbkTJNo3BX8_8j;vUdI$CU*wsd~FNmtkookJIUge4F4C-jTP<)t^h z-#n22c4r**z{qCeKgMmHSR6C?0xw3S8z&SZpgC_r2I5!g+v5sO9Nu&e>u6p7cjHgh zLD#SB(4!I{;zycL(!qOi4(L5NV7`A=bj+p39|mNGKlqG_r` z>W*;&mm+cr_dKdaO42stPB00RCUnhH*Ex21R72ImUExz6D_;rA+|4GeX{P7VQCp9H PuL59W>0t4~jLP^Qx;=0.11.0" + peerDependenciesMeta: + prettier: + optional: true + prettier-plugin-astro: + optional: true + bin: + astro-ls: ./bin/nodeServer.js + checksum: 10c0/ea46894ba994d45deb8c86696587dbd2a99013c352eef066f505a1583cd24e050d3fb4af0db9828be89d597a42e895fcbbd0fd2cb33ea9127f9752405a093068 + languageName: node + linkType: hard + +"@astrojs/markdown-remark@npm:7.2.2": + version: 7.2.2 + resolution: "@astrojs/markdown-remark@npm:7.2.2" + dependencies: + "@astrojs/internal-helpers": "npm:0.10.2" + "@astrojs/prism": "npm:4.0.2" + github-slugger: "npm:^2.0.0" + hast-util-from-html: "npm:^2.0.3" + hast-util-to-text: "npm:^4.0.2" + mdast-util-definitions: "npm:^6.0.0" + rehype-raw: "npm:^7.0.0" + rehype-stringify: "npm:^10.0.1" + remark-gfm: "npm:^4.0.1" + remark-parse: "npm:^11.0.0" + remark-rehype: "npm:^11.1.2" + remark-smartypants: "npm:^3.0.2" + unified: "npm:^11.0.5" + unist-util-remove-position: "npm:^5.0.0" + unist-util-visit: "npm:^5.1.0" + unist-util-visit-parents: "npm:^6.0.2" + vfile: "npm:^6.0.3" + checksum: 10c0/acec0876cdb4d9900bffc72ab41ff5dfd9595308581b330896e0c6b942dc095c8ea150273471a0a70ba99091667a0a0d762137bab1a5154addfd32cd725027e2 + languageName: node + linkType: hard + +"@astrojs/markdown-satteri@npm:0.3.5, @astrojs/markdown-satteri@npm:^0.3.2": + version: 0.3.5 + resolution: "@astrojs/markdown-satteri@npm:0.3.5" + dependencies: + "@astrojs/internal-helpers": "npm:0.10.2" + "@astrojs/prism": "npm:4.0.2" + github-slugger: "npm:^2.0.0" + hast-util-from-html: "npm:^2.0.3" + satteri: "npm:^0.9.1" + checksum: 10c0/4d88de2c7b058c8ab0a1343e35cfec14f50f36c2c7a7f8024f679ef4edb5e42c8ca8c8828446aa9124aa1c2f5fb1e0ec837e7bf789cd775e7e61a2405a09bc24 + languageName: node + linkType: hard + +"@astrojs/mdx@npm:^7.0.0": + version: 7.0.5 + resolution: "@astrojs/mdx@npm:7.0.5" + dependencies: + "@astrojs/internal-helpers": "npm:0.10.2" + "@astrojs/markdown-remark": "npm:7.2.2" + "@mdx-js/mdx": "npm:^3.1.1" + acorn: "npm:^8.16.0" + es-module-lexer: "npm:^2.0.0" + estree-util-visit: "npm:^2.0.0" + hast-util-to-html: "npm:^9.0.5" + piccolore: "npm:^0.1.3" + rehype-raw: "npm:^7.0.0" + remark-gfm: "npm:^4.0.1" + remark-smartypants: "npm:^3.0.2" + source-map: "npm:^0.7.6" + unist-util-visit: "npm:^5.1.0" + vfile: "npm:^6.0.3" + peerDependencies: + "@astrojs/markdown-satteri": ^0.3.1 + astro: ^7.0.0 + peerDependenciesMeta: + "@astrojs/markdown-satteri": + optional: true + checksum: 10c0/e917ee98475b107488fa2bc08d3b60fa5acc771ab925725c0b72e8374fc6ed0287f4684013cf40b000771f3f3aa45bd42733c5c9ff00024fab5b245bf1ca958f + languageName: node + linkType: hard + +"@astrojs/prism@npm:4.0.2": + version: 4.0.2 + resolution: "@astrojs/prism@npm:4.0.2" + dependencies: + prismjs: "npm:^1.30.0" + checksum: 10c0/23a5bc096f966c08be6f8e89adfe9287fd6d08eeb9c4ec7de8172280aa638f2684dcefc5f848f55e211ff63369abc4f0661698daa5e2de6078c8f269215a7101 + languageName: node + linkType: hard + +"@astrojs/react@npm:6.0.2": + version: 6.0.2 + resolution: "@astrojs/react@npm:6.0.2" + dependencies: + "@astrojs/internal-helpers": "npm:0.10.2" + "@vitejs/plugin-react": "npm:^5.2.0" + devalue: "npm:^5.8.1" + ultrahtml: "npm:^1.6.0" + vite: "npm:^8.0.13" + peerDependencies: + "@types/react": ^17.0.50 || ^18.0.21 || ^19.0.0 + "@types/react-dom": ^17.0.17 || ^18.0.6 || ^19.0.0 + react: ^17.0.2 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.2 || ^18.0.0 || ^19.0.0 + checksum: 10c0/47e546a4a75e42738abe5cf3df0971297cd700fbd91649d3e9149a335f29e9be9dbc08ab04490d81bc57f56aa0c6f70d4dfa3241f1b00ece52d80f74b7858dd7 + languageName: node + linkType: hard + +"@astrojs/sitemap@npm:^3.7.2": + version: 3.7.3 + resolution: "@astrojs/sitemap@npm:3.7.3" + dependencies: + sitemap: "npm:^9.0.0" + stream-replace-string: "npm:^2.0.0" + zod: "npm:^4.3.6" + checksum: 10c0/99c0240586ea2ab1868903341a302ff4bd909b53c49c1c8be476921593fbee838623207c4fa88522264d79291f074ea536aef614d6b1fb286f29a1a5d244ec21 + languageName: node + linkType: hard + +"@astrojs/starlight@npm:0.41.5": + version: 0.41.5 + resolution: "@astrojs/starlight@npm:0.41.5" + dependencies: + "@astrojs/markdown-satteri": "npm:^0.3.2" + "@astrojs/mdx": "npm:^7.0.0" + "@astrojs/sitemap": "npm:^3.7.2" + "@pagefind/default-ui": "npm:^1.3.0" + "@types/hast": "npm:^3.0.4" + "@types/js-yaml": "npm:^4.0.9" + "@types/mdast": "npm:^4.0.4" + astro-expressive-code: "npm:^0.44.0" + bcp-47: "npm:^2.1.0" + hast-util-from-html: "npm:^2.0.3" + hast-util-select: "npm:^6.0.4" + hast-util-to-string: "npm:^3.0.1" + hastscript: "npm:^9.0.1" + i18next: "npm:^26.0.7" + js-yaml: "npm:^4.1.1" + klona: "npm:^2.0.6" + magic-string: "npm:^0.30.21" + mdast-util-directive: "npm:^3.1.0" + mdast-util-to-markdown: "npm:^2.1.2" + mdast-util-to-string: "npm:^4.0.0" + pagefind: "npm:^1.5.2" + rehype: "npm:^13.0.2" + rehype-format: "npm:^5.0.1" + remark-directive: "npm:^4.0.0" + satteri: "npm:^0.9.1" + ultrahtml: "npm:^1.6.0" + unified: "npm:^11.0.5" + unist-util-visit: "npm:^5.1.0" + vfile: "npm:^6.0.3" + peerDependencies: + "@astrojs/markdown-remark": ^7.2.0 + astro: ^7.0.2 + peerDependenciesMeta: + "@astrojs/markdown-remark": + optional: true + checksum: 10c0/cfb3d03c9ad05a2aad3b6180efc2bf093a73e36ae5ce4a4bdc0ae051a941d40a3e559f4958c31d1f47a8bcf2bb476ab087a3cc27e204a778c861680da63fd67d + languageName: node + linkType: hard + +"@astrojs/telemetry@npm:3.3.3": + version: 3.3.3 + resolution: "@astrojs/telemetry@npm:3.3.3" + dependencies: + ci-info: "npm:^4.4.0" + dset: "npm:^3.1.4" + is-docker: "npm:^4.0.0" + package-manager-detector: "npm:^1.6.0" + checksum: 10c0/d5373e0aa8cbfd684143027dce4e31f7fd45e90df799ce4d255fb750109173a3dd2fc3ef26532b298c715fe063d6e955984d238e323d96e1d8f6c0a03fbbbc73 + languageName: node + linkType: hard + +"@astrojs/yaml2ts@npm:^0.2.4": + version: 0.2.4 + resolution: "@astrojs/yaml2ts@npm:0.2.4" + dependencies: + yaml: "npm:^2.8.3" + checksum: 10c0/bb0a25ae730ea2b8ba20da4626c9d023ba27898396c2ad71384e0aae10c4479bb5d6e9f4f27aad1f486f1d287200c4753ff949ab16af367788f085b078d4b4e6 languageName: node linkType: hard @@ -4099,6 +4464,73 @@ __metadata: languageName: node linkType: hard +"@bruits/satteri-darwin-arm64@npm:0.9.5": + version: 0.9.5 + resolution: "@bruits/satteri-darwin-arm64@npm:0.9.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@bruits/satteri-darwin-x64@npm:0.9.5": + version: 0.9.5 + resolution: "@bruits/satteri-darwin-x64@npm:0.9.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@bruits/satteri-linux-arm64-gnu@npm:0.9.5": + version: 0.9.5 + resolution: "@bruits/satteri-linux-arm64-gnu@npm:0.9.5" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@bruits/satteri-linux-arm64-musl@npm:0.9.5": + version: 0.9.5 + resolution: "@bruits/satteri-linux-arm64-musl@npm:0.9.5" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@bruits/satteri-linux-x64-gnu@npm:0.9.5": + version: 0.9.5 + resolution: "@bruits/satteri-linux-x64-gnu@npm:0.9.5" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@bruits/satteri-linux-x64-musl@npm:0.9.5": + version: 0.9.5 + resolution: "@bruits/satteri-linux-x64-musl@npm:0.9.5" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@bruits/satteri-wasm32-wasi@npm:0.9.5": + version: 0.9.5 + resolution: "@bruits/satteri-wasm32-wasi@npm:0.9.5" + dependencies: + "@emnapi/core": "npm:1.11.1" + "@emnapi/runtime": "npm:1.11.1" + "@napi-rs/wasm-runtime": "npm:^1.1.6" + conditions: cpu=wasm32 + languageName: node + linkType: hard + +"@bruits/satteri-win32-arm64-msvc@npm:0.9.5": + version: 0.9.5 + resolution: "@bruits/satteri-win32-arm64-msvc@npm:0.9.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@bruits/satteri-win32-x64-msvc@npm:0.9.5": + version: 0.9.5 + resolution: "@bruits/satteri-win32-x64-msvc@npm:0.9.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@bufbuild/protobuf@npm:^2.6.2": version: 2.12.1 resolution: "@bufbuild/protobuf@npm:2.12.1" @@ -4106,6 +4538,15 @@ __metadata: languageName: node linkType: hard +"@capsizecss/unpack@npm:^4.0.0": + version: 4.0.1 + resolution: "@capsizecss/unpack@npm:4.0.1" + dependencies: + fontkitten: "npm:^1.0.3" + checksum: 10c0/72ce03871e137bae1c4925f9dc916a6250cf9b9630a8bb28d2ef7575d20fd7ef0e8966b361aaa3a54c0e8d5ff40b133490bd97a9beb6dc209529102947d392b1 + languageName: node + linkType: hard + "@chainsafe/as-chacha20poly1305@npm:^0.1.0": version: 0.1.0 resolution: "@chainsafe/as-chacha20poly1305@npm:0.1.0" @@ -4483,6 +4924,16 @@ __metadata: languageName: node linkType: hard +"@clack/core@npm:1.4.3": + version: 1.4.3 + resolution: "@clack/core@npm:1.4.3" + dependencies: + fast-wrap-ansi: "npm:^0.2.0" + sisteransi: "npm:^1.0.5" + checksum: 10c0/16cda430974bc02844cfa6e3f0e6e19695d97e915a580ab74603d34a1f00dcbdd8dc856adc3f89405e3555b5cf42568ae8687e748cb92c3434fe4d9fc7ebe664 + languageName: node + linkType: hard + "@clack/prompts@npm:0.11.0": version: 0.11.0 resolution: "@clack/prompts@npm:0.11.0" @@ -4494,6 +4945,18 @@ __metadata: languageName: node linkType: hard +"@clack/prompts@npm:^1.1.0": + version: 1.7.0 + resolution: "@clack/prompts@npm:1.7.0" + dependencies: + "@clack/core": "npm:1.4.3" + fast-string-width: "npm:^3.0.2" + fast-wrap-ansi: "npm:^0.2.0" + sisteransi: "npm:^1.0.5" + checksum: 10c0/a11e4f8d4a03cfe2d9eb21c3263f0252648573977b96e495a7d4f159f7efa929fe775a6ac3fe7ae30d8acb72514d94418a2c4335855d05b575a41aa885c9ff26 + languageName: node + linkType: hard + "@cloudamqp/amqp-client@npm:^2.1.1": version: 2.1.1 resolution: "@cloudamqp/amqp-client@npm:2.1.1" @@ -4675,6 +5138,13 @@ __metadata: languageName: node linkType: hard +"@ctrl/tinycolor@npm:^4.0.4": + version: 4.2.0 + resolution: "@ctrl/tinycolor@npm:4.2.0" + checksum: 10c0/374034581953f6debd950e4b07da833d1d8f7af5aead5117c91545fbe1583c91b2407bc285e1625529839135d6465c2e7549e7a8ad70979cab4c207486798064 + languageName: node + linkType: hard + "@dabh/diagnostics@npm:^2.0.8": version: 2.0.8 resolution: "@dabh/diagnostics@npm:2.0.8" @@ -4890,6 +5360,64 @@ __metadata: languageName: node linkType: hard +"@emmetio/abbreviation@npm:^2.3.3": + version: 2.3.3 + resolution: "@emmetio/abbreviation@npm:2.3.3" + dependencies: + "@emmetio/scanner": "npm:^1.0.4" + checksum: 10c0/835b460706d5920a6f9a569a44b7d98e88d5530e3983af3678b44fa38b4cbdf68b5df933476d72e340779b16e7e7962ffa63142db8d2f59b1175a11c30c14635 + languageName: node + linkType: hard + +"@emmetio/css-abbreviation@npm:^2.1.8": + version: 2.1.8 + resolution: "@emmetio/css-abbreviation@npm:2.1.8" + dependencies: + "@emmetio/scanner": "npm:^1.0.4" + checksum: 10c0/b5b3b39e773185d848b634e48e1b520e6ebffd28bfd0ba34fbcf877ca77e0edb8c7bbf58230cb0621f80f579bd7fd0265f00ab5e09ac482a835897cbdb6182a6 + languageName: node + linkType: hard + +"@emmetio/css-parser@npm:^0.4.1": + version: 0.4.1 + resolution: "@emmetio/css-parser@npm:0.4.1" + dependencies: + "@emmetio/stream-reader": "npm:^2.2.0" + "@emmetio/stream-reader-utils": "npm:^0.1.0" + checksum: 10c0/d34bf6c8d431eaa2b8b9cdff14360755ca7e235392a23666fe89f2e2b14ac54657712c196bbb6cb016b5bbf3e19faf7f413f7de815d35a26fce82a507191950e + languageName: node + linkType: hard + +"@emmetio/html-matcher@npm:^1.3.0": + version: 1.3.0 + resolution: "@emmetio/html-matcher@npm:1.3.0" + dependencies: + "@emmetio/scanner": "npm:^1.0.0" + checksum: 10c0/354f6d4d52ffb124883cc350d6a09942f2ec352b03dfb928ba8c3078b0f570041b6095367367094a307388c158e6b053565c09a9c1db699517801435a0eadb5c + languageName: node + linkType: hard + +"@emmetio/scanner@npm:^1.0.0, @emmetio/scanner@npm:^1.0.4": + version: 1.0.4 + resolution: "@emmetio/scanner@npm:1.0.4" + checksum: 10c0/ae6244e563caaff0f88d7afefc33fd6cfb7cc767ce914b54d35b46002637948cfc65951dba6d6941328afa54c721c225836fafce2de40fb7643660ba09fe7372 + languageName: node + linkType: hard + +"@emmetio/stream-reader-utils@npm:^0.1.0": + version: 0.1.0 + resolution: "@emmetio/stream-reader-utils@npm:0.1.0" + checksum: 10c0/8630a066200976aa07ce25708484e5c2458182961280dc4637a7a6e98e1f3bb59fb7bf3bf26380f74c6ff1fd40d3dbf1143f21c721833199e45f669614a33b87 + languageName: node + linkType: hard + +"@emmetio/stream-reader@npm:^2.2.0": + version: 2.2.0 + resolution: "@emmetio/stream-reader@npm:2.2.0" + checksum: 10c0/d1a5c83b1ef062ce89b48e1fc170cd5f21b0dfa6132dc41421ef7eda455563227c047977cd4c7b70f072fa73b5af553b04cafe381c600df9bd17c58ca8238e8b + languageName: node + linkType: hard + "@emnapi/core@npm:1.11.1, @emnapi/core@npm:^1.4.3": version: 1.11.1 resolution: "@emnapi/core@npm:1.11.1" @@ -5147,6 +5675,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/aix-ppc64@npm:0.28.1" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/android-arm64@npm:0.25.12" @@ -5161,6 +5696,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-arm64@npm:0.28.1" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/android-arm@npm:0.25.12" @@ -5175,6 +5717,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-arm@npm:0.28.1" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/android-x64@npm:0.25.12" @@ -5189,6 +5738,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/android-x64@npm:0.28.1" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/darwin-arm64@npm:0.25.12" @@ -5203,6 +5759,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/darwin-arm64@npm:0.28.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/darwin-x64@npm:0.25.12" @@ -5217,6 +5780,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/darwin-x64@npm:0.28.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/freebsd-arm64@npm:0.25.12" @@ -5231,6 +5801,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/freebsd-arm64@npm:0.28.1" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/freebsd-x64@npm:0.25.12" @@ -5245,6 +5822,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/freebsd-x64@npm:0.28.1" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/linux-arm64@npm:0.25.12" @@ -5259,6 +5843,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-arm64@npm:0.28.1" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/linux-arm@npm:0.25.12" @@ -5273,6 +5864,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-arm@npm:0.28.1" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/linux-ia32@npm:0.25.12" @@ -5287,6 +5885,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-ia32@npm:0.28.1" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/linux-loong64@npm:0.25.12" @@ -5301,6 +5906,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-loong64@npm:0.28.1" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/linux-mips64el@npm:0.25.12" @@ -5315,6 +5927,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-mips64el@npm:0.28.1" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/linux-ppc64@npm:0.25.12" @@ -5329,6 +5948,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-ppc64@npm:0.28.1" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/linux-riscv64@npm:0.25.12" @@ -5343,6 +5969,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-riscv64@npm:0.28.1" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/linux-s390x@npm:0.25.12" @@ -5357,6 +5990,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-s390x@npm:0.28.1" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/linux-x64@npm:0.25.12" @@ -5371,6 +6011,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/linux-x64@npm:0.28.1" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-arm64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/netbsd-arm64@npm:0.25.12" @@ -5385,6 +6032,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/netbsd-arm64@npm:0.28.1" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/netbsd-x64@npm:0.25.12" @@ -5399,6 +6053,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/netbsd-x64@npm:0.28.1" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-arm64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/openbsd-arm64@npm:0.25.12" @@ -5413,6 +6074,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openbsd-arm64@npm:0.28.1" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/openbsd-x64@npm:0.25.12" @@ -5427,6 +6095,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openbsd-x64@npm:0.28.1" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openharmony-arm64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/openharmony-arm64@npm:0.25.12" @@ -5441,6 +6116,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openharmony-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/openharmony-arm64@npm:0.28.1" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/sunos-x64@npm:0.25.12" @@ -5455,6 +6137,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/sunos-x64@npm:0.28.1" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/win32-arm64@npm:0.25.12" @@ -5469,6 +6158,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-arm64@npm:0.28.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/win32-ia32@npm:0.25.12" @@ -5483,6 +6179,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-ia32@npm:0.28.1" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.25.12": version: 0.25.12 resolution: "@esbuild/win32-x64@npm:0.25.12" @@ -5497,6 +6200,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.28.1": + version: 0.28.1 + resolution: "@esbuild/win32-x64@npm:0.28.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0, @eslint-community/eslint-utils@npm:^4.7.0, @eslint-community/eslint-utils@npm:^4.8.0, @eslint-community/eslint-utils@npm:^4.9.0, @eslint-community/eslint-utils@npm:^4.9.1": version: 4.9.1 resolution: "@eslint-community/eslint-utils@npm:4.9.1" @@ -5768,6 +6478,51 @@ __metadata: languageName: node linkType: hard +"@expressive-code/core@npm:^0.44.1": + version: 0.44.1 + resolution: "@expressive-code/core@npm:0.44.1" + dependencies: + "@ctrl/tinycolor": "npm:^4.0.4" + hast-util-select: "npm:^6.0.2" + hast-util-to-html: "npm:^9.0.1" + hast-util-to-text: "npm:^4.0.1" + hastscript: "npm:^9.0.0" + postcss: "npm:^8.4.38" + postcss-nested: "npm:^6.0.1" + unist-util-visit: "npm:^5.0.0" + unist-util-visit-parents: "npm:^6.0.1" + checksum: 10c0/0e2bc031ce475b290e107ae61b4f915daebf2c7672d293d5f3c12dcc0c88f8c9b82d8649a0a9f5da7fdb8bc2a620c8f08d84f536a59c8ea0fc1777758db735a1 + languageName: node + linkType: hard + +"@expressive-code/plugin-frames@npm:^0.44.1": + version: 0.44.1 + resolution: "@expressive-code/plugin-frames@npm:0.44.1" + dependencies: + "@expressive-code/core": "npm:^0.44.1" + checksum: 10c0/d32c6c4ae72439b4a5ac25688180d037df27ce3bc1a361e89732f487109afe8960b1b2a573c3d704bda2feb2e2a1e72828385abc6971d4c823ba3b06153bcd28 + languageName: node + linkType: hard + +"@expressive-code/plugin-shiki@npm:^0.44.1": + version: 0.44.1 + resolution: "@expressive-code/plugin-shiki@npm:0.44.1" + dependencies: + "@expressive-code/core": "npm:^0.44.1" + shiki: "npm:^4.0.2" + checksum: 10c0/3e9fac7ddc3ebf8cf24579bae9a6767b92f5c87d8de3eb47c0dac1ed138070ef2a4148732b3cd63d178c8fe9bc25f46933228a5b2b44615e5ab453eb7e064afb + languageName: node + linkType: hard + +"@expressive-code/plugin-text-markers@npm:^0.44.1": + version: 0.44.1 + resolution: "@expressive-code/plugin-text-markers@npm:0.44.1" + dependencies: + "@expressive-code/core": "npm:^0.44.1" + checksum: 10c0/78b8d2a98b931d78156830d3deeadbfd16fa1e221fe08a7ed8aca00c20663623f20496d1079529d70cea2660bf27c77783c4626797d77b8ce6932d02729096bd + languageName: node + linkType: hard + "@faker-js/faker@npm:10.4.0": version: 10.4.0 resolution: "@faker-js/faker@npm:10.4.0" @@ -8680,7 +9435,7 @@ __metadata: languageName: unknown linkType: soft -"@local/petrinaut-arch-docs@workspace:libs/@local/petrinaut-arch-docs": +"@local/petrinaut-arch-docs@workspace:*, @local/petrinaut-arch-docs@workspace:libs/@local/petrinaut-arch-docs": version: 0.0.0-use.local resolution: "@local/petrinaut-arch-docs@workspace:libs/@local/petrinaut-arch-docs" dependencies: @@ -8846,7 +9601,7 @@ __metadata: languageName: node linkType: hard -"@mdx-js/mdx@npm:^3.1.0": +"@mdx-js/mdx@npm:^3.1.0, @mdx-js/mdx@npm:^3.1.1": version: 3.1.1 resolution: "@mdx-js/mdx@npm:3.1.1" dependencies: @@ -9385,15 +10140,15 @@ __metadata: languageName: node linkType: hard -"@napi-rs/wasm-runtime@npm:^1.1.5, @napi-rs/wasm-runtime@npm:^1.1.6": - version: 1.1.6 - resolution: "@napi-rs/wasm-runtime@npm:1.1.6" +"@napi-rs/wasm-runtime@npm:^1.1.5, @napi-rs/wasm-runtime@npm:^1.1.6, @napi-rs/wasm-runtime@npm:^1.2.0": + version: 1.2.2 + resolution: "@napi-rs/wasm-runtime@npm:1.2.2" dependencies: "@tybys/wasm-util": "npm:^0.10.3" peerDependencies: - "@emnapi/core": ^1.7.1 - "@emnapi/runtime": ^1.7.1 - checksum: 10c0/344518bf3ef65051dda4c00969f293aa4a21ab7dc7822b3f48519b17cd5eaa3f0bc34898d115d50ba59b1817a0cb905d46f7a7223c8249239cd14c28db388e10 + "@emnapi/core": ^1.7.1 || ^2.0.0-alpha.3 + "@emnapi/runtime": ^1.7.1 || ^2.0.0-alpha.3 + checksum: 10c0/670ff8359761660f58d95a29fe22ac959d2c295675144fe9bc35118b0e723d1e0ac192df9896ba428eb13930cf0893b632606b70ccaea259fd6eca1836c2eb09 languageName: node linkType: hard @@ -10081,6 +10836,13 @@ __metadata: languageName: node linkType: hard +"@oslojs/encoding@npm:^1.1.0": + version: 1.1.0 + resolution: "@oslojs/encoding@npm:1.1.0" + checksum: 10c0/5553a0974dca60e1a8b247b7b97abcb141cc7ee4e22444f424a07921d6a5f76a43c316f3ee669222787fdef6549f8749cc6d68ff5a631e2542521c56fe36417f + languageName: node + linkType: hard + "@oxc-project/types@npm:=0.137.0": version: 0.137.0 resolution: "@oxc-project/types@npm:0.137.0" @@ -10095,6 +10857,13 @@ __metadata: languageName: node linkType: hard +"@oxc-project/types@npm:=0.143.0": + version: 0.143.0 + resolution: "@oxc-project/types@npm:0.143.0" + checksum: 10c0/f450bdc6ebd69b09b5d77c1ca45369f9e1a2b69d21f9dfcdaaa2379e8d5228bfdd014e77bef9bc21ca5fd118fb9e2213d30d8eb70299f03b1aac5ee1ab2802ba + languageName: node + linkType: hard + "@oxfmt/binding-android-arm-eabi@npm:0.50.0": version: 0.50.0 resolution: "@oxfmt/binding-android-arm-eabi@npm:0.50.0" @@ -10403,6 +11172,62 @@ __metadata: languageName: node linkType: hard +"@pagefind/darwin-arm64@npm:1.5.2": + version: 1.5.2 + resolution: "@pagefind/darwin-arm64@npm:1.5.2" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@pagefind/darwin-x64@npm:1.5.2": + version: 1.5.2 + resolution: "@pagefind/darwin-x64@npm:1.5.2" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@pagefind/default-ui@npm:^1.3.0": + version: 1.5.2 + resolution: "@pagefind/default-ui@npm:1.5.2" + checksum: 10c0/2e4add8f315b6ceb2355b635bf1bbc1fc84e4d3d1dadafc38696184aff1d459720a8efcb843d58ae798a30512859620960a671311ff8e197d0a67b43c47fdeef + languageName: node + linkType: hard + +"@pagefind/freebsd-x64@npm:1.5.2": + version: 1.5.2 + resolution: "@pagefind/freebsd-x64@npm:1.5.2" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@pagefind/linux-arm64@npm:1.5.2": + version: 1.5.2 + resolution: "@pagefind/linux-arm64@npm:1.5.2" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@pagefind/linux-x64@npm:1.5.2": + version: 1.5.2 + resolution: "@pagefind/linux-x64@npm:1.5.2" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@pagefind/windows-arm64@npm:1.5.2": + version: 1.5.2 + resolution: "@pagefind/windows-arm64@npm:1.5.2" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@pagefind/windows-x64@npm:1.5.2": + version: 1.5.2 + resolution: "@pagefind/windows-x64@npm:1.5.2" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@pandacss/config@npm:1.11.1, @pandacss/config@npm:^1.11.1": version: 1.11.1 resolution: "@pandacss/config@npm:1.11.1" @@ -12501,6 +13326,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-android-arm64@npm:1.2.3": + version: 1.2.3 + resolution: "@rolldown/binding-android-arm64@npm:1.2.3" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@rolldown/binding-darwin-arm64@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-darwin-arm64@npm:1.1.2" @@ -12515,6 +13347,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-darwin-arm64@npm:1.2.3": + version: 1.2.3 + resolution: "@rolldown/binding-darwin-arm64@npm:1.2.3" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@rolldown/binding-darwin-x64@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-darwin-x64@npm:1.1.2" @@ -12529,6 +13368,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-darwin-x64@npm:1.2.3": + version: 1.2.3 + resolution: "@rolldown/binding-darwin-x64@npm:1.2.3" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@rolldown/binding-freebsd-x64@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-freebsd-x64@npm:1.1.2" @@ -12543,6 +13389,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-freebsd-x64@npm:1.2.3": + version: 1.2.3 + resolution: "@rolldown/binding-freebsd-x64@npm:1.2.3" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@rolldown/binding-linux-arm-gnueabihf@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.1.2" @@ -12557,6 +13410,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-arm-gnueabihf@npm:1.2.3": + version: 1.2.3 + resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.2.3" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@rolldown/binding-linux-arm64-gnu@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.1.2" @@ -12571,6 +13431,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-arm64-gnu@npm:1.2.3": + version: 1.2.3 + resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.2.3" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + "@rolldown/binding-linux-arm64-musl@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-linux-arm64-musl@npm:1.1.2" @@ -12585,6 +13452,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-arm64-musl@npm:1.2.3": + version: 1.2.3 + resolution: "@rolldown/binding-linux-arm64-musl@npm:1.2.3" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + "@rolldown/binding-linux-ppc64-gnu@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.1.2" @@ -12599,6 +13473,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-ppc64-gnu@npm:1.2.3": + version: 1.2.3 + resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.2.3" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + "@rolldown/binding-linux-s390x-gnu@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.1.2" @@ -12613,6 +13494,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-s390x-gnu@npm:1.2.3": + version: 1.2.3 + resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.2.3" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + "@rolldown/binding-linux-x64-gnu@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-linux-x64-gnu@npm:1.1.2" @@ -12627,6 +13515,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-x64-gnu@npm:1.2.3": + version: 1.2.3 + resolution: "@rolldown/binding-linux-x64-gnu@npm:1.2.3" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + "@rolldown/binding-linux-x64-musl@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-linux-x64-musl@npm:1.1.2" @@ -12641,6 +13536,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-linux-x64-musl@npm:1.2.3": + version: 1.2.3 + resolution: "@rolldown/binding-linux-x64-musl@npm:1.2.3" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + "@rolldown/binding-openharmony-arm64@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-openharmony-arm64@npm:1.1.2" @@ -12655,6 +13557,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-openharmony-arm64@npm:1.2.3": + version: 1.2.3 + resolution: "@rolldown/binding-openharmony-arm64@npm:1.2.3" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@rolldown/binding-wasm32-wasi@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-wasm32-wasi@npm:1.1.2" @@ -12691,6 +13600,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-win32-arm64-msvc@npm:1.2.3": + version: 1.2.3 + resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.2.3" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@rolldown/binding-win32-x64-msvc@npm:1.1.2": version: 1.1.2 resolution: "@rolldown/binding-win32-x64-msvc@npm:1.1.2" @@ -12705,6 +13621,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/binding-win32-x64-msvc@npm:1.2.3": + version: 1.2.3 + resolution: "@rolldown/binding-win32-x64-msvc@npm:1.2.3" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@rolldown/plugin-babel@npm:0.2.1": version: 0.2.1 resolution: "@rolldown/plugin-babel@npm:0.2.1" @@ -12741,6 +13664,13 @@ __metadata: languageName: node linkType: hard +"@rolldown/pluginutils@npm:1.0.0-rc.3": + version: 1.0.0-rc.3 + resolution: "@rolldown/pluginutils@npm:1.0.0-rc.3" + checksum: 10c0/3928b6282a30f307d1b075d2f217180ae173ea9e00638ce46ab65f089bd5f7a0b2c488ae1ce530f509387793c656a2910337c4cd68fa9d37d7e439365989e699 + languageName: node + linkType: hard + "@rolldown/pluginutils@npm:1.0.0-rc.7": version: 1.0.0-rc.7 resolution: "@rolldown/pluginutils@npm:1.0.0-rc.7" @@ -14176,6 +15106,86 @@ __metadata: languageName: node linkType: hard +"@shikijs/core@npm:4.4.2": + version: 4.4.2 + resolution: "@shikijs/core@npm:4.4.2" + dependencies: + "@shikijs/primitive": "npm:4.4.2" + "@shikijs/types": "npm:4.4.2" + "@shikijs/vscode-textmate": "npm:^10.0.2" + "@types/hast": "npm:^3.0.5" + hast-util-to-html: "npm:^9.0.5" + checksum: 10c0/35058e528dcaea04d3bbf2c3ab6defc352b1d1cc90ac1283645af45f1f208218717ce10e24b27dd03fabdc2b039bedfd0bf69712de1d7ab8f0c67d8c343c6a7e + languageName: node + linkType: hard + +"@shikijs/engine-javascript@npm:4.4.2": + version: 4.4.2 + resolution: "@shikijs/engine-javascript@npm:4.4.2" + dependencies: + "@shikijs/types": "npm:4.4.2" + "@shikijs/vscode-textmate": "npm:^10.0.2" + oniguruma-to-es: "npm:^4.3.6" + checksum: 10c0/2e8cf60249567da010ce73d08acba9a42ef86c29e6ac8876d196558ba633d666498c98a47f1e3ba59308b78e8b74d7e5793a9061ab1b78919fa28528c3de531a + languageName: node + linkType: hard + +"@shikijs/engine-oniguruma@npm:4.4.2": + version: 4.4.2 + resolution: "@shikijs/engine-oniguruma@npm:4.4.2" + dependencies: + "@shikijs/types": "npm:4.4.2" + "@shikijs/vscode-textmate": "npm:^10.0.2" + checksum: 10c0/35ce68c3d1cfdf96626521db8f5256770f6327dcd083f8144bc8fcc43a37ae5d83c4472223784fa70b292c3bed67fa02a5130cfc6c54324d08730c760e5ee1a4 + languageName: node + linkType: hard + +"@shikijs/langs@npm:4.4.2": + version: 4.4.2 + resolution: "@shikijs/langs@npm:4.4.2" + dependencies: + "@shikijs/types": "npm:4.4.2" + checksum: 10c0/041026bd22f2a4b0a073304bf507adc0c175994edf89238ef8e94abc9eeda707e8eed0ee71c0ed2e4c662fb981b7e48d5f5165fd64ffb15596c1273e429da18c + languageName: node + linkType: hard + +"@shikijs/primitive@npm:4.4.2": + version: 4.4.2 + resolution: "@shikijs/primitive@npm:4.4.2" + dependencies: + "@shikijs/types": "npm:4.4.2" + "@shikijs/vscode-textmate": "npm:^10.0.2" + "@types/hast": "npm:^3.0.5" + checksum: 10c0/6b60cd2ef0f385435283266a9c99e9274dc1a56af8f7bd31189a1b4133c31a27678cb58c5ef96772b71a031c3aa0364e1378a8d962f865a04cf6ad8dae1eba5c + languageName: node + linkType: hard + +"@shikijs/themes@npm:4.4.2": + version: 4.4.2 + resolution: "@shikijs/themes@npm:4.4.2" + dependencies: + "@shikijs/types": "npm:4.4.2" + checksum: 10c0/36247ea8db358e8ccf1070d5b113d685abeb53af681f7ab0c07e025c71d53097d50fe5715ec46fb70b137bc821300cb368401effd1c076d80df6c319ae4a9d37 + languageName: node + linkType: hard + +"@shikijs/types@npm:4.4.2": + version: 4.4.2 + resolution: "@shikijs/types@npm:4.4.2" + dependencies: + "@shikijs/vscode-textmate": "npm:^10.0.2" + "@types/hast": "npm:^3.0.5" + checksum: 10c0/219e6ec76378a3740545327d184771fd9b697e4c5a0a409cf3dbdb5cef5eb6e046433aa2509a9bdef7adcd0b22f70d6ca18da3a035f45679da639249caa53a6a + languageName: node + linkType: hard + +"@shikijs/vscode-textmate@npm:^10.0.2": + version: 10.0.2 + resolution: "@shikijs/vscode-textmate@npm:10.0.2" + checksum: 10c0/36b682d691088ec244de292dc8f91b808f95c89466af421cf84cbab92230f03c8348649c14b3251991b10ce632b0c715e416e992dd5f28ff3221dc2693fd9462 + languageName: node + linkType: hard + "@sigma/edge-curve@npm:3.0.0-beta.16": version: 3.0.0-beta.16 resolution: "@sigma/edge-curve@npm:3.0.0-beta.16" @@ -16782,7 +17792,7 @@ __metadata: languageName: node linkType: hard -"@types/estree-jsx@npm:^1.0.0": +"@types/estree-jsx@npm:^1.0.0, @types/estree-jsx@npm:^1.0.5": version: 1.0.5 resolution: "@types/estree-jsx@npm:1.0.5" dependencies: @@ -16925,12 +17935,12 @@ __metadata: languageName: node linkType: hard -"@types/hast@npm:^3.0.0": - version: 3.0.4 - resolution: "@types/hast@npm:3.0.4" +"@types/hast@npm:^3.0.0, @types/hast@npm:^3.0.4, @types/hast@npm:^3.0.5": + version: 3.0.5 + resolution: "@types/hast@npm:3.0.5" dependencies: "@types/unist": "npm:*" - checksum: 10c0/3249781a511b38f1d330fd1e3344eed3c4e7ea8eff82e835d35da78e637480d36fad37a78be5a7aed8465d237ad0446abc1150859d0fde395354ea634decf9f7 + checksum: 10c0/f3af8594a6903a507ed191eda944af18099198d6708c29102ae17118c3e20779f6929e91ed37e033541fd28d58055d8a5910a4366dbdf976cf81b13a464741fb languageName: node linkType: hard @@ -16988,7 +17998,7 @@ __metadata: languageName: node linkType: hard -"@types/js-yaml@npm:^4": +"@types/js-yaml@npm:^4, @types/js-yaml@npm:^4.0.9": version: 4.0.9 resolution: "@types/js-yaml@npm:4.0.9" checksum: 10c0/24de857aa8d61526bbfbbaa383aa538283ad17363fcd5bb5148e2c7f604547db36646440e739d78241ed008702a8920665d1add5618687b6743858fae00da211 @@ -17132,7 +18142,7 @@ __metadata: languageName: node linkType: hard -"@types/mdast@npm:^4.0.0": +"@types/mdast@npm:^4.0.0, @types/mdast@npm:^4.0.4": version: 4.0.4 resolution: "@types/mdast@npm:4.0.4" dependencies: @@ -17185,6 +18195,15 @@ __metadata: languageName: node linkType: hard +"@types/nlcst@npm:^2.0.0": + version: 2.0.3 + resolution: "@types/nlcst@npm:2.0.3" + dependencies: + "@types/unist": "npm:*" + checksum: 10c0/d83549aaee59681ae8fa2a78d8a1b968a41eb7c0422773dff12acbf3661e4b2b2859740c3effdad9d0cd12ea14a0ec33ca302da12106476b627e09d2a029d3c1 + languageName: node + linkType: hard + "@types/node-fetch@npm:2.6.13": version: 2.6.13 resolution: "@types/node-fetch@npm:2.6.13" @@ -17195,12 +18214,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:>=13.7.0, @types/node@npm:^24.0.13": - version: 24.10.13 - resolution: "@types/node@npm:24.10.13" +"@types/node@npm:*, @types/node@npm:>=13.7.0, @types/node@npm:^24.0.13, @types/node@npm:^24.9.2": + version: 24.13.3 + resolution: "@types/node@npm:24.13.3" dependencies: - undici-types: "npm:~7.16.0" - checksum: 10c0/4ff0b9b060b5477c0fec5b11a176f294be588104ab546295db65b17a92ba0a6077b52ad92dd3c0d2154198c7f9d0021e6c1d42b00c9ac7ebfd85632afbcc48a4 + undici-types: "npm:~7.18.0" + checksum: 10c0/a5bc08f49b9581dcdca90e02cd77197a3c799840807664942e5cd5161a553d4d2ae8064f7e3294410d748d7d098b5c1848be7fa50c06f29c4193ab16be4e59eb languageName: node linkType: hard @@ -17476,6 +18495,15 @@ __metadata: languageName: node linkType: hard +"@types/sax@npm:^1.2.1": + version: 1.2.7 + resolution: "@types/sax@npm:1.2.7" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/d077a761a0753b079bf8279b3993948030ca86ed9125437b9b29c1de40db9b2deb7fddc369f014b58861d450e8b8cc75f163aa29dc8cea81952efbfd859168cf + languageName: node + linkType: hard + "@types/semver@npm:7.7.1": version: 7.7.1 resolution: "@types/semver@npm:7.7.1" @@ -17573,7 +18601,7 @@ __metadata: languageName: node linkType: hard -"@types/unist@npm:*, @types/unist@npm:^3.0.0": +"@types/unist@npm:*, @types/unist@npm:^3.0.0, @types/unist@npm:^3.0.3": version: 3.0.3 resolution: "@types/unist@npm:3.0.3" checksum: 10c0/2b1e4adcab78388e088fcc3c0ae8700f76619dbcb4741d7d201f87e2cb346bfc29a89003cfea2d76c996e1061452e14fcd737e8b25aacf949c1f2d6b2bc3dd60 @@ -18422,6 +19450,22 @@ __metadata: languageName: node linkType: hard +"@vitejs/plugin-react@npm:^5.2.0": + version: 5.2.0 + resolution: "@vitejs/plugin-react@npm:5.2.0" + dependencies: + "@babel/core": "npm:^7.29.0" + "@babel/plugin-transform-react-jsx-self": "npm:^7.27.1" + "@babel/plugin-transform-react-jsx-source": "npm:^7.27.1" + "@rolldown/pluginutils": "npm:1.0.0-rc.3" + "@types/babel__core": "npm:^7.20.5" + react-refresh: "npm:^0.18.0" + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 10c0/bac0a409e71eee954a05bc41580411c369bd5f9ef0586a1f9743fba76ad6603c437d93d407d230780015361f93d1592c55e53314813cded6369c36d3c1e8edbf + languageName: node + linkType: hard + "@vitest/browser-playwright@npm:4.1.10": version: 4.1.10 resolution: "@vitest/browser-playwright@npm:4.1.10" @@ -18637,30 +19681,94 @@ __metadata: languageName: node linkType: hard -"@volar/language-core@npm:2.4.23, @volar/language-core@npm:~2.4.11": - version: 2.4.23 - resolution: "@volar/language-core@npm:2.4.23" +"@volar/kit@npm:~2.4.28": + version: 2.4.28 + resolution: "@volar/kit@npm:2.4.28" + dependencies: + "@volar/language-service": "npm:2.4.28" + "@volar/typescript": "npm:2.4.28" + typesafe-path: "npm:^0.2.2" + vscode-languageserver-textdocument: "npm:^1.0.11" + vscode-uri: "npm:^3.0.8" + peerDependencies: + typescript: "*" + checksum: 10c0/c097b930eeb94d432ba7d21e0286ec787931d9fbf1c07e47e0df2d01dfa5a8d9413bac5c2fb4e342edfe57bec29b916427e62d40792ba39b614a2ff1fe3907a1 + languageName: node + linkType: hard + +"@volar/language-core@npm:2.4.28, @volar/language-core@npm:~2.4.11, @volar/language-core@npm:~2.4.28": + version: 2.4.28 + resolution: "@volar/language-core@npm:2.4.28" + dependencies: + "@volar/source-map": "npm:2.4.28" + checksum: 10c0/d41f7327fed7fa5301fbf2d8f96753d645a976b21dbbeb869794a780aa6523d1e6bf258242bc3d8ccd37f8e8b98a04fea9574e6f63badc585a8a3c2e068c4a86 + languageName: node + linkType: hard + +"@volar/language-server@npm:~2.4.28": + version: 2.4.28 + resolution: "@volar/language-server@npm:2.4.28" + dependencies: + "@volar/language-core": "npm:2.4.28" + "@volar/language-service": "npm:2.4.28" + "@volar/typescript": "npm:2.4.28" + path-browserify: "npm:^1.0.1" + request-light: "npm:^0.7.0" + vscode-languageserver: "npm:^9.0.1" + vscode-languageserver-protocol: "npm:^3.17.5" + vscode-languageserver-textdocument: "npm:^1.0.11" + vscode-uri: "npm:^3.0.8" + checksum: 10c0/8266fdab264dbe449501e7d05e584069e6eb6cc5a06bf02968107c3fa32f8eb49b363d57d7abd76e5f637265469be8e13ebce1dd4b3e6ebdfae030691879d283 + languageName: node + linkType: hard + +"@volar/language-service@npm:2.4.28, @volar/language-service@npm:~2.4.28": + version: 2.4.28 + resolution: "@volar/language-service@npm:2.4.28" dependencies: - "@volar/source-map": "npm:2.4.23" - checksum: 10c0/1b8d60c7c0faa29ef5ec46dd2b673227592d0697753767e4df088f7c2d93843828116fe59472bb9d604ba653400be32a538e985730844b1af4f42a7075e62049 + "@volar/language-core": "npm:2.4.28" + vscode-languageserver-protocol: "npm:^3.17.5" + vscode-languageserver-textdocument: "npm:^1.0.11" + vscode-uri: "npm:^3.0.8" + checksum: 10c0/a150d34f0c1c92641ff978eb6045ac6f0717cf16a51cfd3d0798267ccdc91aca21e6746403bf0254c747fa510ea92e3eb064acfa42320c0ebae959fd4a88add8 languageName: node linkType: hard -"@volar/source-map@npm:2.4.23": - version: 2.4.23 - resolution: "@volar/source-map@npm:2.4.23" - checksum: 10c0/08af690093b811d0a37bdd8d306755b4e7f1535b67625c26f6fa6eb9ae081e24c55dabc8231ce8856aa1b731a5ac137b3f0449b34c093923c3545afdbe462c7a +"@volar/source-map@npm:2.4.28": + version: 2.4.28 + resolution: "@volar/source-map@npm:2.4.28" + checksum: 10c0/24b0b02c7f66febe47f0bfda4a5ed4beaf949041eddc6325c7478b900faeb071795b696d97a4f326dde47217d06e40b67129300bc544f054772c5cb84c2f254e languageName: node linkType: hard -"@volar/typescript@npm:^2.4.11": - version: 2.4.23 - resolution: "@volar/typescript@npm:2.4.23" +"@volar/typescript@npm:2.4.28, @volar/typescript@npm:^2.4.11": + version: 2.4.28 + resolution: "@volar/typescript@npm:2.4.28" dependencies: - "@volar/language-core": "npm:2.4.23" + "@volar/language-core": "npm:2.4.28" path-browserify: "npm:^1.0.1" vscode-uri: "npm:^3.0.8" - checksum: 10c0/dbb449b66e627a75f8f6df98b3210c32edff62747a12d1e6237a6dc2a75f26432833d4d3646d6fbd60ed21fa52d7e342437377973b80cf4bbeacee1980ffd0cb + checksum: 10c0/075c890b9ec1cb17f17e38aaed035f8ee7d507439e87270d8e3c394356fc9387fd0bda9ec1069b36ea4c378d9375a08f5bc64c063a83427010ddd86d472124fc + languageName: node + linkType: hard + +"@vscode/emmet-helper@npm:^2.9.3": + version: 2.11.0 + resolution: "@vscode/emmet-helper@npm:2.11.0" + dependencies: + emmet: "npm:^2.4.3" + jsonc-parser: "npm:^2.3.0" + vscode-languageserver-textdocument: "npm:^1.0.1" + vscode-languageserver-types: "npm:^3.15.1" + vscode-uri: "npm:^3.0.8" + checksum: 10c0/41c743258f958ff143f5152a7c8e8e1c6f96336f4147d537ace7c81337d5f9d7f93efd6df16c80e6d9506c51df9c8deb6440765a1deae219e31cdbead3cfd841 + languageName: node + linkType: hard + +"@vscode/l10n@npm:^0.0.18": + version: 0.0.18 + resolution: "@vscode/l10n@npm:0.0.18" + checksum: 10c0/d1fc797001f7d508ab3fa91175f7a50ea98516c4e47830ff2be79163cde9279279514a167a3bad15b7ab7fc243e7808d8f32d3eb41f4a7d6721d9dfdbb38d89e languageName: node linkType: hard @@ -20235,7 +21343,7 @@ __metadata: languageName: node linkType: hard -"ajv-draft-04@npm:~1.0.0": +"ajv-draft-04@npm:^1.0.0, ajv-draft-04@npm:~1.0.0": version: 1.0.0 resolution: "ajv-draft-04@npm:1.0.0" peerDependencies: @@ -20275,6 +21383,15 @@ __metadata: languageName: node linkType: hard +"ajv-i18n@npm:^4.2.0": + version: 4.2.0 + resolution: "ajv-i18n@npm:4.2.0" + peerDependencies: + ajv: ^8.0.0-beta.0 + checksum: 10c0/8c27882f2d46147600538aa8f48a633c72cd65baa37e0fae7159e832122b4ee504f4076f81ea3f78c15d7236a2d499d42a34d1a27b202066edc620f23963f6db + languageName: node + linkType: hard + "ajv-keywords@npm:^3.5.2": version: 3.5.2 resolution: "ajv-keywords@npm:3.5.2" @@ -20326,6 +21443,17 @@ __metadata: languageName: node linkType: hard +"am-i-vibing@npm:^0.4.0": + version: 0.4.0 + resolution: "am-i-vibing@npm:0.4.0" + dependencies: + process-ancestry: "npm:^0.1.0" + bin: + am-i-vibing: dist/cli.mjs + checksum: 10c0/af511ee621c78957e7e9e86b15498d64b9d73c54689e571a03531fe123002d3a5f7bd2d7fadda9cc07511c8f7cfdcc888d897bebeb12d2bc8b725161e64fd3f5 + languageName: node + linkType: hard + "anser@npm:^2.1.1": version: 2.3.2 resolution: "anser@npm:2.3.2" @@ -20450,7 +21578,7 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:~3.1.2": +"anymatch@npm:^3.1.3, anymatch@npm:~3.1.2": version: 3.1.3 resolution: "anymatch@npm:3.1.3" dependencies: @@ -20483,6 +21611,13 @@ __metadata: languageName: node linkType: hard +"arg@npm:^5.0.0": + version: 5.0.2 + resolution: "arg@npm:5.0.2" + checksum: 10c0/ccaf86f4e05d342af6666c569f844bec426595c567d32a8289715087825c2ca7edd8a3d204e4d2fb2aa4602e09a57d0c13ea8c9eea75aac3dbb4af5514e6800e + languageName: node + linkType: hard + "argparse@npm:^1.0.7, argparse@npm:~1.0.3, argparse@npm:~1.0.9": version: 1.0.10 resolution: "argparse@npm:1.0.10" @@ -20575,6 +21710,13 @@ __metadata: languageName: node linkType: hard +"array-iterate@npm:^2.0.0": + version: 2.0.1 + resolution: "array-iterate@npm:2.0.1" + checksum: 10c0/756c08334f95e290f03ab2141b034514af1311ef7b62f15b0f5ea6f8f3033ee9cc6a8f1c3e9ff4803d4d723cf992aa61460acf5fce884936972db966b1da287d + languageName: node + linkType: hard + "array-union@npm:^1.0.1": version: 1.0.2 resolution: "array-union@npm:1.0.2" @@ -20777,6 +21919,90 @@ __metadata: languageName: node linkType: hard +"astro-expressive-code@npm:^0.44.0": + version: 0.44.1 + resolution: "astro-expressive-code@npm:0.44.1" + dependencies: + rehype-expressive-code: "npm:^0.44.1" + url-extras: "npm:^0.1.0" + peerDependencies: + astro: ^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0 + checksum: 10c0/52d57b7d0b8b0001a51e55f37eb82ade6dea93c53ca1455930c21b89f6e245a3e2660da75fbe924ea019f403cc2648675772ac0352f3a92f5486564e79a54a41 + languageName: node + linkType: hard + +"astro@npm:7.1.6": + version: 7.1.6 + resolution: "astro@npm:7.1.6" + dependencies: + "@astrojs/compiler-rs": "npm:^0.3.2" + "@astrojs/internal-helpers": "npm:0.10.2" + "@astrojs/markdown-satteri": "npm:0.3.5" + "@astrojs/telemetry": "npm:3.3.3" + "@capsizecss/unpack": "npm:^4.0.0" + "@clack/prompts": "npm:^1.1.0" + "@oslojs/encoding": "npm:^1.1.0" + "@rollup/pluginutils": "npm:^5.3.0" + am-i-vibing: "npm:^0.4.0" + aria-query: "npm:^5.3.2" + axobject-query: "npm:^4.1.0" + ci-info: "npm:^4.4.0" + clsx: "npm:^2.1.1" + common-ancestor-path: "npm:^2.0.0" + cookie: "npm:^2.0.1" + devalue: "npm:^5.8.1" + diff: "npm:^8.0.3" + dset: "npm:^3.1.4" + es-module-lexer: "npm:^2.0.0" + esbuild: "npm:^0.28.0" + flattie: "npm:^1.1.1" + fontace: "npm:~0.4.1" + get-tsconfig: "npm:5.0.0-beta.4" + github-slugger: "npm:^2.0.0" + html-escaper: "npm:3.0.3" + http-cache-semantics: "npm:^4.2.0" + js-yaml: "npm:^4.3.0" + jsonc-parser: "npm:^3.3.1" + magic-string: "npm:^1.0.0" + magicast: "npm:^0.5.2" + mrmime: "npm:^2.0.1" + neotraverse: "npm:^1.0.1" + obug: "npm:^2.1.1" + p-limit: "npm:^7.3.0" + p-queue: "npm:^9.1.0" + package-manager-detector: "npm:^1.6.0" + piccolore: "npm:^0.1.3" + picomatch: "npm:^4.0.4" + semver: "npm:^7.7.4" + sharp: "npm:^0.34.0 || ^0.35.0" + shiki: "npm:^4.0.2" + smol-toml: "npm:^1.6.0" + svgo: "npm:^4.0.1" + tinyclip: "npm:^0.1.12" + tinyexec: "npm:^1.0.4" + tinyglobby: "npm:^0.2.15" + ultrahtml: "npm:^1.6.0" + unifont: "npm:~0.7.4" + unstorage: "npm:^1.17.5" + vite: "npm:^8.0.13" + vitefu: "npm:^1.1.2" + xxhash-wasm: "npm:^1.1.0" + yargs-parser: "npm:^22.0.0" + zod: "npm:^4.3.6" + peerDependencies: + "@astrojs/markdown-remark": 7.2.2 + dependenciesMeta: + sharp: + optional: true + peerDependenciesMeta: + "@astrojs/markdown-remark": + optional: true + bin: + astro: ./bin/astro.mjs + checksum: 10c0/cdf83d5b19fa5fbb533bbcb7d8a9cef10f9e350ffe1585a5e0e79ead10915e73527270c3eb140ee8641754907e79df7e915a4547663833ab79cbe7d8bef4f590 + languageName: node + linkType: hard + "astrojs-compiler-sync@npm:^1.0.0": version: 1.0.1 resolution: "astrojs-compiler-sync@npm:1.0.1" @@ -21167,6 +22393,17 @@ __metadata: languageName: node linkType: hard +"bcp-47@npm:^2.1.0": + version: 2.1.1 + resolution: "bcp-47@npm:2.1.1" + dependencies: + is-alphabetical: "npm:^2.0.0" + is-alphanumerical: "npm:^2.0.0" + is-decimal: "npm:^2.0.0" + checksum: 10c0/086cb208c8bb0a2dad6ffcfc626ed9f81116037b92cecb63525410ccc943c1dfb78cb1b9ad41a8bcd239d98500933d1c4c44ae6903d5af428f738f2d743f8dec + languageName: node + linkType: hard + "better-opn@npm:^3.0.2": version: 3.0.2 resolution: "better-opn@npm:3.0.2" @@ -21936,6 +23173,15 @@ __metadata: languageName: node linkType: hard +"chokidar@npm:^5.0.0": + version: 5.0.0 + resolution: "chokidar@npm:5.0.0" + dependencies: + readdirp: "npm:^5.0.0" + checksum: 10c0/42fc907cb2a7ff5c9e220f84dae75380a77997f851c2a5e7865a2cf9ae45dd407a23557208cdcdbf3ac8c93341135a1748e4c48c31855f3bfa095e5159b6bdec + languageName: node + linkType: hard + "chownr@npm:^1.1.1": version: 1.1.4 resolution: "chownr@npm:1.1.4" @@ -21970,7 +23216,7 @@ __metadata: languageName: node linkType: hard -"ci-info@npm:^4.0.0, ci-info@npm:^4.3.1": +"ci-info@npm:^4.0.0, ci-info@npm:^4.3.1, ci-info@npm:^4.4.0": version: 4.4.0 resolution: "ci-info@npm:4.4.0" checksum: 10c0/44156201545b8dde01aa8a09ee2fe9fc7a73b1bef9adbd4606c9f61c8caeeb73fb7a575c88b0443f7b4edb5ee45debaa59ed54ba5f99698339393ca01349eb3a @@ -22411,6 +23657,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^11.1.0": + version: 11.1.0 + resolution: "commander@npm:11.1.0" + checksum: 10c0/13cc6ac875e48780250f723fb81c1c1178d35c5decb1abb1b628b3177af08a8554e76b2c0f29de72d69eef7c864d12613272a71fabef8047922bc622ab75a179 + languageName: node + linkType: hard + "commander@npm:^12.0.0, commander@npm:^12.1.0": version: 12.1.0 resolution: "commander@npm:12.1.0" @@ -22460,6 +23713,13 @@ __metadata: languageName: node linkType: hard +"common-ancestor-path@npm:^2.0.0": + version: 2.0.0 + resolution: "common-ancestor-path@npm:2.0.0" + checksum: 10c0/fa0872dc8d5ffb2c0bb006d1f9e7ba4586773df4f0cf3dfa4b4c95710cedb8a78246fbbcc1392c71c882bd5428a2d003851bdd9033f549a445ac2c5deacb45ca + languageName: node + linkType: hard + "common-tags@npm:1.8.2": version: 1.8.2 resolution: "common-tags@npm:1.8.2" @@ -22675,6 +23935,13 @@ __metadata: languageName: node linkType: hard +"cookie-es@npm:^1.2.3": + version: 1.2.3 + resolution: "cookie-es@npm:1.2.3" + checksum: 10c0/429eae6f5130a7380ea024d787d7e1ecc644ca84f9c43dfb70f18761a831d2ba591d28f837ce350892cfff0857d711f3a4ad93a082637bceb478823c339c1a97 + languageName: node + linkType: hard + "cookie-signature@npm:^1.2.1": version: 1.2.2 resolution: "cookie-signature@npm:1.2.2" @@ -22703,6 +23970,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:^2.0.1": + version: 2.0.1 + resolution: "cookie@npm:2.0.1" + checksum: 10c0/dcf3bd6ec9d0f4f32be7575e23242866cb53be55fc3d4cf36215638596661cec1327a3b4ca286ba15eba8f2abd443e422b8c5f708af4ea9f75b4d3dedc67ee4c + languageName: node + linkType: hard + "cookie@npm:~1.0.1": version: 1.0.2 resolution: "cookie@npm:1.0.2" @@ -22887,6 +24161,15 @@ __metadata: languageName: node linkType: hard +"crossws@npm:^0.3.5": + version: 0.3.5 + resolution: "crossws@npm:0.3.5" + dependencies: + uncrypto: "npm:^0.1.3" + checksum: 10c0/9e873546f0806606c4f775219f6811768fc3b3b0765ca8230722e849058ad098318af006e1faa39a8008c03009c37c519f6bccad41b0d78586237585c75fb38b + languageName: node + linkType: hard + "crypt@npm:0.0.2": version: 0.0.2 resolution: "crypt@npm:0.0.2" @@ -22970,6 +24253,16 @@ __metadata: languageName: node linkType: hard +"css-tree@npm:^3.0.1, css-tree@npm:^3.1.0": + version: 3.2.1 + resolution: "css-tree@npm:3.2.1" + dependencies: + mdn-data: "npm:2.27.1" + source-map-js: "npm:^1.2.1" + checksum: 10c0/1f65e9ccaa56112a4706d6f003dd43d777f0dbcf848e66fd320f823192533581f8dd58daa906cb80622658332d50284d6be13b87a6ab4556cbbfe9ef535bbf7e + languageName: node + linkType: hard + "css-tree@npm:~2.2.0": version: 2.2.1 resolution: "css-tree@npm:2.2.1" @@ -23534,6 +24827,13 @@ __metadata: languageName: node linkType: hard +"defu@npm:^6.1.6": + version: 6.1.7 + resolution: "defu@npm:6.1.7" + checksum: 10c0/e6635388103c8be3c574ac31302f6930e5e6eeedba32cb1b30cf993c7d9fb571aec2485446dfa23bfa63e55e66156fe109027a9695db82a50f931e91e8d4bedb + languageName: node + linkType: hard + "degenerator@npm:7.0.1": version: 7.0.1 resolution: "degenerator@npm:7.0.1" @@ -23669,6 +24969,13 @@ __metadata: languageName: node linkType: hard +"destr@npm:^2.0.5": + version: 2.0.5 + resolution: "destr@npm:2.0.5" + checksum: 10c0/efabffe7312a45ad90d79975376be958c50069f1156b94c181199763a7f971e113bd92227c26b94a169c71ca7dbc13583b7e96e5164743969fc79e1ff153e646 + languageName: node + linkType: hard + "destroy@npm:1.2.0, destroy@npm:^1.0.4, destroy@npm:~1.2.0": version: 1.2.0 resolution: "destroy@npm:1.2.0" @@ -23713,6 +25020,13 @@ __metadata: languageName: node linkType: hard +"devalue@npm:^5.8.1": + version: 5.9.0 + resolution: "devalue@npm:5.9.0" + checksum: 10c0/0ad4cc8e041747fdda6900bccf8648b68ec035fdaf72dd8d30b69295108b704e3078e515921758745139ce64dbd5bdc80e3c9491e4a3205926adcc95a2c22907 + languageName: node + linkType: hard + "devlop@npm:^1.0.0, devlop@npm:^1.1.0": version: 1.1.0 resolution: "devlop@npm:1.1.0" @@ -23729,10 +25043,10 @@ __metadata: languageName: node linkType: hard -"diff@npm:~8.0.2": - version: 8.0.3 - resolution: "diff@npm:8.0.3" - checksum: 10c0/d29321c70d3545fdcb56c5fdd76028c3f04c012462779e062303d4c3c531af80d2c360c26b871e6e2b9a971d2422d47e1779a859106c4cac4b5d2d143df70e20 +"diff@npm:^8.0.3, diff@npm:~8.0.2": + version: 8.0.4 + resolution: "diff@npm:8.0.4" + checksum: 10c0/7ee5d03926db4039be7252ac3b0abaae1bd122a2ca971e5ca7270e444e36ff83dd906fad1a719740ca347e97ed5dc8f458a76a8391dbcd7aff363bdafb348a00 languageName: node linkType: hard @@ -23959,6 +25273,13 @@ __metadata: languageName: node linkType: hard +"dset@npm:^3.1.4": + version: 3.1.4 + resolution: "dset@npm:3.1.4" + checksum: 10c0/b67bbd28dd8a539e90c15ffb61100eb64ef995c5270a124d4f99bbb53f4d82f55a051b731ba81f3215dd9dce2b4c8d69927dc20b3be1c5fc88bab159467aa438 + languageName: node + linkType: hard + "dts-resolver@npm:^3.0.0": version: 3.0.0 resolution: "dts-resolver@npm:3.0.0" @@ -24109,6 +25430,16 @@ __metadata: languageName: node linkType: hard +"emmet@npm:^2.4.3": + version: 2.4.11 + resolution: "emmet@npm:2.4.11" + dependencies: + "@emmetio/abbreviation": "npm:^2.3.3" + "@emmetio/css-abbreviation": "npm:^2.1.8" + checksum: 10c0/4099d9d0d5dee766603c4ea03e1b87296bd397a0e8c6d8d5d6dcfdaad3e4581df5d48939a00eb4437dc08c83e857e231222ee037cb34ad63b1f2cce4041c6fc2 + languageName: node + linkType: hard + "emoji-mart@npm:5.6.0": version: 5.6.0 resolution: "emoji-mart@npm:5.6.0" @@ -24740,6 +26071,95 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.28.0": + version: 0.28.1 + resolution: "esbuild@npm:0.28.1" + dependencies: + "@esbuild/aix-ppc64": "npm:0.28.1" + "@esbuild/android-arm": "npm:0.28.1" + "@esbuild/android-arm64": "npm:0.28.1" + "@esbuild/android-x64": "npm:0.28.1" + "@esbuild/darwin-arm64": "npm:0.28.1" + "@esbuild/darwin-x64": "npm:0.28.1" + "@esbuild/freebsd-arm64": "npm:0.28.1" + "@esbuild/freebsd-x64": "npm:0.28.1" + "@esbuild/linux-arm": "npm:0.28.1" + "@esbuild/linux-arm64": "npm:0.28.1" + "@esbuild/linux-ia32": "npm:0.28.1" + "@esbuild/linux-loong64": "npm:0.28.1" + "@esbuild/linux-mips64el": "npm:0.28.1" + "@esbuild/linux-ppc64": "npm:0.28.1" + "@esbuild/linux-riscv64": "npm:0.28.1" + "@esbuild/linux-s390x": "npm:0.28.1" + "@esbuild/linux-x64": "npm:0.28.1" + "@esbuild/netbsd-arm64": "npm:0.28.1" + "@esbuild/netbsd-x64": "npm:0.28.1" + "@esbuild/openbsd-arm64": "npm:0.28.1" + "@esbuild/openbsd-x64": "npm:0.28.1" + "@esbuild/openharmony-arm64": "npm:0.28.1" + "@esbuild/sunos-x64": "npm:0.28.1" + "@esbuild/win32-arm64": "npm:0.28.1" + "@esbuild/win32-ia32": "npm:0.28.1" + "@esbuild/win32-x64": "npm:0.28.1" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/openharmony-arm64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10c0/29cd456a79ce35ac2c7e05fe871330416b2c395c045d849653f843e51378d6e0d6e774d6dcd01b35f4e83238a29bf8decd04fcd34b3780c589a250b21e5f92bb + languageName: node + linkType: hard + "escalade@npm:3.2.0, escalade@npm:^3.1.1, escalade@npm:^3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" @@ -25854,7 +27274,7 @@ __metadata: languageName: node linkType: hard -"eventemitter3@npm:^5.0.1": +"eventemitter3@npm:^5.0.1, eventemitter3@npm:^5.0.4": version: 5.0.4 resolution: "eventemitter3@npm:5.0.4" checksum: 10c0/575b8cac8d709e1473da46f8f15ef311b57ff7609445a7c71af5cd42598583eee6f098fa7a593e30f27e94b8865642baa0689e8fa97c016f742abdb3b1bf6d9a @@ -26106,6 +27526,18 @@ __metadata: languageName: node linkType: hard +"expressive-code@npm:^0.44.1": + version: 0.44.1 + resolution: "expressive-code@npm:0.44.1" + dependencies: + "@expressive-code/core": "npm:^0.44.1" + "@expressive-code/plugin-frames": "npm:^0.44.1" + "@expressive-code/plugin-shiki": "npm:^0.44.1" + "@expressive-code/plugin-text-markers": "npm:^0.44.1" + checksum: 10c0/ca09029718c687bfa494f095a0cc78b5981adc1e19d89a2740b2b7e07b326b205371018d894bfe8abe477cf842b015ef78367ecd575cbb8f2defd6d4910a7967 + languageName: node + linkType: hard + "exsolve@npm:^1.0.7": version: 1.0.8 resolution: "exsolve@npm:1.0.8" @@ -26239,6 +27671,22 @@ __metadata: languageName: node linkType: hard +"fast-string-truncated-width@npm:^3.0.2": + version: 3.0.3 + resolution: "fast-string-truncated-width@npm:3.0.3" + checksum: 10c0/043b8663397d14a3880ce4f3407bcda60b40db9bbeafe62863a35d1f9c69ea17c8da3fcd72de235553e6c9cd053128cde9e24ca0d4a7463208f48db3cd23d981 + languageName: node + linkType: hard + +"fast-string-width@npm:^3.0.2": + version: 3.0.2 + resolution: "fast-string-width@npm:3.0.2" + dependencies: + fast-string-truncated-width: "npm:^3.0.2" + checksum: 10c0/c8822d175315bb353ebe782b65214ac53b13e3bf704e03b132ea7bdfa8de6a636375b3ab7a4097545393d109381c37c4f387c72a462c90b61412dbc4632f39a7 + languageName: node + linkType: hard + "fast-unique-numbers@npm:^8.0.13": version: 8.0.13 resolution: "fast-unique-numbers@npm:8.0.13" @@ -26256,6 +27704,15 @@ __metadata: languageName: node linkType: hard +"fast-wrap-ansi@npm:^0.2.0": + version: 0.2.2 + resolution: "fast-wrap-ansi@npm:0.2.2" + dependencies: + fast-string-width: "npm:^3.0.2" + checksum: 10c0/1aa7be4f7cb86f4bdb14691cb6bcc0b8df8b3b89df142ade3ae1602332dcf6f990cd750a923cd581ca0847808cb4ec1aa5afaafa7a72f849e87a2a62c98fa370 + languageName: node + linkType: hard + "fast-xml-builder@npm:^1.1.5": version: 1.2.0 resolution: "fast-xml-builder@npm:1.2.0" @@ -26588,6 +28045,13 @@ __metadata: languageName: node linkType: hard +"flattie@npm:^1.1.1": + version: 1.1.1 + resolution: "flattie@npm:1.1.1" + checksum: 10c0/a8f8242c7af126cb2f1aa4a067af338fce609fc4c4df183c626fcc70a46c1878ce4aa88cd0dc8ef8f583ad4e7088a3b11ebeb6a62c9c97d75c0b1b0f08182ee3 + languageName: node + linkType: hard + "fn.name@npm:1.x.x": version: 1.1.0 resolution: "fn.name@npm:1.1.0" @@ -26605,6 +28069,15 @@ __metadata: languageName: node linkType: hard +"fontace@npm:~0.4.1": + version: 0.4.1 + resolution: "fontace@npm:0.4.1" + dependencies: + fontkitten: "npm:^1.0.2" + checksum: 10c0/2b3b8bc60192b0e9d87c369f1d72bf5da6c5b0f6055d5eeb0c06693455c55395f9592844273035399289f410202306c1d7f7329312e6e42a2694912d39c70709 + languageName: node + linkType: hard + "fontfaceobserver@npm:2.1.0": version: 2.1.0 resolution: "fontfaceobserver@npm:2.1.0" @@ -26612,6 +28085,15 @@ __metadata: languageName: node linkType: hard +"fontkitten@npm:^1.0.2, fontkitten@npm:^1.0.3": + version: 1.0.3 + resolution: "fontkitten@npm:1.0.3" + dependencies: + tiny-inflate: "npm:^1.0.3" + checksum: 10c0/edee7bde2f824a778cf401a2019ada63376fc8bcc5303d09a11e8a9245f35bd945934abea04aef1ca52e8dbb6ce3a17fe9ecdfa52cc0c92f56b25709fb865a76 + languageName: node + linkType: hard + "for-each@npm:^0.3.3, for-each@npm:^0.3.5": version: 0.3.5 resolution: "for-each@npm:0.3.5" @@ -27095,6 +28577,15 @@ __metadata: languageName: node linkType: hard +"get-tsconfig@npm:5.0.0-beta.4": + version: 5.0.0-beta.4 + resolution: "get-tsconfig@npm:5.0.0-beta.4" + dependencies: + resolve-pkg-maps: "npm:^1.0.0" + checksum: 10c0/6b908fc5d2abc937fca6938f5e9218238f49b11cd07362831383aa5815b25ce6f5fff36d4bcd70caa17778a54922b654745ce6c0b1bb19be8eac87da4e3fc087 + languageName: node + linkType: hard + "get-tsconfig@npm:5.0.0-beta.5": version: 5.0.0-beta.5 resolution: "get-tsconfig@npm:5.0.0-beta.5" @@ -27143,6 +28634,13 @@ __metadata: languageName: node linkType: hard +"github-slugger@npm:^2.0.0": + version: 2.0.0 + resolution: "github-slugger@npm:2.0.0" + checksum: 10c0/21b912b6b1e48f1e5a50b2292b48df0ff6abeeb0691b161b3d93d84f4ae6b1acd6ae23702e914af7ea5d441c096453cf0f621b72d57893946618d21dd1a1c486 + languageName: node + linkType: hard + "glob-parent@npm:6.0.2, glob-parent@npm:^6.0.1, glob-parent@npm:^6.0.2": version: 6.0.2 resolution: "glob-parent@npm:6.0.2" @@ -27730,6 +29228,23 @@ __metadata: languageName: node linkType: hard +"h3@npm:^1.15.10": + version: 1.15.11 + resolution: "h3@npm:1.15.11" + dependencies: + cookie-es: "npm:^1.2.3" + crossws: "npm:^0.3.5" + defu: "npm:^6.1.6" + destr: "npm:^2.0.5" + iron-webcrypto: "npm:^1.2.1" + node-mock-http: "npm:^1.0.4" + radix3: "npm:^1.1.2" + ufo: "npm:^1.6.3" + uncrypto: "npm:^0.1.3" + checksum: 10c0/6ccb421b9f92e02e6330c2b6697b18ef18e9550e4a1708f224ca517c40ecd201cd00967d0feb26e718595e86e985edec1755933cf8792d34fb8504f1c7cc261d + languageName: node + linkType: hard + "handle-thing@npm:^2.0.0": version: 2.0.1 resolution: "handle-thing@npm:2.0.1" @@ -27850,6 +29365,45 @@ __metadata: languageName: node linkType: hard +"hast-util-embedded@npm:^3.0.0": + version: 3.0.0 + resolution: "hast-util-embedded@npm:3.0.0" + dependencies: + "@types/hast": "npm:^3.0.0" + hast-util-is-element: "npm:^3.0.0" + checksum: 10c0/054c3d3b96fcd5c1d1c6f8d38ce1f7f33022ba6362129a022673d0b539f876acdcababbb9df29812fb927294f98ef7a2f44519a80d637fe3eea1819c9e69eeac + languageName: node + linkType: hard + +"hast-util-format@npm:^1.0.0": + version: 1.1.0 + resolution: "hast-util-format@npm:1.1.0" + dependencies: + "@types/hast": "npm:^3.0.0" + hast-util-embedded: "npm:^3.0.0" + hast-util-minify-whitespace: "npm:^1.0.0" + hast-util-phrasing: "npm:^3.0.0" + hast-util-whitespace: "npm:^3.0.0" + html-whitespace-sensitive-tag-names: "npm:^3.0.0" + unist-util-visit-parents: "npm:^6.0.0" + checksum: 10c0/6ab223cffe8a524ef4f2564d0385cab174a52551513b318496b3776e6882594eab4810b0f8d90f20e8291fa4e87fa068e03cba316d83c0836dab12dd140e98df + languageName: node + linkType: hard + +"hast-util-from-html@npm:^2.0.0, hast-util-from-html@npm:^2.0.3": + version: 2.0.3 + resolution: "hast-util-from-html@npm:2.0.3" + dependencies: + "@types/hast": "npm:^3.0.0" + devlop: "npm:^1.1.0" + hast-util-from-parse5: "npm:^8.0.0" + parse5: "npm:^7.0.0" + vfile: "npm:^6.0.0" + vfile-message: "npm:^4.0.0" + checksum: 10c0/993ef707c1a12474c8d4094fc9706a72826c660a7e308ea54c50ad893353d32e139b7cbc67510c2e82feac572b320e3b05aeb13d0f9c6302d61261f337b46764 + languageName: node + linkType: hard + "hast-util-from-parse5@npm:^8.0.0": version: 8.0.3 resolution: "hast-util-from-parse5@npm:8.0.3" @@ -27875,6 +29429,37 @@ __metadata: languageName: node linkType: hard +"hast-util-is-body-ok-link@npm:^3.0.0": + version: 3.0.1 + resolution: "hast-util-is-body-ok-link@npm:3.0.1" + dependencies: + "@types/hast": "npm:^3.0.0" + checksum: 10c0/c320cbd9a9a834b007a6f2f8c271e98b8331c0193adf06e0a7c5ea0acae664e97ce28eb4436e0658bc5cdb8f47390ec1c6cba7c4fe1ded10951fcdd1432f60bf + languageName: node + linkType: hard + +"hast-util-is-element@npm:^3.0.0": + version: 3.0.0 + resolution: "hast-util-is-element@npm:3.0.0" + dependencies: + "@types/hast": "npm:^3.0.0" + checksum: 10c0/f5361e4c9859c587ca8eb0d8343492f3077ccaa0f58a44cd09f35d5038f94d65152288dcd0c19336ef2c9491ec4d4e45fde2176b05293437021570aa0bc3613b + languageName: node + linkType: hard + +"hast-util-minify-whitespace@npm:^1.0.0": + version: 1.0.1 + resolution: "hast-util-minify-whitespace@npm:1.0.1" + dependencies: + "@types/hast": "npm:^3.0.0" + hast-util-embedded: "npm:^3.0.0" + hast-util-is-element: "npm:^3.0.0" + hast-util-whitespace: "npm:^3.0.0" + unist-util-is: "npm:^6.0.0" + checksum: 10c0/20a7d64947e080463084f444ad09c7f28c40e7648ca2d9c6c036e42a67f8e945d352560ff599304c988257c1e477abcf6a1f508c0900211fa58ec1ba21b36533 + languageName: node + linkType: hard + "hast-util-parse-selector@npm:^4.0.0": version: 4.0.0 resolution: "hast-util-parse-selector@npm:4.0.0" @@ -27884,6 +29469,19 @@ __metadata: languageName: node linkType: hard +"hast-util-phrasing@npm:^3.0.0": + version: 3.0.1 + resolution: "hast-util-phrasing@npm:3.0.1" + dependencies: + "@types/hast": "npm:^3.0.0" + hast-util-embedded: "npm:^3.0.0" + hast-util-has-property: "npm:^3.0.0" + hast-util-is-body-ok-link: "npm:^3.0.0" + hast-util-is-element: "npm:^3.0.0" + checksum: 10c0/d77e186ea3d7d62f6db9c4a55c3e6d9f1f6affd5f40250e8de9d73f167ae19fcc02fafe1601dfbe36e90f76ed5013ac004f0b6b398aee3a04a7a81de12788600 + languageName: node + linkType: hard + "hast-util-raw@npm:^9.0.0": version: 9.1.0 resolution: "hast-util-raw@npm:9.1.0" @@ -27905,7 +29503,7 @@ __metadata: languageName: node linkType: hard -"hast-util-select@npm:^6.0.0": +"hast-util-select@npm:^6.0.0, hast-util-select@npm:^6.0.2, hast-util-select@npm:^6.0.4": version: 6.0.4 resolution: "hast-util-select@npm:6.0.4" dependencies: @@ -27952,6 +29550,25 @@ __metadata: languageName: node linkType: hard +"hast-util-to-html@npm:^9.0.0, hast-util-to-html@npm:^9.0.1, hast-util-to-html@npm:^9.0.5": + version: 9.0.5 + resolution: "hast-util-to-html@npm:9.0.5" + dependencies: + "@types/hast": "npm:^3.0.0" + "@types/unist": "npm:^3.0.0" + ccount: "npm:^2.0.0" + comma-separated-tokens: "npm:^2.0.0" + hast-util-whitespace: "npm:^3.0.0" + html-void-elements: "npm:^3.0.0" + mdast-util-to-hast: "npm:^13.0.0" + property-information: "npm:^7.0.0" + space-separated-tokens: "npm:^2.0.0" + stringify-entities: "npm:^4.0.0" + zwitch: "npm:^2.0.4" + checksum: 10c0/b7a08c30bab4371fc9b4a620965c40b270e5ae7a8e94cf885f43b21705179e28c8e43b39c72885d1647965fb3738654e6962eb8b58b0c2a84271655b4d748836 + languageName: node + linkType: hard + "hast-util-to-jsx-runtime@npm:^2.0.0": version: 2.3.6 resolution: "hast-util-to-jsx-runtime@npm:2.3.6" @@ -27990,7 +29607,7 @@ __metadata: languageName: node linkType: hard -"hast-util-to-string@npm:^3.0.0": +"hast-util-to-string@npm:^3.0.0, hast-util-to-string@npm:^3.0.1": version: 3.0.1 resolution: "hast-util-to-string@npm:3.0.1" dependencies: @@ -27999,6 +29616,18 @@ __metadata: languageName: node linkType: hard +"hast-util-to-text@npm:^4.0.1, hast-util-to-text@npm:^4.0.2": + version: 4.0.2 + resolution: "hast-util-to-text@npm:4.0.2" + dependencies: + "@types/hast": "npm:^3.0.0" + "@types/unist": "npm:^3.0.0" + hast-util-is-element: "npm:^3.0.0" + unist-util-find-after: "npm:^5.0.0" + checksum: 10c0/93ecc10e68fe5391c6e634140eb330942e71dea2724c8e0c647c73ed74a8ec930a4b77043b5081284808c96f73f2bee64ee416038ece75a63a467e8d14f09946 + languageName: node + linkType: hard + "hast-util-whitespace@npm:^3.0.0": version: 3.0.0 resolution: "hast-util-whitespace@npm:3.0.0" @@ -28008,7 +29637,7 @@ __metadata: languageName: node linkType: hard -"hastscript@npm:^9.0.0": +"hastscript@npm:^9.0.0, hastscript@npm:^9.0.1": version: 9.0.1 resolution: "hastscript@npm:9.0.1" dependencies: @@ -28205,6 +29834,13 @@ __metadata: languageName: node linkType: hard +"html-escaper@npm:3.0.3": + version: 3.0.3 + resolution: "html-escaper@npm:3.0.3" + checksum: 10c0/a042fa4139127ff7546513e90ea39cc9161a1938ce90122dbc4260d4b7252c9aa8452f4509c0c2889901b8ae9a8699179150f1f99d3f80bcf7317573c5f08f4e + languageName: node + linkType: hard + "html-escaper@npm:^2.0.0, html-escaper@npm:^2.0.2": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -28337,6 +29973,13 @@ __metadata: languageName: node linkType: hard +"html-whitespace-sensitive-tag-names@npm:^3.0.0": + version: 3.0.1 + resolution: "html-whitespace-sensitive-tag-names@npm:3.0.1" + checksum: 10c0/da06cad111f6a432edd85c6cd09f6b5abbb385872fba79f23f939bdd4626920ac2e62507f604ef94eb8449902033bda292774624e3283b4dea4ed1620a2be3b2 + languageName: node + linkType: hard + "html@npm:^1.0.0": version: 1.0.0 resolution: "html@npm:1.0.0" @@ -28394,10 +30037,10 @@ __metadata: languageName: node linkType: hard -"http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.1": - version: 4.1.1 - resolution: "http-cache-semantics@npm:4.1.1" - checksum: 10c0/ce1319b8a382eb3cbb4a37c19f6bfe14e5bb5be3d09079e885e8c513ab2d3cd9214902f8a31c9dc4e37022633ceabfc2d697405deeaf1b8f3552bb4ed996fdfc +"http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.1, http-cache-semantics@npm:^4.2.0": + version: 4.2.0 + resolution: "http-cache-semantics@npm:4.2.0" + checksum: 10c0/45b66a945cf13ec2d1f29432277201313babf4a01d9e52f44b31ca923434083afeca03f18417f599c9ab3d0e7b618ceb21257542338b57c54b710463b4a53e37 languageName: node linkType: hard @@ -28658,6 +30301,18 @@ __metadata: languageName: node linkType: hard +"i18next@npm:^26.0.7": + version: 26.3.6 + resolution: "i18next@npm:26.3.6" + peerDependencies: + typescript: ^5 || ^6 || ^7 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/5920ac8fb6b647a2bdd439d121de04e5297246e52c4d95d13f5faae0824fd45b2faeb66038bcd61fc62362daa815f9c32fb992d6faa3787607d1dfe768e9a8b1 + languageName: node + linkType: hard + "iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3": version: 0.6.3 resolution: "iconv-lite@npm:0.6.3" @@ -29019,6 +30674,13 @@ __metadata: languageName: node linkType: hard +"iron-webcrypto@npm:^1.2.1": + version: 1.2.1 + resolution: "iron-webcrypto@npm:1.2.1" + checksum: 10c0/5cf27c6e2bd3ef3b4970e486235fd82491ab8229e2ed0ac23307c28d6c80d721772a86ed4e9fe2a5cabadd710c2f024b706843b40561fb83f15afee58f809f66 + languageName: node + linkType: hard + "is-absolute@npm:^1.0.0": version: 1.0.0 resolution: "is-absolute@npm:1.0.0" @@ -29213,6 +30875,15 @@ __metadata: languageName: node linkType: hard +"is-docker@npm:^4.0.0": + version: 4.0.0 + resolution: "is-docker@npm:4.0.0" + bin: + is-docker: cli.js + checksum: 10c0/4ee05c305b545422b172cab17f42900b940a5fa77820380d3244784fde69279d6881305f249b3b7fa2e261c2294804100548f5638880842e5b43df72cec29087 + languageName: node + linkType: hard + "is-electron@npm:^2.2.0": version: 2.2.2 resolution: "is-electron@npm:2.2.2" @@ -30194,7 +31865,7 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:4.3.1, js-yaml@npm:^4.1.0, js-yaml@npm:^4.1.1": +"js-yaml@npm:4.3.1, js-yaml@npm:^4.1.0, js-yaml@npm:^4.1.1, js-yaml@npm:^4.3.0": version: 4.3.1 resolution: "js-yaml@npm:4.3.1" dependencies: @@ -30459,6 +32130,20 @@ __metadata: languageName: node linkType: hard +"jsonc-parser@npm:^2.3.0": + version: 2.3.1 + resolution: "jsonc-parser@npm:2.3.1" + checksum: 10c0/b5e823612f6518a4d35e65d3c642e87b994c52a71b6d83d306d59f9b57003a1f6c64659808f0f1c3448991c28916d56faca45222f31ddb1a32effecdef0f0485 + languageName: node + linkType: hard + +"jsonc-parser@npm:^3.0.0, jsonc-parser@npm:^3.3.1": + version: 3.3.1 + resolution: "jsonc-parser@npm:3.3.1" + checksum: 10c0/269c3ae0a0e4f907a914bf334306c384aabb9929bd8c99f909275ebd5c2d3bc70b9bcd119ad794f339dec9f24b6a4ee9cd5a8ab2e6435e730ad4075388fc2ab6 + languageName: node + linkType: hard + "jsondiffpatch@npm:0.7.2": version: 0.7.2 resolution: "jsondiffpatch@npm:0.7.2" @@ -30613,7 +32298,7 @@ __metadata: languageName: node linkType: hard -"kleur@npm:4.1.5": +"kleur@npm:4.1.5, kleur@npm:^4.1.5": version: 4.1.5 resolution: "kleur@npm:4.1.5" checksum: 10c0/e9de6cb49657b6fa70ba2d1448fd3d691a5c4370d8f7bbf1c2f64c24d461270f2117e1b0afe8cb3114f13bbd8e51de158c2a224953960331904e636a5e4c0f2a @@ -30627,6 +32312,13 @@ __metadata: languageName: node linkType: hard +"klona@npm:^2.0.6": + version: 2.0.6 + resolution: "klona@npm:2.0.6" + checksum: 10c0/94eed2c6c2ce99f409df9186a96340558897b3e62a85afdc1ee39103954d2ebe1c1c4e9fe2b0952771771fa96d70055ede8b27962a7021406374fdb695fd4d01 + languageName: node + linkType: hard + "koa-compose@npm:^4.1.0": version: 4.1.0 resolution: "koa-compose@npm:4.1.0" @@ -30912,9 +32604,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-android-arm64@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-android-arm64@npm:1.32.0" +"lightningcss-android-arm64@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-android-arm64@npm:1.33.0" conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -30926,9 +32618,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-darwin-arm64@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-darwin-arm64@npm:1.32.0" +"lightningcss-darwin-arm64@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-darwin-arm64@npm:1.33.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -30940,9 +32632,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-darwin-x64@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-darwin-x64@npm:1.32.0" +"lightningcss-darwin-x64@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-darwin-x64@npm:1.33.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -30954,9 +32646,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-freebsd-x64@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-freebsd-x64@npm:1.32.0" +"lightningcss-freebsd-x64@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-freebsd-x64@npm:1.33.0" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard @@ -30968,9 +32660,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-linux-arm-gnueabihf@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-linux-arm-gnueabihf@npm:1.32.0" +"lightningcss-linux-arm-gnueabihf@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-linux-arm-gnueabihf@npm:1.33.0" conditions: os=linux & cpu=arm languageName: node linkType: hard @@ -30982,9 +32674,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-linux-arm64-gnu@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-linux-arm64-gnu@npm:1.32.0" +"lightningcss-linux-arm64-gnu@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-linux-arm64-gnu@npm:1.33.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard @@ -30996,9 +32688,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-linux-arm64-musl@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-linux-arm64-musl@npm:1.32.0" +"lightningcss-linux-arm64-musl@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-linux-arm64-musl@npm:1.33.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard @@ -31010,9 +32702,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-linux-x64-gnu@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-linux-x64-gnu@npm:1.32.0" +"lightningcss-linux-x64-gnu@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-linux-x64-gnu@npm:1.33.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard @@ -31024,9 +32716,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-linux-x64-musl@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-linux-x64-musl@npm:1.32.0" +"lightningcss-linux-x64-musl@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-linux-x64-musl@npm:1.33.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard @@ -31038,9 +32730,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-win32-arm64-msvc@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-win32-arm64-msvc@npm:1.32.0" +"lightningcss-win32-arm64-msvc@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-win32-arm64-msvc@npm:1.33.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -31052,9 +32744,9 @@ __metadata: languageName: node linkType: hard -"lightningcss-win32-x64-msvc@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-win32-x64-msvc@npm:1.32.0" +"lightningcss-win32-x64-msvc@npm:1.33.0": + version: 1.33.0 + resolution: "lightningcss-win32-x64-msvc@npm:1.33.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -31102,22 +32794,22 @@ __metadata: languageName: node linkType: hard -"lightningcss@npm:^1.32.0": - version: 1.32.0 - resolution: "lightningcss@npm:1.32.0" +"lightningcss@npm:^1.32.0, lightningcss@npm:^1.33.0": + version: 1.33.0 + resolution: "lightningcss@npm:1.33.0" dependencies: detect-libc: "npm:^2.0.3" - lightningcss-android-arm64: "npm:1.32.0" - lightningcss-darwin-arm64: "npm:1.32.0" - lightningcss-darwin-x64: "npm:1.32.0" - lightningcss-freebsd-x64: "npm:1.32.0" - lightningcss-linux-arm-gnueabihf: "npm:1.32.0" - lightningcss-linux-arm64-gnu: "npm:1.32.0" - lightningcss-linux-arm64-musl: "npm:1.32.0" - lightningcss-linux-x64-gnu: "npm:1.32.0" - lightningcss-linux-x64-musl: "npm:1.32.0" - lightningcss-win32-arm64-msvc: "npm:1.32.0" - lightningcss-win32-x64-msvc: "npm:1.32.0" + lightningcss-android-arm64: "npm:1.33.0" + lightningcss-darwin-arm64: "npm:1.33.0" + lightningcss-darwin-x64: "npm:1.33.0" + lightningcss-freebsd-x64: "npm:1.33.0" + lightningcss-linux-arm-gnueabihf: "npm:1.33.0" + lightningcss-linux-arm64-gnu: "npm:1.33.0" + lightningcss-linux-arm64-musl: "npm:1.33.0" + lightningcss-linux-x64-gnu: "npm:1.33.0" + lightningcss-linux-x64-musl: "npm:1.33.0" + lightningcss-win32-arm64-msvc: "npm:1.33.0" + lightningcss-win32-x64-msvc: "npm:1.33.0" dependenciesMeta: lightningcss-android-arm64: optional: true @@ -31141,7 +32833,7 @@ __metadata: optional: true lightningcss-win32-x64-msvc: optional: true - checksum: 10c0/70945bd55097af46fc9fab7f5ed09cd5869d85940a2acab7ee06d0117004a1d68155708a2d462531cea2fc3c67aefc9333a7068c80b0b78dd404c16838809e03 + checksum: 10c0/ce1f8279fbae636dbf37fa6e7385d5f98ed881d72af3362f24afbd4685e19c1fcdfecf17e5dd77f2ebee3d0c23ade276230d85842d07292229a2cffba8ff20a3 languageName: node linkType: hard @@ -31611,10 +33303,10 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1": - version: 11.2.6 - resolution: "lru-cache@npm:11.2.6" - checksum: 10c0/73bbffb298760e71b2bfe8ebc16a311c6a60ceddbba919cfedfd8635c2d125fbfb5a39b71818200e67973b11f8d59c5a9e31d6f90722e340e90393663a66e5cd +"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1, lru-cache@npm:^11.2.7": + version: 11.5.2 + resolution: "lru-cache@npm:11.5.2" + checksum: 10c0/ece1ad731f5b655e85d67047d04bfc13823dc77aa61c5454924a9869ba600a0104e39cc33d726021feef812bc347ca34a5608a5eb1972a5dd0870b7ecd42c3f2 languageName: node linkType: hard @@ -31693,6 +33385,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^1.0.0": + version: 1.1.0 + resolution: "magic-string@npm:1.1.0" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.5" + checksum: 10c0/f904dee2fd190ee5450c754234d8a831228677e9092ed2e545e42746beecf18d617156eb911af55a14496ebe1e60179678c4080c919f74eca7738eba3f38f13a + languageName: node + linkType: hard + "magicast@npm:^0.5.2": version: 0.5.3 resolution: "magicast@npm:0.5.3" @@ -31853,6 +33554,34 @@ __metadata: languageName: node linkType: hard +"mdast-util-definitions@npm:^6.0.0": + version: 6.0.0 + resolution: "mdast-util-definitions@npm:6.0.0" + dependencies: + "@types/mdast": "npm:^4.0.0" + "@types/unist": "npm:^3.0.0" + unist-util-visit: "npm:^5.0.0" + checksum: 10c0/a2e0e51122a3eff4f35379de2c50ee3d8a89bea58488a390b1b40ada95727eb769f87d4bc885e5935d61820d19e0567bc047876db302a2139f3a29668b612b80 + languageName: node + linkType: hard + +"mdast-util-directive@npm:^3.0.0, mdast-util-directive@npm:^3.1.0": + version: 3.1.0 + resolution: "mdast-util-directive@npm:3.1.0" + dependencies: + "@types/mdast": "npm:^4.0.0" + "@types/unist": "npm:^3.0.0" + ccount: "npm:^2.0.0" + devlop: "npm:^1.0.0" + mdast-util-from-markdown: "npm:^2.0.0" + mdast-util-to-markdown: "npm:^2.0.0" + parse-entities: "npm:^4.0.0" + stringify-entities: "npm:^4.0.0" + unist-util-visit-parents: "npm:^6.0.0" + checksum: 10c0/596b093b940197cf43af4d0de12e82a1d2b1eb5add73dd16077aa80e0d0e1f208ea642c420726e59ccd352c193d6ecd5c106d6fab769f252617c75333f91a314 + languageName: node + linkType: hard + "mdast-util-find-and-replace@npm:^3.0.0": version: 3.0.1 resolution: "mdast-util-find-and-replace@npm:3.0.1" @@ -32050,7 +33779,7 @@ __metadata: languageName: node linkType: hard -"mdast-util-to-markdown@npm:^2.0.0": +"mdast-util-to-markdown@npm:^2.0.0, mdast-util-to-markdown@npm:^2.1.2": version: 2.1.2 resolution: "mdast-util-to-markdown@npm:2.1.2" dependencies: @@ -32090,6 +33819,13 @@ __metadata: languageName: node linkType: hard +"mdn-data@npm:2.27.1": + version: 2.27.1 + resolution: "mdn-data@npm:2.27.1" + checksum: 10c0/eb8abf5d22e4d1e090346f5e81b67d23cef14c83940e445da5c44541ad874dc8fb9f6ca236e8258c3a489d9fb5884188a4d7d58773adb9089ac2c0b966796393 + languageName: node + linkType: hard + "media-typer@npm:0.3.0": version: 0.3.0 resolution: "media-typer@npm:0.3.0" @@ -32268,6 +34004,21 @@ __metadata: languageName: node linkType: hard +"micromark-extension-directive@npm:^4.0.0": + version: 4.0.0 + resolution: "micromark-extension-directive@npm:4.0.0" + dependencies: + devlop: "npm:^1.0.0" + micromark-factory-space: "npm:^2.0.0" + micromark-factory-whitespace: "npm:^2.0.0" + micromark-util-character: "npm:^2.0.0" + micromark-util-symbol: "npm:^2.0.0" + micromark-util-types: "npm:^2.0.0" + parse-entities: "npm:^4.0.0" + checksum: 10c0/b4aef0f44339543466ae186130a4514985837b6b12d0c155bd1162e740f631e58f0883a39d0c723206fa0ff53a9b579965c79116f902236f6f123c3340b5fefb + languageName: node + linkType: hard + "micromark-extension-gfm-autolink-literal@npm:^2.0.0": version: 2.1.0 resolution: "micromark-extension-gfm-autolink-literal@npm:2.1.0" @@ -33150,10 +34901,10 @@ __metadata: languageName: node linkType: hard -"mrmime@npm:^2.0.0": - version: 2.0.0 - resolution: "mrmime@npm:2.0.0" - checksum: 10c0/312b35ed288986aec90955410b21ed7427fd1e4ee318cb5fc18765c8d029eeded9444faa46589e5b1ed6b35fb2054a802ac8dcb917ddf6b3e189cb3bf11a965c +"mrmime@npm:^2.0.0, mrmime@npm:^2.0.1": + version: 2.0.1 + resolution: "mrmime@npm:2.0.1" + checksum: 10c0/af05afd95af202fdd620422f976ad67dc18e6ee29beb03dd1ce950ea6ef664de378e44197246df4c7cdd73d47f2e7143a6e26e473084b9e4aa2095c0ad1e1761 languageName: node linkType: hard @@ -33462,6 +35213,13 @@ __metadata: languageName: node linkType: hard +"neotraverse@npm:^1.0.1": + version: 1.0.1 + resolution: "neotraverse@npm:1.0.1" + checksum: 10c0/98e5d2d1974f91d527851a9dcfceb62701a1d31970b324b9f35fbdd244d173edc7a379ea1773f64771a9dc282486e3d3b693b24e14b1e969291612b2ff6b5054 + languageName: node + linkType: hard + "netmask@npm:^2.0.2": version: 2.0.2 resolution: "netmask@npm:2.0.2" @@ -33574,6 +35332,15 @@ __metadata: languageName: node linkType: hard +"nlcst-to-string@npm:^4.0.0": + version: 4.0.0 + resolution: "nlcst-to-string@npm:4.0.0" + dependencies: + "@types/nlcst": "npm:^2.0.0" + checksum: 10c0/a192c8b3365a7c076812004e72ae5b4a1734e582be2a6f3c062f3beecf18868a9fe2d1bad870bfead320fb39830f2c4f3752e5ae6574c4e59157126fd1ddba70 + languageName: node + linkType: hard + "no-case@npm:^3.0.4": version: 3.0.4 resolution: "no-case@npm:3.0.4" @@ -33618,6 +35385,13 @@ __metadata: languageName: node linkType: hard +"node-fetch-native@npm:^1.6.7": + version: 1.6.7 + resolution: "node-fetch-native@npm:1.6.7" + checksum: 10c0/8b748300fb053d21ca4d3db9c3ff52593d5e8f8a2d9fe90cbfad159676e324b954fdaefab46aeca007b5b9edab3d150021c4846444e4e8ab1f4e44cd3807be87 + languageName: node + linkType: hard + "node-fetch@npm:2.7.0, node-fetch@npm:^2.5.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" @@ -33676,6 +35450,13 @@ __metadata: languageName: node linkType: hard +"node-mock-http@npm:^1.0.4": + version: 1.0.5 + resolution: "node-mock-http@npm:1.0.5" + checksum: 10c0/14a36b28423953af95e4659aa1e07208b5a38daaa89c1aac28a193f5420744a2511eb288503019b2fc8de582c70612d2e475d0df1d1a0e44fe3dfd7fc8f5399c + languageName: node + linkType: hard + "node-readable-to-web-readable-stream@npm:^0.4.2": version: 0.4.2 resolution: "node-readable-to-web-readable-stream@npm:0.4.2" @@ -34020,6 +35801,17 @@ __metadata: languageName: node linkType: hard +"ofetch@npm:^1.5.1": + version: 1.5.1 + resolution: "ofetch@npm:1.5.1" + dependencies: + destr: "npm:^2.0.5" + node-fetch-native: "npm:^1.6.7" + ufo: "npm:^1.6.1" + checksum: 10c0/97ebc600512ea0ab401e97c73313218cc53c9b530b32ec8c995c347b0c68887129993168d1753f527761a64c6f93a5d823ce1378ccec95fc65a606f323a79a6c + languageName: node + linkType: hard + "officeparser@npm:6.0.7": version: 6.0.7 resolution: "officeparser@npm:6.0.7" @@ -34036,6 +35828,13 @@ __metadata: languageName: node linkType: hard +"ohash@npm:^2.0.11": + version: 2.0.11 + resolution: "ohash@npm:2.0.11" + checksum: 10c0/d07c8d79cc26da082c1a7c8d5b56c399dd4ed3b2bd069fcae6bae78c99a9bcc3ad813b1e1f49ca2f335292846d689c6141a762cf078727d2302a33d414e69c79 + languageName: node + linkType: hard + "on-finished@npm:^2.3.0, on-finished@npm:^2.4.1, on-finished@npm:~2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -34088,6 +35887,24 @@ __metadata: languageName: node linkType: hard +"oniguruma-parser@npm:^0.12.2": + version: 0.12.2 + resolution: "oniguruma-parser@npm:0.12.2" + checksum: 10c0/fe5255d2cd5a6b845d5a0abe1725898ef40cea5522290dba6ccc08cc388891e9e8007af4baa5059942b786ad44b2b677ef25948039c9ba3f057bd35d2c52076a + languageName: node + linkType: hard + +"oniguruma-to-es@npm:^4.3.6": + version: 4.3.6 + resolution: "oniguruma-to-es@npm:4.3.6" + dependencies: + oniguruma-parser: "npm:^0.12.2" + regex: "npm:^6.1.0" + regex-recursion: "npm:^6.0.2" + checksum: 10c0/044e08b98e706987c2882ccf228de2a671de4aff33e3bd370da137ba599c0d520549625ad26ecc8898502445c991e19f106f584e94382262ba2af02c908ca3cf + languageName: node + linkType: hard + "only@npm:~0.0.2": version: 0.0.2 resolution: "only@npm:0.0.2" @@ -34551,6 +36368,15 @@ __metadata: languageName: node linkType: hard +"p-limit@npm:^7.3.0": + version: 7.3.1 + resolution: "p-limit@npm:7.3.1" + dependencies: + yocto-queue: "npm:^1.2.1" + checksum: 10c0/49b7d7fcc244d4659fa3b260c7b4e632bc72333c60e76e4cd24c7befdeb9b8e22da644e5064e83df9e2ded75245e3e3b9df0e9cf8500b08db8ebf58ac18c6764 + languageName: node + linkType: hard + "p-locate@npm:^3.0.0": version: 3.0.0 resolution: "p-locate@npm:3.0.0" @@ -34592,13 +36418,13 @@ __metadata: languageName: node linkType: hard -"p-queue@npm:^9.0.0": - version: 9.0.0 - resolution: "p-queue@npm:9.0.0" +"p-queue@npm:^9.0.0, p-queue@npm:^9.1.0": + version: 9.3.3 + resolution: "p-queue@npm:9.3.3" dependencies: - eventemitter3: "npm:^5.0.1" + eventemitter3: "npm:^5.0.4" p-timeout: "npm:^7.0.0" - checksum: 10c0/0f27fcbec9e4e02f34cc3660f14b5746798c51a2425dc3d3c5238924c34726b1580606d5d1432fc05304e2350c00b112ded03eb43c1b49de7791a7150fbfcb4e + checksum: 10c0/0d88a35cb3301bee2aac654c0a7018a2e40775fa597f3a06da66f33b0311d106bebbe9732c8e5968ae1e0e0b49f79c874333d2a7b0a57fad469189f236c066d7 languageName: node linkType: hard @@ -34748,6 +36574,45 @@ __metadata: languageName: node linkType: hard +"package-manager-detector@npm:^1.6.0": + version: 1.8.0 + resolution: "package-manager-detector@npm:1.8.0" + checksum: 10c0/4c8e2c47fdc874465d3b3b11934401db9d73470ccbdabeb092e46cb94abbc491d5befc8c271b5ea1ae9c6a881f44c2997e011873365bde9fd6c3dc001221ff7a + languageName: node + linkType: hard + +"pagefind@npm:^1.5.2": + version: 1.5.2 + resolution: "pagefind@npm:1.5.2" + dependencies: + "@pagefind/darwin-arm64": "npm:1.5.2" + "@pagefind/darwin-x64": "npm:1.5.2" + "@pagefind/freebsd-x64": "npm:1.5.2" + "@pagefind/linux-arm64": "npm:1.5.2" + "@pagefind/linux-x64": "npm:1.5.2" + "@pagefind/windows-arm64": "npm:1.5.2" + "@pagefind/windows-x64": "npm:1.5.2" + dependenciesMeta: + "@pagefind/darwin-arm64": + optional: true + "@pagefind/darwin-x64": + optional: true + "@pagefind/freebsd-x64": + optional: true + "@pagefind/linux-arm64": + optional: true + "@pagefind/linux-x64": + optional: true + "@pagefind/windows-arm64": + optional: true + "@pagefind/windows-x64": + optional: true + bin: + pagefind: lib/runner/bin.cjs + checksum: 10c0/bcc4f34f473bfeb09dc4cb464a3547bf958e11ee4dee4144ec7a364bd74b9f7c21f85be71e388b66d30923a9a453dcaed2f6b425f3e67631cadc9650ba741aab + languageName: node + linkType: hard + "pako@npm:^0.2.5": version: 0.2.9 resolution: "pako@npm:0.2.9" @@ -34867,6 +36732,20 @@ __metadata: languageName: node linkType: hard +"parse-latin@npm:^7.0.0": + version: 7.0.0 + resolution: "parse-latin@npm:7.0.0" + dependencies: + "@types/nlcst": "npm:^2.0.0" + "@types/unist": "npm:^3.0.0" + nlcst-to-string: "npm:^4.0.0" + unist-util-modify-children: "npm:^4.0.0" + unist-util-visit-children: "npm:^3.0.0" + vfile: "npm:^6.0.0" + checksum: 10c0/4232a464f98c41c6680575c54bc2c9b21ac4b82a1f796a871bfef5efa6eddaab9bccf734b08cde6b0a5504ef46a0a14041ddd0bc5d9cc70f73a507f93f610596 + languageName: node + linkType: hard + "parse-ms@npm:^4.0.0": version: 4.0.0 resolution: "parse-ms@npm:4.0.0" @@ -35267,6 +37146,13 @@ __metadata: languageName: node linkType: hard +"piccolore@npm:^0.1.3": + version: 0.1.3 + resolution: "piccolore@npm:0.1.3" + checksum: 10c0/999666bb32eccc96a26b0cf3b8afe72f9d4cd4ca0eab5802b404cc84c601d81db4485b5feee5c50dbc8436ba6e1bc6a2310f99c18cee6687e430cc0eb4a36470 + languageName: node + linkType: hard + "picocolors@npm:1.1.1, picocolors@npm:^1.0.0, picocolors@npm:^1.1.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" @@ -35576,6 +37462,17 @@ __metadata: languageName: node linkType: hard +"postcss-nested@npm:^6.0.1": + version: 6.2.0 + resolution: "postcss-nested@npm:6.2.0" + dependencies: + postcss-selector-parser: "npm:^6.1.1" + peerDependencies: + postcss: ^8.2.14 + checksum: 10c0/7f9c3f2d764191a39364cbdcec350f26a312431a569c9ef17408021424726b0d67995ff5288405e3724bb7152a4c92f73c027e580ec91e798800ed3c52e2bc6e + languageName: node + linkType: hard + "postcss-normalize-whitespace@npm:7.0.1": version: 7.0.1 resolution: "postcss-normalize-whitespace@npm:7.0.1" @@ -35597,6 +37494,16 @@ __metadata: languageName: node linkType: hard +"postcss-selector-parser@npm:^6.1.1": + version: 6.1.4 + resolution: "postcss-selector-parser@npm:6.1.4" + dependencies: + cssesc: "npm:^3.0.0" + util-deprecate: "npm:^1.0.2" + checksum: 10c0/996f3290dee08ecb073d5f396d1134e619494cfd83140aec07a29618ee1e76d370769a8959d4c0cfefb24aa96dcffa4e7c4937dd881e03b735d50cba959ceb19 + languageName: node + linkType: hard + "postcss-value-parser@npm:^4.1.0, postcss-value-parser@npm:^4.2.0": version: 4.2.0 resolution: "postcss-value-parser@npm:4.2.0" @@ -35604,7 +37511,7 @@ __metadata: languageName: node linkType: hard -"postcss@npm:8.5.26, postcss@npm:^8.3.11, postcss@npm:^8.4.14, postcss@npm:^8.4.33, postcss@npm:^8.5.15, postcss@npm:^8.5.16, postcss@npm:^8.5.3, postcss@npm:^8.5.6": +"postcss@npm:8.5.26, postcss@npm:^8.3.11, postcss@npm:^8.4.14, postcss@npm:^8.4.33, postcss@npm:^8.4.38, postcss@npm:^8.5.15, postcss@npm:^8.5.23, postcss@npm:^8.5.3, postcss@npm:^8.5.6": version: 8.5.26 resolution: "postcss@npm:8.5.26" dependencies: @@ -35692,12 +37599,12 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^3.2.5": - version: 3.7.4 - resolution: "prettier@npm:3.7.4" +"prettier@npm:^3.2.5, prettier@npm:^3.8.1": + version: 3.9.6 + resolution: "prettier@npm:3.9.6" bin: prettier: bin/prettier.cjs - checksum: 10c0/9675d2cd08eacb1faf1d1a2dbfe24bfab6a912b059fc9defdb380a408893d88213e794a40a2700bd29b140eb3172e0b07c852853f6e22f16f3374659a1a13389 + checksum: 10c0/9f7ddae234035868f3daab3dc34e6de7e54622185afb017378029f0c09a9c023248ccf6f34def0b17a0538e18c47aa1b3188bab72965d1bf735919baa0747e99 languageName: node linkType: hard @@ -35757,6 +37664,13 @@ __metadata: languageName: node linkType: hard +"process-ancestry@npm:^0.1.0": + version: 0.1.0 + resolution: "process-ancestry@npm:0.1.0" + checksum: 10c0/3b20263db7c2eeb567efcef21bf4b3f3363c4bf7041796943fd8a467f888d94eb093a022b8fcc49c8d843f3fc81d15047255d4c2b4990f2d5a4f851f3c5c9cd4 + languageName: node + linkType: hard + "process-nextick-args@npm:^2.0.1, process-nextick-args@npm:~2.0.0": version: 2.0.1 resolution: "process-nextick-args@npm:2.0.1" @@ -36280,6 +38194,13 @@ __metadata: languageName: node linkType: hard +"radix3@npm:^1.1.2": + version: 1.1.2 + resolution: "radix3@npm:1.1.2" + checksum: 10c0/d4a295547f71af079868d2c2ed3814a9296ee026c5488212d58c106e6b4797c6eaec1259b46c9728913622f2240c9a944bfc8e2b3b5f6e4a5045338b1609f1e4 + languageName: node + linkType: hard + "raf-schd@npm:^4.0.2": version: 4.0.3 resolution: "raf-schd@npm:4.0.3" @@ -36696,7 +38617,7 @@ __metadata: languageName: node linkType: hard -"react-refresh@npm:0.18.0": +"react-refresh@npm:0.18.0, react-refresh@npm:^0.18.0": version: 0.18.0 resolution: "react-refresh@npm:0.18.0" checksum: 10c0/34a262f7fd803433a534f50deb27a148112a81adcae440c7d1cbae7ef14d21ea8f2b3d783e858cb7698968183b77755a38b4d4b5b1d79b4f4689c2f6d358fff2 @@ -37033,6 +38954,13 @@ __metadata: languageName: node linkType: hard +"readdirp@npm:^5.0.0": + version: 5.0.0 + resolution: "readdirp@npm:5.0.0" + checksum: 10c0/faf1ec57cff2020f473128da3f8d2a57813cc3a08a36c38cae1c9af32c1579906cc50ba75578043b35bade77e945c098233665797cf9730ba3613a62d6e79219 + languageName: node + linkType: hard + "readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -37282,6 +39210,31 @@ __metadata: languageName: node linkType: hard +"regex-recursion@npm:^6.0.2": + version: 6.0.2 + resolution: "regex-recursion@npm:6.0.2" + dependencies: + regex-utilities: "npm:^2.3.0" + checksum: 10c0/68e8b6889680e904b75d7f26edaf70a1a4dc1087406bff53face4c2929d918fd77c72223843fe816ac8ed9964f96b4160650e8d5909e26a998c6e9de324dadb1 + languageName: node + linkType: hard + +"regex-utilities@npm:^2.3.0": + version: 2.3.0 + resolution: "regex-utilities@npm:2.3.0" + checksum: 10c0/78c550a80a0af75223244fff006743922591bd8f61d91fef7c86b9b56cf9bbf8ee5d7adb6d8991b5e304c57c90103fc4818cf1e357b11c6c669b782839bd7893 + languageName: node + linkType: hard + +"regex@npm:^6.1.0": + version: 6.1.0 + resolution: "regex@npm:6.1.0" + dependencies: + regex-utilities: "npm:^2.3.0" + checksum: 10c0/6e0ee2a1c17d5a66dc1120dfc51899dedf6677857e83a0df4d5a822ebb8645a54a079772efc1ade382b67aad35e4e22b5bd2d33c05ed28b0e000f8f57eb0aec1 + languageName: node + linkType: hard + "regexp-ast-analysis@npm:^0.7.0, regexp-ast-analysis@npm:^0.7.1": version: 0.7.1 resolution: "regexp-ast-analysis@npm:0.7.1" @@ -37396,6 +39349,36 @@ __metadata: languageName: node linkType: hard +"rehype-expressive-code@npm:^0.44.1": + version: 0.44.1 + resolution: "rehype-expressive-code@npm:0.44.1" + dependencies: + expressive-code: "npm:^0.44.1" + checksum: 10c0/7139dcb93dfc479225612ac51d1d7ad9b16e295eadbae4fecd697ab5f6f497dfb2d03317dd7b52a4a71691a8a88b7f23a7d0e216195ca6065b761a4958ae33c9 + languageName: node + linkType: hard + +"rehype-format@npm:^5.0.1": + version: 5.0.1 + resolution: "rehype-format@npm:5.0.1" + dependencies: + "@types/hast": "npm:^3.0.0" + hast-util-format: "npm:^1.0.0" + checksum: 10c0/e87aac3e318ef96688785e108315b23762681f1a834ae52ac449b9787ff63e8435aceb70354fb629d2d733daca4f65889ddcdf0cd44b684ea4f481e8fac750e3 + languageName: node + linkType: hard + +"rehype-parse@npm:^9.0.0": + version: 9.0.1 + resolution: "rehype-parse@npm:9.0.1" + dependencies: + "@types/hast": "npm:^3.0.0" + hast-util-from-html: "npm:^2.0.0" + unified: "npm:^11.0.0" + checksum: 10c0/efa9ca17673fe70e2d322a1d262796bbed5f6a89382f8f8393352bbd6f6bbf1d4d1d050984b86ff9cb6c0fa2535175ab0829e53c94b1e38fc3c158e6c0ad90bc + languageName: node + linkType: hard + "rehype-raw@npm:^7.0.0": version: 7.0.0 resolution: "rehype-raw@npm:7.0.0" @@ -37418,6 +39401,29 @@ __metadata: languageName: node linkType: hard +"rehype-stringify@npm:^10.0.0, rehype-stringify@npm:^10.0.1": + version: 10.0.1 + resolution: "rehype-stringify@npm:10.0.1" + dependencies: + "@types/hast": "npm:^3.0.0" + hast-util-to-html: "npm:^9.0.0" + unified: "npm:^11.0.0" + checksum: 10c0/c643ae3a4862465033e0f1e9f664433767279b4ee9296570746970a79940417ec1fb1997a513659aab97063cf971c5d97e0af8129f590719f01628c8aa480765 + languageName: node + linkType: hard + +"rehype@npm:^13.0.2": + version: 13.0.2 + resolution: "rehype@npm:13.0.2" + dependencies: + "@types/hast": "npm:^3.0.0" + rehype-parse: "npm:^9.0.0" + rehype-stringify: "npm:^10.0.0" + unified: "npm:^11.0.0" + checksum: 10c0/13d82086b673b3ce1fddb54cc8d30be16bde83fb62f1507f0af06070c94b85d07c3780fa994357bad2c9d51b84e4108ff661677b71d187e4f2167cab22d84363 + languageName: node + linkType: hard + "reinterval@npm:^1.1.0": version: 1.1.0 resolution: "reinterval@npm:1.1.0" @@ -37432,7 +39438,19 @@ __metadata: languageName: node linkType: hard -"remark-gfm@npm:4.0.1, remark-gfm@npm:^4.0.0": +"remark-directive@npm:^4.0.0": + version: 4.0.0 + resolution: "remark-directive@npm:4.0.0" + dependencies: + "@types/mdast": "npm:^4.0.0" + mdast-util-directive: "npm:^3.0.0" + micromark-extension-directive: "npm:^4.0.0" + unified: "npm:^11.0.0" + checksum: 10c0/ab1f71ae220913f62833464d483f29de7b4af0dcf1ff626d33cdb5dd81eda1c553a11f779de9fd9b641802586c5f070175824d70f14d53e451a484850705d53a + languageName: node + linkType: hard + +"remark-gfm@npm:4.0.1, remark-gfm@npm:^4.0.0, remark-gfm@npm:^4.0.1": version: 4.0.1 resolution: "remark-gfm@npm:4.0.1" dependencies: @@ -37468,16 +39486,28 @@ __metadata: languageName: node linkType: hard -"remark-rehype@npm:^11.0.0": - version: 11.1.1 - resolution: "remark-rehype@npm:11.1.1" +"remark-rehype@npm:^11.0.0, remark-rehype@npm:^11.1.2": + version: 11.1.2 + resolution: "remark-rehype@npm:11.1.2" dependencies: "@types/hast": "npm:^3.0.0" "@types/mdast": "npm:^4.0.0" mdast-util-to-hast: "npm:^13.0.0" unified: "npm:^11.0.0" vfile: "npm:^6.0.0" - checksum: 10c0/68f986e8ee758d415e93babda2a0d89477c15b7c200edc23b8b1d914dd6e963c5fc151a11cbbbcfa7dd237367ff3ef86e302be90f31f37a17b0748668bd8c65b + checksum: 10c0/f9eccacfb596d9605581dc05bfad28635d6ded5dd0a18e88af5fd4df0d3fcf9612e1501d4513bc2164d833cfe9636dab20400080b09e53f155c6e1442a1231fb + languageName: node + linkType: hard + +"remark-smartypants@npm:^3.0.2": + version: 3.0.3 + resolution: "remark-smartypants@npm:3.0.3" + dependencies: + retext: "npm:^9.0.0" + retext-smartypants: "npm:^6.0.0" + unified: "npm:^11.0.4" + unist-util-visit: "npm:^5.0.0" + checksum: 10c0/24f210e64d44b13559aa6c7423b29a0a3b7fb1b603c3b997c70ad923e1bf1cebff9f234db801c689c02ff5bebdfd5256481c6690928a2465c09cd54a03076514 languageName: node linkType: hard @@ -37533,6 +39563,20 @@ __metadata: languageName: node linkType: hard +"request-light@npm:^0.5.7": + version: 0.5.8 + resolution: "request-light@npm:0.5.8" + checksum: 10c0/65d47f79cbe13c46c10eecb9264ad06ea5f76ea64855766448937c0fe888a70b9c854bf9dc4c2dc1949ea5400030b55bea7d4ba0807fae0202004cee796ea819 + languageName: node + linkType: hard + +"request-light@npm:^0.7.0": + version: 0.7.0 + resolution: "request-light@npm:0.7.0" + checksum: 10c0/1c98f0d74b8a28a6fecb96d4b30c255a27b402f5e10b2696b67ede7257389a7533fba9e8ea122ccec9a7c6f06db36eab5d0b563bd5ff27b15aaa9d3910e31894 + languageName: node + linkType: hard + "require-directory@npm:^2.1.1": version: 2.1.1 resolution: "require-directory@npm:2.1.1" @@ -37714,6 +39758,51 @@ __metadata: languageName: node linkType: hard +"retext-latin@npm:^4.0.0": + version: 4.0.0 + resolution: "retext-latin@npm:4.0.0" + dependencies: + "@types/nlcst": "npm:^2.0.0" + parse-latin: "npm:^7.0.0" + unified: "npm:^11.0.0" + checksum: 10c0/51530be66db9ef6ab8e9cda5dd0598377ff4321481d6a941bf70dac16fa6e9123ff7d8ff093a05c30a3e00e282e37094b845b6130a8005a3cb7186a961ab99cb + languageName: node + linkType: hard + +"retext-smartypants@npm:^6.0.0, retext-smartypants@npm:^6.2.0": + version: 6.2.0 + resolution: "retext-smartypants@npm:6.2.0" + dependencies: + "@types/nlcst": "npm:^2.0.0" + nlcst-to-string: "npm:^4.0.0" + unist-util-visit: "npm:^5.0.0" + checksum: 10c0/36f925353dd7f31df642bca2493524a8daee15f9b0e0dfe7fb8982462d23ccb12a99864989db22f0bacb6d7fea1f696ba96e031d3fbac4f013e1c95ef3fed881 + languageName: node + linkType: hard + +"retext-stringify@npm:^4.0.0": + version: 4.0.0 + resolution: "retext-stringify@npm:4.0.0" + dependencies: + "@types/nlcst": "npm:^2.0.0" + nlcst-to-string: "npm:^4.0.0" + unified: "npm:^11.0.0" + checksum: 10c0/eb2930356c85999a8978092a5d6ba3695fea859c71f221dcdc485704552922641bc17e50fea2ae0599d665192eaad002e98bb4236ecac94a570b73581b99004d + languageName: node + linkType: hard + +"retext@npm:^9.0.0": + version: 9.0.0 + resolution: "retext@npm:9.0.0" + dependencies: + "@types/nlcst": "npm:^2.0.0" + retext-latin: "npm:^4.0.0" + retext-stringify: "npm:^4.0.0" + unified: "npm:^11.0.0" + checksum: 10c0/eee9f66ff6fae5670a5eeccc0b5e2639112f868475273ce307d3079cfe7deb9d1b0f2b8fa28b4ab30abaf8538345185a44908f461a27bbf43c4f94feda90ecac + languageName: node + linkType: hard + "retry-request@npm:^7.0.0": version: 7.0.2 resolution: "retry-request@npm:7.0.2" @@ -37883,7 +39972,7 @@ __metadata: languageName: node linkType: hard -"rolldown@npm:~1.1.2, rolldown@npm:~1.1.4": +"rolldown@npm:~1.1.2": version: 1.1.5 resolution: "rolldown@npm:1.1.5" dependencies: @@ -37941,6 +40030,61 @@ __metadata: languageName: node linkType: hard +"rolldown@npm:~1.2.0": + version: 1.2.3 + resolution: "rolldown@npm:1.2.3" + dependencies: + "@oxc-project/types": "npm:=0.143.0" + "@rolldown/binding-android-arm64": "npm:1.2.3" + "@rolldown/binding-darwin-arm64": "npm:1.2.3" + "@rolldown/binding-darwin-x64": "npm:1.2.3" + "@rolldown/binding-freebsd-x64": "npm:1.2.3" + "@rolldown/binding-linux-arm-gnueabihf": "npm:1.2.3" + "@rolldown/binding-linux-arm64-gnu": "npm:1.2.3" + "@rolldown/binding-linux-arm64-musl": "npm:1.2.3" + "@rolldown/binding-linux-ppc64-gnu": "npm:1.2.3" + "@rolldown/binding-linux-s390x-gnu": "npm:1.2.3" + "@rolldown/binding-linux-x64-gnu": "npm:1.2.3" + "@rolldown/binding-linux-x64-musl": "npm:1.2.3" + "@rolldown/binding-openharmony-arm64": "npm:1.2.3" + "@rolldown/binding-win32-arm64-msvc": "npm:1.2.3" + "@rolldown/binding-win32-x64-msvc": "npm:1.2.3" + "@rolldown/pluginutils": "npm:^1.0.0" + dependenciesMeta: + "@rolldown/binding-android-arm64": + optional: true + "@rolldown/binding-darwin-arm64": + optional: true + "@rolldown/binding-darwin-x64": + optional: true + "@rolldown/binding-freebsd-x64": + optional: true + "@rolldown/binding-linux-arm-gnueabihf": + optional: true + "@rolldown/binding-linux-arm64-gnu": + optional: true + "@rolldown/binding-linux-arm64-musl": + optional: true + "@rolldown/binding-linux-ppc64-gnu": + optional: true + "@rolldown/binding-linux-s390x-gnu": + optional: true + "@rolldown/binding-linux-x64-gnu": + optional: true + "@rolldown/binding-linux-x64-musl": + optional: true + "@rolldown/binding-openharmony-arm64": + optional: true + "@rolldown/binding-win32-arm64-msvc": + optional: true + "@rolldown/binding-win32-x64-msvc": + optional: true + bin: + rolldown: ./bin/cli.mjs + checksum: 10c0/4dbeabc826e59877c7520b2ae1f48d0111e1d21865f5931ff3d184d33f02683e943e65eb24932128fc03701bbcde1b341f313b3fa7f8007368bd1b5e993265f0 + languageName: node + linkType: hard + "rollup@npm:4.59.0": version: 4.59.0 resolution: "rollup@npm:4.59.0" @@ -38315,10 +40459,50 @@ __metadata: languageName: node linkType: hard -"sax@npm:^1.5.0": - version: 1.5.0 - resolution: "sax@npm:1.5.0" - checksum: 10c0/bc3b60a7bfecd40b18256596e96b32df2488339ae1e00a77f842b568f0831228a16c3bd357ec500241ec0b9dc7a475a1286427795c4a8c50bb8e8878f3435dd8 +"satteri@npm:^0.9.1": + version: 0.9.5 + resolution: "satteri@npm:0.9.5" + dependencies: + "@bruits/satteri-darwin-arm64": "npm:0.9.5" + "@bruits/satteri-darwin-x64": "npm:0.9.5" + "@bruits/satteri-linux-arm64-gnu": "npm:0.9.5" + "@bruits/satteri-linux-arm64-musl": "npm:0.9.5" + "@bruits/satteri-linux-x64-gnu": "npm:0.9.5" + "@bruits/satteri-linux-x64-musl": "npm:0.9.5" + "@bruits/satteri-wasm32-wasi": "npm:0.9.5" + "@bruits/satteri-win32-arm64-msvc": "npm:0.9.5" + "@bruits/satteri-win32-x64-msvc": "npm:0.9.5" + "@types/estree-jsx": "npm:^1.0.5" + "@types/hast": "npm:^3.0.4" + "@types/mdast": "npm:^4.0.4" + "@types/unist": "npm:^3.0.3" + dependenciesMeta: + "@bruits/satteri-darwin-arm64": + optional: true + "@bruits/satteri-darwin-x64": + optional: true + "@bruits/satteri-linux-arm64-gnu": + optional: true + "@bruits/satteri-linux-arm64-musl": + optional: true + "@bruits/satteri-linux-x64-gnu": + optional: true + "@bruits/satteri-linux-x64-musl": + optional: true + "@bruits/satteri-wasm32-wasi": + optional: true + "@bruits/satteri-win32-arm64-msvc": + optional: true + "@bruits/satteri-win32-x64-msvc": + optional: true + checksum: 10c0/4a204a57b7b84750cb0fc9a1ab153c0c99dd3c57cac20b785705f6b723c1a465263951ad624eb47808a9501418eb81fa526bbc71f57f8fd07472fdf114e73a9c + languageName: node + linkType: hard + +"sax@npm:^1.4.1, sax@npm:^1.5.0": + version: 1.6.1 + resolution: "sax@npm:1.6.1" + checksum: 10c0/c91a71043d60da50fdf1e2cee936fc7ad4c9376afb8d1022aef5655f279433640859ec06b3b49abfcbe578fc7da2828cc1661e45aaedf835f5393496193437fa languageName: node linkType: hard @@ -38446,7 +40630,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.8.5, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.1, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.3, semver@npm:^7.8.5": +"semver@npm:7.8.5, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.1, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.3, semver@npm:^7.7.4, semver@npm:^7.8.5": version: 7.8.5 resolution: "semver@npm:7.8.5" bin: @@ -38675,7 +40859,7 @@ __metadata: languageName: node linkType: hard -"sharp@npm:0.35.3": +"sharp@npm:0.35.3, sharp@npm:^0.34.0 || ^0.35.0": version: 0.35.3 resolution: "sharp@npm:0.35.3" dependencies: @@ -38804,6 +40988,22 @@ __metadata: languageName: node linkType: hard +"shiki@npm:^4.0.2": + version: 4.4.2 + resolution: "shiki@npm:4.4.2" + dependencies: + "@shikijs/core": "npm:4.4.2" + "@shikijs/engine-javascript": "npm:4.4.2" + "@shikijs/engine-oniguruma": "npm:4.4.2" + "@shikijs/langs": "npm:4.4.2" + "@shikijs/themes": "npm:4.4.2" + "@shikijs/types": "npm:4.4.2" + "@shikijs/vscode-textmate": "npm:^10.0.2" + "@types/hast": "npm:^3.0.5" + checksum: 10c0/2bae4e358c561373ef5b5a7c3456466d91ae413250abad83a6281bc37b178fd5a6b9d5d68c9b2071d3c4f97ae11262faf04d453ecb9a5fd06af5bb765b6ef2dd + languageName: node + linkType: hard + "short-unique-id@npm:^5.2.0": version: 5.2.0 resolution: "short-unique-id@npm:5.2.0" @@ -38974,6 +41174,20 @@ __metadata: languageName: node linkType: hard +"sitemap@npm:^9.0.0": + version: 9.0.1 + resolution: "sitemap@npm:9.0.1" + dependencies: + "@types/node": "npm:^24.9.2" + "@types/sax": "npm:^1.2.1" + arg: "npm:^5.0.0" + sax: "npm:^1.4.1" + bin: + sitemap: dist/esm/cli.js + checksum: 10c0/dddd3c9f30fd09655fa2c1bd9bb182ed860717b3ba681fdc1ac66ad25b5f399dafdd8b87d5ba4ee914ea14e7b11841aadc3af61e4a45242a14f28194efe12416 + languageName: node + linkType: hard + "slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -39033,6 +41247,13 @@ __metadata: languageName: node linkType: hard +"smol-toml@npm:^1.6.0": + version: 1.7.1 + resolution: "smol-toml@npm:1.7.1" + checksum: 10c0/c2494e19e748578cf9f81f918dd66a548dfe5a0e581ab9f980d5420f2319c882c661a62d01d216a43f2db490faaccd2185bcdb6f2ba0658eb303eda9db871d76 + languageName: node + linkType: hard + "snake-case@npm:^3.0.4": version: 3.0.4 resolution: "snake-case@npm:3.0.4" @@ -39500,6 +41721,13 @@ __metadata: languageName: node linkType: hard +"stream-replace-string@npm:^2.0.0": + version: 2.0.0 + resolution: "stream-replace-string@npm:2.0.0" + checksum: 10c0/6cdf6108c57a869c1282dece0728bd7a8e314855bee71992436460192cdf46b3c976451e1e114716af209b2bfefa0e7e4581ca0eebc330d9dfcde341a72d50af + languageName: node + linkType: hard + "stream-shift@npm:^1.0.2": version: 1.0.3 resolution: "stream-shift@npm:1.0.3" @@ -39599,7 +41827,7 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^8.2.0": +"string-width@npm:^8.2.0, string-width@npm:^8.2.1": version: 8.2.2 resolution: "string-width@npm:8.2.2" dependencies: @@ -40019,6 +42247,23 @@ __metadata: languageName: node linkType: hard +"svgo@npm:^4.0.1": + version: 4.0.2 + resolution: "svgo@npm:4.0.2" + dependencies: + commander: "npm:^11.1.0" + css-select: "npm:^5.1.0" + css-tree: "npm:^3.0.1" + css-what: "npm:^6.1.0" + csso: "npm:^5.0.5" + picocolors: "npm:^1.1.1" + sax: "npm:^1.5.0" + bin: + svgo: ./bin/svgo.js + checksum: 10c0/d58c9446d2701dd57ef62a29b4299b157cfc32e2282f82fc68dd7cdca2e1efdf0d8bb0fb30ba0499f322d20b6304be277ffbc60a6b6a75489fa30c5300694b9a + languageName: node + linkType: hard + "swap-case@npm:^2.0.2": version: 2.0.2 resolution: "swap-case@npm:2.0.2" @@ -40433,7 +42678,7 @@ __metadata: languageName: node linkType: hard -"tiny-inflate@npm:^1.0.0": +"tiny-inflate@npm:^1.0.0, tiny-inflate@npm:^1.0.3": version: 1.0.3 resolution: "tiny-inflate@npm:1.0.3" checksum: 10c0/fab687537254f6ec44c9a2e880048fe70da3542aba28f73cda3e74c95cabf342a339372f2a6c032e322324f01accc03ca26c04ba2bad9b3eb8cf3ee99bba7f9b @@ -40454,6 +42699,13 @@ __metadata: languageName: node linkType: hard +"tinyclip@npm:^0.1.12": + version: 0.1.15 + resolution: "tinyclip@npm:0.1.15" + checksum: 10c0/8ebdcc60e0e9ba54edeae28158a7a96e94657ac9387cc03cd5ef6c51c36b92be2ebe10cf5e34c9382124ef766cf82784cca2c5db57cd7ab447b7d04880ee7579 + languageName: node + linkType: hard + "tinyexec@npm:^0.3.2": version: 0.3.2 resolution: "tinyexec@npm:0.3.2" @@ -40461,14 +42713,14 @@ __metadata: languageName: node linkType: hard -"tinyexec@npm:^1.0.2": - version: 1.0.4 - resolution: "tinyexec@npm:1.0.4" - checksum: 10c0/d4a5bbcf6bdb23527a4b74c4aa566f41432167112fe76f420ec7e3a90a3ecfd3a7d944383e2719fc3987b69400f7b928daf08700d145fb527c2e80ec01e198bd +"tinyexec@npm:^1.0.2, tinyexec@npm:^1.0.4": + version: 1.3.0 + resolution: "tinyexec@npm:1.3.0" + checksum: 10c0/e9b89f97489d2aab2cef408da279e6b32547e738d1275032ccb8fd0028a006d93eb70fc51c6cffd9fc2f5aca6c2a273d8b6f73b52d46ee5116da6b94969ef958 languageName: node linkType: hard -"tinyglobby@npm:^0.2.10, tinyglobby@npm:^0.2.11, tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15, tinyglobby@npm:^0.2.17, tinyglobby@npm:^0.2.9": +"tinyglobby@npm:^0.2.10, tinyglobby@npm:^0.2.11, tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15, tinyglobby@npm:^0.2.16, tinyglobby@npm:^0.2.17, tinyglobby@npm:^0.2.9": version: 0.2.17 resolution: "tinyglobby@npm:0.2.17" dependencies: @@ -41156,6 +43408,22 @@ __metadata: languageName: node linkType: hard +"typesafe-path@npm:^0.2.2": + version: 0.2.2 + resolution: "typesafe-path@npm:0.2.2" + checksum: 10c0/05027ea6a52a1e879da39c53fae7f5059e50cf578d586cc6c7741c3eca0b37ee2da4ef16d35ecf9f60cd0e8ca5aca98ea47902672907a19470f2260e8769b933 + languageName: node + linkType: hard + +"typescript-auto-import-cache@npm:^0.3.5": + version: 0.3.6 + resolution: "typescript-auto-import-cache@npm:0.3.6" + dependencies: + semver: "npm:^7.3.8" + checksum: 10c0/aad48d6aa9985f9a056c5c89ab91b6781c5430c0b80652586c2a997f2cc394f3ed70ec18c0c818a8423991a1cfe7e64d010cc2fded9ea192067a3188476c2b51 + languageName: node + linkType: hard + "typescript-eslint@npm:^8.46.4": version: 8.50.0 resolution: "typescript-eslint@npm:8.50.0" @@ -41298,10 +43566,10 @@ __metadata: languageName: node linkType: hard -"ufo@npm:^1.6.1": - version: 1.6.1 - resolution: "ufo@npm:1.6.1" - checksum: 10c0/5a9f041e5945fba7c189d5410508cbcbefef80b253ed29aa2e1f8a2b86f4bd51af44ee18d4485e6d3468c92be9bf4a42e3a2b72dcaf27ce39ce947ec994f1e6b +"ufo@npm:^1.6.1, ufo@npm:^1.6.3": + version: 1.6.4 + resolution: "ufo@npm:1.6.4" + checksum: 10c0/3a2b29e7e3d772fbf6893d7d23bf442981457adb2fe122828abdbda89bedcb81aafd0dcc080e41b45f9a877db00cb42cbfee9639753a19d9b9bd39b5627039cf languageName: node linkType: hard @@ -41358,6 +43626,13 @@ __metadata: languageName: node linkType: hard +"ultrahtml@npm:^1.6.0": + version: 1.7.0 + resolution: "ultrahtml@npm:1.7.0" + checksum: 10c0/a56907b3ea4b0c16d30c37cb814138f8ae47044892a9d1c5ecca0731e24c2c4cd1c9cfa53d57bf508b21bc3b993e3c02bf3f9c3bfc156d18ed7e12c62a140f75 + languageName: node + linkType: hard + "unbox-primitive@npm:^1.1.0": version: 1.1.0 resolution: "unbox-primitive@npm:1.1.0" @@ -41387,6 +43662,13 @@ __metadata: languageName: node linkType: hard +"uncrypto@npm:^0.1.3": + version: 0.1.3 + resolution: "uncrypto@npm:0.1.3" + checksum: 10c0/74a29afefd76d5b77bedc983559ceb33f5bbc8dada84ff33755d1e3355da55a4e03a10e7ce717918c436b4dfafde1782e799ebaf2aadd775612b49f7b5b2998e + languageName: node + linkType: hard + "underscore@npm:1.13.8": version: 1.13.8 resolution: "underscore@npm:1.13.8" @@ -41401,10 +43683,10 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~7.16.0": - version: 7.16.0 - resolution: "undici-types@npm:7.16.0" - checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a +"undici-types@npm:~7.18.0": + version: 7.18.2 + resolution: "undici-types@npm:7.18.2" + checksum: 10c0/85a79189113a238959d7a647368e4f7c5559c3a404ebdb8fc4488145ce9426fcd82252a844a302798dfc0e37e6fb178ff481ed03bc4caf634c5757d9ef43521d languageName: node linkType: hard @@ -41487,7 +43769,7 @@ __metadata: languageName: node linkType: hard -"unified@npm:^11.0.0, unified@npm:^11.0.4": +"unified@npm:^11.0.0, unified@npm:^11.0.4, unified@npm:^11.0.5": version: 11.0.5 resolution: "unified@npm:11.0.5" dependencies: @@ -41502,6 +43784,17 @@ __metadata: languageName: node linkType: hard +"unifont@npm:~0.7.4": + version: 0.7.4 + resolution: "unifont@npm:0.7.4" + dependencies: + css-tree: "npm:^3.1.0" + ofetch: "npm:^1.5.1" + ohash: "npm:^2.0.11" + checksum: 10c0/2b1c23e13083ce52382ac3e430b24c7f7a9a8657480f82847e254d5e3aa8a639c1178fda9ad157c21d2ceff5f7a037021c547501856dca7a93bc75bef6fe2f7d + languageName: node + linkType: hard + "unionfs@npm:^4.5.1": version: 4.5.4 resolution: "unionfs@npm:4.5.4" @@ -41529,6 +43822,16 @@ __metadata: languageName: node linkType: hard +"unist-util-find-after@npm:^5.0.0": + version: 5.0.0 + resolution: "unist-util-find-after@npm:5.0.0" + dependencies: + "@types/unist": "npm:^3.0.0" + unist-util-is: "npm:^6.0.0" + checksum: 10c0/a7cea473c4384df8de867c456b797ff1221b20f822e1af673ff5812ed505358b36f47f3b084ac14c3622cb879ed833b71b288e8aa71025352a2aab4c2925a6eb + languageName: node + linkType: hard + "unist-util-is@npm:^6.0.0": version: 6.0.0 resolution: "unist-util-is@npm:6.0.0" @@ -41538,6 +43841,16 @@ __metadata: languageName: node linkType: hard +"unist-util-modify-children@npm:^4.0.0": + version: 4.0.0 + resolution: "unist-util-modify-children@npm:4.0.0" + dependencies: + "@types/unist": "npm:^3.0.0" + array-iterate: "npm:^2.0.0" + checksum: 10c0/63d44b09a2e4c674c72816d4328d668972e68cc965ea719fef1c642b66a3ebe3b102e284a3213b4920ebccff05e0f689b4eaae8a0e5c3dafcad117d1577496da + languageName: node + linkType: hard + "unist-util-position-from-estree@npm:^2.0.0": version: 2.0.0 resolution: "unist-util-position-from-estree@npm:2.0.0" @@ -41556,6 +43869,16 @@ __metadata: languageName: node linkType: hard +"unist-util-remove-position@npm:^5.0.0": + version: 5.0.0 + resolution: "unist-util-remove-position@npm:5.0.0" + dependencies: + "@types/unist": "npm:^3.0.0" + unist-util-visit: "npm:^5.0.0" + checksum: 10c0/e8c76da4399446b3da2d1c84a97c607b37d03d1d92561e14838cbe4fdcb485bfc06c06cfadbb808ccb72105a80643976d0660d1fe222ca372203075be9d71105 + languageName: node + linkType: hard + "unist-util-stringify-position@npm:^4.0.0": version: 4.0.0 resolution: "unist-util-stringify-position@npm:4.0.0" @@ -41565,24 +43888,33 @@ __metadata: languageName: node linkType: hard -"unist-util-visit-parents@npm:^6.0.0": - version: 6.0.1 - resolution: "unist-util-visit-parents@npm:6.0.1" +"unist-util-visit-children@npm:^3.0.0": + version: 3.0.0 + resolution: "unist-util-visit-children@npm:3.0.0" + dependencies: + "@types/unist": "npm:^3.0.0" + checksum: 10c0/51e95f54fbf11d414952c011c761c3960864948ad3fd2abe3989eb18b18d96b8f48e7ea5ab6f23264d1a3f4f5a1ff76312dd8f2196c78b762098403505c3abb9 + languageName: node + linkType: hard + +"unist-util-visit-parents@npm:^6.0.0, unist-util-visit-parents@npm:^6.0.1, unist-util-visit-parents@npm:^6.0.2": + version: 6.0.2 + resolution: "unist-util-visit-parents@npm:6.0.2" dependencies: "@types/unist": "npm:^3.0.0" unist-util-is: "npm:^6.0.0" - checksum: 10c0/51b1a5b0aa23c97d3e03e7288f0cdf136974df2217d0999d3de573c05001ef04cccd246f51d2ebdfb9e8b0ed2704451ad90ba85ae3f3177cf9772cef67f56206 + checksum: 10c0/f1e4019dbd930301825895e3737b1ee0cd682f7622ddd915062135cbb39f8c090aaece3a3b5eae1f2ea52ec33f0931abb8f8a8b5c48a511a4203e3d360a8cd49 languageName: node linkType: hard -"unist-util-visit@npm:^5.0.0": - version: 5.0.0 - resolution: "unist-util-visit@npm:5.0.0" +"unist-util-visit@npm:^5.0.0, unist-util-visit@npm:^5.1.0": + version: 5.1.0 + resolution: "unist-util-visit@npm:5.1.0" dependencies: "@types/unist": "npm:^3.0.0" unist-util-is: "npm:^6.0.0" unist-util-visit-parents: "npm:^6.0.0" - checksum: 10c0/51434a1d80252c1540cce6271a90fd1a106dbe624997c09ed8879279667fb0b2d3a685e02e92bf66598dcbe6cdffa7a5f5fb363af8fdf90dda6c855449ae39a5 + checksum: 10c0/a56e1bbbf63fcb55abe379e660b9a3367787e8be1e2473bdb7e86cfa6f32b6c1fa0092432d7040b8a30b2fc674bbbe024ffe6d03c3d6bf4839b064f584463a4e languageName: node linkType: hard @@ -41725,6 +44057,81 @@ __metadata: languageName: node linkType: hard +"unstorage@npm:^1.17.5": + version: 1.17.5 + resolution: "unstorage@npm:1.17.5" + dependencies: + anymatch: "npm:^3.1.3" + chokidar: "npm:^5.0.0" + destr: "npm:^2.0.5" + h3: "npm:^1.15.10" + lru-cache: "npm:^11.2.7" + node-fetch-native: "npm:^1.6.7" + ofetch: "npm:^1.5.1" + ufo: "npm:^1.6.3" + peerDependencies: + "@azure/app-configuration": ^1.8.0 + "@azure/cosmos": ^4.2.0 + "@azure/data-tables": ^13.3.0 + "@azure/identity": ^4.6.0 + "@azure/keyvault-secrets": ^4.9.0 + "@azure/storage-blob": ^12.26.0 + "@capacitor/preferences": ^6 || ^7 || ^8 + "@deno/kv": ">=0.9.0" + "@netlify/blobs": ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + "@planetscale/database": ^1.19.0 + "@upstash/redis": ^1.34.3 + "@vercel/blob": ">=0.27.1" + "@vercel/functions": ^2.2.12 || ^3.0.0 + "@vercel/kv": ^1 || ^2 || ^3 + aws4fetch: ^1.0.20 + db0: ">=0.2.1" + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + "@azure/app-configuration": + optional: true + "@azure/cosmos": + optional: true + "@azure/data-tables": + optional: true + "@azure/identity": + optional: true + "@azure/keyvault-secrets": + optional: true + "@azure/storage-blob": + optional: true + "@capacitor/preferences": + optional: true + "@deno/kv": + optional: true + "@netlify/blobs": + optional: true + "@planetscale/database": + optional: true + "@upstash/redis": + optional: true + "@vercel/blob": + optional: true + "@vercel/functions": + optional: true + "@vercel/kv": + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + checksum: 10c0/52bc07d0951129f493cc2d2a5204f49df90c85e7ab3faac0019e3a31f22f507f7a8263fca7ca4d019950c14cbfed4d43e15c9afe4b5dcb307249ebb60d2e538e + languageName: node + linkType: hard + "until-async@npm:^3.0.2": version: 3.0.2 resolution: "until-async@npm:3.0.2" @@ -41801,6 +44208,13 @@ __metadata: languageName: node linkType: hard +"url-extras@npm:^0.1.0": + version: 0.1.0 + resolution: "url-extras@npm:0.1.0" + checksum: 10c0/353ca8190921d793bedaf765b55f94c1d719d1fd696c9a5dafeaf1b4532c1de936649925a0d699aa9613555b8dac82078e51d0a65cb1e48b92817ed68a527b60 + languageName: node + linkType: hard + "url-parse@npm:^1.5.3": version: 1.5.10 resolution: "url-parse@npm:1.5.10" @@ -42224,19 +44638,19 @@ __metadata: languageName: node linkType: hard -"vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0": - version: 8.1.4 - resolution: "vite@npm:8.1.4" +"vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0, vite@npm:^8.0.13": + version: 8.2.0 + resolution: "vite@npm:8.2.0" dependencies: fsevents: "npm:~2.3.3" - lightningcss: "npm:^1.32.0" + lightningcss: "npm:^1.33.0" picomatch: "npm:^4.0.5" - postcss: "npm:^8.5.16" - rolldown: "npm:~1.1.4" + postcss: "npm:^8.5.23" + rolldown: "npm:~1.2.0" tinyglobby: "npm:^0.2.17" peerDependencies: "@types/node": ^20.19.0 || >=22.12.0 - "@vitejs/devtools": ^0.3.0 + "@vitejs/devtools": ^0.4.0 esbuild: ^0.27.0 || ^0.28.0 jiti: ">=1.21.0" less: ^4.0.0 @@ -42277,7 +44691,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/4262cdd0dd00ca99b00a8aef91fc00e58b66e80b54adbef49353bfbbae7342c2d6ddf1180351956086ff53ad186bec03784f57b96e36ab4dad1246547236d79b + checksum: 10c0/bd6a5e7b28973bac06f0e8e54833800e16d1bf38a8aef2acbfea5bbaed6d8fc715fd7cc9b0ec75ef1adbde149bb9167b085c17fe7edcd3a190b4eda16d474e5c languageName: node linkType: hard @@ -42336,6 +44750,18 @@ __metadata: languageName: node linkType: hard +"vitefu@npm:^1.1.2": + version: 1.1.3 + resolution: "vitefu@npm:1.1.3" + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + checksum: 10c0/6cfcd52a17339f63f71ef08787a68850a17a9d9dd7a688e29e41067f963eb2cd5192bb65d38df87fa3a733a21e0229aaa6382201655c1599699e5d6669e3e094 + languageName: node + linkType: hard + "vitest-browser-react@npm:^2.0.2": version: 2.2.0 resolution: "vitest-browser-react@npm:2.2.0" @@ -42422,7 +44848,192 @@ __metadata: languageName: node linkType: hard -"vscode-languageserver-textdocument@npm:^1.0.8": +"volar-service-css@npm:0.0.71": + version: 0.0.71 + resolution: "volar-service-css@npm:0.0.71" + dependencies: + vscode-css-languageservice: "npm:^6.3.0" + vscode-languageserver-textdocument: "npm:^1.0.11" + vscode-uri: "npm:^3.0.8" + peerDependencies: + "@volar/language-service": ~2.4.0 + peerDependenciesMeta: + "@volar/language-service": + optional: true + checksum: 10c0/fd07e55786a18db2f671eea087e2d43116d8c0f504aac699a3b47fc9d6f53d66c0b3652062ca6ae08d8e59905564f72c63af9727d956165061849ab488462e2e + languageName: node + linkType: hard + +"volar-service-emmet@npm:0.0.71": + version: 0.0.71 + resolution: "volar-service-emmet@npm:0.0.71" + dependencies: + "@emmetio/css-parser": "npm:^0.4.1" + "@emmetio/html-matcher": "npm:^1.3.0" + "@vscode/emmet-helper": "npm:^2.9.3" + vscode-uri: "npm:^3.0.8" + peerDependencies: + "@volar/language-service": ~2.4.0 + peerDependenciesMeta: + "@volar/language-service": + optional: true + checksum: 10c0/a3c95991859d0e7e06a23d1201fcbdddbb7be0c525a553881f14ab941efe331adbb27431caf67962ec43a077e424f7e30bada6a52254272572b93b01f55bcf73 + languageName: node + linkType: hard + +"volar-service-html@npm:0.0.71": + version: 0.0.71 + resolution: "volar-service-html@npm:0.0.71" + dependencies: + vscode-html-languageservice: "npm:^5.3.0" + vscode-languageserver-textdocument: "npm:^1.0.11" + vscode-uri: "npm:^3.0.8" + peerDependencies: + "@volar/language-service": ~2.4.0 + peerDependenciesMeta: + "@volar/language-service": + optional: true + checksum: 10c0/b1ff293413945cf7357aadc9484080863e7a855a919ccc4f379123ebabe16f9905f5bd19db60e08028a7282c2926762a006daddabd0e373d5040d32afe2ab364 + languageName: node + linkType: hard + +"volar-service-prettier@npm:0.0.71": + version: 0.0.71 + resolution: "volar-service-prettier@npm:0.0.71" + dependencies: + vscode-uri: "npm:^3.0.8" + peerDependencies: + "@volar/language-service": ~2.4.0 + prettier: ^2.2 || ^3.0 + peerDependenciesMeta: + "@volar/language-service": + optional: true + prettier: + optional: true + checksum: 10c0/f3498260e51a5ee6d762fac0f332e745a5705db9ccd9aca9d154985cff733d22e8be6a41239bdce4cd5dcdaa05f8bd6e1baf27c5585f9b968e93155eb74e2118 + languageName: node + linkType: hard + +"volar-service-typescript-twoslash-queries@npm:0.0.71": + version: 0.0.71 + resolution: "volar-service-typescript-twoslash-queries@npm:0.0.71" + dependencies: + vscode-uri: "npm:^3.0.8" + peerDependencies: + "@volar/language-service": ~2.4.0 + peerDependenciesMeta: + "@volar/language-service": + optional: true + checksum: 10c0/deaea07b08c6dc54763c98217b1b206c885e4434a07018f058e22602f8445ee29ceac70217b1271b6182c979e8ccfe2fbdaa464e327ab8ae8a0eb87517d0adba + languageName: node + linkType: hard + +"volar-service-typescript@npm:0.0.71": + version: 0.0.71 + resolution: "volar-service-typescript@npm:0.0.71" + dependencies: + path-browserify: "npm:^1.0.1" + semver: "npm:^7.6.2" + typescript-auto-import-cache: "npm:^0.3.5" + vscode-languageserver-textdocument: "npm:^1.0.11" + vscode-nls: "npm:^5.2.0" + vscode-uri: "npm:^3.0.8" + peerDependencies: + "@volar/language-service": ~2.4.0 + peerDependenciesMeta: + "@volar/language-service": + optional: true + checksum: 10c0/c197666fca9810e72e9db81d15c527b21dbebd493966199b5e8d690204ba17cbc6cfaeed6be7d72b285bb795ff7fbb45a939463db0cb804360c4966e5811bbc0 + languageName: node + linkType: hard + +"volar-service-yaml@npm:0.0.71": + version: 0.0.71 + resolution: "volar-service-yaml@npm:0.0.71" + dependencies: + vscode-uri: "npm:^3.0.8" + yaml-language-server: "npm:~1.23.0" + peerDependencies: + "@volar/language-service": ~2.4.0 + peerDependenciesMeta: + "@volar/language-service": + optional: true + checksum: 10c0/6d56a6c77a100208e1ae0c5cbc80f547a066012a21574f476b37b59dfb212129efaf1a98fdc4cde8bcc71c61e9d26e0ac326d6c8ea300dffd681bf2c55b50f35 + languageName: node + linkType: hard + +"vscode-css-languageservice@npm:^6.3.0": + version: 6.3.10 + resolution: "vscode-css-languageservice@npm:6.3.10" + dependencies: + "@vscode/l10n": "npm:^0.0.18" + vscode-languageserver-textdocument: "npm:^1.0.12" + vscode-languageserver-types: "npm:3.17.5" + vscode-uri: "npm:^3.1.0" + checksum: 10c0/3f6228d85579c0306343c734716c17128757f3a6e44625deac06622fe248f05fe15da1901581aa50cb8d9539137a49d19f05dca6d13feb2e6bc9f3d9784236bd + languageName: node + linkType: hard + +"vscode-html-languageservice@npm:^5.3.0, vscode-html-languageservice@npm:^5.6.2": + version: 5.6.2 + resolution: "vscode-html-languageservice@npm:5.6.2" + dependencies: + "@vscode/l10n": "npm:^0.0.18" + vscode-languageserver-textdocument: "npm:^1.0.12" + vscode-languageserver-types: "npm:^3.17.5" + vscode-uri: "npm:^3.1.0" + checksum: 10c0/5ef5f3cb959435a3f29d9b5844fb223140b8b5b616c3f54d0c627427853e8c8806d5d93f3a4e3354a3c9d2e9acdf6cb8043479837d20d9dd30ad6d07bf22286d + languageName: node + linkType: hard + +"vscode-json-languageservice@npm:4.1.8": + version: 4.1.8 + resolution: "vscode-json-languageservice@npm:4.1.8" + dependencies: + jsonc-parser: "npm:^3.0.0" + vscode-languageserver-textdocument: "npm:^1.0.1" + vscode-languageserver-types: "npm:^3.16.0" + vscode-nls: "npm:^5.0.0" + vscode-uri: "npm:^3.0.2" + checksum: 10c0/9165703884e5eef52d4bf52294051df6623461cc7a3d458ef1f4fa3f98ac93c3a79efc8c4a15fda6ddf6e43d155c207c3c13534dcac3fe7982ddf1926dbf22a7 + languageName: node + linkType: hard + +"vscode-jsonrpc@npm:8.2.0": + version: 8.2.0 + resolution: "vscode-jsonrpc@npm:8.2.0" + checksum: 10c0/0789c227057a844f5ead55c84679206227a639b9fb76e881185053abc4e9848aa487245966cc2393fcb342c4541241b015a1a2559fddd20ac1e68945c95344e6 + languageName: node + linkType: hard + +"vscode-jsonrpc@npm:9.0.1": + version: 9.0.1 + resolution: "vscode-jsonrpc@npm:9.0.1" + checksum: 10c0/6ca63393dd78675ed33b5537c147e673865557ff93915cd5189ceae039ca7fe01808ffd9a14016732d4789f2102a037d68d4216f557c3380d1c7f3ea50fd85b4 + languageName: node + linkType: hard + +"vscode-languageserver-protocol@npm:3.17.5": + version: 3.17.5 + resolution: "vscode-languageserver-protocol@npm:3.17.5" + dependencies: + vscode-jsonrpc: "npm:8.2.0" + vscode-languageserver-types: "npm:3.17.5" + checksum: 10c0/5f38fd80da9868d706eaa4a025f4aff9c3faad34646bcde1426f915cbd8d7e8b6c3755ce3fef6eebd256ba3145426af1085305f8a76e34276d2e95aaf339a90b + languageName: node + linkType: hard + +"vscode-languageserver-protocol@npm:^3.17.5": + version: 3.18.2 + resolution: "vscode-languageserver-protocol@npm:3.18.2" + dependencies: + vscode-jsonrpc: "npm:9.0.1" + vscode-languageserver-types: "npm:3.18.0" + checksum: 10c0/c77862958063b2d6edcdbab7da1abece58b251051338083296c468d5caafc2c249aba63f1a15011d8b8354e145937252520fe6a9632c146606a833bdeae9784c + languageName: node + linkType: hard + +"vscode-languageserver-textdocument@npm:^1.0.1, vscode-languageserver-textdocument@npm:^1.0.11, vscode-languageserver-textdocument@npm:^1.0.12, vscode-languageserver-textdocument@npm:^1.0.8": version: 1.0.13 resolution: "vscode-languageserver-textdocument@npm:1.0.13" checksum: 10c0/1de174f1de3bfa9e1660a4b3c1b1c711497196d761e33f4d00785fdbfc556ae5a1f440db04771cbc0f6bd66c2a6f303062bc17d8dc46e76820e39fa2d5626564 @@ -42436,14 +45047,32 @@ __metadata: languageName: node linkType: hard -"vscode-languageserver-types@npm:^3.17.3": +"vscode-languageserver-types@npm:3.18.0, vscode-languageserver-types@npm:^3.15.1, vscode-languageserver-types@npm:^3.16.0, vscode-languageserver-types@npm:^3.17.3, vscode-languageserver-types@npm:^3.17.5": version: 3.18.0 resolution: "vscode-languageserver-types@npm:3.18.0" checksum: 10c0/d8c51cd286cc55b4c1f333e87fa3cefc66d2aae8c65f6209e7ecf88032d3326308b333d62b05243c194be8a3304760445b87630b298116c6c612607f011b4de0 languageName: node linkType: hard -"vscode-uri@npm:^3.0.8": +"vscode-languageserver@npm:^9.0.0, vscode-languageserver@npm:^9.0.1": + version: 9.0.1 + resolution: "vscode-languageserver@npm:9.0.1" + dependencies: + vscode-languageserver-protocol: "npm:3.17.5" + bin: + installServerIntoExtension: bin/installServerIntoExtension + checksum: 10c0/8a0838d77c98a211c76e54bd3a6249fc877e4e1a73322673fb0e921168d8e91de4f170f1d4ff7e8b6289d0698207afc6aba6662d4c1cd8e4bd7cae96afd6b0c2 + languageName: node + linkType: hard + +"vscode-nls@npm:^5.0.0, vscode-nls@npm:^5.2.0": + version: 5.2.0 + resolution: "vscode-nls@npm:5.2.0" + checksum: 10c0/dc9e48f58ebbc807f435d351008813a2ea0c9432d51e778bcac9163c0642f929ddb518411ad654e775ce31e24d6acfa8fb7db8893c05b42c2019894e08b050f9 + languageName: node + linkType: hard + +"vscode-uri@npm:^3.0.2, vscode-uri@npm:^3.0.8, vscode-uri@npm:^3.1.0": version: 3.1.0 resolution: "vscode-uri@npm:3.1.0" checksum: 10c0/5f6c9c10fd9b1664d71fab4e9fbbae6be93c7f75bb3a1d9d74399a88ab8649e99691223fd7cef4644376cac6e94fa2c086d802521b9a8e31c5af3e60f0f35624 @@ -43262,6 +45891,13 @@ __metadata: languageName: node linkType: hard +"xxhash-wasm@npm:^1.1.0": + version: 1.1.0 + resolution: "xxhash-wasm@npm:1.1.0" + checksum: 10c0/35aa152fc7d775ae13364fe4fb20ebd89c6ac1f56cdb6060a6d2f1ed68d15180694467e63a4adb3d11936a4798ccd75a540979070e70d9b911e9981bbdd9cea6 + languageName: node + linkType: hard + "y18n@npm:^3.2.1 || ^4.0.0": version: 4.0.3 resolution: "y18n@npm:4.0.3" @@ -43304,6 +45940,37 @@ __metadata: languageName: node linkType: hard +"yaml-language-server@npm:~1.23.0": + version: 1.23.0 + resolution: "yaml-language-server@npm:1.23.0" + dependencies: + "@vscode/l10n": "npm:^0.0.18" + ajv: "npm:^8.17.1" + ajv-draft-04: "npm:^1.0.0" + ajv-i18n: "npm:^4.2.0" + prettier: "npm:^3.8.1" + request-light: "npm:^0.5.7" + vscode-json-languageservice: "npm:4.1.8" + vscode-languageserver: "npm:^9.0.0" + vscode-languageserver-textdocument: "npm:^1.0.1" + vscode-languageserver-types: "npm:^3.16.0" + vscode-uri: "npm:^3.0.2" + yaml: "npm:2.8.3" + bin: + yaml-language-server: bin/yaml-language-server + checksum: 10c0/d5ed44bb542ad9b9a38171983257f47c391120fb703e043395f3490c22d99f0807d90a0cdb7b4b7955f39557162936f41bd0d6f121c95b473bc1deac597c41a1 + languageName: node + linkType: hard + +"yaml@npm:2.8.3": + version: 2.8.3 + resolution: "yaml@npm:2.8.3" + bin: + yaml: bin.mjs + checksum: 10c0/ddff0e11c1b467728d7eb4633db61c5f5de3d8e9373cf84d08fb0cdee03e1f58f02b9f1c51a4a8a865751695addbd465a77f73f1079be91fe5493b29c305fd77 + languageName: node + linkType: hard + "yaml@npm:^1.10.0, yaml@npm:^1.5.0": version: 1.10.3 resolution: "yaml@npm:1.10.3" @@ -43311,7 +45978,7 @@ __metadata: languageName: node linkType: hard -"yaml@npm:^2.3.1": +"yaml@npm:^2.3.1, yaml@npm:^2.8.3": version: 2.9.0 resolution: "yaml@npm:2.9.0" bin: @@ -43393,6 +46060,20 @@ __metadata: languageName: node linkType: hard +"yargs@npm:^18.0.0": + version: 18.1.0 + resolution: "yargs@npm:18.1.0" + dependencies: + cliui: "npm:^9.0.1" + escalade: "npm:^3.1.1" + get-caller-file: "npm:^2.0.5" + string-width: "npm:^8.2.1" + y18n: "npm:^5.0.5" + yargs-parser: "npm:^22.0.0" + checksum: 10c0/86052bbed90a9aaaaf1eabe004f47c7420e1b7f8d314170cfb0ad4721f518ec2fb7ba5a6daa20708b1595aba257f1554b2d5b3f5606356f86aa9dae18de0bec5 + languageName: node + linkType: hard + "yauzl@npm:^2.10.0": version: 2.10.0 resolution: "yauzl@npm:2.10.0" @@ -43436,7 +46117,7 @@ __metadata: languageName: node linkType: hard -"yocto-queue@npm:^1.0.0": +"yocto-queue@npm:^1.0.0, yocto-queue@npm:^1.2.1": version: 1.2.2 resolution: "yocto-queue@npm:1.2.2" checksum: 10c0/36d4793e9cf7060f9da543baf67c55e354f4862c8d3d34de1a1b1d7c382d44171315cc54abf84d8900b8113d742b830108a1434f4898fb244f9b7e8426d4b8f5 @@ -43526,7 +46207,7 @@ __metadata: languageName: node linkType: hard -"zod@npm:4.4.3, zod@npm:^3.25 || ^4.0, zod@npm:^3.25.0 || ^4.0.0, zod@npm:^4.0.0, zod@npm:^4.1.5": +"zod@npm:4.4.3, zod@npm:^3.25 || ^4.0, zod@npm:^3.25.0 || ^4.0.0, zod@npm:^4.0.0, zod@npm:^4.1.5, zod@npm:^4.3.6": version: 4.4.3 resolution: "zod@npm:4.4.3" checksum: 10c0/7ea31b558e88f9faf44f31dd185e2e1cbf51fed3081787fb96cc2534749b50c0acfc6da7f0922a7353ed092dd358c7d50c28ea96c94d04af64191bd33152eca3 @@ -43562,7 +46243,7 @@ __metadata: languageName: node linkType: hard -"zwitch@npm:^2.0.0": +"zwitch@npm:^2.0.0, zwitch@npm:^2.0.4": version: 2.0.4 resolution: "zwitch@npm:2.0.4" checksum: 10c0/3c7830cdd3378667e058ffdb4cf2bb78ac5711214e2725900873accb23f3dfe5f9e7e5a06dcdc5f29605da976fc45c26d9a13ca334d6eea2245a15e77b8fc06e From c78aad4d731221005103b1e856931ed1f394c1da Mon Sep 17 00:00:00 2001 From: Chris Feijoo Date: Wed, 5 Aug 2026 00:57:48 +0200 Subject: [PATCH 3/3] FE-1314: Run experiment runs in parallel across workers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An experiment ran every run in a single worker, using one core however many the machine had. Runs are independent, so they now split across one worker per logical core minus one, capped at the run count — measured at ~4x on 8 shards. Sharding cannot change results: seeds derive from the run's global index rather than its position in a shard, and per-frame statistics recombine through the metric accumulator monoids, so output is byte-identical at every shard count. Scalar frames carry their pre-reduction accumulator state, since a mean of means is not a mean. --- .changeset/parallel-experiment-runs.md | 14 + libs/@hashintel/petrinaut-core/.oxlintrc.json | 1 + .../petrinaut-core/benchmarks/.gitignore | 1 + .../petrinaut-core/benchmarks/README.md | 70 ++ .../benchmarks/coloured-enumeration.mjs | 104 +++ .../benchmarks/flat-stepper-ceiling.mjs | 134 ++++ .../benchmarks/monte-carlo-throughput.mjs | 117 +++ .../petrinaut-core/benchmarks/shard-main.mjs | 121 +++ .../benchmarks/shard-worker.mjs | 81 +++ .../benchmarks/sharded-experiment-worker.mjs | 39 + .../benchmarks/sharded-experiment.mjs | 151 ++++ .../docs/simulation-performance.md | 686 ++++++++++++++++++ libs/@hashintel/petrinaut-core/src/index.ts | 4 + .../petrinaut-core/src/simulation/index.ts | 4 + .../src/simulation/monte-carlo/index.ts | 6 + .../simulation/monte-carlo/metrics/index.ts | 1 + .../monte-carlo/metrics/merge.test.ts | 258 +++++++ .../simulation/monte-carlo/metrics/merge.ts | 287 ++++++++ .../simulation/monte-carlo/metrics/types.ts | 13 + .../monte-carlo/metrics/user-defined.ts | 11 +- .../src/simulation/monte-carlo/run-state.ts | 6 +- .../monte-carlo/runtime/experiment.test.ts | 266 +++++++ .../monte-carlo/runtime/experiment.ts | 349 +++++++-- .../monte-carlo/runtime/shard-plan.test.ts | 114 +++ .../monte-carlo/runtime/shard-plan.ts | 91 +++ .../src/simulation/monte-carlo/types.ts | 12 + .../simulation/monte-carlo/worker/messages.ts | 8 + .../monte-carlo/worker/monte-carlo.worker.ts | 1 + libs/@hashintel/petrinaut/docs/experiments.md | 19 +- .../src/react/experiments/provider.test.tsx | 8 + .../src/react/experiments/provider.tsx | 14 + .../content/simulation/worker-sharding.mdx | 42 ++ 32 files changed, 2953 insertions(+), 80 deletions(-) create mode 100644 .changeset/parallel-experiment-runs.md create mode 100644 libs/@hashintel/petrinaut-core/benchmarks/.gitignore create mode 100644 libs/@hashintel/petrinaut-core/benchmarks/README.md create mode 100644 libs/@hashintel/petrinaut-core/benchmarks/coloured-enumeration.mjs create mode 100644 libs/@hashintel/petrinaut-core/benchmarks/flat-stepper-ceiling.mjs create mode 100644 libs/@hashintel/petrinaut-core/benchmarks/monte-carlo-throughput.mjs create mode 100644 libs/@hashintel/petrinaut-core/benchmarks/shard-main.mjs create mode 100644 libs/@hashintel/petrinaut-core/benchmarks/shard-worker.mjs create mode 100644 libs/@hashintel/petrinaut-core/benchmarks/sharded-experiment-worker.mjs create mode 100644 libs/@hashintel/petrinaut-core/benchmarks/sharded-experiment.mjs create mode 100644 libs/@hashintel/petrinaut-core/docs/simulation-performance.md create mode 100644 libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/metrics/merge.test.ts create mode 100644 libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/metrics/merge.ts create mode 100644 libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/runtime/shard-plan.test.ts create mode 100644 libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/runtime/shard-plan.ts create mode 100644 libs/@local/petrinaut-arch-docs/content/simulation/worker-sharding.mdx diff --git a/.changeset/parallel-experiment-runs.md b/.changeset/parallel-experiment-runs.md new file mode 100644 index 00000000000..b5878a085f8 --- /dev/null +++ b/.changeset/parallel-experiment-runs.md @@ -0,0 +1,14 @@ +--- +"@hashintel/petrinaut-core": patch +"@hashintel/petrinaut": patch +--- + +Run an experiment's runs in parallel across several Web Workers. + +An experiment used to run every one of its runs in a single worker, using one core however many the machine had. Runs are independent, so they now split across one worker per logical core (minus one, so the editor stays responsive), capped at the run count — measured at ~4x on 8 shards on a 10-core machine. + +Sharding cannot change what an experiment reports. Per-run seeds derive from the run's **global** index rather than its position within a shard, so run *i* gets the same seed whichever worker owns it, and each worker's per-frame statistics recombine through the metric accumulator monoids (`empty`/`merge`) — output is byte-identical at every shard count. A frame is only finalised once every still-running shard has reported it, with finished shards dropped from that watermark rather than blocking it. + +Scalar metric frames now carry their pre-reduction accumulator state, because `frameValue` is already reduced and a mean of means is not a mean. + +Hosts can cap or pin parallelism with `experimentShardCount` on `ExperimentsProvider`, or `shardCount` on `createMonteCarloExperiment`. diff --git a/libs/@hashintel/petrinaut-core/.oxlintrc.json b/libs/@hashintel/petrinaut-core/.oxlintrc.json index 838081027d2..5a4b9641d42 100644 --- a/libs/@hashintel/petrinaut-core/.oxlintrc.json +++ b/libs/@hashintel/petrinaut-core/.oxlintrc.json @@ -144,6 +144,7 @@ "dist/**", "build/**", "coverage/**", + "benchmarks/**", "*.gen.*", "*.tsbuildinfo", ".turbo/**" diff --git a/libs/@hashintel/petrinaut-core/benchmarks/.gitignore b/libs/@hashintel/petrinaut-core/benchmarks/.gitignore new file mode 100644 index 00000000000..97c133635a6 --- /dev/null +++ b/libs/@hashintel/petrinaut-core/benchmarks/.gitignore @@ -0,0 +1 @@ +.node-worker-bundle.mjs diff --git a/libs/@hashintel/petrinaut-core/benchmarks/README.md b/libs/@hashintel/petrinaut-core/benchmarks/README.md new file mode 100644 index 00000000000..b46e5196e3d --- /dev/null +++ b/libs/@hashintel/petrinaut-core/benchmarks/README.md @@ -0,0 +1,70 @@ +# Simulation benchmarks + +Measurement harnesses backing +[`../docs/simulation-performance.md`](../docs/simulation-performance.md). These +are investigation tools, not tests — nothing here asserts, and none of it runs +in CI. + +They run against the **built** package, so build first: + +```bash +yarn build +``` + +Then, from this directory: + +```bash +node monte-carlo-throughput.mjs +``` + +## What each one measures + +| Script | Question it answers | +| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| `monte-carlo-throughput.mjs` | Baseline ns/run-frame of the current engine, per-run construction cost, and what metric aggregation adds | +| `flat-stepper-ceiling.mjs` | What the same net costs as hand-written flat typed-array code — i.e. the headroom available without leaving JavaScript | +| `coloured-enumeration.mjs` | How per-frame cost scales with token count when a coloured input arc has weight 2 (the `∏ C(n, w)` blow-up) | +| `shard-main.mjs` | Whether sharding runs across worker threads scales, **and whether it changes results** (simulator level) | +| `sharded-experiment.mjs` | The same question against the production `createMonteCarloExperiment` runtime with real worker threads | + +`flat-stepper-ceiling.mjs` needs no build — it imports nothing from the package. + +## Reading the numbers + +Results are reported per **run-frame** (one run advanced by one frame) rather +than per experiment, because runs finish at different times: a completed run +stops consuming budget, so wall clock alone conflates "faster engine" with +"runs deadlocked earlier". `runFrames` is summed from run summaries. + +Absolute figures are machine-specific. The ratios are the point. + +## The sharding checks + +Two scripts cover sharding at different levels. + +`sharded-experiment.mjs` is the one that matters: it drives the shipped +`createMonteCarloExperiment` over real worker threads at several shard counts. +It bundles the worker for Node first, because the `dist` build wraps the worker +in an inline Blob that only a browser can load. It exits non-zero if any shard +count produces different results. + +`shard-main.mjs` predates the production implementation and spawns +`shard-worker.mjs` directly against the **unmodified** `MonteCarloSimulator`. It +is kept because it isolates the simulator from the experiment runtime, so a +regression can be attributed to one or the other. + +Both check two things: + +1. **Scaling** — wall clock against shard count. +2. **Result preservation** — every frame's merged histogram is fingerprinted and + compared across shard counts. This must print `identical to 1 shard: YES` on + every row. If it ever prints `NO`, the sharding design is wrong, not the + benchmark. + +Two details carry that guarantee, and both must survive into any production +implementation: + +- Seeds derive from the **global** run index, so run _i_ gets the same seed + regardless of which shard owns it. +- Metric state is merged with the accumulator monoid's `merge`, which is + associative and commutative, so shard completion order does not matter. diff --git a/libs/@hashintel/petrinaut-core/benchmarks/coloured-enumeration.mjs b/libs/@hashintel/petrinaut-core/benchmarks/coloured-enumeration.mjs new file mode 100644 index 00000000000..e5f3c49942b --- /dev/null +++ b/libs/@hashintel/petrinaut-core/benchmarks/coloured-enumeration.mjs @@ -0,0 +1,104 @@ +/** + * Measures how per-frame cost scales with token count for a transition whose + * coloured input arc has weight 2. + * + * `enumerateWeightedMarkingIndicesGenerator` materialises the full per-place + * combination list up front, so the expectation is O(C(n, 2)) = O(n^2) work and + * allocation per transition evaluation per frame. + */ +import { performance } from "node:perf_hooks"; + +const DIST = "../dist"; + +const { createMonteCarloSimulator } = await import(`${DIST}/index.js`); +const { compileHirArtifacts } = await import(`${DIST}/hir.js`); + +/** Net: one coloured place `pool`, one transition consuming 2 pool tokens. */ +const sdcpn = { + types: [ + { + id: "t-item", + name: "Item", + iconSlug: "circle", + displayColor: "#00FF00", + elements: [{ elementId: "v", name: "v", type: "real" }], + }, + ], + places: [ + { + id: "pool", + name: "Pool", + colorId: "t-item", + dynamicsEnabled: false, + differentialEquationId: null, + x: 0, + y: 0, + }, + { + id: "sink", + name: "Sink", + colorId: "t-item", + dynamicsEnabled: false, + differentialEquationId: null, + x: 100, + y: 0, + }, + ], + transitions: [ + { + id: "pair", + name: "Pair", + inputArcs: [{ placeId: "pool", weight: 2, type: "standard" }], + outputArcs: [{ placeId: "sink", weight: 1 }], + lambdaType: "predicate", + // Never fires, so token counts stay constant and we measure pure + // enablement/enumeration cost at a fixed marking size. + lambdaCode: "export default Lambda(() => false);", + transitionKernelCode: + "export default TransitionKernel(() => ({ Sink: [{ v: 1 }] }));", + x: 50, + y: 0, + }, + ], + differentialEquations: [], + parameters: [], +}; + +const artifacts = compileHirArtifacts(sdcpn).artifacts; + +process.stdout.write( + "coloured place, input arc weight 2, transition never fires\n" + + "tokens C(n,2) ns/run-frame\n", +); + +for (const tokens of [10, 25, 50, 100, 200, 400]) { + const simulator = createMonteCarloSimulator({ + sdcpn, + initialMarking: { + pool: Array.from({ length: tokens }, (_, index) => ({ v: index })), + sink: [], + }, + parameterValues: {}, + seed: 1, + dt: 0.1, + maxTime: 5, + runCount: 20, + hirArtifacts: artifacts, + metrics: [], + }); + + const start = performance.now(); + simulator.runUntilComplete(); + const ms = performance.now() - start; + + let frames = 0; + for (const summary of simulator.getSummaries()) { + frames += summary.frameNumber; + } + + const combinations = (tokens * (tokens - 1)) / 2; + process.stdout.write( + `${String(tokens).padStart(6)} ${String(combinations).padStart(7)} ` + + `${((ms / frames) * 1e6).toFixed(0).padStart(12)}\n`, + ); +} diff --git a/libs/@hashintel/petrinaut-core/benchmarks/flat-stepper-ceiling.mjs b/libs/@hashintel/petrinaut-core/benchmarks/flat-stepper-ceiling.mjs new file mode 100644 index 00000000000..08d70bb0d6d --- /dev/null +++ b/libs/@hashintel/petrinaut-core/benchmarks/flat-stepper-ceiling.mjs @@ -0,0 +1,134 @@ +/** + * Measures the achievable ceiling for the SIR Monte Carlo workload in plain JS: + * a flat, allocation-free, structure-of-arrays stepper equivalent to what a + * codegen backend (JS or WASM) would emit for the same net. + * + * Mirrors the semantics of the current engine for this net: + * - 3 uncoloured places (S, I, R), 2 transitions (infection, recovery) + * - per transition per frame: one RNG draw, exp(-lambda * timeSinceLastFiring) + * acceptance test, structural enablement on input arc weights + * - infection: S-1, I+1 (consumes 1 S + 1 I, emits 2 I) + * - recovery: I-1, R+1 + * - deadlock when no transition is structurally enabled + */ +import { performance } from "node:perf_hooks"; + +const RUNS = 4000; +const DT = 0.1; +const MAX_TIME = 60; +const MAX_FRAMES = Math.round(MAX_TIME / DT); +const INFECTION_RATE = 0.4; +const RECOVERY_RATE = 0.1; + +const S0 = 500; +const I0 = 5; + +// ---- Structure of arrays: one lane per run, no per-run objects. ------------- +const s = new Int32Array(RUNS).fill(S0); +const i = new Int32Array(RUNS).fill(I0); +const r = new Int32Array(RUNS); +// Elapsed frames since last firing, per transition per run. +const elapsed0 = new Int32Array(RUNS); +const elapsed1 = new Int32Array(RUNS); +const rng = new Uint32Array(RUNS); +const frameNumber = new Int32Array(RUNS); +const active = new Uint8Array(RUNS).fill(1); + +for (let run = 0; run < RUNS; run++) { + rng[run] = (42 + run * 2654435761) >>> 0; +} + +// mulberry32-style step, matching the shape of the engine's seeded RNG: +// one u32 state, one float out. +const start = performance.now(); + +let advancedTotal = 0; +let activeCount = RUNS; + +for (let frame = 0; frame < MAX_FRAMES && activeCount > 0; frame++) { + for (let run = 0; run < RUNS; run++) { + if (active[run] === 0) { + continue; + } + + const sv = s[run]; + const iv = i[run]; + + // Deadlock check: infection needs S>=1 && I>=1, recovery needs I>=1. + if (iv === 0) { + active[run] = 0; + activeCount--; + continue; + } + + let state = rng[run]; + let fired0 = 0; + let fired1 = 0; + + // --- transition 0: infection (S>=1, I>=1) --- + if (sv >= 1 && iv >= 1) { + state = (state + 0x6d2b79f5) >>> 0; + let t = state; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + const u = ((t ^ (t >>> 14)) >>> 0) / 4294967296; + const lambda = INFECTION_RATE * (elapsed0[run] * DT); + if (Math.exp(-lambda) <= u) { + fired0 = 1; + } + } + + // --- transition 1: recovery (I>=1) --- + if (iv >= 1) { + state = (state + 0x6d2b79f5) >>> 0; + let t = state; + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + const u = ((t ^ (t >>> 14)) >>> 0) / 4294967296; + const lambda = RECOVERY_RATE * (elapsed1[run] * DT); + if (Math.exp(-lambda) <= u) { + fired1 = 1; + } + } + + rng[run] = state; + + if (fired0 === 1) { + s[run] = sv - 1; + i[run] = iv + 1; + elapsed0[run] = 0; + } else { + elapsed0[run]++; + } + + if (fired1 === 1) { + i[run] = i[run] - 1; + r[run] = r[run] + 1; + elapsed1[run] = 0; + } else { + elapsed1[run]++; + } + + frameNumber[run]++; + advancedTotal++; + + if (frameNumber[run] >= MAX_FRAMES) { + active[run] = 0; + activeCount--; + } + } +} + +const ms = performance.now() - start; +process.stdout.write( + `flat SoA stepper: ${ms.toFixed(0)} ms, ${advancedTotal} run-frames, ${( + (ms / advancedTotal) * + 1e6 + ).toFixed(0)} ns/run-frame\n`, +); + +let sumR = 0; +for (let run = 0; run < RUNS; run++) { + sumR += r[run]; +} +process.stdout.write(`mean recovered: ${(sumR / RUNS).toFixed(1)}\n`); diff --git a/libs/@hashintel/petrinaut-core/benchmarks/monte-carlo-throughput.mjs b/libs/@hashintel/petrinaut-core/benchmarks/monte-carlo-throughput.mjs new file mode 100644 index 00000000000..f8643674cd9 --- /dev/null +++ b/libs/@hashintel/petrinaut-core/benchmarks/monte-carlo-throughput.mjs @@ -0,0 +1,117 @@ +/** + * Baseline throughput of the current Monte Carlo engine, and the cost that + * metric aggregation adds on top of it. + * + * Reports nanoseconds per *run-frame* (one run advanced by one frame), which is + * the unit that stays comparable as run counts and early-completion rates + * change. + */ +import { performance } from "node:perf_hooks"; + +const { + createMonteCarloSimulator, + createMonteCarloUserDefinedMetric, + createMonteCarloUserDefinedMetricConfigsFromSpecs, +} = await import("../dist/index.js"); +const { compileHirArtifacts } = await import("../dist/hir.js"); +const { sirModel } = await import("../dist/examples/index.js"); + +const RUNS = 4000; +const DT = 0.1; +const MAX_TIME = 60; + +const sdcpn = sirModel.petriNetDefinition; +const artifacts = compileHirArtifacts(sdcpn).artifacts; + +const initialMarking = { + place__susceptible: 500, + place__infected: 5, + place__recovered: 0, +}; + +function build(metricSpecs) { + const metrics = createMonteCarloUserDefinedMetricConfigsFromSpecs( + metricSpecs, + sdcpn, + {}, + ).map((config) => createMonteCarloUserDefinedMetric(config)); + + return createMonteCarloSimulator({ + sdcpn, + initialMarking, + parameterValues: {}, + seed: 42, + dt: DT, + maxTime: MAX_TIME, + runCount: RUNS, + hirArtifacts: artifacts, + metrics, + }); +} + +function measure(label, metricSpecs) { + // Construction is timed separately: buildSimulation() runs once per run, so + // it is a per-run cost rather than a per-frame one. + const constructStart = performance.now(); + const simulator = build(metricSpecs); + const constructMs = performance.now() - constructStart; + + const runStart = performance.now(); + simulator.runUntilComplete(); + const runMs = performance.now() - runStart; + + let runFrames = 0; + for (const summary of simulator.getSummaries()) { + runFrames += summary.frameNumber; + } + + process.stdout.write( + `${label.padEnd(30)} construct ${constructMs.toFixed(0).padStart(5)} ms ` + + `(${((constructMs / RUNS) * 1000).toFixed(0).padStart(4)} µs/run) ` + + `simulate ${runMs.toFixed(0).padStart(6)} ms ` + + `${((runMs / runFrames) * 1e6).toFixed(0).padStart(5)} ns/run-frame\n`, + ); + + return runMs; +} + +const distribution = (id, label, placeId) => ({ + kind: "placeTokenCountMean", + id, + label, + placeId, + runOutput: { type: "distribution", binning: "exact" }, +}); + +process.stdout.write( + `SIR model, ${RUNS} runs, dt ${DT}, maxTime ${MAX_TIME}\n\n`, +); + +const bare = measure("engine only (no metrics)", []); +measure("+ 1 scalar metric", [ + { + kind: "placeTokenCountMean", + id: "m1", + label: "Infected", + placeId: "place__infected", + }, +]); +const hist = measure("+ 1 distribution metric", [ + distribution("m2", "Infected", "place__infected"), +]); +const three = measure("+ 3 metrics (2 distributions)", [ + distribution("a", "S", "place__susceptible"), + distribution("b", "I", "place__infected"), + { + kind: "placeTokenCountMean", + id: "c", + label: "R", + placeId: "place__recovered", + }, +]); + +process.stdout.write( + `\nmetric overhead vs engine only: ` + + `1 distribution ${(((hist - bare) / bare) * 100).toFixed(0)}%, ` + + `3 metrics ${(((three - bare) / bare) * 100).toFixed(0)}%\n`, +); diff --git a/libs/@hashintel/petrinaut-core/benchmarks/shard-main.mjs b/libs/@hashintel/petrinaut-core/benchmarks/shard-main.mjs new file mode 100644 index 00000000000..499966ca6c1 --- /dev/null +++ b/libs/@hashintel/petrinaut-core/benchmarks/shard-main.mjs @@ -0,0 +1,121 @@ +/** + * Prototype: shard one experiment's runs across N worker threads and verify + * (a) wall-clock scaling, and + * (b) that merged per-frame histograms are IDENTICAL regardless of shard + * count — i.e. sharding is result-preserving. + * + * Uses the shipped MonteCarloSimulator unchanged. The only new machinery is + * global-index seed derivation plus a monoid merge of per-frame metric state. + */ +import { availableParallelism } from "node:os"; +import { performance } from "node:perf_hooks"; +import { Worker } from "node:worker_threads"; + +const WORKER = new URL("./shard-worker.mjs", import.meta.url); + +const TOTAL_RUNS = 4000; +const DT = 0.1; +const MAX_TIME = 60; +const BASE_SEED = 42; + +function runShard(runOffset, runCount) { + return new Promise((resolve, reject) => { + const worker = new Worker(WORKER, { + workerData: { + runOffset, + runCount, + baseSeed: BASE_SEED, + dt: DT, + maxTime: MAX_TIME, + }, + }); + worker.once("message", (message) => { + resolve(message); + void worker.terminate(); + }); + worker.once("error", reject); + }); +} + +/** The histogram monoid: merge bin lists by summing frequencies. */ +function mergeShards(results) { + const byFrame = []; + for (const result of results) { + for (const { frameNumber, bins } of result.partialFrames) { + byFrame[frameNumber] ??= new Map(); + const target = byFrame[frameNumber]; + for (const [value, frequency] of bins) { + target.set(value, (target.get(value) ?? 0) + frequency); + } + } + } + return byFrame; +} + +/** Canonical string for comparing merged results across shard counts. */ +function fingerprint(frames) { + return frames + .map((bins, index) => + bins === undefined + ? `${index}:-` + : `${index}:${[...bins.entries()] + .sort(([a], [b]) => a - b) + .map(([value, frequency]) => `${value}x${frequency}`) + .join(",")}`, + ) + .join("|"); +} + +async function measure(shardCount) { + const base = Math.floor(TOTAL_RUNS / shardCount); + const remainder = TOTAL_RUNS % shardCount; + + const shards = []; + let offset = 0; + for (let shard = 0; shard < shardCount; shard++) { + const count = base + (shard < remainder ? 1 : 0); + shards.push({ offset, count }); + offset += count; + } + + const start = performance.now(); + const results = await Promise.all( + shards.map((shard) => runShard(shard.offset, shard.count)), + ); + const ms = performance.now() - start; + + return { + shardCount, + ms, + runFrames: results.reduce((sum, result) => sum + result.runFrames, 0), + fingerprint: fingerprint(mergeShards(results)), + }; +} + +const cores = availableParallelism(); +process.stdout.write( + `availableParallelism = ${cores}\n` + + `${TOTAL_RUNS} runs, dt ${DT}, maxTime ${MAX_TIME}, 1 distribution metric\n` + + `wall-clock includes worker spawn + per-shard HIR compile + per-run buildSimulation\n\n`, +); + +let baselineMs = null; +let baselineFingerprint = null; + +for (const shardCount of [1, 2, 4, 8, 12]) { + // eslint-disable-next-line no-await-in-loop -- sequential so each config gets all cores + const result = await measure(shardCount); + baselineMs ??= result.ms; + baselineFingerprint ??= result.fingerprint; + + process.stdout.write( + `${String(result.shardCount).padStart(2)} shard(s) ${result.ms + .toFixed(0) + .padStart(6)} ms` + + ` speedup ${(baselineMs / result.ms).toFixed(2).padStart(5)}x` + + ` ${result.runFrames} run-frames` + + ` identical to 1 shard: ${ + result.fingerprint === baselineFingerprint ? "YES" : "NO" + }\n`, + ); +} diff --git a/libs/@hashintel/petrinaut-core/benchmarks/shard-worker.mjs b/libs/@hashintel/petrinaut-core/benchmarks/shard-worker.mjs new file mode 100644 index 00000000000..41c23b6090a --- /dev/null +++ b/libs/@hashintel/petrinaut-core/benchmarks/shard-worker.mjs @@ -0,0 +1,81 @@ +/** + * Shard worker: owns a contiguous slice of an experiment's runs and advances + * them with the *unmodified* MonteCarloSimulator, then ships back partial + * per-frame metric state for the main thread to merge. + */ +import { parentPort, workerData } from "node:worker_threads"; + +import { sirModel } from "../dist/examples/index.js"; +import { compileHirArtifacts } from "../dist/hir.js"; +import { + createMonteCarloSimulator, + createMonteCarloUserDefinedMetric, + createMonteCarloUserDefinedMetricConfigsFromSpecs, +} from "../dist/index.js"; + +const { runOffset, runCount, baseSeed, dt, maxTime } = workerData; + +const sdcpn = sirModel.petriNetDefinition; +const artifacts = compileHirArtifacts(sdcpn).artifacts; + +const metrics = createMonteCarloUserDefinedMetricConfigsFromSpecs( + [ + { + kind: "placeTokenCountMean", + id: "infected", + label: "Infected", + placeId: "place__infected", + runOutput: { type: "distribution", binning: "exact" }, + }, + ], + sdcpn, + {}, +).map((config) => createMonteCarloUserDefinedMetric(config)); + +/** + * Seeds are derived from the GLOBAL run index, not the shard-local one, so + * sharding changes only *who* runs a seed — never *which* seeds run. This is + * what makes shard count invisible in the results. + * + * Mirrors `deriveRunSeed` in monte-carlo/run-state.ts. + */ +function deriveRunSeed(seed, globalRunIndex) { + return ( + Math.abs(Math.trunc(seed + (globalRunIndex + 1) * 2_654_435_761)) % + 2_147_483_648 + ); +} + +const simulator = createMonteCarloSimulator({ + sdcpn, + initialMarking: { + place__susceptible: 500, + place__infected: 5, + place__recovered: 0, + }, + parameterValues: {}, + seed: baseSeed, + dt, + maxTime, + runCount, + hirArtifacts: artifacts, + metrics, + runs: Array.from({ length: runCount }, (_, localIndex) => ({ + seed: deriveRunSeed(baseSeed, runOffset + localIndex), + })), +}); + +simulator.runUntilComplete(); + +let runFrames = 0; +for (const summary of simulator.getSummaries()) { + runFrames += summary.frameNumber; +} + +parentPort.postMessage({ + runFrames, + partialFrames: metrics[0].frames.map((frame) => ({ + frameNumber: frame.frameNumber, + bins: frame.bins ?? [], + })), +}); diff --git a/libs/@hashintel/petrinaut-core/benchmarks/sharded-experiment-worker.mjs b/libs/@hashintel/petrinaut-core/benchmarks/sharded-experiment-worker.mjs new file mode 100644 index 00000000000..1ac61aa6e57 --- /dev/null +++ b/libs/@hashintel/petrinaut-core/benchmarks/sharded-experiment-worker.mjs @@ -0,0 +1,39 @@ +/** + * Node worker entry for `sharded-experiment.mjs`. + * + * The shipped Monte Carlo worker is written against the browser worker global + * scope, so under `node:worker_threads` its `self`-style message plumbing is + * bridged to `parentPort` before importing it. + * + * The `dist` build wraps the worker in an inline Blob for the browser, which + * Node cannot import, so `sharded-experiment.mjs` bundles the worker source for + * Node first and this file loads that bundle. + */ +import { parentPort } from "node:worker_threads"; + +const listeners = new Set(); + +globalThis.self = { + postMessage: (message) => parentPort.postMessage(message), + addEventListener: (type, listener) => { + if (type === "message") { + listeners.add(listener); + } + }, + removeEventListener: (type, listener) => { + if (type === "message") { + listeners.delete(listener); + } + }, +}; +globalThis.postMessage = globalThis.self.postMessage; +globalThis.addEventListener = globalThis.self.addEventListener; +globalThis.removeEventListener = globalThis.self.removeEventListener; + +parentPort.on("message", (data) => { + for (const listener of listeners) { + listener({ data }); + } +}); + +await import("./.node-worker-bundle.mjs"); diff --git a/libs/@hashintel/petrinaut-core/benchmarks/sharded-experiment.mjs b/libs/@hashintel/petrinaut-core/benchmarks/sharded-experiment.mjs new file mode 100644 index 00000000000..f2e0c0581f0 --- /dev/null +++ b/libs/@hashintel/petrinaut-core/benchmarks/sharded-experiment.mjs @@ -0,0 +1,151 @@ +/** + * End-to-end check of the production sharded experiment runtime. + * + * Runs the same experiment at several shard counts through + * `createMonteCarloExperiment` with real worker threads, and verifies that the + * merged metric timeline is identical every time. This is the guarantee the + * whole design rests on: shard count changes how fast an experiment finishes, + * never what it reports. + * + * Requires a build (`yarn build`) because the worker is loaded from `dist`. + */ +import { execFileSync } from "node:child_process"; +import { availableParallelism } from "node:os"; +import { performance } from "node:perf_hooks"; +import { Worker } from "node:worker_threads"; + +const { createMonteCarloExperiment } = await import("../dist/index.js"); +const { compileHirArtifacts } = await import("../dist/hir.js"); +const { sirModel } = await import("../dist/examples/index.js"); + +// `dist` wraps the worker in an inline Blob for the browser, which Node cannot +// import, so bundle the worker source for Node before spawning any threads. +execFileSync( + "npx", + [ + "esbuild", + "src/simulation/monte-carlo/worker/monte-carlo.worker.ts", + "--bundle", + "--format=esm", + "--platform=node", + "--outfile=benchmarks/.node-worker-bundle.mjs", + "--log-level=warning", + ], + { cwd: new URL("..", import.meta.url), stdio: "inherit" }, +); + +const TOTAL_RUNS = 2000; +const DT = 0.1; +const MAX_TIME = 60; + +const sdcpn = sirModel.petriNetDefinition; +const artifacts = compileHirArtifacts(sdcpn).artifacts; + +const workerUrl = new URL("./sharded-experiment-worker.mjs", import.meta.url); + +/** + * Adapts a Node `Worker` to the `WorkerLike` shape the transport expects. + * + * In the browser this is just `new Worker(...)`; Node's event API differs + * enough to need a thin shim. + */ +function createWorker() { + const worker = new Worker(workerUrl); + + return { + postMessage: (message) => worker.postMessage(message), + addEventListener: (_type, listener) => { + worker.on("message", (data) => listener({ data })); + }, + terminate: () => void worker.terminate(), + }; +} + +function runExperiment(shardCount) { + return new Promise((resolve, reject) => { + const start = performance.now(); + + createMonteCarloExperiment({ + createWorker, + shardCount, + sdcpn, + initialMarking: { + place__susceptible: 500, + place__infected: 5, + place__recovered: 0, + }, + parameterValues: {}, + seed: 42, + dt: DT, + maxTime: MAX_TIME, + hirArtifacts: artifacts, + runCount: TOTAL_RUNS, + metricSpecs: [ + { + kind: "placeTokenCountMean", + id: "infected", + label: "Infected", + placeId: "place__infected", + runOutput: { type: "distribution", binning: "exact" }, + }, + { + kind: "placeTokenCountMean", + id: "recovered-mean", + label: "Recovered (mean)", + placeId: "place__recovered", + }, + ], + }).then((experiment) => { + experiment.events.subscribe((event) => { + if (event.type === "complete") { + const ms = performance.now() - start; + const { frames } = experiment.metrics.get(); + experiment.dispose(); + resolve({ shardCount, ms, frames }); + } else if (event.type === "error") { + experiment.dispose(); + reject(new Error(event.message)); + } + }); + experiment.start(); + }, reject); + }); +} + +/** Canonical form of the merged timeline, for exact comparison. */ +function fingerprint(frames) { + return frames + .map((frame) => + frame.outputType === "distribution" + ? `${frame.metricId}@${frame.frameNumber}:${frame.bins + .map(([value, frequency]) => `${value}x${frequency}`) + .join(",")}` + : `${frame.metricId}@${frame.frameNumber}:${frame.frameValue}/${frame.runSampleCount}`, + ) + .join("|"); +} + +process.stdout.write( + `availableParallelism = ${availableParallelism()}\n` + + `${TOTAL_RUNS} runs, dt ${DT}, maxTime ${MAX_TIME}, 1 distribution + 1 scalar metric\n\n`, +); + +let baseline = null; + +for (const shardCount of [1, 2, 4, 8]) { + // eslint-disable-next-line no-await-in-loop -- sequential so each run gets all cores + const result = await runExperiment(shardCount); + baseline ??= result; + + const identical = fingerprint(result.frames) === fingerprint(baseline.frames); + process.stdout.write( + `${String(shardCount).padStart(2)} shard(s) ${result.ms.toFixed(0).padStart(6)} ms` + + ` speedup ${(baseline.ms / result.ms).toFixed(2).padStart(5)}x` + + ` ${String(result.frames.length).padStart(5)} merged frames` + + ` identical to 1 shard: ${identical ? "YES" : "NO"}\n`, + ); + + if (!identical) { + process.exitCode = 1; + } +} diff --git a/libs/@hashintel/petrinaut-core/docs/simulation-performance.md b/libs/@hashintel/petrinaut-core/docs/simulation-performance.md new file mode 100644 index 00000000000..84fba39dbfc --- /dev/null +++ b/libs/@hashintel/petrinaut-core/docs/simulation-performance.md @@ -0,0 +1,686 @@ +# Simulation performance: threads, WASM, and GPU + +Status: §3 (worker sharding) is implemented. §2 and §4 measurements still stand +and are unaddressed. §5–§9 remain proposals. + +Goal: make **Experiments** (Monte Carlo batches) as fast as possible, with +per-seed parallelism across threads/workers, and decide whether WASM (browser), +native (server), and WebGPU are worth their cost. + +Everything labelled _measured_ below comes from harnesses in +[`../benchmarks/`](../benchmarks/README.md), run on a 10-core Apple Silicon +laptop, Node 25.6, against the built `dist` of this package. + +--- + +## TL;DR + +1. **The engine is ~36× slower than equivalent flat JavaScript.** The gap is + architectural (allocation, copying, string-keyed lookup, per-run + recompilation), not "JavaScript is slow". WASM addresses the _wrong_ 3% of + the problem until that is fixed. +2. **There is a quadratic blow-up on coloured input arcs with weight ≥ 2** that + dwarfs everything else: 13.4 ms _per run-frame_ at 400 tokens in a place. + That is ~6.7 hours for a 1000-run × 1800-frame experiment. Fixing it is a + contained change and should happen before anything on this list. +3. **Per-seed worker sharding needs no `SharedArrayBuffer` and is + result-preserving — now implemented** (§3.3): ~4× on 8 shards (10-core + machine), byte-identical output at every shard count. The metric accumulators + were already monoids (`empty`/`merge`), so this was designed for. +4. **Optional per-place token capacity is the keystone** (§5). Beyond the + modelling feature, it converts frames from growable to fixed-size, which is + the precondition for SoA layout, a WASM linear-memory ABI, a computable + state-space bound, and any GPU path. +5. **WASM is worth doing, but as a second codegen backend behind whole-loop + codegen, not as a rewrite.** Expect ~1.5–3× over _good_ JS, not over + today's engine. +6. **WebGPU: discrete transitions are possible but only for a restricted + subset, and WGSL has no `f64`** — so a GPU path is a numerically-different + fork, not an acceleration of the existing one. Offloading _only_ ODEs is + architecturally worse than doing nothing (per-frame round-trip). + +Done: **§3 worker sharding**. +Remaining, in order: **§4 hot-path fixes → §5 capacities → §6 whole-loop +codegen → §7 WASM/native → §8 GPU (spike only)**. + +§4 item 1 (the quadratic enumeration blow-up) is the single highest-value change +left and is independent of everything else. + +--- + +## 1. What runs today + +Two engines share `engine/` compilation but differ in stepping: + +| Path | Used by | Frame storage | +| ------------------------------ | ------------------------------- | -------------------------------------------- | +| `engine/compute-next-frame.ts` | interactive single-run playback | immutable; appends every frame to `frames[]` | +| `monte-carlo/advance-run.ts` | **Experiments** | two reusable buffers, swapped per step | + +Experiments are the target, so everything below concerns the Monte Carlo path. +One experiment now runs several Web Workers (§3.3), each owning a slice of the +runs and running its own `MonteCarloSimulator`, which advances its runs +round-robin one frame at a time (`advanceAll()`) and streams per-frame metric +state to the main thread for merging. The §2 measurements below are per worker +and unaffected by that fan-out. + +User code (dynamics, lambdas, kernels, expression metrics) is compiled by the +HIR pipeline in the LSP worker into buffer-ABI JS programs +(`hir/emit-buffer-js.ts`), instantiated via `new Function`, and reads packed +token bytes directly. That part is already well designed — it is the _engine +around it_ that is expensive. + +--- + +## 2. Where the time actually goes (measured) + +### 2.1 Baseline throughput + +SIR example, 500 S + 5 I, 3 uncoloured places, 2 transitions, `dt` 0.1, +`maxTime` 60, 4000 runs (2.4 M run-frames), single thread: + +| Configuration | ns / run-frame | +| --------------------------------------------- | -------------- | +| Current engine, no metrics | **1 143** | +| Current engine, 1 scalar metric | 1 385 | +| Current engine, 1 distribution metric | 2 223 | +| Current engine, 3 metrics (2 distributions) | 3 798 | +| Hand-written flat SoA stepper, same semantics | **31** | + +The last row is the headline. `benchmarks/flat-stepper-ceiling.mjs` implements +the same net — same RNG shape, same `exp(-λ·elapsed) ≤ u` acceptance test, same +deadlock rule — as typed arrays with no allocation, no string lookup, and no +per-frame copying. It produces the same mean recovered count and runs **~36× +faster**. + +Caveat, stated plainly: that stepper is _specialised to one net_. A generic +engine cannot reach 31 ns. But a **code-generating** engine can get close, and +this codebase already generates code (§6). Treat 36× as the ceiling and +5–15× as the realistic target for a generic flat rewrite. + +### 2.2 Self-time profile + +`--cpu-prof` over 2.4 M run-frames, simulation portion only: + +| Cost centre | Share | Why | +| ------------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------- | +| `computeTransitionEffect` (+ closures) | **29%** | allocates an object array per transition per frame (`inputPlaces.map(spread)`), then three more arrays via `.every`/`.filter`×2 | +| `copyMonteCarloFrameBuffer` | **20%** | full frame memcpy every step — _even with zero dynamics_ | +| `advanceRun` + `advanceAll` | 14% | scheduling, `Set`/`Map` churn per step | +| `nextRandom` | 9% | one draw per transition per frame | +| token add/remove/merge | 7% | `Object.entries`, `new Set`, `Record` | +| `enumerateWeightedMarkingIndicesGenerator` | 4% | see §2.4 | +| `getPlaceIndex` | 3% | `Map` lookup **in the innermost loop** | +| GC | 2% | consequence of the above | + +Note what is _absent_: the compiled user code barely registers. The buffer-ABI +programs are fast. The engine wrapping them is not. + +Three of these are pure waste rather than trade-offs: + +- **The frame copy.** `writeFrameAfterDynamics` copies current → next + unconditionally, then applies dynamics. With no dynamics-enabled places + (the SIR case, and many others) the copy is the only thing that happens. +- **String-keyed place lookup.** `frameLayout.placeIndexById.get(placeId)` runs + per place per transition per frame. Indices are known at build time. +- **`Record` for removals/additions.** Every firing allocates + objects keyed by string IDs, immediately iterated with `Object.entries`. + +### 2.3 Per-run construction + +`createRunState` calls `buildSimulation()` **once per run**, and +`buildSimulation` calls `instantiateHirBuffer{Lambda,Kernel,Dynamics}`, each of +which is a `new Function`. + +| Runs | Construction | Per run | +| ----- | ------------ | ------- | +| 100 | 23 ms | 229 µs | +| 1 000 | 95 ms | 95 µs | +| 4 000 | 319 ms | 80 µs | + +For a 1000-run experiment on a 5-transition net that is ~10 000 `new Function` +calls, ~1000 duplicate `frameLayout` maps, and 1000 distinct function identities +at every shared call site in the engine — which also defeats V8 inlining. The +compiled programs are **pure and stateless**; only the scratch buffers +(`placeBases`, `indices`, `kernelStaging`) are per-run, and those are small. +One compiled `SimulationDefinition` should be shared by all runs in a shard, +with per-run state reduced to buffers + RNG + counters. + +This also explains part of why sharding scales sub-linearly (§3): every shard +pays its own construction cost. + +### 2.4 The quadratic blow-up (most important finding) + +`enumerateWeightedMarkingIndicesGenerator` is a generator, but it is only lazy +over the _Cartesian product across arcs_. Per arc it eagerly materialises the +full combination list: + +```ts +const perPlaceCombos = places.map((p) => indexCombinations(p.count, p.weight)); +``` + +`indexCombinations(n, k)` backtracks and pushes **every** k-combination into an +array before a single one is yielded. So a transition with a coloured input arc +of weight `w` over a place holding `n` tokens allocates `C(n, w)` arrays on +_every evaluation, every frame_ — and `computeTransitionEffect` `return`s on the +first accepted combination, so nearly all of it is discarded. + +Measured, one coloured place, one weight-2 input arc, transition never fires: + +| Tokens in place | `C(n,2)` | ns / run-frame | +| --------------- | -------- | -------------- | +| 10 | 45 | 17 892 | +| 25 | 300 | 58 276 | +| 50 | 1 225 | 214 988 | +| 100 | 4 950 | 870 518 | +| 200 | 19 900 | 3 318 793 | +| 400 | 79 800 | **13 406 833** | + +Clean quadratic, ~170 ns per enumerated combination. At 400 tokens that is +**13.4 ms for one run-frame**; a 1000-run × 1800-frame experiment would take +~6.7 hours. Weight 3 makes it cubic. + +The general bound for one transition is `∏ᵢ C(nᵢ, wᵢ)` over its coloured +non-inhibitor input arcs. + +**Fix** (contained, no architecture change): iterate combinations by index +without materialising them — an odometer over per-arc combination ranks, with +`combinationCount = C(n, w)` computed arithmetically and the _k_-th combination +unranked on demand. Cost becomes proportional to combinations actually +_examined_, which for a firing transition is often 1. This is worth doing +independently of everything else in this document. + +### 2.5 Metric aggregation + +`createMonteCarloMetricHistogramAccumulator().add` does `new Map(state)` per +sample — an O(bins) copy per run per frame per metric. With 4000 runs that is +4000 map clones per frame. Measured cost at 4000 runs: **+94%** for one +distribution metric, **+232%** for three metrics. + +Worse, that cost is not constant per unit of work — it **grows with run count**, +because bin count grows with the number of runs sampled: + +| Runs | Engine ns/run-frame | With 1 distribution metric | Metric cost | +| ----- | ------------------- | -------------------------- | ----------- | +| 125 | 1 140 | 1 565 | 425 | +| 500 | 893 | 1 722 | 828 | +| 1 000 | 979 | 1 745 | 766 | +| 4 000 | 1 101 | 2 139 | **1 038** | + +The engine term is flat (~900–1150 ns, within noise); the metric term rises +2.4× across a 32× increase in runs. So doubling an experiment's run count more +than doubles its distribution-metric time — exactly the wrong scaling property +for a tool whose answer to "I need better statistics" is "run more seeds". + +The accumulators are already monoids, which is exactly right for sharding +(§3); they just need mutable-in-place `add` on a private state object, plus a +reused typed-array bin table when binning is fixed-width. Also, +`forEachRunFrame` allocates a fresh `SimulationFrameReader` **per run per frame +per metric** (`createMonteCarloFrameReader`) — that should be one reusable +cursor rebound to each run. + +--- + +## 3. Per-seed parallelism (the headline ask) + +### 3.1 Why this is the easy win + +Runs are fully independent: separate seed, separate RNG state, separate frame +buffers, no shared mutable state. The only cross-run interaction is metric +aggregation, and `MonteCarloMetricMonoid` already exposes `empty`/`merge`. + +So the design is: **shard runs across N workers; each shard runs the existing +simulator over its slice; the main thread merges per-frame metric states.** + +Two invariants make shard count invisible to results: + +1. **Seeds derive from the global run index**, not the shard-local one — so run + _i_ gets the same seed no matter which shard owns it. `deriveRunSeed` already + takes a run index; shards must pass the global one. +2. **Merge is associative and commutative** over the per-frame accumulator + state, so shard completion order does not matter. + +Notably, this needs **no `SharedArrayBuffer`**. The app is not cross-origin +isolated (no COOP/COEP headers), so `SharedArrayBuffer` — and therefore WASM +threads — is unavailable today. Message-passing shards with transferable +`ArrayBuffer`s sidestep that entirely. Worth remembering when §7 tempts you +toward WASM threading. + +### 3.2 Prototype and results + +[`../benchmarks/shard-main.mjs`](../benchmarks/README.md) shards 4000 SIR runs +across Node `worker_threads`, using the **shipped, unmodified** +`MonteCarloSimulator`, with one distribution metric merged on the main thread: + +| Shards | Wall clock | Speedup | Merged result identical to 1 shard | +| ------ | ---------- | ------- | ---------------------------------- | +| 1 | 5 866 ms | 1.00× | — | +| 2 | 2 990 ms | 1.96× | **yes** | +| 4 | 1 785 ms | 3.29× | **yes** | +| 8 | 1 414 ms | 4.15× | **yes** | +| 12 | 1 619 ms | 3.62× | **yes** | + +One representative run; timings vary ±10% between invocations (8 shards +measured between 3.79× and 4.18× across repeats). Result-preservation does not +vary — it is exact, verified by fingerprinting every frame's merged histogram +and comparing across shard counts, identical on every row of every repeat. That +is the property that matters most: sharding must not change what an experiment +reports. + +Scaling is sub-linear and regresses past core count, for understandable +reasons: 4 performance + 6 efficiency cores, per-shard HIR compile, and the +per-run construction cost of §2.3 paid N times. Fixing §2.3 should improve +parallel efficiency as well as single-thread speed. + +### 3.3 What shipped + +Implemented — `createMonteCarloExperiment` now fans out over N transports: + +| Piece | Where | +| ----------------------------------------- | ---------------------------------------------- | +| Shard sizing and contiguous split | `monte-carlo/runtime/shard-plan.ts` | +| Global-index seed derivation | `MonteCarloSimulatorConfig.runIndexOffset` | +| Streaming monoid merge of per-frame state | `monte-carlo/metrics/merge.ts` | +| Fan-out, progress, lifecycle | `monte-carlo/runtime/experiment.ts` | +| Host-level override | `ExperimentsProvider`'s `experimentShardCount` | + +Two design points worth keeping in mind when touching this: + +- **Scalar frames carry `runAggregate`**, the pre-reduction accumulator state, + because `frameValue` cannot be merged — a mean of means is not a mean. Time + aggregation is recomputed on the main thread from merged frame values, while + distribution metrics aggregate per _run_ over time and so are already correct + shard-locally. +- **Frames are released on a watermark**: a frame number finalises only once + every still-running shard has reported it, with finished shards dropped from + the watermark rather than blocking it. That is also why merged output matches + an unsharded run — a finished shard has no active runs left to contribute, + exactly like the completed runs a single simulator skips. + +Measured end to end by `benchmarks/sharded-experiment.mjs`, which drives the real +runtime over real worker threads and fails if any shard count changes results: + +| Shards | Wall clock | Speedup | Identical to 1 shard | +| ------ | ---------- | ------- | -------------------- | +| 1 | 3 156 ms | 1.00× | — | +| 2 | 1 667 ms | 1.89× | yes | +| 4 | 1 058 ms | 2.98× | yes | +| 8 | 769 ms | 4.11× | yes | + +Still open: + +- **Concurrent experiments do not share a pool.** Three 8-way experiments spawn + 24 workers and compete for cores. Memory is roughly unchanged (sharding splits + runs rather than duplicating them), so this degrades gracefully rather than + breaking, and the docs tell users to run experiments one at a time for maximum + speed — but a shared pool would be better. +- **Metric state crosses threads as `[number, number][]`**, not typed arrays, so + it is cloned rather than transferred. +- Scaling is sub-linear past ~4 shards, partly from efficiency cores and partly + because each shard repays the per-run construction cost of §2.3. + +Server side (`petrinaut-cli`), the same sharding applies via +`node:worker_threads` — not yet wired up. Note the existing memory constraint +recorded for the optimisation path — a 1 GB ECS task against multiple Node CLIs +each capped at 768 MB — so shard count there must be bounded by memory, not just +cores. + +--- + +## 4. Hot-path fixes (do these first) + +Ordered by measured value per unit of risk. None require new toolchain, new +ABI, or user-visible change. + +| # | Change | Expected | +| --- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------- | +| 1 | Lazy, index-based combination enumeration (§2.4) | up to ~1000× on affected nets; nothing on weight-1 nets | +| 2 | Share one compiled `SimulationDefinition` across a shard's runs (§2.3) | removes 80–230 µs/run and improves inlining | +| 3 | Skip the frame copy when no place has dynamics; otherwise copy only dynamic places | ~20% | +| 4 | Replace `Record` removals/additions with dense per-place-index typed arrays | ~10% + most GC | +| 5 | Resolve place/transition indices at build time; delete `getPlaceIndex` from the hot loop | ~3% | +| 6 | Mutable-in-place metric accumulators + reusable frame cursor (§2.5) | ~30–50% of metric overhead | +| 7 | Single RNG draw per frame reused across transitions where semantics allow | up to 9% — **needs a semantics decision**, changes streams | + +Items 1–6 are behaviour-preserving. Item 7 changes RNG streams, so existing +seeds would produce different (equally valid) trajectories — a product call, +not an engineering one. + +My estimate for 1–6 combined on a typical coloured net: **5–15×**, before any +threading. Combined with §3 on 8 cores: **20–60×**. That is likely enough to +make WASM optional rather than urgent. + +--- + +## 5. Optional per-place token capacity + +You raised this as a prerequisite; it is, and for more reasons than frame +sizing. + +### 5.1 Shape + +Add to `Place` in `types/sdcpn.ts`: + +```ts +/** Optional hard upper bound on tokens in this place. */ +capacity?: number | null; +``` + +Semantics to decide (see §10): when a firing would exceed capacity, is the +transition _not enabled_ (classical capacity / self-loop semantics), or does it +fire and **error**? These differ observably. Classical Petri-net capacity treats +it as an enablement condition, and that is also the cheaper check — it folds +into the existing structural-enablement test alongside inhibitor arcs. + +### 5.2 What it unlocks + +**Fixed frame size.** With every place bounded, the frame's byte length is +static: + +```text +frameBytes = header + + placeCount × (4 + 4) // counts + offsets + + transitionCount × (8 + 4 + 1) // elapsed + firingCount + flag + + Σ_p capacity_p × stride_p // token region, padded +``` + +No `ensureFrameCapacity`, no reallocation, no `reallocations` counter, no +per-step repacking to keep places contiguous — each place gets a fixed slot and +`placeOffsets` becomes a build-time constant. That removes `applyTokenAdditions` +repacking entirely (a measured 3%, worse on colour-heavy nets) and makes frames +trivially poolable and transferable. + +**A computable state-space bound.** For uncoloured places, reachable markings +are at most `∏ (capacity_p + 1)`. Report it in the UI so users see when they +have specified something astronomically large. For coloured places the bound is +over token _values_ too and is generally not enumerable — be careful not to +promise a "combinatorial explosion" number that only holds for uncoloured nets. + +**A static worst-case enumeration bound.** Per transition, +`∏ᵢ C(capacity_i, wᵢ)` (§2.4) is computable at compile time. This is worth +surfacing as a **lint**: "this transition can enumerate up to 79 800 token +combinations per frame". That warning would have caught §2.4 before it was +measured. + +**The GPU/WASM precondition.** Fixed per-run frame size is what lets you +allocate `runs × frameBytes` as one contiguous block — required for a WASM +linear-memory layout and mandatory for GPU (§8). + +### 5.3 Proposed semantics + +A transition is not enabled when firing would take any output place above its +capacity — the supply-side mirror of an input arc that cannot be satisfied. Three +details that were decisions rather than consequences: + +- **Net change, not gross output.** Constraints are the sum of output arc weights + on a place minus the _standard_ input arc weights on the same place, so a + 1-in/1-out self loop is never blocked by its own full place. Read and inhibitor + arcs consume nothing and therefore do not make room. +- **Same-frame pending output counts.** Output tokens are applied once at the end + of a frame, so the frame's counts lag during transition evaluation. Without + folding in what earlier transitions already committed, two producers feeding + one capped place would each individually fit and jointly overflow. Tracked in + `MonteCarloRunState.pendingOutputCounts`, allocated only for nets that declare + a capacity. +- **Deadlock includes capacity.** Both engines' structural-enablement checks + consider capacity, so a net whose remaining transitions are all blocked by full + places reports deadlock instead of stepping to `maxTime` with nothing + happening. + +Constraints are precomputed per transition at build time +(`engine/capacity.ts`) and would be empty for nets without capacities, so the hot path +pays nothing for a feature it does not use. + +An initial marking above a place's capacity is rejected at build time: capacity +blocks transitions, so it cannot repair a starting state that already violates +the bound. + +Shipped alongside: the `Place.capacity` field and zod schema, a place-inspector +control, and `drawing-a-net.md` / `simulation.md` updates. + +Not done: capacity does **not** yet participate in the HIR artifact fingerprint. +It changes enablement but not any compiled program, so artifacts stay valid; if +capacity ever feeds into codegen (§6) that has to change. The fixed-size frame +layout this unlocks (§5.2) is also still unbuilt — the runtime keeps using +growable frames. + +--- + +## 6. Whole-loop codegen (where the real win is) + +The engine currently interprets net structure at runtime: loop over +`transitionIds`, look up compiled transition, build input descriptors, enumerate, +call lambda, call kernel, apply effects generically. The user code is compiled; +the _net_ is not. + +Since the net is known when an experiment starts, emit **one specialised +stepper per net** — the whole frame loop, with structure unrolled and user code +inlined: + +```text +for each run in shard: + # transition 0: infection (S ≥ 1, I ≥ 1) + if (counts[0] >= 1 && counts[1] >= 1) { …inlined lambda…; …inlined kernel… } + # transition 1: recovery (I ≥ 1) + if (counts[1] >= 1) { … } +``` + +No dispatch, no descriptor objects, no string lookup, no generic enumeration +for weight-1 arcs, no `Record` allocation. This _is_ the flat stepper of §2.1, +generated rather than hand-written. + +This is a natural extension of the existing pipeline, not a new concept: +`emit-buffer-js.ts` already emits per-surface programs from HIR. This emits one +program per _net_, splicing those bodies in. The HIR is designed for it — pure, +no recursion, no unbounded loops, statically unrollable `map`, and the module +docblock already names "JavaScript today, WASM/GPU later" as the intended +backend set. + +Sequencing matters: **do §4 and §5 first.** Codegen over a growable frame with +`Record`-based effects would just generate the same expensive shapes. + +Risks: compile time per experiment start (mitigate by caching on the artifact +fingerprint, which already exists); debuggability of generated code (keep the +current interpreted path as a reference implementation and differential-test +against it — the existing test suite becomes the oracle); and code size for +large nets. + +--- + +## 7. WASM (browser) and native (server) + +### 7.1 The honest case for WASM + +WASM buys predictable numeric performance without a JIT warm-up and without +V8's deoptimisation cliffs. Against **well-written** JS numeric code over typed +arrays, expect **~1.5–3×** — not the 10× that "rewrite it in WASM" implies. All +of §2's cost centres are allocation, copying, and dispatch, and WASM does not +fix those by itself; a flat JS rewrite captures most of the same ground. + +So: WASM is a worthwhile _second backend_ once §6 exists, and a poor +_first_ move. + +### 7.2 Design that works + +The only viable shape is **one module per net containing the entire stepping +loop with user code inlined** — the §6 design, emitted as WASM instead of JS. +Frames live in linear memory as `runs × frameBytes` (needs §5). One host call +advances a whole batch of frames for all runs in the shard, so there are +**zero boundary crossings per frame**. + +Designs to reject: + +- _Rust engine in WASM, user code in JS_ — a boundary crossing per transition + per frame. Slower than today. +- _Rust engine + HIR interpreter in WASM_ — interpretation loses to JIT-compiled + JS on hot code. +- _Runtime Cranelift in the browser_ — not available. + +### 7.3 The `libm` gotcha + +WASM has only `sqrt`, `abs`, `min`, `max`, `ceil`, `floor`, `trunc`, `nearest` +for `f64`. It has **no `exp`, `log`, `pow`, `sin`, `cos`, `tan`** — and +`HIR_MATH_FNS` exposes all of them, with `Math.exp` on the firing path of +_every_ transition evaluation. + +Importing them from JS means a host call per invocation, which would erase the +win. So a WASM backend must ship its own `libm` subset in the module. That is a +known, bounded piece of work, but it is real, and it must produce +**bit-identical** results to V8's `Math.*` or seeded runs will diverge between +backends. Getting bit-identical `exp` across V8 and a hand-rolled +implementation is not guaranteed — this may force accepting per-backend +divergence, which is a product decision about what "same seed" promises. + +Two build routes: + +| Route | Notes | +| --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| Emit WASM bytes directly from HIR | No runtime toolchain; ~1.5–2.5k lines for the emitter; must hand-roll libm | +| Rust engine template + generated Rust for user code, compiled ahead of time | Only works if nets are known at build time — they are not (users author code at runtime), so this cannot be the general path | + +Since nets and user code are authored at runtime, **direct WASM emission from +HIR is the only route that covers the product**. Precedent exists in-repo: +`libs/@blockprotocol/type-system/rust` already ships `cdylib` + `wasm-bindgen`, +and `wasm-bindgen` is pinned in the workspace — but note that precedent is for +_ahead-of-time_ compiled Rust, which is the case that does not apply here. + +### 7.4 Server: native + +The elegant option: **one HIR→WASM backend, two hosts.** The browser runs it on +the built-in engine; `petrinaut-cli` runs the same module under Wasmtime, which +compiles it natively with good codegen. One emitter, one set of semantics, one +place for bit-exactness to be verified — and CPU-native speed on the server. + +Alternative if the server should not depend on WASM: a Rust engine plus +**Cranelift** JIT of the HIR (Cranelift is a normal crate; runtime codegen is +fine off-browser). Faster ceiling, but a second codegen backend to keep +semantically in sync with the browser's — I would avoid that until there is +evidence it is needed. + +Either way the server also wants §3's sharding across `worker_threads` (or OS +processes, given the recorded 768 MB-per-CLI cap). + +--- + +## 8. GPU / WebGPU + +### 8.1 Is Petri-net simulation possible on a GPU? + +Yes — but the parallelism axis has to be **across runs, not within a run**. One +GPU thread per seed is exactly the embarrassingly-parallel shape GPUs want, and +it is what an experiment already is. Within a single run there is very little to +parallelise: transitions must be evaluated in a defined order because each +firing mutates the marking the next transition observes (`applyTokenRemovals` +runs immediately, inside the loop). + +So the model is: `runs` threads, each stepping its own net for many frames, +with metrics reduced on-GPU via atomics into histogram buffers, reading back +only every N frames. + +### 8.2 The four hard constraints + +**1. No `f64` in WGSL.** WebGPU core has `f32`; `f16` behind the `shader-f16` +feature; `f64` is an open proposal, not a shipped extension +([gpuweb#2805](https://github.com/gpuweb/gpuweb/issues/2805)). The entire token +layout is `f64`, dynamics integrate in `f64`, and `real`/`integer` both map to +`f64` lanes. A GPU path is therefore **`f32`** — different rounding, ~7 +significant digits, and integer exactness only to 2²⁴. Consequences: Euler +integration on stiff dynamics degrades noticeably, and **GPU results will not +match CPU results for the same seed**. For a product whose reproducibility +story is "same seed, same trajectory", that is a fork in semantics, not an +optimisation. + +Also no `u64`: the string-pool ids and 128-bit UUIDs need `u32` pairs, with +manual 64-bit arithmetic if compared or hashed. + +**2. Variable token counts.** GPU buffers are fixed at dispatch. §5 capacities +are therefore **mandatory**, not optional, for any GPU path — and the total +allocation is `runs × frameBytes`, so generous capacities multiply fast. + +**3. Unbounded enumeration.** `∏ᵢ C(nᵢ, wᵢ)` combinations per transition with +data-dependent trip counts is hostile to SIMT: divergence plus per-thread work +imbalance. A realistic GPU subset would restrict to weight-1 coloured arcs (or +first-match semantics), which is a real capability reduction. + +**4. Divergence.** Each run fires different transitions, so a subgroup executes +the union of taken branches. Bounded by transition count — the CPU engine walks +all transitions every frame anyway — but kernel bodies with heavy user code +will diverge badly. + +### 8.3 Why "ODEs on the GPU only" is the wrong split + +This is the specific idea worth pushing back on. A frame is +`dynamics → transitions → timers`, and the discrete part reads exactly what the +continuous part wrote. Running dynamics on the GPU and transitions on the CPU +means, **every frame**, uploading the token region, dispatching, and reading +back — with `mapAsync` readback latency in the hundreds of microseconds to +milliseconds, against a whole-frame CPU cost of ~1 µs per run today. The +round-trip would be 100–1000× the work it replaces. + +Same for "timing of the next firing": that is one `exp` and a compare per +transition. There is nothing there to offload. + +The rule: **GPU pays off only if the entire stepping loop lives on the GPU for +many frames without readback.** Partial offload of any per-frame stage is +strictly worse than not doing it. + +### 8.4 Verdict + +Sequence this **after** §4–§7, and scope it as a spike, not a roadmap item. +Realistic upside for a suitable net — bounded capacities, weight-1 arcs, +`f32`-tolerant, thousands of runs — is large (plausibly 10–100× over a +multi-threaded CPU implementation, dominated by how much divergence the net +causes). But it is a second engine with different numerics, a restricted +feature subset, and a hard dependency on §5, and it only helps at run counts +where a well-optimised threaded CPU engine may already be fast enough. + +Reasonable spike, ~1 week, answering one question: for the SIR net with 10 000 +runs, `f32`, capacities set, what is the end-to-end wall clock versus the +threaded CPU path? If it is not ≥10×, drop it. + +A cheaper GPU idea worth noting: keep simulation on the CPU and use WebGPU only +for **rendering** large token populations and distribution charts, which has no +numerical-fidelity problem at all. + +--- + +## 9. Not covered above: the algorithmic option + +Worth flagging because it may beat every item here. The engine uses fixed-`dt` +time-stepping: cost is `frames × transitions` regardless of activity — +`maxTime` 180 / `dt` 0.1 = 1800 frames per run, whether or not anything +happened. For nets where firings are sparse relative to `dt`, an **event-driven +scheme** (Gillespie SSA / next-reaction method, with ODE integration between +events) does work proportional to _events_, not frames. That can be 10–100× +fewer steps, and it composes with everything above. + +It is a semantics change — different trajectories, different meaning for +`dt`, and hybrid continuous/discrete handling — so it would have to be an +opt-in mode. But "reduce the number of steps" is a bigger lever than "make each +step faster", and it deserves evaluation alongside the engineering work. + +--- + +## 10. Questions for you + +Blocking design decisions: + +1. **Capacity semantics** — when a firing would exceed a place's capacity: is + the transition not enabled (classical, cheap, composes with inhibitor arcs), + or does it error and fail the run? Do capacities affect _existing_ nets at + all, or only where explicitly set? +2. **Reproducibility contract** — must the same seed give the same trajectory + across (a) shard counts, (b) engine versions, (c) CPU vs WASM vs GPU + backends? (a) is preserved by the §3 design. (b) is broken by hot-path item 7. + (c) is essentially impossible for GPU (`f32`) and hard for WASM (`libm`). + Knowing which of these you are willing to give up decides §7 and §8. +3. **Worker budget** — one shared pool across concurrent experiments, or per + experiment? What is the acceptable core count while the user keeps editing? + +Sequencing: + +4. Should I start with §4 items 1–6 (measurable, behaviour-preserving, no new + concepts), or do you want the §3 sharding landed first because it is the + visible feature? +5. Is the §9 event-driven mode in scope at all, or is fixed-`dt` a fixed + product decision? diff --git a/libs/@hashintel/petrinaut-core/src/index.ts b/libs/@hashintel/petrinaut-core/src/index.ts index 5ec4897768b..3fcc2841218 100644 --- a/libs/@hashintel/petrinaut-core/src/index.ts +++ b/libs/@hashintel/petrinaut-core/src/index.ts @@ -201,16 +201,20 @@ export { createMonteCarloExperiment, createMonteCarloMetricHistogramAccumulator, createMonteCarloMetricNumericAccumulator, + createMonteCarloMetricShardMerger, createMonteCarloSimulator, createMonteCarloUserDefinedMetricConfigsFromSpecs, createMonteCarloUserDefinedMetric, createSimulation, createWorkerTransport, + getDefaultMonteCarloShardCount, + planMonteCarloShards, } from "./simulation"; export type { BackpressureConfig, CreateMonteCarloExperimentConfig, CreateSimulationConfig, + MonteCarloShardPlanEntry, Simulation, SimulationCompleteEvent, SimulationConfig, diff --git a/libs/@hashintel/petrinaut-core/src/simulation/index.ts b/libs/@hashintel/petrinaut-core/src/simulation/index.ts index 2c936b9e04f..a24e682ccdb 100644 --- a/libs/@hashintel/petrinaut-core/src/simulation/index.ts +++ b/libs/@hashintel/petrinaut-core/src/simulation/index.ts @@ -23,13 +23,17 @@ export { createMonteCarloExperiment, createMonteCarloMetricHistogramAccumulator, createMonteCarloMetricNumericAccumulator, + createMonteCarloMetricShardMerger, createMonteCarloSimulator, createMonteCarloUserDefinedMetricConfigsFromSpecs, createMonteCarloUserDefinedMetric, + getDefaultMonteCarloShardCount, + planMonteCarloShards, } from "./monte-carlo"; export type { CreateMonteCarloExperimentConfig, MonteCarloAdvanceResult, + MonteCarloShardPlanEntry, MonteCarloActiveRunPlaceCountsVisitor, MonteCarloExperiment, MonteCarloExperimentEvent, diff --git a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/index.ts b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/index.ts index 3e27827423f..623bf7a4682 100644 --- a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/index.ts +++ b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/index.ts @@ -3,10 +3,16 @@ export { addAllMonteCarloMetricValues, createMonteCarloMetricHistogramAccumulator, createMonteCarloMetricNumericAccumulator, + createMonteCarloMetricShardMerger, createMonteCarloUserDefinedMetricConfigsFromSpecs, createMonteCarloUserDefinedMetric, } from "./metrics"; export { createMonteCarloExperiment } from "./runtime/experiment"; +export { + getDefaultMonteCarloShardCount, + planMonteCarloShards, +} from "./runtime/shard-plan"; +export type { MonteCarloShardPlanEntry } from "./runtime/shard-plan"; export type { MonteCarloAdvanceResult, MonteCarloRunConfig, diff --git a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/metrics/index.ts b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/metrics/index.ts index adbe17cd8ba..50538bcf7dd 100644 --- a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/metrics/index.ts +++ b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/metrics/index.ts @@ -3,6 +3,7 @@ export { createMonteCarloMetricHistogramAccumulator, createMonteCarloMetricNumericAccumulator, } from "./accumulators"; +export { createMonteCarloMetricShardMerger } from "./merge"; export { createMonteCarloUserDefinedMetricConfigsFromSpecs } from "./specs"; export { createMonteCarloUserDefinedMetric } from "./user-defined"; export type { diff --git a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/metrics/merge.test.ts b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/metrics/merge.test.ts new file mode 100644 index 00000000000..b0b98aa31a3 --- /dev/null +++ b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/metrics/merge.test.ts @@ -0,0 +1,258 @@ +import { describe, expect, it } from "vitest"; + +import { createMonteCarloMetricShardMerger } from "./merge"; + +import type { + MonteCarloUserDefinedMetricAggregation, + MonteCarloUserDefinedMetricFrame, + MonteCarloUserDefinedMetricTimeAggregation, +} from "./types"; + +/** Builds a scalar shard frame as a worker would emit it for `values`. */ +function scalarFrame(options: { + frameNumber: number; + values: readonly number[]; + aggregateRuns?: MonteCarloUserDefinedMetricAggregation; + aggregateTime?: MonteCarloUserDefinedMetricTimeAggregation; + metricId?: string; +}): MonteCarloUserDefinedMetricFrame { + const { + frameNumber, + values, + aggregateRuns = "mean", + aggregateTime = "none", + metricId = "m", + } = options; + + const count = values.length; + const sum = values.reduce((total, value) => total + value, 0); + const frameValue = + count === 0 + ? null + : aggregateRuns === "mean" + ? sum / count + : aggregateRuns === "sum" + ? sum + : aggregateRuns === "min" + ? Math.min(...values) + : aggregateRuns === "max" + ? Math.max(...values) + : values.at(-1)!; + + return { + metricId, + label: metricId, + outputType: "scalar", + frameNumber, + time: frameNumber * 0.1, + value: frameValue, + frameValue, + timeValue: null, + runSampleCount: count, + timeSampleCount: 0, + runAggregate: { + count, + sum, + min: count === 0 ? null : Math.min(...values), + max: count === 0 ? null : Math.max(...values), + last: count === 0 ? null : values.at(-1)!, + }, + aggregateRuns, + aggregateTime, + }; +} + +function distributionFrame( + frameNumber: number, + bins: readonly [number, number][], +): MonteCarloUserDefinedMetricFrame { + return { + metricId: "d", + label: "d", + outputType: "distribution", + frameNumber, + time: frameNumber * 0.1, + value: null, + frameValue: null, + timeValue: null, + bins, + runSampleCount: bins.reduce((total, [, frequency]) => total + frequency, 0), + timeSampleCount: bins.reduce( + (total, [, frequency]) => total + frequency, + 0, + ), + }; +} + +describe("createMonteCarloMetricShardMerger", () => { + it("withholds a frame until every running shard has reported it", () => { + const merger = createMonteCarloMetricShardMerger(2); + + expect( + merger.accept(0, [scalarFrame({ frameNumber: 0, values: [1] })]), + ).toHaveLength(0); + + const released = merger.accept(1, [ + scalarFrame({ frameNumber: 0, values: [3] }), + ]); + + expect(released).toHaveLength(1); + expect(released[0]!.frameValue).toBe(2); + }); + + it("means across shards by weight, not by averaging means", () => { + const merger = createMonteCarloMetricShardMerger(2); + + // Shard 0 has three runs averaging 10; shard 1 has one run at 100. + // A mean of means would give 55; the correct pooled mean is 32.5. + merger.accept(0, [scalarFrame({ frameNumber: 0, values: [5, 10, 15] })]); + const released = merger.accept(1, [ + scalarFrame({ frameNumber: 0, values: [100] }), + ]); + + expect(released[0]!.frameValue).toBe(32.5); + expect(released[0]!.runSampleCount).toBe(4); + }); + + it.each([ + ["sum", 130], + ["min", 5], + ["max", 100], + ] as const)("merges the %s aggregation across shards", (method, expected) => { + const merger = createMonteCarloMetricShardMerger(2); + + merger.accept(0, [ + scalarFrame({ + frameNumber: 0, + values: [5, 10, 15], + aggregateRuns: method, + }), + ]); + const released = merger.accept(1, [ + scalarFrame({ frameNumber: 0, values: [100], aggregateRuns: method }), + ]); + + expect(released[0]!.frameValue).toBe(expected); + }); + + it("resolves `last` by shard order even when shards reply out of order", () => { + const merger = createMonteCarloMetricShardMerger(2); + + // Shard 1 owns the higher global run indices, so its last value wins — + // regardless of which shard's message arrives first. + merger.accept(1, [ + scalarFrame({ frameNumber: 0, values: [100], aggregateRuns: "last" }), + ]); + const released = merger.accept(0, [ + scalarFrame({ frameNumber: 0, values: [5], aggregateRuns: "last" }), + ]); + + expect(released[0]!.frameValue).toBe(100); + }); + + it("sums distribution bins and keeps them sorted", () => { + const merger = createMonteCarloMetricShardMerger(2); + + merger.accept(0, [ + distributionFrame(0, [ + [5, 2], + [1, 1], + ]), + ]); + const released = merger.accept(1, [ + distributionFrame(0, [ + [5, 3], + [9, 1], + ]), + ]); + + expect(released[0]!.outputType).toBe("distribution"); + expect( + (released[0] as { bins: readonly (readonly number[])[] }).bins, + ).toStrictEqual([ + [1, 1], + [5, 5], + [9, 1], + ]); + expect(released[0]!.runSampleCount).toBe(7); + }); + + it("recomputes time aggregation from merged frame values", () => { + const merger = createMonteCarloMetricShardMerger(2); + const frames: MonteCarloUserDefinedMetricFrame[] = []; + + // Merged frame values are 2 then 20, so the running mean is 2 then 11. + for (const [frameNumber, left, right] of [ + [0, 1, 3], + [1, 10, 30], + ] as const) { + merger.accept(0, [ + scalarFrame({ frameNumber, values: [left], aggregateTime: "mean" }), + ]); + frames.push( + ...merger.accept(1, [ + scalarFrame({ frameNumber, values: [right], aggregateTime: "mean" }), + ]), + ); + } + + expect(frames.map((frame) => frame.frameValue)).toStrictEqual([2, 20]); + expect(frames.map((frame) => frame.timeValue)).toStrictEqual([2, 11]); + // `value` follows the time aggregate when one is configured. + expect(frames.map((frame) => frame.value)).toStrictEqual([2, 11]); + expect(frames.map((frame) => frame.timeSampleCount)).toStrictEqual([1, 2]); + }); + + it("stops waiting on a shard once it finishes", () => { + const merger = createMonteCarloMetricShardMerger(2); + + // Shard 1 completes early; shard 0 keeps producing frames and must not + // stall waiting for a shard that will never report again. + merger.accept(1, [scalarFrame({ frameNumber: 0, values: [3] })]); + merger.accept(0, [scalarFrame({ frameNumber: 0, values: [1] })]); + expect(merger.finishShard(1)).toHaveLength(0); + + const released = merger.accept(0, [ + scalarFrame({ frameNumber: 1, values: [7] }), + ]); + + expect(released).toHaveLength(1); + expect(released[0]!.frameNumber).toBe(1); + // Only shard 0 still has active runs, so only its sample counts. + expect(released[0]!.frameValue).toBe(7); + expect(released[0]!.runSampleCount).toBe(1); + }); + + it("releases frames in ascending frame order across metrics", () => { + const merger = createMonteCarloMetricShardMerger(1); + + const released = merger.accept(0, [ + scalarFrame({ frameNumber: 1, values: [1], metricId: "b" }), + scalarFrame({ frameNumber: 0, values: [1], metricId: "a" }), + scalarFrame({ frameNumber: 1, values: [1], metricId: "a" }), + scalarFrame({ frameNumber: 0, values: [1], metricId: "b" }), + ]); + + expect(released.map((frame) => frame.frameNumber)).toStrictEqual([ + 0, 0, 1, 1, + ]); + }); + + it("flushes buffered frames that no shard will complete", () => { + const merger = createMonteCarloMetricShardMerger(2); + + merger.accept(0, [scalarFrame({ frameNumber: 0, values: [1] })]); + + expect(merger.flush().map((frame) => frame.frameNumber)).toStrictEqual([0]); + expect(merger.flush()).toHaveLength(0); + }); + + it("rejects invalid shard counts and indices", () => { + expect(() => createMonteCarloMetricShardMerger(0)).toThrow( + /positive shard count/, + ); + expect(() => createMonteCarloMetricShardMerger(1).accept(1, [])).toThrow( + /Unknown metric shard index/, + ); + }); +}); diff --git a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/metrics/merge.ts b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/metrics/merge.ts new file mode 100644 index 00000000000..981519651b9 --- /dev/null +++ b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/metrics/merge.ts @@ -0,0 +1,287 @@ +/** + * Merging of metric frames produced by the shards of one sharded experiment. + * + * A sharded experiment splits its runs across several workers, each of which + * aggregates only its own runs. Recombining those partial results is what makes + * shard layout invisible in the output, and it is only sound because the + * per-frame aggregates form monoids: + * + * - **distribution** frames carry bins, which merge by summing frequencies; + * - **scalar** frames carry `runAggregate`, which merges through + * `createMonteCarloMetricNumericAccumulator(...).merge` — `frameValue` itself + * cannot be merged, because a mean of means is not a mean. + * + * Time aggregation is handled differently per output type. Distribution metrics + * aggregate each *run* over time, and every run lives in exactly one shard, so + * that work is already correct shard-locally. Scalar metrics aggregate the + * cross-run frame value over time, which depends on the merged value, so it is + * recomputed here as frames finalise. + */ +import { + createMonteCarloMetricNumericAccumulator, + type MonteCarloMetricNumericAccumulatorState, +} from "./accumulators"; + +import type { + MonteCarloUserDefinedMetricDistributionBin, + MonteCarloUserDefinedMetricFrame, +} from "./types"; + +type ShardFrame = { + shardIndex: number; + frame: MonteCarloUserDefinedMetricFrame; +}; + +/** + * Sums bin frequencies across shard frames. + * + * Every shard bins with the same configuration, so equal bin values are the + * same bucket and can be added directly. + */ +function mergeDistributionBins( + shardFrames: readonly ShardFrame[], +): MonteCarloUserDefinedMetricDistributionBin[] { + const totals = new Map(); + + for (const { frame } of shardFrames) { + if (frame.outputType !== "distribution") { + continue; + } + for (const [value, frequency] of frame.bins) { + totals.set(value, (totals.get(value) ?? 0) + frequency); + } + } + + return [...totals.entries()] + .sort(([left], [right]) => left - right) + .map(([value, frequency]) => [value, frequency]); +} + +/** + * Combines one metric's frames for a single frame number across shards. + * + * Shards are folded in index order so the result never depends on which worker + * replied first. That matters for the `last` aggregation, which then resolves to + * the highest-indexed shard that sampled anything — mirroring the + * single-simulator behaviour of taking the highest run index. + */ +function mergeShardFramesForFrameNumber( + unordered: readonly ShardFrame[], +): MonteCarloUserDefinedMetricFrame { + if (unordered.length === 0) { + throw new Error("Cannot merge an empty set of metric shard frames"); + } + + const shardFrames = [...unordered].sort( + (left, right) => left.shardIndex - right.shardIndex, + ); + const first = shardFrames[0]!.frame; + if (shardFrames.length === 1) { + return first; + } + + let runSampleCount = 0; + let timeSampleCount = 0; + for (const { frame } of shardFrames) { + runSampleCount += frame.runSampleCount; + timeSampleCount += frame.timeSampleCount; + } + + if (first.outputType === "distribution") { + return { + ...first, + bins: mergeDistributionBins(shardFrames), + runSampleCount, + timeSampleCount, + }; + } + + const accumulator = createMonteCarloMetricNumericAccumulator( + first.aggregateRuns, + ); + let runAggregate: MonteCarloMetricNumericAccumulatorState = + accumulator.empty(); + for (const { frame } of shardFrames) { + if (frame.outputType !== "scalar") { + continue; + } + runAggregate = accumulator.merge(runAggregate, frame.runAggregate); + } + + const frameValue = accumulator.read(runAggregate); + + return { + ...first, + frameValue, + // Overwritten by the time-aggregation pass when one is configured. + value: frameValue, + timeValue: null, + runAggregate, + runSampleCount, + timeSampleCount, + }; +} + +type TimeAggregationState = { + frameCount: number; + state: MonteCarloMetricNumericAccumulatorState | null; +}; + +/** + * Streaming merger for a sharded experiment's metric frames. + * + * Shards report independently and at different rates, so a frame number can + * only be finalised once every shard that is still running has reported it. + * Shards that finish early are excluded from that watermark rather than + * blocking it — which is also why merged output matches an unsharded run: a + * finished shard has no active runs left to contribute, exactly like the + * completed runs a single simulator skips. + */ +export function createMonteCarloMetricShardMerger(shardCount: number): { + /** Buffers a shard's frames and returns any frames that are now final. */ + accept: ( + shardIndex: number, + frames: readonly MonteCarloUserDefinedMetricFrame[], + ) => MonteCarloUserDefinedMetricFrame[]; + /** Drops a shard from the watermark and releases whatever that unblocks. */ + finishShard: (shardIndex: number) => MonteCarloUserDefinedMetricFrame[]; + /** Releases every buffered frame regardless of the watermark. */ + flush: () => MonteCarloUserDefinedMetricFrame[]; +} { + if (!Number.isInteger(shardCount) || shardCount <= 0) { + throw new Error("Metric shard merger requires a positive shard count"); + } + + /** + * metricId -> frameNumber -> one entry per contributing shard. + * + * Entries keep their shard index because shards report asynchronously but + * merging must be deterministic: the `last` aggregation resolves by shard + * order, not arrival order. + */ + const buffered = new Map>(); + const highestFrameByShard = new Array(shardCount).fill(-1); + const finished = new Array(shardCount).fill(false); + const timeAggregation = new Map(); + + const getWatermark = (): number => { + let watermark = Number.POSITIVE_INFINITY; + for (let shard = 0; shard < shardCount; shard++) { + if (finished[shard]) { + continue; + } + watermark = Math.min(watermark, highestFrameByShard[shard]!); + } + return watermark; + }; + + /** + * Applies the configured time aggregation to a finalised scalar frame. + * + * Called strictly in ascending frame order per metric, so the accumulator + * sees the merged frame values in simulation order. + */ + const applyTimeAggregation = ( + frame: MonteCarloUserDefinedMetricFrame, + ): MonteCarloUserDefinedMetricFrame => { + if (frame.outputType !== "scalar" || frame.aggregateTime === "none") { + return frame; + } + + const accumulator = createMonteCarloMetricNumericAccumulator( + frame.aggregateTime, + ); + let entry = timeAggregation.get(frame.metricId); + if (!entry) { + entry = { frameCount: 0, state: null }; + timeAggregation.set(frame.metricId, entry); + } + + if (frame.frameValue !== null) { + entry.state = accumulator.add( + entry.state ?? accumulator.empty(), + frame.frameValue, + ); + entry.frameCount += 1; + } + + const timeValue = entry.state ? accumulator.read(entry.state) : null; + + return { + ...frame, + timeValue, + value: timeValue ?? frame.frameValue, + timeSampleCount: entry.frameCount, + }; + }; + + const drainUpTo = (watermark: number): MonteCarloUserDefinedMetricFrame[] => { + const released: MonteCarloUserDefinedMetricFrame[] = []; + + for (const [metricId, byFrameNumber] of buffered) { + const ready = [...byFrameNumber.keys()] + .filter((frameNumber) => frameNumber <= watermark) + .sort((left, right) => left - right); + + for (const frameNumber of ready) { + released.push( + applyTimeAggregation( + mergeShardFramesForFrameNumber(byFrameNumber.get(frameNumber)!), + ), + ); + byFrameNumber.delete(frameNumber); + } + + if (byFrameNumber.size === 0) { + buffered.delete(metricId); + } + } + + // Frames are released per metric above; order them globally so consumers + // that append to a flat timeline see monotonic frame numbers. + return released.sort((left, right) => left.frameNumber - right.frameNumber); + }; + + return { + accept(shardIndex, frames) { + if (shardIndex < 0 || shardIndex >= shardCount) { + throw new Error(`Unknown metric shard index ${shardIndex}`); + } + + for (const frame of frames) { + let byFrameNumber = buffered.get(frame.metricId); + if (!byFrameNumber) { + byFrameNumber = new Map(); + buffered.set(frame.metricId, byFrameNumber); + } + + const existing = byFrameNumber.get(frame.frameNumber); + if (existing) { + existing.push({ shardIndex, frame }); + } else { + byFrameNumber.set(frame.frameNumber, [{ shardIndex, frame }]); + } + + highestFrameByShard[shardIndex] = Math.max( + highestFrameByShard[shardIndex]!, + frame.frameNumber, + ); + } + + return drainUpTo(getWatermark()); + }, + + finishShard(shardIndex) { + if (shardIndex < 0 || shardIndex >= shardCount) { + throw new Error(`Unknown metric shard index ${shardIndex}`); + } + + finished[shardIndex] = true; + return drainUpTo(getWatermark()); + }, + + flush() { + return drainUpTo(Number.POSITIVE_INFINITY); + }, + }; +} diff --git a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/metrics/types.ts b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/metrics/types.ts index b89bf67c520..2ada0ef1cd9 100644 --- a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/metrics/types.ts +++ b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/metrics/types.ts @@ -1,5 +1,6 @@ import type { HirMetricArtifact } from "../../../hir-runtime"; import type { SimulationFrameReader } from "../../api"; +import type { MonteCarloMetricNumericAccumulatorState } from "./accumulators"; export type MonteCarloMetricRunStatus = | "ready" @@ -192,6 +193,18 @@ export type MonteCarloUserDefinedScalarMetricFrame = frameValue: number | null; timeValue: number | null; timeSampleCount: number; + /** + * Mergeable across-runs accumulator state behind `frameValue`. + * + * `frameValue` is already reduced and cannot be combined across a sharded + * experiment (a mean of means is not a mean). This state can, via the + * numeric accumulator monoid — see `metrics/merge.ts`. + */ + runAggregate: MonteCarloMetricNumericAccumulatorState; + /** How `runAggregate` reduces to `frameValue`; needed to merge shards. */ + aggregateRuns: MonteCarloUserDefinedMetricAggregation; + /** How frame values reduce over time; re-applied after shards merge. */ + aggregateTime: MonteCarloUserDefinedMetricTimeAggregation; }; export type MonteCarloUserDefinedDistributionMetricFrame = diff --git a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/metrics/user-defined.ts b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/metrics/user-defined.ts index d8444798ee1..8614f0afec0 100644 --- a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/metrics/user-defined.ts +++ b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/metrics/user-defined.ts @@ -163,9 +163,13 @@ export function createMonteCarloUserDefinedMetric( runOutput.aggregateRuns ?? config.aggregateRuns ?? "mean"; const runAccumulator = createMonteCarloMetricNumericAccumulator(aggregateRuns); - const frameValue = runAccumulator.read( - addAllMonteCarloMetricValues(runAccumulator, runValues), + // Kept on the frame so a sharded experiment can merge across shards + // before reducing — see `metrics/merge.ts`. + const runAggregate = addAllMonteCarloMetricValues( + runAccumulator, + runValues, ); + const frameValue = runAccumulator.read(runAggregate); if (frameValue !== null) { scalarFrameCountState = frameCountAccumulator.add( scalarFrameCountState, @@ -195,6 +199,9 @@ export function createMonteCarloUserDefinedMetric( timeValue, runSampleCount: runValues.length, timeSampleCount: scalarFrameCountState.count, + runAggregate, + aggregateRuns, + aggregateTime, }); }, }; diff --git a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/run-state.ts b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/run-state.ts index 97624284ae8..141cc64f08e 100644 --- a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/run-state.ts +++ b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/run-state.ts @@ -81,7 +81,11 @@ export function createRunState( runConfig: MonteCarloRunConfig | undefined, index: number, ): MonteCarloRunState { - const seed = runConfig?.seed ?? deriveRunSeed(config.seed ?? 1, index); + // Seeds derive from the run's *global* index so that sharding an experiment + // across workers reassigns runs without changing which seeds run. + const seed = + runConfig?.seed ?? + deriveRunSeed(config.seed ?? 1, (config.runIndexOffset ?? 0) + index); const initialMarking = runConfig?.initialMarking ?? config.initialMarking; const inputParameterValues = { ...config.parameterValues, diff --git a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/runtime/experiment.test.ts b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/runtime/experiment.test.ts index 525c81e63ba..6a774714f91 100644 --- a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/runtime/experiment.test.ts +++ b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/runtime/experiment.test.ts @@ -3,6 +3,7 @@ import { describe, expect, it, vi } from "vitest"; import { compileHirArtifacts } from "../../../hir/compile"; import { createMonteCarloExperiment } from "./experiment"; +import type { WorkerLike } from "../../../environment"; import type { HirMetricArtifact } from "../../../hir/instantiate"; import type { SDCPN } from "../../../types/sdcpn"; import type { SimulationTransport } from "../../api"; @@ -70,6 +71,15 @@ function makeMetricFrame( timeValue: null, runSampleCount: 1, timeSampleCount: frameNumber + 1, + runAggregate: { + count: 1, + sum: frameNumber, + min: frameNumber, + max: frameNumber, + last: frameNumber, + }, + aggregateRuns: "mean", + aggregateTime: "none", }; } @@ -119,6 +129,262 @@ function createExperimentWithMockTransport(mock: { }); } +/** + * Drives a sharded experiment through fake workers, one mock transport per + * shard, so shard fan-out and metric merging can be exercised without threads. + */ +function createShardedExperiment(options: { + runCount: number; + shardCount: number; +}) { + const mocks: ReturnType[] = []; + + const promise = createMonteCarloExperiment({ + createWorker: () => { + const mock = makeMockTransport(); + mocks.push(mock); + // The real factory returns a Worker; the transport only needs the + // message plumbing, which the mock transport provides directly. + const worker: WorkerLike = { + postMessage: (message) => { + mock.transport.send(message); + }, + addEventListener: (_type, listener) => { + mock.transport.onMessage((message) => { + listener({ data: message }); + }); + }, + terminate: () => { + mock.transport.terminate(); + }, + }; + return worker; + }, + sdcpn: empty(), + initialMarking: {}, + parameterValues: {}, + seed: 1, + dt: 1, + maxTime: 10, + runCount: options.runCount, + shardCount: options.shardCount, + }); + + return { promise, mocks }; +} + +/** Awaits the worker-transport factory's internal promise resolution. */ +const flushMicrotasks = async () => { + await Promise.resolve(); + await Promise.resolve(); +}; + +describe("createMonteCarloExperiment sharding", () => { + it("splits runs across shards with global run index offsets", async () => { + const { mocks } = createShardedExperiment({ runCount: 10, shardCount: 4 }); + await flushMicrotasks(); + + expect(mocks).toHaveLength(4); + expect( + mocks.map((mock) => { + const init = mock.sent.find((message) => message.type === "init"); + return init?.type === "init" + ? { runIndexOffset: init.runIndexOffset, runCount: init.runCount } + : null; + }), + ).toStrictEqual([ + { runIndexOffset: 0, runCount: 3 }, + { runIndexOffset: 3, runCount: 3 }, + { runIndexOffset: 6, runCount: 2 }, + { runIndexOffset: 8, runCount: 2 }, + ]); + }); + + it("never creates more shards than runs", async () => { + const { mocks } = createShardedExperiment({ runCount: 2, shardCount: 8 }); + await flushMicrotasks(); + + expect(mocks).toHaveLength(2); + }); + + it("resolves only once every shard is ready", async () => { + const { promise, mocks } = createShardedExperiment({ + runCount: 4, + shardCount: 2, + }); + await flushMicrotasks(); + + let resolved = false; + void promise.then(() => { + resolved = true; + }); + + mocks[0]!.simulate({ type: "ready" }); + await flushMicrotasks(); + expect(resolved).toBe(false); + + mocks[1]!.simulate({ type: "ready" }); + const experiment = await promise; + + expect(experiment.status.get()).toBe("Ready"); + experiment.dispose(); + }); + + it("merges metric frames across shards before publishing them", async () => { + const { promise, mocks } = createShardedExperiment({ + runCount: 4, + shardCount: 2, + }); + await flushMicrotasks(); + for (const mock of mocks) { + mock.simulate({ type: "ready" }); + } + const experiment = await promise; + + // One shard alone cannot finalise a frame — the other might still report it. + mocks[0]!.simulate({ type: "metricFrames", frames: [makeMetricFrame(0)] }); + expect(experiment.metrics.get().frames).toHaveLength(0); + + mocks[1]!.simulate({ type: "metricFrames", frames: [makeMetricFrame(0)] }); + + const frames = experiment.metrics.get().frames; + expect(frames).toHaveLength(1); + // Both shards contributed one run each at the same value. + expect(frames[0]!.runSampleCount).toBe(2); + + experiment.dispose(); + }); + + it("completes only after every shard completes", async () => { + const { promise, mocks } = createShardedExperiment({ + runCount: 4, + shardCount: 2, + }); + await flushMicrotasks(); + for (const mock of mocks) { + mock.simulate({ type: "ready" }); + } + const experiment = await promise; + const events = vi.fn(); + experiment.events.subscribe(events); + + mocks[0]!.simulate({ + type: "complete", + progress: makeProgress({ allFinished: true, completedRuns: 2 }), + }); + expect(experiment.status.get()).not.toBe("Complete"); + expect(events).not.toHaveBeenCalled(); + + mocks[1]!.simulate({ + type: "complete", + progress: makeProgress({ allFinished: true, completedRuns: 2 }), + }); + + expect(experiment.status.get()).toBe("Complete"); + expect(events).toHaveBeenCalledTimes(1); + // Run tallies sum across shards. + expect(experiment.progress.get()?.completedRuns).toBe(4); + expect(experiment.progress.get()?.allFinished).toBe(true); + + experiment.dispose(); + }); + + it("reports the slowest shard's position while any shard still runs", async () => { + const { promise, mocks } = createShardedExperiment({ + runCount: 4, + shardCount: 2, + }); + await flushMicrotasks(); + for (const mock of mocks) { + mock.simulate({ type: "ready" }); + } + const experiment = await promise; + + mocks[0]!.simulate({ + type: "progress", + progress: makeProgress({ frameNumber: 9, time: 9 }), + }); + mocks[1]!.simulate({ + type: "progress", + progress: makeProgress({ frameNumber: 3, time: 3 }), + }); + + // Merged metrics only extend as far as the slowest shard, so progress must + // not run ahead of the data behind it. + expect(experiment.progress.get()?.frameNumber).toBe(3); + + experiment.dispose(); + }); + + it("cancels every shard when one reports cancellation", async () => { + const { promise, mocks } = createShardedExperiment({ + runCount: 4, + shardCount: 3, + }); + await flushMicrotasks(); + for (const mock of mocks) { + mock.simulate({ type: "ready" }); + } + const experiment = await promise; + + mocks[0]!.simulate({ type: "cancelled", progress: makeProgress() }); + + expect(experiment.status.get()).toBe("Cancelled"); + for (const mock of mocks.slice(1)) { + expect(mock.sent.some((message) => message.type === "cancel")).toBe(true); + } + // Cancelled runs were abandoned, not finished. + expect(experiment.progress.get()?.allFinished).toBe(false); + + experiment.dispose(); + }); + + it("tears down surviving shards when one errors after start", async () => { + const { promise, mocks } = createShardedExperiment({ + runCount: 4, + shardCount: 2, + }); + await flushMicrotasks(); + for (const mock of mocks) { + mock.simulate({ type: "ready" }); + } + const experiment = await promise; + const events = vi.fn(); + experiment.events.subscribe(events); + + mocks[1]!.simulate({ type: "error", message: "boom", itemId: null }); + + expect(experiment.status.get()).toBe("Error"); + expect(events).toHaveBeenCalledWith({ + type: "error", + message: "boom", + itemId: null, + }); + expect(mocks[0]!.isTerminated()).toBe(true); + }); + + it("forwards start to every shard", async () => { + const { promise, mocks } = createShardedExperiment({ + runCount: 4, + shardCount: 2, + }); + await flushMicrotasks(); + for (const mock of mocks) { + mock.simulate({ type: "ready" }); + } + const experiment = await promise; + + experiment.start(); + + for (const mock of mocks) { + expect(mock.sent.some((message) => message.type === "start")).toBe(true); + } + expect(experiment.status.get()).toBe("Running"); + + experiment.dispose(); + }); +}); + describe("createMonteCarloExperiment", () => { it("sends init and resolves when the worker reports ready", async () => { const mock = makeMockTransport(); diff --git a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/runtime/experiment.ts b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/runtime/experiment.ts index 2eb0ff08fd4..7e5cc65f299 100644 --- a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/runtime/experiment.ts +++ b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/runtime/experiment.ts @@ -2,10 +2,15 @@ import { DEFAULT_PETRINAUT_EXTENSIONS } from "../../../extensions"; import { resolveNetParameterValues } from "../../../parameter-values"; import { createWorkerTransport } from "../../runtime/transport"; import { + createMonteCarloMetricShardMerger, createMonteCarloUserDefinedMetricConfigsFromSpecs, createMonteCarloUserDefinedMetric, } from "../metrics"; import { createMonteCarloSimulator } from "../monte-carlo-simulator"; +import { + getDefaultMonteCarloShardCount, + planMonteCarloShards, +} from "./shard-plan"; import type { AbortSignalLike } from "../../../environment"; import type { PetrinautExtensionSettings } from "../../../extensions"; @@ -29,6 +34,7 @@ import type { MonteCarloToMainMessage, MonteCarloWorkerProgress, } from "../worker/messages"; +import type { MonteCarloShardPlanEntry } from "./shard-plan"; export type MonteCarloExperimentState = | "Initializing" @@ -61,6 +67,16 @@ type CreateMonteCarloExperimentBaseConfig = { hirArtifacts?: HirArtifacts; runCount: number; batchSize?: number; + /** + * How many workers to split the runs across. + * + * Defaults to one per logical core minus one (capped at `runCount`). Runs are + * independent and seeds derive from the global run index, so shard count + * changes only how fast an experiment finishes, never what it reports. Only + * honoured for `createWorker` experiments — a caller-supplied `transport` is a + * single channel and always runs as one shard. + */ + shardCount?: number; signal?: AbortSignalLike; }; @@ -485,11 +501,28 @@ export function createMonteCarloExperiment( }); } - let transport: SimulationTransport; + // A caller-supplied transport is a single channel, so it cannot be sharded. + // `createWorker` can be called once per shard. + let shards: MonteCarloShardPlanEntry[]; + let transports: SimulationTransport[]; if ("transport" in config && config.transport !== undefined) { - transport = config.transport; + shards = [{ runIndexOffset: 0, runCount: config.runCount }]; + transports = [config.transport]; } else if ("createWorker" in config && config.createWorker !== undefined) { - transport = createWorkerTransport(config.createWorker); + const { createWorker } = config; + try { + shards = planMonteCarloShards( + config.runCount, + config.shardCount ?? getDefaultMonteCarloShardCount(config.runCount), + ); + } catch (error) { + return Promise.reject( + error instanceof Error + ? error + : new Error("Failed to plan Monte Carlo experiment shards"), + ); + } + transports = shards.map(() => createWorkerTransport(createWorker)); } else { return Promise.reject( new Error( @@ -497,6 +530,8 @@ export function createMonteCarloExperiment( ), ); } + + const shardCount = shards.length; const status = createReadableStore("Initializing"); const progress = createReadableStore(null); const metrics = createReadableStore( @@ -507,10 +542,98 @@ export function createMonteCarloExperiment( return new Promise((resolve, reject) => { let settled = false; - let off: (() => void) | null = null; + let offListeners: (() => void)[] = []; let abortListener: (() => void) | null = null; - const cleanupTransport = ({ sendCancel }: { sendCancel: boolean }) => { + const merger = createMonteCarloMetricShardMerger(shardCount); + const shardProgress = new Array( + shardCount, + ).fill(null); + const shardReady = new Array(shardCount).fill(false); + const shardSettled = new Array(shardCount).fill(false); + /** Shards that will produce no further metric frames, for any reason. */ + const shardFinished = new Array(shardCount).fill(false); + /** + * Shards whose runs all reached a terminal state. + * + * Distinct from `shardFinished`: a cancelled shard stops reporting but its + * runs were abandoned, not finished, so it must not make the experiment + * claim `allFinished`. + */ + const shardCompleted = new Array(shardCount).fill(false); + + const publishMetricFrames = ( + frames: readonly MonteCarloUserDefinedMetricFrame[], + ) => { + if (frames.length > 0) { + metrics.set(appendMetricFrames(metrics.get(), frames)); + } + }; + + /** + * Combines shard progress into one experiment-level view. + * + * Run tallies sum. Frame position reports the slowest shard still running, + * because that is how far the *merged* metric timeline actually extends — + * reporting the fastest shard would run the progress bar ahead of the data + * behind it. + */ + const publishProgress = () => { + let advancedRuns = 0; + let completedRuns = 0; + let erroredRuns = 0; + let activeRuns = 0; + let slowestFrameNumber = Number.POSITIVE_INFINITY; + let slowestTime = Number.POSITIVE_INFINITY; + let furthestFrameNumber = 0; + let furthestTime = 0; + let reported = false; + + for (let shard = 0; shard < shardCount; shard++) { + const current = shardProgress[shard]; + if (!current) { + continue; + } + + reported = true; + advancedRuns += current.advancedRuns; + completedRuns += current.completedRuns; + erroredRuns += current.erroredRuns; + activeRuns += current.activeRuns; + furthestFrameNumber = Math.max( + furthestFrameNumber, + current.frameNumber, + ); + furthestTime = Math.max(furthestTime, current.time); + + if (!shardFinished[shard]) { + slowestFrameNumber = Math.min( + slowestFrameNumber, + current.frameNumber, + ); + slowestTime = Math.min(slowestTime, current.time); + } + } + + if (!reported) { + return; + } + + const stillReporting = !shardFinished.every(Boolean); + + progress.set({ + advancedRuns, + completedRuns, + erroredRuns, + activeRuns, + allFinished: shardCompleted.every(Boolean), + runCount: config.runCount, + frameNumber: stillReporting ? slowestFrameNumber : furthestFrameNumber, + time: stillReporting ? slowestTime : furthestTime, + }); + }; + + const cleanupTransports = ({ sendCancel }: { sendCancel: boolean }) => { if (disposed) { return; } @@ -520,18 +643,21 @@ export function createMonteCarloExperiment( config.signal?.removeEventListener("abort", abortListener); abortListener = null; } - off?.(); - off = null; - - if (sendCancel) { - try { - transport.send({ type: "cancel" }); - } catch { - // Transport may already be torn down. + for (const off of offListeners) { + off(); + } + offListeners = []; + + for (const transport of transports) { + if (sendCancel) { + try { + transport.send({ type: "cancel" }); + } catch { + // Transport may already be torn down. + } } + transport.terminate(); } - - transport.terminate(); }; const rejectBeforeReady = (error: Error) => { @@ -540,7 +666,7 @@ export function createMonteCarloExperiment( } settled = true; - cleanupTransport({ sendCancel: false }); + cleanupTransports({ sendCancel: false }); reject(error); }; @@ -551,7 +677,7 @@ export function createMonteCarloExperiment( error.name = "AbortError"; reject(error); } - cleanupTransport({ sendCancel: true }); + cleanupTransports({ sendCancel: true }); }; abortListener = onAbort; @@ -566,61 +692,134 @@ export function createMonteCarloExperiment( return; } status.set("Running"); - transport.send({ type: "start" }); + for (const transport of transports) { + transport.send({ type: "start" }); + } }, cancel() { if (disposed) { return; } - transport.send({ type: "cancel" }); + for (const transport of transports) { + transport.send({ type: "cancel" }); + } }, dispose() { - cleanupTransport({ sendCancel: true }); + cleanupTransports({ sendCancel: true }); }, }; - off = transport.onMessage((rawMessage) => { - const message = rawMessage as MonteCarloToMainMessage; - - switch (message.type) { - case "ready": { - status.set("Ready"); - if (!settled) { - settled = true; - resolve(handle); + /** + * Marks a shard as done and, once every shard is, emits the terminal event. + * + * The experiment is only complete when all shards complete; a single + * cancellation or error is terminal for the whole experiment, matching the + * single-worker contract. + */ + const settleShard = ( + shardIndex: number, + outcome: "complete" | "cancelled", + ) => { + if (shardSettled[shardIndex]) { + return; + } + shardSettled[shardIndex] = true; + shardFinished[shardIndex] = true; + shardCompleted[shardIndex] = outcome === "complete"; + publishMetricFrames(merger.finishShard(shardIndex)); + + if (outcome === "cancelled") { + // One shard cancelling cancels the experiment; stop the rest so they + // do not keep burning cores on results nobody will read. + for (const transport of transports) { + try { + transport.send({ type: "cancel" }); + } catch { + // Transport may already be torn down. } - break; - } - case "metricFrames": { - metrics.set(appendMetricFrames(metrics.get(), message.frames)); - break; } - case "progress": - progress.set(message.progress); - break; - case "complete": - progress.set(message.progress); - status.set("Complete"); - events.emit({ type: "complete", progress: message.progress }); - break; - case "cancelled": - progress.set(message.progress); - status.set("Cancelled"); - events.emit({ type: "cancelled", progress: message.progress }); - break; - case "error": - status.set("Error"); - events.emit({ - type: "error", - message: message.message, - itemId: message.itemId, - }); - if (!settled) { - rejectBeforeReady(new Error(message.message)); - } - break; + publishProgress(); + status.set("Cancelled"); + events.emit({ type: "cancelled", progress: progress.get() }); + return; } - }); + + publishProgress(); + + if (shardSettled.every(Boolean)) { + // Nothing should be left buffered, but flush so a shard that stopped + // mid-frame cannot silently strand its last frames. + publishMetricFrames(merger.flush()); + const finalProgress = progress.get(); + status.set("Complete"); + events.emit({ + type: "complete", + progress: finalProgress ?? { + advancedRuns: 0, + completedRuns: 0, + erroredRuns: 0, + activeRuns: 0, + allFinished: true, + runCount: config.runCount, + frameNumber: 0, + time: 0, + }, + }); + } + }; + + offListeners = transports.map((transport, shardIndex) => + transport.onMessage((rawMessage) => { + const message = rawMessage as MonteCarloToMainMessage; + + switch (message.type) { + case "ready": { + shardReady[shardIndex] = true; + if (shardReady.every(Boolean)) { + status.set("Ready"); + if (!settled) { + settled = true; + resolve(handle); + } + } + break; + } + case "metricFrames": { + publishMetricFrames(merger.accept(shardIndex, message.frames)); + break; + } + case "progress": + shardProgress[shardIndex] = message.progress; + publishProgress(); + break; + case "complete": + shardProgress[shardIndex] = message.progress; + settleShard(shardIndex, "complete"); + break; + case "cancelled": + if (message.progress) { + shardProgress[shardIndex] = message.progress; + } + settleShard(shardIndex, "cancelled"); + break; + case "error": + status.set("Error"); + events.emit({ + type: "error", + message: message.message, + itemId: message.itemId, + }); + if (settled) { + // Already handed the caller a handle, so surface the error as an + // event and tear the remaining shards down. + cleanupTransports({ sendCancel: true }); + } else { + rejectBeforeReady(new Error(message.message)); + } + break; + } + }), + ); if (config.signal) { if (config.signal.aborted) { @@ -631,20 +830,24 @@ export function createMonteCarloExperiment( } try { - transport.send({ - type: "init", - sdcpn: config.sdcpn, - extensions: config.extensions, - initialMarking: config.initialMarking, - parameterValues: config.parameterValues, - seed: config.seed, - dt: config.dt, - maxTime: config.maxTime, - hirArtifacts: config.hirArtifacts, - runCount: config.runCount, - batchSize: config.batchSize, - metricSpecs: "metricSpecs" in config ? config.metricSpecs : undefined, - }); + for (const [shardIndex, transport] of transports.entries()) { + const shard = shards[shardIndex]!; + transport.send({ + type: "init", + sdcpn: config.sdcpn, + extensions: config.extensions, + initialMarking: config.initialMarking, + parameterValues: config.parameterValues, + seed: config.seed, + dt: config.dt, + maxTime: config.maxTime, + hirArtifacts: config.hirArtifacts, + runCount: shard.runCount, + runIndexOffset: shard.runIndexOffset, + batchSize: config.batchSize, + metricSpecs: "metricSpecs" in config ? config.metricSpecs : undefined, + }); + } } catch (error) { rejectBeforeReady( error instanceof Error diff --git a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/runtime/shard-plan.test.ts b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/runtime/shard-plan.test.ts new file mode 100644 index 00000000000..1fe621a3320 --- /dev/null +++ b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/runtime/shard-plan.test.ts @@ -0,0 +1,114 @@ +import { describe, expect, it, vi } from "vitest"; + +import { + getDefaultMonteCarloShardCount, + planMonteCarloShards, +} from "./shard-plan"; + +describe("planMonteCarloShards", () => { + it("splits runs evenly when they divide exactly", () => { + expect(planMonteCarloShards(1000, 4)).toStrictEqual([ + { runIndexOffset: 0, runCount: 250 }, + { runIndexOffset: 250, runCount: 250 }, + { runIndexOffset: 500, runCount: 250 }, + { runIndexOffset: 750, runCount: 250 }, + ]); + }); + + it("spreads the remainder across leading shards instead of one straggler", () => { + const plan = planMonteCarloShards(1003, 4); + + expect(plan.map((shard) => shard.runCount)).toStrictEqual([ + 251, 251, 251, 250, + ]); + // Slice sizes differ by at most one, so no shard becomes the run everyone + // else waits on. + expect(Math.max(...plan.map((shard) => shard.runCount))).toBe(251); + }); + + it("covers every run exactly once, contiguously", () => { + for (const [runCount, shardCount] of [ + [1000, 7], + [13, 5], + [100, 3], + ] as const) { + const plan = planMonteCarloShards(runCount, shardCount); + + let expectedOffset = 0; + for (const shard of plan) { + expect(shard.runIndexOffset).toBe(expectedOffset); + expect(shard.runCount).toBeGreaterThan(0); + expectedOffset += shard.runCount; + } + expect(expectedOffset).toBe(runCount); + } + }); + + it("never creates an empty shard when runs are scarcer than shards", () => { + const plan = planMonteCarloShards(3, 16); + + expect(plan).toHaveLength(3); + expect(plan.every((shard) => shard.runCount === 1)).toBe(true); + }); + + it("produces a single full slice for one shard", () => { + expect(planMonteCarloShards(500, 1)).toStrictEqual([ + { runIndexOffset: 0, runCount: 500 }, + ]); + }); + + it.each([ + [0, 4], + [-1, 4], + [1.5, 4], + [10, 0], + [10, -2], + ])("rejects runCount %s with shardCount %s", (runCount, shardCount) => { + expect(() => planMonteCarloShards(runCount, shardCount)).toThrow( + /requires a positive/, + ); + }); +}); + +describe("getDefaultMonteCarloShardCount", () => { + // `navigator` is getter-only in Node, so it has to be stubbed rather than + // assigned. + const withHardwareConcurrency = ( + value: number | undefined, + body: () => void, + ) => { + vi.stubGlobal( + "navigator", + value === undefined ? {} : { hardwareConcurrency: value }, + ); + try { + body(); + } finally { + vi.unstubAllGlobals(); + } + }; + + it("leaves one core for the main thread", () => { + withHardwareConcurrency(10, () => { + expect(getDefaultMonteCarloShardCount(1000)).toBe(9); + }); + }); + + it("never exceeds the run count", () => { + withHardwareConcurrency(10, () => { + expect(getDefaultMonteCarloShardCount(3)).toBe(3); + }); + }); + + it("still yields one shard on a single-core host", () => { + withHardwareConcurrency(1, () => { + expect(getDefaultMonteCarloShardCount(1000)).toBe(1); + }); + }); + + it("falls back to one shard when the host reports no core count", () => { + withHardwareConcurrency(undefined, () => { + expect(getDefaultMonteCarloShardCount(1000)).toBe(1); + }); + }); +}); diff --git a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/runtime/shard-plan.ts b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/runtime/shard-plan.ts new file mode 100644 index 00000000000..c992da9fbcd --- /dev/null +++ b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/runtime/shard-plan.ts @@ -0,0 +1,91 @@ +/** + * Splitting an experiment's runs across worker shards. + * + * Runs are independent, so an experiment is parallelised by giving each worker a + * contiguous slice of the run range. Each slice carries its global start index + * so seeds stay tied to the run's position in the experiment rather than its + * position within a shard — which is what keeps results identical no matter how + * many shards are used. + */ + +export type MonteCarloShardPlanEntry = { + /** Index of this shard's first run within the whole experiment. */ + runIndexOffset: number; + /** How many runs this shard owns. Always at least 1. */ + runCount: number; +}; + +/** + * Reads the host's logical core count, where the host exposes one. + * + * Returns `null` in environments without the hint (some workers, older + * runtimes, SSR) so callers can fall back rather than guess. + */ +function detectHardwareConcurrency(): number | null { + const navigatorLike = ( + globalThis as { + navigator?: { hardwareConcurrency?: number }; + } + ).navigator; + const reported = navigatorLike?.hardwareConcurrency; + + return typeof reported === "number" && + Number.isFinite(reported) && + reported > 0 + ? Math.floor(reported) + : null; +} + +/** + * Default shard count for an experiment of `runCount` runs. + * + * One core is left for the main thread so the editor stays responsive while an + * experiment runs, and the count never exceeds the number of runs — an empty + * shard would have nothing to simulate. Falls back to a single shard when the + * host reports no core count, matching the previous single-worker behaviour. + */ +export function getDefaultMonteCarloShardCount(runCount: number): number { + const cores = detectHardwareConcurrency(); + if (cores === null) { + return 1; + } + + return Math.max(1, Math.min(runCount, cores - 1)); +} + +/** + * Splits `runCount` runs into at most `shardCount` contiguous slices. + * + * The remainder is spread one run at a time across the leading shards rather + * than piled onto the last one, so slice sizes differ by at most one and no + * single shard becomes the straggler that the whole experiment waits on. + * + * @throws Error if `runCount` or `shardCount` is not a positive integer. + */ +export function planMonteCarloShards( + runCount: number, + shardCount: number, +): MonteCarloShardPlanEntry[] { + if (!Number.isInteger(runCount) || runCount <= 0) { + throw new Error("Monte Carlo shard planning requires a positive runCount"); + } + if (!Number.isInteger(shardCount) || shardCount <= 0) { + throw new Error( + "Monte Carlo shard planning requires a positive shardCount", + ); + } + + const shards = Math.min(runCount, shardCount); + const baseSize = Math.floor(runCount / shards); + const remainder = runCount % shards; + + const plan: MonteCarloShardPlanEntry[] = []; + let runIndexOffset = 0; + for (let shard = 0; shard < shards; shard++) { + const size = baseSize + (shard < remainder ? 1 : 0); + plan.push({ runIndexOffset, runCount: size }); + runIndexOffset += size; + } + + return plan; +} diff --git a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/types.ts b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/types.ts index 660f6ff6522..79e3ce0b92f 100644 --- a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/types.ts +++ b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/types.ts @@ -27,6 +27,18 @@ export type MonteCarloSimulatorConfig = { dt: number; maxTime: number; runs?: readonly MonteCarloRunConfig[]; + /** + * Index of this simulator's first run within a larger logical experiment. + * + * Default seeds derive from `runIndexOffset + localIndex`, so an experiment + * split across several simulators (one per worker shard) gives run *i* the + * same seed regardless of which shard owns it. Shard layout therefore never + * changes an experiment's results. + * + * `runs[].seed`, `getRunSummary(index)` and `MonteCarloRunSummary.index` all + * stay shard-local; only seed derivation uses the global index. + */ + runIndexOffset?: number; /** Initial token region capacity per run, in bytes. */ initialTokenByteCapacity?: number; metrics?: readonly MonteCarloFrameMetric[]; diff --git a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/worker/messages.ts b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/worker/messages.ts index 4af3a65a41b..ef3cbdf196f 100644 --- a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/worker/messages.ts +++ b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/worker/messages.ts @@ -20,7 +20,15 @@ export type MonteCarloInitMessage = { /** Precompiled HIR artifacts (`compileHirArtifacts`) — required for any * dynamics/lambda/kernel user code in the net. */ hirArtifacts?: HirArtifacts; + /** Number of runs this worker owns — a slice of the experiment when sharded. */ runCount: number; + /** + * Index of this worker's first run within the whole experiment. + * + * Seeds derive from the global index, so an experiment sharded across workers + * runs the same set of seeds as an unsharded one. + */ + runIndexOffset?: number; batchSize?: number; metricSpecs?: readonly MonteCarloMetricSpec[]; }; diff --git a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/worker/monte-carlo.worker.ts b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/worker/monte-carlo.worker.ts index 2abfad23f08..7fc2cda0ed3 100644 --- a/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/worker/monte-carlo.worker.ts +++ b/libs/@hashintel/petrinaut-core/src/simulation/monte-carlo/worker/monte-carlo.worker.ts @@ -166,6 +166,7 @@ function initialize(message: MonteCarloInitMessage): void { maxTime: message.maxTime, hirArtifacts: message.hirArtifacts, runCount: message.runCount, + runIndexOffset: message.runIndexOffset, metrics: userMetrics, }); batchSize = message.batchSize ?? DEFAULT_BATCH_SIZE; diff --git a/libs/@hashintel/petrinaut/docs/experiments.md b/libs/@hashintel/petrinaut/docs/experiments.md index 2337d8ed2f5..54a947c8c41 100644 --- a/libs/@hashintel/petrinaut/docs/experiments.md +++ b/libs/@hashintel/petrinaut/docs/experiments.md @@ -32,20 +32,31 @@ Experiments progress through five status labels: | Status | Meaning | | ---------------- | ------------------------------------------------------------------------------------------------- | -| **Initializing** | The experiment has been created and the worker is starting up. | +| **Initializing** | The experiment has been created and its workers are starting up. | | **Running** | Runs are in progress. | | **Complete** | All runs finished without error. | | **Error** | The experiment failed to start or hit an unrecoverable error. The drawer shows the error message. | -| **Cancelled** | You clicked **Cancel**, or the worker was cancelled. | +| **Cancelled** | You clicked **Cancel**, or the experiment was cancelled. | -Each experiment runs in its own background Web Worker, so simulation playback and editor interactions stay responsive. Multiple experiments can run concurrently. +Experiments run in background Web Workers, so simulation playback and editor interactions stay responsive. Multiple experiments can run concurrently. + +### Parallel runs + +Because every run is independent, an experiment splits its runs across several workers and runs them at the same time -- by default one worker per processor core, leaving one core free so the editor stays responsive. A 1000-run experiment on an 8-core machine runs roughly 4x faster than it would on a single worker. + +This never changes an experiment's results. Each run's seed is derived from its position in the experiment rather than from which worker happens to execute it, and the per-frame statistics from each worker are combined into the same distributions you would get from running everything sequentially. Re-running an experiment with the same configuration and seed gives the same numbers regardless of how many cores the machine has. + +Two consequences worth knowing: + +- Progress reports the slowest worker's position, so the progress bar never runs ahead of the results behind it. +- Several experiments running at once each use the same number of workers, so they compete for cores and all of them slow down. Run them one at a time if you want any single one to finish as fast as possible. ### Actions In the experiment's view drawer (open it by clicking a row in the list, or any experiment in the top-bar **Active experiments** popover): - **Cancel** -- stops the experiment. Only available while it is initializing or running. -- **Remove** -- deletes the record and disposes the worker. Available after completion, cancellation, or error. +- **Remove** -- deletes the record and disposes the experiment's workers. Available after completion, cancellation, or error. - **Close** -- closes the drawer without affecting the experiment. There is no built-in restart action -- to re-run with the same configuration, **Create** a new experiment with the same settings. diff --git a/libs/@hashintel/petrinaut/src/react/experiments/provider.test.tsx b/libs/@hashintel/petrinaut/src/react/experiments/provider.test.tsx index 0a3ffb9cde9..653102d60c8 100644 --- a/libs/@hashintel/petrinaut/src/react/experiments/provider.test.tsx +++ b/libs/@hashintel/petrinaut/src/react/experiments/provider.test.tsx @@ -77,6 +77,11 @@ function makeMetricFrame(): MonteCarloUserDefinedMetricFrame { timeValue: null, runSampleCount: 2, timeSampleCount: 1, + // Carried on every scalar frame so a sharded experiment can merge across + // shards before reducing. + runAggregate: { count: 2, sum: 2, min: 1, max: 1, last: 1 }, + aggregateRuns: "mean", + aggregateTime: "none", }; } @@ -208,6 +213,9 @@ const TestWrapper = ({ MonteCarloToMainMessage > } + // One shard, so a single fake worker stands in for the whole + // experiment. Sharding itself is covered in petrinaut-core. + experimentShardCount={1} > diff --git a/libs/@hashintel/petrinaut/src/react/experiments/provider.tsx b/libs/@hashintel/petrinaut/src/react/experiments/provider.tsx index 68513c80338..dcf2d4c7079 100644 --- a/libs/@hashintel/petrinaut/src/react/experiments/provider.tsx +++ b/libs/@hashintel/petrinaut/src/react/experiments/provider.tsx @@ -35,6 +35,15 @@ import { type ExperimentsProviderProps = React.PropsWithChildren<{ workerFactory?: WorkerFactory; + /** + * How many workers each experiment splits its runs across. + * + * Defaults to one per logical core minus one. Sharding never changes an + * experiment's results, only how quickly it finishes, so this exists for hosts + * that need to cap CPU use (or pin behaviour in tests) rather than to affect + * output. + */ + experimentShardCount?: number; }>; type ExperimentHandleRegistration = { @@ -163,6 +172,7 @@ function assertExperimentInput(input: CreateExperimentInput): void { export const ExperimentsProvider: React.FC = ({ children, workerFactory, + experimentShardCount, }) => { const { extensions, petriNetDefinition } = use(SDCPNContext); const { requestHirArtifacts } = use(LanguageClientContext); @@ -170,6 +180,7 @@ export const ExperimentsProvider: React.FC = ({ const petriNetDefinitionRef = useLatest(petriNetDefinition); const extensionsRef = useLatest(extensions); const workerFactoryRef = useLatest(workerFactory ?? createMonteCarloWorker); + const shardCountRef = useLatest(experimentShardCount); const registrationsRef = useRef( new Map(), ); @@ -433,6 +444,9 @@ export const ExperimentsProvider: React.FC = ({ const handle = await createMonteCarloExperiment({ ...experimentConfigBase, createWorker: workerFactoryRef.current, + ...(shardCountRef.current === undefined + ? {} + : { shardCount: shardCountRef.current }), metricSpecs, signal: abortController.signal, }); diff --git a/libs/@local/petrinaut-arch-docs/content/simulation/worker-sharding.mdx b/libs/@local/petrinaut-arch-docs/content/simulation/worker-sharding.mdx new file mode 100644 index 00000000000..35dcce46106 --- /dev/null +++ b/libs/@local/petrinaut-arch-docs/content/simulation/worker-sharding.mdx @@ -0,0 +1,42 @@ +--- +title: Worker sharding +description: How one experiment's runs split across Web Workers without changing what it reports. +sidebar_order: 20 +attachTo: core.simulation.monte-carlo +--- + +Runs are independent — own seed, own RNG state, own frame buffers, no shared +mutable state. An experiment therefore splits them across one worker per logical +core minus one, each running its own `MonteCarloSimulator` over a slice of the +runs. Measured ~4x at 8 shards on a 10-core machine. + +No `SharedArrayBuffer` is involved: the app is not cross-origin isolated, and +message-passing shards over independent runs need no shared memory. + +## Why sharding cannot change results + +Two properties carry it, and both are easy to break: + +**Seeds derive from the run's global index.** `runIndexOffset + localIndex`, so +run *i* gets the same seed whichever worker owns it. Seeding from the position +*within* a shard would silently correlate runs that must be independent. + +**Frames merge through a monoid.** `frameValue` on a scalar metric frame is +already reduced and cannot be combined — a mean of means is not a mean — so +frames carry their pre-reduction accumulator state, and `metrics/merge.ts` +recombines it. Output is byte-identical at every shard count. + +## The frame watermark + +A frame is finalised only once every *still-running* shard has reported it. +Finished shards are dropped from the watermark rather than blocking it, which is +what stops a completed shard stalling the experiment. + +Progress therefore reports the slowest shard's position — the bar never runs +ahead of the results behind it. + +```text +runs 0..k -> Worker 1 ┐ +runs k..2k -> Worker 2 ├─ per-frame accumulator state -> merge -> frame N +runs 2k..N -> Worker 3 ┘ (at slowest shard) +```