From 42ec5db2f351ca39962e396cdb35f8f6f9f60fe7 Mon Sep 17 00:00:00 2001
From: AriusII <35581726+AriusII@users.noreply.github.com>
Date: Mon, 21 Sep 2026 22:42:26 +0200
Subject: [PATCH] Expose structured capability evidence reason code
---
docs/adr/0004-capability-matrix.md | 3 +
.../PublicAPI.Unshipped.txt | 8 +
.../CheatEngine.Client.Abstractions/README.md | 4 +
.../Runtime/ClientCapabilityEvidence.cs | 61 ++++--
.../ClientCapabilityEvidenceReasonCode.cs | 23 ++
.../Runtime/ClientCapabilityEvidenceTests.cs | 206 ++++++++++++++++++
tests/CheatEngine.Client.AotProbe/Program.cs | 15 ++
7 files changed, 300 insertions(+), 20 deletions(-)
create mode 100644 libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidenceReasonCode.cs
create mode 100644 tests/CheatEngine.Client.Abstractions.Tests/Runtime/ClientCapabilityEvidenceTests.cs
diff --git a/docs/adr/0004-capability-matrix.md b/docs/adr/0004-capability-matrix.md
index f767687..ff29f2d 100644
--- a/docs/adr/0004-capability-matrix.md
+++ b/docs/adr/0004-capability-matrix.md
@@ -65,6 +65,9 @@ been live-qualified.
or host-side behavior that ordinary CI has not established.
- The matrix gives reviewers one place to distinguish a deliberate product gate from a missing implementation, keeping
package release claims honest as SDK evidence changes.
+- Capability evidence exposes the stable `EffectiveReasonCode` of the gate that supplied its display reason. Consumers
+ use that code with the gate's independently observed state rather than parsing text or reproducing the Client's
+ priority order; it does not promote an unknown, faulted, malformed, or unqualified capability to `Available`.
## Current template boundary
diff --git a/libs/CheatEngine.Client.Abstractions/PublicAPI.Unshipped.txt b/libs/CheatEngine.Client.Abstractions/PublicAPI.Unshipped.txt
index 4e36c4c..2ada242 100644
--- a/libs/CheatEngine.Client.Abstractions/PublicAPI.Unshipped.txt
+++ b/libs/CheatEngine.Client.Abstractions/PublicAPI.Unshipped.txt
@@ -37,6 +37,7 @@ CheatEngine.Client.Runtime.ClientCapabilityEvidence.ClientCapabilityEvidence() -
CheatEngine.Client.Runtime.ClientCapabilityEvidence.ClientCapabilityEvidence(CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate implementation, CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate package, CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate host, CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate liveQualification, CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate policy, CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate lifetime) -> void
CheatEngine.Client.Runtime.ClientCapabilityEvidence.AvailabilityState.get -> CheatEngine.Client.Runtime.ClientCapabilityAvailabilityState
CheatEngine.Client.Runtime.ClientCapabilityEvidence.EffectiveReason.get -> string!
+CheatEngine.Client.Runtime.ClientCapabilityEvidence.EffectiveReasonCode.get -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceReasonCode
CheatEngine.Client.Runtime.ClientCapabilityEvidence.Host.get -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate
CheatEngine.Client.Runtime.ClientCapabilityEvidence.Implementation.get -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate
CheatEngine.Client.Runtime.ClientCapabilityEvidence.IsExecutable.get -> bool
@@ -52,6 +53,13 @@ CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate.IsSatisfied.get -> bool
CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate.Reason.get -> string!
CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate.State.get -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceState
CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate.Equals(CheatEngine.Client.Runtime.ClientCapabilityEvidenceGate other) -> bool
+CheatEngine.Client.Runtime.ClientCapabilityEvidenceReasonCode
+CheatEngine.Client.Runtime.ClientCapabilityEvidenceReasonCode.Host = 2 -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceReasonCode
+CheatEngine.Client.Runtime.ClientCapabilityEvidenceReasonCode.Implementation = 0 -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceReasonCode
+CheatEngine.Client.Runtime.ClientCapabilityEvidenceReasonCode.Lifetime = 5 -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceReasonCode
+CheatEngine.Client.Runtime.ClientCapabilityEvidenceReasonCode.LiveQualification = 3 -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceReasonCode
+CheatEngine.Client.Runtime.ClientCapabilityEvidenceReasonCode.Package = 1 -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceReasonCode
+CheatEngine.Client.Runtime.ClientCapabilityEvidenceReasonCode.Policy = 4 -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceReasonCode
CheatEngine.Client.Runtime.ClientCapabilityEvidenceState
CheatEngine.Client.Runtime.ClientCapabilityEvidenceState.Faulted = 3 -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceState
CheatEngine.Client.Runtime.ClientCapabilityEvidenceState.Malformed = 4 -> CheatEngine.Client.Runtime.ClientCapabilityEvidenceState
diff --git a/libs/CheatEngine.Client.Abstractions/README.md b/libs/CheatEngine.Client.Abstractions/README.md
index bb98602..64def6b 100644
--- a/libs/CheatEngine.Client.Abstractions/README.md
+++ b/libs/CheatEngine.Client.Abstractions/README.md
@@ -79,6 +79,10 @@ host observation, live qualification, policy, and activation lifetime are distin
missing, faulted, and malformed host observations remain distinguishable instead of being collapsed into a generic
unavailable result.
+`Evidence.EffectiveReasonCode` is the stable, typed identity of the gate supplying `Evidence.EffectiveReason`; use it
+with that gate's public state instead of parsing the human-readable reason text or duplicating the Client's deterministic
+priority. The reason text remains available for display and diagnostics.
+
In particular, the value-scan contract and state model are published, but the Core implementation
does **not** currently create a live `MemScan`/`FoundList` session. The next SDK line now contains a
production owner factory with parent rollback and child-before-parent teardown, but Client
diff --git a/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidence.cs b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidence.cs
index 8917c9d..a27f181 100644
--- a/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidence.cs
+++ b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidence.cs
@@ -86,100 +86,121 @@ public ClientCapabilityAvailabilityState AvailabilityState
public bool IsExecutable => AllSatisfied;
/// Gets the reason for the highest-priority missing, faulted, malformed, or unknown prerequisite.
- public string EffectiveReason => GetEffectiveGate().Reason;
+ public string EffectiveReason => GetGate(EffectiveReasonCode).Reason;
+
+ ///
+ /// Gets the stable code for the evidence gate that supplies . Its state remains
+ /// available through the corresponding evidence-gate property.
+ ///
+ public ClientCapabilityEvidenceReasonCode EffectiveReasonCode => GetEffectiveReasonCode();
private bool AllSatisfied =>
Implementation.IsSatisfied && Package.IsSatisfied && Host.IsSatisfied && LiveQualification.IsSatisfied &&
Policy.IsSatisfied && Lifetime.IsSatisfied;
- private ClientCapabilityEvidenceGate GetEffectiveGate()
+ private ClientCapabilityEvidenceReasonCode GetEffectiveReasonCode()
{
if (Lifetime.State == ClientCapabilityEvidenceState.Missing)
{
- return Lifetime;
+ return ClientCapabilityEvidenceReasonCode.Lifetime;
}
if (Policy.State == ClientCapabilityEvidenceState.Missing)
{
- return Policy;
+ return ClientCapabilityEvidenceReasonCode.Policy;
}
if (Implementation.State == ClientCapabilityEvidenceState.Missing)
{
- return Implementation;
+ return ClientCapabilityEvidenceReasonCode.Implementation;
}
if (Package.State == ClientCapabilityEvidenceState.Missing)
{
- return Package;
+ return ClientCapabilityEvidenceReasonCode.Package;
}
if (Host.State == ClientCapabilityEvidenceState.Missing)
{
- return Host;
+ return ClientCapabilityEvidenceReasonCode.Host;
}
if (LiveQualification.State == ClientCapabilityEvidenceState.Missing)
{
- return LiveQualification;
+ return ClientCapabilityEvidenceReasonCode.LiveQualification;
}
if (Host.State is ClientCapabilityEvidenceState.Faulted or ClientCapabilityEvidenceState.Malformed)
{
- return Host;
+ return ClientCapabilityEvidenceReasonCode.Host;
}
if (Package.State is ClientCapabilityEvidenceState.Faulted or ClientCapabilityEvidenceState.Malformed)
{
- return Package;
+ return ClientCapabilityEvidenceReasonCode.Package;
}
if (LiveQualification.State is ClientCapabilityEvidenceState.Faulted or ClientCapabilityEvidenceState.Malformed)
{
- return LiveQualification;
+ return ClientCapabilityEvidenceReasonCode.LiveQualification;
}
if (Implementation.State is ClientCapabilityEvidenceState.Faulted or ClientCapabilityEvidenceState.Malformed)
{
- return Implementation;
+ return ClientCapabilityEvidenceReasonCode.Implementation;
}
if (Policy.State is ClientCapabilityEvidenceState.Faulted or ClientCapabilityEvidenceState.Malformed)
{
- return Policy;
+ return ClientCapabilityEvidenceReasonCode.Policy;
}
if (Lifetime.State is ClientCapabilityEvidenceState.Faulted or ClientCapabilityEvidenceState.Malformed)
{
- return Lifetime;
+ return ClientCapabilityEvidenceReasonCode.Lifetime;
}
if (Host.State == ClientCapabilityEvidenceState.Unknown)
{
- return Host;
+ return ClientCapabilityEvidenceReasonCode.Host;
}
if (Package.State == ClientCapabilityEvidenceState.Unknown)
{
- return Package;
+ return ClientCapabilityEvidenceReasonCode.Package;
}
if (LiveQualification.State == ClientCapabilityEvidenceState.Unknown)
{
- return LiveQualification;
+ return ClientCapabilityEvidenceReasonCode.LiveQualification;
}
if (Implementation.State == ClientCapabilityEvidenceState.Unknown)
{
- return Implementation;
+ return ClientCapabilityEvidenceReasonCode.Implementation;
}
if (Policy.State == ClientCapabilityEvidenceState.Unknown)
{
- return Policy;
+ return ClientCapabilityEvidenceReasonCode.Policy;
}
- return Lifetime;
+ return ClientCapabilityEvidenceReasonCode.Lifetime;
+ }
+
+ private ClientCapabilityEvidenceGate GetGate(ClientCapabilityEvidenceReasonCode reasonCode)
+ {
+ return reasonCode switch
+ {
+ ClientCapabilityEvidenceReasonCode.Implementation => Implementation,
+ ClientCapabilityEvidenceReasonCode.Package => Package,
+ ClientCapabilityEvidenceReasonCode.Host => Host,
+ ClientCapabilityEvidenceReasonCode.LiveQualification => LiveQualification,
+ ClientCapabilityEvidenceReasonCode.Policy => Policy,
+ ClientCapabilityEvidenceReasonCode.Lifetime => Lifetime,
+ _ => throw new ArgumentOutOfRangeException(nameof(reasonCode), reasonCode,
+ "The Client capability evidence reason code is not defined.")
+ };
}
private bool HasState(ClientCapabilityEvidenceState state)
diff --git a/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidenceReasonCode.cs b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidenceReasonCode.cs
new file mode 100644
index 0000000..fa35b75
--- /dev/null
+++ b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidenceReasonCode.cs
@@ -0,0 +1,23 @@
+namespace CheatEngine.Client.Runtime;
+
+/// Identifies the evidence gate that supplies .
+public enum ClientCapabilityEvidenceReasonCode : byte
+{
+ /// The operational Client adapter gate supplies the effective reason.
+ Implementation = 0,
+
+ /// The consumed package artifact gate supplies the effective reason.
+ Package = 1,
+
+ /// The Cheat Engine host observation gate supplies the effective reason.
+ Host = 2,
+
+ /// The live host-qualification gate supplies the effective reason.
+ LiveQualification = 3,
+
+ /// The Client policy gate supplies the effective reason.
+ Policy = 4,
+
+ /// The activation-lifetime gate supplies the effective reason.
+ Lifetime = 5
+}
diff --git a/tests/CheatEngine.Client.Abstractions.Tests/Runtime/ClientCapabilityEvidenceTests.cs b/tests/CheatEngine.Client.Abstractions.Tests/Runtime/ClientCapabilityEvidenceTests.cs
new file mode 100644
index 0000000..5700d2e
--- /dev/null
+++ b/tests/CheatEngine.Client.Abstractions.Tests/Runtime/ClientCapabilityEvidenceTests.cs
@@ -0,0 +1,206 @@
+using CheatEngine.Client.Runtime;
+
+namespace CheatEngine.Client.Abstractions.Tests.Runtime;
+
+public sealed class ClientCapabilityEvidenceTests
+{
+ [Theory]
+ [MemberData(nameof(EffectiveReasonPriorityCases))]
+ public void EffectiveReasonCodePreservesEveryGatePriority(
+ ClientCapabilityEvidenceState state,
+ ClientCapabilityEvidenceReasonCode expectedReasonCode,
+ ClientCapabilityAvailabilityState expectedAvailabilityState)
+ {
+ ClientCapabilityEvidence evidence = CreateEvidenceForPriority(state, expectedReasonCode);
+ ClientCapabilityAvailability availability = new(ClientCapabilityId.ProcessSelection, evidence);
+
+ Assert.Equal(expectedReasonCode, evidence.EffectiveReasonCode);
+ Assert.Equal(ReasonFor(expectedReasonCode), evidence.EffectiveReason);
+ Assert.Equal(expectedAvailabilityState, evidence.AvailabilityState);
+ Assert.Equal(expectedAvailabilityState, availability.State);
+ Assert.Equal(evidence.EffectiveReason, availability.Reason);
+ }
+
+ [Fact]
+ public void MissingReasonWinsOverFaultedMalformedAndUnknownReasons()
+ {
+ ClientCapabilityEvidence evidence = new(
+ new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Unknown, ReasonFor(
+ ClientCapabilityEvidenceReasonCode.Implementation)),
+ new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Malformed, ReasonFor(
+ ClientCapabilityEvidenceReasonCode.Package)),
+ new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Faulted, ReasonFor(
+ ClientCapabilityEvidenceReasonCode.Host)),
+ new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Missing, ReasonFor(
+ ClientCapabilityEvidenceReasonCode.LiveQualification)),
+ new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Unknown, ReasonFor(
+ ClientCapabilityEvidenceReasonCode.Policy)),
+ new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Satisfied, ReasonFor(
+ ClientCapabilityEvidenceReasonCode.Lifetime)));
+
+ Assert.Equal(ClientCapabilityEvidenceReasonCode.LiveQualification, evidence.EffectiveReasonCode);
+ Assert.Equal(ReasonFor(ClientCapabilityEvidenceReasonCode.LiveQualification), evidence.EffectiveReason);
+ Assert.Equal(ClientCapabilityAvailabilityState.Unavailable, evidence.AvailabilityState);
+ }
+
+ [Theory]
+ [InlineData(ClientCapabilityEvidenceState.Faulted)]
+ [InlineData(ClientCapabilityEvidenceState.Malformed)]
+ public void FaultedAndMalformedReasonsWinOverUnknownReasons(ClientCapabilityEvidenceState failedState)
+ {
+ ClientCapabilityEvidence evidence = new(
+ new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Satisfied, ReasonFor(
+ ClientCapabilityEvidenceReasonCode.Implementation)),
+ new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Satisfied, ReasonFor(
+ ClientCapabilityEvidenceReasonCode.Package)),
+ new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Unknown, ReasonFor(
+ ClientCapabilityEvidenceReasonCode.Host)),
+ new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Satisfied, ReasonFor(
+ ClientCapabilityEvidenceReasonCode.LiveQualification)),
+ new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Satisfied, ReasonFor(
+ ClientCapabilityEvidenceReasonCode.Policy)),
+ new ClientCapabilityEvidenceGate(failedState, ReasonFor(ClientCapabilityEvidenceReasonCode.Lifetime)));
+
+ Assert.Equal(ClientCapabilityEvidenceReasonCode.Lifetime, evidence.EffectiveReasonCode);
+ Assert.Equal(ReasonFor(ClientCapabilityEvidenceReasonCode.Lifetime), evidence.EffectiveReason);
+ Assert.Equal(ClientCapabilityAvailabilityState.Unknown, evidence.AvailabilityState);
+ }
+
+ [Fact]
+ public void AllSatisfiedEvidenceUsesTheLifetimeReasonCode()
+ {
+ ClientCapabilityEvidence evidence = CreateEvidenceForPriority(
+ ClientCapabilityEvidenceState.Satisfied, ClientCapabilityEvidenceReasonCode.Lifetime);
+
+ Assert.Equal(ClientCapabilityEvidenceReasonCode.Lifetime, evidence.EffectiveReasonCode);
+ Assert.Equal(ReasonFor(ClientCapabilityEvidenceReasonCode.Lifetime), evidence.EffectiveReason);
+ Assert.True(evidence.IsExecutable);
+ Assert.Equal(ClientCapabilityAvailabilityState.Available, evidence.AvailabilityState);
+ }
+
+ [Theory]
+ [InlineData(ClientCapabilityAvailabilityState.Available, ClientCapabilityEvidenceReasonCode.Lifetime)]
+ [InlineData(ClientCapabilityAvailabilityState.Unavailable, ClientCapabilityEvidenceReasonCode.Lifetime)]
+ [InlineData(ClientCapabilityAvailabilityState.Unknown, ClientCapabilityEvidenceReasonCode.Host)]
+ public void LegacyAvailabilityConstructionRetainsStateAndDisplayReason(
+ ClientCapabilityAvailabilityState state,
+ ClientCapabilityEvidenceReasonCode expectedReasonCode)
+ {
+ const string reason = "Legacy display reason.";
+ ClientCapabilityAvailability availability = new(ClientCapabilityId.ProcessSelection, state, reason);
+
+ Assert.Equal(state, availability.State);
+ Assert.Equal(expectedReasonCode, availability.Evidence.EffectiveReasonCode);
+ Assert.Equal(reason, availability.Evidence.EffectiveReason);
+ Assert.Equal(reason, availability.Reason);
+ Assert.Equal(state == ClientCapabilityAvailabilityState.Available, availability.IsAvailable);
+ Assert.Equal(state != ClientCapabilityAvailabilityState.Unknown, availability.IsKnown);
+ }
+
+ [Fact]
+ public void EffectiveReasonCodesUseStableUnderlyingValues()
+ {
+ Assert.Equal((byte) 0, (byte) ClientCapabilityEvidenceReasonCode.Implementation);
+ Assert.Equal((byte) 1, (byte) ClientCapabilityEvidenceReasonCode.Package);
+ Assert.Equal((byte) 2, (byte) ClientCapabilityEvidenceReasonCode.Host);
+ Assert.Equal((byte) 3, (byte) ClientCapabilityEvidenceReasonCode.LiveQualification);
+ Assert.Equal((byte) 4, (byte) ClientCapabilityEvidenceReasonCode.Policy);
+ Assert.Equal((byte) 5, (byte) ClientCapabilityEvidenceReasonCode.Lifetime);
+ }
+
+ public static IEnumerable