Skip to content
Open
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
24 changes: 22 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ SysML2.NET is a .NET C# SDK implementing the OMG SysML v2 specification. It prov
|---|---|
| **KerML 1.0** | abstract syntax (metaclasses, properties, multiplicities, ordering, redefinitions), derived properties/operations (OCL), invariants, implied relationships (§8.4.2) |
| **SysML v2.0** | the systems layer on top of KerML — plus the textual concrete syntax |
| **Systems Modeling API & Services 1.0** | PIM types and services, REST/HTTP binding (`SysML2.NET.REST`, `PIM/`, `SysML2.NET.Serializer.Dictionary`) |
| **Systems Modeling API & Services 1.0** | the PIM types and services (`PIM/`, hand-written from `ptc/25-02-29`) and the REST/HTTP PSM that binds them (`PSM/`, generated from `ptc/25-02-30`), plus `SysML2.NET.REST` and `SysML2.NET.Serializer.Dictionary` — see "PIM vs PSM" |
| **Model interchange** | the project/archive interchange format (`SysML2.NET.Kpar`) |

Rules that hold at every layer:
Expand Down Expand Up @@ -107,7 +107,8 @@ SysML2.NET (core: netstandard2.1)
├── Core/POCO/ - Hand-coded: IContainedElement, IContainedRelationship
├── Extend/ - Auto-generated extension methods per metaclass
├── Decorators/ - [Class], [Property], [Implements] attributes
├── PIM/ - Platform-Independent Model DTOs (REST API types)
├── PIM/ - Platform-Independent API model, hand-written (DTO/ + POCO/)
├── PSM/ - REST/HTTP Platform-Specific model, generated from the OpenAPI
├── ModelInterchange/ - Archive/project interchange types (kpar)
└── Common/IData.cs - Base interface with Id property

