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
35 changes: 29 additions & 6 deletions .claude/commands/implement-extensions-batch.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: Spawn the 4-role team across N SysML2.NET Extend files in one run — creates a batch branch, assigns the related GitHub issues to the user, and updates each issue's checklist on completion
description: Spawn the 4-role team across N SysML2.NET Extend files in one run — creates and pushes a batch branch, assigns the related GitHub issues to the user, and updates each issue's checklist on completion
argument-hint: <file1.cs> <file2.cs> [<file3.cs> ...] (2–6 Extension file names; each will be normalised to SysML2.NET/Extend/<Foo>Extensions.cs)
---

Expand All @@ -13,7 +13,9 @@ single-file flow:
1. **Pre-flight validation** of every file + its GitHub issue, before any state
change.
2. **Creates a new git branch** off `development` with a deterministic name
derived from the batch's issue numbers.
derived from the batch's issue numbers, AND pushes it to `origin` with
upstream tracking set so the user can immediately open a pull request after
committing.
3. **Assigns every related GitHub issue to the invoking user** (`@me`).
4. **Parallelises agent spawns across files** wherever their target files are
disjoint.
Expand Down Expand Up @@ -138,11 +140,25 @@ batch-impl-extensions-<dashed-issue-numbers>
- If more than 4 issues: include the first 4 + `-plus<N-4>` suffix (e.g.
`batch-impl-extensions-123-180-186-190-plus2` for N=6).

Create:
Create locally **and immediately publish to `origin` with upstream tracking**:
```bash
git switch -c <branch-name> origin/development
git push -u origin <branch-name>
```

