diff --git a/docs/superpowers/specs/2026-04-10-rules-engine-usertask-forms-design.md b/docs/superpowers/specs/2026-04-10-rules-engine-usertask-forms-design.md new file mode 100644 index 0000000..eb1995c --- /dev/null +++ b/docs/superpowers/specs/2026-04-10-rules-engine-usertask-forms-design.md @@ -0,0 +1,231 @@ +# Rules Engine + User-Task Form Rendering — Design Spec + +> Sub-projects B+C of the v1.0 remaining work. +> Covers P3.5 (rules engine) and P2.3 (user-task form rendering). +> P2.2 (BPMN designer) is deferred to v1.1. + +## 1. Context + +With P3.2/P3.3/P3.6/R3 shipped, the framework has metadata-driven forms, a +form designer, and a metadata admin UI. Two pieces remain for v1.0: + +1. **P2.3 — User-task form rendering**: Flowable user-tasks need a SPA page + where operators can see pending tasks and complete them by filling out + metadata-driven forms (using the MetadataFormRenderer we just built). + +2. **P3.5 — Rules engine**: Key users need to create "if event X happens and + condition Y is met, then do action Z" rules through the metadata admin UI, + without writing code. Rules are stored as `metadata__rule` rows. + +## 2. P2.3 — User-Task Form Rendering + +### New backend endpoints + +Add to `WorkflowController`: + +``` +GET /api/v1/workflow/tasks + → List pending user-tasks for the current user (or all if admin) + Returns: List with taskId, taskName, formKey, + processDefinitionKey, processInstanceId, createTime, assignee + +GET /api/v1/workflow/tasks/{taskId} + → Get task details + variables + Returns: UserTaskDetail with taskId, taskName, formKey, variables, + processDefinitionKey, processInstanceId, createTime + +POST /api/v1/workflow/tasks/{taskId}/complete + → Complete the task with form values + Request body: Map (the submitted form data) + Returns: 204 No Content +``` + +These use Flowable's `TaskService.createTaskQuery()` and +`TaskService.complete(taskId, variables)`. + +The `formKey` on the user-task (set in BPMN XML as `flowable:formKey="vibe:plate-approval-task"`) +maps to a form definition slug in `metadata__form`. The SPA strips the `vibe:` +prefix and fetches the form via the existing +`GET /api/v1/_meta/metadata/forms/{slug}` endpoint. + +### New SPA page: UserTasksPage + +Route: `/workflow/tasks` + +- Lists pending user-tasks in a DataTable (task name, process, created time, assignee) +- Click a task → expands inline or navigates to `/workflow/tasks/{taskId}` +- TaskDetailPage renders the form using `` + with `initialValues` from the task variables +- Submit button calls `POST /api/v1/workflow/tasks/{taskId}/complete` +- On success, navigates back to the task list + +### SPA route: TaskDetailPage + +Route: `/workflow/tasks/:taskId` + +- Fetches task detail (variables + formKey) +- Strips `vibe:` prefix from formKey → slug +- Renders `` +- If no formKey, shows a simple "Complete" button with no form + +## 3. P3.5 — Rules Engine + +### Rule definition schema (stored in `metadata__rule.payload`) + +```typescript +interface RuleDefinition { + slug: string // unique identifier + name: string // display name + description?: string + enabled: boolean // toggle on/off without deleting + triggerEvent: string // event class simple name: "SalesOrderConfirmedEvent" + conditions: Array<{ + field: string // event property path: "totalAmount" + operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'in' + value: string // compared value (coerced to field type at eval time) + }> + conditionLogic: 'AND' | 'OR' // how to combine multiple conditions + actions: Array<{ + type: 'log' | 'set-field' | 'publish-event' + config: Record + }> + version: number +} +``` + +### Built-in action types (v1.0) + +| Action type | What it does | Config | +|---|---|---| +| `log` | Writes an INFO log line with a template message | `{ message: "Order {orderCode} exceeded threshold" }` | +| `set-field` | Sets a field on the event's aggregate via a REST call or ext update | `{ entity: "SalesOrder", field: "ext.priority", value: "HIGH" }` | +| `publish-event` | Publishes a synthetic event to the event bus | `{ eventType: "RuleTriggeredEvent", data: "{...}" }` | + +v1.0 keeps actions simple. Complex actions (send email, call webhook, create entity) are v1.1. + +### Backend: RuleEngine service + +New class in `platform-metadata` (or a new `platform-rules` module — but YAGNI, +put it in `platform-metadata` since rules are metadata): + +```kotlin +@Component +class RuleEngine( + private val eventBus: EventBus, + private val jdbc: NamedParameterJdbcTemplate, + private val objectMapper: ObjectMapper, +) { + @PostConstruct + fun loadAndSubscribe() { + // 1. Read all enabled rules from metadata__rule + // 2. Group by triggerEvent + // 3. Subscribe to each distinct event type via eventBus.subscribe(topic, listener) + // using the aggregateType as the topic string + // 4. When an event fires, evaluate conditions against event properties + // 5. If conditions match, execute actions + } + + fun refresh() { + // Unsubscribe all, reload, resubscribe + // Called after rule CRUD operations + } +} +``` + +### Condition evaluation + +Events are domain objects with typed properties. The rule engine serializes the +event to a `Map` via Jackson, then evaluates each condition: + +```kotlin +fun evaluateCondition(eventMap: Map, condition: Condition): Boolean { + val actual = eventMap[condition.field] ?: return false + val expected = condition.value + return when (condition.operator) { + "eq" -> actual.toString() == expected + "neq" -> actual.toString() != expected + "gt" -> (actual as? Number)?.toDouble()?.let { it > expected.toDouble() } ?: false + "gte" -> (actual as? Number)?.toDouble()?.let { it >= expected.toDouble() } ?: false + "lt" -> (actual as? Number)?.toDouble()?.let { it < expected.toDouble() } ?: false + "lte" -> (actual as? Number)?.toDouble()?.let { it <= expected.toDouble() } ?: false + "contains" -> actual.toString().contains(expected) + "in" -> expected.split(",").map { it.trim() }.contains(actual.toString()) + else -> false + } +} +``` + +### Backend endpoints (in MetadataController or new RuleController) + +``` +GET /api/v1/_meta/metadata/rules → List +GET /api/v1/_meta/metadata/rules/{slug} → RuleDefinition +PUT /api/v1/_meta/metadata/rules/{slug} → RuleDefinition (upsert, source='user') +DELETE /api/v1/_meta/metadata/rules/{slug} → 204 +``` + +Same source='user' enforcement as forms/list-views. +After every write, call `ruleEngine.refresh()`. + +### SPA: Rules tab in MetadataAdmin + +Add a 7th tab "Rules" to MetadataAdminPage: + +- DataTable showing: Name, Trigger Event, Enabled (toggle), Conditions (count), Actions (count), Source badge +- "New Rule" button opens an inline form: + - Name, description + - Trigger event dropdown (populated from known event types) + - Conditions builder: field, operator, value (add/remove rows) + - Condition logic toggle (AND/OR) + - Actions builder: type dropdown, config key-value inputs (add/remove rows) + - Enabled checkbox + - Save/Cancel +- Edit button per source='user' row +- Delete button per source='user' row + +### MetadataYaml extension + +Add `rules: List` to `MetadataYamlFile` so core PBCs and plug-ins +can ship default rules in their metadata YAML. Same loader/wipe pattern. + +## 4. Scope Decisions + +| Decision | Choice | +|---|---| +| BPMN designer (P2.2) | Deferred to v1.1 — customers use Camunda Modeler or hand-edit BPMN XML | +| Rule action types | v1.0: log, set-field, publish-event. v1.1: email, webhook, create entity | +| Rule condition complexity | v1.0: flat field conditions with AND/OR. v1.1: nested groups, expressions | +| User-task assignment | v1.0: all tasks visible to all authenticated users. v1.1: role-based assignment | + +## 5. File Inventory + +### New files (Backend) +- `platform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/rules/RuleEngine.kt` +- `platform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/rules/RuleEvaluator.kt` +- `platform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/web/RuleController.kt` +- `platform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/yaml/RuleYaml.kt` +- `platform/platform-metadata/src/test/kotlin/org/vibeerp/platform/metadata/rules/RuleEvaluatorTest.kt` + +### New files (Frontend) +- `web/src/pages/UserTasksPage.tsx` +- `web/src/pages/TaskDetailPage.tsx` + +### Modified files +- `platform/platform-workflow/src/main/kotlin/org/vibeerp/platform/workflow/http/WorkflowController.kt` — add task endpoints +- `platform/platform-workflow/src/main/kotlin/org/vibeerp/platform/workflow/WorkflowService.kt` — add task methods +- `platform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/yaml/MetadataYaml.kt` — add RuleYaml +- `platform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/MetadataLoader.kt` — process rules +- `web/src/pages/MetadataAdminPage.tsx` — add Rules tab +- `web/src/api/client.ts` — add workflow task + rule API functions +- `web/src/types/api.ts` — add UserTask + Rule types +- `web/src/i18n/messages.ts` — add keys +- `web/src/App.tsx` — add routes +- `web/src/layout/AppLayout.tsx` — add nav entry +- `platform/platform-bootstrap/src/main/kotlin/org/vibeerp/platform/bootstrap/web/SpaController.kt` — add workflow routes + +## 6. Testing + +- `RuleEvaluatorTest.kt` — unit tests for condition evaluation (eq, gt, lt, contains, in, AND/OR logic) +- `RuleEngine` — integration: load rules from metadata, verify event subscription and action execution +- Manual smoke: create a rule via UI, trigger the event, verify action fires +- Manual smoke: start a BPMN process with a user-task, see it in the tasks page, complete it with the form