Skip to content

Remediate the 2026-09-22 audit and overhaul CI/CD - #86

Merged
AriusII merged 202 commits into
mainfrom
feat/audit-remediation-cicd
Sep 23, 2026
Merged

AriusII merged 202 commits into
mainfrom
feat/audit-remediation-cicd

Conversation

@AriusII

@AriusII AriusII commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Work in progress — do not merge. This pull request is the CI vehicle for the audit remediation branch. It will be marked ready once every wave below is integrated and verified.

Why

The CheatEngineNet audit of 2026-09-22 (pinned on a56b6c1) concluded that the SDK has a substantial low-level base to keep and consolidate, and that the next steps are: qualify the loading profile before extending, align the consumed package, fix the Client divergences, and measure Lua coverage honestly (four distinct measures). This branch implements that work together with a professional CI/CD overhaul. The Client branch with the same name carries the Client side.

Plan

Wave Content
0 Branch, formatting pass (.git-blame-ignore-revs), repository-test scaffolding, security settings, context for sub-agents
1 Foundations: dead links and orphaned validators removed, public API tracking + ApiCompat baseline 1.0.0 (next version 2.0.0), lock files, qualification framework (support profile, Q01–Q48 matrix, local sandboxed CE runner), CI pipeline (format, zizmor, dependency review, exact-artifact package tests, coverage ratchet, workflow contract tests), governance (CodeQL, Scorecard, pr-policy, Dependabot), release chain (draft-first, SBOM, provenance, tuple manifest)
Checkpoint B C3/C4 qualification of the base on Cheat Engine 7.7.0.10621 x64 before any extension
2 Domains: generator omission/nil and results, resources and ownership, runtime observation (x64 profile fix), bounded AOB and scans, ABI width gate and 159-slot classic registry, Lua threading contract and host emulator, Lua surface catalogue with the four measures
3 Events (timers, hotkeys), object members and inheritance, registration leases in examples, legacy registration obsolete, benchmarks
4 Catalogue tags, live qualification receipts, CHANGELOG 2.0.0, migration guide, audit traceability
5 Verification, adversarial review, settings

Evidence discipline

Qualification levels C0–C4 are kept distinct: a managed or fixture test is never presented as a Cheat Engine host qualification. Nothing is published or tagged from this branch.

Summary

  • Expanded the Engine API for host and target observations, target-qualified resource ownership, memory-record activation, symbol lists, Auto Assembler outcomes, and instruction assembly. Added PublicAPI tracking and updated ABI callback fields to void*.
  • Added LuaOptional<T>, strict integer marshalling, and explicit Lua operation statuses. Updated binding generators and analyzers to support optional arguments and results, variadic results, and Outcome calls, with diagnostics for invalid shapes and look-alike contract types.
  • Added exact-host qualification tooling and Q-ID traits. Added tests for Lua stack behavior, generator output, runtime and ownership contracts, native ABI contracts, and packaged-consumer behavior.
  • Added locked dependencies, NuGet audit and API compatibility checks, SBOM metadata, and package provenance and isolation checks.
  • Overhauled CI and release workflows with pinned toolchains, locked restores, package-first testing, workflow contract tests, attestations, and NuGet publication verification.
  • No test results or CI status are provided. The objectives describe the work as in progress and not ready to merge.

Promote the migration style rules that dotnet format can apply safely (IDE0008, IDE0011, IDE0040, IDE0044, IDE0055, IDE0065, IDE0090, IDE0161) from suggestion to warning, as the .editorconfig comment planned for a dedicated reformat pass, and apply dotnet format to the whole solution. EnforceCodeStyleInBuild and TreatWarningsAsErrors now keep the tree formatted; IDE0005 and IDE1006 keep their previous severities. No behavior change.
Add CheatEngine.SDK.Repository.Tests, a fast project that only reads committed files, with its first contract: every project on disk is in the solution unless an explicit, reasoned exclusion says otherwise. Later remediation lots add their documentation, workflow, qualification and catalogue contracts there instead of reviving script-only gates.

Pre-register the package versions the parallel remediation lots need (PublicApiAnalyzers, Microsoft.Sbom.Targets, MTP HangDump and CrashDump, YamlDotNet) so they do not all edit Directory.Packages.props, make the Engine.Tests FakeHost partial so lots can extend it in separate files, and record the formatting commit in .git-blame-ignore-revs.
dotnet format left LF-only files untouched while the IDE0055 build check expects the platform newline, so a file written with LF endings failed the build without being fixable by the formatter. Declare end_of_line = crlf to match .gitattributes, keeping the two LF-pinned native bridge inputs on LF.
@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough
📝 Walkthrough

Merge Risk: 🟡 Moderate · up to 92bf0

This work-in-progress branch reworks resource ownership, allocation, Auto Assembler, and symbol cleanup reporting. Two outcome-reporting gaps remain. A custom allocator failure can be reported as "never ran" even though it may already have freed memory. After a detach, a patch release reports an engine failure instead of the documented lifecycle error. Earlier concerns also remain open: CI no longer checks native bridge reproducibility, and several documents and release notes are inaccurate. Resolve these, or explicitly accept them, before merging.

🚥 Pre-merge checks | ✅ 6 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Public Api Documentation And Changelog ⚠️ Warning The PR changes public API and behavior, but the Unreleased changelog does not cover the changes. The diff adds the public LuaOptional and LuaOptional<T> API, adds `TargetMemoryAllocator.TryAllocat… Add complete [Unreleased] changelog entries for every added, removed, signature-changed, and behavior-changed public or protected API in libs/, src/, analyzers/, and source-generators/. Include the new Lua optional/result, runtime…
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is an imperative 49-character sentence with no trailing period. It accurately summarizes the audit remediation and CI/CD overhaul covered by the changeset.
Native Abi Evidence ✅ Passed The check applies because the PR changes libs/CheatEngine.SDK.Abi/ and native/. The changed ABI README identifies cepluginsdk.h and cepluginsdk.pas, pins them to Cheat Engine commit `ec45d5f..…
Lua Stack Balance Tests ✅ Passed The pull request changes Lua stack behavior in LuaCallSupport and generated Lua bindings, so the check applies. LuaGlobalCallEmitter emits LuaState.TryCall(...) for all generated call forms and …
Dependency Direction ✅ Passed No forbidden dependency or Client-level policy was introduced. The diff contains no CheatEngine.Client or CheatEngine.Mcp ProjectReference, PackageReference, using, or namespace declaration. The added…
Workflow Hygiene ✅ Passed Workflow hygiene passes. The pull request changes .github files, so the check applies. All 62 external action references use 40-character commit SHAs with version comments; no pull_request_target trig…
Full details: Public Api Documentation And Changelog

Explanation

The PR changes public API and behavior, but the Unreleased changelog does not cover the changes. The diff adds the public LuaOptional and LuaOptional&lt;T&gt; API, adds TargetMemoryAllocator.TryAllocate, removes ITargetMemoryAllocationOutcomeOperations and AllocateWithOutcome, and changes public Int32Marshaller, Int64Marshaller, and AddressMarshaller read behavior. The Unreleased section has no matching entries for these APIs or behaviors, and the behavior/removal changes are not marked Breaking. Sibling READMEs and XML documentation are present for representative additions. The changed diagnostics do have documentation pages and release-tracking rows, but that does not satisfy the missing changelog requirement.

Resolution

