This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
SysML2.NET is a .NET C# SDK implementing the OMG SysML v2 specification (based on Beta 4 pilot implementation). It provides metaclass DTOs/POCOs, serializers (JSON, XMI, MessagePack), a REST client, a DAL layer, and a Blazor WebAssembly viewer application. Current version: 0.19.0.
# Restore and build entire solution
dotnet restore SysML2.NET.sln
dotnet build SysML2.NET.sln
# Run all tests
dotnet test SysML2.NET.sln
# Run tests for a specific project
dotnet test SysML2.NET.Tests/SysML2.NET.Tests.csproj
dotnet test SysML2.NET.Serializer.Json.Tests/SysML2.NET.Serializer.Json.Tests.csproj
# Run a single test by name
dotnet test SysML2.NET.Tests/SysML2.NET.Tests.csproj --filter "FullyQualifiedName~AcceptActionUsageExtensionsTestFixture"
# Run with coverage (as CI does)
dotnet-coverage collect "dotnet test SysML2.NET.sln --no-build" -f xml -o coverage.xmlTest framework: NUnit. Test classes use [TestFixture] and [Test] attributes.
When writing or modifying unit tests in any *.Tests/ project: read TESTING.md at the repo root for the NUnit conventions (one [Test] per method-under-test, Assert.That everywhere, Assert.EnterMultipleScope only for consecutive asserts, mandatory positive + negative coverage, assertion idiom preferences, Verify{MethodUnderTest} naming).
- favour duplicated code in codegeneration to have staticaly defined methods that provide performance over reflection based code.
- code generation is done by processing the UML model and creating handlebars templates
- When working on the grammar/textual notation code generator (
SysML2.NET.CodeGenerator/HandleBarHelpers/RulesHelper.csand related grammar processing): readSysML2.NET.CodeGenerator/GRAMMAR.mdfor the KEBNF grammar model, cursor/builder conventions, and code-gen patterns already handled.
Every code change touching any of the following paths MUST be verified by the textual-notation-reviewer agent before reporting the change as complete or committing:
- Every file under
SysML2.NET.Serializer.TextualNotation/Writers/— both hand-coded partials (*.cs), the generatedAutoGenTextualNotationBuilder/*.cs,IsValidForguard extensions (TextualNotationValidationExtensions.cs), and any membership / string / cursor helpers that sit beside them. - Every file under
SysML2.NET/LexicalRules/— both hand-coded members and the generatedAutoGenLexicalRules/*.cs(Keywords,SymbolicKeywordKind,SymbolicKeywordKindExtensions). SysML2.NET.CodeGenerator/HandleBarHelpers/RulesHelper.csand any Handlebars template underSysML2.NET.CodeGenerator/Templates/Uml/that emits textual-notation or lexical-rules code.
The KEBNF grammar context applies to ALL of these locations — not just the generator. When implementing or reviewing hand-coded methods in SysML2.NET.Serializer.TextualNotation/Writers/, the author and the reviewer must re-ground in:
SysML2.NET.CodeGenerator/GRAMMAR.md— the cursor / builder conventions and patternsResources/SysML-textual-bnf.kebnfandResources/KerML-textual-bnf.kebnf— the grammar source of truth- The rule's
<para>{…}</para>XML doc on the generated sibling method (if the method is a HandCoded companion)
The agent is defined at .claude/agents/textual-notation-reviewer.md. Invoke it with the rule(s) being implemented, the KEBNF text, and the file paths to review. It enforces:
- the
Move()↔+=Golden Rule (cursor advances only on+=consumption; directcursor.Move()calls are forbidden after any callee that already advances the cursor internally) - EBNF quantifier semantics (
?= 0..1 → singleif;*= 0+ →whileloop;+= 1+ → emit-once then loop) - correct runtime type discriminators (e.g.
ISpecializationIS the cursor element, not wrapped inIOwningMembership) - absence of greedy-builder pitfalls that silently drop interleaved elements
- consistency between the hand-coded method and the grammar rule it implements (name, target type, element order, alternatives)
Reason this is mandatory: reviewer passes have caught real grammar-correctness bugs (wrong discriminator, silent element drop, missing * loop, spurious double-Move() in FeatureSpecialization* loops) that would have shipped broken textual notation without failing any existing test.
Most code in this repo is auto-generated — files marked THIS IS AN AUTOMATICALLY GENERATED FILE. ANY MANUAL CHANGES WILL BE OVERWRITTEN! must not be edited directly.
The pipeline:
- Input:
Resources/KerML_only_xmi.umlandResources/SysML_only_xmi.uml— these two UML-based XMI files define the KerML and SysML v2 specification respectively. They are the single source of truth for all generated DTOs, POCOs, serializers, extension methods, and other auto-generated code. All OCL constraints (derivation rules, validation invariants, and operation body conditions) for each metaclass are also defined within these XMI files. - Generator:
SysML2.NET.CodeGeneratorreads these viauml4net.xmi, uses Handlebars templates (Templates/Uml/*.hbs) to generate code - Output:
AutoGen*directories across multiple projects
Generator classes in SysML2.NET.CodeGenerator/Generators/UmlHandleBarsGenerators/ produce:
- DTOs and interfaces →
SysML2.NET/Core/AutoGenDto/ - POCOs →
SysML2.NET/Core/AutoGenPoco/ - Enums →
SysML2.NET/Core/AutoGenEnum/ - JSON serializers/deserializers →
SysML2.NET.Serializer.Json/Core/AutoGenSerializer/andAutoGenDeSerializer/ - MessagePack formatters →
SysML2.NET.Serializer.MessagePack/ - Extension methods (Extend) →
SysML2.NET/Extend/ - DAL factories →
SysML2.NET.Dal/Core/
The XMI files (Resources/KerML_only_xmi.uml, Resources/SysML_only_xmi.uml) define the structure of the metamodel and the OCL constraints. The KEBNF files (Resources/SysML-textual-bnf.kebnf, Resources/KerML-textual-bnf.kebnf) define the concrete textual syntax. Neither narrates the semantics or intent of a metaclass, the rationale behind an OCL constraint, the contract of the REST API, or the idiomatic use of a notation construct. For that, this repo carries the formal OMG specification texts (PDF→text) under Resources/specification/. Treat them as cross-references — not as a generation input.
Resources/specification/1-Kernel_Modeling_Language.pdf.txt— Kernel Modeling Language (KerML) Version 1.0 (OMG formal/2026-03-01). Consult when working with metaclasses in theRoot.*,Core.*, andKernel.*namespaces (underSysML2.NET/Core/AutoGenDto/andAutoGenPoco/), when an OCL constraint is unclear, or when reasoning about element/relationship/feature/classification semantics that the XMI does not spell out.Resources/specification/2a-OMG_Systems_Modeling_Language.pdf.txt— OMG Systems Modeling Language (SysML) Version 2.0, Part 1: Language Specification (OMG formal/2026-03-02). Consult when working with the systems-engineering-specific metaclasses inSystems.*namespaces — Parts, Ports, Connections, Interfaces, Actions, States, Interactions, Requirements, Constraints, Use Cases, Analysis/Verification Cases, Views, Metadata — and to ground the Definition/Usage pattern.Resources/specification/3-Systems_Modeling_API_and_Services.pdf.txt— Systems Modeling API and Services Version 1.0 (OMG formal/2026-03-04). Consult when working inSysML2.NET.REST/,SysML2.NET/PIM/,SysML2.NET.Serializer.Dictionary/, orSysML2.NET/ModelInterchange/. Defines the Platform-Independent Model (ProjectService, ElementNavigationService, ProjectDataVersioningService, QueryService, ExternalRelationshipService, ProjectUsageService) and the REST/HTTP and OSLC PSMs.Resources/specification/Intro to the SysML v2 Language-Textual Notation.pdf.txt— SST tutorial, Release 2026-03. Informative companion to the KEBNF grammar; consult for canonical examples and idioms when implementing or reviewing rules underSysML2.NET.Serializer.TextualNotation/Writers/andSysML2.NET/LexicalRules/.Resources/specification/Intro to the SysML v2 Language-Graphical Notation.pdf.txt— SST tutorial, Release 2026-03. Consult when working onSysML2.NET.Viewer/(Blazor) for the visual-rendering conventions of each metaclass family.
These text files are large (PDF-converted, up to 1.3 MB) and the conversion is not always clean. Read them with Read offset/limit and use Grep to jump to chapter/section anchors (e.g. ^7\.\d+, Clause 8\., or a metaclass name) rather than loading whole files into context.
SysML2.NET (core: netstandard2.1)
├── Core/AutoGenDto/ - 342 files: DTO classes + interfaces (171 metaclasses × 2)
├── Core/AutoGenPoco/ - POCO classes + interfaces
├── Core/AutoGenEnum/ - Enums (FeatureDirectionKind, VisibilityKind, etc.)
├── Core/DTO/ - Hand-coded base: IElement : IData
├── Core/POCO/ - Hand-coded: IContainedElement, IContainedRelationship
├── Extend/ - Auto-generated extension methods per metaclass
├── Decorators/ - [Class], [Property], [Implements] attributes from UML
├── PIM/ - Platform-Independent Model DTOs (REST API types)
├── ModelInterchange/ - Archive/project interchange types (kpar support)
└── Common/IData.cs - Base interface with Id property
SysML2.NET.Extensions - Comparers, utilities across metaclasses
SysML2.NET.Serializer.Json - JSON (de)serialization via System.Text.Json
SysML2.NET.Serializer.Xmi - XMI (de)serialization
SysML2.NET.Serializer.MessagePack - MessagePack binary serialization
SysML2.NET.Serializer.Dictionary - Dictionary-based serialization (PIM)
SysML2.NET.Serializer.TextualNotation - Writers/, Writers/AutoGenTextualNotationBuilder/, validation extensions, cursor helpers
SysML2.NET.Dal - Data Access Layer (Assembler, ElementFactory)
SysML2.NET.REST - REST client + Session for SysML2 API servers
SysML2.NET.Kpar - Reader/Writer for .kpar archive format
SysML2.NET.Viewer - Blazor WebAssembly app (net9.0)
SysML2.NET.CodeGenerator - Code generation tool (net10.0, not packaged)
Each metaclass exists in two forms:
- DTO (Data Transfer Object): Lightweight, uses
Guidreferences for relationships. Used for serialization/transport. Properties reference other elements byGuidID. - POCO (Plain Old CLR Object): Rich object model with resolved object references. Used for in-memory manipulation. Uses
ContainerList<T>for containment relationships.
Both share the same I{MetaclassName} interface from AutoGenDto/. The hand-coded Core/DTO/IElement.cs adds IData (which provides Guid Id) to the root interface.
Auto-generated DTOs use structured namespaces reflecting the KerML/SysML package hierarchy:
SysML2.NET.Core.DTO.Root.Elements(Element, Annotation, etc.)SysML2.NET.Core.DTO.Core.Types(Type, Feature, Classifier, etc.)SysML2.NET.Core.DTO.Systems.Actions(ActionUsage, etc.)
- Core library (
SysML2.NET):netstandard2.1 - Test projects and CodeGenerator:
net10.0 - Viewer:
net9.0(Blazor WebAssembly)
- Paths are ALWAYS repo-relative — NEVER absolute. This rule applies to every path the agent writes anywhere: code comments, XML doc
<see cref="…"/>and prose, source-string citations, error/log messages, commit messages, PR bodies, GitHub issue bodies,.team-notes/spec files, plan files, skill prompts and agent briefs (e.g. saySysML2.NET/Extend/FooExtensions.cs, NOTC:\CODE\SysML2.NET\SysML2.NET\Extend\FooExtensions.csand NOT/c/CODE/SysML2.NET/...). Use forward slashes. Reason: absolute paths are user-/machine-specific and leak the local filesystem into the repo and into communication with other contributors — they break for anyone else, get stale on rename/move, and are noisy. The ONLY exception is theRead/Edit/Writetoolfile_pathparameter, which the tool implementation requires to be absolute — those tool arguments are not user-visible artifacts. Everything you author as content must be repo-relative. - Commit messages use prefix tags:
[Add],[Update],[Remove],[Fix]— except for issue-fixing commits produced by/implement-extensionsand/implement-extensions-batch, which use the canonical short formFix #<n>(single issue) orFix #<n1> #<n2> …(batch) so GitHub auto-closes the issues on merge. - Main branch:
master. Development branch:development. All feature work targetsdevelopmentvia PR;masteris downstream only. - CI: GitHub Actions (
CodeQuality.yml) — builds, tests, and runs SonarQube analysis - License: Apache 2.0 (code), LGPL v3.0 (metamodel files)
- To add a new metaclass: update the UML XMI source files, then run the code generators — do not manually create AutoGen files
Direct pushes to development or master are forbidden. All work lives on a feature branch.
Agent boundaries are strict and minimal:
- The agent must NOT auto-commit, EVER.
git commitis the user's responsibility — no exceptions, no asking, no "for convenience". The user reviewsgit diffand commits manually. - The agent must NOT push commits, open PRs, or merge by default. Push + PR + merge are the user's job too. The agent only performs push/PR if the user explicitly asks for them in-conversation; otherwise it stays out of git remote operations entirely.
- When the agent creates a branch (typically inside
/implement-extensions-batchstep 6), it must:- create it locally with
git switch -c <branch> origin/development, AND - immediately push the empty branch to
originwithgit push -u origin <branch>, so the remote ref exists at the same commit asorigin/developmentand the user's later push of the actual commit becomes a trivial fast-forward. This is the only push the agent performs by default. It is safe because the branch tip equalsorigin/development's tip — no new commits, no force flags, no risk of overwriting.
- create it locally with
- At the end of any task that creates a branch, the agent stops with a final summary that includes:
- the in-scope files modified, the test counts, the reviewer verdict, etc.,
- a pre-filled commit message (
Fix #<n>for single-issue runs,Fix #<n1> #<n2> …for batches — single line, no body, noCo-Authored-Bytrailer, no "🤖 Generated with …" footer), - a handoff line telling the user how to stage + commit + push the resulting commit themselves. Example:
Review
git diff, stage the in-scope files (git add <path> …— NEVER-A/.), commit with the message above, thengit push(the remote branch already exists, so this is a fast-forward — no-uneeded). Open the PR yourself via the GitHub UI orgh pr create --base development. - This is the end of the agent's involvement. The agent does NOT proceed to push the commit, does NOT open the PR, unless the user explicitly asks. Typical case: the user handles both.
If the user does explicitly ask the agent to push or open the PR (rare; user-initiated only):
- The agent verifies: current branch is not
development/master,git log -1matches the canonicalFix #<n>…form,git status --porcelainis empty. - Then
git push origin <branch>— NEVER--force, NEVER--force-with-lease, NEVER--no-verify. - Then
gh pr create --base development --head <branch> --title "Fix #<n>…" --body-file <pr-body-tmp>— NEVER--base master, NEVER--draftunless the user asked.
Failure modes:
git push -u origin <branch>(step 3) fails because the branch already exists on origin → abort, surface to user, do not force.- Branch creation requested but the current branch is
developmentormasterAND the user asked for in-place work → REFUSE. Feature work must live on a feature branch first. - If the user asks the agent to push a commit and that commit was made by the agent (somehow), refuse and surface the policy violation. The agent's commits are forbidden by construction; if one exists, it is a bug that needs human review.
Why this split: the user is the reviewer of record. The commit is the review and the push is the delivery — both are the user's calls. The agent's git involvement is bounded to: (a) create the branch locally + push the empty ref (so the user's push later is frictionless), and (b) leave the rest alone. This was tightened after two failures: first the agent auto-pushed branches to development directly, then over-corrected by auto-committing on the user's behalf.
-
Prefer comparing 'Count' to 0 rather than using 'Any()', both for clarity and for performance
-
Use 'StringBuilder.Append(char)' instead of 'StringBuilder.Append(string)' when the input is a constant unit string
-
Prefer 'string.IsNullOrWhiteSpace' over 'string.IsNullOrEmpty' when checking the non-nullable value of a string
-
Prefer switch expressions/statements over if-else chains when applicable
-
Prefer LINQ as much as possible — including for projection / filter / aggregation over collections (
items.Where(...).Select(...).ToList(),result.AddRange(items.Select(...)),items.Any(predicate), etc.) instead of hand-rolledforeach+if+.Add()loops. The ONE exception is straightforward positional or range access on a concreteList/array:list[^1]beatslist.Last(),array[1..^1]beatsarray.Skip(1).SkipLast(1)— indexer/range syntax is more performant there. Outside that narrow exception, LINQ wins for clarity AND maintainability. -
Prefer C# collection expressions (
[a, b, c],[..xs],[]) overnew[] { ... },new List<T> { ... },new T[] { ... }when constructing a collection. Applies to both production code AND tests (e.g.Is.EqualTo([classifier1, classifier2])notIs.EqualTo(new[] { classifier1, classifier2 }),return [];notreturn new List<T>();). Fall back to explicit construction only when type inference cannot pick the right collection type. -
Use meaningful variable names instead of single-letter names in any context (e.g., 'charIndex' instead of 'i', 'currentChar' instead of 'c', 'element' instead of 'e')
-
Use 'NotSupportedException' (not 'NotImplementedException') for placeholder/stub methods that require manual implementation
-
Prefer C# property patterns ('x is IType { Prop: value }') over declared-variable-plus-predicate form ('x is IType name && name.Prop == value') when the narrowed variable is only consulted once; the property-pattern form is more concise and intent-revealing
-
Always use C# auto-properties (
public T Foo { get; private set; },public T Foo { get; init; },public T Foo { get; }) — NEVER pair a private backing field with an expression-bodied or full-getter property when there is no non-trivial logic (validation, normalisation, lazy init, event firing). Mere storage is never a justification for a backing field; the compiler collapses auto-properties to the same IL. -
For test fixtures: default to ONE
[Test]method per class / method-under-test packing every scenario (happy path, edge cases, null guards, alternate inputs) into multipleAssert.Thatcalls inside that one test — perTESTING.md§2. Do NOT write one[Test]per scenario when the setup is shared; that produces a bloated test list and duplicated arrange boilerplate. Split into separate[Test]methods only when each scenario has a genuinely distinct, complex setup. -
Surround every braced block (
if,else if,while,for,foreach,switch,using,try/catch/finally,lock,do…while, anonymous{ }) with a blank line on both sides — the rule does NOT apply at the very start/end of a method body, nor between a}and a continuation keyword (else,catch,finally,whileofdo…while) that belongs to the same control flow -
When invoking an operation or derived property on a POCO from inside an extension method, call the POCO's instance member (e.g.
subject.IsDistinguishableFrom(other),subject.qualifiedName), NOT the staticComputeXxxOperation/ComputeXxxextension method. Virtual dispatch on the POCO honors operation/property REDEFINITION in subclass POCOs; calling the static extension directly bypasses dispatch and silently skips overrides. The static-extension form is reserved EXCLUSIVELY for the C# translation of OCLself.oclAsType(SuperType).method()— an explicit upcast that mandates targeting the SuperType's body (e.g.Usage::namingFeature()→FeatureExtensions.ComputeNamingFeatureOperation(usage);OwningMembership::path()→RelationshipExtensions.ComputeRedefinedPathOperation(owningMembership)) -
IRelationship.OwnedRelatedElementandIElement.OwnedRelationshipstorage collections are[0..*]— NEVER cardinality-limited. The [1..1] / [0..1] multiplicities that appear in the metamodel apply to derived / redefined properties (e.g.OwningMembership::ownedMemberElement,FeatureMembership::ownedMemberFeature,SubjectMembership::ownedSubjectParameter), NOT to the underlying storage. When implementing such a derivation, project from the collection — do not assume positional indexing. For the common case of a[1..1]derived property, use the canonical shared helperElementExtensions.RequireSingleOfType<T>(this IReadOnlyList<IElement>, string)fromSysML2.NET/Extensions/ElementExtensions.cs— it does a zero-allocation index-based scan, early-exits on the second match, and throwsIncompleteModelExceptionwith distinct "missing" vs "more than one" diagnostics:// [1..1] type-narrowed redefinition (e.g. SubjectMembership::ownedSubjectParameter : IUsage) return subject.OwnedRelatedElement.RequireSingleOfType<ITargetType>(nameof(subject)); // [1..1] non-narrowing redefinition (e.g. OwningMembership::ownedMemberElement : IElement) return subject.OwnedRelatedElement.RequireSingleOfType<IElement>(nameof(subject));
The helper signature is
internal static T RequireSingleOfType<T>(this IReadOnlyList<IElement> elements, string subjectName) where T : class, IElement. BecauseIReadOnlyList<T>is covariant, the same helper works onIElement.OwnedRelationship(whose element type isIRelationship : IElement) without an additional overload.The failure mode the helper produces matches the derived property's declared multiplicity as recorded in the
[Property(lowerValue:…, upperValue:…)]attribute on the generated POCO interface (or in the UML XMI):Multiplicity Empty projection Single-match projection 2+ match projection [1..1](lowerValue=1, upperValue=1)throw IncompleteModelExceptionreturn the match throw IncompleteModelException[0..1](lowerValue=0, upperValue=1)return null(do NOT use the helper — write inline)return the match throw IncompleteModelException(inline)[0..*]/[1..*](use List<T>projection; not this pattern)n/a n/a IncompleteModelExceptionis the loud signal to SDK users that the model is malformed — DO NOT swallow it asnullwhen the multiplicity is[1..1], and DO NOT raise it for the empty case when the multiplicity is[0..1](a legitimately-optional property).Do NOT use
.Count != 1 → throwfollowed byOwnedRelatedElement[0] as ITargetType— that pattern (a) silently drops the correctly-typed element when it does not sit at index 0 (AssignOwnershipallows owned related elements for bothIOwningMembershipANDIAnnotation, so a Membership can carry annotation targets alongside the member element), and (b) always allocates aList<T>viaOfType<T>().ToList()even when the answer is decidable after the first two elements.