Skip to content

test(dry-generator): consolidate V2_6_V2_7 into topic-first layout - #233

Draft
ottobolyos wants to merge 42 commits into
TrakHound:masterfrom
ottobolyos:feat/dry-generator-phase1-consolidation
Draft

test(dry-generator): consolidate V2_6_V2_7 into topic-first layout#233
ottobolyos wants to merge 42 commits into
TrakHound:masterfrom
ottobolyos:feat/dry-generator-phase1-consolidation

Conversation

@ottobolyos

@ottobolyos ottobolyos commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Executes the DRY-generator campaign in full on a single branch. Phase 1
(topic-first fixture consolidation) and Phase 2 (three permanent
reflection / source-scan guards) land the topology rewrite for
tests/MTConnect.NET-Common-Tests/; Phase 3 (Scriban template
consolidation via Shape A / B) lands two byte-identity-preserving
mergers on build/MTConnect.NET-SysML-Import/ plus a new
tests/MTConnect.NET-Generator-Tests/ project carrying the current-XMI
byte-identical regen guard; Phase 4 (delta-driven emitter +
generator-fidelity audit) closes the pre-existing baseline drift and
lands the --previous-xmi CLI plus the mutated-XMI cross-verify;
Phase 5 (verification pass) lands as further commits on this branch.

Scope decision (ottobolyos-approved 2026-08-20): a single PR keeps the
convention rewrite, the template consolidation, and the delta emitter
reviewable as one coherent unit. The earlier "Phases 3 – 5 ship as
separate campaign PRs" plan is superseded.

Motivation

Two intertwined pressures drive this campaign:

  • Per-version-folder rot. The old V<major>_<minor>/ fixture
    convention forces every spec-version bump to author a whole new
    folder of fixtures. The topic-first convention lets the next bump ADD
    a [TestCase(MTConnectVersions.VersionXY)] line and be done.
  • Per-version-template rot. The Scriban template family had two
    latent duplication axes: near-identical partial-class hosts that
    differed only in class name and doc summary, and paired
    descriptions-class templates that differed only in the Get(...)
    overload's parameter type. Every future version bump the campaign
    ships would double the maintenance surface unless the templates are
    collapsed first.

Without both landing, MTConnect Standard v2.8 support would ship as
another V2_8/ fixture folder plus another set of per-version template
copies — every new spec release compounding the maintenance cost.

Scope: all five phases in this PR

  • Phase 1 — topic-first fixture consolidation. Deletes
    V2_6_V2_7/; lands six new topic fixtures at their canonical
    topic-first location.
  • Phase 2 — permanent guards. Three complementary reflection /
    source-scan checks keep the new convention machine-enforced.
  • Phase 3 — template consolidation via Shape A / B. Two
    byte-identity-preserving mergers land on the Scriban template family;
    a new test project carries the current-XMI byte-identical regen
    guard.
  • Phase 4 — delta-driven regen mechanism (--previous-xmi CLI flag
    on the SysML importer) plus the SysML-XMI 1:1 fidelity audit that
    closes the Phase 3.1 baseline drift (15 orphan .g.cs files
    triaged, 63 whitespace-drift files refreshed, byte-identical guard
    flipped from [Explicit] to normal [Test]).
  • Phase 5 — verification pass (full-tree cross-check + wire-format
    matrix). Lands on this branch once Phase 4 is stable.

Approach

The design decisions applied are documented as inline choices on each
fixture / template.

Phase 1 / 2 — fixture topology

  • D1 — Version-gate mechanism: [TestCaseSource(typeof(MTConnectVersionMatrix), nameof(MTConnectVersionMatrix.All))] on each behavioral method, gated with Assume.That(v >= MTConnectVersions.Version??). Rows below the floor surface as Inconclusive in the test explorer, making "gated out" visually distinct from "ran and passed". MTConnectVersionMatrix.All already ships at tests/MTConnect.NET-Common-Tests/TestHelpers/MTConnectVersionMatrix.cs; the reflection-driven matrix picks up future version constants automatically without per-test edits.
  • D5 — Rollout order: single-PR big-bang. At HEAD there is exactly one per-version folder (V2_6_V2_7/); a staged rollout would have no distinguishing value.

The MTConnectVersions constant tests are kept as plain [Test] since they exercise structural invariants of the MTConnectVersions type itself (constant values, distinctness, monotonicity), not per-version behavioral gates.

Phase 3 — template consolidation

  • Shape A — templates whose per-call variants differ only in
    surface parameters (class name, doc line) collapse to one template
    with a Scriban comment header naming the version range it applies to.
    No if mtc_version >= … gates inside; every version consumes the
    same emission.
  • Shape B — templates whose per-caller variants differ in small
    fragments collapse to one template with {{ if … }} gates around the
    differentiated fragments. Kept below the ~5-gate over-abstraction
    ceiling per the plan's Phase 3.2 rule.

Every consolidation is atomic (its own commit) and verified by the new
Regen_is_deterministic_across_two_invocations guard in
tests/MTConnect.NET-Generator-Tests/. Byte-identity of every emitted
.g.cs file against the committed tree for the consolidated templates
is preserved.

Phase 3.1 finding — pre-existing baseline drift (RESOLVED in Phase 4)

The Phase 3.1 dry-run surfaced pre-existing drift between the
current-XMI regen and the committed libraries/**/*.g.cs tree: 15
hand-authored interface / observation files the current generator no
longer emits, plus 63 files (predominantly trailing-blank-line
whitespace) that differ from the current-XMI regen output. The
strict-baseline guard shipped in Phase 3 marked [Explicit]
documenting the drift; Phase 4 resolves it in full (per-fix breakdown
below) and flips the guard to plain [Test].

Phase 4 — SysML-XMI 1:1 fidelity audit + delta-driven regen

Phase 4 closes Phase 3.1 in two atomic movements plus the delta CLI.

Fidelity audit resolution. The 15 orphaned .g.cs files decompose as:

  • 10 Pallet measurement interfaces (IHeightMeasurement,
    ILengthMeasurement, ILoadedHeightMeasurement, ILoadedLengthMeasurement,
    ILoadedSwingMeasurement, ILoadedWeightMeasurement,
    ILoadedWidthMeasurement, ISwingMeasurement, IWeightMeasurement,
    IWidthMeasurement) — the concrete Pallet class emissions
    (HeightMeasurement : Measurement, IHeightMeasurement, etc.) each
    reference an I{Name} interface that MeasurementModel .RenderInterface() returned null for. Fixed by adding a new
    Pallets.MeasurementInterface.scriban template and wiring
    MeasurementModel.RenderInterface() to invoke it. All 10 interfaces
    now emit deterministically from the generator; the previously
    hand-committed files are refreshed to the generator's output
    (single per-file diff: copyright year 2024 → 2025 from the template).
  • 5 orphaned files (Assets/Files/IAbstractFile.g.cs,
    Assets/Files/IFileArchetype.g.cs,
    Devices/Configurations/IRelationship.g.cs,
    Observations/Events/NetworkWireless.g.cs,
    Observations/Events/SensorStateDetect.g.cs) — dead code from
    historical renames (superseded by IAbstractFileAsset,
    IFileArchetypeAsset, IConfigurationRelationship, etc.) or
    spurious enum emissions with no consumers. Verified via codebase-wide
    grep that zero .cs files reference any of them. Deleted.

The 63 whitespace-drift .g.cs files are refreshed to current
generator output in a mechanical bulk commit (regen → copy over
committed tree → commit). No template changes; the drift was
accumulated delta from earlier template touches never mirrored into
the committed tree.

A late-stage fidelity finding (fix(generator): drop stale ToolingMeasurement.Code inheritance override) removes an obsolete
MarkInheritedProperties hand-stitched seed that force-added Code
to Assets.CuttingTools.ToolingMeasurement's inherited-name set on
the design assumption that Measurement.g.cs under CuttingTools was
hand-frozen. The runtime now emits that file via the sharedMeasurement
injection (which uses the Pallet Measurement class as base — no Code),
so the override produced a CS0109 build regression under
TreatWarningsAsErrors=true post-refresh. Override dropped, the
default inheritance walk correctly determines ToolingMeasurement.Code
does not hide anything, no new keyword emitted, build green.

After all three commits: byte-identical guard flipped from
[Test, Explicit] to plain [Test] and passes on every default
dotnet test invocation.

Delta CLI (--previous-xmi). Two new opt-in flags on the SysML importer:

  • --previous-xmi <path> — the prior spec-version XMI file.
  • --compat-version-label <label> — Compat file basename (default
    Previous).