Expand All @@ -129,6 +130,25 @@ Each metaclass exists twice: **DTO** (lightweight, relationships by `Guid`, for

Generated namespaces mirror the KerML/SysML package hierarchy: `SysML2.NET.Core.DTO.Root.Elements`, `…Core.Types`, `…Systems.Actions`.

### PIM vs PSM — two models, not two copies

The Systems Modeling API & Services specification defines a Platform Independent Model (Clause 7) and, on top of it, a REST/HTTP Platform Specific Model (Clause 8). **The solution ships both because they are different models, related by Clause 8.1.2 Table 9 — neither is a redundant copy of the other.** Mixing them produces wrong conclusions; keep the two columns below straight.

| | **PIM** — `SysML2.NET/PIM/` | **PSM** — `SysML2.NET/PSM/` |
|---|---|---|
| What it is | the platform-independent API model | the HTTP-specific binding of that model |
| Source of truth | the normative PIM UML model (OMG `ptc/25-02-29`, `Systems-Modeling-API.xmi`) plus the Clause 7 prose | `Resources/ptc-25-02-30.json`, the normative OpenAPI document |
| How it is written | **hand-written** | **code-generated** into `PSM/AutoGenDto/` — never hand-edit |
| Shape | domain shape: `Guid` / resolved references, `DTO` + `POCO` split, `[Property]` decorators | wire shape: `@id` / `@type`, nullable members, request/response families, `IRequest` / `IResponse` markers, no decorators |

Consequences that matter in practice:

- **Check a PIM fact against the PIM model, never against the OpenAPI, and vice versa.** They genuinely disagree: `Branch.head`, `Query.where`, `Record.description` and `CommitReference.deleted` are all `[1..1]` in `ptc/25-02-29` while the OpenAPI declares them nullable, and the PIM's `Query::project` is `owningProject` on the wire. A PSM divergence from the PIM is a question for the specification, not a defect in our PIM types.
- **`ptc/25-02-29` is not in the repository** — only the OpenAPI is. Fetch it from `https://www.omg.org/spec/SystemsModelingAPI/20250201/Systems-Modeling-API.xmi` when a PIM structural claim has to be verified; do not substitute the OpenAPI or the specification PDF's diagrams for it.
- **Clause 2 ranks the sources:** where the document and the machine-readable files conflict, the machine-readable files win. That is why `Query::orderBy` carries `isOrdered: false` even though the Clause 7.1.4 prose says the order is significant.
- **`[Property(xmiId: …)]` on a PIM type carries the real `xmi:id` from `ptc/25-02-29`**, so the decorator is traceable to the model. `xmiId: "sysml2.net"` means "no element in `ptc/25-02-29`" — the repo-wide placeholder for a member the source model does not define.
- Hypha does **not** cover this specification. It fetches `3-Systems_Modeling_API_and_Services.pdf` but generates no queryable spec knowledge for it, so API/Services questions are answered from the PDF and the two `ptc` artifacts, not from `hypha:spec-citation`.

## Key conventions

- **Paths are ALWAYS repo-relative — NEVER absolute**, in every artifact you author: code comments, XML docs, error messages, commit/PR/issue bodies, plan files, agent briefs. Use forward slashes (`SysML2.NET/Extend/FooExtensions.cs`). The ONLY exception is the `Read`/`Edit`/`Write` `file_path` parameter, which the tools require to be absolute.
Expand Down
12 changes: 6 additions & 6 deletions SysML2.NET.Serializer.Json.Tests/ApiSerializerTestFixture.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
// -------------------------------------------------------------------------------------------------
// <copyright file="ApiSerializerTestFixture.cs" company="Starion Group S.A.">
//
//
// Copyright 2022-2025 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 Down Expand Up @@ -73,7 +73,7 @@ private void CreateTestData()
Created = new DateTime(1976, 8, 20, 0, 0, 0, DateTimeKind.Utc),
Description = "",
OwningProject = Guid.Parse("9b0e1914-3241-461e-b9ee-a3ff5120de4e"),
PreviousCommit = Guid.Empty,
PreviousCommits = [Guid.Parse("8d3c3f31-2b2f-4f4b-8f35-6b2e0f4a4a11")],
ResourceIdentifier = "http://www.stariongroup.eu/commit",
};

Expand Down
50 changes: 41 additions & 9 deletions SysML2.NET.Serializer.Json.Tests/DeSerializerTestFixture.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
// -------------------------------------------------------------------------------------------------
// <copyright file="DeSerializerTestFixture.cs" company="Starion Group S.A.">
//
//
// Copyright 2022-2025 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 Down Expand Up @@ -64,14 +64,28 @@ public class DeSerializerTestFixture
/// </remarks>
private const int LargePayloadElementCount = 4000;

/// <summary>
/// A merge commit json object that names two immediately preceding commits
/// </summary>
private const string MergeCommit = """[{"@id":"6d7ad9fd-6520-4ff2-885b-8c5c129e6c27","@type":"Commit","created":"2023-03-13T17:53:59.111354-04:00","description":null,"owningProject":{"@id":"000e9890-6935-43e6-a5d7-5d7cac601f4c"},"previousCommit":[{"@id":"1e4c2a06-0f2f-4a3c-9d64-0f4b2f6d9a11"},{"@id":"2b7d5e18-6a4f-4d1e-8c32-7d5a1c8e4b22"}]}]""";

/// <summary>
/// The identifiers that the <see cref="MergeCommit"/> payload names as preceding commits
/// </summary>
private static readonly Guid[] MergeCommitPredecessors =
[
Guid.Parse("1e4c2a06-0f2f-4a3c-9d64-0f4b2f6d9a11"),
Guid.Parse("2b7d5e18-6a4f-4d1e-8c32-7d5a1c8e4b22")
];

private DeSerializer deSerializer;

[SetUp]
public void SetUp()
{
this.deSerializer = new DeSerializer();
}