Add complete [Unreleased] changelog entries for every added, removed, signature-changed, and behavior-changed public or protected API in libs/, src/, analyzers/, and source-generators/. Include the new Lua optional/result, runtime, allocation, Auto Assembler, symbol-list, ABI, and generator contracts. Document the removed allocation outcome interface and AllocateWithOutcome, and the changed marshalling behavior. Mark every removal and behavior change as Breaking. Re-audit all changed public members for XML documentation and update each owning README when its contract is documented there. Keep the diagnostic documentation and analyzer release-tracking entries synchronized with all diagnostic changes.


Comment @coderabbitai help to get the list of available commands.

AotProbe and LiveProbe are restored and built on their own, so the solution-wide formatting pass skipped them and the CI aot job now failed on IDE0055 and IDE0011 once those rules became build errors. Apply dotnet format whitespace and style to both projects. LiveProbe still does not compile for an unrelated, pre-existing reason; the qualification work brings it into the solution and fixes it.
Validate-CeSurfaceCatalog.py and Validate-EngineeringManifest.py read
inputs under documentations/ (the surface catalogue JSON, backlog.json,
ARCHIVE_RECONCILIATION.md and the work-item pages). Commit 4020a32
deleted that tree and the CI job that ran them, so the scripts and their
unit tests in eng/tests/ can no longer run and nothing references them.

Repository rules are enforced by C# tests instead of scripts (audit F14,
.coderabbit.yaml "no script-only gates"): the documentation integrity
tests replace the link checks, the Lua surface catalogue returns with its
own C# tests, and the protected-operation catalogue check moves to C#
with the ABI work. The engineering backlog is retired.

With no Python file left in the tree, the __pycache__/ and *.py[cod]
ignore patterns go too.
Commit 4020a32 removed the documentations/ tree, and the maintainer
decided not to restore it (audit F14, ADR-12, SRCREG-06). The pages that
still need a destination get one under docs/, rebuilt from the audit
rather than restored, and every page carries the header "Recreated
2026-09 from the audit, not the historical documentations/ tree".

docs/README.md is the index: it names the audit only by the SHA-256 of
its MANIFESTE.md, lists the rebuilt areas, summarises the C0-C4 evidence
levels, and records the retired paths (as code spans, never links) with
their replacement, so statements that relied on them stay "declared, not
recovered" (DeclaredRepo) instead of silently disappearing. It also maps
the unresolvable architecture-review scenario ids (R25/T049-T050,
R26/T051-T052, R34/T067-T068/T076) to the closest audit scenarios
(Q09/Q10, Q19, Q30).

