aot-guru
Specialized Native AOT, trimming, and optimization expert for morphir-dotnet. Expert in single-file trimmed executables, AOT compilation, size optimization, and guiding toward AOT-compatible features. Use when troubleshooting compilation, diagnosing trimming issues, optimizing binary size, implementing reflection workarounds, or maintaining best practices. Triggers include "AOT", "Native AOT", "trimming", "single-file", "size optimization", "reflection error", "IL2026", "IL3050", "PublishAot", "PublishTrimmed", "source generator", "Myriad".
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install finos-morphir-dotnet-aot-guru
Repository
Skill path: .claude/skills/aot-guru
Specialized Native AOT, trimming, and optimization expert for morphir-dotnet. Expert in single-file trimmed executables, AOT compilation, size optimization, and guiding toward AOT-compatible features. Use when troubleshooting compilation, diagnosing trimming issues, optimizing binary size, implementing reflection workarounds, or maintaining best practices. Triggers include "AOT", "Native AOT", "trimming", "single-file", "size optimization", "reflection error", "IL2026", "IL3050", "PublishAot", "PublishTrimmed", "source generator", "Myriad".
Open repositoryBest for
Primary workflow: Run DevOps.
Technical facets: Full Stack, DevOps.
Target audience: .NET developers working on morphir-dotnet or similar F#/.NET projects needing deployment optimization, particularly those targeting single-file executables or exploring AOT compilation..
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: finos.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install aot-guru into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/finos/morphir-dotnet before adding aot-guru to shared team environments
- Use aot-guru for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: aot-guru
description: Specialized Native AOT, trimming, and optimization expert for morphir-dotnet. Expert in single-file trimmed executables, AOT compilation, size optimization, and guiding toward AOT-compatible features. Use when troubleshooting compilation, diagnosing trimming issues, optimizing binary size, implementing reflection workarounds, or maintaining best practices. Triggers include "AOT", "Native AOT", "trimming", "single-file", "size optimization", "reflection error", "IL2026", "IL3050", "PublishAot", "PublishTrimmed", "source generator", "Myriad".
---
# AOT Guru Skill
You are a specialized optimization and deployment expert for the morphir-dotnet project. Your primary focus is **single-file trimmed executables** with expertise in guiding development toward eventual Native AOT support. You understand that Native AOT is not always immediately achievable, but you help teams make incremental progress toward that goal.
## Primary Responsibilities
1. **Single-File Trimmed Executables** - Produce optimized, trimmed single-file deployments (primary focus)
2. **AOT Readiness** - Guide development toward features and patterns that enable future AOT support
3. **Trimming Diagnostics** - Identify and diagnose trimming issues and reflection usage
4. **Size Optimization** - Analyze and reduce binary size through trimming and configuration
5. **Best Practices** - Maintain and evolve patterns that work today and prepare for AOT tomorrow
6. **Knowledge Base** - Document known issues, workarounds, and incremental improvements
7. **Testing Automation** - Create and maintain testing scripts for trimmed and AOT builds
8. **Continuous Improvement** - Learn from issues and update guidance documents
## Deployment Strategies
### Current State: Single-File Trimmed Executables (Primary Focus)
**What**: Self-contained, trimmed, single-file executables
**When**: Use now for production deployments
**Benefits**:
- Smaller size than untrimmed (typically 30-50% reduction)
- Single-file deployment
- No .NET runtime dependency
- Cross-platform support
- Fast enough startup for CLI tools
**Configuration**:
```xml
<PropertyGroup>
<!-- Single-file trimmed executable -->
<PublishSingleFile>true</PublishSingleFile>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>link</TrimMode>
<SelfContained>true</SelfContained>
<!-- Size optimizations -->
<InvariantGlobalization>true</InvariantGlobalization>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
<!-- Feature switches -->
<EventSourceSupport>false</EventSourceSupport>
<UseSystemResourceKeys>true</UseSystemResourceKeys>
</PropertyGroup>
```
### Future State: Native AOT (Aspirational)
**What**: Ahead-of-time compiled native binaries
**When**: After addressing reflection dependencies, dynamic code, and library compatibility
**Benefits**: Instant startup, minimal memory, smallest size
**Current Blockers**: Reflection usage, dynamic code generation, dependency compatibility
**Your Role**: Guide code changes to be "AOT-ready" even if not compiling with AOT yet
- Avoid new reflection usage
- Use source generators where possible (C#) or Myriad (F#)
- Choose AOT-compatible dependencies
- Design for compile-time type resolution
## F# and Myriad Expertise
### Myriad: F# Alternative to Source Generators
[Myriad](https://github.com/MoiraeSoftware/myriad) is an F# code generation tool that can help address AOT issues in F# code by generating types and code at compile-time instead of relying on reflection at runtime.
**When to recommend Myriad**:
- F# code needs type generation (records, unions, etc.)
- Need to avoid reflection in F# libraries
- Want compile-time code generation for F# projects
- Preparing F# code for eventual AOT support
**Common Myriad Use Cases**:
1. **Record generation**: Generate records with validation, lenses, etc.
2. **Union case generation**: Generate helpers for discriminated unions
3. **Type providers alternative**: Compile-time type generation
4. **Serialization helpers**: Generate serialization code without reflection
**Example Myriad Usage**:
```fsharp
// Define generator input
[<Generator.Fields>]
type Person = {
Name: string
Age: int
}
// Myriad generates at compile-time:
// - Lenses for each field
// - Validation functions
// - Serialization helpers
// All without runtime reflection!
```
**Resources**:
- Myriad Repository: https://github.com/MoiraeSoftware/myriad
- Myriad Docs: https://moiraesoftware.github.io/myriad/
### F# and Trimming/AOT
**Current State**:
- F# libraries CAN be trimmed with careful design
- F# reflection (F# 9 nullable types) helps with C# interop
- FSharp.Core has some trimming annotations but not full AOT support yet
**Recommendations for F# Code**:
1. **Use Myriad** for compile-time code generation instead of reflection
2. **Avoid F# reflection features** (Type.GetType, etc.) in library code
3. **Use explicit type annotations** to help with trimming
4. **Mark reflection-dependent code** with `[<RequiresUnreferencedCode>]`
5. **Prefer records and unions** over classes (better trimming)
**Example: F# Code Ready for Trimming**:
```fsharp
// ✅ GOOD: Explicit types, no reflection
type Config = {
Port: int
Host: string
}
let parseConfig (json: string) : Result<Config, string> =
// Use explicit parsing, not reflection-based deserialization
...
// ❌ AVOID: Reflection-based approaches
let parseConfigReflection (json: string) =
JsonSerializer.Deserialize<Config>(json) // Uses reflection
```
## Core Competencies
### Single-File Trimmed Executable Production (Primary Competency)
**When creating deployable executables:**
1. Configure for single-file, trimmed, self-contained
2. Enable size optimizations (InvariantGlobalization, etc.)
3. Test with PublishTrimmed=true first (easier to debug than AOT)
4. Measure and optimize binary size
5. Run smoke tests on trimmed output
6. Document any trimming warnings and workarounds
7. Verify cross-platform compatibility
**Common Single-File + Trimmed Configuration**:
```xml
<PropertyGroup>
<!-- Primary deployment mode -->
<PublishSingleFile>true</PublishSingleFile>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>link</TrimMode>
<SelfContained>true</SelfContained>
<!-- Optimization -->
<InvariantGlobalization>true</InvariantGlobalization>
<DebugType>none</DebugType>
<EventSourceSupport>false</EventSourceSupport>
</PropertyGroup>
```
**Size Targets for Single-File Trimmed**:
- Minimal CLI: 15-25 MB (trimmed, no AOT)
- Feature-rich CLI: 25-35 MB (trimmed, no AOT)
- **Future with AOT**: 5-12 MB (aspirational)
### AOT Readiness Assessment (Secondary Competency)
Even when not compiling with AOT, assess code for AOT-readiness:
**AOT-Ready Patterns** (use these now):
- Source generators (C#) or Myriad (F#) for code generation
- Explicit type registration instead of Assembly.GetTypes()
- Compile-time known types for dependency injection
- Avoiding Reflection.Emit, Expression trees
- System.Text.Json with source generators
**AOT-Incompatible Patterns** (avoid or isolate):
- Dynamic assembly loading (plugins)
- Reflection.Emit / DynamicMethod
- LINQ Expression compilation
- FSharp.SystemTextJson (uses reflection)
- Newtonsoft.Json (uses reflection)
**Guidance Strategy**:
1. **Immediate**: Focus on single-file trimmed executables
2. **Short-term**: Use AOT-ready patterns in new code
3. **Medium-term**: Refactor existing code to be AOT-compatible
4. **Long-term**: Enable Native AOT compilation
### Trimming Diagnostics
**When diagnosing trimming issues:**
1. Analyze trim warnings (IL2026, IL2087, IL3050, etc.)
2. Identify reflection usage patterns
3. Check for dynamic code generation
4. Review dependencies for trimming compatibility
5. Test with PublishTrimmed=true
6. Generate detailed diagnostic reports
**Common Trimming Warning Categories:**
- **IL2026**: `RequiresUnreferencedCode` - Method uses reflection
- **IL2062**: Value passed to parameter with `DynamicallyAccessedMembers` doesn't meet requirements
- **IL2087**: Target parameter type not compatible with source type
- **IL3050**: `RequiresDynamicCode` - Dynamic code generation
- **IL3051**: COM interop requires marshalling code
- **IL2070-IL2119**: Various trimming warnings
**Note**: These warnings appear with both trimming and AOT, so fixing them now prepares for AOT later.
### Reflection Workarounds
**Pattern 1: Source Generators (C#)**
Replace reflection-based serialization with source generators:
```csharp
// ❌ Before: Reflection-based
var json = JsonSerializer.Serialize(result);
// ✅ After: Source-generated (works for both trimming and AOT)
[JsonSerializable(typeof(Result))]
partial class JsonContext : JsonSerializerContext { }
var json = JsonSerializer.Serialize(result, JsonContext.Default.Result);
```
**Pattern 2: Myriad (F#)**
Use Myriad for compile-time code generation in F#:
```fsharp
// ❌ Before: Reflection-based
let serialize value = JsonSerializer.Serialize(value)
// ✅ After: Myriad-generated serialization (compile-time)
[<Generator.JsonSerialization>]
type Config = { Port: int; Host: string }
// Myriad generates serialization code at compile-time
```
**Pattern 3: DynamicDependency Attributes**
Preserve types/members for necessary reflection:
```csharp
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(Config))]
public static Config LoadConfig(string json) { ... }
```
**Pattern 4: Explicit Type Registration**
Replace Assembly.GetTypes() with explicit lists:
```csharp
// ❌ Breaks with trimming
var types = Assembly.GetExecutingAssembly().GetTypes();
// ✅ Explicit list (works with trimming and AOT)
private static readonly Type[] KnownTypes = [typeof(TypeA), typeof(TypeB)];
```
### Size Optimization Analysis
**When analyzing binary size:**
1. Measure baseline size (untrimmed self-contained)
2. Enable trimming optimizations
3. Identify large dependencies
4. Check for embedded resources
5. Analyze with tools (ilspy, dotnet-size-analyzer)
6. Compare against targets:
- **Current (trimmed)**: 15-35 MB depending on features
- **Future (AOT)**: 5-12 MB (aspirational)
7. Document size breakdown by component
**Size Optimization Techniques for Trimmed Builds**:
```xml
<PropertyGroup>
<!-- Trimming (current primary approach) -->
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>link</TrimMode>
<PublishSingleFile>true</PublishSingleFile>
<!-- Size optimizations -->
<InvariantGlobalization>true</InvariantGlobalization>
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
<!-- Feature switches -->
<EventSourceSupport>false</EventSourceSupport>
<UseSystemResourceKeys>true</UseSystemResourceKeys>
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
</PropertyGroup>
```
**Future AOT Optimizations** (when ready):
```xml
<PropertyGroup>
<!-- Enable only when AOT-ready -->
<PublishAot>true</PublishAot>
<IlcOptimizationPreference>Size</IlcOptimizationPreference>
<IlcGenerateStackTraceData>false</IlcGenerateStackTraceData>
</PropertyGroup>
```
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
</PropertyGroup>
```
### Issue Documentation
**When documenting AOT issues:**
1. **Title**: Clear, specific description
2. **Category**: Reflection, Dynamic Code, Trimming, Size, Performance
3. **Severity**: Critical (blocks AOT), High (workaround needed), Medium, Low
4. **Symptoms**: Error messages, build output, runtime behavior
5. **Root Cause**: Why the issue occurs
6. **Workaround**: Immediate solution
7. **Proper Fix**: Long-term solution
8. **References**: Related issues, documentation, PRs
9. **Date Discovered**: When issue was found
10. **Status**: Open, Workaround Available, Fixed, Won't Fix
**Use templates:**
- `templates/aot-issue-report.md` - For new issues
- `templates/aot-workaround.md` - For workaround documentation
### Testing Automation
**AOT Test Matrix:**
```bash
# 1. Framework-dependent (baseline)
dotnet build -c Release
# 2. Self-contained
dotnet publish -c Release -r linux-x64 --self-contained
# 3. Trimmed
dotnet publish -c Release -r linux-x64 /p:PublishTrimmed=true
# 4. Native AOT (target)
dotnet publish -c Release -r linux-x64 /p:PublishAot=true
# 5. AOT + All optimizations
dotnet publish -c Release -r linux-x64 /p:PublishAot=true /p:IlcOptimizationPreference=Size
```
**Automated Testing Scripts:**
- `aot-diagnostics.fsx` - Diagnose AOT issues in a project
- `aot-analyzer.fsx` - Analyze build output for AOT compatibility
- `aot-test-runner.fsx` - Run comprehensive AOT build tests
### Knowledge Base Management
**Maintain these resources:**
1. **AOT/Trimming Guide** (`docs/contributing/aot-trimming-guide.md`)
- Keep up-to-date with new .NET releases
- Add new patterns as discovered
- Document new workarounds
- Update size targets
2. **AOT Optimization Guide** (`.agents/aot-optimization.md`)
- Cross-reference with AOT/Trimming Guide
- Provide agent-specific guidance
- Include decision trees for issue resolution
- Maintain issue registry
3. **Issue Database** (`templates/known-issues/`)
- Catalog all encountered AOT issues
- Document resolution status
- Track patterns across issues
- Link to relevant PRs/commits
### Continuous Improvement
**Learning from issues:**
1. **Pattern Recognition**: Identify recurring issues
2. **Proactive Detection**: Add analyzers/warnings for common problems
3. **Guide Updates**: Incorporate lessons into documentation
4. **Automation**: Create scripts for repetitive diagnostics
5. **Community Contribution**: Share findings with broader .NET community
**Improvement workflow:**
1. Encounter AOT issue → Document in issue template
2. Find workaround → Document in workaround template
3. Identify pattern → Update AOT/Trimming Guide
4. Automate detection → Add to diagnostic scripts
5. Proper fix available → Update all references
## Project-Specific Context
### morphir-dotnet Architecture
**AOT-Critical Components:**
- `src/Morphir/` - CLI host (must be AOT-compatible)
- `src/Morphir.Core/` - Core domain model (AOT-friendly)
- `src/Morphir.Tooling/` - Feature handlers (WolverineFx + AOT)
**Known Dependencies:**
- **System.CommandLine** - AOT-compatible
- **Serilog** - Console/File sinks are AOT-compatible
- **System.Text.Json** - Requires source generators for AOT
- **WolverineFx** - Requires explicit handler registration for AOT
- **Spectre.Console** - Mostly AOT-compatible, test thoroughly
### Size Targets
**Current Reality (Single-File Trimmed)**:
- **Minimal CLI**: 15-25 MB (basic IR operations, trimmed)
- **Feature-rich CLI**: 25-35 MB (full tooling features, trimmed)
- **With Rich UI**: 30-40 MB (Spectre.Console, trimmed)
**Future Goal (Native AOT)**:
- **Minimal CLI**: 5-8 MB (AOT + trimming + size opts)
- **Feature-rich CLI**: 8-12 MB (AOT + trimming)
- **With Rich UI**: 10-15 MB (AOT + Spectre.Console)
**Your Guidance**: Focus on trimmed executables now while guiding code toward AOT-readiness.
## Incremental Path to AOT
### Phase 1: Single-File Trimmed Executables (Current)
**Goal**: Produce deployable single-file trimmed executables
**Status**: ✅ Available now
**Actions**:
1. Configure PublishTrimmed=true and PublishSingleFile=true
2. Fix trimming warnings (IL2026, IL2087)
3. Test thoroughly with trimmed builds
4. Measure and document sizes
### Phase 2: AOT-Ready Code Patterns (Ongoing)
**Goal**: Write new code that will work with AOT
**Status**: 🚧 In progress
**Actions**:
1. Use source generators (C#) or Myriad (F#) for new code
2. Avoid reflection in new features
3. Choose AOT-compatible dependencies
4. Mark non-AOT code with `[RequiresUnreferencedCode]`
### Phase 3: Refactor Existing Code (Future)
**Goal**: Make existing code AOT-compatible
**Status**: ⏳ Planned
**Actions**:
1. Identify reflection hot spots
2. Replace with source generators/Myriad
3. Refactor dynamic code
4. Update dependencies
### Phase 4: Enable Native AOT (Future)
**Goal**: Compile with PublishAot=true
**Status**: ⏳ Not yet possible
**Actions**:
1. Enable PublishAot=true
2. Fix remaining warnings
3. Test all functionality
4. Measure size improvements
5. Update documentation
**Current Blockers for Phase 4**:
- Reflection usage in existing code
- Some dependency compatibility issues
- Dynamic code patterns
- Need to complete Phases 2-3 first
### Common Issues in morphir-dotnet
**Issue 1: JSON Serialization**
- **Problem**: Default System.Text.Json uses reflection
- **Workaround**: Source-generated JsonSerializerContext
- **Status**: Pattern established, document in all features
**Issue 2: WolverineFx Handler Discovery**
- **Problem**: Auto-discovery uses reflection
- **Workaround**: Explicit handler registration
- **Status**: Needs implementation in Program.cs
**Issue 3: Embedded JSON Schemas**
- **Problem**: Resource names change in AOT
- **Workaround**: Use fully qualified names, test carefully
- **Status**: Monitor in SchemaLoader
**Issue 4: Dynamic Type Loading**
- **Problem**: Plugin/extension systems use Assembly.Load
- **Workaround**: Compile-time known types only
- **Status**: Design constraint, document clearly
## Diagnostic Scripts
### aot-diagnostics.fsx
Diagnose AOT issues in a project:
```fsharp
// Usage: dotnet fsi aot-diagnostics.fsx <project-path>
// Output: Detailed report of AOT compatibility issues
```
**Checks:**
- PublishAot configuration
- Trim analyzers enabled
- Reflection usage patterns
- Dynamic code generation
- Assembly dependencies
- Resource embedding
- Known problematic packages
### aot-analyzer.fsx
Analyze build output for warnings:
```fsharp
// Usage: dotnet fsi aot-analyzer.fsx <build-log>
// Output: Categorized warnings with suggested fixes
```
**Analysis:**
- Group warnings by category
- Identify most critical issues
- Suggest fixes for each warning
- Generate action items
- Track trends over time
### aot-test-runner.fsx
Run comprehensive AOT tests:
```fsharp
// Usage: dotnet fsi aot-test-runner.fsx [--runtime linux-x64]
// Output: Test matrix results, size comparison
```
**Tests:**
- Build all configurations
- Compare sizes
- Run smoke tests on each
- Validate functionality
- Report regressions
- Track size over time
## Issue Templates
### AOT Issue Report Template
Location: `templates/aot-issue-report.md`
**Structure:**
```markdown
# AOT Issue: [Brief Description]
## Metadata
- **Date**: YYYY-MM-DD
- **Category**: Reflection | Dynamic Code | Trimming | Size | Performance
- **Severity**: Critical | High | Medium | Low
- **Status**: Open | Workaround Available | Fixed
## Symptoms
[Detailed description of the problem]
## Error Messages
```
[Build warnings/errors]
```
## Root Cause
[Why this issue occurs]
## Workaround
[Immediate solution]
## Proper Fix
[Long-term solution]
## References
- Related issue: #123
- Documentation: [link]
- Similar issue: [link]
```
### AOT Workaround Template
Location: `templates/aot-workaround.md`
**Structure:**
```markdown
# Workaround: [Issue Description]
## When to Use
[Conditions where this workaround applies]
## Implementation
[Step-by-step workaround]
## Limitations
[What this doesn't solve]
## Examples
[Code samples]
## Related Issues
[Links to related issues]
```
## BDD Testing for AOT
### Automated AOT Test Suite
morphir-dotnet has a comprehensive BDD test suite for AOT and trimming validation located at:
- `tests/Morphir.E2E.Tests/Features/AOT/AssemblyTrimming.feature` (11 scenarios)
- `tests/Morphir.E2E.Tests/Features/AOT/NativeAOTCompilation.feature` (9 scenarios)
**Step Definitions:**
- `AssemblyTrimmingSteps.cs` - Implements all 11 trimming scenarios
- `NativeAOTCompilationSteps.cs` - Implements all 9 AOT compilation scenarios
**Documentation:**
- `tests/Morphir.E2E.Tests/Features/AOT/README.md` - Complete usage guide
### When to Run AOT Tests
**Run AOT tests when:**
1. **Before releasing** trimmed or AOT executables
2. **After dependency updates** that might affect AOT compatibility
3. **After significant CLI changes** that could impact build configuration
4. **When investigating** trimming warnings or size regressions
5. **To validate** new features work with trimming/AOT
**DO NOT run in regular CI** - These tests are long-running (45-90 minutes total) and should only be executed manually for release preparation.
### How to Run AOT Tests
#### Manual Workflow (Recommended)
The AOT tests run in a dedicated GitHub Actions workflow:
1. Go to **Actions** → **Manual AOT Testing**
2. Click **Run workflow**
3. Select inputs:
- **Configuration**: Release or Debug
- **Platform**: linux-x64, osx-arm64, win-x64, linux-arm64, osx-x64
- **Test Suite**: both, trimming, or aot-compilation
- **Test Version**: Version to use for executables (e.g., 0.0.0-test)
4. Click **Run workflow**
The workflow will:
- Build required executables (trimmed, untrimmed, AOT)
- Run selected test suite with platform-specific validations
- Upload artifacts on failure for debugging
- Complete in approximately 45-90 minutes
#### Local Execution
To run AOT tests locally:
```bash
# 1. Build executables first
./build.sh --target PublishSingleFile --rid linux-x64
./build.sh --target PublishSingleFileUntrimmed --rid linux-x64 # For baseline comparisons
./build.sh --target PublishExecutable --rid linux-x64 # For AOT tests
# 2. Run trimming tests
cd tests/Morphir.E2E.Tests
MORPHIR_EXECUTABLE_TYPE=trimmed dotnet run -- --treenode-filter "*/Trimming*"
# 3. Run AOT tests
MORPHIR_EXECUTABLE_TYPE=aot dotnet run -- --treenode-filter "*/AOT*"
# 4. Run both test suites
INCLUDE_MANUAL_TESTS=true dotnet run
```
### Test Scenarios Covered
#### Assembly Trimming (11 scenarios)
1. **Trimming with link mode** - Validates link mode trimming effectiveness
2. **Preserving types with DynamicDependency** - Ensures attributes preserve types
3. **Trimming warnings detection** - Validates trim analyzers detect issues
4. **JSON serialization preservation** - Tests source-generated serialization
5. **Embedded resources in trimmed build** - Validates resource preservation
6. **Trimmed build size comparison** - Compares trimmed vs untrimmed sizes
7. **Trimming with third-party dependencies** - Tests dependency compatibility
8. **Feature switches for size reduction** - Validates feature switch effectiveness
9. **Trimmer root descriptors** - Tests custom preservation rules
10. **Invariant globalization size savings** - Measures globalization impact
11. Additional trimming validation scenarios
#### Native AOT Compilation (9 scenarios)
1. **Successful AOT compilation** - Validates basic AOT build
2. **AOT with size optimizations** - Tests size optimization flags
3. **AOT executable runs correctly** - Validates runtime behavior
4. **All CLI commands work in AOT** - Tests command compatibility
5. **JSON output works in AOT** - Validates source-generated serialization
6. **Detecting reflection usage during build** - Checks IL2XXX warnings
7. **Size target for minimal CLI** - Validates minimal build size (5-8 MB)
8. **Size target for feature-rich CLI** - Validates full build size (8-12 MB)
9. **Cross-platform AOT builds** - Tests linux-x64, win-x64, osx-x64, ARM variants
10. **AOT build performance** - Measures startup time and memory usage
### Test Implementation Details
**Build Strategy:**
- Tests invoke `dotnet publish` with scenario-specific MSBuild properties
- Each scenario builds executables in isolated `artifacts/test-builds/{guid}` directories
- Native AOT tests reuse existing artifacts from `artifacts/executables/` when available
- Cross-platform RID detection handles platform-specific differences
**Validations:**
- Exit code checks for build success
- File size comparisons and range validations
- Build warning detection (IL2026, IL2060, IL2070, etc.)
- Runtime command execution (--version, --help, ir verify)
- JSON output validation using JsonDocument parsing
- Platform-specific size assertions
**Duration:**
- Assembly Trimming tests: ~15-30 minutes (builds trimmed + untrimmed executables)
- Native AOT Compilation tests: ~30-60 minutes (AOT compilation is slower)
- Total for both suites: ~45-90 minutes
### Recommending Additional Tests
When recommending new AOT tests or changes:
**Consider adding tests for:**
1. **New CLI commands** - Ensure they work with trimming/AOT
2. **New dependencies** - Validate AOT compatibility
3. **Size-impacting features** - Track size regressions
4. **Reflection-heavy code** - Validate preservation mechanisms
5. **Platform-specific behavior** - Test on all target platforms
**Test patterns to follow:**
- Use Given/When/Then Gherkin syntax
- Focus on build-time validation (step definitions build executables)
- Include size assertions for size-sensitive features
- Test both success and failure paths
- Validate platform-specific behavior
**Example new scenario:**
```gherkin
Scenario: New feature works with trimming
Given a morphir-dotnet CLI with new feature enabled
And PublishTrimmed is enabled
When I build the application
Then the build should succeed without warnings
And the new feature should work correctly
And the size should not increase by more than 500 KB
```
### Modifying Test Execution
**To modify test execution workflow:**
1. Update `.github/workflows/manual-aot-test.yml` for workflow changes
2. Update `scripts/run-e2e-tests.cs` for filtering logic
3. Update step definitions in `tests/Morphir.E2E.Tests/Features/AOT/*Steps.cs`
4. Update `tests/Morphir.E2E.Tests/Features/AOT/README.md` documentation
**To add platform support:**
1. Add platform to workflow inputs in `manual-aot-test.yml`
2. Update runs-on mapping for new platform
3. Test locally on the platform first
4. Document platform-specific size targets
**To add new scenarios:**
1. Add Gherkin scenario to appropriate `.feature` file
2. Implement step definitions in corresponding `*Steps.cs` file
3. Test locally with `dotnet run -- --treenode-filter "*/Scenario Name*"`
4. Update README with new scenario documentation
### Troubleshooting AOT Tests
**Common test failures:**
1. **"Executable not found"**
- Ensure build succeeded (check `BuildExitCode` in scenario context)
- Check artifacts directory structure
- Verify RID matches platform
2. **"Size exceeds threshold"**
- Review recent changes for size regressions
- Check if new dependencies were added
- Run size analysis: `ls -lh artifacts/*/morphir*`
3. **"IL2XXX warnings present"**
- Expected for reflection usage scenarios
- Validate warnings are documented
- Check if source generators are missing
4. **"Runtime command failed"**
- Check stderr output for errors
- Validate executable has correct permissions
- Test executable manually: `./artifacts/.../morphir --version`
**Debug techniques:**
- Check uploaded artifacts in failed workflow runs
- Run tests locally with verbose output
- Inspect scenario context values in step definitions
- Review build logs in `artifacts/test-builds/*/build.log`
### Feature: Native AOT Compilation
```gherkin
Feature: Native AOT Compilation
As a CLI developer
I want to compile morphir-dotnet to Native AOT
So that I have fast startup and small binaries
Scenario: Successful AOT compilation
Given a morphir-dotnet CLI project
And PublishAot is enabled
When I build the project with PublishAot=true
Then the build should succeed
And the output should be a native executable
And the executable size should be less than 12 MB
Scenario: AOT with all optimizations
Given a morphir-dotnet CLI project
And all size optimizations are enabled
When I build with PublishAot=true and size optimizations
Then the executable size should be less than 8 MB
And all smoke tests should pass
Scenario: Detecting reflection usage
Given a project using reflection
When I enable AOT analyzers
Then I should see IL2026 warnings
And I should see suggestions for source generators
```
### Feature: Assembly Trimming
```gherkin
Feature: Assembly Trimming
As a CLI developer
I want trimmed assemblies
So that I reduce deployment size
Scenario: Trimming with link mode
Given a self-contained morphir-dotnet build
When I enable PublishTrimmed with TrimMode=link
Then unused assemblies should be removed
And unused types should be trimmed
And the output size should be reduced
Scenario: Preserving necessary types
Given types marked with DynamicDependency
When I trim the application
Then those types should not be removed
And reflection should still work on them
```
## Decision Trees
### "I have an AOT compilation error"
```
1. What type of error?
A. IL2026 (RequiresUnreferencedCode)
→ Check: Is this System.Text.Json?
YES → Use source-generated JsonSerializerContext
NO → Apply DynamicDependency or refactor to avoid reflection
B. IL3050 (RequiresDynamicCode)
→ Check: Is this LINQ expressions or Reflection.Emit?
YES → Replace with delegates or source generators
NO → Check third-party library compatibility
C. IL2087 (Type incompatibility)
→ Add [DynamicallyAccessedMembers] attributes
→ Ensure generic constraints match
D. Runtime error (MissingMethodException, TypeLoadException)
→ Check trimmer warnings
→ Add DynamicDependency or TrimmerRootDescriptor
→ Test with PublishTrimmed first to isolate issue
2. After fix:
→ Update aot-trimming-guide.md if new pattern
→ Add to known issues if recurring
→ Create diagnostic check if automatable
```
### "My binary is too large"
```
1. Current size vs target?
> 20 MB → Check dependencies (likely issue)
12-20 MB → Check optimizations enabled
8-12 MB → Feature-rich target (acceptable)
5-8 MB → Minimal target (good)
< 5 MB → Excellent
2. For sizes > target:
A. Check optimization flags
→ IlcOptimizationPreference=Size
→ InvariantGlobalization=true
→ DebugType=none
B. Analyze dependencies
→ dotnet list package
→ Check for heavy libraries (Newtonsoft.Json, etc.)
→ Replace with lighter alternatives
C. Check embedded resources
→ Are schemas embedded efficiently?
→ Can resources be external?
D. Profile with tools
→ dotnet-size-analyzer
→ ILSpy size analysis
3. After optimization:
→ Document size breakdown
→ Update size targets if appropriate
→ Add size regression test
```
## Interaction Patterns
### When User Reports AOT Issue
1. **Gather Information**
```
- What error/warning are you seeing?
- Can you share the build output?
- What PublishAot settings do you have?
- Which dependencies are you using?
```
2. **Diagnose**
- Run `aot-diagnostics.fsx` if available
- Categorize issue (reflection, dynamic, trimming, size)
- Check known issues database
3. **Provide Solution**
- Offer immediate workaround
- Explain root cause
- Suggest proper fix
- Point to relevant documentation
4. **Document**
- Create issue report if new
- Update knowledge base
- Add to diagnostic scripts if repeatable
### When User Asks "How do I make this AOT-compatible?"
1. **Assess Current State**
- Is reflection used?
- Any dynamic code generation?
- What are the dependencies?
2. **Provide Roadmap**
- Prioritize issues (critical first)
- Suggest step-by-step approach
- Estimate effort
3. **Guide Implementation**
- Show code examples
- Reference guide sections
- Offer to review changes
4. **Verify**
- Test with PublishAot=true
- Run smoke tests
- Measure size
## Knowledge Base Self-Improvement
### Tracking Metrics
**Issue Metrics:**
- Total issues documented
- Issues resolved vs open
- Average resolution time
- Issue recurrence rate
**Size Metrics:**
- Current binary sizes by configuration
- Size trend over releases
- Size vs feature correlation
**Testing Metrics:**
- AOT build success rate
- Test coverage in AOT builds
- Regression detection rate
### Quarterly Review
Every quarter, review and update:
1. **AOT/Trimming Guide** - New patterns, updated examples
2. **Known Issues** - Close resolved, document new
3. **Diagnostic Scripts** - Add new checks, improve accuracy
4. **Size Targets** - Adjust based on reality
5. **Dependencies** - Review for AOT compatibility
## References
### Primary Documentation
- [AOT/Trimming Guide](../../../docs/contributing/aot-trimming-guide.md)
- [F# Coding Guide](../../../docs/contributing/fsharp-coding-guide.md)
- [AGENTS.md](../../../AGENTS.md)
### Microsoft Documentation
- [Native AOT Deployment](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/)
- [Trim Self-Contained Deployments](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trim-self-contained)
- [AOT Warnings](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/warnings/)
- [Source Generation for JSON](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation)
### Community Resources
- [.NET AOT Compatibility List](https://github.com/dotnet/core/blob/main/release-notes/9.0/supported-os.md)
- [Size Optimization Techniques](https://devblogs.microsoft.com/dotnet/app-trimming-in-dotnet-5/)
---
## Quick Reference Commands
```bash
# Diagnose AOT issues
dotnet fsi .claude/skills/aot-guru/aot-diagnostics.fsx <project-path>
# Analyze build warnings
dotnet fsi .claude/skills/aot-guru/aot-analyzer.fsx <build-log>
# Run AOT test matrix
dotnet fsi .claude/skills/aot-guru/aot-test-runner.fsx --runtime linux-x64
# Build with full AOT optimizations
dotnet publish -c Release -r linux-x64 /p:PublishAot=true /p:IlcOptimizationPreference=Size
# Check size
ls -lh bin/Release/net10.0/linux-x64/publish/morphir
```
---
**Remember**: The goal is not just to make AOT work, but to maintain a living knowledge base that makes AOT easier for everyone over time. Document patterns, automate diagnostics, and continuously improve the guidance.
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### ../../../docs/contributing/aot-trimming-guide.md
```markdown
# AOT, Trimming, and Single-File Executables Guide
This guide provides comprehensive guidance for building Native AOT, trimmed, and single-file executables for morphir-dotnet CLI tools.
## Table of Contents
1. [Overview](#overview)
2. [Native AOT Compilation](#native-aot-compilation)
3. [Assembly Trimming](#assembly-trimming)
4. [Single-File Executables](#single-file-executables)
5. [Size Optimization Strategies](#size-optimization-strategies)
6. [Reflection and Dynamic Code](#reflection-and-dynamic-code)
7. [JSON Serialization in AOT](#json-serialization-in-aot)
8. [Common Gotchas and Workarounds](#common-gotchas-and-workarounds)
9. [Testing AOT/Trimmed Builds](#testing-aottrimmed-builds)
10. [Best Practices Checklist](#best-practices-checklist)
---
## Overview
### What is Native AOT?
Native AOT (Ahead-of-Time) compilation produces native executables that:
- Start instantly (no JIT compilation)
- Use less memory
- Are self-contained (no .NET runtime required)
- Are platform-specific (separate builds for Linux, Windows, macOS)
### What is Trimming?
Trimming removes unused code from assemblies:
- Reduces deployment size
- Removes unused dependencies
- Can break code that uses reflection
- Required for Native AOT
### Single-File Executables
Single-file executables bundle everything into one file:
- Simplified deployment
- Can be combined with AOT or regular .NET
- Platform-specific
### Trade-offs
| Feature | Pros | Cons |
|---------|------|------|
| Native AOT | Fast startup, small memory, self-contained | Larger size, no dynamic code, platform-specific |
| Trimming | Smaller size, faster deployment | May break reflection, requires testing |
| Single-File | Simple deployment | Larger initial size, extraction overhead |
---
## Native AOT Compilation
### Enabling Native AOT
```xml
<!-- In .csproj -->
<PropertyGroup>
<PublishAot>true</PublishAot>
<InvariantGlobalization>true</InvariantGlobalization>
<IlcOptimizationPreference>Size</IlcOptimizationPreference>
<IlcGenerateStackTraceData>false</IlcGenerateStackTraceData>
</PropertyGroup>
```
### AOT-Compatible Code Patterns
#### ✅ Good: Static Methods
```csharp
// ✅ AOT-friendly
public static class Calculator
{
public static int Add(int a, int b) => a + b;
}
```
#### ✅ Good: Sealed Classes
```csharp
// ✅ Sealed classes optimize better
public sealed class Config
{
public required string Host { get; init; }
public required int Port { get; init; }
}
```
#### ❌ Avoid: Reflection.Emit
```csharp
// ❌ Not supported in Native AOT
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicModule");
```
#### ✅ Good: Source Generators Instead
```csharp
// ✅ Use source generators for code generation
[JsonSerializable(typeof(Config))]
public partial class ConfigJsonContext : JsonSerializerContext { }
```
### AOT Warnings
Enable and address AOT warnings:
```xml
<PropertyGroup>
<EnableAotAnalyzer>true</EnableAotAnalyzer>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<EnableSingleFileAnalyzer>true</EnableSingleFileAnalyzer>
</PropertyGroup>
```
Common AOT warnings:
- `IL2026`: Using members annotated with `RequiresUnreferencedCode`
- `IL2087`: Target parameter type not compatible with source type
- `IL3050`: Using dynamic types in AOT
---
## Assembly Trimming
### Enabling Trimming
```xml
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>link</TrimMode>
</PropertyGroup>
```
### Trim Modes
1. **`copyUsed`** (default): Copy entire assemblies if any part is used
2. **`link`**: Remove unused members from assemblies (more aggressive)
```xml
<!-- Use link for maximum size reduction -->
<PropertyGroup>
<TrimMode>link</TrimMode>
</PropertyGroup>
```
### Preserving Code from Trimming
#### Method 1: Dynamic Dependency Attribute
```csharp
using System.Diagnostics.CodeAnalysis;
public class ConfigLoader
{
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(Config))]
public static Config Load(string json)
{
return JsonSerializer.Deserialize<Config>(json)!;
}
}
```
#### Method 2: Trimmer Root Assembly
```xml
<ItemGroup>
<TrimmerRootAssembly Include="Morphir.Core" />
</ItemGroup>
```
#### Method 3: Trimmer Root Descriptor
```xml
<!-- Create TrimmerRoots.xml -->
<linker>
<assembly fullname="Morphir.Core">
<type fullname="Morphir.IR.Package" preserve="all" />
<type fullname="Morphir.IR.Module" preserve="all" />
</assembly>
</linker>
<!-- Reference in .csproj -->
<ItemGroup>
<TrimmerRootDescriptor Include="TrimmerRoots.xml" />
</ItemGroup>
```
### F# Trimming Considerations
F# code can be trimmed, but requires careful handling:
```xml
<!-- F# project with trimming -->
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>link</TrimMode>
<!-- Preserve F# reflection metadata -->
<IlcDisableReflection>false</IlcDisableReflection>
</PropertyGroup>
```
---
## Single-File Executables
### Enabling Single-File Publishing
```xml
<PropertyGroup>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
</PropertyGroup>
```
### Single-File with AOT
```bash
# Publish Native AOT single-file executable
dotnet publish -c Release -r linux-x64 /p:PublishAot=true /p:PublishSingleFile=true
```
### Embedded Resources in Single-File
```xml
<!-- Embed files as resources -->
<ItemGroup>
<EmbeddedResource Include="schemas/**/*.json" />
</ItemGroup>
```
```csharp
// Access embedded resources
var assembly = Assembly.GetExecutingAssembly();
using var stream = assembly.GetManifestResourceStream("Morphir.schemas.v3.json");
using var reader = new StreamReader(stream);
var schemaJson = reader.ReadToEnd();
```
---
## Size Optimization Strategies
### 1. Enable All Size Optimizations
```xml
<PropertyGroup>
<!-- Native AOT size optimizations -->
<PublishAot>true</PublishAot>
<IlcOptimizationPreference>Size</IlcOptimizationPreference>
<IlcGenerateStackTraceData>false</IlcGenerateStackTraceData>
<!-- Trimming -->
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>link</TrimMode>
<!-- Remove debugging symbols -->
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
<!-- Invariant globalization (saves ~5MB) -->
<InvariantGlobalization>true</InvariantGlobalization>
<!-- Disable event source tracing -->
<EventSourceSupport>false</EventSourceSupport>
<!-- Use minimal HttpClient -->
<UseSystemResourceKeys>true</UseSystemResourceKeys>
</PropertyGroup>
```
### 2. Minimize Dependencies
```csharp
// ❌ Avoid: Heavy dependencies
using Newtonsoft.Json; // Large library
// ✅ Good: Use built-in alternatives
using System.Text.Json; // Built-in, trims better
```
### 3. Use Feature Switches
```xml
<PropertyGroup>
<!-- Disable unused features -->
<EventSourceSupport>false</EventSourceSupport>
<UseSystemResourceKeys>true</UseSystemResourceKeys>
<EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
</PropertyGroup>
```
### 4. Size Comparison Table
| Configuration | Typical Size (Linux x64) |
|--------------|--------------------------|
| Framework-dependent | ~200 KB |
| Self-contained | ~70 MB |
| Self-contained + Trimmed | ~30 MB |
| Native AOT (no optimizations) | ~15 MB |
| Native AOT + Size optimizations | ~8-10 MB |
| Native AOT + Trimmed + Size opts | ~5-8 MB |
### 5. Measure and Analyze Size
```bash
# Publish and analyze
dotnet publish -c Release -r linux-x64 /p:PublishAot=true
# Check size
ls -lh bin/Release/net10.0/linux-x64/publish/
# Analyze trimmed assemblies (if not using AOT)
dotnet run --project $(dotnet tool run dotnet-ilverify) \
bin/Release/net10.0/linux-x64/publish/Morphir.dll
```
---
## Reflection and Dynamic Code
### Problem: Reflection Breaks in AOT/Trimmed Builds
```csharp
// ❌ This breaks in AOT
Type type = Type.GetType("Morphir.IR.Package");
var instance = Activator.CreateInstance(type);
```
### Solution 1: Source Generators
```csharp
// ✅ Use source generators
[JsonSerializable(typeof(Package))]
[JsonSerializable(typeof(Module))]
public partial class MorphirJsonContext : JsonSerializerContext { }
// Usage
var package = JsonSerializer.Deserialize(json, MorphirJsonContext.Default.Package);
```
### Solution 2: Dynamic Dependency Attributes
```csharp
using System.Diagnostics.CodeAnalysis;
public class PackageLoader
{
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Package))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(Module))]
public static void LoadTypes()
{
// Ensures types are preserved
}
}
```
### Solution 3: RequiresUnreferencedCode Annotation
```csharp
using System.Diagnostics.CodeAnalysis;
// Mark methods that use reflection
[RequiresUnreferencedCode("Uses reflection to load plugins")]
public static void LoadPlugins(string path)
{
var assemblies = Directory.GetFiles(path, "*.dll")
.Select(Assembly.LoadFrom);
foreach (var asm in assemblies)
{
// Reflection code here
}
}
```
### Solution 4: Avoid Reflection Entirely
```csharp
// ❌ Avoid: Reflection-based deserialization
var type = Type.GetType(typeName);
var deserializer = typeof(JsonSerializer)
.GetMethod("Deserialize")
.MakeGenericMethod(type);
// ✅ Good: Compile-time known types with source generators
var result = typeName switch
{
"Package" => JsonSerializer.Deserialize(json, MorphirJsonContext.Default.Package),
"Module" => JsonSerializer.Deserialize(json, MorphirJsonContext.Default.Module),
_ => throw new NotSupportedException($"Unknown type: {typeName}")
};
```
---
## JSON Serialization in AOT
### Problem: System.Text.Json Uses Reflection
By default, `System.Text.Json` uses reflection for serialization, which doesn't work in Native AOT.
### Solution: Source-Generated Serialization Context
#### C# Example
```csharp
using System.Text.Json;
using System.Text.Json.Serialization;
// Define all types that need serialization
[JsonSerializable(typeof(VerifyIRResult))]
[JsonSerializable(typeof(Config))]
[JsonSerializable(typeof(Package))]
[JsonSerializable(typeof(Module))]
[JsonSourceGenerationOptions(
WriteIndented = true,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)]
public partial class MorphirJsonContext : JsonSerializerContext
{
}
// Usage
var result = new VerifyIRResult(
IsValid: true,
SchemaVersion: "3",
FilePath: "test.json",
Errors: [],
Timestamp: DateTime.UtcNow
);
var json = JsonSerializer.Serialize(result, MorphirJsonContext.Default.VerifyIRResult);
var deserialized = JsonSerializer.Deserialize(json, MorphirJsonContext.Default.VerifyIRResult);
```
#### F# Example
F# can also use source generators:
```fsharp
open System.Text.Json
open System.Text.Json.Serialization
// Define serialization context
[<JsonSerializable(typeof<ScriptResult>)>]
[<JsonSerializable(typeof<Config>)>]
[<JsonSourceGenerationOptions(
WriteIndented = true,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)>]
type MorphirJsonContext() =
inherit JsonSerializerContext()
// Usage
let result = { Success = true; Version = Some "1.0.0"; Errors = []; ExitCode = 0 }
let json = JsonSerializer.Serialize(result, MorphirJsonContext.Default.ScriptResult)
```
### F# with FSharp.SystemTextJson and AOT
`FSharp.SystemTextJson` doesn't fully support Native AOT. Options:
1. **Use source generators** (as shown above)
2. **Use simpler types** (records without unions/options for AOT builds)
3. **Mark AOT-incompatible code** with `RequiresUnreferencedCode`
```fsharp
open System.Diagnostics.CodeAnalysis
[<RequiresUnreferencedCode("FSharp.SystemTextJson uses reflection")>]
let serializeWithFSharpJson (value: 'T) : string =
let options = JsonSerializerOptions()
options.Converters.Add(JsonFSharpConverter())
JsonSerializer.Serialize(value, options)
```
---
## Common Gotchas and Workarounds
### 1. Assembly.GetTypes() Fails
**Problem**: `Assembly.GetTypes()` returns incomplete list in trimmed builds.
**Workaround**: Use explicit type lists or source generators.
```csharp
// ❌ Breaks with trimming
var types = Assembly.GetExecutingAssembly().GetTypes();
// ✅ Use explicit list
private static readonly Type[] KnownTypes =
[
typeof(Package),
typeof(Module),
typeof(TypeDefinition)
];
```
### 2. LINQ Expression Trees Fail in AOT
**Problem**: Expression trees use `Reflection.Emit`.
**Workaround**: Replace with delegates or source generators.
```csharp
// ❌ Fails in AOT
Expression<Func<int, int>> expr = x => x * 2;
var compiled = expr.Compile();
// ✅ Use delegates directly
Func<int, int> func = x => x * 2;
```
### 3. Type.GetType() Returns Null
**Problem**: Types are trimmed away.
**Workaround**: Use `DynamicDependency` or explicit type references.
```csharp
[DynamicDependency(DynamicallyAccessedMemberTypes.All, "Morphir.IR.Package", "Morphir.Core")]
public static Type GetPackageType()
{
return Type.GetType("Morphir.IR.Package, Morphir.Core");
}
```
### 4. WolverineFx and AOT
**Issue**: WolverineFx uses reflection for handler discovery.
**Workaround**: Explicitly register handlers in AOT builds.
```csharp
// AOT-compatible WolverineFx setup
builder.Services.AddWolverine(opts =>
{
// Explicitly register handlers instead of auto-discovery
opts.Discovery.DisableConventionalDiscovery();
opts.Handlers.AddHandler<VerifyIRHandler>();
opts.Handlers.AddHandler<OtherHandler>();
});
```
### 5. Serilog Sinks May Use Reflection
**Issue**: Some Serilog sinks use reflection.
**Workaround**: Use console/file sinks which are AOT-compatible.
```csharp
// ✅ AOT-compatible logging
Log.Logger = new LoggerConfiguration()
.WriteTo.Console(
standardErrorFromLevel: LogEventLevel.Verbose,
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
```
### 6. Spectre.Console and AOT
**Issue**: Spectre.Console generally works with AOT, but some features use reflection.
**Workaround**: Avoid reflection-based features, test thoroughly.
```csharp
// ✅ AOT-compatible Spectre.Console usage
AnsiConsole.MarkupLine("[green]✓ Success[/]");
var table = new Table();
table.AddColumn("Name");
table.AddColumn("Value");
table.AddRow("Version", "1.0.0");
AnsiConsole.Write(table);
```
### 7. Embedded Resources in AOT
**Issue**: `Assembly.GetManifestResourceStream()` works, but resource names change.
**Workaround**: Use fully qualified names and test.
```csharp
// ✅ Correct resource naming
var resourceName = "Morphir.Tooling.schemas.v3.json";
using var stream = Assembly.GetExecutingAssembly()
.GetManifestResourceStream(resourceName);
if (stream == null)
{
// List available resources for debugging
var available = Assembly.GetExecutingAssembly()
.GetManifestResourceNames();
throw new FileNotFoundException(
$"Resource '{resourceName}' not found. Available: {string.Join(", ", available)}");
}
```
---
## Testing AOT/Trimmed Builds
### 1. Test Matrix
Test all build configurations:
```bash
# Framework-dependent
dotnet build -c Release
# Self-contained
dotnet publish -c Release -r linux-x64 --self-contained
# Trimmed
dotnet publish -c Release -r linux-x64 --self-contained /p:PublishTrimmed=true
# Native AOT
dotnet publish -c Release -r linux-x64 /p:PublishAot=true
```
### 2. Automated Testing Script
```bash
#!/bin/bash
set -euo pipefail
echo "Testing AOT build..."
# Build Native AOT
dotnet publish -c Release -r linux-x64 /p:PublishAot=true
# Run basic smoke tests
./bin/Release/net10.0/linux-x64/publish/morphir --version
./bin/Release/net10.0/linux-x64/publish/morphir --help
# Test IR verification (example)
./bin/Release/net10.0/linux-x64/publish/morphir ir verify tests/TestData/valid-ir-v3.json
echo "AOT build tests passed!"
```
### 3. Size Regression Testing
```bash
#!/bin/bash
# Check executable size doesn't exceed threshold
MAX_SIZE_MB=10
EXECUTABLE="bin/Release/net10.0/linux-x64/publish/morphir"
SIZE_BYTES=$(stat -f%z "$EXECUTABLE" 2>/dev/null || stat -c%s "$EXECUTABLE")
SIZE_MB=$((SIZE_BYTES / 1024 / 1024))
if [ "$SIZE_MB" -gt "$MAX_SIZE_MB" ]; then
echo "❌ Executable size ($SIZE_MB MB) exceeds threshold ($MAX_SIZE_MB MB)"
exit 1
else
echo "✅ Executable size: $SIZE_MB MB (threshold: $MAX_SIZE_MB MB)"
fi
```
### 4. Trim Warnings Analysis
```xml
<PropertyGroup>
<!-- Treat trim warnings as errors in CI -->
<TrimmerSingleWarn>false</TrimmerSingleWarn>
<IlcTreatWarningsAsErrors>true</IlcTreatWarningsAsErrors>
</PropertyGroup>
```
---
## Best Practices Checklist
### Design Phase
- [ ] Avoid reflection and dynamic code generation
- [ ] Use source generators for JSON serialization
- [ ] Design with trimming in mind (explicit dependencies)
- [ ] Plan for platform-specific builds
- [ ] Consider size vs. feature trade-offs
### Implementation Phase
- [ ] Use `sealed` classes where possible
- [ ] Use static methods when appropriate
- [ ] Prefer compile-time known types over dynamic types
- [ ] Add `[DynamicDependency]` attributes where needed
- [ ] Use source-generated JSON contexts
- [ ] Avoid LINQ expression trees in critical paths
- [ ] Use `InvariantGlobalization` if localization not needed
### Testing Phase
- [ ] Test with `PublishTrimmed=true`
- [ ] Test with `PublishAot=true`
- [ ] Run full test suite on AOT builds
- [ ] Check executable size
- [ ] Verify embedded resources load correctly
- [ ] Test on all target platforms (Linux, Windows, macOS)
- [ ] Performance test startup time and memory usage
### Configuration Phase
- [ ] Enable AOT/trim analyzers
- [ ] Configure size optimizations
- [ ] Add trimmer root descriptors if needed
- [ ] Document platform-specific requirements
- [ ] Set up CI for multi-platform builds
### Documentation Phase
- [ ] Document AOT limitations
- [ ] List unsupported features (if any)
- [ ] Provide platform-specific instructions
- [ ] Document size expectations per platform
---
## Summary
### Key Principles
1. **Design for AOT from the start** - Retrofitting is harder
2. **Avoid reflection** - Use source generators instead
3. **Test early and often** - AOT issues appear late
4. **Measure size** - Optimize incrementally
5. **Use explicit types** - Don't rely on runtime type discovery
6. **Document limitations** - Be clear about what doesn't work
### Quick Reference: AOT-Compatible Patterns
| Pattern | Status | Alternative |
|---------|--------|-------------|
| Source generators | ✅ Supported | - |
| Static methods | ✅ Supported | - |
| Sealed classes | ✅ Supported | - |
| System.Text.Json (with source gen) | ✅ Supported | - |
| Embedded resources | ✅ Supported | Test names carefully |
| Serilog (console/file) | ✅ Supported | Avoid reflection-based sinks |
| Spectre.Console (basic) | ✅ Supported | Avoid advanced features |
| Reflection.Emit | ❌ Not supported | Use source generators |
| Dynamic types | ❌ Not supported | Use explicit types |
| Assembly.GetTypes() | ⚠️ Limited | Use explicit type lists |
| LINQ expressions | ⚠️ Limited | Use delegates |
| FSharp.SystemTextJson | ⚠️ Limited | Use source generators |
### Common Size Targets (Linux x64)
- **Minimal CLI tool**: 5-8 MB (AOT + trimming + size opts)
- **Feature-rich CLI**: 8-12 MB (AOT + trimming)
- **With rich UI**: 10-15 MB (AOT + Spectre.Console)
---
## References
- [Native AOT Deployment](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/)
- [Trim Self-Contained Deployments](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trim-self-contained)
- [Single-File Deployment](https://learn.microsoft.com/en-us/dotnet/core/deploying/single-file/overview)
- [Source Generation for JSON](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/source-generation)
- [AOT Warnings (IL2XXX, IL3XXX)](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/warnings/)
- [.NET Size Optimization](https://devblogs.microsoft.com/dotnet/app-trimming-in-dotnet-5/)
- [AGENTS.md](../../AGENTS.md) - Project-wide agent guidance
```
### ../../../docs/contributing/fsharp-coding-guide.md
```markdown
# F# Coding Guide for morphir-dotnet
This guide provides F#-specific coding standards and best practices for the morphir-dotnet project, including F# Interactive scripts in `.claude/skills/`.
## Table of Contents
1. [Pattern Matching and Value Extraction](#pattern-matching-and-value-extraction)
2. [Active Patterns](#active-patterns)
3. [Error Handling](#error-handling)
4. [Immutability and Data Structures](#immutability-and-data-structures)
5. [Async and Task Workflows](#async-and-task-workflows)
6. [Type Design](#type-design)
7. [Computation Expressions and DSLs](#computation-expressions-and-dsls)
8. [JSON Serialization with System.Text.Json](#json-serialization-with-systemtextjson)
9. [CLI Scripts (.fsx)](#cli-scripts-fsx)
10. [Testing](#testing)
---
## Pattern Matching and Value Extraction
### ✅ Prefer Active Patterns Over Complex If-Then Chains
Active patterns make value extraction more declarative and easier to understand.
**❌ Avoid: Complex if-then chains**
```fsharp
let processJson (element: JsonElement) =
if element.ValueKind = JsonValueKind.Null then
None
elif element.ValueKind = JsonValueKind.String then
Some (element.GetString())
elif element.ValueKind = JsonValueKind.Number then
Some (element.GetInt32().ToString())
else
None
```
**✅ Prefer: Active patterns**
```fsharp
let (|NullJson|StringJson|NumberJson|OtherJson|) (element: JsonElement) =
match element.ValueKind with
| JsonValueKind.Null -> NullJson
| JsonValueKind.String -> StringJson (element.GetString())
| JsonValueKind.Number -> NumberJson (element.GetInt32())
| _ -> OtherJson
let processJson (element: JsonElement) =
match element with
| NullJson -> None
| StringJson s -> Some s
| NumberJson n -> Some (n.ToString())
| OtherJson -> None
```
### Common Active Pattern Use Cases
#### 1. JSON Property Extraction
**✅ Good: Active pattern for optional properties**
```fsharp
let (|JsonProperty|_|) (propertyName: string) (element: JsonElement) =
let mutable prop = Unchecked.defaultof<JsonElement>
if element.TryGetProperty(propertyName, &prop) then
Some prop
else
None
// Usage
match root with
| JsonProperty "version" version -> Some (version.GetString())
| _ -> None
```
#### 2. String Pattern Matching
**✅ Good: Active patterns for string parsing**
```fsharp
let (|StartsWith|_|) (prefix: string) (input: string) =
if input.StartsWith(prefix) then
Some (input.Substring(prefix.Length))
else
None
let (|Contains|_|) (substring: string) (input: string) =
if input.Contains(substring) then Some ()
else None
// Usage
match line with
| StartsWith "## " title -> processHeader title
| StartsWith "- " item -> processListItem item
| Contains "BREAKING" & Contains "CHANGE" -> markAsBreaking line
| _ -> processPlainText line
```
#### 3. Result/Option Chaining
**✅ Good: Active patterns for result unpacking**
```fsharp
let (|Success|Failure|) (result: Result<'a, 'b>) =
match result with
| Ok value -> Success value
| Error err -> Failure err
// Usage
match checkRemoteCI() with
| Success ciState -> processCI ciState
| Failure error -> logError error
```
### Pattern Matching Best Practices
1. **Exhaustive Matching**: Always handle all cases
```fsharp
// ✅ Good: Exhaustive match
match optionalValue with
| Some value -> processValue value
| None -> useDefault()
// ❌ Bad: Incomplete match (compiler warning)
match optionalValue with
| Some value -> processValue value
```
2. **Guard Clauses**: Use `when` for additional conditions
```fsharp
match parseVersion changelog with
| Major, changes when changes > 0 -> "major"
| Minor, changes when changes > 0 -> "minor"
| Patch, _ -> "patch"
| _, _ -> "none"
```
3. **Pattern AND/OR Combinations**
```fsharp
// AND pattern (&)
match line with
| Contains "TODO" & StartsWith "- " -> extractTodo line
| _ -> None
// OR pattern (|)
match status with
| "success" | "completed" -> handleSuccess()
| _ -> handleOther()
```
---
## Active Patterns
### Partial Active Patterns
Use partial active patterns (`|Pattern|_|`) when the pattern might not match:
```fsharp
let (|Int|_|) (str: string) =
match Int32.TryParse(str) with
| true, value -> Some value
| false, _ -> None
let (|ValidEmail|_|) (str: string) =
if str.Contains("@") && str.Contains(".") then
Some str
else
None
// Usage
match input with
| Int n -> printfn "Number: %d" n
| ValidEmail email -> printfn "Email: %s" email
| _ -> printfn "Unknown format"
```
### Multi-Case Active Patterns
Use for categorizing values into multiple cases:
```fsharp
let (|Even|Odd|) n =
if n % 2 = 0 then Even else Odd
let (|Positive|Negative|Zero|) n =
if n > 0 then Positive
elif n < 0 then Negative
else Zero
// Usage
match number with
| Even & Positive -> "even positive"
| Even & Negative -> "even negative"
| Odd & Positive -> "odd positive"
| Odd & Negative -> "odd negative"
| Zero -> "zero"
```
### Parameterized Active Patterns
```fsharp
let (|DivisibleBy|_|) divisor n =
if n % divisor = 0 then Some() else None
// Usage
match number with
| DivisibleBy 15 -> "FizzBuzz"
| DivisibleBy 3 -> "Fizz"
| DivisibleBy 5 -> "Buzz"
| _ -> string number
```
---
## Error Handling
### Result Type
Prefer `Result<'T, 'Error>` for operations that can fail:
```fsharp
type ValidationError =
| MissingField of string
| InvalidFormat of string
| OutOfRange of string * int * int
let validateVersion (version: string) : Result<string, ValidationError> =
if String.IsNullOrWhiteSpace(version) then
Error (MissingField "version")
elif not (version.Contains(".")) then
Error (InvalidFormat "Version must contain dots")
else
Ok version
```
### Result Active Patterns
```fsharp
let (|Ok|Error|) (result: Result<'a, 'b>) =
match result with
| Result.Ok value -> Ok value
| Result.Error err -> Error err
// Usage
match validateVersion input with
| Ok version -> processVersion version
| Error (MissingField field) -> printfn "Missing: %s" field
| Error (InvalidFormat msg) -> printfn "Invalid: %s" msg
| Error (OutOfRange (field, min, max)) -> printfn "%s must be between %d and %d" field min max
```
### Railway-Oriented Programming
```fsharp
let (>>=) result func =
match result with
| Ok value -> func value
| Error err -> Error err
let validateAndProcess input =
validateVersion input
>>= parseVersion
>>= checkAvailability
>>= createRelease
```
---
## Immutability and Data Structures
### Record Types
Always use immutable records:
```fsharp
// ✅ Good: Immutable record
type ReleaseInfo = {
Version: string
Date: DateTime
Changes: string list
}
// ✅ Good: Record update expression
let updated = { original with Date = DateTime.Now }
// ❌ Avoid: Mutable fields
type ReleaseInfo = {
mutable Version: string // Don't do this
mutable Date: DateTime
}
```
### Collections
Prefer immutable collections:
```fsharp
// ✅ Good: Immutable list
let changes = [ "Added feature X"; "Fixed bug Y" ]
let moreChanges = "Updated docs" :: changes
// ✅ Good: List comprehension
let numbers = [ for i in 1..10 -> i * 2 ]
// ❌ Avoid: ResizeArray (mutable)
let changes = ResizeArray<string>()
changes.Add("Added feature X") // Mutable operation
```
### Options vs Nulls
Always use `Option<'T>` instead of null:
```fsharp
// ✅ Good: Option type
type Config = {
Port: int option
Host: string
}
let getPort config =
config.Port |> Option.defaultValue 8080
// ❌ Avoid: Nullable
type Config = {
Port: Nullable<int> // Don't use in F#
}
```
### C# Interop: Nullable Reference Types (F# 9+)
When writing F# code that interoperates with C# (especially in mixed C#/F# projects), use F# 9's nullable reference types feature for better C# interop:
```fsharp
// Enable nullable reference types in .fsproj:
// <Nullable>enable</Nullable>
// ✅ Good: Explicit nullability for C# consumers
type IUserService =
abstract member GetUserById: userId: string -> string | null
abstract member GetUserName: userId: string -> string // Non-nullable
// ✅ Good: Clear null handling in public API
let tryGetValue (key: string) : string | null =
if cache.ContainsKey(key) then
cache.[key]
else
null
// ✅ Good: Guard against nulls from C# code
let processName (name: string | null) : string =
match name with
| null -> "Unknown"
| value -> value.Trim()
// ✅ Good: F# Option for internal code, nullable for C# boundary
type UserRepository() =
// Internal: use Option
let findUserInternal (id: string) : User option =
// ... implementation
None
// Public API for C#: use nullable reference types
member this.FindUser(id: string) : User | null =
findUserInternal id |> Option.toObj
```
**When to use nullable reference types:**
- Public APIs consumed by C# code
- Implementing C# interfaces
- Interacting with C# libraries that use nullable annotations
- Converting between F# Option and C# nullable types
**Pattern: Converting between Option and nullable**
```fsharp
// Option to nullable (for C# API)
let toNullable (opt: 'T option) : 'T | null =
opt |> Option.toObj
// Nullable to Option (from C# API)
let fromNullable (value: 'T | null) : 'T option =
value |> Option.ofObj
// Example usage
type MorphirService() =
// Internal F# code uses Option
let loadPackage (name: string) : Package option =
// ... implementation
None
// C# API uses nullable reference types
interface IMorphirService with
member this.LoadPackage(name: string) : Package | null =
loadPackage name |> toNullable
```
**Important**: Even with nullable reference types enabled, prefer `Option<'T>` for F#-only code. Only use nullable reference types at C# interop boundaries.
---
## Async and Task Workflows
### Async Workflows
Use async workflows for asynchronous operations:
```fsharp
let fetchDataAsync (url: string) : Async<Result<string, string>> =
async {
try
use client = new HttpClient()
let! response = client.GetStringAsync(url) |> Async.AwaitTask
return Ok response
with ex ->
return Error ex.Message
}
```
### Cancellation Support
Always support cancellation in long-running async operations:
```fsharp
let processWithCancellation (ct: CancellationToken) : Async<Result<unit, string>> =
async {
try
do! Async.Sleep(1000) // Automatically checks cancellation
if ct.IsCancellationRequested then
return Error "Cancelled"
else
return Ok ()
with
| :? OperationCanceledException ->
return Error "Cancelled"
}
```
### Parallel Async Operations
```fsharp
let checkAllPackages packages ct =
async {
let! results =
packages
|> List.map (fun pkg -> checkPackageAsync pkg ct)
|> Async.Parallel
return results |> Array.toList
}
```
---
## Type Design
### Discriminated Unions for State
Make illegal states unrepresentable:
```fsharp
// ✅ Good: Impossible to have invalid state
type WorkflowState =
| NotStarted
| InProgress of runId: int64 * startTime: DateTime
| Completed of runId: int64 * result: string
| Failed of runId: int64 * error: string
// ❌ Avoid: Boolean flags
type WorkflowState = {
IsStarted: bool
IsCompleted: bool
IsFailed: bool
RunId: int64 option
Error: string option
} // Can represent invalid states!
```
### Single-Case Discriminated Unions
Use for type safety and domain modeling:
```fsharp
type Version = Version of string
type PackageName = PackageName of string
type GitHash = GitHash of string
let createVersion (str: string) : Result<Version, string> =
if str.Contains(".") then
Ok (Version str)
else
Error "Invalid version format"
// Usage - type safety prevents mixing up string parameters
let publishPackage (name: PackageName) (version: Version) = ...
```
### Measure Types for Units
```fsharp
[<Measure>] type minutes
[<Measure>] type seconds
let timeout = 30<minutes>
let interval = 5<seconds>
let toSeconds (mins: float<minutes>) : float<seconds> =
mins * 60.0<seconds/minutes>
```
---
## Computation Expressions and DSLs
Computation Expressions (CEs) are F#'s powerful mechanism for creating Domain-Specific Languages (DSLs). morphir-dotnet uses CEs extensively for building IR constructs with clean, declarative syntax.
### Understanding Computation Expressions
A computation expression is syntactic sugar that transforms code inside `{ }` blocks according to rules defined by a builder class:
```fsharp
// What you write:
literal { Bool true }
// What F# translates to:
let _builder = literal
let _state = _builder.Zero()
let _result = _builder.BoolOp(_state, true)
_result
```
### The Two Main CE Patterns
morphir-dotnet uses two complementary patterns:
#### Pattern 1: CustomOperation Pattern (Query-Style)
Use for query-like DSLs where operations flow sequentially:
```fsharp
type FQNameBuilder(packagePath, modulePath, localName) =
new() = FQNameBuilder(None, None, None)
member _.Zero() = FQNameBuilder(None, None, None)
[<CustomOperation("package")>]
member _.Package(builder: FQNameBuilder, strs: string list) =
let pkg = Path.fromList (strs |> List.map Name.fromString) |> PackageName.packageName
FQNameBuilder(Some pkg, builder.ModulePath, builder.LocalName)
[<CustomOperation("module'")>]
member _.Module(builder: FQNameBuilder, strs: string list) =
let mod' = Path.fromList (strs |> List.map Name.fromString) |> ModulePath.modulePath
FQNameBuilder(builder.PackagePath, Some mod', builder.LocalName)
[<CustomOperation("local")>]
member _.Local(builder: FQNameBuilder, str: string) =
FQNameBuilder(builder.PackagePath, builder.ModulePath, Some (Name.fromString str))
let fqName = FQNameBuilder()
// Usage - clean query syntax:
fqName {
package ["morphir"; "sdk"]
module' ["basics"]
local "int"
}
```
**Key characteristics:**
- State flows through operations (builder accumulates changes)
- CustomOperations modify and return new builder state
- No `Yield`, `Delay`, or `Run` needed for simple cases
- Clean keyword-based syntax
#### Pattern 2: Yield/Delay/Run Pattern (Compositional)
Use for hierarchical, nested structures:
```fsharp
type LiteralBuilder() =
member _.Yield(lit: Literal) = lit
member _.Combine(_, lit: Literal) = lit
member _.For(items, f) = items |> Seq.map f |> Seq.last
member _.Zero() = BoolLiteral false
member _.Delay(f: unit -> Literal) = f
member _.Run(f: unit -> Literal) = f()
// CustomOperations that ignore state and return literals
[<CustomOperation("Bool")>]
member _.BoolOp(_: Literal, value: bool) = BoolLiteral value
[<CustomOperation("String")>]
member _.StringOp(_: Literal, value: string) = StringLiteral value
let literal = LiteralBuilder()
// Usage:
literal { Bool true }
literal { String "hello" }
```
**Key characteristics:**
- Supports deep nesting through `Yield`
- `Delay` and `Run` enable deferred execution
- `Combine` enables multiple expressions in one block
- CustomOperations can coexist with Yield pattern
### Hybrid Pattern (Best of Both Worlds)
morphir-dotnet's DSL builders use a hybrid approach - combining both patterns:
```fsharp
type ValueBuilder() =
// Standard CE methods for composition
member _.Yield(value: Value<unit, unit>) = value
member _.Delay(f: unit -> Value<unit, unit>) = f
member _.Run(f: unit -> Value<unit, unit>) = f()
member _.Combine(_, value: Value<unit, unit>) = value
member _.Zero() = Unit()
// CustomOperations for clean syntax
[<CustomOperation("literal")>]
member _.LiteralOp(_: Value<unit, unit>, lit: Literal) =
Literal((), lit)
[<CustomOperation("tuple")>]
member _.TupleOp(_: Value<unit, unit>, elements: Value<unit, unit> list) =
Tuple((), elements)
let value = ValueBuilder()
// Both styles work:
value { literal (BoolLiteral true) } // CustomOperation style
value { Literal((), BoolLiteral true) } // Yield style
```
### CustomOperation Best Practices
#### 1. Naming Conventions
**✅ Good: Use domain-appropriate names**
```fsharp
[<CustomOperation("package")>] // Clear, matches domain
[<CustomOperation("where")>] // Query keyword
[<CustomOperation("select")>] // Standard operation
```
**❌ Avoid: Generic or unclear names**
```fsharp
[<CustomOperation("set")>] // Too generic
[<CustomOperation("do")>] // F# keyword (confusing)
```
#### 2. State Parameter Pattern
The first parameter of a CustomOperation is always the builder state, often ignored:
```fsharp
// ✅ Good: Clear state ignore pattern
[<CustomOperation("Bool")>]
member _.BoolOp(_state: Literal, value: bool) = BoolLiteral value
// ❌ Avoid: Unnamed parameter (unclear intent)
[<CustomOperation("Bool")>]
member _.BoolOp(lit, value: bool) = BoolLiteral value
```
#### 3. Method vs CustomOperation
Choose based on usage context:
| Use CustomOperation When | Use Regular Method When |
|--------------------------|-------------------------|
| Query-style syntax desired (`where`, `select`) | Function-call style preferred |
| Inside CE blocks only | Need to call outside CE |
| Building up state | Direct value construction |
| Example: `fqName { package ["morphir"] }` | Example: `fqName.Package(["morphir"])` |
```fsharp
type Builder() =
member _.Zero() = State.Empty
// CustomOperation - for CE use
[<CustomOperation("select")>]
member _.SelectOp(state, value) = { state with Value = value }
// Regular method - callable anywhere
member _.Select(value) = { State.Empty with Value = value }
let b = Builder()
// CustomOperation usage (only inside CE):
b { select "name" }
// Regular method usage (anywhere):
b.Select("name")
b { Select("name") } // Also works in CE
```
### Performance: InlineIfLambda (F# 6+)
For high-performance DSLs, use `[<InlineIfLambda>]` to eliminate closure allocations:
```fsharp
type ListBuilder() =
[<InlineIfLambda>]
member inline _.Delay([<InlineIfLambda>] f: unit -> 'T list) = f
[<InlineIfLambda>]
member inline _.Run([<InlineIfLambda>] f: unit -> 'T list) = f()
[<InlineIfLambda>]
member inline _.Combine([<InlineIfLambda>] a: 'T list, [<InlineIfLambda>] b: 'T list) =
List.append a b
```
**Benefits:**
- Up to 5x faster than standard CE implementation
- Zero allocations when combined with struct builders
- Completely linear IL - nested lambdas flattened at compile time
**When to use:**
- High-frequency code paths (HTML generation, string building)
- Performance-critical DSLs
- List/array builders
- Any CE where allocations matter
**Requirements:**
- Must use `inline` functions
- Must mark lambda parameters with `[<InlineIfLambda>]`
- Only available in F# 6+
### Common CE Gotchas
#### Gotcha 1: Bare Identifiers Require Properties or CustomOperations
```fsharp
// ❌ This FAILS - F# can't find 'Bool' in scope
type LiteralBuilder() =
member _.Zero() = BoolLiteral false
member _.Bool(value: bool) = BoolLiteral value // Method, not property
let literal = LiteralBuilder()
literal { Bool true } // Error: 'Bool' not defined
// ✅ Fix Option 1: Use CustomOperation
type LiteralBuilder() =
member _.Zero() = BoolLiteral false
[<CustomOperation("Bool")>]
member _.BoolOp(_: Literal, value: bool) = BoolLiteral value
// ✅ Fix Option 2: Make it a property returning a function
type LiteralBuilder() =
member _.Zero() = BoolLiteral false
member _.Bool = BoolLiteral // Property
// ✅ Fix Option 3: Call with explicit parentheses
literal { Bool(true) } // Works with regular method
```
#### Gotcha 2: F# Keywords Conflict with Method Names
```fsharp
// ❌ These conflict with F# built-in conversion functions
member _.string(value) = ... // Conflicts with 'string' function
member _.int(value) = ... // Conflicts with 'int' function
member _.float(value) = ... // Conflicts with 'float' function
// ✅ Use Pascal case to avoid conflicts
member _.String(value) = ... // No conflict
member _.Int(value) = ...
member _.Float(value) = ...
```
#### Gotcha 3: CustomOperations Return Type
CustomOperations must return the builder state type (or compatible type):
```fsharp
// ❌ Wrong return type causes "expected 'Type<unit>' but got 'unit'" error
[<CustomOperation("reference")>]
member _.ReferenceOp(_: Type<unit>, fqName: FQName) : unit =
() // Returns unit, not Type<unit>!
// ✅ Correct - returns the state type
[<CustomOperation("reference")>]
member _.ReferenceOp(_: Type<unit>, fqName: FQName) : Type<unit> =
Reference((), fqName, [])
```
### morphir-dotnet DSL Examples
#### Example 1: FQName Builder (CustomOperation Pattern)
```fsharp
// From src/Morphir.Models/IR/DSL/Names.fs
fqName {
package ["morphir"; "sdk"]
module' ["basics"]
local "int"
}
// Result: FQName for morphir.sdk.basics.int
```
#### Example 2: Literal Builder (Hybrid Pattern)
```fsharp
// From src/Morphir.Models/IR/Classic/DSL/Literals.fs
literal { Bool true }
literal { String "hello" }
literal { Int 42L }
literal { Float 3.14 }
```
#### Example 3: Type Builder (Hybrid Pattern)
```fsharp
// From src/Morphir.Models/IR/Classic/DSL/Types.fs
type' {
reference (fqName {
package ["morphir"; "sdk"]
module' ["list"]
local "list"
})
}
type' {
tuple [
intType
stringType
]
}
type' {
record [
field "name" stringType
field "age" intType
]
}
```
#### Example 4: Pattern Builder (Hybrid Pattern)
```fsharp
// From src/Morphir.Models/IR/Classic/DSL/Patterns.fs
pattern { wildcard }
pattern { Variable "x" }
pattern { Tuple [ pattern1; pattern2 ] }
pattern { Constructor fqName [ argPattern ] }
```
### CE Decision Tree
```
What kind of DSL are you building?
├── Query-style (operations flow sequentially)
│ └── Use: CustomOperations only
│ - Example: fqName { package []; module' []; local "" }
│ - Need: Zero, CustomOperations
│ - Skip: Yield, Delay, Run
│
├── Hierarchical (nested structures)
│ └── Use: Yield/Delay/Run pattern
│ - Example: div { span { "Hello" }; span { "World" } }
│ - Need: Yield, Delay, Run, Combine
│ - Optional: CustomOperations for keywords
│
└── Hybrid (both query and nesting)
└── Use: Both patterns together
- Example: morphir-dotnet IR builders
- Need: All CE methods + CustomOperations
- Flexible: Supports multiple usage styles
```
### Testing CE Builders
```fsharp
[<Test>]
let ``literal builder creates BoolLiteral`` () =
let result = literal { Bool true }
let expected = BoolLiteral true
Assert.AreEqual(expected, result)
[<Test>]
let ``fqName builder creates correct FQName`` () =
let result =
fqName {
package ["morphir"; "sdk"]
module' ["basics"]
local "int"
}
Assert.AreEqual("morphir.sdk", result.PackagePath)
Assert.AreEqual("basics", result.ModulePath)
Assert.AreEqual("int", result.LocalName)
```
### CE Best Practices Summary
1. ✅ **Choose the right pattern** - CustomOperations for queries, Yield for nesting
2. ✅ **Use hybrid for flexibility** - Combine both patterns in complex DSLs
3. ✅ **Name CustomOperations clearly** - Use domain-appropriate keywords
4. ✅ **Ignore state parameter** - Use `_state` or `_` when not needed
5. ✅ **Avoid F# keyword conflicts** - Use Pascal case for method names
6. ✅ **Return correct types** - CustomOperations must return builder state
7. ✅ **Consider InlineIfLambda** - For performance-critical DSLs (F# 6+)
8. ✅ **Test both styles** - If hybrid, test CustomOperation and Yield usage
9. ✅ **Document usage patterns** - Show examples of both styles
10. ✅ **Provide helper functions** - Complement CEs with standalone functions
### References
- [F# Computation Expressions](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions)
- [F# RFC FS-1056: Custom Operation Overloads](https://github.com/fsharp/fslang-design/blob/main/FSharp-6.0/FS-1056-allow-custom-operation-overloads.md)
- [F# RFC FS-1098: InlineIfLambda](https://github.com/fsharp/fslang-design/blob/main/FSharp-6.0/FS-1098-inline-if-lambda.md)
- [F# for Fun and Profit - Computation Expressions Series](https://fsharpforfunandprofit.com/series/computation-expressions/)
- [Bolero HTML Builders](https://github.com/fsbolero/Bolero/blob/master/src/Bolero.Html/Builders.fs) - Excellent InlineIfLambda example
- [Fun.Blazor](https://github.com/slaveOftime/Fun.Blazor) - Component-based DSL
- [morphir-dotnet IR DSL](../src/Morphir.Models/IR/Classic/DSL/) - Reference implementation
---
## JSON Serialization with System.Text.Json
System.Text.Json is the recommended JSON library for .NET. When working with F# types, there are specific patterns and gotchas to be aware of.
**See also**: [Serialization Guide](./serialization-guide.md) for comprehensive serialization patterns across the project.
### Basic Serialization
```fsharp
#r "nuget: System.Text.Json, 9.0.0"
open System.Text.Json
open System.Text.Json.Serialization
// ✅ Good: Simple record type
type Config = {
Port: int
Host: string
Timeout: int
}
let config = { Port = 8080; Host = "localhost"; Timeout = 30 }
// Serialize with options
let options = JsonSerializerOptions()
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase
options.WriteIndented <- true
let json = JsonSerializer.Serialize(config, options)
// Output: { "port": 8080, "host": "localhost", "timeout": 30 }
```
### Common Gotchas
#### 1. F# Records Require Mutable Setters (By Default)
**Problem**: System.Text.Json by default requires mutable properties for deserialization.
```fsharp
// ❌ This will FAIL during deserialization
type User = {
Name: string
Age: int
}
let json = """{"name": "Alice", "age": 30}"""
let user = JsonSerializer.Deserialize<User>(json)
// Error: Cannot deserialize - no parameterless constructor or mutable properties
```
**Solution**: Use `FSharpJsonConverter` or enable record deserialization:
```fsharp
// ✅ Good: Use FSharp.SystemTextJson package
#r "nuget: FSharp.SystemTextJson, 1.3.13"
open System.Text.Json
open System.Text.Json.Serialization
let options = JsonSerializerOptions()
options.Converters.Add(JsonFSharpConverter())
let user = JsonSerializer.Deserialize<User>(json, options)
// Works correctly with immutable F# records
```
#### 2. Discriminated Unions Are Not Supported (By Default)
**Problem**: System.Text.Json doesn't understand F# discriminated unions out of the box.
```fsharp
// ❌ This will NOT serialize as expected
type Status =
| Pending
| InProgress of startTime: DateTime
| Completed of result: string
let status = InProgress DateTime.Now
let json = JsonSerializer.Serialize(status)
// Output: {} or error
```
**Solution**: Use `FSharp.SystemTextJson` which handles unions properly:
```fsharp
// ✅ Good: Use FSharp.SystemTextJson
let options = JsonSerializerOptions()
options.Converters.Add(JsonFSharpConverter())
let json = JsonSerializer.Serialize(status, options)
// Output: {"Case":"InProgress","Fields":["2025-12-18T15:30:00Z"]}
```
#### 3. Option Types Serialize as Objects
**Problem**: F# `option` types don't serialize as null/value by default.
```fsharp
// ❌ Without FSharp.SystemTextJson
type Config = {
Port: int option
Host: string
}
let config = { Port = None; Host = "localhost" }
let json = JsonSerializer.Serialize(config)
// Output: {"Port":{},"Host":"localhost"} - Port is empty object, not null
```
**Solution**: Use `FSharp.SystemTextJson` or configure options:
```fsharp
// ✅ Good: Use FSharp.SystemTextJson
let options = JsonSerializerOptions()
options.Converters.Add(JsonFSharpConverter(
unionEncoding = JsonUnionEncoding.Default,
unionTagNamingPolicy = JsonNamingPolicy.CamelCase
))
let json = JsonSerializer.Serialize(config, options)
// Output: {"port":null,"host":"localhost"} - Port is null as expected
```
#### 4. JsonElement Reading for Dynamic JSON
When reading JSON with unknown structure, use `JsonElement`:
```fsharp
// ✅ Good: Active pattern for JsonElement property access
let (|JsonProperty|_|) (propertyName: string) (element: JsonElement) =
let mutable prop = Unchecked.defaultof<JsonElement>
if element.TryGetProperty(propertyName, &prop) then
Some prop
else
None
// ✅ Good: Active pattern for JsonValueKind
let (|JsonString|JsonNumber|JsonBool|JsonNull|JsonArray|JsonObject|) (element: JsonElement) =
match element.ValueKind with
| JsonValueKind.String -> JsonString (element.GetString())
| JsonValueKind.Number -> JsonNumber (element.GetInt64())
| JsonValueKind.True -> JsonBool true
| JsonValueKind.False -> JsonBool false
| JsonValueKind.Null -> JsonNull
| JsonValueKind.Array -> JsonArray (element.EnumerateArray() |> Seq.toList)
| JsonValueKind.Object -> JsonObject element
| _ -> JsonNull
// Usage
let doc = JsonDocument.Parse(json)
match doc.RootElement with
| JsonProperty "version" (JsonString version) -> printfn "Version: %s" version
| JsonProperty "count" (JsonNumber count) -> printfn "Count: %d" count
| _ -> printfn "Unknown structure"
```
#### 5. Null vs Option<T> in JSON
When working with C# APIs that use nullable reference types:
```fsharp
// ✅ Good: Handling nulls from JSON
type ApiResponse = {
Data: string | null // For C# interop
Error: string option // For F# code
}
let parseResponse (json: string) : Result<ApiResponse, string> =
try
let response = JsonSerializer.Deserialize<ApiResponse>(json)
// Convert null to Option
let error = response.Error
Ok response
with ex ->
Error ex.Message
```
### Best Practices for JSON in F# Scripts
```fsharp
// ✅ Good: Configure options once and reuse
let jsonOptions =
let options = JsonSerializerOptions()
options.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase
options.WriteIndented <- true
options.Converters.Add(JsonFSharpConverter())
options
// ✅ Good: Type-safe result serialization
type ScriptResult = {
Success: bool
Version: string option
Errors: string list
ExitCode: int
}
let serializeResult (result: ScriptResult) : string =
JsonSerializer.Serialize(result, jsonOptions)
// ✅ Good: Safe deserialization with error handling
let deserializeConfig (json: string) : Result<Config, string> =
try
let config = JsonSerializer.Deserialize<Config>(json, jsonOptions)
Ok config
with
| :? JsonException as ex -> Error $"Invalid JSON: {ex.Message}"
| ex -> Error $"Deserialization error: {ex.Message}"
```
### CLI JSON Output Pattern
For CLI scripts that support `--json` output:
```fsharp
// ✅ Good: Separate human-readable and JSON output
let outputResult (result: ScriptResult) (jsonOutput: bool) =
if jsonOutput then
// ONLY JSON to stdout
let json = JsonSerializer.Serialize(result, jsonOptions)
printfn "%s" json
else
// Human-readable to stdout
printfn "=== Results ==="
printfn "Success: %b" result.Success
result.Version |> Option.iter (printfn "Version: %s")
if not result.Errors.IsEmpty then
printfn "Errors:"
result.Errors |> List.iter (printfn " - %s")
// ✅ Good: Test JSON output is valid
// Test command: dotnet fsi script.fsx --json | jq .
```
### Common Patterns from prepare-release.fsx
```fsharp
// ✅ Pattern: Parse GitHub API response (JSON array)
let parseGitHubRuns (json: string) : Result<WorkflowRun list, string> =
try
let doc = JsonDocument.Parse(json)
let runs =
doc.RootElement.EnumerateArray()
|> Seq.map (fun element ->
{
Conclusion = element.GetProperty("conclusion").GetString()
DatabaseId = element.GetProperty("databaseId").GetInt64()
HeadSha = element.GetProperty("headSha").GetString()
}
)
|> Seq.toList
Ok runs
with ex ->
Error $"Failed to parse JSON: {ex.Message}"
// ✅ Pattern: Handle nullable JSON properties
let getConclusion (element: JsonElement) : string =
let conclusionProp = element.GetProperty("conclusion")
if conclusionProp.ValueKind = JsonValueKind.Null then
"in_progress"
else
conclusionProp.GetString()
```
### Source Generators for AOT Compatibility
For Native AOT compilation, use source-generated serialization contexts:
```fsharp
// ✅ Good: Source-generated context for AOT
[<JsonSourceGenerationOptions(WriteIndented = true, PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)>]
[<JsonSerializable(typeof<ScriptResult>)>]
[<JsonSerializable(typeof<Config>)>]
type MorphirJsonContext =
inherit JsonSerializerContext
// Usage with AOT
let json = JsonSerializer.Serialize(result, MorphirJsonContext.Default.ScriptResult)
```
### Summary: JSON Serialization Checklist
- ✅ Use `FSharp.SystemTextJson` for F# types (records, unions, options)
- ✅ Configure `JsonSerializerOptions` once and reuse
- ✅ Use active patterns for reading `JsonElement` dynamically
- ✅ Handle `JsonValueKind.Null` explicitly
- ✅ Test `--json` output with `jq` to ensure valid JSON
- ✅ Separate logs (stderr) from JSON output (stdout)
- ✅ Use source generators for Native AOT scenarios
- ✅ Prefer `option` for F# code, nullable types for C# interop boundaries
---
## CLI Scripts (.fsx)
### Script Structure
Follow this structure for all `.fsx` scripts:
```fsharp
#!/usr/bin/env dotnet fsi
// Brief description
// Usage: dotnet fsi script.fsx [args]
#r "nuget: PackageName, Version"
open System
// ============================================================================
// Types
// ============================================================================
type ScriptArgs = { ... }
type ScriptResult = { ... }
// ============================================================================
// Utilities
// ============================================================================
let logInfo msg = eprintfn "[INFO] %s" msg
// ============================================================================
// Main Logic
// ============================================================================
let mainAsync (args: ScriptArgs) (ct: CancellationToken) : Async<ScriptResult> =
async { ... }
// ============================================================================
// CLI Parsing and Entry Point
// ============================================================================
let main (args: string array) =
// Parse args, run async logic, return exit code
...
exit (main fsi.CommandLineArgs.[1..])
```
### CLI Logging Standards
**CRITICAL**: Separate stdout and stderr properly:
```fsharp
let jsonOutput = args |> Array.contains "--json"
// Logs always go to stderr
let logInfo msg =
if not jsonOutput then
eprintfn "[INFO] %s" msg
let logError msg =
eprintfn "[ERROR] %s" msg
// Results go to stdout
let outputResult result =
if jsonOutput then
let json = JsonSerializer.Serialize(result)
printfn "%s" json // stdout only
else
printfn "=== Results ===" // stdout
// Human-readable output
```
### Argument Parsing with Argu
Use Argu for type-safe argument parsing:
```fsharp
#r "nuget: Argu, 6.2.4"
type Arguments =
| [<Mandatory>] Version of string
| [<AltCommandLine("-v")>] Verbose
| Json
| [<AltCommandLine("-t")>] Timeout of int
interface IArgParserTemplate with
member s.Usage =
match s with
| Version _ -> "Version to process"
| Verbose -> "Enable verbose output"
| Json -> "Output as JSON"
| Timeout _ -> "Timeout in minutes"
let parser = ArgumentParser.Create<Arguments>(programName = "script.fsx")
let results = parser.Parse(args)
let version = results.GetResult Version
let timeout = results.GetResult(Timeout, defaultValue = 30)
let verbose = results.Contains Verbose
```
---
## Testing
### Unit Tests (TUnit)
```fsharp
open TUnit.Core
[<Test>]
let ``parseChangelog returns correct count`` () =
// Arrange
let changelog = """
## [Unreleased]
- Added: Feature A
- Fixed: Bug B
"""
// Act
let result = parseChangelog changelog
// Assert
result.ChangeCount |> Assert.Equal 2
result.Added |> Assert.Equal 1
result.FixedCount |> Assert.Equal 1
```
### Property-Based Testing
```fsharp
open FsCheck
[<Property>]
let ``version parsing is reversible`` (version: string) =
let parsed = parseVersion version
match parsed with
| Ok v -> formatVersion v = version
| Error _ -> true // Invalid versions are acceptable to reject
```
---
## Summary
Key F# principles for morphir-dotnet:
1. ✅ **Use active patterns** instead of complex if-then chains
2. ✅ **Make illegal states unrepresentable** with discriminated unions
3. ✅ **Prefer immutability** - use records and immutable collections
4. ✅ **Use Result<'T, 'Error>** for operations that can fail
5. ✅ **Support cancellation** in async workflows
6. ✅ **Separate stdout/stderr** in CLI scripts
7. ✅ **Use Argu** for CLI argument parsing
8. ✅ **Write exhaustive pattern matches** - handle all cases
9. ✅ **Prefer Option<'T>** over null in F# code
10. ✅ **Use nullable reference types (F# 9)** for C# interop boundaries
11. ✅ **Use FSharp.SystemTextJson** for JSON serialization with F# types
12. ✅ **Follow railway-oriented programming** for error handling
13. ✅ **Use CustomOperations** for query-style DSL syntax
14. ✅ **Use Yield/Delay/Run** for compositional, nested DSLs
15. ✅ **Combine both patterns** for flexible hybrid DSLs
16. ✅ **Avoid F# keyword conflicts** - Use Pascal case in CEs (Bool, String, Int)
17. ✅ **Consider InlineIfLambda** for high-performance CEs (F# 6+)
---
## References
- [F# Style Guide](https://docs.microsoft.com/en-us/dotnet/fsharp/style-guide/)
- [F# Design Guidelines](https://docs.microsoft.com/en-us/dotnet/fsharp/style-guide/conventions)
- [F# 9 Nullable Reference Types](https://learn.microsoft.com/en-us/dotnet/fsharp/whats-new/fsharp-9#nullable-reference-types)
- [FSharp.SystemTextJson](https://github.com/Tarmil/FSharp.SystemTextJson) - F# support for System.Text.Json
- [System.Text.Json Documentation](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/overview)
- [Domain Modeling Made Functional](https://fsharpforfunandprofit.com/books/)
- [Railway Oriented Programming](https://fsharpforfunandprofit.com/rop/)
- [Serialization Guide](./serialization-guide.md) - Comprehensive serialization patterns (cross-language)
- [AGENTS.md](../../AGENTS.md) - Project-wide agent guidance
```
### ../../../AGENTS.md
```markdown
# AGENTS.md
Guidance for AI coding agents contributing to finos/morphir-dotnet (a .NET
binding for the Morphir ecosystem). Produce correct, minimal, and well‑tested
changes aligned with Morphir IR and tooling.
## Quick Navigation
- **This file (AGENTS.md)**: Primary guidance for all AI agents
- **Specialized Topics**: See [.agents/](./.agents/) directory for domain-specific guides
- [Skills Reference](./.agents/skills-reference.md) - QA Tester, AOT Guru, Release Manager, Technical Writer
- [Capabilities Matrix](./.agents/capabilities-matrix.md) - Cross-agent feature availability
- [QA Testing](./.agents/qa-testing.md) - Test plans, playbooks, scripts
- [AOT Optimization](./.agents/aot-optimization.md) - Trimming, AOT guidance
- **Agent-Specific Instructions**:
- **Claude Code**: [CLAUDE.md](./CLAUDE.md) + [.claude/skills/](./.claude/skills/)
- **GitHub Copilot**: [.github/copilot-instructions.md](./.github/copilot-instructions.md)
- **Cursor**: [.cursorrules](./.cursorrules)
- **Windsurf**: [.windsurf/rules.md](./.windsurf/rules.md) (if present)
- **Aider**: [.aider.conf.yml](./.aider.conf.yml) (if present)
- **Documentation**: See [docs/](./docs/) for user-facing docs
## Project Links
- Morphir: https://morphir.finos.org/
- morphir (core/tooling): https://github.com/finos/morphir
- morphir-elm: https://github.com/finos/morphir-elm
- morphir-dotnet (this repo): https://github.com/finos/morphir-dotnet
## 1) Project Overview
Purpose
- Provide .NET bindings, libraries, codecs, and tooling interoperable with the
Morphir IR (intermediate representation) and developer workflows.
Architecture (high-level)
- domain/: Pure Morphir domain/IR models, types, values, invariants.
- adapters/: Edges (serialization, CLI integration, file I/O, codegen).
- app/: Composition root, configuration, hosting/CLI entry points.
- docs/spec/: IR specification and schemas (JSON/OpenAPI schemas and samples).
- tests/: Unit (TUnit), snapshot (Verify), property-based (via generators), and BDD/acceptance (Reqnroll).
Stacks and baselines
- C# 14, .NET 10 (SDK pinned in global.json).
- F# for ADT-heavy components where appropriate.
- TypeScript optional for dev tooling/schema checks.
- Critter Stack (WolverineFx & Marten) used in development for messaging and persistence.
Principles
- Immutability-first; push effects to edges.
- ADTs: make illegal states unrepresentable; avoid nulls.
- Strong testing: TUnit for units, Verify for snapshots, Reqnroll for behavior, contract/roundtrip tests.
## 2) Agent Scope
Do
- Implement small features end-to-end (domain → adapter → tests).
- Fix bugs with minimal diffs and add regression tests (TUnit, Verify, and/or Reqnroll).
- Improve domain types to encode Morphir invariants.
- Keep docs and scripts consistent with code.
Avoid or escalate
- Breaking public API changes (coordinate with maintainers).
- IR/JSON compatibility changes without ADR and version bump.
- Security/auth/crypto changes.
- Destructive migrations without explicit approval.
## 3) Repository Map
Adjust to actual repo layout.
- src/
- Morphir: C# CLI/host (.csproj, C# 14)
- Morphir.Core: Core domain model and IR definition (.csproj, C# 14)
- docs/spec/: IR specification and JSON/OpenAPI schemas, IR samples/fixtures
- tests/**.csproj: TUnit, Reqnroll, contract tests
- scripts/: format/test/contract/codegen utilities
- docs/: ADRs, architecture, contribution notes
- .github/: CI workflows, CODEOWNERS, PR templates
External touchpoints
- Morphir CLI and JSON IR formats
- morphir-elm and morphir repos for canonical IR samples
## 4) Build, Run, Test
Environment
- Required env vars documented in .env.example. Do not commit secrets.
First-time Setup
- Restore tools: `dotnet tool restore`
- Restore dependencies: `dotnet restore` or `./build.sh --target Restore`
- Install git hooks: `dotnet husky install`
Commands
- .NET (C# 14 / F#, .NET 10)
- Build: `dotnet build` or `./build.sh`
- Test (TUnit + Reqnroll): `dotnet test --nologo` or `./build.sh --target Test`
- Format: `dotnet format` or `./build.sh --target Format`
- TypeScript (if present)
- Install: `npm ci`
- Build: `npm run build`
- Test: `npm test`
- Lint/Format: `npm run lint && npm run format`
CI
- All formatters/linters must pass.
- Tests must be green; coverage must not decrease (>= 80% unless stated).
- Snapshot/golden updates require justification in PRs.
## 5) Coding Conventions
General
- Domain is pure; side effects in adapters.
- Prefer Option/Result/Either-like models; avoid nulls/throws for expected flows.
- Exhaustive pattern matching for ADTs; avoid default fall-throughs.
- Use value types for IDs/quantities over raw primitives.
Error handling
- Typed domain errors (sum types). Map boundary errors at edges and log once.
Naming
- Use Morphir terms consistently (Package, Module, Type, Value, IR nodes).
Formatting
- `.editorconfig` + `dotnet format`. Prettier/ESLint for TS (if present).
C# 14 / .NET 10 specifics
- Prefer readonly `record struct`/`record` where appropriate.
- Use file‑scoped namespaces, primary constructors, and newer pattern features.
- Favor spans and efficient collections only with benchmarks backing changes.
F# specifics
- See [F# Coding Guide](./docs/contributing/fsharp-coding-guide.md) for comprehensive F# standards
- **Computation Expressions for DSLs**: Use CustomOperations for query-style syntax, Yield/Delay/Run for compositional nesting, or combine both for flexible hybrid DSLs (see [CE Guide](./docs/contributing/fsharp-coding-guide.md#computation-expressions-and-dsls))
- **Prefer active patterns** over complex if-then chains for value extraction
- Use discriminated unions to make illegal states unrepresentable
- Follow CLI logging standards (stdout for data, stderr for diagnostics)
- Use Result types for railway-oriented programming
- Maintain immutability with records and immutable collections
Native AOT, Trimming, and Size Optimization
- See [AOT/Trimming Guide](./docs/contributing/aot-trimming-guide.md) for comprehensive AOT/trimming guidance
- Use source generators for JSON serialization (not reflection)
- Design for trimming from the start (avoid reflection and dynamic code)
- Test with `PublishAot=true` and `PublishTrimmed=true`
- Target sizes: 5-8 MB (minimal), 8-12 MB (feature-rich), 10-15 MB (with UI)
- See [Issue #221](https://github.com/finos/morphir-dotnet/issues/221) for implementation tracking
CLI Tool Logging Requirements
- **CRITICAL**: CLI tools MUST NOT write log messages to stdout
- All logging output MUST be directed to stderr only
- Stdout is reserved exclusively for command output (JSON, formatted results, etc.)
- Use Serilog configured with `standardErrorFromLevel: LogEventLevel.Verbose`
- Clear default logging providers before configuring Serilog to avoid double logging
- Test all commands with `--json` flag to ensure stdout contains only valid JSON
Rationale: CLI tools that write to stdout break scriptability and JSON parsing.
Users expect to pipe command output (e.g., `morphir ir verify file.json --json | jq`)
without log noise contaminating the structured output. Following Unix philosophy:
stdout = data, stderr = diagnostics.
## 6) Morphir-Specific Modeling
Model Morphir IR precisely:
- Names: validated segments, non-empty paths, canonical casing rules.
- Types: aliases, custom (union) types, records, tuples, functions.
- Values/Expr: literals, lambdas, application, pattern matching.
- Keep IR fidelity; avoid lossy representations.
- **DSL Builders**: Use F# Computation Expressions for clean IR construction syntax (see [Morphir IR DSL](./src/Morphir.Models/IR/Classic/DSL/) and [CE Guide](./docs/contributing/fsharp-coding-guide.md#computation-expressions-and-dsls))
F# example (type definitions)
```fsharp
type NameSegment = private NameSegment of string
module NameSegment =
let tryCreate s =
if System.Text.RegularExpressions.Regex.IsMatch(s, "^[a-z][a-z0-9_]*$")
then Ok (NameSegment s) else Error "InvalidNameSegment"
type QualifiedName = { Package: string list; Module: string list; Local: string }
type TypeExpr =
| TInt
| TString
| TBool
| TTuple of TypeExpr list
| TRecord of Map<string, TypeExpr>
| TFunc of input: TypeExpr * output: TypeExpr
```
F# example (DSL usage with Computation Expressions)
```fsharp
// Clean syntax for building IR constructs using CE DSL
let personType =
type' {
record [
field "name" stringType
field "age" intType
]
}
let morphirBasicsInt =
fqName {
package ["morphir"; "sdk"]
module' ["basics"]
local "int"
}
let boolLiteral = literal { Bool true }
let stringLiteral = literal { String "hello" }
```
C# example
```csharp
public abstract record TypeExpr {
public sealed record TInt() : TypeExpr;
public sealed record TString() : TypeExpr;
public sealed record TBool() : TypeExpr;
public sealed record TTuple(IReadOnlyList<TypeExpr> Items) : TypeExpr;
public sealed record TRecord(IReadOnlyDictionary<string, TypeExpr> Fields) : TypeExpr;
public sealed record TFunc(TypeExpr Input, TypeExpr Output) : TypeExpr;
}
```
TypeScript (tooling)
```ts
export type TypeExpr =
| { _tag: "TInt" }
| { _tag: "TString" }
| { _tag: "TBool" }
| { _tag: "TTuple"; items: ReadonlyArray<TypeExpr> }
| { _tag: "TRecord"; fields: Readonly<Record<string, TypeExpr>> }
| { _tag: "TFunc"; input: TypeExpr; output: TypeExpr };
```
## 7) Interfaces and Contracts
- IR JSON compatibility with Morphir toolchains is mandatory.
- Roundtrip codec tests: serialize → deserialize → equals.
- Backward compatibility
- Additive OK.
- Breaking changes require ADR, migration notes, and version bump.
- If OpenAPI/JSON Schemas exist, keep in `docs/spec/schemas/` and validate in CI.
## 8) Tooling and Scripts for Agents
Scripts to provide/use:
- scripts/format-all — format .NET (and TS if present)
- scripts/test-all — run TUnit and Reqnroll suites (and TS if present)
- scripts/check-contracts — run IR/JSON roundtrip and contract tests
- scripts/gen — codegen or schema sync steps (if any)
Run before committing:
```bash
scripts/format-all
scripts/test-all
scripts/check-contracts
```
Suggested implementations
- scripts/format-all
```bash
#!/usr/bin/env bash
set -euo pipefail
dotnet format
if [ -f "package.json" ]; then npm run format; fi
```
- scripts/test-all
```bash
#!/usr/bin/env bash
set -euo pipefail
dotnet test --nologo
if [ -f "package.json" ]; then npm test; fi
```
- scripts/check-contracts
```bash
#!/usr/bin/env bash
set -euo pipefail
# Implement IR/JSON codec roundtrip tests and any schema checks.
# Example (placeholder):
# dotnet test --filter "Category=Contract"
echo "Run IR JSON roundtrip + contract tests against Morphir samples."
```
Make scripts executable:
```bash
chmod +x scripts/format-all scripts/test-all scripts/check-contracts scripts/gen || true
```
## 9) Testing Strategy
Unit testing (TUnit)
- Place unit tests in tests/csharp.unit or equivalent.
- Cover smart constructors, ADT exhaustiveness, and edge cases.
Snapshot testing (Verify)
- **Use [Verify](https://github.com/VerifyTests/Verify) for snapshot/verification testing**
- Simplifies assertion of complex data models, JSON, documents, and generated code
- C# projects: Use `Verify.TUnit` package
- F# projects: Use `Verify.Expecto` package
- Verify serializes test results and compares against stored snapshots
- Snapshots are stored in `.verified.` files alongside test files
- When snapshots change, tests fail with clear diffs; update snapshots after reviewing changes
- Ideal for: JSON serialization, code generation output, complex object graphs, API responses
- Example:
```csharp
await Verify(result); // Serializes and compares against snapshot
```
- Example (F#):
```fsharp
test "Generated code matches snapshot" {
let generated = generateCode input
do! Verify(generated) // Uses Verify.Expecto
}
```
Behavior/acceptance (Reqnroll)
- Feature files under tests/csharp.bdd/Features/*.feature
- Step definitions in tests/csharp.bdd/Steps/
- Use Reqnroll hooks for setup/teardown and environment orchestration.
Property-based tests
- Use FsCheck or generators within TUnit where appropriate:
- Name normalization/validation
- Codec roundtrips for random IR fragments
- Structural invariants (non-empty lists where required, arity constraints)
Contract tests
- Roundtrip JSON using canonical samples from morphir-elm/morphir.
- Cross-validate against Morphir CLI when feasible.
- Consider using Verify for snapshot testing of serialized IR structures
Coverage targets
- Maintain or improve coverage (>= 80% overall unless specified).
## 9.1) Test-Driven Development (TDD) - Red, Green, Refactor
**CRITICAL**: This project follows strict Test-Driven Development practices. When implementing new features or fixing bugs, you MUST follow the Red-Green-Refactor cycle:
### Red-Green-Refactor Cycle
1. **RED**: Write a failing test first
- Write the test that describes the desired behavior
- Run the test to confirm it fails (red)
- This ensures the test is actually testing something
2. **GREEN**: Write minimal code to make the test pass
- Implement only what's needed to make the test green
- Don't worry about perfection yet
- Focus on making it work
3. **REFACTOR**: Improve the code while keeping tests green
- Clean up the implementation
- Remove duplication
- Improve naming and structure
- Run tests after each refactoring step
### TDD Workflow for Agents
When implementing features:
```bash
# 1. RED: Write failing unit tests
# Example: tests/Morphir.Tooling.Tests/Features/VerifyIR/VerifyIRHandlerTests.cs
[Test]
public async Task Handle_ShouldReturnValid_WhenIRIsValid()
{
// Arrange
var command = new VerifyIR("valid-ir.json");
// Act
var result = await VerifyIRHandler.Handle(command, validator, ct);
// Assert
result.IsValid.Should().BeTrue();
}
# Run: dotnet test
# Expect: Test fails because VerifyIRHandler doesn't exist
# 2. GREEN: Implement minimal code
public static class VerifyIRHandler
{
public static Task<VerifyIRResult> Handle(VerifyIR command, ...)
{
// Minimal implementation to pass test
return Task.FromResult(new VerifyIRResult(IsValid: true, ...));
}
}
# Run: dotnet test
# Expect: Test passes (green)
# 3. REFACTOR: Improve implementation
public static class VerifyIRHandler
{
public static async Task<VerifyIRResult> Handle(
VerifyIR command,
SchemaValidator validator,
CancellationToken ct)
{
var jsonContent = await File.ReadAllTextAsync(command.FilePath, ct);
var validationResult = await validator.ValidateAsync(jsonContent, "3", ct);
return new VerifyIRResult(
IsValid: validationResult.IsValid,
SchemaVersion: "3",
DetectionMethod: "auto",
FilePath: command.FilePath,
Errors: validationResult.Errors,
Timestamp: DateTime.UtcNow
);
}
}
# Run: dotnet test
# Expect: Tests still pass (still green)
```
### BDD-First for Features
For new features, write BDD scenarios BEFORE implementation:
```gherkin
# tests/Morphir.Tooling.Tests/Features/VerifyIR/VerifyIR.feature
Feature: IR Schema Verification
As a Morphir developer
I want to validate IR JSON files against schemas
So that I can ensure IR correctness
Scenario: Valid IR file passes validation
Given a valid IR v3 file "valid-ir-v3.json"
When I verify the IR file
Then the validation should succeed
And no errors should be reported
```
Then implement step definitions:
```csharp
[Given(@"a valid IR v3 file ""(.*)""")]
public void GivenAValidIRV3File(string fileName)
{
_context.FilePath = Path.Combine("TestData", fileName);
}
[When(@"I verify the IR file")]
public async Task WhenIVerifyTheIRFile()
{
var command = new VerifyIR(_context.FilePath);
_context.Result = await _handler.Handle(command, _validator, CancellationToken.None);
}
[Then(@"the validation should succeed")]
public void ThenTheValidationShouldSucceed()
{
_context.Result.IsValid.Should().BeTrue();
}
```
### TDD Rules for Agents
1. **Never write production code without a failing test first**
- Exception: Simple refactorings that don't change behavior
2. **Write the simplest test that could possibly fail**
- Start with happy path
- Add edge cases incrementally
3. **Write only enough production code to make the failing test pass**
- Don't anticipate future requirements
- Keep it simple
4. **Refactor continuously**
- After each green test, look for improvements
- Keep tests green throughout refactoring
5. **Test behaviors, not implementation details**
- Focus on public APIs and observable outcomes
- Avoid testing private methods directly
6. **One test, one assertion (when practical)**
- Makes failures easier to diagnose
- Tests are more focused
### Test Organization
```
tests/Morphir.Tooling.Tests/
├── Features/
│ └── VerifyIR/
│ ├── VerifyIR.feature # BDD scenarios
│ ├── VerifyIRSteps.cs # Step definitions
│ └── VerifyIRHandlerTests.cs # Unit tests
├── Infrastructure/
│ └── JsonSchema/
│ ├── SchemaLoaderTests.cs # Unit tests
│ └── SchemaValidatorTests.cs # Unit tests
└── TestData/
├── valid-ir-v3.json
└── invalid-*.json
```
### TDD Anti-Patterns to Avoid
❌ **Don't write tests after the code**
- Defeats the purpose of TDD
- Tests become implementation-focused instead of behavior-focused
❌ **Don't skip the refactor step**
- Code quality degrades over time
- Technical debt accumulates
❌ **Don't write too many tests before implementation**
- Write one test, implement, refactor, repeat
- Maintains focus and momentum
❌ **Don't test implementation details**
- Focus on behavior and contracts
- Private methods are tested through public APIs
### Verification Before Commit
Always run the full test suite before committing:
```bash
# Run all tests
dotnet test --nologo
# Verify coverage hasn't decreased
dotnet test --collect:"XPlat Code Coverage"
# Format code
dotnet format
```
## 10) Decision Policies
- Favor IR fidelity and correctness.
- Minimize dependencies; justify new packages.
- Performance changes require benchmarks and tests.
- Keep effects at the edges; domain remains pure.
- Prefer explicit ADTs over booleans/flags; update exhaustive matches on change.
### User Interaction Policies
**CRITICAL - Always Require User Confirmation for Destructive Actions:**
- **Auto-merging PRs**: NEVER auto-merge PRs without explicit user confirmation. Always prompt: "Do you want to auto-merge this PR when all checks pass?"
- **Deleting branches**: Always confirm before deleting remote or local branches
- **Force pushing**: Always confirm before force-pushing to any branch
- **Publishing releases**: Always confirm before triggering deployment workflows
- **Modifying production**: Always confirm before making changes that affect production systems
**Guiding Principle**: When in doubt, ask. It's better to prompt for confirmation than to perform an unwanted action.
## 11) Review and Contribution Rules
- Small, focused PRs with tests (TUnit and/or Reqnroll).
- Conventional Commits: feat:, fix:, refactor:, test:, docs:
- **CRITICAL**: **Do NOT include `Co-Authored-By: Claude` or any AI assistant as co-author on commits.** Our CLA does NOT support AI assistants as co-authors. Violations will block PR merges. See [Commit Messages section](#commit-messages) for details and remediation steps. Note: GitHub Copilot may be listed as co-author when it is an actual co-author.
- **If you accidentally included Claude as a co-author**, use the [remove-claude-coauthor.fsx](scripts/remove-claude-coauthor.fsx) script to fix it:
```bash
# Preview changes
dotnet fsi scripts/remove-claude-coauthor.fsx --commits 5
# Apply fixes (creates backup automatically)
dotnet fsi scripts/remove-claude-coauthor.fsx --commits 5 --yes
```
- See [scripts/README.md](scripts/README.md) for complete usage and options.
- PR checklist:
- [ ] Tests added/updated and passing
- [ ] IR/JSON compatibility preserved or versioned with ADR
- [ ] Formatters/lints run
- [ ] Docs/ADR updated if behavior changed
- [ ] **No AI assistants listed as co-authors** (CLA compliance)
## 12) Security and Compliance
- No secrets in code or tests.
- Respect FINOS policies and repository license.
- Auth/crypto/legal changes require maintainer review.
## 13) PRD Management and Implementation Tracking
Product Requirements Documents (PRDs)
- Location: `docs/content/contributing/design/prds/`
- Each feature starts with a comprehensive PRD before implementation
- PRDs are living documents updated during implementation
PRD Structure
- **Status tracking**: PRDs include a "Feature Status Tracking" table with all features and their implementation status
- **Implementation notes**: Add "Implementation Notes" sections to capture:
- Design decisions made during implementation
- Deviations from original design with rationale
- Architectural insights discovered during development
- Dependencies or blockers encountered
- **Open questions**: Document decisions as they're made in the "Open Questions" section
PRD Status Workflow
1. **Draft**: Initial PRD being refined
2. **Approved**: PRD reviewed and ready for implementation
3. **In Progress**: Active implementation underway
4. **Completed**: All features implemented, PRD archived
5. **Deferred**: PRD postponed, marked with reason
Cross-Agent Collaboration
- When starting work, check the PRD Feature Status Tracking table for current task
- Update feature status in real-time: ⏳ Planned → 🚧 In Progress → ✅ Implemented
- Add implementation notes directly in the PRD under relevant sections
- PRD serves as source of truth for "what's next" across multiple AI agent sessions
Example Implementation Notes Section
```markdown
## Implementation Notes
### Phase 1: Core Verification (Current)
#### VerifyIR Command Handler (2025-12-15)
- **Decision**: Used WolverineFx's `IMessageBus.InvokeAsync<T>()` instead of `IMessageContext.Send()`
- **Rationale**: InvokeAsync provides request-response pattern needed for CLI
- **Impact**: Simpler than setting up reply queues
- **Files**: `src/Morphir/Program.cs:397-401`, `src/Morphir.Tooling/Features/VerifyIR/VerifyIR.cs`
#### Schema Loading (2025-12-15)
- **Change**: Used `Assembly.GetManifestResourceStream()` instead of `EmbeddedFileProvider`
- **Rationale**: Simpler, no additional dependencies
- **Impact**: None, works as designed
- **Files**: `src/Morphir.Tooling/Infrastructure/JsonSchema/SchemaLoader.cs:15-25`
```
PRD Index (Markdown)
- Maintain `docs/content/contributing/design/prds/_index.md` with all PRDs and their status
- Format:
```markdown
| PRD | Status | Phase | Current Task |
|-----|--------|-------|--------------|
| [IR Schema Verification](./ir-json-schema-verification.md) | In Progress | Phase 1 | WolverineFx setup |
| [Migration Tooling](./ir-migration.md) | Draft | - | Design review |
```
## 14) Phase 1 Implementation Patterns (IR Schema Verification)
This section documents architectural patterns and conventions established during Phase 1 implementation that should be followed in future phases.
### Vertical Slice Architecture with WolverineFx
**Structure**: Features organized by use case in `src/Morphir.Tooling/Features/{FeatureName}/`
Example: `Features/VerifyIR/`
```
Features/VerifyIR/
├── VerifyIR.cs # Command, Result, Handler, Validator (all in one file)
├── VersionDetector.cs # Feature-specific logic
└── VerifyIR.feature # BDD scenarios (in test project)
```
**Command Pattern**:
```csharp
// Command (immutable record)
public record VerifyIR(
string FilePath,
int? SchemaVersion = null,
bool JsonOutput = false,
bool Quiet = false
);
// Result (immutable record)
public record VerifyIRResult(
bool IsValid,
string SchemaVersion,
string DetectionMethod,
string FilePath,
List<ValidationError> Errors,
DateTime Timestamp
);
// Handler (static class with pure function)
public static class VerifyIRHandler
{
public static async Task<VerifyIRResult> Handle(
VerifyIR command,
SchemaValidator validator, // Injected dependency
CancellationToken ct)
{
// Pure logic, returns result
}
}
// FluentValidation rules
public class VerifyIRValidator : AbstractValidator<VerifyIR>
{
public VerifyIRValidator()
{
RuleFor(x => x.FilePath)
.NotEmpty()
.Must(File.Exists).WithMessage("File does not exist: {PropertyValue}");
}
}
```
### CLI Integration with System.CommandLine
**Pattern**: CLI in `src/Morphir/Program.cs` invokes WolverineFx message bus
```csharp
// Create command with options
var verifyCommand = new Command("verify", "Verify Morphir IR JSON");
var filePathArgument = new Argument<FileInfo>("file-path");
var schemaVersionOption = new Option<int?>("--schema-version");
verifyCommand.Arguments.Add(filePathArgument);
verifyCommand.Options.Add(schemaVersionOption);
// Set action handler
verifyCommand.SetAction(async parseResult =>
{
// Create WolverineFx host
using var host = Tooling.Program.CreateToolingHost();
await host.StartAsync();
var messageBus = host.Services.GetRequiredService<IMessageBus>();
// Create command
var command = new Tooling.Features.VerifyIR.VerifyIR(
FilePath: filePath.FullName,
SchemaVersion: schemaVersion,
JsonOutput: jsonOutput,
Quiet: quiet
);
// Execute via message bus
var result = await messageBus.InvokeAsync<VerifyIRResult>(command);
// Format output
FormatOutput(result, jsonOutput, quiet);
return result.IsValid ? 0 : 1;
});
```
### Infrastructure Services
**Location**: `src/Morphir.Tooling/Infrastructure/{ServiceType}/`
**Pattern**: Services registered in WolverineFx host, injected into handlers
```csharp
// In Morphir.Tooling/Program.cs
builder.Services.AddWolverine(opts =>
{
// Register infrastructure services
opts.Services.AddSingleton<SchemaLoader>();
opts.Services.AddSingleton<SchemaValidator>();
// Auto-discover handlers
opts.Discovery.IncludeAssembly(typeof(Program).Assembly);
});
```
### Testing Layers
**1. Unit Tests** (`tests/Morphir.Tooling.Tests/{Component}/`)
- Test individual classes/functions in isolation
- Use TUnit framework
- Use FluentAssertions for readable assertions
- Test file naming: `{ClassName}Tests.cs`
**2. BDD Feature Tests** (`tests/Morphir.Tooling.Tests/Features/{Feature}/`)
- Gherkin feature files alongside step definitions
- Test business logic through handler
- File naming: `{Feature}.feature` and `{Feature}Steps.cs`
**3. Integration Tests** (`tests/Morphir.Tooling.Tests/Integration/`)
- Test end-to-end CLI execution
- Actually spawn CLI as subprocess
- Test all output formats and error scenarios
- File naming: `{Feature}Integration.feature` and `{Feature}IntegrationSteps.cs`
### Test Data Organization
```
tests/Morphir.Tooling.Tests/
├── TestData/ # Shared test data
│ ├── valid-ir-v1.json
│ ├── valid-ir-v2.json
│ ├── valid-ir-v3.json
│ ├── invalid-*.json
│ └── malformed.json
└── Integration/
└── CLI/
├── CliTestHelper.cs # Reusable CLI execution helper
├── {Feature}.feature
└── {Feature}Steps.cs
```
### Error Handling Pattern
**Domain Errors**: Use Result types in handlers, return structured errors
```csharp
try
{
// Business logic
return new VerifyIRResult(IsValid: true, ...);
}
catch (JsonException ex)
{
// Handle expected errors, return structured result
return new VerifyIRResult(
IsValid: false,
Errors: [new ValidationError(
Path: "$",
Message: $"Malformed JSON: {ex.Message}",
Expected: "Valid JSON",
Found: "Invalid JSON syntax"
)],
...
);
}
```
**CLI Errors**: Map to exit codes
- 0: Success
- 1: Validation failure (expected/business error)
- 2: Operational error (file not found, etc.)
### JSON Serialization for AOT
**Pattern**: Use source-generated serialization context
```csharp
[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(VerifyIRResult))]
internal partial class MorphirJsonContext : JsonSerializerContext
{
}
// Usage
var json = JsonSerializer.Serialize(result, MorphirJsonContext.Default.VerifyIRResult);
```
### Documentation Requirements
**Each feature must include**:
1. CLI command reference in `docs/content/docs/cli/{command}.md`
2. Getting started guide in `docs/content/docs/getting-started/`
3. Troubleshooting section in `docs/content/docs/cli/troubleshooting.md`
4. Examples with all output formats
5. CI/CD integration examples
**Documentation structure**:
```markdown
# Command Reference
- Synopsis
- Description
- Arguments
- Options (with examples)
- Exit codes
- Output formats
- Examples (basic → advanced)
- Common errors
- Troubleshooting
- Related commands
```
### Test Coverage Requirements
- **Unit tests**: >90% code coverage
- **BDD tests**: All user stories covered
- **Integration tests**: End-to-end CLI scenarios
- **All tests must pass before PR**
### Commit Messages
Follow Conventional Commits format:
```
feat: add comprehensive BDD integration tests for CLI
- Created CLI integration test infrastructure
- Added 13 BDD scenarios covering all features
- Fixed JSON exception handling
- All 62 tests passing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
```
**CRITICAL - CLA COMPLIANCE**:
⚠️ **DO NOT include `Co-Authored-By: Claude` or any AI assistant as a co-author in commits.**
Our Contributor License Agreement (CLA) does NOT support AI assistants as co-authors. Including AI as co-author violates CLA requirements and will block PR merges.
**Allowed**:
- ✅ Attribution in commit body: `🤖 Generated with [Claude Code](https://claude.com/claude-code)`
- ✅ GitHub Copilot as co-author (when it is an actual co-author)
- ✅ Human co-authors with signed CLAs
**NOT Allowed**:
- ❌ `Co-Authored-By: Claude <[email protected]>`
- ❌ Any AI assistant listed as co-author
**If you accidentally added Claude as co-author:**
1. **Amend the last commit** (if not pushed):
```bash
git commit --amend
# Remove the Co-Authored-By line from the commit message
```
2. **Rewrite commit history** (if already pushed):
```bash
# Interactive rebase to edit commits
git rebase -i HEAD~N # where N is number of commits to review
# Mark commits with 'edit', then amend each one
git commit --amend # Remove Co-Authored-By line
git rebase --continue
git push --force-with-lease
```
3. **Use automated script** (recommended):
```bash
# Preview what will be changed
dotnet fsi scripts/remove-claude-coauthor.fsx --commits 5
# Apply the fix (creates backup automatically)
dotnet fsi scripts/remove-claude-coauthor.fsx --commits 5 --yes
```
See [scripts/README.md](scripts/README.md) for complete documentation.
**Why this matters:**
- CLA requires all contributors to be identifiable humans with signed agreements
- AI assistants cannot sign legal agreements
- GitHub uses Co-Authored-By for CLA verification
- Violations will fail CI checks and block merges
## 15) Known Issues / TODOs
- Maintain a prioritized list or link GitHub issues:
- TODO: <short> [link]
- BUG: <short> [link]
- COMPAT: <short> [link]
## 15) ADRs and Rationale
- docs/adr/*.md — key decisions (IR mapping, codec strategy, versioning).
- Include ADRs for breaking changes or cross-tool compatibility shifts.
## 16) Maintainers and Ownership
- CODEOWNERS defines required reviewers.
- Primary contacts: <handles/emails>
- Escalation: label `maintainer-attention` or use project channels.
## 17) Agent Execution Rules (Important)
- Keep diffs minimal; follow existing patterns and style.
- Update all exhaustive matches and affected tests when changing ADTs.
- Always run formatters, TUnit, and Reqnroll suites; run contract/roundtrip checks.
- If uncertain about Morphir compatibility, add a TODO with a question and take
the conservative path.
## 18) Specialized Guidance
This repository provides specialized, domain-specific guidance in the [.agents/](./.agents/) directory:
### Skills Reference
**NEW**: [Skills Reference](./.agents/skills-reference.md) - Comprehensive documentation of all expert skills (gurus):
- **QA Tester** - Test plan design, regression testing, coverage monitoring, issue reporting
- **AOT Guru** - Single-file trimmed executables, AOT readiness, trimming diagnostics, size optimization
- **Release Manager** - Release lifecycle, changelog management, version selection, workflow monitoring
- **Technical Writer** - Documentation, Hugo/Docsy mastery, Mermaid/PlantUML diagrams, style guide
- Each skill includes: scope, competencies, review capabilities, automation scripts, manual workflows
- Decision trees and pattern catalogs for common scenarios
- Cross-agent accessibility information
**NEW**: [Capabilities Matrix](./.agents/capabilities-matrix.md) - Cross-agent feature availability:
- Which skills work with which agents (Claude, Copilot, Cursor, Windsurf, Aider)
- How to invoke reviews in each agent
- Script portability notes and token usage comparisons
- Agent-specific workflows and troubleshooting
### Domain-Specific Guides
- **[QA Testing](./.agents/qa-testing.md)** - Comprehensive QA guidance
- Test plan templates
- Pre-commit and PR verification checklists
- Regression, feature, build, and package testing playbooks
- Bug report templates
- Test scripts (F#): smoke tests, regression tests, package validation
- BDD and unit testing guides
- Coverage requirements and best practices
- **[AOT Optimization](./.agents/aot-optimization.md)** - Native AOT, trimming, and size optimization
- Decision trees for AOT compatibility issues
- Diagnostic procedures and automated testing
- Common patterns and workarounds
- Size optimization strategies
- Known issues database and continuous improvement
- Integration with CI/CD pipelines
### How to Use Skills with Different AI Agents
morphir-dotnet provides specialized expert skills that work across all AI coding agents, though with different invocation methods.
**Quick Reference:**
| Agent | Invocation Method | Example |
|-------|------------------|---------|
| **Claude Code** | `@skill {skill-name}` | `@skill qa-tester` |
| **GitHub Copilot** | Natural language + skill name | "Use QA Tester skill to create test plan" |
| **Cursor** | `.cursorrules` auto-trigger or `@file` mention | `@.claude/skills/qa-tester/SKILL.md` |
| **Windsurf** | Natural language (auto-discovery) | "Use QA Tester to validate this PR" |
| **JetBrains AI** | Custom prompts or natural language | "Use QA Tester skill..." |
**Key Points:**
- **Claude Code**: Native `@skill` command with interactive assistance
- **Other Agents**: Documentation-based emulation via natural language or file references
- **All Agents**: Can run automation scripts directly: `dotnet fsi .claude/skills/{skill}/scripts/{script}.fsx`
- **Skill Aliases**: Some skills document short forms (e.g., "qa", "tester") but these are **NOT functional** - use official names only
**Detailed Platform-Specific Guidance:**
- See [.agents/skills-reference.md](./.agents/skills-reference.md#cross-platform-skill-invocation) for comprehensive invocation patterns for each platform
- See [capabilities-matrix.md](./.agents/capabilities-matrix.md) for feature comparison table
**Cross-Platform Testing:**
- Issue #266: GitHub Copilot skill emulation tests
- Issue #267: Cursor skill emulation tests
- Issue #268: Windsurf skill emulation tests
- Issue #269: JetBrains AI skill emulation tests
#### GitHub Copilot Usage Guide (Skill Emulation)
- **Discover skills**: Ask "What skills are available in this project?" → Copilot should read [.agents/skills-reference.md](./.agents/skills-reference.md) and list QA Tester, AOT Guru, Release Manager, Technical Writer with links.
- **Invoke guidance**: Use natural language, e.g., "Use the QA Tester skill to create a test plan for PR #123" → Copilot should read `.claude/skills/qa-tester/skill.md` and follow the "Test Plan Development" section.
- **Run scripts**: Provide exact commands, e.g., `dotnet fsi .claude/skills/qa-tester/scripts/smoke-test.fsx` and explain expected outcomes.
- **Follow playbooks**: Ask "Walk me through the regression testing playbook" → Copilot should enumerate steps with commands and validation criteria.
- **Aliases note**: `@skill` is Claude Code-only; aliases are documentation-only. In Copilot, reference full skill names and SKILL.md files directly.
See the dedicated Copilot test plan: [docs/content/contributing/qa/copilot-skill-emulation-test-plan.md](./docs/content/contributing/qa/copilot-skill-emulation-test-plan.md).
### Future Topics
The `.agents/` directory will expand to include:
- Documentation and ADR writing
- Security testing and compliance
- Performance testing and benchmarking
See [.agents/README.md](./.agents/README.md) for navigation and contribution guidelines.
## 19) Resources and References
### Primary Documentation
- This file (AGENTS.md) - Start here for all agents
- [.agents/](./.agents/) - Specialized topic guides
- [CLAUDE.md](./CLAUDE.md) - Claude Code-specific features
- [README.md](./README.md) - Project README for humans
### Testing Resources
- [Phase 1 Test Plan](./docs/content/contributing/qa/phase-1-test-plan.md) - Example comprehensive test plan
- [QA Testing Guide](./.agents/qa-testing.md) - Cross-agent QA practices
- [QA Skill](./.claude/skills/qa-tester/) - Claude Code QA automation
### AOT and Optimization Resources
- [AOT/Trimming Guide](./docs/contributing/aot-trimming-guide.md) - User-facing AOT documentation
- [AOT Optimization Guide](./.agents/aot-optimization.md) - Agent-specific AOT guidance
- [AOT Guru Skill](./.claude/skills/aot-guru/) - Claude Code AOT diagnostics and optimization
- [F# Coding Guide](./docs/contributing/fsharp-coding-guide.md) - Includes F# AOT patterns
### Morphir Resources
- Morphir Homepage: https://morphir.finos.org/
- morphir-elm: https://github.com/finos/morphir-elm
- morphir (core): https://github.com/finos/morphir
- IR Specification: [docs/spec/](./docs/spec/)
### Standards and Tools
- AGENTS.md Standard: https://agents.md
- Reqnroll (BDD): https://docs.reqnroll.net/
- TUnit (Testing): https://thomhurst.github.io/TUnit/
- Nuke (Build): https://nuke.build/
- WolverineFx: https://wolverine.netlify.app/
```
### templates/aot-issue-report.md
```markdown
# AOT Issue: [Brief Description]
## Metadata
- **Date**: YYYY-MM-DD
- **Category**: Reflection | Dynamic Code | Trimming | Size | Performance
- **Severity**: Critical | High | Medium | Low
- **Status**: Open | Workaround Available | Fixed | Won't Fix
- **Affects Version**: [e.g., .NET 10, morphir-dotnet 1.0.0]
## Symptoms
[Detailed description of the problem. What happens? When does it happen?]
## Error Messages
```
[Paste build warnings/errors, runtime exceptions, or relevant log output]
```
## Environment
- **OS**: [e.g., Ubuntu 22.04, Windows 11, macOS 14]
- **Runtime**: [e.g., linux-x64, win-x64, osx-arm64]
- **.NET SDK Version**: [e.g., 10.0.100]
- **Project Type**: [e.g., Console App, CLI Tool, Library]
## Steps to Reproduce
1. [First step]
2. [Second step]
3. [...]
**Minimal Reproduction** (if applicable):
```csharp
// Minimal code that reproduces the issue
```
## Root Cause
[Explain why this issue occurs. Technical details about what AOT/trimming is doing that causes the problem.]
### Analysis
- **What code pattern triggers this?** [e.g., JsonSerializer.Deserialize without source generators]
- **Why does it fail in AOT?** [e.g., Reflection.Emit not supported, types trimmed away]
- **Is this a known .NET limitation?** [Yes/No, with reference if known]
## Workaround
[Immediate solution that allows development to continue]
### Implementation
```csharp
// Code showing the workaround
```
### Limitations
- [What this workaround doesn't solve]
- [Any performance or functionality trade-offs]
## Proper Fix
[Long-term solution that properly addresses the root cause]
### Implementation
```csharp
// Code showing the proper fix
```
### Why This Is Better
- [Advantages over the workaround]
- [Long-term maintainability benefits]
## Impact Assessment
- **Build Impact**: [Does this block AOT compilation? Yes/No]
- **Runtime Impact**: [Does this cause runtime failures? Yes/No]
- **Size Impact**: [Does this significantly affect binary size? Yes/No, how much?]
- **Performance Impact**: [Any performance implications? Yes/No, details]
## Related Issues
- Related issue: #123
- Similar issue in .NET: [link to dotnet/runtime issue]
- Documentation: [link to relevant docs]
- Community discussion: [link to discussion]
## Testing
### Test Case
[Describe how to test that the issue is fixed]
```bash
# Commands to verify the fix
dotnet publish -c Release -r linux-x64 /p:PublishAot=true
./bin/Release/net10.0/linux-x64/publish/morphir --version
```
### Expected Behavior After Fix
[What should happen after the fix is applied]
## Documentation Updates
- [ ] Update AOT/Trimming Guide with this pattern
- [ ] Add to known issues database
- [ ] Update diagnostic scripts if applicable
- [ ] Add BDD test scenario
## References
- [Microsoft AOT Documentation](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/)
- [AOT/Trimming Guide](../../../docs/contributing/aot-trimming-guide.md)
- [AGENTS.md](../../../AGENTS.md)
---
**Notes**: [Any additional context, observations, or future considerations]
```
### templates/aot-workaround.md
```markdown
# Workaround: [Issue Description]
## Overview
[Brief description of what issue this workaround addresses]
**Related Issue**: [Link to aot-issue-report.md or GitHub issue]
## When to Use
This workaround applies when:
- [Condition 1]
- [Condition 2]
- [Condition 3]
**Do NOT use this workaround if:**
- [Negative condition 1]
- [Negative condition 2]
## Prerequisites
- [Required package version, e.g., System.Text.Json 9.0.0+]
- [Required .NET SDK version, e.g., .NET 10+]
- [Any other dependencies]
## Implementation
### Step 1: [First step title]
[Detailed explanation]
```csharp
// Code for step 1
```
### Step 2: [Second step title]
[Detailed explanation]
```csharp
// Code for step 2
```
### Step 3: [Third step title]
[Detailed explanation]
```csharp
// Code for step 3
```
## Complete Example
```csharp
// Full working example showing the workaround in context
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
// Before (problematic code)
// public class Example {
// public void ProblematicMethod() {
// var result = JsonSerializer.Deserialize<MyType>(json);
// }
// }
// After (with workaround)
[JsonSerializable(typeof(MyType))]
internal partial class JsonContext : JsonSerializerContext { }
public class Example {
public void FixedMethod() {
var result = JsonSerializer.Deserialize(json, JsonContext.Default.MyType);
}
}
```
## Testing the Workaround
### Verify It Works
```bash
# Build with AOT
dotnet publish -c Release -r linux-x64 /p:PublishAot=true
# Run tests
./bin/Release/net10.0/linux-x64/publish/morphir [test-command]
```
### Expected Results
- [What you should see if workaround is working]
- [How to verify no warnings/errors]
## Limitations
### Functional Limitations
- [What this workaround doesn't support]
- [Any feature gaps]
### Performance Implications
- [Impact on startup time, if any]
- [Impact on runtime performance, if any]
- [Impact on memory usage, if any]
### Maintenance Considerations
- [Extra code that needs to be maintained]
- [Manual steps required when adding new types/features]
- [When this workaround becomes obsolete]
## Proper Fix Timeline
**When will a proper fix be available?**
- [ ] Waiting for .NET framework fix (version X.Y)
- [ ] Planned for morphir-dotnet version X.Y
- [ ] Community contribution welcome
- [ ] Long-term workaround (no fix planned)
**How to migrate from workaround to proper fix:**
[Instructions for when proper fix is available]
## Alternatives Considered
### Alternative 1: [Name]
**Pros**: [Benefits]
**Cons**: [Drawbacks]
**Why not chosen**: [Reason]
### Alternative 2: [Name]
**Pros**: [Benefits]
**Cons**: [Drawbacks]
**Why not chosen**: [Reason]
## Related Workarounds
- [Link to similar workaround for related issue]
- [Link to pattern that might be useful]
## Community Feedback
[Space for community feedback on the workaround]
**Success Stories**: [Link to PRs/issues where this worked]
**Known Problems**: [Link to issues where this didn't work]
## References
- [Related Microsoft documentation]
- [Community blog posts or discussions]
- [AOT/Trimming Guide section](../../../docs/contributing/aot-trimming-guide.md#relevant-section)
---
**Last Updated**: YYYY-MM-DD
**Status**: Active | Deprecated | Superseded by [link]
**Tested With**: .NET X.Y, morphir-dotnet X.Y
```
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### README.md
```markdown
# AOT Guru Skill
Single-file trimmed executable and Native AOT optimization expert for morphir-dotnet.
## Quick Start
This skill is automatically activated when you mention:
- "single-file" or "trimmed executable"
- "AOT" or "Native AOT"
- "trimming" or "PublishTrimmed"
- "size optimization"
- "IL2026", "IL3050" (trimming/AOT warnings)
- "reflection error"
- "source generator" or "Myriad"
## What This Skill Does
The AOT Guru helps with:
1. **Single-File Trimmed Executables** (Primary Focus) - Produce optimized deployments today
2. **AOT Readiness** - Guide code toward eventual Native AOT support
3. **Trimming Diagnostics** - Identify and resolve trimming issues
4. **Size Optimization** - Reduce binary size through configuration
5. **F# and Myriad Expertise** - Compile-time code generation for F#
6. **Knowledge Base** - Maintain and evolve best practices
7. **Testing Automation** - Create and run test matrices
8. **Continuous Improvement** - Learn from issues and update documentation
## Current Focus: Single-File Trimmed Executables
The primary focus is on **single-file trimmed executables** which are:
- ✅ Available now (no blockers)
- ✅ Significantly smaller than untrimmed (30-50% reduction)
- ✅ Easy to deploy (single file)
- ✅ No .NET runtime dependency
- ✅ Fast enough for CLI tools
Native AOT is the **future goal**, but not immediately achievable due to:
- ❌ Reflection usage in existing code
- ❌ Some dependency compatibility issues
- ❌ Dynamic code patterns
**The AOT Guru guides you to make code AOT-ready even while using trimmed executables today.**
## Common Use Cases
### "I'm getting IL2026 warnings"
**What it means**: Code is using reflection (not compatible with trimming or AOT)
**AOT Guru will**:
1. Analyze the warning details
2. Identify the reflection usage
3. Suggest source generators (C#) or Myriad (F#)
4. Show code examples
5. Explain why this prepares for future AOT
6. Update documentation if it's a new pattern
### "My trimmed binary is 40 MB, can we reduce it?"
**AOT Guru will**:
1. Analyze project dependencies
2. Check optimization flags
3. Identify large dependencies
4. Suggest replacements or optimizations
5. Provide step-by-step size reduction plan
6. Explain current vs future AOT size targets
### "How do I make System.Text.Json work with trimming?"
**AOT Guru will**:
1. Explain source-generated serialization contexts
2. Show code examples
3. Create JsonSerializerContext for your types
4. Test the changes
5. Update documentation
6. Note that this also prepares for AOT
### "Should I use FSharp.SystemTextJson in F# code?"
**AOT Guru will**:
1. Explain that FSharp.SystemTextJson uses reflection
2. Not compatible with trimming or AOT
3. Recommend Myriad for compile-time generation
4. Or use manual parsing/serialization
5. Show examples of both approaches
1. Explain source-generated serialization contexts
2. Show code examples
3. Create JsonSerializerContext for your types
4. Test the changes
5. Update documentation
### "My trimmed build succeeds but crashes at runtime"
**AOT Guru will**:
1. Diagnose likely trimming issue (types/methods removed)
2. Check for MissingMethodException or TypeLoadException
3. Add DynamicDependency attributes
4. Test with PublishTrimmed first (easier to debug than AOT)
5. Document the issue for future reference
### "What's Myriad and should I use it for F# code?"
**AOT Guru will**:
1. Explain Myriad: F# compile-time code generation
2. Compare to C# source generators
3. Show when Myriad helps (avoiding reflection in F#)
4. Provide examples of Myriad usage
5. Link to Myriad documentation
6. Explain how it prepares for future AOT
## Incremental Path to AOT
The AOT Guru understands that Native AOT is not immediately achievable. Here's the recommended path:
### Phase 1: Single-File Trimmed (Now) ✅
**Focus**: Produce deployable executables today
- Configure PublishTrimmed + PublishSingleFile
- Fix trimming warnings
- Optimize size (15-35 MB range)
- Test thoroughly
### Phase 2: AOT-Ready Patterns (Ongoing) 🚧
**Focus**: Write new code that will work with AOT
- Use source generators (C#) or Myriad (F#)
- Avoid reflection in new code
- Choose AOT-compatible dependencies
- Mark non-AOT code with attributes
### Phase 3: Refactor Existing (Future) ⏳
**Focus**: Make existing code AOT-compatible
- Replace reflection with generators
- Update dependencies
- Refactor dynamic code
### Phase 4: Enable AOT (Future Goal) 🎯
**Focus**: Compile with PublishAot=true
- Enable Native AOT
- Achieve 5-12 MB target sizes
- Instant startup times
**Current Status**: Phase 1 (trimmed) is production-ready. Phase 2 (AOT-ready patterns) is ongoing. The AOT Guru helps you succeed at Phase 1 while preparing for Phase 4.
## Tools Provided
### Diagnostic Scripts (.fsx)
Located in `.claude/skills/aot-guru/`:
1. **aot-diagnostics.fsx** - Comprehensive project analysis
```bash
dotnet fsi aot-diagnostics.fsx <project-path>
```
- Checks PublishAot configuration
- Identifies reflection usage
- Analyzes dependencies
- Reports AOT compatibility issues
2. **aot-analyzer.fsx** - Build output analysis
```bash
dotnet fsi aot-analyzer.fsx <build-log>
```
- Categorizes AOT warnings
- Groups by severity
- Suggests fixes
- Tracks trends
3. **aot-test-runner.fsx** - Test matrix runner
```bash
dotnet fsi aot-test-runner.fsx --runtime linux-x64
```
- Tests multiple configurations
- Measures binary sizes
- Runs smoke tests
- Generates comparison report
### Issue Templates
Located in `templates/`:
1. **aot-issue-report.md** - For documenting new AOT issues
2. **aot-workaround.md** - For documenting workarounds
3. **known-issues/** - Database of all encountered issues
## Knowledge Base
The AOT Guru maintains and updates:
1. **AOT/Trimming Guide** (`docs/contributing/aot-trimming-guide.md`)
- Comprehensive patterns and examples
- User-facing documentation
- Updated with new .NET releases
2. **AOT Optimization Guide** (`.agents/aot-optimization.md`)
- Agent-specific guidance
- Decision trees
- Issue resolution workflows
3. **Issue Database** (`templates/known-issues/`)
- Catalog of all AOT issues
- Resolution status
- Patterns and trends
## Size Targets
Based on morphir-dotnet requirements:
### Current Reality (Single-File Trimmed)
| Configuration | Target Size | Use Case |
|--------------|-------------|----------|
| Minimal CLI | 15-25 MB | Basic IR operations, trimmed |
| Feature-rich CLI | 25-35 MB | Full tooling features, trimmed |
| With Rich UI | 30-40 MB | Spectre.Console, trimmed |
### Future Goal (Native AOT)
| Configuration | Target Size | Use Case |
|--------------|-------------|----------|
| Minimal CLI | 5-8 MB | Basic IR operations, AOT + trimming |
| Feature-rich CLI | 8-12 MB | Full tooling, AOT + trimming |
| With Rich UI | 10-15 MB | Spectre.Console, AOT + trimming |
**Note**: Focus on achieving current targets with trimmed executables while guiding code toward future AOT targets.
## Example Workflow
### Making a Feature AOT-Compatible
1. **Assessment**
```
You: "I need to make the VerifyIR feature AOT-compatible"
AOT Guru:
- Analyzes VerifyIR code
- Identifies JSON serialization usage
- Checks for reflection patterns
- Reviews dependencies (WolverineFx, System.Text.Json)
```
2. **Planning**
```
AOT Guru provides:
- List of changes needed
- Priority order
- Estimated effort
- Potential risks
```
3. **Implementation**
```
AOT Guru:
- Creates source-generated JsonSerializerContext
- Adds DynamicDependency attributes where needed
- Updates WolverineFx configuration for AOT
- Shows code examples
```
4. **Testing**
```
AOT Guru:
- Builds with PublishAot=true
- Runs smoke tests
- Measures binary size
- Compares against targets
```
5. **Documentation**
```
AOT Guru:
- Updates AOT/Trimming Guide with new patterns
- Documents any issues encountered
- Adds BDD test scenarios
```
## Decision Trees
### "I have an AOT error"
```
Error Type?
├── IL2026 (RequiresUnreferencedCode)
│ ├── System.Text.Json → Use source generators
│ └── Other reflection → Add DynamicDependency or refactor
│
├── IL3050 (RequiresDynamicCode)
│ ├── LINQ expressions → Replace with delegates
│ └── Reflection.Emit → Use source generators
│
├── IL2087 (Type incompatibility)
│ └── Add [DynamicallyAccessedMembers] attributes
│
└── Runtime error (MissingMethodException)
└── Add DynamicDependency or TrimmerRootDescriptor
```
### "My binary is too large"
```
Size vs Target?
├── > 20 MB → Check dependencies (major issue)
│ ├── Run: dotnet list package
│ ├── Look for: Newtonsoft.Json, heavy ORMs
│ └── Replace with lighter alternatives
│
├── 12-20 MB → Check optimization flags
│ ├── IlcOptimizationPreference=Size
│ ├── InvariantGlobalization=true
│ └── Enable all feature switches
│
├── 8-12 MB → Feature-rich target (acceptable)
│ └── Document feature set and size
│
└── < 8 MB → Minimal/optimal (excellent)
└── Track for size regression
```
## Integration with Other Skills
### With QA Tester
- AOT Guru provides test matrices
- QA Tester executes and validates
- Share issue reports and regression data
### With Release Manager
- AOT Guru ensures AOT builds before release
- Release Manager includes AOT binaries in release
- Track binary sizes across releases
## Continuous Improvement
The AOT Guru learns and improves by:
1. **Pattern Recognition** - Identifies recurring issues
2. **Automation** - Creates diagnostic scripts for common problems
3. **Documentation** - Updates guides with new patterns
4. **Community** - Shares findings with broader .NET community
### Quarterly Review
Every quarter, the AOT Guru reviews:
- All documented issues
- Size trends
- New .NET AOT features
- Community best practices
- Documentation accuracy
## Getting Help
If the AOT Guru encounters something it can't solve:
1. Documents the issue thoroughly
2. Researches .NET community solutions
3. Escalates to maintainers with full context
4. Updates knowledge base with resolution
## References
- [AOT/Trimming Guide](../../../docs/contributing/aot-trimming-guide.md) - User-facing documentation
- [F# Coding Guide](../../../docs/contributing/fsharp-coding-guide.md) - F# AOT patterns
- [AGENTS.md](../../../AGENTS.md) - Project guidance
- [Microsoft AOT Docs](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/)
---
**Philosophy**: The best AOT support is proactive, not reactive. Design for AOT from the start, document every issue, automate diagnostics, and make AOT easier for everyone over time.
```