Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions packages/engine-gorules/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"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"
Comment on lines +1 to +12
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

},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
}
},
"scripts": {
"build": "tsup",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@gorules/zen-engine": "^0.54.0",
"@ruminaider/flowprint-engine": "workspace:*"
},
"devDependencies": {
"tsup": "^8.0.0",
"typescript": "^5.5.0",
"vitest": "^2.0.0"
}
}
72 changes: 72 additions & 0 deletions packages/engine-gorules/src/__tests__/evaluator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { describe, it, expect } from 'vitest'
import { evaluateExpression, evaluateExpressions } from '../evaluator.js'

describe('evaluateExpression', () => {
it('evaluates arithmetic: qty * price', () => {
const result = evaluateExpression('qty * price', { qty: 5, price: 10 })
expect(result).toBe(50)
})

it('evaluates boolean with ZEN syntax: and/or', () => {
const result = evaluateExpression(
'tier == "enterprise" and amount > 10000',
{ tier: 'enterprise', amount: 15000 },
)
expect(result).toBe(true)
})

it('evaluates boolean false result', () => {
const result = evaluateExpression(
'tier == "enterprise" and amount > 10000',
{ tier: 'basic', amount: 15000 },
)
expect(result).toBe(false)
})

it('evaluates string comparison', () => {
const result = evaluateExpression('status == "active"', { status: 'active' })
expect(result).toBe(true)
})

it('evaluates numeric comparison', () => {
const result = evaluateExpression('score >= 80', { score: 85 })
expect(result).toBe(true)
})

it('throws on invalid expression syntax', () => {
expect(() => evaluateExpression('((( invalid', {})).toThrow()
})
})

describe('evaluateExpressions', () => {
it('evaluates multiple expressions', () => {
const result = evaluateExpressions(
{
subtotal: 'qty * price',
tax: 'qty * price * 0.1',
},
{ qty: 5, price: 10 },
)
expect(result).toEqual({
subtotal: 50,
tax: 5,
})
})

it('later expressions reference earlier results', () => {
const result = evaluateExpressions(
{
subtotal: 'qty * price',
total: 'subtotal * 1.1',
},
{ qty: 5, price: 10 },
)
expect(result.subtotal).toBe(50)
expect(result.total).toBeCloseTo(55)
})

it('returns empty object for empty expressions', () => {
const result = evaluateExpressions({}, { qty: 5 })
expect(result).toEqual({})
})
})
113 changes: 113 additions & 0 deletions packages/engine-gorules/src/__tests__/rules-evaluator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { describe, it, expect, afterAll } from 'vitest'
import { evaluateRulesViaZen, disposeZenEngine } from '../rules-evaluator.js'
import type { RulesDocument } from '@ruminaider/flowprint-engine'

afterAll(() => {
disposeZenEngine()
})

function makeDoc(overrides: Partial<RulesDocument> = {}): RulesDocument {
return {
schema: 'flowprint-rules/1.0',
name: 'test-rules',
hit_policy: 'first',
rules: [],
...overrides,
}
}

describe('evaluateRulesViaZen', () => {
describe('first hit policy', () => {
it('returns first matching rule output', async () => {
const doc = makeDoc({
hit_policy: 'first',
inputs: ['tier'],
rules: [
{ when: { tier: { eq: 'enterprise' } }, then: { discount: 20 } },
{ when: { tier: { eq: 'pro' } }, then: { discount: 10 } },
{ then: { discount: 0 } },
],
})

const result = await evaluateRulesViaZen(doc, { tier: 'enterprise' })
expect(result.hit_policy).toBe('first')
expect(result.output).toEqual({ discount: 20 })
expect(result.matched_count).toBe(1)
})

it('returns empty object when no match', async () => {
const doc = makeDoc({
hit_policy: 'first',
inputs: ['tier'],
rules: [
{ when: { tier: { eq: 'enterprise' } }, then: { discount: 20 } },
],
})

const result = await evaluateRulesViaZen(doc, { tier: 'basic' })
expect(result.matched_count).toBe(0)
expect(result.output).toEqual({})
})

it('evaluates numeric comparison operators', async () => {
const doc = makeDoc({
hit_policy: 'first',
inputs: ['amount'],
rules: [
{ when: { amount: { gte: 1000 } }, then: { tier: 'high' } },
{ when: { amount: { gte: 100 } }, then: { tier: 'mid' } },
{ then: { tier: 'low' } },
],
})

const result = await evaluateRulesViaZen(doc, { amount: 500 })
expect(result.output).toEqual({ tier: 'mid' })
})
})

describe('collect hit policy', () => {
it('returns all matching rules as array', async () => {
const doc = makeDoc({
hit_policy: 'collect',
inputs: ['x'],
rules: [
{ when: { x: { gt: 0 } }, then: { rule: 1 } },
{ when: { x: { gt: 5 } }, then: { rule: 2 } },
{ when: { x: { gt: 100 } }, then: { rule: 3 } },
],
})

const result = await evaluateRulesViaZen(doc, { x: 10 })
expect(result.hit_policy).toBe('collect')
expect(result.output).toEqual([{ rule: 1 }, { rule: 2 }])
expect(result.matched_count).toBe(2)
})

it('returns empty array when no match', async () => {
const doc = makeDoc({
hit_policy: 'collect',
inputs: ['x'],
rules: [
{ when: { x: { gt: 100 } }, then: { rule: 1 } },
],
})

const result = await evaluateRulesViaZen(doc, { x: 5 })
expect(result.output).toEqual([])
expect(result.matched_count).toBe(0)
})
})

describe('wildcard rules', () => {
it('matches wildcard rule (no when clause)', async () => {
const doc = makeDoc({
hit_policy: 'first',
rules: [{ then: { default: true } }],
})

const result = await evaluateRulesViaZen(doc, {})
expect(result.output).toEqual({ default: true })
expect(result.matched_count).toBe(1)
})
})
})
Loading