fix(common): coerce empty Result to UNAVAILABLE by value class - #217
Conversation
|
When I add a new observation to the latest c++ agent, I can add an empty string for an Event DataItem. This would be why I would like to make the default behavior (even if we just default the configuration flag to allow it) to allow empty strings to be passed. Also looking at the MTConnect Standard, I don't see where it explicitly prevents an empty string for "non vocabulary" events such as PROGRAM, MESSAGE, etc. I only see where it states to use UNAVAILABLE when a Valid Data Value cannot be determined, I don't see where it says an empty string is not a Valid Data Value for those Event types. Can you point me to where it says this? I hate to argue this but it could be a fairly significant breaking change for users so I want to make sure it doesn't cause any issues with existing implementations. Thanks! |
e64c5f8 to
b2dcd16
Compare
e52b8f0 to
e3c3305
Compare
|
@PatrickRitchie — thanks for the review, and apologies for the slow follow-up. I reshaped the PR to match what you asked for. The value-class-aware coerce now only fires on For the enum-Event case where you may still want the old "empty is meaningful" behaviour on a per-agent basis, there's a new Coverage floor added for the four combinations that matter: |
dotnet format --verify-no-changes was failing against master with hundreds of unformatted diagnostics (mostly tab/space indentation drift), so every PR built on top of it inherited a dirty baseline before touching a single line of its own. Runs `dotnet format MTConnect.NET.sln` (default warn severity, per the repository's existing .editorconfig rules) and commits the result: indentation, brace placement and other whitespace-only corrections across 61 files, concentrated in MTConnect.NET-Common, MTConnect.NET-HTTP and the build/ tooling projects. No behavioural changes; `dotnet build` succeeds cleanly on the result.
Adds a `format` job to the existing build-test-coverage workflow that runs `dotnet format MTConnect.NET.sln --verify-no-changes` on every push to master and every non-draft pull request targeting it, so formatting drift is caught before merge instead of silently compounding into the baseline. Uses the default (warn) severity rather than --severity info: at info severity dotnet format also attempts to auto-fix pre-existing Roslyn analyzer diagnostics across the repository, which is a separate, larger undertaking from verifying whitespace/style formatting and out of scope for this gate.
This is often used for messages and program names. SHDR should pass the value as is to the Agent and the Agent should then decide (based on validation level) whether to accept the value or not.
…nectDevices validation. This allows a device to be validated at a different level than observations/assets
The new `DeviceValidationLevel` property + enum added on this branch adds one config key row to `IAgentConfiguration` and `AgentConfiguration`; the drift gate `docs/scripts/generate-reference.sh --check` reports DRIFT until the generated `docs/reference/configuration.md` catches up. Runs the generator to produce the current output.
Classifies each DataItem into one of three value classes derived from the MTConnect Standard, Part 2 - Devices Information Model, and coerces null, empty, or whitespace-only Results to UNAVAILABLE only when the target value class forbids the empty string: * Numeric (all Samples, per the "Sample MUST always be reported in float" requirement, and numeric-typed Events enumerated by the SysML model - PART_COUNT, LINE_NUMBER, BLOCK_COUNT, HARDNESS, TOOL_OFFSET, and kindred integer/float Result types): always coerced. * Enumeration (Events whose Type has a controlled vocabulary, e.g. EXECUTION, CONTROLLER_MODE, AVAILABILITY): coerced by default; the AllowEmptyResultForEnumEvents configuration flag preserves the empty Result when integrators require parity with adapters that emit empty values. * String (free-form Event Types such as PROGRAM, MESSAGE, TOOL_ID, ASSET_CHANGED, and every other non-vocabulary Type): never coerced; the standard's default Observation::result value type is `string`, and the reference C++ agent accepts empty strings for these Events. Adds DataItemValueClass and DataItem.GetValueClass(IDataItem) to expose the classification, plus the AllowEmptyResultForEnumEvents flag on IAgentConfiguration/AgentConfiguration (default false). Regenerates docs/reference/configuration.md for the new flag.
Covers every arm of the classifier introduced on the same branch: * Numeric (SAMPLE, and numeric-typed Events like PART_COUNT): empty and whitespace Results are coerced to UNAVAILABLE under every input- validation level including Strict. * Enumeration Event (AVAILABILITY): the empty family is coerced when AllowEmptyResultForEnumEvents is false (the default), and preserved verbatim when the flag is true. A concrete vocabulary member is always preserved verbatim. * String Event (PROGRAM, MESSAGE, TOOL_ID): the empty family and arbitrary text are preserved verbatim, matching the reference C++ agent's behavior for non-vocabulary Event Types. * Direct GetValueClass assertions: classifies each representative DataItem into the correct value class.
…ontract Coverage FLOOR (CONVENTIONS §1.0d-trigies-novodecies) — extends the existing `AddObservationEmptyResultCoerceTests` and adds two new fixtures to close the audit gaps identified during Ultrareview cycle: 1. Numeric-Event allow-list exhaustiveness. Every entry in the SysML numeric-typed Event allow-list (ACTIVATION_COUNT, AXIS_FEEDRATE_OVERRIDE, BLOCK_COUNT, CYCLE_COUNT, DEACTIVATION_COUNT, HARDNESS, LINE_NUMBER, LOAD_COUNT, MATERIAL_LAYER, MEASUREMENT_VALUE, NETWORK_PORT, PART_COUNT, PART_INDEX, PATH_FEEDRATE_OVERRIDE, PROGRAM_NEST_LEVEL, ROTARY_VELOCITY_OVERRIDE, THICKNESS, TOOL_OFFSET, TRANSFER_COUNT, UNCERTAINTY, UNLOAD_COUNT) has both a direct `DataItem.GetValueClass` classifier assertion and an end-to-end `AddObservation` coerce assertion, so any drift between the DataItem.cs allow-list and the test surface is caught immediately. 2. DataItemValueClass switch-arm guard. Iterates every enum value and asserts each is reachable through `GetValueClass` on a representative DataItem, catching a silently-added arm that has no coerce-path branch. 3. `DeviceValidationLevelSplitTests` — new fixture pinning the split between `DeviceValidationLevel` (governs `NormalizeDevice`) and `InputValidationLevel` (governs the observation-input path). Covers every arm (Ignore, Warning, Remove, Strict) × every branch point (invalid Component, Composition, DataItem) and pins the independence invariant (`DeviceValidationLevel = Strict` rejects even when `InputValidationLevel = Ignore`, and the converse). 4. `ShdrDataItemParseEmptyValueTests` — new fixture pinning the parse-side complement of the coerce. `ShdrDataItem.FromString` previously dropped any key-value pair whose value token was missing (bare trailing key `avail|AVAILABLE|program` and trailing-pipe `program|`); the fix preserves such pairs with an empty-string Result so the agent's value-class-aware coerce runs at the correct layer. Verified RED against `upstream/master` (3 of 5 tests fail on pre-fix ShdrDataItem.cs); GREEN with the fix in place. All 84 Common-Tests observation-coerce/device-validation cases plus the 5 SHDR parse-empty-value cases pass on bluefin under `dotnet test -c Debug`. Composition: - §1.0d-trigies-octies (TDD-before-fix) — SHDR fixture proven RED pre-fix, GREEN post-fix. - §1.0d-trigies-novodecies (coverage FLOOR) — every enum arm covered, every allow-list entry pinned. - §10 + §10a (100 % + positive-and-negative) — Numeric coerce, Enum coerce, String preserve, and the flag-driven Enum-preserve escape hatch all covered.
Adds four RED cases to `AddObservationEmptyResultCoerceTests` that pin the value-class classifier and empty-Result coerce paths for the DATA_SET, TABLE, and TIME_SERIES representations on SAMPLE DataItems. Each case asserts that a legitimate structured payload survives the coerce path verbatim — Result key is not rewritten to UNAVAILABLE, SampleCount / Count is not overwritten to 0, and Samples / Entries survive: - `Sample_TimeSeries_Payload_Preserved_Not_Coerced` - `Sample_DataSet_Payload_Preserved_Not_Coerced` - `Sample_Table_Payload_Preserved_Not_Coerced` - `GetValueClass_Sample_NonValueRepresentation_Is_String` Spec authority: MTConnect Standard, Part 2 — Devices Information Model, Value Properties of Sample. The DATA_SET / TABLE / TIME_SERIES representations carry structured payloads (Entries, Cells, Samples) rather than a single Result. `TimeSeriesObservationInput`, `DataSetObservationInput`, and `TableObservationInput` legitimately omit the Result key by design — corrupting them with the UNAVAILABLE sentinel would break spec compliance for every legitimate multi-value observation. Reproduces the classifier hole on HEAD `7a0e8529`: `GetValueClass` returns Numeric for any SAMPLE DataItem regardless of Representation, so `IsEmptyResult(input)` sees a null Result key on a structured input and `ShouldCoerceEmptyResultToUnavailable` fires — overwriting Result with UNAVAILABLE and (via `IsUnavailable = true`) triggering the representation switch to set SampleCount / Count = 0. All four cases are RED against this commit; the immediately-following commit introduces the classifier gate that turns them GREEN. Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo
…ntation Extends the value-class classifier introduced in 5fd1130 so DATA_SET, TABLE, and TIME_SERIES observations bypass the empty-Result coerce path for both SAMPLE and EVENT categories. Before this fix, `GetValueClass` returned `Numeric` for every SAMPLE DataItem regardless of Representation, so a legitimate `TimeSeriesObservationInput` / `DataSetObservationInput` / `TableObservationInput` (which by design carries `ValueKeys.SampleN` / `Count` / structured `Entries` rather than `ValueKeys.Result`) tripped `IsEmptyResult` — the coerce then rewrote the observation to `UNAVAILABLE` and set `IsUnavailable = true`, which in turn caused the downstream representation switch in `AddObservation` to overwrite the caller's `SampleCount` / `Count` with `0`. Every legitimate multi-value SAMPLE observation was silently corrupted. The classifier now short-circuits every non-VALUE representation to `String` at the top of `GetValueClass`, mirroring the already-correct handling for non-VALUE EVENTs. Structured payloads are outside the empty-Result classifier's remit for both categories. Spec authority: MTConnect Standard, Part 2 — Devices Information Model, Value Properties of Sample and Representation. The RED cases added in the immediately-preceding commit turn GREEN with this change; the pre-existing 4013-case suite continues to pass unchanged. Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo
…lit and value-class coerce - `docs/concepts/agent-validation-events.md`: mermaid diagram now shows the two independent axes — device-shape validators (Invalid Component / Composition / DataItem / Device) branch on `DeviceValidationLevel`; observation and asset validators (Invalid Observation / Asset) branch on `InputValidationLevel`. Wire-up example and the contributor extension template updated to gate each noun on the appropriate axis, so a contributor adding a new device-shape validator does not accidentally wire it to `InputValidationLevel`. Cross-reference block enumerates both knobs. - `docs/concepts/observations.md`: new "Value-class-aware empty-Result coerce" subsection under "The Unavailable sentinel" pins the wire-visible contract — the value class each DataItem belongs to (Numeric, Enumeration, String), the empty-Result policy for each, and the representation-aware short-circuit that preserves DATA_SET / TABLE / TIME_SERIES payloads. - `libraries/MTConnect.NET-Common/Agents/InputValidationLevel.cs`: XML summary narrowed from "input data fails validation against the device model" to "an observation or asset input fails per-DataItem validation"; cross-references `DeviceValidationLevel` for the device-shape axis and documents the common integrator profile of `InputValidationLevel = Strict` alongside `DeviceValidationLevel = Warning`. Closes the documentation-audit findings raised in the Ultrareview cycle against `b2dcd165` — stale `InputValidationLevel` references in the validation-events concept page, missing subsection on the coerce policy in the observations concept page, and the stale surface summary on `InputValidationLevel`. Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo
…/null classifier
Closes the cycle-2 coverage-FLOOR gaps identified during Ultrareview:
1. Symmetric EVENT + non-VALUE representation coverage. The classifier's
non-VALUE short-circuit fires for BOTH SAMPLE and EVENT categories, but
only SAMPLE had end-to-end tests. Adds:
- `EnumEvent_DataSet_Payload_Preserved_Not_Coerced` (Availability + DATA_SET
Entries survive)
- `EnumEvent_Table_Payload_Preserved_Not_Coerced` (Availability + TABLE
Cells survive)
- `GetValueClass_Event_NonValueRepresentation_Is_String` (direct
classifier: EVENT + DATA_SET/TABLE/TIME_SERIES all short-circuit to
String, including a numeric-typed Event whose Type would otherwise
resolve to Numeric).
2. Explicit empty / whitespace Result on non-VALUE representations. The
cycle-1 SAMPLE tests exercise the no-Result-key case. Adds the caller-
writes-Result-explicitly path where `IsEmptyResult` returns true but
the classifier short-circuits to String, so the switch default in
`ShouldCoerceEmptyResultToUnavailable` returns false and the payload
survives verbatim:
- `Sample_TimeSeries_Explicit_EmptyResult_Preserved` (Result="", Samples
+ SampleCount survive)
- `Sample_DataSet_Explicit_WhitespaceResult_Preserved` (Result=" ",
Count survives)
- `Sample_Table_Explicit_EmptyResult_Preserved` (Result="", Count
survives)
3. `GetValueClass_Condition_Is_String` pins the CONDITION short-circuit
added at the top of `GetValueClass` alongside the non-VALUE gate. The
pre-fix code also delivered String for CONDITION (via the `Category !=
EVENT` fallthrough) but neither state has a direct test.
4. `GetValueClass_Null_DataItem_Is_String` pins the null-guard so a caller
with an unresolved DataItem observes the fail-safe classification
rather than a `NullReferenceException`.
All 81 `AddObservationEmptyResultCoerce` fixture cases pass on bluefin
under `dotnet test -c Debug`. The three explicit-empty-Result SAMPLE
tests would fail against pre-fix `b2dcd165` (coerce would fire and stamp
Result=UNAVAILABLE + IsUnavailable=true + Count/SampleCount=0); they turn
GREEN with the classifier gate at `8909eb41`. The remaining five cases
close coverage-FLOOR branches that pre-fix already handled correctly but
lacked test surface.
Composition:
- CONVENTIONS §1.0d-trigies-novodecies (coverage FLOOR)
- §10 + §10a (100 % + positive-and-negative)
- §1.0d-trigies-octies (TDD-before-fix satisfied by cycle-1 09bed7c for
the non-VALUE-SAMPLE bug class; this commit extends coverage rather
than introducing a new fix)
Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo
…CLI + agent README Closes the three residual cycle-2 documentation-audit findings against head 0281e9b: - `docs/concepts/agent-validation-events.md` (contributor test template): the step-4 hypothetical `InvalidDeviceModelAdded` test now configures `DeviceValidationLevel = Strict` (matching step 3, which categorises DeviceModel as a device-shape noun). A contributor copying the template would previously have got a test that exercised the wrong axis. - `docs/cli/agent.md` (top-level config keys table): adds rows for `deviceValidationLevel` (Ignore(0) / Warning(1) / Remove(2) / Strict(3), default Warning, governs the Invalid Component / Composition / DataItem raise-sites) and `allowEmptyResultForEnumEvents` (bool, default false, preserves empty Result for controlled-vocabulary Events). Existing `inputValidationLevel` row narrowed to the observation / asset axis it actually governs post-split. - `agent/MTConnect.NET-Agent/README.md`: fixes the stale `inputValidationLevel` bullet (previously called level 2 "Strict" and omitted "Remove") and adds companion bullets for the two new peer keys. Claude-Session: https://claude.ai/code/session_0162RfaA55VT8NX6QfU7RUVo
e3c3305 to
6b214d6
Compare
Sibling of 8e5eed8 (ReadRequestBytes unify) — deletes the NET9_0_OR_GREATER guard around ReadExactlyAsync in LimitedBodyStream.DiscardAllAsync so the drain path uses the same accumulator loop on every TFM. ReadExactlyAsync into a fixed 8 KB buffer throws EndOfStreamException on any body smaller than 8 KB and on the final iteration of larger drains — that exception then propagates through the outer HttpServer catch, kills keep-alive, and 500s the client. The uniform ReadAsync loop treats a 0-byte read as premature EOF and signals the caller cleanly. Extracted from PR TrakHound#219 cycle-2 F-CR-201 during the 2026-08-20 clean-split of the empty-Result / multi-TFM / warnings-cleanup three-way commit contamination — the other content of the original mixed commit belongs to PR TrakHound#217, so only the LimitedBodyStream.cs change ships here.
Adds a testing/workflows.md section describing the release-pack job introduced by 1e96847 (the CI matrix gate) — trigger scope, exit contract, and the local dotnet pack repro command. The docs make it explicit that the release-pack matrix runs the full net461 → net9.0 sweep on every push and non-draft PR, catching the class of Release-only CS/NU/CA/SYSLIB diagnostics the Debug matrix does not exercise. Extracted from PR TrakHound#219 cycle-1 F-DOC-005 during the 2026-08-20 clean-split — the other content of the mixed source commit belongs to PR TrakHound#217 (DeviceValidationLevel docs), so only the workflows.md addition ships here.
Sibling of 8e5eed8 (ReadRequestBytes unify) — deletes the NET9_0_OR_GREATER guard around ReadExactlyAsync in LimitedBodyStream.DiscardAllAsync so the drain path uses the same accumulator loop on every TFM. ReadExactlyAsync into a fixed 8 KB buffer throws EndOfStreamException on any body smaller than 8 KB and on the final iteration of larger drains — that exception then propagates through the outer HttpServer catch, kills keep-alive, and 500s the client. The uniform ReadAsync loop treats a 0-byte read as premature EOF and signals the caller cleanly. Extracted from PR TrakHound#219 cycle-2 F-CR-201 during the 2026-08-20 clean-split of the empty-Result / multi-TFM / warnings-cleanup three-way commit contamination — the other content of the original mixed commit belongs to PR TrakHound#217, so only the LimitedBodyStream.cs change ships here.
Adds a testing/workflows.md section describing the release-pack job introduced by 1e96847 (the CI matrix gate) — trigger scope, exit contract, and the local dotnet pack repro command. The docs make it explicit that the release-pack matrix runs the full net461 → net9.0 sweep on every push and non-draft PR, catching the class of Release-only CS/NU/CA/SYSLIB diagnostics the Debug matrix does not exercise. Extracted from PR TrakHound#219 cycle-1 F-DOC-005 during the 2026-08-20 clean-split — the other content of the mixed source commit belongs to PR TrakHound#217 (DeviceValidationLevel docs), so only the workflows.md addition ships here.
Summary
Reshapes the empty-Result coerce on
MTConnectAgent.AddObservationfrom an unconditional rewrite to a value-class-aware one, per @PatrickRitchie's discussion feedback (comment 5322277097). The MTConnect Standard, Part 2 - Devices Information Model, ties Result validity to the DataItem's category and (for Events) its controlled vocabulary; the SDK now coerces null / empty / whitespace Results only where the value class forbids the empty string.Three value classes are derived from the DataItem's category, representation, and type:
resultattribute): empty Results are always coerced.MTConnect.Observations.Events.<Type>enum): empty Results are coerced by default; the newAllowEmptyResultForEnumEventsconfiguration flag preserves the empty Result when an integrator needs parity with adapters that emit empty values for these Events.Observation::resultdefault value type isstring, does not forbid the empty string, and the reference C++ agent accepts empty strings for these Events.CONDITION observations are untouched (their state axis - Normal / Warning / Fault / Unavailable - is handled separately via
ConditionLevel). DATA_SET / TABLE / TIME_SERIES representations carry structured payloads (Entries, Cells, Samples) rather than a single Result and are short-circuited to the String class - their inputs legitimately omit the Result key by design, so the coerce path leaves them untouched for both SAMPLE and EVENT categories.Behavior change
AMPERAGE = ""UNAVAILABLEPART_COUNT = ""integer)UNAVAILABLEEXECUTION = ""(flag default)UNAVAILABLEEXECUTION = ""(flag = true)""(verbatim)PROGRAM = """"(verbatim)MESSAGE = """"(verbatim)TOOL_ID = """"(verbatim)ASSET_CHANGED = """"(verbatim)X(Representation = TIME_SERIES / DATA_SET / TABLE) with real payloadUNAVAILABLEinjection, noSampleCount/CountoverwriteStrict input-validation continues to coerce rather than silently drop for the Numeric and Enumeration classes.
New public surface
MTConnect.Devices.DataItems.DataItemValueClass-String/Enumeration/Numeric.DataItem.GetValueClass(IDataItem)- static classifier, representation-aware for both SAMPLE and EVENT.IAgentConfiguration.AllowEmptyResultForEnumEvents/AgentConfiguration.AllowEmptyResultForEnumEvents- defaultfalse, serialized keyallowEmptyResultForEnumEvents.MTConnect.Agents.DeviceValidationLevel-Ignore/Warning/Remove/Strict.IAgentConfiguration.DeviceValidationLevel/AgentConfiguration.DeviceValidationLevel- defaultWarning, serialized keydeviceValidationLevel; independent ofInputValidationLevelso integrators can pick each axis separately (a common profile isInputValidationLevel = StrictalongsideDeviceValidationLevel = Warning).Tests
tests/MTConnect.NET-Common-Tests/Agents/AddObservationEmptyResultCoerceTests.cs(extended):UNAVAILABLEinjection, noSampleCount/Countoverwrite, Samples / Entries / Cells survive.false(default); preserved verbatim whentrue; concrete vocabulary member always preserved.DataItemValueClassenum-arm reachability guard: iterates every enum value to confirm each is reachable through the classifier; a new arm added without a coerce-path branch trips the guard.GetValueClassassertions across representative DataItems (VALUE + non-VALUE representations for SAMPLE, controlled-vocabulary EVENTs, free-form EVENTs, numeric-typed EVENTs).tests/MTConnect.NET-Common-Tests/Agents/DeviceValidationLevelSplitTests.cs(new): every arm (Ignore / Warning / Remove / Strict) x every branch point (invalid Component, Composition, DataItem) pinned; independence fromInputValidationLevelverified; configuration defaults pinned toWarning.tests/MTConnect.NET-SHDR-Tests/ShdrDataItemParseEmptyValueTests.cs(new): parse-side coverage for the SHDR empty-value pass-through - trailingkey|, bare-trailing-key, and multi-pair variants each yield an empty Result rather than being dropped at the parser, letting the agent's value-class coerce run at the right layer.Full solution
dotnet testmatrix on this branch (excluding E2E): 5,098 unit / integration tests across MTConnect.NET-Common-Tests (4,072), MTConnect.NET-SHDR-Tests (40), MTConnect.NET-JSON-Tests (63), MTConnect.NET-JSON-cppagent-Tests (363), MTConnect.NET-Docs-Tests (67), MTConnect.NET-XML-Tests (98), MTConnect.NET-HTTP-Tests (112), MTConnect-Compliance-Tests (221), MTConnect.NET-AgentModule-MqttRelay-Tests (62) - all green.dotnet format --verify-no-changesreturns 0.Files touched
libraries/MTConnect.NET-Common/Devices/DataItems/DataItemValueClass.cs- new enum.libraries/MTConnect.NET-Common/Devices/DataItem.cs-GetValueClassclassifier with an enum-type reflection lookup, a numeric-Event allow-list mirrored from the SysML model, and a representation-aware short-circuit that returnsStringfor DATA_SET / TABLE / TIME_SERIES on both SAMPLE and EVENT categories.libraries/MTConnect.NET-Common/Agents/MTConnectAgent.cs- value-class-driven coerce inAddObservation;ShouldCoerceEmptyResultToUnavailable,IsEmptyResult, andCoerceEmptyResultToUnavailablehelpers.libraries/MTConnect.NET-Common/Agents/DeviceValidationLevel.cs- new enum split fromInputValidationLevel.libraries/MTConnect.NET-Common/Agents/InputValidationLevel.cs- summary narrowed to observation / asset input; device-shape validation now handled byDeviceValidationLevel.libraries/MTConnect.NET-Common/Configurations/IAgentConfiguration.cs+AgentConfiguration.cs- the newAllowEmptyResultForEnumEventsflag andDeviceValidationLevelproperty.libraries/MTConnect.NET-SHDR/Shdr/ShdrDataItem.cs- pass empty SHDR values through to the agent rather than dropping them at the parser.docs/concepts/agent-validation-events.md- mermaid, wire-up, and contributor extension guide updated for the InputValidationLevel / DeviceValidationLevel split.docs/concepts/observations.md- new "Value-class-aware empty-Result coerce" subsection under "The Unavailable sentinel".docs/reference/configuration.md- regenerated to surface the new flags.tests/MTConnect.NET-Common-Tests/Agents/AddObservationEmptyResultCoerceTests.cs- extended.tests/MTConnect.NET-Common-Tests/Agents/DeviceValidationLevelSplitTests.cs- new fixture.tests/MTConnect.NET-SHDR-Tests/ShdrDataItemParseEmptyValueTests.cs- new fixture.Depends on
Dime review cycle 1
GetValueClassreturnedNumericfor every SAMPLE regardless of representation, soTimeSeriesObservationInput/DataSetObservationInput/TableObservationInput(which by design omit the Result key) tripped the coerce and were silently corrupted toUNAVAILABLEwithSampleCount/Countoverwritten to0. FIXED: classifier gated onRepresentation == VALUEfor SAMPLE, matching the already-correct handling for non-VALUE EVENTs; four RED-first fixture cases pin the fix. MEDIUMIAgentConfigurationsurface addition is a source-compat break: intentional in v7 major and enumerated under "New public surface" above. MEDIUMAllowEmptyResultForEnumEventsdefault preserves back-compat for adapters emitting empty vocabulary values: intentional per @PatrickRitchie's feedback. LOW findings (legacyinputValidationLevelmigration, cross-assembly enum lookup): tracked for a follow-up PR - out of scope for this fix.Assembly.GetTypeargument (Assembly.GetTypecorrectly ignores assembly qualifiers so no cross-assembly load is possible;ArgumentExceptionfall-through kept as advisory), SHDR pass-through amplification (matches spec intent). All LOW - tracked, no atomic fix required.IsEmptyResult, droppreservedpre-filter inCoerceEmptyResultToUnavailable). Both safe but not required; not applied in this cycle to keep the diff minimal.EmptyResultCoercedmulticast event;IDataItem.ValueClassextension surface; user-extensibleRegisterCustomValueClassseam. All follow-up (impact MEDIUM at most, no correctness gap).agent-validation-events.mdfor the InputValidationLevel / DeviceValidationLevel split), 2 MEDIUM (missingobservations.mdsubsection, missing PR-body enumeration ofDeviceValidationLevel), 1 LOW (staleInputValidationLevel.cssummary). ALL FIXED: mermaid + prose + contributor wire-up template updated inagent-validation-events.md; new "Value-class-aware empty-Result coerce" subsection landed inobservations.md;DeviceValidationLevelenumerated above;InputValidationLevel.cssummary narrowed to observation / asset input.7a0e8529): numeric-Event allow-list exhaustiveness (21 entries),DataItemValueClassenum-arm reachability guard, dedicatedDeviceValidationLevelSplitTestsfixture (12 cases), SHDR parse-side pass-through fixture (5 cases with retroactive RED proof againstupstream/master), configuration defaults pinned. One LOW cross-serializer wire-format test tracked (JSON / XML test projects do not reference MTConnect.NET-Common; needs Integration-Tests scaffolding out of scope for this cycle).Dime review cycle 2
09bed7c0(4 RED tests): assertions bind to observable state that flipped under the pre-fix classifier; not tautological, not helper-masked. Delta8909eb41(GetValueClassreorder): semantic change confined toSAMPLE + non-VALUE → String; every other input class returns the same value class as before. Delta13b655f2(docs): mermaid + prose match the actual raise-sites (DeviceValidationLevelgatesInvalidComponentAdded/InvalidCompositionAdded/InvalidDataItemAddedat theNormalizeDevicesites;InputValidationLevelgatesInvalidObservationAdded/InvalidAssetAdded). Code snippets compile.agent-validation-events.md:193contributor test template still wiredInputValidationLevelfor a device-shape noun; MEDIUMdocs/cli/agent.mdtop-level config-keys table omitted the two new keys; LOWagent/MTConnect.NET-Agent/README.md:255inputValidationLevelbullet had the wrong level mapping (missing Remove) and no companion bullets. ALL FIXED in commite52b8f0d.0281e9b7, 8 new tests): explicit-empty-Result on TIME_SERIES / DATA_SET / TABLE SAMPLE (three HIGH — the whitespace-only case exercises a caller-writes-Result="" path the cycle-1 tests did not cover); EVENT + DATA_SET / EVENT + TABLE payload preservation (two MEDIUM — symmetric coverage of the classifier's short-circuit); direct classifier assertion for EVENT + non-VALUE where Representation must trump the SysML numeric allow-list (one MEDIUM); direct classifier assertion for CONDITION short-circuit (LOW); direct null-guard test (LOW). Cycle-1 RED-vs-GREEN spot-check confirmed via diff inspection:b2dcd165classifier returns Numeric for SAMPLE regardless of Representation (RED);8909eb41adds theRepresentation != VALUE ⇒ Stringshort-circuit ahead of the SAMPLE branch (GREEN). TDD ordering satisfies the RED-first rule.(Zero unfixed findings — Ready-eligible.)