Commit 0e14f616e336b37360b51c29e5de69b8abec2245

Authored by zichun
1 parent a989f134

docs(spec): rules engine (P3.5) + user-task forms (P2.3) design

P3.5: event-condition-action rules stored as metadata rows, evaluated
when events fire. v1.0 actions: log, set-field, publish-event.
P2.3: SPA pages for listing and completing Flowable user-tasks
using MetadataFormRenderer. P2.2 BPMN designer deferred to v1.1.
docs/superpowers/specs/2026-04-10-rules-engine-usertask-forms-design.md 0 → 100644
  1 +# Rules Engine + User-Task Form Rendering — Design Spec
  2 +
  3 +> Sub-projects B+C of the v1.0 remaining work.
  4 +> Covers P3.5 (rules engine) and P2.3 (user-task form rendering).
  5 +> P2.2 (BPMN designer) is deferred to v1.1.
  6 +
  7 +## 1. Context
  8 +
  9 +With P3.2/P3.3/P3.6/R3 shipped, the framework has metadata-driven forms, a
  10 +form designer, and a metadata admin UI. Two pieces remain for v1.0:
  11 +
  12 +1. **P2.3 — User-task form rendering**: Flowable user-tasks need a SPA page
  13 + where operators can see pending tasks and complete them by filling out
  14 + metadata-driven forms (using the MetadataFormRenderer we just built).
  15 +
  16 +2. **P3.5 — Rules engine**: Key users need to create "if event X happens and
  17 + condition Y is met, then do action Z" rules through the metadata admin UI,
  18 + without writing code. Rules are stored as `metadata__rule` rows.
  19 +
  20 +## 2. P2.3 — User-Task Form Rendering
  21 +
  22 +### New backend endpoints
  23 +
  24 +Add to `WorkflowController`:
  25 +
  26 +```
  27 +GET /api/v1/workflow/tasks
  28 + → List pending user-tasks for the current user (or all if admin)
  29 + Returns: List<UserTaskSummary> with taskId, taskName, formKey,
  30 + processDefinitionKey, processInstanceId, createTime, assignee
  31 +
  32 +GET /api/v1/workflow/tasks/{taskId}
  33 + → Get task details + variables
  34 + Returns: UserTaskDetail with taskId, taskName, formKey, variables,
  35 + processDefinitionKey, processInstanceId, createTime
  36 +
  37 +POST /api/v1/workflow/tasks/{taskId}/complete
  38 + → Complete the task with form values
  39 + Request body: Map<String, Any?> (the submitted form data)
  40 + Returns: 204 No Content
  41 +```
  42 +
  43 +These use Flowable's `TaskService.createTaskQuery()` and
  44 +`TaskService.complete(taskId, variables)`.
  45 +
  46 +The `formKey` on the user-task (set in BPMN XML as `flowable:formKey="vibe:plate-approval-task"`)
  47 +maps to a form definition slug in `metadata__form`. The SPA strips the `vibe:`
  48 +prefix and fetches the form via the existing
  49 +`GET /api/v1/_meta/metadata/forms/{slug}` endpoint.
  50 +
  51 +### New SPA page: UserTasksPage
  52 +
  53 +Route: `/workflow/tasks`
  54 +
  55 +- Lists pending user-tasks in a DataTable (task name, process, created time, assignee)
  56 +- Click a task → expands inline or navigates to `/workflow/tasks/{taskId}`
  57 +- TaskDetailPage renders the form using `<MetadataFormRenderer slug={formKey} />`
  58 + with `initialValues` from the task variables
  59 +- Submit button calls `POST /api/v1/workflow/tasks/{taskId}/complete`
  60 +- On success, navigates back to the task list
  61 +
  62 +### SPA route: TaskDetailPage
  63 +
  64 +Route: `/workflow/tasks/:taskId`
  65 +
  66 +- Fetches task detail (variables + formKey)
  67 +- Strips `vibe:` prefix from formKey → slug
  68 +- Renders `<MetadataFormRenderer slug={slug} initialValues={variables} onSubmit={completeTask} />`
  69 +- If no formKey, shows a simple "Complete" button with no form
  70 +
  71 +## 3. P3.5 — Rules Engine
  72 +
  73 +### Rule definition schema (stored in `metadata__rule.payload`)
  74 +
  75 +```typescript
  76 +interface RuleDefinition {
  77 + slug: string // unique identifier
  78 + name: string // display name
  79 + description?: string
  80 + enabled: boolean // toggle on/off without deleting
  81 + triggerEvent: string // event class simple name: "SalesOrderConfirmedEvent"
  82 + conditions: Array<{
  83 + field: string // event property path: "totalAmount"
  84 + operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'in'
  85 + value: string // compared value (coerced to field type at eval time)
  86 + }>
  87 + conditionLogic: 'AND' | 'OR' // how to combine multiple conditions
  88 + actions: Array<{
  89 + type: 'log' | 'set-field' | 'publish-event'
  90 + config: Record<string, string>
  91 + }>
  92 + version: number
  93 +}
  94 +```
  95 +
  96 +### Built-in action types (v1.0)
  97 +
  98 +| Action type | What it does | Config |
  99 +|---|---|---|
  100 +| `log` | Writes an INFO log line with a template message | `{ message: "Order {orderCode} exceeded threshold" }` |
  101 +| `set-field` | Sets a field on the event's aggregate via a REST call or ext update | `{ entity: "SalesOrder", field: "ext.priority", value: "HIGH" }` |
  102 +| `publish-event` | Publishes a synthetic event to the event bus | `{ eventType: "RuleTriggeredEvent", data: "{...}" }` |
  103 +
  104 +v1.0 keeps actions simple. Complex actions (send email, call webhook, create entity) are v1.1.
  105 +
  106 +### Backend: RuleEngine service
  107 +
  108 +New class in `platform-metadata` (or a new `platform-rules` module — but YAGNI,
  109 +put it in `platform-metadata` since rules are metadata):
  110 +
  111 +```kotlin
  112 +@Component
  113 +class RuleEngine(
  114 + private val eventBus: EventBus,
  115 + private val jdbc: NamedParameterJdbcTemplate,
  116 + private val objectMapper: ObjectMapper,
  117 +) {
  118 + @PostConstruct
  119 + fun loadAndSubscribe() {
  120 + // 1. Read all enabled rules from metadata__rule
  121 + // 2. Group by triggerEvent
  122 + // 3. Subscribe to each distinct event type via eventBus.subscribe(topic, listener)
  123 + // using the aggregateType as the topic string
  124 + // 4. When an event fires, evaluate conditions against event properties
  125 + // 5. If conditions match, execute actions
  126 + }
  127 +
  128 + fun refresh() {
  129 + // Unsubscribe all, reload, resubscribe
  130 + // Called after rule CRUD operations
  131 + }
  132 +}
  133 +```
  134 +
  135 +### Condition evaluation
  136 +
  137 +Events are domain objects with typed properties. The rule engine serializes the
  138 +event to a `Map<String, Any?>` via Jackson, then evaluates each condition:
  139 +
  140 +```kotlin
  141 +fun evaluateCondition(eventMap: Map<String, Any?>, condition: Condition): Boolean {
  142 + val actual = eventMap[condition.field] ?: return false
  143 + val expected = condition.value
  144 + return when (condition.operator) {
  145 + "eq" -> actual.toString() == expected
  146 + "neq" -> actual.toString() != expected
  147 + "gt" -> (actual as? Number)?.toDouble()?.let { it > expected.toDouble() } ?: false
  148 + "gte" -> (actual as? Number)?.toDouble()?.let { it >= expected.toDouble() } ?: false
  149 + "lt" -> (actual as? Number)?.toDouble()?.let { it < expected.toDouble() } ?: false
  150 + "lte" -> (actual as? Number)?.toDouble()?.let { it <= expected.toDouble() } ?: false
  151 + "contains" -> actual.toString().contains(expected)
  152 + "in" -> expected.split(",").map { it.trim() }.contains(actual.toString())
  153 + else -> false
  154 + }
  155 +}
  156 +```
  157 +
  158 +### Backend endpoints (in MetadataController or new RuleController)
  159 +
  160 +```
  161 +GET /api/v1/_meta/metadata/rules → List<RuleDefinition>
  162 +GET /api/v1/_meta/metadata/rules/{slug} → RuleDefinition
  163 +PUT /api/v1/_meta/metadata/rules/{slug} → RuleDefinition (upsert, source='user')
  164 +DELETE /api/v1/_meta/metadata/rules/{slug} → 204
  165 +```
  166 +
  167 +Same source='user' enforcement as forms/list-views.
  168 +After every write, call `ruleEngine.refresh()`.
  169 +
  170 +### SPA: Rules tab in MetadataAdmin
  171 +
  172 +Add a 7th tab "Rules" to MetadataAdminPage:
  173 +
  174 +- DataTable showing: Name, Trigger Event, Enabled (toggle), Conditions (count), Actions (count), Source badge
  175 +- "New Rule" button opens an inline form:
  176 + - Name, description
  177 + - Trigger event dropdown (populated from known event types)
  178 + - Conditions builder: field, operator, value (add/remove rows)
  179 + - Condition logic toggle (AND/OR)
  180 + - Actions builder: type dropdown, config key-value inputs (add/remove rows)
  181 + - Enabled checkbox
  182 + - Save/Cancel
  183 +- Edit button per source='user' row
  184 +- Delete button per source='user' row
  185 +
  186 +### MetadataYaml extension
  187 +
  188 +Add `rules: List<RuleYaml>` to `MetadataYamlFile` so core PBCs and plug-ins
  189 +can ship default rules in their metadata YAML. Same loader/wipe pattern.
  190 +
  191 +## 4. Scope Decisions
  192 +
  193 +| Decision | Choice |
  194 +|---|---|
  195 +| BPMN designer (P2.2) | Deferred to v1.1 — customers use Camunda Modeler or hand-edit BPMN XML |
  196 +| Rule action types | v1.0: log, set-field, publish-event. v1.1: email, webhook, create entity |
  197 +| Rule condition complexity | v1.0: flat field conditions with AND/OR. v1.1: nested groups, expressions |
  198 +| User-task assignment | v1.0: all tasks visible to all authenticated users. v1.1: role-based assignment |
  199 +
  200 +## 5. File Inventory
  201 +
  202 +### New files (Backend)
  203 +- `platform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/rules/RuleEngine.kt`
  204 +- `platform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/rules/RuleEvaluator.kt`
  205 +- `platform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/web/RuleController.kt`
  206 +- `platform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/yaml/RuleYaml.kt`
  207 +- `platform/platform-metadata/src/test/kotlin/org/vibeerp/platform/metadata/rules/RuleEvaluatorTest.kt`
  208 +
  209 +### New files (Frontend)
  210 +- `web/src/pages/UserTasksPage.tsx`
  211 +- `web/src/pages/TaskDetailPage.tsx`
  212 +
  213 +### Modified files
  214 +- `platform/platform-workflow/src/main/kotlin/org/vibeerp/platform/workflow/http/WorkflowController.kt` — add task endpoints
  215 +- `platform/platform-workflow/src/main/kotlin/org/vibeerp/platform/workflow/WorkflowService.kt` — add task methods
  216 +- `platform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/yaml/MetadataYaml.kt` — add RuleYaml
  217 +- `platform/platform-metadata/src/main/kotlin/org/vibeerp/platform/metadata/MetadataLoader.kt` — process rules
  218 +- `web/src/pages/MetadataAdminPage.tsx` — add Rules tab
  219 +- `web/src/api/client.ts` — add workflow task + rule API functions
  220 +- `web/src/types/api.ts` — add UserTask + Rule types
  221 +- `web/src/i18n/messages.ts` — add keys
  222 +- `web/src/App.tsx` — add routes
  223 +- `web/src/layout/AppLayout.tsx` — add nav entry
  224 +- `platform/platform-bootstrap/src/main/kotlin/org/vibeerp/platform/bootstrap/web/SpaController.kt` — add workflow routes
  225 +
  226 +## 6. Testing
  227 +
  228 +- `RuleEvaluatorTest.kt` — unit tests for condition evaluation (eq, gt, lt, contains, in, AND/OR logic)
  229 +- `RuleEngine` — integration: load rules from metadata, verify event subscription and action execution
  230 +- Manual smoke: create a rule via UI, trigger the event, verify action fires
  231 +- Manual smoke: start a BPMN process with a user-task, see it in the tasks page, complete it with the form