diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..6615a71 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,107 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json + +language: "en-US" +early_access: false + +reviews: + profile: "chill" + request_changes_workflow: false + high_level_summary: true + review_status: false + poem: false + + # The GitHub App performs automatic reviews. No CodeRabbit token, CLI, or workflow is used here. + auto_review: + enabled: true + auto_incremental_review: true + auto_pause_after_reviewed_commits: 0 + drafts: false + base_branches: ["main"] + ignore_usernames: ["dependabot[bot]"] + + tools: + github-checks: + enabled: true + # pull-request-ci.yml runs the pinned, checksum-verified actionlint binary. + actionlint: + enabled: false + + path_filters: + - "!artifacts/**" + - "!TestResults/**" + - "!**/bin/**" + - "!**/obj/**" + - "!**/packages.lock.json" + - "!**/*.nupkg" + - "!**/*.snupkg" + - "!**/*.g.cs" + - "!**/*.generated.cs" + + path_instructions: + - path: "libs/CheatEngine.Client.Abstractions/**" + instructions: | + Preserve a public, high-level Client API. Do not expose LuaState, CEObject, Owned, native pointers, + activation-bound handles, or SDK lifetime ownership. SDK values that cross this API must be stable copied values. + + - path: "libs/CheatEngine.Client.Core/**" + instructions: | + Treat Core as the internal execution adapter to CheatEngine.SDK. Verify SDK mappings stay internal, partial + effects and operational errors remain observable, and Client resources are released before SDK detachment. + Flag any public Client API that leaks an SDK handle or an SDK lifetime responsibility. + + - path: "libs/CheatEngine.Client.Hosting/**" + instructions: | + Review activation lifecycle changes carefully: construction must not invoke Cheat Engine, DI scopes are per + activation, cleanup is deterministic, and no state survives a disable/enable cycle. + + - path: "src/**" + instructions: | + Keep the facade developer-focused and independent from ABI, Lua binding, native ownership, and dispatcher + details. Direct SDK dependencies must not bypass the Client Core layer. + + - path: "source-generators/**" + instructions: | + Require deterministic generated output and diagnostics that preserve public-boundary protections. Do not relax + lifetime or interop safeguards without focused tests, and do not introduce public SDK-bound handles. + + - path: "templates/**" + instructions: | + Keep templates approachable for plugin developers while preserving the explicit SDK bootstrap and generation + assets they require. Package and template smoke coverage belongs in the C# test suite. + + - path: "tests/**" + instructions: | + Require behavior tests for success, failure, cancellation, cleanup, and lifecycle transitions. Packaging tests + must consume the packages built by CI and keep realistic Client plus SDK dependencies. + + - path: ".github/**" + instructions: | + Review least privilege, full-SHA action pins, reusable-workflow contracts, artifact producer/consumer paths, + fork and secret guards, and deterministic dependency sources. Do not duplicate the actionlint check already + executed by pull-request-ci.yml. + + - path: "{Directory.Build.props,Directory.Build.targets,Directory.Packages.props,global.json}" + instructions: | + Preserve the net10/MTP/package validation contracts and central dependency management. Flag a change that + weakens the Client to SDK layering rules or makes builds and tests less reproducible. + + - path: "{README.md,ROADMAP.md,docs/**}" + instructions: | + Keep the architecture boundary explicit: CheatEngine.SDK owns ABI, native bindings, Lua globals, dispatcher, + and native resource ownership; CheatEngine.Client owns high-level workflows, policies, and developer ergonomics. + +knowledge_base: + code_guidelines: + enabled: true + filePatterns: + - "CLAUDE.md" + linked_repositories: + - repository: "CheatEngineNet/CheatEngine.SDK" + instructions: | + This is the authoritative low-level SDK. Use it to assess API/package compatibility and invariants around ABI, + native ownership, dispatcher behavior, and Lua bridging. Client changes must not duplicate or expose these + implementation responsibilities. + +chat: + # Keep conversations opt-in with an explicit @coderabbitai mention. + auto_reply: false diff --git a/.editorconfig b/.editorconfig index c1897ef..22262b1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -138,6 +138,11 @@ dotnet_naming_style.underscore_camel_case.capitalization = camel_case # IDE0005 requires XML documentation generation to run during a command-line # build. Shipping projects already enable it; tests and samples intentionally # retain it as an editor suggestion rather than acquiring build-only XML output. + + +# CS8762: Parameter must have a non-null value when exiting in some condition. +dotnet_diagnostic.CS8762.severity = none + [libs/**.cs] dotnet_diagnostic.IDE0005.severity = error diff --git a/.github/ISSUE_TEMPLATE/engineering-work.yml b/.github/ISSUE_TEMPLATE/engineering-work.yml deleted file mode 100644 index 9d77a17..0000000 --- a/.github/ISSUE_TEMPLATE/engineering-work.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Engineering work item -description: Define an evidence-backed, owned and reviewable engineering outcome. -title: "[CLI] " -body: - - type: markdown - attributes: - value: | - Keep CE integration in SDK and application policy/workflows in Client. - Do not report fixture, source or AOT publication evidence as live-host qualification. - - type: textarea - id: outcome - attributes: - label: Outcome and owner - description: State the problem, owning layer, desired behavior and explicit exclusions. - validations: - required: true - - type: textarea - id: evidence - attributes: - label: Source and current behavior - description: Include pinned file/commit links and distinguish observations from hypotheses. - validations: - required: true - - type: textarea - id: scope - attributes: - label: Requirements and risks - description: Include ownership, target/lifecycle, cancellation, partial effects and compatibility. - validations: - required: true - - type: textarea - id: acceptance - attributes: - label: Acceptance criteria and validation - description: Specify executable checks, commands, fixtures and live gates; mark unexecuted tests. - validations: - required: true - - type: textarea - id: dependencies - attributes: - label: Parent, blockers and required artifacts - description: Link issues and exact containing SDK artifacts; also set native GitHub relationships. - validations: - required: true - - type: textarea - id: delivery - attributes: - label: Proposed branch, PR scope and documentation - description: Use a focused PR; do not auto-close upstream work or create an epic-sized change. diff --git a/.github/ISSUE_TEMPLATE/epic.yml b/.github/ISSUE_TEMPLATE/epic.yml deleted file mode 100644 index 6086b83..0000000 --- a/.github/ISSUE_TEMPLATE/epic.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Engineering epic -description: Define an evidence-backed, owned and reviewable engineering outcome. -title: "[CLI] " -body: - - type: markdown - attributes: - value: | - Keep CE integration in SDK and application policy/workflows in Client. - Do not report fixture, source or AOT publication evidence as live-host qualification. - - type: textarea - id: outcome - attributes: - label: Outcome and owner - description: State the problem, owning layer, desired behavior and explicit exclusions. - validations: - required: true - - type: textarea - id: evidence - attributes: - label: Source and current behavior - description: Include pinned file/commit links and distinguish observations from hypotheses. - validations: - required: true - - type: textarea - id: scope - attributes: - label: Requirements and risks - description: Include ownership, target/lifecycle, cancellation, partial effects and compatibility. - validations: - required: true - - type: textarea - id: acceptance - attributes: - label: Acceptance criteria and validation - description: Specify executable checks, commands, fixtures and live gates; mark unexecuted tests. - validations: - required: true - - type: textarea - id: dependencies - attributes: - label: Parent, blockers and required artifacts - description: Link issues and exact containing SDK artifacts; also set native GitHub relationships. - validations: - required: true - - type: textarea - id: delivery - attributes: - label: Proposed branch, PR scope and documentation - description: Use a focused PR; do not auto-close upstream work or create an epic-sized change. diff --git a/.github/ISSUE_TEMPLATE/research.yml b/.github/ISSUE_TEMPLATE/research.yml deleted file mode 100644 index fc03620..0000000 --- a/.github/ISSUE_TEMPLATE/research.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Research and qualification -description: Define an evidence-backed, owned and reviewable engineering outcome. -title: "[CLI] " -body: - - type: markdown - attributes: - value: | - Keep CE integration in SDK and application policy/workflows in Client. - Do not report fixture, source or AOT publication evidence as live-host qualification. - - type: textarea - id: outcome - attributes: - label: Outcome and owner - description: State the problem, owning layer, desired behavior and explicit exclusions. - validations: - required: true - - type: textarea - id: evidence - attributes: - label: Source and current behavior - description: Include pinned file/commit links and distinguish observations from hypotheses. - validations: - required: true - - type: textarea - id: scope - attributes: - label: Requirements and risks - description: Include ownership, target/lifecycle, cancellation, partial effects and compatibility. - validations: - required: true - - type: textarea - id: acceptance - attributes: - label: Acceptance criteria and validation - description: Specify executable checks, commands, fixtures and live gates; mark unexecuted tests. - validations: - required: true - - type: textarea - id: dependencies - attributes: - label: Parent, blockers and required artifacts - description: Link issues and exact containing SDK artifacts; also set native GitHub relationships. - validations: - required: true - - type: textarea - id: delivery - attributes: - label: Proposed branch, PR scope and documentation - description: Use a focused PR; do not auto-close upstream work or create an epic-sized change. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..73fd4ad --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,21 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + groups: + github-actions: + patterns: ["*"] + + - package-ecosystem: nuget + directory: / + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + groups: + dotnet: + patterns: ["*"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3864c01..0ecbc16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,16 @@ name: Client validation on: workflow_call: + inputs: + collect_sonar_coverage: + description: Produce Microsoft XML coverage reports consumed by the guarded Sonar job. + required: false + type: boolean + default: false + secrets: + SONAR_TOKEN: + description: SonarQube Cloud token for protected same-repository analysis. + required: false permissions: contents: read @@ -37,19 +47,31 @@ jobs: **/packages.lock.json Directory.Packages.props - - name: Validate engineering backlog and validator tests - run: | - python --version - python eng/Validate-EngineeringManifest.py - python -m unittest discover -s eng/tests -v - - name: Restore locked dependency graph - run: dotnet restore CheatEngine.Client.slnx --locked-mode + run: | + dotnet restore CheatEngine.Client.slnx --locked-mode + if ($LASTEXITCODE -ne 0) { + throw "dotnet restore failed with exit code $LASTEXITCODE." + } - name: Build Release - run: dotnet build CheatEngine.Client.slnx --configuration Release --no-restore --warnaserror + run: | + dotnet build CheatEngine.Client.slnx --configuration Release --no-restore --warnaserror + if ($LASTEXITCODE -ne 0) { + throw "dotnet build failed with exit code $LASTEXITCODE." + } + + - name: Pack and validate public package APIs + run: | + dotnet pack CheatEngine.Client.slnx --configuration Release --no-build --no-restore --output artifacts/packages + if ($LASTEXITCODE -ne 0) { + throw "dotnet pack failed with exit code $LASTEXITCODE." + } - name: Run unit tests through Microsoft Testing Platform + env: + COLLECT_SONAR_COVERAGE: ${{ inputs.collect_sonar_coverage }} + CHEATENGINE_CLIENT_PACKAGE_SOURCE: ${{ github.workspace }}/artifacts/packages run: | $projects = @(Get-ChildItem -Path tests -Filter '*.Tests.csproj' -Recurse -File | Sort-Object Name) if ($projects.Count -eq 0) { @@ -66,9 +88,53 @@ jobs: '--results-directory', $results, '--fail-skips', 'on' ) + + if ($env:COLLECT_SONAR_COVERAGE -eq 'true') { + $coverage = [IO.Path]::GetFullPath((Join-Path (Join-Path 'artifacts/sonar-test-results' $project.BaseName) 'coverage.xml')) + $options += @( + '--coverage', + '--coverage-output', $coverage, + '--coverage-output-format', 'xml' + ) + } + dotnet test @options + if ($LASTEXITCODE -ne 0) { + throw "Microsoft Testing Platform failed for '$($project.FullName)' with exit code $LASTEXITCODE." + } } + - name: Verify Sonar coverage reports + if: ${{ inputs.collect_sonar_coverage && !cancelled() }} + run: | + $projects = @(Get-ChildItem -Path tests -Filter '*.Tests.csproj' -Recurse -File) + $reports = @($projects | ForEach-Object { + Join-Path (Join-Path 'artifacts/sonar-test-results' $_.BaseName) 'coverage.xml' + }) + $missingReports = @($reports | Where-Object { -not (Test-Path -LiteralPath $_ -PathType Leaf) }) + if ($missingReports.Count -gt 0) { + throw "Microsoft Testing Platform did not produce XML coverage report(s): $($missingReports -join ', ')." + } + + foreach ($report in $reports) { + if ((Get-Item -LiteralPath $report).Length -eq 0) { + throw "Coverage report '$report' is empty." + } + + try { + $coverage = [xml](Get-Content -LiteralPath $report -Raw) + } + catch { + throw "Coverage report '$report' is not valid XML. $($_.Exception.Message)" + } + + if ($null -eq $coverage.DocumentElement) { + throw "Coverage report '$report' has no XML document element." + } + } + + Write-Host "Verified $($reports.Count) non-empty XML coverage report(s)." + - name: Summarize test results if: ${{ !cancelled() }} run: | @@ -101,15 +167,14 @@ jobs: if-no-files-found: warn retention-days: 14 - - name: Pack and validate public package APIs - run: dotnet pack CheatEngine.Client.slnx --configuration Release --no-build --no-restore - - - name: Smoke test isolated package consumption - shell: pwsh - run: ./eng/Invoke-PackageSmoke.ps1 -PackageSource ./artifacts/packages - - - name: Smoke test local template installation - run: ./eng/Invoke-TemplateSmoke.ps1 -PackageSource ./artifacts/packages + - name: Upload Sonar coverage reports + if: ${{ inputs.collect_sonar_coverage && !cancelled() }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: sonar-coverage + path: artifacts/sonar-test-results/*/coverage.xml + if-no-files-found: error + retention-days: 14 - name: Upload package artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -122,10 +187,18 @@ jobs: retention-days: 14 - name: Publish Native AOT reference probe - run: dotnet publish tests/CheatEngine.Client.AotProbe/CheatEngine.Client.AotProbe.csproj --configuration Release --runtime win-x64 --no-restore --output ./artifacts/aot-probe + run: | + dotnet publish tests/CheatEngine.Client.AotProbe/CheatEngine.Client.AotProbe.csproj --configuration Release --runtime win-x64 --no-restore --output ./artifacts/aot-probe + if ($LASTEXITCODE -ne 0) { + throw "dotnet publish failed with exit code $LASTEXITCODE." + } - name: Run Native AOT reference probe - run: ./artifacts/aot-probe/CheatEngine.Client.AotProbe.exe + run: | + ./artifacts/aot-probe/CheatEngine.Client.AotProbe.exe + if ($LASTEXITCODE -ne 0) { + throw "The Native AOT reference probe failed with exit code $LASTEXITCODE." + } - name: Upload Native AOT probe if: ${{ !cancelled() }} @@ -136,22 +209,92 @@ jobs: if-no-files-found: warn retention-days: 14 + sonar: + name: Sonar + needs: validate + if: ${{ inputs.collect_sonar_coverage }} + uses: ./.github/workflows/sonar.yml + with: + ci_based_analysis: true + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + + dependency-review: + name: Dependency review + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false }} + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Review dependency changes + uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 + with: + fail-on-severity: high + + lint-workflows: + name: Lint workflows + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false }} + runs-on: windows-latest + timeout-minutes: 5 + env: + ACTIONLINT_VERSION: 1.7.12 + ACTIONLINT_SHA256: 6e7241b51e6817ea6a047693d8e6fed13b31819c9a0dd6c5a726e1592d22f6e9 + steps: + - name: Checkout workflow definitions + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + sparse-checkout: .github + persist-credentials: false + + - name: Run actionlint + run: | + $ErrorActionPreference = 'Stop' + $archive = Join-Path $env:RUNNER_TEMP 'actionlint.zip' + $url = "https://github.com/rhysd/actionlint/releases/download/v$env:ACTIONLINT_VERSION/actionlint_$($env:ACTIONLINT_VERSION)_windows_amd64.zip" + Invoke-WebRequest -Uri $url -OutFile $archive -MaximumRetryCount 3 -RetryIntervalSec 5 + $actual = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actual -ne $env:ACTIONLINT_SHA256) { + throw "actionlint $env:ACTIONLINT_VERSION has SHA-256 $actual, expected $env:ACTIONLINT_SHA256." + } + $destination = Join-Path $env:RUNNER_TEMP 'actionlint' + Expand-Archive -LiteralPath $archive -DestinationPath $destination + & (Join-Path $destination 'actionlint.exe') -color + if ($LASTEXITCODE -ne 0) { + throw "actionlint failed with exit code $LASTEXITCODE." + } + gate: name: Gate if: ${{ always() }} - needs: validate + needs: [validate, sonar, dependency-review, lint-workflows] runs-on: windows-latest timeout-minutes: 5 permissions: {} steps: - - name: Check validation result + - name: Check required results env: VALIDATE_RESULT: ${{ needs.validate.result }} + SONAR_RESULT: ${{ needs.sonar.result }} + SONAR_REQUIRED: ${{ inputs.collect_sonar_coverage }} + DEPENDENCY_RESULT: ${{ needs.dependency-review.result }} + LINT_RESULT: ${{ needs.lint-workflows.result }} run: | - "| Job | Result |", "| --- | --- |", "| validate | $env:VALIDATE_RESULT |" | - Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 - if ($env:VALIDATE_RESULT -ne 'success') { - Write-Host "::error::Validation finished with '$env:VALIDATE_RESULT'." + $results = [ordered]@{ + validate = $env:VALIDATE_RESULT + sonar = $env:SONAR_RESULT + 'dependency-review' = $env:DEPENDENCY_RESULT + 'lint-workflows' = $env:LINT_RESULT + } + $rows = $results.GetEnumerator() | ForEach-Object { "| $($_.Key) | $($_.Value) |" } + '| Job | Result |', '| --- | --- |', $rows | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 + $failed = @($results.GetEnumerator() | Where-Object { + $_.Key -eq 'validate' -and $_.Value -ne 'success' -or + $_.Key -eq 'sonar' -and $env:SONAR_REQUIRED -eq 'true' -and $_.Value -ne 'success' -or + $_.Key -in 'dependency-review', 'lint-workflows' -and $_.Value -ne 'success' + } | ForEach-Object Key) + if ($failed.Count -gt 0) { + Write-Host "::error::Required job(s) did not succeed: $($failed -join ', ')." exit 1 } diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index 13a965c..26dfcc8 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -3,22 +3,6 @@ name: Main CI on: push: branches: [main] - paths: - - '.editorconfig' - - '.github/**' - - 'CheatEngine.Client.slnx' - - 'Directory.Build.props' - - 'Directory.Build.targets' - - 'Directory.Packages.props' - - 'README.md' - - 'ROADMAP.md' - - 'docs/**' - - 'eng/**' - - 'global.json' - - 'libs/**' - - 'src/**' - - 'templates/**' - - 'tests/**' merge_group: types: [checks_requested] workflow_dispatch: @@ -34,3 +18,7 @@ jobs: ci: name: CI uses: ./.github/workflows/ci.yml + with: + collect_sonar_coverage: ${{ vars.SONAR_CI_ENABLED == 'true' && github.ref == 'refs/heads/main' }} + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/pull-request-ci.yml b/.github/workflows/pull-request-ci.yml index 60ba7b7..5210a23 100644 --- a/.github/workflows/pull-request-ci.yml +++ b/.github/workflows/pull-request-ci.yml @@ -3,22 +3,6 @@ name: Pull request CI on: pull_request: types: [opened, synchronize, reopened, ready_for_review] - paths: - - '.editorconfig' - - '.github/**' - - 'CheatEngine.Client.slnx' - - 'Directory.Build.props' - - 'Directory.Build.targets' - - 'Directory.Packages.props' - - 'README.md' - - 'ROADMAP.md' - - 'docs/**' - - 'eng/**' - - 'global.json' - - 'libs/**' - - 'src/**' - - 'templates/**' - - 'tests/**' concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number }} @@ -32,35 +16,11 @@ jobs: name: CI if: github.event.pull_request.draft == false uses: ./.github/workflows/ci.yml - - lint-workflows: - name: Lint workflows - if: github.event.pull_request.draft == false - runs-on: windows-latest - timeout-minutes: 5 - env: - ACTIONLINT_VERSION: 1.7.12 - ACTIONLINT_SHA256: 6e7241b51e6817ea6a047693d8e6fed13b31819c9a0dd6c5a726e1592d22f6e9 - - steps: - - name: Checkout workflow definitions - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - sparse-checkout: .github - persist-credentials: false - - - name: Run actionlint - run: | - $ErrorActionPreference = 'Stop' - $archive = Join-Path $env:RUNNER_TEMP 'actionlint.zip' - $url = "https://github.com/rhysd/actionlint/releases/download/v$env:ACTIONLINT_VERSION/actionlint_$($env:ACTIONLINT_VERSION)_windows_amd64.zip" - Invoke-WebRequest -Uri $url -OutFile $archive -MaximumRetryCount 3 -RetryIntervalSec 5 - - $actual = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant() - if ($actual -ne $env:ACTIONLINT_SHA256) { - throw "actionlint $env:ACTIONLINT_VERSION has SHA-256 $actual, expected $env:ACTIONLINT_SHA256." - } - - $destination = Join-Path $env:RUNNER_TEMP 'actionlint' - Expand-Archive -LiteralPath $archive -DestinationPath $destination - & (Join-Path $destination 'actionlint.exe') -color + with: + collect_sonar_coverage: >- + ${{ vars.SONAR_CI_ENABLED == 'true' + && github.event.pull_request.draft == false + && github.event.pull_request.head.repo.full_name == github.repository + && github.event.pull_request.user.login != 'dependabot[bot]' }} + secrets: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml new file mode 100644 index 0000000..c75b941 --- /dev/null +++ b/.github/workflows/sonar.yml @@ -0,0 +1,231 @@ +name: Sonar + +on: + workflow_call: + inputs: + ci_based_analysis: + description: Acknowledge that SonarQube Cloud Automatic Analysis is disabled for this project. + required: false + type: boolean + default: true + project-key: + description: SonarQube Cloud project key. + type: string + default: CheatEngineNet_CheatEngine.Client + organization: + description: SonarQube Cloud organization key. + type: string + default: cheatenginenet + secrets: + SONAR_TOKEN: + description: SonarQube Cloud token for protected, same-repository analysis. + required: true + +permissions: + contents: read + +defaults: + run: + shell: pwsh + +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + MSBUILDDISABLENODEREUSE: true + SONAR_SCANNER_VERSION: 11.3.0 + +jobs: + analyze: + name: Analyze + runs-on: windows-latest + timeout-minutes: 25 + env: + SONAR_PROJECT_KEY: ${{ inputs.project-key }} + SONAR_ORGANIZATION: ${{ inputs.organization }} + + steps: + # SonarQube Cloud has no supported API that reads a project's Automatic Analysis setting. The scanner begin + # call below is the earliest authoritative service-side preflight and fails before restore/build if Automatic + # Analysis was not disabled in the project's Administration > Analysis Method UI. + - name: Preflight CI-based analysis + env: + CI_BASED_ANALYSIS: ${{ inputs.ci_based_analysis }} + run: | + if ($env:CI_BASED_ANALYSIS -ne 'true') { + throw 'Sonar analysis requires CI-based analysis. Disable Automatic Analysis in SonarQube Cloud before enabling this workflow.' + } + + $sonarUserHome = Join-Path $env:RUNNER_TEMP 'sonar-user-home' + "SONAR_USER_HOME=$sonarUserHome" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + + @( + '### Sonar analysis mode', + '', + 'CI-based analysis is enabled for this run. SonarQube Cloud Automatic Analysis must remain disabled for this project.' + ) | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 + + - name: Require Sonar token + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: | + if ([string]::IsNullOrWhiteSpace($env:SONAR_TOKEN)) { + throw 'SONAR_TOKEN is required when SONAR_CI_ENABLED is true.' + } + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Set up JDK 21 + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 + with: + distribution: zulu + java-version: '21' + + - name: Install pinned .NET SDK + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + global-json-file: global.json + # This secret-bearing job restores only through the generated NuGet.Config below. It deliberately does not + # restore a global package cache that another PR workflow could have populated. + cache: false + + # Only main saves this cache. Internal PRs may restore a known main-branch analyzer cache but cannot publish one. + - name: Restore Sonar analyzer cache + id: sonar-cache-restore + uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: ${{ runner.temp }}/sonar-user-home/cache + key: sonar-analyzers-${{ runner.os }}-${{ runner.arch }}-${{ env.SONAR_SCANNER_VERSION }}-v1 + + - name: Download Sonar coverage reports + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: sonar-coverage + path: artifacts/sonar-test-results + + - name: Verify coverage reports + run: | + $projects = @(Get-ChildItem -Path tests -Filter '*.Tests.csproj' -Recurse -File) + $reports = @($projects | ForEach-Object { + Join-Path (Join-Path 'artifacts/sonar-test-results' $_.BaseName) 'coverage.xml' + }) + $missingReports = @($reports | Where-Object { -not (Test-Path -LiteralPath $_ -PathType Leaf) }) + if ($missingReports.Count -gt 0) { + throw "Missing Sonar XML coverage report(s): $($missingReports -join ', ')." + } + + foreach ($report in $reports) { + if ((Get-Item -LiteralPath $report).Length -eq 0) { + throw "Coverage report '$report' is empty." + } + + try { + $coverage = [xml](Get-Content -LiteralPath $report -Raw) + } + catch { + throw "Coverage report '$report' is not valid XML. $($_.Exception.Message)" + } + + if ($null -eq $coverage.DocumentElement) { + throw "Coverage report '$report' has no XML document element." + } + } + + Write-Host "Verified $($reports.Count) non-empty XML coverage report(s)." + + - name: Create isolated NuGet configuration + id: isolated-nuget-config + run: | + $configPath = Join-Path $env:RUNNER_TEMP 'sonar-nuget.config' + @' + + + + + + + + '@ | Set-Content -LiteralPath $configPath -Encoding utf8NoBOM + + "path=$configPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + + - name: Install scanner from nuget.org only + env: + NUGET_CONFIG: ${{ steps.isolated-nuget-config.outputs.path }} + run: | + dotnet tool install dotnet-sonarscanner --tool-path "$env:RUNNER_TEMP/sonar-scanner" --version $env:SONAR_SCANNER_VERSION --configfile "$env:NUGET_CONFIG" + if ($LASTEXITCODE -ne 0) { + throw "dotnet-sonarscanner installation failed with exit code $LASTEXITCODE." + } + + - name: Begin analysis + env: + SONARQUBE_SCANNER_PARAMS: '{"sonar.token":"${{ secrets.SONAR_TOKEN }}"}' + run: | + # These findings conflict with deliberate repository contracts. Keep them in scanner configuration so the + # source remains free of Sonar-only attributes and suppressions. Test projects are excluded explicitly below. + $ignoredIssues = @( + @{ Key = 'noLinq'; Rule = 'csharpsquid:S3267'; Resource = '**/*.cs' } + @{ Key = 'unsafeInterop'; Rule = 'csharpsquid:S6640'; Resource = '**/*.cs' } + @{ Key = 'nullableFlow'; Rule = 'csharpsquid:S8970'; Resource = '**/*.cs' } + @{ Key = 'apiOverloadLayout'; Rule = 'csharpsquid:S4136'; Resource = 'libs/**' } + @{ Key = 'generatorInstances'; Rule = 'csharpsquid:S2325'; Resource = 'source-generators/**' } + @{ Key = 'emittedFragments'; Rule = 'csharpsquid:S1192'; Resource = 'source-generators/**' } + @{ Key = 'generatorComments'; Rule = 'csharpsquid:S125'; Resource = 'source-generators/**' } + ) + $arguments = @( + "/k:$env:SONAR_PROJECT_KEY" + "/o:$env:SONAR_ORGANIZATION" + '/d:sonar.exclusions=artifacts/**,tests/CheatEngine.Client.Benchmarks/**' + '/d:sonar.dotnet.excludeTestProjects=true' + '/d:sonar.cs.vscoveragexml.reportsPaths=artifacts/sonar-test-results/*/coverage.xml' + "/d:sonar.issue.ignore.multicriteria=$(($ignoredIssues.Key) -join ',')" + '/d:sonar.qualitygate.wait=true' + '/d:sonar.qualitygate.timeout=300' + ) + foreach ($issue in $ignoredIssues) { + $arguments += "/d:sonar.issue.ignore.multicriteria.$($issue.Key).ruleKey=$($issue.Rule)" + $arguments += "/d:sonar.issue.ignore.multicriteria.$($issue.Key).resourceKey=$($issue.Resource)" + } + & "$env:RUNNER_TEMP/sonar-scanner/dotnet-sonarscanner.exe" begin @arguments + if ($LASTEXITCODE -ne 0) { + throw "Sonar begin analysis failed with exit code $LASTEXITCODE." + } + + - name: Restore locked dependency graph from nuget.org only + env: + NUGET_CONFIG: ${{ steps.isolated-nuget-config.outputs.path }} + run: | + dotnet restore CheatEngine.Client.slnx --locked-mode --configfile "$env:NUGET_CONFIG" + if ($LASTEXITCODE -ne 0) { + throw "dotnet restore failed with exit code $LASTEXITCODE." + } + + - name: Build + run: | + dotnet build CheatEngine.Client.slnx --configuration Release --no-restore --no-incremental --disable-build-servers + if ($LASTEXITCODE -ne 0) { + throw "dotnet build failed with exit code $LASTEXITCODE." + } + + - name: End analysis and wait for quality gate + env: + SONARQUBE_SCANNER_PARAMS: '{"sonar.token":"${{ secrets.SONAR_TOKEN }}"}' + run: | + & "$env:RUNNER_TEMP/sonar-scanner/dotnet-sonarscanner.exe" end + if ($LASTEXITCODE -ne 0) { + throw "Sonar end analysis failed with exit code $LASTEXITCODE." + } + + - name: Save Sonar analyzer cache from main + if: >- + ${{ success() + && github.ref == 'refs/heads/main' + && steps.sonar-cache-restore.outputs.cache-hit != 'true' }} + uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + with: + path: ${{ runner.temp }}/sonar-user-home/cache + key: sonar-analyzers-${{ runner.os }}-${{ runner.arch }}-${{ env.SONAR_SCANNER_VERSION }}-v1 diff --git a/CheatEngine.Client.slnx b/CheatEngine.Client.slnx index dc3f222..32b24c0 100644 --- a/CheatEngine.Client.slnx +++ b/CheatEngine.Client.slnx @@ -38,13 +38,16 @@ Path="tests/CheatEngine.Client.Extensions.DependencyInjection.Tests/CheatEngine.Client.Extensions.DependencyInjection.Tests.csproj"/> - + - + - + CheatEngine.Client.Hosting - -> CheatEngine.Client.Extensions.DependencyInjection - -> CheatEngine.Client.Core -> CheatEngine.SDK -> Cheat Engine Lua/runtime - ^ -CheatEngine.Client.Abstractions <- CheatEngine.Client.Fluent -``` - -`Abstractions` owns public contracts, copied value types, failures, and module boundaries. `Fluent` creates immutable -operation descriptions and has no SDK access. `Core` is the only Client domain layer that maps operations to the SDK. -The DI extension owns explicit registrations and options validation; `Hosting` connects that composition to the plugin -lifecycle. The root `CheatEngine.Client` package is the consumer-facing umbrella. - -The labels in the diagram are package and assembly identities, not consumer namespace prefixes. Public contracts use -functional namespaces such as `CheatEngine.Client.Memory`, `.Scanning`, `.Tables`, `.Lua`, `.Runtime`, and `.Processes` -regardless of their delivery package. - -## Consequences and project value - -- Public APIs do not expose `LuaState`, `CEObject`, `Owned`, native pointers, or other host-bound SDK lifetimes. -- Application Lua exports are registered explicitly through `ILuaClient.RegisterModule`; the returned lease owns - activation-scoped unregistration. An `ILuaModule` may encapsulate generated SDK bindings internally but cannot let a - Lua state or SDK handle cross the Client contract. -- Builders remain pure. A complete Cheat Engine operation, including temporary-owner cleanup, crosses the SDK boundary - as one synchronous operation. This keeps fluent composition testable without a live host. -- A future remote bridge is a separate plugin-hosted product with its own protocol, authentication, backpressure, and - epoch-lifetime decision. It cannot introduce retries, transport concerns, or serializable handles into - `ICheatEngineClient` V1. diff --git a/docs/adr/0002-plugin-activation-lifecycle.md b/docs/adr/0002-plugin-activation-lifecycle.md deleted file mode 100644 index 08d3fa0..0000000 --- a/docs/adr/0002-plugin-activation-lifecycle.md +++ /dev/null @@ -1,55 +0,0 @@ -# ADR 0002: One Client activation per plugin enable epoch - -- Status: Accepted -- Date: 2026-09-20 - -## Context - -The SDK creates a plugin through a parameterless constructor and attaches the Lua runtime only for the enabled -lifetime. A long-lived provider, service, callback, or CE resource would therefore be able to outlive the host state -that makes it valid. - -## Decision and why - -`CheatEngineClientPlugin` creates a new `CheatEnginePluginBuilder`, service provider, and DI scope from `OnEnable`. -The derived plugin adds its own configuration sources in `Configure`; the Client does not load configuration implicitly. -For file-based configuration, a plugin may explicitly add an optional `appsettings.json` with `reloadOnChange: false`. - -The base validates options after building the provider, resolves the activation-scoped aggregate `ICheatEngineClient`, -enables registered modules in registration order, then invokes `OnClientEnabled`. A failed enable rolls back every -callback that was entered. - -On disable, the base clears the active Client reference first, invokes `OnClientDisabling`, disables enabled modules in -reverse order, drains Client-owned CE resources while the host is still attached, and finally disposes the scope, -provider, and configuration. Cleanup is best-effort and aggregates failures only after every step has been attempted. - -Before an activation is published, construction has a separate rollback path. It releases only the stages that were -actually acquired—scope, provider, then the host-created configuration—in reverse construction order. Each stage gets -one independent cleanup attempt. The construction exception remains the primary failure; cleanup failures follow it as -diagnostics. Module callbacks and Client-owned CE resource draining are never run for an incomplete activation. - -## Invariants - -- SDK calls are forbidden from plugin constructors, field initializers, and static initialization. SDK-dependent - services exist only while the plugin is enabled. -- `Configure` is a one-activation composition hook. It must not build a provider or reconfigure Client options after - provider construction; reloadable runtime configuration would violate the activation boundary. -- A Client scope, cancellation token, SDK handle, Lua reference, or CE-owned resource never crosses an enable/disable - epoch. `ICheatEngineClient.Epoch` and `Stopping` identify the active lifetime. -- Activation-local is the fresh-provider boundary, not a blanket `ServiceLifetime.Scoped` rule. A second scope from - one provider has fresh scoped application/module services but reuses provider singletons, options, codecs, and the - Client graph; it is not another activation and is not supported as a persistent-root hosting model. -- Public Client operations are synchronous. They do not retain Lua state across an `await`, and main-thread work enters - the SDK dispatcher as a bounded operation. -- The DI container owns disposal of services it created. Hosting owns the `ConfigurationManager` instance it created - and releases it once after the scope and provider; it never disposes resolved services individually. A disposable - application service has one owning registration; aliases must not make one disposable instance container-owned by - multiple descriptors. - -## Consequences and project value - -- Re-enable starts from a new composition rather than a partially disposed singleton graph. -- Reverse-order cleanup and rollback make module ownership explicit and make failure paths unit-testable without - mocking SDK statics. -- Clearing admission before cleanup prevents consumers from acquiring an activation while its resources are being - released. diff --git a/docs/adr/0003-package-and-aot-policy.md b/docs/adr/0003-package-and-aot-policy.md deleted file mode 100644 index 7b02a80..0000000 --- a/docs/adr/0003-package-and-aot-policy.md +++ /dev/null @@ -1,47 +0,0 @@ -# ADR 0003: Package and AOT policy - -- Status: Accepted -- Date: 2026-09-20 - -## Context - -The SDK's plugin entry-point generator and native Lua bridge are activated by a direct package reference in the plugin -project. Indirect NuGet dependencies do not provide a safe substitute for those build assets. At the same time, Native -AOT compatibility analysis can validate library dependencies without proving that Cheat Engine can load a Native AOT -plugin DLL. - -## Decision and why - -The Client baseline is version `0.1.0`, targets .NET 10 with C# 14, and enables trimming and AOT compatibility analysis -for shipping projects. Central package management pins `CheatEngine.SDK` to `1.0.0` in this repository; SDK-facing -published packages declare the compatible dependency range `[1.0.0, 2.0.0)`. - -The standalone template targets Windows x64 and references the Client, SDK, and JSON configuration provider directly: - -```xml - - - -``` - -The direct `CheatEngine.SDK` reference is deliberate. It activates the generated Cheat Engine plugin entry point and -copies the native Lua bridge. Generated plugin projects set `true`; -the `CheatEngine.Client.Hosting` build target then reports `CECLIENT001` if the project omits that direct SDK reference. - -## Delivery verification - -Windows CI restores in locked mode; builds and tests Release; packs with public API validation; smoke-tests isolated -package consumption and local template installation; and publishes then runs the `win-x64` Native AOT reference probe. -Live Cheat Engine checks are intentionally outside ordinary CI and remain explicit host-validation gates. - -## Consequences and project value - -- The maintained template is the source example for real package consumption, including the two direct references a - plugin needs outside this repository. -- A standalone generated plugin uses explicit local package versions. A future template using central package management - must bring its own `Directory.Packages.props`; it cannot inherit this repository's file. -- `CheatEngine.Client.AotProbe` validates the shipping graph under Native AOT analysis. It does not claim that Cheat - Engine can load a Native AOT plugin; supported deployment remains the managed SDK plugin output together with its - runtime configuration and native bridge. -- A dependency that requires reflection, runtime type discovery, dynamic code, or reflection-based JSON serialization - needs a trimming-safe alternative before entering a shipping Client project. diff --git a/docs/adr/0004-capability-matrix.md b/docs/adr/0004-capability-matrix.md deleted file mode 100644 index ff29f2d..0000000 --- a/docs/adr/0004-capability-matrix.md +++ /dev/null @@ -1,82 +0,0 @@ -# ADR 0004: Capability delivery matrix - -- Status: Accepted -- Date: 2026-09-20 - -## Context - -An interface alone does not establish that a Cheat Engine operation is safe to create, use, dispose, or repeat across -plugin activation. Source and package evidence must also be kept separate. The reviewed SDK source baseline -`aa3fcc3cdf629468e69d0c68817183d44a719894` contains -`CheatEngine.SDK.Engine.Scanning.Values.MemoryScanSessions.TryCreate`, whereas the released `CheatEngine.SDK` 1.0.0 -package consumed by Client does not expose that factory. The Client therefore cannot adopt `MemScan` and `FoundList` -instances from the released package without inventing an unverified handle-lifetime contract. - -| Evidence object | `MemoryScanSessions.TryCreate` | Client consequence | -|---|---|---| -| SDK source `aa3fcc3cdf629468e69d0c68817183d44a719894` | Present | A later SDK artifact may make an adoption path possible; source presence alone does not enable Client. | -| Released NuGet `CheatEngine.SDK` 1.0.0 | Absent | `IValueScanner` remains capability-gated. | - -The required Cheat Engine 7.7 x64 ownership and lifecycle qualification remains separate from both observations. - -## Decision and why - -The Client reports implementation status separately from public vocabulary. A capability is not represented as usable -only because an abstraction can describe it. - -### Status vocabulary - -- **Implemented**: the aggregate `ICheatEngineClient` composes an operational Client implementation for an enable - epoch. The listed host boundary still applies before claiming live-host qualification. -- **Capability-gated**: a public surface exists, but the implementation reports an unsupported capability instead of - assuming an unproven SDK ownership, affinity, or cancellation contract. -- **Deferred**: V1 intentionally provides no operational route through the aggregate Client. - -### Runtime evidence - -`ClientCapabilityAvailability.Evidence` records six independent gates for every Client capability: -implementation, the consumed package artifact, host observation, live qualification, activation policy, and -activation lifetime. The legacy `State` is only a projection of that evidence: it is `Available` when every gate is -satisfied, `Unavailable` when a gate is known to be missing, and `Unknown` when the remaining evidence is unknown, -faulted, or malformed. A faulted or malformed host observation is deliberately not reported as an unavailable host. - -An explicit `EnableUnsafeLuaExecution()` opt-in satisfies the policy gate only. It never establishes that the package -contains an implementation, that the active Cheat Engine host exposes the required globals, or that the operation has -been live-qualified. - -| Area | Public surface | Current source status | Boundary before live qualification | -|---|---|---|---| -| Plugin lifecycle and DI | `CheatEngineClientPlugin`, `CheatEnginePluginBuilder`, modules, options | Implemented | Exercise enable, rollback, disable, and repeated epoch activation in a CE host. | -| Runtime and capability facts | `ICheatEngineRuntime` | Implemented | Add evidence-backed observations per CE version and architecture as the SDK surface expands. | -| Process, module, and inspection | `IProcessClient`, `IInspectionClient` | Implemented | Map only SDK APIs with verified normal-return and thread contracts. | -| Typed memory | `IMemoryClient`, codecs, and requests | Implemented | Keep reads and writes bounded and classify host failures without leaking Lua state. | -| AOB scan | `IPatternScanner`, `AobScanRequest` | Implemented | Copy returned addresses and release temporary SDK owners in the same CE operation. | -| Value scan | `IValueScanner`, `IValueScanSession`, page records | Capability-gated | Require CE 7.7 x64 evidence for creation, first/next scan, read, ordered destruction, disable, and re-enable. | -| Address tables | `ITableClient`, copied record contracts | Implemented | Validate current-table lifetime, updates, and configured table-root enforcement. | -| Protected and unsafe Lua | `ILuaClient`, `IUnsafeLuaClient` | Protected operations implemented; arbitrary source is policy-gated | Keep arbitrary source opt-in and unavailable by default. | -| IPC or remote Client | None in V1 | Deferred | Define transport, authentication, handle epochs, and backpressure in a separate product decision. | - -## Consequences and project value - -- `IValueScanner.CreateSession` remains unavailable until the internal `createMemScan` and `createFoundList` owner has - passed creation, first and next scans, reading, ordered destruction, disable, and re-enable in Cheat Engine 7.7 x64. - The Client will not bypass the SDK restriction with reflection or a hand-rolled public owner. -- Templates and README examples can expose only guarded operations. They must not imply value scanning, arbitrary Lua, - 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 - -The template is the maintained source example. It compiles against the hosted plugin shape with explicit JSON -configuration and no reload watcher, demonstrates explicit module DI and validated options, and registers an -application-owned `ILuaModule` through `ILuaClient.RegisterModule`. Its activation-scoped lease owns unregistration of -the generated SDK Lua export. SDK binding calls remain inside that application module; no Lua state or SDK ownership -handle crosses the Client contract. - -The generated project takes an Address List snapshot and performs a guarded AOB and typed-memory probe. It performs no -operation when the required runtime precondition is absent, does not log target-memory contents, and retains the epoch, -ownership, and main-thread constraints defined by these ADRs. diff --git a/docs/adr/README.md b/docs/adr/README.md deleted file mode 100644 index 8f024d2..0000000 --- a/docs/adr/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Architecture Decision Records - -## Context - -These records define the architectural constraints for `CheatEngine.Client`: its public boundaries, plugin lifetime, -package layout, and delivered capability scope. They complement API documentation; they are not a product roadmap or a -substitute for the SDK contract. - -## Why this directory exists - -The Client sits above a host-bound SDK with thread-affinity and ownership rules. Recording the decisions keeps a -convenient public API from silently acquiring unsafe handles, cross-epoch state, indirect SDK build assets, or -unsupported host assumptions. - -## How the records improve the project - -Each ADR is a review boundary. A change that alters a listed decision must update the relevant record or add a new one, -so source code, package behavior, templates, and CI gates remain aligned. - -| Record | Decision | Primary effect | -|---|---|---| -| [0001](0001-layered-in-process-architecture.md) | Keep the Client in-process and isolate SDK domain mapping in Core. | Prevent host handles and transport concerns from leaking through functional Client APIs. | -| [0002](0002-plugin-activation-lifecycle.md) | Create one composition and Client scope for each plugin enable epoch. | Makes cleanup, module rollback, and epoch invalidation deterministic. | -| [0003](0003-package-and-aot-policy.md) | Require a direct SDK reference in plugin projects and distinguish AOT analysis from an AOT plugin binary. | Preserves SDK generators and native bridge assets at the plugin boundary. | -| [0004](0004-capability-matrix.md) | Report implemented, gated, and deferred capabilities separately. | Prevents contracts and templates from implying unverified Cheat Engine behavior. | - -## Delivery alignment - -The Windows CI workflow restores the locked graph, builds and tests Release, packs the public APIs, validates isolated -package and template consumption, then publishes and runs the Native AOT graph probe. Live Cheat Engine validation is -intentionally opt-in and remains a release boundary where an ADR identifies it; ordinary CI does not claim to replace -that host evidence. diff --git a/docs/engineering/ARCHIVE_RECONCILIATION.md b/docs/engineering/ARCHIVE_RECONCILIATION.md deleted file mode 100644 index 8bb6f6a..0000000 --- a/docs/engineering/ARCHIVE_RECONCILIATION.md +++ /dev/null @@ -1,52 +0,0 @@ -# Architecture-Review Archive Reconciliation - -## Purpose and evidence boundary - -This document makes the supplied `CheatEngineNet_Architecture_Review_2026-09-21.zip` usable as a bounded, reproducible planning input. Its machine-readable counterpart is [archive-reconciliation.json](archive-reconciliation.json), which the repository validator checks. - -The archive is context. It is not a source checkout, a restored NuGet package, a generated consumer, or a Cheat Engine live run. Consequently, a capability can be source-observed while still failing the package or live gate. For example, the reviewed SDK source at `aa3fcc3cdf629468e69d0c68817183d44a719894` contains the later `MemoryScanSessions.TryCreate` work, whereas published `CheatEngine.SDK` 1.0.0 does not; [ADR 0004](../adr/0004-capability-matrix.md) keeps Client publication gated accordingly. - -## Archive identity and inventory - -| Item | Verified value | -|---|---| -| Archive file | `CheatEngineNet_Architecture_Review_2026-09-21.zip` | -| Archive SHA-256 | `fc1f178916ec14fb993e405018112ddedd8dc316a86aa6fe9251b42c09547802` | -| Internal content manifest | `SHA256SUMS.json` | -| Internal manifest SHA-256 | `a81aa3e52362eab942a4e1d211502958d70cb240f263072d6e39b87920440680` | -| Findings register | 36 canonical IDs, `R01`–`R36` | -| Qualification plan | 80 scenarios, all specified rather than executed | -| Ownership matrix | 32 rows | -| Capability matrix | 17 rows | - -The archive was independently checked with its supplied `tools/validate_package.py --require-manifest` command and Python test suite. The committed validator checks the recorded identity and complete finding crosswalk; it intentionally does not expect the archive to be present at a particular local path. - -## Findings-to-Client crosswalk - -The following groups cover each canonical finding exactly once. They point to the stable Client plan IDs rather than claim that a historical issue, ADR, or test ID has been recreated. - -| Group | Findings | Client execution items | -|---|---|---| -| Evidence and delivery | R17, R18, R19, R31, R32, R35, R36 | CLI-001, CLI-002, CLI-003, CLI-022, CLI-023, CLI-024 | -| Immediate corrections | R02, R03, R04, R12, R21 | CLI-004, CLI-005, CLI-006, CLI-021 | -| SDK contracts | R01, R08, R09, R10, R16 | CLI-007, CLI-008, CLI-010, CLI-011 | -| Identity and owners | R05, R06, R07, R20, R27 | CLI-009, CLI-016, CLI-017 | -| Bounded workflows | R11, R13, R14, R22, R28, R29 | CLI-013, CLI-014, CLI-015, CLI-020 | -| Advanced qualification | R15, R23, R24, R25, R26, R30, R33, R34 | CLI-012, CLI-018, CLI-019, CLI-023, CLI-024 | - -## Ownership and qualification sequence - -The archive ownership matrix assigns CE mappings, ABI safety, Lua protection, and low-level native owners to the SDK. Client owns activation policy, DI composition, typed workflows, and fluent public ergonomics. Package compatibility and live qualification are shared: the SDK supplies a supportable artifact and the Client verifies its actual consumer and plugin-host behavior. - -The planning order follows the archive roadmap: establish package and host identity; make process/dispatcher/rollback deterministic; consume the SDK contracts; add the higher-level workflows; qualify retained-resource ownership; then validate callbacks, deployment, multi-plugin behavior, and performance. The [cross-repository sequence](CROSS_REPOSITORY_SEQUENCE.md) and work-item dependency graph express the actionable edges. - -## Source, package, fixture, and live evidence - -| Evidence level | What it can establish | What it cannot establish | -|---|---|---| -| Source | An exact reviewed commit contains a code path or contract. | That a released package contains it or that a host can use it. | -| Package | The exact nupkg and generated consumer expose the qualified contract. | That it works with the selected Cheat Engine host. | -| Fixture | The declared controlled fixture exercised the stated behavior. | Broader host/version compatibility. | -| Live | The named CE 7.7 host and artifact passed the recorded scenario. | A universal compatibility or security guarantee. | - -No level may be silently promoted. The 80 archive scenarios remain `Specified_Not_Executed` until their actual environment, command, result, and cleanup evidence are recorded. See [the validation protocol](VALIDATION_PROTOCOL.md) for the repository and live qualification gates. diff --git a/docs/engineering/CROSS_REPOSITORY_SEQUENCE.md b/docs/engineering/CROSS_REPOSITORY_SEQUENCE.md deleted file mode 100644 index 5077845..0000000 --- a/docs/engineering/CROSS_REPOSITORY_SEQUENCE.md +++ /dev/null @@ -1,29 +0,0 @@ -# Cross-Repository Execution Sequence - -## Start with independent reliability work - -Client expected process failures, codec-context expiration and complete activation rollback can be refined immediately. They must not wait for the entire low-level research roadmap. This initial lane includes CLI-004, CLI-005 and CLI-006, followed by composed regression tests. Preserve the production dispatcher's intentional treatment of programming exceptions. - -## Establish the reusable SDK contract - -SDK-007 supplies factual outcomes. SDK-008 centralizes built-in runtime/process/symbol/table semantics. SDK-010 and SDK-011 establish target authority and safe resource handoff. SDK-012 qualifies Lua universe/state behavior. SDK-009 supplies ownership-aware registration. SDK-021 supplies record commands. SDK-022 compiles combined generated consumers; SDK-023 records the minimum containing artifact. - -These dependencies are not a demand for a giant PR. Contract, implementation, tests and migration can be reviewable slices. Keep each primitive's evidence and first containing package explicit. Registering a name, disposing an owner or reading an address must not independently reinterpret CE inside Client. - -## Adopt instead of bypass - -CLI-007/008/009 consume the SDK semantic/outcome/target contracts. CLI-010 consumes SDK registration safety while preserving Client composition policy. Existing stable SDK value types are allowed deliberately; raw Lua state, borrowed CE objects and native owners are not normal Client DTOs. CLI-011 protects nested generic and generated signatures. - -## Qualify vertical workflows - -AOB outcomes and scan workload are SDK-014; cardinality and copied-result budgets are CLI-013. Source presence of MemoryScanSessions does not close SDK-015 qualification, and the Client adapter remains gated until CLI-016 is accepted. Allocation and patch handoff depend on target identity and exact cleanup semantics; CLI-017 proposes separate allocation and patch PRs under one coherent adoption issue. - -SDK-018 owns debugger continuation and borrowed callback state. CLI-018 exposes copied observation and qualified synchronous decision interfaces, not a second debugger. CLI-019 independently bounds pending consumers and defines disposal. Dropping telemetry cannot discard a required native continuation decision. - -## Artifact gates do not disappear after the first release - -SDK-023 is the minimum contract artifact. A later SDK-017 or SDK-018 implementation may require a later package. Every consuming Client issue must name a package that contains all its actual prerequisites. “SDK task closed” and “some SDK package exists” are insufficient. No release number is assigned before the artifact is published and its contents verified. - -## Delivery links - -The machine-readable dependency list is `docs/engineering/backlog.json`: each item has `blocked_by` and `blocks`, while `external_dependencies` records SDK-to-Client edges. The deployed issue bodies and Project membership are a receipt, not proof that every native parent/dependency edge or saved Project view has been re-read. Verify those GitHub-specific relations separately before marking a work item ready. diff --git a/docs/engineering/EVIDENCE_AND_LIMITS.md b/docs/engineering/EVIDENCE_AND_LIMITS.md deleted file mode 100644 index b268855..0000000 --- a/docs/engineering/EVIDENCE_AND_LIMITS.md +++ /dev/null @@ -1,36 +0,0 @@ -# Evidence, Provenance, and Deployment Receipt - -## Review inputs and limits - -The architecture-review archive supplied for this bootstrap was validated with its own SHA-256, JSON, Markdown-link, and Python-tool checks. Its identity, inventory, bounded finding crosswalk, and source/package/fixture/live boundary are recorded in [the archive reconciliation](ARCHIVE_RECONCILIATION.md). It provides the decision brief, findings, ownership/capability matrices, and qualification scenarios that shaped this plan. It is context rather than a source clone, a NuGet artifact, or live-host evidence: it does not establish a byte-for-byte import, exhaustive reconciliation of every historical finding, or a verified mapping to every original ADR/test ID. SDK-001 and CLI-001 explicitly retain that continuity work. New stable planning IDs are used instead of inventing original IDs. - -## Current repository observations - -| Repository | Main inspected | Relevant merged work | -|---|---|---| -| CheatEngine.SDK | `aa3fcc3cdf629468e69d0c68817183d44a719894` | PR #19: owned runtime primitives and generated Lua marshalling. | -| CheatEngine.Client | `923a4ded85898f53ef4cd2ff5872d2fd9001071a` | PR #7: fluent high-level APIs and generated Lua modules. | - -Both repositories reported no open issues/PRs at the initial metadata read; an organization issue search found no open issues in these two repositories. The SDK search returned eight historical issues, including closed NuGet/publication and documentation work. Those were not reopened. Main is protected. The metadata's user-level push/admin flags did not establish the active integration's effective write permissions. - -SDK CONTRIBUTING and AGENTS and the Client README were read in this bootstrap. Relevant .github/root metadata was inspected to preserve existing conventions. Most deep implementation references come from the prior review; they are marked `PriorAuditReference`, not misrepresented as newly executed or fully reread. The prior official CE comparison remains pinned to `ec45d5f47f92a239ba0bf51ec5d04a7509c3fd37`, not an inspected live CE 7.7 binary. - -## Deployment receipt as of September 21, 2026 - -The first preparation attempt received `Resource not accessible by integration`. That historical failure is not the final deployment state. A later operator-authorized import created and populated the private [CheatEngineNet — Engineering Execution Project](https://github.com/orgs/CheatEngineNet/projects/1): - -- the Client roadmap root is [#8](https://github.com/CheatEngineNet/CheatEngine.Client/issues/8), with epics [#9–#16](https://github.com/CheatEngineNet/CheatEngine.Client/issues/9) and leaves [#17–#40](https://github.com/CheatEngineNet/CheatEngine.Client/issues/17); -- the Project has the Client planning fields `CE Planning ID`, `CE Layer`, `CE Phase`, `CE Priority`, `CE Readiness`, `CE Start`, and `CE Target` in addition to GitHub built-ins; -- the root, epics, leaves, milestones, native hierarchy, and declared Client/SDK dependencies were read back after import. Their issue bodies contain stable `ce-bootstrap:` markers. - -This is a metadata receipt, not a product-validation result. Project views, workflow automation, and any subsequent human edits must be inspected in GitHub before a planning state is relied upon. - -## Tool coverage - -GitHub metadata is verified with authenticated repository tooling after the operator has authorized it. Repository files do not contain credentials, automation for Project mutation, or a claim that a local document can replace GitHub's current state. - -## Verification categories - -`ArchiveValidated`, `MetadataRead`, `PriorAuditReference`, `WebPrimary`, `Proposal`, `FixtureVerified` and `LiveVerified` are distinct. Planning records carry no FixtureVerified or LiveVerified status. Archive validation covers the archive package and its Python tests only; repository product validation is recorded separately with its exact commands and results. - -The archive is not a source clone. Existing implementation claims are credited, but qualification work must recheck exact source and actual containing package. Package version, source commit and assembly version remain separate identifiers. diff --git a/docs/engineering/GOVERNANCE.md b/docs/engineering/GOVERNANCE.md deleted file mode 100644 index 3e363cf..0000000 --- a/docs/engineering/GOVERNANCE.md +++ /dev/null @@ -1,39 +0,0 @@ -# Engineering Operating Model - -## Authority and source of truth - -The versioned roadmap records intent, architecture, acceptance evidence and proposed sequencing. GitHub issues record current execution, decisions and delivery links. Pull requests contain reviewable changes. The organization Project aggregates those same issues; it is not a second copy of their specifications. Repository milestones are outcome groups. Epic issues are parents, not monolithic implementation tasks. - -The bootstrap manifest is the reviewed **initial import specification**, not a bot that continuously overwrites human refinement. Reruns reuse stable markers and leave existing issue bodies, checked tasks, assignments, states and dates untouched. Refinements belong in the issue and subsequent documentation PRs. Regenerate a new reviewed import only for genuinely new scope. - -## Definition of Ready - -A leaf is ready when the owner layer is unambiguous; the source call path has been revalidated; the desired behavior, exclusions and acceptance criteria are understood; blocking contracts have reached the required state; the containing SDK artifact is known for Client consumption; and the maintainer has accepted the PR scope. A lack of incoming graph edges alone does not make an issue ready. - -Research may start before an implementation prerequisite is complete when its scope is discovery rather than mutation. Record that distinction; do not remove a real delivery blocker merely to make a board look unblocked. Artifact publication and live-host qualification are independent from source merge. - -## Definition of Done - -Require a focused reviewed PR, exact validation commands and results, updated contract/support documentation, compatibility notes, and evidence that cleanup, failures and relevant target/activation transitions work. For source-only or fixture-only delivery, retain the remaining live gate explicitly. A public interface, merged class, green documentation check or AOT executable publish does not establish live CE support. - -An epic closes only when each child is completed with evidence or explicitly deferred through an accepted decision. A deferred capability is not delivered. The roadmap root is a navigation and acceptance record, not an issue every child PR should close. - -## Branch and PR policy - -Create one branch for a coherent leaf or clearly documented slice, not one per epic and not all proposed branches in advance. Target main through a PR. Preserve SDK CONTRIBUTING and AGENTS: focused imperative commits, no required Conventional Commit prefixes, no Co-authored-by trailers, no LINQ in C# production changes, and existing build/style rules. This bootstrap makes no protection or release-workflow changes. - -A PR closes only the issue it actually completes. Cross-repository prerequisites are references and blockers, not collateral closing directives. The intended sequence is SDK contract PR, containing SDK package, Client adapter PR, then declared profile qualification. There is no automatic merge, version tag, release or package publication in the tooling. - -## Priority and planning - -P1 protects an affected reliability or compatibility promise. P2 is near-term alignment or capability delivery. P3 is optional/exploratory scope requiring deliberate prioritization. These are not security severity levels. No effort points, assignees, dates or delivery versions are invented. Dates should be added only after capacity and dependency commitments are agreed. - -## Refinement cadence and exceptions - -Review blocked work, package availability, live evidence and narrow PR scope during normal team refinement. Temporary Client mappings require a SDK owner, issue, replacement artifact and removal condition. Do not introduce an indefinite “Core may do anything” exception. Respect intentional raw SDK expert access without exposing raw state or owners through normal Client APIs. - -## Sources - -- [SDK contribution rules](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/CONTRIBUTING.md) -- [SDK repository guidelines](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/AGENTS.md) -- [Client architecture and live gates](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md) diff --git a/docs/engineering/PROJECT_CONFIGURATION.md b/docs/engineering/PROJECT_CONFIGURATION.md deleted file mode 100644 index 0f2ea87..0000000 --- a/docs/engineering/PROJECT_CONFIGURATION.md +++ /dev/null @@ -1,34 +0,0 @@ -# Organization Project Configuration - -## One shared execution Project - -The private organization [CheatEngineNet — Engineering Execution Project](https://github.com/orgs/CheatEngineNet/projects/1) contains the SDK and Client roadmap issues. Maintain separate SDK and Client table views rather than duplicating every issue into independently maintained planning boards. Each repository retains its own seven milestones and roadmap root. - -This repository deliberately does not ship a Project-mutating script or a second Project manifest. Project administration remains an explicit operator action: read the current metadata first, make a narrow GitHub change only when authorized, and retain the resulting issue/Project receipt. The local [`backlog.json`](backlog.json) is the reviewed source for Client planning IDs, fields, hierarchy, and dependency intent; it is not a remote-state synchronizer. - -## Fields - -Use built-in Status and repository/milestone data. Add CE Planning ID, CE Layer, CE Phase, CE Priority, CE Readiness, CE Start and CE Target. Readiness begins at Needs refinement; having no blocker does not automatically imply Ready. Contract, artifact and live gates are tracked explicitly in issues and reviewed before changing readiness. Dates remain empty until real capacity and sequencing commitments exist. - -## Saved views and remaining UI configuration - -| View | Layout | Selection | Purpose | -|---|---|---|---| -| SDK Backlog | Table | SDK repository | Group by milestone; show priority, readiness and dependencies. | -| Client Backlog | Table | Client repository | Show SDK blockers and exact package gates. | -| Cross-repository Board | Board | Open issues | Group by built-in Status; do not duplicate state into several label schemes. | -| Blocking Contracts | Table | Open issues | Display native dependency fields and separate artifact blockers. | -| Roadmap | Roadmap | Epic label | Use CE Start / CE Target only after dates are accepted. | - -The Project currently has the named CE fields. Grouping, sorting, roadmap date-field binding, saved-view configuration, and Project workflow automation remain explicit GitHub UI checks; they are not asserted by the local manifest validator. GitHub roadmap visualization requires date information, so an undated outcome plan must not manufacture dates just to fill a chart. - -## Operating guidance - -Keep completed issues in the Project unless the team approves archival behavior. Avoid automatic closure of epics or cross-repository issues on a child merge. Keep unsafe/unqualified capability work visible as deferred or gated, not silently removed. Do not add a new organization issue type globally just to represent an epic; the bootstrap uses ordinary issues, a dedicated label, and native hierarchy. - -## Sources - -- [Project creation](https://cli.github.com/manual/gh_project_create) -- [Field creation](https://cli.github.com/manual/gh_project_field-create) -- [Item editing](https://cli.github.com/manual/gh_project_item-edit) -- [Roadmap layout](https://docs.github.com/en/issues/planning-and-tracking-with-projects/customizing-views-in-your-project/customizing-the-roadmap-layout) diff --git a/docs/engineering/README.md b/docs/engineering/README.md deleted file mode 100644 index e588845..0000000 --- a/docs/engineering/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Engineering Reference - -Start with [the repository roadmap](../../ROADMAP.md) and the work-item specifications in `work-items/`. - -This documentation is the reviewed engineering baseline. GitHub issues and PR discussions become the execution record after deployment; the local manifest is not a continuous state-synchronization service and must not overwrite later human refinement. - -The package baseline is September 21, 2026. The original preparation session was denied GitHub write access, then an operator-authorized deployment created the recorded Project and issue set. The receipt is documented in [Evidence and limits](EVIDENCE_AND_LIMITS.md); documentation still does not prove native metadata or product qualification. - -Read [Governance](GOVERNANCE.md), [Evidence and limits](EVIDENCE_AND_LIMITS.md), [the archive reconciliation](ARCHIVE_RECONCILIATION.md), [Cross-repository sequencing](CROSS_REPOSITORY_SEQUENCE.md), and [Validation](VALIDATION_PROTOCOL.md). - -SDK owns integration facts, native safety, Lua protection and resource authority. Client owns developer workflows and policy. A contract in source, a published artifact and a live-tested capability are different evidence states. diff --git a/docs/engineering/VALIDATION_PROTOCOL.md b/docs/engineering/VALIDATION_PROTOCOL.md deleted file mode 100644 index ba3f523..0000000 --- a/docs/engineering/VALIDATION_PROTOCOL.md +++ /dev/null @@ -1,39 +0,0 @@ -# Validation and Release Protocol - -## Package validation versus product validation - -Run the repository-owned structural gate for the delivered Client manifest and work-item documents: - -```powershell -python eng/Validate-EngineeringManifest.py -python -m unittest discover -s eng/tests -v -``` - -The supplied architecture-review archive has its own independent integrity check; run it from the extracted archive root: - -```powershell -python tools/validate_package.py . --require-manifest -python -m unittest discover -s tools/tests -v -``` - -The repository validator also checks the committed archive-reconciliation inventory: the archive identity, 36 finding IDs, 80 specified scenarios, ownership summary, and references to known Client items. It does not read an archive from a machine-specific path. These checks validate documentation/data/tool integrity only. They do not establish that either .NET repository builds or that a Cheat Engine plugin works. Product validation uses the commands in the repository [README](../../README.md), and acceptance records begin with Not executed until their actual evidence is attached. - -## Source and fixture gates - -Recheck the exact call path before implementing an inherited audit observation. The process Try failure test must compose the production dispatcher behavior; a fake that catches a different exception cannot validate the seam. Runtime probes must distinguish missing global from a present throwing function. AOB must distinguish a qualified no-match shape from Lua/global/userdata failures. Retained codec contexts must fail before SDK entry after the invocation expires. - -Inject failures before native mutation, after native success, before owner publication, during each cleanup stage and after mutation before snapshot refresh. Report partial effects and uncertain cleanup; do not assume false means nothing happened. Two target fixtures must prove that old owners never release into a newly selected process. - -## Package and generated consumer gates - -Use shipped packages in clean consumers, not sibling project references. Record content hashes and bridge/generator identity. Test minimum and selected later SDK versions; the version range is not a claim that every possible intermediate package was tested. Compile generated code and inspect public nested types rather than relying only on emitted text or shallow reflection tests. - -## Live and performance gates - -Record host executable, Lua/bridge identity, OS/architecture, target profile and exact plugin artifacts. Include enable/failure/disable/re-enable, target changes and simultaneous plugins. A standalone AOT executable publish is not a native plugin load/unload test. Explicitly retain unavailable support when the live gate is missing. - -Benchmark direct SDK, immediate/queued Client, primitive/batch/codec/snapshot/AOB/event and cleanup paths separately. Record managed bytes, native/Lua measures when available, boundary-call counts and latency distributions. Preserve target, cancellation, outcome and ownership semantics. No performance values are supplied by this bootstrap. - -## GitHub deployment verification - -Read back each issue marker and milestone, all native parents and blocking edges, the two documentation branch trees and draft PRs. The optional Project verifier checks membership, configured fields and saved-view names/layouts/filters. Grouping, roadmap date-field binding and automation remain manual checks. Preserve the receipt when a run stops. A partial run is not transactionally rolled back or relabeled successful. diff --git a/docs/engineering/archive-reconciliation.json b/docs/engineering/archive-reconciliation.json deleted file mode 100644 index a3eb112..0000000 --- a/docs/engineering/archive-reconciliation.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "schema_version": 1, - "archive": { - "file_name": "CheatEngineNet_Architecture_Review_2026-09-21.zip", - "sha256": "fc1f178916ec14fb993e405018112ddedd8dc316a86aa6fe9251b42c09547802", - "content_manifest": "SHA256SUMS.json", - "content_manifest_sha256": "a81aa3e52362eab942a4e1d211502958d70cb240f263072d6e39b87920440680" - }, - "baselines": { - "client_source": "923a4ded85898f53ef4cd2ff5872d2fd9001071a", - "sdk_source": "aa3fcc3cdf629468e69d0c68817183d44a719894", - "sdk_published_package": "1.0.0", - "target_profile": "Cheat Engine 7.7, Windows x64" - }, - "counts": { - "findings": 36, - "specified_product_scenarios": 80, - "ownership_rows": 32, - "capability_rows": 17 - }, - "evidence_levels": [ - "source", - "package", - "fixture", - "live" - ], - "ownership_summary": [ - { - "area": "Cheat Engine mappings, native ABI safety, Lua protection, and low-level native owners", - "owner": "SDK", - "client_boundary": "Client consumes only supported SDK contracts; it does not reproduce ABI, longjmp, bootstrap, or native-owner mechanics." - }, - { - "area": "Activation workflow, DI composition, policy, and public fluent ergonomics", - "owner": "Client", - "client_boundary": "Client owns application-facing lifecycle policy and expresses SDK-backed workflows without leaking SDK handles." - }, - { - "area": "Package compatibility, generated-consumer proof, and plugin-host qualification", - "owner": "Shared", - "client_boundary": "SDK supplies the package contract; Client validates its consumer graph. A source observation is not package or live proof." - }, - { - "area": "Fixture and live Cheat Engine qualification", - "owner": "Shared", - "client_boundary": "Client scenarios run only after the corresponding SDK source and package prerequisites are established." - } - ], - "finding_groups": [ - { - "id": "evidence-and-delivery", - "description": "Provenance, package identity, generated-consumer, deployment, and capability evidence are planned through the evidence and qualification leaves.", - "findings": ["R17", "R18", "R19", "R31", "R32", "R35", "R36"], - "client_items": ["CLI-001", "CLI-002", "CLI-003", "CLI-022", "CLI-023", "CLI-024"] - }, - { - "id": "immediate-corrections", - "description": "Activation ownership, deterministic rollback, dispatcher behavior, and runtime capability checks are sequenced before high-level workflows.", - "findings": ["R02", "R03", "R04", "R12", "R21"], - "client_items": ["CLI-004", "CLI-005", "CLI-006", "CLI-021"] - }, - { - "id": "sdk-contracts", - "description": "Client-facing processes, runtime diagnostics, failure mapping, and public contracts remain gated on their upstream SDK contracts.", - "findings": ["R01", "R08", "R09", "R10", "R16"], - "client_items": ["CLI-007", "CLI-008", "CLI-010", "CLI-011"] - }, - { - "id": "identity-and-owners", - "description": "Target identity changes, retained resources, scanning, and patch ownership are bounded as Client workflows over SDK-native ownership.", - "findings": ["R05", "R06", "R07", "R20", "R27"], - "client_items": ["CLI-009", "CLI-016", "CLI-017"] - }, - { - "id": "bounded-workflows", - "description": "Memory codecs, AOB/value scan behavior, tables, configuration policy, and destructive-operation bounds remain explicit consumer responsibilities.", - "findings": ["R11", "R13", "R14", "R22", "R28", "R29"], - "client_items": ["CLI-013", "CLI-014", "CLI-015", "CLI-020"] - }, - { - "id": "advanced-qualification", - "description": "Callbacks, reentrancy, generated modules, AOT, performance, plugin coexistence, and registration are qualified only at their dedicated later gates.", - "findings": ["R15", "R23", "R24", "R25", "R26", "R30", "R33", "R34"], - "client_items": ["CLI-012", "CLI-018", "CLI-019", "CLI-023", "CLI-024"] - } - ], - "not_proof": [ - "The reconciliation does not attest that the archive is a source clone or that its source observations appear in a published package.", - "The reconciliation does not mark any of the 80 scenarios as fixture- or live-executed.", - "The reconciliation does not replace the controlled Cheat Engine 7.7 live qualification gate." - ] -} diff --git a/docs/engineering/backlog.json b/docs/engineering/backlog.json deleted file mode 100644 index e28a5d8..0000000 --- a/docs/engineering/backlog.json +++ /dev/null @@ -1,2381 +0,0 @@ -{ - "schema_version": 1, - "bootstrap_id": "ce-engineering-2026-09-21", - "repository": { - "key": "client", - "repository": "CheatEngineNet/CheatEngine.Client", - "baseline": "923a4ded85898f53ef4cd2ff5872d2fd9001071a", - "default_branch": "main", - "bootstrap_branch": "chore/engineering-bootstrap-2026-09-21", - "docs_root": "docs/engineering", - "prefix": "CLI" - }, - "items": [ - { - "id": "CLI-PLAN", - "repository_key": "client", - "kind": "roadmap", - "title": "Establish the Client developer-workflow roadmap and execution baseline", - "parent": null, - "epic": null, - "milestone": null, - "priority": "P1", - "observation": "This is the navigation root for the engineering bootstrap, not an implemented feature or a GitHub Project object.", - "objective": "Connect outcome milestones, owned epics, implementable issues, source evidence, package gates and focused PRs.", - "requirements": [ - "Keep one owning repository per implementation task.", - "Keep hierarchy, blocking dependencies, package gates and release qualification distinct.", - "Never close this root automatically from one child implementation PR." - ], - "acceptance": [ - "Every work item links to its owning epic and verified sources.", - "Native GitHub relationships and deployment status are recorded accurately.", - "No speculative due dates, assignees, release versions or live-verification results are assigned." - ], - "tests": [ - "Manifest integrity, hierarchy and cross-repository dependency validation." - ], - "risks": "An attractive board cannot substitute for precise contracts, acceptance evidence, and maintainable issue scopes.", - "blocked_by": [], - "sources": [ - "S01", - "C01", - "G01", - "G02", - "G03", - "G04" - ], - "branch": null, - "pr_title": null, - "scope_exclusions": [ - "No automatic implementation branch farm, auto-merge, release or protection change." - ], - "completion_artifacts": [ - "Navigable roadmap, detailed backlog and verified deployment receipt." - ], - "mandatory": "Governance root; close only when the defined roadmap scope is accepted or deliberately superseded.", - "evidence": "MetadataRead + Proposal: the initial GitHub preparation attempt was denied (Resource not accessible by integration). A later operator-authorized import created and populated the private CheatEngineNet — Engineering Execution Project #1 and created Client issues #8–#40. This is a planning-metadata receipt only; it does not establish implementation, package, fixture, or live-host qualification.", - "children": [ - "CLI-E01", - "CLI-E02", - "CLI-E03", - "CLI-E04", - "CLI-E05", - "CLI-E06", - "CLI-E07", - "CLI-E08" - ], - "blocks": [], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan.", - "labels": [ - "ce:bootstrap", - "ce:kind:roadmap", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-PLAN.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Connect outcome milestones, owned epics, implementable issues, source evidence, package gates and focused PRs.", - "body_template": "\n\n## CLI-PLAN — Establish the Client developer-workflow roadmap and execution baseline\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** roadmap · **Priority:** P1 · **Milestone:** Cross-phase navigation\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nThis is the navigation root for the engineering bootstrap, not an implemented feature or a GitHub Project object.\n\n**Evidence classification:** `MetadataRead` + `Proposal`: the initial GitHub preparation attempt was denied (`Resource not accessible by integration`). A later operator-authorized import created and populated the private CheatEngineNet — Engineering Execution Project #1 and created Client issues #8–#40. This is a planning-metadata receipt only; it does not establish implementation, package, fixture, or live-host qualification.\n\n### Outcome and rationale\n\nConnect outcome milestones, owned epics, implementable issues, source evidence, package gates and focused PRs.\n\n**Expected benefit:** Connect outcome milestones, owned epics, implementable issues, source evidence, package gates and focused PRs.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Keep one owning repository per implementation task.\n- [ ] Keep hierarchy, blocking dependencies, package gates and release qualification distinct.\n- [ ] Never close this root automatically from one child implementation PR.\n\n### Acceptance criteria\n\n- [ ] Every work item links to its owning epic and verified sources.\n- [ ] Native GitHub relationships and deployment status are recorded accurately.\n- [ ] No speculative due dates, assignees, release versions or live-verification results are assigned.\n\n### Required validation\n\n- [ ] Manifest integrity, hierarchy and cross-repository dependency validation.\n\n### Scope exclusions\n\n- No automatic implementation branch farm, auto-merge, release or protection change.\n\n### Parent and children\n\nRepository roadmap root; not a GitHub Projects object.\n\n- {{issue:CLI-E01}}\n- {{issue:CLI-E02}}\n- {{issue:CLI-E03}}\n- {{issue:CLI-E04}}\n- {{issue:CLI-E05}}\n- {{issue:CLI-E06}}\n- {{issue:CLI-E07}}\n- {{issue:CLI-E08}}\n\n### Blocked by\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Blocks\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Package and readiness gate\n\nIdentify the first containing artifact before describing new source as consumable support. No version is invented by this plan.\n\n### Proposed branch and PR\n\nNo epic-sized implementation branch. Children have focused branch/PR proposals. The bootstrap creates only one documentation/tooling branch and draft PR per repository.\n\n### Risks and compatibility\n\nAn attractive board cannot substitute for precise contracts, acceptance evidence, and maintainable issue scopes.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Navigable roadmap, detailed backlog and verified deployment receipt.\n\nGovernance root; close only when the defined roadmap scope is accepted or deliberately superseded.\n\n### Sources\n\n- [S01 — Existing build/test, focused branches and release rules; preserve rather than overwrite.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/CONTRIBUTING.md)\n- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md)\n- [G01 — Issue API; filter pull_request entries from issue lists.](https://docs.github.com/en/rest/issues/issues)\n- [G02 — Repository-specific milestones.](https://docs.github.com/en/rest/issues/milestones)\n- [G03 — Native hierarchy uses issue IDs, not issue numbers.](https://docs.github.com/en/rest/issues/sub-issues)\n- [G04 — Native blocked-by relationship with the blocker issue_id.](https://docs.github.com/en/rest/issues/issue-dependencies)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-E01", - "repository_key": "client", - "kind": "epic", - "title": "Governance and capability truth", - "parent": "CLI-PLAN", - "epic": null, - "milestone": "CLI-M0", - "priority": "P1", - "observation": "This grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing.", - "objective": "Maintain the developer-facing support model and evidence-linked execution plan.", - "requirements": [ - "Review child source evidence and preserve the SDK/Client responsibility boundary.", - "Sequence only real dependencies; do not block a child on closing its own parent.", - "Require an explicit decision and evidence for any deferred capability." - ], - "acceptance": [ - "Each child is completed with evidence or explicitly deferred by an approved decision.", - "Cross-repository artifact gates have named versions and hashes when adopted.", - "Compatibility and support documentation match actual delivery, not interface count." - ], - "tests": [ - "Review the acceptance evidence of each child; do not count parent closure as an additional runtime test." - ], - "risks": "A grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child.", - "blocked_by": [], - "sources": [ - "C01", - "C19", - "H02", - "H01", - "C09", - "C14", - "G01", - "G02", - "G03", - "G04" - ], - "branch": null, - "pr_title": null, - "scope_exclusions": [ - "No monolithic epic implementation PR." - ], - "completion_artifacts": [ - "Linked child PRs, tests, and a final scope/deferral review." - ], - "mandatory": "Outcome group; optional scope remains explicitly gated or deferred.", - "evidence": "Proposal derived from the attached summaries and pinned source references.", - "children": [ - "CLI-001", - "CLI-002", - "CLI-003" - ], - "blocks": [], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan.", - "labels": [ - "ce:bootstrap", - "ce:kind:epic", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-E01.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Maintain the developer-facing support model and evidence-linked execution plan.", - "body_template": "\n\n## CLI-E01 — Governance and capability truth\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** epic · **Priority:** P1 · **Milestone:** CLI-M0\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nThis grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing.\n\n**Evidence classification:** Proposal derived from the attached summaries and pinned source references.\n\n### Outcome and rationale\n\nMaintain the developer-facing support model and evidence-linked execution plan.\n\n**Expected benefit:** Maintain the developer-facing support model and evidence-linked execution plan.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Review child source evidence and preserve the SDK/Client responsibility boundary.\n- [ ] Sequence only real dependencies; do not block a child on closing its own parent.\n- [ ] Require an explicit decision and evidence for any deferred capability.\n\n### Acceptance criteria\n\n- [ ] Each child is completed with evidence or explicitly deferred by an approved decision.\n- [ ] Cross-repository artifact gates have named versions and hashes when adopted.\n- [ ] Compatibility and support documentation match actual delivery, not interface count.\n\n### Required validation\n\n- [ ] Review the acceptance evidence of each child; do not count parent closure as an additional runtime test.\n\n### Scope exclusions\n\n- No monolithic epic implementation PR.\n\n### Parent and children\n\nParent: {{issue:CLI-PLAN}}\n\n- {{issue:CLI-001}}\n- {{issue:CLI-002}}\n- {{issue:CLI-003}}\n\n### Blocked by\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Blocks\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Package and readiness gate\n\nIdentify the first containing artifact before describing new source as consumable support. No version is invented by this plan.\n\n### Proposed branch and PR\n\nNo epic-sized implementation branch. Children have focused branch/PR proposals. The bootstrap creates only one documentation/tooling branch and draft PR per repository.\n\n### Risks and compatibility\n\nA grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Linked child PRs, tests, and a final scope/deferral review.\n\nOutcome group; optional scope remains explicitly gated or deferred.\n\n### Sources\n\n- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md)\n- [C19 — Prior audit: SDK 1.0.0 resolution; artifact bytes not independently inspected.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/packages.lock.json)\n- [H02 — Merged high-level API/modules; preserve this work.](https://github.com/CheatEngineNet/CheatEngine.Client/pull/7)\n- [H01 — Merged foundations; author-reported tests are not independently executed.](https://github.com/CheatEngineNet/CheatEngine.SDK/pull/19)\n- [C09 — Prior audit: capability and exception-family mismatch.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/RuntimeClient.cs)\n- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs)\n- [G01 — Issue API; filter pull_request entries from issue lists.](https://docs.github.com/en/rest/issues/issues)\n- [G02 — Repository-specific milestones.](https://docs.github.com/en/rest/issues/milestones)\n- [G03 — Native hierarchy uses issue IDs, not issue numbers.](https://docs.github.com/en/rest/issues/sub-issues)\n- [G04 — Native blocked-by relationship with the blocker issue_id.](https://docs.github.com/en/rest/issues/issue-dependencies)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-E02", - "repository_key": "client", - "kind": "epic", - "title": "Immediate reliability corrections", - "parent": "CLI-PLAN", - "epic": null, - "milestone": "CLI-M1", - "priority": "P1", - "observation": "This grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing.", - "objective": "Repair classified failures, retained contexts, and partial activation cleanup independently of broad SDK expansion.", - "requirements": [ - "Review child source evidence and preserve the SDK/Client responsibility boundary.", - "Sequence only real dependencies; do not block a child on closing its own parent.", - "Require an explicit decision and evidence for any deferred capability." - ], - "acceptance": [ - "Each child is completed with evidence or explicitly deferred by an approved decision.", - "Cross-repository artifact gates have named versions and hashes when adopted.", - "Compatibility and support documentation match actual delivery, not interface count." - ], - "tests": [ - "Review the acceptance evidence of each child; do not count parent closure as an additional runtime test." - ], - "risks": "A grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child.", - "blocked_by": [], - "sources": [ - "C05", - "C06", - "C03", - "C04", - "C07", - "C14" - ], - "branch": null, - "pr_title": null, - "scope_exclusions": [ - "No monolithic epic implementation PR." - ], - "completion_artifacts": [ - "Linked child PRs, tests, and a final scope/deferral review." - ], - "mandatory": "Outcome group; optional scope remains explicitly gated or deferred.", - "evidence": "Proposal derived from the attached summaries and pinned source references.", - "children": [ - "CLI-004", - "CLI-005", - "CLI-006" - ], - "blocks": [], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan.", - "labels": [ - "ce:bootstrap", - "ce:kind:epic", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-E02.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Repair classified failures, retained contexts, and partial activation cleanup independently of broad SDK expansion.", - "body_template": "\n\n## CLI-E02 — Immediate reliability corrections\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** epic · **Priority:** P1 · **Milestone:** CLI-M1\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nThis grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing.\n\n**Evidence classification:** Proposal derived from the attached summaries and pinned source references.\n\n### Outcome and rationale\n\nRepair classified failures, retained contexts, and partial activation cleanup independently of broad SDK expansion.\n\n**Expected benefit:** Repair classified failures, retained contexts, and partial activation cleanup independently of broad SDK expansion.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Review child source evidence and preserve the SDK/Client responsibility boundary.\n- [ ] Sequence only real dependencies; do not block a child on closing its own parent.\n- [ ] Require an explicit decision and evidence for any deferred capability.\n\n### Acceptance criteria\n\n- [ ] Each child is completed with evidence or explicitly deferred by an approved decision.\n- [ ] Cross-repository artifact gates have named versions and hashes when adopted.\n- [ ] Compatibility and support documentation match actual delivery, not interface count.\n\n### Required validation\n\n- [ ] Review the acceptance evidence of each child; do not count parent closure as an additional runtime test.\n\n### Scope exclusions\n\n- No monolithic epic implementation PR.\n\n### Parent and children\n\nParent: {{issue:CLI-PLAN}}\n\n- {{issue:CLI-004}}\n- {{issue:CLI-005}}\n- {{issue:CLI-006}}\n\n### Blocked by\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Blocks\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Package and readiness gate\n\nIdentify the first containing artifact before describing new source as consumable support. No version is invented by this plan.\n\n### Proposed branch and PR\n\nNo epic-sized implementation branch. Children have focused branch/PR proposals. The bootstrap creates only one documentation/tooling branch and draft PR per repository.\n\n### Risks and compatibility\n\nA grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Linked child PRs, tests, and a final scope/deferral review.\n\nOutcome group; optional scope remains explicitly gated or deferred.\n\n### Sources\n\n- [C05 — Prior audit: production adapter rethrows callback exceptions.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs)\n- [C06 — Prior audit: expected no-target failure and local target observations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/ProcessClient.cs)\n- [C03 — Prior audit: scoped codecs, partial batches and failure projection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/MemoryClient.cs)\n- [C04 — Public extension context with an implementation-enforced lifetime.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Abstractions/Memory/IMemoryReadContext.cs)\n- [C07 — Prior audit: early construction cleanup differs from normal cleanup.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Hosting/CheatEngineClientPlugin.cs)\n- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-E03", - "repository_key": "client", - "kind": "epic", - "title": "SDK contract adoption", - "parent": "CLI-PLAN", - "epic": null, - "milestone": "CLI-M2", - "priority": "P1", - "observation": "This grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing.", - "objective": "Replace duplicated built-in CE interpretation with the actual containing SDK artifacts.", - "requirements": [ - "Review child source evidence and preserve the SDK/Client responsibility boundary.", - "Sequence only real dependencies; do not block a child on closing its own parent.", - "Require an explicit decision and evidence for any deferred capability." - ], - "acceptance": [ - "Each child is completed with evidence or explicitly deferred by an approved decision.", - "Cross-repository artifact gates have named versions and hashes when adopted.", - "Compatibility and support documentation match actual delivery, not interface count." - ], - "tests": [ - "Review the acceptance evidence of each child; do not count parent closure as an additional runtime test." - ], - "risks": "A grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child.", - "blocked_by": [], - "sources": [ - "C02", - "C09", - "C19", - "C03", - "C13", - "C11", - "N03", - "C08", - "C06", - "C05" - ], - "branch": null, - "pr_title": null, - "scope_exclusions": [ - "No monolithic epic implementation PR." - ], - "completion_artifacts": [ - "Linked child PRs, tests, and a final scope/deferral review." - ], - "mandatory": "Outcome group; optional scope remains explicitly gated or deferred.", - "evidence": "Proposal derived from the attached summaries and pinned source references.", - "children": [ - "CLI-007", - "CLI-008", - "CLI-009" - ], - "blocks": [], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan.", - "labels": [ - "ce:bootstrap", - "ce:kind:epic", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-E03.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Replace duplicated built-in CE interpretation with the actual containing SDK artifacts.", - "body_template": "\n\n## CLI-E03 — SDK contract adoption\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** epic · **Priority:** P1 · **Milestone:** CLI-M2\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nThis grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing.\n\n**Evidence classification:** Proposal derived from the attached summaries and pinned source references.\n\n### Outcome and rationale\n\nReplace duplicated built-in CE interpretation with the actual containing SDK artifacts.\n\n**Expected benefit:** Replace duplicated built-in CE interpretation with the actual containing SDK artifacts.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Review child source evidence and preserve the SDK/Client responsibility boundary.\n- [ ] Sequence only real dependencies; do not block a child on closing its own parent.\n- [ ] Require an explicit decision and evidence for any deferred capability.\n\n### Acceptance criteria\n\n- [ ] Each child is completed with evidence or explicitly deferred by an approved decision.\n- [ ] Cross-repository artifact gates have named versions and hashes when adopted.\n- [ ] Compatibility and support documentation match actual delivery, not interface count.\n\n### Required validation\n\n- [ ] Review the acceptance evidence of each child; do not count parent closure as an additional runtime test.\n\n### Scope exclusions\n\n- No monolithic epic implementation PR.\n\n### Parent and children\n\nParent: {{issue:CLI-PLAN}}\n\n- {{issue:CLI-007}}\n- {{issue:CLI-008}}\n- {{issue:CLI-009}}\n\n### Blocked by\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Blocks\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Package and readiness gate\n\nIdentify the first containing artifact before describing new source as consumable support. No version is invented by this plan.\n\n### Proposed branch and PR\n\nNo epic-sized implementation branch. Children have focused branch/PR proposals. The bootstrap creates only one documentation/tooling branch and draft PR per repository.\n\n### Risks and compatibility\n\nA grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Linked child PRs, tests, and a final scope/deferral review.\n\nOutcome group; optional scope remains explicitly gated or deferred.\n\n### Sources\n\n- [C02 — Prior audit: built-in CE Lua declarations remain Client-owned.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Infrastructure/ClientLuaGlobals.cs)\n- [C09 — Prior audit: capability and exception-family mismatch.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/RuntimeClient.cs)\n- [C19 — Prior audit: SDK 1.0.0 resolution; artifact bytes not independently inspected.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/packages.lock.json)\n- [C03 — Prior audit: scoped codecs, partial batches and failure projection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/MemoryClient.cs)\n- [C13 — Prior audit: operation/registration generation and recursive type checks.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/source-generators/CheatEngine.Client.SourceGenerators.Lua/CheatEngineLuaGenerator.cs)\n- [C11 — Prior audit: SDK delegation but ambiguous false becomes rejection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/SdkAobScanPort.cs)\n- [N03 — Source/binary/behavioral compatibility distinctions.](https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/breaking-changes)\n- [C08 — Prior audit: local observed epoch is not authoritative target identity.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Infrastructure/TargetSelectionLifetime.cs)\n- [C06 — Prior audit: expected no-target failure and local target observations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/ProcessClient.cs)\n- [C05 — Prior audit: production adapter rethrows callback exceptions.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-E04", - "repository_key": "client", - "kind": "epic", - "title": "Modules, public boundaries, and activation composition", - "parent": "CLI-PLAN", - "epic": null, - "milestone": "CLI-M3", - "priority": "P1", - "observation": "This grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing.", - "objective": "Keep application extensibility safe without leaking raw SDK state or duplicating registration mechanisms.", - "requirements": [ - "Review child source evidence and preserve the SDK/Client responsibility boundary.", - "Sequence only real dependencies; do not block a child on closing its own parent.", - "Require an explicit decision and evidence for any deferred capability." - ], - "acceptance": [ - "Each child is completed with evidence or explicitly deferred by an approved decision.", - "Cross-repository artifact gates have named versions and hashes when adopted.", - "Compatibility and support documentation match actual delivery, not interface count." - ], - "tests": [ - "Review the acceptance evidence of each child; do not count parent closure as an additional runtime test." - ], - "risks": "A grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child.", - "blocked_by": [], - "sources": [ - "C13", - "S06", - "C07", - "C20", - "N03", - "C14", - "S08" - ], - "branch": null, - "pr_title": null, - "scope_exclusions": [ - "No monolithic epic implementation PR." - ], - "completion_artifacts": [ - "Linked child PRs, tests, and a final scope/deferral review." - ], - "mandatory": "Outcome group; optional scope remains explicitly gated or deferred.", - "evidence": "Proposal derived from the attached summaries and pinned source references.", - "children": [ - "CLI-010", - "CLI-011", - "CLI-012" - ], - "blocks": [], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan.", - "labels": [ - "ce:bootstrap", - "ce:kind:epic", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-E04.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Keep application extensibility safe without leaking raw SDK state or duplicating registration mechanisms.", - "body_template": "\n\n## CLI-E04 — Modules, public boundaries, and activation composition\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** epic · **Priority:** P1 · **Milestone:** CLI-M3\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nThis grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing.\n\n**Evidence classification:** Proposal derived from the attached summaries and pinned source references.\n\n### Outcome and rationale\n\nKeep application extensibility safe without leaking raw SDK state or duplicating registration mechanisms.\n\n**Expected benefit:** Keep application extensibility safe without leaking raw SDK state or duplicating registration mechanisms.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Review child source evidence and preserve the SDK/Client responsibility boundary.\n- [ ] Sequence only real dependencies; do not block a child on closing its own parent.\n- [ ] Require an explicit decision and evidence for any deferred capability.\n\n### Acceptance criteria\n\n- [ ] Each child is completed with evidence or explicitly deferred by an approved decision.\n- [ ] Cross-repository artifact gates have named versions and hashes when adopted.\n- [ ] Compatibility and support documentation match actual delivery, not interface count.\n\n### Required validation\n\n- [ ] Review the acceptance evidence of each child; do not count parent closure as an additional runtime test.\n\n### Scope exclusions\n\n- No monolithic epic implementation PR.\n\n### Parent and children\n\nParent: {{issue:CLI-PLAN}}\n\n- {{issue:CLI-010}}\n- {{issue:CLI-011}}\n- {{issue:CLI-012}}\n\n### Blocked by\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Blocks\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Package and readiness gate\n\nIdentify the first containing artifact before describing new source as consumable support. No version is invented by this plan.\n\n### Proposed branch and PR\n\nNo epic-sized implementation branch. Children have focused branch/PR proposals. The bootstrap creates only one documentation/tooling branch and draft PR per repository.\n\n### Risks and compatibility\n\nA grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Linked child PRs, tests, and a final scope/deferral review.\n\nOutcome group; optional scope remains explicitly gated or deferred.\n\n### Sources\n\n- [C13 — Prior audit: operation/registration generation and recursive type checks.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/source-generators/CheatEngine.Client.SourceGenerators.Lua/CheatEngineLuaGenerator.cs)\n- [S06 — Prior audit: generation stamps but name-based unregister.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaEmit/LuaRegistrationEmitter.cs)\n- [C07 — Prior audit: early construction cleanup differs from normal cleanup.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Hosting/CheatEngineClientPlugin.cs)\n- [C20 — Prior audit: approved SDK values versus broad compile/runtime dependency.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Abstractions/CheatEngine.Client.Abstractions.csproj)\n- [N03 — Source/binary/behavioral compatibility distinctions.](https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/breaking-changes)\n- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs)\n- [S08 — Prior audit: static host state and runtime admission.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/libs/CheatEngine.SDK.Hosting/Bootstrap/PluginHost.cs)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-E05", - "repository_key": "client", - "kind": "epic", - "title": "Memory, scan, and table workflows", - "parent": "CLI-PLAN", - "epic": null, - "milestone": "CLI-M4", - "priority": "P1", - "observation": "This grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing.", - "objective": "Preserve cardinality, partial effects, limits and target consistency across friendly APIs.", - "requirements": [ - "Review child source evidence and preserve the SDK/Client responsibility boundary.", - "Sequence only real dependencies; do not block a child on closing its own parent.", - "Require an explicit decision and evidence for any deferred capability." - ], - "acceptance": [ - "Each child is completed with evidence or explicitly deferred by an approved decision.", - "Cross-repository artifact gates have named versions and hashes when adopted.", - "Compatibility and support documentation match actual delivery, not interface count." - ], - "tests": [ - "Review the acceptance evidence of each child; do not count parent closure as an additional runtime test." - ], - "risks": "A grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child.", - "blocked_by": [], - "sources": [ - "C10", - "C11", - "C16", - "C03", - "C05", - "C01", - "C12", - "C18", - "C02" - ], - "branch": null, - "pr_title": null, - "scope_exclusions": [ - "No monolithic epic implementation PR." - ], - "completion_artifacts": [ - "Linked child PRs, tests, and a final scope/deferral review." - ], - "mandatory": "Outcome group; optional scope remains explicitly gated or deferred.", - "evidence": "Proposal derived from the attached summaries and pinned source references.", - "children": [ - "CLI-013", - "CLI-014", - "CLI-015" - ], - "blocks": [], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan.", - "labels": [ - "ce:bootstrap", - "ce:kind:epic", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-E05.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Preserve cardinality, partial effects, limits and target consistency across friendly APIs.", - "body_template": "\n\n## CLI-E05 — Memory, scan, and table workflows\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** epic · **Priority:** P1 · **Milestone:** CLI-M4\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nThis grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing.\n\n**Evidence classification:** Proposal derived from the attached summaries and pinned source references.\n\n### Outcome and rationale\n\nPreserve cardinality, partial effects, limits and target consistency across friendly APIs.\n\n**Expected benefit:** Preserve cardinality, partial effects, limits and target consistency across friendly APIs.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Review child source evidence and preserve the SDK/Client responsibility boundary.\n- [ ] Sequence only real dependencies; do not block a child on closing its own parent.\n- [ ] Require an explicit decision and evidence for any deferred capability.\n\n### Acceptance criteria\n\n- [ ] Each child is completed with evidence or explicitly deferred by an approved decision.\n- [ ] Cross-repository artifact gates have named versions and hashes when adopted.\n- [ ] Compatibility and support documentation match actual delivery, not interface count.\n\n### Required validation\n\n- [ ] Review the acceptance evidence of each child; do not count parent closure as an additional runtime test.\n\n### Scope exclusions\n\n- No monolithic epic implementation PR.\n\n### Parent and children\n\nParent: {{issue:CLI-PLAN}}\n\n- {{issue:CLI-013}}\n- {{issue:CLI-014}}\n- {{issue:CLI-015}}\n\n### Blocked by\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Blocks\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Package and readiness gate\n\nIdentify the first containing artifact before describing new source as consumable support. No version is invented by this plan.\n\n### Proposed branch and PR\n\nNo epic-sized implementation branch. Children have focused branch/PR proposals. The bootstrap creates only one documentation/tooling branch and draft PR per repository.\n\n### Risks and compatibility\n\nA grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Linked child PRs, tests, and a final scope/deferral review.\n\nOutcome group; optional scope remains explicitly gated or deferred.\n\n### Sources\n\n- [C10 — Prior audit: whole scan precedes module lookup and post-filtering.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/PatternScanner.cs)\n- [C11 — Prior audit: SDK delegation but ambiguous false becomes rejection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/SdkAobScanPort.cs)\n- [C16 — Prior audit: immutable terminals, post-filter limits, readability naming.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Fluent/Scanning/AobScanBuilder.cs)\n- [C03 — Prior audit: scoped codecs, partial batches and failure projection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/MemoryClient.cs)\n- [C05 — Prior audit: production adapter rethrows callback exceptions.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs)\n- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md)\n- [C12 — Prior audit: Parent/destroy semantics and mutation/snapshot ambiguity.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/SdkTableRecordMutationPort.cs)\n- [C18 — Prior audit: table-root allowlist and documented path TOCTOU limitation.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Infrastructure/CoreClientPolicy.cs)\n- [C02 — Prior audit: built-in CE Lua declarations remain Client-owned.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Infrastructure/ClientLuaGlobals.cs)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-E06", - "repository_key": "client", - "kind": "epic", - "title": "Qualified advanced workflow adoption", - "parent": "CLI-PLAN", - "epic": null, - "milestone": "CLI-M5", - "priority": "P1", - "observation": "This grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing.", - "objective": "Enable scan, allocation, patch and event workflows only after their specific lower-layer gates.", - "requirements": [ - "Review child source evidence and preserve the SDK/Client responsibility boundary.", - "Sequence only real dependencies; do not block a child on closing its own parent.", - "Require an explicit decision and evidence for any deferred capability." - ], - "acceptance": [ - "Each child is completed with evidence or explicitly deferred by an approved decision.", - "Cross-repository artifact gates have named versions and hashes when adopted.", - "Compatibility and support documentation match actual delivery, not interface count." - ], - "tests": [ - "Review the acceptance evidence of each child; do not count parent closure as an additional runtime test." - ], - "risks": "A grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child.", - "blocked_by": [], - "sources": [ - "S03", - "C14", - "C21", - "S04", - "S05", - "C15", - "U01" - ], - "branch": null, - "pr_title": null, - "scope_exclusions": [ - "No monolithic epic implementation PR." - ], - "completion_artifacts": [ - "Linked child PRs, tests, and a final scope/deferral review." - ], - "mandatory": "Outcome group; optional scope remains explicitly gated or deferred.", - "evidence": "Proposal derived from the attached summaries and pinned source references.", - "children": [ - "CLI-016", - "CLI-017", - "CLI-018" - ], - "blocks": [], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan.", - "labels": [ - "ce:bootstrap", - "ce:kind:epic", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-E06.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Enable scan, allocation, patch and event workflows only after their specific lower-layer gates.", - "body_template": "\n\n## CLI-E06 — Qualified advanced workflow adoption\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** epic · **Priority:** P1 · **Milestone:** CLI-M5\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nThis grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing.\n\n**Evidence classification:** Proposal derived from the attached summaries and pinned source references.\n\n### Outcome and rationale\n\nEnable scan, allocation, patch and event workflows only after their specific lower-layer gates.\n\n**Expected benefit:** Enable scan, allocation, patch and event workflows only after their specific lower-layer gates.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Review child source evidence and preserve the SDK/Client responsibility boundary.\n- [ ] Sequence only real dependencies; do not block a child on closing its own parent.\n- [ ] Require an explicit decision and evidence for any deferred capability.\n\n### Acceptance criteria\n\n- [ ] Each child is completed with evidence or explicitly deferred by an approved decision.\n- [ ] Cross-repository artifact gates have named versions and hashes when adopted.\n- [ ] Compatibility and support documentation match actual delivery, not interface count.\n\n### Required validation\n\n- [ ] Review the acceptance evidence of each child; do not count parent closure as an additional runtime test.\n\n### Scope exclusions\n\n- No monolithic epic implementation PR.\n\n### Parent and children\n\nParent: {{issue:CLI-PLAN}}\n\n- {{issue:CLI-016}}\n- {{issue:CLI-017}}\n- {{issue:CLI-018}}\n\n### Blocked by\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Blocks\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Package and readiness gate\n\nIdentify the first containing artifact before describing new source as consumable support. No version is invented by this plan.\n\n### Proposed branch and PR\n\nNo epic-sized implementation branch. Children have focused branch/PR proposals. The bootstrap creates only one documentation/tooling branch and draft PR per repository.\n\n### Risks and compatibility\n\nA grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Linked child PRs, tests, and a final scope/deferral review.\n\nOutcome group; optional scope remains explicitly gated or deferred.\n\n### Sources\n\n- [S03 — Prior audit: new factory already exists; qualify, do not recreate.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanSessions.cs)\n- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs)\n- [C21 — Live gate protocol identified by current README; not independently re-read here.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/docs/live-capability-gates.md)\n- [S04 — Prior audit: current-target allocation bindings.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/libs/CheatEngine.SDK.Engine/Allocation/LuaTargetMemoryAllocationOperations.cs)\n- [S05 — Prior audit: patch and disable-info ownership handoff.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/libs/CheatEngine.SDK.Engine/Assembly/AutoAssemblerPatcher.cs)\n- [C15 — Prior audit: bounded event ring, pending reader list and async continuations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/Events/BoundedEventStream.cs)\n- [U01 — Historical loader/tables/callback dispatch comparison.](https://github.com/cheat-engine/cheat-engine/blob/ec45d5f47f92a239ba0bf51ec5d04a7509c3fd37/Cheat%20Engine/plugin.pas)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-E07", - "repository_key": "client", - "kind": "epic", - "title": "Developer experience and bounded observation", - "parent": "CLI-PLAN", - "epic": null, - "milestone": "CLI-M5", - "priority": "P2", - "observation": "This grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing.", - "objective": "Define coherent lifetime, reader, completion and discoverability policies.", - "requirements": [ - "Review child source evidence and preserve the SDK/Client responsibility boundary.", - "Sequence only real dependencies; do not block a child on closing its own parent.", - "Require an explicit decision and evidence for any deferred capability." - ], - "acceptance": [ - "Each child is completed with evidence or explicitly deferred by an approved decision.", - "Cross-repository artifact gates have named versions and hashes when adopted.", - "Compatibility and support documentation match actual delivery, not interface count." - ], - "tests": [ - "Review the acceptance evidence of each child; do not count parent closure as an additional runtime test." - ], - "risks": "A grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child.", - "blocked_by": [], - "sources": [ - "C15", - "C01", - "C16", - "N03", - "C06", - "C14" - ], - "branch": null, - "pr_title": null, - "scope_exclusions": [ - "No monolithic epic implementation PR." - ], - "completion_artifacts": [ - "Linked child PRs, tests, and a final scope/deferral review." - ], - "mandatory": "Outcome group; optional scope remains explicitly gated or deferred.", - "evidence": "Proposal derived from the attached summaries and pinned source references.", - "children": [ - "CLI-019", - "CLI-020", - "CLI-021" - ], - "blocks": [], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan.", - "labels": [ - "ce:bootstrap", - "ce:kind:epic", - "ce:priority:P2" - ], - "path": "docs/engineering/work-items/CLI-E07.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Define coherent lifetime, reader, completion and discoverability policies.", - "body_template": "\n\n## CLI-E07 — Developer experience and bounded observation\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** epic · **Priority:** P2 · **Milestone:** CLI-M5\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nThis grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing.\n\n**Evidence classification:** Proposal derived from the attached summaries and pinned source references.\n\n### Outcome and rationale\n\nDefine coherent lifetime, reader, completion and discoverability policies.\n\n**Expected benefit:** Define coherent lifetime, reader, completion and discoverability policies.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Review child source evidence and preserve the SDK/Client responsibility boundary.\n- [ ] Sequence only real dependencies; do not block a child on closing its own parent.\n- [ ] Require an explicit decision and evidence for any deferred capability.\n\n### Acceptance criteria\n\n- [ ] Each child is completed with evidence or explicitly deferred by an approved decision.\n- [ ] Cross-repository artifact gates have named versions and hashes when adopted.\n- [ ] Compatibility and support documentation match actual delivery, not interface count.\n\n### Required validation\n\n- [ ] Review the acceptance evidence of each child; do not count parent closure as an additional runtime test.\n\n### Scope exclusions\n\n- No monolithic epic implementation PR.\n\n### Parent and children\n\nParent: {{issue:CLI-PLAN}}\n\n- {{issue:CLI-019}}\n- {{issue:CLI-020}}\n- {{issue:CLI-021}}\n\n### Blocked by\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Blocks\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Package and readiness gate\n\nIdentify the first containing artifact before describing new source as consumable support. No version is invented by this plan.\n\n### Proposed branch and PR\n\nNo epic-sized implementation branch. Children have focused branch/PR proposals. The bootstrap creates only one documentation/tooling branch and draft PR per repository.\n\n### Risks and compatibility\n\nA grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Linked child PRs, tests, and a final scope/deferral review.\n\nOutcome group; optional scope remains explicitly gated or deferred.\n\n### Sources\n\n- [C15 — Prior audit: bounded event ring, pending reader list and async continuations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/Events/BoundedEventStream.cs)\n- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md)\n- [C16 — Prior audit: immutable terminals, post-filter limits, readability naming.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Fluent/Scanning/AobScanBuilder.cs)\n- [N03 — Source/binary/behavioral compatibility distinctions.](https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/breaking-changes)\n- [C06 — Prior audit: expected no-target failure and local target observations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/ProcessClient.cs)\n- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-E08", - "repository_key": "client", - "kind": "epic", - "title": "Package, deployment, and release readiness", - "parent": "CLI-PLAN", - "epic": null, - "milestone": "CLI-M6", - "priority": "P1", - "observation": "This grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing.", - "objective": "Validate actual packages, external consumers, deployment sets and measured end-to-end behavior.", - "requirements": [ - "Review child source evidence and preserve the SDK/Client responsibility boundary.", - "Sequence only real dependencies; do not block a child on closing its own parent.", - "Require an explicit decision and evidence for any deferred capability." - ], - "acceptance": [ - "Each child is completed with evidence or explicitly deferred by an approved decision.", - "Cross-repository artifact gates have named versions and hashes when adopted.", - "Compatibility and support documentation match actual delivery, not interface count." - ], - "tests": [ - "Review the acceptance evidence of each child; do not count parent closure as an additional runtime test." - ], - "risks": "A grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child.", - "blocked_by": [], - "sources": [ - "C19", - "C20", - "C01", - "N03", - "C17", - "S11", - "C21", - "C03", - "C15", - "S13" - ], - "branch": null, - "pr_title": null, - "scope_exclusions": [ - "No monolithic epic implementation PR." - ], - "completion_artifacts": [ - "Linked child PRs, tests, and a final scope/deferral review." - ], - "mandatory": "Outcome group; optional scope remains explicitly gated or deferred.", - "evidence": "Proposal derived from the attached summaries and pinned source references.", - "children": [ - "CLI-022", - "CLI-023", - "CLI-024" - ], - "blocks": [], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan.", - "labels": [ - "ce:bootstrap", - "ce:kind:epic", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-E08.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Validate actual packages, external consumers, deployment sets and measured end-to-end behavior.", - "body_template": "\n\n## CLI-E08 — Package, deployment, and release readiness\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** epic · **Priority:** P1 · **Milestone:** CLI-M6\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nThis grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing.\n\n**Evidence classification:** Proposal derived from the attached summaries and pinned source references.\n\n### Outcome and rationale\n\nValidate actual packages, external consumers, deployment sets and measured end-to-end behavior.\n\n**Expected benefit:** Validate actual packages, external consumers, deployment sets and measured end-to-end behavior.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Review child source evidence and preserve the SDK/Client responsibility boundary.\n- [ ] Sequence only real dependencies; do not block a child on closing its own parent.\n- [ ] Require an explicit decision and evidence for any deferred capability.\n\n### Acceptance criteria\n\n- [ ] Each child is completed with evidence or explicitly deferred by an approved decision.\n- [ ] Cross-repository artifact gates have named versions and hashes when adopted.\n- [ ] Compatibility and support documentation match actual delivery, not interface count.\n\n### Required validation\n\n- [ ] Review the acceptance evidence of each child; do not count parent closure as an additional runtime test.\n\n### Scope exclusions\n\n- No monolithic epic implementation PR.\n\n### Parent and children\n\nParent: {{issue:CLI-PLAN}}\n\n- {{issue:CLI-022}}\n- {{issue:CLI-023}}\n- {{issue:CLI-024}}\n\n### Blocked by\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Blocks\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Package and readiness gate\n\nIdentify the first containing artifact before describing new source as consumable support. No version is invented by this plan.\n\n### Proposed branch and PR\n\nNo epic-sized implementation branch. Children have focused branch/PR proposals. The bootstrap creates only one documentation/tooling branch and draft PR per repository.\n\n### Risks and compatibility\n\nA grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Linked child PRs, tests, and a final scope/deferral review.\n\nOutcome group; optional scope remains explicitly gated or deferred.\n\n### Sources\n\n- [C19 — Prior audit: SDK 1.0.0 resolution; artifact bytes not independently inspected.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/packages.lock.json)\n- [C20 — Prior audit: approved SDK values versus broad compile/runtime dependency.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Abstractions/CheatEngine.Client.Abstractions.csproj)\n- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md)\n- [N03 — Source/binary/behavioral compatibility distinctions.](https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/breaking-changes)\n- [C17 — Prior audit: build-only native calls, external-base checks and per-file deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Hosting/buildTransitive/CheatEngine.Client.Hosting.targets)\n- [S11 — Protected native boundary and bridge contract reference.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/native/cheatengine-sdk-lua-bridge/README.md)\n- [C21 — Live gate protocol identified by current README; not independently re-read here.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/docs/live-capability-gates.md)\n- [C03 — Prior audit: scoped codecs, partial batches and failure projection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/MemoryClient.cs)\n- [C15 — Prior audit: bounded event ring, pending reader list and async continuations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/Events/BoundedEventStream.cs)\n- [S13 — Live fixture entry point, identified in contribution guidance.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/tests/CheatEngine.SDK.LivePlugin/README.md)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-001", - "repository_key": "client", - "kind": "governance", - "title": "Reconcile Client audit evidence with source and consumed packages", - "parent": "CLI-E01", - "epic": "CLI-E01", - "milestone": "CLI-M0", - "priority": "P1", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "The reviewed archive is validated planning context, not source/package/live proof; existing high-level APIs from PR #7 must not be recreated as missing.", - "objective": "Establish Client-specific provenance and a truthful theme-to-work-item crosswalk.", - "requirements": [ - "Record Client/SDK commits, the actually consumed nupkg and source-only additions separately.", - "Import original finding/ADR/test identifiers only from recovered original registers.", - "Retain unavailable advanced implementations until their individual gates pass." - ], - "acceptance": [ - "No invented original finding ID or assertion of exhaustive archive coverage.", - "Each Client adoption item identifies its corresponding SDK work item.", - "Historical source statements are revalidated before changes." - ], - "tests": [ - "Ledger rejects source present as proof of packaged/live support.", - "Cross-repository reference validator catches unresolved planning IDs." - ], - "risks": "The summaries are a usable planning basis, not a substitute for all detailed audit registers.", - "blocked_by": [], - "sources": [ - "C01", - "C19", - "H02", - "H01" - ], - "branch": "docs/cli-001-evidence-reconciliation", - "pr_title": "Reconcile Client audit evidence with source and consumed packages", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [ - "CLI-002" - ], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan.", - "labels": [ - "ce:bootstrap", - "ce:kind:governance", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-001.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Establish Client-specific provenance and a truthful theme-to-work-item crosswalk.", - "body_template": "\n\n## CLI-001 — Reconcile Client audit evidence with source and consumed packages\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** governance · **Priority:** P1 · **Milestone:** CLI-M0\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nThe Client main revision matches the prior review. The prior complete ZIP is not locally available, and existing high-level APIs from PR #7 must not be recreated as missing.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nEstablish Client-specific provenance and a truthful theme-to-work-item crosswalk.\n\n**Expected benefit:** Establish Client-specific provenance and a truthful theme-to-work-item crosswalk.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Record Client/SDK commits, the actually consumed nupkg and source-only additions separately.\n- [ ] Import original finding/ADR/test identifiers only from recovered original registers.\n- [ ] Retain unavailable advanced implementations until their individual gates pass.\n\n### Acceptance criteria\n\n- [ ] No invented original finding ID or assertion of exhaustive archive coverage.\n- [ ] Each Client adoption item identifies its corresponding SDK work item.\n- [ ] Historical source statements are revalidated before changes.\n\n### Required validation\n\n- [ ] Ledger rejects source present as proof of packaged/live support.\n- [ ] Cross-repository reference validator catches unresolved planning IDs.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E01}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Blocks\n\n- {{issue:CLI-002}}\n\n### Package and readiness gate\n\nIdentify the first containing artifact before describing new source as consumable support. No version is invented by this plan.\n\n### Proposed branch and PR\n\nBranch: `docs/cli-001-evidence-reconciliation`\n\nPR title: **Reconcile Client audit evidence with source and consumed packages**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nThe summaries are a usable planning basis, not a substitute for all detailed audit registers.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md)\n- [C19 — Prior audit: SDK 1.0.0 resolution; artifact bytes not independently inspected.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/packages.lock.json)\n- [H02 — Merged high-level API/modules; preserve this work.](https://github.com/CheatEngineNet/CheatEngine.Client/pull/7)\n- [H01 — Merged foundations; author-reported tests are not independently executed.](https://github.com/CheatEngineNet/CheatEngine.SDK/pull/19)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-002", - "repository_key": "client", - "kind": "feature", - "title": "Separate capability implementation, host evidence, and policy", - "parent": "CLI-E01", - "epic": "CLI-E01", - "milestone": "CLI-M0", - "priority": "P1", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "Runtime capability probes and unavailable service registrations describe different support dimensions. Opting into unsafe Lua is policy, not proof of working host support.", - "objective": "Expose a small immutable capability model with explicit reasons and independently sourced dimensions.", - "requirements": [ - "Distinguish implemented, available in package, observed in host, live-qualified, policy-allowed and active-lifetime prerequisites.", - "Do not treat every probe exception as unavailability; preserve unavailable versus present-but-failing.", - "Generate documentation from the same ledger without auto-promoting discovered symbols." - ], - "acceptance": [ - "Every unavailable public domain explains its actual missing gate.", - "Enabling an option cannot manufacture host evidence.", - "Existing supported workflows remain discoverable without raw SDK details." - ], - "tests": [ - "Missing, throwing and malformed runtime global cases.", - "Policy combinations and unavailable-adapter status tests." - ], - "risks": "Avoid a large generic capability framework; deterministic records and reasoned derivation are sufficient.", - "blocked_by": [ - "CLI-001" - ], - "sources": [ - "C09", - "C14", - "C01" - ], - "branch": "feat/cli-002-capability-evidence-model", - "pr_title": "Separate capability implementation, host evidence, and policy", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [ - "CLI-016", - "CLI-020" - ], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan.", - "labels": [ - "ce:bootstrap", - "ce:kind:feature", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-002.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Expose a small immutable capability model with explicit reasons and independently sourced dimensions.", - "body_template": "\n\n## CLI-002 — Separate capability implementation, host evidence, and policy\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P1 · **Milestone:** CLI-M0\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nRuntime capability probes and unavailable service registrations describe different support dimensions. Opting into unsafe Lua is policy, not proof of working host support.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nExpose a small immutable capability model with explicit reasons and independently sourced dimensions.\n\n**Expected benefit:** Expose a small immutable capability model with explicit reasons and independently sourced dimensions.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Distinguish implemented, available in package, observed in host, live-qualified, policy-allowed and active-lifetime prerequisites.\n- [ ] Do not treat every probe exception as unavailability; preserve unavailable versus present-but-failing.\n- [ ] Generate documentation from the same ledger without auto-promoting discovered symbols.\n\n### Acceptance criteria\n\n- [ ] Every unavailable public domain explains its actual missing gate.\n- [ ] Enabling an option cannot manufacture host evidence.\n- [ ] Existing supported workflows remain discoverable without raw SDK details.\n\n### Required validation\n\n- [ ] Missing, throwing and malformed runtime global cases.\n- [ ] Policy combinations and unavailable-adapter status tests.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E01}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\n- {{issue:CLI-001}}\n\n### Blocks\n\n- {{issue:CLI-016}}\n- {{issue:CLI-020}}\n\n### Package and readiness gate\n\nIdentify the first containing artifact before describing new source as consumable support. No version is invented by this plan.\n\n### Proposed branch and PR\n\nBranch: `feat/cli-002-capability-evidence-model`\n\nPR title: **Separate capability implementation, host evidence, and policy**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nAvoid a large generic capability framework; deterministic records and reasoned derivation are sufficient.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C09 — Prior audit: capability and exception-family mismatch.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/RuntimeClient.cs)\n- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs)\n- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-003", - "repository_key": "client", - "kind": "governance", - "title": "Adopt Client governance, roadmap, and issue-to-PR workflow", - "parent": "CLI-E01", - "epic": "CLI-E01", - "milestone": "CLI-M0", - "priority": "P1", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "Client has a mature README and existing ADR/build/live gates. This bootstrap must add management structure, not replace architecture or release policy.", - "objective": "Adopt additive planning documents, native issue hierarchy, blockers and review templates.", - "requirements": [ - "Keep Issues as current execution state and Markdown as scope/architecture authority.", - "Use focused branches from main only after readiness; no empty implementation branches for the entire backlog.", - "Keep cross-repository blockers explicit and verify actual GitHub metadata separately from local plans." - ], - "acceptance": [ - "The documentation/tooling PR contains no product runtime changes.", - "Roadmap, milestone, epic and task links are navigable after deployment.", - "No automated merge, publication, reviewer assignment or branch protection change." - ], - "tests": [ - "Manifest, DAG, source and local link checks.", - "Two-run deployment idempotency in a test repository." - ], - "risks": "A PR is not a deployment of native project metadata, and a Markdown checklist is not a native sub-issue relation.", - "blocked_by": [], - "sources": [ - "C01", - "G01", - "G02", - "G03", - "G04" - ], - "branch": "docs/cli-003-engineering-governance", - "pr_title": "Adopt Client governance, roadmap, and issue-to-PR workflow", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan.", - "labels": [ - "ce:bootstrap", - "ce:kind:governance", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-003.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Adopt additive planning documents, native issue hierarchy, blockers and review templates.", - "body_template": "\n\n## CLI-003 — Adopt Client governance, roadmap, and issue-to-PR workflow\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** governance · **Priority:** P1 · **Milestone:** CLI-M0\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nClient has a mature README and existing ADR/build/live gates. This bootstrap must add management structure, not replace architecture or release policy.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nAdopt additive planning documents, native issue hierarchy, blockers and review templates.\n\n**Expected benefit:** Adopt additive planning documents, native issue hierarchy, blockers and review templates.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Keep Issues as current execution state and Markdown as scope/architecture authority.\n- [ ] Use focused branches from main only after readiness; no empty implementation branches for the entire backlog.\n- [ ] Keep cross-repository blockers explicit and verify actual GitHub metadata separately from local plans.\n\n### Acceptance criteria\n\n- [ ] The documentation/tooling PR contains no product runtime changes.\n- [ ] Roadmap, milestone, epic and task links are navigable after deployment.\n- [ ] No automated merge, publication, reviewer assignment or branch protection change.\n\n### Required validation\n\n- [ ] Manifest, DAG, source and local link checks.\n- [ ] Two-run deployment idempotency in a test repository.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E01}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Blocks\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Package and readiness gate\n\nIdentify the first containing artifact before describing new source as consumable support. No version is invented by this plan.\n\n### Proposed branch and PR\n\nBranch: `docs/cli-003-engineering-governance`\n\nPR title: **Adopt Client governance, roadmap, and issue-to-PR workflow**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nA PR is not a deployment of native project metadata, and a Markdown checklist is not a native sub-issue relation.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md)\n- [G01 — Issue API; filter pull_request entries from issue lists.](https://docs.github.com/en/rest/issues/issues)\n- [G02 — Repository-specific milestones.](https://docs.github.com/en/rest/issues/milestones)\n- [G03 — Native hierarchy uses issue IDs, not issue numbers.](https://docs.github.com/en/rest/issues/sub-issues)\n- [G04 — Native blocked-by relationship with the blocker issue_id.](https://docs.github.com/en/rest/issues/issue-dependencies)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-004", - "repository_key": "client", - "kind": "bug", - "title": "Align expected process failures with production dispatcher semantics", - "parent": "CLI-E02", - "epic": "CLI-E02", - "milestone": "CLI-M1", - "priority": "P1", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "The prior audit traces expected no-target exceptions through a dispatcher that rethrows callback exceptions; a test fake can instead return false and mask the mismatch.", - "objective": "Return expected domain failures as typed outcomes without swallowing application callback bugs.", - "requirements": [ - "Classify no target, disappeared process and missing local metadata inside ProcessClient.", - "Keep the general dispatcher policy for programmer/user callback exceptions explicit.", - "Make contract fakes reproduce production exception transport rather than an easier imagined adapter." - ], - "acceptance": [ - "TryGetCurrent with PID zero returns the documented TargetNotAttached outcome.", - "An arbitrary user Action exception still follows the dispatcher contract.", - "Race-to-exit lookup failures have stable, documented semantics." - ], - "tests": [ - "Production-semantics dispatcher plus controlled process host.", - "Composed regression for no-target, disappeared target and throwing user callback." - ], - "risks": "Changing every dispatcher exception into false would hide defects and break a different contract.", - "blocked_by": [], - "sources": [ - "C05", - "C06" - ], - "branch": "fix/cli-004-process-try-semantics", - "pr_title": "Align expected process failures with production dispatcher semantics", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan.", - "labels": [ - "ce:bootstrap", - "ce:kind:bug", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-004.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Return expected domain failures as typed outcomes without swallowing application callback bugs.", - "body_template": "\n\n## CLI-004 — Align expected process failures with production dispatcher semantics\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** bug · **Priority:** P1 · **Milestone:** CLI-M1\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nThe prior audit traces expected no-target exceptions through a dispatcher that rethrows callback exceptions; a test fake can instead return false and mask the mismatch.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nReturn expected domain failures as typed outcomes without swallowing application callback bugs.\n\n**Expected benefit:** Return expected domain failures as typed outcomes without swallowing application callback bugs.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Classify no target, disappeared process and missing local metadata inside ProcessClient.\n- [ ] Keep the general dispatcher policy for programmer/user callback exceptions explicit.\n- [ ] Make contract fakes reproduce production exception transport rather than an easier imagined adapter.\n\n### Acceptance criteria\n\n- [ ] TryGetCurrent with PID zero returns the documented TargetNotAttached outcome.\n- [ ] An arbitrary user Action exception still follows the dispatcher contract.\n- [ ] Race-to-exit lookup failures have stable, documented semantics.\n\n### Required validation\n\n- [ ] Production-semantics dispatcher plus controlled process host.\n- [ ] Composed regression for no-target, disappeared target and throwing user callback.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E02}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Blocks\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Package and readiness gate\n\nIdentify the first containing artifact before describing new source as consumable support. No version is invented by this plan.\n\n### Proposed branch and PR\n\nBranch: `fix/cli-004-process-try-semantics`\n\nPR title: **Align expected process failures with production dispatcher semantics**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nChanging every dispatcher exception into false would hide defects and break a different contract.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C05 — Prior audit: production adapter rethrows callback exceptions.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs)\n- [C06 — Prior audit: expected no-target failure and local target observations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/ProcessClient.cs)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-005", - "repository_key": "client", - "kind": "bug", - "title": "Expire memory-codec contexts after each invocation", - "parent": "CLI-E02", - "epic": "CLI-E02", - "milestone": "CLI-M1", - "priority": "P1", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "The prior audit describes a retainable reference context whose methods call SDK memory directly without an invocation expiration guard; pointer width is cached in that context.", - "objective": "Make custom-codec access valid only inside its original invocation.", - "requirements": [ - "Expire in finally on successful and failing codec calls.", - "Validate active invocation, activation identity and allowed thread before any SDK operation, including scoped metadata access.", - "Use current SDK facts now and strengthen target binding when the authoritative target contract is adopted; do not delay the local expiration fix." - ], - "acceptance": [ - "Retained references fail before invoking SDK after return or exception.", - "Cross-thread and re-enable reuse cannot use a recycled invocation.", - "Do not pool contexts while an old reference could reach a later invocation." - ], - "tests": [ - "Capture public context and invoke afterward with SDK call counters.", - "PointerSize after expiration, codec exception, worker use and re-enable." - ], - "risks": "A ref-struct-only public redesign would be a separate compatibility decision; a small expiration guard can close the immediate lifetime gap.", - "blocked_by": [], - "sources": [ - "C03", - "C04", - "C07" - ], - "branch": "fix/cli-005-codec-context-lifetime", - "pr_title": "Expire memory-codec contexts after each invocation", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [ - "CLI-014" - ], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan.", - "labels": [ - "ce:bootstrap", - "ce:kind:bug", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-005.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Make custom-codec access valid only inside its original invocation.", - "body_template": "\n\n## CLI-005 — Expire memory-codec contexts after each invocation\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** bug · **Priority:** P1 · **Milestone:** CLI-M1\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nThe prior audit describes a retainable reference context whose methods call SDK memory directly without an invocation expiration guard; pointer width is cached in that context.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nMake custom-codec access valid only inside its original invocation.\n\n**Expected benefit:** Make custom-codec access valid only inside its original invocation.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Expire in finally on successful and failing codec calls.\n- [ ] Validate active invocation, activation identity and allowed thread before any SDK operation, including scoped metadata access.\n- [ ] Use current SDK facts now and strengthen target binding when the authoritative target contract is adopted; do not delay the local expiration fix.\n\n### Acceptance criteria\n\n- [ ] Retained references fail before invoking SDK after return or exception.\n- [ ] Cross-thread and re-enable reuse cannot use a recycled invocation.\n- [ ] Do not pool contexts while an old reference could reach a later invocation.\n\n### Required validation\n\n- [ ] Capture public context and invoke afterward with SDK call counters.\n- [ ] PointerSize after expiration, codec exception, worker use and re-enable.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E02}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Blocks\n\n- {{issue:CLI-014}}\n\n### Package and readiness gate\n\nIdentify the first containing artifact before describing new source as consumable support. No version is invented by this plan.\n\n### Proposed branch and PR\n\nBranch: `fix/cli-005-codec-context-lifetime`\n\nPR title: **Expire memory-codec contexts after each invocation**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nA ref-struct-only public redesign would be a separate compatibility decision; a small expiration guard can close the immediate lifetime gap.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C03 — Prior audit: scoped codecs, partial batches and failure projection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/MemoryClient.cs)\n- [C04 — Public extension context with an implementation-enforced lifetime.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Abstractions/Memory/IMemoryReadContext.cs)\n- [C07 — Prior audit: early construction cleanup differs from normal cleanup.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Hosting/CheatEngineClientPlugin.cs)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-006", - "repository_key": "client", - "kind": "bug", - "title": "Complete all activation rollback stages and preserve original errors", - "parent": "CLI-E02", - "epic": "CLI-E02", - "milestone": "CLI-M1", - "priority": "P1", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "Prior review observes sequential unprotected scope/provider/configuration cleanup in partial CreateActivation, while normal cleanup already aggregates failures.", - "objective": "Use a consistent all-stages cleanup policy for partially and fully constructed activations.", - "requirements": [ - "Attempt each owned cleanup stage independently and exactly as ownership permits.", - "Preserve original construction failure before secondary cleanup diagnostics.", - "Keep CE resource/module teardown before SDK detach; avoid double DI ownership." - ], - "acceptance": [ - "A throwing scope Dispose does not skip provider/configuration cleanup.", - "The original enable failure remains identifiable.", - "Normal disable order is preserved and repeated teardown is controlled." - ], - "tests": [ - "Failure during service resolution plus failing scope/provider/configuration disposables.", - "Module-enable rollback and re-enable after failed activation." - ], - "risks": "Disposal aggregation is not permission to destroy a shared service twice or detach before admitted work drains.", - "blocked_by": [], - "sources": [ - "C07", - "C14" - ], - "branch": "fix/cli-006-activation-rollback", - "pr_title": "Complete all activation rollback stages and preserve original errors", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [ - "CLI-010", - "CLI-012" - ], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan.", - "labels": [ - "ce:bootstrap", - "ce:kind:bug", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-006.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Use a consistent all-stages cleanup policy for partially and fully constructed activations.", - "body_template": "\n\n## CLI-006 — Complete all activation rollback stages and preserve original errors\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** bug · **Priority:** P1 · **Milestone:** CLI-M1\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nPrior review observes sequential unprotected scope/provider/configuration cleanup in partial CreateActivation, while normal cleanup already aggregates failures.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nUse a consistent all-stages cleanup policy for partially and fully constructed activations.\n\n**Expected benefit:** Use a consistent all-stages cleanup policy for partially and fully constructed activations.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Attempt each owned cleanup stage independently and exactly as ownership permits.\n- [ ] Preserve original construction failure before secondary cleanup diagnostics.\n- [ ] Keep CE resource/module teardown before SDK detach; avoid double DI ownership.\n\n### Acceptance criteria\n\n- [ ] A throwing scope Dispose does not skip provider/configuration cleanup.\n- [ ] The original enable failure remains identifiable.\n- [ ] Normal disable order is preserved and repeated teardown is controlled.\n\n### Required validation\n\n- [ ] Failure during service resolution plus failing scope/provider/configuration disposables.\n- [ ] Module-enable rollback and re-enable after failed activation.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E02}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Blocks\n\n- {{issue:CLI-010}}\n- {{issue:CLI-012}}\n\n### Package and readiness gate\n\nIdentify the first containing artifact before describing new source as consumable support. No version is invented by this plan.\n\n### Proposed branch and PR\n\nBranch: `fix/cli-006-activation-rollback`\n\nPR title: **Complete all activation rollback stages and preserve original errors**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nDisposal aggregation is not permission to destroy a shared service twice or detach before admitted work drains.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C07 — Prior audit: early construction cleanup differs from normal cleanup.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Hosting/CheatEngineClientPlugin.cs)\n- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-007", - "repository_key": "client", - "kind": "feature", - "title": "Replace built-in CE Lua declarations with SDK semantic operations", - "parent": "CLI-E03", - "epic": "CLI-E03", - "milestone": "CLI-M2", - "priority": "P1", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "The Client currently owns built-in CE names/signatures through ClientLuaGlobals despite using the SDK generator.", - "objective": "Make Client Core a semantic adapter, not a second CE mapping catalog.", - "requirements": [ - "Replace every built-in declaration with its corresponding published SDK operation.", - "Retain exact-name process ambiguity policy, table-root authorization and user-facing workflows.", - "Update runtime architecture interpretation consistently instead of independently decoding targetIs64Bit." - ], - "acceptance": [ - "The reviewed eleven-name correspondence has no untracked duplicates.", - "Unavailable/throwing/malformed outcomes retain SDK origin.", - "Required minimum SDK artifact is part of the package contract." - ], - "tests": [ - "Semantic adapter parity fixtures for process/runtime/symbol/table operations.", - "Clean consumer restore against minimum SDK artifact." - ], - "risks": "Do not remove working mappings before the replacement package is consumable; record temporary exceptions explicitly.", - "blocked_by": [ - "SDK-008", - "SDK-023" - ], - "sources": [ - "C02", - "C09", - "C19" - ], - "branch": "feat/cli-007-adopt-sdk-semantic-operations", - "pr_title": "Replace built-in CE Lua declarations with SDK semantic operations", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [ - "CLI-022" - ], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.", - "labels": [ - "ce:bootstrap", - "ce:kind:feature", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-007.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Make Client Core a semantic adapter, not a second CE mapping catalog.", - "body_template": "\n\n## CLI-007 — Replace built-in CE Lua declarations with SDK semantic operations\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P1 · **Milestone:** CLI-M2\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nThe Client currently owns built-in CE names/signatures through ClientLuaGlobals despite using the SDK generator.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nMake Client Core a semantic adapter, not a second CE mapping catalog.\n\n**Expected benefit:** Make Client Core a semantic adapter, not a second CE mapping catalog.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Replace every built-in declaration with its corresponding published SDK operation.\n- [ ] Retain exact-name process ambiguity policy, table-root authorization and user-facing workflows.\n- [ ] Update runtime architecture interpretation consistently instead of independently decoding targetIs64Bit.\n\n### Acceptance criteria\n\n- [ ] The reviewed eleven-name correspondence has no untracked duplicates.\n- [ ] Unavailable/throwing/malformed outcomes retain SDK origin.\n- [ ] Required minimum SDK artifact is part of the package contract.\n\n### Required validation\n\n- [ ] Semantic adapter parity fixtures for process/runtime/symbol/table operations.\n- [ ] Clean consumer restore against minimum SDK artifact.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E03}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\n- {{issue:SDK-008}}\n- {{issue:SDK-023}}\n\n### Blocks\n\n- {{issue:CLI-022}}\n\n### Package and readiness gate\n\nBefore Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.\n\n### Proposed branch and PR\n\nBranch: `feat/cli-007-adopt-sdk-semantic-operations`\n\nPR title: **Replace built-in CE Lua declarations with SDK semantic operations**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nDo not remove working mappings before the replacement package is consumable; record temporary exceptions explicitly.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C02 — Prior audit: built-in CE Lua declarations remain Client-owned.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Infrastructure/ClientLuaGlobals.cs)\n- [C09 — Prior audit: capability and exception-family mismatch.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/RuntimeClient.cs)\n- [C19 — Prior audit: SDK 1.0.0 resolution; artifact bytes not independently inspected.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/packages.lock.json)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-008", - "repository_key": "client", - "kind": "feature", - "title": "Preserve SDK failure provenance and partial effect information", - "parent": "CLI-E03", - "epic": "CLI-E03", - "milestone": "CLI-M2", - "priority": "P1", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "Prior audit: memory failures become strings/generic errors and generated false results may be labeled LuaError regardless of original cause.", - "objective": "Project SDK outcomes losslessly while keeping normal Client contracts handle-free.", - "requirements": [ - "Separate misuse, expected failure and integration failure.", - "Keep optional diagnostics cold-path; do not parse message text.", - "Add completed-step/effect fields for state-changing workflows and document compatibility changes." - ], - "acceptance": [ - "Missing global, host rejection, malformed result and Lua error remain distinct.", - "An empty successful result does not mask unavailable capability.", - "A post-mutation refresh failure does not imply no effect." - ], - "tests": [ - "Cross-layer outcome table using actual SDK fixture behavior.", - "Old consumer compilation and exception/behavior compatibility tests." - ], - "risks": "A blanket result redesign can break application fakes; prefer additive overloads/adapters.", - "blocked_by": [ - "SDK-007", - "SDK-023" - ], - "sources": [ - "C03", - "C13", - "C11", - "N03" - ], - "branch": "feat/cli-008-lossless-sdk-outcomes", - "pr_title": "Preserve SDK failure provenance and partial effect information", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [ - "CLI-013", - "CLI-014", - "CLI-015", - "CLI-017" - ], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.", - "labels": [ - "ce:bootstrap", - "ce:kind:feature", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-008.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Project SDK outcomes losslessly while keeping normal Client contracts handle-free.", - "body_template": "\n\n## CLI-008 — Preserve SDK failure provenance and partial effect information\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P1 · **Milestone:** CLI-M2\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nPrior audit: memory failures become strings/generic errors and generated false results may be labeled LuaError regardless of original cause.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nProject SDK outcomes losslessly while keeping normal Client contracts handle-free.\n\n**Expected benefit:** Project SDK outcomes losslessly while keeping normal Client contracts handle-free.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Separate misuse, expected failure and integration failure.\n- [ ] Keep optional diagnostics cold-path; do not parse message text.\n- [ ] Add completed-step/effect fields for state-changing workflows and document compatibility changes.\n\n### Acceptance criteria\n\n- [ ] Missing global, host rejection, malformed result and Lua error remain distinct.\n- [ ] An empty successful result does not mask unavailable capability.\n- [ ] A post-mutation refresh failure does not imply no effect.\n\n### Required validation\n\n- [ ] Cross-layer outcome table using actual SDK fixture behavior.\n- [ ] Old consumer compilation and exception/behavior compatibility tests.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E03}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\n- {{issue:SDK-007}}\n- {{issue:SDK-023}}\n\n### Blocks\n\n- {{issue:CLI-013}}\n- {{issue:CLI-014}}\n- {{issue:CLI-015}}\n- {{issue:CLI-017}}\n\n### Package and readiness gate\n\nBefore Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.\n\n### Proposed branch and PR\n\nBranch: `feat/cli-008-lossless-sdk-outcomes`\n\nPR title: **Preserve SDK failure provenance and partial effect information**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nA blanket result redesign can break application fakes; prefer additive overloads/adapters.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C03 — Prior audit: scoped codecs, partial batches and failure projection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/MemoryClient.cs)\n- [C13 — Prior audit: operation/registration generation and recursive type checks.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/source-generators/CheatEngine.Client.SourceGenerators.Lua/CheatEngineLuaGenerator.cs)\n- [C11 — Prior audit: SDK delegation but ambiguous false becomes rejection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/SdkAobScanPort.cs)\n- [N03 — Source/binary/behavioral compatibility distinctions.](https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/breaking-changes)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-009", - "repository_key": "client", - "kind": "feature", - "title": "Bind Client workflows to authoritative SDK target identity", - "parent": "CLI-E03", - "epic": "CLI-E03", - "milestone": "CLI-M2", - "priority": "P1", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "Client observes selection changes through local snapshots. SDK-owned target identity is needed for future allocations, patches and stale queued operations.", - "objective": "Capture SDK target/activation authority for effectful workflow steps and high-level leases.", - "requirements": [ - "Validate captured identity at actual execution, not only before waiting for dispatch.", - "Keep pure copied snapshots distinct from live operations.", - "Never release an old lease through the new current target or invisibly switch targets during cleanup." - ], - "acceptance": [ - "External target change invalidates affected workflow leases.", - "PID reuse/A-to-B-to-A cannot silently reactivate an old lease.", - "The local epoch is only a Client policy marker, not duplicated native authority." - ], - "tests": [ - "Queued work plus external target switch.", - "Stale lease method/release, terminated process and same PID/new incarnation." - ], - "risks": "Only explicit original-target cleanup or safe refusal is acceptable under uncertain identity.", - "blocked_by": [ - "SDK-010", - "SDK-011", - "SDK-023" - ], - "sources": [ - "C08", - "C06", - "C05" - ], - "branch": "feat/cli-009-target-bound-workflows", - "pr_title": "Bind Client workflows to authoritative SDK target identity", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [ - "CLI-013", - "CLI-014", - "CLI-015", - "CLI-016", - "CLI-017", - "CLI-018" - ], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.", - "labels": [ - "ce:bootstrap", - "ce:kind:feature", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-009.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Capture SDK target/activation authority for effectful workflow steps and high-level leases.", - "body_template": "\n\n## CLI-009 — Bind Client workflows to authoritative SDK target identity\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P1 · **Milestone:** CLI-M2\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nClient observes selection changes through local snapshots. SDK-owned target identity is needed for future allocations, patches and stale queued operations.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nCapture SDK target/activation authority for effectful workflow steps and high-level leases.\n\n**Expected benefit:** Capture SDK target/activation authority for effectful workflow steps and high-level leases.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Validate captured identity at actual execution, not only before waiting for dispatch.\n- [ ] Keep pure copied snapshots distinct from live operations.\n- [ ] Never release an old lease through the new current target or invisibly switch targets during cleanup.\n\n### Acceptance criteria\n\n- [ ] External target change invalidates affected workflow leases.\n- [ ] PID reuse/A-to-B-to-A cannot silently reactivate an old lease.\n- [ ] The local epoch is only a Client policy marker, not duplicated native authority.\n\n### Required validation\n\n- [ ] Queued work plus external target switch.\n- [ ] Stale lease method/release, terminated process and same PID/new incarnation.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E03}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\n- {{issue:SDK-010}}\n- {{issue:SDK-011}}\n- {{issue:SDK-023}}\n\n### Blocks\n\n- {{issue:CLI-013}}\n- {{issue:CLI-014}}\n- {{issue:CLI-015}}\n- {{issue:CLI-016}}\n- {{issue:CLI-017}}\n- {{issue:CLI-018}}\n\n### Package and readiness gate\n\nBefore Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.\n\n### Proposed branch and PR\n\nBranch: `feat/cli-009-target-bound-workflows`\n\nPR title: **Bind Client workflows to authoritative SDK target identity**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nOnly explicit original-target cleanup or safe refusal is acceptable under uncertain identity.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C08 — Prior audit: local observed epoch is not authoritative target identity.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Infrastructure/TargetSelectionLifetime.cs)\n- [C06 — Prior audit: expected no-target failure and local target observations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/ProcessClient.cs)\n- [C05 — Prior audit: production adapter rethrows callback exceptions.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-010", - "repository_key": "client", - "kind": "feature", - "title": "Use SDK registration leases for generated Lua modules", - "parent": "CLI-E04", - "epic": "CLI-E04", - "milestone": "CLI-M3", - "priority": "P1", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "The Client generator emits stack preflight/rollback while SDK generation already owns marshalling and closure generation.", - "objective": "Retain Client descriptors/order/policy and delegate registration safety to SDK leases.", - "requirements": [ - "Generate module descriptors and calls to the SDK registration contract instead of raw stack mechanisms.", - "Keep application collision policy explicit and release modules in reverse activation order.", - "Preserve a later replacement global when disposing the original module registration." - ], - "acceptance": [ - "Client-generated built-in module code does not implement a second low-level registration algorithm.", - "Partial enable failure releases only owned registrations.", - "Retained function values cannot call disposed plugin code." - ], - "tests": [ - "Compiled consumer with two modules, name replacement and partial registration failure.", - "Disable/re-enable and Lua reset fixtures." - ], - "risks": "Client-local reservations alone cannot serialize arbitrary scripts or other plugins.", - "blocked_by": [ - "SDK-009", - "SDK-022", - "SDK-023", - "CLI-006" - ], - "sources": [ - "C13", - "S06", - "C07" - ], - "branch": "feat/cli-010-sdk-module-registration", - "pr_title": "Use SDK registration leases for generated Lua modules", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [ - "CLI-022" - ], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.", - "labels": [ - "ce:bootstrap", - "ce:kind:feature", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-010.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Retain Client descriptors/order/policy and delegate registration safety to SDK leases.", - "body_template": "\n\n## CLI-010 — Use SDK registration leases for generated Lua modules\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P1 · **Milestone:** CLI-M3\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nThe Client generator emits stack preflight/rollback while SDK generation already owns marshalling and closure generation.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nRetain Client descriptors/order/policy and delegate registration safety to SDK leases.\n\n**Expected benefit:** Retain Client descriptors/order/policy and delegate registration safety to SDK leases.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Generate module descriptors and calls to the SDK registration contract instead of raw stack mechanisms.\n- [ ] Keep application collision policy explicit and release modules in reverse activation order.\n- [ ] Preserve a later replacement global when disposing the original module registration.\n\n### Acceptance criteria\n\n- [ ] Client-generated built-in module code does not implement a second low-level registration algorithm.\n- [ ] Partial enable failure releases only owned registrations.\n- [ ] Retained function values cannot call disposed plugin code.\n\n### Required validation\n\n- [ ] Compiled consumer with two modules, name replacement and partial registration failure.\n- [ ] Disable/re-enable and Lua reset fixtures.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E04}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\n- {{issue:SDK-009}}\n- {{issue:SDK-022}}\n- {{issue:SDK-023}}\n- {{issue:CLI-006}}\n\n### Blocks\n\n- {{issue:CLI-022}}\n\n### Package and readiness gate\n\nBefore Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.\n\n### Proposed branch and PR\n\nBranch: `feat/cli-010-sdk-module-registration`\n\nPR title: **Use SDK registration leases for generated Lua modules**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nClient-local reservations alone cannot serialize arbitrary scripts or other plugins.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C13 — Prior audit: operation/registration generation and recursive type checks.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/source-generators/CheatEngine.Client.SourceGenerators.Lua/CheatEngineLuaGenerator.cs)\n- [S06 — Prior audit: generation stamps but name-based unregister.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaEmit/LuaRegistrationEmitter.cs)\n- [C07 — Prior audit: early construction cleanup differs from normal cleanup.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Hosting/CheatEngineClientPlugin.cs)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-011", - "repository_key": "client", - "kind": "bug", - "title": "Enforce recursive public and generated API type boundaries", - "parent": "CLI-E04", - "epic": "CLI-E04", - "milestone": "CLI-M3", - "priority": "P1", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "The prior review identifies shallow result-type checks and a generic branch that can bypass subsequent forbidden-definition checks.", - "objective": "Inspect the complete public type graph while allowing deliberately approved SDK value contracts.", - "requirements": [ - "Traverse generic definitions/arguments, arrays, constraints, nested returns and emitted code.", - "Reject LuaState, raw CE handles, ABI records, callbacks and ownership wrappers from normal Client contracts.", - "Keep approved immutable addresses/IDs/enums; do not clone every SDK value or split packages prematurely." - ], - "acceptance": [ - "Nested forbidden types are rejected consistently by tests/generator diagnostics.", - "Allowed immutable SDK values remain usable.", - "Generated consumer compilation tests the boundary, not only source strings." - ], - "tests": [ - "Nested generic/array/constraint positives and negatives.", - "Old application fake implementations and default builder tests." - ], - "risks": "Compile-time architecture checks are not a security sandbox. Type moves can also break assembly identity.", - "blocked_by": [], - "sources": [ - "C13", - "C20", - "N03" - ], - "branch": "fix/cli-011-recursive-type-boundaries", - "pr_title": "Enforce recursive public and generated API type boundaries", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [ - "CLI-022" - ], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan.", - "labels": [ - "ce:bootstrap", - "ce:kind:bug", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-011.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Inspect the complete public type graph while allowing deliberately approved SDK value contracts.", - "body_template": "\n\n## CLI-011 — Enforce recursive public and generated API type boundaries\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** bug · **Priority:** P1 · **Milestone:** CLI-M3\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nThe prior review identifies shallow result-type checks and a generic branch that can bypass subsequent forbidden-definition checks.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nInspect the complete public type graph while allowing deliberately approved SDK value contracts.\n\n**Expected benefit:** Inspect the complete public type graph while allowing deliberately approved SDK value contracts.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Traverse generic definitions/arguments, arrays, constraints, nested returns and emitted code.\n- [ ] Reject LuaState, raw CE handles, ABI records, callbacks and ownership wrappers from normal Client contracts.\n- [ ] Keep approved immutable addresses/IDs/enums; do not clone every SDK value or split packages prematurely.\n\n### Acceptance criteria\n\n- [ ] Nested forbidden types are rejected consistently by tests/generator diagnostics.\n- [ ] Allowed immutable SDK values remain usable.\n- [ ] Generated consumer compilation tests the boundary, not only source strings.\n\n### Required validation\n\n- [ ] Nested generic/array/constraint positives and negatives.\n- [ ] Old application fake implementations and default builder tests.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E04}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Blocks\n\n- {{issue:CLI-022}}\n\n### Package and readiness gate\n\nIdentify the first containing artifact before describing new source as consumable support. No version is invented by this plan.\n\n### Proposed branch and PR\n\nBranch: `fix/cli-011-recursive-type-boundaries`\n\nPR title: **Enforce recursive public and generated API type boundaries**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nCompile-time architecture checks are not a security sandbox. Type moves can also break assembly identity.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C13 — Prior audit: operation/registration generation and recursive type checks.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/source-generators/CheatEngine.Client.SourceGenerators.Lua/CheatEngineLuaGenerator.cs)\n- [C20 — Prior audit: approved SDK values versus broad compile/runtime dependency.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Abstractions/CheatEngine.Client.Abstractions.csproj)\n- [N03 — Source/binary/behavioral compatibility distinctions.](https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/breaking-changes)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-012", - "repository_key": "client", - "kind": "validation", - "title": "Specify activation-local DI and multi-plugin composition", - "parent": "CLI-E04", - "epic": "CLI-E04", - "milestone": "CLI-M3", - "priority": "P2", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "The plugin creates a fresh provider per enable, with services registered as singletons within that provider. Extra scopes in a persistent provider do not recreate those singletons.", - "objective": "Make the supported composition lifetime explicit and test it independently of SDK load-context isolation.", - "requirements": [ - "Document one provider per activation and ownership of DI-created disposables.", - "Test aliases, modules and options without duplicate disposal.", - "Qualify simultaneous plugins against the SDK coexistence result; do not infer it from separate providers." - ], - "acceptance": [ - "Every enable receives fresh Client lifetime and module state.", - "Two scopes in one provider have documented behavior.", - "No promise of persistent-root hosting without an explicit activation factory design." - ], - "tests": [ - "Provider/scope/alias disposal matrix.", - "Two plugins in one host after SDK coexistence qualification." - ], - "risks": "Changing all registrations to scoped mechanically can change semantics and does not solve shared SDK static state.", - "blocked_by": [ - "CLI-006", - "SDK-005" - ], - "sources": [ - "C07", - "C14", - "S08" - ], - "branch": "test/cli-012-activation-di-contract", - "pr_title": "Specify activation-local DI and multi-plugin composition", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.", - "labels": [ - "ce:bootstrap", - "ce:kind:validation", - "ce:priority:P2" - ], - "path": "docs/engineering/work-items/CLI-012.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Make the supported composition lifetime explicit and test it independently of SDK load-context isolation.", - "body_template": "\n\n## CLI-012 — Specify activation-local DI and multi-plugin composition\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** validation · **Priority:** P2 · **Milestone:** CLI-M3\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nThe plugin creates a fresh provider per enable, with services registered as singletons within that provider. Extra scopes in a persistent provider do not recreate those singletons.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nMake the supported composition lifetime explicit and test it independently of SDK load-context isolation.\n\n**Expected benefit:** Make the supported composition lifetime explicit and test it independently of SDK load-context isolation.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Document one provider per activation and ownership of DI-created disposables.\n- [ ] Test aliases, modules and options without duplicate disposal.\n- [ ] Qualify simultaneous plugins against the SDK coexistence result; do not infer it from separate providers.\n\n### Acceptance criteria\n\n- [ ] Every enable receives fresh Client lifetime and module state.\n- [ ] Two scopes in one provider have documented behavior.\n- [ ] No promise of persistent-root hosting without an explicit activation factory design.\n\n### Required validation\n\n- [ ] Provider/scope/alias disposal matrix.\n- [ ] Two plugins in one host after SDK coexistence qualification.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E04}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\n- {{issue:CLI-006}}\n- {{issue:SDK-005}}\n\n### Blocks\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Package and readiness gate\n\nBefore Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.\n\n### Proposed branch and PR\n\nBranch: `test/cli-012-activation-di-contract`\n\nPR title: **Specify activation-local DI and multi-plugin composition**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nChanging all registrations to scoped mechanically can change semantics and does not solve shared SDK static state.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C07 — Prior audit: early construction cleanup differs from normal cleanup.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Hosting/CheatEngineClientPlugin.cs)\n- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs)\n- [S08 — Prior audit: static host state and runtime admission.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/libs/CheatEngine.SDK.Hosting/Bootstrap/PluginHost.cs)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-013", - "repository_key": "client", - "kind": "feature", - "title": "Make AOB cardinality and result budgets truthful", - "parent": "CLI-E05", - "epic": "CLI-E05", - "milestone": "CLI-M4", - "priority": "P1", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "Current scanning runs before module resolution and post-filters native results. FirstOrNone cannot safely infer no-match from an ambiguous SDK false.", - "objective": "Compose qualified SDK scan outcomes into predictable fluent cardinality semantics.", - "requirements": [ - "Resolve absent/invalid module requests before expensive native work when target consistency permits.", - "Map qualified no-match to success/null, not every false.", - "Differentiate copied-result caps from native workload bounds and rename ReadableExecutable with a compatibility alias matching actual flags." - ], - "acceptance": [ - "FirstOrNone, RequireSingle and Take behave distinctly on zero/one/many matches.", - "A missing module proven before admission does not launch a full scan.", - "Documentation never claims Take(1) bounds CE scan work." - ], - "tests": [ - "No-hit/error/malformed outcomes and uniqueness/truncation tests.", - "Module/range boundaries and protection-name compatibility examples." - ], - "risks": "Early-stop cannot prove uniqueness; post-filtering and ordering must remain explicit.", - "blocked_by": [ - "SDK-014", - "SDK-023", - "CLI-008", - "CLI-009" - ], - "sources": [ - "C10", - "C11", - "C16" - ], - "branch": "feat/cli-013-aob-cardinality-budgets", - "pr_title": "Make AOB cardinality and result budgets truthful", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [ - "CLI-020" - ], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.", - "labels": [ - "ce:bootstrap", - "ce:kind:feature", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-013.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Compose qualified SDK scan outcomes into predictable fluent cardinality semantics.", - "body_template": "\n\n## CLI-013 — Make AOB cardinality and result budgets truthful\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P1 · **Milestone:** CLI-M4\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nCurrent scanning runs before module resolution and post-filters native results. FirstOrNone cannot safely infer no-match from an ambiguous SDK false.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nCompose qualified SDK scan outcomes into predictable fluent cardinality semantics.\n\n**Expected benefit:** Compose qualified SDK scan outcomes into predictable fluent cardinality semantics.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Resolve absent/invalid module requests before expensive native work when target consistency permits.\n- [ ] Map qualified no-match to success/null, not every false.\n- [ ] Differentiate copied-result caps from native workload bounds and rename ReadableExecutable with a compatibility alias matching actual flags.\n\n### Acceptance criteria\n\n- [ ] FirstOrNone, RequireSingle and Take behave distinctly on zero/one/many matches.\n- [ ] A missing module proven before admission does not launch a full scan.\n- [ ] Documentation never claims Take(1) bounds CE scan work.\n\n### Required validation\n\n- [ ] No-hit/error/malformed outcomes and uniqueness/truncation tests.\n- [ ] Module/range boundaries and protection-name compatibility examples.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E05}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\n- {{issue:SDK-014}}\n- {{issue:SDK-023}}\n- {{issue:CLI-008}}\n- {{issue:CLI-009}}\n\n### Blocks\n\n- {{issue:CLI-020}}\n\n### Package and readiness gate\n\nBefore Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.\n\n### Proposed branch and PR\n\nBranch: `feat/cli-013-aob-cardinality-budgets`\n\nPR title: **Make AOB cardinality and result budgets truthful**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nEarly-stop cannot prove uniqueness; post-filtering and ordering must remain explicit.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C10 — Prior audit: whole scan precedes module lookup and post-filtering.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/PatternScanner.cs)\n- [C11 — Prior audit: SDK delegation but ambiguous false becomes rejection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/SdkAobScanPort.cs)\n- [C16 — Prior audit: immutable terminals, post-filter limits, readability naming.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Fluent/Scanning/AobScanBuilder.cs)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-014", - "repository_key": "client", - "kind": "feature", - "title": "Report memory batch effects and cancellation milestones", - "parent": "CLI-E05", - "epic": "CLI-E05", - "milestone": "CLI-M4", - "priority": "P1", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "Homogeneous batches reduce dispatch count but still perform sequential primitive calls. Failure indices in text do not provide a stable partial-effect contract.", - "objective": "Expose completed prefix, failed/canceled step and effect certainty for composed memory workflows.", - "requirements": [ - "Preserve current admission-only cancellation where promised; make between-step cancellation an explicit policy.", - "Copy span input before it outlives its synchronous caller; do not introduce dangling zero-copy buffers.", - "Keep pointer-width/address arithmetic in qualified SDK primitives." - ], - "acceptance": [ - "Failure at k reports which prior operations completed.", - "Cancellation never claims to interrupt an already-running synchronous CE primitive.", - "One dispatch is not described as one native call or an atomic transaction." - ], - "tests": [ - "Failure/cancellation before admission, between steps and after native success.", - "Span lifetime, pointer overflow and custom-codec regression tests." - ], - "risks": "Rollback writes are not a transaction and can fail; do not retry partial workflows invisibly.", - "blocked_by": [ - "SDK-013", - "CLI-008", - "CLI-009", - "CLI-005" - ], - "sources": [ - "C03", - "C05", - "C01" - ], - "branch": "feat/cli-014-memory-effects-cancellation", - "pr_title": "Report memory batch effects and cancellation milestones", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.", - "labels": [ - "ce:bootstrap", - "ce:kind:feature", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-014.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Expose completed prefix, failed/canceled step and effect certainty for composed memory workflows.", - "body_template": "\n\n## CLI-014 — Report memory batch effects and cancellation milestones\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P1 · **Milestone:** CLI-M4\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nHomogeneous batches reduce dispatch count but still perform sequential primitive calls. Failure indices in text do not provide a stable partial-effect contract.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nExpose completed prefix, failed/canceled step and effect certainty for composed memory workflows.\n\n**Expected benefit:** Expose completed prefix, failed/canceled step and effect certainty for composed memory workflows.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Preserve current admission-only cancellation where promised; make between-step cancellation an explicit policy.\n- [ ] Copy span input before it outlives its synchronous caller; do not introduce dangling zero-copy buffers.\n- [ ] Keep pointer-width/address arithmetic in qualified SDK primitives.\n\n### Acceptance criteria\n\n- [ ] Failure at k reports which prior operations completed.\n- [ ] Cancellation never claims to interrupt an already-running synchronous CE primitive.\n- [ ] One dispatch is not described as one native call or an atomic transaction.\n\n### Required validation\n\n- [ ] Failure/cancellation before admission, between steps and after native success.\n- [ ] Span lifetime, pointer overflow and custom-codec regression tests.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E05}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\n- {{issue:SDK-013}}\n- {{issue:CLI-008}}\n- {{issue:CLI-009}}\n- {{issue:CLI-005}}\n\n### Blocks\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Package and readiness gate\n\nBefore Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.\n\n### Proposed branch and PR\n\nBranch: `feat/cli-014-memory-effects-cancellation`\n\nPR title: **Report memory batch effects and cancellation milestones**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nRollback writes are not a transaction and can fail; do not retry partial workflows invisibly.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C03 — Prior audit: scoped codecs, partial batches and failure projection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/MemoryClient.cs)\n- [C05 — Prior audit: production adapter rethrows callback exceptions.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs)\n- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-015", - "repository_key": "client", - "kind": "feature", - "title": "Adopt typed record and symbol commands with safe workflow policy", - "parent": "CLI-E05", - "epic": "CLI-E05", - "milestone": "CLI-M4", - "priority": "P1", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "Client record mutation uses Parent/destroy via generic SDK object/Lua access and can mutate before snapshot refresh fails.", - "objective": "Delegate CE mutation semantics to SDK while retaining high-level IDs, snapshots and trust policy.", - "requirements": [ - "Replace raw property/method operations with typed SDK commands.", - "Represent command success separately from snapshot refresh.", - "Keep table-root and raw-script opt-in defaults without claiming path checks sandbox content." - ], - "acceptance": [ - "No built-in Client adapter repairs CE record ownership through raw stack code.", - "Delete remains explicit; disposing a borrowed snapshot cannot destroy GUI data.", - "Post-mutation refresh failure preserves the completed effect." - ], - "tests": [ - "Missing/cyclic/self-parent records and snapshot failure after mutation.", - "Denied roots, observed reparse points and explicit trust assumptions." - ], - "risks": "A generic helper relocation alone does not fix ownership; safe path defaults do not eliminate TOCTOU.", - "blocked_by": [ - "SDK-021", - "SDK-023", - "CLI-008", - "CLI-009" - ], - "sources": [ - "C12", - "C18", - "C02" - ], - "branch": "feat/cli-015-typed-record-symbol-workflows", - "pr_title": "Adopt typed record and symbol commands with safe workflow policy", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.", - "labels": [ - "ce:bootstrap", - "ce:kind:feature", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-015.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Delegate CE mutation semantics to SDK while retaining high-level IDs, snapshots and trust policy.", - "body_template": "\n\n## CLI-015 — Adopt typed record and symbol commands with safe workflow policy\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P1 · **Milestone:** CLI-M4\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nClient record mutation uses Parent/destroy via generic SDK object/Lua access and can mutate before snapshot refresh fails.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nDelegate CE mutation semantics to SDK while retaining high-level IDs, snapshots and trust policy.\n\n**Expected benefit:** Delegate CE mutation semantics to SDK while retaining high-level IDs, snapshots and trust policy.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Replace raw property/method operations with typed SDK commands.\n- [ ] Represent command success separately from snapshot refresh.\n- [ ] Keep table-root and raw-script opt-in defaults without claiming path checks sandbox content.\n\n### Acceptance criteria\n\n- [ ] No built-in Client adapter repairs CE record ownership through raw stack code.\n- [ ] Delete remains explicit; disposing a borrowed snapshot cannot destroy GUI data.\n- [ ] Post-mutation refresh failure preserves the completed effect.\n\n### Required validation\n\n- [ ] Missing/cyclic/self-parent records and snapshot failure after mutation.\n- [ ] Denied roots, observed reparse points and explicit trust assumptions.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E05}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\n- {{issue:SDK-021}}\n- {{issue:SDK-023}}\n- {{issue:CLI-008}}\n- {{issue:CLI-009}}\n\n### Blocks\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Package and readiness gate\n\nBefore Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.\n\n### Proposed branch and PR\n\nBranch: `feat/cli-015-typed-record-symbol-workflows`\n\nPR title: **Adopt typed record and symbol commands with safe workflow policy**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nA generic helper relocation alone does not fix ownership; safe path defaults do not eliminate TOCTOU.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C12 — Prior audit: Parent/destroy semantics and mutation/snapshot ambiguity.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/SdkTableRecordMutationPort.cs)\n- [C18 — Prior audit: table-root allowlist and documented path TOCTOU limitation.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Infrastructure/CoreClientPolicy.cs)\n- [C02 — Prior audit: built-in CE Lua declarations remain Client-owned.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Infrastructure/ClientLuaGlobals.cs)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-016", - "repository_key": "client", - "kind": "feature", - "title": "Adopt qualified SDK value-scan sessions", - "parent": "CLI-E06", - "epic": "CLI-E06", - "milestone": "CLI-M5", - "priority": "P2", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "The SDK factory exists but the current Client deliberately registers an unavailable value-scanner implementation.", - "objective": "Deliver one high-level scan-session vertical slice after its factory, artifact and live gates pass.", - "requirements": [ - "Wrap the SDK-created owner; never reconstruct it from borrowed handles.", - "Define Client request validation, session transitions, progress and copied result limits.", - "Keep unsupported transitions/host profiles explicitly unavailable." - ], - "acceptance": [ - "The enabled adapter links the qualified factory and first-containing package.", - "Every transition has failure/cleanup and target-change coverage.", - "No source-only or interface-only status is advertised as live support." - ], - "tests": [ - "SDK conformance plus Client success/failure/state-transition fixtures.", - "Live enable/disable/re-enable/target-change scan session." - ], - "risks": "UI-friendly progress cannot substitute for a qualified native cancellation contract.", - "blocked_by": [ - "SDK-015", - "SDK-023", - "CLI-009", - "CLI-002" - ], - "sources": [ - "S03", - "C14", - "C21" - ], - "branch": "feat/cli-016-value-scan-adoption", - "pr_title": "Adopt qualified SDK value-scan sessions", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.", - "labels": [ - "ce:bootstrap", - "ce:kind:feature", - "ce:priority:P2" - ], - "path": "docs/engineering/work-items/CLI-016.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Deliver one high-level scan-session vertical slice after its factory, artifact and live gates pass.", - "body_template": "\n\n## CLI-016 — Adopt qualified SDK value-scan sessions\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P2 · **Milestone:** CLI-M5\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nThe SDK factory exists but the current Client deliberately registers an unavailable value-scanner implementation.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nDeliver one high-level scan-session vertical slice after its factory, artifact and live gates pass.\n\n**Expected benefit:** Deliver one high-level scan-session vertical slice after its factory, artifact and live gates pass.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Wrap the SDK-created owner; never reconstruct it from borrowed handles.\n- [ ] Define Client request validation, session transitions, progress and copied result limits.\n- [ ] Keep unsupported transitions/host profiles explicitly unavailable.\n\n### Acceptance criteria\n\n- [ ] The enabled adapter links the qualified factory and first-containing package.\n- [ ] Every transition has failure/cleanup and target-change coverage.\n- [ ] No source-only or interface-only status is advertised as live support.\n\n### Required validation\n\n- [ ] SDK conformance plus Client success/failure/state-transition fixtures.\n- [ ] Live enable/disable/re-enable/target-change scan session.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E06}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\n- {{issue:SDK-015}}\n- {{issue:SDK-023}}\n- {{issue:CLI-009}}\n- {{issue:CLI-002}}\n\n### Blocks\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Package and readiness gate\n\nBefore Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.\n\n### Proposed branch and PR\n\nBranch: `feat/cli-016-value-scan-adoption`\n\nPR title: **Adopt qualified SDK value-scan sessions**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nUI-friendly progress cannot substitute for a qualified native cancellation contract.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [S03 — Prior audit: new factory already exists; qualify, do not recreate.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanSessions.cs)\n- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs)\n- [C21 — Live gate protocol identified by current README; not independently re-read here.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/docs/live-capability-gates.md)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-017", - "repository_key": "client", - "kind": "feature", - "title": "Adopt target-bound allocation and patch leases", - "parent": "CLI-E06", - "epic": "CLI-E06", - "milestone": "CLI-M5", - "priority": "P1", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "The Client has gated allocation/assembly contracts; SDK main contains underlying primitives that need target and handoff qualification.", - "objective": "Expose separate high-level allocation and patch workflows that compose qualified SDK owners.", - "requirements": [ - "Use explicit opt-in and safe defaults; do not rebuild disable-info or raw deallocation.", - "Capture activation/target identity and track owner transfer failure.", - "Split allocation and patch implementation into two focused PRs under this work item if they require independent review." - ], - "acceptance": [ - "A stale lease cannot mutate or release against a different target.", - "A failure after native success retains cleanup authority or reports unknown effect.", - "Availability records the exact SDK/live evidence for each workflow separately." - ], - "tests": [ - "Owner creation/tracking/publication failure and double disposal.", - "Live target switch and partial cleanup for allocation and patch independently." - ], - "risks": "Patch scripts are privileged in-process operations, not sandboxed statements. Separate outcomes prevent unsafe retries.", - "blocked_by": [ - "SDK-011", - "SDK-017", - "SDK-023", - "CLI-009", - "CLI-008" - ], - "sources": [ - "S04", - "S05", - "C14", - "C21" - ], - "branch": "feat/cli-017-allocation-patch-leases", - "pr_title": "Adopt target-bound allocation and patch leases", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.", - "labels": [ - "ce:bootstrap", - "ce:kind:feature", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-017.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Expose separate high-level allocation and patch workflows that compose qualified SDK owners.", - "body_template": "\n\n## CLI-017 — Adopt target-bound allocation and patch leases\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P1 · **Milestone:** CLI-M5\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nThe Client has gated allocation/assembly contracts; SDK main contains underlying primitives that need target and handoff qualification.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nExpose separate high-level allocation and patch workflows that compose qualified SDK owners.\n\n**Expected benefit:** Expose separate high-level allocation and patch workflows that compose qualified SDK owners.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Use explicit opt-in and safe defaults; do not rebuild disable-info or raw deallocation.\n- [ ] Capture activation/target identity and track owner transfer failure.\n- [ ] Split allocation and patch implementation into two focused PRs under this work item if they require independent review.\n\n### Acceptance criteria\n\n- [ ] A stale lease cannot mutate or release against a different target.\n- [ ] A failure after native success retains cleanup authority or reports unknown effect.\n- [ ] Availability records the exact SDK/live evidence for each workflow separately.\n\n### Required validation\n\n- [ ] Owner creation/tracking/publication failure and double disposal.\n- [ ] Live target switch and partial cleanup for allocation and patch independently.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E06}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\n- {{issue:SDK-011}}\n- {{issue:SDK-017}}\n- {{issue:SDK-023}}\n- {{issue:CLI-009}}\n- {{issue:CLI-008}}\n\n### Blocks\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Package and readiness gate\n\nBefore Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.\n\n### Proposed branch and PR\n\nBranch: `feat/cli-017-allocation-patch-leases`\n\nPR title: **Adopt target-bound allocation and patch leases**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nPatch scripts are privileged in-process operations, not sandboxed statements. Separate outcomes prevent unsafe retries.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [S04 — Prior audit: current-target allocation bindings.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/libs/CheatEngine.SDK.Engine/Allocation/LuaTargetMemoryAllocationOperations.cs)\n- [S05 — Prior audit: patch and disable-info ownership handoff.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/libs/CheatEngine.SDK.Engine/Assembly/AutoAssemblerPatcher.cs)\n- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs)\n- [C21 — Live gate protocol identified by current README; not independently re-read here.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/docs/live-capability-gates.md)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-018", - "repository_key": "client", - "kind": "research", - "title": "Define gated debugger and subscription workflow adapters", - "parent": "CLI-E06", - "epic": "CLI-E06", - "milestone": "CLI-M5", - "priority": "P2", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "Debugger, timer and hotkey contracts are currently unavailable; a copied-event stream exists separately.", - "objective": "Specify Client-friendly synchronous decisions and subscription workflows over qualified SDK mechanisms.", - "requirements": [ - "Keep native continuation decisions synchronous and distinguish them from async observations.", - "Delegate callback/thunk/rooting/unregister to SDK leases.", - "Enable each producer only after its own host/cleanup qualification and policy review." - ], - "acceptance": [ - "No async consumer is responsible for unblocking a native debug event.", - "Expired activations cannot receive native callbacks.", - "Unavailable status persists where an SDK producer is not qualified." - ], - "tests": [ - "Decision versus observation contract fixtures.", - "Late callback, slow reader and teardown races with real qualified producers." - ], - "risks": "This specification is not permission to implement a second debugger backend in Client.", - "blocked_by": [ - "SDK-018", - "SDK-019", - "CLI-019", - "CLI-009" - ], - "sources": [ - "C14", - "C15", - "U01" - ], - "branch": "feat/cli-018-advanced-event-workflows", - "pr_title": "Define gated debugger and subscription workflow adapters", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.", - "labels": [ - "ce:bootstrap", - "ce:kind:research", - "ce:priority:P2" - ], - "path": "docs/engineering/work-items/CLI-018.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Specify Client-friendly synchronous decisions and subscription workflows over qualified SDK mechanisms.", - "body_template": "\n\n## CLI-018 — Define gated debugger and subscription workflow adapters\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** research · **Priority:** P2 · **Milestone:** CLI-M5\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nDebugger, timer and hotkey contracts are currently unavailable; a copied-event stream exists separately.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nSpecify Client-friendly synchronous decisions and subscription workflows over qualified SDK mechanisms.\n\n**Expected benefit:** Specify Client-friendly synchronous decisions and subscription workflows over qualified SDK mechanisms.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Keep native continuation decisions synchronous and distinguish them from async observations.\n- [ ] Delegate callback/thunk/rooting/unregister to SDK leases.\n- [ ] Enable each producer only after its own host/cleanup qualification and policy review.\n\n### Acceptance criteria\n\n- [ ] No async consumer is responsible for unblocking a native debug event.\n- [ ] Expired activations cannot receive native callbacks.\n- [ ] Unavailable status persists where an SDK producer is not qualified.\n\n### Required validation\n\n- [ ] Decision versus observation contract fixtures.\n- [ ] Late callback, slow reader and teardown races with real qualified producers.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E06}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\n- {{issue:SDK-018}}\n- {{issue:SDK-019}}\n- {{issue:CLI-019}}\n- {{issue:CLI-009}}\n\n### Blocks\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Package and readiness gate\n\nBefore Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.\n\n### Proposed branch and PR\n\nBranch: `feat/cli-018-advanced-event-workflows`\n\nPR title: **Define gated debugger and subscription workflow adapters**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nThis specification is not permission to implement a second debugger backend in Client.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs)\n- [C15 — Prior audit: bounded event ring, pending reader list and async continuations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/Events/BoundedEventStream.cs)\n- [U01 — Historical loader/tables/callback dispatch comparison.](https://github.com/cheat-engine/cheat-engine/blob/ec45d5f47f92a239ba0bf51ec5d04a7509c3fd37/Cheat%20Engine/plugin.pas)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-019", - "repository_key": "client", - "kind": "feature", - "title": "Bound event consumers and specify stream completion", - "parent": "CLI-E07", - "epic": "CLI-E07", - "milestone": "CLI-M5", - "priority": "P2", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "The prior audit observes a bounded event ring but an independent linked list of pending reads and multiple enumerators. The implementation uses a lock.", - "objective": "Define a bounded observation-delivery contract beyond the ring capacity alone.", - "requirements": [ - "Choose single-reader default or explicitly bounded competing-reader behavior; do not imply broadcast.", - "Specify disposal of an enumerator with a pending read, cancellation, fairness and loss counts.", - "Keep continuations asynchronous and payloads copied; measure lock contention rather than call it lock-free." - ], - "acceptance": [ - "Pending reader memory is bounded or excess readers are rejected.", - "Disposal/cancellation cannot strand pending reads.", - "Dropped observations never discard required native decisions." - ], - "tests": [ - "Many readers, canceled pending read, close/complete/dispose and overflow fixtures.", - "Publication latency under a slow consumer." - ], - "risks": "Changing queue implementation for fashion can break loss/ordering/completion semantics; qualify the contract first.", - "blocked_by": [], - "sources": [ - "C15", - "C01" - ], - "branch": "feat/cli-019-bounded-event-consumers", - "pr_title": "Bound event consumers and specify stream completion", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [ - "CLI-018" - ], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan.", - "labels": [ - "ce:bootstrap", - "ce:kind:feature", - "ce:priority:P2" - ], - "path": "docs/engineering/work-items/CLI-019.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Define a bounded observation-delivery contract beyond the ring capacity alone.", - "body_template": "\n\n## CLI-019 — Bound event consumers and specify stream completion\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P2 · **Milestone:** CLI-M5\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nThe prior audit observes a bounded event ring but an independent linked list of pending reads and multiple enumerators. The implementation uses a lock.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nDefine a bounded observation-delivery contract beyond the ring capacity alone.\n\n**Expected benefit:** Define a bounded observation-delivery contract beyond the ring capacity alone.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Choose single-reader default or explicitly bounded competing-reader behavior; do not imply broadcast.\n- [ ] Specify disposal of an enumerator with a pending read, cancellation, fairness and loss counts.\n- [ ] Keep continuations asynchronous and payloads copied; measure lock contention rather than call it lock-free.\n\n### Acceptance criteria\n\n- [ ] Pending reader memory is bounded or excess readers are rejected.\n- [ ] Disposal/cancellation cannot strand pending reads.\n- [ ] Dropped observations never discard required native decisions.\n\n### Required validation\n\n- [ ] Many readers, canceled pending read, close/complete/dispose and overflow fixtures.\n- [ ] Publication latency under a slow consumer.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E07}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Blocks\n\n- {{issue:CLI-018}}\n\n### Package and readiness gate\n\nIdentify the first containing artifact before describing new source as consumable support. No version is invented by this plan.\n\n### Proposed branch and PR\n\nBranch: `feat/cli-019-bounded-event-consumers`\n\nPR title: **Bound event consumers and specify stream completion**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nChanging queue implementation for fashion can break loss/ordering/completion semantics; qualify the contract first.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C15 — Prior audit: bounded event ring, pending reader list and async continuations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/Events/BoundedEventStream.cs)\n- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-020", - "repository_key": "client", - "kind": "governance", - "title": "Standardize fluent terminals and supported workflow examples", - "parent": "CLI-E07", - "epic": "CLI-E07", - "milestone": "CLI-M4", - "priority": "P2", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "Existing immutable builders and explicit cardinality terminals are useful. Public interfaces also include unavailable domains that require clear status.", - "objective": "Improve discoverability without replacing the existing fluent model or exposing low-level complexity.", - "requirements": [ - "Keep configuration side-effect free and execution terminals explicit.", - "Describe ordering, absence, limits, cancellation milestones and cleanup near each supported operation.", - "Compile examples against shipped packages; label research APIs and unavailable domains before examples." - ], - "acceptance": [ - "Examples cover success, expected failure and cleanup for each supported workflow.", - "Fluent names match actual guarantees; compatibility aliases preserve old callers.", - "No claim of safe/atomic/async/zero-allocation lacks a precise contract." - ], - "tests": [ - "Template/example compilation in clean consumers.", - "Default builder, ambiguous cardinality and old public API tests." - ], - "risks": "A huge facade is not delivered capability. Avoid adding placeholder interfaces only to increase API count.", - "blocked_by": [ - "CLI-002", - "CLI-013" - ], - "sources": [ - "C01", - "C16", - "N03" - ], - "branch": "docs/cli-020-fluent-developer-guidance", - "pr_title": "Standardize fluent terminals and supported workflow examples", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan.", - "labels": [ - "ce:bootstrap", - "ce:kind:governance", - "ce:priority:P2" - ], - "path": "docs/engineering/work-items/CLI-020.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Improve discoverability without replacing the existing fluent model or exposing low-level complexity.", - "body_template": "\n\n## CLI-020 — Standardize fluent terminals and supported workflow examples\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** governance · **Priority:** P2 · **Milestone:** CLI-M4\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nExisting immutable builders and explicit cardinality terminals are useful. Public interfaces also include unavailable domains that require clear status.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nImprove discoverability without replacing the existing fluent model or exposing low-level complexity.\n\n**Expected benefit:** Improve discoverability without replacing the existing fluent model or exposing low-level complexity.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Keep configuration side-effect free and execution terminals explicit.\n- [ ] Describe ordering, absence, limits, cancellation milestones and cleanup near each supported operation.\n- [ ] Compile examples against shipped packages; label research APIs and unavailable domains before examples.\n\n### Acceptance criteria\n\n- [ ] Examples cover success, expected failure and cleanup for each supported workflow.\n- [ ] Fluent names match actual guarantees; compatibility aliases preserve old callers.\n- [ ] No claim of safe/atomic/async/zero-allocation lacks a precise contract.\n\n### Required validation\n\n- [ ] Template/example compilation in clean consumers.\n- [ ] Default builder, ambiguous cardinality and old public API tests.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E07}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\n- {{issue:CLI-002}}\n- {{issue:CLI-013}}\n\n### Blocks\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Package and readiness gate\n\nIdentify the first containing artifact before describing new source as consumable support. No version is invented by this plan.\n\n### Proposed branch and PR\n\nBranch: `docs/cli-020-fluent-developer-guidance`\n\nPR title: **Standardize fluent terminals and supported workflow examples**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nA huge facade is not delivered capability. Avoid adding placeholder interfaces only to increase API count.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md)\n- [C16 — Prior audit: immutable terminals, post-filter limits, readability naming.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Fluent/Scanning/AobScanBuilder.cs)\n- [N03 — Source/binary/behavioral compatibility distinctions.](https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/breaking-changes)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-021", - "repository_key": "client", - "kind": "feature", - "title": "Unify stale-client and local-process operation semantics", - "parent": "CLI-E07", - "epic": "CLI-E07", - "milestone": "CLI-M1", - "priority": "P2", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "Some local process enumeration/unavailable paths bypass normal Client dispatch. Local BCL metadata is not proof of CE backend target identity.", - "objective": "Make activation requirements explicit for stateful operations and separate legitimate offline diagnostics.", - "requirements": [ - "Apply stale-client policy consistently before operational work, even for unavailable implementations.", - "Label local process discovery/enrichment as local; no speculative remote backend.", - "Document cancellation ordering and keep pure immutable snapshots usable where intended." - ], - "acceptance": [ - "Disable/re-enable does not produce different lifetime behavior by domain accident.", - "Offline diagnostics have a separate explicit contract.", - "Local process metadata is never mistaken for authoritative SDK target identity." - ], - "tests": [ - "Stale facade across local, implemented and unavailable domains.", - "Canceled enumeration and disappearing local process fixtures." - ], - "risks": "Adding lifetime exceptions indiscriminately can remove useful offline diagnostics; name that boundary deliberately.", - "blocked_by": [], - "sources": [ - "C06", - "C14", - "C01" - ], - "branch": "feat/cli-021-stale-local-operation-policy", - "pr_title": "Unify stale-client and local-process operation semantics", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan.", - "labels": [ - "ce:bootstrap", - "ce:kind:feature", - "ce:priority:P2" - ], - "path": "docs/engineering/work-items/CLI-021.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Make activation requirements explicit for stateful operations and separate legitimate offline diagnostics.", - "body_template": "\n\n## CLI-021 — Unify stale-client and local-process operation semantics\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P2 · **Milestone:** CLI-M1\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nSome local process enumeration/unavailable paths bypass normal Client dispatch. Local BCL metadata is not proof of CE backend target identity.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nMake activation requirements explicit for stateful operations and separate legitimate offline diagnostics.\n\n**Expected benefit:** Make activation requirements explicit for stateful operations and separate legitimate offline diagnostics.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Apply stale-client policy consistently before operational work, even for unavailable implementations.\n- [ ] Label local process discovery/enrichment as local; no speculative remote backend.\n- [ ] Document cancellation ordering and keep pure immutable snapshots usable where intended.\n\n### Acceptance criteria\n\n- [ ] Disable/re-enable does not produce different lifetime behavior by domain accident.\n- [ ] Offline diagnostics have a separate explicit contract.\n- [ ] Local process metadata is never mistaken for authoritative SDK target identity.\n\n### Required validation\n\n- [ ] Stale facade across local, implemented and unavailable domains.\n- [ ] Canceled enumeration and disappearing local process fixtures.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E07}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Blocks\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Package and readiness gate\n\nIdentify the first containing artifact before describing new source as consumable support. No version is invented by this plan.\n\n### Proposed branch and PR\n\nBranch: `feat/cli-021-stale-local-operation-policy`\n\nPR title: **Unify stale-client and local-process operation semantics**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nAdding lifetime exceptions indiscriminately can remove useful offline diagnostics; name that boundary deliberately.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C06 — Prior audit: expected no-target failure and local target observations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/ProcessClient.cs)\n- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs)\n- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-022", - "repository_key": "client", - "kind": "validation", - "title": "Validate minimum SDK artifacts and public compatibility", - "parent": "CLI-E08", - "epic": "CLI-E08", - "milestone": "CLI-M6", - "priority": "P1", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "Client publishes a compatible SDK range, but source availability, package content and behavioral support are not equivalent.", - "objective": "Test packed Client consumption against the exact minimum and selected newer SDK artifacts.", - "requirements": [ - "Record source/package/bridge/generator identity for each test combination.", - "Preserve direct SDK bootstrap/native asset reference rules.", - "Test old source, old binaries where supported, application interface implementations and generated consumers." - ], - "acceptance": [ - "No sibling project reference masks missing published members.", - "Required interface changes and failure/cancellation semantics have migration notes.", - "The declared range is not described as a fully tested set of all possible packages." - ], - "tests": [ - "Minimal plugin, custom codec/module, relay library and old application fakes.", - "AOT graph publication separate from managed live plugin loading." - ], - "risks": "Moving public types between assemblies can break binary identity even with unchanged namespaces.", - "blocked_by": [ - "SDK-023", - "CLI-007", - "CLI-010", - "CLI-011" - ], - "sources": [ - "C19", - "C20", - "C01", - "N03" - ], - "branch": "test/cli-022-package-compatibility-matrix", - "pr_title": "Validate minimum SDK artifacts and public compatibility", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [ - "CLI-023", - "CLI-024" - ], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.", - "labels": [ - "ce:bootstrap", - "ce:kind:validation", - "ce:priority:P1" - ], - "path": "docs/engineering/work-items/CLI-022.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Test packed Client consumption against the exact minimum and selected newer SDK artifacts.", - "body_template": "\n\n## CLI-022 — Validate minimum SDK artifacts and public compatibility\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** validation · **Priority:** P1 · **Milestone:** CLI-M6\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nClient publishes a compatible SDK range, but source availability, package content and behavioral support are not equivalent.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nTest packed Client consumption against the exact minimum and selected newer SDK artifacts.\n\n**Expected benefit:** Test packed Client consumption against the exact minimum and selected newer SDK artifacts.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Record source/package/bridge/generator identity for each test combination.\n- [ ] Preserve direct SDK bootstrap/native asset reference rules.\n- [ ] Test old source, old binaries where supported, application interface implementations and generated consumers.\n\n### Acceptance criteria\n\n- [ ] No sibling project reference masks missing published members.\n- [ ] Required interface changes and failure/cancellation semantics have migration notes.\n- [ ] The declared range is not described as a fully tested set of all possible packages.\n\n### Required validation\n\n- [ ] Minimal plugin, custom codec/module, relay library and old application fakes.\n- [ ] AOT graph publication separate from managed live plugin loading.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E08}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\n- {{issue:SDK-023}}\n- {{issue:CLI-007}}\n- {{issue:CLI-010}}\n- {{issue:CLI-011}}\n\n### Blocks\n\n- {{issue:CLI-023}}\n- {{issue:CLI-024}}\n\n### Package and readiness gate\n\nBefore Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.\n\n### Proposed branch and PR\n\nBranch: `test/cli-022-package-compatibility-matrix`\n\nPR title: **Validate minimum SDK artifacts and public compatibility**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nMoving public types between assemblies can break binary identity even with unchanged namespaces.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C19 — Prior audit: SDK 1.0.0 resolution; artifact bytes not independently inspected.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/packages.lock.json)\n- [C20 — Prior audit: approved SDK values versus broad compile/runtime dependency.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Abstractions/CheatEngine.Client.Abstractions.csproj)\n- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md)\n- [N03 — Source/binary/behavioral compatibility distinctions.](https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/breaking-changes)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-023", - "repository_key": "client", - "kind": "feature", - "title": "Verify coherent deployment sets and external plugin inheritance", - "parent": "CLI-E08", - "epic": "CLI-E08", - "milestone": "CLI-M6", - "priority": "P2", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "Hosting build tooling performs per-file deployment and metadata inheritance checks. A build-only filesystem P/Invoke is not a runtime CE bypass.", - "objective": "Make deployment/profile validation truthful, failure-aware and compatible with documented plugin inheritance.", - "requirements": [ - "Produce a file/hash manifest and detect stale or mixed SDK/bridge/Client outputs.", - "Prefer versioned staging where supported; otherwise report partial replacement and require an explicit offline deployment policy.", - "Resolve external base metadata without executing user assemblies, or document the supported inheritance restriction." - ], - "acceptance": [ - "A failed second file replacement reports the installed subset.", - "Stale manifest-owned files are detected without deleting arbitrary user files.", - "An external shared base is either supported by a tested resolver or rejected with precise guidance." - ], - "tests": [ - "Partial deployment, locked file, missing bridge and stale-version tests.", - "Direct, same-assembly, external-base, manual/generated bootstrap consumer fixtures." - ], - "risks": "Do not claim directory atomicity or safe hot reload; do not automatically modify CE runtime configuration.", - "blocked_by": [ - "CLI-022" - ], - "sources": [ - "C17", - "C01", - "S11" - ], - "branch": "feat/cli-023-deployment-profile-integrity", - "pr_title": "Verify coherent deployment sets and external plugin inheritance", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [ - "CLI-024" - ], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan.", - "labels": [ - "ce:bootstrap", - "ce:kind:feature", - "ce:priority:P2" - ], - "path": "docs/engineering/work-items/CLI-023.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Make deployment/profile validation truthful, failure-aware and compatible with documented plugin inheritance.", - "body_template": "\n\n## CLI-023 — Verify coherent deployment sets and external plugin inheritance\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P2 · **Milestone:** CLI-M6\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nHosting build tooling performs per-file deployment and metadata inheritance checks. A build-only filesystem P/Invoke is not a runtime CE bypass.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nMake deployment/profile validation truthful, failure-aware and compatible with documented plugin inheritance.\n\n**Expected benefit:** Make deployment/profile validation truthful, failure-aware and compatible with documented plugin inheritance.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Produce a file/hash manifest and detect stale or mixed SDK/bridge/Client outputs.\n- [ ] Prefer versioned staging where supported; otherwise report partial replacement and require an explicit offline deployment policy.\n- [ ] Resolve external base metadata without executing user assemblies, or document the supported inheritance restriction.\n\n### Acceptance criteria\n\n- [ ] A failed second file replacement reports the installed subset.\n- [ ] Stale manifest-owned files are detected without deleting arbitrary user files.\n- [ ] An external shared base is either supported by a tested resolver or rejected with precise guidance.\n\n### Required validation\n\n- [ ] Partial deployment, locked file, missing bridge and stale-version tests.\n- [ ] Direct, same-assembly, external-base, manual/generated bootstrap consumer fixtures.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E08}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\n- {{issue:CLI-022}}\n\n### Blocks\n\n- {{issue:CLI-024}}\n\n### Package and readiness gate\n\nIdentify the first containing artifact before describing new source as consumable support. No version is invented by this plan.\n\n### Proposed branch and PR\n\nBranch: `feat/cli-023-deployment-profile-integrity`\n\nPR title: **Verify coherent deployment sets and external plugin inheritance**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nDo not claim directory atomicity or safe hot reload; do not automatically modify CE runtime configuration.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C17 — Prior audit: build-only native calls, external-base checks and per-file deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Hosting/buildTransitive/CheatEngine.Client.Hosting.targets)\n- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md)\n- [S11 — Protected native boundary and bridge contract reference.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/native/cheatengine-sdk-lua-bridge/README.md)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - }, - { - "id": "CLI-024", - "repository_key": "client", - "kind": "validation", - "title": "Publish end-to-end qualification and performance budgets", - "parent": "CLI-E08", - "epic": "CLI-E08", - "milestone": "CLI-M6", - "priority": "P2", - "evidence": "InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.", - "observation": "No .NET/live-host/benchmark execution was performed by the audit summaries or this bootstrap. Planned acceptance tests require actual recorded results.", - "objective": "Qualify the release scope from developer workflow through SDK/host behavior and cleanup.", - "requirements": [ - "Measure primitive, dispatch, batch, snapshot, codec, AOB and event paths separately.", - "Record CLR allocations, available Lua/native metrics, boundary calls and latency distributions.", - "Run declared success/failure/disable/re-enable/target-change scenarios on a purpose-built target." - ], - "acceptance": [ - "No performance improvement weakens error, lifetime, target or cancellation semantics.", - "Source/package/live/AOT publication results remain independently labeled.", - "Only qualified workflows are advertised; deferred advanced domains remain visible." - ], - "tests": [ - "Direct SDK versus immediate/queued Client and equal-semantics batch comparisons.", - "Controlled live plugin and two-plugin regression profiles." - ], - "risks": "Pooling retained contexts or exposing borrowed spans to claim zero allocations would invalidate the safety model.", - "blocked_by": [ - "CLI-022", - "CLI-023", - "SDK-024" - ], - "sources": [ - "C01", - "C21", - "C03", - "C15", - "S13" - ], - "branch": "test/cli-024-release-performance-evidence", - "pr_title": "Publish end-to-end qualification and performance budgets", - "scope_exclusions": [ - "No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.", - "Keep integration mechanisms in SDK and application policy/workflows in Client." - ], - "completion_artifacts": [ - "Focused PR linked to the implementation issue.", - "Commands, environment and actual regression results; unexecuted cases remain pending.", - "Updated contract/capability documentation and compatibility notes." - ], - "mandatory": "Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.", - "children": [], - "blocks": [], - "status": "Needs refinement", - "repository": "CheatEngineNet/CheatEngine.Client", - "artifact_gate": "Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.", - "labels": [ - "ce:bootstrap", - "ce:kind:validation", - "ce:priority:P2" - ], - "path": "docs/engineering/work-items/CLI-024.md", - "long_term_impact": "One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.", - "benefits": "Qualify the release scope from developer workflow through SDK/host behavior and cleanup.", - "body_template": "\n\n## CLI-024 — Publish end-to-end qualification and performance budgets\n\n**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** validation · **Priority:** P2 · **Milestone:** CLI-M6\n\n**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap.\n\n### Context and evidence\n\nNo .NET/live-host/benchmark execution was performed by the audit summaries or this bootstrap. Planned acceptance tests require actual recorded results.\n\n**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation.\n\n### Outcome and rationale\n\nQualify the release scope from developer workflow through SDK/host behavior and cleanup.\n\n**Expected benefit:** Qualify the release scope from developer workflow through SDK/host behavior and cleanup.\n\n### Architectural responsibility\n\nSDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience.\n\n### Technical requirements and task checklist\n\n- [ ] Measure primitive, dispatch, batch, snapshot, codec, AOB and event paths separately.\n- [ ] Record CLR allocations, available Lua/native metrics, boundary calls and latency distributions.\n- [ ] Run declared success/failure/disable/re-enable/target-change scenarios on a purpose-built target.\n\n### Acceptance criteria\n\n- [ ] No performance improvement weakens error, lifetime, target or cancellation semantics.\n- [ ] Source/package/live/AOT publication results remain independently labeled.\n- [ ] Only qualified workflows are advertised; deferred advanced domains remain visible.\n\n### Required validation\n\n- [ ] Direct SDK versus immediate/queued Client and equal-semantics batch comparisons.\n- [ ] Controlled live plugin and two-plugin regression profiles.\n\n### Scope exclusions\n\n- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes.\n- Keep integration mechanisms in SDK and application policy/workflows in Client.\n\n### Parent and children\n\nParent: {{issue:CLI-E08}}\n\nImplementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome.\n\n### Blocked by\n\n- {{issue:CLI-022}}\n- {{issue:CLI-023}}\n- {{issue:SDK-024}}\n\n### Blocks\n\nNone declared. This does not waive evidence, policy, or package requirements.\n\n### Package and readiness gate\n\nBefore Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions.\n\n### Proposed branch and PR\n\nBranch: `test/cli-024-release-performance-evidence`\n\nPR title: **Publish end-to-end qualification and performance budgets**\n\nOpen against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch.\n\n### Risks and compatibility\n\nPooling retained contexts or exposing borrowed spans to claim zero allocations would invalidate the safety model.\n\n**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable.\n\n### Definition of done\n\n- [ ] Focused PR linked to the implementation issue.\n- [ ] Commands, environment and actual regression results; unexecuted cases remain pending.\n- [ ] Updated contract/capability documentation and compatibility notes.\n\nRequired for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented.\n\n### Sources\n\n- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md)\n- [C21 — Live gate protocol identified by current README; not independently re-read here.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/docs/live-capability-gates.md)\n- [C03 — Prior audit: scoped codecs, partial batches and failure projection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/MemoryClient.cs)\n- [C15 — Prior audit: bounded event ring, pending reader list and async continuations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/Events/BoundedEventStream.cs)\n- [S13 — Live fixture entry point, identified in contribution guidance.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/tests/CheatEngine.SDK.LivePlugin/README.md)\n\n### Maintainer notes\n\nKeep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun.\n" - } - ], - "milestones": [ - { - "id": "CLI-M0", - "repository_key": "client", - "title": "M0 — Governance and capability truth", - "objective": "Governance and capability truth", - "entry_criteria": "Issue-level prerequisites satisfied; this milestone is an outcome grouping, not an artificial global serial lock.", - "exit_criteria": "Public status separates implementation, artifact, host qualification and policy.", - "due_on": null, - "release_version": null - }, - { - "id": "CLI-M1", - "repository_key": "client", - "title": "M1 — Reliability and activation integrity", - "objective": "Reliability and activation integrity", - "entry_criteria": "Issue-level prerequisites satisfied; this milestone is an outcome grouping, not an artificial global serial lock.", - "exit_criteria": "No-target, retained-codec and construction-rollback tests match production semantics.", - "due_on": null, - "release_version": null - }, - { - "id": "CLI-M2", - "repository_key": "client", - "title": "M2 — SDK boundary adoption", - "objective": "SDK boundary adoption", - "entry_criteria": "Issue-level prerequisites satisfied; this milestone is an outcome grouping, not an artificial global serial lock.", - "exit_criteria": "Built-in integration delegates to shipped SDK contracts with preserved outcomes.", - "due_on": null, - "release_version": null - }, - { - "id": "CLI-M3", - "repository_key": "client", - "title": "M3 — Modules and public API boundaries", - "objective": "Modules and public API boundaries", - "entry_criteria": "Issue-level prerequisites satisfied; this milestone is an outcome grouping, not an artificial global serial lock.", - "exit_criteria": "Generated public contracts contain no raw state/owner escape; activation/module lifetime tested.", - "due_on": null, - "release_version": null - }, - { - "id": "CLI-M4", - "repository_key": "client", - "title": "M4 — Workflow and fluent correctness", - "objective": "Workflow and fluent correctness", - "entry_criteria": "Issue-level prerequisites satisfied; this milestone is an outcome grouping, not an artificial global serial lock.", - "exit_criteria": "Cardinality, partial effects, budgets and fluent names are truthful and documented.", - "due_on": null, - "release_version": null - }, - { - "id": "CLI-M5", - "repository_key": "client", - "title": "M5 — Evidence-gated advanced workflows", - "objective": "Evidence-gated advanced workflows", - "entry_criteria": "Issue-level prerequisites satisfied; this milestone is an outcome grouping, not an artificial global serial lock.", - "exit_criteria": "Only workflows with target/ownership/cleanup evidence are enabled.", - "due_on": null, - "release_version": null - }, - { - "id": "CLI-M6", - "repository_key": "client", - "title": "M6 — Packaging and developer readiness", - "objective": "Packaging and developer readiness", - "entry_criteria": "Issue-level prerequisites satisfied; this milestone is an outcome grouping, not an artificial global serial lock.", - "exit_criteria": "Clean consumer/template/deployment/live results independently recorded.", - "due_on": null, - "release_version": null - } - ], - "external_dependencies": [ - { - "blocker": "SDK-008", - "blocked": "CLI-007", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-023", - "blocked": "CLI-007", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-007", - "blocked": "CLI-008", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-023", - "blocked": "CLI-008", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-010", - "blocked": "CLI-009", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-011", - "blocked": "CLI-009", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-023", - "blocked": "CLI-009", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-009", - "blocked": "CLI-010", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-022", - "blocked": "CLI-010", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-023", - "blocked": "CLI-010", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-005", - "blocked": "CLI-012", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-014", - "blocked": "CLI-013", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-023", - "blocked": "CLI-013", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-013", - "blocked": "CLI-014", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-021", - "blocked": "CLI-015", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-023", - "blocked": "CLI-015", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-015", - "blocked": "CLI-016", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-023", - "blocked": "CLI-016", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-011", - "blocked": "CLI-017", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-017", - "blocked": "CLI-017", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-023", - "blocked": "CLI-017", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-018", - "blocked": "CLI-018", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-019", - "blocked": "CLI-018", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-023", - "blocked": "CLI-022", - "cross_repository": true, - "relationship": "blocked_by" - }, - { - "blocker": "SDK-024", - "blocked": "CLI-024", - "cross_repository": true, - "relationship": "blocked_by" - } - ], - "evidence_limits": [ - "The review archive is validated context, not a source clone; no exact finding-ID reconciliation is claimed.", - "Pinned source references marked PriorAuditReference were not all re-read in this bootstrap.", - "No repository compilation, .NET tests, live CE tests or benchmarks executed.", - "Four connector mutations were attempted and all returned 403; no successful remote writes." - ] -} diff --git a/docs/engineering/work-items/CLI-001.md b/docs/engineering/work-items/CLI-001.md deleted file mode 100644 index 9fb1bdd..0000000 --- a/docs/engineering/work-items/CLI-001.md +++ /dev/null @@ -1,96 +0,0 @@ - - -## CLI-001 — Reconcile Client audit evidence with source and consumed packages - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** governance · **Priority:** P1 · **Milestone:** CLI-M0 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -The Client main revision matches the prior review. The prior complete ZIP is not locally available, and existing high-level APIs from PR #7 must not be recreated as missing. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Establish Client-specific provenance and a truthful theme-to-work-item crosswalk. - -**Expected benefit:** Establish Client-specific provenance and a truthful theme-to-work-item crosswalk. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Record Client/SDK commits, the actually consumed nupkg and source-only additions separately. -- [ ] Import original finding/ADR/test identifiers only from recovered original registers. -- [ ] Retain unavailable advanced implementations until their individual gates pass. - -### Acceptance criteria - -- [ ] No invented original finding ID or assertion of exhaustive archive coverage. -- [ ] Each Client adoption item identifies its corresponding SDK work item. -- [ ] Historical source statements are revalidated before changes. - -### Required validation - -- [ ] Ledger rejects source present as proof of packaged/live support. -- [ ] Cross-repository reference validator catches unresolved planning IDs. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/9 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -None declared. This does not waive evidence, policy, or package requirements. - -### Blocks - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/18 - -### Package and readiness gate - -Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan. - -### Proposed branch and PR - -Branch: `docs/cli-001-evidence-reconciliation` - -PR title: **Reconcile Client audit evidence with source and consumed packages** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -The summaries are a usable planning basis, not a substitute for all detailed audit registers. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md) -- [C19 — Prior audit: SDK 1.0.0 resolution; artifact bytes not independently inspected.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/packages.lock.json) -- [H02 — Merged high-level API/modules; preserve this work.](https://github.com/CheatEngineNet/CheatEngine.Client/pull/7) -- [H01 — Merged foundations; author-reported tests are not independently executed.](https://github.com/CheatEngineNet/CheatEngine.SDK/pull/19) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-002.md b/docs/engineering/work-items/CLI-002.md deleted file mode 100644 index ac0f063..0000000 --- a/docs/engineering/work-items/CLI-002.md +++ /dev/null @@ -1,96 +0,0 @@ - - -## CLI-002 — Separate capability implementation, host evidence, and policy - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P1 · **Milestone:** CLI-M0 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -Runtime capability probes and unavailable service registrations describe different support dimensions. Opting into unsafe Lua is policy, not proof of working host support. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Expose a small immutable capability model with explicit reasons and independently sourced dimensions. - -**Expected benefit:** Expose a small immutable capability model with explicit reasons and independently sourced dimensions. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Distinguish implemented, available in package, observed in host, live-qualified, policy-allowed and active-lifetime prerequisites. -- [ ] Do not treat every probe exception as unavailability; preserve unavailable versus present-but-failing. -- [ ] Generate documentation from the same ledger without auto-promoting discovered symbols. - -### Acceptance criteria - -- [ ] Every unavailable public domain explains its actual missing gate. -- [ ] Enabling an option cannot manufacture host evidence. -- [ ] Existing supported workflows remain discoverable without raw SDK details. - -### Required validation - -- [ ] Missing, throwing and malformed runtime global cases. -- [ ] Policy combinations and unavailable-adapter status tests. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/9 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/17 - -### Blocks - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/32 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/36 - -### Package and readiness gate - -Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan. - -### Proposed branch and PR - -Branch: `feat/cli-002-capability-evidence-model` - -PR title: **Separate capability implementation, host evidence, and policy** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -Avoid a large generic capability framework; deterministic records and reasoned derivation are sufficient. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C09 — Prior audit: capability and exception-family mismatch.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/RuntimeClient.cs) -- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs) -- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-003.md b/docs/engineering/work-items/CLI-003.md deleted file mode 100644 index b746e5a..0000000 --- a/docs/engineering/work-items/CLI-003.md +++ /dev/null @@ -1,97 +0,0 @@ - - -## CLI-003 — Adopt Client governance, roadmap, and issue-to-PR workflow - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** governance · **Priority:** P1 · **Milestone:** CLI-M0 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -Client has a mature README and existing ADR/build/live gates. This bootstrap must add management structure, not replace architecture or release policy. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Adopt additive planning documents, native issue hierarchy, blockers and review templates. - -**Expected benefit:** Adopt additive planning documents, native issue hierarchy, blockers and review templates. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Keep Issues as current execution state and Markdown as scope/architecture authority. -- [ ] Use focused branches from main only after readiness; no empty implementation branches for the entire backlog. -- [ ] Keep cross-repository blockers explicit and verify actual GitHub metadata separately from local plans. - -### Acceptance criteria - -- [ ] The documentation/tooling PR contains no product runtime changes. -- [ ] Roadmap, milestone, epic and task links are navigable after deployment. -- [ ] No automated merge, publication, reviewer assignment or branch protection change. - -### Required validation - -- [ ] Manifest, DAG, source and local link checks. -- [ ] Two-run deployment idempotency in a test repository. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/9 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -None declared. This does not waive evidence, policy, or package requirements. - -### Blocks - -None declared. This does not waive evidence, policy, or package requirements. - -### Package and readiness gate - -Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan. - -### Proposed branch and PR - -Branch: `docs/cli-003-engineering-governance` - -PR title: **Adopt Client governance, roadmap, and issue-to-PR workflow** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -A PR is not a deployment of native project metadata, and a Markdown checklist is not a native sub-issue relation. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md) -- [G01 — Issue API; filter pull_request entries from issue lists.](https://docs.github.com/en/rest/issues/issues) -- [G02 — Repository-specific milestones.](https://docs.github.com/en/rest/issues/milestones) -- [G03 — Native hierarchy uses issue IDs, not issue numbers.](https://docs.github.com/en/rest/issues/sub-issues) -- [G04 — Native blocked-by relationship with the blocker issue_id.](https://docs.github.com/en/rest/issues/issue-dependencies) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-004.md b/docs/engineering/work-items/CLI-004.md deleted file mode 100644 index 012b036..0000000 --- a/docs/engineering/work-items/CLI-004.md +++ /dev/null @@ -1,94 +0,0 @@ - - -## CLI-004 — Align expected process failures with production dispatcher semantics - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** bug · **Priority:** P1 · **Milestone:** CLI-M1 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -The prior audit traces expected no-target exceptions through a dispatcher that rethrows callback exceptions; a test fake can instead return false and mask the mismatch. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Return expected domain failures as typed outcomes without swallowing application callback bugs. - -**Expected benefit:** Return expected domain failures as typed outcomes without swallowing application callback bugs. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Classify no target, disappeared process and missing local metadata inside ProcessClient. -- [ ] Keep the general dispatcher policy for programmer/user callback exceptions explicit. -- [ ] Make contract fakes reproduce production exception transport rather than an easier imagined adapter. - -### Acceptance criteria - -- [ ] TryGetCurrent with PID zero returns the documented TargetNotAttached outcome. -- [ ] An arbitrary user Action exception still follows the dispatcher contract. -- [ ] Race-to-exit lookup failures have stable, documented semantics. - -### Required validation - -- [ ] Production-semantics dispatcher plus controlled process host. -- [ ] Composed regression for no-target, disappeared target and throwing user callback. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/10 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -None declared. This does not waive evidence, policy, or package requirements. - -### Blocks - -None declared. This does not waive evidence, policy, or package requirements. - -### Package and readiness gate - -Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan. - -### Proposed branch and PR - -Branch: `fix/cli-004-process-try-semantics` - -PR title: **Align expected process failures with production dispatcher semantics** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -Changing every dispatcher exception into false would hide defects and break a different contract. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C05 — Prior audit: production adapter rethrows callback exceptions.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs) -- [C06 — Prior audit: expected no-target failure and local target observations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/ProcessClient.cs) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-005.md b/docs/engineering/work-items/CLI-005.md deleted file mode 100644 index b31988f..0000000 --- a/docs/engineering/work-items/CLI-005.md +++ /dev/null @@ -1,95 +0,0 @@ - - -## CLI-005 — Expire memory-codec contexts after each invocation - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** bug · **Priority:** P1 · **Milestone:** CLI-M1 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -The prior audit describes a retainable reference context whose methods call SDK memory directly without an invocation expiration guard; pointer width is cached in that context. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Make custom-codec access valid only inside its original invocation. - -**Expected benefit:** Make custom-codec access valid only inside its original invocation. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Expire in finally on successful and failing codec calls. -- [ ] Validate active invocation, activation identity and allowed thread before any SDK operation, including scoped metadata access. -- [ ] Use current SDK facts now and strengthen target binding when the authoritative target contract is adopted; do not delay the local expiration fix. - -### Acceptance criteria - -- [ ] Retained references fail before invoking SDK after return or exception. -- [ ] Cross-thread and re-enable reuse cannot use a recycled invocation. -- [ ] Do not pool contexts while an old reference could reach a later invocation. - -### Required validation - -- [ ] Capture public context and invoke afterward with SDK call counters. -- [ ] PointerSize after expiration, codec exception, worker use and re-enable. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/10 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -None declared. This does not waive evidence, policy, or package requirements. - -### Blocks - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/30 - -### Package and readiness gate - -Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan. - -### Proposed branch and PR - -Branch: `fix/cli-005-codec-context-lifetime` - -PR title: **Expire memory-codec contexts after each invocation** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -A ref-struct-only public redesign would be a separate compatibility decision; a small expiration guard can close the immediate lifetime gap. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C03 — Prior audit: scoped codecs, partial batches and failure projection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/MemoryClient.cs) -- [C04 — Public extension context with an implementation-enforced lifetime.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Abstractions/Memory/IMemoryReadContext.cs) -- [C07 — Prior audit: early construction cleanup differs from normal cleanup.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Hosting/CheatEngineClientPlugin.cs) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-006.md b/docs/engineering/work-items/CLI-006.md deleted file mode 100644 index 31bb94d..0000000 --- a/docs/engineering/work-items/CLI-006.md +++ /dev/null @@ -1,95 +0,0 @@ - - -## CLI-006 — Complete all activation rollback stages and preserve original errors - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** bug · **Priority:** P1 · **Milestone:** CLI-M1 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -Prior review observes sequential unprotected scope/provider/configuration cleanup in partial CreateActivation, while normal cleanup already aggregates failures. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Use a consistent all-stages cleanup policy for partially and fully constructed activations. - -**Expected benefit:** Use a consistent all-stages cleanup policy for partially and fully constructed activations. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Attempt each owned cleanup stage independently and exactly as ownership permits. -- [ ] Preserve original construction failure before secondary cleanup diagnostics. -- [ ] Keep CE resource/module teardown before SDK detach; avoid double DI ownership. - -### Acceptance criteria - -- [ ] A throwing scope Dispose does not skip provider/configuration cleanup. -- [ ] The original enable failure remains identifiable. -- [ ] Normal disable order is preserved and repeated teardown is controlled. - -### Required validation - -- [ ] Failure during service resolution plus failing scope/provider/configuration disposables. -- [ ] Module-enable rollback and re-enable after failed activation. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/10 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -None declared. This does not waive evidence, policy, or package requirements. - -### Blocks - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/26 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/28 - -### Package and readiness gate - -Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan. - -### Proposed branch and PR - -Branch: `fix/cli-006-activation-rollback` - -PR title: **Complete all activation rollback stages and preserve original errors** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -Disposal aggregation is not permission to destroy a shared service twice or detach before admitted work drains. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C07 — Prior audit: early construction cleanup differs from normal cleanup.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Hosting/CheatEngineClientPlugin.cs) -- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-007.md b/docs/engineering/work-items/CLI-007.md deleted file mode 100644 index 1e968c1..0000000 --- a/docs/engineering/work-items/CLI-007.md +++ /dev/null @@ -1,96 +0,0 @@ - - -## CLI-007 — Replace built-in CE Lua declarations with SDK semantic operations - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P1 · **Milestone:** CLI-M2 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -The Client currently owns built-in CE names/signatures through ClientLuaGlobals despite using the SDK generator. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Make Client Core a semantic adapter, not a second CE mapping catalog. - -**Expected benefit:** Make Client Core a semantic adapter, not a second CE mapping catalog. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Replace every built-in declaration with its corresponding published SDK operation. -- [ ] Retain exact-name process ambiguity policy, table-root authorization and user-facing workflows. -- [ ] Update runtime architecture interpretation consistently instead of independently decoding targetIs64Bit. - -### Acceptance criteria - -- [ ] The reviewed eleven-name correspondence has no untracked duplicates. -- [ ] Unavailable/throwing/malformed outcomes retain SDK origin. -- [ ] Required minimum SDK artifact is part of the package contract. - -### Required validation - -- [ ] Semantic adapter parity fixtures for process/runtime/symbol/table operations. -- [ ] Clean consumer restore against minimum SDK artifact. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/11 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/36 -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/51 - -### Blocks - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/38 - -### Package and readiness gate - -Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions. - -### Proposed branch and PR - -Branch: `feat/cli-007-adopt-sdk-semantic-operations` - -PR title: **Replace built-in CE Lua declarations with SDK semantic operations** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -Do not remove working mappings before the replacement package is consumable; record temporary exceptions explicitly. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C02 — Prior audit: built-in CE Lua declarations remain Client-owned.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Infrastructure/ClientLuaGlobals.cs) -- [C09 — Prior audit: capability and exception-family mismatch.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/RuntimeClient.cs) -- [C19 — Prior audit: SDK 1.0.0 resolution; artifact bytes not independently inspected.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/packages.lock.json) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-008.md b/docs/engineering/work-items/CLI-008.md deleted file mode 100644 index 077029e..0000000 --- a/docs/engineering/work-items/CLI-008.md +++ /dev/null @@ -1,100 +0,0 @@ - - -## CLI-008 — Preserve SDK failure provenance and partial effect information - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P1 · **Milestone:** CLI-M2 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -Prior audit: memory failures become strings/generic errors and generated false results may be labeled LuaError regardless of original cause. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Project SDK outcomes losslessly while keeping normal Client contracts handle-free. - -**Expected benefit:** Project SDK outcomes losslessly while keeping normal Client contracts handle-free. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Separate misuse, expected failure and integration failure. -- [ ] Keep optional diagnostics cold-path; do not parse message text. -- [ ] Add completed-step/effect fields for state-changing workflows and document compatibility changes. - -### Acceptance criteria - -- [ ] Missing global, host rejection, malformed result and Lua error remain distinct. -- [ ] An empty successful result does not mask unavailable capability. -- [ ] A post-mutation refresh failure does not imply no effect. - -### Required validation - -- [ ] Cross-layer outcome table using actual SDK fixture behavior. -- [ ] Old consumer compilation and exception/behavior compatibility tests. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/11 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/35 -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/51 - -### Blocks - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/29 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/30 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/31 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/33 - -### Package and readiness gate - -Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions. - -### Proposed branch and PR - -Branch: `feat/cli-008-lossless-sdk-outcomes` - -PR title: **Preserve SDK failure provenance and partial effect information** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -A blanket result redesign can break application fakes; prefer additive overloads/adapters. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C03 — Prior audit: scoped codecs, partial batches and failure projection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/MemoryClient.cs) -- [C13 — Prior audit: operation/registration generation and recursive type checks.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/source-generators/CheatEngine.Client.SourceGenerators.Lua/CheatEngineLuaGenerator.cs) -- [C11 — Prior audit: SDK delegation but ambiguous false becomes rejection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/SdkAobScanPort.cs) -- [N03 — Source/binary/behavioral compatibility distinctions.](https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/breaking-changes) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-009.md b/docs/engineering/work-items/CLI-009.md deleted file mode 100644 index e9c5a26..0000000 --- a/docs/engineering/work-items/CLI-009.md +++ /dev/null @@ -1,102 +0,0 @@ - - -## CLI-009 — Bind Client workflows to authoritative SDK target identity - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P1 · **Milestone:** CLI-M2 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -Client observes selection changes through local snapshots. SDK-owned target identity is needed for future allocations, patches and stale queued operations. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Capture SDK target/activation authority for effectful workflow steps and high-level leases. - -**Expected benefit:** Capture SDK target/activation authority for effectful workflow steps and high-level leases. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Validate captured identity at actual execution, not only before waiting for dispatch. -- [ ] Keep pure copied snapshots distinct from live operations. -- [ ] Never release an old lease through the new current target or invisibly switch targets during cleanup. - -### Acceptance criteria - -- [ ] External target change invalidates affected workflow leases. -- [ ] PID reuse/A-to-B-to-A cannot silently reactivate an old lease. -- [ ] The local epoch is only a Client policy marker, not duplicated native authority. - -### Required validation - -- [ ] Queued work plus external target switch. -- [ ] Stale lease method/release, terminated process and same PID/new incarnation. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/11 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/38 -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/39 -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/51 - -### Blocks - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/29 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/30 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/31 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/32 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/33 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/34 - -### Package and readiness gate - -Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions. - -### Proposed branch and PR - -Branch: `feat/cli-009-target-bound-workflows` - -PR title: **Bind Client workflows to authoritative SDK target identity** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -Only explicit original-target cleanup or safe refusal is acceptable under uncertain identity. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C08 — Prior audit: local observed epoch is not authoritative target identity.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Infrastructure/TargetSelectionLifetime.cs) -- [C06 — Prior audit: expected no-target failure and local target observations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/ProcessClient.cs) -- [C05 — Prior audit: production adapter rethrows callback exceptions.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-010.md b/docs/engineering/work-items/CLI-010.md deleted file mode 100644 index a70bc82..0000000 --- a/docs/engineering/work-items/CLI-010.md +++ /dev/null @@ -1,98 +0,0 @@ - - -## CLI-010 — Use SDK registration leases for generated Lua modules - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P1 · **Milestone:** CLI-M3 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -The Client generator emits stack preflight/rollback while SDK generation already owns marshalling and closure generation. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Retain Client descriptors/order/policy and delegate registration safety to SDK leases. - -**Expected benefit:** Retain Client descriptors/order/policy and delegate registration safety to SDK leases. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Generate module descriptors and calls to the SDK registration contract instead of raw stack mechanisms. -- [ ] Keep application collision policy explicit and release modules in reverse activation order. -- [ ] Preserve a later replacement global when disposing the original module registration. - -### Acceptance criteria - -- [ ] Client-generated built-in module code does not implement a second low-level registration algorithm. -- [ ] Partial enable failure releases only owned registrations. -- [ ] Retained function values cannot call disposed plugin code. - -### Required validation - -- [ ] Compiled consumer with two modules, name replacement and partial registration failure. -- [ ] Disable/re-enable and Lua reset fixtures. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/12 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/37 -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/50 -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/51 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/22 - -### Blocks - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/38 - -### Package and readiness gate - -Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions. - -### Proposed branch and PR - -Branch: `feat/cli-010-sdk-module-registration` - -PR title: **Use SDK registration leases for generated Lua modules** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -Client-local reservations alone cannot serialize arbitrary scripts or other plugins. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C13 — Prior audit: operation/registration generation and recursive type checks.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/source-generators/CheatEngine.Client.SourceGenerators.Lua/CheatEngineLuaGenerator.cs) -- [S06 — Prior audit: generation stamps but name-based unregister.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaEmit/LuaRegistrationEmitter.cs) -- [C07 — Prior audit: early construction cleanup differs from normal cleanup.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Hosting/CheatEngineClientPlugin.cs) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-011.md b/docs/engineering/work-items/CLI-011.md deleted file mode 100644 index 24dfe28..0000000 --- a/docs/engineering/work-items/CLI-011.md +++ /dev/null @@ -1,95 +0,0 @@ - - -## CLI-011 — Enforce recursive public and generated API type boundaries - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** bug · **Priority:** P1 · **Milestone:** CLI-M3 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -The prior review identifies shallow result-type checks and a generic branch that can bypass subsequent forbidden-definition checks. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Inspect the complete public type graph while allowing deliberately approved SDK value contracts. - -**Expected benefit:** Inspect the complete public type graph while allowing deliberately approved SDK value contracts. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Traverse generic definitions/arguments, arrays, constraints, nested returns and emitted code. -- [ ] Reject LuaState, raw CE handles, ABI records, callbacks and ownership wrappers from normal Client contracts. -- [ ] Keep approved immutable addresses/IDs/enums; do not clone every SDK value or split packages prematurely. - -### Acceptance criteria - -- [ ] Nested forbidden types are rejected consistently by tests/generator diagnostics. -- [ ] Allowed immutable SDK values remain usable. -- [ ] Generated consumer compilation tests the boundary, not only source strings. - -### Required validation - -- [ ] Nested generic/array/constraint positives and negatives. -- [ ] Old application fake implementations and default builder tests. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/12 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -None declared. This does not waive evidence, policy, or package requirements. - -### Blocks - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/38 - -### Package and readiness gate - -Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan. - -### Proposed branch and PR - -Branch: `fix/cli-011-recursive-type-boundaries` - -PR title: **Enforce recursive public and generated API type boundaries** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -Compile-time architecture checks are not a security sandbox. Type moves can also break assembly identity. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C13 — Prior audit: operation/registration generation and recursive type checks.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/source-generators/CheatEngine.Client.SourceGenerators.Lua/CheatEngineLuaGenerator.cs) -- [C20 — Prior audit: approved SDK values versus broad compile/runtime dependency.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Abstractions/CheatEngine.Client.Abstractions.csproj) -- [N03 — Source/binary/behavioral compatibility distinctions.](https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/breaking-changes) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-012.md b/docs/engineering/work-items/CLI-012.md deleted file mode 100644 index 317ffd9..0000000 --- a/docs/engineering/work-items/CLI-012.md +++ /dev/null @@ -1,108 +0,0 @@ - - -## CLI-012 — Specify activation-local DI and multi-plugin composition - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** validation · **Priority:** P2 · **Milestone:** CLI-M3 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -The plugin creates a fresh provider per enable, with services registered as singletons within that provider. Extra scopes in a persistent provider do not recreate those singletons. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Make the supported composition lifetime explicit and test it independently of SDK load-context isolation. - -**Expected benefit:** Make the supported composition lifetime explicit and test it independently of SDK load-context isolation. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Document one provider per activation and ownership of DI-created disposables. -- [ ] Test aliases, modules and options without duplicate disposal. -- [ ] Qualify simultaneous plugins against the SDK coexistence result; do not infer it from separate providers. - -### Acceptance criteria - -- [ ] Every enable receives fresh Client lifetime and module state. -- [ ] Two scopes in one provider have documented behavior. -- [ ] No promise of persistent-root hosting without an explicit activation factory design. - -### Required validation - -- [ ] Provider/scope/alias disposal matrix. -- [ ] Two plugins in one host after SDK coexistence qualification. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/12 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/22 -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/33 - -### Blocks - -None declared. This does not waive evidence, policy, or package requirements. - -### Package and readiness gate - -Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions. - -### Proposed branch and PR - -Branch: `test/cli-012-activation-di-contract` - -PR title: **Specify activation-local DI and multi-plugin composition** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -Changing all registrations to scoped mechanically can change semantics and does not solve shared SDK static state. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C07 — Prior audit: early construction cleanup differs from normal cleanup.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Hosting/CheatEngineClientPlugin.cs) -- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs) -- [S08 — Prior audit: static host state and runtime admission.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/libs/CheatEngine.SDK.Hosting/Bootstrap/PluginHost.cs) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. - -2026-09-21 — Prepared a follow-up coexistence fixture increment after the post-merge acceptance audit. The runner builds -fresh, disjoint A/B/collision plugin closures, validates their declared runtime/native assets and hashes, and emits a -`BuildPrepared_NotLiveQualified` receipt. The fixture now has an explicit overlapping Lua-export contender, a -disable-one/survive-other Lua assertion sequence, and opt-in target/retained-owner probe commands. No Cheat Engine host -or target process was started, inspected, attached, configured, or modified by this preparation. - -CLI-012 remains open. Exact blockers are: (1) no verified controlled Cheat Engine 7.7 x64 host profile and transcript; -(2) no two authorised disposable target fixtures for the selection-change extension; (3) no exact qualified -side-by-side Client/SDK package tuple and host-loader observation; and (4) the released Client/SDK 1.0.0 tuple reports -allocation ownership as `CapabilityUnavailable`, so retained-owner behavior cannot be counted as a passing result. -The SDK PR #56 merge is lifecycle/source evidence, not a completed live coexistence qualification. diff --git a/docs/engineering/work-items/CLI-013.md b/docs/engineering/work-items/CLI-013.md deleted file mode 100644 index 7b51f24..0000000 --- a/docs/engineering/work-items/CLI-013.md +++ /dev/null @@ -1,98 +0,0 @@ - - -## CLI-013 — Make AOB cardinality and result budgets truthful - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P1 · **Milestone:** CLI-M4 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -Current scanning runs before module resolution and post-filters native results. FirstOrNone cannot safely infer no-match from an ambiguous SDK false. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Compose qualified SDK scan outcomes into predictable fluent cardinality semantics. - -**Expected benefit:** Compose qualified SDK scan outcomes into predictable fluent cardinality semantics. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Resolve absent/invalid module requests before expensive native work when target consistency permits. -- [ ] Map qualified no-match to success/null, not every false. -- [ ] Differentiate copied-result caps from native workload bounds and rename ReadableExecutable with a compatibility alias matching actual flags. - -### Acceptance criteria - -- [ ] FirstOrNone, RequireSingle and Take behave distinctly on zero/one/many matches. -- [ ] A missing module proven before admission does not launch a full scan. -- [ ] Documentation never claims Take(1) bounds CE scan work. - -### Required validation - -- [ ] No-hit/error/malformed outcomes and uniqueness/truncation tests. -- [ ] Module/range boundaries and protection-name compatibility examples. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/13 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/42 -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/51 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/24 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/25 - -### Blocks - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/36 - -### Package and readiness gate - -Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions. - -### Proposed branch and PR - -Branch: `feat/cli-013-aob-cardinality-budgets` - -PR title: **Make AOB cardinality and result budgets truthful** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -Early-stop cannot prove uniqueness; post-filtering and ordering must remain explicit. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C10 — Prior audit: whole scan precedes module lookup and post-filtering.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/PatternScanner.cs) -- [C11 — Prior audit: SDK delegation but ambiguous false becomes rejection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/SdkAobScanPort.cs) -- [C16 — Prior audit: immutable terminals, post-filter limits, readability naming.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Fluent/Scanning/AobScanBuilder.cs) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-014.md b/docs/engineering/work-items/CLI-014.md deleted file mode 100644 index 77e1172..0000000 --- a/docs/engineering/work-items/CLI-014.md +++ /dev/null @@ -1,98 +0,0 @@ - - -## CLI-014 — Report memory batch effects and cancellation milestones - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P1 · **Milestone:** CLI-M4 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -Homogeneous batches reduce dispatch count but still perform sequential primitive calls. Failure indices in text do not provide a stable partial-effect contract. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Expose completed prefix, failed/canceled step and effect certainty for composed memory workflows. - -**Expected benefit:** Expose completed prefix, failed/canceled step and effect certainty for composed memory workflows. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Preserve current admission-only cancellation where promised; make between-step cancellation an explicit policy. -- [ ] Copy span input before it outlives its synchronous caller; do not introduce dangling zero-copy buffers. -- [ ] Keep pointer-width/address arithmetic in qualified SDK primitives. - -### Acceptance criteria - -- [ ] Failure at k reports which prior operations completed. -- [ ] Cancellation never claims to interrupt an already-running synchronous CE primitive. -- [ ] One dispatch is not described as one native call or an atomic transaction. - -### Required validation - -- [ ] Failure/cancellation before admission, between steps and after native success. -- [ ] Span lifetime, pointer overflow and custom-codec regression tests. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/13 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/41 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/24 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/25 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/21 - -### Blocks - -None declared. This does not waive evidence, policy, or package requirements. - -### Package and readiness gate - -Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions. - -### Proposed branch and PR - -Branch: `feat/cli-014-memory-effects-cancellation` - -PR title: **Report memory batch effects and cancellation milestones** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -Rollback writes are not a transaction and can fail; do not retry partial workflows invisibly. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C03 — Prior audit: scoped codecs, partial batches and failure projection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/MemoryClient.cs) -- [C05 — Prior audit: production adapter rethrows callback exceptions.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs) -- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-015.md b/docs/engineering/work-items/CLI-015.md deleted file mode 100644 index aa3516e..0000000 --- a/docs/engineering/work-items/CLI-015.md +++ /dev/null @@ -1,98 +0,0 @@ - - -## CLI-015 — Adopt typed record and symbol commands with safe workflow policy - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P1 · **Milestone:** CLI-M4 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -Client record mutation uses Parent/destroy via generic SDK object/Lua access and can mutate before snapshot refresh fails. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Delegate CE mutation semantics to SDK while retaining high-level IDs, snapshots and trust policy. - -**Expected benefit:** Delegate CE mutation semantics to SDK while retaining high-level IDs, snapshots and trust policy. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Replace raw property/method operations with typed SDK commands. -- [ ] Represent command success separately from snapshot refresh. -- [ ] Keep table-root and raw-script opt-in defaults without claiming path checks sandbox content. - -### Acceptance criteria - -- [ ] No built-in Client adapter repairs CE record ownership through raw stack code. -- [ ] Delete remains explicit; disposing a borrowed snapshot cannot destroy GUI data. -- [ ] Post-mutation refresh failure preserves the completed effect. - -### Required validation - -- [ ] Missing/cyclic/self-parent records and snapshot failure after mutation. -- [ ] Denied roots, observed reparse points and explicit trust assumptions. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/13 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/49 -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/51 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/24 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/25 - -### Blocks - -None declared. This does not waive evidence, policy, or package requirements. - -### Package and readiness gate - -Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions. - -### Proposed branch and PR - -Branch: `feat/cli-015-typed-record-symbol-workflows` - -PR title: **Adopt typed record and symbol commands with safe workflow policy** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -A generic helper relocation alone does not fix ownership; safe path defaults do not eliminate TOCTOU. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C12 — Prior audit: Parent/destroy semantics and mutation/snapshot ambiguity.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/SdkTableRecordMutationPort.cs) -- [C18 — Prior audit: table-root allowlist and documented path TOCTOU limitation.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Infrastructure/CoreClientPolicy.cs) -- [C02 — Prior audit: built-in CE Lua declarations remain Client-owned.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Infrastructure/ClientLuaGlobals.cs) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-016.md b/docs/engineering/work-items/CLI-016.md deleted file mode 100644 index 7347b6d..0000000 --- a/docs/engineering/work-items/CLI-016.md +++ /dev/null @@ -1,98 +0,0 @@ - - -## CLI-016 — Adopt qualified SDK value-scan sessions - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P2 · **Milestone:** CLI-M5 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -The SDK factory exists but the current Client deliberately registers an unavailable value-scanner implementation. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Deliver one high-level scan-session vertical slice after its factory, artifact and live gates pass. - -**Expected benefit:** Deliver one high-level scan-session vertical slice after its factory, artifact and live gates pass. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Wrap the SDK-created owner; never reconstruct it from borrowed handles. -- [ ] Define Client request validation, session transitions, progress and copied result limits. -- [ ] Keep unsupported transitions/host profiles explicitly unavailable. - -### Acceptance criteria - -- [ ] The enabled adapter links the qualified factory and first-containing package. -- [ ] Every transition has failure/cleanup and target-change coverage. -- [ ] No source-only or interface-only status is advertised as live support. - -### Required validation - -- [ ] SDK conformance plus Client success/failure/state-transition fixtures. -- [ ] Live enable/disable/re-enable/target-change scan session. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/14 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/43 -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/51 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/25 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/18 - -### Blocks - -None declared. This does not waive evidence, policy, or package requirements. - -### Package and readiness gate - -Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions. - -### Proposed branch and PR - -Branch: `feat/cli-016-value-scan-adoption` - -PR title: **Adopt qualified SDK value-scan sessions** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -UI-friendly progress cannot substitute for a qualified native cancellation contract. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [S03 — Prior audit: new factory already exists; qualify, do not recreate.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanSessions.cs) -- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs) -- [C21 — Live gate protocol identified by current README; not independently re-read here.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/docs/live-capability-gates.md) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-017.md b/docs/engineering/work-items/CLI-017.md deleted file mode 100644 index 444cf14..0000000 --- a/docs/engineering/work-items/CLI-017.md +++ /dev/null @@ -1,100 +0,0 @@ - - -## CLI-017 — Adopt target-bound allocation and patch leases - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P1 · **Milestone:** CLI-M5 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -The Client has gated allocation/assembly contracts; SDK main contains underlying primitives that need target and handoff qualification. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Expose separate high-level allocation and patch workflows that compose qualified SDK owners. - -**Expected benefit:** Expose separate high-level allocation and patch workflows that compose qualified SDK owners. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Use explicit opt-in and safe defaults; do not rebuild disable-info or raw deallocation. -- [ ] Capture activation/target identity and track owner transfer failure. -- [ ] Split allocation and patch implementation into two focused PRs under this work item if they require independent review. - -### Acceptance criteria - -- [ ] A stale lease cannot mutate or release against a different target. -- [ ] A failure after native success retains cleanup authority or reports unknown effect. -- [ ] Availability records the exact SDK/live evidence for each workflow separately. - -### Required validation - -- [ ] Owner creation/tracking/publication failure and double disposal. -- [ ] Live target switch and partial cleanup for allocation and patch independently. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/14 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/39 -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/45 -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/51 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/25 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/24 - -### Blocks - -None declared. This does not waive evidence, policy, or package requirements. - -### Package and readiness gate - -Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions. - -### Proposed branch and PR - -Branch: `feat/cli-017-allocation-patch-leases` - -PR title: **Adopt target-bound allocation and patch leases** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -Patch scripts are privileged in-process operations, not sandboxed statements. Separate outcomes prevent unsafe retries. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [S04 — Prior audit: current-target allocation bindings.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/libs/CheatEngine.SDK.Engine/Allocation/LuaTargetMemoryAllocationOperations.cs) -- [S05 — Prior audit: patch and disable-info ownership handoff.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/libs/CheatEngine.SDK.Engine/Assembly/AutoAssemblerPatcher.cs) -- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs) -- [C21 — Live gate protocol identified by current README; not independently re-read here.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/docs/live-capability-gates.md) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-018.md b/docs/engineering/work-items/CLI-018.md deleted file mode 100644 index 2e52ec5..0000000 --- a/docs/engineering/work-items/CLI-018.md +++ /dev/null @@ -1,98 +0,0 @@ - - -## CLI-018 — Define gated debugger and subscription workflow adapters - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** research · **Priority:** P2 · **Milestone:** CLI-M5 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -Debugger, timer and hotkey contracts are currently unavailable; a copied-event stream exists separately. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Specify Client-friendly synchronous decisions and subscription workflows over qualified SDK mechanisms. - -**Expected benefit:** Specify Client-friendly synchronous decisions and subscription workflows over qualified SDK mechanisms. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Keep native continuation decisions synchronous and distinguish them from async observations. -- [ ] Delegate callback/thunk/rooting/unregister to SDK leases. -- [ ] Enable each producer only after its own host/cleanup qualification and policy review. - -### Acceptance criteria - -- [ ] No async consumer is responsible for unblocking a native debug event. -- [ ] Expired activations cannot receive native callbacks. -- [ ] Unavailable status persists where an SDK producer is not qualified. - -### Required validation - -- [ ] Decision versus observation contract fixtures. -- [ ] Late callback, slow reader and teardown races with real qualified producers. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/14 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/46 -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/47 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/35 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/25 - -### Blocks - -None declared. This does not waive evidence, policy, or package requirements. - -### Package and readiness gate - -Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions. - -### Proposed branch and PR - -Branch: `feat/cli-018-advanced-event-workflows` - -PR title: **Define gated debugger and subscription workflow adapters** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -This specification is not permission to implement a second debugger backend in Client. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs) -- [C15 — Prior audit: bounded event ring, pending reader list and async continuations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/Events/BoundedEventStream.cs) -- [U01 — Historical loader/tables/callback dispatch comparison.](https://github.com/cheat-engine/cheat-engine/blob/ec45d5f47f92a239ba0bf51ec5d04a7509c3fd37/Cheat%20Engine/plugin.pas) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-019.md b/docs/engineering/work-items/CLI-019.md deleted file mode 100644 index 386cefe..0000000 --- a/docs/engineering/work-items/CLI-019.md +++ /dev/null @@ -1,94 +0,0 @@ - - -## CLI-019 — Bound event consumers and specify stream completion - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P2 · **Milestone:** CLI-M5 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -The prior audit observes a bounded event ring but an independent linked list of pending reads and multiple enumerators. The implementation uses a lock. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Define a bounded observation-delivery contract beyond the ring capacity alone. - -**Expected benefit:** Define a bounded observation-delivery contract beyond the ring capacity alone. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Choose single-reader default or explicitly bounded competing-reader behavior; do not imply broadcast. -- [ ] Specify disposal of an enumerator with a pending read, cancellation, fairness and loss counts. -- [ ] Keep continuations asynchronous and payloads copied; measure lock contention rather than call it lock-free. - -### Acceptance criteria - -- [ ] Pending reader memory is bounded or excess readers are rejected. -- [ ] Disposal/cancellation cannot strand pending reads. -- [ ] Dropped observations never discard required native decisions. - -### Required validation - -- [ ] Many readers, canceled pending read, close/complete/dispose and overflow fixtures. -- [ ] Publication latency under a slow consumer. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/15 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -None declared. This does not waive evidence, policy, or package requirements. - -### Blocks - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/34 - -### Package and readiness gate - -Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan. - -### Proposed branch and PR - -Branch: `feat/cli-019-bounded-event-consumers` - -PR title: **Bound event consumers and specify stream completion** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -Changing queue implementation for fashion can break loss/ordering/completion semantics; qualify the contract first. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C15 — Prior audit: bounded event ring, pending reader list and async continuations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/Events/BoundedEventStream.cs) -- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-020.md b/docs/engineering/work-items/CLI-020.md deleted file mode 100644 index b46d4bf..0000000 --- a/docs/engineering/work-items/CLI-020.md +++ /dev/null @@ -1,96 +0,0 @@ - - -## CLI-020 — Standardize fluent terminals and supported workflow examples - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** governance · **Priority:** P2 · **Milestone:** CLI-M4 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -Existing immutable builders and explicit cardinality terminals are useful. Public interfaces also include unavailable domains that require clear status. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Improve discoverability without replacing the existing fluent model or exposing low-level complexity. - -**Expected benefit:** Improve discoverability without replacing the existing fluent model or exposing low-level complexity. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Keep configuration side-effect free and execution terminals explicit. -- [ ] Describe ordering, absence, limits, cancellation milestones and cleanup near each supported operation. -- [ ] Compile examples against shipped packages; label research APIs and unavailable domains before examples. - -### Acceptance criteria - -- [ ] Examples cover success, expected failure and cleanup for each supported workflow. -- [ ] Fluent names match actual guarantees; compatibility aliases preserve old callers. -- [ ] No claim of safe/atomic/async/zero-allocation lacks a precise contract. - -### Required validation - -- [ ] Template/example compilation in clean consumers. -- [ ] Default builder, ambiguous cardinality and old public API tests. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/15 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/18 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/29 - -### Blocks - -None declared. This does not waive evidence, policy, or package requirements. - -### Package and readiness gate - -Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan. - -### Proposed branch and PR - -Branch: `docs/cli-020-fluent-developer-guidance` - -PR title: **Standardize fluent terminals and supported workflow examples** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -A huge facade is not delivered capability. Avoid adding placeholder interfaces only to increase API count. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md) -- [C16 — Prior audit: immutable terminals, post-filter limits, readability naming.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Fluent/Scanning/AobScanBuilder.cs) -- [N03 — Source/binary/behavioral compatibility distinctions.](https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/breaking-changes) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-021.md b/docs/engineering/work-items/CLI-021.md deleted file mode 100644 index 44847fb..0000000 --- a/docs/engineering/work-items/CLI-021.md +++ /dev/null @@ -1,101 +0,0 @@ - - -## CLI-021 — Unify stale-client and local-process operation semantics - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P2 · **Milestone:** CLI-M1 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -Some local process enumeration/unavailable paths bypass normal Client dispatch. Local BCL metadata is not proof of CE backend target identity. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Make activation requirements explicit for stateful operations and separate legitimate offline diagnostics. - -**Expected benefit:** Make activation requirements explicit for stateful operations and separate legitimate offline diagnostics. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [x] Apply stale-client policy consistently before operational work, even for unavailable implementations. -- [x] Label local process discovery/enrichment as local; no speculative remote backend. -- [x] Document cancellation ordering and keep pure immutable snapshots usable where intended. - -### Acceptance criteria - -- [x] Disable/re-enable does not produce different lifetime behavior by domain accident. -- [x] Offline diagnostics have a separate explicit contract. -- [x] Local process metadata is never mistaken for authoritative SDK target identity. - -### Required validation - -- [x] Stale facade across local, implemented and unavailable domains. -- [x] Canceled enumeration and disappearing local process fixtures. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/15 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -None declared. This does not waive evidence, policy, or package requirements. - -### Blocks - -None declared. This does not waive evidence, policy, or package requirements. - -### Package and readiness gate - -Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan. - -### Proposed branch and PR - -Branch: `feat/cli-021-stale-local-operation-policy` - -PR title: **Unify stale-client and local-process operation semantics** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -Adding lifetime exceptions indiscriminately can remove useful offline diagnostics; name that boundary deliberately. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C06 — Prior audit: expected no-target failure and local target observations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/ProcessClient.cs) -- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs) -- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. - -Implementation: local BCL enumeration moved to `ILocalProcessDiagnostics` with `LocalProcessId`; `IProcessClient` -continues to describe only Cheat Engine selection. Missing BCL metadata now yields optional enrichment, not a target -detach. Stateful and capability-gated services admit the activation before policy or cancellation; offline snapshots -remain pure managed values. Release build passed with zero warnings. Core tests passed (238); the full solution run had -one pre-existing source-generator snapshot newline mismatch, while its other 522 tests passed. diff --git a/docs/engineering/work-items/CLI-022.md b/docs/engineering/work-items/CLI-022.md deleted file mode 100644 index 19023cd..0000000 --- a/docs/engineering/work-items/CLI-022.md +++ /dev/null @@ -1,100 +0,0 @@ - - -## CLI-022 — Validate minimum SDK artifacts and public compatibility - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** validation · **Priority:** P1 · **Milestone:** CLI-M6 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -Client publishes a compatible SDK range, but source availability, package content and behavioral support are not equivalent. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Test packed Client consumption against the exact minimum and selected newer SDK artifacts. - -**Expected benefit:** Test packed Client consumption against the exact minimum and selected newer SDK artifacts. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Record source/package/bridge/generator identity for each test combination. -- [ ] Preserve direct SDK bootstrap/native asset reference rules. -- [ ] Test old source, old binaries where supported, application interface implementations and generated consumers. - -### Acceptance criteria - -- [ ] No sibling project reference masks missing published members. -- [ ] Required interface changes and failure/cancellation semantics have migration notes. -- [ ] The declared range is not described as a fully tested set of all possible packages. - -### Required validation - -- [ ] Minimal plugin, custom codec/module, relay library and old application fakes. -- [ ] AOT graph publication separate from managed live plugin loading. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/16 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/51 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/23 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/26 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/27 - -### Blocks - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/39 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/40 - -### Package and readiness gate - -Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions. - -### Proposed branch and PR - -Branch: `test/cli-022-package-compatibility-matrix` - -PR title: **Validate minimum SDK artifacts and public compatibility** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -Moving public types between assemblies can break binary identity even with unchanged namespaces. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C19 — Prior audit: SDK 1.0.0 resolution; artifact bytes not independently inspected.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/packages.lock.json) -- [C20 — Prior audit: approved SDK values versus broad compile/runtime dependency.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Abstractions/CheatEngine.Client.Abstractions.csproj) -- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md) -- [N03 — Source/binary/behavioral compatibility distinctions.](https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/breaking-changes) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-023.md b/docs/engineering/work-items/CLI-023.md deleted file mode 100644 index bb91f61..0000000 --- a/docs/engineering/work-items/CLI-023.md +++ /dev/null @@ -1,95 +0,0 @@ - - -## CLI-023 — Verify coherent deployment sets and external plugin inheritance - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** feature · **Priority:** P2 · **Milestone:** CLI-M6 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -Hosting build tooling performs per-file deployment and metadata inheritance checks. A build-only filesystem P/Invoke is not a runtime CE bypass. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Make deployment/profile validation truthful, failure-aware and compatible with documented plugin inheritance. - -**Expected benefit:** Make deployment/profile validation truthful, failure-aware and compatible with documented plugin inheritance. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Produce a file/hash manifest and detect stale or mixed SDK/bridge/Client outputs. -- [ ] Prefer versioned staging where supported; otherwise report partial replacement and require an explicit offline deployment policy. -- [ ] Resolve external base metadata without executing user assemblies, or document the supported inheritance restriction. - -### Acceptance criteria - -- [ ] A failed second file replacement reports the installed subset. -- [ ] Stale manifest-owned files are detected without deleting arbitrary user files. -- [ ] An external shared base is either supported by a tested resolver or rejected with precise guidance. - -### Required validation - -- [ ] Partial deployment, locked file, missing bridge and stale-version tests. -- [ ] Direct, same-assembly, external-base, manual/generated bootstrap consumer fixtures. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/16 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/38 - -### Blocks - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/40 - -### Package and readiness gate - -Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan. - -### Proposed branch and PR - -Branch: `feat/cli-023-deployment-profile-integrity` - -PR title: **Verify coherent deployment sets and external plugin inheritance** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -Do not claim directory atomicity or safe hot reload; do not automatically modify CE runtime configuration. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C17 — Prior audit: build-only native calls, external-base checks and per-file deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Hosting/buildTransitive/CheatEngine.Client.Hosting.targets) -- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md) -- [S11 — Protected native boundary and bridge contract reference.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/native/cheatengine-sdk-lua-bridge/README.md) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-024.md b/docs/engineering/work-items/CLI-024.md deleted file mode 100644 index 59aa203..0000000 --- a/docs/engineering/work-items/CLI-024.md +++ /dev/null @@ -1,99 +0,0 @@ - - -## CLI-024 — Publish end-to-end qualification and performance budgets - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** validation · **Priority:** P2 · **Milestone:** CLI-M6 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -No .NET/live-host/benchmark execution was performed by the audit summaries or this bootstrap. Planned acceptance tests require actual recorded results. - -**Evidence classification:** InheritedAuditBasis + Proposal; revalidate the pinned call path before implementation. - -### Outcome and rationale - -Qualify the release scope from developer workflow through SDK/host behavior and cleanup. - -**Expected benefit:** Qualify the release scope from developer workflow through SDK/host behavior and cleanup. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Measure primitive, dispatch, batch, snapshot, codec, AOB and event paths separately. -- [ ] Record CLR allocations, available Lua/native metrics, boundary calls and latency distributions. -- [ ] Run declared success/failure/disable/re-enable/target-change scenarios on a purpose-built target. - -### Acceptance criteria - -- [ ] No performance improvement weakens error, lifetime, target or cancellation semantics. -- [ ] Source/package/live/AOT publication results remain independently labeled. -- [ ] Only qualified workflows are advertised; deferred advanced domains remain visible. - -### Required validation - -- [ ] Direct SDK versus immediate/queued Client and equal-semantics batch comparisons. -- [ ] Controlled live plugin and two-plugin regression profiles. - -### Scope exclusions - -- No unrelated rewrite, automatic capability activation, package publication, or repository-protection changes. -- Keep integration mechanisms in SDK and application policy/workflows in Client. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/16 - -Implementation leaf. Keep small tasks in this checklist; split a new issue only for an independently reviewable outcome. - -### Blocked by - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/38 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/39 -- https://github.com/CheatEngineNet/CheatEngine.SDK/issues/52 - -### Blocks - -None declared. This does not waive evidence, policy, or package requirements. - -### Package and readiness gate - -Before Client implementation is considered ready, identify and restore the exact published SDK package containing EVERY required primitive. SDK-023 is the minimum-contract publication task, not a promise that an earlier artifact includes later scan, patch, or debugger additions. - -### Proposed branch and PR - -Branch: `test/cli-024-release-performance-evidence` - -PR title: **Publish end-to-end qualification and performance budgets** - -Open against `main` only after refinement and prerequisite review. Add `Closes ` for this leaf when the PR actually completes it. Reference upstream SDK work as a dependency, not as an issue to close. The bootstrap does not create this implementation branch. - -### Risks and compatibility - -Pooling retained contexts or exposing borrowed spans to claim zero allocations would invalidate the safety model. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Focused PR linked to the implementation issue. -- [ ] Commands, environment and actual regression results; unexecuted cases remain pending. -- [ ] Updated contract/capability documentation and compatibility notes. - -Required for the affected capability. Optional profiles may be deferred through an explicit ADR, not marked as implemented. - -### Sources - -- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md) -- [C21 — Live gate protocol identified by current README; not independently re-read here.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/docs/live-capability-gates.md) -- [C03 — Prior audit: scoped codecs, partial batches and failure projection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/MemoryClient.cs) -- [C15 — Prior audit: bounded event ring, pending reader list and async continuations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/Events/BoundedEventStream.cs) -- [S13 — Live fixture entry point, identified in contribution guidance.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/tests/CheatEngine.SDK.LivePlugin/README.md) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-E01.md b/docs/engineering/work-items/CLI-E01.md deleted file mode 100644 index d480139..0000000 --- a/docs/engineering/work-items/CLI-E01.md +++ /dev/null @@ -1,96 +0,0 @@ - - -## CLI-E01 — Governance and capability truth - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** epic · **Priority:** P1 · **Milestone:** CLI-M0 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -This grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing. - -**Evidence classification:** Proposal derived from the attached summaries and pinned source references. - -### Outcome and rationale - -Maintain the developer-facing support model and evidence-linked execution plan. - -**Expected benefit:** Maintain the developer-facing support model and evidence-linked execution plan. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Review child source evidence and preserve the SDK/Client responsibility boundary. -- [ ] Sequence only real dependencies; do not block a child on closing its own parent. -- [ ] Require an explicit decision and evidence for any deferred capability. - -### Acceptance criteria - -- [ ] Each child is completed with evidence or explicitly deferred by an approved decision. -- [ ] Cross-repository artifact gates have named versions and hashes when adopted. -- [ ] Compatibility and support documentation match actual delivery, not interface count. - -### Required validation - -- [ ] Review the acceptance evidence of each child; do not count parent closure as an additional runtime test. - -### Scope exclusions - -- No monolithic epic implementation PR. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/8 - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/17 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/18 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/19 - -### Blocked by - -None declared. This does not waive evidence, policy, or package requirements. - -### Blocks - -None declared. This does not waive evidence, policy, or package requirements. - -### Package and readiness gate - -Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan. - -### Proposed branch and PR - -No epic-sized implementation branch. Children have focused branch/PR proposals. The bootstrap creates only one documentation/tooling branch and draft PR per repository. - -### Risks and compatibility - -A grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Linked child PRs, tests, and a final scope/deferral review. - -Outcome group; optional scope remains explicitly gated or deferred. - -### Sources - -- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md) -- [C19 — Prior audit: SDK 1.0.0 resolution; artifact bytes not independently inspected.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/packages.lock.json) -- [H02 — Merged high-level API/modules; preserve this work.](https://github.com/CheatEngineNet/CheatEngine.Client/pull/7) -- [H01 — Merged foundations; author-reported tests are not independently executed.](https://github.com/CheatEngineNet/CheatEngine.SDK/pull/19) -- [C09 — Prior audit: capability and exception-family mismatch.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/RuntimeClient.cs) -- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs) -- [G01 — Issue API; filter pull_request entries from issue lists.](https://docs.github.com/en/rest/issues/issues) -- [G02 — Repository-specific milestones.](https://docs.github.com/en/rest/issues/milestones) -- [G03 — Native hierarchy uses issue IDs, not issue numbers.](https://docs.github.com/en/rest/issues/sub-issues) -- [G04 — Native blocked-by relationship with the blocker issue_id.](https://docs.github.com/en/rest/issues/issue-dependencies) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-E02.md b/docs/engineering/work-items/CLI-E02.md deleted file mode 100644 index 21ecea3..0000000 --- a/docs/engineering/work-items/CLI-E02.md +++ /dev/null @@ -1,92 +0,0 @@ - - -## CLI-E02 — Immediate reliability corrections - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** epic · **Priority:** P1 · **Milestone:** CLI-M1 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -This grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing. - -**Evidence classification:** Proposal derived from the attached summaries and pinned source references. - -### Outcome and rationale - -Repair classified failures, retained contexts, and partial activation cleanup independently of broad SDK expansion. - -**Expected benefit:** Repair classified failures, retained contexts, and partial activation cleanup independently of broad SDK expansion. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Review child source evidence and preserve the SDK/Client responsibility boundary. -- [ ] Sequence only real dependencies; do not block a child on closing its own parent. -- [ ] Require an explicit decision and evidence for any deferred capability. - -### Acceptance criteria - -- [ ] Each child is completed with evidence or explicitly deferred by an approved decision. -- [ ] Cross-repository artifact gates have named versions and hashes when adopted. -- [ ] Compatibility and support documentation match actual delivery, not interface count. - -### Required validation - -- [ ] Review the acceptance evidence of each child; do not count parent closure as an additional runtime test. - -### Scope exclusions - -- No monolithic epic implementation PR. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/8 - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/20 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/21 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/22 - -### Blocked by - -None declared. This does not waive evidence, policy, or package requirements. - -### Blocks - -None declared. This does not waive evidence, policy, or package requirements. - -### Package and readiness gate - -Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan. - -### Proposed branch and PR - -No epic-sized implementation branch. Children have focused branch/PR proposals. The bootstrap creates only one documentation/tooling branch and draft PR per repository. - -### Risks and compatibility - -A grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Linked child PRs, tests, and a final scope/deferral review. - -Outcome group; optional scope remains explicitly gated or deferred. - -### Sources - -- [C05 — Prior audit: production adapter rethrows callback exceptions.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs) -- [C06 — Prior audit: expected no-target failure and local target observations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/ProcessClient.cs) -- [C03 — Prior audit: scoped codecs, partial batches and failure projection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/MemoryClient.cs) -- [C04 — Public extension context with an implementation-enforced lifetime.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Abstractions/Memory/IMemoryReadContext.cs) -- [C07 — Prior audit: early construction cleanup differs from normal cleanup.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Hosting/CheatEngineClientPlugin.cs) -- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-E03.md b/docs/engineering/work-items/CLI-E03.md deleted file mode 100644 index ef3fc6f..0000000 --- a/docs/engineering/work-items/CLI-E03.md +++ /dev/null @@ -1,96 +0,0 @@ - - -## CLI-E03 — SDK contract adoption - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** epic · **Priority:** P1 · **Milestone:** CLI-M2 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -This grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing. - -**Evidence classification:** Proposal derived from the attached summaries and pinned source references. - -### Outcome and rationale - -Replace duplicated built-in CE interpretation with the actual containing SDK artifacts. - -**Expected benefit:** Replace duplicated built-in CE interpretation with the actual containing SDK artifacts. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Review child source evidence and preserve the SDK/Client responsibility boundary. -- [ ] Sequence only real dependencies; do not block a child on closing its own parent. -- [ ] Require an explicit decision and evidence for any deferred capability. - -### Acceptance criteria - -- [ ] Each child is completed with evidence or explicitly deferred by an approved decision. -- [ ] Cross-repository artifact gates have named versions and hashes when adopted. -- [ ] Compatibility and support documentation match actual delivery, not interface count. - -### Required validation - -- [ ] Review the acceptance evidence of each child; do not count parent closure as an additional runtime test. - -### Scope exclusions - -- No monolithic epic implementation PR. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/8 - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/23 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/24 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/25 - -### Blocked by - -None declared. This does not waive evidence, policy, or package requirements. - -### Blocks - -None declared. This does not waive evidence, policy, or package requirements. - -### Package and readiness gate - -Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan. - -### Proposed branch and PR - -No epic-sized implementation branch. Children have focused branch/PR proposals. The bootstrap creates only one documentation/tooling branch and draft PR per repository. - -### Risks and compatibility - -A grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Linked child PRs, tests, and a final scope/deferral review. - -Outcome group; optional scope remains explicitly gated or deferred. - -### Sources - -- [C02 — Prior audit: built-in CE Lua declarations remain Client-owned.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Infrastructure/ClientLuaGlobals.cs) -- [C09 — Prior audit: capability and exception-family mismatch.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/RuntimeClient.cs) -- [C19 — Prior audit: SDK 1.0.0 resolution; artifact bytes not independently inspected.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/packages.lock.json) -- [C03 — Prior audit: scoped codecs, partial batches and failure projection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/MemoryClient.cs) -- [C13 — Prior audit: operation/registration generation and recursive type checks.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/source-generators/CheatEngine.Client.SourceGenerators.Lua/CheatEngineLuaGenerator.cs) -- [C11 — Prior audit: SDK delegation but ambiguous false becomes rejection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/SdkAobScanPort.cs) -- [N03 — Source/binary/behavioral compatibility distinctions.](https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/breaking-changes) -- [C08 — Prior audit: local observed epoch is not authoritative target identity.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Infrastructure/TargetSelectionLifetime.cs) -- [C06 — Prior audit: expected no-target failure and local target observations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/ProcessClient.cs) -- [C05 — Prior audit: production adapter rethrows callback exceptions.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-E04.md b/docs/engineering/work-items/CLI-E04.md deleted file mode 100644 index 3805ba0..0000000 --- a/docs/engineering/work-items/CLI-E04.md +++ /dev/null @@ -1,93 +0,0 @@ - - -## CLI-E04 — Modules, public boundaries, and activation composition - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** epic · **Priority:** P1 · **Milestone:** CLI-M3 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -This grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing. - -**Evidence classification:** Proposal derived from the attached summaries and pinned source references. - -### Outcome and rationale - -Keep application extensibility safe without leaking raw SDK state or duplicating registration mechanisms. - -**Expected benefit:** Keep application extensibility safe without leaking raw SDK state or duplicating registration mechanisms. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Review child source evidence and preserve the SDK/Client responsibility boundary. -- [ ] Sequence only real dependencies; do not block a child on closing its own parent. -- [ ] Require an explicit decision and evidence for any deferred capability. - -### Acceptance criteria - -- [ ] Each child is completed with evidence or explicitly deferred by an approved decision. -- [ ] Cross-repository artifact gates have named versions and hashes when adopted. -- [ ] Compatibility and support documentation match actual delivery, not interface count. - -### Required validation - -- [ ] Review the acceptance evidence of each child; do not count parent closure as an additional runtime test. - -### Scope exclusions - -- No monolithic epic implementation PR. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/8 - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/26 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/27 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/28 - -### Blocked by - -None declared. This does not waive evidence, policy, or package requirements. - -### Blocks - -None declared. This does not waive evidence, policy, or package requirements. - -### Package and readiness gate - -Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan. - -### Proposed branch and PR - -No epic-sized implementation branch. Children have focused branch/PR proposals. The bootstrap creates only one documentation/tooling branch and draft PR per repository. - -### Risks and compatibility - -A grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Linked child PRs, tests, and a final scope/deferral review. - -Outcome group; optional scope remains explicitly gated or deferred. - -### Sources - -- [C13 — Prior audit: operation/registration generation and recursive type checks.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/source-generators/CheatEngine.Client.SourceGenerators.Lua/CheatEngineLuaGenerator.cs) -- [S06 — Prior audit: generation stamps but name-based unregister.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/source-generators/CheatEngine.SDK.SourceGenerators.Shared/LuaEmit/LuaRegistrationEmitter.cs) -- [C07 — Prior audit: early construction cleanup differs from normal cleanup.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Hosting/CheatEngineClientPlugin.cs) -- [C20 — Prior audit: approved SDK values versus broad compile/runtime dependency.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Abstractions/CheatEngine.Client.Abstractions.csproj) -- [N03 — Source/binary/behavioral compatibility distinctions.](https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/breaking-changes) -- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs) -- [S08 — Prior audit: static host state and runtime admission.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/libs/CheatEngine.SDK.Hosting/Bootstrap/PluginHost.cs) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-E05.md b/docs/engineering/work-items/CLI-E05.md deleted file mode 100644 index fda73fc..0000000 --- a/docs/engineering/work-items/CLI-E05.md +++ /dev/null @@ -1,95 +0,0 @@ - - -## CLI-E05 — Memory, scan, and table workflows - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** epic · **Priority:** P1 · **Milestone:** CLI-M4 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -This grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing. - -**Evidence classification:** Proposal derived from the attached summaries and pinned source references. - -### Outcome and rationale - -Preserve cardinality, partial effects, limits and target consistency across friendly APIs. - -**Expected benefit:** Preserve cardinality, partial effects, limits and target consistency across friendly APIs. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Review child source evidence and preserve the SDK/Client responsibility boundary. -- [ ] Sequence only real dependencies; do not block a child on closing its own parent. -- [ ] Require an explicit decision and evidence for any deferred capability. - -### Acceptance criteria - -- [ ] Each child is completed with evidence or explicitly deferred by an approved decision. -- [ ] Cross-repository artifact gates have named versions and hashes when adopted. -- [ ] Compatibility and support documentation match actual delivery, not interface count. - -### Required validation - -- [ ] Review the acceptance evidence of each child; do not count parent closure as an additional runtime test. - -### Scope exclusions - -- No monolithic epic implementation PR. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/8 - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/29 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/30 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/31 - -### Blocked by - -None declared. This does not waive evidence, policy, or package requirements. - -### Blocks - -None declared. This does not waive evidence, policy, or package requirements. - -### Package and readiness gate - -Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan. - -### Proposed branch and PR - -No epic-sized implementation branch. Children have focused branch/PR proposals. The bootstrap creates only one documentation/tooling branch and draft PR per repository. - -### Risks and compatibility - -A grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Linked child PRs, tests, and a final scope/deferral review. - -Outcome group; optional scope remains explicitly gated or deferred. - -### Sources - -- [C10 — Prior audit: whole scan precedes module lookup and post-filtering.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/PatternScanner.cs) -- [C11 — Prior audit: SDK delegation but ambiguous false becomes rejection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/SdkAobScanPort.cs) -- [C16 — Prior audit: immutable terminals, post-filter limits, readability naming.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Fluent/Scanning/AobScanBuilder.cs) -- [C03 — Prior audit: scoped codecs, partial batches and failure projection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/MemoryClient.cs) -- [C05 — Prior audit: production adapter rethrows callback exceptions.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Dispatching/SdkMainThreadDispatcher.cs) -- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md) -- [C12 — Prior audit: Parent/destroy semantics and mutation/snapshot ambiguity.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/SdkTableRecordMutationPort.cs) -- [C18 — Prior audit: table-root allowlist and documented path TOCTOU limitation.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Infrastructure/CoreClientPolicy.cs) -- [C02 — Prior audit: built-in CE Lua declarations remain Client-owned.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Infrastructure/ClientLuaGlobals.cs) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-E06.md b/docs/engineering/work-items/CLI-E06.md deleted file mode 100644 index 0fb4707..0000000 --- a/docs/engineering/work-items/CLI-E06.md +++ /dev/null @@ -1,93 +0,0 @@ - - -## CLI-E06 — Qualified advanced workflow adoption - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** epic · **Priority:** P1 · **Milestone:** CLI-M5 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -This grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing. - -**Evidence classification:** Proposal derived from the attached summaries and pinned source references. - -### Outcome and rationale - -Enable scan, allocation, patch and event workflows only after their specific lower-layer gates. - -**Expected benefit:** Enable scan, allocation, patch and event workflows only after their specific lower-layer gates. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Review child source evidence and preserve the SDK/Client responsibility boundary. -- [ ] Sequence only real dependencies; do not block a child on closing its own parent. -- [ ] Require an explicit decision and evidence for any deferred capability. - -### Acceptance criteria - -- [ ] Each child is completed with evidence or explicitly deferred by an approved decision. -- [ ] Cross-repository artifact gates have named versions and hashes when adopted. -- [ ] Compatibility and support documentation match actual delivery, not interface count. - -### Required validation - -- [ ] Review the acceptance evidence of each child; do not count parent closure as an additional runtime test. - -### Scope exclusions - -- No monolithic epic implementation PR. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/8 - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/32 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/33 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/34 - -### Blocked by - -None declared. This does not waive evidence, policy, or package requirements. - -### Blocks - -None declared. This does not waive evidence, policy, or package requirements. - -### Package and readiness gate - -Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan. - -### Proposed branch and PR - -No epic-sized implementation branch. Children have focused branch/PR proposals. The bootstrap creates only one documentation/tooling branch and draft PR per repository. - -### Risks and compatibility - -A grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Linked child PRs, tests, and a final scope/deferral review. - -Outcome group; optional scope remains explicitly gated or deferred. - -### Sources - -- [S03 — Prior audit: new factory already exists; qualify, do not recreate.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/libs/CheatEngine.SDK.Engine/Scanning/Values/MemoryScanSessions.cs) -- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs) -- [C21 — Live gate protocol identified by current README; not independently re-read here.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/docs/live-capability-gates.md) -- [S04 — Prior audit: current-target allocation bindings.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/libs/CheatEngine.SDK.Engine/Allocation/LuaTargetMemoryAllocationOperations.cs) -- [S05 — Prior audit: patch and disable-info ownership handoff.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/libs/CheatEngine.SDK.Engine/Assembly/AutoAssemblerPatcher.cs) -- [C15 — Prior audit: bounded event ring, pending reader list and async continuations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/Events/BoundedEventStream.cs) -- [U01 — Historical loader/tables/callback dispatch comparison.](https://github.com/cheat-engine/cheat-engine/blob/ec45d5f47f92a239ba0bf51ec5d04a7509c3fd37/Cheat%20Engine/plugin.pas) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-E07.md b/docs/engineering/work-items/CLI-E07.md deleted file mode 100644 index ce26dcb..0000000 --- a/docs/engineering/work-items/CLI-E07.md +++ /dev/null @@ -1,92 +0,0 @@ - - -## CLI-E07 — Developer experience and bounded observation - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** epic · **Priority:** P2 · **Milestone:** CLI-M5 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -This grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing. - -**Evidence classification:** Proposal derived from the attached summaries and pinned source references. - -### Outcome and rationale - -Define coherent lifetime, reader, completion and discoverability policies. - -**Expected benefit:** Define coherent lifetime, reader, completion and discoverability policies. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Review child source evidence and preserve the SDK/Client responsibility boundary. -- [ ] Sequence only real dependencies; do not block a child on closing its own parent. -- [ ] Require an explicit decision and evidence for any deferred capability. - -### Acceptance criteria - -- [ ] Each child is completed with evidence or explicitly deferred by an approved decision. -- [ ] Cross-repository artifact gates have named versions and hashes when adopted. -- [ ] Compatibility and support documentation match actual delivery, not interface count. - -### Required validation - -- [ ] Review the acceptance evidence of each child; do not count parent closure as an additional runtime test. - -### Scope exclusions - -- No monolithic epic implementation PR. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/8 - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/35 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/36 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/37 - -### Blocked by - -None declared. This does not waive evidence, policy, or package requirements. - -### Blocks - -None declared. This does not waive evidence, policy, or package requirements. - -### Package and readiness gate - -Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan. - -### Proposed branch and PR - -No epic-sized implementation branch. Children have focused branch/PR proposals. The bootstrap creates only one documentation/tooling branch and draft PR per repository. - -### Risks and compatibility - -A grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Linked child PRs, tests, and a final scope/deferral review. - -Outcome group; optional scope remains explicitly gated or deferred. - -### Sources - -- [C15 — Prior audit: bounded event ring, pending reader list and async continuations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/Events/BoundedEventStream.cs) -- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md) -- [C16 — Prior audit: immutable terminals, post-filter limits, readability naming.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Fluent/Scanning/AobScanBuilder.cs) -- [N03 — Source/binary/behavioral compatibility distinctions.](https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/breaking-changes) -- [C06 — Prior audit: expected no-target failure and local target observations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/ProcessClient.cs) -- [C14 — Prior audit: provider-local singleton services and unavailable adapters.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-E08.md b/docs/engineering/work-items/CLI-E08.md deleted file mode 100644 index d6bf715..0000000 --- a/docs/engineering/work-items/CLI-E08.md +++ /dev/null @@ -1,96 +0,0 @@ - - -## CLI-E08 — Package, deployment, and release readiness - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** epic · **Priority:** P1 · **Milestone:** CLI-M6 - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -This grouping coordinates the linked implementation issues. Existing capabilities are credited by each child rather than redeclared as missing. - -**Evidence classification:** Proposal derived from the attached summaries and pinned source references. - -### Outcome and rationale - -Validate actual packages, external consumers, deployment sets and measured end-to-end behavior. - -**Expected benefit:** Validate actual packages, external consumers, deployment sets and measured end-to-end behavior. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Review child source evidence and preserve the SDK/Client responsibility boundary. -- [ ] Sequence only real dependencies; do not block a child on closing its own parent. -- [ ] Require an explicit decision and evidence for any deferred capability. - -### Acceptance criteria - -- [ ] Each child is completed with evidence or explicitly deferred by an approved decision. -- [ ] Cross-repository artifact gates have named versions and hashes when adopted. -- [ ] Compatibility and support documentation match actual delivery, not interface count. - -### Required validation - -- [ ] Review the acceptance evidence of each child; do not count parent closure as an additional runtime test. - -### Scope exclusions - -- No monolithic epic implementation PR. - -### Parent and children - -Parent: https://github.com/CheatEngineNet/CheatEngine.Client/issues/8 - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/38 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/39 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/40 - -### Blocked by - -None declared. This does not waive evidence, policy, or package requirements. - -### Blocks - -None declared. This does not waive evidence, policy, or package requirements. - -### Package and readiness gate - -Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan. - -### Proposed branch and PR - -No epic-sized implementation branch. Children have focused branch/PR proposals. The bootstrap creates only one documentation/tooling branch and draft PR per repository. - -### Risks and compatibility - -A grouping can span several phases. Its completion milestone is not a reason to delay an earlier independent child. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Linked child PRs, tests, and a final scope/deferral review. - -Outcome group; optional scope remains explicitly gated or deferred. - -### Sources - -- [C19 — Prior audit: SDK 1.0.0 resolution; artifact bytes not independently inspected.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/packages.lock.json) -- [C20 — Prior audit: approved SDK values versus broad compile/runtime dependency.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Abstractions/CheatEngine.Client.Abstractions.csproj) -- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md) -- [N03 — Source/binary/behavioral compatibility distinctions.](https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/breaking-changes) -- [C17 — Prior audit: build-only native calls, external-base checks and per-file deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Hosting/buildTransitive/CheatEngine.Client.Hosting.targets) -- [S11 — Protected native boundary and bridge contract reference.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/native/cheatengine-sdk-lua-bridge/README.md) -- [C21 — Live gate protocol identified by current README; not independently re-read here.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/docs/live-capability-gates.md) -- [C03 — Prior audit: scoped codecs, partial batches and failure projection.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/MemoryClient.cs) -- [C15 — Prior audit: bounded event ring, pending reader list and async continuations.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/libs/CheatEngine.Client.Core/Domains/Events/BoundedEventStream.cs) -- [S13 — Live fixture entry point, identified in contribution guidance.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/tests/CheatEngine.SDK.LivePlugin/README.md) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/engineering/work-items/CLI-PLAN.md b/docs/engineering/work-items/CLI-PLAN.md deleted file mode 100644 index c0f12be..0000000 --- a/docs/engineering/work-items/CLI-PLAN.md +++ /dev/null @@ -1,97 +0,0 @@ - - -## CLI-PLAN — Establish the Client developer-workflow roadmap and execution baseline - -**Owner:** `CheatEngineNet/CheatEngine.Client` · **Kind:** roadmap · **Priority:** P1 · **Milestone:** Cross-phase navigation - -**Initial status:** Needs refinement. **Runtime validation:** Not executed by this bootstrap. - -### Context and evidence - -This is the navigation root for the engineering bootstrap, not an implemented feature or a GitHub Project object. - -**Evidence classification:** `MetadataRead` + `Proposal`: the initial GitHub preparation attempt was denied (`Resource not accessible by integration`). A later operator-authorized import created and populated the private CheatEngineNet — Engineering Execution Project #1 and created Client issues [#8–#40](https://github.com/CheatEngineNet/CheatEngine.Client/issues/8). This is a planning-metadata receipt only; it does not establish implementation, package, fixture, or live-host qualification. - -### Outcome and rationale - -Connect outcome milestones, owned epics, implementable issues, source evidence, package gates and focused PRs. - -**Expected benefit:** Connect outcome milestones, owned epics, implementable issues, source evidence, package gates and focused PRs. - -### Architectural responsibility - -SDK owns CE mappings, native safety, factual outcomes and low-level owners. Client owns application policy, typed workflows, composition, and developer experience. - -### Technical requirements and task checklist - -- [ ] Keep one owning repository per implementation task. -- [ ] Keep hierarchy, blocking dependencies, package gates and release qualification distinct. -- [ ] Never close this root automatically from one child implementation PR. - -### Acceptance criteria - -- [ ] Every work item links to its owning epic and verified sources. -- [ ] Native GitHub relationships and deployment status are recorded accurately. -- [ ] No speculative due dates, assignees, release versions or live-verification results are assigned. - -### Required validation - -- [ ] Manifest integrity, hierarchy and cross-repository dependency validation. - -### Scope exclusions - -- No automatic implementation branch farm, auto-merge, release or protection change. - -### Parent and children - -Repository roadmap root; not a GitHub Projects object. - -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/9 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/10 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/11 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/12 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/13 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/14 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/15 -- https://github.com/CheatEngineNet/CheatEngine.Client/issues/16 - -### Blocked by - -None declared. This does not waive evidence, policy, or package requirements. - -### Blocks - -None declared. This does not waive evidence, policy, or package requirements. - -### Package and readiness gate - -Identify the first containing artifact before describing new source as consumable support. No version is invented by this plan. - -### Proposed branch and PR - -No epic-sized implementation branch. Children have focused branch/PR proposals. The bootstrap creates only one documentation/tooling branch and draft PR per repository. - -### Risks and compatibility - -An attractive board cannot substitute for precise contracts, acceptance evidence, and maintainable issue scopes. - -**Long-term impact:** One traceable owner, stable acceptance criteria, and explicit compatibility evidence reduce repeated integration fixes and make future API changes reviewable. - -### Definition of done - -- [ ] Navigable roadmap, detailed backlog and verified deployment receipt. - -Governance root; close only when the defined roadmap scope is accepted or deliberately superseded. - -### Sources - -- [S01 — Existing build/test, focused branches and release rules; preserve rather than overwrite.](https://github.com/CheatEngineNet/CheatEngine.SDK/blob/aa3fcc3cdf629468e69d0c68817183d44a719894/CONTRIBUTING.md) -- [C01 — Current in-process architecture, capability gates and managed deployment.](https://github.com/CheatEngineNet/CheatEngine.Client/blob/923a4ded85898f53ef4cd2ff5872d2fd9001071a/README.md) -- [G01 — Issue API; filter pull_request entries from issue lists.](https://docs.github.com/en/rest/issues/issues) -- [G02 — Repository-specific milestones.](https://docs.github.com/en/rest/issues/milestones) -- [G03 — Native hierarchy uses issue IDs, not issue numbers.](https://docs.github.com/en/rest/issues/sub-issues) -- [G04 — Native blocked-by relationship with the blocker issue_id.](https://docs.github.com/en/rest/issues/issue-dependencies) - -### Maintainer notes - -Keep execution updates, actual commands/results, decisions, and refinements here. The bootstrap does not overwrite an existing issue body on rerun. diff --git a/docs/live-capability-gates.md b/docs/live-capability-gates.md deleted file mode 100644 index dff1bc8..0000000 --- a/docs/live-capability-gates.md +++ /dev/null @@ -1,146 +0,0 @@ -# Cheat Engine 7.7 x64 live-capability gates - -## Purpose and promotion rule - -This document is the release gate for advanced CheatEngine.Client capabilities. It is deliberately stricter than a build, unit test, package smoke test, or Native AOT probe: every listed operation crosses a Cheat Engine-owned Lua, ABI, callback, or ownership boundary whose real lifecycle must be observed in the supported host. - -Every capability below starts as Unknown or Unavailable. A public interface, a completed implementation, an SDK binding, or an ordinary-CI result is not evidence that it may return Available. The only promotion rule is: - -> A capability may return Available only after its complete domain scenario below has passed in Cheat Engine 7.7 x64, the required evidence has been captured and reviewed, and the result has been linked from the capability matrix. A partial result, another CE version or architecture, or a run without cleanup, disable, re-enable, and target replacement leaves the capability Unknown or Unavailable. - -Run gates only against a local process the operator is authorised to control and that is disposable. Use a purpose-made test executable with known memory, code, file, and timing behaviour. Never use a production process, an online game, or a target that cannot be restored. The test must use an actual 64-bit Cheat Engine 7.7 host rather than an SDK fake or simulated Lua runtime. - -The supported artifact remains a framework-dependent managed net10.0 plugin folder. Client libraries are analysed for trimming and Native AOT compatibility, and the AOT probe validates that analysis. Neither analysis nor the probe means Cheat Engine can load a Native AOT plugin DLL. Exercise the managed plugin with its .deps.json, .runtimeconfig.json, Client and SDK assemblies, and native SDK bridge deployed together. - -## Common protocol and evidence - -Capture this bundle for every domain: - -1. **Host:** CE version/build, x64, Windows version, Client/SDK package or commit versions, plugin hash, timestamp. -2. **Target:** fixture executable path/hash, PID, bitness, selection epoch, and confirmation it is local, authorised, and disposable. Do not log sensitive target-memory contents. -3. **Operation:** request metadata, copied Client result, stable CheatEngineFailure on errors, and host-side evidence (log, UI screenshot, process/module or memory-map observation) of the intended effect. -4. **Lifecycle:** enable, success, intentional failure, cleanup, disable, fresh re-enable, and target replacement; record Client epoch and capability observation at every boundary. -5. **Negative checks:** rejected work makes no host mutation; stale leases/callbacks never act in a new epoch; no callback blocks a Cheat Engine or Lua thread. - -The raw log remains access controlled; the attached review summary is redacted. It names the exact host, SDK, and Client versions. A changed SDK ABI or CE version requires a new relevant gate run; older evidence is not inherited. - -| Required phase | Assertion | -|---|---| -| Initial state | The service is resolvable, returns Unknown/Unavailable, and makes no speculative mutation. | -| Success | Only copied Client data or activation-scoped Client leases return; the host effect is independently observed. | -| Expected failure | Invalid input, unavailable primitive, and host error become stable failures; no partial resource, callback, patch, or allocation survives. | -| Cleanup | A lease disposes once in the documented order and the host returns to its recorded baseline. | -| Disable | Admission closes before SDK/Lua teardown; Client resources are neutralised and released while the SDK context is valid. | -| Re-enable | A new Client epoch has only new resources; old leases, callbacks, and subscriptions are rejected or inert. | -| Target change | Old selection-bound resources are reclaimed/invalidated before a new PID is used and cannot act on it. | - -## Allocations — Allocations - -**Preconditions.** Select the disposable local target, record PID and selection epoch, and choose a positive bounded size with a documented protection mode. The allocation must not enter a cache or pool. Start with the Allocations capability observed as Unknown/Unavailable. - -**Success.** Allocate(TargetAllocationRequest) returns ITargetMemoryLease with a non-zero address, requested size, and current selection epoch. Verify the range belongs to the selected PID and, where supported, write/read one bounded fixture value. No SDK owner or raw handle may escape in its result. - -**Failure, cleanup, and lifecycle.** Exercise zero, oversized, unsupported, and injected SDK allocation failures; each must yield CapabilityUnavailable or a classified failure without a remote range leak. Dispose a successful lease twice and prove deallocation occurs at most once. Selection change and disable release the old range before detachment; an old lease cannot touch the replacement target. Re-enable then allocates a fresh range in a new epoch. - -**Evidence.** Capture address, size, PID, old/new epochs, allocation/deallocation statuses, memory-map evidence, and no-leak proof after failure, disable, and target replacement. - -## Assembly and Auto Assembler — Assembly - -**Preconditions.** Use a known instruction sequence in the fixture. Capture original bounded byte and disassembly snapshots. A test Auto Assembler script is reversible, has a valid [DISABLE] path, contains neither targetSelf nor host pointers, and touches only the fixture target. Start Unknown/Unavailable. - -**Success.** Verify copied disassembly, instruction size, previous-instruction lookup, comments, and a one-instruction assembly result. Apply the reversible script and obtain IAutoAssemblerPatchLease; independently observe its one intended effect. Dispose it and prove the SDK executes retained disableInfo once and restores original bytes. - -**Failure, cleanup, and lifecycle.** Test malformed assembly, invalid address, and a script failing part way through. Each returns a classified error and leaves original bytes intact; a partially-created patch rolls back. Double disposal cannot execute [DISABLE] twice. Disable and target change restore a live patch before Lua detaches; an old lease cannot disable a patch from a new epoch or PID. Re-enable uses a newly created lease only. - -**Evidence.** Capture script hash, original/patched/restored byte hashes, copied instruction values, disableInfo status, PID/epochs, failures, and host disassembly evidence. - -## Remote execution and DLL injection — RemoteExecution - -**Preconditions.** Select the authorised disposable x64 target. Supply a trusted target-compatible test DLL using an existing absolute path. Calls use a strictly positive finite timeout; parameters flow through a Client-owned allocation lease, never a caller-supplied host pointer. Begin Unknown/Unavailable. - -**Success.** Demonstrate deterministic injection and a harmless exported fixture action with a bounded copied result. Verify parameter allocation belongs to the PID, the action honours its timeout, and allocation is released on both normal completion and remote error. A public result retains no remote pointer. - -**Failure, cleanup, and lifecycle.** Reject relative, missing, inaccessible, and wrong-bitness DLL paths; zero or negative timeout; bad parameters; remote exception; and timeout. None may leak an allocation, pending callback, or unrelated mutation. If CE cannot safely unload an injected DLL, state that limitation and use a new fixture process for remaining phases instead of claiming unload semantics. Disable while a call is pending, re-enable, and prove old state cannot complete into the new epoch. On target change, release old allocations/call state before new PID use. - -**Evidence.** Capture canonical DLL path/hash, PID/epochs, timeout, allocation lifecycle, exit/result status, host logs, and process-module evidence. - -## Debugger and breakpoints — Debugger - -**Preconditions.** Use a debugger-safe fixture with a known breakpoint address and controlled trigger. An IBreakpointLease callback receives a copied event and returns BreakpointDisposition synchronously; it never awaits an async consumer or blocks Lua/CE. Any event stream has positive capacity and named overflow policy. Begin Unknown/Unavailable. - -**Success.** Trigger once and verify copied address/register/context values and immediate disposition against fixture behaviour. Saturate an optional stream to prove its selected overflow policy and observable drop count without blocking the producer. - -**Failure, cleanup, and lifecycle.** Invalid/unmapped registration and a handler exception leave no armed breakpoint or uncaught host exception. Dispose, trigger again, and prove no admission. On disable: close admission, neutralise the SDK callback, complete streams, release the lease, then detach Lua. Re-enable has a new scope; a retained old closure/subscription is inert. Changing target before trigger never delivers old breakpoint work to the new PID. - -**Evidence.** Capture breakpoint IDs, event sequence, disposition, stream capacity/drop count, callback-thread proof, PID/epochs, and removal proof. - -## Hotkeys — Hotkeys - -**Preconditions.** Choose a non-conflicting reversible fixture chord in a controlled environment. A handler receives only copied events. An asynchronous projection always supplies positive-capacity EventStreamOptions and an explicit overflow policy (DropOldest default, DropNewest, or FailSubscription). Begin Unknown/Unavailable. - -**Success.** Register IHotkeyLease, trigger the chord through the host, and prove exactly one copied event reaches the active epoch. Deliberately fill a stream and record loss behaviour without blocking the Lua/registration thread. - -**Failure, cleanup, and lifecycle.** A duplicate/conflicting chord and handler failure leave no partial registration or host exception. Disposal prevents the chord reaching the plugin. Disable closes admission, neutralises callbacks, completes subscribers, releases host hotkey, then permits Lua detach. A fresh re-enable lease works while the former delegate/stream remains inert. Changing target must not retain old selection-bound state or direct work to it. - -**Evidence.** Capture chord, registration/removal statuses, event/loss counts, epochs, and host UI/log proof. - -## Timers — Timers - -**Preconditions.** Use a positive bounded interval and callbacks that record copied timestamps only. Every async consumer uses positive-capacity EventStreamOptions; a timer callback never waits for a consumer or calls a blocking dispatcher. Begin Unknown/Unavailable. - -**Success.** Acquire ITimerLease, observe a bounded number of ticks within documented tolerance, and prove timer and stream are activation-scoped. Fill the stream to verify overflow/drop accounting and a non-blocked CE/Lua thread. - -**Failure, cleanup, and lifecycle.** Zero, negative, unsupported interval and injected host creation failure leave no timer. After disposal, wait more than two intervals and prove no admission. Disable closes admission, neutralises callback, completes stream, and frees the timer before Lua teardown. Re-enable creates a new timer/scope; an old lease cannot stop or receive it. A target replacement must not cause a tick to use an old selection epoch. - -**Evidence.** Capture requested/observed intervals, tick/event/drop counts, timer IDs, callback-thread proof, PID/epochs, and final stop evidence. - -## Speed — Speed - -**Preconditions.** Record the host baseline speed and use a disposable local fixture. Inputs are finite and strictly positive. Speed is host-wide rather than a target allocation lease, so the gate explicitly restores baseline; Client must not silently initialise or reset it. Begin Unknown/Unavailable. - -**Success.** Read a copied baseline, set a known finite positive multiplier, read it back, and verify the fixture's timing change within a documented tolerance. Restore the exact recorded baseline as the test cleanup action. - -**Failure, cleanup, and lifecycle.** Reject zero, negative, NaN, and infinities before host mutation. A host error leaves the last valid value intact. At disable, re-enable, and target change, observe/record state without assuming per-process ownership, then restore the pre-test setting before finishing. - -**Evidence.** Capture baseline/request/read-back values, timing samples/tolerance, validation/host failures, host setting evidence, PID/epochs, and baseline-restoration proof. - -## Hashing — Hashing - -**Preconditions.** Test target-memory hashing and file hashing separately. For memory, select the local fixture and a bounded readable known-byte range. For files, use an authorised local fixture with a known digest. Begin Unknown/Unavailable. - -**Success.** Hash each input independently and compare copied digests with external known values. Define any same-content equivalence only under the documented algorithm/encoding. Neither operation mutates target memory or a file. - -**Failure, cleanup, and lifecycle.** Exercise unreadable/out-of-range memory, missing/unauthorised file, invalid range/algorithm, and selection change during memory hashing. Each returns a stable failure without a borrowed buffer, open file handle, or hashing the replacement PID. The service has no host lease, but disable/re-enable safely stop admission and a prior-epoch result is never reported as current. - -**Evidence.** Capture algorithm, bounded range metadata or canonical fixture path/hash, expected/actual digest, errors, resource checks, PID/epochs, and redacted no-content-logging proof. - -## DBVM observation, initialisation, and watches — Dbvm - -**Preconditions.** Use only a dedicated, authorised VM test environment whose hardware/OS support is understood by the operator. Observe DBVM state first: Client never initialises DBVM implicitly. Explicit initialisation requires a separate operator-approved rollback/VM-reset plan. Begin Unknown/Unavailable. - -**Success.** Prove state observation is side-effect free. Only when explicit initialisation is authorised, record pre/post state and install IDbvmWatchLease on a harmless fixture range. Verify copied events, bounded stream behaviour, and immediate non-blocking callback handling. - -**Failure, cleanup, and lifecycle.** Unsupported hardware, denied init, invalid range, and callback failure may not initialise DBVM or leave a watch active. Dispose watch and prove removal. If no safe in-session DBVM shutdown exists, document it, use the approved VM reset, and do not claim a Client deinitialisation guarantee. Disable closes admission, neutralises callbacks, completes streams, and removes watches before Lua detach. Re-enable creates fresh watches; selection change removes old watches before a new PID. - -**Evidence.** Capture observation/explicit-init decision, hardware/VM facts, watch IDs/ranges, stream/drop counts, lifecycle logs, reset proof where needed, and PID/epochs. - -## Value scanning — ValueScanning - -**Preconditions.** Select a fixture exposing known mutable values in a bounded documented region. SDK must have a production factory that creates and owns MemScan plus FoundList; Client must not hand-construct or adopt an unproven owner. Begin Unknown/Unavailable even if the public state machine exists. - -**Success.** Create one session, perform first scan, mutate the fixture, perform next scan, and read bounded copied result pages. Prove factory construction order is parent scan then child found-list. Confirm Client metadata exposes no LuaState, LuaRef, CEObject, Owned, borrowed Lua value, or raw pointer. - -**Failure, cleanup, and lifecycle.** Inject child creation failure after parent creation and prove parent rollback. Exercise invalid requests, cancellation before admission, empty/no-result scan, and failure during next scan. Dispose and prove child is destroyed before parent; disposal is idempotent and no owner remains after disable or selection change. Re-enable creates a new session; retained old page/session cannot scan or read new epoch data. - -**Evidence.** Capture request/result counts without sensitive values, parent/child create-destroy order, page bounds, cancellation/failure statuses, PID/epochs, CE scan UI/log evidence, and leak checks. - -## Recording the decision - -Attach the evidence bundle to the release issue or capability matrix, with exact host/SDK/Client versions and a pass/fail result for every phase. The review must affirm all four points: - -1. The managed plugin artifact ran in Cheat Engine 7.7 x64. -2. Success, expected failure, cleanup, disable, re-enable, and target replacement passed. -3. Ownership, epochs, callback admission, and Lua detachment were observed rather than inferred. -4. The high-level boundary holds: normal Client code sees copied data and leases only, never SDK/Lua handles or raw pointers. - -Only four affirmative answers and the domain-specific evidence permit Available. Otherwise keep Unknown/Unavailable, retain the published contract if any, and exclude the feature from unguarded examples. diff --git a/eng/Invoke-LivePluginCoexistenceFixture.ps1 b/eng/Invoke-LivePluginCoexistenceFixture.ps1 deleted file mode 100644 index b7587a6..0000000 --- a/eng/Invoke-LivePluginCoexistenceFixture.ps1 +++ /dev/null @@ -1,319 +0,0 @@ -[CmdletBinding()] -param( - [switch]$Build, - - [ValidateSet('Debug', 'Release')] - [string]$Configuration = 'Release', - - [string]$PluginASdkVersion = '1.0.0', - - [string]$PluginBSdkVersion = '1.0.0', - - [string]$PluginCollisionSdkVersion = '1.0.0', - - [string]$BundleRoot, - - [string]$PluginABundlePath, - - [string]$PluginBBundlePath, - - [string]$PluginCollisionBundlePath, - - [string]$ReceiptPath -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$repositoryRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..')) -$fixtureRoot = Join-Path $repositoryRoot 'tests/CheatEngine.Client.LivePlugin.Coexistence' -$pluginAProject = Join-Path $fixtureRoot 'PluginA/CheatEngine.Client.LivePlugin.Coexistence.PluginA.csproj' -$pluginBProject = Join-Path $fixtureRoot 'PluginB/CheatEngine.Client.LivePlugin.Coexistence.PluginB.csproj' -$pluginCollisionProject = Join-Path $fixtureRoot 'PluginCollision/CheatEngine.Client.LivePlugin.Coexistence.PluginCollision.csproj' - -function Resolve-ConcreteDirectory { - param( - [Parameter(Mandatory)] [string]$Path, - [Parameter(Mandatory)] [string]$Label - ) - - if (-not (Test-Path -LiteralPath $Path -PathType Container)) { - throw "$Label directory '$Path' does not exist." - } - - return (Resolve-Path -LiteralPath $Path).Path -} - -function Assert-FreshBundleRoot { - param([Parameter(Mandatory)] [string]$Path) - - if (Test-Path -LiteralPath $Path) { - throw "Refusing to reuse coexistence bundle root '$Path'. Use a newly created, empty path so stale dependencies cannot qualify as part of a closure." - } -} - -function Invoke-FixtureBuild { - param( - [Parameter(Mandatory)] [string]$ProjectPath, - [Parameter(Mandatory)] [string]$SdkVersion, - [Parameter(Mandatory)] [string]$DeploymentPath - ) - - & dotnet build $ProjectPath --configuration $Configuration ` - "-p:CoexistenceSdkPackageVersion=$SdkVersion" ` - "-p:CheatEnginePluginOutputPath=$DeploymentPath" - if ($LASTEXITCODE -ne 0) { - throw "Fixture build failed for '$ProjectPath' (SDK package '$SdkVersion')." - } -} - -function Get-DependencyAssets { - param( - [Parameter(Mandatory)] [object]$Deps, - [Parameter(Mandatory)] [string]$BundlePath, - [Parameter(Mandatory)] [string]$Label - ) - - $targetProperties = @($Deps.targets.PSObject.Properties) - if ($targetProperties.Count -ne 1) { - throw "$Label dependency manifest must contain exactly one runtime target; found $($targetProperties.Count)." - } - - $assetPaths = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) - foreach ($library in $targetProperties[0].Value.PSObject.Properties) { - foreach ($sectionName in @('runtime', 'native')) { - $sectionProperty = $library.Value.PSObject.Properties[$sectionName] - if ($null -eq $sectionProperty) { - continue - } - - $section = $sectionProperty.Value - - foreach ($asset in $section.PSObject.Properties) { - $relativePath = $asset.Name.Replace('/', [IO.Path]::DirectorySeparatorChar) - $assetPath = Join-Path $BundlePath $relativePath - if (-not (Test-Path -LiteralPath $assetPath -PathType Leaf)) { - # NuGet's deps manifest names package assets (for example lib/net10.0/foo.dll), whereas a managed - # plugin deployment flattens its runtime closure beside the plugin DLL. - $assetPath = Join-Path $BundlePath ([IO.Path]::GetFileName($relativePath)) - if (-not (Test-Path -LiteralPath $assetPath -PathType Leaf)) { - throw "$Label bundle is missing dependency asset '$($asset.Name)' declared by '$($library.Name)'." - } - } - - [void]$assetPaths.Add($relativePath) - } - } - } - - return @($assetPaths | Sort-Object) -} - -function Get-ResolvedSdkPackage { - param( - [Parameter(Mandatory)] [string]$ProjectPath, - [Parameter(Mandatory)] [string]$Label, - [Parameter(Mandatory)] [string]$SdkAssemblyPath, - [Parameter(Mandatory)] [string]$RequestedSdkVersion - ) - - $projectName = [IO.Path]::GetFileNameWithoutExtension($ProjectPath) - $assetsPath = Join-Path $repositoryRoot "artifacts/obj/$projectName/project.assets.json" - if (-not (Test-Path -LiteralPath $assetsPath -PathType Leaf)) { - throw "$Label has no restored project.assets.json at '$assetsPath'." - } - - $assets = Get-Content -LiteralPath $assetsPath -Raw | ConvertFrom-Json - $sdkLibrary = @($assets.libraries.PSObject.Properties | Where-Object Name -like 'CheatEngine.SDK/*') - if ($sdkLibrary.Count -ne 1) { - throw "$Label must resolve exactly one CheatEngine.SDK package; found $($sdkLibrary.Count)." - } - - $resolvedVersion = $sdkLibrary[0].Name.Substring('CheatEngine.SDK/'.Length) - if ($resolvedVersion -ne $RequestedSdkVersion) { - throw "$Label resolved CheatEngine.SDK '$resolvedVersion', not requested '$RequestedSdkVersion'." - } - - $targetProperties = @($assets.targets.PSObject.Properties) - if ($targetProperties.Count -ne 1) { - throw "$Label restored project.assets.json must contain exactly one runtime target; found $($targetProperties.Count)." - } - - $sdkTarget = $targetProperties[0].Value.PSObject.Properties[$sdkLibrary[0].Name] - if ($null -eq $sdkTarget -or $null -eq $sdkTarget.Value.runtime) { - throw "$Label restored CheatEngine.SDK package has no runtime assets." - } - - $runtimeAssemblyPaths = @( - $sdkTarget.Value.runtime.PSObject.Properties.Name | - Where-Object { $_ -match '^lib/[^/]+/CheatEngine\.SDK\.dll$' } - ) - if ($runtimeAssemblyPaths.Count -ne 1) { - throw "$Label restored CheatEngine.SDK package must expose exactly one CheatEngine.SDK runtime assembly; found $($runtimeAssemblyPaths.Count)." - } - - $bundleAssemblyHash = (Get-FileHash -LiteralPath $SdkAssemblyPath -Algorithm SHA256).Hash - $packageRelativePath = $sdkLibrary[0].Value.path.Replace('/', [IO.Path]::DirectorySeparatorChar) - $runtimeAssemblyRelativePath = $runtimeAssemblyPaths[0].Replace('/', [IO.Path]::DirectorySeparatorChar) - foreach ($packageFolder in $assets.packageFolders.PSObject.Properties.Name) { - $packageAssemblyPath = Join-Path (Join-Path $packageFolder $packageRelativePath) $runtimeAssemblyRelativePath - if ((Test-Path -LiteralPath $packageAssemblyPath -PathType Leaf) -and - (Get-FileHash -LiteralPath $packageAssemblyPath -Algorithm SHA256).Hash -eq $bundleAssemblyHash) { - return [ordered]@{ - Id = 'CheatEngine.SDK' - Version = $resolvedVersion - ContentHash = $sdkLibrary[0].Value.sha512 - Verification = 'VerifiedPackageAssemblyMatch' - BundleAssemblySha256 = $bundleAssemblyHash - } - } - } - - return [ordered]@{ - Id = 'CheatEngine.SDK' - Verification = 'UnverifiedBundleAssembly' - BundleAssemblySha256 = $bundleAssemblyHash - Reason = 'The supplied bundle CheatEngine.SDK.dll did not match the runtime assembly from the resolved approved package.' - } -} - -function Get-BundleReceipt { - param( - [Parameter(Mandatory)] [string]$Label, - [Parameter(Mandatory)] [string]$BundlePath, - [Parameter(Mandatory)] [string]$ProjectPath, - [Parameter(Mandatory)] [string]$PluginAssemblyName, - [Parameter(Mandatory)] [string]$RequestedSdkVersion - ) - - $resolvedBundle = Resolve-ConcreteDirectory -Path $BundlePath -Label $Label - $pluginAssemblyPath = Join-Path $resolvedBundle "$PluginAssemblyName.dll" - $depsPath = Join-Path $resolvedBundle "$PluginAssemblyName.deps.json" - $runtimeConfigPath = Join-Path $resolvedBundle "$PluginAssemblyName.runtimeconfig.json" - $requiredFiles = @( - $pluginAssemblyPath, - $depsPath, - $runtimeConfigPath, - (Join-Path $resolvedBundle 'CheatEngine.SDK.dll'), - (Join-Path $resolvedBundle 'CheatEngine.Client.Abstractions.dll'), - (Join-Path $resolvedBundle 'CheatEngine.Client.Core.dll'), - (Join-Path $resolvedBundle 'CheatEngine.Client.Extensions.DependencyInjection.dll'), - (Join-Path $resolvedBundle 'CheatEngine.Client.Hosting.dll'), - (Join-Path $resolvedBundle 'cheatengine-sdk-lua-bridge.dll') - ) - foreach ($requiredFile in $requiredFiles) { - if (-not (Test-Path -LiteralPath $requiredFile -PathType Leaf)) { - throw "$Label bundle is missing required deployment file '$requiredFile'." - } - } - - $deps = Get-Content -LiteralPath $depsPath -Raw | ConvertFrom-Json - $dependencyAssets = Get-DependencyAssets -Deps $deps -BundlePath $resolvedBundle -Label $Label - $sdkAssemblyPath = Join-Path $resolvedBundle 'CheatEngine.SDK.dll' - $sdkPackage = Get-ResolvedSdkPackage -ProjectPath $ProjectPath -Label $Label -SdkAssemblyPath $sdkAssemblyPath ` - -RequestedSdkVersion $RequestedSdkVersion - - $fileRecords = @( - Get-ChildItem -LiteralPath $resolvedBundle -File -Recurse | - Sort-Object FullName | - ForEach-Object { - [ordered]@{ - Path = [IO.Path]::GetRelativePath($resolvedBundle, $_.FullName).Replace('\', '/') - Length = $_.Length - Sha256 = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash - } - } - ) - - $sdkAssembly = [Reflection.AssemblyName]::GetAssemblyName($sdkAssemblyPath) - return [ordered]@{ - Label = $Label - BundlePath = $resolvedBundle - PluginAssembly = [ordered]@{ - Name = $PluginAssemblyName - Sha256 = (Get-FileHash -LiteralPath $pluginAssemblyPath -Algorithm SHA256).Hash - } - SdkPackage = $sdkPackage - SdkAssemblyIdentity = $sdkAssembly.FullName - DependencyAssets = $dependencyAssets - Files = $fileRecords - } -} - -if ($Build) { - if ([string]::IsNullOrWhiteSpace($BundleRoot)) { - $BundleRoot = Join-Path $repositoryRoot ("artifacts/live-plugin-coexistence/" + [Guid]::NewGuid().ToString('N')) - } - - $resolvedBundleRoot = [IO.Path]::GetFullPath($BundleRoot) - Assert-FreshBundleRoot -Path $resolvedBundleRoot - $PluginABundlePath = Join-Path $resolvedBundleRoot 'PluginA' - $PluginBBundlePath = Join-Path $resolvedBundleRoot 'PluginB' - $PluginCollisionBundlePath = Join-Path $resolvedBundleRoot 'PluginCollision' - - Invoke-FixtureBuild -ProjectPath $pluginAProject -SdkVersion $PluginASdkVersion -DeploymentPath $PluginABundlePath - Invoke-FixtureBuild -ProjectPath $pluginBProject -SdkVersion $PluginBSdkVersion -DeploymentPath $PluginBBundlePath - Invoke-FixtureBuild -ProjectPath $pluginCollisionProject -SdkVersion $PluginCollisionSdkVersion -DeploymentPath $PluginCollisionBundlePath -} -else { - if ([string]::IsNullOrWhiteSpace($PluginABundlePath) -or [string]::IsNullOrWhiteSpace($PluginBBundlePath) -or - [string]::IsNullOrWhiteSpace($PluginCollisionBundlePath)) { - throw 'Supply -Build or all three existing bundle paths.' - } -} - -$pluginAReceipt = Get-BundleReceipt -Label 'PluginA' -BundlePath $PluginABundlePath -ProjectPath $pluginAProject ` - -PluginAssemblyName 'CheatEngine.Client.LivePlugin.Coexistence.PluginA' -RequestedSdkVersion $PluginASdkVersion -$pluginBReceipt = Get-BundleReceipt -Label 'PluginB' -BundlePath $PluginBBundlePath -ProjectPath $pluginBProject ` - -PluginAssemblyName 'CheatEngine.Client.LivePlugin.Coexistence.PluginB' -RequestedSdkVersion $PluginBSdkVersion -$pluginCollisionReceipt = Get-BundleReceipt -Label 'PluginCollision' -BundlePath $PluginCollisionBundlePath ` - -ProjectPath $pluginCollisionProject -PluginAssemblyName 'CheatEngine.Client.LivePlugin.Coexistence.PluginCollision' ` - -RequestedSdkVersion $PluginCollisionSdkVersion - -$bundlePaths = @($pluginAReceipt.BundlePath, $pluginBReceipt.BundlePath, $pluginCollisionReceipt.BundlePath) -for ($first = 0; $first -lt $bundlePaths.Count; $first++) { - for ($second = $first + 1; $second -lt $bundlePaths.Count; $second++) { - if ($bundlePaths[$first] -eq $bundlePaths[$second] -or - $bundlePaths[$first].StartsWith($bundlePaths[$second] + [IO.Path]::DirectorySeparatorChar, - [StringComparison]::OrdinalIgnoreCase) -or - $bundlePaths[$second].StartsWith($bundlePaths[$first] + [IO.Path]::DirectorySeparatorChar, - [StringComparison]::OrdinalIgnoreCase)) { - throw "Coexistence bundles must be disjoint directories; found '$($bundlePaths[$first])' and '$($bundlePaths[$second])'." - } - } -} - -if ([string]::IsNullOrWhiteSpace($ReceiptPath)) { - $receiptBase = if ($Build) { Split-Path -Parent $PluginABundlePath } else { [IO.Path]::GetTempPath() } - $ReceiptPath = Join-Path $receiptBase 'coexistence-build-receipt.json' -} - -$resolvedReceiptPath = [IO.Path]::GetFullPath($ReceiptPath) -$receiptDirectory = Split-Path -Parent $resolvedReceiptPath -if (-not [string]::IsNullOrWhiteSpace($receiptDirectory)) { - [IO.Directory]::CreateDirectory($receiptDirectory) | Out-Null -} - -$receipt = [ordered]@{ - Schema = 'CheatEngine.Client.LivePlugin.Coexistence.Receipt/v1' - EvidenceState = 'BuildPrepared_NotLiveQualified' - GeneratedAtUtc = [DateTime]::UtcNow.ToString('O', [Globalization.CultureInfo]::InvariantCulture) - Runner = [ordered]@{ - Path = $PSCommandPath - BuildPerformed = [bool]$Build - Configuration = $Configuration - } - Qualification = [ordered]@{ - HostRun = 'Not executed by this runner' - Collision = 'Specified; requires controlled-host transcript' - DisableOneSurvivesOther = 'Specified; requires controlled-host transcript' - TargetSwitch = 'Specified; requires two authorized disposable targets and controlled-host transcript' - RetainedOwner = 'Specified; current Client tuple can report capability unavailable and must not be counted as a pass' - SideBySideSdk = 'Specified; package tuples are recorded but loader isolation remains host-qualified' - } - Bundles = @($pluginAReceipt, $pluginBReceipt, $pluginCollisionReceipt) -} - -$receipt | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $resolvedReceiptPath -Encoding utf8NoBOM -Write-Host "Prepared and verified three isolated plugin bundles. Receipt: $resolvedReceiptPath" -Write-Host 'No Cheat Engine process was started, inspected, attached, configured, or modified. This is build/package-layout evidence only.' diff --git a/eng/Invoke-PackageSmoke.ps1 b/eng/Invoke-PackageSmoke.ps1 deleted file mode 100644 index 559bd1b..0000000 --- a/eng/Invoke-PackageSmoke.ps1 +++ /dev/null @@ -1,386 +0,0 @@ -[CmdletBinding()] -param( - [Parameter(Mandatory)] - [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })] - [string]$PackageSource, - - [ValidatePattern('^\d+\.\d+\.\d+([-.].+)?$')] - [string]$ClientVersion = '0.1.0' -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$resolvedPackageSource = (Resolve-Path -LiteralPath $PackageSource).Path -$expectedPackage = Join-Path $resolvedPackageSource "CheatEngine.Client.$ClientVersion.nupkg" -if (-not (Test-Path -LiteralPath $expectedPackage -PathType Leaf)) { - throw "Expected package '$expectedPackage' was not found. Run dotnet pack before the smoke test." -} - -$expectedHostingPackage = Join-Path $resolvedPackageSource "CheatEngine.Client.Hosting.$ClientVersion.nupkg" -if (-not (Test-Path -LiteralPath $expectedHostingPackage -PathType Leaf)) { - throw "Expected package '$expectedHostingPackage' was not found. Run dotnet pack before the smoke test." -} - -$temporaryBase = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()) -$smokeDirectory = [IO.Path]::GetFullPath((Join-Path $temporaryBase ("CheatEngine.Client.PackageSmoke." + [Guid]::NewGuid().ToString('N')))) -if (-not $smokeDirectory.StartsWith($temporaryBase, [StringComparison]::OrdinalIgnoreCase)) { - throw "Refusing to use a smoke-test directory outside the system temporary directory: '$smokeDirectory'." -} - -function Write-SmokeProject { - param( - [Parameter(Mandatory)] [string]$ProjectDirectory, - [Parameter(Mandatory)] [bool]$IncludeClientReference, - [Parameter(Mandatory)] [bool]$IncludeSdkReference, - [bool]$IncludeHostingReference = $false, - [ValidateRange(0, 2)] [int]$PluginCount = 1, - [string]$LanguageVersion = '14.0', - [string]$PlatformTarget = 'x64', - [bool]$GenerateEntryPoint = $true, - [bool]$ManualBootstrap = $false - ) - - $clientReference = if ($IncludeClientReference) { - ' ' - } - else { - '' - } - - $sdkReference = if ($IncludeSdkReference) { - ' ' - } - else { - '' - } - - $hostingReference = if ($IncludeHostingReference) { - ' ' - } - else { - '' - } - - $projectXml = @" - - - net10.0 - $LanguageVersion - enable - enable - $PlatformTarget - true - $GenerateEntryPoint - $ManualBootstrap - false - true - obj/Generated - - -$clientReference -$hostingReference -$sdkReference - - -"@ - - Set-Content -LiteralPath (Join-Path $ProjectDirectory 'Smoke.Plugin.csproj') -Value $projectXml -Encoding utf8NoBOM - $pluginClasses = for ($index = 1; $index -le $PluginCount; $index++) { - @" -[CheatEnginePlugin("Package smoke plugin $index")] -public sealed class Plugin$index : CheatEngineClientPlugin -{ - protected override void Configure(CheatEnginePluginBuilder builder) - { - } - - protected override void OnClientEnabled(ICheatEngineClient client) - { - _ = client.Memory.At(default(Address)); - _ = client.Patterns.Aob("00").FirstOrNone(); - } -} -"@ - } - - if ($PluginCount -eq 0) { - $pluginClasses = @' -public sealed class NotAPlugin; -'@ - } - - $manualBootstrapSource = if ($ManualBootstrap) { - @' -namespace CESDK -{ - public static class CESDK - { - public static int CEPluginInitialize(IntPtr initialization, int version) - { - return 1; - } - } -} -'@ - } - else { - '' - } - - $pluginSource = @" -using System; -using CheatEngine.Client; -using CheatEngine.Client.Hosting; -using CheatEngine.Client.Memory; -using CheatEngine.Client.Scanning; -using CheatEngine.SDK.Annotations.Plugin; -using CheatEngine.SDK.Engine.Values; - -$pluginClasses -$manualBootstrapSource -"@ - Set-Content -LiteralPath (Join-Path $ProjectDirectory 'Plugin.cs') -Value $pluginSource -Encoding utf8NoBOM -} - -function Assert-ExpectedBuildFailure { - param( - [Parameter(Mandatory)] [string]$ProjectPath, - [Parameter(Mandatory)] [string]$ExpectedDiagnostic, - [string[]]$AdditionalArguments = @() - ) - - $output = & dotnet build $ProjectPath --configuration Release --no-restore @AdditionalArguments 2>&1 | Out-String - if ($LASTEXITCODE -eq 0) { - throw "The negative isolated plugin build unexpectedly succeeded; expected $ExpectedDiagnostic." - } - if ($output -notmatch $ExpectedDiagnostic) { - throw "The negative isolated plugin build failed, but did not report $ExpectedDiagnostic.`n$output" - } - - # A diagnosed negative build is expected. Clear the native command status only after asserting its exact diagnostic. - $global:LASTEXITCODE = 0 -} - -function Assert-PackageEntries { - param( - [Parameter(Mandatory)] [string]$PackagePath, - [Parameter(Mandatory)] [string[]]$ExpectedEntries - ) - - Add-Type -AssemblyName System.IO.Compression.FileSystem - $archive = [IO.Compression.ZipFile]::OpenRead($PackagePath) - try { - $entries = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase) - foreach ($entry in $archive.Entries) { - [void]$entries.Add($entry.FullName) - } - - foreach ($expectedEntry in $ExpectedEntries) { - if (-not $entries.Contains($expectedEntry)) { - throw "Package '$PackagePath' is missing '$expectedEntry'." - } - } - } - finally { - $archive.Dispose() - } -} - -try { - New-Item -ItemType Directory -Path $smokeDirectory | Out-Null - - Assert-PackageEntries -PackagePath $expectedHostingPackage -ExpectedEntries @( - 'analyzers/dotnet/cs/CheatEngine.Client.SourceGenerators.Lua.dll', - 'buildTransitive/CheatEngine.Client.Hosting.props', - 'buildTransitive/CheatEngine.Client.Hosting.targets' - ) - - $configurationPath = Join-Path $smokeDirectory 'NuGet.Config' - $escapedSource = [Security.SecurityElement]::Escape($resolvedPackageSource) - $escapedPackageCache = [Security.SecurityElement]::Escape((Join-Path $smokeDirectory '.packages')) - $nuGetConfiguration = @" - - - - - - - - - - - -"@ - Set-Content -LiteralPath $configurationPath -Value $nuGetConfiguration -Encoding utf8NoBOM - - $positiveDirectory = Join-Path $smokeDirectory 'positive' - New-Item -ItemType Directory -Path $positiveDirectory | Out-Null - Write-SmokeProject -ProjectDirectory $positiveDirectory -IncludeClientReference $true -IncludeSdkReference $true - & dotnet restore (Join-Path $positiveDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath - if ($LASTEXITCODE -ne 0) { - throw 'The positive isolated package restore failed.' - } - - $deploymentDirectory = Join-Path $smokeDirectory 'deployment' - & dotnet build (Join-Path $positiveDirectory 'Smoke.Plugin.csproj') --configuration Release --no-restore ` - "-p:CheatEnginePluginOutputPath=$deploymentDirectory" - if ($LASTEXITCODE -ne 0) { - throw 'The positive isolated package build failed.' - } - - # Reuse the destination so the deployment task must replace every staged file, not only create a fresh layout. - & dotnet build (Join-Path $positiveDirectory 'Smoke.Plugin.csproj') --configuration Release --no-restore ` - "-p:CheatEnginePluginOutputPath=$deploymentDirectory" - if ($LASTEXITCODE -ne 0) { - throw 'The managed deployment could not atomically replace an existing plugin layout.' - } - - $positiveOutput = Join-Path $positiveDirectory 'bin/Release/net10.0' - $requiredOutputFiles = @( - 'Smoke.Plugin.dll', - 'Smoke.Plugin.deps.json', - 'Smoke.Plugin.runtimeconfig.json', - 'cheatengine-sdk-lua-bridge.dll', - 'CheatEngine.SDK.dll', - 'CheatEngine.Client.Abstractions.dll', - 'CheatEngine.Client.Core.dll', - 'CheatEngine.Client.Fluent.dll', - 'CheatEngine.Client.Extensions.DependencyInjection.dll', - 'CheatEngine.Client.Hosting.dll' - ) - foreach ($requiredOutputFile in $requiredOutputFiles) { - $requiredOutputPath = Join-Path $positiveOutput $requiredOutputFile - if (-not (Test-Path -LiteralPath $requiredOutputPath -PathType Leaf)) { - throw "The positive isolated package output is missing '$requiredOutputFile'." - } - } - - foreach ($requiredDeploymentFile in $requiredOutputFiles) { - $requiredDeploymentPath = Join-Path $deploymentDirectory $requiredDeploymentFile - if (-not (Test-Path -LiteralPath $requiredDeploymentPath -PathType Leaf)) { - throw "The prepared deployment is missing '$requiredDeploymentFile'." - } - } - - $generatedEntryPoints = @(Get-ChildItem -LiteralPath (Join-Path $positiveDirectory 'obj') -Recurse -File | - Where-Object Name -eq 'CheatEngine.SDK.EntryPoint.g.cs') - if ($generatedEntryPoints.Count -ne 1) { - throw "Expected exactly one generated CESDK bootstrap source, found $($generatedEntryPoints.Count)." - } - $generatedEntryPoint = $generatedEntryPoints[0] - $generatedEntryPointText = Get-Content -LiteralPath $generatedEntryPoint.FullName -Raw - if ($generatedEntryPointText -notmatch 'namespace CESDK' -or - $generatedEntryPointText -notmatch 'CEPluginInitialize') { - throw 'The direct SDK reference did not emit the expected CESDK bootstrap source.' - } - - $negativeDirectory = Join-Path $smokeDirectory 'negative' - New-Item -ItemType Directory -Path $negativeDirectory | Out-Null - Write-SmokeProject -ProjectDirectory $negativeDirectory -IncludeClientReference $true -IncludeSdkReference $false - & dotnet restore (Join-Path $negativeDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath - if ($LASTEXITCODE -ne 0) { - throw 'The negative isolated package restore failed before CECLIENT001 could be evaluated.' - } - - Assert-ExpectedBuildFailure -ProjectPath (Join-Path $negativeDirectory 'Smoke.Plugin.csproj') -ExpectedDiagnostic 'CECLIENT001' - - $missingClientDirectory = Join-Path $smokeDirectory 'missing-client' - New-Item -ItemType Directory -Path $missingClientDirectory | Out-Null - Write-SmokeProject -ProjectDirectory $missingClientDirectory -IncludeClientReference $false -IncludeSdkReference $true ` - -IncludeHostingReference $true - & dotnet restore (Join-Path $missingClientDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath - if ($LASTEXITCODE -ne 0) { - throw 'The missing-Client isolated package restore failed before CECLIENT002 could be evaluated.' - } - Assert-ExpectedBuildFailure -ProjectPath (Join-Path $missingClientDirectory 'Smoke.Plugin.csproj') -ExpectedDiagnostic 'CECLIENT002' - - $zeroPluginDirectory = Join-Path $smokeDirectory 'zero-plugin' - New-Item -ItemType Directory -Path $zeroPluginDirectory | Out-Null - Write-SmokeProject -ProjectDirectory $zeroPluginDirectory -IncludeClientReference $true -IncludeSdkReference $true -PluginCount 0 - & dotnet restore (Join-Path $zeroPluginDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath - if ($LASTEXITCODE -ne 0) { - throw 'The zero-plugin isolated package restore failed before CECLIENT003 could be evaluated.' - } - Assert-ExpectedBuildFailure -ProjectPath (Join-Path $zeroPluginDirectory 'Smoke.Plugin.csproj') -ExpectedDiagnostic 'CECLIENT003' - - $multiplePluginDirectory = Join-Path $smokeDirectory 'multiple-plugin' - New-Item -ItemType Directory -Path $multiplePluginDirectory | Out-Null - Write-SmokeProject -ProjectDirectory $multiplePluginDirectory -IncludeClientReference $true -IncludeSdkReference $true ` - -PluginCount 2 -GenerateEntryPoint $false -ManualBootstrap $true - & dotnet restore (Join-Path $multiplePluginDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath - if ($LASTEXITCODE -ne 0) { - throw 'The multiple-plugin isolated package restore failed before CECLIENT004 could be evaluated.' - } - Assert-ExpectedBuildFailure -ProjectPath (Join-Path $multiplePluginDirectory 'Smoke.Plugin.csproj') -ExpectedDiagnostic 'CECLIENT004' - - $manualBootstrapDirectory = Join-Path $smokeDirectory 'manual-bootstrap' - New-Item -ItemType Directory -Path $manualBootstrapDirectory | Out-Null - Write-SmokeProject -ProjectDirectory $manualBootstrapDirectory -IncludeClientReference $true -IncludeSdkReference $true ` - -GenerateEntryPoint $false - & dotnet restore (Join-Path $manualBootstrapDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath - if ($LASTEXITCODE -ne 0) { - throw 'The manual-bootstrap isolated package restore failed before CECLIENT008 could be evaluated.' - } - Assert-ExpectedBuildFailure -ProjectPath (Join-Path $manualBootstrapDirectory 'Smoke.Plugin.csproj') -ExpectedDiagnostic 'CECLIENT008' - - $languageDirectory = Join-Path $smokeDirectory 'language' - New-Item -ItemType Directory -Path $languageDirectory | Out-Null - Write-SmokeProject -ProjectDirectory $languageDirectory -IncludeClientReference $true -IncludeSdkReference $true -LanguageVersion '13.0' - & dotnet restore (Join-Path $languageDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath - if ($LASTEXITCODE -ne 0) { - throw 'The language-version isolated package restore failed before CECLIENT006 could be evaluated.' - } - Assert-ExpectedBuildFailure -ProjectPath (Join-Path $languageDirectory 'Smoke.Plugin.csproj') -ExpectedDiagnostic 'CECLIENT006' - - $platformDirectory = Join-Path $smokeDirectory 'platform' - New-Item -ItemType Directory -Path $platformDirectory | Out-Null - Write-SmokeProject -ProjectDirectory $platformDirectory -IncludeClientReference $true -IncludeSdkReference $true -PlatformTarget 'x86' - & dotnet restore (Join-Path $platformDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath - if ($LASTEXITCODE -ne 0) { - throw 'The platform isolated package restore failed before CECLIENT007 could be evaluated.' - } - Assert-ExpectedBuildFailure -ProjectPath (Join-Path $platformDirectory 'Smoke.Plugin.csproj') -ExpectedDiagnostic 'CECLIENT007' - - $frameworkDirectory = Join-Path $smokeDirectory 'framework' - New-Item -ItemType Directory -Path $frameworkDirectory | Out-Null - Write-SmokeProject -ProjectDirectory $frameworkDirectory -IncludeClientReference $true -IncludeSdkReference $true - & dotnet restore (Join-Path $frameworkDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath - if ($LASTEXITCODE -ne 0) { - throw 'The framework isolated package restore failed before CECLIENT005 could be evaluated.' - } - Assert-ExpectedBuildFailure -ProjectPath (Join-Path $frameworkDirectory 'Smoke.Plugin.csproj') -ExpectedDiagnostic 'CECLIENT005' ` - -AdditionalArguments @('-p:TargetFramework=net9.0') - - $anyCpuDirectory = Join-Path $smokeDirectory 'anycpu' - New-Item -ItemType Directory -Path $anyCpuDirectory | Out-Null - Write-SmokeProject -ProjectDirectory $anyCpuDirectory -IncludeClientReference $true -IncludeSdkReference $true -PlatformTarget 'AnyCPU' - & dotnet restore (Join-Path $anyCpuDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath - if ($LASTEXITCODE -ne 0) { - throw 'The AnyCPU isolated package restore failed.' - } - & dotnet build (Join-Path $anyCpuDirectory 'Smoke.Plugin.csproj') --configuration Release --no-restore - if ($LASTEXITCODE -ne 0) { - throw 'The AnyCPU isolated plugin build failed.' - } - - $manualBootstrapSuccessDirectory = Join-Path $smokeDirectory 'manual-bootstrap-success' - New-Item -ItemType Directory -Path $manualBootstrapSuccessDirectory | Out-Null - Write-SmokeProject -ProjectDirectory $manualBootstrapSuccessDirectory -IncludeClientReference $true -IncludeSdkReference $true ` - -GenerateEntryPoint $false -ManualBootstrap $true - & dotnet restore (Join-Path $manualBootstrapSuccessDirectory 'Smoke.Plugin.csproj') --configfile $configurationPath - if ($LASTEXITCODE -ne 0) { - throw 'The manual-bootstrap-success isolated package restore failed.' - } - & dotnet build (Join-Path $manualBootstrapSuccessDirectory 'Smoke.Plugin.csproj') --configuration Release --no-restore - if ($LASTEXITCODE -ne 0) { - throw 'The explicit manual bootstrap isolated plugin build failed.' - } - - Write-Host 'Package smoke test passed: direct package references, Client plugin profile diagnostics, generated SDK bootstrap, and managed deployment layout are verified.' -} -finally { - if (Test-Path -LiteralPath $smokeDirectory -PathType Container) { - Remove-Item -LiteralPath $smokeDirectory -Recurse -Force - } -} diff --git a/eng/Invoke-TemplateSmoke.ps1 b/eng/Invoke-TemplateSmoke.ps1 deleted file mode 100644 index 7384445..0000000 --- a/eng/Invoke-TemplateSmoke.ps1 +++ /dev/null @@ -1,98 +0,0 @@ -[CmdletBinding()] -param( - [Parameter(Mandatory)] - [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })] - [string]$PackageSource, - - [ValidatePattern('^\d+\.\d+\.\d+([-.].+)?$')] - [string]$TemplateVersion = '0.1.0' -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$resolvedPackageSource = (Resolve-Path -LiteralPath $PackageSource).Path -$templatePackage = Join-Path $resolvedPackageSource "CheatEngine.Client.Templates.$TemplateVersion.nupkg" -if (-not (Test-Path -LiteralPath $templatePackage -PathType Leaf)) { - throw "Expected template package '$templatePackage' was not found. Run dotnet pack before the smoke test." -} - -$temporaryBase = [IO.Path]::GetFullPath([IO.Path]::GetTempPath()) -$smokeDirectory = [IO.Path]::GetFullPath((Join-Path $temporaryBase ("CheatEngine.Client.TemplateSmoke." + [Guid]::NewGuid().ToString('N')))) -if (-not $smokeDirectory.StartsWith($temporaryBase, [StringComparison]::OrdinalIgnoreCase)) { - throw "Refusing to use a smoke-test directory outside the system temporary directory: '$smokeDirectory'." -} - -$previousDotnetCliHome = $env:DOTNET_CLI_HOME -$previousDotnetNewHome = $env:DOTNET_NEW_HOME -try { - New-Item -ItemType Directory -Path $smokeDirectory | Out-Null - $env:DOTNET_CLI_HOME = Join-Path $smokeDirectory '.dotnet-cli' - $env:DOTNET_NEW_HOME = Join-Path $smokeDirectory '.template-engine' - - $configurationPath = Join-Path $smokeDirectory 'NuGet.Config' - $escapedSource = [Security.SecurityElement]::Escape($resolvedPackageSource) - $escapedPackageCache = [Security.SecurityElement]::Escape((Join-Path $smokeDirectory '.packages')) - $nuGetConfiguration = @" - - - - - - - - - - - -"@ - Set-Content -LiteralPath $configurationPath -Value $nuGetConfiguration -Encoding utf8NoBOM - - & dotnet new install $templatePackage --force - if ($LASTEXITCODE -ne 0) { - throw 'Local template installation failed.' - } - - & dotnet new ceplugin --dry-run --name Smoke.Plugin --output (Join-Path $smokeDirectory 'dry-run') - if ($LASTEXITCODE -ne 0) { - throw 'Template dry run failed.' - } - - $instantiatedDirectory = Join-Path $smokeDirectory 'Smoke.Plugin' - & dotnet new ceplugin --name Smoke.Plugin --output $instantiatedDirectory - if ($LASTEXITCODE -ne 0) { - throw 'Template instantiation failed.' - } - - $projectPath = Join-Path $instantiatedDirectory 'Smoke.Plugin.csproj' - & dotnet restore $projectPath --configfile $configurationPath - if ($LASTEXITCODE -ne 0) { - throw 'Instantiated template restore failed.' - } - - & dotnet build $projectPath --configuration Release --no-restore - if ($LASTEXITCODE -ne 0) { - throw 'Instantiated template build failed.' - } - - Write-Host 'Template smoke test passed: local installation, dry run, instantiation, restore, and Release build succeeded.' -} -finally { - if ([string]::IsNullOrEmpty($previousDotnetCliHome)) { - Remove-Item Env:DOTNET_CLI_HOME -ErrorAction SilentlyContinue - } - else { - $env:DOTNET_CLI_HOME = $previousDotnetCliHome - } - - if ([string]::IsNullOrEmpty($previousDotnetNewHome)) { - Remove-Item Env:DOTNET_NEW_HOME -ErrorAction SilentlyContinue - } - else { - $env:DOTNET_NEW_HOME = $previousDotnetNewHome - } - - if (Test-Path -LiteralPath $smokeDirectory -PathType Container) { - Remove-Item -LiteralPath $smokeDirectory -Recurse -Force - } -} diff --git a/eng/Validate-EngineeringManifest.py b/eng/Validate-EngineeringManifest.py deleted file mode 100644 index 70d5ba4..0000000 --- a/eng/Validate-EngineeringManifest.py +++ /dev/null @@ -1,504 +0,0 @@ -"""Validate the versioned Client engineering backlog without contacting GitHub.""" - -from __future__ import annotations - -import argparse -import json -import re -import sys -from collections.abc import Iterable, Mapping -from pathlib import Path -from typing import Any - - -MANIFEST_RELATIVE_PATH = Path("docs/engineering/backlog.json") -WORK_ITEMS_RELATIVE_PATH = Path("docs/engineering/work-items") -ARCHIVE_RECONCILIATION_RELATIVE_PATH = Path("docs/engineering/archive-reconciliation.json") -PLANNING_ID = re.compile(r"^CLI-(?:PLAN|E0[1-8]|0(?:0[1-9]|1[0-9]|2[0-4]))$") -ALLOWED_KINDS = frozenset({"roadmap", "epic", "governance", "bug", "feature", "validation", "research"}) -ALLOWED_PRIORITIES = frozenset({"P1", "P2", "P3"}) -ALLOWED_STATUSES = frozenset( - { - "Needs refinement", - "Contract blocked", - "Artifact blocked", - "Ready", - "In progress", - "In review", - "Qualified", - "Deferred", - } -) -REQUIRED_ITEM_FIELDS = frozenset( - { - "id", - "repository_key", - "kind", - "title", - "parent", - "epic", - "milestone", - "priority", - "observation", - "objective", - "requirements", - "acceptance", - "tests", - "risks", - "blocked_by", - "sources", - "branch", - "pr_title", - "scope_exclusions", - "completion_artifacts", - "mandatory", - "evidence", - "children", - "blocks", - "status", - "repository", - "artifact_gate", - "labels", - "path", - "long_term_impact", - "benefits", - "body_template", - } -) - - -def require(condition: bool, message: str) -> None: - """Raise an always-enabled validation error instead of relying on assert.""" - if not condition: - raise ValueError(message) - - -def require_non_empty_strings(value: Any, field: str, item_id: str) -> list[str]: - require(isinstance(value, list) and value, f"{item_id}: {field} must be a non-empty array.") - require( - all(isinstance(entry, str) and entry.strip() for entry in value), - f"{item_id}: {field} must contain non-empty strings.", - ) - return value - - -def ensure_acyclic(dependencies: Mapping[str, Iterable[str]]) -> None: - """Fail with the cycle path when local blocked-by dependencies are cyclic.""" - state: dict[str, int] = {} - stack: list[str] = [] - - def visit(node: str) -> None: - node_state = state.get(node, 0) - if node_state == 1: - start = stack.index(node) - raise ValueError("Cyclic local blocked_by dependency: " + " -> ".join([*stack[start:], node])) - if node_state == 2: - return - - state[node] = 1 - stack.append(node) - for dependency in dependencies[node]: - visit(dependency) - stack.pop() - state[node] = 2 - - for item_id in dependencies: - visit(item_id) - - -def ensure_acyclic_parent_hierarchy(items: Mapping[str, Mapping[str, Any]]) -> None: - """Fail with the cycle path when the single-parent hierarchy is cyclic.""" - state: dict[str, int] = {} - stack: list[str] = [] - - def visit(item_id: str) -> None: - item_state = state.get(item_id, 0) - if item_state == 1: - start = stack.index(item_id) - raise ValueError("Cyclic parent hierarchy: " + " -> ".join([*stack[start:], item_id])) - if item_state == 2: - return - - state[item_id] = 1 - stack.append(item_id) - parent = items[item_id]["parent"] - if parent is not None: - visit(parent) - stack.pop() - state[item_id] = 2 - - for item_id in items: - visit(item_id) - - -def ensure_connected_parent_hierarchy(items: Mapping[str, Mapping[str, Any]], roadmap_id: str) -> None: - """Fail when any planning item cannot be reached from the roadmap root.""" - visited: set[str] = set() - stack = [roadmap_id] - while stack: - item_id = stack.pop() - if item_id in visited: - continue - visited.add(item_id) - stack.extend(items[item_id]["children"]) - disconnected = sorted(set(items) - visited) - require( - not disconnected, - f"Hierarchy is disconnected from roadmap root {roadmap_id}: {', '.join(disconnected)}.", - ) - - -def validate_parent_hierarchy(items: Mapping[str, Mapping[str, Any]]) -> None: - """Require one connected roadmap-to-epic-to-leaf parent tree.""" - for item_id, item in items.items(): - parent = item["parent"] - require( - parent is None or (isinstance(parent, str) and parent in items), - f"{item_id}: parent references an unknown item.", - ) - children = item["children"] - require(isinstance(children, list), f"{item_id}: children must be an array.") - require( - all(isinstance(child, str) and child in items for child in children), - f"{item_id}: children references an unknown item.", - ) - require(len(children) == len(set(children)), f"{item_id}: children must not contain duplicates.") - - ensure_acyclic_parent_hierarchy(items) - - for item_id, item in items.items(): - parent = item["parent"] - if parent is not None: - require(item_id in items[parent]["children"], f"{item_id}: parent does not list this child.") - for child in item["children"]: - require(items[child]["parent"] == item_id, f"{item_id}: child {child} has a different parent.") - - roadmap_ids = [item_id for item_id, item in items.items() if item["kind"] == "roadmap"] - require( - len(roadmap_ids) == 1, - f"Hierarchy must contain exactly one roadmap root; found {len(roadmap_ids)} roadmap items.", - ) - roadmap_id = roadmap_ids[0] - root_ids = [item_id for item_id, item in items.items() if item["parent"] is None] - require( - root_ids == [roadmap_id], - f"Hierarchy must have exactly one parentless root, roadmap {roadmap_id}; found {', '.join(root_ids) or 'none'}.", - ) - - roadmap = items[roadmap_id] - require(roadmap["milestone"] is None and roadmap["epic"] is None, f"{roadmap_id}: roadmap root cannot have a milestone or epic.") - for child in roadmap["children"]: - require(items[child]["kind"] == "epic", f"{roadmap_id}: roadmap child {child} must be an epic.") - - for item_id, item in items.items(): - if item_id == roadmap_id: - continue - if item["kind"] == "epic": - require(item["parent"] == roadmap_id, f"{item_id}: epic parent must be roadmap root {roadmap_id}.") - require(item["epic"] is None, f"{item_id}: an epic cannot belong to another epic.") - for child in item["children"]: - require( - items[child]["kind"] not in {"roadmap", "epic"}, - f"{item_id}: epic child {child} must be an implementation leaf.", - ) - else: - epic_id = item["epic"] - require( - isinstance(epic_id, str) and epic_id in items and items[epic_id]["kind"] == "epic", - f"{item_id}: leaf must name an existing epic.", - ) - require(item["parent"] == epic_id, f"{item_id}: leaf parent and epic must agree.") - require(not item["children"], f"{item_id}: implementation leaves cannot have children.") - - ensure_connected_parent_hierarchy(items, roadmap_id) - - -def validate_manifest_data(data: Mapping[str, Any]) -> tuple[dict[str, dict[str, Any]], dict[str, int]]: - """Validate backlog schema and local/cross-repository dependency invariants.""" - require(data.get("schema_version") == 1, "backlog.json: schema_version must be 1.") - require(data.get("bootstrap_id") == "ce-engineering-2026-09-21", "backlog.json: unexpected bootstrap_id.") - - repository = data.get("repository") - require(isinstance(repository, dict), "backlog.json: repository must be an object.") - require(repository.get("key") == "client", "backlog.json: repository.key must be client.") - require( - repository.get("repository") == "CheatEngineNet/CheatEngine.Client", - "backlog.json: repository.repository must be CheatEngineNet/CheatEngine.Client.", - ) - - raw_items = data.get("items") - require(isinstance(raw_items, list) and raw_items, "backlog.json: items must be a non-empty array.") - items: dict[str, dict[str, Any]] = {} - for item in raw_items: - require(isinstance(item, dict), "backlog.json: every item must be an object.") - item_id = item.get("id") - require(isinstance(item_id, str) and PLANNING_ID.fullmatch(item_id), f"Invalid planning ID: {item_id!r}.") - require(item_id not in items, f"Duplicate planning ID: {item_id}.") - missing = REQUIRED_ITEM_FIELDS - item.keys() - require(not missing, f"{item_id}: missing required fields: {', '.join(sorted(missing))}.") - require(item["repository_key"] == "client", f"{item_id}: repository_key must be client.") - require( - item["repository"] == repository["repository"], - f"{item_id}: repository does not match manifest repository.", - ) - require(item["kind"] in ALLOWED_KINDS, f"{item_id}: unknown kind {item['kind']!r}.") - require(item["priority"] in ALLOWED_PRIORITIES, f"{item_id}: unknown priority {item['priority']!r}.") - require(item["status"] in ALLOWED_STATUSES, f"{item_id}: unknown status {item['status']!r}.") - require_non_empty_strings(item["requirements"], "requirements", item_id) - require_non_empty_strings(item["acceptance"], "acceptance", item_id) - require_non_empty_strings(item["tests"], "tests", item_id) - require_non_empty_strings(item["sources"], "sources", item_id) - require_non_empty_strings(item["scope_exclusions"], "scope_exclusions", item_id) - require_non_empty_strings(item["completion_artifacts"], "completion_artifacts", item_id) - require(isinstance(item["blocked_by"], list), f"{item_id}: blocked_by must be an array.") - require(isinstance(item["blocks"], list), f"{item_id}: blocks must be an array.") - require(isinstance(item["children"], list), f"{item_id}: children must be an array.") - required_text = [ - item["title"], - item["observation"], - item["objective"], - item["risks"], - item["mandatory"], - item["evidence"], - item["artifact_gate"], - item["long_term_impact"], - item["benefits"], - item["body_template"], - ] - require( - all(isinstance(value, str) and value.strip() for value in required_text), - f"{item_id}: required text fields must be non-empty strings.", - ) - if item["kind"] in {"roadmap", "epic"}: - require( - item["branch"] is None and item["pr_title"] is None, - f"{item_id}: roadmap and epic items must not propose an implementation branch or PR title.", - ) - else: - require( - isinstance(item["branch"], str) - and item["branch"].strip() - and isinstance(item["pr_title"], str) - and item["pr_title"].strip(), - f"{item_id}: implementation leaves must include a branch and PR title.", - ) - marker = f"" - require(marker in item["body_template"], f"{item_id}: body_template has no stable marker.") - expected_path = (WORK_ITEMS_RELATIVE_PATH / f"{item_id}.md").as_posix() - require(item["path"] == expected_path, f"{item_id}: path must be {expected_path}.") - require( - {"ce:bootstrap", f"ce:kind:{item['kind']}", f"ce:priority:{item['priority']}"}.issubset(item["labels"]), - f"{item_id}: labels must include bootstrap, kind, and priority values.", - ) - items[item_id] = item - - milestones = data.get("milestones") - require(isinstance(milestones, list) and milestones, "backlog.json: milestones must be a non-empty array.") - milestone_ids: set[str] = set() - for milestone in milestones: - require(isinstance(milestone, dict), "backlog.json: every milestone must be an object.") - milestone_id = milestone.get("id") - require( - isinstance(milestone_id, str) and re.fullmatch(r"CLI-M[0-6]", milestone_id), - f"Invalid milestone ID: {milestone_id!r}.", - ) - require(milestone_id not in milestone_ids, f"Duplicate milestone ID: {milestone_id}.") - require(milestone.get("repository_key") == "client", f"{milestone_id}: repository_key must be client.") - require( - milestone.get("due_on") is None and milestone.get("release_version") is None, - f"{milestone_id}: bootstrap must not invent a due date or release version.", - ) - milestone_ids.add(milestone_id) - - external_dependencies = data.get("external_dependencies") - require(isinstance(external_dependencies, list), "backlog.json: external_dependencies must be an array.") - external_edges: set[tuple[str, str]] = set() - for edge in external_dependencies: - require(isinstance(edge, dict), "backlog.json: every external dependency must be an object.") - blocker, blocked, relationship = edge.get("blocker"), edge.get("blocked"), edge.get("relationship") - require(isinstance(blocker, str) and blocker.startswith("SDK-"), f"Invalid external blocker: {blocker!r}.") - require(blocked in items, f"External dependency has unknown Client item: {blocked!r}.") - require(relationship == "blocked_by", f"{blocker} -> {blocked}: relationship must be blocked_by.") - require((blocker, blocked) not in external_edges, f"Duplicate external dependency: {blocker} -> {blocked}.") - external_edges.add((blocker, blocked)) - - validate_parent_hierarchy(items) - - local_dependencies: dict[str, list[str]] = {} - for item_id, item in items.items(): - milestone = item["milestone"] - require(milestone is None or milestone in milestone_ids, f"{item_id}: unknown milestone {milestone!r}.") - - local_dependencies[item_id] = [] - for blocker in item["blocked_by"]: - require(isinstance(blocker, str) and blocker, f"{item_id}: blocked_by contains an invalid value.") - if blocker in items: - require( - item_id in items[blocker]["blocks"], - f"{item_id}: local blocker {blocker} lacks the reverse blocks edge.", - ) - local_dependencies[item_id].append(blocker) - else: - require((blocker, item_id) in external_edges, f"{item_id}: undeclared external blocker {blocker}.") - for blocked in item["blocks"]: - require(blocked in items, f"{item_id}: blocks references an unknown item: {blocked!r}.") - require( - item_id in items[blocked]["blocked_by"], - f"{item_id}: blocked item {blocked} lacks the reverse blocked_by edge.", - ) - - ensure_acyclic(local_dependencies) - - for blocker, blocked in external_edges: - require(blocker in items[blocked]["blocked_by"], f"{blocker} -> {blocked}: missing external blocked_by edge.") - - return items, {"items": len(items), "milestones": len(milestone_ids), "external_dependencies": len(external_edges)} - - -def validate_work_item_documents(repository_root: Path, items: Mapping[str, Mapping[str, Any]]) -> None: - """Verify the actual versioned documents behind the manifest item paths.""" - for item_id, item in items.items(): - document = repository_root / item["path"] - require(document.is_file(), f"{item_id}: missing work-item document {item['path']}.") - content = document.read_text(encoding="utf-8") - marker = f"" - require(content.count(marker) == 1, f"{item_id}: document must contain exactly one stable marker.") - require(f"## {item_id} " in content, f"{item_id}: document has no matching level-two heading.") - - -def validate_archive_reconciliation(repository_root: Path, item_ids: set[str]) -> dict[str, int]: - """Validate the checked-in, bounded reconciliation of the supplied review archive.""" - reconciliation_path = repository_root / ARCHIVE_RECONCILIATION_RELATIVE_PATH - require( - reconciliation_path.is_file(), - f"Missing archive reconciliation: {ARCHIVE_RECONCILIATION_RELATIVE_PATH.as_posix()}.", - ) - try: - reconciliation = json.loads(reconciliation_path.read_text(encoding="utf-8")) - except json.JSONDecodeError as exception: - raise ValueError( - f"Invalid JSON in {ARCHIVE_RECONCILIATION_RELATIVE_PATH.as_posix()}: {exception}" - ) from exception - - require(isinstance(reconciliation, dict), "archive-reconciliation.json: root must be an object.") - require(reconciliation.get("schema_version") == 1, "archive-reconciliation.json: schema_version must be 1.") - - archive = reconciliation.get("archive") - require(isinstance(archive, dict), "archive-reconciliation.json: archive must be an object.") - require( - archive.get("file_name") == "CheatEngineNet_Architecture_Review_2026-09-21.zip", - "archive-reconciliation.json: unexpected archive file name.", - ) - require( - isinstance(archive.get("sha256"), str) and re.fullmatch(r"[0-9a-f]{64}", archive["sha256"]), - "archive-reconciliation.json: archive.sha256 must be a lowercase SHA-256 value.", - ) - require( - archive.get("content_manifest") == "SHA256SUMS.json" - and isinstance(archive.get("content_manifest_sha256"), str) - and re.fullmatch(r"[0-9a-f]{64}", archive["content_manifest_sha256"]), - "archive-reconciliation.json: content manifest identity is invalid.", - ) - - counts = reconciliation.get("counts") - require(isinstance(counts, dict), "archive-reconciliation.json: counts must be an object.") - expected_counts = { - "findings": 36, - "specified_product_scenarios": 80, - "ownership_rows": 32, - "capability_rows": 17, - } - require(counts == expected_counts, "archive-reconciliation.json: unexpected archive inventory counts.") - - evidence_levels = reconciliation.get("evidence_levels") - require( - evidence_levels == ["source", "package", "fixture", "live"], - "archive-reconciliation.json: evidence_levels must preserve the source/package/fixture/live boundary.", - ) - ownership = reconciliation.get("ownership_summary") - require( - isinstance(ownership, list) and ownership, - "archive-reconciliation.json: ownership_summary must be non-empty.", - ) - for entry in ownership: - require(isinstance(entry, dict), "archive-reconciliation.json: every ownership entry must be an object.") - require( - all( - isinstance(entry.get(key), str) and entry[key].strip() - for key in ("area", "owner", "client_boundary") - ), - "archive-reconciliation.json: ownership entries require area, owner, and client_boundary.", - ) - require(entry["owner"] in {"SDK", "Client", "Shared"}, "archive-reconciliation.json: unknown ownership owner.") - - groups = reconciliation.get("finding_groups") - require(isinstance(groups, list) and groups, "archive-reconciliation.json: finding_groups must be non-empty.") - reconciled_findings: list[str] = [] - for group in groups: - require(isinstance(group, dict), "archive-reconciliation.json: every finding group must be an object.") - require( - all(isinstance(group.get(key), str) and group[key].strip() for key in ("id", "description")), - "archive-reconciliation.json: finding groups require id and description.", - ) - findings = require_non_empty_strings(group.get("findings"), "findings", group["id"]) - client_items = require_non_empty_strings(group.get("client_items"), "client_items", group["id"]) - require(set(client_items) <= item_ids, f"{group['id']}: references an unknown Client planning item.") - reconciled_findings.extend(findings) - - expected_findings = {f"R{number:02d}" for number in range(1, 37)} - require( - set(reconciled_findings) == expected_findings and len(reconciled_findings) == len(expected_findings), - "archive-reconciliation.json: finding groups must cover R01 through R36 exactly once.", - ) - return { - "archive_findings": counts["findings"], - "specified_product_scenarios": counts["specified_product_scenarios"], - } - - -def validate_repository(repository_root: Path) -> dict[str, int]: - """Validate the checked-in manifest and matching work-item documents.""" - root = repository_root.resolve() - manifest_path = root / MANIFEST_RELATIVE_PATH - require(manifest_path.is_file(), f"Missing engineering manifest: {MANIFEST_RELATIVE_PATH.as_posix()}.") - try: - data = json.loads(manifest_path.read_text(encoding="utf-8")) - except json.JSONDecodeError as exception: - raise ValueError(f"Invalid JSON in {MANIFEST_RELATIVE_PATH.as_posix()}: {exception}") from exception - require(isinstance(data, dict), "backlog.json: root must be an object.") - items, summary = validate_manifest_data(data) - validate_work_item_documents(root, items) - summary.update(validate_archive_reconciliation(root, set(items))) - return summary - - -def parse_arguments() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--repository-root", - type=Path, - default=Path(__file__).resolve().parents[1], - help="Repository root containing docs/engineering/backlog.json (default: script parent).", - ) - return parser.parse_args() - - -def main() -> int: - arguments = parse_arguments() - try: - summary = validate_repository(arguments.repository_root) - except (OSError, ValueError) as exception: - print(f"Engineering manifest validation failed: {exception}", file=sys.stderr) - return 1 - print( - "Validated {items} engineering work items, {milestones} milestones, " - "{external_dependencies} external dependencies, and the {archive_findings}-finding/" - "{specified_product_scenarios}-scenario archive reconciliation; product tests were not executed.".format( - **summary - ) - ) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/eng/tests/test_validate_engineering_manifest.py b/eng/tests/test_validate_engineering_manifest.py deleted file mode 100644 index 2bc5573..0000000 --- a/eng/tests/test_validate_engineering_manifest.py +++ /dev/null @@ -1,160 +0,0 @@ -from __future__ import annotations - -import copy -import importlib.util -import json -import tempfile -import unittest -from pathlib import Path - - -REPOSITORY_ROOT = Path(__file__).resolve().parents[2] -VALIDATOR_PATH = REPOSITORY_ROOT / "eng" / "Validate-EngineeringManifest.py" -SPECIFICATION = importlib.util.spec_from_file_location("engineering_manifest_validator", VALIDATOR_PATH) -if SPECIFICATION is None or SPECIFICATION.loader is None: - raise RuntimeError(f"Unable to load validator from {VALIDATOR_PATH}.") -VALIDATOR = importlib.util.module_from_spec(SPECIFICATION) -SPECIFICATION.loader.exec_module(VALIDATOR) - - -def load_manifest() -> dict: - return json.loads((REPOSITORY_ROOT / "docs" / "engineering" / "backlog.json").read_text(encoding="utf-8")) - - -class EngineeringManifestValidatorTests(unittest.TestCase): - def test_checked_in_manifest_and_documents_are_valid(self) -> None: - summary = VALIDATOR.validate_repository(REPOSITORY_ROOT) - - self.assertEqual( - summary, - { - "items": 33, - "milestones": 7, - "external_dependencies": 25, - "archive_findings": 36, - "specified_product_scenarios": 80, - }, - ) - - def test_external_dependency_must_belong_to_its_declared_client_item(self) -> None: - manifest = load_manifest() - cli_007 = next(item for item in manifest["items"] if item["id"] == "CLI-007") - cli_007["blocked_by"].remove("SDK-008") - cli_007["blocked_by"].append("SDK-007") - - with self.assertRaisesRegex(ValueError, "undeclared external blocker SDK-007"): - VALIDATOR.validate_manifest_data(manifest) - - def test_item_path_must_name_the_versioned_work_item_document(self) -> None: - manifest = load_manifest() - cli_001 = next(item for item in manifest["items"] if item["id"] == "CLI-001") - cli_001["path"] = "issues/client/CLI-001.md" - - with self.assertRaisesRegex(ValueError, "path must be docs/engineering/work-items/CLI-001.md"): - VALIDATOR.validate_manifest_data(manifest) - - def test_cycle_is_rejected(self) -> None: - dependencies = {"CLI-001": ["CLI-002"], "CLI-002": ["CLI-001"]} - - with self.assertRaisesRegex(ValueError, "Cyclic local blocked_by dependency"): - VALIDATOR.ensure_acyclic(dependencies) - - def test_parent_cycle_is_rejected_independently_of_blocked_by(self) -> None: - manifest = copy.deepcopy(load_manifest()) - first_epic = next(item for item in manifest["items"] if item["id"] == "CLI-E01") - second_epic = next(item for item in manifest["items"] if item["id"] == "CLI-E02") - first_epic["parent"] = second_epic["id"] - second_epic["parent"] = first_epic["id"] - first_epic["children"].append(second_epic["id"]) - second_epic["children"].append(first_epic["id"]) - - with self.assertRaisesRegex(ValueError, "Cyclic parent hierarchy: CLI-E01 -> CLI-E02 -> CLI-E01"): - VALIDATOR.validate_manifest_data(manifest) - - def test_parent_hierarchy_requires_one_roadmap_root(self) -> None: - manifest = copy.deepcopy(load_manifest()) - epic = next(item for item in manifest["items"] if item["id"] == "CLI-E01") - epic["kind"] = "roadmap" - epic["labels"].append("ce:kind:roadmap") - - with self.assertRaisesRegex(ValueError, "exactly one roadmap root; found 2 roadmap items"): - VALIDATOR.validate_manifest_data(manifest) - - def test_parent_hierarchy_rejects_a_disconnected_forest(self) -> None: - hierarchy = { - "CLI-PLAN": {"children": []}, - "CLI-E01": {"children": []}, - } - - with self.assertRaisesRegex(ValueError, "Hierarchy is disconnected from roadmap root CLI-PLAN: CLI-E01"): - VALIDATOR.ensure_connected_parent_hierarchy(hierarchy, "CLI-PLAN") - - def test_roadmap_children_must_be_epics(self) -> None: - manifest = copy.deepcopy(load_manifest()) - roadmap = next(item for item in manifest["items"] if item["id"] == "CLI-PLAN") - epic = next(item for item in manifest["items"] if item["id"] == "CLI-E01") - leaf = next(item for item in manifest["items"] if item["id"] == "CLI-001") - roadmap["children"].append(leaf["id"]) - epic["children"].remove(leaf["id"]) - leaf["parent"] = roadmap["id"] - - with self.assertRaisesRegex(ValueError, "CLI-PLAN: roadmap child CLI-001 must be an epic"): - VALIDATOR.validate_manifest_data(manifest) - - def test_epics_must_be_direct_children_of_the_roadmap_root(self) -> None: - manifest = copy.deepcopy(load_manifest()) - roadmap = next(item for item in manifest["items"] if item["id"] == "CLI-PLAN") - first_epic = next(item for item in manifest["items"] if item["id"] == "CLI-E01") - second_epic = next(item for item in manifest["items"] if item["id"] == "CLI-E02") - roadmap["children"].remove(first_epic["id"]) - second_epic["children"].append(first_epic["id"]) - first_epic["parent"] = second_epic["id"] - - with self.assertRaisesRegex(ValueError, "CLI-E01: epic parent must be roadmap root CLI-PLAN"): - VALIDATOR.validate_manifest_data(manifest) - - def test_implementation_leaves_cannot_have_children(self) -> None: - manifest = copy.deepcopy(load_manifest()) - epic = next(item for item in manifest["items"] if item["id"] == "CLI-E01") - leaf = next(item for item in manifest["items"] if item["id"] == "CLI-001") - nested_leaf = next(item for item in manifest["items"] if item["id"] == "CLI-002") - epic["children"].remove(nested_leaf["id"]) - leaf["children"].append(nested_leaf["id"]) - nested_leaf["parent"] = leaf["id"] - - with self.assertRaisesRegex(ValueError, "CLI-001: implementation leaves cannot have children"): - VALIDATOR.validate_manifest_data(manifest) - - def test_reverse_dependency_is_required(self) -> None: - manifest = copy.deepcopy(load_manifest()) - cli_001 = next(item for item in manifest["items"] if item["id"] == "CLI-001") - cli_001["blocks"] = [] - - with self.assertRaisesRegex(ValueError, "lacks the reverse blocks edge"): - VALIDATOR.validate_manifest_data(manifest) - - def test_archive_reconciliation_requires_every_finding_exactly_once(self) -> None: - reconciliation_path = REPOSITORY_ROOT / "docs" / "engineering" / "archive-reconciliation.json" - reconciliation = json.loads(reconciliation_path.read_text(encoding="utf-8")) - reconciliation["finding_groups"][0]["findings"].remove("R17") - temporary_root = self._write_reconciliation(reconciliation) - item_ids = {item["id"] for item in load_manifest()["items"]} - - with self.assertRaisesRegex(ValueError, "cover R01 through R36 exactly once"): - VALIDATOR.validate_archive_reconciliation(temporary_root, item_ids) - - def _write_reconciliation(self, reconciliation: dict) -> Path: - temporary_root = self._temporary_directory() - reconciliation_directory = temporary_root / "docs" / "engineering" - reconciliation_directory.mkdir(parents=True) - reconciliation_path = reconciliation_directory / "archive-reconciliation.json" - reconciliation_path.write_text(json.dumps(reconciliation), encoding="utf-8") - return temporary_root - - def _temporary_directory(self) -> Path: - temporary_directory = self.enterContext(tempfile.TemporaryDirectory()) - return Path(temporary_directory) - - -if __name__ == "__main__": - unittest.main() diff --git a/libs/CheatEngine.Client.Abstractions/Memory/IMemoryBatchClient.cs b/libs/CheatEngine.Client.Abstractions/Memory/IMemoryBatchClient.cs new file mode 100644 index 0000000..01cfedc --- /dev/null +++ b/libs/CheatEngine.Client.Abstractions/Memory/IMemoryBatchClient.cs @@ -0,0 +1,18 @@ +namespace CheatEngine.Client.Memory; + +/// Executes primitive batches while preserving their per-operation outcome details. +/// +/// This companion contract is intentionally separate from so existing client +/// implementations remain source-compatible. Batch writes are sequential and never imply a transaction or a +/// rollback. +/// +public interface IMemoryBatchClient +{ + /// Executes a primitive read batch and returns its completed immutable value prefix and failure details. + public MemoryPrimitiveBatchReadOutcome ReadPrimitiveBatchDetailed(MemoryPrimitiveBatchReadRequest request, + CancellationToken cancellationToken = default); + + /// Executes a primitive write batch and returns its completed count and observable effect state. + public MemoryPrimitiveBatchWriteOutcome WritePrimitiveBatchDetailed(MemoryPrimitiveBatchWriteRequest request, + CancellationToken cancellationToken = default); +} diff --git a/libs/CheatEngine.Client.Abstractions/Memory/MemoryBatchWriteEffectState.cs b/libs/CheatEngine.Client.Abstractions/Memory/MemoryBatchWriteEffectState.cs new file mode 100644 index 0000000..edd3d3a --- /dev/null +++ b/libs/CheatEngine.Client.Abstractions/Memory/MemoryBatchWriteEffectState.cs @@ -0,0 +1,17 @@ +namespace CheatEngine.Client.Memory; + +/// Describes the observable target-memory effect of a sequential primitive batch write. +public enum MemoryBatchWriteEffectState +{ + /// No write is known to have reached the target. + NotStarted = 0, + + /// A strict prefix completed before a later write failed. + Partial = 1, + + /// Every requested write completed. + Complete = 2, + + /// The dispatcher could not establish whether the target observed any write. + Unknown = 3 +} diff --git a/libs/CheatEngine.Client.Abstractions/Memory/MemoryPrimitiveBatchReadOutcome.cs b/libs/CheatEngine.Client.Abstractions/Memory/MemoryPrimitiveBatchReadOutcome.cs new file mode 100644 index 0000000..2421d81 --- /dev/null +++ b/libs/CheatEngine.Client.Abstractions/Memory/MemoryPrimitiveBatchReadOutcome.cs @@ -0,0 +1,86 @@ +using System.Collections.Immutable; + +using CheatEngine.Client.Results; + +namespace CheatEngine.Client.Memory; + +/// Describes the sequential outcome of one primitive batch read. +/// The homogeneous primitive value type. +public sealed class MemoryPrimitiveBatchReadOutcome +{ + /// Creates a read outcome and copies the completed value prefix. + public MemoryPrimitiveBatchReadOutcome(int attemptedCount, int completedCount, int? failedIndex, + CheatEngineFailure? cause, ReadOnlySpan readPrefix) + { + ValidateCounts(attemptedCount, completedCount, failedIndex, cause); + if (readPrefix.Length != completedCount) + { + throw new ArgumentException("The read prefix length must equal the completed operation count.", + nameof(readPrefix)); + } + + AttemptedCount = attemptedCount; + CompletedCount = completedCount; + FailedIndex = failedIndex; + Cause = cause; + ReadPrefix = ImmutableArray.Create(readPrefix.ToArray()); + } + + /// Gets the number of operations requested by the batch. + public int AttemptedCount + { + get; + } + + /// Gets the number of reads known to have completed in order. + public int CompletedCount + { + get; + } + + /// Gets the individual read index that failed, or when admission or dispatch failed. + public int? FailedIndex + { + get; + } + + /// Gets the expected admission, dispatch, or target-memory failure when the batch did not complete. + public CheatEngineFailure? Cause + { + get; + } + + /// Gets an immutable copy of the successfully read values before . + public ImmutableArray ReadPrefix + { + get; + } + + /// Gets whether every requested read completed successfully. + public bool Succeeded => Cause is null && CompletedCount == AttemptedCount; + + private static void ValidateCounts(int attemptedCount, int completedCount, int? failedIndex, + CheatEngineFailure? cause) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(attemptedCount); + if (completedCount < 0 || completedCount > attemptedCount) + { + throw new ArgumentOutOfRangeException(nameof(completedCount)); + } + + if (failedIndex is { } index && (index < 0 || index >= attemptedCount || index != completedCount)) + { + throw new ArgumentOutOfRangeException(nameof(failedIndex)); + } + + if (cause is null && (failedIndex is not null || completedCount != attemptedCount)) + { + throw new ArgumentException("An incomplete read outcome requires a failure cause.", nameof(cause)); + } + + if (cause is not null && completedCount == attemptedCount) + { + throw new ArgumentException("A completed read outcome cannot contain a failure cause.", nameof(cause)); + } + } +} diff --git a/libs/CheatEngine.Client.Abstractions/Memory/MemoryPrimitiveBatchWriteOutcome.cs b/libs/CheatEngine.Client.Abstractions/Memory/MemoryPrimitiveBatchWriteOutcome.cs new file mode 100644 index 0000000..20a3997 --- /dev/null +++ b/libs/CheatEngine.Client.Abstractions/Memory/MemoryPrimitiveBatchWriteOutcome.cs @@ -0,0 +1,96 @@ +using CheatEngine.Client.Results; + +namespace CheatEngine.Client.Memory; + +/// Describes the sequential outcome and effect state of one primitive batch write. +public sealed class MemoryPrimitiveBatchWriteOutcome +{ + /// Creates a write outcome with a known or explicitly unknown target effect state. + public MemoryPrimitiveBatchWriteOutcome(int attemptedCount, int completedCount, int? failedIndex, + CheatEngineFailure? cause, MemoryBatchWriteEffectState effectState) + { + Validate(attemptedCount, completedCount, failedIndex, cause, effectState); + AttemptedCount = attemptedCount; + CompletedCount = completedCount; + FailedIndex = failedIndex; + Cause = cause; + EffectState = effectState; + } + + /// Gets the number of operations requested by the batch. + public int AttemptedCount + { + get; + } + + /// Gets the number of writes known to have completed in order. + public int CompletedCount + { + get; + } + + /// Gets the individual write index that failed, or when it is not known. + public int? FailedIndex + { + get; + } + + /// Gets the expected admission, dispatch, or target-memory failure when the batch did not complete. + public CheatEngineFailure? Cause + { + get; + } + + /// Gets the known, partial, complete, or unknown target-memory effect state. + public MemoryBatchWriteEffectState EffectState + { + get; + } + + /// Gets whether every requested write completed successfully. + public bool Succeeded => Cause is null && EffectState == MemoryBatchWriteEffectState.Complete; + + private static void Validate(int attemptedCount, int completedCount, int? failedIndex, + CheatEngineFailure? cause, MemoryBatchWriteEffectState effectState) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(attemptedCount); + if (completedCount < 0 || completedCount > attemptedCount) + { + throw new ArgumentOutOfRangeException(nameof(completedCount)); + } + + if (!Enum.IsDefined(effectState)) + { + throw new ArgumentOutOfRangeException(nameof(effectState)); + } + + if (failedIndex is { } index && (index < 0 || index >= attemptedCount || index != completedCount)) + { + throw new ArgumentOutOfRangeException(nameof(failedIndex)); + } + + if (cause is null && (completedCount != attemptedCount || effectState != MemoryBatchWriteEffectState.Complete)) + { + throw new ArgumentException("An incomplete write outcome requires a failure cause.", nameof(cause)); + } + + if (cause is not null && + (effectState == MemoryBatchWriteEffectState.Complete || completedCount == attemptedCount)) + { + throw new ArgumentException("A completed write outcome cannot contain a failure cause.", nameof(cause)); + } + + if (effectState == MemoryBatchWriteEffectState.NotStarted && completedCount != 0) + { + throw new ArgumentException("A not-started write outcome cannot contain completed writes.", + nameof(effectState)); + } + + if (effectState == MemoryBatchWriteEffectState.Partial && + (completedCount == 0 || completedCount == attemptedCount)) + { + throw new ArgumentException("A partial write outcome requires a strict completed prefix.", + nameof(effectState)); + } + } +} diff --git a/libs/CheatEngine.Client.Abstractions/Memory/MemoryResourceLimits.cs b/libs/CheatEngine.Client.Abstractions/Memory/MemoryResourceLimits.cs new file mode 100644 index 0000000..8803052 --- /dev/null +++ b/libs/CheatEngine.Client.Abstractions/Memory/MemoryResourceLimits.cs @@ -0,0 +1,104 @@ +namespace CheatEngine.Client.Memory; + +/// Defines the memory-work budgets captured for one Client activation. +/// +/// Configuration can populate this mutable value before activation. The Core client copies and validates it when it +/// is created, so a later configuration mutation cannot change an active client's admission policy. +/// +public sealed class MemoryResourceLimits +{ + /// Gets the default maximum number of bytes copied by one read. + public const int DefaultMaximumReadBytes = 1_048_576; + + /// Gets the default maximum number of bytes copied by one write. + public const int DefaultMaximumWriteBytes = 1_048_576; + + /// Gets the default maximum encoded byte length of one string operation. + public const int DefaultMaximumStringBytes = 65_536; + + /// Gets the default maximum payload bytes represented by one primitive batch. + public const int DefaultMaximumBatchPayloadBytes = 65_536; + + /// Gets the default maximum number of operations represented by one primitive batch. + public const int DefaultMaximumBatchOperationCount = MemoryBatchLimits.MaximumOperations; + + /// Initializes the default activation memory budgets. + public MemoryResourceLimits() + { + } + + /// Initializes explicitly bounded activation memory budgets. + public MemoryResourceLimits(int maximumReadBytes, int maximumWriteBytes, int maximumStringBytes, + int maximumBatchPayloadBytes, int maximumBatchOperationCount) + { + Validate(maximumReadBytes, nameof(maximumReadBytes)); + Validate(maximumWriteBytes, nameof(maximumWriteBytes)); + Validate(maximumStringBytes, nameof(maximumStringBytes)); + Validate(maximumBatchPayloadBytes, nameof(maximumBatchPayloadBytes)); + ValidateBatchOperationCount(maximumBatchOperationCount, nameof(maximumBatchOperationCount)); + + MaximumReadBytes = maximumReadBytes; + MaximumWriteBytes = maximumWriteBytes; + MaximumStringBytes = maximumStringBytes; + MaximumBatchPayloadBytes = maximumBatchPayloadBytes; + MaximumBatchOperationCount = maximumBatchOperationCount; + } + + /// Gets or sets the maximum bytes that one target-memory read may materialize. + public int MaximumReadBytes + { + get; + set; + } = DefaultMaximumReadBytes; + + /// Gets or sets the maximum bytes that one target-memory write may copy. + public int MaximumWriteBytes + { + get; + set; + } = DefaultMaximumWriteBytes; + + /// Gets or sets the maximum encoded bytes admitted for one target-string operation. + public int MaximumStringBytes + { + get; + set; + } = DefaultMaximumStringBytes; + + /// Gets or sets the maximum scalar payload bytes admitted for one primitive batch. + public int MaximumBatchPayloadBytes + { + get; + set; + } = DefaultMaximumBatchPayloadBytes; + + /// Gets or sets the maximum primitive operations admitted for one batch. + /// The value can tighten, but never raise, . + public int MaximumBatchOperationCount + { + get; + set; + } = DefaultMaximumBatchOperationCount; + + /// Creates an independently validated copy for an activation-bound client. + public MemoryResourceLimits CreateSnapshot() + { + return new MemoryResourceLimits(MaximumReadBytes, MaximumWriteBytes, MaximumStringBytes, + MaximumBatchPayloadBytes, MaximumBatchOperationCount); + } + + private static void Validate(int value, string parameterName) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value, parameterName); + } + + private static void ValidateBatchOperationCount(int value, string parameterName) + { + Validate(value, parameterName); + if (value > MemoryBatchLimits.MaximumOperations) + { + throw new ArgumentOutOfRangeException(parameterName, + $"A memory batch is limited to {MemoryBatchLimits.MaximumOperations} operations."); + } + } +} diff --git a/libs/CheatEngine.Client.Abstractions/Processes/LocalProcessId.cs b/libs/CheatEngine.Client.Abstractions/Processes/LocalProcessId.cs index 49d895c..b223378 100644 --- a/libs/CheatEngine.Client.Abstractions/Processes/LocalProcessId.cs +++ b/libs/CheatEngine.Client.Abstractions/Processes/LocalProcessId.cs @@ -12,5 +12,8 @@ public LocalProcessId(int value) } /// Gets the locally observed numeric process identifier. - public int Value { get; } + public int Value + { + get; + } } diff --git a/libs/CheatEngine.Client.Abstractions/Processes/ProcessSnapshot.cs b/libs/CheatEngine.Client.Abstractions/Processes/ProcessSnapshot.cs index b3be05b..39e287d 100644 --- a/libs/CheatEngine.Client.Abstractions/Processes/ProcessSnapshot.cs +++ b/libs/CheatEngine.Client.Abstractions/Processes/ProcessSnapshot.cs @@ -4,7 +4,10 @@ namespace CheatEngine.Client.Processes; /// An immutable snapshot of the process currently selected in Cheat Engine. -/// The identifier and architecture are Cheat Engine observations. Name and executable path are optional local BCL enrichment and do not establish liveness or authoritative target provenance. +/// +/// The identifier and architecture are Cheat Engine observations. Name and executable path are optional local BCL +/// enrichment and do not establish liveness or authoritative target provenance. +/// public readonly record struct ProcessSnapshot { /// Creates a selected-process snapshot without a target-architecture observation. diff --git a/libs/CheatEngine.Client.Abstractions/PublicAPI.Unshipped.txt b/libs/CheatEngine.Client.Abstractions/PublicAPI.Unshipped.txt index 2ada242..577c11e 100644 --- a/libs/CheatEngine.Client.Abstractions/PublicAPI.Unshipped.txt +++ b/libs/CheatEngine.Client.Abstractions/PublicAPI.Unshipped.txt @@ -1,4 +1,47 @@ #nullable enable +CheatEngine.Client.Memory.IMemoryBatchClient +CheatEngine.Client.Memory.IMemoryBatchClient.ReadPrimitiveBatchDetailed(CheatEngine.Client.Memory.MemoryPrimitiveBatchReadRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Memory.MemoryPrimitiveBatchReadOutcome! +CheatEngine.Client.Memory.IMemoryBatchClient.WritePrimitiveBatchDetailed(CheatEngine.Client.Memory.MemoryPrimitiveBatchWriteRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Memory.MemoryPrimitiveBatchWriteOutcome! +CheatEngine.Client.Memory.MemoryBatchWriteEffectState +CheatEngine.Client.Memory.MemoryBatchWriteEffectState.Complete = 2 -> CheatEngine.Client.Memory.MemoryBatchWriteEffectState +CheatEngine.Client.Memory.MemoryBatchWriteEffectState.NotStarted = 0 -> CheatEngine.Client.Memory.MemoryBatchWriteEffectState +CheatEngine.Client.Memory.MemoryBatchWriteEffectState.Partial = 1 -> CheatEngine.Client.Memory.MemoryBatchWriteEffectState +CheatEngine.Client.Memory.MemoryBatchWriteEffectState.Unknown = 3 -> CheatEngine.Client.Memory.MemoryBatchWriteEffectState +CheatEngine.Client.Memory.MemoryPrimitiveBatchReadOutcome +CheatEngine.Client.Memory.MemoryPrimitiveBatchReadOutcome.AttemptedCount.get -> int +CheatEngine.Client.Memory.MemoryPrimitiveBatchReadOutcome.Cause.get -> CheatEngine.Client.Results.CheatEngineFailure? +CheatEngine.Client.Memory.MemoryPrimitiveBatchReadOutcome.CompletedCount.get -> int +CheatEngine.Client.Memory.MemoryPrimitiveBatchReadOutcome.FailedIndex.get -> int? +CheatEngine.Client.Memory.MemoryPrimitiveBatchReadOutcome.MemoryPrimitiveBatchReadOutcome(int attemptedCount, int completedCount, int? failedIndex, CheatEngine.Client.Results.CheatEngineFailure? cause, System.ReadOnlySpan readPrefix) -> void +CheatEngine.Client.Memory.MemoryPrimitiveBatchReadOutcome.ReadPrefix.get -> System.Collections.Immutable.ImmutableArray +CheatEngine.Client.Memory.MemoryPrimitiveBatchReadOutcome.Succeeded.get -> bool +CheatEngine.Client.Memory.MemoryPrimitiveBatchWriteOutcome +CheatEngine.Client.Memory.MemoryPrimitiveBatchWriteOutcome.AttemptedCount.get -> int +CheatEngine.Client.Memory.MemoryPrimitiveBatchWriteOutcome.Cause.get -> CheatEngine.Client.Results.CheatEngineFailure? +CheatEngine.Client.Memory.MemoryPrimitiveBatchWriteOutcome.CompletedCount.get -> int +CheatEngine.Client.Memory.MemoryPrimitiveBatchWriteOutcome.EffectState.get -> CheatEngine.Client.Memory.MemoryBatchWriteEffectState +CheatEngine.Client.Memory.MemoryPrimitiveBatchWriteOutcome.FailedIndex.get -> int? +CheatEngine.Client.Memory.MemoryPrimitiveBatchWriteOutcome.MemoryPrimitiveBatchWriteOutcome(int attemptedCount, int completedCount, int? failedIndex, CheatEngine.Client.Results.CheatEngineFailure? cause, CheatEngine.Client.Memory.MemoryBatchWriteEffectState effectState) -> void +CheatEngine.Client.Memory.MemoryPrimitiveBatchWriteOutcome.Succeeded.get -> bool +CheatEngine.Client.Memory.MemoryResourceLimits +const CheatEngine.Client.Memory.MemoryResourceLimits.DefaultMaximumBatchOperationCount = 1024 -> int +const CheatEngine.Client.Memory.MemoryResourceLimits.DefaultMaximumBatchPayloadBytes = 65536 -> int +const CheatEngine.Client.Memory.MemoryResourceLimits.DefaultMaximumReadBytes = 1048576 -> int +const CheatEngine.Client.Memory.MemoryResourceLimits.DefaultMaximumStringBytes = 65536 -> int +const CheatEngine.Client.Memory.MemoryResourceLimits.DefaultMaximumWriteBytes = 1048576 -> int +CheatEngine.Client.Memory.MemoryResourceLimits.CreateSnapshot() -> CheatEngine.Client.Memory.MemoryResourceLimits! +CheatEngine.Client.Memory.MemoryResourceLimits.MaximumBatchOperationCount.get -> int +CheatEngine.Client.Memory.MemoryResourceLimits.MaximumBatchOperationCount.set -> void +CheatEngine.Client.Memory.MemoryResourceLimits.MaximumBatchPayloadBytes.get -> int +CheatEngine.Client.Memory.MemoryResourceLimits.MaximumBatchPayloadBytes.set -> void +CheatEngine.Client.Memory.MemoryResourceLimits.MaximumReadBytes.get -> int +CheatEngine.Client.Memory.MemoryResourceLimits.MaximumReadBytes.set -> void +CheatEngine.Client.Memory.MemoryResourceLimits.MaximumStringBytes.get -> int +CheatEngine.Client.Memory.MemoryResourceLimits.MaximumStringBytes.set -> void +CheatEngine.Client.Memory.MemoryResourceLimits.MaximumWriteBytes.get -> int +CheatEngine.Client.Memory.MemoryResourceLimits.MaximumWriteBytes.set -> void +CheatEngine.Client.Memory.MemoryResourceLimits.MemoryResourceLimits() -> void +CheatEngine.Client.Memory.MemoryResourceLimits.MemoryResourceLimits(int maximumReadBytes, int maximumWriteBytes, int maximumStringBytes, int maximumBatchPayloadBytes, int maximumBatchOperationCount) -> void CheatEngine.Client.Processes.ILocalProcessDiagnostics CheatEngine.Client.Processes.ILocalProcessDiagnostics.GetProcesses(CheatEngine.Client.Processes.ProcessEnumerationRequest request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> CheatEngine.Client.Processes.ProcessEnumerationResult CheatEngine.Client.Processes.ILocalProcessDiagnostics.TryGetProcesses(CheatEngine.Client.Processes.ProcessEnumerationRequest request, out CheatEngine.Client.Processes.ProcessEnumerationResult result, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool diff --git a/libs/CheatEngine.Client.Abstractions/README.md b/libs/CheatEngine.Client.Abstractions/README.md index 64def6b..3e5ae28 100644 --- a/libs/CheatEngine.Client.Abstractions/README.md +++ b/libs/CheatEngine.Client.Abstractions/README.md @@ -80,7 +80,8 @@ missing, faulted, and malformed host observations remain distinguishable instead 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 +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 diff --git a/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidence.cs b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidence.cs index a27f181..661aa72 100644 --- a/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidence.cs +++ b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidence.cs @@ -89,7 +89,7 @@ public ClientCapabilityAvailabilityState AvailabilityState public string EffectiveReason => GetGate(EffectiveReasonCode).Reason; /// - /// Gets the stable code for the evidence gate that supplies . Its state remains + /// Gets the stable code for the evidence gate that supplies . Its state remains /// available through the corresponding evidence-gate property. /// public ClientCapabilityEvidenceReasonCode EffectiveReasonCode => GetEffectiveReasonCode(); diff --git a/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidenceReasonCode.cs b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidenceReasonCode.cs index fa35b75..892c98e 100644 --- a/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidenceReasonCode.cs +++ b/libs/CheatEngine.Client.Abstractions/Runtime/ClientCapabilityEvidenceReasonCode.cs @@ -1,6 +1,6 @@ namespace CheatEngine.Client.Runtime; -/// Identifies the evidence gate that supplies . +/// Identifies the evidence gate that supplies . public enum ClientCapabilityEvidenceReasonCode : byte { /// The operational Client adapter gate supplies the effective reason. diff --git a/libs/CheatEngine.Client.Abstractions/Scanning/AobScanRange.cs b/libs/CheatEngine.Client.Abstractions/Scanning/AobScanRange.cs index 6690fed..4eb843a 100644 --- a/libs/CheatEngine.Client.Abstractions/Scanning/AobScanRange.cs +++ b/libs/CheatEngine.Client.Abstractions/Scanning/AobScanRange.cs @@ -4,9 +4,9 @@ namespace CheatEngine.Client.Scanning; /// An inclusive target-address range used to filter copied AOB match addresses. /// -/// String-form AOBScan does not accept start and stop address arguments. Core therefore applies this filter -/// after copying each matching address from the SDK-owned result list and before it contributes to the caller's -/// materialization limit. +/// String-form AOBScan does not accept start and stop address arguments. The global scan is therefore not +/// narrowed by this range: Core applies it while copying each matching address from the SDK-owned result list and +/// before it contributes to the caller's materialization limit. /// public readonly record struct AobScanRange { diff --git a/libs/CheatEngine.Client.Abstractions/Scanning/AobScanRequest.cs b/libs/CheatEngine.Client.Abstractions/Scanning/AobScanRequest.cs index fe0cce5..35ec6eb 100644 --- a/libs/CheatEngine.Client.Abstractions/Scanning/AobScanRequest.cs +++ b/libs/CheatEngine.Client.Abstractions/Scanning/AobScanRequest.cs @@ -3,7 +3,7 @@ namespace CheatEngine.Client.Scanning; -/// An immutable AOB scan request with an explicit managed materialization limit. +/// An immutable AOB scan request with an explicit managed, post-filter materialization limit. public readonly record struct AobScanRequest { /// Creates an AOB scan request. @@ -40,19 +40,20 @@ public AobScanOptions Options get; } - /// Gets the maximum number of managed matches the caller permits. + /// Gets the maximum number of copied addresses that may survive managed post-filters. + /// This bounds result materialization only; it does not bound or terminate the global Cheat Engine scan. public int MaximumResults { get; } - /// Gets the optional module whose copied address range filters the results. + /// Gets the optional module Core resolves before the global scan and applies as a copied-address post-filter. public ModuleName? Module { get; } - /// Gets the optional inclusive range that filters copied address results. + /// Gets the optional inclusive copied-address post-filter. public AobScanRange? Range { get; diff --git a/libs/CheatEngine.Client.Abstractions/Scanning/IPatternScanner.cs b/libs/CheatEngine.Client.Abstractions/Scanning/IPatternScanner.cs index 23da161..d628f4f 100644 --- a/libs/CheatEngine.Client.Abstractions/Scanning/IPatternScanner.cs +++ b/libs/CheatEngine.Client.Abstractions/Scanning/IPatternScanner.cs @@ -2,13 +2,13 @@ namespace CheatEngine.Client.Scanning; -/// Runs bounded AOB scans and copies all returned addresses before releasing SDK-owned objects. +/// Runs global AOB scans, then copies post-filtered addresses before releasing SDK-owned objects. public interface IPatternScanner { - /// Tries to run one bounded pattern scan. + /// Tries to run one scan with managed post-filtered result materialization. public bool TryScan(AobScanRequest request, out AobScanResult result, out CheatEngineFailure failure, CancellationToken cancellationToken = default); - /// Runs one bounded pattern scan or throws when the operation fails. + /// Runs one scan with managed post-filtered result materialization or throws when it fails. public AobScanResult Scan(AobScanRequest request, CancellationToken cancellationToken = default); } diff --git a/libs/CheatEngine.Client.Core/Domains/Allocations/UnavailableAllocationClient.cs b/libs/CheatEngine.Client.Core/Domains/Allocations/UnavailableAllocationClient.cs index 550d277..4b2eaee 100644 --- a/libs/CheatEngine.Client.Core/Domains/Allocations/UnavailableAllocationClient.cs +++ b/libs/CheatEngine.Client.Core/Domains/Allocations/UnavailableAllocationClient.cs @@ -2,8 +2,8 @@ using CheatEngine.Client.Allocations; using CheatEngine.Client.Core.Domains.Events; -using CheatEngine.Client.Results; using CheatEngine.Client.Core.Infrastructure; +using CheatEngine.Client.Results; namespace CheatEngine.Client.Core.Domains.Allocations; @@ -11,13 +11,19 @@ namespace CheatEngine.Client.Core.Domains.Allocations; internal sealed class UnavailableAllocationClient : IAllocationClient { private readonly CoreLifetime? _lifetime; - internal UnavailableAllocationClient(CoreLifetime? lifetime = null) => _lifetime = lifetime; + + internal UnavailableAllocationClient(CoreLifetime? lifetime = null) + { + _lifetime = lifetime; + } + public bool TryAllocate(TargetAllocationRequest request, [NotNullWhen(true)] out ITargetMemoryLease? lease, out CheatEngineFailure failure, CancellationToken cancellationToken = default) { lease = null; - failure = UnavailableCapabilityFailure.Create(_lifetime, "Target allocations", "Allocations.Allocate", cancellationToken); + failure = UnavailableCapabilityFailure.Create(_lifetime, "Target allocations", "Allocations.Allocate", + cancellationToken); return false; } diff --git a/libs/CheatEngine.Client.Core/Domains/Assembly/UnavailableAssemblyClient.cs b/libs/CheatEngine.Client.Core/Domains/Assembly/UnavailableAssemblyClient.cs index 3fdcbef..5c18600 100644 --- a/libs/CheatEngine.Client.Core/Domains/Assembly/UnavailableAssemblyClient.cs +++ b/libs/CheatEngine.Client.Core/Domains/Assembly/UnavailableAssemblyClient.cs @@ -3,8 +3,8 @@ using CheatEngine.Client.Assembly; using CheatEngine.Client.Core.Domains.Events; -using CheatEngine.Client.Results; using CheatEngine.Client.Core.Infrastructure; +using CheatEngine.Client.Results; using CheatEngine.SDK.Engine.Values; namespace CheatEngine.Client.Core.Domains.Assembly; @@ -13,7 +13,12 @@ namespace CheatEngine.Client.Core.Domains.Assembly; internal sealed class UnavailableAssemblyClient : IAssemblyClient { private readonly CoreLifetime? _lifetime; - internal UnavailableAssemblyClient(CoreLifetime? lifetime = null) => _lifetime = lifetime; + + internal UnavailableAssemblyClient(CoreLifetime? lifetime = null) + { + _lifetime = lifetime; + } + public bool TryDisassemble(Address address, out AssemblyInstructionSnapshot instruction, out CheatEngineFailure failure, CancellationToken cancellationToken = default) @@ -104,7 +109,8 @@ public IAutoAssemblerPatchLease ApplyPatch(AutoAssemblerScript script, private CheatEngineFailure CreateFailure(string operation, CancellationToken cancellationToken) { - return UnavailableCapabilityFailure.Create(_lifetime, "Assembly, disassembly, and Auto Assembler patches", operation, + return UnavailableCapabilityFailure.Create(_lifetime, "Assembly, disassembly, and Auto Assembler patches", + operation, cancellationToken); } } diff --git a/libs/CheatEngine.Client.Core/Domains/Dbvm/UnavailableDbvmClient.cs b/libs/CheatEngine.Client.Core/Domains/Dbvm/UnavailableDbvmClient.cs index 1937ac8..72bdf78 100644 --- a/libs/CheatEngine.Client.Core/Domains/Dbvm/UnavailableDbvmClient.cs +++ b/libs/CheatEngine.Client.Core/Domains/Dbvm/UnavailableDbvmClient.cs @@ -17,6 +17,7 @@ internal UnavailableDbvmClient(CoreLifetime? lifetime = null) { _lifetime = lifetime; } + public bool TryGetStatus(out DbvmStatusSnapshot status, out CheatEngineFailure failure, CancellationToken cancellationToken = default) { @@ -66,7 +67,8 @@ public IDbvmWatchLease RegisterWatch(DbvmWatchRequest request, DbvmWatchHandler private CheatEngineFailure CreateFailure(string operation, CancellationToken cancellationToken) { - return UnavailableCapabilityFailure.Create(_lifetime, "DBVM observation, explicit initialization, and watches", operation, + return UnavailableCapabilityFailure.Create(_lifetime, "DBVM observation, explicit initialization, and watches", + operation, cancellationToken); } } diff --git a/libs/CheatEngine.Client.Core/Domains/Debugger/UnavailableDebuggerClient.cs b/libs/CheatEngine.Client.Core/Domains/Debugger/UnavailableDebuggerClient.cs index b19743c..d7313f3 100644 --- a/libs/CheatEngine.Client.Core/Domains/Debugger/UnavailableDebuggerClient.cs +++ b/libs/CheatEngine.Client.Core/Domains/Debugger/UnavailableDebuggerClient.cs @@ -17,6 +17,7 @@ internal UnavailableDebuggerClient(CoreLifetime? lifetime = null) { _lifetime = lifetime; } + public bool TryRegisterBreakpoint(BreakpointRequest request, BreakpointHandler handler, EventStreamOptions streamOptions, [NotNullWhen(true)] out IBreakpointLease? lease, out CheatEngineFailure failure, diff --git a/libs/CheatEngine.Client.Core/Domains/Events/BoundedEventStream.cs b/libs/CheatEngine.Client.Core/Domains/Events/BoundedEventStream.cs index 97c9218..21fb5c2 100644 --- a/libs/CheatEngine.Client.Core/Domains/Events/BoundedEventStream.cs +++ b/libs/CheatEngine.Client.Core/Domains/Events/BoundedEventStream.cs @@ -375,7 +375,10 @@ internal ReadResult(T value) internal static ReadResult End => new(false, default); } - private sealed class PendingRead(BoundedEventStream owner, Enumerator reader, CancellationToken cancellationToken) + private sealed class PendingRead( + BoundedEventStream owner, + Enumerator reader, + CancellationToken cancellationToken) { private readonly CancellationToken _cancellationToken = cancellationToken; private readonly BoundedEventStream _owner = owner; diff --git a/libs/CheatEngine.Client.Core/Domains/Events/EventStreamLease.cs b/libs/CheatEngine.Client.Core/Domains/Events/EventStreamLease.cs index 17c937b..17e4c03 100644 --- a/libs/CheatEngine.Client.Core/Domains/Events/EventStreamLease.cs +++ b/libs/CheatEngine.Client.Core/Domains/Events/EventStreamLease.cs @@ -51,19 +51,20 @@ public void Dispose() } Volatile.Write(ref _disposing, 1); - Exception? firstFailure = null; - firstFailure = RunCleanupStep(_stream.CloseAdmission, firstFailure); - firstFailure = RunCleanupStep(_neutralizeCallback, firstFailure); - firstFailure = RunCleanupStep(_stream.Complete, firstFailure); - firstFailure = RunCleanupStep(_releaseHostRegistration, firstFailure); - firstFailure = RunCleanupStep(Untrack, firstFailure); - Volatile.Write(ref _released, 1); - Volatile.Write(ref _disposing, 0); - - if (firstFailure is not null) - { - ExceptionDispatchInfo.Capture(firstFailure).Throw(); - } + } + + Exception? firstFailure = null; + firstFailure = RunCleanupStep(_stream.CloseAdmission, firstFailure); + firstFailure = RunCleanupStep(_neutralizeCallback, firstFailure); + firstFailure = RunCleanupStep(_stream.Complete, firstFailure); + firstFailure = RunCleanupStep(_releaseHostRegistration, firstFailure); + firstFailure = RunCleanupStep(Untrack, firstFailure); + Volatile.Write(ref _released, 1); + Volatile.Write(ref _disposing, 0); + + if (firstFailure is not null) + { + ExceptionDispatchInfo.Capture(firstFailure).Throw(); } } diff --git a/libs/CheatEngine.Client.Core/Domains/Events/UnavailableCapabilityFailure.cs b/libs/CheatEngine.Client.Core/Domains/Events/UnavailableCapabilityFailure.cs index a78c553..37c55f7 100644 --- a/libs/CheatEngine.Client.Core/Domains/Events/UnavailableCapabilityFailure.cs +++ b/libs/CheatEngine.Client.Core/Domains/Events/UnavailableCapabilityFailure.cs @@ -1,7 +1,7 @@ using System.Diagnostics; -using CheatEngine.Client.Results; using CheatEngine.Client.Core.Infrastructure; +using CheatEngine.Client.Results; namespace CheatEngine.Client.Core.Domains.Events; @@ -10,7 +10,9 @@ internal static class UnavailableCapabilityFailure { internal static CheatEngineFailure Create(string capabilityName, string operation, CancellationToken cancellationToken) - => Create(null, capabilityName, operation, cancellationToken); + { + return Create(null, capabilityName, operation, cancellationToken); + } internal static CheatEngineFailure Create(CoreLifetime? lifetime, string capabilityName, string operation, CancellationToken cancellationToken) diff --git a/libs/CheatEngine.Client.Core/Domains/Hashing/UnavailableHashingClient.cs b/libs/CheatEngine.Client.Core/Domains/Hashing/UnavailableHashingClient.cs index 85e66f6..596a1c0 100644 --- a/libs/CheatEngine.Client.Core/Domains/Hashing/UnavailableHashingClient.cs +++ b/libs/CheatEngine.Client.Core/Domains/Hashing/UnavailableHashingClient.cs @@ -14,6 +14,7 @@ internal UnavailableHashingClient(CoreLifetime? lifetime = null) { _lifetime = lifetime; } + public bool TryHashMemory(MemoryHashRequest request, out HashDigest digest, out CheatEngineFailure failure, CancellationToken cancellationToken = default) { @@ -44,6 +45,7 @@ public HashDigest HashFile(FileHashRequest request, CancellationToken cancellati private CheatEngineFailure CreateFailure(string operation, CancellationToken cancellationToken) { - return UnavailableCapabilityFailure.Create(_lifetime, "Target-memory and file hashing", operation, cancellationToken); + return UnavailableCapabilityFailure.Create(_lifetime, "Target-memory and file hashing", operation, + cancellationToken); } } diff --git a/libs/CheatEngine.Client.Core/Domains/Hotkeys/UnavailableHotkeyClient.cs b/libs/CheatEngine.Client.Core/Domains/Hotkeys/UnavailableHotkeyClient.cs index a3c8589..910fa8a 100644 --- a/libs/CheatEngine.Client.Core/Domains/Hotkeys/UnavailableHotkeyClient.cs +++ b/libs/CheatEngine.Client.Core/Domains/Hotkeys/UnavailableHotkeyClient.cs @@ -17,6 +17,7 @@ internal UnavailableHotkeyClient(CoreLifetime? lifetime = null) { _lifetime = lifetime; } + public bool TryRegister(HotkeyRegistration registration, HotkeyHandler handler, EventStreamOptions streamOptions, [NotNullWhen(true)] out IHotkeyLease? lease, out CheatEngineFailure failure, CancellationToken cancellationToken = default) diff --git a/libs/CheatEngine.Client.Core/Domains/IMemoryCodecContextPort.cs b/libs/CheatEngine.Client.Core/Domains/IMemoryCodecContextPort.cs index 6c0fc49..af16c63 100644 --- a/libs/CheatEngine.Client.Core/Domains/IMemoryCodecContextPort.cs +++ b/libs/CheatEngine.Client.Core/Domains/IMemoryCodecContextPort.cs @@ -1,3 +1,5 @@ +using System.Runtime.CompilerServices; + using CheatEngine.Client.Core.Infrastructure; using CheatEngine.SDK.Engine.Memory; using CheatEngine.SDK.Engine.Values; @@ -16,20 +18,190 @@ internal interface IMemoryCodecContextPort public bool TryReadBytes(Address address, Span destination, out string? failure); public bool TryWriteBytes(Address address, ReadOnlySpan source, out string? failure); + + /// Tries one built-in primitive read after the Core client has admitted the operation. + public bool TryReadPrimitive(Address address, out T value, out string? failure) + { + return SdkMemoryPrimitivePort.TryRead(address, out value, out failure); + } + + /// Tries one built-in primitive write after the Core client has admitted the operation. + public bool TryWritePrimitive(Address address, T value, out string? failure) + { + return SdkMemoryPrimitivePort.TryWrite(address, value, out failure); + } +} + +/// Maps the Core's supported primitive set to the SDK while retaining an injectable port boundary. +internal static class SdkMemoryPrimitivePort +{ + internal static bool TryRead(Address address, out T value, out string? failure) + { + if (typeof(T) == typeof(byte)) + { + return TryRead(TargetMemory.TryReadUInt8, address, out value, out failure); + } + + if (typeof(T) == typeof(sbyte)) + { + return TryRead(TargetMemory.TryReadInt8, address, out value, out failure); + } + + if (typeof(T) == typeof(ushort)) + { + return TryRead(TargetMemory.TryReadUInt16, address, out value, out failure); + } + + if (typeof(T) == typeof(short)) + { + return TryRead(TargetMemory.TryReadInt16, address, out value, out failure); + } + + if (typeof(T) == typeof(uint)) + { + return TryRead(TargetMemory.TryReadUInt32, address, out value, out failure); + } + + if (typeof(T) == typeof(int)) + { + return TryRead(TargetMemory.TryReadInt32, address, out value, out failure); + } + + if (typeof(T) == typeof(ulong)) + { + return TryRead(TargetMemory.TryReadUInt64, address, out value, out failure); + } + + if (typeof(T) == typeof(long)) + { + return TryRead(TargetMemory.TryReadInt64, address, out value, out failure); + } + + if (typeof(T) == typeof(float)) + { + return TryRead(TargetMemory.TryReadSingle, address, out value, out failure); + } + + if (typeof(T) == typeof(double)) + { + return TryRead(TargetMemory.TryReadDouble, address, out value, out failure); + } + + if (typeof(T) == typeof(Address)) + { + return TryRead(TargetMemory.TryReadPointer, address, out value, out failure); + } + + value = default!; + failure = $"'{typeof(T).FullName}' is not a built-in CheatEngine.Client memory type."; + return false; + } + + internal static bool TryWrite(Address address, T value, out string? failure) + { + if (typeof(T) == typeof(byte)) + { + return TryWrite(TargetMemory.TryWriteUInt8, address, value, out failure); + } + + if (typeof(T) == typeof(sbyte)) + { + return TryWrite(TargetMemory.TryWriteInt8, address, value, out failure); + } + + if (typeof(T) == typeof(ushort)) + { + return TryWrite(TargetMemory.TryWriteUInt16, address, value, out failure); + } + + if (typeof(T) == typeof(short)) + { + return TryWrite(TargetMemory.TryWriteInt16, address, value, out failure); + } + + if (typeof(T) == typeof(uint)) + { + return TryWrite(TargetMemory.TryWriteUInt32, address, value, out failure); + } + + if (typeof(T) == typeof(int)) + { + return TryWrite(TargetMemory.TryWriteInt32, address, value, out failure); + } + + if (typeof(T) == typeof(ulong)) + { + return TryWrite(TargetMemory.TryWriteUInt64, address, value, out failure); + } + + if (typeof(T) == typeof(long)) + { + return TryWrite(TargetMemory.TryWriteInt64, address, value, out failure); + } + + if (typeof(T) == typeof(float)) + { + return TryWrite(TargetMemory.TryWriteSingle, address, value, out failure); + } + + if (typeof(T) == typeof(double)) + { + return TryWrite(TargetMemory.TryWriteDouble, address, value, out failure); + } + + if (typeof(T) == typeof(Address)) + { + return TryWrite(TargetMemory.TryWritePointer, address, value, out failure); + } + + failure = $"'{typeof(T).FullName}' is not a built-in CheatEngine.Client memory type."; + return false; + } + + private static bool TryRead(Reader reader, Address address, out T value, out string? failure) + { + if (reader(address, out TValue readValue, out MemoryAccessFailure sdkFailure)) + { + value = Unsafe.As(ref readValue); + failure = null; + return true; + } + + value = default!; + failure = sdkFailure.ToString(); + return false; + } + + private static bool TryWrite(Writer writer, Address address, T value, out string? failure) + { + TValue writeValue = Unsafe.As(ref value); + if (writer(address, writeValue, out MemoryAccessFailure sdkFailure)) + { + failure = null; + return true; + } + + failure = sdkFailure.ToString(); + return false; + } + + private delegate bool Reader(Address address, out T value, out MemoryAccessFailure failure); + + private delegate bool Writer(Address address, T value, out MemoryAccessFailure failure); } /// Calls the SDK memory primitives after the owning context has admitted the operation. internal sealed class SdkMemoryCodecContextPort : IMemoryCodecContextPort { + private SdkMemoryCodecContextPort() + { + } + internal static SdkMemoryCodecContextPort Instance { get; } = new(); - private SdkMemoryCodecContextPort() - { - } - public bool IsTarget64Bit() { return ClientLuaGlobals.TargetIs64Bit(); diff --git a/libs/CheatEngine.Client.Core/Domains/ITableRecordLookupPort.cs b/libs/CheatEngine.Client.Core/Domains/ITableRecordLookupPort.cs new file mode 100644 index 0000000..1431814 --- /dev/null +++ b/libs/CheatEngine.Client.Core/Domains/ITableRecordLookupPort.cs @@ -0,0 +1,17 @@ +using CheatEngine.Client.Tables; +using CheatEngine.SDK.Engine.AddressList; + +namespace CheatEngine.Client.Core.Domains; + +/// +/// Separates static SDK Address List access from lookup classification so the Client's public failure contract +/// remains deterministic and directly testable. +/// +internal interface ITableRecordLookupPort +{ + public RecordLookupStatus TryGetRecord(int index, out MemoryRecordSnapshot record); + + public RecordLookupStatus TryGetRecord(MemoryRecordId id, out MemoryRecordSnapshot record); + + public RecordLookupStatus TryGetSelected(out MemoryRecordSnapshot record); +} diff --git a/libs/CheatEngine.Client.Core/Domains/LocalProcessDiagnostics.cs b/libs/CheatEngine.Client.Core/Domains/LocalProcessDiagnostics.cs index 72219fb..846cff5 100644 --- a/libs/CheatEngine.Client.Core/Domains/LocalProcessDiagnostics.cs +++ b/libs/CheatEngine.Client.Core/Domains/LocalProcessDiagnostics.cs @@ -66,7 +66,7 @@ public bool TryGetProcesses( return true; } catch (Exception exception) when (exception is ArgumentException or InvalidOperationException or Win32Exception - or PlatformNotSupportedException) + or PlatformNotSupportedException) { result = default; failure = new CheatEngineFailure( diff --git a/libs/CheatEngine.Client.Core/Domains/LuaClient.cs b/libs/CheatEngine.Client.Core/Domains/LuaClient.cs index 36c4428..76630b2 100644 --- a/libs/CheatEngine.Client.Core/Domains/LuaClient.cs +++ b/libs/CheatEngine.Client.Core/Domains/LuaClient.cs @@ -11,11 +11,12 @@ namespace CheatEngine.Client.Core.Domains; internal sealed class LuaClient : ILuaClient { - private readonly ICheatEngineDispatcher _dispatcher; private readonly Action? _admitStatefulOperation; + private readonly ICheatEngineDispatcher _dispatcher; private readonly Func _epochProvider; private readonly Func _isContextCurrent; + private readonly Func _isStopping; // This is deliberately a single lock-protected reservation. A generated module has two identities that must move // together: its managed module instance and the immutable Lua name set published in its descriptor. Reserving the @@ -42,7 +43,8 @@ private LuaClient(ICheatEngineDispatcher dispatcher, LuaClientInitialization ini initialization.IsContextCurrent, initialization.TrackLease, initialization.UntrackLease, - initialization.AdmitStatefulOperation) + initialization.AdmitStatefulOperation, + initialization.IsStopping) { } @@ -53,11 +55,13 @@ internal LuaClient( Func isContextCurrent, Action? trackLease = null, Action? untrackLease = null, - Action? admitStatefulOperation = null) + Action? admitStatefulOperation = null, + Func? isStopping = null) { _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); _epochProvider = epochProvider ?? throw new ArgumentNullException(nameof(epochProvider)); _isContextCurrent = isContextCurrent ?? throw new ArgumentNullException(nameof(isContextCurrent)); + _isStopping = isStopping ?? (static () => false); _trackLease = trackLease ?? (static _ => { }); @@ -92,6 +96,20 @@ public bool TryRegisterModule(ILuaModule luaModule, [NotNullWhen(true)] out ILua return false; } + LuaModuleLease created = new(luaModule, _epochProvider(), _dispatcher, _isContextCurrent, _untrackLease, + ReleaseModule); + try + { + _trackLease(created); + } + catch (Exception trackingException) + { + failure = FailAfterAbandoningUnregisteredLease( + CoreFailureFactory.FromException("Lua.RegisterModule", trackingException), + created); + return false; + } + bool registered = false; CheatEngineFailure moduleFailure = default; if (!_dispatcher.TryInvoke( @@ -100,6 +118,7 @@ public bool TryRegisterModule(ILuaModule luaModule, [NotNullWhen(true)] out ILua try { luaModule.Register(); + created.ConfirmRegistration(); registered = true; } catch (Exception exception) @@ -110,40 +129,41 @@ public bool TryRegisterModule(ILuaModule luaModule, [NotNullWhen(true)] out ILua out failure, cancellationToken)) { - ReleaseModule(luaModule); + failure = FailAfterAbandoningUnregisteredLease(failure, created); return false; } if (!registered) { - ReleaseModule(luaModule); - failure = moduleFailure; + failure = FailAfterAbandoningUnregisteredLease(moduleFailure, created); return false; } - LuaModuleLease created = new(luaModule, _epochProvider(), _dispatcher, _isContextCurrent, _untrackLease, - ReleaseModule); try { - _trackLease(created); - lease = created; - failure = default; - return true; + // Registration and lifetime tracking are deliberately handed off in this order. If shutdown starts while + // Register runs, the already-tracked lease is drained by the hosting cleanup scope rather than redispatched + // from this worker after ordinary dispatch admission has closed. + Admit("Lua.RegisterModule"); } catch (Exception exception) { - try - { - created.Dispose(); - } - catch + CheatEngineFailure admissionFailure = CoreFailureFactory.FromException("Lua.RegisterModule", exception); + if (_isStopping()) { - // The track failure remains the meaningful result; the module was still removed from this Client scope. + // The Core lifetime owns the pre-tracked, registered lease. Leaving it there gives the main-thread + // cleanup scope one admitted unregistration attempt and avoids publishing a lease to a stopped caller. + failure = admissionFailure; + return false; } - failure = CoreFailureFactory.FromException("Lua.RegisterModule", exception); + failure = FailAfterRegisteredLease(admissionFailure, created); return false; } + + lease = created; + failure = default; + return true; } public ILuaModuleLease RegisterModule(ILuaModule luaModule, CancellationToken cancellationToken = default) @@ -417,11 +437,60 @@ private static LuaClientInitialization CreateProductionInitialization(CoreLifeti return new LuaClientInitialization( () => lifetime.Epoch, () => lifetime.IsActivationCurrent, + () => lifetime.Stopping.IsCancellationRequested, lease => lifetime.Track(lease), lease => lifetime.Untrack(lease), lifetime.ThrowIfInactive); } + private void Admit(string operation) + { + _admitStatefulOperation?.Invoke(operation); + } + + private static CheatEngineFailure FailAfterAbandoningUnregisteredLease( + CheatEngineFailure primaryFailure, + LuaModuleLease lease) + { + try + { + lease.AbandonRegistration(); + return primaryFailure; + } + catch (Exception cleanupException) + { + return WithSecondaryFailure(primaryFailure, cleanupException); + } + } + + private static CheatEngineFailure FailAfterRegisteredLease(CheatEngineFailure primaryFailure, LuaModuleLease lease) + { + try + { + lease.Dispose(); + return primaryFailure; + } + catch (Exception cleanupException) + { + return WithSecondaryFailure(primaryFailure, cleanupException); + } + } + + private static CheatEngineFailure WithSecondaryFailure(CheatEngineFailure primaryFailure, + Exception secondaryFailure) + { + Exception primaryException = primaryFailure.Exception ?? new CheatEngineOperationException(primaryFailure); + AggregateException combined = new( + "Lua module registration failed and its handoff cleanup encountered an additional failure.", + primaryException, + secondaryFailure); + return new CheatEngineFailure( + primaryFailure.Kind, + primaryFailure.Operation, + $"{primaryFailure.Message} The registration handoff cleanup also failed: {secondaryFailure.Message}", + combined); + } + private readonly record struct LuaOperationResult( bool Succeeded, TResult Result, @@ -430,12 +499,11 @@ private readonly record struct LuaOperationResult( private readonly record struct LuaClientInitialization( Func EpochProvider, Func IsContextCurrent, + Func IsStopping, Action TrackLease, Action UntrackLease, Action AdmitStatefulOperation); - private void Admit(string operation) => _admitStatefulOperation?.Invoke(operation); - private readonly struct LuaOperationDispatchState { private readonly LuaClient _client; diff --git a/libs/CheatEngine.Client.Core/Domains/LuaModuleLease.cs b/libs/CheatEngine.Client.Core/Domains/LuaModuleLease.cs index 12d02f1..18145f7 100644 --- a/libs/CheatEngine.Client.Core/Domains/LuaModuleLease.cs +++ b/libs/CheatEngine.Client.Core/Domains/LuaModuleLease.cs @@ -1,3 +1,5 @@ +using System.Runtime.ExceptionServices; + using CheatEngine.Client.Dispatching; using CheatEngine.Client.Lua; @@ -26,6 +28,7 @@ internal sealed class LuaModuleLease( releaseModule ?? throw new ArgumentNullException(nameof(releaseModule)); private readonly Action _untrack = untrack ?? throw new ArgumentNullException(nameof(untrack)); + private int _registered; private int _released; public long Epoch @@ -44,6 +47,12 @@ public void Dispose() return; } + if (Volatile.Read(ref _registered) == 0) + { + CompleteRelease(); + return; + } + // A detached activation has no legal SDK dispatch path left. There is nothing more that this lease can // safely do, so release managed ownership without attempting to call Cheat Engine. if (!_isActivationCurrent()) @@ -61,10 +70,72 @@ public void Dispose() } } + /// Marks the module as registered while the registration dispatcher callback still owns the main thread. + internal void ConfirmRegistration() + { + lock (_disposeLock) + { + if (Volatile.Read(ref _released) != 0) + { + throw new InvalidOperationException("The Lua module lease was released before registration completed."); + } + + Volatile.Write(ref _registered, 1); + } + } + + /// Releases a tracked handoff that never completed . + internal void AbandonRegistration() + { + lock (_disposeLock) + { + if (Volatile.Read(ref _released) != 0) + { + return; + } + + if (Volatile.Read(ref _registered) != 0) + { + throw new InvalidOperationException( + "A registered Lua module lease cannot be abandoned without unregistration."); + } + + CompleteRelease(); + } + } + private void CompleteRelease() { Volatile.Write(ref _released, 1); - _untrack(this); - _releaseModule(_module); + List? failures = null; + try + { + _untrack(this); + } + catch (Exception exception) + { + (failures ??= []).Add(exception); + } + + try + { + _releaseModule(_module); + } + catch (Exception exception) + { + (failures ??= []).Add(exception); + } + + if (failures is null) + { + return; + } + + if (failures.Count == 1) + { + ExceptionDispatchInfo.Capture(failures[0]).Throw(); + } + + throw new AggregateException("Lua module lease release encountered one or more cleanup failures.", failures); } } diff --git a/libs/CheatEngine.Client.Core/Domains/MemoryClient.cs b/libs/CheatEngine.Client.Core/Domains/MemoryClient.cs index 98e0768..baf5c4b 100644 --- a/libs/CheatEngine.Client.Core/Domains/MemoryClient.cs +++ b/libs/CheatEngine.Client.Core/Domains/MemoryClient.cs @@ -14,16 +14,21 @@ namespace CheatEngine.Client.Core.Domains; -internal sealed class MemoryClient : IMemoryClient +internal sealed class MemoryClient : IMemoryClient, IMemoryBatchClient { + private readonly IMemoryCodecContextPort _codecContextPort; private readonly ICheatEngineDispatcher _dispatcher; private readonly CoreLifetime _lifetime; - - private readonly IMemoryCodecContextPort _codecContextPort; + private readonly MemoryResourceLimits _limits; internal MemoryClient(ICheatEngineDispatcher dispatcher, CoreLifetime lifetime) - : this(dispatcher, lifetime, SdkMemoryCodecContextPort.Instance) + : this(dispatcher, lifetime, SdkMemoryCodecContextPort.Instance, new MemoryResourceLimits()) + { + } + + internal MemoryClient(ICheatEngineDispatcher dispatcher, CoreLifetime lifetime, MemoryResourceLimits limits) + : this(dispatcher, lifetime, SdkMemoryCodecContextPort.Instance, limits) { } @@ -31,10 +36,83 @@ internal MemoryClient( ICheatEngineDispatcher dispatcher, CoreLifetime lifetime, IMemoryCodecContextPort codecContextPort) + : this(dispatcher, lifetime, codecContextPort, new MemoryResourceLimits()) + { + } + + internal MemoryClient( + ICheatEngineDispatcher dispatcher, + CoreLifetime lifetime, + IMemoryCodecContextPort codecContextPort, + MemoryResourceLimits limits) { _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); _lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime)); _codecContextPort = codecContextPort ?? throw new ArgumentNullException(nameof(codecContextPort)); + _limits = (limits ?? throw new ArgumentNullException(nameof(limits))).CreateSnapshot(); + } + + public MemoryPrimitiveBatchReadOutcome ReadPrimitiveBatchDetailed(MemoryPrimitiveBatchReadRequest request, + CancellationToken cancellationToken = default) + { + ValidateBatch(request.Addresses, nameof(request)); + int attemptedCount = request.Addresses.Length; + if (!TryAdmitBatch(attemptedCount, false, "Memory.ReadPrimitiveBatch", + out CheatEngineFailure admissionFailure)) + { + return new MemoryPrimitiveBatchReadOutcome(attemptedCount, 0, null, admissionFailure, []); + } + + PrimitiveBatchReadInput input = new(request, _codecContextPort); + if (!TryInvoke(input, static current => PrimitiveMemoryCodec.ReadBatch(current.Request, current.Port), + out PrimitiveBatchReadOutcome outcome, out CheatEngineFailure dispatchFailure, cancellationToken)) + { + return new MemoryPrimitiveBatchReadOutcome(attemptedCount, 0, null, dispatchFailure, []); + } + + if (outcome.Succeeded) + { + return new MemoryPrimitiveBatchReadOutcome(attemptedCount, attemptedCount, null, null, outcome.Values); + } + + CheatEngineFailure failure = + CreateBatchFailure(outcome.Handled, false, outcome.FailedIndex, outcome.Failure); + return new MemoryPrimitiveBatchReadOutcome(attemptedCount, outcome.FailedIndex, outcome.FailedIndex, + failure, outcome.Values); + } + + public MemoryPrimitiveBatchWriteOutcome WritePrimitiveBatchDetailed(MemoryPrimitiveBatchWriteRequest request, + CancellationToken cancellationToken = default) + { + ValidateBatch(request.Values, nameof(request)); + int attemptedCount = request.Values.Length; + if (!TryAdmitBatch(attemptedCount, true, "Memory.WritePrimitiveBatch", + out CheatEngineFailure admissionFailure)) + { + return new MemoryPrimitiveBatchWriteOutcome(attemptedCount, 0, null, admissionFailure, + MemoryBatchWriteEffectState.NotStarted); + } + + PrimitiveBatchWriteInput input = new(request, _codecContextPort); + if (!TryInvoke(input, static current => PrimitiveMemoryCodec.WriteBatch(current.Request, current.Port), + out PrimitiveBatchWriteOutcome outcome, out CheatEngineFailure dispatchFailure, cancellationToken)) + { + return new MemoryPrimitiveBatchWriteOutcome(attemptedCount, 0, null, dispatchFailure, + MemoryBatchWriteEffectState.Unknown); + } + + if (outcome.Succeeded) + { + return new MemoryPrimitiveBatchWriteOutcome(attemptedCount, attemptedCount, null, null, + MemoryBatchWriteEffectState.Complete); + } + + CheatEngineFailure failure = CreateBatchFailure(outcome.Handled, true, outcome.FailedIndex, outcome.Failure); + MemoryBatchWriteEffectState effectState = outcome.FailedIndex == 0 + ? MemoryBatchWriteEffectState.NotStarted + : MemoryBatchWriteEffectState.Partial; + return new MemoryPrimitiveBatchWriteOutcome(attemptedCount, outcome.FailedIndex, outcome.FailedIndex, failure, + effectState); } public bool TryReadPrimitive(Address address, [MaybeNullWhen(false)] out T value, @@ -109,28 +187,17 @@ public void WritePrimitive(Address address, T value, CancellationToken cancel public bool TryReadPrimitiveBatch(MemoryPrimitiveBatchReadRequest request, out ImmutableArray values, out CheatEngineFailure failure, CancellationToken cancellationToken = default) { - ValidateBatch(request.Addresses, nameof(request)); - if (!TryInvoke(request, static current => PrimitiveMemoryCodec.ReadBatch(current), - out PrimitiveBatchReadOutcome outcome, out failure, cancellationToken)) - { - values = []; - return false; - } - - if (!outcome.Succeeded) + MemoryPrimitiveBatchReadOutcome outcome = ReadPrimitiveBatchDetailed(request, cancellationToken); + if (outcome.Succeeded) { - values = []; - failure = !outcome.Handled - ? new CheatEngineFailure(CheatEngineFailureKind.Unsupported, "Memory.ReadPrimitiveBatch", - $"'{typeof(T).FullName}' is not a built-in CheatEngine.Client memory type.") - : new CheatEngineFailure(CheatEngineFailureKind.MemoryReadFailed, "Memory.ReadPrimitiveBatch", - $"The batch read failed at index {outcome.FailedIndex}: {outcome.Failure}"); - return false; + values = outcome.ReadPrefix; + failure = default; + return true; } - values = ImmutableCollectionsMarshal.AsImmutableArray(outcome.Values!); - failure = default; - return true; + values = []; + failure = outcome.Cause!.Value; + return false; } public ImmutableArray ReadPrimitiveBatch(MemoryPrimitiveBatchReadRequest request, @@ -149,25 +216,15 @@ public ImmutableArray ReadPrimitiveBatch(MemoryPrimitiveBatchReadRequest(MemoryPrimitiveBatchWriteRequest request, out CheatEngineFailure failure, CancellationToken cancellationToken = default) { - ValidateBatch(request.Values, nameof(request)); - if (!TryInvoke(request, static current => PrimitiveMemoryCodec.WriteBatch(current), - out PrimitiveBatchWriteOutcome outcome, out failure, cancellationToken)) + MemoryPrimitiveBatchWriteOutcome outcome = WritePrimitiveBatchDetailed(request, cancellationToken); + if (outcome.Succeeded) { - return false; - } - - if (!outcome.Succeeded) - { - failure = !outcome.Handled - ? new CheatEngineFailure(CheatEngineFailureKind.Unsupported, "Memory.WritePrimitiveBatch", - $"'{typeof(T).FullName}' is not a built-in CheatEngine.Client memory type.") - : new CheatEngineFailure(CheatEngineFailureKind.MemoryWriteFailed, "Memory.WritePrimitiveBatch", - $"The batch write failed at index {outcome.FailedIndex}: {outcome.Failure}"); - return false; + failure = default; + return true; } - failure = default; - return true; + failure = outcome.Cause!.Value; + return false; } public void WritePrimitiveBatch(MemoryPrimitiveBatchWriteRequest request, @@ -250,6 +307,13 @@ public bool TryReadBytes(MemoryBytesReadRequest request, out ImmutableArray captured = ImmutableArray.Empty; string? hostFailure = null; bool succeeded = false; @@ -295,6 +359,12 @@ public bool TryWriteBytes(MemoryBytesWriteRequest request, out CheatEngineFailur throw new ArgumentException("At least one byte is required.", nameof(request)); } + if (!TryAdmitPayload(request.Bytes.Length, _limits.MaximumWriteBytes, true, "Memory.WriteBytes", "byte write", + out failure)) + { + return false; + } + string? hostFailure = null; bool succeeded = false; if (!_dispatcher.TryInvoke(() => @@ -325,6 +395,13 @@ public bool TryReadString(MemoryStringReadRequest request, [NotNullWhen(true)] o out CheatEngineFailure failure, CancellationToken cancellationToken = default) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(request.MaximumLength); + if (!TryAdmitPayload(GetEncodedByteLength(request.MaximumLength, request.WideCharacter), + _limits.MaximumStringBytes, false, "Memory.ReadString", "string read", out failure)) + { + value = null; + return false; + } + string? captured = null; string? hostFailure = null; bool succeeded = false; @@ -361,11 +438,18 @@ public bool TryWriteString(MemoryStringWriteRequest request, out CheatEngineFail CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request.Value); - if (request.MaximumLength > 0 && GetEncodedLength(request.Value, request.WideCharacter) > request.MaximumLength) + int encodedLength = GetEncodedLength(request.Value, request.WideCharacter); + if (request.MaximumLength > 0 && encodedLength > request.MaximumLength) { throw new ArgumentException("The encoded text exceeds the explicit maximum length.", nameof(request)); } + if (!TryAdmitPayload(GetEncodedByteLength(request.Value, request.WideCharacter), _limits.MaximumStringBytes, + true, "Memory.WriteString", "string write", out failure)) + { + return false; + } + string? hostFailure = null; bool succeeded = false; if (!_dispatcher.TryInvoke(() => @@ -462,7 +546,8 @@ private bool TryReadCore(MemoryReadRequest request, [MaybeNullWhen(false)] out string? failure) { failure = null; - TargetMemoryCodecContext context = TargetMemoryCodecContext.Create(_lifetime, _dispatcher, _codecContextPort); + TargetMemoryCodecContext context = TargetMemoryCodecContext.Create(_lifetime, _dispatcher, _codecContextPort, + _limits); try { if (request.Codec.TryRead(context, request.Address, out value)) @@ -482,7 +567,8 @@ private bool TryReadCore(MemoryReadRequest request, [MaybeNullWhen(false)] private bool TryWriteCore(MemoryWriteRequest request, out string? failure) { failure = null; - TargetMemoryCodecContext context = TargetMemoryCodecContext.Create(_lifetime, _dispatcher, _codecContextPort); + TargetMemoryCodecContext context = TargetMemoryCodecContext.Create(_lifetime, _dispatcher, _codecContextPort, + _limits); try { T value = request.Value; @@ -514,11 +600,73 @@ private static void ValidateBatch(ImmutableArray values, string parameterN } } + private bool TryAdmitBatch(int attemptedCount, bool isWrite, string operation, out CheatEngineFailure failure) + { + if (attemptedCount > _limits.MaximumBatchOperationCount) + { + failure = CreateLimitFailure(isWrite, operation, "batch operation count", attemptedCount, + _limits.MaximumBatchOperationCount); + return false; + } + + return TryAdmitPayload(PrimitiveMemoryCodec.GetPayloadBytes(attemptedCount), + _limits.MaximumBatchPayloadBytes, isWrite, operation, "batch payload", out failure); + } + + private static bool TryAdmitPayload(long requestedBytes, int limit, bool isWrite, string operation, string resource, + out CheatEngineFailure failure) + { + if (requestedBytes > limit) + { + failure = CreateLimitFailure(isWrite, operation, resource, requestedBytes, limit); + return false; + } + + failure = default; + return true; + } + + private static CheatEngineFailure CreateLimitFailure(bool isWrite, string operation, string resource, + long requestedBytes, int limit) + { + string direction = isWrite ? "write" : "read"; + string unit = resource == "batch operation count" ? "operations" : "bytes"; + return new CheatEngineFailure(CheatEngineFailureKind.OperationRejected, operation, + $"The requested {resource} of {requestedBytes} {unit} exceeds the activation {direction} budget of {limit} {unit}."); + } + + private static CheatEngineFailure CreateBatchFailure(bool handled, bool isWrite, int failedIndex, + string? hostFailure) + { + string operation = isWrite ? "Memory.WritePrimitiveBatch" : "Memory.ReadPrimitiveBatch"; + if (!handled) + { + return new CheatEngineFailure(CheatEngineFailureKind.Unsupported, operation, + $"'{typeof(T).FullName}' is not a built-in CheatEngine.Client memory type."); + } + + CheatEngineFailureKind kind = + isWrite ? CheatEngineFailureKind.MemoryWriteFailed : CheatEngineFailureKind.MemoryReadFailed; + string action = isWrite ? "write" : "read"; + return new CheatEngineFailure(kind, operation, + $"The batch {action} failed at index {failedIndex}: {hostFailure ?? "Cheat Engine rejected the target-memory operation."}"); + } + private static int GetEncodedLength(string value, bool wideCharacter) { return wideCharacter ? value.Length : Encoding.UTF8.GetByteCount(value); } + private static long GetEncodedByteLength(int length, bool wideCharacter) + { + return wideCharacter ? (long) length * sizeof(char) : length; + } + + private static long GetEncodedByteLength(string value, bool wideCharacter) + { + return wideCharacter ? (long) value.Length * sizeof(char) : Encoding.UTF8.GetByteCount(value); + } + private static bool TryMapMemoryFailure(bool succeeded, bool isWrite, string operation, string? hostFailure, out CheatEngineFailure failure) { @@ -535,21 +683,24 @@ private static bool TryMapMemoryFailure(bool succeeded, bool isWrite, string ope return false; } - private static TTo Reinterpret(TFrom value) - { - return Unsafe.As(ref value); - } - private readonly record struct PrimitiveWriteInput(Address Address, T Value); private readonly record struct PrimitiveReadOutcome(bool Handled, bool Succeeded, T Value, string? Failure); private readonly record struct PrimitiveWriteOutcome(bool Handled, bool Succeeded, string? Failure); + private readonly record struct PrimitiveBatchReadInput( + MemoryPrimitiveBatchReadRequest Request, + IMemoryCodecContextPort Port); + + private readonly record struct PrimitiveBatchWriteInput( + MemoryPrimitiveBatchWriteRequest Request, + IMemoryCodecContextPort Port); + private readonly record struct PrimitiveBatchReadOutcome( bool Handled, bool Succeeded, - T[]? Values, + T[] Values, int FailedIndex, string? Failure); @@ -561,135 +712,74 @@ private readonly record struct PrimitiveBatchWriteOutcome( private static class PrimitiveMemoryCodec { + private static bool IsSupported => + typeof(T) == typeof(byte) || + typeof(T) == typeof(sbyte) || + typeof(T) == typeof(ushort) || + typeof(T) == typeof(short) || + typeof(T) == typeof(uint) || + typeof(T) == typeof(int) || + typeof(T) == typeof(ulong) || + typeof(T) == typeof(long) || + typeof(T) == typeof(float) || + typeof(T) == typeof(double) || + typeof(T) == typeof(Address); + internal static PrimitiveReadOutcome Read(Address address) { - if (typeof(T) == typeof(byte)) - { - return Read(TargetMemory.TryReadUInt8, address); - } - - if (typeof(T) == typeof(sbyte)) - { - return Read(TargetMemory.TryReadInt8, address); - } - - if (typeof(T) == typeof(ushort)) - { - return Read(TargetMemory.TryReadUInt16, address); - } - - if (typeof(T) == typeof(short)) - { - return Read(TargetMemory.TryReadInt16, address); - } - - if (typeof(T) == typeof(uint)) - { - return Read(TargetMemory.TryReadUInt32, address); - } - - if (typeof(T) == typeof(int)) - { - return Read(TargetMemory.TryReadInt32, address); - } - - if (typeof(T) == typeof(ulong)) - { - return Read(TargetMemory.TryReadUInt64, address); - } - - if (typeof(T) == typeof(long)) - { - return Read(TargetMemory.TryReadInt64, address); - } - - if (typeof(T) == typeof(float)) - { - return Read(TargetMemory.TryReadSingle, address); - } - - if (typeof(T) == typeof(double)) - { - return Read(TargetMemory.TryReadDouble, address); - } + return Read(SdkMemoryCodecContextPort.Instance, address); + } - if (typeof(T) == typeof(Address)) + internal static PrimitiveReadOutcome Read(IMemoryCodecContextPort port, Address address) + { + if (!IsSupported) { - return Read
(TargetMemory.TryReadPointer, address); + return new PrimitiveReadOutcome(false, false, default!, null); } - return new PrimitiveReadOutcome(false, false, default!, null); + return port.TryReadPrimitive(address, out T value, out string? failure) + ? new PrimitiveReadOutcome(true, true, value, null) + : new PrimitiveReadOutcome(true, false, default!, failure); } internal static PrimitiveWriteOutcome Write(Address address, T value) { - if (typeof(T) == typeof(byte)) - { - return Write(TargetMemory.TryWriteUInt8, address, value); - } - - if (typeof(T) == typeof(sbyte)) - { - return Write(TargetMemory.TryWriteInt8, address, value); - } - - if (typeof(T) == typeof(ushort)) - { - return Write(TargetMemory.TryWriteUInt16, address, value); - } - - if (typeof(T) == typeof(short)) - { - return Write(TargetMemory.TryWriteInt16, address, value); - } - - if (typeof(T) == typeof(uint)) - { - return Write(TargetMemory.TryWriteUInt32, address, value); - } - - if (typeof(T) == typeof(int)) - { - return Write(TargetMemory.TryWriteInt32, address, value); - } - - if (typeof(T) == typeof(ulong)) - { - return Write(TargetMemory.TryWriteUInt64, address, value); - } + return Write(SdkMemoryCodecContextPort.Instance, address, value); + } - if (typeof(T) == typeof(long)) + internal static PrimitiveWriteOutcome Write(IMemoryCodecContextPort port, Address address, T value) + { + if (!IsSupported) { - return Write(TargetMemory.TryWriteInt64, address, value); + return new PrimitiveWriteOutcome(false, false, null); } - if (typeof(T) == typeof(float)) - { - return Write(TargetMemory.TryWriteSingle, address, value); - } + return port.TryWritePrimitive(address, value, out string? failure) + ? new PrimitiveWriteOutcome(true, true, null) + : new PrimitiveWriteOutcome(true, false, failure); + } - if (typeof(T) == typeof(double)) - { - return Write(TargetMemory.TryWriteDouble, address, value); - } + internal static long GetPayloadBytes(int operationCount) + { + return (long) Unsafe.SizeOf() * operationCount; + } - if (typeof(T) == typeof(Address)) + internal static PrimitiveBatchReadOutcome ReadBatch(MemoryPrimitiveBatchReadRequest request, + IMemoryCodecContextPort port) + { + if (!IsSupported) { - return Write
(TargetMemory.TryWritePointer, address, value); + return new PrimitiveBatchReadOutcome(false, false, [], 0, null); } - return new PrimitiveWriteOutcome(false, false, null); - } - - internal static PrimitiveBatchReadOutcome ReadBatch(MemoryPrimitiveBatchReadRequest request) - { T[] values = new T[request.Addresses.Length]; for (int index = 0; index < request.Addresses.Length; index++) { - PrimitiveReadOutcome current = Read(request.Addresses[index]); + PrimitiveReadOutcome current = Read(port, request.Addresses[index]); if (!current.Succeeded) { - return new PrimitiveBatchReadOutcome(current.Handled, false, null, index, current.Failure); + return new PrimitiveBatchReadOutcome(current.Handled, false, values[..index], index, + current.Failure); } values[index] = current.Value; @@ -698,12 +788,18 @@ internal static PrimitiveBatchReadOutcome ReadBatch(MemoryPrimitiveBatchReadR return new PrimitiveBatchReadOutcome(true, true, values, -1, null); } - internal static PrimitiveBatchWriteOutcome WriteBatch(MemoryPrimitiveBatchWriteRequest request) + internal static PrimitiveBatchWriteOutcome WriteBatch(MemoryPrimitiveBatchWriteRequest request, + IMemoryCodecContextPort port) { + if (!IsSupported) + { + return new PrimitiveBatchWriteOutcome(false, false, 0, null); + } + for (int index = 0; index < request.Values.Length; index++) { MemoryAddressValue current = request.Values[index]; - PrimitiveWriteOutcome outcome = Write(current.Address, current.Value); + PrimitiveWriteOutcome outcome = Write(port, current.Address, current.Value); if (!outcome.Succeeded) { return new PrimitiveBatchWriteOutcome(outcome.Handled, false, index, outcome.Failure); @@ -712,30 +808,8 @@ internal static PrimitiveBatchWriteOutcome WriteBatch(MemoryPrimitiveBatchWriteR return new PrimitiveBatchWriteOutcome(true, true, -1, null); } - - private static PrimitiveReadOutcome Read(Reader reader, Address address) - { - if (reader(address, out TValue value, out MemoryAccessFailure failure)) - { - return new PrimitiveReadOutcome(true, true, Reinterpret(value), null); - } - - return new PrimitiveReadOutcome(true, false, default!, failure.ToString()); - } - - private static PrimitiveWriteOutcome Write(Writer writer, Address address, T value) - { - TValue targetValue = Reinterpret(value); - return writer(address, targetValue, out MemoryAccessFailure failure) - ? new PrimitiveWriteOutcome(true, true, null) - : new PrimitiveWriteOutcome(true, false, failure.ToString()); - } } - private delegate bool Reader(Address address, out T value, out MemoryAccessFailure failure); - - private delegate bool Writer(Address address, T value, out MemoryAccessFailure failure); - private sealed class TargetMemoryCodecContext : IMemoryReadContext, IMemoryWriteContext { private const string _operation = "Memory.CodecContext"; @@ -743,19 +817,24 @@ private sealed class TargetMemoryCodecContext : IMemoryReadContext, IMemoryWrite private readonly long _activationEpoch; private readonly ICheatEngineDispatcher _dispatcher; private readonly CoreLifetime _lifetime; + private readonly MemoryResourceLimits _limits; private readonly IMemoryCodecContextPort _port; private readonly int _threadId; private int _expired; private int _pointerSize; + private int _readBytesAdmitted; + private int _writeBytesAdmitted; private TargetMemoryCodecContext( CoreLifetime lifetime, ICheatEngineDispatcher dispatcher, - IMemoryCodecContextPort port) + IMemoryCodecContextPort port, + MemoryResourceLimits limits) { _lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime)); _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); _port = port ?? throw new ArgumentNullException(nameof(port)); + _limits = limits ?? throw new ArgumentNullException(nameof(limits)); _activationEpoch = lifetime.Epoch; _threadId = Environment.CurrentManagedThreadId; } @@ -785,6 +864,11 @@ public int PointerSize public bool TryReadBytes(Address address, Span destination) { ThrowIfUnusable(); + if (!TryAdmitCodecBytes(destination.Length, _limits.MaximumReadBytes, ref _readBytesAdmitted, "read")) + { + return false; + } + if (_port.TryReadBytes(address, destination, out string? failure)) { Failure = null; @@ -798,6 +882,11 @@ public bool TryReadBytes(Address address, Span destination) public bool TryWriteBytes(Address address, ReadOnlySpan source) { ThrowIfUnusable(); + if (!TryAdmitCodecBytes(source.Length, _limits.MaximumWriteBytes, ref _writeBytesAdmitted, "write")) + { + return false; + } + if (_port.TryWriteBytes(address, source, out string? failure)) { Failure = null; @@ -811,9 +900,10 @@ public bool TryWriteBytes(Address address, ReadOnlySpan source) internal static TargetMemoryCodecContext Create( CoreLifetime lifetime, ICheatEngineDispatcher dispatcher, - IMemoryCodecContextPort port) + IMemoryCodecContextPort port, + MemoryResourceLimits limits) { - return new TargetMemoryCodecContext(lifetime, dispatcher, port); + return new TargetMemoryCodecContext(lifetime, dispatcher, port, limits); } internal void Expire() @@ -836,5 +926,17 @@ private void ThrowIfUnusable() _lifetime.ThrowIfInactive(_operation); } + + private bool TryAdmitCodecBytes(int requestedBytes, int limit, ref int admittedBytes, string direction) + { + if (requestedBytes > limit - admittedBytes) + { + Failure = $"The memory codec {direction} exceeds the activation {direction} budget of {limit} bytes."; + return false; + } + + admittedBytes += requestedBytes; + return true; + } } } diff --git a/libs/CheatEngine.Client.Core/Domains/PatternScanner.cs b/libs/CheatEngine.Client.Core/Domains/PatternScanner.cs index 4649dae..25aa216 100644 --- a/libs/CheatEngine.Client.Core/Domains/PatternScanner.cs +++ b/libs/CheatEngine.Client.Core/Domains/PatternScanner.cs @@ -32,7 +32,8 @@ public bool TryScan(AobScanRequest request, out AobScanResult result, out CheatE AobScanResult captured = default; CheatEngineFailure hostFailure = default; bool succeeded = false; - if (!_dispatcher.TryInvoke(() => succeeded = TryScanCore(request, out captured, out hostFailure), + if (!_dispatcher.TryInvoke( + () => succeeded = TryScanCore(request, cancellationToken, out captured, out hostFailure), out failure, cancellationToken)) { result = default; @@ -62,11 +63,41 @@ public AobScanResult Scan(AobScanRequest request, CancellationToken cancellation return default; } - private bool TryScanCore(AobScanRequest request, out AobScanResult result, + private bool TryScanCore(AobScanRequest request, CancellationToken cancellationToken, out AobScanResult result, out CheatEngineFailure failure) { + if (TryGetCancellationFailure(cancellationToken, out failure)) + { + result = default; + return false; + } + + bool hasModuleRange = false; + ModuleRange moduleRange = default; + if (request.Module.HasValue && + !TryGetModuleRange(request.Module.Value, out hasModuleRange, out moduleRange, out failure)) + { + result = default; + return false; + } + + // Module resolution is deliberately completed before the unbounded CE AOB scan. The range still acts as a + // managed post-filter because the SDK AOB binding does not accept a module constraint. + if (TryGetCancellationFailure(cancellationToken, out failure)) + { + result = default; + return false; + } + AobScanHostStatus status = _scanPort.TryScan(request.Pattern.Value, request.Options, out IAobMatchList? matchList); + if (TryGetCancellationFailure(cancellationToken, out failure)) + { + matchList?.Dispose(); + result = default; + return false; + } + if (status == AobScanHostStatus.Rejected) { result = default; @@ -85,26 +116,22 @@ private bool TryScanCore(AobScanRequest request, out AobScanResult result, using (matchList) { - if (!matchList.TryGetCount(out int count) || count < 0) + if (TryGetCancellationFailure(cancellationToken, out failure)) { result = default; - failure = new CheatEngineFailure(CheatEngineFailureKind.InvalidHostResult, _scanOperation, - "Cheat Engine returned an invalid AOB result count."); return false; } - bool hasModuleRange = false; - ModuleRange moduleRange = default; - if (request.Module.HasValue && - !TryGetModuleRange(request.Module.Value, out hasModuleRange, out moduleRange, - out failure)) + if (!matchList.TryGetCount(out int count) || count < 0) { result = default; + failure = new CheatEngineFailure(CheatEngineFailureKind.InvalidHostResult, _scanOperation, + "Cheat Engine returned an invalid AOB result count."); return false; } - return TryMaterializeMatches(matchList, count, request, hasModuleRange, moduleRange, out result, - out failure); + return TryMaterializeMatches(matchList, count, request, hasModuleRange, moduleRange, cancellationToken, + out result, out failure); } } @@ -114,6 +141,7 @@ private static bool TryMaterializeMatches( AobScanRequest request, bool hasModuleRange, ModuleRange moduleRange, + CancellationToken cancellationToken, out AobScanResult result, out CheatEngineFailure failure) { @@ -122,12 +150,24 @@ private static bool TryMaterializeMatches( ImmutableArray
.Builder materialized = ImmutableArray.CreateBuilder
(); for (int index = 0; index < count; index++) { + if (TryGetCancellationFailure(cancellationToken, out failure)) + { + result = default; + return false; + } + if (!TryGetMatchAddress(matches, index, out Address address, out failure)) { result = default; return false; } + if (TryGetCancellationFailure(cancellationToken, out failure)) + { + result = default; + return false; + } + if (!IsIncluded(address, request, hasModuleRange, moduleRange)) { continue; @@ -143,6 +183,12 @@ private static bool TryMaterializeMatches( materialized.Add(address); } + if (TryGetCancellationFailure(cancellationToken, out failure)) + { + result = default; + return false; + } + result = new AobScanResult(materialized.ToImmutable(), false); failure = default; return true; @@ -163,6 +209,19 @@ private static bool TryGetMatchAddress(IAobMatchList matches, int index, out Add return false; } + private static bool TryGetCancellationFailure(CancellationToken cancellationToken, out CheatEngineFailure failure) + { + if (!cancellationToken.IsCancellationRequested) + { + failure = default; + return false; + } + + failure = new CheatEngineFailure(CheatEngineFailureKind.Cancelled, _scanOperation, + "The AOB scan was cancelled."); + return true; + } + private static bool IsIncluded(Address address, AobScanRequest request, bool hasModuleRange, ModuleRange moduleRange) { @@ -208,11 +267,11 @@ private bool TryGetModuleRange(ModuleName requested, out bool hasRange, out Modu out CheatEngineFailure failure) { ModuleInfo[] modules = new ModuleInfo[_maximumModuleSnapshot]; + hasRange = false; + range = default; InspectionStatus status = _scanPort.EnumerateModules(modules, out int written); if (status != InspectionStatus.Success) { - hasRange = false; - range = default; failure = new CheatEngineFailure( status == InspectionStatus.DestinationTooSmall ? CheatEngineFailureKind.ResultLimitExceeded @@ -221,8 +280,14 @@ private bool TryGetModuleRange(ModuleName requested, out bool hasRange, out Modu return false; } + if ((uint) written > modules.Length) + { + failure = new CheatEngineFailure(CheatEngineFailureKind.InvalidHostResult, _inModuleOperation, + "Cheat Engine returned an invalid module count."); + return false; + } + bool found = false; - range = default; for (int index = 0; index < written; index++) { ModuleInfo module = modules[index]; @@ -233,7 +298,6 @@ private bool TryGetModuleRange(ModuleName requested, out bool hasRange, out Modu if (found) { - hasRange = false; failure = new CheatEngineFailure(CheatEngineFailureKind.AmbiguousMatch, _inModuleOperation, $"More than one module named '{requested.Value}' was present in the selected target."); return false; @@ -241,7 +305,6 @@ private bool TryGetModuleRange(ModuleName requested, out bool hasRange, out Modu if (!module.ImageSize.HasValue) { - hasRange = false; failure = new CheatEngineFailure(CheatEngineFailureKind.CapabilityUnavailable, _inModuleOperation, "Cheat Engine did not report the requested module's image size."); return false; @@ -249,7 +312,6 @@ private bool TryGetModuleRange(ModuleName requested, out bool hasRange, out Modu if (module.ImageSize.Value.Value == 0) { - hasRange = false; failure = new CheatEngineFailure(CheatEngineFailureKind.InvalidHostResult, _inModuleOperation, "Cheat Engine reported a zero-length requested module."); return false; @@ -266,7 +328,6 @@ private bool TryGetModuleRange(ModuleName requested, out bool hasRange, out Modu return true; } - hasRange = false; failure = new CheatEngineFailure(CheatEngineFailureKind.NotFound, _inModuleOperation, $"Module '{requested.Value}' was not present in the selected target."); return false; diff --git a/libs/CheatEngine.Client.Core/Domains/ProbeResult.cs b/libs/CheatEngine.Client.Core/Domains/ProbeResult.cs index bda6008..cc4704c 100644 --- a/libs/CheatEngine.Client.Core/Domains/ProbeResult.cs +++ b/libs/CheatEngine.Client.Core/Domains/ProbeResult.cs @@ -27,12 +27,14 @@ internal static ProbeResult MissingCapability() internal static ProbeResult Unknown(string reason) { - return new ProbeResult(new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Unknown, reason), default); + return new ProbeResult(new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Unknown, reason), + default); } internal static ProbeResult Faulted(string reason) { - return new ProbeResult(new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Faulted, reason), default); + return new ProbeResult(new ClientCapabilityEvidenceGate(ClientCapabilityEvidenceState.Faulted, reason), + default); } internal static ProbeResult Malformed(string reason) diff --git a/libs/CheatEngine.Client.Core/Domains/ProcessClient.cs b/libs/CheatEngine.Client.Core/Domains/ProcessClient.cs index bf2df22..ab9a794 100644 --- a/libs/CheatEngine.Client.Core/Domains/ProcessClient.cs +++ b/libs/CheatEngine.Client.Core/Domains/ProcessClient.cs @@ -21,12 +21,14 @@ internal sealed class ProcessClient : IProcessClient private ProcessSelection? _lastSelection; internal ProcessClient(ICheatEngineDispatcher dispatcher, CoreLifetime lifetime) - : this(dispatcher, new LocalProcessHost(), lifetime?.TargetSelection ?? throw new ArgumentNullException(nameof(lifetime)), lifetime.ThrowIfInactive) + : this(dispatcher, new LocalProcessHost(), + lifetime?.TargetSelection ?? throw new ArgumentNullException(nameof(lifetime)), lifetime.ThrowIfInactive) { } internal ProcessClient(ICheatEngineDispatcher dispatcher, IProcessHost host, CoreLifetime lifetime) - : this(dispatcher, host, lifetime?.TargetSelection ?? throw new ArgumentNullException(nameof(lifetime)), lifetime.ThrowIfInactive) + : this(dispatcher, host, lifetime?.TargetSelection ?? throw new ArgumentNullException(nameof(lifetime)), + lifetime.ThrowIfInactive) { } @@ -38,7 +40,8 @@ internal ProcessClient( { } - internal ProcessClient(ICheatEngineDispatcher dispatcher, IProcessHost host, TargetSelectionLifetime selectionLifetime, Action? admitStatefulOperation) + internal ProcessClient(ICheatEngineDispatcher dispatcher, IProcessHost host, + TargetSelectionLifetime selectionLifetime, Action? admitStatefulOperation) { _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); _host = host ?? throw new ArgumentNullException(nameof(host)); @@ -94,17 +97,18 @@ public bool TryAttach( { throw new ArgumentOutOfRangeException(nameof(processId)); } + Admit("Processes.Attach"); CurrentProcessCapture captured = default; if (!_dispatcher.TryInvoke( - () => - { - _host.OpenProcess(processId.Value); - captured = CaptureCurrent("Processes.Attach"); - }, - out failure, - cancellationToken)) + () => + { + _host.OpenProcess(processId.Value); + captured = CaptureCurrent("Processes.Attach"); + }, + out failure, + cancellationToken)) { snapshot = default; return false; @@ -153,13 +157,14 @@ public bool TryAttachExactName( failure = Cancelled("Processes.AttachExactName"); return false; } + IReadOnlyList matches; try { matches = _host.FindProcessesByExactName(expectedName); } catch (Exception exception) when (exception is ArgumentException or InvalidOperationException or Win32Exception - or PlatformNotSupportedException) + or PlatformNotSupportedException) { snapshot = default; failure = new CheatEngineFailure( @@ -335,7 +340,8 @@ private CurrentProcessCapture CaptureCurrent(string operation) } CheatEngineArchitecture architecture = TryGetTargetArchitecture(); - return new CurrentProcessCapture(ObserveSelection(id, hasLocalMetadata ? process : default, architecture, operation)); + return new CurrentProcessCapture(ObserveSelection(id, hasLocalMetadata ? process : default, architecture, + operation)); } private static bool TryGetCapturedSnapshot( @@ -479,7 +485,10 @@ private bool TryUnavailable( return false; } - private void Admit(string operation) => _admitStatefulOperation?.Invoke(operation); + private void Admit(string operation) + { + _admitStatefulOperation?.Invoke(operation); + } private static CheatEngineFailure Cancelled(string operation) { diff --git a/libs/CheatEngine.Client.Core/Domains/RecordLookupStatus.cs b/libs/CheatEngine.Client.Core/Domains/RecordLookupStatus.cs new file mode 100644 index 0000000..5b6316e --- /dev/null +++ b/libs/CheatEngine.Client.Core/Domains/RecordLookupStatus.cs @@ -0,0 +1,10 @@ +namespace CheatEngine.Client.Core.Domains; + +/// Outcome of one protected Address List record lookup. +internal enum RecordLookupStatus +{ + Success, + NotFound, + AddressListUnavailable, + InvalidRecord +} diff --git a/libs/CheatEngine.Client.Core/Domains/RemoteExecution/UnavailableRemoteExecutionClient.cs b/libs/CheatEngine.Client.Core/Domains/RemoteExecution/UnavailableRemoteExecutionClient.cs index de70b24..6259d50 100644 --- a/libs/CheatEngine.Client.Core/Domains/RemoteExecution/UnavailableRemoteExecutionClient.cs +++ b/libs/CheatEngine.Client.Core/Domains/RemoteExecution/UnavailableRemoteExecutionClient.cs @@ -14,6 +14,7 @@ internal UnavailableRemoteExecutionClient(CoreLifetime? lifetime = null) { _lifetime = lifetime; } + public bool TryInjectLibrary(RemoteDllInjectionRequest request, out CheatEngineFailure failure, CancellationToken cancellationToken = default) { @@ -43,6 +44,7 @@ public RemoteCallResult Invoke(RemoteCallRequest request, CancellationToken canc private CheatEngineFailure CreateFailure(string operation, CancellationToken cancellationToken) { - return UnavailableCapabilityFailure.Create(_lifetime, "Remote execution and injection", operation, cancellationToken); + return UnavailableCapabilityFailure.Create(_lifetime, "Remote execution and injection", operation, + cancellationToken); } } diff --git a/libs/CheatEngine.Client.Core/Domains/RuntimeClient.cs b/libs/CheatEngine.Client.Core/Domains/RuntimeClient.cs index 6280cdd..4159b54 100644 --- a/libs/CheatEngine.Client.Core/Domains/RuntimeClient.cs +++ b/libs/CheatEngine.Client.Core/Domains/RuntimeClient.cs @@ -231,10 +231,12 @@ private ClientCapabilities CreateClientCapabilities(ProbeResult openedProc qualificationUnknown, policyNotRequired, lifetime), Describe(ClientCapabilityId.TypedMemory, implemented, packageUnknown, unprobedHost, qualificationUnknown, policyNotRequired, lifetime), - Describe(ClientCapabilityId.PatternScanning, implemented, packageUnknown, unprobedHost, qualificationUnknown, + Describe(ClientCapabilityId.PatternScanning, implemented, packageUnknown, unprobedHost, + qualificationUnknown, policyNotRequired, lifetime), Describe(ClientCapabilityId.ValueScanning, contractOnly, - Missing("CheatEngine.SDK 1.0.0 does not provide the public MemScan and FoundList ownership factory required by Client."), + Missing( + "CheatEngine.SDK 1.0.0 does not provide the public MemScan and FoundList ownership factory required by Client."), unprobedHost, qualificationUnknown, policyNotRequired, lifetime), Describe(ClientCapabilityId.Inspection, implemented, packageUnknown, unprobedHost, qualificationUnknown, policyNotRequired, lifetime), @@ -246,13 +248,15 @@ private ClientCapabilities CreateClientCapabilities(ProbeResult openedProc qualificationUnknown, _policy.EnableUnsafeLuaExecution ? Satisfied("Unsafe Lua execution was explicitly enabled for this activation.") - : Missing("Unsafe Lua execution requires explicit EnableUnsafeLuaExecution opt-in for this activation."), + : Missing( + "Unsafe Lua execution requires explicit EnableUnsafeLuaExecution opt-in for this activation."), lifetime), Describe(ClientCapabilityId.Allocations, contractOnly, packageUnknown, unprobedHost, qualificationUnknown, policyNotRequired, lifetime), Describe(ClientCapabilityId.Assembly, contractOnly, packageUnknown, unprobedHost, qualificationUnknown, policyNotRequired, lifetime), - Describe(ClientCapabilityId.RemoteExecution, contractOnly, packageUnknown, unprobedHost, qualificationUnknown, + Describe(ClientCapabilityId.RemoteExecution, contractOnly, packageUnknown, unprobedHost, + qualificationUnknown, policyNotRequired, lifetime), Describe(ClientCapabilityId.Debugger, contractOnly, packageUnknown, unprobedHost, qualificationUnknown, policyNotRequired, lifetime), @@ -310,7 +314,8 @@ private static ProbeResult ValidateOpenedProcess(ProbeResult probe) { return probe.HasValue && probe.Value is { } processId && (processId < 0 || processId > int.MaxValue) - ? ProbeResult.Malformed("Cheat Engine returned an opened process identifier outside the supported PID range.") + ? ProbeResult.Malformed( + "Cheat Engine returned an opened process identifier outside the supported PID range.") : probe; } diff --git a/libs/CheatEngine.Client.Core/Domains/SdkAobScanPort.cs b/libs/CheatEngine.Client.Core/Domains/SdkAobScanPort.cs index 46e0110..91447fd 100644 --- a/libs/CheatEngine.Client.Core/Domains/SdkAobScanPort.cs +++ b/libs/CheatEngine.Client.Core/Domains/SdkAobScanPort.cs @@ -13,7 +13,7 @@ public AobScanHostStatus TryScan(string pattern, AobScanOptions options, [NotNullWhen(true)] out IAobMatchList? matches) { matches = null; - if (!AobScanner.TryScan(pattern, options, out Owned? owner) || owner is null) + if (!AobScanner.TryScan(pattern, options, out Owned? owner)) { return AobScanHostStatus.Rejected; } diff --git a/libs/CheatEngine.Client.Core/Domains/SdkTableRecordLookupPort.cs b/libs/CheatEngine.Client.Core/Domains/SdkTableRecordLookupPort.cs new file mode 100644 index 0000000..1402c93 --- /dev/null +++ b/libs/CheatEngine.Client.Core/Domains/SdkTableRecordLookupPort.cs @@ -0,0 +1,43 @@ +using CheatEngine.Client.Tables; +using CheatEngine.SDK.Engine.AddressList; + +namespace CheatEngine.Client.Core.Domains; + +/// Protected SDK implementation of Address List record lookups. +internal sealed class SdkTableRecordLookupPort : ITableRecordLookupPort +{ + public RecordLookupStatus TryGetRecord(int index, out MemoryRecordSnapshot record) + { + return TryLookup(list => list.TryGetMemoryRecord(index, out MemoryRecord value) ? value : null, out record); + } + + public RecordLookupStatus TryGetRecord(MemoryRecordId id, out MemoryRecordSnapshot record) + { + return TryLookup(list => list.TryGetMemoryRecordById(id, out MemoryRecord value) ? value : null, out record); + } + + public RecordLookupStatus TryGetSelected(out MemoryRecordSnapshot record) + { + return TryLookup(list => list.TryGetSelectedRecord(out MemoryRecord value) ? value : null, out record); + } + + private static RecordLookupStatus TryLookup(Func selector, + out MemoryRecordSnapshot record) + { + record = default; + if (!AddressListAccess.TryGetCurrent(out AddressList list)) + { + return RecordLookupStatus.AddressListUnavailable; + } + + MemoryRecord? value = selector(list); + if (!value.HasValue) + { + return RecordLookupStatus.NotFound; + } + + return TableClient.TrySnapshot(value.Value, out record) + ? RecordLookupStatus.Success + : RecordLookupStatus.InvalidRecord; + } +} diff --git a/libs/CheatEngine.Client.Core/Domains/Speed/UnavailableSpeedClient.cs b/libs/CheatEngine.Client.Core/Domains/Speed/UnavailableSpeedClient.cs index 3cd906d..c901356 100644 --- a/libs/CheatEngine.Client.Core/Domains/Speed/UnavailableSpeedClient.cs +++ b/libs/CheatEngine.Client.Core/Domains/Speed/UnavailableSpeedClient.cs @@ -14,6 +14,7 @@ internal UnavailableSpeedClient(CoreLifetime? lifetime = null) { _lifetime = lifetime; } + public bool TryGetMultiplier(out SpeedMultiplier multiplier, out CheatEngineFailure failure, CancellationToken cancellationToken = default) { diff --git a/libs/CheatEngine.Client.Core/Domains/TableClient.cs b/libs/CheatEngine.Client.Core/Domains/TableClient.cs index 54fe7a9..0e055aa 100644 --- a/libs/CheatEngine.Client.Core/Domains/TableClient.cs +++ b/libs/CheatEngine.Client.Core/Domains/TableClient.cs @@ -15,16 +15,19 @@ internal sealed class TableClient( ICheatEngineDispatcher dispatcher, CoreClientPolicy policy, ITableRecordMutationPort? recordMutations = null, - CoreLifetime? lifetime = null) : ITableClient + CoreLifetime? lifetime = null, + ITableRecordLookupPort? recordLookups = null) : ITableClient { private const string _getHierarchyOperation = "Tables.GetHierarchy"; private readonly ICheatEngineDispatcher _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); + private readonly CoreLifetime? _lifetime = lifetime; + private readonly CoreClientPolicy _policy = policy ?? throw new ArgumentNullException(nameof(policy)); + private readonly ITableRecordLookupPort _recordLookups = recordLookups ?? new SdkTableRecordLookupPort(); private readonly ITableRecordMutationPort _recordMutations = recordMutations ?? new SdkTableRecordMutationPort(); - private readonly CoreLifetime? _lifetime = lifetime; public bool TryGetCurrent(out AddressTableSnapshot table, out CheatEngineFailure failure, CancellationToken cancellationToken = default) @@ -145,17 +148,15 @@ public bool TryGetRecord(int index, out MemoryRecordSnapshot record, out CheatEn CancellationToken cancellationToken = default) { ArgumentOutOfRangeException.ThrowIfNegative(index); - return TryRecord("Tables.GetRecord", - list => list.TryGetMemoryRecord(index, out MemoryRecord value) ? value : null, - out record, out failure, cancellationToken); + return TryRecord("Tables.GetRecord", (out result) => + _recordLookups.TryGetRecord(index, out result), out record, out failure, cancellationToken); } public bool TryGetRecord(MemoryRecordId id, out MemoryRecordSnapshot record, out CheatEngineFailure failure, CancellationToken cancellationToken = default) { - return TryRecord("Tables.GetRecord", - list => list.TryGetMemoryRecordById(id, out MemoryRecord value) ? value : null, - out record, out failure, cancellationToken); + return TryRecord("Tables.GetRecord", (out result) => + _recordLookups.TryGetRecord(id, out result), out record, out failure, cancellationToken); } public MemoryRecordSnapshot GetRecord(int index, CancellationToken cancellationToken = default) @@ -183,8 +184,8 @@ public MemoryRecordSnapshot GetRecord(MemoryRecordId id, CancellationToken cance public bool TryGetSelected(out MemoryRecordSnapshot record, out CheatEngineFailure failure, CancellationToken cancellationToken = default) { - return TryRecord("Tables.GetSelected", list => list.TryGetSelectedRecord(out MemoryRecord value) ? value : null, - out record, out failure, cancellationToken); + return TryRecord("Tables.GetSelected", _recordLookups.TryGetSelected, out record, out failure, + cancellationToken); } public MemoryRecordSnapshot GetSelected(CancellationToken cancellationToken = default) @@ -469,34 +470,24 @@ public void SaveTable(TableSaveRequest request, CancellationToken cancellationTo } } - private bool TryRecord(string operation, Func selector, + private bool TryRecord(string operation, RecordLookup lookup, out MemoryRecordSnapshot record, out CheatEngineFailure failure, CancellationToken cancellationToken) { MemoryRecordSnapshot captured = default; - bool succeeded = false; - if (!_dispatcher.TryInvoke(() => - { - if (!AddressListAccess.TryGetCurrent(out AddressList list)) - { - return; - } - - MemoryRecord? value = selector(list); - succeeded = value.HasValue && TrySnapshot(value.Value, out captured); - }, out failure, cancellationToken)) + RecordLookupStatus status = RecordLookupStatus.InvalidRecord; + if (!_dispatcher.TryInvoke(() => status = lookup(out captured), out failure, cancellationToken)) { record = default; return false; } record = captured; - if (succeeded) + if (status == RecordLookupStatus.Success) { return true; } - failure = new CheatEngineFailure(CheatEngineFailureKind.NotFound, operation, - "The requested Cheat Engine memory record was not found or was malformed."); + failure = LookupFailure(operation, status); return false; } @@ -782,6 +773,20 @@ private static CheatEngineFailure HostFailure(string operation) "Cheat Engine did not return the expected Address List contract."); } + private static CheatEngineFailure LookupFailure(string operation, RecordLookupStatus status) + { + return status switch + { + RecordLookupStatus.NotFound => new CheatEngineFailure(CheatEngineFailureKind.NotFound, operation, + "The requested Cheat Engine memory record was not found."), + RecordLookupStatus.AddressListUnavailable => new CheatEngineFailure( + CheatEngineFailureKind.CapabilityUnavailable, operation, + "Cheat Engine's Address List capability is unavailable."), + RecordLookupStatus.InvalidRecord => HostFailure(operation), + _ => HostFailure(operation) + }; + } + private static CheatEngineFailure ResultLimitFailure(string operation, int maximumItems) { return new CheatEngineFailure(CheatEngineFailureKind.ResultLimitExceeded, operation, @@ -827,4 +832,6 @@ private enum HierarchyBuildProblem DepthLimit, InvalidShape } + + private delegate RecordLookupStatus RecordLookup(out MemoryRecordSnapshot record); } diff --git a/libs/CheatEngine.Client.Core/Domains/Timers/UnavailableTimerClient.cs b/libs/CheatEngine.Client.Core/Domains/Timers/UnavailableTimerClient.cs index 8ff19e4..0333f26 100644 --- a/libs/CheatEngine.Client.Core/Domains/Timers/UnavailableTimerClient.cs +++ b/libs/CheatEngine.Client.Core/Domains/Timers/UnavailableTimerClient.cs @@ -17,6 +17,7 @@ internal UnavailableTimerClient(CoreLifetime? lifetime = null) { _lifetime = lifetime; } + public bool TryRegister(TimerRequest request, TimerHandler handler, EventStreamOptions streamOptions, [NotNullWhen(true)] out ITimerLease? lease, out CheatEngineFailure failure, CancellationToken cancellationToken = default) diff --git a/libs/CheatEngine.Client.Core/Domains/UnavailableValueScanner.cs b/libs/CheatEngine.Client.Core/Domains/UnavailableValueScanner.cs index c27a94b..b5f9aac 100644 --- a/libs/CheatEngine.Client.Core/Domains/UnavailableValueScanner.cs +++ b/libs/CheatEngine.Client.Core/Domains/UnavailableValueScanner.cs @@ -1,8 +1,8 @@ using System.Diagnostics; +using CheatEngine.Client.Core.Infrastructure; using CheatEngine.Client.Results; using CheatEngine.Client.Scanning; -using CheatEngine.Client.Core.Infrastructure; namespace CheatEngine.Client.Core.Domains; @@ -12,8 +12,6 @@ namespace CheatEngine.Client.Core.Domains; /// internal sealed class UnavailableValueScanner : IValueScanner { - private readonly CoreLifetime? _lifetime; - internal UnavailableValueScanner(CoreLifetime? lifetime = null) => _lifetime = lifetime; // This is a deliberate product gate, not a transient host-capability probe. SDK 1.0.0 exposes the state machine // only through MemoryScanSession.Adopt(Owned, Owned), while Owned has an internal // constructor and the Lua-global generator cannot marshal CEObject results. Bypassing that with reflection or a @@ -23,6 +21,13 @@ internal sealed class UnavailableValueScanner : IValueScanner "createFoundList, and its generated Lua globals cannot return CEObject handles. Enablement requires the " + "Cheat Engine 7.7 ownership and reactivation live gate."; + private readonly CoreLifetime? _lifetime; + + internal UnavailableValueScanner(CoreLifetime? lifetime = null) + { + _lifetime = lifetime; + } + public bool TryCreateSession(out IValueScanSession? session, out CheatEngineFailure failure, CancellationToken cancellationToken = default) { diff --git a/libs/CheatEngine.Client.Core/Domains/UnsafeLuaClient.cs b/libs/CheatEngine.Client.Core/Domains/UnsafeLuaClient.cs index f1a3821..a0e2012 100644 --- a/libs/CheatEngine.Client.Core/Domains/UnsafeLuaClient.cs +++ b/libs/CheatEngine.Client.Core/Domains/UnsafeLuaClient.cs @@ -18,7 +18,8 @@ internal sealed class UnsafeLuaClient : IUnsafeLuaClient private readonly CoreClientPolicy _policy; internal UnsafeLuaClient(SdkMainThreadDispatcher dispatcher, CoreClientPolicy policy, CoreLifetime lifetime) - : this((ICheatEngineDispatcher) (dispatcher ?? throw new ArgumentNullException(nameof(dispatcher))), policy, lifetime) + : this((ICheatEngineDispatcher) (dispatcher ?? throw new ArgumentNullException(nameof(dispatcher))), policy, + lifetime) { } diff --git a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientOptions.cs b/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientOptions.cs index a817f6d..011d776 100644 --- a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientOptions.cs +++ b/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientOptions.cs @@ -1,5 +1,7 @@ using System.ComponentModel.DataAnnotations; +using CheatEngine.Client.Memory; + namespace CheatEngine.Client.Extensions.DependencyInjection; /// Configuration values used by the high-level Cheat Engine client during one plugin activation. @@ -28,4 +30,15 @@ public string[]? AllowedTableRoots get; set; } = Array.Empty(); + + /// Gets or sets the target-memory budgets captured when an activation creates its client services. + /// + /// The registration validates and copies these values when it constructs MemoryClient; changing this + /// options object afterwards cannot change the active memory policy. + /// + public MemoryResourceLimits? MemoryResourceLimits + { + get; + set; + } = new(); } diff --git a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientOptionsSemanticValidator.cs b/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientOptionsSemanticValidator.cs index 171567f..32e0484 100644 --- a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientOptionsSemanticValidator.cs +++ b/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientOptionsSemanticValidator.cs @@ -20,6 +20,20 @@ public ValidateOptionsResult Validate(string? name, CheatEngineClientOptions opt return ValidateOptionsResult.Fail("AllowedTableRoots must be an empty array or contain absolute paths."); } + if (options.MemoryResourceLimits is null) + { + return ValidateOptionsResult.Fail("MemoryResourceLimits must be configured."); + } + + try + { + _ = options.MemoryResourceLimits.CreateSnapshot(); + } + catch (ArgumentOutOfRangeException exception) + { + return ValidateOptionsResult.Fail($"MemoryResourceLimits is invalid: {exception.Message}"); + } + HashSet roots = new(StringComparer.OrdinalIgnoreCase); foreach (string root in options.AllowedTableRoots) { diff --git a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs b/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs index 2ae4439..10c2794 100644 --- a/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs +++ b/libs/CheatEngine.Client.Extensions.DependencyInjection/CheatEngineClientServiceCollectionExtensions.cs @@ -130,11 +130,21 @@ private static void AddCoreServices(IServiceCollection services) serviceProvider.GetRequiredService()); services.TryAddSingleton(static serviceProvider => - new MemoryClient( + { + CheatEngineClientOptions options = + serviceProvider.GetRequiredService>().Value; + MemoryResourceLimits limits = options.MemoryResourceLimits + ?? throw new InvalidOperationException( + "MemoryResourceLimits must be validated before the Client memory service is created."); + return new MemoryClient( serviceProvider.GetRequiredService(), - serviceProvider.GetRequiredService())); + serviceProvider.GetRequiredService(), + limits); + }); services.TryAddSingleton(static serviceProvider => serviceProvider.GetRequiredService()); + services.TryAddSingleton(static serviceProvider => + serviceProvider.GetRequiredService()); services.TryAddSingleton(static serviceProvider => new PatternScanner(serviceProvider.GetRequiredService())); diff --git a/libs/CheatEngine.Client.Extensions.DependencyInjection/PublicAPI.Unshipped.txt b/libs/CheatEngine.Client.Extensions.DependencyInjection/PublicAPI.Unshipped.txt index 0242f49..38e93aa 100644 --- a/libs/CheatEngine.Client.Extensions.DependencyInjection/PublicAPI.Unshipped.txt +++ b/libs/CheatEngine.Client.Extensions.DependencyInjection/PublicAPI.Unshipped.txt @@ -1,2 +1,4 @@ #nullable enable +CheatEngine.Client.Extensions.DependencyInjection.CheatEngineClientOptions.MemoryResourceLimits.get -> CheatEngine.Client.Memory.MemoryResourceLimits? +CheatEngine.Client.Extensions.DependencyInjection.CheatEngineClientOptions.MemoryResourceLimits.set -> void CheatEngine.Client.Extensions.DependencyInjection.CheatEngineClientBuilder.AddLuaModule() -> CheatEngine.Client.Extensions.DependencyInjection.CheatEngineClientBuilder! diff --git a/libs/CheatEngine.Client.Fluent/PublicAPI.Unshipped.txt b/libs/CheatEngine.Client.Fluent/PublicAPI.Unshipped.txt index d9f4230..3b8c4b1 100644 --- a/libs/CheatEngine.Client.Fluent/PublicAPI.Unshipped.txt +++ b/libs/CheatEngine.Client.Fluent/PublicAPI.Unshipped.txt @@ -26,3 +26,4 @@ CheatEngine.Client.Memory.MemoryPrimitiveBatchBuilder.Read(System.ReadOnlySpa CheatEngine.Client.Memory.MemoryPrimitiveBatchBuilder.TryRead(System.ReadOnlySpan addresses, out System.Collections.Immutable.ImmutableArray values, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool CheatEngine.Client.Memory.MemoryPrimitiveBatchBuilder.TryWrite(System.ReadOnlySpan> values, out CheatEngine.Client.Results.CheatEngineFailure failure, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> bool CheatEngine.Client.Memory.MemoryPrimitiveBatchBuilder.Write(System.ReadOnlySpan> values, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> void +CheatEngine.Client.Scanning.AobScanBuilder.Executable() -> CheatEngine.Client.Scanning.AobScanBuilder diff --git a/libs/CheatEngine.Client.Fluent/Scanning/AobScanBuilder.cs b/libs/CheatEngine.Client.Fluent/Scanning/AobScanBuilder.cs index e4e96e2..ac4c34c 100644 --- a/libs/CheatEngine.Client.Fluent/Scanning/AobScanBuilder.cs +++ b/libs/CheatEngine.Client.Fluent/Scanning/AobScanBuilder.cs @@ -32,28 +32,31 @@ public AobScanOptions Options get; } - /// Gets the optional module-range filter that Core resolves before materializing matches. + /// + /// Gets the optional module name that Core resolves before the global scan and uses as a copied-address + /// post-filter. + /// public ModuleName? Module { get; } - /// Gets the optional inclusive copied-address range filter. + /// Gets the optional inclusive copied-address post-filter. public AobScanRange? Range { get; } - /// Returns an equivalent builder that restricts results to one named target module. - /// The non-empty module name understood by Cheat Engine's symbol handler. + /// Returns an equivalent builder with a named-module copied-address post-filter. + /// The non-empty module name that Core resolves before starting the global scan. /// A new immutable builder. public AobScanBuilder InModule(string moduleName) { return InModule(new ModuleName(moduleName)); } - /// Returns an equivalent builder that restricts results to one target module. - /// The module-range filter resolved by Core before result materialization. + /// Returns an equivalent builder with a target-module copied-address post-filter. + /// The module name Core resolves before the global scan, then applies while copying results. /// A new immutable builder. public AobScanBuilder InModule(ModuleName module) { @@ -65,30 +68,42 @@ public AobScanBuilder InModule(ModuleName module) return new AobScanBuilder(_scanner, Pattern, Options, module, Range); } - /// Returns an equivalent builder that retains only match addresses in an inclusive target-address range. + /// Returns an equivalent builder with an inclusive copied-address post-filter. /// The first included target address. /// The last included target address. /// A new immutable builder. /// - /// The SDK's string-form AOBScan binding has no start/end arguments. Core applies this range while - /// copying the owned result list, before the requested materialization limit is counted. + /// The SDK's string-form AOBScan binding has no start/end arguments. This does not narrow the global + /// Cheat Engine scan; Core applies the range while copying the owned result list, before the materialization + /// limit is counted. /// public AobScanBuilder InRange(Address start, Address end) { return new AobScanBuilder(_scanner, Pattern, Options, Module, new AobScanRange(start, end)); } - /// Returns an equivalent builder that searches read-only executable memory. + /// Returns an equivalent builder that searches executable, non-copy-on-write, non-writable memory. /// /// Cheat Engine's documented protection grammar does not expose a readable bit. +X-C-W therefore means /// executable, not copy-on-write, and not writable memory. /// /// A new immutable builder. - public AobScanBuilder ReadableExecutable() + public AobScanBuilder Executable() { return WithOptions(new AobScanOptions("+X-C-W", Options.AlignmentMethod, Options.AlignmentParameter)); } + /// Returns an equivalent executable-memory builder through the historical compatibility name. + /// + /// This is an alias for . Cheat Engine's documented protection grammar has no readable + /// bit, so the name does not promise a readable-memory constraint. + /// + /// A new immutable builder. + public AobScanBuilder ReadableExecutable() + { + return Executable(); + } + /// Returns an equivalent builder with the exact Cheat Engine protection expression. /// /// The protection expression accepted by Cheat Engine, or to omit @@ -123,10 +138,14 @@ public AobFirstMatchBuilder FirstOrNone() return new AobFirstMatchBuilder(RequireScanner(), BuildRequest(1)); } - /// Selects an operation that materializes no more than the requested number of matches. - /// The positive maximum number of copied addresses to materialize. + /// Selects an operation that materializes no more than the requested number of post-filtered matches. + /// The positive maximum number of copied addresses that survive managed post-filters. /// An immutable bounded-result terminal builder. /// is zero or negative. + /// + /// This bound applies only while Core materializes the SDK-owned result list. It is not pushed into Cheat Engine, + /// does not request early termination, and does not reduce global scan work. + /// public AobManyMatchBuilder Take(int maximumResults) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maximumResults); diff --git a/libs/CheatEngine.Client.Hosting/CheatEngineClientPlugin.cs b/libs/CheatEngine.Client.Hosting/CheatEngineClientPlugin.cs index 6c4dd0a..5174772 100644 --- a/libs/CheatEngine.Client.Hosting/CheatEngineClientPlugin.cs +++ b/libs/CheatEngine.Client.Hosting/CheatEngineClientPlugin.cs @@ -11,7 +11,10 @@ namespace CheatEngine.Client.Hosting; -/// Base class that activates an activation-owned for each Cheat Engine enable epoch. +/// +/// Base class that activates an activation-owned for each Cheat Engine enable +/// epoch. +/// /// /// The SDK constructs a plugin through a parameterless factory and reuses that instance across enable/disable cycles. /// This base class therefore creates a fresh validated provider and scope only from , when the diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..4d736c1 --- /dev/null +++ b/nuget.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/source-generators/CheatEngine.Client.SourceGenerators.Lua/AnalyzerReleases.Unshipped.md b/source-generators/CheatEngine.Client.SourceGenerators.Lua/AnalyzerReleases.Unshipped.md index c776cd5..bf7c95c 100644 --- a/source-generators/CheatEngine.Client.SourceGenerators.Lua/AnalyzerReleases.Unshipped.md +++ b/source-generators/CheatEngine.Client.SourceGenerators.Lua/AnalyzerReleases.Unshipped.md @@ -1,7 +1,7 @@ ### New Rules Rule ID | Category | Severity | Notes -------------|------------------------|----------|--------------------------------------------------------------- +------------|------------------------|----------|-------------------------------------------------------------------------------------- CECLUA1001 | CheatEngine.Client.Lua | Error | A generated Lua module must be a supported partial class. CECLUA1002 | CheatEngine.Client.Lua | Error | A generated Lua module requires static SDK bindings. CECLUA1003 | CheatEngine.Client.Lua | Error | A generated Lua module requires at least one export. diff --git a/source-generators/CheatEngine.Client.SourceGenerators.Lua/CheatEngineLuaGenerator.cs b/source-generators/CheatEngine.Client.SourceGenerators.Lua/CheatEngineLuaGenerator.cs index 876983b..f7428c0 100644 --- a/source-generators/CheatEngine.Client.SourceGenerators.Lua/CheatEngineLuaGenerator.cs +++ b/source-generators/CheatEngine.Client.SourceGenerators.Lua/CheatEngineLuaGenerator.cs @@ -28,8 +28,10 @@ public sealed class CheatEngineLuaGenerator : IIncrementalGenerator private const string LuaResultMapperMetadataName = "CheatEngine.Client.Lua.ILuaResultMapper"; private const string LuaClassAttributeMetadataName = "CheatEngine.SDK.Annotations.Lua.LuaClassAttribute"; private const string CheatEngineSdkAssemblyPrefix = "CheatEngine.SDK"; + private const string CheatEngineSdkObjectContractMetadataName = "CheatEngine.SDK.Engine.Objects.ICEObject"; + private const int ClientBoundaryMaximumDepth = 32; private const int ClientBoundaryMaximumNodes = 256; @@ -59,6 +61,53 @@ public sealed class CheatEngineLuaGenerator : IIncrementalGenerator "CheatEngine.SDK.Engine.Values.Address" }; + // The generated Client contract is an ownership boundary, so framework provenance is not sufficient proof + // that a value is safe to expose. Keep this deliberately small and require all contained type arguments to + // pass the same boundary walk. + private static readonly HashSet ApprovedFrameworkValueTypes = new(StringComparer.Ordinal) + { + "System.DateOnly", + "System.DateTime", + "System.DateTimeOffset", + "System.Decimal", + "System.Guid", + "System.Half", + "System.Index", + "System.Int128", + "System.Range", + "System.Text.Rune", + "System.TimeOnly", + "System.TimeSpan", + "System.UInt128" + }; + + private static readonly HashSet ApprovedFrameworkGenericCollectionTypes = new(StringComparer.Ordinal) + { + "System.Collections.Frozen.FrozenDictionary", + "System.Collections.Frozen.FrozenSet", + "System.Collections.Generic.IEnumerable", + "System.Collections.Generic.IReadOnlyCollection", + "System.Collections.Generic.IReadOnlyDictionary", + "System.Collections.Generic.IReadOnlyList", + "System.Collections.Generic.IReadOnlySet", + "System.Collections.Immutable.IImmutableDictionary", + "System.Collections.Immutable.IImmutableList", + "System.Collections.Immutable.IImmutableQueue", + "System.Collections.Immutable.IImmutableSet", + "System.Collections.Immutable.IImmutableStack", + "System.Collections.Immutable.ImmutableArray", + "System.Collections.Immutable.ImmutableDictionary", + "System.Collections.Immutable.ImmutableHashSet", + "System.Collections.Immutable.ImmutableList", + "System.Collections.Immutable.ImmutableQueue", + "System.Collections.Immutable.ImmutableSortedDictionary", + "System.Collections.Immutable.ImmutableSortedSet", + "System.Collections.Immutable.ImmutableStack", + "System.Collections.ObjectModel.ReadOnlyCollection", + "System.Collections.ObjectModel.ReadOnlyDictionary", + "System.Nullable" + }; + /// public void Initialize(IncrementalGeneratorInitializationContext context) { @@ -245,7 +294,7 @@ private static string EmitOperation(OperationCandidate candidate) source.WriteLine("if (!" + candidate.BindingsType + "." + candidate.MethodName + "(" + EmitOutArgumentList(candidate.Parameters) + "))"); source.OpenBlock(); - source.WriteLine("result = default;"); + source.WriteLine("result = default!;"); source.WriteLine("failure = new global::CheatEngine.Client.Results.CheatEngineFailure("); source.Indent(); source.WriteLine("global::CheatEngine.Client.Results.CheatEngineFailureKind.LuaError,"); @@ -270,7 +319,7 @@ private static string EmitOperation(OperationCandidate candidate) source.CloseBlock(); source.WriteLine("catch (global::CheatEngine.SDK.Lua.Calls.LuaException exception)"); source.OpenBlock(); - source.WriteLine("result = default;"); + source.WriteLine("result = default!;"); source.WriteLine("failure = new global::CheatEngine.Client.Results.CheatEngineFailure("); source.Indent(); source.WriteLine("global::CheatEngine.Client.Results.CheatEngineFailureKind.LuaError,"); @@ -282,7 +331,7 @@ private static string EmitOperation(OperationCandidate candidate) source.CloseBlock(); source.WriteLine("catch (global::System.Exception exception)"); source.OpenBlock(); - source.WriteLine("result = default;"); + source.WriteLine("result = default!;"); source.WriteLine("failure = new global::CheatEngine.Client.Results.CheatEngineFailure("); source.Indent(); source.WriteLine("global::CheatEngine.Client.Results.CheatEngineFailureKind.BindingError,"); @@ -455,7 +504,8 @@ SpecialType.System_Boolean or SpecialType.System_Byte or SpecialType.System_SByt SpecialType.System_Char or SpecialType.System_Int16 or SpecialType.System_UInt16 or SpecialType.System_Int32 or SpecialType.System_UInt32 or SpecialType.System_Int64 or SpecialType.System_UInt64 or SpecialType.System_Single or SpecialType.System_Double or - SpecialType.System_String or SpecialType.System_IntPtr or SpecialType.System_UIntPtr => true, + SpecialType.System_String or SpecialType.System_IntPtr or SpecialType.System_UIntPtr or + SpecialType.System_Void => true, _ => false }; } @@ -472,7 +522,7 @@ private static bool TryFindClientBoundaryViolation(ITypeSymbol type, ClientBound private static bool TryFindClientBoundaryViolation(ITypeSymbol type, ClientBoundaryRole role, string path, HashSet visited, ref int visitedCount, int depth, out string violation) { - if (depth > ClientBoundaryMaximumDepth || ++visitedCount > ClientBoundaryMaximumNodes) + if (depth > ClientBoundaryMaximumDepth) { violation = path + " exceeds the supported Client result type graph budget."; return true; @@ -484,6 +534,12 @@ private static bool TryFindClientBoundaryViolation(ITypeSymbol type, ClientBound return false; } + if (++visitedCount > ClientBoundaryMaximumNodes) + { + violation = path + " exceeds the supported Client result type graph budget."; + return true; + } + if (type.TypeKind == TypeKind.Error || type.TypeKind == TypeKind.Dynamic) { violation = path + " uses an unresolved or dynamic type."; @@ -516,7 +572,8 @@ private static bool TryFindClientBoundaryViolation(ITypeSymbol type, ClientBound if (type is ITypeParameterSymbol typeParameter) { - return TryFindConstraintViolation(typeParameter, role, path, visited, ref visitedCount, depth, out violation); + return TryFindConstraintViolation(typeParameter, role, path, visited, ref visitedCount, depth, + out violation); } if (type is not INamedTypeSymbol named) @@ -531,10 +588,10 @@ private static bool TryFindClientBoundaryViolation(ITypeSymbol type, ClientBound } foreach (ITypeParameterSymbol parameter in named.OriginalDefinition.TypeParameters - .OrderBy(static candidate => candidate.Ordinal)) + .OrderBy(static candidate => candidate.Ordinal)) { if (TryFindConstraintViolation(parameter, role, path + "." + parameter.Name, visited, ref visitedCount, - depth + 1, out violation)) + depth + 1, out violation)) { return true; } @@ -545,7 +602,7 @@ private static bool TryFindClientBoundaryViolation(ITypeSymbol type, ClientBound foreach (IFieldSymbol element in named.TupleElements) { if (TryFindClientBoundaryViolation(element.Type, role, path + "." + element.Name, visited, - ref visitedCount, depth + 1, out violation)) + ref visitedCount, depth + 1, out violation)) { return true; } @@ -555,7 +612,7 @@ private static bool TryFindClientBoundaryViolation(ITypeSymbol type, ClientBound foreach (ITypeSymbol argument in named.TypeArguments) { if (TryFindClientBoundaryViolation(argument, role, path + "<" + TypeName(argument) + ">", visited, - ref visitedCount, depth + 1, out violation)) + ref visitedCount, depth + 1, out violation)) { return true; } @@ -576,7 +633,7 @@ private static bool TryFindNamedTypeViolation(INamedTypeSymbol type, ClientBound { string metadataName = type.OriginalDefinition.ToDisplayString(); if (metadataName is "CheatEngine.SDK.Lua.State.LuaState" or "CheatEngine.SDK.Lua.References.LuaRef" or - "CheatEngine.SDK.Engine.Objects.CEObject" or "CheatEngine.SDK.Engine.Objects.Owned" || + "CheatEngine.SDK.Engine.Objects.CEObject" or "CheatEngine.SDK.Engine.Objects.Owned" || type.ContainingNamespace.ToDisplayString().Contains(".Interop", StringComparison.Ordinal) || HasAttribute(type, LuaClassAttributeMetadataName) || ImplementsSdkObjectContract(type)) { @@ -604,6 +661,12 @@ private static bool TryFindNamedTypeViolation(INamedTypeSymbol type, ClientBound return true; } + if (IsFrameworkType(type) && !IsApprovedFrameworkClientBoundaryType(type)) + { + violation = path + " exposes unsupported framework type '" + metadataName + "'."; + return true; + } + violation = string.Empty; return false; } @@ -614,7 +677,7 @@ private static bool TryFindConstraintViolation(ITypeParameterSymbol typeParamete foreach (ITypeSymbol constraint in typeParameter.ConstraintTypes.OrderBy(TypeName, StringComparer.Ordinal)) { if (TryFindClientBoundaryViolation(constraint, role, path + " constraint", visited, ref visitedCount, - depth + 1, out violation)) + depth + 1, out violation)) { return true; } @@ -627,30 +690,39 @@ private static bool TryFindConstraintViolation(ITypeParameterSymbol typeParamete private static bool TryFindUserDefinedDtoViolation(INamedTypeSymbol type, ClientBoundaryRole role, string path, HashSet visited, ref int visitedCount, int depth, out string violation) { - if (type.BaseType is { SpecialType: not SpecialType.System_Object } baseType && + if (type.BaseType is + { + SpecialType: not SpecialType.System_Object and not SpecialType.System_ValueType and + not SpecialType.System_Enum + } baseType && TryFindClientBoundaryViolation(baseType, role, path + ".base", visited, ref visitedCount, depth + 1, - out violation)) + out violation)) { return true; } foreach (INamedTypeSymbol implementedInterface in type.Interfaces.OrderBy(TypeName, StringComparer.Ordinal)) { + if (IsSafeFrameworkDtoImplementationContract(implementedInterface)) + { + continue; + } + if (TryFindClientBoundaryViolation(implementedInterface, role, path + ".interface", visited, - ref visitedCount, depth + 1, out violation)) + ref visitedCount, depth + 1, out violation)) { return true; } } foreach (ISymbol member in type.GetMembers().OrderBy(static candidate => candidate.MetadataName, - StringComparer.Ordinal)) + StringComparer.Ordinal)) { switch (member) { case IFieldSymbol { IsStatic: false } field: if (TryFindClientBoundaryViolation(field.Type, role, path + "." + field.Name, visited, - ref visitedCount, depth + 1, out violation)) + ref visitedCount, depth + 1, out violation)) { return true; } @@ -658,9 +730,9 @@ private static bool TryFindUserDefinedDtoViolation(INamedTypeSymbol type, Client break; case IPropertySymbol { IsStatic: false } property: if (TryFindClientBoundaryViolation(property.Type, role, path + "." + property.Name, visited, - ref visitedCount, depth + 1, out violation) || - TryFindParameterViolation(property.Parameters, role, path + "." + property.Name, visited, - ref visitedCount, depth + 1, out violation)) + ref visitedCount, depth + 1, out violation) || + TryFindParameterViolation(property.Parameters, role, path + "." + property.Name, visited, + ref visitedCount, depth + 1, out violation)) { return true; } @@ -668,20 +740,21 @@ private static bool TryFindUserDefinedDtoViolation(INamedTypeSymbol type, Client break; case IEventSymbol { IsStatic: false } @event: if (TryFindClientBoundaryViolation(@event.Type, role, path + "." + @event.Name, visited, - ref visitedCount, depth + 1, out violation)) + ref visitedCount, depth + 1, out violation)) { return true; } break; case IMethodSymbol { IsStatic: false } method when !method.IsImplicitlyDeclared && - method.DeclaredAccessibility != Accessibility.Private: + method.DeclaredAccessibility != + Accessibility.Private: violation = string.Empty; if (method.ReturnsByRef || method.ReturnsByRefReadonly || - TryFindClientBoundaryViolation(method.ReturnType, role, path + "." + method.Name, visited, - ref visitedCount, depth + 1, out violation) || - TryFindParameterViolation(method.Parameters, role, path + "." + method.Name, visited, - ref visitedCount, depth + 1, out violation)) + TryFindClientBoundaryViolation(method.ReturnType, role, path + "." + method.Name, visited, + ref visitedCount, depth + 1, out violation) || + TryFindParameterViolation(method.Parameters, role, path + "." + method.Name, visited, + ref visitedCount, depth + 1, out violation)) { violation = string.IsNullOrEmpty(violation) ? path + "." + method.Name + " exposes a by-reference return." @@ -709,7 +782,7 @@ private static bool TryFindParameterViolation(ImmutableArray p } if (TryFindClientBoundaryViolation(parameter.Type, role, path + " parameter '" + parameter.Name + "'", - visited, ref visitedCount, depth + 1, out violation)) + visited, ref visitedCount, depth + 1, out violation)) { return true; } @@ -727,6 +800,18 @@ private static bool IsFrameworkOrApprovedSdkValue(INamedTypeSymbol type) return true; } + return IsApprovedFrameworkClientBoundaryType(type); + } + + private static bool IsApprovedFrameworkClientBoundaryType(INamedTypeSymbol type) + { + string metadataName = type.OriginalDefinition.ToDisplayString(); + return IsScalar(type) || type.IsTupleType || ApprovedFrameworkValueTypes.Contains(metadataName) || + ApprovedFrameworkGenericCollectionTypes.Contains(metadataName); + } + + private static bool IsFrameworkType(INamedTypeSymbol type) + { string? assemblyName = type.ContainingAssembly?.Name; return assemblyName is not null && !assemblyName.StartsWith(CheatEngineSdkAssemblyPrefix, StringComparison.Ordinal) && @@ -734,6 +819,13 @@ private static bool IsFrameworkOrApprovedSdkValue(INamedTypeSymbol type) assemblyName.StartsWith("Microsoft", StringComparison.Ordinal)); } + private static bool IsSafeFrameworkDtoImplementationContract(INamedTypeSymbol type) + { + return type.OriginalDefinition.ToDisplayString() is "System.IAsyncDisposable" or "System.IComparable" or + "System.IComparable" or "System.IConvertible" or "System.IDisposable" or "System.IEquatable" or + "System.IFormattable" or "System.ISpanFormattable" or "System.IUtf8SpanFormattable"; + } + private static bool IsCheatEngineSdkType(INamedTypeSymbol type) { return type.ContainingAssembly?.Name.StartsWith(CheatEngineSdkAssemblyPrefix, StringComparison.Ordinal) == true; @@ -746,12 +838,6 @@ private static bool ImplementsSdkObjectContract(INamedTypeSymbol type) StringComparison.Ordinal)); } - private enum ClientBoundaryRole - { - MapperSource, - ClientResult - } - private static string TypeDeclaration(INamedTypeSymbol type, bool isStatic) { string accessibility = type.DeclaredAccessibility == Accessibility.Public ? "public" : "internal"; @@ -772,6 +858,12 @@ private static string EscapeIdentifier(string name) : name; } + private enum ClientBoundaryRole + { + MapperSource, + ClientResult + } + private sealed class ModuleCandidate { private ModuleCandidate(Diagnostic? diagnostic, string hintName, string @namespace, string typeDeclaration, @@ -838,9 +930,9 @@ public static ModuleCandidate Create(GeneratorAttributeSyntaxContext context) { INamedTypeSymbol? module = context.TargetSymbol as INamedTypeSymbol; Location location = context.TargetNode.GetLocation(); - if (module is null || module.TypeKind != TypeKind.Class || module.IsStatic || - module.ContainingType is not null || - module.TypeParameters.Length != 0 || !IsPartial(module)) + if (module is null || module.TypeKind != TypeKind.Class || module.IsStatic || module.IsAbstract || + module.IsFileLocal || module.ContainingType is not null || + module.OriginalDefinition.TypeParameters.Length != 0 || !IsPartial(module)) { return Invalid(ModuleDiagnosticDescriptors.InvalidModuleShape, location); } @@ -859,7 +951,8 @@ nonPublicConstructor is null AttributeData attribute = context.Attributes[0]; if (attribute.ConstructorArguments.Length == 0 || attribute.ConstructorArguments[0].Value is not INamedTypeSymbol bindings || !bindings.IsStatic || - bindings.TypeKind != TypeKind.Class) + bindings.TypeKind != TypeKind.Class || bindings.IsFileLocal || + bindings.OriginalDefinition.TypeParameters.Length != 0) { return Invalid(ModuleDiagnosticDescriptors.InvalidBindingsType, location); } @@ -1033,7 +1126,8 @@ public static OperationCandidate Create(GeneratorAttributeSyntaxContext context) method.MethodKind != MethodKind.Ordinary || !method.IsStatic || method.IsGenericMethod || !method.IsPartialDefinition || method.PartialImplementationPart is not null || method.ReturnsByRef || method.ReturnsByRefReadonly || - method.ContainingType.ContainingType is not null || method.ContainingType.TypeParameters.Length != 0 || + method.ContainingType.ContainingType is not null || method.ContainingType.IsFileLocal || + method.ContainingType.OriginalDefinition.TypeParameters.Length != 0 || !method.ContainingType.IsStatic || !IsPartial(method.ContainingType)) { return Invalid(OperationDiagnosticDescriptors.InvalidOperationShape, location); @@ -1099,13 +1193,13 @@ method.PartialImplementationPart is not null || method.ReturnsByRef || method.Re return Invalid(OperationDiagnosticDescriptors.InvalidMapper, location, mapper.Name, method.Name); } else if (TryFindClientBoundaryViolation(sourceResult, ClientBoundaryRole.MapperSource, - out string sourceViolation)) + out string sourceViolation)) { return Invalid(OperationDiagnosticDescriptors.UnsafeMappedType, location, method.Name, sourceViolation); } else if (TryFindClientBoundaryViolation(result, ClientBoundaryRole.ClientResult, - out string resultViolation)) + out string resultViolation)) { return Invalid(OperationDiagnosticDescriptors.UnsafeMappedType, location, method.Name, resultViolation); @@ -1173,12 +1267,12 @@ private static class ModuleDiagnosticDescriptors { public static readonly DiagnosticDescriptor InvalidModuleShape = new( "CECLUA1001", "Lua module must be a non-static partial class", - "[CheatEngineLuaModule] requires a top-level, non-static, non-generic partial class", + "[CheatEngineLuaModule] requires a top-level, concrete, non-static, non-generic, non-file-local partial class", "CheatEngine.Client.Lua", DiagnosticSeverity.Error, true); public static readonly DiagnosticDescriptor InvalidBindingsType = new( "CECLUA1002", "Lua module requires a static SDK bindings type", - "The bindings type for [CheatEngineLuaModule] must be a static class that owns SDK-generated registration methods", + "The bindings type for [CheatEngineLuaModule] must be a non-generic, non-file-local static class that owns SDK-generated registration methods", "CheatEngine.Client.Lua", DiagnosticSeverity.Error, true); public static readonly DiagnosticDescriptor NoExports = new( @@ -1206,7 +1300,7 @@ private static class OperationDiagnosticDescriptors { public static readonly DiagnosticDescriptor InvalidOperationShape = new( "CECLUA1101", "Lua operation requires a supported SDK global declaration", - "[CheatEngineLuaOperation] requires a static partial [LuaGlobal] method in a top-level static partial class", + "[CheatEngineLuaOperation] requires a static partial [LuaGlobal] method in a top-level, non-generic, non-file-local static partial class", "CheatEngine.Client.Lua", DiagnosticSeverity.Error, true); public static readonly DiagnosticDescriptor OverloadedOperation = new( diff --git a/templates/CheatEngine.Client.Templates/README.md b/templates/CheatEngine.Client.Templates/README.md index 3cdac49..5f3892f 100644 --- a/templates/CheatEngine.Client.Templates/README.md +++ b/templates/CheatEngine.Client.Templates/README.md @@ -47,13 +47,14 @@ managed deployment requirements. Keep both direct package references when adapti ## Validate the template from this repository -Pack the repository first so the smoke test can restore the Client packages from `artifacts/packages`, then run the -dedicated validation script: +Pack the repository first so the C# consumer smoke test can restore the Client packages from `artifacts/packages`: ```powershell -dotnet pack CheatEngine.Client.slnx --configuration Release -.\eng\Invoke-TemplateSmoke.ps1 -PackageSource .\artifacts\packages +dotnet pack CheatEngine.Client.slnx --configuration Release --output .\artifacts\packages +$env:CHEATENGINE_CLIENT_PACKAGE_SOURCE = (Resolve-Path .\artifacts\packages).Path +dotnet test --project .\tests\CheatEngine.Client.Tests\CheatEngine.Client.Tests.csproj --configuration Release --no-build --no-restore --fail-skips on ``` -The smoke test installs the locally packed template, runs `dotnet new ceplugin --dry-run`, instantiates it into a +The package smoke test installs the locally packed template, runs `dotnet new ceplugin --dry-run`, instantiates it into +a temporary directory, restores it against the local package source, and builds it in Release configuration. diff --git a/tests/CheatEngine.Client.Abstractions.Tests/Memory/MemoryResourceLimitsAndBatchOutcomeTests.cs b/tests/CheatEngine.Client.Abstractions.Tests/Memory/MemoryResourceLimitsAndBatchOutcomeTests.cs new file mode 100644 index 0000000..db06467 --- /dev/null +++ b/tests/CheatEngine.Client.Abstractions.Tests/Memory/MemoryResourceLimitsAndBatchOutcomeTests.cs @@ -0,0 +1,87 @@ +using CheatEngine.Client.Memory; +using CheatEngine.Client.Results; + +namespace CheatEngine.Client.Abstractions.Tests.Memory; + +public sealed class MemoryResourceLimitsAndBatchOutcomeTests +{ + [Fact] + public void DefaultLimitsPreserveTheDocumentedSafeBudgetsAndBatchHardCap() + { + MemoryResourceLimits limits = new(); + + Assert.Equal(MemoryResourceLimits.DefaultMaximumReadBytes, limits.MaximumReadBytes); + Assert.Equal(MemoryResourceLimits.DefaultMaximumWriteBytes, limits.MaximumWriteBytes); + Assert.Equal(MemoryResourceLimits.DefaultMaximumStringBytes, limits.MaximumStringBytes); + Assert.Equal(MemoryResourceLimits.DefaultMaximumBatchPayloadBytes, limits.MaximumBatchPayloadBytes); + Assert.Equal(MemoryBatchLimits.MaximumOperations, limits.MaximumBatchOperationCount); + } + + [Theory] + [InlineData(0, 1, 1, 1, 1)] + [InlineData(1, 0, 1, 1, 1)] + [InlineData(1, 1, 0, 1, 1)] + [InlineData(1, 1, 1, 0, 1)] + [InlineData(1, 1, 1, 1, 0)] + [InlineData(1, 1, 1, 1, MemoryBatchLimits.MaximumOperations + 1)] + public void ExplicitLimitsRejectEveryInvalidBudget(int readBytes, int writeBytes, int stringBytes, + int batchPayloadBytes, int batchOperationCount) + { + Assert.Throws(() => new MemoryResourceLimits(readBytes, writeBytes, stringBytes, + batchPayloadBytes, batchOperationCount)); + } + + [Fact] + public void SnapshotIsIndependentAndValidatesConfigurationBoundProperties() + { + MemoryResourceLimits configured = new(33, 34, 35, 36, 37); + MemoryResourceLimits snapshot = configured.CreateSnapshot(); + configured.MaximumReadBytes = 1; + configured.MaximumWriteBytes = 2; + configured.MaximumStringBytes = 3; + configured.MaximumBatchPayloadBytes = 4; + configured.MaximumBatchOperationCount = 5; + + Assert.Equal(33, snapshot.MaximumReadBytes); + Assert.Equal(34, snapshot.MaximumWriteBytes); + Assert.Equal(35, snapshot.MaximumStringBytes); + Assert.Equal(36, snapshot.MaximumBatchPayloadBytes); + Assert.Equal(37, snapshot.MaximumBatchOperationCount); + Assert.Throws(() => new MemoryResourceLimits + { + MaximumBatchOperationCount = MemoryBatchLimits.MaximumOperations + 1 + }.CreateSnapshot()); + } + + [Fact] + public void ReadOutcomeCopiesItsCompletedPrefixAndExposesItsFailureDetails() + { + int[] prefix = [11, 22]; + CheatEngineFailure cause = new(CheatEngineFailureKind.MemoryReadFailed, "Memory.ReadPrimitiveBatch", "Denied."); + + MemoryPrimitiveBatchReadOutcome outcome = new(3, 2, 2, cause, prefix); + prefix[0] = 99; + + Assert.Equal(3, outcome.AttemptedCount); + Assert.Equal(2, outcome.CompletedCount); + Assert.Equal(2, outcome.FailedIndex); + Assert.Equal(cause, outcome.Cause); + Assert.Equal([11, 22], outcome.ReadPrefix); + Assert.False(outcome.Succeeded); + } + + [Fact] + public void WriteOutcomeDistinguishesPartialAndUnknownEffects() + { + CheatEngineFailure cause = new(CheatEngineFailureKind.MemoryWriteFailed, "Memory.WritePrimitiveBatch", + "Denied."); + MemoryPrimitiveBatchWriteOutcome partial = new(3, 2, 2, cause, MemoryBatchWriteEffectState.Partial); + MemoryPrimitiveBatchWriteOutcome unknown = new(3, 0, null, cause, MemoryBatchWriteEffectState.Unknown); + + Assert.Equal(MemoryBatchWriteEffectState.Partial, partial.EffectState); + Assert.Equal(2, partial.CompletedCount); + Assert.Equal(MemoryBatchWriteEffectState.Unknown, unknown.EffectState); + Assert.Null(unknown.FailedIndex); + Assert.False(partial.Succeeded); + } +} diff --git a/tests/CheatEngine.Client.Abstractions.Tests/Runtime/ClientCapabilityEvidenceTests.cs b/tests/CheatEngine.Client.Abstractions.Tests/Runtime/ClientCapabilityEvidenceTests.cs index 5700d2e..dd59c91 100644 --- a/tests/CheatEngine.Client.Abstractions.Tests/Runtime/ClientCapabilityEvidenceTests.cs +++ b/tests/CheatEngine.Client.Abstractions.Tests/Runtime/ClientCapabilityEvidenceTests.cs @@ -4,6 +4,29 @@ namespace CheatEngine.Client.Abstractions.Tests.Runtime; public sealed class ClientCapabilityEvidenceTests { + public static IEnumerable EffectiveReasonPriorityCases + { + get + { + foreach (ClientCapabilityEvidenceState state in new[] + { + ClientCapabilityEvidenceState.Missing, ClientCapabilityEvidenceState.Faulted, + ClientCapabilityEvidenceState.Malformed, ClientCapabilityEvidenceState.Unknown + }) + { + ClientCapabilityAvailabilityState expectedAvailabilityState = + state == ClientCapabilityEvidenceState.Missing + ? ClientCapabilityAvailabilityState.Unavailable + : ClientCapabilityAvailabilityState.Unknown; + + foreach (ClientCapabilityEvidenceReasonCode expectedReasonCode in GetPriority(state)) + { + yield return [state, expectedReasonCode, expectedAvailabilityState]; + } + } + } + } + [Theory] [MemberData(nameof(EffectiveReasonPriorityCases))] public void EffectiveReasonCodePreservesEveryGatePriority( @@ -108,31 +131,6 @@ public void EffectiveReasonCodesUseStableUnderlyingValues() Assert.Equal((byte) 5, (byte) ClientCapabilityEvidenceReasonCode.Lifetime); } - public static IEnumerable EffectiveReasonPriorityCases - { - get - { - foreach (ClientCapabilityEvidenceState state in new[] - { - ClientCapabilityEvidenceState.Missing, - ClientCapabilityEvidenceState.Faulted, - ClientCapabilityEvidenceState.Malformed, - ClientCapabilityEvidenceState.Unknown - }) - { - ClientCapabilityAvailabilityState expectedAvailabilityState = - state == ClientCapabilityEvidenceState.Missing - ? ClientCapabilityAvailabilityState.Unavailable - : ClientCapabilityAvailabilityState.Unknown; - - foreach (ClientCapabilityEvidenceReasonCode expectedReasonCode in GetPriority(state)) - { - yield return [state, expectedReasonCode, expectedAvailabilityState]; - } - } - } - } - private static ClientCapabilityEvidence CreateEvidenceForPriority( ClientCapabilityEvidenceState state, ClientCapabilityEvidenceReasonCode expectedReasonCode) @@ -156,7 +154,7 @@ private static ClientCapabilityEvidenceGate CreateGate( int expectedPriorityIndex) { ClientCapabilityEvidenceState gateState = state == ClientCapabilityEvidenceState.Satisfied || - Array.IndexOf(priority, code) < expectedPriorityIndex + Array.IndexOf(priority, code) < expectedPriorityIndex ? ClientCapabilityEvidenceState.Satisfied : state; return new ClientCapabilityEvidenceGate(gateState, ReasonFor(code)); @@ -177,14 +175,14 @@ private static ClientCapabilityEvidenceReasonCode[] GetPriority(ClientCapability ], ClientCapabilityEvidenceState.Faulted or ClientCapabilityEvidenceState.Malformed or ClientCapabilityEvidenceState.Unknown => - [ - ClientCapabilityEvidenceReasonCode.Host, - ClientCapabilityEvidenceReasonCode.Package, - ClientCapabilityEvidenceReasonCode.LiveQualification, - ClientCapabilityEvidenceReasonCode.Implementation, - ClientCapabilityEvidenceReasonCode.Policy, - ClientCapabilityEvidenceReasonCode.Lifetime - ], + [ + ClientCapabilityEvidenceReasonCode.Host, + ClientCapabilityEvidenceReasonCode.Package, + ClientCapabilityEvidenceReasonCode.LiveQualification, + ClientCapabilityEvidenceReasonCode.Implementation, + ClientCapabilityEvidenceReasonCode.Policy, + ClientCapabilityEvidenceReasonCode.Lifetime + ], ClientCapabilityEvidenceState.Satisfied => [ ClientCapabilityEvidenceReasonCode.Lifetime, diff --git a/tests/CheatEngine.Client.AotProbe/Program.cs b/tests/CheatEngine.Client.AotProbe/Program.cs index 512b5c9..9a2b350 100644 --- a/tests/CheatEngine.Client.AotProbe/Program.cs +++ b/tests/CheatEngine.Client.AotProbe/Program.cs @@ -75,10 +75,10 @@ satisfiedGate, satisfiedGate, satisfiedGate, satisfiedGate, satisfiedGate, satisfiedGate); ClientCapabilityAvailability capabilityAvailability = new(ClientCapabilityId.ProcessSelection, evidence); if (evidence.EffectiveReasonCode != ClientCapabilityEvidenceReasonCode.Lifetime || - evidence.EffectiveReason != evidenceReason || - capabilityAvailability.State != ClientCapabilityAvailabilityState.Available || - !capabilityAvailability.IsAvailable || !capabilityAvailability.IsKnown || - capabilityAvailability.Reason != evidenceReason) + evidence.EffectiveReason != evidenceReason || + capabilityAvailability.State != ClientCapabilityAvailabilityState.Available || + !capabilityAvailability.IsAvailable || !capabilityAvailability.IsKnown || + capabilityAvailability.Reason != evidenceReason) { return 1; } diff --git a/tests/CheatEngine.Client.Benchmarks/Program.cs b/tests/CheatEngine.Client.Benchmarks/Program.cs index 7facd17..388f80d 100644 --- a/tests/CheatEngine.Client.Benchmarks/Program.cs +++ b/tests/CheatEngine.Client.Benchmarks/Program.cs @@ -9,121 +9,121 @@ namespace CheatEngine.Client.Benchmarks { -internal static class BenchmarkEntryPoint -{ - public static int Run(string[] args) + internal static class BenchmarkEntryPoint { - string artifactsDirectory = BenchmarkArtifactsDirectory.Resolve(args); - IConfig config = DefaultConfig.Instance - .WithArtifactsPath(artifactsDirectory) - .AddExporter(JsonExporter.Full); + public static int Run(string[] args) + { + string artifactsDirectory = BenchmarkArtifactsDirectory.Resolve(args); + IConfig config = DefaultConfig.Instance + .WithArtifactsPath(artifactsDirectory) + .AddExporter(JsonExporter.Full); - Summary[] summaries = BenchmarkSwitcher - .FromAssembly(typeof(BenchmarkEntryPoint).Assembly) - .Run(args, config) - .ToArray(); + Summary[] summaries = BenchmarkSwitcher + .FromAssembly(typeof(BenchmarkEntryPoint).Assembly) + .Run(args, config) + .ToArray(); - if (summaries.Length == 0) - { - return BenchmarkInvocation.IsInformational(args) ? 0 : 1; - } + if (summaries.Length == 0) + { + return BenchmarkInvocation.IsInformational(args) ? 0 : 1; + } - if (summaries.HasError()) - { - return 1; - } + if (summaries.HasError()) + { + return 1; + } - BenchmarkSuiteMetadata.WriteTo(artifactsDirectory); - return 0; + BenchmarkSuiteMetadata.WriteTo(artifactsDirectory); + return 0; + } } -} -internal static class BenchmarkArtifactsDirectory -{ - private const string DefaultDirectoryName = "BenchmarkDotNet.Artifacts"; - - public static string Resolve(IReadOnlyList args) + internal static class BenchmarkArtifactsDirectory { - ArgumentNullException.ThrowIfNull(args); + private const string DefaultDirectoryName = "BenchmarkDotNet.Artifacts"; - string artifactsPath = Path.Combine(Environment.CurrentDirectory, DefaultDirectoryName); - for (int index = 0; index < args.Count; index++) + public static string Resolve(IReadOnlyList args) { - if (TryReadArtifactsPath(args, ref index, out string? specifiedPath)) + ArgumentNullException.ThrowIfNull(args); + + string artifactsPath = Path.Combine(Environment.CurrentDirectory, DefaultDirectoryName); + for (int index = 0; index < args.Count; index++) { - artifactsPath = specifiedPath!; + if (TryReadArtifactsPath(args, ref index, out string? specifiedPath)) + { + artifactsPath = specifiedPath!; + } } - } - return new DirectoryInfo(artifactsPath).FullName; - } + return new DirectoryInfo(artifactsPath).FullName; + } - private static bool TryReadArtifactsPath( - IReadOnlyList args, - ref int index, - out string? artifactsPath) - { - string argument = args[index]!; - if (argument.Equals("-a", StringComparison.OrdinalIgnoreCase) || - argument.Equals("--artifacts", StringComparison.OrdinalIgnoreCase)) + private static bool TryReadArtifactsPath( + IReadOnlyList args, + ref int index, + out string? artifactsPath) { - artifactsPath = index + 1 < args.Count ? args[++index] : null; - return artifactsPath is not null; - } + string argument = args[index]!; + if (argument.Equals("-a", StringComparison.OrdinalIgnoreCase) || + argument.Equals("--artifacts", StringComparison.OrdinalIgnoreCase)) + { + artifactsPath = index + 1 < args.Count ? args[++index] : null; + return artifactsPath is not null; + } - return TryReadAssignedArtifactsPath(argument, "-a=", out artifactsPath) || - TryReadAssignedArtifactsPath(argument, "--artifacts=", out artifactsPath); - } + return TryReadAssignedArtifactsPath(argument, "-a=", out artifactsPath) || + TryReadAssignedArtifactsPath(argument, "--artifacts=", out artifactsPath); + } - private static bool TryReadAssignedArtifactsPath( - string argument, - string prefix, - out string? artifactsPath) - { - if (argument.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + private static bool TryReadAssignedArtifactsPath( + string argument, + string prefix, + out string? artifactsPath) { - artifactsPath = argument[prefix.Length..]; - return true; - } + if (argument.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + artifactsPath = argument[prefix.Length..]; + return true; + } - artifactsPath = null; - return false; + artifactsPath = null; + return false; + } } -} -internal static class BenchmarkInvocation -{ - public static bool IsInformational(IReadOnlyList args) + internal static class BenchmarkInvocation { - ArgumentNullException.ThrowIfNull(args); - - return args.Any(static argument => - argument.Equals("--help", StringComparison.OrdinalIgnoreCase) || - argument.Equals("-?", StringComparison.Ordinal) || - argument.Equals("--version", StringComparison.OrdinalIgnoreCase) || - argument.Equals("--info", StringComparison.OrdinalIgnoreCase) || - argument.Equals("--list", StringComparison.OrdinalIgnoreCase) || - argument.StartsWith("--list=", StringComparison.OrdinalIgnoreCase)); + public static bool IsInformational(IReadOnlyList args) + { + ArgumentNullException.ThrowIfNull(args); + + return args.Any(static argument => + argument.Equals("--help", StringComparison.OrdinalIgnoreCase) || + argument.Equals("-?", StringComparison.Ordinal) || + argument.Equals("--version", StringComparison.OrdinalIgnoreCase) || + argument.Equals("--info", StringComparison.OrdinalIgnoreCase) || + argument.Equals("--list", StringComparison.OrdinalIgnoreCase) || + argument.StartsWith("--list=", StringComparison.OrdinalIgnoreCase)); + } } -} -internal static class BenchmarkSummaryExtensions -{ - /// - /// Determines whether BenchmarkDotNet 0.15.8 reported a critical validation failure or an unsuccessful benchmark. - /// - /// - /// Version 0.15.8 does not expose the HasError() helper suggested by the review. Its public result - /// surface exposes these two signals instead: and - /// . - /// - public static bool HasError(this IEnumerable summaries) + internal static class BenchmarkSummaryExtensions { - ArgumentNullException.ThrowIfNull(summaries); + /// + /// Determines whether BenchmarkDotNet 0.15.8 reported a critical validation failure or an unsuccessful benchmark. + /// + /// + /// Version 0.15.8 does not expose the HasError() helper suggested by the review. Its public result + /// surface exposes these two signals instead: and + /// . + /// + public static bool HasError(this IEnumerable summaries) + { + ArgumentNullException.ThrowIfNull(summaries); - return summaries.Any(static summary => - summary.HasCriticalValidationErrors || - summary.Reports.Any(static report => !report.Success)); + return summaries.Any(static summary => + summary.HasCriticalValidationErrors || + summary.Reports.Any(static report => !report.Success)); + } } } -} diff --git a/tests/CheatEngine.Client.Core.Tests/Dispatching/SdkMainThreadDispatcherBehaviorTests.cs b/tests/CheatEngine.Client.Core.Tests/Dispatching/SdkMainThreadDispatcherBehaviorTests.cs index afc8420..68fecff 100644 --- a/tests/CheatEngine.Client.Core.Tests/Dispatching/SdkMainThreadDispatcherBehaviorTests.cs +++ b/tests/CheatEngine.Client.Core.Tests/Dispatching/SdkMainThreadDispatcherBehaviorTests.cs @@ -7,8 +7,18 @@ namespace CheatEngine.Client.Core.Tests.Dispatching; public sealed class SdkMainThreadDispatcherBehaviorTests { - [Fact] - public void TryInvokeExecutesActionAndGenericCallbacksThroughTheInjectedMainThreadInvoker() + public enum DispatchForm + { + Action, + Function, + StatefulFunction + } + + [Theory] + [InlineData(DispatchForm.Action)] + [InlineData(DispatchForm.Function)] + [InlineData(DispatchForm.StatefulFunction)] + public void TryInvokeContractExecutesCallbacksAndReturnsTheirSuccessfulResults(DispatchForm form) { using ControlledCoreLifetimeContext context = new(); using CoreLifetime lifetime = new(context); @@ -16,78 +26,127 @@ public void TryInvokeExecutesActionAndGenericCallbacksThroughTheInjectedMainThre SdkMainThreadDispatcher dispatcher = new(lifetime, invoker); bool callbackRan = false; - bool actionSucceeded = dispatcher.TryInvoke(() => callbackRan = true, out CheatEngineFailure actionFailure, + DispatchInvocation invocation = TryInvoke(form, dispatcher, () => callbackRan = true, TestContext.Current.CancellationToken); - bool functionSucceeded = dispatcher.TryInvoke(static () => 42, out int result, - out CheatEngineFailure functionFailure, TestContext.Current.CancellationToken); - Assert.True(actionSucceeded); + Assert.True(invocation.Succeeded); Assert.True(callbackRan); - Assert.Equal(default, actionFailure); - Assert.True(functionSucceeded); - Assert.Equal(42, result); - Assert.Equal(default, functionFailure); - Assert.Equal(1, invoker.ActionCalls); - Assert.Equal(1, invoker.FunctionCalls); + Assert.Equal(default, invocation.Failure); + Assert.Equal(form == DispatchForm.Action ? null : 42, invocation.Result); + AssertInvokedOnlyThrough(form, invoker); } - [Fact] - public void InternalStatefulFastPathForwardsValueStateWithoutUsingThePublicClosureFallback() + [Theory] + [InlineData(DispatchForm.Action)] + [InlineData(DispatchForm.Function)] + [InlineData(DispatchForm.StatefulFunction)] + public void TryInvokeContractRethrowsTheSameCallbackExceptionInstance(DispatchForm form) { using ControlledCoreLifetimeContext context = new(); using CoreLifetime lifetime = new(context); RecordingMainThreadInvoker invoker = new(); -#pragma warning disable CA1859 // This test intentionally dispatches through the internal interface contract. - IStatefulCheatEngineDispatcher dispatcher = new SdkMainThreadDispatcher(lifetime, invoker); -#pragma warning restore CA1859 + SdkMainThreadDispatcher dispatcher = new(lifetime, invoker); + InvalidOperationException expected = new("callback failure"); - bool succeeded = dispatcher.TryInvoke(21, static value => value * 2, out int result, - out CheatEngineFailure failure, TestContext.Current.CancellationToken); + InvalidOperationException actual = Assert.Throws(() => + TryInvoke(form, dispatcher, () => throw expected, TestContext.Current.CancellationToken)); - Assert.True(succeeded); - Assert.Equal(42, result); - Assert.Equal(default, failure); - Assert.Equal(1, invoker.StateFunctionCalls); - Assert.Equal(0, invoker.FunctionCalls); + Assert.Same(expected, actual); + AssertInvokedOnlyThrough(form, invoker); } - [Fact] - public void CallbackExceptionsAreRethrownWithoutBeingClassifiedAsHostFailures() + [Theory] + [InlineData(DispatchForm.Action)] + [InlineData(DispatchForm.Function)] + [InlineData(DispatchForm.StatefulFunction)] + public void TryInvokeContractMapsInfrastructureFailuresToStructuredClientFailures(DispatchForm form) { using ControlledCoreLifetimeContext context = new(); using CoreLifetime lifetime = new(context); - SdkMainThreadDispatcher dispatcher = new(lifetime, new RecordingMainThreadInvoker()); - InvalidOperationException expectedActionException = new("action callback"); - InvalidOperationException expectedFunctionException = new("function callback"); - - InvalidOperationException actionException = Assert.Throws(() => - dispatcher.TryInvoke(() => throw expectedActionException, - out CheatEngineFailure _, TestContext.Current.CancellationToken)); - InvalidOperationException functionException = Assert.Throws(() => - dispatcher.TryInvoke(() => throw expectedFunctionException, out _, - out CheatEngineFailure _, TestContext.Current.CancellationToken)); - - Assert.Same(expectedActionException, actionException); - Assert.Same(expectedFunctionException, functionException); + InvalidOperationException expected = new("host queue unavailable"); + RecordingMainThreadInvoker invoker = new() { HostException = expected }; + SdkMainThreadDispatcher dispatcher = new(lifetime, invoker); + bool callbackRan = false; + + DispatchInvocation invocation = TryInvoke(form, dispatcher, () => callbackRan = true, + TestContext.Current.CancellationToken); + + Assert.False(invocation.Succeeded); + Assert.Equal(DefaultResultForFailedInvocation(form), invocation.Result); + Assert.Equal(CheatEngineFailureKind.OperationRejected, invocation.Failure.Kind); + Assert.Equal("Dispatcher.Invoke", invocation.Failure.Operation); + Assert.Equal("host queue unavailable", invocation.Failure.Message); + Assert.Same(expected, invocation.Failure.Exception); + Assert.False(callbackRan); + AssertInvokedOnlyThrough(form, invoker); } - [Fact] - public void TryInvokeMapsMainThreadInfrastructureFailuresToStableClientFailures() + [Theory] + [InlineData(DispatchForm.Action)] + [InlineData(DispatchForm.Function)] + [InlineData(DispatchForm.StatefulFunction)] + public void TryInvokeContractRejectsCancelledWorkBeforeDispatchAdmission(DispatchForm form) { using ControlledCoreLifetimeContext context = new(); using CoreLifetime lifetime = new(context); - SdkMainThreadDispatcher dispatcher = new(lifetime, - new RecordingMainThreadInvoker { HostException = new InvalidOperationException("host queue unavailable") }); + RecordingMainThreadInvoker invoker = new(); + SdkMainThreadDispatcher dispatcher = new(lifetime, invoker); + using CancellationTokenSource cancellation = new(); + cancellation.Cancel(); + bool callbackRan = false; + + DispatchInvocation invocation = TryInvoke(form, dispatcher, () => callbackRan = true, cancellation.Token); + + Assert.False(invocation.Succeeded); + Assert.Equal(DefaultResultForFailedInvocation(form), invocation.Result); + Assert.Equal(CheatEngineFailureKind.Cancelled, invocation.Failure.Kind); + Assert.Equal("Dispatcher.Invoke", invocation.Failure.Operation); + Assert.False(callbackRan); + Assert.Equal(0, invoker.InvocationCount); + } + + [Theory] + [InlineData(DispatchForm.Action)] + [InlineData(DispatchForm.Function)] + [InlineData(DispatchForm.StatefulFunction)] + public void TryInvokeContractRejectsAnExpiredActivationBeforeDispatchAdmission(DispatchForm form) + { + using ControlledCoreLifetimeContext context = new() { IsCurrent = false }; + using CoreLifetime lifetime = new(context); + RecordingMainThreadInvoker invoker = new(); + SdkMainThreadDispatcher dispatcher = new(lifetime, invoker); + bool callbackRan = false; + + CheatEngineActivationExpiredException exception = Assert.Throws(() => + TryInvoke(form, dispatcher, () => callbackRan = true, TestContext.Current.CancellationToken)); + + Assert.Equal(CheatEngineFailureKind.ActivationExpired, exception.Failure.Kind); + Assert.Equal("Dispatcher.Invoke", exception.Failure.Operation); + Assert.False(callbackRan); + Assert.Equal(0, invoker.InvocationCount); + } + + [Theory] + [InlineData(DispatchForm.Action)] + [InlineData(DispatchForm.Function)] + [InlineData(DispatchForm.StatefulFunction)] + public void TryInvokeContractPrioritizesAnExpiredActivationOverPreAdmissionCancellation(DispatchForm form) + { + using ControlledCoreLifetimeContext context = new() { IsCurrent = false }; + using CoreLifetime lifetime = new(context); + RecordingMainThreadInvoker invoker = new(); + SdkMainThreadDispatcher dispatcher = new(lifetime, invoker); + using CancellationTokenSource cancellation = new(); + cancellation.Cancel(); - bool succeeded = dispatcher.TryInvoke(static () => + CheatEngineActivationExpiredException exception = Assert.Throws(() => + TryInvoke(form, dispatcher, static () => { - }, out CheatEngineFailure failure, - TestContext.Current.CancellationToken); + }, cancellation.Token)); - Assert.False(succeeded); - Assert.Equal(CheatEngineFailureKind.OperationRejected, failure.Kind); - Assert.Equal("Dispatcher.Invoke", failure.Operation); - Assert.Equal("host queue unavailable", failure.Message); + Assert.Equal(CheatEngineFailureKind.ActivationExpired, exception.Failure.Kind); + Assert.Equal("Dispatcher.Invoke", exception.Failure.Operation); + Assert.Equal(0, invoker.InvocationCount); } [Fact] @@ -106,8 +165,75 @@ public void ConstructorRejectsMissingLifecycleOrMainThreadInvoker() Assert.Equal("mainThread", invokerException.ParamName); } + private static DispatchInvocation TryInvoke( + DispatchForm form, + SdkMainThreadDispatcher dispatcher, + Action callback, + CancellationToken cancellationToken) + { + switch (form) + { + case DispatchForm.Action: + { + bool succeeded = dispatcher.TryInvoke(callback, out CheatEngineFailure failure, cancellationToken); + return new DispatchInvocation(succeeded, null, failure); + } + case DispatchForm.Function: + { + bool succeeded = dispatcher.TryInvoke( + () => + { + callback(); + return 42; + }, + out int result, + out CheatEngineFailure failure, + cancellationToken); + return new DispatchInvocation(succeeded, result, failure); + } + case DispatchForm.StatefulFunction: + { +#pragma warning disable CA1859 // This helper intentionally exercises the internal stateful dispatch contract. + IStatefulCheatEngineDispatcher statefulDispatcher = dispatcher; +#pragma warning restore CA1859 + CallbackState state = new(callback, 42); + bool succeeded = statefulDispatcher.TryInvoke( + state, + static current => + { + current.Callback(); + return current.Result; + }, + out int result, + out CheatEngineFailure failure, + cancellationToken); + return new DispatchInvocation(succeeded, result, failure); + } + default: + throw new ArgumentOutOfRangeException(nameof(form), form, null); + } + } + + private static void AssertInvokedOnlyThrough(DispatchForm form, RecordingMainThreadInvoker invoker) + { + Assert.Equal(form == DispatchForm.Action ? 1 : 0, invoker.ActionCalls); + Assert.Equal(form == DispatchForm.Function ? 1 : 0, invoker.FunctionCalls); + Assert.Equal(form == DispatchForm.StatefulFunction ? 1 : 0, invoker.StateFunctionCalls); + } + + private static int? DefaultResultForFailedInvocation(DispatchForm form) + { + return form == DispatchForm.Action ? null : 0; + } + + private readonly record struct CallbackState(Action Callback, int Result); + + private readonly record struct DispatchInvocation(bool Succeeded, int? Result, CheatEngineFailure Failure); + private sealed class RecordingMainThreadInvoker : IMainThreadInvoker { + internal int InvocationCount => ActionCalls + FunctionCalls + StateFunctionCalls; + internal int ActionCalls { get; diff --git a/tests/CheatEngine.Client.Core.Tests/Dispatching/SdkMainThreadDispatcherTests.cs b/tests/CheatEngine.Client.Core.Tests/Dispatching/SdkMainThreadDispatcherTests.cs index 4bdcee8..e5cee25 100644 --- a/tests/CheatEngine.Client.Core.Tests/Dispatching/SdkMainThreadDispatcherTests.cs +++ b/tests/CheatEngine.Client.Core.Tests/Dispatching/SdkMainThreadDispatcherTests.cs @@ -21,23 +21,6 @@ public void TryInvokeRejectsNullCallbacksBeforeEnteringThePluginDispatchGate() Assert.Equal("callback", functionException.ParamName); } - [Fact] - public void TryInvokeReportsCancellationBeforeCallingTheSdkDispatcher() - { - using ControlledCoreLifetimeContext context = new(); - using CoreLifetime lifetime = new(context); - SdkMainThreadDispatcher dispatcher = new(lifetime); - using CancellationTokenSource cancellation = new(); - cancellation.Cancel(); - - bool succeeded = dispatcher.TryInvoke(static () => throw new InvalidOperationException("must not run"), - out CheatEngineFailure failure, cancellation.Token); - - Assert.False(succeeded); - Assert.Equal(CheatEngineFailureKind.Cancelled, failure.Kind); - Assert.Equal("Dispatcher.Invoke", failure.Operation); - } - [Fact] public void InvokeConvertsARejectedDispatchToThePublicFailureException() { diff --git a/tests/CheatEngine.Client.Core.Tests/Domains/Events/EventStreamLeaseTests.cs b/tests/CheatEngine.Client.Core.Tests/Domains/Events/EventStreamLeaseTests.cs index a8aa61f..afc4905 100644 --- a/tests/CheatEngine.Client.Core.Tests/Domains/Events/EventStreamLeaseTests.cs +++ b/tests/CheatEngine.Client.Core.Tests/Domains/Events/EventStreamLeaseTests.cs @@ -63,6 +63,96 @@ public void DisposeIsIdempotentAndDoesNotReleaseTheHostTwice() Assert.Equal(1, released); } + [Fact(Timeout = 10_000)] + public async Task DisposeDoesNotHoldItsGateWhileExternalTeardownWaitsForReentrantCallbacks() + { + CancellationToken cancellationToken = TestContext.Current.CancellationToken; + TaskCompletionSource neutralizationStarted = new(TaskCreationOptions.RunContinuationsAsynchronously); + TaskCompletionSource neutralizationCallbackStopped = new(TaskCreationOptions.RunContinuationsAsynchronously); + TaskCompletionSource releaseStarted = new(TaskCreationOptions.RunContinuationsAsynchronously); + TaskCompletionSource releaseCallbackStopped = new(TaskCreationOptions.RunContinuationsAsynchronously); + List calls = []; + BoundedEventStream stream = new(new EventStreamOptions(1)); + EventStreamLease lease = null!; + + Task callback = Task.Run(async () => + { + await neutralizationStarted.Task.WaitAsync(cancellationToken); + lease.Dispose(); + neutralizationCallbackStopped.SetResult(); + + await releaseStarted.Task.WaitAsync(cancellationToken); + lease.Dispose(); + releaseCallbackStopped.SetResult(); + }, cancellationToken); + + lease = new EventStreamLease( + stream, + () => + { + calls.Add("neutralize"); + neutralizationStarted.SetResult(); + neutralizationCallbackStopped.Task.Wait(cancellationToken); + }, + () => + { + calls.Add("release"); + releaseStarted.SetResult(); + releaseCallbackStopped.Task.Wait(cancellationToken); + }, + _ => calls.Add("untrack")); + + await Task.Run(lease.Dispose, cancellationToken).WaitAsync(cancellationToken); + await callback.WaitAsync(cancellationToken); + + Assert.True(lease.IsReleased); + Assert.True(stream.IsCompleted); + Assert.Equal(["neutralize", "release", "untrack"], calls); + } + + [Fact(Timeout = 10_000)] + public async Task ConcurrentDisposalsRunOnlyOneCleanupSequence() + { + CancellationToken cancellationToken = TestContext.Current.CancellationToken; + using ManualResetEventSlim allowNeutralizationToFinish = new(false); + TaskCompletionSource neutralizationStarted = new(TaskCreationOptions.RunContinuationsAsynchronously); + int neutralized = 0; + int released = 0; + int untracked = 0; + BoundedEventStream stream = new(new EventStreamOptions(1)); + EventStreamLease lease = new( + stream, + () => + { + Interlocked.Increment(ref neutralized); + neutralizationStarted.SetResult(); + allowNeutralizationToFinish.Wait(); + }, + () => Interlocked.Increment(ref released), + _ => Interlocked.Increment(ref untracked)); + + Task firstDispose = Task.Run(lease.Dispose, cancellationToken); + await neutralizationStarted.Task.WaitAsync(cancellationToken); + Task secondDispose = Task.Run(lease.Dispose, cancellationToken); + + try + { + await secondDispose.WaitAsync(cancellationToken); + Assert.False(firstDispose.IsCompleted); + } + finally + { + allowNeutralizationToFinish.Set(); + } + + await firstDispose.WaitAsync(cancellationToken); + + Assert.True(lease.IsReleased); + Assert.Equal(1, neutralized); + Assert.Equal(1, released); + Assert.Equal(1, untracked); + } + [Fact] public async Task DisposeCompletesTheStreamAndReleasesTheHostEvenWhenNeutralizationFails() { diff --git a/tests/CheatEngine.Client.Core.Tests/Domains/LocalProcessDiagnosticsTests.cs b/tests/CheatEngine.Client.Core.Tests/Domains/LocalProcessDiagnosticsTests.cs index 4e2d20d..f515685 100644 --- a/tests/CheatEngine.Client.Core.Tests/Domains/LocalProcessDiagnosticsTests.cs +++ b/tests/CheatEngine.Client.Core.Tests/Domains/LocalProcessDiagnosticsTests.cs @@ -67,7 +67,10 @@ public void TryGetProcessesRejectsAnInvalidRequestBeforeReadingTheLocalCatalog() [Fact] public void TryGetProcessesMapsLocalCatalogFailuresWithoutClaimingTargetState() { - FakeLocalProcessHost host = new([]) { GetLocalProcessesException = new InvalidOperationException("fixture enumeration failed") }; + FakeLocalProcessHost host = new([]) + { + GetLocalProcessesException = new InvalidOperationException("fixture enumeration failed") + }; LocalProcessDiagnostics diagnostics = new(host); bool succeeded = diagnostics.TryGetProcesses( @@ -104,11 +107,28 @@ public void CopiedLocalDiagnosticsRemainUsableAfterAClientActivationExpires() private sealed class FakeLocalProcessHost(IReadOnlyList processes) : IProcessHost { - internal int GetLocalProcessesCalls { get; private set; } - internal Exception? GetLocalProcessesException { get; init; } + internal int GetLocalProcessesCalls + { + get; + private set; + } + + internal Exception? GetLocalProcessesException + { + get; + init; + } + + public long GetOpenedProcessId() + { + return 0; + } + + public void OpenProcess(long processId) + { + throw new NotSupportedException(); + } - public long GetOpenedProcessId() => 0; - public void OpenProcess(long processId) => throw new NotSupportedException(); public bool TryGetLocalProcess(int processId, out LocalProcessInfo process) { process = default; @@ -126,7 +146,14 @@ public IReadOnlyList GetLocalProcesses() return processes; } - public IReadOnlyList FindProcessesByExactName(string processName) => []; - public CheatEngineArchitecture GetTargetArchitecture() => CheatEngineArchitecture.Unknown; + public IReadOnlyList FindProcessesByExactName(string processName) + { + return []; + } + + public CheatEngineArchitecture GetTargetArchitecture() + { + return CheatEngineArchitecture.Unknown; + } } } diff --git a/tests/CheatEngine.Client.Core.Tests/Domains/MemoryClientDispatchFailureTests.cs b/tests/CheatEngine.Client.Core.Tests/Domains/MemoryClientDispatchFailureTests.cs index 649d540..c35133e 100644 --- a/tests/CheatEngine.Client.Core.Tests/Domains/MemoryClientDispatchFailureTests.cs +++ b/tests/CheatEngine.Client.Core.Tests/Domains/MemoryClientDispatchFailureTests.cs @@ -116,7 +116,8 @@ public void PrimitiveBatchesPreserveTheDispatcherFailureWithoutAdmittingAnyTarge [Fact] public void DefaultPrimitiveBatchesAreRejectedBeforeDispatch() { - MemoryClient client = new(new RejectingDispatcher(Failure("Test.ShouldNotDispatch")), InertCoreLifetime.Create()); + MemoryClient client = new(new RejectingDispatcher(Failure("Test.ShouldNotDispatch")), + InertCoreLifetime.Create()); Assert.Throws(() => client.TryReadPrimitiveBatch(default, out ImmutableArray _, out _, TestContext.Current.CancellationToken)); @@ -170,7 +171,8 @@ public void ReadAndWriteConvenienceMethodsThrowTheClassifiedDispatcherFailure() [InlineData("pointer")] public void InvalidDefaultRequestCannotReachTheDispatcher(string requestKind) { - MemoryClient client = new(new RejectingDispatcher(Failure("Test.ShouldNotDispatch")), InertCoreLifetime.Create()); + MemoryClient client = new(new RejectingDispatcher(Failure("Test.ShouldNotDispatch")), + InertCoreLifetime.Create()); switch (requestKind) { @@ -192,7 +194,8 @@ public void InvalidDefaultRequestCannotReachTheDispatcher(string requestKind) [Fact] public void DefaultStringAndByteWritesAreRejectedBeforeDispatch() { - MemoryClient client = new(new RejectingDispatcher(Failure("Test.ShouldNotDispatch")), InertCoreLifetime.Create()); + MemoryClient client = new(new RejectingDispatcher(Failure("Test.ShouldNotDispatch")), + InertCoreLifetime.Create()); Assert.Throws(() => client.TryWriteBytes(default, out _, TestContext.Current.CancellationToken)); diff --git a/tests/CheatEngine.Client.Core.Tests/Domains/MemoryClientResourceLimitsAndBatchOutcomeTests.cs b/tests/CheatEngine.Client.Core.Tests/Domains/MemoryClientResourceLimitsAndBatchOutcomeTests.cs new file mode 100644 index 0000000..39bfc70 --- /dev/null +++ b/tests/CheatEngine.Client.Core.Tests/Domains/MemoryClientResourceLimitsAndBatchOutcomeTests.cs @@ -0,0 +1,517 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; + +using CheatEngine.Client.Core.Domains; +using CheatEngine.Client.Core.Tests.TestSupport; +using CheatEngine.Client.Dispatching; +using CheatEngine.Client.Memory; +using CheatEngine.Client.Results; +using CheatEngine.SDK.Engine.Values; + +namespace CheatEngine.Client.Core.Tests.Domains; + +public sealed class MemoryClientResourceLimitsAndBatchOutcomeTests +{ + private static readonly Address _address = new(0x700000); + + [Fact] + public void EveryDirectBudgetRejectsWorkBeforeDispatcherAdmission() + { + CountingDispatcher dispatcher = new(); + MemoryClient byteClient = CreateClient(dispatcher, new MemoryResourceLimits(1, 1, 1, 64, 2)); + + Assert.False(byteClient.TryReadBytes(new MemoryBytesReadRequest(_address, 2), out ImmutableArray bytes, + out CheatEngineFailure readFailure, TestContext.Current.CancellationToken)); + Assert.Empty(bytes); + Assert.Equal(CheatEngineFailureKind.OperationRejected, readFailure.Kind); + Assert.False(byteClient.TryWriteBytes(new MemoryBytesWriteRequest(_address, [1, 2]), + out CheatEngineFailure writeFailure, TestContext.Current.CancellationToken)); + Assert.Equal(CheatEngineFailureKind.OperationRejected, writeFailure.Kind); + Assert.False(byteClient.TryReadString(new MemoryStringReadRequest(_address, 1, true), out string? text, + out CheatEngineFailure stringReadFailure, TestContext.Current.CancellationToken)); + Assert.Null(text); + Assert.Equal(CheatEngineFailureKind.OperationRejected, stringReadFailure.Kind); + Assert.False(byteClient.TryWriteString(new MemoryStringWriteRequest(_address, "A", true), + out CheatEngineFailure stringWriteFailure, TestContext.Current.CancellationToken)); + Assert.Equal(CheatEngineFailureKind.OperationRejected, stringWriteFailure.Kind); + Assert.Equal(0, dispatcher.InvocationCount); + + MemoryClient countClient = CreateClient(dispatcher, new MemoryResourceLimits(4, 4, 4, 64, 1)); + MemoryPrimitiveBatchReadOutcome countOutcome = countClient.ReadPrimitiveBatchDetailed( + new MemoryPrimitiveBatchReadRequest([_address, _address + 4]), TestContext.Current.CancellationToken); + Assert.Equal(MemoryBatchWriteEffectState.NotStarted, countClient.WritePrimitiveBatchDetailed( + new MemoryPrimitiveBatchWriteRequest([ + new MemoryAddressValue(_address, 1), new MemoryAddressValue(_address + 4, 2) + ]), + TestContext.Current.CancellationToken).EffectState); + Assert.Equal(0, countOutcome.CompletedCount); + Assert.Null(countOutcome.FailedIndex); + Assert.Equal(CheatEngineFailureKind.OperationRejected, countOutcome.Cause?.Kind); + Assert.Equal(0, dispatcher.InvocationCount); + + MemoryClient payloadClient = CreateClient(dispatcher, new MemoryResourceLimits(8, 8, 8, sizeof(int), 2)); + MemoryPrimitiveBatchReadOutcome payloadOutcome = payloadClient.ReadPrimitiveBatchDetailed( + new MemoryPrimitiveBatchReadRequest([_address, _address + 4]), TestContext.Current.CancellationToken); + MemoryPrimitiveBatchWriteOutcome payloadWriteOutcome = payloadClient.WritePrimitiveBatchDetailed( + new MemoryPrimitiveBatchWriteRequest([ + new MemoryAddressValue(_address, 1), new MemoryAddressValue(_address + 4, 2) + ]), + TestContext.Current.CancellationToken); + Assert.Equal(0, payloadOutcome.CompletedCount); + Assert.Equal(CheatEngineFailureKind.OperationRejected, payloadOutcome.Cause?.Kind); + Assert.Equal(MemoryBatchWriteEffectState.NotStarted, payloadWriteOutcome.EffectState); + Assert.Equal(CheatEngineFailureKind.OperationRejected, payloadWriteOutcome.Cause?.Kind); + Assert.Equal(0, dispatcher.InvocationCount); + } + + [Fact] + public void DirectByteReadAndWriteBudgetsUseTheirIndependentlyConfiguredLimits() + { + CheatEngineFailure dispatchFailure = new(CheatEngineFailureKind.InvalidState, "Test.Dispatcher", "Rejected."); + RejectingDispatcher dispatcher = new(dispatchFailure); + MemoryClient client = CreateClient(dispatcher, new MemoryResourceLimits(2, 1, 32, 32, 2)); + + Assert.False(client.TryReadBytes(new MemoryBytesReadRequest(_address, 2), out _, + out CheatEngineFailure readFailure, TestContext.Current.CancellationToken)); + Assert.False(client.TryWriteBytes(new MemoryBytesWriteRequest(_address, [1, 2]), + out CheatEngineFailure writeFailure, TestContext.Current.CancellationToken)); + + Assert.Equal(dispatchFailure, readFailure); + Assert.Equal(CheatEngineFailureKind.OperationRejected, writeFailure.Kind); + Assert.Equal("Memory.WriteBytes", writeFailure.Operation); + Assert.Equal(1, dispatcher.InvocationCount); + } + + [Fact] + public void BatchPayloadAdmissionUsesTheActualLongElementSizeBeforeDispatch() + { + CountingDispatcher dispatcher = new(); + MemoryClient client = CreateClient(dispatcher, new MemoryResourceLimits(32, 32, 32, 4, 2)); + + MemoryPrimitiveBatchWriteOutcome outcome = client.WritePrimitiveBatchDetailed( + new MemoryPrimitiveBatchWriteRequest([new MemoryAddressValue(_address, 10L)]), + TestContext.Current.CancellationToken); + + Assert.False(outcome.Succeeded); + Assert.Equal(1, outcome.AttemptedCount); + Assert.Equal(0, outcome.CompletedCount); + Assert.Equal(CheatEngineFailureKind.OperationRejected, outcome.Cause?.Kind); + Assert.Equal(MemoryBatchWriteEffectState.NotStarted, outcome.EffectState); + Assert.Equal(0, dispatcher.InvocationCount); + } + + [Fact] + public void ExactResourceBoundariesAreAdmittedToTheDispatcher() + { + CheatEngineFailure expected = new(CheatEngineFailureKind.InvalidState, "Test.Dispatcher", "Rejected."); + MemoryClient client = CreateClient(new RejectingDispatcher(expected), new MemoryResourceLimits(2, 2, 2, 8, 2)); + MemoryPrimitiveBatchReadRequest reads = new([_address, _address + 4]); + MemoryPrimitiveBatchWriteRequest writes = new([ + new MemoryAddressValue(_address, 1), new MemoryAddressValue(_address + 4, 2) + ]); + + Assert.False(client.TryReadBytes(new MemoryBytesReadRequest(_address, 2), out _, + out CheatEngineFailure byteReadFailure, + TestContext.Current.CancellationToken)); + Assert.False(client.TryWriteBytes(new MemoryBytesWriteRequest(_address, [1, 2]), + out CheatEngineFailure byteWriteFailure, + TestContext.Current.CancellationToken)); + Assert.False(client.TryReadString(new MemoryStringReadRequest(_address, 1, true), out _, + out CheatEngineFailure stringReadFailure, TestContext.Current.CancellationToken)); + Assert.False(client.TryWriteString(new MemoryStringWriteRequest(_address, "A", true), + out CheatEngineFailure stringWriteFailure, TestContext.Current.CancellationToken)); + + Assert.Equal(expected, byteReadFailure); + Assert.Equal(expected, byteWriteFailure); + Assert.Equal(expected, stringReadFailure); + Assert.Equal(expected, stringWriteFailure); + Assert.Equal(expected, client.ReadPrimitiveBatchDetailed(reads, TestContext.Current.CancellationToken).Cause); + Assert.Equal(expected, client.WritePrimitiveBatchDetailed(writes, TestContext.Current.CancellationToken).Cause); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void DetailedReadReportsEveryHostFailureIndexAndItsImmutableCompletedPrefix(int failedIndex) + { + BatchPort port = new() { ReadFailureIndex = failedIndex }; + MemoryClient client = CreateClient(new CountingDispatcher(), new MemoryResourceLimits(32, 32, 32, 32, 3), port); + MemoryPrimitiveBatchReadOutcome outcome = client.ReadPrimitiveBatchDetailed( + new MemoryPrimitiveBatchReadRequest([_address, _address + 4, _address + 8]), + TestContext.Current.CancellationToken); + + Assert.False(outcome.Succeeded); + Assert.Equal(3, outcome.AttemptedCount); + Assert.Equal(failedIndex, outcome.CompletedCount); + Assert.Equal(failedIndex, outcome.FailedIndex); + Assert.Equal(Enumerable.Range(0, failedIndex).Select(static index => 100 + index), outcome.ReadPrefix); + Assert.Equal(CheatEngineFailureKind.MemoryReadFailed, outcome.Cause?.Kind); + Assert.Equal(failedIndex + 1, port.ReadInvocationCount); + } + + [Theory] + [InlineData(0, MemoryBatchWriteEffectState.NotStarted)] + [InlineData(1, MemoryBatchWriteEffectState.Partial)] + [InlineData(2, MemoryBatchWriteEffectState.Partial)] + public void DetailedWriteReportsEveryHostFailureIndexWithoutRollback(int failedIndex, + MemoryBatchWriteEffectState expectedEffectState) + { + BatchPort port = new() { WriteFailureIndex = failedIndex }; + MemoryClient client = CreateClient(new CountingDispatcher(), new MemoryResourceLimits(32, 32, 32, 32, 3), port); + MemoryPrimitiveBatchWriteOutcome outcome = client.WritePrimitiveBatchDetailed( + new MemoryPrimitiveBatchWriteRequest([ + new MemoryAddressValue(_address, 10), new MemoryAddressValue(_address + 4, 20), + new MemoryAddressValue(_address + 8, 30) + ]), + TestContext.Current.CancellationToken); + + Assert.False(outcome.Succeeded); + Assert.Equal(3, outcome.AttemptedCount); + Assert.Equal(failedIndex, outcome.CompletedCount); + Assert.Equal(failedIndex, outcome.FailedIndex); + Assert.Equal(expectedEffectState, outcome.EffectState); + Assert.Equal(CheatEngineFailureKind.MemoryWriteFailed, outcome.Cause?.Kind); + Assert.Equal(Enumerable.Range(0, failedIndex).Select(static index => 10 + index * 10), port.CommittedValues); + Assert.Equal(failedIndex + 1, port.WriteInvocationCount); + } + + [Fact] + public void DetailedWriteReportsCompleteEffectAndLegacyWrappersPreserveTheOldFailureShape() + { + BatchPort successfulPort = new(); + CountingDispatcher successfulDispatcher = new(); + MemoryClient successfulClient = CreateClient(successfulDispatcher, new MemoryResourceLimits(32, 32, 32, 32, 3), + successfulPort); + MemoryPrimitiveBatchWriteOutcome success = successfulClient.WritePrimitiveBatchDetailed( + new MemoryPrimitiveBatchWriteRequest([ + new MemoryAddressValue(_address, 10), new MemoryAddressValue(_address + 4, 20) + ]), + TestContext.Current.CancellationToken); + + Assert.True(success.Succeeded); + Assert.Equal(2, success.CompletedCount); + Assert.Equal(MemoryBatchWriteEffectState.Complete, success.EffectState); + Assert.Null(success.Cause); + Assert.Equal([10, 20], successfulPort.CommittedValues); + Assert.Equal(2, successfulPort.WriteInvocationCount); + Assert.Equal(1, successfulDispatcher.InvocationCount); + + BatchPort failedPort = new() { ReadFailureIndex = 1, WriteFailureIndex = 1 }; + MemoryClient failedClient = CreateClient(new CountingDispatcher(), new MemoryResourceLimits(32, 32, 32, 32, 3), + failedPort); + MemoryPrimitiveBatchReadRequest reads = new([_address, _address + 4]); + MemoryPrimitiveBatchWriteRequest writes = new([ + new MemoryAddressValue(_address, 10), new MemoryAddressValue(_address + 4, 20) + ]); + + Assert.False(failedClient.TryReadPrimitiveBatch(reads, out ImmutableArray values, + out CheatEngineFailure readFailure, TestContext.Current.CancellationToken)); + Assert.Empty(values); + Assert.Equal(CheatEngineFailureKind.MemoryReadFailed, readFailure.Kind); + Assert.False(failedClient.TryWritePrimitiveBatch(writes, out CheatEngineFailure writeFailure, + TestContext.Current.CancellationToken)); + Assert.Equal(CheatEngineFailureKind.MemoryWriteFailed, writeFailure.Kind); + } + + [Fact] + public void DispatcherFailureLeavesWriteEffectUnknownAndDoesNotExposeAFailedIndex() + { + CheatEngineFailure expected = new(CheatEngineFailureKind.InvalidState, "Test.Dispatcher", "Rejected."); + MemoryClient client = + CreateClient(new RejectingDispatcher(expected), new MemoryResourceLimits(32, 32, 32, 32, 3)); + + MemoryPrimitiveBatchWriteOutcome outcome = client.WritePrimitiveBatchDetailed( + new MemoryPrimitiveBatchWriteRequest([new MemoryAddressValue(_address, 10)]), + TestContext.Current.CancellationToken); + + Assert.False(outcome.Succeeded); + Assert.Equal(0, outcome.CompletedCount); + Assert.Null(outcome.FailedIndex); + Assert.Equal(expected, outcome.Cause); + Assert.Equal(MemoryBatchWriteEffectState.Unknown, outcome.EffectState); + } + + [Fact] + public void ConstructionSnapshotsLimitsAndCodecProgrammingExceptionsStillPropagate() + { + MemoryResourceLimits configured = new(32, 32, 32, 32, 2); + BatchPort port = new(); + MemoryClient client = CreateClient(new CountingDispatcher(), configured, port); + configured.MaximumBatchOperationCount = 1; + + MemoryPrimitiveBatchReadOutcome outcome = client.ReadPrimitiveBatchDetailed( + new MemoryPrimitiveBatchReadRequest([_address, _address + 4]), TestContext.Current.CancellationToken); + Assert.True(outcome.Succeeded); + Assert.Equal(2, outcome.CompletedCount); + Assert.Throws(() => client.TryRead( + new MemoryReadRequest(_address, new ThrowingCodec()), + out _, out _, TestContext.Current.CancellationToken)); + } + + [Fact] + public void DefaultLimitsAdmitANormalBatchAndCodecContextsStopOversizedUnknownBuffers() + { + BatchPort defaultPort = new(); + MemoryClient defaultClient = CreateClient(new CountingDispatcher(), new MemoryResourceLimits(), defaultPort); + MemoryPrimitiveBatchReadOutcome defaultOutcome = defaultClient.ReadPrimitiveBatchDetailed( + new MemoryPrimitiveBatchReadRequest([_address, _address + 4]), TestContext.Current.CancellationToken); + + Assert.True(defaultOutcome.Succeeded); + Assert.Equal([100, 101], defaultOutcome.ReadPrefix); + + BatchPort constrainedPort = new(); + MemoryClient constrainedClient = CreateClient(new CountingDispatcher(), + new MemoryResourceLimits(1, 1, 32, 32, 2), + constrainedPort); + bool succeeded = constrainedClient.TryRead(new MemoryReadRequest(_address, new OversizedReadCodec()), + out _, out CheatEngineFailure failure, TestContext.Current.CancellationToken); + + Assert.False(succeeded); + Assert.Equal(CheatEngineFailureKind.MemoryReadFailed, failure.Kind); + Assert.Equal(0, constrainedPort.RawReadInvocationCount); + } + + [Fact] + public void CustomCodecWriteAboveTheConfiguredBudgetDoesNotReachTheRawPort() + { + BatchPort port = new(); + MemoryClient client = CreateClient(new CountingDispatcher(), new MemoryResourceLimits(2, 1, 32, 32, 2), port); + + bool succeeded = client.TryWrite(new MemoryWriteRequest(_address, 42, new OversizedWriteCodec()), + out CheatEngineFailure failure, TestContext.Current.CancellationToken); + + Assert.False(succeeded); + Assert.Equal(CheatEngineFailureKind.MemoryWriteFailed, failure.Kind); + Assert.Contains("exceeds the activation write budget of 1 bytes", failure.Message, StringComparison.Ordinal); + Assert.Equal(0, port.RawWriteInvocationCount); + } + + private static MemoryClient CreateClient(ICheatEngineDispatcher dispatcher, MemoryResourceLimits limits, + IMemoryCodecContextPort? port = null) + { + return new MemoryClient(dispatcher, InertCoreLifetime.Create(), port ?? new BatchPort(), limits); + } + + private sealed class BatchPort : IMemoryCodecContextPort + { + internal List CommittedValues + { + get; + } = []; + + internal int ReadFailureIndex + { + get; + init; + } = -1; + + internal int ReadInvocationCount + { + get; + private set; + } + + internal int RawReadInvocationCount + { + get; + private set; + } + + internal int RawWriteInvocationCount + { + get; + private set; + } + + internal int WriteFailureIndex + { + get; + init; + } = -1; + + internal int WriteInvocationCount + { + get; + private set; + } + + public bool IsTarget64Bit() + { + return true; + } + + public bool TryReadBytes(Address address, Span destination, out string? failure) + { + RawReadInvocationCount++; + destination.Clear(); + failure = null; + return true; + } + + public bool TryReadPrimitive(Address address, out T value, out string? failure) + { + int index = ReadInvocationCount++; + if (index == ReadFailureIndex) + { + value = default!; + failure = $"Read failure {index}."; + return false; + } + + value = (T) (object) (100 + index); + failure = null; + return true; + } + + public bool TryWriteBytes(Address address, ReadOnlySpan source, out string? failure) + { + RawWriteInvocationCount++; + failure = null; + return true; + } + + public bool TryWritePrimitive(Address address, T value, out string? failure) + { + int index = WriteInvocationCount++; + if (index == WriteFailureIndex) + { + failure = $"Write failure {index}."; + return false; + } + + CommittedValues.Add((int) (object) value!); + failure = null; + return true; + } + } + + private sealed class ThrowingCodec : IMemoryCodec + { + public bool TryRead(IMemoryReadContext context, Address address, out int value) + { + throw new InvalidOperationException("Codec programming failures must not be mapped."); + } + + public bool TryWrite(IMemoryWriteContext context, Address address, in int value) + { + throw new InvalidOperationException("Codec programming failures must not be mapped."); + } + } + + private sealed class OversizedReadCodec : IMemoryCodec + { + public bool TryRead(IMemoryReadContext context, Address address, out int value) + { + Span destination = stackalloc byte[2]; + bool succeeded = context.TryReadBytes(address, destination); + value = 0; + return succeeded; + } + + public bool TryWrite(IMemoryWriteContext context, Address address, in int value) + { + return false; + } + } + + private sealed class OversizedWriteCodec : IMemoryCodec + { + public bool TryRead(IMemoryReadContext context, Address address, out int value) + { + value = default; + return false; + } + + public bool TryWrite(IMemoryWriteContext context, Address address, in int value) + { + Span source = stackalloc byte[2]; + return context.TryWriteBytes(address, source); + } + } + + private sealed class CountingDispatcher : ICheatEngineDispatcher + { + internal int InvocationCount + { + get; + private set; + } + + public bool IsMainThread => true; + + public bool TryInvoke(Action callback, out CheatEngineFailure failure, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(callback); + InvocationCount++; + callback(); + failure = default; + return true; + } + + public bool TryInvoke(Func callback, [MaybeNullWhen(false)] out T result, + out CheatEngineFailure failure, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(callback); + InvocationCount++; + result = callback(); + failure = default; + return true; + } + + public void Invoke(Action callback, CancellationToken cancellationToken = default) + { + _ = TryInvoke(callback, out _, cancellationToken); + } + + public T Invoke(Func callback, CancellationToken cancellationToken = default) + { + _ = TryInvoke(callback, out T? result, out _, cancellationToken); + return result!; + } + } + + private sealed class RejectingDispatcher(CheatEngineFailure failure) : ICheatEngineDispatcher + { + private readonly CheatEngineFailure _failure = failure; + + internal int InvocationCount + { + get; + private set; + } + + public bool IsMainThread => false; + + public bool TryInvoke(Action callback, out CheatEngineFailure failure, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(callback); + InvocationCount++; + failure = _failure; + return false; + } + + public bool TryInvoke(Func callback, [MaybeNullWhen(false)] out T result, + out CheatEngineFailure failure, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(callback); + InvocationCount++; + result = default; + failure = _failure; + return false; + } + + public void Invoke(Action callback, CancellationToken cancellationToken = default) + { + _ = TryInvoke(callback, out _, cancellationToken); + } + + public T Invoke(Func callback, CancellationToken cancellationToken = default) + { + _ = TryInvoke(callback, out T? result, out _, cancellationToken); + return result!; + } + } +} diff --git a/tests/CheatEngine.Client.Core.Tests/Domains/MemoryCodecContextLifetimeTests.cs b/tests/CheatEngine.Client.Core.Tests/Domains/MemoryCodecContextLifetimeTests.cs index d89d97b..0932898 100644 --- a/tests/CheatEngine.Client.Core.Tests/Domains/MemoryCodecContextLifetimeTests.cs +++ b/tests/CheatEngine.Client.Core.Tests/Domains/MemoryCodecContextLifetimeTests.cs @@ -177,10 +177,7 @@ public void ContextFromPriorInvocationCannotBeRevivedDuringALaterInvocation() Assert.True(client.TryRead(new MemoryReadRequest(_address, firstCodec), out _, out _, TestContext.Current.CancellationToken)); IMemoryReadContext firstContext = Assert.IsAssignableFrom(firstCodec.ReadContext); - CapturingCodec secondCodec = new() - { - ReadAction = _ => AssertExpired(() => ConsumePointerSize(firstContext)) - }; + CapturingCodec secondCodec = new() { ReadAction = _ => AssertExpired(() => ConsumePointerSize(firstContext)) }; Assert.True(client.TryRead(new MemoryReadRequest(_address, secondCodec), out _, out _, TestContext.Current.CancellationToken)); diff --git a/tests/CheatEngine.Client.Core.Tests/Domains/PatternScannerBehaviorTests.cs b/tests/CheatEngine.Client.Core.Tests/Domains/PatternScannerBehaviorTests.cs new file mode 100644 index 0000000..f381b16 --- /dev/null +++ b/tests/CheatEngine.Client.Core.Tests/Domains/PatternScannerBehaviorTests.cs @@ -0,0 +1,395 @@ +using System.Diagnostics.CodeAnalysis; + +using CheatEngine.Client.Core.Dispatching; +using CheatEngine.Client.Core.Domains; +using CheatEngine.Client.Core.Tests.TestSupport; +using CheatEngine.Client.Results; +using CheatEngine.Client.Scanning; +using CheatEngine.SDK.Engine.Inspection; +using CheatEngine.SDK.Engine.Scanning.Aob; +using CheatEngine.SDK.Engine.Values; + +namespace CheatEngine.Client.Core.Tests.Domains; + +public sealed class PatternScannerBehaviorTests +{ + [Fact] + public void TryScanResolvesModuleBeforeTheGlobalScanAndAppliesModuleAndRangeAsPostFilters() + { + RecordingAobMatchList matches = new(["3FFF", "4000", "4010", "4020", "4100"]); + FakeAobScanPort port = new(matches) { Modules = [Module("game.exe", 0x4000, 0x100)] }; + PatternScanner scanner = CreateScanner(port); + AobScanRequest request = CreateRequest( + new ModuleName("game.exe"), new AobScanRange(0x4010, 0x4020), 3); + + bool succeeded = scanner.TryScan(request, out AobScanResult result, out CheatEngineFailure failure, + TestContext.Current.CancellationToken); + + Assert.True(succeeded); + Assert.Equal(default, failure); + Assert.Equal([0x4010, 0x4020], result.Matches); + Assert.False(result.IsTruncated); + Assert.Equal(1, port.EnumerationCalls); + Assert.Equal(1, port.ScanCalls); + Assert.Equal(1, port.EnumerationCallsWhenScanStarted); + Assert.Equal(5, matches.ItemCalls); + Assert.True(matches.IsDisposed); + } + + [Fact] + public void TryScanCountsOnlyPostFilteredAddressesAgainstTheMaterializationLimit() + { + RecordingAobMatchList matches = new(["3FFF", "4000", "4001", "40FF"]); + FakeAobScanPort port = new(matches) { Modules = [Module("game.exe", 0x4000, 0x100)] }; + PatternScanner scanner = CreateScanner(port); + AobScanRequest request = CreateRequest(new ModuleName("game.exe"), null, 2); + + bool succeeded = scanner.TryScan(request, out AobScanResult result, out CheatEngineFailure failure, + TestContext.Current.CancellationToken); + + Assert.True(succeeded); + Assert.Equal(default, failure); + Assert.Equal([0x4000, 0x4001], result.Matches); + Assert.True(result.IsTruncated); + Assert.Equal(4, matches.ItemCalls); + Assert.True(matches.IsDisposed); + } + + [Fact] + public void TryScanRejectsAnUnknownModuleWithoutStartingTheGlobalScan() + { + FakeAobScanPort port = new() { Modules = [Module("other.exe", 0x4000, 0x100)] }; + PatternScanner scanner = CreateScanner(port); + + bool succeeded = scanner.TryScan(CreateRequest(new ModuleName("game.exe"), null, 1), + out AobScanResult result, out CheatEngineFailure failure, TestContext.Current.CancellationToken); + + Assert.False(succeeded); + Assert.Equal(default, result); + Assert.Equal(CheatEngineFailureKind.NotFound, failure.Kind); + Assert.Equal("Patterns.InModule", failure.Operation); + Assert.Equal(1, port.EnumerationCalls); + Assert.Equal(0, port.ScanCalls); + } + + [Fact] + public void TryScanRejectsAnAmbiguousModuleWithoutStartingTheGlobalScan() + { + FakeAobScanPort port = new() + { + Modules = [Module("game.exe", 0x4000, 0x100), Module("GAME.EXE", 0x5000, 0x100)] + }; + PatternScanner scanner = CreateScanner(port); + + bool succeeded = scanner.TryScan(CreateRequest(new ModuleName("game.exe"), null, 1), + out AobScanResult result, out CheatEngineFailure failure, TestContext.Current.CancellationToken); + + Assert.False(succeeded); + Assert.Equal(default, result); + Assert.Equal(CheatEngineFailureKind.AmbiguousMatch, failure.Kind); + Assert.Equal("Patterns.InModule", failure.Operation); + Assert.Equal(1, port.EnumerationCalls); + Assert.Equal(0, port.ScanCalls); + } + + [Fact] + public void TryScanRejectsAModuleWithoutAnImageSizeBeforeStartingTheGlobalScan() + { + FakeAobScanPort port = new() { Modules = [Module("game.exe", 0x4000, null)] }; + PatternScanner scanner = CreateScanner(port); + + bool succeeded = scanner.TryScan(CreateRequest(new ModuleName("game.exe"), null, 1), + out AobScanResult result, out CheatEngineFailure failure, TestContext.Current.CancellationToken); + + Assert.False(succeeded); + Assert.Equal(default, result); + Assert.Equal(CheatEngineFailureKind.CapabilityUnavailable, failure.Kind); + Assert.Equal("Patterns.InModule", failure.Operation); + Assert.Equal(0, port.ScanCalls); + } + + [Fact] + public void TryScanRejectsAZeroLengthModuleBeforeStartingTheGlobalScan() + { + FakeAobScanPort port = new() { Modules = [Module("game.exe", 0x4000, 0)] }; + PatternScanner scanner = CreateScanner(port); + + bool succeeded = scanner.TryScan(CreateRequest(new ModuleName("game.exe"), null, 1), + out AobScanResult result, out CheatEngineFailure failure, TestContext.Current.CancellationToken); + + Assert.False(succeeded); + Assert.Equal(default, result); + Assert.Equal(CheatEngineFailureKind.InvalidHostResult, failure.Kind); + Assert.Equal("Patterns.InModule", failure.Operation); + Assert.Equal(0, port.ScanCalls); + } + + [Fact] + public void TryScanRejectsAnInvalidModuleEnumerationCountBeforeStartingTheGlobalScan() + { + FakeAobScanPort port = new() { ReportedModuleCount = 4097 }; + PatternScanner scanner = CreateScanner(port); + + bool succeeded = scanner.TryScan(CreateRequest(new ModuleName("game.exe"), null, 1), + out AobScanResult result, out CheatEngineFailure failure, TestContext.Current.CancellationToken); + + Assert.False(succeeded); + Assert.Equal(default, result); + Assert.Equal(CheatEngineFailureKind.InvalidHostResult, failure.Kind); + Assert.Equal("Patterns.InModule", failure.Operation); + Assert.Equal(0, port.ScanCalls); + } + + [Fact] + public void TryScanCancellationDuringModuleEnumerationPreventsTheGlobalScan() + { + using CancellationTokenSource cancellation = new(); + FakeAobScanPort port = new() + { + Modules = [Module("game.exe", 0x4000, 0x100)], OnEnumerateModules = cancellation.Cancel + }; + PatternScanner scanner = CreateScanner(port); + + bool succeeded = scanner.TryScan(CreateRequest(new ModuleName("game.exe"), null, 1), + out AobScanResult result, out CheatEngineFailure failure, cancellation.Token); + + Assert.False(succeeded); + Assert.Equal(default, result); + Assert.Equal(CheatEngineFailureKind.Cancelled, failure.Kind); + Assert.Equal("Patterns.Scan", failure.Operation); + Assert.Equal(1, port.EnumerationCalls); + Assert.Equal(0, port.ScanCalls); + } + + [Fact] + public void TryScanObservesCancellationAtTheBeginningOfTheDispatchedCallback() + { + using CancellationTokenSource cancellation = new(); + FakeAobScanPort port = new(); + PatternScanner scanner = CreateScanner(port, new CancellingMainThreadInvoker(cancellation)); + + bool succeeded = scanner.TryScan(CreateRequest(null, null, 1), + out AobScanResult result, out CheatEngineFailure failure, cancellation.Token); + + Assert.False(succeeded); + Assert.Equal(default, result); + Assert.Equal(CheatEngineFailureKind.Cancelled, failure.Kind); + Assert.Equal("Patterns.Scan", failure.Operation); + Assert.Equal(0, port.EnumerationCalls); + Assert.Equal(0, port.ScanCalls); + } + + [Fact] + public void TryScanObservesCancellationAfterTheGlobalScanAndDisposesTheOwnedList() + { + using CancellationTokenSource cancellation = new(); + RecordingAobMatchList matches = new(["400000"]); + FakeAobScanPort port = new(matches) { OnScan = cancellation.Cancel }; + PatternScanner scanner = CreateScanner(port); + + bool succeeded = scanner.TryScan(CreateRequest(null, null, 1), + out AobScanResult result, out CheatEngineFailure failure, cancellation.Token); + + Assert.False(succeeded); + Assert.Equal(default, result); + Assert.Equal(CheatEngineFailureKind.Cancelled, failure.Kind); + Assert.Equal("Patterns.Scan", failure.Operation); + Assert.Equal(1, port.ScanCalls); + Assert.Equal(0, matches.CountCalls); + Assert.Equal(0, matches.ItemCalls); + Assert.True(matches.IsDisposed); + } + + [Fact] + public void TryScanObservesCancellationDuringCopyDisposesTheOwnedListAndDoesNotPublishAPrefix() + { + using CancellationTokenSource cancellation = new(); + RecordingAobMatchList matches = new(["400000", "400001", "400002"]) + { + OnTryGetItem = index => + { + if (index == 1) + { + cancellation.Cancel(); + } + } + }; + FakeAobScanPort port = new(matches); + PatternScanner scanner = CreateScanner(port); + + bool succeeded = scanner.TryScan(CreateRequest(null, null, 3), + out AobScanResult result, out CheatEngineFailure failure, cancellation.Token); + + Assert.False(succeeded); + Assert.Equal(default, result); + Assert.Equal(CheatEngineFailureKind.Cancelled, failure.Kind); + Assert.Equal("Patterns.Scan", failure.Operation); + Assert.Equal(1, port.ScanCalls); + Assert.Equal(2, matches.ItemCalls); + Assert.True(matches.IsDisposed); + } + + private static PatternScanner CreateScanner(FakeAobScanPort port, IMainThreadInvoker? mainThread = null) + { + return new PatternScanner( + new SdkMainThreadDispatcher(InertCoreLifetime.Create(), mainThread ?? new InlineMainThreadInvoker()), port); + } + + private static AobScanRequest CreateRequest(ModuleName? module, AobScanRange? range, int maximumResults) + { + return new AobScanRequest(new AobPattern("90"), AobScanOptions.Default, maximumResults, module, range); + } + + private static ModuleInfo Module(string name, ulong baseAddress, ulong? imageSize) + { + MemorySize? size = imageSize.HasValue ? new MemorySize(imageSize.Value) : null; + return new ModuleInfo(name, new Address(baseAddress), size, true, name); + } + + private sealed class FakeAobScanPort(RecordingAobMatchList? matchList = null) : IAobScanPort + { + internal ModuleInfo[] Modules + { + get; + init; + } = []; + + internal int? ReportedModuleCount + { + get; + init; + } + + internal Action? OnScan + { + get; + init; + } + + internal Action? OnEnumerateModules + { + get; + init; + } + + internal int EnumerationCalls + { + get; + private set; + } + + internal int ScanCalls + { + get; + private set; + } + + internal int? EnumerationCallsWhenScanStarted + { + get; + private set; + } + + public AobScanHostStatus TryScan(string pattern, AobScanOptions options, + [NotNullWhen(true)] out IAobMatchList? matches) + { + ScanCalls++; + EnumerationCallsWhenScanStarted ??= EnumerationCalls; + OnScan?.Invoke(); + matches = matchList; + return matchList is null ? AobScanHostStatus.InvalidResult : AobScanHostStatus.Success; + } + + public InspectionStatus EnumerateModules(ModuleInfo[] destination, out int written) + { + EnumerationCalls++; + OnEnumerateModules?.Invoke(); + Array.Copy(Modules, destination, Math.Min(Modules.Length, destination.Length)); + written = ReportedModuleCount ?? Modules.Length; + return InspectionStatus.Success; + } + } + + private sealed class RecordingAobMatchList(IReadOnlyList items) : IAobMatchList + { + internal Action? OnTryGetItem + { + get; + init; + } + + internal int CountCalls + { + get; + private set; + } + + internal int ItemCalls + { + get; + private set; + } + + internal bool IsDisposed + { + get; + private set; + } + + public bool TryGetCount(out int count) + { + CountCalls++; + count = items.Count; + return true; + } + + public bool TryGetItem(int index, [NotNullWhen(true)] out string? value) + { + ItemCalls++; + OnTryGetItem?.Invoke(index); + if ((uint) index >= items.Count) + { + value = null; + return false; + } + + value = items[index]; + return true; + } + + public void Dispose() + { + IsDisposed = true; + } + } + + private sealed class CancellingMainThreadInvoker(CancellationTokenSource cancellation) : IMainThreadInvoker + { + public Exception? Invoke(Action callback) + { + cancellation.Cancel(); + try + { + callback(); + return null; + } + catch (Exception exception) + { + return exception; + } + } + + public MainThreadInvocationResult Invoke(Func callback) + { + cancellation.Cancel(); + try + { + return new MainThreadInvocationResult(callback(), null); + } + catch (Exception exception) + { + return new MainThreadInvocationResult(default!, exception); + } + } + } +} diff --git a/tests/CheatEngine.Client.Core.Tests/Domains/ProcessClientTests.cs b/tests/CheatEngine.Client.Core.Tests/Domains/ProcessClientTests.cs index f5cc24f..4829c70 100644 --- a/tests/CheatEngine.Client.Core.Tests/Domains/ProcessClientTests.cs +++ b/tests/CheatEngine.Client.Core.Tests/Domains/ProcessClientTests.cs @@ -1,5 +1,5 @@ -using CheatEngine.Client.Core.Domains; using CheatEngine.Client.Core.Dispatching; +using CheatEngine.Client.Core.Domains; using CheatEngine.Client.Core.Infrastructure; using CheatEngine.Client.Core.Tests.TestSupport; using CheatEngine.Client.Dispatching; diff --git a/tests/CheatEngine.Client.Core.Tests/Domains/RuntimeClientTests.cs b/tests/CheatEngine.Client.Core.Tests/Domains/RuntimeClientTests.cs index 8718e3a..5ad9334 100644 --- a/tests/CheatEngine.Client.Core.Tests/Domains/RuntimeClientTests.cs +++ b/tests/CheatEngine.Client.Core.Tests/Domains/RuntimeClientTests.cs @@ -470,17 +470,9 @@ public bool TryInvoke(Action callback, out CheatEngineFailure failure, } InvocationCount++; - try - { - callback(); - failure = default; - return true; - } - catch (Exception exception) - { - failure = FromException(exception); - return false; - } + callback(); + failure = default; + return true; } public bool TryInvoke(Func callback, out T result, out CheatEngineFailure failure, @@ -495,18 +487,9 @@ public bool TryInvoke(Func callback, out T result, out CheatEngineFailure } InvocationCount++; - try - { - result = callback(); - failure = default; - return true; - } - catch (Exception exception) - { - result = default!; - failure = FromException(exception); - return false; - } + result = callback(); + failure = default; + return true; } public void Invoke(Action callback, CancellationToken cancellationToken = default) @@ -535,16 +518,5 @@ private static CheatEngineFailure Cancelled() "Dispatcher.Invoke", "Cancelled before dispatch."); } - - private static CheatEngineFailure FromException(Exception exception) - { - return new CheatEngineFailure( - exception is EngineMarshallingException - ? CheatEngineFailureKind.InvalidHostResult - : CheatEngineFailureKind.OperationRejected, - "Dispatcher.Invoke", - exception.Message, - exception); - } } } diff --git a/tests/CheatEngine.Client.Core.Tests/Domains/TableClientLookupTests.cs b/tests/CheatEngine.Client.Core.Tests/Domains/TableClientLookupTests.cs new file mode 100644 index 0000000..f897c42 --- /dev/null +++ b/tests/CheatEngine.Client.Core.Tests/Domains/TableClientLookupTests.cs @@ -0,0 +1,216 @@ +using System.Diagnostics.CodeAnalysis; + +using CheatEngine.Client.Core.Domains; +using CheatEngine.Client.Core.Infrastructure; +using CheatEngine.Client.Dispatching; +using CheatEngine.Client.Results; +using CheatEngine.Client.Tables; +using CheatEngine.SDK.Engine.AddressList; +using CheatEngine.SDK.Engine.Enums; + +namespace CheatEngine.Client.Core.Tests.Domains; + +public sealed class TableClientLookupTests +{ + [Fact] + public void TryGetRecordMapsAnUnavailableAddressListToCapabilityUnavailable() + { + FakeRecordLookupPort lookups = new() { IndexStatus = RecordLookupStatus.AddressListUnavailable }; + TableClient client = CreateClient(lookups); + + bool succeeded = client.TryGetRecord(3, out MemoryRecordSnapshot record, out CheatEngineFailure failure, + TestContext.Current.CancellationToken); + + Assert.False(succeeded); + Assert.Equal(default, record); + Assert.Equal(CheatEngineFailureKind.CapabilityUnavailable, failure.Kind); + Assert.Equal("Tables.GetRecord", failure.Operation); + Assert.Equal("Cheat Engine's Address List capability is unavailable.", failure.Message); + Assert.Equal(3, lookups.LastIndex); + } + + [Fact] + public void TryGetRecordPreservesNotFoundForAnAbsentRecord() + { + FakeRecordLookupPort lookups = new() { IdStatus = RecordLookupStatus.NotFound }; + TableClient client = CreateClient(lookups); + MemoryRecordId id = new(42); + + bool succeeded = client.TryGetRecord(id, out MemoryRecordSnapshot record, out CheatEngineFailure failure, + TestContext.Current.CancellationToken); + + Assert.False(succeeded); + Assert.Equal(default, record); + Assert.Equal(CheatEngineFailureKind.NotFound, failure.Kind); + Assert.Equal("Tables.GetRecord", failure.Operation); + Assert.Equal("The requested Cheat Engine memory record was not found.", failure.Message); + Assert.Equal(id, lookups.LastId); + } + + [Fact] + public void TryGetSelectedMapsAMalformedRecordToInvalidHostResult() + { + FakeRecordLookupPort lookups = new() { SelectedStatus = RecordLookupStatus.InvalidRecord }; + TableClient client = CreateClient(lookups); + + bool succeeded = client.TryGetSelected(out MemoryRecordSnapshot record, out CheatEngineFailure failure, + TestContext.Current.CancellationToken); + + Assert.False(succeeded); + Assert.Equal(default, record); + Assert.Equal(CheatEngineFailureKind.InvalidHostResult, failure.Kind); + Assert.Equal("Tables.GetSelected", failure.Operation); + Assert.Equal("Cheat Engine did not return the expected Address List contract.", failure.Message); + Assert.Equal(1, lookups.SelectedCalls); + } + + [Fact] + public void TryGetRecordReturnsThePortSnapshotWhenTheLookupSucceeds() + { + MemoryRecordSnapshot expected = Snapshot(42, "Health"); + FakeRecordLookupPort lookups = new() { IdStatus = RecordLookupStatus.Success, IdRecord = expected }; + TableClient client = CreateClient(lookups); + + bool succeeded = client.TryGetRecord(new MemoryRecordId(42), out MemoryRecordSnapshot record, + out CheatEngineFailure failure, TestContext.Current.CancellationToken); + + Assert.True(succeeded); + Assert.Equal(expected, record); + Assert.Equal(default, failure); + } + + private static TableClient CreateClient(FakeRecordLookupPort lookups) + { + return new TableClient(new InlineDispatcher(), CoreClientPolicy.SafeDefaults, recordLookups: lookups); + } + + private static MemoryRecordSnapshot Snapshot(int id, string description) + { + return new MemoryRecordSnapshot( + new MemoryRecordId(id), + 0, + new MemoryRecordContentSnapshot(description, "game.exe+24", "50", VariableType.Dword), + new MemoryRecordStateSnapshot(null)); + } + + private sealed class FakeRecordLookupPort : ITableRecordLookupPort + { + internal RecordLookupStatus IndexStatus + { + get; + init; + } = RecordLookupStatus.Success; + + internal RecordLookupStatus IdStatus + { + get; + init; + } = RecordLookupStatus.Success; + + internal RecordLookupStatus SelectedStatus + { + get; + init; + } = RecordLookupStatus.Success; + + internal MemoryRecordSnapshot IdRecord + { + get; + init; + } + + internal int LastIndex + { + get; + private set; + } + + internal MemoryRecordId LastId + { + get; + private set; + } + + internal int SelectedCalls + { + get; + private set; + } + + public RecordLookupStatus TryGetRecord(int index, out MemoryRecordSnapshot record) + { + LastIndex = index; + record = default; + return IndexStatus; + } + + public RecordLookupStatus TryGetRecord(MemoryRecordId id, out MemoryRecordSnapshot record) + { + LastId = id; + record = IdRecord; + return IdStatus; + } + + public RecordLookupStatus TryGetSelected(out MemoryRecordSnapshot record) + { + SelectedCalls++; + record = default; + return SelectedStatus; + } + } + + private sealed class InlineDispatcher : ICheatEngineDispatcher + { + public bool IsMainThread => true; + + public bool TryInvoke(Action callback, out CheatEngineFailure failure, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(callback); + if (cancellationToken.IsCancellationRequested) + { + failure = new CheatEngineFailure(CheatEngineFailureKind.Cancelled, "Test.Dispatcher", "Cancelled."); + return false; + } + + callback(); + failure = default; + return true; + } + + public bool TryInvoke(Func callback, [MaybeNullWhen(false)] out T result, + out CheatEngineFailure failure, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(callback); + if (cancellationToken.IsCancellationRequested) + { + result = default; + failure = new CheatEngineFailure(CheatEngineFailureKind.Cancelled, "Test.Dispatcher", "Cancelled."); + return false; + } + + result = callback(); + failure = default; + return true; + } + + public void Invoke(Action callback, CancellationToken cancellationToken = default) + { + if (!TryInvoke(callback, out CheatEngineFailure failure, cancellationToken)) + { + failure.Throw(); + } + } + + public T Invoke(Func callback, CancellationToken cancellationToken = default) + { + if (TryInvoke(callback, out T? result, out CheatEngineFailure failure, cancellationToken)) + { + return result; + } + + failure.Throw(); + return default!; + } + } +} diff --git a/tests/CheatEngine.Client.Core.Tests/Lua/LuaModuleRegistrationTests.cs b/tests/CheatEngine.Client.Core.Tests/Lua/LuaModuleRegistrationTests.cs index 1432f43..070737c 100644 --- a/tests/CheatEngine.Client.Core.Tests/Lua/LuaModuleRegistrationTests.cs +++ b/tests/CheatEngine.Client.Core.Tests/Lua/LuaModuleRegistrationTests.cs @@ -1,7 +1,9 @@ using System.Collections.Immutable; +using CheatEngine.Client.Core.Dispatching; using CheatEngine.Client.Core.Domains; using CheatEngine.Client.Core.Infrastructure; +using CheatEngine.Client.Core.Tests.TestSupport; using CheatEngine.Client.Dispatching; using CheatEngine.Client.Lua; using CheatEngine.Client.Results; @@ -142,6 +144,49 @@ public void FailedDescribedRegistrationReleasesItsNameReservationForTheNextModul Assert.Equal(["replacement.register"], replacement.Events); } + [Fact] + public void TryRegisterModuleReleasesTrackedReservationWhenDispatcherRejectsRegistration() + { + CheatEngineFailure dispatchFailure = new(CheatEngineFailureKind.InvalidState, "Test.Dispatcher", "Rejected."); + ImmediateDispatcher dispatcher = new() { TryInvokeFailure = dispatchFailure }; + List tracked = []; + int trackCount = 0; + int untrackCount = 0; + DescribedRecordingModule rejected = new("rejected", "diagnostics", ["diagnostics"]); + DescribedRecordingModule replacement = new("replacement", "diagnostics", ["diagnostics"]); + LuaClient client = CreateClient( + dispatcher, + static () => true, + lease => + { + trackCount++; + tracked.Add(lease); + }, + lease => + { + untrackCount++; + tracked.Remove(lease); + }); + + bool succeeded = client.TryRegisterModule(rejected, out ILuaModuleLease? rejectedLease, + out CheatEngineFailure failure, TestContext.Current.CancellationToken); + + Assert.False(succeeded); + Assert.Null(rejectedLease); + Assert.Equal(dispatchFailure, failure); + Assert.Empty(rejected.Events); + Assert.Equal(1, trackCount); + Assert.Equal(1, untrackCount); + Assert.Empty(tracked); + + dispatcher.TryInvokeFailure = null; + using ILuaModuleLease replacementLease = + client.RegisterModule(replacement, TestContext.Current.CancellationToken); + + Assert.Equal(["replacement.register"], replacement.Events); + Assert.Single(tracked, replacementLease); + } + [Fact] public async Task ConcurrentDescribedRegistrationRejectsTheSecondModuleBeforeEitherOfItsLuaExportsMutate() { @@ -226,23 +271,207 @@ public void TryRegisterModuleMapsAnApplicationModuleFailureAndDoesNotCreateALeas } [Fact] - public void TryRegisterModuleRollsBackTheGeneratedRegistrationWhenTheActivationCannotTrackItsLease() + public void TryRegisterModuleDoesNotMutateLuaAndReleasesReservationsWhenLeaseTrackingFails() { ImmediateDispatcher dispatcher = new(); - RecordingModule module = new("diagnostics"); + bool trackingAvailable = false; + List tracked = []; + DescribedRecordingModule rejected = new("rejected", "diagnostics", ["diagnostics"]); + DescribedRecordingModule replacement = new("replacement", "diagnostics", ["diagnostics"]); LuaClient client = CreateClient( dispatcher, static () => true, - static _ => throw new InvalidOperationException("activation is closed")); + lease => + { + if (!trackingAvailable) + { + throw new InvalidOperationException("activation is closed"); + } - bool succeeded = client.TryRegisterModule(module, out ILuaModuleLease? lease, out CheatEngineFailure failure, + tracked.Add(lease); + }, + lease => + { + tracked.Remove(lease); + }); + + bool succeeded = client.TryRegisterModule(rejected, out ILuaModuleLease? lease, out CheatEngineFailure failure, TestContext.Current.CancellationToken); Assert.False(succeeded); Assert.Null(lease); Assert.Equal(CheatEngineFailureKind.OperationRejected, failure.Kind); + Assert.Empty(rejected.Events); + Assert.Equal(0, dispatcher.InvocationCount); + Assert.Empty(tracked); + + trackingAvailable = true; + using ILuaModuleLease replacementLease = + client.RegisterModule(replacement, TestContext.Current.CancellationToken); + + Assert.Equal(["replacement.register"], replacement.Events); + Assert.Single(tracked, replacementLease); + } + + [Fact] + public void TryRegisterModuleRetainsAnAbandonmentFailureAlongsideTheLeaseTrackingFailure() + { + ImmediateDispatcher dispatcher = new(); + bool failTracking = true; + bool failUntracking = true; + DescribedRecordingModule rejected = new("rejected", "diagnostics", ["diagnostics"]); + DescribedRecordingModule replacement = new("replacement", "diagnostics", ["diagnostics"]); + LuaClient client = CreateClient( + dispatcher, + static () => true, + _ => + { + if (failTracking) + { + throw new InvalidOperationException("tracking failed"); + } + }, + _ => + { + if (failUntracking) + { + throw new InvalidOperationException("untracking failed"); + } + }); + + Assert.False(client.TryRegisterModule(rejected, out ILuaModuleLease? lease, out CheatEngineFailure failure, + TestContext.Current.CancellationToken)); + + Assert.Null(lease); + Assert.Equal(CheatEngineFailureKind.OperationRejected, failure.Kind); + Assert.Contains("tracking failed", failure.Message, StringComparison.Ordinal); + Assert.Contains("untracking failed", failure.Message, StringComparison.Ordinal); + AggregateException aggregate = Assert.IsType(failure.Exception); + Assert.Collection( + aggregate.InnerExceptions, + exception => Assert.Equal("tracking failed", exception.Message), + exception => Assert.Equal("untracking failed", exception.Message)); + Assert.Empty(rejected.Events); + Assert.Equal(0, dispatcher.InvocationCount); + + failTracking = false; + failUntracking = false; + using ILuaModuleLease replacementLease = + client.RegisterModule(replacement, TestContext.Current.CancellationToken); + + Assert.Equal(["replacement.register"], replacement.Events); + } + + [Fact] + public void TryRegisterModuleDefersARegistrationCompletedDuringShutdownToMainThreadCleanupWithoutPublishingALease() + { + using ControlledCoreLifetimeContext context = new(); + using CoreLifetime lifetime = new(context); + RecordingMainThreadInvoker invoker = new(); + SdkMainThreadDispatcher dispatcher = new(lifetime, invoker); + DescribedRecordingModule module = new("diagnostics", "diagnostics", ["diagnostics"], onRegister: context.Stop); + LuaClient client = new(dispatcher, lifetime); + + bool succeeded = client.TryRegisterModule(module, out ILuaModuleLease? lease, out CheatEngineFailure failure, + TestContext.Current.CancellationToken); + + Assert.False(succeeded); + Assert.Null(lease); + Assert.Equal(CheatEngineFailureKind.InvalidState, failure.Kind); + Assert.Equal("Lua.RegisterModule", failure.Operation); + Assert.IsType(failure.Exception); + Assert.Equal(["diagnostics.register"], module.Events); + Assert.Equal(1, invoker.ActionCalls); + + using (lifetime.EnterCleanupScope()) + { + lifetime.DrainOwnedResourcesForDisable(); + } + Assert.Equal(["diagnostics.register", "diagnostics.unregister"], module.Events); + Assert.Equal(2, invoker.ActionCalls); + } + + [Fact] + public void TryRegisterModuleSecondAdmissionFailureOutsideStoppingUnregistersUntracksAndAllowsReplacement() + { + ImmediateDispatcher dispatcher = new(); + List tracked = []; + int trackCount = 0; + int untrackCount = 0; + int admissionCount = 0; + DescribedRecordingModule rejected = new("rejected", "diagnostics", ["diagnostics"]); + DescribedRecordingModule replacement = new("replacement", "diagnostics", ["diagnostics"]); + LuaClient client = CreateClient( + dispatcher, + static () => true, + lease => + { + trackCount++; + tracked.Add(lease); + }, + lease => + { + untrackCount++; + tracked.Remove(lease); + }, + _ => + { + admissionCount++; + if (admissionCount == 2) + { + throw new InvalidOperationException("Second admission failed."); + } + }, + static () => false); + + bool succeeded = client.TryRegisterModule(rejected, out ILuaModuleLease? rejectedLease, + out CheatEngineFailure failure, TestContext.Current.CancellationToken); + + Assert.False(succeeded); + Assert.Null(rejectedLease); + Assert.Equal(CheatEngineFailureKind.OperationRejected, failure.Kind); + Assert.Equal("Lua.RegisterModule", failure.Operation); + Assert.Equal(["rejected.register", "rejected.unregister"], rejected.Events); Assert.Equal(2, dispatcher.InvocationCount); + Assert.Equal(1, trackCount); + Assert.Equal(1, untrackCount); + Assert.Empty(tracked); + + using ILuaModuleLease replacementLease = + client.RegisterModule(replacement, TestContext.Current.CancellationToken); + + Assert.Equal(["replacement.register"], replacement.Events); + Assert.Equal(4, admissionCount); + Assert.Equal(2, trackCount); + Assert.Equal(1, untrackCount); + Assert.Single(tracked, replacementLease); + } + + [Fact] + public void TryRegisterModulePreservesADeferredUnregistrationFailureFromShutdownCleanup() + { + using ControlledCoreLifetimeContext context = new(); + using CoreLifetime lifetime = new(context); + RecordingMainThreadInvoker invoker = new(); + SdkMainThreadDispatcher dispatcher = new(lifetime, invoker); + RecordingModule module = new("diagnostics", onRegister: context.Stop, unregisterFailureCount: 1); + LuaClient client = new(dispatcher, lifetime); + + Assert.False(client.TryRegisterModule(module, out ILuaModuleLease? lease, out CheatEngineFailure failure, + TestContext.Current.CancellationToken)); + Assert.Null(lease); + Assert.Equal(CheatEngineFailureKind.InvalidState, failure.Kind); + + using (lifetime.EnterCleanupScope()) + { + InvalidOperationException cleanupException = Assert.Throws( + lifetime.DrainOwnedResourcesForDisable); + Assert.Equal("generated unregistration failed", cleanupException.Message); + } + + Assert.Equal(["diagnostics.register", "diagnostics.unregister"], module.Events); + Assert.Equal(2, invoker.ActionCalls); } [Fact] @@ -356,16 +585,20 @@ private static LuaClient CreateClient( ICheatEngineDispatcher dispatcher, Func isActivationCurrent, Action? trackLease = null, - Action? untrackLease = null) + Action? untrackLease = null, + Action? admitStatefulOperation = null, + Func? isStopping = null) { - return new LuaClient(dispatcher, static () => 81, isActivationCurrent, trackLease, untrackLease); + return new LuaClient(dispatcher, static () => 81, isActivationCurrent, trackLease, untrackLease, + admitStatefulOperation, isStopping); } private sealed class RecordingModule( string name, Exception? registerException = null, List? events = null, - int unregisterFailureCount = 0) : ILuaModule + int unregisterFailureCount = 0, + Action? onRegister = null) : ILuaModule { private int _remainingUnregisterFailures = unregisterFailureCount; @@ -377,6 +610,7 @@ internal List Events public void Register() { Events.Add(name + ".register"); + onRegister?.Invoke(); if (registerException is not null) { throw registerException; @@ -401,9 +635,10 @@ internal DescribedRecordingModule( string name, string moduleName, string[] exports, - Exception? registerException = null) + Exception? registerException = null, + Action? onRegister = null) { - _inner = new RecordingModule(name, registerException); + _inner = new RecordingModule(name, registerException, onRegister: onRegister); Descriptor = new LuaModuleDescriptor(moduleName, exports.Select(static export => new LuaExportDescriptor(export)).ToImmutableArray()); } @@ -440,6 +675,12 @@ internal Exception? InvokeException set; } + internal CheatEngineFailure? TryInvokeFailure + { + get; + set; + } + public bool IsMainThread => true; public bool TryInvoke(Action callback, out CheatEngineFailure failure, @@ -453,6 +694,12 @@ public bool TryInvoke(Action callback, out CheatEngineFailure failure, } InvocationCount++; + if (TryInvokeFailure.HasValue) + { + failure = TryInvokeFailure.Value; + return false; + } + callback(); failure = default; return true; @@ -470,6 +717,13 @@ public bool TryInvoke(Func callback, out TResult result, out C } InvocationCount++; + if (TryInvokeFailure.HasValue) + { + result = default!; + failure = TryInvokeFailure.Value; + return false; + } + result = callback(); failure = default; return true; @@ -572,4 +826,41 @@ internal bool WaitUntilEntered(TimeSpan timeout) return _entered.Wait(timeout); } } + + private sealed class RecordingMainThreadInvoker : IMainThreadInvoker + { + internal int ActionCalls + { + get; + private set; + } + + public Exception? Invoke(Action callback) + { + ArgumentNullException.ThrowIfNull(callback); + ActionCalls++; + try + { + callback(); + return null; + } + catch (Exception exception) + { + return exception; + } + } + + public MainThreadInvocationResult Invoke(Func callback) + { + ArgumentNullException.ThrowIfNull(callback); + try + { + return new MainThreadInvocationResult(callback(), null); + } + catch (Exception exception) + { + return new MainThreadInvocationResult(default!, exception); + } + } + } } diff --git a/tests/CheatEngine.Client.Extensions.DependencyInjection.Tests/CheatEngineClientOptionsSemanticValidatorTests.cs b/tests/CheatEngine.Client.Extensions.DependencyInjection.Tests/CheatEngineClientOptionsSemanticValidatorTests.cs index 7d9f08b..702b9f6 100644 --- a/tests/CheatEngine.Client.Extensions.DependencyInjection.Tests/CheatEngineClientOptionsSemanticValidatorTests.cs +++ b/tests/CheatEngine.Client.Extensions.DependencyInjection.Tests/CheatEngineClientOptionsSemanticValidatorTests.cs @@ -1,3 +1,5 @@ +using CheatEngine.Client.Memory; + using Microsoft.Extensions.Options; namespace CheatEngine.Client.Extensions.DependencyInjection.Tests; @@ -14,6 +16,26 @@ public void ValidateAcceptsAnEmptyAllowedRootList() Assert.True(result.Succeeded); } + [Fact] + public void ValidateRejectsMissingOrInvalidMemoryResourceLimits() + { + ValidateOptionsResult missing = _validator.Validate(null, + new CheatEngineClientOptions { MemoryResourceLimits = null }); + ValidateOptionsResult invalid = _validator.Validate(null, + new CheatEngineClientOptions + { + MemoryResourceLimits = new MemoryResourceLimits + { + MaximumBatchOperationCount = MemoryBatchLimits.MaximumOperations + 1 + } + }); + + Assert.True(missing.Failed); + Assert.Contains("MemoryResourceLimits", missing.FailureMessage, StringComparison.Ordinal); + Assert.True(invalid.Failed); + Assert.Contains("MemoryResourceLimits", invalid.FailureMessage, StringComparison.Ordinal); + } + [Fact] public void ValidateRejectsNullAllowedRootList() { diff --git a/tests/CheatEngine.Client.Extensions.DependencyInjection.Tests/CheatEngineClientServiceCollectionExtensionsTests.cs b/tests/CheatEngine.Client.Extensions.DependencyInjection.Tests/CheatEngineClientServiceCollectionExtensionsTests.cs index be4a3ba..78d6435 100644 --- a/tests/CheatEngine.Client.Extensions.DependencyInjection.Tests/CheatEngineClientServiceCollectionExtensionsTests.cs +++ b/tests/CheatEngine.Client.Extensions.DependencyInjection.Tests/CheatEngineClientServiceCollectionExtensionsTests.cs @@ -41,6 +41,7 @@ public void AddCheatEngineClientRegistersDescriptorsThatPassProviderValidationWi Assert.Contains(services, static descriptor => descriptor.ServiceType == typeof(ICheatEngineClient)); Assert.Contains(services, static descriptor => descriptor.ServiceType == typeof(ILocalProcessDiagnostics)); Assert.Contains(services, static descriptor => descriptor.ServiceType == typeof(IMemoryCodec)); + Assert.Contains(services, static descriptor => descriptor.ServiceType == typeof(IMemoryBatchClient)); Assert.Contains(services, static descriptor => descriptor.ServiceType == typeof(IAllocationClient)); Assert.Contains(services, static descriptor => descriptor.ServiceType == typeof(IAssemblyClient)); Assert.Contains(services, static descriptor => descriptor.ServiceType == typeof(IRemoteExecutionClient)); @@ -124,6 +125,21 @@ public void RootOverloadUsesTheDefaultClientSection() Assert.Equal([allowedRoot], Assert.IsType(options.AllowedTableRoots)); } + [Fact] + public void RootOverloadBindsMemoryResourceLimitsForTheActivationSnapshot() + { + using ConfigurationManager configuration = new(); + configuration["CheatEngineClient:MemoryResourceLimits:MaximumReadBytes"] = "37"; + ServiceCollection services = new(); + services.AddCheatEngineClient(configuration); + + using ServiceProvider provider = services.BuildServiceProvider(); + CheatEngineClientOptions options = provider.GetRequiredService>().Value; + + Assert.NotNull(options.MemoryResourceLimits); + Assert.Equal(37, options.MemoryResourceLimits.MaximumReadBytes); + } + [Fact] public void ConfigurationCannotEnableUnsafeLuaExecution() { diff --git a/tests/CheatEngine.Client.Fluent.Tests/Memory/MemoryPrimitiveBatchBuilderTests.cs b/tests/CheatEngine.Client.Fluent.Tests/Memory/MemoryPrimitiveBatchBuilderTests.cs index 3839531..0ac81bc 100644 --- a/tests/CheatEngine.Client.Fluent.Tests/Memory/MemoryPrimitiveBatchBuilderTests.cs +++ b/tests/CheatEngine.Client.Fluent.Tests/Memory/MemoryPrimitiveBatchBuilderTests.cs @@ -26,9 +26,9 @@ public void DefaultBatchBuilderRejectsEveryTerminalBeforeBuildingOrDispatchingAR }); foreach (InvalidOperationException exception in new[] - { - readException, tryReadException, writeException, tryWriteException, - }) + { + readException, tryReadException, writeException, tryWriteException + }) { Assert.Contains("Memory.Batch(memory)", exception.Message); Assert.Contains("memory.Batch()", exception.Message); diff --git a/tests/CheatEngine.Client.Fluent.Tests/Scanning/AobFluentBuilderTests.cs b/tests/CheatEngine.Client.Fluent.Tests/Scanning/AobFluentBuilderTests.cs index d8200fc..4b52096 100644 --- a/tests/CheatEngine.Client.Fluent.Tests/Scanning/AobFluentBuilderTests.cs +++ b/tests/CheatEngine.Client.Fluent.Tests/Scanning/AobFluentBuilderTests.cs @@ -32,7 +32,7 @@ public void ConfigurationMethodsReturnNewBuilderWithoutChangingTheOriginal() FakePatternScanner scanner = new(); AobScanBuilder original = scanner.Aob("48 8B ?? 89"); - AobScanBuilder configured = original.InModule("game.exe").InRange(0x400000, 0x4FFFFF).ReadableExecutable(); + AobScanBuilder configured = original.InModule("game.exe").InRange(0x400000, 0x4FFFFF).Executable(); Assert.Null(original.Module); Assert.Null(original.Range); @@ -43,6 +43,18 @@ public void ConfigurationMethodsReturnNewBuilderWithoutChangingTheOriginal() Assert.Equal("48 8B ?? 89", configured.Pattern.Value); } + [Fact] + public void ReadableExecutableRemainsACompatibleAliasForExecutable() + { + FakePatternScanner scanner = new(); + + AobScanBuilder executable = scanner.Aob("90").Executable(); + AobScanBuilder readableExecutable = scanner.Aob("90").ReadableExecutable(); + + Assert.Equal("+X-C-W", executable.Options.ProtectionFlags); + Assert.Equal(executable.Options, readableExecutable.Options); + } + [Fact] public void RequireSingleUsesTwoResultLimitAndReturnsTheOnlyMatch() { diff --git a/tests/CheatEngine.Client.Hosting.Tests/CheatEngineClientPluginTests.cs b/tests/CheatEngine.Client.Hosting.Tests/CheatEngineClientPluginTests.cs index 1aa32a5..7e7c6cf 100644 --- a/tests/CheatEngine.Client.Hosting.Tests/CheatEngineClientPluginTests.cs +++ b/tests/CheatEngine.Client.Hosting.Tests/CheatEngineClientPluginTests.cs @@ -321,7 +321,8 @@ public void FailedModuleEnableRollsBackAndTheSamePluginCanEnableAgain() Assert.Equal( [ "configure", "module.enabled", "cleanup.enter", "module.disabling", "cleanup.drain", "cleanup.exit", - "configure", "module.enabled", "client.enabled", "cleanup.enter", "client.disabling", "module.disabling", + "configure", "module.enabled", "client.enabled", "cleanup.enter", "client.disabling", + "module.disabling", "cleanup.drain", "cleanup.exit" ], events); @@ -626,7 +627,7 @@ public void Dispose() public interface IActivationOwnedAlias { - ActivationOwnedDisposable OwnedDisposable + public ActivationOwnedDisposable OwnedDisposable { get; } diff --git a/tests/CheatEngine.Client.LivePlugin.Coexistence/CoexistenceDiagnostics.cs b/tests/CheatEngine.Client.LivePlugin.Coexistence/CoexistenceDiagnostics.cs index a1832e4..fd38323 100644 --- a/tests/CheatEngine.Client.LivePlugin.Coexistence/CoexistenceDiagnostics.cs +++ b/tests/CheatEngine.Client.LivePlugin.Coexistence/CoexistenceDiagnostics.cs @@ -4,9 +4,9 @@ using CheatEngine.Client; using CheatEngine.Client.Allocations; +using CheatEngine.Client.Hosting; using CheatEngine.Client.Processes; using CheatEngine.Client.Results; -using CheatEngine.Client.Hosting; using CheatEngine.SDK.Hosting.Bootstrap; namespace LivePlugin.Coexistence; @@ -96,7 +96,7 @@ internal static string RetainOwner() } if (!client.Allocations.TryAllocate(new TargetAllocationRequest(16), out ITargetMemoryLease? owner, - out CheatEngineFailure failure)) + out CheatEngineFailure failure)) { return DescribeFailure("Owner", failure); } diff --git a/tests/CheatEngine.Client.LivePlugin.Coexistence/PluginA/packages.lock.json b/tests/CheatEngine.Client.LivePlugin.Coexistence/PluginA/packages.lock.json index 30eb595..4709618 100644 --- a/tests/CheatEngine.Client.LivePlugin.Coexistence/PluginA/packages.lock.json +++ b/tests/CheatEngine.Client.LivePlugin.Coexistence/PluginA/packages.lock.json @@ -23,16 +23,66 @@ "Microsoft.Extensions.Primitives": "10.0.12" } }, - "Microsoft.Extensions.Configuration.Abstractions": { + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "TDYD33TSRpXKZWlmTXNlj5kCihxatmv2Ec1u6C+bMYLphCS7PoSLE9Pjd/nunDoE7yETk+LLKjVJX78HYtWjpA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "10.0.12", + "contentHash": "dYfCLR52UA+3DL7C4I/pvSaRPkNqxrUAQmbFL2u0zvYKKzqgrFCJl08Df+F1aYc8leu9JvpC9bsURUdpExcBXQ==" + }, + "cheatengine.client.abstractions": { + "type": "Project", + "dependencies": { + "CheatEngine.SDK": "[1.0.0, 2.0.0)" + } + }, + "cheatengine.client.core": { + "type": "Project", + "dependencies": { + "CheatEngine.Client.Abstractions": "[0.1.0, )", + "CheatEngine.SDK": "[1.0.0, 2.0.0)" + } + }, + "cheatengine.client.extensions.dependencyinjection": { + "type": "Project", + "dependencies": { + "CheatEngine.Client.Abstractions": "[0.1.0, )", + "CheatEngine.Client.Core": "[0.1.0, )", + "Microsoft.Extensions.Configuration.Abstractions": "[10.0.12, )", + "Microsoft.Extensions.DependencyInjection": "[10.0.12, )", + "Microsoft.Extensions.Logging": "[10.0.12, )", + "Microsoft.Extensions.Options.ConfigurationExtensions": "[10.0.12, )", + "Microsoft.Extensions.Options.DataAnnotations": "[10.0.12, )" + } + }, + "cheatengine.client.hosting": { + "type": "Project", + "dependencies": { + "CheatEngine.Client.Extensions.DependencyInjection": "[0.1.0, )", + "CheatEngine.SDK": "[1.0.0, 2.0.0)", + "Microsoft.Extensions.DependencyInjection": "[10.0.12, )", + "Microsoft.Extensions.Logging": "[10.0.12, )" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.12, )", + "resolved": "10.0.12", "contentHash": "8xaGcvS/qZ1otoxPQCEJkNva389CVL/plNcvIETZhQTETYdRkYDPEYhUMoAGONo4FU45ufdfE0j29AfWVVj0wA==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.12" } }, "Microsoft.Extensions.Configuration.Binder": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "dAgIf1TOr8KLs+aBRIbXUZBjHoSH4rDG8+XkX/Q6AZwkQdMA0+yPDKTHsieeXdZDfpOpZVHzOnuAb5Z2nX3KsA==", "dependencies": { @@ -41,7 +91,8 @@ } }, "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "lXyK2O5GoYvfxW8eCFcD16JFbcoSTM1sJkAM0UHS1jZyl9NYMW64Tqm6OQFT0IDBjZi+xHt95/Zg+nxZhGFhZg==", "dependencies": { @@ -49,12 +100,14 @@ } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "9/qymSh7hVDMGTGwrLz8MRp5zRyXy9adGDOs4HwRdnLil3oZGYuWeZjbmHgCQ9BL1qBroVfgUK3U/nb61617Cw==" }, "Microsoft.Extensions.Logging": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "6I46fTPfgYkrjRYfRXbho9WOvOelTnNjWuZws/hzGHDASH1LEJeA4VKK9k3wJvido8o7jJSB5WkMTonX7HM1bA==", "dependencies": { @@ -64,24 +117,17 @@ } }, "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "+24lC4plfbEDNfLAdTV/SWKS7dW+16X4HdydO3R++134kSNTzcbYA4KpR1Hdh6uWisB8Za3AzwyOn+K+NxWIug==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12" } }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "10.0.12", - "contentHash": "TDYD33TSRpXKZWlmTXNlj5kCihxatmv2Ec1u6C+bMYLphCS7PoSLE9Pjd/nunDoE7yETk+LLKjVJX78HYtWjpA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", - "Microsoft.Extensions.Primitives": "10.0.12" - } - }, "Microsoft.Extensions.Options.ConfigurationExtensions": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "rqpu4qj5WE9x1IHGXSIgHBKi7IUlQaHyp4aXCYIanG2OghlUMFZpZTgExaXwcvmLAJHsxKQWMPpc7D2WIbCVtA==", "dependencies": { @@ -93,52 +139,14 @@ } }, "Microsoft.Extensions.Options.DataAnnotations": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "rPqU/cnDMmL4Gx7sglNnYwh/upWxSLTwVe0ysT88WH9HEE3OfZgaT7TyM86C4GeXwDsDQaj95fUGL5jEB3vxng==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", "Microsoft.Extensions.Options": "10.0.12" } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "10.0.12", - "contentHash": "dYfCLR52UA+3DL7C4I/pvSaRPkNqxrUAQmbFL2u0zvYKKzqgrFCJl08Df+F1aYc8leu9JvpC9bsURUdpExcBXQ==" - }, - "cheatengine.client.abstractions": { - "type": "Project", - "dependencies": { - "CheatEngine.SDK": "[1.0.0, 2.0.0)" - } - }, - "cheatengine.client.core": { - "type": "Project", - "dependencies": { - "CheatEngine.Client.Abstractions": "[0.1.0, )", - "CheatEngine.SDK": "[1.0.0, 2.0.0)" - } - }, - "cheatengine.client.extensions.dependencyinjection": { - "type": "Project", - "dependencies": { - "CheatEngine.Client.Abstractions": "[0.1.0, )", - "CheatEngine.Client.Core": "[0.1.0, )", - "Microsoft.Extensions.Configuration.Abstractions": "[10.0.12, )", - "Microsoft.Extensions.DependencyInjection": "[10.0.12, )", - "Microsoft.Extensions.Logging": "[10.0.12, )", - "Microsoft.Extensions.Options.ConfigurationExtensions": "[10.0.12, )", - "Microsoft.Extensions.Options.DataAnnotations": "[10.0.12, )" - } - }, - "cheatengine.client.hosting": { - "type": "Project", - "dependencies": { - "CheatEngine.Client.Extensions.DependencyInjection": "[0.1.0, )", - "CheatEngine.SDK": "[1.0.0, 2.0.0)", - "Microsoft.Extensions.DependencyInjection": "[10.0.12, )", - "Microsoft.Extensions.Logging": "[10.0.12, )" - } } } } diff --git a/tests/CheatEngine.Client.LivePlugin.Coexistence/PluginB/packages.lock.json b/tests/CheatEngine.Client.LivePlugin.Coexistence/PluginB/packages.lock.json index 30eb595..4709618 100644 --- a/tests/CheatEngine.Client.LivePlugin.Coexistence/PluginB/packages.lock.json +++ b/tests/CheatEngine.Client.LivePlugin.Coexistence/PluginB/packages.lock.json @@ -23,16 +23,66 @@ "Microsoft.Extensions.Primitives": "10.0.12" } }, - "Microsoft.Extensions.Configuration.Abstractions": { + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "TDYD33TSRpXKZWlmTXNlj5kCihxatmv2Ec1u6C+bMYLphCS7PoSLE9Pjd/nunDoE7yETk+LLKjVJX78HYtWjpA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "10.0.12", + "contentHash": "dYfCLR52UA+3DL7C4I/pvSaRPkNqxrUAQmbFL2u0zvYKKzqgrFCJl08Df+F1aYc8leu9JvpC9bsURUdpExcBXQ==" + }, + "cheatengine.client.abstractions": { + "type": "Project", + "dependencies": { + "CheatEngine.SDK": "[1.0.0, 2.0.0)" + } + }, + "cheatengine.client.core": { + "type": "Project", + "dependencies": { + "CheatEngine.Client.Abstractions": "[0.1.0, )", + "CheatEngine.SDK": "[1.0.0, 2.0.0)" + } + }, + "cheatengine.client.extensions.dependencyinjection": { + "type": "Project", + "dependencies": { + "CheatEngine.Client.Abstractions": "[0.1.0, )", + "CheatEngine.Client.Core": "[0.1.0, )", + "Microsoft.Extensions.Configuration.Abstractions": "[10.0.12, )", + "Microsoft.Extensions.DependencyInjection": "[10.0.12, )", + "Microsoft.Extensions.Logging": "[10.0.12, )", + "Microsoft.Extensions.Options.ConfigurationExtensions": "[10.0.12, )", + "Microsoft.Extensions.Options.DataAnnotations": "[10.0.12, )" + } + }, + "cheatengine.client.hosting": { + "type": "Project", + "dependencies": { + "CheatEngine.Client.Extensions.DependencyInjection": "[0.1.0, )", + "CheatEngine.SDK": "[1.0.0, 2.0.0)", + "Microsoft.Extensions.DependencyInjection": "[10.0.12, )", + "Microsoft.Extensions.Logging": "[10.0.12, )" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.12, )", + "resolved": "10.0.12", "contentHash": "8xaGcvS/qZ1otoxPQCEJkNva389CVL/plNcvIETZhQTETYdRkYDPEYhUMoAGONo4FU45ufdfE0j29AfWVVj0wA==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.12" } }, "Microsoft.Extensions.Configuration.Binder": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "dAgIf1TOr8KLs+aBRIbXUZBjHoSH4rDG8+XkX/Q6AZwkQdMA0+yPDKTHsieeXdZDfpOpZVHzOnuAb5Z2nX3KsA==", "dependencies": { @@ -41,7 +91,8 @@ } }, "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "lXyK2O5GoYvfxW8eCFcD16JFbcoSTM1sJkAM0UHS1jZyl9NYMW64Tqm6OQFT0IDBjZi+xHt95/Zg+nxZhGFhZg==", "dependencies": { @@ -49,12 +100,14 @@ } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "9/qymSh7hVDMGTGwrLz8MRp5zRyXy9adGDOs4HwRdnLil3oZGYuWeZjbmHgCQ9BL1qBroVfgUK3U/nb61617Cw==" }, "Microsoft.Extensions.Logging": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "6I46fTPfgYkrjRYfRXbho9WOvOelTnNjWuZws/hzGHDASH1LEJeA4VKK9k3wJvido8o7jJSB5WkMTonX7HM1bA==", "dependencies": { @@ -64,24 +117,17 @@ } }, "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "+24lC4plfbEDNfLAdTV/SWKS7dW+16X4HdydO3R++134kSNTzcbYA4KpR1Hdh6uWisB8Za3AzwyOn+K+NxWIug==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12" } }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "10.0.12", - "contentHash": "TDYD33TSRpXKZWlmTXNlj5kCihxatmv2Ec1u6C+bMYLphCS7PoSLE9Pjd/nunDoE7yETk+LLKjVJX78HYtWjpA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", - "Microsoft.Extensions.Primitives": "10.0.12" - } - }, "Microsoft.Extensions.Options.ConfigurationExtensions": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "rqpu4qj5WE9x1IHGXSIgHBKi7IUlQaHyp4aXCYIanG2OghlUMFZpZTgExaXwcvmLAJHsxKQWMPpc7D2WIbCVtA==", "dependencies": { @@ -93,52 +139,14 @@ } }, "Microsoft.Extensions.Options.DataAnnotations": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "rPqU/cnDMmL4Gx7sglNnYwh/upWxSLTwVe0ysT88WH9HEE3OfZgaT7TyM86C4GeXwDsDQaj95fUGL5jEB3vxng==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", "Microsoft.Extensions.Options": "10.0.12" } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "10.0.12", - "contentHash": "dYfCLR52UA+3DL7C4I/pvSaRPkNqxrUAQmbFL2u0zvYKKzqgrFCJl08Df+F1aYc8leu9JvpC9bsURUdpExcBXQ==" - }, - "cheatengine.client.abstractions": { - "type": "Project", - "dependencies": { - "CheatEngine.SDK": "[1.0.0, 2.0.0)" - } - }, - "cheatengine.client.core": { - "type": "Project", - "dependencies": { - "CheatEngine.Client.Abstractions": "[0.1.0, )", - "CheatEngine.SDK": "[1.0.0, 2.0.0)" - } - }, - "cheatengine.client.extensions.dependencyinjection": { - "type": "Project", - "dependencies": { - "CheatEngine.Client.Abstractions": "[0.1.0, )", - "CheatEngine.Client.Core": "[0.1.0, )", - "Microsoft.Extensions.Configuration.Abstractions": "[10.0.12, )", - "Microsoft.Extensions.DependencyInjection": "[10.0.12, )", - "Microsoft.Extensions.Logging": "[10.0.12, )", - "Microsoft.Extensions.Options.ConfigurationExtensions": "[10.0.12, )", - "Microsoft.Extensions.Options.DataAnnotations": "[10.0.12, )" - } - }, - "cheatengine.client.hosting": { - "type": "Project", - "dependencies": { - "CheatEngine.Client.Extensions.DependencyInjection": "[0.1.0, )", - "CheatEngine.SDK": "[1.0.0, 2.0.0)", - "Microsoft.Extensions.DependencyInjection": "[10.0.12, )", - "Microsoft.Extensions.Logging": "[10.0.12, )" - } } } } diff --git a/tests/CheatEngine.Client.LivePlugin.Coexistence/PluginCollision/packages.lock.json b/tests/CheatEngine.Client.LivePlugin.Coexistence/PluginCollision/packages.lock.json index 30eb595..4709618 100644 --- a/tests/CheatEngine.Client.LivePlugin.Coexistence/PluginCollision/packages.lock.json +++ b/tests/CheatEngine.Client.LivePlugin.Coexistence/PluginCollision/packages.lock.json @@ -23,16 +23,66 @@ "Microsoft.Extensions.Primitives": "10.0.12" } }, - "Microsoft.Extensions.Configuration.Abstractions": { + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "10.0.12", + "contentHash": "TDYD33TSRpXKZWlmTXNlj5kCihxatmv2Ec1u6C+bMYLphCS7PoSLE9Pjd/nunDoE7yETk+LLKjVJX78HYtWjpA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", + "Microsoft.Extensions.Primitives": "10.0.12" + } + }, + "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "10.0.12", + "contentHash": "dYfCLR52UA+3DL7C4I/pvSaRPkNqxrUAQmbFL2u0zvYKKzqgrFCJl08Df+F1aYc8leu9JvpC9bsURUdpExcBXQ==" + }, + "cheatengine.client.abstractions": { + "type": "Project", + "dependencies": { + "CheatEngine.SDK": "[1.0.0, 2.0.0)" + } + }, + "cheatengine.client.core": { + "type": "Project", + "dependencies": { + "CheatEngine.Client.Abstractions": "[0.1.0, )", + "CheatEngine.SDK": "[1.0.0, 2.0.0)" + } + }, + "cheatengine.client.extensions.dependencyinjection": { + "type": "Project", + "dependencies": { + "CheatEngine.Client.Abstractions": "[0.1.0, )", + "CheatEngine.Client.Core": "[0.1.0, )", + "Microsoft.Extensions.Configuration.Abstractions": "[10.0.12, )", + "Microsoft.Extensions.DependencyInjection": "[10.0.12, )", + "Microsoft.Extensions.Logging": "[10.0.12, )", + "Microsoft.Extensions.Options.ConfigurationExtensions": "[10.0.12, )", + "Microsoft.Extensions.Options.DataAnnotations": "[10.0.12, )" + } + }, + "cheatengine.client.hosting": { + "type": "Project", + "dependencies": { + "CheatEngine.Client.Extensions.DependencyInjection": "[0.1.0, )", + "CheatEngine.SDK": "[1.0.0, 2.0.0)", + "Microsoft.Extensions.DependencyInjection": "[10.0.12, )", + "Microsoft.Extensions.Logging": "[10.0.12, )" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "CentralTransitive", + "requested": "[10.0.12, )", + "resolved": "10.0.12", "contentHash": "8xaGcvS/qZ1otoxPQCEJkNva389CVL/plNcvIETZhQTETYdRkYDPEYhUMoAGONo4FU45ufdfE0j29AfWVVj0wA==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.12" } }, "Microsoft.Extensions.Configuration.Binder": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "dAgIf1TOr8KLs+aBRIbXUZBjHoSH4rDG8+XkX/Q6AZwkQdMA0+yPDKTHsieeXdZDfpOpZVHzOnuAb5Z2nX3KsA==", "dependencies": { @@ -41,7 +91,8 @@ } }, "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "lXyK2O5GoYvfxW8eCFcD16JFbcoSTM1sJkAM0UHS1jZyl9NYMW64Tqm6OQFT0IDBjZi+xHt95/Zg+nxZhGFhZg==", "dependencies": { @@ -49,12 +100,14 @@ } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "9/qymSh7hVDMGTGwrLz8MRp5zRyXy9adGDOs4HwRdnLil3oZGYuWeZjbmHgCQ9BL1qBroVfgUK3U/nb61617Cw==" }, "Microsoft.Extensions.Logging": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "6I46fTPfgYkrjRYfRXbho9WOvOelTnNjWuZws/hzGHDASH1LEJeA4VKK9k3wJvido8o7jJSB5WkMTonX7HM1bA==", "dependencies": { @@ -64,24 +117,17 @@ } }, "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "+24lC4plfbEDNfLAdTV/SWKS7dW+16X4HdydO3R++134kSNTzcbYA4KpR1Hdh6uWisB8Za3AzwyOn+K+NxWIug==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12" } }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "10.0.12", - "contentHash": "TDYD33TSRpXKZWlmTXNlj5kCihxatmv2Ec1u6C+bMYLphCS7PoSLE9Pjd/nunDoE7yETk+LLKjVJX78HYtWjpA==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", - "Microsoft.Extensions.Primitives": "10.0.12" - } - }, "Microsoft.Extensions.Options.ConfigurationExtensions": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "rqpu4qj5WE9x1IHGXSIgHBKi7IUlQaHyp4aXCYIanG2OghlUMFZpZTgExaXwcvmLAJHsxKQWMPpc7D2WIbCVtA==", "dependencies": { @@ -93,52 +139,14 @@ } }, "Microsoft.Extensions.Options.DataAnnotations": { - "type": "Transitive", + "type": "CentralTransitive", + "requested": "[10.0.12, )", "resolved": "10.0.12", "contentHash": "rPqU/cnDMmL4Gx7sglNnYwh/upWxSLTwVe0ysT88WH9HEE3OfZgaT7TyM86C4GeXwDsDQaj95fUGL5jEB3vxng==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.12", "Microsoft.Extensions.Options": "10.0.12" } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "10.0.12", - "contentHash": "dYfCLR52UA+3DL7C4I/pvSaRPkNqxrUAQmbFL2u0zvYKKzqgrFCJl08Df+F1aYc8leu9JvpC9bsURUdpExcBXQ==" - }, - "cheatengine.client.abstractions": { - "type": "Project", - "dependencies": { - "CheatEngine.SDK": "[1.0.0, 2.0.0)" - } - }, - "cheatengine.client.core": { - "type": "Project", - "dependencies": { - "CheatEngine.Client.Abstractions": "[0.1.0, )", - "CheatEngine.SDK": "[1.0.0, 2.0.0)" - } - }, - "cheatengine.client.extensions.dependencyinjection": { - "type": "Project", - "dependencies": { - "CheatEngine.Client.Abstractions": "[0.1.0, )", - "CheatEngine.Client.Core": "[0.1.0, )", - "Microsoft.Extensions.Configuration.Abstractions": "[10.0.12, )", - "Microsoft.Extensions.DependencyInjection": "[10.0.12, )", - "Microsoft.Extensions.Logging": "[10.0.12, )", - "Microsoft.Extensions.Options.ConfigurationExtensions": "[10.0.12, )", - "Microsoft.Extensions.Options.DataAnnotations": "[10.0.12, )" - } - }, - "cheatengine.client.hosting": { - "type": "Project", - "dependencies": { - "CheatEngine.Client.Extensions.DependencyInjection": "[0.1.0, )", - "CheatEngine.SDK": "[1.0.0, 2.0.0)", - "Microsoft.Extensions.DependencyInjection": "[10.0.12, )", - "Microsoft.Extensions.Logging": "[10.0.12, )" - } } } } diff --git a/tests/CheatEngine.Client.LivePlugin.Coexistence/README.md b/tests/CheatEngine.Client.LivePlugin.Coexistence/README.md index 4c3a5bb..866c2ad 100644 --- a/tests/CheatEngine.Client.LivePlugin.Coexistence/README.md +++ b/tests/CheatEngine.Client.LivePlugin.Coexistence/README.md @@ -39,10 +39,10 @@ generator and bridge assets. `CoexistenceSdkPackageVersion` can be overridden pe has an exact candidate SDK package source and tuple to qualify. A non-default version deliberately disables this fixture's lock-file write path; it is not an invitation to invent or float package versions. -The source fixture is not a replacement for a clean Client package consumer: `eng/Invoke-PackageSmoke.ps1` remains the -Client package-consumer gate. The coexistence runner imports the Hosting deployment target solely to stage a **fresh** -complete source-fixture closure. It skips the direct-package profile validation because the Client graph is a project -reference. Its receipt labels that distinction explicitly. +The source fixture is not a replacement for a clean Client package consumer. The package-consumer gate is the C# +`PackageConsumptionSmokeTests` suite, which consumes the immutable package directory supplied through +`CHEATENGINE_CLIENT_PACKAGE_SOURCE`. The coexistence fixture skips direct-package profile validation because its +Client graph is a project reference; that distinction must remain explicit in any live qualification receipt. The resolved SDK package is not evidence that it contains the later SDK PR #56 source merge, nor is that merge a published-package or live-host qualification. Before a real run, identify the exact qualified Client/SDK package tuple, @@ -57,25 +57,10 @@ observation, not a portable hosting claim. ## Prepare isolated bundles and a receipt -Run the opt-in preparation runner from the repository root. It builds each project directly into a new bundle directory, -validates the `.deps.json` runtime/native closure plus the plugin, Client, SDK and native bridge assets, hashes every -file, and emits a JSON receipt. It does not start, inspect, attach to, configure, or modify Cheat Engine or a target -process. - -```powershell -pwsh .\eng\Invoke-LivePluginCoexistenceFixture.ps1 -Build -``` - -The output contains three disjoint directories and `coexistence-build-receipt.json`. Preserve all three directories -and the receipt. To prepare a future exact SDK package tuple, pass its version independently for each plugin, point -NuGet at the approved package source through the normal restore configuration, and retain the resulting receipt: - -```powershell -pwsh .\eng\Invoke-LivePluginCoexistenceFixture.ps1 -Build ` - -PluginASdkVersion ` - -PluginBSdkVersion ` - -PluginCollisionSdkVersion -``` +The current checkout does not include an automated bundle-preparation runner. Do not infer that a live fixture or a +receipt exists from this document. For a future exact SDK package tuple, prepare three disjoint output directories +with an approved harness, record the package and assembly hashes, and retain the complete dependency closure and +host transcript before loading Cheat Engine. Different requested package versions only make a side-by-side live run eligible. The receipt's assembly identities, package content hashes, full closures, and host transcript must still establish what the exact Cheat Engine loader did. diff --git a/tests/CheatEngine.Client.SourceGenerators.Lua.Tests/CheatEngineLuaGeneratorTests.cs b/tests/CheatEngine.Client.SourceGenerators.Lua.Tests/CheatEngineLuaGeneratorTests.cs index 1e97831..15d1a2d 100644 --- a/tests/CheatEngine.Client.SourceGenerators.Lua.Tests/CheatEngineLuaGeneratorTests.cs +++ b/tests/CheatEngine.Client.SourceGenerators.Lua.Tests/CheatEngineLuaGeneratorTests.cs @@ -118,24 +118,24 @@ public void ModuleAdapterSnapshotUsesOneAdmittedOperationAndPreflightsEveryExpor string generated = run.GeneratedText("PluginLuaModule.CheatEngineLuaModule.g.cs"); Assert.Equal( NormalizeLineEndings(""" - // - #nullable enable + // + #nullable enable - namespace TestPlugin; + namespace TestPlugin; - internal partial class PluginLuaModule : global::CheatEngine.Client.Lua.IDescribedLuaModule - { - /// Initializes a Lua module instance for activation-scoped dependency injection. - public PluginLuaModule() - { - } + internal partial class PluginLuaModule : global::CheatEngine.Client.Lua.IDescribedLuaModule + { + /// Initializes a Lua module instance for activation-scoped dependency injection. + public PluginLuaModule() + { + } - private static readonly global::CheatEngine.Client.Lua.LuaModuleDescriptor s_descriptor = - new global::CheatEngine.Client.Lua.LuaModuleDescriptor( - "plugin", - global::System.Collections.Immutable.ImmutableArray.Create(new global::CheatEngine.Client.Lua.LuaExportDescriptor("status"), new global::CheatEngine.Client.Lua.LuaExportDescriptor("ping"))); + private static readonly global::CheatEngine.Client.Lua.LuaModuleDescriptor s_descriptor = + new global::CheatEngine.Client.Lua.LuaModuleDescriptor( + "plugin", + global::System.Collections.Immutable.ImmutableArray.Create(new global::CheatEngine.Client.Lua.LuaExportDescriptor("status"), new global::CheatEngine.Client.Lua.LuaExportDescriptor("ping"))); - """ + "\n"), + """ + "\n"), NormalizeLineEndings(generated[..generated.IndexOf("\t/// ", StringComparison.Ordinal)])); Assert.Equal(2, Count(generated, "LuaRuntime.AcquireOperation()")); Assert.Contains("EnsureExportsAreVacant(operation.State);", generated, StringComparison.Ordinal); @@ -289,6 +289,89 @@ public void InvalidModuleFormsProduceActionableDiagnostics(string moduleDeclarat Assert.Empty(run.GeneratedSources); } + [Theory] + [InlineData("abstract", "concrete")] + [InlineData("file", "file-local")] + public void AbstractOrFileLocalModulesProduceDeterministicShapeDiagnostics(string modifier, + string expectedMessageFragment) + { + string source = ModulePrefix + + "[CheatEngineLuaModule(typeof(PluginLuaBindings), \"plugin\")] " + modifier + + " partial class PluginLuaModule { }"; + + Diagnostic first = Assert.Single(GeneratorRun.Execute(source).Diagnostics + .Where(static diagnostic => diagnostic.Id == "CECLUA1001")); + Diagnostic second = Assert.Single(GeneratorRun.Execute(source).Diagnostics + .Where(static diagnostic => diagnostic.Id == "CECLUA1001")); + + Assert.Equal(first.GetMessage(CultureInfo.InvariantCulture), second.GetMessage(CultureInfo.InvariantCulture)); + Assert.Contains(expectedMessageFragment, first.GetMessage(CultureInfo.InvariantCulture), + StringComparison.Ordinal); + } + + [Theory] + [InlineData("file static partial class FileLuaBindings", "typeof(FileLuaBindings)", "file-local")] + [InlineData("internal static partial class GenericLuaBindings", "typeof(GenericLuaBindings<>)", "non-generic")] + public void FileLocalOrGenericBindingsProduceDeterministicDiagnostics(string bindingsDeclaration, + string bindingsType, string expectedMessageFragment) + { + string source = + $$""" + using CheatEngine.Client.Lua; + using CheatEngine.SDK.Annotations.Lua; + namespace TestPlugin; + {{bindingsDeclaration}} + { + [LuaFunction("status")] + public static string Status() => "ok"; + } + + [CheatEngineLuaModule({{bindingsType}}, "plugin")] + internal sealed partial class PluginLuaModule + { + } + """; + + Diagnostic first = Assert.Single(GeneratorRun.Execute(source).Diagnostics + .Where(static diagnostic => diagnostic.Id == "CECLUA1002")); + Diagnostic second = Assert.Single(GeneratorRun.Execute(source).Diagnostics + .Where(static diagnostic => diagnostic.Id == "CECLUA1002")); + + Assert.Equal(first.GetMessage(CultureInfo.InvariantCulture), second.GetMessage(CultureInfo.InvariantCulture)); + Assert.Contains(expectedMessageFragment, first.GetMessage(CultureInfo.InvariantCulture), + StringComparison.Ordinal); + } + + [Theory] + [InlineData("file static partial class Globals", "file-local")] + [InlineData("internal static partial class Globals", "non-generic")] + public void FileLocalOrGenericOperationContainersProduceDeterministicDiagnostics(string containerDeclaration, + string expectedMessageFragment) + { + string source = + $$""" + using CheatEngine.Client.Lua; + using CheatEngine.SDK.Annotations.Lua; + namespace TestPlugin; + {{containerDeclaration}} + { + [CheatEngineLuaOperation] + [LuaGlobal("readVersion")] + public static partial int ReadVersion(); + } + """; + + Diagnostic first = Assert.Single(GeneratorRun.Execute(source).Diagnostics + .Where(static diagnostic => diagnostic.Id == "CECLUA1101")); + Diagnostic second = Assert.Single(GeneratorRun.Execute(source).Diagnostics + .Where(static diagnostic => diagnostic.Id == "CECLUA1101")); + + Assert.Equal(first.GetMessage(CultureInfo.InvariantCulture), second.GetMessage(CultureInfo.InvariantCulture)); + Assert.Contains(expectedMessageFragment, first.GetMessage(CultureInfo.InvariantCulture), + StringComparison.Ordinal); + Assert.Empty(GeneratorRun.Execute(source).GeneratedSources); + } + [Fact] public void DuplicateModuleExportsProduceAnActionableDiagnostic() { @@ -380,19 +463,77 @@ public void MapperRejectsForbiddenTypesNestedInArraysGenericsTuplesAndDtos() CreateMappedOperationSource("UnsafeCallback", "internal delegate void UnsafeCallback(global::CheatEngine.SDK.Lua.References.LuaRef reference);"), CreateMappedOperationSource("ConstrainedSnapshot", - "internal interface IUnsafeConstraint { global::CheatEngine.SDK.Lua.References.LuaRef Reference { get; } }" + Environment.NewLine + + "internal interface IUnsafeConstraint { global::CheatEngine.SDK.Lua.References.LuaRef Reference { get; } }" + + Environment.NewLine + "internal sealed class ConstrainedSnapshot where T : IUnsafeConstraint { }") ]; foreach (string source in sources) { GeneratorRun run = GeneratorRun.Execute(source); - Diagnostic diagnostic = Assert.Single(run.Diagnostics.Where(static candidate => candidate.Id == "CECLUA1106")); + Diagnostic diagnostic = + Assert.Single(run.Diagnostics.Where(static candidate => candidate.Id == "CECLUA1106")); Assert.Equal("CECLUA1106", diagnostic.Id); Assert.Empty(run.GeneratedSources); } } + [Fact] + public void MapperRejectsOpaqueFrameworkAndReflectionTypesEvenInsideApprovedCollections() + { + (string ResultType, string ExpectedType)[] cases = + [ + ("global::System.Collections.ArrayList", "System.Collections.ArrayList"), + ("global::System.Collections.IEnumerable", "System.Collections.IEnumerable"), + ("global::System.Runtime.InteropServices.GCHandle", "System.Runtime.InteropServices.GCHandle"), + ("global::Microsoft.Win32.SafeHandles.SafeFileHandle", "Microsoft.Win32.SafeHandles.SafeFileHandle"), + ("global::System.Reflection.MemberInfo", "System.Reflection.MemberInfo"), + ("global::System.Buffers.IMemoryOwner", "System.Buffers.IMemoryOwner"), + ("global::System.Collections.Immutable.ImmutableArray", + "Microsoft.Win32.SafeHandles.SafeFileHandle") + ]; + + foreach ((string resultType, string expectedType) in cases) + { + GeneratorRun run = GeneratorRun.Execute(CreateMappedOperationSource(resultType)); + Diagnostic diagnostic = + Assert.Single(run.Diagnostics.Where(static candidate => candidate.Id == "CECLUA1106")); + Assert.Contains(expectedType, diagnostic.GetMessage(CultureInfo.InvariantCulture), + StringComparison.Ordinal); + Assert.Empty(run.GeneratedSources); + } + } + + [Fact] + public void MapperAllowsClosedImmutableAndReadOnlyFrameworkCollections() + { + string[] resultTypes = + [ + "global::System.Collections.Generic.IEnumerable", + "global::System.Collections.Generic.IReadOnlyList>", + "global::System.Collections.Immutable.ImmutableDictionary>", + "global::System.Collections.ObjectModel.ReadOnlyDictionary" + ]; + + foreach (string resultType in resultTypes) + { + GeneratorRun run = GeneratorRun.Execute(CreateMappedOperationSource(resultType)); + Assert.Empty(run.Diagnostics); + Assert.Single(run.GeneratedSources); + Compilation generatedConsumer = run.OutputCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText( + """ + namespace TestPlugin; + internal static partial class Globals + { + public static partial SdkSnapshot GetUnsafe() => new(); + } + """, + new CSharpParseOptions(LanguageVersion.CSharp14), + cancellationToken: TestContext.Current.CancellationToken)); + AssertNoCompilerDiagnostics(generatedConsumer); + } + } + [Fact] public void RecursiveBoundaryDiagnosticsAreDeterministic() { @@ -406,10 +547,10 @@ public void RecursiveBoundaryDiagnosticsAreDeterministic() Diagnostic second = Assert.Single(GeneratorRun.Execute(source).Diagnostics .Where(static candidate => candidate.Id == "CECLUA1106")); - Assert.Equal(first.GetMessage(System.Globalization.CultureInfo.InvariantCulture), - second.GetMessage(System.Globalization.CultureInfo.InvariantCulture)); + Assert.Equal(first.GetMessage(CultureInfo.InvariantCulture), + second.GetMessage(CultureInfo.InvariantCulture)); Assert.Contains("forbidden SDK lifetime or interop type", - first.GetMessage(System.Globalization.CultureInfo.InvariantCulture), StringComparison.Ordinal); + first.GetMessage(CultureInfo.InvariantCulture), StringComparison.Ordinal); } [Fact] @@ -540,24 +681,12 @@ private static string CreateMappedOperationSource(string resultType, string? add string sourceType = "SdkSnapshot") { return string.Join( - Environment.NewLine, - [ - "using CheatEngine.Client.Lua;", - "using CheatEngine.SDK.Annotations.Lua;", - "namespace TestPlugin;", - "internal sealed class SdkSnapshot { }", - additionalDeclarations ?? string.Empty, - "internal readonly struct UnsafeMapper : ILuaResultMapper<" + sourceType + ", " + resultType + ">", - "{", - "\tpublic static " + resultType + " Map(" + sourceType + " source) => default;", - "}", - "internal static partial class Globals", - "{", - "\t[CheatEngineLuaOperation(typeof(UnsafeMapper))]", - "\t[LuaGlobal(\"unsafe\")]", - "\tpublic static partial " + sourceType + " GetUnsafe();", - "}" - ]); + Environment.NewLine, "using CheatEngine.Client.Lua;", "using CheatEngine.SDK.Annotations.Lua;", + "namespace TestPlugin;", "internal sealed class SdkSnapshot { }", additionalDeclarations ?? string.Empty, + "internal readonly struct UnsafeMapper : ILuaResultMapper<" + sourceType + ", " + resultType + ">", "{", + "\tpublic static " + resultType + " Map(" + sourceType + " source) => default;", "}", + "internal static partial class Globals", "{", "\t[CheatEngineLuaOperation(typeof(UnsafeMapper))]", + "\t[LuaGlobal(\"unsafe\")]", "\tpublic static partial " + sourceType + " GetUnsafe();", "}"); } private static void AssertNoCompilerDiagnostics(Compilation compilation) diff --git a/tests/CheatEngine.Client.Tests/Infrastructure/DotNetProcess.cs b/tests/CheatEngine.Client.Tests/Infrastructure/DotNetProcess.cs new file mode 100644 index 0000000..237f0d6 --- /dev/null +++ b/tests/CheatEngine.Client.Tests/Infrastructure/DotNetProcess.cs @@ -0,0 +1,79 @@ +using System.Diagnostics; +using System.Text; + +namespace CheatEngine.Client.Tests.Infrastructure; + +/// Runs the pinned dotnet command without shell quoting or shared CLI state. +internal static class DotNetProcess +{ + private static readonly TimeSpan Timeout = TimeSpan.FromMinutes(10); + + internal static Task RunAsync(string workingDirectory, params string[] arguments) + { + return RunAsync(workingDirectory, new Dictionary(StringComparer.Ordinal), arguments); + } + + internal static async Task RunAsync(string workingDirectory, + IReadOnlyDictionary environment, params string[] arguments) + { + ArgumentException.ThrowIfNullOrWhiteSpace(workingDirectory); + ArgumentNullException.ThrowIfNull(environment); + ArgumentNullException.ThrowIfNull(arguments); + + ProcessStartInfo startInfo = new("dotnet") + { + WorkingDirectory = workingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true, + StandardOutputEncoding = Encoding.UTF8, + StandardErrorEncoding = Encoding.UTF8, + UseShellExecute = false + }; + foreach (string argument in arguments) + { + startInfo.ArgumentList.Add(argument); + } + + foreach ((string name, string value) in environment) + { + startInfo.Environment[name] = value; + } + + using Process process = new() { StartInfo = startInfo }; + if (!process.Start()) + { + throw new InvalidOperationException("The dotnet process did not start."); + } + + Task standardOutput = process.StandardOutput.ReadToEndAsync(); + Task standardError = process.StandardError.ReadToEndAsync(); + using CancellationTokenSource cancellation = new(Timeout); + try + { + await process.WaitForExitAsync(cancellation.Token); + } + catch (OperationCanceledException) when (cancellation.IsCancellationRequested) + { + process.Kill(true); + await process.WaitForExitAsync(); + throw new TimeoutException($"dotnet {string.Join(' ', arguments)} exceeded {Timeout}."); + } + + return new DotNetProcessResult(arguments, process.ExitCode, await standardOutput, await standardError); + } +} + +/// Captures a completed dotnet invocation for assertion diagnostics. +internal sealed record DotNetProcessResult( + IReadOnlyList Arguments, + int ExitCode, + string StandardOutput, + string StandardError) +{ + public override string ToString() + { + return $"dotnet {string.Join(' ', Arguments)} exited with {ExitCode}.{Environment.NewLine}" + + $"stdout:{Environment.NewLine}{StandardOutput}{Environment.NewLine}" + + $"stderr:{Environment.NewLine}{StandardError}"; + } +} diff --git a/tests/CheatEngine.Client.Tests/Infrastructure/TemporaryDirectory.cs b/tests/CheatEngine.Client.Tests/Infrastructure/TemporaryDirectory.cs new file mode 100644 index 0000000..56081c5 --- /dev/null +++ b/tests/CheatEngine.Client.Tests/Infrastructure/TemporaryDirectory.cs @@ -0,0 +1,49 @@ +namespace CheatEngine.Client.Tests.Infrastructure; + +/// Creates and removes a uniquely owned directory below the operating system temporary directory. +internal sealed class TemporaryDirectory : IDisposable +{ + private readonly string _root; + + internal TemporaryDirectory(string purpose) + { + ArgumentException.ThrowIfNullOrWhiteSpace(purpose); + + _root = System.IO.Path.GetFullPath(System.IO.Path.Combine(System.IO.Path.GetTempPath(), + "CheatEngine.Client.Tests")); + Path = System.IO.Path.GetFullPath(System.IO.Path.Combine(_root, purpose, Guid.NewGuid().ToString("N"))); + if (!Path.StartsWith(_root + System.IO.Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException($"Temporary directory '{Path}' is outside '{_root}'."); + } + + Directory.CreateDirectory(Path); + } + + internal string Path + { + get; + } + + public void Dispose() + { + if (Directory.Exists(Path)) + { + Directory.Delete(Path, true); + } + } + + internal string CreateDirectory(string name) + { + ArgumentException.ThrowIfNullOrWhiteSpace(name); + + string directory = System.IO.Path.GetFullPath(System.IO.Path.Combine(Path, name)); + if (!directory.StartsWith(Path + System.IO.Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException($"Temporary child directory '{directory}' is outside '{Path}'."); + } + + Directory.CreateDirectory(directory); + return directory; + } +} diff --git a/tests/CheatEngine.Client.Tests/Packaging/PackageConsumptionSmokeTests.cs b/tests/CheatEngine.Client.Tests/Packaging/PackageConsumptionSmokeTests.cs new file mode 100644 index 0000000..4ec58ee --- /dev/null +++ b/tests/CheatEngine.Client.Tests/Packaging/PackageConsumptionSmokeTests.cs @@ -0,0 +1,316 @@ +using System.IO.Compression; +using System.Security; +using System.Text; +using System.Xml.Linq; + +using CheatEngine.Client.Tests.Infrastructure; + +namespace CheatEngine.Client.Tests.Packaging; + +[CollectionDefinition(Name, DisableParallelization = true)] +public sealed class PackageConsumptionSmokeSerialGroup +{ + public const string Name = "Package consumption smoke"; +} + +/// Exercises the packages that plugin authors consume, outside the repository's project graph. +[Collection(PackageConsumptionSmokeSerialGroup.Name)] +public sealed class PackageConsumptionSmokeTests +{ + private const string PackageSourceEnvironmentVariable = "CHEATENGINE_CLIENT_PACKAGE_SOURCE"; + private const string ClientPackageId = "CheatEngine.Client"; + private const string HostingPackageId = "CheatEngine.Client.Hosting"; + private const string TemplatePackageId = "CheatEngine.Client.Templates"; + + private const string ConsumerSource = """ + using CheatEngine.Client; + using CheatEngine.Client.Hosting; + using CheatEngine.Client.Memory; + using CheatEngine.Client.Scanning; + using CheatEngine.SDK.Annotations.Plugin; + using CheatEngine.SDK.Engine.Values; + + [CheatEnginePlugin("Package smoke plugin")] + public sealed class Plugin : CheatEngineClientPlugin + { + protected override void Configure(CheatEnginePluginBuilder builder) + { + } + + protected override void OnClientEnabled(ICheatEngineClient client) + { + _ = client.Memory.At(default(Address)); + _ = client.Patterns.Aob("00").FirstOrNone(); + } + } + """; + + [Fact] + public async Task PackagedClientAndTemplateCanBeInstalledInstantiatedAndBuiltInIsolatedDirectories() + { + using TemporaryDirectory temporary = new("PackageConsumptionSmoke"); + string packageSource = await ResolvePackageSourceAsync(temporary); + + PackageArchive clientPackage = FindPackage(packageSource, ClientPackageId); + PackageArchive hostingPackage = FindPackage(packageSource, HostingPackageId); + PackageArchive templatePackage = FindPackage(packageSource, TemplatePackageId); + AssertArchiveContains(hostingPackage.Path, + "analyzers/dotnet/cs/CheatEngine.Client.SourceGenerators.Lua.dll", + "buildTransitive/CheatEngine.Client.Hosting.props", + "buildTransitive/CheatEngine.Client.Hosting.targets"); + AssertArchiveContains(templatePackage.Path, + "content/CheatEngine.Plugin/.template.config/template.json", + "content/CheatEngine.Plugin/CheatEngine.Plugin.csproj", + "content/CheatEngine.Plugin/Modules/PluginClientModule.cs", + "content/CheatEngine.Plugin/Modules/PluginLuaModule.cs"); + + string nuGetConfiguration = WriteNuGetConfiguration(temporary, packageSource); + await BuildIsolatedPackageConsumerAsync(temporary, nuGetConfiguration, clientPackage.Version); + await InstallInstantiateAndBuildTemplateAsync(temporary, nuGetConfiguration, templatePackage.Path); + } + + [Fact] + public async Task PackagedClientPluginWithoutDirectSdkReferenceReportsCECLIENT001() + { + using TemporaryDirectory temporary = new("PackageConsumptionSmoke"); + string packageSource = await ResolvePackageSourceAsync(temporary); + + PackageArchive clientPackage = FindPackage(packageSource, ClientPackageId); + string nuGetConfiguration = WriteNuGetConfiguration(temporary, packageSource); + string consumerDirectory = temporary.CreateDirectory("missing-sdk-package-consumer"); + string projectPath = Path.Combine(consumerDirectory, "MissingSdk.Plugin.csproj"); + await File.WriteAllTextAsync(projectPath, + CreateConsumerProject(clientPackage.Version, false), new UTF8Encoding(false), + TestContext.Current.CancellationToken); + + await AssertDotNetSuccessAsync(consumerDirectory, "restore", projectPath, "--configfile", nuGetConfiguration); + + DotNetProcessResult buildResult = await DotNetProcess.RunAsync(consumerDirectory, + "build", projectPath, "--configuration", "Release", "--no-restore"); + Assert.True(buildResult.ExitCode != 0, buildResult.ToString()); + Assert.Contains("CECLIENT001", buildResult.StandardOutput + buildResult.StandardError, + StringComparison.Ordinal); + } + + private static async Task BuildIsolatedPackageConsumerAsync(TemporaryDirectory temporary, string nuGetConfiguration, + string clientVersion) + { + string consumerDirectory = temporary.CreateDirectory("package-consumer"); + string projectPath = Path.Combine(consumerDirectory, "Smoke.Plugin.csproj"); + await File.WriteAllTextAsync(projectPath, CreateConsumerProject(clientVersion), new UTF8Encoding(false)); + await File.WriteAllTextAsync(Path.Combine(consumerDirectory, "Plugin.cs"), ConsumerSource, + new UTF8Encoding(false)); + + await AssertDotNetSuccessAsync(consumerDirectory, "restore", projectPath, "--configfile", nuGetConfiguration); + + string deploymentDirectory = temporary.CreateDirectory("deployment"); + await AssertDotNetSuccessAsync(consumerDirectory, "build", projectPath, "--configuration", "Release", + "--no-restore", + $"-p:CheatEnginePluginOutputPath={deploymentDirectory}"); + await AssertDotNetSuccessAsync(consumerDirectory, "build", projectPath, "--configuration", "Release", + "--no-restore", + $"-p:CheatEnginePluginOutputPath={deploymentDirectory}"); + + string outputDirectory = Path.Combine(consumerDirectory, "bin", "Release", "net10.0"); + string[] requiredAssets = + [ + "Smoke.Plugin.dll", + "Smoke.Plugin.deps.json", + "Smoke.Plugin.runtimeconfig.json", + "cheatengine-sdk-lua-bridge.dll", + "CheatEngine.SDK.dll", + "CheatEngine.Client.Abstractions.dll", + "CheatEngine.Client.Core.dll", + "CheatEngine.Client.Fluent.dll", + "CheatEngine.Client.Extensions.DependencyInjection.dll", + "CheatEngine.Client.Hosting.dll" + ]; + Assert.All(requiredAssets, asset => + { + Assert.True(File.Exists(Path.Combine(outputDirectory, asset)), + $"Isolated package-consumer output is missing '{asset}'."); + Assert.True(File.Exists(Path.Combine(deploymentDirectory, asset)), + $"Isolated plugin deployment is missing '{asset}'."); + }); + + string[] generatedEntryPoints = Directory.GetFiles(Path.Combine(consumerDirectory, "obj"), + "CheatEngine.SDK.EntryPoint.g.cs", SearchOption.AllDirectories); + string generatedEntryPoint = Assert.Single(generatedEntryPoints); + string generatedEntryPointText = await File.ReadAllTextAsync(generatedEntryPoint); + Assert.Contains("namespace CESDK", generatedEntryPointText, StringComparison.Ordinal); + Assert.Contains("CEPluginInitialize", generatedEntryPointText, StringComparison.Ordinal); + } + + private static async Task InstallInstantiateAndBuildTemplateAsync(TemporaryDirectory temporary, + string nuGetConfiguration, + string templatePackage) + { + string templateHome = temporary.CreateDirectory("template-home"); + IReadOnlyDictionary environment = new Dictionary(StringComparer.Ordinal) + { + ["DOTNET_CLI_HOME"] = Path.Combine(templateHome, ".dotnet-cli"), + ["DOTNET_NEW_HOME"] = Path.Combine(templateHome, ".template-engine") + }; + + await AssertDotNetSuccessAsync(templateHome, environment, "new", "install", templatePackage, "--force"); + await AssertDotNetSuccessAsync(templateHome, environment, "new", "ceplugin", "--dry-run", "--name", + "Smoke.Plugin", + "--output", Path.Combine(templateHome, "dry-run")); + + string instantiatedDirectory = Path.Combine(templateHome, "Smoke.Plugin"); + await AssertDotNetSuccessAsync(templateHome, environment, "new", "ceplugin", "--name", "Smoke.Plugin", + "--output", + instantiatedDirectory); + + string projectPath = Path.Combine(instantiatedDirectory, "Smoke.Plugin.csproj"); + Assert.True(File.Exists(projectPath), "Template instantiation did not produce the expected plugin project."); + await AssertDotNetSuccessAsync(instantiatedDirectory, environment, "restore", projectPath, "--configfile", + nuGetConfiguration); + await AssertDotNetSuccessAsync(instantiatedDirectory, environment, "build", projectPath, "--configuration", + "Release", + "--no-restore"); + } + + private static async Task AssertDotNetSuccessAsync(string workingDirectory, params string[] arguments) + { + await AssertDotNetSuccessAsync(workingDirectory, new Dictionary(StringComparer.Ordinal), + arguments); + } + + private static async Task AssertDotNetSuccessAsync(string workingDirectory, + IReadOnlyDictionary environment, + params string[] arguments) + { + DotNetProcessResult result = await DotNetProcess.RunAsync(workingDirectory, environment, arguments); + Assert.True(result.ExitCode == 0, result.ToString()); + } + + private static async Task ResolvePackageSourceAsync(TemporaryDirectory temporary) + { + string? configuredPackageSource = Environment.GetEnvironmentVariable(PackageSourceEnvironmentVariable); + if (configuredPackageSource is not null) + { + Assert.False(string.IsNullOrWhiteSpace(configuredPackageSource), + $"{PackageSourceEnvironmentVariable} is set but empty. " + + "It must be an absolute directory containing prebuilt .nupkg files."); + Assert.True(Path.IsPathFullyQualified(configuredPackageSource), + $"{PackageSourceEnvironmentVariable} must be an absolute directory path, but was '{configuredPackageSource}'."); + Assert.True(Directory.Exists(configuredPackageSource), + $"{PackageSourceEnvironmentVariable} points to a missing directory: '{configuredPackageSource}'."); + return configuredPackageSource; + } + + string repositoryRoot = FindRepositoryRoot(); + string packageSource = temporary.CreateDirectory("packages"); + await AssertDotNetSuccessAsync(repositoryRoot, + "pack", Path.Combine(repositoryRoot, "CheatEngine.Client.slnx"), "--configuration", "Release", + "--output", packageSource); + return packageSource; + } + + private static PackageArchive FindPackage(string packageSource, string packageId) + { + PackageArchive[] packages = Directory.GetFiles(packageSource, "*.nupkg") + .Select(ReadPackageArchive) + .ToArray(); + return Assert.Single(packages, package => package.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase)); + } + + private static PackageArchive ReadPackageArchive(string packagePath) + { + using ZipArchive archive = ZipFile.OpenRead(packagePath); + ZipArchiveEntry nuspec = Assert.Single(archive.Entries, + static entry => entry.FullName.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase)); + using Stream stream = nuspec.Open(); + XDocument document = XDocument.Load(stream); + XNamespace packageNamespace = document.Root!.Name.Namespace; + XElement metadata = document.Root.Element(packageNamespace + "metadata") + ?? throw new InvalidOperationException( + $"Package '{packagePath}' does not declare metadata."); + string id = metadata.Element(packageNamespace + "id")?.Value + ?? throw new InvalidOperationException($"Package '{packagePath}' does not declare an id."); + string version = metadata.Element(packageNamespace + "version")?.Value + ?? throw new InvalidOperationException($"Package '{packagePath}' does not declare a version."); + return new PackageArchive(id, packagePath, version); + } + + private static void AssertArchiveContains(string packagePath, params string[] expectedEntries) + { + using ZipArchive archive = ZipFile.OpenRead(packagePath); + HashSet entries = archive.Entries.Select(static entry => entry.FullName) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + Assert.All(expectedEntries, entry => Assert.Contains(entry, entries, StringComparer.OrdinalIgnoreCase)); + } + + private static string WriteNuGetConfiguration(TemporaryDirectory temporary, string packageSource) + { + string packageCache = temporary.CreateDirectory("packages-cache"); + string path = Path.Combine(temporary.Path, "NuGet.Config"); + string configuration = $""" + + + + + + + + + + + + """; + File.WriteAllText(path, configuration, new UTF8Encoding(false)); + return path; + } + + private static string EscapeXml(string value) + { + return SecurityElement.Escape(value) ?? + throw new InvalidOperationException("Could not escape NuGet configuration."); + } + + private static string FindRepositoryRoot() + { + for (DirectoryInfo? candidate = new(AppContext.BaseDirectory); + candidate is not null; + candidate = candidate.Parent) + { + if (File.Exists(Path.Combine(candidate.FullName, "CheatEngine.Client.slnx"))) + { + return candidate.FullName; + } + } + + throw new DirectoryNotFoundException( + "Could not find the CheatEngine.Client repository root from the test output."); + } + + private static string CreateConsumerProject(string clientVersion, bool hasDirectSdkPackageReference = true) + { + string sdkPackageReference = hasDirectSdkPackageReference + ? " " + : string.Empty; + + return $$""" + + + net10.0 + 14.0 + enable + enable + x64 + true + false + true + obj/Generated + + + + {{sdkPackageReference}} + + + """; + } + + private sealed record PackageArchive(string Id, string Path, string Version); +} diff --git a/tests/CheatEngine.Client.Tests/PublicClientSignatureBoundaryTests.cs b/tests/CheatEngine.Client.Tests/PublicClientSignatureBoundaryTests.cs index aebb012..6619db0 100644 --- a/tests/CheatEngine.Client.Tests/PublicClientSignatureBoundaryTests.cs +++ b/tests/CheatEngine.Client.Tests/PublicClientSignatureBoundaryTests.cs @@ -1,11 +1,17 @@ +using System.Collections; +using System.Collections.Immutable; using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.InteropServices; using CheatEngine.Client.Extensions.DependencyInjection; using CheatEngine.Client.Hosting; using CheatEngine.Client.Memory; using CheatEngine.SDK.Engine.Objects; +using CheatEngine.SDK.Engine.Values; using CheatEngine.SDK.Lua.References; -using CheatEngine.SDK.Lua.State; + +using Microsoft.Win32.SafeHandles; using ReflectionAssembly = System.Reflection.Assembly; @@ -14,6 +20,9 @@ namespace CheatEngine.Client.Tests; /// Protects the aggregate package boundary from raw SDK lifetime and Lua implementation types. public sealed class PublicClientSignatureBoundaryTests { + private const int ClientBoundaryMaximumDepth = 32; + private const int ClientBoundaryMaximumNodes = 256; + private static readonly HashSet ApprovedSdkValueTypes = new(StringComparer.Ordinal) { "CheatEngine.SDK.Engine.AddressList.MemoryRecordId", @@ -40,6 +49,73 @@ public sealed class PublicClientSignatureBoundaryTests "CheatEngine.SDK.Engine.Values.Address" }; + private static readonly HashSet ApprovedFrameworkValueTypes = new(StringComparer.Ordinal) + { + "System.Action", + "System.Attribute", + "System.DateOnly", + "System.DateTime", + "System.DateTimeOffset", + "System.Decimal", + "System.Exception", + "System.Guid", + "System.Half", + "System.Index", + "System.Int128", + "System.Range", + "System.Text.Rune", + "System.Threading.CancellationToken", + "System.TimeOnly", + "System.TimeSpan", + "System.UInt128", + "System.Version" + }; + + private static readonly HashSet ApprovedFrameworkGenericCollectionTypes = new(StringComparer.Ordinal) + { + "System.Collections.Frozen.FrozenDictionary`2", + "System.Collections.Frozen.FrozenSet`1", + "System.Collections.Generic.IAsyncEnumerable`1", + "System.Collections.Generic.IEnumerable`1", + "System.Collections.Generic.IReadOnlyCollection`1", + "System.Collections.Generic.IReadOnlyDictionary`2", + "System.Collections.Generic.IReadOnlyList`1", + "System.Collections.Generic.IReadOnlySet`1", + "System.Collections.Immutable.IImmutableDictionary`2", + "System.Collections.Immutable.IImmutableList`1", + "System.Collections.Immutable.IImmutableQueue`1", + "System.Collections.Immutable.IImmutableSet`1", + "System.Collections.Immutable.IImmutableStack`1", + "System.Collections.Immutable.ImmutableArray`1", + "System.Collections.Immutable.ImmutableDictionary`2", + "System.Collections.Immutable.ImmutableHashSet`1", + "System.Collections.Immutable.ImmutableList`1", + "System.Collections.Immutable.ImmutableQueue`1", + "System.Collections.Immutable.ImmutableSortedDictionary`2", + "System.Collections.Immutable.ImmutableSortedSet`1", + "System.Collections.Immutable.ImmutableStack`1", + "System.Collections.ObjectModel.ReadOnlyCollection`1", + "System.Collections.ObjectModel.ReadOnlyDictionary`2", + "System.Action`1", + "System.Func`1", + "System.Nullable`1", + "System.ReadOnlySpan`1", + "System.Span`1" + }; + + private static readonly HashSet ApprovedFrameworkIntegrationTypes = new(StringComparer.Ordinal) + { + "Microsoft.Extensions.Configuration.ConfigurationManager", + "Microsoft.Extensions.Configuration.IConfiguration", + "Microsoft.Extensions.Configuration.IConfigurationRoot", + "Microsoft.Extensions.Configuration.IConfigurationSection", + "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Microsoft.Extensions.DependencyInjection.IServiceScope", + "Microsoft.Extensions.DependencyInjection.ServiceProvider", + "Microsoft.Extensions.Logging.ILogger", + "Microsoft.Extensions.Options.ValidateOptionsResult" + }; + [Fact] public void AllPublicClientSignaturesAreHandleFreeAndUseOnlyApprovedSdkValueTypes() { @@ -69,6 +145,17 @@ public void RecursiveVerifierRejectsNestedSdkHandlesOwnershipDelegatesAndConstra AssertViolation(typeof(ConstrainedDto<>), "forbidden SDK handle"); } + [Fact] + public void RecursiveVerifierRejectsOpaqueFrameworkAndReflectionTypes() + { + AssertViolation(typeof(ArrayList), "unsupported framework type"); + AssertViolation(typeof(IEnumerable), "unsupported framework type"); + AssertViolation(typeof(GCHandle), "interop namespace type"); + AssertViolation(typeof(SafeFileHandle), "unsupported framework type"); + AssertViolation(typeof(ReflectionAssembly), "unsupported framework type"); + AssertViolation(typeof(MemberInfo), "unsupported framework type"); + } + [Fact] public void RecursiveVerifierRejectsPointersByReferenceAndFunctionPointers() { @@ -85,13 +172,45 @@ public void RecursiveVerifierRejectsPointersByReferenceAndFunctionPointers() public void RecursiveVerifierAllowsApprovedSdkValuesInsideSafeContainers() { List violations = []; - VerifyType(typeof(CheatEngine.SDK.Engine.Values.Address[]), "approved array", violations); - VerifyType(typeof(IReadOnlyList), "approved generic", violations); - VerifyType(typeof((CheatEngine.SDK.Engine.Values.Address Address, int Version)), "approved tuple", violations); + VerifyType(typeof(Address[]), "approved array", violations); + VerifyType(typeof(IEnumerable
), "approved enumerable", violations); + VerifyType(typeof(IReadOnlyList
), "approved generic", violations); + VerifyType(typeof(ImmutableArray>), "approved immutable generic", violations); + VerifyType(typeof((Address Address, int Version)), "approved tuple", violations); Assert.Empty(violations); } + [Fact] + public void RecursiveVerifierFailsClosedWhenTheTypeGraphExceedsTheDepthBudget() + { + Type nested = typeof(Address); + for (int index = 0; index <= ClientBoundaryMaximumDepth; index++) + { + nested = typeof(IReadOnlyList<>).MakeGenericType(nested); + } + + AssertViolation(nested, "graph budget"); + } + + [Fact] + public void RecursiveVerifierFailsClosedWhenTheTypeGraphExceedsTheNodeBudget() + { + AssemblyBuilder assembly = AssemblyBuilder.DefineDynamicAssembly( + new AssemblyName("ClientBoundaryWideGraph"), AssemblyBuilderAccess.Run); + ModuleBuilder module = assembly.DefineDynamicModule("ClientBoundaryWideGraph"); + Type[] leaves = Enumerable.Range(0, ClientBoundaryMaximumNodes) + .Select(index => module.DefineType("Leaf" + index, TypeAttributes.Public).CreateType()!) + .ToArray(); + TypeBuilder root = module.DefineType("Root", TypeAttributes.Public); + foreach ((Type leaf, int index) in leaves.Select((leaf, index) => (leaf, index))) + { + root.DefineField("_leaf" + index, leaf, FieldAttributes.Private); + } + + AssertViolation(root.CreateType()!, "graph budget"); + } + private static IEnumerable GetAggregateClientAssemblies() { Queue pending = new( @@ -134,6 +253,17 @@ private static void VerifyDeclaredMembers(Type publicType, List violatio { const BindingFlags PublicDeclared = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; + if (typeof(Delegate).IsAssignableFrom(publicType)) + { + MethodInfo? invoke = publicType.GetMethod("Invoke", BindingFlags.Public | BindingFlags.Instance); + if (invoke is not null) + { + VerifyType(invoke.ReturnType, invoke.ToString(), violations, invoke); + VerifyParameters(invoke.GetParameters(), invoke, violations); + } + + return; + } foreach (ConstructorInfo constructor in publicType.GetConstructors(PublicDeclared)) { @@ -142,6 +272,11 @@ private static void VerifyDeclaredMembers(Type publicType, List violatio foreach (MethodInfo method in publicType.GetMethods(PublicDeclared)) { + if (IsObjectEqualityMethod(method)) + { + continue; + } + VerifyType(method.ReturnType, method.ToString(), violations, method); VerifyParameters(method.GetParameters(), method, violations); VerifyGenericParameterConstraints(method.GetGenericArguments(), method.ToString(), violations); @@ -169,35 +304,44 @@ private static void VerifyDeclaredMembers(Type publicType, List violatio private static void VerifyParameters(IEnumerable parameters, MemberInfo member, List violations) { - VerifyParameters(parameters, member, violations, [], 0); + HashSet visited = []; + int visitedCount = 0; + VerifyParameters(parameters, member, violations, visited, ref visitedCount, 0); } private static void VerifyParameters(IEnumerable parameters, MemberInfo member, - List violations, HashSet visited, int depth) + List violations, HashSet visited, ref int visitedCount, int depth) { foreach (ParameterInfo parameter in parameters) { VerifyType(parameter.ParameterType, $"{member} parameter '{parameter.Name}'", violations, member, visited, - depth + 1); + ref visitedCount, depth + 1); } } private static void VerifyGenericParameterConstraints(IEnumerable genericParameters, string? member, List violations) { - VerifyGenericParameterConstraints(genericParameters, member, violations, [], 0); + HashSet visited = []; + int visitedCount = 0; + VerifyGenericParameterConstraints(genericParameters, member, violations, visited, ref visitedCount, 0); } private static void VerifyGenericParameterConstraints(IEnumerable genericParameters, string? member, - List violations, HashSet visited, int depth) + List violations, HashSet visited, ref int visitedCount, int depth) { foreach (Type genericParameter in genericParameters.Where(static parameter => parameter.IsGenericParameter)) { foreach (Type constraint in genericParameter.GetGenericParameterConstraints() .OrderBy(static type => type.FullName, StringComparer.Ordinal)) { - VerifyType(constraint, $"{member} generic parameter '{genericParameter.Name}'", violations, null, visited, - depth + 1); + if (constraint == typeof(ValueType) || constraint == typeof(Enum)) + { + continue; + } + + VerifyType(constraint, $"{member} generic parameter '{genericParameter.Name}'", violations, null, + visited, ref visitedCount, depth + 1); } } } @@ -206,17 +350,35 @@ private static void VerifyType(Type? type, string? source, List violatio MemberInfo? declaringMember = null) { HashSet visited = []; - VerifyType(type, source, violations, declaringMember, visited, 0); + int visitedCount = 0; + VerifyType(type, source, violations, declaringMember, visited, ref visitedCount, 0); } private static void VerifyType(Type? type, string? source, List violations, MemberInfo? declaringMember, - HashSet visited, int depth) + HashSet visited, ref int visitedCount, int depth) { - if (type is null || depth > 32 || !visited.Add(type)) + if (type is null) { return; } + if (depth > ClientBoundaryMaximumDepth) + { + violations.Add($"{source} exceeds the supported Client result type graph budget."); + return; + } + + if (!visited.Add(type)) + { + return; + } + + if (++visitedCount > ClientBoundaryMaximumNodes) + { + violations.Add($"{source} exceeds the supported Client result type graph budget."); + return; + } + if (type.IsFunctionPointer) { violations.Add($"{source} exposes a function pointer type '{type}'."); @@ -231,23 +393,27 @@ private static void VerifyType(Type? type, string? source, List violatio if (type.IsByRef) { - VerifyType(type.GetElementType(), source, violations, declaringMember, visited, depth + 1); + VerifyType(type.GetElementType(), source, violations, declaringMember, visited, ref visitedCount, + depth + 1); return; } if (type.IsArray) { - VerifyType(type.GetElementType(), source, violations, declaringMember, visited, depth + 1); + VerifyType(type.GetElementType(), source, violations, declaringMember, visited, ref visitedCount, + depth + 1); return; } if (type.IsGenericParameter) { - VerifyGenericParameterConstraints(type.GetGenericParameterConstraints(), source, violations, visited, depth); + VerifyGenericParameterConstraints(type.GetGenericParameterConstraints(), source, violations, visited, + ref visitedCount, depth); return; } - if (IsForbiddenSdkType(type, source, violations, declaringMember)) + if (IsForbiddenSdkType(type, source, violations, declaringMember) || + IsUnsupportedFrameworkType(type, source, violations, declaringMember)) { return; } @@ -255,10 +421,11 @@ private static void VerifyType(Type? type, string? source, List violatio if (type.IsGenericType) { Type genericDefinition = type.GetGenericTypeDefinition(); - VerifyGenericParameterConstraints(genericDefinition.GetGenericArguments(), source, violations, visited, depth); + VerifyGenericParameterConstraints(genericDefinition.GetGenericArguments(), source, violations, visited, + ref visitedCount, depth); foreach (Type argument in type.GetGenericArguments()) { - VerifyType(argument, source, violations, declaringMember, visited, depth + 1); + VerifyType(argument, source, violations, declaringMember, visited, ref visitedCount, depth + 1); } } @@ -267,8 +434,10 @@ private static void VerifyType(Type? type, string? source, List violatio MethodInfo? invoke = type.GetMethod("Invoke", BindingFlags.Public | BindingFlags.Instance); if (invoke is not null) { - VerifyType(invoke.ReturnType, $"{source} delegate return", violations, invoke, visited, depth + 1); - VerifyParameters(invoke.GetParameters(), invoke, violations, visited, depth); + VerifyType(invoke.ReturnType, $"{source} delegate return", violations, invoke, visited, + ref visitedCount, + depth + 1); + VerifyParameters(invoke.GetParameters(), invoke, violations, visited, ref visitedCount, depth); } return; @@ -279,8 +448,8 @@ private static void VerifyType(Type? type, string? source, List violatio return; } - VerifyTypeHierarchy(type, source, violations, declaringMember, visited, depth); - VerifyTypeMembers(type, source, violations, visited, depth); + VerifyTypeHierarchy(type, source, violations, declaringMember, visited, ref visitedCount, depth); + VerifyTypeMembers(type, source, violations, visited, ref visitedCount, depth); } private static bool IsForbiddenSdkType(Type type, string? source, List violations, @@ -312,24 +481,82 @@ private static bool IsForbiddenSdkType(Type type, string? source, List v return false; } + private static bool IsUnsupportedFrameworkType(Type type, string? source, List violations, + MemberInfo? declaringMember) + { + Type definition = type.IsGenericType ? type.GetGenericTypeDefinition() : type; + if (!IsFrameworkType(definition) || IsApprovedFrameworkClientBoundaryType(definition) || + IsApprovedAttributeMetadataType(definition, declaringMember)) + { + return false; + } + + string typeName = definition.FullName ?? definition.Name; + violations.Add($"{source} exposes unsupported framework type '{typeName}'."); + return true; + } + + private static bool IsApprovedFrameworkClientBoundaryType(Type type) + { + Type definition = type.IsGenericType ? type.GetGenericTypeDefinition() : type; + string typeName = definition.FullName ?? definition.Name; + return IsScalar(type) || IsTuple(type) || ApprovedFrameworkValueTypes.Contains(typeName) || + ApprovedFrameworkGenericCollectionTypes.Contains(typeName) || + ApprovedFrameworkIntegrationTypes.Contains(typeName); + } + + private static bool IsApprovedAttributeMetadataType(Type type, MemberInfo? declaringMember) + { + return type == typeof(Type) && declaringMember?.DeclaringType is { } declaringType && + typeof(Attribute).IsAssignableFrom(declaringType); + } + + private static bool IsScalar(Type type) + { + return type.IsPrimitive || type == typeof(void) || type == typeof(string) || type == typeof(IntPtr) || + type == typeof(UIntPtr); + } + + private static bool IsTuple(Type type) + { + return type.FullName?.StartsWith("System.ValueTuple", StringComparison.Ordinal) == true; + } + + private static bool IsFrameworkType(Type type) + { + string? assemblyName = type.Assembly.GetName().Name; + return assemblyName is not null && + (assemblyName.StartsWith("System", StringComparison.Ordinal) || + assemblyName.StartsWith("Microsoft", StringComparison.Ordinal)); + } + private static bool ShouldInspectTypeMembers(Type type) { - return type.Assembly == typeof(PublicClientSignatureBoundaryTests).Assembly || - type.Assembly.GetName().Name?.StartsWith("CheatEngine.Client", StringComparison.Ordinal) == true; + return type.Assembly.IsDynamic || type.Assembly == typeof(PublicClientSignatureBoundaryTests).Assembly || + (type.IsValueType && + type.Assembly.GetName().Name?.StartsWith("CheatEngine.Client", StringComparison.Ordinal) == true); } private static void VerifyTypeHierarchy(Type type, string? source, List violations, - MemberInfo? declaringMember, HashSet visited, int depth) + MemberInfo? declaringMember, HashSet visited, ref int visitedCount, int depth) { - if (type.BaseType is { } baseType && baseType != typeof(object) && !IsRequiredPluginBase(baseType)) + if (type.BaseType is { } baseType && baseType != typeof(object) && baseType != typeof(ValueType) && + baseType != typeof(Enum) && !IsRequiredPluginBase(baseType)) { - VerifyType(baseType, $"{source} base type", violations, declaringMember, visited, depth + 1); + VerifyType(baseType, $"{source} base type", violations, declaringMember, visited, ref visitedCount, + depth + 1); } foreach (Type implementedInterface in type.GetInterfaces().OrderBy(static candidate => candidate.FullName, - StringComparer.Ordinal)) + StringComparer.Ordinal)) { - VerifyType(implementedInterface, $"{source} interface", violations, declaringMember, visited, depth + 1); + if (IsSafeFrameworkDtoImplementationContract(implementedInterface)) + { + continue; + } + + VerifyType(implementedInterface, $"{source} interface", violations, declaringMember, visited, + ref visitedCount, depth + 1); } } @@ -339,42 +566,65 @@ private static bool IsRequiredPluginBase(Type type) return type.FullName == "CheatEngine.SDK.Hosting.Plugin.CheatEnginePlugin"; } + private static bool IsSafeFrameworkDtoImplementationContract(Type type) + { + Type definition = type.IsGenericType ? type.GetGenericTypeDefinition() : type; + return definition.FullName is "System.IAsyncDisposable" or "System.IComparable" or "System.IComparable`1" or + "System.IConvertible" or "System.IDisposable" or "System.IEquatable`1" or "System.IFormattable" or + "System.ISpanFormattable" or "System.IUtf8SpanFormattable"; + } + private static void VerifyTypeMembers(Type type, string? source, List violations, HashSet visited, - int depth) + ref int visitedCount, int depth) { const BindingFlags DeclaredInstance = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | - BindingFlags.DeclaredOnly; + BindingFlags.DeclaredOnly; foreach (FieldInfo field in type.GetFields(DeclaredInstance).OrderBy(static candidate => candidate.Name, - StringComparer.Ordinal)) + StringComparer.Ordinal)) { - VerifyType(field.FieldType, $"{source} field '{field.Name}'", violations, field, visited, depth + 1); + VerifyType(field.FieldType, $"{source} field '{field.Name}'", violations, field, visited, + ref visitedCount, depth + 1); } - foreach (PropertyInfo property in type.GetProperties(DeclaredInstance).OrderBy(static candidate => candidate.Name, - StringComparer.Ordinal)) + foreach (PropertyInfo property in type.GetProperties(DeclaredInstance).OrderBy( + static candidate => candidate.Name, + StringComparer.Ordinal)) { VerifyType(property.PropertyType, $"{source} property '{property.Name}'", violations, property, visited, - depth + 1); - VerifyParameters(property.GetIndexParameters(), property, violations, visited, depth); + ref visitedCount, depth + 1); + VerifyParameters(property.GetIndexParameters(), property, violations, visited, ref visitedCount, depth); } foreach (ConstructorInfo constructor in type.GetConstructors(DeclaredInstance).OrderBy(static candidate => - candidate.ToString(), StringComparer.Ordinal)) + candidate.ToString(), StringComparer.Ordinal)) { - VerifyParameters(constructor.GetParameters(), constructor, violations, visited, depth); + VerifyParameters(constructor.GetParameters(), constructor, violations, visited, ref visitedCount, depth); } foreach (MethodInfo method in type.GetMethods(DeclaredInstance) - .Where(static candidate => !candidate.IsPrivate) - .OrderBy(static candidate => candidate.ToString(), StringComparer.Ordinal)) + .Where(static candidate => !candidate.IsPrivate) + .OrderBy(static candidate => candidate.ToString(), StringComparer.Ordinal)) { - VerifyType(method.ReturnType, $"{source} method '{method.Name}'", violations, method, visited, depth + 1); - VerifyParameters(method.GetParameters(), method, violations, visited, depth); - VerifyGenericParameterConstraints(method.GetGenericArguments(), method.ToString(), violations, visited, depth); + if (IsObjectEqualityMethod(method)) + { + continue; + } + + VerifyType(method.ReturnType, $"{source} method '{method.Name}'", violations, method, visited, + ref visitedCount, depth + 1); + VerifyParameters(method.GetParameters(), method, violations, visited, ref visitedCount, depth); + VerifyGenericParameterConstraints(method.GetGenericArguments(), method.ToString(), violations, visited, + ref visitedCount, depth); } } + private static bool IsObjectEqualityMethod(MethodInfo method) + { + return method.Name == nameof(object.Equals) && !method.IsStatic && method.ReturnType == typeof(bool) && + method.GetParameters() is [ParameterInfo { ParameterType: var type }] && type == typeof(object); + } + private static bool IsShippedRuntimeCapabilitiesDebt(Type type, MemberInfo? declaringMember) { // PublicAPI.Shipped preserves this one legacy class reference. It is deliberately a member-level exception: @@ -383,8 +633,8 @@ private static bool IsShippedRuntimeCapabilitiesDebt(Type type, MemberInfo? decl declaringMember?.DeclaringType?.FullName == "CheatEngine.Client.Runtime.CheatEngineRuntimeSnapshot" && declaringMember switch { - FieldInfo { Name: "k__BackingField" } => true, - ConstructorInfo => true, + FieldInfo { Name: "k__BackingField" } => true, + ConstructorInfo => true, MethodInfo { Name: "get_SdkCapabilities" } => true, PropertyInfo { Name: "SdkCapabilities" } => true, _ => false @@ -408,7 +658,7 @@ private static void AssertViolation(Type type, string expectedFragment) private interface IUnsafeConstraint { - LuaRef Reference + public LuaRef Reference { get; } diff --git a/tests/CheatEngine.Client.Tests/README.md b/tests/CheatEngine.Client.Tests/README.md index 781ea62..3dd13d5 100644 --- a/tests/CheatEngine.Client.Tests/README.md +++ b/tests/CheatEngine.Client.Tests/README.md @@ -16,8 +16,10 @@ The smoke tests compile against the assembled consumer graph and inspect selecte handle types. They catch accidental dependency omissions, namespace regressions, and public leakage of `LuaState`, `LuaRef`, `CEObject`, `Owned`, `MemScan`, or `FoundList` before package smoke tests run. -The suite is activation-independent. It does not replace Core lifecycle tests, template/package smoke tests, or the -opt-in live validation required for host-dependent capabilities such as value scans. +The suite is activation-independent. `PackageConsumptionSmokeTests` consumes the exact package directory supplied +through `CHEATENGINE_CLIENT_PACKAGE_SOURCE`, validates the template package, and builds isolated consumers. It does +not replace Core lifecycle tests or the opt-in live validation required for host-dependent capabilities such as value +scans. ## Run