feat: improve CLQMS docs layout and add new content

- 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
This commit is contained in:
mahdahar 2026-03-16 16:23:29 +07:00
parent cc7f101932
commit fcf996435c
5 changed files with 613 additions and 5 deletions

View File

@ -6,7 +6,7 @@ layout: base.njk
<div class="flex flex-col lg:flex-row gap-8 max-w-6xl mx-auto"> <div class="flex flex-col lg:flex-row gap-8 max-w-6xl mx-auto">
<!-- Sidebar Navigation - Sticky on scroll --> <!-- Sidebar Navigation - Sticky on scroll -->
<aside class="lg:w-64 flex-shrink-0"> <aside class="lg:w-64 flex-shrink-0">
<div class="lg:sticky lg:top-24 bg-base-200/50 backdrop-blur-xl border border-white/5 rounded-2xl p-6"> <div class="lg:sticky lg:top-24 bg-base-200/50 backdrop-blur-xl border border-white/5 rounded-2xl p-6 overflow-y-auto max-h-[calc(100vh-8rem)]">
<!-- Back to CLQMS Home --> <!-- Back to CLQMS Home -->
<a href="/projects/clqms01/" class="flex items-center gap-2 text-sm text-base-content/70 hover:text-primary mb-6 transition-colors"> <a href="/projects/clqms01/" class="flex items-center gap-2 text-sm text-base-content/70 hover:text-primary mb-6 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@ -71,7 +71,7 @@ layout: base.njk
</aside> </aside>
<!-- Main Content --> <!-- Main Content -->
<main class="flex-1 min-w-0 max-w-3xl"> <main class="flex-1 min-w-0 max-w-4xl">
<article class="pb-12"> <article class="pb-12">
<!-- Post header --> <!-- Post header -->
<header class="mb-10"> <header class="mb-10">

View File

@ -4,7 +4,7 @@ tags: clqms
title: "CLQMS: Test Definition API Examples" title: "CLQMS: Test Definition API Examples"
description: "Reference documentation for test maintenance API endpoints and data structures." description: "Reference documentation for test maintenance API endpoints and data structures."
date: 2025-12-10 date: 2025-12-10
order: 10 order: 6
--- ---
# Test Definition API Examples # Test Definition API Examples

View File

@ -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`

View File

@ -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 doesnt 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": "<compiled JSON here>",
"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.

View File

@ -10,7 +10,7 @@ order: 0
<div class="flex flex-col lg:flex-row gap-8 max-w-6xl mx-auto"> <div class="flex flex-col lg:flex-row gap-8 max-w-6xl mx-auto">
<!-- Sidebar Navigation - Sticky on scroll --> <!-- Sidebar Navigation - Sticky on scroll -->
<aside class="lg:w-64 flex-shrink-0 order-2 lg:order-1"> <aside class="lg:w-64 flex-shrink-0 order-2 lg:order-1">
<div class="lg:sticky lg:top-24 bg-base-200/50 backdrop-blur-xl border border-white/5 rounded-2xl p-6"> <div class="lg:sticky lg:top-24 bg-base-200/50 backdrop-blur-xl border border-white/5 rounded-2xl p-6 overflow-y-auto max-h-[calc(100vh-8rem)]">
<!-- Core Documentation --> <!-- Core Documentation -->
<div class="mb-6"> <div class="mb-6">
<h3 class="font-bold text-sm text-base-content/50 uppercase tracking-wider mb-3 flex items-center gap-2"> <h3 class="font-bold text-sm text-base-content/50 uppercase tracking-wider mb-3 flex items-center gap-2">
@ -72,7 +72,7 @@ order: 0
</aside> </aside>
<!-- Main Content --> <!-- Main Content -->
<main class="flex-1 min-w-0 max-w-3xl order-1 lg:order-2"> <main class="flex-1 min-w-0 max-w-4xl order-1 lg:order-2">
<div class="pb-12"> <div class="pb-12">
<!-- Hero Section --> <!-- Hero Section -->
<section class="mb-12"> <section class="mb-12">