Skip to content

feat(engine-gorules): GoRules ZEN wrapper + rules DRY fix#20

Open
albertgwo wants to merge 3 commits intofeat/engine-pr5-scoped-compensationfrom
feat/engine-pr8-gorules-zen
Open

feat(engine-gorules): GoRules ZEN wrapper + rules DRY fix#20
albertgwo wants to merge 3 commits intofeat/engine-pr5-scoped-compensationfrom
feat/engine-pr8-gorules-zen

Conversation

@albertgwo
Copy link
Copy Markdown
Contributor

@albertgwo albertgwo commented Mar 19, 2026

User description

Summary

  • Add `@ruminaider/flowprint-engine-gorules` package wrapping GoRules ZEN engine
  • Extract shared rules evaluation logic into `core.ts` (DRY fix across engine + gorules)
  • ZEN evaluator supports `.json` decision tables and expression evaluation
  • Translator converts flowprint rules format to ZEN-compatible format

Dependency

Base: `feat/engine-pr5-scoped-compensation`

PR 12 of 16 in the execution engine implementation.

Test plan

  • ZEN evaluator processes decision tables correctly
  • Translator converts flowprint rules to ZEN format
  • Shared core logic works identically in both engine and gorules
  • GoRules WASM loads and executes successfully

Generated description

Below is a concise technical summary of the changes proposed in this PR:
Add a @ruminaider/flowprint-engine-gorules package that wraps GoRules ZEN to evaluate flowprint expressions and rules via JDM with caching, expression helpers, translator, and tests. Extract the shared operator/condition/hit policy helpers into a core module so both the browser and Node evaluators reuse the same logic while keeping bundler configs aware of the new entry point.

TopicDetails
GoRules evaluator Add GoRules ZEN wrapper, translator, and expression helpers so flowprint rules evaluate via JDM with cached decisions and dedicated tests for expressions/rules translation.
Modified files (11)
  • packages/engine-gorules/package.json
  • packages/engine-gorules/src/__tests__/evaluator.test.ts
  • packages/engine-gorules/src/__tests__/rules-evaluator.test.ts
  • packages/engine-gorules/src/__tests__/rules-translator.test.ts
  • packages/engine-gorules/src/evaluator.ts
  • packages/engine-gorules/src/index.ts
  • packages/engine-gorules/src/rules-evaluator.ts
  • packages/engine-gorules/src/rules-translator.ts
  • packages/engine-gorules/tsconfig.json
  • packages/engine-gorules/tsup.config.ts
  • packages/engine-gorules/vitest.config.ts
Latest Contributors(0)
UserCommitDate
Shared rules core Extract core rules helpers (operators, hit policies, dot-path resolution) into shared core.ts and rewire browser/Node evaluators plus bundler config to consume it.
Modified files (5)
  • packages/engine/src/__tests__/rules/evaluator-browser.test.ts
  • packages/engine/src/rules/core.ts
  • packages/engine/src/rules/evaluator-browser.ts
  • packages/engine/src/rules/evaluator.ts
  • packages/engine/tsup.config.ts
Latest Contributors(1)
UserCommitDate
albertgwo@gmail.comfeat-complete-the-loop...March 02, 2026
This pull request is reviewed by Baz. Review like a pro on (Baz).

Move pure evaluation functions (resolveDotPath, ruleMatches,
evaluateCondition, normalizeCondition, evaluateOperator, applyHitPolicy)
from evaluator-browser.ts into a new core.ts module. Both Node.js and
browser evaluators now import from core.ts, eliminating duplication.
New @ruminaider/flowprint-engine-gorules package wrapping @gorules/zen-engine:
- evaluateExpression/evaluateExpressions: sync expression evaluation via ZEN
- translateToJDM: converts flowprint .rules.yaml to GoRules JDM format
- evaluateRulesViaZen: async decision table evaluation with caching
- conditionToUnary: operator-to-ZEN unary expression mapping

Supports all flowprint operators (eq, gt, gte, lt, lte, not_eq, in,
not_in, between) and all hit policies (first, collect, all, priority).
38 tests covering:
- Expression evaluation: arithmetic, boolean logic, ZEN syntax, errors
- Multiple expressions with chained references
- Rules translator: all operator mappings, JDM structure, hit policies
- Rules evaluator: first/collect hit policies, wildcard rules, no-match
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying flowprint with  Cloudflare Pages  Cloudflare Pages