[Test]
[TestCase(false)]
[TestCase(true)]
Expand Down Expand Up @@ -144,7 +158,7 @@ public async Task Verify_that_iData_from_sysmlcore_json_can_be_deserialized_asyn
Assert.That(feature.DeclaredShortName, Is.Null);
Assert.That(feature.Direction, Is.EqualTo(FeatureDirectionKind.Out));
}

cts.Dispose();
}

Expand Down Expand Up @@ -209,7 +223,7 @@ public void Verify_that_particular_project_and_commits_from_restapi_json_can_be_
using (Assert.EnterMultipleScope())
{
Assert.That(firstCommit.OwningProject, Is.EqualTo(Guid.Parse("000e9890-6935-43e6-a5d7-5d7cac601f4c")));
Assert.That(firstCommit.PreviousCommit, Is.EqualTo(Guid.Empty));
Assert.That(firstCommit.PreviousCommits, Has.Count.EqualTo(0));
Assert.That(firstCommit.Description, Is.Null);
Assert.That(firstCommit.Created, Is.EqualTo(DateTime.Parse("2023-03-13T17:53:59.111354-04:00", CultureInfo.InvariantCulture)));
}
Expand All @@ -232,12 +246,30 @@ public void Verify_that_particular_project_and_particular_commit_from_restapi_js
using (Assert.EnterMultipleScope())
{
Assert.That(firstCommit.OwningProject, Is.EqualTo(Guid.Parse("000e9890-6935-43e6-a5d7-5d7cac601f4c")));
Assert.That(firstCommit.PreviousCommit, Is.EqualTo(Guid.Empty));
Assert.That(firstCommit.PreviousCommits, Has.Count.EqualTo(0));
Assert.That(firstCommit.Description, Is.Null);
Assert.That(firstCommit.Created, Is.EqualTo(DateTime.Parse("2023-03-13T17:53:59.111354-04:00", CultureInfo.InvariantCulture)));
}
}

[Test]
[TestCase(false)]
[TestCase(true)]
public void Verify_that_a_merge_commit_from_restapi_json_can_be_deserialized(bool shouldDeserializeDerivedProperties)
{
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(MergeCommit));

var data = this.deSerializer.DeSerialize(stream, SerializationModeKind.JSON, SerializationTargetKind.PSM, shouldDeserializeDerivedProperties);

var mergeCommit = data.OfType<Commit>().Single();

using (Assert.EnterMultipleScope())
{
Assert.That(mergeCommit.PreviousCommits, Is.EquivalentTo(MergeCommitPredecessors));
Assert.That(mergeCommit.OwningProject, Is.EqualTo(Guid.Parse("000e9890-6935-43e6-a5d7-5d7cac601f4c")));
}
}

[Test]
[TestCase(false)]
[TestCase(true)]
Expand Down
19 changes: 9 additions & 10 deletions SysML2.NET.Serializer.Json/PIM/CommitDeSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
}
else
{
logger.LogDebug($"the description Json property was not found in the Commit: {dtoInstance.Id}");

Check warning on line 120 in SysML2.NET.Serializer.Json/PIM/CommitDeSerializer.cs

View workflow job for this annotation

GitHub Actions / Build

Don't use string interpolation in logging message templates.
}

if (jsonElement.TryGetProperty("owningProject"u8, out JsonElement owningProjectProperty))
Expand All @@ -140,23 +140,22 @@
}
else
{
logger.LogDebug($"the owningProject Json property was not found in the Commit: {dtoInstance.Id}");

Check warning on line 143 in SysML2.NET.Serializer.Json/PIM/CommitDeSerializer.cs

View workflow job for this annotation

GitHub Actions / Build

Don't use string interpolation in logging message templates.
}

