Canonical, machine-readable conventions for automated agents working in
fhir-codegen. This file is the single source of truth that the
.github/skills/dev-* skills read before naming any build, test, or lint
command.
Precedence. This file is authoritative for commands, conventions, and
invariants an agent must follow. README.md and
docs/ are authoritative for rationale, configuration reference,
and operational detail, and are the place to look for the "why". If this
file contradicts the repository itself, the repository wins — fix this file.
fhir-codegen ingests FHIR specification packages (NPM-style hl7.fhir.*
packages) and exports them into other languages and formats — TypeScript,
C#/Firely, OpenAPI, Ruby, SQLite, FHIR Shorthand, CQL, Info, and a
cross-version mapping pipeline.
The framing fact: this is a code generator whose output is consumed by other projects. Output shape is the product. A change that alters generated artifacts is a breaking change for downstream consumers even when the C# compiles cleanly, so generation tests and their expected output carry more weight here than they would in an ordinary library.
The libraries also ship as NuGet packages (see fhir-codegen.props), so
public API changes in Fhir.CodeGen.* are breaking changes.
| Path | Contents |
|---|---|
src/fhir-codegen/ |
System.CommandLine-based CLI (OutputType=Exe). |
src/fhir-codegen-shared/ |
Shared Project (.projitems) imported by the CLI. |
src/Fhir.CodeGen.Common/ |
Lightweight POCOs, shared models, polyfills. Dependency-light by design. |
src/Fhir.CodeGen.Packages/ |
FHIR package cache management (download, resolve, registry lookup). |
src/Fhir.CodeGen.CrossVersionLoader/ |
Load and reconcile artifacts across R2/R3/R4/R4B/R5. |
src/Fhir.CodeGen.MappingLanguage/ |
FML (FHIR Mapping Language) parser/abstractions. |
src/Fhir.CodeGen.LangSQLite/, src/Fhir.CodeGen.SQLiteGenerator/ |
SQLite export backend. |
src/Fhir.CodeGen.Lib/ |
Core engine: loader → normalized model → language exporters. |
src/Fhir.CodeGen.Lib/Language/ |
The main extension point — one ILanguage implementation per output format. |
src/Fhir.CodeGen.Comparison/ |
Package/artifact diffing. |
src/Fhir.CodeGen.CrossVersionExporter/ |
Produces cross-version artifacts. |
src/performance-test-cli/ |
Standalone perf tooling. |
src/*.Tests/ |
xUnit test projects, colocated with their target. |
src/Fhir.CodeGen.Lib.Tests/TestData/ |
Test fixtures, copied to output via PreserveNewest. |
docs/articles/, docs/specs/ |
Narrative docs and per-step pipeline specs. |
docfx/ |
Documentation site generation. |
languageInput/ |
Hand-maintained input assets for certain exporters. |
Ignored paths (see .gitignore): /scratch, /generated, /temp,
/firely, /cytoscape, /fhirVersions contents, *.sqlite. Nothing under
/scratch is ever committed.
- .NET 9 targeting pack is required. There is no
global.json, so the SDK version is a floor, not an exact pin — any SDK that can targetnet9.0works. CI pinsDOTNET_VERSION: '9'(.github/workflows/build-and-test.yml); a .NET 10 SDK buildingnet9.0is also known-good locally. - Every project targets
net9.0exceptFhir.CodeGen.SQLiteGenerator, which targetsnetstandard2.0. Do not "fix" that one tonet9.0. fhir-codegen.props(imported by the project files) setsLangVersion 14.0,Nullable enable,ImplicitUsings enablesolution-wide. Change these there, not per-project.- Versions are declared per-project in each
.csproj. There is no central package management and no lock file, so a dependency bump must be applied consistently across every project that references the package (theHl7.Fhir.*family is currently5.13.3everywhere). - Warnings are not errors. No project sets
TreatWarningsAsErrors,EnforceCodeStyleInBuild,AnalysisLevel, orAnalysisMode. - Tests need the FHIR package cache populated — see "Test" below.
dotnet build fhir-codegen.sln -c ReleaseScoped to a single project:
dotnet build src/Fhir.CodeGen.Lib/Fhir.CodeGen.Lib.csproj -c ReleaseThe expected baseline is 0 errors, 1 warning. The one warning is pre-existing and unrelated to any current work:
src/Fhir.CodeGen.Lib/SqlOnFhir/ViewDefinition.cs(159,40): warning CS3021:
'ViewDefinition.ConstantComponent.Value' does not need a CLSCompliant
attribute because the assembly does not have a CLSCompliant attribute
Anything beyond that should be investigated before it is attributed —
confirm against a clean checkout or HEAD before calling it a regression.
There is a single build track. fhir-codegen and performance-test-cli
are Exe; everything else is a library. No AOT, native, or publish-only
check exists.
xUnit 2.9.3 with Shouldly 4.3.0 for assertions — not
FluentAssertions. The runner is VSTest
(Microsoft.NET.Test.Sdk 17.14.1 + xunit.runner.visualstudio 3.1.5);
there is no global.json "runner" entry and no OutputType=Exe test
project, so Microsoft.Testing.Platform is not in use.
That means dotnet test --filter <expression> is the valid filter
syntax, supporting FullyQualifiedName~Substring,
FullyQualifiedName=Exact, and trait expressions such as
RequiresExternalRepo!=true. Do not use MTP's -class / -method flags.
dotnet test --configuration Release --framework net9.0 --filter "RequiresExternalRepo!=true"This is exactly what CI runs. Always keep the
RequiresExternalRepo!=true filter: tests carrying
[Trait("RequiresExternalRepo", "true")] clone the HL7 cross-version IG
repositories and are skipped in CI.
dotnet test src/Fhir.CodeGen.Lib.Tests/Fhir.CodeGen.Lib.Tests.csproj --filter "RequiresExternalRepo!=true"Test projects: Fhir.CodeGen.Lib.Tests,
Fhir.CodeGen.MappingLanguage.Tests, Fhir.CodeGen.Packages.Tests,
fhir-codegen.Tests.
dotnet test src/Fhir.CodeGen.Lib.Tests/Fhir.CodeGen.Lib.Tests.csproj --filter "FullyQualifiedName~GenerationTests"
dotnet test src/Fhir.CodeGen.Lib.Tests/Fhir.CodeGen.Lib.Tests.csproj --filter "FullyQualifiedName=Fhir.CodeGen.Lib.Tests.GenerationTests.MyTest"Prefer the smallest command that covers the change. Escalate to the full suite only when the focused run indicates you need to.
Most tests load real FHIR core packages from ~/.fhir and will fail if
the cache is empty. Populate it once, either by running fhir-codegen
against the packages, or with firely.terminal the way CI does:
dotnet tool install -g firely.terminal
fhir config inflate off
fhir config regenerate off
fhir install hl7.fhir.r2.core 1.0.2 ; fhir install hl7.fhir.r2.expansions 1.0.2
fhir install hl7.fhir.r3.core 3.0.2 ; fhir install hl7.fhir.r3.expansions 3.0.2
fhir install hl7.fhir.r4.core 4.0.1 ; fhir install hl7.fhir.r4.expansions 4.0.1
fhir install hl7.fhir.r4b.core 4.3.0 ; fhir install hl7.fhir.r4b.expansions 4.3.0
fhir install hl7.fhir.r5.core 5.0.0 ; fhir install hl7.fhir.r5.expansions 5.0.0No separate lint step; there is no linter target, no Makefile, and no
formatter script. .editorconfig is editor-enforced only —
EnforceCodeStyleInBuild is not set anywhere, so dotnet build will not
fail on a style violation.
stylecop.json is present and defines the copyright-header text and using
ordering, but StyleCop.Analyzers is not referenced by any project, so
those rules are conventions rather than build-enforced gates. Honor them
anyway; do not add the analyzer package unless asked.
dotnet run --project src/fhir-codegen/fhir-codegen.csproj -- generate TypeScript -p hl7.fhir.r4.core --output-path ./R4.tsTop-level commands: generate, compare, xver, docs. Global options
include -p/--package, --output-path, --fhir-cache, --fhir-version,
--offline, --resolve-dependencies. Run with --help, or
generate --help, to see the current set.
No environment variables are required. The package cache defaults to the
user's .fhir directory; override with --fhir-cache.
The authoritative source is the repo-root .editorconfig
(root = true), supplemented by stylecop.json. Neither is enforced by the
build — see "Lint / format".
- UTF-8, CRLF line endings, final newline required, trailing whitespace
trimmed, 4-space indent (
tab_width = 4, no tabs). - Prefer explicit types.
csharp_style_var_for_built_in_types,csharp_style_var_when_type_is_apparent, andcsharp_style_var_elsewhereare allfalse. This is the opposite of the wider .NET default — usevaronly where the type is unmistakable from the right-hand side. - Accessibility modifiers are required on non-interface members
(
dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning). - Prefer collection expressions (
[]) overnew List<T>()/new()for empty and simple initializers. usingdirectives go outside the namespace,System.*first (stylecop.jsonorderingRules).- New
.csfiles carry the copyright header:Coverage is currently partial (roughly 272 of 414 files). Add it to new files; do not open a review finding or a bulk-edit PR over the files that lack it.// <copyright file="Foo.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. // </copyright>
- Comment only what needs clarification. This codebase is deliberately light on inline commentary; do not add narration.
- Match the surrounding file. Consistency with neighbouring code beats any general preference.
These are decisions, not preferences. Violating one is a review Blocker.
- Language exporters are auto-discovered. Every exporter lives under
src/Fhir.CodeGen.Lib/Language/{Name}.cs(orLanguage/{Name}/) and implementsILanguage.LanguageManagerfinds them by reflection at static-init time — never register one manually. Adding the type is the registration. - Exporter options must stay in sync in two places. Each exporter
exposes a nested
*Options : ConfigGenerate. Every option needs both a[ConfigOption(ArgName = "--foo", Description = "…")]attribute and a matchingConfigurationOptionstatic holding aSystem.CommandLine.Option<T>. Changing one without the other silently breaks the CLI surface. SeeLanguage/TypeScript.csand theFirely/,OpenApi/,Info/,Ruby/,SQLite/,Shorthand/,Cql/directories as canonical examples. - Multi-version FHIR types coexist via
extern alias. TheHl7.Fhir.*assemblies for different FHIR versions define colliding type names, so.csprojfiles apply MSBuild aliases in anAddPackageAliasestarget:coreR3/coreR4/coreR4B/coreR5insrc/fhir-codegen/fhir-codegen.csproj, andstu3/r4/r4binsrc/Fhir.CodeGen.Lib.Tests/. In files that touch these, useextern aliasand fully qualify. Never add a bare top-levelusing Hl7.Fhir.Model;in an aliased context. DISABLE_XMLis defined inFhir.CodeGen.LibandFhir.CodeGen.LangSQLite, in both Debug and Release. Do not introduce code paths that require XML serialization without guarding them or extending the define.NETSTANDARD2_0branches are load-bearing.Fhir.CodeGen.SQLiteGeneratortargetsnetstandard2.0, andCodeGenCommonPolyfill.csis linked in fromFhir.CodeGen.Common. Preserve those branches when editing.Fhir.CodeGen.Commonstays dependency-light. Everything else depends on it; adding a heavy dependency there propagates everywhere.- Dependency direction is one-way:
Common←Packages/CrossVersionLoader/MappingLanguage/LangSQLite←Lib← CLI. Do not introduce a cycle or makeCommondepend upward. - Tests do not write generated artifacts to disk.
GenerationTests.WriteGeneratedFilesisfalse(src/Fhir.CodeGen.Lib.Tests/GenerationTests.cs:29). Toggle it locally when debugging; never commit it astrue. - For new SQLite interaction code the stated preference is the
cslightdbgen.sqlitegenNuGet package over hand-rolled ADO. No project references it today — the existing SQLite backend usesMicrosoft.Data.Sqlite— so introducing it adds a dependency and needs the user's go-ahead.
- Conventional commits:
<type>(<scope>): <subject>. Types in active use:feat,fix,docs,chore,refactor,test. Subject in the imperative, target ≤ 72 characters. Scope is optional but strongly encouraged and widely used here — e.g.docs(xver):,fix(cli):,fix(tests):,chore(deps):. - Required trailer, verbatim, when an agent contributed to the change:
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> - One logical change per commit.
- Run the test suite with the
RequiresExternalRepo!=truefilter before pushing. - When the GitHub integration below is on and the slot carries an
Issuebinding,dev-doadds anIssue: #Ntrailer to each phase commit, in addition to every trailer required above. When the integration is off or the slot is unbound, nothing is added. - Agents do not push and do not open pull requests unless the user explicitly asks.
Off by default, in two independent ways. A repository whose
AGENTS.md has no ## GitHub Integration section is off. A section
whose Enabled row says no is equally off. In either case no skill
prompts about GitHub, and the dev-* loop behaves exactly as it did
before this feature existed.
The block below is machine-managed. This section is the normative definition of both sentinel strings: every skill that reads or writes the block reproduces the opener and the closer byte-for-byte from here, and no skill re-derives, paraphrases, or reformats them.
| Setting | Value |
|---|---|
| Enabled | no |
| Repository | n/a |
| Label — feature request | n/a |
| Label — bug report | n/a |
| Label — docs-only (additive) | n/a |
| Changelog file | n/a |
| Changelog entry format | n/a |
| PR opens as draft | n/a |
These sentinels are not dev-setup's ignore-file sentinels. The
ignore-file block that dev-setup maintains in .gitignore or
.git/info/exclude is delimited by
# >>> dev-* skills (managed by dev-setup) >>> and
# <<< dev-* skills (managed by dev-setup) <<<. That is a different
block in a different file, with a # comment prefix rather than an
HTML comment. Do not conflate the two, and never substitute one pair for
the other.
Rules for the block:
- Only
dev-setup,dev-issue, anddev-pr-openmay rewrite it, and only in place — never a second copy, never appended to the end of the file. - Hand-written text outside the sentinels is never touched. Everything a human writes in this section survives every rewrite.
- A recorded value of
no,none, orn/ais a resolved answer, not a missing one. It must never re-trigger a prompt on a later run. - When
Enabledisno, every other row isn/a.
Local inner-loop work is organized into slots under scratch/:
scratch/<MMDD>-<##>/
featurerequest.md # authored by the dev-request skill
bugreport.md # authored by the dev-report skill
approach-a.md # authored by dev-approach (minimum change)
approach-b.md # authored by dev-approach (cleanest architecture)
approach-c.md # authored by dev-approach (unconstrained)
approach.md # authored by dev-approach (the judge's selection)
plan.md # authored by dev-plan, updated by dev-do
analysis.md # authored by dev-review
<MMDD>is the local date (zero-padded month + day);<##>is a zero-padded two-digit slot number.scratch/is ignored —/scratchin.gitignore. Nothing in it is ever committed. Do notgit add -fscratch contents, do not relocate scratch artifacts into tracked paths, and do not remove/scratchfrom.gitignore.- Because the slot is ignored, no plan phase may declare a
scratch/path as an owned path.plan.mdis a control file thatdev-doedits continuously and never stages or commits.
- Read this file before proposing any build, test, or lint command. Never invent a command. If something you need is not documented here, say so rather than guessing.
- Subagents must use the same model configuration as the spawning agent.
- Do not add new linting, building, or testing tooling without being asked.
In particular, do not add StyleCop.Analyzers or set
EnforceCodeStyleInBuildon your own initiative. - Prefer the smallest targeted verification that covers the change; escalate to the full suite only when the targeted run indicates it is needed.
--filter "RequiresExternalRepo!=true"is not optional on a full test run. Dropping it makes the suite try to clone external HL7 repositories.- Treat generated output as the contract. When a change moves generated artifacts, say so explicitly and show a sample diff of the output, not just the C# diff.
.github/copilot-instructions.mdcovers much of the same ground for the GitHub Copilot coding agent. If the two disagree, they are both wrong — reconcile them rather than picking one.- The
docs/specs/tree documents the cross-version (xver) pipeline step by step. Consult it before changing anything underFhir.CodeGen.CrossVersionLoaderorFhir.CodeGen.CrossVersionExporter.