Latest commit: e48fef4
Status:🚫  Build failed.

View logs

Comment on lines +41 to +50
const response = await decision.evaluate(input)

const isCollect = doc.hit_policy === 'collect' || doc.hit_policy === 'all'

return {
hit_policy: doc.hit_policy,
matched_count: isCollect
? (Array.isArray(response.result) ? response.result.length : 0)
: (isNonEmptyObject(response.result) ? 1 : 0),
output: response.result,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

For hit_policy: 'all' the GoRules wrapper returns matched_count:0/output:[] instead of throwing like the core engine — should we throw when doc.hit_policy === 'all' and the translated result array is empty?

Finding type: Logical Bugs | Severity: 🔴 High


Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents:

In packages/engine-gorules/src/rules-evaluator.ts around lines 41 to 50, the
evaluateRulesViaZen function treats hit_policy 'all' as a collect array and returns
matched_count 0/output [] instead of throwing when no rules matched. Change the logic so
that after awaiting decision.evaluate(input) and before returning, if doc.hit_policy ===
'all' and response.result is an array with length === 0, throw an Error (matching the
core engine semantics) rather than returning a zero count. Keep the existing
matched_count/output computation for other cases.

Heads up!

Your free trial ends tomorrow.
To keep getting your PRs reviewed by Baz, update your team's subscription

Comment on lines +265 to +274
function normalizeCondition(condition: Condition): OperatorCondition {
if (
condition === null ||
typeof condition === 'string' ||
typeof condition === 'number' ||
typeof condition === 'boolean'
) {
return { eq: condition }
}
return condition
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

normalizeCondition duplicates the exact scalar-to-{eq} normalization that already exists in packages/engine/src/rules/core.ts (lines 77‑86). Any future change to shorthand handling (e.g., new operators, null/boolean treatment) would need to be applied in both translator and engine logic, which is easy to miss and causes inconsistent condition normalization between the GoRules translator and the Flowprint runtime. Can we import/re‑export the shared normalizeCondition helper from the Flowprint rules core (or add it to a shared utils module) so the translator and runtime use the same logic rather than duplicating the implementation?

Finding type: Code Dedup and Conventions | Severity: 🟢 Low


Want Baz to fix this for you? Activate Fixer

Heads up!

Your free trial ends tomorrow.
To keep getting your PRs reviewed by Baz, update your team's subscription

Comment on lines +284 to +288
return `> ${String(operand)}`
case 'gte':
return `>= ${String(operand)}`
case 'lt':
return `< ${String(operand)}`
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

operatorToUnary stringifies Condition values and emits unparseable literals for non-primitives like objects/arrays/dates — should we validate/reject non-primitive operands or serialize arrays per GoRules before emitting the cell?

Finding type: Logical Bugs | Severity: 🔴 High


Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents:

In packages/engine-gorules/src/rules-translator.ts around lines 284 to 288, the
operatorToUnary function currently interpolates String(operand) for gt/gte/lt/lte which
will produce invalid JDM when operand is a non-primitive (object/array/date). Change
operatorToUnary to validate operands: for gt/gte/lt/lte require a number or
numeric-string and throw a clear Error if the operand is not a primitive number; for
eq/not_eq accept string/number/boolean/null only and throw for objects; for
in/not_in/between allow arrays but validate each element is a primitive
(string/number/boolean/null) and serialize via formatLiteral (or throw if any element is
non-primitive); for default cases throw an error for unsupported operand types instead
of String(operand). Also update conditionToUnary to catch and rethrow errors with the
field name and operator context so invalid rules fail fast with helpful messages.

Heads up!

Your free trial ends tomorrow.
To keep getting your PRs reviewed by Baz, update your team's subscription

Comment on lines +324 to +328
function valueToExpression(value: unknown): string {
if (typeof value === 'string') {
return `"${value}"`
}
if (value === null) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Should we guard/throw in valueToExpression when an output isn't a primitive or array literal, or serialize it to a supported form, since non-primitives become [object Object] or functions that GoRules can't parse?

Finding type: Logical Bugs | Severity: 🔴 High


Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents:

In packages/engine-gorules/src/rules-translator.ts around lines 324 to 328, the
valueToExpression function currently only quotes strings and falls back to
String(value), which produces '[object Object]' for objects/arrays/dates. Change
valueToExpression to: accept primitives (string -> quoted, null -> 'null',
number/boolean -> String), handle arrays by recursively serializing each element (only
allow primitives inside arrays) and emit a bracketed list or comma-separated literals,
and explicitly detect and throw a descriptive error for unsupported types (plain
objects, Dates, functions, symbols) instead of returning String(value). Ensure
tests/usage expect a thrown error for unsupported outputs or that arrays are serialized
deterministically.

Heads up!

Your free trial ends tomorrow.
To keep getting your PRs reviewed by Baz, update your team's subscription

Comment on lines +1 to +12
{
"name": "@ruminaider/flowprint-engine-gorules",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@ruminaider/flowprint-engine-gorules added without a .changeset/*.md file — should we add a changeset?

Finding type: AI Coding Guidelines | Severity: 🟠 Medium


Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents:

In packages/engine-gorules/package.json around lines 1-34, a new package
"@ruminaider/flowprint-engine-gorules" was added but no .changeset/*.md file was
created. Add a new changeset file under .changeset (for example
.changeset/add-flowprint-engine-gorules.md) that documents this package addition:
include a YAML-style header or changeset YAML mapping listing
"@ruminaider/flowprint-engine-gorules: minor" (or patch if you prefer), and a short
description like "Add @ruminaider/flowprint-engine-gorules package" plus an optional
reference to the PR/author. Ensure the new file follows the repo's changeset format so
the CI will recognize the package change.

Heads up!

Your free trial ends tomorrow.
To keep getting your PRs reviewed by Baz, update your team's subscription

@albertgwo
Copy link
Copy Markdown
Contributor Author

Code Review Findings

Critical (90-100 confidence)

1. hit_policy: 'all' behavioral divergence (95)

  • File: packages/engine-gorules/src/rules-evaluator.ts:~60-68
  • Core engine's applyHitPolicy throws when zero rules match for 'all' policy. ZEN evaluator silently returns { matched_count: 0, output: [] }. Consumers relying on the error-on-no-match guarantee see silent failures with GoRules.
  • Fix: Add post-ZEN check: if (doc.hit_policy === 'all' && result.length === 0) throw

2. normalizeCondition duplicated instead of imported — contradicts DRY goal (92)

  • File: packages/engine-gorules/src/rules-translator.ts:~854-864
  • Exact copy of function from packages/engine/src/rules/core.ts. This PR's stated goal is DRY extraction, yet this function was re-implemented.
  • Fix: Export normalizeCondition from @ruminaider/flowprint-engine public API, import in gorules.

3. core.ts added as tsup entry point but no exports field in package.json (90)

  • Files: packages/engine/tsup.config.ts and packages/engine/package.json
  • Builds to dist/rules/core.js but no export map entry. Will fail under strict ESM resolution.
  • Fix: Either add "./core" export entry or remove from tsup entry array (let it be bundled as internal chunk).

Important (80-89 confidence)

4. Unbounded decisionCache with no eviction (85)

  • File: packages/engine-gorules/src/rules-evaluator.ts:529-530
  • Map<string, ...> keyed by JSON.stringify(doc). No size limit, no TTL. Leaks JS heap + WASM memory in long-running processes.
  • Fix: Add LRU eviction or document memory implications.

5. not_in translation has no end-to-end test through ZEN engine (85)

  • Translation tested (string output) but no evaluator test exercises not_in through actual ZEN execution.

6. valueToExpression silently produces garbage for non-primitives (83)

  • File: packages/engine-gorules/src/rules-translator.ts:866-897
  • Objects produce "[object Object]", arrays produce comma-joined strings. ZEN evaluates as expressions, not literals.
  • Fix: Add type guards that throw for unsupported types.

7. Missing changeset file (82)

  • New package + engine modifications.

8. ZenRulesResult.hit_policy typed as string instead of HitPolicy union (80)

  • File: packages/engine-gorules/src/rules-evaluator.ts:525

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