Qualification, ABI/NativeAOT and catalogue content arrives with later
work, so those pages are placeholders that name their owning work and
wave. They exist now so that re-pointed links resolve immediately. The
NativeAOT placeholder already states the restriction Microsoft documents
for Native AOT libraries: unloading them with FreeLibrary is not
supported (https://learn.microsoft.com/dotnet/core/deploying/native-aot/libraries).

The solution lists the new pages under /docs/, /docs/abi/ and
/docs/catalog/; the qualification pages are listed by the qualification
work together with its own files.
Commit 4020a32 removed documentations/**, and 33 relative links (plus one
code-span path to native/cheatengine-sdk-lua-bridge/AUDIT.md) were left
pointing at it (audit F14, ADR-12, A09-15, A09-16, A21-22). They are
re-pointed, not restored:

- ROADMAP: the 24 SDK-0xx work-item links become plain identifiers
  (titles kept, tables re-padded), and a pointer to the retired-
  documentation table replaces the sentence that sent readers to live
  issues: no issue or Project tracks these work items.
- The capability-matrix links split into the concerns the audit
  separates: per-surface provenance and semantics go to the Lua surface
  catalogue (docs/catalog), executed host evidence to the qualification
  matrix (docs/qualification, value scans = Q25/Q26).
- The source index link goes to the support profile, which records the
  celua.txt digest; the live-probe templates to the local qualification
  protocol; ADR-006 to the NativeAOT profile restrictions (docs/abi);
  SDK-005 to the Q09/Q10 coexistence scenarios; AUDIT.md to the bridge
  README.
- The advanced-domain and structure-family pointers go to the deferred
  families of the catalogue, without claiming it records them already.

Targets that later work fills in are placeholder pages, so every link
resolves now. Where a wrap had split "and" / "the" onto its own line in
the address-list example, the paragraph is re-flowed.
Several READMEs stated things that no committed file, test or workflow
supports (audit A00-03, AR-01, ADR-12e, AX07-15, SRC03-06). Reword them
to what is true today, without linking to anything that does not exist:

- exemples/api: CESDK9101 does not only stop x86. The packaged target
  (unchanged since 1.0.0) accepts an unset PlatformTarget, AnyCPU or
  x64 and rejects every other explicit value, as PlatformTargetTests
  proves for x86, ARM, ARM64, Itanium and an unknown value.
- LiveProbe: "the only automated validation of this project is
  compilation" is false; no workflow builds it (it is outside the
  solution). The qualification work states its compile check once the
  project joins the solution.
- Hosting and Coexistence: the architecture-review scenario ids
  R25/T049-T050, R26/T051-T052 and R34/T067-T068/T076 only resolved in a
  retired archive page. They become the audit scenarios Q09/Q10, Q19 and
  Q30, still "not executed"; docs/README.md records the mapping.
- Abi: the source index that recorded the installed-file hashes was
  retired. Those hashes stay declarations (DeclaredRepo) until the
  support profile re-measures them, instead of reading as verified.

A sweep of every tracked Markdown file for "100 %", "complete/full
coverage", "fully supported", "all public Lua", "every CE version or
profile", "7.5.1" and "exact CE 7.7 source" finds nothing to neutralize.
The dead documentations/ links rotted because the checks that caught
them were Python scripts that CI stopped running. Repository rules live
in C# tests instead (.coderabbit.yaml, "no script-only gates"), and this
one is blocking and offline: it runs in both build-test legs through
dotnet test --solution, reads files only, never starts a process and
never reaches the network (audit F14, ADR-12, A21-32, A23-F14-4).

DocumentationIntegrityTests applies one rule per test to every Markdown
file of the working tree:

- relative links, images, reference definitions and HTML href/src
  resolve with exact case (Windows, where CI runs, is case-insensitive,
  so every segment is matched against the directory entries), never
  above the root or into build output;
- #fragments match a heading (GitHub slug rules, duplicate suffixes) or
  an explicit <a id|name> anchor of the target page;
- no absolute local path (drive, file: URI, user profile folder);
- no link into the retired documentations/ tree, and only the docs
  index names retired pages;
- blob/main and tree/main links to this repository resolve, and so do
  the frozen links of the README published in 1.0.0 (ADR-12);
- packed READMEs use absolute https:// links only, since nuget.org
  cannot resolve relative ones
  (https://learn.microsoft.com/nuget/nuget-org/package-readme-on-nuget-org#allowed-domains-for-images-and-badges);
- docs/ pages carry the "Recreated 2026-09" header, placeholders name
  their lot and wave, and the docs index links every top-level entry;
- no "100 %", "complete/full coverage", "fully supported" or "fully
  compatible" claim unless the line negates it (A00-03, A20-14).

MarkdownDocument is a dependency-free CommonMark subset parser (adding
Markdig would touch central package versions and lock files): fenced
code, code spans, HTML comments and backslash escapes are opaque, link
text may span lines (a line-by-line parser missed the LiveProbe link),
and every link maps back to the line of its destination. Repository-
specific values live in DocumentationConventions so the Client can
mirror the design. MarkdownDocumentTests exercises the parser and every
rule on in-memory pages, which proves each gate fails on its regression.
The repository is about to commit NuGet lock files. The SDK's implicit
packages (Microsoft.NET.ILLink.Tasks, the ILCompiler packages) are
recorded in those locks and move with the SDK version, so a roll-forward
to another feature band would break every locked restore. global.json
therefore requires 10.0.401 exactly (rollForward: disable) and names the
install command in sdk.errorMessage, available since the .NET 10 SDK:
https://learn.microsoft.com/dotnet/core/tools/global-json#rollforward
https://learn.microsoft.com/dotnet/core/tools/global-json#errormessage

AnalysisLevel moves from latest-recommended to the explicit pin
10.0-recommended (private property _CheatEngineSdkPinnedAnalysisLevel).
On SDK 10.0.401 'latest' resolves to 10.0, so the selected NetAnalyzers
global configs are identical before and after (checked with
-getItem:EditorConfigFiles on a library, a Roslyn component and a test
project) and the Debug build stays at zero warnings. A newer SDK band can
no longer add CA/IDE errors without a reviewed commit.

CESDK9004 (Directory.Build.targets, BeforeBuild) fails any project whose
evaluated AnalysisLevel differs from the pin, which covers project-level
and command-line overrides.

ToolchainPinTests (Repository.Tests/Toolchain) keeps the committed values
honest: exact SDK version with rollForward disable and no prerelease, an
errorMessage naming the pinned version and install command, a
release-shaped analysis level that moves with the SDK major/minor, and no
other MSBuild file setting AnalysisLevel.
The previous policy relied on TreatWarningsAsErrors and did not say what
happens to low and moderate advisories. It now follows the documented
"dedicated audit pipeline" pattern:
https://learn.microsoft.com/nuget/concepts/auditing-packages#running-nuget-audit-in-ci

- Ordinary builds: NuGetAudit true, mode all, level low. NU1903 (high)
  and NU1904 (critical) are appended to WarningsAsErrors, so they fail
  restore even in a project that turns TreatWarningsAsErrors off.
  NU1901/NU1902 (low/moderate) and NU1900/NU1905 (audit source trouble)
  stay visible warnings, so an advisory published overnight does not turn
  every required check red without a commit.
- restore -p:AuditPipeline=true (the scheduled strict audit): every
  NU1900-NU1905 code is an error.

CESDK9009 guards the policy in three places:
- BeforeBuild in every project: NuGetAudit/NuGetAuditMode/NuGetAuditLevel
  weakened, NU1903/NU1904 (every audit code in AuditPipeline mode) listed
  in NoWarn or WarningsNotAsErrors, or missing from WarningsAsErrors.
  Lists are compared as items split on ';', ',' and whitespace, so
  NU19031 never matches and case does not matter.
- Before CollectNuGetAuditSuppressions (inside restore): a
  NuGetAuditSuppress item must be declared in Directory.Build.props with
  Justification and Expires (yyyy-MM-dd) metadata; the AuditPipeline run
  fails once Expires is past, ordinary builds never fail on the calendar.
- Before GenerateNuspec of a packable project: a stable (non-prerelease)
  version cannot be packed while any suppression exists, so the release
  path never suppresses.

Directory.Solution.targets asserts, for CI solution restores, that
RestoreProjectsAuditedCount + RestoreSkippedCount equals
RestoreProjectCount (documented "ensure restore audited projects"
check). It is imported for CheatEngine.SDK.slnx: a forced restore with
CI=true logs 35 audited of 35, a no-op restore 35 up to date, and
-p:NuGetAudit=false fails with CESDK9009.

ToolchainPinTests gains text checks for the policy, the solution-level
assertion and the suppression rules.
Every public API change must now be explicit before the 2.0 domain work
changes the surface. Microsoft.CodeAnalysis.PublicApiAnalyzers (5.6.0,
already pinned in Directory.Packages.props) is referenced by the six
libs/ projects through eng/Shipping.props; src/CheatEngine.SDK declares
no type and is not tracked. RS0016/RS0017 keep their default warning
severity, which TreatWarningsAsErrors turns into build errors.

PublicAPI.Shipped.txt is the surface of the published CheatEngine.SDK
1.0.0 package, not of the tagged sources alone. It was generated in a
temporary detached worktree at v1.0.0 (a6fefb9) with the same SDK
10.0.401 and Roslyn 5.9.0: dotnet format analyzers --diagnostics RS0016
wrote the hand-written surface, and the five EngineApi-generated
declarations of CheatEngine.SDK.Engine.Generated.MemoryScalars were
taken from the RS0016 messages, because the fixer cannot edit generated
documents although the analyzer does track them. That worktree then
built RS0016/RS0017-clean in Release, and its seven assemblies were
dumped with System.Reflection.Metadata and diffed against the dump of
lib/net10.0 of the published nupkg (lock contentHash n7nHqZ8v...gA==):
155 types and 1375 members on both sides, 0 removed, 0 added, 0 changed,
and every 1.0.0 type appears in the Shipped files.

PublicAPI.Unshipped.txt holds the delta to HEAD: 668 additions from the
same fixer, and the nine *REMOVED* lines RS0017 reported, which are
exactly the known breaks: the AddressResolutionOptions constructor,
UseHostSymbolTable accessors and Deconstruct; the typed function-pointer
Callback fields of AddressListPluginInit and
DisassemblerContextPluginInit (now void*); and the renumbered
MemoryAccessFailure members DestinationTooSmall, WriteFailed and
InvalidResult. No RS0026/RS0027 fired. Files are ordinally sorted after
the #nullable enable header so they merge by union and sort.

CESDK9003 (Directory.Build.targets) fails a libs/ project without both
files, because a missing file silently disables tracking.

PublicApiFileTests (Repository.Tests/PublicApi) check that every library
has both files and nothing else does, the header, ordinal order without
duplicates or blank lines, no *REMOVED* marker in Shipped, every removed
line repeating a Shipped line exactly, and no Unshipped line redeclaring
a live Shipped one.
main already breaks the published 1.0.0 API, and the maintainer decided
that the next release is 2.0.0 with every break declared.

Version line: MinVerMinimumMajorMinor goes from 1.0 to 2.0, so untagged
commits pack as 2.0.0-alpha.0.N. MinVer derives AssemblyVersion from the
major (2.0.0.0), and the GeneratedCode attribute in consumers' generated
files changes accordingly.

Baseline: src/CheatEngine.SDK sets PackageValidationBaselineVersion
1.0.0 next to EnablePackageValidation, so every pack compares
lib/net10.0 with the published package (downloaded at restore as a
PackageDownload):
https://learn.microsoft.com/dotnet/fundamentals/apicompat/package-validation/baseline-version-validator
CompatibilitySuppressions.xml was generated once with
-p:ApiCompatGenerateSuppressionFile=true and reviewed: nine baseline
suppressions, all Left == Right. CP0002 for the AddressResolutionOptions
constructor, UseHostSymbolTable accessors and Deconstruct; CP0011 for
MemoryAccessFailure.DestinationTooSmall, WriteFailed and InvalidResult;
and, contrary to the expectation that ApiCompat has no rule for a field
type change, CP0002 on the F: DocIds of AddressListPluginInit.Callback
and DisassemblerContextPluginInit.Callback (typed function pointer to
void*). No CP0003 (assembly version 1.0.0.0 to 2.0.0.0) is reported.

Guards (Directory.Build.targets, before GenerateNuspec, packable only):
- CESDK9006: package validation off, no baseline or strict mode; in a CI
  build (ContinuousIntegrationBuild), any of ApiCompatGenerateSuppression-
  File, GenerateCompatibilitySuppressionFile, ApiCompatPermitUnnecessary-
  Suppressions, DisablePackageBaselineValidation, RunApiCompat=false or a
  custom PackageValidationBaselinePath. Any CPxxxx or PKVxxx code in NoWarn
  (ApiCompat honors NoWarn) is refused as well.
- CESDK9007 (after MinVer): a suppression file with baseline
  suppressions requires a package major above the
  baseline's, so the old -p:MinVerVersionOverride=1.0.0 rehearsal fails.
Verified: a CI-like pack produces exactly one
CheatEngine.SDK.2.0.0-alpha.0.51.nupkg with ApiCompat green; removing a
CP0011 or CP0002 entry fails the pack; a stale entry fails it
("Unnecessary suppressions found"); MinVerVersionOverride=1.1.0 fails
with CESDK9007; CI + ApiCompatGenerateSuppressionFile and strict mode fail
with CESDK9006; a 2.0.0 override packs.

eng/api/ records the rest of the contract:
- apicompat-invisible-changes.txt: *REMOVED* lines ApiCompat cannot see
  (none today), plus the attribute-only changes invisible to both tools.
- client-consumed-sdk-types.txt: the Client's SDK-type allowlist and the
  enums it translates, copied from Client 881c14c with provenance.
- client-induced-breaks.txt: the suppressions touching those types (the
  AddressResolutionOptions and MemoryAccessFailure entries), for the
  Client's docs/migration/sdk-2.0.md.
- README.md: public API tracking, the baseline and its regeneration
  command (integrator only), the Client flag, the enum policy, the 1.0.0
  source-versus-package identities, the toolchain pin and the NuGet audit
  policy, and the guard table.

Tests (Repository.Tests/PublicApi):
- CompatibilitySuppressionTests (trait Qualification=Q48, SDK-side C0
  evidence only): suppressions are baseline, same-assembly CP0001/CP0002/
  CP0011 entries; every suppression names a *REMOVED* line and every
  *REMOVED* line is suppressed or declared invisible (matched by type and
  member name); the induced-break list is exactly derived; every
  consumed type resolves or is marked unresolved.
- EnumContractTests: the nine CE-constant enums, CheatEngineArchitecture
  and TargetAbi keep their 1.0.0 members; the 23 enums added since 1.0.0
  are classified; status/outcome enums start with a neutral zero member
  (Unknown; Unspecified, Uninitialized, NotAttempted tolerated) except
  eight pending ones owned by S-SCAN, S-RT, S-RES and S-GEN-A, a list the
  ratchet test only lets shrink.
The release must be able to attest what the package contains, and a
consumer must be able to trace the package back to its commit.

SBOM: src/CheatEngine.SDK references Microsoft.Sbom.Targets 4.1.13
(already pinned; a development dependency, PrivateAssets="all", so the
nuspec keeps zero dependencies) and sets GenerateSBOM unconditionally,
so local, fixture and CI packs carry the same inventory at
_manifest/spdx_2.2/manifest.spdx.json (SPDX 2.2, the tool default) with
its .sha256 sidecar. Supplier CheatEngineNet, namespace under the
repository URL, license fetching off, tool verbosity Warning. The
build-component scan stays on the project folder, which holds no package
manifest: the SBOM describes the package and each of its 27 other
entries with SHA-256/SHA-1, and lists no build-only tool as a shipped
component. The tool's "no packages detected" line is expected (no NuGet
dependency) and is console output, not an MSBuild warning.

Microsoft.Sbom.Targets reads $(Version) at evaluation for the SBOM
package version, before MinVer sets it, which produced "1.0.0";
CheatEngineSdkAlignSbomPackageVersion re-points it to $(PackageVersion)
after MinVer. CESDK9008 fails a pack without GenerateSBOM or without the
package reference. The nupkg is not byte-reproducible (random document
namespace part, creation time, re-zip); eng/api/README.md states that
reproducibility is promised for the hashed assemblies and the bridge.

Repository metadata: PublishRepositoryUrl=true, recommended with the
SDK-integrated Source Link
(https://learn.microsoft.com/dotnet/standard/library-guidance/sourcelink),
keeps <repository type url commit> in the nuspec as in 1.0.0.

PackagedUmbrellaFixture gains two read-only accessors, PackagePath and
Nuspec, assigned in ReadPackedNupkg (additive, for S-CI-REL's rework).
SupplyChainPackageTests (collection PackagedUmbrellaSuite, trait
Category=Packaging) checks the SBOM identity and sidecar, the SHA-256 of
every shipped assembly and of the bridge, SBOM inventory == package
entries, no repository contract file packed, the MinVer line, the
<major>.0.0.0 assembly versions, the nuspec repository commit and Source
Link to raw.githubusercontent.com at that commit. The whole
tests/CheatEngine.SDK.Tests project passes locally (74 tests, 1 min 24
s). Pack cost added by this lot: SBOM about 4.7 s, package validation
about 0.4 s; a fixture-equivalent pack takes 11 s, far below the
3-minute PackTimeout, which stays unchanged.
Version and content must be locked for every project (audit ch.21 exit
criteria), including the projects CI builds outside the solution, and
the lock files must only ever be written by one reproducible procedure.

Directory.Build.props sets RestorePackagesWithLockFile=true for every
project. RestoreLockedMode is deliberately not set in MSBuild: locked
mode is passed by the CI entry points, and a global locked mode would
make the --force-evaluate restores of the lock check fail with NU1005:
https://learn.microsoft.com/nuget/consume-packages/package-references-in-project-files#locking-dependencies
https://learn.microsoft.com/nuget/reference/errors-and-warnings/nu1005

CESDK9005 (BeforeBuild, every project) fails a project that builds with
RestorePackagesWithLockFile or ManagePackageVersionsCentrally other than
true. With a lock file present, restore reports NU1005 first; with
--no-restore the guard itself fires (both verified).

eng/Update-LockFiles.ps1 [-Verify] (pwsh 7, strict mode, every dotnet and
git exit code checked, comment-based help; PSScriptAnalyzer 1.25.0 reports
no finding at any severity):
- refuses a non-Windows host (AOT win-x64 sections record host-specific
  ILCompiler packages) and any SDK other than global.json's, printing
  the install command;
- enumerates projects from git (tracked plus untracked non-ignored
  *.csproj) and the slnx <Project> entries, with an empty but explicit
  exclusion list;
- restores CheatEngine.SDK.slnx, then each out-of-solution project, with
  --force-evaluate; gives an unchanged lock its committed bytes back and
  keeps the committed final-newline state of a changed one; verifies
  with --locked-mode;
- checks the structure from evaluated properties (dotnet msbuild
  -getProperty): a lock per project, version 2 under CPM, no
  CentralTransitive in a version 1 lock, a <tfm>/<rid> section per
  runtime identifier with runtime.<rid>.Microsoft.DotNet.ILCompiler when
  PublishAot is true, and no CheatEngine.* package from a feed;
- default mode lists the changed locks; -Verify fails, naming the files
  and the command, on a diff or an untracked lock.

A first run generated 37 lock files in 56 s with every check green;
they are committed separately. eng/api/README.md documents the
procedure, the Dependabot workflow and the guard.
First introduction of the lock files, generated by
./eng/Update-LockFiles.ps1 on Windows with .NET SDK 10.0.401 and nothing
else: 37 packages.lock.json, one per tracked project, including the two
projects outside the solution (tests/CheatEngine.SDK.AotProbe and
tests/CheatEngine.SDK.LiveProbe). All are format version 2 (Central
Package Management everywhere); CentralTransitive entries appear only
under tests/, where eng/Tests.props enables transitive pinning. The
three runtime-specific projects (AotProbe, NativeAotLibraryProbe,
NativeAotLoaderHarness) carry a net10.0/win-x64 section, and the two
Native AOT ones lock runtime.win-x64.Microsoft.DotNet.ILCompiler, so
'dotnet publish --no-restore' keeps working. No lock resolves a
CheatEngine.* package: the ApiCompat 1.0.0 baseline is a
PackageDownload. The files keep NuGet's format without a final newline.

From now on the integrator is the only writer of these files and
regenerates them with the script (never by hand, never on a merge).

LockFileTests (Repository.Tests/LockFiles) mirror the script's
structural checks offline: a lock per project, supported version and
version 2 under CPM, no CentralTransitive in a version 1 lock, the
RID section and ILCompiler package of Native AOT projects, no
CheatEngine.* package, and NuGet's no-final-newline ending.
Right after the --force-evaluate regeneration every project is up to
date, so a plain `dotnet restore --locked-mode` takes NuGet's no-op path
and never compares the lock files with the project inputs. Observed:
with a hand-edited lock (resolved or requested version changed), a
no-op locked restore still exits 0, while `--locked-mode --force` fails
with NU1403 (content hash) or NU1004 (inconsistent lock). The script now
passes --force to its verification restores; --force is compatible with
locked mode (only --force-evaluate is not, NU1005).

-Verify still passes on the committed tree (32 s, no diff), and a
committed lock edit makes -Verify fail naming the file.
PSScriptAnalyzer 1.25.0: no finding.
SbomGenerationVerbosity=Warning was meant to keep the SBOM tool's
warnings while dropping its information output. Measured on a pack, the
tool prints the same component-detection and telemetry lines at Warning
and at Error, so the setting changed nothing and its comment was wrong.
The property is removed and eng/api/README.md now records the observed
facts: the scan enumerates the six files of src/CheatEngine.SDK,
including packages.lock.json, which is not a component source, and
reports no package.
LiveProbe did not compile: LiveProbeState.cs imported the test namespace
CheatEngine.SDK.LiveProbe.Tests only so that its compile-linked copy in
LiveProbe.Tests could find the ProbeHostGlobals stub (CS0234). Move the
stub into the plugin's own namespace, LiveProbe, and drop the import, so
both projects resolve the same type name. With that error gone, the
remaining IDE0008 (var) findings surfaced and are fixed by dotnet format.

Add the project to CheatEngine.SDK.slnx with its x64 platform mapping,
exactly the entry `dotnet sln add -s tests` produces, applied as a single
hunk instead of the serializer's whole-file rewrite so parallel solution
edits stay mergeable. Remove its now stale exclusion from
SolutionInventoryTests. CI compiles the harness from now on; nothing in
CI loads or runs it.
The exact-host qualification scenarios need facts that only the loaded
plugin can report, and some of them need a controlled failure. Add them to
the harness, all opt-in and inert without the existing fail-closed gate:

- ce77_live_probe_status() now also reports the plugin id, epoch, reported
  exports size (Q03) and PluginHost.LastInitRecordArgument next to the raw
  second bootstrap integer, still without interpretation (Q04);
  ce77_live_probe_status_json() returns the same facts, the assembly
  locations, MVIDs and Hosting load context (Q40) and the fault decisions
  as one ce77-live-probe-status-v1 object for the runner's driver.
- ce77_live_probe_throw_managed_exception() throws inside the generated
  thunk so a pcall can record the catchable Lua error (Q14 at C3).
- liveprobe.fault.json next to the plugin selects FactoryCreate, OnEnable
  or OnDisable as the stage that throws (Q06, Q08). It is read once per
  enable, without Lua, only when authorized; unknown content is ignored
  and reported.
- -p:LiveProbeNonAsciiName=true builds a variant whose name mixes Latin-1
  and non-cp1252 characters, to observe how CE 7.7 decodes the ANSI name
  (Q05.a, AnsiNameBuffer).
- ce77_live_probe_pump_messages(seconds) pumps host messages from admitted
  main-thread work so the operator can untick the plugin during a
  callback; it reports the phases it saw (Q07, an observation only).

The new logic lives in Lua-free files compile-linked into LiveProbe.Tests,
with tests for the gate, the stage selection and the raw status values.
Both READMEs move to the SDK section layout, document the hooks and point
to the local qualification protocol.
Qualification results need machine-checkable shapes before anything can
cite them. Add four JSON Schema draft 2020-12 documents under
docs/qualification/schemas, every object closed:

- support-profile: the documentary public-source profile and the
  qualifiable CE 7.7.0.10621 x64 managed-hostfxr profile, the Checkpoint A
  decisions, the unsupported routes and dated measurements.
- qualification-matrix: one row per Q01-Q48 scenario and sub-row, with the
  scenario block (preconditions, operation, expected, expected category)
  and one cell per level. C0-C2 cells take executed, traited CI tests;
  C3/C4 cells take committed receipts only, name the profile and, once
  executed, the tree and package they came from.
- qualification-receipt: one redacted C3/C4 run with operator, load route,
  tree, exact CI package identities, host, bridge, per-file bundle hashes,
  target, HKCU diff (names only), timings and its event log.
- qualification-events: the structured transcript committed next to a
  receipt.

They follow the frozen contract in shared-contracts sections 2.0-2.3 with
the ratified additions A-SQUAL-1 to A-SQUAL-4, including the rule that a
hash naming a committed JSON document is computed after CRLF-to-LF
normalization. Other lots and the Client copy these files, so they land
before the validator that enforces them.
Repository rules are C# tests, never script-only gates, and Repository.Tests
takes no package. Add a validator for exactly the JSON Schema 2020-12
keywords the v0 schemas use (type, const, enum, pattern, required,
properties, additionalProperties, items, minItems, minimum, allOf, anyOf,
if/then/else, local $ref, boolean schemas), with ECMA-262 patterns, plus
the semantic rules a schema cannot express: matrix structure and sub-row
aggregation, profile citations, evidence kinds, receipt resolution and
freshness, Automated evidence resolved to traited methods of solution
test modules, receipt identity and preflight, event-log redaction, and
the LF-normalized hash rule.

QualificationSchemaTests pins the schemas: draft 2020-12, repository $id,
closed objects, only implemented keywords, and every required and enum
list equal to the validator constants, so schema and validator cannot
drift. A self-test proves the validator reports each violation kind.
A qualification result must name the exact host it ran on, and the public
Cheat Engine source must never stand in for the 7.7 binary (F01, ADR-02).
Add docs/qualification/support-profile.json and its page with two
profiles:

- ce-public-src-ec45d5f, documentary and never qualifiable: the public
  source that declares 7.5.1 and the historical CLR bootstrap.
- ce-7.7.0.10621-x64-managed-hostfxr, the only qualifiable profile: exact
  executable hash and version, excluded variants, Lua module, celua.txt,
  the locally modified ce.runtimeconfig.json, observed runtimes, the
  profile-relevant HKCU names (no data), LocalProcess as the only
  qualified backend, the authorized targets and plugin contract version 6.

The page also separates the identities (public source, binary, SDK tree,
v1.0.0 tag, consumed 1.0.0 package), the SDK-branch and Client 1.0.0
tuples with their three package identities, the runtime policy warning,
the proposed Checkpoint A decisions CPA-1 to CPA-3, the unsupported
routes and the dated measurement record. Hashes were measured read-only
on 2026-09-23; no test reads the installation. SupportProfileTests pin
the documents to the schema, to the committed Lua fixture, the LiveProbe
authorization constants, AbiConstants.SdkVersion and the checked-in
bridge, reject a qualifiable documentary profile, and enforce canonical
JSON, no local path and no global score.
A receipt is only evidence when it names everything the result depends
on (A20-03, A20-09, A04-22, ADR-12). Add QualificationReceiptTests with a
complete sample receipt and event log as raw string literals, and
refusal proofs for each rule the audit and the plan state:

- citing the documentary public-source profile (A00-05, A17-03);
- any drive, user-profile or file:// path, so receipts stay
  distributable (F14);
- a Passed receipt without passKind, and a pass kind on a non-passed
  receipt (A20-11);
- a missing tree hash, package hash, content hash or pull request field
  (A20-05, PR-SEQ-19);
- a local pack instead of the CI artifact or nuget.org, and a CI
  artifact without its run;
- an id that does not encode start time, scenario and package, and a
  fixture-level receipt;
- a run on another host unless recorded as a justified NotApplicable.

Committed receipts, none today, must validate where they are committed,
match their event log's LF-normalized hash, carry no local path or raw
debug output, and be cited by the matrix cell they qualify.
The audit executed nothing, and the existing tests are resources, not
results, until each one is mapped to the scenario it evidences (A20-01,
QUAL-MAT-2). Add only [Trait("Qualification", "Qxx")] attribute lines to
the test methods whose assertions match the scenario's success criterion
in analyses/20: Q01, Q02, Q03, Q04, Q05, Q05.a, Q06, Q07, Q08, Q11, Q12,
Q14, Q15, Q16, Q17 and Q39.

Every tagged test was read against its criterion first. Three candidates
stay untagged because they do not prove their scenario:
DisablePluginTests.OnDisable_throwing_is_logged_but_reports_TRUE_after_the_plugin_is_disabled
(TRUE after a failed plugin cleanup is the opposite of Q08's "no false
cleanly disabled" evidence), and ReentrancyTests
Enable_nested_in_OnEnable_is_refused_and_does_not_replace_the_context and
Enable_nested_in_OnDisable_is_refused_and_the_disable_stands (nested
enables, not the re-entrant disable of Q07). InitializeManagedTests
Second_call_is_idempotent_and_writes_the_same_bytes_including_the_name_pointer
joins Q05 because it proves the stable-name half of that scenario.

The four modules pass with --fail-skips on, and --filter-trait
"Qualification=Q07" selects exactly the five Q07 methods.
The audit's 48 scenarios are exit criteria, not results, and nothing in
the repository said which of them any evidence covers. Add
docs/qualification/matrix.json: one row per Q01-Q48 plus 14 sub-rows
(Q05.a, Q08.a, Q09.a-b, Q30.a-e, Q31.a, Q32.a-d), each with its audit
wording, owner, required levels copied from analyses/20, findings, a
scenario block (preconditions, operation, expected result, expected
category) and one cell per level.

Seeding is conservative. C0-C2 cells are Passed only where the traited
tests of the previous commit ran green on this head (Q01-Q08, Q05.a,
Q11, Q12, Q14-Q17, Q39), with RefusalVerified where the expected outcome
is a refusal. Every C3/C4 cell is NotExecuted on the qualifiable profile,
except the profile decisions: Q38 C3, Q39 C3, Q42 C3/C4 and the
file-as-process, CEServer, x86-host and ARM sub-rows are NotApplicable
with a justification. Client-owned rows (Q33, Q43-Q45) are NotApplicable
here and point to the Client matrix. Parents equal their sub-rows'
aggregate as the contract defines it.

QualificationMatrixTests enforce the audit oracle, schema, structure,
profiles, pass kinds, evidence kinds, aggregation, receipt resolution and
freshness, Automated evidence resolved to traited methods of solution
test modules, and trait/matrix parity in both directions. The README
explains levels, statuses, the hash and freshness rules and how receipts
are read; its matrix summary and the support profile's Not executed list
are generated from the matrix and checked by tests.
The host spike scanned Cheat Engine's tutorial programs, whose global
match counts moved with their loaded modules, so a scan result could not
be compared with anything. Add tests/CheatEngine.SDK.QualificationTarget,
a console program the runner attaches Cheat Engine to (Q25-Q32 inputs):

- a 16-byte marker held as constant data of the image, eight pinned heap
  copies 64 bytes apart, and a region with 20000 non-overlapping
  repetitions of a second pattern computed at run time;
- Int32 and Int64 cells that the stdin command step advances by one;
- one JSON ready record (cheatengine-qualification-target/v0) with PID,
  architecture, pointer size, image base, every region with its pattern
  and a count the target measures itself, and the value addresses.

It references no SDK project, writes no file except an optional ready
file, and exits on exit or end of input. It is published with Native AOT
for win-x64 and win-x86 because the lab machine has no x86 .NET 10
runtime; cross-publishing win-x86 from x64 works with the installed MSVC
toolset (verified; https://learn.microsoft.com/dotnet/core/deploying/native-aot/#platform-architecture-restrictions).
Both images hold the marker twice, which the measured count reports.

The project joins the solution (the entry `dotnet sln add -s tests`
writes, as one hunk), so CI compiles it. Solution restores now download
the win-x86 runtime, NativeAOT runtime, apphost and ILCompiler packs
(77.6 MB); DisableTransitiveFrameworkReferenceDownloads avoids a further
49 MB of ASP.NET Core and Windows Desktop packs. QualificationProjectShape
tests pin the shape of the target and of LiveProbe: in the solution,
never a test module, never packed, no SDK reference, x64 plugin.
C3/C4 evidence needs a repeatable way to run the exact Cheat Engine host
without touching the installation or the operator's settings, and it
must never run in CI. Add eng/qualification:

- Invoke-LocalQualification.ps1: the CI guard is the first statement
  (exit 3); a read-only preflight compares the installed executable, Lua
  module, runtimeconfig and celua.txt with support-profile.json (exit 4)
  and refuses an elevated runner, another Cheat Engine instance, build
  load or a dirty tree unless explicitly allowed (exit 5); runs hold the
  Global\ce-lab mutex, mirror the installation into a sandbox verified
  file by file, build every harness from the exact package in a
  throw-away consumer with an isolated NUGET_PACKAGES and check its
  closure (plugin entry point, SDK assemblies, deps.json without project
  entries, runtimeconfig, packaged bridge), record the bridge
  fingerprint, publish the qualification target, export HKCU, drive
  Cheat Engine through a generated autorun driver with a watchdog and
  operator handshakes, clean up, restore HKCU only on a non-empty
  difference with no other instance running (exit 7 otherwise), and
  write one redacted receipt and event log per scenario, or a smoke
  report that is never a receipt.
- QualificationRunner.psm1: the pure helpers (LF hash rule, registry
  export parsing and name-only diff, redaction, event-log bounding,
  receipt id and assembly, Lua literals, bundle closure, pass-rule
  evaluation) so tests can exercise them without Cheat Engine.
- driver/zz_cesdk_qualification.template.lua: a recorder that runs one
  step per timer tick under pcall, persists its progress across a Lua
  state reset and appends one JSON event per line.
- scenarios.json: the Checkpoint B plan (C3 Q02-Q08, Q05.a, Q14, Q15,
  Q18, Q19, Q40; C4 Q09.a/Q09.b), with Q17 kept Manual and Q39
  NotApplicable, each with a declarative pass rule.

LocalQualificationRunnerTests run pwsh (failing, never skipping, when it
is absent): the guard for CI, GITHUB_ACTIONS and TF_BUILD, a scope-aware
AST proof that nothing writes into the installation, strict mode, no
workflow reference, receipt assembly validated by the C# receipt rules,
redaction, name-only registry diffs, closure refusals, scenario/harness
consistency, and every generated driver compiled with the committed Lua
5.3 module. PSScriptAnalyzer reports no warning or error.
dotnet sln add tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests, the only slnx entry that survives the maintainer's pivot (the eng/abi and docs/abi folder entries the old snapshot added no longer apply, since neither path exists on the current tip).
dotnet format --include (scoped to the files this lot touched) fixes the IDE0055/MA0002 findings left by writing the restored files with git show (which bypasses the eol=crlf checkout filter). It also adds StringComparer.Ordinal to a batch of Assert.Contains calls in ClassicSlotRegistryDocumentTests.cs (MA0002). One file, tests/CheatEngine.SDK.NativeAotLoaderHarness/LibraryProbeContract.cs, is compiled into two projects (the harness and its new Tests project via <Compile Link>); formatting it through both project contexts in one solution-wide dotnet format call made the tool insert literal 'Unmerged change' conflict markers into the file. Restored it from the preserved pre-format commit and re-verified with dotnet format scoped to each project individually (no further change needed). dotnet build CheatEngine.SDK.slnx -c Debug now succeeds with 0 warnings, 0 errors.
TestMethodTraits.Of only matched a void-returning method; broaden it to Task/ValueTask so it finds CheckStackFailureProcessTests.Direct_checkstack_growth_with_a_rejecting_allocator_returns_zero_and_recovers (an async Task test), restoring Checkstack_proof_record_matches_the_bundled_lua_and_the_lf_pinned_bridge_sources. Re-apply the libs/CheatEngine.SDK.Lua.Interop/README.md Q23 paragraph and Promise bullet: the second worktree recreation (see the final report) restored this file from its pre-edit committed content and the edit was not redone until this pass, so Retired_live_ids_are_absent correctly caught the regression. tests/CheatEngine.SDK.Repository.Tests now passes 174/175; the one remaining failure (Every_project_has_a_committed_lock_file, for the new NativeAotLoaderHarness.Tests project) is the expected, integrator-owned lock-file gap reported in requestsForOtherFiles.
The lot added tests/CheatEngine.SDK.NativeAotLoaderHarness.Tests to
CheatEngine.SDK.slnx without a packages.lock.json (lot agents never
regenerate lock files; that is the integrator's job). Generate it with
dotnet restore --force-evaluate and verify --locked-mode for this
project, the full solution and both out-of-slnx AOT/LiveProbe
projects.
Wave 2 landed five lots on this branch (s-gen-a, s-rt, s-res, s-scan,
s-abi); the Unreleased section still reflected the pre-Wave-2, pre-pivot
state. Drop the qualification-support-profile bullet: the maintainer's
no-custom-scripting pivot deleted docs/qualification/ and
eng/qualification/ before Wave 2 started, so the feature it described no
longer exists on this branch. Reword the documentation bullet the same
way (no docs/ index remains to link).

Add Added/Changed/Fixed entries for what the five lots actually shipped:
LuaOptional<T> and the optional/variadic EngineApi grammar (S-GEN-A);
bounded, deadline-terminable and first-found AOB scans plus the
NoResult zero-match model (S-SCAN); TargetReleaseStatus.RefusedRuntimeChanged
(S-RES); RuntimeInfo.TryDeriveTargetArchitecture and the
PointerSize.FromArchitecture obsoletion (S-RT); and the CESDK9102/
CESDK0006/CESDK1020 diagnostics (S-ABI, S-RT). Call out the shared
Unknown = 0 renumbering of every SDK-owned outcome enum added since
1.0.0 as one breaking-but-unreported-by-ApiCompat entry, since
S-GEN-A, S-RT and S-SCAN each applied the same orchestrator decision to
a different enum.
S-ABI published the F02 NativeAOT plugin restrictions in
libs/CheatEngine.SDK.Abi/README.md and the packed src/CheatEngine.SDK/
README.md, but the root README's Requirements section, the page a
plugin author reads first, never mentioned that a NativeAOT plugin DLL
is not a supported profile. Add one line there and link to the packed
README's "Load profiles and limits" section instead of the originally
proposed docs/abi/nativeaot-profile.md, which does not exist under the
no-docs/ pivot.
The maintainer's no-custom-scripting pivot dropped S-CAT-A's docs/catalog/
deliverable (it is now moot; its target no longer exists on this
branch), but this README still pointed coverage measurement at that
path. Describe the Lua surface catalogue as a concept only, the same
way libs/CheatEngine.SDK.Engine/README.md already does, instead of
naming a path that was never created.
WI-4 (branch N): the Checkpoint B Q05.a receipt is not available on this
branch (no local Cheat Engine qualification run). Per the O5 recommendation,
keep the existing ANSI encoding of AnsiNameBuffer unchanged and document the
ANSI-vs-UTF8 question as open pending a future host-based qualification run,
in the type's XML remarks and the Hosting README. No code or test behaviour
changes.
WI-5: HostLog.IdentifyOnEnable (programmatic) and the
CHEATENGINE_SDK_IDENTIFY_ON_ENABLE=1 environment variable opt an enable
attempt into a single, bounded CheatEngineSdkIdentification log entry,
written before the exports record is copied so it survives a later bind
or construction failure. It reports SDK version/commit/consistency, the
Hosting assembly MVID and a redacted load-context token, the plugin id
and factory assembly, the raw (never interpreted) bootstrap argument, the
bridge fingerprint/hash, the bound Lua module file name/hash, and the CE
process file/version and runtime/arch - fixed key order, each value
bounded to 128 characters (the bridge fingerprint's fixed 129-character
shape is the single documented exception), the whole entry to 1024. The
builder calls no Lua API and constructs no plugin.

No local Cheat Engine qualification run is available on this branch, so
every fact here is recorded as diagnostic only, never as a qualified
observation.
WI-5: covers the opt-in gate (programmatic and environment-seam),
emission before the exports record is copied (including when the Lua
bind fails), the fixed key order and per-value/whole-entry bounds, the
sdk.version/sdk.commit and bridge.fingerprint field shapes, the
Lua-module file name and hash reported without a directory, that no
value contains a directory separator, a drive root or the current user
name, that building the line calls no Lua API and constructs no plugin,
and the raw host argument being recorded without interpretation.
Traited Qualification=Q46 where the test evidences that scenario.
A C++20 host that drives the coexistence A/B plugin protocol through the real
managed (hostfxr) load path (component and default ALC routes), measuring
Hosting-instance identity, MVID and ALC relationships instead of assuming
them. Never starts, reads or references an installed Cheat Engine.

build.ps1 mirrors tests/native-abi-fixture/build.ps1's VsDevCmd pattern and
writes a manifest of every input's SHA-256 plus the observed toolchain
versions. Verified end to end: separate-folder and shared-folder component
routes both isolate each plugin's Hosting instance; the default-ALC route
measurably rejects the second plugin's factory registration.
Appended only, at the end of the existing identity string: the Checkpoint B
runner and receipts parse the existing prefix and keep working unmodified.
Lets a C2 or exact-host observer tell whether two enabled plugins share one
PluginHost static instance (distinct type handles) and whether a re-enable
advanced the SDK's attach epoch.
NativeHostEmulatorTests runs the emulator against the prebuilt Coexistence
plugin outputs (never a ProjectReference, so no packages.lock.json changes)
for the separate-folder, shared-folder and default-ALC scenarios, gated by
CESDK_NATIVE_HOST_EMULATOR_DIR/_REQUIRED exactly like the native ABI
fixture's own pair. Verified green end to end against a locally patched
build of the Coexistence plugins (see the lot report); red as committed
today because tests/CheatEngine.SDK.LivePlugin.Coexistence/CoexistencePlugin.props
never opts into entry-point generation for a direct ProjectReference build,
which is outside this lot's owned files.
Confirms build.ps1 checks the exit code after cl.exe, writes only under its
own output directory, and that no file under tests/native-host-emulator
references an installed Cheat Engine.
Implements F04/ADR-07's conservative concurrency default and the 2.0
resetLuaState decision (WI-1, WI-3). LuaRuntime.AcquireOperation()-family
calls now refuse a worker thread with LuaAdmissionStatus.ThreadNotAdmitted
before the host's state provider ever runs, unless the caller is the
captured main thread, already inside admitted work, or the single
documented default exception (MainThreadDispatcher's synchronize hand-off,
now routed through AcquireOperationForMainThreadDispatch). A worker opts in
through LuaRuntime.AdmitWorkerThreads(), gated [Experimental("CESDK5001")]
until Q19 passes at C3 and C4.

Every attachment is now stamped with a private-registry universe marker,
checked raw and allocation-free on each admitted acquisition. A mismatch
means the host replaced its Lua state outside the SDK-owned reset path:
LuaRuntime.ExternalStateResetDetected becomes true, every admission path
refuses with ExternalStateReset until the next Attach, and Detach/Attach
abandon live callbacks and host subscriptions instead of unregistering
into the replacement registry (A08-22). There remains no public reset API.

No local CE qualification run is available for either the worker opt-in or
the reset-detection observation, so both are documented as unqualified
pending a future host-based validation run.
WI-2 (F04/A23-F04-3): the "one per assembly load context (one per plugin)"
and "one load context hosts one plugin" phrasing asserted a host loader
cardinality this SDK copy has never observed. Reword every occurrence to
the fact this SDK copy actually owns (one binding per loaded assembly
instance) and defer the load-context/plugin equivalence to Q09 evidence.

Add ConcurrencyContractDocumentationTests to keep both claims out of
libs/** and tests/** going forward.
WI-6 (A24-21, SRC02-06): a sink that calls HostLog.Write from its own
Write, directly or through a path that logs, no longer recurses towards an
uncatchable StackOverflowException. A per-thread guard drops the reentrant
write and counts it (HostLog.DroppedReentrantEntries); other threads are
unaffected. Move the second-factory-registration rejection log in
PluginHost.TryRegisterFactory outside the registration gate, so a sink
reacting to it never runs while that lock is held.

Document the containment contract on IHostLogSink and HostLog, and update
EnablePluginTests' existing reentrant-disable assertion for the new
drop-and-count behaviour instead of the log entry it used to observe.
The bootstrap argument is proven never to change the 36-byte record
write, for int.MinValue through int.MaxValue (A04-01). A free-counter
seam on LuaModuleLocator.BindLocated proves repeated enabling releases
exactly the one loader reference each already-bound lookup adds,
never more and never less (A05-01). Disable now has a test that pumps
a worker genuinely blocked inside the host's real Lua synchronize
call, not only a MainThreadDispatcher.Dispatch override, before it
detaches (A08-28), and the existing GUI-thread pump test is tagged
Qualification=Q07 to match.

Per the maintainer's pivot there is no docs/qualification/matrix.json
on this branch, so the brief's "Matrix" bullet is dropped; the
Qualification traits above are the part that still applies.
Hosting README gains a Promise item naming the three WI-8 exit
tests. tests/CheatEngine.SDK.Hosting.Tests/README.md, which predated
this lot's merge and named no individual test, gains bullets for the
threading default, external-reset detection, log containment,
opt-in identification, the native host emulator and the WI-8 exit
tests contributed by the three merged S-HOST worktrees.
CoexistencePlugin.props built PluginA/PluginB through a direct
ProjectReference chain and never set CheatEngineSdkGenerateEntryPoint or
registered it as a CompilerVisibleProperty, unlike the packaged
src/CheatEngine.SDK/build/CheatEngine.SDK.props asset. The EntryPoint
generator therefore stayed silent by its own documented design and
neither plugin assembly carried a CESDK.CESDK bootstrap type, so
NativeHostEmulatorTests reported skipped instead of ok for every
bootstrap/enable fact (5 of 9 failing).

Mirror the packaged asset's two-line opt-in on the fixture props.
Verified locally: rebuilt Debug (0 warnings/errors), rebuilt the native
host emulator, and reran NativeHostEmulatorTests with
CESDK_NATIVE_HOST_EMULATOR_DIR/REQUIRED=true set: 9/9 pass. Update the
emulator README's "known local dependency" section to describe the fix
instead of the prior skipped-not-failed caveat.

Authorized by the binding orchestrator decision on S-HOST WI-7
CoexistencePlugin.props (2026-09-23), granting exactly this two-block
XML change as a direct follow-up before integration.
The CoexistencePlugin.props fix in the previous commit makes the
EntryPoint generator emit CESDK.CESDK.CEPluginInitialize for both
coexistence fixtures, so LivePluginClosureTests now finds a real entry
point where it previously expected none. The test's own contract is
that the pending list only shrinks once a bundle gains the entry
point, so remove both coexistence projects from it and update the
class doc comment; CheatEngine.SDK.LivePlugin is unaffected and stays
pending. Full-solution Debug suite: 4097/4097 passing (was 4095/4097).
Adds the native-host-emulator job to ci.yml (build tests/native-host-
emulator/build.ps1, upload its artifact, feed it to the Debug build-
test leg and gate.needs), matching the id and name the workflow
contract already reserved for it. Updates the frozen build-test needs
assertion to match.

Documents everything Wave 2 shipped: the Lua concurrency contract and
CESDK5001 in CONTRIBUTING.md and .coderabbit.yaml, the CESDK5xxx
identifier range in the analyzer catalog, and the CHANGELOG entries
for the threading default, external-reset detection, the
identification opt-in, log containment, and the native host emulator.
@sonarqubecloud

Copy link
Copy Markdown

@AriusII
AriusII merged commit 9a8e609 into main Sep 23, 2026
20 checks passed
@AriusII
AriusII deleted the feat/audit-remediation-cicd branch September 23, 2026 19:48
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.

2 participants