Instructions for AI coding agents working on the GenPRES repository. Make edits small, test-driven, and follow existing repository patterns.
⚠️ CRITICAL — SCRIPT-ONLY CODE POLICY⚠️ DO NOT write new code in source files (
.fs). All new features, fixes, enhancements, and experiments MUST be implemented exclusively in F# Interactive script files (.fsx) in theScripts/directories. The user will review your work and decide what to migrate to source files.Allowed changes to
.fssource files:
- Adding, updating, or correcting comments and documentation
- Targeted refactoring of a single function when explicitly requested by the user
- Client-side UI code in
src/Informedica.GenPRES.Client/— this is the only exception, because Fable/Elmish UI code cannot be run in FSI scriptsNOT allowed in other
.fssource files:
- Adding new functions or modules
- Implementing new features or bug fixes
- Any code change not explicitly requested as a source-file edit
This policy exists because GenPRES is a medical device software project. Unreviewed code changes to source files risk introducing unvalidated behavior into clinical medication workflows. The user is the sole gatekeeper for source file changes.
See the Script-Based Development Workflow section below for how to work within this constraint.
GenPRES is a Clinical Decision Support System (CDSS) for medication prescribing, built entirely in F# using the SAFE Stack (Saturn, Azure, Fable, Elmish). It provides safe and efficient medication order entry, calculation, and validation for medical settings.
- .NET SDK, Node.js, and npm
For the canonical list of supported versions, see the Toolchain Requirements section in DEVELOPMENT.md. For environment variables, see DEVELOPMENT.md.
IMPORTANT: This repository contains multiple projects. Always specify the solution file:
# CORRECT - build the entire solution
dotnet run build
# CORRECT - run the server tests
dotnet run servertests
# INCORRECT - will fail with "more than one project" error
dotnet build
dotnet testdotnet run- Start full application (server + client with hot reload)dotnet run list- Show all available build targetsdotnet run Build- Build the solutiondotnet run Bundle- Create production bundledotnet run Clean- Clean build artifacts- Access the application at
http://localhost:5173
dotnet run ServerTests- Run all F# unit tests using Expectodotnet run TestHeadless- Run tests in headless modedotnet run WatchTests- Run tests in watch modedotnet test GenPRES.sln- Alternative way to run all tests
Individual library tests:
dotnet test tests/Informedica.GenSOLVER.Tests/
dotnet test tests/Informedica.GenORDER.Tests/
dotnet test tests/Informedica.GenUNITS.Tests/
# ... etc for other test projectsdotnet run Format- Format F# code using Fantomas
docker build --build-arg GENPRES_URL_ARG="your_secret_url_id" -t halcwb/genpres .docker run -it -p 8080:8085 halcwb/genpresdotnet run DockerRun- Run pre-built Docker image
- F# libraries under
src/ - Tests:
tests/(Expecto + FsCheck). Look for BigRational and ValueUnit tests. - Resource loading and tests:
src/Informedica.GenForm.Lib/Api.fsandtests/ - 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/genpres_resource_requirements.md
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!
- All medication rules and constraints stored in Google Spreadsheets
- Downloaded as CSV and parsed dynamically
GENPRES_URL_IDenvironment variable controls which spreadsheet to use- Local cache files provide offline medication data access
- Client-server communication via Fable.Remoting (type-safe RPC)
- API contracts defined in
src/Informedica.GenPRES.Shared/Api.fs - Server processes medication calculations and returns validated results
- Docs with sheet specs:
docs/mdr/design-history/genpres_resource_requirements.md. - Check
genpres_resource_requirements.mdfor expected sheet and column names. - Resources are loaded from Google Sheets via
Web.getDataFromSheet dataUrlId "SheetName". - Mapping helper functions use
Csv.getStringColumn/Csv.getFloatOptionColumnand call getString/getFloat-style delegates. - The central
ResourceConfig(inApi.fs) expects functions returningGenFormResult<'T>(alias forResult<'T, Message list>). Use the*Resultvariants where present (e.g.,Mapping.getRouteMappingorMapping.getRouteMappingResult) and wrap withdelaywhen the signature expects aunit -> GenFormResult<_>. - To add/modify sheet mappings: adjust the mapper in the corresponding module (e.g.,
Product.Reconstitution.get,DoseRule.get) and updategenpres_resource_requirements.mdto reflect column names. - Update the mapper to read columns by name using the
getdelegate (e.g.,let get = getColumn row in get "Generic"), parse withBigRational.toBrs/getFloatas appropriate. - If adding optional numeric columns, use
getFloatOptionColumnandOption.bind BigRational.fromFloat.
- IO and parsing functions should return
GenFormResult<'T>(i.e., Result). UseFsToolkit.ErrorHandling.ResultCEcomputation expression for readability (result { let! x = ... }). - When editing
ResourceConfigor callers, make sure to handleResultvalues consistently; useResult.bind, CE, ordelayfor unit-returning getters.
- BigRational operations are used broadly for dosing math. Respect existing helpers in
Informedica.GenUnits.Lib. removeBigRationalMultiplessemantics: it keeps the smallest positive BigRational representatives and removes later values that are integer multiples of a previously kept value. Example: [1/3; 1/2; 1] → keep 1/2 and 1/3 (both non-multiples of each other), but if 1/2 and 1 are present, keep 1/2 and remove 1 (1 is multiple of 1/2).- Use
BigRational.isMultiplewhen reasoning about integer multiples. - Prefer using existing helpers like
ValueUnit.singleWithUnit,ValueUnit.withUnit, etc., when manipulating units. - Use BigRational for all medication calculations (absolute precision).
- Use
[<RequireQualifiedAccess>]on DUs and modules.
All tests use Expecto with Expecto.Flip for fluent assertions:
open Expecto
open Expecto.Flip
test "example test" {
actual
|> Expect.equal "should match expected" expected
}Test scenarios are defined in tests/Informedica.GenORDER.Tests/Scenarios.fs and include:
pcmSupp- Paracetamol suppositoryamfo- Amphotericin B liposomal IVmorfCont- Morphine continuous infusionpcmDrink- Paracetamol oral liquidcotrim- Cotrimoxazoletpn/tpnComplete- Total parenteral nutritionfullMedication- Fully populated medication (all fields set)
MSBUILD : error MSB1011: Specify which project or solution file to use
because this folder contains more than one project or solution file.
Solution: Always specify GenPRES.sln:
dotnet build GenPRES.sln
dotnet test GenPRES.slnIf FSI scripts fail to load dependencies, ensure you're running from the script's directory:
cd src/Informedica.GenORDER.Lib/Scripts
dotnet fsi Tests.fsxIf FSI scripts fail because DLLs are not found, rebuild the solution first:
dotnet build GenPRES.slnIMPORTANT: All new code MUST be written in .fsx script files only — never in .fs source files. The user will review and migrate verified code to the codebase. See the critical policy at the top of this document.
GenPRES uses an FSI script-based workflow for safely implementing new functionality in a mature ("brown-field") codebase. Instead of modifying production source files directly, you copy or shadow existing code into .fsx scripts, experiment and test interactively, and only migrate verified code back to the codebase.
Commit d51252c added a "pick nearest higher else lower component quantity" feature that ultimately touched 3 libraries and 7 source files (Array.fs, ValueUnit.fs, OrderVariable.fs, Order.fs, OrderProcessor.fs). But it was prototyped first in a single script — src/Informedica.GenUNITS.Lib/Scripts/Api.fsx:
#load "load.fsx" // loads GenUnits source files + compiled Utils DLL
open Informedica.GenUnits.Lib
// 1. Prototype a helper that belongs in Utils.Lib
module Array =
let inline pickNearestHigherElseLower target xs =
if Array.isEmpty xs then invalidArg "xs" "Array cannot be empty"
let ys = xs |> Array.sort
match ys |> Array.tryFind (fun x -> x >= target) with
| Some x -> x // smallest value >= target
| None -> ys[ys.Length - 1] // no higher value: take highest lower
// 2. Prototype a ValueUnit function that uses the Array helper above
module ValueUnit =
let pickNearestHigherElseLower (target: ValueUnit) (candidates: ValueUnit) =
if candidates |> ValueUnit.isEmpty then candidates
elif candidates |> ValueUnit.eqsGroup target |> not then candidates
else
candidates
|> ValueUnit.toBase
|> ValueUnit.applyToValue (fun brs1 ->
target
|> ValueUnit.getBaseValue
|> Array.tryExactlyOne
|> Option.map (fun br ->
[| brs1 |> Array.pickNearestHigherElseLower br |]
)
|> Option.defaultValue brs1
)
|> ValueUnit.toUnitBecause load.fsx loads the GenUnits source files via #load and references the compiled Utils DLL via #r, you can prototype functions from multiple libraries in one interactive session. Once the logic is verified in FSI, the code is migrated to the appropriate source files across projects.
Every library has a Scripts/ directory containing:
load.fsx— Bootstrap script that loads compiled DLLs from dependent libraries and#loads the library's own.fssource files. This gives FSI access to the full library context.- Development scripts (e.g.,
Solver.fsx,Medication.fsx,Tests.fsx) — Working scripts for experimentation and testing.
Example load.fsx pattern:
#r "nuget: MathNet.Numerics.FSharp"
#r "../../Informedica.Utils.Lib/bin/Debug/net10.0/Informedica.Utils.Lib.dll"
#load "../Types.fs"
#load "../Variable.fs"
#load "../Solver.fs"
// ... etc- Set the current directory — Always start with
Environment.CurrentDirectory <- __SOURCE_DIRECTORY__so relative paths resolve correctly. - Load project context — Use
#load "load.fsx"to load all dependencies. - Reference NuGet packages inline — Use
#r "nuget: Expecto, 9.0.4"for test frameworks or other packages. - Copy only the code you need — Don't drag entire modules; start with just the functions you plan to modify.
- Modify and extend — Refactor, optimize, or add new features in the script.
- Write tests in the same script — Verify your changes with inline Expecto tests.
- Reuse existing test suites — Load test files from the
tests/directory via#loadand run them against your modified code. - Migrate when confident — Once verified, move the improved code back into the source files.
Shadow an existing module to extend it with new functions while keeping all original functions accessible:
#load "load.fsx"
open Informedica.GenOrder.Lib
// Shadow the Medication module to add new functions
module Medication =
// Open the original module - all existing functions become available
open Informedica.GenOrder.Lib.Medication
// Add new function
let fromString (s: string) : Result<Medication, string list> =
// implementation...
// Existing functions like toString, template, toOrderDto are now
// automatically available as Medication.toString, etc.NOTE The module has the same name as the original (Medication), but because it's defined in the script, it shadows the original module. By opening the original module inside the new one, you bring all existing functions into scope, allowing you to call them as if they were part of the new module.
This allows calling both new and existing functions through the same module name:
let text = myMed |> Medication.toString // original function
let parsed = text |> Medication.fromString // new functionWrite Expecto tests directly in the script file:
#r "nuget: expecto"
open Expecto
open Expecto.Flip
let tests =
testList "feature tests" [
test "roundtrip works" {
let original = Scenarios.pcmSupp
let text = original |> Medication.toString |> String.concat "\n"
match text |> Medication.fromString with
| Error errs -> failwith $"Parse failed: {errs}"
| Ok parsed ->
parsed.Id |> Expect.equal "Id matches" original.Id
}
]
runTestsWithCLIArgs [] [||] testsYou can also reuse existing tests from the test projects:
// Load existing tests directly
#load "../../../tests/Informedica.GenSOLVER.Tests/Tests.fs"
open Informedica.GenSolver.Tests
// Run existing test suites against your modified codeThe fsi-mcp-server provides a persistent FSI session accessible via MCP tools. This enables AI-assisted interactive F# development without restarting FSI between queries.
Available MCP tools:
mcp__fsi-mcp__get_fsi_status— Check if the server is runningmcp__fsi-mcp__send_fsharp_code— Execute F# code (end statements with;;)mcp__fsi-mcp__load_f_sharp_script— Load and execute.fsxscript filesmcp__fsi-mcp__get_recent_fsi_events— View recent FSI output and errors
Path resolution strategy:
FSI's #load directive resolves relative paths from its include path, not from System.IO.Directory.GetCurrentDirectory(). When loading scripts via MCP, always start by adding the script's directory to FSI's include path using #I:
// Step 1: Set the include path to the script's directory
#I "/absolute/path/to/script/directory";;
// Step 2: Now relative #load paths resolve correctly
#load "../Types.fs";;
#load "../Utils.fs";;
#load "load.fsx";;Important:
System.IO.Directory.SetCurrentDirectory()does not affect#loadpath resolution — you must use#I- The MCP
load_f_sharp_scripttool sends script statements to FSI individually, so#loaddirectives inside scripts also resolve from FSI's include path. Set#Ibefore callingload_f_sharp_script - Scripts should include
#I __SOURCE_DIRECTORY__at the top so they work both when run viadotnet fsi(where__SOURCE_DIRECTORY__is the script's directory) and when loaded after manually setting#Ivia MCP - The FSI session is persistent — types loaded multiple times create conflicts (e.g.,
FSI_0005.Types.gramvsFSI_0010.Types.gram). Load dependencies once per session. If conflicts occur, the FSI server must be restarted - DLL reference changes require a manual restart. Once a DLL is loaded via
#r, the .NET runtime cannot unload it. If you rebuild a referenced DLL (e.g., afterdotnet run build), the FSI session will still use the old version. Reloading source files via#loaddoes not have this problem — they are recompiled each time. Agent action: After any build that changes referenced DLLs, prompt the user to manually restart the FSI MCP server before continuing with FSI work
- Partial evaluation — Select part of a script and send it to FSI to validate small functions without reloading everything.
- Keep FSI sessions alive — Build up state interactively rather than restarting FSI each time.
- Modularize scripts — Break scripts into logical regions (helpers, refactored code, tests) with comments for easier navigation.
- Rebuild before scripting — Run
dotnet build GenPRES.slnfirst soload.fsxcan find the compiled DLLs.
- Production requires proprietary medication cache files (not in repository)
- Demo version uses sample medication data included in repository
- Google Spreadsheets contain live configuration — changes affect running systems
- 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/genpres_resource_requirements.mdif spreadsheet columns or semantics changed. - Add notes to CONTRIBUTING.md if the change introduces a new external dependency or changes deployment behavior.
This policy applies to all contributors, not just AI agents.
LLMs must not be given direct write access to
.fssource files, except for client-side UI code insrc/Informedica.GenPRES.Client/.
Contributors using AI coding tools (GitHub Copilot, Claude, Cursor, Warp AI, etc.) must route all non-UI code through .fsx scripts first, following the script-based development workflow described above. The human contributor is responsible for reviewing, verifying, and manually migrating script code into source files.
This restriction exists because GenPRES is a medical device software project. Allowing LLMs to directly modify source files risks introducing unvalidated behavior into clinical medication workflows. Human review of every source file change is a safety requirement.
Contributors must also disclose when code submitted in a pull request is vibe coded — see CONTRIBUTING.md for the definition and disclosure requirements.
- Small, focused change with < 300 LOC modified when possible.
- Add or update unit tests covering the change.
- Ensure
dotnet run servertestspasses locally for affected projects. - Update
genpres_resource_requirements.mdif spreadsheet column names or semantics change. - Use conventional commit message with scope and short description.
- Coding standards: F# Coding Instructions
- Code formatting: F# Code Formatting
- Commit conventions: Commit Message Instructions
- Architecture: ARCHITECTURE.md
- Development setup: DEVELOPMENT.md
- Contributing: CONTRIBUTING.md
- Domain model:
docs/domain/core-domain.md