When --previous-xmi is supplied, the generator renders both XMIs to
isolated scratch directories under %TMP%/mtc-sysml-delta-<guid>,
diffs the emitted .g.cs trees at the file level, and emits only:

  • ADDED files (present in new tree only) → written to their normal
    libraries/<library>/... location.
  • CHANGED files (present in both trees, different bytes) → written
    to their normal location (new tree's version).
  • REMOVED files (present in prev tree only) → skipped entirely.
  • UNCHANGED files (present in both, identical bytes) →
    concentrated into one Compat/<label>.g.cs per library, with each
    block still carrying its own namespace X { ... } declaration
    (multi-namespace concentration is legal C#). Byte-identity to the
    deterministically-emitted full-tree output preserved per plan D4
    invariant.

The full-tree mode (default when --previous-xmi is omitted) is
preserved bit-for-bit. Delta mode is orthogonal, opt-in, and rolls
back cleanly to full-tree by dropping the flag from the automation
script.

The plan's original historical-XMI-iteration verification is out of
scope (ottobolyos-approved 2026-08-20 — the build/sysml-model
submodule ships a single snapshot per spec bump). Replaced by
DeltaRegenTests.cs which produces a mutated XMI programmatically
(swapping one ownedComment body) and asserts the delta captures
the mutation surgically.

What changed

Phase 1

  • tests/MTConnect.NET-Common-Tests/V2_6_V2_7/ deleted (7 files, 606 LOC).
  • 6 new topic fixtures land at their canonical topic-first location:
    • MTConnectVersionsTests.cs (project root) — 5 constant-value invariants of the MTConnectVersions type.
    • Devices/Components/ComponentTests.cs — 4 methods (CuttingTorch, Electrode, PinTool, ToolHolder).
    • Devices/DataItems/DataItemTypeTests.cs — 6 v2.6 methods + 1 v2.7 parametric sweep (8 [TestCase] rows × 17 versions cross-multiplied).
    • Devices/Configurations/ConfigurationTests.cs — 16 methods across DataSet base, Axis / Origin / Rotation / Scale / Translation and their *DataSet siblings plus the corresponding Abstract* bases.
    • Observations/SampleObservationTests.cs — WaterHardness sample-envelope round-trip.
    • Enums/EnumArmTests.cs — MediaType.QIF_MBD arm-presence check.

Phase 2

  • tests/MTConnect.NET-Common-Tests/DryGenerator/AssertionParityTests.cs — RED-first coverage-parity guard. Holds an inline migration map of the 34 pre-migration method entries and asserts every one has a live post-migration home in the topic layout. GREEN after the topic fixtures land; permanent regression guard.
  • tests/MTConnect.NET-Common-Tests/DryGenerator/PerVersionFolderProhibitionTests.cs — permanent topology guard. Three complementary checks fire RED against any V<N_M>/ directory, V<N_M>*Tests.cs file, or V<N_M>*Tests fixture class regrowth. bin//obj/ are excluded so a package-cache artifact matching the pattern cannot trip a false positive; a HistoricalAnchors allowlist accommodates deliberately-pinned fixtures (empty at HEAD).
  • tests/MTConnect.NET-Common-Tests/DryGenerator/TopicFixtureCoverageTests.cs — permanent topic-fixture coverage guard. For each of the 23 spec-anchor types migrated out of the deprecated V2_6_V2_7/ folder, a source-scan check asserts the type name appears at least once in its canonical topic-fixture source file. Uses whole-word regex match so shorter anchors (Axis) do not spuriously match longer type names (AbstractAxis, AxisDataSet). Sibling floor + on-disk-topology guards catch anchor-set shrinkage and file-move regressions. Complements AssertionParityTests (method-name resolution) by adding source-text-mention resolution, catching the rename-plus-body-swap regression a name-only guard would miss.
  • docs/testing/version-matrix-convention.md (new) — the single-source-of-truth reference for the topic-first single-file-per-topic convention: the rule, how to add a fixture for a new spec version, when to keep plain [Test], how HistoricalAnchors and AssertionParityTests work.
  • docs/testing/v2-6.md, docs/testing/v2-7.md, docs/testing.md — repointed every "Pinned test" cell + the "Test classes" narrative at the post-migration home (Devices/DataItems/DataItemTypeTests, Devices/Components/ComponentTests, Devices/Configurations/ConfigurationTests, Observations/SampleObservationTests, Enums/EnumArmTests, MTConnectVersionsTests). Cross-links to version-matrix-convention.md from every entry point so a future v2.8 contributor finds the recipe.
  • tests/MTConnect.NET-Common-Tests/Reflection/RegeneratedTypesCoverageTests.cs — two inline comments rewritten to reflect the topic-first layout (previously pointed at the deleted V2_6_V2_7/ folder).

Phase 3

  • New test project tests/MTConnect.NET-Generator-Tests/ wired via dotnet sln add. Two guards ship side-by-side:
    • Regen_is_deterministic_across_two_invocations — the everyday [Test]. Regenerates against the current XMI twice into separate scratch directories under .claude/gen-test-out/ (persistent, gitignored path per repo convention) and asserts the two emitted .g.cs trees are byte-identical. Locks in the determinism guarantee every template consolidation relies on.
    • Current_XMI_regen_matches_committed_g_cs_tree — strict-baseline guard. Ships as [Test, Explicit] in Phase 3 and flips to plain [Test] in Phase 4 after the fidelity audit closes the drift.
  • Xml Shape-A consolidationXmlCuttingItem.scriban + XmlCuttingToolLifeCycle.scriban (22 LOC each, differing only in emitted class name and doc summary) collapse to a single XmlMeasurementArrayHost.scriban. Each call site in Xml/TemplateRenderer.cs supplies class_name, summary, and the shared types array. Byte-identity of both libraries/MTConnect.NET-XML/Assets/CuttingTools/XmlCutting{Item,ToolLifeCycle}.g.cs preserved.
  • CSharp Shape-B consolidationEnumDescriptions.scriban + EnumStringDescriptions.scriban (38 LOC each, differing in three fragments: class-doc wording, Get(...) parameter type, Get(...) doc summary) collapse to a single EnumOrStringDescriptions.scriban with three {{ if is_string }} gates. EnumStringModel exposes IsString => true; EnumModel and ObservationModel rely on Scriban's null-is-falsy default. Byte-identity of every emitted *Descriptions.g.cs file preserved.
  • Template inventory: 23 → 21 (net −2). One further consolidation candidate (Devices.ComponentType.scriban + Devices.CompositionType.scriban) was scoped out this pass: a merger requires six independent gate points (namespace, base class, kind noun, is_organizer, ctor format, abstract keyword), exceeding the plan's ≤5-gate over-abstraction ceiling without proportionate parameterization surgery.

Phase 4

Six atomic commits close the Phase 3.1 baseline drift and land the
delta CLI:

  • fix(generator): emit Pallet measurement interfaces — adds
    build/MTConnect.NET-SysML-Import/CSharp/Templates/Pallets.MeasurementInterface.scriban;
    wires MeasurementModel.RenderInterface() to invoke it. Regens
    the 10 previously hand-committed I{Name}Measurement.g.cs files
    under libraries/MTConnect.NET-Common/Assets/Pallet/.
  • chore(generator): remove stale orphan .g.cs files — deletes
    the 5 zero-consumer orphan files (IAbstractFile.g.cs,
    IFileArchetype.g.cs, IRelationship.g.cs, NetworkWireless.g.cs,
    SensorStateDetect.g.cs).
  • chore(generator): refresh .g.cs to current generator output
    regens the committed tree to close the 63-file whitespace-drift set.
    Mechanical bulk refresh; no template changes.
  • test(generator): flip current-XMI byte-identical guard from Explicit to Test
    Current_XMI_regen_matches_committed_g_cs_tree now runs on every
    default dotnet test invocation, blocking any future template or
    committed-tree drift.
  • feat(generator): add --previous-xmi flag for delta-driven regen
    new --previous-xmi <path> + --compat-version-label <label> CLI
    flags on build/MTConnect.NET-SysML-Import/Program.cs. Delta mode
    renders both XMIs to scratch, diffs at file level, and emits ADDED /
    CHANGED to normal paths + UNCHANGED concentrated to Compat/<label>.g.cs
    per library. REMOVED files skipped.
  • test(generator): mutated-XMI cross-verify of delta emission
    tests/MTConnect.NET-Generator-Tests/DeltaRegenTests.cs. Two tests:
    a same-XMI-both-sides guard (asserts every file concentrates into
    Compat) and a mutated-XMI cross-verify (rewrites one XMI
    ownedComment body, asserts the delta surfaces exactly two changed
    files — the enum + its Descriptions — with the mutation marker in
    each, no marker in Compat files).
  • fix(generator): drop stale ToolingMeasurement.Code inheritance override
    removes the obsolete MarkInheritedProperties seed that force-added
    Code to ToolingMeasurement's inherited-name set on a stale design
    assumption. Restores byte-identity + fixes CS0109 build regression
    post-refresh.
  • test(generator): skip bin/obj generated files in byte-identity walks
    filters /bin/ and /obj/ from the .g.cs walk in both fixtures
    so MSBuild-generated intermediates (GlobalUsings.g.cs, etc.) don't
    spuriously flip the guards RED on already-built hosts.

Migration script

The migration is driven by a repository-conventional migration script (gitignored, not part of the shipped tree). The script validates that every hand-authored topic file is in place, emits renames.tsv and citations.tsv audit artifacts, git rms the deprecated folder, and runs a post-migration sanity build. Topic-file authoring itself remains a hand step: version-gate mechanism selection and spec-source citation verification are hand-checked, not scriptable.

Parity guarantee

Three permanent guards cover the fixture migration:

  • AssertionParityTests enumerates every [Test] / [TestCase] / [TestCaseSource] method in the test assembly (excluding the deprecated V2_6_V2_7 namespace) and asserts every entry in the 34-entry migration map resolves to a live method. Two rename conventions apply:

    • strip _in_v2_6 / _in_v2_7 suffixes — the version is now a property of the [TestCaseSource(MTConnectVersionMatrix.All)] matrix rather than encoded in the method name;
    • strip the V2_7_ prefix on V2_7_DataItem_constructs_with_correct_metadata — same rationale.

    Every other migration is an identity rename. A separate Migration_map_covers_the_full_baseline_of_34_entries guard fires RED if the map itself is trimmed below the 34-entry baseline.

  • PerVersionFolderProhibitionTests enforces the folder-topology invariant: no V<N_M>/ directory, V<N_M>*Tests.cs file, or V<N_M>*Tests fixture class may return to the tree.

  • TopicFixtureCoverageTests enforces the coverage-parity invariant at source-text level: for every migrated spec-anchor type, the designated topic-fixture source file must name the type at least once. Catches the rename-plus-body-swap regression a name-only guard would miss.

For Phase 3, byte-identity is enforced by Regen_is_deterministic_across_two_invocations in the new generator-tests project. Phase 4 adds Current_XMI_regen_matches_committed_g_cs_tree (now plain [Test], not [Explicit]) plus the two DeltaRegenTests guards.

Test renames

Verbatim table generated from the migration script's rename audit (34 rows, one per pre-migration [Test] / [TestCase] method entry). case_map = identity means the [TestCase] parameter tuples migrated verbatim; the one non-identity row (V2_7_DataItem_constructs_with_correct_metadata) cross-multiplies its 8 (Type, TypeId, Category) tuples with MTConnectVersionMatrix.All under the new matrix mechanism.

Old file Old method New file New method Case map Rationale
V2_6ComponentAndEnumTests.cs CuttingTorchComponent_constructs_with_correct_type Devices/Components/ComponentTests.cs CuttingTorchComponent_constructs_with_correct_type identity identity migration
V2_6ComponentAndEnumTests.cs ElectrodeComponent_constructs_with_correct_type Devices/Components/ComponentTests.cs ElectrodeComponent_constructs_with_correct_type identity identity migration
V2_6ComponentAndEnumTests.cs MediaType_QIF_MBD_value_present_in_v2_6 Enums/EnumArmTests.cs MediaType_QIF_MBD_value_present identity strip _in_v2_6 suffix; version is now matrix property
V2_6DataItemTypeTests.cs AssetAddedDataItem_constructs_with_event_metadata Devices/DataItems/DataItemTypeTests.cs AssetAddedDataItem_constructs_with_event_metadata identity identity migration
V2_6DataItemTypeTests.cs AssetAddedDataItem_with_deviceId_produces_qualified_id Devices/DataItems/DataItemTypeTests.cs AssetAddedDataItem_with_deviceId_produces_qualified_id identity identity migration
V2_6DataItemTypeTests.cs AssociatedAssetIdDataItem_constructs_with_event_metadata Devices/DataItems/DataItemTypeTests.cs AssociatedAssetIdDataItem_constructs_with_event_metadata identity identity migration
V2_6DataItemTypeTests.cs AssetAddedDataItem_inherits_from_DataItem Devices/DataItems/DataItemTypeTests.cs AssetAddedDataItem_inherits_from_DataItem identity identity migration
V2_6DataItemTypeTests.cs AssociatedAssetIdDataItem_inherits_from_DataItem Devices/DataItems/DataItemTypeTests.cs AssociatedAssetIdDataItem_inherits_from_DataItem identity identity migration
V2_6DataItemTypeTests.cs AssetChangedDataItem_description_narrowed_in_v2_6 Devices/DataItems/DataItemTypeTests.cs AssetChangedDataItem_description_narrowed identity strip _in_v2_6 suffix; version is now matrix property
V2_7DataItemTypeTests.cs V2_7_DataItem_constructs_with_correct_metadata Devices/DataItems/DataItemTypeTests.cs DataItem_constructs_with_correct_metadata (Type,TypeId,Category) tuples cross-multiplied with MTConnectVersionMatrix.All strip V2_7_ prefix; version is now matrix property
MTConnectVersionsTests.cs Version26_constant_equals_2_6 MTConnectVersionsTests.cs Version26_constant_equals_2_6 identity identity migration; kept plain [Test] (constant-value invariant, not version-sensitive)
MTConnectVersionsTests.cs Version27_constant_equals_2_7 MTConnectVersionsTests.cs Version27_constant_equals_2_7 identity identity migration; kept plain [Test] (constant-value invariant, not version-sensitive)
MTConnectVersionsTests.cs Max_equals_Version27 MTConnectVersionsTests.cs Max_equals_Version27 identity identity migration; kept plain [Test] (constant-value invariant, not version-sensitive)
MTConnectVersionsTests.cs Every_published_version_constant_is_distinct_and_monotonic MTConnectVersionsTests.cs Every_published_version_constant_is_distinct_and_monotonic identity identity migration; kept plain [Test] (structural invariant)
MTConnectVersionsTests.cs Version19_field_does_not_exist MTConnectVersionsTests.cs Version19_field_does_not_exist identity identity migration; kept plain [Test] (structural invariant)
V2_7ComponentTests.cs PinToolComponent_constructs_with_correct_type Devices/Components/ComponentTests.cs PinToolComponent_constructs_with_correct_type identity identity migration
V2_7ComponentTests.cs ToolHolderComponent_constructs_with_correct_type Devices/Components/ComponentTests.cs ToolHolderComponent_constructs_with_correct_type identity identity migration
V2_7ConfigurationDataSetTests.cs DataSet_base_constructs_and_implements_IDataSet Devices/Configurations/ConfigurationTests.cs DataSet_base_constructs_and_implements_IDataSet identity identity migration
V2_7ConfigurationDataSetTests.cs AxisDataSet_has_xyz_fields_and_implements_IDataSet Devices/Configurations/ConfigurationTests.cs AxisDataSet_has_xyz_fields_and_implements_IDataSet identity identity migration
V2_7ConfigurationDataSetTests.cs OriginDataSet_has_xyz_fields_and_implements_IDataSet Devices/Configurations/ConfigurationTests.cs OriginDataSet_has_xyz_fields_and_implements_IDataSet identity identity migration
V2_7ConfigurationDataSetTests.cs RotationDataSet_has_abc_fields_and_implements_IDataSet Devices/Configurations/ConfigurationTests.cs RotationDataSet_has_abc_fields_and_implements_IDataSet identity identity migration
V2_7ConfigurationDataSetTests.cs ScaleDataSet_implements_IDataSet Devices/Configurations/ConfigurationTests.cs ScaleDataSet_implements_IDataSet identity identity migration
V2_7ConfigurationDataSetTests.cs TranslationDataSet_implements_IDataSet Devices/Configurations/ConfigurationTests.cs TranslationDataSet_implements_IDataSet identity identity migration
V2_7ConfigurationDataSetTests.cs Axis_inherits_AbstractAxis_and_constructs Devices/Configurations/ConfigurationTests.cs Axis_inherits_AbstractAxis_and_constructs identity identity migration
V2_7ConfigurationDataSetTests.cs Origin_inherits_AbstractOrigin Devices/Configurations/ConfigurationTests.cs Origin_inherits_AbstractOrigin identity identity migration
V2_7ConfigurationDataSetTests.cs Rotation_inherits_AbstractRotation Devices/Configurations/ConfigurationTests.cs Rotation_inherits_AbstractRotation identity identity migration
V2_7ConfigurationDataSetTests.cs Scale_inherits_AbstractScale Devices/Configurations/ConfigurationTests.cs Scale_inherits_AbstractScale identity identity migration
V2_7ConfigurationDataSetTests.cs Translation_inherits_AbstractTranslation Devices/Configurations/ConfigurationTests.cs Translation_inherits_AbstractTranslation identity identity migration
V2_7ConfigurationDataSetTests.cs AbstractAxis_is_abstract Devices/Configurations/ConfigurationTests.cs AbstractAxis_is_abstract identity identity migration
V2_7ConfigurationDataSetTests.cs AbstractOrigin_is_abstract Devices/Configurations/ConfigurationTests.cs AbstractOrigin_is_abstract identity identity migration
V2_7ConfigurationDataSetTests.cs AbstractRotation_is_abstract Devices/Configurations/ConfigurationTests.cs AbstractRotation_is_abstract identity identity migration
V2_7ConfigurationDataSetTests.cs AbstractScale_is_abstract Devices/Configurations/ConfigurationTests.cs AbstractScale_is_abstract identity identity migration
V2_7ConfigurationDataSetTests.cs AbstractTranslation_is_abstract Devices/Configurations/ConfigurationTests.cs AbstractTranslation_is_abstract identity identity migration
V2_7SampleObservationTests.cs WaterHardness_sample_observation_round_trip Observations/SampleObservationTests.cs WaterHardness_sample_observation_round_trip identity identity migration

Depends on

None. This PR is orthogonal to every other open campaign PR — it touches only tests/MTConnect.NET-Common-Tests/, docs/testing/, build/MTConnect.NET-SysML-Import/, and adds the new tests/MTConnect.NET-Generator-Tests/ project. The eventual MTConnect Standard v2.8 bump PR depends on THIS.

Phase 5 — verification pass

Full-tree cross-check + wire-format matrix + all-categories dotnet test sweep on bluefin at head 619dd1bc, 9 assemblies green (Common 3987, XML 131, JSON 68, MqttRelay 62, Docs 67, HTTP 112, JSON-cppagent 363, SHDR 35, Compliance 225 — 5,050 tests / 0 failed / 0 skipped, filtered-excluding the documented Docs-Tests bluefin Playwright flake covered by #227's baseline).

Phase 5 also lands four categories of atomic test / doc / generator hardening on top of the Phase 4 head:

  • Generator delta emitter hardeningfix(generator): delta emitter deletes stale UNCHANGED / REMOVED files (delta CLI now cleans up files no longer emitted by the current XMI, closing a stale-artifact class the Phase 4 baseline left latent).
  • Generator CLI failure-path coveragetest(generator): CLI failure-path coverage for Program.cs, test(generator): delta-mode Compat body + stats-line invariants, test(generator): relax malformed-XMI test to non-zero exit contract (tightens the delta-mode invariants pinned in Phase 4 and rounds out the failure-mode surface of Program.cs).
  • Test-harness robustnessfix(tests): drain stdout/stderr concurrently + tighten TopicAnchors floor (removes a bluefin-observed deadlock in the process-driver + raises the TopicFixtureCoverageTests coverage floor to reflect the actual anchor set at HEAD).
  • Documentationdocs(generator): document delta CLI + Stryker gate + generator-tests tier (docs/testing/mutation-testing.md, docs/testing/version-matrix-convention.md, and the two per-version testing pages document the new delta CLI flag, the Stryker gate, and the generator-tests tier).

Phase 5 — mutation-testing framework adoption

chore(tests): adopt Stryker.NET mutation-testing framework lands stryker-config.json at the repo root wired to MTConnect.NET-Common as the pilot target (net8.0, --mutation-level Complete, .g.cs generated surfaces excluded from mutation, thresholds at 100). A follow-up commit chore(tests): exclude Stryker Regex mutator (upstream bug workaround) disables the Regex mutator to work around a Stryker.NET v4.16.0 crash on interpolated-string Regex constructors (upstream issue tracked in the mutation-testing doc).

Baseline mutation run on MTConnect.NET-Common at head 43497c5d (bluefin, concurrency 4, 4 min elapsed):

Metric Count
Total mutants 13,990
Killed 790
Survived 1,211
NoCoverage 8,275
Timeout 7
CompileError 188
Ignored 3,519
Files covered 1,141

The 7.75% baseline is expected: this PR ADOPTS the framework. Killing every survived + no-coverage mutant is realistically 100+ engineering hours and cross-cuts every subsystem in the assembly (codecs, model, validators, formatters), so a companion commit chore(tests): pin Stryker threshold at 7.75% baseline pins stryker-config.json thresholds to { high: 8, low: 5, break: 5 } — well below the 7.75% baseline so the current PR does not regress and so subsequent PRs inherit a pass-through gate until the dedicated coverage-quality campaign raises the floor. #242 tracks the phased follow-up campaign (categorize survivors -> draft kill tests per subsystem -> raise thresholds in step to 20 -> 40 -> 60 -> 80%+ -> expand Stryker to the sibling assemblies -> wire into CI as a required check once the pilot clears 80%).

Dime review cycle 1

Six-agent sweep (code-review + security-audit + simplification + improvement + documentation-audit + test-coverage-audit) dispatched at head d83cff2d. Findings distribution:

Agent Findings
code-review 2 MEDIUM + 1 LOW
security-audit 0
simplification 6 LOW
improvement 3 HIGH + 4 MEDIUM + 3 LOW
documentation-audit 1 HIGH + 3 MEDIUM
test-coverage-audit 2 HIGH + 3 MEDIUM + 1 LOW + 1 INFO

Disposition — three-state (Fixed / Closed-with-rationale / Tracked):

  • Fixed — 12 fixes landed atomically in-cycle. The eight commits after d83cff2d (through 43497c5d) are the fix landings + the mutation-testing-framework adoption plus its Regex-mutator workaround.
  • Tracked — five items deferred to follow-up PRs / issues: test-harness DRY consolidation, DeltaStats as record struct, --compat-version-label auto-derivation, dotnet run overhead removal on the generator smoke path, redundant --json-dump re-Parse, ADDED / REMOVED partition direct assertion, Parse throw-hardening, and a Stryker CI runner.
  • Closed with rationale — two items: Measurement.Code deliberate spec-alignment (Section 8.2 §Measurement code retains the spec's normative token, not the more-general C# identifier the reviewer proposed); empty-HistoricalAnchors documented anticipation (the reviewer's proposal treats an anticipated multi-version anchor map as a same-PR gap; the map is deliberately unused until the second historical spec version lands).

Full test sweep on bluefin at head 619dd1bc: 9 of 10 assemblies green (Docs-Tests documented bluefin Playwright flake, docs-orthogonal to this PR — same class of infra flake pinned by #227's baseline).

(Zero unfixed findings — Ready-eligible.)

Dime review cycle 2

Six-agent sweep dispatched at head 619dd1bc (the ottobolyos-directive Stryker baseline-pin commit). Cycle-2 scope is deliberately narrow — the sole diff under review is stryker-config.json threshold values { high: 100, low: 100, break: 100 } -> { high: 8, low: 5, break: 5 } + a JSONC provenance header cross-referencing #242. Findings distribution:

Agent Findings
code-review 1 MEDIUM + 1 LOW
security-audit 0
simplification 0
improvement 1 MEDIUM + 3 LOW
documentation-audit 3 HIGH + 2 MEDIUM
test-coverage-audit 1 HIGH + 2 MEDIUM + 1 LOW

Disposition — three-state (Fixed / Closed-with-rationale / Tracked):

  • Fixed — 7 items landed atomically across two commits:
    • 28d9b9fe test(generator): pin stryker-config.json thresholds + JSONC-parser assumption (test-coverage-audit) — 231-LOC StrykerConfigPinTests.cs guarding break window [5, 8], monotone ordering break <= low <= high <= 100, JSONC parseability via System.Text.Json with CommentHandling=Skip, Regex mutator ignore-list membership, and pilot-project pin (MTConnect.NET-Common.csproj + MTConnect.NET-Common-Tests); 5/5 GREEN + 27/27 pre-existing = 32/32 Generator-Tests GREEN on bluefin net8.0.
    • d79e9fc8 docs(testing): sync mutation-testing.md + Stryker.NET pin to cycle-2 threshold pin (this cycle) — F-CR-001 rewrites the stryker-config.json JSONC header to say explicitly that high: 8 sits above the 7.75 % baseline as an aspirational marker while low: 5 and break: 5 sit below as the pass-through gate; F-IMP-001 pins dotnet-stryker at v4.16.0 in .config/dotnet-tools.json alongside dotnet-reportgenerator-globaltool so the baseline stays reproducible under dotnet tool restore; F-DOC-001..005 sync docs/testing/mutation-testing.md in five places (JSON snippet, Thresholds bullet, --break-at CI shape, disposition-rules third arm, References list test-quality: raise MTConnect.NET-Common mutation score from 7.75% baseline #242 pointer).
  • Tracked — 3 LOW deferred to test-quality: raise MTConnect.NET-Common mutation score from 7.75% baseline #242: F-IMP-002 (upgrade the bare #242 JSONC-comment reference to the full https://github.com/... URL for editor click-through in non-git contexts); F-IMP-003 (flip since.enabled: true for incremental-mutation runs once the pilot clears 20 %); F-IMP-004 (emit a sibling stryker-baseline.json capturing the score / date / SHA / ignored-mutators / tool-version snapshot so a CI check can diff future runs against the pinned baseline, not just against the break threshold).
  • Closed with rationale — 2 LOW: F-CR-002 (the JSONC header's PR #233 head 43497c5d remains accurate as the measurement anchor for the 7.75 % baseline even after the head advances past that SHA); the simplification agent's no-finding is a self-close (the diff is already at minimum shape for a follow-up-tracked baseline pin).

Threshold values kept at { high: 8, low: 5, break: 5 } per user directive 2026-08-20; only prose + tool pin updated this cycle. Full test sweep on bluefin at head d79e9fc8 matches cycle-1's assembly count (docs-only + config-only diff introduces no new production behavior).

(Zero unfixed findings — Ready-eligible.)

Follow-up

  • Component / Composition template merger — the third consolidation
    candidate identified by the Phase 3.2 audit. Deferred this pass
    (six-gate ceiling breach without parameterization surgery); reopens
    if a future spec bump's delta emission surfaces a compelling reason
    to unify the two.
  • Historical-XMI iteration — the plan's original Phase 4.4 cross-verify
    across every build/sysml-model tag is out of scope this pass
    (ottobolyos 2026-08-20; the submodule ships one snapshot per spec
    bump). Reopens once the submodule is extended with multi-tag history
    or the campaign transitions to hosting spec-tags externally.
  • Mutation-testing kill campaign — the framework adopted in Phase 5
    reports 1,211 Survived + 8,275 NoCoverage mutants on
    MTConnect.NET-Common at the pilot baseline (mutation score 7.75%).
    Killing every surviving and no-coverage mutant is a multi-week
    test-authoring effort tracked in test-quality: raise MTConnect.NET-Common mutation score from 7.75% baseline #242 — the
    phased campaign categorizes survivors, drafts kill tests per subsystem,
    and raises thresholds in step (20 -> 40 -> 60 -> 80%+), then expands
    Stryker to the sibling assemblies and wires it into CI as a required
    check once the pilot clears 80%. The stryker-config.json landed here
    • the mutation-testing doc together stand the gate up so every future
      PR CAN run the mutation sweep; the pin at { high: 8, low: 5, break: 5 }
      keeps the current PR + every future one green until the dedicated
      campaign raises the floor.

@ottobolyos
ottobolyos marked this pull request as ready for review August 19, 2026 14:30
@ottobolyos

ottobolyos commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

@PatrickRitchie — this one is ready for review whenever you have a moment.

It consolidates the V2_6_V2_7/ per-version fixture family into topic-first single-file layout with matrix-parameterised version gates, and adds two permanent reflection guards (AssertionParityTests, PerVersionFolderProhibitionTests) that machine-enforce the new convention going forward. Zero library-side code changes — purely a test-suite refactor plus scaffolding.

Full unfiltered sweep on my end: 4975 passed / 0 failed / 0 skipped. Mutation test on the folder-prohibition guard confirms it fires RED when the pattern regrows.

I'm happy to walk through any of the design decisions or split it up differently if the shape isn't quite right.

ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 19, 2026
@ottobolyos
ottobolyos marked this pull request as draft August 19, 2026 19:03
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 19, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 19, 2026
- V2_7DataItemTypeTests.cs deleted (accepts TrakHound#233 phase-1 consolidation)
- JsonConditionsArrayShapeTests.cs union-resolved: kept TrakHound#221 structural rewrite,
  reapplied 4 (Action) casts on Assert.Throws<JsonException> sites for NUnit 4
  compatibility.

Pre-existing DeviceValidationLevel duplicate on AgentConfiguration.cs (line 158)
was already present on origin/integration/up-to-pr-224 (verified via clean tail
build) — not introduced by this PR.
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 19, 2026
Rebuilt after Ultrareview cycle 1 MEDIUM fix on chore/nunit-4-upgrade
(head 5e205e3): 3 ClassicAssert.AreEqual sites migrated to
Assert.That constraint form, both NUnit.Framework.Legacy imports dropped.

- V2_7DataItemTypeTests.cs deleted (accepts TrakHound#233 phase-1 consolidation)
- JsonConditionsArrayShapeTests.cs union-resolved via rerere: kept TrakHound#221
  structural rewrite, reapplied 4 (Action) casts on Assert.Throws sites
  for NUnit 4 compatibility.

Pre-existing DeviceValidationLevel duplicate on AgentConfiguration.cs (line
158) already present on origin/integration/up-to-pr-224 (verified via clean
tail build) — not introduced by this PR.
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 19, 2026
@ottobolyos
ottobolyos force-pushed the feat/dry-generator-phase1-consolidation branch from caace93 to 43497c5 Compare August 20, 2026 17:36
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 20, 2026
…sumption

Guards the cycle-2 threshold pin ({ high: 8, low: 5, break: 5 } at the
7.75% baseline) against silent regression AND against a phantom ratchet
that would raise the ceiling without a matching kill-test campaign
commit. Five NUnit tests cover the five orthogonal invariants:

1. StrykerConfig_parses_as_JSONC_with_line_comments — the file must
   open with a '//' JSONC comment (the baseline rationale) and must
   parse under System.Text.Json with CommentHandling=Skip. If the
   Stryker.NET JSONC-parser assumption ever breaks, the file's own
   parse round-trip catches it at build time instead of at Stryker
   run time.

2. Threshold_break_sits_inside_ratchet_window — pins break in [5, 8].
   Below 5 = coverage regression; above 8 = phantom ratchet without a
   matching baseline-lift commit. Either edit-direction forces the
   test constants to be updated alongside the config change.

3. Threshold_triple_is_monotonically_ordered — break <= low <= high
   <= 100. Guards against Stryker-config errors that would fail the
   run before any mutation was scored.

4. Regex_mutator_stays_on_the_ignore_list — pins the preceding
   chore(tests): exclude Stryker Regex mutator workaround.

5. Pilot_project_stays_pinned_to_MTConnect_NET_Common — the 7.75%
   baseline was measured against MTConnect.NET-Common with the full
   MTConnect.NET-Common-Tests suite. Swapping the pilot project
   silently invalidates the baseline.

Verified RED-when-mutated: temporarily setting break: 100 flips
Threshold_break_sits_inside_ratchet_window + Threshold_triple_is_
monotonically_ordered RED; reverting restores GREEN (5/5 pass, 32/32
Generator-Tests pass on bluefin, net8.0, 2 m 17 s).

Coverage-FLOOR pin per CONVENTIONS section 1.0d-trigies-novodecies.

Refs: TrakHound#233, TrakHound#242
@ottobolyos ottobolyos changed the title test(dry-generator): consolidate V2_6_V2_7 fixtures into topic-first layout with version gates test(dry-generator): consolidate V2_6_V2_7 into topic-first layout Aug 20, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
…sumption

Guards the cycle-2 threshold pin ({ high: 8, low: 5, break: 5 } at the
7.75% baseline) against silent regression AND against a phantom ratchet
that would raise the ceiling without a matching kill-test campaign
commit. Five NUnit tests cover the five orthogonal invariants:

1. StrykerConfig_parses_as_JSONC_with_line_comments — the file must
   open with a '//' JSONC comment (the baseline rationale) and must
   parse under System.Text.Json with CommentHandling=Skip. If the
   Stryker.NET JSONC-parser assumption ever breaks, the file's own
   parse round-trip catches it at build time instead of at Stryker
   run time.

2. Threshold_break_sits_inside_ratchet_window — pins break in [5, 8].
   Below 5 = coverage regression; above 8 = phantom ratchet without a
   matching baseline-lift commit. Either edit-direction forces the
   test constants to be updated alongside the config change.

3. Threshold_triple_is_monotonically_ordered — break <= low <= high
   <= 100. Guards against Stryker-config errors that would fail the
   run before any mutation was scored.

4. Regex_mutator_stays_on_the_ignore_list — pins the preceding
   chore(tests): exclude Stryker Regex mutator workaround.

5. Pilot_project_stays_pinned_to_MTConnect_NET_Common — the 7.75%
   baseline was measured against MTConnect.NET-Common with the full
   MTConnect.NET-Common-Tests suite. Swapping the pilot project
   silently invalidates the baseline.

Verified RED-when-mutated: temporarily setting break: 100 flips
Threshold_break_sits_inside_ratchet_window + Threshold_triple_is_
monotonically_ordered RED; reverting restores GREEN (5/5 pass, 32/32
Generator-Tests pass on bluefin, net8.0, 2 m 17 s).

Coverage-FLOOR pin per CONVENTIONS section 1.0d-trigies-novodecies.

Refs: TrakHound#233, TrakHound#242
@ottobolyos
ottobolyos force-pushed the feat/dry-generator-phase1-consolidation branch from c7b3d03 to 7f8ebbd Compare August 21, 2026 06:18
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
…ctVersions.Max

Task #408 amendment to PR TrakHound#233 Phase 4. Ships the zero-config delta-mode
entry point the version-bump plan (Phase 3) now assumes.

CLI shape changes:
- Add --new-xmi as the preferred spelling for the primary XMI input.
  --xmi is preserved as a legacy alias for backwards compatibility with
  every existing caller.
- Add --full-tree as the explicit escape hatch that disables both delta
  paths and forces full regeneration.
- Default behaviour when neither --previous-xmi nor --full-tree is
  supplied is now the zero-config auto-derive path: PREV_VERSION is
  parsed out of libraries/MTConnect.NET-Common/MTConnectVersions.cs
  under --output and the prior-version XMI is resolved in this order:

    Strategy B (primary):
      build/.cache/sysml-prev/MTConnectSysMLModel_v${PREV_VERSION}.xml
      populated per Phase 3.2 of the version-bump runbook.

    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.

    Strategy C (fail-hard):
      Neither resolves. Throws with an actionable message naming both
      probed paths and pointing at --previous-xmi (explicit override)
      and --full-tree (delta-disable escape hatch). The throw is
      caught at the CLI boundary and mapped to a clean
      "error: ..." stderr line + exit 1, so the operator sees a CLI
      failure and not a runtime-exception stack trace.

- Compat-label auto-derive: when --compat-version-label is not
  supplied AND the previous-XMI is auto-derived, the label defaults to
  v${PREV_XY_UNDERSCORE} (e.g. "v2_7"). When --previous-xmi is
  supplied without a label, the legacy "Previous" default is
  preserved for backwards compatibility with the two existing DeltaRegen
  fixtures.

Implementation notes:
- MTConnectVersions.cs is parsed textually rather than reflected off
  MTConnect.NET-Common at runtime. Adding a hard dependency on
  MTConnect.NET-Common would produce an awkward
  generator-emits-into-its-own-dependency ordering on clean rebuilds;
  text parsing keeps the importer standalone and matches the shape the
  version-bump plan's automation script already uses.
- The submodule-tag check runs `git -C <submodule> describe
  --exact-match --tags HEAD` in a suppressed subprocess. Non-zero exit
  or any exception (git binary missing, permission denied, dir not a
  git repo) reduces to "try the next strategy" — Strategy A missing is
  the routine dev-loop signal, not a fatal condition.
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
Phase 4.3 verification. Two tests exercise the delta-driven regen mode:

- Delta_mode_against_same_XMI_concentrates_every_file_into_Compat —
  runs `--previous-xmi = --xmi`, asserts zero individual .g.cs files
  are emitted and exactly three Compat/<label>.g.cs files exist (one
  per library).
- Delta_mode_against_mutated_XMI_emits_only_the_changed_file —
  produces a mutated XMI by rewriting a single ownedComment body
  (CoordinateSystemEnum.MACHINE, XMI id
  _19_0_3_68e0225_1597921579016_122540_182), runs delta with the
  original as --previous-xmi, and asserts:
    * exactly two files emerge as CHANGED (DataItemCoordinateSystem
      .g.cs + DataItemCoordinateSystemDescriptions.g.cs);
    * each CHANGED file carries the MUTATED_DELTA_MARKER string;
    * the Compat files do NOT carry the marker (proving unchanged /
      changed are cleanly partitioned).

The mutated-XMI approach is the ottobolyos-approved scope replacement
(2026-08-20) for the plan's original historical-XMI-iteration
verification, since the build/sysml-model submodule ships a single
snapshot per spec bump.
Phase 4.1 refresh surfaced a CS0109 build regression on the newly
refreshed .g.cs tree: `ToolingMeasurement.Code` was still being emitted
with `public new string Code` even though the base `Measurement.g.cs`
under `Assets.CuttingTools` no longer carries a `Code` property.

Root cause: MarkInheritedProperties in
`build/MTConnect.NET-SysML-Import/CSharp/TemplateRenderer.cs` had a
hand-stitched override for `Assets.CuttingTools.ToolingMeasurement`
that force-added `Code` to the inherited-name set, based on the design
assumption that `Assets.CuttingTools.Measurement.g.cs` was
hand-maintained / frozen and therefore its property list would never
appear in the export-side ClassModel graph the inheritance walk
traverses. That assumption is stale — the current runtime DOES emit
`Assets.CuttingTools.Measurement.g.cs` via the `sharedMeasurement`
injection in
`libraries/MTConnect.NET-SysML/Models/Assets/MTConnectAssetInformationModel.cs`
(which imports the Pallet Measurement class under Assets.CuttingTools
so `ToolingMeasurement : Measurement` has a compilable base). The
Pallet Measurement lacks `Code`, so the derived
`ToolingMeasurement.Code` doesn't hide anything, and the forced `new`
produced a CS0109 "does not hide an accessible member" error under the
solution-wide `TreatWarningsAsErrors=true`.

Fix: remove the override, keep the explanatory comment updated to
reflect the current renderer flow. The default inheritance walk now
correctly detects that Measurement has no Code property, so
`ToolingMeasurement.Code` emits plain (no `new`).

Refreshes `ToolingMeasurement.g.cs` accordingly so the byte-identical
guard stays green.
Both the byte-identical guard and the delta cross-verify walk every
`.g.cs` under a root. On any host where MSBuild has built the solution,
MSBuild-generated intermediates land under each project's bin/ and
obj/ subtrees (GlobalUsings.g.cs from
Microsoft.NET.Sdk.CSharp.CoreCompile.targets, ImplicitNamespaceImports.g.cs,
etc.). The generator never touches them; their presence spuriously
flipped `Current_XMI_regen_matches_committed_g_cs_tree` RED on bluefin
after the sln Debug build populated the intermediates.

Fix: filter `/bin/` and `/obj/` (and the same as prefixes) from the walk
in both fixtures.
Adds stryker-config.json at repo root targeting MTConnect.NET-Common
with MTConnect.NET-Common-Tests as the test project. Configuration:

- reporters: progress, cleartext, html, json — html for maintainer
  browsing, json for CI ingestion, cleartext for terminal replay.
- thresholds: high=low=break=100 — 100 percent mutation score is the
  Ultrareview coverage-quality gate per section 1.0d-trigies-septdecies.
- concurrency: 4 — matches bluefin CPU budget without oversubscribing.
- mutation-level: Complete — every Stryker mutator active.
- mutate excludes: every '*.g.cs' generator output tree so mutants land
  only on hand-authored code (generator emission has its own
  byte-identity guard in tests/MTConnect.NET-Generator-Tests/).

Adoption entry point per user D3 directive 2026-08-20. Subsequent
config extensions cover MTConnect.NET-Generator-Tests + other library
projects once the Common project reaches zero surviving mutants.
Adds CliInvocationFailureTests.cs pinning every early-return branch the
SysML importer's Program.cs guards its entry point on. Every documented
exit code (0 for --help / -h, 2 for usage errors, 1 for runtime
failures) and every RequireValue-throw case (flag arriving without its
trailing value) gets one assertion.

Coverage delta:

  - Unknown flag → exit 2 + stderr echoes flag verbatim.
  - Missing --xmi / --output → exit 2 + stderr names the required flag.
  - Nonexistent XMI / --previous-xmi / --output paths → exit 1 with
    the specific "not found" fingerprint.
  - --help / -h → exit 0 with the usage banner covering --previous-xmi
    and --compat-version-label (Phase 4.3 additions).
  - Missing value after --xmi / --previous-xmi / --compat-version-label
    / --output / --json-dump → non-zero exit with the offending flag
    named in stderr.
  - Missing library subdirectory under a valid output root → non-zero
    exit surfacing the fail-fast DirectoryNotFoundException.
  - Malformed XMI (well-formed XML that is not SysML) → exit 1 with the
    "Failed to parse XMI" stderr line, covering the parse-null branch.
  - --json-dump success path → writes the JSON tree + echoes the
    resolved path on stdout.

Sixteen new tests, ~180 additional lines of covered branches in
Program.cs (every non-happy-path arm reachable from the CLI). Pins the
CLI contract the operator actually sees end-to-end via `dotnet run
--no-build --project build/MTConnect.NET-SysML-Import`, not against an
internal helper — so a refactor that moves the flag parser into a
library would still be exercised through the entry-point contract.

coverage-FLOOR gap closure.
Adds DeltaCompatAndStatsTests.cs pinning the Compat-file contract and
the operator-facing stdout stats line that DeltaRegenTests does not
cover.

Coverage delta:

  - Same-XMI stats-line surface: added=0, changed=0, removed-skipped=0,
    unchanged-concentrated>0, compat-files-written=3.
  - Mutated-XMI stats-line surface: changed>0, added=0, removed-skipped=0.
  - Default --compat-version-label "Previous" (per Program.cs header)
    is honored when the flag is omitted from the CLI.
  - Compat file header carries the TrakHound copyright + MIT license
    banner + plan-D4 provenance summary + byte-identical promise.
  - Compat body carries the `// --- from <relative-path> ---` divider
    per concentrated entry.
  - Compat body preserves multiple `namespace X { ... }` blocks
    (multi-namespace concentration is the point of plan D4).
  - stdout stats line uses the exact documented key order + syntax so a
    silent rename of any counter key flips the fixture RED.

Seven new tests. Pairs with DeltaRegenTests (CHANGED + UNCHANGED-
concentrated coverage) to raise the delta-orchestration line coverage.

ADDED / REMOVED partition-arm coverage remains an untested branch
because a class-name mutation in the XMI (the only way to force those
arms via the current CLI contract) requires XMI-schema knowledge that
would make the test fragile against upstream MTConnect Standard bumps.
Tracked as F-COV-004 below.

coverage-FLOOR gap closure.
MTConnectModel.Parse can either return null (Program's null-guard hits
and returns exit 1) OR throw an unhandled exception on structurally-
invalid input (host returns 134 on Linux). Both satisfy the coverage
contract "malformed input is a runtime failure"; the stricter exit-1
surface would be a hardening improvement (top-level try/catch mapping
parse exceptions to `return 1;`) tracked as a follow-up.

Widens the assertion to `Not.Zero` + a grep on stdout|stderr for any
parse-failure fingerprint (Failed to parse XMI, Exception, XmlException,
NullReference).

coverage-FLOOR gap closure.
Ultrareview cycle 1 (F-CR-002 MEDIUM) surfaced a logic gap: the delta
emitter writes ADDED / CHANGED files to their normal libraries/<lib>/*
paths and concentrates UNCHANGED bodies into Compat/<label>.g.cs, but
never removes the pre-existing individual .g.cs files for the
concentrated / removed types. On a fresh scratch directory (which is
exactly how DeltaRegenTests exercises the flow) both partitions are
empty so no collision surfaces; on the realistic use case — --output
pointed at the repo root that already carries a full committed .g.cs
tree — every UNCHANGED type would appear twice (individual file plus
Compat block), producing CS0101 duplicate-type-definition errors at
compile time. Every REMOVED type would linger past its spec removal.

Fix: emit-time `DeleteIfExists` on both partitions.

- UNCHANGED files: after appending to compatBody, delete the individual
  file from outputRoot so Compat/<label>.g.cs is the sole namespace host
  for the type.
- REMOVED files: delete the individual file so a type dropped from the
  new spec version stops shipping in the library.

`DeleteIfExists` is idempotent (missing files no-op) and does not follow
symlinks on .NET, so it composes cleanly with the existing scratch-dir
cleanup semantics. The four DeltaRegenTests + DeltaCompatAndStatsTests
guard the invariant against regression.

Ultrareview cycle 1 (F-IMP-003 HIGH) surfaced a hardening gap in the
same code path: --compat-version-label flows through
Path.Combine(compatDir, $"{label}.g.cs") unvalidated, so a hostile
value like `../../etc/passwd` would escape the Compat/ directory.
Add `IsSafeCompatLabel` regex allowlist at flag-parse time
(`^[A-Za-z0-9_\-][A-Za-z0-9_\-.]*$`, 1..64 chars, no leading dot,
rejects path separators / drive letters / ASCII control chars). The
default "Previous" and typical spec labels like "v2_6" / "v2.5-rc3"
pass; hostile inputs reject at exit 2 with a diagnostic pointing at the
allowed shape. Complements the security-audit review's operator-trusted
premise with a one-line defensive guard.

Ultrareview cycle 1 (F-SIMP-005 LOW, F-SIMP-006 LOW) applied
opportunistically in the same touch: `ByteEquals` collapses to
`((ReadOnlySpan<byte>)a).SequenceEqual(b)`; the five `$""`
interpolation prefixes on the Compat-header appendLine calls drop the
`$` (no `{}` placeholders).

Ultrareview cycle 1 finding-IDs — F-CR-002 (MEDIUM), F-IMP-003 (HIGH),
F-SIMP-005 (LOW), F-SIMP-006 (LOW).
…loor

Ultrareview cycle 1 (F-IMP-004 MEDIUM) surfaced a classic Process-pipe
deadlock across every generator-tests fixture: ReadToEnd() on
stdout followed by ReadToEnd() on stderr blocks forever if the child
writes >4 KB to stderr (Linux pipe-buffer default) while stdout is
short — the child blocks on write to the full pipe and the parent
blocks on the empty pipe.

Fix: Task.WhenAll on the two async ReadToEndAsync calls, then
WaitForExit. Applied to ByteIdenticalRegenTests, DeltaRegenTests,
DeltaCompatAndStatsTests, and CliInvocationFailureTests — every
generator-test that dispatches the SysML importer via
Process.Start.

Ultrareview cycle 1 (F-CR-003 LOW) surfaced an off-by-one on the
TopicAnchors floor in TopicFixtureCoverageTests: the map holds 23
entries but the floor asserted Is.GreaterThanOrEqualTo(22) with a
comment saying "22 distinct anchor types". Silent drop of one anchor
row would have slipped past the guard.

Fix: tighten to Is.EqualTo(23), aligned with the AssertionParityTests
pattern that pins the migration map at exactly 34. Comment updated to
call out the exact per-topic breakdown (4 Components + 3 DataItems +
11 Configuration + 1 WaterHardness + 2 Enum + 2 Version).

Ultrareview cycle 1 finding-IDs — F-IMP-004 (MEDIUM), F-CR-003 (LOW).
…tier

Ultrareview cycle 1 documentation-audit surfaced three gaps:

- F-DOC-001 (HIGH) — build/MTConnect.NET-SysML-Import/README.md's CLI
  table still listed only --xmi, --output, --json-dump, --help. Add
  rows for --previous-xmi and --compat-version-label plus a new
  "Delta mode (--previous-xmi)" subsection with the four-way ADDED /
  CHANGED / REMOVED / UNCHANGED partition semantics, the label safety
  contract, the delete-stale-individual behavior, and a worked v2.7
  → v2.8 invocation. Program.cs's own header already pointed readers
  at "build/MTConnect.NET-SysML-Import/README.md for the full usage
  guide" (L43) — the README now honors that pointer.
- F-DOC-002 (MEDIUM) — no docs page mentioned Stryker.NET. Add
  docs/testing/mutation-testing.md documenting the config, the
  100 % mutation-score gate, the install / run / CI story, and the
  two-way disposition rule for surviving mutants (kill or exclude
  with rationale). Cross-linked from docs/testing.md's fourth tier.
- F-DOC-003 (MEDIUM) — docs/testing.md's "Test tiers" section listed
  three tiers and did not enumerate the new
  tests/MTConnect.NET-Generator-Tests/ project. Add a fourth tier
  bullet naming the four fixtures (ByteIdenticalRegen, DeltaRegen,
  CliInvocationFailure, DeltaCompatAndStats) and cross-linking to
  mutation-testing.md.

Ultrareview cycle 1 finding-IDs — F-DOC-001 (HIGH), F-DOC-002 (MEDIUM),
F-DOC-003 (MEDIUM).
Initial Stryker.NET v4.16.0 run against MTConnect.NET-Common crashed
after 1:20 elapsed with:

    Unhandled exception. System.InvalidCastException:
        Unable to cast object of type
        'Microsoft.CodeAnalysis.CSharp.Syntax.InterpolatedStringExpressionSyntax'
        to type 'Microsoft.CodeAnalysis.CSharp.Syntax.LiteralExpressionSyntax'.
    at Stryker.Core.Mutators.RegexMutator.ApplyMutations
        (ObjectCreationExpressionSyntax node, SemanticModel semanticModel)
        in /_/src/Stryker.Core/Stryker.Core/Mutators/RegexMutator.cs:line 42

Root cause: RegexMutator assumes `new Regex(literal)` and blind-casts
the constructor's first argument to LiteralExpressionSyntax; any
`new Regex($"...")` interpolated-string pattern in the target
assembly detonates.

Workaround: add "Regex" to `excluded-mutations` so the rest of the
mutator set (arithmetic, logical, string, boolean, statement,
conditional, unary, checked, initializer, LINQ, method, block,
assignment) still runs. Re-enable once the upstream RegexMutator
starts handling interpolated string patterns.

Filed with the config as an inline `//` comment so a future reader
finds the rationale next to the exclusion.
…d#242 tracks raising coverage

The first Stryker.NET run against the pilot target — MTConnect.NET-Common
with the full MTConnect.NET-Common-Tests suite — established a baseline
mutation score of 7.75% on 2026-08-20 (Stryker.NET v4.16.0, Regex mutator
ignored per the preceding chore(tests): exclude Stryker Regex mutator
commit).

Killing every surviving mutant is realistically 100+ engineering hours
and cross-cuts every subsystem in the assembly (codecs, model,
validators, formatters). That scope does not belong on the DRY-generator
PR whose diff is test consolidation + fixture reorganisation — not
production-code additions.

This commit pins stryker-config.json thresholds to
{ high: 8, low: 5, break: 5 } — well below the 7.75% baseline so the
current PR does not regress and so subsequent PRs inherit a pass-through
gate until the dedicated coverage-quality campaign raises the floor.

A top-of-file JSONC comment records the baseline, the date, and a
cross-reference to TrakHound#242 which tracks the phased
follow-up campaign (categorize survivors -> draft kill tests per
subsystem -> raise thresholds in step to 20 -> 40 -> 60 -> 80%+ ->
expand Stryker to the sibling assemblies -> wire into CI as a required
check once the pilot clears 80%).

Refs: TrakHound#242
…sumption

Guards the cycle-2 threshold pin ({ high: 8, low: 5, break: 5 } at the
7.75% baseline) against silent regression AND against a phantom ratchet
that would raise the ceiling without a matching kill-test campaign
commit. Five NUnit tests cover the five orthogonal invariants:

1. StrykerConfig_parses_as_JSONC_with_line_comments — the file must
   open with a '//' JSONC comment (the baseline rationale) and must
   parse under System.Text.Json with CommentHandling=Skip. If the
   Stryker.NET JSONC-parser assumption ever breaks, the file's own
   parse round-trip catches it at build time instead of at Stryker
   run time.

2. Threshold_break_sits_inside_ratchet_window — pins break in [5, 8].
   Below 5 = coverage regression; above 8 = phantom ratchet without a
   matching baseline-lift commit. Either edit-direction forces the
   test constants to be updated alongside the config change.

3. Threshold_triple_is_monotonically_ordered — break <= low <= high
   <= 100. Guards against Stryker-config errors that would fail the
   run before any mutation was scored.

4. Regex_mutator_stays_on_the_ignore_list — pins the preceding
   chore(tests): exclude Stryker Regex mutator workaround.

5. Pilot_project_stays_pinned_to_MTConnect_NET_Common — the 7.75%
   baseline was measured against MTConnect.NET-Common with the full
   MTConnect.NET-Common-Tests suite. Swapping the pilot project
   silently invalidates the baseline.

Verified RED-when-mutated: temporarily setting break: 100 flips
Threshold_break_sits_inside_ratchet_window + Threshold_triple_is_
monotonically_ordered RED; reverting restores GREEN (5/5 pass, 32/32
Generator-Tests pass on bluefin, net8.0, 2 m 17 s).

Coverage-FLOOR pin per CONVENTIONS section 1.0d-trigies-novodecies.

Refs: TrakHound#233, TrakHound#242
…threshold pin

Ultrareview cycle 2 (7 findings across code-review + improvement +
documentation-audit) surfaced three follow-ups on the preceding
619dd1b stryker-config.json threshold pin. All FIX-IN-CYCLE, all
closed atomically here:

- F-CR-001 (MEDIUM) — the earlier JSONC-header wording claimed
  "thresholds pinned below the baseline" which was only accurate for
  `low: 5` and `break: 5`; `high: 8` sits ABOVE the 7.75% baseline as
  an aspirational marker, so baseline-conforming runs report yellow
  rather than green. Rewrite the JSONC header to say so explicitly and
  to name the mechanism (Stryker exits non-zero only below `break`).
- F-IMP-001 (MEDIUM) — pin `dotnet-stryker` at v4.16.0 in
  .config/dotnet-tools.json alongside `dotnet-reportgenerator-globaltool`.
  Without a local-tool pin a subsequent `dotnet tool update` could
  swap in a Stryker release with a different mutator set and the
  7.75% baseline would stop meaning what the JSONC comment claims
  within one cadence. The JSONC comment now cross-references the pin.
- F-DOC-001..005 (3 HIGH + 2 MEDIUM) — docs/testing/mutation-testing.md
  still pinned the old `100 / 100 / 100` thresholds in three places
  (JSON snippet, prose bullet, `--break-at 100` CI shape) and closed
  with a two-disposition rule that contradicted the pass-through gate.
  Every stale reference retuned to the 8 / 5 / 5 pin; the snippet
  gains a `jsonc` fence + a header comment mirroring the shipped
  file's provenance block; the disposition rules grow a third arm for
  the scoped transitional accommodation while restating that zero
  survivors remains the long-term merge gate; the References list
  now includes TrakHound#242.

Values pinned per user directive 2026-08-20; no threshold-value drift
on this commit. Pin tests introduced at 28d9b9f still assert
`break in [5, 8]`, monotone ordering, JSONC parseability, Regex-mutator
ignore, and pilot-project pin — none of which this diff touches.

Cycle-2 disposition summary: 7 FIX-IN-CYCLE closed atomically across
28d9b9f (test guards) + this commit (config + docs sync); 2 LOW
CLOSE-with-rationale (F-CR-002 SHA-as-measurement-anchor,
F-DOC/simplification/security no-finding); 3 LOW TRACK on TrakHound#242
(F-IMP-002 clickable URL, F-IMP-003 since.enabled incremental,
F-IMP-004 stryker-baseline.json snapshot). Zero unfixed MEDIUM+
findings.

Refs: TrakHound#242
The DRY-generator phase-1 consolidation adds `--previous-xmi` (opt-in
delta-driven mode) and `--compat-version-label` (label for the
`Compat/<label>.g.cs` re-emit surface) to `sysml-import`, but
`docs/reference/cli.md` wasn't refreshed after the flag surface changed.
The drift gate (`docs/scripts/generate-reference.sh --check`) now flags
`docs/reference/cli.md` as out of date on the docs-site workflow.
Rerunning the generator without `--check` inserts the two missing rows.
`check-broken-links.mjs` (invoked by the docs-site "Check internal links"
job after the "Prepare generated docs" gate now passes) treats every
markdown link that resolves outside `docs/` as broken, per the docs-root
containment defense in `isInsideDocsRoot`. `version-matrix-convention.md`
carried six relative `../../tests/…` / `../../libraries/…` links to
in-tree code files — all outside the docs root — which the checker
flagged as broken as soon as the docs-prepare gate cleared.

Rewrites the six links to absolute
`https://github.com/TrakHound/MTConnect.NET/blob/master/…` URLs,
matching the convention already used in `docs/concepts/agent-validation-events.md`.
External URLs are not validated by the checker by design (third-party
state is not a CI gate), so the links keep pointing at the code the
prose references without tripping the containment rule.
…ctVersions.Max

Task #408 amendment to PR TrakHound#233 Phase 4. Ships the zero-config delta-mode
entry point the version-bump plan (Phase 3) now assumes.

CLI shape changes:
- Add --new-xmi as the preferred spelling for the primary XMI input.
  --xmi is preserved as a legacy alias for backwards compatibility with
  every existing caller.
- Add --full-tree as the explicit escape hatch that disables both delta
  paths and forces full regeneration.
- Default behavior when neither --previous-xmi nor --full-tree is
  supplied is now the zero-config auto-derive path: PREV_VERSION is
  parsed out of libraries/MTConnect.NET-Common/MTConnectVersions.cs
  under --output and the prior-version XMI is resolved in this order:

    Strategy B (primary):
      build/.cache/sysml-prev/MTConnectSysMLModel_v${PREV_VERSION}.xml
      populated per Phase 3.2 of the version-bump runbook.

    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.

    Strategy C (fail-hard):
      Neither resolves. Throws with an actionable message naming both
      probed paths and pointing at --previous-xmi (explicit override)
      and --full-tree (delta-disable escape hatch). The throw is
      caught at the CLI boundary and mapped to a clean
      "error: ..." stderr line + exit 1, so the operator sees a CLI
      failure and not a runtime-exception stack trace.

- Compat-label auto-derive: when --compat-version-label is not
  supplied AND the previous-XMI is auto-derived, the label defaults to
  v${PREV_XY_UNDERSCORE} (e.g. "v2_7"). When --previous-xmi is
  supplied without a label, the legacy "Previous" default is
  preserved for backwards compatibility with the two existing DeltaRegen
  fixtures.

Implementation notes:
- MTConnectVersions.cs is parsed textually rather than reflected off
  MTConnect.NET-Common at runtime. Adding a hard dependency on
  MTConnect.NET-Common would produce an awkward
  generator-emits-into-its-own-dependency ordering on clean rebuilds;
  text parsing keeps the importer standalone and matches the shape the
  version-bump plan's automation script already uses.
- The submodule-tag check runs `git -C <submodule> describe
  --exact-match --tags HEAD` in a suppressed subprocess. Non-zero exit
  or any exception (git binary missing, permission denied, dir not a
  git repo) reduces to "try the next strategy" — Strategy A missing is
  the routine dev-loop signal, not a fatal condition.
…xisting fixtures

Adds AutoDerivePreviousXmiTests fixture pinning the five behavior arms
of the task #408 CLI amendment:

- Auto_derive_from_MTConnectVersionsMax_uses_cache_when_present —
  Strategy B (primary) resolves against a populated
  build/.cache/sysml-prev/MTConnectSysMLModel_v${PREV_VERSION}.xml,
  emits delta with the auto-derived "v2_7" Compat label.
- Auto_derive_from_MTConnectVersionsMax_falls_back_to_submodule_tag_when_cache_absent —
  Strategy A (fallback) resolves against a synthetic git-tagged
  build/sysml-model when the cache is empty.
- Auto_derive_from_MTConnectVersionsMax_fails_hard_when_neither_cache_nor_tag_resolves —
  Strategy C (fail-hard) surfaces every actionable message field: the
  resolved PREV_VERSION, both probed paths, the --previous-xmi
  override, and the --full-tree escape hatch.
- Explicit_previous_xmi_wins_over_auto_derive — explicit --previous-xmi
  short-circuits the resolver even when the cache carries a different
  (mutated) XMI, preventing the cache from leaking into an
  operator-directed delta.
- Full_tree_flag_disables_delta_mode — --full-tree short-circuits
  before the resolver runs, produces no Compat file, and emits the
  full generated tree.

Each test bootstraps a synthetic --output tree with library scaffolding,
a pinned MTConnectVersions.cs skeleton, and (for the Strategy A test) a
minimal git repo tagged v2.7. The synthetic git helper explicitly
disables commit.gpgsign / tag.gpgsign / tag.forceSignAnnotated per-repo
so the fixture works on hosts with global signing hooks.

Existing fixture adaptations for the CLI change:

- ByteIdenticalRegenTests: adds --full-tree to the generator invocation
  so the byte-identical guard stays on the full-regeneration path — the
  scratch --output dirs lack MTConnectVersions.cs, so the zero-config
  auto-derive would otherwise abort the invocation before any templates
  render.
- CliInvocationFailureTests: three cases (Missing_library_subdirectory,
  Malformed_xmi, JsonDump) now pass --full-tree so the assertions bind
  to the surface they were originally pinning (RenderCommonClasses,
  MTConnectModel.Parse, --json-dump), not to the new auto-derive
  failure. The --help + missing-flag assertions grow to cover
  --new-xmi, --full-tree, and the legacy --xmi alias mention so the
  help contract is machine-enforced.

Local run on the fresh worktree: 37 / 37 pass, 4 m 10 s wall clock.
…to-derive

Refreshes build/MTConnect.NET-SysML-Import/README.md to match the task #408
CLI surface:

- Quick-start invocation drops --xmi in favor of --new-xmi and demonstrates
  the zero-config single-flag form as the common case, with --full-tree
  documented alongside for callers that want the pre-#408 semantics.
- CLI table adds rows for --new-xmi (preferred), --xmi (legacy alias), and
  --full-tree (escape hatch), and rewrites the --compat-version-label row to
  document the auto-derived v${X}_${Y} default vs. the legacy "Previous"
  fallback that applies with an explicit --previous-xmi.
- New "Zero-config delta mode" section documents Strategy B (cache) →
  Strategy A (submodule tag) → Strategy C (fail-hard) resolver order, the
  fail-hard stderr shape, and the standard single-flag Phase 3 invocation.
- "Delta mode with explicit --previous-xmi" section keeps the override-only
  use cases (cross-version audit, historical snapshot, skip-version bump)
  discoverable for maintainers who need them.
- "Adding a new MTConnect Standard version" runbook renames its regenerate
  command to --new-xmi so the runbook shows the preferred flag surface.
Cycle-3 ultrareview coverage FLOOR:
close the failure-path and boundary gaps in the task #408 amendment on top
of the existing `AutoDerivePreviousXmiTests` / `CliInvocationFailureTests`
fixtures. Every added test invokes the generator via `dotnet run --no-build`
so the assertions bind to the CLI contract, not to any internal helper.

Auto-derive failure paths (AutoDerivePreviousXmiTests):
 * Missing MTConnectVersions.cs → FileNotFoundException → exit 1 with the
   four-recovery-path message.
 * Present-but-Max-less MTConnectVersions.cs → InvalidOperationException
   for the first regex miss; names the Max convention element.
 * Max => VersionNN with no matching const table entry → second regex
   miss branch; names the un-resolvable constant.
 * Submodule dir present but not a git repo → TryGetSubmoduleTag returns
   null via the exception path; Strategy A rejects; Strategy C fires.
 * Submodule git repo with wrong tag (v9.9 vs Max=v2.7) → tag-mismatch
   branch of the Ordinal comparison; Strategy A rejects; Strategy C fires.

IsSafeCompatLabel guard (CliInvocationFailureTests):
 * 13-case TestCaseSource of hostile labels (path traversal, forward /
   back slash, leading dot, whitespace, over-length, shell metachars,
   control chars) — every case must reject at exit 2 with the
   "not a safe filename" fingerprint.
 * Explicit empty-string boundary case (RequireValue returns "" rather
   than throwing; IsSafeCompatLabel must still reject).
 * 6-case positive `[TestCase]` list of documented safe labels (defaults,
   auto-derived shapes, 64-char length-upper-bound boundary) — every case
   must accept and reach the full-tree branch, catching accidental
   regex tightening in a later refactor.

--new-xmi RequireValue arm (CliInvocationFailureTests):
 * Task #408 introduced --new-xmi as a distinct switch case parallel to
   the legacy --xmi alias. Pin the RequireValue throw so the preferred-
   flag arm can't silently regress while --xmi coverage still passes.

Full-tree floor threshold (AutoDerivePreviousXmiTests):
 * Was `emittedFiles > 100` — accepted any partial emission including
   the ~10-file delta subset. Tighten to `> 700` (current baseline is
   892) so a delta-mode leakage into the full-tree path trips loudly;
   ordinary spec-shrink drift stays tolerated.

All 63 generator-tests pass locally (was 37 pre-cycle-3; +26 net new).
…erived)" label; narrow catch-comment claim

Dime review cycle 3 (F-IMP-401 + two LOWs).

F-IMP-401 — regex-race in `ReadMTConnectVersionsMax`: without a
comment-strip pass, a stale `// public static Version Max => Version27;`
line left in place during a version bump wins the first-match against the
live `Max => Version28;` declaration below it, pinning PREV_VERSION to
the wrong version. Fix: strip both `//` line comments and `/* … */`
block comments from `MTConnectVersions.cs` before applying the regexes,
via a new `StripCSharpComments` helper that walks strings and character
literals verbatim (regular, `@"…"` verbatim, `$"…"` / `$@"…"` / `@$"…"`
interpolated) so a comment sigil inside a literal is not chewed up.
Newlines are preserved so downstream regex line-number reporting still
points at the source. New RED-then-GREEN test pins the fix:
`AutoDerivePreviousXmiTests.Commented_out_Max_declaration_does_not_confuse_the_parser`
writes a `MTConnectVersions.cs` with a `//`-commented decoy AND a `/* */`
block-commented decoy above the live `Max => Version29`, populates ONLY
the v2.9 cache, and asserts the resolver picks v2.9 (not the v2.7 / v2.8
decoys).

LOW — "(auto-derived)" label lie: when the operator passed an explicit
`--compat-version-label` alongside the zero-config prev-XMI path, that
explicit label wins the `??=` default, yet the stdout `Label:` line
unconditionally suffixed "(auto-derived)". Fix: capture whether the
label was auto-derived (label null AND autoDerived non-null before the
`??=`) into a `compatLabelIsAutoDerived` bool, and only append the
"(auto-derived)" suffix in that branch.

LOW — catch-filter narrower than the comment claims: the try/catch
around `ResolvePreviousXmi` only catches `InvalidOperationException` and
`FileNotFoundException`, but its comment said "Any auto-derive failure
is caught". Narrow the comment to name the expected exception classes
explicitly and state that other classes (IOException, UnauthorizedAccessException)
intentionally escape as an unhandled stack trace — the right signal for
an unexpected host-level failure the operator-facing recovery text
cannot address.
Dime review cycle 3 collateral: the RequireValue-detection regex in
`CollectDotNetTool` used `[\s\S]{0,200}?` — a 200-char non-greedy scan
after the `case "--flag":` label — which meant a boolean flag whose
case body sits ABOVE a value-taking case falsely inherited the
neighbor's value shape. Concretely: `case "--full-tree": fullTree =
true; break;` followed by `case "--output": … RequireValue(…)` was
90-odd chars, so `--full-tree` was regenerated into `docs/reference/cli.md`
with a spurious `<value>` in the Argument column.

Fix: change the scan bound from a fixed-length window to a
"stop-at-next-boundary" negative lookahead — the scan halts at the next
`case "…":`, `default:`, or `break;`. Boolean flags whose case body is
just `flag = true; break;` no longer reach across into the next case
body.

Regenerated `docs/reference/cli.md` in the following commit picks up
the corrected Argument column for `--full-tree`. This DocsGen defect surfaced while landing the F-DOC-C3-001 regen, so
the fix ships in the same PR rather than as a follow-up.
…hape

Dime review cycle 3 (F-DOC-C3-001).

`docs/reference/cli.md` was stale against the auto-derive CLI landed in
prior cycle-1 / cycle-2 commits: the SysML-Import section still listed
only the legacy `--previous-xmi` + `--xmi` opt-in delta flags and
carried the pre-Phase-4 flag descriptions. CI drift-check
(`docs/scripts/generate-reference.sh --check`) would have failed on the
committed tree.

Regenerated via `bash docs/scripts/generate-reference.sh`. Diff:
`--new-xmi` and `--full-tree` land as new rows with the up-to-date
descriptions from `Program.cs`'s leading `//` Flags: block; `--xmi`
description switches to the "legacy alias" wording; `--previous-xmi`
description switches to the "edge-case override" wording;
`--compat-version-label` description picks up the new auto-derive
default clause. `--full-tree` has no Argument column (the DocsGen fix
in the preceding commit bounded the RequireValue detection to the
current case block so the boolean flag no longer inherits `--output`'s
value shape).

Drift check now clean:
    $ bash docs/scripts/generate-reference.sh --check
    ==> reference pages match source inventory.
Dime review cycle 3 (F-DOC-C3-002).

`docs/cli/sysml-import.md` was still on the pre-Phase-4 "hardcoded paths
in Program.cs, not a parameterized CLI" narrative — obsolete after the
`--xmi` / `--output` / `--previous-xmi` CLI landed at Phase 4 and the
`--new-xmi` / `--full-tree` / zero-config-auto-derive shape landed in
this PR's cycle-1 / cycle-2 commits. A user landing on this page from
`docs/cli/index.md` would find only the "future change may refactor
Program.cs" hedge — no synopsis, no flag table, no zero-config default
description, no --full-tree escape-hatch coverage.

Full rewrite matching the shape of `docs/cli/agent.md` and
`docs/cli/adapter.md`: Synopsis code block, Flags table, Modes section
(delta zero-config default, delta --previous-xmi override, full-tree
opt-in), Delta emission stats, Example invocations, Exit codes,
Maintainer workflow, Configuration, Output discipline, Verification,
See also. Zero-config default is called out prominently (matches the
cycle's UX goal: no hand-editing of Program.cs, no path juggling for
the common case). `--previous-xmi` documented as the edge-case override
for cross-version audits and version-skipping bumps. `--full-tree`
documented as the escape hatch when the delta path is impossible.
Compat-label auto-derive default (`v${X}_${Y}`) documented alongside
the legacy `Previous` default. Legacy `--xmi` alias covered so operator
scripts do not break.
Dime review cycle 5 (test-coverage-audit): close the FLOOR gaps the cycle-4
fix batch (718cf00 + 6b471f0) left open. Every added test lands as a
direct branch-scoped pin — the existing golden-file / integration-style
fixtures catch the same regressions transitively, but the direct pins
surface each regression with a branch-scoped failure message before the
downstream diff is even computed.

StripCSharpComments (Program.cs → SourceStripper.cs):
 * Extracted from the top-level static local function to a testable
   internal static class in the same generator project. The
   `MTConnect.NET-Generator-Tests` csproj gains a shared-source
   `<Compile Include>` link on `SourceStripper.cs` so the tests
   compile it into their own assembly — no runtime linkage against the
   generator executable, so the byte-identical-regen tests still treat
   the generator as an external CLI.
 * Refactor extracts the two mirrored string-walker inner loops into
   `WalkVerbatim` / `WalkRegular` helpers so each escape-rule (`""` for
   verbatim, `\` for regular) has a single source-of-truth.
 * `@$"…"` interpolated-verbatim ordering gets its own explicit
   branch, matching the class-level docstring's claim that every C# 8+
   ordering is recognized. Traced observable behavior: the pre-fix
   fallback (bare `@` + regular `$"…"` walker) produces byte-identical
   stripped output because both walkers preserve string content
   verbatim and the buggy walker's premature termination at each `""`
   is immediately re-entered as a new regular string — no `//` ever
   leaks into top-level state between atoms. The explicit branch is
   therefore a code-clarity move (matches the docstring, halves the
   branch-count of the fallback path), not an observable bug fix. The
   pin is on the FLOOR panel, not on a bug regression.
 * `SourceStripperTests` — 16 direct branch pins:
   – line comment stripped to same-width whitespace with newline preserved
   – line comment at EOF without trailing newline terminates cleanly
   – block comment same-width whitespace + embedded newlines preserved
   – block comment spanning multiple lines preserves line count
   – regular string with `//` sigil inside preserved verbatim
   – regular string with `/* */` sigil preserved verbatim
   – regular string with `\"` escape terminates at the correct quote
   – interpolated `$"…"` preserved verbatim
   – verbatim `@"…"` with `""` escape preserved
   – verbatim `@"…"` with embedded newlines preserved
   – interpolated-verbatim `$@"…"` with `""` escape preserved
   – interpolated-verbatim `@$"…"` (alt order) with `""` escape preserved
   – character literal `'/'` does not trip line-comment sniff
   – character literal with `\'` escape terminates correctly
   – composite input strips only comments not literals
   – strip preserves length + newline count (line-number invariant)

Label-lie guard (Program.cs:196, 231-233):
 * `Auto_derived_label_carries_the_auto_derived_suffix_on_stdout` —
   positive branch (compatLabelIsAutoDerived=true): the stdout Label:
   line carries the "(auto-derived)" suffix when the label was
   genuinely auto-derived. Pre-cycle-5 no test asserted this suffix.
 * `Explicit_label_alongside_zero_config_prev_xmi_does_not_get_auto_derived_suffix` —
   negative branch (compatLabelIsAutoDerived=false): when the operator
   passes an explicit --compat-version-label alongside the zero-config
   prev-XMI, the explicit label wins and the "(auto-derived)" suffix
   is NOT appended. This is the direct pin for the cycle-4 label-lie
   fix — pre-fix, the suffix appeared unconditionally.

DocsGen bounded-scan (CliInventory.cs:352-353):
 * `SysMLImport_FullTree_Flag_Is_Detected_As_Switch_Not_Value_Flag`
   in DocsReferenceGenerationTests — asserts --full-tree in the
   collected CliInventory has ArgShape == null (i.e., detected as a
   boolean switch, not a value-taking flag). Pre-cycle-4 the
   RequireValue-detection regex used an unbounded lookahead and
   --full-tree falsely inherited the neighbouring --output case's
   `<value>` shape. The golden-file `Cli_Page_Is_In_Sync_With_Source`
   test would also catch the regression, but the targeted pin here
   surfaces the specific branch-scoped failure without recomputing
   the whole cli.md diff.

Verification:
 * All 16 SourceStripperTests pass locally (Debug, dotnet 8.0.104,
   45 ms).
 * All 82 generator-tests pass locally (Debug, 6m5s) and on bluefin
   (Debug, dotnet 10.0.302, 3m25s).
 * All 68 DocsReferenceGenerationTests pass locally (exit 0) and on
   bluefin. The 5 RouteCheckTests failures on bluefin's rsync-copy
   are pre-existing environmental gaps (npm-driven vitepress dist
   build not run on the ephemeral path) — unrelated to this cycle.
Dime review cycle 5 (F-CR-502, LOW).

The cycle-4 rewrite regressed four sites from AmE to BrE: 'behaviour'
(three sites: legacy-behavior parenthetical, pre-Phase-4 preservation
clause, See-also end-to-end blurb) and 'containerised' (See-also
dotnet.sh reference). The docs corpus uses AmE by convention
(cli/agent.md has '## Exit behavior'; cli/adapter.md, cli/dotnet-sh.md,
cli/test-sh.md, and the troubleshooting pages all use 'containerized').

Per CONVENTIONS section 1.0d-decies + 1.0d-trigies-vicies-octies,
committed docs use AmE spelling with CMOS typography; BrE is reserved
for user-authored prose (PR bodies, discussion drafts, issue comments).
This file was the outlier - sibling docs/cli/*.md pages already carry
AmE consistently.
… F-SIMP-501

MTConnectVersion.Max extraction previously routed MTConnectVersions.cs
through SourceStripper.StripComments (a literal-aware C# comment walker)
before applying the Max / VersionXY regex pair. The stripper carried
non-trivial complexity (~200 LOC + literal handling for regular / verbatim
/ interpolated / interpolated-verbatim / char literals + a shared-source
link into the test csproj) to defend against exactly one hypothetical
scenario: a stale commented-out `// Max => Version27;` decoy above the
live `public static Version Max => Version29;` declaration fooling the
first-match regex into pinning the wrong PREV_VERSION.

That defence is achievable with a targeted anchor pattern alone. Both
regexes now line-anchor via `(?m)^[ \t]*public…`, which rejects any
match where a `//` sits between line start and `public`. 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; … */`
above the live declaration still loses to the live line below it. The
combined `line-anchor + last-match` guard preserves the observable
behaviour the `Commented_out_Max_declaration_does_not_confuse_the_parser`
regression pin verifies (the live version wins over both `//`-line and
block-comment decoys above it), without a comment-stripper walker.

Files deleted: build/MTConnect.NET-SysML-Import/SourceStripper.cs +
tests/MTConnect.NET-Generator-Tests/SourceStripperTests.cs + the
shared-source `<Compile Include="…/SourceStripper.cs" Link="…"/>` element
on the generator-tests csproj.

Also added: PREV == NEW guard. When the new XMI's filename encodes the
same major.minor as the auto-derived PREV_VERSION (matched via the
`_[vV]<major>.<minor>.xml` case-insensitive suffix so both the
`MTConnectSysMLModel_v<X>.<Y>.xml` and `MTConnectSysMLModel_V<X>.<Y>.xml`
naming variants land), the delta is empty by construction. The importer
writes a `warning: latest MTConnect version (v<X>.<Y>) is already
supported by MTConnectVersions.Max — no delta to derive; skipping emit.`
line to stderr and exits 0 (no-op emit). Un-suffixed filenames like the
default `MTConnectSysMLModel.xml` submodule snapshot fall through the
guard silently and reach the normal delta emitter.

Two new tests pin the guard in AutoDerivePreviousXmiTests:
`Prev_equals_new_warns_and_no_ops_when_new_xmi_filename_encodes_current_max`
exercises the guard-fires path; `Prev_equals_new_guard_stays_silent_when_new_xmi_filename_has_no_version_suffix`
exercises the guard-silent path (default un-suffixed filename must not
trigger the early return).

Closes F-IMP-504 (raw-string literal handling in the stripper) +
F-IMP-505 (preprocessor-directive handling) — both non-goals for a file
that has neither raw strings nor `#if` blocks.
@ottobolyos
ottobolyos force-pushed the feat/dry-generator-phase1-consolidation branch from d76aa54 to 7008734 Compare August 21, 2026 16:06
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
ottobolyos added a commit to ottobolyos/mtconnect.net that referenced this pull request Aug 21, 2026
…ntegration/up-to-pr-233

# Conflicts:
#	tests/MTConnect.NET-Common-Tests/V2_6_V2_7/V2_7DataItemTypeTests.cs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant