diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 199a1259..a8f2e8e1 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -91,7 +91,7 @@ dotnet test tests/Informedica.GenUNITS.Tests/ - Resource loading and tests: `src/Informedica.GenForm.Lib/Api.fs` and `tests/` - Sheet parsers: `Mapping.fs`, `Product.fs`, `DoseRule.fs`, `SolutionRule.fs`, `RenalRule.fs` - Unit and BigRational helpers: `src/Informedica.GenUnits.Lib/ValueUnit.fs` -- Sheet documentation: `docs/mdr/design-history/0003-resource-requirements.md` +- Sheet documentation: the `Data` record types in `src/Informedica.GenFORM.Lib/Types.fs` (one record per sheet, columns documented on the fields), with the column names enforced by the `ColumnContract` tests in `tests/Informedica.GenFORM.Tests/Tests.fs` **Important:** an opt-in strategy is used in the `.gitignore` file — you have to specifically define what should be included instead of the other way around! diff --git a/.github/prompts/add-dose-rule.prompt.md b/.github/prompts/add-dose-rule.prompt.md index d1e1b187..69de76f5 100644 --- a/.github/prompts/add-dose-rule.prompt.md +++ b/.github/prompts/add-dose-rule.prompt.md @@ -8,7 +8,7 @@ Add or modify a medication rule (dose rule, solution rule, product, etc.) in Gen ## Steps -1. **Understand the sheet structure** — read `docs/mdr/design-history/0003-resource-requirements.md` to find the relevant sheet and column names. +1. **Understand the sheet structure** — read the matching `Data` record in `src/Informedica.GenFORM.Lib/Types.fs` (its XML summary names the sheet and parser; the field comments carry column names, units and encodings) and the declared column lists in `DoseRuleToDataTests.ColumnContract` (`tests/Informedica.GenFORM.Tests/Tests.fs`). 2. **Locate the parser** — find the corresponding module: - Dose rules → `src/Informedica.GenForm.Lib/DoseRule.fs` diff --git a/AGENTS.md b/AGENTS.md index 9faba541..89009011 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,7 +53,13 @@ dotnet test - `dotnet run list` - Show all available build targets - `dotnet run Build` - Build the solution - `dotnet run Bundle` - Create production bundle +- `dotnet run CheckVersions` - Proves that every project shipped in GenPRES.sln reports the same version as the repo-root Directory.Build.props - `dotnet run Clean` - Clean build artifacts +- `dotnet run DockerBuild` - Builds the Docker image, version-labelled from `Directory.Build.props` +- `dotnet run DockerRun` - Runs a Docker container +- `dotnet run Format` - Uses Fantomas to format F# code +- `dotnet run MarkdownLint` - Runs the mark down linter +- `dotnet run RestoreClient` - Runs the client npm restore process - Access the application at `http://localhost:5173` ### Testing @@ -87,17 +93,18 @@ dotnet test tests/Informedica.GenUNITS.Tests/ ## Key Code Locations - F# libraries under `src/` -- Tests: `tests/` (Expecto + FsCheck). Look for BigRational and ValueUnit tests. -- Resource loading and tests: `src/Informedica.GenForm.Lib/Api.fs` and `tests/` -- Sheet parsers: `Mapping.fs`, `Product.fs`, `DoseRule.fs`, `SolutionRule.fs`, `RenalRule.fs` +- Tests: `tests/` (uses Expecto + FsCheck). +- Resource loading: `src/Informedica.GenForm.Lib/Api.fs` +- Resource parsers: `Mapping.fs`, `Product.fs`, `DoseRuleData.fs`, `SolutionRule.fs`, `RenalRule.fs` - Unit and BigRational helpers: `src/Informedica.GenUnits.Lib/ValueUnit.fs` -- Sheet documentation: `docs/mdr/design-history/0003-resource-requirements.md` +- Sheet documentation: the `Data` record types in `src/Informedica.GenFORM.Lib/Types.fs` (one record per sheet, columns documented on the fields), with the column names enforced by the `ColumnContract` tests in `tests/Informedica.GenFORM.Tests/Tests.fs` **Important:** an opt-in strategy is used in the `.gitignore` file — you have to specifically define what should be included instead of the other way around! +The same applies to the docker ignore file. ## Configuration Architecture -- All medication rules and constraints stored in Google Spreadsheets +- All medication rules and constraints (currently) stored in Google Spreadsheets - Downloaded as CSV and parsed dynamically - `GENPRES_URL_ID` environment variable controls which spreadsheet to use - Local cache files provide offline medication data access @@ -110,12 +117,12 @@ dotnet test tests/Informedica.GenUNITS.Tests/ ## Resource Loading Pattern -- Docs with sheet specs: `docs/mdr/design-history/0003-resource-requirements.md`. -- Check `0003-resource-requirements.md` for expected sheet and column names. +- Sheet specs live in code: each sheet has a record in the `Data` module of `src/Informedica.GenFORM.Lib/Types.fs`, whose XML summary names the sheet and the parser and whose field comments carry the column name (where it differs), unit, separator and boolean spelling. +- Check that record — and the declared column lists in `DoseRuleToDataTests.ColumnContract` (`tests/Informedica.GenFORM.Tests/Tests.fs`) — for expected sheet and column names. - Resources are loaded from Google Sheets via `Web.getDataFromSheet dataUrlId "SheetName"`. - Mapping helper functions use `Csv.getStringColumn` / `Csv.getFloatOptionColumn` and call getString/getFloat-style delegates. - The central `ResourceConfig` (in `Api.fs`) expects functions returning `GenFormResult<'T>` (alias for `Result<'T, Message list>`). Use the `*Result` variants where present (e.g., `Mapping.getRouteMapping` or `Mapping.getRouteMappingResult`) and wrap with `delay` when the signature expects a `unit -> GenFormResult<_>`. -- To add/modify sheet mappings: adjust the mapper in the corresponding module (e.g., `Product.Reconstitution.get`, `DoseRule.get`) and update `0003-resource-requirements.md` to reflect column names. +- To add/modify sheet mappings: adjust the mapper in the corresponding module (e.g., `Product.Reconstitution.parseReconstitution`, `DoseRuleData.parseDoseRuleData`), update the field comments on the matching `Data` record, and update the declared column list in the column-contract test. - Update the mapper to read columns by name using the `get` delegate (e.g., `let get = getColumn row in get "Generic"`), parse with `BigRational.toBrs` / `getFloat` as appropriate. - If adding optional numeric columns, use `getFloatOptionColumn` and `Option.bind BigRational.fromFloat`. @@ -381,7 +388,7 @@ FSI's `#load` directive resolves relative paths from its *include path*, **not** ## Safety, MDR and Documentation -- This project targets clinical medication workflows. Any change that affects dosing, rules, parsing, or resource mapping must include: unit tests, changelog entry, and an update to `docs/mdr/design-history/0003-resource-requirements.md` if spreadsheet columns or semantics changed. +- This project targets clinical medication workflows. Any change that affects dosing, rules, parsing, or resource mapping must include: unit tests, a changelog entry, and — if spreadsheet columns or semantics changed — updated field comments on the corresponding `Data` record in `GenFORM.Lib/Types.fs` plus an updated column-contract test. - Add notes to CONTRIBUTING.md if the change introduces a new external dependency or changes deployment behavior. ## AI/LLM Usage Policy @@ -401,7 +408,7 @@ Contributors must also disclose when code submitted in a pull request is **vibe - [ ] Small, focused change with < 300 LOC modified when possible. - [ ] Add or update unit tests covering the change. - [ ] Ensure `dotnet run servertests` passes locally for affected projects. -- [ ] Update `0003-resource-requirements.md` if spreadsheet column names or semantics change. +- [ ] Update the `Data` record comments and the column-contract test if spreadsheet column names or semantics change. - [ ] Use conventional commit message with scope and short description. ## Related Documentation @@ -412,4 +419,4 @@ Contributors must also disclose when code submitted in a pull request is **vibe - Architecture: [ARCHITECTURE.md](ARCHITECTURE.md) - Development setup: [DEVELOPMENT.md](DEVELOPMENT.md) - Contributing: [CONTRIBUTING.md](CONTRIBUTING.md) -- Domain model: `docs/domain/core-domain.md` +- Domain model: [Core Domain Model](docs/domain/core-domain.md) diff --git a/CLAUDE.md b/CLAUDE.md index 38dd2b45..6ffc7f79 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,20 +10,20 @@ At the start of each session, read these documents for full project context. ### Project governance and workflow -@AGENTS.md -@DEVELOPMENT.md -@CONTRIBUTING.md +- @AGENTS.md +- @DEVELOPMENT.md +- @CONTRIBUTING.md ### Coding standards -@.github/instructions/fsharp-coding.instructions.md -@.github/instructions/fsharp-code-formatting.instructions.md -@.github/instructions/commit-message.instructions.md +- @.github/instructions/fsharp-coding.instructions.md +- @.github/instructions/fsharp-code-formatting.instructions.md +- @.github/instructions/commit-message.instructions.md ### Domain documentation -@docs/domain/core-domain.md -@docs/domain/gensolver-from-orders-to-quantitative-solutions.md +- @docs/domain/core-domain.md +- @docs/domain/gensolver-from-orders-to-quantitative-solutions.md ### Architecture and data diff --git a/docs/data-extraction/doserule-extraction-flowchart.md b/docs/data-extraction/doserule-extraction-flowchart.md index 6b4ea7e6..5880d120 100644 --- a/docs/data-extraction/doserule-extraction-flowchart.md +++ b/docs/data-extraction/doserule-extraction-flowchart.md @@ -470,6 +470,6 @@ Each pass writes a per-generic JSON dump to its `passNJsonDir`: - `src/Informedica.GenFORM.Lib/DoseType.fs:46-98` — `DoseType.fromString` / `toDescription` (canonical `doseType` enum mirrored by Pass 2). - `src/Informedica.GenFORM.Lib/DoseRule.fs` — `getFromGetData` / `getData` / `processDoseRuleData` / `mapToDoseRule` / `addDoseLimits` / `Print.toMarkdown` / `Print.printGenerics`; the production parser Pass 5 (§6.5) bridges into. `Resources.defaultResourceConfig` / `loadAllResourcesWithConfig` in `src/Informedica.GenFORM.Lib/Api.fs`. - `data/sources/Rules/doserules.tsv` — final downstream typed-emit target (TBD, §8). -- [`docs/mdr/design-history/0003-resource-requirements.md`](../mdr/design-history/0003-resource-requirements.md) §9 — DoseRules sheet column spec, including the `IsAdult` column and its clearing invariant. +- [`src/Informedica.GenFORM.Lib/DoseRuleData.fs`](../../src/Informedica.GenFORM.Lib/DoseRuleData.fs) — `headers` (canonical DoseRules column order) and `parseDoseRuleData`; the `IsAdult` column and its clearing invariant are specified in [ADR-0021](../mdr/design-history/0021-isadult-patient-category-facet.md). - [`docs/domain/genform-free-text-to-operational-rules.md`](../domain/genform-free-text-to-operational-rules.md) §3, §5, §6.1, §6.2, **Addendum C.2** (DoseRule field spec; canonical source for `Gender = male / female` etc.). - [`docs/domain/core-domain.md`](../domain/core-domain.md) — OKRs and rule hierarchy. diff --git a/docs/discrepancies-analysis.md b/docs/discrepancies-analysis.md index f8890581..35454759 100644 --- a/docs/discrepancies-analysis.md +++ b/docs/discrepancies-analysis.md @@ -3,7 +3,7 @@ This document tracks remaining, actionable mismatches between: - the current domain documents (especially `docs/domain/core-domain.md` and `docs/domain/genform-free-text-to-operational-rules.md`), -- the resource contract documentation (`docs/mdr/design-history/0003-resource-requirements.md`), and +- the resource contract, which since 2026-08-03 lives on the `Data` record types in `src/Informedica.GenFORM.Lib/Types.fs` rather than in a document, and - the implemented types used at runtime and across the API boundary. It intentionally focuses on discrepancies that matter for correctness, shared understanding, or API interoperability. Purely internal representation choices (e.g., using a richer unit type instead of a raw number) are not treated as discrepancies unless they contradict the domain docs. @@ -17,7 +17,6 @@ It intentionally focuses on discrepancies that matter for correctness, shared un - **Documentation**: - `docs/domain/core-domain.md` - `docs/domain/genform-free-text-to-operational-rules.md` - - `docs/mdr/design-history/0003-resource-requirements.md` - **Implementation**: - `src/Informedica.GenFORM.Lib/Types.fs` - `src/Informedica.GenORDER.Lib/Types.fs` diff --git a/docs/mdr/design-history/0000-change-log.md b/docs/mdr/design-history/0000-change-log.md index ea0c80ef..97e2401c 100644 --- a/docs/mdr/design-history/0000-change-log.md +++ b/docs/mdr/design-history/0000-change-log.md @@ -22,6 +22,7 @@ Maintain this document as a reverse-chronological log of significant design chan | Date | ADR | Summary | |------|-----|---------| +| 2026-08-03 | (retires ADR-0003, ADR-0012) | Spreadsheet column specification retired as a document; the sheet contract now lives on the `Data` record types in `GenFORM.Lib/Types.fs` and is enforced by column-contract tests. See issue #411 | | 2026-04-30 | [ADR-0020](0020-fhir-r4-integration.md) | FHIR R4 EHR integration design proposed; stateless GenPRES with bidirectional MedicationRequest translation and G-Standard GPK coding | | 2026-04-27 | [ADR-0019](0019-shared-clinical-calculations.md) | Shared library clinical calculations proposed; BSA, age, and renal eGFR formulas available to both server and client | | 2026-04-26 | [ADR-0018](0018-nlp-dose-rule-extraction.md) | LLM-based dose-rule extraction pipeline proposed; multi-stage FSX pipeline with human review gate | @@ -32,13 +33,13 @@ Maintain this document as a reverse-chronological log of significant design chan | 2026-03-29 | [ADR-0013](0013-adr-template-based-navigation.md) | Template-based navigation to prescribe view accepted | | 2026-03-28 | [ADR-0009](0009-mcp-server-architecture.md) | MCP server architecture proposed | | 2026-03-25 | [ADR-0007](0007-clean-safe-architecture.md) | Clean SAFE architecture accepted (all 4 phases complete) | -| 2025-12-21 | [ADR-0012](0012-resource-verification.md) | Resource requirements verified against GenFORM implementation | +| 2025-12-21 | ADR-0012 (retired 2026-08-03) | Resource requirements verified against GenFORM implementation | | 2026-03-01 | [ADR-0008](0008-agent-architecture.md) | Agent architecture proposed | | 2024-01-01 | [ADR-0011](0011-universal-layout-overflow.md) | Universal layout overflow design accepted | | 2024-01-01 | [ADR-0010](0010-analysis-solve-order-triggers.md) | Analysis of SolveOrder trigger paths | | 2024-01-01 | [ADR-0006](0006-ui-order-view.md) | Quantitative order constraint navigation proposed | | 2024-01-01 | [ADR-0005](0005-ui-nutrition-view.md) | Nutrition view layout proposed | | 2024-01-01 | [ADR-0004](0004-ui-wireframes.md) | UI wireframes accepted | -| 2024-01-01 | [ADR-0003](0003-resource-requirements.md) | Resource requirements specification accepted | +| 2024-01-01 | ADR-0003 (retired 2026-08-03) | Resource requirements specification accepted | | 2024-01-01 | [ADR-0001](0001-system-architecture.md) | System architecture accepted | | 2021-12-02 | [ADR-0002](0002-state-of-affairs.md) | State of affairs documented | diff --git a/docs/mdr/design-history/0003-resource-requirements.md b/docs/mdr/design-history/0003-resource-requirements.md deleted file mode 100644 index eb5cb344..00000000 --- a/docs/mdr/design-history/0003-resource-requirements.md +++ /dev/null @@ -1,953 +0,0 @@ -# ADR-0003: Resource Requirements and Spreadsheet Schema - -**Date**: 2024-01-01 -**Status**: Accepted - -## Context - -GenPRES loads all medication rules, mapping tables, and configuration from Google Spreadsheets. A normative specification of required sheet names, column names, and data formats is needed to ensure consistent parsing across code changes and to support MDR traceability. - -## Decision - -Maintain a living specification document that describes every required spreadsheet sheet, its column definitions, data formats, and usage in the GenFORM library. This document is the authoritative reference for the `IResourceProvider` contract. - -## Consequences - -- Any change to a sheet name or column definition must be reflected in this document. -- Agents and contributors must check this document before adding or modifying sheet parsers. -- The specification is verified against the implementation; see [ADR-0012](0012-resource-verification.md) for the verification report. - ---- - -# GenPRES Resource Requirements Documentation - -## Overview - -GenPRES relies on external Google Spreadsheet resources to provide medication data, mapping information, and configuration settings. These spreadsheets serve as the primary data source for the application and are accessed via a configurable URL ID. - -This document describes the spreadsheet resources that implement the Operational Knowledge Rules (OKRs) defined in the [GenFORM specification](../../domain/genform-free-text-to-operational-rules.md). - -## Core Definitions - -The following definitions align with the [GenFORM domain model](../../domain/genform-free-text-to-operational-rules.md): - -| Term | Definition | -| ---- | ---------- | -| *Operational Knowledge Rule (OKR)* | A fully structured, machine-interpretable and constraint-based representation of medication knowledge used for prescribing, preparation, and administration. | -| *Dose Rule* | An OKR that defines qualitative and quantitative constraints for dosing a specific generic in a defined clinical context. | -| *Dose Limit* | A set of numeric constraints defining the minimum, maximum, or normative allowable dose. | -| *Dose Type* | The temporal category of dosing: once, onceTimed, discontinuous, timed, or continuous. | -| *Selection Constraint* | A rule element used to determine which calculation constraints apply (e.g., patient demographics, route, indication). | -| *Calculation Constraint* | A quantitative rule element used to compute dose, rate, volume, or timing. | -| *Adjustment Unit* | A patient normalization unit used to scale doses (e.g., kg for weight, m² for BSA). | -| *Reconstitution* | The process of converting a medication into an administrable form by adding a diluent. | -| *Expansion Volume* | The increase in total volume resulting from reconstitution. | -| *Solution* | The process of further adjusting concentration and volume for safe administration. | -| *Form* | The pharmaceutical form of a medication (e.g., tablet, injection, solution). Also referred to as "Shape" in some legacy contexts. | -| *Generic Product (GPK)* | An abstract pharmaceutical product definition independent of branding. | -| *Patient Category* | A classification of patients by demographic ranges (age, weight, BSA, gestational age) used to determine which rules apply. | - -## Configuration - -The spreadsheet URL is configured through the `GENPRES_URL_ID` environment variable: - -```bash -export GENPRES_URL_ID=1IZ3sbmrM4W4OuSYELRmCkdxpN9SlBI-5TLSvXWhHVmA -``` - -## Required Spreadsheet Sheets - -### 1. Routes Sheet - -**Purpose**: Maps long Z-index route names to standardized short names for medication administration routes. - -**Required Columns**: - -- `ZIndex` - Full Z-index route name (e.g., "Intravenous administration") -- `ShortDutch` - Short Dutch route name (e.g., "iv") - -**Usage**: Used by `routeMapping` function to standardize route terminology across the application. - -**Example Data**: - -| ZIndex | ShortDutch | -|--------|------------| -| Intravenous administration | iv | -| Oral administration | po | -| Subcutaneous injection | sc | - ---- - -### 2. Units Sheet - -**Purpose**: Maps long Z-index unit names to standardized short names and provides unit grouping information. - -**Required Columns**: - -- `ZIndexUnitLong` - Full Z-index unit name -- `Unit` - Short standardized unit name -- `MetaVisionUnit` - MetaVision system unit equivalent -- `Group` - Unit category/group classification - -**Usage**: Used by `unitMapping` function to standardize unit terminology and enable unit conversions. - -**Example Data**: - -| ZIndexUnitLong | Unit | MetaVisionUnit | Group | -|----------------|------|----------------|-------| -| milligram | mg | mg | Mass | -| milliliter | ml | ml | Volume | -| international unit | iu | IU | International | - ---- - -### 3. FormRoute Sheet (Form-Route Mapping) - -**Purpose**: Defines medication form constraints, dosing limits, and administration requirements based on route and pharmaceutical form combinations. - -**Required Columns**: - -- `Route` - Administration route -- `Form` - Pharmaceutical form (Form in GenFORM terminology) -- `Unit` - Base unit for the medication form -- `DoseUnit` - Base Unit for dose calculations -- `MinDoseQty` - Minimum dose quantity (optional) -- `MaxDoseQty` - Maximum dose quantity (optional) -- `MinDoseQtyKg` - Minimum dose per kilogram (optional) -- `MaxDoseQtyKg` - Maximum dose per kilogram (optional) -- `Divisible` - Divisibility factor (optional) -- `Timed` - Boolean indicating if administration runs over a time period (accepts "true"/"false") -- `Reconstitute` - Boolean indicating if reconstitution is required (accepts "true"/"false") -- `IsSolution` - Boolean indicating if the form is a solution (accepts "true"/"false") - -**Usage**: Used by `mappingFormRoute` to provide clinical decision support for medication administration. - -**Example Data**: - -| Route | Form | Unit | DoseUnit | MinDoseQty | MaxDoseQty | MinDoseQtyKg | MaxDoseQtyKg | Divisible | Timed | Reconstitute | IsSolution | -|-------|-----------|-------|----------|------------|------------|--------------|--------------|-----------|-------|--------------|------------| -| iv | injection | ml | mg | 1 | 100 | 0.1 | 10 | 0.5 | true | false | true | -| po | tablet | piece | mg | 0.5 | 50 | 0.01 | 5 | 0.25 | false | false | false | - ---- - -### 4. ValidForms Sheet (ValidForms) - -**Purpose**: Defines the complete list of valid pharmaceutical forms supported by the system. - -**Required Columns**: - -- `Form` - Valid pharmaceutical form name (Form in GenFORM terminology) - -**Usage**: Used by `validForms` function to validate pharmaceutical form inputs. - -**Example Data**: - -| Form | -|-------------| -| tablet | -| capsule | -| injection | -| solution | -| cream | -| suppository | - ---- - -### 5. Reconstitution Sheet - -**Purpose**: Provides reconstitution rules for converting medications into administrable form by adding a diluent. This sheet implements the Reconstitution Rule model from GenFORM. - -**Required Columns**: - -#### Basic Identification (Selection Constraints) - -- `GPK` - Generic Product Code identifier (Generic.GPK in GenFORM) -- `Route` - Administration route for reconstitution -- `Loc` - Organizational location / hospital / institute (Setting.Location in GenFORM, optional) -- `Dep` - Department/unit where reconstitution applies (Setting.Department in GenFORM, optional) - -#### Solution Parameters (Calculation Constraints) - -These fields correspond to the Solution object in GenFORM: - -- `DiluentVol` - Diluent volume required in mL (numeric) -- `ExpansionVol` - Expansion volume (increase in total volume resulting from reconstitution) in mL (numeric, optional) -- `Diluents` - Acceptable diluents (semicolon-separated list) - -**Usage**: Used by `Reconstitution.get` to provide reconstitution instructions for injectable products. - -**Example Data**: - -| GPK | Route | Dep | DiluentVol | ExpansionVol | Diluents | -|-------|-------|-----|------------|--------------|----------------------| -| 12345 | iv | ICU | 10 | 0.5 | NaCl 0.9%;Glucose 5% | -| 67890 | iv | NEO | 5 | 0.2 | WFI;NaCl 0.9% | - ---- - -### 6. EntFeeding Sheet - -**Purpose**: Defines enteral feeding products with nutritional composition. - -**Required Columns**: - -- `Name` - Product name -- `Eenheid` - Base unit for the product -- `volume mL` - Volume per unit (numeric) -- `energie kCal` - Energy content in kcal (numeric, optional) -- `eiwit g` - Protein content in grams (numeric, optional) -- `KH g` - Carbohydrate (koolhydraat) content in grams (numeric, optional) -- `vet g` - Fat content in grams (numeric, optional) -- `Na mmol` - Sodium content in mmol (numeric, optional) -- `K mmol` - Potassium content in mmol (numeric, optional) -- `Ca mmol` - Calcium content in mmol (numeric, optional) -- `P mmol` - Phosphate content in mmol (numeric, optional) -- `Mg mmol` - Magnesium content in mmol (numeric, optional) -- `Fe mmol` - Iron content in mmol (numeric, optional) -- `VitD IE` - Vitamin D content in International Units (numeric, optional) -- `Cl mmol` - Chloride content in mmol (numeric, optional) - -**Usage**: Used by `Enteral.get` to provide enteral nutrition products with nutritional information. - -**Example Data**: - -| Name | Eenheid | volume mL | energie kCal | eiwit g | KH g | vet g | Na mmol | -|------|---------|-----------|--------------|---------|------|-------|---------| -| Nutrison Standard | ml | 1 | 1.0 | 0.04 | 0.123 | 0.039 | 0.65 | - ---- - -### 7. ParentMeds Sheet - -**Purpose**: Defines parenteral nutrition and medication products with composition. - -**Required Columns**: - -- `GPK` - Generic Product Code identifier -- `Name` - Product name -- `volume mL` - Volume per unit (numeric) -- `glucose g` - Glucose content in grams (numeric, optional) -- `energie kCal` - Energy content in kcal (numeric, optional) -- `eiwit g` - Protein content in grams (numeric, optional) -- `koolhydraat g` - Total carbohydrate content in grams (numeric, optional) -- `vet g` - Fat content in grams (numeric, optional) -- `natrium mmol` - Sodium content in mmol (numeric, optional) -- `kalium mmol` - Potassium content in mmol (numeric, optional) -- `calcium mmol` - Calcium content in mmol (numeric, optional) -- `fosfaat mmol` - Phosphate content in mmol (numeric, optional) -- `magnesium mmol` - Magnesium content in mmol (numeric, optional) -- `ijzer mmol` - Iron content in mmol (numeric, optional) -- `VitD IE` - Vitamin D content in International Units (numeric, optional) -- `chloor mmol` - Chloride content in mmol (numeric, optional) -- `Oplosmiddel` - Indicates if product is a solvent (accepts "TRUE"/"FALSE") -- `Verdunner` - Indicates if product is a diluent (accepts "TRUE"/"FALSE") - -**Usage**: Used by `Parenteral.get` to provide parenteral nutrition and IV medication products. - -**Example Data**: - -| GPK | Name | volume mL | glucose g | natrium mmol | Oplosmiddel | Verdunner | -|-------|------------|-----------|-----------|--------------|-------------|------------| -| 99999 | Glucose 5% | 1 | 0.05 | 0 | FALSE | TRUE | - ---- - -### 8. Formulary Sheet - -**Purpose**: Defines the hospital formulary with product configurations and department availability. - -**Required Columns**: - -- `GPKODE` - Generic Product Code (numeric) -- `UMCU` - Hospital availability indicator (column name in sheet: "UMCU", field name in code: Apotheek) -- `ICC` - Intensive Care availability indicator -- `NEO` - Neonatal unit availability indicator -- `ICK` - Pediatric ICU availability indicator -- `HCK` - High Care availability indicator -- `Generic` - Generic product name -- `UseGenName` - Use generic name flag (accepts "x" for true) -- `UseForm` - Use Form in naming flag (accepts "x" for true) -- `UseBrand` - Use brand name flag (accepts "x" for true) -- `TallMan` - Tall Man lettering for safety -- `Mmol` - Molar concentration (numeric, optional) -- `Divisible` - Divisibility factor (numeric, optional) - -Sample data: - -| GPKODE | Type | UMCU | ICC | NEO | ICK | HCK | Generic | UseGenName | UseForm | UseBrand | TallMan | Mmol | Divisible | GStand | Form | Brand | GenName | Unit | Energy kCal | Carb g | Prot g | Lip g | Sod mmol | Pot mmol | Calc mmol | Posph mmol | Magn mmol | Chlor mmol | Iron mmol | VitD IE | IsReconste | IsDilute | IsAdditive | -|-----------------|------------|------|-----|-----|-----|-----|-----------------|------------|---------|----------|---------|------|-----------|----------------|-------------|-------|----------------|------|-------------|--------|--------|-------|----------|----------|-----------|------------|-----------|------------|-----------|---------|------------|----------|------------| -| 104299 | medication | | | | | | ACETYLCYSTEINE | | | | | | | ACETYLCYSTEINE | OOGDRUPPELS | | ACETYLCYSTEINE | | | | | | | | | | | | | | | | | -| Samenstelling C | parenteral | x | x | x | x | | Samenstelling C | | | | | | | | | | | mL | 0.32 | 0 | 0.08 | 0 | 0.008 | 0.02 | 0.028 | 0.024 | 0.008 | 0.072 | 0 | 0 | FALSE | FALSE | - - -**Usage**: Used by the main `get` function to create the primary product formulary with department-specific configurations. - -**Example Data**: - -| GPKODE | UMCU | ICC | NEO | Generic | UseGenName | UseForm | UseBrand | TallMan | Mmol | Divisible | -|--------|------|-----|-----|-------------|------------|---------|----------|---------|------|------------| -| 12345 | x | x | | paracetamol | x | | | | | 2 | -| 67890 | x | | x | amoxicillin | x | x | | | | | - ---- - -### 9. DoseRules Sheet - -**Purpose**: Defines clinical dosing rules and limits for medications across different patient populations, routes, and clinical scenarios. - -**Required Columns**: - -#### Dose Rule: Basic Identification (Selection Constraints) - -- `Id` - Unique dose rule identifier (opaque to consumers; GenFORM `DoseRule.Id`) -- `GrpId` - Rule-group identifier shared by every rule belonging to the same - clinical context (Source, Generic, Form, Brand, Route, Indication, Patient - Category); rules in one group differ only in dose type/schedule. GenFORM - `DoseRule.GroupId` -- `SortNo` - Ordinal rank within a rule group, for stable presentation order - (GenFORM `DoseRule.SortNo`) -- `Source` - Data source identifier (e.g., "NKF", "FK"). Parsed into the GenFORM - `Source` DU (`Identified`/`Other`) -- `Indication` - Clinical indication for the medication -- `Generic` - Generic (base) substance name. Combined at ingest with `Form` / - `Brand` into the GenFORM `Generic` record (`GenericLabel`): brand takes - precedence over form when both are present. The **base** name (without the - form/brand qualifier) is what external sources such as the G-Standaard are - keyed on for dose checking -- `Form` - Pharmaceutical form (Form in GenFORM terminology) -- `Brand` - Brand name (optional) -- `Route` - Administration route -- `GPKs` - Generic Product Codes (semicolon-separated list); narrows the attached - product components -- `HPKs` - Trade/prescription product codes (HPK; semicolon-separated). Narrows - the attached trade products (takes precedence over `GPKs`/`Brand`/`Form`) -- `SourceText` - Original source text before structured decomposition (GenFORM - `DoseRule.SourceText`) -- `PatientText` - Original free-text description of the patient category (GenFORM - `DoseRule.PatientText`) -- `ScheduleText` - Dosing schedule description (Source.Text in GenFORM) -- `CmpBased` - Component-based flag for combination products -- `Component` - Component name for combination products -- `Substance` - Active substance name - -#### Dose Rule: Setting and Patient Category (Selection Constraints) - -These fields define the clinical setting and patient category (demographic ranges) that determine rule applicability: - -- `Dep` - Department/ward (Setting.Department in GenFORM terminology) -- `Gender` - Patient gender -- `IsAdult` - Positive-only adult-confirmation flag (optional; `"x"` or empty). `"x"` means the rule applies to adults so **age is not a concern**; consequently `MinAge` / `MaxAge` must be empty for that row. Empty carries **no** meaning (it does NOT assert "not an adult"). System-resolved by the extraction pipeline (preliminary keyword at Pass 1, confirmed at Pass 3); never a negative. -- `MinAge` - Minimum age in days (numeric, optional; empty when `IsAdult = "x"`) -- `MaxAge` - Maximum age in days (numeric, optional; empty when `IsAdult = "x"`) -- `MinWeight` - Minimum weight in grams (numeric, optional) -- `MaxWeight` - Maximum weight in grams (numeric, optional) -- `MinBSA` - Minimum body surface area in m² (numeric, optional) -- `MaxBSA` - Maximum body surface area in m² (numeric, optional) -- `MinGestAge` - Minimum gestational age in days (numeric, optional) -- `MaxGestAge` - Maximum gestational age in days (numeric, optional) -- `MinPMAge` - Minimum post-menstrual age in days (numeric, optional) -- `MaxPMAge` - Maximum post-menstrual age in days (numeric, optional) - -> **Note (`IsAdult` — extraction-side today; blocking precondition before ingest).** This column is produced and enforced by the FTK extraction pipeline (`ftk_extract_v2.fsx`; see [`docs/data-extraction/doserule-extraction-flowchart.md`](../../data-extraction/doserule-extraction-flowchart.md)). It is **not yet consumed** by the GenFORM `.fs` source — `DoseRuleData` (`Types.fs`), the `DoseRule.getData` sheet parser, and the `PatientCategory` domain type currently use the numeric age ranges only. Because the pipeline empties `MinAge`/`MaxAge` when `IsAdult = "x"`, such a row has no age bound and an unconsumed flag, so **no `IsAdult = "x"` row may reach GenFORM ingest** until a maintainer change either parses the flag into `DoseRuleData` *and* enforces "adult patients only" in patient-matching, or treats it as a typed-emit-time assertion with an equivalent matching guard. Design rationale and rejected alternatives: [ADR-0021](0021-isadult-patient-category-facet.md). - -#### Dose Rule: Dose Configuration (Selection and Calculation Constraints) - -- `DoseType` - Temporal category of dosing. Valid values: "once", "onceTimed", "discontinuous", "timed", or "continuous" -- `DoseText` - Dose type description text (DoseType.DoseText in GenFORM, can be empty) -- `Freqs` - Schedule frequencies (semicolon-separated numeric values) -- `DoseUnit` - Base dose unit for DoseLimit -- `AdjustUnit` - Patient adjustment unit (e.g., "kg" for weight, "m2" for BSA) -- `FreqUnit` - Frequency unit for Schedule (e.g., "day", "hour") -- `RateUnit` - Rate unit for DoseLimit (e.g., "hour", "min") - -#### Dose Rule: Schedule Parameters (Calculation Constraints) - -These fields correspond to the Schedule object in GenFORM: - -- `MinTime` - Minimum time for infusion of a dose (numeric, optional) -- `MaxTime` - Maximum time for infusion of a dose (numeric, optional) -- `TimeUnit` - Time unit for infusion measurement -- `MinInt` - Minimum interval between two doses (numeric, optional) -- `MaxInt` - Maximum interval between two doses (numeric, optional) -- `IntUnit` - Interval unit -- `MinDur` - Minimum duration of the dose rule (numeric, optional) -- `MaxDur` - Maximum duration of the dose rule (numeric, optional) -- `DurUnit` - Duration time unit - -#### Dose Rule: Dose Limits (Calculation Constraints) - -These fields correspond to the DoseLimit object in GenFORM: - -- `MinQty` - Minimum dose quantity (numeric, optional) -- `MaxQty` - Maximum dose quantity (numeric, optional) -- `NormQtyAdj` - Normal patient-adjusted dose quantity (numeric, optional) -- `MinQtyAdj` - Minimum patient-adjusted dose quantity (numeric, optional) -- `MaxQtyAdj` - Maximum patient-adjusted dose quantity (numeric, optional) -- `MinPerTime` - Minimum dose quantity per time (numeric, optional) -- `MaxPerTime` - Maximum dose quantity per time (numeric, optional) -- `NormPerTimeAdj` - Normal adjusted dose per time (numeric, optional) -- `MinPerTimeAdj` - Minimum adjusted dose per time (numeric, optional) -- `MaxPerTimeAdj` - Maximum adjusted dose per time (numeric, optional) -- `MinRate` - Minimum rate (numeric, optional) -- `MaxRate` - Maximum rate (numeric, optional) -- `MinRateAdj` - Minimum adjusted rate (numeric, optional) -- `MaxRateAdj` - Maximum adjusted rate (numeric, optional) - -**Usage**: Used by `DoseRule.get` to provide comprehensive clinical dosing guidance with patient-specific limits and safety constraints. - -**Example Data**: - -| Source | Generic | Form | Route | Indication | DoseType | MinWeight | MaxWeight | DoseUnit | MinQty | MaxQty | AdjustUnit | MinQtyAdj | MaxQtyAdj | -|--------|-------------|------------|-------|------------|---------------|-----------|-----------|----------|--------|--------|------------|-----------|------------| -| NKF | paracetamol | tablet | po | fever | discontinous | 10 | 50 | mg | 500 | 1000 | kg | 10 | 15 | -| NKF | amoxicillin | suspension | po | infection | discontinuous | 5 | 80 | mg | | | kg | 25 | 50 | - ---- - -### 10. SolutionRules Sheet - -**Purpose**: Defines solution rules for IV solution preparation, including diluent requirements and concentration limits for injectable medications. This sheet implements the Solution Rule model from GenFORM. - -**Required Columns**: - -#### Solution Rule: Basic Identification (Selection Constraints) - -- `Generic` - Generic medication name -- `Form` - Pharmaceutical form (Form in GenFORM terminology, optional) -- `Route` - Administration route -- `Indication` - Clinical indication (optional, must match corresponding Dose Rule) - -#### Solution Rule: Setting, Administration Access Device, and Patient Category (Selection Constraints) - -- `Dep` - Department/ward (Setting.Department in GenFORM terminology, optional) -- `CVL` - Administration access device: Central Venous Line (accepts "x" for true) -- `PVL` - Administration access device: Peripheral Venous Line (accepts "x" for true) -- `MinAge` - Minimum age in days (numeric, optional) -- `MaxAge` - Maximum age in days (numeric, optional) -- `MinWeight` - Minimum weight in grams (numeric, optional) -- `MaxWeight` - Maximum weight in grams (numeric, optional) -- `MinGestAge` - Minimum gestational age in days (numeric, optional) -- `MaxGestAge` - Maximum gestational age in days (numeric, optional) - -#### Solution Rule: Dose Configuration (Selection Constraints) - -- `MinDose` - Minimum dose (SolutionRule.MinDose in GenFORM, numeric, optional) -- `MaxDose` - Maximum dose (SolutionRule.MaxDose in GenFORM, numeric, optional) -- `DoseType` - Temporal category of dosing: "once", "onceTimed", "discontinuous", "timed", or "continuous" - -#### Solution Rule: Solution Parameters (Calculation Constraints) - -These fields correspond to the SolutionRule object in GenFORM: - -- `Solutions` - Acceptable diluents (pipe-separated list: "solution1|solution2") -- `Volumes` - Standard volume quantities (semicolon-separated numeric values in mL) -- `MinVol` - Minimum volume in mL (numeric, optional) -- `MaxVol` - Maximum volume in mL (numeric, optional) -- `MinVolAdj` - Minimum volume per kg in mL/kg (numeric, optional) -- `MaxVolAdj` - Maximum volume per kg in mL/kg (numeric, optional) -- `MinPerc` - Minimum percentage of solution for DoseQuantity (Administration Fraction min, numeric, optional) -- `MaxPerc` - Maximum percentage of solution for DoseQuantity (Administration Fraction max, numeric, optional) - -#### Solution Rule: Solution Limits (Calculation Constraints) - -These fields correspond to the SolutionLimit object in GenFORM: - -- `Substance` - Active substance name (for concentration limits) -- `Unit` - Substance unit (SubstUnit in GenFORM) -- `Quantities` - Standard substance quantities (semicolon-separated numeric values) -- `MinQty` - Minimum substance quantity (numeric, optional) -- `MaxQty` - Maximum substance quantity (numeric, optional) -- `MinDrip` - Minimum infusion rate in mL/hour (numeric, optional) -- `MaxDrip` - Maximum infusion rate in mL/hour (numeric, optional) -- `MinConc` - Minimum substance concentration in SubstUnit/mL (numeric, optional) -- `MaxConc` - Maximum substance concentration in SubstUnit/mL (numeric, optional) - -**Usage**: Used by `SolutionRule.get` (implements Solution Rule) to provide IV solution preparation guidance with concentration limits and diluent requirements. - -**Example Data**: - -| Generic | Form | Route | Indication | Dep | CVL | PVL | MinWeight | MaxWeight | DoseType | Solutions | Volumes | MinVol | MaxVol | Substance | Unit | MinConc | MaxConc | -|----------|-----------|-------|------------|-----|-----|-----|-----------|-----------|-------------|-----------------------|------------|--------|--------|-----------|------|---------|----------| -| dopamine | injection | iv | shock | ICU | x | | 1 | 80 | maintenance | NaCl 0.9%\|Glucose 5% | 50;100;250 | 50 | 250 | dopamine | mg | 0.4 | 3.2 | -| morphine | injection | iv | pain | | x | x | 10 | | maintenance | NaCl 0.9% | 50;100 | 50 | 100 | morphine | mg | 0.1 | 1 | - ---- - -### 11. RenalRules Sheet - -**Purpose**: Defines renal dose adjustments for medications based on kidney function, dialysis status, and patient characteristics. This sheet implements the Renal Rule model from GenFORM. - -**Required Columns**: - -#### Renal Rule: Basic Identification (Selection Constraints) - -- `Generic` - Generic medication name -- `Route` - Administration route -- `Indication` - Clinical indication (optional, must match corresponding Dose Rule) -- `Source` - Data source identifier (e.g., "NKF", "KI/DOQI") - -#### Renal Rule: Patient Category (Selection Constraints) - -- `MinAge` - Minimum age in days (numeric, optional) -- `MaxAge` - Maximum age in days (numeric, optional) - -#### Renal Rule: Renal Function Parameters (Selection Constraints) - -These fields correspond to the Renal object in GenFORM: - -- `IntDial` - Intermittent hemodialysis (accepts "x" for applies) -- `ContDial` - Continuous hemodialysis (accepts "x" for applies) -- `PerDial` - Peritoneal dialysis (accepts "x" for applies) -- `MinGFR` - Minimum standardized GFR in mL/min/1.73m² (numeric, optional) -- `MaxGFR` - Maximum standardized GFR in mL/min/1.73m² (numeric, optional) - -#### Renal Rule: Dose Configuration (Selection and Calculation Constraints) - -- `DoseType` - Temporal category of dosing: "once", "onceTimed", "discontinuous", "timed", or "continuous" -- `DoseText` - Dose type description (DoseType.DoseText in GenFORM) -- `Freqs` - Schedule frequencies (semicolon-separated numeric values) -- `DoseRed` - Dose adjustment type: "rel" for Relative Adjustment, "abs" for Absolute Adjustment -- `DoseUnit` - Base dose unit -- `AdjustUnit` - Patient adjustment unit (e.g., "kg", "m2") -- `FreqUnit` - Frequency unit (e.g., "day", "hour") -- `RateUnit` - Rate unit (e.g., "hour", "min") - -#### Renal Rule: Schedule Parameters (Calculation Constraints) - -These fields correspond to the Schedule object in GenFORM: - -- `MinInt` - Minimum interval duration (numeric, optional) -- `MaxInt` - Maximum interval duration (numeric, optional) -- `IntUnit` - Interval time unit - -#### Renal Rule: Substance and Adjustment (Selection and Calculation Constraints) - -- `Substance` - Active substance name (must match corresponding Dose Rule) - -#### Dose Limits (Calculation Constraints - Renal Adjusted) - -These fields implement the Adjustment object in GenFORM. Values are either relative (multiplier) or absolute based on `DoseRed`: - -- `MinQty` - Minimum dose quantity (numeric, optional) -- `MaxQty` - Maximum dose quantity (numeric, optional) -- `NormQtyAdj` - Normal patient-adjusted dose quantity (space-dash-space separated values: "val1 - val2") -- `MinQtyAdj` - Minimum patient-adjusted dose quantity (numeric, optional) -- `MaxQtyAdj` - Maximum patient-adjusted dose quantity (numeric, optional) -- `MinPerTime` - Minimum dose per time (numeric, optional) -- `MaxPerTime` - Maximum dose per time (numeric, optional) -- `NormPerTimeAdj` - Normal patient-adjusted dose per time (space-dash-space separated values) -- `MinPerTimeAdj` - Minimum patient-adjusted dose per time (numeric, optional) -- `MaxPerTimeAdj` - Maximum patient-adjusted dose per time (numeric, optional) -- `MinRate` - Minimum dose rate (numeric, optional) -- `MaxRate` - Maximum dose rate (numeric, optional) -- `MinRateAdj` - Minimum patient-adjusted dose rate (numeric, optional) -- `MaxRateAdj` - Maximum patient-adjusted dose rate (numeric, optional) - -**Usage**: Used by `RenalRule.get` to provide renal dose adjustments based on kidney function and dialysis status. Rules are applied to patients ≥28 days of age with impaired renal function. - -**Example Data**: - -| Generic | Route | Source | MinGFR | MaxGFR | ContDial | IntDial | PerDial | DoseType | DoseRed | Substance | DoseUnit | AdjustUnit | MinQtyAdj | MaxQtyAdj | NormQtyAdj | -|------------|-------|---------|--------|--------|----------|---------|---------|---------------|---------|------------|----------|------------|-----------|-----------|-------------| -| gentamicin | iv | NKF | 30 | 59 | | | | discontinuous | abs | gentamicin | mg | kg | 3 | 5 | 4 - 5 | -| digoxin | po | KI/DOQI | | 50 | | | | discontinuous | rel | digoxin | mcg | | | | 0.5 | -| vancomycin | iv | NKF | | | x | | | discontinuous | abs | vancomycin | mg | kg | 10 | 15 | 15 | - ---- - -### 12. Emergency Treatment Sheets - -**Purpose**: Defines emergency treatment protocols and bolus medication data for resuscitation scenarios. - -> **Implementation Note**: Emergency treatment data loading is currently handled in the client application (`EmergencyList.fs`), not in the GenFORM.Lib library. The sheets are documented here for completeness and future migration to the library layer. - -#### 12A. Bolus Medication Data Sheet - -**Required Columns**: - -- `hospital` - Hospital identifier -- `indication` - Clinical indication (e.g., "reanimatie", "shock") -- `medication` - Medication name -- `minWeight` - Minimum weight for application (numeric) -- `maxWeight` - Maximum weight for application (numeric) -- `dose` - Normal dose per kg (numeric) -- `min` - Minimum absolute dose (numeric) -- `max` - Maximum absulute dose (numeric) -- `conc` - Concentration (numeric) -- `unit` - Unit of measurement -- `remark` - Additional remarks or instructions - -**Usage**: Used by `EmergencyTreatment.parse` to create bolus medication protocols for emergency scenarios. - -**Example Data**: - -| hospital | indication | medication | minWeight | maxWeight | dose | min | max | conc | unit | remark | -|----------|-------------|------------|-----------|-----------|------|-----|-----|------|--------|--------| -| UMCU | intubation | fentanyl | 0 | 0 | 1 | 0 | 50 | 50 | microg | | -| UMCU | bradycardia | atropin | 0 | 0 | 0.02 | 0.1 | 1 | 0.5 | mg | | - -#### 12B. Continuous Medication Data Sheet - -**Required Columns**: - -- `hospital` - Hospital identifier -- `catagory` - Category of medication -- `indication` - Clinical indication -- `dosetype` - Type of dose (e.g., "start", "maintenance") -- `medication` - Medication name -- `generic` - Generic name -- `unit` - Unit of measurement -- `doseunit` - Dose unit (e.g., "mg/kg/uur") -- `minweight` - Minimum weight for application (numeric) -- `maxweight` - Maximum weight for application (numeric) -- `quantity` - Quantity in solution (numeric) -- `total` - Total volume (numeric) -- `mindose` - Minimum dose rate (numeric) -- `maxdose` - Maximum dose rate (numeric) -- `absmax` - Absolute maximum dose (numeric) -- `minconc` - Minimum concentration (numeric) -- `maxconc` - Maximum concentration (numeric) -- `solution` - Solution type - -**Usage**: Used by `ContinuousMedication.parse` to create continuous infusion protocols. - -**Example Data**: - -| hospital | catagory | indication | dosetype | medication | generic | unit | doseunit | minweight | maxweight | quantity | total | mindose | maxdose | absmax | solution | -|----------|-----------------|------------|----------|---------------|---------------|------|---------------|-----------|-----------|----------|-------|---------|---------|--------|------------| -| UMCU | cardiovasculair | shock | start | noradrenaline | noradrenaline | mg | microg/kg/min | 3 | 80 | 4 | 50 | 0.05 | 2 | 10 | NaCl 0.9% | - -#### 12C. Products Data Sheet - -**Required Columns**: - -- `indication` - Clinical indication -- `medication` - Medication name -- `conc` - Concentration (numeric) -- `unit` - Unit of measurement - -**Usage**: Used by `Products.parse` to define available medication products and their concentrations. - -**Example Data**: - -| indication | medication | conc | unit | -|------------|------------|------|------| -| reanimatie | adrenaline | 0.1 | mg | -| shock | noradrenaline | 1 | mg | - -#### 12D. Normal Values Data Sheet - -**Required Columns**: - -- `sex` - Gender ("M" or "F") -- `age` - Age in years (numeric) -- `p3` - 3rd percentile value (numeric) -- `mean` - Mean value (numeric) -- `p97` - 97th percentile value (numeric) - -**Usage**: Used by `NormalValues.parse` to provide reference ranges for weight and height estimation. - -**Example Data**: - -| sex | age | p3 | mean | p97 | -|-----|-----|-----|------|-----| -| M | 0.5 | 2.5 | 3.5 | 4.5 | -| F | 0.5 | 2.3 | 3.3 | 4.3 | - ---- - -## Data Access Pattern - -All sheets are accessed through the following pattern: - -1. **URL Construction**: Uses `Web.getDataUrlIdGenPres()` to get the configured URL ID -2. **Data Retrieval**: Uses `Web.getDataFromSheet dataUrlId "SheetName"` to fetch sheet data -3. **Column Mapping**: Uses `Csv.getStringColumn` and `Csv.getFloatColumn` for data extraction -4. **Header Processing**: First row is treated as headers, subsequent rows as data -5. **Data Parsing**: Parse functions use `getString` and `getFloat` helper functions to extract column data -6. **Caching**: Some functions use `Memoization.memoize` for performance optimization - -## Data Requirements - -### Data Quality Standards - -- **Consistency**: All text fields should use consistent casing and spelling -- **Completeness**: Required fields must not be empty -- **Validation**: Unit names must be valid according to the GenUnits library -- **Standardization**: Route and pharmaceutical form names should follow established medical terminology -- **Boolean Fields**: Must use "true"/"false" or "x" for flags (case-insensitive) -- **Numeric Fields**: Must be valid numbers for concentration and quantity fields -- **Semicolon Separation**: Multi-value fields use semicolon separation -- **Pipe Separation**: Solution names use pipe separation ("|") -- **Space-Dash-Space Separation**: Range values use " - " separation for renal rules -- **Clinical Validation**: Dose limits must be clinically appropriate and evidence-based -- **Concentration Safety**: Solution concentration limits must ensure patient safety -- **Renal Safety**: Renal adjustments must be based on established nephrology guidelines -- **Emergency Protocols**: Emergency treatment data must follow established resuscitation guidelines - -### Performance Considerations - -- **Caching**: Functions that access these sheets implement memoization for performance -- **Data Size**: Sheets should be optimized for reasonable loading times -- **Update Frequency**: Changes to sheets require application restart or cache invalidation -- **Deduplication**: DoseRules sheet uses distinct filtering to prevent duplicate entries -- **Complex Processing**: SolutionRules and RenalRules undergo complex grouping and filtering operations -- **Age Restrictions**: RenalRules only apply to patients ≥28 days of age -- **Emergency Data**: Emergency treatment sheets require rapid access for critical care scenarios - -### Security and Access - -- **Read Access**: Application requires read access to the Google Spreadsheet -- **API Limits**: Consider Google Sheets API rate limits for frequent access -- **Backup**: Maintain backup copies of critical data sheets -- **Version Control**: Track changes to data sheets for audit purposes -- **Clinical Governance**: Dose rule changes require clinical review and approval -- **IV Safety**: Solution rules require specialized clinical validation -- **Nephrology Review**: Renal adjustment rules require nephrology specialist approval -- **Emergency Review**: Emergency treatment protocols require intensive care specialist approval - -## Error Handling - -The application should gracefully handle: - -- **Missing Sheets**: Return appropriate defaults or error messages -- **Missing Columns**: Log warnings and continue with available data -- **Invalid Data**: Validate and sanitize input data -- **Network Issues**: Implement retry logic for sheet access failures -- **Clinical Data Issues**: Validate dose ranges and clinical appropriateness -- **Solution Compatibility**: Validate diluent compatibility with medications -- **Renal Function Validation**: Ensure GFR values and dialysis flags are clinically appropriate -- **Emergency Data Validation**: Ensure emergency protocols are clinically safe and current - -## Development vs Production - -- **Demo Mode**: Uses `GENPRES_PROD=0` with sample data -- **Production Mode**: Uses full proprietary medication database -- **Cache Files**: Production may use local cache files for performance -- **Data Updates**: Production requires careful data validation before updates -- **Clinical Review**: All dose rule changes require clinical pharmacist approval -- **IV Preparation**: Solution rules require specialized pharmacy validation -- **Renal Guidelines**: Renal rules must align with current nephrology practice guidelines -- **Emergency Protocols**: Emergency treatment data must align with current resuscitation guidelines - -## Medical Device Regulation (MDR) Compliance - -This documentation is part of the Design History File (DHF) for GenPRES, supporting MDR compliance requirements: - -- **Traceability**: Documents data sources and their validation requirements -- **Risk Management**: Identifies data quality risks and mitigation strategies -- **Change Control**: Establishes procedures for data updates and validation -- **Verification**: Provides basis for data verification and validation testing -- **Clinical Evidence**: Dose rules are based on established clinical guidelines and evidence -- **IV Safety**: Solution rules follow established pharmaceutical compounding standards -- **Renal Safety**: Renal adjustment rules follow established nephrology guidelines and evidence-based practice -- **Emergency Care**: Emergency treatment protocols follow established resuscitation and critical care guidelines - -## Related Documents - -This document describes the spreadsheet resources implementing the Operational Knowledge Rules (OKRs) defined in the GenFORM specification. - -| Document | Description | Relationship | -|----------------------------------------------------------------------------------|----------------------------------------------------------|-----------------------------------------------------------------------| -| [GenFORM Specification](../../domain/genform-free-text-to-operational-rules.md) | Free text to Operational Knowledge Rules transformation | **Reference specification** - defines rule models and terminology | -| [Core Domain Model](../../domain/core-domain.md) | Central domain definitions and transformation pipeline | GenFORM is Layer 1 in the pipeline | -| [GenORDER](../../domain/genorder-operational-rules-to-orders.md) | Transforms OKRs to Order Scenarios | Consumes rules defined in these spreadsheets | -| [GenSOLVER](../../domain/gensolver-from-orders-to-quantitative-solutions.md) | Constraint solving engine | Provides algorithmic foundation for rule application | - -## Terminology Mapping - -For backward compatibility, some spreadsheet column names differ from GenFORM terminology: - -| Spreadsheet Term | GenFORM Term | Notes | -|------------------|--------------|-------| -| Form | Form | Pharmaceutical form | -| Dep | Setting.Department | Department/ward | -| SolutionRules | Solution Rules | IV solution preparation rules | -| CVL/PVL | Administration Access Device | Central/Peripheral Venous Line | -| DoseRed | Adjustment | Relative or Absolute dose adjustment | -| eGFR | Standardized GFR | mL/min/1.73m² | -| maintenance/start/max | DoseType values | Use: once, onceTimed, discontinuous, timed, continuous | - -## Design History: Shape → Form Migration - -### Date: December 21, 2025 - -### Change Description - -Completed systematic migration from "Shape" to "Form" terminology throughout the GenPRES codebase to align with medical terminology and GenFORM domain model specification. - -### Rationale - -1. **Medical Terminology**: "Pharmaceutical form" is the standard medical/pharmaceutical term -2. **Domain Model Alignment**: GenFORM specification consistently uses "Form" and "Pharmaceutical Form" -3. **Industry Standards**: Pharmaceutical industry documentation uses "form" terminology -4. **Documentation Consistency**: All domain documentation uses "Form" terminology - -### Implementation Status: COMPLETE - -#### Type Definitions - All Updated - -**GenFORM.Lib** (`src/Informedica.GenFORM.Lib/Types.fs`): -- ✓ `FormRoute.Form` (line 39) - "The pharmaceutical form" -- ✓ `Product.UseForm` (line 143) - "Use pharmaceutical form" -- ✓ `Product.Form` (line 155) - "The pharmaceutical form of the Product" -- ✓ `FormularyProduct.UseForm` (line 187) -- ✓ `LimitTarget.OrderableTarget` (line 204) - discriminated union case -- ✓ `DoseRuleData.Form` (line 346) -- ✓ `DoseRule.Form` (line 417) - "The pharmaceutical pharmaceutical form of the DoseRule" -- ✓ `SolutionRule.Form` (line 469) - "The pharmaceutical form of the SolutionRule" -- ✓ `ProductFilter.Form` (line 550) -- ✓ `DoseFilter.Form` (line 564) - "the pharmaceutical form to filter on" -- ✓ `SolutionFilter.Form` (line 582) - "The pharmaceutical form of the SolutionRule" -- ✓ `Product.Form` field - -**GenORDER.Lib** (`src/Informedica.GenORDER.Lib/Types.fs`): - -- ✓ `Component.Form` (line 162) - "The pharmaceutical form of a component" -- ✓ `ProductComponent.Form` (line 336) - "The pharmaceutical form of the product" -- ✓ `OrderScenario.Form` (line 437) - "The pharmaceutical form of the order" -- ✓ `Filter.Forms` (line 483) - "The list of pharmaceutical forms to select from" -- ✓ `Filter.Form` (line 497) - "The selected pharmaceutical form" - -**ZIndex.Lib**: - -- ✓ `GenPresProduct.Form` field (line 280) -- ✓ `GenericProduct.Form` field (line 260) - -**GenPRES.Shared**: - -- ✓ `OrderScenario.Form`, `Filter.Forms`, `Filter.Form` fields - -#### Script Files - All Updated - -All F# script files updated to use `.Form` instead of `.Shape`: - -- ✓ `src/Informedica.GenFORM.Lib/Scripts/Scripts.fsx` -- ✓ `src/Informedica.GenFORM.Lib/Scripts/Check.fsx` -- ✓ `src/Informedica.GenORDER.Lib/Scripts/Medication.fsx` -- ✓ `src/Informedica.GenORDER.Lib/Scripts/Scenarios.fsx` -- ✓ `src/Informedica.GenORDER.Lib/Notebooks/total-parenteral-nutrition.dib` -- ✓ `src/Informedica.GenORDER.Lib/Notebooks/total-parenteral-nutritin.ipynb` -- ✓ `src/Informedica.ZIndex.Lib/Scripts/Tests.fsx` -- ✓ `src/Informedica.ZIndex.Lib/Scripts/Formulary.fsx` -- ✓ `src/Informedica.ZIndex.Lib/code-review.md` - -#### Documentation - Verified Consistent - -- ✓ Domain documentation (`docs/domain/*.md`) consistently uses "Pharmaceutical Form" and "Form" -- ✓ All type definitions include proper documentation comments using "pharmaceutical form" -- ✓ No references to "Shape" for pharmaceutical forms remain in domain documentation - -### Backward Compatibility - -**Spreadsheet Data Sources**: The current sheet contract is aligned with the implementation and uses **Form** naming: - -- `FormRoute` sheet uses `Form` -- `ValidForms` sheet name and `Form` column -- `DoseRules` sheet uses `Form` -- `SolutionRules`/Solution rules sheet uses `Form` -- `Formulary` sheet uses `UseForm` - -If legacy datasets still contain `Shape`/`ValidShapes`/`UseShape`, they must be migrated (or handled explicitly in the loader) before they will load correctly. - -### Impact Assessment - -**Breaking Changes**: - -- Type field names changed from `Shape` to `Form` in all F# types -- Any external code referencing `.Shape` properties will need to update to `.Form` - -**Non-Breaking**: - -- Spreadsheet schema unchanged (still uses "Shape" columns) -- Data mapping logic preserved -- Clinical functionality unchanged - -### Verification - -1. ✓ All type definitions reviewed and confirmed to use "Form" -2. ✓ All script files updated and tested -3. ✓ Domain documentation alignment verified -4. ✓ Resource sheet contract uses Form naming -5. ✓ Code comments updated to reflect "pharmaceutical form" terminology - -### Risk Analysis - -**Risk**: Terminology inconsistency between code and data sources -**Mitigation**: Comprehensive documentation in this file explaining the mapping - -**Risk**: Breaking changes for external consumers -**Mitigation**: Version-controlled change with clear documentation - -**Risk**: Confusion during development/maintenance -**Mitigation**: Terminology mapping table (above) clarifies the translation - -### Regulatory Impact - -This change supports MDR compliance by: - -- Improving alignment with medical terminology standards -- Enhancing documentation clarity for clinical validation -- Maintaining traceability through design history documentation -- Supporting usability by using familiar pharmaceutical terminology - -### Related Changes - -- Discrepancies analysis document updated (`docs/discrepancies-analysis.md`) -- Shape → Form migration marked as complete in appendix -- Documentation recommendations updated to reflect completed migration - -## Design History: GenFORM v2 Domain-Model Rewrite - -### Date: June 7, 2026 - -### Change Description - -Migrated the GenFORM.Lib v2 domain-model rewrite into source. The change is a -type-model rewrite, not a sheet-schema rewrite — most spreadsheet columns are -unchanged. The schema-relevant points are recorded here for traceability. - -### Spreadsheet schema impact - -**DoseRules sheet** (Section 9) gained columns now parsed by -`DoseRuleData`/`DoseRule.getData`: - -- `Id`, `GrpId`, `SortNo` — rule provenance / grouping / ordering. -- `SourceText`, `PatientText` — original free text behind the structured row. -- `HPKs` — trade-product (HPK) narrowing, alongside the existing `GPKs`. -- `CmpBased` — component-based flag for combination products. - -`Generic` + `Form` + `Brand` are combined at ingest into the GenFORM `Generic` -record (`GenericLabel` = `Shorthand` / `Canonical` / `GenericForm` / -`GenericBrand`), with brand taking precedence over form when both restrict. - -**Formulary sheet** (Section 8) is unchanged. The existing `GStand` column now -maps to `FormularyProduct.GStandName` (the `UseForm` / `UseBrand` columns are -still read by the sheet contract but are no longer surfaced as type fields). - -### G-Standaard dose-check semantics (safety-relevant) - -Dose checking (`Check.fs` → `GStand.createDoseRules`) and solution/renal rule -matching key on the **base** generic substance name, not the display label. A -branded rule such as `GenericBrand("glycopyrronium", "Sialanar")` resolves to -`"glycopyrronium"` for these lookups (via `Generic.genericName`), while the full -label (`"glycopyrronium (Sialanar)"`) is retained for selection and display. -Regression test: `GenericLabel name vs label` in -`tests/Informedica.GenFORM.Tests`. - -### Type-model changes (no sheet impact) - -- `Product` → `ProductComponent` (adds `TradeProducts`; drops - `UseGenericName`/`UseForm`/`UseBrand`/flat `Product`). -- `DoseRule` adds `Id`/`DataId`/`GroupId`/`SortNo`/`SourceText`/`PatientText`; - `Generic` is a record, `Source` a DU, `RenalRule` → `RenalRuleSource`. -- `PatientCategory.Age`: `MinMax` → `Age` DU (`AbsoluteAge` / `IsAdult`). -- `ComponentLimit.GPKs` → `ProductIds: ProductId[]`. - -### Verification - -- `dotnet build GenPRES.sln` clean across the solution. -- `dotnet run servertests` green (GenFORM, GenORDER, Server, GenSOLVER, …); the - one unrelated red is `ZIndex.Tests` requiring the proprietary - `data/zindex/BST052T` file absent from the checkout. diff --git a/docs/mdr/design-history/0012-resource-verification.md b/docs/mdr/design-history/0012-resource-verification.md deleted file mode 100644 index a0e84196..00000000 --- a/docs/mdr/design-history/0012-resource-verification.md +++ /dev/null @@ -1,200 +0,0 @@ -# ADR-0012: Resource Requirements Verification Report - -**Date**: 2025-12-21 -**Status**: Accepted - -## Context - -After significant development on the GenFORM library, the resource requirements specification ([ADR-0003](0003-resource-requirements.md)) needed to be validated against the actual implementation to confirm accuracy and identify any discrepancies or clarifications needed. - -## Decision - -Perform a systematic verification of every sheet and column definition in [ADR-0003](0003-resource-requirements.md) against the actual column names and parsing logic in the GenFORM source files. Document the results, including confirmed matches, clarifications added, and any deviations found. - -## Consequences - -- ADR-0003 is confirmed as highly accurate for all core sheets. -- Minor clarifications were added (UMCU/Apotheek naming, KH abbreviation, koolhydraat usage). -- Emergency Treatment sheets are documented as implemented in client code rather than GenFORM.Lib. -- Future changes to sheet parsers must keep ADR-0003 in sync. - ---- - -# Resource Requirements Verification Report - -**Date**: December 21, 2025 -**Verified Against**: GenFORM.Lib implementation (commit: current) - -## Summary - -This document records the verification of `0003-resource-requirements.md` against the actual implementation in the GenFORM library. - -## Verification Results - -### ✅ Verified Correct Implementations - -The following sheets and their column definitions match the implementation exactly: - -1. **Routes Sheet** (`Mapping.fs` - `getRouteMapping`) - - ✓ Columns: `ZIndex`, `ShortDutch` - - ✓ Sheet name: "Routes" - -2. **Units Sheet** (`Mapping.fs` - `getUnitMapping`) - - ✓ Columns: `ZIndexUnitLong`, `Unit`, `MetaVisionUnit`, `Group` - - ✓ Sheet name: "Units" - -3. **FormRoute Sheet** (`Mapping.fs` - `getFormRoutes`) - - ✓ Columns: `Route`, `Form`, `Unit`, `DoseUnit`, `MinDoseQty`, `MaxDoseQty`, `MinDoseQtyKg`, `MaxDoseQtyKg`, `Divisible`, `Timed`, `Reconstitute`, `IsSolution` - - ✓ Sheet name: "FormRoute" - -4. **ValidForms Sheet** (`Mapping.fs` - `getValidForms`) - - ✓ Column: `Form` - - ✓ Sheet name: "ValidForms" - -5. **Reconstitution Sheet** (`Product.fs` - `Reconstitution.get`) - - ✓ Columns: `GPK`, `Route`, `Dep`, `DiluentVol`, `ExpansionVol`, `Diluents` - - ✓ Sheet name: "Reconstitution" - - ✓ Diluents use semicolon separation (`;`) - -6. **DoseRules Sheet** (`DoseRule.fs` - `getData`) - - ✓ All 40+ columns verified - - ✓ Sheet name: "DoseRules" - - ✓ Includes deduplication logic - -7. **SolutionRules Sheet** (`SolutionRule.fs` - `get`) - - ✓ All columns for solution rules verified - - ✓ Sheet name: "SolutionRules" - - ✓ Solutions use pipe separation (`|`) - - ✓ Volumes and Quantities use semicolon separation (`;`) - -8. **RenalRules Sheet** (`RenalRule.fs` - `getData`) - - ✓ All columns for renal dose adjustments verified - - ✓ Sheet name: "RenalRules" - - ✓ Age restriction (≥28 days) is implemented in filter logic - -### 📝 Clarifications Added - -The following items were clarified in the documentation: - -1. **Formulary Sheet** (`Product.fs` - `getFormularyProducts`) - - Sheet column: `UMCU` - - Code field: `Apotheek` - - **Clarification**: Added note that sheet uses "UMCU" but code maps to `Apotheek` field - - This is a naming convention difference, not an error - -2. **Enteral Feeding Sheet** (`Product.fs` - `Enteral.get`) - - Column `KH g` maps to "koolhydraat g" (Dutch for carbohydrate) - - **Clarification**: Added that KH stands for koolhydraat - -3. **Parenteral Medications Sheet** (`Product.fs` - `Parenteral.get`) - - Both `glucose g` and `koolhydraat g` columns exist - - **Clarification**: Added that koolhydraat represents total carbohydrate content - -4. **Emergency Treatment Sheets** - - **Implementation Location**: Currently in client code (`EmergencyList.fs`), NOT in GenFORM.Lib - - **Clarification**: Added implementation note about location - - Sheets documented: Bolus, Continuous, Products, Normal Values - -### 🔍 Implementation Details Verified - -1. **Data Loading Pattern**: - - ✓ All sheets use `Web.getDataFromSheet dataUrlId "SheetName"` - - ✓ First row is header, subsequent rows are data - - ✓ Column extraction uses `Csv.getStringColumn` and helper functions - -2. **Data Parsing**: - - ✓ Numeric columns use `BigRational.toBrs >> Array.tryHead` pattern - - ✓ Boolean columns check for "x", "true", "TRUE" (case-insensitive where applicable) - - ✓ Multi-value separators: - - Semicolon (`;`) for lists: Frequencies, Volumes, Quantities, GPKs, Diluents - - Pipe (`|`) for alternative solutions - - Space-dash-space (` - `) for norm ranges in renal rules - -3. **Unit Mapping**: - - ✓ Uses `Mapping.mapUnit unitMapping` for unit conversion - - ✓ Defaults to `NoUnit` when mapping fails or field is empty - - ✓ Automatically creates "per" units (e.g., mg/kg, mg/mL) - -4. **Route Mapping**: - - ✓ Uses `Mapping.mapRoute routeMapping` for route standardization - - ✓ Matches on Long, Short, or exact string - - ✓ Case-insensitive comparison - -## Column Name Conventions - -### Consistent Patterns Found - -1. **Min/Max Prefix**: Minimum and maximum values - - `MinAge`, `MaxAge`, `MinWeight`, `MaxWeight`, etc. - -2. **Adj Suffix**: Adjusted (per patient normalization unit) - - `MinQtyAdj`, `MaxQtyAdj`, `NormQtyAdj`, etc. - -3. **Unit Suffix**: Time unit designation - - `FreqUnit`, `RateUnit`, `TimeUnit`, `IntUnit`, `DurUnit` - -4. **Dutch Column Names**: Present in nutrition sheets - - `Eenheid` (unit), `eiwit` (protein), `vet` (fat), `natrium` (sodium), etc. - - `koolhydraat` (carbohydrate), `chloor` (chloride) - -## Data Quality Observations - -### Implemented Validations - -1. **DoseRules**: Deduplication by row content (excluding first column) -2. **Distinct filtering**: Applied where documented -3. **Age restrictions**: RenalRules filter enforces ≥28 days -4. **Required fields**: Checked in `doseRuleDataIsValid` function - -### Type Safety - -- Units are properly typed using GenUnits.Lib -- BigRational used for all numeric dosing values (precision safety) -- ValueUnit combines values with their units (dimensional safety) -- MinMax types ensure proper range handling - -## Recommendations - -### Documentation Improvements ✅ Applied - -1. ✅ Added implementation note for Emergency Treatment sheets -2. ✅ Clarified UMCU/Apotheek column naming -3. ✅ Clarified KH abbreviation in Enteral sheet -4. ✅ Clarified koolhydraat usage in Parenteral sheet - -### Future Considerations - -1. **Emergency Treatment Migration**: Consider moving emergency treatment data loading from client to GenFORM.Lib for consistency - -2. **Column Name Standardization**: Consider aligning field names in code with sheet column names where they differ (e.g., UMCU vs Apotheek) - -3. **Validation Documentation**: Add section documenting the validation logic (e.g., `doseRuleDataIsValid`) - -4. **Unit Mapping Coverage**: Document which units are expected in the Units sheet for full functionality - -## Verification Method - -This verification was performed by: - -1. Reading each implementation file in `src/Informedica.GenFORM.Lib/` -2. Extracting actual column names from `get` function calls -3. Comparing with documented column requirements -4. Checking sheet names in `Web.getDataFromSheet` calls -5. Verifying data transformation logic against documented behavior - -## Conclusion - -The `0003-resource-requirements.md` documentation is **highly accurate** and matches the implementation in GenFORM.Lib with only minor clarifications needed. All core sheets (Routes, Units, FormRoute, ValidForms, Reconstitution, DoseRules, SolutionRules, RenalRules, Enteral, Parenteral, Formulary) are correctly documented. - -The Emergency Treatment sheets are documented but implemented in client code rather than the library, which has been noted in the documentation. - ---- - -**Verified by**: GitHub Copilot (Claude Sonnet 4.5) -**Verification Date**: December 21, 2025 -**Files Examined**: -- `src/Informedica.GenFORM.Lib/Mapping.fs` -- `src/Informedica.GenFORM.Lib/DoseRule.fs` -- `src/Informedica.GenFORM.Lib/SolutionRule.fs` -- `src/Informedica.GenFORM.Lib/Product.fs` -- `src/Informedica.GenFORM.Lib/RenalRule.fs` diff --git a/docs/mdr/design-history/0016-gstand-dose-rule-fallback.md b/docs/mdr/design-history/0016-gstand-dose-rule-fallback.md index 2113152e..44e17a4d 100644 --- a/docs/mdr/design-history/0016-gstand-dose-rule-fallback.md +++ b/docs/mdr/design-history/0016-gstand-dose-rule-fallback.md @@ -122,6 +122,6 @@ When the maintainer is ready to migrate: - [GStandDoseRules.fsx prototype — PR #310](https://github.com/informedica/GenPRES/pull/310) - [G-Standard dose rule check colour coding — PR #309](https://github.com/informedica/GenPRES/pull/309) -- [GenFORM resource requirements — ADR-0003](0003-resource-requirements.md) +- [GenFORM sheet contract — the `Data` record types in `src/Informedica.GenFORM.Lib/Types.fs`](../../../src/Informedica.GenFORM.Lib/Types.fs) - [MCP Server Architecture — ADR-0009](0009-mcp-server-architecture.md) - [ZForm.GStand API — `src/Informedica.ZForm.Lib/GStand.fs`](../../../src/Informedica.ZForm.Lib/GStand.fs) diff --git a/docs/mdr/requirements/software-requirements.md b/docs/mdr/requirements/software-requirements.md index 37da0ad9..0bb49209 100644 --- a/docs/mdr/requirements/software-requirements.md +++ b/docs/mdr/requirements/software-requirements.md @@ -56,7 +56,32 @@ GenPRES is a clinical decision support system for prescribing and managing medic --- -## 6. Security Requirements +## 6. Clinical Knowledge Source and Change Control + +The Operational Knowledge Rules that drive prescribing — dose rules, solution +rules, reconstitution rules, renal rules, and the hospital formulary — are +authored outside the application, in the Google spreadsheet identified by +`GENPRES_URL_ID`, and loaded at startup (ADR-0001). The rules are therefore +changeable without a code release, and the following controls apply to the +*content*, independently of the software release process: + +- Dose rule changes require clinical pharmacist review before they are released. +- Solution rule changes require pharmacy (IV compounding) validation. +- Renal rule changes require nephrology review. +- Emergency treatment protocol changes require intensive-care review. +- Rule changes must be traceable: the spreadsheet retains revision history, and a + change takes effect only on application restart or explicit resource reload, so + the rule set in force for any running instance is identifiable. + +The *structure* of that data — which sheets exist, which columns each carries, +their units and encodings — is not specified here. It is defined by the parsers +and the `Data` record types in `Informedica.GenFORM.Lib` (`Types.fs`) and +enforced by the sheet column-contract tests in `Informedica.GenFORM.Tests`, so +that the specification cannot drift from the implementation that must honour it. + +--- + +## 7. Security Requirements The security baseline is documented in ADR-0015 (`docs/mdr/design-history/0015-security-baseline.md`). Key controls: @@ -70,7 +95,7 @@ The security baseline is documented in ADR-0015 (`docs/mdr/design-history/0015-s --- -## 7. Deployment & Scalability +## 8. Deployment & Scalability - Stateless design supports horizontal scaling in containerised environments. - Configurable environment variables for multi-tenant or hospital-specific deployments. @@ -79,7 +104,7 @@ The security baseline is documented in ADR-0015 (`docs/mdr/design-history/0015-s --- -## 8. Future Enhancements +## 9. Future Enhancements - Integration with clinical databases for longitudinal tracking. - Expanded AI capabilities: anomaly detection, dosage suggestion refinement. diff --git a/docs/roadmap/backlog.md b/docs/roadmap/backlog.md index c71afe96..20c94588 100644 --- a/docs/roadmap/backlog.md +++ b/docs/roadmap/backlog.md @@ -112,13 +112,13 @@ solution-based medications. **Affected areas.** - `src/Informedica.NLP.Lib/`, `Informedica.GenFORM.Lib/` (per-rule extractors + validators: `SolutionRule.fs`, `RenalRule.fs`, reconstitution in `Product.fs`) -- `docs/mdr/design-history/0003-resource-requirements.md` (column/semantics updates) +- `src/Informedica.GenFORM.Lib/Types.fs` `Data` records + the column-contract test (column/semantics updates) **Acceptance criteria.** - Each rule type has an extractor + validator with surfaced `Messages`. - Each round-trips through its `getFromGetData`/`toData` (cf. DoseRule roundtrip work). - Formulary entries extractable and validatable. -- `0003-resource-requirements.md` updated per rule type. +- `Data` record comments and the column-contract test updated per rule type. --- diff --git a/docs/roadmap/feature-ehr-url-parameters.md b/docs/roadmap/feature-ehr-url-parameters.md new file mode 100644 index 00000000..c3ee037c --- /dev/null +++ b/docs/roadmap/feature-ehr-url-parameters.md @@ -0,0 +1,184 @@ +# Feature Request: Extend URL parameters to accept patient data from an external EHR + +## Is your feature request related to a problem? Please describe. + +GenPRES can be launched with a patient context pre-filled via URL query +parameters (e.g. `#patient?by=2020&bm=3&bd=1&wt=12000&cv=y`). This is the +integration point used when an external Electronic Health Record (EHR) links +into GenPRES for a specific patient. + +The current parameter set (parsed in +[App.fs:237](../../src/Informedica.GenPRES.Client/App.fs#L237)) only covers +demographic/clinical values needed for dose calculation: + +| Param | Meaning | +| ----- | ------- | +| `by` / `bm` / `bd` | birth date | +| `ad` | age in days | +| `wt` | weight (gram) | +| `ht` | height (cm) | +| `gw` / `gd` | gestational age weeks / days | +| `cv` | central venous line (`y`) | +| `dp` | department | +| `pg` `la` `dc` `in` `md` `rt` `fr` `dt` | UI / prescription context | + +It **cannot** carry the patient identity or the identity of the ordering user +from the EHR. As a result: + +- The patient shown in GenPRES is anonymous — there is no patient identifier, + first name, or last name to confirm the clinician is prescribing for the + correct patient (a patient-safety concern). +- Venous access is limited to a single boolean CVL flag; peripheral lines and + enteral tubes cannot be conveyed even though the domain already models them. +- There is no way to record *who* is prescribing (login/user context) for + audit / traceability, which MDR-regulated workflows require. + +## Describe the solution you'd like + +Two changes, delivered together: + +1. **Add** the new EHR fields (patient identity, user context, admission + date, bed id, full venous-access list). +2. **Redesign** the whole URL query-parameter scheme to consistent + **three-letter** keys. + +The current scheme mixes two-letter keys (`by`, `wt`, `cv`, …) that are terse, +inconsistent, and already colliding (`ad` = *age in days*, so an admission +`ad` is impossible). Moving to a uniform three-letter convention makes the +contract self-documenting for EHR integrators and frees up a clean namespace +for the new fields. + +### Redesign: full three-letter parameter scheme + +Every parameter — existing and new — under one convention. `Legacy` +shows the current key (blank = new field). + +| Key | Field | Type | Legacy | Notes | +| --- | ----- | ---- | ------ | ----- | +| `byr` | Birth year | int | `by` | | +| `bmo` | Birth month | int | `bm` | default 1 | +| `bdy` | Birth day | int | `bd` | default 1 | +| `agd` | Age in days | int | `ad` | alternative to birth date | +| `wgt` | Weight (gram) | int | `wt` | | +| `hgt` | Height (cm) | int | `ht` | | +| `gaw` | Gestational age weeks | int | `gw` | | +| `gad` | Gestational age days | int | `gd` | | +| `sex` | Gender | `m`/`f` | — | not currently settable via URL | +| `cvl` | Central venous line | `y` | `cv` | see Venous access | +| `pvl` | Peripheral venous line | `y` | — | new | +| `ent` | Enteral tube | `y` | — | new | +| `dep` | Department | string | `dp` | → `Patient.Department` | +| `bed` | Bed Id | string | — | new | +| `adm` | Admission date | ISO `yyyy-mm-dd` | — | new; single ISO value avoids the `ad`/age collision | +| `pid` | Patient Id | string | — | new; external EHR identifier (e.g. MRN) | +| `fnm` | First name | string | — | new; confirmation display only | +| `lnm` | Last name | string | — | new; confirmation display only | +| `usr` | User context | string | — | new; ordering clinician login, for audit | +| `pag` | Page | `el`/`cm`/`pr`/`fm`/`pe` | `pg` | | +| `lan` | Language | `en`/`du`/`fr`/`gr`/`sp`/`it` | `la` | | +| `dsc` | Show disclaimer | `n` | `dc` | | +| `ind` | Indication | string | `in` | | +| `med` | Medication | string | `md` | | +| `rte` | Route | string | `rt` | | +| `frm` | Form | string | `fr` | | +| `dst` | Dose type | string | `dt` | | + +Example redesigned launch URL: + +```text +#patient?byr=2020&bmo=3&bdy=1&wgt=12000&cvl=y&dep=NICU&pid=1234567&fnm=Jan&lnm=Jansen&usr=jdoe&adm=2026-07-10&bed=12 +``` + +### Venous access + +The domain already models access as a list +([Types.fs:129-132](../../src/Informedica.GenPRES.Shared/Types.fs#L129)): + +```fsharp +and Access = + | CVL // central venous line + | PVL // peripheral venous line + | EnteralTube +``` + +but only `CVL` is settable (`cv=y`). Under the three-letter scheme, each +access type gets its own boolean flag — `cvl=y`, `pvl=y`, `ent=y` — which +compose into the `Access list`. (Alternative: a single `acc=cvl,pvl` +comma-separated list; the per-flag form is preferred for consistency with the +rest of the scheme and simpler EHR string-building.) + +### Migration / backwards compatibility + +A full key rename is a **breaking change** for any existing EHR deep-links. +Options, in preference order: + +1. **Dual-read transition.** `parseUrl` accepts both new three-letter keys and + the legacy two-letter keys (legacy → new alias map), logs a deprecation + warning when a legacy key is seen, and drops legacy support after a + published date once EHR integrators have migrated. +2. **Hard cutover.** Coordinate a single switch-over with EHR integrators; + simplest code, but requires all consumers to change at once. + +Recommendation: option 1. The alias map lives only in the parser and is cheap +to remove later. + +### Required supporting changes + +These fields do not exist on the domain `Patient` today +([Types.fs:93-104](../../src/Informedica.GenPRES.Shared/Types.fs#L93)) — the +record has no identifier and no name. Delivering this needs: + +- Extend `Patient` (Shared) with optional `Id`, `FirstName`, `LastName`, + `AdmissionDate` (`DateTime option`), and `BedId` (`string option`) fields. + `Department` already exists as `Patient.Department` + ([Types.fs:103](../../src/Informedica.GenPRES.Shared/Types.fs#L103)); a + `Location` (`string option`) field also already exists + ([Types.fs:102](../../src/Informedica.GenPRES.Shared/Types.fs#L102)) and may + be a fit for bed/ward location — decide whether `bi` maps to a new `BedId` + field or reuses `Location`. +- Introduce a user/session-context value for `us`. There is currently **no** + per-user identity model — the only auth is a single password gate for the + settings page ([App.fs:48-49](../../src/Informedica.GenPRES.Client/App.fs#L48)). + Decide whether `us` is display/audit-only metadata or feeds a future + identity model. +- Update `parseUrl` / `parsePatientParams` + ([App.fs:225-349](../../src/Informedica.GenPRES.Client/App.fs#L225)) to read + the new keys. +- Update the parameter doc comment + ([App.fs:206-224](../../src/Informedica.GenPRES.Client/App.fs#L206)). +- Surface Id + name in the UI patient header so the clinician can confirm the + right patient. + +Per the script-only policy, non-UI Shared type changes are prototyped in +`.fsx` and migrated by a maintainer; the Client parsing/UI is edited directly. + +## Describe alternatives you've considered + +- **Fable.Remoting handshake instead of URL params.** A server call keyed by a + short-lived launch token would keep PII out of the URL/browser history. + Heavier to integrate; the existing EHR link mechanism is URL-based. +- **POST the patient context** rather than GET query string — avoids logging + PII in access logs, but breaks the simple deep-link launch model EHRs use. +- **Do nothing / manual entry.** Clinician re-types name and identity — error + prone and defeats the point of EHR integration. + +## Additional context + +**Privacy / MDR.** `pid`, `fnm`, `lnm`, `usr` are PII. URLs land in browser +history, referer headers, and server access logs. Coding standards already +require redacting PII in logs +([fsharp-coding.instructions.md](../../.github/instructions/fsharp-coding.instructions.md) +— "Avoid logging PII; redact sensitive data"). Note the current warning path +logs the raw URL on parse failure +([App.fs:313](../../src/Informedica.GenPRES.Client/App.fs#L313)) — this must +not leak the new fields. Consider: + +- Redacting `pid/fnm/lnm/usr` (and `bed`, `adm` together with `dep` since + bed+ward+date is identifying) from any URL logging. +- Documenting that transport should be HTTPS. +- Whether the identity fields belong in the URL at all vs. a token exchange. + +**Backwards compatibility.** The three-letter redesign renames existing keys, +so it is a breaking change — see [Migration](#migration--backwards-compatibility) +above. The dual-read transition keeps existing two-letter EHR deep-links +working until legacy support is retired. diff --git a/docs/roadmap/feature-patient-persistence.md b/docs/roadmap/feature-patient-persistence.md new file mode 100644 index 00000000..3f72019a --- /dev/null +++ b/docs/roadmap/feature-patient-persistence.md @@ -0,0 +1,112 @@ +# Feature Request: Patient-State Persistence + +**Status:** Proposed +**Related fit-gap items:** 9.6 (version control / save history), 9.7 (multi-user conflict detection), 10.4 (per-user audit trail), and the "Persistence" summary gap +**Meeting action item (13 Jul 2026):** "Patient-data persistence + patient management (context from MetaVision → GenPRES; store order snapshots)" — Owner: Casper, Blocking: Yes +**Fit-gap source:** `docs/roadmap/fit-gap-ap2019-vs-genpres.md` + +--- + +## 1. Problem + +GenPRES is stateless. Patient context is passed in via URL on every open, which means: + +- Patient data and running orders are **re-entered every session** — the single biggest usability regression versus the legacy AfsprakenProgramma (AP2019). +- URL-carried context is **manipulable** (encoded ≠ encrypted; a role or patient id in a URL can be tampered with). +- There is **no record of who prescribed what, for which patient** — an MDR/IGJ traceability gap (fit-gap 10.4). + +The MVP must persist patient state and running orders so that reopening a patient restores the last saved treatment plan, and so that each saved version is attributable to a named prescriber. + +## 2. What the legacy system does (benchmark) + +The behaviour to replicate lives in the VBA modules that save a snapshot of patient state: + +- **`src/module/ModPatient.bas`** — `Patient_SavePatient` → `SavePatientToDatabase` (orchestration, guards, concurrency check). +- **`src/module/ModDatabase.bas`** — `Database_SavePatient`, `Database_SavePrescriber`, `Database_SaveData` (the actual writes). + +Its model, which we adopt: + +1. **Keyed on the patient identifier (hospital number).** A save is **refused if there is no hospital number** (`ModPatient.bas` guard). Persistence only happens for identified patients. +2. **Append-only, versioned snapshots.** Every save creates a new version (`versionID`, `versionUTC`, `versionDate`); the *latest* version is the current state. Nothing is updated in place; history accretes. +3. **Optimistic concurrency.** Before writing, it compares the in-hand version against the latest stored version and warns the clinician if the stored one is newer ("the appointments have changed since you loaded them — save anyway and become the latest?"). It does **not** hard-lock. +4. **Prescriber attribution on every row.** Each written record carries the prescriber login plus the version timestamps — this is also the audit trail. +5. **Snapshot = patient demographics + prescriber + the full prescription state** (structured key/value data + free-text notes). + +## 3. Proposed behaviour for GenPRES (MVP) + +### 3.1 Scope of a snapshot + +A snapshot is the **complete patient state as a single atomic unit**: + +- Patient context (demographics, weight, age, GA/PMA, access type, etc.). +- The **treatment plan**: the list of running orders. Following the legacy/event-sourcing-flavoured model, each running order carries **its own patient-context snapshot**, so an order is self-contained and reproducible. + +Serialise the snapshot as an opaque payload (JSON blob) — GenPRES's domain owns the shape; the persistence layer treats it as data. + +### 3.2 Persist only when a patient id is present + +Mirroring the legacy guard: **a snapshot is written only when the patient has an identifier.** Unidentified / scratch state is never persisted. (The legacy app offers a "standard patient" fallback with a synthetic id for teaching cases; GenPRES may add an equivalent later, but it is out of MVP scope — for the MVP, "no id ⇒ no save".) + +### 3.3 Append-only, latest-wins + +- Each save inserts a **new version** for that patient id; retrieval returns the **latest** version. +- Every version records **metadata**: created-at (UTC), and **who touched it last** (prescriber identity from the user-management work). +- **No explicit user-facing version history / restore-to-point** in the MVP (out of scope per the meeting — versioning with concurrent edits "opens a can of worms"). History is retained in storage for audit, not exposed as a UI feature yet. + +### 3.4 Concurrency + +- Multi-user conflict is **rare in practice** (one clinician per patient, assigned informally) and is **not an MVP blocker**. +- Implement the **lightweight legacy safeguard**: on save, detect that a newer version exists and surface a soft warning ("this patient was saved more recently by X — save anyway?"). Keep "last touched / by whom" metadata. +- No hard locking, no merge, no real-time conflict resolution in the MVP. + +### 3.5 Audit + +- The prescriber identity + timestamp carried on each saved version **is** the audit record of who saved which patient state when. This satisfies fit-gap 10.4 for the MVP (who did what, for which patient), reusing the hospital-unique login already established by the user-management work. + +## 4. Integration boundary — no MetaVision access + +**GenPRES has no direct access to the MetaVision source system.** This constrains the design: + +- The **patient identifier and patient/user context arrive at GenPRES via its API**, supplied by the MetaVision-side shell (VB.NET scripting that already gates the user and launches GenPRES with context). GenPRES **trusts the sender** and does not call back into MetaVision. +- GenPRES therefore treats the patient id as an **opaque, trusted key**. It does not, and must not, look the patient up in MetaVision, sync demographics from it, or write back to it. +- All persisted patient data lives in **GenPRES's own store**, populated from what the API delivers and what the clinician enters/prescribes — never from a live MetaVision query. + +## 5. Storage — deliberately left open + +The **physical storage design is out of scope for this request** and is a separate architectural decision (flagged in the meeting as taking on an external dependency, which matters more than internal code structure — owner: Mark, for review). + +Assumptions fixed here: + +- Storage is a **relational database**. Exact product, schema, hosting (SQL vs. SQLite/flat-file, container topology, on-prem location) are **to be decided** in the architecture/persistence design step. +- It is **local / on-premise**, inside the hospital firewall. The MVP does **not** target the shared regional data platform. +- The persistence layer exposes, at minimum: `save(patientId, snapshot, prescriber) → version` and `getLatest(patientId) → snapshot | none`, plus `latestVersionMetadata(patientId)` for the concurrency warning. + +**Open questions for the architecture step (do not block writing this request):** + +- Relational schema shape: one snapshot-blob column per version, vs. normalised order rows. (Legacy normalises into key/value + text tables; a blob-per-version is simpler and matches the "atomic snapshot" intent.) +- Retention policy for old versions. +- How the prescriber identity is threaded from the API/user-management layer into each save. + +## 6. Out of scope (MVP) + +- User-facing version history / restore-to-a-previous-version. +- Real-time multi-user conflict resolution, locking, or merge. +- The shared regional (Rotterdam / Leiden / Utrecht) patient-data platform. +- Any MetaVision read/write integration or demographic sync. +- Non-identified ("standard"/teaching) patient persistence. + +## 7. Acceptance criteria + +1. Reopening an identified patient restores the last-saved patient context **and** treatment plan without re-entry. +2. Saving with **no patient id present** performs no write (and gives clear feedback). +3. Each save creates a new version; retrieval always returns the latest. +4. Each stored version records created-at (UTC) and the prescriber who saved it. +5. Saving over a stored version that is newer than the one loaded surfaces a soft "saved more recently by X — continue?" warning; no data is silently lost. +6. GenPRES performs the entire flow using only the patient id/context delivered via its API, with **zero** MetaVision calls. +7. The storage backend is a relational database reachable only from within the hospital firewall; the concrete schema/hosting is resolved in the separate persistence-architecture task. + +## 8. Dependencies + +- **User & access management** (prescriber identity/role) — supplies the "who" on each version; audit (10.4) rides on it. +- **Persistence-architecture review** (owner: Mark) — settles the storage decision before implementation. +- **MetaVision API contract** — defines exactly what patient/user context is delivered to GenPRES's API. diff --git a/src/Informedica.GenFORM.Lib/DoseRuleData.fs b/src/Informedica.GenFORM.Lib/DoseRuleData.fs index 6c6abc8b..48b7bbc9 100644 --- a/src/Informedica.GenFORM.Lib/DoseRuleData.fs +++ b/src/Informedica.GenFORM.Lib/DoseRuleData.fs @@ -9,6 +9,15 @@ module DoseRuleData = open Utils + /// + /// The canonical column order of the "DoseRules" sheet, and the emit order of + /// dataToCsv. The column semantics live on DoseRuleData in + /// Types.fs; which of these may be absent from a sheet is fixed by the tolerant + /// readers in parseDoseRuleData and pinned by the column-contract tests. + /// + // TODO: "Loc" is read by parseDoseRuleData (into PatientCategoryData.Location) + // but missing here, so a dataToCsv round-trip silently drops the location. + // Either add it in the position the sheet uses, or stop reading it. let headers = [ "RowId" @@ -132,6 +141,11 @@ module DoseRuleData = |> Array.tail |> Array.distinctBy (fun row -> row |> Array.tail) |> Array.map (fun r -> + // `get` raises when the column is absent from the header row, so + // every column read through it is REQUIRED. The three readers + // below are the deliberate exceptions: they swallow that failure, + // so the columns they read may be missing from a sheet. Which + // reader a column uses is the whole of its optionality contract. let get = getColumn r let getIfNull col = @@ -147,6 +161,8 @@ module DoseRuleData = let getInt = getIfNull >> Int32.tryParse let toBrOpt = BigRational.toBrs >> Array.tryHead + // required column; "x" is the sheet convention, the rest are + // tolerated spellings let getBool = get >> fun s -> diff --git a/src/Informedica.GenFORM.Lib/DoseType.fs b/src/Informedica.GenFORM.Lib/DoseType.fs index a4c084d4..d7573df1 100644 --- a/src/Informedica.GenFORM.Lib/DoseType.fs +++ b/src/Informedica.GenFORM.Lib/DoseType.fs @@ -63,8 +63,10 @@ module DoseType = NoDoseType, warn + /// /// Get a dose type from a string. Pure wrapper over parse that /// discards the warning. + /// let fromString doseType doseText = parse doseType doseText |> fst diff --git a/src/Informedica.GenFORM.Lib/Mapping.fs b/src/Informedica.GenFORM.Lib/Mapping.fs index 29a7cc1e..50787b75 100644 --- a/src/Informedica.GenFORM.Lib/Mapping.fs +++ b/src/Informedica.GenFORM.Lib/Mapping.fs @@ -54,46 +54,59 @@ module Mapping = Web.getDataFromSheet dataUrlId sheet |> parseSheet apply + /// Map one row of the "Routes" sheet. Named rather than inlined into + /// getRouteMapping so the column contract can be tested without IO. + let routeMappingRow (get: string -> string) (_: string -> float option) = + { + Long = get "ZIndex" + Short = get "ShortDutch" + } + + let getRouteMapping dataUrlId = - fun get _ -> - { - Long = get "ZIndex" - Short = get "ShortDutch" - } + routeMappingRow |> getData dataUrlId Constants.routesSheet |> Result.mapErrorSource "getRouteMapping" + /// Map one row of the "Units" sheet. + let unitMappingRow (get: string -> string) (_: string -> float option) = + { + Long = get "ZIndexUnitLong" + Short = get "Unit" + MV = get "MetaVisionUnit" + Group = get "Group" + } + + let getUnitMapping dataUrlId = - fun get _ -> - { - Long = get "ZIndexUnitLong" - Short = get "Unit" - MV = get "MetaVisionUnit" - Group = get "Group" - } + unitMappingRow |> getData dataUrlId Constants.unitsSheet |> Result.mapErrorSource "getUnitMapping" - let getTotals dataUrlId = - fun get _ -> - let toBrOpt = BigRational.toBrs >> Array.tryHead + /// Map one row of the "Totals" sheet. + let totalsRow (get: string -> string) (_: string -> float option) = + let toBrOpt = BigRational.toBrs >> Array.tryHead - { - Name = get "Name" - MinAge = get "MinAge" |> toBrOpt - MaxAge = get "MaxAge" |> toBrOpt - MinWeight = get "MinWeight" |> toBrOpt - MaxWeight = get "MaxWeight" |> toBrOpt - Unit = get "Unit" |> UnitsParse.fromString - Adj = get "Adj" |> UnitsParse.fromString - TimeUnit = get "TimeUnit" |> UnitsParse.fromString - MinPerTime = get "MinPerTime" |> toBrOpt - MaxPerTime = get "MaxPerTime" |> toBrOpt - MinPerTimeAdj = get "MinPerTimeAdj" |> toBrOpt - MaxPerTimeAdj = get "MaxPerTimeAdj" |> toBrOpt - } + { + Name = get "Name" + MinAge = get "MinAge" |> toBrOpt + MaxAge = get "MaxAge" |> toBrOpt + MinWeight = get "MinWeight" |> toBrOpt + MaxWeight = get "MaxWeight" |> toBrOpt + Unit = get "Unit" |> UnitsParse.fromString + Adj = get "Adj" |> UnitsParse.fromString + TimeUnit = get "TimeUnit" |> UnitsParse.fromString + MinPerTime = get "MinPerTime" |> toBrOpt + MaxPerTime = get "MaxPerTime" |> toBrOpt + MinPerTimeAdj = get "MinPerTimeAdj" |> toBrOpt + MaxPerTimeAdj = get "MaxPerTimeAdj" |> toBrOpt + } + + + let getTotals dataUrlId = + totalsRow |> getData dataUrlId Constants.totalsSheet |> Result.mapErrorSource "getTotals" @@ -137,10 +150,14 @@ module Mapping = | _ -> false - let getFormRoutes dataUrlId unitMapping = + /// + /// Map one row of the "FormRoute" sheet. Takes the unit mappings because the + /// dose columns are only read once a dose unit resolves. + /// + let formRouteRow unitMapping = let mapUnit = mapUnit unitMapping - fun getStr getFlt -> + fun (getStr: string -> string) (getFlt: string -> float option) -> let un = getStr "Unit" |> mapUnit |> Option.defaultValue NoUnit let du = getStr "DoseUnit" |> mapUnit |> Option.defaultValue un @@ -186,6 +203,10 @@ module Mapping = Reconstitute = getStr "Reconstitute" |> String.equalsCapInsens "true" IsSolution = getStr "IsSolution" |> String.equalsCapInsens "true" } + + + let getFormRoutes dataUrlId unitMapping = + formRouteRow unitMapping |> getData dataUrlId Constants.formRouteSheet |> Result.mapErrorSource "getFormRoutes" @@ -219,7 +240,11 @@ module Mapping = |> Array.exists id + /// Map one row of the "ValidForms" sheet. + let validFormRow (get: string -> string) (_: string -> float option) = get "Form" + + let getValidForms dataUrlId = - fun get _ -> get "Form" + validFormRow |> getData dataUrlId Constants.validFormsSheet |> Result.mapErrorSource "getValidFormResult" diff --git a/src/Informedica.GenFORM.Lib/Patient.fs b/src/Informedica.GenFORM.Lib/Patient.fs index 39f2777f..28af13d2 100644 --- a/src/Informedica.GenFORM.Lib/Patient.fs +++ b/src/Informedica.GenFORM.Lib/Patient.fs @@ -86,11 +86,12 @@ module PatientCategory = } = p - // NOTE: the `IsAdult` case returns an empty age range, which the age filter - // treats as "no age restriction". `IsAdult` is therefore not yet enforced in - // patient matching — it is gated at the extraction boundary (no `IsAdult="x"` - // row may reach GenFORM ingest) until a structured adult facet is added. See - // ADR-0021 and ADR-0003 (DoseRules `IsAdult` note). + // TODO: the `IsAdult` case returns an empty age range, which the age filter + // treats as "no age restriction" — so an adult-only rule currently matches a + // patient of ANY age instead of adults only. Until this enforces "adult + // patients only", no `IsAdult="x"` row may reach GenFORM ingest; that is + // gated at the extraction boundary, which also empties MinAge/MaxAge for such + // rows, leaving them with no age bound at all. See ADR-0021. let getAge (pat: PatientCategory) = match pat.Age with | AbsoluteAge a -> a diff --git a/src/Informedica.GenFORM.Lib/Product.fs b/src/Informedica.GenFORM.Lib/Product.fs index ae0a149c..fd864b0f 100644 --- a/src/Informedica.GenFORM.Lib/Product.fs +++ b/src/Informedica.GenFORM.Lib/Product.fs @@ -38,6 +38,10 @@ module Product = | "parenteral" -> ProductType.ParenteralProduct | _ -> ProductType.NoProduct + // TODO: the sheet still carries "UseForm" and "UseBrand" + // columns that nothing reads any more (the naming flags were + // dropped from FormularyProduct). Confirm with the sheet owner + // and remove the columns, or start honouring them again. { GPK = get "GPKODE" ProductType = get "Type" |> getProdType @@ -586,8 +590,10 @@ module Product = |> Array.append enteral + /// /// Impure adapter: reads the ZIndex GenPresProducts and delegates to the /// pure fromGenPresProducts. Kept for existing callers/tests. + /// let get unitMapping routeMapping diff --git a/src/Informedica.GenFORM.Lib/Resources.fs b/src/Informedica.GenFORM.Lib/Resources.fs index 2e3cefc9..e59e43dd 100644 --- a/src/Informedica.GenFORM.Lib/Resources.fs +++ b/src/Informedica.GenFORM.Lib/Resources.fs @@ -219,6 +219,10 @@ module Resources = Keys.formularyProducts.Name, ofResult (fun () -> Product.getFormularyProducts dataUrlId) Keys.reconstitution.Name, ofResult (fun () -> Product.Reconstitution.get dataUrlId) + // Parenteral and enteral products are NOT loaded from sheets of + // their own: they are the Formulary rows whose Type says so, turned + // into ProductComponents with the Formulary nutrition columns as + // their substances. Keys.parenteralMeds.Name, derive (fun r -> r.Get Keys.formularyProducts |> Product.Parenteral.get (r.Get Keys.unitMappings)) diff --git a/src/Informedica.GenFORM.Lib/Types.fs b/src/Informedica.GenFORM.Lib/Types.fs index 18370b27..38d206bf 100644 --- a/src/Informedica.GenFORM.Lib/Types.fs +++ b/src/Informedica.GenFORM.Lib/Types.fs @@ -12,8 +12,14 @@ module Types = type MinMax = Informedica.GenCore.Lib.Ranges.MinMax - /// Associate a Route and a Form - /// setting default values for the other fields + /// + /// Associate a Route and a Form, setting default values for the other fields. + /// + /// One row of the "FormRoute" sheet, parsed by Mapping.getFormRoutes. + /// Field names equal sheet column names except where noted. The boolean columns + /// hold the literal text "true"/"false", matched case-insensitively; every + /// column is required. + /// type FormRoute = { // The Route @@ -28,11 +34,11 @@ module Types = MinDoseQty: ValueUnit option // The maximum Dose quantity MaxDoseQty: ValueUnit option - // The minimum Adjusted Dose quantity + // The minimum Adjusted Dose quantity; sheet column "MinDoseQtyKg" MinDoseQtyPerKg: ValueUnit option - // The maximum Adjusted Dose quantity + // The maximum Adjusted Dose quantity; sheet column "MaxDoseQtyKg" MaxDoseQtyPerKg: ValueUnit option - // The divisibility of a pharmaceutical form + // The divisibility of a pharmaceutical form; sheet column "Divisible" Divisibility: BigRational option // Whether a Dose runs over a Time Timed: bool @@ -82,21 +88,30 @@ module Types = | NoDoseType + /// + /// One row of the "Reconstitution" sheet, parsed by + /// Product.Reconstitution.parseReconstitution. Describes how a product + /// is brought into an administrable form by adding a diluent. Field names equal + /// sheet column names except where noted; every column is required. + /// type Reconstitution = { // The GPK of the reconstitution GPK: string // The route for the reconstitution Route: string - // The location for the reconstitution rule + // The location for the reconstitution rule; sheet column "Loc" Location: string option - // The department for the reconstitution + // The department for the reconstitution; sheet column "Dep" Department: string option - // The volume of the reconstitution + // The volume of the reconstitution, in mL; sheet column "DiluentVol". + // Defaults to 1 mL when the cell is empty. DiluentVolume: ValueUnit - // An optional expansion volume of the reconstitution + // An optional expansion volume of the reconstitution, in mL - the + // increase in total volume caused by dissolving the product; sheet + // column "ExpansionVol" ExpansionVolume: ValueUnit option - // The Diluents for the reconstitution + // The Diluents for the reconstitution, ';'-separated in the sheet Diluents: string[] } @@ -171,15 +186,31 @@ module Types = } - /// The Formulary Product that is - /// available as a GenericProduct. + /// + /// The Formulary Product that is available as a GenericProduct: one row of the + /// "Formulary" sheet, parsed by Product.parseFormularyProducts. + /// + /// + /// Besides the hospital formulary itself, this sheet is the sole source of + /// enteral and parenteral product composition: rows are selected by + /// ProductType and turned into ProductComponents by + /// Product.Enteral.get / Product.Parenteral.get, with the + /// nutrition fields below supplying the substances (see + /// Product.getAdditionalSubstances for the substance naming). + /// + /// Field names equal sheet column names except where noted. Every column is + /// required: a missing column fails the parse. + /// type FormularyProduct = { - /// The GPK code + /// The GPK code; sheet column "GPKODE". A row with GPK "0" is a dummy + /// and is dropped. GPK: string - /// The type of procuct + /// The type of procuct; sheet column "Type", one of "medication", + /// "enteral" or "parenteral" (anything else is NoProduct) ProductType: ProductType - /// The department UMCU, ICC, NEO, ICK, etc... + /// The department UMCU, ICC, NEO, ICK, etc...; collected from the + /// same-named columns, each marked with "x" Departments: string list /// The generic name Generic: string @@ -187,7 +218,7 @@ module Types = TallMan: string /// The Divisibility of the product Divisible: int option - /// Use generic name + /// Use generic name; sheet column "UseGenName", "x" for true UseGenName: bool /// Mmol Mmol: BigRational option @@ -197,10 +228,16 @@ module Types = Brand: string option /// Generic name GenName: string option - /// GStand generic name + /// GStand generic name; sheet column "GStand" GStandName: string option - /// Unit + /// Unit: the form/base unit. Required for enteral and parenteral rows - + /// a row whose Unit does not parse is skipped by those derivations. Unit: string option + // The composition columns below carry a space in the sheet header: + // "Energy kCal", "Carb g", "Prot g", "Lip g", "Sod mmol", "Pot mmol", + // "Calc mmol", "Posph mmol", "Magn mmol", "Chlor mmol", "Iron mmol", + // "VitD IE". + /// Energy in kCal EnergyKCal: BigRational option /// Carbohydrates in g @@ -217,7 +254,7 @@ module Types = CalcMmol: BigRational option /// Phosphorus in mmol PosphMmol: BigRational option - /// Magnesium + /// Magnesium in mmol MagnMmol: BigRational option /// Chloride in mmol ChlorMmol: BigRational option @@ -225,11 +262,12 @@ module Types = IronMmol: BigRational option /// Vitamin D in IE VitDIE: BigRational option - /// Is reconstituted + /// Is reconstituted; sheet column "IsReconste", "x" for true + /// TODO rename to IsReconst IsReconste: bool - /// Is diluted + /// Is diluted; "x" for true IsDilute: bool - /// Is additive + /// Is additive; "x" for true IsAdditive: bool } @@ -676,33 +714,64 @@ module Types = [] module Data = + /// + /// One row of the "Units" sheet, parsed by Mapping.getUnitMapping. + /// Maps a long Z-Index unit name to the short name used throughout the + /// application. Mapping.mapUnit matches on any of Long/Short/MV and + /// yields "{Short}[{Group}]" for UnitsParse.fromString. + /// type UnitMapping = { + // sheet column "ZIndexUnitLong" Long: string + // sheet column "Unit" Short: string + // sheet column "MetaVisionUnit" MV: string + // sheet column "Group": the unit group (Mass, Volume, ...) that + // completes the unit expression Group: string } + /// + /// One row of the "Routes" sheet, parsed by Mapping.getRouteMapping. + /// Maps a long Z-Index route name to the short Dutch name. Long is + /// the canonical form: Mapping.mapRoute matches either and returns + /// Long. + /// type RouteMapping = { + // sheet column "ZIndex" Long: string + // sheet column "ShortDutch" Short: string } + /// /// Reference intake-totals row: per patient age/weight category, the /// unit/time-unit and per-time min/max limits used to compute and annotate - /// aggregated intake (volume, energy, etc.). Loaded from the "Totals" sheet. + /// aggregated intake (volume, energy, etc.). Loaded from the "Totals" sheet + /// by Mapping.getTotals and consumed by + /// Informedica.GenOrder.Lib.Totals.getTotals. + /// + /// + /// Unlike every other resource this one is optional: a load failure yields + /// an empty array plus a Warning rather than failing the whole load (see + /// Resources.defaultRegistry). + /// type TotalsData = { Name: string + // age bounds in DAYS MinAge: BigRational option MaxAge: BigRational option + // weight bounds in GRAMS MinWeight: BigRational option MaxWeight: BigRational option Unit: Unit option + // sheet column "Adj": the patient adjustment unit (kg, m2) Adj: Unit option TimeUnit: Unit option MinPerTime: BigRational option @@ -715,76 +784,147 @@ module Types = type HashId = string - /// Raw dose rule data + /// + /// One row of the "DoseRules" sheet, parsed by + /// DoseRuleData.parseDoseRuleData and built into DoseRules by + /// DoseRuleLoader.fromData. + /// + /// + /// Field names equal sheet column names except where a comment says + /// otherwise; DoseRuleData.headers is the canonical column order and + /// the emit order of dataToCsv. Several rows (one per + /// component/substance dose limit) make up a single dose rule. + /// + /// Columns read through getIfNull/getOpt may be absent from the + /// sheet; every other column is required and its absence fails the parse. + /// type DoseRuleData = { + // Hash over the row's identifying fields, Component and Substance + // included. Rows sharing a RowId are collapsed by dedupRowsByRowId; + // differing dose-limit values across such rows violate the invariant + // and raise a warning. RowId: HashId + // Hash of the dose rule this row contributes to; shared by every + // component/substance row of one rule. Becomes DoseRule.DataId. RuleId: HashId + // sheet column "GrpId": shared by every rule in the same clinical + // context, which differ only in dose type/schedule GrpId: HashId + // Ordinal rank within a rule group, for stable presentation order. + // Defaults to 1 when absent or unparsable. SortNo: int + // Parsed into the Source DU (Identified/Other) Source: string + // The original free text this structured row was derived from SourceText: string Generic: GenericData Indication: string Route: string + // The original free-text description of the patient category PatientText: string Patient: PatientCategoryData ScheduleText: string ScheduleData: ScheduleData + // Validation stamp -> DoseRule.Validated Validated: string option + // G-Standaard check outcome -> DoseRule.Check. Empty when the rule + // agrees with the G-Standaard; written back by Export.fs. FreqCheck: string option DoseCheck: string option } + /// + /// The generic-identifying columns of a "DoseRules" row. Combined at ingest + /// into the Generic record, where the label takes the brand over the + /// form when both are present. Only ONE narrowing survives parsing, by + /// precedence HPKs > GPKs > Brand > Form (see + /// DoseRuleData.withSingleNarrowing). + /// and GenericData = { + // sheet column "Generic": the BASE substance name. External lookups + // (G-Standaard dose check, solution/renal rule matching) key on this, + // not on the display label. Name: string Form: string Brand: string + // ';'-separated in the sheet GPKs: string array HPKs: string array } + /// The patient-category columns of a "DoseRules" row: the demographic ranges + /// that decide which patients a rule applies to. and PatientCategoryData = { + // sheet column "Loc": organizational location / hospital / institute. + // Read tolerantly and absent from `headers`. Location: string + // sheet column "Dep": department / ward Dep: string + // sheet column "IsAdult", "x" for true: the rule applies to adults, + // asserted categorically, so MinAge/MaxAge are empty for such a row. + // Empty asserts NOTHING - it is never a negative. See ADR-0021. IsAdult: bool Gender: Gender + // age bounds in DAYS MinAge: BigRational option MaxAge: BigRational option + // weight bounds in GRAMS MinWeight: BigRational option MaxWeight: BigRational option + // body surface area bounds in m2 MinBSA: BigRational option MaxBSA: BigRational option + // gestational age bounds in DAYS MinGestAge: BigRational option MaxGestAge: BigRational option + // post-menstrual age bounds in DAYS MinPMAge: BigRational option MaxPMAge: BigRational option } + /// The dose-type and schedule columns of a "DoseRules" row. and ScheduleData = { + // one of: once, onceTimed, discontinuous, timed, continuous DoseType: string + // free-text description of the dose type, may be empty DoseText: string + // sheet column "Freqs", ';'-separated Freqs: BigRational array + // the patient adjustment unit (kg, m2) AdjustUnit: string FreqUnit: string RateUnit: string + // infusion time of a single dose, in TimeUnit MinTime: BigRational option MaxTime: BigRational option TimeUnit: string + // sheet columns "MinInt"/"MaxInt": interval between two doses, + // in IntUnit MinInt: BigRational option MaxInt: BigRational option IntUnit: string + // sheet columns "MinDur"/"MaxDur": duration of the whole rule, + // in DurUnit MinDur: BigRational option MaxDur: BigRational option DurUnit: string DoseLimitData: DoseLimitData } + /// The dose-limit columns of a "DoseRules" row. Quantities are per + /// administration, PerTime is accumulated per FreqUnit, Rate is continuous + /// delivery speed; the *Adj variants are normalized to AdjustUnit. + /// + /// Note there is deliberately no NormQtyAdj/NormPerTimeAdj here - normative + /// adjusted doses exist on RenalRuleData only. and DoseLimitData = { + // sheet column "CmpBased", "x" for true: the limit targets the + // component rather than a single substance CmpBased: bool Component: string Substance: string @@ -829,7 +969,17 @@ module Types = Warnings: string list } - /// Raw solution rule data + /// + /// One row of the "SolutionRules" sheet, parsed by + /// SolutionRule.parseSolutionRuleData and built into + /// SolutionRules by SolutionRule.map. Describes how a + /// medication must be diluted and administered. + /// + /// + /// Field names equal sheet column names except where noted. Generic, Route + /// and Indication must match the corresponding DoseRule. Every column is + /// required: a missing column fails the parse. + /// type SolutionRuleData = { // solution rule section @@ -837,66 +987,107 @@ module Types = Form: string Route: string Indication: string + // sheet column "Loc": organizational location / hospital / institute Location: string option + // sheet column "Dep": department / ward Department: string option + // administration access device, "x" for applies CVL: string PVL: string + // age bounds in DAYS MinAge: BigRational option MaxAge: BigRational option + // weight bounds in GRAMS MinWeight: BigRational option MaxWeight: BigRational option + // gestational age bounds in DAYS MinGestAge: BigRational option MaxGestAge: BigRational option + // dose range this solution rule applies to (a selection constraint, + // not a limit to compute) MinDose: BigRational option MaxDose: BigRational option + // one of: once, onceTimed, discontinuous, timed, continuous DoseType: string DoseText: string + // sheet column "Solutions": acceptable diluents, '|'-separated Solutions: string list + // sheet column "Div": divisibility of the solution Div: BigRational option + // standard volumes in mL, ';'-separated Volumes: BigRational array + // total volume bounds in mL MinVol: BigRational option MaxVol: BigRational option + // volume bounds per kg, in mL/kg MinVolAdj: BigRational option MaxVolAdj: BigRational option + // administration fraction: the percentage of the solution that makes + // up one dose quantity MinPerc: BigRational option MaxPerc: BigRational option // solution limit section Component: string Substance: string + // sheet column "Unit": the substance unit the quantity and + // concentration limits below are expressed in Unit: string + // standard substance quantities, ';'-separated Quantities: BigRational array MinQty: BigRational option MaxQty: BigRational option MinQtyAdj: BigRational option MaxQtyAdj: BigRational option + // infusion (drip) rate bounds in mL/hour MinDrip: BigRational option MaxDrip: BigRational option + // concentration bounds in Unit/mL MinConc: BigRational option MaxConc: BigRational option } - /// Raw renal rule data + /// + /// One row of the "RenalRules" sheet, parsed by + /// RenalRule.parseRenalRuleData. Adjusts the dose advice of a + /// matching DoseRule according to renal function; applied to patients of + /// 28 days and older. + /// + /// + /// Field names equal sheet column names except where noted. Generic, Route, + /// Indication and Substance must match the corresponding DoseRule. Every + /// column is required: a missing column fails the parse. + /// type RenalRuleData = { Generic: string Route: string Indication: string Source: string + // age bounds in DAYS MinAge: BigRational option MaxAge: BigRational option + // dialysis modality this rule applies to, "x" for applies ContDial: string IntDial: string PerDial: string + // standardized GFR bounds in mL/min/1.73m2 MinGFR: BigRational option MaxGFR: BigRational option + // one of: once, onceTimed, discontinuous, timed, continuous DoseType: string DoseText: string + // sheet column "Freqs", ';'-separated Frequencies: BigRational array + // sheet columns "MinInt"/"MaxInt", in IntervalUnit MinInterval: BigRational option MaxInterval: BigRational option + // sheet column "IntUnit" IntervalUnit: string Substance: string + // sheet column "DoseRed": how to read the dose values below - + // "rel" for a relative (multiplier) adjustment, "abs" for an + // absolute one DoseRed: string DoseUnit: string AdjustUnit: string @@ -904,11 +1095,14 @@ module Types = RateUnit: string MinQty: BigRational option MaxQty: BigRational option + // " - "-separated in the sheet (e.g. "4 - 5"), unlike the + // ';'-separated multi-value columns elsewhere NormQtyAdj: BigRational array MinQtyAdj: BigRational option MaxQtyAdj: BigRational option MinPerTime: BigRational option MaxPerTime: BigRational option + // " - "-separated, see NormQtyAdj NormPerTimeAdj: BigRational array MinPerTimeAdj: BigRational option MaxPerTimeAdj: BigRational option diff --git a/src/Informedica.GenPRES.Client/Utils.fs b/src/Informedica.GenPRES.Client/Utils.fs index a2e1333d..4575e493 100644 --- a/src/Informedica.GenPRES.Client/Utils.fs +++ b/src/Informedica.GenPRES.Client/Utils.fs @@ -95,6 +95,13 @@ module GoogleDocs = $"https://docs.google.com/spreadsheets/d/{id}/gviz/tq?tqx=out:csv&sheet={sheet}" + // The emergency-list spreadsheet. NOTE this is a SEPARATE workbook from the one + // GENPRES_URL_ID points at: the emergency sheets are fetched here by the client + // and never pass through GenFORM.Lib's resource loading. Its id is hard-coded + // rather than configurable, so a deployment cannot point emergency data + // elsewhere. Sheets used: "emergencylist", "continuousmeds", "products", + // "weight", "height", "weight neo", "height neo"; the parsers live in + // Shared/Models.fs. //https://docs.google.com/spreadsheets/d/1IbIdRUJSovg3hf8E5V-ZydMidlF_iG552vK5NotZLuM/edit?usp=sharing [] let private dataEMLUrlId = "1IbIdRUJSovg3hf8E5V-ZydMidlF_iG552vK5NotZLuM" diff --git a/src/Informedica.GenPRES.Client/Views/ViewHelpers.fs b/src/Informedica.GenPRES.Client/Views/ViewHelpers.fs index 2cd1643c..354c1922 100644 --- a/src/Informedica.GenPRES.Client/Views/ViewHelpers.fs +++ b/src/Informedica.GenPRES.Client/Views/ViewHelpers.fs @@ -200,8 +200,10 @@ module ViewHelpers = | _ -> None + /// /// Like but without a feasibility ceiling — the prediction /// follows the increment grid freely (mirroring the server's unbounded step). + /// let ovarStep (format: decimal -> string) (ovar: OrderVariable) : (int * int -> string * string) option = ovarStepTo None format ovar @@ -242,12 +244,14 @@ module ViewHelpers = ovar |> stepsToCeiling ceiling (definedIncrement ovar) + /// /// Like but counts steps of the LARGE increment /// (the server's calculated LargeIncr used by the jump buttons), falling back to the /// defined increment when the server emits none. Lets a large step saturate at the /// feasibility ceiling exactly like a small step, so the larger increment does not /// overshoot the ceiling and get reverted by the solver. None when there is no ceiling /// or no usable increment. + /// let largeIncrementStepsToCeiling (ceiling: decimal option) (ovar: OrderVariable) : int option = let largeIncr = ovar.LargeIncr |> Option.bind firstSnd |> Option.orElse (definedIncrement ovar) diff --git a/src/Informedica.GenPRES.Shared/Models.fs b/src/Informedica.GenPRES.Shared/Models.fs index f76f80fe..55158543 100644 --- a/src/Informedica.GenPRES.Shared/Models.fs +++ b/src/Informedica.GenPRES.Shared/Models.fs @@ -1016,6 +1016,19 @@ module Models = } + /// + /// Bolus medication for resuscitation scenarios: parse reads the + /// "emergencylist" sheet of the emergency-list spreadsheet (see + /// dataEMLUrlId in the client's Utils - a different workbook from the + /// GENPRES_URL_ID one, loaded by the client rather than by GenFORM.Lib). + /// + /// + /// Columns: hospital, indication, medication, minWeight, maxWeight, dose, min, + /// max, conc, unit, remark. The four "template-generic" / "template-route" / + /// "template-dose-type" / "template-indication" columns are OPTIONAL - they are + /// read only when present in the header row - and preselect a prescription when + /// the user picks the entry. + /// module EmergencyTreatment = @@ -1360,6 +1373,13 @@ module Models = |> List.distinct + /// + /// Continuous infusion protocols: parse reads the "continuousmeds" sheet + /// of the emergency-list spreadsheet (see EmergencyTreatment above for + /// where that workbook lives). Columns: hospital, catagory [sic], indication, + /// dosetype, medication, generic, unit, doseunit, minweight, maxweight, + /// quantity, total, mindose, maxdose, absmax, minconc, maxconc, solution. + /// module ContinuousMedication = open Shared @@ -1517,6 +1537,11 @@ module Models = ) + /// + /// Available medication products and their concentrations: parse reads + /// the "products" sheet of the emergency-list spreadsheet. Columns: indication, + /// medication, conc, unit. + /// module Products = open Shared @@ -1550,6 +1575,12 @@ module Models = | _ -> [] + /// + /// Reference ranges for weight and height estimation: parse reads the + /// "weight", "height", "weight neo" and "height neo" sheets of the + /// emergency-list spreadsheet, all four sharing the columns sex ("M"/"F"), age + /// (years), p3, mean, p97. + /// module NormalValues = open Shared diff --git a/src/Informedica.Utils.Lib/AppPath.fs b/src/Informedica.Utils.Lib/AppPath.fs index 706d9c33..7f5af46a 100644 --- a/src/Informedica.Utils.Lib/AppPath.fs +++ b/src/Informedica.Utils.Lib/AppPath.fs @@ -34,9 +34,11 @@ module AppPath = open System.IO + /// /// Name of the environment variable that, when set, forces the application /// root explicitly. It must point at the directory containing data/ /// (for example /app in the Docker container). + /// [] let GENPRES_ROOT = "GENPRES_ROOT" @@ -122,25 +124,39 @@ module AppPath = Environment.CurrentDirectory |> Path.GetFullPath + /// /// Lazily resolved, memoized application root. Resolution is deferred so /// that any Env.loadDotEnv() setting GENPRES_ROOT runs first. + /// let root = lazy (resolveRoot ()) + /// /// The resolved application root directory (the directory containing data/). + /// let rootPath () = root.Value + /// /// The data/ directory under the application root. + /// let dataDir () = Path.Combine(rootPath (), "data") + /// /// The data/cache/ directory. + /// let cacheDir () = Path.Combine(dataDir (), "cache") + /// /// The data/zindex/ directory. + /// let zindexDir () = Path.Combine(dataDir (), "zindex") + /// /// The data/logs/ directory. + /// let logsDir () = Path.Combine(dataDir (), "logs") + /// /// The data/cache/interactions/ directory. + /// let interactionsDir () = Path.Combine(cacheDir (), "interactions") diff --git a/src/Informedica.Utils.Lib/BCL/RationalX.fs b/src/Informedica.Utils.Lib/BCL/RationalX.fs index 6c68414f..57d2aaa3 100644 --- a/src/Informedica.Utils.Lib/BCL/RationalX.fs +++ b/src/Informedica.Utils.Lib/BCL/RationalX.fs @@ -27,9 +27,11 @@ module private RationalXHelpers = let inline absL (a: int64) = if a < 0L then -a else a + /// /// Fold an int64 into an int32 hash, mixing the upper 32 bits in (a plain /// int cast would discard them, collapsing values that differ only /// above bit 32 into the same bucket). + /// let inline hash64 (x: int64) = int (x ^^^ (x >>> 32)) /// Greatest common divisor of two int64 values (always non-negative). diff --git a/src/Informedica.Utils.Lib/Csv.fs b/src/Informedica.Utils.Lib/Csv.fs index e55833fc..b999c48c 100644 --- a/src/Informedica.Utils.Lib/Csv.fs +++ b/src/Informedica.Utils.Lib/Csv.fs @@ -58,8 +58,22 @@ module Csv = |> unbox<'T> + /// /// Get a column from a row of a CSV file. /// Example: getColumn StringData [| "a"; "b" |] [| "1"; "2" |] "a" will return "1". + /// + /// + /// Header names are matched case-insensitively, and an unknown column name + /// RAISES rather than yielding a default. Callers that parse a whole sheet wrap + /// this in try/with and surface the failure as an Error, which makes the header + /// row of a sheet a checkable contract: feed a parser exactly the column set it + /// is supposed to read and it succeeds, drop any one of them and it fails. The + /// GenFORM column-contract tests rely on that. + /// + /// A column that may legitimately be absent therefore needs an explicit + /// tolerant wrapper at the call site (see DoseRuleData.getIfNull) - there is no + /// way to express optionality here. + /// let inline getColumn<'T> dataType columns row name = columns |> Array.tryFindIndex (String.equalsCapInsens name) diff --git a/tests/Informedica.GenFORM.Tests/FixtureJson.fs b/tests/Informedica.GenFORM.Tests/FixtureJson.fs index 4376a371..73642440 100644 --- a/tests/Informedica.GenFORM.Tests/FixtureJson.fs +++ b/tests/Informedica.GenFORM.Tests/FixtureJson.fs @@ -6,13 +6,15 @@ open Informedica.Utils.Lib.BCL open Newtonsoft.Json +/// /// Shared BigRational JSON helper for the offline round-trip fixtures. /// /// Defined ONCE and used by both the round-trip tests (Tests.fs) and the /// fixture generator (Scripts/DownloadFixtures.fsx, which #loads /// this file), so the writer and the reader can never drift out of sync — a /// mismatch would make every committed fixture silently fail to deserialize. -/// +/// +/// /// This deliberately does NOT reuse Informedica.Utils.Lib.Json: that /// converter (a) checks t = typeof<BigRational>, which never matches /// because MathNet instances are the nested type BigRational+Q, and @@ -22,6 +24,7 @@ open Newtonsoft.Json /// matches with IsAssignableFrom and round-trips a BigRational as a /// compact, BigInteger-safe "num/den" string. Newtonsoft's built-in F# union /// converter handles ValueUnit / Unit / option. +/// module FixtureJson = type BigRationalConverter() = diff --git a/tests/Informedica.GenFORM.Tests/Tests.fs b/tests/Informedica.GenFORM.Tests/Tests.fs index 33c90513..6b96653f 100644 --- a/tests/Informedica.GenFORM.Tests/Tests.fs +++ b/tests/Informedica.GenFORM.Tests/Tests.fs @@ -925,7 +925,338 @@ module DoseRuleToDataTests = data |> DP.buildRules |> Array.collect DoseRule.toData - let tests = + /// + /// Declares which spreadsheet columns each sheet parser reads, and fails when a + /// parser stops agreeing with its declaration. These tests, not a document, are + /// the specification of the sheet contract. + /// + /// + /// Csv.getColumn RAISES on a column that is not in the header row, and + /// every sheet parser wraps its body in try/with -> Error. So two assertions + /// fix a declared column set exactly: + /// + /// 1. parsing a sheet whose header row is EXACTLY the declared set succeeds + /// => the parser reads no column outside the declared set; + /// 2. dropping any single declared column makes the parse fail + /// => the parser really reads every declared column. + /// + /// Columns that may legitimately be absent are listed in tolerated and + /// are exempt from (2) only; that list is the optionality contract. + /// + /// A failure means the parser and the declared column set disagree. Fix + /// whichever is wrong - do NOT relax the test. If a column genuinely became + /// optional, move it to tolerated and make the parser read it through a + /// tolerant reader. + /// + /// Verified by mutation: renaming get "MinQty" to get "MinQuantity" + /// in SolutionRule.fs turns assertion (1) for SolutionRules red. Note it is (1) + /// that catches a rename - (2) stays green because the parse then fails for the + /// wrong reason - so do not weaken (1) into "parses with at least these columns". + /// + module ColumnContract = + + + /// A sheet with the given header row and ONE data row. Cells are empty + /// unless overrides gives a value: every parser must cope with an + /// empty cell, so an empty row keeps each fixture down to the columns that + /// must carry a value for the parser to reach the rest. + let mkSheet (overrides: (string * string) list) (columns: string list) = + let row = + columns + |> List.map (fun c -> + overrides + |> List.tryFind (fst >> String.equalsCapInsens c) + |> Option.map snd + |> Option.defaultValue "" + ) + + [| columns |> List.toArray; row |> List.toArray |] + + + /// The unit mapping the FormRoute fixture needs: without a resolvable dose + /// unit that parser short-circuits and never reads its dose columns. + let unitMappingFixture = + [| + { + Long = "milligram" + Short = "mg" + MV = "mg" + Group = "Mass" + } + |] + + + /// The DoseRules columns, taken from the ONE production list so the two + /// cannot drift. headers is a single tab-joined line. + let doseRuleColumns = + let fromHeaders = + DoseRuleData.headers |> List.head |> String.split "\t" |> List.map String.trim + + // "Loc" is read by the parser but missing from `headers` - see the TODO + // there. Declared here because the sheet does carry it. + fromHeaders @ [ "Loc" ] + + + /// name, declared columns, columns that may be absent, empty-cell + /// overrides, and the parser under test reduced to a success flag. + let sheets: (string * string list * string list * (string * string) list * (string[][] -> bool)) list = + [ + "Routes", + [ "ZIndex"; "ShortDutch" ], + [], + [], + (Mapping.parseSheet Mapping.routeMappingRow >> Result.isOk) + + "Units", + [ "ZIndexUnitLong"; "Unit"; "MetaVisionUnit"; "Group" ], + [], + [], + (Mapping.parseSheet Mapping.unitMappingRow >> Result.isOk) + + "ValidForms", [ "Form" ], [], [], (Mapping.parseSheet Mapping.validFormRow >> Result.isOk) + + "FormRoute", + [ + "Route" + "Form" + "Unit" + "DoseUnit" + "MinDoseQty" + "MaxDoseQty" + "MinDoseQtyKg" + "MaxDoseQtyKg" + "Divisible" + "Timed" + "Reconstitute" + "IsSolution" + ], + [], + [ "Unit", "mg"; "DoseUnit", "mg" ], + (Mapping.parseSheet (Mapping.formRouteRow unitMappingFixture) >> Result.isOk) + + "Totals", + [ + "Name" + "MinAge" + "MaxAge" + "MinWeight" + "MaxWeight" + "Unit" + "Adj" + "TimeUnit" + "MinPerTime" + "MaxPerTime" + "MinPerTimeAdj" + "MaxPerTimeAdj" + ], + [], + [], + (Mapping.parseSheet Mapping.totalsRow >> Result.isOk) + + "Reconstitution", + [ + "GPK" + "Route" + "Loc" + "Dep" + "DiluentVol" + "ExpansionVol" + "Diluents" + ], + [], + [], + (Product.Reconstitution.parseReconstitution >> Result.isOk) + + "Formulary", + [ + "GPKODE" + "Type" + "UMCU" + "ICC" + "NEO" + "ICK" + "HCK" + "Generic" + "TallMan" + "Divisible" + "UseGenName" + "Mmol" + "Form" + "Brand" + "GenName" + "GStand" + "Unit" + "Energy kCal" + "Carb g" + "Prot g" + "Lip g" + "Sod mmol" + "Pot mmol" + "Calc mmol" + "Posph mmol" + "Magn mmol" + "Chlor mmol" + "Iron mmol" + "VitD IE" + "IsReconste" + "IsDilute" + "IsAdditive" + ], + [], + [], + (Product.parseFormularyProducts >> Result.isOk) + + "DoseRules", + doseRuleColumns, + // read through getIfNull / getOpt / getInt, so a sheet may omit them + [ + "RowId" + "RuleId" + "GrpId" + "SortNo" + "SourceText" + "PatientText" + "ScheduleText" + "Loc" + "Validated" + "FreqCheck" + "DoseCheck" + ], + [], + (DoseRuleData.parseDoseRuleData >> Result.isOk) + + "SolutionRules", + [ + "Generic" + "Form" + "Route" + "Indication" + "Loc" + "Dep" + "CVL" + "PVL" + "MinAge" + "MaxAge" + "MinWeight" + "MaxWeight" + "MinGestAge" + "MaxGestAge" + "MinDose" + "MaxDose" + "DoseType" + "DoseText" + "Solutions" + "Volumes" + "Div" + "MinVol" + "MaxVol" + "MinVolAdj" + "MaxVolAdj" + "MinPerc" + "MaxPerc" + "Component" + "Substance" + "Unit" + "Quantities" + "MinQty" + "MaxQty" + "MinQtyAdj" + "MaxQtyAdj" + "MinDrip" + "MaxDrip" + "MinConc" + "MaxConc" + ], + [], + [], + (SolutionRule.parseSolutionRuleData >> Result.isOk) + + "RenalRules", + [ + "Generic" + "Route" + "Indication" + "Source" + "MinAge" + "MaxAge" + "IntDial" + "ContDial" + "PerDial" + "MinGFR" + "MaxGFR" + "DoseType" + "DoseText" + "Freqs" + "DoseRed" + "DoseUnit" + "AdjustUnit" + "FreqUnit" + "RateUnit" + "MinInt" + "MaxInt" + "IntUnit" + "Substance" + "MinQty" + "MaxQty" + "NormQtyAdj" + "MinQtyAdj" + "MaxQtyAdj" + "MinPerTime" + "MaxPerTime" + "NormPerTimeAdj" + "MinPerTimeAdj" + "MaxPerTimeAdj" + "MinRate" + "MaxRate" + "MinRateAdj" + "MaxRateAdj" + ], + [], + [], + (RenalRule.parseRenalRuleData >> Result.isOk) + ] + + + let tests = + testList + "sheet column contract" + [ + for name, columns, tolerated, overrides, parseOk in sheets do + testList + name + [ + test "the declared columns are sufficient to parse the sheet" { + columns + |> mkSheet overrides + |> parseOk + |> Expect.isTrue + $"the %s{name} parser must not read any column outside its declared set" + } + + for col in columns |> List.filter (fun c -> tolerated |> List.contains c |> not) do + test $"the sheet cannot be parsed without column {col}" { + columns + |> List.filter (fun c -> c <> col) + |> mkSheet overrides + |> parseOk + |> Expect.isFalse + $"%s{name}.%s{col} is declared but never read - drop it or read it" + } + + for col in tolerated do + test $"column {col} may be absent" { + columns + |> List.filter (fun c -> c <> col) + |> mkSheet overrides + |> parseOk + |> Expect.isTrue + $"%s{name}.%s{col} is declared optional but the parser requires it" + } + ] + ] + + + let roundTripTests = testList "DoseRule.toData round-trip" [ @@ -996,6 +1327,9 @@ module DoseRuleToDataTests = ] + let tests = testList "DoseRule data" [ roundTripTests; ColumnContract.tests ] + + /// Full DoseRule round-trip on OFFLINE fixtures (no network/cache/env). /// Fixtures (doserules/routemappings/products .json) are generated once by /// Scripts/DownloadFixtures.fsx from the DEMO data and committed; the .fsproj