diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..151dbc5f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# Default owner for everything in this repository. +* @phenixrizen diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..5f15176a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,83 @@ +name: Bug report +description: Something evaluates or behaves incorrectly +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + A minimal decision graph plus its input is by far the most useful thing you can give us. + Without one, most evaluation bugs cannot be reproduced. + - type: textarea + id: what-happened + attributes: + label: What happened + description: What you observed, and what you expected instead. + validations: + required: true + - type: textarea + id: graph + attributes: + label: Decision graph + description: The smallest JDM graph that reproduces this. + render: json + validations: + required: true + - type: textarea + id: input + attributes: + label: Input + description: The context passed to the evaluation. + render: json + validations: + required: true + - type: textarea + id: output + attributes: + label: Actual output + description: What came back, including any error. + render: json + validations: + required: false + - type: dropdown + id: binding + attributes: + label: How are you calling it? + options: + - Rust crate + - Go + - NodeJS + - Python + - C / FFI + - UniFFI + - Other + validations: + required: true + - type: input + id: version + attributes: + label: Version or commit + placeholder: "v1.0.0-beta.12, or a commit SHA" + validations: + required: true + - type: input + id: features + attributes: + label: Cargo features enabled + description: Numeric behaviour differs with `arbitrary_precision`, so this matters for anything involving numbers. + placeholder: "default, or arbitrary_precision" + validations: + required: false + - type: input + id: platform + attributes: + label: Platform + placeholder: "linux-x64-gnu, macOS arm64, ..." + validations: + required: false + - type: checkboxes + id: upstream + attributes: + label: Upstream + options: + - label: I checked whether this also reproduces on upstream `gorules/zen` + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..cac15faf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Bug in upstream ZEN, not specific to this fork + url: https://github.com/gorules/zen/issues + about: If it reproduces on gorules/zen, report it there too so both projects benefit. + - name: Security vulnerability + url: https://github.com/phenixrizen/zen/security/advisories/new + about: Report privately through a security advisory. Please do not open a public issue. + - name: JDM standard and documentation + url: https://docs.gorules.io + about: The JDM standard is GoRules'. Their documentation is the reference. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..dfdd3d08 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,32 @@ +name: Feature request +description: Suggest a capability or improvement +labels: ["enhancement"] +body: + - type: textarea + id: problem + attributes: + label: What problem are you trying to solve? + description: Describe the situation, not the solution. What can you not express today? + validations: + required: true + - type: textarea + id: workaround + attributes: + label: What are you doing instead right now? + description: If there is a workaround, showing it helps size the gap. + validations: + required: false + - type: textarea + id: proposal + attributes: + label: What would you like to see? + description: If you have a shape in mind, sketch it. A JDM snippet or API signature is ideal. + validations: + required: false + - type: checkboxes + id: scope + attributes: + label: Scope + options: + - label: This is a change to the JDM standard itself (GoRules owns the standard; this fork tries to stay compatible with it) + required: false diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000..87583bf0 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,40 @@ +# Security Policy + +## Reporting a vulnerability + +**Do not open a public issue for a security problem.** + +Report it privately through GitHub Security Advisories: +[Report a vulnerability](https://github.com/phenixrizen/zen/security/advisories/new). + +Please include what an attacker can achieve, a minimal reproduction (a JDM graph and input if the +issue is in evaluation), and the affected version or commit. + +## What to expect + +This fork is maintained by one person, so response times are best-effort rather than contractual: + +| stage | realistic timeframe | +| --- | --- | +| Acknowledgement | within a week | +| Initial assessment | within two weeks | +| Fix or mitigation plan | depends on severity and complexity | + +If you have not heard back in two weeks, please ping the advisory thread. + +## Scope + +In scope: the Rust crates in this repository — evaluation, expression handling, the database node +and its SQLite handler, and the bindings. + +Particularly interested in: anything that gets untrusted graph or expression input to escape its +evaluation boundary, reach the filesystem, or reach a database outside its configured root; and +any way to get a value into SQL statement text rather than a bound parameter. + +Out of scope: vulnerabilities in upstream `gorules/zen` that are not specific to this fork should +also be reported to [upstream](https://github.com/gorules/zen). Denial of service from a graph you +authored yourself is not a vulnerability — evaluation is not sandboxed against its own author. + +## Supported versions + +Only the latest `master` is supported. There are no backported security releases. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..3e66d584 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,28 @@ +## What this changes + + + +## Why + + + +## Testing + + + +- [ ] `cargo fmt --all -- --check` +- [ ] `cargo test --workspace --all-features --exclude zen-ffi --exclude zen-nodejs --exclude zen-python --locked` +- [ ] `cargo test --workspace --exclude zen-ffi --exclude zen-nodejs --exclude zen-python --locked` +- [ ] For a bug fix: the new test fails without the fix + +## Relationship to upstream + + + +- [ ] Fork-specific — builds on this fork's own additions +- [ ] Also affects upstream `gorules/zen` +- [ ] Merges cleanly with current upstream `master` + +## Notes for the reviewer + + diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml index 4ce1e83f..b060a7f7 100644 --- a/.github/workflows/go.yaml +++ b/.github/workflows/go.yaml @@ -97,7 +97,7 @@ jobs: name: Release runs-on: ubuntu-latest environment: release - if: "github.ref_type == 'tag' || github.event_name == 'workflow_dispatch'" + if: "(github.ref_type == 'tag' || github.event_name == 'workflow_dispatch')" needs: - build - copy-common @@ -105,7 +105,7 @@ jobs: - name: Checkout zen-go uses: actions/checkout@v3 with: - repository: gorules/zen-go + repository: phenixrizen/zen-go persist-credentials: false token: ${{ secrets.PAT }} - name: Download all artifacts @@ -146,7 +146,7 @@ jobs: - name: Push changes uses: ad-m/github-push-action@master with: - repository: gorules/zen-go + repository: phenixrizen/zen-go github_token: ${{ secrets.PAT }} branch: ${{ env.BRANCH_NAME }} - name: Create PR diff --git a/.github/workflows/uniffi.yaml b/.github/workflows/uniffi.yaml index 705e1fff..611fb84e 100644 --- a/.github/workflows/uniffi.yaml +++ b/.github/workflows/uniffi.yaml @@ -362,7 +362,7 @@ jobs: echo "Detected version: $VERSION" - name: Pack NuGet package - run: dotnet pack nuget/GoRules.ZenEngine.csproj -c Release -p:Version=$VERSION + run: dotnet pack nuget/PhenixRizen.ZenEngine.csproj -c Release -p:Version=$VERSION - name: Upload NuGet package artifact uses: actions/upload-artifact@v4 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..44e909d5 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,59 @@ +# Code of Conduct + +## Our pledge + +We as members, contributors, and leaders pledge to make participation in our community a +harassment-free experience for everyone, regardless of age, body size, visible or invisible +disability, ethnicity, sex characteristics, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, race, caste, color, religion, +or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, +and healthy community. + +## Our standards + +Examples of behavior that contributes to a positive environment: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes +* Focusing on what is best for the overall community + +Examples of unacceptable behavior: + +* The use of sexualized language or imagery, and sexual attention or advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, without their + explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement responsibilities + +Project maintainers are responsible for clarifying and enforcing these standards and will take +appropriate and fair corrective action in response to any behavior that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is +officially representing the community in public spaces. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project +maintainers through a +[private security advisory](https://github.com/phenixrizen/zen/security/advisories/new) or by +contacting **@phenixrizen** directly on GitHub. + +All complaints will be reviewed and investigated promptly and fairly. Maintainers are obligated to +respect the privacy and security of the reporter of any incident. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available +at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. + +[homepage]: https://www.contributor-covenant.org diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..240c395b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,97 @@ +# Contributing + +Contributions are welcome. This is a maintained fork of +[`gorules/zen`](https://github.com/gorules/zen); upstream states it cannot accept code +contributions, so this fork exists to take them. + +Maintained by Phenix Rizen (Nathan Rockhold). + +## Scope: what belongs here + +| belongs in this fork | send upstream instead | +| --- | --- | +| Fixes upstream has not responded to | Anything you would rather see in the canonical project first | +| The fork's own additions (`databaseNode`, `zen-database-sqlite`, `$params`) | JDM standard/spec changes — GoRules owns the standard | +| Correctness fixes with a regression test | — | + +A bug that reproduces on upstream is worth reporting +[there](https://github.com/gorules/zen/issues) too. Both projects benefit, and this fork tracks +upstream `master`. + +## Development setup + +Requires a stable Rust toolchain. Nothing else — the SQLite handler is pure Rust +([Turso](https://github.com/tursodatabase/turso)), so there is no C compiler or vendored +amalgamation in the build. + +```bash +git clone https://github.com/phenixrizen/zen.git +cd zen +cargo build --workspace +``` + +## The test gate + +Run all four commands before opening a PR. CI runs the same ones. + +```bash +# 1. Formatting. +cargo fmt --all -- --check + +# 2. Full workspace, all features. +cargo test --workspace --all-features \ + --exclude zen-ffi --exclude zen-nodejs --exclude zen-python --locked + +# 3. Again with default features. CI runs both; feature unification means +# they do not exercise the same code. +cargo test --workspace \ + --exclude zen-ffi --exclude zen-nodejs --exclude zen-python --locked + +# 4. WASM still builds. +cargo check -p zen-engine --target wasm32-wasip1-threads --all-features +``` + +### Things that will trip you up + +- **`arbitrary_precision` changes numeric output.** With it off, numbers serialize through `f64`. + Some upstream fixtures (`customer-lifetime-value.json`) encode 28-digit decimal expectations and + can only pass with the feature on, which is why step 2 and step 3 differ. If you touch numeric + serialization, run both. +- **Snapshot tests.** `cargo insta` review if you change engine output. A PR should not carry + unexplained `.snap` changes. +- **Tests that need reference data skip by default.** `zen-database-sqlite`'s differential suites + read `ZEN_CATALOG_DIR` and `ZEN_CORPUS_MANIFEST`. Unset means "no bundle available" and they + skip. Never commit a `.db` fixture — build read-only fixtures at test time from a committed + `.sql`. + +## Commit messages + +[Conventional Commits](https://www.conventionalcommits.org/). Release automation parses them, so +the prefix determines the version bump. + +``` +fix(expression): honour TZ when resolving the local timezone +feat(engine): add database node and handler extension point +test(database-sqlite): require an explicit catalog bundle path +``` + +Explain *why* in the body, not just what. A reviewer can read the diff; they cannot read your +reasoning. + +## Pull requests + +- Branch off `master`. +- One logical change per PR. +- A bug fix needs a regression test, and you should confirm the test **fails without the fix**. + State that you did. +- Do not commit absolute paths, machine-specific defaults, or generated artifacts. + +## Tracking upstream + +```bash +git remote add upstream https://github.com/gorules/zen.git +git fetch upstream +git merge upstream/master +``` + +Keep fork-specific changes additive where practical, so upstream merges stay cheap. diff --git a/Cargo.lock b/Cargo.lock index 76c410c1..b1f701fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1205,9 +1205,9 @@ name = "expression_repl" version = "2.0.1" dependencies = [ "colored", + "phenixrizen-zen-expression", "rustyline", "serde_json", - "zen-expression", ] [[package]] @@ -2765,6 +2765,120 @@ dependencies = [ "serde", ] +[[package]] +name = "phenixrizen-zen-engine" +version = "2.0.1" +dependencies = [ + "ahash 0.8.12", + "anyhow", + "arc-swap", + "async-trait", + "base64", + "criterion", + "downcast-rs", + "fixedbitset", + "flate2", + "http", + "insta", + "json_dotpath", + "jsonschema", + "mimalloc", + "nohash-hasher", + "once_cell", + "petgraph", + "phenixrizen-zen-expression", + "phenixrizen-zen-tmpl", + "phenixrizen-zen-types", + "reqsign", + "reqwest 0.12.28", + "rquickjs", + "rust_decimal", + "self_cell", + "serde", + "serde_json", + "serde_path_to_error", + "sha2 0.10.9", + "strum 0.27.2", + "swc_common", + "swc_ecma_ast", + "swc_ecma_parser", + "swc_ts_fast_strip", + "thiserror 1.0.69", + "tokio", + "toml", + "typed-arena", + "zip", +] + +[[package]] +name = "phenixrizen-zen-expression" +version = "2.0.1" +dependencies = [ + "ahash 0.8.12", + "anyhow", + "bumpalo", + "chrono", + "chrono-tz", + "criterion", + "csv", + "fastrand", + "humantime", + "iana-time-zone", + "nohash-hasher", + "once_cell", + "phenixrizen-zen-macros", + "phenixrizen-zen-types", + "recursive", + "regex", + "regex-lite", + "rust_decimal", + "rust_decimal_macros", + "serde", + "serde_json", + "serde_json5", + "smallvec", + "strsim", + "strum 0.27.2", + "strum_macros 0.27.2", + "thiserror 1.0.69", + "toml", +] + +[[package]] +name = "phenixrizen-zen-macros" +version = "2.0.1" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "phenixrizen-zen-tmpl" +version = "2.0.1" +dependencies = [ + "phenixrizen-zen-expression", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "phenixrizen-zen-types" +version = "2.0.1" +dependencies = [ + "ahash 0.8.12", + "hipstr", + "nohash-hasher", + "rust_decimal", + "rust_decimal_macros", + "serde", + "serde_json", + "smallvec", + "thiserror 1.0.69", +] + [[package]] name = "phf" version = "0.11.3" @@ -5994,90 +6108,11 @@ dependencies = [ name = "zen-benchmark" version = "0.0.16" dependencies = [ + "phenixrizen-zen-engine", + "phenixrizen-zen-expression", "serde", "serde_json", "tokio", - "zen-engine", - "zen-expression", -] - -[[package]] -name = "zen-engine" -version = "2.0.1" -dependencies = [ - "ahash 0.8.12", - "anyhow", - "arc-swap", - "async-trait", - "base64", - "criterion", - "downcast-rs", - "fixedbitset", - "flate2", - "http", - "insta", - "json_dotpath", - "jsonschema", - "mimalloc", - "nohash-hasher", - "once_cell", - "petgraph", - "reqsign", - "reqwest 0.12.28", - "rquickjs", - "rust_decimal", - "self_cell", - "serde", - "serde_json", - "serde_path_to_error", - "sha2 0.10.9", - "strum 0.27.2", - "swc_common", - "swc_ecma_ast", - "swc_ecma_parser", - "swc_ts_fast_strip", - "thiserror 1.0.69", - "tokio", - "toml", - "typed-arena", - "zen-expression", - "zen-tmpl", - "zen-types", - "zip", -] - -[[package]] -name = "zen-expression" -version = "2.0.1" -dependencies = [ - "ahash 0.8.12", - "anyhow", - "bumpalo", - "chrono", - "chrono-tz", - "criterion", - "csv", - "fastrand", - "humantime", - "iana-time-zone", - "nohash-hasher", - "once_cell", - "recursive", - "regex", - "regex-lite", - "rust_decimal", - "rust_decimal_macros", - "serde", - "serde_json", - "serde_json5", - "smallvec", - "strsim", - "strum 0.27.2", - "strum_macros 0.27.2", - "thiserror 1.0.69", - "toml", - "zen-macros", - "zen-types", ] [[package]] @@ -6087,24 +6122,14 @@ dependencies = [ "anyhow", "cbindgen", "libc", + "phenixrizen-zen-engine", + "phenixrizen-zen-expression", + "phenixrizen-zen-tmpl", "serde", "serde_json", "strum 0.27.2", "tokio", "tokio-util", - "zen-engine", - "zen-expression", - "zen-tmpl", -] - -[[package]] -name = "zen-macros" -version = "2.0.1" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.117", ] [[package]] @@ -6118,12 +6143,12 @@ dependencies = [ "napi-build", "napi-derive", "napi-sys", + "phenixrizen-zen-engine", + "phenixrizen-zen-expression", + "phenixrizen-zen-tmpl", "serde", "serde_json", "tokio-util", - "zen-engine", - "zen-expression", - "zen-tmpl", ] [[package]] @@ -6133,6 +6158,9 @@ dependencies = [ "anyhow", "either", "json_dotpath", + "phenixrizen-zen-engine", + "phenixrizen-zen-expression", + "phenixrizen-zen-tmpl", "pyo3", "pyo3-async-runtimes", "pythonize", @@ -6140,34 +6168,6 @@ dependencies = [ "serde", "serde_json", "tokio-util", - "zen-engine", - "zen-expression", - "zen-tmpl", -] - -[[package]] -name = "zen-tmpl" -version = "2.0.1" -dependencies = [ - "serde", - "serde_json", - "thiserror 1.0.69", - "zen-expression", -] - -[[package]] -name = "zen-types" -version = "2.0.1" -dependencies = [ - "ahash 0.8.12", - "hipstr", - "nohash-hasher", - "rust_decimal", - "rust_decimal_macros", - "serde", - "serde_json", - "smallvec", - "thiserror 1.0.69", ] [[package]] @@ -6175,12 +6175,12 @@ name = "zen-uniffi" version = "2.0.2" dependencies = [ "async-trait", + "phenixrizen-zen-engine", + "phenixrizen-zen-expression", "serde", "serde_json", "tokio", "uniffi", - "zen-engine", - "zen-expression", ] [[package]] diff --git a/LICENSE b/LICENSE index ca6d24c6..8d4519a0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,5 @@ Copyright 2023 GoRules.io +Copyright 2026 Phenix Rizen (Nathan Rockhold) 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, diff --git a/README.md b/README.md index 9c0e5d2a..dc84d43e 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,53 @@ +> [!NOTE] +> ## This is a maintained fork +> +> **`phenixrizen/zen` is a community-maintained fork of [`gorules/zen`](https://github.com/gorules/zen), maintained by Phenix Rizen (Nathan Rockhold).** +> +> It exists because upstream states it cannot accept code contributions, and submitted fixes go +> unreviewed in practice — including a straightforward timezone defect fix with a regression test, +> which has sat without a response. +> +> This fork tracks upstream `master` and **does accept contributions**. See +> [CONTRIBUTING.md](CONTRIBUTING.md). +> +> ### What this fork adds +> +> | addition | what it does | +> | --- | --- | +> | `databaseNode` + `DatabaseHandler` | A first-class node for looking reference data up from a decision graph, with a host extension point. Values are always bound, never interpolated. | +> | `zen-database-sqlite` | A **pure-Rust** SQLite handler built on [Turso](https://github.com/tursodatabase/turso) — no C, no vendored amalgamation, no `cc` in your build. | +> | Decision-level `$params` | Static parameters supplied per decision and reachable from switch, expression, and function nodes. | +> | `TZ` is honoured | `local` timezone resolution respects the `TZ` environment variable instead of only `/etc/localtime`. | +> | Exact fractional numbers | Fixes silent truncation of every non-integer value when `arbitrary_precision` is off. | +> +> ### Packages +> +> This fork publishes under its own names, so it never collides with upstream: +> +> | | package | +> | --- | --- | +> | Rust | `phenixrizen-zen-engine` (also `-expression`, `-types`, `-tmpl`, `-macros`) | +> | Node.js | `@phenixrizen/zen-engine` | +> | Python | `phenixrizen-zen-engine` | +> | .NET | `PhenixRizen.ZenEngine` | +> +> The Rust crates keep their original library names, so `use zen_engine::…` is unchanged — only +> the dependency line moves. +> +> Not affiliated with or endorsed by GoRules. MIT licensed, same as upstream, with the original +> copyright retained in [LICENSE](LICENSE). + # ZEN Engine **Business logic humans can read and machines can run.** One copy of your rules: the owner reads it, every system runs it. [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) -[![crates.io](https://img.shields.io/crates/v/zen-engine.svg)](https://crates.io/crates/zen-engine) -[![npm](https://img.shields.io/npm/v/@gorules/zen-engine.svg)](https://www.npmjs.com/package/@gorules/zen-engine) -[![PyPI](https://img.shields.io/pypi/v/zen-engine.svg)](https://pypi.org/project/zen-engine/) - -GoRules ZEN Engine +[![crates.io](https://img.shields.io/crates/v/phenixrizen-zen-engine.svg)](https://crates.io/crates/phenixrizen-zen-engine) +[![npm](https://img.shields.io/npm/v/@phenixrizen/zen-engine.svg)](https://www.npmjs.com/package/@phenixrizen/zen-engine) +[![PyPI](https://img.shields.io/pypi/v/phenixrizen-zen-engine.svg)](https://pypi.org/project/phenixrizen-zen-engine/) ZEN Engine is a cross-platform, open-source Business Rules Engine (BRE) written in **Rust**, with native bindings for **Node.js**, **Python**, **Go**, **Java**, **Kotlin** and **.NET**, plus iOS and Android packages. Decisions evaluate in microseconds, run identically on every platform, and are stored as portable JSON. Loading the JSON is up to you: file system, database or service call. -Try it in the free [Online Editor](https://editor.gorules.io) with a built-in simulator, or embed the open-source React [JDM Editor](https://github.com/gorules/jdm-editor) in your own product. - ## Rules that read like sentences Conditions are written the way the business says them, in the ZEN Expression Language. The developer view is one toggle away, and the two can never drift apart: there is only one source of truth, and this engine runs it. @@ -25,7 +60,7 @@ Model a decision on a visual canvas of decision tables, switches, expressions, f Graphs and documents -To go deeper on the decision model and each node type, see the [JDM documentation](https://gorules.io/docs/rules-engine/json-decision-model) and the [ZEN Expression Language](https://gorules.io/docs/rules-engine/expression-language/) reference. +JDM is the JSON Decision Model: the document format this engine loads and evaluates. A JDM document is either a **graph** (decision tables, switches, expressions, functions and reusable sub-decisions on a canvas) or a **policy** (prose, typed data models and tables). Both compile to the same engine and return the same answers. ## What's new in 2.0 @@ -47,7 +82,7 @@ Version 2.0 is the first stable release of the new engine line: ```toml [dependencies] -zen-engine = "2" +phenixrizen-zen-engine = "2" ``` ```rust @@ -69,11 +104,11 @@ async fn evaluate() { ### Node.js ```bash -npm i @gorules/zen-engine +npm i @phenixrizen/zen-engine ``` ```typescript -import { ZenEngine } from '@gorules/zen-engine'; +import { ZenEngine } from '@phenixrizen/zen-engine'; import fs from 'fs/promises'; const content = await fs.readFile('./jdm_graph.json'); @@ -86,7 +121,7 @@ const result = await decision.evaluate({ input: 15 }); ### Python ```bash -pip install zen-engine +pip install phenixrizen-zen-engine ``` ```python @@ -103,34 +138,13 @@ result = decision.evaluate({"input": 15}) Full guides, including loaders for multi-decision graphs and batch evaluation: -* **Node.js** - [GitHub](https://github.com/gorules/zen/blob/master/bindings/nodejs/README.md) | [Documentation](https://gorules.io/docs/developers/bre/engines/nodejs) | [npmjs](https://www.npmjs.com/package/@gorules/zen-engine) -* **Python** - [GitHub](https://github.com/gorules/zen/blob/master/bindings/python/README.md) | [Documentation](https://gorules.io/docs/developers/bre/engines/python) | [pypi](https://pypi.org/project/zen-engine/) -* **Go** - [GitHub](https://github.com/gorules/zen-go) | [Documentation](https://gorules.io/docs/developers/bre/engines/go) -* **Java / Kotlin** - [GitHub](https://github.com/gorules/zen/blob/master/bindings/uniffi) | [Maven Central](https://mvnrepository.com/artifact/io.gorules/zen-engine) -* **.NET** - [GitHub](https://github.com/gorules/zen/blob/master/bindings/uniffi) | [NuGet](https://www.nuget.org/packages/GoRules.ZenEngine) -* **Rust (Core)** - [GitHub](https://github.com/gorules/zen) | [Documentation](https://gorules.io/docs/developers/bre/engines/rust) | [crates.io](https://crates.io/crates/zen-engine) - -## The GoRules platform - -The engine is open at the core; [GoRules](https://gorules.io) is the platform around it. Managed cloud, self-hosted, or embedded with no network hop. SOC 2 Type II. - -### AI that builds rules, and stays reviewable - -An AI copilot and MCP server that edits rules, runs tests and explains decisions. It never deploys. Releases stay with your reviewers. - -GoRules AI - -### Promote like a release, run like a binary - -A release moves from testing to staging to production untouched. Approvals, instant rollback, and a paper trail for every change. +* **Node.js** — [source](bindings/nodejs/README.md) | [npm](https://www.npmjs.com/package/@phenixrizen/zen-engine) +* **Python** — [source](bindings/python/README.md) | [PyPI](https://pypi.org/project/phenixrizen-zen-engine/) +* **Go** — [phenixrizen/zen-go](https://github.com/phenixrizen/zen-go) +* **Java / Kotlin** — [source](bindings/uniffi) +* **.NET** — [source](bindings/uniffi) | [NuGet](https://www.nuget.org/packages/PhenixRizen.ZenEngine) +* **Rust (core)** — [source](core/engine) | [crates.io](https://crates.io/crates/phenixrizen-zen-engine) -Governance - -### Prove it before it ships - -Scenario suites run on every change, coverage is measured against decision paths, and every answer comes with a replayable trace. - -Testing ## Support matrix @@ -150,7 +164,24 @@ Mobile: **Swift (iOS XCFramework)** and **Android (AAR)** packages are published ## Contribution -The JDM standard is growing and we need to keep tight control over its development and roadmap, as a number of companies use GoRules ZEN Engine and GoRules BRMS. For this reason we can't accept code contributions at this moment, apart from help with documentation and additional tests. +**Contributions are welcome here.** This fork exists partly because upstream cannot take them. + +Read [CONTRIBUTING.md](CONTRIBUTING.md) for the development setup and the full test gate. In short: + +```bash +cargo fmt --all -- --check +cargo test --workspace --all-features \ + --exclude zen-ffi --exclude zen-nodejs --exclude zen-python --locked +cargo test --workspace \ + --exclude zen-ffi --exclude zen-nodejs --exclude zen-python --locked +``` + +Security issues should not go in a public issue — see [SECURITY.md](.github/SECURITY.md). + +A bug that also reproduces on upstream `gorules/zen` is worth reporting +[there](https://github.com/gorules/zen/issues) as well, so both projects benefit. + +The JDM standard itself is GoRules'. This fork aims to stay compatible with it rather than diverge. ## License diff --git a/benchmark/rust/Cargo.toml b/benchmark/rust/Cargo.toml index c92fc6b1..dd09db79 100644 --- a/benchmark/rust/Cargo.toml +++ b/benchmark/rust/Cargo.toml @@ -5,8 +5,8 @@ edition = "2021" publish = false [dependencies] -zen-engine = { path = "../../core/engine" } -zen-expression = { path = "../../core/expression" } +zen-engine = { package = "phenixrizen-zen-engine", path = "../../core/engine" } +zen-expression = { package = "phenixrizen-zen-expression", path = "../../core/expression" } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } diff --git a/bindings/c/Cargo.toml b/bindings/c/Cargo.toml index 994f25e0..a46a03c7 100644 --- a/bindings/c/Cargo.toml +++ b/bindings/c/Cargo.toml @@ -13,9 +13,9 @@ serde_json = { workspace = true } strum = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["rt"] } tokio-util = { workspace = true, features = ["rt"] } -zen-engine = { path = "../../core/engine", features = ["arbitrary_precision"] } -zen-expression = { path = "../../core/expression", features = ["arbitrary_precision"] } -zen-tmpl = { path = "../../core/template" } +zen-engine = { package = "phenixrizen-zen-engine", path = "../../core/engine", features = ["arbitrary_precision"] } +zen-expression = { package = "phenixrizen-zen-expression", path = "../../core/expression", features = ["arbitrary_precision"] } +zen-tmpl = { package = "phenixrizen-zen-tmpl", path = "../../core/template" } [lib] crate-type = ["staticlib"] diff --git a/bindings/nodejs/Cargo.toml b/bindings/nodejs/Cargo.toml index fee8c62d..439b340a 100644 --- a/bindings/nodejs/Cargo.toml +++ b/bindings/nodejs/Cargo.toml @@ -15,9 +15,9 @@ napi-sys = "3" napi-derive = "3" tokio-util = { workspace = true, features = ["rt"] } serde_json = { workspace = true } -zen-engine = { path = "../../core/engine", features = ["arbitrary_precision"] } -zen-expression = { path = "../../core/expression", features = ["arbitrary_precision"] } -zen-tmpl = { path = "../../core/template" } +zen-engine = { package = "phenixrizen-zen-engine", path = "../../core/engine", features = ["arbitrary_precision"] } +zen-expression = { package = "phenixrizen-zen-expression", path = "../../core/expression", features = ["arbitrary_precision"] } +zen-tmpl = { package = "phenixrizen-zen-tmpl", path = "../../core/template" } serde = { workspace = true, features = ["derive"] } json_dotpath = { workspace = true } diff --git a/bindings/nodejs/package.json b/bindings/nodejs/package.json index eec287f9..2f27ba6c 100644 --- a/bindings/nodejs/package.json +++ b/bindings/nodejs/package.json @@ -1,5 +1,5 @@ { - "name": "@gorules/zen-engine", + "name": "@phenixrizen/zen-engine", "description": "Open-source Business Rules Engine for Node.js, powered by Rust", "version": "2.0.2", "main": "index.js", @@ -56,8 +56,8 @@ "napi-rs", "node-rs" ], - "author": "GoRules (https://gorules.io)", - "homepage": "https://gorules.io", + "author": "Phenix Rizen ", + "homepage": "https://github.com/phenixrizen/zen", "engines": { "node": ">= 14" }, @@ -79,7 +79,7 @@ "typescript": "^5.9.3" }, "bugs": { - "url": "https://github.com/gorules/zen/issues" + "url": "https://github.com/phenixrizen/zen/issues" }, "publishConfig": { "access": "public", @@ -87,7 +87,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/gorules/zen.git" + "url": "git+https://github.com/phenixrizen/zen.git" }, "scripts": { "build": "napi build --dts temp.d.ts --platform --release", diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml index a289e098..54d835dc 100644 --- a/bindings/python/Cargo.toml +++ b/bindings/python/Cargo.toml @@ -20,6 +20,6 @@ serde = { workspace = true } serde_json = { workspace = true } rust_decimal = { workspace = true, features = ["maths-nopanic"] } tokio-util = { version = "0.7", features = ["rt"] } -zen-engine = { path = "../../core/engine", features = ["arbitrary_precision"] } -zen-expression = { path = "../../core/expression", features = ["arbitrary_precision"] } -zen-tmpl = { path = "../../core/template" } +zen-engine = { package = "phenixrizen-zen-engine", path = "../../core/engine", features = ["arbitrary_precision"] } +zen-expression = { package = "phenixrizen-zen-expression", path = "../../core/expression", features = ["arbitrary_precision"] } +zen-tmpl = { package = "phenixrizen-zen-tmpl", path = "../../core/template" } diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index bce65bd5..1d71fdf8 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -3,7 +3,7 @@ requires = ["maturin>=1,<2"] build-backend = "maturin" [project] -name = "zen-engine" +name = "phenixrizen-zen-engine" requires-python = ">=3.7" classifiers = [ "Programming Language :: Rust", @@ -11,9 +11,9 @@ classifiers = [ "Programming Language :: Python :: Implementation :: PyPy", ] -description = "Open-Source Business Rules Engine" +description = "Open-Source Business Rules Engine (maintained fork of gorules/zen)" readme = "README.md" -authors = [{ name = "GoRules Team", email = "hi@gorules.io" }] +authors = [{ name = "Phenix Rizen", email = "phenixrizen@gmail.com" }] license = { file = "LICENSE" } keywords = ["gorules", @@ -37,7 +37,7 @@ dev = ["black", "bumpver", "isort", "pip-tools", "pytest", "asyncio"] [project.urls] Homepage = "https://gorules.io" Documentation = "https://docs.gorules.io/developers/sdks/python" -Repository = "https://github.com/gorules/zen" +Repository = "https://github.com/phenixrizen/zen" [project.scripts] zenengine = "reader.__main__:main" diff --git a/bindings/uniffi/Cargo.toml b/bindings/uniffi/Cargo.toml index 141bc651..f689b16c 100644 --- a/bindings/uniffi/Cargo.toml +++ b/bindings/uniffi/Cargo.toml @@ -15,8 +15,8 @@ path = "uniffi-bindgen.rs" [dependencies] uniffi = { version = "0.31", features = ["tokio", "cli"] } serde_json = { workspace = true } -zen-engine = { path = "../../core/engine", features = ["arbitrary_precision"] } -zen-expression = { path = "../../core/expression", features = ["arbitrary_precision"] } +zen-engine = { package = "phenixrizen-zen-engine", path = "../../core/engine", features = ["arbitrary_precision"] } +zen-expression = { package = "phenixrizen-zen-expression", path = "../../core/expression", features = ["arbitrary_precision"] } serde = { workspace = true, features = ["derive"] } async-trait = "0.1" tokio = "1.46" diff --git a/bindings/uniffi/nuget/GoRules.ZenEngine.csproj b/bindings/uniffi/nuget/PhenixRizen.ZenEngine.csproj similarity index 90% rename from bindings/uniffi/nuget/GoRules.ZenEngine.csproj rename to bindings/uniffi/nuget/PhenixRizen.ZenEngine.csproj index af1e2e99..0be5b430 100644 --- a/bindings/uniffi/nuget/GoRules.ZenEngine.csproj +++ b/bindings/uniffi/nuget/PhenixRizen.ZenEngine.csproj @@ -2,14 +2,14 @@ net8.0 - GoRules.ZenEngine - GoRules + PhenixRizen.ZenEngine + Phenix Rizen GoRules ZEN Engine - Business Rules Engine for .NET. Execute JSON Decision Models (JDM) with native performance via Rust FFI bindings. MIT - https://gorules.io + https://github.com/phenixrizen/zen icon.png - https://github.com/gorules/zen + https://github.com/phenixrizen/zen rules-engine;business-rules;decision-engine;jdm;gorules enable enable diff --git a/core/engine/Cargo.toml b/core/engine/Cargo.toml index d376012b..5b37829b 100644 --- a/core/engine/Cargo.toml +++ b/core/engine/Cargo.toml @@ -1,14 +1,15 @@ [package] -authors = ["GoRules Team "] -description = "Business rules engine" -name = "zen-engine" +authors = ["Phenix Rizen ", "GoRules Team "] +description = "Business rules engine (maintained fork of gorules/zen)" +name = "phenixrizen-zen-engine" license = "MIT" version = "2.0.1" edition = "2021" -repository = "https://github.com/gorules/zen.git" -homepage = "https://gorules.io" +repository = "https://github.com/phenixrizen/zen.git" +homepage = "https://github.com/phenixrizen/zen" [lib] +name = "zen_engine" doctest = false [dependencies] @@ -27,9 +28,9 @@ rust_decimal = { workspace = true, features = ["maths-nopanic"] } fixedbitset = "0.5" tokio = { workspace = true, features = ["sync", "time"] } rquickjs = { version = "0.10", features = ["macro", "loader", "rust-alloc", "futures", "either", "properties"] } -zen-types = { path = "../types", version = "2.0.1" } -zen-expression = { path = "../expression", version = "2.0.1" } -zen-tmpl = { path = "../template", version = "2.0.1" } +zen-types = { package = "phenixrizen-zen-types", path = "../types", version = "2.0.1" } +zen-expression = { package = "phenixrizen-zen-expression", path = "../expression", version = "2.0.1" } +zen-tmpl = { package = "phenixrizen-zen-tmpl", path = "../template", version = "2.0.1" } nohash-hasher = { workspace = true } downcast-rs = { version = "2.0", features = ["std", "sync"] } arc-swap = "1" diff --git a/core/expression/Cargo.toml b/core/expression/Cargo.toml index f43e1e29..65e23206 100644 --- a/core/expression/Cargo.toml +++ b/core/expression/Cargo.toml @@ -1,12 +1,15 @@ [package] -authors = ["GoRules Team "] -description = "Zen Expression Language" -name = "zen-expression" +authors = ["Phenix Rizen ", "GoRules Team "] +description = "Zen Expression Language (maintained fork of gorules/zen)" +name = "phenixrizen-zen-expression" license = "MIT" version = "2.0.1" edition = "2021" -repository = "https://github.com/gorules/zen.git" -homepage = "https://gorules.io" +repository = "https://github.com/phenixrizen/zen.git" +homepage = "https://github.com/phenixrizen/zen" + +[lib] +name = "zen_expression" [dependencies] smallvec = { version = "1", features = ["union"] } @@ -31,8 +34,8 @@ nohash-hasher = { workspace = true } strsim = "0.11" iana-time-zone = "0.1" -zen-macros = { path = "../macros", version = "2.0.1" } -zen-types = { path = "../types", version = "2.0.1" } +zen-macros = { package = "phenixrizen-zen-macros", path = "../macros", version = "2.0.1" } +zen-types = { package = "phenixrizen-zen-types", path = "../types", version = "2.0.1" } [dev-dependencies] criterion = { workspace = true } diff --git a/core/expression/src/vm/date/mod.rs b/core/expression/src/vm/date/mod.rs index 30246b96..f754299d 100644 --- a/core/expression/src/vm/date/mod.rs +++ b/core/expression/src/vm/date/mod.rs @@ -188,14 +188,29 @@ mod helper { use std::str::FromStr; use std::sync::OnceLock; + /// The local timezone, resolved once per process. + /// + /// `TZ` takes precedence over the system zone, which is how every other date implementation + /// on Unix behaves and is the only way to override the zone in a container that ships the + /// host's `/etc/localtime`. `iana_time_zone` reads that file and ignores the environment, so + /// without this a process started with `TZ=UTC` still computes dates in the host's zone. + /// + /// Resolution is cached, so `TZ` must be set before the first date operation - setting it + /// later in the same process has no effect. fn tz() -> Tz { static CACHED_TZ: OnceLock = OnceLock::new(); *CACHED_TZ.get_or_init(|| { - iana_time_zone::get_timezone() + std::env::var("TZ") .ok() + .filter(|tz| !tz.is_empty()) .and_then(|tz| Tz::from_str(&tz).ok()) - .unwrap_or_else(|| Tz::UTC) + .or_else(|| { + iana_time_zone::get_timezone() + .ok() + .and_then(|tz| Tz::from_str(&tz).ok()) + }) + .unwrap_or(Tz::UTC) }) } diff --git a/core/expression/tests/isolate.rs b/core/expression/tests/isolate.rs index 0412ca00..f1654c9c 100644 --- a/core/expression/tests/isolate.rs +++ b/core/expression/tests/isolate.rs @@ -917,12 +917,19 @@ mod test { #[test] fn test_standard_csv() { + // standard.csv contains date expressions, and the local timezone is resolved once per + // process. Whichever date-touching test runs first seeds that cache, so every one of + // them must pin the zone or the others inherit the host's. + env::set_var("TZ", "UTC"); + let csv_data = include_str!("data/standard.csv"); test_csv_standard(csv_data); } #[test] fn test_dates_csv() { + // See test_standard_csv: the zone is cached process-wide on first use, so this must be + // set before any date expression is evaluated, in this test or a parallel one. env::set_var("TZ", "UTC"); let csv_data = include_str!("data/date.csv"); diff --git a/core/expression_repl/Cargo.toml b/core/expression_repl/Cargo.toml index 0943beb9..02a8825a 100644 --- a/core/expression_repl/Cargo.toml +++ b/core/expression_repl/Cargo.toml @@ -10,5 +10,5 @@ publish = false colored = "3" rustyline = "15" serde_json = { workspace = true } -zen-expression = { path = "../expression", version = "2.0.1" } +zen-expression = { package = "phenixrizen-zen-expression", path = "../expression", version = "2.0.1" } diff --git a/core/macros/Cargo.toml b/core/macros/Cargo.toml index eece5098..d427cc39 100644 --- a/core/macros/Cargo.toml +++ b/core/macros/Cargo.toml @@ -1,13 +1,14 @@ [package] -name = "zen-macros" -description = "Zen Helper Macros" +name = "phenixrizen-zen-macros" +description = "Zen Helper Macros (maintained fork of gorules/zen)" version = "2.0.1" edition = "2024" license = "MIT" -repository = "https://github.com/gorules/zen.git" -homepage = "https://gorules.io" +repository = "https://github.com/phenixrizen/zen.git" +homepage = "https://github.com/phenixrizen/zen" [lib] +name = "zen_macros" proc-macro = true [dependencies] diff --git a/core/template/Cargo.toml b/core/template/Cargo.toml index 0d92fbac..7040c98a 100644 --- a/core/template/Cargo.toml +++ b/core/template/Cargo.toml @@ -1,15 +1,18 @@ [package] -authors = ["GoRules Team "] -description = "Zen Template Language" -name = "zen-tmpl" +authors = ["Phenix Rizen ", "GoRules Team "] +description = "Zen Template Language (maintained fork of gorules/zen)" +name = "phenixrizen-zen-tmpl" license = "MIT" version = "2.0.1" edition = "2021" -repository = "https://github.com/gorules/zen.git" -homepage = "https://gorules.io" +repository = "https://github.com/phenixrizen/zen.git" +homepage = "https://github.com/phenixrizen/zen" + +[lib] +name = "zen_tmpl" [dependencies] -zen-expression = { path = "../expression", version = "2.0.1" } +zen-expression = { package = "phenixrizen-zen-expression", path = "../expression", version = "2.0.1" } thiserror = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/core/types/Cargo.toml b/core/types/Cargo.toml index 25750e10..c1ab0f9d 100644 --- a/core/types/Cargo.toml +++ b/core/types/Cargo.toml @@ -1,11 +1,14 @@ [package] -name = "zen-types" -description = "Zen Core Types" +name = "phenixrizen-zen-types" +description = "Zen Core Types (maintained fork of gorules/zen)" version = "2.0.1" edition = "2024" license = "MIT" -repository = "https://github.com/gorules/zen.git" -homepage = "https://gorules.io" +repository = "https://github.com/phenixrizen/zen.git" +homepage = "https://github.com/phenixrizen/zen" + +[lib] +name = "zen_types" [dependencies] ahash = { workspace = true }