Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 15 additions & 17 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,28 +161,26 @@ Auto-generated DTOs use structured namespaces reflecting the KerML/SysML package
- 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
- 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`, `while` of `do…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 static `ComputeXxxOperation` / `ComputeXxx` extension 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 OCL `self.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.OwnedRelatedElement` and `IElement.OwnedRelationship` storage 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**. The canonical "in-between" pattern is *filter-by-type-then-validate-count*: project with `OfType<ITargetType>()`, then validate the projection count against the **derived property's declared multiplicity** (read it from the `[Property(lowerValue:…, upperValue:…)]` attribute on the generated POCO interface, or directly from the UML XMI). The failure mode depends on the multiplicity:
- **`IRelationship.OwnedRelatedElement` and `IElement.OwnedRelationship` storage 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 helper `ElementExtensions.RequireSingleOfType<T>(this IReadOnlyList<IElement>, string)` from `SysML2.NET/Extensions/ElementExtensions.cs` — it does a zero-allocation index-based scan, early-exits on the second match, and throws `IncompleteModelException` with distinct "missing" vs "more than one" diagnostics:

```csharp
// [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`. Because `IReadOnlyList<T>` is covariant, the same helper works on `IElement.OwnedRelationship` (whose element type is `IRelationship : 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 IncompleteModelException` | return the match | `throw IncompleteModelException` |
| `[0..1]` (lowerValue=0, upperValue=1) | `return null` | return 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 |

`IncompleteModelException` is the loud signal to SDK users that the model is malformed — DO NOT swallow it as `null` when the multiplicity is `[1..1]`, and DO NOT raise it for the empty case when the multiplicity is `[0..1]` (a legitimately-optional property).

```csharp
// [1..1] type-narrowed redefinition (e.g. SubjectMembership::ownedSubjectParameter : IUsage)
var matches = subject.OwnedRelatedElement.OfType<ITargetType>().ToList();

return matches.Count == 1
? matches[0]
: throw new IncompleteModelException($"{nameof(subject)} must have exactly one related element of type {nameof(ITargetType)}");

// [1..1] non-narrowing redefinition (e.g. OwningMembership::ownedMemberElement : IElement)
return subject.OwnedRelatedElement.Count == 1
? subject.OwnedRelatedElement[0]
: throw new IncompleteModelException($"{nameof(subject)} must have exactly one related element");
```

Do NOT use `.Count != 1 → throw` followed by `OwnedRelatedElement[0] as ITargetType` — that pattern silently drops the correctly-typed element when it does not sit at index 0 (e.g. when an `IAnnotation` target is also present, since `AssignOwnership` allows owned related elements for both `IOwningMembership` AND `IAnnotation`). Same rule applies to any other derived property that subsets one of these two `[0..*]` storage collections.
Do NOT use `.Count != 1 → throw` followed by `OwnedRelatedElement[0] as ITargetType` — that pattern (a) silently drops the correctly-typed element when it does not sit at index 0 (`AssignOwnership` allows owned related elements for both `IOwningMembership` AND `IAnnotation`, so a Membership can carry annotation targets alongside the member element), and (b) always allocates a `List<T>` via `OfType<T>().ToList()` even when the answer is decidable after the first two elements.
Original file line number Diff line number Diff line change
Expand Up @@ -173,16 +173,16 @@ public void VerifyComputeIsTriggerActionOperation()

Assert.That(acceptInTransitionC1.ComputeIsTriggerActionOperation(), Is.False);

// Branch C2 — owningType IS TransitionUsage, with Trigger TFM.
// The Where(Kind == Trigger) filter passes, then .transitionFeature is accessed, which dispatches
// through TransitionFeatureMembershipExtensions.ComputeTransitionFeature (still a stub).
// For Later: depends on TransitionFeatureMembershipExtensions.ComputeTransitionFeature at SysML2.NET/Extend/TransitionFeatureMembershipExtensions.cs:51, which is still a stub.
// Branch C2 — owningType IS TransitionUsage, with Trigger TFM whose transitionFeature IS this
// AcceptActionUsage → triggerAction contains self → true.
// Note: a useful negative companion would be a sibling AcceptActionUsage that is NOT wired as the
// transitionFeature of any Trigger TFM (e.g. owned via plain OwningMembership), asserting Is.False.
var transitionOwnerC2 = new TransitionUsage();
var triggerTfm = new TransitionFeatureMembership { Kind = TransitionFeatureKind.Trigger };
var acceptInTransitionC2 = new AcceptActionUsage();
transitionOwnerC2.AssignOwnership(triggerTfm, acceptInTransitionC2);

Assert.That(() => acceptInTransitionC2.ComputeIsTriggerActionOperation(), Throws.TypeOf<NotSupportedException>());
Assert.That(acceptInTransitionC2.ComputeIsTriggerActionOperation(), Is.True);

// Branch C3 — owningType IS TransitionUsage, only non-Trigger TFMs (Effect kind).
// The Where(Kind == Trigger) filter excludes the Effect TFM → triggerAction returns empty list.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,89 @@
// -------------------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------------------
// <copyright file="TransitionFeatureMembershipExtensionsTestFixture.cs" company="Starion Group S.A.">
//
//
// Copyright 2022-2026 Starion Group S.A.
//
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//
// http://www.apache.org/licenses/LICENSE-2.0
//
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//
// </copyright>
// ------------------------------------------------------------------------------------------------

namespace SysML2.NET.Tests.Extend
{
using System;

using NUnit.Framework;


using SysML2.NET.Core.POCO.Root.Elements;
using SysML2.NET.Core.POCO.Root.Namespaces;
using SysML2.NET.Core.POCO.Systems.Actions;
using SysML2.NET.Core.POCO.Systems.States;
using SysML2.NET.Exceptions;
using SysML2.NET.Extensions;

using Type = SysML2.NET.Core.POCO.Core.Types.Type;

[TestFixture]
public class TransitionFeatureMembershipExtensionsTestFixture
{
[Test]
public void ComputeTransitionFeature_ThrowsNotSupportedException()
public void VerifyComputeTransitionFeature()
{
Assert.That(() => ((ITransitionFeatureMembership)null).ComputeTransitionFeature(), Throws.TypeOf<NotSupportedException>());
Assert.That(() => ((ITransitionFeatureMembership)null).ComputeTransitionFeature(), Throws.TypeOf<ArgumentNullException>());

// Empty OwnedRelatedElement → [1..1] violation: throws IncompleteModelException.
var transitionFeatureMembership = new TransitionFeatureMembership();

Assert.That(() => transitionFeatureMembership.ComputeTransitionFeature(), Throws.TypeOf<IncompleteModelException>());

// Single AcceptActionUsage wired via the public API → returned.
var owningType = new Type();
var acceptActionUsage = new AcceptActionUsage();

owningType.AssignOwnership(transitionFeatureMembership, acceptActionUsage);

Assert.That(transitionFeatureMembership.ComputeTransitionFeature(), Is.SameAs(acceptActionUsage));

// Two AcceptActionUsage instances in OwnedRelatedElement → [1..1] violation: throws IncompleteModelException.
var twoStepMembership = new TransitionFeatureMembership();
var firstStep = new AcceptActionUsage();
var secondStep = new AcceptActionUsage();

((IContainedRelationship)twoStepMembership).OwnedRelatedElement.Add(firstStep);
((IContainedRelationship)twoStepMembership).OwnedRelatedElement.Add(secondStep);

Assert.That(() => twoStepMembership.ComputeTransitionFeature(), Throws.TypeOf<IncompleteModelException>());

// Mixed-type owned related elements: exactly one IStep alongside a non-IStep (Namespace).
// The OfType<IStep>() projection MUST pick out the IStep regardless of its position
// (this is the core robustness guarantee — never positionally index the unfiltered collection).
var mixedMembership = new TransitionFeatureMembership();
var siblingNonStep = new Namespace();
var mixedStep = new AcceptActionUsage();

((IContainedRelationship)mixedMembership).OwnedRelatedElement.Add(siblingNonStep);
((IContainedRelationship)mixedMembership).OwnedRelatedElement.Add(mixedStep);

Assert.That(mixedMembership.ComputeTransitionFeature(), Is.SameAs(mixedStep));

// OwnedRelatedElement populated with non-IStep element(s) only → no IStep match:
// [1..1] violation, throws IncompleteModelException.
var nonStepMembership = new TransitionFeatureMembership();
var nonStepElement = new Namespace();

((IContainedRelationship)nonStepMembership).OwnedRelatedElement.Add(nonStepElement);

Assert.That(() => nonStepMembership.ComputeTransitionFeature(), Throws.TypeOf<IncompleteModelException>());
}
}
}
Loading
Loading