From 761ad32c5a6808d966e14541f2fbc3efa615f6ae Mon Sep 17 00:00:00 2001 From: mahdahar <89adham@gmail.com> Date: Thu, 2 Apr 2026 05:15:21 +0700 Subject: [PATCH] docs(clqms): simplify rule and calculator references Clarify API contract focused docs for calc, rule, and audit pages to reduce noisy implementation detail. Escape logical OR table examples as \|\| so Eleventy markdown tables render correctly. --- src/projects/clqms01/007-test-calc-engine.md | 358 ++++++------------- src/projects/clqms01/008-test-rule-engine.md | 315 +++++++--------- src/projects/clqms01/009-audit-logging.md | 275 ++++++-------- 3 files changed, 348 insertions(+), 600 deletions(-) diff --git a/src/projects/clqms01/007-test-calc-engine.md b/src/projects/clqms01/007-test-calc-engine.md index 29cd3b0..719b086 100644 --- a/src/projects/clqms01/007-test-calc-engine.md +++ b/src/projects/clqms01/007-test-calc-engine.md @@ -2,31 +2,47 @@ layout: clqms-post.njk tags: clqms title: "Calculator Service Operators Reference" -description: "Operators, functions, constants, and API usage for the calc engine" +description: "Concept, API contracts, and simplified operator reference for the calc engine" date: 2026-03-17 order: 7 --- -# Calculator Service Operators Reference +# Calculator Service: Concept and API Contract -## Overview +## Concept -The `CalculatorService` (`app/Services/CalculatorService.php`) evaluates formulas with Symfony's `ExpressionLanguage`. This document lists the operators, functions, and constants that are available in the current implementation. +`CalculatorService` evaluates formulas for test results using runtime variables. + +Use it when you need to: + +- transform a raw result into a calculated result, +- apply factors or demographic values (age, gender), +- reuse formula logic from test configuration. + +Two API patterns are used in practice: + +1. **By Test Site ID** - uses the calculation definition configured for that test site. +2. **By Test Code/Name** - resolves by test code or test name and returns a compact result map. + +Responses normally follow `{ status, message, data }`, except the compact test-code endpoint, +which returns a key/value map. --- -## API Endpoints +## Example API Contract -All endpoints live under `/api` and accept JSON. Responses use the standard `{ status, message, data }` envelope unless stated otherwise. - -### Calculate By Test Site - -Uses the `testdefcal` definition for a test site. The incoming body supplies the variables required by the formula. +### 1) Calculate by Test Site ```http -POST /api/calc/testsite/123 +POST /api/calc/testsite/{testSiteID} Content-Type: application/json +``` +Purpose: Evaluate the configured formula for a specific test site. + +Request example: + +```json { "result": 85, "gender": "female", @@ -34,7 +50,7 @@ Content-Type: application/json } ``` -Response: +Success response example: ```json { @@ -52,21 +68,35 @@ Response: } ``` -### Calculate By Code Or Name +Error response example: -Evaluates a configured calculation by `TestSiteCode` or `TestSiteName`. Returns a compact map with a single key/value or `{}` on failure. +```json +{ + "status": "error", + "message": "Missing variable: factor", + "data": null +} +``` + +### 2) Calculate by Test Code or Name ```http -POST /api/calc/testcode/GLU +POST /api/calc/testcode/{testCodeOrName} Content-Type: application/json +``` +Purpose: Evaluate a calculation by test code/name and return a compact map. + +Request example: + +```json { "result": 110, "factor": 1.1 } ``` -Response: +Success response example: ```json { @@ -74,25 +104,31 @@ Response: } ``` +Error response example: + +```json +{} +``` + --- -## Supported Operators +## Formula Reference (Simplified) ### Arithmetic Operators -| Operator | Description | Example | Result | -|----------|-------------|---------|--------| -| `+` | Addition | `5 + 3` | `8` | -| `-` | Subtraction | `10 - 4` | `6` | -| `*` | Multiplication | `6 * 7` | `42` | -| `/` | Division | `20 / 4` | `5` | -| `%` | Modulo | `20 % 6` | `2` | -| `**` | Exponentiation (power) | `2 ** 3` | `8` | +| Operator | Meaning | Example | +|----------|---------|---------| +| `+` | Add | `5 + 3` | +| `-` | Subtract | `10 - 4` | +| `*` | Multiply | `6 * 7` | +| `/` | Divide | `20 / 4` | +| `%` | Modulo | `20 % 6` | +| `**` | Power | `2 ** 3` | ### Comparison Operators -| Operator | Description | Example | -|----------|-------------|---------| +| Operator | Meaning | Example | +|----------|---------|---------| | `==` | Equal | `{result} == 10` | | `!=` | Not equal | `{result} != 10` | | `<` | Less than | `{result} < 10` | @@ -102,245 +138,77 @@ Response: ### Logical Operators -| Operator | Description | Example | -|----------|-------------|---------| -| `and` / `&&` | Logical AND | `{result} > 0 and {factor} > 0` | -| `or` / `||` | Logical OR | `{gender} == 1 or {gender} == 2` | -| `!` / `not` | Logical NOT | `not ({result} > 0)` | +| Operator | Meaning | Example | +|----------|---------|---------| +| `&&` (`and`) | Logical AND | `{result} > 0 && {factor} > 0` | +| `\|\|` (`or`) | Logical OR | `{gender} == 1 \|\| {gender} == 2` | +| `!` (`not`) | Logical NOT | `!({result} > 0)` | ### Conditional Operators -| Operator | Description | Example | -|----------|-------------|---------| +| Operator | Meaning | Example | +|----------|---------|---------| | `?:` | Ternary | `{result} > 10 ? {result} : 10` | -| `??` | Null coalescing | `{result} ?? 0` | +| `??` | Null fallback | `{result} ?? 0` | -### Parentheses +### Functions -Use parentheses to control operation precedence: - -``` -(2 + 3) * 4 // Result: 20 -2 + 3 * 4 // Result: 14 -``` - -### Notes - -- `^` is bitwise XOR (not exponentiation). Use `**` for powers. -- Variables must be numeric after normalization (gender is mapped to 0/1/2). - ---- - -## Functions - -Only the default ExpressionLanguage functions are available: - -| Function | Description | Example | -|----------|-------------|---------| -| `min(a, b, ...)` | Minimum value | `min({result}, 10)` | -| `max(a, b, ...)` | Maximum value | `max({result}, 10)` | +| Function | Meaning | Example | +|----------|---------|---------| +| `min(a, b, ...)` | Lowest value | `min({result}, 10)` | +| `max(a, b, ...)` | Highest value | `max({result}, 10)` | | `constant(name)` | PHP constant by name | `constant("PHP_INT_MAX")` | | `enum(name)` | PHP enum case by name | `enum("App\\Enum\\Status::Active")` | ---- +### Constants -## Constants +| Constant | Meaning | +|----------|---------| +| `true` | Boolean true | +| `false` | Boolean false | +| `null` | Null | -ExpressionLanguage recognizes boolean and null literals: +### Variables Commonly Used -| Constant | Value | Description | -|----------|-------|-------------| -| `true` | `true` | Boolean true | -| `false` | `false` | Boolean false | -| `null` | `null` | Null value | +| Variable | Type | Meaning | +|----------|------|---------| +| `{result}` | Float | Input result value | +| `{factor}` | Float | Multiplier (default usually `1`) | +| `{gender}` | Integer | `0` unknown, `1` female, `2` male | +| `{age}` | Float | Patient age | +| `{ref_low}` | Float | Reference low | +| `{ref_high}` | Float | Reference high | + +Gender can be passed as either numeric values (`0`, `1`, `2`) or strings +(`"unknown"`, `"female"`, `"male"`) and is normalized. + +### Formula Notes + +- Use parentheses for precedence: `(2 + 3) * 4`. +- Use `**` for exponentiation; `^` is XOR. +- Implicit multiplication is not supported (`2x` is invalid, use `2 * x`). --- -## Variables in CalculatorService +## Quick Usage Examples -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 - -Implicit multiplication is not supported. Always use `*` between values: - -| Expression | Use Instead | -|------------|-------------| -| `2x` | `2 * x` | -| `{result}{factor}` | `{result} * {factor}` | - ---- - -## Usage Examples - -### Basic Calculation - -```php -use App\Services\CalculatorService; - -$calculator = new CalculatorService(); - -// Simple arithmetic -$result = $calculator->calculate("5 + 3 * 2"); -// Result: 11 - -// Using min/max -$result = $calculator->calculate("max({result}, 10)", ['result' => 7]); -// Result: 10 -``` - -### 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 -$formula = "(({a} ** 2 + {b} ** 2) ** 0.5)"; -$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) +```txt +{result} * {factor} + 10 +{weight} / ({height} ** 2) +{result} * (1 + 0.1 * {gender}) ``` --- -## Formula Validation +## Validation and Errors -Validate formulas before storing them: +Validate formulas before saving. -```php -$validation = $calculator->validate("{result} / {factor}"); -// Returns: ['valid' => true, 'error' => null] +Typical error cases: -$validation = $calculator->validate("{result} /"); -// Returns: ['valid' => false, 'error' => 'Error message'] -``` +- invalid syntax, +- missing variable, +- non-numeric value after normalization, +- division by zero. -### 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 - -- [Symfony ExpressionLanguage](https://symfony.com/doc/current/components/expression_language.html) -- `app/Services/CalculatorService.php` +Recommended flow: validate formula -> save formula -> evaluate with guarded error handling. diff --git a/src/projects/clqms01/008-test-rule-engine.md b/src/projects/clqms01/008-test-rule-engine.md index e84bc07..aa8cc92 100644 --- a/src/projects/clqms01/008-test-rule-engine.md +++ b/src/projects/clqms01/008-test-rule-engine.md @@ -2,180 +2,153 @@ layout: clqms-post.njk tags: clqms title: "Test Rule Engine Documentation" -description: "Comprehensive guide to the CLQMS Rule Engine DSL, syntax, and action definitions." +description: "Concept, catalog, and API contracts for the CLQMS Rule Engine" date: 2026-03-16 order: 8 --- -# Test Rule Engine Documentation +# Test Rule Engine: Concept and API Contract -## Overview +## Concept -The CLQMS Rule Engine evaluates business rules that inspect orders, patients, and tests, then executes actions when the compiled condition matches. +The CLQMS Rule Engine executes business rules when specific events occur. -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`. +Each rule follows this lifecycle: -### Execution Flow +1. Author DSL in `ConditionExpr`. +2. Compile DSL using `POST /api/rule/compile`. +3. Save compiled JSON in `ConditionExprCompiled`. +4. Link the rule to tests via `testrule.TestSiteID`. +5. On trigger (`test_created`, `result_updated`), execute `then` or `else` actions. -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. +Rules without compiled JSON or without test links are not executed. -> **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 | +| `test_created` | Active | Runs after a new test row is saved | +| `result_updated` | Active | Runs when a result is saved or edited | -Other event codes remain in the database for future workflows, but only `test_created` and `result_updated` are executed by the current application flow. +Other event codes may exist in data, but these are the active runtime triggers. -## Rule Structure +--- -``` -Rule -├── Event Trigger (when to run) -├── Conditions (when to match) -└── Actions (what to do) +## Rule Shape + +```txt +Rule = Event + Condition + Actions ``` -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. +DSL format: -## Syntax Guide - -### Basic Format - -``` +```txt if(condition; then-action; else-action) ``` -### Logical Operators +Multi-action branches use `:` and execute left to right: -- 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. - -``` +```txt 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 +## Catalog (Simplified) ### Conditions -| Function | Description | Example | -|----------|-------------|---------| +| Function / Pattern | Meaning | 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')` | +| `age` comparisons | Numeric age checks | `age >= 18 && age <= 65` | +| `requested('CODE')` | Checks if test code exists in order | `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')` | +| `&&` | AND | `sex('M') && age > 40` | +| `\|\|` | OR | `sex('M') \|\| age > 65` | +| `!` | NOT | `!(sex('M'))` | +| `()` | Grouping | `(sex('M') && age > 40) \|\| priority('S')` | -## Actions +### 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` | +| Action | Meaning | Example | +|--------|---------|---------| +| `result_set(value)` | Set current-context result (deprecated style) | `result_set(0.5)` | +| `result_set('CODE', value)` | Set result for a specific test code | `result_set('tesA', 0.5)` | +| `test_insert('CODE')` | Insert test if missing | `test_insert('HBA1C')` | +| `test_delete('CODE')` | Remove test from order | `test_delete('INS')` | +| `comment_insert('text')` | Insert order comment | `comment_insert('Review required')` | +| `nothing` | No operation | `nothing` | -> **Note:** `set_priority()` was removed. Use `comment_insert()` for priority notes without altering billing. +`set_priority()` is removed; use `comment_insert()` when you need to record priority notes. -## 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`. +## Example API Contract -## 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`. +### 1) Compile DSL ```http POST /api/rule/compile Content-Type: application/json +``` +Purpose: Validate DSL and return compiled JSON for `ConditionExprCompiled`. + +Request example: + +```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. +Success response example: -### Evaluate Expression (Validation) +```json +{ + "status": "success", + "data": { + "raw": "if(sex('M'); result_set(0.5); result_set(0.6))", + "compiled": { + "conditionExpr": "sex('M')", + "then": [{ "type": "result_set", "args": [0.5] }], + "else": [{ "type": "result_set", "args": [0.6] }] + }, + "conditionExprCompiled": "{...json string...}" + } +} +``` -This endpoint simply evaluates an expression against a runtime context. It does not compile DSL or persist the result. +Error response example: + +```json +{ + "status": "error", + "message": "Invalid expression near ';'", + "data": null +} +``` + +### 2) Validate Expression Against Context ```http POST /api/rule/validate Content-Type: application/json +``` +Purpose: Evaluate an expression with context only (no compile, no persistence). + +Request example: + +```json { "expr": "order[\"Age\"] > 18", "context": { @@ -186,12 +159,29 @@ Content-Type: application/json } ``` -### Create Rule (example) +Success response example: + +```json +{ + "status": "success", + "data": { + "result": true + } +} +``` + +### 3) Create Rule ```http POST /api/rule Content-Type: application/json +``` +Purpose: Save rule metadata, DSL, compiled payload, and linked tests. + +Request example: + +```json { "RuleCode": "RULE_001", "RuleName": "Sex-based result", @@ -202,89 +192,32 @@ Content-Type: application/json } ``` -## Database Schema +Success response example: -### 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)) +```json +{ + "status": "success", + "message": "Rule created", + "data": { + "RuleCode": "RULE_001" + } +} ``` -``` -# BEFORE -if(sex('F') ? set_priority('S') : nothing) +--- -# AFTER -if(sex('F'); comment_insert('Female patient - review priority'); nothing) -``` +## Minimal End-to-End Flow -#### Migration Process +1. Compile DSL with `/api/rule/compile`. +2. Store returned `conditionExprCompiled` in `ruledef.ConditionExprCompiled`. +3. Create rule and link `TestSiteIDs` via `/api/rule`. +4. Trigger event (`test_created` or `result_updated`) and verify actions ran. -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. +## Runtime Requirements (Quick Check) -```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. +1. `ConditionExprCompiled` must be present. +2. Rule must be linked to the target `TestSiteID`. +3. Trigger event must match `EventCode`. +4. Context must include required identifiers for write actions. diff --git a/src/projects/clqms01/009-audit-logging.md b/src/projects/clqms01/009-audit-logging.md index f621b7b..546787c 100644 --- a/src/projects/clqms01/009-audit-logging.md +++ b/src/projects/clqms01/009-audit-logging.md @@ -2,201 +2,148 @@ layout: clqms-post.njk tags: clqms title: "Audit Logging" -description: "Unified audit logging model, event catalog, and capture rules for CLQMS." +description: "Audit logging concept and example API contracts for CLQMS" date: 2026-03-17 order: 9 --- -# Audit Logging Strategy -## Overview +# Audit Logging: Concept and Example API Contract -This document defines how CLQMS should capture audit and operational logs across four tables: +## Concept -- `logpatient` — patient, visit, and ADT activity -- `logorder` — orders, tests, specimens, results, and QC -- `logmaster` — master data and configuration changes -- `logsystem` — sessions, security, import/export, and system operations +CLQMS uses four audit tables so domain activity is separated but consistent: -The intent is to audit all domains, including master data changes, and to standardize event capture so reporting and compliance are consistent. +- `logpatient` - patient, identity, consent, insurance, and visit/ADT activity +- `logorder` - orders, specimens, results, and QC activity +- `logmaster` - configuration and master data changes +- `logsystem` - sessions, security, integration jobs, and system operations -## Table Ownership +The goal is traceability: who did what, when, where, and why. -| Event | Table | -| --- | --- | -| Patient registered/updated/merged | `logpatient` | -| Insurance/consent changed | `logpatient` | -| Patient visit (admit/transfer/discharge) | `logpatient` | -| Order created/cancelled | `logorder` | -| Sample received/rejected | `logorder` | -| Result entered/verified/amended | `logorder` | -| Result released/retracted/corrected | `logorder` | -| QC result recorded | `logorder` | -| Test panel added/removed | `logmaster` | -| Reference range changed | `logmaster` | -| Analyzer config updated | `logmaster` | -| User role changed | `logmaster` | -| User login/logout | `logsystem` | -| Import/export job start/end | `logsystem` | +### Shared Audit Fields -## Standard Log Schema (Shared Columns) +All log records should carry the same core metadata: -Use a shared schema for all four tables to keep instrumentation and reporting consistent. The legacy names below match existing patterns and can be reused. +- identity: `UserID`, `SessionID`, `SiteID` +- event classification: `EventID`, `ActivityID` +- change details: `TblName`, `RecID`, `FldName`, `FldValuePrev`, `FldValueNew` +- time/context: `LogDate`, optional `Context` JSON, optional `IpAddress` -| Column | Description | -| --- | --- | -| `LogID` (PK) | Auto increment primary key per table (e.g., `LogPatientID`) | -| `TblName` | Source table name | -| `RecID` | Record ID of the entity | -| `FldName` | Field name that changed (nullable for bulk events) | -| `FldValuePrev` | Previous value (string or JSON) | -| `FldValueNew` | New value (string or JSON) | -| `UserID` | Acting user ID (nullable for system actions) | -| `SiteID` | Site context | -| `DIDType` | Device identifier type | -| `DID` | Device identifier | -| `MachineID` | Workstation or host identifier | -| `SessionID` | Session identifier | -| `AppID` | Client application ID | -| `ProcessID` | Process/workflow identifier | -| `WebPageID` | UI page/context (nullable) | -| `EventID` | Event code (see catalog) | -| `ActivityID` | Action code (create/update/delete/read/etc.) | -| `Reason` | User/system reason | -| `LogDate` | Timestamp of event | -| `Context` | JSON metadata (optional but recommended) | -| `IpAddress` | Remote IP (optional but recommended) | +### Category Reference (Simplified) -Recommended: keep a JSON string in `Context` for extra details (e.g., route, request id, batch id, error message). Use size limits to avoid oversized rows. +- **Patient (`logpatient`)**: register/update/merge, consent/insurance changes, ADT events +- **Order (`logorder`)**: create/cancel/reopen, specimen lifecycle, result verify/amend/release, + QC record/override +- **Master (`logmaster`)**: test definitions, reference ranges, analyzer/integration config, + user/role/permission changes +- **System (`logsystem`)**: login/logout/failures, token lifecycle, import/export jobs, + background process events -## Event Catalog +Recommended `ActivityID` values: +`CREATE`, `UPDATE`, `DELETE`, `READ`, `MERGE`, `SPLIT`, `CANCEL`, `REOPEN`, `VERIFY`, +`AMEND`, `RETRACT`, `RELEASE`, `IMPORT`, `EXPORT`, `LOGIN`, `LOGOUT`. -### logpatient +--- -**Patient core** +## Example API Contract (Reference Pattern) -- Register patient -- Update demographics -- Merge/unmerge/split -- Identity changes (MRN, external identifiers) -- Consent grant/revoke/update -- Insurance add/update/remove -- Patient record view (if required by compliance) +These contracts are a reference pattern for standardizing audit capture and reporting. -**Visit/ADT** +### 1) Write Audit Event -- Admit, transfer, discharge -- Bed/ward/unit changes -- Visit status updates +```http +POST /api/audit/log +Content-Type: application/json +``` -**Other** +Purpose: write one normalized audit event, then route it to the correct log table. -- Patient notes/attachments added/removed -- Patient alerts/flags changes +Request example: -### logorder +```json +{ + "domain": "order", + "TblName": "patres", + "RecID": "12345", + "FldName": "Result", + "FldValuePrev": "1.0", + "FldValueNew": "1.2", + "EventID": "RESULT_VERIFIED", + "ActivityID": "VERIFY", + "Reason": "Auto verification rule passed", + "UserID": "u001", + "SiteID": "s01", + "SessionID": "sess-8f31", + "LogDate": "2026-03-17T10:21:33Z", + "Context": { + "order_id": "OID-7788", + "test_code": "GLU", + "request_id": "req-2a9" + }, + "IpAddress": "10.2.4.8" +} +``` -**Orders/tests** +Success response example: -- Create/cancel/reopen order -- Add/remove tests -- Priority changes -- Order comments added/removed +```json +{ + "status": "success", + "message": "Audit event stored", + "data": { + "table": "logorder", + "logId": 556901 + } +} +``` -**Specimen lifecycle** +Error response example: -- Collected, labeled, received, rejected -- Centrifuged, aliquoted, stored -- Disposed/expired +```json +{ + "status": "error", + "message": "Missing required field: EventID", + "data": null +} +``` -**Results** +### 2) Query Audit Logs -- Result entered/updated -- Verified/amended -- Released/retracted/corrected -- Result comments/interpretation changes -- Auto-verification override +```http +GET /api/audit/logs?domain=order&recId=12345&eventId=RESULT_VERIFIED&from=2026-03-01&to=2026-03-31 +``` -**QC** +Purpose: retrieve normalized audit history with filters for investigation and compliance. -- QC result recorded -- QC failure/override +Success response example: -### logmaster +```json +{ + "status": "success", + "data": [ + { + "logId": 556901, + "table": "logorder", + "EventID": "RESULT_VERIFIED", + "ActivityID": "VERIFY", + "TblName": "patres", + "RecID": "12345", + "FldName": "Result", + "FldValuePrev": "1.0", + "FldValueNew": "1.2", + "UserID": "u001", + "SiteID": "s01", + "LogDate": "2026-03-17T10:21:33Z" + } + ] +} +``` -**Value sets** +--- -- Create/update/retire value set items +## Capture Rules (Short) -**Test definitions** - -- Test definition updates (units, methods, ranges) -- Reference range changes -- Formula/delta check changes -- Test panel membership add/remove - -**Infrastructure** - -- Analyzer/instrument config changes -- Host app integration config -- Coding system changes - -**Users/roles** - -- User create/disable/reset -- Role changes -- Permission changes - -**Sites/workstations** - -- Site/location/workstation CRUD - -### logsystem - -**Sessions & security** - -- Login/logout -- Failed login attempts -- Lockouts/password resets -- Token issue/refresh/revoke -- Authorization failures - -**Import/export** - -- Import/export job start/end -- Batch ID, source, record counts, status - -**System operations** - -- Background jobs start/end -- Integration sync runs -- System config changes -- Service errors that affect data integrity - -## Activity & Event Codes - -Use consistent `ActivityID` and `EventID` values. Recommended defaults: - -- `ActivityID`: `CREATE`, `UPDATE`, `DELETE`, `READ`, `MERGE`, `SPLIT`, `CANCEL`, `REOPEN`, `VERIFY`, `AMEND`, `RETRACT`, `RELEASE`, `IMPORT`, `EXPORT`, `LOGIN`, `LOGOUT` -- `EventID`: domain-specific codes (e.g., `PATIENT_REGISTERED`, `ORDER_CREATED`, `RESULT_VERIFIED`, `QC_RECORDED`) - -## Capture Guidelines - -- Always capture `UserID`, `SessionID`, `SiteID`, and `LogDate` when available. -- If the action is system-driven, set `UserID` to `SYSTEM` (or null) and add context in `Context`. -- Store payload diffs in `FldValuePrev` and `FldValueNew` for single-field changes; for multi-field changes, put a JSON diff in `Context` and leave `FldName` null. -- For bulk operations, store batch metadata in `Context` (`batch_id`, `record_count`, `source`). -- Do not log secrets, tokens, or full PHI when not required. Mask or omit sensitive fields. - -## Retention & Governance - -- Define retention policy per table (e.g., 7 years for patient/order, 2 years for system). -- Archive before purge; record purge activity in `logsystem`. -- Restrict write/delete permissions to service accounts only. - -## Implementation Checklist - -1. Create the four tables with shared schema (or migrate existing log tables to match). -2. Add a single audit service with helpers to build a normalized payload. -3. Instrument controllers/services for each event category above. -4. Add automated tests for representative audit writes. -5. Document `EventID` codes used by each endpoint/service. +- Always include `UserID`, `SessionID`, `SiteID`, `EventID`, `ActivityID`, and `LogDate`. +- For multi-field updates, keep `FldName` null and store the diff in `Context`. +- Do not log secrets/tokens; mask sensitive values in payloads. +- For system-driven actions, use `UserID = SYSTEM` (or null) and explain in `Context`.