if (jsonElement.TryGetProperty("previousCommit"u8, out JsonElement previousCommitProperty))
{
if (previousCommitProperty.ValueKind == JsonValueKind.Null)
if (previousCommitProperty.ValueKind == JsonValueKind.Array)
{
dtoInstance.PreviousCommit = Guid.Empty;
}
else
{
if (previousCommitProperty.TryGetProperty("@id"u8, out JsonElement previousCommitPropertyIdProperty))
foreach (var previousCommitItem in previousCommitProperty.EnumerateArray())
{
var propertyValue = previousCommitPropertyIdProperty.GetString();
if (propertyValue != null)
if (previousCommitItem.TryGetProperty("@id"u8, out JsonElement previousCommitItemIdProperty))
{
dtoInstance.PreviousCommit = Guid.Parse(propertyValue);
var propertyValue = previousCommitItemIdProperty.GetString();
if (propertyValue != null)
{
dtoInstance.PreviousCommits.Add(Guid.Parse(propertyValue));
}
}
}
}
Expand All @@ -165,7 +164,7 @@
{
if (logger.IsEnabled(LogLevel.Debug))
{
logger.LogDebug("the owningProject Json property was not found in the Commit: {Id}", dtoInstance.Id);
logger.LogDebug("the previousCommit Json property was not found in the Commit: {Id}", dtoInstance.Id);
}
}

Expand Down
14 changes: 11 additions & 3 deletions SysML2.NET.Serializer.Json/PIM/CommitSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,17 @@ internal static void Serialize(object obj, Utf8JsonWriter writer, SerializationM
writer.WriteString("@id"u8, commit.OwningProject);
writer.WriteEndObject();
writer.WriteString("resourceIdentifier"u8, commit.ResourceIdentifier);
writer.WriteStartObject("previousCommit"u8);
writer.WriteString("@id"u8, commit.PreviousCommit);
writer.WriteEndObject();
writer.WriteStartArray("previousCommit"u8);
if (commit.PreviousCommits != null)
{
foreach (var item in commit.PreviousCommits)
{
writer.WriteStartObject();
writer.WriteString("@id"u8, item);
writer.WriteEndObject();
}
}
writer.WriteEndArray();
writer.WriteEndObject();

break;
Expand Down
5 changes: 4 additions & 1 deletion SysML2.NET/PIM/DTO/Branch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,20 @@ namespace SysML2.NET.PIM.DTO
{
using System;

using SysML2.NET.Decorators;

/// <summary>
/// A <see cref="Branch"/> is a type of CommitReference. A <see cref="Branch"/> is a pointer to a <see cref="Commit"/> (Branch.head).
/// The <see cref="Commit"/> history of a <see cref="Project"/> on a given branch can be computed by recursively navigating
/// <see cref="Commit.PreviousCommit"/>, starting from the head <see cref="Commit"/> of the <see cref="Branch"/> (Branch.head)
/// <see cref="Commit.PreviousCommits"/>, starting from the head <see cref="Commit"/> of the <see cref="Branch"/> (Branch.head)
/// </summary>
public class Branch : CommitReference
{
/// <summary>
/// Gets or sets the commit to which the branch is currently pointing. It represents the latest state of the
/// <see cref="Project"/> on the given branch
/// </summary>
[Property(xmiId: "_19_0_4_58901f1_1614695877725_676676_515", aggregation: AggregationKind.None, lowerValue: 1, upperValue: 1, isOrdered: false, isReadOnly: false, isDerived: false, isDerivedUnion: false, isUnique: true, defaultValue: null)]
public Guid Head { get; set; }
}
}
32 changes: 28 additions & 4 deletions SysML2.NET/PIM/DTO/Commit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ namespace SysML2.NET.PIM.DTO
using System;
using System.Collections.Generic;

using SysML2.NET.Decorators;
using SysML2.NET.PIM;

/// <summary>
Expand All @@ -31,9 +32,9 @@ namespace SysML2.NET.PIM.DTO
/// a <see cref="Project"/>. A <see cref="Project"/> has 0 or more <see cref="Commit"/>s
/// </summary>
/// <remarks>
/// <see cref="Commit"/>s are immutable. For a given <see cref="Commit"/> record, the value of <c>change</c>
/// <see cref="Commit"/>s are immutable. For a given <see cref="Commit"/> record, the value of <see cref="Change"/>
/// cannot be modified after a <see cref="Commit"/> has been created. If a modification is required, a new <see cref="Commit"/>
/// record can be created with a different value of <c>change</c>.
/// record can be created with a different value of <see cref="Change"/>.
/// <see cref="Commit"/>s are not destructible1. A <see cref="Commit"/> record cannot be deleted during normal
/// end-user operation. <see cref="Commit"/>s represent the history and evolution of a <see cref="Project"/>.
/// Deleting and mutating <see cref="Commit"/> records must be disabled for the normal end-user operations
Expand All @@ -44,16 +45,39 @@ public class Commit : Record
/// <summary>
/// Gets or sets the timestamp at which the <see cref="Commit"/> was created
/// </summary>
[Property(xmiId: "_19_0_2_12e503d9_1577820469727_54677_66063", aggregation: AggregationKind.None, lowerValue: 1, upperValue: 1, isOrdered: false, isReadOnly: false, isDerived: false, isDerivedUnion: false, isUnique: true, defaultValue: null)]
public DateTime Created { get; set; }

/// <summary>
/// Gets or sets the <see cref="Project"/> that owns the <see cref="Commit"/>.
/// </summary>
[Property(xmiId: "_19_0_2_12e503d9_1577820469728_610810_66065", aggregation: AggregationKind.None, lowerValue: 1, upperValue: 1, isOrdered: false, isReadOnly: false, isDerived: false, isDerivedUnion: false, isUnique: true, defaultValue: null)]
public Guid OwningProject { get; set; }

/// <summary>
/// Gets or sets the immediately preceding <see cref="Commit"/>s
/// Gets or sets the set of immediately preceding <see cref="Commit"/>s
/// </summary>
public Guid PreviousCommit { get; set; }
[Property(xmiId: "_19_0_2_12e503d9_1577820469727_74119_66064", aggregation: AggregationKind.None, lowerValue: 0, upperValue: int.MaxValue, isOrdered: false, isReadOnly: false, isDerived: false, isDerivedUnion: false, isUnique: true, defaultValue: null)]
public List<Guid> PreviousCommits { get; set; } = [];

/// <summary>
/// Gets or sets the set of <see cref="DataVersion"/> records representing data that is created, updated,
/// or deleted in the <see cref="Commit"/>
/// </summary>
/// <remarks>
/// A deletion is recorded by a <see cref="DataVersion"/> whose payload is <c>null</c>.
/// </remarks>
[Property(xmiId: "_19_0_2_12e503d9_1577820469724_226337_66058", aggregation: AggregationKind.None, lowerValue: 1, upperValue: int.MaxValue, isOrdered: false, isReadOnly: false, isDerived: false, isDerivedUnion: false, isUnique: true, defaultValue: null)]
public List<Guid> Change { get; set; } = [];

/// <summary>
/// Gets or sets the set of cumulative <see cref="DataVersion"/> records in a <see cref="Project"/> at the <see cref="Commit"/>
/// </summary>
/// <remarks>
/// Derived by Systems Modeling API and Services, Clause 7.1.2 from <see cref="Change"/> and the
/// versioned data of <see cref="PreviousCommits"/>.
/// </remarks>
[Property(xmiId: "_19_0_4_58901f1_1629496059367_814933_41", aggregation: AggregationKind.None, lowerValue: 0, upperValue: int.MaxValue, isOrdered: false, isReadOnly: false, isDerived: true, isDerivedUnion: false, isUnique: true, defaultValue: null)]
public List<Guid> versionedData { get; internal set; } = [];
}
}
Loading
Loading