Skip to content

Latest commit

 

History

History
93 lines (77 loc) · 7.42 KB

File metadata and controls

93 lines (77 loc) · 7.42 KB
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
type
project

Rule engine — v1 scope and deferred backlog

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.

Shared premise

  • 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.
  • 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 }. Same code on both sides for clean dedup. v1 stores literal message; i18n key resolution is deferred.

v1 scope (ships first)

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.

Deferred — track here, build when justified

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.

Concepts borrowed from GNWebSoft.Forms

  • 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.

Where MTP improves on Forms

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.

Related

  • [[project-mtp-pascalcase-db]] — rule JSON keys + DB columns follow PascalCase.
  • [[project-mtp-tenancy-model]] — rule tables live inside each client DB, no ClientId column.
  • [[project-mtp-stack]] — Zod + React Hook Form + TanStack Query on client; FluentValidation only for shape on server.
  • [[project-mtp-persistence-model]] — FlowVersion row is immutable, append-only; aligns with event-sourced thinking.