| name | project-mtp-rule-engine-v1 | ||
|---|---|---|---|
| description | v1 scope of the UI rule engine (page 04). What ships first, what is deferred, and which concepts are borrowed vs improved relative to GNWebSoft.Forms. | ||
| metadata |
|
Authoritative design lives in 04-ui-rule-config.html. This memory file pins the v1 cut and the deferred list so the scope does not drift across sessions.
Why: First implementation is intentionally narrow. Earlier review of D:\Projects\GNWebSoft.Forms (existing internal framework, .NET 10 + Dapper + SQL Server + React 19 + Zod) showed where to borrow and where MTP improves. Locking the v1 cut avoids re-litigating the same scope every session.
How to apply: When working on rule-engine, validation, or admin-config features, default to the v1 shape below. Push back if a request implies anything from the deferred list — confirm before expanding scope.
- One rule JSON, stored server-side (Postgres
jsonb). - Two evaluators consume it:
- C#
JsonRuleEvaluator— authoritative, runs on submit. - TS evaluator — runtime in browser, also synthesises a Zod schema from the same JSON to feed React Hook Form.
- C#
- Client validation is best-effort for fast feedback. Server is the source of truth. Never assume client = full rule set.
- Error contract: rule emits
{ code, field, message }. Samecodeon both sides for clean dedup. v1 stores literalmessage; i18n key resolution is deferred.
| Area | v1 decision |
|---|---|
| Storage | Single jsonb per feature flow. Hand-edit JSON for v1. |
| Severity | Blocking only. No Confirmable / Advisory yet. |
| Operators | eq, neq, lt, lte, gt, gte, required, requiredWhen, matches, in, length. |
| Cross-field rules | Out. Field-level only. condition nesting stays designed but unused. |
| Conditional logic | Show / Hide only, predicate eq or in. |
| Options source | Static inline list, or endpoint URL. No cascading placeholders. |
| Layout | Single-page form. No stepper, tabs, or sections. |
| Field contract | { key, label, type, required, options? }. FieldKey immutable. |
| Versioning | FlowVersionId column from day one. Every write = new row, never UPDATE. Binding table itself deferred, but column is non-negotiable — retrofitting after data exists is painful. |
| Admin UI | None. JSON hand-edited until engine proves itself on 2–3 features. |
| Item | Trigger to revisit |
|---|---|
| Stepper / multi-step forms | First feature that legitimately can't fit on one screen. |
FieldGroup (Section + Tabs) |
>15 fields or distinct logical groupings. |
Confirmable severity (soft fail, override with reason) |
First "are you sure?" requirement. |
Advisory severity (warning, never blocks) |
First "nudge the user" requirement. |
Cross-field rules (requiredWhen, forbiddenWhen, DateBefore, GreaterThan, …) |
First feature with two interdependent fields. Use existing condition nesting, not a separate table. |
Conditional actions beyond Show/Hide: Enable, Disable, MakeRequired, MakeOptional, ClearValue |
When Show/Hide proves insufficient — typically conditional required. |
Cascading options ({SelectedByValue} placeholder + ParentFieldId) |
First parent-child dropdown (Country → State). |
FEATURE_FLOW_BINDING indirection (named bindings, rollback by swap) |
When two flow versions need to coexist (A/B, staged rollout, rollback). |
| Dry-run endpoint for draft rules | Before shipping the admin UI editor. |
| Admin UI rule editor | After engine proven on 2–3 features. |
i18n key resolution (code → localised message) |
Second locale lands, or first reusable rule across features. |
| Operator registry versioning + client-degraded mode | When operator set grows past v1 list, or older clients linger in prod. Client seeing an unknown operator must mark form client-validation-degraded and rely on server, never silently pass. |
Curated schema endpoint (GET /admin/features/{key}/schema) |
Built alongside the admin UI editor. |
| Form-as-routable-resource (custom URL, access level, menu order) — Forms-style | When features need their own public URLs outside /features/{key}. |
| File-upload field type + storage (local / S3) | First feature that captures a document. |
buildValidationSchema.ts— server config → Zod at runtime. Generalise for MTP rule JSON.useConditionalLogic+useWatch— reactive client-side rule evaluation on RHF state.- Three-way split field rules · cross-field rules · conditional rules. Distinct concerns, never collapse into one operator list.
- Conditional action vocabulary (Show/Hide/Enable/Disable/MakeRequired/MakeOptional/ClearValue). Effect axis is independent of severity axis. Keep both.
FieldGroup(Section vs Tabs) as data, not code.- Multi-step + per-step validation pattern.
- Cascading options via
ParentFieldId+{SelectedByValue}placeholder. [DatasourceEndpoint]opt-in attribute for admin datasource picker. Better than a hand-curated schema doc.- Form-as-routable-resource (
CustomRoute,AccessLevel, menu integration). - Dry-run on draft rules before save.
| Forms approach | MTP approach |
|---|---|
| SQL Server + 40 SPs for form CRUD | Postgres 17 + jsonb. Rule JSON in a single column. No SP for rule logic (platform rule 7). |
| No version/binding indirection — edits mutate live rules | FEATURE_FLOW_VERSION + FEATURE_FLOW_BINDING. Edit = fork, deploy = binding swap, rollback free. |
| Single severity — every rule blocks | Blocking / Confirmable / Advisory. v1 ships Blocking only; contract reserves the others. |
Normalised rule tables (ValidationRule, CrossFieldValidationRule, ConditionalRule, ConditionalAction) |
Collapse to two jsonb columns per flow version: validationRules + conditionalRules. Cheaper migration, easier diff, one fetch. |
No FieldKey contract — entity IDs only |
Stable FieldKey, never reused or renamed once data exists (platform rule 11). |
Server FluentValidation hand-coded + client Zod hand-built from parallel config |
Single rule JSON drives both. FluentValidation reserved for shape (*ShapeValidator). Rule engine owns everything config-driven. |
| Rule load not visibly tenant-scoped | Rule repo queries always tenant + flow scoped. DB-per-client makes tenant scope trivial. |
| No audit on rule edit / binding switch | Every binding change logs who / when / from-version / to-version. |
| Operators implicit, not versioned | Shared operator registry in code, versioned. Client + server import the same enum. Unknown operator on client → client-validation-degraded, never silent pass. |
| Error messages stored as raw strings | Store code only in rule JSON. Client resolves via i18n key. Server returns code, never a localised string. |
- [[project-mtp-pascalcase-db]] — rule JSON keys + DB columns follow PascalCase.
- [[project-mtp-tenancy-model]] — rule tables live inside each client DB, no
ClientIdcolumn. - [[project-mtp-stack]] — Zod + React Hook Form + TanStack Query on client; FluentValidation only for shape on server.
- [[project-mtp-persistence-model]] —
FlowVersionrow is immutable, append-only; aligns with event-sourced thinking.