Skip to content

Commit ce9bf55

Browse files
1 parent a02e44f commit ce9bf55

13 files changed

Lines changed: 485 additions & 295 deletions

.claude/commands/implement-extensions-batch.md

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
---
2-
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
2+
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
33
argument-hint: <file1.cs> <file2.cs> [<file3.cs> ...] (2–6 Extension file names; each will be normalised to SysML2.NET/Extend/<Foo>Extensions.cs)
44
---
55

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

141-
Create:
143+
Create locally **and immediately publish to `origin` with upstream tracking**:
142144
```bash
143145
git switch -c <branch-name> origin/development
146+
git push -u origin <branch-name>
144147
```
145148

149+
The push lifts the branch onto the remote at the same commit as
150+
`origin/development` (no diff yet — that comes after the batch's edits + the
151+
user's commit). Setting upstream now means:
152+
- The user's eventual `git push` after committing needs no flags.
153+
- A pull request can be opened via the GitHub UI or `gh pr create` as soon as
154+
the user pushes their first commit, without an additional `git push -u`
155+
step.
156+
157+
If the `git push -u` fails (network, auth, branch-protection refusing empty
158+
pushes), log the failure but **continue with the batch**. The implementation
159+
work is the main goal; the branch will still exist locally and the user can
160+
re-push manually at the end. Surface the failure clearly in the final summary.
161+
146162
Refuse if the branch already exists locally OR on origin (`git ls-remote
147163
--exit-code origin <branch>`) — ask the user to pick a different batch or delete
148164
the stale branch.
@@ -253,7 +269,10 @@ step-11 logic from `/implement-extensions`:
253269

254270
Print to the user:
255271

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

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

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

275297
## Failure handling
276298

@@ -281,6 +303,7 @@ Print to the user:
281303
| Ambiguous issue | Step 2 | `AskUserQuestion` for an explicit issue number per file. |
282304
| Dirty working tree | Step 4 | Abort, ask user to commit/stash. |
283305
| Branch already exists | Step 6 | Abort; ask user to pick a different batch or delete the stale branch. |
306+
| `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). |
284307
| `gh issue edit --add-assignee` fails for one issue | Step 7 | Log + continue (non-blocking; implementation still proceeds). |
285308
| Production build fails after implementer | Step 10.1 | Attribute to the file, re-dispatch that implementer. |
286309
| Targeted test fails | Step 10.2 | Attribute (OCL vs test bug), re-dispatch correct role. |

SysML2.NET.Tests/Extend/ExpressionExtensionsTestFixture.cs

Lines changed: 90 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
// -------------------------------------------------------------------------------------------------
22
// <copyright file="ExpressionExtensionsTestFixture.cs" company="Starion Group S.A.">
3-
//
3+
//
44
// Copyright 2022-2026 Starion Group S.A.
5-
//
5+
//
66
// Licensed under the Apache License, Version 2.0 (the "License");
77
// you may not use this file except in compliance with the License.
88
// You may obtain a copy of the License at
9-
//
9+
//
1010
// http://www.apache.org/licenses/LICENSE-2.0
11-
//
11+
//
1212
// Unless required by applicable law or agreed to in writing, software
1313
// distributed under the License is distributed on an "AS IS" BASIS,
1414
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1515
// See the License for the specific language governing permissions and
1616
// limitations under the License.
17-
//
17+
//
1818
// </copyright>
1919
// ------------------------------------------------------------------------------------------------
2020

@@ -28,15 +28,73 @@ namespace SysML2.NET.Tests.Extend
2828
using SysML2.NET.Core.Core.Types;
2929
using SysML2.NET.Core.POCO.Core.Features;
3030
using SysML2.NET.Core.POCO.Core.Types;
31-
using SysML2.NET.Core.POCO.Kernel.Expressions;
3231
using SysML2.NET.Core.POCO.Kernel.FeatureValues;
3332
using SysML2.NET.Core.POCO.Kernel.Functions;
34-
using SysML2.NET.Core.POCO.Root.Elements;
3533
using SysML2.NET.Extensions;
3634

35+
using Type = SysML2.NET.Core.POCO.Core.Types.Type;
36+
3737
[TestFixture]
3838
public class ExpressionExtensionsTestFixture
3939
{
40+
[Test]
41+
public void VerifyComputeCheckConditionOperation()
42+
{
43+
// Null guard on subject.
44+
Assert.That(() => ((IExpression)null).ComputeCheckConditionOperation(null), Throws.TypeOf<ArgumentNullException>());
45+
46+
var expression = new Expression();
47+
48+
// target == null: forwarded to Evaluate; empty-resultExprs branch returns [] → false (Count != 1).
49+
Assert.That(expression.ComputeCheckConditionOperation(null), Is.False);
50+
51+
// Empty: Evaluate returns [] (no ResultExpressionMembership) → false (Count != 1).
52+
Assert.That(expression.ComputeCheckConditionOperation(null), Is.False);
53+
54+
// Populated case: ResultExpressionMembership wired — Evaluate delegates through the inner
55+
// (empty) Expression and returns the empty list. CheckCondition requires Count == 1 AND
56+
// a LiteralBoolean { Value: true }, so it returns false.
57+
var expressionWithRem = new Expression();
58+
var resultExprMembership = new ResultExpressionMembership();
59+
var innerExpression = new Expression();
60+
expressionWithRem.AssignOwnership(resultExprMembership, innerExpression);
61+
62+
Assert.That(expressionWithRem.ComputeCheckConditionOperation(null), Is.False);
63+
}
64+
65+
[Test]
66+
public void VerifyComputeEvaluateOperation()
67+
{
68+
// Null guard on subject.
69+
Assert.That(() => ((IExpression)null).ComputeEvaluateOperation(null), Throws.TypeOf<ArgumentNullException>());
70+
71+
var expression = new Expression();
72+
73+
// target == null is permitted for the empty branch (base body doesn't dereference target).
74+
Assert.That(() => expression.ComputeEvaluateOperation(null), Throws.Nothing);
75+
76+
// Empty: no IResultExpressionMembership in ownedFeatureMembership → returns empty list.
77+
Assert.That(expression.ComputeEvaluateOperation(null), Is.Empty);
78+
79+
// Discrimination: FeatureMembership with non-ResultExpressionMembership type → still empty.
80+
var plainMembership = new FeatureMembership();
81+
var plainFeature = new Feature();
82+
expression.AssignOwnership(plainMembership, plainFeature);
83+
84+
Assert.That(expression.ComputeEvaluateOperation(null), Is.Empty);
85+
86+
// Populated case: ResultExpressionMembership wired — the production code resolves
87+
// resultExpressionMembership.ownedResultExpression (the inner Expression) and recursively
88+
// calls Evaluate on it. The inner Expression is itself empty (no nested
89+
// ResultExpressionMembership), so its Evaluate returns the empty list.
90+
var expressionWithRem = new Expression();
91+
var resultExprMembership = new ResultExpressionMembership();
92+
var innerExpression = new Expression();
93+
expressionWithRem.AssignOwnership(resultExprMembership, innerExpression);
94+
95+
Assert.That(expressionWithRem.ComputeEvaluateOperation(null), Is.Empty);
96+
}
97+
4098
[Test]
4199
public void VerifyComputeFunction()
42100
{
@@ -48,7 +106,7 @@ public void VerifyComputeFunction()
48106
Assert.That(expression.ComputeFunction(), Is.Null);
49107

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

@@ -93,38 +151,6 @@ public void VerifyComputeIsModelLevelEvaluable()
93151
Assert.That(expressionWithNonImplied.ComputeIsModelLevelEvaluable(), Is.False);
94152
}
95153

96-
[Test]
97-
public void VerifyComputeResult()
98-
{
99-
Assert.That(() => ((IExpression)null).ComputeResult(), Throws.TypeOf<ArgumentNullException>());
100-
101-
var expression = new Expression();
102-
103-
// Empty: no featureMembership → null.
104-
Assert.That(expression.ComputeResult(), Is.Null);
105-
106-
// Negative: FeatureMembership with non-ReturnParameterMembership → null.
107-
var featureMembership = new FeatureMembership();
108-
var plainFeature = new Feature();
109-
expression.AssignOwnership(featureMembership, plainFeature);
110-
111-
Assert.That(expression.ComputeResult(), Is.Null);
112-
113-
// Positive: one ReturnParameterMembership with ownedMemberParameter set → that Feature returned.
114-
var resultFeature = new Feature();
115-
var returnParamMembership = new ReturnParameterMembership();
116-
expression.AssignOwnership(returnParamMembership, resultFeature);
117-
118-
Assert.That(expression.ComputeResult(), Is.SameAs(resultFeature));
119-
120-
// Multiple ReturnParameterMemberships (illegal per OCL validation but tolerated) → first returned.
121-
var secondResultFeature = new Feature();
122-
var secondReturnParamMembership = new ReturnParameterMembership();
123-
expression.AssignOwnership(secondReturnParamMembership, secondResultFeature);
124-
125-
Assert.That(expression.ComputeResult(), Is.SameAs(resultFeature));
126-
}
127-
128154
[Test]
129155
public void VerifyComputeModelLevelEvaluableOperation()
130156
{
@@ -205,85 +231,49 @@ public void VerifyComputeModelLevelEvaluableOperation()
205231

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

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

220-
Assert.That(
221-
() => expressionBranchB.ComputeModelLevelEvaluableOperation([]),
222-
Throws.TypeOf<NotSupportedException>());
244+
Assert.That(expressionBranchB.ComputeModelLevelEvaluableOperation([]), Is.True);
223245
}
224246

225247
[Test]
226-
public void VerifyComputeEvaluateOperation()
248+
public void VerifyComputeResult()
227249
{
228-
// Null guard on subject.
229-
Assert.That(() => ((IExpression)null).ComputeEvaluateOperation(null), Throws.TypeOf<ArgumentNullException>());
250+
Assert.That(() => ((IExpression)null).ComputeResult(), Throws.TypeOf<ArgumentNullException>());
230251

231252
var expression = new Expression();
232253

233-
// target == null is permitted for the empty branch (base body doesn't dereference target).
234-
Assert.That(() => expression.ComputeEvaluateOperation(null), Throws.Nothing);
235-
236-
// Empty: no IResultExpressionMembership in ownedFeatureMembership → returns empty list.
237-
Assert.That(expression.ComputeEvaluateOperation(null), Is.Empty);
254+
// Empty: no featureMembership → null.
255+
Assert.That(expression.ComputeResult(), Is.Null);
238256

239-
// Discrimination: FeatureMembership with non-ResultExpressionMembership type → still empty.
240-
var plainMembership = new FeatureMembership();
257+
// Negative: FeatureMembership with non-ReturnParameterMembership → null.
258+
var featureMembership = new FeatureMembership();
241259
var plainFeature = new Feature();
242-
expression.AssignOwnership(plainMembership, plainFeature);
243-
244-
Assert.That(expression.ComputeEvaluateOperation(null), Is.Empty);
245-
246-
// STUB-BLOCKER: wire a ResultExpressionMembership → the production code calls
247-
// resultExpressionMembership.ownedResultExpression, which dispatches to
248-
// ResultExpressionMembershipExtensions.ComputeOwnedResultExpression, which is a stub
249-
// that throws NotSupportedException. Assert the stub propagates.
250-
var expressionWithRem = new Expression();
251-
var resultExprMembership = new ResultExpressionMembership();
252-
var innerExpression = new Expression();
253-
expressionWithRem.AssignOwnership(resultExprMembership, innerExpression);
254-
255-
// ResultExpressionMembershipExtensions.ComputeOwnedResultExpression is a stub — NSE expected.
256-
Assert.That(
257-
() => expressionWithRem.ComputeEvaluateOperation(null),
258-
Throws.TypeOf<NotSupportedException>());
259-
}
260-
261-
[Test]
262-
public void VerifyComputeCheckConditionOperation()
263-
{
264-
// Null guard on subject.
265-
Assert.That(() => ((IExpression)null).ComputeCheckConditionOperation(null), Throws.TypeOf<ArgumentNullException>());
260+
expression.AssignOwnership(featureMembership, plainFeature);
266261

267-
var expression = new Expression();
262+
Assert.That(expression.ComputeResult(), Is.Null);
268263

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

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

275-
// STUB-BLOCKER: wire a ResultExpressionMembership → Evaluate delegates to ComputeEvaluateOperation,
276-
// which accesses resultExpressionMembership.ownedResultExpression →
277-
// ResultExpressionMembershipExtensions.ComputeOwnedResultExpression is a stub → NSE propagates.
278-
var expressionWithRem = new Expression();
279-
var resultExprMembership = new ResultExpressionMembership();
280-
var innerExpression = new Expression();
281-
expressionWithRem.AssignOwnership(resultExprMembership, innerExpression);
271+
// Multiple ReturnParameterMemberships (illegal per OCL validation but tolerated) → first returned.
272+
var secondResultFeature = new Feature();
273+
var secondReturnParamMembership = new ReturnParameterMembership();
274+
expression.AssignOwnership(secondReturnParamMembership, secondResultFeature);
282275

283-
// ResultExpressionMembershipExtensions.ComputeOwnedResultExpression is a stub — NSE expected.
284-
Assert.That(
285-
() => expressionWithRem.ComputeCheckConditionOperation(null),
286-
Throws.TypeOf<NotSupportedException>());
276+
Assert.That(expression.ComputeResult(), Is.SameAs(resultFeature));
287277
}
288278
}
289279
}

0 commit comments

Comments
 (0)