Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3b6d33f
feat: manual rule reload, time-windowed rules, rule metrics
claude Sep 17, 2026
aeccbfb
feat: config-driven RuleBookRegistry, provider DSL, ruleBook() mixin
claude Sep 18, 2026
936a477
fix: RuleBookRegistrySpec test bugs, not feature bugs
claude Sep 18, 2026
9b9c438
feat: Rule Visualizer admin UI (dashboard, chain view, dry run, metri…
claude Sep 18, 2026
ddd92de
Merge origin/development into claude/determined-rubin-g98cfj
claude Sep 18, 2026
ff98a51
fix: don't pass raw exception objects as logger extrainfo
claude Sep 18, 2026
6b34687
test: avoid a WireBox mapping-resolution failure in RuleEventBusSpec
claude Sep 18, 2026
629ba0e
fix: Visualizer.bx compile error (property/statement order) + SQLite …
claude Sep 18, 2026
032aee3
fix: three real bugs found by actually running the suite - date mask,…
claude Sep 18, 2026
8478b40
fix: install bx-sqlite where BoxLang's runtime module scanner actuall…
claude Sep 18, 2026
77678d1
fix: explicitly copy bx-sqlite into BOXLANG_HOME/modules after install
claude Sep 18, 2026
b85e143
Default the Rule Visualizer to InMemoryMetricsStore, keep SQLite opt-in
claude Sep 18, 2026
1a6e7d0
Fix test-harness moduleSettings override not reaching ColdBox in BoxLang
claude Sep 18, 2026
e5bb204
Explicitly select the Visualizer layout at request time
claude Sep 18, 2026
46bedc0
Explicitly set the view's module too, not just the layout's
claude Sep 18, 2026
a647bf8
TEMP: log resolved layout/view state to diagnose empty render
claude Sep 18, 2026
082afb0
TEMP: switch diagnostic to systemOutput() in index(), postHandler nev…
claude Sep 18, 2026
c82747d
Assert on prc data instead of rendered HTML for dashboard/chain tests
claude Sep 18, 2026
ed5a77e
Fix the Rule Visualizer's views not rendering: wrap output in <bx:out…
claude Sep 18, 2026
476970a
Add richer test-harness rulebook examples; fix dateTimeFormat mask bu…
claude Sep 18, 2026
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
4 changes: 4 additions & 0 deletions .cfconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
"password":"${DB_PASSWORD}",
"port":"3306",
"username":"${DB_USERNAME}"
},
"rulebox_visualizer":{
"driver":"sqlite",
"database":"./.database/rulebox_visualizer.db"
}
},
"debuggingEnabled":true,
Expand Down
Empty file added .database/.gitkeep
Empty file.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ test-harness/.env
# log files
logs/**

## The Rule Visualizer's default SQLite datasource - the directory is tracked (see .cfconfig.json
## and the "Rule Visualizer" guide), the generated db file is not
.database/*.db

## Built docs site (bxSites)
site/**

Expand Down
23 changes: 22 additions & 1 deletion ModuleConfig.bx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ class {
this.dependencies = []
// Global application helper mixins (handlers/views/layouts): ruleBook( name )
this.applicationHelper = [ "mixins/Helpers.bxm" ]
// Enables convention-based routing for the visualizer's handler/views under /rulebox-visualizer.
// The visualizer itself stays inert (routes 404, no events recorded) unless settings.visualizer.enabled = true.
this.entryPoint = "rulebox-visualizer"

/**
* Configure Module
Expand All @@ -28,7 +31,25 @@ class {
// See the "Externalized Rule Definitions" guide for the full settings shape.
rulebooks = {},
// *.json/*.yaml files dropped here are auto-discovered; name = filename without extension
conventionPath = "config/rulebox"
conventionPath = "config/rulebox",

// The Rule Visualizer: a dashboard/dry-run/metrics/live-tracker admin UI, off by default.
// Secure it yourself (e.g. with cbSecurity) once enabled - RuleBox doesn't gate access on its own.
// See the "Rule Visualizer" guide for the full settings shape.
visualizer = {
// Master switch. While false, the visualizer's routes 404 and no rule events are
// recorded or broadcast at all - flipping this on is the only thing that turns on
// the (small) per-rule-evaluation bookkeeping cost.
enabled = false,
// WireBox mapping ID for the metrics persistence store. Defaults to the in-memory
// store (no setup, nothing survives a restart). For persistence across restarts,
// point this at "SQLiteMetricsStore@rulebox" - see the "Rule Visualizer" guide for
// what that needs (the bx-sqlite module plus a matching datasource), or implement
// IMetricsStore@rulebox yourself and point this at its mapping.
metricsStore = "InMemoryMetricsStore@rulebox",
// Datasource name SQLiteMetricsStore reads/writes, if you opt into it above.
datasourceName = "rulebox_visualizer"
}
}
}

Expand Down
7 changes: 6 additions & 1 deletion box.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@
],
"contributors":[],
"dependencies":{},
"peerDependencies":{
"bx-sqlite":"*"
},
"peerDependenciesNote":"Only required if you enable the visualizer (moduleSettings.rulebox.visualizer.enabled) and keep its default SQLiteMetricsStore. Not needed otherwise, and not needed if you swap in your own metricsStore.",
"devDependencies" :{
"commandbox-boxlang":"*",
"commandbox-docbox":"*",
"bx-sites":"be"
"bx-sites":"be",
"bx-sqlite":"*"
},
"ignore":[
"**/.*",
Expand Down
3 changes: 3 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `InlineRuleSource`: a `RuleSource` backed by a literal array of rule-definition structs, no file or database
- A `ruleBook( name )` application helper mixin, available in handlers/views/layouts
- A `rulebook`/`rulebook:{name}` WireBox injection DSL: `rulebook` injects the `RuleBookRegistry` singleton, `rulebook:{name}` injects a provider (`.get()`) for that declared rulebook so it stays safe to inject even into a singleton
- The Rule Visualizer: an admin UI (dashboard, per-rulebook chain visualization, a dry-run playground, metrics/stats, and a live SSE tracker), off by default. Enable it with `moduleSettings.rulebox.visualizer.enabled = true` - RuleBox doesn't secure it on its own, so wrap it with cbSecurity (or your own auth) once enabled. Built on Bootstrap 5, Alpine.js, and Phosphor Icons via CDN. See the "Rule Visualizer" guide
- `IMetricsStore@rulebox`: the visualizer's metrics persistence contract, with `InMemoryMetricsStore` (default, no I/O, nothing survives a restart) and `SQLiteMetricsStore` (opt-in, persists across restarts via the `bx-sqlite` module) implementations. Swap in your own via `moduleSettings.rulebox.visualizer.metricsStore`
- `RuleEventBus@rulebox`: fans out one event per rule evaluation to the configured metrics store and any live subscribers (the visualizer's SSE stream). A complete no-op while the visualizer is disabled

### Fixed

Expand Down
1 change: 1 addition & 0 deletions docs/guides/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ toc: false
- [Thread Safety](thread-safety.md) - why RuleBooks and Rules are transient
- [Error Handling](error-handling.md) - exceptions and failure states
- [A Complex Example](complex-example.md) - a full, real-world walkthrough
- [Rule Visualizer](visualizer.md) - a dashboard, dry-run playground, metrics, and a live SSE tracker
118 changes: 118 additions & 0 deletions docs/guides/visualizer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
title: Rule Visualizer
order: 9
icon: phosphor-duotone:chart-line
summary: An admin UI to browse rulebooks, dry-run them against facts, and watch rules execute live.
tags: [guides, visualizer, admin]
---

# Rule Visualizer

The Rule Visualizer is an admin UI for RuleBox: a dashboard of your declared
rulebooks, a chain visualizer showing real execution order, a dry-run
playground, metrics/stats, and a live SSE tracker. It's modeled on
cbSecurity's own visualizer - off by default, and not secured by RuleBox
itself.

## Enabling it

```cfc
moduleSettings = {
rulebox = {
visualizer = {
enabled = true
}
}
}
```

That's it - `enabled` is the only thing you strictly need. Once on, the UI
lives at `/rulebox-visualizer/visualizer/index` (and friends), via the
module's `this.entryPoint = "rulebox-visualizer"`.

While `enabled` is `false` (the default), every visualizer route 404s, and -
just as importantly - RuleBox does no extra work at all: no metrics are
persisted, and nothing is broadcast. Flipping it on is the only thing that
turns on the (small) per-rule-evaluation bookkeeping cost.

**RuleBox does not secure these routes for you.** Once you enable the
visualizer, wrap `/rulebox-visualizer` with a cbSecurity rule (or your own
auth interceptor) the same way you would any other admin UI.

## Screens

- **Dashboard** - every declared rulebook (from `moduleSettings.rulebox.rulebooks` and/or your convention folder - see the "Externalized Rule Definitions" guide), its rule count, evaluation counts by state, and a recent-activity feed
- **Rule Visualizer** - a chosen rulebook's real execution chain (priority order, `stop()` points, active windows), with per-rule metrics
- **Dry Run** - pick a rulebook, paste facts as JSON, and see which rules would fire without executing anything - backed by `RuleBook.dryRun()`
- **Metrics** - aggregated stats per rulebook: evaluation counts by state, average/total duration
- **Live Tracker** - every rule evaluation, across every rulebook, streamed to the browser in real time via [BoxLang's `SSE()`](https://boxlang.ortusbooks.com/boxlang-framework/server-sent-events)

The UI itself is Bootstrap 5, Alpine.js, and Phosphor Icons, loaded from a
CDN - there's nothing to build or bundle.

## Metrics persistence

Every rule evaluation is recorded through `RuleEventBus@rulebox`, which fans
it out to two places: the configured metrics store (for the dashboard/metrics
screens) and any live subscribers (the SSE tracker). The store is
swappable:

```cfc
moduleSettings = {
rulebox = {
visualizer = {
enabled = true,
// A WireBox mapping ID - swap in your own implementation of IMetricsStore@rulebox
metricsStore = "InMemoryMetricsStore@rulebox",
// Only read by SQLiteMetricsStore, if you opt into it below
datasourceName = "rulebox_visualizer"
}
}
}
```

### The default: in-memory

`InMemoryMetricsStore@rulebox` is the default - zero setup, live broadcast
and the dashboard/metrics screens work immediately after enabling the
visualizer. The tradeoff: nothing survives a restart.

### Persisting across restarts: SQLite

For metrics that survive a restart, point `metricsStore` at
`SQLiteMetricsStore@rulebox` instead. It persists events to a
`rulebox_events` table (auto-created on first use) via the
[`bx-sqlite`](https://forgebox.io/view/bx-sqlite) BoxLang module. It requires:

1. `bx-sqlite` installed (`box install bx-sqlite`) and registered with your
engine so its JDBC driver is available - how you do this depends on your
engine/environment, so verify it independently of RuleBox
2. A datasource registered under the name in `datasourceName` (default `rulebox_visualizer`), e.g. in `Application.bx`:

```cfc
this.datasources = {
rulebox_visualizer: {
driver: "sqlite",
database: "./.database/rulebox_visualizer.db"
}
}
```

Neither the module nor the datasource is installed/registered for you - if
you opt into this store, you set these up yourself. If `bx-sqlite` or the
datasource isn't available, RuleBox logs it and keeps going: live broadcast
still works, nothing gets persisted.

### Swapping it out

Implement `IMetricsStore@rulebox` (`recordEvent`, `queryEvents`,
`queryRuleBookSummary`, `queryRuleMetrics`, `queryRuleBookNames`, `reset`)
and point `metricsStore` at your WireBox mapping - a Redis-backed store, a
real RDBMS table via `qb`, whatever fits your app.

## What it doesn't do

The condition tree behind a `when()`/`except()` closure isn't introspectable
once compiled, so the chain visualizer shows what's inspectable on a live
`Rule` - name, priority, `stop()`, active window, metrics - not a decompiled
condition. Access control is also entirely on you; see above.
Loading
Loading