@smile-digital-health/crl
v5.4.2
Published
Clinical Reasoning Language (CRL) parser and validator
Downloads
1,871
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.
CRL 5.3.0
This release adds the Medical Validation Question–Result view, route question cards,
a separate Result Questionnaire, and MV-scoped wording proposals for the owning KE.
CRL/CEL syntax and CQL/FHIR emission semantics are unchanged from 5.2.0. Core APIs
add selected answer values, presentation field ownership, and execution-model access
without source correspondence. Policy storage can be discovered from src/crl plus
src/cel without provenance; source highlighting still requires correspondence.
Authoring kit 2.2 documents MV case intent and the wording-patch handoff. Folder-based
MV/regression emission filtering is not implemented. See the release notes
and MV guide.
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 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.
For the authoring reference over MCP, call authoring_kit({}) to discover guidance,
or authoring_kit({view:"full",format:"markdown"}) for the complete audited Markdown.
The response is the same document as the release's authoring-kit.md; the agent can
read or save it. JSON remains the default. See the kit guide.
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
The package is publicly available from npm; installation does not require an npm account or token.
npm install @smile-digital-health/[email protected]Pin the version so generated-artifact migrations are adopted deliberately:
"dependencies": {
"@smile-digital-health/crl": "5.3.0"
}CRL 5.2 renames generated retrieval libraries from LocalPrimitives/ExternalPrimitives to LocalElements/ExternalElements, including CQL includes and FHIR Library identities. Regenerate CQL and FHIR together and replace the previous generated-file inventory, preserving independently authored files. Re-emission does not automatically delete obsolete artifacts. See the 5.2.0 migration notes. A ^5 or ^5.1.0 dependency can acquire this update automatically; use an exact version pin when upgrades require coordinated artifact replacement.
CRL/CEL syntax is unchanged by 5.2. When upgrading from 5.0.0, also update the authoring-kit npm/MCP calls described in the 5.1.0 release notes. Content from before the CRL5 language transition may need language migration using the current authoring kit.
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
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>/src/fhir/<ResourceType>/<id>.json. CQL emit (the existing --target cql / default behavior) lands at <project-root>/src/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 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
validateCRL performs single-file semantic validation. Inspect success, errors and warnings; use the path-based CLI or MCP validator when imports need project context.
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.
