Skip to content

spike(scripting): JS execution prototype - deterministic Math.random/Date.now#198

Open
ake2l wants to merge 1 commit into
developmentfrom
spike/js-execution-prototype
Open

spike(scripting): JS execution prototype - deterministic Math.random/Date.now#198
ake2l wants to merge 1 commit into
developmentfrom
spike/js-execution-prototype

Conversation

@ake2l

@ake2l ake2l commented Jul 6, 2026

Copy link
Copy Markdown
Member

Summary

Prototype, not a finished feature. Not wired into the DSL/parser - no {js:...} inline-script support or .js-execute element yet. This answers one question: can CE run JS deterministically at all, given it's a deterministic-first engine (AGENTS.md rule 6: same seed → same output) and a JS engine ships nondeterministic builtins by default.

  • datamimic_ce/scripting/js_engine.py wraps mini-racer (embedded V8), gated behind a new optional dependency group: pip install datamimic_ce[js]. Not imported anywhere else - zero impact on the base install.
  • The hard part: Math.random()/Date.now() are real wall-clock/real-random by default. Bridging Math.random through a Python callback per call is possible (mini_racer.wrap_py_function) but async-only, which doesn't fit CE's synchronous evaluation model. Instead: derive one seed from CE's own RNG chain (spawn_rng/derive_child_seed - the same helper BaseDomainGenerator._derive_rng already uses for every other generator) and seed a small pure-JS PRNG (Mulberry32) with it - no Python↔JS bridge needed per eval call. Date.now pins to the same DETERMINISTIC_ANCHOR every other "now"-derived field in CE already uses (explicitly anchored to UTC, since the anchor itself is a naive datetime and .timestamp() on a naive datetime uses the local machine's timezone otherwise - would have silently broken cross-machine reproducibility).
  • Unseeded (no rng passed): left alone, native Math.random/Date.now - matches "no rngSeed = every run differs, by design."

Known gap, not solved here: bare new Date() (argument-less) still reads real wall-clock - only Date.now() is pinned. Fine for a feasibility spike, would need closing before real use.

Still open, this PR doesn't decide it: whether/how to wire this into the DSL ({js:...}, .js-execute) is a product and security call - executing arbitrary JS is a real surface, and that decision belongs to a human, not this prototype.

Test plan

  • Basic eval works.
  • Same seed → identical Math.random() sequence across two separately-constructed engines (both a single draw and a 10-call sequence).
  • Different seeds diverge.
  • Date.now() pins regardless of seed value.
  • An unseeded engine stays genuinely non-deterministic.
  • mypy/ruff clean; tests skip gracefully (pytest.importorskip) if mini-racer isn't installed, so CI without the js extra is unaffected.

…Date.now

PROTOTYPE, not wired into the DSL/parser. Evaluates JS via mini-racer
(embedded V8, optional dependency: pip install datamimic_ce[js]) - the
capability {js:...} inline scripts and .js-execute would need, since CE has
no JS engine today.

The part worth prototyping isn't "can we eval JS" - it's that a JS engine
ships nondeterministic builtins (Math.random, Date.now/new Date) inside a
deterministic-first engine (AGENTS.md rule 6: same seed -> same output).
Bridging Math.random through a Python callback per call is possible
(mini-racer's wrap_py_function) but async-only, which doesn't fit CE's
synchronous evaluation model. Instead: derive ONE seed from CE's own RNG
chain (spawn_rng/derive_child_seed, the same helper
BaseDomainGenerator._derive_rng uses) and seed a small pure-JS PRNG
(Mulberry32) with it - no Python<->JS bridge needed per eval call. Date.now
is pinned to the same DETERMINISTIC_ANCHOR every other "now"-derived field in
CE already uses (anchored to UTC explicitly, since the anchor itself is a
naive datetime and .timestamp() on a naive datetime uses local-machine
timezone otherwise).

Verified: same seed -> identical Math.random() sequence across two
separately-constructed engines (both a single draw and a 10-call sequence);
different seeds diverge; Date.now() pins regardless of seed value; an
unseeded engine stays genuinely non-deterministic (matches "no rngSeed =
every run differs, by design").

Known gap, not solved here: new Date() (argument-less) still reads real
wall-clock - only Date.now() is pinned. Fine for a feasibility spike, would
need closing before real use.

Kept as a local, unpushed branch - not a PR. The remaining decision (wire
into the DSL as {js:...}/.js-execute, or not) is a product/security call,
not an engineering one this spike can settle by itself.
@sonarqubecloud

sonarqubecloud Bot commented Jul 6, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant