diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 71ecdfd33..28057a043 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -7,6 +7,12 @@
"commands": [
"reportgenerator"
]
+ },
+ "dotnet-stryker": {
+ "version": "4.16.0",
+ "commands": [
+ "stryker"
+ ]
}
}
}
diff --git a/MTConnect.NET.sln b/MTConnect.NET.sln
index 5f678ae53..b7daadfae 100644
--- a/MTConnect.NET.sln
+++ b/MTConnect.NET.sln
@@ -141,6 +141,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MTConnect.NET-Tests-Agents"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MTConnect.NET-HTTP-Tests", "tests\MTConnect.NET-HTTP-Tests\MTConnect.NET-HTTP-Tests.csproj", "{3E89B860-A428-470C-8E48-0DDABC4027F0}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MTConnect.NET-Generator-Tests", "tests\MTConnect.NET-Generator-Tests\MTConnect.NET-Generator-Tests.csproj", "{8B61CE3B-DC8A-47CE-A34B-38BC57DFFD57}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -509,6 +511,14 @@ Global
{3E89B860-A428-470C-8E48-0DDABC4027F0}.Package|Any CPU.Build.0 = Debug|Any CPU
{3E89B860-A428-470C-8E48-0DDABC4027F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3E89B860-A428-470C-8E48-0DDABC4027F0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8B61CE3B-DC8A-47CE-A34B-38BC57DFFD57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8B61CE3B-DC8A-47CE-A34B-38BC57DFFD57}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8B61CE3B-DC8A-47CE-A34B-38BC57DFFD57}.Docker|Any CPU.ActiveCfg = Debug|Any CPU
+ {8B61CE3B-DC8A-47CE-A34B-38BC57DFFD57}.Docker|Any CPU.Build.0 = Debug|Any CPU
+ {8B61CE3B-DC8A-47CE-A34B-38BC57DFFD57}.Package|Any CPU.ActiveCfg = Debug|Any CPU
+ {8B61CE3B-DC8A-47CE-A34B-38BC57DFFD57}.Package|Any CPU.Build.0 = Debug|Any CPU
+ {8B61CE3B-DC8A-47CE-A34B-38BC57DFFD57}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8B61CE3B-DC8A-47CE-A34B-38BC57DFFD57}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -565,6 +575,7 @@ Global
{17E64F59-0E62-4FCE-BEC4-EABBCF95B9A2} = {BBF53739-168D-4635-8595-083AC0C65E4C}
{AE09D1CA-5572-40BF-B984-74230E8634E1} = {14375E03-6BF8-45E6-B868-D2399368992B}
{3E89B860-A428-470C-8E48-0DDABC4027F0} = {14375E03-6BF8-45E6-B868-D2399368992B}
+ {8B61CE3B-DC8A-47CE-A34B-38BC57DFFD57} = {14375E03-6BF8-45E6-B868-D2399368992B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {CC13D3AD-18BF-4695-AB2A-087EF0885B20}
diff --git a/build/MTConnect.NET-DocsGen/CliInventory.cs b/build/MTConnect.NET-DocsGen/CliInventory.cs
index e6ca63b0e..a507d5218 100644
--- a/build/MTConnect.NET-DocsGen/CliInventory.cs
+++ b/build/MTConnect.NET-DocsGen/CliInventory.cs
@@ -342,10 +342,15 @@ private static CliInfo CollectDotNetTool(string name, string file, string repoRo
if (headerDescs.TryGetValue(flagName, out var headerDesc)) desc = headerDesc;
desc ??= ExtractDotnetFlagDescription(text, flagName);
- // Detect whether the case body calls `RequireValue` — if it
- // does, the flag takes a value.
+ // Detect whether the case body calls `RequireValue` — if it does,
+ // the flag takes a value. The scan is bounded to the CURRENT case
+ // block only: it stops at the next `case "…":` label, a `default:`
+ // label, or a `break;` terminator, so a boolean flag whose case
+ // body sits above a value-taking case (like `--full-tree` above
+ // `case "--output": … RequireValue(…)`) does not falsely inherit
+ // the neighbour's value shape.
bool takesValue = Regex.IsMatch(text,
- $@"case\s+""{Regex.Escape(flagName)}""\s*:[\s\S]{{0,200}}?RequireValue");
+ $@"case\s+""{Regex.Escape(flagName)}""\s*:(?:(?!\s*case\s+""|\s*default\s*:|\bbreak\s*;)[\s\S])*?RequireValue");
flags.Add(new CliFlag(
Name: flagName,
Short: null,
diff --git a/build/MTConnect.NET-SysML-Import/CSharp/EnumModel.cs b/build/MTConnect.NET-SysML-Import/CSharp/EnumModel.cs
index be6b5358a..d8c83a92e 100644
--- a/build/MTConnect.NET-SysML-Import/CSharp/EnumModel.cs
+++ b/build/MTConnect.NET-SysML-Import/CSharp/EnumModel.cs
@@ -93,7 +93,7 @@ public string RenderModel()
public string RenderDescriptions()
{
if (Values == null || Values.Count == 0) return null;
- var template = TemplateLoader.LoadOrThrow("CSharp", "Templates", "EnumDescriptions.scriban");
+ var template = TemplateLoader.LoadOrThrow("CSharp", "Templates", "EnumOrStringDescriptions.scriban");
return template.Render(this);
}
}
diff --git a/build/MTConnect.NET-SysML-Import/CSharp/EnumStringModel.cs b/build/MTConnect.NET-SysML-Import/CSharp/EnumStringModel.cs
index 654dacf1c..1e88f9c3c 100644
--- a/build/MTConnect.NET-SysML-Import/CSharp/EnumStringModel.cs
+++ b/build/MTConnect.NET-SysML-Import/CSharp/EnumStringModel.cs
@@ -14,6 +14,14 @@ internal class EnumStringModel : MTConnectEnumModel, ITemplateModel
public bool IsPartial { get; set; }
+ // Consumed by the Shape-B consolidated EnumOrStringDescriptions.scriban
+ // template: gates the class-doc wording, the Get(...) overload's
+ // parameter type (string vs. enum-typed), and the Get(...) doc summary.
+ // EnumModel and ObservationModel do NOT expose this — Scriban resolves
+ // a missing member as null (falsy), producing the enum-shape emission
+ // for those two callers.
+ public bool IsString => true;
+
public EnumStringModel() { }
@@ -88,7 +96,7 @@ public string RenderModel()
public string RenderDescriptions()
{
- var template = TemplateLoader.LoadOrThrow("CSharp", "Templates", "EnumStringDescriptions.scriban");
+ var template = TemplateLoader.LoadOrThrow("CSharp", "Templates", "EnumOrStringDescriptions.scriban");
return template.Render(this);
}
}
diff --git a/build/MTConnect.NET-SysML-Import/CSharp/MeasurementModel.cs b/build/MTConnect.NET-SysML-Import/CSharp/MeasurementModel.cs
index 5ac65a18f..2c9c01342 100644
--- a/build/MTConnect.NET-SysML-Import/CSharp/MeasurementModel.cs
+++ b/build/MTConnect.NET-SysML-Import/CSharp/MeasurementModel.cs
@@ -66,7 +66,11 @@ public string RenderModel()
}
///
- public string RenderInterface() => null;
+ public string RenderInterface()
+ {
+ var template = TemplateLoader.LoadOrThrow("CSharp", "Templates", "Pallets.MeasurementInterface.scriban");
+ return template.Render(this);
+ }
///
public string RenderDescriptions() => null;
diff --git a/build/MTConnect.NET-SysML-Import/CSharp/ObservationModel.cs b/build/MTConnect.NET-SysML-Import/CSharp/ObservationModel.cs
index b788e2a0a..d89a4f260 100644
--- a/build/MTConnect.NET-SysML-Import/CSharp/ObservationModel.cs
+++ b/build/MTConnect.NET-SysML-Import/CSharp/ObservationModel.cs
@@ -72,7 +72,7 @@ public string RenderModel()
///
public string RenderDescriptions()
{
- var template = TemplateLoader.LoadOrThrow("CSharp", "Templates", "EnumDescriptions.scriban");
+ var template = TemplateLoader.LoadOrThrow("CSharp", "Templates", "EnumOrStringDescriptions.scriban");
return template.Render(this);
}
}
diff --git a/build/MTConnect.NET-SysML-Import/CSharp/TemplateRenderer.cs b/build/MTConnect.NET-SysML-Import/CSharp/TemplateRenderer.cs
index 8b9bbec83..010b497e9 100644
--- a/build/MTConnect.NET-SysML-Import/CSharp/TemplateRenderer.cs
+++ b/build/MTConnect.NET-SysML-Import/CSharp/TemplateRenderer.cs
@@ -643,20 +643,19 @@ private static void MarkInheritedProperties(
break;
case "Assets.CuttingTools.ToolingMeasurement":
- // ToolingMeasurement extends `Measurement` (the
- // CuttingTools abstract Measurement base, NOT
- // Assets.Pallet.Measurement). The CuttingTools
- // Measurement.g.cs is hand-maintained / frozen —
- // not produced by any current renderer flow — so
- // it never enters the export-side ClassModel
- // graph the inheritance walk traverses, and a
- // Name-only lookup of "Measurement" resolves to
- // Pallet.Measurement (which lacks Code). Class
- // side only — IMeasurement.g.cs has `Code`
- // commented out, so the interface child does NOT
- // hide anything and emitting `new` there would
- // produce CS0109 instead.
- classOnlyNames.Add("Code");
+ // No hand-stitched inheritance seed needed. The
+ // Assets.CuttingTools.Measurement base IS produced
+ // by the current renderer flow (via
+ // MTConnectAssetInformationModel.ParseAssetInformationModel's
+ // sharedMeasurement injection which imports the
+ // Pallet Measurement class under Assets.CuttingTools),
+ // so the export-side ClassModel graph already carries
+ // its property list. The Pallet Measurement lacks
+ // Code, and the interface IMeasurement.g.cs likewise
+ // has Code commented out — hence emitting `new` on
+ // ToolingMeasurement.Code would raise CS0109 on both
+ // the class and interface sides. Fall through to the
+ // default inheritance walk with no override.
break;
}
diff --git a/build/MTConnect.NET-SysML-Import/CSharp/Templates/EnumDescriptions.scriban b/build/MTConnect.NET-SysML-Import/CSharp/Templates/EnumOrStringDescriptions.scriban
similarity index 53%
rename from build/MTConnect.NET-SysML-Import/CSharp/Templates/EnumDescriptions.scriban
rename to build/MTConnect.NET-SysML-Import/CSharp/Templates/EnumOrStringDescriptions.scriban
index 16691ed33..23d908b3d 100644
--- a/build/MTConnect.NET-SysML-Import/CSharp/Templates/EnumDescriptions.scriban
+++ b/build/MTConnect.NET-SysML-Import/CSharp/Templates/EnumOrStringDescriptions.scriban
@@ -1,10 +1,15 @@
// Copyright (c) 2024 TrakHound Inc., All Rights Reserved.
// TrakHound Inc. licenses this file to you under the MIT license.
+{{-# Shape-B consolidated Descriptions template. Valid for every MTConnect version. #}}
+{{-# Covers both the enum-descriptions and the string-constant-descriptions callers. #}}
+{{-# When is_string is truthy, the Get(...) overload takes a `string value` and the #}}
+{{-# class doc reads "string constant" instead of "value"; when falsy, the Get(...) #}}
+{{-# overload takes an enum-typed value and the class doc reads "value". #}}
namespace {{namespace}}
{
///
- /// Description text for each value as defined by the MTConnect Standard.
+ /// Description text for each {{ if is_string }}string constant{{ else }}value{{ end }} as defined by the MTConnect Standard.
///
public static class {{name}}Descriptions
{
@@ -21,9 +26,9 @@ namespace {{namespace}}
///
- /// Returns the MTConnect Standard description text for the specified value, or null when none is defined.
+ /// Returns the MTConnect Standard description text for the specified{{ if is_string }}{{ else }} {{ end }} value, or null when none is defined.
///
- public static string Get({{name}} value)
+ public static string Get({{ if is_string }}string{{ else }}{{name}}{{ end }} value)
{
switch (value)
{
diff --git a/build/MTConnect.NET-SysML-Import/CSharp/Templates/EnumStringDescriptions.scriban b/build/MTConnect.NET-SysML-Import/CSharp/Templates/EnumStringDescriptions.scriban
deleted file mode 100644
index f85216280..000000000
--- a/build/MTConnect.NET-SysML-Import/CSharp/Templates/EnumStringDescriptions.scriban
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (c) 2024 TrakHound Inc., All Rights Reserved.
-// TrakHound Inc. licenses this file to you under the MIT license.
-
-namespace {{namespace}}
-{
- ///
- /// Description text for each string constant as defined by the MTConnect Standard.
- ///
- public static class {{name}}Descriptions
- {
-{{- i = 0 }}{{- for value in values }}{{ i = i + 1 }}
- ///
- /// {{value.description}}
- ///
- public const string {{value.name}} = "{{value.description}}";
- {{- if (i < (values | array.size)) }}
- {{ end }}
-{{- end }}
-
-{{- if ((values | array.size) > 0) }}{{ i = 0 }}
-
-
- ///
- /// Returns the MTConnect Standard description text for the specified value, or null when none is defined.
- ///
- public static string Get(string value)
- {
- switch (value)
- {
-{{- for value in values }}{{ i = i + 1 }}
- case {{name}}.{{value.name}}: return "{{value.description}}";
-{{- end }}
- }
-
- return null;
- }
-{{- end }}
- }
-}
\ No newline at end of file
diff --git a/build/MTConnect.NET-SysML-Import/CSharp/Templates/Pallets.MeasurementInterface.scriban b/build/MTConnect.NET-SysML-Import/CSharp/Templates/Pallets.MeasurementInterface.scriban
new file mode 100644
index 000000000..97dcf30ea
--- /dev/null
+++ b/build/MTConnect.NET-SysML-Import/CSharp/Templates/Pallets.MeasurementInterface.scriban
@@ -0,0 +1,12 @@
+// Copyright (c) 2025 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+namespace {{namespace}}
+{
+ ///
+ /// {{description}}
+ ///
+ public interface I{{name}} : IMeasurement
+ {
+ }
+}
\ No newline at end of file
diff --git a/build/MTConnect.NET-SysML-Import/Program.cs b/build/MTConnect.NET-SysML-Import/Program.cs
index b4ad131b7..706556438 100644
--- a/build/MTConnect.NET-SysML-Import/Program.cs
+++ b/build/MTConnect.NET-SysML-Import/Program.cs
@@ -2,39 +2,100 @@
using MTConnect.SysML.CSharp;
using MTConnect.SysML.Json_cppagent;
using MTConnect.SysML.Xml;
+using System.Diagnostics;
using System.Linq;
+using System.Text;
using System.Text.Json;
+using System.Text.RegularExpressions;
// SysML importer entry point. Runs on Linux / macOS / Windows / CI.
//
// Usage:
// dotnet run --project build/MTConnect.NET-SysML-Import \
-// -- --xmi \
+// -- --new-xmi \
// --output \
+// [--previous-xmi ] \
+// [--compat-version-label ] \
+// [--full-tree] \
// [--json-dump ]
//
// Flags:
-// --xmi SysML XMI file to consume. Required.
-// --output Repository root. Each subgenerator writes into its own
-// libraries// subtree under this root.
-// Required.
-// --json-dump Optional. Writes the parsed MTConnectModel as JSON
-// for debugging.
+// --new-xmi SysML XMI file to consume. Required. Preferred
+// spelling; --xmi remains as a legacy alias.
+// --xmi Legacy alias for --new-xmi. Kept for backwards
+// compatibility with existing callers; new invocations
+// should prefer --new-xmi.
+// --output Repository root. Each subgenerator writes into its
+// own libraries// subtree under this
+// root. Required.
+// --previous-xmi Edge-case override for delta-driven mode. When
+// supplied, uses this file as the previous-version
+// XMI and skips the zero-config auto-derive step.
+// Typical use cases: cross-version audit runs,
+// regenerating against a historical XMI snapshot,
+// and version-bumps that skip a version (where
+// MTConnectVersions.Max does not match the intended
+// PREV_VERSION).
+// --compat-version-label
+// Label used for the Compat/.g.cs file name in
+// delta mode. When --previous-xmi is supplied without
+// an explicit label, defaults to "Previous" for
+// backwards compatibility. When the previous-XMI is
+// auto-derived from MTConnectVersions.Max, defaults
+// to "v${PREV_XY_UNDERSCORE}" (e.g. "v2_7").
+// --full-tree Explicit opt-in for the full-regeneration path.
+// Disables both the zero-config auto-derive delta
+// and the --previous-xmi override; every emitted
+// .g.cs re-lands under its normal path.
+// --json-dump Optional. Writes the parsed MTConnectModel as JSON
+// for debugging.
+//
+// Zero-config delta mode (default):
+// When neither --previous-xmi nor --full-tree is supplied, the importer
+// auto-derives PREV_VERSION from MTConnectVersions.Max (parsed out of
+// libraries/MTConnect.NET-Common/MTConnectVersions.cs under --output) and
+// resolves the prior-version XMI via one of:
+// Strategy B (primary): build/.cache/sysml-prev/MTConnectSysMLModel_v${PREV_VERSION}.xml
+// Strategy A (fallback): build/sysml-model/MTConnectSysMLModel.xml, gated
+// on the submodule being checked out to tag
+// v${PREV_VERSION} exactly.
+// Strategy C (fail-hard): neither resolves — throws with an actionable
+// message naming both probed paths and pointing at
+// the --previous-xmi override + the --full-tree
+// escape hatch.
//
// See build/MTConnect.NET-SysML-Import/README.md for the full usage guide,
-// the "Adding a new MTConnect Standard version" runbook, and the determinism
-// guarantee (regen against a pinned XMI tag must produce zero diff).
+// the "Adding a new MTConnect Standard version" runbook, the determinism
+// guarantee (regen against a pinned XMI tag must produce zero diff), and the
+// delta-mode design notes (plan D4 — partial-class re-emit, not
+// [TypeForwardedTo]).
-string? xmiPath = null;
+string? newXmiPath = null;
+string? previousXmiPath = null;
+string? compatVersionLabel = null;
string? outputRoot = null;
string? jsonDumpPath = null;
+bool fullTree = false;
for (int i = 0; i < args.Length; i++)
{
switch (args[i])
{
+ case "--new-xmi":
+ newXmiPath = RequireValue(args, ref i, "--new-xmi");
+ break;
case "--xmi":
- xmiPath = RequireValue(args, ref i, "--xmi");
+ // Legacy alias for --new-xmi. Preserved for backwards compatibility.
+ newXmiPath = RequireValue(args, ref i, "--xmi");
+ break;
+ case "--previous-xmi":
+ previousXmiPath = RequireValue(args, ref i, "--previous-xmi");
+ break;
+ case "--compat-version-label":
+ compatVersionLabel = RequireValue(args, ref i, "--compat-version-label");
+ break;
+ case "--full-tree":
+ fullTree = true;
break;
case "--output":
outputRoot = RequireValue(args, ref i, "--output");
@@ -53,16 +114,24 @@
}
}
-if (string.IsNullOrEmpty(xmiPath))
+if (string.IsNullOrEmpty(newXmiPath))
{
- Console.Error.WriteLine("error: --xmi is required.");
+ // Legacy callers passed --xmi; the required-flag message keeps the same
+ // spelling so grep-based operator scripts continue to match.
+ Console.Error.WriteLine("error: --new-xmi is required (legacy alias --xmi is still accepted).");
PrintHelp();
return 2;
}
-if (!File.Exists(xmiPath))
+if (!File.Exists(newXmiPath))
+{
+ Console.Error.WriteLine($"error: XMI file not found: {newXmiPath}");
+ return 1;
+}
+
+if (previousXmiPath is not null && !File.Exists(previousXmiPath))
{
- Console.Error.WriteLine($"error: XMI file not found: {xmiPath}");
+ Console.Error.WriteLine($"error: --previous-xmi file not found: {previousXmiPath}");
return 1;
}
@@ -79,33 +148,159 @@
return 1;
}
-Console.WriteLine($"XMI: {xmiPath}");
+// Delta-mode source selection: --full-tree overrides everything; else
+// --previous-xmi if explicit; else zero-config auto-derive from
+// MTConnectVersions.Max. The expected auto-derive failure modes
+// (InvalidOperationException from a parse-shape miss or a Strategy-C fail-hard;
+// FileNotFoundException from a missing MTConnectVersions.cs) are caught and
+// mapped to a clean `error: ...` + exit 1 so the operator sees an actionable
+// message on stderr rather than a stack trace on stdout. Any other exception
+// class (e.g. IOException from a filesystem race, UnauthorizedAccessException)
+// is intentionally NOT caught here — those escape to the runtime and surface
+// as an unhandled stack trace, which is the right signal for an unexpected
+// host-level failure that the operator-facing recovery text cannot address.
+string? resolvedPreviousXmi = null;
+string? autoDerivedCompatLabel = null;
+if (!fullTree)
+{
+ if (previousXmiPath is not null)
+ {
+ resolvedPreviousXmi = previousXmiPath;
+ }
+ else
+ {
+ try
+ {
+ var (resolvedXmi, previousVersion) = ResolvePreviousXmi(outputRoot);
+ resolvedPreviousXmi = resolvedXmi;
+ autoDerivedCompatLabel = $"v{previousVersion.Major}_{previousVersion.Minor}";
+
+ // PREV == NEW guard: when the new XMI's filename encodes the same
+ // version as the auto-derived PREV_VERSION (from MTConnectVersions.Max),
+ // the delta is empty by construction — MTConnectVersions.Max already
+ // matches the version being generated. Log a warning and no-op
+ // (skip delta generation, return success 0). The filename convention
+ // is `MTConnectSysMLModel_v..xml`; the case-insensitive
+ // `_[vV]` prefix admits both the lowercase and uppercase-V variants
+ // seen historically.
+ var newVersionMatch = Regex.Match(
+ Path.GetFileName(newXmiPath),
+ @"_[vV](?\d+)\.(?\d+)\.xml$");
+ if (newVersionMatch.Success)
+ {
+ var newMajor = int.Parse(newVersionMatch.Groups["major"].Value);
+ var newMinor = int.Parse(newVersionMatch.Groups["minor"].Value);
+ if (newMajor == previousVersion.Major && newMinor == previousVersion.Minor)
+ {
+ Console.Error.WriteLine(
+ $"warning: latest MTConnect version (v{previousVersion.Major}.{previousVersion.Minor}) is already supported by MTConnectVersions.Max — no delta to derive; skipping emit.");
+ return 0;
+ }
+ }
+ }
+ catch (Exception ex) when (ex is InvalidOperationException or FileNotFoundException)
+ {
+ // Both throw sites carry actionable operator-facing messages
+ // (probed cache path, expected submodule tag, override flag,
+ // escape hatch). Surface the message on stderr with an `error:`
+ // prefix and return the standard "runtime failure" exit code
+ // (1) so the operator sees a clean CLI failure and not an
+ // unhandled-exception stack trace.
+ Console.Error.WriteLine($"error: {ex.Message}");
+ return 1;
+ }
+ }
+}
+
+// Compat-label defaulting: explicit --compat-version-label wins; else the
+// auto-derived "v${X}_${Y}" when we're in zero-config mode; else the legacy
+// "Previous" default (preserved for callers passing an explicit
+// --previous-xmi without a label).
+bool compatLabelIsAutoDerived = compatVersionLabel is null && autoDerivedCompatLabel is not null;
+compatVersionLabel ??= autoDerivedCompatLabel ?? "Previous";
+
+// --compat-version-label flows through Path.Combine(compatDir, $"{label}.g.cs")
+// and must produce a safe, contained filename on every host. Reject anything
+// that would escape the Compat/ directory (path-separator sequences), select
+// a legal but surprising target (drive letters, NUL bytes, ASCII controls),
+// or produce a hidden dotfile. The auto-derived "v${X}_${Y}" shape and the
+// legacy "Previous" default always pass; hostile operator input rejects here.
+if (!IsSafeCompatLabel(compatVersionLabel))
+{
+ Console.Error.WriteLine(
+ $"error: --compat-version-label value '{compatVersionLabel}' is not a safe filename. " +
+ "Allowed shape: 1 to 64 characters, ASCII letters / digits / '_' / '-' / '.', no leading dot.");
+ return 2;
+}
+
+Console.WriteLine($"XMI: {newXmiPath}");
+if (fullTree)
+{
+ Console.WriteLine("Mode: full-tree (--full-tree; delta paths disabled)");
+}
+else if (previousXmiPath is not null)
+{
+ Console.WriteLine($"Prev: {resolvedPreviousXmi}");
+ Console.WriteLine($"Label: {compatVersionLabel}");
+ Console.WriteLine("Mode: delta (--previous-xmi override)");
+}
+else
+{
+ Console.WriteLine($"Prev: {resolvedPreviousXmi} (auto-derived from MTConnectVersions.Max)");
+ // Annotate "(auto-derived)" only when the label WAS auto-derived. When
+ // the operator passed an explicit --compat-version-label alongside the
+ // zero-config prev-XMI path, that explicit label wins the ??= above,
+ // and annotating it "(auto-derived)" would be a lie.
+ Console.WriteLine(compatLabelIsAutoDerived
+ ? $"Label: {compatVersionLabel} (auto-derived)"
+ : $"Label: {compatVersionLabel}");
+ Console.WriteLine("Mode: delta (zero-config)");
+}
Console.WriteLine($"Output: {outputRoot}");
if (jsonDumpPath is not null)
Console.WriteLine($"JSON: {jsonDumpPath}");
-var mtconnectModel = MTConnectModel.Parse(xmiPath);
-if (mtconnectModel == null)
+if (jsonDumpPath is not null)
{
- // Fail fast on a null model. The renderers below internally null-check
- // and silently no-op, producing zero output and exit 0. Surface the parse
- // failure here so the operator gets a proper non-zero exit + stderr.
- Console.Error.WriteLine($"error: Failed to parse XMI: {xmiPath}");
- return 1;
+ var mtconnectModelForDump = MTConnectModel.Parse(newXmiPath);
+ if (mtconnectModelForDump == null)
+ {
+ Console.Error.WriteLine($"error: Failed to parse XMI: {newXmiPath}");
+ return 1;
+ }
+ RenderJsonFile(mtconnectModelForDump, jsonDumpPath);
}
-Console.WriteLine($"Model parsed: type={mtconnectModel.GetType().Name}");
-if (jsonDumpPath is not null)
- RenderJsonFile(mtconnectModel, jsonDumpPath);
+if (fullTree || resolvedPreviousXmi is null)
+{
+ // Full-tree mode (either explicit --full-tree or a caller that has
+ // somehow reached here without a resolved previous XMI). Preserves the
+ // pre-Phase-4 behaviour bit-for-bit.
+ var mtconnectModel = MTConnectModel.Parse(newXmiPath);
+ if (mtconnectModel == null)
+ {
+ // Fail fast on a null model. The renderers below internally null-check
+ // and silently no-op, producing zero output and exit 0. Surface the parse
+ // failure here so the operator gets a proper non-zero exit + stderr.
+ Console.Error.WriteLine($"error: Failed to parse XMI: {newXmiPath}");
+ return 1;
+ }
+ Console.WriteLine($"Model parsed: type={mtconnectModel.GetType().Name}");
+
+ Console.WriteLine("Rendering C# common classes...");
+ RenderCommonClasses(mtconnectModel, outputRoot);
+ Console.WriteLine("Rendering JSON-cppagent formatters...");
+ RenderJsonComponents(mtconnectModel, outputRoot);
+ Console.WriteLine("Rendering XML formatters...");
+ RenderXmlComponents(mtconnectModel, outputRoot);
+ Console.WriteLine("Done.");
+ return 0;
+}
-Console.WriteLine("Rendering C# common classes...");
-RenderCommonClasses(mtconnectModel, outputRoot);
-Console.WriteLine("Rendering JSON-cppagent formatters...");
-RenderJsonComponents(mtconnectModel, outputRoot);
-Console.WriteLine("Rendering XML formatters...");
-RenderXmlComponents(mtconnectModel, outputRoot);
-Console.WriteLine("Done.");
-return 0;
+// Delta mode. Render both XMIs into isolated scratch directories, then diff at
+// the file level and emit only the changed/added files into outputRoot,
+// concentrating unchanged files into Compat/.g.cs per library.
+return RenderDelta(newXmiPath, resolvedPreviousXmi, outputRoot, compatVersionLabel!);
static string RequireValue(string[] argv, ref int index, string flag)
@@ -116,6 +311,193 @@ static string RequireValue(string[] argv, ref int index, string flag)
return argv[index];
}
+// Validates that --compat-version-label produces a safe on-disk filename inside
+// the per-library Compat/ directory. Rejects: null / empty / whitespace, any
+// path separator or drive letter, ASCII control chars, leading dots (hidden
+// files), and lengths outside 1..64 chars. The default "Previous" always
+// passes; auto-derived "v${X}_${Y}" labels always pass; hostile inputs like
+// "../../etc/passwd" or "Compat/../secret" reject at argument-parse time.
+static bool IsSafeCompatLabel(string? label)
+{
+ if (string.IsNullOrWhiteSpace(label)) return false;
+ if (label.Length > 64) return false;
+ return Regex.IsMatch(label, @"^[A-Za-z0-9_\-][A-Za-z0-9_\-.]*$");
+}
+
+// Auto-derives the previous-version XMI path from MTConnectVersions.Max in the
+// current tree state. Returns the resolved XMI path plus the parsed Version so
+// the caller can derive the auto-derived Compat label. Throws with an
+// actionable message when neither Strategy B (cache) nor Strategy A (submodule
+// tag) resolves.
+//
+// Strategy A (fallback): build/sysml-model/MTConnectSysMLModel.xml, gated on
+// the submodule tip being checked out exactly at tag v${PREV_VERSION}.
+//
+// Strategy B (primary): build/.cache/sysml-prev/MTConnectSysMLModel_v${PREV_VERSION}.xml.
+//
+// Strategy C (fail-hard): neither resolves — throw naming both probed paths so
+// the operator can either populate the cache (Phase 3.2 of the version-bump
+// runbook), re-check the submodule tag, pass --previous-xmi explicitly,
+// or pass --full-tree to disable delta mode.
+static (string XmiPath, Version PreviousVersion) ResolvePreviousXmi(string outputRoot)
+{
+ var previousVersion = ReadMTConnectVersionsMax(outputRoot);
+
+ // Strategy B (primary): cache path.
+ var cachePath = Path.Combine(
+ outputRoot, "build", ".cache", "sysml-prev",
+ $"MTConnectSysMLModel_v{previousVersion.Major}.{previousVersion.Minor}.xml");
+ if (File.Exists(cachePath))
+ {
+ return (cachePath, previousVersion);
+ }
+
+ // Strategy A (fallback): submodule tag check.
+ var submoduleDir = Path.Combine(outputRoot, "build", "sysml-model");
+ var submoduleXmi = Path.Combine(submoduleDir, "MTConnectSysMLModel.xml");
+ var expectedTag = $"v{previousVersion.Major}.{previousVersion.Minor}";
+ if (Directory.Exists(submoduleDir) && File.Exists(submoduleXmi))
+ {
+ var currentTag = TryGetSubmoduleTag(submoduleDir);
+ if (currentTag is not null && string.Equals(currentTag, expectedTag, StringComparison.Ordinal))
+ {
+ return (submoduleXmi, previousVersion);
+ }
+ }
+
+ // Strategy C: fail-hard with an actionable message.
+ throw new InvalidOperationException(
+ $"PREV_VERSION auto-derivation from MTConnectVersions.Max = {previousVersion.Major}.{previousVersion.Minor} failed. " +
+ $"Neither cache path '{cachePath}' nor submodule tag '{expectedTag}' resolved. " +
+ "Pass --previous-xmi explicitly to override, or --full-tree to disable delta mode.");
+}
+
+// Parses MTConnectVersions.cs under `outputRoot` and returns the version that
+// `Max` currently names. The parser locates the `public static Version Max =>
+// VersionXY;` line, resolves `VersionXY` to its `new Version(X, Y)` literal
+// below, and returns that Version. Text parsing keeps the importer free of a
+// runtime dependency on MTConnect.NET-Common (which would create an awkward
+// generator-emits-into-its-own-dependency ordering during clean rebuilds).
+//
+// Both regexes are line-anchored (`(?m)^[ \t]*public…`) so a stale
+// `// Max => Version27;` decoy above the real declaration cannot match — the
+// `//` sits between line start and `public`, breaking the anchor. A last-match
+// preference (`.Matches().Last()`) is applied on both patterns so a
+// hypothetical block-commented decoy of the shape
+// `/* … public static Version Max => Version28; … */`
+// still loses to the live line below it. This is the F-SIMP-501 shrink;
+// no comment-stripper walker is required for the two-shape MTConnectVersions.cs
+// surface.
+static Version ReadMTConnectVersionsMax(string outputRoot)
+{
+ var versionsPath = Path.Combine(
+ outputRoot, "libraries", "MTConnect.NET-Common", "MTConnectVersions.cs");
+ if (!File.Exists(versionsPath))
+ {
+ throw new FileNotFoundException(
+ $"MTConnectVersions.cs not found at {versionsPath}. " +
+ "The auto-derive path needs this file to determine PREV_VERSION. " +
+ "Pass --previous-xmi explicitly to bypass the auto-derive, " +
+ "or --full-tree to disable delta mode.",
+ versionsPath);
+ }
+
+ // Read the source raw and rely on a line-anchored regex to skip
+ // `// Max => Version27;` decoys — the `[ \t]*public` prefix at line start
+ // (multiline mode) cannot match a `//`-commented line because the `//`
+ // sits between line start and `public`. This is the F-SIMP-501 shrink:
+ // no comment stripper, no shared-source link, no literal-aware walker;
+ // just a targeted anchor plus a last-match preference so a hypothetical
+ // block-commented decoy above the real declaration still loses to the
+ // live line below it.
+ var source = File.ReadAllText(versionsPath);
+
+ // Match `public static Version Max => VersionXY;` at line start (multiline)
+ // so a `// public static Version Max => Version27;` decoy is skipped by
+ // the `[ \t]*` prefix which admits only whitespace before `public`. The
+ // naming convention is enforced by the hand-authored constant table
+ // above the Max property.
+ var maxMatches = Regex.Matches(source, @"(?m)^[ \t]*public\s+static\s+Version\s+Max\s*=>\s*Version(?\d+)\s*;");
+ if (maxMatches.Count == 0)
+ {
+ throw new InvalidOperationException(
+ $"Could not locate `public static Version Max => VersionXY;` in {versionsPath}. " +
+ "The auto-derive path relies on the documented naming convention. " +
+ "Pass --previous-xmi explicitly to bypass the auto-derive, " +
+ "or --full-tree to disable delta mode.");
+ }
+
+ var xy = maxMatches[maxMatches.Count - 1].Groups["xy"].Value;
+
+ // Match `public static readonly Version VersionXY = new Version(X, Y);`
+ // so we can recover the major.minor pair. Accepts optional whitespace and
+ // the `new(...)` target-typed form as well as the explicit `new Version(...)`.
+ // Same line-anchor discipline as above.
+ var constMatches = Regex.Matches(
+ source,
+ $@"(?m)^[ \t]*public\s+static\s+readonly\s+Version\s+Version{xy}\s*=\s*new(?:\s+Version)?\s*\(\s*(?\d+)\s*,\s*(?\d+)\s*\)\s*;");
+ if (constMatches.Count == 0)
+ {
+ throw new InvalidOperationException(
+ $"Could not locate `public static readonly Version Version{xy} = new Version(X, Y);` in {versionsPath}. " +
+ "The auto-derive path relies on the documented naming convention. " +
+ "Pass --previous-xmi explicitly to bypass the auto-derive, " +
+ "or --full-tree to disable delta mode.");
+ }
+
+ var lastConst = constMatches[constMatches.Count - 1];
+ var major = int.Parse(lastConst.Groups["major"].Value);
+ var minor = int.Parse(lastConst.Groups["minor"].Value);
+ return new Version(major, minor);
+}
+
+// Runs `git -C describe --exact-match --tags HEAD` and returns
+// the tag name on success, null on any failure (non-git dir, no exact-match
+// tag, git binary absent, non-zero exit). The tag-mismatch path is a normal
+// zero-config outcome (Phase 3 checks the submodule out to the NEW-VERSION
+// tag; the auto-derive expects PREV_VERSION), so failure here is a routine
+// signal for "try the next strategy", not a fatal condition.
+static string? TryGetSubmoduleTag(string submoduleDir)
+{
+ try
+ {
+ var psi = new ProcessStartInfo("git")
+ {
+ WorkingDirectory = submoduleDir,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+ psi.ArgumentList.Add("-C");
+ psi.ArgumentList.Add(submoduleDir);
+ psi.ArgumentList.Add("describe");
+ psi.ArgumentList.Add("--exact-match");
+ psi.ArgumentList.Add("--tags");
+ psi.ArgumentList.Add("HEAD");
+
+ using var proc = Process.Start(psi);
+ if (proc is null)
+ return null;
+
+ var stdoutTask = proc.StandardOutput.ReadToEndAsync();
+ var stderrTask = proc.StandardError.ReadToEndAsync();
+ System.Threading.Tasks.Task.WhenAll(stdoutTask, stderrTask).GetAwaiter().GetResult();
+ proc.WaitForExit();
+
+ if (proc.ExitCode != 0)
+ return null;
+
+ return stdoutTask.Result.Trim();
+ }
+ catch (Exception)
+ {
+ // Any exception path (git binary missing, permission denied, submodule
+ // dir doesn't even hold a .git file) reduces to "try the next strategy".
+ return null;
+ }
+}
+
static void PrintHelp()
{
Console.WriteLine("""
@@ -123,10 +505,22 @@ MTConnect.NET SysML Importer
Usage:
dotnet run --project build/MTConnect.NET-SysML-Import -- \
- --xmi \
+ --new-xmi \
--output \
+ [--previous-xmi ] \
+ [--compat-version-label ] \
+ [--full-tree] \
[--json-dump ]
+ Zero-config delta mode is the default: PREV_VERSION is auto-derived
+ from MTConnectVersions.Max in the current tree and the prior-version
+ XMI is resolved from build/.cache/sysml-prev/ or the build/sysml-model
+ submodule when it is checked out at the matching tag.
+
+ Pass --full-tree to force full regeneration (both delta paths off).
+ Pass --previous-xmi to override the auto-derived prior XMI.
+ The legacy --xmi flag is accepted as an alias for --new-xmi.
+
See build/MTConnect.NET-SysML-Import/README.md for the full guide.
""");
}
@@ -181,3 +575,231 @@ static void RenderXmlComponents(MTConnectModel model, string outputRoot)
XmlTemplateRenderer.Render(model, outputPath);
}
+
+// Delta-driven regen. Renders both --new-xmi and --previous-xmi to isolated
+// scratch trees, then emits the diff:
+//
+// - Files present only in the NEW tree (ADDED) → written to outputRoot.
+// - Files present in both trees with different bytes (CHANGED) → written
+// to outputRoot (NEW tree's version).
+// - Files present only in the PREV tree (REMOVED) → DELETED from outputRoot
+// so the type stops shipping (the spec dropped it).
+// - Files present in both trees with identical bytes (UNCHANGED) →
+// concentrated into Compat/.g.cs per library, per plan D4. The
+// individual .g.cs at outputRoot is DELETED so the Compat file is the
+// sole namespace host (avoids CS0101 duplicate-type errors when the
+// delta runs against a repo already carrying the committed .g.cs tree,
+// which is the realistic use case).
+//
+// The Compat file concatenates the unchanged files' bodies verbatim (each
+// still carries its own `namespace X { ... }` block, so multi-namespace
+// concentration is legal C#). Byte-identity of unchanged files is preserved,
+// satisfying the Phase 4.1 invariant that Compat/.g.cs is
+// `git diff`-clean against the deterministically-emitted full-tree output.
+static int RenderDelta(string newXmiPath, string prevXmiPath, string outputRoot, string compatLabel)
+{
+ var scratchRoot = Path.Combine(Path.GetTempPath(), $"mtc-sysml-delta-{Guid.NewGuid():N}");
+ var prevScratch = Path.Combine(scratchRoot, "prev");
+ var newScratch = Path.Combine(scratchRoot, "new");
+
+ try
+ {
+ Console.WriteLine($"Delta scratch: {scratchRoot}");
+ Console.WriteLine("Rendering previous-XMI full tree to scratch...");
+ RenderFullTreeToScratch(prevXmiPath, prevScratch);
+ Console.WriteLine("Rendering new-XMI full tree to scratch...");
+ RenderFullTreeToScratch(newXmiPath, newScratch);
+
+ Console.WriteLine("Diffing scratch trees and emitting delta...");
+ var stats = EmitDelta(prevScratch, newScratch, outputRoot, compatLabel);
+
+ Console.WriteLine(
+ $"Delta emission: added={stats.Added}, changed={stats.Changed}, " +
+ $"unchanged-concentrated={stats.UnchangedConcentrated}, removed-skipped={stats.RemovedSkipped}, " +
+ $"compat-files-written={stats.CompatFilesWritten}");
+ Console.WriteLine("Done.");
+ return 0;
+ }
+ finally
+ {
+ if (Directory.Exists(scratchRoot))
+ {
+ try { Directory.Delete(scratchRoot, recursive: true); }
+ catch (IOException ex)
+ {
+ Console.Error.WriteLine($"warning: could not clean scratch {scratchRoot}: {ex.Message}");
+ }
+ }
+ }
+}
+
+// Runs the full-tree renderer pipeline against `xmiPath`, writing every
+// `.g.cs` artefact into /libraries//... Mirrors the
+// full-tree branch above so the emitted bytes are byte-identical to what
+// the operator would get from `--new-xmi --output `
+// with no `--previous-xmi`.
+static void RenderFullTreeToScratch(string xmiPath, string scratchRoot)
+{
+ Directory.CreateDirectory(scratchRoot);
+ Directory.CreateDirectory(Path.Combine(scratchRoot, "libraries", "MTConnect.NET-Common"));
+ Directory.CreateDirectory(Path.Combine(scratchRoot, "libraries", "MTConnect.NET-JSON-cppagent"));
+ Directory.CreateDirectory(Path.Combine(scratchRoot, "libraries", "MTConnect.NET-XML"));
+
+ var model = MTConnectModel.Parse(xmiPath);
+ if (model == null)
+ throw new InvalidOperationException($"Failed to parse XMI: {xmiPath}");
+
+ RenderCommonClasses(model, scratchRoot);
+ RenderJsonComponents(model, scratchRoot);
+ RenderXmlComponents(model, scratchRoot);
+}
+
+// File-level diff between prev and new scratch trees, emitting the delta into
+// outputRoot. Returns per-category counts for the console summary.
+static DeltaStats EmitDelta(string prevScratch, string newScratch, string outputRoot, string compatLabel)
+{
+ var stats = new DeltaStats();
+
+ // Iterate per library so each library gets its own Compat/.g.cs.
+ string[] libraries = { "MTConnect.NET-Common", "MTConnect.NET-JSON-cppagent", "MTConnect.NET-XML" };
+ foreach (var library in libraries)
+ {
+ var prevLibrary = Path.Combine(prevScratch, "libraries", library);
+ var newLibrary = Path.Combine(newScratch, "libraries", library);
+ var outputLibrary = Path.Combine(outputRoot, "libraries", library);
+
+ if (!Directory.Exists(outputLibrary))
+ throw new DirectoryNotFoundException($"{library} not found under output root: {outputLibrary}");
+
+ var prevFiles = EnumerateGeneratedFiles(prevLibrary);
+ var newFiles = EnumerateGeneratedFiles(newLibrary);
+
+ var compatBody = new StringBuilder();
+ var compatFileCount = 0;
+
+ foreach (var relativePath in newFiles.Keys.OrderBy(k => k, StringComparer.Ordinal))
+ {
+ var newContent = newFiles[relativePath];
+ if (prevFiles.TryGetValue(relativePath, out var prevContent))
+ {
+ if (ByteEquals(prevContent, newContent))
+ {
+ // UNCHANGED — concentrate into Compat file.
+ if (compatFileCount == 0)
+ {
+ compatBody.AppendLine("// Copyright (c) 2025 TrakHound Inc., All Rights Reserved.");
+ compatBody.AppendLine("// TrakHound Inc. licenses this file to you under the MIT license.");
+ compatBody.AppendLine();
+ compatBody.AppendLine("// Compat re-emit: concentrates every .g.cs file whose content did NOT change");
+ compatBody.AppendLine("// between --previous-xmi and --new-xmi. Each block carries its own namespace,");
+ compatBody.AppendLine("// so multi-namespace concentration is legal C#. Byte-identical to the");
+ compatBody.AppendLine("// full-tree emission for every included type (plan D4).");
+ compatBody.AppendLine();
+ }
+ compatBody.AppendLine($"// --- from {relativePath.Replace('\\', '/')} ---");
+ compatBody.Append(System.Text.Encoding.UTF8.GetString(newContent));
+ if (newContent.Length == 0 || newContent[^1] != (byte)'\n')
+ compatBody.AppendLine();
+ compatBody.AppendLine();
+ compatFileCount++;
+ stats.UnchangedConcentrated++;
+
+ // Concentration replaces the individual file. If the operator ran
+ // the delta against an outputRoot that already carried the
+ // committed .g.cs tree (the realistic use case — the repo root),
+ // leaving the individual file in place would produce CS0101
+ // duplicate-type errors when the Compat file re-emits the same
+ // namespaces. Delete pre-existing individual .g.cs so Compat is
+ // the single source for UNCHANGED types.
+ DeleteIfExists(Path.Combine(outputLibrary, relativePath));
+ }
+ else
+ {
+ // CHANGED — emit new-tree version at its normal location.
+ WriteFile(Path.Combine(outputLibrary, relativePath), newContent);
+ stats.Changed++;
+ }
+ }
+ else
+ {
+ // ADDED — emit new-tree version at its normal location.
+ WriteFile(Path.Combine(outputLibrary, relativePath), newContent);
+ stats.Added++;
+ }
+ }
+
+ // REMOVED types (present in prev tree, absent from new tree). Delete the
+ // stale individual file from outputRoot so it stops shipping in the
+ // library; the type is intentionally gone from the new spec version.
+ foreach (var relativePath in prevFiles.Keys)
+ {
+ if (!newFiles.ContainsKey(relativePath))
+ {
+ DeleteIfExists(Path.Combine(outputLibrary, relativePath));
+ stats.RemovedSkipped++;
+ }
+ }
+
+ if (compatFileCount > 0)
+ {
+ var compatDir = Path.Combine(outputLibrary, "Compat");
+ Directory.CreateDirectory(compatDir);
+ var compatPath = Path.Combine(compatDir, $"{compatLabel}.g.cs");
+ File.WriteAllText(compatPath, compatBody.ToString());
+ stats.CompatFilesWritten++;
+ }
+ }
+
+ return stats;
+}
+
+// Enumerates every .g.cs file under `root` and returns a dictionary keyed by
+// the forward-slash-normalised path relative to `root`, with the raw file
+// bytes as value. Ordinal-key comparer keeps cross-platform behaviour
+// consistent (Linux CI vs. Windows local).
+static Dictionary EnumerateGeneratedFiles(string root)
+{
+ var result = new Dictionary(StringComparer.Ordinal);
+ if (!Directory.Exists(root))
+ return result;
+
+ foreach (var path in Directory.EnumerateFiles(root, "*.g.cs", SearchOption.AllDirectories))
+ {
+ var relative = Path.GetRelativePath(root, path).Replace('\\', '/');
+ result[relative] = File.ReadAllBytes(path);
+ }
+ return result;
+}
+
+static bool ByteEquals(byte[] a, byte[] b)
+ => ((ReadOnlySpan)a).SequenceEqual(b);
+
+static void WriteFile(string path, byte[] contents)
+{
+ var directory = Path.GetDirectoryName(path);
+ if (!string.IsNullOrEmpty(directory))
+ Directory.CreateDirectory(directory);
+ File.WriteAllBytes(path, contents);
+}
+
+// Idempotent delete. Missing files no-op; existing files delete without following
+// symlinks (System.IO.File.Delete on .NET removes the link, not the target).
+static void DeleteIfExists(string path)
+{
+ if (File.Exists(path))
+ File.Delete(path);
+}
+
+// Per-run counters emitted at the tail of a delta invocation. Every category
+// is reported so the operator can spot a regression at a glance (a spec bump
+// that renames a type surfaces as {changed:1, removed-skipped:1, added:1};
+// whereas a spec bump that only adds enum arms surfaces as {changed:1,
+// unchanged-concentrated:N-1, removed-skipped:0, added:0}).
+internal sealed class DeltaStats
+{
+ public int Added { get; set; }
+ public int Changed { get; set; }
+ public int UnchangedConcentrated { get; set; }
+ public int RemovedSkipped { get; set; }
+ public int CompatFilesWritten { get; set; }
+}
diff --git a/build/MTConnect.NET-SysML-Import/README.md b/build/MTConnect.NET-SysML-Import/README.md
index 45c239ed9..21a278677 100644
--- a/build/MTConnect.NET-SysML-Import/README.md
+++ b/build/MTConnect.NET-SysML-Import/README.md
@@ -46,20 +46,33 @@ Each `/tmp/sysml-vX.Y/MTConnectSysMLModel.xml` can then be passed to a separate
### 2. Run the importer
```bash
-# From the repo root, after the submodule is checked out:
+# From the repo root, after the submodule is checked out. Zero-config: the
+# importer auto-derives PREV_VERSION from MTConnectVersions.Max and resolves
+# the prior-version XMI automatically, so the common case is a single-flag
+# invocation.
dotnet run --project build/MTConnect.NET-SysML-Import \
- -- --xmi build/sysml-model/MTConnectSysMLModel.xml \
+ -- --new-xmi build/sysml-model/MTConnectSysMLModel.xml \
--output "$(pwd)"
+
+# Force full regeneration (skip both delta paths):
+dotnet run --project build/MTConnect.NET-SysML-Import \
+ -- --new-xmi build/sysml-model/MTConnectSysMLModel.xml \
+ --output "$(pwd)" \
+ --full-tree
```
If running against a side worktree (for multi-version regens):
```bash
dotnet run --project build/MTConnect.NET-SysML-Import \
- -- --xmi /tmp/sysml-v2.5/MTConnectSysMLModel.xml \
- --output "$(pwd)"
+ -- --new-xmi /tmp/sysml-v2.5/MTConnectSysMLModel.xml \
+ --output "$(pwd)" \
+ --full-tree
```
+The `--xmi` flag from pre-#408 invocations is still accepted as a legacy alias
+for `--new-xmi`; new call sites should prefer `--new-xmi`.
+
### 3. Inspect + commit
```bash
@@ -79,12 +92,53 @@ Split the regen into per-target commits so reviewers can audit each layer indepe
| Flag | Required | Default | Purpose |
|---|---|---|---|
-| `--xmi ` | Yes | — | Path to the SysML XMI file to consume. |
+| `--new-xmi ` | Yes | — | Path to the new-version SysML XMI to consume. Preferred spelling from task #408 onwards. |
+| `--xmi ` | — | — | Legacy alias for `--new-xmi`. Kept for backwards compatibility with pre-#408 callers; new invocations should prefer `--new-xmi`. |
| `--output ` | Yes | — | Repository root. Each renderer writes into its own `libraries//` subtree under this root. |
+| `--previous-xmi ` | No | auto-derived | Explicit override for the prior-version XMI in delta mode. When supplied, skips the zero-config auto-derive step and uses this file as the previous-version XMI. Typical use cases: cross-version audit runs, regenerating against a historical XMI snapshot, and version-bumps that skip a version (where `MTConnectVersions.Max` does not match the intended `PREV_VERSION`). Files present in `--previous-xmi`'s tree but absent from `--new-xmi`'s tree (REMOVED types) are **deleted** from the output tree. Files concentrated into `Compat/.g.cs` (UNCHANGED types) are also deleted from their individual `.g.cs` paths — the Compat file becomes the sole namespace host to avoid CS0101 duplicate-type collisions when `--output` points at a repo already carrying a full committed `.g.cs` tree. |
+| `--compat-version-label ` | No | `v${PREV_XY_UNDERSCORE}` (auto-derived) or `Previous` (explicit-override fallback) | Label used for the `Compat/.g.cs` file name in delta mode. In zero-config mode the label auto-derives from `MTConnectVersions.Max` as `v${X}_${Y}` (e.g. `v2_7`); with an explicit `--previous-xmi` the legacy `Previous` default applies. Must match `^[A-Za-z0-9_\-][A-Za-z0-9_\-.]*$`, ≤ 64 chars, no leading dot — hostile inputs like `../../etc/passwd` reject at exit 2. Ignored under `--full-tree`. |
+| `--full-tree` | No | delta by default | Explicit opt-in for the full-regeneration path. Disables both the zero-config auto-derive delta and the `--previous-xmi` override; every emitted `.g.cs` re-lands under its normal path. |
| `--json-dump ` | No | not written | If set, dumps the parsed `MTConnectModel` as JSON. Useful for debugging. |
| `--help`, `-h` | — | — | Print usage and exit. |
-`--xmi` and `--output` are mandatory. Running with no arguments exits with `error: --xmi is required.` (exit code 2) and prints help.
+`--new-xmi` (or `--xmi`) and `--output` are mandatory. Running with no arguments exits with `error: --new-xmi is required (legacy alias --xmi is still accepted).` (exit code 2) and prints help.
+
+### Zero-config delta mode (default from task #408)
+
+When neither `--previous-xmi` nor `--full-tree` is supplied, the importer parses `MTConnectVersions.Max` from `libraries/MTConnect.NET-Common/MTConnectVersions.cs` under `--output` and resolves the prior-version XMI internally, in this order:
+
+1. **Strategy B (primary)** — `build/.cache/sysml-prev/MTConnectSysMLModel_v${PREV_VERSION}.xml`, populated per Phase 3.2 of the version-bump runbook (`docs/testing/vX-Y.md`).
+2. **Strategy A (fallback)** — `build/sysml-model/MTConnectSysMLModel.xml`, gated on `git -C build/sysml-model describe --exact-match --tags HEAD` returning `v${PREV_VERSION}` exactly. Covers the dev-loop case where the operator has not yet promoted the submodule tip past the prior-version tag.
+3. **Strategy C (fail-hard)** — neither resolves. Exits with `error: PREV_VERSION auto-derivation from MTConnectVersions.Max = ${PREV_VERSION} failed. …` on stderr + exit code 1, naming both probed paths and directing the operator at `--previous-xmi` (explicit override) or `--full-tree` (delta-disable escape hatch).
+
+The zero-config default keeps a Phase 3 version-bump invocation single-flag:
+
+```bash
+dotnet run --project build/MTConnect.NET-SysML-Import \
+ -- --new-xmi build/sysml-model/MTConnectSysMLModel.xml \
+ --output "$(pwd)"
+```
+
+### Delta mode with explicit `--previous-xmi`
+
+The explicit-override path is for the exceptional cases where `MTConnectVersions.Max` does not name the intended `PREV_VERSION` (cross-version audit, regen against a historical XMI snapshot, or a spec bump that skips a version).
+
+```bash
+dotnet run --project build/MTConnect.NET-SysML-Import \
+ -- --new-xmi /tmp/mtconnect-sysml/v2.8/MTConnectSysMLModel.xml \
+ --previous-xmi /tmp/mtconnect-sysml/v2.5/MTConnectSysMLModel.xml \
+ --compat-version-label v2_5 \
+ --output "$(pwd)"
+```
+
+Emission partitions per file:
+
+- **ADDED** (in new only) → written to normal `libraries//...` path.
+- **CHANGED** (in both, different bytes) → written to normal path (new tree's version).
+- **REMOVED** (in prev only) → deleted from `--output` (the type stops shipping — the spec dropped it).
+- **UNCHANGED** (in both, identical bytes) → concentrated into `libraries//Compat/.g.cs`; the individual `.g.cs` file is deleted from the output tree so the Compat file is the sole namespace host.
+
+Byte-identity of unchanged types is preserved (plan D4 invariant); dropping `--previous-xmi` returns to full-tree mode bit-for-bit. Regression is covered by `tests/MTConnect.NET-Generator-Tests/DeltaRegenTests` and `DeltaCompatAndStatsTests`.
## Visual Studio F5 workflow
@@ -127,7 +181,7 @@ public static readonly Version Version28 = new Version(2, 8); // add the const
git -C /tmp/mtconnect-sysml fetch --tags
git -C /tmp/mtconnect-sysml checkout v2.8
dotnet run --project build/MTConnect.NET-SysML-Import \
- -- --xmi /tmp/mtconnect-sysml/MTConnectSysMLModel.xml \
+ -- --new-xmi /tmp/mtconnect-sysml/MTConnectSysMLModel.xml \
--output "$(pwd)"
```
diff --git a/build/MTConnect.NET-SysML-Import/Xml/TemplateRenderer.cs b/build/MTConnect.NET-SysML-Import/Xml/TemplateRenderer.cs
index 0da5904d3..dab81ea4d 100644
--- a/build/MTConnect.NET-SysML-Import/Xml/TemplateRenderer.cs
+++ b/build/MTConnect.NET-SysML-Import/Xml/TemplateRenderer.cs
@@ -23,20 +23,42 @@ public static void Render(MTConnectModel mtconnectModel, string outputPath)
{
if (mtconnectModel != null && !string.IsNullOrEmpty(outputPath))
{
- // All three Xml templates render the same CuttingToolMeasurementsModel —
- // build it once, then drive the three (template, output) pairs through
- // a shared helper. Output is byte-identical to the previous three-method
- // form; the templates differ only in which model fields they read.
+ // The one CuttingToolMeasurementsModel drives every Xml artefact.
+ // The XmlMeasurements.scriban template emits the per-measurement
+ // Xml wrapper subclasses; the shared Shape-A host template
+ // (XmlMeasurementArrayHost.scriban) emits both partial-class
+ // artefacts (XmlCuttingToolLifeCycle + XmlCuttingItem), each with
+ // its own class name and doc-summary values. Consolidating the
+ // two per-host templates into one keeps emission byte-identical.
var measurementsModel = BuildCuttingToolMeasurementsModel(mtconnectModel);
- var renders = new (string Template, string OutputRelative)[]
+
+ RenderTo("XmlMeasurements.scriban", measurementsModel, "Assets/CuttingTools/XmlMeasurements", outputPath);
+
+ var arrayHosts = new (string ClassName, string Summary, string OutputRelative)[]
{
- ("XmlMeasurements.scriban", "Assets/CuttingTools/XmlMeasurements"),
- ("XmlCuttingToolLifeCycle.scriban", "Assets/CuttingTools/XmlCuttingToolLifeCycle"),
- ("XmlCuttingItem.scriban", "Assets/CuttingTools/XmlCuttingItem"),
+ (
+ "XmlCuttingToolLifeCycle",
+ "The set of physical and geometric measurements that characterize the cutting tool\n /// over its life cycle. Each element is deserialized into the concrete\n /// subclass registered for its MTConnect measurement type.",
+ "Assets/CuttingTools/XmlCuttingToolLifeCycle"
+ ),
+ (
+ "XmlCuttingItem",
+ "The set of physical and geometric measurements that characterize this cutting item.\n /// Each element is deserialized into the concrete subclass\n /// registered for its MTConnect measurement type.",
+ "Assets/CuttingTools/XmlCuttingItem"
+ ),
};
- foreach (var (template, output) in renders)
+ foreach (var (className, summary, output) in arrayHosts)
{
- RenderTo(template, measurementsModel, output, outputPath);
+ // Anonymous model — Scriban resolves properties by snake_case
+ // convention, so ClassName → class_name, Summary → summary,
+ // Types → types.
+ var hostModel = new
+ {
+ class_name = className,
+ summary = summary,
+ types = measurementsModel.Types
+ };
+ RenderTo("XmlMeasurementArrayHost.scriban", hostModel, output, outputPath);
}
}
}
diff --git a/build/MTConnect.NET-SysML-Import/Xml/Templates/XmlCuttingToolLifeCycle.scriban b/build/MTConnect.NET-SysML-Import/Xml/Templates/XmlCuttingToolLifeCycle.scriban
deleted file mode 100644
index 4267cb395..000000000
--- a/build/MTConnect.NET-SysML-Import/Xml/Templates/XmlCuttingToolLifeCycle.scriban
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) 2023 TrakHound Inc., All Rights Reserved.
-// TrakHound Inc. licenses this file to you under the MIT license.
-
-using MTConnect.Assets.CuttingTools.Measurements;
-using System.Collections.Generic;
-using System.Xml.Serialization;
-
-namespace MTConnect.Assets.Xml.CuttingTools
-{
- public partial class XmlCuttingToolLifeCycle
- {
- ///
- /// The set of physical and geometric measurements that characterize the cutting tool
- /// over its life cycle. Each element is deserialized into the concrete
- /// subclass registered for its MTConnect measurement type.
- ///
- [XmlArray("Measurements")]
-{{- for type in types }}
- [XmlArrayItem({{type.name}}.TypeId, typeof(Xml{{type.name}}))]
-{{- end }}
- public List Measurements { get; set; }
- }
-}
\ No newline at end of file
diff --git a/build/MTConnect.NET-SysML-Import/Xml/Templates/XmlCuttingItem.scriban b/build/MTConnect.NET-SysML-Import/Xml/Templates/XmlMeasurementArrayHost.scriban
similarity index 50%
rename from build/MTConnect.NET-SysML-Import/Xml/Templates/XmlCuttingItem.scriban
rename to build/MTConnect.NET-SysML-Import/Xml/Templates/XmlMeasurementArrayHost.scriban
index 63a15bf23..1aac24db1 100644
--- a/build/MTConnect.NET-SysML-Import/Xml/Templates/XmlCuttingItem.scriban
+++ b/build/MTConnect.NET-SysML-Import/Xml/Templates/XmlMeasurementArrayHost.scriban
@@ -1,5 +1,11 @@
// Copyright (c) 2023 TrakHound Inc., All Rights Reserved.
// TrakHound Inc. licenses this file to you under the MIT license.
+{{-# Shape-A consolidated host template. Valid for every MTConnect version. #}}
+{{-# One template renders every partial-class XML wrapper (XmlCuttingToolLifeCycle, #}}
+{{-# XmlCuttingItem, ...) that exposes the full set of cutting-tool measurement #}}
+{{-# subclasses through a single typed [XmlArray("Measurements")] collection. Each #}}
+{{-# call site supplies its class_name, summary (a doc-comment fragment), and the #}}
+{{-# shared types array of measurement models. #}}
using MTConnect.Assets.CuttingTools.Measurements;
using System.Collections.Generic;
@@ -7,12 +13,10 @@ using System.Xml.Serialization;
namespace MTConnect.Assets.Xml.CuttingTools
{
- public partial class XmlCuttingItem
+ public partial class {{class_name}}
{
///
- /// The set of physical and geometric measurements that characterize this cutting item.
- /// Each element is deserialized into the concrete subclass
- /// registered for its MTConnect measurement type.
+ /// {{summary}}
///
[XmlArray("Measurements")]
{{- for type in types }}
diff --git a/docs/cli/sysml-import.md b/docs/cli/sysml-import.md
index 1d7e1c1bf..e18193c3d 100644
--- a/docs/cli/sysml-import.md
+++ b/docs/cli/sysml-import.md
@@ -1,78 +1,164 @@
-# SysML importer
+# SysML importer CLI
-`MTConnect.NET-SysML-Import` is the in-repo code-generator that turns an XMI export of the MTConnect SysML model into the `*.g.cs` source files under `libraries/MTConnect.NET-Common/`, `libraries/MTConnect.NET-XML/`, and `libraries/MTConnect.NET-JSON-cppagent/`. It is the bridge between the standard's normative model (`mtconnect/mtconnect_sysml_model`) and the .NET library's typed surface.
+`MTConnect.NET-SysML-Import` is the in-repo code generator that turns an XMI export of the MTConnect SysML model into the `*.g.cs` source files under `libraries/MTConnect.NET-Common/`, `libraries/MTConnect.NET-XML/`, and `libraries/MTConnect.NET-JSON-cppagent/`. It is the bridge between the standard's normative model (`mtconnect/mtconnect_sysml_model`) and the .NET library's typed surface.
-This tool is **not shipped** as a binary. It lives in `build/MTConnect.NET-SysML-Import/` and is run by maintainers when:
+The importer is **not shipped** as a binary. It lives in `build/MTConnect.NET-SysML-Import/` and is run by maintainers during a spec-version bump, a generator-template change, or the addition of a new wire-format codec. End users consume the regenerated `*.g.cs` files transparently through the shipped NuGet packages.
-- A new spec version is tagged in `mtconnect/mtconnect_sysml_model` and the library is being moved onto it.
-- A code-side change to the generator templates lands and the `*.g.cs` files need to be regenerated.
-- A new generator-output target is added (e.g. a new wire-format codec).
+The CLI surface is defined by `Program.cs` in `build/MTConnect.NET-SysML-Import/`. See also the auto-generated [CLI reference entry](../reference/cli#mtconnect-net-sysml-import), which is emitted from the same source at docs-build time.
-End users do not run this tool. End users consume the regenerated `*.g.cs` files transparently through the shipped NuGet packages.
+## Synopsis
-## Source
+```text
+dotnet run --project build/MTConnect.NET-SysML-Import -- \
+ --new-xmi \
+ --output \
+ [--previous-xmi ] \
+ [--compat-version-label ] \
+ [--full-tree] \
+ [--json-dump ]
+```
-- Project: `build/MTConnect.NET-SysML-Import/MTConnect.NET-SysML-Import.csproj`
-- Entry point: `build/MTConnect.NET-SysML-Import/Program.cs`
-- Renderer templates: `build/MTConnect.NET-SysML-Import/CSharp/`, `build/MTConnect.NET-SysML-Import/Xml/`, `build/MTConnect.NET-SysML-Import/Json-cppagent/`
-- Parser library: `libraries/MTConnect.NET-SysML/` (the `MTConnect.SysML` namespace — XMI loader plus the in-memory `MTConnectModel`).
+`--new-xmi` and `--output` are required. Every other flag is optional. The default mode is **zero-config delta**: the importer auto-derives PREV_VERSION from `MTConnectVersions.Max` in the current tree and resolves the prior-version XMI from a well-known cache path or a tag-gated submodule checkout. No hand-editing of `Program.cs` is needed.
-## Current CLI surface
+## Flags
-`Program.cs` is a top-level statement file with **hardcoded paths**, not a parameterized CLI. Running it executes four steps in sequence against the input file and the output directories listed at the top of `Program.cs`:
+| Flag | Argument | Description |
+|---|---|---|
+| `--new-xmi` | `` | SysML XMI file to consume. Required. Preferred spelling; `--xmi` remains as a legacy alias. |
+| `--xmi` | `` | Legacy alias for `--new-xmi`. Kept for backwards compatibility with existing callers; new invocations should prefer `--new-xmi`. Both flags land on the same `newXmiPath` slot; passing both is legal (the second wins). |
+| `--output` | `` | Repository root. Each subgenerator writes into its own `libraries//` subtree under this root. Required. |
+| `--previous-xmi` | `` | Edge-case override for delta-driven mode. When supplied, the importer uses this file as the previous-version XMI and skips the zero-config auto-derive step. Typical use cases: cross-version audit runs, regenerating against a historical XMI snapshot, and version bumps that skip a version (where `MTConnectVersions.Max` does not match the intended PREV_VERSION). |
+| `--compat-version-label` | `` | Label used for the `Compat/.g.cs` file name in delta mode. When `--previous-xmi` is supplied without an explicit label, defaults to `Previous` for backwards compatibility. When the previous-XMI is auto-derived from `MTConnectVersions.Max`, defaults to `v${PREV_XY_UNDERSCORE}` (e.g. `v2_7`). Rejected at argument parse time if the value would produce an unsafe filename (path separators, drive letters, ASCII control chars, leading dots, or length outside 1–64 chars). |
+| `--full-tree` | — | Explicit opt-in for the full-regeneration path. Disables both the zero-config auto-derive delta and the `--previous-xmi` override; every emitted `*.g.cs` re-lands under its normal path. Use this when the delta path is impossible (no prior XMI available, no cache populated, submodule tag unknown) or when reviewing the whole generated tree in a single diff. |
+| `--json-dump` | `` | Optional. Writes the parsed `MTConnectModel` as JSON to `` for debugging. Runs before the renderers so the dump reflects the exact input to the delta step. |
+| `--help`, `-h` | — | Print usage information and exit. |
-1. **Parse XMI** — `MTConnectModel.Parse(xmlPath)` reads the XMI export.
-2. **Render JSON dump** — serializes the parsed model to a JSON file (currently at `C:\temp\mtconnect-model.json`). Useful for inspecting what the parser saw.
-3. **Render `MTConnect.NET-Common` C# classes** — `CSharpTemplateRenderer.Render(model, outputPath)` writes `*.g.cs` files into `libraries/MTConnect.NET-Common/`.
-4. **Render `MTConnect.NET-JSON-cppagent` codec classes** — `JsonCppAgentTemplateRenderer.Render(model, outputPath)` writes `*.g.cs` files into `libraries/MTConnect.NET-JSON-cppagent/`.
-5. **Render `MTConnect.NET-XML` codec classes** — `XmlTemplateRenderer.Render(model, outputPath)` writes `*.g.cs` files into `libraries/MTConnect.NET-XML/`.
+## Modes
-The three paths a maintainer typically needs to edit before running the tool are at the top of `Program.cs`:
+The importer picks one of three modes based on the flag combination:
-| Variable | Purpose | Current default |
-|---|---|---|
-| `xmlPath` | XMI input file path. The `MTConnectSysMLModel.xml` file produced by exporting the standard's SysML model. | `D:\TrakHound\MTConnect\Standard\v2.5\MTConnectSysMLModel.xml` |
-| The JSON dump path inside `RenderJsonFile()` | Where to write the in-memory model as JSON (for inspection / diffing). | `C:\temp\mtconnect-model.json` |
-| The `outputPath` literals inside `RenderCommonClasses()` / `RenderJsonComponents()` / `RenderXmlComponents()` | Relative paths from the executable to each target library, computed against `AppDomain.CurrentDomain.BaseDirectory`. | `../../../../../libraries/MTConnect.NET-Common`, `../../../../../libraries/MTConnect.NET-JSON-cppagent`, `../../../../../libraries/MTConnect.NET-XML` (resolved from `bin/{Configuration}/{TFM}/` so they land in the source tree). |
+### Delta (zero-config, default)
+
+Fires when neither `--previous-xmi` nor `--full-tree` is supplied. The importer parses `libraries/MTConnect.NET-Common/MTConnectVersions.cs` under `--output`, extracts `PREV_VERSION` from the `Max` property (e.g. `Max => Version27` resolves to `2.7`), and resolves the prior-version XMI using one of these strategies in order:
+
+1. **Strategy B (primary)** — `build/.cache/sysml-prev/MTConnectSysMLModel_v${PREV_VERSION}.xml` under `--output`. Populated by the maintainer as part of the version-bump runbook (Phase 3.2).
+2. **Strategy A (fallback)** — `build/sysml-model/MTConnectSysMLModel.xml`, gated on the submodule tip being checked out at tag `v${PREV_VERSION}` exactly. `git -C build/sysml-model describe --exact-match --tags HEAD` must return the expected tag; anything else falls through.
+3. **Strategy C (fail-hard)** — neither resolves. The importer throws with an actionable message naming the probed cache path, the expected submodule tag, the `--previous-xmi` override, and the `--full-tree` escape hatch.
+
+The auto-derived Compat label is `v${X}_${Y}` (e.g. `v2_7`), matching the resolved PREV_VERSION. An explicit `--compat-version-label` overrides that default.
+
+### Delta (`--previous-xmi` override)
+
+Fires when `--previous-xmi ` is supplied without `--full-tree`. The importer uses the supplied file directly as the prior-version XMI and skips the auto-derive resolver entirely. The Compat label defaults to `Previous` in this mode (legacy behavior) unless `--compat-version-label` is passed.
+
+Use this mode for cross-version audit runs, historical XMI snapshots, or version bumps that skip a version.
+
+### Full-tree (`--full-tree`)
+
+Fires when `--full-tree` is supplied. Disables both delta paths and re-emits every generated file under its normal `libraries//…/*.g.cs` path. Preserves the pre-Phase-4 behavior bit for bit.
+
+Use this mode when the delta path is impossible (no cache, wrong submodule tag) or when the maintainer wants to review the full generated tree in a single diff.
+
+## Delta emission
+
+In delta mode the importer renders both XMIs into isolated scratch directories, diffs them at the file level, and writes only the difference back to `--output`:
+
+- **ADDED** (in NEW only) — written normally under the target library.
+- **CHANGED** (in both, different bytes) — written normally (the NEW tree's version).
+- **REMOVED** (in PREV only) — deleted from `--output` so the type stops shipping.
+- **UNCHANGED** (in both, identical bytes) — concentrated into `Compat/.g.cs` per library; the individual per-type `*.g.cs` is deleted so the Compat file is the sole namespace host (avoids `CS0101` duplicate-type errors when the delta runs against a repo already carrying the committed `*.g.cs` tree).
+
+The stats line printed to stdout (`Delta emission: added=A changed=C removed=R unchanged-concentrated=U`) is the operator's summary of what landed.
+
+## Example invocations
+
+Zero-config delta against the checked-in submodule XMI:
+
+```bash
+dotnet run --project build/MTConnect.NET-SysML-Import -- \
+ --new-xmi build/sysml-model/MTConnectSysMLModel.xml \
+ --output .
+```
+
+The importer parses `MTConnectVersions.Max`, probes the cache and submodule tag, and emits the delta.
+
+Version bump that skips a version — pass the historical XMI explicitly:
+
+```bash
+dotnet run --project build/MTConnect.NET-SysML-Import -- \
+ --new-xmi ~/xmi/MTConnectSysMLModel-v2.9.xml \
+ --previous-xmi ~/xmi/MTConnectSysMLModel-v2.5.xml \
+ --compat-version-label v2_5 \
+ --output .
+```
+
+Full regeneration (delta paths disabled):
+
+```bash
+dotnet run --project build/MTConnect.NET-SysML-Import -- \
+ --new-xmi build/sysml-model/MTConnectSysMLModel.xml \
+ --output . \
+ --full-tree
+```
+
+Dump the parsed model to JSON alongside a delta regen:
+
+```bash
+dotnet run --project build/MTConnect.NET-SysML-Import -- \
+ --new-xmi build/sysml-model/MTConnectSysMLModel.xml \
+ --output . \
+ --json-dump /tmp/mtconnect-model.json
+```
+
+Windows / PowerShell uses the same command shape; only the paths change.
+
+## Exit codes
-A future change may refactor `Program.cs` to read `--xmi `, `--output `, and `--json-dump ` from `args[]`; this page will be updated when that lands. Until then, the workflow is: edit the three constants, save, run, commit the regenerated `*.g.cs`.
+| Code | Meaning |
+|---|---|
+| `0` | Success. Delta / full-tree emission completed and every renderer returned cleanly. |
+| `1` | Runtime failure — XMI file not found, output root not found, parse error, or the auto-derive fail-hard message. Details on stderr with an `error:` prefix. |
+| `2` | Usage error — missing required flag (`--new-xmi`, `--output`), unknown flag, or an unsafe `--compat-version-label` value. Help text is reprinted on stderr. |
-## Example workflow
+## Maintainer workflow
-The full maintainer workflow to advance the library onto a new spec version:
+The full workflow to advance the library onto a new spec version:
```bash
-# 1. Fetch the new XMI from mtconnect/mtconnect_sysml_model
-git -C ~/git/mtconnect/mtconnect_sysml_model pull
-cp ~/git/mtconnect/mtconnect_sysml_model/MTConnectSysMLModel.xml \
- ~/MTConnect/Standard/v2.6/MTConnectSysMLModel.xml
+# 1. Fetch the new XMI from mtconnect/mtconnect_sysml_model into the submodule
+git submodule update --remote build/sysml-model
-# 2. Edit build/MTConnect.NET-SysML-Import/Program.cs:
-# point xmlPath at the new v2.6 file.
+# 2. Populate the prior-version cache so the auto-derive resolver has a hit
+mkdir -p build/.cache/sysml-prev
+cp build/sysml-model/MTConnectSysMLModel.xml \
+ build/.cache/sysml-prev/MTConnectSysMLModel_v2.7.xml
+git -C build/sysml-model checkout v2.8
-# 3. Run the importer
-dotnet run --project build/MTConnect.NET-SysML-Import
+# 3. Run the importer (zero-config delta)
+dotnet run --project build/MTConnect.NET-SysML-Import -- \
+ --new-xmi build/sysml-model/MTConnectSysMLModel.xml \
+ --output .
# 4. Verify the regenerated *.g.cs files compile + tests pass
tools/test.sh
-# 5. Diff the regenerated output to confirm only spec-driven changes landed
-git diff --stat libraries/MTConnect.NET-Common/**/*.g.cs
+# 5. Diff the delta output to confirm only spec-driven changes landed
+git diff --stat libraries/**/*.g.cs
# 6. Commit the regeneration in a single commit per spec version
git add libraries/**/*.g.cs
-git commit -m "build(sysml): regenerate against v2.6 XMI"
+git commit -m "build(sysml): regenerate against v2.8 XMI"
```
-On Windows the same workflow works under PowerShell; the only path that needs editing is the `xmlPath` constant in `Program.cs`, which is currently spelled as a Windows-style path.
+See `build/MTConnect.NET-SysML-Import/README.md` for the full "Adding a new MTConnect Standard version" runbook, the determinism guarantee (regen against a pinned XMI tag must produce zero diff), and the delta-mode design notes (plan D4 — partial-class re-emit, not `[TypeForwardedTo]`).
## Configuration
-The tool has no configuration file. All inputs and outputs are literal strings in `Program.cs`. The renderer templates themselves are checked in under `build/MTConnect.NET-SysML-Import/CSharp/`, `build/MTConnect.NET-SysML-Import/Xml/`, and `build/MTConnect.NET-SysML-Import/Json-cppagent/`; editing a template changes what the next regeneration emits.
+The importer has no configuration file. Every input and output is supplied on the command line. The renderer templates themselves are checked in under `build/MTConnect.NET-SysML-Import/CSharp/`, `build/MTConnect.NET-SysML-Import/Xml/`, and `build/MTConnect.NET-SysML-Import/Json-cppagent/`; editing a template changes what the next regeneration emits.
## Output discipline
-- The regenerator overwrites every `*.g.cs` file it produces. Files that are not regenerated (because the corresponding SysML element disappeared in the new spec version) are **not** deleted — the maintainer reviews the diff and removes orphans manually.
+- The regenerator overwrites every `*.g.cs` file it produces. In delta mode, REMOVED files (types the spec dropped) are actively deleted from `--output`. In full-tree mode, orphan files (regenerator output that no longer maps to a live SysML element) are **not** deleted — the maintainer reviews the diff and removes orphans manually.
- Hand-written files alongside the generated ones use the convention `.cs` (hand-written) versus `.g.cs` (generated). The hand-written file typically adds members the generator does not produce (e.g. helper methods, secondary constructors); both files live in the same `partial class`.
- `git diff libraries/**/*.g.cs` after a regeneration is the authoritative review surface for spec-version advancement.
@@ -95,10 +181,11 @@ A regeneration is considered clean when (a) the test suite is green at every pre
## See also
+- [CLI reference → `MTConnect.NET-SysML-Import`](../reference/cli#mtconnect-net-sysml-import) — the auto-generated flag table emitted from `Program.cs` at docs-build time.
- [Configure & Use → Run](/configure/run) — running the agent against the regenerated library to verify end-to-end behavior.
- [Compliance](/compliance/) — the per-version compliance matrix that the regenerator advances.
- [API reference → `MTConnect.SysML.MTConnectModel`](/api/MTConnect.SysML.MTConnectModel) — the in-memory model the XMI parser produces and the renderers walk.
- [API reference → `MTConnect.SysML.ModelHelper`](/api/MTConnect.SysML.ModelHelper) — the helper surface the per-language renderers call into.
-- [API reference → `MTConnect.SysML` namespace](/api/MTConnect.SysML) — the SysML model + per-renderer entry points.
+- [API reference → `MTConnect.SysML` namespace](/api/MTConnect.SysML) — the SysML model plus per-renderer entry points.
- [`tools/test.sh`](./test-sh) — runs after a regeneration to verify the suite stays green.
- [`tools/dotnet.sh`](./dotnet-sh) — wraps the `dotnet run` invocation if the regeneration is being done inside a containerized SDK.
diff --git a/docs/reference/cli.md b/docs/reference/cli.md
index 77ddd2945..46df1bd1b 100644
--- a/docs/reference/cli.md
+++ b/docs/reference/cli.md
@@ -85,10 +85,14 @@ Parses an `MTConnectSysMLModel.xml` (the XMI export of the standard's SysML mode
| Flag | Short | Argument | Description |
| --- | --- | --- | --- |
+| `--compat-version-label` | | `` | <label> Label used for the Compat/<label>.g.cs file name in delta mode. When --previous-xmi is supplied without an explicit label, defaults to "Previous" for backwards compatibility. When the previous-XMI is auto-derived from MTConnectVersions.Max, defaults to "v${PREV_XY_UNDERSCORE}" (e.g. "v2_7"). |
+| `--full-tree` | | | Explicit opt-in for the full-regeneration path. Disables both the zero-config auto-derive delta and the --previous-xmi override; every emitted .g.cs re-lands under its normal path. |
| `--help` | | | |
| `--json-dump` | | `` | Optional. Writes the parsed MTConnectModel as JSON for debugging. |
+| `--new-xmi` | | `` | SysML XMI file to consume. Required. Preferred spelling; --xmi remains as a legacy alias. |
| `--output` | | `` | Repository root. Each subgenerator writes into its own libraries/<LibraryName>/ subtree under this root. Required. |
-| `--xmi` | | `` | SysML XMI file to consume. Required. |
+| `--previous-xmi` | | `` | Edge-case override for delta-driven mode. When supplied, uses this file as the previous-version XMI and skips the zero-config auto-derive step. Typical use cases: cross-version audit runs, regenerating against a historical XMI snapshot, and version-bumps that skip a version (where MTConnectVersions.Max does not match the intended PREV_VERSION). |
+| `--xmi` | | `` | Legacy alias for --new-xmi. Kept for backwards compatibility with existing callers; new invocations should prefer --new-xmi. |
### `dotnet.sh`
diff --git a/docs/testing.md b/docs/testing.md
index 6edc3ad5b..7a556c7f2 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -6,17 +6,19 @@ This page is the entry point for everything test-related in MTConnect.NET. Per-v
- [`docs/testing/v2-6.md`](testing/v2-6.md) — MTConnect Standard v2.6 compliance matrix.
- [`docs/testing/v2-7.md`](testing/v2-7.md) — MTConnect Standard v2.7 compliance matrix.
+- [`docs/testing/version-matrix-convention.md`](testing/version-matrix-convention.md) — topic-first single-file-per-topic fixture convention (how to add tests for a new spec version).
- [`docs/testing/workflows.md`](testing/workflows.md) — CI workflow + local harness catalog.
Each matrix lists every spec-defined element / attribute / enum value introduced or modified at that version with status (`Live` / `Pending`) and the test class that pins it.
## Test tiers
-The repo organizes tests into three tiers:
+The repo organizes tests into four tiers:
1. **Unit + integration** — `tests/-Tests/`. Fast (< 30 s on a clean run), runs by default in CI and on `tools/test.sh` / `tools/test.ps1`. Filtered by `Category!=XsdLoadStrict` so the strict XSD-load gate does not block the green path.
2. **Compliance** — `tests/Compliance/MTConnect-Compliance-Tests/`. Layered (`L1_XsdValidation`, `L2_CrossImpl`); see [`tests/Compliance/MTConnect-Compliance-Tests/README.md`](https://github.com/TrakHound/MTConnect.NET/blob/master/tests/Compliance/MTConnect-Compliance-Tests/README.md). Opt-in via `tools/test.sh --compliance` or `tools/test.ps1 -Compliance`.
3. **E2E** — `tests/MTConnect.NET-Integration-Tests/` + `tests/E2E/**`. Docker-gated. Opt-in via `tools/test.sh --e2e` or `MTCONNECT_E2E_DOCKER=true`.
+4. **Generator regen guards** — `tests/MTConnect.NET-Generator-Tests/`. Dispatches the `build/MTConnect.NET-SysML-Import` CLI via `dotnet run --no-build` and asserts byte-identical regeneration against the current XMI (`Regen_is_deterministic_across_two_invocations` + `Current_XMI_regen_matches_committed_g_cs_tree`) plus surgical delta capture on a mutated-XMI cross-verify (`Delta_mode_against_same_XMI_concentrates_every_file_into_Compat` + `Delta_mode_against_mutated_XMI_emits_only_the_changed_file`). Complementary CLI failure-path + Compat body + stats-line invariants pinned by `CliInvocationFailureTests` and `DeltaCompatAndStatsTests`. Runs by default in the standard `dotnet test` sweep; see [`docs/testing/mutation-testing.md`](testing/mutation-testing.md) for the paired Stryker.NET mutation-score gate.
## Local entry points
diff --git a/docs/testing/mutation-testing.md b/docs/testing/mutation-testing.md
new file mode 100644
index 000000000..220fd02e8
--- /dev/null
+++ b/docs/testing/mutation-testing.md
@@ -0,0 +1,77 @@
+# Mutation testing — Stryker.NET
+
+Stryker.NET is the mutation-testing framework adopted as the Ultrareview coverage-quality gate in PR #233 (per user D3 directive, 2026-08-20). Mutation testing complements line / branch coverage by mutating the code under test and asserting that at least one test fails per mutation — surviving mutants are gaps the coverage report cannot see.
+
+## Configuration
+
+`stryker-config.json` at the repo root pins the entry-point project + test project:
+
+```jsonc
+// Baseline mutation score is 7.75% on 2026-08-20 (see the JSONC header
+// on the shipped stryker-config.json for the full provenance block).
+{
+ "stryker-config": {
+ "project": "MTConnect.NET-Common.csproj",
+ "solution": "MTConnect.NET.sln",
+ "test-projects": ["tests/MTConnect.NET-Common-Tests/MTConnect.NET-Common-Tests.csproj"],
+ "target-framework": "net8.0",
+ "reporters": ["progress", "cleartext", "html", "json"],
+ "thresholds": { "high": 8, "low": 5, "break": 5 },
+ "concurrency": 4,
+ "mutation-level": "Complete",
+ "mutate": [
+ "!**/*.g.cs",
+ "!libraries/MTConnect.NET-Common/Assets/**/*.g.cs",
+ "!libraries/MTConnect.NET-Common/Devices/**/*.g.cs",
+ "!libraries/MTConnect.NET-Common/Observations/**/*.g.cs"
+ ],
+ "ignore-mutations": ["Regex"]
+ }
+}
+```
+
+Key choices:
+
+- **Target: `MTConnect.NET-Common`** — the largest hand-authored surface. Subsequent adoptions extend the roster (`MTConnect.NET-Generator-Tests`, `MTConnect.NET-XML`, `MTConnect.NET-JSON-cppagent`) once the Common project reaches zero surviving mutants.
+- **Reporters** — `progress` + `cleartext` for the terminal replay, `html` for browsable maintainer report, `json` for CI ingestion. No `dashboard` reporter (no external API surface).
+- **Thresholds: 8 / 5 / 5** — pinned above the 7.75 % baseline established on 2026-08-20 (Stryker.NET v4.16.0, Regex mutator ignored). `break: 5` and `low: 5` sit below the baseline so a baseline-conforming run passes CI; `high: 8` sits above so the same run reports yellow rather than green, keeping visible pressure on the phased campaign at TrakHound/MTConnect.NET#242 to raise the floor (20 -> 40 -> 60 -> 80 %+). The long-term Ultrareview target under CONVENTIONS §1.0d-trigies-septdecies remains 100 %.
+- **Mutate excludes: every `*.g.cs`** — generator output is not hand-authored code; mutating it produces meaningless results. Fidelity of the generator emission is guarded by `tests/MTConnect.NET-Generator-Tests/` byte-identity + delta cross-verify tests.
+- **Concurrency: 4** — matches the bluefin CPU budget without oversubscribing.
+
+## Running locally
+
+Install the tool once per machine:
+
+```bash
+dotnet tool install -g dotnet-stryker
+```
+
+Then, from the repo root:
+
+```bash
+dotnet stryker
+```
+
+A typical run takes 30 – 60 minutes on the `MTConnect.NET-Common` surface (four-way concurrency, ~250 hand-authored source files). Results land under `StrykerOutput//` — open `reports/mutation-report.html` for the browsable report and `mutation-report.json` for automation.
+
+## Running in CI
+
+The Stryker gate is not wired into `dotnet.yml` yet; the config lands standalone in PR #233 with the runner integration deferred to a follow-up PR per user directive. When wired, the workflow shape is `dotnet stryker --config-file stryker-config.json --reporter json` on a nightly cron + label-triggered on-demand, uploading `StrykerOutput/**/*` as an artefact and failing the job on `--break-at 5` (the pinned baseline gate — see the top-of-file JSONC comment in `stryker-config.json` and TrakHound/MTConnect.NET#242 for the phased raise).
+
+## Handling surviving mutants
+
+Every surviving mutant has three acceptable dispositions:
+
+1. **Killed by a new test.** Add a test that would fail if the mutation were shipped, land it in the same PR that introduced the surface. This is the default disposition — 99 % of surviving mutants deserve a matching test.
+2. **Explicit exclusion with rationale.** Add the mutant to `stryker-config.json`'s `mutate.excluded-mutations` list (or use a `// Stryker disable next-line ` pragma at the source site) with a comment explaining why the mutation is spec-equivalent / performance-equivalent / defensively-unreachable. Rare — needs code-level rationale.
+3. **Deferred to the coverage-quality campaign.** Until TrakHound/MTConnect.NET#242 raises the pinned break threshold in step, survivors that keep the score at or above the pinned break (5 %) do not block merge; catalogue them per subsystem in the #242 phase plan. This disposition is a scoped transitional accommodation, not a general-purpose escape hatch — every survivor still needs an eventual disposition 1 or 2.
+
+Zero surviving mutants (or fully-justified exclusions) remains the long-term merge gate; the pinned 7.75 % baseline is the interim floor per #242.
+
+## References
+
+- Stryker.NET:
+- Configuration options:
+- CONVENTIONS §1.0d-trigies-septdecies (Ultrareview coverage-quality gate)
+- PR #233 adoption commit — `chore(tests): adopt Stryker.NET mutation-testing framework`
+- TrakHound/MTConnect.NET#242 — phased coverage-quality campaign that raises the pinned thresholds toward 100 %
diff --git a/docs/testing/v2-6.md b/docs/testing/v2-6.md
index e6ecf8dea..9d2a3e6da 100644
--- a/docs/testing/v2-6.md
+++ b/docs/testing/v2-6.md
@@ -10,27 +10,27 @@ XMI source: [`mtconnect/mtconnect_sysml_model`](https://github.com/mtconnect/mtc
| TypeId | Class | Category | Pinned test |
|---|---|---|---|
-| `ASSET_ADDED` | `AssetAddedDataItem` | EVENT | `V2_6DataItemTypeTests.AssetAddedDataItem_*` |
-| `ASSOCIATED_ASSET_ID` | `AssociatedAssetIdDataItem` | EVENT | `V2_6DataItemTypeTests.AssociatedAssetIdDataItem_*` |
+| `ASSET_ADDED` | `AssetAddedDataItem` | EVENT | `Devices/DataItems/DataItemTypeTests.AssetAddedDataItem_*` |
+| `ASSOCIATED_ASSET_ID` | `AssociatedAssetIdDataItem` | EVENT | `Devices/DataItems/DataItemTypeTests.AssociatedAssetIdDataItem_*` |
## New Component types
| TypeId | Class | Pinned test |
|---|---|---|
-| `CuttingTorch` | `CuttingTorchComponent` | `V2_6ComponentAndEnumTests.CuttingTorchComponent_constructs_with_correct_type` |
-| `Electrode` | `ElectrodeComponent` | `V2_6ComponentAndEnumTests.ElectrodeComponent_constructs_with_correct_type` |
+| `CuttingTorch` | `CuttingTorchComponent` | `Devices/Components/ComponentTests.CuttingTorchComponent_constructs_with_correct_type` |
+| `Electrode` | `ElectrodeComponent` | `Devices/Components/ComponentTests.ElectrodeComponent_constructs_with_correct_type` |
## New enum values
| Enum | Value | File | Pinned test |
|---|---|---|---|
-| `MediaType` | `QIF_MBD` | `Devices/Configurations/MediaType.g.cs` | `V2_6ComponentAndEnumTests.MediaType_QIF_MBD_value_present_in_v2_6` |
+| `MediaType` | `QIF_MBD` | `Devices/Configurations/MediaType.g.cs` | `Enums/EnumArmTests.MediaType_QIF_MBD_value_present` |
## Modified types (docstring + structural)
| File | Change | Pinned test |
|---|---|---|
-| `AssetChangedDataItem.g.cs` | Description narrowed to "AssetId of the Asset that has been changed"; the additions case is now covered by `AssetAddedDataItem`. | `V2_6DataItemTypeTests.AssetChangedDataItem_description_narrowed_in_v2_6` |
+| `AssetChangedDataItem.g.cs` | Description narrowed to "AssetId of the Asset that has been changed"; the additions case is now covered by `AssetAddedDataItem`. | `Devices/DataItems/DataItemTypeTests.AssetChangedDataItem_description_narrowed` |
| `Configuration.g.cs` + `IConfiguration.g.cs` | `Relationships` description: now allows asset-to-asset associations. | covered by regen |
| `AssetRelationship.g.cs` + `IAssetRelationship.g.cs` | Description: now allows asset-to-asset, not just component-to-asset. | covered by regen |
| `ConfigurationRelationship.g.cs`, `ComponentRelationship.g.cs`, `DeviceRelationship.g.cs` and matching `I*.g.cs` | Docstring tweaks. | covered by regen |
@@ -47,11 +47,12 @@ XMI source: [`mtconnect/mtconnect_sysml_model`](https://github.com/mtconnect/mtc
## Test classes
-All tests live under `tests/MTConnect.NET-Common-Tests/V2_6_V2_7/`:
+Fixtures follow the topic-first single-file-per-topic layout established by the Phase 1 DRY-generator consolidation (see [`version-matrix-convention.md`](./version-matrix-convention.md)); version-gated assertions run across `MTConnectVersionMatrix.All` with `Assume.That(v, Is.GreaterThanOrEqualTo(...))` gates:
-- `MTConnectVersionsTests` — `Version26` / `Version27` constants, `Max == Version27`, reflection sweep over all 17 versions, no `v1.9` constant present.
-- `V2_6DataItemTypeTests` — `AssetAdded` + `AssociatedAssetId` construction + `DataItem` inheritance + `AssetChanged` description regression pin.
-- `V2_6ComponentAndEnumTests` — `CuttingTorch` + `Electrode` components, `MediaType.QIF_MBD` enum value.
+- `tests/MTConnect.NET-Common-Tests/MTConnectVersionsTests.cs` — `Version26` / `Version27` constants, `Max == Version27`, reflection sweep over all 17 versions, no `v1.9` constant present. Kept as plain `[Test]` (constant-value invariants).
+- `tests/MTConnect.NET-Common-Tests/Devices/DataItems/DataItemTypeTests.cs` — `AssetAdded` + `AssociatedAssetId` construction + `DataItem` inheritance + `AssetChanged` description regression pin (matrix-parameterised; v2.6 floor).
+- `tests/MTConnect.NET-Common-Tests/Devices/Components/ComponentTests.cs` — `CuttingTorch` + `Electrode` components (matrix-parameterised; v2.6 floor).
+- `tests/MTConnect.NET-Common-Tests/Enums/EnumArmTests.cs` — `MediaType.QIF_MBD` enum value (matrix-parameterised; v2.6 floor).
## XSD compliance
diff --git a/docs/testing/v2-7.md b/docs/testing/v2-7.md
index c9af731c5..b981179fc 100644
--- a/docs/testing/v2-7.md
+++ b/docs/testing/v2-7.md
@@ -10,14 +10,14 @@ XMI source: [`mtconnect/mtconnect_sysml_model`](https://github.com/mtconnect/mtc
| TypeId | Class | Category | Pinned test |
|---|---|---|---|
-| `BINDING_STATE` | `BindingStateDataItem` | EVENT | `V2_7DataItemTypeTests.V2_7_DataItem_constructs_with_correct_metadata` (BindingStateDataItem case) |
-| `DEPTH` | `DepthDataItem` | EVENT | `V2_7DataItemTypeTests.V2_7_DataItem_constructs_with_correct_metadata` (DepthDataItem case) |
-| `FIXTURE_ASSET_ID` | `FixtureAssetIdDataItem` | EVENT | `V2_7DataItemTypeTests.V2_7_DataItem_constructs_with_correct_metadata` (FixtureAssetIdDataItem case) |
-| `SWING_ANGLE` | `SwingAngleDataItem` | EVENT | `V2_7DataItemTypeTests.V2_7_DataItem_constructs_with_correct_metadata` (SwingAngleDataItem case) |
-| `SWING_DIAMETER` | `SwingDiameterDataItem` | EVENT | `V2_7DataItemTypeTests.V2_7_DataItem_constructs_with_correct_metadata` (SwingDiameterDataItem case) |
-| `SWING_RADIUS` | `SwingRadiusDataItem` | EVENT | `V2_7DataItemTypeTests.V2_7_DataItem_constructs_with_correct_metadata` (SwingRadiusDataItem case) |
-| `TASK_ASSET_ID` | `TaskAssetIdDataItem` | EVENT | `V2_7DataItemTypeTests.V2_7_DataItem_constructs_with_correct_metadata` (TaskAssetIdDataItem case) |
-| `WATER_HARDNESS` | `WaterHardnessDataItem` | SAMPLE | `V2_7DataItemTypeTests.V2_7_DataItem_constructs_with_correct_metadata` (WaterHardnessDataItem case) + `V2_7SampleObservationTests.WaterHardness_*` |
+| `BINDING_STATE` | `BindingStateDataItem` | EVENT | `Devices/DataItems/DataItemTypeTests.DataItem_constructs_with_correct_metadata` (BindingStateDataItem case) |
+| `DEPTH` | `DepthDataItem` | EVENT | `Devices/DataItems/DataItemTypeTests.DataItem_constructs_with_correct_metadata` (DepthDataItem case) |
+| `FIXTURE_ASSET_ID` | `FixtureAssetIdDataItem` | EVENT | `Devices/DataItems/DataItemTypeTests.DataItem_constructs_with_correct_metadata` (FixtureAssetIdDataItem case) |
+| `SWING_ANGLE` | `SwingAngleDataItem` | EVENT | `Devices/DataItems/DataItemTypeTests.DataItem_constructs_with_correct_metadata` (SwingAngleDataItem case) |
+| `SWING_DIAMETER` | `SwingDiameterDataItem` | EVENT | `Devices/DataItems/DataItemTypeTests.DataItem_constructs_with_correct_metadata` (SwingDiameterDataItem case) |
+| `SWING_RADIUS` | `SwingRadiusDataItem` | EVENT | `Devices/DataItems/DataItemTypeTests.DataItem_constructs_with_correct_metadata` (SwingRadiusDataItem case) |
+| `TASK_ASSET_ID` | `TaskAssetIdDataItem` | EVENT | `Devices/DataItems/DataItemTypeTests.DataItem_constructs_with_correct_metadata` (TaskAssetIdDataItem case) |
+| `WATER_HARDNESS` | `WaterHardnessDataItem` | SAMPLE | `Devices/DataItems/DataItemTypeTests.DataItem_constructs_with_correct_metadata` (WaterHardnessDataItem case) + `Observations/SampleObservationTests.WaterHardness_*` |
Several types that look "measurement-y" (`SwingAngle`, `SwingDiameter`, `SwingRadius`, `Depth`) are EVENT in the v2.7 spec rather than SAMPLE. The pinned test locks the spec category so a future regen drift is caught immediately.
@@ -25,33 +25,33 @@ Several types that look "measurement-y" (`SwingAngle`, `SwingDiameter`, `SwingRa
| TypeId | Class | Pinned test |
|---|---|---|
-| `PinTool` | `PinToolComponent` | `V2_7ComponentTests.PinToolComponent_constructs_with_correct_type` |
-| `ToolHolder` | `ToolHolderComponent` | `V2_7ComponentTests.ToolHolderComponent_constructs_with_correct_type` |
+| `PinTool` | `PinToolComponent` | `Devices/Components/ComponentTests.PinToolComponent_constructs_with_correct_type` |
+| `ToolHolder` | `ToolHolderComponent` | `Devices/Components/ComponentTests.ToolHolderComponent_constructs_with_correct_type` |
## New Configuration sub-elements (geometric primitives + DataSet variants)
v2.7 introduces five geometric primitives (`Axis`, `Origin`, `Rotation`, `Scale`, `Translation`) under `Devices/Configurations/`, each with three concrete forms:
-- An `Abstract` base class — pinned-abstract by `V2_7ConfigurationDataSetTests.Abstract_is_abstract`.
-- A concrete `` element — pinned by `V2_7ConfigurationDataSetTests._inherits_Abstract` (`_and_constructs` for `Axis`).
-- A concrete `DataSet` data-set sibling — pinned by `V2_7ConfigurationDataSetTests.DataSet_*`.
+- An `Abstract` base class — pinned-abstract by `Devices/Configurations/ConfigurationTests.Abstract_is_abstract`.
+- A concrete `` element — pinned by `Devices/Configurations/ConfigurationTests._inherits_Abstract` (`_and_constructs` for `Axis`).
+- A concrete `DataSet` data-set sibling — pinned by `Devices/Configurations/ConfigurationTests.DataSet_*`.
-The five primitives also share a new abstract `DataSet` base (and its `IDataSet` interface) under `Devices/Configurations/DataSet.g.cs`. The base is grafted from the SysML `Observation.Representations` package via the cross-package parent resolver in `MTConnectClassModel.ResolveDanglingParents`, so the entire family compiles even though the parent's home package is `Observation`. Pinned by `V2_7ConfigurationDataSetTests.DataSet_base_constructs_and_implements_IDataSet`.
+The five primitives also share a new abstract `DataSet` base (and its `IDataSet` interface) under `Devices/Configurations/DataSet.g.cs`. The base is grafted from the SysML `Observation.Representations` package via the cross-package parent resolver in `MTConnectClassModel.ResolveDanglingParents`, so the entire family compiles even though the parent's home package is `Observation`. Pinned by `Devices/Configurations/ConfigurationTests.DataSet_base_constructs_and_implements_IDataSet`.
| Family | Concrete | DataSet variant | Pinned test |
|---|---|---|---|
-| `AbstractAxis` | `Axis` | `AxisDataSet` | `V2_7ConfigurationDataSetTests.{AbstractAxis_is_abstract,Axis_inherits_AbstractAxis_and_constructs,AxisDataSet_has_xyz_fields_and_inherits_DataSet}` |
-| `AbstractOrigin` | `Origin` | `OriginDataSet` | `V2_7ConfigurationDataSetTests.{AbstractOrigin_is_abstract,Origin_inherits_AbstractOrigin,OriginDataSet_has_xyz_fields_and_inherits_DataSet}` |
-| `AbstractRotation` | `Rotation` | `RotationDataSet` | `V2_7ConfigurationDataSetTests.{AbstractRotation_is_abstract,Rotation_inherits_AbstractRotation,RotationDataSet_has_abc_fields_and_inherits_DataSet}` |
-| `AbstractScale` | `Scale` | `ScaleDataSet` | `V2_7ConfigurationDataSetTests.{AbstractScale_is_abstract,Scale_inherits_AbstractScale,ScaleDataSet_inherits_DataSet}` |
-| `AbstractTranslation` | `Translation` | `TranslationDataSet` | `V2_7ConfigurationDataSetTests.{AbstractTranslation_is_abstract,Translation_inherits_AbstractTranslation,TranslationDataSet_inherits_DataSet}` |
-| `DataSet` (grafted base) | — | — | `V2_7ConfigurationDataSetTests.DataSet_base_constructs_and_implements_IDataSet` |
+| `AbstractAxis` | `Axis` | `AxisDataSet` | `Devices/Configurations/ConfigurationTests.{AbstractAxis_is_abstract,Axis_inherits_AbstractAxis_and_constructs,AxisDataSet_has_xyz_fields_and_implements_IDataSet}` |
+| `AbstractOrigin` | `Origin` | `OriginDataSet` | `Devices/Configurations/ConfigurationTests.{AbstractOrigin_is_abstract,Origin_inherits_AbstractOrigin,OriginDataSet_has_xyz_fields_and_implements_IDataSet}` |
+| `AbstractRotation` | `Rotation` | `RotationDataSet` | `Devices/Configurations/ConfigurationTests.{AbstractRotation_is_abstract,Rotation_inherits_AbstractRotation,RotationDataSet_has_abc_fields_and_implements_IDataSet}` |
+| `AbstractScale` | `Scale` | `ScaleDataSet` | `Devices/Configurations/ConfigurationTests.{AbstractScale_is_abstract,Scale_inherits_AbstractScale,ScaleDataSet_implements_IDataSet}` |
+| `AbstractTranslation` | `Translation` | `TranslationDataSet` | `Devices/Configurations/ConfigurationTests.{AbstractTranslation_is_abstract,Translation_inherits_AbstractTranslation,TranslationDataSet_implements_IDataSet}` |
+| `DataSet` (grafted base) | — | — | `Devices/Configurations/ConfigurationTests.DataSet_base_constructs_and_implements_IDataSet` |
## New Observation enum
| Enum | File | Pinned test |
|---|---|---|
-| `BindingState` (Event observation enum) | `Observations/Events/BindingState.g.cs` | covered by `V2_7DataItemTypeTests` (BindingStateDataItem case asserts EVENT category) |
+| `BindingState` (Event observation enum) | `Observations/Events/BindingState.g.cs` | covered by `Devices/DataItems/DataItemTypeTests.DataItem_constructs_with_correct_metadata` (BindingStateDataItem case asserts EVENT category) |
## Pallet asset measurements (regenerated)
@@ -73,13 +73,13 @@ The v2.7 XMI rewrites the descriptions / docstrings on every `Assets/Pallet/` me
## Test classes
-All tests live under `tests/MTConnect.NET-Common-Tests/V2_6_V2_7/`:
+Fixtures follow the topic-first single-file-per-topic layout established by the Phase 1 DRY-generator consolidation (see [`version-matrix-convention.md`](./version-matrix-convention.md)); version-gated assertions run across `MTConnectVersionMatrix.All` with `Assume.That(v, Is.GreaterThanOrEqualTo(...))` gates:
-- `MTConnectVersionsTests` — `Version27` constant, `Max == Version27`, reflection sweep across all 17 versions.
-- `V2_7DataItemTypeTests` — eight parametric cases pinning `TypeId` + `Category` for every v2.7 DataItem.
-- `V2_7ComponentTests` — `PinTool` + `ToolHolder` components.
-- `V2_7ConfigurationDataSetTests` — `DataSet` base + `IDataSet`, the `Abstract` / `` / `DataSet` triplet for `Axis` / `Origin` / `Rotation` / `Scale` / `Translation`.
-- `V2_7SampleObservationTests` — round-trip coverage for the SAMPLE-category v2.7 type (`WaterHardness`).
+- `tests/MTConnect.NET-Common-Tests/MTConnectVersionsTests.cs` — `Version27` constant, `Max == Version27`, reflection sweep across all 17 versions. Kept as plain `[Test]` (constant-value invariants).
+- `tests/MTConnect.NET-Common-Tests/Devices/DataItems/DataItemTypeTests.cs::DataItem_constructs_with_correct_metadata` — eight parametric cases pinning `TypeId` + `Category` for every v2.7 DataItem (matrix-parameterised; v2.7 floor).
+- `tests/MTConnect.NET-Common-Tests/Devices/Components/ComponentTests.cs` — `PinTool` + `ToolHolder` components (matrix-parameterised; v2.7 floor).
+- `tests/MTConnect.NET-Common-Tests/Devices/Configurations/ConfigurationTests.cs` — `DataSet` base + `IDataSet`, the `Abstract` / `` / `DataSet` triplet for `Axis` / `Origin` / `Rotation` / `Scale` / `Translation` (matrix-parameterised; v2.7 floor).
+- `tests/MTConnect.NET-Common-Tests/Observations/SampleObservationTests.cs` — round-trip coverage for the SAMPLE-category v2.7 type (`WaterHardness`) (matrix-parameterised; v2.7 floor).
## XSD compliance
diff --git a/docs/testing/version-matrix-convention.md b/docs/testing/version-matrix-convention.md
new file mode 100644
index 000000000..e4053ca05
--- /dev/null
+++ b/docs/testing/version-matrix-convention.md
@@ -0,0 +1,59 @@
+# Version-matrix convention (topic-first single-file-per-topic layout)
+
+Established by the Phase 1 DRY-generator consolidation (PR TrakHound/MTConnect.NET#233, 2026-08-19). Enforced permanently by [`DryGenerator/PerVersionFolderProhibitionTests.cs`](https://github.com/TrakHound/MTConnect.NET/blob/master/tests/MTConnect.NET-Common-Tests/DryGenerator/PerVersionFolderProhibitionTests.cs); parity between pre- and post-migration assertions is pinned by [`DryGenerator/AssertionParityTests.cs`](https://github.com/TrakHound/MTConnect.NET/blob/master/tests/MTConnect.NET-Common-Tests/DryGenerator/AssertionParityTests.cs).
+
+## The rule
+
+A test fixture's name and folder must reflect the **topic** under test, never the spec version that introduced it.
+
+- Correct: `tests/MTConnect.NET-Common-Tests/Devices/DataItems/DataItemTypeTests.cs`, `Devices/Components/ComponentTests.cs`, `Enums/EnumArmTests.cs`.
+- Prohibited: `tests/MTConnect.NET-Common-Tests/V2_6_V2_7/*.cs`, `V2_8/DataItemTypeTests.cs`, `V2_8ComponentAndEnumTests.cs`. The prohibition guard flags any directory matching `V/` or any fixture class matching `V*Tests`.
+
+Version becomes a **parameter**, not a **container**. A single fixture file houses every version's assertions for that topic; the fixture iterates over `MTConnectVersionMatrix.All` and gates each assertion with `Assume.That`.
+
+## How to add a fixture for a new spec version
+
+1. Ensure the version constant exists on [`MTConnect.MTConnectVersions`](https://github.com/TrakHound/MTConnect.NET/blob/master/libraries/MTConnect.NET-Common/MTConnectVersions.cs) (for example `public static readonly Version Version28 = new(2, 8);`). The matrix (`MTConnectVersionMatrix.All`) discovers it via reflection — no per-test edit is required.
+2. Find the topic file the new element belongs to (or create a new one under `Devices/`, `Observations/`, `Enums/`, or `Assets/`). Never create a `V2_8/` folder.
+3. Add a method with the matrix source and the version gate:
+
+ ```csharp
+ /// Pins the behaviour expressed by the test name: my new spec type constructs with correct metadata.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void MyNewSpecType_constructs_with_correct_metadata(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version28),
+ "MyNewSpecType was introduced in MTConnect v2.8.");
+
+ var d = new MyNewSpecTypeDataItem();
+ Assert.That(d.Type, Is.EqualTo("MY_NEW_SPEC_TYPE"));
+ // …
+ }
+ ```
+
+ Rows below the floor surface as `Inconclusive` in the test explorer (they neither pass nor fail); rows at or above the floor exercise the assertion.
+4. Update the corresponding `docs/testing/v-.md` compliance matrix to point at the new method.
+5. Do **not** name the method with a version prefix / suffix (`V2_8_*`, `*_in_v2_8`). Version is encoded in the matrix parameter, not the method name.
+
+## When to keep a plain `[Test]` (no matrix)
+
+Assertions that pin **constant-value invariants** — for example `MTConnectVersions.Version27 == new Version(2, 7)` — are not per-version behaviour. Keep them as plain `[Test]` (see `tests/MTConnect.NET-Common-Tests/MTConnectVersionsTests.cs`). The prohibition guard does not flag topic-file `[Test]` methods; only fixture-class name and folder shape matter.
+
+## Historical anchors
+
+`PerVersionFolderProhibitionTests.HistoricalAnchors` is an allowlist for deliberately-pinned fixtures (for example a `CppAgentParityWorkflowTests` pinned to a specific spec version for spec-fidelity reasons). Each entry must include a rationale comment. At HEAD the list is empty — introducing a legitimate pin requires an edit visible in the PR diff, which reviewers must approve on the rationale.
+
+## Migration-parity guard (`AssertionParityTests`)
+
+`AssertionParityTests.MigrationMap` records the 34-entry baseline captured on 2026-08-19 (pre-migration methods under `V2_6_V2_7/`) and asserts every entry has a post-migration home. It is a permanent regression tripwire: accidental deletion of any of those 34 method names in the topic files fires the parity test immediately.
+
+The `Every_baseline_assertion_has_a_post_migration_home` reflection sweep is cheap (≈ 5-30 ms on a warm CLR) and runs in the default `dotnet test` shape.
+
+## References
+
+- Migration PR: [TrakHound/MTConnect.NET#233](https://github.com/TrakHound/MTConnect.NET/pull/233).
+- Prohibition guard: [`tests/MTConnect.NET-Common-Tests/DryGenerator/PerVersionFolderProhibitionTests.cs`](https://github.com/TrakHound/MTConnect.NET/blob/master/tests/MTConnect.NET-Common-Tests/DryGenerator/PerVersionFolderProhibitionTests.cs).
+- Parity guard: [`tests/MTConnect.NET-Common-Tests/DryGenerator/AssertionParityTests.cs`](https://github.com/TrakHound/MTConnect.NET/blob/master/tests/MTConnect.NET-Common-Tests/DryGenerator/AssertionParityTests.cs).
+- Matrix source: [`tests/MTConnect.NET-Common-Tests/TestHelpers/MTConnectVersionMatrix.cs`](https://github.com/TrakHound/MTConnect.NET/blob/master/tests/MTConnect.NET-Common-Tests/TestHelpers/MTConnectVersionMatrix.cs).
+- Compliance-matrix pages: [`v2-6.md`](./v2-6.md), [`v2-7.md`](./v2-7.md).
diff --git a/libraries/MTConnect.NET-Common/Assets/Asset.g.cs b/libraries/MTConnect.NET-Common/Assets/Asset.g.cs
index 41d96e041..4843e9861 100644
--- a/libraries/MTConnect.NET-Common/Assets/Asset.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/Asset.g.cs
@@ -20,51 +20,61 @@ public partial class Asset : IAsset
/// Unique identifier for an Asset.
///
public string AssetId { get; set; }
+
///
/// Technical information about an entity describing its physical layout, functional characteristics, and relationships with other entities.
///
public MTConnect.Devices.Configurations.IConfiguration Configuration { get; set; }
+
///
/// Textual description for Asset.
///
public string Description { get; set; }
+
///
/// Associated piece of equipment's UUID that supplied the Asset's data.uuid defined in Device Information Model.
///
public string DeviceUuid { get; set; }
+
///
/// Condensed message digest from a secure one-way hash function. FIPS PUB 180-4
///
public string Hash { get; set; }
+
///
///
///
public System.Collections.Generic.IEnumerable Manufacturers { get; set; }
+
///
///
///
public string Model { get; set; }
+
///
/// Indicator that the Asset has been removed from the piece of equipment.
///
public bool Removed { get; set; }
+
///
///
///
public string SerialNumber { get; set; }
+
///
///
///
public string Station { get; set; }
+
///
/// Time the Asset data was last modified.
diff --git a/libraries/MTConnect.NET-Common/Assets/ComponentConfigurationParameters/Parameter.g.cs b/libraries/MTConnect.NET-Common/Assets/ComponentConfigurationParameters/Parameter.g.cs
index a628a796f..1217b7fa6 100644
--- a/libraries/MTConnect.NET-Common/Assets/ComponentConfigurationParameters/Parameter.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/ComponentConfigurationParameters/Parameter.g.cs
@@ -20,31 +20,37 @@ public class Parameter : IParameter
/// Internal identifier, register, or address.
///
public string Identifier { get; set; }
+
///
/// Maximum allowed value.
///
public double? Maximum { get; set; }
+
///
/// Minimal allowed value.
///
public double? Minimum { get; set; }
+
///
/// Descriptive name.
///
public string Name { get; set; }
+
///
/// Nominal value.
///
public double? Nominal { get; set; }
+
///
/// Engineering units.units **SHOULD** be SI or MTConnect Units.
///
public string Units { get; set; }
+
///
/// Configured value.
diff --git a/libraries/MTConnect.NET-Common/Assets/ComponentConfigurationParameters/ParameterSet.g.cs b/libraries/MTConnect.NET-Common/Assets/ComponentConfigurationParameters/ParameterSet.g.cs
index f5f988461..4f4a94b68 100644
--- a/libraries/MTConnect.NET-Common/Assets/ComponentConfigurationParameters/ParameterSet.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/ComponentConfigurationParameters/ParameterSet.g.cs
@@ -20,6 +20,7 @@ public class ParameterSet : IParameterSet
/// Name of the parameter set if more than one exists.
///
public string Name { get; set; }
+
///
/// Property that determines the characteristic or behavior of an entity.
diff --git a/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingItem.g.cs b/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingItem.g.cs
index 575d797f4..eaa9abed9 100644
--- a/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingItem.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingItem.g.cs
@@ -20,46 +20,55 @@ public partial class CuttingItem : ICuttingItem
/// Status of the cutting tool.
///
public System.Collections.Generic.IEnumerable CutterStatus { get; set; }
+
///
/// Free-form description of the cutting item.
///
public string Description { get; set; }
+
///
/// Material composition for this cutting item.
///
public string Grade { get; set; }
+
///
/// Number or numbers representing the individual cutting item or items on the tool.Indices **SHOULD** start numbering with the inserts or CuttingItem furthest from the gauge line and increasing in value as the items get closer to the gauge line. Items at the same distance **MAY** be arbitrarily numbered.> Note: In XML, the representation **MUST** be a single number ('1') or a comma separated set of individual elements ('1,2,3,4'), or as a inclusive range of values as in ('1-10') or any combination of ranges and numbers as in '1-4,6-10,22'. There **MUST NOT** be spaces or non-integer values in the text representation.
///
public string Indices { get; set; }
+
///
/// Manufacturer identifier of this cutting item.
///
public string ItemId { get; set; }
+
///
/// The tool life measured in tool wear.
///
public System.Collections.Generic.IEnumerable ItemLife { get; set; }
+
///
/// Free form description of the location on the cutting tool.Locus **MAY** be any free form string, but **SHOULD** adhere to the following rules:* The location numbering **SHOULD** start at the furthest CuttingItem and work it’s way back to the CuttingItem closest to the gauge line.* Flutes **SHOULD** be identified as such using the word `FLUTE`:. For example: `FLUTE`: 1, `INSERT`: 2 - would indicate the first flute and the second furthest insert from the end of the tool on that flute.* Other designations such as `CARTRIDGE` **MAY** be included, but should be identified using upper case and followed by a colon (:).
///
public string Locus { get; set; }
+
///
/// Manufacturers of the cutting item.This will reference the tool item and adaptive items specifically. The cutting itemsmanufacturers’ will be a property of CuttingItem.> Note: In XML, the representation **MUST** be a comma(,) delimited list of manufacturer names. See CuttingItem Schema Diagrams.
///
public System.Collections.Generic.IEnumerable Manufacturers { get; set; }
+
///
/// A collection of measurements relating to this cutting item.
///
public System.Collections.Generic.IEnumerable Measurements { get; set; }
+
///
/// Tool group this item is assigned in the part program.
diff --git a/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingToolArchetypeAsset.g.cs b/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingToolArchetypeAsset.g.cs
index b4b3d6c32..cc7f7ee2f 100644
--- a/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingToolArchetypeAsset.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingToolArchetypeAsset.g.cs
@@ -20,16 +20,19 @@ public partial class CuttingToolArchetypeAsset : Asset, ICuttingToolArchetypeAss
/// Detailed structure of the cutting tool which is static during its lifecycle. ISO 13399.
///
public MTConnect.Assets.CuttingTools.ICuttingToolDefinition CuttingToolDefinition { get; set; }
+
///
/// Data regarding the application or use of the tool.This data is provided by various pieces of equipment (i.e. machine tool, presetter) and statistical process control applications. Life cycle data will not remain static, but will change periodically when a tool is used or measured.
///
public MTConnect.Assets.CuttingTools.ICuttingToolLifeCycle CuttingToolLifeCycle { get; set; }
+
///
/// Unique identifier for this assembly.
///
public new string SerialNumber { get; set; }
+
///
/// Identifier for a class of cutting tools.
diff --git a/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingToolArchetypeReference.g.cs b/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingToolArchetypeReference.g.cs
index 2be636e5a..9d20ff084 100644
--- a/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingToolArchetypeReference.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingToolArchetypeReference.g.cs
@@ -20,6 +20,7 @@ public class CuttingToolArchetypeReference : ICuttingToolArchetypeReference
/// URL of the CuttingToolArchetype information model.
///
public string Source { get; set; }
+
///
/// `assetId` of the related CuttingToolArchetype.
diff --git a/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingToolAsset.g.cs b/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingToolAsset.g.cs
index 1fa37a4ac..78fd6e8ec 100644
--- a/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingToolAsset.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingToolAsset.g.cs
@@ -20,21 +20,25 @@ public partial class CuttingToolAsset : Asset, ICuttingToolAsset
/// AssetId and/or the URL of the data source of CuttingToolArchetype.
///
public MTConnect.Assets.CuttingTools.ICuttingToolArchetypeReference CuttingToolArchetypeReference { get; set; }
+
///
/// Detailed structure of the cutting tool which is static during its lifecycle. ISO 13399.
///
public MTConnect.Assets.CuttingTools.ICuttingToolDefinition CuttingToolDefinition { get; set; }
+
///
/// Data regarding the application or use of the tool.This data is provided by various pieces of equipment (i.e. machine tool, presetter) and statistical process control applications. Life cycle data will not remain static, but will change periodically when a tool is used or measured.
///
public MTConnect.Assets.CuttingTools.ICuttingToolLifeCycle CuttingToolLifeCycle { get; set; }
+
///
/// Unique identifier for this assembly.
///
public new string SerialNumber { get; set; }
+
///
/// Identifier for a class of cutting tools.
diff --git a/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingToolDefinition.g.cs b/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingToolDefinition.g.cs
index 7c67aec2b..ceebbdd99 100644
--- a/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingToolDefinition.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingToolDefinition.g.cs
@@ -20,6 +20,7 @@ public class CuttingToolDefinition : ICuttingToolDefinition
/// Identifies the expected representation of the enclosed data.
///
public MTConnect.Assets.CuttingTools.FormatType Format { get; set; }
+
///
/// Format.
diff --git a/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingToolLifeCycle.g.cs b/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingToolLifeCycle.g.cs
index 6610a3e69..e4db3287a 100644
--- a/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingToolLifeCycle.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/CuttingTools/CuttingToolLifeCycle.g.cs
@@ -20,51 +20,61 @@ public partial class CuttingToolLifeCycle : ICuttingToolLifeCycle
/// Identifier for the capability to connect any component of the cutting tool together, except Assembly Items, on the machine side. Code: `CCMS`
///
public string ConnectionCodeMachineSide { get; set; }
+
///
/// Status of the cutting tool.
///
public System.Collections.Generic.IEnumerable CutterStatus { get; set; }
+
///
/// Part of of the tool that physically removes the material from the workpiece by shear deformation.
///
public System.Collections.Generic.IEnumerable CuttingItems { get; set; }
+
///
/// Location of the pot or spindle the cutting tool currently resides in.positiveOverlap is provided, the tool reserves additional locations on either side, otherwise if they are not given, no additional locations are required for this tool.positiveOverlap of 1, the first pot **MAY** be occupied as well.
///
public MTConnect.Assets.CuttingTools.ILocation Location { get; set; }
+
///
/// Constrained scalar value associated with a cutting tool.
///
public System.Collections.Generic.IEnumerable Measurements { get; set; }
+
///
/// Constrained process feed rate for the tool in mm/s.minimum **MUST** be specified.
///
public MTConnect.Assets.CuttingTools.IProcessFeedRate ProcessFeedRate { get; set; }
+
///
/// Constrained process spindle speed for the tool in revolutions/minute.minimum **MUST** be specified.
///
public MTConnect.Assets.CuttingTools.IProcessSpindleSpeed ProcessSpindleSpeed { get; set; }
+
///
/// Tool group this tool is assigned in the part program.
///
public string ProgramToolGroup { get; set; }
+
///
/// Number of the tool as referenced in the part program.
///
public string ProgramToolNumber { get; set; }
+
///
/// Number of times the cutter has been reconditioned.
///
public MTConnect.Assets.CuttingTools.IReconditionCount ReconditionCount { get; set; }
+
///
/// Cutting tool life as related to the assembly.
diff --git a/libraries/MTConnect.NET-Common/Assets/CuttingTools/ItemLife.g.cs b/libraries/MTConnect.NET-Common/Assets/CuttingTools/ItemLife.g.cs
index 2dc1f81f6..dc1c37297 100644
--- a/libraries/MTConnect.NET-Common/Assets/CuttingTools/ItemLife.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/CuttingTools/ItemLife.g.cs
@@ -20,26 +20,31 @@ public class ItemLife : IItemLife
/// Indicates if the item life counts from zero to maximum or maximum to zero.
///
public MTConnect.Assets.CuttingTools.CountDirectionType CountDirection { get; set; }
+
///
/// Initial life of the item when it is new.
///
public double? Initial { get; set; }
+
///
/// End of life limit for this item.
///
public double? Limit { get; set; }
+
///
/// Type of item life being accumulated.
///
public MTConnect.Assets.CuttingTools.ToolLifeType Type { get; set; }
+
///
/// Value of ItemLife.
///
public double Value { get; set; }
+
///
/// Point at which a item life warning will be raised.
diff --git a/libraries/MTConnect.NET-Common/Assets/CuttingTools/Location.g.cs b/libraries/MTConnect.NET-Common/Assets/CuttingTools/Location.g.cs
index 97f1ba637..164283482 100644
--- a/libraries/MTConnect.NET-Common/Assets/CuttingTools/Location.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/CuttingTools/Location.g.cs
@@ -20,41 +20,49 @@ public class Location : ILocation
/// Automatic tool changer associated with a tool.
///
public string AutomaticToolChanger { get; set; }
+
///
/// Number of locations at lower index values from this location.
///
public int? NegativeOverlap { get; set; }
+
///
/// Number of locations at higher index value from this location.
///
public int? PositiveOverlap { get; set; }
+
///
/// Tool bar associated with a tool.
///
public string ToolBar { get; set; }
+
///
/// Tool magazine associated with a tool.
///
public string ToolMagazine { get; set; }
+
///
/// Tool rack associated with a tool.
///
public string ToolRack { get; set; }
+
///
/// Turret associated with a tool.
///
public string Turret { get; set; }
+
///
/// Type of location being identified. value**MUST** be a numeric value.
///
public MTConnect.Assets.CuttingTools.LocationType Type { get; set; }
+
///
///
diff --git a/libraries/MTConnect.NET-Common/Assets/CuttingTools/Measurement.g.cs b/libraries/MTConnect.NET-Common/Assets/CuttingTools/Measurement.g.cs
index de4754402..e6afa5399 100644
--- a/libraries/MTConnect.NET-Common/Assets/CuttingTools/Measurement.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/CuttingTools/Measurement.g.cs
@@ -1,56 +1,57 @@
// Copyright (c) 2024 TrakHound Inc., All Rights Reserved.
// TrakHound Inc. licenses this file to you under the MIT license.
-// MTConnect SysML v2.3 : UML ID = EAID_C09F377D_8946_421b_B746_E23C01D97EAC
+// MTConnect SysML v2.3 : UML ID = _2024x_68e0225_1727793846441_986747_23754
namespace MTConnect.Assets.CuttingTools
{
///
- /// Constrained scalar value associated with a cutting tool.
+ /// Constrained scalar value associated with an Asset
///
public partial class Measurement : IMeasurement
{
///
/// The description of this type as defined by the MTConnect Standard.
///
- public const string DescriptionText = "Constrained scalar value associated with a cutting tool.";
+ public const string DescriptionText = "Constrained scalar value associated with an Asset";
- ///
- /// Shop specific code for the measurement. ISO 13399 codes **MAY** be used for these codes as well. code values.
- ///
- public string Code { get; set; }
-
///
/// Maximum value for the measurement.
///
public double? Maximum { get; set; }
+
///
/// Minimum value for the measurement.
///
public double? Minimum { get; set; }
+
///
/// NativeUnits.
///
public string NativeUnits { get; set; }
+
///
/// As advertised value for the measurement.
///
public double? Nominal { get; set; }
+
///
/// Number of significant digits in the reported value.
///
public int? SignificantDigits { get; set; }
+
///
/// Units.
///
public string Units { get; set; }
+
///
///
///
diff --git a/libraries/MTConnect.NET-Common/Assets/CuttingTools/ProcessFeedRate.g.cs b/libraries/MTConnect.NET-Common/Assets/CuttingTools/ProcessFeedRate.g.cs
index 6ed1b3c8e..e7159796c 100644
--- a/libraries/MTConnect.NET-Common/Assets/CuttingTools/ProcessFeedRate.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/CuttingTools/ProcessFeedRate.g.cs
@@ -20,16 +20,19 @@ public class ProcessFeedRate : IProcessFeedRate
/// Upper bound for the tool’s process target feedrate.
///
public double? Maximum { get; set; }
+
///
/// Lower bound for the tool's feedrate.
///
public double? Minimum { get; set; }
+
///
/// Nominal feedrate the tool is designed to operate at.
///
public double? Nominal { get; set; }
+
///
///
diff --git a/libraries/MTConnect.NET-Common/Assets/CuttingTools/ProcessSpindleSpeed.g.cs b/libraries/MTConnect.NET-Common/Assets/CuttingTools/ProcessSpindleSpeed.g.cs
index 2ad2bc015..cefb5be97 100644
--- a/libraries/MTConnect.NET-Common/Assets/CuttingTools/ProcessSpindleSpeed.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/CuttingTools/ProcessSpindleSpeed.g.cs
@@ -20,16 +20,19 @@ public class ProcessSpindleSpeed : IProcessSpindleSpeed
/// Upper bound for the tool’s target spindle speed.
///
public double? Maximum { get; set; }
+
///
/// Lower bound for the tools spindle speed.
///
public double? Minimum { get; set; }
+
///
/// Nominal speed the tool is designed to operate at.
///
public double? Nominal { get; set; }
+
///
///
diff --git a/libraries/MTConnect.NET-Common/Assets/CuttingTools/ReconditionCount.g.cs b/libraries/MTConnect.NET-Common/Assets/CuttingTools/ReconditionCount.g.cs
index e0e56122b..a565cecfd 100644
--- a/libraries/MTConnect.NET-Common/Assets/CuttingTools/ReconditionCount.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/CuttingTools/ReconditionCount.g.cs
@@ -20,6 +20,7 @@ public class ReconditionCount : IReconditionCount
/// Maximum number of times the tool may be reconditioned.
///
public int? MaximumCount { get; set; }
+
///
/// CuttingToolLifeCycle.
diff --git a/libraries/MTConnect.NET-Common/Assets/CuttingTools/ToolLife.g.cs b/libraries/MTConnect.NET-Common/Assets/CuttingTools/ToolLife.g.cs
index 4cc0b9479..c78fb2ed0 100644
--- a/libraries/MTConnect.NET-Common/Assets/CuttingTools/ToolLife.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/CuttingTools/ToolLife.g.cs
@@ -20,26 +20,31 @@ public partial class ToolLife : IToolLife
/// Indicates if the tool life counts from zero to maximum or maximum to zero.
///
public MTConnect.Assets.CuttingTools.CountDirectionType CountDirection { get; set; }
+
///
/// Initial life of the tool when it is new.
///
public double? Initial { get; set; }
+
///
/// End of life limit for the tool.
///
public double? Limit { get; set; }
+
///
/// Type of tool life being accumulated.
///
public MTConnect.Assets.CuttingTools.ToolLifeType Type { get; set; }
+
///
/// Value of ToolLife.
///
public double Value { get; set; }
+
///
/// Point at which a tool life warning will be raised.
diff --git a/libraries/MTConnect.NET-Common/Assets/CuttingTools/ToolingMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/CuttingTools/ToolingMeasurement.g.cs
index 385e7e41e..e0292917b 100644
--- a/libraries/MTConnect.NET-Common/Assets/CuttingTools/ToolingMeasurement.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/CuttingTools/ToolingMeasurement.g.cs
@@ -19,6 +19,6 @@ public partial class ToolingMeasurement : Measurement, IToolingMeasurement
///
/// Shop specific code for the measurement. ISO 13399 codes **MAY** be used for these codes as well. code values.
///
- public new string Code { get; set; }
+ public string Code { get; set; }
}
}
\ No newline at end of file
diff --git a/libraries/MTConnect.NET-Common/Assets/Files/AbstractFileAsset.g.cs b/libraries/MTConnect.NET-Common/Assets/Files/AbstractFileAsset.g.cs
index 68daf0fa5..e09b9b032 100644
--- a/libraries/MTConnect.NET-Common/Assets/Files/AbstractFileAsset.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/Files/AbstractFileAsset.g.cs
@@ -20,26 +20,31 @@ public abstract partial class AbstractFileAsset : Asset, IAbstractFileAsset
/// Category of application that will use this file.
///
public MTConnect.Assets.Files.ApplicationCategory ApplicationCategory { get; set; }
+
///
/// Type of application that will use this file.
///
public MTConnect.Assets.Files.ApplicationType ApplicationType { get; set; }
+
///
/// Remark or interpretation for human interpretation associated with a File or FileArchetype.
///
public System.Collections.Generic.IEnumerable FileComments { get; set; }
+
///
/// Key-value pair providing additional metadata about a File.
///
public System.Collections.Generic.IEnumerable FileProperties { get; set; }
+
///
/// Mime type of the file.
///
public string MediaType { get; set; }
+
///
/// Name of the file.
diff --git a/libraries/MTConnect.NET-Common/Assets/Files/FileAsset.g.cs b/libraries/MTConnect.NET-Common/Assets/Files/FileAsset.g.cs
index c738c5d11..b4f05955c 100644
--- a/libraries/MTConnect.NET-Common/Assets/Files/FileAsset.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/Files/FileAsset.g.cs
@@ -20,41 +20,49 @@ public partial class FileAsset : AbstractFileAsset, IFileAsset
/// Time the file was created.
///
public System.DateTime CreationTime { get; set; }
+
///
/// Reference to the target Device for this File.
///
public System.Collections.Generic.IEnumerable Destinations { get; set; }
+
///
/// URL reference to the file location.
///
public MTConnect.Assets.Files.IFileLocation Location { get; set; }
+
///
/// Time the file was modified.
///
public System.DateTime? ModificationTime { get; set; }
+
///
/// Public key used to verify the signature.
///
public string PublicKey { get; set; }
+
///
/// Secure hash of the file.
///
public string Signature { get; set; }
+
///
/// Size of the file in bytes.
///
public int Size { get; set; }
+
///
/// State of the file.
///
public MTConnect.Assets.Files.FileState State { get; set; }
+
///
/// Version identifier of the file.
diff --git a/libraries/MTConnect.NET-Common/Assets/Files/FileComment.g.cs b/libraries/MTConnect.NET-Common/Assets/Files/FileComment.g.cs
index a5e91f1cf..5588683ad 100644
--- a/libraries/MTConnect.NET-Common/Assets/Files/FileComment.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/Files/FileComment.g.cs
@@ -20,6 +20,7 @@ public class FileComment : IFileComment
/// Time the comment was made.
///
public System.DateTime Timestamp { get; set; }
+
///
/// Text of the comment about the file.
diff --git a/libraries/MTConnect.NET-Common/Assets/Files/FileLocation.g.cs b/libraries/MTConnect.NET-Common/Assets/Files/FileLocation.g.cs
index 02f6cfcfa..efb406293 100644
--- a/libraries/MTConnect.NET-Common/Assets/Files/FileLocation.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/Files/FileLocation.g.cs
@@ -20,6 +20,7 @@ public class FileLocation : IFileLocation
/// URL reference to the file.`href` is of type `xlink:href` from the W3C XLink specification.
///
public string Href { get; set; }
+
///
/// Type of href for the xlink href type. **MUST** be `locator` referring to a URL.
diff --git a/libraries/MTConnect.NET-Common/Assets/Files/FileProperty.g.cs b/libraries/MTConnect.NET-Common/Assets/Files/FileProperty.g.cs
index afd944148..fcb6a0f89 100644
--- a/libraries/MTConnect.NET-Common/Assets/Files/FileProperty.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/Files/FileProperty.g.cs
@@ -20,6 +20,7 @@ public class FileProperty : IFileProperty
/// Name of the FileProperty.
///
public string Name { get; set; }
+
///
/// The value of the FileProperty.
diff --git a/libraries/MTConnect.NET-Common/Assets/Files/IAbstractFile.g.cs b/libraries/MTConnect.NET-Common/Assets/Files/IAbstractFile.g.cs
deleted file mode 100644
index 3198b4690..000000000
--- a/libraries/MTConnect.NET-Common/Assets/Files/IAbstractFile.g.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (c) 2023 TrakHound Inc., All Rights Reserved.
-// TrakHound Inc. licenses this file to you under the MIT license.
-
-namespace MTConnect.Assets.Files
-{
- ///
- /// Abstract Asset that contains the common properties of the File and FileArchetype types.
- ///
- public interface IAbstractFile : IAsset
- {
- ///
- /// Category of application that will use this file.
- ///
- MTConnect.Assets.Files.ApplicationCategory ApplicationCategory { get; }
-
- ///
- /// Type of application that will use this file.
- ///
- MTConnect.Assets.Files.ApplicationType ApplicationType { get; }
-
- ///
- /// Remark or interpretation for human interpretation associated with a File or FileArchetype.
- ///
- System.Collections.Generic.IEnumerable FileComments { get; }
-
- ///
- /// Key-value pair providing additional metadata about a File.
- ///
- System.Collections.Generic.IEnumerable FileProperties { get; }
-
- ///
- /// Mime type of the file.
- ///
- string MediaType { get; }
-
- ///
- /// Name of the file.
- ///
- string Name { get; }
- }
-}
\ No newline at end of file
diff --git a/libraries/MTConnect.NET-Common/Assets/Files/IFileArchetype.g.cs b/libraries/MTConnect.NET-Common/Assets/Files/IFileArchetype.g.cs
deleted file mode 100644
index ed35e2e25..000000000
--- a/libraries/MTConnect.NET-Common/Assets/Files/IFileArchetype.g.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (c) 2023 TrakHound Inc., All Rights Reserved.
-// TrakHound Inc. licenses this file to you under the MIT license.
-
-namespace MTConnect.Assets.Files
-{
- ///
- /// AbstractFile type that provides information common to all versions of a file.
- ///
- public interface IFileArchetype : IAbstractFile
- {
- }
-}
\ No newline at end of file
diff --git a/libraries/MTConnect.NET-Common/Assets/Fixture/FixtureAsset.g.cs b/libraries/MTConnect.NET-Common/Assets/Fixture/FixtureAsset.g.cs
index e6895c1e5..a0fffd5a3 100644
--- a/libraries/MTConnect.NET-Common/Assets/Fixture/FixtureAsset.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/Fixture/FixtureAsset.g.cs
@@ -20,16 +20,19 @@ public partial class FixtureAsset : PhysicalAsset, IFixtureAsset
/// Actuation type of the Fixture's clamping mechanism.
///
public string ClampingMethod { get; set; }
+
///
/// Identifier of the Pallet.
///
public string FixtureId { get; set; }
+
///
/// Number or sequence assigned to the Fixture in a group of Fixtures.
///
public int FixtureNumber { get; set; }
+
///
/// Actuation type of the Fixture's mounting mechanism.
diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/IHeightMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/IHeightMeasurement.g.cs
index d6856276d..f89def2f4 100644
--- a/libraries/MTConnect.NET-Common/Assets/Pallet/IHeightMeasurement.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/Pallet/IHeightMeasurement.g.cs
@@ -1,4 +1,4 @@
-// Copyright (c) 2024 TrakHound Inc., All Rights Reserved.
+// Copyright (c) 2025 TrakHound Inc., All Rights Reserved.
// TrakHound Inc. licenses this file to you under the MIT license.
namespace MTConnect.Assets.Pallet
diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/ILengthMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/ILengthMeasurement.g.cs
index 7323449c7..c07133c1f 100644
--- a/libraries/MTConnect.NET-Common/Assets/Pallet/ILengthMeasurement.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/Pallet/ILengthMeasurement.g.cs
@@ -1,4 +1,4 @@
-// Copyright (c) 2024 TrakHound Inc., All Rights Reserved.
+// Copyright (c) 2025 TrakHound Inc., All Rights Reserved.
// TrakHound Inc. licenses this file to you under the MIT license.
namespace MTConnect.Assets.Pallet
diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/ILoadedHeightMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/ILoadedHeightMeasurement.g.cs
index b14910f4f..2319758ea 100644
--- a/libraries/MTConnect.NET-Common/Assets/Pallet/ILoadedHeightMeasurement.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/Pallet/ILoadedHeightMeasurement.g.cs
@@ -1,4 +1,4 @@
-// Copyright (c) 2024 TrakHound Inc., All Rights Reserved.
+// Copyright (c) 2025 TrakHound Inc., All Rights Reserved.
// TrakHound Inc. licenses this file to you under the MIT license.
namespace MTConnect.Assets.Pallet
diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/ILoadedLengthMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/ILoadedLengthMeasurement.g.cs
index 716016ba0..047707089 100644
--- a/libraries/MTConnect.NET-Common/Assets/Pallet/ILoadedLengthMeasurement.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/Pallet/ILoadedLengthMeasurement.g.cs
@@ -1,4 +1,4 @@
-// Copyright (c) 2024 TrakHound Inc., All Rights Reserved.
+// Copyright (c) 2025 TrakHound Inc., All Rights Reserved.
// TrakHound Inc. licenses this file to you under the MIT license.
namespace MTConnect.Assets.Pallet
diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/ILoadedSwingMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/ILoadedSwingMeasurement.g.cs
index 05f4d3ca9..3024f1dba 100644
--- a/libraries/MTConnect.NET-Common/Assets/Pallet/ILoadedSwingMeasurement.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/Pallet/ILoadedSwingMeasurement.g.cs
@@ -1,4 +1,4 @@
-// Copyright (c) 2024 TrakHound Inc., All Rights Reserved.
+// Copyright (c) 2025 TrakHound Inc., All Rights Reserved.
// TrakHound Inc. licenses this file to you under the MIT license.
namespace MTConnect.Assets.Pallet
diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/ILoadedWeightMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/ILoadedWeightMeasurement.g.cs
index 292fe40d9..8b7811564 100644
--- a/libraries/MTConnect.NET-Common/Assets/Pallet/ILoadedWeightMeasurement.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/Pallet/ILoadedWeightMeasurement.g.cs
@@ -1,4 +1,4 @@
-// Copyright (c) 2024 TrakHound Inc., All Rights Reserved.
+// Copyright (c) 2025 TrakHound Inc., All Rights Reserved.
// TrakHound Inc. licenses this file to you under the MIT license.
namespace MTConnect.Assets.Pallet
diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/ILoadedWidthMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/ILoadedWidthMeasurement.g.cs
index b3ffe8213..699b9761c 100644
--- a/libraries/MTConnect.NET-Common/Assets/Pallet/ILoadedWidthMeasurement.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/Pallet/ILoadedWidthMeasurement.g.cs
@@ -1,4 +1,4 @@
-// Copyright (c) 2024 TrakHound Inc., All Rights Reserved.
+// Copyright (c) 2025 TrakHound Inc., All Rights Reserved.
// TrakHound Inc. licenses this file to you under the MIT license.
namespace MTConnect.Assets.Pallet
diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/ISwingMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/ISwingMeasurement.g.cs
index 2e08f0941..7a89acfef 100644
--- a/libraries/MTConnect.NET-Common/Assets/Pallet/ISwingMeasurement.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/Pallet/ISwingMeasurement.g.cs
@@ -1,4 +1,4 @@
-// Copyright (c) 2024 TrakHound Inc., All Rights Reserved.
+// Copyright (c) 2025 TrakHound Inc., All Rights Reserved.
// TrakHound Inc. licenses this file to you under the MIT license.
namespace MTConnect.Assets.Pallet
diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/IWeightMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/IWeightMeasurement.g.cs
index 4c4d9cd07..795c83e4a 100644
--- a/libraries/MTConnect.NET-Common/Assets/Pallet/IWeightMeasurement.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/Pallet/IWeightMeasurement.g.cs
@@ -1,4 +1,4 @@
-// Copyright (c) 2024 TrakHound Inc., All Rights Reserved.
+// Copyright (c) 2025 TrakHound Inc., All Rights Reserved.
// TrakHound Inc. licenses this file to you under the MIT license.
namespace MTConnect.Assets.Pallet
diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/IWidthMeasurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/IWidthMeasurement.g.cs
index b24346c1f..fbdd15f4f 100644
--- a/libraries/MTConnect.NET-Common/Assets/Pallet/IWidthMeasurement.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/Pallet/IWidthMeasurement.g.cs
@@ -1,4 +1,4 @@
-// Copyright (c) 2024 TrakHound Inc., All Rights Reserved.
+// Copyright (c) 2025 TrakHound Inc., All Rights Reserved.
// TrakHound Inc. licenses this file to you under the MIT license.
namespace MTConnect.Assets.Pallet
diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/Measurement.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/Measurement.g.cs
index ebb607274..6cde906cc 100644
--- a/libraries/MTConnect.NET-Common/Assets/Pallet/Measurement.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/Pallet/Measurement.g.cs
@@ -20,31 +20,37 @@ public partial class Measurement : IMeasurement
/// Maximum value for the measurement.
///
public double? Maximum { get; set; }
+
///
/// Minimum value for the measurement.
///
public double? Minimum { get; set; }
+
///
/// NativeUnits.
///
public string NativeUnits { get; set; }
+
///
/// As advertised value for the measurement.
///
public double? Nominal { get; set; }
+
///
/// Number of significant digits in the reported value.
///
public int? SignificantDigits { get; set; }
+
///
/// Units.
///
public string Units { get; set; }
+
///
///
diff --git a/libraries/MTConnect.NET-Common/Assets/Pallet/PalletAsset.g.cs b/libraries/MTConnect.NET-Common/Assets/Pallet/PalletAsset.g.cs
index eec208381..cd392755f 100644
--- a/libraries/MTConnect.NET-Common/Assets/Pallet/PalletAsset.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/Pallet/PalletAsset.g.cs
@@ -20,21 +20,25 @@ public partial class PalletAsset : PhysicalAsset, IPalletAsset
/// Actuation type of the Pallet's clamping mechanism.
///
public string ClampingMethod { get; set; }
+
///
/// Actuation type of the Pallet's mounting mechanism.
///
public string MountingMethod { get; set; }
+
///
/// Identifier of the Pallet.
///
public string PalletId { get; set; }
+
///
/// Number or sequence assigned to the Pallet in a group of Pallets.
///
public int PalletNumber { get; set; }
+
///
/// Type of Pallet. Common types of pallet include: Process, Warehouse, Shipping, Fixture and Machine.
diff --git a/libraries/MTConnect.NET-Common/Assets/PhysicalAsset.g.cs b/libraries/MTConnect.NET-Common/Assets/PhysicalAsset.g.cs
index a348806ce..dfcfc61c3 100644
--- a/libraries/MTConnect.NET-Common/Assets/PhysicalAsset.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/PhysicalAsset.g.cs
@@ -20,21 +20,25 @@ public partial class PhysicalAsset : IPhysicalAsset
/// Date of calibration of the Asset.
///
public System.DateTime CalibrationDate { get; set; }
+
///
/// Date of last inspection of the Asset.
///
public System.DateTime InspectionDate { get; set; }
+
///
/// Date of creation or built of the Asset.
///
public System.DateTime ManufactureDate { get; set; }
+
///
/// Constrained scalar value associated with an Asset
///
public MTConnect.Assets.CuttingTools.IMeasurement Measurement { get; set; }
+
///
/// Date of next inspection of the Asset.
diff --git a/libraries/MTConnect.NET-Common/Assets/QIF/QIFDocumentWrapperAsset.g.cs b/libraries/MTConnect.NET-Common/Assets/QIF/QIFDocumentWrapperAsset.g.cs
index a3e53bcb9..0c3e9f63f 100644
--- a/libraries/MTConnect.NET-Common/Assets/QIF/QIFDocumentWrapperAsset.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/QIF/QIFDocumentWrapperAsset.g.cs
@@ -20,6 +20,7 @@ public partial class QIFDocumentWrapperAsset : Asset, IQIFDocumentWrapperAsset
/// QIF Document as given by the QIF standard.
///
public string QIFDocument { get; set; }
+
///
/// Contained QIF Document type as defined in the QIF Standard.
diff --git a/libraries/MTConnect.NET-Common/Assets/RawMaterials/Material.g.cs b/libraries/MTConnect.NET-Common/Assets/RawMaterials/Material.g.cs
index 44dc746cd..95233f234 100644
--- a/libraries/MTConnect.NET-Common/Assets/RawMaterials/Material.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/RawMaterials/Material.g.cs
@@ -20,36 +20,43 @@ public class Material : IMaterial
/// Unique identifier for the material.
///
public string Id { get; set; }
+
///
/// Manufacturer's lot code of the material.
///
public string Lot { get; set; }
+
///
/// Name of the material manufacturer.
///
public string Manufacturer { get; set; }
+
///
/// Lot code of the raw feed stock for the material, from the feed stock manufacturer.
///
public string ManufacturingCode { get; set; }
+
///
/// Manufacturing date of the material from the material manufacturer.
///
public System.DateTime? ManufacturingDate { get; set; }
+
///
/// ASTM standard code that the material complies with.
///
public string MaterialCode { get; set; }
+
///
/// Name of the material. Examples: `ULTM9085`, `ABS`, `4140`.
///
public string Name { get; set; }
+
///
/// Type of material. Examples: `Metal`, `Polymer`, `Wood`, `4140`, `Recycled`, `Prestine` and `Used`.
diff --git a/libraries/MTConnect.NET-Common/Assets/RawMaterials/RawMaterialAsset.g.cs b/libraries/MTConnect.NET-Common/Assets/RawMaterials/RawMaterialAsset.g.cs
index 16e0017ac..47604be3e 100644
--- a/libraries/MTConnect.NET-Common/Assets/RawMaterials/RawMaterialAsset.g.cs
+++ b/libraries/MTConnect.NET-Common/Assets/RawMaterials/RawMaterialAsset.g.cs
@@ -20,76 +20,91 @@ public partial class RawMaterialAsset : Asset, IRawMaterialAsset
/// Type of container holding the raw material. Examples: `Pallet`, `Canister`, `Cartridge`, `Tank`, `Bin`, `Roll`, and `Spool`.
///
public string ContainerType { get; set; }
+
///
/// Dimension of material currently in raw material.
///
public MTConnect.Millimeter3D CurrentDimension { get; set; }
+
///
/// Quantity of material currently in raw material.
///
public int? CurrentQuantity { get; set; }
+
///
/// Amount of material currently in raw material.
///
public double? CurrentVolume { get; set; }
+
///
/// Date raw material was first used.
///
public System.DateTime? FirstUseDate { get; set; }
+
///
/// Form of the raw material.
///
public MTConnect.Assets.RawMaterials.Form Form { get; set; }
+
///
/// Material has existing usable volume.
///
public bool? HasMaterial { get; set; }
+
///
/// Dimension of material initially placed in raw material when manufactured.
///
public MTConnect.Millimeter3D InitialDimension { get; set; }
+
///
/// Quantity of material initially placed in raw material when manufactured.
///
public int? InitialQuantity { get; set; }
+
///
/// Amount of material initially placed in raw material when manufactured.
///
public double? InitialVolume { get; set; }
+
///
/// Date raw material was last used.
///
public System.DateTime? LastUseDate { get; set; }
+
///
/// Date the raw material was created.
///
public System.DateTime? ManufacturingDate { get; set; }
+
///
/// Material used as the RawMaterial.
///
public MTConnect.Assets.RawMaterials.IMaterial Material { get; set; }
+
///
/// Name of the raw material.Examples: `Container1` and `AcrylicContainer`.
///
public string Name { get; set; }
+
///
/// ISO process type supported by this raw material. Examples include: `VAT_POLYMERIZATION`, `BINDER_JETTING`, `MATERIAL_EXTRUSION`, `MATERIAL_JETTING`, `SHEET_LAMINATION`, `POWDER_BED_FUSION` and `DIRECTED_ENERGY_DEPOSITION`.
///
public string ProcessKind { get; set; }
+
///
/// Serial number of the raw material.
diff --git a/libraries/MTConnect.NET-Common/Devices/AbstractDataItemRelationship.g.cs b/libraries/MTConnect.NET-Common/Devices/AbstractDataItemRelationship.g.cs
index d4be03ddb..66dc9a359 100644
--- a/libraries/MTConnect.NET-Common/Devices/AbstractDataItemRelationship.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/AbstractDataItemRelationship.g.cs
@@ -20,6 +20,7 @@ public abstract partial class AbstractDataItemRelationship : IAbstractDataItemRe
/// Reference to the related entity's `id`.
///
public string IdRef { get; set; }
+
///
/// Descriptive name associated with this AbstractDataItemRelationship.
diff --git a/libraries/MTConnect.NET-Common/Devices/CellDefinition.g.cs b/libraries/MTConnect.NET-Common/Devices/CellDefinition.g.cs
index 80b1fbc8b..77c61fa3c 100644
--- a/libraries/MTConnect.NET-Common/Devices/CellDefinition.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/CellDefinition.g.cs
@@ -20,26 +20,31 @@ public class CellDefinition : ICellDefinition
/// Textual description for CellDefinition.
///
public string Description { get; set; }
+
///
/// Unique identification of the Cell in the Definition. key.
///
public string Key { get; set; }
+
///
/// Key.
///
public string KeyType { get; set; }
+
///
/// SubType. See DataItem.
///
public string SubType { get; set; }
+
///
/// Type. See DataItem Types.
///
public string Type { get; set; }
+
///
/// Units. See Value Properties of DataItem.
diff --git a/libraries/MTConnect.NET-Common/Devices/Component.g.cs b/libraries/MTConnect.NET-Common/Devices/Component.g.cs
index 6e3d12a12..099e1d1b1 100644
--- a/libraries/MTConnect.NET-Common/Devices/Component.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Component.g.cs
@@ -20,56 +20,67 @@ public partial class Component : IComponent
/// Logical or physical entity that provides a capability.
///
public System.Collections.Generic.IEnumerable Components { get; set; }
+
///
/// Functional part of a piece of equipment contained within a Component.
///
public System.Collections.Generic.IEnumerable Compositions { get; set; }
+
///
/// Technical information about an entity describing its physical layout, functional characteristics, and relationships with other entities.
///
public MTConnect.Devices.Configurations.IConfiguration Configuration { get; set; }
+
///
/// Specifies the CoordinateSystem for this Component and its children.
///
public string CoordinateSystemIdRef { get; set; }
+
///
/// Descriptive content.
///
public MTConnect.Devices.IDescription Description { get; set; }
+
///
/// Unique identifier for the Component.
///
public string Id { get; set; }
+
///
/// Name of the Component.name **MUST** be unique for all child Component entities of a parent Component.
///
public string Name { get; set; }
+
///
/// Common name associated with Component.
///
public string NativeName { get; set; }
+
///
/// Pointer to information that is associated with another entity defined elsewhere in the MTConnectDevices entity for a piece of equipment.
///
public System.Collections.Generic.IEnumerable References { get; set; }
+
///
/// Interval in milliseconds between the completion of the reading of the data associated with the Component until the beginning of the next sampling of that data.This information may be used by client software applications to understand how often information from a Component is expected to be refreshed.The refresh rate for data from all child Component entities will be thesampleInterval provided for the child Component.
///
public double SampleInterval { get; set; }
+
///
/// SampleInterval.
///
public double SampleRate { get; set; }
+
///
/// Universally unique identifier for the Component.
diff --git a/libraries/MTConnect.NET-Common/Devices/Composition.g.cs b/libraries/MTConnect.NET-Common/Devices/Composition.g.cs
index 7a6af5ca4..f7aeaeabd 100644
--- a/libraries/MTConnect.NET-Common/Devices/Composition.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Composition.g.cs
@@ -20,61 +20,73 @@ public partial class Composition : IComposition
/// Technical information about an entity describing its physical layout, functional characteristics, and relationships with other entities.
///
public MTConnect.Devices.Configurations.IConfiguration Configuration { get; set; }
+
///
/// Descriptive content.
///
public MTConnect.Devices.IDescription Description { get; set; }
+
///
/// Unique identifier for the Composition element.
///
public string Id { get; set; }
+
///
/// Name of the Composition element.
///
public string Name { get; set; }
+
///
/// Type of Composition.
///
public string Type { get; set; }
+
///
/// Universally unique identifier for the Composition.
///
public string Uuid { get; set; }
+
///
/// Logical or physical entity that provides a capability.
///
public System.Collections.Generic.IEnumerable Components { get; set; }
+
///
/// Functional part of a piece of equipment contained within a Component.
///
public System.Collections.Generic.IEnumerable Compositions { get; set; }
+
///
/// Specifies the CoordinateSystem for this Component and its children.
///
public string CoordinateSystemIdRef { get; set; }
+
///
/// Common name associated with Component.
///
public string NativeName { get; set; }
+
///
/// Pointer to information that is associated with another entity defined elsewhere in the MTConnectDevices entity for a piece of equipment.
///
public System.Collections.Generic.IEnumerable References { get; set; }
+
///
/// Interval in milliseconds between the completion of the reading of the data associated with the Component until the beginning of the next sampling of that data.This information may be used by client software applications to understand how often information from a Component is expected to be refreshed.The refresh rate for data from all child Component entities will be thesampleInterval provided for the child Component.
///
public double SampleInterval { get; set; }
+
///
/// SampleInterval.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/AlarmLimits.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/AlarmLimits.g.cs
index a993c11ac..d07f1e118 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/AlarmLimits.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/AlarmLimits.g.cs
@@ -20,16 +20,19 @@ public class AlarmLimits : IAlarmLimits
/// Lower conformance boundary for a variable.> Note: immediate concern or action may be required.
///
public double? LowerLimit { get; set; }
+
///
/// Lower boundary indicating increased concern and supervision may be required.
///
public double? LowerWarning { get; set; }
+
///
/// Upper conformance boundary for a variable.> Note: immediate concern or action may be required.
///
public double? UpperLimit { get; set; }
+
///
/// Upper boundary indicating increased concern and supervision may be required.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/AssetRelationship.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/AssetRelationship.g.cs
index d72a7b347..bad1449d1 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/AssetRelationship.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/AssetRelationship.g.cs
@@ -20,11 +20,13 @@ public class AssetRelationship : ConfigurationRelationship, IAssetRelationship
/// Uuid of the related Asset.
///
public string AssetIdRef { get; set; }
+
///
/// Type of Asset being referenced.
///
public string AssetType { get; set; }
+
///
/// URI reference to the associated Asset.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/AxisDataSet.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/AxisDataSet.g.cs
index 6b51e9676..927781f0f 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/AxisDataSet.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/AxisDataSet.g.cs
@@ -20,11 +20,13 @@ public class AxisDataSet : AbstractAxis, IAxisDataSet, IDataSet
/// X-component of Axis.
///
public double X { get; set; }
+
///
/// Y-component of Axis.
///
public double Y { get; set; }
+
///
/// Z-component of Axis.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/Channel.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/Channel.g.cs
index b21aa10c9..8f0a3409a 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/Channel.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/Channel.g.cs
@@ -20,26 +20,31 @@ public class Channel : IChannel
/// Date upon which the sensor unit was last calibrated to the sensor element.
///
public System.DateTime? CalibrationDate { get; set; }
+
///
/// The initials of the person verifying the validity of the calibration data.
///
public string CalibrationInitials { get; set; }
+
///
/// Textual description for Channel.
///
public string Description { get; set; }
+
///
/// Name of the specific sensing element.
///
public string Name { get; set; }
+
///
/// Date upon which the sensor element is next scheduled to be calibrated with the sensor unit.
///
public System.DateTime? NextCalibrationDate { get; set; }
+
///
/// Unique identifier that will only refer to a specific sensing element.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/Configuration.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/Configuration.g.cs
index 1d8be3109..9296e1f9a 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/Configuration.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/Configuration.g.cs
@@ -20,36 +20,43 @@ public class Configuration : IConfiguration
/// Reference system that associates a unique set of n parameters with each point in an n-dimensional space. ISO 10303-218:2004
///
public System.Collections.Generic.IEnumerable CoordinateSystems { get; set; }
+
///
/// Reference to a file containing an image of the Component.
///
public System.Collections.Generic.IEnumerable ImageFiles { get; set; }
+
///
/// Movement of the component relative to a coordinate system.
///
public MTConnect.Devices.Configurations.IMotion Motion { get; set; }
+
///
/// Potential energy sources for the Component.
///
public MTConnect.Devices.Configurations.IPowerSource PowerSource { get; set; }
+
///
/// Association between two pieces of equipment or assets that may function independently but together perform a manufacturing operation.
///
public System.Collections.Generic.IEnumerable Relationships { get; set; }
+
///
/// Configuration for a Sensor.
///
public MTConnect.Devices.Configurations.ISensorConfiguration SensorConfiguration { get; set; }
+
///
/// References to a file with the three-dimensional geometry of the Component or Composition.
///
public MTConnect.Devices.Configurations.ISolidModel SolidModel { get; set; }
+
///
/// Design characteristics for a piece of equipment.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/ConfigurationRelationship.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/ConfigurationRelationship.g.cs
index dda5933bf..175cf2b58 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/ConfigurationRelationship.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/ConfigurationRelationship.g.cs
@@ -20,16 +20,19 @@ public abstract class ConfigurationRelationship : IConfigurationRelationship
/// Defines whether the services or functions provided by the associated piece of equipment is required for the operation of this piece of equipment.
///
public MTConnect.Devices.Configurations.CriticalityType? Criticality { get; set; }
+
///
/// Unique identifier for this ConfigurationRelationship.
///
public string Id { get; set; }
+
///
/// Name associated with this ConfigurationRelationship.
///
public string Name { get; set; }
+
///
/// Defines the authority that this piece of equipment has relative to the associated piece of equipment.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/ControlLimits.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/ControlLimits.g.cs
index 3f20c7e94..241424c7b 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/ControlLimits.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/ControlLimits.g.cs
@@ -20,21 +20,25 @@ public class ControlLimits : IControlLimits
/// Lower conformance boundary for a variable.> Note: immediate concern or action may be required.
///
public double? LowerLimit { get; set; }
+
///
/// Lower boundary indicating increased concern and supervision may be required.
///
public double? LowerWarning { get; set; }
+
///
/// Numeric target or expected value.
///
public double? Nominal { get; set; }
+
///
/// Upper conformance boundary for a variable.> Note: immediate concern or action may be required.
///
public double? UpperLimit { get; set; }
+
///
/// Upper boundary indicating increased concern and supervision may be required.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/CoordinateSystem.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/CoordinateSystem.g.cs
index 692cf1f60..e4331db02 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/CoordinateSystem.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/CoordinateSystem.g.cs
@@ -20,41 +20,49 @@ public class CoordinateSystem : ICoordinateSystem
/// Natural language description of the CoordinateSystem.
///
public string Description { get; set; }
+
///
/// Unique identifier for the coordinate system.
///
public string Id { get; set; }
+
///
/// Name of the coordinate system.
///
public string Name { get; set; }
+
///
/// Manufacturer's name or users name for the coordinate system.
///
public string NativeName { get; set; }
+
///
/// Coordinates of the origin position of a coordinate system.
///
public MTConnect.Devices.Configurations.IAbstractOrigin Origin { get; set; }
+
///
/// Id.
///
public string ParentIdRef { get; set; }
+
///
/// Process of transforming to the origin position of the coordinate system from a parent coordinate system using Translation and Rotation.
///
public MTConnect.Devices.Configurations.ITransformation Transformation { get; set; }
+
///
/// Type of coordinate system.
///
public MTConnect.Devices.Configurations.CoordinateSystemType Type { get; set; }
+
///
/// UUID for the coordinate system.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/DeviceRelationship.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/DeviceRelationship.g.cs
index 20eecd55c..5d9993567 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/DeviceRelationship.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/DeviceRelationship.g.cs
@@ -20,16 +20,19 @@ public class DeviceRelationship : ConfigurationRelationship, IDeviceRelationship
/// Uuid of the associated piece of equipment.
///
public string DeviceUuidRef { get; set; }
+
///
/// URI identifying the agent that is publishing information for the associated piece of equipment.
///
public string Href { get; set; }
+
///
/// Defines the services or capabilities that the referenced piece of equipment provides relative to this piece of equipment.
///
public MTConnect.Devices.Configurations.RoleType? Role { get; set; }
+
///
/// `xlink:type`**MUST** have a fixed value of `locator` as defined in W3C XLink 1.1 https://www.w3.org/TR/xlink11/.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/IRelationship.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/IRelationship.g.cs
deleted file mode 100644
index c70512669..000000000
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/IRelationship.g.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (c) 2023 TrakHound Inc., All Rights Reserved.
-// TrakHound Inc. licenses this file to you under the MIT license.
-
-namespace MTConnect.Devices.Configurations
-{
- ///
- /// Association between two pieces of equipment that function independently but together perform a manufacturing operation.
- ///
- public interface IRelationship
- {
- ///
- /// Defines whether the services or functions provided by the associated piece of equipment is required for the operation of this piece of equipment.
- ///
- MTConnect.Devices.Configurations.CriticalityType Criticality { get; }
-
- ///
- /// Unique identifier for this ConfigurationRelationship.
- ///
- string Id { get; }
-
- ///
- /// Name associated with this ConfigurationRelationship.
- ///
- string Name { get; }
-
- ///
- /// Defines the authority that this piece of equipment has relative to the associated piece of equipment.
- ///
- MTConnect.Devices.Configurations.RelationshipType Type { get; }
- }
-}
\ No newline at end of file
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/ImageFile.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/ImageFile.g.cs
index 276c2ae40..62f3f3e25 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/ImageFile.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/ImageFile.g.cs
@@ -20,16 +20,19 @@ public class ImageFile : IImageFile
/// URL giving the location of the image file.
///
public string Href { get; set; }
+
///
/// Unique identifier of the image file.
///
public string Id { get; set; }
+
///
/// Mime type of the image file.
///
public string MediaType { get; set; }
+
///
/// Description of the image file.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/Motion.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/Motion.g.cs
index 8689cd1bd..d5297c5a9 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/Motion.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/Motion.g.cs
@@ -20,41 +20,49 @@ public class Motion : IMotion
/// Describes if this component is actuated directly or indirectly as a result of other motion.
///
public MTConnect.Devices.Configurations.MotionActuationType Actuation { get; set; }
+
///
/// Axis along or around which the Component moves relative to a coordinate system.
///
public MTConnect.Devices.Configurations.IAbstractAxis Axis { get; set; }
+
///
/// Coordinate system within which the kinematic motion occurs.
///
public string CoordinateSystemIdRef { get; set; }
+
///
/// Textual description for Motion.
///
public string Description { get; set; }
+
///
/// Unique identifier for this element.
///
public string Id { get; set; }
+
///
/// Coordinates of the origin position of a coordinate system.
///
public MTConnect.Devices.Configurations.IAbstractOrigin Origin { get; set; }
+
///
/// Id.The kinematic chain connects all components using the parent relations. All motion is connected to the motion of the parent. The first node in the chain will not have a parent.
///
public string ParentIdRef { get; set; }
+
///
/// Process of transforming to the origin position of the coordinate system from a parent coordinate system using Translation and Rotation.
///
public MTConnect.Devices.Configurations.ITransformation Transformation { get; set; }
+
///
/// Type of motion.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/OriginDataSet.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/OriginDataSet.g.cs
index 8648a4ccb..e0779b41c 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/OriginDataSet.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/OriginDataSet.g.cs
@@ -20,11 +20,13 @@ public class OriginDataSet : AbstractOrigin, IOriginDataSet, IDataSet
/// X-coordinate.
///
public string X { get; set; }
+
///
/// Y-coordinate.
///
public string Y { get; set; }
+
///
/// X-coordinate.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/PowerSource.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/PowerSource.g.cs
index 977f3cba6..12ae371bf 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/PowerSource.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/PowerSource.g.cs
@@ -20,21 +20,25 @@ public class PowerSource : IPowerSource
/// Reference to the Component providing observations about the power source.
///
public string ComponentIdRef { get; set; }
+
///
/// Unique identifier for the power source.
///
public string Id { get; set; }
+
///
/// Optional precedence for a given power source.
///
public int Order { get; set; }
+
///
/// Type of the power source.
///
public MTConnect.Devices.Configurations.PowerSourceType Type { get; set; }
+
///
/// Name of the power source.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/ProcessSpecification.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/ProcessSpecification.g.cs
index adb16849a..58016d448 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/ProcessSpecification.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/ProcessSpecification.g.cs
@@ -20,11 +20,13 @@ public class ProcessSpecification : Specification, IProcessSpecification
/// Set of limits that is used to trigger warning or alarm indicators.
///
public MTConnect.Devices.Configurations.IAlarmLimits AlarmLimits { get; set; }
+
///
/// Set of limits that is used to indicate whether a process variable is stable and in control.
///
public MTConnect.Devices.Configurations.IControlLimits ControlLimits { get; set; }
+
///
/// Set of limits that define a range of values designating acceptable performance for a variable.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/RotationDataSet.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/RotationDataSet.g.cs
index 3a5ecb0fd..faf57eb80 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/RotationDataSet.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/RotationDataSet.g.cs
@@ -20,11 +20,13 @@ public class RotationDataSet : AbstractRotation, IRotationDataSet, IDataSet
/// Rotation about X axis.
///
public string A { get; set; }
+
///
/// Rotation about Y axis.
///
public string B { get; set; }
+
///
/// Rotation about Z axis.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/ScaleDataSet.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/ScaleDataSet.g.cs
index 561b1db71..503a7d488 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/ScaleDataSet.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/ScaleDataSet.g.cs
@@ -20,11 +20,13 @@ public class ScaleDataSet : AbstractScale, IScaleDataSet, IDataSet
/// Multiplier for X axis.
///
public double X { get; set; }
+
///
/// Multiplier for Y axis.
///
public double Y { get; set; }
+
///
/// Multiplier for Z axis.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/SensorConfiguration.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/SensorConfiguration.g.cs
index e5df6628d..06b575f68 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/SensorConfiguration.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/SensorConfiguration.g.cs
@@ -20,21 +20,25 @@ public class SensorConfiguration : ISensorConfiguration
/// Date upon which the sensor unit was last calibrated.
///
public System.DateTime? CalibrationDate { get; set; }
+
///
/// The initials of the person verifying the validity of the calibration data.
///
public string CalibrationInitials { get; set; }
+
///
/// Sensing element of a Sensor.
///
public System.Collections.Generic.IEnumerable Channels { get; set; }
+
///
/// Version number for the sensor unit as specified by the manufacturer.
///
public string FirmwareVersion { get; set; }
+
///
/// Date upon which the sensor unit is next scheduled to be calibrated.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/SolidModel.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/SolidModel.g.cs
index b55330026..7f1e2bc59 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/SolidModel.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/SolidModel.g.cs
@@ -20,46 +20,55 @@ public class SolidModel : ISolidModel
/// Reference to the coordinate system for this SolidModel.
///
public string CoordinateSystemIdRef { get; set; }
+
///
/// URL giving the location of the SolidModel. solidModelIdRef is used.href is of type `xlink:href` from the W3C XLink specification.
///
public string Href { get; set; }
+
///
/// Unique identifier for this element.
///
public string Id { get; set; }
+
///
/// SolidModelIdRef **MUST** be given. > Note: `Item` defined in ASME Y14.100 - A nonspecific term used to denote any unit or product, including materials, parts, assemblies, equipment, accessories, and computer software.
///
public string ItemRef { get; set; }
+
///
/// Format of the referenced document.
///
public MTConnect.Devices.Configurations.MediaType MediaType { get; set; }
+
///
/// NativeUnits. See DataItem.
///
public string NativeUnits { get; set; }
+
///
/// Either a single multiplier applied to all three dimensions or a three space multiplier given in the X, Y, and Z dimensions in the coordinate system used for the SolidModel.
///
public MTConnect.Devices.Configurations.IAbstractScale Scale { get; set; }
+
///
/// Associated model file if an item reference is used.
///
public string SolidModelIdRef { get; set; }
+
///
/// Process of transforming to the origin position of the coordinate system from a parent coordinate system using Translation and Rotation.
///
public MTConnect.Devices.Configurations.ITransformation Transformation { get; set; }
+
///
/// Units. See DataItem.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/Specification.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/Specification.g.cs
index 611148668..bcfa8555a 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/Specification.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/Specification.g.cs
@@ -20,76 +20,91 @@ public class Specification : ISpecification
/// Id associated with this entity.
///
public string CompositionIdRef { get; set; }
+
///
/// References the CoordinateSystem for geometric Specification elements.
///
public string CoordinateSystemIdRef { get; set; }
+
///
/// Id associated with this entity.
///
public string DataItemIdRef { get; set; }
+
///
/// Unique identifier for this Specification.
///
public string Id { get; set; }
+
///
/// Lower conformance boundary for a variable.> Note: immediate concern or action may be required.
///
public double? LowerLimit { get; set; }
+
///
/// Lower boundary indicating increased concern and supervision may be required.
///
public double? LowerWarning { get; set; }
+
///
/// Numeric upper constraint.
///
public double? Maximum { get; set; }
+
///
/// Numeric lower constraint.
///
public double? Minimum { get; set; }
+
///
/// Name provides additional meaning and differentiates between Specification entities.
///
public string Name { get; set; }
+
///
/// Numeric target or expected value.
///
public double? Nominal { get; set; }
+
///
/// Reference to the creator of the Specification.
///
public MTConnect.Devices.Configurations.Originator Originator { get; set; }
+
///
/// SubType. See DataItem.
///
public string SubType { get; set; }
+
///
/// Type. See DataItem Types.
///
public string Type { get; set; }
+
///
/// Units. See DataItem.
///
public string Units { get; set; }
+
///
/// Upper conformance boundary for a variable.> Note: immediate concern or action may be required.
///
public double? UpperLimit { get; set; }
+
///
/// Upper boundary indicating increased concern and supervision may be required.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/SpecificationLimits.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/SpecificationLimits.g.cs
index d348eabef..f36d7dd27 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/SpecificationLimits.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/SpecificationLimits.g.cs
@@ -20,11 +20,13 @@ public class SpecificationLimits : ISpecificationLimits
/// Lower conformance boundary for a variable.> Note: immediate concern or action may be required.
///
public double? LowerLimit { get; set; }
+
///
/// Numeric target or expected value.
///
public double? Nominal { get; set; }
+
///
/// Upper conformance boundary for a variable.> Note: immediate concern or action may be required.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/Transformation.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/Transformation.g.cs
index ce53a868a..a22e29ceb 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/Transformation.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/Transformation.g.cs
@@ -20,6 +20,7 @@ public class Transformation : ITransformation
/// Rotations about X, Y, and Z axes are expressed in A, B, and C respectively within a 3-dimensional vector.
///
public MTConnect.Devices.Configurations.IAbstractRotation Rotation { get; set; }
+
///
/// Translations along X, Y, and Z axes are expressed as x,y, and z respectively within a 3-dimensional vector.
diff --git a/libraries/MTConnect.NET-Common/Devices/Configurations/TranslationDataSet.g.cs b/libraries/MTConnect.NET-Common/Devices/Configurations/TranslationDataSet.g.cs
index 1291d23be..452c693cc 100644
--- a/libraries/MTConnect.NET-Common/Devices/Configurations/TranslationDataSet.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Configurations/TranslationDataSet.g.cs
@@ -20,11 +20,13 @@ public class TranslationDataSet : AbstractTranslation, ITranslationDataSet, IDat
/// Translation along X axis.
///
public string X { get; set; }
+
///
/// Translation along Y axis.
///
public string Y { get; set; }
+
///
/// Translation along Z axis.
diff --git a/libraries/MTConnect.NET-Common/Devices/Constraints.g.cs b/libraries/MTConnect.NET-Common/Devices/Constraints.g.cs
index 1653e7817..d28ada396 100644
--- a/libraries/MTConnect.NET-Common/Devices/Constraints.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Constraints.g.cs
@@ -20,21 +20,25 @@ public class Constraints : IConstraints
/// Provides a means to control when an agent records updated information for a DataItem.
///
public MTConnect.Devices.IFilter Filter { get; set; }
+
///
/// Numeric upper constraint.If the data reported for a data item is a range of numeric values, the expected value reported **MAY** be described with an upper limit defined by this constraint.
///
public double? Maximum { get; set; }
+
///
/// Numeric lower constraint.If the data reported for a data item is a range of numeric values, the expected value reported **MAY** be described with a lower limit defined by this constraint.
///
public double? Minimum { get; set; }
+
///
/// Numeric target or expected value.
///
public double? Nominal { get; set; }
+
///
/// Single data value that is expected to be reported for a DataItem.Value **MUST NOT** be used in conjunction with any other Constraint elements.
diff --git a/libraries/MTConnect.NET-Common/Devices/DataItem.g.cs b/libraries/MTConnect.NET-Common/Devices/DataItem.g.cs
index f6e8a6de1..801bbfefb 100644
--- a/libraries/MTConnect.NET-Common/Devices/DataItem.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/DataItem.g.cs
@@ -20,111 +20,133 @@ public partial class DataItem : IDataItem
/// Specifies the kind of information provided by a data item.
///
public MTConnect.Devices.DataItemCategory Category { get; set; }
+
///
/// Identifier attribute of the Composition that the reported data is most closely associated.
///
public string CompositionId { get; set; }
+
///
/// Organize a set of expected values that can be reported for a DataItem.
///
public MTConnect.Devices.IConstraints Constraints { get; set; }
+
///
/// For measured values relative to a coordinate system like Position, the coordinate system used may be reported.coordinateSystemIdRef.
///
public MTConnect.Devices.DataItemCoordinateSystem CoordinateSystem { get; set; }
+
///
/// Associated CoordinateSystem context for the DataItem.
///
public string CoordinateSystemIdRef { get; set; }
+
///
/// Representation is either `DATA_SET` or `TABLE`.
///
public MTConnect.Devices.IDataItemDefinition Definition { get; set; }
+
///
/// Indication signifying whether each value reported for the Observation is significant and whether duplicate values are to be suppressed.discrete, the default value **MUST** be `false`.
///
public bool Discrete { get; set; }
+
///
/// Provides a means to control when an agent records updated information for a DataItem.
///
public System.Collections.Generic.IEnumerable Filters { get; set; }
+
///
/// Unique identifier for this data item.
///
public string Id { get; set; }
+
///
/// Starting value for a DataItem as well as the value to be set for the DataItem after a reset event.
///
public string InitialValue { get; set; }
+
///
/// Name of the data item.
///
public string Name { get; set; }
+
///
/// Used to convert the reported value to represent the original measured value.
///
public int NativeScale { get; set; }
+
///
/// Native units of measurement for the reported value of the data item.
///
public string NativeUnits { get; set; }
+
///
/// Association between a DataItem and another entity.
///
public System.Collections.Generic.IEnumerable Relationships { get; set; }
+
///
/// Description of a means to interpret data consisting of multiple data points or samples reported as a single value. representation is not specified, it **MUST** be determined to be `VALUE`.
///
public MTConnect.Devices.DataItemRepresentation Representation { get; set; }
+
///
/// Type of event that may cause a reset to occur.
///
public MTConnect.Devices.DataItemResetTrigger? ResetTrigger { get; set; }
+
///
/// Rate at which successive samples of a data item are recorded by a piece of equipment.
///
public double SampleRate { get; set; }
+
///
/// Number of significant digits in the reported value.
///
public int? SignificantDigits { get; set; }
+
///
/// Identifies the Component, DataItem, or Composition from which a measured value originates.
///
public MTConnect.Devices.ISource Source { get; set; }
+
///
/// Type of statistical calculation performed on a series of data samples to provide the reported data value.
///
public MTConnect.Devices.DataItemStatistic? Statistic { get; set; }
+
///
/// Type.
///
public string SubType { get; set; }
+
///
/// Type of data being measured. See DataItem Types.
///
public string Type { get; set; }
+
///
/// Unit of measurement for the reported value of the data item.
diff --git a/libraries/MTConnect.NET-Common/Devices/DataItemDefinition.g.cs b/libraries/MTConnect.NET-Common/Devices/DataItemDefinition.g.cs
index 1d2010f68..226b84540 100644
--- a/libraries/MTConnect.NET-Common/Devices/DataItemDefinition.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/DataItemDefinition.g.cs
@@ -20,11 +20,13 @@ public class DataItemDefinition : IDataItemDefinition
/// Semantic definition of a Cell.
///
public System.Collections.Generic.IEnumerable CellDefinitions { get; set; }
+
///
/// Textual description for Definition.
///
public string Description { get; set; }
+
///
/// Semantic definition of an Entry.
diff --git a/libraries/MTConnect.NET-Common/Devices/Description.g.cs b/libraries/MTConnect.NET-Common/Devices/Description.g.cs
index 7bfa4657d..5973a108b 100644
--- a/libraries/MTConnect.NET-Common/Devices/Description.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Description.g.cs
@@ -20,21 +20,25 @@ public class Description : IDescription
/// Name of the manufacturer of the physical or logical part of a piece of equipment represented by this element.
///
public string Manufacturer { get; set; }
+
///
/// Model description of the physical part or logical function of a piece of equipment represented by this element.
///
public string Model { get; set; }
+
///
/// Serial number associated with a piece of equipment.
///
public string SerialNumber { get; set; }
+
///
/// Identifier where a manufacturing function takes place.
///
public string Station { get; set; }
+
///
/// Description of the element.
diff --git a/libraries/MTConnect.NET-Common/Devices/Device.g.cs b/libraries/MTConnect.NET-Common/Devices/Device.g.cs
index 3d5eb9919..5aadc71db 100644
--- a/libraries/MTConnect.NET-Common/Devices/Device.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Device.g.cs
@@ -20,66 +20,79 @@ public partial class Device : IDevice
/// Condensed message digest from a secure one-way hash function. FIPS PUB 180-4
///
public string Hash { get; set; }
+
///
/// MTConnect version of the Device Information Model used to configure the information to be published for a piece of equipment in an MTConnect Response Document.
///
public System.Version MTConnectVersion { get; set; }
+
///
/// Name of an element or a piece of equipment.
///
public string Name { get; set; }
+
///
/// Universally unique identifier for the element.
///
public string Uuid { get; set; }
+
///
/// Logical or physical entity that provides a capability.
///
public System.Collections.Generic.IEnumerable Components { get; set; }
+
///
/// Functional part of a piece of equipment contained within a Component.
///
public System.Collections.Generic.IEnumerable Compositions { get; set; }
+
///
/// Technical information about an entity describing its physical layout, functional characteristics, and relationships with other entities.
///
public MTConnect.Devices.Configurations.IConfiguration Configuration { get; set; }
+
///
/// Specifies the CoordinateSystem for this Component and its children.
///
public string CoordinateSystemIdRef { get; set; }
+
///
/// Descriptive content.
///
public MTConnect.Devices.IDescription Description { get; set; }
+
///
/// Unique identifier for the Component.
///
public string Id { get; set; }
+
///
/// Common name associated with Component.
///
public string NativeName { get; set; }
+
///
/// Pointer to information that is associated with another entity defined elsewhere in the MTConnectDevices entity for a piece of equipment.
///
public System.Collections.Generic.IEnumerable References { get; set; }
+
///
/// Interval in milliseconds between the completion of the reading of the data associated with the Component until the beginning of the next sampling of that data.This information may be used by client software applications to understand how often information from a Component is expected to be refreshed.The refresh rate for data from all child Component entities will be thesampleInterval provided for the child Component.
///
public double SampleInterval { get; set; }
+
///
/// SampleInterval.
diff --git a/libraries/MTConnect.NET-Common/Devices/EntryDefinition.g.cs b/libraries/MTConnect.NET-Common/Devices/EntryDefinition.g.cs
index 2b807d0ad..2a23a541f 100644
--- a/libraries/MTConnect.NET-Common/Devices/EntryDefinition.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/EntryDefinition.g.cs
@@ -20,31 +20,37 @@ public class EntryDefinition : IEntryDefinition
/// Semantic definition of a Cell.
///
public System.Collections.Generic.IEnumerable CellDefinitions { get; set; }
+
///
/// Textual description for EntryDefinition.
///
public string Description { get; set; }
+
///
/// Unique identification of the Entry in the Definition. key.
///
public string Key { get; set; }
+
///
/// Key.
///
public string KeyType { get; set; }
+
///
/// SubType. See DataItem.
///
public string SubType { get; set; }
+
///
/// Type. See DataItem Types.
///
public string Type { get; set; }
+
///
/// Units. See Value Properties of DataItem.
diff --git a/libraries/MTConnect.NET-Common/Devices/Filter.g.cs b/libraries/MTConnect.NET-Common/Devices/Filter.g.cs
index 672b82be2..413fe8927 100644
--- a/libraries/MTConnect.NET-Common/Devices/Filter.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Filter.g.cs
@@ -20,6 +20,7 @@ public class Filter : IFilter
/// Type of Filter.
///
public MTConnect.Devices.DataItemFilterType Type { get; set; }
+
///
///
diff --git a/libraries/MTConnect.NET-Common/Devices/References/Reference.g.cs b/libraries/MTConnect.NET-Common/Devices/References/Reference.g.cs
index 5242e059c..4ab619b18 100644
--- a/libraries/MTConnect.NET-Common/Devices/References/Reference.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/References/Reference.g.cs
@@ -20,16 +20,19 @@ public abstract partial class Reference : IReference
/// Id that contains the information to be associated with this entity.
///
public string DataItemId { get; set; }
+
///
/// Pointer to the `id` of an entity that contains the information to be associated with this entity.
///
public string IdRef { get; set; }
+
///
/// name of an element or a piece of equipment.
///
public string Name { get; set; }
+
///
/// Id that contains the information to be associated with this entity.
diff --git a/libraries/MTConnect.NET-Common/Devices/Source.g.cs b/libraries/MTConnect.NET-Common/Devices/Source.g.cs
index 532862167..857d2c0d1 100644
--- a/libraries/MTConnect.NET-Common/Devices/Source.g.cs
+++ b/libraries/MTConnect.NET-Common/Devices/Source.g.cs
@@ -20,16 +20,19 @@ public class Source : ISource
/// Identifier of the Component that represents the physical part of a piece of equipment where the data represented by the DataItem originated.
///
public string ComponentId { get; set; }
+
///
/// Identifier of the Composition that represents the physical part of a piece of equipment where the data represented by the DataItem originated.
///
public string CompositionId { get; set; }
+
///
/// Identifier of the DataItem that represents the originally measured value of the data referenced by this DataItem.
///
public string DataItemId { get; set; }
+
///
/// Identifier of the source entity.
diff --git a/libraries/MTConnect.NET-Common/Observations/Events/NetworkWireless.g.cs b/libraries/MTConnect.NET-Common/Observations/Events/NetworkWireless.g.cs
deleted file mode 100644
index 395953e27..000000000
--- a/libraries/MTConnect.NET-Common/Observations/Events/NetworkWireless.g.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) 2023 TrakHound Inc., All Rights Reserved.
-// TrakHound Inc. licenses this file to you under the MIT license.
-
-namespace MTConnect.Observations.Events
-{
- ///
- ///
- ///
- public enum NetworkWireless
- {
- ///
- ///
- ///
- YES,
-
- ///
- ///
- ///
- NO
- }
-}
\ No newline at end of file
diff --git a/libraries/MTConnect.NET-Common/Observations/Events/SensorStateDetect.g.cs b/libraries/MTConnect.NET-Common/Observations/Events/SensorStateDetect.g.cs
deleted file mode 100644
index 19886904e..000000000
--- a/libraries/MTConnect.NET-Common/Observations/Events/SensorStateDetect.g.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (c) 2023 TrakHound Inc., All Rights Reserved.
-// TrakHound Inc. licenses this file to you under the MIT license.
-
-namespace MTConnect.Observations.Events
-{
- ///
- ///
- ///
- public enum SensorStateDetect
- {
- ///
- /// Activation state of the Composition is in an `ON` condition, it is operating, or it is powered.
- ///
- ON,
-
- ///
- /// Activation state of the Composition is in an `OFF` condition, it is not operating, or it is not powered.
- ///
- OFF
- }
-}
\ No newline at end of file
diff --git a/stryker-config.json b/stryker-config.json
new file mode 100644
index 000000000..d5b92fea6
--- /dev/null
+++ b/stryker-config.json
@@ -0,0 +1,57 @@
+// Baseline mutation score is 7.75% on 2026-08-20 (measured with
+// MTConnect.NET-Common as the pilot assembly + the full
+// MTConnect.NET-Common-Tests suite, at PR #233 head 43497c5d, Stryker.NET
+// v4.16.0 with the Regex mutator ignored).
+//
+// Thresholds. `break: 5` and `low: 5` sit below the 7.75% baseline, so a
+// baseline-conforming run passes CI (Stryker exits non-zero only when the
+// score drops below `break`). `high: 8` sits deliberately ABOVE the
+// baseline as an aspirational marker — a baseline-conforming run reports
+// yellow (`low <= score < high`) rather than green, keeping visible
+// pressure on the follow-up campaign until the floor rises above `high`.
+// See TrakHound/MTConnect.NET#242 for the phase-by-phase plan
+// (categorise survivors -> kill by subsystem -> raise thresholds in step
+// to 20 -> 40 -> 60 -> 80%+ -> expand Stryker to sibling assemblies).
+//
+// Stryker.NET tool version is pinned in `.config/dotnet-tools.json`
+// alongside the thresholds so the 7.75% baseline stays reproducible; a
+// mutator-set change in a later Stryker release could shift the score
+// even against unchanged production code.
+//
+// JSONC (JSON with comments) is the Stryker.NET native config format;
+// leave these comments in place through subsequent edits.
+{
+ "stryker-config": {
+ "project": "MTConnect.NET-Common.csproj",
+ "solution": "MTConnect.NET.sln",
+ "test-projects": [
+ "tests/MTConnect.NET-Common-Tests/MTConnect.NET-Common-Tests.csproj"
+ ],
+ "target-framework": "net8.0",
+ "reporters": [
+ "progress",
+ "cleartext",
+ "html",
+ "json"
+ ],
+ "thresholds": {
+ "high": 8,
+ "low": 5,
+ "break": 5
+ },
+ "concurrency": 4,
+ "mutation-level": "Complete",
+ "since": {
+ "enabled": false
+ },
+ "mutate": [
+ "!**/*.g.cs",
+ "!libraries/MTConnect.NET-Common/Assets/**/*.g.cs",
+ "!libraries/MTConnect.NET-Common/Devices/**/*.g.cs",
+ "!libraries/MTConnect.NET-Common/Observations/**/*.g.cs"
+ ],
+ "ignore-mutations": [
+ "Regex"
+ ]
+ }
+}
diff --git a/tests/MTConnect.NET-Common-Tests/Devices/Components/ComponentTests.cs b/tests/MTConnect.NET-Common-Tests/Devices/Components/ComponentTests.cs
new file mode 100644
index 000000000..a1b9b7cfa
--- /dev/null
+++ b/tests/MTConnect.NET-Common-Tests/Devices/Components/ComponentTests.cs
@@ -0,0 +1,104 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using System;
+using MTConnect.Devices.Components;
+using MTConnect.Tests.Common.TestHelpers;
+using NUnit.Framework;
+
+namespace MTConnect.NET_Common_Tests.Devices.Components
+{
+ // Version-gated shape assertions for the Component subclasses introduced
+ // across the v2.6 and v2.7 MTConnect Standard releases.
+ //
+ // - XMI: https://github.com/mtconnect/mtconnect_sysml_model tags
+ // v2.6 (SHA 08185447bf86…) — CuttingTorch, Electrode
+ // v2.7 (SHA 25796ac591bb…) — PinTool, ToolHolder
+ // UML classes under Device Information Model > Components.
+ // - XSD: https://schemas.mtconnect.org/schemas/MTConnectDevices_2.6.xsd
+ // MTConnectDevices_2.7.xsd
+ // (each TypeId appears in the ComponentType enumeration.)
+ // - Prose: MTConnect Standard Part_2.0_Devices_v2.6 section 3.4.18
+ // "CuttingTorch" / section 3.4.21 "Electrode";
+ // Part_2.0_Devices_v2.7 section 7 "Component types" (PinTool,
+ // ToolHolder).
+ //
+ // Every fixture below is matrix-parameterised over
+ // MTConnectVersionMatrix.All per plan Design Decision D1
+ // (2026-08-19); Assume.That gates each assertion to versions where the
+ // spec introduced the type. Rows below the floor surface as
+ // Inconclusive in the test explorer, which is the D1-ruled shape for
+ // "gated out" versus "ran and passed".
+ /// Pins the behaviour expressed by the test name: component tests.
+ [TestFixture]
+ public class ComponentTests
+ {
+ // Source: XMI v2.6 UML `CuttingTorch` (Component Types); XSD v2.6
+ // ``.
+ /// Pins the behaviour expressed by the test name: cutting torch component constructs with correct type.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void CuttingTorchComponent_constructs_with_correct_type(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version26),
+ "CuttingTorch was introduced in MTConnect v2.6.");
+
+ var c = new CuttingTorchComponent();
+ Assert.That(c.Type, Is.EqualTo("CuttingTorch"));
+ Assert.That(c.Name, Is.Null);
+ Assert.That(CuttingTorchComponent.TypeId, Is.EqualTo("CuttingTorch"));
+ Assert.That(CuttingTorchComponent.NameId, Is.EqualTo("cuttingTorch"));
+ }
+
+ // Source: XMI v2.6 UML `Electrode` (Component Types); XSD v2.6
+ // ``.
+ /// Pins the behaviour expressed by the test name: electrode component constructs with correct type.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void ElectrodeComponent_constructs_with_correct_type(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version26),
+ "Electrode was introduced in MTConnect v2.6.");
+
+ var c = new ElectrodeComponent();
+ Assert.That(c.Type, Is.EqualTo("Electrode"));
+ Assert.That(c.Name, Is.Null);
+ Assert.That(ElectrodeComponent.TypeId, Is.EqualTo("Electrode"));
+ Assert.That(ElectrodeComponent.NameId, Is.EqualTo("electrode"));
+ }
+
+ // Source: XMI v2.7 UML `PinTool` (Component Types); XSD v2.7
+ // ComponentType enumeration value `PinTool`.
+ /// Pins the behaviour expressed by the test name: pin tool component constructs with correct type.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void PinToolComponent_constructs_with_correct_type(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version27),
+ "PinTool was introduced in MTConnect v2.7.");
+
+ var c = new PinToolComponent();
+ Assert.That(c.Type, Is.EqualTo("PinTool"));
+ Assert.That(c.Name, Is.Null);
+ Assert.That(PinToolComponent.TypeId, Is.EqualTo("PinTool"));
+ Assert.That(PinToolComponent.NameId, Is.EqualTo("pinTool"));
+ }
+
+ // Source: XMI v2.7 UML `ToolHolder` (Component Types); XSD v2.7
+ // ComponentType enumeration value `ToolHolder`.
+ /// Pins the behaviour expressed by the test name: tool holder component constructs with correct type.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void ToolHolderComponent_constructs_with_correct_type(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version27),
+ "ToolHolder was introduced in MTConnect v2.7.");
+
+ var c = new ToolHolderComponent();
+ Assert.That(c.Type, Is.EqualTo("ToolHolder"));
+ Assert.That(c.Name, Is.Null);
+ Assert.That(ToolHolderComponent.TypeId, Is.EqualTo("ToolHolder"));
+ Assert.That(ToolHolderComponent.NameId, Is.EqualTo("toolHolder"));
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-Common-Tests/Devices/Configurations/ConfigurationTests.cs b/tests/MTConnect.NET-Common-Tests/Devices/Configurations/ConfigurationTests.cs
new file mode 100644
index 000000000..4d9f8ab12
--- /dev/null
+++ b/tests/MTConnect.NET-Common-Tests/Devices/Configurations/ConfigurationTests.cs
@@ -0,0 +1,252 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using System;
+using MTConnect.Devices.Configurations;
+using MTConnect.Tests.Common.TestHelpers;
+using NUnit.Framework;
+
+namespace MTConnect.NET_Common_Tests.Devices.Configurations
+{
+ // Version-gated shape assertions for the v2.7 Configuration sub-element
+ // family: new geometric primitives (Axis, Origin, Rotation, Scale,
+ // Translation) and their data-set representation siblings (*DataSet),
+ // plus the cross-package-grafted DataSet base that the universal
+ // cross-package parent resolver brought into the Devices.Configurations
+ // namespace.
+ //
+ // - XMI: https://github.com/mtconnect/mtconnect_sysml_model @ tag v2.7
+ // UML classes under Device Information Model > Configurations:
+ // * Axis / AxisDataSet
+ // * Origin / OriginDataSet
+ // * Rotation / RotationDataSet
+ // * Scale / ScaleDataSet
+ // * Translation / TranslationDataSet
+ // plus the abstract bases (AbstractAxis, AbstractOrigin, etc.).
+ // - XSD: https://schemas.mtconnect.org/schemas/MTConnectDevices_2.7.xsd
+ // (the geometric-primitive complexTypes encode the same shape
+ // on the wire under ).
+ // - Prose: MTConnect Standard Part_2.0_Devices_v2.7 section 10
+ // "Configuration" — describes how Component-level Configuration
+ // carries the geometric primitives that locate a Component in
+ // space.
+ //
+ // Every fixture below is matrix-parameterised over
+ // MTConnectVersionMatrix.All per plan Design Decision D1
+ // (2026-08-19). Assume.That gates every assertion to v2.7 (the version
+ // that introduced the Configuration family); rows below the floor
+ // surface as Inconclusive.
+ /// Pins the behaviour expressed by the test name: configuration tests.
+ [TestFixture]
+ public class ConfigurationTests
+ {
+ // The DataSet base (grafted from Observation.Representations via the
+ // universal resolver) compiles, instantiates, and surfaces its
+ // const description.
+ /// Pins the behaviour expressed by the test name: data set base constructs and implements i data set.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void DataSet_base_constructs_and_implements_IDataSet(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version27),
+ "DataSet was grafted into Devices.Configurations in MTConnect v2.7.");
+
+ var ds = new DataSet();
+ Assert.That(ds, Is.InstanceOf());
+ Assert.That(DataSet.DescriptionText, Is.Not.Null.And.Not.Empty);
+ }
+
+ // The five concrete sub-types follow the same shape: parameterless
+ // ctor, populates X/Y/Z (or A/B/C) fields, implements IDataSet
+ // (interface, not the concrete DataSet base — *DataSet types
+ // polymorphically extend their Abstract base, gaining IDataSet
+ // as a marker interface so XML/JSON serialisers can narrow on it).
+ /// Pins the behaviour expressed by the test name: axis data set has xyz fields and implements i data set.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void AxisDataSet_has_xyz_fields_and_implements_IDataSet(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version27),
+ "AxisDataSet was introduced in MTConnect v2.7.");
+
+ var a = new AxisDataSet { X = 1.0, Y = 2.0, Z = 3.0 };
+ Assert.That(a, Is.InstanceOf());
+ Assert.That(a, Is.InstanceOf());
+ Assert.That(a.X, Is.EqualTo(1.0));
+ Assert.That(a.Y, Is.EqualTo(2.0));
+ Assert.That(a.Z, Is.EqualTo(3.0));
+ }
+
+ /// Pins the behaviour expressed by the test name: origin data set has xyz fields and implements i data set.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void OriginDataSet_has_xyz_fields_and_implements_IDataSet(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version27),
+ "OriginDataSet was introduced in MTConnect v2.7.");
+
+ var o = new OriginDataSet { X = "1", Y = "2", Z = "3" };
+ Assert.That(o, Is.InstanceOf());
+ Assert.That(o, Is.InstanceOf());
+ }
+
+ /// Pins the behaviour expressed by the test name: rotation data set has abc fields and implements i data set.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void RotationDataSet_has_abc_fields_and_implements_IDataSet(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version27),
+ "RotationDataSet was introduced in MTConnect v2.7.");
+
+ // Rotations are reported as A (about X), B (about Y), C (about Z).
+ var r = new RotationDataSet { A = "10", B = "20", C = "30" };
+ Assert.That(r, Is.InstanceOf());
+ Assert.That(r, Is.InstanceOf());
+ }
+
+ /// Pins the behaviour expressed by the test name: scale data set implements i data set.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void ScaleDataSet_implements_IDataSet(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version27),
+ "ScaleDataSet was introduced in MTConnect v2.7.");
+
+ var s = new ScaleDataSet();
+ Assert.That(s, Is.InstanceOf());
+ Assert.That(s, Is.InstanceOf());
+ }
+
+ /// Pins the behaviour expressed by the test name: translation data set implements i data set.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void TranslationDataSet_implements_IDataSet(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version27),
+ "TranslationDataSet was introduced in MTConnect v2.7.");
+
+ var t = new TranslationDataSet();
+ Assert.That(t, Is.InstanceOf());
+ Assert.That(t, Is.InstanceOf());
+ }
+
+ // Concrete (non-DataSet) representations of the same primitives,
+ // also landed in v2.7 alongside their DataSet siblings.
+ /// Pins the behaviour expressed by the test name: axis inherits abstract axis and constructs.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void Axis_inherits_AbstractAxis_and_constructs(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version27),
+ "Axis was introduced in MTConnect v2.7.");
+
+ var a = new Axis { Value = "X" };
+ Assert.That(a, Is.InstanceOf());
+ Assert.That(a, Is.InstanceOf());
+ Assert.That(a.Value, Is.EqualTo("X"));
+ }
+
+ /// Pins the behaviour expressed by the test name: origin inherits abstract origin.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void Origin_inherits_AbstractOrigin(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version27),
+ "Origin was introduced in MTConnect v2.7.");
+
+ var o = new Origin();
+ Assert.That(o, Is.InstanceOf());
+ Assert.That(o, Is.InstanceOf());
+ }
+
+ /// Pins the behaviour expressed by the test name: rotation inherits abstract rotation.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void Rotation_inherits_AbstractRotation(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version27),
+ "Rotation was introduced in MTConnect v2.7.");
+
+ Assert.That(new Rotation(), Is.InstanceOf());
+ }
+
+ /// Pins the behaviour expressed by the test name: scale inherits abstract scale.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void Scale_inherits_AbstractScale(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version27),
+ "Scale was introduced in MTConnect v2.7.");
+
+ Assert.That(new Scale(), Is.InstanceOf());
+ }
+
+ /// Pins the behaviour expressed by the test name: translation inherits abstract translation.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void Translation_inherits_AbstractTranslation(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version27),
+ "Translation was introduced in MTConnect v2.7.");
+
+ Assert.That(new Translation(), Is.InstanceOf());
+ }
+
+ // The Abstract* bases are abstract — verify so a future regen that
+ // accidentally drops the abstract modifier trips here.
+ /// Pins the behaviour expressed by the test name: abstract axis is abstract.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void AbstractAxis_is_abstract(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version27),
+ "AbstractAxis was introduced in MTConnect v2.7.");
+
+ Assert.That(typeof(AbstractAxis).IsAbstract, Is.True);
+ }
+
+ /// Pins the behaviour expressed by the test name: abstract origin is abstract.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void AbstractOrigin_is_abstract(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version27),
+ "AbstractOrigin was introduced in MTConnect v2.7.");
+
+ Assert.That(typeof(AbstractOrigin).IsAbstract, Is.True);
+ }
+
+ /// Pins the behaviour expressed by the test name: abstract rotation is abstract.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void AbstractRotation_is_abstract(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version27),
+ "AbstractRotation was introduced in MTConnect v2.7.");
+
+ Assert.That(typeof(AbstractRotation).IsAbstract, Is.True);
+ }
+
+ /// Pins the behaviour expressed by the test name: abstract scale is abstract.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void AbstractScale_is_abstract(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version27),
+ "AbstractScale was introduced in MTConnect v2.7.");
+
+ Assert.That(typeof(AbstractScale).IsAbstract, Is.True);
+ }
+
+ /// Pins the behaviour expressed by the test name: abstract translation is abstract.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void AbstractTranslation_is_abstract(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version27),
+ "AbstractTranslation was introduced in MTConnect v2.7.");
+
+ Assert.That(typeof(AbstractTranslation).IsAbstract, Is.True);
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-Common-Tests/Devices/DataItems/DataItemTypeTests.cs b/tests/MTConnect.NET-Common-Tests/Devices/DataItems/DataItemTypeTests.cs
new file mode 100644
index 000000000..30bd15319
--- /dev/null
+++ b/tests/MTConnect.NET-Common-Tests/Devices/DataItems/DataItemTypeTests.cs
@@ -0,0 +1,208 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using MTConnect.Devices;
+using MTConnect.Devices.DataItems;
+using MTConnect.Tests.Common.TestHelpers;
+using NUnit.Framework;
+
+namespace MTConnect.NET_Common_Tests.Devices.DataItems
+{
+ // Version-gated shape assertions for every DataItem type the v2.6 and
+ // v2.7 SysML XMI introduces.
+ //
+ // - XMI: https://github.com/mtconnect/mtconnect_sysml_model tags
+ // v2.6 (SHA 08185447bf86…):
+ // * AssetAddedDataItem — xmi:id _2024x_68e0225_1744799118784_270323_23376
+ // * AssociatedAssetIdDataItem — xmi:id _2024x_68e0225_1744800465544_…
+ // * AssetChangedDataItem — description rewritten in v2.6
+ // v2.7 (SHA 25796ac591bb…) — Observation Types package:
+ // * BindingState (Event), Depth (Event), FixtureAssetId (Event),
+ // SwingAngle (Event), SwingDiameter (Event), SwingRadius (Event),
+ // TaskAssetId (Event), WaterHardness (Sample).
+ // - XSD: https://schemas.mtconnect.org/schemas/MTConnectStreams_2.6.xsd
+ // MTConnectStreams_2.7.xsd
+ // (each TypeId is encoded in the EventEnum / SampleEnum
+ // enumerations.)
+ // - Prose: MTConnect Standard Part_2.0_Streams_v2.6 section 11.5 "Asset
+ // events" (asset-event split rationale);
+ // Part_2.0_Streams_v2.7 sections 11/13 "Event/Sample types"
+ // (v2.7 additions).
+ //
+ // Every fixture below is matrix-parameterised over
+ // MTConnectVersionMatrix.All per plan Design Decision D1
+ // (2026-08-19). Assume.That gates each assertion to versions where
+ // the spec introduced the type; rows below the floor surface as
+ // Inconclusive in the test explorer.
+ /// Pins the behaviour expressed by the test name: data item type tests.
+ [TestFixture]
+ public class DataItemTypeTests
+ {
+ // Source: XMI v2.6 UML class `AssetAddedDataItem`; XSD v2.6 enum
+ // `EventEnum` value `ASSET_ADDED`.
+ /// Pins the behaviour expressed by the test name: asset added data item constructs with event metadata.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void AssetAddedDataItem_constructs_with_event_metadata(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version26),
+ "AssetAddedDataItem was introduced in MTConnect v2.6.");
+
+ var d = new AssetAddedDataItem();
+ Assert.That(d.Type, Is.EqualTo("ASSET_ADDED"));
+ Assert.That(d.Name, Is.EqualTo("assetAdded"));
+ Assert.That(d.Category, Is.EqualTo(DataItemCategory.EVENT));
+ Assert.That(AssetAddedDataItem.TypeId, Is.EqualTo("ASSET_ADDED"));
+ Assert.That(AssetAddedDataItem.NameId, Is.EqualTo("assetAdded"));
+ Assert.That(AssetAddedDataItem.CategoryId, Is.EqualTo(DataItemCategory.EVENT));
+ }
+
+ // Source: XMI v2.6 — `DataItem.id` formation rule via parent device.
+ /// Pins the behaviour expressed by the test name: asset added data item with device id produces qualified id.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void AssetAddedDataItem_with_deviceId_produces_qualified_id(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version26),
+ "AssetAddedDataItem was introduced in MTConnect v2.6.");
+
+ var d = new AssetAddedDataItem("dev01");
+ Assert.That(d.Id, Is.Not.Null.And.Not.Empty);
+ Assert.That(d.Id, Does.Contain("dev01"));
+ Assert.That(d.Type, Is.EqualTo("ASSET_ADDED"));
+ }
+
+ // Source: XMI v2.6 UML class `AssociatedAssetIdDataItem`; XSD v2.6
+ // EventEnum value `ASSOCIATED_ASSET_ID`.
+ /// Pins the behaviour expressed by the test name: associated asset id data item constructs with event metadata.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void AssociatedAssetIdDataItem_constructs_with_event_metadata(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version26),
+ "AssociatedAssetIdDataItem was introduced in MTConnect v2.6.");
+
+ var d = new AssociatedAssetIdDataItem();
+ Assert.That(d.Type, Is.EqualTo(AssociatedAssetIdDataItem.TypeId));
+ Assert.That(d.Name, Is.EqualTo(AssociatedAssetIdDataItem.NameId));
+ Assert.That(d.Category, Is.EqualTo(AssociatedAssetIdDataItem.CategoryId));
+ Assert.That(AssociatedAssetIdDataItem.TypeId, Is.EqualTo("ASSOCIATED_ASSET_ID"));
+ Assert.That(AssociatedAssetIdDataItem.CategoryId, Is.EqualTo(DataItemCategory.EVENT));
+ }
+
+ // Source: XMI v2.6 — generalization of `AssetAddedDataItem` is `DataItem`.
+ /// Pins the behaviour expressed by the test name: asset added data item inherits from data item.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void AssetAddedDataItem_inherits_from_DataItem(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version26),
+ "AssetAddedDataItem was introduced in MTConnect v2.6.");
+
+ Assert.That(typeof(AssetAddedDataItem).BaseType, Is.EqualTo(typeof(DataItem)));
+ }
+
+ // Source: XMI v2.6 — generalization of `AssociatedAssetIdDataItem` is `DataItem`.
+ /// Pins the behaviour expressed by the test name: associated asset id data item inherits from data item.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void AssociatedAssetIdDataItem_inherits_from_DataItem(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version26),
+ "AssociatedAssetIdDataItem was introduced in MTConnect v2.6.");
+
+ Assert.That(typeof(AssociatedAssetIdDataItem).BaseType, Is.EqualTo(typeof(DataItem)));
+ }
+
+ // Source: XMI v2.6 description on `AssetChangedDataItem` (was "added or
+ // changed" in v2.5; now "changed" only). Prose confirms in
+ // Part_2.0_Streams_v2.6 section 11.5.
+ /// Pins the behaviour expressed by the test name: asset changed data item description narrowed.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void AssetChangedDataItem_description_narrowed(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version26),
+ "The narrowed description shipped in MTConnect v2.6.");
+
+ Assert.That(AssetChangedDataItem.DescriptionText,
+ Is.EqualTo("AssetId of the Asset that has been changed."),
+ "AssetChangedDataItem description must reflect the v2.6 split " +
+ "where 'added' moved to AssetAddedDataItem");
+ }
+
+ // Combined enumeration of the (Type, ExpectedTypeId, ExpectedCategory)
+ // triples for the v2.7 DataItem additions, cross-multiplied with
+ // MTConnectVersionMatrix.All so each row exercises the full 17-way
+ // version matrix. Assume.That gates every row to v2.7.
+ //
+ // Categories match what the v2.7 SysML XMI declares — the spec
+ // authority. Several types that look "measurement-y" (SwingAngle,
+ // Depth, etc.) are EVENT in the spec rather than SAMPLE; locking
+ // them so a future regen drift is caught immediately.
+ /// Enumerates the (type, expected type id, expected category, version) rows for the v2.7 DataItem sweep.
+ /// The parametric matrix.
+ public static IEnumerable V27DataItemCases()
+ {
+ var kinds = new (Type Type, string TypeId, DataItemCategory Category)[]
+ {
+ (typeof(BindingStateDataItem), "BINDING_STATE", DataItemCategory.EVENT),
+ (typeof(DepthDataItem), "DEPTH", DataItemCategory.EVENT),
+ (typeof(FixtureAssetIdDataItem), "FIXTURE_ASSET_ID", DataItemCategory.EVENT),
+ (typeof(SwingAngleDataItem), "SWING_ANGLE", DataItemCategory.EVENT),
+ (typeof(SwingDiameterDataItem), "SWING_DIAMETER", DataItemCategory.EVENT),
+ (typeof(SwingRadiusDataItem), "SWING_RADIUS", DataItemCategory.EVENT),
+ (typeof(TaskAssetIdDataItem), "TASK_ASSET_ID", DataItemCategory.EVENT),
+ (typeof(WaterHardnessDataItem), "WATER_HARDNESS", DataItemCategory.SAMPLE),
+ };
+
+ foreach (var v in MTConnectVersionMatrix.All)
+ {
+ foreach (var (type, typeId, category) in kinds)
+ {
+ yield return new TestCaseData(type, typeId, category, v)
+ .SetName($"DataItem_constructs_with_correct_metadata({type.Name},{typeId},{category},{v})");
+ }
+ }
+ }
+
+ // Source: XMI v2.7 Observation Types package (each entry above).
+ /// Pins the behaviour expressed by the test name: data item constructs with correct metadata.
+ /// The data item type.
+ /// The expected type id.
+ /// The expected category.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(nameof(V27DataItemCases))]
+ public void DataItem_constructs_with_correct_metadata(
+ Type dataItemType,
+ string expectedTypeId,
+ DataItemCategory expectedCategory,
+ Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version27),
+ "These DataItem types were introduced in MTConnect v2.7.");
+
+ // Wrap with Assert.DoesNotThrow so a missing parameterless ctor
+ // surfaces as a clear NUnit failure with the offending type
+ // name rather than a bare MissingMethodException.
+ object? instance = null;
+ Assert.DoesNotThrow(() => instance = Activator.CreateInstance(dataItemType),
+ $"{dataItemType.Name} should have a public parameterless constructor");
+ Assert.That(instance, Is.Not.Null);
+ Assert.That(instance, Is.InstanceOf());
+
+ var di = (DataItem)instance!;
+ Assert.That(di.Type, Is.EqualTo(expectedTypeId),
+ $"{dataItemType.Name}.Type should be the spec TypeId");
+ Assert.That(di.Category, Is.EqualTo(expectedCategory),
+ $"{dataItemType.Name}.Category should be {expectedCategory}");
+
+ var typeIdConst = dataItemType.GetField("TypeId",
+ System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)?.GetRawConstantValue();
+ Assert.That(typeIdConst, Is.EqualTo(expectedTypeId),
+ $"{dataItemType.Name}.TypeId static const should match the spec TypeId");
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-Common-Tests/DryGenerator/AssertionParityTests.cs b/tests/MTConnect.NET-Common-Tests/DryGenerator/AssertionParityTests.cs
new file mode 100644
index 000000000..c6eb6b9ad
--- /dev/null
+++ b/tests/MTConnect.NET-Common-Tests/DryGenerator/AssertionParityTests.cs
@@ -0,0 +1,173 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using NUnit.Framework;
+
+namespace MTConnect.NET_Common_Tests.DryGenerator
+{
+ // Parity guard for the DRY-generator campaign's Phase 1 migration.
+ //
+ // The migration collapses the per-version fixture family under
+ // tests/MTConnect.NET-Common-Tests/V2_6_V2_7/
+ // into single-topic fixtures at their canonical location:
+ // tests/MTConnect.NET-Common-Tests/Devices/DataItems/DataItemTypeTests.cs
+ // tests/MTConnect.NET-Common-Tests/Devices/Components/ComponentTests.cs
+ // tests/MTConnect.NET-Common-Tests/Devices/Configurations/ConfigurationTests.cs
+ // tests/MTConnect.NET-Common-Tests/Observations/SampleObservationTests.cs
+ // tests/MTConnect.NET-Common-Tests/Enums/EnumArmTests.cs
+ // tests/MTConnect.NET-Common-Tests/MTConnectVersionsTests.cs
+ //
+ // Every assertion the pre-migration fixtures carried MUST re-appear at
+ // its post-migration home. This fixture asserts that invariant by
+ // walking a hardcoded baseline snapshot of the 34 pre-migration
+ // [Test] / [TestCase] method entries (captured 2026-08-19 from
+ // extra-files.user/plans/dry-generator-phase0/baseline-assertions-2026-08-19.txt)
+ // against the live reflection view of the test assembly.
+ //
+ // States:
+ // - RED (pre-migration): the new topic fixtures do not exist yet;
+ // the baseline entries have no post-migration home. Every entry
+ // surfaces as a missing target.
+ // - GREEN (post-migration): every baseline entry resolves to a
+ // method that carries [Test] / [TestCase] / [TestCaseSource] and
+ // lives OUTSIDE the deprecated V2_6_V2_7 namespace. The V2_6_V2_7
+ // folder itself is deleted; PerVersionFolderProhibitionTests
+ // enforces the deletion permanently.
+ //
+ // Renames are declared inline via MigrationMap below. The gitignored
+ // extra-files.user/plans/dry-generator-phase0/renames.tsv artefact is a
+ // human-facing audit trail; the assertion source of truth lives in this
+ // fixture so the test is portable across clones.
+ /// Pins the behaviour expressed by the test name: assertion parity tests.
+ [TestFixture]
+ public class AssertionParityTests
+ {
+ // Every pre-migration method's expected post-migration name.
+ // Identity entries (OldMethod == NewMethod) migrate under the same
+ // name; renames carry a NewMethod that strips the `_in_v2_6`
+ // suffix or the `V2_7_` prefix, since the version is now a
+ // property of the [TestCaseSource(MTConnectVersionMatrix.All)]
+ // matrix rather than encoded in the method name.
+ private static readonly (string OldFile, string OldMethod, string NewMethod)[] MigrationMap =
+ {
+ // V2_6ComponentAndEnumTests.cs (3 methods)
+ ("V2_6ComponentAndEnumTests.cs", "CuttingTorchComponent_constructs_with_correct_type", "CuttingTorchComponent_constructs_with_correct_type"),
+ ("V2_6ComponentAndEnumTests.cs", "ElectrodeComponent_constructs_with_correct_type", "ElectrodeComponent_constructs_with_correct_type"),
+ ("V2_6ComponentAndEnumTests.cs", "MediaType_QIF_MBD_value_present_in_v2_6", "MediaType_QIF_MBD_value_present"),
+
+ // V2_6DataItemTypeTests.cs (6 methods)
+ ("V2_6DataItemTypeTests.cs", "AssetAddedDataItem_constructs_with_event_metadata", "AssetAddedDataItem_constructs_with_event_metadata"),
+ ("V2_6DataItemTypeTests.cs", "AssetAddedDataItem_with_deviceId_produces_qualified_id", "AssetAddedDataItem_with_deviceId_produces_qualified_id"),
+ ("V2_6DataItemTypeTests.cs", "AssociatedAssetIdDataItem_constructs_with_event_metadata", "AssociatedAssetIdDataItem_constructs_with_event_metadata"),
+ ("V2_6DataItemTypeTests.cs", "AssetAddedDataItem_inherits_from_DataItem", "AssetAddedDataItem_inherits_from_DataItem"),
+ ("V2_6DataItemTypeTests.cs", "AssociatedAssetIdDataItem_inherits_from_DataItem", "AssociatedAssetIdDataItem_inherits_from_DataItem"),
+ ("V2_6DataItemTypeTests.cs", "AssetChangedDataItem_description_narrowed_in_v2_6", "AssetChangedDataItem_description_narrowed"),
+
+ // V2_7DataItemTypeTests.cs (1 method, 8 [TestCase] rows)
+ ("V2_7DataItemTypeTests.cs", "V2_7_DataItem_constructs_with_correct_metadata", "DataItem_constructs_with_correct_metadata"),
+
+ // MTConnectVersionsTests.cs (5 methods — kept plain [Test] since
+ // these test constant-value invariants, not per-version behaviour)
+ ("MTConnectVersionsTests.cs", "Version26_constant_equals_2_6", "Version26_constant_equals_2_6"),
+ ("MTConnectVersionsTests.cs", "Version27_constant_equals_2_7", "Version27_constant_equals_2_7"),
+ ("MTConnectVersionsTests.cs", "Max_equals_Version27", "Max_equals_Version27"),
+ ("MTConnectVersionsTests.cs", "Every_published_version_constant_is_distinct_and_monotonic", "Every_published_version_constant_is_distinct_and_monotonic"),
+ ("MTConnectVersionsTests.cs", "Version19_field_does_not_exist", "Version19_field_does_not_exist"),
+
+ // V2_7ComponentTests.cs (2 methods)
+ ("V2_7ComponentTests.cs", "PinToolComponent_constructs_with_correct_type", "PinToolComponent_constructs_with_correct_type"),
+ ("V2_7ComponentTests.cs", "ToolHolderComponent_constructs_with_correct_type", "ToolHolderComponent_constructs_with_correct_type"),
+
+ // V2_7ConfigurationDataSetTests.cs (16 methods)
+ ("V2_7ConfigurationDataSetTests.cs", "DataSet_base_constructs_and_implements_IDataSet", "DataSet_base_constructs_and_implements_IDataSet"),
+ ("V2_7ConfigurationDataSetTests.cs", "AxisDataSet_has_xyz_fields_and_implements_IDataSet", "AxisDataSet_has_xyz_fields_and_implements_IDataSet"),
+ ("V2_7ConfigurationDataSetTests.cs", "OriginDataSet_has_xyz_fields_and_implements_IDataSet", "OriginDataSet_has_xyz_fields_and_implements_IDataSet"),
+ ("V2_7ConfigurationDataSetTests.cs", "RotationDataSet_has_abc_fields_and_implements_IDataSet", "RotationDataSet_has_abc_fields_and_implements_IDataSet"),
+ ("V2_7ConfigurationDataSetTests.cs", "ScaleDataSet_implements_IDataSet", "ScaleDataSet_implements_IDataSet"),
+ ("V2_7ConfigurationDataSetTests.cs", "TranslationDataSet_implements_IDataSet", "TranslationDataSet_implements_IDataSet"),
+ ("V2_7ConfigurationDataSetTests.cs", "Axis_inherits_AbstractAxis_and_constructs", "Axis_inherits_AbstractAxis_and_constructs"),
+ ("V2_7ConfigurationDataSetTests.cs", "Origin_inherits_AbstractOrigin", "Origin_inherits_AbstractOrigin"),
+ ("V2_7ConfigurationDataSetTests.cs", "Rotation_inherits_AbstractRotation", "Rotation_inherits_AbstractRotation"),
+ ("V2_7ConfigurationDataSetTests.cs", "Scale_inherits_AbstractScale", "Scale_inherits_AbstractScale"),
+ ("V2_7ConfigurationDataSetTests.cs", "Translation_inherits_AbstractTranslation", "Translation_inherits_AbstractTranslation"),
+ ("V2_7ConfigurationDataSetTests.cs", "AbstractAxis_is_abstract", "AbstractAxis_is_abstract"),
+ ("V2_7ConfigurationDataSetTests.cs", "AbstractOrigin_is_abstract", "AbstractOrigin_is_abstract"),
+ ("V2_7ConfigurationDataSetTests.cs", "AbstractRotation_is_abstract", "AbstractRotation_is_abstract"),
+ ("V2_7ConfigurationDataSetTests.cs", "AbstractScale_is_abstract", "AbstractScale_is_abstract"),
+ ("V2_7ConfigurationDataSetTests.cs", "AbstractTranslation_is_abstract", "AbstractTranslation_is_abstract"),
+
+ // V2_7SampleObservationTests.cs (1 method)
+ ("V2_7SampleObservationTests.cs", "WaterHardness_sample_observation_round_trip", "WaterHardness_sample_observation_round_trip"),
+ };
+
+ /// Pins the invariant: every baseline assertion has a post-migration home.
+ [Test]
+ public void Every_baseline_assertion_has_a_post_migration_home()
+ {
+ var postMigrationMethods = EnumeratePostMigrationTestMethods();
+ var missing = new List();
+
+ foreach (var (oldFile, oldMethod, newMethod) in MigrationMap)
+ {
+ if (!postMigrationMethods.Contains(newMethod))
+ {
+ missing.Add($"{oldFile}::{oldMethod} -> {newMethod}");
+ }
+ }
+
+ Assert.That(missing, Is.Empty,
+ "Baseline assertions missing a post-migration home:\n "
+ + string.Join("\n ", missing));
+ }
+
+ /// Pins the invariant: the migration map covers every baseline entry.
+ [Test]
+ public void Migration_map_covers_the_full_baseline_of_34_entries()
+ {
+ // Guard against silent shrinkage of the map itself. The Phase 0
+ // baseline captured exactly 34 [Test] / [TestCase] method
+ // entries; if a future edit trims the map below that floor, the
+ // parity guard is inspecting less than the full baseline and
+ // this fixture must fail loudly.
+ Assert.That(MigrationMap.Length, Is.EqualTo(34),
+ "MigrationMap has drifted from the 34-entry baseline captured "
+ + "on 2026-08-19. Re-verify against "
+ + "extra-files.user/plans/dry-generator-phase0/baseline-assertions-2026-08-19.txt "
+ + "before editing.");
+ }
+
+ // Reflect over the test assembly and return every method name that
+ // carries [Test], [TestCase], or [TestCaseSource] and lives OUTSIDE
+ // the deprecated V2_6_V2_7 namespace. The name-only granularity
+ // matches the plan's Phase 1.4 assertion-diff shape.
+ private static ISet EnumeratePostMigrationTestMethods()
+ {
+ var assembly = typeof(AssertionParityTests).Assembly;
+ return assembly.GetTypes()
+ .Where(t => t.Namespace != null
+ && !t.Namespace.Contains("V2_6_V2_7", StringComparison.Ordinal))
+ .SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.Instance))
+ .Where(HasNUnitTestAttribute)
+ .Select(m => m.Name)
+ .ToHashSet(StringComparer.Ordinal);
+ }
+
+ private static bool HasNUnitTestAttribute(MethodInfo method)
+ {
+ foreach (var attribute in method.GetCustomAttributes(inherit: false))
+ {
+ if (attribute is TestAttribute
+ || attribute is TestCaseAttribute
+ || attribute is TestCaseSourceAttribute)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-Common-Tests/DryGenerator/PerVersionFolderProhibitionTests.cs b/tests/MTConnect.NET-Common-Tests/DryGenerator/PerVersionFolderProhibitionTests.cs
new file mode 100644
index 000000000..efa086e8a
--- /dev/null
+++ b/tests/MTConnect.NET-Common-Tests/DryGenerator/PerVersionFolderProhibitionTests.cs
@@ -0,0 +1,199 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using NUnit.Framework;
+
+namespace MTConnect.NET_Common_Tests.DryGenerator
+{
+ // Permanent regression guard against the deprecated per-version
+ // fixture-folder convention. Fires RED if any new V/ directory
+ // or V*Tests fixture class returns to
+ // tests/MTConnect.NET-Common-Tests/.
+ //
+ // Enforcement rules (plan §"Single-test-file-per-topic convention"):
+ // * No V/ directory under tests/MTConnect.NET-Common-Tests/.
+ // * No fixture class matching the V*Tests pattern in the
+ // assembly, except historical anchors listed in HistoricalAnchors.
+ // * No fixture file name matching V*Tests.cs on disk.
+ //
+ // Historical anchors (e.g. CppAgentParityWorkflowTests pinned to
+ // Version25) are NOT migrated — they document a deliberate,
+ // permanent pin. Add such classes to HistoricalAnchors with a
+ // rationale comment before the entry.
+ /// Pins the behaviour expressed by the test name: per version folder prohibition tests.
+ [TestFixture]
+ public class PerVersionFolderProhibitionTests
+ {
+ // Fixture full-class-names allowed to keep a V* naming
+ // convention (historical anchors that document a permanent
+ // version pin). Each entry must include a rationale comment.
+ private static readonly HashSet HistoricalAnchors = new(StringComparer.Ordinal)
+ {
+ // No historical anchors at HEAD — this list exists so that a
+ // future contributor introducing a deliberately-pinned
+ // fixture (e.g. CppAgentParityWorkflowTests pinned to a
+ // specific version for spec-fidelity reasons) can document
+ // the pin here rather than trip the guard.
+ };
+
+ /// Pins the invariant: no V-N-M subdirectory exists under the test project.
+ [Test]
+ public void No_per_version_directory_exists_under_tests_MTConnect_NET_Common_Tests()
+ {
+ var testsRoot = LocateTestProjectRoot();
+ var offenders = Directory.EnumerateDirectories(
+ testsRoot,
+ "V*",
+ SearchOption.AllDirectories)
+ .Where(path => !IsUnderIgnoredDirectory(path))
+ .Where(IsPerVersionDirectoryName)
+ .Select(path => Path.GetRelativePath(testsRoot, path))
+ .OrderBy(x => x, StringComparer.Ordinal)
+ .ToList();
+
+ Assert.That(offenders, Is.Empty,
+ "Per-version fixture directories (V/) are deprecated by the "
+ + "DRY-generator campaign. Migrate the fixtures into a topic-first "
+ + "layout (Devices/DataItems/, Devices/Components/, etc.) with "
+ + "matrix-parameterised version-gated assertions. Offending directories:\n "
+ + string.Join("\n ", offenders));
+ }
+
+ /// Pins the invariant: no V-N-M fixture file lives on disk under the test project.
+ [Test]
+ public void No_per_version_fixture_file_exists_under_tests_MTConnect_NET_Common_Tests()
+ {
+ var testsRoot = LocateTestProjectRoot();
+ var offenders = Directory.EnumerateFiles(
+ testsRoot,
+ "V*Tests.cs",
+ SearchOption.AllDirectories)
+ .Where(path => !IsUnderIgnoredDirectory(path))
+ .Where(path => IsPerVersionFileName(Path.GetFileName(path)))
+ .Select(path => Path.GetRelativePath(testsRoot, path))
+ .OrderBy(x => x, StringComparer.Ordinal)
+ .ToList();
+
+ Assert.That(offenders, Is.Empty,
+ "Per-version fixture files (V*Tests.cs) are deprecated by the "
+ + "DRY-generator campaign. Rename to a topic-first name (e.g. "
+ + "V2_7DataItemTypeTests.cs -> DataItemTypeTests.cs). Offending files:\n "
+ + string.Join("\n ", offenders));
+ }
+
+ /// Pins the invariant: no V-N-M fixture class exists in the test assembly.
+ [Test]
+ public void No_per_version_fixture_class_exists_in_the_test_assembly()
+ {
+ var assembly = typeof(PerVersionFolderProhibitionTests).Assembly;
+ var offenders = assembly.GetTypes()
+ .Where(t => t.IsPublic || t.IsNestedPublic)
+ .Where(t => t.GetCustomAttribute() != null)
+ .Where(t => IsPerVersionClassName(t.Name))
+ .Where(t => !HistoricalAnchors.Contains(t.FullName ?? t.Name))
+ .Select(t => t.FullName ?? t.Name)
+ .OrderBy(x => x, StringComparer.Ordinal)
+ .ToList();
+
+ Assert.That(offenders, Is.Empty,
+ "Per-version fixture classes (V*Tests) are deprecated by the "
+ + "DRY-generator campaign. If a class is a deliberate historical anchor "
+ + "(e.g. a permanent version pin for spec-fidelity reasons), add its "
+ + "full name to PerVersionFolderProhibitionTests.HistoricalAnchors with "
+ + "a rationale comment. Offending classes:\n "
+ + string.Join("\n ", offenders));
+ }
+
+ // Locate the test project's source root by walking up from the test
+ // binary's directory. The test project's .csproj lives at the root.
+ // This walker is resilient to being invoked from bin/Debug/net8.0/,
+ // bin/Release/net8.0/, or a runsettings-overridden directory.
+ private static string LocateTestProjectRoot()
+ {
+ var dir = new DirectoryInfo(TestContext.CurrentContext.TestDirectory);
+ while (dir != null)
+ {
+ if (File.Exists(Path.Combine(dir.FullName, "MTConnect.NET-Common-Tests.csproj")))
+ {
+ return dir.FullName;
+ }
+ dir = dir.Parent;
+ }
+ throw new InvalidOperationException(
+ "Could not locate MTConnect.NET-Common-Tests.csproj from test directory: "
+ + TestContext.CurrentContext.TestDirectory);
+ }
+
+ // Match V_[__...] directory names —
+ // e.g. V2_6, V2_6_V2_7, V1_8. Any leading-V-then-underscored-digits
+ // sequence counts.
+ private static bool IsPerVersionDirectoryName(string absolutePath)
+ {
+ var name = Path.GetFileName(absolutePath);
+ return LooksLikePerVersionToken(name);
+ }
+
+ // Match V_*Tests.cs — the file naming convention
+ // the migration retires. Excludes anything without the V-prefix
+ // digit-underscored pattern.
+ private static bool IsPerVersionFileName(string fileName)
+ {
+ if (!fileName.EndsWith("Tests.cs", StringComparison.Ordinal))
+ {
+ return false;
+ }
+ // Strip ".cs" and the "Tests" suffix; the head must still
+ // start with a per-version token.
+ var head = fileName.Substring(0, fileName.Length - "Tests.cs".Length);
+ return LooksLikePerVersionToken(head);
+ }
+
+ // Match V_*Tests class names for the assembly
+ // reflection sweep.
+ private static bool IsPerVersionClassName(string className)
+ {
+ if (!className.EndsWith("Tests", StringComparison.Ordinal))
+ {
+ return false;
+ }
+ var head = className.Substring(0, className.Length - "Tests".Length);
+ return LooksLikePerVersionToken(head);
+ }
+
+ // A per-version token starts with 'V', then one-or-more digits,
+ // then an underscore, then one-or-more digits, then any suffix
+ // (which may include additional V_ segments).
+ private static bool LooksLikePerVersionToken(string head)
+ {
+ if (string.IsNullOrEmpty(head) || head[0] != 'V')
+ {
+ return false;
+ }
+ int i = 1;
+ // one or more digits after V
+ if (i >= head.Length || !char.IsDigit(head[i])) return false;
+ while (i < head.Length && char.IsDigit(head[i])) i++;
+ // required underscore separator
+ if (i >= head.Length || head[i] != '_') return false;
+ i++;
+ // one or more digits after the underscore
+ if (i >= head.Length || !char.IsDigit(head[i])) return false;
+ return true;
+ }
+
+ // The recursive directory walker crosses into bin/ and obj/ under
+ // Debug builds; filter those out so the guard reflects the source
+ // tree rather than build artefacts.
+ private static bool IsUnderIgnoredDirectory(string absolutePath)
+ {
+ var normalised = absolutePath.Replace('\\', '/');
+ return normalised.Contains("/bin/", StringComparison.Ordinal)
+ || normalised.Contains("/obj/", StringComparison.Ordinal);
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-Common-Tests/DryGenerator/TopicFixtureCoverageTests.cs b/tests/MTConnect.NET-Common-Tests/DryGenerator/TopicFixtureCoverageTests.cs
new file mode 100644
index 000000000..3aad7e304
--- /dev/null
+++ b/tests/MTConnect.NET-Common-Tests/DryGenerator/TopicFixtureCoverageTests.cs
@@ -0,0 +1,207 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using NUnit.Framework;
+
+namespace MTConnect.NET_Common_Tests.DryGenerator
+{
+ // Phase 2.2 topic-fixture coverage guard (DRY-generator campaign plan
+ // §2.2 — "Extend RegeneratedTypesCoverageTests with per-topic coverage").
+ //
+ // For every spec-anchor type migrated out of the deprecated per-version
+ // fixture family (tests/MTConnect.NET-Common-Tests/V2_6_V2_7/) the
+ // canonical topic-fixture file MUST name the type at least once. This
+ // is a permanent guard: a future edit that renames a topic fixture or
+ // deletes an anchor assertion without a replacement fires RED here.
+ //
+ // Complementarity with the sibling guards:
+ // - AssertionParityTests holds the 34-entry method-name migration
+ // map and asserts every entry resolves to a live [Test] method.
+ // It does NOT check that the resolved method actually references
+ // the anchor type (a rename that changed the fixture location AND
+ // replaced the assertion body would slip past a name-only guard).
+ // - PerVersionFolderProhibitionTests asserts no V/ topology
+ // regrowth. It says nothing about coverage of anchor types.
+ // - TopicFixtureCoverageTests (this file) asserts every anchor type
+ // name appears in its designated topic-fixture source file. This
+ // is the source-text cross-check the plan calls for.
+ //
+ // Source of truth for the anchor list: the migration renames.tsv
+ // artefact under extra-files.user/plans/dry-generator-phase0/
+ // (gitignored — the tsv is the audit trail; the C# entries below
+ // are the assertion source). Every anchor type mentioned in a
+ // renames.tsv row lands here with its topic-fixture destination.
+ //
+ // Substring-match risk: the scan uses a whole-token regex
+ // (\b\b) so a shorter type name (e.g. Axis) does not falsely
+ // match a longer one (AxisDataSet, AbstractAxis). Comments inside the
+ // topic fixture count as valid mentions — the guard is that the type
+ // name is present in the source text, not that a specific attribute
+ // shape references it.
+ //
+ // Source authority:
+ // - SysML XMI: https://github.com/mtconnect/mtconnect_sysml_model
+ // (per-version tag). Every anchor type maps to a UML class that
+ // the SysML importer emits into MTConnect.NET-Common.
+ // - MTConnect Standard Part 2 — Devices Information Model /
+ // Part 3 — Streams / Part 4 — Assets. Defines the topic
+ // hierarchy the topic-fixture files mirror.
+ /// Pins the invariant: every migrated spec-anchor type is named in its designated topic-fixture source file.
+ [TestFixture]
+ public class TopicFixtureCoverageTests
+ {
+ // (anchor_type_name, topic_fixture_relative_path) pairs sourced
+ // verbatim from renames.tsv. New spec-version bumps append rows
+ // here alongside the topic-fixture edit; this map is the ONE
+ // place the anchor-set is versioned.
+ //
+ // Path is relative to the tests/MTConnect.NET-Common-Tests/
+ // project root. Forward slashes match POSIX conventions; the
+ // path resolver below normalises for Windows.
+ private static readonly (string AnchorType, string TopicFixtureRelativePath)[] TopicAnchors =
+ {
+ // --- Component types (2 v2.6, 2 v2.7) ---------------------
+ ("CuttingTorchComponent", "Devices/Components/ComponentTests.cs"),
+ ("ElectrodeComponent", "Devices/Components/ComponentTests.cs"),
+ ("PinToolComponent", "Devices/Components/ComponentTests.cs"),
+ ("ToolHolderComponent", "Devices/Components/ComponentTests.cs"),
+
+ // --- DataItem types (v2.6 anchor set) ---------------------
+ ("AssetAddedDataItem", "Devices/DataItems/DataItemTypeTests.cs"),
+ ("AssociatedAssetIdDataItem", "Devices/DataItems/DataItemTypeTests.cs"),
+ ("AssetChangedDataItem", "Devices/DataItems/DataItemTypeTests.cs"),
+
+ // --- Configuration DataSet types (v2.7 anchor set) --------
+ ("DataSet", "Devices/Configurations/ConfigurationTests.cs"),
+ ("AxisDataSet", "Devices/Configurations/ConfigurationTests.cs"),
+ ("OriginDataSet", "Devices/Configurations/ConfigurationTests.cs"),
+ ("RotationDataSet", "Devices/Configurations/ConfigurationTests.cs"),
+ ("ScaleDataSet", "Devices/Configurations/ConfigurationTests.cs"),
+ ("TranslationDataSet", "Devices/Configurations/ConfigurationTests.cs"),
+ ("AbstractAxis", "Devices/Configurations/ConfigurationTests.cs"),
+ ("AbstractOrigin", "Devices/Configurations/ConfigurationTests.cs"),
+ ("AbstractRotation", "Devices/Configurations/ConfigurationTests.cs"),
+ ("AbstractScale", "Devices/Configurations/ConfigurationTests.cs"),
+ ("AbstractTranslation", "Devices/Configurations/ConfigurationTests.cs"),
+
+ // --- Sample observation (v2.7 anchor) ---------------------
+ ("WaterHardness", "Observations/SampleObservationTests.cs"),
+
+ // --- Enum arm (v2.6 anchor) -------------------------------
+ ("MediaType", "Enums/EnumArmTests.cs"),
+ ("QIF_MBD", "Enums/EnumArmTests.cs"),
+
+ // --- MTConnectVersions constants (v2.6/v2.7 anchors) ------
+ ("Version26", "MTConnectVersionsTests.cs"),
+ ("Version27", "MTConnectVersionsTests.cs"),
+ };
+
+ /// Produces one test-case row per (anchor_type, topic_fixture) pair.
+ /// Enumeration of NUnit TestCaseData rows keyed by anchor type name.
+ public static IEnumerable Anchors()
+ {
+ foreach (var (anchorType, topicFixtureRelativePath) in TopicAnchors)
+ {
+ yield return new TestCaseData(anchorType, topicFixtureRelativePath)
+ .SetName($"Topic_fixture_names_{anchorType}");
+ }
+ }
+
+ /// Pins the invariant: the designated topic fixture source references the anchor type by name at least once.
+ /// The spec-anchor type name (as it appears in generated C# sources).
+ /// Path to the topic-fixture file, relative to the test project root; forward-slash separator.
+ [Test]
+ [TestCaseSource(nameof(Anchors))]
+ public void Topic_fixture_source_references_anchor_type(string anchorType, string topicFixtureRelativePath)
+ {
+ var testsRoot = LocateTestProjectRoot();
+ var absolutePath = Path.Combine(testsRoot,
+ topicFixtureRelativePath.Replace('/', Path.DirectorySeparatorChar));
+
+ Assert.That(File.Exists(absolutePath), Is.True,
+ $"Topic fixture file '{topicFixtureRelativePath}' is missing under '{testsRoot}'. "
+ + "The topic-first convention requires every anchor type to live in its "
+ + "canonical topic fixture; adding an anchor row to TopicAnchors and then "
+ + "renaming/deleting the target file is the failure mode this guard catches.");
+
+ var source = File.ReadAllText(absolutePath);
+ // Whole-word match so that a shorter anchor (e.g. Axis) does not
+ // spuriously match a longer type name (AbstractAxis, AxisDataSet).
+ var pattern = new Regex($@"\b{Regex.Escape(anchorType)}\b", RegexOptions.CultureInvariant);
+ Assert.That(pattern.IsMatch(source), Is.True,
+ $"Topic fixture '{topicFixtureRelativePath}' does not reference the anchor "
+ + $"type '{anchorType}'. The DRY-generator Phase 1 migration pinned this "
+ + $"type at this topic-fixture home; a coverage-parity regression happens when "
+ + "the anchor is silently removed. Restore the assertion (or move the anchor "
+ + "row in TopicFixtureCoverageTests.TopicAnchors to a different topic fixture "
+ + "AND leave a rationale) before landing the change.");
+ }
+
+ /// Pins the smoke-invariant: the TopicAnchors map does not silently shrink below the migrated baseline.
+ [Test]
+ public void TopicAnchors_covers_at_least_the_full_migrated_baseline()
+ {
+ // The Phase 1 migration surfaced 23 distinct anchor types across
+ // six topic fixtures (4 Components + 3 DataItems + 11 Configuration
+ // + 1 WaterHardness + 2 Enum + 2 Version). The map above
+ // enumerates them explicitly. A future edit that changes the
+ // anchor list must land alongside a rationale in the topic
+ // fixture AND update this pinned count with the same rationale.
+ // Exact-equality matches the AssertionParityTests pattern (which
+ // pins the migration map at exactly 34) so a silent drop of one
+ // row cannot slip past a "≥ baseline" smoke floor.
+ Assert.That(TopicAnchors.Length, Is.EqualTo(23),
+ $"TopicAnchors is at {TopicAnchors.Length} entries — the Phase 1 "
+ + "migration baseline is exactly 23 entries. Restore the anchor rows "
+ + "or, if the change is intentional, update this pinned count with a "
+ + "rationale that cross-references the topic fixture change.");
+ }
+
+ /// Pins the smoke-invariant: every distinct topic fixture named in TopicAnchors is present on disk.
+ [Test]
+ public void Every_topic_fixture_named_in_TopicAnchors_exists_on_disk()
+ {
+ var testsRoot = LocateTestProjectRoot();
+ var missing = TopicAnchors
+ .Select(row => row.TopicFixtureRelativePath)
+ .Distinct(StringComparer.Ordinal)
+ .Where(rel => !File.Exists(Path.Combine(testsRoot,
+ rel.Replace('/', Path.DirectorySeparatorChar))))
+ .OrderBy(x => x, StringComparer.Ordinal)
+ .ToList();
+
+ Assert.That(missing, Is.Empty,
+ "Topic fixture files named in TopicFixtureCoverageTests.TopicAnchors do not "
+ + "exist on disk. Restore the file or repoint the anchor rows to the "
+ + "correct topic-fixture home. Missing files:\n "
+ + string.Join("\n ", missing));
+ }
+
+ // Locate the test project's source root by walking up from the
+ // test binary's directory. The test project's .csproj lives at
+ // the root. This walker mirrors the pattern used in
+ // PerVersionFolderProhibitionTests so both guards resolve the
+ // same root under bin/Debug/net8.0/, bin/Release/net8.0/, and
+ // any runsettings-overridden test directory.
+ private static string LocateTestProjectRoot()
+ {
+ var dir = new DirectoryInfo(TestContext.CurrentContext.TestDirectory);
+ while (dir != null)
+ {
+ if (dir.EnumerateFiles("MTConnect.NET-Common-Tests.csproj").Any())
+ return dir.FullName;
+ dir = dir.Parent;
+ }
+
+ throw new InvalidOperationException(
+ "Could not locate MTConnect.NET-Common-Tests.csproj by walking up from "
+ + $"'{TestContext.CurrentContext.TestDirectory}'. TopicFixtureCoverageTests "
+ + "needs the source-tree root to open topic-fixture files for scanning.");
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-Common-Tests/Enums/EnumArmTests.cs b/tests/MTConnect.NET-Common-Tests/Enums/EnumArmTests.cs
new file mode 100644
index 000000000..4ca9d90fe
--- /dev/null
+++ b/tests/MTConnect.NET-Common-Tests/Enums/EnumArmTests.cs
@@ -0,0 +1,46 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using System;
+using MTConnect.Devices.Configurations;
+using MTConnect.Tests.Common.TestHelpers;
+using NUnit.Framework;
+
+namespace MTConnect.NET_Common_Tests.Enums
+{
+ // Version-gated enum-arm assertions. Each fixture pins a specific
+ // enum-value addition against its introducing MTConnect Standard
+ // version.
+ //
+ // - XMI: https://github.com/mtconnect/mtconnect_sysml_model tag list
+ // — every enum in this file traces to a UML enum extension in
+ // a specific v2.x tag.
+ // - XSD: https://schemas.mtconnect.org/schemas/MTConnectDevices_.xsd
+ // — the simpleType enumerations mirror the XMI additions.
+ // - Prose: MTConnect Standard Part_2.0_Devices/Streams — each enum
+ // extension is described in the part that owns the enum.
+ //
+ // Every fixture below is matrix-parameterised over
+ // MTConnectVersionMatrix.All per plan Design Decision D1
+ // (2026-08-19); Assume.That gates each row to versions where the arm
+ // shipped.
+ /// Pins the behaviour expressed by the test name: enum arm tests.
+ [TestFixture]
+ public class EnumArmTests
+ {
+ // Source: XMI v2.6 enum `MediaTypeEnum` member `QIF_MBD`.
+ // XSD v2.6 lists QIF_MBD inside the MediaType simpleType
+ // enumeration. Prose Part_3.0_Devices_v2.6 section 4.7.2.5
+ // introduces "ISO 10303 QIF model-based design" as the rationale.
+ /// Pins the behaviour expressed by the test name: media type q i f m b d value present.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void MediaType_QIF_MBD_value_present(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version26),
+ "MediaType.QIF_MBD was introduced in MTConnect v2.6.");
+
+ Assert.That(Enum.IsDefined(typeof(MediaType), "QIF_MBD"), Is.True);
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-Common-Tests/V2_6_V2_7/MTConnectVersionsTests.cs b/tests/MTConnect.NET-Common-Tests/MTConnectVersionsTests.cs
similarity index 77%
rename from tests/MTConnect.NET-Common-Tests/V2_6_V2_7/MTConnectVersionsTests.cs
rename to tests/MTConnect.NET-Common-Tests/MTConnectVersionsTests.cs
index b4799004f..b59cdad05 100644
--- a/tests/MTConnect.NET-Common-Tests/V2_6_V2_7/MTConnectVersionsTests.cs
+++ b/tests/MTConnect.NET-Common-Tests/MTConnectVersionsTests.cs
@@ -1,11 +1,24 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
using System;
using System.Linq;
using System.Reflection;
using NUnit.Framework;
-namespace MTConnect.NET_Common_Tests.V2_6_V2_7
+namespace MTConnect.NET_Common_Tests
{
- // Constants-level pins on `MTConnectVersions` for v2.6 and v2.7.
+ // Constants-level invariants on the MTConnectVersions class.
+ //
+ // These assertions test the shape of the MTConnectVersions type itself
+ // (constant values, distinctness, monotonicity, absence of forbidden
+ // constants). They are structural invariants of the type, NOT
+ // per-version behavioural gates, so they run as plain [Test] rather
+ // than under the [TestCaseSource(MTConnectVersionMatrix.All)] matrix
+ // that governs the behavioural fixtures elsewhere in this project.
+ // The plan's Design Decision D1 (2026-08-19) reserves the matrix for
+ // version-sensitive assertions; constant-value assertions live outside
+ // that scope.
//
// - XMI: https://github.com/mtconnect/mtconnect_sysml_model/tree/v2.6
// /v2.7
@@ -14,16 +27,16 @@ namespace MTConnect.NET_Common_Tests.V2_6_V2_7
// - XSD: https://schemas.mtconnect.org/schemas/MTConnectDevices_2.6.xsd
// MTConnectDevices_2.7.xsd
// (each XSD's targetNamespace embeds the version it represents.)
- // - Prose: MTConnect Standard `Part_1.0_Overview_v2.7.pdf` section 1 "Versioning"
- // (the document numbering scheme — v1.0 through v2.7 with v1.9
- // intentionally skipped — is described here.)
+ // - Prose: MTConnect Standard `Part_1.0_Overview_v2.7.pdf` section 1
+ // "Versioning" (the document numbering scheme — v1.0 through
+ // v2.7 with v1.9 intentionally skipped — is described here.)
/// Pins the behaviour expressed by the test name: m t connect versions tests.
[TestFixture]
public class MTConnectVersionsTests
{
// Source: MTConnect SysML model, tag v2.6.
- // The model's version-list element introduces 2.6 between 2.5 and (later)
- // 2.7 with no in-between fractional versions.
+ // The model's version-list element introduces 2.6 between 2.5 and
+ // (later) 2.7 with no in-between fractional versions.
/// Pins the behaviour expressed by the test name: version26 constant equals 2 6.
[Test]
public void Version26_constant_equals_2_6()
@@ -48,8 +61,8 @@ public void Max_equals_Version27()
}
// Pin that the version list contains no 1.9 entry.
- // Source: MTConnect Standard Part_1.0_Overview prose section 1 "Versioning";
- // confirmed by the absence of an XMI tag `v1.9` in
+ // Source: MTConnect Standard Part_1.0_Overview prose section 1
+ // "Versioning"; confirmed by the absence of an XMI tag `v1.9` in
// `mtconnect/mtconnect_sysml_model` (tags: v2.5 b61907fb78,
// v2.6 08185447bf, v2.7 25796ac591).
/// Pins the behaviour expressed by the test name: every published version constant is distinct and monotonic.
@@ -63,8 +76,8 @@ public void Every_published_version_constant_is_distinct_and_monotonic()
.OrderBy(x => x.Value)
.ToList();
- // 17 expected: v1.0-v1.8 (9) + v2.0-v2.7 (8). The Standard skipped v1.9
- // entirely so there is no Version19 constant.
+ // 17 expected: v1.0-v1.8 (9) + v2.0-v2.7 (8). The Standard skipped
+ // v1.9 entirely so there is no Version19 constant.
Assert.That(versions.Count, Is.EqualTo(17),
"Expected 17 version constants (v1.0-v1.8 plus v2.0-v2.7). Got " +
string.Join(", ", versions.Select(x => x.Name)));
diff --git a/tests/MTConnect.NET-Common-Tests/Observations/SampleObservationTests.cs b/tests/MTConnect.NET-Common-Tests/Observations/SampleObservationTests.cs
new file mode 100644
index 000000000..55cf8b2d4
--- /dev/null
+++ b/tests/MTConnect.NET-Common-Tests/Observations/SampleObservationTests.cs
@@ -0,0 +1,76 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using System;
+using MTConnect.Devices;
+using MTConnect.Devices.DataItems;
+using MTConnect.Observations;
+using MTConnect.Tests.Common.TestHelpers;
+using NUnit.Framework;
+
+namespace MTConnect.NET_Common_Tests.Observations
+{
+ // Version-gated Sample-envelope round-trip assertions for the
+ // SAMPLE-category DataItems introduced across MTConnect Standard
+ // versions (currently WaterHardness at v2.7).
+ //
+ // - XMI: https://github.com/mtconnect/mtconnect_sysml_model @ tag v2.7
+ // UML class `WaterHardnessDataItem` declares
+ // `category = SAMPLE`, MinimumVersion = v2.7. (Hardness is
+ // measured in mineral content of cooling water — used in
+ // machining workflows where coolant chemistry affects tool
+ // life.)
+ // - XSD: https://schemas.mtconnect.org/schemas/MTConnectStreams_2.7.xsd
+ // enum `SampleEnum` value `WATER_HARDNESS` is the
+ // sample-category element name on the wire.
+ // - Prose: MTConnect Standard Part_2.0_Streams_v2.7 section 11
+ // "Sample observation types" — describes how SAMPLE-category
+ // observations carry continuous-numeric values reported at
+ // agent-defined intervals.
+ //
+ // Every fixture below is matrix-parameterised over
+ // MTConnectVersionMatrix.All per plan Design Decision D1
+ // (2026-08-19). Assume.That gates each row to versions where the
+ // sample type shipped.
+ /// Pins the behaviour expressed by the test name: sample observation tests.
+ [TestFixture]
+ public class SampleObservationTests
+ {
+ // Source: XMI v2.7 — `WaterHardness` is the only SAMPLE-category
+ // type introduced in v2.7 (the rest are EVENT). Tests the
+ // round-trip from creating a DataItem of this v2.7 type, attaching
+ // a SampleValueObservation, and reading back the value. If the
+ // library starts dropping the link between the DataItem's TypeId
+ // and the observation's reported type, this test catches it.
+ /// Pins the behaviour expressed by the test name: water hardness sample observation round trip.
+ /// The MTConnect Standard version under test.
+ [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))]
+ public void WaterHardness_sample_observation_round_trip(Version v)
+ {
+ Assume.That(v, Is.GreaterThanOrEqualTo(MTConnectVersions.Version27),
+ "WaterHardness was introduced in MTConnect v2.7.");
+
+ var dataItem = new WaterHardnessDataItem("dev01");
+ Assert.That(dataItem.Category, Is.EqualTo(DataItemCategory.SAMPLE));
+
+ var observation = new SampleValueObservation
+ {
+ DataItemId = dataItem.Id,
+ Result = "12.5",
+ Timestamp = System.DateTime.UtcNow,
+ Sequence = 42,
+ };
+
+ // Carrier preserves DataItemId so a downstream lookup of the
+ // type (DataItemId -> TypeId via the agent's DataItem registry)
+ // resolves back to WATER_HARDNESS.
+ Assert.That(observation.DataItemId, Is.EqualTo(dataItem.Id));
+ Assert.That(observation.Result, Is.EqualTo("12.5"));
+ Assert.That(observation.Sequence, Is.EqualTo(42));
+
+ // The DataItem's Type field is what cppagent JSON / XML
+ // formatters look at when rendering the SAMPLE element name.
+ Assert.That(dataItem.Type, Is.EqualTo("WATER_HARDNESS"));
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-Common-Tests/Reflection/RegeneratedTypesCoverageTests.cs b/tests/MTConnect.NET-Common-Tests/Reflection/RegeneratedTypesCoverageTests.cs
index 7c26b1c78..57eff0392 100644
--- a/tests/MTConnect.NET-Common-Tests/Reflection/RegeneratedTypesCoverageTests.cs
+++ b/tests/MTConnect.NET-Common-Tests/Reflection/RegeneratedTypesCoverageTests.cs
@@ -215,8 +215,9 @@ public void Type_can_be_constructed(Type type)
// Every public regenerated type's default ctor must execute at
// least once so it counts as covered. This single parametric
// case satisfies that for the class-with-bare-ctor case; ctors
- // with arguments are covered by the typed fixtures under
- // V2_6_V2_7/.
+ // with arguments are covered by the topic-first fixtures under
+ // Devices/DataItems/, Devices/Components/, Devices/Configurations/,
+ // Observations/, and Enums/ (Phase 1 DRY-generator consolidation).
object? instance = null;
Assert.DoesNotThrow(
() => instance = Activator.CreateInstance(type),
@@ -240,8 +241,9 @@ public void Type_round_trips_default_property_values(Type type)
//
// Properties without a public setter (read-only computed
// properties such as Id) are skipped — the spec contract for
- // those is "derived from other state", and the V2_6_V2_7
- // hand-written fixtures pin their semantics.
+ // those is "derived from other state", and the topic-first
+ // fixtures under Devices/DataItems/, Devices/Components/,
+ // Devices/Configurations/, and Observations/ pin their semantics.
var instance = Activator.CreateInstance(type)!;
foreach (var property in GetRoundTrippableProperties(type))
diff --git a/tests/MTConnect.NET-Common-Tests/V2_6_V2_7/V2_6ComponentAndEnumTests.cs b/tests/MTConnect.NET-Common-Tests/V2_6_V2_7/V2_6ComponentAndEnumTests.cs
deleted file mode 100644
index bcecc8dab..000000000
--- a/tests/MTConnect.NET-Common-Tests/V2_6_V2_7/V2_6ComponentAndEnumTests.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using System;
-using MTConnect.Devices.Components;
-using MTConnect.Devices.Configurations;
-using NUnit.Framework;
-
-namespace MTConnect.NET_Common_Tests.V2_6_V2_7
-{
- // Pins the non-DataItem v2.6 surface: new Component subclasses + the
- // MediaType enum's QIF_MBD addition.
- //
- // - XMI: mtconnect/mtconnect_sysml_model @ v2.6 (SHA 08185447bf86…)
- // * UML class `CuttingTorch` — Component Types package
- // * UML class `Electrode` — Component Types package
- // * Enum `MediaTypeEnum` value `QIF_MBD`
- // - XSD: schemas.mtconnect.org/schemas/MTConnectDevices_2.6.xsd
- // (Component element list + MediaType simpleType enumeration)
- // - Prose: MTConnect Standard Part_3.0_Devices_v2.6
- // section 3.4.18 "CuttingTorch" / section 3.4.21 "Electrode"
- // section 4.7.2.5 MediaType (introduces QIF_MBD)
- /// Pins the behaviour expressed by the test name: v2 6 component and enum tests.
- [TestFixture]
- public class V2_6ComponentAndEnumTests
- {
- // Source: XMI v2.6 UML `CuttingTorch` (Component Types); XSD v2.6
- // ``.
- /// Pins the behaviour expressed by the test name: cutting torch component constructs with correct type.
- [Test]
- public void CuttingTorchComponent_constructs_with_correct_type()
- {
- var c = new CuttingTorchComponent();
- Assert.That(c.Type, Is.EqualTo("CuttingTorch"));
- Assert.That(c.Name, Is.Null);
- Assert.That(CuttingTorchComponent.TypeId, Is.EqualTo("CuttingTorch"));
- Assert.That(CuttingTorchComponent.NameId, Is.EqualTo("cuttingTorch"));
- }
-
- // Source: XMI v2.6 UML `Electrode` (Component Types); XSD v2.6
- // ``.
- /// Pins the behaviour expressed by the test name: electrode component constructs with correct type.
- [Test]
- public void ElectrodeComponent_constructs_with_correct_type()
- {
- var c = new ElectrodeComponent();
- Assert.That(c.Type, Is.EqualTo("Electrode"));
- Assert.That(c.Name, Is.Null);
- Assert.That(ElectrodeComponent.TypeId, Is.EqualTo("Electrode"));
- Assert.That(ElectrodeComponent.NameId, Is.EqualTo("electrode"));
- }
-
- // Source: XMI v2.6 enum `MediaTypeEnum` member `QIF_MBD`. XSD v2.6 lists
- // QIF_MBD inside the MediaType simpleType enumeration. Prose
- // Part_3.0_Devices_v2.6 section 4.7.2.5 introduces "ISO 10303 QIF model-based
- // design" as the rationale.
- /// Pins the behaviour expressed by the test name: media type q i f m b d value present in v2 6.
- [Test]
- public void MediaType_QIF_MBD_value_present_in_v2_6()
- {
- Assert.That(Enum.IsDefined(typeof(MediaType), "QIF_MBD"), Is.True);
- }
- }
-}
diff --git a/tests/MTConnect.NET-Common-Tests/V2_6_V2_7/V2_6DataItemTypeTests.cs b/tests/MTConnect.NET-Common-Tests/V2_6_V2_7/V2_6DataItemTypeTests.cs
deleted file mode 100644
index 1919e9d1d..000000000
--- a/tests/MTConnect.NET-Common-Tests/V2_6_V2_7/V2_6DataItemTypeTests.cs
+++ /dev/null
@@ -1,94 +0,0 @@
-using System;
-using MTConnect.Devices;
-using MTConnect.Devices.DataItems;
-using NUnit.Framework;
-
-namespace MTConnect.NET_Common_Tests.V2_6_V2_7
-{
- // Pins every DataItem type the v2.6 SysML XMI introduces.
- //
- // - XMI: mtconnect/mtconnect_sysml_model @ v2.6 (SHA 08185447bf86…)
- // UML classes:
- // * `AssetAddedDataItem` — xmi:id _2024x_68e0225_1744799118784_270323_23376
- // * `AssociatedAssetIdDataItem` — xmi:id _2024x_68e0225_1744800465544_…
- // * `AssetChangedDataItem` — description rewritten in v2.6
- // - XSD: schemas.mtconnect.org/schemas/MTConnectStreams_2.6.xsd
- // (the EVENT category for both new types is encoded in the
- // MTConnectStreams XSD's enumerations.)
- // - Prose: MTConnect Standard Part_2.0_Streams_v2.6 section 11.5 "Asset events"
- // (clarifies the v2.5 → v2.6 split — `AssetChanged` narrowed to
- // changes only; `AssetAdded` introduced for additions.)
- /// Pins the behaviour expressed by the test name: v2 6 data item type tests.
- [TestFixture]
- public class V2_6DataItemTypeTests
- {
- // Source: XMI v2.6 UML class `AssetAddedDataItem`; XSD v2.6 enum `EventEnum`
- // value `ASSET_ADDED`.
- /// Pins the behaviour expressed by the test name: asset added data item constructs with event metadata.
- [Test]
- public void AssetAddedDataItem_constructs_with_event_metadata()
- {
- var d = new AssetAddedDataItem();
- Assert.That(d.Type, Is.EqualTo("ASSET_ADDED"));
- Assert.That(d.Name, Is.EqualTo("assetAdded"));
- Assert.That(d.Category, Is.EqualTo(DataItemCategory.EVENT));
- Assert.That(AssetAddedDataItem.TypeId, Is.EqualTo("ASSET_ADDED"));
- Assert.That(AssetAddedDataItem.NameId, Is.EqualTo("assetAdded"));
- Assert.That(AssetAddedDataItem.CategoryId, Is.EqualTo(DataItemCategory.EVENT));
- }
-
- // Source: XMI v2.6 — `DataItem.id` formation rule via parent device.
- /// Pins the behaviour expressed by the test name: asset added data item with device id produces qualified id.
- [Test]
- public void AssetAddedDataItem_with_deviceId_produces_qualified_id()
- {
- var d = new AssetAddedDataItem("dev01");
- Assert.That(d.Id, Is.Not.Null.And.Not.Empty);
- Assert.That(d.Id, Does.Contain("dev01"));
- Assert.That(d.Type, Is.EqualTo("ASSET_ADDED"));
- }
-
- // Source: XMI v2.6 UML class `AssociatedAssetIdDataItem`; XSD v2.6
- // EventEnum value `ASSOCIATED_ASSET_ID`.
- /// Pins the behaviour expressed by the test name: associated asset id data item constructs with event metadata.
- [Test]
- public void AssociatedAssetIdDataItem_constructs_with_event_metadata()
- {
- var d = new AssociatedAssetIdDataItem();
- Assert.That(d.Type, Is.EqualTo(AssociatedAssetIdDataItem.TypeId));
- Assert.That(d.Name, Is.EqualTo(AssociatedAssetIdDataItem.NameId));
- Assert.That(d.Category, Is.EqualTo(AssociatedAssetIdDataItem.CategoryId));
- Assert.That(AssociatedAssetIdDataItem.TypeId, Is.EqualTo("ASSOCIATED_ASSET_ID"));
- Assert.That(AssociatedAssetIdDataItem.CategoryId, Is.EqualTo(DataItemCategory.EVENT));
- }
-
- // Source: XMI v2.6 — generalization of `AssetAddedDataItem` is `DataItem`.
- /// Pins the behaviour expressed by the test name: asset added data item inherits from data item.
- [Test]
- public void AssetAddedDataItem_inherits_from_DataItem()
- {
- Assert.That(typeof(AssetAddedDataItem).BaseType, Is.EqualTo(typeof(DataItem)));
- }
-
- // Source: XMI v2.6 — generalization of `AssociatedAssetIdDataItem` is `DataItem`.
- /// Pins the behaviour expressed by the test name: associated asset id data item inherits from data item.
- [Test]
- public void AssociatedAssetIdDataItem_inherits_from_DataItem()
- {
- Assert.That(typeof(AssociatedAssetIdDataItem).BaseType, Is.EqualTo(typeof(DataItem)));
- }
-
- // Source: XMI v2.6 description on `AssetChangedDataItem` (was "added or
- // changed" in v2.5; now "changed" only). Prose confirms in
- // Part_2.0_Streams_v2.6 section 11.5.
- /// Pins the behaviour expressed by the test name: asset changed data item description narrowed in v2 6.
- [Test]
- public void AssetChangedDataItem_description_narrowed_in_v2_6()
- {
- Assert.That(AssetChangedDataItem.DescriptionText,
- Is.EqualTo("AssetId of the Asset that has been changed."),
- "AssetChangedDataItem description must reflect the v2.6 split " +
- "where 'added' moved to AssetAddedDataItem");
- }
- }
-}
diff --git a/tests/MTConnect.NET-Common-Tests/V2_6_V2_7/V2_7ComponentTests.cs b/tests/MTConnect.NET-Common-Tests/V2_6_V2_7/V2_7ComponentTests.cs
deleted file mode 100644
index 1e61a0008..000000000
--- a/tests/MTConnect.NET-Common-Tests/V2_6_V2_7/V2_7ComponentTests.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using MTConnect.Devices.Components;
-using NUnit.Framework;
-
-namespace MTConnect.NET_Common_Tests.V2_6_V2_7
-{
- // Pins the v2.7 Component subclasses (PinTool, ToolHolder).
- //
- // - XMI: https://github.com/mtconnect/mtconnect_sysml_model @ tag v2.7
- // UML classes under Device Information Model > Components:
- // * PinTool — pin-style tooling component
- // * ToolHolder — tool-holder component
- // - XSD: https://schemas.mtconnect.org/schemas/MTConnectDevices_2.7.xsd
- // (each TypeId appears in the ComponentType enumeration).
- // - Prose: MTConnect Standard Part_2.0_Devices_v2.7 section 7 "Component
- // types" — describes intended use of each Component subclass.
- /// Pins the behaviour expressed by the test name: v2 7 component tests.
- [TestFixture]
- public class V2_7ComponentTests
- {
- /// Pins the behaviour expressed by the test name: pin tool component constructs with correct type.
- [Test]
- public void PinToolComponent_constructs_with_correct_type()
- {
- var c = new PinToolComponent();
- Assert.That(c.Type, Is.EqualTo("PinTool"));
- Assert.That(c.Name, Is.Null);
- Assert.That(PinToolComponent.TypeId, Is.EqualTo("PinTool"));
- Assert.That(PinToolComponent.NameId, Is.EqualTo("pinTool"));
- }
-
- /// Pins the behaviour expressed by the test name: tool holder component constructs with correct type.
- [Test]
- public void ToolHolderComponent_constructs_with_correct_type()
- {
- var c = new ToolHolderComponent();
- Assert.That(c.Type, Is.EqualTo("ToolHolder"));
- Assert.That(c.Name, Is.Null);
- Assert.That(ToolHolderComponent.TypeId, Is.EqualTo("ToolHolder"));
- Assert.That(ToolHolderComponent.NameId, Is.EqualTo("toolHolder"));
- }
- }
-}
diff --git a/tests/MTConnect.NET-Common-Tests/V2_6_V2_7/V2_7ConfigurationDataSetTests.cs b/tests/MTConnect.NET-Common-Tests/V2_6_V2_7/V2_7ConfigurationDataSetTests.cs
deleted file mode 100644
index 10503fa66..000000000
--- a/tests/MTConnect.NET-Common-Tests/V2_6_V2_7/V2_7ConfigurationDataSetTests.cs
+++ /dev/null
@@ -1,175 +0,0 @@
-using MTConnect.Devices.Configurations;
-using NUnit.Framework;
-
-namespace MTConnect.NET_Common_Tests.V2_6_V2_7
-{
- // Pins the v2.7 Configuration sub-element family: new geometric primitives
- // (Axis, Origin, Rotation, Scale, Translation) and their data-set
- // representation siblings (*DataSet) — plus the cross-package-grafted
- // DataSet base that the universal cross-package parent resolver brought
- // into the Devices.Configurations namespace.
- //
- // - XMI: https://github.com/mtconnect/mtconnect_sysml_model @ tag v2.7
- // UML classes under Device Information Model > Configurations:
- // * Axis / AxisDataSet
- // * Origin / OriginDataSet
- // * Rotation / RotationDataSet
- // * Scale / ScaleDataSet
- // * Translation / TranslationDataSet
- // plus the abstract bases (AbstractAxis, AbstractOrigin, etc.).
- // - XSD: https://schemas.mtconnect.org/schemas/MTConnectDevices_2.7.xsd
- // (the geometric-primitive complexTypes encode the same shape
- // on the wire under ).
- // - Prose: MTConnect Standard Part_2.0_Devices_v2.7 section 10 "Configuration"
- // — describes how Component-level Configuration carries the
- // geometric primitives that locate a Component in space.
- /// Pins the behaviour expressed by the test name: v2 7 configuration data set tests.
- [TestFixture]
- public class V2_7ConfigurationDataSetTests
- {
- // The DataSet base (grafted from Observation.Representations via the
- // universal resolver) compiles, instantiates, and surfaces its
- // const description.
- /// Pins the behaviour expressed by the test name: data set base constructs and implements i data set.
- [Test]
- public void DataSet_base_constructs_and_implements_IDataSet()
- {
- var ds = new DataSet();
- Assert.That(ds, Is.InstanceOf());
- Assert.That(DataSet.DescriptionText, Is.Not.Null.And.Not.Empty);
- }
-
- // The five concrete sub-types follow the same shape: parameterless ctor,
- // populates X/Y/Z (or A/B/C) fields, implements IDataSet (interface,
- // not the concrete DataSet base — *DataSet types polymorphically
- // extend their Abstract base, gaining IDataSet as a marker
- // interface so XML/JSON serialisers can narrow on it).
- /// Pins the behaviour expressed by the test name: axis data set has xyz fields and implements i data set.
- [Test]
- public void AxisDataSet_has_xyz_fields_and_implements_IDataSet()
- {
- var a = new AxisDataSet { X = 1.0, Y = 2.0, Z = 3.0 };
- Assert.That(a, Is.InstanceOf());
- Assert.That(a, Is.InstanceOf());
- Assert.That(a.X, Is.EqualTo(1.0));
- Assert.That(a.Y, Is.EqualTo(2.0));
- Assert.That(a.Z, Is.EqualTo(3.0));
- }
-
- /// Pins the behaviour expressed by the test name: origin data set has xyz fields and implements i data set.
- [Test]
- public void OriginDataSet_has_xyz_fields_and_implements_IDataSet()
- {
- var o = new OriginDataSet { X = "1", Y = "2", Z = "3" };
- Assert.That(o, Is.InstanceOf());
- Assert.That(o, Is.InstanceOf());
- }
-
- /// Pins the behaviour expressed by the test name: rotation data set has abc fields and implements i data set.
- [Test]
- public void RotationDataSet_has_abc_fields_and_implements_IDataSet()
- {
- // Rotations are reported as A (about X), B (about Y), C (about Z).
- var r = new RotationDataSet { A = "10", B = "20", C = "30" };
- Assert.That(r, Is.InstanceOf());
- Assert.That(r, Is.InstanceOf());
- }
-
- /// Pins the behaviour expressed by the test name: scale data set implements i data set.
- [Test]
- public void ScaleDataSet_implements_IDataSet()
- {
- var s = new ScaleDataSet();
- Assert.That(s, Is.InstanceOf());
- Assert.That(s, Is.InstanceOf());
- }
-
- /// Pins the behaviour expressed by the test name: translation data set implements i data set.
- [Test]
- public void TranslationDataSet_implements_IDataSet()
- {
- var t = new TranslationDataSet();
- Assert.That(t, Is.InstanceOf());
- Assert.That(t, Is.InstanceOf());
- }
-
- // Concrete (non-DataSet) representations of the same primitives, also
- // landed in v2.7 alongside their DataSet siblings.
- /// Pins the behaviour expressed by the test name: axis inherits abstract axis and constructs.
- [Test]
- public void Axis_inherits_AbstractAxis_and_constructs()
- {
- var a = new Axis { Value = "X" };
- Assert.That(a, Is.InstanceOf());
- Assert.That(a, Is.InstanceOf());
- Assert.That(a.Value, Is.EqualTo("X"));
- }
-
- /// Pins the behaviour expressed by the test name: origin inherits abstract origin.
- [Test]
- public void Origin_inherits_AbstractOrigin()
- {
- var o = new Origin();
- Assert.That(o, Is.InstanceOf());
- Assert.That(o, Is.InstanceOf());
- }
-
- /// Pins the behaviour expressed by the test name: rotation inherits abstract rotation.
- [Test]
- public void Rotation_inherits_AbstractRotation()
- {
- Assert.That(new Rotation(), Is.InstanceOf());
- }
-
- /// Pins the behaviour expressed by the test name: scale inherits abstract scale.
- [Test]
- public void Scale_inherits_AbstractScale()
- {
- Assert.That(new Scale(), Is.InstanceOf());
- }
-
- /// Pins the behaviour expressed by the test name: translation inherits abstract translation.
- [Test]
- public void Translation_inherits_AbstractTranslation()
- {
- Assert.That(new Translation(), Is.InstanceOf());
- }
-
- // The Abstract* bases are abstract — verify so a future regen that
- // accidentally drops the abstract modifier trips here.
- /// Pins the behaviour expressed by the test name: abstract axis is abstract.
- [Test]
- public void AbstractAxis_is_abstract()
- {
- Assert.That(typeof(AbstractAxis).IsAbstract, Is.True);
- }
-
- /// Pins the behaviour expressed by the test name: abstract origin is abstract.
- [Test]
- public void AbstractOrigin_is_abstract()
- {
- Assert.That(typeof(AbstractOrigin).IsAbstract, Is.True);
- }
-
- /// Pins the behaviour expressed by the test name: abstract rotation is abstract.
- [Test]
- public void AbstractRotation_is_abstract()
- {
- Assert.That(typeof(AbstractRotation).IsAbstract, Is.True);
- }
-
- /// Pins the behaviour expressed by the test name: abstract scale is abstract.
- [Test]
- public void AbstractScale_is_abstract()
- {
- Assert.That(typeof(AbstractScale).IsAbstract, Is.True);
- }
-
- /// Pins the behaviour expressed by the test name: abstract translation is abstract.
- [Test]
- public void AbstractTranslation_is_abstract()
- {
- Assert.That(typeof(AbstractTranslation).IsAbstract, Is.True);
- }
- }
-}
diff --git a/tests/MTConnect.NET-Common-Tests/V2_6_V2_7/V2_7DataItemTypeTests.cs b/tests/MTConnect.NET-Common-Tests/V2_6_V2_7/V2_7DataItemTypeTests.cs
deleted file mode 100644
index 0faef2fc1..000000000
--- a/tests/MTConnect.NET-Common-Tests/V2_6_V2_7/V2_7DataItemTypeTests.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-using System;
-using MTConnect.Devices;
-using MTConnect.Devices.DataItems;
-using NUnit.Framework;
-
-namespace MTConnect.NET_Common_Tests.V2_6_V2_7
-{
- // Pins every DataItem type the v2.7 SysML XMI introduces.
- //
- // - XMI: mtconnect/mtconnect_sysml_model @ v2.7 (SHA 25796ac591bb…)
- // UML classes under Observation Information Model > Observation Types:
- // * BindingState (Event) — Bonding/joining state
- // * Depth (Event) — Tool / part penetration
- // * FixtureAssetId (Event) — Asset reference
- // * SwingAngle (Event) — Mill/lathe swing
- // * SwingDiameter (Event) — Mill/lathe swing
- // * SwingRadius (Event) — Mill/lathe swing
- // * TaskAssetId (Event) — Asset reference
- // * WaterHardness (Sample) — Coolant water mineral level
- // - XSD: schemas.mtconnect.org/schemas/MTConnectStreams_2.7.xsd
- // (each TypeId is encoded in the EventEnum / SampleEnum
- // enumerations.)
- // - Prose: MTConnect Standard Part_2.0_Streams_v2.7 section 11/section 13 "Event/Sample
- // types" — describes intended use of each type.
- /// Pins the behaviour expressed by the test name: v2 7 data item type tests.
- [TestFixture]
- public class V2_7DataItemTypeTests
- {
- // Categories below match what the v2.7 SysML XMI declares — the spec
- // authority. Several types that look "measurement-y" (SwingAngle, Depth,
- // etc.) are EVENT in the spec rather than SAMPLE; locking them so a
- // future regen drift is caught immediately.
- /// Pins the behaviour expressed by the test name: v2 7 data item constructs with correct metadata.
- /// The data item type.
- /// The expected type id.
- /// The expected category.
- [TestCase(typeof(BindingStateDataItem), "BINDING_STATE", DataItemCategory.EVENT)]
- [TestCase(typeof(DepthDataItem), "DEPTH", DataItemCategory.EVENT)]
- [TestCase(typeof(FixtureAssetIdDataItem), "FIXTURE_ASSET_ID", DataItemCategory.EVENT)]
- [TestCase(typeof(SwingAngleDataItem), "SWING_ANGLE", DataItemCategory.EVENT)]
- [TestCase(typeof(SwingDiameterDataItem), "SWING_DIAMETER", DataItemCategory.EVENT)]
- [TestCase(typeof(SwingRadiusDataItem), "SWING_RADIUS", DataItemCategory.EVENT)]
- [TestCase(typeof(TaskAssetIdDataItem), "TASK_ASSET_ID", DataItemCategory.EVENT)]
- [TestCase(typeof(WaterHardnessDataItem), "WATER_HARDNESS", DataItemCategory.SAMPLE)]
- public void V2_7_DataItem_constructs_with_correct_metadata(
- Type dataItemType, string expectedTypeId, DataItemCategory expectedCategory)
- {
- // Wrap with Assert.DoesNotThrow so a missing parameterless ctor
- // surfaces as a clear NUnit failure with the offending type name
- // rather than a bare MissingMethodException.
- object? instance = null;
- Assert.DoesNotThrow(() => instance = Activator.CreateInstance(dataItemType),
- $"{dataItemType.Name} should have a public parameterless constructor");
- Assert.That(instance, Is.Not.Null);
- Assert.That(instance, Is.InstanceOf());
-
- var di = (DataItem)instance!;
- Assert.That(di.Type, Is.EqualTo(expectedTypeId),
- $"{dataItemType.Name}.Type should be the spec TypeId");
- Assert.That(di.Category, Is.EqualTo(expectedCategory),
- $"{dataItemType.Name}.Category should be {expectedCategory}");
-
- var typeIdConst = dataItemType.GetField("TypeId",
- System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static)?.GetRawConstantValue();
- Assert.That(typeIdConst, Is.EqualTo(expectedTypeId),
- $"{dataItemType.Name}.TypeId static const should match the spec TypeId");
- }
- }
-}
diff --git a/tests/MTConnect.NET-Common-Tests/V2_6_V2_7/V2_7SampleObservationTests.cs b/tests/MTConnect.NET-Common-Tests/V2_6_V2_7/V2_7SampleObservationTests.cs
deleted file mode 100644
index bc906c56b..000000000
--- a/tests/MTConnect.NET-Common-Tests/V2_6_V2_7/V2_7SampleObservationTests.cs
+++ /dev/null
@@ -1,66 +0,0 @@
-using MTConnect.Devices;
-using MTConnect.Devices.DataItems;
-using MTConnect.Observations;
-using NUnit.Framework;
-
-namespace MTConnect.NET_Common_Tests.V2_6_V2_7
-{
- // Sample envelope coverage for the v2.7 SAMPLE-category DataItems introduced
- // by [#133](https://github.com/TrakHound/MTConnect.NET/issues/133).
- //
- // - XMI: mtconnect/mtconnect_sysml_model @ v2.7 (SHA 25796ac591bb…)
- // UML class `WaterHardnessDataItem` declares
- // `category = SAMPLE`, MinimumVersion = v2.7. (Hardness measured in
- // mineral content of cooling water — used in machining workflows
- // where coolant chemistry affects tool life.)
- // - XSD: schemas.mtconnect.org/schemas/MTConnectStreams_2.7.xsd
- // enum `SampleEnum` value `WATER_HARDNESS` is the sample-category
- // element name on the wire.
- // - Prose: MTConnect Standard Part_2.0_Streams_v2.7 section 11 "Sample observation
- // types" — describes how SAMPLE-category observations carry
- // continuous-numeric values reported at agent-defined intervals.
- //
- // This fixture is the SAMPLE-envelope counterpart to V2_7DataItemTypeTests
- // (which is shape-only). Here we focus on round-tripping a SAMPLE
- // observation through the library's `SampleValueObservation` carrier and
- // confirm the (DataItem, Observation) pair carries the v2.7 type metadata
- // intact.
- /// Pins the behaviour expressed by the test name: v2 7 sample observation tests.
- [TestFixture]
- public class V2_7SampleObservationTests
- {
- // Source: XMI v2.7 — `WaterHardness` is the only SAMPLE-category type
- // introduced in v2.7 (the rest are EVENT). Tests the round-trip from
- // creating a DataItem of this v2.7 type, attaching a SampleValueObservation,
- // and reading back the value. If the library starts dropping the link
- // between the DataItem's TypeId and the observation's reported type,
- // this test catches it.
- /// Pins the behaviour expressed by the test name: water hardness sample observation round trip.
- [Test]
- public void WaterHardness_sample_observation_round_trip()
- {
- var dataItem = new WaterHardnessDataItem("dev01");
- Assert.That(dataItem.Category, Is.EqualTo(DataItemCategory.SAMPLE));
-
- var observation = new SampleValueObservation
- {
- DataItemId = dataItem.Id,
- Result = "12.5",
- Timestamp = System.DateTime.UtcNow,
- Sequence = 42,
- };
-
- // Carrier preserves DataItemId so a downstream lookup of the type
- // (DataItemId → TypeId via the agent's DataItem registry) resolves
- // back to WATER_HARDNESS.
- Assert.That(observation.DataItemId, Is.EqualTo(dataItem.Id));
- Assert.That(observation.Result, Is.EqualTo("12.5"));
- Assert.That(observation.Sequence, Is.EqualTo(42));
-
- // The DataItem's Type field is what cppagent JSON / XML formatters
- // look at when rendering the SAMPLE element name.
- Assert.That(dataItem.Type, Is.EqualTo("WATER_HARDNESS"));
- }
-
- }
-}
diff --git a/tests/MTConnect.NET-Docs-Tests/DocsReferenceGenerationTests.cs b/tests/MTConnect.NET-Docs-Tests/DocsReferenceGenerationTests.cs
index 922a42391..12e48485e 100644
--- a/tests/MTConnect.NET-Docs-Tests/DocsReferenceGenerationTests.cs
+++ b/tests/MTConnect.NET-Docs-Tests/DocsReferenceGenerationTests.cs
@@ -124,6 +124,39 @@ public void Cli_Page_Is_In_Sync_With_Source()
}
}
+ ///
+ /// Direct pin for the cycle-4 DocsGen bounded-scan fix
+ /// (CliInventory.CollectDotNetTool ): the takesValue
+ /// regex must be bounded to the CURRENT case block, otherwise
+ /// a boolean switch flag sitting above a value-taking neighbour
+ /// (e.g. --full-tree above case "--output": … RequireValue )
+ /// would falsely inherit the neighbour's <value> shape.
+ ///
+ ///
+ /// The golden-file Cli_Page_Is_In_Sync_With_Source test would
+ /// also catch this via the rendered cli.md , but a targeted
+ /// unit-style pin here surfaces the regression with a branch-scoped
+ /// failure message before the golden-file diff is even computed.
+ ///
+ ///
+ [Test]
+ public void SysMLImport_FullTree_Flag_Is_Detected_As_Switch_Not_Value_Flag()
+ {
+ var clis = CliInventory.Collect(RepoRoot);
+ var sysml = clis.FirstOrDefault(c => c.Name == "MTConnect.NET-SysML-Import");
+ Assert.That(sysml, Is.Not.Null,
+ "MTConnect.NET-SysML-Import must be discovered in the inventory.");
+
+ var fullTree = sysml!.Flags.FirstOrDefault(f => f.Name == "--full-tree");
+ Assert.That(fullTree, Is.Not.Null,
+ "--full-tree flag must appear in the sysml-import inventory.");
+ Assert.That(fullTree!.ArgShape, Is.Null,
+ "--full-tree is a boolean switch (case body: `fullTree = true; break;`). "
+ + "The bounded RequireValue scan must NOT leak in the value shape from the "
+ + "neighbouring --output / --json-dump cases. An ArgShape of `` here "
+ + "means the bounded-scan regex regressed to an unbounded lookahead.");
+ }
+
/// Pins the behaviour expressed by the test name: endpoint code has no stale entries in markdown.
[Test]
public void Endpoint_Code_Has_No_Stale_Entries_In_Markdown()
diff --git a/tests/MTConnect.NET-Generator-Tests/AutoDerivePreviousXmiTests.cs b/tests/MTConnect.NET-Generator-Tests/AutoDerivePreviousXmiTests.cs
new file mode 100644
index 000000000..433cc391e
--- /dev/null
+++ b/tests/MTConnect.NET-Generator-Tests/AutoDerivePreviousXmiTests.cs
@@ -0,0 +1,856 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using NUnit.Framework;
+
+namespace MTConnect.NET_Generator_Tests
+{
+ ///
+ /// Zero-config PREV_VERSION auto-derive coverage for the SysML importer
+ /// (task #408 amendment to PR #233 Phase 4).
+ ///
+ ///
+ /// When neither --previous-xmi nor --full-tree is supplied,
+ /// the importer parses MTConnectVersions.Max from
+ /// libraries/MTConnect.NET-Common/MTConnectVersions.cs under
+ /// --output and resolves the prior-version XMI in this priority
+ /// order:
+ ///
+ /// -
+ /// Strategy B (primary):
+ ///
build/.cache/sysml-prev/MTConnectSysMLModel_v${PREV_VERSION}.xml .
+ ///
+ /// -
+ /// Strategy A (fallback):
+ ///
build/sysml-model/MTConnectSysMLModel.xml , gated on
+ /// git -C build/sysml-model describe --exact-match --tags HEAD
+ /// returning v${PREV_VERSION} exactly.
+ ///
+ /// -
+ /// Strategy C (fail-hard): throw with an actionable message when
+ /// neither resolves.
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Every case here is exercised end-to-end via dotnet run --no-build
+ /// --project build/MTConnect.NET-SysML-Import against a synthetic
+ /// --output scratch tree that mimics the repo layout so the
+ /// auto-derive resolver sees a controlled world: a pinned
+ /// MTConnectVersions.cs , a curated cache directory, and (where
+ /// needed) a synthetic git-tagged build/sysml-model . The
+ /// assertions bind to the CLI contract, not to any internal helper.
+ ///
+ ///
+ [TestFixture]
+ public class AutoDerivePreviousXmiTests
+ {
+ private const string SlnFileName = "MTConnect.NET.sln";
+ private const string GeneratorProject = "build/MTConnect.NET-SysML-Import";
+ private const string RealXmiRelativePath = "build/sysml-model/MTConnectSysMLModel.xml";
+ private const string ScratchRoot = ".claude/gen-test-out/auto-derive";
+
+ // A minimal MTConnectVersions.cs skeleton — enough for the auto-derive
+ // regex to lock onto `public static Version Max => VersionXY;` and
+ // `public static readonly Version VersionXY = new Version(X, Y);`. The
+ // constants below cover the Max we pin the tests against; the
+ // `Version27` constant matches the current-tree Max at #233 landing
+ // so the tests stay in step with the shipped fixture XMI.
+ private const string SyntheticVersionsCs = @"// Copyright (c) 2026 TrakHound Inc.
+
+using System;
+
+namespace MTConnect
+{
+ public static class MTConnectVersions
+ {
+ public static Version Max => Version27;
+
+ public static readonly Version Version26 = new Version(2, 6);
+ public static readonly Version Version27 = new Version(2, 7);
+ }
+}
+";
+
+ [Test]
+ public void Auto_derive_from_MTConnectVersionsMax_uses_cache_when_present()
+ {
+ var repoRoot = FindRepoRoot();
+ var realXmi = Path.Combine(repoRoot, RealXmiRelativePath);
+ Assert.That(File.Exists(realXmi), Is.True,
+ $"XMI snapshot missing at {realXmi}. Is the build/sysml-model submodule initialised?");
+
+ var scratch = InitScratchRepoLayout("cache-primary");
+ WriteSyntheticVersionsCs(scratch);
+
+ // Strategy B setup: populate the cache path with the current-tree
+ // XMI as a stand-in for the prior-version XMI. Using the same bytes
+ // for --new-xmi and the cache produces a "same XMI on both sides"
+ // delta — every emitted file lands in the UNCHANGED-concentrated
+ // partition, so the stdout stats line is grep-able for
+ // `unchanged-concentrated=N>0` and the Compat file appears at the
+ // expected auto-derived label path.
+ var cacheDir = Path.Combine(scratch, "build", ".cache", "sysml-prev");
+ Directory.CreateDirectory(cacheDir);
+ var cachePath = Path.Combine(cacheDir, "MTConnectSysMLModel_v2.7.xml");
+ File.Copy(realXmi, cachePath);
+
+ var (exitCode, stdout, stderr) = RunAutoDerive(realXmi, scratch);
+
+ Assert.That(exitCode, Is.Zero,
+ $"Auto-derive with cache present should succeed.\nstdout:\n{stdout}\nstderr:\n{stderr}");
+ Assert.That(stdout, Does.Contain("auto-derived from MTConnectVersions.Max"),
+ "stdout must announce that PREV_VERSION was auto-derived so the operator sees which strategy fired.");
+ Assert.That(stdout, Does.Contain("MTConnectSysMLModel_v2.7.xml"),
+ "stdout must echo the resolved cache path so the operator can verify Strategy B ran.");
+ Assert.That(stdout, Does.Contain("Delta emission:"),
+ "Auto-derive must reach the delta emitter, not the full-tree branch.");
+
+ // The auto-derived Compat label is `v2_7` (from Max = Version27),
+ // and same-XMI-on-both-sides forces every file into the UNCHANGED
+ // partition — so exactly one Compat/v2_7.g.cs lands per library.
+ var compatFiles = Directory
+ .EnumerateFiles(scratch, "v2_7.g.cs", SearchOption.AllDirectories)
+ .Select(p => p.Replace('\\', '/'))
+ .Where(p => p.Contains("/Compat/"))
+ .ToList();
+ Assert.That(compatFiles.Count, Is.EqualTo(3),
+ "One auto-labelled Compat file per library (three libraries): "
+ + string.Join(", ", compatFiles));
+ }
+
+ [Test]
+ public void Auto_derive_from_MTConnectVersionsMax_falls_back_to_submodule_tag_when_cache_absent()
+ {
+ var repoRoot = FindRepoRoot();
+ var realXmi = Path.Combine(repoRoot, RealXmiRelativePath);
+ Assert.That(File.Exists(realXmi), Is.True,
+ $"XMI snapshot missing at {realXmi}. Is the build/sysml-model submodule initialised?");
+
+ var scratch = InitScratchRepoLayout("submodule-fallback");
+ WriteSyntheticVersionsCs(scratch);
+ // No cache path populated — Strategy B misses. Strategy A must fire.
+
+ // Strategy A setup: build a synthetic git repo at
+ // /build/sysml-model, drop the XMI in, and tag HEAD as
+ // v2.7 (matching MTConnectVersions.Max in the synthetic
+ // MTConnectVersions.cs). The auto-derive resolver runs
+ // `git -C build/sysml-model describe --exact-match --tags HEAD`
+ // and accepts the tree only when the tag matches exactly.
+ var submoduleDir = Path.Combine(scratch, "build", "sysml-model");
+ Directory.CreateDirectory(submoduleDir);
+ File.Copy(realXmi, Path.Combine(submoduleDir, "MTConnectSysMLModel.xml"));
+ InitGitRepoWithTag(submoduleDir, "v2.7");
+
+ var (exitCode, stdout, stderr) = RunAutoDerive(realXmi, scratch);
+
+ Assert.That(exitCode, Is.Zero,
+ $"Auto-derive with only the submodule-tag path available should succeed.\n"
+ + $"stdout:\n{stdout}\nstderr:\n{stderr}");
+ Assert.That(stdout, Does.Contain("auto-derived from MTConnectVersions.Max"),
+ "stdout must announce the auto-derive.");
+ Assert.That(stdout, Does.Contain(Path.Combine(submoduleDir, "MTConnectSysMLModel.xml")),
+ "stdout must echo the resolved submodule XMI path so the operator can verify Strategy A ran.");
+ Assert.That(stdout, Does.Contain("Delta emission:"),
+ "Strategy A must reach the delta emitter, not the full-tree branch.");
+ }
+
+ [Test]
+ public void Auto_derive_from_MTConnectVersionsMax_fails_hard_when_neither_cache_nor_tag_resolves()
+ {
+ var repoRoot = FindRepoRoot();
+ var realXmi = Path.Combine(repoRoot, RealXmiRelativePath);
+
+ var scratch = InitScratchRepoLayout("fail-hard");
+ WriteSyntheticVersionsCs(scratch);
+ // No cache populated. No submodule directory populated.
+
+ var (exitCode, _, stderr) = RunAutoDerive(realXmi, scratch);
+
+ Assert.That(exitCode, Is.Not.Zero,
+ "Neither Strategy B nor Strategy A resolving must fail the invocation, not silently no-op.");
+ Assert.That(stderr, Does.Contain("PREV_VERSION auto-derivation"),
+ "stderr must name the auto-derive failure class so the operator knows which resolver aborted.");
+ Assert.That(stderr, Does.Contain("MTConnectVersions.Max = 2.7"),
+ "stderr must state the resolved PREV_VERSION so the operator can cross-check the current Max.");
+ Assert.That(stderr, Does.Contain("MTConnectSysMLModel_v2.7.xml"),
+ "stderr must name the probed cache path so the operator can drop the file in.");
+ Assert.That(stderr, Does.Contain("v2.7"),
+ "stderr must name the expected submodule tag so the operator can check the submodule tip.");
+ Assert.That(stderr, Does.Contain("--previous-xmi"),
+ "stderr must direct the operator to the explicit-override flag.");
+ Assert.That(stderr, Does.Contain("--full-tree"),
+ "stderr must direct the operator to the delta-disable escape hatch.");
+ }
+
+ [Test]
+ public void Auto_derived_label_carries_the_auto_derived_suffix_on_stdout()
+ {
+ // Label-lie guard positive branch (F-IMP cycle 4): when the
+ // Compat label is genuinely auto-derived (no explicit
+ // --compat-version-label passed, zero-config prev-XMI resolved),
+ // the stdout `Label:` line must carry the "(auto-derived)"
+ // suffix so the operator sees at a glance which resolution
+ // strategy the CLI took. The `compatLabelIsAutoDerived` bool
+ // in Program.cs is TRUE on this branch.
+ var repoRoot = FindRepoRoot();
+ var realXmi = Path.Combine(repoRoot, RealXmiRelativePath);
+
+ var scratch = InitScratchRepoLayout("label-auto-derived-suffix");
+ WriteSyntheticVersionsCs(scratch);
+ var cacheDir = Path.Combine(scratch, "build", ".cache", "sysml-prev");
+ Directory.CreateDirectory(cacheDir);
+ File.Copy(realXmi, Path.Combine(cacheDir, "MTConnectSysMLModel_v2.7.xml"));
+
+ var (exitCode, stdout, stderr) = RunAutoDerive(realXmi, scratch);
+ Assert.That(exitCode, Is.Zero, $"stdout:\n{stdout}\nstderr:\n{stderr}");
+ Assert.That(stdout, Does.Contain("Label: v2_7 (auto-derived)"),
+ "Auto-derived label must be announced with the \"(auto-derived)\" suffix — "
+ + "positive branch of the compatLabelIsAutoDerived ternary in Program.cs.");
+ }
+
+ [Test]
+ public void Explicit_label_alongside_zero_config_prev_xmi_does_not_get_auto_derived_suffix()
+ {
+ // Label-lie guard negative branch (F-IMP cycle 4): the
+ // operator can pass an explicit --compat-version-label
+ // ALONGSIDE the zero-config prev-XMI path. The explicit label
+ // wins the `??=` default; annotating it "(auto-derived)"
+ // would be a lie. Pre-fix, the stdout unconditionally
+ // suffixed "(auto-derived)" whenever the delta mode
+ // announcement fired without --previous-xmi; the fix
+ // introduced a `compatLabelIsAutoDerived` bool so only the
+ // genuinely auto-derived branch appends the suffix.
+ var repoRoot = FindRepoRoot();
+ var realXmi = Path.Combine(repoRoot, RealXmiRelativePath);
+
+ var scratch = InitScratchRepoLayout("label-explicit-no-suffix");
+ WriteSyntheticVersionsCs(scratch);
+ var cacheDir = Path.Combine(scratch, "build", ".cache", "sysml-prev");
+ Directory.CreateDirectory(cacheDir);
+ File.Copy(realXmi, Path.Combine(cacheDir, "MTConnectSysMLModel_v2.7.xml"));
+
+ const string explicitLabel = "Custom-Release-Label";
+ var (exitCode, stdout, stderr) = RunGenerator(scratch,
+ "--new-xmi", realXmi,
+ "--output", scratch,
+ "--compat-version-label", explicitLabel);
+
+ Assert.That(exitCode, Is.Zero, $"stdout:\n{stdout}\nstderr:\n{stderr}");
+ Assert.That(stdout, Does.Contain($"Label: {explicitLabel}"),
+ "The explicit --compat-version-label must appear on the Label: line.");
+ Assert.That(stdout, Does.Not.Contain($"Label: {explicitLabel} (auto-derived)"),
+ "The explicit label must NOT carry the \"(auto-derived)\" suffix — the "
+ + "operator supplied it themselves, so the suffix would misattribute "
+ + "authorship. This is the negative branch of the compatLabelIsAutoDerived "
+ + "ternary and the direct pin for the cycle-4 label-lie fix.");
+ Assert.That(stdout, Does.Contain("Mode: delta (zero-config)"),
+ "The zero-config delta path must still fire — the auto-derive resolver "
+ + "runs (the cache is resolved), only the label default is bypassed.");
+ }
+
+ [Test]
+ public void Explicit_previous_xmi_wins_over_auto_derive()
+ {
+ var repoRoot = FindRepoRoot();
+ var realXmi = Path.Combine(repoRoot, RealXmiRelativePath);
+
+ var scratch = InitScratchRepoLayout("explicit-wins");
+ WriteSyntheticVersionsCs(scratch);
+
+ // Populate the cache with a MUTATED copy of the XMI. If the
+ // resolver picked the cache (Strategy B) over the explicit
+ // --previous-xmi, the delta would surface CoordinateSystem-shaped
+ // changes (from the mutation) instead of zero-change output. The
+ // explicit --previous-xmi points at the pristine XMI, matching
+ // --new-xmi bit-for-bit, so a correctly-prioritised resolver
+ // produces `changed=0` while a broken one produces `changed>0`.
+ var cacheDir = Path.Combine(scratch, "build", ".cache", "sysml-prev");
+ Directory.CreateDirectory(cacheDir);
+ var cachePath = Path.Combine(cacheDir, "MTConnectSysMLModel_v2.7.xml");
+ var mutated = File.ReadAllText(realXmi)
+ .Replace(
+ "unchangeable coordinate system that has machine zero as its origin.",
+ "OVERRIDE_TEST_MARKER coordinate system that has machine zero as its origin.");
+ File.WriteAllText(cachePath, mutated);
+
+ var (exitCode, stdout, stderr) = RunWithExplicitPrevious(realXmi, previousXmi: realXmi, scratch);
+
+ Assert.That(exitCode, Is.Zero,
+ $"Explicit --previous-xmi should succeed even when the cache carries a different XMI.\n"
+ + $"stdout:\n{stdout}\nstderr:\n{stderr}");
+ Assert.That(stdout, Does.Not.Contain("auto-derived from MTConnectVersions.Max"),
+ "Explicit --previous-xmi must skip the auto-derive announcement — the delta is operator-directed.");
+ Assert.That(stdout, Does.Contain("--previous-xmi override"),
+ "stdout must announce the explicit-override mode so the operator sees which path fired.");
+
+ // If the resolver had picked the cache, the mutation would surface
+ // as CHANGED files. With the explicit prev matching the new XMI,
+ // changed=0.
+ var stats = ParseChanged(stdout);
+ Assert.That(stats, Is.Zero,
+ "Explicit --previous-xmi matched --new-xmi bit-for-bit; changed must be zero. "
+ + "A non-zero count means the cache leaked into the delta — the explicit override lost.");
+ }
+
+ [Test]
+ public void Missing_MTConnectVersions_cs_fails_with_actionable_message()
+ {
+ // ReadMTConnectVersionsMax throws FileNotFoundException when the
+ // versions file is absent under --output. The top-level try/catch
+ // in Program.cs (lines 172-182) maps that to stderr `error: ...`
+ // + exit 1. Pin the actionable message the operator sees so a
+ // later refactor of the error text still names all four
+ // recovery paths (probed file path, --previous-xmi override,
+ // --full-tree escape hatch, expected file location).
+ var repoRoot = FindRepoRoot();
+ var realXmi = Path.Combine(repoRoot, RealXmiRelativePath);
+
+ var scratch = InitScratchRepoLayout("versions-cs-missing");
+ // Deliberately do NOT write MTConnectVersions.cs — the guard
+ // must fire before the resolver reaches Strategy A/B/C.
+
+ var (exitCode, _, stderr) = RunAutoDerive(realXmi, scratch);
+
+ Assert.That(exitCode, Is.EqualTo(1),
+ $"Missing MTConnectVersions.cs must exit 1 via the top-level catch, not stack-trace.\nstderr:\n{stderr}");
+ Assert.That(stderr, Does.Contain("MTConnectVersions.cs not found"),
+ "stderr must name the missing file so the operator can locate it.");
+ Assert.That(stderr, Does.Contain("--previous-xmi"),
+ "stderr must direct the operator to the explicit-override flag.");
+ Assert.That(stderr, Does.Contain("--full-tree"),
+ "stderr must direct the operator to the delta-disable escape hatch.");
+ }
+
+ [Test]
+ public void MTConnectVersions_cs_without_Max_declaration_fails_hard()
+ {
+ // The Max regex miss surfaces as InvalidOperationException →
+ // top-level catch → exit 1 with a message that pinpoints the
+ // convention the parser expects. Write a syntactically-valid
+ // C# file that carries no `public static Version Max => ...`
+ // property so the regex miss fires; the parser must reject
+ // rather than silently no-op or default to a wrong version.
+ var repoRoot = FindRepoRoot();
+ var realXmi = Path.Combine(repoRoot, RealXmiRelativePath);
+
+ var scratch = InitScratchRepoLayout("versions-cs-no-max");
+ var targetPath = Path.Combine(
+ scratch, "libraries", "MTConnect.NET-Common", "MTConnectVersions.cs");
+ File.WriteAllText(targetPath, @"// Missing Max property; the parser must reject this file.
+using System;
+namespace MTConnect
+{
+ public static class MTConnectVersions
+ {
+ public static readonly Version Version27 = new Version(2, 7);
+ }
+}
+");
+
+ var (exitCode, _, stderr) = RunAutoDerive(realXmi, scratch);
+
+ Assert.That(exitCode, Is.EqualTo(1),
+ $"Missing Max property must exit 1 via the top-level catch.\nstderr:\n{stderr}");
+ Assert.That(stderr, Does.Contain("Could not locate"),
+ "stderr must announce a parse-shape failure, not a resolver failure.");
+ Assert.That(stderr, Does.Contain("Max"),
+ "stderr must name the missing convention element so the operator knows what to restore.");
+ Assert.That(stderr, Does.Contain("--previous-xmi"),
+ "stderr must direct the operator to the explicit-override flag.");
+ }
+
+ [Test]
+ public void MTConnectVersions_cs_without_const_table_entry_fails_hard()
+ {
+ // The Max property resolves to a VersionXY constant that must
+ // exist in the file's const table. If Max => VersionNN but no
+ // `public static readonly Version VersionNN = new Version(...)`,
+ // the resolver throws InvalidOperationException. This exercises
+ // the second `if (!constMatch.Success)` branch, distinct from
+ // the Max-regex-miss branch above.
+ var repoRoot = FindRepoRoot();
+ var realXmi = Path.Combine(repoRoot, RealXmiRelativePath);
+
+ var scratch = InitScratchRepoLayout("versions-cs-no-const");
+ var targetPath = Path.Combine(
+ scratch, "libraries", "MTConnect.NET-Common", "MTConnectVersions.cs");
+ File.WriteAllText(targetPath, @"// Max points at Version99 which is not declared.
+using System;
+namespace MTConnect
+{
+ public static class MTConnectVersions
+ {
+ public static Version Max => Version99;
+ public static readonly Version Version27 = new Version(2, 7);
+ }
+}
+");
+
+ var (exitCode, _, stderr) = RunAutoDerive(realXmi, scratch);
+
+ Assert.That(exitCode, Is.EqualTo(1),
+ $"Missing const-table entry must exit 1 via the top-level catch.\nstderr:\n{stderr}");
+ Assert.That(stderr, Does.Contain("Version99"),
+ "stderr must name the un-resolvable constant so the operator can add it.");
+ Assert.That(stderr, Does.Contain("Could not locate"),
+ "stderr must carry the parse-shape failure fingerprint.");
+ }
+
+ [Test]
+ public void Commented_out_Max_declaration_does_not_confuse_the_parser()
+ {
+ // Regression pin (F-IMP-401, dime cycle 3): a stale
+ // `// public static Version Max => Version27;` line commented out
+ // above the LIVE `public static Version Max => Version29;` line
+ // would win the first-match regex without a comment-strip pass,
+ // pinning PREV_VERSION to the wrong version (v2.7 not v2.9). This
+ // fixture writes such a file with an OLD version commented out
+ // above a NEW version live, then populates the cache path for the
+ // NEW version. Auto-derive must pick up the NEW version (v2.9),
+ // resolving the NEW cache path and NOT the OLD one.
+ var repoRoot = FindRepoRoot();
+ var realXmi = Path.Combine(repoRoot, RealXmiRelativePath);
+
+ var scratch = InitScratchRepoLayout("commented-max-decoy");
+
+ // MTConnectVersions.cs with a commented-out decoy Max line above
+ // the live Max line. Both line comments (`//`) and a block-comment
+ // (`/* ... */`) decoy are exercised so the strip covers both
+ // shapes.
+ var versionsPath = Path.Combine(
+ scratch, "libraries", "MTConnect.NET-Common", "MTConnectVersions.cs");
+ File.WriteAllText(versionsPath, @"// Copyright (c) 2026 TrakHound Inc.
+
+using System;
+
+namespace MTConnect
+{
+ public static class MTConnectVersions
+ {
+ // Historical decoy — the pre-bump Max line, kept as documentation.
+ // public static Version Max => Version27;
+
+ /* Alternative shape decoy retained for reference:
+ public static Version Max => Version28;
+ */
+
+ public static Version Max => Version29;
+
+ public static readonly Version Version27 = new Version(2, 7);
+ public static readonly Version Version28 = new Version(2, 8);
+ public static readonly Version Version29 = new Version(2, 9);
+ }
+}
+");
+
+ // Populate the cache for v2.9 ONLY. If the parser is fooled by
+ // either comment-out decoy, it will look for v2.7 or v2.8 cache
+ // paths (which are absent), fall through to Strategy C, and
+ // fail-hard with a version-mismatched fingerprint.
+ var cacheDir = Path.Combine(scratch, "build", ".cache", "sysml-prev");
+ Directory.CreateDirectory(cacheDir);
+ File.Copy(realXmi, Path.Combine(cacheDir, "MTConnectSysMLModel_v2.9.xml"));
+
+ var (exitCode, stdout, stderr) = RunAutoDerive(realXmi, scratch);
+
+ Assert.That(exitCode, Is.Zero,
+ $"Comment-stripped parse must pick the live Max = Version29 and hit the v2.9 cache.\n"
+ + $"stdout:\n{stdout}\nstderr:\n{stderr}");
+ Assert.That(stdout, Does.Contain("MTConnectSysMLModel_v2.9.xml"),
+ "The comment-stripped parse must resolve the v2.9 cache path (live Max), "
+ + "not the v2.7 / v2.8 decoy paths.");
+ Assert.That(stdout, Does.Not.Contain("MTConnectSysMLModel_v2.7.xml"),
+ "The commented-out `Max => Version27` decoy must not fool the parser.");
+ Assert.That(stdout, Does.Not.Contain("MTConnectSysMLModel_v2.8.xml"),
+ "The block-commented `Max => Version28` decoy must not fool the parser.");
+ }
+
+ [Test]
+ public void Prev_equals_new_warns_and_no_ops_when_new_xmi_filename_encodes_current_max()
+ {
+ // PREV == NEW guard: when the new XMI's filename encodes the same
+ // version as the auto-derived PREV_VERSION (from MTConnectVersions.Max),
+ // the delta is empty by construction — the max already matches the
+ // version being generated. Exit 0 + a warning on stderr, no delta
+ // emit. Filename convention is `MTConnectSysMLModel_v..xml`.
+ var repoRoot = FindRepoRoot();
+ var realXmi = Path.Combine(repoRoot, RealXmiRelativePath);
+ Assert.That(File.Exists(realXmi), Is.True,
+ $"XMI snapshot missing at {realXmi}. Is the build/sysml-model submodule initialised?");
+
+ var scratch = InitScratchRepoLayout("prev-eq-new-guard");
+ WriteSyntheticVersionsCs(scratch);
+
+ // Populate the cache so Strategy B resolves — the guard runs AFTER
+ // ResolvePreviousXmi succeeds. Without a cache, Strategy C would
+ // fire and exit 1 before the guard could evaluate.
+ var cacheDir = Path.Combine(scratch, "build", ".cache", "sysml-prev");
+ Directory.CreateDirectory(cacheDir);
+ File.Copy(realXmi, Path.Combine(cacheDir, "MTConnectSysMLModel_v2.7.xml"));
+
+ // Copy realXmi to a filename that matches MTConnectVersions.Max (v2.7)
+ // so the guard's filename regex matches and the versions equate.
+ var newXmiVersioned = Path.Combine(scratch, "MTConnectSysMLModel_v2.7.xml");
+ File.Copy(realXmi, newXmiVersioned);
+
+ var (exitCode, stdout, stderr) = RunAutoDerive(newXmiVersioned, scratch);
+
+ Assert.That(exitCode, Is.Zero,
+ $"PREV==NEW must exit 0 with warning, not fail.\nstdout:\n{stdout}\nstderr:\n{stderr}");
+ Assert.That(stderr, Does.Contain("already supported by MTConnectVersions.Max"),
+ "stderr must announce the no-delta-to-derive warning so the operator sees why nothing was emitted.");
+ Assert.That(stderr, Does.Contain("v2.7"),
+ "stderr must name the version so the operator can verify the guard fired on the intended version.");
+ Assert.That(stdout, Does.Not.Contain("Delta emission:"),
+ "PREV==NEW must skip the delta emitter — no-op semantics.");
+ }
+
+ [Test]
+ public void Prev_equals_new_guard_stays_silent_when_new_xmi_filename_has_no_version_suffix()
+ {
+ // The PREV==NEW guard predicates on the new XMI filename encoding a
+ // version via the `_v..xml` suffix. A filename without
+ // that suffix (the default `MTConnectSysMLModel.xml` snapshot shape)
+ // must fall through to the normal delta emit — no warning, no early
+ // return, even when MTConnectVersions.Max would numerically match
+ // the underlying XMI's version. This preserves the default Phase 3
+ // workflow where the newXmi is the un-suffixed submodule snapshot.
+ var repoRoot = FindRepoRoot();
+ var realXmi = Path.Combine(repoRoot, RealXmiRelativePath);
+
+ var scratch = InitScratchRepoLayout("prev-eq-new-unsuffixed");
+ WriteSyntheticVersionsCs(scratch);
+
+ var cacheDir = Path.Combine(scratch, "build", ".cache", "sysml-prev");
+ Directory.CreateDirectory(cacheDir);
+ File.Copy(realXmi, Path.Combine(cacheDir, "MTConnectSysMLModel_v2.7.xml"));
+
+ // newXmi is realXmi at its default un-suffixed path — guard's regex
+ // does not match, guard stays silent, delta emitter runs.
+ var (exitCode, stdout, stderr) = RunAutoDerive(realXmi, scratch);
+
+ Assert.That(exitCode, Is.Zero,
+ $"Un-suffixed new-xmi filename must NOT trigger the guard.\nstdout:\n{stdout}\nstderr:\n{stderr}");
+ Assert.That(stderr, Does.Not.Contain("already supported by MTConnectVersions.Max"),
+ "Un-suffixed filename must not trigger the PREV==NEW warning.");
+ Assert.That(stdout, Does.Contain("Delta emission:"),
+ "Un-suffixed filename must reach the delta emitter, not the guard's early return.");
+ }
+
+ [Test]
+ public void Submodule_dir_without_git_repo_falls_through_to_fail_hard()
+ {
+ // Strategy A gates on TryGetSubmoduleTag returning a matching
+ // tag. When the submodule dir exists and holds an XMI but is
+ // NOT a git repository, `git describe` fails and
+ // TryGetSubmoduleTag returns null — Strategy A rejects, and
+ // Strategy C fires. Distinct from the "no submodule dir at all"
+ // path already covered by the fail-hard fixture.
+ var repoRoot = FindRepoRoot();
+ var realXmi = Path.Combine(repoRoot, RealXmiRelativePath);
+
+ var scratch = InitScratchRepoLayout("submodule-not-git");
+ WriteSyntheticVersionsCs(scratch);
+
+ var submoduleDir = Path.Combine(scratch, "build", "sysml-model");
+ Directory.CreateDirectory(submoduleDir);
+ File.Copy(realXmi, Path.Combine(submoduleDir, "MTConnectSysMLModel.xml"));
+ // NO git init — TryGetSubmoduleTag must return null.
+
+ var (exitCode, _, stderr) = RunAutoDerive(realXmi, scratch);
+
+ Assert.That(exitCode, Is.EqualTo(1),
+ $"A non-git submodule dir must fall through to Strategy C, not Strategy A.\nstderr:\n{stderr}");
+ Assert.That(stderr, Does.Contain("PREV_VERSION auto-derivation"),
+ "stderr must announce the Strategy-C fail-hard, not accept the un-tagged tree.");
+ Assert.That(stderr, Does.Contain("v2.7"),
+ "stderr must state the expected submodule tag so the operator sees which tag was required.");
+ }
+
+ [Test]
+ public void Submodule_git_repo_with_wrong_tag_falls_through_to_fail_hard()
+ {
+ // Strategy A accepts only an EXACT-match tag. A git repo tagged
+ // v9.9 (not v2.7 = MTConnectVersions.Max) must reject and fall
+ // through to Strategy C. This exercises the branch where
+ // TryGetSubmoduleTag returns a non-null string that fails the
+ // Ordinal comparison against expectedTag.
+ var repoRoot = FindRepoRoot();
+ var realXmi = Path.Combine(repoRoot, RealXmiRelativePath);
+
+ var scratch = InitScratchRepoLayout("submodule-wrong-tag");
+ WriteSyntheticVersionsCs(scratch);
+
+ var submoduleDir = Path.Combine(scratch, "build", "sysml-model");
+ Directory.CreateDirectory(submoduleDir);
+ File.Copy(realXmi, Path.Combine(submoduleDir, "MTConnectSysMLModel.xml"));
+ InitGitRepoWithTag(submoduleDir, "v9.9");
+
+ var (exitCode, _, stderr) = RunAutoDerive(realXmi, scratch);
+
+ Assert.That(exitCode, Is.EqualTo(1),
+ $"A wrong-tagged submodule must fall through to Strategy C.\nstderr:\n{stderr}");
+ Assert.That(stderr, Does.Contain("PREV_VERSION auto-derivation"),
+ "stderr must announce the Strategy-C fail-hard, not silently accept the wrong tag.");
+ Assert.That(stderr, Does.Contain("v2.7"),
+ "stderr must state the expected tag (v2.7) so the operator sees the mismatch.");
+ }
+
+ [Test]
+ public void Full_tree_flag_disables_delta_mode()
+ {
+ var repoRoot = FindRepoRoot();
+ var realXmi = Path.Combine(repoRoot, RealXmiRelativePath);
+
+ var scratch = InitScratchRepoLayout("full-tree");
+ WriteSyntheticVersionsCs(scratch);
+
+ // Populate the cache so auto-derive WOULD succeed if it were
+ // allowed to run. --full-tree must skip both delta paths and
+ // trigger the full-tree branch instead, producing no Compat
+ // file and no `Delta emission:` stats line.
+ var cacheDir = Path.Combine(scratch, "build", ".cache", "sysml-prev");
+ Directory.CreateDirectory(cacheDir);
+ File.Copy(realXmi, Path.Combine(cacheDir, "MTConnectSysMLModel_v2.7.xml"));
+
+ var (exitCode, stdout, stderr) = RunWithFullTree(realXmi, scratch);
+
+ Assert.That(exitCode, Is.Zero,
+ $"--full-tree must succeed against a valid tree.\nstdout:\n{stdout}\nstderr:\n{stderr}");
+ Assert.That(stdout, Does.Contain("full-tree"),
+ "stdout must announce that the full-tree path fired.");
+ Assert.That(stdout, Does.Not.Contain("Delta emission:"),
+ "--full-tree must skip the delta emitter's stats line — the delta path is fully disabled.");
+ Assert.That(stdout, Does.Not.Contain("auto-derived from MTConnectVersions.Max"),
+ "--full-tree must short-circuit before the auto-derive resolver runs.");
+
+ var compatFiles = Directory
+ .EnumerateFiles(scratch, "*.g.cs", SearchOption.AllDirectories)
+ .Where(p => p.Replace('\\', '/').Contains("/Compat/"))
+ .ToList();
+ Assert.That(compatFiles, Is.Empty,
+ "--full-tree must emit zero Compat/*.g.cs files (Compat is delta-mode-only). "
+ + "Unexpected Compat files:\n " + string.Join("\n ", compatFiles));
+
+ // Full-tree emits the whole tree — at least the current-XMI
+ // baseline count of files. The committed tree ships ~892 .g.cs
+ // files at v2.7 landing (2026-08-20); pin a floor of 700 so
+ // ordinary spec-shrink drift (a version dropping ~15 types) is
+ // tolerated but a delta-mode leakage (which would emit only the
+ // ~10-file diff, not the full tree) trips the guard loudly.
+ // A previous `>100` threshold accepted any partial emission
+ // including the delta subset.
+ var emittedFiles = Directory
+ .EnumerateFiles(scratch, "*.g.cs", SearchOption.AllDirectories)
+ .Count();
+ Assert.That(emittedFiles, Is.GreaterThan(700),
+ "--full-tree must emit the whole generated tree, not the delta subset. "
+ + $"Actual .g.cs count: {emittedFiles}. A count in the ~10-100 range "
+ + "signals a delta-mode leak; a count under 700 signals substantial spec "
+ + "shrink and should ratchet this floor after human review.");
+ }
+
+ // --- helpers -----------------------------------------------------
+
+ private static string FindRepoRoot()
+ {
+ var current = new DirectoryInfo(AppContext.BaseDirectory);
+ while (current != null)
+ {
+ if (File.Exists(Path.Combine(current.FullName, SlnFileName)))
+ return current.FullName;
+ current = current.Parent;
+ }
+ throw new DirectoryNotFoundException(
+ $"Could not locate {SlnFileName} in any ancestor of {AppContext.BaseDirectory}.");
+ }
+
+ // Creates the scratch dir with a repo-like layout: the three library
+ // subdirectories the renderers guard against, plus the build/ tree
+ // ancestor that the cache and submodule strategies probe under.
+ private static string InitScratchRepoLayout(string suffix)
+ {
+ var repoRoot = FindRepoRoot();
+ var path = Path.Combine(repoRoot, ScratchRoot, suffix);
+ if (Directory.Exists(path))
+ {
+ // A prior test run may have left a synthetic git repo behind
+ // whose .git/objects tree resists a plain recursive delete on
+ // some filesystems. Two-pass delete: first try recursive,
+ // then if that fails, chmod the tree writable and retry.
+ TryDeleteTree(path);
+ }
+ Directory.CreateDirectory(path);
+ Directory.CreateDirectory(Path.Combine(path, "libraries", "MTConnect.NET-Common"));
+ Directory.CreateDirectory(Path.Combine(path, "libraries", "MTConnect.NET-JSON-cppagent"));
+ Directory.CreateDirectory(Path.Combine(path, "libraries", "MTConnect.NET-XML"));
+ Directory.CreateDirectory(Path.Combine(path, "build"));
+ return path;
+ }
+
+ private static void TryDeleteTree(string path)
+ {
+ try
+ {
+ Directory.Delete(path, recursive: true);
+ }
+ catch (UnauthorizedAccessException)
+ {
+ // Loose read-only bits on git pack files trip the plain delete
+ // on Windows; clear them and retry once.
+ foreach (var f in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories))
+ {
+ try { File.SetAttributes(f, FileAttributes.Normal); } catch { }
+ }
+ Directory.Delete(path, recursive: true);
+ }
+ }
+
+ private static void WriteSyntheticVersionsCs(string scratch)
+ {
+ var target = Path.Combine(
+ scratch, "libraries", "MTConnect.NET-Common", "MTConnectVersions.cs");
+ File.WriteAllText(target, SyntheticVersionsCs);
+ }
+
+ // Bootstraps a minimal git repo at `dir`, stages every present file,
+ // commits, and tags the commit `tagName`. The auto-derive Strategy A
+ // path runs `git -C describe --exact-match --tags HEAD`; this
+ // helper produces the shape that lookup expects.
+ //
+ // Every git config that could pull in a signing hook is disabled
+ // per-repo (commit.gpgsign, tag.gpgsign, tag.forceSignAnnotated) so the
+ // helper works on a developer host with the tester's global-config
+ // signing hooks (ottobolyos runs `commit.gpgsign=true` + `tag.gpgsign=true`
+ // globally — those defaults would abort the synthetic tag on a host
+ // without a matching GPG key context).
+ private static void InitGitRepoWithTag(string dir, string tagName)
+ {
+ RunGit(dir, "init", "-q");
+ RunGit(dir, "config", "user.email", "auto-derive-test@example.invalid");
+ RunGit(dir, "config", "user.name", "Auto Derive Test");
+ RunGit(dir, "config", "commit.gpgsign", "false");
+ RunGit(dir, "config", "tag.gpgsign", "false");
+ RunGit(dir, "config", "tag.forceSignAnnotated", "false");
+ RunGit(dir, "add", "-A");
+ RunGit(dir, "commit", "-q", "-m", "synthetic sysml-model snapshot for auto-derive test");
+ // Explicit lightweight tag — no `-a`, no `-s`, no message — so the
+ // synthetic tag lands regardless of tester-side GPG state. The
+ // per-repo `tag.gpgsign=false` above is defence-in-depth for the
+ // same concern.
+ RunGit(dir, "tag", tagName);
+ }
+
+ private static void RunGit(string workingDir, params string[] args)
+ {
+ var psi = new ProcessStartInfo("git")
+ {
+ WorkingDirectory = workingDir,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+ foreach (var a in args)
+ psi.ArgumentList.Add(a);
+
+ using var proc = Process.Start(psi)
+ ?? throw new InvalidOperationException($"Failed to start git {string.Join(' ', args)}.");
+ var stdoutTask = proc.StandardOutput.ReadToEndAsync();
+ var stderrTask = proc.StandardError.ReadToEndAsync();
+ System.Threading.Tasks.Task.WhenAll(stdoutTask, stderrTask).GetAwaiter().GetResult();
+ proc.WaitForExit();
+ if (proc.ExitCode != 0)
+ {
+ throw new InvalidOperationException(
+ $"git {string.Join(' ', args)} exited {proc.ExitCode} in {workingDir}. " +
+ $"stderr:\n{stderrTask.Result}");
+ }
+ }
+
+ // Auto-derive invocation shape: only --new-xmi + --output. No
+ // --previous-xmi, no --full-tree — this is exactly the zero-config
+ // form Phase 3 of the version-bump plan calls.
+ private static (int ExitCode, string Stdout, string Stderr) RunAutoDerive(
+ string newXmi, string output)
+ {
+ return RunGenerator(output, "--new-xmi", newXmi, "--output", output);
+ }
+
+ private static (int ExitCode, string Stdout, string Stderr) RunWithExplicitPrevious(
+ string newXmi, string previousXmi, string output)
+ {
+ return RunGenerator(output,
+ "--new-xmi", newXmi,
+ "--previous-xmi", previousXmi,
+ "--output", output);
+ }
+
+ private static (int ExitCode, string Stdout, string Stderr) RunWithFullTree(
+ string newXmi, string output)
+ {
+ return RunGenerator(output,
+ "--new-xmi", newXmi,
+ "--output", output,
+ "--full-tree");
+ }
+
+ private static (int ExitCode, string Stdout, string Stderr) RunGenerator(
+ string outputRootForCwd, params string[] cliArgs)
+ {
+ var repoRoot = FindRepoRoot();
+ var psi = new ProcessStartInfo("dotnet")
+ {
+ // Run `dotnet run` from the REAL repo root so the generator
+ // project builds correctly (the ProjectReference on the test
+ // csproj already built it, and --no-build below reuses that
+ // output). The generator's --output points at the SCRATCH
+ // dir, so all path probes land there.
+ WorkingDirectory = repoRoot,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+ psi.ArgumentList.Add("run");
+ psi.ArgumentList.Add("--no-build");
+ psi.ArgumentList.Add("--project");
+ psi.ArgumentList.Add(GeneratorProject);
+ psi.ArgumentList.Add("--");
+ foreach (var arg in cliArgs)
+ psi.ArgumentList.Add(arg);
+
+ using var proc = Process.Start(psi)
+ ?? throw new InvalidOperationException("Failed to start dotnet run for the generator.");
+
+ // Drain stdout and stderr concurrently — see ByteIdenticalRegenTests
+ // for the deadlock defence this pattern encodes.
+ var stdoutTask = proc.StandardOutput.ReadToEndAsync();
+ var stderrTask = proc.StandardError.ReadToEndAsync();
+ System.Threading.Tasks.Task.WhenAll(stdoutTask, stderrTask).GetAwaiter().GetResult();
+ proc.WaitForExit();
+ return (proc.ExitCode, stdoutTask.Result, stderrTask.Result);
+ }
+
+ // Extracts the `changed=N` value from the delta stats line so the
+ // explicit-override test can pin the CHANGED count. Returns -1 on
+ // absent stats line (which is a distinct failure mode from
+ // changed=0).
+ private static int ParseChanged(string stdout)
+ {
+ var match = System.Text.RegularExpressions.Regex.Match(
+ stdout, @"Delta emission:.*?changed=(?\d+)");
+ if (!match.Success)
+ throw new AssertionException(
+ "stdout does not carry the expected 'Delta emission: ... changed=N ...' stats line.\n"
+ + stdout);
+ return int.Parse(match.Groups["c"].Value);
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-Generator-Tests/ByteIdenticalRegenTests.cs b/tests/MTConnect.NET-Generator-Tests/ByteIdenticalRegenTests.cs
new file mode 100644
index 000000000..3d4421dec
--- /dev/null
+++ b/tests/MTConnect.NET-Generator-Tests/ByteIdenticalRegenTests.cs
@@ -0,0 +1,262 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using NUnit.Framework;
+
+namespace MTConnect.NET_Generator_Tests
+{
+ ///
+ /// Byte-identical regeneration guards for the SysML importer.
+ ///
+ /// Two guards land side by side:
+ ///
+ ///
+ /// —
+ /// the everyday guard. Regenerates the tree twice against the same
+ /// XMI and asserts the two emitted trees are byte-identical. This
+ /// locks in the determinism guarantee the template consolidations
+ /// in Phase 3 rely on: any consolidation that alters emission
+ /// behaviour flips this test RED regardless of whether the
+ /// committed libraries/**/*.g.cs tree is currently in sync
+ /// with the generator.
+ /// —
+ /// the strict baseline guard. Diffs a fresh regen against the
+ /// committed tree and fails on any drift. The Phase 3.1 dry-run
+ /// on 2026-08-20 surfaced a 78-file drift (15 committed
+ /// .g.cs files the generator no longer emits + 63 files
+ /// with content drift); Phase 4.1 resolved every case in the
+ /// preceding commit train (10 missing Pallet measurement
+ /// interfaces routed through a new template + MeasurementModel
+ /// .RenderInterface() wire-up, 5 orphaned .g.cs files
+ /// deleted after a codebase-wide grep confirmed zero consumers,
+ /// 63 whitespace-drift files refreshed to current-generator
+ /// output). The guard now runs on every CI test sweep.
+ ///
+ ///
+ /// Scope decision (ottobolyos 2026-08-20): current-XMI only. The
+ /// build/sysml-model submodule ships one snapshot per MTConnect
+ /// Standard version bump; iterating over historical XMI tags is not part
+ /// of the Phase 3 scope. When a new spec version lands, a sibling
+ /// byte-identical guard commit adds coverage for that version's XMI.
+ ///
+ [TestFixture]
+ public class ByteIdenticalRegenTests
+ {
+ // Well-known repo-relative paths. Discovery walks up from the test
+ // assembly's base directory to the repo root (the first ancestor
+ // that contains MTConnect.NET.sln).
+ private const string SlnFileName = "MTConnect.NET.sln";
+ private const string GeneratorProject = "build/MTConnect.NET-SysML-Import";
+ private const string XmiRelativePath = "build/sysml-model/MTConnectSysMLModel.xml";
+ private const string GenScratchDirPrimary = ".claude/gen-test-out/byte-identical";
+ private const string GenScratchDirSecondary = ".claude/gen-test-out/byte-identical-2";
+
+ [Test]
+ public void Regen_is_deterministic_across_two_invocations()
+ {
+ var repoRoot = FindRepoRoot();
+ var xmiPath = Path.Combine(repoRoot, XmiRelativePath);
+ Assert.That(File.Exists(xmiPath), Is.True,
+ $"XMI snapshot missing at {xmiPath}. Is the build/sysml-model submodule initialised?");
+
+ var scratchA = Path.Combine(repoRoot, GenScratchDirPrimary);
+ var scratchB = Path.Combine(repoRoot, GenScratchDirSecondary);
+ InitScratch(scratchA);
+ InitScratch(scratchB);
+
+ RunGenerator(repoRoot, xmiPath, scratchA);
+ RunGenerator(repoRoot, xmiPath, scratchB);
+
+ var hashesA = HashGeneratedTree(Path.Combine(scratchA, "libraries"));
+ var hashesB = HashGeneratedTree(Path.Combine(scratchB, "libraries"));
+
+ var diff = CompareTrees(hashesA, hashesB);
+ Assert.That(diff.Length, Is.Zero,
+ "Regenerator is NOT deterministic: two back-to-back invocations against " +
+ "the same XMI produced different .g.cs trees. Any Phase 3 template " +
+ "consolidation that changes the emission surface would flip this test RED.\n\n" +
+ diff);
+ }
+
+ [Test]
+ public void Current_XMI_regen_matches_committed_g_cs_tree()
+ {
+ var repoRoot = FindRepoRoot();
+ var xmiPath = Path.Combine(repoRoot, XmiRelativePath);
+ Assert.That(File.Exists(xmiPath), Is.True,
+ $"XMI snapshot missing at {xmiPath}. Is the build/sysml-model submodule initialised?");
+
+ var scratchRoot = Path.Combine(repoRoot, GenScratchDirPrimary);
+ InitScratch(scratchRoot);
+ RunGenerator(repoRoot, xmiPath, scratchRoot);
+
+ var emitted = HashGeneratedTree(Path.Combine(scratchRoot, "libraries"));
+ var committed = HashGeneratedTree(Path.Combine(repoRoot, "libraries"));
+
+ var diff = CompareTrees(committed, emitted, leftLabel: "committed", rightLabel: "regenerated");
+ Assert.That(diff.Length, Is.Zero,
+ "Regeneration is not byte-identical to the committed .g.cs tree. Either " +
+ "the templates changed emission behaviour, the parser drifted, or the " +
+ "committed generated files were hand-edited.\n\n" + diff);
+ }
+
+ // --- helpers -----------------------------------------------------
+
+ // Locates the repo root by walking up from the test assembly's base
+ // directory until a directory containing MTConnect.NET.sln is found.
+ private static string FindRepoRoot()
+ {
+ var current = new DirectoryInfo(AppContext.BaseDirectory);
+ while (current != null)
+ {
+ if (File.Exists(Path.Combine(current.FullName, SlnFileName)))
+ return current.FullName;
+ current = current.Parent;
+ }
+ throw new DirectoryNotFoundException(
+ $"Could not locate {SlnFileName} in any ancestor of {AppContext.BaseDirectory}. " +
+ "The test must run from within the MTConnect.NET repository.");
+ }
+
+ // Wipes the target directory, then scaffolds the three library
+ // subdirectories the generator's Program.cs guards its renderer
+ // entry points on (fail-fast against pointing --output at the
+ // wrong tree). The generator populates only .g.cs files inside
+ // these subtrees; hand-authored .cs files live alongside but are
+ // never emitted, so the scaffolding stays empty.
+ private static void InitScratch(string path)
+ {
+ if (Directory.Exists(path))
+ Directory.Delete(path, recursive: true);
+ Directory.CreateDirectory(path);
+ Directory.CreateDirectory(Path.Combine(path, "libraries", "MTConnect.NET-Common"));
+ Directory.CreateDirectory(Path.Combine(path, "libraries", "MTConnect.NET-JSON-cppagent"));
+ Directory.CreateDirectory(Path.Combine(path, "libraries", "MTConnect.NET-XML"));
+ }
+
+ // Invokes the generator via `dotnet run --no-build --project
+ // -- --xmi --output `. The generator project is
+ // wired as a ProjectReference on this test csproj so MSBuild
+ // builds it ahead of the test run; --no-build keeps the invocation
+ // cheap. Non-zero exit fires the caller with the full stdout /
+ // stderr in the exception.
+ private static void RunGenerator(string repoRoot, string xmiPath, string scratchRoot)
+ {
+ var psi = new ProcessStartInfo("dotnet")
+ {
+ WorkingDirectory = repoRoot,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+ psi.ArgumentList.Add("run");
+ psi.ArgumentList.Add("--no-build");
+ psi.ArgumentList.Add("--project");
+ psi.ArgumentList.Add(GeneratorProject);
+ psi.ArgumentList.Add("--");
+ psi.ArgumentList.Add("--xmi");
+ psi.ArgumentList.Add(xmiPath);
+ psi.ArgumentList.Add("--output");
+ psi.ArgumentList.Add(scratchRoot);
+ // --full-tree pins the byte-identical guard to the full-regeneration
+ // path. Without it the zero-config auto-derive (task #408) would
+ // kick in against the scratch dir, which lacks
+ // libraries/MTConnect.NET-Common/MTConnectVersions.cs, and abort
+ // with a PREV_VERSION resolver error before any templates render.
+ psi.ArgumentList.Add("--full-tree");
+
+ using var proc = Process.Start(psi)
+ ?? throw new InvalidOperationException("Failed to start dotnet run for the generator.");
+
+ // Drain stdout AND stderr concurrently. Blocking on ReadToEnd() for
+ // one pipe while the child writes >4 KB to the other deadlocks
+ // (Linux pipe buffer fills, child blocks on write, parent blocks on
+ // read of the empty pipe). Task.WhenAll on the two async reads and
+ // WaitForExitAsync side-steps the deadlock entirely.
+ var stdoutTask = proc.StandardOutput.ReadToEndAsync();
+ var stderrTask = proc.StandardError.ReadToEndAsync();
+ System.Threading.Tasks.Task.WhenAll(stdoutTask, stderrTask).GetAwaiter().GetResult();
+ proc.WaitForExit();
+ var stdout = stdoutTask.Result;
+ var stderr = stderrTask.Result;
+
+ if (proc.ExitCode != 0)
+ {
+ throw new InvalidOperationException(
+ $"Generator exited with code {proc.ExitCode}.\n" +
+ $"stdout:\n{stdout}\n" +
+ $"stderr:\n{stderr}");
+ }
+ }
+
+ // Walks the tree, hashing every .g.cs file. Returns a dictionary
+ // keyed by the path relative to (forward-slash normalised)
+ // with the SHA-256 hash of the file's byte content as value.
+ //
+ // MSBuild-generated intermediates under bin/ and obj/ (a library's
+ // GlobalUsings.g.cs from Microsoft.NET.Sdk.CSharp.CoreCompile.targets,
+ // ImplicitNamespaceImports.g.cs, etc.) are skipped — the generator
+ // never touches them, and their presence would spuriously flip this
+ // test RED on any host that has already built the solution.
+ private static Dictionary HashGeneratedTree(string root)
+ {
+ var result = new Dictionary(StringComparer.Ordinal);
+ if (!Directory.Exists(root))
+ return result;
+
+ using var sha = SHA256.Create();
+ foreach (var file in Directory.EnumerateFiles(root, "*.g.cs", SearchOption.AllDirectories))
+ {
+ var relative = Path.GetRelativePath(root, file).Replace('\\', '/');
+ if (relative.Contains("/bin/") || relative.Contains("/obj/") ||
+ relative.StartsWith("bin/") || relative.StartsWith("obj/"))
+ continue;
+ using var stream = File.OpenRead(file);
+ result[relative] = sha.ComputeHash(stream);
+ }
+ return result;
+ }
+
+ // Emits a human-readable diff report between two path -> hash
+ // dictionaries. Returns an empty string when the two are identical.
+ private static string CompareTrees(
+ Dictionary left,
+ Dictionary right,
+ string leftLabel = "expected",
+ string rightLabel = "actual")
+ {
+ var onlyLeft = left.Keys.Except(right.Keys).OrderBy(k => k).ToList();
+ var onlyRight = right.Keys.Except(left.Keys).OrderBy(k => k).ToList();
+ var mismatched = left.Keys.Intersect(right.Keys)
+ .Where(k => !left[k].SequenceEqual(right[k]))
+ .OrderBy(k => k)
+ .ToList();
+
+ var report = new StringBuilder();
+ AppendListing(report, $"Missing in {rightLabel} (present in {leftLabel})", onlyLeft);
+ AppendListing(report, $"Extra in {rightLabel} (absent from {leftLabel})", onlyRight);
+ AppendListing(report, "Content mismatch", mismatched);
+ return report.ToString();
+ }
+
+ private static void AppendListing(StringBuilder sink, string heading, List entries)
+ {
+ if (entries.Count == 0)
+ return;
+
+ sink.AppendLine($"{heading} ({entries.Count} files):");
+ foreach (var path in entries.Take(20))
+ sink.AppendLine($" {path}");
+ if (entries.Count > 20)
+ sink.AppendLine($" ... and {entries.Count - 20} more");
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-Generator-Tests/CliInvocationFailureTests.cs b/tests/MTConnect.NET-Generator-Tests/CliInvocationFailureTests.cs
new file mode 100644
index 000000000..319994c46
--- /dev/null
+++ b/tests/MTConnect.NET-Generator-Tests/CliInvocationFailureTests.cs
@@ -0,0 +1,528 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using NUnit.Framework;
+
+namespace MTConnect.NET_Generator_Tests
+{
+ ///
+ /// CLI failure-path coverage for the SysML importer's Program.cs .
+ ///
+ ///
+ /// The importer's happy path (full-tree regen + delta regen) is covered
+ /// by and .
+ /// This fixture pins the EARLY-RETURN branches — every documented exit
+ /// code, every invalid-input surface, and the two RequireValue
+ /// throws that fire when a flag arrives without its trailing value.
+ ///
+ ///
+ ///
+ /// Every case is exercised end-to-end via dotnet run --no-build
+ /// --project build/MTConnect.NET-SysML-Import so the assertions bind
+ /// to the CLI contract the operator actually sees, not to an internal
+ /// helper. Exit codes are documented in the Program.cs header:
+ ///
+ /// 0 — success (including --help / -h ).
+ /// 1 — runtime failure (file not found, parse null, missing library subdir).
+ /// 2 — usage failure (missing / unknown flag).
+ ///
+ ///
+ ///
+ ///
+ /// Extra runtime failures (RequireValue throws on a dangling
+ /// flag, MTConnectModel.Parse returns null on a malformed XMI,
+ /// a missing library subdirectory throws DirectoryNotFoundException )
+ /// surface as non-zero exit codes; the assertions there are on
+ /// ExitCode != 0 plus the stderr fingerprint, since the .NET
+ /// runtime unhandled-exception exit code (0x80000000-ish, negative
+ /// signed) is host-dependent.
+ ///
+ ///
+ [TestFixture]
+ public class CliInvocationFailureTests
+ {
+ private const string SlnFileName = "MTConnect.NET.sln";
+ private const string GeneratorProject = "build/MTConnect.NET-SysML-Import";
+ private const string XmiRelativePath = "build/sysml-model/MTConnectSysMLModel.xml";
+ private const string ScratchRoot = ".claude/gen-test-out/cli-failure";
+
+ [Test]
+ public void Unknown_flag_exits_2_and_stderr_names_the_flag()
+ {
+ var (exitCode, _, stderr) = Run("--not-a-real-flag");
+ Assert.That(exitCode, Is.EqualTo(2),
+ "Unknown flags are a usage error; exit 2 is the documented contract.");
+ Assert.That(stderr, Does.Contain("Unknown argument"),
+ "stderr should name the flag class so the operator sees a discoverable message.");
+ Assert.That(stderr, Does.Contain("--not-a-real-flag"),
+ "stderr should echo the offending flag verbatim.");
+ }
+
+ [Test]
+ public void Missing_xmi_flag_exits_2_with_required_message()
+ {
+ var scratch = InitScratch("missing-xmi");
+ var (exitCode, _, stderr) = Run("--output", scratch);
+ Assert.That(exitCode, Is.EqualTo(2),
+ "Missing --new-xmi is a usage error; exit 2.");
+ Assert.That(stderr, Does.Contain("--new-xmi"),
+ "stderr should identify which required flag is missing.");
+ Assert.That(stderr, Does.Contain("--xmi"),
+ "stderr should also mention the legacy --xmi alias so operators grepping for the pre-#233 flag name still see the required-flag hint.");
+ Assert.That(stderr, Does.Contain("required"),
+ "stderr should call out that the flag is required.");
+ }
+
+ [Test]
+ public void Missing_output_flag_exits_2_with_required_message()
+ {
+ var repoRoot = FindRepoRoot();
+ var xmi = Path.Combine(repoRoot, XmiRelativePath);
+ var (exitCode, _, stderr) = Run("--xmi", xmi);
+ Assert.That(exitCode, Is.EqualTo(2),
+ "Missing --output is a usage error; exit 2.");
+ Assert.That(stderr, Does.Contain("--output"),
+ "stderr should identify which required flag is missing.");
+ }
+
+ [Test]
+ public void Nonexistent_xmi_file_exits_1_with_not_found_message()
+ {
+ var scratch = InitScratch("nonexistent-xmi");
+ var bogusXmi = Path.Combine(scratch, "does-not-exist.xml");
+ var (exitCode, _, stderr) = Run("--xmi", bogusXmi, "--output", scratch);
+ Assert.That(exitCode, Is.EqualTo(1),
+ "Missing XMI file is a runtime failure; exit 1.");
+ Assert.That(stderr, Does.Contain("XMI file not found"),
+ "stderr should name the failure class so the operator can act.");
+ Assert.That(stderr, Does.Contain(bogusXmi),
+ "stderr should echo the resolved path so a typo is grep-able.");
+ }
+
+ [Test]
+ public void Nonexistent_previous_xmi_file_exits_1_with_not_found_message()
+ {
+ var repoRoot = FindRepoRoot();
+ var xmi = Path.Combine(repoRoot, XmiRelativePath);
+ var scratch = InitScratch("nonexistent-previous-xmi");
+ var bogusPrev = Path.Combine(scratch, "does-not-exist-prev.xml");
+ var (exitCode, _, stderr) = Run("--xmi", xmi, "--previous-xmi", bogusPrev, "--output", scratch);
+ Assert.That(exitCode, Is.EqualTo(1),
+ "Missing --previous-xmi file is a runtime failure; exit 1.");
+ Assert.That(stderr, Does.Contain("--previous-xmi file not found"),
+ "stderr should name the exact flag whose target is missing.");
+ Assert.That(stderr, Does.Contain(bogusPrev),
+ "stderr should echo the resolved path.");
+ }
+
+ [Test]
+ public void Nonexistent_output_root_exits_1_with_not_found_message()
+ {
+ var repoRoot = FindRepoRoot();
+ var xmi = Path.Combine(repoRoot, XmiRelativePath);
+ var bogusOutput = Path.Combine(repoRoot, ScratchRoot, "does-not-exist-output");
+ // Deliberately do NOT create bogusOutput's directory.
+ if (Directory.Exists(bogusOutput))
+ Directory.Delete(bogusOutput, recursive: true);
+ var (exitCode, _, stderr) = Run("--xmi", xmi, "--output", bogusOutput);
+ Assert.That(exitCode, Is.EqualTo(1),
+ "Missing output root is a runtime failure; exit 1.");
+ Assert.That(stderr, Does.Contain("Output root not found"),
+ "stderr should identify the output-root failure class.");
+ }
+
+ [Test]
+ public void Help_flag_exits_0_and_stdout_carries_usage_banner()
+ {
+ var (exitCode, stdout, _) = Run("--help");
+ Assert.That(exitCode, Is.Zero,
+ "--help is a documented success path; exit 0.");
+ Assert.That(stdout, Does.Contain("MTConnect.NET SysML Importer"),
+ "Help output must carry the tool banner so the operator knows what they're using.");
+ Assert.That(stdout, Does.Contain("--new-xmi"),
+ "Help must list --new-xmi (preferred flag; task #408).");
+ Assert.That(stdout, Does.Contain("--xmi"),
+ "Help must also mention --xmi (legacy alias documented for pre-#408 callers).");
+ Assert.That(stdout, Does.Contain("--previous-xmi"),
+ "Help must list --previous-xmi (added in Phase 4.3).");
+ Assert.That(stdout, Does.Contain("--compat-version-label"),
+ "Help must list --compat-version-label (added in Phase 4.3).");
+ Assert.That(stdout, Does.Contain("--full-tree"),
+ "Help must list --full-tree (added in task #408 as the escape hatch that disables both delta paths).");
+ }
+
+ [Test]
+ public void Short_help_flag_exits_0()
+ {
+ var (exitCode, stdout, _) = Run("-h");
+ Assert.That(exitCode, Is.Zero, "-h is the short form of --help; exit 0.");
+ Assert.That(stdout, Does.Contain("MTConnect.NET SysML Importer"),
+ "-h must produce the same banner as --help.");
+ }
+
+ [Test]
+ public void Missing_value_after_xmi_flag_exits_non_zero_with_argument_exception()
+ {
+ // RequireValue throws ArgumentException when the flag is the last
+ // token and no value follows. The unhandled exception bubbles to
+ // the CLR host and returns a non-zero exit code; the stderr
+ // fingerprint carries the ArgumentException message.
+ var (exitCode, _, stderr) = Run("--xmi");
+ Assert.That(exitCode, Is.Not.Zero,
+ "A flag with no trailing value is a runtime failure; exit must be non-zero.");
+ Assert.That(stderr, Does.Contain("--xmi"),
+ "stderr should name the offending flag.");
+ Assert.That(stderr, Does.Contain("requires a value").Or.Contain("ArgumentException"),
+ "stderr should carry the RequireValue-throw fingerprint.");
+ }
+
+ [Test]
+ public void Missing_value_after_new_xmi_flag_exits_non_zero()
+ {
+ // Task #408 introduced --new-xmi as the preferred spelling. Its
+ // RequireValue arm is a distinct switch case from --xmi; pin the
+ // parallel failure surface so a later refactor can't silently
+ // regress the preferred-flag arm while leaving the legacy alias
+ // exercised.
+ var (exitCode, _, stderr) = Run("--new-xmi");
+ Assert.That(exitCode, Is.Not.Zero,
+ "A --new-xmi with no trailing value is a runtime failure; exit non-zero.");
+ Assert.That(stderr, Does.Contain("--new-xmi"),
+ "stderr should name the offending flag verbatim.");
+ Assert.That(stderr, Does.Contain("requires a value").Or.Contain("ArgumentException"),
+ "stderr should carry the RequireValue-throw fingerprint.");
+ }
+
+ [Test]
+ public void Missing_value_after_previous_xmi_flag_exits_non_zero()
+ {
+ var repoRoot = FindRepoRoot();
+ var xmi = Path.Combine(repoRoot, XmiRelativePath);
+ // --previous-xmi is the last token; RequireValue throws.
+ var (exitCode, _, stderr) = Run("--xmi", xmi, "--previous-xmi");
+ Assert.That(exitCode, Is.Not.Zero,
+ "A --previous-xmi with no trailing value is a runtime failure; exit non-zero.");
+ Assert.That(stderr, Does.Contain("--previous-xmi"),
+ "stderr should name the offending flag.");
+ }
+
+ [Test]
+ public void Missing_value_after_compat_version_label_exits_non_zero()
+ {
+ var repoRoot = FindRepoRoot();
+ var xmi = Path.Combine(repoRoot, XmiRelativePath);
+ var (exitCode, _, stderr) = Run("--xmi", xmi, "--compat-version-label");
+ Assert.That(exitCode, Is.Not.Zero,
+ "A --compat-version-label with no trailing value is a runtime failure; exit non-zero.");
+ Assert.That(stderr, Does.Contain("--compat-version-label"),
+ "stderr should name the offending flag.");
+ }
+
+ [Test]
+ public void Missing_value_after_output_flag_exits_non_zero()
+ {
+ var repoRoot = FindRepoRoot();
+ var xmi = Path.Combine(repoRoot, XmiRelativePath);
+ var (exitCode, _, stderr) = Run("--xmi", xmi, "--output");
+ Assert.That(exitCode, Is.Not.Zero,
+ "A --output with no trailing value is a runtime failure; exit non-zero.");
+ Assert.That(stderr, Does.Contain("--output"),
+ "stderr should name the offending flag.");
+ }
+
+ [Test]
+ public void Missing_value_after_json_dump_flag_exits_non_zero()
+ {
+ var repoRoot = FindRepoRoot();
+ var xmi = Path.Combine(repoRoot, XmiRelativePath);
+ var (exitCode, _, stderr) = Run("--xmi", xmi, "--json-dump");
+ Assert.That(exitCode, Is.Not.Zero,
+ "A --json-dump with no trailing value is a runtime failure; exit non-zero.");
+ Assert.That(stderr, Does.Contain("--json-dump"),
+ "stderr should name the offending flag.");
+ }
+
+ [Test]
+ public void Missing_library_subdirectory_under_output_root_throws()
+ {
+ // Output root exists, but the required libraries/MTConnect.NET-Common
+ // subdirectory is absent. Program's RenderCommonClasses fails
+ // fast with a DirectoryNotFoundException. Pass --full-tree so the
+ // zero-config auto-derive path doesn't intercept first with its
+ // own "MTConnectVersions.cs not found" surface — this fixture is
+ // pinning the RenderCommonClasses failure, not the auto-derive
+ // failure (that path is covered by AutoDerivePreviousXmiTests).
+ var repoRoot = FindRepoRoot();
+ var xmi = Path.Combine(repoRoot, XmiRelativePath);
+ var scratch = InitScratch("missing-lib-subdir");
+ // Deliberately do NOT create the libraries/MTConnect.NET-Common subdir.
+ var (exitCode, _, stderr) = Run("--xmi", xmi, "--output", scratch, "--full-tree");
+ Assert.That(exitCode, Is.Not.Zero,
+ "A missing library subdirectory must fail fast, not silently no-op.");
+ Assert.That(stderr, Does.Contain("MTConnect.NET-Common").Or.Contain("DirectoryNotFoundException"),
+ "stderr should identify which subdir is missing so the operator can create it.");
+ }
+
+ [Test]
+ public void Malformed_xmi_exits_non_zero_and_surfaces_parse_failure()
+ {
+ var scratch = InitScratchWithLibraries("malformed-xmi");
+ var badXmi = Path.Combine(scratch, "malformed.xml");
+ // A well-formed XML that is not a SysML XMI. Two possible
+ // surface behaviours:
+ // (a) MTConnectModel.Parse returns null → Program's full-tree
+ // branch prints "error: Failed to parse XMI" and returns 1.
+ // (b) MTConnectModel.Parse throws (missing UML root element,
+ // unhandled KeyNotFoundException, etc.) → the CLR host
+ // returns a non-zero abnormal-termination exit code
+ // (typically 134 on Linux, i.e. SIGABRT from an unhandled
+ // exception).
+ // Both surfaces satisfy the coverage contract "malformed input is
+ // a runtime failure". The (a) branch is the graceful, operator-
+ // friendly one and would be a nice hardening target (a top-level
+ // try/catch that mapped every parse exception to `return 1;`);
+ // that hardening is tracked as a follow-up finding.
+ File.WriteAllText(badXmi, " ");
+ // --full-tree so the parse failure lands in the full-tree branch, not
+ // in the zero-config auto-derive's PREV_VERSION resolver — this
+ // fixture is pinning the parse-failure surface, not the auto-derive
+ // one (that path is covered by AutoDerivePreviousXmiTests).
+ var (exitCode, stdout, stderr) = Run("--xmi", badXmi, "--output", scratch, "--full-tree");
+ Assert.That(exitCode, Is.Not.Zero,
+ $"A malformed XMI must fail the invocation. exit={exitCode}\nstdout:\n{stdout}\nstderr:\n{stderr}");
+ var combined = stdout + "\n" + stderr;
+ Assert.That(combined,
+ Does.Contain("Failed to parse XMI")
+ .Or.Contain("parse")
+ .Or.Contain("Exception")
+ .Or.Contain("XmlException")
+ .Or.Contain("NullReference"),
+ "The output stream must surface a parse-failure fingerprint the operator can grep for.");
+ }
+
+ // Every hostile / malformed --compat-version-label value that
+ // IsSafeCompatLabel is designed to reject. Each case exercises the
+ // exit-2 guard branch (Program.cs:198-204). The regex accepts
+ // 1..64 chars of [A-Za-z0-9_\-] followed by [A-Za-z0-9_\-.]*, no
+ // leading dot; anything else must reject at argument-parse time.
+ //
+ // The `TestCaseSource` shape (versus inline `[TestCase]`) keeps the
+ // 65-char oversize label programmatically constructed rather than
+ // hard-coded, so a later ratchet of the length limit needs to
+ // change only the source method + the guard.
+ [TestCaseSource(nameof(HostileCompatLabelCases))]
+ public void Hostile_compat_version_label_rejects_with_exit_2(
+ string hostileLabel, string scenario)
+ {
+ var repoRoot = FindRepoRoot();
+ var xmi = Path.Combine(repoRoot, XmiRelativePath);
+ var scratch = InitScratchWithLibraries($"hostile-label-{scenario}");
+
+ // --full-tree short-circuits the auto-derive resolver so the
+ // safety check is exercised in isolation. Without --full-tree
+ // the zero-config resolver would abort first (no
+ // MTConnectVersions.cs under the scratch tree), masking the
+ // IsSafeCompatLabel branch under a different early-return.
+ var (exitCode, _, stderr) = Run(
+ "--xmi", xmi,
+ "--output", scratch,
+ "--compat-version-label", hostileLabel,
+ "--full-tree");
+
+ Assert.That(exitCode, Is.EqualTo(2),
+ $"Hostile --compat-version-label '{hostileLabel}' ({scenario}) must reject with usage-error exit 2. "
+ + $"An exit 0/1 signals the IsSafeCompatLabel guard was bypassed. stderr:\n{stderr}");
+ Assert.That(stderr, Does.Contain("--compat-version-label"),
+ "stderr must name the offending flag so the operator sees which value the parser rejected.");
+ Assert.That(stderr, Does.Contain("not a safe filename"),
+ "stderr must carry the guard's rejection fingerprint (`not a safe filename`) "
+ + "so the operator distinguishes label-shape rejection from other exit-2 causes.");
+ }
+
+ // Enumerates the hostile-label surface. Each entry is
+ // (label, scenario-slug); the slug feeds the scratch-dir suffix so
+ // parallel runs don't collide. Every entry must reject via
+ // IsSafeCompatLabel returning false.
+ private static object[] HostileCompatLabelCases()
+ {
+ return new object[]
+ {
+ new object[] { "../etc/passwd", "path-traversal-parent" },
+ new object[] { "Compat/../secret", "path-traversal-inline" },
+ new object[] { "sub/dir", "forward-slash" },
+ new object[] { "sub\\dir", "backslash" },
+ new object[] { ".hidden", "leading-dot" },
+ new object[] { "..", "double-dot" },
+ new object[] { " ", "whitespace-only" },
+ new object[] { "with space", "internal-space" },
+ new object[] { "label;drop", "semicolon-injection" },
+ new object[] { "label$var", "shell-metachar" },
+ new object[] { "label|pipe", "pipe" },
+ new object[] { "label\ttab", "control-tab" },
+ new object[] { new string('A', 65), "over-length" },
+ };
+ }
+
+ [Test]
+ public void Empty_compat_version_label_rejects_with_exit_2()
+ {
+ // An empty string satisfies the flag-has-a-value check (RequireValue
+ // returns "" rather than throwing) but must fail the safety guard
+ // (IsNullOrWhiteSpace short-circuits IsSafeCompatLabel to false).
+ // This is the boundary case for the length-lower-bound arm.
+ var repoRoot = FindRepoRoot();
+ var xmi = Path.Combine(repoRoot, XmiRelativePath);
+ var scratch = InitScratchWithLibraries("hostile-label-empty");
+ var (exitCode, _, stderr) = Run(
+ "--xmi", xmi,
+ "--output", scratch,
+ "--compat-version-label", "",
+ "--full-tree");
+ Assert.That(exitCode, Is.EqualTo(2),
+ $"An empty --compat-version-label must reject with usage-error exit 2. stderr:\n{stderr}");
+ Assert.That(stderr, Does.Contain("not a safe filename"),
+ "stderr must carry the guard's rejection fingerprint.");
+ }
+
+ // Safe-label positive cases — every documented default and every
+ // pattern the auto-derive machinery emits must PASS the guard so a
+ // ratchet of the regex (accidentally tightening it) can't silently
+ // regress the happy path.
+ [TestCase("Previous")]
+ [TestCase("v2_7")]
+ [TestCase("v10_15")]
+ [TestCase("Release-2.6.0")]
+ [TestCase("a")]
+ // 64-char boundary case: 26 upper + 26 lower + 10 digits + `_-` = 64.
+ // Exercises the length-upper-bound `label.Length > 64` arm at its
+ // exact accept-side boundary.
+ [TestCase("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789__")]
+ public void Safe_compat_version_label_accepts_and_reaches_full_tree_branch(string safeLabel)
+ {
+ // Boundary: the last case is exactly 64 chars — the length-upper
+ // bound. IsSafeCompatLabel rejects >64 but must accept ==64.
+ Assert.That(safeLabel.Length, Is.LessThanOrEqualTo(64),
+ "Test setup invariant — safe labels sit inside the length window.");
+
+ var repoRoot = FindRepoRoot();
+ var xmi = Path.Combine(repoRoot, XmiRelativePath);
+ // Sanitise the label for the scratch-dir suffix (the label may
+ // contain '.' which is legal in filenames but collides with the
+ // dir-slug convention). Replace non-alnum with '_'.
+ var scratchSuffix = "safe-label-" + System.Text.RegularExpressions.Regex.Replace(safeLabel, @"[^A-Za-z0-9]", "_");
+ if (scratchSuffix.Length > 96) scratchSuffix = scratchSuffix[..96];
+ var scratch = InitScratchWithLibraries(scratchSuffix);
+
+ var (exitCode, _, stderr) = Run(
+ "--xmi", xmi,
+ "--output", scratch,
+ "--compat-version-label", safeLabel,
+ "--full-tree");
+
+ Assert.That(exitCode, Is.Zero,
+ $"Safe --compat-version-label '{safeLabel}' must not be rejected by the guard. stderr:\n{stderr}");
+ Assert.That(stderr, Does.Not.Contain("not a safe filename"),
+ "stderr must not carry the guard rejection message for a safe label.");
+ }
+
+ [Test]
+ public void JsonDump_writes_the_dump_file_when_flag_supplied()
+ {
+ var repoRoot = FindRepoRoot();
+ var xmi = Path.Combine(repoRoot, XmiRelativePath);
+ var scratch = InitScratchWithLibraries("json-dump");
+ var dumpPath = Path.Combine(scratch, "model.json");
+ // --full-tree so the JSON-dump path is exercised without the
+ // zero-config auto-derive stepping in (which would resolve to the
+ // same-tree v2.7 XMI and successfully run delta mode, wasting time
+ // on a delta the test doesn't assert against).
+ var (exitCode, stdout, stderr) = Run("--xmi", xmi, "--output", scratch, "--json-dump", dumpPath, "--full-tree");
+ Assert.That(exitCode, Is.Zero,
+ $"--json-dump plus a valid XMI + output should succeed.\nstdout:\n{stdout}\nstderr:\n{stderr}");
+ Assert.That(File.Exists(dumpPath), Is.True,
+ "The dump file should exist at the requested path.");
+ var dumpContent = File.ReadAllText(dumpPath);
+ Assert.That(dumpContent.Length, Is.GreaterThan(1024),
+ "The dump content must be a non-trivial JSON tree, not an empty file.");
+ Assert.That(dumpContent.TrimStart(), Does.StartWith("{"),
+ "The dump content must start as a JSON object.");
+ Assert.That(stdout, Does.Contain("JSON dump: writing to"),
+ "stdout should echo the resolved dump path so the operator can verify placement.");
+ }
+
+ // --- helpers -----------------------------------------------------
+
+ private static string FindRepoRoot()
+ {
+ var current = new DirectoryInfo(AppContext.BaseDirectory);
+ while (current != null)
+ {
+ if (File.Exists(Path.Combine(current.FullName, SlnFileName)))
+ return current.FullName;
+ current = current.Parent;
+ }
+ throw new DirectoryNotFoundException(
+ $"Could not locate {SlnFileName} in any ancestor of {AppContext.BaseDirectory}.");
+ }
+
+ // Creates the scratch dir root without library subdirectories. Used
+ // when the test needs a "valid output-root path that lacks the
+ // library scaffolding" (exercises the throw path in
+ // RenderCommonClasses / RenderJsonComponents / RenderXmlComponents).
+ private static string InitScratch(string suffix)
+ {
+ var repoRoot = FindRepoRoot();
+ var path = Path.Combine(repoRoot, ScratchRoot, suffix);
+ if (Directory.Exists(path))
+ Directory.Delete(path, recursive: true);
+ Directory.CreateDirectory(path);
+ return path;
+ }
+
+ // Creates the scratch dir root PLUS the three library subdirectories
+ // the generator's full-tree branch guards against. Used for the
+ // happy-path adjacent cases (malformed XMI, JSON-dump).
+ private static string InitScratchWithLibraries(string suffix)
+ {
+ var path = InitScratch(suffix);
+ Directory.CreateDirectory(Path.Combine(path, "libraries", "MTConnect.NET-Common"));
+ Directory.CreateDirectory(Path.Combine(path, "libraries", "MTConnect.NET-JSON-cppagent"));
+ Directory.CreateDirectory(Path.Combine(path, "libraries", "MTConnect.NET-XML"));
+ return path;
+ }
+
+ private static (int ExitCode, string Stdout, string Stderr) Run(params string[] cliArgs)
+ {
+ var repoRoot = FindRepoRoot();
+ var psi = new ProcessStartInfo("dotnet")
+ {
+ WorkingDirectory = repoRoot,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+ psi.ArgumentList.Add("run");
+ psi.ArgumentList.Add("--no-build");
+ psi.ArgumentList.Add("--project");
+ psi.ArgumentList.Add(GeneratorProject);
+ psi.ArgumentList.Add("--");
+ foreach (var arg in cliArgs)
+ psi.ArgumentList.Add(arg);
+
+ using var proc = Process.Start(psi)
+ ?? throw new InvalidOperationException("Failed to start dotnet run for the generator.");
+
+ // Drain stdout and stderr concurrently — see ByteIdenticalRegenTests
+ // for the deadlock defence this pattern encodes.
+ var stdoutTask = proc.StandardOutput.ReadToEndAsync();
+ var stderrTask = proc.StandardError.ReadToEndAsync();
+ System.Threading.Tasks.Task.WhenAll(stdoutTask, stderrTask).GetAwaiter().GetResult();
+ proc.WaitForExit();
+ return (proc.ExitCode, stdoutTask.Result, stderrTask.Result);
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-Generator-Tests/DeltaCompatAndStatsTests.cs b/tests/MTConnect.NET-Generator-Tests/DeltaCompatAndStatsTests.cs
new file mode 100644
index 000000000..20a3d3993
--- /dev/null
+++ b/tests/MTConnect.NET-Generator-Tests/DeltaCompatAndStatsTests.cs
@@ -0,0 +1,352 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using NUnit.Framework;
+
+namespace MTConnect.NET_Generator_Tests
+{
+ ///
+ /// Delta-mode invariants that does not
+ /// cover: the Compat file's header + multi-namespace concentration, the
+ /// --compat-version-label default ("Previous" ), and the
+ /// stdout stats line's per-category counter reporting.
+ ///
+ ///
+ /// The plan-D4 contract for the concentrated Compat file is:
+ ///
+ /// - Prefixed with the TrakHound copyright + MIT licence header.
+ /// - Each concentrated block introduced by a
+ ///
// --- from <relative-path> --- divider and prefixed by
+ /// the source file's original body verbatim (including its
+ /// namespace X { ... } block, since multi-namespace
+ /// concentration is legal C#).
+ /// - Byte-identical to the source file's body for every UNCHANGED
+ /// entry (so
git diff shows zero drift after a rebuild).
+ ///
+ ///
+ ///
+ ///
+ /// The stdout stats line is the operator's telemetry surface: every
+ /// invocation prints Delta emission: added=N, changed=N,
+ /// unchanged-concentrated=N, removed-skipped=N, compat-files-written=N
+ /// so a spec bump's shape is grep-able. Pinning the format keeps the
+ /// operator-facing contract explicit; a silent rename of any counter
+ /// key would flip these tests RED.
+ ///
+ ///
+ [TestFixture]
+ public class DeltaCompatAndStatsTests
+ {
+ private const string SlnFileName = "MTConnect.NET.sln";
+ private const string GeneratorProject = "build/MTConnect.NET-SysML-Import";
+ private const string XmiRelativePath = "build/sysml-model/MTConnectSysMLModel.xml";
+ private const string ScratchRoot = ".claude/gen-test-out/delta-compat";
+
+ [Test]
+ public void Same_XMI_stats_line_reports_zero_added_changed_removed_and_positive_unchanged()
+ {
+ var repoRoot = FindRepoRoot();
+ var xmiPath = Path.Combine(repoRoot, XmiRelativePath);
+ var scratch = InitScratch("same-stats");
+
+ var (exitCode, stdout, stderr) = RunDelta(xmiPath, previousXmiPath: xmiPath,
+ compatLabel: "Baseline", output: scratch);
+ Assert.That(exitCode, Is.Zero, $"Generator exited non-zero.\nstdout:\n{stdout}\nstderr:\n{stderr}");
+
+ var stats = ParseStatsLine(stdout);
+ Assert.That(stats.Added, Is.Zero, "Same XMI on both sides: no ADDED files.");
+ Assert.That(stats.Changed, Is.Zero, "Same XMI on both sides: no CHANGED files.");
+ Assert.That(stats.RemovedSkipped, Is.Zero, "Same XMI on both sides: no REMOVED files.");
+ Assert.That(stats.UnchangedConcentrated, Is.GreaterThan(0),
+ "Same XMI on both sides: every emitted file goes into the UNCHANGED-concentrated partition.");
+ Assert.That(stats.CompatFilesWritten, Is.EqualTo(3),
+ "Same XMI on both sides: one Compat/.g.cs per library (three libraries).");
+ }
+
+ [Test]
+ public void Mutated_XMI_stats_line_reports_changed_gt_zero_and_zero_added_removed()
+ {
+ var repoRoot = FindRepoRoot();
+ var xmiPath = Path.Combine(repoRoot, XmiRelativePath);
+ var scratch = InitScratch("mutated-stats");
+
+ var originalXmi = File.ReadAllText(xmiPath);
+ const string original =
+ "unchangeable coordinate system that has machine zero as its origin.";
+ const string mutated =
+ "STATS_MUTATION_MARKER coordinate system that has machine zero as its origin.";
+ Assert.That(originalXmi, Does.Contain(original),
+ "XMI fixture must retain the stable mutation target; update the constant if the source moved.");
+
+ var mutatedXmi = originalXmi.Replace(original, mutated);
+ var mutatedXmiPath = Path.Combine(scratch, "MutatedSysML.xml");
+ File.WriteAllText(mutatedXmiPath, mutatedXmi);
+
+ var (exitCode, stdout, stderr) = RunDelta(mutatedXmiPath, previousXmiPath: xmiPath,
+ compatLabel: "PriorSpec", output: scratch);
+ Assert.That(exitCode, Is.Zero, $"Generator exited non-zero.\nstdout:\n{stdout}\nstderr:\n{stderr}");
+
+ var stats = ParseStatsLine(stdout);
+ Assert.That(stats.Changed, Is.GreaterThan(0),
+ "Description-only mutation must surface at least one CHANGED file.");
+ Assert.That(stats.Added, Is.Zero,
+ "Description-only mutation must not surface any ADDED files (no new type names).");
+ Assert.That(stats.RemovedSkipped, Is.Zero,
+ "Description-only mutation must not surface any REMOVED files (no dropped type names).");
+ Assert.That(stats.UnchangedConcentrated, Is.GreaterThan(0),
+ "Description-only mutation must leave the majority of files UNCHANGED-concentrated.");
+ }
+
+ [Test]
+ public void Default_compat_version_label_is_Previous_when_flag_omitted()
+ {
+ // The default value is documented in Program.cs as "Previous".
+ // Verify the emitted Compat/.g.cs file uses that label
+ // when --compat-version-label is not passed.
+ var repoRoot = FindRepoRoot();
+ var xmiPath = Path.Combine(repoRoot, XmiRelativePath);
+ var scratch = InitScratch("default-label");
+
+ var (exitCode, stdout, stderr) = RunDeltaWithoutLabel(xmiPath, previousXmiPath: xmiPath,
+ output: scratch);
+ Assert.That(exitCode, Is.Zero, $"Generator exited non-zero.\nstdout:\n{stdout}\nstderr:\n{stderr}");
+
+ var compatFiles = Directory
+ .EnumerateFiles(scratch, "*.g.cs", SearchOption.AllDirectories)
+ .Select(p => p.Replace('\\', '/'))
+ .Where(p => p.Contains("/Compat/"))
+ .ToList();
+
+ Assert.That(compatFiles.Count, Is.EqualTo(3),
+ "One Compat file per library (three libraries).");
+ foreach (var compatFile in compatFiles)
+ Assert.That(compatFile, Does.EndWith("/Compat/Previous.g.cs"),
+ "When --compat-version-label is omitted, the file name must default to 'Previous.g.cs'.");
+ }
+
+ [Test]
+ public void Compat_file_header_carries_copyright_and_licence_and_plan_D4_summary()
+ {
+ var repoRoot = FindRepoRoot();
+ var xmiPath = Path.Combine(repoRoot, XmiRelativePath);
+ var scratch = InitScratch("compat-header");
+
+ var (exitCode, stdout, stderr) = RunDelta(xmiPath, previousXmiPath: xmiPath,
+ compatLabel: "HeaderCheck", output: scratch);
+ Assert.That(exitCode, Is.Zero, $"Generator exited non-zero.\nstdout:\n{stdout}\nstderr:\n{stderr}");
+
+ var compatFile = Directory
+ .EnumerateFiles(scratch, "HeaderCheck.g.cs", SearchOption.AllDirectories)
+ .FirstOrDefault();
+ Assert.That(compatFile, Is.Not.Null,
+ "At least one Compat/HeaderCheck.g.cs should be present after the delta emission.");
+
+ var body = File.ReadAllText(compatFile!);
+ Assert.That(body, Does.Contain("// Copyright (c)"),
+ "Compat file must open with the TrakHound copyright header.");
+ Assert.That(body, Does.Contain("TrakHound Inc. licenses this file to you under the MIT license."),
+ "Compat file must carry the MIT licence banner.");
+ Assert.That(body, Does.Contain("plan D4"),
+ "Compat file must reference plan D4 in its provenance comment.");
+ Assert.That(body, Does.Contain("Byte-identical to the"),
+ "Compat file must promise byte-identical re-emission for concentrated types.");
+ }
+
+ [Test]
+ public void Compat_file_body_carries_from_divider_per_concentrated_entry()
+ {
+ var repoRoot = FindRepoRoot();
+ var xmiPath = Path.Combine(repoRoot, XmiRelativePath);
+ var scratch = InitScratch("compat-dividers");
+
+ var (exitCode, stdout, stderr) = RunDelta(xmiPath, previousXmiPath: xmiPath,
+ compatLabel: "Divider", output: scratch);
+ Assert.That(exitCode, Is.Zero, $"Generator exited non-zero.\nstdout:\n{stdout}\nstderr:\n{stderr}");
+
+ var compatFiles = Directory
+ .EnumerateFiles(scratch, "Divider.g.cs", SearchOption.AllDirectories)
+ .ToList();
+ Assert.That(compatFiles, Is.Not.Empty, "At least one Compat/Divider.g.cs must exist.");
+
+ foreach (var compatFile in compatFiles)
+ {
+ var body = File.ReadAllText(compatFile);
+ var dividers = Regex.Matches(body, @"^// --- from .+ ---$", RegexOptions.Multiline).Count;
+ Assert.That(dividers, Is.GreaterThan(0),
+ $"Compat file {compatFile} must have at least one `// --- from ---` divider.");
+ }
+ }
+
+ [Test]
+ public void Compat_file_body_preserves_multiple_namespace_blocks()
+ {
+ var repoRoot = FindRepoRoot();
+ var xmiPath = Path.Combine(repoRoot, XmiRelativePath);
+ var scratch = InitScratch("compat-namespaces");
+
+ var (exitCode, stdout, stderr) = RunDelta(xmiPath, previousXmiPath: xmiPath,
+ compatLabel: "Namespaces", output: scratch);
+ Assert.That(exitCode, Is.Zero, $"Generator exited non-zero.\nstdout:\n{stdout}\nstderr:\n{stderr}");
+
+ // MTConnect.NET-Common carries the highest namespace diversity;
+ // its Compat file must retain multiple `namespace X` blocks.
+ var commonCompat = Directory
+ .EnumerateFiles(Path.Combine(scratch, "libraries", "MTConnect.NET-Common"),
+ "Namespaces.g.cs", SearchOption.AllDirectories)
+ .FirstOrDefault();
+ Assert.That(commonCompat, Is.Not.Null,
+ "MTConnect.NET-Common/Compat/Namespaces.g.cs must exist.");
+
+ var body = File.ReadAllText(commonCompat!);
+ var namespaceMatches = Regex.Matches(body, @"^\s*namespace\s+MTConnect", RegexOptions.Multiline).Count;
+ Assert.That(namespaceMatches, Is.GreaterThan(1),
+ "Multi-namespace concentration is the whole point of plan D4's Compat design; "
+ + "a single-namespace Compat body signals the concatenation has collapsed the "
+ + "source-file boundaries.");
+ }
+
+ [Test]
+ public void Stats_line_uses_the_exact_documented_key_ordering_and_syntax()
+ {
+ var repoRoot = FindRepoRoot();
+ var xmiPath = Path.Combine(repoRoot, XmiRelativePath);
+ var scratch = InitScratch("stats-syntax");
+
+ var (exitCode, stdout, _) = RunDelta(xmiPath, previousXmiPath: xmiPath,
+ compatLabel: "Syntax", output: scratch);
+ Assert.That(exitCode, Is.Zero, "Same XMI on both sides is a valid delta invocation.");
+
+ // Exact regex so a rename of a counter key trips this test loudly.
+ var pattern = new Regex(
+ @"Delta emission: added=\d+, changed=\d+, unchanged-concentrated=\d+, "
+ + @"removed-skipped=\d+, compat-files-written=\d+");
+ Assert.That(pattern.IsMatch(stdout), Is.True,
+ "stdout must carry the operator-facing stats line in its documented shape. "
+ + "Renaming any counter key breaks the operator's grep contract.");
+ }
+
+ // --- helpers -----------------------------------------------------
+
+ private sealed class StatsLine
+ {
+ public int Added;
+ public int Changed;
+ public int UnchangedConcentrated;
+ public int RemovedSkipped;
+ public int CompatFilesWritten;
+ }
+
+ private static StatsLine ParseStatsLine(string stdout)
+ {
+ var match = Regex.Match(stdout,
+ @"Delta emission: added=(?\d+), changed=(?\d+), "
+ + @"unchanged-concentrated=(?\d+), removed-skipped=(?\d+), "
+ + @"compat-files-written=(?\d+)");
+ if (!match.Success)
+ throw new AssertionException(
+ "stdout does not carry the expected 'Delta emission: ...' stats line.\n" + stdout);
+ return new StatsLine
+ {
+ Added = int.Parse(match.Groups["a"].Value),
+ Changed = int.Parse(match.Groups["c"].Value),
+ UnchangedConcentrated = int.Parse(match.Groups["u"].Value),
+ RemovedSkipped = int.Parse(match.Groups["r"].Value),
+ CompatFilesWritten = int.Parse(match.Groups["w"].Value)
+ };
+ }
+
+ private static string FindRepoRoot()
+ {
+ var current = new DirectoryInfo(AppContext.BaseDirectory);
+ while (current != null)
+ {
+ if (File.Exists(Path.Combine(current.FullName, SlnFileName)))
+ return current.FullName;
+ current = current.Parent;
+ }
+ throw new DirectoryNotFoundException(
+ $"Could not locate {SlnFileName} in any ancestor of {AppContext.BaseDirectory}.");
+ }
+
+ private static string InitScratch(string suffix)
+ {
+ var repoRoot = FindRepoRoot();
+ var path = Path.Combine(repoRoot, ScratchRoot, suffix);
+ if (Directory.Exists(path))
+ Directory.Delete(path, recursive: true);
+ Directory.CreateDirectory(path);
+ Directory.CreateDirectory(Path.Combine(path, "libraries", "MTConnect.NET-Common"));
+ Directory.CreateDirectory(Path.Combine(path, "libraries", "MTConnect.NET-JSON-cppagent"));
+ Directory.CreateDirectory(Path.Combine(path, "libraries", "MTConnect.NET-XML"));
+ return path;
+ }
+
+ private static (int ExitCode, string Stdout, string Stderr) RunDelta(
+ string xmiPath, string previousXmiPath, string compatLabel, string output)
+ {
+ var repoRoot = FindRepoRoot();
+ var psi = BuildStartInfo(repoRoot);
+ psi.ArgumentList.Add("run");
+ psi.ArgumentList.Add("--no-build");
+ psi.ArgumentList.Add("--project");
+ psi.ArgumentList.Add(GeneratorProject);
+ psi.ArgumentList.Add("--");
+ psi.ArgumentList.Add("--xmi");
+ psi.ArgumentList.Add(xmiPath);
+ psi.ArgumentList.Add("--previous-xmi");
+ psi.ArgumentList.Add(previousXmiPath);
+ psi.ArgumentList.Add("--compat-version-label");
+ psi.ArgumentList.Add(compatLabel);
+ psi.ArgumentList.Add("--output");
+ psi.ArgumentList.Add(output);
+ return Execute(psi);
+ }
+
+ private static (int ExitCode, string Stdout, string Stderr) RunDeltaWithoutLabel(
+ string xmiPath, string previousXmiPath, string output)
+ {
+ var repoRoot = FindRepoRoot();
+ var psi = BuildStartInfo(repoRoot);
+ psi.ArgumentList.Add("run");
+ psi.ArgumentList.Add("--no-build");
+ psi.ArgumentList.Add("--project");
+ psi.ArgumentList.Add(GeneratorProject);
+ psi.ArgumentList.Add("--");
+ psi.ArgumentList.Add("--xmi");
+ psi.ArgumentList.Add(xmiPath);
+ psi.ArgumentList.Add("--previous-xmi");
+ psi.ArgumentList.Add(previousXmiPath);
+ psi.ArgumentList.Add("--output");
+ psi.ArgumentList.Add(output);
+ return Execute(psi);
+ }
+
+ private static ProcessStartInfo BuildStartInfo(string repoRoot) => new("dotnet")
+ {
+ WorkingDirectory = repoRoot,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ private static (int ExitCode, string Stdout, string Stderr) Execute(ProcessStartInfo psi)
+ {
+ using var proc = Process.Start(psi)
+ ?? throw new InvalidOperationException("Failed to start dotnet run for the generator.");
+
+ // Drain stdout and stderr concurrently — see ByteIdenticalRegenTests
+ // for the deadlock defence this pattern encodes.
+ var stdoutTask = proc.StandardOutput.ReadToEndAsync();
+ var stderrTask = proc.StandardError.ReadToEndAsync();
+ System.Threading.Tasks.Task.WhenAll(stdoutTask, stderrTask).GetAwaiter().GetResult();
+ proc.WaitForExit();
+ return (proc.ExitCode, stdoutTask.Result, stderrTask.Result);
+ }
+ }
+}
diff --git a/tests/MTConnect.NET-Generator-Tests/DeltaRegenTests.cs b/tests/MTConnect.NET-Generator-Tests/DeltaRegenTests.cs
new file mode 100644
index 000000000..1b2cd1167
--- /dev/null
+++ b/tests/MTConnect.NET-Generator-Tests/DeltaRegenTests.cs
@@ -0,0 +1,258 @@
+// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
+// TrakHound Inc. licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using NUnit.Framework;
+
+namespace MTConnect.NET_Generator_Tests
+{
+ ///