You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Reframed from the original report. This is not "an engine is missing — ship it." It is: the js expression dialect should be retired, because js already has a correct, real home as the L2 ScriptBody escape hatch, and a second js surface at the L1 expression layer is redundant and works against the platform's AI-safety model.
Separately, the original title's premise is inverted — hasDialect('js') returns true, not false — because the stub-detection in hasDialect is dead code. That is a real latent bug worth fixing regardless of the js decision.
Correction to the original report
hasDialect('js') returns true, not false (verified empirically).
Root cause:hasDialect detects stubs via dialect.startsWith('stub:'). But stubs are registered under their real name — makeStub('js') produces { dialect: 'js', … }, and register keys the map on engine.dialect, so a stub must carry the real name to be found by registry.get('js'). No engine is ever registered with a stub:-prefixed dialect, so the check is dead code and hasDialect collapses to registry.has(). It reports the js stub as a real engine.
False-green test:hasDialect distinguishes real engines from stubs only asserts hasDialect('cel') === true; it never asserts the stub case. If it asserted hasDialect('js') === false it would fail today. The test name promises a guarantee it does not check.
Stale doc comment:registry.ts:8 calls cron a stub, but cron is a real engine (registry.ts:47). Only js is a stub.
Two js surfaces — do not conflate
Slot
Status
Nature
js expression dialect (this issue)
L1 expression envelope — the same slot as CEL: formula fields, validation/visibility/sharing predicates, flow conditions
The platform already ships a clean two-tier model: L1 = CEL (declarative predicates/formulas) and L2 = sandboxed js (procedural bodies). "Given CEL exists, why js?" — because you want an escape hatch, and that escape hatch already exists at L2. The jsexpression dialect is a third thing that belongs to neither tier.
Why retire rather than ship
Redundant with L2. Procedural js already has a home. A js-as-formula layer just overlaps it.
Corrosive to the AI-safety model. CEL is non-Turing-complete, side-effect-free, and statically analyzable → it can be linted, explained, diffed, and reviewed in the draft-gating flow (ADR-0033), and rejected at author time by the validate gate. AI-authored CEL is verifiable before it runs. Arbitrary js is not — you can only know it is correct by running it. ADR-0096 explicitly warns on "handing AI a trusted body" and steers execution surfaces toward a bounded posture; a new unbounded js expression surface reverses that trajectory and undercuts the "structured, lintable/explainable/diffable" AI-native thesis.
Ownership cost. Shipping @objectstack/plugin-js-vm means owning a JS sandbox (sandbox-escape is an entire CVE class) — for a capability that duplicates L2.
Primary — remove it. Drop js from ExpressionDialect and delete the stub (registry.ts:51). Precondition: confirm there are no dialect: 'js' consumers (open-core = 0 today; if cloud/EE has any, go through a deprecation window). Keep js strictly at L2 ScriptBody.
Fallback — only if there is real inline-js-formula demand (e.g. migrating Salesforce formula fields that exceed CEL): do not default it into open-core. Ship it as an explicit opt-in enterprise plugin, keep CEL the strong default, and require explicit registration plus a load-time lint warn. This keeps the AI build agent emitting CEL only, with js as a loud human-only opt-in — consistent with ADR-0096's bounded posture.
Regardless of the js decision, fix the hygiene bugs now (independent of the strategy above):
Mark stubs explicitly (e.g. a stub: true field on the engine) and make hasDialect honor it, so it returns false for stubs — the behavior its name and test title already claim.
Add the missing assertion expect(hasDialect('js')).toBe(false) (prove the gate can go red before trusting it).
Fix the stale registry.ts:8 comment (cron is real, not a stub).
Provenance
Found during the #1880 docs implementation-accuracy audit (PR #3243); deepened into an architecture question during evaluation. Supersedes the original "ship the plugin or annotate the enum" framing — annotation preserves both the drift and the authoring invitation; removal (or hard opt-in gating) eliminates them.
Summary
Reframed from the original report. This is not "an engine is missing — ship it." It is: the
jsexpression dialect should be retired, becausejsalready has a correct, real home as the L2 ScriptBody escape hatch, and a secondjssurface at the L1 expression layer is redundant and works against the platform's AI-safety model.Separately, the original title's premise is inverted —
hasDialect('js')returnstrue, notfalse— because the stub-detection inhasDialectis dead code. That is a real latent bug worth fixing regardless of thejsdecision.Correction to the original report
hasDialect('js')returnstrue, notfalse(verified empirically).hasDialectdetects stubs viadialect.startsWith('stub:'). But stubs are registered under their real name —makeStub('js')produces{ dialect: 'js', … }, andregisterkeys the map onengine.dialect, so a stub must carry the real name to be found byregistry.get('js'). No engine is ever registered with astub:-prefixed dialect, so the check is dead code andhasDialectcollapses toregistry.has(). It reports thejsstub as a real engine.hasDialect distinguishes real engines from stubsonly assertshasDialect('cel') === true; it never asserts the stub case. If it assertedhasDialect('js') === falseit would fail today. The test name promises a guarantee it does not check.crona stub, butcronis a real engine (registry.ts:47). Onlyjsis a stub.Two
jssurfaces — do not conflatejsexpression dialect (this issue)language: 'js'{ language:'js', capabilities:[…] }, sandboxed + capability-gated (hook.zod.ts:99, action.zod.ts:335)The platform already ships a clean two-tier model: L1 = CEL (declarative predicates/formulas) and L2 = sandboxed js (procedural bodies). "Given CEL exists, why
js?" — because you want an escape hatch, and that escape hatch already exists at L2. Thejsexpression dialect is a third thing that belongs to neither tier.Why retire rather than ship
@objectstack/plugin-js-vmmeans owning a JS sandbox (sandbox-escape is an entire CVE class) — for a capability that duplicates L2.ExpressionDialectenum advertises a capability that does not exist, inviting authors and the AI build agent to trydialect: 'js'. This is the same declared-but-unenforced drift Audit: several event/subscription/connector enums are schema-only (declared, no runtime consumer) #3197 / docs(spec): annotate schema-only event/subscription/connector enums as not-yet-enforced (#3197) #3212 keep cleaning up.Recommendation
Primary — remove it. Drop
jsfromExpressionDialectand delete the stub (registry.ts:51). Precondition: confirm there are nodialect: 'js'consumers (open-core = 0 today; if cloud/EE has any, go through a deprecation window). Keepjsstrictly at L2 ScriptBody.Fallback — only if there is real inline-js-formula demand (e.g. migrating Salesforce formula fields that exceed CEL): do not default it into open-core. Ship it as an explicit opt-in enterprise plugin, keep CEL the strong default, and require explicit registration plus a load-time lint warn. This keeps the AI build agent emitting CEL only, with js as a loud human-only opt-in — consistent with ADR-0096's bounded posture.
Regardless of the
jsdecision, fix the hygiene bugs now (independent of the strategy above):stub: truefield on the engine) and makehasDialecthonor it, so it returnsfalsefor stubs — the behavior its name and test title already claim.expect(hasDialect('js')).toBe(false)(prove the gate can go red before trusting it).Provenance
Found during the #1880 docs implementation-accuracy audit (PR #3243); deepened into an architecture question during evaluation. Supersedes the original "ship the plugin or annotate the enum" framing — annotation preserves both the drift and the authoring invitation; removal (or hard opt-in gating) eliminates them.