From fcf996435c7ede6efc0980dd01da81ec8e417b87 Mon Sep 17 00:00:00 2001 From: mahdahar <89adham@gmail.com> Date: Mon, 16 Mar 2026 16:23:29 +0700 Subject: [PATCH] feat: improve CLQMS docs layout and add new content MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add overflow-y-auto and max-height to sidebar for scrollability - Increase main content width from max-w-3xl to max-w-4xl - Fix order number for test-api-examples (10 → 6) - Add new documentation: calculator operators and test rule engine --- src/_layouts/clqms-post.njk | 4 +- src/projects/clqms01/006-test-api-examples.md | 2 +- .../clqms01/007-calculator-operators.md | 318 ++++++++++++++++++ src/projects/clqms01/008-test-rule-engine.md | 290 ++++++++++++++++ src/projects/clqms01/index.njk | 4 +- 5 files changed, 613 insertions(+), 5 deletions(-) create mode 100644 src/projects/clqms01/007-calculator-operators.md create mode 100644 src/projects/clqms01/008-test-rule-engine.md diff --git a/src/_layouts/clqms-post.njk b/src/_layouts/clqms-post.njk index e8b7d1d..2581e72 100644 --- a/src/_layouts/clqms-post.njk +++ b/src/_layouts/clqms-post.njk @@ -6,7 +6,7 @@ layout: base.njk
-
+
diff --git a/src/projects/clqms01/006-test-api-examples.md b/src/projects/clqms01/006-test-api-examples.md index be4fd61..8baf074 100644 --- a/src/projects/clqms01/006-test-api-examples.md +++ b/src/projects/clqms01/006-test-api-examples.md @@ -4,7 +4,7 @@ tags: clqms title: "CLQMS: Test Definition API Examples" description: "Reference documentation for test maintenance API endpoints and data structures." date: 2025-12-10 -order: 10 +order: 6 --- # Test Definition API Examples diff --git a/src/projects/clqms01/007-calculator-operators.md b/src/projects/clqms01/007-calculator-operators.md new file mode 100644 index 0000000..bb1ef5c --- /dev/null +++ b/src/projects/clqms01/007-calculator-operators.md @@ -0,0 +1,318 @@ +--- +layout: clqms-post.njk +tags: clqms +title: "Calculator Service Operators Reference" +description: "Complete reference for mathematical operators, functions, and constants available in the CalculatorService." +date: 2026-03-16 +order: 7 +--- + +# Calculator Service Operators Reference + +## Overview + +The `CalculatorService` (`app/Services/CalculatorService.php`) uses the [mossadal/math-parser](https://github.com/mossadal/math-parser) library to safely evaluate mathematical expressions. This document lists all available operators, functions, and constants. + +--- + +## Supported Operators + +### Arithmetic Operators + +| Operator | Description | Example | Result | +|----------|-------------|---------|--------| +| `+` | Addition | `5 + 3` | `8` | +| `-` | Subtraction | `10 - 4` | `6` | +| `*` | Multiplication | `6 * 7` | `42` | +| `/` | Division | `20 / 4` | `5` | +| `^` | Exponentiation (power) | `2 ^ 3` | `8` | +| `!` | Factorial | `5!` | `120` | +| `!!` | Semi-factorial (double factorial) | `5!!` | `15` | + +### Parentheses + +Use parentheses to control operation precedence: + +``` +(2 + 3) * 4 // Result: 20 +2 + 3 * 4 // Result: 14 +``` + +--- + +## Mathematical Functions + +### Rounding Functions + +| Function | Description | Example | +|----------|-------------|---------| +| `sqrt(x)` | Square root | `sqrt(16)` → `4` | +| `round(x)` | Round to nearest integer | `round(3.7)` → `4` | +| `ceil(x)` | Round up to integer | `ceil(3.2)` → `4` | +| `floor(x)` | Round down to integer | `floor(3.9)` → `3` | +| `abs(x)` | Absolute value | `abs(-5)` → `5` | +| `sgn(x)` | Sign function | `sgn(-10)` → `-1` | + +### Trigonometric Functions (Radians) + +| Function | Description | Example | +|----------|-------------|---------| +| `sin(x)` | Sine | `sin(pi/2)` → `1` | +| `cos(x)` | Cosine | `cos(0)` → `1` | +| `tan(x)` | Tangent | `tan(pi/4)` → `1` | +| `cot(x)` | Cotangent | `cot(pi/4)` → `1` | + +### Trigonometric Functions (Degrees) + +| Function | Description | Example | +|----------|-------------|---------| +| `sind(x)` | Sine (degrees) | `sind(90)` → `1` | +| `cosd(x)` | Cosine (degrees) | `cosd(0)` → `1` | +| `tand(x)` | Tangent (degrees) | `tand(45)` → `1` | +| `cotd(x)` | Cotangent (degrees) | `cotd(45)` → `1` | + +### Hyperbolic Functions + +| Function | Description | Example | +|----------|-------------|---------| +| `sinh(x)` | Hyperbolic sine | `sinh(1)` → `1.175...` | +| `cosh(x)` | Hyperbolic cosine | `cosh(1)` → `1.543...` | +| `tanh(x)` | Hyperbolic tangent | `tanh(1)` → `0.761...` | +| `coth(x)` | Hyperbolic cotangent | `coth(2)` → `1.037...` | + +### Inverse Trigonometric Functions + +| Function | Aliases | Description | Example | +|----------|---------|-------------|---------| +| `arcsin(x)` | `asin(x)` | Inverse sine | `arcsin(0.5)` → `0.523...` | +| `arccos(x)` | `acos(x)` | Inverse cosine | `arccos(0.5)` → `1.047...` | +| `arctan(x)` | `atan(x)` | Inverse tangent | `arctan(1)` → `0.785...` | +| `arccot(x)` | `acot(x)` | Inverse cotangent | `arccot(1)` → `0.785...` | + +### Inverse Hyperbolic Functions + +| Function | Aliases | Description | Example | +|----------|---------|-------------|---------| +| `arsinh(x)` | `asinh(x)`, `arcsinh(x)` | Inverse hyperbolic sine | `arsinh(1)` → `0.881...` | +| `arcosh(x)` | `acosh(x)`, `arccosh(x)` | Inverse hyperbolic cosine | `arcosh(2)` → `1.316...` | +| `artanh(x)` | `atanh(x)`, `arctanh(x)` | Inverse hyperbolic tangent | `artanh(0.5)` → `0.549...` | +| `arcoth(x)` | `acoth(x)`, `arccoth(x)` | Inverse hyperbolic cotangent | `arcoth(2)` → `0.549...` | + +### Logarithmic & Exponential Functions + +| Function | Aliases | Description | Example | +|----------|---------|-------------|---------| +| `exp(x)` | - | Exponential (e^x) | `exp(2)` → `7.389...` | +| `log(x)` | `ln(x)` | Natural logarithm (base e) | `log(e)` → `1` | +| `log10(x)` | `lg(x)` | Logarithm base 10 | `log10(100)` → `2` | + +--- + +## Constants + +| Constant | Value | Description | Example | +|----------|-------|-------------|---------| +| `pi` | 3.14159265... | Ratio of circle circumference to diameter | `pi * r ^ 2` | +| `e` | 2.71828182... | Euler's number | `e ^ x` | +| `NAN` | Not a Number | Invalid mathematical result | - | +| `INF` | Infinity | Positive infinity | - | + +--- + +## Variables in CalculatorService + +When using `calculateFromDefinition()`, the following variables are automatically available: + +| Variable | Description | Type | +|----------|-------------|------| +| `{result}` | The test result value | Float | +| `{factor}` | Calculation factor (default: 1) | Float | +| `{gender}` | Gender value (0=Unknown, 1=Female, 2=Male) | Integer | +| `{age}` | Patient age | Float | +| `{ref_low}` | Reference range low value | Float | +| `{ref_high}` | Reference range high value | Float | + +### Gender Mapping + +The `gender` variable accepts the following values: + +| Value | Description | +|-------|-------------| +| `0` | Unknown | +| `1` | Female | +| `2` | Male | + +Or use string values: `'unknown'`, `'female'`, `'male'` + +--- + +## Implicit Multiplication + +The parser supports implicit multiplication (no explicit `*` operator needed): + +| Expression | Parsed As | Result (x=2, y=3) | +|------------|-----------|-------------------| +| `2x` | `2 * x` | `4` | +| `x sin(x)` | `x * sin(x)` | `1.818...` | +| `2xy` | `2 * x * y` | `12` | +| `x^2y` | `x^2 * y` | `12` | + +**Note:** Implicit multiplication has the same precedence as explicit multiplication. `xy^2z` is parsed as `x*y^2*z`, NOT as `x*y^(2*z)`. + +--- + +## Usage Examples + +### Basic Calculation + +```php +use App\Services\CalculatorService; + +$calculator = new CalculatorService(); + +// Simple arithmetic +$result = $calculator->calculate("5 + 3 * 2"); +// Result: 11 + +// Using functions +$result = $calculator->calculate("sqrt(16) + abs(-5)"); +// Result: 9 + +// Using constants +$result = $calculator->calculate("2 * pi * r", ['r' => 5]); +// Result: 31.415... +``` + +### With Variables + +```php +$formula = "{result} * {factor} + 10"; +$variables = [ + 'result' => 5.2, + 'factor' => 2 +]; + +$result = $calculator->calculate($formula, $variables); +// Result: 20.4 +``` + +### BMI Calculation + +```php +$formula = "{weight} / ({height} ^ 2)"; +$variables = [ + 'weight' => 70, // kg + 'height' => 1.75 // meters +]; + +$result = $calculator->calculate($formula, $variables); +// Result: 22.86 +``` + +### Gender-Based Calculation + +```php +// Apply different multipliers based on gender +$formula = "{result} * (1 + 0.1 * {gender})"; +$variables = [ + 'result' => 100, + 'gender' => 1 // Female = 1 +]; + +$result = $calculator->calculate($formula, $variables); +// Result: 110 +``` + +### Complex Formula + +```php +// Pythagorean theorem with rounding +$formula = "round(sqrt({a} ^ 2 + {b} ^ 2))"; +$variables = [ + 'a' => 3, + 'b' => 4 +]; + +$result = $calculator->calculate($formula, $variables); +// Result: 5 +``` + +### Using calculateFromDefinition + +```php +$calcDef = [ + 'FormulaCode' => '{result} * {factor} + {gender}', + 'Factor' => 2 +]; + +$testValues = [ + 'result' => 10, + 'gender' => 1 // Female +]; + +$result = $calculator->calculateFromDefinition($calcDef, $testValues); +// Result: 21 (10 * 2 + 1) +``` + +--- + +## Formula Validation + +Validate formulas before storing them: + +```php +$validation = $calculator->validate("{result} / {factor}"); +// Returns: ['valid' => true, 'error' => null] + +$validation = $calculator->validate("{result} /"); +// Returns: ['valid' => false, 'error' => 'Error message'] +``` + +### Extract Variables + +Get a list of variables used in a formula: + +```php +$variables = $calculator->extractVariables("{result} * {factor} + {age}"); +// Returns: ['result', 'factor', 'age'] +``` + +--- + +## Error Handling + +The service throws exceptions for invalid formulas or missing variables: + +```php +try { + $result = $calculator->calculate("{result} / 0"); +} catch (\Exception $e) { + // Handle division by zero or other errors + log_message('error', $e->getMessage()); +} +``` + +Common errors: + +- **Invalid formula syntax**: Malformed expressions +- **Missing variable**: Variable placeholder not provided in data array +- **Non-numeric value**: Variables must be numeric +- **Division by zero**: Mathematical error + +--- + +## Best Practices + +1. **Always validate formulas** before storing in database +2. **Use placeholder syntax** `{variable_name}` for clarity +3. **Handle exceptions** in production code +4. **Test edge cases** like zero values and boundary conditions +5. **Document formulas** with comments in your code + +--- + +## References + +- [math-parser GitHub](https://github.com/mossadal/math-parser) +- [math-parser Documentation](http://mossadal.github.io/math-parser/) +- `app/Services/CalculatorService.php` diff --git a/src/projects/clqms01/008-test-rule-engine.md b/src/projects/clqms01/008-test-rule-engine.md new file mode 100644 index 0000000..e84bc07 --- /dev/null +++ b/src/projects/clqms01/008-test-rule-engine.md @@ -0,0 +1,290 @@ +--- +layout: clqms-post.njk +tags: clqms +title: "Test Rule Engine Documentation" +description: "Comprehensive guide to the CLQMS Rule Engine DSL, syntax, and action definitions." +date: 2026-03-16 +order: 8 +--- + +# Test Rule Engine Documentation + +## Overview + +The CLQMS Rule Engine evaluates business rules that inspect orders, patients, and tests, then executes actions when the compiled condition matches. + +Rules are authored using a domain specific language stored in `ruledef.ConditionExpr`. Before the platform executes any rule, the DSL must be compiled into JSON and stored in `ConditionExprCompiled`, and each rule must be linked to the tests it should influence via `testrule`. + +### Execution Flow + +1. Write or edit the DSL in `ConditionExpr`. +2. POST the expression to `POST /api/rule/compile` to validate syntax and produce compiled JSON. +3. Save the compiled payload into `ConditionExprCompiled` and persist the rule in `ruledef`. +4. Link the rule to one or more tests through `testrule.TestSiteID` (rules only run for linked tests). +5. When the configured event fires (`test_created` or `result_updated`), the engine evaluates `ConditionExprCompiled` and runs the resulting `then` or `else` actions. + +> **Note:** The rule engine currently fires only for `test_created` and `result_updated`. Other event codes can exist in the database but are not triggered by the application unless additional `RuleEngineService::run(...)` calls are added. + +## Event Triggers + +| Event Code | Status | Trigger Point | +|------------|--------|----------------| +| `test_created` | Active | Fired after a new test row is persisted; the handler calls `RuleEngineService::run('test_created', ...)` to evaluate test-scoped rules | +| `result_updated` | Active | Fired whenever a test result is saved or updated so result-dependent rules run immediately | + +Other event codes remain in the database for future workflows, but only `test_created` and `result_updated` are executed by the current application flow. + +## Rule Structure + +``` +Rule +├── Event Trigger (when to run) +├── Conditions (when to match) +└── Actions (what to do) +``` + +The DSL expression lives in `ConditionExpr`. The compile endpoint (`/api/rule/compile`) renders the lifeblood of execution, producing `conditionExpr`, `valueExpr`, `then`, and `else` nodes that the engine consumes at runtime. + +## Syntax Guide + +### Basic Format + +``` +if(condition; then-action; else-action) +``` + +### Logical Operators + +- Use `&&` for AND (all sub-conditions must match). +- Use `||` for OR (any matching branch satisfies the rule). +- Surround mixed logic with parentheses for clarity and precedence. + +### Multi-Action Syntax + +Actions within any branch are separated by `:` and evaluated in order. Every `then` and `else` branch must end with an action; use `nothing` when no further work is required. + +``` +if(sex('M'); result_set(0.5):test_insert('HBA1C'); nothing) +``` + +### Multiple Rules + +Create each rule as its own `ruledef` row; do not chain expressions with commas. The `testrule` table manages rule-to-test mappings, so multiple rules can attach to the same test. Example: + +1. Insert `RULE_MALE_RESULT` and `RULE_SENIOR_COMMENT` in `ruledef`. +2. Add two `testrule` rows linking each rule to the appropriate `TestSiteID`. + +Each rule compiles and runs independently when its trigger fires and the test is linked. + +## Available Functions + +### Conditions + +| Function | Description | Example | +|----------|-------------|---------| +| `sex('M'|'F')` | Match patient sex | `sex('M')` | +| `priority('R'|'S'|'U')` | Match order priority | `priority('S')` | +| `age > 18` | Numeric age comparisons (`>`, `<`, `>=`, `<=`) | `age >= 18 && age <= 65` | +| `requested('CODE')` | Check whether the order already requested a test (queries `patres`) | `requested('GLU')` | + +### Logical Operators + +| Operator | Meaning | Example | +|----------|---------|---------| +| `&&` | AND (all truthy) | `sex('M') && age > 40` | +| `||` | OR (any truthy) | `sex('M') || age > 65` | +| `()` | Group expressions | `(sex('M') && age > 40) || priority('S')` | + +## Actions + +| Action | Description | Example | +|--------|-------------|---------| +| `result_set(value)` | (deprecated) Write to `patres.Result` for the current context test | `result_set(0.5)` | +| `result_set('CODE', value)` | Target a specific test by `TestSiteCode`, allowing multiple tests to be updated in one rule | `result_set('tesA', 0.5)` | +| `test_insert('CODE')` | Insert a test row by `TestSiteCode` if it doesn’t already exist for the order | `test_insert('HBA1C')` | +| `test_delete('CODE')` | Remove a previously requested test from the current order when the rule deems it unnecessary | `test_delete('INS')` | +| `comment_insert('text')` | Insert an order comment (`ordercom`) describing priority or clinical guidance | `comment_insert('Male patient - review')` | +| `nothing` | Explicit no-op to terminate an action chain | `nothing` | + +> **Note:** `set_priority()` was removed. Use `comment_insert()` for priority notes without altering billing. + +## Runtime Requirements + +1. **Compiled expression required:** Rules without `ConditionExprCompiled` are ignored (see `RuleEngineService::run`). +2. **Order context:** `context.order.InternalOID` must exist for any action that writes to `patres` or `ordercom`. +3. **TestSiteID:** `result_set()` needs `testSiteID` (either provided in context or from `order.TestSiteID`). When you provide a `TestSiteCode` as the first argument (`result_set('tesA', value)`), the engine resolves that code before writing the result. `test_insert()` resolves a `TestSiteID` via the `TestSiteCode` in `TestDefSiteModel`, and `test_delete()` removes the matching `TestSiteID` rows when needed. +4. **Requested check:** `requested('CODE')` inspects `patres` rows for the same `OrderID` and `TestSiteCode`. + +## Examples + +``` +if(sex('M'); result_set('tesA', 0.5):result_set('tesB', 1.2); result_set('tesA', 0.6):result_set('tesB', 1.0)) +``` +Sets both `tesA`/`tesB` results together per branch. + +``` +if(requested('GLU'); test_insert('HBA1C'):test_insert('INS'); nothing) +``` +Adds new tests when glucose is already requested. + +``` +if(sex('M') && age > 40; result_set(1.2); result_set(1.0)) +``` + +``` +if((sex('M') && age > 40) || (sex('F') && age > 50); result_set(1.5); result_set(1.0)) +``` + +``` +if(priority('S'); result_set('URGENT'):test_insert('STAT_TEST'); result_set('NORMAL')) +``` + +``` +if(sex('M') && age > 40; result_set(1.5):test_insert('EXTRA_TEST'):comment_insert('Male over 40'); nothing) +``` + +``` +if(sex('F') && (age >= 18 && age <= 50) && priority('S'); result_set('HIGH_PRIO'):comment_insert('Female stat 18-50'); result_set('NORMAL')) +``` + +``` +if(requested('GLU'); test_delete('INS'):comment_insert('Duplicate insulin request removed'); nothing) +``` + +## API Usage + +### Compile DSL + +Validates the DSL and returns a compiled JSON structure that should be persisted in `ConditionExprCompiled`. + +```http +POST /api/rule/compile +Content-Type: application/json + +{ + "expr": "if(sex('M'); result_set(0.5); result_set(0.6))" +} +``` + +The response contains `raw`, `compiled`, and `conditionExprCompiled` fields; store the JSON payload in `ConditionExprCompiled` before saving the rule. + +### Evaluate Expression (Validation) + +This endpoint simply evaluates an expression against a runtime context. It does not compile DSL or persist the result. + +```http +POST /api/rule/validate +Content-Type: application/json + +{ + "expr": "order[\"Age\"] > 18", + "context": { + "order": { + "Age": 25 + } + } +} +``` + +### Create Rule (example) + +```http +POST /api/rule +Content-Type: application/json + +{ + "RuleCode": "RULE_001", + "RuleName": "Sex-based result", + "EventCode": "test_created", + "ConditionExpr": "if(sex('M'); result_set(0.5); result_set(0.6))", + "ConditionExprCompiled": "", + "TestSiteIDs": [1, 2] +} +``` + +## Database Schema + +### Tables + +- **ruledef** – stores rule metadata, raw DSL, and compiled JSON. +- **testrule** – mapping table that links rules to tests via `TestSiteID`. +- **ruleaction** – deprecated. Actions are now embedded in `ConditionExprCompiled`. + +### Key Columns + +| Column | Table | Description | +|--------|-------|-------------| +| `EventCode` | ruledef | The trigger event (typically `test_created` or `result_updated`). | +| `ConditionExpr` | ruledef | Raw DSL expression (semicolon syntax). | +| `ConditionExprCompiled` | ruledef | JSON payload consumed at runtime (`then`, `else`, etc.). | +| `ActionType` / `ActionParams` | ruleaction | Deprecated; actions live in compiled JSON now. | + +## Best Practices + +1. Always run `POST /api/rule/compile` before persisting a rule so `ConditionExprCompiled` exists. +2. Link each rule to the relevant tests via `testrule.TestSiteID`—rules are scoped to linked tests. +3. Use multi-action (`:`) to bundle several actions in a single branch; finish the branch with `nothing` if no further work is needed. +4. Prefer `comment_insert()` over the removed `set_priority()` action when documenting priority decisions. +5. Group complex boolean logic with parentheses for clarity when mixing `&&` and `||`. +6. Use `requested('CODE')` responsibly; it performs a database lookup on `patres` so avoid invoking it in high-frequency loops without reason. + +## Migration Guide + +### Syntax Changes (v2.0) + +The DSL moved from ternary (`condition ? action : action`) to semicolon syntax. Existing rules must be migrated via the provided script. + +| Old Syntax | New Syntax | +|------------|------------| +| `if(condition ? action : action)` | `if(condition; action; action)` | + +#### Migration Examples + +``` +# BEFORE +if(sex('M') ? result_set(0.5) : result_set(0.6)) + +# AFTER +if(sex('M'); result_set(0.5); result_set(0.6)) +``` + +``` +# BEFORE +if(sex('F') ? set_priority('S') : nothing) + +# AFTER +if(sex('F'); comment_insert('Female patient - review priority'); nothing) +``` + +#### Migration Process + +Run the migration which: + +1. Converts ternary syntax to semicolon syntax. +2. Recompiles every expression into JSON so the engine consumes `ConditionExprCompiled` directly. +3. Eliminates reliance on the `ruleaction` table. + +```bash +php spark migrate +``` + +## Troubleshooting + +### Rule Not Executing + +1. Ensure the rule has a compiled payload (`ConditionExprCompiled`). +2. Confirm the rule is linked to the relevant `TestSiteID` in `testrule`. +3. Verify the `EventCode` matches the currently triggered event (`test_created` or `result_updated`). +4. Check that `EndDate IS NULL` for both `ruledef` and `testrule` (soft deletes disable execution). +5. Use `/api/rule/compile` to validate the DSL and view errors. + +### Invalid Expression + +1. POST the expression to `/api/rule/compile` to get a detailed compilation error. +2. If using `/api/rule/validate`, supply the expected `context` payload; the endpoint simply evaluates the expression without saving it. + +### Runtime Errors + +- `RESULT_SET requires context.order.InternalOID` or `testSiteID`: include those fields in the context passed to `RuleEngineService::run()`. +- `TEST_INSERT` failures mean the provided `TestSiteCode` does not exist or the rule attempted to insert a duplicate test; check `testdefsite` and existing `patres` rows. +- `COMMENT_INSERT requires comment`: ensure the action provides text. diff --git a/src/projects/clqms01/index.njk b/src/projects/clqms01/index.njk index 1590974..66862aa 100644 --- a/src/projects/clqms01/index.njk +++ b/src/projects/clqms01/index.njk @@ -10,7 +10,7 @@ order: 0
-
+