The push lifts the branch onto the remote at the same commit as
`origin/development` (no diff yet — that comes after the batch's edits + the
user's commit). Setting upstream now means:
- The user's eventual `git push` after committing needs no flags.
- A pull request can be opened via the GitHub UI or `gh pr create` as soon as
the user pushes their first commit, without an additional `git push -u`
step.

If the `git push -u` fails (network, auth, branch-protection refusing empty
pushes), log the failure but **continue with the batch**. The implementation
work is the main goal; the branch will still exist locally and the user can
re-push manually at the end. Surface the failure clearly in the final summary.

Refuse if the branch already exists locally OR on origin (`git ls-remote
--exit-code origin <branch>`) — ask the user to pick a different batch or delete
the stale branch.
Expand Down Expand Up @@ -253,7 +269,10 @@ step-11 logic from `/implement-extensions`:

Print to the user:

- **Branch**: name + base ref + how to delete-if-aborting.
- **Branch**: name + base ref + remote-tracking state (`pushed to origin` /
`local only — push failed at step 6, push manually with: git push -u origin <branch>`)
+ how to delete-if-aborting (locally: `git branch -D <branch>`; remotely if
pushed: `git push origin --delete <branch>`).
- **Per-file table**:

| File | Stubs impl. | Targeted tests | Reg. sweep impact | Reviewer | Issue |
Expand All @@ -269,8 +288,11 @@ Print to the user:
- Out-of-scope blockers surfaced (e.g. "VerifyComputeX in <Sibling>TestFixture
is still stub-blocked on `<UpstreamMethod>` — consider a follow-up issue").

- **Reminder**: nothing is auto-committed. User reviews `git diff`, decides
whether to commit / push / open PR.
- **Reminder**: nothing is auto-committed. The branch exists locally (and on
`origin` with upstream tracking, when step 6's push succeeded). User reviews
`git diff`, commits, then `git push` (no flags needed — tracking is already
set) and opens the PR via `gh pr create --base development --head <branch>`
or the GitHub UI.

## Failure handling

Expand All @@ -281,6 +303,7 @@ Print to the user:
| Ambiguous issue | Step 2 | `AskUserQuestion` for an explicit issue number per file. |
| Dirty working tree | Step 4 | Abort, ask user to commit/stash. |
| Branch already exists | Step 6 | Abort; ask user to pick a different batch or delete the stale branch. |
| `git push -u origin <branch>` fails (network, auth, branch protection) | Step 6 | Log + continue (non-blocking; surface clearly in step 14 final summary with a manual re-push command). |
| `gh issue edit --add-assignee` fails for one issue | Step 7 | Log + continue (non-blocking; implementation still proceeds). |
| Production build fails after implementer | Step 10.1 | Attribute to the file, re-dispatch that implementer. |
| Targeted test fails | Step 10.2 | Attribute (OCL vs test bug), re-dispatch correct role. |
Expand Down
190 changes: 90 additions & 100 deletions SysML2.NET.Tests/Extend/ExpressionExtensionsTestFixture.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
// -------------------------------------------------------------------------------------------------
// <copyright file="ExpressionExtensionsTestFixture.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>
// ------------------------------------------------------------------------------------------------

Expand All @@ -28,15 +28,73 @@ namespace SysML2.NET.Tests.Extend
using SysML2.NET.Core.Core.Types;
using SysML2.NET.Core.POCO.Core.Features;
using SysML2.NET.Core.POCO.Core.Types;
using SysML2.NET.Core.POCO.Kernel.Expressions;
using SysML2.NET.Core.POCO.Kernel.FeatureValues;
using SysML2.NET.Core.POCO.Kernel.Functions;
using SysML2.NET.Core.POCO.Root.Elements;
using SysML2.NET.Extensions;

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

[TestFixture]
public class ExpressionExtensionsTestFixture
{
[Test]
public void VerifyComputeCheckConditionOperation()
{
// Null guard on subject.
Assert.That(() => ((IExpression)null).ComputeCheckConditionOperation(null), Throws.TypeOf<ArgumentNullException>());

var expression = new Expression();

// target == null: forwarded to Evaluate; empty-resultExprs branch returns [] → false (Count != 1).
Assert.That(expression.ComputeCheckConditionOperation(null), Is.False);

// Empty: Evaluate returns [] (no ResultExpressionMembership) → false (Count != 1).
Assert.That(expression.ComputeCheckConditionOperation(null), Is.False);

// Populated case: ResultExpressionMembership wired — Evaluate delegates through the inner
// (empty) Expression and returns the empty list. CheckCondition requires Count == 1 AND
// a LiteralBoolean { Value: true }, so it returns false.
var expressionWithRem = new Expression();
var resultExprMembership = new ResultExpressionMembership();
var innerExpression = new Expression();
expressionWithRem.AssignOwnership(resultExprMembership, innerExpression);

Assert.That(expressionWithRem.ComputeCheckConditionOperation(null), Is.False);
}

[Test]
public void VerifyComputeEvaluateOperation()
{
// Null guard on subject.
Assert.That(() => ((IExpression)null).ComputeEvaluateOperation(null), Throws.TypeOf<ArgumentNullException>());

var expression = new Expression();

// target == null is permitted for the empty branch (base body doesn't dereference target).
Assert.That(() => expression.ComputeEvaluateOperation(null), Throws.Nothing);

// Empty: no IResultExpressionMembership in ownedFeatureMembership → returns empty list.
Assert.That(expression.ComputeEvaluateOperation(null), Is.Empty);

// Discrimination: FeatureMembership with non-ResultExpressionMembership type → still empty.
var plainMembership = new FeatureMembership();
var plainFeature = new Feature();
expression.AssignOwnership(plainMembership, plainFeature);

Assert.That(expression.ComputeEvaluateOperation(null), Is.Empty);

// Populated case: ResultExpressionMembership wired — the production code resolves
// resultExpressionMembership.ownedResultExpression (the inner Expression) and recursively
// calls Evaluate on it. The inner Expression is itself empty (no nested
// ResultExpressionMembership), so its Evaluate returns the empty list.
var expressionWithRem = new Expression();
var resultExprMembership = new ResultExpressionMembership();
var innerExpression = new Expression();
expressionWithRem.AssignOwnership(resultExprMembership, innerExpression);

Assert.That(expressionWithRem.ComputeEvaluateOperation(null), Is.Empty);
}

[Test]
public void VerifyComputeFunction()
{
Expand All @@ -48,7 +106,7 @@ public void VerifyComputeFunction()
Assert.That(expression.ComputeFunction(), Is.Null);

// Negative: FeatureTyping pointing at a non-Function Type → null.
var nonFunctionType = new SysML2.NET.Core.POCO.Core.Types.Type();
var nonFunctionType = new Type();
var typingToNonFunction = new FeatureTyping { Type = nonFunctionType };
expression.AssignOwnership(typingToNonFunction);

Expand Down Expand Up @@ -93,38 +151,6 @@ public void VerifyComputeIsModelLevelEvaluable()
Assert.That(expressionWithNonImplied.ComputeIsModelLevelEvaluable(), Is.False);
}

[Test]
public void VerifyComputeResult()
{
Assert.That(() => ((IExpression)null).ComputeResult(), Throws.TypeOf<ArgumentNullException>());

var expression = new Expression();

// Empty: no featureMembership → null.
Assert.That(expression.ComputeResult(), Is.Null);

// Negative: FeatureMembership with non-ReturnParameterMembership → null.
var featureMembership = new FeatureMembership();
var plainFeature = new Feature();
expression.AssignOwnership(featureMembership, plainFeature);

Assert.That(expression.ComputeResult(), Is.Null);

// Positive: one ReturnParameterMembership with ownedMemberParameter set → that Feature returned.
var resultFeature = new Feature();
var returnParamMembership = new ReturnParameterMembership();
expression.AssignOwnership(returnParamMembership, resultFeature);

Assert.That(expression.ComputeResult(), Is.SameAs(resultFeature));

// Multiple ReturnParameterMemberships (illegal per OCL validation but tolerated) → first returned.
var secondResultFeature = new Feature();
var secondReturnParamMembership = new ReturnParameterMembership();
expression.AssignOwnership(secondReturnParamMembership, secondResultFeature);

Assert.That(expression.ComputeResult(), Is.SameAs(resultFeature));
}

[Test]
public void VerifyComputeModelLevelEvaluableOperation()
{
Expand Down Expand Up @@ -205,85 +231,49 @@ public void VerifyComputeModelLevelEvaluableOperation()

Assert.That(expressionWithValuation.ComputeModelLevelEvaluableOperation([]), Is.True);

// BRANCH_B: ownedFeature under ResultExpressionMembership — STUB-BLOCKER.
// Although BRANCH_B logic only accesses owningFeatureMembership and calls ModelLevelEvaluable
// on the inner Expression, the production code first computes expressionSubject.result (line 157
// of ExpressionExtensions.cs), which traverses featureMembership → inheritedMembership →
// ownedFeature → ResultExpressionMembership.ownedMemberFeature → ownedResultExpression →
// ResultExpressionMembershipExtensions.ComputeOwnedResultExpression, which is a stub.
// Therefore the BRANCH_B recursion path cannot be exercised until that stub is implemented.
// BRANCH_B: ownedFeature owned via ResultExpressionMembership — passes branchB.
// The inner Expression is empty (no specializations, no features), so its recursive
// ModelLevelEvaluable call returns true; the outer Expression's single owned feature
// satisfies branchB (owningFeatureMembership is IResultExpressionMembership AND
// innerExpression.ModelLevelEvaluable is true). Outer returns true.
var expressionBranchB = new Expression();
var innerExpression = new Expression();
var resultExprMembershipB = new ResultExpressionMembership();
expressionBranchB.AssignOwnership(resultExprMembershipB, innerExpression);

Assert.That(
() => expressionBranchB.ComputeModelLevelEvaluableOperation([]),
Throws.TypeOf<NotSupportedException>());
Assert.That(expressionBranchB.ComputeModelLevelEvaluableOperation([]), Is.True);
}

[Test]
public void VerifyComputeEvaluateOperation()
public void VerifyComputeResult()
{
// Null guard on subject.
Assert.That(() => ((IExpression)null).ComputeEvaluateOperation(null), Throws.TypeOf<ArgumentNullException>());
Assert.That(() => ((IExpression)null).ComputeResult(), Throws.TypeOf<ArgumentNullException>());

var expression = new Expression();

// target == null is permitted for the empty branch (base body doesn't dereference target).
Assert.That(() => expression.ComputeEvaluateOperation(null), Throws.Nothing);

// Empty: no IResultExpressionMembership in ownedFeatureMembership → returns empty list.
Assert.That(expression.ComputeEvaluateOperation(null), Is.Empty);
// Empty: no featureMembership → null.
Assert.That(expression.ComputeResult(), Is.Null);

// Discrimination: FeatureMembership with non-ResultExpressionMembership type → still empty.
var plainMembership = new FeatureMembership();
// Negative: FeatureMembership with non-ReturnParameterMembership → null.
var featureMembership = new FeatureMembership();
var plainFeature = new Feature();
expression.AssignOwnership(plainMembership, plainFeature);

Assert.That(expression.ComputeEvaluateOperation(null), Is.Empty);

// STUB-BLOCKER: wire a ResultExpressionMembership → the production code calls
// resultExpressionMembership.ownedResultExpression, which dispatches to
// ResultExpressionMembershipExtensions.ComputeOwnedResultExpression, which is a stub
// that throws NotSupportedException. Assert the stub propagates.
var expressionWithRem = new Expression();
var resultExprMembership = new ResultExpressionMembership();
var innerExpression = new Expression();
expressionWithRem.AssignOwnership(resultExprMembership, innerExpression);

// ResultExpressionMembershipExtensions.ComputeOwnedResultExpression is a stub — NSE expected.
Assert.That(
() => expressionWithRem.ComputeEvaluateOperation(null),
Throws.TypeOf<NotSupportedException>());
}

[Test]
public void VerifyComputeCheckConditionOperation()
{
// Null guard on subject.
Assert.That(() => ((IExpression)null).ComputeCheckConditionOperation(null), Throws.TypeOf<ArgumentNullException>());
expression.AssignOwnership(featureMembership, plainFeature);

var expression = new Expression();
Assert.That(expression.ComputeResult(), Is.Null);

// target == null: forwarded to Evaluate; empty-resultExprs branch returns [] → false (Count != 1).
Assert.That(expression.ComputeCheckConditionOperation(null), Is.False);
// Positive: one ReturnParameterMembership with ownedMemberParameter set → that Feature returned.
var resultFeature = new Feature();
var returnParamMembership = new ReturnParameterMembership();
expression.AssignOwnership(returnParamMembership, resultFeature);

// Empty: Evaluate returns [] (no ResultExpressionMembership) → false (Count != 1).
Assert.That(expression.ComputeCheckConditionOperation(null), Is.False);
Assert.That(expression.ComputeResult(), Is.SameAs(resultFeature));

// STUB-BLOCKER: wire a ResultExpressionMembership → Evaluate delegates to ComputeEvaluateOperation,
// which accesses resultExpressionMembership.ownedResultExpression →
// ResultExpressionMembershipExtensions.ComputeOwnedResultExpression is a stub → NSE propagates.
var expressionWithRem = new Expression();
var resultExprMembership = new ResultExpressionMembership();
var innerExpression = new Expression();
expressionWithRem.AssignOwnership(resultExprMembership, innerExpression);
// Multiple ReturnParameterMemberships (illegal per OCL validation but tolerated) → first returned.
var secondResultFeature = new Feature();
var secondReturnParamMembership = new ReturnParameterMembership();
expression.AssignOwnership(secondReturnParamMembership, secondResultFeature);

// ResultExpressionMembershipExtensions.ComputeOwnedResultExpression is a stub — NSE expected.
Assert.That(
() => expressionWithRem.ComputeCheckConditionOperation(null),
Throws.TypeOf<NotSupportedException>());
Assert.That(expression.ComputeResult(), Is.SameAs(resultFeature));
}
}
}
Loading
Loading