@smile-digital-health/crl
v4.92.1
Published
Clinical Reasoning Language (CRL) parser and validator
Readme
@smile-digital-health/crl
Clinical Reasoning Language (CRL) parser and validator
Clinical Reasoning Language (CRL) provides a structured way to model clinical decision logic, concept derivations, clinical activities, measurements, and summaries — while remaining simple, readable, and guideline-compatible.
Overview
See the User Guide for a comprehensive introduction to the CRL language, syntax, and authoring best practices. For the CLI and MCP tool reference (emit, validate, the 7 MCP tools — for both CRL and CEL), see TOOLING.md.
CRL is a domain-specific language designed for expressing clinical practice guidelines in a structured and machine-readable format. The language is implemented in TypeScript and provides a comprehensive set of tools for processing CRL documents.
Metadata annotations
CRL concepts can carry typed metadata via an @tag convention on meta lines (e.g. @description, @ke-feedback, @kg-concept) — for descriptions, knowledge-engineer feedback, plain-language logic, external-store hints, and extraction provenance. See the metadata model and the canonical tag registry. (Draft: the convention parses today; Validator enforcement is forthcoming.)
Installation
This is a private package. To install it, you need:
An npm account with access to
@smile-digital-healthpackages- Contact your team lead to request access
- You will receive an invitation to join the organization
- Each developer should use their own npm account
Set up authentication:
# Log in to npm - recommended for individual developers npm login
Or create/edit ~/.npmrc with your personal access token (note the second line is NOT a comment):
@smile-digital-health:registry=https://registry.npmjs.org/
//registry.npmjs.org/:_authToken=YOUR_NPM_TOKENSecurity Note: Always use your personal npm token. Do not share tokens between team members.
Once authenticated, install the package:
npm install @smile-digital-health/crlOr add it to your package.json:
"dependencies": {
"@smile-digital-health/crl": "^0.6.1"
}For Package Maintainers
Publishing to npm
The package is automatically published to npm when a new GitHub release is created (non-draft, non-prerelease). To verify what will be published:
# See what files would be included in the package
npm publish --dry-run
# Create the package locally without publishing
npm pack
# Examine contents of the generated .tgz file
tar -ztvf <package-name>.tgzCutting a release (build both artifacts + upload to GitHub)
Releases ship two artifacts side-by-side: the npm tarball (smile-digital-health-crl-<version>.tgz) and the VS Code extension VSIX (crl-language-support-<version>.vsix).
Heads-up (Windows): if you have VS Code open with the CRL extension active, its bundled MCP server keeps dist/ files open and both npm pack and npm run package will fail with EPERM on dist/. Close VS Code (or just disable the CRL extension) before running the build commands below.
# 1. Close VS Code (or disable the CRL extension) so the MCP server releases any lock on dist/.
# 2. From the repo root, build the npm tarball for the core package:
npm pack -w @smile-digital-health/crl
# 3. Build the extension VSIX (also from the repo root):
npm run package -w crl-language-support
# 4. Upload the resulting .tgz and .vsix to the corresponding GitHub release.Both artifacts are produced at the version declared in their respective package.json files. Bump those (and tag the commit v<version>) before running the steps above so the produced filenames match the release tag.
Features
Language Features
- Decision blocks with nested conditions
- Concept definitions with type and value specifications
- Activity statements with perform types
- Terminology statements with valueset, system/code, and unknown definitions
- FHIR resource type support
- String literals with proper escaping
- Comments (single-line and block)
Processing Features
- Lexical analysis with detailed token information
- Syntax parsing with error recovery
- AST generation with type safety
- Semantic validation with comprehensive error reporting
- Cross-platform compatibility (Windows, Mac, Linux)
Development Features
- TypeScript implementation for type safety
- Comprehensive test suite
- Detailed documentation
- Example implementations
- Development tools and utilities
CLI Usage
⚠️ WARNING: When using npm scripts and passing arguments to the underlying CLI, always use
--before your arguments. Otherwise, npm will not pass them to your script.Example:
npm run cli:lexer -- --path path/to/your/file.crl --pretty
The CRL package includes command-line tools for processing CRL files. Each tool supports the following options:
--path <file>: Specify the path to a CRL file to process. If omitted, the main example file is used by default.--pretty: Output formatted (pretty-printed) results instead of raw JSON.
Lexer Tool
npx ts-node src/cli/run-lexer.ts
npx ts-node src/cli/run-lexer.ts --pretty
npx ts-node src/cli/run-lexer.ts --path path/to/your/file.crl
npx ts-node src/cli/run-lexer.ts --path path/to/your/file.crl --prettyParser Tool
npx ts-node src/cli/run-parser.ts
npx ts-node src/cli/run-parser.ts --pretty
npx ts-node src/cli/run-parser.ts --path path/to/your/file.crl
npx ts-node src/cli/run-parser.ts --path path/to/your/file.crl --prettyAST Tool
npx ts-node src/cli/run-ast.ts
npx ts-node src/cli/run-ast.ts --pretty
npx ts-node src/cli/run-ast.ts --path path/to/your/file.crl
npx ts-node src/cli/run-ast.ts --path path/to/your/file.crl --prettyValidator Tool
npx ts-node src/cli/run-validator.ts
npx ts-node src/cli/run-validator.ts --pretty
npx ts-node src/cli/run-validator.ts --path path/to/your/file.crl
npx ts-node src/cli/run-validator.ts --path path/to/your/file.crl --prettyBy default, each command processes the main example CRL file. To process a different file, provide the file path using --path. Use --pretty for formatted output.
FSH-to-CRL Transformer Tool
The FSH-to-CRL transformer converts FHIR Shorthand (FSH) files into Clinical Reasoning Language (CRL) files. It supports advanced mapping and deduplication logic for activities, concepts, and terminology blocks.
Activity Mapping Enhancements
- Conditional
do not perform: If an activity in FSH hasdoNotPerform = true, the generated CRL will emitdo not performinstead ofperformfor that activity. - Activity Terminology Block Emission: For activities with a
medicationCodeableConcept, the terminology block uses thesystemandcodeproperties directly from the FSH object. For activities usingdynamicValue.expression.expressionwithpath = "code.coding", the code and system are extracted from the CQL code expression. - Deduplication and Suffixing: Terminology blocks are unique by identifier and body. If a duplicate identifier is encountered with a different body, a numeric suffix (e.g.,
_2) is added to the identifier. If both identifier and body are the same, the block is not duplicated. - Extraction Logic: For
medicationCodeableConcept, the transformer usessystem,code, andidentifierfrom the FSH object. FordynamicValue.expression.expression(wherepath = "code.coding"), the transformer usessystemandcodefrom the CQL code expression string andidentifierfrom the corresponding description.
For more details, see the User Guide and the technical mapping documentation.
CLI Tool Usage
You can run specific CLI modules using the following command:
npm run cli:<module>For example, to run the lexer CLI on a CRL file:
npm run cli:lexerReplace <module> with the desired CLI module name (e.g., lexer, parser, etc.).
FHIR Definition Emit (v2.4.0)
CRL emits CPG-IG-conformant FHIR Definition resources (ValueSet, Library, ActivityDefinition, PlanDefinition) alongside the existing CQL emit.
crl-emit --path <root.crl> --out-dir <project-root> --target fhir-defOutput lands at <project-root>/fhir/<ResourceType>/<id>.json. CQL emit (the existing --target cql / default behavior) lands at <project-root>/cql/<library-name>.cql. Library content references the CQL file via the relative path ../../cql/<library-name>.cql.
For semantic rules, layout details, deliberate spec deviations, and the MCP emit_crl_fhir tool, see USER_GUIDE.md §"Emitting FHIR Definition resources".
API Usage & Reference
All API functions return a ParseResult object. If there are any lexical or syntax errors, these are collected and returned in the errors array (not just printed to the console). You should always check result.success and handle errors accordingly.
The package provides four main functions for processing CRL code:
1. Tokenization
import { tokenizeCRL } from '@smile-digital-health/crl';
const result = tokenizeCRL(`# Example
decision "Test":
- when "Condition" then recommend activity "Action".
`);
if (result.success) {
// Access the tokens
console.log(result.result);
} else {
// Handle errors
console.error(result.errors);
}2. Parsing
import { parseCRL } from '@smile-digital-health/crl';
const result = parseCRL(`# Example
decision "Test":
- when "Condition" then recommend activity "Action".
`);
if (result.success) {
// Access the parse tree
console.log(result.result);
} else {
// Handle errors
console.error(result.errors);
}3. AST Building
import { buildCRL } from '@smile-digital-health/crl';
const result = buildCRL(`# Example
decision "Test":
- when "Condition" then recommend activity "Action".
`);
if (result.success) {
// Access the AST
console.log(result.result);
} else {
// Handle errors
console.error(result.errors);
}4. Validation
⚠️ Note: Validation functionality is not yet implemented. The
validateCRLfunction is a placeholder and will likely throw or return an error if used.
import { validateCRL } from '@smile-digital-health/crl';
const result = validateCRL(`# Example
decision "Test":
- when "Condition" then recommend activity "Action".
`);
if (result.success) {
// Access the validated AST
console.log(result.result);
} else {
// Handle validation errors
console.error(result.errors);
}Core Functions & Types
interface Token {
line: number;
column: number;
type: string;
text: string;
}
interface ParseResult<T> {
success: boolean;
result?: T;
errors?: string[];
}Examples
Example AST Comparison
To compare the generated AST with the expected AST, you can run:
npx ts-node --log-error src/examples/compare-ast.tsThis will:
- Parse the example CRL file (
docs/Measles Immunization Decision.crl) - Generate an AST from the parsed input
- Compare it with the expected AST (
docs/Expected AST.ast) - Display any differences between the two ASTs
The comparison includes:
- Line count matching
- Whitespace-normalized matching
- Structure matching
- Detailed line-by-line comparison of differences
FSH-to-CRL Example Data
The example FSH files and CRL outputs in src/examples/fsh/who/smart-example-immz/ and src/examples/crl/who/smart-example-immz/ are derived from the WHO SMART Guidelines - Example IG for Measles Immunization.
- Source repository: WorldHealthOrganization/smart-example-immz
- License: CC BY-IGO 3.0
These examples are used for development, testing, and demonstration of the transformer.
Updating Grammar & Generated Files
Updating grammar files
npm run generateThis will extract all grammar-driven types and regenerate the lexer and parser.
Grammar-Driven Types
- The lists of valid activity types, concept types, and concept value types are defined in the
validTypesarrays in theACTIVITY_TYPE,CONCEPT_TYPE, andCONCEPT_VALUE_TYPErules ofsrc/grammar/CRLLexer.g4. - Scripts (
scripts/extractActivityTypes.js,scripts/extractConceptTypes.js,scripts/extractConceptValueTypes.js) automatically extract these lists and write them tosrc/grammar/activityTypes.json,src/grammar/conceptTypes.json, andsrc/grammar/conceptValueTypes.json. - TypeScript modules (
src/grammar/activityTypes.ts,src/grammar/conceptTypes.ts,src/grammar/conceptValueTypes.ts) import these JSON files and export both the arrays and type-safe union types. - All code (lexer, AST, error listener, etc.) should import from these modules to avoid drift.
ANTLR-Generated Files
The following files are generated by ANTLR and should not be edited manually or tracked in git:
src/grammar/generated/antlr/CRLLexer.tssrc/grammar/generated/antlr/CRLParser.tssrc/grammar/generated/antlr/CRLParserVisitor.tssrc/grammar/generated/antlr/CRLParserListener.tssrc/grammar/generated/antlr/CRLLexer.tokenssrc/grammar/generated/antlr/CRLLexer.interpsrc/grammar/generated/antlr/CRLParser.tokenssrc/grammar/generated/antlr/CRLParser.interp
To regenerate these files, run:
npm run generateThese files are ignored by git via .gitignore and will be recreated as needed.
Development
Building
npm run buildTest Documentation
The project includes a comprehensive test suite to ensure the correctness of the lexer and parser. Here's how to run and work with the tests:
Running Tests
# Run all tests
npm test
# Run tests with coverage
npm run test:coverage
# Run specific test files (vitest positional filter — substring match on the test path)
npx vitest run --root ../.. --project crl src/ast/tests/decision-structure.test.ts
# Run tests in watch mode (useful during development)
npm run test:watchTest Categories
The test suite is organized into several categories:
Basic Tokens (
basic-tokens.test.ts)- Tests for fundamental language tokens
- Keywords, punctuation, and basic syntax
FHIR Types (
fhir-types.test.ts)- Tests for FHIR-specific activity types
- Validation of CPG-prefixed activity types
- Error handling for invalid types
Grammar Example (
grammar-example.test.ts)- Tests the lexer against the complete grammar example
- Validates real-world usage scenarios
Error Handling (
error-handling.test.ts)- Tests for invalid input handling
- Error message validation
- Recovery from syntax errors
Comments (
comments.test.ts)- Tests for comment handling
- Single-line and block comments
- Comment placement and nesting
Writing Tests
When adding new tests:
- Place test files in the appropriate directory, for example
src/lexer/tests/ - Follow the existing test patterns
- Include both positive and negative test cases
- Use descriptive test names
- Add comments explaining complex test scenarios
Example test structure:
describe('Feature Name', () => {
it('should handle valid input', () => {
// Test valid cases
});
it('should handle invalid input', () => {
// Test error cases
});
});Test Utilities
The test suite includes several utility functions:
getAllTokens: Retrieves all tokens from a lexerverifyTokenSequence: Validates token sequencesgetActionTokenSequence: Helper for activity type testsgetCaseFeatureTokenSequence: Helper for concept type testsgetValueTypeTokenSequence: Helper for value type tests
Linting
The project uses ESLint for code quality checks. To run the linter:
# Lint all TypeScript files
npx eslint . --ext .ts
# Lint and automatically fix issues where possible
npx eslint . --ext .ts --fixRunning Examples
The project includes several example files that demonstrate different features and validation scenarios:
# Run the basic usage example
npm run exampleRegenerating ANTLR Files
If you modify the grammar in src/grammar/CRL.g4, you'll need to regenerate the lexer and parser:
cd src/grammar
antlr4ts -Xforce-atn -o src/grammar/generated src/grammar/CRLLexer.g4 && antlr4ts -Xforce-atn -o src/grammar/generated src/grammar/CRLParser.g4Note: This project uses a custom AST implementation that uses ANTLR's visitor pattern. The generated files are used only for lexing and parsing directly, while semantic analysis and interpretation are handled by our custom AST implementation.
📦 Release Process & Checklist
Release Process:
The authoritative release checklist and instructions are maintained in a single location:
.github/PULL_REQUEST_TEMPLATE/release.md
Always follow the steps in this file when preparing a new release. This ensures consistency and reduces maintenance overhead. If you need to update the release process, update the PR template only.
Developer Notes & Error Handling
Developer Notes: Release Checklist & GitHub Automation
- Pull Request Templates: The project uses GitHub's PR template system to standardize release PRs and ensure all required steps are followed.
- Release Automation: The release process is automated via scripts and GitHub Actions. Always ensure your working directory is clean and up-to-date before running release scripts.
- CI/CD: All tests and builds are run in GitHub Actions before a release is published. If any step fails, the release will be blocked.
- Generated Files: Auto-generated files (e.g., ANTLR outputs, grammar-driven types) are ignored by git and should never be edited manually. Always use the provided scripts to regenerate them.
- Documentation: Keep all documentation referencing the release checklist up-to-date by linking to the PR template.
For more details, see the release PR template and the automation scripts in .github/scripts/.
Error Handling Strategy
This project implements robust, layered error handling across all stages of the CRL pipeline:
| Layer | Responsibility | Error Type | How Errors Are Reported | |-----------|-------------------------------------------------|--------------------|----------------------------------------| | Lexer | Tokenization, valid characters/tokens | LexicalError | Collected by error listener, JSON | | Parser | Syntax, structure, required/optional elements | ParserError | Collected by error listener, JSON | | AST | Parse tree → AST, structural integrity | AstError | Collected by AST builder, JSON | | Validator | Domain/semantic rules, cross-references, logic | ValidationError | Collected by validator, plain text |
- Lexical and parser errors are collected by their respective error listeners and reported as JSON strings with a
typeproperty (e.g.,"LexicalError","ParserError"). - AST errors are reported as
"AstError"if the AST builder encounters a structural problem not caught by the parser. - Validation errors are reported by the validator after AST construction.
Example Error Object
{
"type": "ParserError",
"line": 1,
"column": 51,
"message": "Syntax error: missing 'done' at '<EOF>'",
"details": {
"offendingSymbol": { "text": "<EOF>", ... }
}
}Test Output and Console Errors
These tests run on vitest (migrated from jest). There is intentionally no global console.error suppression: a global mute made real failure diagnostics dead code, and the muted lines only fire on failure paths anyway (so removing it added no noise on green runs). A suite that intentionally exercises an error path mutes console.error locally instead — e.g. vi.spyOn(console, "error").mockImplementation(() => {}) scoped to that test, restored after.
Editor Support: VS Code Extension
We provide a Visual Studio Code extension for CRL files:
- Syntax highlighting for
.crlfiles - Comment support
- Language basics for a better editing experience
For installation instructions and full details, see crl-vscode/README.md
License
This project is licensed under the Apache License 2.0. See the LICENSE file for details.
